Files
contracts-flask/site/app.py
T
naeel a2a094969b
Deploy contracts-flask / validate (push) Successful in 0s
feat: async DrHider — POST job + poll + download, v0.5/v1.0.188
2026-06-29 16:16:18 +04:00

193 lines
6.5 KiB
Python

"""contracts-flask — Flask-фронтенд для сверки договоров.
1:1 функционал с Lucee-версией.
Все тяжёлые операции — на ВМ (contracts.kube5s.ru).
"""
import os
import uuid
import threading
import httpx
from flask import Flask, render_template, request, jsonify
app = Flask(__name__)
# DrHider job store (in-memory, no persistence)
_drhider_jobs = {}
_drhider_lock = threading.Lock()
VM_API = os.environ.get("VM_API", "https://contracts.kube5s.ru")
LLM_URL = os.environ.get("LLM_API_URL", "https://api.aillm.ru/v1/chat/completions")
LLM_KEY = os.environ.get("LLM_API_KEY", "")
LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-oss-120b")
@app.route("/")
def index():
return render_template("index.html", vm_api=VM_API)
@app.route("/chat", methods=["POST"])
def chat():
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"})
try:
with httpx.Client(timeout=10) as client:
resp = client.get(f"{VM_API}/api/spec-current", params={"contract_id": contract_id})
resp.raise_for_status()
rows = resp.json().get("rows", [])
except Exception as e:
return jsonify({"ok": False, "error": f"VM error: {e}"})
if not rows:
return jsonify({"ok": True, "answer": "Нет данных. Сначала запустите сравнение."})
ctx = "\n".join(f"{r.get('name','')} | цена={r.get('price')} | объём={r.get('qty')} | сумма={r.get('sum')} | начало={r.get('date_start')}" for r in rows)
prompt = f"Ты — анализатор договоров. Данные:\n{ctx}\n\nВопрос: {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()
return jsonify({"ok": True, "answer": resp.json()["choices"][0]["message"]["content"]})
except Exception as e:
return jsonify({"ok": False, "error": f"LLM error: {e}"})
@app.route("/api/prompts", methods=["GET"])
def prompts_get():
try:
with httpx.Client(timeout=10) as client:
resp = client.get(f"{VM_API}/api/prompts", params=request.args)
return jsonify(resp.json())
except Exception as e:
return jsonify({"ok": False, "error": str(e)})
@app.route("/api/prompts/list", methods=["GET"])
def prompts_list():
try:
with httpx.Client(timeout=10) as client:
resp = client.get(f"{VM_API}/api/prompts/list")
return jsonify(resp.json())
except Exception as e:
return jsonify({"ok": False, "error": str(e)})
@app.route("/api/prompts/save", methods=["POST"])
def prompts_save():
try:
with httpx.Client(timeout=10) as client:
resp = client.post(f"{VM_API}/api/prompts/save", json=request.get_json())
return jsonify(resp.json())
except Exception as e:
return jsonify({"ok": False, "error": str(e)})
@app.route("/api/prompts/activate", methods=["POST"])
def prompts_activate():
try:
with httpx.Client(timeout=10) as client:
resp = client.post(f"{VM_API}/api/prompts/activate", json=request.get_json())
return jsonify(resp.json())
except Exception as e:
return jsonify({"ok": False, "error": str(e)})
@app.route("/architect")
def architect():
return render_template("architect.html")
@app.route("/ci-cd")
def ci_cd():
return render_template("ci-cd.html")
@app.route("/DrHider")
def drhider():
return render_template("drhider.html")
@app.route("/api/drhider", methods=["POST"])
def api_drhider():
"""Обфускация: старт фоновой задачи."""
from services.drhider import obfuscate_files
uploaded = request.files.getlist("files")
if not uploaded:
return jsonify({"ok": False, "error": "Нет файлов"}), 400
files = [(f.filename, f.read(), f.content_type or "") for f in uploaded if f.filename]
if not files:
return jsonify({"ok": False, "error": "Нет файлов"}), 400
job_id = uuid.uuid4().hex
with _drhider_lock:
_drhider_jobs[job_id] = {"done": False, "data": None, "error": None}
def _process():
try:
zip_data, _csv = obfuscate_files(files)
with _drhider_lock:
_drhider_jobs[job_id] = {"done": True, "data": zip_data, "error": None}
except Exception as e:
with _drhider_lock:
_drhider_jobs[job_id] = {"done": True, "data": None, "error": str(e)}
threading.Thread(target=_process, daemon=True).start()
return jsonify({"ok": True, "job_id": job_id})
@app.route("/api/drhider/<job_id>/status")
def api_drhider_status(job_id):
"""Статус задачи."""
with _drhider_lock:
job = _drhider_jobs.get(job_id)
if not job:
return jsonify({"ok": False, "error": "Неизвестный job_id"}), 404
return jsonify({"ok": True, "done": job["done"], "error": job["error"]})
@app.route("/api/drhider/<job_id>/download")
def api_drhider_download(job_id):
"""Скачать результат."""
from flask import send_file
import io
with _drhider_lock:
job = _drhider_jobs.get(job_id)
if not job or not job["done"]:
return jsonify({"ok": False, "error": "Не готов"}), 404
if job["error"]:
return jsonify({"ok": False, "error": job["error"]}), 500
buf = io.BytesIO(job["data"])
buf.seek(0)
# Очистка
with _drhider_lock:
_drhider_jobs.pop(job_id, None)
return send_file(buf, mimetype="application/zip", as_attachment=True, download_name="drhider_output.zip")
@app.route("/health")
def health():
return {"ok": True, "service": "contracts-flask"}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, threaded=True)
# ── Совместимость с Lucee-путями ──────────────────────────────
@app.route("/chat.cfm", methods=["POST"])
def chat_cfm():
return chat()
@app.route("/prompt.cfm", methods=["GET"])
def prompt_cfm():
return prompts_get()