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:
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()
|
||||
Reference in New Issue
Block a user