Files
contracts-flask/site/services/engine.py
T
Repinoid 79de42da96
Deploy contracts-flask / validate (push) Canceled after 0s
fix: harden extraction batch handling
2026-10-31 19:06:34 +03:00

82 lines
3.5 KiB
Python

"""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", []),
}