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.
Files changed (56) hide show
  1. {agentx_python-0.6.6/agentx_python.egg-info → agentx_python-0.6.8}/PKG-INFO +17 -3
  2. {agentx_python-0.6.6 → agentx_python-0.6.8}/README.md +16 -2
  3. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/agentx.py +2 -1
  4. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/_term.py +6 -0
  5. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/client.py +23 -2
  6. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/datasets.py +32 -1
  7. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/evaluation_settings.py +13 -0
  8. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/models.py +115 -9
  9. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/results.py +6 -0
  10. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/runner.py +103 -9
  11. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/monitor/__init__.py +5 -1
  12. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/monitor/client.py +35 -1
  13. agentx_python-0.6.8/agentx/monitor/models.py +85 -0
  14. agentx_python-0.6.8/agentx/monitor/signals.py +41 -0
  15. agentx_python-0.6.8/agentx/version.py +1 -0
  16. {agentx_python-0.6.6 → agentx_python-0.6.8/agentx_python.egg-info}/PKG-INFO +17 -3
  17. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/SOURCES.txt +1 -0
  18. agentx_python-0.6.6/agentx/monitor/models.py +0 -37
  19. agentx_python-0.6.6/agentx/version.py +0 -1
  20. {agentx_python-0.6.6 → agentx_python-0.6.8}/LICENSE +0 -0
  21. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/__init__.py +0 -0
  22. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/__init__.py +0 -0
  23. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/adapters/__init__.py +0 -0
  24. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  25. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/adapters/precomputed.py +0 -0
  26. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/adapters/raw.py +0 -0
  27. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/redaction.py +0 -0
  28. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/reporting.py +0 -0
  29. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/evaluations/tracing.py +0 -0
  30. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/exceptions.py +0 -0
  31. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/__init__.py +0 -0
  32. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/_perf.py +0 -0
  33. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/anthropic.py +0 -0
  34. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/crewai.py +0 -0
  35. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/google_adk.py +0 -0
  36. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/google_genai.py +0 -0
  37. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/langchain.py +0 -0
  38. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/integrations/openai_agents.py +0 -0
  39. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/monitor/patterns.py +0 -0
  40. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/py.typed +0 -0
  41. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/resources/__init__.py +0 -0
  42. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/resources/agent.py +0 -0
  43. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/resources/conversation.py +0 -0
  44. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/resources/workforce.py +0 -0
  45. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/tracing/__init__.py +0 -0
  46. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/tracing/ci_types.py +0 -0
  47. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/tracing/ingest_client.py +0 -0
  48. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/tracing/tracer.py +0 -0
  49. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx/util.py +0 -0
  50. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/dependency_links.txt +0 -0
  51. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/not-zip-safe +0 -0
  52. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/requires.txt +0 -0
  53. {agentx_python-0.6.6 → agentx_python-0.6.8}/agentx_python.egg-info/top_level.txt +0 -0
  54. {agentx_python-0.6.6 → agentx_python-0.6.8}/setup.cfg +0 -0
  55. {agentx_python-0.6.6 → agentx_python-0.6.8}/setup.py +0 -0
  56. {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.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
- See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide dataset builder, framework adapters, similarity metrics, and the complete API reference.
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
- See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide dataset builder, framework adapters, similarity metrics, and the complete API reference.
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(self, run_id: str) -> Dict[str, Any]:
242
- return self._request("POST", f"/runs/{run_id}/analyze", json={}, timeout=300)
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
- main = {"query": query}
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 Report(BaseModel):
442
- run_id: str = Field(alias="runId")
443
- dataset_id: str = Field(alias="datasetId")
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
- print(
109
- f"{bold('Executing')} {n_q} question{'s' if n_q != 1 else ''} × {n_r} run{'s' if n_r != 1 else ''}"
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(self) -> Report:
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(self._run.run_id)
236
- print(f" {green('✓')} Analysis complete")
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, evaluation_settings: Optional[EvaluationSettings] = None
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.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
- See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide dataset builder, framework adapters, similarity metrics, and the complete API reference.
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
 
@@ -34,6 +34,7 @@ agentx/monitor/__init__.py
34
34
  agentx/monitor/client.py
35
35
  agentx/monitor/models.py
36
36
  agentx/monitor/patterns.py
37
+ agentx/monitor/signals.py
37
38
  agentx/resources/__init__.py
38
39
  agentx/resources/agent.py
39
40
  agentx/resources/conversation.py
@@ -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