agentx-python 0.6.6__tar.gz → 0.6.8__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.6/agentx_python.egg-info → agentx_python-0.6.8}/PKG-INFO +17 -3
- {agentx_python-0.6.6 → agentx_python-0.6.8}/README.md +16 -2
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/agentx.py +2 -1
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/_term.py +6 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/client.py +23 -2
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/datasets.py +32 -1
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/evaluation_settings.py +13 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/models.py +115 -9
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/results.py +6 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/runner.py +103 -9
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/monitor/__init__.py +5 -1
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/monitor/client.py +35 -1
- agentx_python-0.6.8/agentx/monitor/models.py +85 -0
- agentx_python-0.6.8/agentx/monitor/signals.py +41 -0
- agentx_python-0.6.8/agentx/version.py +1 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8/agentx_python.egg-info}/PKG-INFO +17 -3
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/SOURCES.txt +1 -0
- agentx_python-0.6.6/agentx/monitor/models.py +0 -37
- agentx_python-0.6.6/agentx/version.py +0 -1
- {agentx_python-0.6.6 → agentx_python-0.6.8}/LICENSE +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/__init__.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/redaction.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/exceptions.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/_perf.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/crewai.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/langchain.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/openai_agents.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/py.typed +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/resources/agent.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/tracing/ingest_client.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/tracing/tracer.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/util.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/setup.cfg +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/setup.py +0 -0
- {agentx_python-0.6.6 → agentx_python-0.6.8}/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.8
|
|
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
|
---
|
|
@@ -286,9 +293,16 @@ report = (
|
|
|
286
293
|
print(report.average_rating) # LLM-graded score, 0–10
|
|
287
294
|
print(report.cosine_similarity) # embedding cosine, 0–1 (None if not enabled)
|
|
288
295
|
print(report.jaccard_similarity) # token-set overlap, 0–1 (None if not enabled)
|
|
296
|
+
|
|
297
|
+
print(report.summary) # AI-generated narrative from .analyze()
|
|
298
|
+
print(report.recommendations) # list of prioritized, actionable fixes
|
|
289
299
|
```
|
|
290
300
|
|
|
291
|
-
|
|
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.
|
|
302
|
+
|
|
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.
|
|
292
306
|
|
|
293
307
|
---
|
|
294
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
|
---
|
|
@@ -238,9 +245,16 @@ report = (
|
|
|
238
245
|
print(report.average_rating) # LLM-graded score, 0–10
|
|
239
246
|
print(report.cosine_similarity) # embedding cosine, 0–1 (None if not enabled)
|
|
240
247
|
print(report.jaccard_similarity) # token-set overlap, 0–1 (None if not enabled)
|
|
248
|
+
|
|
249
|
+
print(report.summary) # AI-generated narrative from .analyze()
|
|
250
|
+
print(report.recommendations) # list of prioritized, actionable fixes
|
|
241
251
|
```
|
|
242
252
|
|
|
243
|
-
|
|
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.
|
|
254
|
+
|
|
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.
|
|
244
258
|
|
|
245
259
|
---
|
|
246
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,
|
|
@@ -71,6 +71,12 @@ class Spinner:
|
|
|
71
71
|
self._thread.start()
|
|
72
72
|
return self
|
|
73
73
|
|
|
74
|
+
def update(self, message: str) -> None:
|
|
75
|
+
"""Change the displayed message while the spinner keeps running."""
|
|
76
|
+
self._message = message
|
|
77
|
+
if not _IS_TTY:
|
|
78
|
+
print(f" {message}...", flush=True)
|
|
79
|
+
|
|
74
80
|
def __exit__(self, *_) -> None:
|
|
75
81
|
if not _IS_TTY:
|
|
76
82
|
return
|
|
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional
|
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
11
|
from agentx.evaluations.models import (
|
|
12
|
+
AnalysisStatus,
|
|
12
13
|
BatchAppendResponse,
|
|
13
14
|
Dataset,
|
|
14
15
|
EvaluationResult,
|
|
@@ -238,8 +239,28 @@ class EvaluationsClient:
|
|
|
238
239
|
"POST", f"/runs/{run_id}/finalize", json={"status": "completed"}
|
|
239
240
|
)
|
|
240
241
|
|
|
241
|
-
def analyze_run(
|
|
242
|
-
|
|
242
|
+
def analyze_run(
|
|
243
|
+
self,
|
|
244
|
+
run_id: str,
|
|
245
|
+
mode: Optional[str] = None,
|
|
246
|
+
quality_mode: Optional[str] = None,
|
|
247
|
+
judges: Optional[List[str]] = None,
|
|
248
|
+
) -> Dict[str, Any]:
|
|
249
|
+
# Starts the durable analysis job and returns immediately (e.g. {"jobId": ..., "status":
|
|
250
|
+
# "pending"}); poll get_analysis_status() until it reaches a terminal status, then call
|
|
251
|
+
# get_report(). mode/quality_mode/judges mirror the dashboard's AnalyzeEvaluationRequest.
|
|
252
|
+
payload: Dict[str, Any] = {}
|
|
253
|
+
if mode is not None:
|
|
254
|
+
payload["mode"] = mode
|
|
255
|
+
if quality_mode is not None:
|
|
256
|
+
payload["qualityMode"] = quality_mode
|
|
257
|
+
if judges is not None:
|
|
258
|
+
payload["judges"] = [{"model": m} for m in judges]
|
|
259
|
+
return self._request("POST", f"/runs/{run_id}/analyze", json=payload, timeout=30)
|
|
260
|
+
|
|
261
|
+
def get_analysis_status(self, run_id: str) -> AnalysisStatus:
|
|
262
|
+
data = self._request("GET", f"/runs/{run_id}/analyze-status")
|
|
263
|
+
return AnalysisStatus(**data)
|
|
243
264
|
|
|
244
265
|
def get_run(self, run_id: str) -> Dict[str, Any]:
|
|
245
266
|
return self._request("GET", f"/runs/{run_id}")
|
|
@@ -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
|
|
@@ -438,11 +486,9 @@ class SovereigntyIndex(BaseModel):
|
|
|
438
486
|
extra = "ignore"
|
|
439
487
|
|
|
440
488
|
|
|
441
|
-
class
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
status: str = "completed"
|
|
445
|
-
statistics: Optional[ReportStatistics] = None
|
|
489
|
+
class AnalysisResult(BaseModel):
|
|
490
|
+
"""Shared qualitative-report fields, produced by ``client.evaluations.run(...).analyze()``."""
|
|
491
|
+
|
|
446
492
|
summary: Optional[str] = None
|
|
447
493
|
consistency_score: Optional[float] = Field(default=None, alias="consistencyScore")
|
|
448
494
|
instruction_adherence: Optional[ReportInstructionAdherence] = Field(
|
|
@@ -461,6 +507,17 @@ class Report(BaseModel):
|
|
|
461
507
|
weaknesses: List[str] = Field(default_factory=list)
|
|
462
508
|
overall_rating: Optional[str] = Field(default=None, alias="overallRating")
|
|
463
509
|
recommendations: List[ReportRecommendation] = Field(default_factory=list)
|
|
510
|
+
|
|
511
|
+
class Config:
|
|
512
|
+
populate_by_name = True
|
|
513
|
+
extra = "ignore"
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class Report(AnalysisResult):
|
|
517
|
+
run_id: str = Field(alias="runId")
|
|
518
|
+
dataset_id: str = Field(alias="datasetId")
|
|
519
|
+
status: str = "completed"
|
|
520
|
+
statistics: Optional[ReportStatistics] = None
|
|
464
521
|
low_scoring_cases: List[Dict[str, Any]] = Field(
|
|
465
522
|
default_factory=list, alias="lowScoringCases"
|
|
466
523
|
)
|
|
@@ -548,3 +605,52 @@ class Report(BaseModel):
|
|
|
548
605
|
def average_rating(self) -> Optional[float]:
|
|
549
606
|
"""Convenience accessor matching cosine_similarity / jaccard_similarity."""
|
|
550
607
|
return self.statistics.average_rating if self.statistics is not None else None
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class AnalysisLevelProgress(BaseModel):
|
|
611
|
+
total: int = 0
|
|
612
|
+
completed: int = 0
|
|
613
|
+
failed: int = 0
|
|
614
|
+
percentage: int = 0
|
|
615
|
+
|
|
616
|
+
class Config:
|
|
617
|
+
extra = "ignore"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class AnalysisProgress(BaseModel):
|
|
621
|
+
overall_percentage: int = Field(default=0, alias="overallPercentage")
|
|
622
|
+
current_level: Optional[str] = Field(default=None, alias="currentLevel")
|
|
623
|
+
levels: Dict[str, AnalysisLevelProgress] = Field(default_factory=dict)
|
|
624
|
+
|
|
625
|
+
class Config:
|
|
626
|
+
populate_by_name = True
|
|
627
|
+
extra = "ignore"
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class AnalysisFailureReason(BaseModel):
|
|
631
|
+
code: str
|
|
632
|
+
message: str
|
|
633
|
+
retryable: bool = False
|
|
634
|
+
|
|
635
|
+
class Config:
|
|
636
|
+
extra = "ignore"
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
class AnalysisStatus(BaseModel):
|
|
640
|
+
"""Returned by ``client.evaluations.run(...).analyze()``'s polling loop
|
|
641
|
+
(``EvaluationsClient.get_analysis_status``). ``status`` is terminal once it's one of
|
|
642
|
+
"completed", "partially_failed", or "failed"."""
|
|
643
|
+
|
|
644
|
+
job_id: Optional[str] = Field(default=None, alias="jobId")
|
|
645
|
+
status: str = "not_started"
|
|
646
|
+
progress: AnalysisProgress = Field(default_factory=AnalysisProgress)
|
|
647
|
+
failure_reason: Optional[AnalysisFailureReason] = Field(default=None, alias="failureReason")
|
|
648
|
+
warnings: List[Dict[str, Any]] = Field(default_factory=list)
|
|
649
|
+
|
|
650
|
+
class Config:
|
|
651
|
+
populate_by_name = True
|
|
652
|
+
extra = "ignore"
|
|
653
|
+
|
|
654
|
+
@property
|
|
655
|
+
def is_terminal(self) -> bool:
|
|
656
|
+
return self.status in ("completed", "partially_failed", "failed")
|
|
@@ -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
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import time
|
|
4
5
|
import uuid
|
|
5
6
|
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
|
6
7
|
|
|
@@ -9,6 +10,7 @@ from agentx.evaluations.adapters.precomputed import PrecomputedAdapter
|
|
|
9
10
|
from agentx.evaluations.adapters.http_endpoint import HttpEndpointAdapter
|
|
10
11
|
from agentx.evaluations.client import EvaluationsClient
|
|
11
12
|
from agentx.evaluations.models import (
|
|
13
|
+
AnalysisStatus,
|
|
12
14
|
Dataset,
|
|
13
15
|
EvaluationCase,
|
|
14
16
|
EvaluationResult,
|
|
@@ -43,6 +45,15 @@ AdapterLike = Union[
|
|
|
43
45
|
HttpEndpointAdapter,
|
|
44
46
|
]
|
|
45
47
|
|
|
48
|
+
_ANALYSIS_LEVEL_LABELS = {
|
|
49
|
+
"l1_score": "scoring responses",
|
|
50
|
+
"l2_question_reduce": "reducing questions",
|
|
51
|
+
"l3_cluster_reduce": "reducing clusters",
|
|
52
|
+
"l4_final_reduce": "writing final report",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_DEFAULT_JUDGE_MODEL = "gpt-5.5"
|
|
56
|
+
|
|
46
57
|
|
|
47
58
|
class EvaluationRunContext:
|
|
48
59
|
"""
|
|
@@ -82,7 +93,7 @@ class EvaluationRunContext:
|
|
|
82
93
|
def execute(self, adapter: AdapterLike) -> "EvaluationRunContext":
|
|
83
94
|
"""Run all cases locally and submit batches to AgentX."""
|
|
84
95
|
normalized = _wrap_adapter(adapter)
|
|
85
|
-
cases = _build_cases(self._dataset, self._evaluation_settings)
|
|
96
|
+
cases = _build_cases(self._dataset, self._run, self._evaluation_settings)
|
|
86
97
|
max_batch = self._run.limits.max_batch_size
|
|
87
98
|
|
|
88
99
|
# Banner
|
|
@@ -97,6 +108,7 @@ class EvaluationRunContext:
|
|
|
97
108
|
if self._evaluation_settings
|
|
98
109
|
else self._dataset.number_of_requests
|
|
99
110
|
)
|
|
111
|
+
n_smoke = sum(1 for c in cases if c.is_smoke_test_variant)
|
|
100
112
|
|
|
101
113
|
print(cyan(sep))
|
|
102
114
|
print(f" {bold('AgentX Evaluation')} {dim('—')} {name}")
|
|
@@ -105,9 +117,11 @@ class EvaluationRunContext:
|
|
|
105
117
|
if display:
|
|
106
118
|
print(f" {dim('Agent :')} {display} {dim(f'({framework} / {runtime})')}")
|
|
107
119
|
print()
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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)
|
|
111
125
|
|
|
112
126
|
# Resume: skip already-submitted keys
|
|
113
127
|
already_done = self._fetch_submitted_keys()
|
|
@@ -228,12 +242,61 @@ class EvaluationRunContext:
|
|
|
228
242
|
# Step 3: analyze + report
|
|
229
243
|
# ------------------------------------------------------------------
|
|
230
244
|
|
|
231
|
-
def analyze(
|
|
245
|
+
def analyze(
|
|
246
|
+
self,
|
|
247
|
+
mode: Optional[str] = None,
|
|
248
|
+
quality_mode: Optional[str] = None,
|
|
249
|
+
judges: Optional[List[str]] = None,
|
|
250
|
+
poll_interval: float = 5.0,
|
|
251
|
+
timeout: float = 1800.0,
|
|
252
|
+
) -> Report:
|
|
253
|
+
"""Generate the qualitative AI analysis report.
|
|
254
|
+
|
|
255
|
+
Runs the same durable, multi-stage pipeline as the dashboard's "Analyze" button: each
|
|
256
|
+
response is scored by 1-3 LLM judges (``judges``), then reduced into the final report.
|
|
257
|
+
This starts the job and polls until it finishes, which can take noticeably longer than a
|
|
258
|
+
single LLM call for larger runs.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
mode: "auto" (default), "sync", or "batch" - how item scoring executes server-side.
|
|
262
|
+
quality_mode: "quality_first" or "balanced" - how many items get a second/third judge.
|
|
263
|
+
judges: 1-3 model ids, e.g. ``["gpt-5.5", "claude-opus-4-8"]``. Defaults to a single
|
|
264
|
+
judge, ``["gpt-5.5"]``, rather than the dashboard's 3-judge default - SDK runs are
|
|
265
|
+
typically lighter-weight, quick-start evaluations.
|
|
266
|
+
poll_interval: seconds between status checks while waiting.
|
|
267
|
+
timeout: give up waiting after this many seconds (the job keeps running server-side;
|
|
268
|
+
call ``get_report()`` later to check on it).
|
|
269
|
+
"""
|
|
270
|
+
if judges is not None and not (1 <= len(judges) <= 3):
|
|
271
|
+
raise ValueError("judges must contain 1-3 model ids")
|
|
272
|
+
resolved_judges = judges if judges is not None else [_DEFAULT_JUDGE_MODEL]
|
|
273
|
+
|
|
232
274
|
print()
|
|
233
|
-
with Spinner("Analyzing — AI is reviewing your results"):
|
|
275
|
+
with Spinner("Analyzing — AI is reviewing your results") as spinner:
|
|
234
276
|
try:
|
|
235
|
-
self._client.analyze_run(
|
|
236
|
-
|
|
277
|
+
self._client.analyze_run(
|
|
278
|
+
self._run.run_id,
|
|
279
|
+
mode=mode,
|
|
280
|
+
quality_mode=quality_mode,
|
|
281
|
+
judges=resolved_judges,
|
|
282
|
+
)
|
|
283
|
+
deadline = time.monotonic() + timeout
|
|
284
|
+
status = self._client.get_analysis_status(self._run.run_id)
|
|
285
|
+
while not status.is_terminal and time.monotonic() < deadline:
|
|
286
|
+
level = _ANALYSIS_LEVEL_LABELS.get(status.progress.current_level, "")
|
|
287
|
+
spinner.update(
|
|
288
|
+
f"Analyzing, {level + ' ' if level else ''}{status.progress.overall_percentage}%"
|
|
289
|
+
)
|
|
290
|
+
time.sleep(poll_interval)
|
|
291
|
+
status = self._client.get_analysis_status(self._run.run_id)
|
|
292
|
+
|
|
293
|
+
if not status.is_terminal:
|
|
294
|
+
print(f" {yellow('!')} Still running after {int(timeout)}s, check the dashboard for status")
|
|
295
|
+
elif status.status == "failed":
|
|
296
|
+
reason = status.failure_reason.message if status.failure_reason else "unknown error"
|
|
297
|
+
print(f" {red('✗')} Analyze failed: {dim(reason)}")
|
|
298
|
+
else:
|
|
299
|
+
print(f" {green('✓')} Analysis complete")
|
|
237
300
|
except Exception as exc:
|
|
238
301
|
print(f" {red('✗')} Analyze failed: {dim(str(exc))}")
|
|
239
302
|
logger.warning("Analyze request failed: %s", exc)
|
|
@@ -280,6 +343,12 @@ class EvaluationsRunner:
|
|
|
280
343
|
against."""
|
|
281
344
|
return self._client.list_models(provider)
|
|
282
345
|
|
|
346
|
+
def get_analysis_status(self, run_id: str) -> AnalysisStatus:
|
|
347
|
+
"""Check on an in-progress ``.analyze()`` job by run id, without needing
|
|
348
|
+
the ``EvaluationRunContext`` that started it (e.g. from a separate
|
|
349
|
+
script execution)."""
|
|
350
|
+
return self._client.get_analysis_status(run_id)
|
|
351
|
+
|
|
283
352
|
def run(
|
|
284
353
|
self,
|
|
285
354
|
dataset_id: str,
|
|
@@ -334,7 +403,9 @@ def _wrap_adapter(adapter: AdapterLike) -> Callable[[EvaluationCase], Evaluation
|
|
|
334
403
|
|
|
335
404
|
|
|
336
405
|
def _build_cases(
|
|
337
|
-
dataset: Dataset,
|
|
406
|
+
dataset: Dataset,
|
|
407
|
+
run: EvaluationRun,
|
|
408
|
+
evaluation_settings: Optional[EvaluationSettings] = None,
|
|
338
409
|
) -> List[EvaluationCase]:
|
|
339
410
|
cases: List[EvaluationCase] = []
|
|
340
411
|
# When an independent evaluation_settings was chosen (evaluation_settings_id
|
|
@@ -353,6 +424,14 @@ def _build_cases(
|
|
|
353
424
|
evaluation_settings.sovereignty_models if evaluation_settings else dataset.sovereignty_models
|
|
354
425
|
)
|
|
355
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
|
+
}
|
|
356
435
|
for q_idx, question in enumerate(dataset.questions):
|
|
357
436
|
mq = question.main_question
|
|
358
437
|
for run_num in range(1, n_runs + 1):
|
|
@@ -371,6 +450,21 @@ def _build_cases(
|
|
|
371
450
|
model=model,
|
|
372
451
|
)
|
|
373
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
|
+
)
|
|
374
468
|
return cases
|
|
375
469
|
|
|
376
470
|
|
|
@@ -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)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.6.8"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentx-python
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.8
|
|
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
|
---
|
|
@@ -286,9 +293,16 @@ report = (
|
|
|
286
293
|
print(report.average_rating) # LLM-graded score, 0–10
|
|
287
294
|
print(report.cosine_similarity) # embedding cosine, 0–1 (None if not enabled)
|
|
288
295
|
print(report.jaccard_similarity) # token-set overlap, 0–1 (None if not enabled)
|
|
296
|
+
|
|
297
|
+
print(report.summary) # AI-generated narrative from .analyze()
|
|
298
|
+
print(report.recommendations) # list of prioritized, actionable fixes
|
|
289
299
|
```
|
|
290
300
|
|
|
291
|
-
|
|
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.
|
|
302
|
+
|
|
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.
|
|
292
306
|
|
|
293
307
|
---
|
|
294
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.6"
|
|
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
|