909 lines
30 KiB
Python
909 lines
30 KiB
Python
"""
|
||
Gestionnaire d'apprentissage pour RPA Vision V2.
|
||
Gère les transitions de mode, le calcul de confiance et l'état d'apprentissage.
|
||
"""
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Any, Tuple
|
||
from datetime import datetime
|
||
from dataclasses import asdict
|
||
import numpy as np
|
||
|
||
from .models import TaskProfile, Action
|
||
from .embeddings_manager import EmbeddingsManager
|
||
from .logger import Logger
|
||
from .ui_change_detector import UIChangeDetector
|
||
|
||
|
||
class LearningManager:
|
||
"""
|
||
Gestionnaire d'apprentissage qui suit la progression, gère les transitions
|
||
de mode et calcule les scores de confiance.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
embeddings_manager: EmbeddingsManager,
|
||
logger: Logger,
|
||
config: Dict[str, Any],
|
||
profiles_path: str = "data/user_profiles"
|
||
):
|
||
"""
|
||
Initialise le gestionnaire d'apprentissage.
|
||
|
||
Args:
|
||
embeddings_manager: Gestionnaire d'embeddings
|
||
logger: Logger pour la journalisation
|
||
config: Configuration globale
|
||
profiles_path: Chemin vers les profils utilisateur
|
||
"""
|
||
self.embeddings_manager = embeddings_manager
|
||
self.logger = logger
|
||
self.config = config
|
||
self.profiles_path = Path(profiles_path)
|
||
|
||
# Créer le répertoire de profils
|
||
self.profiles_path.mkdir(parents=True, exist_ok=True)
|
||
|
||
# État actuel
|
||
self.mode = "shadow" # Mode initial
|
||
self.tasks: Dict[str, TaskProfile] = {}
|
||
self.current_task_id: Optional[str] = None
|
||
self.current_context: Dict[str, Any] = {}
|
||
|
||
# Seuils de configuration
|
||
self.autopilot_observations = config.get("thresholds", {}).get(
|
||
"autopilot_observations", 20
|
||
)
|
||
self.autopilot_concordance = config.get("thresholds", {}).get(
|
||
"autopilot_concordance", 0.95
|
||
)
|
||
self.confidence_min = config.get("thresholds", {}).get(
|
||
"confidence_min", 0.90
|
||
)
|
||
self.rollback_confidence = config.get("thresholds", {}).get(
|
||
"rollback_confidence", 0.85
|
||
)
|
||
|
||
# Initialiser le détecteur de changements UI
|
||
self.ui_change_detector = UIChangeDetector(
|
||
embeddings_manager,
|
||
logger,
|
||
config
|
||
)
|
||
|
||
# Charger les profils existants
|
||
self._load_profiles()
|
||
|
||
# Charger les tâches existantes dans l'index FAISS
|
||
self._load_existing_tasks_to_index()
|
||
|
||
self.logger.log_action({
|
||
"action": "learning_manager_initialized",
|
||
"mode": self.mode,
|
||
"num_tasks": len(self.tasks)
|
||
})
|
||
|
||
def _save_profile(self, task_id: str):
|
||
"""Sauvegarde un profil de tâche."""
|
||
if task_id not in self.tasks:
|
||
return
|
||
|
||
try:
|
||
profile = self.tasks[task_id]
|
||
profile_file = self.profiles_path / f"{task_id}.json"
|
||
|
||
with open(profile_file, 'w', encoding='utf-8') as f:
|
||
json.dump(profile.to_json(), f, indent=2, ensure_ascii=False)
|
||
|
||
self.logger.log_action({
|
||
"action": "profile_saved",
|
||
"task_id": task_id
|
||
})
|
||
|
||
except Exception as e:
|
||
self.logger.log_action({
|
||
"action": "profile_save_error",
|
||
"task_id": task_id,
|
||
"error": str(e)
|
||
})
|
||
|
||
def observe(self, action: Action):
|
||
"""
|
||
Enregistre une observation en mode Shadow.
|
||
|
||
Args:
|
||
action: Action observée
|
||
"""
|
||
# Obtenir ou créer le profil de tâche
|
||
task_id = self.current_task_id or self._generate_task_id(action)
|
||
|
||
if task_id not in self.tasks:
|
||
self.tasks[task_id] = TaskProfile(
|
||
task_id=task_id,
|
||
task_name=f"Tâche {task_id}",
|
||
mode="shadow",
|
||
observation_count=0,
|
||
concordance_rate=0.0,
|
||
confidence_score=0.0,
|
||
correction_count=0,
|
||
last_execution=datetime.now(),
|
||
window_whitelist=[action.window_title],
|
||
action_sequence=[],
|
||
embeddings=[],
|
||
metadata={}
|
||
)
|
||
|
||
task = self.tasks[task_id]
|
||
task.observation_count += 1
|
||
task.action_sequence.append(action)
|
||
task.last_execution = datetime.now()
|
||
|
||
# Ajouter l'embedding si disponible
|
||
if action.embedding is not None:
|
||
task.embeddings.append(action.embedding)
|
||
|
||
# Ajouter à l'index FAISS
|
||
self.embeddings_manager.add_to_index(
|
||
action.embedding,
|
||
{
|
||
"task_id": task_id,
|
||
"action_type": action.action_type,
|
||
"target_element": action.target_element,
|
||
"timestamp": action.timestamp.isoformat()
|
||
}
|
||
)
|
||
|
||
self.logger.log_action({
|
||
"action": "observation_recorded",
|
||
"task_id": task_id,
|
||
"observation_count": task.observation_count,
|
||
"action_type": action.action_type
|
||
})
|
||
|
||
# Sauvegarder le profil
|
||
self._save_profile(task_id)
|
||
|
||
# Vérifier si on peut passer en mode Assisté
|
||
if task.observation_count >= 5 and task.mode == "shadow":
|
||
self._transition_mode(task_id, "assist")
|
||
|
||
def _generate_task_id(self, action: Action) -> str:
|
||
"""Génère un ID de tâche basé sur l'action."""
|
||
window_clean = action.window_title.replace(" ", "_").lower()
|
||
element_clean = action.target_element.replace(" ", "_").lower()
|
||
return f"{window_clean}_{element_clean}_{action.action_type}"
|
||
|
||
def suggest_action(self, context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
Génère une suggestion d'action en mode Assisté.
|
||
|
||
Args:
|
||
context: Contexte actuel (fenêtre, capture d'écran, etc.)
|
||
|
||
Returns:
|
||
Suggestion d'action ou None
|
||
"""
|
||
if not self.current_task_id or self.current_task_id not in self.tasks:
|
||
return None
|
||
|
||
task = self.tasks[self.current_task_id]
|
||
|
||
if task.mode != "assist" and task.mode != "auto":
|
||
return None
|
||
|
||
# Trouver l'action la plus similaire dans l'historique
|
||
if not task.embeddings or 'current_embedding' not in context:
|
||
return None
|
||
|
||
current_emb = context['current_embedding']
|
||
|
||
# Rechercher les embeddings similaires
|
||
similar = self.embeddings_manager.search_similar(current_emb, k=3)
|
||
|
||
if not similar:
|
||
return None
|
||
|
||
# Prendre le plus similaire
|
||
best_match = similar[0]
|
||
similarity = best_match['similarity']
|
||
|
||
# Trouver l'action correspondante
|
||
matching_action = None
|
||
for action in task.action_sequence:
|
||
if (action.timestamp.isoformat() ==
|
||
best_match['metadata'].get('timestamp')):
|
||
matching_action = action
|
||
break
|
||
|
||
if not matching_action:
|
||
return None
|
||
|
||
# Calculer la confiance
|
||
vision_conf = similarity
|
||
llm_score = context.get('llm_score', 0.5)
|
||
confidence = self.calculate_confidence(vision_conf, llm_score, self.current_task_id)
|
||
|
||
suggestion = {
|
||
"action": matching_action,
|
||
"confidence": confidence,
|
||
"similarity": similarity,
|
||
"task_id": self.current_task_id
|
||
}
|
||
|
||
self.logger.log_action({
|
||
"action": "suggestion_generated",
|
||
"task_id": self.current_task_id,
|
||
"confidence": confidence,
|
||
"action_type": matching_action.action_type
|
||
})
|
||
|
||
return suggestion
|
||
|
||
def confirm_action(self, feedback: Dict[str, Any]):
|
||
"""
|
||
Traite la validation ou correction utilisateur.
|
||
|
||
Args:
|
||
feedback: Dictionnaire avec type (accept/reject/correct) et données
|
||
"""
|
||
feedback_type = feedback.get("type")
|
||
task_id = feedback.get("task_id", self.current_task_id)
|
||
|
||
if not task_id or task_id not in self.tasks:
|
||
return
|
||
|
||
task = self.tasks[task_id]
|
||
|
||
if feedback_type == "accept":
|
||
# Action acceptée, augmenter la concordance
|
||
self._update_concordance(task_id, success=True)
|
||
|
||
self.logger.log_action({
|
||
"action": "action_accepted",
|
||
"task_id": task_id,
|
||
"concordance_rate": task.concordance_rate
|
||
})
|
||
|
||
elif feedback_type == "reject":
|
||
# Action rejetée
|
||
self._update_concordance(task_id, success=False)
|
||
|
||
self.logger.log_action({
|
||
"action": "action_rejected",
|
||
"task_id": task_id,
|
||
"concordance_rate": task.concordance_rate
|
||
})
|
||
|
||
elif feedback_type == "correct":
|
||
# Correction fournie
|
||
task.correction_count += 1
|
||
corrected_action = feedback.get("corrected_action")
|
||
|
||
if corrected_action:
|
||
# Ajouter la correction à la séquence
|
||
task.action_sequence.append(corrected_action)
|
||
|
||
# Mettre à jour les embeddings
|
||
if corrected_action.embedding is not None:
|
||
task.embeddings.append(corrected_action.embedding)
|
||
self.embeddings_manager.add_to_index(
|
||
corrected_action.embedding,
|
||
{
|
||
"task_id": task_id,
|
||
"action_type": corrected_action.action_type,
|
||
"target_element": corrected_action.target_element,
|
||
"timestamp": corrected_action.timestamp.isoformat(),
|
||
"is_correction": True
|
||
}
|
||
)
|
||
|
||
self._update_concordance(task_id, success=False)
|
||
|
||
self.logger.log_correction({
|
||
"task_id": task_id,
|
||
"correction_count": task.correction_count,
|
||
"corrected_action": corrected_action.to_dict() if corrected_action else None
|
||
})
|
||
|
||
# Sauvegarder le profil
|
||
self._save_profile(task_id)
|
||
|
||
# Vérifier les transitions de mode
|
||
self._check_mode_transitions(task_id)
|
||
|
||
def _update_concordance(self, task_id: str, success: bool):
|
||
"""Met à jour le taux de concordance."""
|
||
if task_id not in self.tasks:
|
||
return
|
||
|
||
task = self.tasks[task_id]
|
||
|
||
# Utiliser une moyenne mobile sur les 10 dernières exécutions
|
||
if not hasattr(task, '_recent_results'):
|
||
task.metadata['recent_results'] = []
|
||
|
||
recent_results = task.metadata.get('recent_results', [])
|
||
recent_results.append(1 if success else 0)
|
||
|
||
# Garder seulement les 10 derniers
|
||
if len(recent_results) > 10:
|
||
recent_results = recent_results[-10:]
|
||
|
||
task.metadata['recent_results'] = recent_results
|
||
task.concordance_rate = sum(recent_results) / len(recent_results)
|
||
|
||
def calculate_confidence(
|
||
self,
|
||
vision_conf: float,
|
||
llm_score: float,
|
||
task_id: str
|
||
) -> float:
|
||
"""
|
||
Calcule le score de confiance pondéré.
|
||
|
||
Args:
|
||
vision_conf: Confiance de la détection vision (0-1)
|
||
llm_score: Score du LLM (0-1)
|
||
task_id: ID de la tâche
|
||
|
||
Returns:
|
||
Score de confiance (0-1)
|
||
"""
|
||
# Obtenir la performance historique
|
||
history_score = self._get_historical_performance(task_id)
|
||
|
||
# Formule : 0.6 × vision + 0.3 × llm + 0.1 × historique
|
||
confidence = (
|
||
0.6 * vision_conf +
|
||
0.3 * llm_score +
|
||
0.1 * history_score
|
||
)
|
||
|
||
return max(0.0, min(1.0, confidence))
|
||
|
||
def _get_historical_performance(self, task_id: str) -> float:
|
||
"""Obtient la performance historique d'une tâche."""
|
||
if task_id not in self.tasks:
|
||
return 0.5
|
||
|
||
task = self.tasks[task_id]
|
||
return task.concordance_rate
|
||
|
||
def evaluate_task(self, task_id: str) -> Dict[str, Any]:
|
||
"""
|
||
Évalue une tâche et retourne ses métriques.
|
||
|
||
Args:
|
||
task_id: ID de la tâche
|
||
|
||
Returns:
|
||
Dictionnaire de métriques
|
||
"""
|
||
if task_id not in self.tasks:
|
||
return {}
|
||
|
||
task = self.tasks[task_id]
|
||
|
||
return {
|
||
"task_id": task_id,
|
||
"task_name": task.task_name,
|
||
"mode": task.mode,
|
||
"observation_count": task.observation_count,
|
||
"concordance_rate": task.concordance_rate,
|
||
"confidence_score": task.confidence_score,
|
||
"correction_count": task.correction_count,
|
||
"correction_rate": (
|
||
task.correction_count / max(1, task.observation_count)
|
||
),
|
||
"last_execution": task.last_execution.isoformat() if task.last_execution else None
|
||
}
|
||
|
||
def should_transition_to_auto(self, task_id: str) -> bool:
|
||
"""
|
||
Vérifie si une tâche remplit les critères pour passer en Autopilot.
|
||
|
||
Args:
|
||
task_id: ID de la tâche
|
||
|
||
Returns:
|
||
True si éligible, False sinon
|
||
"""
|
||
if task_id not in self.tasks:
|
||
return False
|
||
|
||
task = self.tasks[task_id]
|
||
|
||
return (
|
||
task.observation_count >= self.autopilot_observations and
|
||
task.concordance_rate >= self.autopilot_concordance
|
||
)
|
||
|
||
def rollback_if_low_confidence(self, task_id: str):
|
||
"""
|
||
Rétrograde une tâche au mode Assisté si la confiance est faible.
|
||
|
||
Args:
|
||
task_id: ID de la tâche
|
||
"""
|
||
if task_id not in self.tasks:
|
||
return
|
||
|
||
task = self.tasks[task_id]
|
||
|
||
if task.mode == "auto" and task.confidence_score < self.confidence_min:
|
||
self._transition_mode(task_id, "assist")
|
||
|
||
self.logger.log_mode_transition(
|
||
task_id,
|
||
"auto",
|
||
"assist",
|
||
f"Confiance faible: {task.confidence_score:.2f}"
|
||
)
|
||
|
||
def _check_mode_transitions(self, task_id: str):
|
||
"""Vérifie et effectue les transitions de mode si nécessaire."""
|
||
if task_id not in self.tasks:
|
||
return
|
||
|
||
task = self.tasks[task_id]
|
||
|
||
# Shadow → Assist (après 5 observations)
|
||
if task.mode == "shadow" and task.observation_count >= 5:
|
||
self._transition_mode(task_id, "assist")
|
||
|
||
# Assist → Auto (si critères remplis)
|
||
elif task.mode == "assist" and self.should_transition_to_auto(task_id):
|
||
self._transition_mode(task_id, "auto")
|
||
|
||
# Auto → Assist (si confiance faible)
|
||
elif task.mode == "auto" and task.concordance_rate < self.rollback_confidence:
|
||
self._transition_mode(task_id, "assist")
|
||
|
||
def _transition_mode(self, task_id: str, new_mode: str):
|
||
"""Effectue une transition de mode."""
|
||
if task_id not in self.tasks:
|
||
return
|
||
|
||
task = self.tasks[task_id]
|
||
old_mode = task.mode
|
||
|
||
if old_mode == new_mode:
|
||
return
|
||
|
||
task.mode = new_mode
|
||
|
||
self.logger.log_mode_transition(
|
||
task_id,
|
||
old_mode,
|
||
new_mode,
|
||
f"Observations: {task.observation_count}, Concordance: {task.concordance_rate:.2%}"
|
||
)
|
||
|
||
self._save_profile(task_id)
|
||
|
||
def get_mode(self) -> str:
|
||
"""Retourne le mode opérationnel actuel."""
|
||
if self.current_task_id and self.current_task_id in self.tasks:
|
||
return self.tasks[self.current_task_id].mode
|
||
return self.mode
|
||
|
||
def get_current_intent(self) -> str:
|
||
"""Retourne l'intention actuelle."""
|
||
return self.current_context.get("intent", "")
|
||
|
||
def set_current_intent(self, intent: str):
|
||
"""
|
||
Définit l'intention actuelle.
|
||
|
||
Args:
|
||
intent: Intention à définir (ex: "button", "text field", "form")
|
||
"""
|
||
self.current_context["intent"] = intent
|
||
|
||
self.logger.log_action({
|
||
"action": "intent_set",
|
||
"intent": intent,
|
||
"mode": self.mode
|
||
})
|
||
|
||
def create_task_from_signatures(
|
||
self,
|
||
signatures: List[Dict[str, Any]],
|
||
description: str = "Tâche automatique"
|
||
) -> TaskProfile:
|
||
"""
|
||
Crée une tâche à partir de signatures visuelles.
|
||
|
||
Args:
|
||
signatures: Liste de signatures d'actions
|
||
description: Description de la tâche
|
||
|
||
Returns:
|
||
TaskProfile créé
|
||
"""
|
||
import hashlib
|
||
import json
|
||
|
||
# Générer un ID unique
|
||
task_id = hashlib.md5(
|
||
json.dumps(str(signatures[0])).encode()
|
||
).hexdigest()[:8]
|
||
|
||
# Créer le profil de tâche
|
||
task = TaskProfile(
|
||
task_id=f"task_{task_id}",
|
||
task_name=description,
|
||
window_whitelist=[signatures[0].get("window", "Unknown")],
|
||
observation_count=len(signatures),
|
||
embeddings=[sig.get("embedding") for sig in signatures if sig.get("embedding") is not None],
|
||
metadata={"signatures": signatures} # Stocker les signatures dans metadata
|
||
)
|
||
|
||
# Ajouter aux tâches
|
||
self.tasks[task.task_id] = task
|
||
|
||
# Sauvegarder
|
||
self._save_task(task)
|
||
|
||
self.logger.log_action({
|
||
"action": "task_created",
|
||
"task_id": task.task_id,
|
||
"signatures_count": len(signatures)
|
||
})
|
||
|
||
return task
|
||
|
||
def _save_task(self, task: TaskProfile):
|
||
"""Sauvegarde une tâche sur disque."""
|
||
import json
|
||
import pickle
|
||
from pathlib import Path
|
||
|
||
task_dir = self.profiles_path / task.task_id
|
||
task_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# Sauvegarder les métadonnées
|
||
metadata = {
|
||
"task_id": task.task_id,
|
||
"task_name": task.task_name,
|
||
"window_whitelist": task.window_whitelist,
|
||
"observation_count": task.observation_count,
|
||
"mode": task.mode,
|
||
"confidence_score": task.confidence_score
|
||
}
|
||
|
||
with open(task_dir / "metadata.json", "w") as f:
|
||
json.dump(metadata, f, indent=2)
|
||
|
||
# Sauvegarder les signatures (depuis metadata)
|
||
if "signatures" in task.metadata:
|
||
with open(task_dir / "signatures.pkl", "wb") as f:
|
||
pickle.dump(task.metadata["signatures"], f)
|
||
|
||
# Sauvegarder l'index FAISS automatiquement (MVP)
|
||
try:
|
||
self.embeddings_manager.save_index()
|
||
self.logger.log_action({
|
||
"action": "faiss_index_saved",
|
||
"task_id": task.task_id
|
||
})
|
||
except Exception as e:
|
||
self.logger.log_action({
|
||
"action": "faiss_index_save_error",
|
||
"task_id": task.task_id,
|
||
"error": str(e)
|
||
})
|
||
|
||
def load_task(self, task_id: str) -> Optional[TaskProfile]:
|
||
"""Charge une tâche depuis le disque."""
|
||
import json
|
||
import pickle
|
||
|
||
task_dir = self.profiles_path / task_id
|
||
|
||
if not task_dir.exists():
|
||
return None
|
||
|
||
try:
|
||
# Charger métadonnées
|
||
with open(task_dir / "metadata.json", "r") as f:
|
||
metadata = json.load(f)
|
||
|
||
task = TaskProfile(
|
||
task_id=metadata["task_id"],
|
||
task_name=metadata.get("task_name", metadata.get("description", "Unknown")),
|
||
window_whitelist=metadata.get("window_whitelist", [metadata.get("window_title", "Unknown")]),
|
||
observation_count=metadata.get("observation_count", metadata.get("observations", 0)),
|
||
mode=metadata.get("mode", "shadow"),
|
||
confidence_score=metadata.get("confidence_score", 0.0)
|
||
)
|
||
|
||
# Charger signatures dans metadata
|
||
sig_file = task_dir / "signatures.pkl"
|
||
if sig_file.exists():
|
||
with open(sig_file, "rb") as f:
|
||
task.metadata["signatures"] = pickle.load(f)
|
||
|
||
return task
|
||
|
||
except Exception as e:
|
||
self.logger.log_action({
|
||
"action": "task_load_failed",
|
||
"task_id": task_id,
|
||
"error": str(e)
|
||
})
|
||
return None
|
||
|
||
def set_current_task(self, task_id: str):
|
||
"""Définit la tâche actuelle."""
|
||
self.current_task_id = task_id
|
||
|
||
def set_current_context(self, context: Dict[str, Any]):
|
||
"""Définit le contexte actuel."""
|
||
self.current_context = context
|
||
|
||
def record_execution(self, decision: Dict[str, Any]):
|
||
"""
|
||
Enregistre l'exécution d'une action.
|
||
|
||
Args:
|
||
decision: Dictionnaire décrivant la décision et l'action
|
||
"""
|
||
task_id = decision.get("task_id", self.current_task_id)
|
||
|
||
if not task_id or task_id not in self.tasks:
|
||
return
|
||
|
||
task = self.tasks[task_id]
|
||
task.last_execution = datetime.now()
|
||
|
||
# Mettre à jour le score de confiance
|
||
if "confidence" in decision:
|
||
task.confidence_score = decision["confidence"]
|
||
|
||
self.logger.log_action({
|
||
"action": "execution_recorded",
|
||
"task_id": task_id,
|
||
"confidence": task.confidence_score,
|
||
"mode": task.mode
|
||
})
|
||
|
||
self._save_profile(task_id)
|
||
|
||
def get_all_tasks(self) -> List[Dict[str, Any]]:
|
||
"""Retourne toutes les tâches avec leurs métriques."""
|
||
return [self.evaluate_task(task_id) for task_id in self.tasks.keys()]
|
||
|
||
def get_task_stats(self) -> Dict[str, Any]:
|
||
"""Retourne des statistiques globales."""
|
||
total_tasks = len(self.tasks)
|
||
shadow_tasks = sum(1 for t in self.tasks.values() if t.mode == "shadow")
|
||
assist_tasks = sum(1 for t in self.tasks.values() if t.mode == "assist")
|
||
auto_tasks = sum(1 for t in self.tasks.values() if t.mode == "auto")
|
||
|
||
return {
|
||
"total_tasks": total_tasks,
|
||
"shadow_tasks": shadow_tasks,
|
||
"assist_tasks": assist_tasks,
|
||
"auto_tasks": auto_tasks,
|
||
"current_mode": self.get_mode()
|
||
}
|
||
|
||
def check_ui_changes(
|
||
self,
|
||
task_id: str,
|
||
current_embedding: np.ndarray,
|
||
predicted_bbox: Optional[Tuple[int, int, int, int]] = None,
|
||
actual_bbox: Optional[Tuple[int, int, int, int]] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Vérifie les changements UI pour une tâche et déclenche le ré-entraînement si nécessaire.
|
||
|
||
Args:
|
||
task_id: ID de la tâche
|
||
current_embedding: Embedding visuel actuel
|
||
predicted_bbox: Bbox prédite (optionnel)
|
||
actual_bbox: Bbox réelle (optionnel)
|
||
|
||
Returns:
|
||
Résultats de la vérification
|
||
"""
|
||
if task_id not in self.tasks:
|
||
return {
|
||
"error": "task_not_found",
|
||
"task_id": task_id
|
||
}
|
||
|
||
task = self.tasks[task_id]
|
||
|
||
# Vérifier les changements avec le détecteur
|
||
result = self.ui_change_detector.check_and_trigger_retraining(
|
||
task_id,
|
||
current_embedding,
|
||
task.embeddings,
|
||
predicted_bbox,
|
||
actual_bbox
|
||
)
|
||
|
||
# Si ré-entraînement déclenché, mettre à jour le profil
|
||
if result.get("retraining_triggered"):
|
||
# Ajouter l'embedding actuel pour améliorer la détection future
|
||
task.embeddings.append(current_embedding)
|
||
|
||
# Ajouter à l'index FAISS
|
||
self.embeddings_manager.add_to_index(
|
||
current_embedding,
|
||
{
|
||
"task_id": task_id,
|
||
"timestamp": datetime.now().isoformat(),
|
||
"is_retraining": True
|
||
}
|
||
)
|
||
|
||
# Sauvegarder le profil
|
||
self._save_profile(task_id)
|
||
|
||
# Logger l'événement
|
||
self.logger.log_action({
|
||
"action": "ui_change_retraining",
|
||
"task_id": task_id,
|
||
"similarity": result.get("similarity"),
|
||
"deltas": result.get("deltas"),
|
||
"reason": "ui_drift_detected"
|
||
})
|
||
|
||
return result
|
||
|
||
def monitor_execution_drift(
|
||
self,
|
||
task_id: str,
|
||
predicted_action: Action,
|
||
actual_action: Action
|
||
) -> bool:
|
||
"""
|
||
Surveille la dérive entre l'action prédite et l'action réelle.
|
||
|
||
Args:
|
||
task_id: ID de la tâche
|
||
predicted_action: Action prédite par le système
|
||
actual_action: Action réelle effectuée/validée
|
||
|
||
Returns:
|
||
True si dérive détectée et ré-entraînement déclenché
|
||
"""
|
||
if task_id not in self.tasks:
|
||
return False
|
||
|
||
# Vérifier les changements UI
|
||
result = self.check_ui_changes(
|
||
task_id,
|
||
actual_action.embedding,
|
||
predicted_action.bbox,
|
||
actual_action.bbox
|
||
)
|
||
|
||
return result.get("retraining_triggered", False)
|
||
|
||
def get_ui_change_stats(self) -> Dict[str, Any]:
|
||
"""
|
||
Retourne les statistiques de détection de changements UI.
|
||
|
||
Returns:
|
||
Statistiques du détecteur de changements
|
||
"""
|
||
return self.ui_change_detector.get_stats()
|
||
|
||
def _load_profiles(self):
|
||
"""Charge les profils de tâches existants."""
|
||
if not self.profiles_path.exists():
|
||
return
|
||
|
||
for profile_file in self.profiles_path.glob("*.json"):
|
||
try:
|
||
with open(profile_file, 'r', encoding='utf-8') as f:
|
||
json_str = f.read()
|
||
|
||
# Charger le profil (sans embeddings pour l'instant)
|
||
task = TaskProfile.from_json(json_str)
|
||
self.tasks[task.task_id] = task
|
||
|
||
self.logger.log_action({
|
||
"action": "profile_loaded",
|
||
"task_id": task.task_id,
|
||
"mode": task.mode
|
||
})
|
||
|
||
except Exception as e:
|
||
self.logger.log_action({
|
||
"action": "profile_load_error",
|
||
"file": str(profile_file),
|
||
"error": str(e)
|
||
})
|
||
|
||
def _load_existing_tasks_to_index(self):
|
||
"""
|
||
Charge les tâches existantes dans l'index FAISS au démarrage.
|
||
Résout le problème : index vide malgré 40 tâches sauvegardées.
|
||
"""
|
||
from .faiss_index_builder import FAISSIndexBuilder
|
||
|
||
self.logger.log_action({
|
||
"action": "load_existing_tasks_started"
|
||
})
|
||
|
||
try:
|
||
# Créer le builder
|
||
builder = FAISSIndexBuilder(
|
||
self.embeddings_manager,
|
||
self.logger,
|
||
str(self.profiles_path)
|
||
)
|
||
|
||
# Vérifier l'intégrité de l'index
|
||
report = builder.verify_index_integrity()
|
||
|
||
self.logger.log_action({
|
||
"action": "index_integrity_check",
|
||
"report": report
|
||
})
|
||
|
||
# Si l'index est vide ou incohérent, le reconstruire
|
||
if report['needs_rebuild']:
|
||
self.logger.log_action({
|
||
"action": "rebuilding_index_automatically",
|
||
"reason": "index_empty_or_inconsistent"
|
||
})
|
||
|
||
stats = builder.rebuild_index(force=True)
|
||
|
||
self.logger.log_action({
|
||
"action": "index_rebuilt_automatically",
|
||
"stats": stats
|
||
})
|
||
|
||
if stats['success']:
|
||
self.logger.log_action({
|
||
"action": "load_existing_tasks_success",
|
||
"embeddings_loaded": stats['embeddings_added'],
|
||
"tasks_processed": stats['tasks_processed']
|
||
})
|
||
else:
|
||
self.logger.log_action({
|
||
"action": "load_existing_tasks_failed",
|
||
"reason": "rebuild_failed"
|
||
})
|
||
else:
|
||
self.logger.log_action({
|
||
"action": "load_existing_tasks_skipped",
|
||
"reason": "index_already_consistent",
|
||
"index_size": report['actual_embeddings']
|
||
})
|
||
|
||
# Charger les tâches dans self.tasks si pas déjà fait
|
||
tasks_info = builder.scan_tasks()
|
||
for task_info in tasks_info:
|
||
task_id = task_info['task_id']
|
||
if task_id not in self.tasks:
|
||
# Charger la tâche
|
||
task = self.load_task(task_id)
|
||
if task:
|
||
self.tasks[task_id] = task
|
||
self.logger.log_action({
|
||
"action": "task_loaded_from_disk",
|
||
"task_id": task_id
|
||
})
|
||
|
||
except Exception as e:
|
||
self.logger.log_action({
|
||
"action": "load_existing_tasks_error",
|
||
"error": str(e)
|
||
})
|
||
# Ne pas bloquer le démarrage si erreur
|
||
import traceback
|
||
self.logger.log_action({
|
||
"action": "load_existing_tasks_traceback",
|
||
"traceback": traceback.format_exc()
|
||
})
|