agentx-python 0.8.22__tar.gz → 0.8.23__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 (98) hide show
  1. {agentx_python-0.8.22 → agentx_python-0.8.23}/PKG-INFO +8 -1
  2. {agentx_python-0.8.22 → agentx_python-0.8.23}/README.md +7 -0
  3. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/client.py +21 -4
  4. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/evaluation_settings.py +14 -1
  5. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/models.py +4 -0
  6. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/reporting.py +1 -1
  7. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/results.py +10 -0
  8. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/runner.py +72 -5
  9. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/_traced_call.py +5 -1
  10. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/autogen.py +4 -1
  11. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/crewai.py +4 -1
  12. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/langchain.py +4 -0
  13. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/llamaindex.py +6 -1
  14. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/judge_scorers.py +13 -4
  15. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/models.py +7 -5
  16. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/tracing/ingest_client.py +17 -4
  17. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/tracing/tracer.py +2 -2
  18. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/version.py +2 -2
  19. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx_python.egg-info/PKG-INFO +8 -1
  20. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_docs_match_sdk.py +15 -0
  21. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_judge_scorers.py +33 -0
  22. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_selfhost_analysis_fallback.py +21 -0
  23. {agentx_python-0.8.22 → agentx_python-0.8.23}/LICENSE +0 -0
  24. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/__init__.py +0 -0
  25. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/agentx.py +0 -0
  26. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/cli.py +0 -0
  27. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/__init__.py +0 -0
  28. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/_term.py +0 -0
  29. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/adapters/__init__.py +0 -0
  30. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  31. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/adapters/precomputed.py +0 -0
  32. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/adapters/raw.py +0 -0
  33. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/datasets.py +0 -0
  34. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/prompts.py +0 -0
  35. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/tool_schemas.py +0 -0
  36. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/evaluations/tracing.py +0 -0
  37. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/exceptions.py +0 -0
  38. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/export.py +0 -0
  39. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/feedback.py +0 -0
  40. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/__init__.py +0 -0
  41. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/anthropic.py +0 -0
  42. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/databricks.py +0 -0
  43. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/google_adk.py +0 -0
  44. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/google_genai.py +0 -0
  45. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/litellm.py +0 -0
  46. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/moveworks.py +0 -0
  47. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/openai.py +0 -0
  48. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/integrations/openai_agents.py +0 -0
  49. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/__init__.py +0 -0
  50. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/agents.py +0 -0
  51. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/client.py +0 -0
  52. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/improvement_groups.py +0 -0
  53. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/online_evaluators.py +0 -0
  54. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/patterns.py +0 -0
  55. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/profile.py +0 -0
  56. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/review_queue.py +0 -0
  57. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/rules.py +0 -0
  58. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/scorer_groups.py +0 -0
  59. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/scorers.py +0 -0
  60. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/sessions.py +0 -0
  61. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/monitor/signals.py +0 -0
  62. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/outcomes.py +0 -0
  63. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/projects.py +0 -0
  64. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/py.typed +0 -0
  65. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/resources/__init__.py +0 -0
  66. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/resources/agent.py +0 -0
  67. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/resources/conversation.py +0 -0
  68. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/resources/workforce.py +0 -0
  69. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/testing.py +0 -0
  70. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/traces.py +0 -0
  71. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/tracing/__init__.py +0 -0
  72. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/tracing/ci_types.py +0 -0
  73. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/tracing/eval_scope.py +0 -0
  74. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/tracing/framework_detect.py +0 -0
  75. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx/util.py +0 -0
  76. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx_python.egg-info/SOURCES.txt +0 -0
  77. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx_python.egg-info/dependency_links.txt +0 -0
  78. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx_python.egg-info/entry_points.txt +0 -0
  79. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx_python.egg-info/not-zip-safe +0 -0
  80. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx_python.egg-info/requires.txt +0 -0
  81. {agentx_python-0.8.22 → agentx_python-0.8.23}/agentx_python.egg-info/top_level.txt +0 -0
  82. {agentx_python-0.8.22 → agentx_python-0.8.23}/setup.cfg +0 -0
  83. {agentx_python-0.8.22 → agentx_python-0.8.23}/setup.py +0 -0
  84. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_cli_launcher.py +0 -0
  85. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_deep_dive_fixes.py +0 -0
  86. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_error_taxonomy.py +0 -0
  87. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_eval_scope.py +0 -0
  88. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_framework_detect.py +0 -0
  89. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_integration.py +0 -0
  90. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_integrations.py +0 -0
  91. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_multi_judge.py +0 -0
  92. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_pairwise.py +0 -0
  93. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_review_queue.py +0 -0
  94. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_runner_features.py +0 -0
  95. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_selfhost_compat.py +0 -0
  96. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_span_tree.py +0 -0
  97. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_testing.py +0 -0
  98. {agentx_python-0.8.22 → agentx_python-0.8.23}/tests/test_wire_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.22
3
+ Version: 0.8.23
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
@@ -255,6 +255,13 @@ extra:
255
255
  | LlamaIndex | `pip install "agentx-python[llamaindex]"` | `AgentXLlamaIndexHandler` |
256
256
  | AutoGen | `pip install "agentx-python[autogen]"` | `AgentXAutoGenObserver` |
257
257
 
258
+ > **Warning: pick ONE instrumentation layer per LLM call.** Do not combine
259
+ > `AgentXCallbackHandler` (or any framework integration) with a patched provider client
260
+ > (`patch_openai_client`, `patch_anthropic_client`, `patch_genai_client`) on the same code
261
+ > path. A patched call that runs outside an active span emits its own root trace, so every
262
+ > LLM call the framework already traces gets a duplicate trace - and its cost is counted
263
+ > twice.
264
+
258
265
  Two more platforms are covered by **pull importers** rather than in-process hooks, each with its
259
266
  own CLI: `agentx-moveworks` (Moveworks Data API sync, no extra needed) and `agentx-databricks`
260
267
  (`pip install "agentx-python[databricks]"`, MLflow/Databricks trace sync).
@@ -190,6 +190,13 @@ extra:
190
190
  | LlamaIndex | `pip install "agentx-python[llamaindex]"` | `AgentXLlamaIndexHandler` |
191
191
  | AutoGen | `pip install "agentx-python[autogen]"` | `AgentXAutoGenObserver` |
192
192
 
193
+ > **Warning: pick ONE instrumentation layer per LLM call.** Do not combine
194
+ > `AgentXCallbackHandler` (or any framework integration) with a patched provider client
195
+ > (`patch_openai_client`, `patch_anthropic_client`, `patch_genai_client`) on the same code
196
+ > path. A patched call that runs outside an active span emits its own root trace, so every
197
+ > LLM call the framework already traces gets a duplicate trace - and its cost is counted
198
+ > twice.
199
+
193
200
  Two more platforms are covered by **pull importers** rather than in-process hooks, each with its
194
201
  own CLI: `agentx-moveworks` (Moveworks Data API sync, no extra needed) and `agentx-databricks`
195
202
  (`pip install "agentx-python[databricks]"`, MLflow/Databricks trace sync).
@@ -221,7 +221,16 @@ class EvaluationsClient:
221
221
  the Sovereignty & Portability Index. Pass ``provider`` (e.g. "Google")
222
222
  to filter."""
223
223
  params = {"provider": provider} if provider else None
224
- data = self._request("GET", "/models", params=params)
224
+ try:
225
+ data = self._request("GET", "/models", params=params)
226
+ except AgentXEvaluationsError as exc:
227
+ if exc.status_code == 404:
228
+ raise AgentXEvaluationsError(
229
+ "list_models is hosted-only; on self-host pass any model id your judge "
230
+ "key can reach, or use client.monitor.* portability models",
231
+ status_code=404,
232
+ ) from exc
233
+ raise
225
234
  items = data if isinstance(data, list) else data.get("models", [])
226
235
  return [ModelInfo(**m) for m in items]
227
236
 
@@ -546,12 +555,20 @@ class EvaluationsClient:
546
555
  ) -> bool:
547
556
  """Return True if ``exc`` is the 404 that means "this engine is self-host".
548
557
 
549
- Only a 404 qualifies. Anything else - auth, validation, a 500, a dead connection -
550
- is a real failure on a route that does exist, and must propagate rather than be
551
- retried against a different endpoint that would mask it.
558
+ Only a route-level 404 qualifies. Anything else - auth, validation, a 500, a dead
559
+ connection - is a real failure on a route that does exist, and must propagate rather
560
+ than be retried against a different endpoint that would mask it.
561
+
562
+ A resource 404 does not qualify either: the engine's SDK router answers these routes
563
+ with bodies naming the missing resource ("Run not found" / "No analysis found for
564
+ this run"), so latching on one would permanently reroute every later analysis call
565
+ to the dashboard router because a caller once passed a wrong run id.
552
566
  """
553
567
  if exc.status_code != 404:
554
568
  return False
569
+ body = str(exc)
570
+ if "Run not found" in body or "No analysis found for this run" in body:
571
+ return False
555
572
  if self._analysis_on_dashboard_router is None:
556
573
  logger.info(
557
574
  "%s is not served from %s; using the dashboard router at %s "
@@ -74,8 +74,21 @@ class EvaluationSettingsBuilder:
74
74
  # Sandboxed JS scorers run per result alongside the judge - each entry is
75
75
  # {"name": ..., "enabled": True, "code": "..."} where the code is a JS function body
76
76
  # receiving (input, output, expected, toolCalls) and returning {score, reasoning}.
77
+ # Normalized the same way DatasetBuilder does: id defaulted, name optional (the
78
+ # engine defaults it), enabled default True - raw pass-through sent entries the
79
+ # engine's shape validation rejects.
77
80
  if code_scorers:
78
- self._payload["codeScorers"] = list(code_scorers)
81
+ import uuid as _uuid
82
+
83
+ self._payload["codeScorers"] = [
84
+ {
85
+ "id": scorer.get("id") or _uuid.uuid4().hex[:12],
86
+ "name": scorer.get("name"),
87
+ "code": scorer["code"],
88
+ "enabled": scorer.get("enabled", True),
89
+ }
90
+ for scorer in code_scorers
91
+ ]
79
92
 
80
93
  def publish(self) -> EvaluationSettings:
81
94
  logger.info("Publishing evaluation settings '%s'", self._payload["name"])
@@ -51,6 +51,10 @@ class TestCase(BaseModel):
51
51
  expected_knowledge_base: Optional[List[str]] = Field(default=None, alias="expectedKnowledgeBase")
52
52
  expected_delegations: Optional[List[str]] = Field(default=None, alias="expectedDelegations")
53
53
  judge_guideline: Optional[str] = Field(default=None, alias="judgeGuideline")
54
+ # Engine-side trajectory match (e.g. {"tools": ["search"], "mode": "in_order"}) and the
55
+ # expected retrieval context for RAG grading - carried so import_dataset round-trips them.
56
+ expected_trajectory: Optional[Dict[str, Any]] = Field(default=None, alias="expectedTrajectory")
57
+ expected_retrieval_context: Optional[Any] = Field(default=None, alias="expectedRetrievalContext")
54
58
  smoke_test: Optional[SmokeTestSettings] = Field(default=None, alias="smokeTest")
55
59
  # Named subsets this case belongs to (e.g. ["smoke"], ["full", "regression"]).
56
60
  # ``run(dataset_id, split="smoke")`` runs only cases tagged with that split.
@@ -170,7 +170,7 @@ def print_report(report: Report) -> None:
170
170
 
171
171
  # --- Low-scoring cases ---
172
172
  if report.low_scoring_cases:
173
- _section("Low-scoring Cases (rating < 5)")
173
+ _section("Low-scoring Cases (rating <= 5)")
174
174
  for case in report.low_scoring_cases[:5]:
175
175
  q = (case.get("query") or case.get("questionText", ""))[:80]
176
176
  rating = case.get("rating", "?")
@@ -87,6 +87,16 @@ def normalize_result(
87
87
  else:
88
88
  output = {"text": str(raw)} if raw is not None else {"text": ""}
89
89
 
90
+ if error is None and (
91
+ output is None
92
+ or (set(output) <= {"text"} and not str(output.get("text") or "").strip())
93
+ ):
94
+ # An empty output with no error would fail the engine's row validation and silently
95
+ # vanish from the run - store it as an explicit failed row instead.
96
+ error = ResultError(type="EmptyOutput", message="Agent returned no output")
97
+ if output is None:
98
+ output = {"text": ""}
99
+
90
100
  has_timings = (
91
101
  latency_ms is not None or input_tokens is not None or output_tokens is not None
92
102
  )
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import os
5
+ import sys
5
6
  import time
6
7
 
7
8
  import requests
@@ -307,9 +308,23 @@ class EvaluationRunContext:
307
308
  # generator happens to be garbage-collected.
308
309
  if results_iter is not None:
309
310
  results_iter.close()
310
-
311
- if batch:
312
- self._flush_batch(batch)
311
+ # Flush the trailing partial batch HERE, not after the try: a mid-run exception
312
+ # (agent crash, Ctrl-C) used to discard up to max_batch - 1 already-paid-for
313
+ # results still waiting in it.
314
+ if batch:
315
+ propagating = sys.exc_info()[1]
316
+ try:
317
+ self._flush_batch(batch)
318
+ except Exception as flush_exc:
319
+ if propagating is None:
320
+ raise
321
+ # An exception is already propagating out of the loop - a flush failure
322
+ # here must not mask it.
323
+ logger.error(
324
+ "Trailing batch flush failed while handling %r: %s",
325
+ propagating,
326
+ flush_exc,
327
+ )
313
328
 
314
329
  return self
315
330
 
@@ -326,6 +341,15 @@ class EvaluationRunContext:
326
341
  _say(
327
342
  f" {green('✓')} Scored {resp.accepted} result{'s' if resp.accepted != 1 else ''}"
328
343
  )
344
+ if resp.failed_validation > 0:
345
+ # The engine accepts the batch but silently drops rows that fail its
346
+ # validation (typically empty output and no error) - say so, or those
347
+ # cases just vanish from the report.
348
+ _say(
349
+ f" {yellow('!')} {resp.failed_validation} result"
350
+ f"{'s' if resp.failed_validation != 1 else ''} failed validation "
351
+ "(empty output and no error) and did not get stored"
352
+ )
329
353
  logger.info(
330
354
  "Batch %s: accepted=%d duplicates=%d failed=%d",
331
355
  batch_id[:8],
@@ -415,8 +439,11 @@ class EvaluationRunContext:
415
439
  ``no_regression=True`` fails it when the average dropped more than ``tolerance``
416
440
  (default 0.5, judge scores are noisy) below the dataset's previous completed run.
417
441
  At least one check is required. On a multi-judge run, ``scorer`` (an additional
418
- scorer's id or name, e.g. ``scorer="Safety"``) gates that scorer's own average
419
- instead of the primary's - "fail if Safety is low even when the average looks fine". Prints a CI-log-friendly verdict and returns a
442
+ judge scorer's id or name, e.g. ``scorer="Safety"``) gates that scorer's own average
443
+ instead of the primary's - "fail if Safety is low even when the average looks fine".
444
+ Only judge scorers resolve here: deterministic scorer-group members (pattern/code
445
+ kinds) have no per-run judge average, so naming one is rejected by the engine.
446
+ Prints a CI-log-friendly verdict and returns a
420
447
  :class:`GateResult` - the caller decides the exit code::
421
448
 
422
449
  report = client.evaluations.run(...).execute(my_agent).finalize()
@@ -469,6 +496,16 @@ class EvaluationRunContext:
469
496
  """Number of submitted results that have received a rating so far."""
470
497
  return self._live_stats.rated_count if self._live_stats else 0
471
498
 
499
+ @property
500
+ def skipped_count(self) -> int:
501
+ """Number of submitted results the judge could not score."""
502
+ return self._live_stats.skipped_count if self._live_stats else 0
503
+
504
+ @property
505
+ def failed_count(self) -> int:
506
+ """Number of submitted results that carried an error."""
507
+ return self._live_stats.failed_count if self._live_stats else 0
508
+
472
509
  @property
473
510
  def average_rating(self) -> Optional[float]:
474
511
  """Live average rating across all results scored so far. Populated as
@@ -639,6 +676,36 @@ class EvaluationsRunner:
639
676
  script execution)."""
640
677
  return self._client.get_analysis_status(run_id)
641
678
 
679
+ # Run-lifecycle calls by id - the standalone forms of what run()/execute()/finalize()/
680
+ # analyze() drive for you, for scripts operating on a run created elsewhere.
681
+
682
+ def init_run(self, dataset_id: str, subject, **kwargs):
683
+ """Create a run row without executing anything - the standalone form of :meth:`run`.
684
+ Accepts the same kwargs as ``EvaluationsClient.init_run``."""
685
+ return self._client.init_run(dataset_id, subject, **kwargs)
686
+
687
+ def append_results(self, run_id: str, batch_id: str, results: list):
688
+ """Submit one batch of results to a run by id (scored synchronously server-side)."""
689
+ return self._client.append_results(run_id, batch_id, results)
690
+
691
+ def finalize_run(self, run_id: str) -> dict:
692
+ """Mark a run completed by id - the standalone form of
693
+ ``EvaluationRunContext.finalize()``."""
694
+ return self._client.finalize_run(run_id)
695
+
696
+ def analyze_run(self, run_id: str, **kwargs) -> dict:
697
+ """Start the LLM analysis of a finalized run by id; poll
698
+ :meth:`get_analysis_status`, then :meth:`get_report`."""
699
+ return self._client.analyze_run(run_id, **kwargs)
700
+
701
+ def get_report(self, run_id: str):
702
+ """The analyzed report for a run by id, once analysis has finished."""
703
+ return self._client.get_report(run_id)
704
+
705
+ def get_submitted_keys(self, run_id: str) -> list:
706
+ """Idempotency keys a run has already accepted - what execute() uses to resume."""
707
+ return self._client.get_submitted_keys(run_id)
708
+
642
709
  def gate_run(
643
710
  self,
644
711
  run_id: str,
@@ -153,7 +153,11 @@ def finish_llm_call(
153
153
  )
154
154
  return
155
155
 
156
- span = tracer.trace(name, metadata=metadata, framework=framework, model=model, session_id=session_id)
156
+ # A patched provider call outside any active span becomes its own root trace - it is a bare
157
+ # model call, so stamp it "llm" rather than leaving the kind unset.
158
+ span = tracer.trace(
159
+ name, metadata=metadata, framework=framework, model=model, session_id=session_id, span_kind="llm"
160
+ )
157
161
  span.__enter__()
158
162
  span._start = start_t
159
163
  span.input = input_repr
@@ -107,7 +107,10 @@ class AgentXAutoGenObserver:
107
107
  # explicit return/break/continue there would silently swallow any exception
108
108
  # propagating from agent_or_team.run() above (see crewai.py's kickoff() for the same
109
109
  # hazard spelled out in full).
110
- with self._tracer.trace(self._name, metadata=self._metadata, session_id=self._session_id) as span:
110
+ # span_kind="agent": the root of a standalone team/agent run is the agent run itself.
111
+ with self._tracer.trace(
112
+ self._name, metadata=self._metadata, session_id=self._session_id, span_kind="agent"
113
+ ) as span:
111
114
  span._start = start_t
112
115
  if error:
113
116
  span.set_error(error)
@@ -93,7 +93,10 @@ class AgentXCrewObserver:
93
93
  # `return` here (this whole method body runs inside the try's `finally`) - an
94
94
  # explicit return/break/continue in a finally block silently swallows any exception
95
95
  # propagating from crew.kickoff() above.
96
- with self._tracer.trace(self._name, metadata=self._metadata, session_id=self._session_id) as span:
96
+ # span_kind="agent": the root of a standalone crew kickoff is the agent run itself.
97
+ with self._tracer.trace(
98
+ self._name, metadata=self._metadata, session_id=self._session_id, span_kind="agent"
99
+ ) as span:
97
100
  span._start = start
98
101
  if error:
99
102
  span.set_error(error)
@@ -595,6 +595,9 @@ class AgentXCallbackHandler(BaseCallbackHandler):
595
595
  else self._metadata
596
596
  ),
597
597
  session_id=self._session_id,
598
+ # The root of a standalone chain/agent invocation is the agent run itself,
599
+ # not one of its llm/tool/retrieval children.
600
+ span_kind="agent",
598
601
  ) as span:
599
602
  # __enter__ just set _start to "now" - overridden to the chain's real start time,
600
603
  # see llamaindex.py's _send_trace for the identical fix and full rationale.
@@ -655,6 +658,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
655
658
  else self._metadata
656
659
  ),
657
660
  session_id=self._session_id,
661
+ span_kind="agent",
658
662
  ) as span:
659
663
  span._start = state["start"]
660
664
  span.set_error(str(error))
@@ -359,7 +359,12 @@ class AgentXLlamaIndexHandler(BaseCallbackHandler):
359
359
  # tool_calls loop reads latency_ms for duration and the timestamps for position, so each
360
360
  # tool call lands correctly in the tree panel instead of defaulting to offset 0.
361
361
  with self._tracer.trace(
362
- self._name, metadata=self._metadata, session_id=self._session_id, framework="llamaindex"
362
+ self._name,
363
+ metadata=self._metadata,
364
+ session_id=self._session_id,
365
+ framework="llamaindex",
366
+ # The root of a standalone query/agent invocation is the agent run itself.
367
+ span_kind="agent",
363
368
  ) as span:
364
369
  # __enter__ just set _start to "now" - overridden to the query's real start time so
365
370
  # __exit__'s latency_ms reflects the actual run, not the few microseconds between this
@@ -116,6 +116,7 @@ class JudgeScorersClient:
116
116
  judge_prompt: Optional[str] = None,
117
117
  judge_model: Optional[str] = None,
118
118
  tool_context: Optional[str] = None,
119
+ requires_expected: Optional[bool] = None,
119
120
  # Offline profile (dataset-run grading)
120
121
  number_of_requests: int = 1,
121
122
  vector_similarity: bool = False,
@@ -135,13 +136,17 @@ class JudgeScorersClient:
135
136
  alert_threshold: Optional[float] = 5,
136
137
  severity: str = "medium",
137
138
  agent_ids: Optional[List[str]] = None,
138
- idle_seconds: int = 120,
139
+ idle_seconds: Optional[int] = None,
139
140
  ) -> "JudgeScorerBuilder":
140
141
  """Snake_case builder with ``.publish()``, the unified successor of
141
142
  ``client.evaluations.settings.builder(...)`` - same offline fields (plus ``thresholds``,
142
143
  ``tool_context``) and, new here, the online profile in the same call. The scorer the
143
144
  builder publishes is one entity: its ``.id`` is what ``client.evaluations.run(...,
144
145
  scorer_id=...)`` takes, and its live profile is what online scoring keys on."""
146
+ if idle_seconds is not None and scope != "session":
147
+ # idleSeconds only applies to session scope - with trace scope the engine ignores
148
+ # it, so an explicit value here would be silently inert.
149
+ raise ValueError("idle_seconds requires scope='session'")
145
150
  judge: Dict[str, Any] = {}
146
151
  for key, value in (
147
152
  ("acceptanceCriteria", acceptance_criteria),
@@ -150,6 +155,7 @@ class JudgeScorersClient:
150
155
  ("judgePrompt", judge_prompt),
151
156
  ("judgeModel", judge_model),
152
157
  ("toolContext", tool_context),
158
+ ("requiresExpected", requires_expected),
153
159
  ):
154
160
  if value is not None:
155
161
  judge[key] = value
@@ -178,7 +184,7 @@ class JudgeScorersClient:
178
184
  "scope": scope,
179
185
  "alertThreshold": alert_threshold,
180
186
  "severity": severity,
181
- "idleSeconds": idle_seconds,
187
+ "idleSeconds": idle_seconds if idle_seconds is not None else 120,
182
188
  }
183
189
  if agent_ids:
184
190
  online["scopeMode"] = "selected"
@@ -329,7 +335,9 @@ class JudgeScorersClient:
329
335
 
330
336
  def ratings(self, scorer_id: str, window: str = "7d") -> "List[OnlineEvaluatorRatingPoint]":
331
337
  """Bucketed average-rating-over-time for this scorer's live checks - same typed points
332
- the legacy online_evaluators client returns, so scripts migrate without shape changes."""
338
+ the legacy online_evaluators client returns, so scripts migrate without shape changes.
339
+ ``window`` accepts "24h", "7d", or "30d" only (unlike :meth:`calibration`, which also
340
+ takes "rubric")."""
333
341
  from agentx.monitor.models import OnlineEvaluatorRatingPoint
334
342
 
335
343
  data = self._request("GET", f"/online-evaluators/{self._profile_id(scorer_id)}/ratings?window={window}")
@@ -337,7 +345,8 @@ class JudgeScorersClient:
337
345
 
338
346
  def events(self, scorer_id: str, window: str = "7d") -> "List[OnlineEvaluatorEvent]":
339
347
  """Individually scored traces behind the ratings series, worst-rated first - typed, same
340
- as the legacy online_evaluators client."""
348
+ as the legacy online_evaluators client. ``window`` accepts "24h", "7d", or "30d" only
349
+ (unlike :meth:`calibration`, which also takes "rubric")."""
341
350
  from agentx.monitor.models import OnlineEvaluatorEvent
342
351
 
343
352
  data = self._request("GET", f"/online-evaluators/{self._profile_id(scorer_id)}/events?window={window}")
@@ -10,13 +10,14 @@ class MonitorPattern(BaseModel):
10
10
  ``pattern_ids`` entry in ``tracer.trace(..., monitor=True, pattern_ids=[...])``.
11
11
 
12
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``.
13
+ healthy tally instead.
15
14
 
16
15
  On self-host the engine stores a pattern as a list of ``conditions`` (each with its own
17
- detector kind and match settings) - the flat ``include_terms``/``regex``/
18
- ``semantic_prompt`` fields are display-only projections derived from the first condition;
19
- ``conditions`` is the truth.
16
+ detector kind and match settings) and ``conditions`` is the only truth: the wire always
17
+ carries ``includeTerms``/``excludeTerms`` as ``[]`` and omits ``regex``/``semanticPrompt``
18
+ entirely (they are legacy display fields kept for wire compatibility), so the flat
19
+ ``include_terms``/``exclude_terms``/``regex``/``semantic_prompt`` attributes here stay
20
+ empty/None - read the match settings from ``conditions``.
20
21
  """
21
22
 
22
23
  id: str = Field(alias="_id")
@@ -41,6 +42,7 @@ class MonitorPattern(BaseModel):
41
42
  agent_ids: List[str] = Field(default_factory=list, alias="agentIds")
42
43
 
43
44
  class Config:
45
+ populate_by_name = True
44
46
  extra = "ignore"
45
47
 
46
48
 
@@ -179,7 +179,9 @@ class IngestClient:
179
179
  resp = self._session.post(self._endpoint, json=payload, timeout=10)
180
180
  except requests.RequestException as exc:
181
181
  self._warn_delivery(f"{exc.__class__.__name__}: {exc}")
182
- logger.debug("agentx ingest sync send error: %s", exc)
182
+ # WARNING, not debug: the sync caller explicitly asked for a trace_id back, so
183
+ # a dropped trace here silently becomes trace_id None downstream.
184
+ logger.warning("agentx sync trace send failed (%s) - trace dropped, no trace_id", exc)
183
185
  return None
184
186
  if resp.status_code in (429, 503) and attempt < 2:
185
187
  retry_after = resp.headers.get("Retry-After")
@@ -193,7 +195,13 @@ class IngestClient:
193
195
  continue
194
196
  if not resp.ok:
195
197
  self._warn_delivery(f"HTTP {resp.status_code}", status=resp.status_code)
196
- logger.debug("agentx ingest sync HTTP %d: %s", resp.status_code, resp.text[:200])
198
+ # WARNING, not debug: the sync caller explicitly asked for a trace_id back, so
199
+ # a dropped trace here silently becomes trace_id None downstream.
200
+ logger.warning(
201
+ "agentx sync trace send failed (HTTP %d: %s) - trace dropped, no trace_id",
202
+ resp.status_code,
203
+ resp.text[:200],
204
+ )
197
205
  return None
198
206
  try:
199
207
  return resp.json().get("trace_id")
@@ -211,11 +219,16 @@ class IngestClient:
211
219
  resp = self._session.post(self._endpoint, json=payload, timeout=10)
212
220
  except requests.RequestException as exc:
213
221
  self._warn_delivery(f"{exc.__class__.__name__}: {exc}")
214
- logger.debug("agentx ingest sync send error: %s", exc)
222
+ # WARNING, not debug - same reasoning as send_trace_sync.
223
+ logger.warning("agentx sync trace send failed (%s) - trace dropped, no trace_id", exc)
215
224
  return None
216
225
  if not resp.ok:
217
226
  self._warn_delivery(f"HTTP {resp.status_code}", status=resp.status_code)
218
- logger.debug("agentx ingest sync HTTP %d: %s", resp.status_code, resp.text[:200])
227
+ logger.warning(
228
+ "agentx sync trace send failed (HTTP %d: %s) - trace dropped, no trace_id",
229
+ resp.status_code,
230
+ resp.text[:200],
231
+ )
219
232
  return None
220
233
  try:
221
234
  body = resp.json()
@@ -1283,8 +1283,8 @@ class Tracer:
1283
1283
  (available as ``span.trace_id`` once that `with` block exits).
1284
1284
  dataset_id: EvaluationSettings ID to score against.
1285
1285
  question_index: Optional index into the dataset's questions array.
1286
- When supplied, that question's ``expectedResults``
1287
- is included in the scoring prompt.
1286
+ Hosted-only: the self-host engine ignores it and
1287
+ scores the trace's own input/output as-is.
1288
1288
 
1289
1289
  Returns:
1290
1290
  Dict with keys: ``run_id``, ``trace_id``, ``rating``,
@@ -1,7 +1,7 @@
1
- VERSION = "0.8.22"
1
+ VERSION = "0.8.23"
2
2
 
3
3
  # The AgentX-trace-eval release this SDK version is tested against - what `agentx-trace-eval`
4
4
  # installs and converges to (see agentx/cli.py). Bump together with VERSION when releasing, so
5
5
  # every published SDK names a known-good engine+dashboard pair. Users can override with
6
6
  # AGENTX_TRACE_EVAL_VERSION=<tag|latest>.
7
- ENGINE_VERSION = "v0.3.23"
7
+ ENGINE_VERSION = "v0.3.24"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.22
3
+ Version: 0.8.23
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
@@ -255,6 +255,13 @@ extra:
255
255
  | LlamaIndex | `pip install "agentx-python[llamaindex]"` | `AgentXLlamaIndexHandler` |
256
256
  | AutoGen | `pip install "agentx-python[autogen]"` | `AgentXAutoGenObserver` |
257
257
 
258
+ > **Warning: pick ONE instrumentation layer per LLM call.** Do not combine
259
+ > `AgentXCallbackHandler` (or any framework integration) with a patched provider client
260
+ > (`patch_openai_client`, `patch_anthropic_client`, `patch_genai_client`) on the same code
261
+ > path. A patched call that runs outside an active span emits its own root trace, so every
262
+ > LLM call the framework already traces gets a duplicate trace - and its cost is counted
263
+ > twice.
264
+
258
265
  Two more platforms are covered by **pull importers** rather than in-process hooks, each with its
259
266
  own CLI: `agentx-moveworks` (Moveworks Data API sync, no extra needed) and `agentx-databricks`
260
267
  (`pip install "agentx-python[databricks]"`, MLflow/Databricks trace sync).
@@ -95,6 +95,21 @@ def test_documented_judge_scorer_methods_exist():
95
95
  assert not missing, "documented but not on JudgeScorersClient: " + ", ".join(missing)
96
96
 
97
97
 
98
+ def test_run_lifecycle_calls_documented_on_client_evaluations_exist():
99
+ """The mintlify API reference shows client.evaluations.<method>(...) for the whole run
100
+ lifecycle - these delegate to the private EvaluationsClient, and every one must exist on
101
+ the public runner or the documented snippets raise AttributeError."""
102
+ for method in (
103
+ "init_run",
104
+ "append_results",
105
+ "finalize_run",
106
+ "analyze_run",
107
+ "get_report",
108
+ "get_submitted_keys",
109
+ ):
110
+ assert hasattr(EvaluationsRunner, method), f"client.evaluations.{method} is documented but missing"
111
+
112
+
98
113
  def test_documented_builder_keywords_are_real_parameters():
99
114
  keywords = [
100
115
  (doc, keyword)
@@ -214,6 +214,39 @@ def test_judge_scorers_builder_matches_legacy_builder_ergonomics(monkeypatch):
214
214
  assert payload["online"]["agentIds"] == ["support-agent"]
215
215
 
216
216
 
217
+ def test_builder_rejects_idle_seconds_without_session_scope():
218
+ """idleSeconds only applies to session scope - the engine silently ignores it with trace
219
+ scope, so an explicit idle_seconds without scope="session" is a hard error, not an inert
220
+ wire field."""
221
+ client = JudgeScorersClient(api_key="agtx_local_test", base_url="http://localhost:1")
222
+ with pytest.raises(ValueError, match="idle_seconds requires scope='session'"):
223
+ client.builder("Support quality", live=True, idle_seconds=300)
224
+
225
+
226
+ def test_builder_sends_idle_seconds_and_requires_expected(monkeypatch):
227
+ """With scope="session", an explicit idle_seconds reaches the wire, and requires_expected
228
+ lands in the judge section as requiresExpected."""
229
+ client = JudgeScorersClient(api_key="agtx_local_test", base_url="http://localhost:1")
230
+ captured = {}
231
+
232
+ def fake_request(method, path, **kwargs):
233
+ captured["payload"] = kwargs.get("json")
234
+ return {"judgeScorer": {"_id": "s1", "name": "Support quality", "judge": {}, "offline": {}, "online": None}}
235
+
236
+ monkeypatch.setattr(client, "_request", fake_request)
237
+ client.builder(
238
+ "Support quality",
239
+ requires_expected=True,
240
+ live=True,
241
+ scope="session",
242
+ idle_seconds=300,
243
+ ).publish()
244
+ payload = captured["payload"]
245
+ assert payload["judge"]["requiresExpected"] is True
246
+ assert payload["online"]["scope"] == "session"
247
+ assert payload["online"]["idleSeconds"] == 300
248
+
249
+
217
250
  def test_from_env_honors_selfhost_base_url_conventions(monkeypatch):
218
251
  """from_env silently targeting the hosted default while the shell exports the self-host
219
252
  conventions (AGENTX_SELFHOST_BASE_URL / BASE_URL) produced confusing auth errors - it now
@@ -271,6 +271,27 @@ def test_failures_that_are_not_404_propagate_untouched(status):
271
271
  assert client._analysis_on_dashboard_router is None
272
272
 
273
273
 
274
+ @pytest.mark.parametrize("body", [{"error": "Run not found"}, {"error": "No analysis found for this run. POST /runs/:runId/analyze first."}])
275
+ def test_resource_404s_do_not_latch_the_dashboard_fallback(body):
276
+ """A 404 whose body names the missing resource comes from a route that EXISTS - the SDK
277
+ router answered it. It must propagate as-is and must not permanently reroute every later
278
+ analysis call to the dashboard router."""
279
+ client, session = make_client(
280
+ {
281
+ ("GET", f"{SDK_ROOT}/runs/{RUN}/analyze-status"): FakeResponse(404, body),
282
+ # Present, and must not be reached.
283
+ ("GET", f"{API_ROOT}/evaluate/analyze/{RUN}/status"): FakeResponse(200, STATUS_BODY),
284
+ }
285
+ )
286
+
287
+ with pytest.raises(AgentXEvaluationsError) as caught:
288
+ client.get_analysis_status(RUN)
289
+
290
+ assert caught.value.status_code == 404
291
+ assert not [u for u in session.urls() if "/evaluate/" in u], "masked a resource 404"
292
+ assert client._analysis_on_dashboard_router is None
293
+
294
+
274
295
  def test_auth_errors_are_not_mistaken_for_a_missing_route():
275
296
  client, session = make_client(
276
297
  {("GET", f"{SDK_ROOT}/runs/{RUN}/analyze-status"): FakeResponse(401, {"e": "nope"})}
File without changes
File without changes
File without changes