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//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})