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 CHANGED
@@ -23,6 +23,7 @@ class AgentX:
23
23
  from agentx.evaluations.client import EvaluationsClient
24
24
  from agentx.evaluations.runner import EvaluationsRunner
25
25
  from agentx.version import VERSION
26
+
26
27
  _eval_client = EvaluationsClient(
27
28
  api_key=self.api_key,
28
29
  sdk_version=VERSION,
@@ -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 = _c("0")
17
- BOLD = _c("1")
18
- DIM = _c("2")
19
- GREEN = _c("32")
17
+ RESET = _c("0")
18
+ BOLD = _c("1")
19
+ DIM = _c("2")
20
+ GREEN = _c("32")
20
21
  YELLOW = _c("33")
21
- RED = _c("31")
22
- CYAN = _c("36")
22
+ RED = _c("31")
23
+ CYAN = _c("36")
23
24
  MAGENTA = _c("35")
24
25
 
25
26
 
26
- def green(s: str) -> str: return f"{GREEN}{s}{RESET}"
27
- def yellow(s: str) -> str: return f"{YELLOW}{s}{RESET}"
28
- def red(s: str) -> str: return f"{RED}{s}{RESET}"
29
- def cyan(s: str) -> str: return f"{CYAN}{s}{RESET}"
30
- def bold(s: str) -> str: return f"{BOLD}{s}{RESET}"
31
- def dim(s: str) -> str: return f"{DIM}{s}{RESET}"
32
- def magenta(s: str) -> str: return f"{MAGENTA}{s}{RESET}"
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(f"\r {CYAN}{frame}{RESET} {self._message} {DIM}(this may take ~60s+){RESET}")
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(str(case.question_index))
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(self, case: EvaluationCase, latency_ms: int | None = None) -> EvaluationResult:
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)
@@ -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__(self, api_key: str, sdk_version: str = "unknown", base_url: str = None):
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 = (base_url or os.getenv("AGENTX_API_BASE_URL", _UTIL_API_BASE)).rstrip("/")
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
- "x-api-key": self._api_key,
59
- "Content-Type": "application/json",
60
- "User-Agent": f"{SDK_NAME}/{self._sdk_version}",
61
- "accept": "*/*",
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("Retryable status %d (attempt %d)", resp.status_code, attempt + 1)
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 [Dataset(**d) for d in (data if isinstance(data, list) else data.get("datasets", []))]
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(self, run_id: str, batch_id: str, results: List[EvaluationResult]) -> BatchAppendResponse:
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("POST", f"/runs/{run_id}/finalize", json={"status": "completed"})
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}"
@@ -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
- "main_question": main,
62
- "follow_up_questions": follow_up_questions or [],
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("Publishing dataset '%s' with %d case(s)", self._payload["name"], len(self._payload["questions"]))
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(row.get("expected_capabilities", "")),
113
- expected_knowledge_base=_split_semi(row.get("expected_knowledge_base", "")),
114
- expected_delegations=_split_semi(row.get("expected_delegations", "")),
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(row.get("expected_knowledge_base", "")),
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