Les outils et l'état user:
1. Installer
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # puis mettez votre clé dans GOOGLE_API_KEYLe fichier .env.example contient des valeurs factices. python-dotenv lit le .env au démarrage ; ne le versionnez jamais.
2. La règle d'adaptation, en pur Python
def ajuster_niveau(niveau: int, serie: list[bool]) -> tuple[int, list[bool]]:
"""Règle déterministe : 2 bonnes réponses d'affilée -> +1, 2 erreurs d'affilée -> -1."""
if serie[-2:] == [True, True] and niveau < 5:
return niveau + 1, []
if serie[-2:] == [False, False] and niveau > 1:
return niveau - 1, []
return niveau, serie[-2:]La série (les dernières réponses depuis le dernier changement) repart de zéro quand le niveau change : sans cela, une troisième bonne réponse ferait monter l'élève une seconde fois.
Avec cette règle, un élève au niveau 2 répond juste, juste, juste. Quel est son niveau ?
Après deux bonnes réponses, il passe au niveau 3 et la série repart de []. La troisième bonne réponse donne une série [True] : il faudra une quatrième bonne réponse pour monter au niveau 4.
3. Des outils qui écrivent dans l'état
Un outil ADK est une fonction Python ordinaire : ADK lit sa signature et sa docstring pour décrire l'outil au modèle. Si la fonction déclare un paramètre tool_context: ToolContext, ADK le remplit automatiquement (le modèle ne le voit pas) : il donne accès à tool_context.state.
def enregistrer_reponse(theme: str, notion: str, correcte: bool, tool_context: ToolContext) -> dict:
"""Enregistre la réponse de l'élève et recalcule le niveau du thème.
Args:
theme: le thème de la question (ex. "python").
notion: la notion testée, en 2 à 4 mots (ex. "compréhension de liste").
correcte: True si la réponse de l'élève est juste.
"""
progression = dict(tool_context.state.get("user:progression", {}))
fiche = dict(progression.get(theme.lower()) or {"niveau": 2, "serie": [], "bonnes": 0, "total": 0, "a_revoir": []})
ancien = fiche["niveau"]
fiche["total"] += 1
fiche["bonnes"] += int(correcte)
a_revoir = [n for n in fiche["a_revoir"] if n != notion]
fiche["a_revoir"] = a_revoir if correcte else (a_revoir + [notion])[-5:]
fiche["niveau"], fiche["serie"] = ajuster_niveau(ancien, fiche["serie"] + [correcte])
progression[theme.lower()] = fiche
tool_context.state["user:progression"] = progression
return {"niveau": fiche["niveau"], "libelle": NIVEAUX[fiche["niveau"]],
"changement": fiche["niveau"] - ancien, "score": f"{fiche['bonnes']}/{fiche['total']}"}L'outil definir_niveau(theme, niveau) suit le même modèle : il crée la fiche du thème après le test de positionnement. Le code complet est dans agent.py.
flowchart TB
M["Gemini décide : la réponse est juste"] --> C["appel enregistrer_reponse(theme, notion, correcte=True)"]
C --> F["ajuster_niveau (Python)"]
F --> W["tool_context.state['user:progression'] = ..."]
W --> D["Événement avec state_delta"]
D --> S[("Service de session")]
C --> R["Résultat renvoyé au modèle : niveau, changement, score"]
4. L'instruction et l'injection d'état
Dans l'instruction d'un agent, {cle} est remplacé par la valeur de session.state["cle"] à chaque tour ; {cle?} (avec un point d'interrogation) ne lève pas d'erreur si la clé est absente.
INSTRUCTION = f"""Tu es un tuteur patient qui parle français et tutoie l'élève.
Niveaux : {NIVEAUX}.
Progression mémorisée de l'élève (vide = nouvel élève) : {{user:progression?}}
Déroulé :
1. Si le thème demandé n'est pas dans la progression : pose 2 questions de positionnement
(une facile, une plus difficile), UNE à la fois, puis appelle definir_niveau.
Si le thème est connu : salue l'élève, rappelle son niveau et ses notions à revoir.
2. Pose UNE question à la fois, adaptée au niveau du thème. Privilégie les notions à revoir.
3. Quand l'élève répond : juge la réponse, appelle enregistrer_reponse, puis donne un retour
court (pourquoi c'est juste ou faux) et enchaîne avec la question suivante au niveau renvoyé.
Si "changement" vaut 1 ou -1, annonce le changement de niveau.
Ne donne jamais la réponse avant que l'élève ait essayé. Reste concis (moins de 120 mots)."""
root_agent = Agent(
name="tuteur",
model=os.environ.get("MODEL", "gemini-3.5-flash"),
instruction=INSTRUCTION,
tools=[definir_niveau, enregistrer_reponse],
)Les doubles accolades {{user:progression?}} sont nécessaires parce que la chaîne est une f-string Python : après formatage, ADK reçoit bien {user:progression?}. Le dictionnaire {1: 'débutant', ...} produit par {NIVEAUX} contient lui aussi des accolades, mais ce n'est pas un nom de clé valide : ADK le laisse tel quel.
Que voit le modèle si l'instruction contient {user:progression?} et que l'élève est nouveau ?
Le point d'interrogation rend la variable optionnelle : ADK la remplace par une chaîne vide si la clé n'existe pas. Sans « ? », une clé absente provoque une erreur.
Exercice : un compteur de séances
Ajoutez à l'état user: le nombre de questions posées aujourd'hui et faites dire au tuteur « Bravo, 10 questions aujourd'hui ! » à la dixième. Quelle clé, quel outil modifier, et que faut-il ajouter à l'instruction ?
Voir la solution
Stockez une clé user:jour (date ISO) et user:questions_du_jour, mises à jour dans enregistrer_reponse :
from datetime import date
aujourdhui = date.today().isoformat()
if tool_context.state.get("user:jour") != aujourdhui:
tool_context.state["user:jour"] = aujourdhui
tool_context.state["user:questions_du_jour"] = 0
tool_context.state["user:questions_du_jour"] = tool_context.state["user:questions_du_jour"] + 1Renvoyez la valeur dans le dictionnaire de résultat ("questions_du_jour": ...) et ajoutez à l'instruction : « Si questions_du_jour vaut 10, félicite l'élève. » Le calcul reste dans le code, le modèle ne fait que le formuler.
Résultat obtenu
Appels d'outils réellement émis par Gemini pendant la démonstration du 2026-10-15 :
[outil] definir_niveau {'niveau': 3, 'theme': 'python'}
[outil] enregistrer_reponse {'notion': 'argument par défaut mutable', 'theme': 'python', 'correcte': True}
[outil] enregistrer_reponse {'theme': 'python', 'notion': 'méthode dictionnaire get', 'correcte': True}
[outil] enregistrer_reponse {'theme': 'python', 'notion': 'décorateurs de fonction', 'correcte': False}
Progression enregistrée : {'python': {'niveau': 4, 'serie': [False], 'bonnes': 2, 'total': 3,
'a_revoir': ['décorateurs de fonction']}}Deux bonnes réponses au niveau 3 ont fait passer l'élève au niveau 4, puis l'erreur a démarré une nouvelle série [False] et ajouté la notion à revoir.
Je sais faire
- Écrire un outil ADK qui reçoit
tool_contextet modifie l'état - Expliquer pourquoi on réaffecte la clé d'état au lieu de la modifier en place
- Injecter une clé d'état dans l'instruction avec
{cle?} - Garder les règles métier dans des fonctions testables