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.
- {agentx_python-0.8.23 → agentx_python-0.8.25}/PKG-INFO +4 -2
- {agentx_python-0.8.23 → agentx_python-0.8.25}/README.md +3 -1
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/agentx.py +45 -18
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/client.py +19 -9
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/datasets.py +19 -5
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/evaluation_settings.py +12 -2
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/models.py +10 -15
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/reporting.py +1 -1
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/runner.py +27 -9
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/crewai.py +21 -11
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/google_adk.py +110 -15
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/langchain.py +57 -28
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/litellm.py +17 -2
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/llamaindex.py +97 -88
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/openai_agents.py +3 -0
- agentx_python-0.8.25/agentx/monitor/__init__.py +48 -0
- agentx_python-0.8.25/agentx/monitor/_transport.py +48 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/agents.py +13 -2
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/client.py +30 -12
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/improvement_groups.py +55 -8
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/judge_scorers.py +88 -10
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/models.py +4 -0
- agentx_python-0.8.25/agentx/monitor/patterns.py +250 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/profile.py +2 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/review_queue.py +10 -2
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/rules.py +21 -4
- agentx_python-0.8.25/agentx/monitor/scorer_groups.py +146 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/scorers.py +55 -10
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/resources/agent.py +28 -9
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/resources/conversation.py +31 -10
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/resources/workforce.py +34 -9
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/ingest_client.py +22 -5
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/tracer.py +119 -32
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/util.py +9 -1
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/version.py +2 -2
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/PKG-INFO +4 -2
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/SOURCES.txt +4 -1
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_judge_scorers.py +16 -3
- agentx_python-0.8.25/tests/test_pattern_update_merge.py +200 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_span_tree.py +103 -1
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_wire_models.py +43 -0
- agentx_python-0.8.25/tests/test_workforce_binding.py +71 -0
- agentx_python-0.8.23/agentx/monitor/__init__.py +0 -17
- agentx_python-0.8.23/agentx/monitor/patterns.py +0 -125
- agentx_python-0.8.23/agentx/monitor/scorer_groups.py +0 -93
- {agentx_python-0.8.23 → agentx_python-0.8.25}/LICENSE +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/__init__.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/cli.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/results.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/exceptions.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/export.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/feedback.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/autogen.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/sessions.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/outcomes.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/projects.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/py.typed +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/testing.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/traces.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/eval_scope.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx/tracing/framework_detect.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/setup.cfg +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/setup.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_cli_launcher.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_deep_dive_fixes.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_docs_match_sdk.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_error_taxonomy.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_eval_scope.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_framework_detect.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_integration.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_integrations.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_multi_judge.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_pairwise.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_review_queue.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_runner_features.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_selfhost_analysis_fallback.py +0 -0
- {agentx_python-0.8.23 → agentx_python-0.8.25}/tests/test_selfhost_compat.py +0 -0
- {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.
|
|
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).
|
|
23
|
-
#
|
|
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
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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 -
|
|
186
|
-
#
|
|
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
|
-
|
|
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
|
|
510
|
-
|
|
511
|
-
``submittedKeys`` - to find out
|
|
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
|
-
|
|
521
|
-
|
|
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
|
|
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:
|
|
97
|
-
# the
|
|
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
|
-
|
|
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:
|
|
68
|
-
#
|
|
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[
|
|
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"
|
|
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-
|
|
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 =
|
|
570
|
-
while
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
)
|