aboutsummaryrefslogtreecommitdiffstats
path: root/assistant.py
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-23 09:22:19 +0200
committerDanilo M. <danix@danix.xyz>2026-10-23 09:22:19 +0200
commit75d9eb77a7e249f7ff8286c681dc2ec4380342be (patch)
treec0dfbf0205cb39d40850446c9a577f32ff8cef33 /assistant.py
parentb3ec33b9a0f851792f98eda8b209d9cf9798d46a (diff)
downloaddesktop-assistant-75d9eb77a7e249f7ff8286c681dc2ec4380342be.tar.gz
desktop-assistant-75d9eb77a7e249f7ff8286c681dc2ec4380342be.zip
titles: let the model name a conversation after its first reply
A new conversation is still saved under the first six words of the question, then the model is asked for a title of at most six words in the conversation's language, thinking off, and the row is renamed. The request runs after done under the reply's turn, through ask(), so the next ask, stop, new or open hangs it up through interrupt(): a title never holds the next reply back, and a dropped one leaves the six words. A failure is logged only, since the reply is already saved. The self-check's fake llama-server answers title requests apart from the scripted replies, so a title can never take a reply meant for an ask in the test that follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'assistant.py')
-rw-r--r--assistant.py44
1 files changed, 43 insertions, 1 deletions
diff --git a/assistant.py b/assistant.py
index f246101..cf92c16 100644
--- a/assistant.py
+++ b/assistant.py
@@ -244,6 +244,8 @@ PAGE_PX = 1600 # longest side of a page render
MAX_AUDIO_S = 60
HEAR = ("Transcribe this audio exactly, in the language spoken (Italian or English). "
"Output only the transcript.")
+TITLE = ("Write a title of at most six words for this conversation, in the language "
+ "it is in. Output only the title.")
class Refused(Exception):
@@ -1027,13 +1029,38 @@ def answer(mine, text, files=()):
if turn != mine:
log("interrupted")
return
+ fresh = cur["id"] is None
cur["id"] = save_turn(DB, cur["id"], text, said, used, entry, chips)
+ conv = cur["id"]
cur["history"].append(entry)
cur["turns"].append({"user": text, "reply": said, "tools": used, "files": chips})
cur["pending"] = None
emit({"type": "done", "id": cur["id"]})
emit({"type": "state", "state": "idle"})
log("assistant:", said)
+ if fresh:
+ retitle(mine, conv, text, said)
+
+
+def retitle(mine, conv, user, said):
+ # The first six words stand until the model names the conversation. Asked
+ # after done, under the reply's turn: the next ask, stop, new or open
+ # hangs it up, so it never holds a reply back, and the six words stay.
+ try:
+ msgs = [{"role": "user",
+ "content": f"{TITLE}\n\nUser: {user[:2000]}\n\nAssistant: {said[:2000]}"}]
+ text = "".join(p for k, p in ask(msgs, {}, None, mine, think=False, meter=False)
+ if k == "token")
+ line = next((l for l in text.splitlines() if l.strip()), "")
+ title = line.strip().strip("\"'*#").strip()[:80]
+ with lock:
+ if turn != mine or not title:
+ return
+ with DB:
+ DB.execute("UPDATE conversations SET title = ? WHERE id = ?", (title, conv))
+ log(f"titled: {title}")
+ except Exception: # the reply is saved; a title is not worth an error
+ log("title failed:\n" + traceback.format_exc().rstrip())
def main():
@@ -1290,7 +1317,15 @@ def test():
self.wfile.write(json.dumps(body).encode())
def do_POST(self):
- script["body"] = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
+ body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
+ if str(body["messages"][0]["content"]).startswith(TITLE):
+ # Answered apart, so a title never takes a reply meant for an ask.
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(b"data: " + json.dumps({"choices": [{"delta": {
+ "content": script.get("title", "")}}]}).encode() + b"\n\ndata: [DONE]\n\n")
+ return
+ script["body"] = body
script.get("bodies", []).append(script["body"])
if script.get("hold"):
# A long prompt pass: nothing is sent, so only the client
@@ -1354,6 +1389,7 @@ def test():
{"tool_calls": [{"index": 0, "id": "c1", "function": {"name": "now",
"arguments": "{}"}}]}],
[{"content": "It is "}, {"content": "noon."}]]
+ script["title"] = '"Ora attuale"\n'
send(type="ask", text="what time is it")
got = [(e["type"], e.get("delta") or e.get("name") or e.get("state") or e.get("text"))
for e in until("done") if e["type"] != "ctx"]
@@ -1364,6 +1400,12 @@ def test():
assert until("state")[-1]["state"] == "idle"
assert script["body"]["chat_template_kwargs"]["enable_thinking"] is True
conv = cur["id"]
+ for _ in range(40): # the title comes after done
+ if list_convs(DB)[0]["title"] == "Ora attuale":
+ break
+ time.sleep(0.05)
+ assert list_convs(DB)[0]["title"] == "Ora attuale", list_convs(DB)
+ script["title"] = ""
assert load_conv(DB, conv)[1] == [{"user": "what time is it", "reply": "It is noon.",
"tools": ["now"], "files": []}]
# The tool result went to the model but history keeps a placeholder.