"""Drive a fortnight of conversation through POST /api/chat and record it. The 2026-08-07 week of usage was typed by hand. This is the same reach and the same turn source, tap:text, so it exercises the path the mic and telegram take. The endpoint is a form POST that redirects to /chat with the reply in the query string. Reading the Location header is the whole protocol, so nothing here parses HTML. This exists to be re-run. The baseline is 2026-08-08 against master at beb093a, in docs/evals/2026-08-08-two-weeks.md. Re-running the same turns after a routing change is the comparison, so edit the turns file by adding, never by rewriting. python3 scripts/usage-run.py scripts/testdata/usage-turns.txt out-prefix Input is one utterance per line. A line starting with "# " opens a day. A blank line is ignored. Output is a markdown transcript and a jsonl log beside it. """ import json import sys import time import urllib.error import urllib.parse import urllib.request URL = "http://127.0.0.1:9201/api/chat" TIMEOUT = 90 class NoRedirect(urllib.request.HTTPRedirectHandler): """A 303 carries the reply. Following it would throw the reply away.""" def redirect_request(self, *a, **kw): return None # ProxyHandler({}) is not optional. This box exports http_proxy, urllib honours # it, and the proxy answers 503 for a loopback address. OPENER = urllib.request.build_opener(NoRedirect, urllib.request.ProxyHandler({})) def turn(text): body = urllib.parse.urlencode({"text": text}).encode() t0 = time.perf_counter() try: OPENER.open(urllib.request.Request(URL, data=body), timeout=TIMEOUT) return {"reply": "", "error": "no redirect", "secs": time.perf_counter() - t0} except urllib.error.HTTPError as e: dt = time.perf_counter() - t0 if e.code != 303: return {"reply": "", "error": f"HTTP {e.code}", "secs": dt} loc = e.headers.get("Location", "") q = urllib.parse.parse_qs(urllib.parse.urlparse(loc).query) return { "reply": q.get("r", [""])[0], "source": q.get("src", q.get("source", [""]))[0], "trace": q.get("t", [""])[0], "secs": dt, } except Exception as e: # a dead box must not lose the turns already done return {"reply": "", "error": str(e), "secs": time.perf_counter() - t0} def main(): lines = [l.rstrip("\n") for l in open(sys.argv[1])] prefix = sys.argv[2] md = open(prefix + "-transcript.md", "w") log = open(prefix + ".jsonl", "w") day = 0 n = 0 print(f"# Raw transcript, two weeks of usage\n", file=md) for line in lines: if not line.strip(): continue if line.startswith("# "): if day: print("```\n", file=md) day += 1 print(f"## {line[2:]}\n\n```", file=md) continue n += 1 r = turn(line) r["day"] = day r["n"] = n r["utterance"] = line log.write(json.dumps(r, ensure_ascii=False) + "\n") log.flush() reply = r.get("error") or r["reply"] print(f"YOU: {line}", file=md) print(f"MAVEN: {reply}", file=md) tag = f"[{r['secs']:.1f}s" if r.get("source"): tag += f" src={r['source']}" print(f" {tag} t={r.get('trace', '')}]\n", file=md) md.flush() print(f"{n:3} d{day} {r['secs']:5.1f}s {line[:40]:40s} -> {reply[:60]}", flush=True) print("```", file=md) md.close() log.close() if __name__ == "__main__": main()