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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user