revert: app/ → site/ (drhider proves site/ works fine)
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
"""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 API_KEY, LLM_URL, LLM_KEY, LLM_MODEL
|
||||
import httpx
|
||||
|
||||
api_bp = Blueprint("api", __name__)
|
||||
|
||||
|
||||
def _require_auth():
|
||||
"""Проверка API-ключа для опасных операций."""
|
||||
if API_KEY:
|
||||
key = request.headers.get("X-Api-Key", "")
|
||||
if key != API_KEY:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ── 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-ключ) ────────────────────────────────────
|
||||
|
||||
@api_bp.route("/api/cleanup", methods=["POST"])
|
||||
def api_cleanup():
|
||||
"""Полная очистка всех данных (кроме prompts)."""
|
||||
if not _require_auth():
|
||||
return jsonify(ok=False, error="unauthorized"), 401
|
||||
|
||||
execute("DELETE FROM spec_current")
|
||||
execute("DELETE FROM spec_events")
|
||||
execute("DELETE FROM supplements")
|
||||
execute("DELETE FROM upload_chunks")
|
||||
execute("DELETE FROM documents")
|
||||
execute("DELETE FROM contracts")
|
||||
return jsonify(ok=True, message="all data cleaned")
|
||||
Reference in New Issue
Block a user