Flashcard
Question/réponse classique pour la mémorisation rapide. Le format le plus simple et le plus efficace pour le rappel actif.
Cette section présente le flux complet de génération de cartes, depuis l’action utilisateur sur l’application mobile jusqu’à la création des cartes par le service d’intelligence artificielle. Ce pipeline illustre l’interaction entre les trois composants principaux de l’architecture Mindlet.
sequenceDiagram
autonumber
participant U as 📱 Utilisateur
participant M as React Native
participant A as Laravel API
participant Q as Redis Queue
participant IA as LangGraph
participant DB as PostgreSQL
participant WS as WebSocket
U->>M: Upload document + paramètres
M->>A: POST /v1/cards/generate
A->>A: Valider quota & input
A->>Q: Dispatch GenerateCardsJob
A-->>M: 202 Accepted {generation_id}
M->>M: Afficher "En cours..."
Q->>A: Job démarre
A->>WS: CardGenerationStarted
WS-->>M: Event started
M->>M: Mise à jour UI
A->>IA: POST /runs/wait (chunks, params)
Note over IA: Planner → Generator → Critic → Refiner → Finalizer
IA-->>A: {cards, metadata}
A->>WS: CardsGenerated
WS-->>M: Event generated
A->>DB: INSERT cards, collection
A->>WS: CardsPersisted
WS-->>M: Event persisted
A->>A: Consommer quota
A->>WS: CardGenerationCompleted
WS-->>M: Event completed
M->>M: Notification + refresh liste
U->>M: Voir les cartes générées L’utilisateur peut générer des cartes de deux manières :
/** * Mutation pour déclencher la génération de cartes. * * Gère : * - L'envoi des paramètres au backend * - La mise à jour du store de génération * - L'invalidation du cache après complétion */export const useAiCardGeneration = () => { const setGenerationStarted = useAiGenerationStore((s) => s.setGenerationStarted)
return apiClient.useMutation( 'post', '/v1/cards/generate', { onSuccess: (data) => { // Initialiser le tracking côté frontend setGenerationStarted(data.generation_id, data.collection_id)
Toast.show({ type: 'info', text1: 'Génération en cours', text2: 'Vous serez notifié quand les cartes seront prêtes', }) }, onError: (error) => { Toast.show({ type: 'error', text1: 'Erreur', text2: getErrorMessage(error), }) }, } )}Le store Zustand aiGeneration maintient l’état de la génération côté client :
interface AiGenerationState { isGenerating: boolean generationId: string | null collectionId: string | null error: string | null shouldScrollToTop: boolean
setGenerationStarted: (generationId: string, collectionId?: string) => void setGenerationCompleted: (generationId: string) => void setGenerationFailed: (generationId: string, reason?: string) => void reset: () => void}
export const useAiGenerationStore = create<AiGenerationState>((set) => ({ isGenerating: false, generationId: null, collectionId: null, error: null, shouldScrollToTop: false,
setGenerationStarted: (generationId, collectionId) => { set({ isGenerating: true, generationId, collectionId: collectionId ?? null, error: null, }) },
setGenerationCompleted: (generationId) => { set({ isGenerating: false, shouldScrollToTop: true, // Auto-scroll vers les nouvelles cartes }) },
setGenerationFailed: (generationId, reason) => { set({ isGenerating: false, error: reason ?? 'Une erreur est survenue', }) },}))Le composant AiWebSocketListener reçoit les événements broadcast par le backend :
export function AiWebSocketListener() { const echo = useEchoStore((s) => s.echo) const userId = useAuthStore((s) => s.user?.id) const store = useAiGenerationStore()
useEffect(() => { if (!echo || !userId) return
const channel = echo.private(`users.${userId}`)
// 6 événements possibles channel.listen('.card.generation.started', (e) => { store.setGenerationStarted(e.generation_id, e.collection_id) })
channel.listen('.card.generation.generated', (e) => { // Cartes générées par l'IA, en cours de persistance })
channel.listen('.card.generation.collection_created', (e) => { // Nouvelle collection créée pour les cartes })
channel.listen('.card.generation.cards_persisted', (e) => { // Cartes sauvegardées en base })
channel.listen('.card.generation.completed', (e) => { store.setGenerationCompleted(e.generation_id)
// Invalider le cache pour afficher les nouvelles cartes queryClient.invalidateQueries({ queryKey: collectionsQueryKeys.list(), })
// Notification push locale Notifications.scheduleNotificationAsync({ content: { title: 'Cartes générées !', body: `${e.total_cards_persisted} cartes créées avec succès`, }, trigger: null, }) })
channel.listen('.card.generation.failed', (e) => { store.setGenerationFailed(e.generation_id, e.reason) })
return () => { channel.stopListening('.card.generation.started') channel.stopListening('.card.generation.completed') channel.stopListening('.card.generation.failed') } }, [echo, userId])
return null}Le contrôleur reçoit la requête, valide les données et dispatch un job asynchrone :
class GenerateCardsController extends Controller{ public function __invoke( GenerateCardsRequest $request, CardGenerationService $service ): JsonResponse { // La Form Request valide et convertit en DTO $input = $request->toDto();
// Le service vérifie le quota et dispatch le job $result = $service->dispatchGeneration( user: $request->user(), input: $input );
// Réponse immédiate (le traitement continue en arrière-plan) return response()->json([ 'generation_id' => $result->generationId, 'status' => $result->status->value, 'total_cards_requested' => $result->totalCardsRequested, ], 202); // HTTP 202 Accepted }}class CardGenerationService{ public function __construct( private readonly QuotaService $quotaService, private readonly GenerationHistoryService $history, ) {}
public function dispatchGeneration( User $user, CardGenerationInput $input ): CardGenerationDispatchResult { // 1. Vérifier que l'utilisateur a du quota $this->quotaService->guardCanGenerate( $user, $input->getTotalCardsRequested() );
// 2. Enregistrer la demande dans l'historique $generation = $this->history->recordRequest($user, $input);
// 3. Dispatcher le job vers la queue Redis GenerateCardsJob::dispatch( $user->id, $generation->generation_id, $input )->onQueue('ai'); // Queue dédiée aux jobs IA
return new CardGenerationDispatchResult( generationId: $generation->generation_id, status: CardGenerationStatus::QUEUED, totalCardsRequested: $input->getTotalCardsRequested(), ); }}Le job s’exécute en arrière-plan et orchestre tout le processus :
class GenerateCardsJob implements ShouldQueue{ public int $tries = 5; public int $timeout = 600; // 10 minutes max public string $queue = 'ai';
public function handle( CardGenerationService $service, CardPersistenceService $persistence, QuotaService $quota, ): void { $user = User::findOrFail($this->userId);
// 1. Notifier le début du traitement event(new CardGenerationStarted( $this->userId, $this->generationId, $this->input->getTotalCardsRequested() ));
// 2. Attendre le batch d'embeddings si nécessaire if ($this->input->batchId) { $this->waitForBatchReady($this->input->batchId); }
// 3. Appeler le service IA LangGraph $result = $service->generate($user, $this->input);
event(new CardsGenerated( $this->userId, $this->generationId, $result->stats->cardsGenerated ));
// 4. Persister les cartes en base $persistenceResult = $persistence->persist($user, $result, $this->input);
if ($persistenceResult->collectionCreated) { event(new CollectionCreated( $this->userId, $this->generationId, $persistenceResult->collectionId, $persistenceResult->collectionTitle )); }
event(new CardsPersisted( $this->userId, $this->generationId, $persistenceResult->collectionId, $persistenceResult->cardsPersisted ));
// 5. Consommer le quota APRÈS la persistance réussie $quota->consumeQuota($user, $result->stats->cardsGenerated);
// 6. Notifier la complétion event(new CardGenerationCompleted( $this->userId, $this->generationId, $persistenceResult->collectionId, $result->stats->cardsGenerated, $persistenceResult->cardsPersisted )); }
public function failed(\Throwable $exception): void { event(new CardGenerationFailed( $this->userId, $this->generationId, $exception->getMessage(), $this->input->batchId )); }}class LangGraphCardService{ public function generate(array $input): array { // Construction du payload pour LangGraph $payload = [ 'assistant_id' => config('langgraph.assistants.card_generator'), 'input' => [ 'prompt' => $input['prompt'] ?? null, 'language' => $input['language'], 'user_context' => $input['user_context'] ?? null, 'card_types' => $input['card_types'], 'difficulty' => $input['difficulty'] ?? 'intermediate', 'embedded_chunks' => $input['embedded_chunks'] ?? [], ], ];
// Appel synchrone avec attente (jusqu'à 5 minutes) $response = $this->client->runWait($payload);
return [ 'generated_cards' => $response['generated_cards'] ?? [], 'collection_metadata' => $response['collection_metadata'] ?? null, 'quality_scores' => $response['quality_scores'] ?? [], ]; }}Une fois le backend connecté au service IA, le Card Generator Graph prend le relais. Voir les sections suivantes pour le détail de l’architecture agentic.
flowchart LR
subgraph "Backend Laravel"
JOB["GenerateCardsJob"]
end
subgraph "Service IA"
VAL["Validate Inputs"]
SEL["Select Chunks"]
PLAN["Planner"]
GEN["Generator"]
CRIT["Critic"]
REF["Refiner"]
FIN["Finalizer"]
end
JOB -->|HTTP POST| VAL
VAL --> SEL --> PLAN --> GEN --> CRIT
CRIT -->|Score < 7.5| REF --> GEN
CRIT -->|Score >= 7.5| FIN
FIN -->|Response JSON| JOB
style JOB fill:#1565c0,stroke:#1976d2
style PLAN fill:#e65100,stroke:#f57c00
style CRIT fill:#ad1457,stroke:#c2185b flowchart TB
subgraph "Frontend Mobile"
UI["📱 Interface utilisateur"]
STORE["Zustand Store"]
RQ["React Query Cache"]
WS_CLIENT["WebSocket Client"]
end
subgraph "Backend Laravel"
CTRL["Controller"]
SVC["CardGenerationService"]
JOB["GenerateCardsJob"]
PERSIST["CardPersistenceService"]
QUOTA["QuotaService"]
EVENTS["Event Broadcaster"]
end
subgraph "Infrastructure"
REDIS["Redis Queue"]
PG[("PostgreSQL")]
REVERB["Laravel Reverb"]
end
subgraph "Service IA"
LANGGRAPH["LangGraph Cloud"]
AGENTS["Planner → Generator → Critic → Refiner"]
MISTRAL["Mistral AI"]
end
UI -->|1. POST /generate| CTRL
CTRL --> SVC
SVC -->|2. Dispatch| REDIS
REDIS -->|3. Process| JOB
JOB -->|4. Call| LANGGRAPH
LANGGRAPH --> AGENTS
AGENTS --> MISTRAL
MISTRAL -->|5. Response| AGENTS
AGENTS -->|6. Cards JSON| JOB
JOB -->|7. Save| PERSIST --> PG
JOB --> QUOTA
JOB -->|8. Broadcast| EVENTS --> REVERB
REVERB -->|9. WebSocket| WS_CLIENT
WS_CLIENT --> STORE
STORE --> UI
UI -->|10. Refresh| RQ
style UI fill:#2e7d32,stroke:#388e3c
style LANGGRAPH fill:#e65100,stroke:#f57c00
style PG fill:#1565c0,stroke:#1976d2
style REVERB fill:#7b1fa2,stroke:#9c27b0 Le backend broadcast 6 événements distincts pendant le processus de génération :
| Événement | Moment | Payload |
|---|---|---|
card.generation.started | Job démarre | generation_id, total_cards_requested |
card.generation.generated | IA termine | generation_id, cards_generated |
card.generation.collection_created | Nouvelle collection | collection_id, title |
card.generation.cards_persisted | Cartes en DB | collection_id, cards_persisted |
card.generation.completed | Succès final | collection_id, total_* |
card.generation.failed | Erreur | reason, batch_id |
Le Card Generator Graph est le composant le plus complexe et innovant du système. Contrairement à un simple appel LLM qui génère des cartes en une seule passe, nous utilisons une architecture agentic — c’est-à-dire un système où plusieurs agents IA collaborent et s’auto-corrigent.
Le système utilise 5 agents spécialisés qui collaborent en boucle jusqu’à atteindre la qualité souhaitée :
| Agent | Rôle | Analogie |
|---|---|---|
| Planner 📋 | Analyse le contenu et crée une stratégie de génération | Le chef de projet qui fait le plan |
| Generator ✏️ | Crée les cartes selon le plan établi | Le rédacteur qui écrit |
| Critic 🔍 | Évalue chaque carte et détecte les problèmes | Le relecteur qui pointe les erreurs |
| Refiner 🔧 | Améliore les cartes selon le feedback du Critic | Le correcteur qui améliore |
| Finalizer ✅ | Valide et génère les métadonnées finales | Le responsable qualité qui approuve |
flowchart LR
A["📋 Planner"] --> B["✏️ Generator"]
B --> C["🔍 Critic"]
C -->|"Score < 7.5"| D["🔧 Refiner"]
D -->|"Éessayer"| B
C -->|"Score ≥ 7.5"| E["✅ Finalizer"]
D -->|"Max tentatives"| E
style A fill:#1565c0,stroke:#1976d2
style B fill:#e65100,stroke:#f57c00
style C fill:#ad1457,stroke:#c2185b
style D fill:#2e7d32,stroke:#388e3c
style E fill:#00838f,stroke:#0097a7 Ce diagramme montre les boucles de feedback qui sont la clé de la qualité. Notez comment le critic peut renvoyer vers le generator (pour régénérer) ou vers le refiner (pour améliorer) :
graph TD;
__start__([__start__]):::first
validate_inputs(validate_inputs)
select_chunks(select_chunks)
planner(planner)
generator(generator)
critic(critic)
refiner(refiner)
finalizer(finalizer)
__end__([__end__]):::last
__start__ --> validate_inputs;
validate_inputs -. end .-> __end__;
validate_inputs -.-> select_chunks;
select_chunks --> planner;
planner --> generator;
generator --> critic;
critic -.-> finalizer;
critic -.-> generator;
critic -.-> refiner;
refiner -.-> finalizer;
refiner -.-> generator;
finalizer --> __end__;
classDef default fill:#5e35b1,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#7c4dff Le diagramme ci-dessous montre l’architecture interne avec les différents outils (tools) que chaque agent peut utiliser :
flowchart TB
subgraph AGENT["🤖 CARD GENERATION AGENT<br/>(mistral-medium-latest)"]
direction TB
PLANNER["🎯 PLANNER<br/>Analyse le contenu<br/>Planifie la stratégie"]
subgraph LOOP["🔄 Boucle d'amélioration"]
direction LR
GENERATOR["⚡ GENERATOR<br/>Crée les cartes"]
CRITIC["🔍 CRITIC<br/>Évalue qualité<br/>& diversité"]
REFINER["✨ REFINER<br/>Améliore<br/>les cartes"]
end
COLLECTION["📚 CARD COLLECTION"]
FINALIZER["📋 FINALIZER<br/>Génère metadata<br/>Vérifie complétude"]
end
PLANNER --> GENERATOR
GENERATOR --> CRITIC
CRITIC -->|"Besoin d'amélioration"| REFINER
REFINER -->|"Re-génération"| GENERATOR
CRITIC -->|"Cartes validées"| COLLECTION
REFINER -->|"Cartes améliorées"| COLLECTION
COLLECTION --> FINALIZER
style AGENT fill:#424242,stroke:#616161,stroke-width:2px
style PLANNER fill:#1565c0,stroke:#1976d2
style GENERATOR fill:#e65100,stroke:#f57c00
style CRITIC fill:#ad1457,stroke:#c2185b
style REFINER fill:#2e7d32,stroke:#388e3c
style COLLECTION fill:#7b1fa2,stroke:#9c27b0
style FINALIZER fill:#00838f,stroke:#0097a7
style LOOP fill:#37474f,stroke:#78909c,stroke-dasharray: 5 5 Le système peut générer 9 types de cartes différents, chacun adapté à un style d’apprentissage particulier. Le Planner décide de la répartition optimale en fonction du contenu.
class CardType(str, Enum): """Types de cartes disponibles pour la génération.
Chaque type a un format de sortie spécifique et une difficulté de génération différente (reflétée dans le nombre de tokens). """ FLASHCARD = "flashcard" # Question/Réponse classique MCQ = "mcq" # QCM avec distracteurs intelligents FREE_TEXT = "free_text" # Réponse libre évaluée par mots-clés TRUE_OR_FALSE = "true_or_false" # Affirmation à valider MATCH_PAIR = "match_pair" # Correspondance terme ↔ définition SLIDER = "slider" # Estimation numérique DRAG_AND_DROP = "drag_and_drop" # Placement dans des catégories RANKING = "ranking" # Classement par ordre GEO_GUESS = "geo_guess" # Localisation sur une carteFlashcard
Question/réponse classique pour la mémorisation rapide. Le format le plus simple et le plus efficace pour le rappel actif.
QCM
Question à choix multiples avec distracteurs intelligents. Le système génère des options plausibles mais incorrectes pour tester la compréhension.
Vrai/Faux
Affirmation à valider avec explication détaillée. Idéal pour corriger les idées reçues.
Match Pair
Relier des éléments correspondants entre eux.
Texte libre
Réponse ouverte pour approfondir la compréhension.
Classement
Ordonner des éléments selon un critère donné.
Validation des inputs
Sélection des chunks
Planification (Planner)
Génération (Generator)
Évaluation (Critic)
Raffinement (Refiner)
Finalisation
Chaque agent a un rôle précis et utilise un prompt YAML dédié. Voici comment ils fonctionnent en détail :
Le Planner est le stratège du système. Il analyse les chunks et décide :
class GenerationPlan(BaseModel): """Plan de génération créé par le Planner.
Ce plan guide le Generator pour produire des cartes équilibrées et adaptées au contenu source. """ card_distribution: Dict[CardType, int] # Ex: {FLASHCARD: 5, MCQ: 3} focus_topics: List[str] # Sujets identifiés à couvrir difficulty_curve: List[int] # [1, 2, 2, 3, 3, 4] = progression reasoning: str # Explication (pour le debug)Le Generator crée les cartes selon le plan établi. Il utilise les chunks comme source de vérité et respecte le format de chaque type :
{ "type": "flashcard", "question": "Qu'est-ce que la photosynthèse ?", "answer": "La photosynthèse est le processus par lequel...", "difficulty": 2, "tags": ["biologie", "plantes"]}{ "type": "mcq", "question": "Quel organite réalise la photosynthèse ?", "correct_answer": "Chloroplaste", "options": [ "Chloroplaste", "Mitochondrie", "Ribosome", "Noyau" ], "explanation": "Les chloroplastes contiennent...", "difficulty": 3}{ "type": "true_or_false", "statement": "La photosynthèse produit de l'oxygène.", "is_true": True, "explanation": "La photosynthèse libère de l'O2...", "difficulty": 1}Le Critic est le contrôle qualité du système. Pour chaque carte générée, il évalue :
class CardQualityScore(BaseModel): """Score de qualité calculé par le Critic.
Chaque dimension est évaluée de 0 à 1, puis combinée en un score global sur 10. """ clarity: float # La question est-elle claire et compréhensible ? relevance: float # La carte teste-t-elle bien le contenu source ? difficulty_fit: float # Le niveau correspond-il à celui demandé ? uniqueness: float # La carte est-elle différente des autres ? overall: float # Score final = moyenne pondérée (0-10) suggestions: List[str] # Conseils précis pour améliorer la carte| Critère | Seuil | Explication |
|---|---|---|
| Score minimum | 7.5/10 | En dessous, la carte part au Refiner |
| Similarité max | 75% | Au-delà, considéré comme doublon |
| Tentatives max | 2 | Après 2 échecs, la carte est rejetée |
Le Refiner améliore les cartes qui n’ont pas atteint le score minimum. Il reçoit la carte originale + les suggestions du Critic :
Le Refiner améliore les cartes selon le feedback du Critic :
graph LR
A[Carte originale] --> B{Score < 7.5?}
B -->|Non| C[Approuvée]
B -->|Oui| D[Refiner]
D --> E{Tentative < 2?}
E -->|Oui| F[Carte améliorée]
F --> G[Re-évaluation]
E -->|Non| H[Rejetée]
style C fill:#388e3c
style H fill:#c62828 class AgentActionType(str, Enum): """Types d'actions disponibles pour l'agent""" GENERATE = "generate" # Générer de nouvelles cartes EVALUATE = "evaluate" # Évaluer les cartes existantes REFINE = "refine" # Améliorer une carte REMOVE_DUPLICATE = "remove" # Supprimer un doublon FINALIZE = "finalize" # Finaliser la collectionclass AgentDecision(str, Enum): """Décisions possibles après évaluation""" APPROVE = "approve" # Carte approuvée REFINE = "refine" # Nécessite amélioration REGENERATE = "regenerate" # Regénérer complètement REJECT = "reject" # Rejeter la carte FINISH = "finish" # Terminer la générationclass CritiqueResult(BaseModel): """Résultat de l'évaluation par le Critic""" cards_approved: List[str] # IDs des cartes approuvées cards_to_refine: List[str] # IDs à améliorer cards_to_reject: List[str] # IDs à rejeter duplicate_pairs: List[Tuple] # Paires de doublons diversity_score: float # Score de diversité needs_more_generation: bool # Si plus de cartes nécessairesLe module batch_optimizer.py calcule dynamiquement la taille des batches pour optimiser les appels LLM :
@dataclass(frozen=True)class BatchConfig: """Configuration de batch optimisée""" batch_size: int # Nombre de cartes par batch estimated_input_tokens: int # Tokens d'entrée estimés estimated_output_tokens: int # Tokens de sortie estimés total_batches: int # Nombre total de batches| Type de carte | Tokens output estimés |
|---|---|
| Flashcard | 150 |
| QCM | 250 |
| Vrai/Faux | 120 |
| Match Pair | 200 |
| Texte libre | 180 |
| Classement | 220 |
| Drag & Drop | 280 |
| Slider | 100 |
| Geo Guess | 150 |
from src.graphs.card_generator import card_generator_graphfrom src.models.card_types import CardType, CardTypeRequest
# Configuration des types de cartes souhaitéscard_requests = [ CardTypeRequest(card_type=CardType.FLASHCARD, count=5), CardTypeRequest(card_type=CardType.MCQ, count=3), CardTypeRequest(card_type=CardType.TRUE_OR_FALSE, count=2)]
# Invocation du grapheresult = await card_generator_graph.ainvoke({ "embedded_chunks": embedded_chunks, # Depuis le graphe d'embedding "card_type_requests": card_requests, "language": "fr", "user_context": "Sujet: Biologie - La photosynthèse"})
# Récupération des résultatsgenerated_cards = result["generated_cards"]metadata = result["collection_metadata"]
print(f"Cartes générées: {len(generated_cards)}")print(f"Score moyen: {metadata['average_quality_score']}")Les prompts des agents sont stockés dans src/prompts/ au format YAML avec support multi-langue :
system: fr: | Tu es un expert en création de contenu pédagogique. Tu génères des cartes d'apprentissage de haute qualité.
en: | You are an expert in educational content creation. You generate high-quality learning cards.
user: fr: | Génère {count} cartes de type {card_type} à partir du contenu suivant: {content}
Niveau de difficulté: {difficulty}| Fichier | Agent | Rôle |
|---|---|---|
planner.yaml | Planner | Planification de la génération |
generator.yaml | Generator | Création des cartes |
critic.yaml | Critic | Évaluation de qualité |
refiner.yaml | Refiner | Amélioration des cartes |
| Métrique | Description | Cible |
|---|---|---|
| Score moyen | Qualité moyenne des cartes | > 8.0/10 |
| Taux d’approbation | Cartes approuvées du premier coup | > 70% |
| Doublons détectés | Cartes similaires identifiées | < 5% |
| Couverture sujet | % du contenu couvert | > 85% |
| Diversité types | Équilibre entre les types | Selon config |
Génération intelligente de cartes d’apprentissage grâce à une architecture agentic.