Fix comparison pipeline event and transaction handling
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
"""Конфигурация приложения — все настройки в одном месте."""
|
||||
import os
|
||||
|
||||
VERSION = "2.0.20"
|
||||
VERSION = "2.0.21"
|
||||
|
||||
LLM_URL = os.getenv("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
|
||||
LLM_KEY = os.getenv("LLM_API_KEY", "")
|
||||
|
||||
@@ -10,12 +10,29 @@ import sqlite3
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
DB_PATH = "/tmp/contracts.db"
|
||||
_db_session_key = None
|
||||
_local = threading.local()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def transaction():
|
||||
"""Run DB writes atomically on the current thread connection."""
|
||||
conn = get_conn()
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
_local.in_transaction = True
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
_local.in_transaction = False
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Создать/пересоздать БД + схему. Вызывается при старте и после cleanup."""
|
||||
global _db_session_key
|
||||
@@ -171,6 +188,7 @@ def execute(sql, params=None):
|
||||
conn = get_conn()
|
||||
sql = _pg_to_sqlite(sql)
|
||||
cur = conn.execute(sql, params or [])
|
||||
if not getattr(_local, "in_transaction", False):
|
||||
conn.commit()
|
||||
return cur.rowcount
|
||||
|
||||
|
||||
+30
-15
@@ -1,6 +1,6 @@
|
||||
"""spec_events — event sourcing: apply ops, reset contract."""
|
||||
import json, uuid
|
||||
from db.connection import query, execute, get_conn
|
||||
from db.connection import query, execute, get_conn, transaction
|
||||
|
||||
|
||||
def reset(contract_id):
|
||||
@@ -27,9 +27,15 @@ def get_next_seq(contract_id):
|
||||
|
||||
def apply_ops(contract_id, supplement_id, document_id, ops, prompt_id, raw_llm_response):
|
||||
"""Apply ADD/UPDATE/DELETE ops. Returns summary dict."""
|
||||
with transaction():
|
||||
return _apply_ops(contract_id, supplement_id, document_id, ops, prompt_id, raw_llm_response)
|
||||
|
||||
|
||||
def _apply_ops(contract_id, supplement_id, document_id, ops, prompt_id, raw_llm_response):
|
||||
added = 0
|
||||
updated = 0
|
||||
deleted = 0
|
||||
unresolved = 0
|
||||
seq = get_next_seq(contract_id)
|
||||
|
||||
for op in ops:
|
||||
@@ -42,6 +48,7 @@ def apply_ops(contract_id, supplement_id, document_id, ops, prompt_id, raw_llm_r
|
||||
# ADD without name → UNRESOLVED
|
||||
_log_unresolved(contract_id, supplement_id, seq, op, prompt_id, document_id, raw_llm_response, "ADD with empty name")
|
||||
seq += 1
|
||||
unresolved += 1
|
||||
continue
|
||||
name_hash = _hash(name, nr.get("date_start"))
|
||||
execute(
|
||||
@@ -65,6 +72,7 @@ def apply_ops(contract_id, supplement_id, document_id, ops, prompt_id, raw_llm_r
|
||||
if not th:
|
||||
_log_unresolved(contract_id, supplement_id, seq, op, prompt_id, document_id, raw_llm_response, "UPDATE with empty target_hash")
|
||||
seq += 1
|
||||
unresolved += 1
|
||||
continue
|
||||
execute(
|
||||
"""INSERT INTO spec_events (id, contract_id, supplement_id, seq, action, target_hash,
|
||||
@@ -86,6 +94,7 @@ def apply_ops(contract_id, supplement_id, document_id, ops, prompt_id, raw_llm_r
|
||||
if not th:
|
||||
_log_unresolved(contract_id, supplement_id, seq, op, prompt_id, document_id, raw_llm_response, "DELETE with empty target_hash")
|
||||
seq += 1
|
||||
unresolved += 1
|
||||
continue
|
||||
execute(
|
||||
"""INSERT INTO spec_events (id, contract_id, supplement_id, seq, action, target_hash,
|
||||
@@ -104,27 +113,19 @@ def apply_ops(contract_id, supplement_id, document_id, ops, prompt_id, raw_llm_r
|
||||
|
||||
elif action == "UNRESOLVED":
|
||||
# Log but don't apply
|
||||
execute(
|
||||
"""INSERT INTO spec_events (id, contract_id, supplement_id, seq, action, target_hash,
|
||||
new_values, comment, status, prompt_version, source_document_id, raw_llm_response)
|
||||
VALUES (%s, %s, %s, %s, 'UNRESOLVED', %s, %s, %s, 'unresolved', %s, %s, %s)""",
|
||||
(
|
||||
str(uuid.uuid4()), contract_id, supplement_id, seq,
|
||||
op.get("target_hash", ""),
|
||||
json.dumps(op.get("new_values", {}), ensure_ascii=False),
|
||||
op.get("reason", op.get("comment", "")),
|
||||
prompt_id, document_id,
|
||||
json.dumps(raw_llm_response, ensure_ascii=False),
|
||||
),
|
||||
)
|
||||
_log_unresolved(contract_id, supplement_id, seq, op, prompt_id, document_id, raw_llm_response,
|
||||
op.get("reason", op.get("comment", "")))
|
||||
seq += 1
|
||||
unresolved += 1
|
||||
|
||||
else:
|
||||
# Unknown action — log as UNRESOLVED
|
||||
_log_unresolved(contract_id, supplement_id, seq, op, prompt_id, document_id, raw_llm_response,
|
||||
f"unknown action: {action}")
|
||||
seq += 1
|
||||
unresolved += 1
|
||||
|
||||
return {"added": added, "updated": updated, "deleted": deleted}
|
||||
return {"added": added, "updated": updated, "deleted": deleted, "unresolved": unresolved}
|
||||
|
||||
|
||||
def _log_unresolved(contract_id, supplement_id, seq, op, prompt_id, document_id, raw_llm_response, reason):
|
||||
@@ -187,6 +188,20 @@ def _update_spec_current(contract_id, name_hash, new_values):
|
||||
sets.append(f"{field} = %s")
|
||||
params.append(new_values[field])
|
||||
if sets:
|
||||
if "name" in new_values or "date_start" in new_values:
|
||||
updated_name = new_values.get("name")
|
||||
updated_date = new_values.get("date_start")
|
||||
if updated_name is None or updated_date is None:
|
||||
current = query(
|
||||
"SELECT name, date_start FROM spec_current WHERE contract_id = %s AND name_hash = %s",
|
||||
(contract_id, name_hash),
|
||||
)
|
||||
if current:
|
||||
updated_name = updated_name if updated_name is not None else current[0]["name"]
|
||||
updated_date = updated_date if updated_date is not None else current[0]["date_start"]
|
||||
if updated_name is not None:
|
||||
sets.append("name_hash = %s")
|
||||
params.append(_hash(updated_name, updated_date))
|
||||
sets.append("updated_at = datetime('now')")
|
||||
params.extend([contract_id, name_hash])
|
||||
execute(
|
||||
|
||||
@@ -77,6 +77,15 @@ def run_pipeline(contract_id, order_ids, build_prompt_fn):
|
||||
ops = result.get("ops", [])
|
||||
mode = result.get("mode", "llm")
|
||||
|
||||
if mode == "full_replace" and not ops:
|
||||
yield {
|
||||
"type": "extract_error",
|
||||
"supplement_id": sid,
|
||||
"filename": filename,
|
||||
"error": "full_replace returned empty ops",
|
||||
}
|
||||
continue
|
||||
|
||||
# Трансляция target_id ("r1","r2"...) → target_hash (name_hash из current_spec).
|
||||
# LLM возвращает target_id, а apply_ops() читает target_hash — без этого UPDATE/DELETE уходят в UNRESOLVED.
|
||||
for _op in ops:
|
||||
@@ -120,7 +129,7 @@ def run_pipeline(contract_id, order_ids, build_prompt_fn):
|
||||
}
|
||||
|
||||
# Arithmetic check
|
||||
check_arithmetic(ops)
|
||||
arithmetic_warnings = check_arithmetic(ops)
|
||||
|
||||
yield {
|
||||
"type": "applied",
|
||||
@@ -128,6 +137,7 @@ def run_pipeline(contract_id, order_ids, build_prompt_fn):
|
||||
"filename": filename,
|
||||
"summary": summary,
|
||||
"ops": applied_ops,
|
||||
"arithmetic_warnings": arithmetic_warnings,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -139,7 +149,7 @@ def run_pipeline(contract_id, order_ids, build_prompt_fn):
|
||||
}
|
||||
|
||||
total_time = round(time.time() - t0, 1)
|
||||
yield {"type": "complete", "total_time_s": total_time}
|
||||
yield {"type": "done", "total_time_s": total_time}
|
||||
|
||||
|
||||
def _elements_to_text(ej):
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<img src="/static/logo.svg" alt="Nubes">
|
||||
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.20</span></span>
|
||||
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v2.0.21</span></span>
|
||||
<div id="pipelineStepper" style="display:flex;gap:8px;font-size:11px;align-items:center;color:var(--muted);">
|
||||
<span id="stepUpload">○ Загрузка</span><span>→</span>
|
||||
<span id="stepClassify">○ Классификация</span><span>→</span>
|
||||
@@ -224,11 +224,11 @@
|
||||
import { listZipFiles } from '/upload/zip/list_zip_files.js';
|
||||
window.listZipFiles = listZipFiles;
|
||||
</script>
|
||||
<script src="/static/state.js?v=2.0.20"></script>
|
||||
<script src="/static/app_utils.js?v=2.0.20"></script>
|
||||
<script src="/static/files.js?v=2.0.20"></script>
|
||||
<script src="/static/groups.js?v=2.0.20"></script>
|
||||
<script src="/static/compare.js?v=2.0.20"></script>
|
||||
<script src="/static/app.js?v=2.0.20"></script>
|
||||
<script src="/static/state.js?v=2.0.21"></script>
|
||||
<script src="/static/app_utils.js?v=2.0.21"></script>
|
||||
<script src="/static/files.js?v=2.0.21"></script>
|
||||
<script src="/static/groups.js?v=2.0.21"></script>
|
||||
<script src="/static/compare.js?v=2.0.21"></script>
|
||||
<script src="/static/app.js?v=2.0.21"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestApplyOpsAdd:
|
||||
def test_add(self, db):
|
||||
ops = [{"action": "ADD", "new_row": {"name": "Аренда стойко-места", "price": 50000, "qty": 1, "sum": 50000, "date_start": "2025-01-01"}}]
|
||||
s = spec_events.apply_ops(CID, SID, DID, ops, "pid", {})
|
||||
assert s == {"added": 1, "updated": 0, "deleted": 0}
|
||||
assert s == {"added": 1, "updated": 0, "deleted": 0, "unresolved": 0}
|
||||
rows = spec_current.list_by_contract(CID)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["name"] == "Аренда стойко-места"
|
||||
@@ -27,7 +27,7 @@ class TestApplyOpsAdd:
|
||||
def test_add_empty_name_unresolved(self, db):
|
||||
ops = [{"action": "ADD", "new_row": {"name": ""}}]
|
||||
s = spec_events.apply_ops(CID, SID, DID, ops, "pid", {})
|
||||
assert s == {"added": 0, "updated": 0, "deleted": 0}
|
||||
assert s == {"added": 0, "updated": 0, "deleted": 0, "unresolved": 1}
|
||||
assert spec_current.list_by_contract(CID) == []
|
||||
|
||||
def test_add_multiple(self, db):
|
||||
|
||||
Reference in New Issue
Block a user