1. Qu’est-ce que le contexte de workflow et pourquoi en a‑t‑on besoin
Dans une application web classique, vous avez une idée assez claire d’où vit l’état : base de données, cache, plus quelque chose côté front comme Redux ou l’état local de React. Dans une ChatGPT App, c’est plus amusant : l’état est réparti entre trois mondes — à l’intérieur du modèle (historique du dialogue), dans le widget (état UI) et sur votre serveur/MCP (données métier).
Par contexte de workflow, nous entendrons l’ensemble des données nécessaires pour répondre aux questions « à quelle étape en sommes‑nous » et « qu’est‑ce qui est déjà connu ». Pour notre projet pédagogique GiftGenius, le contexte inclut :
- le profil du destinataire du cadeau : âge, sexe, centres d’intérêt ;
- le budget et, éventuellement, la devise ;
- la liste des idées générées et lesquelles l’utilisateur a liké ou masquées ;
- des aspects techniques : identifiant de session ou de workflow, statut (« profile_collected », « ideas_shown », « checkout_started »).
Ce contexte n’est pas utile qu’à vous, développeur backend. Il est nécessaire au modèle lui‑même pour comprendre quelles questions ont déjà été posées, quels outils ont déjà été invoqués et de quoi il est question à l’instant. Et il est utile à l’utilisateur pour ne pas repartir de zéro lorsqu’il revient dans le chat.
L’utilisateur pense intuitivement que « ChatGPT se souvient de tout ». En réalité, le modèle ne se souvient que du texte du dialogue, tant qu’il tient dans la fenêtre de contexte. Des éléments structurés comme order_id, cart_id ou « liste des idées likées » doivent être stockés sur votre serveur, sinon vous obtiendrez une machine parfaite à produire des affirmations convaincantes mais erronées.
2. Trois niveaux d’état : UI, LLM, métier
Le plus simple est de concevoir la conservation du contexte via un modèle à trois couches d’état. C’est la « State Triad ».
Table des niveaux
Utilisons un petit tableau :
| Niveau | Où il vit | Cycle de vie | Responsable de | Exemple dans GiftGenius |
|---|---|---|---|---|
| UI State | Widget (React, widgetState) | Tant que le chat / le message avec le widget est ouvert | État visuel, saisie locale | Quelles cartes sont mises en surbrillance, état du formulaire |
| LLM Context | Historique du chat chez OpenAI | Tant que le message « tient » dans le contexte | Compréhension du dialogue et raisonnement | « Nous cherchons un cadeau pour maman, budget 50 $ » |
| Business State | MCP / votre backend (base de données/Redis) | Aussi longtemps que vous le décidez (persistant) | Source de vérité : données vérifiées, statuts | { step: "ideas", budget: 50, liked: [42, 51] } |
La couche UI est rapide et réactive, mais très fragile : ChatGPT peut « démonter » l’iframe avec le widget lorsque vous remontez l’historique, puis le remonter plus tard. C’est précisément pour cela qu’existe widgetState, qui vit un peu plus longtemps que le composant React et se synchronise avec le client hôte de ChatGPT.
La couche LLM donne au modèle le sentiment d’un dialogue continu, mais ne stocke que du texte et des appels d’outils. Vous pouvez y placer du JSON avec votre panier, mais cela revient à insérer du JSON dans du texte — le modèle ne le traitera pas comme une base de données.
La couche métier est celle que vous pouvez contrôler en tant qu’ingénieur : on y place des données validées, des index, des statuts de commande. Dès que votre scénario devient sérieux (cadeaux, réservation, apprentissage), cette couche doit devenir la source principale de vérité sur l’état.
Le principal problème d’ingénierie est d’éviter que ces trois couches divergent. L’utilisateur change le budget dans le widget, le modèle raisonne encore avec l’ancien, et la base contient une troisième valeur — voilà une recette classique pour des comportements étranges.
3. Ce que nous sauvegardons exactement : structure de WorkflowContext
Pour être concrets, décrivons en TypeScript l’interface de contexte pour GiftGenius. Supposons que nous ayons déjà plusieurs étapes : collecte du profil, choix du budget, génération d’idées et affichage/likes.
Commençons par une structure simple :
// backend/types/workflow.ts
export type GiftWorkflowStep =
| "profile"
| "budget"
| "ideas"
| "checkout";
export interface GiftWorkflowContext {
id: string; // workflowId — identifiant du scénario
userId?: string; // si l’authentification est déjà configurée
currentStep: GiftWorkflowStep;
profile?: {
age?: number;
gender?: string;
interests?: string[];
};
budget?: {
min?: number;
max?: number;
currency: string;
};
ideas?: {
id: string;
title: string;
}[];
likedIdeaIds: string[];
hiddenIdeaIds: string[];
updatedAt: number; // timestamp pour TTL/nettoyage
}
Ce n’est pas le schéma final, mais les éléments importants sont déjà en place. Il y a :
- un identifiant de workflow, grâce auquel nous rechercherons ce contexte ;
- l’étape courante, qui aidera à la fois le widget et le modèle à comprendre où nous en sommes ;
- un ensemble de champs qui seront remplis à différentes étapes ;
- des champs techniques comme l’heure de mise à jour.
À part, parlons des identifiants. Dans ce cours, par workflowId nous entendons l’identifiant d’un scénario précis dans notre backend/MCP. Il peut coïncider avec l’identifiant de session du dialogue ChatGPT (sessionId), mais nous ne comptons pas dessus. userId est l’identifiant d’utilisateur de votre système d’authentification (s’il existe) ; un même utilisateur peut avoir plusieurs workflows actifs. Le champ id contient justement ce workflowId, à partir duquel nous cherchons et mettons à jour le contexte.
Dans les sections suivantes, nous verrons trois choses : où stocker ces objets, comment les y écrire et comment les récupérer — vers le widget et vers le modèle.
4. Où stocker l’état : options et compromis
Il est utile de réfléchir à la conservation de l’état selon deux axes : où il est stocké et combien de temps il vit. Dans cette section, nous nous concentrons sur le lieu de stockage ; nous reviendrons plus loin sur la durée de vie dans le check‑list et le bloc sur les erreurs typiques.
Commençons par le lieu de stockage.
À l’intérieur du dialogue (dans le prompt)
Parfois on se dit : « Renvoyons à chaque fois au modèle un JSON avec l’état courant, et qu’il se débrouille. » Cela fonctionne pour des scénarios très simples et de courtes chaînes d’étapes, mais bute vite sur deux problèmes : la limite de longueur du contexte et l’absence de toute garantie d’intégrité des données.
De plus, le protocole MCP est par nature stateless : comme HTTP, il ne conserve aucun état entre les requêtes par défaut. Pour rattacher un appel d’outil à une session précise, vous devez transmettre explicitement un identifiant — workflow ou session id — soit dans les arguments de l’outil, soit via des métadonnées/en‑têtes.
Par conséquent, stocker l’état métier uniquement dans le dialogue est plutôt une expérience pédagogique qu’une architecture.
Dans le widget : UI + widgetState
Au niveau UI, nous utilisons l’état React habituel (useState, useReducer, etc.), mais, comme déjà dit, le composant peut être démonté. Dans l’Apps SDK, il existe pour cela le mécanisme widgetState, qui vit en dehors de React et se synchronise avec l’hôte ChatGPT. Si, au montage du widget, vous récupérez la valeur sauvegardée et, lors des changements, vous la réécrivez, vous obtenez un stockage local assez pratique.
Ce stockage convient très bien pour l’état purement visuel : quelles cartes sont repliées, quel onglet est actif, ce que l’utilisateur a saisi dans le formulaire avant d’appuyer sur « Suivant ». Mais il ne remplace pas le serveur : dès que l’utilisateur ouvre le chat sur un autre appareil ou après une semaine, widgetState peut ne plus aider. Et y implémenter la logique métier est discutable.
Sur le serveur/MCP : Map, Redis, base de données
Enfin, l’option principale pour la production : nous stockons GiftWorkflowContext côté serveur MCP ou service backend. Puisque le client et le serveur MCP sont stateless par protocole, nous devons transmettre workflowId (ou state_token) dans chaque appel d’outil pour savoir quel contexte mettre à jour.
Plusieurs options d’implémentation :
- Map en mémoire dans Node.js — adaptée aux démos et à l’environnement de dev : tout est rapide, mais disparaît au redémarrage ;
- Redis ou un autre cache en mémoire avec TTL — bien pour des scénarios d’assistant en quelques étapes : ça vit une à deux heures, puis peut être supprimé ;
- une base SQL/NoSQL classique — indispensable pour les scénarios du type « revenir après une semaine » ou « brouillons et paniers ».
Dans ce cours, nous n’entrerons pas dans le détail d’une base concrète ; nous nous concentrerons sur l’interface et sur ce qui doit y entrer.
5. Stockage minimal sur le serveur MCP : Map par workflowId
Commençons par quelque chose de terre‑à‑terre : une simple Map en mémoire dans le serveur MCP, où la clé est le workflowId. Dans une démo pédagogique, on peut l’assimiler à l’sessionId du dialogue, mais en production il vaut mieux garder workflowId comme identifiant séparé du scénario. La valeur de cette Map sera GiftWorkflowContext. En production réelle, vous remplacerez cela par Redis ou une base de données, mais l’API restera la même.
Supposons que notre serveur MCP soit en TypeScript. Ajoutons ceci près de l’initialisation :
// mcp/workflowStore.ts
import { GiftWorkflowContext } from "../backend/types/workflow";
const workflows = new Map<string, GiftWorkflowContext>();
export function getWorkflow(id: string): GiftWorkflowContext | undefined {
return workflows.get(id);
}
export function saveWorkflow(ctx: GiftWorkflowContext): void {
workflows.set(ctx.id, { ...ctx, updatedAt: Date.now() });
}
Ensuite — un outil qui enregistre le profil du destinataire. L’important est qu’il accepte le workflowId et les données du profil, et qu’il mette à jour/crée le contexte correspondant à l’intérieur :
// mcp/tools/setProfile.ts
import { jsonSchema } from "@modelcontextprotocol/sdk"; // alias
import { getWorkflow, saveWorkflow } from "../workflowStore";
export const setProfileTool = {
name: "gift_set_profile",
description: "Enregistre le profil du destinataire du cadeau",
inputSchema: jsonSchema.object({
workflowId: jsonSchema.string(),
age: jsonSchema.number().optional(),
gender: jsonSchema.string().optional(),
interests: jsonSchema.array(jsonSchema.string()).optional()
}),
async run(input: any) {
const existing = getWorkflow(input.workflowId);
const ctx = existing ?? {
id: input.workflowId,
currentStep: "profile",
likedIdeaIds: [],
hiddenIdeaIds: []
};
ctx.profile = {
age: input.age,
gender: input.gender,
interests: input.interests ?? []
};
ctx.currentStep = "budget";
saveWorkflow(ctx);
return {
structuredContent: {
type: "profileSaved",
workflowId: ctx.id,
profile: ctx.profile,
nextStep: ctx.currentStep
}
};
}
};
Cet outil résout déjà deux tâches : enregistrer le profil et faire avancer currentStep à l’étape suivante. Dans un projet réel, vous souhaiterez peut‑être séparer les outils « enregistrer des données » et « passer à l’étape », mais pour comprendre le concept, cette variante convient.
Notez le workflowId dans les arguments : c’est ce paramètre qui rattache l’appel d’outil au bon contexte. La partie cliente (widget ou agent) doit le stocker quelque part et le transmettre.
6. Liaison avec l’Apps SDK : où obtenir workflowId et sessionId
La question « d’où vient le workflowId » dans les ChatGPT Apps est un peu philosophique. Les possibilités dépendent de l’utilisation ou non de l’authentification, de MCP directement ou de l’Agents SDK. En gros, deux options : génération côté serveur lors du premier appel d’outil ou génération dans le widget et transmission vers le bas.
Pour l’exemple pédagogique, supposons que la première étape soit l’appel d’un outil MCP qui crée le workflow, puis que le widget ne fasse que récupérer son id.
La variante la plus simple :
// mcp/tools/startWorkflow.ts
import { randomUUID } from "crypto";
import { saveWorkflow } from "../workflowStore";
export const startWorkflowTool = {
name: "gift_start_workflow",
description: "Crée un nouveau workflow de sélection de cadeaux",
inputSchema: { type: "object", properties: {} },
async run() {
const id = randomUUID();
saveWorkflow({
id,
currentStep: "profile",
likedIdeaIds: [],
hiddenIdeaIds: [],
updatedAt: Date.now()
});
return {
structuredContent: {
type: "workflowStarted",
workflowId: id,
currentStep: "profile"
}
};
}
};
Ensuite, le modèle, ayant reçu le workflowId dans la réponse de l’outil, peut :
- le conserver de manière cachée dans le contexte ;
- le transmettre au widget via structuredContent, pour que le widget l’enregistre dans widgetState et commence à le fournir lors des appels suivants aux outils.
Côté widget, le code ressemblera à ceci.
7. Stocker workflowId et l’état UI local dans le widget
Supposons que nous ayons un widget de liste d’idées, qui souhaite connaître le workflow qu’il affiche et se souvenir des likes locaux, même si le composant est démonté. En version simplifiée :
// app/widgets/GiftIdeasWidget.tsx
import { useEffect, useState } from "react";
interface Idea {
id: string;
title: string;
}
interface WidgetProps {
widgetId: string;
workflowId: string; // provenant de structuredContent
ideas: Idea[];
}
interface UiState {
liked: string[];
}
export function GiftIdeasWidget(props: WidgetProps) {
const [uiState, setUiState] = useState<UiState>({ liked: [] });
useEffect(() => {
window.openai.getWidgetState<UiState>(props.widgetId).then(saved => {
if (saved) setUiState(saved);
});
}, [props.widgetId]);
function toggleLike(id: string) {
const exists = uiState.liked.includes(id);
const next: UiState = {
liked: exists
? uiState.liked.filter(x => x !== id)
: [...uiState.liked, id]
};
setUiState(next);
window.openai.setWidgetState(props.widgetId, next);
// on peut aussi déclencher ici l’outil MCP "gift_like_idea"
}
return (
<ul>
{props.ideas.map(idea => (
<li key={idea.id}>
{idea.title}
<button onClick={() => toggleLike(idea.id)}>
{uiState.liked.includes(idea.id) ? "★" : "☆"}
</button>
</li>
))}
</ul>
);
}
Ici, widgetState est utilisé strictement comme couche UI : nous nous souvenons des idées mises en évidence. En bonne pratique, il faut aussi envoyer les likes au serveur (via un outil MCP ou un endpoint API dans Next.js) pour que la couche métier sache également ce que l’utilisateur a choisi.
Il est important de ne pas essayer de construire tout le workflow sur widgetState. Il doit rester une couche additionnelle au contexte métier côté serveur.
8. Restauration du scénario : l’utilisateur est revenu
Passons maintenant à un cas plus intéressant : l’utilisateur ferme ChatGPT, revient après quelques heures ou jours et rouvre le même chat. Que doit‑il se passer ?
L’UX idéale est la suivante : le modèle et l’App comprennent que l’utilisateur a déjà un workflow en cours, récupèrent son contexte et disent quelque chose comme : « Vous avez déjà indiqué le profil et le budget, continuons avec le choix des idées. »
Architecturalement, cela ressemble à ceci :
- Votre serveur stocke un GiftWorkflowContext, rattaché à un certain userId ou au moins à un workflowId interne.
- Lors d’une nouvelle requête (ou du premier appel d’outil dans le cadre du dialogue), l’App interroge le serveur : « Existe‑t‑il un workflow actif pour cet utilisateur ? »
- Si oui, le serveur le renvoie et, éventuellement, un indicateur resume que le modèle utilise dans sa réponse.
Dans une simple démo monolithique, on peut considérer que le serveur MCP et l’application Next.js vivent dans le même dépôt (voire le même processus), et donc nous réutilisons le même workflowStore du côté MCP et dans les routes API.
Dans Next.js, cela peut être une simple route API :
// app/api/gift/workflow/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getWorkflow } from "@/mcp/workflowStore"; // dans cette démo, MCP et Next.js partagent le même stockage
export async function GET(req: NextRequest) {
const id = req.nextUrl.searchParams.get("workflowId");
if (!id) return NextResponse.json({ error: "Missing workflowId" }, { status: 400 });
const ctx = getWorkflow(id);
if (!ctx) return NextResponse.json({ exists: false });
return NextResponse.json({
exists: true,
context: ctx
});
}
Le widget (ou l’outil MCP) peut appeler cet endpoint quand il a besoin de rafraîchir l’état : par exemple, au premier montage ou lors d’un changement d’étape. Dans la configuration pédagogique, l’association workflowId + stockage dans une Map suffit ; en production réelle, vous y ajouterez l’autorisation et la vérification d’appartenance à l’utilisateur.
Si vous utilisez l’Agents SDK ou une orchestration plus complexe, l’idée peut être étendue à des « checkpoints » — la sauvegarde de l’état aux extrémités d’étapes majeures, à partir desquelles l’agent peut reprendre après un redémarrage. Mais cela fera l’objet du module suivant.
9. Avancer/reculer et historique des étapes
La question surgit inévitablement : « Peut‑on revenir d’une étape en arrière ? » Pour l’utilisateur, c’est très naturel : modifier le budget, corriger les centres d’intérêt, retirer un article superflu de la sélection.
Techniquement, cela signifie deux choses :
- il faut stocker non seulement l’étape courante, mais aussi l’historique des décisions prises ;
- il faut recalculer avec soin les données dérivées après un rollback.
Une des options est d’ajouter au contexte un champ history, qui contiendra des instantanés d’étapes. Par exemple :
export interface StepSnapshot {
step: GiftWorkflowStep;
payload: any; // données spécifiques à l’étape
createdAt: number;
}
export interface GiftWorkflowContext {
// ...champs précédents
history: StepSnapshot[];
}
Lorsque l’utilisateur remplit le profil, vous ajoutez à l’historique un instantané step : "profile". Quand il change le budget — un autre instantané. Lors d’un retour au profil, vous :
- mettez à jour currentStep = "profile" ;
- coupez éventuellement l’historique jusqu’à l’index voulu ;
- recalculez les valeurs dérivées (par exemple, vider les idées et les likes si elles dépendent du budget).
Au niveau du modèle, la synchronisation est importante : si l’utilisateur appuie sur le bouton « Retour » dans le widget, il faut envoyer un appel d’outil qui mettra à jour le contexte métier et renverra dans sa réponse une description explicite du nouvel état. Sinon, vous obtiendrez le problème classique de désynchronisation : l’UI affiche l’étape 2, alors que le modèle est persuadé que vous êtes à l’étape 3.
Au niveau du widget, le rollback peut ressembler à un simple bouton :
async function goBackToProfile() {
await fetch("/api/gift/workflow/back", {
method: "POST",
body: JSON.stringify({ workflowId, targetStep: "profile" })
});
// on met à jour l’UI, on nettoie l’état local
}
Et c’est le serveur qui décidera quoi nettoyer dans le contexte et quel message envoyer au modèle via la réponse de l’outil.
10. Relier tout cela au modèle : contexte pour le raisonnement
Tout ce que nous faisons avec l’état est, in fine, nécessaire non seulement à l’utilisateur, mais aussi au LLM. Le modèle doit comprendre :
- ce qui est déjà connu (par exemple, le profil du destinataire et le budget) ;
- quelles étapes ont déjà été franchies ;
- s’il existe des processus inachevés.
La façon d’acheminer ces informations vers le modèle dépend de l’architecture de l’App : vous pouvez les injecter dans le system prompt, les renvoyer dans ToolOutput sous forme structurée, ou utiliser des champs spéciaux _meta/annotations si l’SDK les prend en charge.
Un schéma typique :
- L’outil MCP renvoie dans structuredContent un instantané succinct du contexte : l’étape actuelle, les champs clés et, éventuellement, le workflowId.
- L’Apps SDK transforme cela en widget ou en texte + données cachées.
- Le modèle, voyant le structuredContent, comprend que le scénario a repris et construit la suite en conséquence.
Dans certains cas, si le modèle a « oublié » des paramètres importants ou commence à halluciner, vous pouvez forcer une mise à jour du contexte : appeler un outil spécial qui renverra l’état à jour, et le modèle « se remettra dans le contexte ».
Il est important de ne pas essayer d’injecter dans le modèle tout le GiftWorkflowContext jusqu’au dernier champ. Les éléments clés suffisent : pour qui cherchons‑nous un cadeau, quel budget, combien d’idées ont déjà été affichées, y a‑t‑il un checkout en cours.
11. Mini check‑list pour concevoir un WorkflowContext
Avant de passer aux erreurs typiques, il est utile de formuler un petit ensemble de questions auxquelles vous devez répondre lorsque vous concevez le contexte de workflow (vous pouvez littéralement les noter à côté de l’interface) :
- Quelles étapes comporte le scénario et quel ensemble minimal de données est nécessaire à chacune ?
Cela vous protégera des monstres JSON géants « au cas où ». - Que faut‑il mémoriser uniquement dans le cadre d’un chat, et quoi entre les sessions et les appareils ?
Le premier peut rester dans widgetState et les prompts, le second doit absolument aller dans la base côté serveur. - À quoi ressemblera l’identifiant du contexte ?
Cela peut être un couple userId + scenario, un workflowId séparé, ou les deux. L’essentiel est de pouvoir retrouver sans ambiguïté le contexte dans la base. - Comment allez‑vous nettoyer les anciens workflows ?
Pour une démo, « ne jamais nettoyer » est acceptable, mais en production vous aurez besoin d’un TTL ou de jobs en arrière‑plan qui suppriment les workflows anciens. - L’utilisateur a‑t‑il besoin d’un retour en arrière et comment l’implémenterez‑vous ?
Allez‑vous stocker un arbre de branches ou une liste linéaire d’étapes avec possibilité de rollback suffit‑elle ?
Et pour finir : simulez mentalement le scénario « l’utilisateur est revenu une semaine plus tard dans un autre chat ». Si vous ne pouvez pas expliquer comment l’App saura qu’il existe un ancien workflow et quoi lui afficher, il faut renforcer la partie stockage persistant.
12. Erreurs typiques lors de la gestion du contexte entre les étapes
Erreur n° 1 : tout stocker uniquement dans l’historique du dialogue.
La tentation apparaît parfois : « Le modèle voit tout dans le texte, listons simplement à chaque fois dans le prompt le budget, les produits et les choix de l’utilisateur. » Cette approche se heurte rapidement aux limites de contexte et ne garantit absolument pas l’intégrité : le modèle peut « oublier » un fait important ou confondre des identifiants. Les éléments critiques métier (argent, réservations, commandes) doivent vivre dans votre backend/MCP en tant que source de vérité.
Erreur n° 2 : tenter de construire tout le workflow uniquement sur widgetState.
widgetState dans l’Apps SDK résout la survie de l’état UI entre le démontage du widget et son remontage, pas le stockage long terme du workflow. Si vous essayez d’y stocker le profil, le panier et l’historique des étapes, vous obtiendrez du chaos lors d’un changement d’appareil et l’impossibilité de se restaurer après longtemps. Le widget gère les aspects visuels et le confort local. Toute la logique du scénario doit vivre sur le serveur.
Erreur n° 3 : absence d’un workflowId ou d’une autre clé explicite.
Il arrive que le développeur s’appuie sur des identifiants implicites comme conversation_id sans introduire sa propre notion de workflow. Résultat : impossible de distinguer un scénario d’un autre, de séparer plusieurs workflows en parallèle ou de restaurer précisément celui souhaité. Une simple chaîne workflowId partout où il y a des outils et des endpoints API résout beaucoup de problèmes, surtout en MCP, qui est stateless par protocole.
Erreur n° 4 : mélanger l’état UI et la logique métier.
Cas classique : on met dans widgetState non seulement « quel onglet est ouvert », mais aussi « quels produits sont dans le panier », puis on essaie de prendre des décisions côté serveur à partir de cet état. Au moindre désalignement (le widget s’est affiché mais la requête n’est pas encore arrivée, ou l’inverse), le modèle voit une réalité, l’UI une autre et la base une troisième. La frontière des responsabilités doit être claire : le serveur stocke et valide les données métier, le widget les affiche et permet à l’utilisateur de les modifier.
Erreur n° 5 : absence de scénario de restauration et de rollback.
Il est très facile de dessiner un « happy path » où l’utilisateur suit parfaitement les étapes, rien ne tombe, ChatGPT ne redémarre pas et le tunnel ne se coupe pas. En réalité, chaque étape peut échouer, l’utilisateur peut partir au milieu puis revenir une semaine plus tard. Si vous n’avez pas structuré WorkflowContext, si vous n’avez pas réfléchi à la recherche d’un workflow « actif », et si vous n’avez pas prévu de boutons « Retour » et « Continuer plus tard », votre scénario sera fragile et frustrant pour les utilisateurs. Un contexte bien pensé est la base de la tolérance aux pannes, sujet de la prochaine leçon.
GO TO FULL VERSION