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