agentx-python 0.8.22__tar.gz → 0.8.24__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.22 → agentx_python-0.8.24}/PKG-INFO +8 -1
- {agentx_python-0.8.22 → agentx_python-0.8.24}/README.md +7 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/agentx.py +28 -5
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/client.py +40 -13
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/datasets.py +19 -5
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/evaluation_settings.py +26 -3
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/models.py +14 -15
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/reporting.py +2 -2
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/results.py +10 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/runner.py +99 -14
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/_traced_call.py +5 -1
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/autogen.py +4 -1
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/crewai.py +23 -10
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/google_adk.py +110 -15
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/langchain.py +61 -28
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/litellm.py +17 -2
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/llamaindex.py +103 -89
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/openai_agents.py +3 -0
- agentx_python-0.8.24/agentx/monitor/__init__.py +39 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/agents.py +13 -2
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/client.py +25 -9
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/improvement_groups.py +39 -4
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/judge_scorers.py +68 -8
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/models.py +11 -5
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/patterns.py +34 -1
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/profile.py +2 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/review_queue.py +10 -2
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/rules.py +6 -2
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/scorer_groups.py +33 -4
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/scorers.py +37 -4
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/tracing/ingest_client.py +39 -9
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/tracing/tracer.py +116 -31
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/version.py +2 -2
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx_python.egg-info/PKG-INFO +8 -1
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_docs_match_sdk.py +15 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_judge_scorers.py +48 -2
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_selfhost_analysis_fallback.py +21 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_span_tree.py +73 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_wire_models.py +43 -0
- agentx_python-0.8.22/agentx/monitor/__init__.py +0 -17
- {agentx_python-0.8.22 → agentx_python-0.8.24}/LICENSE +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/__init__.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/cli.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/exceptions.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/export.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/feedback.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/sessions.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/outcomes.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/projects.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/py.typed +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/resources/agent.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/testing.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/traces.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/tracing/eval_scope.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/tracing/framework_detect.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx/util.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx_python.egg-info/SOURCES.txt +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/setup.cfg +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/setup.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_cli_launcher.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_deep_dive_fixes.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_error_taxonomy.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_eval_scope.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_framework_detect.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_integration.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_integrations.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_multi_judge.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_pairwise.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_review_queue.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_runner_features.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/tests/test_selfhost_compat.py +0 -0
- {agentx_python-0.8.22 → agentx_python-0.8.24}/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.24
|
|
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).
|
|
@@ -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
|
|
|
@@ -136,6 +137,11 @@ 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))
|
|
@@ -143,9 +149,17 @@ class AgentX:
|
|
|
143
149
|
if response.status_code == 200:
|
|
144
150
|
return Agent(**response.json())
|
|
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))
|
|
@@ -153,7 +167,10 @@ class AgentX:
|
|
|
153
167
|
if response.status_code == 200:
|
|
154
168
|
return [Agent(**agent) for agent in response.json()]
|
|
155
169
|
else:
|
|
156
|
-
raise
|
|
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
|
+
)
|
|
157
174
|
|
|
158
175
|
@staticmethod
|
|
159
176
|
def list_workforces() -> List["Workforce"]:
|
|
@@ -214,12 +231,18 @@ class AgentX:
|
|
|
214
231
|
return {"ok": True, "base_url": base}
|
|
215
232
|
|
|
216
233
|
def get_profile(self):
|
|
217
|
-
"""Get the current user's profile information.
|
|
234
|
+
"""Get the current user's profile information.
|
|
235
|
+
|
|
236
|
+
Hosted platform only - the self-host engine does not serve /access/getProfile;
|
|
237
|
+
self-host agent/monitoring data lives under ``client.monitor`` (e.g.
|
|
238
|
+
``client.monitor.agents.list()``).
|
|
239
|
+
"""
|
|
218
240
|
url = f"{self.base_url or api_base()}/access/getProfile"
|
|
219
241
|
response = requests.get(url, headers=get_headers(self.api_key))
|
|
220
242
|
if response.status_code == 200:
|
|
221
243
|
return response.json()
|
|
222
244
|
else:
|
|
223
|
-
raise
|
|
224
|
-
f"Failed to get profile: {response.status_code} - {response.reason}"
|
|
245
|
+
raise AgentXError(
|
|
246
|
+
f"Failed to get profile: {response.status_code} - {response.reason}. "
|
|
247
|
+
"This endpoint is hosted-platform only - on self-host use client.monitor."
|
|
225
248
|
)
|
|
@@ -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
|
|
@@ -221,7 +220,16 @@ class EvaluationsClient:
|
|
|
221
220
|
the Sovereignty & Portability Index. Pass ``provider`` (e.g. "Google")
|
|
222
221
|
to filter."""
|
|
223
222
|
params = {"provider": provider} if provider else None
|
|
224
|
-
|
|
223
|
+
try:
|
|
224
|
+
data = self._request("GET", "/models", params=params)
|
|
225
|
+
except AgentXEvaluationsError as exc:
|
|
226
|
+
if exc.status_code == 404:
|
|
227
|
+
raise AgentXEvaluationsError(
|
|
228
|
+
"list_models is hosted-only; on self-host pass any model id your judge "
|
|
229
|
+
"key can reach, or use client.monitor.* portability models",
|
|
230
|
+
status_code=404,
|
|
231
|
+
) from exc
|
|
232
|
+
raise
|
|
225
233
|
items = data if isinstance(data, list) else data.get("models", [])
|
|
226
234
|
return [ModelInfo(**m) for m in items]
|
|
227
235
|
|
|
@@ -239,7 +247,9 @@ class EvaluationsClient:
|
|
|
239
247
|
"""Deletes the dataset, its grading config, and both version histories. Past runs are
|
|
240
248
|
kept (their dataset reference degrades to a bare id). The engine refuses (409) when the
|
|
241
249
|
dataset's config is attached to a live scorer."""
|
|
242
|
-
|
|
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)
|
|
243
253
|
|
|
244
254
|
def list_datasets(self) -> List[Dataset]:
|
|
245
255
|
data = self._request("GET", "/datasets", params=self._workspace_params())
|
|
@@ -431,6 +441,12 @@ class EvaluationsClient:
|
|
|
431
441
|
quality_mode: Optional[str] = None,
|
|
432
442
|
judges: Optional[List[str]] = None,
|
|
433
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
|
+
"""
|
|
434
450
|
# Starts the durable analysis job and returns immediately (e.g. {"jobId": ..., "status":
|
|
435
451
|
# "pending"}); poll get_analysis_status() until it reaches a terminal status, then call
|
|
436
452
|
# get_report(). mode/quality_mode/judges mirror the dashboard's AnalyzeEvaluationRequest.
|
|
@@ -497,9 +513,10 @@ class EvaluationsClient:
|
|
|
497
513
|
return self._report_from_dashboard(run_id)
|
|
498
514
|
|
|
499
515
|
def get_missing_results(self, run_id: str) -> List[Dict[str, Any]]:
|
|
500
|
-
"""Deprecated: on self-host the route
|
|
501
|
-
|
|
502
|
-
``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."""
|
|
503
520
|
import warnings
|
|
504
521
|
|
|
505
522
|
warnings.warn(
|
|
@@ -508,8 +525,10 @@ class EvaluationsClient:
|
|
|
508
525
|
DeprecationWarning,
|
|
509
526
|
stacklevel=2,
|
|
510
527
|
)
|
|
511
|
-
|
|
512
|
-
|
|
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 []
|
|
513
532
|
|
|
514
533
|
def get_submitted_keys(self, run_id: str) -> List[str]:
|
|
515
534
|
"""Idempotency keys this run has already accepted - what execute() uses to resume a
|
|
@@ -546,12 +565,20 @@ class EvaluationsClient:
|
|
|
546
565
|
) -> bool:
|
|
547
566
|
"""Return True if ``exc`` is the 404 that means "this engine is self-host".
|
|
548
567
|
|
|
549
|
-
Only a 404 qualifies. Anything else - auth, validation, a 500, a dead
|
|
550
|
-
is a real failure on a route that does exist, and must propagate rather
|
|
551
|
-
retried against a different endpoint that would mask it.
|
|
568
|
+
Only a route-level 404 qualifies. Anything else - auth, validation, a 500, a dead
|
|
569
|
+
connection - is a real failure on a route that does exist, and must propagate rather
|
|
570
|
+
than be retried against a different endpoint that would mask it.
|
|
571
|
+
|
|
572
|
+
A resource 404 does not qualify either: the engine's SDK router answers these routes
|
|
573
|
+
with bodies naming the missing resource ("Run not found" / "No analysis found for
|
|
574
|
+
this run"), so latching on one would permanently reroute every later analysis call
|
|
575
|
+
to the dashboard router because a caller once passed a wrong run id.
|
|
552
576
|
"""
|
|
553
577
|
if exc.status_code != 404:
|
|
554
578
|
return False
|
|
579
|
+
body = str(exc)
|
|
580
|
+
if "Run not found" in body or "No analysis found for this run" in body:
|
|
581
|
+
return False
|
|
555
582
|
if self._analysis_on_dashboard_router is None:
|
|
556
583
|
logger.info(
|
|
557
584
|
"%s is not served from %s; using the dashboard router at %s "
|
|
@@ -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,
|
|
@@ -74,10 +75,32 @@ class EvaluationSettingsBuilder:
|
|
|
74
75
|
# Sandboxed JS scorers run per result alongside the judge - each entry is
|
|
75
76
|
# {"name": ..., "enabled": True, "code": "..."} where the code is a JS function body
|
|
76
77
|
# receiving (input, output, expected, toolCalls) and returning {score, reasoning}.
|
|
78
|
+
# Normalized the same way DatasetBuilder does: id defaulted, name optional (the
|
|
79
|
+
# engine defaults it), enabled default True - raw pass-through sent entries the
|
|
80
|
+
# engine's shape validation rejects.
|
|
77
81
|
if code_scorers:
|
|
78
|
-
|
|
82
|
+
import uuid as _uuid
|
|
83
|
+
|
|
84
|
+
self._payload["codeScorers"] = [
|
|
85
|
+
{
|
|
86
|
+
"id": scorer.get("id") or _uuid.uuid4().hex[:12],
|
|
87
|
+
"name": scorer.get("name"),
|
|
88
|
+
"code": scorer["code"],
|
|
89
|
+
"enabled": scorer.get("enabled", True),
|
|
90
|
+
}
|
|
91
|
+
for scorer in code_scorers
|
|
92
|
+
]
|
|
79
93
|
|
|
80
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
|
+
)
|
|
81
104
|
logger.info("Publishing evaluation settings '%s'", self._payload["name"])
|
|
82
105
|
return self._client.create_evaluation_settings(self._payload)
|
|
83
106
|
|
|
@@ -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.
|
|
@@ -175,27 +179,22 @@ class Prompt(BaseModel):
|
|
|
175
179
|
# Evaluation subject
|
|
176
180
|
# ---------------------------------------------------------------------------
|
|
177
181
|
|
|
178
|
-
FrameworkKind = Literal[
|
|
179
|
-
"raw_python",
|
|
180
|
-
"openai",
|
|
181
|
-
"anthropic",
|
|
182
|
-
"google",
|
|
183
|
-
"langchain",
|
|
184
|
-
"llamaindex",
|
|
185
|
-
"crewai",
|
|
186
|
-
"autogen",
|
|
187
|
-
"n8n",
|
|
188
|
-
"flowise",
|
|
189
|
-
"other",
|
|
190
|
-
]
|
|
191
|
-
|
|
192
182
|
RuntimeKind = Literal["local", "ci", "customer_hosted", "low_code"]
|
|
193
183
|
|
|
194
184
|
|
|
195
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
|
+
|
|
196
195
|
kind: Literal["custom_agent", "agentx_agent", "agentx_team"] = "custom_agent"
|
|
197
196
|
display_name: Optional[str] = Field(default=None, alias="displayName")
|
|
198
|
-
framework: Optional[
|
|
197
|
+
framework: Optional[str] = None
|
|
199
198
|
framework_version: Optional[str] = Field(default=None, alias="frameworkVersion")
|
|
200
199
|
runtime: Optional[RuntimeKind] = "local"
|
|
201
200
|
agent_instructions: Optional[str] = Field(default=None, alias="agentInstructions")
|
|
@@ -170,9 +170,9 @@ 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
|
|
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}")
|
|
@@ -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
|
|
@@ -228,6 +229,7 @@ class EvaluationRunContext:
|
|
|
228
229
|
)
|
|
229
230
|
return normalized(case)
|
|
230
231
|
|
|
232
|
+
executor = None
|
|
231
233
|
if concurrency > 1:
|
|
232
234
|
import concurrent.futures
|
|
233
235
|
import contextvars
|
|
@@ -307,9 +309,29 @@ class EvaluationRunContext:
|
|
|
307
309
|
# generator happens to be garbage-collected.
|
|
308
310
|
if results_iter is not None:
|
|
309
311
|
results_iter.close()
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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)
|
|
318
|
+
# Flush the trailing partial batch HERE, not after the try: a mid-run exception
|
|
319
|
+
# (agent crash, Ctrl-C) used to discard up to max_batch - 1 already-paid-for
|
|
320
|
+
# results still waiting in it.
|
|
321
|
+
if batch:
|
|
322
|
+
propagating = sys.exc_info()[1]
|
|
323
|
+
try:
|
|
324
|
+
self._flush_batch(batch)
|
|
325
|
+
except Exception as flush_exc:
|
|
326
|
+
if propagating is None:
|
|
327
|
+
raise
|
|
328
|
+
# An exception is already propagating out of the loop - a flush failure
|
|
329
|
+
# here must not mask it.
|
|
330
|
+
logger.error(
|
|
331
|
+
"Trailing batch flush failed while handling %r: %s",
|
|
332
|
+
propagating,
|
|
333
|
+
flush_exc,
|
|
334
|
+
)
|
|
313
335
|
|
|
314
336
|
return self
|
|
315
337
|
|
|
@@ -326,6 +348,15 @@ class EvaluationRunContext:
|
|
|
326
348
|
_say(
|
|
327
349
|
f" {green('✓')} Scored {resp.accepted} result{'s' if resp.accepted != 1 else ''}"
|
|
328
350
|
)
|
|
351
|
+
if resp.failed_validation > 0:
|
|
352
|
+
# The engine accepts the batch but silently drops rows that fail its
|
|
353
|
+
# validation (typically empty output and no error) - say so, or those
|
|
354
|
+
# cases just vanish from the report.
|
|
355
|
+
_say(
|
|
356
|
+
f" {yellow('!')} {resp.failed_validation} result"
|
|
357
|
+
f"{'s' if resp.failed_validation != 1 else ''} failed validation "
|
|
358
|
+
"(empty output and no error) and did not get stored"
|
|
359
|
+
)
|
|
329
360
|
logger.info(
|
|
330
361
|
"Batch %s: accepted=%d duplicates=%d failed=%d",
|
|
331
362
|
batch_id[:8],
|
|
@@ -415,8 +446,11 @@ class EvaluationRunContext:
|
|
|
415
446
|
``no_regression=True`` fails it when the average dropped more than ``tolerance``
|
|
416
447
|
(default 0.5, judge scores are noisy) below the dataset's previous completed run.
|
|
417
448
|
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".
|
|
449
|
+
judge scorer's id or name, e.g. ``scorer="Safety"``) gates that scorer's own average
|
|
450
|
+
instead of the primary's - "fail if Safety is low even when the average looks fine".
|
|
451
|
+
Only judge scorers resolve here: deterministic scorer-group members (pattern/code
|
|
452
|
+
kinds) have no per-run judge average, so naming one is rejected by the engine.
|
|
453
|
+
Prints a CI-log-friendly verdict and returns a
|
|
420
454
|
:class:`GateResult` - the caller decides the exit code::
|
|
421
455
|
|
|
422
456
|
report = client.evaluations.run(...).execute(my_agent).finalize()
|
|
@@ -469,6 +503,16 @@ class EvaluationRunContext:
|
|
|
469
503
|
"""Number of submitted results that have received a rating so far."""
|
|
470
504
|
return self._live_stats.rated_count if self._live_stats else 0
|
|
471
505
|
|
|
506
|
+
@property
|
|
507
|
+
def skipped_count(self) -> int:
|
|
508
|
+
"""Number of submitted results the judge could not score."""
|
|
509
|
+
return self._live_stats.skipped_count if self._live_stats else 0
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def failed_count(self) -> int:
|
|
513
|
+
"""Number of submitted results that carried an error."""
|
|
514
|
+
return self._live_stats.failed_count if self._live_stats else 0
|
|
515
|
+
|
|
472
516
|
@property
|
|
473
517
|
def average_rating(self) -> Optional[float]:
|
|
474
518
|
"""Live average rating across all results scored so far. Populated as
|
|
@@ -509,7 +553,7 @@ class EvaluationRunContext:
|
|
|
509
553
|
response's mode field for what actually ran.
|
|
510
554
|
quality_mode: "quality_first" or "balanced" - how many items get a second/third judge.
|
|
511
555
|
judges: 1-3 model ids from ``client.evaluations.list_models()``, e.g.
|
|
512
|
-
``["gpt-5.6-luna", "claude-opus-
|
|
556
|
+
``["gpt-5.6-luna", "claude-opus-5"]``. Omit to let the engine score with its
|
|
513
557
|
platform default model (a single judge, rather than the dashboard's 3-judge
|
|
514
558
|
default - SDK runs are typically lighter-weight, quick-start evaluations).
|
|
515
559
|
poll_interval: seconds between status checks while waiting.
|
|
@@ -529,16 +573,27 @@ class EvaluationRunContext:
|
|
|
529
573
|
judges=judges,
|
|
530
574
|
)
|
|
531
575
|
deadline = time.monotonic() + timeout
|
|
532
|
-
status =
|
|
533
|
-
while
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
)
|
|
538
594
|
time.sleep(poll_interval)
|
|
539
|
-
status = self._client.get_analysis_status(self._run.run_id)
|
|
540
595
|
|
|
541
|
-
if not status.is_terminal:
|
|
596
|
+
if status is None or not status.is_terminal:
|
|
542
597
|
_say(f" {yellow('!')} Still running after {int(timeout)}s, check the dashboard for status")
|
|
543
598
|
elif status.status == "failed":
|
|
544
599
|
reason = status.failure_reason.message if status.failure_reason else "unknown error"
|
|
@@ -639,6 +694,36 @@ class EvaluationsRunner:
|
|
|
639
694
|
script execution)."""
|
|
640
695
|
return self._client.get_analysis_status(run_id)
|
|
641
696
|
|
|
697
|
+
# Run-lifecycle calls by id - the standalone forms of what run()/execute()/finalize()/
|
|
698
|
+
# analyze() drive for you, for scripts operating on a run created elsewhere.
|
|
699
|
+
|
|
700
|
+
def init_run(self, dataset_id: str, subject, **kwargs):
|
|
701
|
+
"""Create a run row without executing anything - the standalone form of :meth:`run`.
|
|
702
|
+
Accepts the same kwargs as ``EvaluationsClient.init_run``."""
|
|
703
|
+
return self._client.init_run(dataset_id, subject, **kwargs)
|
|
704
|
+
|
|
705
|
+
def append_results(self, run_id: str, batch_id: str, results: list):
|
|
706
|
+
"""Submit one batch of results to a run by id (scored synchronously server-side)."""
|
|
707
|
+
return self._client.append_results(run_id, batch_id, results)
|
|
708
|
+
|
|
709
|
+
def finalize_run(self, run_id: str) -> dict:
|
|
710
|
+
"""Mark a run completed by id - the standalone form of
|
|
711
|
+
``EvaluationRunContext.finalize()``."""
|
|
712
|
+
return self._client.finalize_run(run_id)
|
|
713
|
+
|
|
714
|
+
def analyze_run(self, run_id: str, **kwargs) -> dict:
|
|
715
|
+
"""Start the LLM analysis of a finalized run by id; poll
|
|
716
|
+
:meth:`get_analysis_status`, then :meth:`get_report`."""
|
|
717
|
+
return self._client.analyze_run(run_id, **kwargs)
|
|
718
|
+
|
|
719
|
+
def get_report(self, run_id: str):
|
|
720
|
+
"""The analyzed report for a run by id, once analysis has finished."""
|
|
721
|
+
return self._client.get_report(run_id)
|
|
722
|
+
|
|
723
|
+
def get_submitted_keys(self, run_id: str) -> list:
|
|
724
|
+
"""Idempotency keys a run has already accepted - what execute() uses to resume."""
|
|
725
|
+
return self._client.get_submitted_keys(run_id)
|
|
726
|
+
|
|
642
727
|
def gate_run(
|
|
643
728
|
self,
|
|
644
729
|
run_id: str,
|
|
@@ -153,7 +153,11 @@ def finish_llm_call(
|
|
|
153
153
|
)
|
|
154
154
|
return
|
|
155
155
|
|
|
156
|
-
span
|
|
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
|
-
|
|
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)
|