aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--assistant.py40
1 files changed, 28 insertions, 12 deletions
diff --git a/assistant.py b/assistant.py
index 4107092..9004243 100644
--- a/assistant.py
+++ b/assistant.py
@@ -522,6 +522,9 @@ def interrupt():
turn += 1
if writing["on"]: # a write has no pending turn to close
writing["on"] = False
+ # Whatever ended it (stop, an ask, a new conversation): the canvas
+ # waits for an outcome, so it gets one.
+ emit({"type": "error", "message": "stopped", "turn": True})
emit({"type": "state", "state": "idle"})
conn = inflight["conn"]
if conn is not None and conn.sock is not None:
@@ -566,7 +569,8 @@ def handle(raw):
and "/" not in cmd["name"])
else None)
if bad:
- emit({"type": "error", "message": bad})
+ # A refused write ends the canvas's wait for it, so it carries turn.
+ emit({"type": "error", "message": bad, **({"turn": True} if kind == "write" else {})})
return
try:
with lock:
@@ -693,7 +697,7 @@ def llm_model():
return model_inputs()[0]
-def ask(messages, calls, tools=None, mine=None, think=None):
+def ask(messages, calls, tools=None, mine=None, think=None, meter=True):
# Yields ("think", text) and ("token", text) as they stream; tool-call
# fragments go into calls. mine is the reply's turn: a stop shuts the
# connection from the other thread (interrupt), which ends the reading.
@@ -715,7 +719,7 @@ def ask(messages, calls, tools=None, mine=None, think=None):
return # stopped before the request went out
inflight["conn"] = conn
try:
- yield from stream(conn, u.path, body, calls)
+ yield from stream(conn, u.path, body, calls, meter)
except OSError:
if mine is not None and turn != mine:
return # the stop hung up under the read
@@ -727,7 +731,7 @@ def ask(messages, calls, tools=None, mine=None, think=None):
conn.close()
-def stream(conn, path, body, calls):
+def stream(conn, path, body, calls, meter=True):
conn.request("POST", path, json.dumps(body).encode(), {"Content-Type": "application/json"})
r = conn.getresponse()
if r.status >= 400:
@@ -748,7 +752,7 @@ def stream(conn, path, body, calls):
if "error" in chunk: # a failure mid-stream arrives as a chunk
err = chunk["error"]
raise RuntimeError(err.get("message", str(err)) if isinstance(err, dict) else str(err))
- if chunk.get("usage"):
+ if chunk.get("usage") and meter:
# How full the model's context is: history, tool results and
# tool definitions all count.
ctx["used"] = chunk["usage"]["prompt_tokens"]
@@ -801,7 +805,8 @@ def unfence(text):
# Models often wrap a whole document in one code fence; the file wants
# what is inside it.
lines = text.strip().split("\n")
- if len(lines) >= 2 and lines[0].startswith("```") and lines[-1].strip() == "```":
+ if (len(lines) >= 2 and lines[0].startswith("```") and lines[-1].strip() == "```"
+ and not any(l.startswith("```") for l in lines[1:-1])): # one fence, not two blocks
return "\n".join(lines[1:-1]) + "\n"
return text
@@ -811,8 +816,8 @@ def write(mine, files, prompt, d, name):
# no history in, nothing kept in the conversation, no thinking, no tools.
tmp = None
try:
- free = (ctx["max"] - ctx["used"]) // 2 * 3 if ctx["max"] else 24000
- blocks, refs, _ = attach(files, max(free, 2000))
+ # No history goes with a write, so half the whole context is its share.
+ blocks, refs, _ = attach(files, ctx["max"] // 2 * 3 if ctx["max"] else 24000)
model, inputs = model_inputs()
images = inputs is None or "image" in inputs
if refs and not images:
@@ -825,7 +830,8 @@ def write(mine, files, prompt, d, name):
body = "\n\n".join(blocks + [prompt])
msgs = [{"role": "user", "content": [{"type": "text", "text": body}] + refs if refs else body}]
text, shown = "", 0.0
- for kind, piece in ask(wire(msgs, images), {}, None, mine, think=False):
+ # meter=False: the chat's context meter is not about this request.
+ for kind, piece in ask(wire(msgs, images), {}, None, mine, think=False, meter=False):
if turn != mine:
return
if kind == "token":
@@ -835,9 +841,11 @@ def write(mine, files, prompt, d, name):
emit({"type": "progress", "chars": len(text)})
if turn != mine: # stopped: the hang-up ended the stream
return
+ if not text.strip():
+ raise Refused("the model gave an empty reply; nothing written")
os.makedirs(d, exist_ok=True)
tmp = os.path.join(d, f".write-{os.getpid()}-{mine}.tmp")
- with open(tmp, "w") as f:
+ with open(tmp, "w", encoding="utf-8") as f:
f.write(unfence(text))
with lock:
if turn != mine:
@@ -1439,7 +1447,9 @@ def test():
send(type="write", files=[pdf], prompt="As Markdown.", dir=fd, name="held.md")
until("state")
send(type="stop")
- assert until("state")[-1]["state"] == "idle"
+ seen = until("state")
+ assert seen[-1]["state"] == "idle"
+ assert {"type": "error", "message": "stopped", "turn": True} in seen # an outcome
time.sleep(0.3)
assert not os.path.exists(os.path.join(fd, "held.md"))
assert not [n for n in os.listdir(fd) if n.startswith(".write-")]
@@ -1447,7 +1457,13 @@ def test():
for bad in ({"dir": "relative", "name": "x.md"}, {"dir": fd, "name": "a/b.md"},
{"dir": fd, "name": ""}):
send(type="write", files=[pdf], prompt="p", **bad)
- assert until("error")[-1]["message"] == "bad write", bad
+ e = until("error")[-1]
+ assert e["message"] == "bad write" and e["turn"] is True, bad
+ assert unfence("```\na\n```\nmid\n```\nb\n```") == "```\na\n```\nmid\n```\nb\n```"
+ script["replies"] = [[{"content": " "}]]
+ send(type="write", files=[pdf], prompt="p", dir=fd, name="empty.md")
+ assert "empty reply" in until("error")[-1]["message"]
+ assert not os.path.exists(os.path.join(fd, "empty.md"))
# llama-server down: an error, not a dead thread.
CFG["llm"] = "http://127.0.0.1:9/v1"
send(type="ask", text="anyone?")