Steps 3-8: config, blueprints (upload/pipeline/api/prompts/health/pages), new app.py (no VM proxy), process.py adapted to generator, VM_API=''

This commit is contained in:
2026-07-14 22:30:58 +04:00
parent e635cb855a
commit 8f688215cc
13 changed files with 733 additions and 125 deletions
+53 -117
View File
@@ -1,128 +1,64 @@
"""contracts-flask — Flask-фронтенд для сверки договоров.
1:1 функционал с Lucee-версией.
Все тяжёлые операции — на ВМ (contracts.kube5s.ru).
"""contracts-flask v2.0 — полный перенос с ВМ на Flask.
Больше никаких прокси на contracts.kube5s.ru — всё локально.
"""
import os
import httpx
from flask import Flask, render_template, request, jsonify
app = Flask(__name__)
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")
from flask import Flask
from app.config import VERSION, MAX_CONTENT_LENGTH
from app.routes import register_routes
@app.route("/")
def index():
return render_template("index.html", vm_api=VM_API)
def create_app():
app = Flask(__name__)
app.config["VERSION"] = VERSION
app.config["MAX_CONTENT_LENGTH"] = MAX_CONTENT_LENGTH
# DB auto-seed — schema migration + seed prompts + crash recovery
_init_db()
# Регистрация всех blueprint'ов
register_routes(app)
# no_cache на все ответы
@app.after_request
def no_cache(response):
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
return app
@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"})
def _init_db():
"""Schema migration + recovery: сброс застрявших classify."""
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}"})
from app.db.connection import execute
if not rows:
return jsonify({"ok": True, "answer": "Нет данных. Сначала запустите сравнение."})
for sql in [
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_raw text",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_input text",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS zip_source text",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS content_hash text",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS batch_id uuid",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS classify_status text DEFAULT 'pending'",
"ALTER TABLE documents ALTER COLUMN classify_status SET DEFAULT 'pending'",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS doc_type text",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS own_number text",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS parent_number text",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS doc_date date",
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS counterparty text",
]:
execute(sql)
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Ответь кратко, только по данным."
from app.db import prompts as db_prompts
db_prompts.seed_defaults()
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}"})
# Crash recovery: сбросить застрявшие classify
execute(
"UPDATE documents SET classify_status='pending', error_message=NULL "
"WHERE classify_status='processing'"
)
except Exception:
pass # БД недоступна при старте — не фатально
@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")
@app.route("/DrHider")
def drhider():
return render_template("drhider.html", vm_api=VM_API)
@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()
app = create_app()