Audit & Fonctionnement — Architecture Pédagogique Interactive (WhatsApp & Voix)
Ce document est destiné à l'équipe technique et aux experts pédagogiques. Il détaille la transition de Xamlé d'un bot textuel "statique" vers un moteur pédagogique interactif centré sur la voix (audioUrl / TTS), les interactions rapides (buttonsJson), et le retour intelligent par l'IA (UserProgress / OpenAI).
1. Modèle de Données (Prisma)
Pour soutenir des leçons interactives qui ne se valident que sous l'action de l'étudiant, deux changements majeurs ont été effectués dans packages/database/prisma/schema.prisma.
A. Le Contenu de la Leçon (TrackDay)
L'ancien format texte brut a été enrichi pour supporter les audios et les interactions (boutons, vocaux).
model TrackDay {
id String @id @default(uuid())
trackId String
dayNumber Int
title String?
audioUrl String? // URL de l'audio pré-enregistré (sur R2) ou généré par TTS
lessonText String? // Texte de remplacement de l'audio
exerciseType ExerciseType @default(TEXT) // Type (TEXT, AUDIO, BUTTON)
exercisePrompt String? // Question de validation à envoyer en texte ou en bouton
validationKeyword String? // (Option) Si la validation nécessite un mot précis
buttonsJson Json? // Structure du bouton if ExerciseType == BUTTON
unlockCondition String? // Logique pour débloquer le jour suivant
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
track Track @relation(fields: [trackId], references: [id])
}
enum ExerciseType {
TEXT
AUDIO
BUTTON
}
B. Le Suivi de Mémorisation et Statut (UserProgress)
Au lieu de passer l'enrollement au "jour suivant" immédiatement, nous utilisons désormais un modèle précis pour l'exercice en cours.
model UserProgress {
id String @id @default(uuid())
userId String
trackId String
currentDay Int @default(1)
score Int @default(0) // Permet la gamification
lastInteraction DateTime @default(now())
exerciseStatus ExerciseStatus @default(PENDING) // PENDING -> l'étudiant doit répondre
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
track Track @relation(fields: [trackId], references: [id])
@@unique([userId, trackId])
}
enum ExerciseStatus {
PENDING
COMPLETED
}
2. Le Moteur d'Envoi côté Worker (pedagogy.ts)
La logique d'envoi du contenu (déclenchée par l'inscription ou la validation d'un exercice) a été isolée dans apps/whatsapp-worker/src/pedagogy.ts.
La fonction principale, sendLessonDay(...), structure l'expérience en trois temps :
- Envoyer la leçon audio ou texte.
- Envoyer la question interactive.
- Bloquer l'utilisateur au statut
PENDING.
// Extrait de pedagogy.ts dans le worker Railway
export async function sendLessonDay(userId: string, trackId: string, dayNumber: number) {
// 1. Envoi Audio (Via R2 ou TTS par défaut)
if (finalAudioUrl) {
await sendAudioMessage(user.phone, finalAudioUrl);
} else if (lessonText) {
await sendTextMessage(user.phone, lessonText);
}
// 2. Envoi de l'exercice interactif
if (trackDay.exercisePrompt) {
if (trackDay.exerciseType === 'BUTTON' && trackDay.buttonsJson) {
// Appelle l'API Meta avec le format Interactive Button
const buttons = trackDay.buttonsJson as Array<{ id: string; title: string }>;
await sendInteractiveButtonMessage(user.phone, trackDay.exercisePrompt, buttons);
} else {
await sendTextMessage(user.phone, trackDay.exercisePrompt);
}
}
// 3. Mise en attente de la réponse de l'utilisateur
await prisma.userProgress.upsert({
where: { userId_trackId: { userId, trackId } },
update: { currentDay: dayNumber, exerciseStatus: 'PENDING', lastInteraction: new Date() },
create: { userId, trackId, currentDay: dayNumber, exerciseStatus: 'PENDING' }
});
}
3. Le Routage Webhook (WhatsAppMessageSchema et Interception)
L'API (Hugging Face) écoute les réponses. Si une réponse est reçue avec le statut PENDING, elle l'envoie au coach IA.
De plus, le typage Zod de l'événement entrant a été corrigé pour intercepter les boutons WhatsApp :
// Extrait de apps/api/src/routes/whatsapp.ts
const WhatsAppMessageSchema = z.object({
from: z.string(),
type: z.enum(['text', 'audio', 'image', 'video', 'document', 'sticker', 'reaction', 'interactive']),
text: z.object({ body: z.string() }).optional(),
audio: z.object({ id: z.string(), mime_type: z.string().optional() }).optional(),
interactive: z.object({
type: z.enum(['button_reply', 'list_reply']),
button_reply: z.object({ id: z.string(), title: z.string() }).optional()
}).optional()
});
// Dans le parsing du payload, on convertit un clic bouton en "Text", ou un Audio via STT Whisper :
if (message.type === 'interactive' && message.interactive) {
if (message.interactive.type === 'button_reply' && message.interactive.button_reply) {
text = message.interactive.button_reply.id; // L'ID du bouton devient la réponse envoyée à l'IA
}
} else if (message.type === 'audio' && message.audio) {
// On télécharge l'audio de Meta et on utilise Whisper pour le transcrire
text = await aiService.transcribeAudio(buffer, 'message.ogg');
}
4. La Boucle de Correction IA (whatsapp.ts et ai.ts)
Lorsqu'une réponse (texte ou bouton) est interceptée par l'API alors qu'un exercice est "PENDING", elle sollicite generateFeedback.
// Logique d'interception (app/api/src/services/whatsapp.ts)
const pendingProgress = await prisma.userProgress.findFirst({
where: { userId: user.id, exerciseStatus: 'PENDING' },
include: { track: true }
});
if (pendingProgress) {
await scheduleMessage(user.id, "⏳ Analyse de votre réponse...");
// Génération du feedback IA
const feedback = await aiService.generateFeedback(
text, // Ce que l'utilisateur a répondu ou cliqué
trackDay.exercisePrompt || '', // L'exercice attendu
trackDay.lessonText || '' // Le contexte de la leçon
);
await scheduleMessage(user.id, feedback);
// ✅ On débloque l'utilisateur
await prisma.userProgress.update({
where: { id: pendingProgress.id },
data: { exerciseStatus: 'COMPLETED', score: { increment: 1 } }
});
}
Le Prompt Pédagogique (OpenAI GPT-4o-mini)
L'intelligence est formatée pour un coach bienveillant sénégalais du secteur informel (WhatsApp Native).
// apps/api/src/services/ai/index.ts
const prompt = `
Tu es un coach business bienveillant pour des entrepreneurs du secteur informel au Sénégal.
L'étudiant vient d'écouter cette leçon : "${lessonContent}"
L'exercice attendu était : "${expectedExercise}"
Voici la réponse ou action de l'étudiant : "${userInput}"
Ta mission :
Analyse sa réponse logiquement. Valide-la avec beaucoup d'enthousiasme, et ajoute UN conseil très pratique, précis, ou une mini correction bienveillante.
Contrainte stricte : MAXIMUM 5 LIGNES.
Format WhatsApp : Utilise un langage très simple, direct, des mots courants, une structure aérée avec du *gras* pour les mots clés importants, et des emojis adaptés.
`;
5. Fonctionnalités Utiles Ajoutées pour le Déboguage
- Le mot-clé
INSCRIPTION(WhatsApp) : Pour ne pas bloquer les utilisateurs en cours de test, le mot-clé supprime les données "ville" et "activité", et "enrollments" pour REMETTRE L'ÉTUDIANT À ZÉRO à la volée. - Le mot-clé
SUITE(WhatsApp) : Par défaut, la prochaine leçon (lorsque "COMPLETED" est atteint) n'arrive que le lendemain matin via le Scheduler de BullMQ. Si l'utilisateur envoieSUITE, nous forçons le passage immédiat au jour suivant sans attendre 24 heures.
6. Feedbacks Requis par l'Expert et Axes d'Amélioration (V2)
L'expert observant la codebase doit envisager ces améliorations :
- Fallback si Feedback "Négatif" : Actuellement, le système génère un feedback bienveillant et passe
UserProgressàCOMPLETEDmême si l'étudiant dit une erreur ou envoie un contenu inapproprié. Il faudrait un "score de validation IA" (true/false) dans le schéma Zod de l'IA. Sifalse, on laisse le statut àPENDING. - Audio Natifs R2 : Lors de la création d'un "TrackDay", le tableau de bord Admin (pas encore construit sur ce pan) doit pouvoir uploader de vrais MP3 professionnels vers R2, et enregistrer l'URL dans
TrackDay.audioUrl. Sans quoi le système "fallback" sur la génération automatique (TTS) assez robotique. - Limites des Boutons WhatsApp :
- Meta impose un Maximum de 3 boutons interactifs par message. S'il y a 4 questions, il faudra switcher vers l'événement
List_reply(Menu interactif WhatsApp). - Les titres des boutons sont limités à 20 caractères.
- Meta impose un Maximum de 3 boutons interactifs par message. S'il y a 4 questions, il faudra switcher vers l'événement
- Vidéos Verticales : Le schéma BDD ne gère pas encore de champ
videoUrl. Quand le format Vlog entrera en lice, il suffira de rajouter ce champ dansTrackDayet la fonctionsendVideoMessageau Wrapper API Cloud Meta.