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.
- amacs/__init__.py +88 -0
- amacs/adaptive/__init__.py +32 -0
- amacs/adaptive/adaptation_engine.py +116 -0
- amacs/adaptive/evaluator.py +210 -0
- amacs/adaptive/monitor.py +153 -0
- amacs/adaptive/reconfigurator.py +271 -0
- amacs/agents/__init__.py +17 -0
- amacs/agents/analysis_agent.py +23 -0
- amacs/agents/base_agent.py +648 -0
- amacs/agents/search_agent.py +41 -0
- amacs/agents/validator_agent.py +23 -0
- amacs/agents/writer_agent.py +23 -0
- amacs/aggregation/__init__.py +146 -0
- amacs/aggregation/aggregator.py +5 -0
- amacs/benchmarks/__init__.py +7 -0
- amacs/benchmarks/benchmark_suite.py +57 -0
- amacs/cache.py +95 -0
- amacs/cli.py +72 -0
- amacs/communication/__init__.py +122 -0
- amacs/communication/bus.py +5 -0
- amacs/config.py +174 -0
- amacs/context_builder.py +128 -0
- amacs/coordination/__init__.py +13 -0
- amacs/coordination/base.py +57 -0
- amacs/coordination/debate.py +109 -0
- amacs/coordination/manager_worker.py +97 -0
- amacs/decorator.py +59 -0
- amacs/exceptions.py +197 -0
- amacs/executor.py +587 -0
- amacs/integrations/__init__.py +17 -0
- amacs/integrations/llm_providers.py +845 -0
- amacs/integrations/monitoring.py +82 -0
- amacs/integrations/vector_db.py +129 -0
- amacs/orchestrator/__init__.py +21 -0
- amacs/orchestrator/agent_selector.py +87 -0
- amacs/orchestrator/llm_decomposer.py +95 -0
- amacs/orchestrator/scheduler.py +195 -0
- amacs/orchestrator/task_analyzer.py +167 -0
- amacs/orchestrator/task_decomposer.py +151 -0
- amacs/pipeline.py +320 -0
- amacs/pricing.py +65 -0
- amacs/results.py +86 -0
- amacs/strategy.py +147 -0
- amacs/tools/__init__.py +40 -0
- amacs/tools/base_tool.py +49 -0
- amacs/tools/builtins.py +218 -0
- amacs/tracing.py +157 -0
- amacs-0.2.0.dist-info/METADATA +268 -0
- amacs-0.2.0.dist-info/RECORD +59 -0
- amacs-0.2.0.dist-info/WHEEL +5 -0
- amacs-0.2.0.dist-info/entry_points.txt +2 -0
- amacs-0.2.0.dist-info/licenses/LICENSE +21 -0
- amacs-0.2.0.dist-info/top_level.txt +2 -0
- benchmarks/__init__.py +7 -0
- benchmarks/benchmark_suite.py +10 -0
- benchmarks/report.py +79 -0
- benchmarks/run.py +141 -0
- benchmarks/systems/__init__.py +1 -0
- 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]
|