diff --git a/modules/services/exam_extract.py b/modules/services/exam_extract.py index 583c31b..4b87472 100644 --- a/modules/services/exam_extract.py +++ b/modules/services/exam_extract.py @@ -40,6 +40,19 @@ def _get(base: str, slug: str, timeout: int = 30) -> Dict[str, Any]: 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, 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. diff --git a/routers/exam/templates.py b/routers/exam/templates.py index 40d3394..72746e6 100644 --- a/routers/exam/templates.py +++ b/routers/exam/templates.py @@ -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) rows = _map_service_contract_to_rows(template_id, contract, pdf_bytes) _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: - 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 @@ -1034,6 +1042,24 @@ async def auto_map_status( 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}") async def replace_template( template_id: str,