Quillwright / quillwright /api /document.py
Aarya2004
Deploy: sync hosted Space to local app (chat, document capture, Modal backends, pages, mobile/QR)
47b2a99
Raw
History Blame Contribute Delete
2.63 kB
"""Document Capture: a handed-over document -> Observations + Proposed Line Items (ADR-0011).
Thin adapter over the Extraction role (Nemotron Parse on Modal): file path in, JSON
the frontend confirm card renders out. The model is injectable for tests. In
production it resolves to ParseModel when FF_MODAL_PARSE_URL is set; otherwise a
deterministic demo parse runs the REAL blocks_to_pipeline logic over a canned
supplier quote, so the stub Space demos the flow with zero models (the same
honest-scaffolding pattern as _stub_perception in api/estimate.py).
"""
import os
from quillwright.backends.parse import blocks_to_pipeline
# The canned supplier quote the demo "reads" when no Modal Parse endpoint is
# configured. Mirrors the test fixture in test_parse_backend.py.
_DEMO_BLOCKS = [
{"class": "Title", "bbox": [], "text": "ACME HVAC Supply — Quote #1042"},
{
"class": "Table",
"bbox": [],
"text": (
"| Item | Qty | Unit Price |\n"
"| --- | --- | --- |\n"
"| Dual run capacitor | 2 | $42.50 |\n"
"| Compressor contactor | 1 | $28.00 |\n"
"| R-410A refrigerant | 4 | $30.00 |\n"
),
},
{"class": "Text", "bbox": [], "text": "Net 30 terms. Prices valid 30 days."},
]
def _resolve_extraction():
"""Real Nemotron Parse when its Modal URL is configured; else None (demo parse)."""
if os.environ.get("FF_MODAL_PARSE_URL"):
from quillwright.resolver import ModelResolver
return ModelResolver(mode="best", backend="modal").for_role("extraction")
return None
def parse_document_capture(path: str, model=None) -> dict:
"""Parse the document at `path`; return {model, observations, proposed_items}.
Every price stays *proposed* — the human confirms it in the UI before it becomes
a LineItem with price_source="document" (Facts-from-Tools, ADR-0004/0011).
"""
parser = model if model is not None else _resolve_extraction()
if parser is None:
observations, proposed = blocks_to_pipeline(_DEMO_BLOCKS)
name = "parse-stub (demo quote)"
else:
observations, proposed = parser.parse_document(path)
name = parser.name
return {
"model": name,
"observations": [{"kind": o.kind, "text": o.text} for o in observations],
"proposed_items": [
{
"description": p.description,
"quantity": p.quantity,
"unit": p.unit,
"rate": p.rate,
"source_text": p.source_text,
}
for p in proposed
],
}