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.
Files changed (61) hide show
  1. pygeai_orchestration/__init__.py +99 -0
  2. pygeai_orchestration/cli/__init__.py +7 -0
  3. pygeai_orchestration/cli/__main__.py +11 -0
  4. pygeai_orchestration/cli/commands/__init__.py +13 -0
  5. pygeai_orchestration/cli/commands/base.py +192 -0
  6. pygeai_orchestration/cli/error_handler.py +123 -0
  7. pygeai_orchestration/cli/formatters.py +419 -0
  8. pygeai_orchestration/cli/geai_orch.py +270 -0
  9. pygeai_orchestration/cli/interactive.py +265 -0
  10. pygeai_orchestration/cli/texts/help.py +169 -0
  11. pygeai_orchestration/core/__init__.py +130 -0
  12. pygeai_orchestration/core/base/__init__.py +23 -0
  13. pygeai_orchestration/core/base/agent.py +121 -0
  14. pygeai_orchestration/core/base/geai_agent.py +144 -0
  15. pygeai_orchestration/core/base/geai_orchestrator.py +77 -0
  16. pygeai_orchestration/core/base/orchestrator.py +142 -0
  17. pygeai_orchestration/core/base/pattern.py +161 -0
  18. pygeai_orchestration/core/base/tool.py +149 -0
  19. pygeai_orchestration/core/common/__init__.py +18 -0
  20. pygeai_orchestration/core/common/context.py +140 -0
  21. pygeai_orchestration/core/common/memory.py +176 -0
  22. pygeai_orchestration/core/common/message.py +50 -0
  23. pygeai_orchestration/core/common/state.py +181 -0
  24. pygeai_orchestration/core/composition.py +190 -0
  25. pygeai_orchestration/core/config.py +356 -0
  26. pygeai_orchestration/core/exceptions.py +400 -0
  27. pygeai_orchestration/core/handlers.py +380 -0
  28. pygeai_orchestration/core/utils/__init__.py +37 -0
  29. pygeai_orchestration/core/utils/cache.py +138 -0
  30. pygeai_orchestration/core/utils/config.py +94 -0
  31. pygeai_orchestration/core/utils/logging.py +57 -0
  32. pygeai_orchestration/core/utils/metrics.py +184 -0
  33. pygeai_orchestration/core/utils/validators.py +140 -0
  34. pygeai_orchestration/dev/__init__.py +15 -0
  35. pygeai_orchestration/dev/debug.py +288 -0
  36. pygeai_orchestration/dev/templates.py +321 -0
  37. pygeai_orchestration/dev/testing.py +301 -0
  38. pygeai_orchestration/patterns/__init__.py +15 -0
  39. pygeai_orchestration/patterns/multi_agent.py +237 -0
  40. pygeai_orchestration/patterns/planning.py +219 -0
  41. pygeai_orchestration/patterns/react.py +221 -0
  42. pygeai_orchestration/patterns/reflection.py +134 -0
  43. pygeai_orchestration/patterns/tool_use.py +170 -0
  44. pygeai_orchestration/tests/__init__.py +1 -0
  45. pygeai_orchestration/tests/test_base_classes.py +187 -0
  46. pygeai_orchestration/tests/test_cache.py +184 -0
  47. pygeai_orchestration/tests/test_cli_formatters.py +232 -0
  48. pygeai_orchestration/tests/test_common.py +214 -0
  49. pygeai_orchestration/tests/test_composition.py +265 -0
  50. pygeai_orchestration/tests/test_config.py +301 -0
  51. pygeai_orchestration/tests/test_dev_utils.py +337 -0
  52. pygeai_orchestration/tests/test_exceptions.py +327 -0
  53. pygeai_orchestration/tests/test_handlers.py +307 -0
  54. pygeai_orchestration/tests/test_metrics.py +171 -0
  55. pygeai_orchestration/tests/test_patterns.py +165 -0
  56. pygeai_orchestration-0.1.0b2.dist-info/METADATA +290 -0
  57. pygeai_orchestration-0.1.0b2.dist-info/RECORD +61 -0
  58. pygeai_orchestration-0.1.0b2.dist-info/WHEEL +5 -0
  59. pygeai_orchestration-0.1.0b2.dist-info/entry_points.txt +2 -0
  60. pygeai_orchestration-0.1.0b2.dist-info/licenses/LICENSE +8 -0
  61. 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