"""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/") 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/", 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(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")