feat(agent_chat): Ajout des composants conversationnels
Nouveaux composants pour l'agent conversationnel : - IntentParser: Analyse des intentions utilisateur (règles + LLM optionnel) - ConfirmationLoop: Validation avant actions critiques (niveaux de risque) - ResponseGenerator: Génération de réponses en langage naturel - ConversationManager: Gestion du contexte multi-tour Endpoint /api/chat ajouté pour le flux conversationnel complet. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,69 @@
|
||||
"""
|
||||
RPA Vision V3 - Command Interface
|
||||
RPA Vision V3 - Agent Chat
|
||||
|
||||
Interface web légère pour communiquer avec le système RPA.
|
||||
Interface conversationnelle pour communiquer avec le système RPA.
|
||||
Style "Spotlight/Alfred" - minimaliste et efficace.
|
||||
|
||||
Composants:
|
||||
- IntentParser: Compréhension des intentions utilisateur
|
||||
- ConfirmationLoop: Validation avant actions critiques
|
||||
- ResponseGenerator: Réponses en langage naturel
|
||||
- ConversationManager: Contexte multi-tour
|
||||
|
||||
Auteur: Dom - Janvier 2026
|
||||
"""
|
||||
|
||||
from .intent_parser import (
|
||||
IntentParser,
|
||||
IntentType,
|
||||
ParsedIntent,
|
||||
get_intent_parser
|
||||
)
|
||||
|
||||
from .confirmation import (
|
||||
ConfirmationLoop,
|
||||
PendingConfirmation,
|
||||
ConfirmationStatus,
|
||||
RiskLevel,
|
||||
get_confirmation_loop
|
||||
)
|
||||
|
||||
from .response_generator import (
|
||||
ResponseGenerator,
|
||||
ResponseTone,
|
||||
GeneratedResponse,
|
||||
get_response_generator
|
||||
)
|
||||
|
||||
from .conversation_manager import (
|
||||
ConversationManager,
|
||||
ConversationSession,
|
||||
ConversationTurn,
|
||||
ConversationContext,
|
||||
get_conversation_manager
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Intent Parser
|
||||
"IntentParser",
|
||||
"IntentType",
|
||||
"ParsedIntent",
|
||||
"get_intent_parser",
|
||||
# Confirmation
|
||||
"ConfirmationLoop",
|
||||
"PendingConfirmation",
|
||||
"ConfirmationStatus",
|
||||
"RiskLevel",
|
||||
"get_confirmation_loop",
|
||||
# Response Generator
|
||||
"ResponseGenerator",
|
||||
"ResponseTone",
|
||||
"GeneratedResponse",
|
||||
"get_response_generator",
|
||||
# Conversation Manager
|
||||
"ConversationManager",
|
||||
"ConversationSession",
|
||||
"ConversationTurn",
|
||||
"ConversationContext",
|
||||
"get_conversation_manager",
|
||||
]
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RPA Vision V3 - Interface de Commande Web
|
||||
RPA Vision V3 - Agent Chat
|
||||
|
||||
Interface web légère pour communiquer avec le système RPA.
|
||||
Interface conversationnelle pour communiquer avec le système RPA.
|
||||
Style "Spotlight/Alfred" - minimaliste et efficace.
|
||||
|
||||
Composants intégrés:
|
||||
- IntentParser: Compréhension des intentions utilisateur
|
||||
- ConfirmationLoop: Validation avant actions critiques
|
||||
- ResponseGenerator: Réponses en langage naturel
|
||||
- ConversationManager: Contexte multi-tour
|
||||
|
||||
Usage:
|
||||
python command_interface/app.py
|
||||
python agent_chat/app.py
|
||||
|
||||
Puis ouvrir: http://localhost:5002
|
||||
|
||||
Auteur: Dom - Janvier 2026
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -27,6 +35,12 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from core.workflow import SemanticMatcher, VariableManager
|
||||
|
||||
# Import des composants conversationnels
|
||||
from .intent_parser import IntentParser, IntentType, get_intent_parser
|
||||
from .confirmation import ConfirmationLoop, ConfirmationStatus, RiskLevel, get_confirmation_loop
|
||||
from .response_generator import ResponseGenerator, get_response_generator
|
||||
from .conversation_manager import ConversationManager, get_conversation_manager
|
||||
|
||||
# GPU Resource Manager (optional)
|
||||
try:
|
||||
from core.gpu import get_gpu_resource_manager, ExecutionMode
|
||||
@@ -44,6 +58,11 @@ socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
# Global state
|
||||
matcher: Optional[SemanticMatcher] = None
|
||||
gpu_manager = None
|
||||
intent_parser: Optional[IntentParser] = None
|
||||
confirmation_loop: Optional[ConfirmationLoop] = None
|
||||
response_generator: Optional[ResponseGenerator] = None
|
||||
conversation_manager: Optional[ConversationManager] = None
|
||||
|
||||
execution_status = {
|
||||
"running": False,
|
||||
"workflow": None,
|
||||
@@ -57,6 +76,7 @@ command_history: List[Dict[str, Any]] = []
|
||||
def init_system():
|
||||
"""Initialiser tous les composants du système."""
|
||||
global matcher, gpu_manager
|
||||
global intent_parser, confirmation_loop, response_generator, conversation_manager
|
||||
|
||||
# 1. SemanticMatcher
|
||||
try:
|
||||
@@ -75,6 +95,21 @@ def init_system():
|
||||
logger.warning(f"⚠ GPU Resource Manager: {e}")
|
||||
gpu_manager = None
|
||||
|
||||
# 3. Composants conversationnels
|
||||
try:
|
||||
intent_parser = get_intent_parser(use_llm=False) # LLM optionnel
|
||||
confirmation_loop = get_confirmation_loop()
|
||||
response_generator = get_response_generator()
|
||||
conversation_manager = get_conversation_manager()
|
||||
logger.info("✓ Composants conversationnels initialisés")
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Composants conversationnels: {e}")
|
||||
# Fallback aux composants de base
|
||||
intent_parser = IntentParser(use_llm=False)
|
||||
confirmation_loop = ConfirmationLoop()
|
||||
response_generator = ResponseGenerator()
|
||||
conversation_manager = ConversationManager()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Routes Web
|
||||
@@ -241,6 +276,192 @@ def api_history():
|
||||
return jsonify({"history": command_history[-20:]})
|
||||
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
def api_chat():
|
||||
"""
|
||||
Endpoint conversationnel principal.
|
||||
|
||||
Utilise le flux complet:
|
||||
1. IntentParser: Analyse l'intention
|
||||
2. ConversationManager: Gère le contexte multi-tour
|
||||
3. ConfirmationLoop: Valide les actions sensibles
|
||||
4. ResponseGenerator: Génère la réponse
|
||||
"""
|
||||
data = request.json
|
||||
message = data.get('message', '').strip()
|
||||
session_id = data.get('session_id')
|
||||
|
||||
if not message:
|
||||
return jsonify({"error": "Message vide"}), 400
|
||||
|
||||
# 1. Obtenir ou créer la session
|
||||
session = conversation_manager.get_or_create_session(session_id=session_id)
|
||||
|
||||
# 2. Parser l'intention
|
||||
intent = intent_parser.parse(message)
|
||||
|
||||
# 3. Résoudre les références anaphoriques (ex: "le même", "celui-ci")
|
||||
intent = conversation_manager.resolve_references(session, intent)
|
||||
|
||||
# 4. Construire le contexte
|
||||
context = conversation_manager.get_context_summary(session)
|
||||
context["execution_status"] = execution_status
|
||||
|
||||
# 5. Traiter selon le type d'intention
|
||||
result = {}
|
||||
action_taken = None
|
||||
|
||||
if intent.intent_type == IntentType.CONFIRM:
|
||||
# Confirmer une action en attente
|
||||
pending = conversation_manager.get_pending_confirmation(session)
|
||||
if pending:
|
||||
confirmation_loop.confirm(pending.id)
|
||||
conversation_manager.clear_pending_confirmation(session)
|
||||
result = {"confirmed": True, "workflow": pending.workflow_name}
|
||||
action_taken = "confirmed"
|
||||
|
||||
# Lancer l'exécution
|
||||
socketio.start_background_task(
|
||||
execute_workflow_from_confirmation, pending, session.session_id
|
||||
)
|
||||
else:
|
||||
result = {"confirmed": False}
|
||||
|
||||
elif intent.intent_type == IntentType.DENY:
|
||||
# Refuser une action en attente
|
||||
pending = conversation_manager.get_pending_confirmation(session)
|
||||
if pending:
|
||||
confirmation_loop.deny(pending.id)
|
||||
conversation_manager.clear_pending_confirmation(session)
|
||||
result = {"denied": True}
|
||||
action_taken = "denied"
|
||||
|
||||
elif intent.intent_type == IntentType.EXECUTE:
|
||||
# Exécuter un workflow
|
||||
if matcher and intent.workflow_hint:
|
||||
match = matcher.find_workflow(intent.workflow_hint, min_confidence=0.2)
|
||||
|
||||
if match:
|
||||
# Évaluer le risque
|
||||
risk = confirmation_loop.evaluate_risk(
|
||||
match.workflow_name,
|
||||
{**match.extracted_params, **intent.parameters}
|
||||
)
|
||||
|
||||
if confirmation_loop.requires_confirmation(risk):
|
||||
# Créer une demande de confirmation
|
||||
conf = confirmation_loop.create_confirmation_request(
|
||||
workflow_name=match.workflow_name,
|
||||
parameters={**match.extracted_params, **intent.parameters},
|
||||
action_type="execute",
|
||||
risk_level=risk
|
||||
)
|
||||
conversation_manager.set_pending_confirmation(session, conf)
|
||||
|
||||
# Générer la réponse de confirmation
|
||||
response = response_generator.generate_confirmation_request(conf)
|
||||
result = {"needs_confirmation": True, "confirmation": conf.to_dict()}
|
||||
action_taken = "confirmation_requested"
|
||||
|
||||
else:
|
||||
# Exécuter directement
|
||||
all_params = {**match.extracted_params, **intent.parameters}
|
||||
result = {
|
||||
"success": True,
|
||||
"workflow": match.workflow_name,
|
||||
"params": all_params,
|
||||
"confidence": match.confidence
|
||||
}
|
||||
action_taken = "executed"
|
||||
|
||||
socketio.start_background_task(execute_workflow, match, all_params)
|
||||
else:
|
||||
result = {"not_found": True, "query": intent.workflow_hint}
|
||||
else:
|
||||
result = {"error": "Pas de workflow spécifié"}
|
||||
|
||||
elif intent.intent_type == IntentType.LIST:
|
||||
# Lister les workflows
|
||||
if matcher:
|
||||
workflows = [
|
||||
{"name": wf.name, "description": wf.description}
|
||||
for wf in matcher.get_all_workflows()
|
||||
]
|
||||
result = {"workflows": workflows}
|
||||
else:
|
||||
result = {"workflows": []}
|
||||
action_taken = "listed"
|
||||
|
||||
elif intent.intent_type == IntentType.STATUS:
|
||||
result = {"execution": execution_status}
|
||||
action_taken = "status_checked"
|
||||
|
||||
elif intent.intent_type == IntentType.CANCEL:
|
||||
if execution_status.get("running"):
|
||||
execution_status["running"] = False
|
||||
execution_status["message"] = "Annulé"
|
||||
result = {"cancelled": True}
|
||||
else:
|
||||
result = {"cancelled": False}
|
||||
action_taken = "cancelled"
|
||||
|
||||
elif intent.intent_type == IntentType.HISTORY:
|
||||
result = {"history": command_history[-10:]}
|
||||
action_taken = "history_shown"
|
||||
|
||||
elif intent.intent_type == IntentType.HELP:
|
||||
result = {}
|
||||
action_taken = "help_shown"
|
||||
|
||||
elif intent.clarification_needed:
|
||||
result = {"clarification_needed": True}
|
||||
action_taken = "clarification_requested"
|
||||
|
||||
# 6. Générer la réponse (si pas déjà fait pour confirmation)
|
||||
if action_taken != "confirmation_requested":
|
||||
response = response_generator.generate(intent, context, result)
|
||||
|
||||
# 7. Enregistrer le tour dans la conversation
|
||||
conversation_manager.add_turn(
|
||||
session=session,
|
||||
user_message=message,
|
||||
intent=intent,
|
||||
response=response.message,
|
||||
action_taken=action_taken,
|
||||
result=result
|
||||
)
|
||||
|
||||
# 8. Retourner la réponse
|
||||
return jsonify({
|
||||
"session_id": session.session_id,
|
||||
"intent": intent.to_dict(),
|
||||
"response": response.to_dict(),
|
||||
"result": result,
|
||||
"context": {
|
||||
"current_workflow": session.context.current_workflow,
|
||||
"has_pending_confirmation": session.context.pending_confirmation is not None
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def execute_workflow_from_confirmation(confirmation, session_id):
|
||||
"""Exécuter un workflow après confirmation."""
|
||||
global execution_status
|
||||
|
||||
if not matcher:
|
||||
return
|
||||
|
||||
# Trouver le workflow
|
||||
match = matcher.find_workflow(confirmation.workflow_name, min_confidence=0.1)
|
||||
if not match:
|
||||
return
|
||||
|
||||
# Utiliser les paramètres confirmés (ou modifiés)
|
||||
params = confirmation.modified_parameters or confirmation.parameters
|
||||
|
||||
execute_workflow(match, params)
|
||||
|
||||
|
||||
@app.route('/api/gpu/<action>', methods=['POST'])
|
||||
def api_gpu_action(action):
|
||||
"""Contrôler le GPU Resource Manager."""
|
||||
|
||||
408
agent_chat/confirmation.py
Normal file
408
agent_chat/confirmation.py
Normal file
@@ -0,0 +1,408 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RPA Vision V3 - ConfirmationLoop
|
||||
Système de confirmation avant les actions critiques.
|
||||
|
||||
Ce module gère la validation utilisateur pour les actions sensibles :
|
||||
- Exécution de workflows
|
||||
- Actions destructives ou irréversibles
|
||||
- Actions sur des données sensibles
|
||||
|
||||
Auteur: Dom - Janvier 2026
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, List, Optional, Callable
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RiskLevel(Enum):
|
||||
"""Niveaux de risque des actions."""
|
||||
LOW = "low" # Pas de confirmation requise
|
||||
MEDIUM = "medium" # Confirmation simple
|
||||
HIGH = "high" # Confirmation détaillée
|
||||
CRITICAL = "critical" # Double confirmation
|
||||
|
||||
|
||||
class ConfirmationStatus(Enum):
|
||||
"""Statuts de confirmation."""
|
||||
PENDING = "pending"
|
||||
CONFIRMED = "confirmed"
|
||||
DENIED = "denied"
|
||||
EXPIRED = "expired"
|
||||
MODIFIED = "modified" # Confirmé avec modifications
|
||||
|
||||
|
||||
@dataclass
|
||||
class PendingConfirmation:
|
||||
"""Une confirmation en attente."""
|
||||
id: str
|
||||
action_type: str
|
||||
workflow_name: str
|
||||
parameters: Dict[str, Any]
|
||||
risk_level: RiskLevel
|
||||
created_at: datetime
|
||||
expires_at: datetime
|
||||
status: ConfirmationStatus = ConfirmationStatus.PENDING
|
||||
confirmation_message: str = ""
|
||||
details: List[str] = field(default_factory=list)
|
||||
modified_parameters: Optional[Dict[str, Any]] = None
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
return datetime.now() > self.expires_at
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"action_type": self.action_type,
|
||||
"workflow_name": self.workflow_name,
|
||||
"parameters": self.parameters,
|
||||
"risk_level": self.risk_level.value,
|
||||
"status": self.status.value,
|
||||
"confirmation_message": self.confirmation_message,
|
||||
"details": self.details,
|
||||
"expires_in_seconds": max(0, (self.expires_at - datetime.now()).total_seconds()),
|
||||
"modified_parameters": self.modified_parameters
|
||||
}
|
||||
|
||||
|
||||
class ConfirmationLoop:
|
||||
"""
|
||||
Gestionnaire de confirmations pour l'agent conversationnel.
|
||||
|
||||
Gère le flux de confirmation pour les actions sensibles :
|
||||
1. Analyse du risque de l'action
|
||||
2. Génération du message de confirmation
|
||||
3. Attente de la réponse utilisateur
|
||||
4. Validation et exécution
|
||||
"""
|
||||
|
||||
# Configuration par niveau de risque
|
||||
RISK_CONFIG = {
|
||||
RiskLevel.LOW: {
|
||||
"requires_confirmation": False,
|
||||
"timeout_seconds": 0,
|
||||
"message_template": "Exécution de {workflow_name}..."
|
||||
},
|
||||
RiskLevel.MEDIUM: {
|
||||
"requires_confirmation": True,
|
||||
"timeout_seconds": 60,
|
||||
"message_template": "Voulez-vous exécuter '{workflow_name}' ?"
|
||||
},
|
||||
RiskLevel.HIGH: {
|
||||
"requires_confirmation": True,
|
||||
"timeout_seconds": 120,
|
||||
"message_template": "⚠️ Action importante : '{workflow_name}'\n\nParamètres :\n{params_list}\n\nConfirmez-vous cette action ?"
|
||||
},
|
||||
RiskLevel.CRITICAL: {
|
||||
"requires_confirmation": True,
|
||||
"timeout_seconds": 180,
|
||||
"message_template": "🚨 ACTION CRITIQUE : '{workflow_name}'\n\n{details}\n\nCette action est irréversible. Tapez 'confirmer' pour continuer."
|
||||
}
|
||||
}
|
||||
|
||||
# Mots-clés associés aux niveaux de risque
|
||||
RISK_KEYWORDS = {
|
||||
RiskLevel.CRITICAL: [
|
||||
"supprimer", "delete", "effacer", "purger", "reset", "formater",
|
||||
"production", "live", "client", "facturation", "paiement"
|
||||
],
|
||||
RiskLevel.HIGH: [
|
||||
"modifier", "update", "éditer", "changer", "migrer", "transfert",
|
||||
"export", "envoyer", "publier", "synchroniser"
|
||||
],
|
||||
RiskLevel.MEDIUM: [
|
||||
"créer", "create", "générer", "ajouter", "nouveau", "copier"
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, default_timeout: int = 60):
|
||||
"""
|
||||
Initialiser le gestionnaire de confirmations.
|
||||
|
||||
Args:
|
||||
default_timeout: Timeout par défaut en secondes
|
||||
"""
|
||||
self.default_timeout = default_timeout
|
||||
self.pending_confirmations: Dict[str, PendingConfirmation] = {}
|
||||
self._confirmation_counter = 0
|
||||
|
||||
def evaluate_risk(
|
||||
self,
|
||||
workflow_name: str,
|
||||
parameters: Dict[str, Any],
|
||||
action_type: str = "execute"
|
||||
) -> RiskLevel:
|
||||
"""
|
||||
Évaluer le niveau de risque d'une action.
|
||||
|
||||
Args:
|
||||
workflow_name: Nom du workflow
|
||||
parameters: Paramètres de l'action
|
||||
action_type: Type d'action
|
||||
|
||||
Returns:
|
||||
RiskLevel correspondant
|
||||
"""
|
||||
text_to_analyze = f"{workflow_name} {action_type} {str(parameters)}".lower()
|
||||
|
||||
# Vérifier les mots-clés par ordre de criticité
|
||||
for risk_level in [RiskLevel.CRITICAL, RiskLevel.HIGH, RiskLevel.MEDIUM]:
|
||||
for keyword in self.RISK_KEYWORDS.get(risk_level, []):
|
||||
if keyword in text_to_analyze:
|
||||
logger.debug(f"Risk {risk_level.value} detected by keyword: {keyword}")
|
||||
return risk_level
|
||||
|
||||
return RiskLevel.LOW
|
||||
|
||||
def create_confirmation_request(
|
||||
self,
|
||||
workflow_name: str,
|
||||
parameters: Dict[str, Any],
|
||||
action_type: str = "execute",
|
||||
risk_level: Optional[RiskLevel] = None,
|
||||
custom_message: Optional[str] = None
|
||||
) -> PendingConfirmation:
|
||||
"""
|
||||
Créer une demande de confirmation.
|
||||
|
||||
Args:
|
||||
workflow_name: Nom du workflow
|
||||
parameters: Paramètres de l'action
|
||||
action_type: Type d'action
|
||||
risk_level: Niveau de risque (auto-détecté si non fourni)
|
||||
custom_message: Message personnalisé
|
||||
|
||||
Returns:
|
||||
PendingConfirmation créée
|
||||
"""
|
||||
# Auto-détecter le risque si non fourni
|
||||
if risk_level is None:
|
||||
risk_level = self.evaluate_risk(workflow_name, parameters, action_type)
|
||||
|
||||
# Générer un ID unique
|
||||
self._confirmation_counter += 1
|
||||
confirmation_id = f"conf_{self._confirmation_counter}_{datetime.now().strftime('%H%M%S')}"
|
||||
|
||||
# Calculer le timeout
|
||||
config = self.RISK_CONFIG[risk_level]
|
||||
timeout = config["timeout_seconds"] or self.default_timeout
|
||||
expires_at = datetime.now() + timedelta(seconds=timeout)
|
||||
|
||||
# Générer le message de confirmation
|
||||
if custom_message:
|
||||
message = custom_message
|
||||
else:
|
||||
message = self._generate_confirmation_message(
|
||||
workflow_name, parameters, risk_level
|
||||
)
|
||||
|
||||
# Générer les détails
|
||||
details = self._generate_details(workflow_name, parameters, risk_level)
|
||||
|
||||
# Créer la confirmation
|
||||
confirmation = PendingConfirmation(
|
||||
id=confirmation_id,
|
||||
action_type=action_type,
|
||||
workflow_name=workflow_name,
|
||||
parameters=parameters,
|
||||
risk_level=risk_level,
|
||||
created_at=datetime.now(),
|
||||
expires_at=expires_at,
|
||||
confirmation_message=message,
|
||||
details=details
|
||||
)
|
||||
|
||||
# Stocker si confirmation requise
|
||||
if config["requires_confirmation"]:
|
||||
self.pending_confirmations[confirmation_id] = confirmation
|
||||
logger.info(f"Confirmation request created: {confirmation_id} ({risk_level.value})")
|
||||
|
||||
return confirmation
|
||||
|
||||
def _generate_confirmation_message(
|
||||
self,
|
||||
workflow_name: str,
|
||||
parameters: Dict[str, Any],
|
||||
risk_level: RiskLevel
|
||||
) -> str:
|
||||
"""Générer le message de confirmation."""
|
||||
config = self.RISK_CONFIG[risk_level]
|
||||
template = config["message_template"]
|
||||
|
||||
# Formater la liste des paramètres
|
||||
params_list = "\n".join([f" • {k}: {v}" for k, v in parameters.items()])
|
||||
if not params_list:
|
||||
params_list = " (aucun paramètre)"
|
||||
|
||||
# Générer les détails pour les actions critiques
|
||||
details = ""
|
||||
if risk_level == RiskLevel.CRITICAL:
|
||||
details = "Cette action peut affecter des données importantes."
|
||||
|
||||
return template.format(
|
||||
workflow_name=workflow_name,
|
||||
params_list=params_list,
|
||||
details=details
|
||||
)
|
||||
|
||||
def _generate_details(
|
||||
self,
|
||||
workflow_name: str,
|
||||
parameters: Dict[str, Any],
|
||||
risk_level: RiskLevel
|
||||
) -> List[str]:
|
||||
"""Générer les détails de l'action."""
|
||||
details = []
|
||||
|
||||
# Détails basiques
|
||||
details.append(f"Workflow: {workflow_name}")
|
||||
|
||||
if parameters:
|
||||
for key, value in parameters.items():
|
||||
details.append(f" {key}: {value}")
|
||||
|
||||
# Avertissements selon le risque
|
||||
if risk_level == RiskLevel.CRITICAL:
|
||||
details.append("⚠️ Cette action est irréversible")
|
||||
elif risk_level == RiskLevel.HIGH:
|
||||
details.append("⚠️ Cette action modifiera des données")
|
||||
|
||||
return details
|
||||
|
||||
def confirm(
|
||||
self,
|
||||
confirmation_id: str,
|
||||
modified_parameters: Optional[Dict[str, Any]] = None
|
||||
) -> Optional[PendingConfirmation]:
|
||||
"""
|
||||
Confirmer une action en attente.
|
||||
|
||||
Args:
|
||||
confirmation_id: ID de la confirmation
|
||||
modified_parameters: Paramètres modifiés (optionnel)
|
||||
|
||||
Returns:
|
||||
La confirmation mise à jour, ou None si non trouvée
|
||||
"""
|
||||
confirmation = self.pending_confirmations.get(confirmation_id)
|
||||
|
||||
if not confirmation:
|
||||
logger.warning(f"Confirmation not found: {confirmation_id}")
|
||||
return None
|
||||
|
||||
if confirmation.is_expired():
|
||||
confirmation.status = ConfirmationStatus.EXPIRED
|
||||
logger.info(f"Confirmation expired: {confirmation_id}")
|
||||
return confirmation
|
||||
|
||||
if modified_parameters:
|
||||
confirmation.status = ConfirmationStatus.MODIFIED
|
||||
confirmation.modified_parameters = modified_parameters
|
||||
else:
|
||||
confirmation.status = ConfirmationStatus.CONFIRMED
|
||||
|
||||
logger.info(f"Confirmation confirmed: {confirmation_id}")
|
||||
return confirmation
|
||||
|
||||
def deny(self, confirmation_id: str) -> Optional[PendingConfirmation]:
|
||||
"""
|
||||
Refuser une action en attente.
|
||||
|
||||
Args:
|
||||
confirmation_id: ID de la confirmation
|
||||
|
||||
Returns:
|
||||
La confirmation mise à jour, ou None si non trouvée
|
||||
"""
|
||||
confirmation = self.pending_confirmations.get(confirmation_id)
|
||||
|
||||
if not confirmation:
|
||||
logger.warning(f"Confirmation not found: {confirmation_id}")
|
||||
return None
|
||||
|
||||
confirmation.status = ConfirmationStatus.DENIED
|
||||
logger.info(f"Confirmation denied: {confirmation_id}")
|
||||
return confirmation
|
||||
|
||||
def get_pending(self, confirmation_id: Optional[str] = None) -> Optional[PendingConfirmation]:
|
||||
"""
|
||||
Obtenir une ou la dernière confirmation en attente.
|
||||
|
||||
Args:
|
||||
confirmation_id: ID spécifique (optionnel)
|
||||
|
||||
Returns:
|
||||
La confirmation ou None
|
||||
"""
|
||||
if confirmation_id:
|
||||
return self.pending_confirmations.get(confirmation_id)
|
||||
|
||||
# Retourner la dernière confirmation en attente non expirée
|
||||
for conf in reversed(list(self.pending_confirmations.values())):
|
||||
if conf.status == ConfirmationStatus.PENDING and not conf.is_expired():
|
||||
return conf
|
||||
|
||||
return None
|
||||
|
||||
def cleanup_expired(self) -> int:
|
||||
"""
|
||||
Nettoyer les confirmations expirées.
|
||||
|
||||
Returns:
|
||||
Nombre de confirmations nettoyées
|
||||
"""
|
||||
expired_ids = [
|
||||
conf_id for conf_id, conf in self.pending_confirmations.items()
|
||||
if conf.is_expired() or conf.status != ConfirmationStatus.PENDING
|
||||
]
|
||||
|
||||
for conf_id in expired_ids:
|
||||
del self.pending_confirmations[conf_id]
|
||||
|
||||
if expired_ids:
|
||||
logger.info(f"Cleaned up {len(expired_ids)} confirmations")
|
||||
|
||||
return len(expired_ids)
|
||||
|
||||
def requires_confirmation(self, risk_level: RiskLevel) -> bool:
|
||||
"""Vérifier si un niveau de risque requiert une confirmation."""
|
||||
return self.RISK_CONFIG[risk_level]["requires_confirmation"]
|
||||
|
||||
|
||||
# Singleton pour utilisation globale
|
||||
_confirmation_loop: Optional[ConfirmationLoop] = None
|
||||
|
||||
def get_confirmation_loop() -> ConfirmationLoop:
|
||||
"""Obtenir l'instance globale du gestionnaire de confirmations."""
|
||||
global _confirmation_loop
|
||||
if _confirmation_loop is None:
|
||||
_confirmation_loop = ConfirmationLoop()
|
||||
return _confirmation_loop
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Tests rapides
|
||||
loop = ConfirmationLoop()
|
||||
|
||||
test_cases = [
|
||||
("facturation_client", {"client": "Acme"}, "execute"),
|
||||
("supprimer_donnees", {"table": "clients"}, "execute"),
|
||||
("export_rapport", {"format": "pdf"}, "execute"),
|
||||
("mise_a_jour_production", {"env": "live"}, "update"),
|
||||
]
|
||||
|
||||
print("=== Tests ConfirmationLoop ===\n")
|
||||
for workflow, params, action in test_cases:
|
||||
risk = loop.evaluate_risk(workflow, params, action)
|
||||
conf = loop.create_confirmation_request(workflow, params, action)
|
||||
|
||||
print(f"Workflow: {workflow}")
|
||||
print(f" Risk: {risk.value}")
|
||||
print(f" Requires confirmation: {loop.requires_confirmation(risk)}")
|
||||
print(f" Message: {conf.confirmation_message[:100]}...")
|
||||
print()
|
||||
489
agent_chat/conversation_manager.py
Normal file
489
agent_chat/conversation_manager.py
Normal file
@@ -0,0 +1,489 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RPA Vision V3 - ConversationManager
|
||||
Gestionnaire de contexte multi-tour pour l'agent conversationnel.
|
||||
|
||||
Ce module gère :
|
||||
- L'historique des échanges
|
||||
- Le contexte courant (workflow en cours, paramètres accumulés)
|
||||
- La résolution des références anaphoriques ("le", "ça", "celui-ci")
|
||||
- Les sessions utilisateur
|
||||
|
||||
Auteur: Dom - Janvier 2026
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
|
||||
from .intent_parser import ParsedIntent, IntentType
|
||||
from .confirmation import PendingConfirmation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversationTurn:
|
||||
"""Un tour de conversation."""
|
||||
turn_id: int
|
||||
timestamp: datetime
|
||||
user_message: str
|
||||
intent: ParsedIntent
|
||||
response: str
|
||||
action_taken: Optional[str] = None
|
||||
result: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"turn_id": self.turn_id,
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
"user_message": self.user_message,
|
||||
"intent": self.intent.to_dict(),
|
||||
"response": self.response,
|
||||
"action_taken": self.action_taken,
|
||||
"result": self.result
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversationContext:
|
||||
"""Contexte courant de la conversation."""
|
||||
# Workflow en cours de discussion
|
||||
current_workflow: Optional[str] = None
|
||||
current_workflow_params: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Confirmation en attente
|
||||
pending_confirmation: Optional[PendingConfirmation] = None
|
||||
|
||||
# Entités mentionnées (pour références anaphoriques)
|
||||
mentioned_entities: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Dernier résultat d'exécution
|
||||
last_execution_result: Optional[Dict[str, Any]] = None
|
||||
|
||||
# État de la conversation
|
||||
awaiting_clarification: bool = False
|
||||
clarification_for: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"current_workflow": self.current_workflow,
|
||||
"current_workflow_params": self.current_workflow_params,
|
||||
"pending_confirmation": self.pending_confirmation.to_dict() if self.pending_confirmation else None,
|
||||
"mentioned_entities": self.mentioned_entities,
|
||||
"last_execution_result": self.last_execution_result,
|
||||
"awaiting_clarification": self.awaiting_clarification,
|
||||
"clarification_for": self.clarification_for
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversationSession:
|
||||
"""Session de conversation utilisateur."""
|
||||
session_id: str
|
||||
user_id: Optional[str]
|
||||
created_at: datetime
|
||||
last_activity: datetime
|
||||
turns: List[ConversationTurn] = field(default_factory=list)
|
||||
context: ConversationContext = field(default_factory=ConversationContext)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"user_id": self.user_id,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"last_activity": self.last_activity.isoformat(),
|
||||
"turns_count": len(self.turns),
|
||||
"context": self.context.to_dict()
|
||||
}
|
||||
|
||||
|
||||
class ConversationManager:
|
||||
"""
|
||||
Gestionnaire de conversations multi-tour.
|
||||
|
||||
Maintient le contexte à travers plusieurs échanges :
|
||||
- Historique des tours
|
||||
- Entités mentionnées
|
||||
- Workflow et paramètres en cours
|
||||
- Résolution des références
|
||||
"""
|
||||
|
||||
# Mots de référence anaphorique
|
||||
ANAPHORIC_REFERENCES = {
|
||||
"le": "workflow",
|
||||
"la": "workflow",
|
||||
"celui-ci": "workflow",
|
||||
"celle-ci": "workflow",
|
||||
"ça": "last_action",
|
||||
"cela": "last_action",
|
||||
"lui": "entity",
|
||||
"elle": "entity",
|
||||
"ce client": "client",
|
||||
"cette facture": "invoice",
|
||||
"le même": "last_value",
|
||||
"pareil": "last_value",
|
||||
"idem": "last_value"
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session_timeout_minutes: int = 30,
|
||||
max_history_turns: int = 50,
|
||||
persistence_path: Optional[Path] = None
|
||||
):
|
||||
"""
|
||||
Initialiser le gestionnaire de conversations.
|
||||
|
||||
Args:
|
||||
session_timeout_minutes: Timeout d'inactivité
|
||||
max_history_turns: Nombre max de tours à garder
|
||||
persistence_path: Chemin pour persister les sessions
|
||||
"""
|
||||
self.session_timeout = timedelta(minutes=session_timeout_minutes)
|
||||
self.max_history_turns = max_history_turns
|
||||
self.persistence_path = persistence_path
|
||||
self.sessions: Dict[str, ConversationSession] = {}
|
||||
|
||||
if persistence_path:
|
||||
self._load_sessions()
|
||||
|
||||
def get_or_create_session(
|
||||
self,
|
||||
session_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None
|
||||
) -> ConversationSession:
|
||||
"""
|
||||
Obtenir ou créer une session de conversation.
|
||||
|
||||
Args:
|
||||
session_id: ID de session (généré si non fourni)
|
||||
user_id: ID utilisateur (optionnel)
|
||||
|
||||
Returns:
|
||||
ConversationSession active
|
||||
"""
|
||||
# Générer un ID si non fourni
|
||||
if not session_id:
|
||||
session_id = self._generate_session_id(user_id)
|
||||
|
||||
# Vérifier si la session existe et n'est pas expirée
|
||||
if session_id in self.sessions:
|
||||
session = self.sessions[session_id]
|
||||
if datetime.now() - session.last_activity < self.session_timeout:
|
||||
session.last_activity = datetime.now()
|
||||
return session
|
||||
else:
|
||||
# Session expirée, créer une nouvelle
|
||||
logger.info(f"Session expired: {session_id}")
|
||||
del self.sessions[session_id]
|
||||
|
||||
# Créer une nouvelle session
|
||||
session = ConversationSession(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
created_at=datetime.now(),
|
||||
last_activity=datetime.now()
|
||||
)
|
||||
self.sessions[session_id] = session
|
||||
logger.info(f"New session created: {session_id}")
|
||||
|
||||
return session
|
||||
|
||||
def add_turn(
|
||||
self,
|
||||
session: ConversationSession,
|
||||
user_message: str,
|
||||
intent: ParsedIntent,
|
||||
response: str,
|
||||
action_taken: Optional[str] = None,
|
||||
result: Optional[Dict[str, Any]] = None
|
||||
) -> ConversationTurn:
|
||||
"""
|
||||
Ajouter un tour à la conversation.
|
||||
|
||||
Args:
|
||||
session: Session de conversation
|
||||
user_message: Message utilisateur
|
||||
intent: Intention parsée
|
||||
response: Réponse générée
|
||||
action_taken: Action effectuée
|
||||
result: Résultat de l'action
|
||||
|
||||
Returns:
|
||||
ConversationTurn créé
|
||||
"""
|
||||
turn = ConversationTurn(
|
||||
turn_id=len(session.turns) + 1,
|
||||
timestamp=datetime.now(),
|
||||
user_message=user_message,
|
||||
intent=intent,
|
||||
response=response,
|
||||
action_taken=action_taken,
|
||||
result=result
|
||||
)
|
||||
|
||||
session.turns.append(turn)
|
||||
session.last_activity = datetime.now()
|
||||
|
||||
# Mettre à jour le contexte
|
||||
self._update_context(session, intent, result)
|
||||
|
||||
# Limiter l'historique
|
||||
if len(session.turns) > self.max_history_turns:
|
||||
session.turns = session.turns[-self.max_history_turns:]
|
||||
|
||||
# Persister si configuré
|
||||
if self.persistence_path:
|
||||
self._save_session(session)
|
||||
|
||||
return turn
|
||||
|
||||
def resolve_references(
|
||||
self,
|
||||
session: ConversationSession,
|
||||
intent: ParsedIntent
|
||||
) -> ParsedIntent:
|
||||
"""
|
||||
Résoudre les références anaphoriques dans l'intention.
|
||||
|
||||
Args:
|
||||
session: Session de conversation
|
||||
intent: Intention à résoudre
|
||||
|
||||
Returns:
|
||||
ParsedIntent avec références résolues
|
||||
"""
|
||||
resolved_intent = intent
|
||||
context = session.context
|
||||
|
||||
# Vérifier si le message contient des références
|
||||
message_lower = intent.raw_query.lower()
|
||||
|
||||
for reference, ref_type in self.ANAPHORIC_REFERENCES.items():
|
||||
if reference in message_lower:
|
||||
if ref_type == "workflow" and context.current_workflow:
|
||||
# Référence au workflow courant
|
||||
if not resolved_intent.workflow_hint:
|
||||
resolved_intent.workflow_hint = context.current_workflow
|
||||
logger.debug(f"Resolved '{reference}' to workflow: {context.current_workflow}")
|
||||
|
||||
elif ref_type == "last_action" and context.last_execution_result:
|
||||
# Référence à la dernière action
|
||||
resolved_intent.parameters.update(
|
||||
context.last_execution_result.get("params", {})
|
||||
)
|
||||
|
||||
elif ref_type == "entity" and context.mentioned_entities:
|
||||
# Référence à une entité mentionnée
|
||||
for entity_type, entity_value in context.mentioned_entities.items():
|
||||
if entity_type not in resolved_intent.parameters:
|
||||
resolved_intent.parameters[entity_type] = entity_value
|
||||
|
||||
elif ref_type in context.mentioned_entities:
|
||||
# Référence à un type d'entité spécifique
|
||||
if ref_type not in resolved_intent.parameters:
|
||||
resolved_intent.parameters[ref_type] = context.mentioned_entities[ref_type]
|
||||
|
||||
elif ref_type == "last_value" and context.current_workflow_params:
|
||||
# Répéter les derniers paramètres
|
||||
for key, value in context.current_workflow_params.items():
|
||||
if key not in resolved_intent.parameters:
|
||||
resolved_intent.parameters[key] = value
|
||||
|
||||
return resolved_intent
|
||||
|
||||
def _update_context(
|
||||
self,
|
||||
session: ConversationSession,
|
||||
intent: ParsedIntent,
|
||||
result: Optional[Dict[str, Any]]
|
||||
):
|
||||
"""Mettre à jour le contexte après un tour."""
|
||||
context = session.context
|
||||
|
||||
# Mettre à jour le workflow courant
|
||||
if intent.intent_type == IntentType.EXECUTE:
|
||||
if intent.workflow_hint:
|
||||
context.current_workflow = intent.workflow_hint
|
||||
if intent.parameters:
|
||||
context.current_workflow_params.update(intent.parameters)
|
||||
|
||||
# Mettre à jour les entités mentionnées
|
||||
for entity in intent.entities:
|
||||
entity_type = entity["type"]
|
||||
entity_value = entity["value"]
|
||||
context.mentioned_entities[entity_type] = entity_value
|
||||
|
||||
# Mettre à jour le dernier résultat
|
||||
if result and result.get("success"):
|
||||
context.last_execution_result = result
|
||||
|
||||
# Gérer les clarifications
|
||||
if intent.clarification_needed:
|
||||
context.awaiting_clarification = True
|
||||
context.clarification_for = intent.workflow_hint
|
||||
elif intent.intent_type in [IntentType.CONFIRM, IntentType.DENY]:
|
||||
context.awaiting_clarification = False
|
||||
context.clarification_for = None
|
||||
|
||||
def get_context_summary(self, session: ConversationSession) -> Dict[str, Any]:
|
||||
"""
|
||||
Obtenir un résumé du contexte pour les prompts LLM.
|
||||
|
||||
Args:
|
||||
session: Session de conversation
|
||||
|
||||
Returns:
|
||||
Dict résumant le contexte
|
||||
"""
|
||||
context = session.context
|
||||
|
||||
# Résumé des derniers tours
|
||||
recent_turns = []
|
||||
for turn in session.turns[-5:]:
|
||||
recent_turns.append({
|
||||
"user": turn.user_message,
|
||||
"intent": turn.intent.intent_type.value,
|
||||
"action": turn.action_taken
|
||||
})
|
||||
|
||||
return {
|
||||
"current_workflow": context.current_workflow,
|
||||
"current_params": context.current_workflow_params,
|
||||
"mentioned_entities": context.mentioned_entities,
|
||||
"awaiting_clarification": context.awaiting_clarification,
|
||||
"recent_turns": recent_turns
|
||||
}
|
||||
|
||||
def set_pending_confirmation(
|
||||
self,
|
||||
session: ConversationSession,
|
||||
confirmation: PendingConfirmation
|
||||
):
|
||||
"""Définir une confirmation en attente."""
|
||||
session.context.pending_confirmation = confirmation
|
||||
|
||||
def get_pending_confirmation(
|
||||
self,
|
||||
session: ConversationSession
|
||||
) -> Optional[PendingConfirmation]:
|
||||
"""Obtenir la confirmation en attente."""
|
||||
return session.context.pending_confirmation
|
||||
|
||||
def clear_pending_confirmation(self, session: ConversationSession):
|
||||
"""Effacer la confirmation en attente."""
|
||||
session.context.pending_confirmation = None
|
||||
|
||||
def cleanup_expired_sessions(self) -> int:
|
||||
"""
|
||||
Nettoyer les sessions expirées.
|
||||
|
||||
Returns:
|
||||
Nombre de sessions nettoyées
|
||||
"""
|
||||
now = datetime.now()
|
||||
expired = [
|
||||
sid for sid, session in self.sessions.items()
|
||||
if now - session.last_activity > self.session_timeout
|
||||
]
|
||||
|
||||
for sid in expired:
|
||||
del self.sessions[sid]
|
||||
logger.info(f"Session cleaned up: {sid}")
|
||||
|
||||
return len(expired)
|
||||
|
||||
def _generate_session_id(self, user_id: Optional[str]) -> str:
|
||||
"""Générer un ID de session unique."""
|
||||
base = f"{user_id or 'anon'}_{datetime.now().isoformat()}"
|
||||
return hashlib.md5(base.encode()).hexdigest()[:16]
|
||||
|
||||
def _load_sessions(self):
|
||||
"""Charger les sessions depuis le stockage."""
|
||||
if not self.persistence_path:
|
||||
return
|
||||
|
||||
sessions_file = self.persistence_path / "sessions.json"
|
||||
if sessions_file.exists():
|
||||
try:
|
||||
with open(sessions_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
# TODO: Reconstruire les sessions depuis JSON
|
||||
logger.info(f"Loaded {len(data)} sessions")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load sessions: {e}")
|
||||
|
||||
def _save_session(self, session: ConversationSession):
|
||||
"""Sauvegarder une session."""
|
||||
if not self.persistence_path:
|
||||
return
|
||||
|
||||
try:
|
||||
self.persistence_path.mkdir(parents=True, exist_ok=True)
|
||||
session_file = self.persistence_path / f"session_{session.session_id}.json"
|
||||
|
||||
with open(session_file, 'w') as f:
|
||||
json.dump(session.to_dict(), f, indent=2, default=str)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save session: {e}")
|
||||
|
||||
|
||||
# Singleton pour utilisation globale
|
||||
_conversation_manager: Optional[ConversationManager] = None
|
||||
|
||||
def get_conversation_manager(
|
||||
persistence_path: Optional[Path] = None
|
||||
) -> ConversationManager:
|
||||
"""Obtenir l'instance globale du gestionnaire de conversations."""
|
||||
global _conversation_manager
|
||||
if _conversation_manager is None:
|
||||
_conversation_manager = ConversationManager(persistence_path=persistence_path)
|
||||
return _conversation_manager
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Tests rapides
|
||||
from .intent_parser import IntentParser
|
||||
|
||||
parser = IntentParser()
|
||||
manager = ConversationManager()
|
||||
|
||||
# Simuler une conversation
|
||||
session = manager.get_or_create_session(user_id="test_user")
|
||||
|
||||
test_conversation = [
|
||||
"facturer le client Acme",
|
||||
"oui",
|
||||
"et pour le client Beta ?", # Référence implicite
|
||||
"le même format", # Référence anaphorique
|
||||
"statut",
|
||||
]
|
||||
|
||||
print("=== Tests ConversationManager ===\n")
|
||||
for message in test_conversation:
|
||||
intent = parser.parse(message)
|
||||
|
||||
# Résoudre les références
|
||||
resolved = manager.resolve_references(session, intent)
|
||||
|
||||
print(f"User: {message}")
|
||||
print(f"Intent: {resolved.intent_type.value}")
|
||||
print(f"Workflow: {resolved.workflow_hint}")
|
||||
print(f"Params: {resolved.parameters}")
|
||||
|
||||
# Ajouter le tour
|
||||
manager.add_turn(
|
||||
session=session,
|
||||
user_message=message,
|
||||
intent=resolved,
|
||||
response="[Réponse simulée]",
|
||||
action_taken=resolved.intent_type.value
|
||||
)
|
||||
|
||||
print(f"Context workflow: {session.context.current_workflow}")
|
||||
print()
|
||||
448
agent_chat/intent_parser.py
Normal file
448
agent_chat/intent_parser.py
Normal file
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RPA Vision V3 - IntentParser
|
||||
Parseur d'intentions basé sur LLM pour l'agent conversationnel.
|
||||
|
||||
Ce module analyse les requêtes utilisateur en langage naturel et extrait :
|
||||
- L'intention principale (execute, query, configure, help, etc.)
|
||||
- Les paramètres associés
|
||||
- Le niveau de confiance
|
||||
|
||||
Auteur: Dom - Janvier 2026
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IntentType(Enum):
|
||||
"""Types d'intentions supportées."""
|
||||
EXECUTE = "execute" # Exécuter un workflow
|
||||
QUERY = "query" # Poser une question sur les workflows
|
||||
LIST = "list" # Lister les workflows disponibles
|
||||
CONFIGURE = "configure" # Configurer un paramètre
|
||||
HELP = "help" # Demander de l'aide
|
||||
STATUS = "status" # Vérifier le statut
|
||||
CANCEL = "cancel" # Annuler l'exécution en cours
|
||||
HISTORY = "history" # Voir l'historique
|
||||
CONFIRM = "confirm" # Confirmer une action
|
||||
DENY = "deny" # Refuser une action
|
||||
CLARIFY = "clarify" # Demander une clarification
|
||||
UNKNOWN = "unknown" # Intention non reconnue
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParsedIntent:
|
||||
"""Résultat du parsing d'une intention."""
|
||||
intent_type: IntentType
|
||||
confidence: float
|
||||
raw_query: str
|
||||
workflow_hint: Optional[str] = None
|
||||
parameters: Dict[str, Any] = field(default_factory=dict)
|
||||
entities: List[Dict[str, Any]] = field(default_factory=list)
|
||||
clarification_needed: bool = False
|
||||
clarification_question: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"intent_type": self.intent_type.value,
|
||||
"confidence": self.confidence,
|
||||
"raw_query": self.raw_query,
|
||||
"workflow_hint": self.workflow_hint,
|
||||
"parameters": self.parameters,
|
||||
"entities": self.entities,
|
||||
"clarification_needed": self.clarification_needed,
|
||||
"clarification_question": self.clarification_question
|
||||
}
|
||||
|
||||
|
||||
class IntentParser:
|
||||
"""
|
||||
Parseur d'intentions pour l'agent conversationnel.
|
||||
|
||||
Utilise une combinaison de règles et optionnellement un LLM
|
||||
pour comprendre les requêtes utilisateur.
|
||||
"""
|
||||
|
||||
# Patterns pour la détection d'intentions par règles
|
||||
INTENT_PATTERNS = {
|
||||
IntentType.EXECUTE: [
|
||||
r"(?:lance|exécute|démarre|fait|run|start|execute)\s+(.+)",
|
||||
r"(?:je veux|je voudrais|peux-tu)\s+(.+)",
|
||||
r"(?:facturer?|créer?|générer?|exporter?)\s+(.+)",
|
||||
r"^(.+)\s+(?:maintenant|tout de suite|svp|stp)$",
|
||||
],
|
||||
IntentType.LIST: [
|
||||
r"(?:liste|montre|affiche|quels sont)\s+(?:les\s+)?(?:workflows?|processus|automatisations?)",
|
||||
r"(?:qu'est-ce que|que)\s+(?:je peux|tu peux)\s+faire",
|
||||
r"(?:workflows?|processus)\s+disponibles?",
|
||||
],
|
||||
IntentType.QUERY: [
|
||||
r"(?:comment|pourquoi|quand|où|qui)\s+(.+)\?",
|
||||
r"(?:explique|décris|détaille)\s+(.+)",
|
||||
r"(?:qu'est-ce que|c'est quoi)\s+(.+)",
|
||||
],
|
||||
IntentType.HELP: [
|
||||
r"(?:aide|help|assistance|sos)",
|
||||
r"(?:comment ça marche|comment utiliser)",
|
||||
r"\?{2,}",
|
||||
],
|
||||
IntentType.STATUS: [
|
||||
r"(?:statut|status|état|où en est)",
|
||||
r"(?:ça avance|progression|progress)",
|
||||
r"(?:terminé|fini|done)\s*\?",
|
||||
],
|
||||
IntentType.CANCEL: [
|
||||
r"(?:annule|stop|arrête|cancel|abort)",
|
||||
r"(?:laisse tomber|oublie)",
|
||||
],
|
||||
IntentType.HISTORY: [
|
||||
r"(?:historique|history|dernières?\s+commandes?)",
|
||||
r"(?:qu'est-ce que j'ai fait|actions? précédentes?)",
|
||||
],
|
||||
IntentType.CONFIRM: [
|
||||
r"^(?:oui|yes|ok|d'accord|go|lance|confirme|valide|c'est bon)$",
|
||||
r"^(?:vas-y|fais-le|proceed|continue)$",
|
||||
],
|
||||
IntentType.DENY: [
|
||||
r"^(?:non|no|annule|stop|pas ça|mauvais)$",
|
||||
r"^(?:arrête|ne fais pas|cancel)$",
|
||||
],
|
||||
}
|
||||
|
||||
# Patterns pour l'extraction d'entités
|
||||
ENTITY_PATTERNS = {
|
||||
"client": [
|
||||
r"(?:client|société|entreprise)\s+([A-Z][a-zA-Z0-9\s]+)",
|
||||
r"pour\s+([A-Z][a-zA-Z0-9\s]+?)(?:\s|$|,)",
|
||||
],
|
||||
"date": [
|
||||
r"(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})",
|
||||
r"(aujourd'hui|demain|hier|lundi|mardi|mercredi|jeudi|vendredi)",
|
||||
],
|
||||
"format": [
|
||||
r"(?:en|format|type)\s+(pdf|excel|csv|json|xml)",
|
||||
],
|
||||
"amount": [
|
||||
r"(\d+(?:[.,]\d+)?)\s*(?:€|euros?|dollars?|\$)",
|
||||
],
|
||||
"range": [
|
||||
r"de\s+([A-Za-z])\s+à\s+([A-Za-z])",
|
||||
r"(\d+)\s*(?:-|à|to)\s*(\d+)",
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, use_llm: bool = False, llm_endpoint: str = "http://localhost:11434"):
|
||||
"""
|
||||
Initialiser le parseur d'intentions.
|
||||
|
||||
Args:
|
||||
use_llm: Utiliser un LLM pour l'analyse (optionnel)
|
||||
llm_endpoint: URL de l'endpoint Ollama
|
||||
"""
|
||||
self.use_llm = use_llm
|
||||
self.llm_endpoint = llm_endpoint
|
||||
self.llm_available = False
|
||||
|
||||
if use_llm:
|
||||
self._check_llm_availability()
|
||||
|
||||
def _check_llm_availability(self) -> bool:
|
||||
"""Vérifier si le LLM est disponible."""
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(f"{self.llm_endpoint}/api/tags", timeout=2)
|
||||
self.llm_available = response.status_code == 200
|
||||
if self.llm_available:
|
||||
logger.info("✓ LLM disponible pour IntentParser")
|
||||
return self.llm_available
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM non disponible: {e}")
|
||||
self.llm_available = False
|
||||
return False
|
||||
|
||||
def parse(self, query: str, context: Optional[Dict[str, Any]] = None) -> ParsedIntent:
|
||||
"""
|
||||
Parser une requête utilisateur.
|
||||
|
||||
Args:
|
||||
query: La requête en langage naturel
|
||||
context: Contexte de la conversation (optionnel)
|
||||
|
||||
Returns:
|
||||
ParsedIntent avec l'intention détectée
|
||||
"""
|
||||
query = query.strip()
|
||||
|
||||
if not query:
|
||||
return ParsedIntent(
|
||||
intent_type=IntentType.UNKNOWN,
|
||||
confidence=0.0,
|
||||
raw_query=query
|
||||
)
|
||||
|
||||
# Normaliser la requête
|
||||
normalized = self._normalize_query(query)
|
||||
|
||||
# 1. Détecter l'intention par règles
|
||||
intent_type, rule_confidence = self._detect_intent_by_rules(normalized)
|
||||
|
||||
# 2. Extraire les entités
|
||||
entities = self._extract_entities(query)
|
||||
|
||||
# 3. Extraire le hint de workflow
|
||||
workflow_hint = self._extract_workflow_hint(normalized, intent_type)
|
||||
|
||||
# 4. Construire les paramètres depuis les entités
|
||||
parameters = self._entities_to_parameters(entities)
|
||||
|
||||
# 5. Si le LLM est disponible et la confiance est basse, utiliser le LLM
|
||||
if self.use_llm and self.llm_available and rule_confidence < 0.7:
|
||||
llm_result = self._parse_with_llm(query, context)
|
||||
if llm_result and llm_result.confidence > rule_confidence:
|
||||
return llm_result
|
||||
|
||||
# 6. Vérifier si une clarification est nécessaire
|
||||
clarification_needed, clarification_question = self._check_clarification_needed(
|
||||
intent_type, parameters, workflow_hint
|
||||
)
|
||||
|
||||
return ParsedIntent(
|
||||
intent_type=intent_type,
|
||||
confidence=rule_confidence,
|
||||
raw_query=query,
|
||||
workflow_hint=workflow_hint,
|
||||
parameters=parameters,
|
||||
entities=entities,
|
||||
clarification_needed=clarification_needed,
|
||||
clarification_question=clarification_question
|
||||
)
|
||||
|
||||
def _normalize_query(self, query: str) -> str:
|
||||
"""Normaliser une requête pour le matching."""
|
||||
# Convertir en minuscules
|
||||
normalized = query.lower()
|
||||
|
||||
# Supprimer la ponctuation excessive
|
||||
normalized = re.sub(r'[!.]+$', '', normalized)
|
||||
|
||||
# Normaliser les espaces
|
||||
normalized = re.sub(r'\s+', ' ', normalized).strip()
|
||||
|
||||
return normalized
|
||||
|
||||
def _detect_intent_by_rules(self, query: str) -> Tuple[IntentType, float]:
|
||||
"""Détecter l'intention par matching de patterns."""
|
||||
best_intent = IntentType.UNKNOWN
|
||||
best_confidence = 0.0
|
||||
|
||||
for intent_type, patterns in self.INTENT_PATTERNS.items():
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
# Calculer la confiance basée sur la qualité du match
|
||||
match_length = len(match.group(0))
|
||||
query_length = len(query)
|
||||
confidence = min(0.9, 0.5 + (match_length / query_length) * 0.4)
|
||||
|
||||
if confidence > best_confidence:
|
||||
best_confidence = confidence
|
||||
best_intent = intent_type
|
||||
|
||||
# Si aucune intention trouvée mais la requête ressemble à une commande
|
||||
if best_intent == IntentType.UNKNOWN and len(query.split()) >= 2:
|
||||
# Supposer que c'est une demande d'exécution
|
||||
best_intent = IntentType.EXECUTE
|
||||
best_confidence = 0.4
|
||||
|
||||
return best_intent, best_confidence
|
||||
|
||||
def _extract_entities(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""Extraire les entités nommées de la requête."""
|
||||
entities = []
|
||||
|
||||
for entity_type, patterns in self.ENTITY_PATTERNS.items():
|
||||
for pattern in patterns:
|
||||
matches = re.finditer(pattern, query, re.IGNORECASE)
|
||||
for match in matches:
|
||||
groups = match.groups()
|
||||
value = groups[0] if len(groups) == 1 else groups
|
||||
|
||||
entities.append({
|
||||
"type": entity_type,
|
||||
"value": value,
|
||||
"start": match.start(),
|
||||
"end": match.end(),
|
||||
"text": match.group(0)
|
||||
})
|
||||
|
||||
return entities
|
||||
|
||||
def _extract_workflow_hint(self, query: str, intent_type: IntentType) -> Optional[str]:
|
||||
"""Extraire un indice sur le workflow demandé."""
|
||||
if intent_type not in [IntentType.EXECUTE, IntentType.QUERY]:
|
||||
return None
|
||||
|
||||
# Supprimer les mots-clés d'intention
|
||||
hint = query
|
||||
for pattern in self.INTENT_PATTERNS.get(intent_type, []):
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match and match.groups():
|
||||
hint = match.group(1)
|
||||
break
|
||||
|
||||
# Nettoyer le hint
|
||||
hint = re.sub(r'^\s*(le|la|les|un|une|des)\s+', '', hint)
|
||||
hint = hint.strip()
|
||||
|
||||
return hint if hint else None
|
||||
|
||||
def _entities_to_parameters(self, entities: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Convertir les entités en paramètres."""
|
||||
parameters = {}
|
||||
|
||||
for entity in entities:
|
||||
entity_type = entity["type"]
|
||||
value = entity["value"]
|
||||
|
||||
# Gérer les plages
|
||||
if entity_type == "range" and isinstance(value, tuple):
|
||||
parameters["start"] = value[0]
|
||||
parameters["end"] = value[1]
|
||||
else:
|
||||
# Si le paramètre existe déjà, le convertir en liste
|
||||
if entity_type in parameters:
|
||||
if isinstance(parameters[entity_type], list):
|
||||
parameters[entity_type].append(value)
|
||||
else:
|
||||
parameters[entity_type] = [parameters[entity_type], value]
|
||||
else:
|
||||
parameters[entity_type] = value
|
||||
|
||||
return parameters
|
||||
|
||||
def _check_clarification_needed(
|
||||
self,
|
||||
intent_type: IntentType,
|
||||
parameters: Dict[str, Any],
|
||||
workflow_hint: Optional[str]
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""Vérifier si une clarification est nécessaire."""
|
||||
|
||||
if intent_type == IntentType.EXECUTE:
|
||||
# Si pas de hint de workflow, demander clarification
|
||||
if not workflow_hint:
|
||||
return True, "Quel workflow souhaitez-vous exécuter ?"
|
||||
|
||||
# Si le hint est trop vague
|
||||
if len(workflow_hint.split()) <= 1:
|
||||
return True, f"Pouvez-vous préciser ce que vous entendez par '{workflow_hint}' ?"
|
||||
|
||||
return False, None
|
||||
|
||||
def _parse_with_llm(self, query: str, context: Optional[Dict[str, Any]]) -> Optional[ParsedIntent]:
|
||||
"""Utiliser le LLM pour parser la requête."""
|
||||
try:
|
||||
import requests
|
||||
|
||||
prompt = f"""Analyse cette requête utilisateur pour un système RPA.
|
||||
|
||||
Requête: "{query}"
|
||||
|
||||
Contexte: {json.dumps(context) if context else "Aucun"}
|
||||
|
||||
Réponds en JSON avec:
|
||||
- intent: execute|query|list|configure|help|status|cancel|history|confirm|deny|unknown
|
||||
- confidence: 0.0 à 1.0
|
||||
- workflow_hint: le workflow demandé si applicable
|
||||
- parameters: dict des paramètres extraits
|
||||
- clarification_needed: true/false
|
||||
- clarification_question: question à poser si besoin
|
||||
|
||||
JSON:"""
|
||||
|
||||
response = requests.post(
|
||||
f"{self.llm_endpoint}/api/generate",
|
||||
json={
|
||||
"model": "qwen2.5:7b",
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.1}
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json().get("response", "")
|
||||
# Extraire le JSON de la réponse
|
||||
json_match = re.search(r'\{[^{}]+\}', result, re.DOTALL)
|
||||
if json_match:
|
||||
data = json.loads(json_match.group(0))
|
||||
|
||||
intent_str = data.get("intent", "unknown")
|
||||
try:
|
||||
intent_type = IntentType(intent_str)
|
||||
except ValueError:
|
||||
intent_type = IntentType.UNKNOWN
|
||||
|
||||
return ParsedIntent(
|
||||
intent_type=intent_type,
|
||||
confidence=data.get("confidence", 0.5),
|
||||
raw_query=query,
|
||||
workflow_hint=data.get("workflow_hint"),
|
||||
parameters=data.get("parameters", {}),
|
||||
entities=[],
|
||||
clarification_needed=data.get("clarification_needed", False),
|
||||
clarification_question=data.get("clarification_question")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"LLM parsing failed: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Singleton pour utilisation globale
|
||||
_intent_parser: Optional[IntentParser] = None
|
||||
|
||||
def get_intent_parser(use_llm: bool = False) -> IntentParser:
|
||||
"""Obtenir l'instance globale du parseur d'intentions."""
|
||||
global _intent_parser
|
||||
if _intent_parser is None:
|
||||
_intent_parser = IntentParser(use_llm=use_llm)
|
||||
return _intent_parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Tests rapides
|
||||
parser = IntentParser(use_llm=False)
|
||||
|
||||
test_queries = [
|
||||
"facturer le client Acme",
|
||||
"lance le workflow de facturation",
|
||||
"quels workflows sont disponibles ?",
|
||||
"aide",
|
||||
"oui",
|
||||
"annule",
|
||||
"statut",
|
||||
"exporter le rapport en PDF pour Client ABC",
|
||||
"créer une facture de 1500€ pour Société XYZ",
|
||||
"facturer les clients de A à Z",
|
||||
]
|
||||
|
||||
print("=== Tests IntentParser ===\n")
|
||||
for query in test_queries:
|
||||
result = parser.parse(query)
|
||||
print(f"Query: {query}")
|
||||
print(f" Intent: {result.intent_type.value} ({result.confidence:.2f})")
|
||||
print(f" Workflow: {result.workflow_hint}")
|
||||
print(f" Params: {result.parameters}")
|
||||
if result.clarification_needed:
|
||||
print(f" Clarification: {result.clarification_question}")
|
||||
print()
|
||||
633
agent_chat/response_generator.py
Normal file
633
agent_chat/response_generator.py
Normal file
@@ -0,0 +1,633 @@
|
||||
#!/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
|
||||
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
|
||||
RESPONSE_TEMPLATES = {
|
||||
IntentType.EXECUTE: {
|
||||
"success": [
|
||||
"J'ai lancé le workflow '{workflow}'. {details}",
|
||||
"Le workflow '{workflow}' est en cours d'exécution. {details}",
|
||||
"C'est parti pour '{workflow}' ! {details}"
|
||||
],
|
||||
"error": [
|
||||
"Impossible d'exécuter '{workflow}': {error}",
|
||||
"Erreur lors du lancement de '{workflow}': {error}",
|
||||
"Le workflow '{workflow}' a échoué: {error}"
|
||||
],
|
||||
"not_found": [
|
||||
"Je n'ai pas trouvé de workflow correspondant à '{query}'.",
|
||||
"Aucun workflow ne correspond à '{query}'. Voulez-vous voir la liste ?",
|
||||
"'{query}' ne correspond à aucun workflow connu."
|
||||
]
|
||||
},
|
||||
IntentType.LIST: {
|
||||
"success": [
|
||||
"Voici les workflows disponibles :\n{list}",
|
||||
"J'ai trouvé {count} workflows :\n{list}",
|
||||
],
|
||||
"empty": [
|
||||
"Aucun workflow n'est configuré pour le moment.",
|
||||
"La liste des workflows est vide."
|
||||
]
|
||||
},
|
||||
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}'.",
|
||||
"Je ne peux pas répondre à cette question sur '{topic}'."
|
||||
]
|
||||
},
|
||||
IntentType.HELP: {
|
||||
"general": [
|
||||
"Je suis votre assistant RPA. Voici ce que je peux faire :\n\n"
|
||||
"• Exécuter des workflows : \"lance facturation client Acme\"\n"
|
||||
"• Lister les workflows : \"quels workflows sont disponibles ?\"\n"
|
||||
"• Voir le statut : \"où en est l'exécution ?\"\n"
|
||||
"• Annuler : \"annule\"\n\n"
|
||||
"Tapez votre commande en langage naturel !",
|
||||
]
|
||||
},
|
||||
IntentType.STATUS: {
|
||||
"running": [
|
||||
"Exécution en cours : '{workflow}'\nProgression : {progress}%\n{message}",
|
||||
"Le workflow '{workflow}' s'exécute ({progress}%): {message}"
|
||||
],
|
||||
"idle": [
|
||||
"Aucune exécution en cours. Système prêt.",
|
||||
"Tout est calme. Que puis-je faire pour vous ?"
|
||||
],
|
||||
"completed": [
|
||||
"Dernière exécution : '{workflow}' - {status}",
|
||||
"'{workflow}' est terminé : {status}"
|
||||
]
|
||||
},
|
||||
IntentType.CANCEL: {
|
||||
"success": [
|
||||
"Exécution annulée.",
|
||||
"J'ai arrêté le workflow en cours.",
|
||||
"Annulation effectuée."
|
||||
],
|
||||
"nothing": [
|
||||
"Rien à annuler, aucune exécution en cours.",
|
||||
"Il n'y a pas d'exécution active."
|
||||
]
|
||||
},
|
||||
IntentType.HISTORY: {
|
||||
"success": [
|
||||
"Voici vos dernières commandes :\n{history}",
|
||||
"Historique récent :\n{history}"
|
||||
],
|
||||
"empty": [
|
||||
"Pas encore d'historique.",
|
||||
"Vous n'avez pas encore exécuté de commandes."
|
||||
]
|
||||
},
|
||||
IntentType.CONFIRM: {
|
||||
"accepted": [
|
||||
"Très bien, j'exécute '{workflow}'.",
|
||||
"C'est parti pour '{workflow}' !",
|
||||
"Confirmé. Lancement de '{workflow}'."
|
||||
],
|
||||
"no_pending": [
|
||||
"Il n'y a rien à confirmer.",
|
||||
"Aucune action en attente de confirmation."
|
||||
]
|
||||
},
|
||||
IntentType.DENY: {
|
||||
"cancelled": [
|
||||
"Action annulée.",
|
||||
"D'accord, j'annule.",
|
||||
"Compris, on oublie."
|
||||
]
|
||||
},
|
||||
IntentType.CLARIFY: {
|
||||
"question": [
|
||||
"{question}",
|
||||
]
|
||||
},
|
||||
IntentType.UNKNOWN: {
|
||||
"default": [
|
||||
"Je n'ai pas compris. Pouvez-vous reformuler ?",
|
||||
"Désolé, je ne comprends pas '{query}'. Tapez 'aide' pour voir les commandes.",
|
||||
"'{query}' ? Je ne suis pas sûr de comprendre."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Suggestions par contexte
|
||||
CONTEXTUAL_SUGGESTIONS = {
|
||||
"after_execute": [
|
||||
"voir le statut",
|
||||
"annuler",
|
||||
"liste des workflows"
|
||||
],
|
||||
"after_error": [
|
||||
"aide",
|
||||
"liste des workflows",
|
||||
"réessayer"
|
||||
],
|
||||
"after_list": [
|
||||
"exécuter un workflow",
|
||||
"aide"
|
||||
],
|
||||
"idle": [
|
||||
"facturer client X",
|
||||
"liste des workflows",
|
||||
"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 du workflow
|
||||
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=["annuler"] 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 du workflow
|
||||
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:
|
||||
emoji = "✅"
|
||||
status = "terminé avec succès"
|
||||
suggestions = self.CONTEXTUAL_SUGGESTIONS["idle"]
|
||||
else:
|
||||
emoji = "❌"
|
||||
status = "échoué"
|
||||
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_error"]
|
||||
|
||||
response_message = f"{emoji} **{workflow_name}** {status}\n\n{message}"
|
||||
|
||||
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("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"])
|
||||
message = template.format(query=intent.raw_query)
|
||||
suggestions = self.CONTEXTUAL_SUGGESTIONS["after_error"]
|
||||
|
||||
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_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_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", "liste des workflows"],
|
||||
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()
|
||||
Reference in New Issue
Block a user