#!/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+|des\s+)?(?:workflows?|processus|automatisations?)", r"liste\s+des\s+workflows?", r"(?:qu'est-ce que|que)\s+(?:je peux|tu peux)\s+faire", r"(?:workflows?|processus)\s+disponibles?", r"(?:voir|afficher)\s+(?:les\s+|tous\s+les\s+)?workflows?", ], 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()