v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
5
core/analytics/realtime/__init__.py
Normal file
5
core/analytics/realtime/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Real-time analytics components."""
|
||||
|
||||
from .realtime_analytics import RealtimeAnalytics
|
||||
|
||||
__all__ = ['RealtimeAnalytics']
|
||||
283
core/analytics/realtime/realtime_analytics.py
Normal file
283
core/analytics/realtime/realtime_analytics.py
Normal file
@@ -0,0 +1,283 @@
|
||||
"""Real-time analytics for active workflows."""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Dict, Any, Optional, List, Callable
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from ..collection.metrics_collector import MetricsCollector, ExecutionMetrics
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LiveExecution:
|
||||
"""Live execution tracking."""
|
||||
execution_id: str
|
||||
workflow_id: str
|
||||
started_at: datetime
|
||||
current_step: int = 0
|
||||
total_steps: int = 0
|
||||
steps_completed: int = 0
|
||||
steps_failed: int = 0
|
||||
current_node_id: Optional[str] = None
|
||||
last_update: datetime = field(default_factory=datetime.now)
|
||||
|
||||
@property
|
||||
def progress_percent(self) -> float:
|
||||
"""Calculate progress percentage."""
|
||||
if self.total_steps == 0:
|
||||
return 0.0
|
||||
return (self.steps_completed / self.total_steps) * 100
|
||||
|
||||
@property
|
||||
def estimated_completion(self) -> Optional[datetime]:
|
||||
"""Estimate completion time."""
|
||||
if self.steps_completed == 0 or self.total_steps == 0:
|
||||
return None
|
||||
|
||||
elapsed = (datetime.now() - self.started_at).total_seconds()
|
||||
avg_time_per_step = elapsed / self.steps_completed
|
||||
remaining_steps = self.total_steps - self.steps_completed
|
||||
estimated_remaining = avg_time_per_step * remaining_steps
|
||||
|
||||
from datetime import timedelta
|
||||
return datetime.now() + timedelta(seconds=estimated_remaining)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
'execution_id': self.execution_id,
|
||||
'workflow_id': self.workflow_id,
|
||||
'started_at': self.started_at.isoformat(),
|
||||
'current_step': self.current_step,
|
||||
'total_steps': self.total_steps,
|
||||
'steps_completed': self.steps_completed,
|
||||
'steps_failed': self.steps_failed,
|
||||
'current_node_id': self.current_node_id,
|
||||
'progress_percent': self.progress_percent,
|
||||
'estimated_completion': self.estimated_completion.isoformat() if self.estimated_completion else None,
|
||||
'last_update': self.last_update.isoformat()
|
||||
}
|
||||
|
||||
|
||||
class RealtimeAnalytics:
|
||||
"""Real-time analytics for active workflows."""
|
||||
|
||||
def __init__(self, metrics_collector: Optional[MetricsCollector] = None):
|
||||
"""
|
||||
Initialize real-time analytics.
|
||||
|
||||
Args:
|
||||
metrics_collector: Metrics collector instance
|
||||
"""
|
||||
self.collector = metrics_collector
|
||||
self.active_executions: Dict[str, LiveExecution] = {}
|
||||
self.subscribers: Dict[str, List[Callable]] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
logger.info("RealtimeAnalytics initialized")
|
||||
|
||||
def track_execution(
|
||||
self,
|
||||
execution_id: str,
|
||||
workflow_id: str,
|
||||
total_steps: int = 0
|
||||
) -> None:
|
||||
"""
|
||||
Start tracking an execution in real-time.
|
||||
|
||||
Args:
|
||||
execution_id: Execution identifier
|
||||
workflow_id: Workflow identifier
|
||||
total_steps: Total number of steps
|
||||
"""
|
||||
with self._lock:
|
||||
self.active_executions[execution_id] = LiveExecution(
|
||||
execution_id=execution_id,
|
||||
workflow_id=workflow_id,
|
||||
started_at=datetime.now(),
|
||||
total_steps=total_steps
|
||||
)
|
||||
|
||||
# Notify subscribers
|
||||
self._notify_subscribers(execution_id, 'started')
|
||||
|
||||
logger.info(f"Tracking execution: {execution_id}")
|
||||
|
||||
def update_progress(
|
||||
self,
|
||||
execution_id: str,
|
||||
current_step: int,
|
||||
total_steps: Optional[int] = None,
|
||||
current_node_id: Optional[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Update execution progress.
|
||||
|
||||
Args:
|
||||
execution_id: Execution identifier
|
||||
current_step: Current step number
|
||||
total_steps: Total steps (updates if provided)
|
||||
current_node_id: Current node ID
|
||||
"""
|
||||
with self._lock:
|
||||
if execution_id not in self.active_executions:
|
||||
logger.warning(f"Execution not tracked: {execution_id}")
|
||||
return
|
||||
|
||||
execution = self.active_executions[execution_id]
|
||||
execution.current_step = current_step
|
||||
if total_steps is not None:
|
||||
execution.total_steps = total_steps
|
||||
if current_node_id is not None:
|
||||
execution.current_node_id = current_node_id
|
||||
execution.last_update = datetime.now()
|
||||
|
||||
# Notify subscribers
|
||||
self._notify_subscribers(execution_id, 'progress')
|
||||
|
||||
def record_step_complete(
|
||||
self,
|
||||
execution_id: str,
|
||||
success: bool
|
||||
) -> None:
|
||||
"""
|
||||
Record step completion.
|
||||
|
||||
Args:
|
||||
execution_id: Execution identifier
|
||||
success: Whether step succeeded
|
||||
"""
|
||||
with self._lock:
|
||||
if execution_id not in self.active_executions:
|
||||
return
|
||||
|
||||
execution = self.active_executions[execution_id]
|
||||
if success:
|
||||
execution.steps_completed += 1
|
||||
else:
|
||||
execution.steps_failed += 1
|
||||
execution.last_update = datetime.now()
|
||||
|
||||
# Notify subscribers
|
||||
self._notify_subscribers(execution_id, 'step_complete')
|
||||
|
||||
def complete_execution(
|
||||
self,
|
||||
execution_id: str,
|
||||
status: str
|
||||
) -> None:
|
||||
"""
|
||||
Mark execution as complete.
|
||||
|
||||
Args:
|
||||
execution_id: Execution identifier
|
||||
status: Final status
|
||||
"""
|
||||
with self._lock:
|
||||
if execution_id in self.active_executions:
|
||||
del self.active_executions[execution_id]
|
||||
|
||||
# Notify subscribers
|
||||
self._notify_subscribers(execution_id, 'completed', {'status': status})
|
||||
|
||||
logger.info(f"Execution completed: {execution_id} ({status})")
|
||||
|
||||
def get_live_metrics(self, execution_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get live metrics for an execution.
|
||||
|
||||
Args:
|
||||
execution_id: Execution identifier
|
||||
|
||||
Returns:
|
||||
Live metrics dictionary or None
|
||||
"""
|
||||
with self._lock:
|
||||
execution = self.active_executions.get(execution_id)
|
||||
if not execution:
|
||||
return None
|
||||
return execution.to_dict()
|
||||
|
||||
def get_all_active(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all active executions.
|
||||
|
||||
Returns:
|
||||
List of active execution metrics
|
||||
"""
|
||||
with self._lock:
|
||||
return [e.to_dict() for e in self.active_executions.values()]
|
||||
|
||||
def subscribe(
|
||||
self,
|
||||
execution_id: str,
|
||||
callback: Callable[[str, Dict], None]
|
||||
) -> None:
|
||||
"""
|
||||
Subscribe to real-time updates for an execution.
|
||||
|
||||
Args:
|
||||
execution_id: Execution identifier
|
||||
callback: Callback function (event_type, data)
|
||||
"""
|
||||
with self._lock:
|
||||
if execution_id not in self.subscribers:
|
||||
self.subscribers[execution_id] = []
|
||||
self.subscribers[execution_id].append(callback)
|
||||
|
||||
logger.debug(f"Subscriber added for {execution_id}")
|
||||
|
||||
def unsubscribe(
|
||||
self,
|
||||
execution_id: str,
|
||||
callback: Optional[Callable] = None
|
||||
) -> None:
|
||||
"""
|
||||
Unsubscribe from updates.
|
||||
|
||||
Args:
|
||||
execution_id: Execution identifier
|
||||
callback: Specific callback to remove (None = remove all)
|
||||
"""
|
||||
with self._lock:
|
||||
if execution_id not in self.subscribers:
|
||||
return
|
||||
|
||||
if callback is None:
|
||||
del self.subscribers[execution_id]
|
||||
else:
|
||||
self.subscribers[execution_id] = [
|
||||
cb for cb in self.subscribers[execution_id] if cb != callback
|
||||
]
|
||||
|
||||
def _notify_subscribers(
|
||||
self,
|
||||
execution_id: str,
|
||||
event_type: str,
|
||||
data: Optional[Dict] = None
|
||||
) -> None:
|
||||
"""Notify subscribers of an event."""
|
||||
with self._lock:
|
||||
callbacks = self.subscribers.get(execution_id, []).copy()
|
||||
|
||||
if not callbacks:
|
||||
return
|
||||
|
||||
# Get current metrics
|
||||
metrics = self.get_live_metrics(execution_id)
|
||||
event_data = {
|
||||
'event_type': event_type,
|
||||
'execution_id': execution_id,
|
||||
'metrics': metrics,
|
||||
**(data or {})
|
||||
}
|
||||
|
||||
# Call subscribers (outside lock)
|
||||
for callback in callbacks:
|
||||
try:
|
||||
callback(event_type, event_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Subscriber callback error: {e}")
|
||||
Reference in New Issue
Block a user