first commit

This commit is contained in:
Sam
2025-08-27 16:54:36 +02:00
commit 779d68dba9
10 changed files with 1298 additions and 0 deletions

801
app.py Normal file
View File

@@ -0,0 +1,801 @@
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")