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
|
# Import des composants conversationnels
|
||||||
from .intent_parser import IntentParser, IntentType, get_intent_parser
|
from .intent_parser import IntentParser, IntentType, get_intent_parser
|
||||||
from .confirmation import ConfirmationLoop, ConfirmationStatus, RiskLevel, get_confirmation_loop
|
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 .conversation_manager import ConversationManager, get_conversation_manager
|
||||||
from .autonomous_planner import AutonomousPlanner, get_autonomous_planner, ExecutionPlan
|
from .autonomous_planner import AutonomousPlanner, get_autonomous_planner, ExecutionPlan
|
||||||
from .gesture_catalog import GestureCatalog
|
from .gesture_catalog import GestureCatalog
|
||||||
@@ -532,6 +532,89 @@ def api_history():
|
|||||||
return jsonify({"history": command_history[-20:]})
|
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'])
|
@app.route('/api/chat', methods=['POST'])
|
||||||
def api_chat():
|
def api_chat():
|
||||||
"""
|
"""
|
||||||
@@ -761,9 +844,29 @@ def api_chat():
|
|||||||
|
|
||||||
# 6. Générer la réponse (si pas déjà fait pour confirmation)
|
# 6. Générer la réponse (si pas déjà fait pour confirmation)
|
||||||
if action_taken != "confirmation_requested":
|
if action_taken != "confirmation_requested":
|
||||||
# Si c'est un import de données (même via CONFIRM/DENY), forcer le handler DATA_IMPORT
|
# 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:
|
||||||
|
# Fallback vers les templates hardcodés si LLM indisponible
|
||||||
if action_taken == "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
|
from dataclasses import replace as _dc_replace
|
||||||
data_intent = _dc_replace(intent, intent_type=IntentType.DATA_IMPORT)
|
data_intent = _dc_replace(intent, intent_type=IntentType.DATA_IMPORT)
|
||||||
response = response_generator.generate(data_intent, context, result)
|
response = response_generator.generate(data_intent, context, result)
|
||||||
|
|||||||
Reference in New Issue
Block a user