183 lines
7.4 KiB
Python
183 lines
7.4 KiB
Python
"""API blueprint — groups, documents, supplements, sync, cleanup, spec-current, chat."""
|
||
from flask import Blueprint, request, jsonify
|
||
from .db import documents, supplements, spec_current
|
||
from .db.connection import execute, query
|
||
from .services.grouping import group_documents, apply_groups
|
||
from .config import LLM_URL, LLM_KEY, LLM_MODEL
|
||
import httpx
|
||
|
||
api_bp = Blueprint("api", __name__)
|
||
|
||
|
||
# ── Supplements ──────────────────────────────────────────────────
|
||
|
||
@api_bp.route("/api/supplements")
|
||
def api_supplements():
|
||
cid = request.args.get("contract_id")
|
||
if not cid:
|
||
return jsonify(ok=False, error="contract_id required"), 400
|
||
rows = supplements.list_by_contract(cid)
|
||
return jsonify(ok=True, supplements=rows)
|
||
|
||
|
||
# ── Documents ────────────────────────────────────────────────────
|
||
|
||
@api_bp.route("/api/documents/<doc_id>")
|
||
def api_document(doc_id):
|
||
doc = documents.get(doc_id)
|
||
if not doc:
|
||
return jsonify(ok=False, error="not found"), 404
|
||
return jsonify(ok=True, **{
|
||
k: doc.get(k) for k in [
|
||
"id", "filename", "status", "elements_json", "doc_type",
|
||
"own_number", "parent_number", "doc_date", "counterparty",
|
||
"classify_status", "classify_raw", "classify_input",
|
||
]
|
||
})
|
||
|
||
|
||
@api_bp.route("/api/documents/<doc_id>", methods=["DELETE"])
|
||
def api_document_delete(doc_id):
|
||
supps = query("SELECT id, contract_id FROM supplements WHERE document_id = %s", (doc_id,))
|
||
for s in (supps or []):
|
||
execute(
|
||
"""DELETE FROM spec_current WHERE contract_id = %s
|
||
AND last_event_id IN (SELECT id FROM spec_events WHERE supplement_id = %s)""",
|
||
(s["contract_id"], s["id"]),
|
||
)
|
||
execute("DELETE FROM spec_events WHERE supplement_id = %s", (s["id"],))
|
||
execute("DELETE FROM supplements WHERE id = %s", (s["id"],))
|
||
execute("DELETE FROM documents WHERE id = %s", (doc_id,))
|
||
return jsonify(ok=True)
|
||
|
||
|
||
# ── Sync ─────────────────────────────────────────────────────────
|
||
|
||
@api_bp.route("/api/sync", methods=["POST"])
|
||
def api_sync():
|
||
"""Удалить документы, не входящие в keep_ids."""
|
||
body = request.get_json()
|
||
keep_ids = set(body.get("keep_ids", []))
|
||
if len(keep_ids) > 1000:
|
||
return jsonify(ok=False, error="too many keep_ids (max 1000)"), 400
|
||
|
||
docs = query("SELECT id FROM documents", ())
|
||
deleted = 0
|
||
for d in (docs or []):
|
||
if d["id"] in keep_ids:
|
||
continue
|
||
supps = query("SELECT id, contract_id FROM supplements WHERE document_id = %s", (d["id"],))
|
||
for s in (supps or []):
|
||
execute(
|
||
"""DELETE FROM spec_current WHERE contract_id = %s
|
||
AND last_event_id IN (SELECT id FROM spec_events WHERE supplement_id = %s)""",
|
||
(s["contract_id"], s["id"]),
|
||
)
|
||
execute("DELETE FROM spec_events WHERE supplement_id = %s", (s["id"],))
|
||
execute("DELETE FROM supplements WHERE id = %s", (s["id"],))
|
||
execute("DELETE FROM documents WHERE id = %s", (d["id"],))
|
||
deleted += 1
|
||
|
||
execute("DELETE FROM contracts WHERE id NOT IN (SELECT DISTINCT contract_id FROM supplements)")
|
||
return jsonify(ok=True, deleted=deleted)
|
||
|
||
|
||
# ── Groups ───────────────────────────────────────────────────────
|
||
|
||
@api_bp.route("/api/groups")
|
||
def api_groups():
|
||
batch_id = request.args.get("batch")
|
||
if not batch_id:
|
||
return jsonify(ok=False, error="batch required"), 400
|
||
result = group_documents(batch_id)
|
||
return jsonify(result)
|
||
|
||
|
||
@api_bp.route("/api/batch-progress")
|
||
def api_batch_progress():
|
||
batch_id = request.args.get("batch")
|
||
if not batch_id:
|
||
return jsonify(ok=False, error="batch required"), 400
|
||
counts = documents.count_by_status(batch_id)
|
||
docs = documents.list_by_batch(batch_id)
|
||
return jsonify(ok=True, counts=counts, total=len(docs))
|
||
|
||
|
||
@api_bp.route("/api/apply-groups", methods=["POST"])
|
||
def api_apply_groups():
|
||
body = request.get_json()
|
||
batch_id = body.get("batch_id")
|
||
groups = body.get("groups", [])
|
||
if not batch_id:
|
||
return jsonify(ok=False, error="batch_id required"), 400
|
||
result = apply_groups(batch_id, groups)
|
||
return jsonify(result)
|
||
|
||
|
||
# ── Spec current ─────────────────────────────────────────────────
|
||
|
||
@api_bp.route("/api/spec-current")
|
||
def api_spec_current():
|
||
cid = request.args.get("contract_id")
|
||
if not cid:
|
||
return jsonify(ok=False, error="contract_id required"), 400
|
||
rows = spec_current.list_by_contract(cid)
|
||
return jsonify(ok=True, rows=rows)
|
||
|
||
|
||
# ── Chat (совместимость с Lucee /chat.cfm) ──────────────────────
|
||
|
||
@api_bp.route("/chat", methods=["POST"])
|
||
@api_bp.route("/chat.cfm", methods=["POST"])
|
||
def chat():
|
||
"""Чат с LLM по данным спецификации. Совместим с Lucee-форматом ответа."""
|
||
contract_id = request.args.get("contract_id", "")
|
||
question = request.form.get("question", "")
|
||
if not contract_id or not question:
|
||
return jsonify(OK=False, ERROR="contract_id and question required")
|
||
|
||
rows = spec_current.list_by_contract(contract_id)
|
||
if not rows:
|
||
return jsonify(OK=True, ANSWER="Нет данных. Сначала запустите сравнение.")
|
||
|
||
ctx = "\n".join(
|
||
f"{r.get('name','')} | цена={r.get('price')} | объём={r.get('qty')} | "
|
||
f"сумма={r.get('sum')} | начало={r.get('date_start')}"
|
||
for r in rows
|
||
)
|
||
prompt = (
|
||
f"Ты — анализатор договоров. Данные:\n{ctx}\n\n"
|
||
f"Вопрос: {question}\nОтветь кратко, только по данным."
|
||
)
|
||
|
||
try:
|
||
with httpx.Client(http2=True, timeout=120) as client:
|
||
resp = client.post(
|
||
LLM_URL,
|
||
json={
|
||
"model": LLM_MODEL,
|
||
"messages": [{"role": "user", "content": prompt}],
|
||
"max_tokens": 1000,
|
||
"temperature": 0.1,
|
||
},
|
||
headers={
|
||
"Authorization": f"Bearer {LLM_KEY}",
|
||
"Content-Type": "application/json",
|
||
},
|
||
)
|
||
resp.raise_for_status()
|
||
answer = resp.json()["choices"][0]["message"]["content"]
|
||
return jsonify(OK=True, ANSWER=answer)
|
||
except Exception as e:
|
||
return jsonify(OK=False, ERROR=f"LLM error: {e}")
|
||
|
||
|
||
# ── Cleanup (атомарное удаление БД) ──────────────────────────────
|
||
|
||
@api_bp.route("/api/cleanup", methods=["POST"])
|
||
def api_cleanup():
|
||
"""Полная очистка: os.remove(DB) + init новой. Данные гарантированно стёрты."""
|
||
from .db.connection import cleanup_db
|
||
cleanup_db()
|
||
return jsonify(ok=True, message="all data cleaned")
|