Aarya2004
Deploy: sync hosted Space to local app (chat, document capture, Modal backends, pages, mobile/QR)
47b2a99 | """Thin adapter: HTTP request -> the tested agent backend -> JSON. | |
| Holds no business logic; it only drives quillwright.agent and shapes the result | |
| for the frontend. Kept testable without a running server. | |
| """ | |
| import json | |
| import os | |
| from langgraph.checkpoint.memory import InMemorySaver | |
| from langgraph.types import Command | |
| from quillwright.agent import build_agent | |
| from quillwright.catalog import Catalog | |
| from quillwright.estimate_store import EstimateStore | |
| from quillwright.memory import Memory | |
| from quillwright.models import Capture | |
| from quillwright.resolver import ModelResolver, StubModel | |
| CATALOG = Catalog.from_file("data/sample_catalog.json") | |
| # On-device memory of past jobs (private JSON file). | |
| MEMORY_PATH = "/tmp/quillwright_memory.json" | |
| _MEMORY: Memory | None = None | |
| def _memory() -> Memory: | |
| global _MEMORY | |
| if _MEMORY is None: | |
| _MEMORY = Memory(MEMORY_PATH) | |
| return _MEMORY | |
| def reset_memory() -> None: | |
| """Drop the in-process memory (re-reads MEMORY_PATH next use). For tests.""" | |
| global _MEMORY | |
| _MEMORY = None | |
| # Per-Account Estimate Store (ADR-0013) — separate from Episodic Memory above. | |
| # A singleton mirroring _memory(); re-reads its env path after reset_estimate_store(). | |
| _ESTIMATE_STORE: EstimateStore | None = None | |
| def estimate_store() -> EstimateStore: | |
| global _ESTIMATE_STORE | |
| if _ESTIMATE_STORE is None: | |
| _ESTIMATE_STORE = EstimateStore() | |
| return _ESTIMATE_STORE | |
| def reset_estimate_store() -> None: | |
| """Drop the in-process store (re-reads env path next use). For tests.""" | |
| global _ESTIMATE_STORE | |
| _ESTIMATE_STORE = None | |
| def save_estimate_record(rows, job_title, tax_rate, thread, id=None) -> dict: | |
| """Recalc to authoritative numbers (Facts-from-Tools), then persist the snapshot.""" | |
| from quillwright.api.recalc import recalc_estimate | |
| est = recalc_estimate(rows, job_title=job_title, tax_rate=tax_rate) | |
| return estimate_store().save(estimate=est, thread=thread, id=id) | |
| # FF_REAL_MODELS=1 uses real local models via Ollama; otherwise the demo stub. | |
| REAL_MODELS = os.environ.get("FF_REAL_MODELS") == "1" | |
| # Demo-only keyword -> observation map. Honest scaffolding for when there is no | |
| # photo (transcript only) or real models are off; lets the note drive what's "seen". | |
| _DEMO_VOCAB = { | |
| "capacitor": {"kind": "part", "text": "capacitor", "confidence": 0.9}, | |
| "contactor": {"kind": "part", "text": "contactor", "confidence": 0.9}, | |
| "refrigerant": {"kind": "part", "text": "refrigerant_r410a", "confidence": 0.85}, | |
| "labor": {"kind": "part", "text": "labor", "confidence": 0.95}, | |
| "unobtainium": {"kind": "part", "text": "unobtainium", "confidence": 0.7}, | |
| } | |
| def _stub_perception(transcript: str) -> StubModel: | |
| """Transcript-aware stub used when there's no real photo or real models are off.""" | |
| low = transcript.lower() | |
| obs = [v for k, v in _DEMO_VOCAB.items() if k in low] | |
| if not obs: # always produce something so the demo never dead-ends | |
| obs = [_DEMO_VOCAB["capacitor"], _DEMO_VOCAB["labor"]] | |
| return StubModel(responses=[json.dumps(obs)]) | |
| def _perception(transcript: str, has_real_image: bool): | |
| """The Perception role for a real photo: hosted Omni (Best Stack) when its | |
| Modal URL is configured, MiniCPM-V via Ollama under FF_REAL_MODELS, else stub.""" | |
| if has_real_image: | |
| from quillwright.resolver import modal_resolver_if_configured | |
| modal = modal_resolver_if_configured("perception") | |
| if modal is not None: | |
| return modal.for_role("perception") | |
| if REAL_MODELS: | |
| return ModelResolver(mode="private", backend="ollama").for_role("perception") | |
| return _stub_perception(transcript) | |
| def _brain(): | |
| """Real tool-calling brain when enabled; else None (deterministic path). | |
| Local Ollama (FF_REAL_MODELS=1) or hosted Modal Best-Stack (FF_BACKEND=modal); | |
| brain_resolver() picks based on env. | |
| """ | |
| if REAL_MODELS or os.environ.get("FF_BACKEND") == "modal": | |
| from quillwright.resolver import brain_resolver | |
| return brain_resolver().for_role("brain") | |
| return None | |
| def _estimate_payload(est) -> dict: | |
| return { | |
| "job_title": est.job_title, | |
| "line_items": [ | |
| { | |
| "description": li.description, | |
| "quantity": li.quantity, | |
| "unit": li.unit, | |
| "rate": li.rate, | |
| "subtotal": li.subtotal, | |
| } | |
| for li in est.line_items | |
| ], | |
| "subtotal": est.subtotal, | |
| "tax_rate": est.tax_rate, | |
| "tax": est.tax, | |
| "total": est.total, | |
| } | |
| def _trace_payload(trace) -> list[dict]: | |
| return [ | |
| {"action": s.action, "model": s.model, "detail": s.detail, "status": s.status} | |
| for s in trace | |
| ] | |
| def forge_estimate( | |
| transcript: str, trade: str = "hvac", image_paths: list[str] | None = None | |
| ) -> dict: | |
| """Run the agent once (non-streaming) and return trace + estimate as JSON.""" | |
| images = [p for p in (image_paths or []) if os.path.isfile(p)] | |
| agent = build_agent( | |
| _perception(transcript, bool(images)), CATALOG, InMemorySaver(), brain_model=_brain() | |
| ) | |
| cap = Capture( | |
| image_paths=images or ["demo.jpg"], transcript=transcript, trade_hint=trade or "Job" | |
| ) | |
| out = agent.invoke( | |
| {"capture": cap, "observations": [], "line_items": [], "trace": [], "estimate": None}, | |
| {"configurable": {"thread_id": "ui"}}, | |
| ) | |
| est = out.get("estimate") | |
| payload = { | |
| "trace": _trace_payload(out["trace"]), | |
| "estimate": _estimate_payload(est) if est is not None else None, | |
| } | |
| if payload["estimate"] is not None: | |
| _autosave(payload["estimate"]) | |
| return payload | |
| def _autosave(estimate: dict) -> None: | |
| """Auto-save a finished estimate so 'My Estimates' populates (ADR-0013 lifecycle). | |
| Best-effort: persistence must never fail a forge.""" | |
| try: | |
| estimate_store().save(estimate=estimate, thread=[]) | |
| except Exception: # noqa: BLE001 — persistence is best-effort | |
| pass | |
| # Active runs by thread_id, so a paused run can be resumed with the same agent + checkpointer. | |
| # {thread_id: {"agent": compiled_graph, "emitted": int}} | |
| _RUNS: dict[str, dict] = {} | |
| def _step_event(step) -> dict: | |
| return { | |
| "type": "trace", | |
| "step": { | |
| "action": step.action, | |
| "model": step.model, | |
| "detail": step.detail, | |
| "status": step.status, | |
| }, | |
| } | |
| def _drive(agent, payload, thread_id: str): | |
| """Stream a run (or resume) to completion or the next Agent Pause. | |
| Yields trace events, then either a pause event (and stops) or an estimate event. | |
| """ | |
| run = _RUNS[thread_id] | |
| cfg = {"configurable": {"thread_id": thread_id}} | |
| estimate = None | |
| for chunk in agent.stream(payload, cfg, stream_mode="updates"): | |
| # An interrupt surfaces under the "__interrupt__" key rather than a node update. | |
| if "__interrupt__" in chunk: | |
| intr = chunk["__interrupt__"][0] | |
| data = intr.value if hasattr(intr, "value") else intr | |
| yield { | |
| "type": "pause", | |
| "reason": data.get("reason", "Need your input"), | |
| "item": data.get("item", ""), | |
| } | |
| return | |
| for _node, update in chunk.items(): | |
| trace = update.get("trace") | |
| if trace is not None: | |
| for step in trace[run["emitted"] :]: | |
| yield _step_event(step) | |
| run["emitted"] = len(trace) | |
| if update.get("estimate") is not None: | |
| estimate = update["estimate"] | |
| # Record the finished job to Episodic memory. | |
| if estimate is not None: | |
| _memory().record_run( | |
| run.get("transcript", ""), | |
| [li.description for li in estimate.line_items], | |
| total=estimate.total, | |
| ) | |
| est_payload = _estimate_payload(estimate) if estimate is not None else None | |
| if est_payload is not None: | |
| _autosave(est_payload) # ADR-0013: finished forge auto-saves to the store | |
| yield { | |
| "type": "estimate", | |
| "estimate": est_payload, | |
| } | |
| _RUNS.pop(thread_id, None) | |
| def forge_estimate_stream( | |
| transcript: str, | |
| trade: str = "hvac", | |
| thread_id: str = "ui", | |
| image_paths: list[str] | None = None, | |
| ): | |
| """Run the agent, yielding each new trace step, then a pause OR the estimate. | |
| Events: {"type":"trace",...} per step, then {"type":"pause",...} or {"type":"estimate",...}. | |
| """ | |
| images = [p for p in (image_paths or []) if os.path.isfile(p)] | |
| agent = build_agent( | |
| _perception(transcript, bool(images)), CATALOG, InMemorySaver(), brain_model=_brain() | |
| ) | |
| _RUNS[thread_id] = {"agent": agent, "emitted": 0, "transcript": transcript} | |
| cap = Capture( | |
| image_paths=images or ["demo.jpg"], transcript=transcript, trade_hint=trade or "Job" | |
| ) | |
| # Surface a recall of similar past jobs before estimating ("it learns"). | |
| recalled = _recall_similar(transcript) | |
| if recalled: | |
| yield { | |
| "type": "trace", | |
| "step": { | |
| "action": "recall", | |
| "model": "memory", | |
| "detail": recalled, | |
| "status": "ok", | |
| }, | |
| } | |
| init = {"capture": cap, "observations": [], "line_items": [], "trace": [], "estimate": None} | |
| yield from _drive(agent, init, thread_id) | |
| def _recall_similar(transcript: str) -> str: | |
| """Find the most relevant past job; return a short human description, or ''.""" | |
| for word in sorted(set(transcript.lower().split()), key=len, reverse=True): | |
| if len(word) < 4: | |
| continue | |
| runs = _memory().recall(word) | |
| if runs: | |
| items = ", ".join(runs[0]["line_items"][:3]) | |
| return f"Similar past job: “{runs[0]['transcript']}” ({items})" | |
| return "" | |
| def resume_estimate_stream(value, thread_id: str = "ui"): | |
| """Resume a paused run with the human-supplied value; continue streaming to completion.""" | |
| run = _RUNS.get(thread_id) | |
| if run is None: | |
| yield {"type": "estimate", "estimate": None} | |
| return | |
| yield from _drive(run["agent"], Command(resume=value), thread_id) | |