agentx-python 0.6.4__tar.gz → 0.6.6__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 (55) hide show
  1. {agentx_python-0.6.4/agentx_python.egg-info → agentx_python-0.6.6}/PKG-INFO +27 -1
  2. {agentx_python-0.6.4 → agentx_python-0.6.6}/README.md +26 -0
  3. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/agentx.py +18 -2
  4. agentx_python-0.6.6/agentx/evaluations/__init__.py +7 -0
  5. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/client.py +65 -6
  6. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/datasets.py +12 -1
  7. agentx_python-0.6.6/agentx/evaluations/evaluation_settings.py +109 -0
  8. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/models.py +89 -7
  9. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/reporting.py +14 -0
  10. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/results.py +4 -0
  11. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/runner.py +87 -10
  12. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/integrations/crewai.py +7 -1
  13. agentx_python-0.6.6/agentx/monitor/__init__.py +10 -0
  14. agentx_python-0.6.6/agentx/monitor/client.py +134 -0
  15. agentx_python-0.6.6/agentx/monitor/models.py +37 -0
  16. agentx_python-0.6.6/agentx/monitor/patterns.py +121 -0
  17. agentx_python-0.6.6/agentx/resources/__init__.py +0 -0
  18. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/resources/conversation.py +2 -2
  19. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/tracing/ingest_client.py +24 -0
  20. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/tracing/tracer.py +86 -5
  21. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/util.py +2 -1
  22. agentx_python-0.6.6/agentx/version.py +1 -0
  23. {agentx_python-0.6.4 → agentx_python-0.6.6/agentx_python.egg-info}/PKG-INFO +27 -1
  24. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx_python.egg-info/SOURCES.txt +7 -0
  25. agentx_python-0.6.6/agentx_python.egg-info/not-zip-safe +1 -0
  26. {agentx_python-0.6.4 → agentx_python-0.6.6}/setup.py +5 -0
  27. agentx_python-0.6.4/agentx/evaluations/__init__.py +0 -3
  28. agentx_python-0.6.4/agentx/version.py +0 -1
  29. {agentx_python-0.6.4 → agentx_python-0.6.6}/LICENSE +0 -0
  30. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/__init__.py +0 -0
  31. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/_term.py +0 -0
  32. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/adapters/__init__.py +0 -0
  33. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  34. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/adapters/precomputed.py +0 -0
  35. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/adapters/raw.py +0 -0
  36. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/redaction.py +0 -0
  37. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/evaluations/tracing.py +0 -0
  38. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/exceptions.py +0 -0
  39. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/integrations/__init__.py +0 -0
  40. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/integrations/_perf.py +0 -0
  41. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/integrations/anthropic.py +0 -0
  42. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/integrations/google_adk.py +0 -0
  43. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/integrations/google_genai.py +0 -0
  44. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/integrations/langchain.py +0 -0
  45. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/integrations/openai_agents.py +0 -0
  46. /agentx_python-0.6.4/agentx/resources/__init__.py → /agentx_python-0.6.6/agentx/py.typed +0 -0
  47. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/resources/agent.py +0 -0
  48. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/resources/workforce.py +0 -0
  49. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/tracing/__init__.py +0 -0
  50. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx/tracing/ci_types.py +0 -0
  51. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx_python.egg-info/dependency_links.txt +0 -0
  52. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx_python.egg-info/requires.txt +0 -0
  53. {agentx_python-0.6.4 → agentx_python-0.6.6}/agentx_python.egg-info/top_level.txt +0 -0
  54. {agentx_python-0.6.4 → agentx_python-0.6.6}/setup.cfg +0 -0
  55. {agentx_python-0.6.4 → agentx_python-0.6.6}/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.6
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
@@ -70,6 +70,7 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
70
70
  - [Chat (streaming and non-streaming)](#chat-streaming-and-non-streaming)
71
71
  - [Workforce (multi-agent orchestration)](#workforce-multi-agent-orchestration) — teams of agents with a designated manager
72
72
  - [Production tracing](#production-tracing) — record live agent runs from any framework
73
+ - [Monitor](#monitor) — automatic production monitoring, patterns and signals
73
74
  - [Custom agent evaluations](#custom-agent-evaluations) — LLM-as-a-judge, cosine / Jaccard similarity
74
75
  - [Links](#links)
75
76
 
@@ -245,6 +246,30 @@ See **[TRACING.md](TRACING.md)** for the complete guide — session grouping, er
245
246
 
246
247
  ---
247
248
 
249
+ ## Monitor
250
+
251
+ Automatic production monitoring: check traces against detection patterns and get back triage-ready signals. A **pattern** is a first-class SDK resource with a real id, just like a `Dataset` or `EvaluationSettings`: create one once, then reference it by id at trace time.
252
+
253
+ ```python
254
+ pattern = client.monitor.patterns.builder(
255
+ name="Promises a refund",
256
+ detector_kind="semantic",
257
+ semantic_prompt="The response promises a refund.",
258
+ severity="high",
259
+ ).publish()
260
+
261
+ with client.tracer.trace("support-agent", monitor=True, pattern_ids=[pattern.id]) as span:
262
+ span.output = call_llm(query)
263
+ ```
264
+
265
+ `monitor=True` checks the trace immediately, no dashboard setup required. `pattern_ids` restricts detection to exactly those patterns; omit it to run the full default sweep instead (built-in checks like empty response, trace error, and latency regression, plus every pattern enabled for the workspace).
266
+
267
+ This works independently of the dashboard's per-agent monitoring toggle (Governance > Observe > Agents), which still auto-checks every trace from an agent once enabled there, with no code changes needed either way.
268
+
269
+ See **[TRACING.md](TRACING.md)** for the complete Monitor guide.
270
+
271
+ ---
272
+
248
273
  ## Custom agent evaluations
249
274
 
250
275
  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.
@@ -274,3 +299,4 @@ See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder,
274
299
  - **PyPI** — [agentx-python](https://pypi.org/project/agentx-python/)
275
300
  - **Tracing docs** — [TRACING.md](TRACING.md)
276
301
  - **Evaluations docs** — [EVALUATIONS.md](EVALUATIONS.md)
302
+ - **Monitor docs** — [docs.agentx.so/sdk/monitor](https://docs.agentx.so/sdk/monitor)
@@ -22,6 +22,7 @@ Also see [SDK Developer Docs](https://developers.agentx.so), [API Reference Docs
22
22
  - [Chat (streaming and non-streaming)](#chat-streaming-and-non-streaming)
23
23
  - [Workforce (multi-agent orchestration)](#workforce-multi-agent-orchestration) — teams of agents with a designated manager
24
24
  - [Production tracing](#production-tracing) — record live agent runs from any framework
25
+ - [Monitor](#monitor) — automatic production monitoring, patterns and signals
25
26
  - [Custom agent evaluations](#custom-agent-evaluations) — LLM-as-a-judge, cosine / Jaccard similarity
26
27
  - [Links](#links)
27
28
 
@@ -197,6 +198,30 @@ See **[TRACING.md](TRACING.md)** for the complete guide — session grouping, er
197
198
 
198
199
  ---
199
200
 
201
+ ## Monitor
202
+
203
+ Automatic production monitoring: check traces against detection patterns and get back triage-ready signals. A **pattern** is a first-class SDK resource with a real id, just like a `Dataset` or `EvaluationSettings`: create one once, then reference it by id at trace time.
204
+
205
+ ```python
206
+ pattern = client.monitor.patterns.builder(
207
+ name="Promises a refund",
208
+ detector_kind="semantic",
209
+ semantic_prompt="The response promises a refund.",
210
+ severity="high",
211
+ ).publish()
212
+
213
+ with client.tracer.trace("support-agent", monitor=True, pattern_ids=[pattern.id]) as span:
214
+ span.output = call_llm(query)
215
+ ```
216
+
217
+ `monitor=True` checks the trace immediately, no dashboard setup required. `pattern_ids` restricts detection to exactly those patterns; omit it to run the full default sweep instead (built-in checks like empty response, trace error, and latency regression, plus every pattern enabled for the workspace).
218
+
219
+ This works independently of the dashboard's per-agent monitoring toggle (Governance > Observe > Agents), which still auto-checks every trace from an agent once enabled there, with no code changes needed either way.
220
+
221
+ See **[TRACING.md](TRACING.md)** for the complete Monitor guide.
222
+
223
+ ---
224
+
200
225
  ## Custom agent evaluations
201
226
 
202
227
  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.
@@ -226,3 +251,4 @@ See **[EVALUATIONS.md](EVALUATIONS.md)** for the full guide — dataset builder,
226
251
  - **PyPI** — [agentx-python](https://pypi.org/project/agentx-python/)
227
252
  - **Tracing docs** — [TRACING.md](TRACING.md)
228
253
  - **Evaluations docs** — [EVALUATIONS.md](EVALUATIONS.md)
254
+ - **Monitor docs** — [docs.agentx.so/sdk/monitor](https://docs.agentx.so/sdk/monitor)
@@ -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
@@ -24,6 +29,7 @@ class AgentX:
24
29
 
25
30
  from agentx.evaluations.client import EvaluationsClient
26
31
  from agentx.evaluations.runner import EvaluationsRunner
32
+ from agentx.monitor.client import MonitorClient
27
33
  from agentx.tracing.ingest_client import IngestClient
28
34
  from agentx.tracing.tracer import Tracer
29
35
  from agentx.version import VERSION
@@ -32,9 +38,19 @@ class AgentX:
32
38
  api_key=self.api_key,
33
39
  sdk_version=VERSION,
34
40
  base_url=self.base_url,
41
+ workspace_id=self.workspace_id,
35
42
  )
36
43
  self.evaluations = EvaluationsRunner(_eval_client)
37
44
 
45
+ # Monitor: create/reuse patterns (client.monitor.patterns) that a trace can be checked
46
+ # against at send time via tracer.trace(..., monitor=True, pattern_ids=[...]).
47
+ self.monitor = MonitorClient(
48
+ api_key=self.api_key,
49
+ sdk_version=VERSION,
50
+ base_url=self.base_url,
51
+ workspace_id=self.workspace_id,
52
+ )
53
+
38
54
  _ingest_client = IngestClient(
39
55
  api_key=self.api_key,
40
56
  sdk_version=VERSION,
@@ -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