agentx-python 0.4.9__py3-none-any.whl → 0.4.11__py3-none-any.whl
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/agentx.py +1 -0
- agentx/evaluations/_term.py +36 -14
- agentx/evaluations/adapters/precomputed.py +3 -1
- agentx/evaluations/adapters/raw.py +4 -1
- agentx/evaluations/client.py +45 -12
- agentx/evaluations/datasets.py +52 -9
- agentx/evaluations/models.py +188 -14
- agentx/evaluations/redaction.py +17 -5
- agentx/evaluations/reporting.py +34 -8
- agentx/evaluations/results.py +21 -5
- agentx/evaluations/runner.py +56 -15
- agentx/resources/workforce.py +3 -1
- agentx/version.py +1 -1
- agentx_python-0.4.11.dist-info/METADATA +198 -0
- agentx_python-0.4.11.dist-info/RECORD +27 -0
- agentx_python-0.4.9.dist-info/METADATA +0 -141
- agentx_python-0.4.9.dist-info/RECORD +0 -27
- {agentx_python-0.4.9.dist-info → agentx_python-0.4.11.dist-info}/WHEEL +0 -0
- {agentx_python-0.4.9.dist-info → agentx_python-0.4.11.dist-info}/licenses/LICENSE +0 -0
- {agentx_python-0.4.9.dist-info → agentx_python-0.4.11.dist-info}/top_level.txt +0 -0
agentx/agentx.py
CHANGED
agentx/evaluations/_term.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Minimal ANSI terminal helpers — no external dependencies."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import itertools
|
|
@@ -13,23 +14,42 @@ def _c(code: str) -> str:
|
|
|
13
14
|
return f"\033[{code}m" if _IS_TTY else ""
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
RESET
|
|
17
|
-
BOLD
|
|
18
|
-
DIM
|
|
19
|
-
GREEN
|
|
17
|
+
RESET = _c("0")
|
|
18
|
+
BOLD = _c("1")
|
|
19
|
+
DIM = _c("2")
|
|
20
|
+
GREEN = _c("32")
|
|
20
21
|
YELLOW = _c("33")
|
|
21
|
-
RED
|
|
22
|
-
CYAN
|
|
22
|
+
RED = _c("31")
|
|
23
|
+
CYAN = _c("36")
|
|
23
24
|
MAGENTA = _c("35")
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
def green(s: str) -> str:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
def green(s: str) -> str:
|
|
28
|
+
return f"{GREEN}{s}{RESET}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def yellow(s: str) -> str:
|
|
32
|
+
return f"{YELLOW}{s}{RESET}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def red(s: str) -> str:
|
|
36
|
+
return f"{RED}{s}{RESET}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cyan(s: str) -> str:
|
|
40
|
+
return f"{CYAN}{s}{RESET}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def bold(s: str) -> str:
|
|
44
|
+
return f"{BOLD}{s}{RESET}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def dim(s: str) -> str:
|
|
48
|
+
return f"{DIM}{s}{RESET}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def magenta(s: str) -> str:
|
|
52
|
+
return f"{MAGENTA}{s}{RESET}"
|
|
33
53
|
|
|
34
54
|
|
|
35
55
|
class Spinner:
|
|
@@ -64,6 +84,8 @@ class Spinner:
|
|
|
64
84
|
for frame in itertools.cycle(self._FRAMES):
|
|
65
85
|
if self._stop.is_set():
|
|
66
86
|
break
|
|
67
|
-
sys.stdout.write(
|
|
87
|
+
sys.stdout.write(
|
|
88
|
+
f"\r {CYAN}{frame}{RESET} {self._message} {DIM}(this may take ~60s+){RESET}"
|
|
89
|
+
)
|
|
68
90
|
sys.stdout.flush()
|
|
69
91
|
time.sleep(0.08)
|
|
@@ -27,7 +27,9 @@ class PrecomputedAdapter:
|
|
|
27
27
|
self._lookup = {str(k): v for k, v in outputs.items()}
|
|
28
28
|
|
|
29
29
|
def run(self, case: EvaluationCase) -> EvaluationResult:
|
|
30
|
-
raw = self._lookup.get(case.case_id) or self._lookup.get(
|
|
30
|
+
raw = self._lookup.get(case.case_id) or self._lookup.get(
|
|
31
|
+
str(case.question_index)
|
|
32
|
+
)
|
|
31
33
|
if raw is None:
|
|
32
34
|
raw = ""
|
|
33
35
|
return normalize_result(case, raw, latency_ms=0)
|
|
@@ -23,8 +23,11 @@ class RawCallableAdapter:
|
|
|
23
23
|
def __init__(self, fn: Callable[[EvaluationCase], Any]):
|
|
24
24
|
self._fn = fn
|
|
25
25
|
|
|
26
|
-
def run(
|
|
26
|
+
def run(
|
|
27
|
+
self, case: EvaluationCase, latency_ms: int | None = None
|
|
28
|
+
) -> EvaluationResult:
|
|
27
29
|
import time
|
|
30
|
+
|
|
28
31
|
start = time.monotonic()
|
|
29
32
|
try:
|
|
30
33
|
raw = self._fn(case)
|
agentx/evaluations/client.py
CHANGED
|
@@ -14,6 +14,7 @@ from agentx.evaluations.models import (
|
|
|
14
14
|
EvaluationResult,
|
|
15
15
|
EvaluationRun,
|
|
16
16
|
EvaluationSubject,
|
|
17
|
+
ModelInfo,
|
|
17
18
|
Report,
|
|
18
19
|
)
|
|
19
20
|
|
|
@@ -42,26 +43,33 @@ class AgentXValidationError(AgentXEvaluationsError):
|
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
class EvaluationsClient:
|
|
45
|
-
def __init__(
|
|
46
|
+
def __init__(
|
|
47
|
+
self, api_key: str, sdk_version: str = "unknown", base_url: str = None
|
|
48
|
+
):
|
|
46
49
|
if not api_key:
|
|
47
50
|
raise AgentXAuthError("AGENTX_API_KEY is required")
|
|
48
51
|
self._api_key = api_key
|
|
49
52
|
self._sdk_version = sdk_version
|
|
50
53
|
# Priority: constructor arg > env var > SDK default
|
|
51
54
|
# Always append /custom-agent-evaluations so users only need to provide /api/v1
|
|
52
|
-
_api_base = (
|
|
55
|
+
_api_base = (
|
|
56
|
+
base_url or os.getenv("AGENTX_API_BASE_URL", _UTIL_API_BASE)
|
|
57
|
+
).rstrip("/")
|
|
53
58
|
if not _api_base.endswith("/custom-agent-evaluations"):
|
|
54
59
|
_api_base = f"{_api_base}/custom-agent-evaluations"
|
|
55
60
|
self._base_url = _api_base
|
|
56
61
|
self._session = requests.Session()
|
|
57
|
-
self._session.headers.update(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
self._session.headers.update(
|
|
63
|
+
{
|
|
64
|
+
"x-api-key": self._api_key,
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"User-Agent": f"{SDK_NAME}/{self._sdk_version}",
|
|
67
|
+
"accept": "*/*",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
63
70
|
# Expose dataset builder factory
|
|
64
71
|
from agentx.evaluations.datasets import DatasetClient
|
|
72
|
+
|
|
65
73
|
self.datasets = DatasetClient(self)
|
|
66
74
|
|
|
67
75
|
# ------------------------------------------------------------------
|
|
@@ -86,7 +94,9 @@ class EvaluationsClient:
|
|
|
86
94
|
if resp.status_code == 422:
|
|
87
95
|
raise AgentXValidationError(resp.text)
|
|
88
96
|
if resp.status_code in _RETRYABLE_STATUS and attempt < _MAX_RETRIES - 1:
|
|
89
|
-
logger.debug(
|
|
97
|
+
logger.debug(
|
|
98
|
+
"Retryable status %d (attempt %d)", resp.status_code, attempt + 1
|
|
99
|
+
)
|
|
90
100
|
last_exc = AgentXEvaluationsError(f"HTTP {resp.status_code}")
|
|
91
101
|
continue
|
|
92
102
|
if not resp.ok:
|
|
@@ -97,6 +107,19 @@ class EvaluationsClient:
|
|
|
97
107
|
return resp.text
|
|
98
108
|
raise AgentXEvaluationsError(f"Request failed after retries: {last_exc}")
|
|
99
109
|
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# Model registry
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def list_models(self, provider: Optional[str] = None) -> List[ModelInfo]:
|
|
115
|
+
"""List the LLM models AgentX supports — the same set selectable for
|
|
116
|
+
the Sovereignty & Portability Index. Pass ``provider`` (e.g. "Google")
|
|
117
|
+
to filter."""
|
|
118
|
+
params = {"provider": provider} if provider else None
|
|
119
|
+
data = self._request("GET", "/models", params=params)
|
|
120
|
+
items = data if isinstance(data, list) else data.get("models", [])
|
|
121
|
+
return [ModelInfo(**m) for m in items]
|
|
122
|
+
|
|
100
123
|
# ------------------------------------------------------------------
|
|
101
124
|
# Dataset endpoints
|
|
102
125
|
# ------------------------------------------------------------------
|
|
@@ -107,7 +130,10 @@ class EvaluationsClient:
|
|
|
107
130
|
|
|
108
131
|
def list_datasets(self) -> List[Dataset]:
|
|
109
132
|
data = self._request("GET", "/datasets")
|
|
110
|
-
return [
|
|
133
|
+
return [
|
|
134
|
+
Dataset(**d)
|
|
135
|
+
for d in (data if isinstance(data, list) else data.get("datasets", []))
|
|
136
|
+
]
|
|
111
137
|
|
|
112
138
|
def get_dataset(self, dataset_id: str) -> Dataset:
|
|
113
139
|
data = self._request("GET", f"/datasets/{dataset_id}")
|
|
@@ -124,6 +150,7 @@ class EvaluationsClient:
|
|
|
124
150
|
python_version: Optional[str] = None,
|
|
125
151
|
) -> EvaluationRun:
|
|
126
152
|
from agentx.version import VERSION
|
|
153
|
+
|
|
127
154
|
payload = {
|
|
128
155
|
"datasetId": dataset_id,
|
|
129
156
|
"evaluationSubject": subject.model_dump(by_alias=True, exclude_none=True),
|
|
@@ -138,7 +165,9 @@ class EvaluationsClient:
|
|
|
138
165
|
data = self._request("POST", "/runs", json=payload)
|
|
139
166
|
return EvaluationRun(**data)
|
|
140
167
|
|
|
141
|
-
def append_results(
|
|
168
|
+
def append_results(
|
|
169
|
+
self, run_id: str, batch_id: str, results: List[EvaluationResult]
|
|
170
|
+
) -> BatchAppendResponse:
|
|
142
171
|
payload = {
|
|
143
172
|
"batchId": batch_id,
|
|
144
173
|
"results": [_result_to_payload(r) for r in results],
|
|
@@ -147,7 +176,9 @@ class EvaluationsClient:
|
|
|
147
176
|
return BatchAppendResponse(**data)
|
|
148
177
|
|
|
149
178
|
def finalize_run(self, run_id: str) -> Dict[str, Any]:
|
|
150
|
-
return self._request(
|
|
179
|
+
return self._request(
|
|
180
|
+
"POST", f"/runs/{run_id}/finalize", json={"status": "completed"}
|
|
181
|
+
)
|
|
151
182
|
|
|
152
183
|
def analyze_run(self, run_id: str) -> Dict[str, Any]:
|
|
153
184
|
return self._request("POST", f"/runs/{run_id}/analyze", json={}, timeout=300)
|
|
@@ -168,6 +199,7 @@ class EvaluationsClient:
|
|
|
168
199
|
# Helpers
|
|
169
200
|
# ---------------------------------------------------------------------------
|
|
170
201
|
|
|
202
|
+
|
|
171
203
|
def _result_to_payload(r: EvaluationResult) -> dict:
|
|
172
204
|
d = r.model_dump(by_alias=True, exclude_none=True)
|
|
173
205
|
# Rename snake_case fields the model may have stored locally
|
|
@@ -180,4 +212,5 @@ def _result_to_payload(r: EvaluationResult) -> dict:
|
|
|
180
212
|
|
|
181
213
|
def _python_version() -> str:
|
|
182
214
|
import sys
|
|
215
|
+
|
|
183
216
|
return f"{sys.version_info.major}.{sys.version_info.minor}"
|
agentx/evaluations/datasets.py
CHANGED
|
@@ -27,6 +27,10 @@ class DatasetBuilder:
|
|
|
27
27
|
acceptance_criteria: Optional[str] = None,
|
|
28
28
|
rejection_criteria: Optional[str] = None,
|
|
29
29
|
evaluation_criteria: Optional[str] = None,
|
|
30
|
+
vector_similarity: bool = False,
|
|
31
|
+
jaccard_similarity: bool = False,
|
|
32
|
+
similarity_model: Optional[str] = None,
|
|
33
|
+
sovereignty_models: Optional[List[str]] = None,
|
|
30
34
|
):
|
|
31
35
|
self._client = client
|
|
32
36
|
self._payload: Dict[str, Any] = {
|
|
@@ -38,6 +42,22 @@ class DatasetBuilder:
|
|
|
38
42
|
"evaluationCriteria": evaluation_criteria,
|
|
39
43
|
"questions": [],
|
|
40
44
|
}
|
|
45
|
+
# Opt-in similarity metrics, surfaced on the report as cosine_similarity /
|
|
46
|
+
# jaccard_similarity (computed against each case's expected_results).
|
|
47
|
+
if vector_similarity:
|
|
48
|
+
vs: Dict[str, Any] = {"enabled": True}
|
|
49
|
+
if similarity_model:
|
|
50
|
+
vs["model"] = similarity_model
|
|
51
|
+
self._payload["vectorSimilarity"] = vs
|
|
52
|
+
if jaccard_similarity:
|
|
53
|
+
self._payload["jaccardSimilarity"] = {"enabled": True}
|
|
54
|
+
# Sovereignty & Portability — the models to compare on this dataset (use
|
|
55
|
+
# client.evaluations.list_models() to discover valid ids).
|
|
56
|
+
if sovereignty_models:
|
|
57
|
+
self._payload["sovereigntyIndex"] = {
|
|
58
|
+
"enabled": True,
|
|
59
|
+
"models": list(sovereignty_models),
|
|
60
|
+
}
|
|
41
61
|
|
|
42
62
|
def add_case(
|
|
43
63
|
self,
|
|
@@ -57,16 +77,22 @@ class DatasetBuilder:
|
|
|
57
77
|
main["expectedKnowledgeBase"] = expected_knowledge_base
|
|
58
78
|
if expected_delegations:
|
|
59
79
|
main["expectedDelegations"] = expected_delegations
|
|
60
|
-
self._payload["questions"].append(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
self._payload["questions"].append(
|
|
81
|
+
{
|
|
82
|
+
"main_question": main,
|
|
83
|
+
"follow_up_questions": follow_up_questions or [],
|
|
84
|
+
}
|
|
85
|
+
)
|
|
64
86
|
return self
|
|
65
87
|
|
|
66
88
|
def publish(self) -> Dataset:
|
|
67
89
|
if not self._payload["questions"]:
|
|
68
90
|
raise ValueError("Dataset must have at least one case before publishing")
|
|
69
|
-
logger.info(
|
|
91
|
+
logger.info(
|
|
92
|
+
"Publishing dataset '%s' with %d case(s)",
|
|
93
|
+
self._payload["name"],
|
|
94
|
+
len(self._payload["questions"]),
|
|
95
|
+
)
|
|
70
96
|
return self._client.create_dataset(self._payload)
|
|
71
97
|
|
|
72
98
|
# ------------------------------------------------------------------
|
|
@@ -109,9 +135,15 @@ class DatasetBuilder:
|
|
|
109
135
|
builder.add_case(
|
|
110
136
|
query=query,
|
|
111
137
|
expected_results=row.get("expected_results", "").strip() or None,
|
|
112
|
-
expected_capabilities=_split_semi(
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
expected_capabilities=_split_semi(
|
|
139
|
+
row.get("expected_capabilities", "")
|
|
140
|
+
),
|
|
141
|
+
expected_knowledge_base=_split_semi(
|
|
142
|
+
row.get("expected_knowledge_base", "")
|
|
143
|
+
),
|
|
144
|
+
expected_delegations=_split_semi(
|
|
145
|
+
row.get("expected_delegations", "")
|
|
146
|
+
),
|
|
115
147
|
)
|
|
116
148
|
|
|
117
149
|
if invalid:
|
|
@@ -150,7 +182,9 @@ class DatasetBuilder:
|
|
|
150
182
|
query=query,
|
|
151
183
|
expected_results=_str_or_none(row.get("expected_results")),
|
|
152
184
|
expected_capabilities=_split_semi(row.get("expected_capabilities", "")),
|
|
153
|
-
expected_knowledge_base=_split_semi(
|
|
185
|
+
expected_knowledge_base=_split_semi(
|
|
186
|
+
row.get("expected_knowledge_base", "")
|
|
187
|
+
),
|
|
154
188
|
expected_delegations=_split_semi(row.get("expected_delegations", "")),
|
|
155
189
|
)
|
|
156
190
|
|
|
@@ -176,6 +210,10 @@ class DatasetClient:
|
|
|
176
210
|
acceptance_criteria: Optional[str] = None,
|
|
177
211
|
rejection_criteria: Optional[str] = None,
|
|
178
212
|
evaluation_criteria: Optional[str] = None,
|
|
213
|
+
vector_similarity: bool = False,
|
|
214
|
+
jaccard_similarity: bool = False,
|
|
215
|
+
similarity_model: Optional[str] = None,
|
|
216
|
+
sovereignty_models: Optional[List[str]] = None,
|
|
179
217
|
) -> DatasetBuilder:
|
|
180
218
|
return DatasetBuilder(
|
|
181
219
|
self._client,
|
|
@@ -185,6 +223,10 @@ class DatasetClient:
|
|
|
185
223
|
acceptance_criteria=acceptance_criteria,
|
|
186
224
|
rejection_criteria=rejection_criteria,
|
|
187
225
|
evaluation_criteria=evaluation_criteria,
|
|
226
|
+
vector_similarity=vector_similarity,
|
|
227
|
+
jaccard_similarity=jaccard_similarity,
|
|
228
|
+
similarity_model=similarity_model,
|
|
229
|
+
sovereignty_models=sovereignty_models,
|
|
188
230
|
)
|
|
189
231
|
|
|
190
232
|
def from_csv(self, path: str, name: str, **kwargs) -> DatasetBuilder:
|
|
@@ -204,6 +246,7 @@ class DatasetClient:
|
|
|
204
246
|
# Helpers
|
|
205
247
|
# ---------------------------------------------------------------------------
|
|
206
248
|
|
|
249
|
+
|
|
207
250
|
def _split_semi(value: Any) -> Optional[List[str]]:
|
|
208
251
|
if not value:
|
|
209
252
|
return None
|