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,717 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Performance Monitoring System for Omnimancer.
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive performance monitoring for agent operations,
|
|
5
|
+
including token usage tracking, context efficiency measurement, operation timing,
|
|
6
|
+
and resource utilization metrics with optimization suggestions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import statistics
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from collections import defaultdict, 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, Callable, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from .persona import PersonaManager, get_persona_manager
|
|
22
|
+
from .status_core import (
|
|
23
|
+
AgentEvent,
|
|
24
|
+
EventType,
|
|
25
|
+
OperationType,
|
|
26
|
+
)
|
|
27
|
+
from .status_manager import UnifiedStatusManager as AgentStatusManager
|
|
28
|
+
from .status_manager import (
|
|
29
|
+
get_status_manager,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MetricType(Enum):
|
|
36
|
+
"""Types of performance metrics."""
|
|
37
|
+
|
|
38
|
+
TOKEN_USAGE = "token_usage"
|
|
39
|
+
OPERATION_TIMING = "operation_timing"
|
|
40
|
+
CONTEXT_EFFICIENCY = "context_efficiency"
|
|
41
|
+
RESOURCE_UTILIZATION = "resource_utilization"
|
|
42
|
+
SUCCESS_RATE = "success_rate"
|
|
43
|
+
COST_TRACKING = "cost_tracking"
|
|
44
|
+
PERSONA_PERFORMANCE = "persona_performance"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AlertLevel(Enum):
|
|
48
|
+
"""Alert severity levels."""
|
|
49
|
+
|
|
50
|
+
INFO = "info"
|
|
51
|
+
WARNING = "warning"
|
|
52
|
+
CRITICAL = "critical"
|
|
53
|
+
EMERGENCY = "emergency"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OptimizationType(Enum):
|
|
57
|
+
"""Types of optimization suggestions."""
|
|
58
|
+
|
|
59
|
+
PROVIDER_SWITCHING = "provider_switching"
|
|
60
|
+
CONTEXT_REDUCTION = "context_reduction"
|
|
61
|
+
PERSONA_OPTIMIZATION = "persona_optimization"
|
|
62
|
+
RESOURCE_TUNING = "resource_tuning"
|
|
63
|
+
COST_OPTIMIZATION = "cost_optimization"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class TokenUsageMetrics:
|
|
68
|
+
"""Token usage metrics for a specific operation or time period."""
|
|
69
|
+
|
|
70
|
+
total_tokens: int = 0
|
|
71
|
+
input_tokens: int = 0
|
|
72
|
+
output_tokens: int = 0
|
|
73
|
+
cached_tokens: int = 0
|
|
74
|
+
provider_name: str = ""
|
|
75
|
+
model_name: str = ""
|
|
76
|
+
operation_type: OperationType = OperationType.API_CALL
|
|
77
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
78
|
+
cost_estimate: float = 0.0
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def efficiency_ratio(self) -> float:
|
|
82
|
+
"""Calculate token efficiency ratio (output/total)."""
|
|
83
|
+
return (
|
|
84
|
+
(self.output_tokens / self.total_tokens) if self.total_tokens > 0 else 0.0
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class OperationTiming:
|
|
90
|
+
"""Timing metrics for agent operations."""
|
|
91
|
+
|
|
92
|
+
operation_id: str
|
|
93
|
+
operation_type: OperationType
|
|
94
|
+
start_time: datetime
|
|
95
|
+
end_time: Optional[datetime] = None
|
|
96
|
+
duration_ms: float = 0.0
|
|
97
|
+
context_load_time_ms: float = 0.0
|
|
98
|
+
api_call_time_ms: float = 0.0
|
|
99
|
+
processing_time_ms: float = 0.0
|
|
100
|
+
queue_wait_time_ms: float = 0.0
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def is_complete(self) -> bool:
|
|
104
|
+
"""Check if timing is complete."""
|
|
105
|
+
return self.end_time is not None
|
|
106
|
+
|
|
107
|
+
def complete_timing(self, end_time: Optional[datetime] = None) -> None:
|
|
108
|
+
"""Complete the timing measurement."""
|
|
109
|
+
if not end_time:
|
|
110
|
+
end_time = datetime.now()
|
|
111
|
+
self.end_time = end_time
|
|
112
|
+
self.duration_ms = (end_time - self.start_time).total_seconds() * 1000
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class ContextMetrics:
|
|
117
|
+
"""Context efficiency metrics."""
|
|
118
|
+
|
|
119
|
+
context_size_chars: int = 0
|
|
120
|
+
context_size_tokens: int = 0
|
|
121
|
+
useful_context_ratio: float = 0.0 # % of context that was actually used
|
|
122
|
+
context_compression_ratio: float = 0.0
|
|
123
|
+
context_load_time_ms: float = 0.0
|
|
124
|
+
context_type: str = "" # "file", "conversation", "documentation", etc.
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def efficiency_score(self) -> float:
|
|
128
|
+
"""Calculate overall context efficiency score (0-100)."""
|
|
129
|
+
base_score = self.useful_context_ratio * 100
|
|
130
|
+
# Penalize slow loading
|
|
131
|
+
time_penalty = min(10, self.context_load_time_ms / 100)
|
|
132
|
+
return max(0, base_score - time_penalty)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class ResourceUtilization:
|
|
137
|
+
"""Resource utilization metrics."""
|
|
138
|
+
|
|
139
|
+
memory_mb: float = 0.0
|
|
140
|
+
cpu_percent: float = 0.0
|
|
141
|
+
network_requests: int = 0
|
|
142
|
+
disk_reads: int = 0
|
|
143
|
+
disk_writes: int = 0
|
|
144
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def overall_utilization(self) -> float:
|
|
148
|
+
"""Calculate overall resource utilization score."""
|
|
149
|
+
return (self.memory_mb / 1024) * 0.3 + self.cpu_percent * 0.7
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class PerformanceAlert:
|
|
154
|
+
"""Performance monitoring alert."""
|
|
155
|
+
|
|
156
|
+
alert_id: str
|
|
157
|
+
level: AlertLevel
|
|
158
|
+
title: str
|
|
159
|
+
description: str
|
|
160
|
+
metric_type: MetricType
|
|
161
|
+
threshold_value: float
|
|
162
|
+
current_value: float
|
|
163
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
164
|
+
persona_name: Optional[str] = None
|
|
165
|
+
provider_name: Optional[str] = None
|
|
166
|
+
suggested_actions: List[str] = field(default_factory=list)
|
|
167
|
+
auto_resolvable: bool = False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class OptimizationSuggestion:
|
|
172
|
+
"""Performance optimization suggestion."""
|
|
173
|
+
|
|
174
|
+
suggestion_id: str
|
|
175
|
+
optimization_type: OptimizationType
|
|
176
|
+
title: str
|
|
177
|
+
description: str
|
|
178
|
+
potential_improvement: str # "20% faster", "30% cost reduction", etc.
|
|
179
|
+
confidence: float # 0-1
|
|
180
|
+
implementation_difficulty: str # "easy", "medium", "hard"
|
|
181
|
+
estimated_impact: Dict[str, float] = field(default_factory=dict)
|
|
182
|
+
required_actions: List[str] = field(default_factory=list)
|
|
183
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class PerformanceSnapshot:
|
|
188
|
+
"""Point-in-time performance snapshot."""
|
|
189
|
+
|
|
190
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
191
|
+
token_usage: TokenUsageMetrics = field(default_factory=TokenUsageMetrics)
|
|
192
|
+
context_metrics: ContextMetrics = field(default_factory=ContextMetrics)
|
|
193
|
+
resource_utilization: ResourceUtilization = field(
|
|
194
|
+
default_factory=ResourceUtilization
|
|
195
|
+
)
|
|
196
|
+
active_operations: int = 0
|
|
197
|
+
success_rate: float = 100.0
|
|
198
|
+
average_response_time_ms: float = 0.0
|
|
199
|
+
persona_name: Optional[str] = None
|
|
200
|
+
provider_name: Optional[str] = None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class PerformanceMetricsCollector:
|
|
204
|
+
"""Collects and aggregates performance metrics."""
|
|
205
|
+
|
|
206
|
+
def __init__(self, max_history_size: int = 10000):
|
|
207
|
+
"""Initialize the metrics collector."""
|
|
208
|
+
self.max_history_size = max_history_size
|
|
209
|
+
self.token_usage_history: deque = deque(maxlen=max_history_size)
|
|
210
|
+
self.timing_history: deque = deque(maxlen=max_history_size)
|
|
211
|
+
self.context_history: deque = deque(maxlen=max_history_size)
|
|
212
|
+
self.resource_history: deque = deque(maxlen=max_history_size)
|
|
213
|
+
self.active_timings: Dict[str, OperationTiming] = {}
|
|
214
|
+
self._lock = threading.RLock()
|
|
215
|
+
|
|
216
|
+
def record_token_usage(self, tokens: TokenUsageMetrics) -> None:
|
|
217
|
+
"""Record token usage metrics."""
|
|
218
|
+
with self._lock:
|
|
219
|
+
self.token_usage_history.append(tokens)
|
|
220
|
+
logger.debug(
|
|
221
|
+
f"Recorded token usage: {tokens.total_tokens} tokens for {tokens.provider_name}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def start_operation_timing(
|
|
225
|
+
self, operation_id: str, operation_type: OperationType
|
|
226
|
+
) -> OperationTiming:
|
|
227
|
+
"""Start timing an operation."""
|
|
228
|
+
with self._lock:
|
|
229
|
+
timing = OperationTiming(
|
|
230
|
+
operation_id=operation_id,
|
|
231
|
+
operation_type=operation_type,
|
|
232
|
+
start_time=datetime.now(),
|
|
233
|
+
)
|
|
234
|
+
self.active_timings[operation_id] = timing
|
|
235
|
+
return timing
|
|
236
|
+
|
|
237
|
+
def complete_operation_timing(self, operation_id: str) -> Optional[OperationTiming]:
|
|
238
|
+
"""Complete timing measurement for an operation."""
|
|
239
|
+
with self._lock:
|
|
240
|
+
if operation_id in self.active_timings:
|
|
241
|
+
timing = self.active_timings.pop(operation_id)
|
|
242
|
+
timing.complete_timing()
|
|
243
|
+
self.timing_history.append(timing)
|
|
244
|
+
logger.debug(
|
|
245
|
+
f"Completed timing for {operation_id}: {timing.duration_ms}ms"
|
|
246
|
+
)
|
|
247
|
+
return timing
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
def record_context_metrics(self, context: ContextMetrics) -> None:
|
|
251
|
+
"""Record context efficiency metrics."""
|
|
252
|
+
with self._lock:
|
|
253
|
+
self.context_history.append(context)
|
|
254
|
+
logger.debug(
|
|
255
|
+
f"Recorded context metrics: {context.efficiency_score:.1f} efficiency score"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def record_resource_utilization(self, resources: ResourceUtilization) -> None:
|
|
259
|
+
"""Record resource utilization metrics."""
|
|
260
|
+
with self._lock:
|
|
261
|
+
self.resource_history.append(resources)
|
|
262
|
+
|
|
263
|
+
def get_token_usage_stats(
|
|
264
|
+
self, time_window: Optional[timedelta] = None
|
|
265
|
+
) -> Dict[str, Any]:
|
|
266
|
+
"""Get aggregated token usage statistics."""
|
|
267
|
+
with self._lock:
|
|
268
|
+
if not self.token_usage_history:
|
|
269
|
+
return {"total_tokens": 0, "providers": {}, "operations": {}}
|
|
270
|
+
|
|
271
|
+
# Filter by time window if provided
|
|
272
|
+
cutoff_time = datetime.now() - time_window if time_window else None
|
|
273
|
+
relevant_usage = [
|
|
274
|
+
usage
|
|
275
|
+
for usage in self.token_usage_history
|
|
276
|
+
if not cutoff_time or usage.timestamp >= cutoff_time
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
if not relevant_usage:
|
|
280
|
+
return {"total_tokens": 0, "providers": {}, "operations": {}}
|
|
281
|
+
|
|
282
|
+
# Aggregate statistics
|
|
283
|
+
total_tokens = sum(usage.total_tokens for usage in relevant_usage)
|
|
284
|
+
total_cost = sum(usage.cost_estimate for usage in relevant_usage)
|
|
285
|
+
|
|
286
|
+
# Group by provider
|
|
287
|
+
provider_stats = defaultdict(
|
|
288
|
+
lambda: {"tokens": 0, "cost": 0.0, "requests": 0}
|
|
289
|
+
)
|
|
290
|
+
for usage in relevant_usage:
|
|
291
|
+
provider_stats[usage.provider_name]["tokens"] += usage.total_tokens
|
|
292
|
+
provider_stats[usage.provider_name]["cost"] += usage.cost_estimate
|
|
293
|
+
provider_stats[usage.provider_name]["requests"] += 1
|
|
294
|
+
|
|
295
|
+
# Group by operation type
|
|
296
|
+
operation_stats = defaultdict(lambda: {"tokens": 0, "requests": 0})
|
|
297
|
+
for usage in relevant_usage:
|
|
298
|
+
operation_stats[usage.operation_type.value][
|
|
299
|
+
"tokens"
|
|
300
|
+
] += usage.total_tokens
|
|
301
|
+
operation_stats[usage.operation_type.value]["requests"] += 1
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"total_tokens": total_tokens,
|
|
305
|
+
"total_cost": total_cost,
|
|
306
|
+
"total_requests": len(relevant_usage),
|
|
307
|
+
"providers": dict(provider_stats),
|
|
308
|
+
"operations": dict(operation_stats),
|
|
309
|
+
"time_period": (
|
|
310
|
+
f"{time_window.total_seconds():.0f} seconds"
|
|
311
|
+
if time_window
|
|
312
|
+
else "all time"
|
|
313
|
+
),
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
def get_performance_snapshot(self) -> PerformanceSnapshot:
|
|
317
|
+
"""Get current performance snapshot."""
|
|
318
|
+
with self._lock:
|
|
319
|
+
# Calculate recent metrics (last 10 minutes)
|
|
320
|
+
recent_window = timedelta(minutes=10)
|
|
321
|
+
recent_tokens = [
|
|
322
|
+
usage
|
|
323
|
+
for usage in self.token_usage_history
|
|
324
|
+
if (datetime.now() - usage.timestamp) <= recent_window
|
|
325
|
+
]
|
|
326
|
+
recent_timings = [
|
|
327
|
+
timing
|
|
328
|
+
for timing in self.timing_history
|
|
329
|
+
if timing.is_complete
|
|
330
|
+
and (datetime.now() - timing.start_time) <= recent_window
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
# Aggregate token usage
|
|
334
|
+
token_metrics = TokenUsageMetrics()
|
|
335
|
+
if recent_tokens:
|
|
336
|
+
token_metrics.total_tokens = sum(t.total_tokens for t in recent_tokens)
|
|
337
|
+
token_metrics.input_tokens = sum(t.input_tokens for t in recent_tokens)
|
|
338
|
+
token_metrics.output_tokens = sum(
|
|
339
|
+
t.output_tokens for t in recent_tokens
|
|
340
|
+
)
|
|
341
|
+
token_metrics.cost_estimate = sum(
|
|
342
|
+
t.cost_estimate for t in recent_tokens
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Calculate average response time
|
|
346
|
+
avg_response_time = 0.0
|
|
347
|
+
if recent_timings:
|
|
348
|
+
avg_response_time = statistics.mean(
|
|
349
|
+
t.duration_ms for t in recent_timings
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Calculate success rate
|
|
353
|
+
total_ops = len(recent_timings)
|
|
354
|
+
successful_ops = len(
|
|
355
|
+
[t for t in recent_timings if t.duration_ms > 0]
|
|
356
|
+
) # Completed ops
|
|
357
|
+
success_rate = (
|
|
358
|
+
(successful_ops / total_ops * 100) if total_ops > 0 else 100.0
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return PerformanceSnapshot(
|
|
362
|
+
token_usage=token_metrics,
|
|
363
|
+
active_operations=len(self.active_timings),
|
|
364
|
+
success_rate=success_rate,
|
|
365
|
+
average_response_time_ms=avg_response_time,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class PerformanceMonitor:
|
|
370
|
+
"""Main performance monitoring system."""
|
|
371
|
+
|
|
372
|
+
def __init__(
|
|
373
|
+
self,
|
|
374
|
+
status_manager: Optional[AgentStatusManager] = None,
|
|
375
|
+
persona_manager: Optional[PersonaManager] = None,
|
|
376
|
+
storage_path: Optional[Path] = None,
|
|
377
|
+
):
|
|
378
|
+
"""Initialize the performance monitor."""
|
|
379
|
+
self.status_manager = status_manager or get_status_manager()
|
|
380
|
+
self.persona_manager = persona_manager or get_persona_manager()
|
|
381
|
+
self.storage_path = storage_path or Path.home() / ".omnimancer" / "performance"
|
|
382
|
+
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
383
|
+
|
|
384
|
+
self.metrics_collector = PerformanceMetricsCollector()
|
|
385
|
+
self.alerts: List[PerformanceAlert] = []
|
|
386
|
+
self.suggestions: List[OptimizationSuggestion] = []
|
|
387
|
+
self.alert_thresholds = self._initialize_thresholds()
|
|
388
|
+
self.is_monitoring = False
|
|
389
|
+
self._lock = threading.RLock()
|
|
390
|
+
|
|
391
|
+
# Event listeners
|
|
392
|
+
self._event_listeners: List[Callable] = []
|
|
393
|
+
|
|
394
|
+
logger.info("Performance monitor initialized")
|
|
395
|
+
|
|
396
|
+
def _initialize_thresholds(self) -> Dict[str, Dict[str, float]]:
|
|
397
|
+
"""Initialize default alert thresholds."""
|
|
398
|
+
return {
|
|
399
|
+
"token_usage": {
|
|
400
|
+
"high_usage_per_minute": 10000,
|
|
401
|
+
"excessive_usage_per_minute": 50000,
|
|
402
|
+
"cost_threshold_per_hour": 10.0,
|
|
403
|
+
},
|
|
404
|
+
"timing": {
|
|
405
|
+
"slow_response_ms": 5000,
|
|
406
|
+
"very_slow_response_ms": 15000,
|
|
407
|
+
"context_load_slow_ms": 2000,
|
|
408
|
+
},
|
|
409
|
+
"context": {
|
|
410
|
+
"low_efficiency_threshold": 30.0,
|
|
411
|
+
"very_low_efficiency_threshold": 10.0,
|
|
412
|
+
},
|
|
413
|
+
"resources": {"high_memory_mb": 1024, "high_cpu_percent": 80.0},
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def start_monitoring(self) -> None:
|
|
417
|
+
"""Start performance monitoring."""
|
|
418
|
+
if self.is_monitoring:
|
|
419
|
+
logger.warning("Performance monitoring is already active")
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
self.is_monitoring = True
|
|
423
|
+
|
|
424
|
+
# Register with status manager for events
|
|
425
|
+
if self.status_manager:
|
|
426
|
+
self.status_manager.add_event_listener(self._handle_agent_event)
|
|
427
|
+
|
|
428
|
+
logger.info("Performance monitoring started")
|
|
429
|
+
|
|
430
|
+
def stop_monitoring(self) -> None:
|
|
431
|
+
"""Stop performance monitoring."""
|
|
432
|
+
if not self.is_monitoring:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
self.is_monitoring = False
|
|
436
|
+
|
|
437
|
+
# Unregister from status manager
|
|
438
|
+
if self.status_manager:
|
|
439
|
+
self.status_manager.remove_event_listener(self._handle_agent_event)
|
|
440
|
+
|
|
441
|
+
logger.info("Performance monitoring stopped")
|
|
442
|
+
|
|
443
|
+
def record_api_call_metrics(
|
|
444
|
+
self,
|
|
445
|
+
provider_name: str,
|
|
446
|
+
model_name: str,
|
|
447
|
+
input_tokens: int,
|
|
448
|
+
output_tokens: int,
|
|
449
|
+
cached_tokens: int = 0,
|
|
450
|
+
cost_estimate: float = 0.0,
|
|
451
|
+
operation_type: OperationType = OperationType.API_CALL,
|
|
452
|
+
) -> None:
|
|
453
|
+
"""Record API call metrics."""
|
|
454
|
+
metrics = TokenUsageMetrics(
|
|
455
|
+
total_tokens=input_tokens + output_tokens,
|
|
456
|
+
input_tokens=input_tokens,
|
|
457
|
+
output_tokens=output_tokens,
|
|
458
|
+
cached_tokens=cached_tokens,
|
|
459
|
+
provider_name=provider_name,
|
|
460
|
+
model_name=model_name,
|
|
461
|
+
operation_type=operation_type,
|
|
462
|
+
cost_estimate=cost_estimate,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
self.metrics_collector.record_token_usage(metrics)
|
|
466
|
+
self._check_token_usage_alerts(metrics)
|
|
467
|
+
|
|
468
|
+
def start_operation_tracking(
|
|
469
|
+
self, operation_id: str, operation_type: OperationType
|
|
470
|
+
) -> str:
|
|
471
|
+
"""Start tracking an operation."""
|
|
472
|
+
timing = self.metrics_collector.start_operation_timing(
|
|
473
|
+
operation_id, operation_type
|
|
474
|
+
)
|
|
475
|
+
return timing.operation_id
|
|
476
|
+
|
|
477
|
+
def complete_operation_tracking(
|
|
478
|
+
self, operation_id: str
|
|
479
|
+
) -> Optional[OperationTiming]:
|
|
480
|
+
"""Complete tracking an operation."""
|
|
481
|
+
timing = self.metrics_collector.complete_operation_timing(operation_id)
|
|
482
|
+
if timing:
|
|
483
|
+
self._check_timing_alerts(timing)
|
|
484
|
+
return timing
|
|
485
|
+
|
|
486
|
+
def record_context_usage(
|
|
487
|
+
self,
|
|
488
|
+
context_size_chars: int,
|
|
489
|
+
context_size_tokens: int,
|
|
490
|
+
useful_ratio: float,
|
|
491
|
+
load_time_ms: float,
|
|
492
|
+
context_type: str = "",
|
|
493
|
+
) -> None:
|
|
494
|
+
"""Record context usage metrics."""
|
|
495
|
+
context_metrics = ContextMetrics(
|
|
496
|
+
context_size_chars=context_size_chars,
|
|
497
|
+
context_size_tokens=context_size_tokens,
|
|
498
|
+
useful_context_ratio=useful_ratio,
|
|
499
|
+
context_load_time_ms=load_time_ms,
|
|
500
|
+
context_type=context_type,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
self.metrics_collector.record_context_metrics(context_metrics)
|
|
504
|
+
self._check_context_alerts(context_metrics)
|
|
505
|
+
|
|
506
|
+
def get_performance_dashboard_data(self) -> Dict[str, Any]:
|
|
507
|
+
"""Get data for performance dashboard."""
|
|
508
|
+
snapshot = self.metrics_collector.get_performance_snapshot()
|
|
509
|
+
recent_stats = self.metrics_collector.get_token_usage_stats(timedelta(hours=1))
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
"current_snapshot": asdict(snapshot),
|
|
513
|
+
"recent_stats": recent_stats,
|
|
514
|
+
"active_alerts": [
|
|
515
|
+
asdict(alert) for alert in self.alerts[-10:]
|
|
516
|
+
], # Last 10 alerts
|
|
517
|
+
"suggestions": [
|
|
518
|
+
asdict(suggestion) for suggestion in self.suggestions[-5:]
|
|
519
|
+
], # Last 5 suggestions
|
|
520
|
+
"monitoring_status": ("active" if self.is_monitoring else "inactive"),
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
def _handle_agent_event(self, event: AgentEvent) -> None:
|
|
524
|
+
"""Handle agent events for performance monitoring."""
|
|
525
|
+
if not self.is_monitoring:
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
if event.event_type == EventType.OPERATION_STARTED:
|
|
530
|
+
# Start tracking operation
|
|
531
|
+
if event.operation:
|
|
532
|
+
self.start_operation_tracking(
|
|
533
|
+
event.operation.operation_id,
|
|
534
|
+
event.operation.operation_type,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
elif event.event_type == EventType.OPERATION_COMPLETED:
|
|
538
|
+
# Complete tracking operation
|
|
539
|
+
if event.operation:
|
|
540
|
+
self.complete_operation_tracking(event.operation.operation_id)
|
|
541
|
+
|
|
542
|
+
elif event.event_type == EventType.OPERATION_FAILED:
|
|
543
|
+
# Record failed operation
|
|
544
|
+
if event.operation:
|
|
545
|
+
self.complete_operation_tracking(event.operation.operation_id)
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.error(f"Error handling agent event: {e}")
|
|
549
|
+
|
|
550
|
+
def _check_token_usage_alerts(self, metrics: TokenUsageMetrics) -> None:
|
|
551
|
+
"""Check for token usage alerts."""
|
|
552
|
+
with self._lock:
|
|
553
|
+
thresholds = self.alert_thresholds["token_usage"]
|
|
554
|
+
|
|
555
|
+
# Check recent usage (last minute)
|
|
556
|
+
recent_usage = self.metrics_collector.get_token_usage_stats(
|
|
557
|
+
timedelta(minutes=1)
|
|
558
|
+
)
|
|
559
|
+
tokens_per_minute = recent_usage["total_tokens"]
|
|
560
|
+
|
|
561
|
+
if tokens_per_minute > thresholds["excessive_usage_per_minute"]:
|
|
562
|
+
self._create_alert(
|
|
563
|
+
AlertLevel.CRITICAL,
|
|
564
|
+
"Excessive Token Usage",
|
|
565
|
+
f"Using {tokens_per_minute:,} tokens per minute (threshold: {thresholds['excessive_usage_per_minute']:,})",
|
|
566
|
+
MetricType.TOKEN_USAGE,
|
|
567
|
+
thresholds["excessive_usage_per_minute"],
|
|
568
|
+
tokens_per_minute,
|
|
569
|
+
provider_name=metrics.provider_name,
|
|
570
|
+
suggested_actions=[
|
|
571
|
+
"Consider switching to a more efficient model",
|
|
572
|
+
"Reduce context size",
|
|
573
|
+
"Implement response caching",
|
|
574
|
+
],
|
|
575
|
+
)
|
|
576
|
+
elif tokens_per_minute > thresholds["high_usage_per_minute"]:
|
|
577
|
+
self._create_alert(
|
|
578
|
+
AlertLevel.WARNING,
|
|
579
|
+
"High Token Usage",
|
|
580
|
+
f"Using {tokens_per_minute:,} tokens per minute (threshold: {thresholds['high_usage_per_minute']:,})",
|
|
581
|
+
MetricType.TOKEN_USAGE,
|
|
582
|
+
thresholds["high_usage_per_minute"],
|
|
583
|
+
tokens_per_minute,
|
|
584
|
+
provider_name=metrics.provider_name,
|
|
585
|
+
suggested_actions=[
|
|
586
|
+
"Monitor usage patterns",
|
|
587
|
+
"Consider optimizing context",
|
|
588
|
+
],
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
def _check_timing_alerts(self, timing: OperationTiming) -> None:
|
|
592
|
+
"""Check for timing-based alerts."""
|
|
593
|
+
with self._lock:
|
|
594
|
+
thresholds = self.alert_thresholds["timing"]
|
|
595
|
+
|
|
596
|
+
if timing.duration_ms > thresholds["very_slow_response_ms"]:
|
|
597
|
+
self._create_alert(
|
|
598
|
+
AlertLevel.WARNING,
|
|
599
|
+
"Very Slow Response",
|
|
600
|
+
f"Operation took {timing.duration_ms:.0f}ms (threshold: {thresholds['very_slow_response_ms']:.0f}ms)",
|
|
601
|
+
MetricType.OPERATION_TIMING,
|
|
602
|
+
thresholds["very_slow_response_ms"],
|
|
603
|
+
timing.duration_ms,
|
|
604
|
+
suggested_actions=[
|
|
605
|
+
"Check network connectivity",
|
|
606
|
+
"Consider switching providers",
|
|
607
|
+
],
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
def _check_context_alerts(self, context: ContextMetrics) -> None:
|
|
611
|
+
"""Check for context efficiency alerts."""
|
|
612
|
+
with self._lock:
|
|
613
|
+
thresholds = self.alert_thresholds["context"]
|
|
614
|
+
efficiency_score = context.efficiency_score
|
|
615
|
+
|
|
616
|
+
if efficiency_score < thresholds["very_low_efficiency_threshold"]:
|
|
617
|
+
self._create_alert(
|
|
618
|
+
AlertLevel.WARNING,
|
|
619
|
+
"Very Low Context Efficiency",
|
|
620
|
+
f"Context efficiency is {efficiency_score:.1f}% (threshold: {thresholds['very_low_efficiency_threshold']:.1f}%)",
|
|
621
|
+
MetricType.CONTEXT_EFFICIENCY,
|
|
622
|
+
thresholds["very_low_efficiency_threshold"],
|
|
623
|
+
efficiency_score,
|
|
624
|
+
suggested_actions=[
|
|
625
|
+
"Reduce context size",
|
|
626
|
+
"Improve context relevance",
|
|
627
|
+
"Consider context compression techniques",
|
|
628
|
+
],
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def _create_alert(
|
|
632
|
+
self,
|
|
633
|
+
level: AlertLevel,
|
|
634
|
+
title: str,
|
|
635
|
+
description: str,
|
|
636
|
+
metric_type: MetricType,
|
|
637
|
+
threshold_value: float,
|
|
638
|
+
current_value: float,
|
|
639
|
+
provider_name: Optional[str] = None,
|
|
640
|
+
persona_name: Optional[str] = None,
|
|
641
|
+
suggested_actions: Optional[List[str]] = None,
|
|
642
|
+
) -> PerformanceAlert:
|
|
643
|
+
"""Create a new performance alert."""
|
|
644
|
+
alert = PerformanceAlert(
|
|
645
|
+
alert_id=f"alert_{int(time.time() * 1000)}",
|
|
646
|
+
level=level,
|
|
647
|
+
title=title,
|
|
648
|
+
description=description,
|
|
649
|
+
metric_type=metric_type,
|
|
650
|
+
threshold_value=threshold_value,
|
|
651
|
+
current_value=current_value,
|
|
652
|
+
provider_name=provider_name,
|
|
653
|
+
persona_name=persona_name,
|
|
654
|
+
suggested_actions=suggested_actions or [],
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
self.alerts.append(alert)
|
|
658
|
+
logger.warning(f"Performance alert created: {title}")
|
|
659
|
+
|
|
660
|
+
# Notify listeners
|
|
661
|
+
for listener in self._event_listeners:
|
|
662
|
+
try:
|
|
663
|
+
listener("performance_alert", alert)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
logger.error(f"Error notifying alert listener: {e}")
|
|
666
|
+
|
|
667
|
+
return alert
|
|
668
|
+
|
|
669
|
+
def add_event_listener(self, listener: Callable) -> None:
|
|
670
|
+
"""Add an event listener for performance events."""
|
|
671
|
+
self._event_listeners.append(listener)
|
|
672
|
+
|
|
673
|
+
def remove_event_listener(self, listener: Callable) -> None:
|
|
674
|
+
"""Remove an event listener."""
|
|
675
|
+
if listener in self._event_listeners:
|
|
676
|
+
self._event_listeners.remove(listener)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
# Global performance monitor instance
|
|
680
|
+
_performance_monitor: Optional[PerformanceMonitor] = None
|
|
681
|
+
_monitor_lock = threading.Lock()
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def get_performance_monitor() -> PerformanceMonitor:
|
|
685
|
+
"""Get the global performance monitor instance."""
|
|
686
|
+
global _performance_monitor
|
|
687
|
+
if _performance_monitor is None:
|
|
688
|
+
with _monitor_lock:
|
|
689
|
+
if _performance_monitor is None:
|
|
690
|
+
_performance_monitor = PerformanceMonitor()
|
|
691
|
+
return _performance_monitor
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def set_performance_monitor(monitor: PerformanceMonitor) -> None:
|
|
695
|
+
"""Set the global performance monitor instance."""
|
|
696
|
+
global _performance_monitor
|
|
697
|
+
with _monitor_lock:
|
|
698
|
+
_performance_monitor = monitor
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def initialize_performance_monitoring(
|
|
702
|
+
auto_start: bool = True,
|
|
703
|
+
) -> PerformanceMonitor:
|
|
704
|
+
"""Initialize the performance monitoring system."""
|
|
705
|
+
monitor = get_performance_monitor()
|
|
706
|
+
if auto_start:
|
|
707
|
+
monitor.start_monitoring()
|
|
708
|
+
return monitor
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def shutdown_performance_monitoring() -> None:
|
|
712
|
+
"""Shutdown the performance monitoring system."""
|
|
713
|
+
global _performance_monitor
|
|
714
|
+
if _performance_monitor:
|
|
715
|
+
_performance_monitor.stop_monitoring()
|
|
716
|
+
with _monitor_lock:
|
|
717
|
+
_performance_monitor = None
|