Typed chat: free hosted models -> local model (llama-server), no keyword rules; busy reply + Retry #7
@ -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}}}]}
|
||||
"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"]}}}]}
|
||||
"<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).
|
||||
"""
|
||||
|
||||
|
||||
# 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):
|
||||
# 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 "{}") +
|
||||
"\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": "user", "content": _visitor(message) if local else message})
|
||||
t_end = time.time() + (timeout or LLM_TIMEOUT)
|
||||
alias = _aliases(store) if local else {}
|
||||
content = _call_llm(cfg, model, msgs, timeout=timeout)
|
||||
plan = _parse_agent_json(content)
|
||||
plan = _unalias(_parse_agent_json(content), alias)
|
||||
tool_results = []
|
||||
tools = [t for t in (plan.get("tools") or []) if isinstance(t, dict)][:3]
|
||||
if 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},
|
||||
{"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."}]
|
||||
left = t_end - time.time()
|
||||
if left < 2:
|
||||
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]
|
||||
reply = str(plan.get("reply") or "").strip()
|
||||
if not reply and not actions:
|
||||
raise LLMError("empty_plan", "")
|
||||
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}
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
return
|
||||
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.",
|
||||
"actions": [{"type": "show_cards", "payload": {"heading": "Bikes", "listing_ids": ids}},
|
||||
{"type": "not_allowed_component", "payload": {}}]}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/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
|
||||
import os, re, 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"):
|
||||
@ -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")
|
||||
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"))
|
||||
# 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
|
||||
import http.server, threading
|
||||
M = {"n": 500}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user