agentx-python 0.4.9__py3-none-any.whl → 0.4.10__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 +31 -12
- agentx/evaluations/datasets.py +24 -9
- agentx/evaluations/models.py +108 -14
- agentx/evaluations/redaction.py +17 -5
- agentx/evaluations/reporting.py +34 -8
- agentx/evaluations/results.py +21 -5
- agentx/evaluations/runner.py +34 -15
- agentx/resources/workforce.py +3 -1
- agentx/version.py +1 -1
- agentx_python-0.4.10.dist-info/METADATA +198 -0
- agentx_python-0.4.10.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.10.dist-info}/WHEEL +0 -0
- {agentx_python-0.4.9.dist-info → agentx_python-0.4.10.dist-info}/licenses/LICENSE +0 -0
- {agentx_python-0.4.9.dist-info → agentx_python-0.4.10.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
|
@@ -42,26 +42,33 @@ class AgentXValidationError(AgentXEvaluationsError):
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class EvaluationsClient:
|
|
45
|
-
def __init__(
|
|
45
|
+
def __init__(
|
|
46
|
+
self, api_key: str, sdk_version: str = "unknown", base_url: str = None
|
|
47
|
+
):
|
|
46
48
|
if not api_key:
|
|
47
49
|
raise AgentXAuthError("AGENTX_API_KEY is required")
|
|
48
50
|
self._api_key = api_key
|
|
49
51
|
self._sdk_version = sdk_version
|
|
50
52
|
# Priority: constructor arg > env var > SDK default
|
|
51
53
|
# Always append /custom-agent-evaluations so users only need to provide /api/v1
|
|
52
|
-
_api_base = (
|
|
54
|
+
_api_base = (
|
|
55
|
+
base_url or os.getenv("AGENTX_API_BASE_URL", _UTIL_API_BASE)
|
|
56
|
+
).rstrip("/")
|
|
53
57
|
if not _api_base.endswith("/custom-agent-evaluations"):
|
|
54
58
|
_api_base = f"{_api_base}/custom-agent-evaluations"
|
|
55
59
|
self._base_url = _api_base
|
|
56
60
|
self._session = requests.Session()
|
|
57
|
-
self._session.headers.update(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
self._session.headers.update(
|
|
62
|
+
{
|
|
63
|
+
"x-api-key": self._api_key,
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"User-Agent": f"{SDK_NAME}/{self._sdk_version}",
|
|
66
|
+
"accept": "*/*",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
63
69
|
# Expose dataset builder factory
|
|
64
70
|
from agentx.evaluations.datasets import DatasetClient
|
|
71
|
+
|
|
65
72
|
self.datasets = DatasetClient(self)
|
|
66
73
|
|
|
67
74
|
# ------------------------------------------------------------------
|
|
@@ -86,7 +93,9 @@ class EvaluationsClient:
|
|
|
86
93
|
if resp.status_code == 422:
|
|
87
94
|
raise AgentXValidationError(resp.text)
|
|
88
95
|
if resp.status_code in _RETRYABLE_STATUS and attempt < _MAX_RETRIES - 1:
|
|
89
|
-
logger.debug(
|
|
96
|
+
logger.debug(
|
|
97
|
+
"Retryable status %d (attempt %d)", resp.status_code, attempt + 1
|
|
98
|
+
)
|
|
90
99
|
last_exc = AgentXEvaluationsError(f"HTTP {resp.status_code}")
|
|
91
100
|
continue
|
|
92
101
|
if not resp.ok:
|
|
@@ -107,7 +116,10 @@ class EvaluationsClient:
|
|
|
107
116
|
|
|
108
117
|
def list_datasets(self) -> List[Dataset]:
|
|
109
118
|
data = self._request("GET", "/datasets")
|
|
110
|
-
return [
|
|
119
|
+
return [
|
|
120
|
+
Dataset(**d)
|
|
121
|
+
for d in (data if isinstance(data, list) else data.get("datasets", []))
|
|
122
|
+
]
|
|
111
123
|
|
|
112
124
|
def get_dataset(self, dataset_id: str) -> Dataset:
|
|
113
125
|
data = self._request("GET", f"/datasets/{dataset_id}")
|
|
@@ -124,6 +136,7 @@ class EvaluationsClient:
|
|
|
124
136
|
python_version: Optional[str] = None,
|
|
125
137
|
) -> EvaluationRun:
|
|
126
138
|
from agentx.version import VERSION
|
|
139
|
+
|
|
127
140
|
payload = {
|
|
128
141
|
"datasetId": dataset_id,
|
|
129
142
|
"evaluationSubject": subject.model_dump(by_alias=True, exclude_none=True),
|
|
@@ -138,7 +151,9 @@ class EvaluationsClient:
|
|
|
138
151
|
data = self._request("POST", "/runs", json=payload)
|
|
139
152
|
return EvaluationRun(**data)
|
|
140
153
|
|
|
141
|
-
def append_results(
|
|
154
|
+
def append_results(
|
|
155
|
+
self, run_id: str, batch_id: str, results: List[EvaluationResult]
|
|
156
|
+
) -> BatchAppendResponse:
|
|
142
157
|
payload = {
|
|
143
158
|
"batchId": batch_id,
|
|
144
159
|
"results": [_result_to_payload(r) for r in results],
|
|
@@ -147,7 +162,9 @@ class EvaluationsClient:
|
|
|
147
162
|
return BatchAppendResponse(**data)
|
|
148
163
|
|
|
149
164
|
def finalize_run(self, run_id: str) -> Dict[str, Any]:
|
|
150
|
-
return self._request(
|
|
165
|
+
return self._request(
|
|
166
|
+
"POST", f"/runs/{run_id}/finalize", json={"status": "completed"}
|
|
167
|
+
)
|
|
151
168
|
|
|
152
169
|
def analyze_run(self, run_id: str) -> Dict[str, Any]:
|
|
153
170
|
return self._request("POST", f"/runs/{run_id}/analyze", json={}, timeout=300)
|
|
@@ -168,6 +185,7 @@ class EvaluationsClient:
|
|
|
168
185
|
# Helpers
|
|
169
186
|
# ---------------------------------------------------------------------------
|
|
170
187
|
|
|
188
|
+
|
|
171
189
|
def _result_to_payload(r: EvaluationResult) -> dict:
|
|
172
190
|
d = r.model_dump(by_alias=True, exclude_none=True)
|
|
173
191
|
# Rename snake_case fields the model may have stored locally
|
|
@@ -180,4 +198,5 @@ def _result_to_payload(r: EvaluationResult) -> dict:
|
|
|
180
198
|
|
|
181
199
|
def _python_version() -> str:
|
|
182
200
|
import sys
|
|
201
|
+
|
|
183
202
|
return f"{sys.version_info.major}.{sys.version_info.minor}"
|
agentx/evaluations/datasets.py
CHANGED
|
@@ -57,16 +57,22 @@ class DatasetBuilder:
|
|
|
57
57
|
main["expectedKnowledgeBase"] = expected_knowledge_base
|
|
58
58
|
if expected_delegations:
|
|
59
59
|
main["expectedDelegations"] = expected_delegations
|
|
60
|
-
self._payload["questions"].append(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
self._payload["questions"].append(
|
|
61
|
+
{
|
|
62
|
+
"main_question": main,
|
|
63
|
+
"follow_up_questions": follow_up_questions or [],
|
|
64
|
+
}
|
|
65
|
+
)
|
|
64
66
|
return self
|
|
65
67
|
|
|
66
68
|
def publish(self) -> Dataset:
|
|
67
69
|
if not self._payload["questions"]:
|
|
68
70
|
raise ValueError("Dataset must have at least one case before publishing")
|
|
69
|
-
logger.info(
|
|
71
|
+
logger.info(
|
|
72
|
+
"Publishing dataset '%s' with %d case(s)",
|
|
73
|
+
self._payload["name"],
|
|
74
|
+
len(self._payload["questions"]),
|
|
75
|
+
)
|
|
70
76
|
return self._client.create_dataset(self._payload)
|
|
71
77
|
|
|
72
78
|
# ------------------------------------------------------------------
|
|
@@ -109,9 +115,15 @@ class DatasetBuilder:
|
|
|
109
115
|
builder.add_case(
|
|
110
116
|
query=query,
|
|
111
117
|
expected_results=row.get("expected_results", "").strip() or None,
|
|
112
|
-
expected_capabilities=_split_semi(
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
expected_capabilities=_split_semi(
|
|
119
|
+
row.get("expected_capabilities", "")
|
|
120
|
+
),
|
|
121
|
+
expected_knowledge_base=_split_semi(
|
|
122
|
+
row.get("expected_knowledge_base", "")
|
|
123
|
+
),
|
|
124
|
+
expected_delegations=_split_semi(
|
|
125
|
+
row.get("expected_delegations", "")
|
|
126
|
+
),
|
|
115
127
|
)
|
|
116
128
|
|
|
117
129
|
if invalid:
|
|
@@ -150,7 +162,9 @@ class DatasetBuilder:
|
|
|
150
162
|
query=query,
|
|
151
163
|
expected_results=_str_or_none(row.get("expected_results")),
|
|
152
164
|
expected_capabilities=_split_semi(row.get("expected_capabilities", "")),
|
|
153
|
-
expected_knowledge_base=_split_semi(
|
|
165
|
+
expected_knowledge_base=_split_semi(
|
|
166
|
+
row.get("expected_knowledge_base", "")
|
|
167
|
+
),
|
|
154
168
|
expected_delegations=_split_semi(row.get("expected_delegations", "")),
|
|
155
169
|
)
|
|
156
170
|
|
|
@@ -204,6 +218,7 @@ class DatasetClient:
|
|
|
204
218
|
# Helpers
|
|
205
219
|
# ---------------------------------------------------------------------------
|
|
206
220
|
|
|
221
|
+
|
|
207
222
|
def _split_semi(value: Any) -> Optional[List[str]]:
|
|
208
223
|
if not value:
|
|
209
224
|
return None
|
agentx/evaluations/models.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, List, Literal, Optional, Union
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
5
|
-
|
|
4
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
5
|
|
|
7
6
|
# ---------------------------------------------------------------------------
|
|
8
7
|
# Observable trace
|
|
9
8
|
# ---------------------------------------------------------------------------
|
|
10
9
|
|
|
10
|
+
|
|
11
11
|
class TraceEvent(BaseModel):
|
|
12
12
|
type: str
|
|
13
13
|
name: Optional[str] = None
|
|
@@ -30,6 +30,7 @@ class ObservableTrace(BaseModel):
|
|
|
30
30
|
# Dataset
|
|
31
31
|
# ---------------------------------------------------------------------------
|
|
32
32
|
|
|
33
|
+
|
|
33
34
|
class TestCase(BaseModel):
|
|
34
35
|
query: str
|
|
35
36
|
expected_results: Optional[str] = None
|
|
@@ -68,8 +69,17 @@ class Dataset(BaseModel):
|
|
|
68
69
|
# ---------------------------------------------------------------------------
|
|
69
70
|
|
|
70
71
|
FrameworkKind = Literal[
|
|
71
|
-
"raw_python",
|
|
72
|
-
"
|
|
72
|
+
"raw_python",
|
|
73
|
+
"openai",
|
|
74
|
+
"anthropic",
|
|
75
|
+
"google",
|
|
76
|
+
"langchain",
|
|
77
|
+
"llamaindex",
|
|
78
|
+
"crewai",
|
|
79
|
+
"autogen",
|
|
80
|
+
"n8n",
|
|
81
|
+
"flowise",
|
|
82
|
+
"other",
|
|
73
83
|
]
|
|
74
84
|
|
|
75
85
|
RuntimeKind = Literal["local", "ci", "customer_hosted", "low_code"]
|
|
@@ -93,10 +103,15 @@ class EvaluationSubject(BaseModel):
|
|
|
93
103
|
# Run
|
|
94
104
|
# ---------------------------------------------------------------------------
|
|
95
105
|
|
|
106
|
+
|
|
96
107
|
class ServerLimits(BaseModel):
|
|
97
108
|
max_batch_size: int = Field(default=10, alias="maxBatchSize")
|
|
98
|
-
max_trace_bytes_per_result: int = Field(
|
|
99
|
-
|
|
109
|
+
max_trace_bytes_per_result: int = Field(
|
|
110
|
+
default=20000, alias="maxTraceBytesPerResult"
|
|
111
|
+
)
|
|
112
|
+
max_metadata_bytes_per_result: int = Field(
|
|
113
|
+
default=4000, alias="maxMetadataBytesPerResult"
|
|
114
|
+
)
|
|
100
115
|
|
|
101
116
|
class Config:
|
|
102
117
|
populate_by_name = True
|
|
@@ -119,6 +134,7 @@ class EvaluationRun(BaseModel):
|
|
|
119
134
|
# Evaluation case (one item from the dataset, passed to the user's callable)
|
|
120
135
|
# ---------------------------------------------------------------------------
|
|
121
136
|
|
|
137
|
+
|
|
122
138
|
class EvaluationCase(BaseModel):
|
|
123
139
|
case_id: str
|
|
124
140
|
question_index: int
|
|
@@ -137,6 +153,7 @@ class EvaluationCase(BaseModel):
|
|
|
137
153
|
# Result produced by the user's callable and normalised by the SDK
|
|
138
154
|
# ---------------------------------------------------------------------------
|
|
139
155
|
|
|
156
|
+
|
|
140
157
|
class ResultError(BaseModel):
|
|
141
158
|
type: str
|
|
142
159
|
message: str
|
|
@@ -162,7 +179,9 @@ class EvaluationResult(BaseModel):
|
|
|
162
179
|
run_number: int
|
|
163
180
|
input: Dict[str, Any]
|
|
164
181
|
output: Optional[Dict[str, Any]] = None
|
|
165
|
-
observable_trace: Optional[ObservableTrace] = Field(
|
|
182
|
+
observable_trace: Optional[ObservableTrace] = Field(
|
|
183
|
+
default=None, alias="observableTrace"
|
|
184
|
+
)
|
|
166
185
|
error: Optional[ResultError] = None
|
|
167
186
|
timings: Optional[ResultTimings] = None
|
|
168
187
|
metadata: Optional[Dict[str, Any]] = None
|
|
@@ -177,6 +196,7 @@ class EvaluationResult(BaseModel):
|
|
|
177
196
|
# Scored result (returned by API after scoring)
|
|
178
197
|
# ---------------------------------------------------------------------------
|
|
179
198
|
|
|
199
|
+
|
|
180
200
|
class ScoredResult(BaseModel):
|
|
181
201
|
idempotency_key: str = Field(alias="idempotencyKey")
|
|
182
202
|
rating: Optional[float] = None
|
|
@@ -191,6 +211,7 @@ class ScoredResult(BaseModel):
|
|
|
191
211
|
# Batch append response
|
|
192
212
|
# ---------------------------------------------------------------------------
|
|
193
213
|
|
|
214
|
+
|
|
194
215
|
class BatchAppendResponse(BaseModel):
|
|
195
216
|
run_id: str = Field(alias="runId")
|
|
196
217
|
batch_id: str = Field(alias="batchId")
|
|
@@ -198,7 +219,9 @@ class BatchAppendResponse(BaseModel):
|
|
|
198
219
|
duplicates: int = 0
|
|
199
220
|
failed_validation: int = Field(default=0, alias="failedValidation")
|
|
200
221
|
status: str = "in_progress"
|
|
201
|
-
scored_results: List[ScoredResult] = Field(
|
|
222
|
+
scored_results: List[ScoredResult] = Field(
|
|
223
|
+
default_factory=list, alias="scoredResults"
|
|
224
|
+
)
|
|
202
225
|
|
|
203
226
|
class Config:
|
|
204
227
|
populate_by_name = True
|
|
@@ -209,11 +232,14 @@ class BatchAppendResponse(BaseModel):
|
|
|
209
232
|
# Analysis / report
|
|
210
233
|
# ---------------------------------------------------------------------------
|
|
211
234
|
|
|
235
|
+
|
|
212
236
|
class ReportStatistics(BaseModel):
|
|
213
237
|
number_of_runs: int = Field(default=0, alias="numberOfRuns")
|
|
214
238
|
average_rating: float = Field(default=0.0, alias="averageRating")
|
|
215
239
|
min_rating: float = Field(default=0.0, alias="minRating")
|
|
216
240
|
max_rating: float = Field(default=0.0, alias="maxRating")
|
|
241
|
+
cosine_similarity: Optional[float] = Field(default=None, alias="cosineSimilarity")
|
|
242
|
+
jaccard_similarity: Optional[float] = Field(default=None, alias="jaccardSimilarity")
|
|
217
243
|
|
|
218
244
|
class Config:
|
|
219
245
|
populate_by_name = True
|
|
@@ -242,7 +268,9 @@ class ReportResponsePatterns(BaseModel):
|
|
|
242
268
|
|
|
243
269
|
class ReportReasoningAnalysis(BaseModel):
|
|
244
270
|
cot_quality: Optional[str] = Field(default=None, alias="cotQuality")
|
|
245
|
-
reasoning_patterns: List[str] = Field(
|
|
271
|
+
reasoning_patterns: List[str] = Field(
|
|
272
|
+
default_factory=list, alias="reasoningPatterns"
|
|
273
|
+
)
|
|
246
274
|
reasoning_gaps: List[str] = Field(default_factory=list, alias="reasoningGaps")
|
|
247
275
|
rating: Optional[str] = None
|
|
248
276
|
|
|
@@ -278,17 +306,83 @@ class Report(BaseModel):
|
|
|
278
306
|
statistics: Optional[ReportStatistics] = None
|
|
279
307
|
summary: Optional[str] = None
|
|
280
308
|
consistency_score: Optional[float] = Field(default=None, alias="consistencyScore")
|
|
281
|
-
instruction_adherence: Optional[ReportInstructionAdherence] = Field(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
309
|
+
instruction_adherence: Optional[ReportInstructionAdherence] = Field(
|
|
310
|
+
default=None, alias="instructionAdherence"
|
|
311
|
+
)
|
|
312
|
+
response_patterns: Optional[ReportResponsePatterns] = Field(
|
|
313
|
+
default=None, alias="responsePatterns"
|
|
314
|
+
)
|
|
315
|
+
reasoning_analysis: Optional[ReportReasoningAnalysis] = Field(
|
|
316
|
+
default=None, alias="reasoningAnalysis"
|
|
317
|
+
)
|
|
318
|
+
tool_usage_analysis: Optional[ReportToolUsageAnalysis] = Field(
|
|
319
|
+
default=None, alias="toolUsageAnalysis"
|
|
320
|
+
)
|
|
285
321
|
strengths: List[str] = Field(default_factory=list)
|
|
286
322
|
weaknesses: List[str] = Field(default_factory=list)
|
|
287
323
|
overall_rating: Optional[str] = Field(default=None, alias="overallRating")
|
|
288
324
|
recommendations: List[ReportRecommendation] = Field(default_factory=list)
|
|
289
|
-
low_scoring_cases: List[Dict[str, Any]] = Field(
|
|
325
|
+
low_scoring_cases: List[Dict[str, Any]] = Field(
|
|
326
|
+
default_factory=list, alias="lowScoringCases"
|
|
327
|
+
)
|
|
290
328
|
dashboard_url: Optional[str] = Field(default=None, alias="dashboardUrl")
|
|
291
329
|
|
|
292
330
|
class Config:
|
|
293
331
|
populate_by_name = True
|
|
294
332
|
extra = "ignore"
|
|
333
|
+
|
|
334
|
+
@model_validator(mode="before")
|
|
335
|
+
@classmethod
|
|
336
|
+
def _hoist_similarity_into_statistics(cls, data: Any) -> Any:
|
|
337
|
+
"""Backend may send similarity metrics either at the top level (e.g.
|
|
338
|
+
``cosineSimilarity``) or nested under ``statistics``. Normalize so the
|
|
339
|
+
nested form is always populated when either is present."""
|
|
340
|
+
if not isinstance(data, dict):
|
|
341
|
+
return data
|
|
342
|
+
stats = data.get("statistics")
|
|
343
|
+
stats = dict(stats) if isinstance(stats, dict) else {}
|
|
344
|
+
for top_key, nested_key in (
|
|
345
|
+
("cosineSimilarity", "cosineSimilarity"),
|
|
346
|
+
("cosine_similarity", "cosine_similarity"),
|
|
347
|
+
("jaccardSimilarity", "jaccardSimilarity"),
|
|
348
|
+
("jaccard_similarity", "jaccard_similarity"),
|
|
349
|
+
):
|
|
350
|
+
top_val = data.get(top_key)
|
|
351
|
+
if top_val is None:
|
|
352
|
+
continue
|
|
353
|
+
if (
|
|
354
|
+
stats.get("cosineSimilarity") is None
|
|
355
|
+
and stats.get("cosine_similarity") is None
|
|
356
|
+
and "cosine" in nested_key.lower()
|
|
357
|
+
):
|
|
358
|
+
stats[nested_key] = top_val
|
|
359
|
+
if (
|
|
360
|
+
stats.get("jaccardSimilarity") is None
|
|
361
|
+
and stats.get("jaccard_similarity") is None
|
|
362
|
+
and "jaccard" in nested_key.lower()
|
|
363
|
+
):
|
|
364
|
+
stats[nested_key] = top_val
|
|
365
|
+
if stats:
|
|
366
|
+
data["statistics"] = stats
|
|
367
|
+
return data
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def cosine_similarity(self) -> Optional[float]:
|
|
371
|
+
"""Average cosine similarity across scored results, or ``None`` if the
|
|
372
|
+
metric was not enabled for the dataset or no result has a value yet."""
|
|
373
|
+
return (
|
|
374
|
+
self.statistics.cosine_similarity if self.statistics is not None else None
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def jaccard_similarity(self) -> Optional[float]:
|
|
379
|
+
"""Average Jaccard similarity across scored results, or ``None`` if the
|
|
380
|
+
metric was not enabled for the dataset or no result has a value yet."""
|
|
381
|
+
return (
|
|
382
|
+
self.statistics.jaccard_similarity if self.statistics is not None else None
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def average_rating(self) -> Optional[float]:
|
|
387
|
+
"""Convenience accessor matching cosine_similarity / jaccard_similarity."""
|
|
388
|
+
return self.statistics.average_rating if self.statistics is not None else None
|
agentx/evaluations/redaction.py
CHANGED
|
@@ -5,10 +5,10 @@ from typing import Any, Dict
|
|
|
5
5
|
|
|
6
6
|
# Patterns that look like secrets
|
|
7
7
|
_SECRET_PATTERNS = [
|
|
8
|
-
re.compile(r"sk-[A-Za-z0-9]{20,}"),
|
|
8
|
+
re.compile(r"sk-[A-Za-z0-9]{20,}"), # OpenAI / Anthropic style keys
|
|
9
9
|
re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*", re.IGNORECASE),
|
|
10
10
|
re.compile(r"(?i)(password|passwd|secret|token|api[_-]?key)\s*[=:]\s*\S+"),
|
|
11
|
-
re.compile(r"[A-Za-z0-9+/]{40,}={0,2}"),
|
|
11
|
+
re.compile(r"[A-Za-z0-9+/]{40,}={0,2}"), # long base64-like strings
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
_REDACTED = "[REDACTED]"
|
|
@@ -33,9 +33,21 @@ def redact_dict(obj: Any, _depth: int = 0) -> Any:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
_SENSITIVE_KEYS = {
|
|
36
|
-
"password",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
36
|
+
"password",
|
|
37
|
+
"passwd",
|
|
38
|
+
"secret",
|
|
39
|
+
"token",
|
|
40
|
+
"api_key",
|
|
41
|
+
"apikey",
|
|
42
|
+
"authorization",
|
|
43
|
+
"auth",
|
|
44
|
+
"cookie",
|
|
45
|
+
"session",
|
|
46
|
+
"credential",
|
|
47
|
+
"private_key",
|
|
48
|
+
"privatekey",
|
|
49
|
+
"access_key",
|
|
50
|
+
"accesskey",
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
|
agentx/evaluations/reporting.py
CHANGED
|
@@ -2,15 +2,23 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from agentx.evaluations.models import Report
|
|
4
4
|
from agentx.evaluations._term import (
|
|
5
|
-
bold,
|
|
5
|
+
bold,
|
|
6
|
+
cyan,
|
|
7
|
+
dim,
|
|
8
|
+
green,
|
|
9
|
+
yellow,
|
|
10
|
+
red,
|
|
11
|
+
magenta,
|
|
12
|
+
RESET,
|
|
13
|
+
BOLD,
|
|
6
14
|
)
|
|
7
15
|
|
|
8
|
-
_SEP
|
|
16
|
+
_SEP = "─" * 60
|
|
9
17
|
_THIN = "─" * 40
|
|
10
18
|
|
|
11
19
|
_RATING_COLORS = {"high": green, "medium": yellow, "low": red}
|
|
12
|
-
_RATING_ICONS
|
|
13
|
-
_PRI_COLORS
|
|
20
|
+
_RATING_ICONS = {"high": "●", "medium": "◑", "low": "○"}
|
|
21
|
+
_PRI_COLORS = {"high": red, "medium": yellow, "low": dim}
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
def _rating_badge(rating: str | None) -> str:
|
|
@@ -32,14 +40,32 @@ def print_report(report: Report) -> None:
|
|
|
32
40
|
print(cyan(_SEP))
|
|
33
41
|
print(f" {dim('Run :')} {dim(report.run_id)}")
|
|
34
42
|
print(f" {dim('Dataset :')} {dim(report.dataset_id)}")
|
|
35
|
-
print(
|
|
43
|
+
print(
|
|
44
|
+
f" {dim('Status :')} {green(report.status) if report.status == 'completed' else yellow(report.status)}"
|
|
45
|
+
)
|
|
36
46
|
|
|
37
47
|
if report.statistics:
|
|
38
48
|
s = report.statistics
|
|
39
49
|
avg = s.average_rating
|
|
40
50
|
avg_color = green if avg >= 7 else (yellow if avg >= 4 else red)
|
|
41
51
|
print(f" {dim('Cases :')} {s.number_of_runs}")
|
|
42
|
-
print(
|
|
52
|
+
print(
|
|
53
|
+
f" {dim('Rating :')} {avg_color(f'{avg:.1f}/10')} {dim(f'(min {s.min_rating:.1f} / max {s.max_rating:.1f})')}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
cos = report.cosine_similarity
|
|
57
|
+
if cos is not None:
|
|
58
|
+
cos_color = green if cos >= 0.85 else (yellow if cos >= 0.6 else red)
|
|
59
|
+
print(
|
|
60
|
+
f" {dim('Cosine :')} {cos_color(f'{cos * 100:.1f}%')} {dim('(vector similarity)')}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
jac = report.jaccard_similarity
|
|
64
|
+
if jac is not None:
|
|
65
|
+
jac_color = green if jac >= 0.6 else (yellow if jac >= 0.3 else red)
|
|
66
|
+
print(
|
|
67
|
+
f" {dim('Jaccard :')} {jac_color(f'{jac * 100:.1f}%')} {dim('(token-set overlap)')}"
|
|
68
|
+
)
|
|
43
69
|
|
|
44
70
|
if report.consistency_score is not None:
|
|
45
71
|
cs = report.consistency_score
|
|
@@ -120,8 +146,8 @@ def print_report(report: Report) -> None:
|
|
|
120
146
|
)
|
|
121
147
|
for rec in sorted_recs:
|
|
122
148
|
pri_fn = _PRI_COLORS.get(rec.priority or "low", dim)
|
|
123
|
-
pri
|
|
124
|
-
cat
|
|
149
|
+
pri = pri_fn(f"[{rec.priority.upper()}]") if rec.priority else ""
|
|
150
|
+
cat = dim(f"({rec.category})") if rec.category else ""
|
|
125
151
|
print(f" {pri} {cat} {rec.recommendation}")
|
|
126
152
|
if rec.reasoning:
|
|
127
153
|
print(f" {dim('→')} {dim(rec.reasoning)}")
|
agentx/evaluations/results.py
CHANGED
|
@@ -58,9 +58,13 @@ def normalize_result(
|
|
|
58
58
|
input_tokens = _to_int(raw.get("input_tokens"))
|
|
59
59
|
output_tokens = _to_int(raw.get("output_tokens"))
|
|
60
60
|
if input_tokens is None and isinstance(meta_raw, dict):
|
|
61
|
-
input_tokens = _to_int(
|
|
61
|
+
input_tokens = _to_int(
|
|
62
|
+
meta_raw.get("input_tokens") or meta_raw.get("prompt_tokens")
|
|
63
|
+
)
|
|
62
64
|
if output_tokens is None and isinstance(meta_raw, dict):
|
|
63
|
-
output_tokens = _to_int(
|
|
65
|
+
output_tokens = _to_int(
|
|
66
|
+
meta_raw.get("output_tokens") or meta_raw.get("completion_tokens")
|
|
67
|
+
)
|
|
64
68
|
|
|
65
69
|
err_raw = raw.get("error")
|
|
66
70
|
if err_raw:
|
|
@@ -77,7 +81,9 @@ def normalize_result(
|
|
|
77
81
|
else:
|
|
78
82
|
output = {"text": str(raw)} if raw is not None else {"text": ""}
|
|
79
83
|
|
|
80
|
-
has_timings =
|
|
84
|
+
has_timings = (
|
|
85
|
+
latency_ms is not None or input_tokens is not None or output_tokens is not None
|
|
86
|
+
)
|
|
81
87
|
return EvaluationResult(
|
|
82
88
|
case_id=case.case_id,
|
|
83
89
|
question_index=case.question_index,
|
|
@@ -86,12 +92,22 @@ def normalize_result(
|
|
|
86
92
|
output=output,
|
|
87
93
|
observableTrace=trace,
|
|
88
94
|
error=error,
|
|
89
|
-
timings=
|
|
95
|
+
timings=(
|
|
96
|
+
ResultTimings(
|
|
97
|
+
latencyMs=latency_ms,
|
|
98
|
+
inputTokens=input_tokens,
|
|
99
|
+
outputTokens=output_tokens,
|
|
100
|
+
)
|
|
101
|
+
if has_timings
|
|
102
|
+
else None
|
|
103
|
+
),
|
|
90
104
|
metadata=metadata,
|
|
91
105
|
)
|
|
92
106
|
|
|
93
107
|
|
|
94
|
-
def normalize_error(
|
|
108
|
+
def normalize_error(
|
|
109
|
+
case: EvaluationCase, exc: Exception, latency_ms: Optional[int] = None
|
|
110
|
+
) -> EvaluationResult:
|
|
95
111
|
"""Build a failed-case result from an exception."""
|
|
96
112
|
return EvaluationResult(
|
|
97
113
|
case_id=case.case_id,
|
agentx/evaluations/runner.py
CHANGED
|
@@ -20,7 +20,15 @@ from agentx.evaluations.redaction import redact_dict
|
|
|
20
20
|
from agentx.evaluations.reporting import print_report
|
|
21
21
|
from agentx.evaluations.results import normalize_result, normalize_error
|
|
22
22
|
from agentx.evaluations._term import (
|
|
23
|
-
bold,
|
|
23
|
+
bold,
|
|
24
|
+
cyan,
|
|
25
|
+
green,
|
|
26
|
+
yellow,
|
|
27
|
+
red,
|
|
28
|
+
dim,
|
|
29
|
+
BOLD,
|
|
30
|
+
RESET,
|
|
31
|
+
Spinner,
|
|
24
32
|
)
|
|
25
33
|
|
|
26
34
|
logger = logging.getLogger(__name__)
|
|
@@ -80,7 +88,9 @@ class EvaluationRunContext:
|
|
|
80
88
|
if display:
|
|
81
89
|
print(f" {dim('Agent :')} {display} {dim(f'({framework} / {runtime})')}")
|
|
82
90
|
print()
|
|
83
|
-
print(
|
|
91
|
+
print(
|
|
92
|
+
f"{bold('Executing')} {n_q} question{'s' if n_q != 1 else ''} × {n_r} run{'s' if n_r != 1 else ''}"
|
|
93
|
+
)
|
|
84
94
|
|
|
85
95
|
# Resume: skip already-submitted keys
|
|
86
96
|
already_done = self._fetch_submitted_keys()
|
|
@@ -120,7 +130,9 @@ class EvaluationRunContext:
|
|
|
120
130
|
with Spinner(f"Scoring — AI is rating {n} result{'s' if n != 1 else ''}"):
|
|
121
131
|
try:
|
|
122
132
|
resp = self._client.append_results(self._run.run_id, batch_id, batch)
|
|
123
|
-
print(
|
|
133
|
+
print(
|
|
134
|
+
f" {green('✓')} Scored {resp.accepted} result{'s' if resp.accepted != 1 else ''}"
|
|
135
|
+
)
|
|
124
136
|
logger.info(
|
|
125
137
|
"Batch %s: accepted=%d duplicates=%d failed=%d",
|
|
126
138
|
batch_id[:8],
|
|
@@ -229,12 +241,17 @@ class EvaluationsRunner:
|
|
|
229
241
|
# Helpers
|
|
230
242
|
# ---------------------------------------------------------------------------
|
|
231
243
|
|
|
244
|
+
|
|
232
245
|
def _wrap_adapter(adapter: AdapterLike) -> Callable[[EvaluationCase], EvaluationResult]:
|
|
233
|
-
if isinstance(
|
|
246
|
+
if isinstance(
|
|
247
|
+
adapter, (RawCallableAdapter, PrecomputedAdapter, HttpEndpointAdapter)
|
|
248
|
+
):
|
|
234
249
|
return adapter.run
|
|
235
250
|
if callable(adapter):
|
|
236
251
|
return RawCallableAdapter(adapter).run
|
|
237
|
-
raise TypeError(
|
|
252
|
+
raise TypeError(
|
|
253
|
+
f"adapter must be callable or an Adapter instance, got {type(adapter)}"
|
|
254
|
+
)
|
|
238
255
|
|
|
239
256
|
|
|
240
257
|
def _build_cases(dataset: Dataset) -> List[EvaluationCase]:
|
|
@@ -243,16 +260,18 @@ def _build_cases(dataset: Dataset) -> List[EvaluationCase]:
|
|
|
243
260
|
for q_idx, question in enumerate(dataset.questions):
|
|
244
261
|
mq = question.main_question
|
|
245
262
|
for run_num in range(1, n_runs + 1):
|
|
246
|
-
cases.append(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
cases.append(
|
|
264
|
+
EvaluationCase(
|
|
265
|
+
case_id=f"case-{q_idx}",
|
|
266
|
+
question_index=q_idx,
|
|
267
|
+
run_number=run_num,
|
|
268
|
+
query=mq.query,
|
|
269
|
+
expected_results=mq.expected_results,
|
|
270
|
+
expected_capabilities=mq.expected_capabilities,
|
|
271
|
+
expected_knowledge_base=mq.expected_knowledge_base,
|
|
272
|
+
expected_delegations=mq.expected_delegations,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
256
275
|
return cases
|
|
257
276
|
|
|
258
277
|
|
agentx/resources/workforce.py
CHANGED
|
@@ -84,7 +84,9 @@ class Workforce(BaseModel):
|
|
|
84
84
|
self, conversation_id: str, message: str, context: int = -1
|
|
85
85
|
) -> Iterator[ChatResponse]:
|
|
86
86
|
"""Send a message to a team conversation and stream the response."""
|
|
87
|
-
url =
|
|
87
|
+
url = (
|
|
88
|
+
f"{api_base()}/access/teams/conversations/{conversation_id}/jsonmessagesse"
|
|
89
|
+
)
|
|
88
90
|
response = requests.post(
|
|
89
91
|
url, headers=get_headers(), json={"message": message, "context": context}
|
|
90
92
|
)
|
agentx/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "0.4.
|
|
1
|
+
VERSION = "0.4.10"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentx-python
|
|
3
|
+
Version: 0.4.10
|
|
4
|
+
Summary: Official Python SDK for AgentX (https://www.agentx.so/)
|
|
5
|
+
Home-page: https://github.com/AgentX-ai/AgentX-python
|
|
6
|
+
Author: Robin Wang and AgentX Team
|
|
7
|
+
Author-email: contact@agentx.so
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: urllib3>=1.26.11
|
|
15
|
+
Requires-Dist: certifi
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: pydantic
|
|
18
|
+
Requires-Dist: pydantic_core
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: author-email
|
|
21
|
+
Dynamic: classifier
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/agentx-python/)
|
|
33
|
+
|
|
34
|
+
The official Python SDK for **[AgentX](https://www.agentx.so/)** — build, chat with, and orchestrate AI agents in a few lines of code.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Contents
|
|
39
|
+
|
|
40
|
+
- [Why AgentX](#why-agentx)
|
|
41
|
+
- [Installation](#installation)
|
|
42
|
+
- [Authentication](#authentication)
|
|
43
|
+
- [Quick start](#quick-start)
|
|
44
|
+
- [Working with agents](#working-with-agents)
|
|
45
|
+
- [List agents](#list-agents)
|
|
46
|
+
- [Start a conversation](#start-a-conversation)
|
|
47
|
+
- [Chat (streaming and non-streaming)](#chat-streaming-and-non-streaming)
|
|
48
|
+
- [Workforce (multi-agent orchestration)](#workforce-multi-agent-orchestration)
|
|
49
|
+
- [Agent Evaluations](#custom-agent-evaluations) — LLM-as-a-judge, cosine / Jaccard similarity
|
|
50
|
+
- [Links](#links)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Why AgentX
|
|
55
|
+
|
|
56
|
+
- **Simple mental model** — `Agent → Conversation → Message`.
|
|
57
|
+
- **Chain-of-thought** is built in, no extra plumbing.
|
|
58
|
+
- **Bring any LLM** — works across major open and closed-source vendors.
|
|
59
|
+
- **Batteries included** — voice (ASR/TTS), image generation, document/CSV/Excel/OCR, RAG with built-in re-ranking.
|
|
60
|
+
- **MCP support** — connect any Model Context Protocol server.
|
|
61
|
+
- **Multi-agent orchestration** — workforces of agents with a designated manager, across LLM vendors.
|
|
62
|
+
- **Agent Evaluations** — score any agent (LangChain, CrewAI, OpenAI, Anthropic, HTTP, …) with LLM-as-a-judge ratings plus optional cosine and Jaccard similarity metrics.
|
|
63
|
+
- **A2A** — Each agent can be published with agent-to-agent protocol compatible.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install --upgrade agentx-python
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Python 3.9 or newer.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Authentication
|
|
78
|
+
|
|
79
|
+
Get your API key at [app.agentx.so](https://app.agentx.so), then either pass it inline or expose it as an environment variable.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# Option A — pass the key inline
|
|
83
|
+
from agentx import AgentX
|
|
84
|
+
client = AgentX(api_key="your-api-key-here")
|
|
85
|
+
|
|
86
|
+
# Option B — set AGENTX_API_KEY in your environment, then:
|
|
87
|
+
client = AgentX.from_env()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Quick start
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from agentx import AgentX
|
|
96
|
+
|
|
97
|
+
client = AgentX.from_env()
|
|
98
|
+
|
|
99
|
+
# Pick an existing agent and chat with it
|
|
100
|
+
agent = client.list_agents()[0]
|
|
101
|
+
conversation = agent.new_conversation()
|
|
102
|
+
print(conversation.chat("Hello! What can you help me with?"))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
That's it. The remaining sections show the same primitives in more detail.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Working with agents
|
|
110
|
+
|
|
111
|
+
### List agents
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
agents = client.list_agents()
|
|
115
|
+
print(f"You have {len(agents)} agents")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Start a conversation
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
agent = client.get_agent(id="<agent-id>")
|
|
122
|
+
|
|
123
|
+
# Either resume an existing conversation…
|
|
124
|
+
existing = agent.list_conversations()
|
|
125
|
+
last = existing[-1]
|
|
126
|
+
for msg in last.list_messages():
|
|
127
|
+
print(msg)
|
|
128
|
+
|
|
129
|
+
# …or start a fresh one
|
|
130
|
+
conversation = agent.new_conversation()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Chat (streaming and non-streaming)
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# Blocking — returns the full response once it's ready
|
|
137
|
+
response = conversation.chat("What is your name?")
|
|
138
|
+
print(response)
|
|
139
|
+
|
|
140
|
+
# Streaming — yields ChatResponse objects as the model produces them
|
|
141
|
+
for chunk in conversation.chat_stream("Hello, what is your name?"):
|
|
142
|
+
if chunk.text:
|
|
143
|
+
print(chunk.text, end="")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Each `ChatResponse` chunk exposes the agent's `text` and, where applicable, its `cot` (chain-of-thought) reasoning, along with any retrieved references and tasks.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Workforce (multi-agent orchestration)
|
|
151
|
+
|
|
152
|
+
A **workforce** is a team of agents coordinated by a designated manager agent. Workforces can mix LLM vendors and route work between specialists.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
workforces = client.list_workforces()
|
|
156
|
+
workforce = workforces[0]
|
|
157
|
+
|
|
158
|
+
print(f"Workforce: {workforce.name}")
|
|
159
|
+
print(f"Manager: {workforce.manager.name}")
|
|
160
|
+
print(f"Agents: {[a.name for a in workforce.agents]}")
|
|
161
|
+
|
|
162
|
+
# Chat with the workforce — the manager decides which agent(s) to delegate to
|
|
163
|
+
conversation = workforce.new_conversation()
|
|
164
|
+
for chunk in workforce.chat_stream(conversation.id, "How can you help me with this project?"):
|
|
165
|
+
if chunk.text:
|
|
166
|
+
print(chunk.text, end="")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Custom agent evaluations
|
|
172
|
+
|
|
173
|
+
Evaluate **any** AI agent — LangChain, CrewAI, AutoGen, LlamaIndex, OpenAI, Anthropic, HTTP endpoints, or plain Python — using AgentX as the scoring and reporting backend. Includes optional **cosine** and **Jaccard** similarity metrics alongside LLM-graded ratings.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
report = (
|
|
177
|
+
client.evaluations
|
|
178
|
+
.run(dataset_id="evds_…", subject={"kind": "custom_agent", "framework": "raw_python"})
|
|
179
|
+
.execute(my_agent_fn)
|
|
180
|
+
.finalize()
|
|
181
|
+
.analyze()
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
print(report.average_rating) # LLM-graded score, 0–10
|
|
185
|
+
print(report.cosine_similarity) # embedding cosine, 0–1 (None if not enabled)
|
|
186
|
+
print(report.jaccard_similarity) # token-set overlap, 0–1 (None if not enabled)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder, framework adapters, similarity metrics, and the complete API reference.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Links
|
|
194
|
+
|
|
195
|
+
- **Dashboard** — [app.agentx.so](https://app.agentx.so)
|
|
196
|
+
- **Website** — [agentx.so](https://www.agentx.so/)
|
|
197
|
+
- **PyPI** — [agentx-python](https://pypi.org/project/agentx-python/)
|
|
198
|
+
- **Evaluations docs** — [EVALUATIONS.md](EVALUATIONS.md)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
agentx/__init__.py,sha256=AiY83tsCtjSMIBVsvNuxOgSU4i-rCREQrqDrNmCoSd0,279
|
|
2
|
+
agentx/agentx.py,sha256=_I2n77Db0rV3TSDZ44h1Trb6DluDoCBiMsu_KgdNU8I,3012
|
|
3
|
+
agentx/util.py,sha256=yOV3VVdc0KUbw5pEkSRiO2lZzEtL1vOsnB_f5tFk948,665
|
|
4
|
+
agentx/version.py,sha256=QnLb5RDU7syePvc1qNaT3NKjL1mC7ZCUkl8JHLvJm_8,19
|
|
5
|
+
agentx/evaluations/__init__.py,sha256=OmdCHiBO3Qcsck0UkOyVqSvfRs6nXbSi-2FXzyRfsog,89
|
|
6
|
+
agentx/evaluations/_term.py,sha256=S4blZgH66wHvkt2hR29h8JE5xnakBeD7dlZdUW9Wfoo,2065
|
|
7
|
+
agentx/evaluations/client.py,sha256=6y6_o0rZgMKOQLVIZEfgDhD7H_oMDXG3Em5pql7rJlU,7048
|
|
8
|
+
agentx/evaluations/datasets.py,sha256=_VaL6fQ8q33qpiawDr7qORQID8oJwNwMVgdNxM9FOns,8107
|
|
9
|
+
agentx/evaluations/models.py,sha256=G-F_LRK-pT1fYLSqVzHxSCtuRVPpmKALmR1Zr0W-gac,12773
|
|
10
|
+
agentx/evaluations/redaction.py,sha256=arvY9f9XxKHl6-K8P60Xzhb330nXF8a3wOp15d9i0bo,1410
|
|
11
|
+
agentx/evaluations/reporting.py,sha256=JJejSUhgnlYDJXEPgLqR9JoJXAPWDiu3fBGnKHM5EIM,5851
|
|
12
|
+
agentx/evaluations/results.py,sha256=tbUbewEpOyDlcUKxnSRk2m2pCDWk-NuHaAnmI8YZTGQ,4052
|
|
13
|
+
agentx/evaluations/runner.py,sha256=gJY9qSwhNmeKxMBm9AvDNk-WsdR9qU-OuasZbqYxikM,10593
|
|
14
|
+
agentx/evaluations/tracing.py,sha256=MSJD9bzfInMi7E70mc8mWUc0fx5DLkOgUzRru2mNQoc,1825
|
|
15
|
+
agentx/evaluations/adapters/__init__.py,sha256=fK8Hx75usbiY03XUSmnLnSrwBXip2CQs24YZcdEgj6w,287
|
|
16
|
+
agentx/evaluations/adapters/http_endpoint.py,sha256=3YDwpf9u6u3Xj8kLhERmShBeGMviKHCA0Dhr0t6KMmA,2061
|
|
17
|
+
agentx/evaluations/adapters/precomputed.py,sha256=BNNLQuzYFWyLSwsrMGv6MvRTroKY5Hz-Lm4F2RszESU,1210
|
|
18
|
+
agentx/evaluations/adapters/raw.py,sha256=FkDq_mdf21-xt5EHnGyIGcS6VZxkEkFcmM9VTd-T5sA,1130
|
|
19
|
+
agentx/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
agentx/resources/agent.py,sha256=ZKpxDYzJbKgOX4-W86U__b4gko7XlbwB4q6L8Az9uo0,2011
|
|
21
|
+
agentx/resources/conversation.py,sha256=sDaIhTQPiM3MvAKBK0SKVtBF-LppBXXfRv1-r_aFLoc,4431
|
|
22
|
+
agentx/resources/workforce.py,sha256=lfGVkoV9lcOp-lZScjTvwRarJMkhzfMfLm4syJDujGc,4168
|
|
23
|
+
agentx_python-0.4.10.dist-info/licenses/LICENSE,sha256=6ZbiPNFmv3xBb44LGhAa3PZYK0ROAztsd5LRFZDlGFE,1074
|
|
24
|
+
agentx_python-0.4.10.dist-info/METADATA,sha256=vpaVPSOKSN3GaQ_p-i8rKQtXwgYTYMDdzGbsaiFT6gM,6078
|
|
25
|
+
agentx_python-0.4.10.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
26
|
+
agentx_python-0.4.10.dist-info/top_level.txt,sha256=s-q-HB9Gb_QdrZNacSeQyF_c25gQooMy7DlxzgLOHPk,7
|
|
27
|
+
agentx_python-0.4.10.dist-info/RECORD,,
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: agentx-python
|
|
3
|
-
Version: 0.4.9
|
|
4
|
-
Summary: Official Python SDK for AgentX (https://www.agentx.so/)
|
|
5
|
-
Home-page: https://github.com/AgentX-ai/AgentX-python
|
|
6
|
-
Author: Robin Wang and AgentX Team
|
|
7
|
-
Author-email: contact@agentx.so
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.6
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
Requires-Dist: urllib3>=1.26.11
|
|
15
|
-
Requires-Dist: certifi
|
|
16
|
-
Requires-Dist: requests
|
|
17
|
-
Requires-Dist: pydantic
|
|
18
|
-
Requires-Dist: pydantic_core
|
|
19
|
-
Dynamic: author
|
|
20
|
-
Dynamic: author-email
|
|
21
|
-
Dynamic: classifier
|
|
22
|
-
Dynamic: description
|
|
23
|
-
Dynamic: description-content-type
|
|
24
|
-
Dynamic: home-page
|
|
25
|
-
Dynamic: license-file
|
|
26
|
-
Dynamic: requires-dist
|
|
27
|
-
Dynamic: requires-python
|
|
28
|
-
Dynamic: summary
|
|
29
|
-
|
|
30
|
-

|
|
31
|
-
|
|
32
|
-
[](https://pypi.org/project/agentx-python/)
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## A fast way to build AI Agents and create agent workforces
|
|
37
|
-
|
|
38
|
-
The official AgentX Python SDK for [AgentX](https://www.agentx.so/)
|
|
39
|
-
|
|
40
|
-
Why build AI agents with AgentX?
|
|
41
|
-
|
|
42
|
-
- Simplicity — Agent → Conversation → Message structure.
|
|
43
|
-
- Chain-of-thought built in.
|
|
44
|
-
- Choose from most open and closed-source LLM vendors.
|
|
45
|
-
- Built-in Voice (ASR, TTS), Image Gen, Document, CSV/Excel, OCR, and more.
|
|
46
|
-
- Support for all running MCP (Model Context Protocol) servers.
|
|
47
|
-
- RAG with built-in re-rank.
|
|
48
|
-
- Multi-agent workforce orchestration.
|
|
49
|
-
- Multiple agents working together with a designated manager agent.
|
|
50
|
-
- Cross-LLM-vendor, multi-agent orchestration.
|
|
51
|
-
- A2A — agent-to-agent protocol (coming soon)
|
|
52
|
-
|
|
53
|
-
## Installation
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
pip install --upgrade agentx-python
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Quick Start
|
|
60
|
-
|
|
61
|
-
```python
|
|
62
|
-
from agentx import AgentX
|
|
63
|
-
|
|
64
|
-
client = AgentX(api_key="your-api-key-here")
|
|
65
|
-
|
|
66
|
-
agents = client.list_agents()
|
|
67
|
-
print(f"You have {len(agents)} agents")
|
|
68
|
-
|
|
69
|
-
if agents:
|
|
70
|
-
agent = agents[0]
|
|
71
|
-
conversation = agent.new_conversation()
|
|
72
|
-
response = conversation.chat("Hello! What can you help me with?")
|
|
73
|
-
print(response)
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Usage
|
|
77
|
-
|
|
78
|
-
Provide an `api_key` inline or set `AGENTX_API_KEY` as an environment variable.
|
|
79
|
-
Get your API key at https://app.agentx.so
|
|
80
|
-
|
|
81
|
-
### Agent
|
|
82
|
-
|
|
83
|
-
```python
|
|
84
|
-
from agentx import AgentX
|
|
85
|
-
|
|
86
|
-
client = AgentX(api_key="<your api key here>")
|
|
87
|
-
print(client.list_agents())
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Conversation
|
|
91
|
-
|
|
92
|
-
```python
|
|
93
|
-
my_agent = client.get_agent(id="<agent id here>")
|
|
94
|
-
|
|
95
|
-
existing_conversations = my_agent.list_conversations()
|
|
96
|
-
last_conversation = existing_conversations[-1]
|
|
97
|
-
msgs = last_conversation.list_messages()
|
|
98
|
-
print(msgs)
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### Chat
|
|
102
|
-
|
|
103
|
-
```python
|
|
104
|
-
a_conversation = my_agent.get_conversation(id="<conversation id here>")
|
|
105
|
-
|
|
106
|
-
response = a_conversation.chat_stream("Hello, what is your name?")
|
|
107
|
-
for chunk in response:
|
|
108
|
-
print(chunk)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
\*`cot` stands for chain-of-thought
|
|
112
|
-
|
|
113
|
-
### Workforce
|
|
114
|
-
|
|
115
|
-
```python
|
|
116
|
-
from agentx import AgentX
|
|
117
|
-
|
|
118
|
-
client = AgentX(api_key="<your api key here>")
|
|
119
|
-
|
|
120
|
-
workforces = client.list_workforces()
|
|
121
|
-
workforce = workforces[0]
|
|
122
|
-
print(f"Workforce: {workforce.name}")
|
|
123
|
-
print(f"Manager: {workforce.manager.name}")
|
|
124
|
-
print(f"Agents: {[agent.name for agent in workforce.agents]}")
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
#### Chat with Workforce
|
|
128
|
-
|
|
129
|
-
```python
|
|
130
|
-
conversation = workforce.new_conversation()
|
|
131
|
-
response = workforce.chat_stream(conversation.id, "How can you help me with this project?")
|
|
132
|
-
for chunk in response:
|
|
133
|
-
if chunk.text:
|
|
134
|
-
print(chunk.text, end="")
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## Custom Agent Evaluations
|
|
140
|
-
|
|
141
|
-
See **[EVALUATIONS.md](EVALUATIONS.md)** for full documentation — installation, dataset builder, framework examples (OpenAI, Anthropic, Google, LangChain, CrewAI, AutoGen, LlamaIndex, HTTP endpoints), and the complete API reference.
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
agentx/__init__.py,sha256=AiY83tsCtjSMIBVsvNuxOgSU4i-rCREQrqDrNmCoSd0,279
|
|
2
|
-
agentx/agentx.py,sha256=ICZ5oT-WESgkVBuYExLOAGrYoJUdGoAKYa3BsfD6W_g,3011
|
|
3
|
-
agentx/util.py,sha256=yOV3VVdc0KUbw5pEkSRiO2lZzEtL1vOsnB_f5tFk948,665
|
|
4
|
-
agentx/version.py,sha256=3FouI0PeO06qTMDNHCv_qKuT_7JOUBQr5L9BYxVd794,18
|
|
5
|
-
agentx/evaluations/__init__.py,sha256=OmdCHiBO3Qcsck0UkOyVqSvfRs6nXbSi-2FXzyRfsog,89
|
|
6
|
-
agentx/evaluations/_term.py,sha256=Vcfx89TiORVvh-tGGQfvxtBJEPJsxUv5S7LUplubaAA,2023
|
|
7
|
-
agentx/evaluations/client.py,sha256=Wd44taWQMTjn8SdJohb8K_PIbU0Rzck76G9wpn-RO7k,6858
|
|
8
|
-
agentx/evaluations/datasets.py,sha256=s9B-S7i83JChP9MDjtCMGPMkvz3emXohyQClJD1YVMg,7849
|
|
9
|
-
agentx/evaluations/models.py,sha256=p9yDqJY6YP5FbMZZHS_RsngfCUb7v8LakodrtL3NQ8o,10041
|
|
10
|
-
agentx/evaluations/redaction.py,sha256=zljolnX64kaBUiqrPGD_kSFZvb65z2CvMrryoJtwY3k,1374
|
|
11
|
-
agentx/evaluations/reporting.py,sha256=g1zLJ_BR0IfO3VHEM0xm8wyEkgkQvLJQ-NdIW8lNMD0,5266
|
|
12
|
-
agentx/evaluations/results.py,sha256=-K122Z1_fIaDAeP0neLI9EXwEnPESbu8z5qo_aXRrZY,3859
|
|
13
|
-
agentx/evaluations/runner.py,sha256=5_QIpU8PSAQniFdubW1CnJK4CQ3HQhyN1u683t926hA,10406
|
|
14
|
-
agentx/evaluations/tracing.py,sha256=MSJD9bzfInMi7E70mc8mWUc0fx5DLkOgUzRru2mNQoc,1825
|
|
15
|
-
agentx/evaluations/adapters/__init__.py,sha256=fK8Hx75usbiY03XUSmnLnSrwBXip2CQs24YZcdEgj6w,287
|
|
16
|
-
agentx/evaluations/adapters/http_endpoint.py,sha256=3YDwpf9u6u3Xj8kLhERmShBeGMviKHCA0Dhr0t6KMmA,2061
|
|
17
|
-
agentx/evaluations/adapters/precomputed.py,sha256=yuJrDv6SH1mRSCz3EgxPSUu9iBJeTY2uJflK5S0QxcM,1188
|
|
18
|
-
agentx/evaluations/adapters/raw.py,sha256=AqSHTM3p45LK4Wo1lkUboclMY97SYFx9kzDQ6GNbGA8,1115
|
|
19
|
-
agentx/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
agentx/resources/agent.py,sha256=ZKpxDYzJbKgOX4-W86U__b4gko7XlbwB4q6L8Az9uo0,2011
|
|
21
|
-
agentx/resources/conversation.py,sha256=sDaIhTQPiM3MvAKBK0SKVtBF-LppBXXfRv1-r_aFLoc,4431
|
|
22
|
-
agentx/resources/workforce.py,sha256=QpyQNBQR24MbcAPtrPZZsdfSuXDSOyNmlcfWeHudXmY,4144
|
|
23
|
-
agentx_python-0.4.9.dist-info/licenses/LICENSE,sha256=6ZbiPNFmv3xBb44LGhAa3PZYK0ROAztsd5LRFZDlGFE,1074
|
|
24
|
-
agentx_python-0.4.9.dist-info/METADATA,sha256=-o556esk8h8G-_rhhOxXTUbfUaV4Z_-xK3i1mzK9DYo,3691
|
|
25
|
-
agentx_python-0.4.9.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
26
|
-
agentx_python-0.4.9.dist-info/top_level.txt,sha256=s-q-HB9Gb_QdrZNacSeQyF_c25gQooMy7DlxzgLOHPk,7
|
|
27
|
-
agentx_python-0.4.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|