Files
Geniusia_v2/geniusia2/core/suggestion_manager.py
2026-03-05 00:20:25 +01:00

697 lines
25 KiB
Python

"""
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é