From 5a07e0dee5169b8b8cbcc7e1bc988cd3359a00d4 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 18 Mar 2026 00:55:06 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20L=C3=A9a=20r=C3=A9pond=20via=20LLM=20?= =?UTF-8?q?=E2=80=94=20r=C3=A9ponses=20naturelles=20au=20lieu=20de=20templ?= =?UTF-8?q?ates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _generate_lea_response() appelle Ollama qwen3:8b avec persona Léa - Fallback templates si LLM indisponible - Intent parser conservé pour la détection d'actions - think=false pour éviter les réponses vides qwen3 Co-Authored-By: Claude Opus 4.6 (1M context) --- agent_chat/app.py | 119 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/agent_chat/app.py b/agent_chat/app.py index d0160ac00..a6d1344b2 100644 --- a/agent_chat/app.py +++ b/agent_chat/app.py @@ -42,7 +42,7 @@ from core.workflow import SemanticMatcher, VariableManager # Import des composants conversationnels from .intent_parser import IntentParser, IntentType, get_intent_parser from .confirmation import ConfirmationLoop, ConfirmationStatus, RiskLevel, get_confirmation_loop -from .response_generator import ResponseGenerator, get_response_generator +from .response_generator import ResponseGenerator, GeneratedResponse, get_response_generator from .conversation_manager import ConversationManager, get_conversation_manager from .autonomous_planner import AutonomousPlanner, get_autonomous_planner, ExecutionPlan from .gesture_catalog import GestureCatalog @@ -532,6 +532,89 @@ def api_history(): return jsonify({"history": command_history[-20:]}) +# ============================================================================= +# Réponses LLM naturelles (persona Léa) +# ============================================================================= + +# Modèle texte pour les réponses conversationnelles (pas besoin de vision) +_LEA_LLM_MODEL = os.environ.get("LEA_LLM_MODEL", "qwen3:8b") + +_LEA_SYSTEM_PROMPT = """Tu es Léa, une assistante professionnelle chaleureuse et bienveillante. + +Règles : +- Tu vouvoies TOUJOURS l'utilisateur +- Tu es naturelle, avec un peu d'humour quand c'est approprié +- Réponses COURTES : 1 à 3 phrases maximum +- JAMAIS de jargon technique (pas de workflow, RPA, API, agent, streaming, variable, base de données) +- Tu parles de "tâches" pour désigner les processus automatisés +- Tu parles d'"apprentissage" pour l'enregistrement de tâches +- Tu utilises des emojis avec parcimonie (1 max par message) +- Si on te demande quelque chose d'impossible (café, nourriture...) → réponds avec humour +- Si l'utilisateur te remercie → sois chaleureuse +- Si l'utilisateur est mécontent → sois empathique et propose ton aide +- NE JAMAIS inventer des capacités que tu n'as pas +- NE JAMAIS mentionner de termes techniques + +Ce que tu sais faire : +- Apprendre des tâches en observant l'utilisateur +- Refaire des tâches apprises +- Importer des fichiers Excel +- Répondre aux questions sur les tâches connues""" + + +def _generate_lea_response( + user_message: str, + intent_result: dict, + action_result: dict = None, +) -> Optional[str]: + """Générer une réponse naturelle via le LLM avec le persona de Léa. + + Retourne le texte de la réponse, ou None si le LLM est indisponible + (fallback vers les templates hardcodés). + """ + # Construire le contexte pour le LLM + context_parts = [] + if action_result: + # Informer le LLM du résultat de l'action pour qu'il formule sa réponse + summary = json.dumps(action_result, ensure_ascii=False, default=str)[:300] + context_parts.append(f"[Action effectuée : {summary}]") + + if context_parts: + user_prompt = f"{chr(10).join(context_parts)}\n\nUtilisateur : {user_message}\nLéa :" + else: + user_prompt = f"Utilisateur : {user_message}\nLéa :" + + # Appel Ollama via /api/chat + try: + resp = http_requests.post( + "http://localhost:11434/api/chat", + json={ + "model": _LEA_LLM_MODEL, + "messages": [ + {"role": "system", "content": _LEA_SYSTEM_PROMPT}, + {"role": "user", "content": user_prompt}, + ], + "stream": False, + "think": False, # Désactiver le mode réflexion (qwen3) + "options": {"temperature": 0.7, "num_predict": 150}, + }, + timeout=15, + ) + + if resp.ok: + content = resp.json().get("message", {}).get("content", "") + # Nettoyer la réponse (enlever les balises think, etc.) + content = content.strip() + if "" in content: + content = content.split("")[-1].strip() + if content: + return content + except Exception as e: + logger.warning(f"LLM indisponible pour la réponse Léa : {e}") + + return None # Fallback vers les templates + + @app.route('/api/chat', methods=['POST']) def api_chat(): """ @@ -761,14 +844,34 @@ def api_chat(): # 6. Générer la réponse (si pas déjà fait pour confirmation) if action_taken != "confirmation_requested": - # Si c'est un import de données (même via CONFIRM/DENY), forcer le handler DATA_IMPORT - if action_taken == "data_import": - # Créer un intent fictif pour le dispatch vers _handle_data_import - from dataclasses import replace as _dc_replace - data_intent = _dc_replace(intent, intent_type=IntentType.DATA_IMPORT) - response = response_generator.generate(data_intent, context, result) + # Tenter une réponse LLM naturelle (persona Léa) + lea_text = _generate_lea_response(message, intent.to_dict(), result or None) + + if lea_text: + # Réponse LLM réussie — on construit la GeneratedResponse + # Garder les suggestions du template (utiles pour l'UX) + if action_taken == "data_import": + from dataclasses import replace as _dc_replace + data_intent = _dc_replace(intent, intent_type=IntentType.DATA_IMPORT) + fallback = response_generator.generate(data_intent, context, result) + else: + fallback = response_generator.generate(intent, context, result) + + response = GeneratedResponse( + message=lea_text, + suggestions=fallback.suggestions, + action_required=fallback.action_required, + action_type=fallback.action_type, + metadata=fallback.metadata, + ) else: - response = response_generator.generate(intent, context, result) + # Fallback vers les templates hardcodés si LLM indisponible + if action_taken == "data_import": + from dataclasses import replace as _dc_replace + data_intent = _dc_replace(intent, intent_type=IntentType.DATA_IMPORT) + response = response_generator.generate(data_intent, context, result) + else: + response = response_generator.generate(intent, context, result) # 7. Enregistrer le tour dans la conversation conversation_manager.add_turn(