devsquad 3.6.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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
EnhancedWorker Output Slicing (P1-3)
|
|
5
|
+
|
|
6
|
+
Adds incremental output capability to EnhancedWorker:
|
|
7
|
+
- Splits large outputs into configurable slices
|
|
8
|
+
- Writes intermediate slices to scratchpad for real-time monitoring
|
|
9
|
+
- Provides progress tracking during long-running tasks
|
|
10
|
+
|
|
11
|
+
Spec reference: SPEC_V35_Agent_Skills_Quality_Framework.md Section 7.3
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class OutputSlice:
|
|
24
|
+
"""A single slice of output with metadata."""
|
|
25
|
+
slice_number: int
|
|
26
|
+
total_slices: int
|
|
27
|
+
content: str
|
|
28
|
+
line_start: int
|
|
29
|
+
line_end: int
|
|
30
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
33
|
+
return {
|
|
34
|
+
"slice_number": self.slice_number,
|
|
35
|
+
"total_slices": self.total_slices,
|
|
36
|
+
"line_range": f"{self.line_start}-{self.line_end}",
|
|
37
|
+
"content_length": len(self.content),
|
|
38
|
+
"timestamp": self.timestamp,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class SlicedOutput:
|
|
44
|
+
"""Complete sliced output result."""
|
|
45
|
+
original_output: str
|
|
46
|
+
slices: List[OutputSlice] = field(default_factory=list)
|
|
47
|
+
total_lines: int = 0
|
|
48
|
+
total_slices: int = 0
|
|
49
|
+
was_sliced: bool = False
|
|
50
|
+
|
|
51
|
+
def get_full_output(self) -> str:
|
|
52
|
+
if not self.was_sliced:
|
|
53
|
+
return self.original_output
|
|
54
|
+
return "\n".join(s.content for s in self.slices)
|
|
55
|
+
|
|
56
|
+
def get_slice(self, index: int) -> Optional[OutputSlice]:
|
|
57
|
+
if 0 <= index < len(self.slices):
|
|
58
|
+
return self.slices[index]
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"total_lines": self.total_lines,
|
|
64
|
+
"total_slices": self.total_slices,
|
|
65
|
+
"was_sliced": self.was_sliced,
|
|
66
|
+
"slices": [s.to_dict() for s in self.slices],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OutputSlicer:
|
|
71
|
+
"""
|
|
72
|
+
Splits large outputs into manageable slices.
|
|
73
|
+
|
|
74
|
+
Usage:
|
|
75
|
+
slicer = OutputSlicer(max_slice_lines=100)
|
|
76
|
+
result = slicer.slice_output(large_text)
|
|
77
|
+
print(f"Total: {result.total_slices} slices")
|
|
78
|
+
for s in result.slices:
|
|
79
|
+
print(s.content)
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
DEFAULT_MAX_SLICE_LINES = 100
|
|
83
|
+
SLICE_HEADER_TEMPLATE = "\n--- Slice {current}/{total} (lines {start}-{end}) ---\n"
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
max_slice_lines: int = DEFAULT_MAX_SLICE_LINES,
|
|
88
|
+
include_headers: bool = True,
|
|
89
|
+
write_to_scratchpad: bool = False,
|
|
90
|
+
scratchpad=None,
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
Initialize output slicer.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
max_slice_lines: Maximum lines per slice (default: 100)
|
|
97
|
+
include_headers: Add slice headers to each slice
|
|
98
|
+
write_to_scratchpad: Write intermediate slices to scratchpad
|
|
99
|
+
scratchpad: Scratchpad instance for writing slices
|
|
100
|
+
"""
|
|
101
|
+
self.max_slice_lines = max(max_slice_lines, 10) # Minimum 10 lines
|
|
102
|
+
self.include_headers = include_headers
|
|
103
|
+
self.write_to_scratchpad = write_to_scratchpad
|
|
104
|
+
self._scratchpad = scratchpad
|
|
105
|
+
|
|
106
|
+
def slice_output(
|
|
107
|
+
self,
|
|
108
|
+
output: str,
|
|
109
|
+
task_id: Optional[str] = None,
|
|
110
|
+
role_id: Optional[str] = None,
|
|
111
|
+
) -> SlicedOutput:
|
|
112
|
+
"""
|
|
113
|
+
Split output into slices if it exceeds max_slice_lines.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
output: The full output text to potentially slice
|
|
117
|
+
task_id: Optional task identifier for scratchpad keys
|
|
118
|
+
role_id: Optional role identifier for scratchpad keys
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
SlicedOutput with all metadata
|
|
122
|
+
"""
|
|
123
|
+
if not output or not output.strip():
|
|
124
|
+
return SlicedOutput(
|
|
125
|
+
original_output=output,
|
|
126
|
+
total_lines=0,
|
|
127
|
+
total_slices=0,
|
|
128
|
+
was_sliced=False,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
lines = output.split('\n')
|
|
132
|
+
total_lines = len(lines)
|
|
133
|
+
|
|
134
|
+
if total_lines <= self.max_slice_lines:
|
|
135
|
+
return SlicedOutput(
|
|
136
|
+
original_output=output,
|
|
137
|
+
total_lines=total_lines,
|
|
138
|
+
total_slices=1,
|
|
139
|
+
was_sliced=False,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Calculate number of slices needed
|
|
143
|
+
total_slices = (total_lines + self.max_slice_lines - 1) // self.max_slice_lines
|
|
144
|
+
|
|
145
|
+
slices = []
|
|
146
|
+
for i in range(0, total_lines, self.max_slice_lines):
|
|
147
|
+
slice_num = i // self.max_slice_lines + 1
|
|
148
|
+
line_start = i + 1
|
|
149
|
+
line_end = min(i + self.max_slice_lines, total_lines)
|
|
150
|
+
slice_lines = lines[i:i + self.max_slice_lines]
|
|
151
|
+
|
|
152
|
+
if self.include_headers:
|
|
153
|
+
header = self.SLICE_HEADER_TEMPLATE.format(
|
|
154
|
+
current=slice_num,
|
|
155
|
+
total=total_slices,
|
|
156
|
+
start=line_start,
|
|
157
|
+
end=line_end,
|
|
158
|
+
)
|
|
159
|
+
content = header + '\n'.join(slice_lines)
|
|
160
|
+
else:
|
|
161
|
+
content = '\n'.join(slice_lines)
|
|
162
|
+
|
|
163
|
+
output_slice = OutputSlice(
|
|
164
|
+
slice_number=slice_num,
|
|
165
|
+
total_slices=total_slices,
|
|
166
|
+
content=content,
|
|
167
|
+
line_start=line_start,
|
|
168
|
+
line_end=line_end,
|
|
169
|
+
)
|
|
170
|
+
slices.append(output_slice)
|
|
171
|
+
|
|
172
|
+
# Write to scratchpad if enabled
|
|
173
|
+
if self.write_to_scratchpad and self._scratchpad is not None:
|
|
174
|
+
try:
|
|
175
|
+
key = f"{role_id or 'unknown'}/slice_{slice_num}"
|
|
176
|
+
if task_id:
|
|
177
|
+
key = f"{task_id}/{key}"
|
|
178
|
+
self._scratchpad.write(key, content)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.debug("Failed to write slice %d to scratchpad: %s", slice_num, e)
|
|
181
|
+
|
|
182
|
+
return SlicedOutput(
|
|
183
|
+
original_output=output,
|
|
184
|
+
slices=slices,
|
|
185
|
+
total_lines=total_lines,
|
|
186
|
+
total_slices=total_slices,
|
|
187
|
+
was_sliced=True,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def batch_slice_outputs(
|
|
191
|
+
self,
|
|
192
|
+
outputs: List[str],
|
|
193
|
+
task_ids: Optional[List[str]] = None,
|
|
194
|
+
) -> List[SlicedOutput]:
|
|
195
|
+
"""
|
|
196
|
+
Slice multiple outputs at once.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
outputs: List of output strings to slice
|
|
200
|
+
task_ids: Optional list of task IDs (must match outputs length)
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
List of SlicedOutput results
|
|
204
|
+
"""
|
|
205
|
+
results = []
|
|
206
|
+
for i, output in enumerate(outputs):
|
|
207
|
+
tid = task_ids[i] if task_ids and i < len(task_ids) else None
|
|
208
|
+
result = self.slice_output(output, task_id=tid)
|
|
209
|
+
results.append(result)
|
|
210
|
+
return results
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def create_default_slicer() -> OutputSlicer:
|
|
214
|
+
"""Create slicer with default settings (100 lines per slice)."""
|
|
215
|
+
return OutputSlicer()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def create_compact_slicer() -> OutputSlicer:
|
|
219
|
+
"""Create compact slicer for small context windows (50 lines)."""
|
|
220
|
+
return OutputSlicer(max_slice_lines=50)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def create_large_slicer() -> OutputSlicer:
|
|
224
|
+
"""Create large slicer for detailed outputs (200 lines)."""
|
|
225
|
+
return OutputSlicer(max_slice_lines=200)
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Performance Monitoring Module
|
|
5
|
+
|
|
6
|
+
Tracks and analyzes system performance metrics:
|
|
7
|
+
- Response time monitoring
|
|
8
|
+
- Resource usage tracking
|
|
9
|
+
- Performance bottleneck detection
|
|
10
|
+
- Real-time metrics dashboard
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from scripts.collaboration.performance_monitor import monitor_performance
|
|
14
|
+
|
|
15
|
+
@monitor_performance(name="llm_call")
|
|
16
|
+
def call_llm(prompt: str):
|
|
17
|
+
return response
|
|
18
|
+
|
|
19
|
+
from scripts.collaboration.performance_monitor import get_monitor
|
|
20
|
+
stats = get_monitor().get_stats()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import time
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Callable, Dict, Any, List, Optional
|
|
26
|
+
from functools import wraps
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from collections import defaultdict, deque
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import psutil
|
|
33
|
+
_PSUTIL_AVAILABLE = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
_PSUTIL_AVAILABLE = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class PerformanceMetric:
|
|
43
|
+
"""Performance metric data structure."""
|
|
44
|
+
name: str
|
|
45
|
+
start_time: float
|
|
46
|
+
end_time: float
|
|
47
|
+
duration: float
|
|
48
|
+
cpu_percent: float
|
|
49
|
+
memory_mb: float
|
|
50
|
+
success: bool
|
|
51
|
+
error: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"name": self.name,
|
|
56
|
+
"duration_ms": self.duration * 1000,
|
|
57
|
+
"cpu_percent": self.cpu_percent,
|
|
58
|
+
"memory_mb": self.memory_mb,
|
|
59
|
+
"success": self.success,
|
|
60
|
+
"error": self.error,
|
|
61
|
+
"timestamp": datetime.fromtimestamp(self.start_time).isoformat()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class FunctionStats:
|
|
67
|
+
"""Function execution statistics."""
|
|
68
|
+
name: str
|
|
69
|
+
call_count: int = 0
|
|
70
|
+
success_count: int = 0
|
|
71
|
+
failure_count: int = 0
|
|
72
|
+
total_duration: float = 0.0
|
|
73
|
+
min_duration: float = float('inf')
|
|
74
|
+
max_duration: float = 0.0
|
|
75
|
+
recent_metrics: deque = field(default_factory=lambda: deque(maxlen=100))
|
|
76
|
+
|
|
77
|
+
def add_metric(self, metric: PerformanceMetric):
|
|
78
|
+
"""Add a performance metric."""
|
|
79
|
+
self.call_count += 1
|
|
80
|
+
if metric.success:
|
|
81
|
+
self.success_count += 1
|
|
82
|
+
else:
|
|
83
|
+
self.failure_count += 1
|
|
84
|
+
|
|
85
|
+
self.total_duration += metric.duration
|
|
86
|
+
self.min_duration = min(self.min_duration, metric.duration)
|
|
87
|
+
self.max_duration = max(self.max_duration, metric.duration)
|
|
88
|
+
self.recent_metrics.append(metric)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def avg_duration(self) -> float:
|
|
92
|
+
"""Average execution duration."""
|
|
93
|
+
return self.total_duration / self.call_count if self.call_count > 0 else 0.0
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def success_rate(self) -> float:
|
|
97
|
+
"""Success rate."""
|
|
98
|
+
return self.success_count / self.call_count if self.call_count > 0 else 0.0
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def p95_duration(self) -> float:
|
|
102
|
+
"""P95 response time."""
|
|
103
|
+
if not self.recent_metrics:
|
|
104
|
+
return 0.0
|
|
105
|
+
durations = sorted([m.duration for m in self.recent_metrics])
|
|
106
|
+
idx = int(len(durations) * 0.95)
|
|
107
|
+
return durations[idx] if idx < len(durations) else durations[-1]
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def p99_duration(self) -> float:
|
|
111
|
+
"""P99 response time."""
|
|
112
|
+
if not self.recent_metrics:
|
|
113
|
+
return 0.0
|
|
114
|
+
durations = sorted([m.duration for m in self.recent_metrics])
|
|
115
|
+
idx = int(len(durations) * 0.99)
|
|
116
|
+
return durations[idx] if idx < len(durations) else durations[-1]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _get_cpu_percent() -> float:
|
|
120
|
+
"""Safely get CPU percent, returns 0.0 if psutil unavailable."""
|
|
121
|
+
if not _PSUTIL_AVAILABLE:
|
|
122
|
+
return 0.0
|
|
123
|
+
try:
|
|
124
|
+
return psutil.Process().cpu_percent()
|
|
125
|
+
except Exception:
|
|
126
|
+
return 0.0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _get_memory_mb() -> float:
|
|
130
|
+
"""Safely get memory usage in MB, returns 0.0 if psutil unavailable."""
|
|
131
|
+
if not _PSUTIL_AVAILABLE:
|
|
132
|
+
return 0.0
|
|
133
|
+
try:
|
|
134
|
+
return psutil.Process().memory_info().rss / 1024 / 1024
|
|
135
|
+
except Exception:
|
|
136
|
+
return 0.0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class PerformanceMonitor:
|
|
140
|
+
"""
|
|
141
|
+
Performance Monitor
|
|
142
|
+
|
|
143
|
+
Features:
|
|
144
|
+
- Automatic function execution time tracking
|
|
145
|
+
- CPU and memory usage monitoring
|
|
146
|
+
- P95/P99 response time calculation
|
|
147
|
+
- Performance bottleneck detection
|
|
148
|
+
- Performance report generation
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, max_history: int = 1000):
|
|
152
|
+
"""
|
|
153
|
+
Initialize performance monitor.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
max_history: Maximum number of historical records to retain
|
|
157
|
+
"""
|
|
158
|
+
self.function_stats: Dict[str, FunctionStats] = defaultdict(
|
|
159
|
+
lambda: FunctionStats(name="")
|
|
160
|
+
)
|
|
161
|
+
self.all_metrics: deque = deque(maxlen=max_history)
|
|
162
|
+
self.start_time = time.time()
|
|
163
|
+
|
|
164
|
+
def record_metric(self, metric: PerformanceMetric):
|
|
165
|
+
"""Record a performance metric."""
|
|
166
|
+
self.all_metrics.append(metric)
|
|
167
|
+
|
|
168
|
+
if metric.name not in self.function_stats:
|
|
169
|
+
self.function_stats[metric.name] = FunctionStats(name=metric.name)
|
|
170
|
+
|
|
171
|
+
self.function_stats[metric.name].add_metric(metric)
|
|
172
|
+
|
|
173
|
+
def monitor(self, name: str):
|
|
174
|
+
"""
|
|
175
|
+
Decorator: monitor function performance.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
name: Function name identifier
|
|
179
|
+
"""
|
|
180
|
+
def decorator(func: Callable) -> Callable:
|
|
181
|
+
@wraps(func)
|
|
182
|
+
def wrapper(*args, **kwargs):
|
|
183
|
+
start_time = time.time()
|
|
184
|
+
start_cpu = _get_cpu_percent()
|
|
185
|
+
start_memory = _get_memory_mb()
|
|
186
|
+
|
|
187
|
+
success = True
|
|
188
|
+
error = None
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
result = func(*args, **kwargs)
|
|
192
|
+
return result
|
|
193
|
+
except Exception as e:
|
|
194
|
+
success = False
|
|
195
|
+
error = str(e)
|
|
196
|
+
raise
|
|
197
|
+
finally:
|
|
198
|
+
end_time = time.time()
|
|
199
|
+
end_cpu = _get_cpu_percent()
|
|
200
|
+
end_memory = _get_memory_mb()
|
|
201
|
+
|
|
202
|
+
metric = PerformanceMetric(
|
|
203
|
+
name=name,
|
|
204
|
+
start_time=start_time,
|
|
205
|
+
end_time=end_time,
|
|
206
|
+
duration=end_time - start_time,
|
|
207
|
+
cpu_percent=(start_cpu + end_cpu) / 2,
|
|
208
|
+
memory_mb=(start_memory + end_memory) / 2,
|
|
209
|
+
success=success,
|
|
210
|
+
error=error
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self.record_metric(metric)
|
|
214
|
+
|
|
215
|
+
return wrapper
|
|
216
|
+
return decorator
|
|
217
|
+
|
|
218
|
+
def get_stats(self, function_name: Optional[str] = None) -> Dict[str, Any]:
|
|
219
|
+
"""
|
|
220
|
+
Get statistics (compatible with MonitorProvider Protocol).
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
function_name: If specified, return only that function's stats
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Statistics dictionary
|
|
227
|
+
"""
|
|
228
|
+
if function_name:
|
|
229
|
+
if function_name not in self.function_stats:
|
|
230
|
+
return {}
|
|
231
|
+
|
|
232
|
+
stats = self.function_stats[function_name]
|
|
233
|
+
return {
|
|
234
|
+
"name": stats.name,
|
|
235
|
+
"call_count": stats.call_count,
|
|
236
|
+
"success_count": stats.success_count,
|
|
237
|
+
"failure_count": stats.failure_count,
|
|
238
|
+
"success_rate": f"{stats.success_rate * 100:.1f}%",
|
|
239
|
+
"avg_duration_ms": stats.avg_duration * 1000,
|
|
240
|
+
"min_duration_ms": stats.min_duration * 1000,
|
|
241
|
+
"max_duration_ms": stats.max_duration * 1000,
|
|
242
|
+
"p95_duration_ms": stats.p95_duration * 1000,
|
|
243
|
+
"p99_duration_ms": stats.p99_duration * 1000,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
llm_calls = [s for name, s in self.function_stats.items() if name.startswith("llm_call:")]
|
|
247
|
+
agent_executions = [s for name, s in self.function_stats.items() if name.startswith("agent:")]
|
|
248
|
+
|
|
249
|
+
total_llm_calls = sum(s.call_count for s in llm_calls)
|
|
250
|
+
total_agent_executions = sum(s.call_count for s in agent_executions)
|
|
251
|
+
|
|
252
|
+
avg_llm_duration = (
|
|
253
|
+
sum(s.total_duration for s in llm_calls) / total_llm_calls
|
|
254
|
+
if total_llm_calls > 0 else 0.0
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
avg_agent_duration = (
|
|
258
|
+
sum(s.total_duration for s in agent_executions) / total_agent_executions
|
|
259
|
+
if total_agent_executions > 0 else 0.0
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
"total_llm_calls": total_llm_calls,
|
|
264
|
+
"total_agent_executions": total_agent_executions,
|
|
265
|
+
"avg_llm_duration": avg_llm_duration,
|
|
266
|
+
"avg_agent_duration": avg_agent_duration,
|
|
267
|
+
"total_tokens": 0,
|
|
268
|
+
"uptime_seconds": time.time() - self.start_time,
|
|
269
|
+
"total_metrics": len(self.all_metrics),
|
|
270
|
+
"functions": {
|
|
271
|
+
name: {
|
|
272
|
+
"call_count": stats.call_count,
|
|
273
|
+
"success_rate": f"{stats.success_rate * 100:.1f}%",
|
|
274
|
+
"avg_duration_ms": stats.avg_duration * 1000,
|
|
275
|
+
"p95_duration_ms": stats.p95_duration * 1000,
|
|
276
|
+
}
|
|
277
|
+
for name, stats in self.function_stats.items()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
def get_slowest_functions(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
282
|
+
"""Get the slowest functions by average duration."""
|
|
283
|
+
sorted_funcs = sorted(
|
|
284
|
+
self.function_stats.values(),
|
|
285
|
+
key=lambda s: s.avg_duration,
|
|
286
|
+
reverse=True
|
|
287
|
+
)[:limit]
|
|
288
|
+
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
"name": stats.name,
|
|
292
|
+
"avg_duration_ms": stats.avg_duration * 1000,
|
|
293
|
+
"call_count": stats.call_count,
|
|
294
|
+
"p95_duration_ms": stats.p95_duration * 1000,
|
|
295
|
+
}
|
|
296
|
+
for stats in sorted_funcs
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
def get_bottlenecks(self, threshold_ms: float = 1000) -> List[Dict[str, Any]]:
|
|
300
|
+
"""
|
|
301
|
+
Detect performance bottlenecks.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
threshold_ms: Threshold in milliseconds; functions above this are bottlenecks
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
List of bottleneck entries
|
|
308
|
+
"""
|
|
309
|
+
bottlenecks = []
|
|
310
|
+
|
|
311
|
+
for name, stats in self.function_stats.items():
|
|
312
|
+
if stats.avg_duration * 1000 > threshold_ms:
|
|
313
|
+
bottlenecks.append({
|
|
314
|
+
"name": name,
|
|
315
|
+
"avg_duration_ms": stats.avg_duration * 1000,
|
|
316
|
+
"p95_duration_ms": stats.p95_duration * 1000,
|
|
317
|
+
"call_count": stats.call_count,
|
|
318
|
+
"severity": "high" if stats.avg_duration * 1000 > threshold_ms * 2 else "medium"
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
return sorted(bottlenecks, key=lambda x: x["avg_duration_ms"], reverse=True)
|
|
322
|
+
|
|
323
|
+
def get_recent_errors(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
324
|
+
"""Get recent error entries."""
|
|
325
|
+
errors = [m for m in self.all_metrics if not m.success]
|
|
326
|
+
return [m.to_dict() for m in list(errors)[-limit:]]
|
|
327
|
+
|
|
328
|
+
def record_llm_call(
|
|
329
|
+
self,
|
|
330
|
+
backend: str,
|
|
331
|
+
model: str,
|
|
332
|
+
duration: float,
|
|
333
|
+
token_count: int,
|
|
334
|
+
success: bool,
|
|
335
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Record an LLM API call (implements MonitorProvider Protocol)."""
|
|
338
|
+
metric_name = f"llm_call:{backend}:{model}"
|
|
339
|
+
|
|
340
|
+
metric = PerformanceMetric(
|
|
341
|
+
name=metric_name,
|
|
342
|
+
start_time=time.time() - duration,
|
|
343
|
+
end_time=time.time(),
|
|
344
|
+
duration=duration,
|
|
345
|
+
cpu_percent=_get_cpu_percent(),
|
|
346
|
+
memory_mb=_get_memory_mb(),
|
|
347
|
+
success=success,
|
|
348
|
+
error=metadata.get("error") if metadata else None
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
self.record_metric(metric)
|
|
352
|
+
|
|
353
|
+
def record_agent_execution(
|
|
354
|
+
self,
|
|
355
|
+
agent_role: str,
|
|
356
|
+
task: str,
|
|
357
|
+
duration: float,
|
|
358
|
+
success: bool,
|
|
359
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Record an agent execution (implements MonitorProvider Protocol)."""
|
|
362
|
+
metric_name = f"agent:{agent_role}"
|
|
363
|
+
|
|
364
|
+
metric = PerformanceMetric(
|
|
365
|
+
name=metric_name,
|
|
366
|
+
start_time=time.time() - duration,
|
|
367
|
+
end_time=time.time(),
|
|
368
|
+
duration=duration,
|
|
369
|
+
cpu_percent=_get_cpu_percent(),
|
|
370
|
+
memory_mb=_get_memory_mb(),
|
|
371
|
+
success=success,
|
|
372
|
+
error=metadata.get("error") if metadata else None
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
self.record_metric(metric)
|
|
376
|
+
|
|
377
|
+
def generate_report(self, output_path: str) -> None:
|
|
378
|
+
"""Generate performance report to file (implements MonitorProvider Protocol)."""
|
|
379
|
+
try:
|
|
380
|
+
report = self.export_report()
|
|
381
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
382
|
+
f.write(report)
|
|
383
|
+
logger.info("Performance report generated: %s", output_path)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
logger.error("Failed to generate report: %s", e)
|
|
386
|
+
raise IOError(f"Failed to generate report: {e}")
|
|
387
|
+
|
|
388
|
+
def is_available(self) -> bool:
|
|
389
|
+
"""Check if monitoring is available (implements MonitorProvider Protocol)."""
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
def export_report(self) -> str:
|
|
393
|
+
"""Export performance report as Markdown string."""
|
|
394
|
+
stats = self.get_stats()
|
|
395
|
+
slowest = self.get_slowest_functions(5)
|
|
396
|
+
bottlenecks = self.get_bottlenecks()
|
|
397
|
+
errors = self.get_recent_errors(5)
|
|
398
|
+
|
|
399
|
+
report = f"""# Performance Monitoring Report
|
|
400
|
+
|
|
401
|
+
**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
402
|
+
**Uptime**: {stats['uptime_seconds']:.0f} seconds
|
|
403
|
+
**Total Metrics**: {stats['total_metrics']}
|
|
404
|
+
|
|
405
|
+
## Overall Statistics
|
|
406
|
+
|
|
407
|
+
| Function | Calls | Success Rate | Avg Duration | P95 Duration |
|
|
408
|
+
|----------|-------|--------------|--------------|--------------|
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
for name, func_stats in stats['functions'].items():
|
|
412
|
+
report += f"| {name} | {func_stats['call_count']} | {func_stats['success_rate']} | {func_stats['avg_duration_ms']:.1f}ms | {func_stats['p95_duration_ms']:.1f}ms |\n"
|
|
413
|
+
|
|
414
|
+
if slowest:
|
|
415
|
+
report += "\n## Slowest Functions\n\n"
|
|
416
|
+
for i, func in enumerate(slowest, 1):
|
|
417
|
+
report += f"{i}. **{func['name']}**: {func['avg_duration_ms']:.1f}ms avg ({func['call_count']} calls)\n"
|
|
418
|
+
|
|
419
|
+
if bottlenecks:
|
|
420
|
+
report += "\n## Performance Bottlenecks\n\n"
|
|
421
|
+
for bottleneck in bottlenecks:
|
|
422
|
+
report += f"- **{bottleneck['name']}** ({bottleneck['severity']}): {bottleneck['avg_duration_ms']:.1f}ms avg\n"
|
|
423
|
+
|
|
424
|
+
if errors:
|
|
425
|
+
report += "\n## Recent Errors\n\n"
|
|
426
|
+
for error in errors:
|
|
427
|
+
report += f"- **{error['name']}** at {error['timestamp']}: {error['error']}\n"
|
|
428
|
+
|
|
429
|
+
return report
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
_monitor_instance: Optional[PerformanceMonitor] = None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def get_monitor() -> PerformanceMonitor:
|
|
436
|
+
"""Get or create global monitor instance."""
|
|
437
|
+
global _monitor_instance
|
|
438
|
+
if _monitor_instance is None:
|
|
439
|
+
_monitor_instance = PerformanceMonitor()
|
|
440
|
+
return _monitor_instance
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def monitor_performance(name: str):
|
|
444
|
+
"""
|
|
445
|
+
Decorator: monitor function performance.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
name: Function name identifier
|
|
449
|
+
|
|
450
|
+
Example:
|
|
451
|
+
@monitor_performance("llm_call")
|
|
452
|
+
def call_llm(prompt: str):
|
|
453
|
+
return response
|
|
454
|
+
"""
|
|
455
|
+
monitor = get_monitor()
|
|
456
|
+
return monitor.monitor(name)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def reset_monitor():
|
|
460
|
+
"""Reset global monitor instance (mainly for testing)."""
|
|
461
|
+
global _monitor_instance
|
|
462
|
+
_monitor_instance = None
|