Local model: no chat history by default; rules check for look/text edits (marked in the chat header)

This commit is contained in:
leora 2026-10-08 21:35:37 +08:00
parent 549cd46778
commit e4552f5649
4 changed files with 37 additions and 5 deletions

View File

@ -703,6 +703,31 @@ def _focus_results(store, message, actions):
return 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=""): 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 """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).""" 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}) errors.append({"model": local["model"], "error": "local_busy", "ms": 0})
else: else:
try: 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: except Exception:
_local_slots.release() _local_slots.release()
raise raise
@ -767,6 +793,8 @@ def llm_agent(store, message, history, cfg, look=""):
except concurrent.futures.TimeoutError: except concurrent.futures.TimeoutError:
raise LLMError("timeout", f">{cap:.0f}s") raise LLMError("timeout", f">{cap:.0f}s")
out["actions"] = _focus_results(store, message, out["actions"]) 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"], out.update(agent="llm", provider="local", local=True, model=local["model"],
latency_ms=int((time.time() - t0) * 1000), fallbacks=errors) latency_ms=int((time.time() - t0) * 1000), fallbacks=errors)
LAST.update(model=local["model"], at=time.time(), error=None, local=True) LAST.update(model=local["model"], at=time.time(), error=None, local=True)

View File

@ -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 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")' 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":"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)")'

View File

@ -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 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 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 short = (model || "").replace(/:free$/, "");
const secs = ms ? " · " + (ms / 1000).toFixed(1) + "s" : ""; const secs = ms ? " · " + (ms / 1000).toFixed(1) + "s" : "";
if (local) { if (local) {
// the local backup model running on this server (used when the free hosted models are unavailable) // 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)` }); 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; return;
} }
setBadge({ text: "AI: " + short, title: (model || "") + " via OpenRouter (free tier)" }); 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) }); if (event) history.current.push({ role: "user", content: "UI_EVENT " + JSON.stringify(event) });
history.current.push({ role: "assistant", content: out.reply || "" }); history.current.push({ role: "assistant", content: out.reply || "" });
say("bot", out.reply || "(no 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") { else if (out.agent === "fallback") {
setChatMode("built-in agent answered (AI model unavailable)"); 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" }); setBadge({ text: "agent: built-in (AI unavailable)", title: "The hosted model is rate-limited or out of free quota" });

View File

@ -16,7 +16,7 @@ export interface Order {
export interface UIAction { type: string; payload?: any } export interface UIAction { type: string; payload?: any }
export interface ChatReply { export interface ChatReply {
reply?: string; actions?: UIAction[]; agent?: "llm" | "fallback" | "ui-event" | "rate-limit"; model?: string; latency_ms?: number; 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 ChatEvent { type: string; [k: string]: unknown }
export interface Status { export interface Status {