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>
449 lines
16 KiB
Python
449 lines
16 KiB
Python
#!/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()
|