omnimancer-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,771 @@
1
+ """
2
+ Performance Metrics Collection System for Omnimancer Agent Monitoring.
3
+
4
+ This module provides comprehensive metrics collection for agent operations,
5
+ including timing, resource usage, success rates, and operational efficiency.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import statistics
11
+ import threading
12
+ import time
13
+ from collections import deque
14
+ from dataclasses import asdict, dataclass, field
15
+ from datetime import datetime, timedelta
16
+ from enum import Enum
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional, Tuple
19
+
20
+ import psutil
21
+
22
+ from .persona import get_persona_manager
23
+ from .status_core import (
24
+ AgentEvent,
25
+ EventType,
26
+ OperationType,
27
+ )
28
+ from .status_manager import (
29
+ get_status_manager,
30
+ )
31
+ from .token_tracker import (
32
+ get_token_tracker,
33
+ )
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class MetricCategory(Enum):
39
+ """Categories of metrics being collected."""
40
+
41
+ PERFORMANCE = "performance"
42
+ RELIABILITY = "reliability"
43
+ EFFICIENCY = "efficiency"
44
+ RESOURCE = "resource"
45
+ COST = "cost"
46
+ USER_EXPERIENCE = "user_experience"
47
+
48
+
49
+ class AggregationMethod(Enum):
50
+ """Methods for aggregating metrics."""
51
+
52
+ SUM = "sum"
53
+ AVERAGE = "average"
54
+ MEDIAN = "median"
55
+ MIN = "min"
56
+ MAX = "max"
57
+ COUNT = "count"
58
+ RATE = "rate" # per minute/hour/day
59
+
60
+
61
+ @dataclass
62
+ class MetricValue:
63
+ """Individual metric measurement."""
64
+
65
+ name: str
66
+ value: float
67
+ unit: str = ""
68
+ category: MetricCategory = MetricCategory.PERFORMANCE
69
+ timestamp: datetime = field(default_factory=datetime.now)
70
+ labels: Dict[str, str] = field(
71
+ default_factory=dict
72
+ ) # For grouping (persona, provider, etc.)
73
+ metadata: Dict[str, Any] = field(default_factory=dict)
74
+
75
+
76
+ @dataclass
77
+ class AggregatedMetric:
78
+ """Aggregated metric over time period."""
79
+
80
+ name: str
81
+ value: float
82
+ unit: str
83
+ aggregation_method: AggregationMethod
84
+ time_window: timedelta
85
+ sample_count: int
86
+ timestamp: datetime = field(default_factory=datetime.now)
87
+ labels: Dict[str, str] = field(default_factory=dict)
88
+
89
+ # Statistical information
90
+ min_value: Optional[float] = None
91
+ max_value: Optional[float] = None
92
+ std_deviation: Optional[float] = None
93
+
94
+
95
+ @dataclass
96
+ class PerformanceBaseline:
97
+ """Performance baseline for comparison."""
98
+
99
+ metric_name: str
100
+ baseline_value: float
101
+ confidence_interval: Tuple[float, float] # (lower, upper)
102
+ measurement_period: timedelta
103
+ sample_size: int
104
+ established_date: datetime = field(default_factory=datetime.now)
105
+ labels: Dict[str, str] = field(default_factory=dict)
106
+
107
+
108
+ @dataclass
109
+ class SystemResourceSnapshot:
110
+ """System resource usage snapshot."""
111
+
112
+ timestamp: datetime = field(default_factory=datetime.now)
113
+ cpu_percent: float = 0.0
114
+ memory_mb: float = 0.0
115
+ memory_percent: float = 0.0
116
+ disk_usage_gb: float = 0.0
117
+ disk_usage_percent: float = 0.0
118
+ network_bytes_sent: int = 0
119
+ network_bytes_recv: int = 0
120
+ process_count: int = 0
121
+ thread_count: int = 0
122
+ open_files: int = 0
123
+
124
+ @classmethod
125
+ def capture_current(cls) -> "SystemResourceSnapshot":
126
+ """Capture current system resource usage."""
127
+ try:
128
+ # Get current process
129
+ process = psutil.Process()
130
+
131
+ # System-wide metrics
132
+ cpu_percent = psutil.cpu_percent(interval=0.1)
133
+ memory = psutil.virtual_memory()
134
+ disk = psutil.disk_usage("/")
135
+ network = psutil.net_io_counters()
136
+
137
+ # Process-specific metrics
138
+ process.memory_info()
139
+ process.cpu_percent()
140
+
141
+ return cls(
142
+ cpu_percent=cpu_percent,
143
+ memory_mb=memory.used / (1024 * 1024),
144
+ memory_percent=memory.percent,
145
+ disk_usage_gb=disk.used / (1024 * 1024 * 1024),
146
+ disk_usage_percent=(disk.used / disk.total) * 100,
147
+ network_bytes_sent=network.bytes_sent if network else 0,
148
+ network_bytes_recv=network.bytes_recv if network else 0,
149
+ process_count=len(psutil.pids()),
150
+ thread_count=process.num_threads(),
151
+ open_files=len(process.open_files()),
152
+ )
153
+ except Exception as e:
154
+ logger.warning(f"Failed to capture resource snapshot: {e}")
155
+ return cls()
156
+
157
+
158
+ class MetricsBuffer:
159
+ """Circular buffer for storing metrics with automatic aggregation."""
160
+
161
+ def __init__(self, name: str, max_size: int = 10000):
162
+ """Initialize metrics buffer."""
163
+ self.name = name
164
+ self.max_size = max_size
165
+ self.values: deque = deque(maxlen=max_size)
166
+ self.lock = threading.RLock()
167
+
168
+ def add_value(self, value: MetricValue) -> None:
169
+ """Add a metric value to the buffer."""
170
+ with self.lock:
171
+ self.values.append(value)
172
+
173
+ def get_aggregated(
174
+ self,
175
+ time_window: timedelta,
176
+ method: AggregationMethod = AggregationMethod.AVERAGE,
177
+ labels: Optional[Dict[str, str]] = None,
178
+ ) -> Optional[AggregatedMetric]:
179
+ """Get aggregated metric over time window."""
180
+ with self.lock:
181
+ if not self.values:
182
+ return None
183
+
184
+ # Filter by time window
185
+ cutoff_time = datetime.now() - time_window
186
+ relevant_values = [v for v in self.values if v.timestamp >= cutoff_time]
187
+
188
+ # Filter by labels if provided
189
+ if labels:
190
+ relevant_values = [
191
+ v
192
+ for v in relevant_values
193
+ if all(v.labels.get(k) == v for k, v in labels.items())
194
+ ]
195
+
196
+ if not relevant_values:
197
+ return None
198
+
199
+ # Extract numeric values
200
+ numeric_values = [v.value for v in relevant_values]
201
+
202
+ # Calculate aggregated value based on method
203
+ if method == AggregationMethod.SUM:
204
+ agg_value = sum(numeric_values)
205
+ elif method == AggregationMethod.AVERAGE:
206
+ agg_value = statistics.mean(numeric_values)
207
+ elif method == AggregationMethod.MEDIAN:
208
+ agg_value = statistics.median(numeric_values)
209
+ elif method == AggregationMethod.MIN:
210
+ agg_value = min(numeric_values)
211
+ elif method == AggregationMethod.MAX:
212
+ agg_value = max(numeric_values)
213
+ elif method == AggregationMethod.COUNT:
214
+ agg_value = len(numeric_values)
215
+ elif method == AggregationMethod.RATE:
216
+ # Calculate rate per minute
217
+ agg_value = len(numeric_values) / (time_window.total_seconds() / 60)
218
+ else:
219
+ agg_value = statistics.mean(numeric_values)
220
+
221
+ # Calculate statistics
222
+ min_value = min(numeric_values)
223
+ max_value = max(numeric_values)
224
+ std_dev = (
225
+ statistics.stdev(numeric_values) if len(numeric_values) > 1 else 0.0
226
+ )
227
+
228
+ # Get unit from first value (assuming all have same unit)
229
+ unit = relevant_values[0].unit
230
+
231
+ return AggregatedMetric(
232
+ name=self.name,
233
+ value=agg_value,
234
+ unit=unit,
235
+ aggregation_method=method,
236
+ time_window=time_window,
237
+ sample_count=len(numeric_values),
238
+ labels=labels or {},
239
+ min_value=min_value,
240
+ max_value=max_value,
241
+ std_deviation=std_dev,
242
+ )
243
+
244
+ def get_recent_values(self, count: int = 100) -> List[MetricValue]:
245
+ """Get the most recent metric values."""
246
+ with self.lock:
247
+ return list(self.values)[-count:]
248
+
249
+
250
+ class PerformanceMetricsCollector:
251
+ """Main metrics collection system."""
252
+
253
+ def __init__(
254
+ self,
255
+ storage_path: Optional[Path] = None,
256
+ collection_interval: float = 10.0, # seconds
257
+ max_buffer_size: int = 50000,
258
+ ):
259
+ """Initialize the metrics collector."""
260
+ self.storage_path = storage_path or Path.home() / ".omnimancer" / "metrics"
261
+ self.storage_path.mkdir(parents=True, exist_ok=True)
262
+
263
+ self.collection_interval = collection_interval
264
+ self.max_buffer_size = max_buffer_size
265
+
266
+ # Metric buffers
267
+ self.metric_buffers: Dict[str, MetricsBuffer] = {}
268
+ self.resource_snapshots: deque = deque(maxlen=max_buffer_size)
269
+ self.baselines: Dict[str, PerformanceBaseline] = {}
270
+
271
+ # Collection state
272
+ self.is_collecting = False
273
+ self.collection_thread: Optional[threading.Thread] = None
274
+
275
+ # Dependencies
276
+ self.status_manager = get_status_manager()
277
+ self.token_tracker = get_token_tracker()
278
+ self.persona_manager = get_persona_manager()
279
+
280
+ # Metrics tracking
281
+ self.operation_start_times: Dict[str, datetime] = {}
282
+ self.lock = threading.RLock()
283
+
284
+ logger.info("Performance metrics collector initialized")
285
+
286
+ def start_collection(self) -> None:
287
+ """Start metrics collection."""
288
+ if self.is_collecting:
289
+ logger.warning("Metrics collection is already running")
290
+ return
291
+
292
+ self.is_collecting = True
293
+
294
+ # Start background collection thread
295
+ self.collection_thread = threading.Thread(
296
+ target=self._collection_loop, name="MetricsCollection", daemon=True
297
+ )
298
+ self.collection_thread.start()
299
+
300
+ # Register event listeners
301
+ if self.status_manager:
302
+ self.status_manager.add_event_listener(self._handle_agent_event)
303
+
304
+ logger.info("Metrics collection started")
305
+
306
+ def stop_collection(self) -> None:
307
+ """Stop metrics collection."""
308
+ if not self.is_collecting:
309
+ return
310
+
311
+ self.is_collecting = False
312
+
313
+ # Unregister event listeners
314
+ if self.status_manager:
315
+ self.status_manager.remove_event_listener(self._handle_agent_event)
316
+
317
+ # Wait for collection thread to finish
318
+ if self.collection_thread and self.collection_thread.is_alive():
319
+ self.collection_thread.join(timeout=5.0)
320
+
321
+ logger.info("Metrics collection stopped")
322
+
323
+ def _collection_loop(self) -> None:
324
+ """Main collection loop running in background thread."""
325
+ while self.is_collecting:
326
+ try:
327
+ self._collect_system_metrics()
328
+ self._collect_agent_metrics()
329
+ time.sleep(self.collection_interval)
330
+ except Exception as e:
331
+ logger.error(f"Error in metrics collection loop: {e}")
332
+ time.sleep(self.collection_interval)
333
+
334
+ def _collect_system_metrics(self) -> None:
335
+ """Collect system resource metrics."""
336
+ try:
337
+ snapshot = SystemResourceSnapshot.capture_current()
338
+
339
+ with self.lock:
340
+ self.resource_snapshots.append(snapshot)
341
+
342
+ # Convert to individual metrics
343
+ self.record_metric("system.cpu_percent", snapshot.cpu_percent, "percent")
344
+ self.record_metric("system.memory_mb", snapshot.memory_mb, "MB")
345
+ self.record_metric(
346
+ "system.memory_percent", snapshot.memory_percent, "percent"
347
+ )
348
+ self.record_metric("system.disk_usage_gb", snapshot.disk_usage_gb, "GB")
349
+ self.record_metric("system.process_count", snapshot.process_count, "count")
350
+ self.record_metric("system.thread_count", snapshot.thread_count, "count")
351
+
352
+ except Exception as e:
353
+ logger.debug(f"Failed to collect system metrics: {e}")
354
+
355
+ def _collect_agent_metrics(self) -> None:
356
+ """Collect agent-specific metrics."""
357
+ try:
358
+ # Collect token usage metrics
359
+ if self.token_tracker:
360
+ recent_summary = self.token_tracker.get_usage_summary(
361
+ timedelta(minutes=1)
362
+ )
363
+
364
+ self.record_metric(
365
+ "agent.tokens_per_minute",
366
+ recent_summary.get("total_tokens", 0),
367
+ "tokens",
368
+ category=MetricCategory.EFFICIENCY,
369
+ )
370
+
371
+ self.record_metric(
372
+ "agent.cost_per_minute",
373
+ recent_summary.get("total_cost", 0.0),
374
+ "USD",
375
+ category=MetricCategory.COST,
376
+ )
377
+
378
+ self.record_metric(
379
+ "agent.requests_per_minute",
380
+ recent_summary.get("total_requests", 0),
381
+ "requests",
382
+ category=MetricCategory.PERFORMANCE,
383
+ )
384
+
385
+ # Collect persona metrics
386
+ if self.persona_manager:
387
+ active_personas = self.persona_manager.get_active_personas()
388
+ for persona in active_personas:
389
+ labels = {"persona": persona.name}
390
+
391
+ # Record persona-specific metrics
392
+ self.record_metric(
393
+ "persona.active",
394
+ 1.0,
395
+ "boolean",
396
+ labels=labels,
397
+ category=MetricCategory.USER_EXPERIENCE,
398
+ )
399
+
400
+ except Exception as e:
401
+ logger.debug(f"Failed to collect agent metrics: {e}")
402
+
403
+ def record_metric(
404
+ self,
405
+ name: str,
406
+ value: float,
407
+ unit: str = "",
408
+ labels: Optional[Dict[str, str]] = None,
409
+ category: MetricCategory = MetricCategory.PERFORMANCE,
410
+ metadata: Optional[Dict[str, Any]] = None,
411
+ ) -> None:
412
+ """Record a metric value."""
413
+ metric = MetricValue(
414
+ name=name,
415
+ value=value,
416
+ unit=unit,
417
+ category=category,
418
+ labels=labels or {},
419
+ metadata=metadata or {},
420
+ )
421
+
422
+ # Ensure metric buffer exists
423
+ if name not in self.metric_buffers:
424
+ self.metric_buffers[name] = MetricsBuffer(name, self.max_buffer_size)
425
+
426
+ # Add to buffer
427
+ self.metric_buffers[name].add_value(metric)
428
+
429
+ logger.debug(f"Recorded metric {name}={value}{unit}")
430
+
431
+ def record_operation_timing(
432
+ self,
433
+ operation_id: str,
434
+ operation_type: OperationType,
435
+ duration_ms: float,
436
+ success: bool = True,
437
+ persona_name: Optional[str] = None,
438
+ provider_name: Optional[str] = None,
439
+ ) -> None:
440
+ """Record operation timing metrics."""
441
+ labels = {
442
+ "operation_type": operation_type.value,
443
+ "success": str(success).lower(),
444
+ }
445
+
446
+ if persona_name:
447
+ labels["persona"] = persona_name
448
+ if provider_name:
449
+ labels["provider"] = provider_name
450
+
451
+ # Record duration
452
+ self.record_metric(
453
+ "operation.duration_ms",
454
+ duration_ms,
455
+ "ms",
456
+ labels=labels,
457
+ category=MetricCategory.PERFORMANCE,
458
+ )
459
+
460
+ # Record success rate
461
+ self.record_metric(
462
+ "operation.success",
463
+ 1.0 if success else 0.0,
464
+ "boolean",
465
+ labels=labels,
466
+ category=MetricCategory.RELIABILITY,
467
+ )
468
+
469
+ # Record operation count
470
+ self.record_metric(
471
+ "operation.count",
472
+ 1.0,
473
+ "count",
474
+ labels=labels,
475
+ category=MetricCategory.PERFORMANCE,
476
+ )
477
+
478
+ def _handle_agent_event(self, event: AgentEvent) -> None:
479
+ """Handle agent events for metrics collection."""
480
+ try:
481
+ if event.event_type == EventType.OPERATION_STARTED:
482
+ if event.operation:
483
+ with self.lock:
484
+ self.operation_start_times[event.operation.operation_id] = (
485
+ datetime.now()
486
+ )
487
+
488
+ elif event.event_type in [
489
+ EventType.OPERATION_COMPLETED,
490
+ EventType.OPERATION_FAILED,
491
+ ]:
492
+ if event.operation:
493
+ with self.lock:
494
+ start_time = self.operation_start_times.pop(
495
+ event.operation.operation_id, None
496
+ )
497
+
498
+ if start_time:
499
+ duration_ms = (
500
+ datetime.now() - start_time
501
+ ).total_seconds() * 1000
502
+ success = event.event_type == EventType.OPERATION_COMPLETED
503
+
504
+ self.record_operation_timing(
505
+ event.operation.operation_id,
506
+ event.operation.operation_type,
507
+ duration_ms,
508
+ success,
509
+ metadata={"event_type": event.event_type.value},
510
+ )
511
+
512
+ except Exception as e:
513
+ logger.error(f"Error handling agent event for metrics: {e}")
514
+
515
+ def get_metric_summary(
516
+ self,
517
+ metric_name: str,
518
+ time_window: timedelta = timedelta(hours=1),
519
+ aggregation_method: AggregationMethod = AggregationMethod.AVERAGE,
520
+ labels: Optional[Dict[str, str]] = None,
521
+ ) -> Optional[AggregatedMetric]:
522
+ """Get aggregated metric summary."""
523
+ if metric_name not in self.metric_buffers:
524
+ return None
525
+
526
+ return self.metric_buffers[metric_name].get_aggregated(
527
+ time_window, aggregation_method, labels
528
+ )
529
+
530
+ def get_dashboard_metrics(self) -> Dict[str, Any]:
531
+ """Get metrics formatted for dashboard display."""
532
+ metrics = {}
533
+
534
+ # Performance metrics
535
+ response_time = self.get_metric_summary(
536
+ "operation.duration_ms", timedelta(minutes=10)
537
+ )
538
+ success_rate = self.get_metric_summary(
539
+ "operation.success",
540
+ timedelta(minutes=10),
541
+ AggregationMethod.AVERAGE,
542
+ )
543
+
544
+ metrics["performance"] = {
545
+ "avg_response_time_ms": (response_time.value if response_time else 0),
546
+ "success_rate_percent": (
547
+ (success_rate.value * 100) if success_rate else 100
548
+ ),
549
+ }
550
+
551
+ # Resource metrics
552
+ cpu_usage = self.get_metric_summary("system.cpu_percent", timedelta(minutes=5))
553
+ memory_usage = self.get_metric_summary("system.memory_mb", timedelta(minutes=5))
554
+
555
+ metrics["resources"] = {
556
+ "cpu_percent": cpu_usage.value if cpu_usage else 0,
557
+ "memory_mb": memory_usage.value if memory_usage else 0,
558
+ }
559
+
560
+ # Token usage metrics
561
+ tokens_per_min = self.get_metric_summary(
562
+ "agent.tokens_per_minute", timedelta(minutes=10)
563
+ )
564
+ cost_per_min = self.get_metric_summary(
565
+ "agent.cost_per_minute", timedelta(minutes=10)
566
+ )
567
+
568
+ metrics["usage"] = {
569
+ "tokens_per_minute": tokens_per_min.value if tokens_per_min else 0,
570
+ "cost_per_minute": cost_per_min.value if cost_per_min else 0,
571
+ }
572
+
573
+ return metrics
574
+
575
+ def establish_baseline(
576
+ self,
577
+ metric_name: str,
578
+ measurement_period: timedelta = timedelta(days=7),
579
+ labels: Optional[Dict[str, str]] = None,
580
+ ) -> Optional[PerformanceBaseline]:
581
+ """Establish performance baseline for a metric."""
582
+ if metric_name not in self.metric_buffers:
583
+ return None
584
+
585
+ # Get historical data
586
+ cutoff_time = datetime.now() - measurement_period
587
+ with self.lock:
588
+ values = [
589
+ v
590
+ for v in self.metric_buffers[metric_name].values
591
+ if v.timestamp >= cutoff_time
592
+ ]
593
+
594
+ if labels:
595
+ values = [
596
+ v
597
+ for v in values
598
+ if all(v.labels.get(k) == v for k, v in labels.items())
599
+ ]
600
+
601
+ if len(values) < 10: # Need minimum samples
602
+ return None
603
+
604
+ # Calculate baseline statistics
605
+ numeric_values = [v.value for v in values]
606
+ mean_value = statistics.mean(numeric_values)
607
+ std_dev = statistics.stdev(numeric_values)
608
+
609
+ # 95% confidence interval (assuming normal distribution)
610
+ confidence_interval = (
611
+ mean_value - (1.96 * std_dev),
612
+ mean_value + (1.96 * std_dev),
613
+ )
614
+
615
+ baseline = PerformanceBaseline(
616
+ metric_name=metric_name,
617
+ baseline_value=mean_value,
618
+ confidence_interval=confidence_interval,
619
+ measurement_period=measurement_period,
620
+ sample_size=len(values),
621
+ labels=labels or {},
622
+ )
623
+
624
+ # Store baseline
625
+ baseline_key = f"{metric_name}:{hash(str(sorted((labels or {}).items())))}"
626
+ self.baselines[baseline_key] = baseline
627
+
628
+ logger.info(f"Established baseline for {metric_name}: {mean_value:.2f}")
629
+ return baseline
630
+
631
+ def detect_anomalies(
632
+ self, metric_name: str, labels: Optional[Dict[str, str]] = None
633
+ ) -> List[Dict[str, Any]]:
634
+ """Detect anomalies in metrics compared to baseline."""
635
+ baseline_key = f"{metric_name}:{hash(str(sorted((labels or {}).items())))}"
636
+ baseline = self.baselines.get(baseline_key)
637
+
638
+ if not baseline:
639
+ return []
640
+
641
+ # Get recent values
642
+ recent_values = self.get_metric_summary(
643
+ metric_name,
644
+ timedelta(minutes=10),
645
+ AggregationMethod.AVERAGE,
646
+ labels,
647
+ )
648
+
649
+ if not recent_values:
650
+ return []
651
+
652
+ anomalies = []
653
+ current_value = recent_values.value
654
+
655
+ # Check if value is outside confidence interval
656
+ lower_bound, upper_bound = baseline.confidence_interval
657
+
658
+ if current_value < lower_bound:
659
+ anomalies.append(
660
+ {
661
+ "type": "below_baseline",
662
+ "metric_name": metric_name,
663
+ "current_value": current_value,
664
+ "baseline_value": baseline.baseline_value,
665
+ "deviation": baseline.baseline_value - current_value,
666
+ "severity": (
667
+ "high" if current_value < (lower_bound * 0.5) else "medium"
668
+ ),
669
+ }
670
+ )
671
+ elif current_value > upper_bound:
672
+ anomalies.append(
673
+ {
674
+ "type": "above_baseline",
675
+ "metric_name": metric_name,
676
+ "current_value": current_value,
677
+ "baseline_value": baseline.baseline_value,
678
+ "deviation": current_value - baseline.baseline_value,
679
+ "severity": (
680
+ "high" if current_value > (upper_bound * 1.5) else "medium"
681
+ ),
682
+ }
683
+ )
684
+
685
+ return anomalies
686
+
687
+ def export_metrics(
688
+ self,
689
+ time_window: Optional[timedelta] = None,
690
+ filename: Optional[str] = None,
691
+ ) -> Path:
692
+ """Export metrics to JSON file."""
693
+ if not filename:
694
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
695
+ filename = f"metrics_export_{timestamp}.json"
696
+
697
+ filepath = self.storage_path / filename
698
+
699
+ # Collect all metrics
700
+ export_data = {
701
+ "metadata": {
702
+ "export_time": datetime.now().isoformat(),
703
+ "time_window": str(time_window) if time_window else "all_time",
704
+ "collector_version": "1.0",
705
+ },
706
+ "metrics": {},
707
+ "baselines": {},
708
+ }
709
+
710
+ # Export metric buffers
711
+ cutoff_time = datetime.now() - time_window if time_window else None
712
+
713
+ for name, buffer in self.metric_buffers.items():
714
+ with buffer.lock:
715
+ values = list(buffer.values)
716
+ if cutoff_time:
717
+ values = [v for v in values if v.timestamp >= cutoff_time]
718
+
719
+ export_data["metrics"][name] = [asdict(v) for v in values]
720
+
721
+ # Export baselines
722
+ for key, baseline in self.baselines.items():
723
+ export_data["baselines"][key] = asdict(baseline)
724
+
725
+ # Write to file
726
+ with open(filepath, "w") as f:
727
+ json.dump(export_data, f, indent=2, default=str)
728
+
729
+ logger.info(f"Exported metrics to {filepath}")
730
+ return filepath
731
+
732
+
733
+ # Global metrics collector instance
734
+ _metrics_collector: Optional[PerformanceMetricsCollector] = None
735
+ _collector_lock = threading.Lock()
736
+
737
+
738
+ def get_metrics_collector() -> PerformanceMetricsCollector:
739
+ """Get the global metrics collector instance."""
740
+ global _metrics_collector
741
+ if _metrics_collector is None:
742
+ with _collector_lock:
743
+ if _metrics_collector is None:
744
+ _metrics_collector = PerformanceMetricsCollector()
745
+ return _metrics_collector
746
+
747
+
748
+ def set_metrics_collector(collector: PerformanceMetricsCollector) -> None:
749
+ """Set the global metrics collector instance."""
750
+ global _metrics_collector
751
+ with _collector_lock:
752
+ _metrics_collector = collector
753
+
754
+
755
+ def initialize_metrics_collection(
756
+ auto_start: bool = True,
757
+ ) -> PerformanceMetricsCollector:
758
+ """Initialize metrics collection system."""
759
+ collector = get_metrics_collector()
760
+ if auto_start:
761
+ collector.start_collection()
762
+ return collector
763
+
764
+
765
+ def shutdown_metrics_collection() -> None:
766
+ """Shutdown metrics collection system."""
767
+ global _metrics_collector
768
+ if _metrics_collector:
769
+ _metrics_collector.stop_collection()
770
+ with _collector_lock:
771
+ _metrics_collector = None