Add admin AI models panel (list, test, select Mistral models)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
billisdeadandClaude Sonnet 5.5 committed 2026-10-12 09:55:37 +02:00
1 parent defc07648c
commit 13677b3ce3
4 files changed
+162 -5

No files matched your search

+14 -3
View File
@@ -11,6 +11,7 @@ import logging
from openai import OpenAI, BadRequestError
from legal_framework import LEGAL_FILTER_PROMPT, SYNTHESIS_PROMPT, LAW_CHECK_PROMPT, build_filter_prompt
from legifrance_client import get_legifrance_client
from database import get_setting
logger = logging.getLogger(__name__)
@@ -36,6 +37,16 @@ def _get_live_legal_texts() -> dict[str, str]:
return {}
def get_model(kind: str) -> str:
"""kind = "filter" | "synthesis". Priorité : réglage admin (DB) > env > défaut."""
default = "mistral-small-latest" if kind == "filter" else "mistral-large-latest"
try:
saved = get_setting(f"{kind}_model")
except Exception:
saved = None
return saved or os.environ.get(f"{kind.upper()}_MODEL", os.environ.get(f"OPENAI_{kind.upper()}_MODEL", default))
def get_client() -> OpenAI:
"""
Supporte deux modes (par ordre de priorité) :
@@ -75,7 +86,7 @@ def filter_idea(content: str) -> dict:
"""
try:
client = get_client()
filter_model = os.environ.get("FILTER_MODEL", os.environ.get("OPENAI_FILTER_MODEL", "mistral-small-latest"))
filter_model = get_model("filter")
live_texts = _get_live_legal_texts()
system_prompt = build_filter_prompt(live_texts)
if live_texts:
@@ -137,7 +148,7 @@ def check_existing_law(content: str) -> str:
"""
try:
client = get_client()
model = os.environ.get("FILTER_MODEL", os.environ.get("OPENAI_FILTER_MODEL", "mistral-small-latest"))
model = get_model("filter")
response = client.chat.completions.create(
model=model,
max_tokens=200,
@@ -165,7 +176,7 @@ def synthesize_ideas(ideas: list[str]) -> str:
)
try:
client = get_client()
synthesis_model = os.environ.get("SYNTHESIS_MODEL", os.environ.get("OPENAI_SYNTHESIS_MODEL", "mistral-large-latest"))
synthesis_model = get_model("synthesis")
ideas_text = "\n".join(f"{i + 1}. {idea}" for i, idea in enumerate(ideas))
response = client.chat.completions.create(
model=synthesis_model,
+47 -1
View File
@@ -58,8 +58,9 @@ from database import (
close_consultation, get_consultations_to_autoclose, get_consultation_stats,
get_consultation_contributions, delete_consultation,
check_ip_blacklist, record_ip_rejection, get_ip_blacklist, remove_ip_blacklist,
set_setting,
)
from ai_agent import filter_idea, synthesize_ideas, check_existing_law
from ai_agent import filter_idea, synthesize_ideas, check_existing_law, get_client, get_model
# ─── Logging ────────────────────────────────────────────────────────────────
@@ -1290,6 +1291,51 @@ def admin_regenerate_synthesis():
return jsonify({"ok": True, "message": "Régénération lancée en arrière-plan."})
@app.get("/api/admin/ai/models")
@require_admin
def admin_ai_models():
"""Modèles actuellement actifs + liste de ceux accessibles avec la clé en place."""
out = {"filter": get_model("filter"), "synthesis": get_model("synthesis"), "available": [], "error": None}
try:
out["available"] = sorted(m.id for m in get_client().models.list().data)
except Exception as e:
logger.warning("Admin — liste des modèles IA impossible : %s", type(e).__name__)
out["error"] = f"{type(e).__name__}: {str(e)[:200]}"
return jsonify(out)
@app.post("/api/admin/ai/models")
@require_admin
def admin_ai_set_models():
"""Enregistre les modèles filter/synthesis (chaîne vide → retour à l'env)."""
body = request.get_json(silent=True) or {}
for kind in ("filter", "synthesis"):
if kind in body:
v = str(body[kind] or "").strip()
if len(v) > 100:
return jsonify({"error": "invalid_model"}), 400
set_setting(f"{kind}_model", v or None)
logger.info("Admin — modèles IA mis à jour")
return jsonify({"ok": True, "filter": get_model("filter"), "synthesis": get_model("synthesis")})
@app.post("/api/admin/ai/test")
@require_admin
@limiter.limit("10 per minute")
def admin_ai_test():
"""Appel minimal (1 token) pour vérifier qu'un modèle répond avec la clé en place."""
model = str((request.get_json(silent=True) or {}).get("model", "")).strip()
if not model or len(model) > 100:
return jsonify({"error": "invalid_model"}), 400
try:
get_client().chat.completions.create(
model=model, max_tokens=1, messages=[{"role": "user", "content": "ping"}],
)
return jsonify({"ok": True})
except Exception as e:
return jsonify({"ok": False, "error": f"{type(e).__name__}: {str(e)[:200]}"})
@app.get("/api/admin/export/csv")
@require_admin
def admin_export_csv():
+23
View File
@@ -114,6 +114,7 @@ def init_db() -> None:
)
""")
cur.execute("CREATE INDEX IF NOT EXISTS idx_ip_abuse_expires ON ip_abuse(expires_at)")
cur.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
logger.info("Base de données initialisée.")
@@ -575,3 +576,25 @@ def delete_consultation(consultation_id: int) -> bool:
cur.execute("DELETE FROM synthesis WHERE consultation_id = %s", (consultation_id,))
cur.execute("DELETE FROM consultations WHERE id = %s RETURNING id", (consultation_id,))
return cur.fetchone() is not None
# ─── Réglages (clé/valeur) ────────────────────────────────────────────────────
def get_setting(key: str) -> str | None:
with db_cursor() as cur:
cur.execute("SELECT value FROM settings WHERE key = %s", (key,))
row = cur.fetchone()
return row["value"] if row else None
def set_setting(key: str, value: str | None) -> None:
"""value=None → supprime le réglage (retour à la valeur d'environnement)."""
with db_cursor() as cur:
if value is None:
cur.execute("DELETE FROM settings WHERE key = %s", (key,))
else:
cur.execute(
"INSERT INTO settings (key, value) VALUES (%s, %s) "
"ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",
(key, value),
)