- Remove the keyword-rules agent from typed chat: fallback_agent, parse_look_command (+ tables), the copy-edit parser and _rules_check are gone. If every model fails: 'The assistant is busy, please try again in a moment.' with a Retry button (agent 'unavailable', no actions). Clicks, forms and payments stay deterministic (fallback_event); the landing view is a deterministic featured-listings panel; the Sell button opens the empty form. - Local model via llama.cpp llama-server directly (ops/local-llm-watchdog.sh, loopback only, restart on exit/hang, crash backoff, log rotation; deploy.sh ensures it runs). Lean: no vision projector, no weight repack (mmapped), ctx 6144, 1 slot, q8 KV, 6 threads at nice 10, poll 0, 1 GiB prompt cache. Idle 0% CPU, 2.9 GB RSS (0.6 GB private). - Local requests use a JSON GBNF grammar, worked examples in the cached system prompt, and a labelled visitor message. 10/10 correct on the real prompt, warm 4-13 s. - Primer re-primes after a model-server restart (llama-server /metrics counter) and otherwise stays idle. - Fix: hosted calls kept the Authorization header off when a local model was configured (cfg['local'] is a dict on hosted configs); now only the local server itself skips the key. Test added. - Badge/chat header: 'AI: busy', 'AI: quota used up', 'AI: unavailable'; no 'built-in'/'checked by rules' texts. - Tests: proxy checks expect the busy reply (never rules), clicks still work while busy, no-model server, local grammar/ prompt/no key; browser checks for busy + Retry; e2e_http accepts busy for typed turns unless REQUIRE_LLM=1; test_copyedit.py replaced by test_copy_defaults.py; README updated.
56 lines
3.6 KiB
Python
56 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Unit checks for the model-output normalisers used with the local backup model (no server, no model)."""
|
|
import os, sys, tempfile
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
os.environ["AGENT_MARKET_DATA"] = tempfile.mkdtemp()
|
|
for k in ("AGENT_MARKET_LLM_API_KEY_FILE", "AGENT_MARKET_LOCAL_LLM_URL"):
|
|
os.environ.pop(k, None)
|
|
import server # noqa: E402 (seeds a temp store)
|
|
from market import agent, theme # noqa: E402
|
|
|
|
results = []
|
|
def check(name, ok, d=""):
|
|
results.append(bool(ok)); print(("PASS " if ok else "FAIL ") + name + (f" [{d}]" if d else ""))
|
|
|
|
st = server.store
|
|
by = {L["title"]: L["id"] for L in st.all("listings")}
|
|
tut = next(i for t, i in by.items() if "tutoring" in t.lower()); bike = next(i for t, i in by.items() if "kids" in t.lower())
|
|
lawn = next(i for t, i in by.items() if "lawn" in t.lower())
|
|
f = lambda q, ids: agent._focus_results(st, q, [{"type": "show_cards", "payload": {"listing_ids": list(ids)}}])[0]["payload"]["listing_ids"]
|
|
check("focus: unrelated padding dropped when some picks match", f("show me bikes", [tut, bike]) == [bike])
|
|
check("focus: semantic picks kept when nothing matches lexically", f("anything for the garden?", [lawn]) == [lawn])
|
|
check("focus: no keywords -> unchanged", f("show me everything", [tut, bike]) == [tut, bike])
|
|
|
|
a = theme.build_theme_action({"preset": "sunset", "reset": True})
|
|
check("theme: preset + reset = start from the preset (not a bare reset)", a and a.get("preset") == "sunset" and a.get("base") and a.get("reset"), a)
|
|
a = theme.build_theme_action({"reset": True})
|
|
check("theme: bare reset still resets", a == {"reset": True, "patch": {}}, a)
|
|
|
|
h = server.hydrate([{"type": "show_listing_form", "payload": {"title": "Old guitar", "price_cents": 12000, "payment_options": ["cash"]}}], "t")
|
|
d = h[0]["payload"]["defaults"]
|
|
check("listing form: flat payload + price_cents normalised", d == {"title": "Old guitar", "price": 12000, "payment_options": ["cash"]}, d)
|
|
h = server.hydrate([{"type": "show_listing_form", "payload": {"defaults": {"title": "Lamp", "price": 2500}}}], "t")
|
|
check("listing form: normal shape untouched", h[0]["payload"]["defaults"] == {"title": "Lamp", "price": 2500})
|
|
|
|
cfg = agent._llm_config()
|
|
check("no hosted key and no local model -> no chain (typed messages get the busy reply)", cfg is None)
|
|
os.environ.update(AGENT_MARKET_LOCAL_LLM_URL="http://127.0.0.1:9/v1", AGENT_MARKET_LOCAL_LLM_MODEL="m")
|
|
cfg = agent._llm_config()
|
|
check("local model alone is a valid chain", cfg and cfg["models"] == [] and cfg["local"]["model"] == "m" and cfg["local"]["url"].endswith("/v1/chat/completions"))
|
|
# primer: re-prime after the local server restarts (llama-server /metrics counter goes back down) or is down
|
|
import http.server, threading
|
|
M = {"n": 500}
|
|
class _H(http.server.BaseHTTPRequestHandler):
|
|
def log_message(self, *a): pass
|
|
def do_GET(self):
|
|
b = f"# HELP x\nllamacpp:n_decode_total {M['n']}\n".encode(); self.send_response(200); self.end_headers(); self.wfile.write(b)
|
|
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), _H); threading.Thread(target=srv.serve_forever, daemon=True).start()
|
|
loc = {"url": f"http://127.0.0.1:{srv.server_port}/v1/chat/completions"}
|
|
agent._PRIMED["decodes"] = None
|
|
first = agent._local_restarted(loc); M["n"] = 520; grew = agent._local_restarted(loc); M["n"] = 7; dropped = agent._local_restarted(loc)
|
|
srv.shutdown(); down = agent._local_restarted(loc)
|
|
check("primer: restart detected from /metrics, normal use is not a restart, down = re-prime",
|
|
(first, grew, dropped, down) == (False, False, True, True), (first, grew, dropped, down))
|
|
print(f"\n{sum(results)}/{len(results)} passed")
|
|
sys.exit(0 if all(results) else 1)
|