feat: add v3 extraction storage and structured parsing
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"""CRUD для результатов stateless-извлечения отдельных документов.
|
||||
|
||||
Исходный документ и результат LLM разделены намеренно: повторный запуск
|
||||
извлечения может обновить паспорт, не затрагивая байты файла и его структуру.
|
||||
Составные поля сохраняются как JSON-текст, поскольку SQLite не требует
|
||||
отдельного JSON-типа и существующий слой БД уже использует такой подход.
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from db.connection import execute, query
|
||||
|
||||
|
||||
_JSON_FIELDS = (
|
||||
"referenced_numbers_json",
|
||||
"sections_json",
|
||||
"math_validation_json",
|
||||
)
|
||||
_FIELDS = (
|
||||
"doc_class",
|
||||
"intent_mode",
|
||||
"own_number",
|
||||
"own_date",
|
||||
"counterparty_name",
|
||||
"counterparty_inn",
|
||||
*_JSON_FIELDS,
|
||||
"status",
|
||||
"prompt_version",
|
||||
"raw_llm_response",
|
||||
)
|
||||
|
||||
|
||||
def _encode_json(value):
|
||||
"""Сериализовать значения в JSON, сохраняя Decimal как точную строку."""
|
||||
return json.dumps(value, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
def _decode_row(row):
|
||||
"""Вернуть составные поля паспорта как Python-объекты для сервисов."""
|
||||
if not row:
|
||||
return None
|
||||
result = dict(row)
|
||||
for field in _JSON_FIELDS:
|
||||
raw_value = result.get(field)
|
||||
try:
|
||||
result[field.removesuffix("_json")] = json.loads(raw_value or "null")
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
result[field.removesuffix("_json")] = None
|
||||
return result
|
||||
|
||||
|
||||
def upsert_extraction(document_id, data):
|
||||
"""Создать или заменить паспорт документа, не меняя его стабильный ID.
|
||||
|
||||
Входные JSON-поля принимают как Python-списки/словари, так и уже
|
||||
сериализованные строки. Поля, которых нет в `data`, получают безопасные
|
||||
значения по умолчанию при вставке и остаются без изменений при обновлении.
|
||||
"""
|
||||
existing = query(
|
||||
"SELECT id FROM document_extractions WHERE document_id = %s",
|
||||
(document_id,),
|
||||
)
|
||||
values = {}
|
||||
for field in _FIELDS:
|
||||
if field not in data:
|
||||
continue
|
||||
value = data[field]
|
||||
if field in _JSON_FIELDS and not isinstance(value, str):
|
||||
value = _encode_json(value)
|
||||
values[field] = value
|
||||
|
||||
if existing:
|
||||
if values:
|
||||
assignments = ", ".join(f"{field} = %s" for field in values)
|
||||
params = [*values.values(), document_id]
|
||||
execute(
|
||||
f"UPDATE document_extractions SET {assignments}, "
|
||||
"updated_at = datetime('now') WHERE document_id = %s",
|
||||
params,
|
||||
)
|
||||
return get_by_document(document_id)
|
||||
|
||||
extraction_id = str(uuid.uuid4())
|
||||
columns = ["id", "document_id", *values.keys()]
|
||||
placeholders = ", ".join(["%s"] * len(columns))
|
||||
execute(
|
||||
f"INSERT INTO document_extractions ({', '.join(columns)}) "
|
||||
f"VALUES ({placeholders})",
|
||||
[extraction_id, document_id, *values.values()],
|
||||
)
|
||||
return get_by_document(document_id)
|
||||
|
||||
|
||||
def get_by_document(document_id):
|
||||
"""Получить паспорт документа или `None`, если он ещё не извлечён."""
|
||||
rows = query(
|
||||
"SELECT * FROM document_extractions WHERE document_id = %s",
|
||||
(document_id,),
|
||||
)
|
||||
return _decode_row(rows[0]) if rows else None
|
||||
|
||||
|
||||
def list_by_batch(batch_id):
|
||||
"""Получить паспорта всех документов batch в порядке загрузки."""
|
||||
rows = query(
|
||||
"""SELECT e.* FROM document_extractions e
|
||||
JOIN documents d ON d.id = e.document_id
|
||||
WHERE d.batch_id = %s ORDER BY d.created_at, d.id""",
|
||||
(batch_id,),
|
||||
)
|
||||
return [_decode_row(row) for row in rows]
|
||||
Reference in New Issue
Block a user