"""Shared orchestration for the contract reconstruction engine. This module deliberately accepts the batch's persistence-backed extractor and dossier builder as dependencies. Both the browser workflow and the JSON integration boundary therefore execute the same business flow in-process, while tests can replace either dependency without constructing HTTP calls or making network requests. """ def analyze_batch(batch_id, documents, extractor, dossier_builder, progress_callback=None, llm_client=None): """Extract document passports, assemble dossiers, and shape the API result. `extractor` owns LLM interaction and persistence of each extraction; `dossier_builder` consumes those saved passports and persists the resulting graph. Keeping the calls in this order is an invariant: grouping must never observe a partially extracted batch. The callback remains optional because small batches run synchronously, while the existing large-batch route uses it to update persistent progress. """ extraction = extractor( batch_id, llm_client=llm_client, progress_callback=progress_callback, documents=documents, ) # Extractor-level `ok` means every input succeeded; dossier construction only # requires at least one persisted passport and can proceed for that subset. extraction_results = extraction.get("results") or [] total = extraction.get("total", len(documents)) done = extraction.get("done") if done is None: done = sum(1 for result in extraction_results if result.get("ok")) failed = extraction.get("failed") if failed is None: failed = max(total - done, 0) dossier_result = dossier_builder(batch_id) if done > 0 else {"ok": False} dossier_ok = bool(dossier_result.get("ok")) # Keep result usability separate from completeness so clients can continue # with valid dossiers while still reporting failures explicitly. status = ( "failed" if done == 0 or not dossier_ok else "partial" if failed > 0 else "completed" ) documents_by_id = {document.get("id"): document for document in documents} failed_documents = [] for result in extraction_results: if result.get("ok"): continue document_id = result.get("document_id") source_document = documents_by_id.get(document_id, {}) failure = { "document_id": document_id, "filename": source_document.get("filename"), "error": result.get("error") or "extraction failed", } if result.get("persistence_error"): failure["persistence_error"] = result["persistence_error"] failed_documents.append(failure) classes = extraction.get("classes", {}) math_checks_passed = sum( 1 for result in extraction_results if (result.get("extraction") or {}).get("math_validation", {}).get("status") == "VERIFIED" ) return { "ok": status != "failed", "status": status, "partial": status == "partial", "v3": True, "done": done, "total": total, "failed": failed, "failed_documents": failed_documents, "classes": classes, "ignored_accounting": classes.get("ignored_accounting", 0), "math_checks_passed": math_checks_passed, "dossier_count": len(dossier_result.get("dossiers", [])), "ambiguous": dossier_result.get("ambiguous", 0), "unlinked": dossier_result.get("unlinked", []), }