pygeai-orchestration 0.1.0b2__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.
- pygeai_orchestration/__init__.py +99 -0
- pygeai_orchestration/cli/__init__.py +7 -0
- pygeai_orchestration/cli/__main__.py +11 -0
- pygeai_orchestration/cli/commands/__init__.py +13 -0
- pygeai_orchestration/cli/commands/base.py +192 -0
- pygeai_orchestration/cli/error_handler.py +123 -0
- pygeai_orchestration/cli/formatters.py +419 -0
- pygeai_orchestration/cli/geai_orch.py +270 -0
- pygeai_orchestration/cli/interactive.py +265 -0
- pygeai_orchestration/cli/texts/help.py +169 -0
- pygeai_orchestration/core/__init__.py +130 -0
- pygeai_orchestration/core/base/__init__.py +23 -0
- pygeai_orchestration/core/base/agent.py +121 -0
- pygeai_orchestration/core/base/geai_agent.py +144 -0
- pygeai_orchestration/core/base/geai_orchestrator.py +77 -0
- pygeai_orchestration/core/base/orchestrator.py +142 -0
- pygeai_orchestration/core/base/pattern.py +161 -0
- pygeai_orchestration/core/base/tool.py +149 -0
- pygeai_orchestration/core/common/__init__.py +18 -0
- pygeai_orchestration/core/common/context.py +140 -0
- pygeai_orchestration/core/common/memory.py +176 -0
- pygeai_orchestration/core/common/message.py +50 -0
- pygeai_orchestration/core/common/state.py +181 -0
- pygeai_orchestration/core/composition.py +190 -0
- pygeai_orchestration/core/config.py +356 -0
- pygeai_orchestration/core/exceptions.py +400 -0
- pygeai_orchestration/core/handlers.py +380 -0
- pygeai_orchestration/core/utils/__init__.py +37 -0
- pygeai_orchestration/core/utils/cache.py +138 -0
- pygeai_orchestration/core/utils/config.py +94 -0
- pygeai_orchestration/core/utils/logging.py +57 -0
- pygeai_orchestration/core/utils/metrics.py +184 -0
- pygeai_orchestration/core/utils/validators.py +140 -0
- pygeai_orchestration/dev/__init__.py +15 -0
- pygeai_orchestration/dev/debug.py +288 -0
- pygeai_orchestration/dev/templates.py +321 -0
- pygeai_orchestration/dev/testing.py +301 -0
- pygeai_orchestration/patterns/__init__.py +15 -0
- pygeai_orchestration/patterns/multi_agent.py +237 -0
- pygeai_orchestration/patterns/planning.py +219 -0
- pygeai_orchestration/patterns/react.py +221 -0
- pygeai_orchestration/patterns/reflection.py +134 -0
- pygeai_orchestration/patterns/tool_use.py +170 -0
- pygeai_orchestration/tests/__init__.py +1 -0
- pygeai_orchestration/tests/test_base_classes.py +187 -0
- pygeai_orchestration/tests/test_cache.py +184 -0
- pygeai_orchestration/tests/test_cli_formatters.py +232 -0
- pygeai_orchestration/tests/test_common.py +214 -0
- pygeai_orchestration/tests/test_composition.py +265 -0
- pygeai_orchestration/tests/test_config.py +301 -0
- pygeai_orchestration/tests/test_dev_utils.py +337 -0
- pygeai_orchestration/tests/test_exceptions.py +327 -0
- pygeai_orchestration/tests/test_handlers.py +307 -0
- pygeai_orchestration/tests/test_metrics.py +171 -0
- pygeai_orchestration/tests/test_patterns.py +165 -0
- pygeai_orchestration-0.1.0b2.dist-info/METADATA +290 -0
- pygeai_orchestration-0.1.0b2.dist-info/RECORD +61 -0
- pygeai_orchestration-0.1.0b2.dist-info/WHEEL +5 -0
- pygeai_orchestration-0.1.0b2.dist-info/entry_points.txt +2 -0
- pygeai_orchestration-0.1.0b2.dist-info/licenses/LICENSE +8 -0
- pygeai_orchestration-0.1.0b2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MetricType(Enum):
|
|
10
|
+
COUNTER = "counter"
|
|
11
|
+
GAUGE = "gauge"
|
|
12
|
+
HISTOGRAM = "histogram"
|
|
13
|
+
TIMER = "timer"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Metric:
|
|
18
|
+
name: str
|
|
19
|
+
type: MetricType
|
|
20
|
+
value: float
|
|
21
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
22
|
+
labels: dict[str, str] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TimerContext:
|
|
27
|
+
metric_name: str
|
|
28
|
+
labels: dict[str, str]
|
|
29
|
+
collector: "MetricsCollector"
|
|
30
|
+
start_time: float = field(default_factory=time.perf_counter)
|
|
31
|
+
|
|
32
|
+
def __enter__(self):
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
36
|
+
elapsed = time.perf_counter() - self.start_time
|
|
37
|
+
self.collector.record_timer(self.metric_name, elapsed, labels=self.labels)
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MetricsCollector:
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self._metrics: list[Metric] = []
|
|
44
|
+
self._counters: dict[str, float] = defaultdict(float)
|
|
45
|
+
self._gauges: dict[str, float] = {}
|
|
46
|
+
self._histograms: dict[str, list[float]] = defaultdict(list)
|
|
47
|
+
self._timers: dict[str, list[float]] = defaultdict(list)
|
|
48
|
+
|
|
49
|
+
def increment(self, name: str, value: float = 1.0, labels: Optional[dict[str, str]] = None) -> None:
|
|
50
|
+
key = self._make_key(name, labels)
|
|
51
|
+
self._counters[key] += value
|
|
52
|
+
self._record_metric(name, MetricType.COUNTER, self._counters[key], labels)
|
|
53
|
+
|
|
54
|
+
def set_gauge(self, name: str, value: float, labels: Optional[dict[str, str]] = None) -> None:
|
|
55
|
+
key = self._make_key(name, labels)
|
|
56
|
+
self._gauges[key] = value
|
|
57
|
+
self._record_metric(name, MetricType.GAUGE, value, labels)
|
|
58
|
+
|
|
59
|
+
def record_histogram(self, name: str, value: float, labels: Optional[dict[str, str]] = None) -> None:
|
|
60
|
+
key = self._make_key(name, labels)
|
|
61
|
+
self._histograms[key].append(value)
|
|
62
|
+
self._record_metric(name, MetricType.HISTOGRAM, value, labels)
|
|
63
|
+
|
|
64
|
+
def record_timer(self, name: str, duration: float, labels: Optional[dict[str, str]] = None) -> None:
|
|
65
|
+
key = self._make_key(name, labels)
|
|
66
|
+
self._timers[key].append(duration)
|
|
67
|
+
self._record_metric(name, MetricType.TIMER, duration, labels)
|
|
68
|
+
|
|
69
|
+
def timer(self, name: str, labels: Optional[dict[str, str]] = None) -> TimerContext:
|
|
70
|
+
return TimerContext(name, labels or {}, self)
|
|
71
|
+
|
|
72
|
+
def _make_key(self, name: str, labels: Optional[dict[str, str]]) -> str:
|
|
73
|
+
if not labels:
|
|
74
|
+
return name
|
|
75
|
+
label_str = ",".join(f"{k}={v}" for k, v in sorted(labels.items()))
|
|
76
|
+
return f"{name}{{{label_str}}}"
|
|
77
|
+
|
|
78
|
+
def _record_metric(
|
|
79
|
+
self, name: str, metric_type: MetricType, value: float, labels: Optional[dict[str, str]]
|
|
80
|
+
) -> None:
|
|
81
|
+
metric = Metric(
|
|
82
|
+
name=name, type=metric_type, value=value, labels=labels or {}
|
|
83
|
+
)
|
|
84
|
+
self._metrics.append(metric)
|
|
85
|
+
|
|
86
|
+
def get_counter(self, name: str, labels: Optional[dict[str, str]] = None) -> float:
|
|
87
|
+
key = self._make_key(name, labels)
|
|
88
|
+
return self._counters.get(key, 0.0)
|
|
89
|
+
|
|
90
|
+
def get_gauge(self, name: str, labels: Optional[dict[str, str]] = None) -> Optional[float]:
|
|
91
|
+
key = self._make_key(name, labels)
|
|
92
|
+
return self._gauges.get(key)
|
|
93
|
+
|
|
94
|
+
def get_histogram_stats(self, name: str, labels: Optional[dict[str, str]] = None) -> dict[str, float]:
|
|
95
|
+
key = self._make_key(name, labels)
|
|
96
|
+
values = self._histograms.get(key, [])
|
|
97
|
+
if not values:
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
sorted_values = sorted(values)
|
|
101
|
+
return {
|
|
102
|
+
"count": len(values),
|
|
103
|
+
"sum": sum(values),
|
|
104
|
+
"min": min(values),
|
|
105
|
+
"max": max(values),
|
|
106
|
+
"mean": sum(values) / len(values),
|
|
107
|
+
"p50": self._percentile(sorted_values, 50),
|
|
108
|
+
"p95": self._percentile(sorted_values, 95),
|
|
109
|
+
"p99": self._percentile(sorted_values, 99),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def get_timer_stats(self, name: str, labels: Optional[dict[str, str]] = None) -> dict[str, float]:
|
|
113
|
+
key = self._make_key(name, labels)
|
|
114
|
+
values = self._timers.get(key, [])
|
|
115
|
+
if not values:
|
|
116
|
+
return {}
|
|
117
|
+
|
|
118
|
+
sorted_values = sorted(values)
|
|
119
|
+
return {
|
|
120
|
+
"count": len(values),
|
|
121
|
+
"sum": sum(values),
|
|
122
|
+
"min": min(values),
|
|
123
|
+
"max": max(values),
|
|
124
|
+
"mean": sum(values) / len(values),
|
|
125
|
+
"p50": self._percentile(sorted_values, 50),
|
|
126
|
+
"p95": self._percentile(sorted_values, 95),
|
|
127
|
+
"p99": self._percentile(sorted_values, 99),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def _percentile(self, sorted_values: list[float], percentile: int) -> float:
|
|
131
|
+
if not sorted_values:
|
|
132
|
+
return 0.0
|
|
133
|
+
index = int(len(sorted_values) * percentile / 100)
|
|
134
|
+
return sorted_values[min(index, len(sorted_values) - 1)]
|
|
135
|
+
|
|
136
|
+
def get_all_metrics(self) -> list[Metric]:
|
|
137
|
+
return self._metrics.copy()
|
|
138
|
+
|
|
139
|
+
def get_summary(self) -> dict[str, Any]:
|
|
140
|
+
return {
|
|
141
|
+
"counters": dict(self._counters),
|
|
142
|
+
"gauges": dict(self._gauges),
|
|
143
|
+
"histograms": {
|
|
144
|
+
name: self.get_histogram_stats(name.split("{")[0], self._parse_labels(name))
|
|
145
|
+
for name in self._histograms.keys()
|
|
146
|
+
},
|
|
147
|
+
"timers": {
|
|
148
|
+
name: self.get_timer_stats(name.split("{")[0], self._parse_labels(name))
|
|
149
|
+
for name in self._timers.keys()
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def _parse_labels(self, key: str) -> Optional[dict[str, str]]:
|
|
154
|
+
if "{" not in key:
|
|
155
|
+
return None
|
|
156
|
+
label_str = key.split("{")[1].rstrip("}")
|
|
157
|
+
if not label_str:
|
|
158
|
+
return None
|
|
159
|
+
labels = {}
|
|
160
|
+
for pair in label_str.split(","):
|
|
161
|
+
k, v = pair.split("=")
|
|
162
|
+
labels[k] = v
|
|
163
|
+
return labels
|
|
164
|
+
|
|
165
|
+
def clear(self) -> None:
|
|
166
|
+
self._metrics.clear()
|
|
167
|
+
self._counters.clear()
|
|
168
|
+
self._gauges.clear()
|
|
169
|
+
self._histograms.clear()
|
|
170
|
+
self._timers.clear()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class GlobalMetrics:
|
|
174
|
+
_instance: Optional[MetricsCollector] = None
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def get_collector(cls) -> MetricsCollector:
|
|
178
|
+
if cls._instance is None:
|
|
179
|
+
cls._instance = MetricsCollector()
|
|
180
|
+
return cls._instance
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def reset(cls) -> None:
|
|
184
|
+
cls._instance = None
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation utilities for configuration validation.
|
|
3
|
+
|
|
4
|
+
This module provides validation functions for agent, pattern, and tool
|
|
5
|
+
configurations, ensuring correctness before instantiation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
from pydantic import BaseModel, ValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ValidationResult(BaseModel):
|
|
13
|
+
"""
|
|
14
|
+
Result of configuration validation.
|
|
15
|
+
|
|
16
|
+
:param valid: bool - Whether validation passed.
|
|
17
|
+
:param errors: List[str] - Validation errors (blocking).
|
|
18
|
+
:param warnings: List[str] - Validation warnings (non-blocking).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
valid: bool
|
|
22
|
+
errors: List[str] = []
|
|
23
|
+
warnings: List[str] = []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_agent_config(config: Dict[str, Any]) -> ValidationResult:
|
|
27
|
+
"""
|
|
28
|
+
Validate agent configuration dictionary.
|
|
29
|
+
|
|
30
|
+
Checks for required fields and validates parameter ranges for
|
|
31
|
+
agent configuration before creating an AgentConfig instance.
|
|
32
|
+
|
|
33
|
+
:param config: Dict[str, Any] - Agent configuration to validate.
|
|
34
|
+
:return: ValidationResult - Validation outcome with errors/warnings.
|
|
35
|
+
"""
|
|
36
|
+
errors = []
|
|
37
|
+
warnings = []
|
|
38
|
+
|
|
39
|
+
required_fields = ["name", "model"]
|
|
40
|
+
for field in required_fields:
|
|
41
|
+
if field not in config:
|
|
42
|
+
errors.append(f"Missing required field: {field}")
|
|
43
|
+
|
|
44
|
+
if "temperature" in config:
|
|
45
|
+
temp = config["temperature"]
|
|
46
|
+
if not isinstance(temp, (int, float)) or temp < 0 or temp > 2:
|
|
47
|
+
errors.append("Temperature must be between 0 and 2")
|
|
48
|
+
|
|
49
|
+
if "max_tokens" in config:
|
|
50
|
+
tokens = config["max_tokens"]
|
|
51
|
+
if not isinstance(tokens, int) or tokens < 1:
|
|
52
|
+
errors.append("Max tokens must be a positive integer")
|
|
53
|
+
|
|
54
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors, warnings=warnings)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_pattern_config(config: Dict[str, Any]) -> ValidationResult:
|
|
58
|
+
"""
|
|
59
|
+
Validate pattern configuration dictionary.
|
|
60
|
+
|
|
61
|
+
Checks for required fields and validates parameter ranges for
|
|
62
|
+
pattern configuration before creating a PatternConfig instance.
|
|
63
|
+
|
|
64
|
+
:param config: Dict[str, Any] - Pattern configuration to validate.
|
|
65
|
+
:return: ValidationResult - Validation outcome with errors/warnings.
|
|
66
|
+
"""
|
|
67
|
+
errors = []
|
|
68
|
+
warnings = []
|
|
69
|
+
|
|
70
|
+
required_fields = ["name", "pattern_type"]
|
|
71
|
+
for field in required_fields:
|
|
72
|
+
if field not in config:
|
|
73
|
+
errors.append(f"Missing required field: {field}")
|
|
74
|
+
|
|
75
|
+
if "max_iterations" in config:
|
|
76
|
+
iterations = config["max_iterations"]
|
|
77
|
+
if not isinstance(iterations, int) or iterations < 1:
|
|
78
|
+
errors.append("Max iterations must be a positive integer")
|
|
79
|
+
elif iterations > 100:
|
|
80
|
+
warnings.append("Max iterations > 100 may cause performance issues")
|
|
81
|
+
|
|
82
|
+
if "timeout" in config:
|
|
83
|
+
timeout = config["timeout"]
|
|
84
|
+
if timeout is not None and (not isinstance(timeout, (int, float)) or timeout <= 0):
|
|
85
|
+
errors.append("Timeout must be a positive number")
|
|
86
|
+
|
|
87
|
+
valid_pattern_types = ["reflection", "tool_use", "react", "planning", "multi_agent"]
|
|
88
|
+
if "pattern_type" in config and config["pattern_type"] not in valid_pattern_types:
|
|
89
|
+
errors.append(f"Invalid pattern_type. Must be one of: {', '.join(valid_pattern_types)}")
|
|
90
|
+
|
|
91
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors, warnings=warnings)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def validate_tool_config(config: Dict[str, Any]) -> ValidationResult:
|
|
95
|
+
"""
|
|
96
|
+
Validate tool configuration dictionary.
|
|
97
|
+
|
|
98
|
+
Checks for required fields and validates parameter ranges for
|
|
99
|
+
tool configuration before creating a ToolConfig instance.
|
|
100
|
+
|
|
101
|
+
:param config: Dict[str, Any] - Tool configuration to validate.
|
|
102
|
+
:return: ValidationResult - Validation outcome with errors/warnings.
|
|
103
|
+
"""
|
|
104
|
+
errors = []
|
|
105
|
+
warnings = []
|
|
106
|
+
|
|
107
|
+
required_fields = ["name", "description"]
|
|
108
|
+
for field in required_fields:
|
|
109
|
+
if field not in config:
|
|
110
|
+
errors.append(f"Missing required field: {field}")
|
|
111
|
+
|
|
112
|
+
if "timeout" in config:
|
|
113
|
+
timeout = config["timeout"]
|
|
114
|
+
if timeout is not None and (not isinstance(timeout, (int, float)) or timeout <= 0):
|
|
115
|
+
errors.append("Timeout must be a positive number")
|
|
116
|
+
|
|
117
|
+
valid_categories = ["search", "computation", "data_access", "communication", "custom"]
|
|
118
|
+
if "category" in config and config["category"] not in valid_categories:
|
|
119
|
+
errors.append(f"Invalid category. Must be one of: {', '.join(valid_categories)}")
|
|
120
|
+
|
|
121
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors, warnings=warnings)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def validate_pydantic_model(model_class: type[BaseModel], data: Dict[str, Any]) -> ValidationResult:
|
|
125
|
+
"""
|
|
126
|
+
Validate data against a Pydantic model.
|
|
127
|
+
|
|
128
|
+
Attempts to instantiate the model with the provided data and
|
|
129
|
+
captures any validation errors.
|
|
130
|
+
|
|
131
|
+
:param model_class: type[BaseModel] - Pydantic model class to validate against.
|
|
132
|
+
:param data: Dict[str, Any] - Data to validate.
|
|
133
|
+
:return: ValidationResult - Validation outcome with errors.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
model_class(**data)
|
|
137
|
+
return ValidationResult(valid=True)
|
|
138
|
+
except ValidationError as e:
|
|
139
|
+
errors = [f"{err['loc'][0]}: {err['msg']}" for err in e.errors()]
|
|
140
|
+
return ValidationResult(valid=False, errors=errors)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Development utilities for PyGEAI Orchestration."""
|
|
2
|
+
|
|
3
|
+
from pygeai_orchestration.dev.debug import DebugTracer, PatternInspector
|
|
4
|
+
from pygeai_orchestration.dev.templates import PatternTemplate, TemplateGenerator
|
|
5
|
+
from pygeai_orchestration.dev.testing import MockAgent, PatternTestCase, create_test_pattern
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DebugTracer",
|
|
9
|
+
"PatternInspector",
|
|
10
|
+
"PatternTemplate",
|
|
11
|
+
"TemplateGenerator",
|
|
12
|
+
"MockAgent",
|
|
13
|
+
"PatternTestCase",
|
|
14
|
+
"create_test_pattern",
|
|
15
|
+
]
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Debugging utilities for pattern development."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import time
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from pygeai_orchestration.core.base import BasePattern, PatternResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DebugTracer:
|
|
12
|
+
"""Trace pattern execution for debugging."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, enabled: bool = True):
|
|
15
|
+
"""Initialize debug tracer.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
enabled: Enable tracing
|
|
19
|
+
"""
|
|
20
|
+
self.enabled = enabled
|
|
21
|
+
self.traces: List[Dict[str, Any]] = []
|
|
22
|
+
|
|
23
|
+
def trace(
|
|
24
|
+
self,
|
|
25
|
+
event: str,
|
|
26
|
+
pattern: Optional[str] = None,
|
|
27
|
+
data: Optional[Dict[str, Any]] = None
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Record a trace event.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
event: Event description
|
|
33
|
+
pattern: Pattern name
|
|
34
|
+
data: Additional data
|
|
35
|
+
"""
|
|
36
|
+
if not self.enabled:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
trace_entry = {
|
|
40
|
+
"timestamp": time.time(),
|
|
41
|
+
"event": event,
|
|
42
|
+
"pattern": pattern,
|
|
43
|
+
"data": data or {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
self.traces.append(trace_entry)
|
|
47
|
+
|
|
48
|
+
def get_traces(self, pattern: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
49
|
+
"""Get recorded traces.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
pattern: Filter by pattern name
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of trace entries
|
|
56
|
+
"""
|
|
57
|
+
if pattern:
|
|
58
|
+
return [t for t in self.traces if t.get("pattern") == pattern]
|
|
59
|
+
return self.traces
|
|
60
|
+
|
|
61
|
+
def clear(self) -> None:
|
|
62
|
+
"""Clear all traces."""
|
|
63
|
+
self.traces.clear()
|
|
64
|
+
|
|
65
|
+
def format_traces(self, pattern: Optional[str] = None) -> str:
|
|
66
|
+
"""Format traces as readable string.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
pattern: Filter by pattern name
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Formatted trace output
|
|
73
|
+
"""
|
|
74
|
+
traces = self.get_traces(pattern)
|
|
75
|
+
|
|
76
|
+
if not traces:
|
|
77
|
+
return "No traces recorded"
|
|
78
|
+
|
|
79
|
+
lines = ["Debug Traces:", "=" * 60]
|
|
80
|
+
|
|
81
|
+
for i, trace in enumerate(traces, 1):
|
|
82
|
+
timestamp = time.strftime(
|
|
83
|
+
"%H:%M:%S",
|
|
84
|
+
time.localtime(trace["timestamp"])
|
|
85
|
+
)
|
|
86
|
+
event = trace["event"]
|
|
87
|
+
pattern_name = trace.get("pattern", "N/A")
|
|
88
|
+
|
|
89
|
+
lines.append(f"\n[{i}] {timestamp} - {pattern_name}")
|
|
90
|
+
lines.append(f" Event: {event}")
|
|
91
|
+
|
|
92
|
+
if trace.get("data"):
|
|
93
|
+
lines.append(" Data:")
|
|
94
|
+
for key, value in trace["data"].items():
|
|
95
|
+
value_str = str(value)[:100]
|
|
96
|
+
lines.append(f" {key}: {value_str}")
|
|
97
|
+
|
|
98
|
+
return "\n".join(lines)
|
|
99
|
+
|
|
100
|
+
@contextmanager
|
|
101
|
+
def trace_execution(self, pattern_name: str, task: str):
|
|
102
|
+
"""Context manager to trace pattern execution.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
pattern_name: Pattern name
|
|
106
|
+
task: Task being executed
|
|
107
|
+
|
|
108
|
+
Yields:
|
|
109
|
+
Tracer instance
|
|
110
|
+
"""
|
|
111
|
+
self.trace("execution_start", pattern_name, {"task": task})
|
|
112
|
+
start_time = time.time()
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
yield self
|
|
116
|
+
except Exception as e:
|
|
117
|
+
self.trace(
|
|
118
|
+
"execution_error",
|
|
119
|
+
pattern_name,
|
|
120
|
+
{"error": str(e), "type": type(e).__name__}
|
|
121
|
+
)
|
|
122
|
+
raise
|
|
123
|
+
finally:
|
|
124
|
+
duration = time.time() - start_time
|
|
125
|
+
self.trace(
|
|
126
|
+
"execution_end",
|
|
127
|
+
pattern_name,
|
|
128
|
+
{"duration": duration}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class PatternInspector:
|
|
133
|
+
"""Inspect and analyze patterns."""
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def inspect_pattern(pattern: BasePattern) -> Dict[str, Any]:
|
|
137
|
+
"""Inspect pattern details.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
pattern: Pattern to inspect
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Pattern inspection data
|
|
144
|
+
"""
|
|
145
|
+
pattern_class = type(pattern)
|
|
146
|
+
|
|
147
|
+
methods = []
|
|
148
|
+
for name, method in inspect.getmembers(pattern_class, predicate=inspect.isfunction):
|
|
149
|
+
if not name.startswith("_"):
|
|
150
|
+
sig = inspect.signature(method)
|
|
151
|
+
methods.append({
|
|
152
|
+
"name": name,
|
|
153
|
+
"signature": str(sig),
|
|
154
|
+
"is_async": inspect.iscoroutinefunction(method)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"class_name": pattern_class.__name__,
|
|
159
|
+
"module": pattern_class.__module__,
|
|
160
|
+
"config": pattern.config.model_dump() if hasattr(pattern, "config") else {},
|
|
161
|
+
"methods": methods,
|
|
162
|
+
"docstring": inspect.getdoc(pattern_class),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def format_inspection(inspection: Dict[str, Any]) -> str:
|
|
167
|
+
"""Format inspection data as readable string.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
inspection: Inspection data
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Formatted output
|
|
174
|
+
"""
|
|
175
|
+
lines = [
|
|
176
|
+
f"Pattern: {inspection['class_name']}",
|
|
177
|
+
f"Module: {inspection['module']}",
|
|
178
|
+
"=" * 60,
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
if inspection.get("docstring"):
|
|
182
|
+
lines.append(f"\n{inspection['docstring']}\n")
|
|
183
|
+
|
|
184
|
+
lines.append("\nConfiguration:")
|
|
185
|
+
for key, value in inspection.get("config", {}).items():
|
|
186
|
+
lines.append(f" {key}: {value}")
|
|
187
|
+
|
|
188
|
+
lines.append("\nMethods:")
|
|
189
|
+
for method in inspection.get("methods", []):
|
|
190
|
+
async_marker = " (async)" if method["is_async"] else ""
|
|
191
|
+
lines.append(f" {method['name']}{method['signature']}{async_marker}")
|
|
192
|
+
|
|
193
|
+
return "\n".join(lines)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def compare_results(result1: PatternResult, result2: PatternResult) -> Dict[str, Any]:
|
|
197
|
+
"""Compare two pattern results.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
result1: First result
|
|
201
|
+
result2: Second result
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Comparison data
|
|
205
|
+
"""
|
|
206
|
+
return {
|
|
207
|
+
"success_match": result1.success == result2.success,
|
|
208
|
+
"result_match": result1.result == result2.result,
|
|
209
|
+
"iterations_diff": result1.iterations - result2.iterations,
|
|
210
|
+
"metadata_diff": {
|
|
211
|
+
"added": set(result2.metadata.keys()) - set(result1.metadata.keys()),
|
|
212
|
+
"removed": set(result1.metadata.keys()) - set(result2.metadata.keys()),
|
|
213
|
+
"changed": {
|
|
214
|
+
k for k in result1.metadata.keys() & result2.metadata.keys()
|
|
215
|
+
if result1.metadata[k] != result2.metadata[k]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def trace_method(tracer: DebugTracer):
|
|
222
|
+
"""Decorator to trace method calls.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
tracer: Debug tracer instance
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Decorator function
|
|
229
|
+
"""
|
|
230
|
+
def decorator(func: Callable) -> Callable:
|
|
231
|
+
async def async_wrapper(*args, **kwargs):
|
|
232
|
+
pattern_name = args[0].__class__.__name__ if args else "Unknown"
|
|
233
|
+
func_name = func.__name__
|
|
234
|
+
|
|
235
|
+
tracer.trace(
|
|
236
|
+
f"method_call: {func_name}",
|
|
237
|
+
pattern_name,
|
|
238
|
+
{"args": str(args[1:])[:100], "kwargs": str(kwargs)[:100]}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
result = await func(*args, **kwargs)
|
|
243
|
+
tracer.trace(
|
|
244
|
+
f"method_return: {func_name}",
|
|
245
|
+
pattern_name,
|
|
246
|
+
{"result": str(result)[:100]}
|
|
247
|
+
)
|
|
248
|
+
return result
|
|
249
|
+
except Exception as e:
|
|
250
|
+
tracer.trace(
|
|
251
|
+
f"method_error: {func_name}",
|
|
252
|
+
pattern_name,
|
|
253
|
+
{"error": str(e)}
|
|
254
|
+
)
|
|
255
|
+
raise
|
|
256
|
+
|
|
257
|
+
def sync_wrapper(*args, **kwargs):
|
|
258
|
+
pattern_name = args[0].__class__.__name__ if args else "Unknown"
|
|
259
|
+
func_name = func.__name__
|
|
260
|
+
|
|
261
|
+
tracer.trace(
|
|
262
|
+
f"method_call: {func_name}",
|
|
263
|
+
pattern_name,
|
|
264
|
+
{"args": str(args[1:])[:100], "kwargs": str(kwargs)[:100]}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
result = func(*args, **kwargs)
|
|
269
|
+
tracer.trace(
|
|
270
|
+
f"method_return: {func_name}",
|
|
271
|
+
pattern_name,
|
|
272
|
+
{"result": str(result)[:100]}
|
|
273
|
+
)
|
|
274
|
+
return result
|
|
275
|
+
except Exception as e:
|
|
276
|
+
tracer.trace(
|
|
277
|
+
f"method_error: {func_name}",
|
|
278
|
+
pattern_name,
|
|
279
|
+
{"error": str(e)}
|
|
280
|
+
)
|
|
281
|
+
raise
|
|
282
|
+
|
|
283
|
+
if inspect.iscoroutinefunction(func):
|
|
284
|
+
return async_wrapper
|
|
285
|
+
else:
|
|
286
|
+
return sync_wrapper
|
|
287
|
+
|
|
288
|
+
return decorator
|