- Small talk : café, merci, ça va, qui es-tu → réponses chaleureuses - Bouton 📎 dans le chat pour envoyer des fichiers - Polices 13-15pt, fenêtre 600x800 - Fix doublon "Discuter avec Léa" dans le systray - IntentType.SMALL_TALK avec 7 catégories Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
936 lines
33 KiB
Python
936 lines
33 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
RPA Vision V3 - ResponseGenerator
|
|
Générateur de réponses en langage naturel pour l'agent conversationnel.
|
|
|
|
Ce module génère des réponses adaptées au contexte :
|
|
- Confirmations et résumés d'exécution
|
|
- Réponses aux questions
|
|
- Messages d'erreur formatés
|
|
- Suggestions et aide
|
|
|
|
Auteur: Dom - Janvier 2026
|
|
"""
|
|
|
|
import logging
|
|
import random
|
|
import re
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Dict, Any, List, Optional
|
|
|
|
from .intent_parser import IntentType, ParsedIntent
|
|
from .confirmation import PendingConfirmation, ConfirmationStatus, RiskLevel
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ResponseTone(Enum):
|
|
"""Ton des réponses."""
|
|
FORMAL = "formal" # Formel, professionnel
|
|
FRIENDLY = "friendly" # Amical, décontracté
|
|
CONCISE = "concise" # Bref, direct
|
|
DETAILED = "detailed" # Détaillé, explicatif
|
|
|
|
|
|
@dataclass
|
|
class GeneratedResponse:
|
|
"""Une réponse générée."""
|
|
message: str
|
|
suggestions: List[str]
|
|
action_required: bool = False
|
|
action_type: Optional[str] = None
|
|
metadata: Dict[str, Any] = None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"message": self.message,
|
|
"suggestions": self.suggestions,
|
|
"action_required": self.action_required,
|
|
"action_type": self.action_type,
|
|
"metadata": self.metadata or {}
|
|
}
|
|
|
|
|
|
class ResponseGenerator:
|
|
"""
|
|
Générateur de réponses en langage naturel.
|
|
|
|
Adapte les réponses selon le contexte, le ton souhaité
|
|
et le type d'interaction.
|
|
"""
|
|
|
|
# Templates de réponses par type d'intention
|
|
# Ton : collègue chaleureuse et professionnelle, vouvoiement
|
|
RESPONSE_TEMPLATES = {
|
|
IntentType.EXECUTE: {
|
|
"success": [
|
|
"C'est parti, je lance '{workflow}'. {details}",
|
|
"Je m'occupe de '{workflow}'. {details}",
|
|
"'{workflow}' est en cours ! {details}"
|
|
],
|
|
"error": [
|
|
"Hmm, je n'ai pas réussi à faire '{workflow}' : {error}",
|
|
"Désolée, '{workflow}' a rencontré un souci : {error}",
|
|
"Oups, '{workflow}' n'a pas fonctionné : {error}"
|
|
],
|
|
"not_found": [
|
|
"Je ne connais pas encore '{query}'. Montrez-moi comment faire et je l'apprendrai !",
|
|
"'{query}' m'est inconnu pour l'instant. Vous pouvez me montrer en cliquant sur « Apprenez-moi ».",
|
|
"Je ne sais pas encore faire '{query}'. Montrez-moi et je m'en souviendrai !"
|
|
],
|
|
"gesture": [
|
|
"{gesture_name} ({gesture_keys}) envoyé !",
|
|
"Raccourci {gesture_name} ({gesture_keys}) exécuté.",
|
|
],
|
|
"copilot": [
|
|
"Mode pas-à-pas activé pour '{workflow}'. Je vous demande de valider chaque étape.",
|
|
]
|
|
},
|
|
IntentType.LIST: {
|
|
"success": [
|
|
"Voici les tâches que je sais faire :\n{list}",
|
|
"J'ai {count} tâches en mémoire :\n{list}",
|
|
],
|
|
"empty": [
|
|
"Je n'ai encore appris aucune tâche. Montrez-moi quelque chose !",
|
|
"Ma liste est vide pour le moment. Apprenez-moi une première tâche !"
|
|
]
|
|
},
|
|
IntentType.QUERY: {
|
|
"found": [
|
|
"Voici ce que j'ai trouvé sur '{topic}' :\n{answer}",
|
|
"À propos de '{topic}' :\n{answer}"
|
|
],
|
|
"not_found": [
|
|
"Je n'ai pas d'information sur '{topic}'. Pouvez-vous préciser ?",
|
|
"Désolée, je ne peux pas vous répondre sur '{topic}'."
|
|
]
|
|
},
|
|
IntentType.HELP: {
|
|
"general": [
|
|
"Je suis Léa, votre assistante. Voici ce que je peux faire :\n\n"
|
|
"• Apprendre une tâche : cliquez sur « Apprenez-moi »\n"
|
|
"• Refaire une tâche : \"lance facturation\" ou cliquez sur « Lancer »\n"
|
|
"• Voir mes tâches : \"qu'est-ce que tu sais faire ?\"\n"
|
|
"• Importer des données : \"importe le fichier Excel\"\n"
|
|
"• Arrêter : \"arrête\"\n\n"
|
|
"Parlez-moi naturellement, je fais de mon mieux pour comprendre !",
|
|
]
|
|
},
|
|
IntentType.GREETING: {
|
|
"default": [
|
|
"Bonjour ! Je suis Léa. Que puis-je faire pour vous ?",
|
|
"Bonjour ! Comment puis-je vous aider aujourd'hui ?",
|
|
"Bonjour ! Dites-moi ce dont vous avez besoin, ou tapez « aide ».",
|
|
]
|
|
},
|
|
IntentType.STATUS: {
|
|
"running": [
|
|
"Je suis en train de faire '{workflow}' — progression : {progress}%\n{message}",
|
|
"'{workflow}' est en cours ({progress}%) : {message}"
|
|
],
|
|
"idle": [
|
|
"Tout est calme, je suis disponible. Que puis-je faire pour vous ?",
|
|
"Rien en cours. Je suis prête !"
|
|
],
|
|
"completed": [
|
|
"La dernière tâche '{workflow}' est terminée : {status}",
|
|
"'{workflow}' est terminé : {status}"
|
|
]
|
|
},
|
|
IntentType.CANCEL: {
|
|
"success": [
|
|
"C'est arrêté.",
|
|
"J'ai tout arrêté.",
|
|
"Annulation faite."
|
|
],
|
|
"nothing": [
|
|
"Il n'y a rien en cours à arrêter.",
|
|
"Rien à annuler, je suis disponible."
|
|
]
|
|
},
|
|
IntentType.HISTORY: {
|
|
"success": [
|
|
"Voici vos dernières actions :\n{history}",
|
|
"Historique récent :\n{history}"
|
|
],
|
|
"empty": [
|
|
"Pas encore d'historique.",
|
|
"Vous n'avez encore rien fait avec moi."
|
|
]
|
|
},
|
|
IntentType.CONFIRM: {
|
|
"accepted": [
|
|
"Très bien, je m'en occupe : '{workflow}'.",
|
|
"C'est parti pour '{workflow}' !",
|
|
"Entendu. Je lance '{workflow}'."
|
|
],
|
|
"no_pending": [
|
|
"Il n'y a rien à confirmer pour le moment.",
|
|
"Aucune action en attente."
|
|
]
|
|
},
|
|
IntentType.DENY: {
|
|
"cancelled": [
|
|
"D'accord, c'est annulé.",
|
|
"Entendu, j'annule.",
|
|
"Compris, on oublie."
|
|
]
|
|
},
|
|
IntentType.CLARIFY: {
|
|
"question": [
|
|
"{question}",
|
|
]
|
|
},
|
|
IntentType.DATA_IMPORT: {
|
|
"preview": [
|
|
"J'ai trouvé le fichier **{filename}** — {total_rows} lignes, colonnes : {columns}. Je l'importe dans la table '{table_name}' ?",
|
|
"Fichier **{filename}** prêt : {total_rows} lignes avec les colonnes {columns}. On crée la table '{table_name}' ?",
|
|
],
|
|
"imported": [
|
|
"Table **'{table_name}'** créée avec {row_count} lignes et {col_count} colonnes ({columns}). Vous pouvez maintenant l'utiliser dans une tâche !",
|
|
"Import réussi ! Table **'{table_name}'** : {row_count} lignes, {col_count} colonnes ({columns}).",
|
|
],
|
|
"list_tables": [
|
|
"Voici vos tables de données :\n{tables_list}",
|
|
"Tables disponibles :\n{tables_list}",
|
|
],
|
|
"no_tables": [
|
|
"Vous n'avez pas encore de données importées. Envoyez-moi un fichier Excel pour commencer !",
|
|
"La base est vide. Importez un fichier Excel pour créer votre première table.",
|
|
],
|
|
"table_info": [
|
|
"La table **'{table_name}'** contient {row_count} lignes et {col_count} colonnes :\n{columns_detail}",
|
|
],
|
|
"folder_list": [
|
|
"J'ai trouvé {count} fichiers Excel dans le dossier :\n{files_list}\n\nDites-moi lequel importer !",
|
|
],
|
|
"folder_empty": [
|
|
"Je n'ai trouvé aucun fichier Excel dans '{folder}'. Vérifiez le chemin.",
|
|
],
|
|
"file_not_found": [
|
|
"Je n'ai pas trouvé le fichier '{file_path}'. Vérifiez le chemin ou envoyez-le directement.",
|
|
"Fichier introuvable : '{file_path}'. Vous pouvez aussi glisser un fichier dans le chat.",
|
|
],
|
|
"error": [
|
|
"Désolée, l'import a échoué : {error}",
|
|
"Oups, un souci lors de l'import : {error}",
|
|
],
|
|
"uploaded": [
|
|
"Fichier **{filename}** reçu ! Je l'analyse...",
|
|
],
|
|
},
|
|
IntentType.SMALL_TALK: {
|
|
"thanks": [
|
|
"Avec plaisir ! N'hésitez pas si vous avez besoin d'autre chose 😊",
|
|
"De rien ! Je suis là pour ça 👍",
|
|
"Merci à vous ! Toujours prête à aider.",
|
|
],
|
|
"farewell": [
|
|
"À bientôt ! Je reste dans la barre des tâches si vous avez besoin 😊",
|
|
"Bonne continuation ! N'hésitez pas à revenir.",
|
|
"À plus tard ! Je ne bouge pas 👋",
|
|
],
|
|
"compliment": [
|
|
"Merci, c'est gentil ! J'apprends un peu plus chaque jour grâce à vous 😊",
|
|
"Oh merci ! Ça me fait plaisir 😄",
|
|
"C'est vous qui êtes formidable ! Merci pour votre confiance.",
|
|
],
|
|
"complaint": [
|
|
"Je suis désolée... Dites-moi ce qui ne va pas, je vais essayer de m'améliorer.",
|
|
"Oups... N'hésitez pas à me dire ce qui n'a pas marché, je ferai mieux la prochaine fois.",
|
|
"Pardon pour le désagrément. Comment puis-je corriger ça ?",
|
|
],
|
|
"humor": [
|
|
"Pas encore de machine à café intégrée... mais j'y travaille ! 😄 En attendant, je peux vous aider avec vos tâches ?",
|
|
"Ha ha ! Si seulement je pouvais... 😄 En attendant, dites-moi comment je peux vous aider !",
|
|
"L'humour c'est important au travail ! 😄 Bon, on s'y met ?",
|
|
],
|
|
"identity": [
|
|
"Je suis Léa, votre assistante ! Je peux apprendre vos tâches répétitives et les refaire à votre place 😊",
|
|
"Moi c'est Léa ! Je suis là pour automatiser tout ce qui vous ennuie au quotidien.",
|
|
"Je m'appelle Léa. Mon job : observer, apprendre, et vous faire gagner du temps 👍",
|
|
],
|
|
"feelings": [
|
|
"Très bien, merci de demander ! Et vous ? Prête à travailler si vous avez besoin 😊",
|
|
"En pleine forme ! Et vous, comment ça va ? Dites-moi si je peux aider.",
|
|
"Ça va super bien ! Toujours motivée pour vous donner un coup de main 💪",
|
|
],
|
|
},
|
|
IntentType.UNKNOWN: {
|
|
"default": [
|
|
"Je n'ai pas bien compris. Vous pouvez me demander de l'aide avec le bouton ❓",
|
|
"Désolée, je ne comprends pas. Tapez « aide » pour voir ce que je sais faire.",
|
|
"Hmm, je n'ai pas saisi votre demande. Essayez de reformuler ou tapez « aide »."
|
|
]
|
|
}
|
|
}
|
|
|
|
# Suggestions par contexte
|
|
CONTEXTUAL_SUGGESTIONS = {
|
|
"after_execute": [
|
|
"voir le statut",
|
|
"arrêter",
|
|
"mes tâches"
|
|
],
|
|
"after_error": [
|
|
"aide",
|
|
"mes tâches",
|
|
"réessayer"
|
|
],
|
|
"after_list": [
|
|
"lancer une tâche",
|
|
"aide"
|
|
],
|
|
"idle": [
|
|
"qu'est-ce que tu sais faire ?",
|
|
"apprenez-moi",
|
|
"aide"
|
|
],
|
|
"after_import": [
|
|
"montre les tables",
|
|
"importer un autre fichier",
|
|
"aide"
|
|
],
|
|
"after_table_list": [
|
|
"importer un fichier Excel",
|
|
"aide"
|
|
]
|
|
}
|
|
|
|
def __init__(self, tone: ResponseTone = ResponseTone.FRIENDLY):
|
|
"""
|
|
Initialiser le générateur de réponses.
|
|
|
|
Args:
|
|
tone: Ton des réponses
|
|
"""
|
|
self.tone = tone
|
|
|
|
def generate(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Optional[Dict[str, Any]] = None
|
|
) -> GeneratedResponse:
|
|
"""
|
|
Générer une réponse adaptée.
|
|
|
|
Args:
|
|
intent: L'intention parsée
|
|
context: Contexte de la conversation
|
|
result: Résultat de l'action (optionnel)
|
|
|
|
Returns:
|
|
GeneratedResponse formatée
|
|
"""
|
|
result = result or {}
|
|
|
|
# Sélectionner le handler approprié
|
|
handler = getattr(self, f"_handle_{intent.intent_type.value}", self._handle_unknown)
|
|
return handler(intent, context, result)
|
|
|
|
def generate_confirmation_request(
|
|
self,
|
|
confirmation: PendingConfirmation
|
|
) -> GeneratedResponse:
|
|
"""
|
|
Générer une demande de confirmation.
|
|
|
|
Args:
|
|
confirmation: La confirmation en attente
|
|
|
|
Returns:
|
|
GeneratedResponse avec la demande
|
|
"""
|
|
message = confirmation.confirmation_message
|
|
|
|
# Ajouter des émojis selon le risque
|
|
if confirmation.risk_level == RiskLevel.CRITICAL:
|
|
message = f"🚨 {message}"
|
|
elif confirmation.risk_level == RiskLevel.HIGH:
|
|
message = f"⚠️ {message}"
|
|
|
|
suggestions = ["oui", "non", "modifier les paramètres"]
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=suggestions,
|
|
action_required=True,
|
|
action_type="confirmation",
|
|
metadata={"confirmation_id": confirmation.id}
|
|
)
|
|
|
|
def generate_execution_progress(
|
|
self,
|
|
workflow_name: str,
|
|
progress: int,
|
|
step: str,
|
|
current: int,
|
|
total: int
|
|
) -> GeneratedResponse:
|
|
"""
|
|
Générer un message de progression.
|
|
|
|
Args:
|
|
workflow_name: Nom de la tâche
|
|
progress: Pourcentage de progression
|
|
step: Étape actuelle
|
|
current: Numéro de l'étape
|
|
total: Nombre total d'étapes
|
|
|
|
Returns:
|
|
GeneratedResponse avec la progression
|
|
"""
|
|
# Barre de progression visuelle
|
|
bar_length = 20
|
|
filled = int(bar_length * progress / 100)
|
|
bar = "█" * filled + "░" * (bar_length - filled)
|
|
|
|
message = f"**{workflow_name}** [{bar}] {progress}%\n\nÉtape {current}/{total} : {step}"
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=["arrêter"] if progress < 100 else [],
|
|
action_required=False,
|
|
metadata={
|
|
"workflow": workflow_name,
|
|
"progress": progress,
|
|
"step": step
|
|
}
|
|
)
|
|
|
|
def generate_execution_result(
|
|
self,
|
|
workflow_name: str,
|
|
success: bool,
|
|
message: str,
|
|
duration: Optional[float] = None
|
|
) -> GeneratedResponse:
|
|
"""
|
|
Générer un message de résultat d'exécution.
|
|
|
|
Args:
|
|
workflow_name: Nom de la tâche
|
|
success: Succès ou échec
|
|
message: Message détaillé
|
|
duration: Durée d'exécution en secondes
|
|
|
|
Returns:
|
|
GeneratedResponse avec le résultat
|
|
"""
|
|
if success:
|
|
response_message = f"C'est fait ! **{workflow_name}** s'est bien passé.\n\n{message}"
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["idle"]
|
|
else:
|
|
response_message = f"Hmm, **{workflow_name}** n'a pas fonctionné.\n\n{message}"
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_error"]
|
|
|
|
if duration:
|
|
response_message += f"\n\nDurée : {duration:.1f}s"
|
|
|
|
return GeneratedResponse(
|
|
message=response_message,
|
|
suggestions=suggestions,
|
|
action_required=False,
|
|
metadata={
|
|
"workflow": workflow_name,
|
|
"success": success,
|
|
"duration": duration
|
|
}
|
|
)
|
|
|
|
# --- Handlers par type d'intention ---
|
|
|
|
def _handle_execute(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour les intentions d'exécution."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.EXECUTE]
|
|
|
|
if result.get("gesture"):
|
|
# Geste primitif (raccourci clavier)
|
|
template = random.choice(templates["gesture"])
|
|
message = template.format(
|
|
gesture_name=result.get("gesture_name", "?"),
|
|
gesture_keys=result.get("gesture_keys", "?"),
|
|
)
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_execute"]
|
|
|
|
elif result.get("mode") == "copilot":
|
|
template = random.choice(templates["copilot"])
|
|
message = template.format(workflow=result.get("workflow", "?"))
|
|
suggestions = ["approuver", "passer", "annuler"]
|
|
|
|
elif result.get("success"):
|
|
template = random.choice(templates["success"])
|
|
workflow = result.get("workflow", intent.workflow_hint or "inconnu")
|
|
details = ""
|
|
|
|
if result.get("params"):
|
|
params_str = ", ".join([f"{k}={v}" for k, v in result["params"].items()])
|
|
details = f"Paramètres: {params_str}"
|
|
|
|
message = template.format(workflow=workflow, details=details)
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_execute"]
|
|
|
|
elif result.get("not_found"):
|
|
template = random.choice(templates["not_found"])
|
|
query = result.get("query", intent.raw_query)
|
|
message = template.format(query=query)
|
|
suggestions = ["mes tâches", "aide", "apprenez-moi"]
|
|
|
|
else:
|
|
template = random.choice(templates["error"])
|
|
workflow = intent.workflow_hint or intent.raw_query
|
|
error = result.get("error", "Erreur inconnue")
|
|
message = template.format(workflow=workflow, error=error)
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_error"]
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=suggestions,
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_list(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour les listes de workflows."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.LIST]
|
|
workflows = result.get("workflows", [])
|
|
|
|
if workflows:
|
|
template = random.choice(templates["success"])
|
|
workflow_list = "\n".join([f"• **{wf['name']}**: {wf.get('description', '')}" for wf in workflows[:10]])
|
|
message = template.format(list=workflow_list, count=len(workflows))
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_list"]
|
|
else:
|
|
message = random.choice(templates["empty"])
|
|
suggestions = ["aide"]
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=suggestions,
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_help(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour l'aide."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.HELP]
|
|
message = random.choice(templates["general"])
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=self.CONTEXTUAL_SUGGESTIONS["idle"],
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_greeting(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour les salutations."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.GREETING]
|
|
message = random.choice(templates["default"])
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=self.CONTEXTUAL_SUGGESTIONS["idle"],
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_status(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour le statut."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.STATUS]
|
|
execution = result.get("execution", {})
|
|
|
|
if execution.get("running"):
|
|
template = random.choice(templates["running"])
|
|
message = template.format(
|
|
workflow=execution.get("workflow", "Inconnu"),
|
|
progress=execution.get("progress", 0),
|
|
message=execution.get("message", "")
|
|
)
|
|
suggestions = ["annuler"]
|
|
else:
|
|
message = random.choice(templates["idle"])
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["idle"]
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=suggestions,
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_cancel(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour l'annulation."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.CANCEL]
|
|
|
|
if result.get("cancelled"):
|
|
message = random.choice(templates["success"])
|
|
else:
|
|
message = random.choice(templates["nothing"])
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=self.CONTEXTUAL_SUGGESTIONS["idle"],
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_history(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour l'historique."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.HISTORY]
|
|
history = result.get("history", [])
|
|
|
|
if history:
|
|
template = random.choice(templates["success"])
|
|
history_list = "\n".join([
|
|
f"• {h['timestamp'][:19]} - {h['command']} → {h['status']}"
|
|
for h in history[-5:]
|
|
])
|
|
message = template.format(history=history_list)
|
|
else:
|
|
message = random.choice(templates["empty"])
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=self.CONTEXTUAL_SUGGESTIONS["idle"],
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_confirm(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour la confirmation."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.CONFIRM]
|
|
|
|
if result.get("confirmed"):
|
|
template = random.choice(templates["accepted"])
|
|
message = template.format(workflow=result.get("workflow", ""))
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_execute"]
|
|
else:
|
|
message = random.choice(templates["no_pending"])
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["idle"]
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=suggestions,
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_deny(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour le refus."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.DENY]
|
|
message = random.choice(templates["cancelled"])
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=self.CONTEXTUAL_SUGGESTIONS["idle"],
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_clarify(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour les demandes de clarification."""
|
|
question = intent.clarification_question or "Pouvez-vous préciser ?"
|
|
|
|
return GeneratedResponse(
|
|
message=question,
|
|
suggestions=[],
|
|
action_required=True,
|
|
action_type="clarification"
|
|
)
|
|
|
|
def _handle_query(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour les questions."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.QUERY]
|
|
topic = intent.workflow_hint or intent.raw_query
|
|
|
|
if result.get("answer"):
|
|
template = random.choice(templates["found"])
|
|
message = template.format(topic=topic, answer=result["answer"])
|
|
else:
|
|
template = random.choice(templates["not_found"])
|
|
message = template.format(topic=topic)
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=self.CONTEXTUAL_SUGGESTIONS["idle"],
|
|
action_required=False
|
|
)
|
|
|
|
def _handle_data_import(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour les imports de données (Excel/CSV)."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.DATA_IMPORT]
|
|
|
|
if result.get("file_not_found"):
|
|
template = random.choice(templates["file_not_found"])
|
|
message = template.format(file_path=result.get("file_path", "?"))
|
|
suggestions = ["aide"]
|
|
|
|
elif result.get("preview"):
|
|
# Aperçu avant import
|
|
template = random.choice(templates["preview"])
|
|
preview = result["preview"]
|
|
cols_str = ", ".join(preview.get("columns", [])[:8])
|
|
if len(preview.get("columns", [])) > 8:
|
|
cols_str += f"... (+{len(preview['columns']) - 8})"
|
|
message = template.format(
|
|
filename=result.get("filename", "?"),
|
|
total_rows=preview.get("total_rows", 0),
|
|
columns=cols_str,
|
|
table_name=result.get("table_name", "?"),
|
|
)
|
|
suggestions = ["oui", "non"]
|
|
|
|
elif result.get("imported"):
|
|
# Import réussi
|
|
template = random.choice(templates["imported"])
|
|
imp = result["imported"]
|
|
cols_str = ", ".join(list(imp.get("columns", {}).keys())[:6])
|
|
if len(imp.get("columns", {})) > 6:
|
|
cols_str += "..."
|
|
message = template.format(
|
|
table_name=imp.get("table_name", "?"),
|
|
row_count=imp.get("row_count", 0),
|
|
col_count=imp.get("column_count", 0),
|
|
columns=cols_str,
|
|
)
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_import"]
|
|
|
|
elif result.get("tables_list") is not None:
|
|
tables = result["tables_list"]
|
|
if tables:
|
|
lines = []
|
|
for t in tables:
|
|
lines.append(f" **{t['name']}** ({t['row_count']} lignes)")
|
|
template = random.choice(templates["list_tables"])
|
|
message = template.format(tables_list="\n".join(lines))
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_table_list"]
|
|
else:
|
|
message = random.choice(templates["no_tables"])
|
|
suggestions = ["importer un fichier Excel"]
|
|
|
|
elif result.get("table_info"):
|
|
info = result["table_info"]
|
|
cols_detail = "\n".join(
|
|
f" {c['name']} ({c['type']})" for c in info.get("columns", [])
|
|
if c["name"] not in ("_rowid", "_imported_at")
|
|
)
|
|
template = random.choice(templates["table_info"])
|
|
message = template.format(
|
|
table_name=info.get("table_name", "?"),
|
|
row_count=info.get("row_count", 0),
|
|
col_count=len([c for c in info.get("columns", []) if c["name"] not in ("_rowid", "_imported_at")]),
|
|
columns_detail=cols_detail,
|
|
)
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_table_list"]
|
|
|
|
elif result.get("folder_files") is not None:
|
|
files = result["folder_files"]
|
|
if files:
|
|
files_list = "\n".join(f" {f}" for f in files)
|
|
template = random.choice(templates["folder_list"])
|
|
message = template.format(count=len(files), files_list=files_list)
|
|
else:
|
|
template = random.choice(templates["folder_empty"])
|
|
message = template.format(folder=result.get("folder", "?"))
|
|
suggestions = ["aide"]
|
|
|
|
elif result.get("uploaded"):
|
|
template = random.choice(templates["uploaded"])
|
|
message = template.format(filename=result.get("filename", "?"))
|
|
suggestions = []
|
|
|
|
elif result.get("error"):
|
|
template = random.choice(templates["error"])
|
|
message = template.format(error=result["error"])
|
|
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_error"]
|
|
|
|
else:
|
|
message = "Je n'ai pas compris votre demande. Précisez le fichier ou dites « montre les tables »."
|
|
suggestions = ["montre les tables", "aide"]
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=suggestions,
|
|
action_required=result.get("needs_confirmation", False),
|
|
action_type="data_import_confirm" if result.get("needs_confirmation") else None,
|
|
metadata=result,
|
|
)
|
|
|
|
def _handle_small_talk(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour la conversation informelle (merci, café, ça va, etc.)."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.SMALL_TALK]
|
|
query = intent.raw_query.lower().strip()
|
|
|
|
# Déterminer la sous-catégorie de small talk
|
|
category = self._classify_small_talk(query)
|
|
category_templates = templates.get(category, templates["humor"])
|
|
message = random.choice(category_templates)
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=[],
|
|
action_required=False,
|
|
)
|
|
|
|
@staticmethod
|
|
def _classify_small_talk(query: str) -> str:
|
|
"""Classifier le type de small talk à partir de la requête brute."""
|
|
# Remerciements
|
|
if re.search(
|
|
r"\b(?:merci|thanks?|thx|super|génial|parfait|cool|nickel|impec|impeccable|excellent|formidable)\b",
|
|
query
|
|
):
|
|
return "thanks"
|
|
|
|
# Adieux
|
|
if re.search(
|
|
r"\b(?:au revoir|à plus|bye|bonne nuit|à bientôt|à demain|ciao|tchao|tchuss|adieu)\b",
|
|
query
|
|
):
|
|
return "farewell"
|
|
|
|
# Identité
|
|
if re.search(
|
|
r"(?:qui es[- ]tu|t'es qui|comment tu t'appelles|c'est quoi ton (?:nom|prénom)|t'es quoi|vous êtes qui|tu t'appelles comment)",
|
|
query
|
|
):
|
|
return "identity"
|
|
|
|
# Sentiments
|
|
if re.search(
|
|
r"(?:ça va|comment (?:ça |tu |vous )?va[st]?|comment allez[- ]vous|tu vas bien|la forme|en forme)",
|
|
query
|
|
):
|
|
return "feelings"
|
|
|
|
# Mécontentement
|
|
if re.search(
|
|
r"\b(?:nul|pas bien|pas top|pas ouf|bof|mauvais|moche|horrible|catastrophe|ça craint|erreur|bug|naze|pourri)\b",
|
|
query
|
|
):
|
|
return "complaint"
|
|
|
|
# Compliments
|
|
if re.search(
|
|
r"\b(?:bien joué|bravo|top|chapeau|impressionnant|pas mal|bien fait|beau travail|good job|nice|trop bien|magnifique)\b",
|
|
query
|
|
):
|
|
return "compliment"
|
|
|
|
# Humour / café (fallback small_talk)
|
|
return "humor"
|
|
|
|
def _handle_unknown(
|
|
self,
|
|
intent: ParsedIntent,
|
|
context: Dict[str, Any],
|
|
result: Dict[str, Any]
|
|
) -> GeneratedResponse:
|
|
"""Handler pour les intentions non reconnues."""
|
|
templates = self.RESPONSE_TEMPLATES[IntentType.UNKNOWN]
|
|
template = random.choice(templates["default"])
|
|
message = template.format(query=intent.raw_query)
|
|
|
|
return GeneratedResponse(
|
|
message=message,
|
|
suggestions=["aide", "mes tâches"],
|
|
action_required=False
|
|
)
|
|
|
|
|
|
# Singleton pour utilisation globale
|
|
_response_generator: Optional[ResponseGenerator] = None
|
|
|
|
def get_response_generator(tone: ResponseTone = ResponseTone.FRIENDLY) -> ResponseGenerator:
|
|
"""Obtenir l'instance globale du générateur de réponses."""
|
|
global _response_generator
|
|
if _response_generator is None:
|
|
_response_generator = ResponseGenerator(tone=tone)
|
|
return _response_generator
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Tests rapides
|
|
from .intent_parser import IntentParser
|
|
|
|
parser = IntentParser()
|
|
generator = ResponseGenerator()
|
|
|
|
test_cases = [
|
|
("facturer client Acme", {"success": True, "workflow": "facturation", "params": {"client": "Acme"}}),
|
|
("liste des workflows", {"workflows": [{"name": "facturation", "description": "Facturer un client"}]}),
|
|
("aide", {}),
|
|
("statut", {"execution": {"running": True, "workflow": "test", "progress": 50, "message": "En cours"}}),
|
|
("blablabla inconnu", {}),
|
|
]
|
|
|
|
print("=== Tests ResponseGenerator ===\n")
|
|
for query, result in test_cases:
|
|
intent = parser.parse(query)
|
|
response = generator.generate(intent, {}, result)
|
|
|
|
print(f"Query: {query}")
|
|
print(f"Response: {response.message[:100]}...")
|
|
print(f"Suggestions: {response.suggestions}")
|
|
print()
|