agentx-python 0.6.4__tar.gz → 0.6.5__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.6.4/agentx_python.egg-info → agentx_python-0.6.5}/PKG-INFO +1 -1
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/agentx.py +8 -2
- agentx_python-0.6.5/agentx/evaluations/__init__.py +7 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/client.py +65 -6
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/datasets.py +12 -1
- agentx_python-0.6.5/agentx/evaluations/evaluation_settings.py +109 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/models.py +89 -7
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/reporting.py +14 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/results.py +4 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/runner.py +87 -10
- agentx_python-0.6.5/agentx/resources/__init__.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/resources/conversation.py +2 -2
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/tracing/ingest_client.py +24 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/tracing/tracer.py +56 -5
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/util.py +2 -1
- agentx_python-0.6.5/agentx/version.py +1 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5/agentx_python.egg-info}/PKG-INFO +1 -1
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx_python.egg-info/SOURCES.txt +3 -0
- agentx_python-0.6.5/agentx_python.egg-info/not-zip-safe +1 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/setup.py +5 -0
- agentx_python-0.6.4/agentx/evaluations/__init__.py +0 -3
- agentx_python-0.6.4/agentx/version.py +0 -1
- {agentx_python-0.6.4 → agentx_python-0.6.5}/LICENSE +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/README.md +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/__init__.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/redaction.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/exceptions.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/integrations/_perf.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/integrations/crewai.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/integrations/langchain.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/integrations/openai_agents.py +0 -0
- /agentx_python-0.6.4/agentx/resources/__init__.py → /agentx_python-0.6.5/agentx/py.typed +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/resources/agent.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/setup.cfg +0 -0
- {agentx_python-0.6.4 → agentx_python-0.6.5}/tests/test_integration.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Optional
|
|
2
2
|
import requests
|
|
3
3
|
import os
|
|
4
4
|
import logging
|
|
@@ -10,7 +10,12 @@ from agentx.resources.workforce import Workforce
|
|
|
10
10
|
|
|
11
11
|
class AgentX:
|
|
12
12
|
|
|
13
|
-
def __init__(
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
api_key: Optional[str] = None,
|
|
16
|
+
base_url: Optional[str] = None,
|
|
17
|
+
workspace_id: Optional[str] = None,
|
|
18
|
+
):
|
|
14
19
|
self.api_key = api_key or os.getenv("AGENTX_API_KEY")
|
|
15
20
|
if self.api_key and not os.getenv("AGENTX_API_KEY"):
|
|
16
21
|
os.environ["AGENTX_API_KEY"] = self.api_key
|
|
@@ -32,6 +37,7 @@ class AgentX:
|
|
|
32
37
|
api_key=self.api_key,
|
|
33
38
|
sdk_version=VERSION,
|
|
34
39
|
base_url=self.base_url,
|
|
40
|
+
workspace_id=self.workspace_id,
|
|
35
41
|
)
|
|
36
42
|
self.evaluations = EvaluationsRunner(_eval_client)
|
|
37
43
|
|
|
@@ -13,6 +13,7 @@ from agentx.evaluations.models import (
|
|
|
13
13
|
Dataset,
|
|
14
14
|
EvaluationResult,
|
|
15
15
|
EvaluationRun,
|
|
16
|
+
EvaluationSettings,
|
|
16
17
|
EvaluationSubject,
|
|
17
18
|
ModelInfo,
|
|
18
19
|
Report,
|
|
@@ -44,12 +45,20 @@ class AgentXValidationError(AgentXEvaluationsError):
|
|
|
44
45
|
|
|
45
46
|
class EvaluationsClient:
|
|
46
47
|
def __init__(
|
|
47
|
-
self,
|
|
48
|
+
self,
|
|
49
|
+
api_key: str,
|
|
50
|
+
sdk_version: str = "unknown",
|
|
51
|
+
base_url: Optional[str] = None,
|
|
52
|
+
workspace_id: Optional[str] = None,
|
|
48
53
|
):
|
|
49
54
|
if not api_key:
|
|
50
55
|
raise AgentXAuthError("AGENTX_API_KEY is required")
|
|
51
56
|
self._api_key = api_key
|
|
52
57
|
self._sdk_version = sdk_version
|
|
58
|
+
# Falls back to the caller's default workspace server-side when unset — see
|
|
59
|
+
# _with_workspace(). Without this, dataset/settings/run creation silently land in
|
|
60
|
+
# whatever workspace the API key's user defaults to, not the one the caller intended.
|
|
61
|
+
self._workspace_id = workspace_id
|
|
53
62
|
# Priority: constructor arg > env var > SDK default
|
|
54
63
|
# Always append /custom-agent-evaluations so users only need to provide /api/v1
|
|
55
64
|
_api_base = (
|
|
@@ -67,15 +76,29 @@ class EvaluationsClient:
|
|
|
67
76
|
"accept": "*/*",
|
|
68
77
|
}
|
|
69
78
|
)
|
|
70
|
-
# Expose dataset builder
|
|
79
|
+
# Expose dataset / evaluation-settings builder factories
|
|
71
80
|
from agentx.evaluations.datasets import DatasetClient
|
|
81
|
+
from agentx.evaluations.evaluation_settings import EvaluationSettingsClient
|
|
72
82
|
|
|
73
83
|
self.datasets = DatasetClient(self)
|
|
84
|
+
self.settings = EvaluationSettingsClient(self)
|
|
74
85
|
|
|
75
86
|
# ------------------------------------------------------------------
|
|
76
87
|
# Low-level HTTP
|
|
77
88
|
# ------------------------------------------------------------------
|
|
78
89
|
|
|
90
|
+
def _with_workspace(self, payload: dict) -> dict:
|
|
91
|
+
"""Injects the client's workspace_id into a request payload, unless the caller already
|
|
92
|
+
set one explicitly. Without this, requests silently fall back to the API key user's
|
|
93
|
+
default workspace server-side, which may not be the workspace the caller intended."""
|
|
94
|
+
if self._workspace_id and not payload.get("workspaceId"):
|
|
95
|
+
return {**payload, "workspaceId": self._workspace_id}
|
|
96
|
+
return payload
|
|
97
|
+
|
|
98
|
+
def _workspace_params(self) -> Optional[dict]:
|
|
99
|
+
"""Same as _with_workspace(), for GET requests that take workspaceId as a query param."""
|
|
100
|
+
return {"workspaceId": self._workspace_id} if self._workspace_id else None
|
|
101
|
+
|
|
79
102
|
def _request(self, method: str, path: str, timeout: int = 30, **kwargs) -> Any:
|
|
80
103
|
url = f"{self._base_url}{path}"
|
|
81
104
|
last_exc: Optional[Exception] = None
|
|
@@ -125,20 +148,52 @@ class EvaluationsClient:
|
|
|
125
148
|
# ------------------------------------------------------------------
|
|
126
149
|
|
|
127
150
|
def create_dataset(self, payload: dict) -> Dataset:
|
|
128
|
-
data = self._request("POST", "/datasets", json=payload)
|
|
151
|
+
data = self._request("POST", "/datasets", json=self._with_workspace(payload))
|
|
129
152
|
return Dataset(**data)
|
|
130
153
|
|
|
131
154
|
def list_datasets(self) -> List[Dataset]:
|
|
132
|
-
data = self._request("GET", "/datasets")
|
|
155
|
+
data = self._request("GET", "/datasets", params=self._workspace_params())
|
|
133
156
|
return [
|
|
134
157
|
Dataset(**d)
|
|
135
158
|
for d in (data if isinstance(data, list) else data.get("datasets", []))
|
|
136
159
|
]
|
|
137
160
|
|
|
138
161
|
def get_dataset(self, dataset_id: str) -> Dataset:
|
|
139
|
-
data = self._request(
|
|
162
|
+
data = self._request(
|
|
163
|
+
"GET", f"/datasets/{dataset_id}", params=self._workspace_params()
|
|
164
|
+
)
|
|
140
165
|
return Dataset(**data)
|
|
141
166
|
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# Evaluation Settings endpoints — standalone grading config, reusable
|
|
169
|
+
# across datasets.
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def create_evaluation_settings(self, payload: dict) -> EvaluationSettings:
|
|
173
|
+
data = self._request(
|
|
174
|
+
"POST", "/evaluation-settings", json=self._with_workspace(payload)
|
|
175
|
+
)
|
|
176
|
+
return EvaluationSettings(**data)
|
|
177
|
+
|
|
178
|
+
def list_evaluation_settings(self) -> List[EvaluationSettings]:
|
|
179
|
+
data = self._request(
|
|
180
|
+
"GET", "/evaluation-settings", params=self._workspace_params()
|
|
181
|
+
)
|
|
182
|
+
return [
|
|
183
|
+
EvaluationSettings(**e)
|
|
184
|
+
for e in (
|
|
185
|
+
data if isinstance(data, list) else data.get("evaluationSettings", [])
|
|
186
|
+
)
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
def get_evaluation_settings(self, evaluation_settings_id: str) -> EvaluationSettings:
|
|
190
|
+
data = self._request(
|
|
191
|
+
"GET",
|
|
192
|
+
f"/evaluation-settings/{evaluation_settings_id}",
|
|
193
|
+
params=self._workspace_params(),
|
|
194
|
+
)
|
|
195
|
+
return EvaluationSettings(**data)
|
|
196
|
+
|
|
142
197
|
# ------------------------------------------------------------------
|
|
143
198
|
# Run endpoints
|
|
144
199
|
# ------------------------------------------------------------------
|
|
@@ -148,6 +203,7 @@ class EvaluationsClient:
|
|
|
148
203
|
dataset_id: str,
|
|
149
204
|
subject: EvaluationSubject,
|
|
150
205
|
python_version: Optional[str] = None,
|
|
206
|
+
evaluation_settings_id: Optional[str] = None,
|
|
151
207
|
) -> EvaluationRun:
|
|
152
208
|
from agentx.version import VERSION
|
|
153
209
|
|
|
@@ -162,7 +218,9 @@ class EvaluationsClient:
|
|
|
162
218
|
"pythonVersion": python_version or _python_version(),
|
|
163
219
|
},
|
|
164
220
|
}
|
|
165
|
-
|
|
221
|
+
if evaluation_settings_id:
|
|
222
|
+
payload["evaluationSettingsId"] = evaluation_settings_id
|
|
223
|
+
data = self._request("POST", "/runs", json=self._with_workspace(payload))
|
|
166
224
|
return EvaluationRun(**data)
|
|
167
225
|
|
|
168
226
|
def append_results(
|
|
@@ -207,6 +265,7 @@ def _result_to_payload(r: EvaluationResult) -> dict:
|
|
|
207
265
|
d["questionIndex"] = d.pop("question_index", d.get("questionIndex"))
|
|
208
266
|
d["runNumber"] = d.pop("run_number", d.get("runNumber"))
|
|
209
267
|
d["idempotencyKey"] = d.pop("idempotency_key", d.get("idempotencyKey"))
|
|
268
|
+
d["traceId"] = d.pop("trace_id", d.get("traceId"))
|
|
210
269
|
return {k: v for k, v in d.items() if v is not None}
|
|
211
270
|
|
|
212
271
|
|
|
@@ -29,6 +29,8 @@ class DatasetBuilder:
|
|
|
29
29
|
evaluation_criteria: Optional[str] = None,
|
|
30
30
|
vector_similarity: bool = False,
|
|
31
31
|
jaccard_similarity: bool = False,
|
|
32
|
+
bleu_score: bool = False,
|
|
33
|
+
rouge_score: bool = False,
|
|
32
34
|
similarity_model: Optional[str] = None,
|
|
33
35
|
sovereignty_models: Optional[List[str]] = None,
|
|
34
36
|
):
|
|
@@ -43,7 +45,8 @@ class DatasetBuilder:
|
|
|
43
45
|
"questions": [],
|
|
44
46
|
}
|
|
45
47
|
# Opt-in similarity metrics, surfaced on the report as cosine_similarity /
|
|
46
|
-
# jaccard_similarity (computed against each
|
|
48
|
+
# jaccard_similarity / bleu_score / rouge_score (computed against each
|
|
49
|
+
# case's expected_results).
|
|
47
50
|
if vector_similarity:
|
|
48
51
|
vs: Dict[str, Any] = {"enabled": True}
|
|
49
52
|
if similarity_model:
|
|
@@ -51,6 +54,10 @@ class DatasetBuilder:
|
|
|
51
54
|
self._payload["vectorSimilarity"] = vs
|
|
52
55
|
if jaccard_similarity:
|
|
53
56
|
self._payload["jaccardSimilarity"] = {"enabled": True}
|
|
57
|
+
if bleu_score:
|
|
58
|
+
self._payload["bleuScore"] = {"enabled": True}
|
|
59
|
+
if rouge_score:
|
|
60
|
+
self._payload["rougeScore"] = {"enabled": True}
|
|
54
61
|
# Sovereignty & Portability — the models to compare on this dataset (use
|
|
55
62
|
# client.evaluations.list_models() to discover valid ids).
|
|
56
63
|
if sovereignty_models:
|
|
@@ -212,6 +219,8 @@ class DatasetClient:
|
|
|
212
219
|
evaluation_criteria: Optional[str] = None,
|
|
213
220
|
vector_similarity: bool = False,
|
|
214
221
|
jaccard_similarity: bool = False,
|
|
222
|
+
bleu_score: bool = False,
|
|
223
|
+
rouge_score: bool = False,
|
|
215
224
|
similarity_model: Optional[str] = None,
|
|
216
225
|
sovereignty_models: Optional[List[str]] = None,
|
|
217
226
|
) -> DatasetBuilder:
|
|
@@ -225,6 +234,8 @@ class DatasetClient:
|
|
|
225
234
|
evaluation_criteria=evaluation_criteria,
|
|
226
235
|
vector_similarity=vector_similarity,
|
|
227
236
|
jaccard_similarity=jaccard_similarity,
|
|
237
|
+
bleu_score=bleu_score,
|
|
238
|
+
rouge_score=rouge_score,
|
|
228
239
|
similarity_model=similarity_model,
|
|
229
240
|
sovereignty_models=sovereignty_models,
|
|
230
241
|
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from agentx.evaluations.models import EvaluationSettings
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from agentx.evaluations.client import EvaluationsClient
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EvaluationSettingsBuilder:
|
|
15
|
+
"""Fluent builder for creating a standalone, reusable grading config (no
|
|
16
|
+
dataset/questions attached)."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
client: "EvaluationsClient",
|
|
21
|
+
name: str,
|
|
22
|
+
description: Optional[str] = None,
|
|
23
|
+
number_of_requests: int = 1,
|
|
24
|
+
acceptance_criteria: Optional[str] = None,
|
|
25
|
+
rejection_criteria: Optional[str] = None,
|
|
26
|
+
evaluation_criteria: Optional[str] = None,
|
|
27
|
+
vector_similarity: bool = False,
|
|
28
|
+
jaccard_similarity: bool = False,
|
|
29
|
+
bleu_score: bool = False,
|
|
30
|
+
rouge_score: bool = False,
|
|
31
|
+
similarity_model: Optional[str] = None,
|
|
32
|
+
sovereignty_models: Optional[List[str]] = None,
|
|
33
|
+
):
|
|
34
|
+
self._client = client
|
|
35
|
+
self._payload: Dict[str, Any] = {
|
|
36
|
+
"name": name,
|
|
37
|
+
"description": description,
|
|
38
|
+
"numberOfRequests": number_of_requests,
|
|
39
|
+
"acceptanceCriteria": acceptance_criteria,
|
|
40
|
+
"rejectionCriteria": rejection_criteria,
|
|
41
|
+
"evaluationCriteria": evaluation_criteria,
|
|
42
|
+
}
|
|
43
|
+
# Opt-in similarity metrics, mirrors DatasetBuilder's config kwargs.
|
|
44
|
+
if vector_similarity:
|
|
45
|
+
vs: Dict[str, Any] = {"enabled": True}
|
|
46
|
+
if similarity_model:
|
|
47
|
+
vs["model"] = similarity_model
|
|
48
|
+
self._payload["vectorSimilarity"] = vs
|
|
49
|
+
if jaccard_similarity:
|
|
50
|
+
self._payload["jaccardSimilarity"] = {"enabled": True}
|
|
51
|
+
if bleu_score:
|
|
52
|
+
self._payload["bleuScore"] = {"enabled": True}
|
|
53
|
+
if rouge_score:
|
|
54
|
+
self._payload["rougeScore"] = {"enabled": True}
|
|
55
|
+
# Sovereignty & Portability — the models to compare when this config runs
|
|
56
|
+
# (use client.evaluations.list_models() to discover valid ids).
|
|
57
|
+
if sovereignty_models:
|
|
58
|
+
self._payload["sovereigntyIndex"] = {
|
|
59
|
+
"enabled": True,
|
|
60
|
+
"models": list(sovereignty_models),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def publish(self) -> EvaluationSettings:
|
|
64
|
+
logger.info("Publishing evaluation settings '%s'", self._payload["name"])
|
|
65
|
+
return self._client.create_evaluation_settings(self._payload)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EvaluationSettingsClient:
|
|
69
|
+
"""Thin wrapper surfaced as client.evaluations.settings."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, client: "EvaluationsClient"):
|
|
72
|
+
self._client = client
|
|
73
|
+
|
|
74
|
+
def builder(
|
|
75
|
+
self,
|
|
76
|
+
name: str,
|
|
77
|
+
description: Optional[str] = None,
|
|
78
|
+
number_of_requests: int = 1,
|
|
79
|
+
acceptance_criteria: Optional[str] = None,
|
|
80
|
+
rejection_criteria: Optional[str] = None,
|
|
81
|
+
evaluation_criteria: Optional[str] = None,
|
|
82
|
+
vector_similarity: bool = False,
|
|
83
|
+
jaccard_similarity: bool = False,
|
|
84
|
+
bleu_score: bool = False,
|
|
85
|
+
rouge_score: bool = False,
|
|
86
|
+
similarity_model: Optional[str] = None,
|
|
87
|
+
sovereignty_models: Optional[List[str]] = None,
|
|
88
|
+
) -> EvaluationSettingsBuilder:
|
|
89
|
+
return EvaluationSettingsBuilder(
|
|
90
|
+
self._client,
|
|
91
|
+
name=name,
|
|
92
|
+
description=description,
|
|
93
|
+
number_of_requests=number_of_requests,
|
|
94
|
+
acceptance_criteria=acceptance_criteria,
|
|
95
|
+
rejection_criteria=rejection_criteria,
|
|
96
|
+
evaluation_criteria=evaluation_criteria,
|
|
97
|
+
vector_similarity=vector_similarity,
|
|
98
|
+
jaccard_similarity=jaccard_similarity,
|
|
99
|
+
bleu_score=bleu_score,
|
|
100
|
+
rouge_score=rouge_score,
|
|
101
|
+
similarity_model=similarity_model,
|
|
102
|
+
sovereignty_models=sovereignty_models,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def get(self, evaluation_settings_id: str) -> EvaluationSettings:
|
|
106
|
+
return self._client.get_evaluation_settings(evaluation_settings_id)
|
|
107
|
+
|
|
108
|
+
def list(self) -> List[EvaluationSettings]:
|
|
109
|
+
return self._client.list_evaluation_settings()
|
|
@@ -76,6 +76,38 @@ class Dataset(BaseModel):
|
|
|
76
76
|
extra = "ignore"
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
class EvaluationSettings(BaseModel):
|
|
80
|
+
"""A standalone, reusable grading config — no dataset/questions attached.
|
|
81
|
+
Created via ``client.evaluations.settings.builder(...).publish()`` and run
|
|
82
|
+
against any dataset by passing its id as ``evaluation_settings_id`` to
|
|
83
|
+
``client.evaluations.run(...)``."""
|
|
84
|
+
|
|
85
|
+
id: str = Field(alias="_id")
|
|
86
|
+
name: str
|
|
87
|
+
description: Optional[str] = None
|
|
88
|
+
number_of_requests: int = Field(default=1, alias="numberOfRequests")
|
|
89
|
+
acceptance_criteria: Optional[str] = Field(default=None, alias="acceptanceCriteria")
|
|
90
|
+
rejection_criteria: Optional[str] = Field(default=None, alias="rejectionCriteria")
|
|
91
|
+
evaluation_criteria: Optional[str] = Field(default=None, alias="evaluationCriteria")
|
|
92
|
+
status: str = "published"
|
|
93
|
+
# Sovereignty & Portability — models selected to compare when this config runs.
|
|
94
|
+
# Hoisted from the nested ``sovereigntyIndex`` object when enabled.
|
|
95
|
+
sovereignty_models: List[str] = Field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
@model_validator(mode="before")
|
|
98
|
+
@classmethod
|
|
99
|
+
def _extract_sovereignty_models(cls, data: Any) -> Any:
|
|
100
|
+
if isinstance(data, dict):
|
|
101
|
+
sov = data.get("sovereigntyIndex") or data.get("sovereignty_index") or {}
|
|
102
|
+
if isinstance(sov, dict) and sov.get("enabled") and sov.get("models"):
|
|
103
|
+
data = {**data, "sovereignty_models": list(sov.get("models") or [])}
|
|
104
|
+
return data
|
|
105
|
+
|
|
106
|
+
class Config:
|
|
107
|
+
populate_by_name = True
|
|
108
|
+
extra = "ignore"
|
|
109
|
+
|
|
110
|
+
|
|
79
111
|
# ---------------------------------------------------------------------------
|
|
80
112
|
# Evaluation subject
|
|
81
113
|
# ---------------------------------------------------------------------------
|
|
@@ -154,6 +186,22 @@ class ServerLimits(BaseModel):
|
|
|
154
186
|
extra = "ignore"
|
|
155
187
|
|
|
156
188
|
|
|
189
|
+
class LiveStatistics(BaseModel):
|
|
190
|
+
"""Rating aggregate computed server-side from submitted results — available
|
|
191
|
+
as soon as results are scored, independent of the `.analyze()` step (which
|
|
192
|
+
only adds the LLM-driven qualitative report). Returned on the run resource
|
|
193
|
+
(``GET /runs/:runId``) as ``liveStatistics``."""
|
|
194
|
+
|
|
195
|
+
average_rating: Optional[float] = Field(default=None, alias="averageRating")
|
|
196
|
+
min_rating: Optional[float] = Field(default=None, alias="minRating")
|
|
197
|
+
max_rating: Optional[float] = Field(default=None, alias="maxRating")
|
|
198
|
+
rated_count: int = Field(default=0, alias="ratedCount")
|
|
199
|
+
|
|
200
|
+
class Config:
|
|
201
|
+
populate_by_name = True
|
|
202
|
+
extra = "ignore"
|
|
203
|
+
|
|
204
|
+
|
|
157
205
|
class EvaluationRun(BaseModel):
|
|
158
206
|
run_id: str = Field(alias="runId")
|
|
159
207
|
dataset_id: str = Field(alias="datasetId")
|
|
@@ -226,6 +274,10 @@ class EvaluationResult(BaseModel):
|
|
|
226
274
|
timings: Optional[ResultTimings] = None
|
|
227
275
|
metadata: Optional[Dict[str, Any]] = None
|
|
228
276
|
idempotency_key: Optional[str] = Field(default=None, alias="idempotencyKey")
|
|
277
|
+
# Links this result to a PromptTrace ingested via client.tracer.trace(..., sync=True) — lets
|
|
278
|
+
# the dashboard's "Message Trace Details -> Execution Timeline" render the full execution
|
|
279
|
+
# trace for this case, not just the lightweight observable_trace events above.
|
|
280
|
+
trace_id: Optional[str] = Field(default=None, alias="traceId")
|
|
229
281
|
|
|
230
282
|
class Config:
|
|
231
283
|
populate_by_name = True
|
|
@@ -262,6 +314,8 @@ class BatchAppendResponse(BaseModel):
|
|
|
262
314
|
scored_results: List[ScoredResult] = Field(
|
|
263
315
|
default_factory=list, alias="scoredResults"
|
|
264
316
|
)
|
|
317
|
+
# Server-computed rating aggregate, refreshed after this batch — see LiveStatistics.
|
|
318
|
+
live_statistics: Optional[LiveStatistics] = Field(default=None, alias="liveStatistics")
|
|
265
319
|
|
|
266
320
|
class Config:
|
|
267
321
|
populate_by_name = True
|
|
@@ -280,6 +334,8 @@ class ReportStatistics(BaseModel):
|
|
|
280
334
|
max_rating: float = Field(default=0.0, alias="maxRating")
|
|
281
335
|
cosine_similarity: Optional[float] = Field(default=None, alias="cosineSimilarity")
|
|
282
336
|
jaccard_similarity: Optional[float] = Field(default=None, alias="jaccardSimilarity")
|
|
337
|
+
bleu_score: Optional[float] = Field(default=None, alias="bleuScore")
|
|
338
|
+
rouge_score: Optional[float] = Field(default=None, alias="rougeScore")
|
|
283
339
|
|
|
284
340
|
class Config:
|
|
285
341
|
populate_by_name = True
|
|
@@ -356,6 +412,12 @@ class SovereigntyModelMetrics(BaseModel):
|
|
|
356
412
|
average_jaccard_similarity: Optional[float] = Field(
|
|
357
413
|
default=None, alias="averageJaccardSimilarity"
|
|
358
414
|
)
|
|
415
|
+
average_bleu_score: Optional[float] = Field(
|
|
416
|
+
default=None, alias="averageBleuScore"
|
|
417
|
+
)
|
|
418
|
+
average_rouge_score: Optional[float] = Field(
|
|
419
|
+
default=None, alias="averageRougeScore"
|
|
420
|
+
)
|
|
359
421
|
average_latency_ms: Optional[float] = Field(default=None, alias="averageLatencyMs")
|
|
360
422
|
total_input_tokens: Optional[int] = Field(default=None, alias="totalInputTokens")
|
|
361
423
|
total_output_tokens: Optional[int] = Field(default=None, alias="totalOutputTokens")
|
|
@@ -421,11 +483,15 @@ class Report(BaseModel):
|
|
|
421
483
|
return data
|
|
422
484
|
stats = data.get("statistics")
|
|
423
485
|
stats = dict(stats) if isinstance(stats, dict) else {}
|
|
424
|
-
for top_key, nested_key in (
|
|
425
|
-
("cosineSimilarity", "cosineSimilarity"),
|
|
426
|
-
("cosine_similarity", "cosine_similarity"),
|
|
427
|
-
("jaccardSimilarity", "jaccardSimilarity"),
|
|
428
|
-
("jaccard_similarity", "jaccard_similarity"),
|
|
486
|
+
for top_key, nested_key, marker in (
|
|
487
|
+
("cosineSimilarity", "cosineSimilarity", "cosine"),
|
|
488
|
+
("cosine_similarity", "cosine_similarity", "cosine"),
|
|
489
|
+
("jaccardSimilarity", "jaccardSimilarity", "jaccard"),
|
|
490
|
+
("jaccard_similarity", "jaccard_similarity", "jaccard"),
|
|
491
|
+
("bleuScore", "bleuScore", "bleu"),
|
|
492
|
+
("bleu_score", "bleu_score", "bleu"),
|
|
493
|
+
("rougeScore", "rougeScore", "rouge"),
|
|
494
|
+
("rouge_score", "rouge_score", "rouge"),
|
|
429
495
|
):
|
|
430
496
|
top_val = data.get(top_key)
|
|
431
497
|
if top_val is None:
|
|
@@ -433,15 +499,19 @@ class Report(BaseModel):
|
|
|
433
499
|
if (
|
|
434
500
|
stats.get("cosineSimilarity") is None
|
|
435
501
|
and stats.get("cosine_similarity") is None
|
|
436
|
-
and "cosine"
|
|
502
|
+
and marker == "cosine"
|
|
437
503
|
):
|
|
438
504
|
stats[nested_key] = top_val
|
|
439
505
|
if (
|
|
440
506
|
stats.get("jaccardSimilarity") is None
|
|
441
507
|
and stats.get("jaccard_similarity") is None
|
|
442
|
-
and "jaccard"
|
|
508
|
+
and marker == "jaccard"
|
|
443
509
|
):
|
|
444
510
|
stats[nested_key] = top_val
|
|
511
|
+
if stats.get("bleuScore") is None and stats.get("bleu_score") is None and marker == "bleu":
|
|
512
|
+
stats[nested_key] = top_val
|
|
513
|
+
if stats.get("rougeScore") is None and stats.get("rouge_score") is None and marker == "rouge":
|
|
514
|
+
stats[nested_key] = top_val
|
|
445
515
|
if stats:
|
|
446
516
|
data["statistics"] = stats
|
|
447
517
|
return data
|
|
@@ -462,6 +532,18 @@ class Report(BaseModel):
|
|
|
462
532
|
self.statistics.jaccard_similarity if self.statistics is not None else None
|
|
463
533
|
)
|
|
464
534
|
|
|
535
|
+
@property
|
|
536
|
+
def bleu_score(self) -> Optional[float]:
|
|
537
|
+
"""Average BLEU score across scored results, or ``None`` if the metric
|
|
538
|
+
was not enabled for the dataset or no result has a value yet."""
|
|
539
|
+
return self.statistics.bleu_score if self.statistics is not None else None
|
|
540
|
+
|
|
541
|
+
@property
|
|
542
|
+
def rouge_score(self) -> Optional[float]:
|
|
543
|
+
"""Average ROUGE-L (F1) score across scored results, or ``None`` if the
|
|
544
|
+
metric was not enabled for the dataset or no result has a value yet."""
|
|
545
|
+
return self.statistics.rouge_score if self.statistics is not None else None
|
|
546
|
+
|
|
465
547
|
@property
|
|
466
548
|
def average_rating(self) -> Optional[float]:
|
|
467
549
|
"""Convenience accessor matching cosine_similarity / jaccard_similarity."""
|
|
@@ -67,6 +67,20 @@ def print_report(report: Report) -> None:
|
|
|
67
67
|
f" {dim('Jaccard :')} {jac_color(f'{jac * 100:.1f}%')} {dim('(token-set overlap)')}"
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
+
bleu = report.bleu_score
|
|
71
|
+
if bleu is not None:
|
|
72
|
+
bleu_color = green if bleu >= 0.6 else (yellow if bleu >= 0.3 else red)
|
|
73
|
+
print(
|
|
74
|
+
f" {dim('BLEU :')} {bleu_color(f'{bleu * 100:.1f}%')} {dim('(n-gram precision)')}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
rouge = report.rouge_score
|
|
78
|
+
if rouge is not None:
|
|
79
|
+
rouge_color = green if rouge >= 0.6 else (yellow if rouge >= 0.3 else red)
|
|
80
|
+
print(
|
|
81
|
+
f" {dim('ROUGE-L :')} {rouge_color(f'{rouge * 100:.1f}%')} {dim('(longest common subsequence)')}"
|
|
82
|
+
)
|
|
83
|
+
|
|
70
84
|
if report.consistency_score is not None:
|
|
71
85
|
cs = report.consistency_score
|
|
72
86
|
cs_color = green if cs >= 7 else (yellow if cs >= 4 else red)
|
|
@@ -34,6 +34,7 @@ def normalize_result(
|
|
|
34
34
|
|
|
35
35
|
output: Optional[dict] = None
|
|
36
36
|
trace = None
|
|
37
|
+
trace_id: Optional[str] = None
|
|
37
38
|
metadata: Optional[dict] = None
|
|
38
39
|
error: Optional[ResultError] = None
|
|
39
40
|
input_tokens: Optional[int] = None
|
|
@@ -50,6 +51,8 @@ def normalize_result(
|
|
|
50
51
|
output = {"text": str(text)} if text else None
|
|
51
52
|
|
|
52
53
|
trace = build_trace(raw.get("trace") or raw.get("observable_trace"))
|
|
54
|
+
trace_id_raw = raw.get("trace_id") or raw.get("traceId")
|
|
55
|
+
trace_id = str(trace_id_raw) if trace_id_raw else None
|
|
53
56
|
meta_raw = raw.get("metadata")
|
|
54
57
|
if isinstance(meta_raw, dict):
|
|
55
58
|
metadata = redact_dict(meta_raw)
|
|
@@ -102,6 +105,7 @@ def normalize_result(
|
|
|
102
105
|
else None
|
|
103
106
|
),
|
|
104
107
|
metadata=metadata,
|
|
108
|
+
traceId=trace_id,
|
|
105
109
|
)
|
|
106
110
|
|
|
107
111
|
|
|
@@ -13,7 +13,9 @@ from agentx.evaluations.models import (
|
|
|
13
13
|
EvaluationCase,
|
|
14
14
|
EvaluationResult,
|
|
15
15
|
EvaluationRun,
|
|
16
|
+
EvaluationSettings,
|
|
16
17
|
EvaluationSubject,
|
|
18
|
+
LiveStatistics,
|
|
17
19
|
ModelInfo,
|
|
18
20
|
Report,
|
|
19
21
|
)
|
|
@@ -54,14 +56,24 @@ class EvaluationRunContext:
|
|
|
54
56
|
dataset: Dataset,
|
|
55
57
|
run: EvaluationRun,
|
|
56
58
|
subject: EvaluationSubject,
|
|
59
|
+
evaluation_settings: Optional[EvaluationSettings] = None,
|
|
57
60
|
):
|
|
58
61
|
self._client = client
|
|
59
62
|
self._dataset = dataset
|
|
60
63
|
self._run = run
|
|
61
64
|
self._subject = subject
|
|
65
|
+
# When set, this run was started with an independently chosen grading
|
|
66
|
+
# config (evaluation_settings_id) — its fields take precedence over the
|
|
67
|
+
# dataset's own for anything execution-time reads (see _build_cases).
|
|
68
|
+
self._evaluation_settings = evaluation_settings
|
|
62
69
|
self._results: List[EvaluationResult] = []
|
|
63
70
|
self._submitted_keys: Set[str] = set()
|
|
64
71
|
self._report: Optional[Report] = None
|
|
72
|
+
# Server-computed rating aggregate (Evaluate.liveStatistics) — refreshed
|
|
73
|
+
# from the response of each append_results()/finalize_run() call. The
|
|
74
|
+
# API is the single source of truth for this number (same value the
|
|
75
|
+
# dashboard UI reads), so the SDK does not average results itself.
|
|
76
|
+
self._live_stats: Optional[LiveStatistics] = None
|
|
65
77
|
|
|
66
78
|
# ------------------------------------------------------------------
|
|
67
79
|
# Step 1: execute
|
|
@@ -70,7 +82,7 @@ class EvaluationRunContext:
|
|
|
70
82
|
def execute(self, adapter: AdapterLike) -> "EvaluationRunContext":
|
|
71
83
|
"""Run all cases locally and submit batches to AgentX."""
|
|
72
84
|
normalized = _wrap_adapter(adapter)
|
|
73
|
-
cases = _build_cases(self._dataset)
|
|
85
|
+
cases = _build_cases(self._dataset, self._evaluation_settings)
|
|
74
86
|
max_batch = self._run.limits.max_batch_size
|
|
75
87
|
|
|
76
88
|
# Banner
|
|
@@ -80,7 +92,11 @@ class EvaluationRunContext:
|
|
|
80
92
|
runtime = self._subject.runtime or "local"
|
|
81
93
|
display = self._subject.display_name or ""
|
|
82
94
|
n_q = len(self._dataset.questions)
|
|
83
|
-
n_r =
|
|
95
|
+
n_r = (
|
|
96
|
+
self._evaluation_settings.number_of_requests
|
|
97
|
+
if self._evaluation_settings
|
|
98
|
+
else self._dataset.number_of_requests
|
|
99
|
+
)
|
|
84
100
|
|
|
85
101
|
print(cyan(sep))
|
|
86
102
|
print(f" {bold('AgentX Evaluation')} {dim('—')} {name}")
|
|
@@ -137,6 +153,8 @@ class EvaluationRunContext:
|
|
|
137
153
|
with Spinner(f"Scoring — AI is rating {n} result{'s' if n != 1 else ''}"):
|
|
138
154
|
try:
|
|
139
155
|
resp = self._client.append_results(self._run.run_id, batch_id, batch)
|
|
156
|
+
if resp.live_statistics is not None:
|
|
157
|
+
self._live_stats = resp.live_statistics
|
|
140
158
|
print(
|
|
141
159
|
f" {green('✓')} Scored {resp.accepted} result{'s' if resp.accepted != 1 else ''}"
|
|
142
160
|
)
|
|
@@ -168,7 +186,9 @@ class EvaluationRunContext:
|
|
|
168
186
|
print()
|
|
169
187
|
with Spinner("Finalizing — submitting results"):
|
|
170
188
|
try:
|
|
171
|
-
self._client.finalize_run(self._run.run_id)
|
|
189
|
+
data = self._client.finalize_run(self._run.run_id)
|
|
190
|
+
if isinstance(data, dict) and data.get("liveStatistics") is not None:
|
|
191
|
+
self._live_stats = LiveStatistics(**data["liveStatistics"])
|
|
172
192
|
print(f" {green('✓')} Finalized")
|
|
173
193
|
logger.info("Run %s finalized", self._run.run_id)
|
|
174
194
|
except Exception as exc:
|
|
@@ -176,6 +196,34 @@ class EvaluationRunContext:
|
|
|
176
196
|
logger.error("Finalize failed: %s", exc)
|
|
177
197
|
return self
|
|
178
198
|
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
# Live rating stats — server-computed (Evaluate.liveStatistics), refreshed
|
|
201
|
+
# from the response of each append_results()/finalize_run() call. Available
|
|
202
|
+
# as soon as .execute() submits batches, no .analyze() required. The SDK
|
|
203
|
+
# does not average ratings itself — this mirrors exactly what the dashboard
|
|
204
|
+
# UI reads, computed once in the API.
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def rated_count(self) -> int:
|
|
209
|
+
"""Number of submitted results that have received a rating so far."""
|
|
210
|
+
return self._live_stats.rated_count if self._live_stats else 0
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def average_rating(self) -> Optional[float]:
|
|
214
|
+
"""Live average rating across all results scored so far. Populated as
|
|
215
|
+
soon as .execute() submits batches — unlike Report.average_rating,
|
|
216
|
+
does not require .analyze()."""
|
|
217
|
+
return self._live_stats.average_rating if self._live_stats else None
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def min_rating(self) -> Optional[float]:
|
|
221
|
+
return self._live_stats.min_rating if self._live_stats else None
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def max_rating(self) -> Optional[float]:
|
|
225
|
+
return self._live_stats.max_rating if self._live_stats else None
|
|
226
|
+
|
|
179
227
|
# ------------------------------------------------------------------
|
|
180
228
|
# Step 3: analyze + report
|
|
181
229
|
# ------------------------------------------------------------------
|
|
@@ -223,6 +271,7 @@ class EvaluationsRunner:
|
|
|
223
271
|
def __init__(self, client: EvaluationsClient):
|
|
224
272
|
self._client = client
|
|
225
273
|
self.datasets = client.datasets
|
|
274
|
+
self.settings = client.settings
|
|
226
275
|
|
|
227
276
|
def list_models(self, provider: Optional[str] = None) -> List[ModelInfo]:
|
|
228
277
|
"""List the LLM models AgentX supports — the same set selectable for
|
|
@@ -235,20 +284,36 @@ class EvaluationsRunner:
|
|
|
235
284
|
self,
|
|
236
285
|
dataset_id: str,
|
|
237
286
|
subject: Union[Dict[str, Any], EvaluationSubject],
|
|
287
|
+
evaluation_settings_id: Optional[str] = None,
|
|
238
288
|
) -> EvaluationRunContext:
|
|
289
|
+
"""Start a run of ``dataset_id`` against ``subject``. Pass
|
|
290
|
+
``evaluation_settings_id`` to grade against a standalone, reusable
|
|
291
|
+
config (created via ``client.evaluations.settings.builder(...)``)
|
|
292
|
+
instead of the dataset's own default config."""
|
|
239
293
|
if isinstance(subject, dict):
|
|
240
294
|
subject = EvaluationSubject(**subject)
|
|
241
295
|
|
|
242
296
|
dataset = self._client.get_dataset(dataset_id)
|
|
243
|
-
|
|
297
|
+
evaluation_settings = (
|
|
298
|
+
self._client.get_evaluation_settings(evaluation_settings_id)
|
|
299
|
+
if evaluation_settings_id
|
|
300
|
+
else None
|
|
301
|
+
)
|
|
302
|
+
run = self._client.init_run(
|
|
303
|
+
dataset_id, subject, evaluation_settings_id=evaluation_settings_id
|
|
304
|
+
)
|
|
244
305
|
logger.info(
|
|
245
306
|
"Started evaluation run %s on dataset %s (%d case(s), %d repetition(s))",
|
|
246
307
|
run.run_id,
|
|
247
308
|
dataset_id,
|
|
248
309
|
len(dataset.questions),
|
|
249
|
-
|
|
310
|
+
evaluation_settings.number_of_requests
|
|
311
|
+
if evaluation_settings
|
|
312
|
+
else dataset.number_of_requests,
|
|
313
|
+
)
|
|
314
|
+
return EvaluationRunContext(
|
|
315
|
+
self._client, dataset, run, subject, evaluation_settings=evaluation_settings
|
|
250
316
|
)
|
|
251
|
-
return EvaluationRunContext(self._client, dataset, run, subject)
|
|
252
317
|
|
|
253
318
|
|
|
254
319
|
# ---------------------------------------------------------------------------
|
|
@@ -268,14 +333,26 @@ def _wrap_adapter(adapter: AdapterLike) -> Callable[[EvaluationCase], Evaluation
|
|
|
268
333
|
)
|
|
269
334
|
|
|
270
335
|
|
|
271
|
-
def _build_cases(
|
|
336
|
+
def _build_cases(
|
|
337
|
+
dataset: Dataset, evaluation_settings: Optional[EvaluationSettings] = None
|
|
338
|
+
) -> List[EvaluationCase]:
|
|
272
339
|
cases: List[EvaluationCase] = []
|
|
273
|
-
|
|
274
|
-
#
|
|
340
|
+
# When an independent evaluation_settings was chosen (evaluation_settings_id
|
|
341
|
+
# passed to .run()), its numberOfRequests/sovereigntyIndex take precedence
|
|
342
|
+
# over the dataset's own — that's the whole point of decoupling them. With
|
|
343
|
+
# no evaluation_settings, this reproduces today's exact behavior.
|
|
344
|
+
n_runs = max(
|
|
345
|
+
(evaluation_settings.number_of_requests if evaluation_settings else dataset.number_of_requests),
|
|
346
|
+
1,
|
|
347
|
+
)
|
|
348
|
+
# Sovereignty & Portability: when the config selects comparison models, run
|
|
275
349
|
# every question/run once per model in this single run so the report groups
|
|
276
350
|
# results into a per-model portability matrix (mirrors the native route).
|
|
277
351
|
# ``[None]`` keeps legacy single-model behavior (case.model stays unset).
|
|
278
|
-
|
|
352
|
+
sovereignty_models = (
|
|
353
|
+
evaluation_settings.sovereignty_models if evaluation_settings else dataset.sovereignty_models
|
|
354
|
+
)
|
|
355
|
+
models: List[Optional[str]] = list(sovereignty_models) or [None]
|
|
279
356
|
for q_idx, question in enumerate(dataset.questions):
|
|
280
357
|
mq = question.main_question
|
|
281
358
|
for run_num in range(1, n_runs + 1):
|
|
File without changes
|
|
@@ -82,7 +82,7 @@ class Conversation(BaseModel):
|
|
|
82
82
|
f"Failed to retrieve agent details: {response.status_code} - {response.reason}"
|
|
83
83
|
)
|
|
84
84
|
|
|
85
|
-
def chat(self, message: str, context: int = None):
|
|
85
|
+
def chat(self, message: str, context: Optional[int] = None):
|
|
86
86
|
url = f"{api_base()}/access/conversations/{self.id}/message"
|
|
87
87
|
response = requests.post(
|
|
88
88
|
url,
|
|
@@ -91,7 +91,7 @@ class Conversation(BaseModel):
|
|
|
91
91
|
)
|
|
92
92
|
return response.json()
|
|
93
93
|
|
|
94
|
-
def chat_stream(self, message: str, context: int = None) -> Iterator[ChatResponse]:
|
|
94
|
+
def chat_stream(self, message: str, context: Optional[int] = None) -> Iterator[ChatResponse]:
|
|
95
95
|
url = f"{api_base()}/access/conversations/{self.id}/jsonmessagesse"
|
|
96
96
|
response = requests.post(
|
|
97
97
|
url, headers=get_headers(), json={"message": message, "context": context}
|
|
@@ -90,6 +90,30 @@ class IngestClient:
|
|
|
90
90
|
"""Block until all queued traces have been sent (or timeout elapses)."""
|
|
91
91
|
self._queue.join()
|
|
92
92
|
|
|
93
|
+
def send_trace_sync(self, payload: Dict[str, Any]) -> Optional[str]:
|
|
94
|
+
"""
|
|
95
|
+
Send a trace payload synchronously and return the ingested trace's id, or ``None`` on
|
|
96
|
+
failure. Used by ``Tracer.trace(..., sync=True)`` when the caller needs the trace_id back
|
|
97
|
+
immediately (e.g. to attach it to an evaluation result) — unlike ``enqueue()``, this blocks
|
|
98
|
+
and does not retry, trading the tracer's usual fire-and-forget guarantee for a same-call
|
|
99
|
+
result. Never raises; a failed send just means no trace_id (never blocks the caller's eval
|
|
100
|
+
run over a tracing hiccup).
|
|
101
|
+
"""
|
|
102
|
+
if self._workspace_id:
|
|
103
|
+
payload = {**payload, "workspaceId": self._workspace_id}
|
|
104
|
+
try:
|
|
105
|
+
resp = self._session.post(self._endpoint, json=payload, timeout=10)
|
|
106
|
+
except requests.RequestException as exc:
|
|
107
|
+
logger.debug("agentx ingest sync send error: %s", exc)
|
|
108
|
+
return None
|
|
109
|
+
if not resp.ok:
|
|
110
|
+
logger.debug("agentx ingest sync HTTP %d: %s", resp.status_code, resp.text[:200])
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
return resp.json().get("trace_id")
|
|
114
|
+
except Exception:
|
|
115
|
+
return None
|
|
116
|
+
|
|
93
117
|
def evaluate_trace(
|
|
94
118
|
self,
|
|
95
119
|
trace_id: str,
|
|
@@ -77,6 +77,7 @@ class _TraceSpan:
|
|
|
77
77
|
framework: Optional[str] = None,
|
|
78
78
|
model: Optional[str] = None,
|
|
79
79
|
session_id: Optional[str] = None,
|
|
80
|
+
sync: bool = False,
|
|
80
81
|
) -> None:
|
|
81
82
|
self._tracer = tracer
|
|
82
83
|
self.name = name
|
|
@@ -86,6 +87,10 @@ class _TraceSpan:
|
|
|
86
87
|
self._framework = framework
|
|
87
88
|
self._model = model
|
|
88
89
|
self._session_id = session_id
|
|
90
|
+
# When True, __exit__ sends synchronously (blocking) instead of enqueueing, so trace_id
|
|
91
|
+
# is populated by the time the `with` block exits — see Tracer.trace()'s sync param.
|
|
92
|
+
self._sync = sync
|
|
93
|
+
self._trace_id: Optional[str] = None
|
|
89
94
|
|
|
90
95
|
# Fields the caller can set while inside the context manager
|
|
91
96
|
self.output: Any = None
|
|
@@ -123,9 +128,32 @@ class _TraceSpan:
|
|
|
123
128
|
if exc_val is not None and self._error is None:
|
|
124
129
|
self._error = str(exc_val)
|
|
125
130
|
|
|
131
|
+
# Auto-instrumented integrations (patched Anthropic client, AgentXCallbackHandler, ...)
|
|
132
|
+
# populate _execution_steps via _record_llm_call while this span is active. Wrapping a raw
|
|
133
|
+
# API call with no such integration (e.g. a bare `openai` call) never populates it — without
|
|
134
|
+
# this fallback the Execution Timeline would be empty despite the span having real
|
|
135
|
+
# input/output, since nothing else here describes what the wrapped code actually did.
|
|
136
|
+
# Mirrors what the @tracer.trace(...) decorator form has always synthesized for exactly
|
|
137
|
+
# this case (see _wrap_sync/_wrap_async below).
|
|
138
|
+
execution_steps = self._execution_steps or (
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
"name": "LLM Call 1",
|
|
142
|
+
"duration_ms": latency_ms or 0,
|
|
143
|
+
"start_time": self._start,
|
|
144
|
+
"end_time": time.time(),
|
|
145
|
+
"model": self._model or self._captured_model,
|
|
146
|
+
"input": _safe_serialize(self.input) if self.input is not None else None,
|
|
147
|
+
"output": _safe_serialize(self.output) if self.output is not None else None,
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
if self.input is not None or self.output is not None
|
|
151
|
+
else []
|
|
152
|
+
)
|
|
153
|
+
|
|
126
154
|
perf = build_performance_summary(
|
|
127
155
|
total_duration_ms=latency_ms or 0,
|
|
128
|
-
execution_steps=
|
|
156
|
+
execution_steps=execution_steps,
|
|
129
157
|
tool_call_steps=[
|
|
130
158
|
{
|
|
131
159
|
"name": tc.get("name"),
|
|
@@ -141,7 +169,8 @@ class _TraceSpan:
|
|
|
141
169
|
has_errors=self._error is not None,
|
|
142
170
|
)
|
|
143
171
|
|
|
144
|
-
self._tracer._send(
|
|
172
|
+
self._trace_id = self._tracer._send(
|
|
173
|
+
sync=self._sync,
|
|
145
174
|
name=self.name,
|
|
146
175
|
input=_safe_serialize(self.input) if self.input is not None else None,
|
|
147
176
|
output=_safe_serialize(self.output) if self.output is not None else None,
|
|
@@ -158,6 +187,13 @@ class _TraceSpan:
|
|
|
158
187
|
)
|
|
159
188
|
return False # never suppress exceptions
|
|
160
189
|
|
|
190
|
+
@property
|
|
191
|
+
def trace_id(self) -> Optional[str]:
|
|
192
|
+
"""The ingested trace's id — only populated once this span has exited AND it was opened
|
|
193
|
+
with ``tracer.trace(..., sync=True)``. ``None`` for the default (async/enqueued) mode,
|
|
194
|
+
since there's nothing to wait on for a same-call id."""
|
|
195
|
+
return self._trace_id
|
|
196
|
+
|
|
161
197
|
# ------------------------------------------------------------------
|
|
162
198
|
# Called by auto-instrumented integrations (e.g. patch_anthropic_client)
|
|
163
199
|
# when this span is the tracer's active span, instead of them sending
|
|
@@ -578,10 +614,21 @@ class Tracer:
|
|
|
578
614
|
framework: Optional[str] = None,
|
|
579
615
|
model: Optional[str] = None,
|
|
580
616
|
session_id: Optional[str] = None,
|
|
617
|
+
sync: bool = False,
|
|
581
618
|
) -> _TraceSpan:
|
|
582
619
|
"""
|
|
583
620
|
Return a :class:`_TraceSpan` that works as both a decorator and a
|
|
584
621
|
context manager.
|
|
622
|
+
|
|
623
|
+
By default the trace is queued and sent on a background thread — fire-and-forget, never
|
|
624
|
+
blocks the caller, but there's no way to learn the resulting trace_id. Pass ``sync=True``
|
|
625
|
+
to send it synchronously instead (blocks until ingested) so ``span.trace_id`` is populated
|
|
626
|
+
once the ``with`` block exits — e.g. to attach the trace to an evaluation result::
|
|
627
|
+
|
|
628
|
+
with client.tracer.trace("support_agent_call", framework="openai", sync=True) as span:
|
|
629
|
+
resp = call_llm(...)
|
|
630
|
+
span.output = resp
|
|
631
|
+
return {"output": resp, "trace_id": span.trace_id}
|
|
585
632
|
"""
|
|
586
633
|
return _TraceSpan(
|
|
587
634
|
tracer=self,
|
|
@@ -591,6 +638,7 @@ class Tracer:
|
|
|
591
638
|
framework=framework,
|
|
592
639
|
model=model,
|
|
593
640
|
session_id=session_id,
|
|
641
|
+
sync=sync,
|
|
594
642
|
)
|
|
595
643
|
|
|
596
644
|
def flush(self, timeout: float = 5.0) -> None:
|
|
@@ -767,8 +815,8 @@ class Tracer:
|
|
|
767
815
|
as-is — the agent is NOT re-run.
|
|
768
816
|
|
|
769
817
|
Args:
|
|
770
|
-
trace_id: ID
|
|
771
|
-
(available as ``span.
|
|
818
|
+
trace_id: ID of a trace ingested via ``trace(..., sync=True)``
|
|
819
|
+
(available as ``span.trace_id`` once that `with` block exits).
|
|
772
820
|
dataset_id: EvaluationSettings ID to score against.
|
|
773
821
|
question_index: Optional index into the dataset's questions array.
|
|
774
822
|
When supplied, that question's ``expectedResults``
|
|
@@ -784,7 +832,7 @@ class Tracer:
|
|
|
784
832
|
# Internal
|
|
785
833
|
# ------------------------------------------------------------------
|
|
786
834
|
|
|
787
|
-
def _send(self, **kwargs) ->
|
|
835
|
+
def _send(self, sync: bool = False, **kwargs) -> Optional[str]:
|
|
788
836
|
payload = {k: v for k, v in kwargs.items() if v is not None}
|
|
789
837
|
# Remap to snake_case wire format expected by the backend
|
|
790
838
|
wire: Dict[str, Any] = {}
|
|
@@ -844,4 +892,7 @@ class Tracer:
|
|
|
844
892
|
wire["performance_summary"], pending_tool_calls
|
|
845
893
|
)
|
|
846
894
|
|
|
895
|
+
if sync:
|
|
896
|
+
return self._client.send_trace_sync(wire)
|
|
847
897
|
self._client.enqueue(wire)
|
|
898
|
+
return None
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from typing import Optional
|
|
2
3
|
|
|
3
4
|
_DEFAULT_API_BASE = "https://api.agentx.so/api/v1"
|
|
4
5
|
|
|
@@ -15,5 +16,5 @@ def api_base() -> str:
|
|
|
15
16
|
return _DEFAULT_API_BASE
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
def get_headers(api_key: str = None):
|
|
19
|
+
def get_headers(api_key: Optional[str] = None):
|
|
19
20
|
return {"accept": "*/*", "x-api-key": api_key or os.getenv("AGENTX_API_KEY")}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.6.5"
|
|
@@ -4,12 +4,14 @@ setup.py
|
|
|
4
4
|
agentx/__init__.py
|
|
5
5
|
agentx/agentx.py
|
|
6
6
|
agentx/exceptions.py
|
|
7
|
+
agentx/py.typed
|
|
7
8
|
agentx/util.py
|
|
8
9
|
agentx/version.py
|
|
9
10
|
agentx/evaluations/__init__.py
|
|
10
11
|
agentx/evaluations/_term.py
|
|
11
12
|
agentx/evaluations/client.py
|
|
12
13
|
agentx/evaluations/datasets.py
|
|
14
|
+
agentx/evaluations/evaluation_settings.py
|
|
13
15
|
agentx/evaluations/models.py
|
|
14
16
|
agentx/evaluations/redaction.py
|
|
15
17
|
agentx/evaluations/reporting.py
|
|
@@ -39,6 +41,7 @@ agentx/tracing/tracer.py
|
|
|
39
41
|
agentx_python.egg-info/PKG-INFO
|
|
40
42
|
agentx_python.egg-info/SOURCES.txt
|
|
41
43
|
agentx_python.egg-info/dependency_links.txt
|
|
44
|
+
agentx_python.egg-info/not-zip-safe
|
|
42
45
|
agentx_python.egg-info/requires.txt
|
|
43
46
|
agentx_python.egg-info/top_level.txt
|
|
44
47
|
tests/test_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -21,6 +21,11 @@ setup(
|
|
|
21
21
|
name="agentx-python",
|
|
22
22
|
version=get_version(),
|
|
23
23
|
packages=find_packages(),
|
|
24
|
+
# PEP 561: ships type hints for consumers' type checkers (mypy/pyright). Without this marker,
|
|
25
|
+
# every symbol imported from `agentx` is treated as untyped `Any`, silently disabling type
|
|
26
|
+
# checking for anything that touches the SDK.
|
|
27
|
+
package_data={"agentx": ["py.typed"]},
|
|
28
|
+
zip_safe=False,
|
|
24
29
|
install_requires=[
|
|
25
30
|
"urllib3>=1.26.11",
|
|
26
31
|
"certifi",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "0.6.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|