Un lycéen envoie une question à plusieurs éclaireurs en une fois, avec reformulation IA optionnelle et matching automatique via Elasticsearch. Schéma des tables multicontacts / ai-agents : voir entities/multicontact.md.

Flux

Deux points d'entrée, qui convergent vers le même traitement :

  • Avec reformulation IA (par défaut) : POST /multicontact/rephrase appelle l'agent IA pour reformuler la question et crée la ligne tout de suite avec reformulationStatus = abandoned. Puis POST /multicontact/send/generated met à jour le statut en accepted ou modified (calculé côté front en comparant le texte édité au texte reformulé) et lance le matching.
  • Sans reformulation : POST /multicontact/send/original crée directement la ligne en original, choisit un agent actif au hasard (uniquement pour générer la requête Elasticsearch, pas pour reformuler le texte), et lance le matching.

Matching (commun aux deux flux) :

  1. L'agent IA génère un JSON de paramètres de recherche à partir de la question + du profil du lycéen.
  2. type est forcé à eclaireur et limit/page forcés à 3/1, quoi que l'IA ait proposé.
  3. La requête Elasticsearch et ses résultats (elasticQuery/elasticResults) sont sauvegardés avant de vérifier s'il y a des résultats — une recherche à zéro résultat laisse donc une trace exploitable.
  4. Zéro résultat → erreur visible côté lycéen ("essaie une demande moins spécifique"). Sinon, un canal de chat est créé avec chaque éclaireur trouvé (ou un message est posté dans un canal existant s'il y en a déjà un), et channelId1/2/3 sont renseignés.

Le "3" est en dur, pas configurable

  • La limite de recherche (3) est forcée dans le code à plusieurs endroits, indépendamment de ce que l'IA renvoie.
  • Le schéma lui-même n'a que 3 colonnes channelId1/2/3 (pas de table de liaison) — la limite est structurelle, pas juste applicative.
  • Moins de 3 éclaireurs trouvés : les colonnes restantes sont null, la requête réussit quand même. Seul le cas 0 résultat échoue.

reformulationStatus

  • abandoned est la valeur par défaut posée à la création de la ligne dans rephrase() — ce n'est pas un événement détecté après coup. Aucun cron ni timeout ne bascule une ligne vers abandoned : si le lycéen ferme la modale sans envoyer, la ligne reste abandoned indéfiniment par défaut.
  • Le bouton "Envoyer ma question originale" reste toujours disponible, même après avoir vu la reformulation IA — la reformulation ne conditionne jamais l'envoi.

Pièges IA

  • Si le JSON de requête Elasticsearch renvoyé par le LLM n'est pas parsable, le code dégrade silencieusement vers une recherche texte simple (q = userQuestion) plutôt que d'échouer.
  • OPENAI_API_KEY et MISTRAL_API_KEY sont tous les deux obligatoires au démarrage de l'application (les deux clients sont instanciés sans condition), même si un seul provider est réellement utilisé par les agents actifs.
  • Aucun retry, backoff ni suivi de quota : chaque envoi déclenche 2 appels LLM synchrones (reformulation + génération de requête ES), sans cache.

Décalage d'indexation Elasticsearch

Réindexation incrémentale toutes les minutes, complète chaque nuit (voir cron-tasks.md). Un éclaireur qui vient de compléter son profil peut ne pas être matchable pendant ~1 minute ; à l'inverse, un profil ayant perdu son parcours éclaireur peut encore ressortir jusqu'à la prochaine passe — dans ce cas la création du canal échoue pour cet éclaireur précis, l'erreur est loguée et avalée (pas remontée), et le nombre de canaux créés est simplement plus petit que prévu, sans avertissement.

Admin

  • Le bouton "Tester la reformulation" (fiche agent) appelle le vrai endpoint de production — il crée une véritable ligne multicontacts (statut abandoned), même utilisé depuis l'admin.
  • Le bouton "Tester la recherche d'Éclaireurs" est lui sans effet de bord (pas de ligne créée).
  • Seuls les agents status = active peuvent être sélectionnés au hasard pour un vrai lycéen ; draft/archive ne sont atteignables qu'en passant un id explicite (l'admin).

Pièges front / documentation

  • Le toast de succès annonce toujours "3 étudiants Éclaireurs", quel que soit le nombre réel de canaux créés — le composant qui affiche le vrai compte (MulticontactSuccess.vue) existe mais n'est jamais utilisé dans l'app.
  • La collection Bruno (bruno/Multicontact/, bruno/Admin - AI Agents/) est obsolète par rapport aux DTOs actuels (champs eclaireurId, eclaireurIds, systemPrompt, lyceenId/pisteSlug qui n'existent plus) — ne pas s'y fier comme référence d'API à jour.