diff options
| -rw-r--r-- | assistant.py | 40 |
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?") |
