agentx-python 0.6.7__tar.gz → 0.6.9__tar.gz
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.
- {agentx_python-0.6.7 → agentx_python-0.6.9}/PKG-INFO +12 -3
- {agentx_python-0.6.7 → agentx_python-0.6.9}/README.md +11 -2
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/agentx.py +2 -1
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/datasets.py +32 -1
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/evaluation_settings.py +13 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/models.py +52 -4
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/results.py +6 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/runner.py +33 -5
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/integrations/langchain.py +84 -62
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/monitor/__init__.py +5 -1
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/monitor/client.py +35 -1
- agentx_python-0.6.9/agentx/monitor/models.py +85 -0
- agentx_python-0.6.9/agentx/monitor/signals.py +41 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/tracing/tracer.py +7 -1
- agentx_python-0.6.9/agentx/version.py +1 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx_python.egg-info/PKG-INFO +12 -3
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx_python.egg-info/SOURCES.txt +1 -0
- agentx_python-0.6.7/agentx/monitor/models.py +0 -37
- agentx_python-0.6.7/agentx/version.py +0 -1
- {agentx_python-0.6.7 → agentx_python-0.6.9}/LICENSE +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/__init__.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/client.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/redaction.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/exceptions.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/integrations/_perf.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/integrations/crewai.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/integrations/openai_agents.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/py.typed +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/resources/agent.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/tracing/ingest_client.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx/util.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/setup.cfg +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/setup.py +0 -0
- {agentx_python-0.6.7 → agentx_python-0.6.9}/tests/test_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentx-python
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.9
|
|
4
4
|
Summary: Official Python SDK for AgentX (https://www.agentx.so/)
|
|
5
5
|
Home-page: https://github.com/AgentX-ai/AgentX-python
|
|
6
6
|
Author: Robin Wang and AgentX Team
|
|
@@ -85,7 +85,7 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
|
|
|
85
85
|
- **MCP support** — connect any Model Context Protocol server.
|
|
86
86
|
- **Multi-agent orchestration** — workforces of agents with a designated manager, across LLM vendors.
|
|
87
87
|
- **Production tracing** — one decorator or context manager records every agent run (input, output, latency, tool calls, token usage) into your workspace, for any framework.
|
|
88
|
-
- **Agent Evaluations** — score any agent (LangChain, CrewAI, OpenAI, Anthropic, HTTP, …) with LLM-as-a-judge ratings plus optional cosine and Jaccard similarity metrics.
|
|
88
|
+
- **Agent Evaluations** — score any agent (LangChain, CrewAI, OpenAI, Anthropic, HTTP, …) with LLM-as-a-judge ratings plus optional cosine and Jaccard similarity metrics. Configurable judge prompt/model (OpenAI or Anthropic), per-question judge guidelines, and smoke testing for phrasing robustness.
|
|
89
89
|
- **A2A** — Each agent can be published with agent-to-agent protocol compatible.
|
|
90
90
|
|
|
91
91
|
---
|
|
@@ -266,6 +266,13 @@ with client.tracer.trace("support-agent", monitor=True, pattern_ids=[pattern.id]
|
|
|
266
266
|
|
|
267
267
|
This works independently of the dashboard's per-agent monitoring toggle (Governance > Observe > Agents), which still auto-checks every trace from an agent once enabled there, with no code changes needed either way.
|
|
268
268
|
|
|
269
|
+
Read the resulting alerts back with `client.monitor.signals.list()`/`.get()`, no dashboard required:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
for signal in client.monitor.signals.list(severity="high"):
|
|
273
|
+
print(signal.summary, signal.occurrence_count)
|
|
274
|
+
```
|
|
275
|
+
|
|
269
276
|
See **[TRACING.md](TRACING.md)** for the complete Monitor guide.
|
|
270
277
|
|
|
271
278
|
---
|
|
@@ -293,7 +300,9 @@ print(report.recommendations) # list of prioritized, actionable fixes
|
|
|
293
300
|
|
|
294
301
|
`.analyze()` also generates a full qualitative report (strengths, weaknesses, instruction adherence, reasoning quality, and recommendations), running the same durable, multi-judge pipeline as the dashboard's "Analyze" button. `analyze(mode=..., quality_mode=..., judges=[...])` controls how items are scored and by which models. See [AI analysis report](EVALUATIONS.md#ai-analysis-report) in the full guide for the complete field and parameter reference.
|
|
295
302
|
|
|
296
|
-
|
|
303
|
+
Ask a case's question several extra ways each run, LLM-paraphrased server-side, to catch agents that break on phrasing rather than substance, and override the judge's prompt/model per config, see [Smoke testing](EVALUATIONS.md#smoke-testing-phrasing-robustness) and [Configuring the judge](EVALUATIONS.md#configuring-the-judge) in the full guide.
|
|
304
|
+
|
|
305
|
+
See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder, framework adapters, similarity metrics, smoke testing, judge configuration, and the complete API reference.
|
|
297
306
|
|
|
298
307
|
---
|
|
299
308
|
|
|
@@ -37,7 +37,7 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
|
|
|
37
37
|
- **MCP support** — connect any Model Context Protocol server.
|
|
38
38
|
- **Multi-agent orchestration** — workforces of agents with a designated manager, across LLM vendors.
|
|
39
39
|
- **Production tracing** — one decorator or context manager records every agent run (input, output, latency, tool calls, token usage) into your workspace, for any framework.
|
|
40
|
-
- **Agent Evaluations** — score any agent (LangChain, CrewAI, OpenAI, Anthropic, HTTP, …) with LLM-as-a-judge ratings plus optional cosine and Jaccard similarity metrics.
|
|
40
|
+
- **Agent Evaluations** — score any agent (LangChain, CrewAI, OpenAI, Anthropic, HTTP, …) with LLM-as-a-judge ratings plus optional cosine and Jaccard similarity metrics. Configurable judge prompt/model (OpenAI or Anthropic), per-question judge guidelines, and smoke testing for phrasing robustness.
|
|
41
41
|
- **A2A** — Each agent can be published with agent-to-agent protocol compatible.
|
|
42
42
|
|
|
43
43
|
---
|
|
@@ -218,6 +218,13 @@ with client.tracer.trace("support-agent", monitor=True, pattern_ids=[pattern.id]
|
|
|
218
218
|
|
|
219
219
|
This works independently of the dashboard's per-agent monitoring toggle (Governance > Observe > Agents), which still auto-checks every trace from an agent once enabled there, with no code changes needed either way.
|
|
220
220
|
|
|
221
|
+
Read the resulting alerts back with `client.monitor.signals.list()`/`.get()`, no dashboard required:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
for signal in client.monitor.signals.list(severity="high"):
|
|
225
|
+
print(signal.summary, signal.occurrence_count)
|
|
226
|
+
```
|
|
227
|
+
|
|
221
228
|
See **[TRACING.md](TRACING.md)** for the complete Monitor guide.
|
|
222
229
|
|
|
223
230
|
---
|
|
@@ -245,7 +252,9 @@ print(report.recommendations) # list of prioritized, actionable fixes
|
|
|
245
252
|
|
|
246
253
|
`.analyze()` also generates a full qualitative report (strengths, weaknesses, instruction adherence, reasoning quality, and recommendations), running the same durable, multi-judge pipeline as the dashboard's "Analyze" button. `analyze(mode=..., quality_mode=..., judges=[...])` controls how items are scored and by which models. See [AI analysis report](EVALUATIONS.md#ai-analysis-report) in the full guide for the complete field and parameter reference.
|
|
247
254
|
|
|
248
|
-
|
|
255
|
+
Ask a case's question several extra ways each run, LLM-paraphrased server-side, to catch agents that break on phrasing rather than substance, and override the judge's prompt/model per config, see [Smoke testing](EVALUATIONS.md#smoke-testing-phrasing-robustness) and [Configuring the judge](EVALUATIONS.md#configuring-the-judge) in the full guide.
|
|
256
|
+
|
|
257
|
+
See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder, framework adapters, similarity metrics, smoke testing, judge configuration, and the complete API reference.
|
|
249
258
|
|
|
250
259
|
---
|
|
251
260
|
|
|
@@ -43,7 +43,8 @@ class AgentX:
|
|
|
43
43
|
self.evaluations = EvaluationsRunner(_eval_client)
|
|
44
44
|
|
|
45
45
|
# Monitor: create/reuse patterns (client.monitor.patterns) that a trace can be checked
|
|
46
|
-
# against at send time via tracer.trace(..., monitor=True, pattern_ids=[...])
|
|
46
|
+
# against at send time via tracer.trace(..., monitor=True, pattern_ids=[...]), then read
|
|
47
|
+
# back the resulting alerts/findings with client.monitor.signals.
|
|
47
48
|
self.monitor = MonitorClient(
|
|
48
49
|
api_key=self.api_key,
|
|
49
50
|
sdk_version=VERSION,
|
|
@@ -27,6 +27,8 @@ class DatasetBuilder:
|
|
|
27
27
|
acceptance_criteria: Optional[str] = None,
|
|
28
28
|
rejection_criteria: Optional[str] = None,
|
|
29
29
|
evaluation_criteria: Optional[str] = None,
|
|
30
|
+
judge_prompt: Optional[str] = None,
|
|
31
|
+
judge_model: Optional[str] = None,
|
|
30
32
|
vector_similarity: bool = False,
|
|
31
33
|
jaccard_similarity: bool = False,
|
|
32
34
|
bleu_score: bool = False,
|
|
@@ -44,6 +46,13 @@ class DatasetBuilder:
|
|
|
44
46
|
"evaluationCriteria": evaluation_criteria,
|
|
45
47
|
"questions": [],
|
|
46
48
|
}
|
|
49
|
+
# LLM-as-judge overrides for this dataset's own grading config. Omit either to keep the
|
|
50
|
+
# server default (raw prompt template / OpenAI gpt-5.5, see EVALUATIONS.md). judge_model
|
|
51
|
+
# must be one of client.evaluations.list_models() (OpenAI or Anthropic).
|
|
52
|
+
if judge_prompt is not None:
|
|
53
|
+
self._payload["judgePrompt"] = judge_prompt
|
|
54
|
+
if judge_model is not None:
|
|
55
|
+
self._payload["judgeModel"] = judge_model
|
|
47
56
|
# Opt-in similarity metrics, surfaced on the report as cosine_similarity /
|
|
48
57
|
# jaccard_similarity / bleu_score / rouge_score (computed against each
|
|
49
58
|
# case's expected_results).
|
|
@@ -74,8 +83,20 @@ class DatasetBuilder:
|
|
|
74
83
|
expected_knowledge_base: Optional[List[str]] = None,
|
|
75
84
|
expected_delegations: Optional[List[str]] = None,
|
|
76
85
|
follow_up_questions: Optional[List[Dict[str, Any]]] = None,
|
|
86
|
+
judge_guideline: Optional[str] = None,
|
|
87
|
+
smoke_test_count: Optional[int] = None,
|
|
88
|
+
smoke_test_guidance: Optional[str] = None,
|
|
77
89
|
) -> "DatasetBuilder":
|
|
78
|
-
|
|
90
|
+
"""Add a case. `judge_guideline` is optional grading guidance specific to this question.
|
|
91
|
+
|
|
92
|
+
`smoke_test_count`, when set (1-10), asks this question that many extra ways each
|
|
93
|
+
evaluation run, LLM-paraphrased server-side, to catch agents that are brittle to phrasing
|
|
94
|
+
rather than genuinely wrong. `smoke_test_guidance` optionally steers what kind of variants
|
|
95
|
+
get generated (e.g. tone, adversarial phrasing, different languages); the SDK never
|
|
96
|
+
generates or counts variants itself, both fields are only ever consumed server-side.
|
|
97
|
+
Ignored on `follow_up_questions`, only the opening question of a case can be smoke-tested.
|
|
98
|
+
"""
|
|
99
|
+
main: Dict[str, Any] = {"query": query}
|
|
79
100
|
if expected_results:
|
|
80
101
|
main["expectedResults"] = expected_results
|
|
81
102
|
if expected_capabilities:
|
|
@@ -84,6 +105,12 @@ class DatasetBuilder:
|
|
|
84
105
|
main["expectedKnowledgeBase"] = expected_knowledge_base
|
|
85
106
|
if expected_delegations:
|
|
86
107
|
main["expectedDelegations"] = expected_delegations
|
|
108
|
+
if judge_guideline:
|
|
109
|
+
main["judgeGuideline"] = judge_guideline
|
|
110
|
+
if smoke_test_count:
|
|
111
|
+
main["smokeTest"] = {"enabled": True, "count": smoke_test_count}
|
|
112
|
+
if smoke_test_guidance:
|
|
113
|
+
main["smokeTest"]["guidance"] = smoke_test_guidance
|
|
87
114
|
self._payload["questions"].append(
|
|
88
115
|
{
|
|
89
116
|
"main_question": main,
|
|
@@ -217,6 +244,8 @@ class DatasetClient:
|
|
|
217
244
|
acceptance_criteria: Optional[str] = None,
|
|
218
245
|
rejection_criteria: Optional[str] = None,
|
|
219
246
|
evaluation_criteria: Optional[str] = None,
|
|
247
|
+
judge_prompt: Optional[str] = None,
|
|
248
|
+
judge_model: Optional[str] = None,
|
|
220
249
|
vector_similarity: bool = False,
|
|
221
250
|
jaccard_similarity: bool = False,
|
|
222
251
|
bleu_score: bool = False,
|
|
@@ -232,6 +261,8 @@ class DatasetClient:
|
|
|
232
261
|
acceptance_criteria=acceptance_criteria,
|
|
233
262
|
rejection_criteria=rejection_criteria,
|
|
234
263
|
evaluation_criteria=evaluation_criteria,
|
|
264
|
+
judge_prompt=judge_prompt,
|
|
265
|
+
judge_model=judge_model,
|
|
235
266
|
vector_similarity=vector_similarity,
|
|
236
267
|
jaccard_similarity=jaccard_similarity,
|
|
237
268
|
bleu_score=bleu_score,
|
|
@@ -24,6 +24,8 @@ class EvaluationSettingsBuilder:
|
|
|
24
24
|
acceptance_criteria: Optional[str] = None,
|
|
25
25
|
rejection_criteria: Optional[str] = None,
|
|
26
26
|
evaluation_criteria: Optional[str] = None,
|
|
27
|
+
judge_prompt: Optional[str] = None,
|
|
28
|
+
judge_model: Optional[str] = None,
|
|
27
29
|
vector_similarity: bool = False,
|
|
28
30
|
jaccard_similarity: bool = False,
|
|
29
31
|
bleu_score: bool = False,
|
|
@@ -40,6 +42,13 @@ class EvaluationSettingsBuilder:
|
|
|
40
42
|
"rejectionCriteria": rejection_criteria,
|
|
41
43
|
"evaluationCriteria": evaluation_criteria,
|
|
42
44
|
}
|
|
45
|
+
# LLM-as-judge overrides. Omit either to keep the server default (raw prompt template /
|
|
46
|
+
# OpenAI gpt-5.5, see EVALUATIONS.md). judge_model must be one of
|
|
47
|
+
# client.evaluations.list_models() (OpenAI or Anthropic).
|
|
48
|
+
if judge_prompt is not None:
|
|
49
|
+
self._payload["judgePrompt"] = judge_prompt
|
|
50
|
+
if judge_model is not None:
|
|
51
|
+
self._payload["judgeModel"] = judge_model
|
|
43
52
|
# Opt-in similarity metrics, mirrors DatasetBuilder's config kwargs.
|
|
44
53
|
if vector_similarity:
|
|
45
54
|
vs: Dict[str, Any] = {"enabled": True}
|
|
@@ -79,6 +88,8 @@ class EvaluationSettingsClient:
|
|
|
79
88
|
acceptance_criteria: Optional[str] = None,
|
|
80
89
|
rejection_criteria: Optional[str] = None,
|
|
81
90
|
evaluation_criteria: Optional[str] = None,
|
|
91
|
+
judge_prompt: Optional[str] = None,
|
|
92
|
+
judge_model: Optional[str] = None,
|
|
82
93
|
vector_similarity: bool = False,
|
|
83
94
|
jaccard_similarity: bool = False,
|
|
84
95
|
bleu_score: bool = False,
|
|
@@ -94,6 +105,8 @@ class EvaluationSettingsClient:
|
|
|
94
105
|
acceptance_criteria=acceptance_criteria,
|
|
95
106
|
rejection_criteria=rejection_criteria,
|
|
96
107
|
evaluation_criteria=evaluation_criteria,
|
|
108
|
+
judge_prompt=judge_prompt,
|
|
109
|
+
judge_model=judge_model,
|
|
97
110
|
vector_similarity=vector_similarity,
|
|
98
111
|
jaccard_similarity=jaccard_similarity,
|
|
99
112
|
bleu_score=bleu_score,
|
|
@@ -31,14 +31,30 @@ class ObservableTrace(BaseModel):
|
|
|
31
31
|
# ---------------------------------------------------------------------------
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class SmokeTestSettings(BaseModel):
|
|
35
|
+
"""Only meaningful on a question's main_question. See DatasetBuilder.add_case's
|
|
36
|
+
smoke_test_count/smoke_test_guidance for how to set this."""
|
|
37
|
+
|
|
38
|
+
enabled: bool = False
|
|
39
|
+
count: int = 1
|
|
40
|
+
guidance: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
class Config:
|
|
43
|
+
populate_by_name = True
|
|
44
|
+
extra = "ignore"
|
|
45
|
+
|
|
46
|
+
|
|
34
47
|
class TestCase(BaseModel):
|
|
35
48
|
query: str
|
|
36
|
-
expected_results: Optional[str] = None
|
|
37
|
-
expected_capabilities: Optional[List[str]] = None
|
|
38
|
-
expected_knowledge_base: Optional[List[str]] = None
|
|
39
|
-
expected_delegations: Optional[List[str]] = None
|
|
49
|
+
expected_results: Optional[str] = Field(default=None, alias="expectedResults")
|
|
50
|
+
expected_capabilities: Optional[List[str]] = Field(default=None, alias="expectedCapabilities")
|
|
51
|
+
expected_knowledge_base: Optional[List[str]] = Field(default=None, alias="expectedKnowledgeBase")
|
|
52
|
+
expected_delegations: Optional[List[str]] = Field(default=None, alias="expectedDelegations")
|
|
53
|
+
judge_guideline: Optional[str] = Field(default=None, alias="judgeGuideline")
|
|
54
|
+
smoke_test: Optional[SmokeTestSettings] = Field(default=None, alias="smokeTest")
|
|
40
55
|
|
|
41
56
|
class Config:
|
|
57
|
+
populate_by_name = True
|
|
42
58
|
extra = "ignore"
|
|
43
59
|
|
|
44
60
|
|
|
@@ -89,6 +105,10 @@ class EvaluationSettings(BaseModel):
|
|
|
89
105
|
acceptance_criteria: Optional[str] = Field(default=None, alias="acceptanceCriteria")
|
|
90
106
|
rejection_criteria: Optional[str] = Field(default=None, alias="rejectionCriteria")
|
|
91
107
|
evaluation_criteria: Optional[str] = Field(default=None, alias="evaluationCriteria")
|
|
108
|
+
# LLM-as-judge overrides. None means "use the server default" (raw prompt template / OpenAI
|
|
109
|
+
# gpt-5.5). See client.evaluations.settings.builder(judge_prompt=..., judge_model=...).
|
|
110
|
+
judge_prompt: Optional[str] = Field(default=None, alias="judgePrompt")
|
|
111
|
+
judge_model: Optional[str] = Field(default=None, alias="judgeModel")
|
|
92
112
|
status: str = "published"
|
|
93
113
|
# Sovereignty & Portability — models selected to compare when this config runs.
|
|
94
114
|
# Hoisted from the nested ``sovereigntyIndex`` object when enabled.
|
|
@@ -202,12 +222,30 @@ class LiveStatistics(BaseModel):
|
|
|
202
222
|
extra = "ignore"
|
|
203
223
|
|
|
204
224
|
|
|
225
|
+
class SmokeTestVariantGroup(BaseModel):
|
|
226
|
+
"""Paraphrased variants for one question, generated server-side (reusing the same
|
|
227
|
+
generation the dashboard's native runs use) and frozen for the lifetime of the run.
|
|
228
|
+
The SDK never generates or counts these itself, it only consumes what's returned here."""
|
|
229
|
+
|
|
230
|
+
question_index: int = Field(alias="questionIndex")
|
|
231
|
+
variants: List[str] = Field(default_factory=list)
|
|
232
|
+
|
|
233
|
+
class Config:
|
|
234
|
+
populate_by_name = True
|
|
235
|
+
extra = "ignore"
|
|
236
|
+
|
|
237
|
+
|
|
205
238
|
class EvaluationRun(BaseModel):
|
|
206
239
|
run_id: str = Field(alias="runId")
|
|
207
240
|
dataset_id: str = Field(alias="datasetId")
|
|
208
241
|
dataset_version_id: Optional[str] = Field(default=None, alias="datasetVersionId")
|
|
209
242
|
status: str = "in_progress"
|
|
210
243
|
limits: ServerLimits = Field(default_factory=ServerLimits)
|
|
244
|
+
# Present only when at least one question in the dataset has smokeTest.enabled. See
|
|
245
|
+
# SmokeTestVariantGroup.
|
|
246
|
+
smoke_test_variants: Optional[List[SmokeTestVariantGroup]] = Field(
|
|
247
|
+
default=None, alias="smokeTestVariants"
|
|
248
|
+
)
|
|
211
249
|
|
|
212
250
|
class Config:
|
|
213
251
|
populate_by_name = True
|
|
@@ -232,6 +270,12 @@ class EvaluationCase(BaseModel):
|
|
|
232
270
|
# dataset selects comparison models; your callable can read it to pick the
|
|
233
271
|
# model. The SDK also tags the submitted result with it.
|
|
234
272
|
model: Optional[str] = None
|
|
273
|
+
# Smoke test: True when `query` is a server-generated paraphrase variant rather than the
|
|
274
|
+
# dataset's original question text (see SmokeTestVariantGroup). Your callable doesn't need
|
|
275
|
+
# to branch on this, `query` is already the text to ask, but it's available if you want to
|
|
276
|
+
# log or handle variants differently.
|
|
277
|
+
is_smoke_test_variant: bool = False
|
|
278
|
+
smoke_test_variant_text: Optional[str] = None
|
|
235
279
|
|
|
236
280
|
class Config:
|
|
237
281
|
extra = "ignore"
|
|
@@ -278,6 +322,10 @@ class EvaluationResult(BaseModel):
|
|
|
278
322
|
# the dashboard's "Message Trace Details -> Execution Timeline" render the full execution
|
|
279
323
|
# trace for this case, not just the lightweight observable_trace events above.
|
|
280
324
|
trace_id: Optional[str] = Field(default=None, alias="traceId")
|
|
325
|
+
# Smoke test: set by execute() from the originating EvaluationCase, not something you need to
|
|
326
|
+
# set yourself when returning a plain str/dict from your callable.
|
|
327
|
+
is_smoke_test_variant: Optional[bool] = Field(default=None, alias="isSmokeTestVariant")
|
|
328
|
+
smoke_test_variant_text: Optional[str] = Field(default=None, alias="smokeTestVariantText")
|
|
281
329
|
|
|
282
330
|
class Config:
|
|
283
331
|
populate_by_name = True
|
|
@@ -30,6 +30,8 @@ def normalize_result(
|
|
|
30
30
|
raw.case_id = case.case_id
|
|
31
31
|
raw.question_index = case.question_index
|
|
32
32
|
raw.run_number = case.run_number
|
|
33
|
+
raw.is_smoke_test_variant = case.is_smoke_test_variant
|
|
34
|
+
raw.smoke_test_variant_text = case.smoke_test_variant_text
|
|
33
35
|
return raw
|
|
34
36
|
|
|
35
37
|
output: Optional[dict] = None
|
|
@@ -106,6 +108,8 @@ def normalize_result(
|
|
|
106
108
|
),
|
|
107
109
|
metadata=metadata,
|
|
108
110
|
traceId=trace_id,
|
|
111
|
+
isSmokeTestVariant=case.is_smoke_test_variant,
|
|
112
|
+
smokeTestVariantText=case.smoke_test_variant_text,
|
|
109
113
|
)
|
|
110
114
|
|
|
111
115
|
|
|
@@ -126,4 +130,6 @@ def normalize_error(
|
|
|
126
130
|
retryable=False,
|
|
127
131
|
),
|
|
128
132
|
timings=ResultTimings(latencyMs=latency_ms) if latency_ms is not None else None,
|
|
133
|
+
isSmokeTestVariant=case.is_smoke_test_variant,
|
|
134
|
+
smokeTestVariantText=case.smoke_test_variant_text,
|
|
129
135
|
)
|
|
@@ -93,7 +93,7 @@ class EvaluationRunContext:
|
|
|
93
93
|
def execute(self, adapter: AdapterLike) -> "EvaluationRunContext":
|
|
94
94
|
"""Run all cases locally and submit batches to AgentX."""
|
|
95
95
|
normalized = _wrap_adapter(adapter)
|
|
96
|
-
cases = _build_cases(self._dataset, self._evaluation_settings)
|
|
96
|
+
cases = _build_cases(self._dataset, self._run, self._evaluation_settings)
|
|
97
97
|
max_batch = self._run.limits.max_batch_size
|
|
98
98
|
|
|
99
99
|
# Banner
|
|
@@ -108,6 +108,7 @@ class EvaluationRunContext:
|
|
|
108
108
|
if self._evaluation_settings
|
|
109
109
|
else self._dataset.number_of_requests
|
|
110
110
|
)
|
|
111
|
+
n_smoke = sum(1 for c in cases if c.is_smoke_test_variant)
|
|
111
112
|
|
|
112
113
|
print(cyan(sep))
|
|
113
114
|
print(f" {bold('AgentX Evaluation')} {dim('—')} {name}")
|
|
@@ -116,9 +117,11 @@ class EvaluationRunContext:
|
|
|
116
117
|
if display:
|
|
117
118
|
print(f" {dim('Agent :')} {display} {dim(f'({framework} / {runtime})')}")
|
|
118
119
|
print()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
exec_line = f"{bold('Executing')} {n_q} question{'s' if n_q != 1 else ''} × {n_r} run{'s' if n_r != 1 else ''}"
|
|
121
|
+
if n_smoke:
|
|
122
|
+
variant_word = "variant" if n_smoke == 1 else "variants"
|
|
123
|
+
exec_line += f" {dim(f'(+{n_smoke} smoke-test {variant_word})')}"
|
|
124
|
+
print(exec_line)
|
|
122
125
|
|
|
123
126
|
# Resume: skip already-submitted keys
|
|
124
127
|
already_done = self._fetch_submitted_keys()
|
|
@@ -400,7 +403,9 @@ def _wrap_adapter(adapter: AdapterLike) -> Callable[[EvaluationCase], Evaluation
|
|
|
400
403
|
|
|
401
404
|
|
|
402
405
|
def _build_cases(
|
|
403
|
-
dataset: Dataset,
|
|
406
|
+
dataset: Dataset,
|
|
407
|
+
run: EvaluationRun,
|
|
408
|
+
evaluation_settings: Optional[EvaluationSettings] = None,
|
|
404
409
|
) -> List[EvaluationCase]:
|
|
405
410
|
cases: List[EvaluationCase] = []
|
|
406
411
|
# When an independent evaluation_settings was chosen (evaluation_settings_id
|
|
@@ -419,6 +424,14 @@ def _build_cases(
|
|
|
419
424
|
evaluation_settings.sovereignty_models if evaluation_settings else dataset.sovereignty_models
|
|
420
425
|
)
|
|
421
426
|
models: List[Optional[str]] = list(sovereignty_models) or [None]
|
|
427
|
+
# Smoke test: variant text is generated and counted entirely server-side (POST /runs, reusing
|
|
428
|
+
# the same generation the dashboard's native runs use) and handed back on `run`. The SDK never
|
|
429
|
+
# re-derives eligibility, count, or text itself, it only turns what the server already decided
|
|
430
|
+
# into extra cases. Not multiplied across sovereignty_models, matching the server's counting in
|
|
431
|
+
# finalize/missing-results (one case per variant per question, regardless of comparison models).
|
|
432
|
+
smoke_variants_by_question = {
|
|
433
|
+
group.question_index: group.variants for group in (run.smoke_test_variants or [])
|
|
434
|
+
}
|
|
422
435
|
for q_idx, question in enumerate(dataset.questions):
|
|
423
436
|
mq = question.main_question
|
|
424
437
|
for run_num in range(1, n_runs + 1):
|
|
@@ -437,6 +450,21 @@ def _build_cases(
|
|
|
437
450
|
model=model,
|
|
438
451
|
)
|
|
439
452
|
)
|
|
453
|
+
for variant_idx, variant_text in enumerate(smoke_variants_by_question.get(q_idx, [])):
|
|
454
|
+
cases.append(
|
|
455
|
+
EvaluationCase(
|
|
456
|
+
case_id=f"case-{q_idx}",
|
|
457
|
+
question_index=q_idx,
|
|
458
|
+
run_number=n_runs + variant_idx + 1,
|
|
459
|
+
query=variant_text,
|
|
460
|
+
expected_results=mq.expected_results,
|
|
461
|
+
expected_capabilities=mq.expected_capabilities,
|
|
462
|
+
expected_knowledge_base=mq.expected_knowledge_base,
|
|
463
|
+
expected_delegations=mq.expected_delegations,
|
|
464
|
+
is_smoke_test_variant=True,
|
|
465
|
+
smoke_test_variant_text=variant_text,
|
|
466
|
+
)
|
|
467
|
+
)
|
|
440
468
|
return cases
|
|
441
469
|
|
|
442
470
|
|
|
@@ -20,6 +20,7 @@ Requires: ``pip install "agentx-python[langchain]"``
|
|
|
20
20
|
"""
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
import threading
|
|
23
24
|
import time
|
|
24
25
|
from typing import Any, Dict, List, Optional, Union
|
|
25
26
|
from uuid import UUID
|
|
@@ -262,6 +263,13 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
262
263
|
self._pending_retrieval_steps: List[Dict[str, Any]] = []
|
|
263
264
|
# run_id → {"start": float, "query": str}
|
|
264
265
|
self._retrieval_starts: Dict[UUID, Dict[str, Any]] = {}
|
|
266
|
+
# Guards appends to a top-level run's shared aggregate lists (tool_calls,
|
|
267
|
+
# perf_tool_calls, execution_steps, retrieval_steps). LangGraph's ToolNode
|
|
268
|
+
# runs multiple tool calls from one AIMessage concurrently via a thread
|
|
269
|
+
# pool (see langgraph.prebuilt.tool_node.ToolNode._func), so on_tool_end /
|
|
270
|
+
# on_tool_error can fire from several threads at once for the same
|
|
271
|
+
# top-level run.
|
|
272
|
+
self._state_lock = threading.Lock()
|
|
265
273
|
|
|
266
274
|
# ------------------------------------------------------------------
|
|
267
275
|
# Chain lifecycle
|
|
@@ -311,14 +319,14 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
311
319
|
return
|
|
312
320
|
latency_ms = int((time.time() - state["start"]) * 1000)
|
|
313
321
|
output = _extract_output(outputs)
|
|
322
|
+
# Each tool_call dict already carries its own start_time/end_time (set in
|
|
323
|
+
# on_tool_end/on_tool_error), so no re-pairing against perf_tool_calls by
|
|
324
|
+
# index is needed here. That used to be done via zip(), which silently
|
|
325
|
+
# mispaired timestamps when LangGraph's ToolNode ran several tool calls
|
|
326
|
+
# from one AIMessage concurrently (see _state_lock's docstring): two
|
|
327
|
+
# lists appended to from different threads don't necessarily end up in
|
|
328
|
+
# the same relative order.
|
|
314
329
|
tool_calls = state["tool_calls"] or _extract_tool_calls_from_messages(outputs)
|
|
315
|
-
if state["tool_calls"] and len(state["tool_calls"]) == len(state["perf_tool_calls"]):
|
|
316
|
-
# Enrich with the timestamps perf_tool_calls tracked in lockstep,
|
|
317
|
-
# so tool calls interleave correctly in a merged span's timeline.
|
|
318
|
-
tool_calls = [
|
|
319
|
-
{**tc, "start_time": perf.get("start_time"), "end_time": perf.get("end_time")}
|
|
320
|
-
for tc, perf in zip(state["tool_calls"], state["perf_tool_calls"])
|
|
321
|
-
]
|
|
322
330
|
|
|
323
331
|
active_span = self._tracer.current_span
|
|
324
332
|
if active_span is not None:
|
|
@@ -332,6 +340,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
332
340
|
input=state["input"],
|
|
333
341
|
output=output,
|
|
334
342
|
model=state.get("model"),
|
|
343
|
+
framework="langchain",
|
|
335
344
|
input_tokens=state["input_tokens"] or None,
|
|
336
345
|
output_tokens=state["output_tokens"] or None,
|
|
337
346
|
)
|
|
@@ -383,6 +392,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
383
392
|
retrieval_steps=state["retrieval_steps"],
|
|
384
393
|
input=state["input"],
|
|
385
394
|
model=state.get("model"),
|
|
395
|
+
framework="langchain",
|
|
386
396
|
input_tokens=state["input_tokens"] or None,
|
|
387
397
|
output_tokens=state["output_tokens"] or None,
|
|
388
398
|
)
|
|
@@ -485,41 +495,42 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
485
495
|
# Extract token usage from LLMResult for this call
|
|
486
496
|
call_input_tokens: Optional[int] = None
|
|
487
497
|
call_output_tokens: Optional[int] = None
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
for
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
498
|
+
with self._state_lock:
|
|
499
|
+
if top and top in self._runs:
|
|
500
|
+
usage = {}
|
|
501
|
+
if hasattr(response, "llm_output") and isinstance(response.llm_output, dict):
|
|
502
|
+
usage = response.llm_output.get("token_usage") or response.llm_output.get("usage") or {}
|
|
503
|
+
# Also check generations for token counts (some providers put it there)
|
|
504
|
+
if not usage and hasattr(response, "generations"):
|
|
505
|
+
for gen_list in (response.generations or []):
|
|
506
|
+
for gen in (gen_list or []):
|
|
507
|
+
gen_info = getattr(gen, "generation_info", None) or {}
|
|
508
|
+
if gen_info.get("prompt_tokens") or gen_info.get("completion_tokens"):
|
|
509
|
+
usage = gen_info
|
|
510
|
+
break
|
|
511
|
+
if usage:
|
|
512
|
+
call_input_tokens = int(
|
|
513
|
+
usage.get("prompt_tokens") or usage.get("input_tokens") or usage.get("prompt_token_count") or 0
|
|
514
|
+
)
|
|
515
|
+
call_output_tokens = int(
|
|
516
|
+
usage.get("completion_tokens") or usage.get("output_tokens") or usage.get("candidates_token_count") or 0
|
|
517
|
+
)
|
|
518
|
+
self._runs[top]["input_tokens"] += call_input_tokens
|
|
519
|
+
self._runs[top]["output_tokens"] += call_output_tokens
|
|
520
|
+
|
|
521
|
+
if start_t is not None and top and top in self._runs:
|
|
522
|
+
steps = self._runs[top]["execution_steps"]
|
|
523
|
+
steps.append({
|
|
524
|
+
"name": f"LLM Call {len(steps) + 1}",
|
|
525
|
+
"duration_ms": (end_t - start_t) * 1000,
|
|
526
|
+
"start_time": start_t,
|
|
527
|
+
"end_time": end_t,
|
|
528
|
+
"model": llm_state.get("model"),
|
|
529
|
+
"input": llm_state.get("input"),
|
|
530
|
+
"output": _extract_llm_output(response),
|
|
531
|
+
"inputTokenSize": call_input_tokens,
|
|
532
|
+
"outputTokenSize": call_output_tokens,
|
|
533
|
+
})
|
|
523
534
|
|
|
524
535
|
# ------------------------------------------------------------------
|
|
525
536
|
# Tool lifecycle
|
|
@@ -561,18 +572,25 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
561
572
|
"input": state["tool_input"],
|
|
562
573
|
"output": str(output),
|
|
563
574
|
"latency_ms": latency_ms,
|
|
575
|
+
"success": True,
|
|
576
|
+
# Set directly on the tool_call dict (not just perf_tool_calls below) so
|
|
577
|
+
# on_chain_end's merged-span path doesn't need to re-pair the two lists by
|
|
578
|
+
# index later. See _state_lock's docstring for why that used to be unsafe.
|
|
579
|
+
"start_time": start_t,
|
|
580
|
+
"end_time": end_t,
|
|
564
581
|
}
|
|
565
582
|
top = self._find_top_ancestor(parent_run_id)
|
|
566
|
-
|
|
567
|
-
self._runs
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
583
|
+
with self._state_lock:
|
|
584
|
+
if top and top in self._runs:
|
|
585
|
+
self._runs[top]["tool_calls"].append(tool_call)
|
|
586
|
+
self._runs[top]["perf_tool_calls"].append({
|
|
587
|
+
"name": state["tool_name"],
|
|
588
|
+
"duration_ms": (end_t - start_t) * 1000,
|
|
589
|
+
"start_time": start_t,
|
|
590
|
+
"end_time": end_t,
|
|
591
|
+
"input": state["tool_input"],
|
|
592
|
+
"output": tool_call["output"],
|
|
593
|
+
})
|
|
576
594
|
|
|
577
595
|
def on_tool_error(
|
|
578
596
|
self,
|
|
@@ -593,18 +611,22 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
593
611
|
"input": state.get("tool_input"),
|
|
594
612
|
"output": f"ERROR: {error}",
|
|
595
613
|
"latency_ms": int((end_t - start_t) * 1000),
|
|
614
|
+
"success": False,
|
|
615
|
+
"start_time": start_t,
|
|
616
|
+
"end_time": end_t,
|
|
596
617
|
}
|
|
597
618
|
top = self._find_top_ancestor(parent_run_id)
|
|
598
|
-
|
|
599
|
-
self._runs
|
|
600
|
-
|
|
601
|
-
"
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
619
|
+
with self._state_lock:
|
|
620
|
+
if top and top in self._runs:
|
|
621
|
+
self._runs[top]["tool_calls"].append(tool_call)
|
|
622
|
+
self._runs[top]["perf_tool_calls"].append({
|
|
623
|
+
"name": state.get("tool_name", "unknown"),
|
|
624
|
+
"duration_ms": (end_t - start_t) * 1000,
|
|
625
|
+
"start_time": start_t,
|
|
626
|
+
"end_time": end_t,
|
|
627
|
+
"input": tool_call["input"],
|
|
628
|
+
"output": tool_call["output"],
|
|
629
|
+
})
|
|
608
630
|
|
|
609
631
|
# ------------------------------------------------------------------
|
|
610
632
|
# Retriever lifecycle
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
from agentx.monitor.client import MonitorClient
|
|
2
|
-
from agentx.monitor.models import MonitorPattern
|
|
2
|
+
from agentx.monitor.models import MonitorPattern, MonitorSignal, SignalOccurrence
|
|
3
3
|
from agentx.monitor.patterns import MonitorPatternBuilder, MonitorPatternClient
|
|
4
|
+
from agentx.monitor.signals import MonitorSignalClient
|
|
4
5
|
|
|
5
6
|
__all__ = [
|
|
6
7
|
"MonitorClient",
|
|
7
8
|
"MonitorPattern",
|
|
8
9
|
"MonitorPatternBuilder",
|
|
9
10
|
"MonitorPatternClient",
|
|
11
|
+
"MonitorSignal",
|
|
12
|
+
"SignalOccurrence",
|
|
13
|
+
"MonitorSignalClient",
|
|
10
14
|
]
|
|
@@ -7,7 +7,7 @@ from typing import Any, List, Optional
|
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
|
-
from agentx.monitor.models import MonitorPattern
|
|
10
|
+
from agentx.monitor.models import MonitorPattern, MonitorSignal
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -69,8 +69,10 @@ class MonitorClient:
|
|
|
69
69
|
)
|
|
70
70
|
|
|
71
71
|
from agentx.monitor.patterns import MonitorPatternClient
|
|
72
|
+
from agentx.monitor.signals import MonitorSignalClient
|
|
72
73
|
|
|
73
74
|
self.patterns = MonitorPatternClient(self)
|
|
75
|
+
self.signals = MonitorSignalClient(self)
|
|
74
76
|
|
|
75
77
|
# ------------------------------------------------------------------
|
|
76
78
|
# Low-level HTTP
|
|
@@ -132,3 +134,35 @@ class MonitorClient:
|
|
|
132
134
|
"GET", f"/patterns/{pattern_id}", params=self._workspace_params()
|
|
133
135
|
)
|
|
134
136
|
return MonitorPattern(**data["pattern"])
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
# Signal endpoints
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def list_signals(
|
|
143
|
+
self,
|
|
144
|
+
polarity: Optional[str] = None,
|
|
145
|
+
status: Optional[str] = None,
|
|
146
|
+
severity: Optional[str] = None,
|
|
147
|
+
agent_id: Optional[str] = None,
|
|
148
|
+
limit: int = 50,
|
|
149
|
+
) -> List[MonitorSignal]:
|
|
150
|
+
params = {**(self._workspace_params() or {})}
|
|
151
|
+
if polarity is not None:
|
|
152
|
+
params["polarity"] = polarity
|
|
153
|
+
if status is not None:
|
|
154
|
+
params["status"] = status
|
|
155
|
+
if severity is not None:
|
|
156
|
+
params["severity"] = severity
|
|
157
|
+
if agent_id is not None:
|
|
158
|
+
params["agentId"] = agent_id
|
|
159
|
+
if limit is not None:
|
|
160
|
+
params["limit"] = limit
|
|
161
|
+
data = self._request("GET", "/signals", params=params)
|
|
162
|
+
return [MonitorSignal(**s) for s in data.get("signals", [])]
|
|
163
|
+
|
|
164
|
+
def get_signal(self, signal_id: str) -> MonitorSignal:
|
|
165
|
+
data = self._request(
|
|
166
|
+
"GET", f"/signals/{signal_id}", params=self._workspace_params()
|
|
167
|
+
)
|
|
168
|
+
return MonitorSignal(**data["signal"])
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MonitorPattern(BaseModel):
|
|
8
|
+
"""A detection rule checked against production traces. Built via
|
|
9
|
+
``client.monitor.patterns.builder(...).publish()`` and referenced by id as a
|
|
10
|
+
``pattern_ids`` entry in ``tracer.trace(..., monitor=True, pattern_ids=[...])``.
|
|
11
|
+
|
|
12
|
+
A "failure" pattern (the default) raises a signal to triage; a "proper" pattern logs a
|
|
13
|
+
healthy tally instead. Only one of ``include_terms``/``regex``/``semantic_prompt`` is
|
|
14
|
+
meaningful at a time, selected by ``detector_kind``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
id: str = Field(alias="_id")
|
|
18
|
+
key: str
|
|
19
|
+
name: str
|
|
20
|
+
description: Optional[str] = None
|
|
21
|
+
category: Optional[str] = None
|
|
22
|
+
detector_kind: str = Field(default="contains", alias="detectorKind")
|
|
23
|
+
match_target: List[str] = Field(default_factory=lambda: ["response"], alias="matchTarget")
|
|
24
|
+
match_mode: str = Field(default="any", alias="matchMode")
|
|
25
|
+
include_terms: List[str] = Field(default_factory=list, alias="includeTerms")
|
|
26
|
+
exclude_terms: List[str] = Field(default_factory=list, alias="excludeTerms")
|
|
27
|
+
regex: Optional[str] = None
|
|
28
|
+
semantic_prompt: Optional[str] = Field(default=None, alias="semanticPrompt")
|
|
29
|
+
severity: str = "medium"
|
|
30
|
+
polarity: str = "failure"
|
|
31
|
+
enabled: bool = True
|
|
32
|
+
sample_rate: float = Field(default=1.0, alias="sampleRate")
|
|
33
|
+
scope_mode: str = Field(default="all", alias="scopeMode")
|
|
34
|
+
agent_ids: List[str] = Field(default_factory=list, alias="agentIds")
|
|
35
|
+
|
|
36
|
+
class Config:
|
|
37
|
+
extra = "ignore"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SignalOccurrence(BaseModel):
|
|
41
|
+
"""One hit behind a signal, capped at the server's most recent N per signal."""
|
|
42
|
+
|
|
43
|
+
agent_id: Optional[Dict[str, Any]] = Field(default=None, alias="agentId")
|
|
44
|
+
conversation_id: Optional[str] = Field(default=None, alias="conversationId")
|
|
45
|
+
message_id: Optional[str] = Field(default=None, alias="messageId")
|
|
46
|
+
trace_id: Optional[str] = Field(default=None, alias="traceId")
|
|
47
|
+
seen_at: Optional[str] = Field(default=None, alias="seenAt")
|
|
48
|
+
|
|
49
|
+
class Config:
|
|
50
|
+
populate_by_name = True
|
|
51
|
+
extra = "ignore"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MonitorSignal(BaseModel):
|
|
55
|
+
"""An alert/finding produced when a trace matched a pattern (or, for a "proper" pattern,
|
|
56
|
+
a healthy tally). Read-only from the SDK, see ``client.monitor.signals.list()/get()``, a
|
|
57
|
+
signal is the system's output from checking traces against patterns, not something an SDK
|
|
58
|
+
caller constructs."""
|
|
59
|
+
|
|
60
|
+
id: str = Field(alias="_id")
|
|
61
|
+
workspace_id: Optional[str] = Field(default=None, alias="workspaceId")
|
|
62
|
+
# Populated to {"_id", "name", "avatar"} when set, since the server populates this field.
|
|
63
|
+
agent_id: Optional[Dict[str, Any]] = Field(default=None, alias="agentId")
|
|
64
|
+
conversation_id: Optional[str] = Field(default=None, alias="conversationId")
|
|
65
|
+
message_id: Optional[str] = Field(default=None, alias="messageId")
|
|
66
|
+
type: str
|
|
67
|
+
severity: str = "medium"
|
|
68
|
+
polarity: str = "failure"
|
|
69
|
+
status: str = "open"
|
|
70
|
+
score: Optional[float] = None
|
|
71
|
+
threshold: Optional[float] = None
|
|
72
|
+
summary: str
|
|
73
|
+
pattern_key: str = Field(alias="patternKey")
|
|
74
|
+
evidence: Optional[Dict[str, Any]] = None
|
|
75
|
+
root_cause: Optional[str] = Field(default=None, alias="rootCause")
|
|
76
|
+
recommended_actions: List[str] = Field(default_factory=list, alias="recommendedActions")
|
|
77
|
+
review_status: Optional[str] = Field(default=None, alias="reviewStatus")
|
|
78
|
+
first_seen_at: Optional[str] = Field(default=None, alias="firstSeenAt")
|
|
79
|
+
last_seen_at: Optional[str] = Field(default=None, alias="lastSeenAt")
|
|
80
|
+
occurrence_count: int = Field(default=1, alias="occurrenceCount")
|
|
81
|
+
occurrences: List[SignalOccurrence] = Field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
class Config:
|
|
84
|
+
populate_by_name = True
|
|
85
|
+
extra = "ignore"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from agentx.monitor.models import MonitorSignal
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from agentx.monitor.client import MonitorClient
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MonitorSignalClient:
|
|
15
|
+
"""Thin wrapper surfaced as ``client.monitor.signals``. Read-only: a signal is produced by
|
|
16
|
+
checking a trace against patterns (see ``tracer.trace(..., monitor=True, pattern_ids=[...])``),
|
|
17
|
+
not something the SDK creates directly."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, client: "MonitorClient"):
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
def list(
|
|
23
|
+
self,
|
|
24
|
+
polarity: Optional[str] = None,
|
|
25
|
+
status: Optional[str] = None,
|
|
26
|
+
severity: Optional[str] = None,
|
|
27
|
+
agent_id: Optional[str] = None,
|
|
28
|
+
limit: int = 50,
|
|
29
|
+
) -> List[MonitorSignal]:
|
|
30
|
+
"""List this workspace's signals, most recently seen first.
|
|
31
|
+
|
|
32
|
+
`polarity` defaults server-side to failures only ("proper", the healthy tally, is
|
|
33
|
+
excluded); pass ``polarity="all"`` to include both, or ``"proper"``/``"failure"`` to
|
|
34
|
+
narrow to one kind. `limit` is capped at 100 server-side.
|
|
35
|
+
"""
|
|
36
|
+
return self._client.list_signals(
|
|
37
|
+
polarity=polarity, status=status, severity=severity, agent_id=agent_id, limit=limit
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def get(self, signal_id: str) -> MonitorSignal:
|
|
41
|
+
return self._client.get_signal(signal_id)
|
|
@@ -114,6 +114,9 @@ class _TraceSpan:
|
|
|
114
114
|
self._execution_steps: list = []
|
|
115
115
|
self._retrieval_steps: list = []
|
|
116
116
|
self._captured_model: Optional[str] = None
|
|
117
|
+
# Adopted from a merged child run (e.g. AgentXCallbackHandler) when this span itself
|
|
118
|
+
# wasn't opened with an explicit framework= — see _merge_child_run below.
|
|
119
|
+
self._captured_framework: Optional[str] = None
|
|
117
120
|
self._input_tokens: int = 0
|
|
118
121
|
self._output_tokens: int = 0
|
|
119
122
|
# Guards _merge_child_run — with Tracer.use_span(), multiple threads
|
|
@@ -186,7 +189,7 @@ class _TraceSpan:
|
|
|
186
189
|
latency_ms=latency_ms,
|
|
187
190
|
error=self._error,
|
|
188
191
|
metadata=self._metadata,
|
|
189
|
-
framework=self._framework,
|
|
192
|
+
framework=self._framework or self._captured_framework,
|
|
190
193
|
model=self._model or self._captured_model,
|
|
191
194
|
tool_calls=self.tool_calls or None,
|
|
192
195
|
session_id=self._session_id,
|
|
@@ -250,6 +253,7 @@ class _TraceSpan:
|
|
|
250
253
|
input: Any = None,
|
|
251
254
|
output: Any = None,
|
|
252
255
|
model: Optional[str] = None,
|
|
256
|
+
framework: Optional[str] = None,
|
|
253
257
|
input_tokens: Optional[int] = None,
|
|
254
258
|
output_tokens: Optional[int] = None,
|
|
255
259
|
) -> None:
|
|
@@ -281,6 +285,8 @@ class _TraceSpan:
|
|
|
281
285
|
self.output = output
|
|
282
286
|
if model and not self._captured_model:
|
|
283
287
|
self._captured_model = model
|
|
288
|
+
if framework and not self._captured_framework:
|
|
289
|
+
self._captured_framework = framework
|
|
284
290
|
if input_tokens:
|
|
285
291
|
self._input_tokens += input_tokens
|
|
286
292
|
if output_tokens:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.6.9"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentx-python
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.9
|
|
4
4
|
Summary: Official Python SDK for AgentX (https://www.agentx.so/)
|
|
5
5
|
Home-page: https://github.com/AgentX-ai/AgentX-python
|
|
6
6
|
Author: Robin Wang and AgentX Team
|
|
@@ -85,7 +85,7 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
|
|
|
85
85
|
- **MCP support** — connect any Model Context Protocol server.
|
|
86
86
|
- **Multi-agent orchestration** — workforces of agents with a designated manager, across LLM vendors.
|
|
87
87
|
- **Production tracing** — one decorator or context manager records every agent run (input, output, latency, tool calls, token usage) into your workspace, for any framework.
|
|
88
|
-
- **Agent Evaluations** — score any agent (LangChain, CrewAI, OpenAI, Anthropic, HTTP, …) with LLM-as-a-judge ratings plus optional cosine and Jaccard similarity metrics.
|
|
88
|
+
- **Agent Evaluations** — score any agent (LangChain, CrewAI, OpenAI, Anthropic, HTTP, …) with LLM-as-a-judge ratings plus optional cosine and Jaccard similarity metrics. Configurable judge prompt/model (OpenAI or Anthropic), per-question judge guidelines, and smoke testing for phrasing robustness.
|
|
89
89
|
- **A2A** — Each agent can be published with agent-to-agent protocol compatible.
|
|
90
90
|
|
|
91
91
|
---
|
|
@@ -266,6 +266,13 @@ with client.tracer.trace("support-agent", monitor=True, pattern_ids=[pattern.id]
|
|
|
266
266
|
|
|
267
267
|
This works independently of the dashboard's per-agent monitoring toggle (Governance > Observe > Agents), which still auto-checks every trace from an agent once enabled there, with no code changes needed either way.
|
|
268
268
|
|
|
269
|
+
Read the resulting alerts back with `client.monitor.signals.list()`/`.get()`, no dashboard required:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
for signal in client.monitor.signals.list(severity="high"):
|
|
273
|
+
print(signal.summary, signal.occurrence_count)
|
|
274
|
+
```
|
|
275
|
+
|
|
269
276
|
See **[TRACING.md](TRACING.md)** for the complete Monitor guide.
|
|
270
277
|
|
|
271
278
|
---
|
|
@@ -293,7 +300,9 @@ print(report.recommendations) # list of prioritized, actionable fixes
|
|
|
293
300
|
|
|
294
301
|
`.analyze()` also generates a full qualitative report (strengths, weaknesses, instruction adherence, reasoning quality, and recommendations), running the same durable, multi-judge pipeline as the dashboard's "Analyze" button. `analyze(mode=..., quality_mode=..., judges=[...])` controls how items are scored and by which models. See [AI analysis report](EVALUATIONS.md#ai-analysis-report) in the full guide for the complete field and parameter reference.
|
|
295
302
|
|
|
296
|
-
|
|
303
|
+
Ask a case's question several extra ways each run, LLM-paraphrased server-side, to catch agents that break on phrasing rather than substance, and override the judge's prompt/model per config, see [Smoke testing](EVALUATIONS.md#smoke-testing-phrasing-robustness) and [Configuring the judge](EVALUATIONS.md#configuring-the-judge) in the full guide.
|
|
304
|
+
|
|
305
|
+
See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder, framework adapters, similarity metrics, smoke testing, judge configuration, and the complete API reference.
|
|
297
306
|
|
|
298
307
|
---
|
|
299
308
|
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import List, Optional
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class MonitorPattern(BaseModel):
|
|
8
|
-
"""A detection rule checked against production traces. Built via
|
|
9
|
-
``client.monitor.patterns.builder(...).publish()`` and referenced by id as a
|
|
10
|
-
``pattern_ids`` entry in ``tracer.trace(..., monitor=True, pattern_ids=[...])``.
|
|
11
|
-
|
|
12
|
-
A "failure" pattern (the default) raises a signal to triage; a "proper" pattern logs a
|
|
13
|
-
healthy tally instead. Only one of ``include_terms``/``regex``/``semantic_prompt`` is
|
|
14
|
-
meaningful at a time, selected by ``detector_kind``.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
id: str = Field(alias="_id")
|
|
18
|
-
key: str
|
|
19
|
-
name: str
|
|
20
|
-
description: Optional[str] = None
|
|
21
|
-
category: Optional[str] = None
|
|
22
|
-
detector_kind: str = Field(default="contains", alias="detectorKind")
|
|
23
|
-
match_target: List[str] = Field(default_factory=lambda: ["response"], alias="matchTarget")
|
|
24
|
-
match_mode: str = Field(default="any", alias="matchMode")
|
|
25
|
-
include_terms: List[str] = Field(default_factory=list, alias="includeTerms")
|
|
26
|
-
exclude_terms: List[str] = Field(default_factory=list, alias="excludeTerms")
|
|
27
|
-
regex: Optional[str] = None
|
|
28
|
-
semantic_prompt: Optional[str] = Field(default=None, alias="semanticPrompt")
|
|
29
|
-
severity: str = "medium"
|
|
30
|
-
polarity: str = "failure"
|
|
31
|
-
enabled: bool = True
|
|
32
|
-
sample_rate: float = Field(default=1.0, alias="sampleRate")
|
|
33
|
-
scope_mode: str = Field(default="all", alias="scopeMode")
|
|
34
|
-
agent_ids: List[str] = Field(default_factory=list, alias="agentIds")
|
|
35
|
-
|
|
36
|
-
class Config:
|
|
37
|
-
extra = "ignore"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "0.6.7"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|