agentx-python 0.6.4__tar.gz → 0.6.5__tar.gz

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