agentx-python 0.6.34__py3-none-any.whl → 0.6.36__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 +29 -17
- agentx/evaluations/client.py +28 -3
- agentx/evaluations/datasets.py +4 -0
- agentx/evaluations/evaluation_settings.py +17 -1
- agentx/evaluations/models.py +1 -1
- agentx/evaluations/runner.py +16 -11
- agentx/export.py +5 -3
- agentx/feedback.py +5 -3
- agentx/monitor/client.py +19 -2
- agentx/monitor/judge_scorers.py +334 -0
- agentx/monitor/online_evaluators.py +21 -2
- agentx/monitor/scorers.py +4 -2
- agentx/outcomes.py +4 -2
- agentx/projects.py +5 -2
- agentx/traces.py +4 -2
- agentx/tracing/ingest_client.py +46 -6
- agentx/tracing/tracer.py +5 -3
- agentx/version.py +1 -1
- {agentx_python-0.6.34.dist-info → agentx_python-0.6.36.dist-info}/METADATA +1 -1
- {agentx_python-0.6.34.dist-info → agentx_python-0.6.36.dist-info}/RECORD +24 -23
- {agentx_python-0.6.34.dist-info → agentx_python-0.6.36.dist-info}/WHEEL +0 -0
- {agentx_python-0.6.34.dist-info → agentx_python-0.6.36.dist-info}/entry_points.txt +0 -0
- {agentx_python-0.6.34.dist-info → agentx_python-0.6.36.dist-info}/licenses/LICENSE +0 -0
- {agentx_python-0.6.34.dist-info → agentx_python-0.6.36.dist-info}/top_level.txt +0 -0
agentx/agentx.py
CHANGED
|
@@ -20,10 +20,12 @@ class AgentX:
|
|
|
20
20
|
if self.api_key and not os.getenv("AGENTX_API_KEY"):
|
|
21
21
|
os.environ["AGENTX_API_KEY"] = self.api_key
|
|
22
22
|
|
|
23
|
-
# base_url overrides AGENTX_API_BASE_URL env var (and the SDK default)
|
|
23
|
+
# base_url overrides AGENTX_API_BASE_URL env var (and the SDK default). It is
|
|
24
|
+
# deliberately NOT written back into os.environ: the constructor used to do that, which
|
|
25
|
+
# made the last-constructed client silently re-point every other client in the process
|
|
26
|
+
# (deep-dive round 3, bug #1). Each sub-client below receives this value explicitly and
|
|
27
|
+
# captures it at construction instead.
|
|
24
28
|
self.base_url = base_url or os.getenv("AGENTX_API_BASE_URL")
|
|
25
|
-
if self.base_url:
|
|
26
|
-
os.environ["AGENTX_API_BASE_URL"] = self.base_url
|
|
27
29
|
|
|
28
30
|
self.workspace_id = workspace_id or os.getenv("AGENTX_WORKSPACE_ID")
|
|
29
31
|
|
|
@@ -58,29 +60,29 @@ class AgentX:
|
|
|
58
60
|
|
|
59
61
|
# Report real, after-the-fact outcomes ("the ticket got reopened") against traces - the
|
|
60
62
|
# ground truth behind the dashboard's Judge Calibration card. Self-host only.
|
|
61
|
-
self.outcomes = OutcomesClient(api_key=self.api_key)
|
|
63
|
+
self.outcomes = OutcomesClient(api_key=self.api_key, base_url=self.base_url)
|
|
62
64
|
|
|
63
65
|
from agentx.projects import ProjectsClient
|
|
64
66
|
|
|
65
67
|
# Project CRUD (self-host): isolated tenants with their own API keys (P1.1).
|
|
66
|
-
self.projects = ProjectsClient(api_key=self.api_key)
|
|
68
|
+
self.projects = ProjectsClient(api_key=self.api_key, base_url=self.base_url)
|
|
67
69
|
|
|
68
70
|
from agentx.traces import TracesClient
|
|
69
71
|
|
|
70
72
|
# The read side of tracing: trace-by-id detail and paginated listing (P1.2).
|
|
71
|
-
self.traces = TracesClient(api_key=self.api_key)
|
|
73
|
+
self.traces = TracesClient(api_key=self.api_key, base_url=self.base_url)
|
|
72
74
|
|
|
73
75
|
from agentx.export import ExportClient
|
|
74
76
|
|
|
75
77
|
# Bulk NDJSON egress for backup/migration (P2.1): manifest, per-entity streaming, and
|
|
76
78
|
# directory dumps. Self-host only.
|
|
77
|
-
self.export = ExportClient(api_key=self.api_key)
|
|
79
|
+
self.export = ExportClient(api_key=self.api_key, base_url=self.base_url)
|
|
78
80
|
|
|
79
81
|
from agentx.feedback import FeedbackClient
|
|
80
82
|
|
|
81
83
|
# Forward end-user votes ("up"/"down") on traced responses - a "down" raises a signal
|
|
82
84
|
# directly, and every vote feeds Judge Calibration alongside outcomes. Self-host only.
|
|
83
|
-
self.feedback = FeedbackClient(api_key=self.api_key)
|
|
85
|
+
self.feedback = FeedbackClient(api_key=self.api_key, base_url=self.base_url)
|
|
84
86
|
|
|
85
87
|
_ingest_client = IngestClient(
|
|
86
88
|
api_key=self.api_key,
|
|
@@ -92,13 +94,23 @@ class AgentX:
|
|
|
92
94
|
|
|
93
95
|
@classmethod
|
|
94
96
|
def from_env(cls) -> "AgentX":
|
|
95
|
-
"""Create an AgentX client
|
|
96
|
-
|
|
97
|
+
"""Create an AgentX client from the environment: AGENTX_API_KEY plus, for the base URL,
|
|
98
|
+
the first of AGENTX_API_BASE_URL / AGENTX_SELFHOST_BASE_URL / BASE_URL that is set.
|
|
99
|
+
The fallbacks match the conventions the self-host samples and .env files already use,
|
|
100
|
+
so from_env works wherever an explicit AgentX(base_url=...) would."""
|
|
101
|
+
import os
|
|
102
|
+
|
|
103
|
+
base_url = (
|
|
104
|
+
os.getenv("AGENTX_API_BASE_URL")
|
|
105
|
+
or os.getenv("AGENTX_SELFHOST_BASE_URL")
|
|
106
|
+
or os.getenv("BASE_URL")
|
|
107
|
+
)
|
|
108
|
+
return cls(base_url=base_url) if base_url else cls()
|
|
97
109
|
|
|
98
110
|
def get_agent(self, id: str) -> Agent:
|
|
99
|
-
url = f"{api_base()}/access/agents/{id}"
|
|
111
|
+
url = f"{self.base_url or api_base()}/access/agents/{id}"
|
|
100
112
|
# Make a GET request to the AgentX API
|
|
101
|
-
response = requests.get(url, headers=get_headers())
|
|
113
|
+
response = requests.get(url, headers=get_headers(self.api_key))
|
|
102
114
|
# Check if response was successful
|
|
103
115
|
if response.status_code == 200:
|
|
104
116
|
return Agent(**response.json())
|
|
@@ -106,9 +118,9 @@ class AgentX:
|
|
|
106
118
|
raise Exception(f"Failed to retrieve agent: {response.reason}")
|
|
107
119
|
|
|
108
120
|
def list_agents(self) -> List[Agent]:
|
|
109
|
-
url = f"{api_base()}/access/agents"
|
|
121
|
+
url = f"{self.base_url or api_base()}/access/agents"
|
|
110
122
|
# Make a GET request to the AgentX API
|
|
111
|
-
response = requests.get(url, headers=get_headers())
|
|
123
|
+
response = requests.get(url, headers=get_headers(self.api_key))
|
|
112
124
|
# Check if response was successful
|
|
113
125
|
if response.status_code == 200:
|
|
114
126
|
return [Agent(**agent) for agent in response.json()]
|
|
@@ -146,7 +158,7 @@ class AgentX:
|
|
|
146
158
|
"""
|
|
147
159
|
from agentx.exceptions import AgentXAPIError, AgentXAuthError, AgentXConnectionError
|
|
148
160
|
|
|
149
|
-
base = api_base()
|
|
161
|
+
base = (self.base_url or api_base()).rstrip("/")
|
|
150
162
|
# /monitor/patterns: the cheapest key-authenticated endpoint that exists on both the
|
|
151
163
|
# hosted API and the self-host engine's SDK-facing router.
|
|
152
164
|
url = f"{base}/monitor/patterns"
|
|
@@ -173,8 +185,8 @@ class AgentX:
|
|
|
173
185
|
|
|
174
186
|
def get_profile(self):
|
|
175
187
|
"""Get the current user's profile information."""
|
|
176
|
-
url = f"{api_base()}/access/getProfile"
|
|
177
|
-
response = requests.get(url, headers=get_headers())
|
|
188
|
+
url = f"{self.base_url or api_base()}/access/getProfile"
|
|
189
|
+
response = requests.get(url, headers=get_headers(self.api_key))
|
|
178
190
|
if response.status_code == 200:
|
|
179
191
|
return response.json()
|
|
180
192
|
else:
|
agentx/evaluations/client.py
CHANGED
|
@@ -59,6 +59,15 @@ class AgentXValidationError(AgentXEvaluationsError):
|
|
|
59
59
|
pass
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _resolve_scorer_id(scorer_id: Optional[str], evaluation_settings_id: Optional[str]) -> Optional[str]:
|
|
63
|
+
"""One grader, two spellings: ``scorer_id`` is the post-consolidation name for what the wire
|
|
64
|
+
still calls ``evaluationSettingsId`` (the ids are identical by design). Both kwargs are
|
|
65
|
+
accepted everywhere a run picks its grader; passing both with different values is a bug."""
|
|
66
|
+
if scorer_id and evaluation_settings_id and scorer_id != evaluation_settings_id:
|
|
67
|
+
raise ValueError("Pass either scorer_id or evaluation_settings_id (they are the same id), not two different ids")
|
|
68
|
+
return scorer_id or evaluation_settings_id
|
|
69
|
+
|
|
70
|
+
|
|
62
71
|
class EvaluationsClient:
|
|
63
72
|
def __init__(
|
|
64
73
|
self,
|
|
@@ -100,11 +109,21 @@ class EvaluationsClient:
|
|
|
100
109
|
from agentx.evaluations.prompts import PromptClient
|
|
101
110
|
|
|
102
111
|
self.datasets = DatasetClient(self)
|
|
103
|
-
|
|
112
|
+
# Legacy view of an LLM Judge Scorer's offline profile - constructed lazily so its
|
|
113
|
+
# DeprecationWarning fires on first USE, not for every client that never touches it.
|
|
114
|
+
self._settings: "EvaluationSettingsClient | None" = None
|
|
104
115
|
self.prompts = PromptClient(self)
|
|
105
116
|
from agentx.evaluations.tool_schemas import ToolSchemaClient
|
|
106
117
|
self.tool_schemas = ToolSchemaClient(self)
|
|
107
118
|
|
|
119
|
+
@property
|
|
120
|
+
def settings(self) -> "EvaluationSettingsClient":
|
|
121
|
+
if self._settings is None:
|
|
122
|
+
from agentx.evaluations.evaluation_settings import EvaluationSettingsClient
|
|
123
|
+
|
|
124
|
+
self._settings = EvaluationSettingsClient(self)
|
|
125
|
+
return self._settings
|
|
126
|
+
|
|
108
127
|
# ------------------------------------------------------------------
|
|
109
128
|
# Low-level HTTP
|
|
110
129
|
# ------------------------------------------------------------------
|
|
@@ -275,10 +294,16 @@ class EvaluationsClient:
|
|
|
275
294
|
dataset_id: str,
|
|
276
295
|
subject: EvaluationSubject,
|
|
277
296
|
python_version: Optional[str] = None,
|
|
297
|
+
scorer_id: Optional[str] = None,
|
|
278
298
|
evaluation_settings_id: Optional[str] = None,
|
|
279
299
|
) -> EvaluationRun:
|
|
300
|
+
"""``scorer_id`` names the LLM Judge Scorer grading this run (its id doubles as the
|
|
301
|
+
wire's ``evaluationSettingsId``). ``evaluation_settings_id`` is the pre-consolidation
|
|
302
|
+
alias and keeps working."""
|
|
280
303
|
from agentx.version import VERSION
|
|
281
304
|
|
|
305
|
+
grader_id = _resolve_scorer_id(scorer_id, evaluation_settings_id)
|
|
306
|
+
|
|
282
307
|
payload = {
|
|
283
308
|
"datasetId": dataset_id,
|
|
284
309
|
"evaluationSubject": subject.model_dump(by_alias=True, exclude_none=True),
|
|
@@ -290,8 +315,8 @@ class EvaluationsClient:
|
|
|
290
315
|
"pythonVersion": python_version or _python_version(),
|
|
291
316
|
},
|
|
292
317
|
}
|
|
293
|
-
if
|
|
294
|
-
payload["evaluationSettingsId"] =
|
|
318
|
+
if grader_id:
|
|
319
|
+
payload["evaluationSettingsId"] = grader_id
|
|
295
320
|
data = self._request("POST", "/runs", json=self._with_workspace(payload))
|
|
296
321
|
return EvaluationRun(**data)
|
|
297
322
|
|
agentx/evaluations/datasets.py
CHANGED
|
@@ -290,6 +290,7 @@ class DatasetClient:
|
|
|
290
290
|
rouge_score: bool = False,
|
|
291
291
|
similarity_model: Optional[str] = None,
|
|
292
292
|
sovereignty_models: Optional[List[str]] = None,
|
|
293
|
+
code_scorers: Optional[List[Dict[str, Any]]] = None,
|
|
293
294
|
) -> DatasetBuilder:
|
|
294
295
|
return DatasetBuilder(
|
|
295
296
|
self._client,
|
|
@@ -307,6 +308,9 @@ class DatasetClient:
|
|
|
307
308
|
rouge_score=rouge_score,
|
|
308
309
|
similarity_model=similarity_model,
|
|
309
310
|
sovereignty_models=sovereignty_models,
|
|
311
|
+
# Was documented (evaluation/code-scorers.mdx) but not forwarded - DatasetBuilder
|
|
312
|
+
# itself always accepted it. Fixed with the judge-scorer unification.
|
|
313
|
+
code_scorers=code_scorers,
|
|
310
314
|
)
|
|
311
315
|
|
|
312
316
|
def from_csv(self, path: str, name: str, **kwargs) -> DatasetBuilder:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import warnings
|
|
4
5
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from agentx.evaluations.models import EvaluationSettings
|
|
@@ -81,10 +82,25 @@ class EvaluationSettingsBuilder:
|
|
|
81
82
|
|
|
82
83
|
|
|
83
84
|
class EvaluationSettingsClient:
|
|
84
|
-
"""Thin wrapper surfaced as client.evaluations.settings
|
|
85
|
+
"""Thin wrapper surfaced as ``client.evaluations.settings``.
|
|
86
|
+
|
|
87
|
+
Note: an evaluation-settings record is the judge rubric + OFFLINE profile of an
|
|
88
|
+
**LLM Judge Scorer** - the unified entity at ``client.monitor.judge_scorers``, which also
|
|
89
|
+
carries the optional online (live-traffic) profile. This client keeps working unchanged;
|
|
90
|
+
prefer ``judge_scorers`` for new code so both profiles live in one place -
|
|
91
|
+
``client.monitor.judge_scorers.builder(...)`` has the same snake_case ergonomics as the
|
|
92
|
+
builder below, plus tool_context, thresholds, and the live profile in one call."""
|
|
85
93
|
|
|
86
94
|
def __init__(self, client: "EvaluationsClient"):
|
|
87
95
|
self._client = client
|
|
96
|
+
# Soft deprecation: hidden by default (DeprecationWarning), visible under -W or pytest.
|
|
97
|
+
warnings.warn(
|
|
98
|
+
"client.evaluations.settings is the legacy view of an LLM Judge Scorer's offline "
|
|
99
|
+
"profile; prefer client.monitor.judge_scorers, which manages the judge rubric, "
|
|
100
|
+
"offline profile, and online (live-traffic) profile as one entity.",
|
|
101
|
+
DeprecationWarning,
|
|
102
|
+
stacklevel=3,
|
|
103
|
+
)
|
|
88
104
|
|
|
89
105
|
def builder(
|
|
90
106
|
self,
|
agentx/evaluations/models.py
CHANGED
|
@@ -94,7 +94,7 @@ class Dataset(BaseModel):
|
|
|
94
94
|
|
|
95
95
|
class EvaluationSettings(BaseModel):
|
|
96
96
|
"""A standalone, reusable grading config - no dataset/questions attached.
|
|
97
|
-
Created via ``client.
|
|
97
|
+
Created via ``client.monitor.judge_scorers.builder(...).publish()`` (or the legacy ``client.evaluations.settings.builder``) and run
|
|
98
98
|
against any dataset by passing its id as ``evaluation_settings_id`` to
|
|
99
99
|
``client.evaluations.run(...)``."""
|
|
100
100
|
|
agentx/evaluations/runner.py
CHANGED
|
@@ -421,10 +421,15 @@ class EvaluationsRunner:
|
|
|
421
421
|
def __init__(self, client: EvaluationsClient):
|
|
422
422
|
self._client = client
|
|
423
423
|
self.datasets = client.datasets
|
|
424
|
-
self.settings = client.settings
|
|
425
424
|
self.prompts = client.prompts
|
|
426
425
|
self.tool_schemas = client.tool_schemas
|
|
427
426
|
|
|
427
|
+
@property
|
|
428
|
+
def settings(self):
|
|
429
|
+
# Deferred: touching client.settings eagerly here would fire its legacy-surface
|
|
430
|
+
# DeprecationWarning for every runner, used or not.
|
|
431
|
+
return self._client.settings
|
|
432
|
+
|
|
428
433
|
def list_models(self, provider: Optional[str] = None) -> List[ModelInfo]:
|
|
429
434
|
"""List the LLM models AgentX supports - the same set selectable for
|
|
430
435
|
the Sovereignty & Portability Index. Pass ``provider`` (e.g. "Google")
|
|
@@ -482,24 +487,24 @@ class EvaluationsRunner:
|
|
|
482
487
|
self,
|
|
483
488
|
dataset_id: str,
|
|
484
489
|
subject: Union[Dict[str, Any], EvaluationSubject],
|
|
490
|
+
scorer_id: Optional[str] = None,
|
|
485
491
|
evaluation_settings_id: Optional[str] = None,
|
|
486
492
|
) -> EvaluationRunContext:
|
|
487
|
-
"""Start a run of ``dataset_id`` against ``subject``. Pass
|
|
488
|
-
``
|
|
489
|
-
|
|
490
|
-
|
|
493
|
+
"""Start a run of ``dataset_id`` against ``subject``. Pass ``scorer_id`` (an LLM Judge
|
|
494
|
+
Scorer's id, e.g. from ``client.monitor.judge_scorers``) to grade with a specific
|
|
495
|
+
scorer instead of the dataset's default. ``evaluation_settings_id`` is the
|
|
496
|
+
pre-consolidation alias for the same id and keeps working."""
|
|
497
|
+
from agentx.evaluations.client import _resolve_scorer_id
|
|
498
|
+
|
|
491
499
|
if isinstance(subject, dict):
|
|
492
500
|
subject = EvaluationSubject(**subject)
|
|
493
501
|
|
|
502
|
+
grader_id = _resolve_scorer_id(scorer_id, evaluation_settings_id)
|
|
494
503
|
dataset = self._client.get_dataset(dataset_id)
|
|
495
504
|
evaluation_settings = (
|
|
496
|
-
self._client.get_evaluation_settings(
|
|
497
|
-
if evaluation_settings_id
|
|
498
|
-
else None
|
|
499
|
-
)
|
|
500
|
-
run = self._client.init_run(
|
|
501
|
-
dataset_id, subject, evaluation_settings_id=evaluation_settings_id
|
|
505
|
+
self._client.get_evaluation_settings(grader_id) if grader_id else None
|
|
502
506
|
)
|
|
507
|
+
run = self._client.init_run(dataset_id, subject, scorer_id=grader_id)
|
|
503
508
|
logger.info(
|
|
504
509
|
"Started evaluation run %s on dataset %s (%d case(s), %d repetition(s))",
|
|
505
510
|
run.run_id,
|
agentx/export.py
CHANGED
|
@@ -36,13 +36,15 @@ class ExportClient:
|
|
|
36
36
|
(``pg_dump`` / SQLite file copy). There is deliberately no blind row-import endpoint.
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
39
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
40
40
|
self._api_key = api_key
|
|
41
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
42
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
41
43
|
|
|
42
44
|
def manifest(self) -> List[Dict[str, Any]]:
|
|
43
45
|
"""The exportable entities with live row counts: ``[{entity, rows, path}, ...]``."""
|
|
44
46
|
resp = requests.get(
|
|
45
|
-
f"{
|
|
47
|
+
f"{self._base_url}/export", headers=get_headers(self._api_key), timeout=30
|
|
46
48
|
)
|
|
47
49
|
if resp.status_code >= 400:
|
|
48
50
|
raise AgentXExportError(f"Export manifest failed ({resp.status_code}): {resp.text[:200]}")
|
|
@@ -54,7 +56,7 @@ class ExportClient:
|
|
|
54
56
|
timestamp column, e.g. ``createdAt`` for traces, ``lastSeenAt`` for signals)."""
|
|
55
57
|
params = {"since": since} if since else None
|
|
56
58
|
resp = requests.get(
|
|
57
|
-
f"{
|
|
59
|
+
f"{self._base_url}/export/{entity}",
|
|
58
60
|
headers=get_headers(self._api_key),
|
|
59
61
|
params=params,
|
|
60
62
|
stream=True,
|
agentx/feedback.py
CHANGED
|
@@ -26,8 +26,10 @@ class FeedbackClient:
|
|
|
26
26
|
reopened", reported by a workflow); feedback is a human vote with up/down semantics.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
29
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
30
30
|
self._api_key = api_key
|
|
31
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
32
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
31
33
|
|
|
32
34
|
def report(
|
|
33
35
|
self,
|
|
@@ -61,7 +63,7 @@ class FeedbackClient:
|
|
|
61
63
|
payload["endUserId"] = end_user_id
|
|
62
64
|
|
|
63
65
|
resp = requests.post(
|
|
64
|
-
f"{
|
|
66
|
+
f"{self._base_url}/feedback",
|
|
65
67
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
66
68
|
json=payload,
|
|
67
69
|
timeout=10,
|
|
@@ -79,7 +81,7 @@ class FeedbackClient:
|
|
|
79
81
|
"""All votes recorded on one trace, oldest first (``GET /feedback/trace/:traceId``) -
|
|
80
82
|
the same rows the dashboard's trace dialog shows as up/down chips."""
|
|
81
83
|
resp = requests.get(
|
|
82
|
-
f"{
|
|
84
|
+
f"{self._base_url}/feedback/trace/{trace_id}",
|
|
83
85
|
headers=get_headers(self._api_key),
|
|
84
86
|
timeout=10,
|
|
85
87
|
)
|
agentx/monitor/client.py
CHANGED
|
@@ -85,9 +85,18 @@ class MonitorClient:
|
|
|
85
85
|
from agentx.monitor.scorers import ScorersClient
|
|
86
86
|
# Scorers-catalog administration as code: template enable/disable, code/external scorer
|
|
87
87
|
# CRUD and dry runs - full parity with the dashboard's Scorers page (P1.3).
|
|
88
|
-
|
|
88
|
+
# Handed this client's own resolved API root, never the process-global default, so a
|
|
89
|
+
# second AgentX() with a different base_url can't re-point it (deep-dive bug #1).
|
|
90
|
+
self.scorers = ScorersClient(api_key=api_key, base_url=self._api_root())
|
|
91
|
+
from agentx.monitor.judge_scorers import JudgeScorersClient
|
|
92
|
+
# The unified LLM Judge Scorer (rubric + offline/online profiles in one entity) - the
|
|
93
|
+
# surface that matches the product; evaluations.settings and online_evaluators below
|
|
94
|
+
# remain as its profile-level views.
|
|
95
|
+
self.judge_scorers = JudgeScorersClient(api_key=api_key, base_url=self._api_root())
|
|
89
96
|
self.profile = MonitorProfileClient(self)
|
|
90
|
-
|
|
97
|
+
# Legacy view of an LLM Judge Scorer's online profile - constructed lazily so its
|
|
98
|
+
# DeprecationWarning fires on first USE, not for every client that never touches it.
|
|
99
|
+
self._online_evaluators: "MonitorOnlineEvaluatorClient | None" = None
|
|
91
100
|
from agentx.monitor.sessions import MonitorSessionClient
|
|
92
101
|
from agentx.monitor.agents import MonitorAgentClient
|
|
93
102
|
self.sessions = MonitorSessionClient(self)
|
|
@@ -171,6 +180,14 @@ class MonitorClient:
|
|
|
171
180
|
data = self._request("POST", "/online-evaluators", json=self._with_workspace(payload))
|
|
172
181
|
return MonitorOnlineEvaluator(**data["evaluator"])
|
|
173
182
|
|
|
183
|
+
@property
|
|
184
|
+
def online_evaluators(self) -> "MonitorOnlineEvaluatorClient":
|
|
185
|
+
if self._online_evaluators is None:
|
|
186
|
+
from agentx.monitor.online_evaluators import MonitorOnlineEvaluatorClient
|
|
187
|
+
|
|
188
|
+
self._online_evaluators = MonitorOnlineEvaluatorClient(self)
|
|
189
|
+
return self._online_evaluators
|
|
190
|
+
|
|
174
191
|
def list_online_evaluators(self) -> List[MonitorOnlineEvaluator]:
|
|
175
192
|
data = self._request("GET", "/online-evaluators", params=self._workspace_params())
|
|
176
193
|
return [MonitorOnlineEvaluator(**e) for e in data.get("evaluators", [])]
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from agentx.util import api_base, get_headers
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_SENTINEL: Any = object()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentXJudgeScorersError(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JudgeScorer(dict):
|
|
20
|
+
"""Wire object for the unified LLM Judge Scorer (dict subclass so unknown fields
|
|
21
|
+
round-trip). Convenience properties expose the pieces scripts reach for most."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def id(self) -> str:
|
|
25
|
+
return self["_id"]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return self["name"]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def judge(self) -> Dict[str, Any]:
|
|
33
|
+
return self.get("judge", {})
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def offline(self) -> Dict[str, Any]:
|
|
37
|
+
return self.get("offline", {})
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def online(self) -> Optional[Dict[str, Any]]:
|
|
41
|
+
return self.get("online")
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def online_profile_id(self) -> Optional[str]:
|
|
45
|
+
online = self.online
|
|
46
|
+
return online.get("profileId") if online else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class JudgeScorersClient:
|
|
50
|
+
"""Surfaced as ``client.monitor.judge_scorers``: the unified LLM Judge Scorer.
|
|
51
|
+
|
|
52
|
+
One scorer = one judge rubric (``judge``: acceptance/rejection/evaluation criteria, judge
|
|
53
|
+
prompt, judge model) + two setting profiles:
|
|
54
|
+
|
|
55
|
+
- ``offline`` - how dataset runs grade with it (repetitions, similarity metrics, code
|
|
56
|
+
scorers, default/status). The scorer's ``id`` is exactly what
|
|
57
|
+
``client.evaluations.run(dataset_id, scorer_id=scorer.id)`` takes.
|
|
58
|
+
- ``online`` - whether/how it scores live traffic (enabled, sample rate, scope,
|
|
59
|
+
alert threshold). ``None`` means offline-only.
|
|
60
|
+
|
|
61
|
+
Strictly one online profile per scorer. This supersedes the split between
|
|
62
|
+
``client.evaluations.settings`` (the offline half) and ``client.monitor.online_evaluators``
|
|
63
|
+
(the online half) - both keep working, but this is the surface that matches the product.
|
|
64
|
+
|
|
65
|
+
Example::
|
|
66
|
+
|
|
67
|
+
scorer = client.monitor.judge_scorers.create(
|
|
68
|
+
"Support quality",
|
|
69
|
+
judge={"acceptanceCriteria": "Concrete, correct, cites the policy."},
|
|
70
|
+
online={"enabled": True, "sampleRate": 0.2, "alertThreshold": 6},
|
|
71
|
+
)
|
|
72
|
+
client.evaluations.run(dataset_id, subject, scorer_id=scorer.id)
|
|
73
|
+
cal = client.monitor.judge_scorers.calibration(scorer.id)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
77
|
+
self._api_key = api_key
|
|
78
|
+
# Captured once at construction so two clients with different bases can coexist.
|
|
79
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
80
|
+
|
|
81
|
+
def _request(self, method: str, path: str, json: Any = None, timeout: int = 60) -> Any:
|
|
82
|
+
resp = requests.request(
|
|
83
|
+
method,
|
|
84
|
+
f"{self._base_url}/agent-monitoring{path}",
|
|
85
|
+
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
86
|
+
json=json,
|
|
87
|
+
timeout=timeout,
|
|
88
|
+
)
|
|
89
|
+
if resp.status_code >= 400:
|
|
90
|
+
try:
|
|
91
|
+
detail = resp.json().get("error", resp.reason)
|
|
92
|
+
except ValueError:
|
|
93
|
+
detail = resp.reason
|
|
94
|
+
raise AgentXJudgeScorersError(f"Judge scorer request failed ({resp.status_code}): {detail}")
|
|
95
|
+
return resp.json() if resp.text else {}
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# CRUD
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def builder(
|
|
102
|
+
self,
|
|
103
|
+
name: str,
|
|
104
|
+
description: Optional[str] = None,
|
|
105
|
+
*,
|
|
106
|
+
# Judge rubric (shared by every surface that runs this scorer)
|
|
107
|
+
acceptance_criteria: Optional[str] = None,
|
|
108
|
+
rejection_criteria: Optional[str] = None,
|
|
109
|
+
evaluation_criteria: Optional[str] = None,
|
|
110
|
+
judge_prompt: Optional[str] = None,
|
|
111
|
+
judge_model: Optional[str] = None,
|
|
112
|
+
tool_context: Optional[str] = None,
|
|
113
|
+
# Offline profile (dataset-run grading)
|
|
114
|
+
number_of_requests: int = 1,
|
|
115
|
+
vector_similarity: bool = False,
|
|
116
|
+
jaccard_similarity: bool = False,
|
|
117
|
+
bleu_score: bool = False,
|
|
118
|
+
rouge_score: bool = False,
|
|
119
|
+
similarity_model: Optional[str] = None,
|
|
120
|
+
sovereignty_models: Optional[List[str]] = None,
|
|
121
|
+
thresholds: Optional[Dict[str, Any]] = None,
|
|
122
|
+
code_scorers: Optional[List[Dict[str, Any]]] = None,
|
|
123
|
+
is_default: bool = False,
|
|
124
|
+
# Online profile (live traffic). live=True enables scoring immediately - every check is
|
|
125
|
+
# a real judge call on your own key.
|
|
126
|
+
live: bool = False,
|
|
127
|
+
sample_rate: float = 0.1,
|
|
128
|
+
scope: str = "trace",
|
|
129
|
+
alert_threshold: Optional[float] = 5,
|
|
130
|
+
severity: str = "medium",
|
|
131
|
+
agent_ids: Optional[List[str]] = None,
|
|
132
|
+
idle_seconds: int = 120,
|
|
133
|
+
) -> "JudgeScorerBuilder":
|
|
134
|
+
"""Snake_case builder with ``.publish()``, the unified successor of
|
|
135
|
+
``client.evaluations.settings.builder(...)`` - same offline fields (plus ``thresholds``,
|
|
136
|
+
``tool_context``) and, new here, the online profile in the same call. The scorer the
|
|
137
|
+
builder publishes is one entity: its ``.id`` is what ``client.evaluations.run(...,
|
|
138
|
+
scorer_id=...)`` takes, and its live profile is what online scoring keys on."""
|
|
139
|
+
judge: Dict[str, Any] = {}
|
|
140
|
+
for key, value in (
|
|
141
|
+
("acceptanceCriteria", acceptance_criteria),
|
|
142
|
+
("rejectionCriteria", rejection_criteria),
|
|
143
|
+
("evaluationCriteria", evaluation_criteria),
|
|
144
|
+
("judgePrompt", judge_prompt),
|
|
145
|
+
("judgeModel", judge_model),
|
|
146
|
+
("toolContext", tool_context),
|
|
147
|
+
):
|
|
148
|
+
if value is not None:
|
|
149
|
+
judge[key] = value
|
|
150
|
+
offline: Dict[str, Any] = {"numberOfRequests": number_of_requests, "isDefault": is_default}
|
|
151
|
+
if vector_similarity:
|
|
152
|
+
offline["vectorSimilarity"] = (
|
|
153
|
+
{"enabled": True, "model": similarity_model} if similarity_model else {"enabled": True}
|
|
154
|
+
)
|
|
155
|
+
if jaccard_similarity:
|
|
156
|
+
offline["jaccardSimilarity"] = {"enabled": True}
|
|
157
|
+
if bleu_score:
|
|
158
|
+
offline["bleuScore"] = {"enabled": True}
|
|
159
|
+
if rouge_score:
|
|
160
|
+
offline["rougeScore"] = {"enabled": True}
|
|
161
|
+
if sovereignty_models is not None:
|
|
162
|
+
offline["sovereigntyIndex"] = {"enabled": bool(sovereignty_models), "models": sovereignty_models}
|
|
163
|
+
if thresholds is not None:
|
|
164
|
+
offline["thresholds"] = thresholds
|
|
165
|
+
if code_scorers is not None:
|
|
166
|
+
offline["codeScorers"] = code_scorers
|
|
167
|
+
online: Optional[Dict[str, Any]] = None
|
|
168
|
+
if live:
|
|
169
|
+
online = {
|
|
170
|
+
"enabled": True,
|
|
171
|
+
"sampleRate": sample_rate,
|
|
172
|
+
"scope": scope,
|
|
173
|
+
"alertThreshold": alert_threshold,
|
|
174
|
+
"severity": severity,
|
|
175
|
+
"idleSeconds": idle_seconds,
|
|
176
|
+
}
|
|
177
|
+
if agent_ids:
|
|
178
|
+
online["scopeMode"] = "selected"
|
|
179
|
+
online["agentIds"] = agent_ids
|
|
180
|
+
return JudgeScorerBuilder(self, name=name, description=description, judge=judge, offline=offline, online=online)
|
|
181
|
+
|
|
182
|
+
def create(
|
|
183
|
+
self,
|
|
184
|
+
name: str,
|
|
185
|
+
*,
|
|
186
|
+
description: Optional[str] = None,
|
|
187
|
+
judge: Optional[Dict[str, Any]] = None,
|
|
188
|
+
offline: Optional[Dict[str, Any]] = None,
|
|
189
|
+
online: Optional[Dict[str, Any]] = None,
|
|
190
|
+
) -> JudgeScorer:
|
|
191
|
+
"""Create a scorer. ``online=None`` (default) creates it offline-only; pass e.g.
|
|
192
|
+
``online={"enabled": True, "sampleRate": 0.1, "alertThreshold": 5}`` to score live
|
|
193
|
+
traffic from the start. Section dicts use the wire's camelCase keys."""
|
|
194
|
+
payload: Dict[str, Any] = {"name": name}
|
|
195
|
+
if description is not None:
|
|
196
|
+
payload["description"] = description
|
|
197
|
+
if judge is not None:
|
|
198
|
+
payload["judge"] = judge
|
|
199
|
+
if offline is not None:
|
|
200
|
+
payload["offline"] = offline
|
|
201
|
+
if online is not None:
|
|
202
|
+
payload["online"] = online
|
|
203
|
+
return JudgeScorer(self._request("POST", "/judge-scorers", json=payload)["judgeScorer"])
|
|
204
|
+
|
|
205
|
+
def get(self, scorer_id: str) -> JudgeScorer:
|
|
206
|
+
return JudgeScorer(self._request("GET", f"/judge-scorers/{scorer_id}")["judgeScorer"])
|
|
207
|
+
|
|
208
|
+
def list(self) -> List[JudgeScorer]:
|
|
209
|
+
return [JudgeScorer(s) for s in self._request("GET", "/judge-scorers").get("judgeScorers", [])]
|
|
210
|
+
|
|
211
|
+
def update(
|
|
212
|
+
self,
|
|
213
|
+
scorer_id: str,
|
|
214
|
+
*,
|
|
215
|
+
name: Optional[str] = None,
|
|
216
|
+
description: Optional[str] = None,
|
|
217
|
+
judge: Optional[Dict[str, Any]] = None,
|
|
218
|
+
offline: Optional[Dict[str, Any]] = None,
|
|
219
|
+
online: Any = _SENTINEL,
|
|
220
|
+
) -> JudgeScorer:
|
|
221
|
+
"""Sparse update: only the sections you pass change. ``online={...}`` upserts the
|
|
222
|
+
online profile (this is how an offline-only scorer goes live); ``online=None``
|
|
223
|
+
detaches it (refused for the built-in Session Baseline Judge)."""
|
|
224
|
+
payload: Dict[str, Any] = {}
|
|
225
|
+
if name is not None:
|
|
226
|
+
payload["name"] = name
|
|
227
|
+
if description is not None:
|
|
228
|
+
payload["description"] = description
|
|
229
|
+
if judge is not None:
|
|
230
|
+
payload["judge"] = judge
|
|
231
|
+
if offline is not None:
|
|
232
|
+
payload["offline"] = offline
|
|
233
|
+
if online is not _SENTINEL:
|
|
234
|
+
payload["online"] = online
|
|
235
|
+
return JudgeScorer(self._request("PUT", f"/judge-scorers/{scorer_id}", json=payload)["judgeScorer"])
|
|
236
|
+
|
|
237
|
+
def delete(self, scorer_id: str) -> None:
|
|
238
|
+
"""Delete the scorer: rubric, version history, and online profile together.
|
|
239
|
+
Irreversible; refused for the built-in Session Baseline Judge."""
|
|
240
|
+
self._request("DELETE", f"/judge-scorers/{scorer_id}")
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------
|
|
243
|
+
# Online-profile pass-throughs (calibration / tuning / ratings / events)
|
|
244
|
+
# These endpoints key on the online profile's id, resolved here so callers only ever
|
|
245
|
+
# handle the scorer id.
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def _profile_id(self, scorer_id: str) -> str:
|
|
249
|
+
scorer = self.get(scorer_id)
|
|
250
|
+
profile_id = scorer.online_profile_id
|
|
251
|
+
if not profile_id:
|
|
252
|
+
raise AgentXJudgeScorersError(
|
|
253
|
+
f"Judge scorer {scorer_id!r} has no online profile - calibration/tuning/ratings "
|
|
254
|
+
"cover live-traffic scoring. Give it one first: "
|
|
255
|
+
"update(scorer_id, online={'enabled': True})."
|
|
256
|
+
)
|
|
257
|
+
return profile_id
|
|
258
|
+
|
|
259
|
+
def calibration(self, scorer_id: str, window: str = "7d") -> dict:
|
|
260
|
+
"""How this scorer's verdicts compare against recorded ground truth (triage
|
|
261
|
+
corrections, outcomes, end-user votes) over the window."""
|
|
262
|
+
return self._request("GET", f"/online-evaluators/{self._profile_id(scorer_id)}/calibration?window={window}")
|
|
263
|
+
|
|
264
|
+
def tune(self, scorer_id: str, window: str = "7d") -> dict:
|
|
265
|
+
"""Propose a rewrite of the rubric from calibration disagreements (LLM call, slow)."""
|
|
266
|
+
data = self._request(
|
|
267
|
+
"POST", f"/online-evaluators/{self._profile_id(scorer_id)}/tune", json={"window": window}, timeout=300
|
|
268
|
+
)
|
|
269
|
+
# The wire wraps the proposal ({"proposal": {...}}); unwrap like the legacy client so
|
|
270
|
+
# proposal["reasoning"] / the criteria fields are directly addressable.
|
|
271
|
+
return data.get("proposal", data) if isinstance(data, dict) else data
|
|
272
|
+
|
|
273
|
+
def validate_tuning(self, scorer_id: str, criteria: Dict[str, Any], window: str = "7d") -> dict:
|
|
274
|
+
"""Re-judge the disagreement + control cases with candidate criteria (LLM calls, slow)."""
|
|
275
|
+
# The wire takes the criteria fields at the TOP level of the body, not nested.
|
|
276
|
+
return self._request(
|
|
277
|
+
"POST",
|
|
278
|
+
f"/online-evaluators/{self._profile_id(scorer_id)}/tune/validate",
|
|
279
|
+
json={**criteria, "window": window},
|
|
280
|
+
timeout=600,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def publish_tuning(self, scorer_id: str, criteria: Dict[str, Any]) -> dict:
|
|
284
|
+
"""Write tuned criteria onto the scorer's rubric - it applies everywhere the scorer is
|
|
285
|
+
used: online scoring, offline dataset runs, and the playground."""
|
|
286
|
+
return self._request(
|
|
287
|
+
"POST", f"/online-evaluators/{self._profile_id(scorer_id)}/tune/publish", json=dict(criteria)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def ratings(self, scorer_id: str, window: str = "7d") -> "List[OnlineEvaluatorRatingPoint]":
|
|
291
|
+
"""Bucketed average-rating-over-time for this scorer's live checks - same typed points
|
|
292
|
+
the legacy online_evaluators client returns, so scripts migrate without shape changes."""
|
|
293
|
+
from agentx.monitor.models import OnlineEvaluatorRatingPoint
|
|
294
|
+
|
|
295
|
+
data = self._request("GET", f"/online-evaluators/{self._profile_id(scorer_id)}/ratings?window={window}")
|
|
296
|
+
return [OnlineEvaluatorRatingPoint(**p) for p in data.get("points", [])]
|
|
297
|
+
|
|
298
|
+
def events(self, scorer_id: str, window: str = "7d") -> "List[OnlineEvaluatorEvent]":
|
|
299
|
+
"""Individually scored traces behind the ratings series, worst-rated first - typed, same
|
|
300
|
+
as the legacy online_evaluators client."""
|
|
301
|
+
from agentx.monitor.models import OnlineEvaluatorEvent
|
|
302
|
+
|
|
303
|
+
data = self._request("GET", f"/online-evaluators/{self._profile_id(scorer_id)}/events?window={window}")
|
|
304
|
+
return [OnlineEvaluatorEvent(**e) for e in data.get("events", [])]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class JudgeScorerBuilder:
|
|
308
|
+
"""Assembled by :meth:`JudgeScorersClient.builder`; ``.publish()`` creates the scorer."""
|
|
309
|
+
|
|
310
|
+
def __init__(
|
|
311
|
+
self,
|
|
312
|
+
client: "JudgeScorersClient",
|
|
313
|
+
*,
|
|
314
|
+
name: str,
|
|
315
|
+
description: Optional[str],
|
|
316
|
+
judge: Dict[str, Any],
|
|
317
|
+
offline: Dict[str, Any],
|
|
318
|
+
online: Optional[Dict[str, Any]],
|
|
319
|
+
):
|
|
320
|
+
self._client = client
|
|
321
|
+
self._name = name
|
|
322
|
+
self._description = description
|
|
323
|
+
self._judge = judge
|
|
324
|
+
self._offline = offline
|
|
325
|
+
self._online = online
|
|
326
|
+
|
|
327
|
+
def publish(self) -> JudgeScorer:
|
|
328
|
+
return self._client.create(
|
|
329
|
+
self._name,
|
|
330
|
+
description=self._description,
|
|
331
|
+
judge=self._judge or None,
|
|
332
|
+
offline=self._offline or None,
|
|
333
|
+
online=self._online,
|
|
334
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import warnings
|
|
4
5
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from agentx.monitor.models import MonitorOnlineEvaluator, OnlineEvaluatorRatingPoint, OnlineEvaluatorEvent
|
|
@@ -17,7 +18,12 @@ class MonitorOnlineEvaluatorBuilder:
|
|
|
17
18
|
``evaluation_settings_id`` must reference an existing Evaluator config (criteria, judge
|
|
18
19
|
prompt, judge model), the same config datasets/Evaluate runs use, see
|
|
19
20
|
``client.evaluations.settings.builder(...)``.
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
Note: an online evaluator is the ONLINE profile of an **LLM Judge Scorer** - the unified
|
|
23
|
+
entity at ``client.monitor.judge_scorers``. Strictly one profile per config since the
|
|
24
|
+
unification: binding a config that is already another evaluator's profile transparently
|
|
25
|
+
binds a fresh copy of it instead (the response's ``evaluationSettingsId`` is the copy).
|
|
26
|
+
This surface keeps working unchanged; prefer ``judge_scorers`` for new code."""
|
|
21
27
|
|
|
22
28
|
def __init__(
|
|
23
29
|
self,
|
|
@@ -60,10 +66,23 @@ class MonitorOnlineEvaluatorBuilder:
|
|
|
60
66
|
|
|
61
67
|
|
|
62
68
|
class MonitorOnlineEvaluatorClient:
|
|
63
|
-
"""Thin wrapper surfaced as ``client.monitor.online_evaluators``.
|
|
69
|
+
"""Thin wrapper surfaced as ``client.monitor.online_evaluators``.
|
|
70
|
+
|
|
71
|
+
Note: an online evaluator is the ONLINE (live-traffic) profile of an **LLM Judge Scorer** -
|
|
72
|
+
the unified entity at ``client.monitor.judge_scorers``, which also carries the judge rubric
|
|
73
|
+
and the offline profile. This client keeps working unchanged; prefer ``judge_scorers`` for
|
|
74
|
+
new code so both profiles live in one place."""
|
|
64
75
|
|
|
65
76
|
def __init__(self, client: "MonitorClient"):
|
|
66
77
|
self._client = client
|
|
78
|
+
# Soft deprecation: hidden by default (DeprecationWarning), visible under -W or pytest.
|
|
79
|
+
warnings.warn(
|
|
80
|
+
"client.monitor.online_evaluators is the legacy view of an LLM Judge Scorer's "
|
|
81
|
+
"online profile; prefer client.monitor.judge_scorers, which manages the judge "
|
|
82
|
+
"rubric, offline profile, and online profile as one entity.",
|
|
83
|
+
DeprecationWarning,
|
|
84
|
+
stacklevel=3,
|
|
85
|
+
)
|
|
67
86
|
|
|
68
87
|
def builder(
|
|
69
88
|
self,
|
agentx/monitor/scorers.py
CHANGED
|
@@ -33,13 +33,15 @@ class ScorersClient:
|
|
|
33
33
|
wire.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
36
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
37
37
|
self._api_key = api_key
|
|
38
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
39
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
38
40
|
|
|
39
41
|
def _request(self, method: str, path: str, json: Any = None, params: Any = None) -> Any:
|
|
40
42
|
resp = requests.request(
|
|
41
43
|
method,
|
|
42
|
-
f"{
|
|
44
|
+
f"{self._base_url}/agent-monitoring{path}",
|
|
43
45
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
44
46
|
json=json,
|
|
45
47
|
params=params,
|
agentx/outcomes.py
CHANGED
|
@@ -26,8 +26,10 @@ class OutcomesClient:
|
|
|
26
26
|
tracker's webhook, a CRM workflow), and this method is the SDK's way to be that caller.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
29
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
30
30
|
self._api_key = api_key
|
|
31
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
32
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
31
33
|
|
|
32
34
|
def report(
|
|
33
35
|
self,
|
|
@@ -69,7 +71,7 @@ class OutcomesClient:
|
|
|
69
71
|
payload["reportedBy"] = reported_by
|
|
70
72
|
|
|
71
73
|
resp = requests.post(
|
|
72
|
-
f"{
|
|
74
|
+
f"{self._base_url}/outcomes",
|
|
73
75
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
74
76
|
json=payload,
|
|
75
77
|
timeout=10,
|
agentx/projects.py
CHANGED
|
@@ -25,13 +25,16 @@ class ProjectsClient:
|
|
|
25
25
|
users; this client covers the default self-host (auth-disabled) mode.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
28
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
29
29
|
self._api_key = api_key
|
|
30
|
+
# Captured once at construction so two clients with different bases can coexist
|
|
31
|
+
# (deep-dive round 3, bug #1) - never read from process-global state per call.
|
|
32
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
30
33
|
|
|
31
34
|
def _request(self, method: str, path: str, json: Any = None) -> Any:
|
|
32
35
|
resp = requests.request(
|
|
33
36
|
method,
|
|
34
|
-
f"{
|
|
37
|
+
f"{self._base_url}{path}",
|
|
35
38
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
36
39
|
json=json,
|
|
37
40
|
timeout=15,
|
agentx/traces.py
CHANGED
|
@@ -24,12 +24,14 @@ class TracesClient:
|
|
|
24
24
|
span-tree read.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
27
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
28
28
|
self._api_key = api_key
|
|
29
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
30
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
29
31
|
|
|
30
32
|
def _request(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
31
33
|
resp = requests.get(
|
|
32
|
-
f"{
|
|
34
|
+
f"{self._base_url}{path}",
|
|
33
35
|
headers=get_headers(self._api_key),
|
|
34
36
|
params=params or {},
|
|
35
37
|
timeout=15,
|
agentx/tracing/ingest_client.py
CHANGED
|
@@ -72,6 +72,11 @@ class IngestClient:
|
|
|
72
72
|
# failures stay at DEBUG to avoid log spam. client.ping() is the fail-fast startup check.
|
|
73
73
|
self._delivery_warning_emitted = False
|
|
74
74
|
|
|
75
|
+
# Dropped-trace accounting (deep-dive round 3, bug #5): fire-and-forget may drop traces
|
|
76
|
+
# (queue overflow, retries exhausted while the engine is down), but it must never drop
|
|
77
|
+
# them SILENTLY. First drop warns, then every 50th, with the cumulative count.
|
|
78
|
+
self._dropped = 0
|
|
79
|
+
|
|
75
80
|
self._queue: queue.Queue[Optional[Dict[str, Any]]] = queue.Queue(maxsize=_QUEUE_MAX)
|
|
76
81
|
self._worker = threading.Thread(target=self._drain, daemon=True, name="agentx-ingest")
|
|
77
82
|
self._worker.start()
|
|
@@ -90,11 +95,44 @@ class IngestClient:
|
|
|
90
95
|
try:
|
|
91
96
|
self._queue.put_nowait(payload)
|
|
92
97
|
except queue.Full:
|
|
93
|
-
|
|
98
|
+
self._record_drop("queue full")
|
|
99
|
+
|
|
100
|
+
def flush(self, timeout: float = 5.0) -> bool:
|
|
101
|
+
"""Block until all queued traces have been sent, or until ``timeout`` seconds elapse.
|
|
94
102
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
Returns ``True`` when the queue fully drained, ``False`` on deadline - undelivered
|
|
104
|
+
traces stay queued and keep sending in the background. The old implementation called
|
|
105
|
+
``queue.join()``, which has no deadline at all: with the engine down, each queued item
|
|
106
|
+
burned the full retry backoff and a ``flush(timeout=3)`` measurably blocked for over
|
|
107
|
+
two minutes (deep-dive round 3, bug #5). The timeout is now a real wall-clock bound.
|
|
108
|
+
"""
|
|
109
|
+
deadline = time.time() + timeout
|
|
110
|
+
with self._queue.all_tasks_done:
|
|
111
|
+
while self._queue.unfinished_tasks:
|
|
112
|
+
remaining = deadline - time.time()
|
|
113
|
+
if remaining <= 0:
|
|
114
|
+
logger.warning(
|
|
115
|
+
"agentx flush(%.1fs) timed out with %d trace(s) still undelivered - "
|
|
116
|
+
"they remain queued and keep sending in the background",
|
|
117
|
+
timeout,
|
|
118
|
+
self._queue.unfinished_tasks,
|
|
119
|
+
)
|
|
120
|
+
return False
|
|
121
|
+
self._queue.all_tasks_done.wait(remaining)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def _record_drop(self, reason: str) -> None:
|
|
125
|
+
self._dropped += 1
|
|
126
|
+
if self._dropped == 1 or self._dropped % 50 == 0:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"agentx dropped a trace (%s) - %d dropped total this process. "
|
|
129
|
+
"Tracing is fire-and-forget: traces that cannot be delivered are not persisted "
|
|
130
|
+
"locally. Use client.ping() at startup to fail fast on a bad endpoint.",
|
|
131
|
+
reason,
|
|
132
|
+
self._dropped,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
logger.debug("agentx dropped a trace (%s), %d total", reason, self._dropped)
|
|
98
136
|
|
|
99
137
|
def send_trace_sync(self, payload: Dict[str, Any]) -> Optional[str]:
|
|
100
138
|
"""
|
|
@@ -372,7 +410,9 @@ class IngestClient:
|
|
|
372
410
|
try:
|
|
373
411
|
self._send(payload)
|
|
374
412
|
except Exception as exc:
|
|
375
|
-
|
|
413
|
+
# Anything _send didn't classify still cost us this payload - count it as a
|
|
414
|
+
# drop rather than whispering at DEBUG (bug #5's silent half).
|
|
415
|
+
self._record_drop(f"unexpected error: {exc}")
|
|
376
416
|
finally:
|
|
377
417
|
self._queue.task_done()
|
|
378
418
|
|
|
@@ -397,4 +437,4 @@ class IngestClient:
|
|
|
397
437
|
return
|
|
398
438
|
|
|
399
439
|
self._warn_delivery(str(last_exc))
|
|
400
|
-
|
|
440
|
+
self._record_drop(f"retries exhausted: {last_exc}")
|
agentx/tracing/tracer.py
CHANGED
|
@@ -890,9 +890,11 @@ class Tracer:
|
|
|
890
890
|
agent_id=agent_id,
|
|
891
891
|
)
|
|
892
892
|
|
|
893
|
-
def flush(self, timeout: float = 5.0) ->
|
|
894
|
-
"""Block until all queued traces have been delivered
|
|
895
|
-
|
|
893
|
+
def flush(self, timeout: float = 5.0) -> bool:
|
|
894
|
+
"""Block until all queued traces have been delivered, or until ``timeout`` seconds
|
|
895
|
+
elapse. Returns ``True`` when everything drained, ``False`` on deadline (a warning is
|
|
896
|
+
logged and undelivered traces keep sending in the background)."""
|
|
897
|
+
return self._client.flush(timeout)
|
|
896
898
|
|
|
897
899
|
# ------------------------------------------------------------------
|
|
898
900
|
# CI/CD evaluation
|
agentx/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = "0.6.
|
|
1
|
+
VERSION = "0.6.36"
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
agentx/__init__.py,sha256=4iMiGLNU4S-I-WyX0Z7l0BFUY_krVJ3seMQPqyO-ReA,602
|
|
2
|
-
agentx/agentx.py,sha256=
|
|
2
|
+
agentx/agentx.py,sha256=WuzVQtXSPqBhRwKxkUlQ8VG2cwvGpbMcti4n6aTNPwA,8816
|
|
3
3
|
agentx/cli.py,sha256=ueKYPBhW8qd63nvQBQeZ4Mgz9NwEVaM0KH2WvLQ0bGs,6097
|
|
4
4
|
agentx/exceptions.py,sha256=2tDLdZjriFBQbjgzyIcah12RazpbujhqZSRbSXgAml4,1377
|
|
5
|
-
agentx/export.py,sha256=
|
|
6
|
-
agentx/feedback.py,sha256=
|
|
7
|
-
agentx/outcomes.py,sha256=
|
|
8
|
-
agentx/projects.py,sha256=
|
|
5
|
+
agentx/export.py,sha256=FNT00e-j9AQRkNDksUwgHJXgr8-XVPscEH4xUqJHmfM,4265
|
|
6
|
+
agentx/feedback.py,sha256=hMPU3ivUHvhOXOH7gvT4Jy1yztVmoIMOw1MbUNRs5Gk,3643
|
|
7
|
+
agentx/outcomes.py,sha256=yCA6PrO5BrGufEjkzYWTK5wusAOt6w0gSKkH-5x1Fs4,3462
|
|
8
|
+
agentx/projects.py,sha256=JN-nhesELPjEb1P0m9F4-Qw-0eGR68kCtBN5zNhArt0,2540
|
|
9
9
|
agentx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
agentx/traces.py,sha256=
|
|
10
|
+
agentx/traces.py,sha256=sz9gxutlDKNgf0fdCkyzKsGwK5eMAJuIEl3NIwGrsuo,2287
|
|
11
11
|
agentx/util.py,sha256=lt2Kpg4Fj8whrSuAhLEahcPEIYRVpK9RCC5y9ShdTJs,703
|
|
12
|
-
agentx/version.py,sha256=
|
|
12
|
+
agentx/version.py,sha256=Vjh1dxR-kRQ3D_9qSk_EH1XFCc8RMkJ2gALg30ZZ0ns,19
|
|
13
13
|
agentx/evaluations/__init__.py,sha256=Erv7RGFlRxGTG4rVb2uHhCqLLEX_iAWKqkZKzK6CumE,262
|
|
14
14
|
agentx/evaluations/_term.py,sha256=WFpiNzdgDBeJJ-Gg-6X7TwwxllobuE4OqFUTuDQvS3Y,2529
|
|
15
|
-
agentx/evaluations/client.py,sha256=
|
|
16
|
-
agentx/evaluations/datasets.py,sha256=
|
|
17
|
-
agentx/evaluations/evaluation_settings.py,sha256=
|
|
18
|
-
agentx/evaluations/models.py,sha256=
|
|
15
|
+
agentx/evaluations/client.py,sha256=Vo3pvD37f6Z8VgUm3DR0FzSmnO3qYcokGHmiTc8RzwU,27273
|
|
16
|
+
agentx/evaluations/datasets.py,sha256=7OYj5pXpJenViAhPe7Sr_7Sf9KWd7b1OhV-dPL2tso8,14283
|
|
17
|
+
agentx/evaluations/evaluation_settings.py,sha256=geRydHqKpxwkxBVfMIOqkB6NlStyOZrYwZnMHluDwvA,6276
|
|
18
|
+
agentx/evaluations/models.py,sha256=iXM1XReT1PLxjVjgdfMuomFOSAwFhHAY4qBxNjiKw88,28732
|
|
19
19
|
agentx/evaluations/prompts.py,sha256=8xyvMpAl3mD5xuSZozuaohdnCBWe5Q02RldwiPLUO20,3493
|
|
20
20
|
agentx/evaluations/reporting.py,sha256=GtNnL-1eNEQrSqd0yrHi5mGqC9_u0kUAN8Mbjdhw6yY,6389
|
|
21
21
|
agentx/evaluations/results.py,sha256=w7TRUik4eXIqrznXvlF_L9hnObnHvgdRKvihfN1XeAU,4692
|
|
22
|
-
agentx/evaluations/runner.py,sha256=
|
|
22
|
+
agentx/evaluations/runner.py,sha256=wXBUzYvdS_7dlcnIlJQXSWfjGdC0BfWiu4_Ki1rLYtg,26567
|
|
23
23
|
agentx/evaluations/tool_schemas.py,sha256=ZyrnSOnx9xlSAj4swfDey9ZLS2ly-g21DByTwRAGwNA,2323
|
|
24
24
|
agentx/evaluations/tracing.py,sha256=MSJD9bzfInMi7E70mc8mWUc0fx5DLkOgUzRru2mNQoc,1825
|
|
25
25
|
agentx/evaluations/adapters/__init__.py,sha256=fK8Hx75usbiY03XUSmnLnSrwBXip2CQs24YZcdEgj6w,287
|
|
@@ -42,12 +42,13 @@ agentx/integrations/openai.py,sha256=1KLs-hJaeW2emHPwNkmC3zcGksL3zd87uWbcLGRW704
|
|
|
42
42
|
agentx/integrations/openai_agents.py,sha256=tCa3kPxqux5LnNNDQ6zA05OybQrjkc_LeE08YuNPJEs,12742
|
|
43
43
|
agentx/monitor/__init__.py,sha256=MLiASSvQnCy2jqVNRTgPWFhUSf8lGYRQeEgEVVhE5Tg,574
|
|
44
44
|
agentx/monitor/agents.py,sha256=8Xk4jWmNTvdtjFixHiLmw4jj96uqIXWIT6KW8zXoWrc,1006
|
|
45
|
-
agentx/monitor/client.py,sha256=
|
|
45
|
+
agentx/monitor/client.py,sha256=BXaTDAsKJf_3uwrhZMKn4UqAz5Xr0qap-MH9a-CR7d8,16655
|
|
46
|
+
agentx/monitor/judge_scorers.py,sha256=40Xq60QASLW9z2QT2dSTmZhILGeBTfFGLrog5yeessM,14261
|
|
46
47
|
agentx/monitor/models.py,sha256=yTC3WTdTziHMkdeAXlTro-4YiSbBUHMgf9kNZHGfPDA,8470
|
|
47
|
-
agentx/monitor/online_evaluators.py,sha256=
|
|
48
|
+
agentx/monitor/online_evaluators.py,sha256=tTXqRgOb6ah39c5nWMEVXLxEUc8Tqu9E9SFdcHy0ExM,8418
|
|
48
49
|
agentx/monitor/patterns.py,sha256=ruBqLoA2T2KQuqRbbn1kWkIEkiOal3SvyPl9opGyhzI,4185
|
|
49
50
|
agentx/monitor/profile.py,sha256=uGk-4bXT2vmnEQiY5u6rWhAfzZchHBXL74VD3piVHvU,3102
|
|
50
|
-
agentx/monitor/scorers.py,sha256=
|
|
51
|
+
agentx/monitor/scorers.py,sha256=huIZBszrfLKI8I7VV6NFpW6piRsj9HI03rXUwWs2tgs,7336
|
|
51
52
|
agentx/monitor/sessions.py,sha256=gePcL284m3UITxQAtHxwrKFl6bRjmRib1QK6JbZeS9s,815
|
|
52
53
|
agentx/monitor/signals.py,sha256=Ld11lW2lbg8NHBUHdgoY6iwCvJDK-eg4ge4qQ9y7nGY,1432
|
|
53
54
|
agentx/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -56,11 +57,11 @@ agentx/resources/conversation.py,sha256=94pOdM6Pmj67RhikZVBNGkT3V-WKK_ZAD7s27p5-
|
|
|
56
57
|
agentx/resources/workforce.py,sha256=lfGVkoV9lcOp-lZScjTvwRarJMkhzfMfLm4syJDujGc,4168
|
|
57
58
|
agentx/tracing/__init__.py,sha256=l2mIRILE-wWrCD0fekgrIOQUEvtDObzdrkgpQUtXroU,408
|
|
58
59
|
agentx/tracing/ci_types.py,sha256=zFVcZGvc1qbVDdOlEPQ1i5R6terqJHxCzFF4ZjouxxI,1423
|
|
59
|
-
agentx/tracing/ingest_client.py,sha256=
|
|
60
|
-
agentx/tracing/tracer.py,sha256=
|
|
61
|
-
agentx_python-0.6.
|
|
62
|
-
agentx_python-0.6.
|
|
63
|
-
agentx_python-0.6.
|
|
64
|
-
agentx_python-0.6.
|
|
65
|
-
agentx_python-0.6.
|
|
66
|
-
agentx_python-0.6.
|
|
60
|
+
agentx/tracing/ingest_client.py,sha256=5X6RyxtMq1XYgOzsMd3b2f2qpmYpsMnhKm7NB3oup00,17664
|
|
61
|
+
agentx/tracing/tracer.py,sha256=6qLTrV4Fz_ONitlBMKiayRbeoV1dpkTwskTDP5oVt4I,51567
|
|
62
|
+
agentx_python-0.6.36.dist-info/licenses/LICENSE,sha256=gZVsM-nLsE8vlaY6NXXsVoo6IlCClxkToAWmhgT3y_s,10762
|
|
63
|
+
agentx_python-0.6.36.dist-info/METADATA,sha256=lqDEHXO2-wFeK4cx6hGiU1hI-cE_4t92idDVwvza8q8,20133
|
|
64
|
+
agentx_python-0.6.36.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
65
|
+
agentx_python-0.6.36.dist-info/entry_points.txt,sha256=rQqF1JTY3T1yfviBU1r8PU-5mBdfOk6P4bZ1L4BskIM,172
|
|
66
|
+
agentx_python-0.6.36.dist-info/top_level.txt,sha256=s-q-HB9Gb_QdrZNacSeQyF_c25gQooMy7DlxzgLOHPk,7
|
|
67
|
+
agentx_python-0.6.36.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|