Skip to content

Commit

Permalink
feat: added ability to create new conversations in web UI, better err…
Browse files Browse the repository at this point in the history
…or output, capture command output, fixed fork, fixed logpath handling
  • Loading branch information
ErikBjare committed Oct 16, 2023
1 parent 9bd452b commit 3e88e76
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 15 deletions.
12 changes: 8 additions & 4 deletions gptme/logmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def load(
**kwargs,
) -> "LogManager":
"""Loads a conversation log."""
if not Path(logfile).exists():
if str(LOGSDIR) not in str(logfile):
# if the path was not fully specified, assume its a dir in LOGSDIR
logfile = LOGSDIR / logfile / "conversation.jsonl"
if not Path(logfile).exists():
Expand All @@ -135,13 +135,14 @@ def get_last_code_block(self) -> str | None:
def rename(self, name: str) -> None:
# rename the conversation and log file
# if you want to keep the old log, use fork()
self.logfile.rename(self.logfile.parent / f"{name}.log")
self.logfile = self.logfile.parent / f"{name}.log"
self.logfile.rename(LOGSDIR / name / "conversation.jsonl")
self.logfile = LOGSDIR / name / "conversation.jsonl"

def fork(self, name: str) -> None:
# save and switch to a new log file without renaming the old one
self.write()
self.logfile = self.logfile.parent / f"{name}.log"
self.logfile = LOGSDIR / name / "conversation.jsonl"
self.write()

def to_dict(self) -> dict:
return {
Expand All @@ -156,6 +157,9 @@ def write_log(msg_or_log: Message | list[Message], logfile: PathLike) -> None:
If a single message given, append.
If a list of messages given, overwrite.
"""
# create directory if it doesn't exist
Path(logfile).parent.mkdir(parents=True, exist_ok=True)

if isinstance(msg_or_log, Message):
msg = msg_or_log
with open(logfile, "a") as file:
Expand Down
41 changes: 37 additions & 4 deletions gptme/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
- https://matplotlib.org/stable/gallery/user_interfaces/web_application_server_sgskip.html
"""

import io
from contextlib import redirect_stdout

import flask

from .commands import execute_cmd
from .constants import LOGSDIR
from .llm import reply
from .logmanager import LogManager, get_conversations
from .message import Message
Expand All @@ -29,15 +33,37 @@ def api_conversations():

@api.route("/api/conversations/<path:logfile>")
def api_conversation(logfile: str):
"""Get a conversation."""
log = LogManager.load(logfile)
return flask.jsonify(log.to_dict())


@api.route("/api/conversations/<path:logfile>", methods=["PUT"])
def api_conversation_put(logfile: str):
"""Create or update a conversation."""
msgs = []
req_json = flask.request.json
if req_json and "messages" in req_json:
for msg in req_json["messages"]:
msgs.append(
Message(msg["role"], msg["content"], timestamp=msg["timestamp"])
)

logpath = LOGSDIR / logfile / "conversation.jsonl"
if logpath.exists():
raise ValueError(f"Conversation already exists: {logpath}")
logpath.parent.mkdir(parents=True)
log = LogManager(msgs, logfile=logpath)
log.write()
return {"status": "ok"}


@api.route(
"/api/conversations/<path:logfile>",
methods=["POST"],
)
def api_conversation_post(logfile: str):
"""Post a message to the conversation."""
log = LogManager.load(logfile)
req_json = flask.request.json
assert req_json
Expand All @@ -51,15 +77,23 @@ def api_conversation_post(logfile: str):
# generate response
@api.route("/api/conversations/<path:logfile>/generate", methods=["POST"])
def api_conversation_generate(logfile: str):
# Lots copied from cli.py
log = LogManager.load(logfile)

# if prompt is a user-command, execute it
if log[-1].role == "user":
resp = execute_cmd(log[-1], log)
# TODO: capture output of command and return it

f = io.StringIO()
print("Begin capturing stdout, to pass along command output.")
with redirect_stdout(f):
resp = execute_cmd(log[-1], log)
print("Done capturing stdout.")
if resp:
log.write()
return flask.jsonify({"response": resp})
output = f.getvalue()
return flask.jsonify(
[{"role": "system", "content": output, "stored": False}]
)

# performs reduction/context trimming, if necessary
msgs = log.prepare_messages()
Expand All @@ -83,7 +117,6 @@ def api_conversation_generate(logfile: str):
# serve the static assets in the static folder
@api.route("/static/<path:path>")
def static_proxy(path):
print("serving static", path)
return flask.send_from_directory("static", path)


Expand Down
59 changes: 52 additions & 7 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,24 @@
<body>

<div id="app">
<div v-if="error" class="rounded">
<h1>Error</h1>
<pre>{{ error }}</pre>
<div v-if="error" class="rounded container m-auto my-2 p-3 bg-red-100">
<b class="text-red-600">Error</b>
<button class="float-right text-red-400 font-bold" @click="error = null">X</button>
<pre class="text-red-800">{{ error }}</pre>
</div>

<div class="container m-auto p-3" v-if="selectedConversation === null">
<h1 class="text-3xl font-bold mb-4">gptme</h1>
<div class="border rounded p-2">
<h2 class="text-lg font-bold">Conversations</h1>
<table class="table-auto w-full">
<div class="flex">
<span class="my-1 text-2xl font-bold flex-1">Conversations</span>
<a class="rounded-lg border p-2 cursor-pointer hover:bg-gray-200 text-sm mb-2 bg-gray-100" @click="createConversation()">
<b class="text-xl m-0 p-0" style="line-height: 0">+</b>
<span>New conversation</span>
</a>
</div>
<hr>
<table class="mt-4 table-auto w-full">
<thead>
<tr>
<th class="text-left">Name</th>
Expand Down Expand Up @@ -73,6 +81,16 @@ <h1 class="text-lg font-bold">{{ selectedConversation }}</h1>
<div class="font-bold mb-1">{{ capitalize(message.role) }}</div>
<div class="text-sm" v-html="message.html"></div>
</div>
<div v-if="cmdout" class="chat-msg rounded border mb-4 p-2">
<div class="mb-1">
<span class="font-bold">System</span> (not stored)
<!-- clear button to the right -->
<button class="rounded text-sm border p-1 bg-white shadow float-right" @click="cmdout = ''">Clear</button>
</div>
<div>
<pre class="text-sm">{{cmdout}}</pre>
</div>
</div>
<!-- generate button -->
<button
class="rounded border p-2 bg-white shadow"
Expand Down Expand Up @@ -122,12 +140,12 @@ <h1 class="text-lg font-bold">{{ selectedConversation }}</h1>
newMessage: "",

// Status
cmdout: "",
error: "",
generating: false,
},
async mounted() {
const res = await fetch(apiRoot);
this.conversations = await res.json();
this.getConversations();
// if the hash is set, select that conversation
if (window.location.hash) {
this.selectConversation(window.location.hash.slice(1));
Expand All @@ -154,6 +172,10 @@ <h1 class="text-lg font-bold">{{ selectedConversation }}</h1>
},
},
methods: {
async getConversations() {
const res = await fetch(apiRoot);
this.conversations = await res.json();
},
async selectConversation(path) {
// set the hash to the conversation name
window.location.hash = path;
Expand Down Expand Up @@ -181,6 +203,21 @@ <h1 class="text-lg font-bold">{{ selectedConversation }}</h1>
this.scrollToBottom();
});
},
async createConversation() {
const name = prompt("Conversation name");
if (!name) return;
const res = await fetch(`${apiRoot}/${name}`, {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify([]),
});
if (!res.ok) {
this.error = res.statusText;
return;
}
await this.getConversations();
this.selectConversation(name);
},
async sendMessage() {
const payload = JSON.stringify({role: "user", content: this.newMessage});
const req = await fetch(`${apiRoot}/${this.selectedConversation}`, {
Expand Down Expand Up @@ -215,10 +252,18 @@ <h1 class="text-lg font-bold">{{ selectedConversation }}</h1>
this.error = req.statusText;
return;
}
// req.json() can contain (not stored) responses to /commands,
// or the result of the generation.
// if it's unsaved results of a command, we need to display it
const data = await req.json();
if (data.length == 1 && data[0].stored === false) {
this.cmdout = data[0].content;
}
// reload conversation
await this.selectConversation(this.selectedConversation);
},
backToConversations() {
this.getConversations(); // refresh conversations
this.selectedConversation = null;
this.chatLog = [];
window.location.hash = "";
Expand Down

0 comments on commit 3e88e76

Please sign in to comment.