Files
contracts-flask/site/routes/api_bp.py
T
2026-07-15 11:23:13 +04:00

183 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""API blueprint — groups, documents, supplements, sync, cleanup, spec-current, chat."""
from flask import Blueprint, request, jsonify
from site.db import documents, supplements, spec_current
from site.db.connection import execute, query
from site.services.grouping import group_documents, apply_groups
from site.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 site.db.connection import cleanup_db
cleanup_db()
return jsonify(ok=True, message="all data cleaned")