agentx-python 0.8.21__tar.gz → 0.8.23__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentx_python-0.8.21/agentx_python.egg-info → agentx_python-0.8.23}/PKG-INFO +8 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/README.md +7 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/__init__.py +4 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/agentx.py +35 -5
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/client.py +48 -23
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/evaluation_settings.py +14 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/models.py +31 -2
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/reporting.py +1 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/results.py +10 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/runner.py +149 -40
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/exceptions.py +19 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/export.py +2 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/feedback.py +2 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/_traced_call.py +5 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/autogen.py +8 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/crewai.py +70 -29
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/google_adk.py +33 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/langchain.py +91 -53
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/llamaindex.py +91 -47
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/openai_agents.py +18 -2
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/client.py +27 -13
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/improvement_groups.py +2 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/judge_scorers.py +24 -6
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/models.py +7 -5
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/scorer_groups.py +3 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/scorers.py +2 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/outcomes.py +2 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/projects.py +2 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/traces.py +2 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/tracing/ingest_client.py +48 -9
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/tracing/tracer.py +49 -19
- agentx_python-0.8.23/agentx/util.py +29 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/version.py +2 -2
- {agentx_python-0.8.21 → agentx_python-0.8.23/agentx_python.egg-info}/PKG-INFO +8 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx_python.egg-info/SOURCES.txt +3 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_deep_dive_fixes.py +45 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_docs_match_sdk.py +15 -0
- agentx_python-0.8.23/tests/test_error_taxonomy.py +86 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_integrations.py +1 -1
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_judge_scorers.py +80 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_runner_features.py +73 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_selfhost_analysis_fallback.py +21 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_span_tree.py +232 -3
- agentx_python-0.8.23/tests/test_wire_models.py +83 -0
- agentx_python-0.8.21/agentx/util.py +0 -20
- {agentx_python-0.8.21 → agentx_python-0.8.23}/LICENSE +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/cli.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/datasets.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/__init__.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/agents.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/profile.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/review_queue.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/rules.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/sessions.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/py.typed +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/resources/agent.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/testing.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/tracing/eval_scope.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx/tracing/framework_detect.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/setup.cfg +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/setup.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_cli_launcher.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_eval_scope.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_framework_detect.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_integration.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_multi_judge.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_pairwise.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_review_queue.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/tests/test_selfhost_compat.py +0 -0
- {agentx_python-0.8.21 → agentx_python-0.8.23}/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.23
|
|
4
4
|
Summary: Official Python SDK for AgentX (https://www.agentx.so/)
|
|
5
5
|
Home-page: https://github.com/AgentX-ai/AgentX-python
|
|
6
6
|
Author: Robin Wang and AgentX Team
|
|
@@ -255,6 +255,13 @@ extra:
|
|
|
255
255
|
| LlamaIndex | `pip install "agentx-python[llamaindex]"` | `AgentXLlamaIndexHandler` |
|
|
256
256
|
| AutoGen | `pip install "agentx-python[autogen]"` | `AgentXAutoGenObserver` |
|
|
257
257
|
|
|
258
|
+
> **Warning: pick ONE instrumentation layer per LLM call.** Do not combine
|
|
259
|
+
> `AgentXCallbackHandler` (or any framework integration) with a patched provider client
|
|
260
|
+
> (`patch_openai_client`, `patch_anthropic_client`, `patch_genai_client`) on the same code
|
|
261
|
+
> path. A patched call that runs outside an active span emits its own root trace, so every
|
|
262
|
+
> LLM call the framework already traces gets a duplicate trace - and its cost is counted
|
|
263
|
+
> twice.
|
|
264
|
+
|
|
258
265
|
Two more platforms are covered by **pull importers** rather than in-process hooks, each with its
|
|
259
266
|
own CLI: `agentx-moveworks` (Moveworks Data API sync, no extra needed) and `agentx-databricks`
|
|
260
267
|
(`pip install "agentx-python[databricks]"`, MLflow/Databricks trace sync).
|
|
@@ -190,6 +190,13 @@ extra:
|
|
|
190
190
|
| LlamaIndex | `pip install "agentx-python[llamaindex]"` | `AgentXLlamaIndexHandler` |
|
|
191
191
|
| AutoGen | `pip install "agentx-python[autogen]"` | `AgentXAutoGenObserver` |
|
|
192
192
|
|
|
193
|
+
> **Warning: pick ONE instrumentation layer per LLM call.** Do not combine
|
|
194
|
+
> `AgentXCallbackHandler` (or any framework integration) with a patched provider client
|
|
195
|
+
> (`patch_openai_client`, `patch_anthropic_client`, `patch_genai_client`) on the same code
|
|
196
|
+
> path. A patched call that runs outside an active span emits its own root trace, so every
|
|
197
|
+
> LLM call the framework already traces gets a duplicate trace - and its cost is counted
|
|
198
|
+
> twice.
|
|
199
|
+
|
|
193
200
|
Two more platforms are covered by **pull importers** rather than in-process hooks, each with its
|
|
194
201
|
own CLI: `agentx-moveworks` (Moveworks Data API sync, no extra needed) and `agentx-databricks`
|
|
195
202
|
(`pip install "agentx-python[databricks]"`, MLflow/Databricks trace sync).
|
|
@@ -5,6 +5,8 @@ from agentx.version import VERSION
|
|
|
5
5
|
from agentx.exceptions import (
|
|
6
6
|
AgentXError,
|
|
7
7
|
AgentXAuthError,
|
|
8
|
+
AgentXValidationError,
|
|
9
|
+
AgentXConnectionError,
|
|
8
10
|
AgentXAPIError,
|
|
9
11
|
DatasetNotFound,
|
|
10
12
|
CINotEnabled,
|
|
@@ -21,6 +23,8 @@ __all__ = [
|
|
|
21
23
|
"AgentX",
|
|
22
24
|
"AgentXError",
|
|
23
25
|
"AgentXAuthError",
|
|
26
|
+
"AgentXValidationError",
|
|
27
|
+
"AgentXConnectionError",
|
|
24
28
|
"AgentXAPIError",
|
|
25
29
|
"DatasetNotFound",
|
|
26
30
|
"CINotEnabled",
|
|
@@ -3,7 +3,7 @@ import requests
|
|
|
3
3
|
import os
|
|
4
4
|
import logging
|
|
5
5
|
|
|
6
|
-
from agentx.util import get_headers, api_base
|
|
6
|
+
from agentx.util import get_headers, api_base, normalize_base
|
|
7
7
|
from agentx.resources.agent import Agent
|
|
8
8
|
from agentx.resources.workforce import Workforce
|
|
9
9
|
|
|
@@ -16,16 +16,23 @@ class AgentX:
|
|
|
16
16
|
base_url: Optional[str] = None,
|
|
17
17
|
workspace_id: Optional[str] = None,
|
|
18
18
|
):
|
|
19
|
+
# The api_key is NOT written back into os.environ (it used to be): every sub-client
|
|
20
|
+
# below receives it explicitly, and mutating process-global state from a constructor
|
|
21
|
+
# 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.
|
|
19
24
|
self.api_key = api_key or os.getenv("AGENTX_API_KEY")
|
|
20
|
-
if self.api_key and not os.getenv("AGENTX_API_KEY"):
|
|
21
|
-
os.environ["AGENTX_API_KEY"] = self.api_key
|
|
22
25
|
|
|
23
26
|
# base_url overrides AGENTX_API_BASE_URL env var (and the SDK default). It is
|
|
24
27
|
# deliberately NOT written back into os.environ: the constructor used to do that, which
|
|
25
28
|
# made the last-constructed client silently re-point every other client in the process
|
|
26
29
|
# (deep-dive round 3, bug #1). Each sub-client below receives this value explicitly and
|
|
27
|
-
# captures it at construction instead.
|
|
30
|
+
# captures it at construction instead. Normalized (trailing slash and the
|
|
31
|
+
# /custom-agent-evaluations suffix stripped) so an evaluations-shaped URL works for
|
|
32
|
+
# every sub-client, not just evaluations.
|
|
28
33
|
self.base_url = base_url or os.getenv("AGENTX_API_BASE_URL")
|
|
34
|
+
if self.base_url:
|
|
35
|
+
self.base_url = normalize_base(self.base_url)
|
|
29
36
|
|
|
30
37
|
self.workspace_id = workspace_id or os.getenv("AGENTX_WORKSPACE_ID")
|
|
31
38
|
|
|
@@ -91,6 +98,27 @@ class AgentX:
|
|
|
91
98
|
workspace_id=self.workspace_id,
|
|
92
99
|
)
|
|
93
100
|
self.tracer = Tracer(_ingest_client)
|
|
101
|
+
self._ingest_client = _ingest_client
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# Lifecycle
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def close(self, timeout: float = 5.0) -> bool:
|
|
108
|
+
"""Flush queued traces and stop the tracer's background ingest worker.
|
|
109
|
+
|
|
110
|
+
Returns ``True`` when everything drained before ``timeout`` seconds elapsed. Optional -
|
|
111
|
+
an ``atexit`` hook already flushes queued traces on interpreter shutdown - but a
|
|
112
|
+
long-running service that tears clients down mid-process should call it (or use the
|
|
113
|
+
client as a context manager) so worker threads don't accumulate.
|
|
114
|
+
"""
|
|
115
|
+
return self._ingest_client.close(timeout)
|
|
116
|
+
|
|
117
|
+
def __enter__(self) -> "AgentX":
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def __exit__(self, exc_type, exc_val, tb) -> None:
|
|
121
|
+
self.close()
|
|
94
122
|
|
|
95
123
|
@classmethod
|
|
96
124
|
def from_env(cls) -> "AgentX":
|
|
@@ -129,7 +157,9 @@ class AgentX:
|
|
|
129
157
|
|
|
130
158
|
@staticmethod
|
|
131
159
|
def list_workforces() -> List["Workforce"]:
|
|
132
|
-
"""List all workforces/teams.
|
|
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."""
|
|
133
163
|
url = f"{api_base()}/access/teams"
|
|
134
164
|
response = requests.get(url, headers=get_headers())
|
|
135
165
|
if response.status_code == 200:
|
|
@@ -24,7 +24,12 @@ from agentx.evaluations.models import (
|
|
|
24
24
|
|
|
25
25
|
logger = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
|
-
from agentx.util import _DEFAULT_API_BASE as _UTIL_API_BASE
|
|
27
|
+
from agentx.util import _DEFAULT_API_BASE as _UTIL_API_BASE, normalize_base
|
|
28
|
+
|
|
29
|
+
# The canonical error classes (agentx.exceptions) are raised - and re-exported here for
|
|
30
|
+
# compat with code that imported them from this module - so `except agentx.AgentXAuthError`
|
|
31
|
+
# works whichever client raised.
|
|
32
|
+
from agentx.exceptions import AgentXError, AgentXAuthError, AgentXValidationError
|
|
28
33
|
|
|
29
34
|
_DEFAULT_BASE_URL = f"{_UTIL_API_BASE}/custom-agent-evaluations"
|
|
30
35
|
SDK_NAME = "agentx-python"
|
|
@@ -42,7 +47,7 @@ _SELF_HOST_ANALYZE_TIMEOUT = 1800
|
|
|
42
47
|
_SELF_HOST_SCORING_TIMEOUT = 900
|
|
43
48
|
|
|
44
49
|
|
|
45
|
-
class AgentXEvaluationsError(
|
|
50
|
+
class AgentXEvaluationsError(AgentXError):
|
|
46
51
|
"""An evaluations API call failed.
|
|
47
52
|
|
|
48
53
|
``status_code`` carries the HTTP status when the failure came from a response rather
|
|
@@ -55,14 +60,6 @@ class AgentXEvaluationsError(Exception):
|
|
|
55
60
|
self.status_code = status_code
|
|
56
61
|
|
|
57
62
|
|
|
58
|
-
class AgentXAuthError(AgentXEvaluationsError):
|
|
59
|
-
pass
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class AgentXValidationError(AgentXEvaluationsError):
|
|
63
|
-
pass
|
|
64
|
-
|
|
65
|
-
|
|
66
63
|
class EvaluationSubmissionError(AgentXEvaluationsError):
|
|
67
64
|
"""A result batch could not be submitted (after one retry). The run is left unfinalized;
|
|
68
65
|
re-running execute() on the same context resumes past already-submitted cases."""
|
|
@@ -96,13 +93,10 @@ class EvaluationsClient:
|
|
|
96
93
|
# whatever workspace the API key's user defaults to, not the one the caller intended.
|
|
97
94
|
self._workspace_id = workspace_id
|
|
98
95
|
# Priority: constructor arg > env var > SDK default
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if not _api_base.endswith("/custom-agent-evaluations"):
|
|
104
|
-
_api_base = f"{_api_base}/custom-agent-evaluations"
|
|
105
|
-
self._base_url = _api_base
|
|
96
|
+
# normalize_base strips a trailing slash and any /custom-agent-evaluations suffix,
|
|
97
|
+
# then the suffix is appended - users only need to provide /api/v1 either way.
|
|
98
|
+
_api_base = normalize_base(base_url or os.getenv("AGENTX_API_BASE_URL", _UTIL_API_BASE))
|
|
99
|
+
self._base_url = f"{_api_base}/custom-agent-evaluations"
|
|
106
100
|
# None until an analysis call tells us which engine this is; see _api_root.
|
|
107
101
|
self._analysis_on_dashboard_router: Optional[bool] = None
|
|
108
102
|
self._session = requests.Session()
|
|
@@ -184,13 +178,16 @@ class EvaluationsClient:
|
|
|
184
178
|
continue
|
|
185
179
|
|
|
186
180
|
if resp.status_code == 401:
|
|
187
|
-
raise AgentXAuthError("Invalid or missing API key")
|
|
181
|
+
raise AgentXAuthError("Invalid or missing API key", status_code=401)
|
|
188
182
|
if resp.status_code == 422:
|
|
189
183
|
raise AgentXValidationError(resp.text)
|
|
184
|
+
# 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).
|
|
190
187
|
if (
|
|
191
188
|
resp.status_code in _RETRYABLE_STATUS
|
|
192
189
|
and retry
|
|
193
|
-
and attempt <
|
|
190
|
+
and attempt < len(schedule) - 1
|
|
194
191
|
):
|
|
195
192
|
logger.debug(
|
|
196
193
|
"Retryable status %d (attempt %d)", resp.status_code, attempt + 1
|
|
@@ -224,7 +221,16 @@ class EvaluationsClient:
|
|
|
224
221
|
the Sovereignty & Portability Index. Pass ``provider`` (e.g. "Google")
|
|
225
222
|
to filter."""
|
|
226
223
|
params = {"provider": provider} if provider else None
|
|
227
|
-
|
|
224
|
+
try:
|
|
225
|
+
data = self._request("GET", "/models", params=params)
|
|
226
|
+
except AgentXEvaluationsError as exc:
|
|
227
|
+
if exc.status_code == 404:
|
|
228
|
+
raise AgentXEvaluationsError(
|
|
229
|
+
"list_models is hosted-only; on self-host pass any model id your judge "
|
|
230
|
+
"key can reach, or use client.monitor.* portability models",
|
|
231
|
+
status_code=404,
|
|
232
|
+
) from exc
|
|
233
|
+
raise
|
|
228
234
|
items = data if isinstance(data, list) else data.get("models", [])
|
|
229
235
|
return [ModelInfo(**m) for m in items]
|
|
230
236
|
|
|
@@ -500,6 +506,17 @@ class EvaluationsClient:
|
|
|
500
506
|
return self._report_from_dashboard(run_id)
|
|
501
507
|
|
|
502
508
|
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."""
|
|
512
|
+
import warnings
|
|
513
|
+
|
|
514
|
+
warnings.warn(
|
|
515
|
+
"get_missing_results() always returns [] on self-host - use get_submitted_keys() "
|
|
516
|
+
"to resume a run instead.",
|
|
517
|
+
DeprecationWarning,
|
|
518
|
+
stacklevel=2,
|
|
519
|
+
)
|
|
503
520
|
data = self._request("GET", f"/runs/{run_id}/missing-results")
|
|
504
521
|
return data if isinstance(data, list) else data.get("missing", [])
|
|
505
522
|
|
|
@@ -538,12 +555,20 @@ class EvaluationsClient:
|
|
|
538
555
|
) -> bool:
|
|
539
556
|
"""Return True if ``exc`` is the 404 that means "this engine is self-host".
|
|
540
557
|
|
|
541
|
-
Only a 404 qualifies. Anything else - auth, validation, a 500, a dead
|
|
542
|
-
is a real failure on a route that does exist, and must propagate rather
|
|
543
|
-
retried against a different endpoint that would mask it.
|
|
558
|
+
Only a route-level 404 qualifies. Anything else - auth, validation, a 500, a dead
|
|
559
|
+
connection - is a real failure on a route that does exist, and must propagate rather
|
|
560
|
+
than be retried against a different endpoint that would mask it.
|
|
561
|
+
|
|
562
|
+
A resource 404 does not qualify either: the engine's SDK router answers these routes
|
|
563
|
+
with bodies naming the missing resource ("Run not found" / "No analysis found for
|
|
564
|
+
this run"), so latching on one would permanently reroute every later analysis call
|
|
565
|
+
to the dashboard router because a caller once passed a wrong run id.
|
|
544
566
|
"""
|
|
545
567
|
if exc.status_code != 404:
|
|
546
568
|
return False
|
|
569
|
+
body = str(exc)
|
|
570
|
+
if "Run not found" in body or "No analysis found for this run" in body:
|
|
571
|
+
return False
|
|
547
572
|
if self._analysis_on_dashboard_router is None:
|
|
548
573
|
logger.info(
|
|
549
574
|
"%s is not served from %s; using the dashboard router at %s "
|
|
@@ -74,8 +74,21 @@ class EvaluationSettingsBuilder:
|
|
|
74
74
|
# Sandboxed JS scorers run per result alongside the judge - each entry is
|
|
75
75
|
# {"name": ..., "enabled": True, "code": "..."} where the code is a JS function body
|
|
76
76
|
# receiving (input, output, expected, toolCalls) and returning {score, reasoning}.
|
|
77
|
+
# Normalized the same way DatasetBuilder does: id defaulted, name optional (the
|
|
78
|
+
# engine defaults it), enabled default True - raw pass-through sent entries the
|
|
79
|
+
# engine's shape validation rejects.
|
|
77
80
|
if code_scorers:
|
|
78
|
-
|
|
81
|
+
import uuid as _uuid
|
|
82
|
+
|
|
83
|
+
self._payload["codeScorers"] = [
|
|
84
|
+
{
|
|
85
|
+
"id": scorer.get("id") or _uuid.uuid4().hex[:12],
|
|
86
|
+
"name": scorer.get("name"),
|
|
87
|
+
"code": scorer["code"],
|
|
88
|
+
"enabled": scorer.get("enabled", True),
|
|
89
|
+
}
|
|
90
|
+
for scorer in code_scorers
|
|
91
|
+
]
|
|
79
92
|
|
|
80
93
|
def publish(self) -> EvaluationSettings:
|
|
81
94
|
logger.info("Publishing evaluation settings '%s'", self._payload["name"])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, List, Literal, Optional, Union
|
|
4
|
-
from pydantic import AliasChoices, BaseModel, Field, model_validator
|
|
4
|
+
from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator
|
|
5
5
|
|
|
6
6
|
# ---------------------------------------------------------------------------
|
|
7
7
|
# Observable trace
|
|
@@ -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.
|
|
@@ -78,6 +82,17 @@ class Dataset(BaseModel):
|
|
|
78
82
|
# Custom code scorers attached to this dataset - [{ id, name, code, enabled }]. Retrievable,
|
|
79
83
|
# so a fetched dataset round-trips them (import_dataset copies them to the new dataset).
|
|
80
84
|
code_scorers: Optional[List[Dict[str, Any]]] = Field(default=None, alias="codeScorers")
|
|
85
|
+
# Grading config carried on the dataset itself - similarity metric toggles (each a
|
|
86
|
+
# {"enabled": bool, ...} object on the wire), LLM-as-judge overrides, and the raw
|
|
87
|
+
# sovereigntyIndex object. Modeled so a fetched Dataset round-trips them: extra="ignore"
|
|
88
|
+
# used to silently drop all of these on read, and import_dataset lost them on the copy.
|
|
89
|
+
vector_similarity: Optional[Any] = Field(default=None, alias="vectorSimilarity")
|
|
90
|
+
jaccard_similarity: Optional[Any] = Field(default=None, alias="jaccardSimilarity")
|
|
91
|
+
bleu_score: Optional[Any] = Field(default=None, alias="bleuScore")
|
|
92
|
+
rouge_score: Optional[Any] = Field(default=None, alias="rougeScore")
|
|
93
|
+
judge_prompt: Optional[str] = Field(default=None, alias="judgePrompt")
|
|
94
|
+
judge_model: Optional[str] = Field(default=None, alias="judgeModel")
|
|
95
|
+
sovereignty_index: Optional[Dict[str, Any]] = Field(default=None, alias="sovereigntyIndex")
|
|
81
96
|
status: str = "published"
|
|
82
97
|
version_id: Optional[str] = Field(default=None, alias="versionId")
|
|
83
98
|
# Sovereignty & Portability - models selected to compare on this dataset.
|
|
@@ -430,7 +445,12 @@ class RunResultRow(BaseModel):
|
|
|
430
445
|
question_index: Optional[int] = Field(default=None, alias="questionIndex")
|
|
431
446
|
run_number: Optional[int] = Field(default=None, alias="runNumber")
|
|
432
447
|
question_text: Optional[str] = Field(default=None, alias="questionText")
|
|
433
|
-
|
|
448
|
+
# The engine sends the agent's answer as an `output` OBJECT ({"text": ...}), not a
|
|
449
|
+
# `response` string - accept both spellings and lift the dict's text (see the
|
|
450
|
+
# validator below), so row.response actually populates on self-host.
|
|
451
|
+
response: Optional[str] = Field(
|
|
452
|
+
default=None, validation_alias=AliasChoices("response", "output")
|
|
453
|
+
)
|
|
434
454
|
trace_id: Optional[str] = Field(default=None, alias="traceId")
|
|
435
455
|
latency_ms: Optional[float] = Field(default=None, alias="latencyMs")
|
|
436
456
|
input_tokens: Optional[int] = Field(default=None, alias="inputTokens")
|
|
@@ -452,6 +472,15 @@ class RunResultRow(BaseModel):
|
|
|
452
472
|
populate_by_name = True
|
|
453
473
|
extra = "ignore"
|
|
454
474
|
|
|
475
|
+
@field_validator("response", mode="before")
|
|
476
|
+
@classmethod
|
|
477
|
+
def _lift_output_text(cls, value: Any) -> Any:
|
|
478
|
+
# The `output` alias delivers the wire's whole output object - keep the declared
|
|
479
|
+
# Optional[str] by lifting its text field.
|
|
480
|
+
if isinstance(value, dict):
|
|
481
|
+
return value.get("text")
|
|
482
|
+
return value
|
|
483
|
+
|
|
455
484
|
@classmethod
|
|
456
485
|
def from_wire(cls, wire: Dict[str, Any]) -> "RunResultRow":
|
|
457
486
|
row = cls.model_validate(wire)
|
|
@@ -170,7 +170,7 @@ def print_report(report: Report) -> None:
|
|
|
170
170
|
|
|
171
171
|
# --- Low-scoring cases ---
|
|
172
172
|
if report.low_scoring_cases:
|
|
173
|
-
_section("Low-scoring Cases (rating
|
|
173
|
+
_section("Low-scoring Cases (rating <= 5)")
|
|
174
174
|
for case in report.low_scoring_cases[:5]:
|
|
175
175
|
q = (case.get("query") or case.get("questionText", ""))[:80]
|
|
176
176
|
rating = case.get("rating", "?")
|
|
@@ -87,6 +87,16 @@ def normalize_result(
|
|
|
87
87
|
else:
|
|
88
88
|
output = {"text": str(raw)} if raw is not None else {"text": ""}
|
|
89
89
|
|
|
90
|
+
if error is None and (
|
|
91
|
+
output is None
|
|
92
|
+
or (set(output) <= {"text"} and not str(output.get("text") or "").strip())
|
|
93
|
+
):
|
|
94
|
+
# An empty output with no error would fail the engine's row validation and silently
|
|
95
|
+
# vanish from the run - store it as an explicit failed row instead.
|
|
96
|
+
error = ResultError(type="EmptyOutput", message="Agent returned no output")
|
|
97
|
+
if output is None:
|
|
98
|
+
output = {"text": ""}
|
|
99
|
+
|
|
90
100
|
has_timings = (
|
|
91
101
|
latency_ms is not None or input_tokens is not None or output_tokens is not None
|
|
92
102
|
)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import sys
|
|
5
6
|
import time
|
|
6
7
|
|
|
7
8
|
import requests
|
|
@@ -11,7 +12,11 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Union
|
|
|
11
12
|
from agentx.evaluations.adapters.raw import RawCallableAdapter
|
|
12
13
|
from agentx.evaluations.adapters.precomputed import PrecomputedAdapter
|
|
13
14
|
from agentx.evaluations.adapters.http_endpoint import HttpEndpointAdapter
|
|
14
|
-
from agentx.evaluations.client import
|
|
15
|
+
from agentx.evaluations.client import (
|
|
16
|
+
AgentXEvaluationsError,
|
|
17
|
+
EvaluationsClient,
|
|
18
|
+
EvaluationSubmissionError,
|
|
19
|
+
)
|
|
15
20
|
from agentx.evaluations.models import (
|
|
16
21
|
AnalysisStatus,
|
|
17
22
|
Dataset,
|
|
@@ -227,6 +232,7 @@ class EvaluationRunContext:
|
|
|
227
232
|
if concurrency > 1:
|
|
228
233
|
import concurrent.futures
|
|
229
234
|
import contextvars
|
|
235
|
+
from collections import deque
|
|
230
236
|
|
|
231
237
|
def in_scope(case: EvaluationCase) -> EvaluationResult:
|
|
232
238
|
# ContextVars (the eval-run scope) do not cross thread boundaries on their own -
|
|
@@ -240,48 +246,85 @@ class EvaluationRunContext:
|
|
|
240
246
|
if _idem_key(self._run.run_id, case.case_id, case.run_number) not in already_done
|
|
241
247
|
]
|
|
242
248
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=concurrency)
|
|
243
|
-
# map() yields in submission order, so batching/submission below stays deterministic.
|
|
244
|
-
mapped = executor.map(in_scope, pending)
|
|
245
249
|
|
|
246
|
-
def
|
|
250
|
+
def bounded() -> "Iterator[EvaluationResult]":
|
|
251
|
+
# Bounded submit loop instead of executor.map(): map() dispatches EVERY case
|
|
252
|
+
# up front, so a fail-fast flush failure (EvaluationSubmissionError below)
|
|
253
|
+
# still paid for the whole rest of the run in agent calls. Keep at most
|
|
254
|
+
# `concurrency` cases in flight, topping up as results are consumed; yields
|
|
255
|
+
# stay in submission order so batching below is deterministic. On teardown
|
|
256
|
+
# (an exception in the consuming loop closes this generator) whatever is
|
|
257
|
+
# queued but unstarted is cancelled.
|
|
258
|
+
import itertools
|
|
259
|
+
|
|
260
|
+
case_iter = iter(pending)
|
|
261
|
+
in_flight: "deque[concurrent.futures.Future]" = deque()
|
|
247
262
|
try:
|
|
248
|
-
|
|
263
|
+
for case in itertools.islice(case_iter, concurrency):
|
|
264
|
+
in_flight.append(executor.submit(in_scope, case))
|
|
265
|
+
while in_flight:
|
|
266
|
+
result = in_flight.popleft().result()
|
|
267
|
+
next_case = next(case_iter, None)
|
|
268
|
+
if next_case is not None:
|
|
269
|
+
in_flight.append(executor.submit(in_scope, next_case))
|
|
270
|
+
yield result
|
|
249
271
|
finally:
|
|
250
|
-
executor.shutdown(wait=True)
|
|
272
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
251
273
|
|
|
252
|
-
results_iter =
|
|
274
|
+
results_iter = bounded()
|
|
253
275
|
else:
|
|
254
276
|
results_iter = None # sequential path below produces inline
|
|
255
277
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if len(batch) >= max_batch:
|
|
280
|
-
self._flush_batch(batch)
|
|
281
|
-
batch = []
|
|
278
|
+
try:
|
|
279
|
+
for idx, case in enumerate(cases, start=1):
|
|
280
|
+
idem_key = _idem_key(self._run.run_id, case.case_id, case.run_number)
|
|
281
|
+
|
|
282
|
+
if idem_key in already_done:
|
|
283
|
+
logger.debug("Skipping already-submitted case: %s", idem_key)
|
|
284
|
+
_print_progress(idx, total, case, skipped=True)
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
result = next(results_iter) if results_iter is not None else produce(case)
|
|
288
|
+
result.idempotency_key = idem_key
|
|
289
|
+
# Tag the result with the case's model so the server can group it into
|
|
290
|
+
# the Sovereignty & Portability matrix (the callable may also set it).
|
|
291
|
+
if case.model:
|
|
292
|
+
meta = dict(result.metadata or {})
|
|
293
|
+
meta.setdefault("model", case.model)
|
|
294
|
+
result.metadata = meta
|
|
295
|
+
result = EvaluationResult(
|
|
296
|
+
**{**result.model_dump(), "idempotencyKey": idem_key}
|
|
297
|
+
)
|
|
298
|
+
self._results.append(result)
|
|
299
|
+
batch.append(result)
|
|
300
|
+
_print_progress(idx, total, case, result=result)
|
|
282
301
|
|
|
283
|
-
|
|
284
|
-
|
|
302
|
+
if len(batch) >= max_batch:
|
|
303
|
+
self._flush_batch(batch)
|
|
304
|
+
batch = []
|
|
305
|
+
finally:
|
|
306
|
+
# Deterministic teardown: a flush failure mid-run must stop the in-flight agent
|
|
307
|
+
# dispatch NOW (bounded()'s finally cancels queued cases), not whenever the
|
|
308
|
+
# generator happens to be garbage-collected.
|
|
309
|
+
if results_iter is not None:
|
|
310
|
+
results_iter.close()
|
|
311
|
+
# Flush the trailing partial batch HERE, not after the try: a mid-run exception
|
|
312
|
+
# (agent crash, Ctrl-C) used to discard up to max_batch - 1 already-paid-for
|
|
313
|
+
# results still waiting in it.
|
|
314
|
+
if batch:
|
|
315
|
+
propagating = sys.exc_info()[1]
|
|
316
|
+
try:
|
|
317
|
+
self._flush_batch(batch)
|
|
318
|
+
except Exception as flush_exc:
|
|
319
|
+
if propagating is None:
|
|
320
|
+
raise
|
|
321
|
+
# An exception is already propagating out of the loop - a flush failure
|
|
322
|
+
# here must not mask it.
|
|
323
|
+
logger.error(
|
|
324
|
+
"Trailing batch flush failed while handling %r: %s",
|
|
325
|
+
propagating,
|
|
326
|
+
flush_exc,
|
|
327
|
+
)
|
|
285
328
|
|
|
286
329
|
return self
|
|
287
330
|
|
|
@@ -298,6 +341,15 @@ class EvaluationRunContext:
|
|
|
298
341
|
_say(
|
|
299
342
|
f" {green('✓')} Scored {resp.accepted} result{'s' if resp.accepted != 1 else ''}"
|
|
300
343
|
)
|
|
344
|
+
if resp.failed_validation > 0:
|
|
345
|
+
# The engine accepts the batch but silently drops rows that fail its
|
|
346
|
+
# validation (typically empty output and no error) - say so, or those
|
|
347
|
+
# cases just vanish from the report.
|
|
348
|
+
_say(
|
|
349
|
+
f" {yellow('!')} {resp.failed_validation} result"
|
|
350
|
+
f"{'s' if resp.failed_validation != 1 else ''} failed validation "
|
|
351
|
+
"(empty output and no error) and did not get stored"
|
|
352
|
+
)
|
|
301
353
|
logger.info(
|
|
302
354
|
"Batch %s: accepted=%d duplicates=%d failed=%d",
|
|
303
355
|
batch_id[:8],
|
|
@@ -333,9 +385,21 @@ class EvaluationRunContext:
|
|
|
333
385
|
so a re-execute() after a crash skips (and never re-pays for) finished cases."""
|
|
334
386
|
try:
|
|
335
387
|
return set(self._client.get_submitted_keys(self._run.run_id))
|
|
336
|
-
except
|
|
337
|
-
|
|
338
|
-
|
|
388
|
+
except AgentXEvaluationsError as exc:
|
|
389
|
+
if exc.status_code == 404:
|
|
390
|
+
# Older engines without the route: no resume, identical to the historical
|
|
391
|
+
# behavior. ONLY the 404 qualifies - a transient 502/timeout here used to be
|
|
392
|
+
# swallowed too, and an empty resume set silently re-runs (and re-bills)
|
|
393
|
+
# every already-finished case.
|
|
394
|
+
return set()
|
|
395
|
+
_say(f" {red('✗')} Could not fetch already-submitted keys: {dim(str(exc))}")
|
|
396
|
+
logger.error(
|
|
397
|
+
"Resume-key fetch for run %s failed (%s) - refusing to re-run the whole run "
|
|
398
|
+
"blind; retry execute() once the engine is reachable",
|
|
399
|
+
self._run.run_id,
|
|
400
|
+
exc,
|
|
401
|
+
)
|
|
402
|
+
raise
|
|
339
403
|
|
|
340
404
|
# ------------------------------------------------------------------
|
|
341
405
|
# Step 2: finalize
|
|
@@ -375,8 +439,11 @@ class EvaluationRunContext:
|
|
|
375
439
|
``no_regression=True`` fails it when the average dropped more than ``tolerance``
|
|
376
440
|
(default 0.5, judge scores are noisy) below the dataset's previous completed run.
|
|
377
441
|
At least one check is required. On a multi-judge run, ``scorer`` (an additional
|
|
378
|
-
scorer's id or name, e.g. ``scorer="Safety"``) gates that scorer's own average
|
|
379
|
-
instead of the primary's - "fail if Safety is low even when the average looks fine".
|
|
442
|
+
judge scorer's id or name, e.g. ``scorer="Safety"``) gates that scorer's own average
|
|
443
|
+
instead of the primary's - "fail if Safety is low even when the average looks fine".
|
|
444
|
+
Only judge scorers resolve here: deterministic scorer-group members (pattern/code
|
|
445
|
+
kinds) have no per-run judge average, so naming one is rejected by the engine.
|
|
446
|
+
Prints a CI-log-friendly verdict and returns a
|
|
380
447
|
:class:`GateResult` - the caller decides the exit code::
|
|
381
448
|
|
|
382
449
|
report = client.evaluations.run(...).execute(my_agent).finalize()
|
|
@@ -429,6 +496,16 @@ class EvaluationRunContext:
|
|
|
429
496
|
"""Number of submitted results that have received a rating so far."""
|
|
430
497
|
return self._live_stats.rated_count if self._live_stats else 0
|
|
431
498
|
|
|
499
|
+
@property
|
|
500
|
+
def skipped_count(self) -> int:
|
|
501
|
+
"""Number of submitted results the judge could not score."""
|
|
502
|
+
return self._live_stats.skipped_count if self._live_stats else 0
|
|
503
|
+
|
|
504
|
+
@property
|
|
505
|
+
def failed_count(self) -> int:
|
|
506
|
+
"""Number of submitted results that carried an error."""
|
|
507
|
+
return self._live_stats.failed_count if self._live_stats else 0
|
|
508
|
+
|
|
432
509
|
@property
|
|
433
510
|
def average_rating(self) -> Optional[float]:
|
|
434
511
|
"""Live average rating across all results scored so far. Populated as
|
|
@@ -465,6 +542,8 @@ class EvaluationRunContext:
|
|
|
465
542
|
|
|
466
543
|
Args:
|
|
467
544
|
mode: "auto" (default), "sync", or "batch" - how item scoring executes server-side.
|
|
545
|
+
Hosted-only: self-host runs the analysis synchronously regardless; see the
|
|
546
|
+
response's mode field for what actually ran.
|
|
468
547
|
quality_mode: "quality_first" or "balanced" - how many items get a second/third judge.
|
|
469
548
|
judges: 1-3 model ids from ``client.evaluations.list_models()``, e.g.
|
|
470
549
|
``["gpt-5.6-luna", "claude-opus-4-8"]``. Omit to let the engine score with its
|
|
@@ -597,6 +676,36 @@ class EvaluationsRunner:
|
|
|
597
676
|
script execution)."""
|
|
598
677
|
return self._client.get_analysis_status(run_id)
|
|
599
678
|
|
|
679
|
+
# Run-lifecycle calls by id - the standalone forms of what run()/execute()/finalize()/
|
|
680
|
+
# analyze() drive for you, for scripts operating on a run created elsewhere.
|
|
681
|
+
|
|
682
|
+
def init_run(self, dataset_id: str, subject, **kwargs):
|
|
683
|
+
"""Create a run row without executing anything - the standalone form of :meth:`run`.
|
|
684
|
+
Accepts the same kwargs as ``EvaluationsClient.init_run``."""
|
|
685
|
+
return self._client.init_run(dataset_id, subject, **kwargs)
|
|
686
|
+
|
|
687
|
+
def append_results(self, run_id: str, batch_id: str, results: list):
|
|
688
|
+
"""Submit one batch of results to a run by id (scored synchronously server-side)."""
|
|
689
|
+
return self._client.append_results(run_id, batch_id, results)
|
|
690
|
+
|
|
691
|
+
def finalize_run(self, run_id: str) -> dict:
|
|
692
|
+
"""Mark a run completed by id - the standalone form of
|
|
693
|
+
``EvaluationRunContext.finalize()``."""
|
|
694
|
+
return self._client.finalize_run(run_id)
|
|
695
|
+
|
|
696
|
+
def analyze_run(self, run_id: str, **kwargs) -> dict:
|
|
697
|
+
"""Start the LLM analysis of a finalized run by id; poll
|
|
698
|
+
:meth:`get_analysis_status`, then :meth:`get_report`."""
|
|
699
|
+
return self._client.analyze_run(run_id, **kwargs)
|
|
700
|
+
|
|
701
|
+
def get_report(self, run_id: str):
|
|
702
|
+
"""The analyzed report for a run by id, once analysis has finished."""
|
|
703
|
+
return self._client.get_report(run_id)
|
|
704
|
+
|
|
705
|
+
def get_submitted_keys(self, run_id: str) -> list:
|
|
706
|
+
"""Idempotency keys a run has already accepted - what execute() uses to resume."""
|
|
707
|
+
return self._client.get_submitted_keys(run_id)
|
|
708
|
+
|
|
600
709
|
def gate_run(
|
|
601
710
|
self,
|
|
602
711
|
run_id: str,
|