Files
2025-08-27 16:54:36 +02:00

802 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 lhistorique 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": <shared_secret>, "matrix_id": "@user:server" }
Returns: { status: ok, reply: <text>, 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": <shared_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")