Aarya2004
Deploy: sync hosted Space to local app (chat, document capture, Modal backends, pages, mobile/QR)
47b2a99 | from typing import TypedDict, Optional | |
| from langgraph.errors import GraphInterrupt | |
| from langgraph.graph import StateGraph, START, END | |
| from langgraph.types import interrupt | |
| from quillwright.models import Capture, Observation, LineItem, Estimate, TraceStep | |
| from quillwright.catalog import Catalog | |
| from quillwright.resolver import Model | |
| from quillwright.tools import perceive, lookup_price, compute, draft_line_item | |
| from quillwright.brain_loop import run_brain | |
| class AgentState(TypedDict): | |
| capture: Capture | |
| observations: list[Observation] | |
| line_items: list[LineItem] | |
| trace: list[TraceStep] | |
| estimate: Optional[Estimate] | |
| def build_agent(perception_model: Model, catalog: Catalog, checkpointer, brain_model=None): | |
| def perceive_node(state: AgentState) -> dict: | |
| obs: list[Observation] = [] | |
| for path in state["capture"].image_paths: | |
| obs.extend(perceive(path, perception_model)) | |
| trace = state["trace"] + [ | |
| TraceStep( | |
| action="perceive", | |
| model=getattr(perception_model, "name", "model"), | |
| detail=f"found {len(obs)} observation(s)", | |
| ) | |
| ] | |
| return {"observations": obs, "trace": trace} | |
| def deterministic_price(state: AgentState) -> dict: | |
| items = list(state["line_items"]) | |
| trace = list(state["trace"]) | |
| for ob in state["observations"]: | |
| res = lookup_price(ob.text, catalog) | |
| if not res["found"]: | |
| # Agent Pause: ask the human for a price; resumed value comes back here. | |
| human_rate = interrupt({"reason": f"No price for '{ob.text}'", "item": ob.text}) | |
| items.append( | |
| draft_line_item( | |
| ob.text, qty=1, unit="ea", rate=float(human_rate), source="user" | |
| ) | |
| ) | |
| trace.append( | |
| TraceStep(action="price", detail=f"user priced {ob.text}", status="ok") | |
| ) | |
| continue | |
| subtotal = compute(f"1 * {res['rate']}") # qty defaults to 1 in the core | |
| items.append( | |
| draft_line_item( | |
| res["description"], qty=1, unit=res["unit"], rate=res["rate"], source="catalog" | |
| ) | |
| ) | |
| trace.append( | |
| TraceStep( | |
| action="price", | |
| model="lookup_price", | |
| detail=f"{res['description']} -> {subtotal}", | |
| ) | |
| ) | |
| return {"line_items": items, "trace": trace} | |
| def brain_price(state: AgentState) -> dict: | |
| # The LLM brain decides which items to add; deterministic tools own the numbers. | |
| obs_text = ", ".join(ob.text for ob in state["observations"]) | |
| priced_extra: list[LineItem] = [] | |
| while True: | |
| try: | |
| items, brain_trace, pause = run_brain( | |
| brain_model, | |
| catalog, | |
| observations_text=obs_text, | |
| transcript=state["capture"].transcript, | |
| ) | |
| except GraphInterrupt: | |
| raise # an Agent Pause is control flow, not a failure — let it propagate | |
| except Exception as exc: # noqa: BLE001 — brain/model failure: degrade, don't crash | |
| # The LLM brain is unavailable (e.g. Ollama 500). Fall back to the | |
| # deterministic catalog pricer so the forge still produces an estimate | |
| # instead of crashing the stream. Facts-from-Tools still holds. | |
| print(f"[quillwright] brain failed ({exc}); falling back to deterministic pricing.") | |
| out = deterministic_price(state) | |
| out["trace"] = out["trace"] + [ | |
| TraceStep( | |
| action="price", | |
| model="fallback", | |
| detail="Brain unavailable — priced from the catalog directly.", | |
| status="ok", | |
| ) | |
| ] | |
| return out | |
| if pause is None: | |
| trace = state["trace"] + brain_trace | |
| return { | |
| "line_items": list(state["line_items"]) + priced_extra + items, | |
| "trace": trace, | |
| } | |
| # Missing price -> ask the human, record it on the catalog, then re-run the brain. | |
| human_rate = interrupt( | |
| {"reason": f"No price for '{pause['item']}'", "item": pause["item"]} | |
| ) | |
| priced_extra.append( | |
| draft_line_item( | |
| pause["item"], qty=1, unit="ea", rate=float(human_rate), source="user" | |
| ) | |
| ) | |
| catalog.add(pause["item"], pause["item"], "ea", float(human_rate)) | |
| price_node = brain_price if brain_model is not None else deterministic_price | |
| def assemble_node(state: AgentState) -> dict: | |
| est = Estimate( | |
| job_title=state["capture"].trade_hint or "Job", | |
| line_items=state["line_items"], | |
| tax_rate=0.13, | |
| ) | |
| trace = state["trace"] + [TraceStep(action="assemble", detail=f"total {est.total}")] | |
| return {"estimate": est, "trace": trace} | |
| g = StateGraph(AgentState) | |
| g.add_node("perceive", perceive_node) | |
| g.add_node("price", price_node) | |
| g.add_node("assemble", assemble_node) | |
| g.add_edge(START, "perceive") | |
| g.add_edge("perceive", "price") | |
| g.add_edge("price", "assemble") | |
| g.add_edge("assemble", END) | |
| return g.compile(checkpointer=checkpointer) | |