feat: Léa répond via LLM — réponses naturelles au lieu de templates

- _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) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-18 00:55:06 +01:00
parent 5d7ef46c93
commit 5a07e0dee5

View File

@@ -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 "<think>" in content:
content = content.split("</think>")[-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(