Quillwright / quillwright /brain_loop.py
Aarya2004
Deploy: sync hosted Space to local app (chat, document capture, Modal backends, pages, mobile/QR)
47b2a99
Raw
History Blame Contribute Delete
4.04 kB
"""The LLM-driven brain loop: the model decides which items to add and when done.
Pure and Ollama-free for testing — takes any model with a .chat(messages, tools)
method (real OllamaModel or a scripted StubModel). Pricing/math stay in the
deterministic tools (Facts-from-Tools). A missing price is RETURNED as a pause
signal; the graph node turns that into a LangGraph interrupt.
"""
from quillwright.brain_tools import BRAIN_TOOLS, dispatch
from quillwright.catalog import Catalog
from quillwright.models import LineItem, TraceStep
SYSTEM = (
"You are a field-service estimator. You are given a tech's note about a job. Add EACH "
"distinct part and the labor to the estimate by calling add_priced_item(item, quantity). "
"Rules:\n"
"- Call add_priced_item ONCE per distinct item. Do not repeat an item.\n"
"- quantity = how many units or hours. Read it from the note: 'two hours'/'2 hrs' -> 2, "
"'both'/'a pair' -> 2, '4 pounds' -> 4. If no count is stated, use 1.\n"
"- Labor is an item too: add it with the number of hours as the quantity.\n"
"- Never invent prices — the tool applies the catalog price.\n"
"- When every part and the labor have been added, call finish().\n"
"Examples:\n"
"Note: 'replaced the capacitor and did 2 hours labor' -> add_priced_item('capacitor', 1), "
"add_priced_item('labor', 2), finish()\n"
"Note: 'replaced both contactors' -> add_priced_item('contactor', 2), finish()\n"
"Note: 'added 4 lbs refrigerant' -> add_priced_item('refrigerant', 4), finish()"
)
def run_brain(
model,
catalog: Catalog,
observations_text: str,
transcript: str,
max_steps: int = 12,
):
"""Drive the model to build line items. Returns (line_items, trace, pause-or-None)."""
line_items: list[LineItem] = []
trace: list[TraceStep] = []
# The actual model name (e.g. "nemotron-3-nano:4b" or "StubModel") so the trace
# truthfully shows which model answered — no guessing whether a model was hit.
brain_name = getattr(model, "name", "brain")
messages = [
{"role": "system", "content": SYSTEM},
{
"role": "user",
"content": f"Observed items: {observations_text}\nTech's note: {transcript}",
},
]
for _ in range(max_steps):
msg = model.chat(messages, BRAIN_TOOLS)
tool_calls = msg.get("tool_calls") or []
if not tool_calls:
break # model produced plain text -> treat as done
messages.append(msg)
done = False
for call in tool_calls:
fn = call.get("function", {})
name = fn.get("name", "")
args = fn.get("arguments", {}) or {}
result = dispatch(name, args, catalog)
if result["status"] == "need_price":
# Surface a pause for the graph to turn into an interrupt.
return line_items, trace, {"item": result["item"]}
if result["status"] == "done":
done = True
trace.append(
TraceStep(action="finish", model=brain_name, detail="estimate complete")
)
break
if result["status"] == "added":
line_items.append(result["line_item"])
li = result["line_item"]
trace.append(
TraceStep(
action="add_priced_item",
model=brain_name,
detail=f"{li.quantity:g} x {li.description} -> {li.subtotal}",
)
)
_tool_reply(messages, call, f"added {result['line_item'].description}")
else: # unknown tool -> corrective message (validate/repair)
_tool_reply(messages, call, f"error: unknown tool '{name}'")
if done:
break
return line_items, trace, None
def _tool_reply(messages: list[dict], call: dict, content: str) -> None:
messages.append({"role": "tool", "content": content})