import os from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy from dotenv import load_dotenv import pandas as pd from models import db, Player, Step, MessageLog import requests import logging from io import StringIO import re logging.basicConfig(level=logging.INFO) log = logging.getLogger("flask") # Charger .env load_dotenv() app = Flask(__name__) app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev_secret") app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "sqlite:///treasure.db") db.init_app(app) PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY") FLASK_SHARED_SECRET = os.getenv("FLASK_SHARED_SECRET", "secret_flask_matrix") CONTEXT_TURNS = int(os.getenv("CONTEXT_TURNS", 10)) # nbre d'échanges pris en compte GOOGLE_SHEET_CSV_URL = os.getenv("GOOGLE_SHEET_CSV_URL", "") def build_perplexity_history(player, current_user_message, step): # récupère tout l'historique du joueur, ordonné history = MessageLog.query.filter_by(player_id=player.id).order_by(MessageLog.timestamp.asc()).all() messages = [] for msg in history: if msg.sender == "Katniss": messages.append({"role": "assistant", "content": msg.content}) else: messages.append({"role": "user", "content": msg.content}) # On ajoute le message courant et le contexte de l'étape dans UN SEUL message user à la fin context_prefix = ( f"[Contexte: Étape {step.step if step else '?'} - {step.location if step else ''}]\n" f"Énigme: {step.location_enigma if step else ''}\n" ) messages.append({ "role": "user", "content": context_prefix + current_user_message.strip() }) # Nettoie pour garantir alternance cleaned = [] last_role = None for m in messages: if m["role"] == last_role: # Fusionne le contenu avec le dernier du même role (précaution très utile) cleaned[-1]["content"] += "\n\n" + m["content"] else: cleaned.append(m) last_role = m["role"] # Ancien prompt_system remplacé pour prendre en compte le model Step prompt_system = ( "Tu es Katniss, guide d'aventure pour une chasse au trésor (rôle assistant). " "Tu reçois toujours, en contexte, un 'step' qui contient les champs suivants :\n" "- step : numéro de l'étape\n" "- pre_text : texte d'introduction/context avant saisie du code (si présent)\n" "- location : titre du lieu\n" "- location_enigma : énigme principale ou description de l'énigme à créer\n" "- location_hint : indice lié au code (à utiliser progressivement)\n" "- code : la réponse/código (NE JAMAIS révéler au joueur)\n" "- question : question additionnelle éventuelle\n" "- question_hint : indice pour la question\n" "- answer : réponse à la question (NE JAMAIS révéler)\n" "- comments : commentaire pour le MJ (informations internes)\n" "- success_text : texte à afficher après réussite\n" "- image_path, audio_path : chemins vers médias liés à l'étape (mention possible)\n\n" "Règles strictes :\n" "1) Ne jamais fournir directement 'code' ni 'answer'. Si le joueur demande la réponse, refuse poliment et propose un indice.\n" "2) Proposer des indices progressifs : commencer par des indices généraux (réutiliser location_hint ou question_hint), " "puis, si le joueur insiste, donner des indices de plus en plus directs sans révéler la solution.\n" "3) Utiliser pre_text pour contextualiser la demande si présent, et suggérer au joueur d'examiner image_path/audio_path si fournis.\n" "4) Si la requête montre que le joueur a trouvé le code, encourager et rappeler qu'après validation le texte success_text sera affiché.\n" "5) Ne pas inventer d'informations non présentes dans 'step'; se limiter aux champs et à l'historique des messages.\n" "6) Répondre en français, ton amical et encourageant, bref si l'utilisateur demande un indice simple, plus explicite si l'utilisateur a tenté plusieurs fois.\n" ) messages_final = [{"role": "system", "content": prompt_system}] + cleaned[-20:] # Limite à 20 messages récents return messages_final def get_ai_hint(player, current_user_message): step = Step.query.filter_by(step=player.current_step).first() messages = build_perplexity_history(player, current_user_message, step) log.info(messages) url = "https://api.perplexity.ai/chat/completions" headers = { "Authorization": f"Bearer {PERPLEXITY_API_KEY}", "Content-Type": "application/json" } payload = { "model": "sonar", "messages": messages, "max_tokens": 500, "temperature": 0.7, } try: resp = requests.post(url, headers=headers, json=payload, timeout=20) if resp.status_code == 400: app.logger.error(f"Perplexity 400: {resp.text}") return "(Katniss indisponible: historique de messages mal formé, regarde les logs serveur !)" resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] return content.strip() if content else "(Katniss n'a rien répondu, elle ne capte probablement plus au fond du bunker)" except Exception as e: return f"(Katniss est en échec: {e})" def call_perplexity(messages, max_tokens=400, temperature=0.7): """Send messages to Perplexity and return assistant content or error string.""" url = "https://api.perplexity.ai/chat/completions" headers = { "Authorization": f"Bearer {PERPLEXITY_API_KEY}", "Content-Type": "application/json" } payload = { "model": "sonar", "messages": messages, "max_tokens": max_tokens, "temperature": temperature, } try: resp = requests.post(url, headers=headers, json=payload, timeout=20) if resp.status_code == 400: app.logger.error(f"Perplexity 400: {resp.text}") return "(Katniss indisponible: historique de messages mal formé, regarde les logs serveur !)" resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] return content.strip() if content else "" except Exception as e: app.logger.exception("Perplexity error") return f"(Katniss est en échec: {e})" def is_affirmative(text: str) -> bool: """Return True if text looks like a positive/ready answer (oui/ok/yes/etc.).""" if not text: return False normalized = re.sub(r"[^a-z0-9\s]", "", text.lower()) tokens = set(normalized.split()) affirmatives = {"oui", "ouais", "ok", "yes", "yeah", "yep", "go", "pret", "prêt", "daccord", "d'accord"} return len(tokens & affirmatives) > 0 def matches_any(text: str, choices: str) -> bool: """Return True if text matches any of the semicolon-separated choices (case-insensitive). Normalises by lowercasing, trimming and removing punctuation for a more forgiving match. """ if not text: return False if not choices: return False text_norm = re.sub(r"[^a-z0-9\s]", "", text.lower()).strip() for part in str(choices).split(";"): part_norm = re.sub(r"[^a-z0-9\s]", "", part.lower()).strip() if part_norm == text_norm: return True return False # def generate_step_intro(player, step): # """Ask the AI to improve pre_text + location_enigma and return a short intro in French.""" # if not step: # return "(Aucune étape trouvée)" # system = ( # "Tu es Katniss, guide d'aventure et maître du jeu pour une chasse au trésor." # "Améliore et synthétise en français le texte d'introduction fourni (pre_text) et l'énigme (location_enigma) " # "pour qu'il soit engageant et clair pour le joueur. Ne révèle jamais le 'code' ni la 'answer'." # "Parle à la première personne du singulier, comme si tu parlais directement au joueur." # "Renvoie la réponse au format markdown." # ) # user_content = f"Prétexte:\n{step.pre_text or ''}\n\nÉnigme:\n{step.location_enigma or ''}\n\n" # messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}] # return call_perplexity(messages, max_tokens=1500) def generate_step_intro(player, step): """Ask the AI to improve pre_text + location_enigma and return a short intro in French.""" if not step: return "(Aucune étape trouvée)" system = ( "Tu es Katniss, guide d'aventure et maître du jeu pour une chasse au trésor." "Améliore et synthétise en frle texte d'introduction fourni (pre_text) et l'énigme (location_enigma) " "pour qu'il soit engageant et clair pour le joueur. Ne révèle jamais le 'code' ni la 'answer'." "Parle à la première personne du singulier, comme si tu parlais directement au joueur." "Renvoie la réponse au format markdown." ) user_content = f"Prétexte:\n{step.pre_text or ''}\n\nÉnigme:\n{step.location_enigma or ''}\n\n" messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}] return call_perplexity(messages, max_tokens=1500) def generate_intro_text(player, step): """Generate only the intro (pre_text) improved by AI, without revealing the enigma. Returns markdown string.""" if not step: return "(Aucune étape trouvée)" # return step.pre_text or "" system = ( "Renvoi le texte d'introduction fourni (pre_text) au format markdown sans fautes mais ne le modifie pas. Ne rajoute pas d'autre informations. " "") user_content = f"pre_text:\n{step.pre_text or ''}\n\n" messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}] return call_perplexity(messages, max_tokens=800) def generate_enigma_text(player, step): """Generate only the enigma (location_enigma) improved by AI, ready to be presented to the player.""" if not step: return "(Aucune étape trouvée)" # return step.location_enigma or "" system = ( "Renvoie l'énigme (location_enigma) au format markdown sans fautes mais ne la modifie pas. Ne rajoute pas d'autre informations." "" ) user_content = f"Énigme brute:\n{step.location_enigma or ''}\n\n" messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}] return call_perplexity(messages, max_tokens=1000) def is_player_ready_ai(player, reply_text, step): """Ask the AI to judge whether the player's reply indicates readiness. Returns (bool, katniss_message). The AI is instructed to output first line TRUE or FALSE, then a short encouraging message on the next line. """ system = ( "Tu es Katniss, guide d'aventure. Tu dois lire la réponse courte d'un joueur et décider s'il est prêt à recevoir l'énigme suivante. " "Réponds STRICTEMENT sur deux lignes : première ligne TRUE si le joueur est prêt, FALSE sinon ; deuxième ligne un court message d'encouragement en français (1-2 phrases)." ) user_content = f"Contexte étape {step.step if step else '?'} - lieu: {step.location if step else ''}\nRéponse du joueur:\n{reply_text}\n\nRenvoie exactement deux lignes : TRUE/FALSE, puis le message Katniss." messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}] ai_text = call_perplexity(messages, max_tokens=200) if not ai_text: return False, "Prends ton temps, dis-moi quand tu seras prêt·e." # Parse first token for true/false first_line = ai_text.splitlines()[0].strip().lower() if ai_text else "" kat_msg = "\n".join(ai_text.splitlines()[1:]).strip() or "Prends ton temps, dis-moi quand tu seras prêt·e." ready = False if "true" in first_line or "oui" in first_line or "vrai" in first_line: ready = True return ready, kat_msg def generate_formatted_hint(player, step, hint_text, hint_kind="location", hint_index=0): """Ask the AI to present a single hint (already extracted) in a friendly way.""" system = ( "Tu es Katniss, guide d'aventure et maitre du jeu pour Magali. Renvoie l'indice (hint) SANS MODIFICATION, sans fautes, sans autres informations" "Avant l'indice, encourage le joueur." ) user_content = ( f"indice : {hint_text}" "" ) messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}] return call_perplexity(messages, max_tokens=1000) def generate_question_text(player:Player, step:Step): """Ask the AI to format the question to the player if present.""" if not step or not step.question: return "" system = ( "Tu es Katniss, guide d'aventure et maitre du jeu. Reformule la question fournie de façon claire et engageante en français. " "Parle à la première personne du singulier, comme si tu parlais directement au joueur." "Renvoie la réponse au format markdown." "Les questions sont liés à des évenements entre Sam et Magali" ) user_content = f"Question brute:\n{step.question}\n\nRenvoie la question après avoir donner un encouragement pour avoir réussi à trouver le code. indique en gras que c'est une question. redonne le numero de l'etape {step.step}" messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}] return call_perplexity(messages, max_tokens=1000) def generate_success_text(player, step): """Ask the AI to produce an encouraging success message (can use step.success_text as base).""" base = step.success_text or "Bravo ! Tu as réussi cette étape." # system = ( # "Tu es Katniss, guide d'aventure. Génère un court message d'encouragement en français à destination du joueur après réussite. " # "Tu peux améliorer le texte de base sans révéler d'informations supplémentaires." # "garde les élements entre crochets dans le texte de base" # "Parle à la première personne du singulier, comme si tu parlais directement au joueur." # "Renvoie la réponse au format markdown." # ) system = ( "Renvoie le texte d'encouragement fourni (success_text) au format markdown sans fautes mais ne le modifie pas. Ne rajoute pas d'autre informations. " "") # Ask the AI to enhance the base text but ensure we keep the original base (with bracketed URLs) # user_content = ( # f"Texte de base:\n{base}\n\n" # "Améliore ce texte de base pour en faire un court message d'encouragement chaleureux en français." # " NE MODIFIE PAS ni ne SUPPRIME les éléments entre crochets [] présents dans le texte de base." # " Renvoie uniquement le texte d'encouragement (format markdown)." # ) user_content = f"success_text:\n{base}\n\n" messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}] ai_response = call_perplexity(messages, max_tokens=1000) # Always return the original base first (to preserve bracketed URLs), then the AI enhancement # If AI returned an empty string, fall back to base if not ai_response or ai_response.strip() == "": return base return f"{ai_response.strip()}" def fetch_steps_from_gsheet(csv_url=GOOGLE_SHEET_CSV_URL): resp = requests.get(csv_url) resp.raise_for_status() csv_content = resp.text df = pd.read_csv(StringIO(csv_content)) df = df.fillna("") # Remplace NaN par "" steps = [] for _, row in df.iterrows(): # Only accept the exact CSV columns: step, pre_text, location, location_enigma, # location_hint, code, question, question_hint, answer, comments, success_text raw_step = row.get('step', "") try: number = int(float(raw_step)) if str(raw_step).strip() != "" else None except Exception: # skip invalid/empty step rows continue if number is None: continue pre_text = row.get('pre_text', "") location = row.get('location', "") location_enigma = row.get('location_enigma', "") location_hint = row.get('location_hint', "") code = str(row.get('code', "")) question = row.get('question', "") question_hint = row.get('question_hint', "") answer = str(row.get('answer', "")) comments = row.get('comments', "") success_text = row.get('success_text', "") # No media columns expected in this simplified mapping image_path = "" audio_path = "" step = Step( step=number, pre_text=pre_text, location=location, location_enigma=location_enigma, location_hint=location_hint, code=code, question=question, question_hint=question_hint, answer=answer, comments=comments, success_text=success_text, image_path=image_path, audio_path=audio_path, ) steps.append(step) return steps @app.route("/api/matrix/incoming", methods=["POST"]) def matrix_incoming(): data = request.json if data.get("secret") != FLASK_SHARED_SECRET: return jsonify({"error": "Forbidden"}), 403 matrix_id = data.get("sender") text = data.get("body", "").strip() player = Player.query.filter_by(matrix_id=matrix_id).first() is_new_player = False if not player: # initialize player with hint counters and stage (awaiting ready confirmation) player = Player( matrix_id=matrix_id, current_step=1, stage='awaiting_ready', location_hint_index=0, question_hint_index=0, last_sent_step=None, ) db.session.add(player) db.session.commit() is_new_player = True # Log message reçu db.session.add(MessageLog(player_id=player.id, sender="mag", content=text)) step = Step.query.filter_by(step=player.current_step).first() # If no step found, respond accordingly if not step: reply = "(Aucune étape définie pour ce joueur pour le moment.)" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply)) db.session.commit() return jsonify({"reply": reply}) # Ensure player has stage/hint counters attributes (in case models were not migrated) if not hasattr(player, 'stage') or not player.stage: player.stage = 'intro_needed' if not hasattr(player, 'location_hint_index'): player.location_hint_index = 0 if not hasattr(player, 'question_hint_index'): player.question_hint_index = 0 reply = None # If player is awaiting the "ready to play" confirmation if player.stage == 'awaiting_ready': # Use AI to judge whether the player's message indicates readiness and get a short Katniss reply ready, kat_msg = is_player_ready_ai(player, text, step) if ready: # Prepare and send intro for the current step player.stage = 'intro_needed' db.session.commit() if not step: reply = "(Aucune étape définie pour le moment.)" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply)) db.session.commit() return jsonify({"reply": reply}) # Instead of sending enigma now, send only the improved intro and ask readiness for enigma intro = generate_intro_text(player, step) player.stage = 'awaiting_ready_for_enigma' player.location_hint_index = 0 player.question_hint_index = 0 player.last_sent_step = player.current_step db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg)) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=intro)) db.session.commit() # Prompt the user to confirm when they're ready to see the enigma ready_prompt = "T'es-tu prête pour l'énigme ? " db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt)) db.session.commit() return jsonify({"reply": kat_msg + "\n\n" + intro + "\n\n" + ready_prompt}) else: # AI says not ready: send Katniss encouraging message db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg)) db.session.commit() return jsonify({"reply": kat_msg}) # If player answered the intro and we are waiting for them to confirm readiness for the enigma if player.stage == 'awaiting_ready_for_enigma': # Use AI to interpret readiness and get Katniss encouragement ready, kat_msg = is_player_ready_ai(player, text, step) if ready: # send the enigma (improved by AI) and move to awaiting_code enigma = generate_enigma_text(player, step) player.stage = 'awaiting_code' player.location_hint_index = 0 player.question_hint_index = 0 player.last_sent_step = player.current_step db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg)) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=enigma)) db.session.commit() return jsonify({"reply": kat_msg + "\n\n" + enigma}) else: # send encouraging message and remain in awaiting_ready_for_enigma db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg)) db.session.commit() return jsonify({"reply": kat_msg}) # If player is awaiting confirmation after a success to continue to next step if player.stage == 'awaiting_ready_after_success': # The current_step was already incremented at success time; get the new step next_step = Step.query.filter_by(step=player.current_step).first() # Use AI to interpret readiness ready, kat_msg = is_player_ready_ai(player, text, next_step) if ready: if not next_step: reply = "(Aucune étape suivante définie.)" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply)) db.session.commit() return jsonify({"reply": reply}) # send the intro for the next step (improved by AI) and move to awaiting_ready_for_enigma next_intro = generate_intro_text(player, next_step) player.stage = 'awaiting_ready_for_enigma' player.location_hint_index = 0 player.question_hint_index = 0 player.last_sent_step = player.current_step db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg)) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=next_intro)) db.session.commit() ready_prompt = "T'es-tu prête pour l'énigme ?" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt)) db.session.commit() return jsonify({"reply": kat_msg + "\n\n" + next_intro + "\n\n" + ready_prompt}) else: db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg)) db.session.commit() return jsonify({"reply": kat_msg}) # 1) If we need to send the intro for this step if player.stage == 'intro_needed' or player.current_step != getattr(player, 'last_sent_step', None): intro = generate_intro_text(player, step) # store that we've sent the intro and reset hint counters, but wait for readiness before enigma player.stage = 'awaiting_ready_for_enigma' player.location_hint_index = 0 player.question_hint_index = 0 player.last_sent_step = player.current_step db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=intro)) db.session.commit() ready_prompt = "T'es-tu prête pour l'énigme ?" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt)) db.session.commit() return jsonify({"reply": intro + "\n\n" + ready_prompt}) # 2) We're waiting for the code if player.stage == 'awaiting_code': if matches_any(text, step.code or ""): # Correct code # If there's a question, send it (formatted by AI) and change stage if step.question and step.question.strip() != "": qtext = generate_question_text(player, step) player.stage = 'awaiting_answer' player.question_hint_index = 0 reply = qtext or step.question else: # No question: generate success, advance step and ask ready-to-continue success = generate_success_text(player, step) player.current_step += 1 # After success, wait for player's confirmation before sending next intro player.stage = 'awaiting_ready_after_success' db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=success)) db.session.commit() # Check if there's a next step; if so, prepare a ready prompt as followup next_step = Step.query.filter_by(step=player.current_step).first() if next_step: # Optionally pre-generate and store the intro for debugging/history next_intro = generate_intro_text(player, next_step) player.last_sent_step = player.current_step db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=next_intro)) db.session.commit() ready_prompt = "Bravo ! Veux-tu continuer vers l'étape suivante ?" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt)) db.session.commit() return jsonify({"reply": success, "followup": ready_prompt}) else: reply = success + "\n\nTu as terminé toutes les étapes. Félicitations !" db.session.commit() db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply)) db.session.commit() return jsonify({"reply": reply}) else: # Incorrect code: send next location hint (one at a time) raw_hints = (step.location_hint or "").split(";") if step.location_hint else [] idx = int(getattr(player, 'location_hint_index', 0)) if idx < len(raw_hints) and raw_hints[idx].strip() != "": hint_raw = raw_hints[idx].strip() hint_text = generate_formatted_hint(player, step, hint_raw, hint_kind='location', hint_index=idx) player.location_hint_index = idx + 1 else: # No more location hints available; ask AI to give a gentle generic hint hint_text = generate_formatted_hint(player, step, step.location_hint or "", hint_kind='location', hint_index=idx) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=hint_text)) db.session.commit() return jsonify({"reply": hint_text}) # 3) We're waiting for the answer to the question if player.stage == 'awaiting_answer': if matches_any(text, step.answer or ""): # Correct answer: generate success and advance success = generate_success_text(player, step) player.current_step += 1 # After success, wait for player's confirmation before sending next intro player.stage = 'awaiting_ready_after_success' db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=success)) db.session.commit() # send next intro later after confirmation; prepare ready prompt as followup if next exists next_step = Step.query.filter_by(step=player.current_step).first() if next_step: next_intro = generate_step_intro(player, next_step) player.last_sent_step = player.current_step db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=next_intro)) db.session.commit() ready_prompt = "Bravo Mag ! T'es tu prête pour la suite ?" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt)) db.session.commit() return jsonify({"reply": success, "followup": ready_prompt}) else: reply = success + "\n\nTu as terminé toutes les étapes. Félicitations !" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply)) db.session.commit() return jsonify({"reply": reply}) else: # Incorrect answer: provide next question hint (one at a time) raw_qhints = (step.question_hint or "").split(";") if step.question_hint else [] qidx = int(getattr(player, 'question_hint_index', 0)) if qidx < len(raw_qhints) and raw_qhints[qidx].strip() != "": qhint_raw = raw_qhints[qidx].strip() qhint_text = generate_formatted_hint(player, step, qhint_raw, hint_kind='question', hint_index=qidx) player.question_hint_index = qidx + 1 else: qhint_text = generate_formatted_hint(player, step, step.question_hint or "", hint_kind='question', hint_index=qidx) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=qhint_text)) db.session.commit() return jsonify({"reply": qhint_text}) # Log réponse Katniss db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply)) db.session.commit() return jsonify({"reply": reply}) @app.route("/api/admin/reset_player", methods=["POST"]) def reset_player(): data = request.json or {} secret = data.get("secret") matrix_id = data.get("matrix_id") if secret != FLASK_SHARED_SECRET: return jsonify({"error": "Forbidden"}), 403 if not matrix_id: return jsonify({"error": "matrix_id_required"}), 400 player = Player.query.filter_by(matrix_id=matrix_id).first() if not player: # Create player and set initial stage to awaiting_ready so we can prompt them player = Player( matrix_id=matrix_id, current_step=1, stage='awaiting_ready', location_hint_index=0, question_hint_index=0, last_sent_step=None, ) db.session.add(player) db.session.commit() # Prepare and log the ready prompt so the bot can forward it to the user ready_prompt = "Partie initialisée. Es-tu prête à commencer les jeux ?" db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt)) db.session.commit() return jsonify({"status": "created_and_reset", "current_step": player.current_step, "reply": ready_prompt}) # Efface l’historique messages joueur seulement (pas global) MessageLog.query.filter_by(player_id=player.id).delete() player.current_step = 1 # After reset, ask the player if they're ready to start player.stage = 'awaiting_ready' player.location_hint_index = 0 player.question_hint_index = 0 player.last_sent_step = None db.session.commit() ready_prompt = "Partie réinitialisée. T'es-tu prête à recommencer les jeux ? " # Log prompt db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt)) db.session.commit() log.info(f"Player {matrix_id} reset to step 1 and awaiting ready confirmation.") # Return the ready prompt to be sent to the player return jsonify({"status": "reset_ok", "current_step": player.current_step, "reply": ready_prompt}) @app.route("/api/admin/push_message", methods=["POST"]) def admin_push(): data = request.json matrix_id = data.get("matrix_id") text = data.get("text") player = Player.query.filter_by(matrix_id=matrix_id).first() if not player: return jsonify({"error": "player_not_found"}), 404 db.session.add(MessageLog(player_id=player.id, sender="admin", content=text)) db.session.commit() return jsonify({"status": "ok"}) @app.route("/api/admin/gen_success", methods=["POST"]) def gen_success(): """Generate the AI success text for the player's current step (debug endpoint). POST JSON: { "secret": , "matrix_id": "@user:server" } Returns: { status: ok, reply: , current_step: n } """ data = request.json or {} secret = data.get("secret") matrix_id = data.get("matrix_id") if secret != FLASK_SHARED_SECRET: return jsonify({"error": "Forbidden"}), 403 if not matrix_id: return jsonify({"error": "matrix_id_required"}), 400 player = Player.query.filter_by(matrix_id=matrix_id).first() if not player: return jsonify({"error": "player_not_found"}), 404 step = Step.query.filter_by(step=player.current_step).first() if not step: return jsonify({"error": "step_not_found"}), 404 success = generate_success_text(player, step) # Log the generated success text for debugging (do not advance the player) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=success)) db.session.commit() return jsonify({"status": "ok", "reply": success, "current_step": player.current_step}) @app.route("/api/admin/gen_question", methods=["POST"]) def gen_question(): """Generate the AI question text for the player's current step (debug endpoint).""" data = request.json or {} secret = data.get("secret") matrix_id = data.get("matrix_id") if secret != FLASK_SHARED_SECRET: return jsonify({"error": "Forbidden"}), 403 if not matrix_id: return jsonify({"error": "matrix_id_required"}), 400 player = Player.query.filter_by(matrix_id=matrix_id).first() if not player: return jsonify({"error": "player_not_found"}), 404 step = Step.query.filter_by(step=player.current_step).first() if not step: return jsonify({"error": "step_not_found"}), 404 qtext = generate_question_text(player, step) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=qtext)) db.session.commit() return jsonify({"status": "ok", "reply": qtext, "current_step": player.current_step}) @app.route("/api/admin/gen_intro", methods=["POST"]) def gen_intro(): """Generate the AI intro text for the player's current step (debug endpoint).""" data = request.json or {} secret = data.get("secret") matrix_id = data.get("matrix_id") if secret != FLASK_SHARED_SECRET: return jsonify({"error": "Forbidden"}), 403 if not matrix_id: return jsonify({"error": "matrix_id_required"}), 400 player = Player.query.filter_by(matrix_id=matrix_id).first() if not player: return jsonify({"error": "player_not_found"}), 404 step = Step.query.filter_by(step=player.current_step).first() if not step: return jsonify({"error": "step_not_found"}), 404 intro = generate_step_intro(player, step) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=intro)) db.session.commit() return jsonify({"status": "ok", "reply": intro, "current_step": player.current_step}) @app.route("/api/admin/gen_hint", methods=["POST"]) def gen_hint(): """Generate a single formatted hint for the player's current step (debug endpoint). POST JSON: { "secret": , "matrix_id": "@user:server", "hint_kind": "location"|"question", "hint_index": 0, "hint_text": "optional raw hint" } If hint_text is provided it is used directly. Otherwise the endpoint picks the hint at hint_index from the requested hint_kind field. """ data = request.json or {} secret = data.get("secret") matrix_id = data.get("matrix_id") hint_kind = data.get("hint_kind", "location") hint_index = int(data.get("hint_index", 0) or 0) hint_text_override = data.get("hint_text") if secret != FLASK_SHARED_SECRET: return jsonify({"error": "Forbidden"}), 403 if not matrix_id: return jsonify({"error": "matrix_id_required"}), 400 player = Player.query.filter_by(matrix_id=matrix_id).first() if not player: return jsonify({"error": "player_not_found"}), 404 step = Step.query.filter_by(step=player.current_step).first() if not step: return jsonify({"error": "step_not_found"}), 404 if hint_text_override and str(hint_text_override).strip() != "": hint_raw = str(hint_text_override).strip() else: if hint_kind == 'question': raw_list = (step.question_hint or "").split(";") if step.question_hint else [] else: raw_list = (step.location_hint or "").split(";") if step.location_hint else [] if 0 <= hint_index < len(raw_list): hint_raw = raw_list[hint_index].strip() else: # fallback to full hint field hint_raw = (step.question_hint if hint_kind == 'question' else step.location_hint) or "" hint_out = generate_formatted_hint(player, step, hint_raw, hint_kind=hint_kind, hint_index=hint_index) db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=hint_out)) db.session.commit() return jsonify({"status": "ok", "reply": hint_out, "current_step": player.current_step}) # ... Commande init-db inchangée ... # --------- Init DB ---------- @app.cli.command("init-db") def init_db(): db.drop_all() db.create_all() steps = fetch_steps_from_gsheet() for step in steps: db.session.add(step) db.session.commit() print(f"{len(steps)} étapes importées avec succès depuis Google Sheet !") if __name__ == "__main__": with app.app_context(): db.create_all() app.run(host="0.0.0.0", port=5000, debug=os.getenv("DEBUG", "false").lower() == "true")