Skip to content

Commit

Permalink
feat: added ability to send messages and generate replies in web UI, …
Browse files Browse the repository at this point in the history
…refactored commands
  • Loading branch information
ErikBjare committed Oct 16, 2023
1 parent b3053b2 commit 4e9ff47
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 172 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ typecheck:
poetry run mypy --ignore-missing-imports gptme tests scripts

lint:
poetry run ruff -v gptme tests scripts
poetry run ruff gptme tests scripts

format:
poetry run black gptme tests
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ gptme

## 🌐 Web UI

NOTE: The web UI is very early in development, but can be used to browse conversations with pretty formatting.
> [!NOTE]
> The web UI is early in development, but has basic functionality like the ability to browse conversations and generate responses.
To serve the web UI, you need to install gptme with server extras:
```sh
Expand Down Expand Up @@ -107,6 +108,7 @@ MODEL=~/ML/wizardcoder-python-13b-v1.0.Q4_K_M.gguf
poetry run python -m llama_cpp.server --model $MODEL --n_gpu_layers 1 # Use `--n_gpu_layer 1` if you have a M1/M2 chip

# Now, to use it:
export OPENAI_API_BASE="http://localhost:8000/v1"
gptme --llm llama
```

Expand Down
163 changes: 3 additions & 160 deletions gptme/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import sys
from datetime import datetime
from pathlib import Path
from time import sleep
from typing import Generator, Literal

import click
Expand All @@ -39,27 +38,14 @@
from rich import print # noqa: F401
from rich.console import Console

from .commands import CMDFIX, action_descriptions
from .commands import CMDFIX, action_descriptions, execute_cmd
from .constants import HISTORY_FILE, LOGSDIR, PROMPT_USER
from .llm import init_llm, reply
from .logmanager import LogManager, _conversations
from .message import (
Message,
msgs_to_toml,
print_msg,
toml_to_msgs,
)
from .message import Message
from .prompts import initial_prompt_single_message
from .tabcomplete import register_tabcomplete
from .tools import (
execute_msg,
execute_python,
execute_shell,
init_tools,
)
from .tools.shell import get_shell
from .tools.summarize import summarize
from .tools.useredit import edit_text_with_editor
from .tools import execute_msg, init_tools
from .util import epoch_to_age, generate_unique_name

logger = logging.getLogger(__name__)
Expand All @@ -69,119 +55,6 @@
ModelChoice = Literal["gpt-3.5-turbo", "gpt4"]


def handle_cmd(
cmd: str, log: LogManager, no_confirm: bool
) -> Generator[Message, None, None]:
"""Handles a command."""
cmd = cmd.lstrip(CMDFIX)
logger.debug(f"Executing command: {cmd}")
name, *args = cmd.split(" ")
match name:
case "bash" | "sh" | "shell":
yield from execute_shell(" ".join(args), ask=not no_confirm)
case "python" | "py":
yield from execute_python(" ".join(args), ask=not no_confirm)
case "continue":
# undo '/continue' command
log.undo(1, quiet=True)
case "log":
log.undo(1, quiet=True)
log.print(show_hidden="--hidden" in args)
case "rename":
# rename the conversation
new_name = args[0] if args else input("New name: ")
log.rename(new_name)
case "fork":
# fork the conversation
new_name = args[0] if args else input("New name: ")
log.fork(new_name)
case "summarize":
msgs = log.prepare_messages()
msgs = [m for m in msgs if not m.hide]
summary = summarize(msgs)
print(f"Summary: {summary}")
case "edit":
# edit previous messages

# first undo the '/edit' command itself
assert log.log[-1].content == f"{CMDFIX}edit"
log.undo(1, quiet=True)

# generate editable toml of all messages
t = msgs_to_toml(reversed(log.log)) # type: ignore
res = None
while not res:
t = edit_text_with_editor(t, "toml")
try:
res = toml_to_msgs(t)
except Exception as e:
print(f"\nFailed to parse TOML: {e}")
try:
sleep(1)
except KeyboardInterrupt:
yield Message("system", "Interrupted")
return
log.log = list(reversed(res))
log.write()
# now we need to redraw the log so the user isn't seeing stale messages in their buffer
# log.print()
logger.info("Applied edited messages")
case "context":
# print context msg
print(_gen_context_msg())
case "undo":
# undo the '/undo' command itself
log.undo(1, quiet=True)
# if int, undo n messages
n = int(args[0]) if args and args[0].isdigit() else 1
log.undo(n)
case "load":
filename = args[0] if args else input("Filename: ")
with open(filename) as f:
contents = f.read()
yield Message("system", f"# filename: {filename}\n\n{contents}")
case "save":
# undo
log.undo(1, quiet=True)

# save the most recent code block to a file
code = log.get_last_code_block()
if not code:
print("No code block found")
return
filename = args[0] if args else input("Filename: ")
if Path(filename).exists():
ans = input("File already exists, overwrite? [y/N] ")
if ans.lower() != "y":
return
with open(filename, "w") as f:
f.write(code)
print(f"Saved code block to {filename}")
case "exit":
sys.exit(0)
case "replay":
log.undo(1, quiet=True)
print("Replaying conversation...")
for msg in log.log:
if msg.role == "assistant":
for msg in execute_msg(msg, ask=True):
print_msg(msg, oneline=False)
case "impersonate":
content = " ".join(args) if args else input("[impersonate] Assistant: ")
msg = Message("assistant", content)
yield msg
yield from execute_msg(msg, ask=not no_confirm)
case _:
if log.log[-1].content != f"{CMDFIX}help":
print("Unknown command")
# undo the '/help' command itself
log.undo(1, quiet=True)

print("Available commands:")
for cmd, desc in action_descriptions.items():
print(f" {cmd}: {desc}")


script_path = Path(os.path.realpath(__file__))
action_readme = "\n".join(
f" {CMDFIX}{cmd:10s} {desc}." for cmd, desc in action_descriptions.items()
Expand Down Expand Up @@ -372,19 +245,6 @@ def start_server():
server_main()


def execute_cmd(msg, log):
"""Executes any user-command, returns True if command was executed."""
assert msg.role == "user"

# if message starts with ., treat as command
# when command has been run,
if msg.content[:1] in ["/"]:
for resp in handle_cmd(msg.content, log, no_confirm=True):
log.append(resp)
return True
return False


def loop(
log: LogManager,
no_confirm: bool,
Expand Down Expand Up @@ -463,23 +323,6 @@ def get_name(name: str) -> Path:
return logpath


def _gen_context_msg() -> Message:
shell = get_shell()
msgstr = ""

cmd = "pwd"
ret, pwd, _ = shell.run_command(cmd)
assert ret == 0
msgstr += f"$ {cmd}\n{pwd.strip()}\n"

cmd = "git status -s"
ret, git, _ = shell.run_command(cmd)
if ret == 0 and git:
msgstr += f"$ {cmd}\n{git}\n"

return Message("system", msgstr.strip(), hide=True)


# default history if none found
history_examples = [
"What is love?",
Expand Down
147 changes: 145 additions & 2 deletions gptme/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
from typing import Literal
import logging
import sys
from pathlib import Path
from time import sleep
from typing import Generator, Literal

CMDFIX = "/" # prefix for commands, e.g. /help
from .logmanager import LogManager
from .message import (
Message,
msgs_to_toml,
print_msg,
toml_to_msgs,
)
from .tools import execute_msg, execute_python, execute_shell
from .tools.context import _gen_context_msg
from .tools.summarize import summarize
from .tools.useredit import edit_text_with_editor
from .constants import CMDFIX

logger = logging.getLogger(__name__)

Actions = Literal[
"continue",
Expand Down Expand Up @@ -40,3 +57,129 @@
"exit": "Exit the program",
}
COMMANDS = list(action_descriptions.keys())


def execute_cmd(msg, log):
"""Executes any user-command, returns True if command was executed."""
assert msg.role == "user"

# if message starts with ., treat as command
# when command has been run,
if msg.content[:1] in ["/"]:
for resp in handle_cmd(msg.content, log, no_confirm=True):
log.append(resp)
return True
return False


def handle_cmd(
cmd: str, log: LogManager, no_confirm: bool
) -> Generator[Message, None, None]:
"""Handles a command."""
cmd = cmd.lstrip(CMDFIX)
logger.debug(f"Executing command: {cmd}")
name, *args = cmd.split(" ")
match name:
case "bash" | "sh" | "shell":
yield from execute_shell(" ".join(args), ask=not no_confirm)
case "python" | "py":
yield from execute_python(" ".join(args), ask=not no_confirm)
case "continue":
# undo '/continue' command
log.undo(1, quiet=True)
case "log":
log.undo(1, quiet=True)
log.print(show_hidden="--hidden" in args)
case "rename":
# rename the conversation
new_name = args[0] if args else input("New name: ")
log.rename(new_name)
case "fork":
# fork the conversation
new_name = args[0] if args else input("New name: ")
log.fork(new_name)
case "summarize":
msgs = log.prepare_messages()
msgs = [m for m in msgs if not m.hide]
summary = summarize(msgs)
print(f"Summary: {summary}")
case "edit":
# edit previous messages

# first undo the '/edit' command itself
assert log.log[-1].content == f"{CMDFIX}edit"
log.undo(1, quiet=True)

# generate editable toml of all messages
t = msgs_to_toml(reversed(log.log)) # type: ignore
res = None
while not res:
t = edit_text_with_editor(t, "toml")
try:
res = toml_to_msgs(t)
except Exception as e:
print(f"\nFailed to parse TOML: {e}")
try:
sleep(1)
except KeyboardInterrupt:
yield Message("system", "Interrupted")
return
log.log = list(reversed(res))
log.write()
# now we need to redraw the log so the user isn't seeing stale messages in their buffer
# log.print()
logger.info("Applied edited messages")
case "context":
# print context msg
print(_gen_context_msg())
case "undo":
# undo the '/undo' command itself
log.undo(1, quiet=True)
# if int, undo n messages
n = int(args[0]) if args and args[0].isdigit() else 1
log.undo(n)
case "load":
filename = args[0] if args else input("Filename: ")
with open(filename) as f:
contents = f.read()
yield Message("system", f"# filename: {filename}\n\n{contents}")
case "save":
# undo
log.undo(1, quiet=True)

# save the most recent code block to a file
code = log.get_last_code_block()
if not code:
print("No code block found")
return
filename = args[0] if args else input("Filename: ")
if Path(filename).exists():
ans = input("File already exists, overwrite? [y/N] ")
if ans.lower() != "y":
return
with open(filename, "w") as f:
f.write(code)
print(f"Saved code block to {filename}")
case "exit":
sys.exit(0)
case "replay":
log.undo(1, quiet=True)
print("Replaying conversation...")
for msg in log.log:
if msg.role == "assistant":
for msg in execute_msg(msg, ask=True):
print_msg(msg, oneline=False)
case "impersonate":
content = " ".join(args) if args else input("[impersonate] Assistant: ")
msg = Message("assistant", content)
yield msg
yield from execute_msg(msg, ask=not no_confirm)
case _:
if log.log[-1].content != f"{CMDFIX}help":
print("Unknown command")
# undo the '/help' command itself
log.undo(1, quiet=True)

print("Available commands:")
for cmd, desc in action_descriptions.items():
print(f" {cmd}: {desc}")
2 changes: 2 additions & 0 deletions gptme/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from pathlib import Path

CMDFIX = "/" # prefix for commands, e.g. /help

# Prompts
ROLE_COLOR = {
"user": "bright_green",
Expand Down
Loading

0 comments on commit 4e9ff47

Please sign in to comment.