agentx-python 0.8.23__tar.gz → 0.8.25__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 (104) hide show
  1. {agentx_python-0.8.23 → agentx_python-0.8.25}/PKG-INFO +4 -2
  2. {agentx_python-0.8.23 → agentx_python-0.8.25}/README.md +3 -1
  3. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/agentx.py +45 -18
  4. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/client.py +19 -9
  5. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/datasets.py +19 -5
  6. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/evaluation_settings.py +12 -2
  7. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/models.py +10 -15
  8. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/reporting.py +1 -1
  9. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/runner.py +27 -9
  10. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/crewai.py +21 -11
  11. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/google_adk.py +110 -15
  12. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/langchain.py +57 -28
  13. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/litellm.py +17 -2
  14. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/llamaindex.py +97 -88
  15. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/openai_agents.py +3 -0
  16. agentx_python-0.8.25/agentx/monitor/__init__.py +48 -0
  17. agentx_python-0.8.25/agentx/monitor/_transport.py +48 -0
  18. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/agents.py +13 -2
  19. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/client.py +30 -12
  20. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/improvement_groups.py +55 -8
  21. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/judge_scorers.py +88 -10
  22. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/models.py +4 -0
  23. agentx_python-0.8.25/agentx/monitor/patterns.py +250 -0
  24. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/profile.py +2 -0
  25. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/review_queue.py +10 -2
  26. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/rules.py +21 -4
  27. agentx_python-0.8.25/agentx/monitor/scorer_groups.py +146 -0
  28. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/scorers.py +55 -10
  29. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/resources/agent.py +28 -9
  30. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/resources/conversation.py +31 -10
  31. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/resources/workforce.py +34 -9
  32. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/ingest_client.py +22 -5
  33. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/tracer.py +119 -32
  34. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/util.py +9 -1
  35. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/version.py +2 -2
  36. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/PKG-INFO +4 -2
  37. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/SOURCES.txt +4 -1
  38. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_judge_scorers.py +16 -3
  39. agentx_python-0.8.25/tests/test_pattern_update_merge.py +200 -0
  40. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_span_tree.py +103 -1
  41. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_wire_models.py +43 -0
  42. agentx_python-0.8.25/tests/test_workforce_binding.py +71 -0
  43. agentx_python-0.8.23/agentx/monitor/__init__.py +0 -17
  44. agentx_python-0.8.23/agentx/monitor/patterns.py +0 -125
  45. agentx_python-0.8.23/agentx/monitor/scorer_groups.py +0 -93
  46. {agentx_python-0.8.23 → agentx_python-0.8.25}/LICENSE +0 -0
  47. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/__init__.py +0 -0
  48. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/cli.py +0 -0
  49. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/__init__.py +0 -0
  50. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/_term.py +0 -0
  51. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/adapters/__init__.py +0 -0
  52. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  53. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/adapters/precomputed.py +0 -0
  54. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/adapters/raw.py +0 -0
  55. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/prompts.py +0 -0
  56. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/results.py +0 -0
  57. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/tool_schemas.py +0 -0
  58. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/tracing.py +0 -0
  59. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/exceptions.py +0 -0
  60. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/export.py +0 -0
  61. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/feedback.py +0 -0
  62. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/__init__.py +0 -0
  63. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/_traced_call.py +0 -0
  64. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/anthropic.py +0 -0
  65. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/autogen.py +0 -0
  66. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/databricks.py +0 -0
  67. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/google_genai.py +0 -0
  68. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/moveworks.py +0 -0
  69. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/openai.py +0 -0
  70. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/online_evaluators.py +0 -0
  71. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/sessions.py +0 -0
  72. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/signals.py +0 -0
  73. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/outcomes.py +0 -0
  74. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/projects.py +0 -0
  75. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/py.typed +0 -0
  76. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/resources/__init__.py +0 -0
  77. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/testing.py +0 -0
  78. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/traces.py +0 -0
  79. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/__init__.py +0 -0
  80. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/ci_types.py +0 -0
  81. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/eval_scope.py +0 -0
  82. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/framework_detect.py +0 -0
  83. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/dependency_links.txt +0 -0
  84. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/entry_points.txt +0 -0
  85. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/not-zip-safe +0 -0
  86. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/requires.txt +0 -0
  87. {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/top_level.txt +0 -0
  88. {agentx_python-0.8.23 → agentx_python-0.8.25}/setup.cfg +0 -0
  89. {agentx_python-0.8.23 → agentx_python-0.8.25}/setup.py +0 -0
  90. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_cli_launcher.py +0 -0
  91. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_deep_dive_fixes.py +0 -0
  92. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_docs_match_sdk.py +0 -0
  93. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_error_taxonomy.py +0 -0
  94. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_eval_scope.py +0 -0
  95. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_framework_detect.py +0 -0
  96. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_integration.py +0 -0
  97. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_integrations.py +0 -0
  98. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_multi_judge.py +0 -0
  99. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_pairwise.py +0 -0
  100. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_review_queue.py +0 -0
  101. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_runner_features.py +0 -0
  102. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_selfhost_analysis_fallback.py +0 -0
  103. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_selfhost_compat.py +0 -0
  104. {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.23
3
+ Version: 0.8.25
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
@@ -108,7 +108,9 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
108
108
  pip install --upgrade agentx-python
109
109
  ```
110
110
 
111
- Requires Python 3.9 or newer.
111
+ Requires Python 3.9 or newer for the core SDK. Some integration extras have higher floors set
112
+ by their upstream packages - `[crewai]`, `[autogen]`, and `[databricks]` need Python 3.10+ (`[all]`
113
+ therefore does too); the core tracer and every REST surface stay 3.9-compatible.
112
114
 
113
115
  #### Run the self-host governance suite locally
114
116
 
@@ -43,7 +43,9 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
43
43
  pip install --upgrade agentx-python
44
44
  ```
45
45
 
46
- Requires Python 3.9 or newer.
46
+ Requires Python 3.9 or newer for the core SDK. Some integration extras have higher floors set
47
+ by their upstream packages - `[crewai]`, `[autogen]`, and `[databricks]` need Python 3.10+ (`[all]`
48
+ therefore does too); the core tracer and every REST surface stay 3.9-compatible.
47
49
 
48
50
  #### Run the self-host governance suite locally
49
51
 
@@ -4,6 +4,7 @@ import os
4
4
  import logging
5
5
 
6
6
  from agentx.util import get_headers, api_base, normalize_base
7
+ from agentx.exceptions import AgentXError
7
8
  from agentx.resources.agent import Agent
8
9
  from agentx.resources.workforce import Workforce
9
10
 
@@ -19,8 +20,8 @@ class AgentX:
19
20
  # The api_key is NOT written back into os.environ (it used to be): every sub-client
20
21
  # below receives it explicitly, and mutating process-global state from a constructor
21
22
  # re-pointed unrelated code - the same leak the base_url write below had (deep-dive
22
- # round 3, bug #1). Static flows that still read the env (AgentX.list_workforces,
23
- # bare get_headers()) now require the caller to set AGENTX_API_KEY themselves.
23
+ # round 3, bug #1). Flows that still read the env (bare get_headers()) now require
24
+ # the caller to set AGENTX_API_KEY themselves.
24
25
  self.api_key = api_key or os.getenv("AGENTX_API_KEY")
25
26
 
26
27
  # base_url overrides AGENTX_API_BASE_URL env var (and the SDK default). It is
@@ -136,34 +137,54 @@ class AgentX:
136
137
  return cls(base_url=base_url) if base_url else cls()
137
138
 
138
139
  def get_agent(self, id: str) -> Agent:
140
+ """Fetch one hosted-platform agent by id.
141
+
142
+ Hosted platform only - the self-host engine does not serve /access/agents;
143
+ use ``client.monitor.agents.list()`` for self-host agent rows instead.
144
+ """
139
145
  url = f"{self.base_url or api_base()}/access/agents/{id}"
140
146
  # Make a GET request to the AgentX API
141
147
  response = requests.get(url, headers=get_headers(self.api_key))
142
148
  # Check if response was successful
143
149
  if response.status_code == 200:
144
- return Agent(**response.json())
150
+ return Agent(**response.json())._bind(self.api_key, self.base_url)
145
151
  else:
146
- raise Exception(f"Failed to retrieve agent: {response.reason}")
152
+ raise AgentXError(
153
+ f"Failed to retrieve agent: {response.reason}. This endpoint is "
154
+ "hosted-platform only - on self-host use client.monitor.agents.list()."
155
+ )
147
156
 
148
157
  def list_agents(self) -> List[Agent]:
158
+ """List the hosted platform's agents.
159
+
160
+ Hosted platform only - the self-host engine does not serve /access/agents;
161
+ use ``client.monitor.agents.list()`` for self-host agent rows instead.
162
+ """
149
163
  url = f"{self.base_url or api_base()}/access/agents"
150
164
  # Make a GET request to the AgentX API
151
165
  response = requests.get(url, headers=get_headers(self.api_key))
152
166
  # Check if response was successful
153
167
  if response.status_code == 200:
154
- return [Agent(**agent) for agent in response.json()]
168
+ return [Agent(**agent)._bind(self.api_key, self.base_url) for agent in response.json()]
155
169
  else:
156
- raise Exception(f"Failed to list agents: {response.reason}")
157
-
158
- @staticmethod
159
- def list_workforces() -> List["Workforce"]:
160
- """List all workforces/teams. Static, so it reads AGENTX_API_KEY from the environment
161
- directly - the constructor no longer writes ``api_key`` into os.environ, so set the
162
- env var yourself before calling this."""
163
- url = f"{api_base()}/access/teams"
164
- response = requests.get(url, headers=get_headers())
170
+ raise AgentXError(
171
+ f"Failed to list agents: {response.reason}. This endpoint is "
172
+ "hosted-platform only - on self-host use client.monitor.agents.list()."
173
+ )
174
+
175
+ def list_workforces(self) -> List["Workforce"]:
176
+ """List all workforces/teams, each bound to this client's credentials - including each
177
+ workforce's ``manager`` and ``agents``, so their calls authenticate the same way.
178
+
179
+ This used to be documented as a static call (``AgentX.list_workforces()``); that form
180
+ was broken (the old staticmethod body referenced ``self`` and raised NameError on any
181
+ non-empty response). Construct a client instead - ``AgentX().list_workforces()`` picks
182
+ up AGENTX_API_KEY / AGENTX_API_BASE_URL from the environment, which is what the static
183
+ form effectively did."""
184
+ url = f"{self.base_url or api_base()}/access/teams"
185
+ response = requests.get(url, headers=get_headers(self.api_key))
165
186
  if response.status_code == 200:
166
- return [Workforce(**workforce) for workforce in response.json()]
187
+ return [Workforce(**workforce)._bind(self.api_key, self.base_url) for workforce in response.json()]
167
188
  else:
168
189
  raise Exception(
169
190
  f"Failed to list workforces: {response.status_code} - {response.reason}"
@@ -214,12 +235,18 @@ class AgentX:
214
235
  return {"ok": True, "base_url": base}
215
236
 
216
237
  def get_profile(self):
217
- """Get the current user's profile information."""
238
+ """Get the current user's profile information.
239
+
240
+ Hosted platform only - the self-host engine does not serve /access/getProfile;
241
+ self-host agent/monitoring data lives under ``client.monitor`` (e.g.
242
+ ``client.monitor.agents.list()``).
243
+ """
218
244
  url = f"{self.base_url or api_base()}/access/getProfile"
219
245
  response = requests.get(url, headers=get_headers(self.api_key))
220
246
  if response.status_code == 200:
221
247
  return response.json()
222
248
  else:
223
- raise Exception(
224
- f"Failed to get profile: {response.status_code} - {response.reason}"
249
+ raise AgentXError(
250
+ f"Failed to get profile: {response.status_code} - {response.reason}. "
251
+ "This endpoint is hosted-platform only - on self-host use client.monitor."
225
252
  )
@@ -35,7 +35,6 @@ _DEFAULT_BASE_URL = f"{_UTIL_API_BASE}/custom-agent-evaluations"
35
35
  SDK_NAME = "agentx-python"
36
36
 
37
37
  _RETRYABLE_STATUS = {429, 500, 502, 503, 504}
38
- _MAX_RETRIES = 3
39
38
  _RETRY_BACKOFF = [1.0, 2.0, 4.0]
40
39
 
41
40
  # The self-host analyze route judges every result before it responds, so the client has to
@@ -182,8 +181,8 @@ class EvaluationsClient:
182
181
  if resp.status_code == 422:
183
182
  raise AgentXValidationError(resp.text)
184
183
  # Gate on the schedule itself so HTTP-status retries walk the SAME full backoff
185
- # schedule connection errors do - the old `attempt < _MAX_RETRIES - 1` gate left
186
- # the schedule's last entry unreachable for HTTP retries (ingest_client precedent).
184
+ # schedule connection errors do - an earlier fixed retry-count gate left the
185
+ # schedule's last entry unreachable for HTTP retries (ingest_client precedent).
187
186
  if (
188
187
  resp.status_code in _RETRYABLE_STATUS
189
188
  and retry
@@ -248,7 +247,9 @@ class EvaluationsClient:
248
247
  """Deletes the dataset, its grading config, and both version histories. Past runs are
249
248
  kept (their dataset reference degrades to a bare id). The engine refuses (409) when the
250
249
  dataset's config is attached to a live scorer."""
251
- self._request("DELETE", f"/datasets/{dataset_id}")
250
+ # retry=False: a lost response + transport retry would turn a successful
251
+ # delete into a spurious 404.
252
+ self._request("DELETE", f"/datasets/{dataset_id}", retry=False)
252
253
 
253
254
  def list_datasets(self) -> List[Dataset]:
254
255
  data = self._request("GET", "/datasets", params=self._workspace_params())
@@ -440,6 +441,12 @@ class EvaluationsClient:
440
441
  quality_mode: Optional[str] = None,
441
442
  judges: Optional[List[str]] = None,
442
443
  ) -> Dict[str, Any]:
444
+ """Start the qualitative AI-analysis job for a run.
445
+
446
+ ``mode`` ("auto"/"sync"/"batch") is hosted-only: self-host engines run the analysis
447
+ synchronously and ignore it - check the response's mode field for what actually ran
448
+ (mirrors EvaluationRun.analyze's docstring).
449
+ """
443
450
  # Starts the durable analysis job and returns immediately (e.g. {"jobId": ..., "status":
444
451
  # "pending"}); poll get_analysis_status() until it reaches a terminal status, then call
445
452
  # get_report(). mode/quality_mode/judges mirror the dashboard's AnalyzeEvaluationRequest.
@@ -506,9 +513,10 @@ class EvaluationsClient:
506
513
  return self._report_from_dashboard(run_id)
507
514
 
508
515
  def get_missing_results(self, run_id: str) -> List[Dict[str, Any]]:
509
- """Deprecated: on self-host the route's response body has no top-level list, so this
510
- always returns ``[]``. Use :meth:`get_submitted_keys` - the same route's
511
- ``submittedKeys`` - to find out what a run still needs."""
516
+ """Deprecated: on self-host the route returns ``missing: []`` deliberately empty -
517
+ the engine cannot know the client's case list - so this always returns ``[]``.
518
+ Use :meth:`get_submitted_keys` - the same route's ``submittedKeys`` - to find out
519
+ what a run still needs."""
512
520
  import warnings
513
521
 
514
522
  warnings.warn(
@@ -517,8 +525,10 @@ class EvaluationsClient:
517
525
  DeprecationWarning,
518
526
  stacklevel=2,
519
527
  )
520
- data = self._request("GET", f"/runs/{run_id}/missing-results")
521
- return data if isinstance(data, list) else data.get("missing", [])
528
+ # No request at all: the route returns `missing: []` deliberately empty (the engine
529
+ # cannot know the client's case list), so the round-trip only ever bought an empty
530
+ # result.
531
+ return []
522
532
 
523
533
  def get_submitted_keys(self, run_id: str) -> List[str]:
524
534
  """Idempotency keys this run has already accepted - what execute() uses to resume a
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import csv
4
4
  import logging
5
+ import warnings
5
6
  from pathlib import Path
6
7
  from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union
7
8
 
@@ -21,7 +22,8 @@ class DatasetBuilder:
21
22
  ``judge_prompt``/``judge_model`` are LLM-as-judge overrides for the dataset's grading
22
23
  config. NOTE (self-host): the engine's dataset-create route currently ignores both -
23
24
  set them on a judge scorer / the evaluation settings instead. ``sovereignty_models``
24
- is accepted on the wire but not acted on by the self-host engine.
25
+ is dropped by the self-host engine on this route - use
26
+ ``client.monitor.judge_scorers.builder(sovereignty_models=...)`` which persists it.
25
27
  """
26
28
 
27
29
  def __init__(
@@ -93,8 +95,9 @@ class DatasetBuilder:
93
95
  if rouge_score:
94
96
  self._payload["rougeScore"] = {"enabled": True}
95
97
  # Sovereignty & Portability - the models to compare on this dataset (use
96
- # client.evaluations.list_models() to discover valid ids). Self-host: accepted on
97
- # the wire but not acted on by the engine (see class docstring).
98
+ # client.evaluations.list_models() to discover valid ids). Self-host: dropped by
99
+ # the engine on this route - use client.monitor.judge_scorers.builder(
100
+ # sovereignty_models=...) which persists it (see class docstring).
98
101
  if sovereignty_models:
99
102
  self._payload["sovereigntyIndex"] = {
100
103
  "enabled": True,
@@ -161,9 +164,11 @@ class DatasetBuilder:
161
164
  main["smokeTest"] = {"enabled": True, "count": smoke_test_count}
162
165
  if smoke_test_guidance:
163
166
  main["smokeTest"]["guidance"] = smoke_test_guidance
164
- if expected_tools:
167
+ # `is not None`, not truthiness: an explicit empty list is a real assertion (an empty
168
+ # expectedTrajectory means "this case calls no tools") and must reach the wire.
169
+ if expected_tools is not None:
165
170
  main["expectedTrajectory"] = {"tools": expected_tools, "mode": trajectory_match_mode}
166
- if expected_retrieval_context:
171
+ if expected_retrieval_context is not None:
167
172
  main["expectedRetrievalContext"] = expected_retrieval_context
168
173
  if splits:
169
174
  main["splits"] = splits
@@ -178,6 +183,15 @@ class DatasetBuilder:
178
183
  def publish(self) -> Dataset:
179
184
  if not self._payload["questions"]:
180
185
  raise ValueError("Dataset must have at least one case before publishing")
186
+ # Warn at publish time, where the request is known: the engine's dataset-create
187
+ # route drops sovereigntyIndex, so comparison models set here never persist.
188
+ sov = self._payload.get("sovereigntyIndex")
189
+ if isinstance(sov, dict) and sov.get("models"):
190
+ warnings.warn(
191
+ "Self-host ignores sovereigntyIndex on datasets/grading configs - use "
192
+ "judge_scorers.builder(sovereignty_models=...) for model comparison runs.",
193
+ stacklevel=2,
194
+ )
181
195
  logger.info(
182
196
  "Publishing dataset '%s' with %d case(s)",
183
197
  self._payload["name"],
@@ -64,8 +64,9 @@ class EvaluationSettingsBuilder:
64
64
  if rouge_score:
65
65
  self._payload["rougeScore"] = {"enabled": True}
66
66
  # Sovereignty & Portability - the models to compare when this config runs
67
- # (use client.evaluations.list_models() to discover valid ids). Self-host: accepted
68
- # on the wire but not acted on by the engine (same caveat as DatasetBuilder's).
67
+ # (use client.evaluations.list_models() to discover valid ids). Self-host: dropped
68
+ # by the engine on this route - use client.monitor.judge_scorers.builder(
69
+ # sovereignty_models=...) which persists it (same caveat as DatasetBuilder's).
69
70
  if sovereignty_models:
70
71
  self._payload["sovereigntyIndex"] = {
71
72
  "enabled": True,
@@ -91,6 +92,15 @@ class EvaluationSettingsBuilder:
91
92
  ]
92
93
 
93
94
  def publish(self) -> EvaluationSettings:
95
+ # Warn at publish time, where the request is known: the engine's settings-create
96
+ # route drops sovereigntyIndex, so comparison models set here never persist.
97
+ sov = self._payload.get("sovereigntyIndex")
98
+ if isinstance(sov, dict) and sov.get("models"):
99
+ warnings.warn(
100
+ "Self-host ignores sovereigntyIndex on datasets/grading configs - use "
101
+ "judge_scorers.builder(sovereignty_models=...) for model comparison runs.",
102
+ stacklevel=2,
103
+ )
94
104
  logger.info("Publishing evaluation settings '%s'", self._payload["name"])
95
105
  return self._client.create_evaluation_settings(self._payload)
96
106
 
@@ -179,27 +179,22 @@ class Prompt(BaseModel):
179
179
  # Evaluation subject
180
180
  # ---------------------------------------------------------------------------
181
181
 
182
- FrameworkKind = Literal[
183
- "raw_python",
184
- "openai",
185
- "anthropic",
186
- "google",
187
- "langchain",
188
- "llamaindex",
189
- "crewai",
190
- "autogen",
191
- "n8n",
192
- "flowise",
193
- "other",
194
- ]
195
-
196
182
  RuntimeKind = Literal["local", "ci", "customer_hosted", "low_code"]
197
183
 
198
184
 
199
185
  class EvaluationSubject(BaseModel):
186
+ """Describes the agent under evaluation.
187
+
188
+ ``framework`` is an open string - the engine accepts any label (it also stamps
189
+ values like ``openai-agents``, ``langgraph``, ``google-genai``, ``litellm`` from
190
+ the tracing integrations). Common values: ``raw_python``, ``openai``,
191
+ ``anthropic``, ``google``, ``langchain``, ``llamaindex``, ``crewai``,
192
+ ``autogen``, ``n8n``, ``flowise``, ``other``.
193
+ """
194
+
200
195
  kind: Literal["custom_agent", "agentx_agent", "agentx_team"] = "custom_agent"
201
196
  display_name: Optional[str] = Field(default=None, alias="displayName")
202
- framework: Optional[FrameworkKind] = None
197
+ framework: Optional[str] = None
203
198
  framework_version: Optional[str] = Field(default=None, alias="frameworkVersion")
204
199
  runtime: Optional[RuntimeKind] = "local"
205
200
  agent_instructions: Optional[str] = Field(default=None, alias="agentInstructions")
@@ -172,7 +172,7 @@ def print_report(report: Report) -> None:
172
172
  if report.low_scoring_cases:
173
173
  _section("Low-scoring Cases (rating <= 5)")
174
174
  for case in report.low_scoring_cases[:5]:
175
- q = (case.get("query") or case.get("questionText", ""))[:80]
175
+ q = (case.get("query") or case.get("questionText") or "")[:80]
176
176
  rating = case.get("rating", "?")
177
177
  justification = case.get("justification", "")
178
178
  print(f" {red(f'[{rating}]')} {q}")
@@ -229,6 +229,7 @@ class EvaluationRunContext:
229
229
  )
230
230
  return normalized(case)
231
231
 
232
+ executor = None
232
233
  if concurrency > 1:
233
234
  import concurrent.futures
234
235
  import contextvars
@@ -308,6 +309,12 @@ class EvaluationRunContext:
308
309
  # generator happens to be garbage-collected.
309
310
  if results_iter is not None:
310
311
  results_iter.close()
312
+ # And release the pool itself here too: close() on a NEVER-STARTED generator
313
+ # (e.g. every case was already submitted, so next() was never called) does not
314
+ # run bounded()'s finally - its executor.shutdown would never fire. shutdown()
315
+ # is idempotent, so the double call on the normal path is harmless.
316
+ if executor is not None:
317
+ executor.shutdown(wait=False, cancel_futures=True)
311
318
  # Flush the trailing partial batch HERE, not after the try: a mid-run exception
312
319
  # (agent crash, Ctrl-C) used to discard up to max_batch - 1 already-paid-for
313
320
  # results still waiting in it.
@@ -546,7 +553,7 @@ class EvaluationRunContext:
546
553
  response's mode field for what actually ran.
547
554
  quality_mode: "quality_first" or "balanced" - how many items get a second/third judge.
548
555
  judges: 1-3 model ids from ``client.evaluations.list_models()``, e.g.
549
- ``["gpt-5.6-luna", "claude-opus-4-8"]``. Omit to let the engine score with its
556
+ ``["gpt-5.6-luna", "claude-opus-5"]``. Omit to let the engine score with its
550
557
  platform default model (a single judge, rather than the dashboard's 3-judge
551
558
  default - SDK runs are typically lighter-weight, quick-start evaluations).
552
559
  poll_interval: seconds between status checks while waiting.
@@ -566,16 +573,27 @@ class EvaluationRunContext:
566
573
  judges=judges,
567
574
  )
568
575
  deadline = time.monotonic() + timeout
569
- status = self._client.get_analysis_status(self._run.run_id)
570
- while not status.is_terminal and time.monotonic() < deadline:
571
- level = _ANALYSIS_LEVEL_LABELS.get(status.progress.current_level, "")
572
- spinner.update(
573
- f"Analyzing, {level + ' ' if level else ''}{status.progress.overall_percentage}%"
574
- )
576
+ status = None
577
+ while True:
578
+ try:
579
+ status = self._client.get_analysis_status(self._run.run_id)
580
+ except Exception as poll_exc:
581
+ # One transient status-poll failure (network blip, engine restart)
582
+ # must not abort the whole wait - the job keeps running
583
+ # server-side, so keep polling until the deadline.
584
+ logger.debug("Analysis status poll failed: %s", poll_exc)
585
+ if status is not None and status.is_terminal:
586
+ break
587
+ if time.monotonic() >= deadline:
588
+ break
589
+ if status is not None:
590
+ level = _ANALYSIS_LEVEL_LABELS.get(status.progress.current_level, "")
591
+ spinner.update(
592
+ f"Analyzing, {level + ' ' if level else ''}{status.progress.overall_percentage}%"
593
+ )
575
594
  time.sleep(poll_interval)
576
- status = self._client.get_analysis_status(self._run.run_id)
577
595
 
578
- if not status.is_terminal:
596
+ if status is None or not status.is_terminal:
579
597
  _say(f" {yellow('!')} Still running after {int(timeout)}s, check the dashboard for status")
580
598
  elif status.status == "failed":
581
599
  reason = status.failure_reason.message if status.failure_reason else "unknown error"
@@ -84,7 +84,7 @@ class AgentXCrewObserver:
84
84
  if task_timings:
85
85
  execution_steps, _ = self._build_steps_from_timings(task_timings, task_outputs)
86
86
  elif task_outputs:
87
- execution_steps, _ = self._build_steps_evenly_divided(task_outputs, latency_ms)
87
+ execution_steps, _ = self._build_steps_evenly_divided(task_outputs, latency_ms, start)
88
88
 
89
89
  # Each task becomes its own real child span. tool_calls isn't passed to
90
90
  # _merge_child_run here: _build_steps_from_timings/_build_steps_evenly_divided both
@@ -144,14 +144,14 @@ class AgentXCrewObserver:
144
144
  TaskFailedEvent,
145
145
  TaskStartedEvent,
146
146
  )
147
- except ImportError:
147
+ except Exception: # noqa: BLE001 - crewai import raises TypeError (PEP 604) on py3.9, not just ImportError
148
148
  from crewai.utilities.events import crewai_event_bus
149
149
  from crewai.utilities.events.task_events import (
150
150
  TaskCompletedEvent,
151
151
  TaskFailedEvent,
152
152
  TaskStartedEvent,
153
153
  )
154
- except ImportError:
154
+ except Exception: # noqa: BLE001 - crewai import raises TypeError (PEP 604) on py3.9, not just ImportError
155
155
  if not _warned_no_event_bus:
156
156
  _warned_no_event_bus = True
157
157
  logger.warning(
@@ -262,25 +262,33 @@ class AgentXCrewObserver:
262
262
 
263
263
  return execution_steps, tool_calls
264
264
 
265
- def _build_steps_evenly_divided(self, task_outputs: List[Any], latency_ms: float) -> tuple:
265
+ def _build_steps_evenly_divided(self, task_outputs: List[Any], latency_ms: float, start: float) -> tuple:
266
266
  """
267
267
  Fallback for CrewAI versions predating the events module: no
268
268
  per-task timing is available, so attribute the total latency evenly
269
269
  across tasks so the timeline still sums to the measured wall-clock
270
- duration.
270
+ duration. Each synthesized step also gets a real start_time/end_time
271
+ (consecutive per_step_s slices from the kickoff's start) so the
272
+ dashboard timeline can position it, not just size it.
271
273
  """
272
274
  tool_calls: List[Dict[str, Any]] = []
273
275
  execution_steps: List[Dict[str, Any]] = []
274
- for task_out in task_outputs:
276
+ per_step_ms = latency_ms / len(task_outputs)
277
+ per_step_s = per_step_ms / 1000.0
278
+ for i, task_out in enumerate(task_outputs):
275
279
  description = getattr(task_out, "description", "task")
276
280
  name = description[:100]
277
281
  task_output = str(getattr(task_out, "raw", ""))
278
282
  tool_calls.append({"name": name, "input": description, "output": task_output})
279
- execution_steps.append({"name": name, "duration_ms": 0, "input": description, "output": task_output})
280
-
281
- per_step_ms = latency_ms / len(execution_steps)
282
- for step in execution_steps:
283
- step["duration_ms"] = per_step_ms
283
+ step_start = start + i * per_step_s
284
+ execution_steps.append({
285
+ "name": name,
286
+ "duration_ms": per_step_ms,
287
+ "start_time": step_start,
288
+ "end_time": step_start + per_step_s,
289
+ "input": description,
290
+ "output": task_output,
291
+ })
284
292
 
285
293
  return execution_steps, tool_calls
286
294
 
@@ -307,4 +315,6 @@ class AgentXCrewObserver:
307
315
  framework="crewai",
308
316
  session_id=session_id or self._session_id,
309
317
  sync=sync,
318
+ # Same statement kickoff() makes: the root of a crew run is the agent run itself.
319
+ span_kind="agent",
310
320
  )