Files
rpa_vision_v3/agent_chat/intent_parser.py
Dom bc096a3891 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>
2026-01-15 15:20:05 +01:00

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()