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