agentx-python 0.4.8__py3-none-any.whl → 0.4.9__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
@@ -3,37 +3,50 @@ import requests
3
3
  import os
4
4
  import logging
5
5
 
6
- from agentx.util import get_headers
6
+ from agentx.util import get_headers, api_base
7
7
  from agentx.resources.agent import Agent
8
8
  from agentx.resources.workforce import Workforce
9
9
 
10
10
 
11
11
  class AgentX:
12
12
 
13
- def __init__(self, api_key: str = None):
13
+ def __init__(self, api_key: str = None, base_url: str = None):
14
14
  self.api_key = api_key or os.getenv("AGENTX_API_KEY")
15
15
  if self.api_key and not os.getenv("AGENTX_API_KEY"):
16
16
  os.environ["AGENTX_API_KEY"] = self.api_key
17
17
 
18
+ # base_url overrides AGENTX_API_BASE_URL env var (and the SDK default)
19
+ self.base_url = base_url or os.getenv("AGENTX_API_BASE_URL")
20
+ if self.base_url:
21
+ os.environ["AGENTX_API_BASE_URL"] = self.base_url
22
+
23
+ from agentx.evaluations.client import EvaluationsClient
24
+ from agentx.evaluations.runner import EvaluationsRunner
25
+ from agentx.version import VERSION
26
+ _eval_client = EvaluationsClient(
27
+ api_key=self.api_key,
28
+ sdk_version=VERSION,
29
+ base_url=self.base_url,
30
+ )
31
+ self.evaluations = EvaluationsRunner(_eval_client)
32
+
33
+ @classmethod
34
+ def from_env(cls) -> "AgentX":
35
+ """Create an AgentX client using AGENTX_API_KEY (and optionally AGENTX_API_BASE_URL) from the environment."""
36
+ return cls()
37
+
18
38
  def get_agent(self, id: str) -> Agent:
19
- url = f"https://api.agentx.so/api/v1/access/agents/{id}"
39
+ url = f"{api_base()}/access/agents/{id}"
20
40
  # Make a GET request to the AgentX API
21
41
  response = requests.get(url, headers=get_headers())
22
42
  # Check if response was successful
23
43
  if response.status_code == 200:
24
- agent_res = response.json()
25
- return Agent(
26
- id=agent_res.get("_id"),
27
- name=agent_res.get("name"),
28
- avatar=agent_res.get("avatar"),
29
- createdAt=agent_res.get("createdAt"),
30
- updatedAt=agent_res.get("updatedAt"),
31
- )
44
+ return Agent(**response.json())
32
45
  else:
33
46
  raise Exception(f"Failed to retrieve agent: {response.reason}")
34
47
 
35
48
  def list_agents(self) -> List[Agent]:
36
- url = "https://api.agentx.so/api/v1/access/agents"
49
+ url = f"{api_base()}/access/agents"
37
50
  # Make a GET request to the AgentX API
38
51
  response = requests.get(url, headers=get_headers())
39
52
  # Check if response was successful
@@ -45,7 +58,7 @@ class AgentX:
45
58
  @staticmethod
46
59
  def list_workforces() -> List["Workforce"]:
47
60
  """List all workforces/teams."""
48
- url = "https://api.agentx.so/api/v1/access/teams"
61
+ url = f"{api_base()}/access/teams"
49
62
  response = requests.get(url, headers=get_headers())
50
63
  if response.status_code == 200:
51
64
  return [Workforce(**workforce) for workforce in response.json()]
@@ -56,7 +69,7 @@ class AgentX:
56
69
 
57
70
  def get_profile(self):
58
71
  """Get the current user's profile information."""
59
- url = "https://api.agentx.so/api/v1/access/getProfile"
72
+ url = f"{api_base()}/access/getProfile"
60
73
  response = requests.get(url, headers=get_headers())
61
74
  if response.status_code == 200:
62
75
  return response.json()
@@ -0,0 +1,3 @@
1
+ from agentx.evaluations.client import EvaluationsClient
2
+
3
+ __all__ = ["EvaluationsClient"]
@@ -0,0 +1,69 @@
1
+ """Minimal ANSI terminal helpers — no external dependencies."""
2
+ from __future__ import annotations
3
+
4
+ import itertools
5
+ import sys
6
+ import threading
7
+ import time
8
+
9
+ _IS_TTY = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
10
+
11
+
12
+ def _c(code: str) -> str:
13
+ return f"\033[{code}m" if _IS_TTY else ""
14
+
15
+
16
+ RESET = _c("0")
17
+ BOLD = _c("1")
18
+ DIM = _c("2")
19
+ GREEN = _c("32")
20
+ YELLOW = _c("33")
21
+ RED = _c("31")
22
+ CYAN = _c("36")
23
+ MAGENTA = _c("35")
24
+
25
+
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}"
33
+
34
+
35
+ class Spinner:
36
+ """Inline spinner that overwrites the current line on a TTY."""
37
+
38
+ _FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
39
+
40
+ def __init__(self, message: str):
41
+ self._message = message
42
+ self._stop = threading.Event()
43
+ self._thread: threading.Thread | None = None
44
+
45
+ def __enter__(self) -> "Spinner":
46
+ if not _IS_TTY:
47
+ print(f" {self._message}...", flush=True)
48
+ return self
49
+ self._stop.clear()
50
+ self._thread = threading.Thread(target=self._spin, daemon=True)
51
+ self._thread.start()
52
+ return self
53
+
54
+ def __exit__(self, *_) -> None:
55
+ if not _IS_TTY:
56
+ return
57
+ self._stop.set()
58
+ if self._thread:
59
+ self._thread.join()
60
+ sys.stdout.write(f"\r{' ' * (len(self._message) + 12)}\r")
61
+ sys.stdout.flush()
62
+
63
+ def _spin(self) -> None:
64
+ for frame in itertools.cycle(self._FRAMES):
65
+ if self._stop.is_set():
66
+ break
67
+ sys.stdout.write(f"\r {CYAN}{frame}{RESET} {self._message} {DIM}(this may take ~60s+){RESET}")
68
+ sys.stdout.flush()
69
+ time.sleep(0.08)
@@ -0,0 +1,5 @@
1
+ from agentx.evaluations.adapters.raw import RawCallableAdapter
2
+ from agentx.evaluations.adapters.precomputed import PrecomputedAdapter
3
+ from agentx.evaluations.adapters.http_endpoint import HttpEndpointAdapter
4
+
5
+ __all__ = ["RawCallableAdapter", "PrecomputedAdapter", "HttpEndpointAdapter"]
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, Dict, Optional
5
+
6
+ import requests
7
+
8
+ from agentx.evaluations.models import EvaluationCase, EvaluationResult
9
+ from agentx.evaluations.results import normalize_error, normalize_result
10
+
11
+
12
+ class HttpEndpointAdapter:
13
+ """
14
+ Calls a user-hosted HTTP endpoint for each evaluation case.
15
+ The SDK (running locally) makes the request — the AgentX API never
16
+ touches the customer's endpoint.
17
+
18
+ The endpoint receives a POST with::
19
+
20
+ {"query": "...", "case_id": "...", "metadata": {...}}
21
+
22
+ It should respond with a JSON body containing at least ``output``
23
+ (or ``text``) and optionally ``trace`` and ``metadata``.
24
+
25
+ Usage::
26
+
27
+ adapter = HttpEndpointAdapter(
28
+ url="http://localhost:8080/eval",
29
+ headers={"Authorization": "Bearer my-token"},
30
+ timeout=30,
31
+ )
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ url: str,
37
+ headers: Optional[Dict[str, str]] = None,
38
+ timeout: int = 30,
39
+ method: str = "POST",
40
+ ):
41
+ self._url = url
42
+ self._headers = headers or {}
43
+ self._timeout = timeout
44
+ self._method = method.upper()
45
+
46
+ def run(self, case: EvaluationCase) -> EvaluationResult:
47
+ payload = {
48
+ "query": case.query,
49
+ "case_id": case.case_id,
50
+ "question_index": case.question_index,
51
+ "run_number": case.run_number,
52
+ }
53
+ start = time.monotonic()
54
+ try:
55
+ resp = requests.request(
56
+ self._method,
57
+ self._url,
58
+ json=payload,
59
+ headers=self._headers,
60
+ timeout=self._timeout,
61
+ )
62
+ resp.raise_for_status()
63
+ elapsed = int((time.monotonic() - start) * 1000)
64
+ raw = resp.json()
65
+ except Exception as exc:
66
+ elapsed = int((time.monotonic() - start) * 1000)
67
+ return normalize_error(case, exc, latency_ms=elapsed)
68
+
69
+ return normalize_result(case, raw, latency_ms=elapsed)
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Union
4
+
5
+ from agentx.evaluations.models import EvaluationCase, EvaluationResult
6
+ from agentx.evaluations.results import normalize_result
7
+
8
+
9
+ class PrecomputedAdapter:
10
+ """
11
+ Adapter for pre-computed outputs — useful when you already have agent
12
+ responses and just want AgentX to score them.
13
+
14
+ Accepts a list or dict keyed by case_id::
15
+
16
+ outputs = {
17
+ "case-0": "You can reset your password from account settings.",
18
+ "case-1": {"output": "Contact support@example.com", "metadata": {...}},
19
+ }
20
+ adapter = PrecomputedAdapter(outputs)
21
+ """
22
+
23
+ def __init__(self, outputs: Union[List[Any], Dict[str, Any]]):
24
+ if isinstance(outputs, list):
25
+ self._lookup: Dict[str, Any] = {str(i): v for i, v in enumerate(outputs)}
26
+ else:
27
+ self._lookup = {str(k): v for k, v in outputs.items()}
28
+
29
+ def run(self, case: EvaluationCase) -> EvaluationResult:
30
+ raw = self._lookup.get(case.case_id) or self._lookup.get(str(case.question_index))
31
+ if raw is None:
32
+ raw = ""
33
+ return normalize_result(case, raw, latency_ms=0)
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+ from agentx.evaluations.models import EvaluationCase, EvaluationResult
6
+ from agentx.evaluations.results import normalize_error, normalize_result
7
+
8
+
9
+ class RawCallableAdapter:
10
+ """
11
+ Wraps any Python callable that accepts an EvaluationCase and returns
12
+ str | dict | EvaluationResult.
13
+
14
+ Usage::
15
+
16
+ def my_agent(case: EvaluationCase) -> str:
17
+ return my_llm.invoke(case.query)
18
+
19
+ adapter = RawCallableAdapter(my_agent)
20
+ result = adapter.run(case)
21
+ """
22
+
23
+ def __init__(self, fn: Callable[[EvaluationCase], Any]):
24
+ self._fn = fn
25
+
26
+ def run(self, case: EvaluationCase, latency_ms: int | None = None) -> EvaluationResult:
27
+ import time
28
+ start = time.monotonic()
29
+ try:
30
+ raw = self._fn(case)
31
+ except Exception as exc:
32
+ elapsed = int((time.monotonic() - start) * 1000)
33
+ return normalize_error(case, exc, latency_ms=elapsed)
34
+ elapsed = int((time.monotonic() - start) * 1000)
35
+ return normalize_result(case, raw, latency_ms=elapsed)
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import time
6
+ import uuid
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ import requests
10
+
11
+ from agentx.evaluations.models import (
12
+ BatchAppendResponse,
13
+ Dataset,
14
+ EvaluationResult,
15
+ EvaluationRun,
16
+ EvaluationSubject,
17
+ Report,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ from agentx.util import _DEFAULT_API_BASE as _UTIL_API_BASE
23
+
24
+ _DEFAULT_BASE_URL = f"{_UTIL_API_BASE}/custom-agent-evaluations"
25
+ SDK_NAME = "agentx-python"
26
+
27
+ _RETRYABLE_STATUS = {429, 500, 502, 503, 504}
28
+ _MAX_RETRIES = 3
29
+ _RETRY_BACKOFF = [1.0, 2.0, 4.0]
30
+
31
+
32
+ class AgentXEvaluationsError(Exception):
33
+ pass
34
+
35
+
36
+ class AgentXAuthError(AgentXEvaluationsError):
37
+ pass
38
+
39
+
40
+ class AgentXValidationError(AgentXEvaluationsError):
41
+ pass
42
+
43
+
44
+ class EvaluationsClient:
45
+ def __init__(self, api_key: str, sdk_version: str = "unknown", base_url: str = None):
46
+ if not api_key:
47
+ raise AgentXAuthError("AGENTX_API_KEY is required")
48
+ self._api_key = api_key
49
+ self._sdk_version = sdk_version
50
+ # Priority: constructor arg > env var > SDK default
51
+ # 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("/")
53
+ if not _api_base.endswith("/custom-agent-evaluations"):
54
+ _api_base = f"{_api_base}/custom-agent-evaluations"
55
+ self._base_url = _api_base
56
+ 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
+ })
63
+ # Expose dataset builder factory
64
+ from agentx.evaluations.datasets import DatasetClient
65
+ self.datasets = DatasetClient(self)
66
+
67
+ # ------------------------------------------------------------------
68
+ # Low-level HTTP
69
+ # ------------------------------------------------------------------
70
+
71
+ def _request(self, method: str, path: str, timeout: int = 30, **kwargs) -> Any:
72
+ url = f"{self._base_url}{path}"
73
+ last_exc: Optional[Exception] = None
74
+ for attempt, wait in enumerate([0.0] + _RETRY_BACKOFF):
75
+ if wait:
76
+ time.sleep(wait)
77
+ try:
78
+ resp = self._session.request(method, url, timeout=timeout, **kwargs)
79
+ except requests.RequestException as e:
80
+ last_exc = e
81
+ logger.debug("Request error (attempt %d): %s", attempt + 1, e)
82
+ continue
83
+
84
+ if resp.status_code == 401:
85
+ raise AgentXAuthError("Invalid or missing API key")
86
+ if resp.status_code == 422:
87
+ raise AgentXValidationError(resp.text)
88
+ 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)
90
+ last_exc = AgentXEvaluationsError(f"HTTP {resp.status_code}")
91
+ continue
92
+ if not resp.ok:
93
+ raise AgentXEvaluationsError(f"HTTP {resp.status_code}: {resp.text}")
94
+ try:
95
+ return resp.json()
96
+ except Exception:
97
+ return resp.text
98
+ raise AgentXEvaluationsError(f"Request failed after retries: {last_exc}")
99
+
100
+ # ------------------------------------------------------------------
101
+ # Dataset endpoints
102
+ # ------------------------------------------------------------------
103
+
104
+ def create_dataset(self, payload: dict) -> Dataset:
105
+ data = self._request("POST", "/datasets", json=payload)
106
+ return Dataset(**data)
107
+
108
+ def list_datasets(self) -> List[Dataset]:
109
+ data = self._request("GET", "/datasets")
110
+ return [Dataset(**d) for d in (data if isinstance(data, list) else data.get("datasets", []))]
111
+
112
+ def get_dataset(self, dataset_id: str) -> Dataset:
113
+ data = self._request("GET", f"/datasets/{dataset_id}")
114
+ return Dataset(**data)
115
+
116
+ # ------------------------------------------------------------------
117
+ # Run endpoints
118
+ # ------------------------------------------------------------------
119
+
120
+ def init_run(
121
+ self,
122
+ dataset_id: str,
123
+ subject: EvaluationSubject,
124
+ python_version: Optional[str] = None,
125
+ ) -> EvaluationRun:
126
+ from agentx.version import VERSION
127
+ payload = {
128
+ "datasetId": dataset_id,
129
+ "evaluationSubject": subject.model_dump(by_alias=True, exclude_none=True),
130
+ "runSource": "sdk",
131
+ "sdk": {
132
+ "name": SDK_NAME,
133
+ "version": VERSION,
134
+ "runnerVersion": "1",
135
+ "pythonVersion": python_version or _python_version(),
136
+ },
137
+ }
138
+ data = self._request("POST", "/runs", json=payload)
139
+ return EvaluationRun(**data)
140
+
141
+ def append_results(self, run_id: str, batch_id: str, results: List[EvaluationResult]) -> BatchAppendResponse:
142
+ payload = {
143
+ "batchId": batch_id,
144
+ "results": [_result_to_payload(r) for r in results],
145
+ }
146
+ data = self._request("POST", f"/runs/{run_id}/results", json=payload)
147
+ return BatchAppendResponse(**data)
148
+
149
+ def finalize_run(self, run_id: str) -> Dict[str, Any]:
150
+ return self._request("POST", f"/runs/{run_id}/finalize", json={"status": "completed"})
151
+
152
+ def analyze_run(self, run_id: str) -> Dict[str, Any]:
153
+ return self._request("POST", f"/runs/{run_id}/analyze", json={}, timeout=300)
154
+
155
+ def get_run(self, run_id: str) -> Dict[str, Any]:
156
+ return self._request("GET", f"/runs/{run_id}")
157
+
158
+ def get_report(self, run_id: str) -> Report:
159
+ data = self._request("GET", f"/runs/{run_id}/report")
160
+ return Report(**data)
161
+
162
+ def get_missing_results(self, run_id: str) -> List[Dict[str, Any]]:
163
+ data = self._request("GET", f"/runs/{run_id}/missing-results")
164
+ return data if isinstance(data, list) else data.get("missing", [])
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Helpers
169
+ # ---------------------------------------------------------------------------
170
+
171
+ def _result_to_payload(r: EvaluationResult) -> dict:
172
+ d = r.model_dump(by_alias=True, exclude_none=True)
173
+ # Rename snake_case fields the model may have stored locally
174
+ d["caseId"] = d.pop("case_id", d.get("caseId"))
175
+ d["questionIndex"] = d.pop("question_index", d.get("questionIndex"))
176
+ d["runNumber"] = d.pop("run_number", d.get("runNumber"))
177
+ d["idempotencyKey"] = d.pop("idempotency_key", d.get("idempotencyKey"))
178
+ return {k: v for k, v in d.items() if v is not None}
179
+
180
+
181
+ def _python_version() -> str:
182
+ import sys
183
+ return f"{sys.version_info.major}.{sys.version_info.minor}"