feat: add authenticated engine processing boundary

This commit is contained in:
Repinoid
2026-10-31 17:36:55 +03:00
parent 193f0b4069
commit e2ba9ab451
9 changed files with 378 additions and 81 deletions
+46
View File
@@ -0,0 +1,46 @@
"""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,
)
dossier_result = dossier_builder(batch_id) if extraction.get("ok") else {"ok": False}
classes = extraction.get("classes", {})
math_checks_passed = sum(
1 for result in extraction.get("results", [])
if (result.get("extraction") or {}).get("math_validation", {}).get("status") == "VERIFIED"
)
return {
"ok": bool(extraction.get("ok")) and bool(dossier_result.get("ok")),
"v3": True,
"done": extraction.get("done", 0),
"total": len(documents),
"failed": extraction.get("failed", 0),
"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", []),
}
+4 -3
View File
@@ -252,7 +252,7 @@ def extract_document(document, llm_client=None, batch_id=None):
"""Извлечь паспорт одного документа, изолируя ошибки на уровне документа."""
document_id = document["id"]
batch_id = batch_id if batch_id is not None else document.get("batch_id")
elements = _decode_elements(document.get("elements_json"))
elements = _decode_elements(document.get("elements", document.get("elements_json")))
structured_text = format_elements_with_structure(elements)
raw_response = "{}"
try:
@@ -306,13 +306,14 @@ def extract_document(document, llm_client=None, batch_id=None):
return {"ok": False, "document_id": document_id, "error": str(error)}
def extract_batch(batch_id, llm_client=None, progress_callback=None):
def extract_batch(batch_id, llm_client=None, progress_callback=None, documents=None):
"""Обработать документы batch и вернуть сводку без batch-wide failure.
Optional progress callback получает (completed_count, document_result) после
каждого документа; ошибки callback не должны останавливать обработку batch.
"""
documents = db_docs.list_for_extraction(batch_id)
if documents is None:
documents = db_docs.list_for_extraction(batch_id)
results = []
for completed, document in enumerate(documents, start=1):
result = extract_document(document, llm_client=llm_client, batch_id=batch_id)
+7
View File
@@ -15,6 +15,7 @@ from db import supplements as db_supplements
from db import dossiers as db_dossiers
from db import exceptions as db_exceptions
from db import extractions as db_extractions
from db.connection import transaction
_HOMOGLYPHS = str.maketrans({
@@ -164,6 +165,12 @@ def _dossier_exception(batch_id, document_id, title, source_quote):
def build_dossiers_for_batch(batch_id):
"""Atomically rebuild the dossier graph, retaining the old graph on failure."""
with transaction():
return _build_dossiers_for_batch(batch_id)
def _build_dossiers_for_batch(batch_id):
"""Построить и сохранить dossier graph из паспортов документов одного batch.
Полные номера и ссылки образуют рёбра только при совместимом контрагенте.