import os import json import time import threading import subprocess from flask import Blueprint, render_template, jsonify, request from ..db import query_one, get_all_sessions from ..config import SIM_CONFIGS_DIR from .. import process_mgr as pm bp = Blueprint("tools", __name__) # ── Stress test state ───────────────────────────────────────────────────────── _stress_state = { "running": False, "started_at": None, "mode": None, "rounds": None, "pnj_count": None, "output": [], "returncode": None, "error": None, } _stress_lock = threading.Lock() def _run_stress_bg(mode: str, rounds: int, pnj_count: int): global _stress_state ollama_url = os.getenv("OLLAMA_URL", "http://host.docker.internal:11434") model_mj = os.getenv("MODEL_MJ", "qwen2.5:14b") model_pnj = os.getenv("MODEL_PNJ", "qwen2.5:7b") results_path = os.path.join(SIM_CONFIGS_DIR, "crash_results.json") cmd = [ "python3", "/app/tools/llm_crash_test.py", "--mode", mode, "--rounds", str(rounds), "--pnj-count", str(pnj_count), ] env = {**os.environ, "OLLAMA_URL": ollama_url, "MODEL_MJ": model_mj, "MODEL_PNJ": model_pnj, "CRASH_RESULTS_PATH": results_path, "PYTHONUNBUFFERED": "1"} try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env) for line in proc.stdout: with _stress_lock: _stress_state["output"].append(line.rstrip()) proc.wait() with _stress_lock: _stress_state["returncode"] = proc.returncode _stress_state["running"] = False except Exception as e: with _stress_lock: _stress_state["error"] = str(e) _stress_state["running"] = False # ── Routes ──────────────────────────────────────────────────────────────────── @bp.route("/tools") def tools(): all_sessions = get_all_sessions() sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1)) session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,)) return render_template("tools.html", tab="tools", current_sid=sid, all_sessions=all_sessions, session=session, flash_msg=None, flash_ok=True) @bp.route("/api/stress-test/run", methods=["POST"]) def api_stress_run(): global _stress_state with _stress_lock: if _stress_state["running"]: return jsonify({"error": "Un test est déjà en cours.", "running": True}) mode = request.json.get("mode", "simultane") rounds = int(request.json.get("rounds", 5)) pnj_count = int(request.json.get("pnj_count", 2)) _stress_state = { "running": True, "started_at": time.strftime("%H:%M:%S"), "mode": mode, "rounds": rounds, "pnj_count": pnj_count, "output": [], "returncode": None, "error": None, } t = threading.Thread(target=_run_stress_bg, args=(mode, rounds, pnj_count), daemon=True) t.start() return jsonify({"ok": True, "message": f"Test '{mode}' {rounds} rounds lancé."}) @bp.route("/api/stress-test/status") def api_stress_status(): with _stress_lock: return jsonify({ "running": _stress_state["running"], "started_at": _stress_state["started_at"], "mode": _stress_state["mode"], "rounds": _stress_state["rounds"], "output": _stress_state["output"][-60:], "returncode": _stress_state["returncode"], "error": _stress_state["error"], }) @bp.route("/api/stress-test/results") def api_stress_results(): import datetime path = os.path.join(SIM_CONFIGS_DIR, "crash_results.json") if not os.path.exists(path): return jsonify({"error": "Aucun résultat. Lancez d'abord le test."}) try: mtime = os.path.getmtime(path) file_date = datetime.datetime.fromtimestamp(mtime).strftime("%d/%m/%Y %H:%M:%S") with open(path) as f: data = json.load(f) data["file_date"] = file_date return jsonify(data) except Exception as e: return jsonify({"error": f"Erreur lecture : {e}"}) @bp.route("/api/proc//run", methods=["POST"]) def proc_run(name): if name == "mix": if pm.proc_is_running("mix"): return jsonify({"error": "Mix déjà en cours"}), 400 data = request.get_json(silent=True) or {} count = int(data.get("count", 10)) pm.proc_mix_start(count) return jsonify({"ok": True, "label": f"Mode MIX — {count if count else '∞'}× chaque"}) if name not in pm._PROC_DEFS: return jsonify({"error": f"Processus inconnu: {name}"}), 400 if pm.proc_is_running(name): return jsonify({"error": "Déjà en cours"}), 400 defn = pm._PROC_DEFS[name] data = request.get_json(silent=True) or {} count = int(data.get("count", 1)) cmd = list(defn["cmd"]) if name == "sim_enricher" and data.get("type"): try: cmd[cmd.index("all")] = data["type"] except ValueError: pass if name == "lore_enricher" and data.get("faction"): try: cmd[cmd.index("all")] = data["faction"] except ValueError: pass pm.proc_start(name, cmd, defn.get("env_extra", {}), count) return jsonify({"ok": True, "label": defn["label"]}) @bp.route("/api/proc//status") def proc_status(name): return jsonify(pm.proc_get_state(name)) @bp.route("/api/proc//stop", methods=["POST"]) def proc_stop_route(name): pm.proc_stop(name) return jsonify({"ok": True})