From e4552f56497bf632761a58458815e52cd6750af2 Mon Sep 17 00:00:00 2001 From: leora Date: Thu, 8 Oct 2026 21:35:37 +0800 Subject: [PATCH] Local model: no chat history by default; rules check for look/text edits (marked in the chat header) --- market/agent.py | 30 +++++++++++++++++++++++++++++- tests/llm_proxy_check.sh | 4 ++++ web/src/App.tsx | 6 +++--- web/src/types.ts | 2 +- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/market/agent.py b/market/agent.py index 2bb6bd7..db6b0ef 100644 --- a/market/agent.py +++ b/market/agent.py @@ -703,6 +703,31 @@ def _focus_results(store, message, actions): return actions +LOCAL_HISTORY = int(os.environ.get("AGENT_MARKET_LOCAL_LLM_HISTORY", "0")) # small models copy earlier turns; 0 = no history + + +def _rules_check(message, look, out): + """Look/copy commands are where a ~4B model is least reliable (it copies presets, misplaces keys, or replies without + the action). When the deterministic parser recognises the message as a look or text edit, its validated patch + wins and the answer is marked rules_checked (the chat header says so).""" + try: + cur = json.loads(look) if look else {} + except ValueError: + cur = {} + from .theme import parse_look_command + r = parse_look_command(message, cur if isinstance(cur, dict) else {}) + if not r: + return out + reply, payload = r + if payload is None: # a text edit it couldn't place: keep the model's answer if it did something + if not out["actions"]: + out["reply"], out["rules_checked"] = reply, True + return out + out["actions"] = [{"type": "set_theme", "payload": payload}] + out["reply"], out["rules_checked"] = reply, True + return out + + def llm_agent(store, message, history, cfg, look=""): """Hosted models in order (skipping ones cooling down after a 429, and all of them while the daily free quota is used up), then the local backup model with its own timeout. Raises if everything fails (keyword rules answer then).""" @@ -756,7 +781,8 @@ def llm_agent(store, message, history, cfg, look=""): errors.append({"model": local["model"], "error": "local_busy", "ms": 0}) else: try: - fut = _pool.submit(_llm_once, store, local, local["model"], message, history, look, cap) + hist = (history or [])[-LOCAL_HISTORY:] if LOCAL_HISTORY > 0 else [] + fut = _pool.submit(_llm_once, store, local, local["model"], message, hist, look, cap) except Exception: _local_slots.release() raise @@ -767,6 +793,8 @@ def llm_agent(store, message, history, cfg, look=""): except concurrent.futures.TimeoutError: raise LLMError("timeout", f">{cap:.0f}s") out["actions"] = _focus_results(store, message, out["actions"]) + if not message.startswith("UI_EVENT "): + out = _rules_check(message, look, out) out.update(agent="llm", provider="local", local=True, model=local["model"], latency_ms=int((time.time() - t0) * 1000), fallbacks=errors) LAST.update(model=local["model"], at=time.time(), error=None, local=True) diff --git a/tests/llm_proxy_check.sh b/tests/llm_proxy_check.sh index 3f55824..519f7f8 100755 --- a/tests/llm_proxy_check.sh +++ b/tests/llm_proxy_check.sh @@ -85,3 +85,7 @@ chat 18810 | python3 -c ' import json,sys; d=json.load(sys.stdin); assert d["agent"]=="llm" and d.get("local") and d["fallbacks"]==[], d print("PASS local model works with no hosted key configured")' curl -s localhost:18810/api/chat -H 'Content-Type: application/json' -d '{"message":"show me bikes"}' | grep -q test-dummy-key && { echo "FAIL key"; exit 1; } || true +curl -s localhost:18810/api/chat -H 'Content-Type: application/json' -d '{"message":"hide the bullet points","theme":{}}' | python3 -c ' +import json,sys; d=json.load(sys.stdin); a=d["actions"] +assert d["agent"]=="llm" and d.get("local") and d.get("rules_checked") and [x["type"] for x in a]==["set_theme"] and a[0]["payload"]["patch"]["sections"]["hidden"]==["bullets"], d +print("PASS local model look command checked by the rules (validated patch wins, marked rules_checked)")' diff --git a/web/src/App.tsx b/web/src/App.tsx index 122c479..d8addad 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -50,13 +50,13 @@ export default function App() { }, []); const say = useCallback((role: Msg["role"], text: string) => { const id = ++seq; setMsgs((m) => [...m, { id, role, text }]); return id; }, []); const unsay = useCallback((id: number) => setMsgs((m) => m.filter((x) => x.id !== id)), []); - const setModel = useCallback((model?: string, ms?: number, local = false) => { + const setModel = useCallback((model?: string, ms?: number, local = false, checked = false) => { const short = (model || "").replace(/:free$/, ""); const secs = ms ? " · " + (ms / 1000).toFixed(1) + "s" : ""; if (local) { // the local backup model running on this server (used when the free hosted models are unavailable) setBadge({ text: `Local model (${short})`, title: `${model} running on this server (local backup; slower than the hosted models)` }); - setChatMode(`Local model · ${short}${secs}`); + setChatMode(`Local model · ${short}${secs}${checked ? " · look checked by rules" : ""}`); return; } setBadge({ text: "AI: " + short, title: (model || "") + " via OpenRouter (free tier)" }); @@ -98,7 +98,7 @@ export default function App() { if (event) history.current.push({ role: "user", content: "UI_EVENT " + JSON.stringify(event) }); history.current.push({ role: "assistant", content: out.reply || "" }); say("bot", out.reply || "(no reply)"); - if (out.agent === "llm") setModel(out.model, out.latency_ms, !!out.local); + if (out.agent === "llm") setModel(out.model, out.latency_ms, !!out.local, !!out.rules_checked); else if (out.agent === "fallback") { setChatMode("built-in agent answered (AI model unavailable)"); setBadge({ text: "agent: built-in (AI unavailable)", title: "The hosted model is rate-limited or out of free quota" }); diff --git a/web/src/types.ts b/web/src/types.ts index d284b74..0ebf00c 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -16,7 +16,7 @@ export interface Order { export interface UIAction { type: string; payload?: any } export interface ChatReply { reply?: string; actions?: UIAction[]; agent?: "llm" | "fallback" | "ui-event" | "rate-limit"; model?: string; latency_ms?: number; - provider?: string; local?: boolean; + provider?: string; local?: boolean; rules_checked?: boolean; } export interface ChatEvent { type: string; [k: string]: unknown } export interface Status {