agentx-python 0.6.5__tar.gz → 0.6.7__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 (54) hide show
  1. {agentx_python-0.6.5 → agentx_python-0.6.7}/PKG-INFO +32 -1
  2. {agentx_python-0.6.5 → agentx_python-0.6.7}/README.md +31 -0
  3. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/agentx.py +10 -0
  4. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/_term.py +6 -0
  5. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/client.py +23 -2
  6. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/models.py +63 -5
  7. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/runner.py +70 -4
  8. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/integrations/crewai.py +7 -1
  9. agentx_python-0.6.7/agentx/monitor/__init__.py +10 -0
  10. agentx_python-0.6.7/agentx/monitor/client.py +134 -0
  11. agentx_python-0.6.7/agentx/monitor/models.py +37 -0
  12. agentx_python-0.6.7/agentx/monitor/patterns.py +121 -0
  13. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/tracing/tracer.py +30 -0
  14. agentx_python-0.6.7/agentx/version.py +1 -0
  15. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx_python.egg-info/PKG-INFO +32 -1
  16. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx_python.egg-info/SOURCES.txt +4 -0
  17. agentx_python-0.6.5/agentx/version.py +0 -1
  18. {agentx_python-0.6.5 → agentx_python-0.6.7}/LICENSE +0 -0
  19. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/__init__.py +0 -0
  20. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/__init__.py +0 -0
  21. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/adapters/__init__.py +0 -0
  22. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  23. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/adapters/precomputed.py +0 -0
  24. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/adapters/raw.py +0 -0
  25. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/datasets.py +0 -0
  26. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/evaluation_settings.py +0 -0
  27. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/redaction.py +0 -0
  28. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/reporting.py +0 -0
  29. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/results.py +0 -0
  30. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/evaluations/tracing.py +0 -0
  31. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/exceptions.py +0 -0
  32. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/integrations/__init__.py +0 -0
  33. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/integrations/_perf.py +0 -0
  34. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/integrations/anthropic.py +0 -0
  35. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/integrations/google_adk.py +0 -0
  36. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/integrations/google_genai.py +0 -0
  37. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/integrations/langchain.py +0 -0
  38. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/integrations/openai_agents.py +0 -0
  39. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/py.typed +0 -0
  40. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/resources/__init__.py +0 -0
  41. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/resources/agent.py +0 -0
  42. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/resources/conversation.py +0 -0
  43. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/resources/workforce.py +0 -0
  44. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/tracing/__init__.py +0 -0
  45. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/tracing/ci_types.py +0 -0
  46. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/tracing/ingest_client.py +0 -0
  47. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx/util.py +0 -0
  48. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx_python.egg-info/dependency_links.txt +0 -0
  49. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx_python.egg-info/not-zip-safe +0 -0
  50. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx_python.egg-info/requires.txt +0 -0
  51. {agentx_python-0.6.5 → agentx_python-0.6.7}/agentx_python.egg-info/top_level.txt +0 -0
  52. {agentx_python-0.6.5 → agentx_python-0.6.7}/setup.cfg +0 -0
  53. {agentx_python-0.6.5 → agentx_python-0.6.7}/setup.py +0 -0
  54. {agentx_python-0.6.5 → agentx_python-0.6.7}/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.5
3
+ Version: 0.6.7
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
@@ -70,6 +70,7 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
70
70
  - [Chat (streaming and non-streaming)](#chat-streaming-and-non-streaming)
71
71
  - [Workforce (multi-agent orchestration)](#workforce-multi-agent-orchestration) — teams of agents with a designated manager
72
72
  - [Production tracing](#production-tracing) — record live agent runs from any framework
73
+ - [Monitor](#monitor) — automatic production monitoring, patterns and signals
73
74
  - [Custom agent evaluations](#custom-agent-evaluations) — LLM-as-a-judge, cosine / Jaccard similarity
74
75
  - [Links](#links)
75
76
 
@@ -245,6 +246,30 @@ See **[TRACING.md](TRACING.md)** for the complete guide — session grouping, er
245
246
 
246
247
  ---
247
248
 
249
+ ## Monitor
250
+
251
+ Automatic production monitoring: check traces against detection patterns and get back triage-ready signals. A **pattern** is a first-class SDK resource with a real id, just like a `Dataset` or `EvaluationSettings`: create one once, then reference it by id at trace time.
252
+
253
+ ```python
254
+ pattern = client.monitor.patterns.builder(
255
+ name="Promises a refund",
256
+ detector_kind="semantic",
257
+ semantic_prompt="The response promises a refund.",
258
+ severity="high",
259
+ ).publish()
260
+
261
+ with client.tracer.trace("support-agent", monitor=True, pattern_ids=[pattern.id]) as span:
262
+ span.output = call_llm(query)
263
+ ```
264
+
265
+ `monitor=True` checks the trace immediately, no dashboard setup required. `pattern_ids` restricts detection to exactly those patterns; omit it to run the full default sweep instead (built-in checks like empty response, trace error, and latency regression, plus every pattern enabled for the workspace).
266
+
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
+
269
+ See **[TRACING.md](TRACING.md)** for the complete Monitor guide.
270
+
271
+ ---
272
+
248
273
  ## Custom agent evaluations
249
274
 
250
275
  Evaluate **any** AI agent — LangChain, CrewAI, AutoGen, LlamaIndex, OpenAI, Anthropic, HTTP endpoints, or plain Python — using AgentX as the scoring and reporting backend. Includes optional **cosine** and **Jaccard** similarity metrics alongside LLM-graded ratings.
@@ -261,8 +286,13 @@ report = (
261
286
  print(report.average_rating) # LLM-graded score, 0–10
262
287
  print(report.cosine_similarity) # embedding cosine, 0–1 (None if not enabled)
263
288
  print(report.jaccard_similarity) # token-set overlap, 0–1 (None if not enabled)
289
+
290
+ print(report.summary) # AI-generated narrative from .analyze()
291
+ print(report.recommendations) # list of prioritized, actionable fixes
264
292
  ```
265
293
 
294
+ `.analyze()` also generates a full qualitative report (strengths, weaknesses, instruction adherence, reasoning quality, and recommendations), running the same durable, multi-judge pipeline as the dashboard's "Analyze" button. `analyze(mode=..., quality_mode=..., judges=[...])` controls how items are scored and by which models. See [AI analysis report](EVALUATIONS.md#ai-analysis-report) in the full guide for the complete field and parameter reference.
295
+
266
296
  See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder, framework adapters, similarity metrics, and the complete API reference.
267
297
 
268
298
  ---
@@ -274,3 +304,4 @@ See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder,
274
304
  - **PyPI** — [agentx-python](https://pypi.org/project/agentx-python/)
275
305
  - **Tracing docs** — [TRACING.md](TRACING.md)
276
306
  - **Evaluations docs** — [EVALUATIONS.md](EVALUATIONS.md)
307
+ - **Monitor docs** — [docs.agentx.so/sdk/monitor](https://docs.agentx.so/sdk/monitor)
@@ -22,6 +22,7 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
22
22
  - [Chat (streaming and non-streaming)](#chat-streaming-and-non-streaming)
23
23
  - [Workforce (multi-agent orchestration)](#workforce-multi-agent-orchestration) — teams of agents with a designated manager
24
24
  - [Production tracing](#production-tracing) — record live agent runs from any framework
25
+ - [Monitor](#monitor) — automatic production monitoring, patterns and signals
25
26
  - [Custom agent evaluations](#custom-agent-evaluations) — LLM-as-a-judge, cosine / Jaccard similarity
26
27
  - [Links](#links)
27
28
 
@@ -197,6 +198,30 @@ See **[TRACING.md](TRACING.md)** for the complete guide — session grouping, er
197
198
 
198
199
  ---
199
200
 
201
+ ## Monitor
202
+
203
+ Automatic production monitoring: check traces against detection patterns and get back triage-ready signals. A **pattern** is a first-class SDK resource with a real id, just like a `Dataset` or `EvaluationSettings`: create one once, then reference it by id at trace time.
204
+
205
+ ```python
206
+ pattern = client.monitor.patterns.builder(
207
+ name="Promises a refund",
208
+ detector_kind="semantic",
209
+ semantic_prompt="The response promises a refund.",
210
+ severity="high",
211
+ ).publish()
212
+
213
+ with client.tracer.trace("support-agent", monitor=True, pattern_ids=[pattern.id]) as span:
214
+ span.output = call_llm(query)
215
+ ```
216
+
217
+ `monitor=True` checks the trace immediately, no dashboard setup required. `pattern_ids` restricts detection to exactly those patterns; omit it to run the full default sweep instead (built-in checks like empty response, trace error, and latency regression, plus every pattern enabled for the workspace).
218
+
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
+
221
+ See **[TRACING.md](TRACING.md)** for the complete Monitor guide.
222
+
223
+ ---
224
+
200
225
  ## Custom agent evaluations
201
226
 
202
227
  Evaluate **any** AI agent — LangChain, CrewAI, AutoGen, LlamaIndex, OpenAI, Anthropic, HTTP endpoints, or plain Python — using AgentX as the scoring and reporting backend. Includes optional **cosine** and **Jaccard** similarity metrics alongside LLM-graded ratings.
@@ -213,8 +238,13 @@ report = (
213
238
  print(report.average_rating) # LLM-graded score, 0–10
214
239
  print(report.cosine_similarity) # embedding cosine, 0–1 (None if not enabled)
215
240
  print(report.jaccard_similarity) # token-set overlap, 0–1 (None if not enabled)
241
+
242
+ print(report.summary) # AI-generated narrative from .analyze()
243
+ print(report.recommendations) # list of prioritized, actionable fixes
216
244
  ```
217
245
 
246
+ `.analyze()` also generates a full qualitative report (strengths, weaknesses, instruction adherence, reasoning quality, and recommendations), running the same durable, multi-judge pipeline as the dashboard's "Analyze" button. `analyze(mode=..., quality_mode=..., judges=[...])` controls how items are scored and by which models. See [AI analysis report](EVALUATIONS.md#ai-analysis-report) in the full guide for the complete field and parameter reference.
247
+
218
248
  See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder, framework adapters, similarity metrics, and the complete API reference.
219
249
 
220
250
  ---
@@ -226,3 +256,4 @@ See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder,
226
256
  - **PyPI** — [agentx-python](https://pypi.org/project/agentx-python/)
227
257
  - **Tracing docs** — [TRACING.md](TRACING.md)
228
258
  - **Evaluations docs** — [EVALUATIONS.md](EVALUATIONS.md)
259
+ - **Monitor docs** — [docs.agentx.so/sdk/monitor](https://docs.agentx.so/sdk/monitor)
@@ -29,6 +29,7 @@ class AgentX:
29
29
 
30
30
  from agentx.evaluations.client import EvaluationsClient
31
31
  from agentx.evaluations.runner import EvaluationsRunner
32
+ from agentx.monitor.client import MonitorClient
32
33
  from agentx.tracing.ingest_client import IngestClient
33
34
  from agentx.tracing.tracer import Tracer
34
35
  from agentx.version import VERSION
@@ -41,6 +42,15 @@ class AgentX:
41
42
  )
42
43
  self.evaluations = EvaluationsRunner(_eval_client)
43
44
 
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=[...]).
47
+ self.monitor = MonitorClient(
48
+ api_key=self.api_key,
49
+ sdk_version=VERSION,
50
+ base_url=self.base_url,
51
+ workspace_id=self.workspace_id,
52
+ )
53
+
44
54
  _ingest_client = IngestClient(
45
55
  api_key=self.api_key,
46
56
  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}")
@@ -438,11 +438,9 @@ class SovereigntyIndex(BaseModel):
438
438
  extra = "ignore"
439
439
 
440
440
 
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
441
+ class AnalysisResult(BaseModel):
442
+ """Shared qualitative-report fields, produced by ``client.evaluations.run(...).analyze()``."""
443
+
446
444
  summary: Optional[str] = None
447
445
  consistency_score: Optional[float] = Field(default=None, alias="consistencyScore")
448
446
  instruction_adherence: Optional[ReportInstructionAdherence] = Field(
@@ -461,6 +459,17 @@ class Report(BaseModel):
461
459
  weaknesses: List[str] = Field(default_factory=list)
462
460
  overall_rating: Optional[str] = Field(default=None, alias="overallRating")
463
461
  recommendations: List[ReportRecommendation] = Field(default_factory=list)
462
+
463
+ class Config:
464
+ populate_by_name = True
465
+ extra = "ignore"
466
+
467
+
468
+ class Report(AnalysisResult):
469
+ run_id: str = Field(alias="runId")
470
+ dataset_id: str = Field(alias="datasetId")
471
+ status: str = "completed"
472
+ statistics: Optional[ReportStatistics] = None
464
473
  low_scoring_cases: List[Dict[str, Any]] = Field(
465
474
  default_factory=list, alias="lowScoringCases"
466
475
  )
@@ -548,3 +557,52 @@ class Report(BaseModel):
548
557
  def average_rating(self) -> Optional[float]:
549
558
  """Convenience accessor matching cosine_similarity / jaccard_similarity."""
550
559
  return self.statistics.average_rating if self.statistics is not None else None
560
+
561
+
562
+ class AnalysisLevelProgress(BaseModel):
563
+ total: int = 0
564
+ completed: int = 0
565
+ failed: int = 0
566
+ percentage: int = 0
567
+
568
+ class Config:
569
+ extra = "ignore"
570
+
571
+
572
+ class AnalysisProgress(BaseModel):
573
+ overall_percentage: int = Field(default=0, alias="overallPercentage")
574
+ current_level: Optional[str] = Field(default=None, alias="currentLevel")
575
+ levels: Dict[str, AnalysisLevelProgress] = Field(default_factory=dict)
576
+
577
+ class Config:
578
+ populate_by_name = True
579
+ extra = "ignore"
580
+
581
+
582
+ class AnalysisFailureReason(BaseModel):
583
+ code: str
584
+ message: str
585
+ retryable: bool = False
586
+
587
+ class Config:
588
+ extra = "ignore"
589
+
590
+
591
+ class AnalysisStatus(BaseModel):
592
+ """Returned by ``client.evaluations.run(...).analyze()``'s polling loop
593
+ (``EvaluationsClient.get_analysis_status``). ``status`` is terminal once it's one of
594
+ "completed", "partially_failed", or "failed"."""
595
+
596
+ job_id: Optional[str] = Field(default=None, alias="jobId")
597
+ status: str = "not_started"
598
+ progress: AnalysisProgress = Field(default_factory=AnalysisProgress)
599
+ failure_reason: Optional[AnalysisFailureReason] = Field(default=None, alias="failureReason")
600
+ warnings: List[Dict[str, Any]] = Field(default_factory=list)
601
+
602
+ class Config:
603
+ populate_by_name = True
604
+ extra = "ignore"
605
+
606
+ @property
607
+ def is_terminal(self) -> bool:
608
+ return self.status in ("completed", "partially_failed", "failed")
@@ -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
  """
@@ -228,12 +239,61 @@ class EvaluationRunContext:
228
239
  # Step 3: analyze + report
229
240
  # ------------------------------------------------------------------
230
241
 
231
- def analyze(self) -> Report:
242
+ def analyze(
243
+ self,
244
+ mode: Optional[str] = None,
245
+ quality_mode: Optional[str] = None,
246
+ judges: Optional[List[str]] = None,
247
+ poll_interval: float = 5.0,
248
+ timeout: float = 1800.0,
249
+ ) -> Report:
250
+ """Generate the qualitative AI analysis report.
251
+
252
+ Runs the same durable, multi-stage pipeline as the dashboard's "Analyze" button: each
253
+ response is scored by 1-3 LLM judges (``judges``), then reduced into the final report.
254
+ This starts the job and polls until it finishes, which can take noticeably longer than a
255
+ single LLM call for larger runs.
256
+
257
+ Args:
258
+ mode: "auto" (default), "sync", or "batch" - how item scoring executes server-side.
259
+ quality_mode: "quality_first" or "balanced" - how many items get a second/third judge.
260
+ judges: 1-3 model ids, e.g. ``["gpt-5.5", "claude-opus-4-8"]``. Defaults to a single
261
+ judge, ``["gpt-5.5"]``, rather than the dashboard's 3-judge default - SDK runs are
262
+ typically lighter-weight, quick-start evaluations.
263
+ poll_interval: seconds between status checks while waiting.
264
+ timeout: give up waiting after this many seconds (the job keeps running server-side;
265
+ call ``get_report()`` later to check on it).
266
+ """
267
+ if judges is not None and not (1 <= len(judges) <= 3):
268
+ raise ValueError("judges must contain 1-3 model ids")
269
+ resolved_judges = judges if judges is not None else [_DEFAULT_JUDGE_MODEL]
270
+
232
271
  print()
233
- with Spinner("Analyzing — AI is reviewing your results"):
272
+ with Spinner("Analyzing — AI is reviewing your results") as spinner:
234
273
  try:
235
- self._client.analyze_run(self._run.run_id)
236
- print(f" {green('✓')} Analysis complete")
274
+ self._client.analyze_run(
275
+ self._run.run_id,
276
+ mode=mode,
277
+ quality_mode=quality_mode,
278
+ judges=resolved_judges,
279
+ )
280
+ deadline = time.monotonic() + timeout
281
+ status = self._client.get_analysis_status(self._run.run_id)
282
+ while not status.is_terminal and time.monotonic() < deadline:
283
+ level = _ANALYSIS_LEVEL_LABELS.get(status.progress.current_level, "")
284
+ spinner.update(
285
+ f"Analyzing, {level + ' ' if level else ''}{status.progress.overall_percentage}%"
286
+ )
287
+ time.sleep(poll_interval)
288
+ status = self._client.get_analysis_status(self._run.run_id)
289
+
290
+ if not status.is_terminal:
291
+ print(f" {yellow('!')} Still running after {int(timeout)}s, check the dashboard for status")
292
+ elif status.status == "failed":
293
+ reason = status.failure_reason.message if status.failure_reason else "unknown error"
294
+ print(f" {red('✗')} Analyze failed: {dim(reason)}")
295
+ else:
296
+ print(f" {green('✓')} Analysis complete")
237
297
  except Exception as exc:
238
298
  print(f" {red('✗')} Analyze failed: {dim(str(exc))}")
239
299
  logger.warning("Analyze request failed: %s", exc)
@@ -280,6 +340,12 @@ class EvaluationsRunner:
280
340
  against."""
281
341
  return self._client.list_models(provider)
282
342
 
343
+ def get_analysis_status(self, run_id: str) -> AnalysisStatus:
344
+ """Check on an in-progress ``.analyze()`` job by run id, without needing
345
+ the ``EvaluationRunContext`` that started it (e.g. from a separate
346
+ script execution)."""
347
+ return self._client.get_analysis_status(run_id)
348
+
283
349
  def run(
284
350
  self,
285
351
  dataset_id: str,
@@ -121,8 +121,13 @@ class AgentXCrewObserver:
121
121
  input: Optional[Any] = None,
122
122
  metadata: Optional[Dict[str, Any]] = None,
123
123
  session_id: Optional[str] = None,
124
+ sync: bool = False,
124
125
  ):
125
- """Return a context-manager span for manual kickoff wrapping."""
126
+ """Return a context-manager span for manual kickoff wrapping.
127
+
128
+ Pass ``sync=True`` to send synchronously so ``span.trace_id`` is populated once the
129
+ block exits — e.g. to attach the trace to an evaluation result. See ``Tracer.trace()``.
130
+ """
126
131
  from agentx.tracing.tracer import _TraceSpan
127
132
 
128
133
  return _TraceSpan(
@@ -132,4 +137,5 @@ class AgentXCrewObserver:
132
137
  metadata=metadata or self._metadata,
133
138
  framework="crewai",
134
139
  session_id=session_id or self._session_id,
140
+ sync=sync,
135
141
  )
@@ -0,0 +1,10 @@
1
+ from agentx.monitor.client import MonitorClient
2
+ from agentx.monitor.models import MonitorPattern
3
+ from agentx.monitor.patterns import MonitorPatternBuilder, MonitorPatternClient
4
+
5
+ __all__ = [
6
+ "MonitorClient",
7
+ "MonitorPattern",
8
+ "MonitorPatternBuilder",
9
+ "MonitorPatternClient",
10
+ ]
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import time
6
+ from typing import Any, List, Optional
7
+
8
+ import requests
9
+
10
+ from agentx.monitor.models import MonitorPattern
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ from agentx.util import _DEFAULT_API_BASE as _UTIL_API_BASE
15
+
16
+ SDK_NAME = "agentx-python"
17
+
18
+ _RETRYABLE_STATUS = {429, 500, 502, 503, 504}
19
+ _MAX_RETRIES = 3
20
+ _RETRY_BACKOFF = [1.0, 2.0, 4.0]
21
+
22
+
23
+ class AgentXMonitorError(Exception):
24
+ pass
25
+
26
+
27
+ class AgentXAuthError(AgentXMonitorError):
28
+ pass
29
+
30
+
31
+ class AgentXValidationError(AgentXMonitorError):
32
+ pass
33
+
34
+
35
+ class MonitorClient:
36
+ """Low-level HTTP client for the Monitor API (``/monitor``). Accessed via
37
+ ``client.monitor`` on the top-level :class:`agentx.AgentX` instance; most callers
38
+ should use ``client.monitor.patterns.builder(...)`` instead of this directly."""
39
+
40
+ def __init__(
41
+ self,
42
+ api_key: str,
43
+ sdk_version: str = "unknown",
44
+ base_url: Optional[str] = None,
45
+ workspace_id: Optional[str] = None,
46
+ ):
47
+ if not api_key:
48
+ raise AgentXAuthError("AGENTX_API_KEY is required")
49
+ self._api_key = api_key
50
+ self._sdk_version = sdk_version
51
+ # Falls back to the caller's default workspace server-side when unset — mirrors
52
+ # EvaluationsClient. Without this, pattern creation silently lands in whatever
53
+ # workspace the API key's user defaults to, not the one the caller intended.
54
+ self._workspace_id = workspace_id
55
+ _api_base = (
56
+ base_url or os.getenv("AGENTX_API_BASE_URL", _UTIL_API_BASE)
57
+ ).rstrip("/")
58
+ if not _api_base.endswith("/monitor"):
59
+ _api_base = f"{_api_base}/monitor"
60
+ self._base_url = _api_base
61
+ self._session = requests.Session()
62
+ self._session.headers.update(
63
+ {
64
+ "x-api-key": self._api_key,
65
+ "Content-Type": "application/json",
66
+ "User-Agent": f"{SDK_NAME}/{self._sdk_version}",
67
+ "accept": "*/*",
68
+ }
69
+ )
70
+
71
+ from agentx.monitor.patterns import MonitorPatternClient
72
+
73
+ self.patterns = MonitorPatternClient(self)
74
+
75
+ # ------------------------------------------------------------------
76
+ # Low-level HTTP
77
+ # ------------------------------------------------------------------
78
+
79
+ def _with_workspace(self, payload: dict) -> dict:
80
+ if self._workspace_id and not payload.get("workspaceId"):
81
+ return {**payload, "workspaceId": self._workspace_id}
82
+ return payload
83
+
84
+ def _workspace_params(self) -> Optional[dict]:
85
+ return {"workspaceId": self._workspace_id} if self._workspace_id else None
86
+
87
+ def _request(self, method: str, path: str, timeout: int = 30, **kwargs) -> Any:
88
+ url = f"{self._base_url}{path}"
89
+ last_exc: Optional[Exception] = None
90
+ for attempt, wait in enumerate([0.0] + _RETRY_BACKOFF):
91
+ if wait:
92
+ time.sleep(wait)
93
+ try:
94
+ resp = self._session.request(method, url, timeout=timeout, **kwargs)
95
+ except requests.RequestException as e:
96
+ last_exc = e
97
+ logger.debug("Request error (attempt %d): %s", attempt + 1, e)
98
+ continue
99
+
100
+ if resp.status_code == 401:
101
+ raise AgentXAuthError("Invalid or missing API key")
102
+ if resp.status_code == 422:
103
+ raise AgentXValidationError(resp.text)
104
+ if resp.status_code in _RETRYABLE_STATUS and attempt < _MAX_RETRIES - 1:
105
+ logger.debug(
106
+ "Retryable status %d (attempt %d)", resp.status_code, attempt + 1
107
+ )
108
+ last_exc = AgentXMonitorError(f"HTTP {resp.status_code}")
109
+ continue
110
+ if not resp.ok:
111
+ raise AgentXMonitorError(f"HTTP {resp.status_code}: {resp.text}")
112
+ try:
113
+ return resp.json()
114
+ except Exception:
115
+ return resp.text
116
+ raise AgentXMonitorError(f"Request failed after retries: {last_exc}")
117
+
118
+ # ------------------------------------------------------------------
119
+ # Pattern endpoints
120
+ # ------------------------------------------------------------------
121
+
122
+ def create_pattern(self, payload: dict) -> MonitorPattern:
123
+ data = self._request("POST", "/patterns", json=self._with_workspace(payload))
124
+ return MonitorPattern(**data["pattern"])
125
+
126
+ def list_patterns(self) -> List[MonitorPattern]:
127
+ data = self._request("GET", "/patterns", params=self._workspace_params())
128
+ return [MonitorPattern(**p) for p in data.get("patterns", [])]
129
+
130
+ def get_pattern(self, pattern_id: str) -> MonitorPattern:
131
+ data = self._request(
132
+ "GET", f"/patterns/{pattern_id}", params=self._workspace_params()
133
+ )
134
+ return MonitorPattern(**data["pattern"])
@@ -0,0 +1,37 @@
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"
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
5
+
6
+ from agentx.monitor.models import MonitorPattern
7
+
8
+ if TYPE_CHECKING:
9
+ from agentx.monitor.client import MonitorClient
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class MonitorPatternBuilder:
15
+ """Fluent builder for creating a custom pattern. ``detector_kind`` selects which of
16
+ ``include_terms``/``regex``/``semantic_prompt`` is used:
17
+
18
+ - ``"contains"`` (default): ``include_terms`` — a match if any (or all, with
19
+ ``match_mode="all"``) phrase appears in the target text.
20
+ - ``"regex"``: ``regex`` — a single regular expression.
21
+ - ``"semantic"``: ``semantic_prompt`` — an LLM judges whether the response violates the
22
+ described rubric.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ client: "MonitorClient",
28
+ name: str,
29
+ description: Optional[str] = None,
30
+ category: Optional[str] = None,
31
+ detector_kind: str = "contains",
32
+ match_target: Optional[List[str]] = None,
33
+ match_mode: str = "any",
34
+ include_terms: Optional[List[str]] = None,
35
+ exclude_terms: Optional[List[str]] = None,
36
+ regex: Optional[str] = None,
37
+ semantic_prompt: Optional[str] = None,
38
+ severity: str = "medium",
39
+ polarity: str = "failure",
40
+ enabled: bool = True,
41
+ sample_rate: float = 1.0,
42
+ scope_mode: str = "all",
43
+ agent_ids: Optional[List[str]] = None,
44
+ ):
45
+ self._client = client
46
+ self._payload: Dict[str, Any] = {
47
+ "name": name,
48
+ "description": description,
49
+ "category": category,
50
+ "detectorKind": detector_kind,
51
+ "matchTarget": match_target or ["response"],
52
+ "matchMode": match_mode,
53
+ "includeTerms": include_terms or [],
54
+ "excludeTerms": exclude_terms or [],
55
+ "regex": regex,
56
+ "semanticPrompt": semantic_prompt,
57
+ "severity": severity,
58
+ # A "failure" pattern (default) raises a signal to triage; a "proper" pattern logs
59
+ # a healthy tally instead.
60
+ "polarity": polarity,
61
+ "enabled": enabled,
62
+ "sampleRate": sample_rate,
63
+ "scopeMode": scope_mode,
64
+ "agentIds": agent_ids or [],
65
+ }
66
+
67
+ def publish(self) -> MonitorPattern:
68
+ logger.info("Publishing monitor pattern '%s'", self._payload["name"])
69
+ return self._client.create_pattern(self._payload)
70
+
71
+
72
+ class MonitorPatternClient:
73
+ """Thin wrapper surfaced as ``client.monitor.patterns``."""
74
+
75
+ def __init__(self, client: "MonitorClient"):
76
+ self._client = client
77
+
78
+ def builder(
79
+ self,
80
+ name: str,
81
+ description: Optional[str] = None,
82
+ category: Optional[str] = None,
83
+ detector_kind: str = "contains",
84
+ match_target: Optional[List[str]] = None,
85
+ match_mode: str = "any",
86
+ include_terms: Optional[List[str]] = None,
87
+ exclude_terms: Optional[List[str]] = None,
88
+ regex: Optional[str] = None,
89
+ semantic_prompt: Optional[str] = None,
90
+ severity: str = "medium",
91
+ polarity: str = "failure",
92
+ enabled: bool = True,
93
+ sample_rate: float = 1.0,
94
+ scope_mode: str = "all",
95
+ agent_ids: Optional[List[str]] = None,
96
+ ) -> MonitorPatternBuilder:
97
+ return MonitorPatternBuilder(
98
+ self._client,
99
+ name=name,
100
+ description=description,
101
+ category=category,
102
+ detector_kind=detector_kind,
103
+ match_target=match_target,
104
+ match_mode=match_mode,
105
+ include_terms=include_terms,
106
+ exclude_terms=exclude_terms,
107
+ regex=regex,
108
+ semantic_prompt=semantic_prompt,
109
+ severity=severity,
110
+ polarity=polarity,
111
+ enabled=enabled,
112
+ sample_rate=sample_rate,
113
+ scope_mode=scope_mode,
114
+ agent_ids=agent_ids,
115
+ )
116
+
117
+ def get(self, pattern_id: str) -> MonitorPattern:
118
+ return self._client.get_pattern(pattern_id)
119
+
120
+ def list(self) -> List[MonitorPattern]:
121
+ return self._client.list_patterns()
@@ -78,6 +78,8 @@ class _TraceSpan:
78
78
  model: Optional[str] = None,
79
79
  session_id: Optional[str] = None,
80
80
  sync: bool = False,
81
+ monitor: bool = False,
82
+ pattern_ids: Optional[List[str]] = None,
81
83
  ) -> None:
82
84
  self._tracer = tracer
83
85
  self.name = name
@@ -91,6 +93,11 @@ class _TraceSpan:
91
93
  # is populated by the time the `with` block exits — see Tracer.trace()'s sync param.
92
94
  self._sync = sync
93
95
  self._trace_id: Optional[str] = None
96
+ # Monitor: check this trace against patterns immediately on ingest, no dashboard profile
97
+ # required. pattern_ids (if given) fully defines what's checked — only those patterns run,
98
+ # the built-in checks are skipped. See Tracer.trace()'s monitor/pattern_ids params.
99
+ self._monitor = monitor
100
+ self._pattern_ids = pattern_ids
94
101
 
95
102
  # Fields the caller can set while inside the context manager
96
103
  self.output: Any = None
@@ -171,6 +178,8 @@ class _TraceSpan:
171
178
 
172
179
  self._trace_id = self._tracer._send(
173
180
  sync=self._sync,
181
+ monitor=self._monitor or None,
182
+ pattern_ids=self._pattern_ids,
174
183
  name=self.name,
175
184
  input=_safe_serialize(self.input) if self.input is not None else None,
176
185
  output=_safe_serialize(self.output) if self.output is not None else None,
@@ -615,6 +624,8 @@ class Tracer:
615
624
  model: Optional[str] = None,
616
625
  session_id: Optional[str] = None,
617
626
  sync: bool = False,
627
+ monitor: bool = False,
628
+ pattern_ids: Optional[List[str]] = None,
618
629
  ) -> _TraceSpan:
619
630
  """
620
631
  Return a :class:`_TraceSpan` that works as both a decorator and a
@@ -629,6 +640,19 @@ class Tracer:
629
640
  resp = call_llm(...)
630
641
  span.output = resp
631
642
  return {"output": resp, "trace_id": span.trace_id}
643
+
644
+ Pass ``monitor=True`` to check this trace against Monitor patterns immediately, with no
645
+ dashboard profile required. ``pattern_ids`` (ids from ``client.monitor.patterns.builder(
646
+ ...).publish()``) restricts detection to exactly those patterns; omit it to run the full
647
+ default sweep (built-in checks plus every enabled workspace pattern)::
648
+
649
+ pattern = client.monitor.patterns.builder(
650
+ name="Promises a refund", detector_kind="semantic",
651
+ semantic_prompt="The response promises a refund.",
652
+ ).publish()
653
+
654
+ with client.tracer.trace("support_agent_call", monitor=True, pattern_ids=[pattern.id]) as span:
655
+ span.output = call_llm(...)
632
656
  """
633
657
  return _TraceSpan(
634
658
  tracer=self,
@@ -639,6 +663,8 @@ class Tracer:
639
663
  model=model,
640
664
  session_id=session_id,
641
665
  sync=sync,
666
+ monitor=monitor,
667
+ pattern_ids=pattern_ids,
642
668
  )
643
669
 
644
670
  def flush(self, timeout: float = 5.0) -> None:
@@ -862,6 +888,10 @@ class Tracer:
862
888
  wire["input_tokens"] = payload["input_tokens"]
863
889
  if "output_tokens" in payload:
864
890
  wire["output_tokens"] = payload["output_tokens"]
891
+ if "monitor" in payload:
892
+ wire["monitor"] = payload["monitor"]
893
+ if "pattern_ids" in payload:
894
+ wire["pattern_ids"] = payload["pattern_ids"]
865
895
 
866
896
  pending_retrievals, self._pending_retrievals = self._pending_retrievals, []
867
897
  pending_tool_calls, self._pending_tool_calls = self._pending_tool_calls, []
@@ -0,0 +1 @@
1
+ VERSION = "0.6.7"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.5
3
+ Version: 0.6.7
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
@@ -70,6 +70,7 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
70
70
  - [Chat (streaming and non-streaming)](#chat-streaming-and-non-streaming)
71
71
  - [Workforce (multi-agent orchestration)](#workforce-multi-agent-orchestration) — teams of agents with a designated manager
72
72
  - [Production tracing](#production-tracing) — record live agent runs from any framework
73
+ - [Monitor](#monitor) — automatic production monitoring, patterns and signals
73
74
  - [Custom agent evaluations](#custom-agent-evaluations) — LLM-as-a-judge, cosine / Jaccard similarity
74
75
  - [Links](#links)
75
76
 
@@ -245,6 +246,30 @@ See **[TRACING.md](TRACING.md)** for the complete guide — session grouping, er
245
246
 
246
247
  ---
247
248
 
249
+ ## Monitor
250
+
251
+ Automatic production monitoring: check traces against detection patterns and get back triage-ready signals. A **pattern** is a first-class SDK resource with a real id, just like a `Dataset` or `EvaluationSettings`: create one once, then reference it by id at trace time.
252
+
253
+ ```python
254
+ pattern = client.monitor.patterns.builder(
255
+ name="Promises a refund",
256
+ detector_kind="semantic",
257
+ semantic_prompt="The response promises a refund.",
258
+ severity="high",
259
+ ).publish()
260
+
261
+ with client.tracer.trace("support-agent", monitor=True, pattern_ids=[pattern.id]) as span:
262
+ span.output = call_llm(query)
263
+ ```
264
+
265
+ `monitor=True` checks the trace immediately, no dashboard setup required. `pattern_ids` restricts detection to exactly those patterns; omit it to run the full default sweep instead (built-in checks like empty response, trace error, and latency regression, plus every pattern enabled for the workspace).
266
+
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
+
269
+ See **[TRACING.md](TRACING.md)** for the complete Monitor guide.
270
+
271
+ ---
272
+
248
273
  ## Custom agent evaluations
249
274
 
250
275
  Evaluate **any** AI agent — LangChain, CrewAI, AutoGen, LlamaIndex, OpenAI, Anthropic, HTTP endpoints, or plain Python — using AgentX as the scoring and reporting backend. Includes optional **cosine** and **Jaccard** similarity metrics alongside LLM-graded ratings.
@@ -261,8 +286,13 @@ report = (
261
286
  print(report.average_rating) # LLM-graded score, 0–10
262
287
  print(report.cosine_similarity) # embedding cosine, 0–1 (None if not enabled)
263
288
  print(report.jaccard_similarity) # token-set overlap, 0–1 (None if not enabled)
289
+
290
+ print(report.summary) # AI-generated narrative from .analyze()
291
+ print(report.recommendations) # list of prioritized, actionable fixes
264
292
  ```
265
293
 
294
+ `.analyze()` also generates a full qualitative report (strengths, weaknesses, instruction adherence, reasoning quality, and recommendations), running the same durable, multi-judge pipeline as the dashboard's "Analyze" button. `analyze(mode=..., quality_mode=..., judges=[...])` controls how items are scored and by which models. See [AI analysis report](EVALUATIONS.md#ai-analysis-report) in the full guide for the complete field and parameter reference.
295
+
266
296
  See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder, framework adapters, similarity metrics, and the complete API reference.
267
297
 
268
298
  ---
@@ -274,3 +304,4 @@ See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder,
274
304
  - **PyPI** — [agentx-python](https://pypi.org/project/agentx-python/)
275
305
  - **Tracing docs** — [TRACING.md](TRACING.md)
276
306
  - **Evaluations docs** — [EVALUATIONS.md](EVALUATIONS.md)
307
+ - **Monitor docs** — [docs.agentx.so/sdk/monitor](https://docs.agentx.so/sdk/monitor)
@@ -30,6 +30,10 @@ agentx/integrations/google_adk.py
30
30
  agentx/integrations/google_genai.py
31
31
  agentx/integrations/langchain.py
32
32
  agentx/integrations/openai_agents.py
33
+ agentx/monitor/__init__.py
34
+ agentx/monitor/client.py
35
+ agentx/monitor/models.py
36
+ agentx/monitor/patterns.py
33
37
  agentx/resources/__init__.py
34
38
  agentx/resources/agent.py
35
39
  agentx/resources/conversation.py
@@ -1 +0,0 @@
1
- VERSION = "0.6.5"
File without changes
File without changes
File without changes