P3+P4 app: persist audit gate (extraction_meta) + digital-text endpoint
_run_service_extract_merge now records extraction_meta {engine, slug, audit
(cover-total reconciliation), counts} on the template (migration 76) — a durable
trust signal the setup UI shows. New GET /templates/{id}/digital-text proxies the
service's /api/replica for the paper (resolved via extraction_meta.slug), returning
the digital-replica markdown. exam_extract.get_replica client added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7d1876b799
commit
544d858f62
@ -40,6 +40,19 @@ def _get(base: str, slug: str, timeout: int = 30) -> Dict[str, Any]:
|
|||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def get_replica(slug: str, timeout: int = 30) -> Dict[str, Any]:
|
||||||
|
"""The digital-replica markdown for a paper (P4): {slug, title, n_questions, total_marks, markdown,
|
||||||
|
questions:[{label, marks, markdown}]}. Raises ExtractError if the paper has no replica yet."""
|
||||||
|
base = service_url()
|
||||||
|
if not base:
|
||||||
|
raise ExtractError("EXAM_EXTRACT_URL not configured")
|
||||||
|
r = requests.get(f"{base}/api/replica/{slug}", timeout=timeout)
|
||||||
|
if r.status_code == 404:
|
||||||
|
raise ExtractError(f"no digital replica for {slug}")
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def extract_suggestions(slug: str, pdf_bytes: bytes, *, force: bool = False,
|
def extract_suggestions(slug: str, pdf_bytes: bytes, *, force: bool = False,
|
||||||
poll_timeout: int = 1500, poll_interval: int = 5) -> Dict[str, Any]:
|
poll_timeout: int = 1500, poll_interval: int = 5) -> Dict[str, Any]:
|
||||||
"""POST the paper to the service and poll until the analyse contract is ready.
|
"""POST the paper to the service and poll until the analyse contract is ready.
|
||||||
|
|||||||
@ -793,9 +793,17 @@ def _run_service_extract_merge(ctx: ExamContext, template_id: str, pdf_bytes: by
|
|||||||
contract = exam_extract.extract_suggestions(slug, pdf_bytes)
|
contract = exam_extract.extract_suggestions(slug, pdf_bytes)
|
||||||
rows = _map_service_contract_to_rows(template_id, contract, pdf_bytes)
|
rows = _map_service_contract_to_rows(template_id, contract, pdf_bytes)
|
||||||
_refresh_ai_rows(ctx, template_id, rows)
|
_refresh_ai_rows(ctx, template_id, rows)
|
||||||
n_pages = (contract.get("meta") or {}).get("n_pages") or (contract.get("meta") or {}).get("pages")
|
meta = contract.get("meta") or {}
|
||||||
|
# P3: record provenance + the audit cover-reconciliation gate so the setup UI can flag under-reads,
|
||||||
|
# and store the slug so the digital-text view can resolve this paper's replica.
|
||||||
|
updates: Dict[str, Any] = {"extraction_meta": {
|
||||||
|
"engine": "extract-service", "slug": slug, "audit": meta.get("audit") or {},
|
||||||
|
"counts": {k: len(v) for k, v in rows.items()},
|
||||||
|
}}
|
||||||
|
n_pages = meta.get("n_pages") or meta.get("pages")
|
||||||
if n_pages:
|
if n_pages:
|
||||||
ctx.supabase.table("exam_templates").update({"page_count": n_pages}).eq("id", template_id).execute()
|
updates["page_count"] = n_pages
|
||||||
|
ctx.supabase.table("exam_templates").update(updates).eq("id", template_id).execute()
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
@ -1034,6 +1042,24 @@ async def auto_map_status(
|
|||||||
return body
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/templates/{template_id}/digital-text")
|
||||||
|
async def template_digital_text(
|
||||||
|
template_id: str,
|
||||||
|
ctx: ExamContext = Depends(get_exam_context),
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""P4: the digital-replica markdown for this template's paper (stem text, parts, marks, answer-space
|
||||||
|
placeholders, spec refs). Resolves the paper via the slug the extraction used."""
|
||||||
|
template = _fetch_template_or_404(ctx, template_id)
|
||||||
|
_require_source_visibility_or_404(ctx, template)
|
||||||
|
if not exam_extract.is_enabled():
|
||||||
|
raise HTTPException(status_code=503, detail="Extraction service not configured")
|
||||||
|
slug = (template.get("extraction_meta") or {}).get("slug") or _extract_slug(ctx, template)
|
||||||
|
try:
|
||||||
|
return exam_extract.get_replica(slug)
|
||||||
|
except exam_extract.ExtractError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=f"No digital text yet — run auto-map first ({exc})")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/templates/{template_id}")
|
@router.put("/templates/{template_id}")
|
||||||
async def replace_template(
|
async def replace_template(
|
||||||
template_id: str,
|
template_id: str,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user