Local model: focus padded search results on the request's keywords; unit tests

This commit is contained in:
leora 2026-10-08 21:25:55 +08:00
parent c531b427d5
commit 549cd46778
3 changed files with 69 additions and 0 deletions

View File

@ -248,6 +248,7 @@ bash tests/llm_proxy_check.sh # fake LLM: 429 chain,
python3 tests/nl_check.py http://127.0.0.1:8795 # real model: 6 natural-language prompts python3 tests/nl_check.py http://127.0.0.1:8795 # real model: 6 natural-language prompts
python3 tests/test_theme.py # theme spec + customCss sanitiser python3 tests/test_theme.py # theme spec + customCss sanitiser
python3 tests/test_copyedit.py # built-in copy edits (logo/headline/... phrasings) python3 tests/test_copyedit.py # built-in copy edits (logo/headline/... phrasings)
python3 tests/test_agent_units.py # local-model output normalisers (result focus, preset+reset, listing-form shape)
NODE_PATH=... node tests/theme_check.js BASE ./screenshots # chat restyles, guardrail contrast, persistence, hostile stored theme, reset (REQUIRE_LLM=1 to insist on the model) NODE_PATH=... node tests/theme_check.js BASE ./screenshots # chat restyles, guardrail contrast, persistence, hostile stored theme, reset (REQUIRE_LLM=1 to insist on the model)
NODE_PATH=/path/to/node_modules node tests/browser_e2e.js http://127.0.0.1:8795 ./screenshots # headless Chrome (14 checks) NODE_PATH=/path/to/node_modules node tests/browser_e2e.js http://127.0.0.1:8795 ./screenshots # headless Chrome (14 checks)
NODE_PATH=/path/to/node_modules bash tests/fake_model_browser.sh # React UI vs fake model: badges, fallback text, quota, rate-limit toast, sanitised set_theme NODE_PATH=/path/to/node_modules bash tests/fake_model_browser.sh # React UI vs fake model: badges, fallback text, quota, rate-limit toast, sanitised set_theme

View File

@ -677,6 +677,32 @@ def _llm_once(store, cfg, model, message, history, look="", timeout=None):
return {"reply": (reply or "Done.")[:600], "actions": actions, "tools": tool_results} return {"reply": (reply or "Done.")[:600], "actions": actions, "tools": tool_results}
_STOP = set("show me find search look looking for want need some any the a an and or with under over below above cheap "
"please can could you i my our get buy see list all everything something anything thing things stuff near "
"options option there here have has what which that this from about like".split())
def _focus_results(store, message, actions):
"""Small local models sometimes pad show_cards with unrelated listings. If some chosen listings share a keyword with
the request and others share none, keep only the matching ones (semantic picks with no keyword overlap stay)."""
words = {w.rstrip("s") for w in re.findall(r"[a-z]{3,}", (message or "").lower()) if w not in _STOP}
if not words:
return actions
for a in actions:
if a.get("type") != "show_cards" or not isinstance(a.get("payload"), dict):
continue
ids = [i for i in (a["payload"].get("listing_ids") or []) if isinstance(i, str)]
def hit(i):
L = store.get("listings", i) or {}
text = " ".join([L.get("title", ""), L.get("category") or "", " ".join(L.get("tags") or []), L.get("description", "")]).lower()
toks = {t.rstrip("s") for t in re.findall(r"[a-z]{3,}", text)}
return bool(words & toks)
keep = [i for i in ids if hit(i)]
if keep and len(keep) < len(ids):
a["payload"]["listing_ids"] = keep
return actions
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)."""
@ -740,6 +766,7 @@ def llm_agent(store, message, history, cfg, look=""):
out = fut.result(timeout=cap + 1) out = fut.result(timeout=cap + 1)
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.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)

41
tests/test_agent_units.py Normal file
View File

@ -0,0 +1,41 @@
#!/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 -> built-in agent only", 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"))
print(f"\n{sum(results)}/{len(results)} passed")
sys.exit(0 if all(results) else 1)