From 848184db5c6d68515c0a8c8a04c689f11ed1adb7 Mon Sep 17 00:00:00 2001 From: Dom Date: Fri, 16 Jan 2026 18:04:07 +0100 Subject: [PATCH] =?UTF-8?q?feat(agent=5Fchat):=20Activer=20int=C3=A9gratio?= =?UTF-8?q?n=20LLM=20Ollama=20pour=20parsing=20intelligent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Activer use_llm=True par défaut dans app.py - Améliorer le prompt LLM avec contexte des workflows disponibles - Ajouter endpoints /api/llm/status et /api/llm/model pour configuration - Permettre injection dynamique des workflows dans IntentParser - Supporter changement de modèle à chaud Co-Authored-By: Claude Opus 4.5 --- agent_chat/app.py | 58 ++++++++++++++++- agent_chat/intent_parser.py | 120 +++++++++++++++++++++++++++++------- 2 files changed, 153 insertions(+), 25 deletions(-) diff --git a/agent_chat/app.py b/agent_chat/app.py index 45f587b28..0ef82abf6 100644 --- a/agent_chat/app.py +++ b/agent_chat/app.py @@ -115,11 +115,20 @@ def init_system(): # 3. Composants conversationnels try: - intent_parser = get_intent_parser(use_llm=False) # LLM optionnel + intent_parser = get_intent_parser(use_llm=True) # LLM activé (Ollama) confirmation_loop = get_confirmation_loop() response_generator = get_response_generator() conversation_manager = get_conversation_manager() - logger.info("✓ Composants conversationnels initialisés") + + # Injecter les workflows dans l'intent_parser pour contexte LLM + if matcher and intent_parser: + workflows_for_llm = [ + {"name": wf.name, "description": wf.description, "tags": wf.tags} + for wf in matcher.get_all_workflows() + ] + intent_parser.set_workflows(workflows_for_llm) + + logger.info("✓ Composants conversationnels initialisés (LLM activé)") except Exception as e: logger.error(f"✗ Composants conversationnels: {e}") # Fallback aux composants de base @@ -560,6 +569,51 @@ def api_gpu_action(action): return jsonify({"success": False, "error": str(e)}) +@app.route('/api/llm/status') +def api_llm_status(): + """Statut du LLM (Ollama).""" + status = { + "enabled": intent_parser.use_llm if intent_parser else False, + "available": intent_parser.llm_available if intent_parser else False, + "model": intent_parser.llm_model if intent_parser else None, + "endpoint": intent_parser.llm_endpoint if intent_parser else None, + "workflows_loaded": len(intent_parser._workflows_cache) if intent_parser else 0 + } + + # Lister les modèles Ollama disponibles + try: + import requests + response = requests.get("http://localhost:11434/api/tags", timeout=2) + if response.status_code == 200: + models = response.json().get('models', []) + status["available_models"] = [m["name"] for m in models] + except: + status["available_models"] = [] + + return jsonify(status) + + +@app.route('/api/llm/model', methods=['POST']) +def api_llm_set_model(): + """Changer le modèle LLM.""" + data = request.json + model = data.get('model') + + if not model: + return jsonify({"success": False, "error": "Modèle non spécifié"}) + + if intent_parser: + intent_parser.llm_model = model + intent_parser._check_llm_availability() + return jsonify({ + "success": True, + "model": model, + "available": intent_parser.llm_available + }) + + return jsonify({"success": False, "error": "IntentParser non initialisé"}) + + @app.route('/api/help') def api_help(): """Aide et mode d'emploi.""" diff --git a/agent_chat/intent_parser.py b/agent_chat/intent_parser.py index 10e57fbdf..a377e547f 100644 --- a/agent_chat/intent_parser.py +++ b/agent_chat/intent_parser.py @@ -141,21 +141,39 @@ class IntentParser: ], } - def __init__(self, use_llm: bool = False, llm_endpoint: str = "http://localhost:11434"): + def __init__( + self, + use_llm: bool = False, + llm_endpoint: str = "http://localhost:11434", + llm_model: str = "qwen2.5:7b" + ): """ Initialiser le parseur d'intentions. Args: use_llm: Utiliser un LLM pour l'analyse (optionnel) llm_endpoint: URL de l'endpoint Ollama + llm_model: Modèle Ollama à utiliser """ self.use_llm = use_llm self.llm_endpoint = llm_endpoint + self.llm_model = llm_model self.llm_available = False + self._workflows_cache: List[Dict[str, Any]] = [] if use_llm: self._check_llm_availability() + def set_workflows(self, workflows: List[Dict[str, Any]]): + """ + Injecter la liste des workflows disponibles pour enrichir le contexte LLM. + + Args: + workflows: Liste de dict avec 'name', 'description', 'tags' + """ + self._workflows_cache = workflows + logger.info(f"IntentParser: {len(workflows)} workflows injectés") + def _check_llm_availability(self) -> bool: """Vérifier si le LLM est disponible.""" try: @@ -354,39 +372,63 @@ class IntentParser: try: import requests - prompt = f"""Analyse cette requête utilisateur pour un système RPA. + # Construire le contexte des workflows disponibles + workflows_context = "" + if self._workflows_cache: + workflow_names = [w.get("name", "") for w in self._workflows_cache[:15]] + workflows_context = f"\nWorkflows disponibles: {', '.join(workflow_names)}" -Requête: "{query}" + prompt = f"""Tu es un assistant RPA. Analyse cette requête utilisateur. -Contexte: {json.dumps(context) if context else "Aucun"} +REQUÊTE: "{query}" +{workflows_context} +{f"Contexte conversation: {json.dumps(context, ensure_ascii=False)}" if context else ""} -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 +INTENTIONS POSSIBLES: +- execute: l'utilisateur veut lancer/exécuter un workflow +- list: l'utilisateur veut voir les workflows disponibles (mots-clés: liste, quels, workflows, disponibles, montrer) +- query: l'utilisateur pose une question sur un workflow +- status: l'utilisateur demande le statut d'exécution +- cancel: l'utilisateur veut annuler +- history: l'utilisateur veut voir l'historique +- help: l'utilisateur demande de l'aide +- confirm: l'utilisateur confirme (oui, ok, go) +- deny: l'utilisateur refuse (non, annule) +- unknown: impossible à déterminer -JSON:""" +Réponds UNIQUEMENT en JSON valide (pas de texte avant/après): +{{"intent": "...", "confidence": 0.0-1.0, "workflow_hint": "...", "parameters": {{}}, "clarification_needed": false, "clarification_question": null}}""" response = requests.post( f"{self.llm_endpoint}/api/generate", json={ - "model": "qwen2.5:7b", + "model": self.llm_model, "prompt": prompt, "stream": False, - "options": {"temperature": 0.1} + "options": { + "temperature": 0.1, + "num_predict": 200 + } }, - timeout=10 + timeout=15 ) 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) + result = response.json().get("response", "").strip() + logger.debug(f"LLM response: {result[:200]}") + + # Extraire le JSON de la réponse (supporte JSON imbriqué) + json_match = re.search(r'\{.*\}', result, re.DOTALL) if json_match: - data = json.loads(json_match.group(0)) + try: + data = json.loads(json_match.group(0)) + except json.JSONDecodeError: + # Fallback: essayer de parser un JSON simple + simple_match = re.search(r'\{[^{}]+\}', result) + if simple_match: + data = json.loads(simple_match.group(0)) + else: + return None intent_str = data.get("intent", "unknown") try: @@ -394,9 +436,13 @@ JSON:""" except ValueError: intent_type = IntentType.UNKNOWN + confidence = float(data.get("confidence", 0.5)) + # Boost confidence for LLM results + confidence = min(0.95, confidence + 0.1) + return ParsedIntent( intent_type=intent_type, - confidence=data.get("confidence", 0.5), + confidence=confidence, raw_query=query, workflow_hint=data.get("workflow_hint"), parameters=data.get("parameters", {}), @@ -404,6 +450,8 @@ JSON:""" clarification_needed=data.get("clarification_needed", False), clarification_question=data.get("clarification_question") ) + except requests.exceptions.Timeout: + logger.warning("LLM timeout - falling back to rules") except Exception as e: logger.warning(f"LLM parsing failed: {e}") @@ -413,14 +461,40 @@ JSON:""" # 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.""" +def get_intent_parser( + use_llm: bool = False, + llm_model: str = "qwen2.5:7b", + llm_endpoint: str = "http://localhost:11434" +) -> IntentParser: + """ + Obtenir l'instance globale du parseur d'intentions. + + Args: + use_llm: Activer le LLM (Ollama) + llm_model: Modèle à utiliser (qwen2.5:7b par défaut) + llm_endpoint: URL de l'endpoint Ollama + """ global _intent_parser if _intent_parser is None: - _intent_parser = IntentParser(use_llm=use_llm) + _intent_parser = IntentParser( + use_llm=use_llm, + llm_endpoint=llm_endpoint, + llm_model=llm_model + ) + elif use_llm and not _intent_parser.use_llm: + # Réactiver le LLM si demandé + _intent_parser.use_llm = True + _intent_parser.llm_model = llm_model + _intent_parser._check_llm_availability() return _intent_parser +def reset_intent_parser(): + """Réinitialiser le singleton (pour tests).""" + global _intent_parser + _intent_parser = None + + if __name__ == "__main__": # Tests rapides parser = IntentParser(use_llm=False)