Local model: short catalog refs (L1, L2, ...) mapped back to real ids; checkout and cash examples
A 4B model miscopied 8-hex listing ids (picked a tutoring id for a kids' bike). The local prompt now shows L-refs, the server maps them back before tools/hydration (unknown refs are dropped as before), tool results are aliased the same way, and refs are scrubbed from replies. Hosted models still see real ids.
This commit is contained in:
parent
d785091d64
commit
6c2a1d6a40
@ -497,14 +497,46 @@ EXAMPLES (visitor message => your whole answer). Follow the pattern; use the vis
|
|||||||
"a teal to purple gradient behind everything" => {"reply":"Teal to purple gradient background.","actions":[{"type":"set_theme","payload":{"background":{"kind":"gradient","colors":["#12a594","#8e4ec6"],"angle":135}}}]}
|
"a teal to purple gradient behind everything" => {"reply":"Teal to purple gradient background.","actions":[{"type":"set_theme","payload":{"background":{"kind":"gradient","colors":["#12a594","#8e4ec6"],"angle":135}}}]}
|
||||||
"make it neon" => {"reply":"Neon look applied.","actions":[{"type":"set_theme","payload":{"preset":"neon"}}]}
|
"make it neon" => {"reply":"Neon look applied.","actions":[{"type":"set_theme","payload":{"preset":"neon"}}]}
|
||||||
"I'd like to sell my old sofa for $60, cash only" => {"reply":"I've filled in the listing form. Check it and press Publish.","actions":[{"type":"show_listing_form","payload":{"defaults":{"title":"Old sofa","kind":"product","price":6000,"description":"Used sofa in good condition.","category":"home","area":"Demo Town","payment_options":["cash"]}}}]}
|
"I'd like to sell my old sofa for $60, cash only" => {"reply":"I've filled in the listing form. Check it and press Publish.","actions":[{"type":"show_listing_form","payload":{"defaults":{"title":"Old sofa","kind":"product","price":6000,"description":"Used sofa in good condition.","category":"home","area":"Demo Town","payment_options":["cash"]}}}]}
|
||||||
"<a search>" => {"reply":"<one short line>","actions":[{"type":"show_cards","payload":{"heading":"<what was searched>","listing_ids":["<only CATALOG ids that match>"]}}]}
|
"<a search>" => {"reply":"<one short line>","actions":[{"type":"show_cards","payload":{"heading":"<what was searched>","listing_ids":["<id of each CATALOG item that fits>"]}}]}
|
||||||
|
"I'll buy <item> and pay online" => {"reply":"Here's the checkout (price + fee).","actions":[{"type":"show_checkout","payload":{"listing_id":"<that item's CATALOG id>"}}]}
|
||||||
|
"can I pay cash for <item> in person?" => {"reply":"Cash meetup set up, no fee.","actions":[{"type":"show_cash_meetup","payload":{"listing_id":"<that item's CATALOG id>","suggested_place":"Demo Town square","suggested_time":"Saturday 10am"}}]}
|
||||||
Only change what the visitor asked for. A visual or text request returns set_theme only (no listings).
|
Only change what the visitor asked for. A visual or text request returns set_theme only (no listings).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Small models miscopy 8-hex ids (lst_ffe42107 vs lst_29d7f320), so the local model sees short catalog refs (L1, L2, ...)
|
||||||
|
# that are mapped back to real ids before anything runs. Unknown refs are dropped like any invented id.
|
||||||
|
LOCAL_SCHEMA = UI_SCHEMA.replace('"lst_..."', '"L7"').replace("internal ids (lst_...,", "internal ids (L7, lst_...,")
|
||||||
|
ALIAS_RE = re.compile(r"\s*\(?\bL\d{1,3}\b\)?:?")
|
||||||
|
|
||||||
|
|
||||||
|
def _aliases(store):
|
||||||
|
"""ref -> real listing id, in catalog order (stable while the catalog is unchanged, so the prompt stays cacheable)."""
|
||||||
|
return {f"L{i}": c["id"] for i, c in enumerate(_catalog(store), 1)}
|
||||||
|
|
||||||
|
|
||||||
|
def _unalias(obj, alias):
|
||||||
|
"""Map L-refs back to real listing ids in action payloads / tool args (in place)."""
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
for k, v in list(obj.items()):
|
||||||
|
if k == "listing_id" and isinstance(v, str):
|
||||||
|
obj[k] = alias.get(v.strip(), v)
|
||||||
|
elif k == "listing_ids" and isinstance(v, list):
|
||||||
|
obj[k] = [alias.get(x.strip(), x) if isinstance(x, str) else x for x in v]
|
||||||
|
else:
|
||||||
|
_unalias(v, alias)
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
for x in obj:
|
||||||
|
_unalias(x, alias)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def _system_prompt(store, look="", local=False):
|
def _system_prompt(store, look="", local=False):
|
||||||
# stable parts first (schema, examples, catalog) so a local model's prompt cache can reuse them across turns
|
# stable parts first (schema, examples, catalog) so a local model's prompt cache can reuse them across turns
|
||||||
return (UI_SCHEMA + (LOCAL_EXAMPLES if local else "") + "\nCATALOG (all active listings):\n" + json.dumps(_catalog(store))[:9000] +
|
cat = _catalog(store)
|
||||||
|
if local:
|
||||||
|
cat = [{**c, "id": f"L{i}"} for i, c in enumerate(cat, 1)]
|
||||||
|
return ((LOCAL_SCHEMA + LOCAL_EXAMPLES if local else UI_SCHEMA) + "\nCATALOG (all active listings):\n" + json.dumps(cat)[:9000] +
|
||||||
"\nCURRENT_LOOK (this visitor's theme overrides; {} = default dark look):\n" + (look or "{}") +
|
"\nCURRENT_LOOK (this visitor's theme overrides; {} = default dark look):\n" + (look or "{}") +
|
||||||
"\nPAGE_TEXT (what the editable text slots show right now):\n" + _page_text(look))
|
"\nPAGE_TEXT (what the editable text slots show right now):\n" + _page_text(look))
|
||||||
|
|
||||||
@ -588,24 +620,30 @@ def _llm_once(store, cfg, model, message, history, look="", timeout=None):
|
|||||||
msgs.append({"role": h["role"], "content": str(h["content"])[:600]})
|
msgs.append({"role": h["role"], "content": str(h["content"])[:600]})
|
||||||
msgs.append({"role": "user", "content": _visitor(message) if local else message})
|
msgs.append({"role": "user", "content": _visitor(message) if local else message})
|
||||||
t_end = time.time() + (timeout or LLM_TIMEOUT)
|
t_end = time.time() + (timeout or LLM_TIMEOUT)
|
||||||
|
alias = _aliases(store) if local else {}
|
||||||
content = _call_llm(cfg, model, msgs, timeout=timeout)
|
content = _call_llm(cfg, model, msgs, timeout=timeout)
|
||||||
plan = _parse_agent_json(content)
|
plan = _unalias(_parse_agent_json(content), alias)
|
||||||
tool_results = []
|
tool_results = []
|
||||||
tools = [t for t in (plan.get("tools") or []) if isinstance(t, dict)][:3]
|
tools = [t for t in (plan.get("tools") or []) if isinstance(t, dict)][:3]
|
||||||
if tools:
|
if tools:
|
||||||
tool_results = run_tools(store, tools)
|
tool_results = run_tools(store, tools)
|
||||||
|
tr = json.dumps(tool_results)
|
||||||
|
for ref, real in alias.items():
|
||||||
|
tr = tr.replace(real, ref)
|
||||||
msgs += [{"role": "assistant", "content": content},
|
msgs += [{"role": "assistant", "content": content},
|
||||||
{"role": "user", "content": "TOOL_RESULTS:\n" + json.dumps(tool_results)[:6000] +
|
{"role": "user", "content": "TOOL_RESULTS:\n" + tr[:6000] +
|
||||||
"\nNow return the final {\"reply\",\"actions\"} JSON only."}]
|
"\nNow return the final {\"reply\",\"actions\"} JSON only."}]
|
||||||
left = t_end - time.time()
|
left = t_end - time.time()
|
||||||
if left < 2:
|
if left < 2:
|
||||||
raise LLMError("timeout", "no time left for the tool round")
|
raise LLMError("timeout", "no time left for the tool round")
|
||||||
plan = _parse_agent_json(_call_llm(cfg, model, msgs, timeout=left))
|
plan = _unalias(_parse_agent_json(_call_llm(cfg, model, msgs, timeout=left)), alias)
|
||||||
actions = [a for a in (plan.get("actions") or []) if isinstance(a, dict) and a.get("type") in ALLOWED_ACTIONS]
|
actions = [a for a in (plan.get("actions") or []) if isinstance(a, dict) and a.get("type") in ALLOWED_ACTIONS]
|
||||||
reply = str(plan.get("reply") or "").strip()
|
reply = str(plan.get("reply") or "").strip()
|
||||||
if not reply and not actions:
|
if not reply and not actions:
|
||||||
raise LLMError("empty_plan", "")
|
raise LLMError("empty_plan", "")
|
||||||
reply = ID_RE.sub("", reply).strip()
|
reply = ID_RE.sub("", reply).strip()
|
||||||
|
if local:
|
||||||
|
reply = ALIAS_RE.sub("", reply).strip()
|
||||||
return {"reply": (reply or "Done.")[:600], "actions": actions, "tools": tool_results}
|
return {"reply": (reply or "Done.")[:600], "actions": actions, "tools": tool_results}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@ class H(BaseHTTPRequestHandler):
|
|||||||
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers(); self.wfile.write(b)
|
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers(); self.wfile.write(b)
|
||||||
return
|
return
|
||||||
if last.startswith("TOOL_RESULTS"):
|
if last.startswith("TOOL_RESULTS"):
|
||||||
ids = re.findall(r'"id": "(lst_[a-f0-9]+)"', last)[:3]
|
ids = re.findall(r'"id": "(lst_[a-f0-9]+|L\d+)"', last)[:3] # local model sees short refs (L1, L2, ...)
|
||||||
out = {"reply": "Here are bikes I found (mock LLM). Payments are TEST MODE ONLY.",
|
out = {"reply": "Here are bikes I found (mock LLM). Payments are TEST MODE ONLY.",
|
||||||
"actions": [{"type": "show_cards", "payload": {"heading": "Bikes", "listing_ids": ids}},
|
"actions": [{"type": "show_cards", "payload": {"heading": "Bikes", "listing_ids": ids}},
|
||||||
{"type": "not_allowed_component", "payload": {}}]}
|
{"type": "not_allowed_component", "payload": {}}]}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Unit checks for the model-output normalisers used with the local backup model (no server, no model)."""
|
"""Unit checks for the model-output normalisers used with the local backup model (no server, no model)."""
|
||||||
import os, sys, tempfile
|
import os, re, sys, tempfile
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
os.environ["AGENT_MARKET_DATA"] = tempfile.mkdtemp()
|
os.environ["AGENT_MARKET_DATA"] = tempfile.mkdtemp()
|
||||||
for k in ("AGENT_MARKET_LLM_API_KEY_FILE", "AGENT_MARKET_LOCAL_LLM_URL"):
|
for k in ("AGENT_MARKET_LLM_API_KEY_FILE", "AGENT_MARKET_LOCAL_LLM_URL"):
|
||||||
@ -37,6 +37,16 @@ check("no hosted key and no local model -> no chain (typed messages get the busy
|
|||||||
os.environ.update(AGENT_MARKET_LOCAL_LLM_URL="http://127.0.0.1:9/v1", AGENT_MARKET_LOCAL_LLM_MODEL="m")
|
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()
|
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"))
|
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"))
|
||||||
|
# local model: short catalog refs in the prompt, mapped back to real ids; unknown refs left for hydrate() to drop
|
||||||
|
al = agent._aliases(st)
|
||||||
|
sp = agent._system_prompt(st, "", local=True)
|
||||||
|
check("local prompt uses short refs, not hex ids", '"id": "L1"' in sp and not re.search(r"lst_[0-9a-f]{6}", sp.split("CATALOG (all active listings)")[1]) and '"L7"' in sp, len(al))
|
||||||
|
plan = agent._unalias({"actions": [{"type": "show_cards", "payload": {"listing_ids": ["L1", "L999"]}},
|
||||||
|
{"type": "show_checkout", "payload": {"listing_id": "L2"}}]}, al)
|
||||||
|
check("refs mapped back (unknown kept for hydrate to drop)", plan["actions"][0]["payload"]["listing_ids"] == [al["L1"], "L999"] and
|
||||||
|
plan["actions"][1]["payload"]["listing_id"] == al["L2"])
|
||||||
|
check("hosted prompt keeps real ids", al["L1"] in agent._system_prompt(st, "", local=False))
|
||||||
|
|
||||||
# primer: re-prime after the local server restarts (llama-server /metrics counter goes back down) or is down
|
# primer: re-prime after the local server restarts (llama-server /metrics counter goes back down) or is down
|
||||||
import http.server, threading
|
import http.server, threading
|
||||||
M = {"n": 500}
|
M = {"n": 500}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user