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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|