""" Gestionnaire de suggestions pour le Mode Assisté. Gère les suggestions en temps réel, les scores de confiance et les timeouts. """ import time from typing import Dict, Any, Optional, Callable, List from datetime import datetime, timedelta from threading import Lock from .learning_manager import LearningManager from .embeddings_manager import EmbeddingsManager from .logger import Logger from .workflow_matcher import WorkflowMatcher, WorkflowMatch class SuggestionManager: """ Gestionnaire de suggestions pour le Mode Assisté. """ def __init__( self, learning_manager: LearningManager, embeddings_manager: EmbeddingsManager, logger: Logger, config: Dict[str, Any], workflow_matcher: Optional[WorkflowMatcher] = None ): """ Initialise le gestionnaire de suggestions. Args: learning_manager: Gestionnaire d'apprentissage embeddings_manager: Gestionnaire d'embeddings logger: Logger config: Configuration workflow_matcher: Matcher de workflows (optionnel) """ self.learning_manager = learning_manager self.embeddings_manager = embeddings_manager self.logger = logger self.config = config # WorkflowMatcher pour la détection de workflows self.workflow_matcher = workflow_matcher or WorkflowMatcher(logger, config) # Configuration self.similarity_threshold = config.get("assist", {}).get( "similarity_threshold", 0.75 ) self.suggestion_timeout = config.get("assist", {}).get( "suggestion_timeout", 10.0 # secondes ) self.workflow_confidence_threshold = config.get("workflow", {}).get( "min_confidence", 0.80 # 80% par défaut ) # État actuel self.current_suggestion: Optional[Dict[str, Any]] = None self.suggestion_lock = Lock() self.suggestion_start_time: Optional[datetime] = None # Tracking des rejets par workflow self.workflow_rejections: Dict[str, int] = {} # workflow_id -> count self.workflow_priority_adjustments: Dict[str, float] = {} # workflow_id -> multiplier # Callbacks self.on_suggestion_created: Optional[Callable] = None self.on_suggestion_accepted: Optional[Callable] = None self.on_suggestion_rejected: Optional[Callable] = None self.on_suggestion_timeout: Optional[Callable] = None self.logger.log_action({ "action": "suggestion_manager_initialized", "similarity_threshold": self.similarity_threshold, "timeout": self.suggestion_timeout, "workflow_confidence_threshold": self.workflow_confidence_threshold }) def find_suggestion(self, context: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ Recherche une suggestion basée sur le contexte actuel. Args: context: Contexte actuel (embedding, fenêtre, etc.) Returns: Suggestion ou None """ # D'abord, vérifier s'il y a un workflow en cours workflow_suggestion = self._check_workflow_suggestion(context) if workflow_suggestion: return workflow_suggestion # Sinon, recherche classique par embedding embedding = context.get("embedding") if embedding is None: return None # Rechercher dans FAISS results = self.embeddings_manager.search_similar(embedding, k=3) if not results: return None # Filtrer par seuil de similarité best_match = results[0] if best_match["similarity"] < self.similarity_threshold: return None # Récupérer les métadonnées metadata = best_match["metadata"] task_id = metadata.get("task_id") if not task_id: return None # Charger la tâche task = self.learning_manager.load_task(task_id) if not task: return None # Créer la suggestion suggestion = { "type": "action", # Type de suggestion "task_id": task_id, "task_name": task.task_name, "action_type": metadata.get("action_type", "unknown"), "description": metadata.get("description", ""), "similarity": best_match["similarity"], "confidence": self._calculate_confidence(best_match, task), "metadata": metadata, "timestamp": datetime.now() } self.logger.log_action({ "action": "suggestion_found", "task_id": task_id, "similarity": best_match["similarity"], "confidence": suggestion["confidence"] }) return suggestion def _check_workflow_suggestion(self, context: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ Vérifie s'il y a un workflow en cours qui correspond au contexte. Args: context: Contexte actuel Returns: Suggestion de workflow ou None """ # Récupérer l'event_capture depuis le contexte event_capture = context.get("event_capture") if not event_capture: return None # Récupérer les workflows détectés workflows = event_capture.get_workflows() if not workflows: return None # Récupérer la session courante current_session = event_capture.session_manager.current_session if not current_session or not current_session.actions: return None # Comparer avec les workflows connus for workflow in workflows: # Vérifier si on est au début d'un workflow match_score = self._match_workflow_start(current_session.actions, workflow) if match_score >= 0.8: # 80% de correspondance # Calculer quelle est la prochaine étape next_step_index = len(current_session.actions) if next_step_index < len(workflow.steps): next_step = workflow.steps[next_step_index] # Créer une suggestion de workflow suggestion = { "type": "workflow", # Type workflow "workflow_id": workflow.workflow_id, "workflow_name": workflow.name, "current_step": next_step_index, "total_steps": len(workflow.steps), "next_action": { "action_type": next_step.action_type, "description": next_step.target_description, "position": next_step.position }, "remaining_steps": len(workflow.steps) - next_step_index, "confidence": workflow.confidence * match_score, "repetitions": workflow.repetitions, "timestamp": datetime.now() } self.logger.log_action({ "action": "workflow_suggestion_found", "workflow_id": workflow.workflow_id, "step": next_step_index, "confidence": suggestion["confidence"] }) return suggestion return None def _match_workflow_start(self, current_actions: list, workflow) -> float: """ Calcule le score de correspondance entre les actions courantes et le début d'un workflow. Args: current_actions: Actions de la session courante workflow: Workflow à comparer Returns: Score de correspondance (0-1) """ if not current_actions or not workflow.steps: return 0.0 # Comparer les N premières actions n = min(len(current_actions), len(workflow.steps)) matches = 0 for i in range(n): action = current_actions[i] step = workflow.steps[i] # Comparer le type d'action if action.get("action_type") == step.action_type: matches += 1 # Bonus si même fenêtre if action.get("window") == step.window: matches += 0.5 # Score normalisé max_score = n * 1.5 # 1 pour le type + 0.5 pour la fenêtre return matches / max_score if max_score > 0 else 0.0 def _calculate_confidence( self, match: Dict[str, Any], task: Any ) -> float: """ Calcule le score de confiance pour une suggestion. Args: match: Résultat de recherche FAISS task: Profil de tâche Returns: Score de confiance (0-1) """ # Similarité visuelle vision_score = match["similarity"] # Performance historique historical_score = task.concordance_rate if hasattr(task, "concordance_rate") else 0.5 # Formule : 70% vision + 30% historique confidence = 0.7 * vision_score + 0.3 * historical_score return max(0.0, min(1.0, confidence)) def create_suggestion(self, context: Dict[str, Any]) -> bool: """ Crée une nouvelle suggestion si applicable. Args: context: Contexte actuel Returns: True si suggestion créée """ with self.suggestion_lock: # Vérifier qu'il n'y a pas déjà une suggestion active if self.current_suggestion is not None: return False # Rechercher une suggestion suggestion = self.find_suggestion(context) if suggestion is None: return False # Vérifier le seuil de confiance if suggestion["confidence"] < self.similarity_threshold: return False # Créer la suggestion self.current_suggestion = suggestion self.suggestion_start_time = datetime.now() # Callback if self.on_suggestion_created: self.on_suggestion_created(suggestion) self.logger.log_action({ "action": "suggestion_created", "task_id": suggestion["task_id"], "confidence": suggestion["confidence"] }) return True def accept_suggestion(self) -> Optional[Dict[str, Any]]: """ Accepte la suggestion actuelle. Returns: Suggestion acceptée ou None """ with self.suggestion_lock: if self.current_suggestion is None: return None suggestion = self.current_suggestion self.current_suggestion = None self.suggestion_start_time = None # Si c'est une suggestion de workflow, tracker l'acceptation if suggestion.get("type") == "workflow": workflow_id = suggestion.get("workflow_id") if workflow_id: self._track_workflow_acceptance(workflow_id) # Mettre à jour les statistiques (pour les suggestions d'action) if suggestion.get("task_id"): self.learning_manager.confirm_action({ "type": "accept", "task_id": suggestion["task_id"] }) # Callback if self.on_suggestion_accepted: self.on_suggestion_accepted(suggestion) self.logger.log_action({ "action": "suggestion_accepted", "suggestion_type": suggestion.get("type"), "workflow_id": suggestion.get("workflow_id"), "task_id": suggestion.get("task_id") }) return suggestion def reject_suggestion(self) -> bool: """ Rejette la suggestion actuelle. Returns: True si suggestion rejetée """ with self.suggestion_lock: if self.current_suggestion is None: return False suggestion = self.current_suggestion self.current_suggestion = None self.suggestion_start_time = None # Si c'est une suggestion de workflow, tracker le rejet if suggestion.get("type") == "workflow": workflow_id = suggestion.get("workflow_id") if workflow_id: self._track_workflow_rejection(workflow_id) # Mettre à jour les statistiques (pour les suggestions d'action) if suggestion.get("task_id"): self.learning_manager.confirm_action({ "type": "reject", "task_id": suggestion["task_id"] }) # Callback if self.on_suggestion_rejected: self.on_suggestion_rejected(suggestion) self.logger.log_action({ "action": "suggestion_rejected", "suggestion_type": suggestion.get("type"), "workflow_id": suggestion.get("workflow_id"), "task_id": suggestion.get("task_id") }) return True def check_timeout(self) -> bool: """ Vérifie si la suggestion actuelle a expiré. Returns: True si timeout """ with self.suggestion_lock: if self.current_suggestion is None: return False if self.suggestion_start_time is None: return False elapsed = (datetime.now() - self.suggestion_start_time).total_seconds() if elapsed >= self.suggestion_timeout: # Timeout suggestion = self.current_suggestion self.current_suggestion = None self.suggestion_start_time = None # Callback if self.on_suggestion_timeout: self.on_suggestion_timeout(suggestion) self.logger.log_action({ "action": "suggestion_timeout", "task_id": suggestion["task_id"], "elapsed": elapsed }) return True return False def get_current_suggestion(self) -> Optional[Dict[str, Any]]: """Retourne la suggestion actuelle.""" with self.suggestion_lock: return self.current_suggestion def clear_suggestion(self): """Efface la suggestion actuelle.""" with self.suggestion_lock: self.current_suggestion = None self.suggestion_start_time = None def get_stats(self) -> Dict[str, Any]: """Retourne les statistiques du gestionnaire.""" return { "has_active_suggestion": self.current_suggestion is not None, "similarity_threshold": self.similarity_threshold, "timeout": self.suggestion_timeout, "workflow_rejections": len(self.workflow_rejections), "workflows_with_adjusted_priority": len(self.workflow_priority_adjustments) } def check_workflow_match( self, session_actions: List[Dict[str, Any]], workflows: List[Dict[str, Any]] ) -> Optional[WorkflowMatch]: """ Vérifie périodiquement si les actions courantes correspondent à un workflow connu. Cette méthode doit être appelée régulièrement (ex: toutes les 2s) en mode Assist pour détecter les correspondances de workflows. Args: session_actions: Liste des actions de la session courante workflows: Liste des workflows connus Returns: Meilleure correspondance si trouvée, None sinon """ if not session_actions or not workflows: return None # Vérifier qu'il n'y a pas déjà une suggestion active with self.suggestion_lock: if self.current_suggestion is not None: return None # Utiliser le WorkflowMatcher pour trouver les correspondances matches = self.workflow_matcher.match_current_session( session_actions, workflows ) if not matches: return None # Appliquer les ajustements de priorité basés sur les rejets adjusted_matches = [] for match in matches: adjusted_confidence = self._apply_priority_adjustment(match) # Créer un nouveau match avec la confiance ajustée adjusted_match = WorkflowMatch( workflow_id=match.workflow_id, workflow_name=match.workflow_name, confidence=adjusted_confidence, matched_steps=match.matched_steps, total_steps=match.total_steps, remaining_steps=match.remaining_steps, current_step_index=match.current_step_index ) adjusted_matches.append(adjusted_match) # Trier à nouveau par confiance ajustée adjusted_matches.sort(key=lambda m: m.confidence, reverse=True) # Trouver le meilleur match best_match = self.workflow_matcher.find_best_match(adjusted_matches) if best_match: self.logger.log_action({ "action": "workflow_match_found", "workflow_id": best_match.workflow_id, "workflow_name": best_match.workflow_name, "confidence": best_match.confidence, "matched_steps": best_match.matched_steps, "remaining_steps": len(best_match.remaining_steps) }) return best_match def create_workflow_suggestion( self, workflow_match: WorkflowMatch ) -> Optional[Dict[str, Any]]: """ Crée une suggestion de workflow avec les détails des étapes restantes. Args: workflow_match: Correspondance de workflow trouvée Returns: Suggestion créée ou None si impossible """ with self.suggestion_lock: # Vérifier qu'il n'y a pas déjà une suggestion active if self.current_suggestion is not None: return None # Vérifier le seuil de confiance if workflow_match.confidence < self.workflow_confidence_threshold: self.logger.log_action({ "action": "workflow_suggestion_rejected_low_confidence", "workflow_id": workflow_match.workflow_id, "confidence": workflow_match.confidence, "threshold": self.workflow_confidence_threshold }) return None # Créer la suggestion avec les détails des étapes suggestion = { "type": "workflow", "workflow_id": workflow_match.workflow_id, "workflow_name": workflow_match.workflow_name, "confidence": workflow_match.confidence, "current_step": workflow_match.current_step_index, "total_steps": workflow_match.total_steps, "matched_steps": workflow_match.matched_steps, "remaining_steps": workflow_match.remaining_steps, "next_steps_preview": workflow_match.remaining_steps[:3], # 3 prochaines étapes "created_at": datetime.now(), "timeout": self.suggestion_timeout } # Enregistrer la suggestion self.current_suggestion = suggestion self.suggestion_start_time = datetime.now() # Callback if self.on_suggestion_created: self.on_suggestion_created(suggestion) self.logger.log_action({ "action": "workflow_suggestion_created", "workflow_id": workflow_match.workflow_id, "workflow_name": workflow_match.workflow_name, "confidence": workflow_match.confidence, "remaining_steps": len(workflow_match.remaining_steps) }) return suggestion def _apply_priority_adjustment(self, match: WorkflowMatch) -> float: """ Applique l'ajustement de priorité basé sur les rejets précédents. Args: match: Correspondance de workflow Returns: Confiance ajustée """ workflow_id = match.workflow_id # Récupérer le multiplicateur d'ajustement adjustment = self.workflow_priority_adjustments.get(workflow_id, 1.0) # Appliquer l'ajustement adjusted_confidence = match.confidence * adjustment # S'assurer que la confiance reste dans [0, 1] adjusted_confidence = max(0.0, min(1.0, adjusted_confidence)) if adjustment != 1.0: self.logger.log_action({ "action": "priority_adjustment_applied", "workflow_id": workflow_id, "original_confidence": match.confidence, "adjusted_confidence": adjusted_confidence, "adjustment_multiplier": adjustment }) return adjusted_confidence def _track_workflow_rejection(self, workflow_id: str): """ Enregistre un rejet de workflow et ajuste la priorité si nécessaire. Après 3 rejets, la priorité du workflow est réduite (confiance * 0.9). Args: workflow_id: ID du workflow rejeté """ # Incrémenter le compteur de rejets current_rejections = self.workflow_rejections.get(workflow_id, 0) current_rejections += 1 self.workflow_rejections[workflow_id] = current_rejections self.logger.log_action({ "action": "workflow_rejection_tracked", "workflow_id": workflow_id, "total_rejections": current_rejections }) # Après 3 rejets, ajuster la priorité if current_rejections >= 3: # Réduire la confiance de 10% à chaque tranche de 3 rejets adjustment_factor = 0.9 ** (current_rejections // 3) self.workflow_priority_adjustments[workflow_id] = adjustment_factor self.logger.log_action({ "action": "workflow_priority_adjusted", "workflow_id": workflow_id, "rejections": current_rejections, "new_adjustment_factor": adjustment_factor }) def _track_workflow_acceptance(self, workflow_id: str): """ Enregistre une acceptation de workflow et améliore la priorité. Args: workflow_id: ID du workflow accepté """ # Réduire le compteur de rejets (récompenser les acceptations) current_rejections = self.workflow_rejections.get(workflow_id, 0) if current_rejections > 0: current_rejections = max(0, current_rejections - 2) # Réduire de 2 self.workflow_rejections[workflow_id] = current_rejections # Recalculer l'ajustement de priorité if current_rejections >= 3: adjustment_factor = 0.9 ** (current_rejections // 3) self.workflow_priority_adjustments[workflow_id] = adjustment_factor else: # Retirer l'ajustement si moins de 3 rejets if workflow_id in self.workflow_priority_adjustments: del self.workflow_priority_adjustments[workflow_id] self.logger.log_action({ "action": "workflow_acceptance_tracked", "workflow_id": workflow_id, "remaining_rejections": current_rejections }) def on_workflow_detected(self, workflow: Dict[str, Any]): """ Callback appelé quand un workflow est détecté. Peut créer une suggestion immédiate si le workflow est pertinent. Args: workflow: Workflow détecté """ self.logger.log_action({ "action": "workflow_detected_in_suggestion_manager", "workflow_id": workflow.get("workflow_id"), "workflow_name": workflow.get("name"), "confidence": workflow.get("confidence") }) # Pour l'instant, on log seulement # Dans le futur, on pourrait créer une suggestion proactive # basée sur le workflow détecté