Skip to content

Commit

Permalink
Merge pull request #10 from jepler/gpt4
Browse files Browse the repository at this point in the history
Support backend parameters via `-B name:value`
  • Loading branch information
jepler authored Sep 23, 2023
2 parents 1c597cd + e539a85 commit 62a4a69
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 153 deletions.
64 changes: 1 addition & 63 deletions src/chap/__main__.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,8 @@
# SPDX-FileCopyrightText: 2023 Jeff Epler <[email protected]>
#
# SPDX-License-Identifier: MIT
# pylint: disable=import-outside-toplevel

import importlib
import pathlib
import pkgutil
import subprocess

import click

from . import commands # pylint: disable=no-name-in-module


def version_callback(ctx, param, value) -> None: # pylint: disable=unused-argument
if not value or ctx.resilient_parsing:
return

git_dir = pathlib.Path(__file__).parent.parent.parent / ".git"
if git_dir.exists():
version = subprocess.check_output(
["git", f"--git-dir={git_dir}", "describe", "--tags", "--dirty"],
encoding="utf-8",
)
else:
try:
from .__version__ import __version__ as version
except ImportError:
version = "unknown"
prog_name = ctx.find_root().info_name
click.utils.echo(
f"{prog_name}, version {version}",
color=ctx.color,
)
ctx.exit()


class MyCLI(click.MultiCommand):
def list_commands(self, ctx):
rv = []
for pi in pkgutil.walk_packages(commands.__path__):
name = pi.name
if not name.startswith("__"):
rv.append(name)
rv.sort()
return rv

def get_command(self, ctx, cmd_name):
try:
return importlib.import_module("." + cmd_name, commands.__name__).main
except ModuleNotFoundError as exc:
raise click.UsageError(f"Invalid subcommand {cmd_name!r}", ctx) from exc


main = MyCLI(
help="Commandline interface to ChatGPT",
params=[
click.Option(
("--version",),
is_flag=True,
is_eager=True,
help="Show the version and exit",
callback=version_callback,
)
],
)
from .core import main

if __name__ == "__main__":
main()
19 changes: 17 additions & 2 deletions src/chap/backends/lorem.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import asyncio
import random
from dataclasses import dataclass

from lorem_text import lorem

Expand All @@ -18,6 +19,16 @@ def ipartition(s, sep=" "):


class Lorem:
@dataclass
class Parameters:
delay_mu: float = 0.035
delay_sigma: float = 0.02
paragraph_lo: int = 1
paragraph_hi: int = 5

def __init__(self):
self.parameters = self.Parameters()

system_message = (
"(It doesn't matter what you ask, this backend will respond with lorem)"
)
Expand All @@ -26,12 +37,16 @@ async def aask(self, session, query, *, max_query_size=5, timeout=60):
data = self.ask(session, query, max_query_size=max_query_size, timeout=timeout)
for word, opt_sep in ipartition(data):
yield word + opt_sep
await asyncio.sleep(random.uniform(0.02, 0.05))
await asyncio.sleep(
random.gauss(self.parameters.delay_mu, self.parameters.delay_sigma)
)

def ask(
self, session, query, *, max_query_size=5, timeout=60
): # pylint: disable=unused-argument
new_content = lorem.paragraphs(3).replace("\n", "\n\n")
new_content = lorem.paragraphs(
random.randint(self.parameters.paragraph_lo, self.parameters.paragraph_hi)
).replace("\n", "\n\n")
session.session.extend([User(query), Assistant("".join(new_content))])
return new_content

Expand Down
13 changes: 8 additions & 5 deletions src/chap/backends/openai_chatgpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# SPDX-License-Identifier: MIT

import json
from dataclasses import dataclass

import httpx

Expand All @@ -11,19 +12,22 @@


class ChatGPT:
@dataclass
class Parameters:
model: str = "gpt-3.5-turbo"

def __init__(self):
pass
self.parameters = self.Parameters()

system_message: str = "You're named Chap (they/them). Chap strives to answer truthfully and frankly. You have opinions, preferences, and emotions, you can say when you don't know, and you prefer to speak in plain language."

def ask(self, session, query, *, max_query_size=5, timeout=60):
full_prompt = Session(session.session + [User(query)])
del full_prompt.session[1:-max_query_size]

response = httpx.post(
"https://api.openai.com/v1/chat/completions",
json={
"model": "gpt-3.5-turbo",
"model": self.parameters.model,
"messages": full_prompt.to_dict()[ # pylint: disable=no-member
"session"
],
Expand All @@ -33,7 +37,6 @@ def ask(self, session, query, *, max_query_size=5, timeout=60):
},
timeout=timeout,
)

if response.status_code != 200:
print("Failure", response.status_code, response.text)
return None
Expand All @@ -60,7 +63,7 @@ async def aask(self, session, query, *, max_query_size=5, timeout=60):
"https://api.openai.com/v1/chat/completions",
headers={"authorization": f"Bearer {self.get_key()}"},
json={
"model": "gpt-3.5-turbo",
"model": self.parameters.model,
"stream": True,
"messages": full_prompt.to_dict()[ # pylint: disable=no-member
"session"
Expand Down
32 changes: 6 additions & 26 deletions src/chap/commands/ask.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import click
import rich

from ..core import get_api, last_session_path, new_session_path
from ..session import Session
from ..core import uses_new_session

if sys.stdout.isatty():
bold = "\033[1m"
Expand Down Expand Up @@ -80,32 +79,13 @@ async def work():


@click.command
@click.option("--continue-session", "-s", type=click.Path(exists=True), default=None)
@click.option("--last", is_flag=True)
@click.option("--new-session", "-n", type=click.Path(exists=False), default=None)
@click.option("--system-message", "-S", type=str, default=None)
@click.option("--backend", "-b", type=str, default="openai_chatgpt")
@uses_new_session
@click.argument("prompt", nargs=-1, required=True)
def main(
continue_session, last, new_session, system_message, prompt, backend
): # pylint: disable=too-many-arguments
def main(obj, prompt):
"""Ask a question (command-line argument is passed as prompt)"""
if bool(continue_session) + bool(last) + bool(new_session) > 1:
raise SystemExit(
"--continue-session, --last and --new_session are mutually exclusive"
)

api = get_api(backend)

if last:
continue_session = last_session_path()
if continue_session:
session_filename = continue_session
with open(session_filename, "r", encoding="utf-8") as f:
session = Session.from_json(f.read()) # pylint: disable=no-member
else:
session_filename = new_session_path(new_session)
session = Session.new_session(system_message or api.system_message)
session = obj.session
session_filename = obj.session_filename
api = obj.api

# symlink_session_filename(session_filename)

Expand Down
32 changes: 19 additions & 13 deletions src/chap/commands/cat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,35 @@

import click

from ..core import last_session_path
from ..session import Session
from ..core import uses_existing_session


@click.command
@click.option("--session", "-s", type=click.Path(exists=True), default=None)
@click.option("--last", is_flag=True)
def main(session, last):
@uses_existing_session
@click.option("--no-system", is_flag=True)
def main(obj, no_system):
"""Print session in plaintext"""
if bool(session) + bool(last) != 1:
raise SystemExit("Specify either --session, or --last")

if last:
session = last_session_path()
with open(session, "r", encoding="utf-8") as f:
session = Session.from_json(f.read()) # pylint: disable=no-member
session = obj.session

first = True
for row in session.session:
if not first:
print()
first = False
print(row.content)
if row.role == "user":
decoration = "**"
elif row.role == "system":
if no_system:
continue
decoration = "_"
else:
decoration = ""

content = str(row.content).strip()
if "\n" in content:
print(content)
else:
print(f"{decoration}{content}{decoration}")


if __name__ == "__main__":
Expand Down
22 changes: 14 additions & 8 deletions src/chap/commands/grep.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ def list_files_matching_rx(
with open(conversation, "r", encoding="utf-8") as f:
session = Session.from_json(f.read()) # pylint: disable=no-member
for message in session.session:
if rx.search(message.content):
if isinstance(message.content, str) and rx.search(message.content):
yield conversation, message


@click.command
@click.option("--ignore-case", "-i", is_flag=True)
@click.option("--files-with-matches", "-l", is_flag=True)
@click.option("--fixed-strings", "--literal", "-F", is_flag=True)
@click.argument("pattern", nargs=1, required=True)
def main(ignore_case, fixed_strings, pattern):
def main(ignore_case, files_with_matches, fixed_strings, pattern):
"""Search sessions for pattern"""
console = rich.get_console()
if fixed_strings:
Expand All @@ -43,14 +44,19 @@ def main(ignore_case, fixed_strings, pattern):
last_file = None
for f, m in list_files_matching_rx(rx, ignore_case):
if f != last_file:
if last_file:
print()
console.print(f"[bold]{f}[nobold]:")
if files_with_matches:
print(f)
else:
if last_file:
print()
console.print(f"[bold]{f}[nobold]:")
last_file = f
else:
console.print("[dim]---[nodim]")
m.content, _ = rx.subn(lambda p: f"**{p.group(0)}**", m.content)
console.print(to_markdown(m))
if not files_with_matches:
console.print("[dim]---[nodim]")
if not files_with_matches:
m.content, _ = rx.subn(lambda p: f"**{p.group(0)}**", m.content)
console.print(to_markdown(m))


if __name__ == "__main__":
Expand Down
21 changes: 8 additions & 13 deletions src/chap/commands/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from markdown_it import MarkdownIt
from rich.markdown import Markdown

from ..core import last_session_path
from ..session import Session
from ..core import uses_existing_session


def to_markdown(message):
Expand All @@ -22,29 +21,25 @@ def to_markdown(message):
m = Markdown("", style=style)
parser = MarkdownIt()
parser.options["html"] = False
m.parsed = parser.parse(message.content.strip())
m.parsed = parser.parse(str(message.content).strip())
return m


@click.command
@click.option("--session", "-s", type=click.Path(exists=True), default=None)
@click.option("--last", is_flag=True)
def main(session, last):
@uses_existing_session
@click.option("--no-system", is_flag=True)
def main(obj, no_system):
"""Print session with formatting"""
if bool(session) + bool(last) != 1:
raise SystemExit("Specify either --session, or --last")

if last:
session = last_session_path()
with open(session, "r", encoding="utf-8") as f:
session = Session.from_json(f.read()) # pylint: disable=no-member
session = obj.session

console = rich.get_console()
first = True
for row in session.session:
if not first:
console.print()
first = False
if no_system and row.role == "system":
continue
console.print(to_markdown(row))


Expand Down
Loading

0 comments on commit 62a4a69

Please sign in to comment.