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
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)

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

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 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" });

View File

@ -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 {