amacs 0.2.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.
Files changed (59) hide show
  1. amacs/__init__.py +88 -0
  2. amacs/adaptive/__init__.py +32 -0
  3. amacs/adaptive/adaptation_engine.py +116 -0
  4. amacs/adaptive/evaluator.py +210 -0
  5. amacs/adaptive/monitor.py +153 -0
  6. amacs/adaptive/reconfigurator.py +271 -0
  7. amacs/agents/__init__.py +17 -0
  8. amacs/agents/analysis_agent.py +23 -0
  9. amacs/agents/base_agent.py +648 -0
  10. amacs/agents/search_agent.py +41 -0
  11. amacs/agents/validator_agent.py +23 -0
  12. amacs/agents/writer_agent.py +23 -0
  13. amacs/aggregation/__init__.py +146 -0
  14. amacs/aggregation/aggregator.py +5 -0
  15. amacs/benchmarks/__init__.py +7 -0
  16. amacs/benchmarks/benchmark_suite.py +57 -0
  17. amacs/cache.py +95 -0
  18. amacs/cli.py +72 -0
  19. amacs/communication/__init__.py +122 -0
  20. amacs/communication/bus.py +5 -0
  21. amacs/config.py +174 -0
  22. amacs/context_builder.py +128 -0
  23. amacs/coordination/__init__.py +13 -0
  24. amacs/coordination/base.py +57 -0
  25. amacs/coordination/debate.py +109 -0
  26. amacs/coordination/manager_worker.py +97 -0
  27. amacs/decorator.py +59 -0
  28. amacs/exceptions.py +197 -0
  29. amacs/executor.py +587 -0
  30. amacs/integrations/__init__.py +17 -0
  31. amacs/integrations/llm_providers.py +845 -0
  32. amacs/integrations/monitoring.py +82 -0
  33. amacs/integrations/vector_db.py +129 -0
  34. amacs/orchestrator/__init__.py +21 -0
  35. amacs/orchestrator/agent_selector.py +87 -0
  36. amacs/orchestrator/llm_decomposer.py +95 -0
  37. amacs/orchestrator/scheduler.py +195 -0
  38. amacs/orchestrator/task_analyzer.py +167 -0
  39. amacs/orchestrator/task_decomposer.py +151 -0
  40. amacs/pipeline.py +320 -0
  41. amacs/pricing.py +65 -0
  42. amacs/results.py +86 -0
  43. amacs/strategy.py +147 -0
  44. amacs/tools/__init__.py +40 -0
  45. amacs/tools/base_tool.py +49 -0
  46. amacs/tools/builtins.py +218 -0
  47. amacs/tracing.py +157 -0
  48. amacs-0.2.0.dist-info/METADATA +268 -0
  49. amacs-0.2.0.dist-info/RECORD +59 -0
  50. amacs-0.2.0.dist-info/WHEEL +5 -0
  51. amacs-0.2.0.dist-info/entry_points.txt +2 -0
  52. amacs-0.2.0.dist-info/licenses/LICENSE +21 -0
  53. amacs-0.2.0.dist-info/top_level.txt +2 -0
  54. benchmarks/__init__.py +7 -0
  55. benchmarks/benchmark_suite.py +10 -0
  56. benchmarks/report.py +79 -0
  57. benchmarks/run.py +141 -0
  58. benchmarks/systems/__init__.py +1 -0
  59. benchmarks/systems/baselines.py +63 -0
amacs/__init__.py ADDED
@@ -0,0 +1,88 @@
1
+ """AMACS — Adaptive Multi-Agent Coordination System.
2
+
3
+ Add multi-agent AI coordination to any function with a single decorator::
4
+
5
+ from amacs import amacs
6
+
7
+ @amacs(max_agents=8, strategy="performance")
8
+ def research_task(topic: str) -> str:
9
+ return f"Research on {topic}"
10
+
11
+ result = research_task("Renewable Energy")
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ __version__ = "0.2.0"
17
+
18
+ # ── Public API ────────────────────────────────────────────────────────────
19
+
20
+ from amacs.agents.base_agent import AgentResult, BaseAgent, SubTask
21
+ from amacs.communication import CommunicationBus, LogEntry
22
+ from amacs.config import AMACSConfig, Strategy
23
+ from amacs.decorator import amacs
24
+ from amacs.exceptions import (
25
+ AdaptationError,
26
+ AgentError,
27
+ AgentTimeoutError,
28
+ AggregationError,
29
+ AMACSError,
30
+ CommunicationError,
31
+ ConfigurationError,
32
+ LLMProviderError,
33
+ OrchestrationError,
34
+ RetryExhaustedError,
35
+ )
36
+ from amacs.integrations.llm_providers import (
37
+ LLMProvider,
38
+ LLMResponse,
39
+ Message,
40
+ get_provider,
41
+ register_provider,
42
+ )
43
+ from amacs.orchestrator.agent_selector import (
44
+ get_registered_agents,
45
+ register_agent,
46
+ )
47
+ from amacs.pipeline import Pipeline
48
+ from amacs.results import AMACSResult
49
+
50
+ __all__ = [
51
+ # Decorator & Pipeline
52
+ "amacs",
53
+ "Pipeline",
54
+ # Config
55
+
56
+ "AMACSConfig",
57
+ "Strategy",
58
+ # Results & Inspection
59
+ "AMACSResult",
60
+ "AgentResult",
61
+ "LogEntry",
62
+ "SubTask",
63
+ # Agents
64
+ "BaseAgent",
65
+ "get_registered_agents",
66
+ "register_agent",
67
+ # LLM
68
+ "get_provider",
69
+ "register_provider",
70
+ "LLMProvider",
71
+ "Message",
72
+ "LLMResponse",
73
+ # Communication
74
+ "CommunicationBus",
75
+ # Exceptions
76
+ "AMACSError",
77
+ "AgentError",
78
+ "AgentTimeoutError",
79
+ "AggregationError",
80
+ "CommunicationError",
81
+ "ConfigurationError",
82
+ "LLMProviderError",
83
+ "OrchestrationError",
84
+ "RetryExhaustedError",
85
+ "AdaptationError",
86
+ # Meta
87
+ "__version__",
88
+ ]
@@ -0,0 +1,32 @@
1
+ """Adaptive control loop sub-package."""
2
+
3
+ from amacs.adaptive.adaptation_engine import (
4
+ ActionType,
5
+ AdaptationAction,
6
+ AdaptationEngine,
7
+ )
8
+ from amacs.adaptive.evaluator import (
9
+ AgentEvaluation,
10
+ EvaluationReport,
11
+ Evaluator,
12
+ HealthStatus,
13
+ ThresholdConfig,
14
+ )
15
+ from amacs.adaptive.monitor import AgentMetrics, Monitor, SystemSnapshot
16
+ from amacs.adaptive.reconfigurator import ReconfigurationResult, Reconfigurator
17
+
18
+ __all__ = [
19
+ "ActionType",
20
+ "AdaptationAction",
21
+ "AdaptationEngine",
22
+ "AgentEvaluation",
23
+ "AgentMetrics",
24
+ "EvaluationReport",
25
+ "Evaluator",
26
+ "HealthStatus",
27
+ "Monitor",
28
+ "ReconfigurationResult",
29
+ "Reconfigurator",
30
+ "SystemSnapshot",
31
+ "ThresholdConfig",
32
+ ]
@@ -0,0 +1,116 @@
1
+ """Adaptation engine — decides what remedial action to take based on evaluations.
2
+
3
+ Translates :class:`EvaluationReport` findings into concrete :class:`AdaptationAction`
4
+ directives that the :class:`Reconfigurator` can apply.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import Any, Dict, List
12
+
13
+ from amacs.adaptive.evaluator import EvaluationReport, HealthStatus
14
+
15
+
16
+ class ActionType(str, Enum):
17
+ RETRY_WITH_DIFFERENT_AGENT = "retry_with_different_agent"
18
+ SWITCH_MODEL = "switch_model"
19
+ ADD_AGENT = "add_agent"
20
+ REMOVE_AGENT = "remove_agent"
21
+ REDUCE_TEAM = "reduce_team"
22
+ SWAP_AGENT = "swap_agent"
23
+ SKIP_TASK = "skip_task"
24
+ NO_ACTION = "no_action"
25
+
26
+
27
+ @dataclass
28
+ class AdaptationAction:
29
+ """A single remedial action to be executed by the reconfigurator."""
30
+
31
+ action_type: ActionType
32
+ target_agent_id: str
33
+ reason: str
34
+ params: Dict[str, Any] = field(default_factory=dict)
35
+
36
+
37
+ class AdaptationEngine:
38
+ """Decides what changes to make based on evaluation results."""
39
+
40
+ def decide(
41
+ self,
42
+ report: EvaluationReport,
43
+ total_agents: int = 0,
44
+ ) -> List[AdaptationAction]:
45
+ """Return an ordered list of actions to take."""
46
+ actions: List[AdaptationAction] = []
47
+
48
+ if report.system_healthy:
49
+ return [
50
+ AdaptationAction(
51
+ action_type=ActionType.NO_ACTION,
52
+ target_agent_id="",
53
+ reason="System is healthy — no adaptation needed.",
54
+ )
55
+ ]
56
+
57
+ failing_count = 0
58
+ for agent_id in report.underperforming:
59
+ ev = report.evaluations[agent_id]
60
+ params = {
61
+ "original_type": ev.agent_type,
62
+ "trigger": ev.trigger,
63
+ "signal_values": ev.signal_values,
64
+ }
65
+
66
+ if ev.status == HealthStatus.FAILING:
67
+ failing_count += 1
68
+ actions.append(
69
+ AdaptationAction(
70
+ action_type=ActionType.SWAP_AGENT,
71
+ target_agent_id=agent_id,
72
+ reason="; ".join(ev.reasons),
73
+ params=params,
74
+ )
75
+ )
76
+ elif ev.status == HealthStatus.DEGRADED:
77
+ # Quality triggers lead to RETRY_WITH_DIFFERENT_AGENT / SWITCH_MODEL
78
+ if ev.trigger.startswith("quality_signal:"):
79
+ if ev.signal_values.get("quality_score", 1.0) < 0.35 and ev.signal_values.get("critical", False):
80
+ actions.append(
81
+ AdaptationAction(
82
+ action_type=ActionType.ADD_AGENT,
83
+ target_agent_id=agent_id,
84
+ reason="Critical task has a persistently low quality signal.",
85
+ params=params,
86
+ )
87
+ )
88
+ actions.append(
89
+ AdaptationAction(
90
+ action_type=ActionType.RETRY_WITH_DIFFERENT_AGENT,
91
+ target_agent_id=agent_id,
92
+ reason="; ".join(ev.reasons),
93
+ params=params,
94
+ )
95
+ )
96
+ else:
97
+ actions.append(
98
+ AdaptationAction(
99
+ action_type=ActionType.SKIP_TASK,
100
+ target_agent_id=agent_id,
101
+ reason="; ".join(ev.reasons),
102
+ params=params,
103
+ )
104
+ )
105
+
106
+ # systemic issue: reduce team
107
+ if total_agents > 0 and failing_count > total_agents / 2:
108
+ actions.append(
109
+ AdaptationAction(
110
+ action_type=ActionType.REDUCE_TEAM,
111
+ target_agent_id="",
112
+ reason=f"{failing_count}/{total_agents} agents are failing — reducing team.",
113
+ )
114
+ )
115
+
116
+ return actions
@@ -0,0 +1,210 @@
1
+ """Evaluator — compares agent performance against configurable thresholds.
2
+
3
+ Flags agents as "underperforming" when they exceed failure, latency, or quality
4
+ signal limits, feeding actionable signals to the :class:`AdaptationEngine`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from amacs.adaptive.monitor import AgentMetrics, Monitor
14
+
15
+ REFUSAL_PATTERNS = [
16
+ "i cannot",
17
+ "i am unable to",
18
+ "as an ai",
19
+ "i apologize, but",
20
+ "i can't fulfill",
21
+ "i'm sorry, but i cannot",
22
+ "as a language model",
23
+ ]
24
+
25
+
26
+ def is_repeated_text(text: str) -> bool:
27
+ lines = [l.strip() for l in text.splitlines() if l.strip()]
28
+ if len(lines) >= 3 and len(set(lines)) == 1:
29
+ return True
30
+ words = text.split()
31
+ if len(words) >= 12:
32
+ chunk_len = 3
33
+ chunks = [tuple(words[i : i + chunk_len]) for i in range(len(words) - chunk_len + 1)]
34
+ if len(chunks) >= 4 and len(set(chunks)) * 3 <= len(chunks):
35
+ return True
36
+ return False
37
+
38
+
39
+ class HealthStatus(str, Enum):
40
+ HEALTHY = "healthy"
41
+ DEGRADED = "degraded"
42
+ FAILING = "failing"
43
+
44
+
45
+ @dataclass
46
+ class ThresholdConfig:
47
+ """Configurable thresholds for the evaluator."""
48
+
49
+ max_failures: int = 2
50
+ max_latency_seconds: float = 30.0
51
+ max_error_rate: float = 0.5 # 50 %
52
+ min_calls_for_evaluation: int = 1
53
+ min_output_length: int = 20
54
+ min_llm_judge_score: float = 0.6
55
+ max_budget_burn_rate: float = 0.8
56
+ detect_refusal: bool = True
57
+ detect_repeated: bool = True
58
+
59
+
60
+ @dataclass
61
+ class AgentEvaluation:
62
+ """Evaluation result for a single agent."""
63
+
64
+ agent_id: str
65
+ agent_type: str
66
+ status: HealthStatus
67
+ reasons: List[str] = field(default_factory=list)
68
+ metrics: Optional[AgentMetrics] = None
69
+ trigger: str = ""
70
+ signal_values: Dict[str, Any] = field(default_factory=dict)
71
+
72
+
73
+ @dataclass
74
+ class EvaluationReport:
75
+ """Full evaluation report across all agents."""
76
+
77
+ evaluations: Dict[str, AgentEvaluation] = field(default_factory=dict)
78
+ underperforming: List[str] = field(default_factory=list) # agent_ids
79
+ system_healthy: bool = True
80
+
81
+
82
+ class Evaluator:
83
+ """Evaluates agent health against thresholds and quality signals."""
84
+
85
+ def __init__(
86
+ self,
87
+ thresholds: Optional[ThresholdConfig] = None,
88
+ ) -> None:
89
+ self._thresholds = thresholds or ThresholdConfig()
90
+
91
+ def evaluate(self, monitor: Monitor) -> EvaluationReport:
92
+ """Produce an :class:`EvaluationReport` from current monitor state."""
93
+ snap = monitor.snapshot()
94
+ report = EvaluationReport()
95
+
96
+ for agent_id, metrics in snap.agent_metrics.items():
97
+ if metrics is None:
98
+ continue
99
+ ev = self._evaluate_agent(metrics)
100
+ report.evaluations[agent_id] = ev
101
+ if ev.status != HealthStatus.HEALTHY:
102
+ report.underperforming.append(agent_id)
103
+
104
+ report.system_healthy = len(report.underperforming) == 0
105
+ return report
106
+
107
+ def _evaluate_agent(self, m: AgentMetrics) -> AgentEvaluation:
108
+ reasons: List[str] = []
109
+ status = HealthStatus.HEALTHY
110
+ trigger = ""
111
+
112
+ if m.total_calls < self._thresholds.min_calls_for_evaluation:
113
+ return AgentEvaluation(
114
+ agent_id=m.agent_id,
115
+ agent_type=m.agent_type,
116
+ status=HealthStatus.HEALTHY,
117
+ metrics=m,
118
+ trigger="healthy",
119
+ )
120
+
121
+ # 1. failure count
122
+ if m.failed_calls >= self._thresholds.max_failures:
123
+ reasons.append(
124
+ f"Too many failures: {m.failed_calls} >= {self._thresholds.max_failures}"
125
+ )
126
+ status = HealthStatus.FAILING
127
+ trigger = trigger or "failure:max_failures"
128
+
129
+ # 2. error rate
130
+ error_rate = m.failed_calls / m.total_calls if m.total_calls > 0 else 0
131
+ if error_rate >= self._thresholds.max_error_rate:
132
+ reasons.append(
133
+ f"High error rate: {error_rate:.0%} >= {self._thresholds.max_error_rate:.0%}"
134
+ )
135
+ status = HealthStatus.FAILING
136
+ trigger = trigger or "failure:error_rate"
137
+
138
+ # 3. latency
139
+ avg_latency = m.total_latency / m.total_calls if m.total_calls > 0 else 0
140
+ if avg_latency > self._thresholds.max_latency_seconds:
141
+ reasons.append(
142
+ f"High avg latency: {avg_latency:.1f}s > {self._thresholds.max_latency_seconds}s"
143
+ )
144
+ if status == HealthStatus.HEALTHY:
145
+ status = HealthStatus.DEGRADED
146
+ trigger = trigger or "latency:high"
147
+
148
+ # 4. Quality Signals (trigger even when nothing failed!)
149
+ if m.last_content is not None and m.successful_calls > 0:
150
+ content_stripped = m.last_content.strip()
151
+ if len(content_stripped) < self._thresholds.min_output_length:
152
+ reasons.append(
153
+ f"Empty or very short output ({len(content_stripped)} chars < {self._thresholds.min_output_length})"
154
+ )
155
+ status = HealthStatus.FAILING if len(content_stripped) == 0 else HealthStatus.DEGRADED
156
+ trigger = trigger or "quality_signal:short_output"
157
+
158
+ if self._thresholds.detect_refusal and content_stripped:
159
+ content_lower = content_stripped.lower()
160
+ if any(pat in content_lower for pat in REFUSAL_PATTERNS):
161
+ reasons.append("Refusal pattern detected in response")
162
+ status = HealthStatus.FAILING
163
+ trigger = trigger or "quality_signal:refusal_pattern"
164
+
165
+ if (
166
+ self._thresholds.detect_repeated
167
+ and content_stripped
168
+ and is_repeated_text(content_stripped)
169
+ ):
170
+ reasons.append("Repeated output pattern detected")
171
+ if status == HealthStatus.HEALTHY:
172
+ status = HealthStatus.DEGRADED
173
+ trigger = trigger or "quality_signal:repeated_output"
174
+
175
+ if m.quality_scores:
176
+ latest_score = m.quality_scores[-1]
177
+ if latest_score < self._thresholds.min_llm_judge_score:
178
+ reasons.append(
179
+ f"Low LLM judge score: {latest_score:.2f} < {self._thresholds.min_llm_judge_score:.2f}"
180
+ )
181
+ if status == HealthStatus.HEALTHY:
182
+ status = HealthStatus.DEGRADED
183
+ trigger = trigger or "quality_signal:low_judge_score"
184
+
185
+ if m.burn_rate > self._thresholds.max_budget_burn_rate:
186
+ reasons.append(
187
+ f"High budget burn rate: {m.burn_rate:.2f} > {self._thresholds.max_budget_burn_rate:.2f}"
188
+ )
189
+ if status == HealthStatus.HEALTHY:
190
+ status = HealthStatus.DEGRADED
191
+ trigger = trigger or "quality_signal:budget_burn_rate"
192
+
193
+ signal_values = {
194
+ "failed_calls": m.failed_calls,
195
+ "error_rate": error_rate,
196
+ "avg_latency": avg_latency,
197
+ "content_length": len(m.last_content.strip()) if m.last_content else 0,
198
+ "quality_scores": m.quality_scores,
199
+ "burn_rate": m.burn_rate,
200
+ }
201
+
202
+ return AgentEvaluation(
203
+ agent_id=m.agent_id,
204
+ agent_type=m.agent_type,
205
+ status=status,
206
+ reasons=reasons,
207
+ metrics=m,
208
+ trigger=trigger or ("healthy" if status == HealthStatus.HEALTHY else "degraded"),
209
+ signal_values=signal_values,
210
+ )
@@ -0,0 +1,153 @@
1
+ """Monitor — tracks latency, token usage, error rates, and agent health.
2
+
3
+ Provides a real-time view of how each agent is performing during an
4
+ orchestrated execution so the :class:`Evaluator` can flag problems.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from typing import Dict, List, Optional
13
+
14
+
15
+ @dataclass
16
+ class AgentMetrics:
17
+ """Accumulated metrics for a single agent instance."""
18
+
19
+ agent_id: str
20
+ agent_type: str
21
+ total_calls: int = 0
22
+ successful_calls: int = 0
23
+ failed_calls: int = 0
24
+ total_latency: float = 0.0
25
+ total_tokens: int = 0
26
+ last_latency: float = 0.0
27
+ last_error: Optional[str] = None
28
+ timestamps: List[float] = field(default_factory=list)
29
+ last_content: Optional[str] = None
30
+ quality_scores: List[float] = field(default_factory=list)
31
+ burn_rate: float = 0.0
32
+
33
+
34
+ @dataclass
35
+ class SystemSnapshot:
36
+ """Point-in-time snapshot of the entire system's health."""
37
+
38
+ timestamp: float
39
+ active_agents: int
40
+ total_calls: int
41
+ total_failures: int
42
+ avg_latency: float
43
+ total_tokens: int
44
+ agent_metrics: Dict[str, AgentMetrics] = field(default_factory=dict)
45
+
46
+
47
+ class Monitor:
48
+ """Thread-safe performance monitor for the adaptive control loop."""
49
+
50
+ def __init__(self) -> None:
51
+ self._metrics: Dict[str, AgentMetrics] = {}
52
+ self._lock = threading.RLock()
53
+ self._start_time = time.time()
54
+
55
+ def record_success(
56
+ self,
57
+ agent_id: str,
58
+ agent_type: str,
59
+ latency: float,
60
+ tokens: int = 0,
61
+ content: Optional[str] = None,
62
+ quality_score: Optional[float] = None,
63
+ burn_rate: float = 0.0,
64
+ ) -> None:
65
+ """Record a successful agent execution."""
66
+ with self._lock:
67
+ m = self._get_or_create(agent_id, agent_type)
68
+ m.total_calls += 1
69
+ m.successful_calls += 1
70
+ m.total_latency += latency
71
+ m.last_latency = latency
72
+ m.total_tokens += tokens
73
+ if content is not None:
74
+ m.last_content = content
75
+ m.timestamps.append(time.time())
76
+ if quality_score is not None:
77
+ m.quality_scores.append(quality_score)
78
+ if burn_rate > 0:
79
+ m.burn_rate = burn_rate
80
+
81
+ def record_failure(
82
+ self,
83
+ agent_id: str,
84
+ agent_type: str,
85
+ latency: float,
86
+ error: str,
87
+ content: Optional[str] = None,
88
+ ) -> None:
89
+ """Record a failed agent execution."""
90
+ with self._lock:
91
+ m = self._get_or_create(agent_id, agent_type)
92
+ m.total_calls += 1
93
+ m.failed_calls += 1
94
+ m.total_latency += latency
95
+ m.last_latency = latency
96
+ m.last_error = error
97
+ if content is not None:
98
+ m.last_content = content
99
+ m.timestamps.append(time.time())
100
+
101
+ def get_agent_metrics(self, agent_id: str) -> Optional[AgentMetrics]:
102
+ """Return metrics for a specific agent."""
103
+ with self._lock:
104
+ m = self._metrics.get(agent_id)
105
+ if m is None:
106
+ return None
107
+ return AgentMetrics(
108
+ agent_id=m.agent_id,
109
+ agent_type=m.agent_type,
110
+ total_calls=m.total_calls,
111
+ successful_calls=m.successful_calls,
112
+ failed_calls=m.failed_calls,
113
+ total_latency=m.total_latency,
114
+ total_tokens=m.total_tokens,
115
+ last_latency=m.last_latency,
116
+ last_error=m.last_error,
117
+ timestamps=list(m.timestamps),
118
+ last_content=m.last_content,
119
+ quality_scores=list(m.quality_scores),
120
+ burn_rate=m.burn_rate,
121
+ )
122
+
123
+ def snapshot(self) -> SystemSnapshot:
124
+ """Capture a point-in-time snapshot of all agent metrics."""
125
+ with self._lock:
126
+ total_calls = sum(m.total_calls for m in self._metrics.values())
127
+ total_failures = sum(m.failed_calls for m in self._metrics.values())
128
+ total_latency = sum(m.total_latency for m in self._metrics.values())
129
+ total_tokens = sum(m.total_tokens for m in self._metrics.values())
130
+ avg_latency = total_latency / total_calls if total_calls > 0 else 0.0
131
+
132
+ return SystemSnapshot(
133
+ timestamp=time.time(),
134
+ active_agents=len(self._metrics),
135
+ total_calls=total_calls,
136
+ total_failures=total_failures,
137
+ avg_latency=avg_latency,
138
+ total_tokens=total_tokens,
139
+ agent_metrics={k: self.get_agent_metrics(k) for k in self._metrics}, # type: ignore[misc]
140
+ )
141
+
142
+ def reset(self) -> None:
143
+ """Reset all metrics."""
144
+ with self._lock:
145
+ self._metrics.clear()
146
+ self._start_time = time.time()
147
+
148
+ def _get_or_create(self, agent_id: str, agent_type: str) -> AgentMetrics:
149
+ if agent_id not in self._metrics:
150
+ self._metrics[agent_id] = AgentMetrics(
151
+ agent_id=agent_id, agent_type=agent_type
152
+ )
153
+ return self._metrics[agent_id]