edtech / docs /architecture_pedagogique_whatsapp.md
CognxSafeTrack
docs: add audit_production.md + plan_implementation_prod.md; fix admin phone field
f1a06cd

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 :

  1. Envoyer la leçon audio ou texte.
  2. Envoyer la question interactive.
  3. 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 envoie SUITE, 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 :

  1. Fallback si Feedback "Négatif" : Actuellement, le système génère un feedback bienveillant et passe UserProgress à COMPLETED mê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. Si false, on laisse le statut à PENDING.
  2. 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.
  3. 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.
  4. 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 dans TrackDay et la fonction sendVideoMessage au Wrapper API Cloud Meta.