feat(agent_chat): Activer intégration LLM Ollama pour parsing intelligent

- 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 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-16 18:04:07 +01:00
parent 152431803e
commit 848184db5c
2 changed files with 153 additions and 25 deletions

View File

@@ -115,11 +115,20 @@ def init_system():
# 3. Composants conversationnels # 3. Composants conversationnels
try: 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() confirmation_loop = get_confirmation_loop()
response_generator = get_response_generator() response_generator = get_response_generator()
conversation_manager = get_conversation_manager() 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: except Exception as e:
logger.error(f"✗ Composants conversationnels: {e}") logger.error(f"✗ Composants conversationnels: {e}")
# Fallback aux composants de base # Fallback aux composants de base
@@ -560,6 +569,51 @@ def api_gpu_action(action):
return jsonify({"success": False, "error": str(e)}) 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') @app.route('/api/help')
def api_help(): def api_help():
"""Aide et mode d'emploi.""" """Aide et mode d'emploi."""

View File

@@ -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. Initialiser le parseur d'intentions.
Args: Args:
use_llm: Utiliser un LLM pour l'analyse (optionnel) use_llm: Utiliser un LLM pour l'analyse (optionnel)
llm_endpoint: URL de l'endpoint Ollama llm_endpoint: URL de l'endpoint Ollama
llm_model: Modèle Ollama à utiliser
""" """
self.use_llm = use_llm self.use_llm = use_llm
self.llm_endpoint = llm_endpoint self.llm_endpoint = llm_endpoint
self.llm_model = llm_model
self.llm_available = False self.llm_available = False
self._workflows_cache: List[Dict[str, Any]] = []
if use_llm: if use_llm:
self._check_llm_availability() 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: def _check_llm_availability(self) -> bool:
"""Vérifier si le LLM est disponible.""" """Vérifier si le LLM est disponible."""
try: try:
@@ -354,39 +372,63 @@ class IntentParser:
try: try:
import requests 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: INTENTIONS POSSIBLES:
- intent: execute|query|list|configure|help|status|cancel|history|confirm|deny|unknown - execute: l'utilisateur veut lancer/exécuter un workflow
- confidence: 0.0 à 1.0 - list: l'utilisateur veut voir les workflows disponibles (mots-clés: liste, quels, workflows, disponibles, montrer)
- workflow_hint: le workflow demandé si applicable - query: l'utilisateur pose une question sur un workflow
- parameters: dict des paramètres extraits - status: l'utilisateur demande le statut d'exécution
- clarification_needed: true/false - cancel: l'utilisateur veut annuler
- clarification_question: question à poser si besoin - 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( response = requests.post(
f"{self.llm_endpoint}/api/generate", f"{self.llm_endpoint}/api/generate",
json={ json={
"model": "qwen2.5:7b", "model": self.llm_model,
"prompt": prompt, "prompt": prompt,
"stream": False, "stream": False,
"options": {"temperature": 0.1} "options": {
"temperature": 0.1,
"num_predict": 200
}
}, },
timeout=10 timeout=15
) )
if response.status_code == 200: if response.status_code == 200:
result = response.json().get("response", "") result = response.json().get("response", "").strip()
# Extraire le JSON de la réponse logger.debug(f"LLM response: {result[:200]}")
json_match = re.search(r'\{[^{}]+\}', result, re.DOTALL)
# Extraire le JSON de la réponse (supporte JSON imbriqué)
json_match = re.search(r'\{.*\}', result, re.DOTALL)
if json_match: 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") intent_str = data.get("intent", "unknown")
try: try:
@@ -394,9 +436,13 @@ JSON:"""
except ValueError: except ValueError:
intent_type = IntentType.UNKNOWN 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( return ParsedIntent(
intent_type=intent_type, intent_type=intent_type,
confidence=data.get("confidence", 0.5), confidence=confidence,
raw_query=query, raw_query=query,
workflow_hint=data.get("workflow_hint"), workflow_hint=data.get("workflow_hint"),
parameters=data.get("parameters", {}), parameters=data.get("parameters", {}),
@@ -404,6 +450,8 @@ JSON:"""
clarification_needed=data.get("clarification_needed", False), clarification_needed=data.get("clarification_needed", False),
clarification_question=data.get("clarification_question") clarification_question=data.get("clarification_question")
) )
except requests.exceptions.Timeout:
logger.warning("LLM timeout - falling back to rules")
except Exception as e: except Exception as e:
logger.warning(f"LLM parsing failed: {e}") logger.warning(f"LLM parsing failed: {e}")
@@ -413,14 +461,40 @@ JSON:"""
# Singleton pour utilisation globale # Singleton pour utilisation globale
_intent_parser: Optional[IntentParser] = None _intent_parser: Optional[IntentParser] = None
def get_intent_parser(use_llm: bool = False) -> IntentParser: def get_intent_parser(
"""Obtenir l'instance globale du parseur d'intentions.""" 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 global _intent_parser
if _intent_parser is None: 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 return _intent_parser
def reset_intent_parser():
"""Réinitialiser le singleton (pour tests)."""
global _intent_parser
_intent_parser = None
if __name__ == "__main__": if __name__ == "__main__":
# Tests rapides # Tests rapides
parser = IntentParser(use_llm=False) parser = IntentParser(use_llm=False)