refacto: dashboard Flask Blueprints + templates Jinja2, engine run/lore_enricher mis a jour
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
from flask import Blueprint, render_template, jsonify, request, redirect, url_for
|
||||
|
||||
from ..db import query, query_one, execute, get_all_sessions
|
||||
from ..chroma import upsert_lore_enriched, CHROMA_COL_OUT
|
||||
|
||||
bp = Blueprint("lore", __name__)
|
||||
|
||||
|
||||
def _get_proposals(faction: str = "", status: str = "pending") -> list:
|
||||
sql = "SELECT * FROM lore_proposals WHERE 1=1"
|
||||
params = []
|
||||
if faction:
|
||||
sql += " AND faction_slug = %s"
|
||||
params.append(faction)
|
||||
if status:
|
||||
sql += " AND status = %s"
|
||||
params.append(status)
|
||||
sql += " ORDER BY id DESC LIMIT 100"
|
||||
return query(sql, params or None)
|
||||
|
||||
|
||||
def _get_stats() -> dict:
|
||||
rows = query("SELECT status, COUNT(*) as n FROM lore_proposals GROUP BY status")
|
||||
m = {r["status"]: r["n"] for r in rows}
|
||||
return {"pending": m.get("pending", 0), "accepted": m.get("accepted", 0),
|
||||
"rejected": m.get("rejected", 0), "modified": m.get("modified", 0)}
|
||||
|
||||
|
||||
def _get_factions() -> list:
|
||||
rows = query("SELECT DISTINCT faction_slug FROM lore_proposals ORDER BY faction_slug")
|
||||
return [r["faction_slug"] for r in rows]
|
||||
|
||||
|
||||
@bp.route("/lore")
|
||||
def lore():
|
||||
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,))
|
||||
filt_faction = request.args.get("faction", "")
|
||||
filt_status = request.args.get("status", "pending")
|
||||
proposals = _get_proposals(filt_faction, filt_status)
|
||||
flash_msg = request.args.get("msg")
|
||||
flash_ok = request.args.get("ok", "1") == "1"
|
||||
|
||||
# Threads de discussion
|
||||
threads_raw = query("SELECT * FROM lore_proposal_threads ORDER BY created_at ASC") if proposals else []
|
||||
proposal_threads = {}
|
||||
for row in (threads_raw or []):
|
||||
pid = row["proposal_id"]
|
||||
proposal_threads.setdefault(pid, []).append(row)
|
||||
|
||||
return render_template("lore.html",
|
||||
tab="lore", current_sid=sid, all_sessions=all_sessions, session=session,
|
||||
proposals=proposals, lore_stats=_get_stats(),
|
||||
all_factions=_get_factions(), filt_faction=filt_faction, filt_status=filt_status,
|
||||
proposal_threads=proposal_threads, flash_msg=flash_msg, flash_ok=flash_ok,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/lore/action", methods=["POST"])
|
||||
def lore_action():
|
||||
prop_id = int(request.form.get("id", 0))
|
||||
action = request.form.get("action", "")
|
||||
modified_text = request.form.get("modified_text", "").strip()
|
||||
filt_faction = request.form.get("filt_faction", "")
|
||||
filt_status = request.form.get("filt_status", "pending")
|
||||
|
||||
proposal = query_one("SELECT * FROM lore_proposals WHERE id = %s", (prop_id,))
|
||||
if not proposal:
|
||||
return redirect(url_for("lore.lore", msg="Proposition introuvable", ok=0,
|
||||
faction=filt_faction, status=filt_status))
|
||||
try:
|
||||
if action == "accept":
|
||||
execute("UPDATE lore_proposals SET status='accepted', reviewed_at=NOW() WHERE id=%s", (prop_id,))
|
||||
upsert_lore_enriched({**proposal, "modified_text": None})
|
||||
msg = f"Proposition #{prop_id} acceptée et indexée dans {CHROMA_COL_OUT}."
|
||||
elif action == "reject":
|
||||
execute("UPDATE lore_proposals SET status='rejected', reviewed_at=NOW() WHERE id=%s", (prop_id,))
|
||||
msg = f"Proposition #{prop_id} rejetée."
|
||||
elif action == "modify":
|
||||
if not modified_text:
|
||||
modified_text = proposal["proposed_text"]
|
||||
execute("""UPDATE lore_proposals SET status='modified', modified_text=%s, reviewed_at=NOW()
|
||||
WHERE id=%s""", (modified_text, prop_id))
|
||||
upsert_lore_enriched({**proposal, "modified_text": modified_text})
|
||||
msg = f"Proposition #{prop_id} modifiée et indexée dans {CHROMA_COL_OUT}."
|
||||
else:
|
||||
msg = "Action inconnue."
|
||||
ok = 1
|
||||
except Exception as e:
|
||||
msg = f"Erreur: {e}"
|
||||
ok = 0
|
||||
|
||||
return redirect(url_for("lore.lore", msg=msg, ok=ok,
|
||||
faction=filt_faction, status=filt_status))
|
||||
@@ -0,0 +1,83 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for
|
||||
|
||||
from ..db import query_one, execute, get_all_sessions
|
||||
from ..config import SIM_CONFIGS_DIR, load_sim_config, save_sim_config, load_all_modes
|
||||
|
||||
bp = Blueprint("params", __name__)
|
||||
|
||||
|
||||
@bp.route("/params")
|
||||
def params():
|
||||
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,))
|
||||
cfg = load_sim_config(sid)
|
||||
modes = load_all_modes()
|
||||
flash_msg = request.args.get("msg")
|
||||
flash_ok = request.args.get("ok", "1") == "1"
|
||||
return render_template("params.html",
|
||||
tab="params", current_sid=sid, all_sessions=all_sessions,
|
||||
session=session, cfg=cfg, modes=modes, sim_configs_dir=SIM_CONFIGS_DIR,
|
||||
flash_msg=flash_msg, flash_ok=flash_ok,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/params/save", methods=["POST"])
|
||||
def params_save():
|
||||
sid = int(request.args.get("sid", 1))
|
||||
cfg = load_sim_config(sid)
|
||||
if cfg is None:
|
||||
return redirect(url_for("params.params", sid=sid,
|
||||
msg=f"Fichier sim_{sid:03d}.json introuvable dans {SIM_CONFIGS_DIR}", ok=0))
|
||||
f = request.form
|
||||
cfg["name"] = f.get("name", cfg.get("name", ""))
|
||||
cfg["mode"] = f.get("mode", cfg.get("mode", "pacifiste"))
|
||||
cfg.setdefault("tick", {})["tick_sleep_sec"] = int(f.get("tick_sleep_sec", 60))
|
||||
cfg.setdefault("survival", {}).update({
|
||||
"drain_speed": float(f.get("survival_drain_speed", 1.0)),
|
||||
"fatigue_hp_per_2pts": int(f.get("survival_fatigue_hp", 2)),
|
||||
"enabled": "survival_enabled" in f,
|
||||
})
|
||||
cfg.setdefault("encounter", {}).update({
|
||||
"global_rate_multiplier": float(f.get("encounter_rate", 1.0)),
|
||||
"enabled": "encounter_enabled" in f,
|
||||
})
|
||||
cfg.setdefault("economy", {})["caps_global_multiplier"] = float(f.get("caps_multiplier", 1.0))
|
||||
try:
|
||||
save_sim_config(sid, cfg)
|
||||
execute("UPDATE sessions SET mode=%s WHERE id=%s", (cfg["mode"], sid))
|
||||
msg = f"Config sim_{sid:03d}.json sauvegardée. Mode DB mis à jour → {cfg['mode']}."
|
||||
ok = 1
|
||||
except Exception as e:
|
||||
msg = f"Erreur: {e}"
|
||||
ok = 0
|
||||
return redirect(url_for("params.params", sid=sid, msg=msg, ok=ok))
|
||||
|
||||
|
||||
@bp.route("/params/setstatus", methods=["POST"])
|
||||
def params_setstatus():
|
||||
sid = int(request.args.get("sid", 1))
|
||||
status = request.form.get("status", "paused")
|
||||
try:
|
||||
execute("UPDATE sessions SET status=%s WHERE id=%s", (status, sid))
|
||||
msg = f"Session {sid} → status '{status}'."
|
||||
ok = 1
|
||||
except Exception as e:
|
||||
msg = f"Erreur: {e}"
|
||||
ok = 0
|
||||
return redirect(url_for("params.params", sid=sid, msg=msg, ok=ok))
|
||||
|
||||
|
||||
@bp.route("/params/resetday", methods=["POST"])
|
||||
def params_resetday():
|
||||
sid = int(request.args.get("sid", 1))
|
||||
day = int(request.form.get("day", 1))
|
||||
tick = int(request.form.get("tick", 0))
|
||||
try:
|
||||
execute("UPDATE sessions SET current_day=%s, current_tick=%s WHERE id=%s", (day, tick, sid))
|
||||
msg = f"Session {sid} remise à J{day} T{tick}h."
|
||||
ok = 1
|
||||
except Exception as e:
|
||||
msg = f"Erreur: {e}"
|
||||
ok = 0
|
||||
return redirect(url_for("params.params", sid=sid, msg=msg, ok=ok))
|
||||
@@ -0,0 +1,261 @@
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from flask import Blueprint, render_template, jsonify, request, send_file
|
||||
|
||||
from ..db import query, query_one, execute, get_all_sessions
|
||||
from ..config import SIM_CONFIGS_DIR
|
||||
|
||||
bp = Blueprint("sim", __name__)
|
||||
|
||||
_sim_procs = {
|
||||
"truth": {"proc": None, "output": [], "running": False, "returncode": None, "error": None, "sid": None},
|
||||
"enriched": {"proc": None, "output": [], "running": False, "returncode": None, "error": None, "sid": None},
|
||||
}
|
||||
_sim_lock = threading.Lock()
|
||||
|
||||
|
||||
def _run_sim_bg(mode, sid, tick_speed_s):
|
||||
env = {**os.environ,
|
||||
"SESSION_ID": str(sid),
|
||||
"TICK_SLEEP_SEC": str(tick_speed_s),
|
||||
"PYTHONUNBUFFERED": "1"}
|
||||
if mode == "enriched":
|
||||
env["USE_ENRICHED"] = "1"
|
||||
try:
|
||||
row = query_one(
|
||||
"INSERT INTO sim_runs (mode,session_id,tick_speed_s,status,started_at) "
|
||||
"VALUES (%s,%s,%s,'running',NOW()) RETURNING id",
|
||||
(mode, sid, tick_speed_s))
|
||||
run_id = row["id"] if row else None
|
||||
except Exception:
|
||||
run_id = None
|
||||
with _sim_lock:
|
||||
_sim_procs[mode].update({"output": [], "running": True, "returncode": None, "error": None, "sid": sid})
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
["python3", "-u", "/app/src/engine/run.py"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, env=env)
|
||||
with _sim_lock:
|
||||
_sim_procs[mode]["proc"] = proc
|
||||
for line in proc.stdout:
|
||||
with _sim_lock:
|
||||
_sim_procs[mode]["output"].append(line.rstrip())
|
||||
proc.wait()
|
||||
with _sim_lock:
|
||||
_sim_procs[mode]["returncode"] = proc.returncode
|
||||
_sim_procs[mode]["running"] = False
|
||||
if run_id:
|
||||
try:
|
||||
execute("UPDATE sim_runs SET status='stopped',stopped_at=NOW() WHERE id=%s", (run_id,))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
with _sim_lock:
|
||||
_sim_procs[mode]["error"] = str(e)
|
||||
_sim_procs[mode]["running"] = False
|
||||
|
||||
|
||||
def _get_sim_data(sid: int) -> dict:
|
||||
session = query_one("SELECT * FROM sessions WHERE id = %s", (sid,))
|
||||
if not session:
|
||||
session = query_one("SELECT * FROM sessions ORDER BY id DESC LIMIT 1")
|
||||
|
||||
characters = query("""
|
||||
SELECT c.*, COALESCE(c.tier, 1) as tier
|
||||
FROM characters c
|
||||
WHERE c.session_id = %s
|
||||
ORDER BY COALESCE(c.tier,1), c.is_alive DESC, c.name
|
||||
""", (sid,))
|
||||
alive_count = sum(1 for c in characters if c["is_alive"])
|
||||
|
||||
events = query("""
|
||||
SELECT e.*, c.name as actor_name FROM events e
|
||||
LEFT JOIN characters c ON c.id = e.actor_id
|
||||
WHERE e.session_id = %s
|
||||
AND e.event_type NOT IN ('encounter_lambda','encounter_group','encounter_actif','encounter_boss','cycle_tick')
|
||||
ORDER BY e.day DESC, e.tick DESC, e.id DESC LIMIT 50
|
||||
""", (sid,))
|
||||
|
||||
encounters = query("""
|
||||
SELECT e.*, c.name as actor_name FROM events e
|
||||
LEFT JOIN characters c ON c.id = e.actor_id
|
||||
WHERE e.session_id = %s AND e.event_type LIKE 'encounter%%'
|
||||
ORDER BY e.day DESC, e.tick DESC, e.id DESC LIMIT 30
|
||||
""", (sid,))
|
||||
|
||||
world_state = query("""
|
||||
SELECT DISTINCT ON (location_slug) *
|
||||
FROM world_state WHERE session_id = %s
|
||||
ORDER BY location_slug, day DESC
|
||||
""", (sid,))
|
||||
|
||||
faction_relations = query("""
|
||||
SELECT * FROM faction_relations WHERE session_id = %s
|
||||
ORDER BY relation_score ASC
|
||||
""", (sid,))
|
||||
|
||||
tier_labels = {0: "BOSS", 1: "ACTIFS SIM", 2: "PNJ+", 3: "PASSAGE"}
|
||||
tier_raw = query("""
|
||||
SELECT COALESCE(tier,1) as tier,
|
||||
COUNT(*) FILTER (WHERE is_alive) as alive,
|
||||
COUNT(*) FILTER (WHERE NOT is_alive) as dead
|
||||
FROM characters WHERE session_id = %s
|
||||
GROUP BY COALESCE(tier,1) ORDER BY COALESCE(tier,1)
|
||||
""", (sid,))
|
||||
tier_stats = [{"tier": r["tier"], "label": tier_labels.get(r["tier"], "?"),
|
||||
"alive": r["alive"], "dead": r["dead"]} for r in tier_raw]
|
||||
|
||||
zone_stats = query("""
|
||||
SELECT location_slug, COUNT(*) as cnt FROM characters
|
||||
WHERE session_id = %s AND is_alive = TRUE
|
||||
GROUP BY location_slug ORDER BY cnt DESC LIMIT 5
|
||||
""", (sid,))
|
||||
|
||||
ev_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s", (sid,))
|
||||
cb_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='combat'", (sid,))
|
||||
dt_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type='death'", (sid,))
|
||||
enc_total = query_one("SELECT COUNT(*) as n FROM events WHERE session_id=%s AND event_type LIKE 'encounter%%'", (sid,))
|
||||
|
||||
stats = {
|
||||
"Événements total": ev_total["n"] if ev_total else 0,
|
||||
"Combats": cb_total["n"] if cb_total else 0,
|
||||
"Morts": dt_total["n"] if dt_total else 0,
|
||||
"Rencontres": enc_total["n"] if enc_total else 0,
|
||||
"PNJ vivants": alive_count,
|
||||
"PNJ morts": len(characters) - alive_count,
|
||||
}
|
||||
|
||||
boss_inventory = query("""
|
||||
SELECT c.name as char_name, i.item_name, i.item_type, i.quantity, i.is_equipped
|
||||
FROM inventory i
|
||||
JOIN characters c ON c.id = i.character_id
|
||||
WHERE c.session_id = %s AND COALESCE(c.tier,1) = 0
|
||||
ORDER BY c.name, i.is_equipped DESC
|
||||
""", (sid,))
|
||||
|
||||
return dict(
|
||||
session=session,
|
||||
characters=characters,
|
||||
alive_count=alive_count,
|
||||
total_count=len(characters),
|
||||
events=events,
|
||||
encounters=encounters,
|
||||
world_state=world_state,
|
||||
faction_relations=faction_relations,
|
||||
tier_stats=tier_stats,
|
||||
zone_stats=zone_stats,
|
||||
stats=stats,
|
||||
boss_inventory=boss_inventory,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
all_sessions = get_all_sessions()
|
||||
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
|
||||
data = _get_sim_data(sid)
|
||||
return render_template("sim.html", tab="sim", current_sid=sid,
|
||||
all_sessions=all_sessions, flash_msg=None, flash_ok=True, **data)
|
||||
|
||||
|
||||
@bp.route("/api/sim/<mode>/run", methods=["POST"])
|
||||
def sim_run(mode):
|
||||
if mode not in ("truth", "enriched"):
|
||||
return jsonify({"error": "mode invalide"}), 400
|
||||
with _sim_lock:
|
||||
if _sim_procs[mode].get("running"):
|
||||
return jsonify({"error": f"Simulation {mode} déjà en cours"}), 400
|
||||
data = request.get_json(silent=True) or {}
|
||||
sid = int(data.get("sid", 1))
|
||||
tick_speed_s = int(data.get("tick_speed_s", 5))
|
||||
t = threading.Thread(target=_run_sim_bg, args=(mode, sid, tick_speed_s), daemon=True)
|
||||
t.start()
|
||||
return jsonify({"ok": True, "sid": sid, "mode": mode})
|
||||
|
||||
|
||||
@bp.route("/api/sim/<mode>/status")
|
||||
def sim_status(mode):
|
||||
if mode not in ("truth", "enriched"):
|
||||
return jsonify({"error": "mode invalide"}), 400
|
||||
with _sim_lock:
|
||||
state = {k: v for k, v in _sim_procs[mode].items() if k != "proc"}
|
||||
return jsonify(state)
|
||||
|
||||
|
||||
@bp.route("/api/sim/<mode>/stop", methods=["POST"])
|
||||
def sim_stop(mode):
|
||||
if mode not in ("truth", "enriched"):
|
||||
return jsonify({"error": "mode invalide"}), 400
|
||||
with _sim_lock:
|
||||
p = _sim_procs[mode].get("proc")
|
||||
if p:
|
||||
try:
|
||||
p.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@bp.route("/api/sim/<mode>/log")
|
||||
def sim_log(mode):
|
||||
if mode not in ("truth", "enriched"):
|
||||
return jsonify({"error": "mode invalide"}), 400
|
||||
import io
|
||||
import json as _json
|
||||
import datetime as _dt
|
||||
run = query_one("SELECT * FROM sim_runs WHERE mode=%s ORDER BY id DESC LIMIT 1", (mode,))
|
||||
if not run:
|
||||
return jsonify({"error": "Aucun run trouvé — lancez d'abord une simulation"}), 404
|
||||
sid = run["session_id"]
|
||||
session = query_one("SELECT * FROM sessions WHERE id=%s", (sid,))
|
||||
events = query("SELECT * FROM events WHERE session_id=%s ORDER BY id DESC LIMIT 500", (sid,))
|
||||
with _sim_lock:
|
||||
output_lines = list(_sim_procs[mode].get("output", []))
|
||||
log_data = {
|
||||
"meta": {
|
||||
"sim_mode": mode,
|
||||
"session_id": sid,
|
||||
"session_name": session.get("name", "") if session else "",
|
||||
"world": "Venice of Wasteland, Louisiane post-apo",
|
||||
"tick_speed_s": run["tick_speed_s"],
|
||||
"started_at": run["started_at"].isoformat() if run.get("started_at") else None,
|
||||
"stopped_at": run["stopped_at"].isoformat() if run.get("stopped_at") else None,
|
||||
"status": run["status"],
|
||||
"exported_at": _dt.datetime.now().isoformat(),
|
||||
},
|
||||
"console_output": output_lines,
|
||||
"recent_events": [dict(e) for e in events],
|
||||
}
|
||||
buf = io.BytesIO(_json.dumps(log_data, ensure_ascii=False, indent=2, default=str).encode("utf-8"))
|
||||
return send_file(buf, mimetype="application/json", as_attachment=True,
|
||||
download_name=f"sim_{mode}_{sid}.json")
|
||||
|
||||
|
||||
@bp.route("/api/session/set-mode", methods=["POST"])
|
||||
def session_set_mode():
|
||||
data = request.get_json(silent=True) or {}
|
||||
sid = int(data.get("sid", 1))
|
||||
mode = data.get("mode", "").strip()
|
||||
valid = {"pacifiste", "politique", "guerre_commerciale", "guerre", "survie_extreme"}
|
||||
if mode not in valid:
|
||||
return jsonify({"error": f"Mode invalide: {mode}"}), 400
|
||||
try:
|
||||
execute("UPDATE sessions SET mode=%s WHERE id=%s", (mode, sid))
|
||||
return jsonify({"ok": True, "sid": sid, "mode": mode})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route("/api/state")
|
||||
def api_state():
|
||||
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,))
|
||||
alive = query("""
|
||||
SELECT id, name, hp, max_hp, tier, faction_slug, location_slug
|
||||
FROM characters WHERE session_id=%s AND is_alive=TRUE
|
||||
""", (sid,))
|
||||
return jsonify({"session": session, "alive_pnj": alive})
|
||||
@@ -0,0 +1,138 @@
|
||||
import os
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
from flask import Blueprint, render_template, jsonify, request, redirect, url_for
|
||||
|
||||
from ..db import query, query_one, execute, get_all_sessions
|
||||
from ..chroma import upsert_sim_enriched, upsert_sim_reject, CHROMA_SIM_COL
|
||||
|
||||
bp = Blueprint("sim_proposals", __name__)
|
||||
|
||||
|
||||
def _get_proposals(ptype: str = "", status: str = "pending") -> list:
|
||||
sql = "SELECT * FROM sim_proposals WHERE 1=1"
|
||||
params = []
|
||||
if ptype:
|
||||
sql += " AND proposal_type = %s"
|
||||
params.append(ptype)
|
||||
if status:
|
||||
sql += " AND status = %s"
|
||||
params.append(status)
|
||||
sql += " ORDER BY id DESC LIMIT 100"
|
||||
return query(sql, params or None)
|
||||
|
||||
|
||||
def _get_stats() -> dict:
|
||||
rows = query("SELECT status, COUNT(*) as n FROM sim_proposals GROUP BY status")
|
||||
m = {r["status"]: r["n"] for r in rows}
|
||||
return {"pending": m.get("pending", 0), "accepted": m.get("accepted", 0),
|
||||
"rejected": m.get("rejected", 0)}
|
||||
|
||||
|
||||
def _get_types() -> list:
|
||||
rows = query("SELECT DISTINCT proposal_type FROM sim_proposals ORDER BY proposal_type")
|
||||
return [r["proposal_type"] for r in rows]
|
||||
|
||||
|
||||
@bp.route("/sim-proposals")
|
||||
def sim_proposals_view():
|
||||
all_sessions = get_all_sessions()
|
||||
sid = int(request.args.get("sid", all_sessions[-1]["id"] if all_sessions else 1))
|
||||
filt_type = request.args.get("type", "")
|
||||
filt_status = request.args.get("status", "pending")
|
||||
proposals = _get_proposals(filt_type, filt_status)
|
||||
flash_msg = request.args.get("msg")
|
||||
flash_ok = request.args.get("ok", "1") == "1"
|
||||
|
||||
threads_raw = query("SELECT * FROM sim_proposal_threads ORDER BY created_at ASC") if proposals else []
|
||||
proposal_threads = {}
|
||||
for row in (threads_raw or []):
|
||||
pid = row["proposal_id"]
|
||||
proposal_threads.setdefault(pid, []).append(row)
|
||||
|
||||
return render_template("sim_proposals.html",
|
||||
tab="sim_proposals", current_sid=sid, all_sessions=all_sessions,
|
||||
proposals=proposals, sim_stats=_get_stats(), sim_types=_get_types(),
|
||||
filt_type=filt_type, filt_status=filt_status,
|
||||
proposal_threads=proposal_threads, flash_msg=flash_msg, flash_ok=flash_ok,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/sim-proposals/action", methods=["POST"])
|
||||
def sim_proposals_action():
|
||||
prop_id = int(request.form.get("id", 0))
|
||||
action = request.form.get("action", "")
|
||||
filt_type = request.form.get("filt_type", "")
|
||||
filt_status = request.form.get("filt_status", "pending")
|
||||
|
||||
proposal = query_one("SELECT * FROM sim_proposals WHERE id = %s", (prop_id,))
|
||||
if not proposal:
|
||||
return redirect(url_for("sim_proposals.sim_proposals_view", msg="Proposition introuvable", ok=0))
|
||||
try:
|
||||
if action == "accept":
|
||||
execute("UPDATE sim_proposals SET status='accepted', reviewed_at=NOW() WHERE id=%s", (prop_id,))
|
||||
upsert_sim_enriched(dict(proposal))
|
||||
msg = f"Proposition #{prop_id} acceptée → indexée dans {CHROMA_SIM_COL}."
|
||||
elif action == "reject":
|
||||
reason = request.form.get("reject_reason", "").strip()
|
||||
execute("UPDATE sim_proposals SET status='rejected', reviewed_at=NOW(), reject_reason=%s WHERE id=%s",
|
||||
(reason or None, prop_id))
|
||||
if reason:
|
||||
try:
|
||||
upsert_sim_reject(dict(proposal), reason)
|
||||
except Exception:
|
||||
pass
|
||||
msg = f"Proposition #{prop_id} rejetée." + (" Motif indexé." if reason else "")
|
||||
else:
|
||||
msg = "Action inconnue."
|
||||
ok = 1
|
||||
except Exception as e:
|
||||
msg = f"Erreur: {e}"
|
||||
ok = 0
|
||||
|
||||
return redirect(url_for("sim_proposals.sim_proposals_view", msg=msg, ok=ok,
|
||||
type=filt_type, status=filt_status))
|
||||
|
||||
|
||||
@bp.route("/sim-proposals/<int:prop_id>/discuss", methods=["POST"])
|
||||
def sim_proposals_discuss(prop_id):
|
||||
proposal = query_one("SELECT * FROM sim_proposals WHERE id=%s", (prop_id,))
|
||||
if not proposal:
|
||||
return jsonify({"error": "Proposition introuvable"}), 404
|
||||
data = request.get_json(silent=True) or {}
|
||||
user_msg = data.get("message", "").strip()
|
||||
temperature = float(data.get("temperature", 0.75))
|
||||
num_predict = int(data.get("num_predict", 512))
|
||||
if not user_msg:
|
||||
return jsonify({"error": "Message vide"}), 400
|
||||
|
||||
execute("INSERT INTO sim_proposal_threads (proposal_id,role,content,temperature,num_predict) VALUES (%s,'user',%s,%s,%s)",
|
||||
(prop_id, user_msg, temperature, num_predict))
|
||||
|
||||
context = [
|
||||
"Tu es un assistant de game design pour un JDR Fallout post-apocalyptique (Louisiane, 'Venice of Wasteland').",
|
||||
f"Proposition de règle : [{proposal.get('proposal_type','')}] {proposal.get('title','')}",
|
||||
f"Description : {proposal.get('description','')}",
|
||||
f"Rationale : {proposal.get('rationale','')}",
|
||||
"",
|
||||
f"Le maître de jeu demande : {user_msg}",
|
||||
"",
|
||||
"Réponds de façon concise et pratique. Si tu proposes une version modifiée, structure-la clairement.",
|
||||
]
|
||||
ollama_url = os.getenv("OLLAMA_URL", "http://10.8.0.2:11434")
|
||||
model = os.getenv("MODEL_MJ", "qwen2.5:14b")
|
||||
try:
|
||||
payload = json.dumps({"model": model, "prompt": "\n".join(context), "stream": False,
|
||||
"options": {"temperature": temperature, "num_predict": num_predict}}).encode()
|
||||
req = urllib.request.Request(f"{ollama_url}/api/generate",
|
||||
data=payload, headers={"Content-Type": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
result = json.loads(resp.read())
|
||||
llm_response = result.get("response", "").strip()
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Erreur LLM : {e}"}), 500
|
||||
|
||||
execute("INSERT INTO sim_proposal_threads (proposal_id,role,content,temperature,num_predict) VALUES (%s,'llm',%s,%s,%s)",
|
||||
(prop_id, llm_response, temperature, num_predict))
|
||||
return jsonify({"ok": True, "response": llm_response})
|
||||
@@ -0,0 +1,150 @@
|
||||
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/<name>/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/<name>/status")
|
||||
def proc_status(name):
|
||||
return jsonify(pm.proc_get_state(name))
|
||||
|
||||
|
||||
@bp.route("/api/proc/<name>/stop", methods=["POST"])
|
||||
def proc_stop_route(name):
|
||||
pm.proc_stop(name)
|
||||
return jsonify({"ok": True})
|
||||
Reference in New Issue
Block a user