33800 Docs

← Retour

Proposition : Clarification interactive dans le chat AI

Status : IMPLEMENTEE

Date : 31/01/2026

Contexte

Actuellement, quand l'intent detection donne une confiance "medium" (plusieurs connecteurs possibles), le LLM classifier choisit automatiquement. Il se trompe parfois. Et quand le deep search ne trouve rien, l'utilisateur recoit juste "aucun resultat" sans possibilite de preciser sa demande.

Fonctionnalite

Ajouter un mecanisme de clarification interactive : le systeme pose une question a l'utilisateur dans le chat, attend sa reponse, puis reprend l'execution avec l'info supplementaire.

2 cas d'usage

Cas 1 : Medium confidence - choix du connecteur

Avant :

User: "cherche batman"
[LLM classifier choisit automatiquement Jellyfin]
AI: "Resultats Jellyfin..."

Apres :

User: "cherche batman"
AI: "Ou chercher ?
 1. Jellyfin - films, series, musique
 2. Nextcloud - fichiers, documents
 3. GitHub - repos, issues
Reponds avec le numero ou le nom du service."
User: "1"
AI: "Resultats Jellyfin... Batman v Superman, The Batman..."

Cas 2 : Deep search echoue - 0 resultats

Avant :

User: "cherche xenomorph awakening sur jellyfin"
[deep search: 0 resultats]
AI: "Aucun resultat trouve."

Apres :

User: "cherche xenomorph awakening sur jellyfin"
[deep search: 0 resultats]
AI: "Aucun resultat pour 'xenomorph awakening' sur Jellyfin.
Suggestions :
 1. Chercher 'xenomorph' seul
 2. Chercher 'awakening' seul
 3. Chercher sur un autre service
Ou tape un autre terme de recherche."
User: "alien"
AI: "Resultats Jellyfin... Alien, Aliens, Alien 3..."

Implementation technique

1. Migration DB : ajouter pending_intent sur ai_conversations

ALTER TABLE connectors.ai_conversations
ADD COLUMN pending_intent JSONB DEFAULT NULL;

Stocke les donnees necessaires pour reprendre l'execution :

interface PendingIntent {
  type: 'clarify_connector' | 'clarify_search';
  candidates?: IntentCandidate[];  // pour clarify_connector
  originalIntent?: DetectedIntent; // intent d'origine
  searchTerms?: string[];          // termes de recherche originaux
  connector?: string;              // connecteur cible (pour clarify_search)
  instanceName?: string;
  created_at: string;              // pour expiration (ex: 5 min)
}

2. Modifier _processMessage() dans aiChat.ts

Au debut : verifier si la conversation a un pending_intent :

// Check for pending clarification BEFORE intent detection
if (conv.pending_intent) {
  const pending = conv.pending_intent as PendingIntent;
  const age = Date.now() - new Date(pending.created_at).getTime();

  if (age < 300000) { // < 5 min
    const resolved = resolveClarification(content, pending);
    if (resolved) {
      // Clear pending
      await supabase.update('ai_conversations', { pending_intent: null }, { id: conversationId });
      // Execute with resolved intent
      // ... (meme code que high confidence)
    }
  }
  // If expired or not resolved, clear and continue normally
  await supabase.update('ai_conversations', { pending_intent: null }, { id: conversationId });
}

Dans le bloc medium confidence : au lieu du LLM classifier, sauver les candidats et retourner un message de clarification :

if (intent.confidence === 'medium' && intent.candidates && intent.candidates.length > 0) {
  emit({ step: 'clarifying', detail: 'Demande de precision...' });

  // Save pending intent
  const pending: PendingIntent = {
    type: 'clarify_connector',
    candidates: intent.candidates,
    originalIntent: intent,
    created_at: new Date().toISOString(),
  };
  await supabase.update('ai_conversations', { pending_intent: pending }, { id: conversationId });

  // Build clarification message
  let clarifyMsg = 'Ou chercher ?\n';
  intent.candidates.forEach((c, i) => {
    clarifyMsg += ` ${i + 1}. **${c.label}**\n`;
  });
  clarifyMsg += '\nReponds avec le numero ou le nom du service.';

  const assistantMsgResult = await supabase.insert<Message>('ai_messages', {
    conversation_id: conversationId, role: 'assistant',
    content: clarifyMsg, model: null, duration_ms: Date.now() - startTime
  });

  return { userMessage, assistantMessage: assistantMsgResult.data! };
}

Apres le deep search echoue : proposer des alternatives :

if (itemCount === 0 && retriesExhausted) {
  const pending: PendingIntent = {
    type: 'clarify_search',
    originalIntent: intent,
    searchTerms: intent.searchTerms,
    connector: intent.connector,
    instanceName: intent.instanceName,
    created_at: new Date().toISOString(),
  };
  await supabase.update('ai_conversations', { pending_intent: pending }, { id: conversationId });

  // Build suggestions
  let msg = `Aucun resultat pour "${intent.searchTerms?.join(' ')}" sur ${intent.connector}.\n`;
  msg += 'Tu peux :\n';
  const words = intent.searchTerms || [];
  words.forEach((w, i) => { msg += ` ${i + 1}. Chercher "${w}" seul\n`; });
  msg += ` ${words.length + 1}. Chercher sur un autre service\n`;
  msg += '\nOu tape directement un autre terme de recherche.';

  // Save and return
}

3. Nouvelle fonction resolveClarification()

function resolveClarification(
  userReply: string,
  pending: PendingIntent
): DetectedIntent | null {
  const reply = userReply.trim().toLowerCase();

  if (pending.type === 'clarify_connector') {
    // Check if reply is a number
    const num = parseInt(reply, 10);
    if (num >= 1 && num <= (pending.candidates?.length || 0)) {
      const chosen = pending.candidates![num - 1];
      return {
        ...pending.originalIntent!,
        confidence: 'high',
        connector: chosen.connector,
        endpoint: chosen.endpoint,
        instanceName: chosen.instanceName,
        queryParams: chosen.queryParams,
      };
    }
    // Check if reply matches a connector name
    for (const c of pending.candidates || []) {
      if (reply.includes(c.connector)) {
        return { ...pending.originalIntent!, confidence: 'high', ...c };
      }
    }
    return null; // Not a clarification reply, treat as new message
  }

  if (pending.type === 'clarify_search') {
    const num = parseInt(reply, 10);
    const words = pending.searchTerms || [];

    if (num >= 1 && num <= words.length) {
      // Retry with single word
      return {
        ...pending.originalIntent!,
        confidence: 'high',
        searchTerms: [words[num - 1]],
        queryParams: { ...pending.originalIntent!.queryParams, searchTerm: words[num - 1] },
      };
    }
    // Otherwise, treat reply as new search term
    if (reply.length >= 2 && reply.length <= 100) {
      return {
        ...pending.originalIntent!,
        confidence: 'high',
        searchTerms: [reply],
        queryParams: { ...pending.originalIntent!.queryParams, searchTerm: reply },
      };
    }
    return null;
  }

  return null;
}

4. SSE : nouveau step clarify

Ajouter un step SSE pour que le frontend sache que c'est une clarification (pas un message normal) :

emit({ step: 'clarify', detail: 'En attente de precision...' });

Le frontend peut afficher ce step differemment (pas de spinner, juste le message).

Fichiers modifies

Fichier Action
connectors-api/migrations/014_pending_intent.sql CREER - ALTER TABLE add pending_intent
connectors-api/src/services/aiChat.ts MODIFIER - resolveClarification, modifier medium conf + deep search
connectors-api/src/services/intentExecutor.ts MODIFIER - remonter l'info "0 resultats apres deep search"

Ce qui ne change PAS

Comportement de fallback

Ordre d'implementation

  1. Migration DB (pending_intent)
  2. resolveClarification() dans aiChat.ts
  3. Modifier medium confidence → clarification au lieu de LLM classifier
  4. Modifier deep search → clarification si 0 resultats
  5. Test