agentx-python 0.8.15__tar.gz → 0.8.17__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.
- {agentx_python-0.8.15/agentx_python.egg-info → agentx_python-0.8.17}/PKG-INFO +1 -1
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/client.py +18 -1
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/datasets.py +1 -1
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/evaluation_settings.py +1 -1
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/models.py +9 -2
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/runner.py +25 -10
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/client.py +23 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/judge_scorers.py +5 -0
- agentx_python-0.8.17/agentx/monitor/scorer_groups.py +90 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/sessions.py +12 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/version.py +2 -2
- {agentx_python-0.8.15 → agentx_python-0.8.17/agentx_python.egg-info}/PKG-INFO +1 -1
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/SOURCES.txt +2 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_integrations.py +3 -1
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_judge_scorers.py +31 -0
- agentx_python-0.8.17/tests/test_multi_judge.py +76 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_selfhost_analysis_fallback.py +2 -2
- {agentx_python-0.8.15 → agentx_python-0.8.17}/LICENSE +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/README.md +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/__init__.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/agentx.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/cli.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/results.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/exceptions.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/export.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/feedback.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/autogen.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/crewai.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/langchain.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/llamaindex.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/openai_agents.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/__init__.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/agents.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/improvement_groups.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/models.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/profile.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/review_queue.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/rules.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/scorers.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/outcomes.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/projects.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/py.typed +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/resources/agent.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/testing.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/traces.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/eval_scope.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/framework_detect.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/ingest_client.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/tracer.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/util.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/setup.cfg +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/setup.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_cli_launcher.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_deep_dive_fixes.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_docs_match_sdk.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_eval_scope.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_framework_detect.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_integration.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_pairwise.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_review_queue.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_runner_features.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_selfhost_compat.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_span_tree.py +0 -0
- {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_testing.py +0 -0
|
@@ -314,10 +314,15 @@ class EvaluationsClient:
|
|
|
314
314
|
scorer_id: Optional[str] = None,
|
|
315
315
|
evaluation_settings_id: Optional[str] = None,
|
|
316
316
|
split: Optional[str] = None,
|
|
317
|
+
additional_scorer_ids: Optional[List[str]] = None,
|
|
318
|
+
scorer_group_id: Optional[str] = None,
|
|
317
319
|
) -> EvaluationRun:
|
|
318
320
|
"""``scorer_id`` names the LLM Judge Scorer grading this run (its id doubles as the
|
|
319
321
|
wire's ``evaluationSettingsId``). ``evaluation_settings_id`` is the pre-consolidation
|
|
320
|
-
alias and keeps working. ``split`` records the named case subset this run covers.
|
|
322
|
+
alias and keeps working. ``split`` records the named case subset this run covers.
|
|
323
|
+
``additional_scorer_ids`` (self-host): extra judge scorers that each pass their own
|
|
324
|
+
verdict on every result from the same single agent execution - verdicts land in each
|
|
325
|
+
result row's ``judgeScorerResults`` and the run's ``scorerBreakdown``."""
|
|
321
326
|
from agentx.version import VERSION
|
|
322
327
|
|
|
323
328
|
grader_id = _resolve_scorer_id(scorer_id, evaluation_settings_id)
|
|
@@ -335,6 +340,12 @@ class EvaluationsClient:
|
|
|
335
340
|
}
|
|
336
341
|
if grader_id:
|
|
337
342
|
payload["evaluationSettingsId"] = grader_id
|
|
343
|
+
if additional_scorer_ids:
|
|
344
|
+
payload["additionalScorerIds"] = additional_scorer_ids
|
|
345
|
+
# Scorer group grading (self-host): the group's weighted 0-10 aggregate fills the rating
|
|
346
|
+
# column and member verdicts land per row. Mutually exclusive with scorer_id (group wins).
|
|
347
|
+
if scorer_group_id:
|
|
348
|
+
payload["scorerGroupId"] = scorer_group_id
|
|
338
349
|
if split:
|
|
339
350
|
payload["split"] = split
|
|
340
351
|
data = self._request("POST", "/runs", json=self._with_workspace(payload))
|
|
@@ -372,6 +383,7 @@ class EvaluationsClient:
|
|
|
372
383
|
tolerance: Optional[float] = None,
|
|
373
384
|
record: bool = True,
|
|
374
385
|
caller: Optional[str] = "sdk",
|
|
386
|
+
scorer: Optional[str] = None,
|
|
375
387
|
) -> Dict[str, Any]:
|
|
376
388
|
# CI gate (self-host): pass/fail a finalized run against an absolute rating floor and/or
|
|
377
389
|
# the dataset's previous completed run. Recorded into gate history by default (the
|
|
@@ -389,6 +401,11 @@ class EvaluationsClient:
|
|
|
389
401
|
params["record"] = "true"
|
|
390
402
|
if caller:
|
|
391
403
|
params["caller"] = caller
|
|
404
|
+
# Multi-judge runs (self-host): gate a named additional scorer (id or name, e.g.
|
|
405
|
+
# scorer="Safety") instead of the primary - failUnder/noRegression then use that
|
|
406
|
+
# scorer's own per-result verdicts. Unknown names are a hard 400 from the engine.
|
|
407
|
+
if scorer:
|
|
408
|
+
params["scorer"] = scorer
|
|
392
409
|
return self._request("GET", f"/runs/{run_id}/gate", params=params)
|
|
393
410
|
|
|
394
411
|
def analyze_run(
|
|
@@ -48,7 +48,7 @@ class DatasetBuilder:
|
|
|
48
48
|
"questions": [],
|
|
49
49
|
}
|
|
50
50
|
# LLM-as-judge overrides for this dataset's own grading config. Omit either to keep the
|
|
51
|
-
# server default (raw prompt template /
|
|
51
|
+
# server default (raw prompt template / gpt-5.6-luna, see EVALUATIONS.md). judge_model
|
|
52
52
|
# must be one of client.evaluations.list_models() (OpenAI or Anthropic).
|
|
53
53
|
if judge_prompt is not None:
|
|
54
54
|
self._payload["judgePrompt"] = judge_prompt
|
|
@@ -45,7 +45,7 @@ class EvaluationSettingsBuilder:
|
|
|
45
45
|
"evaluationCriteria": evaluation_criteria,
|
|
46
46
|
}
|
|
47
47
|
# LLM-as-judge overrides. Omit either to keep the server default (raw prompt template /
|
|
48
|
-
#
|
|
48
|
+
# gpt-5.6-luna, see EVALUATIONS.md). judge_model must be one of
|
|
49
49
|
# client.evaluations.list_models() (OpenAI or Anthropic).
|
|
50
50
|
if judge_prompt is not None:
|
|
51
51
|
self._payload["judgePrompt"] = judge_prompt
|
|
@@ -75,6 +75,9 @@ class Dataset(BaseModel):
|
|
|
75
75
|
rejection_criteria: Optional[str] = Field(default=None, alias="rejectionCriteria")
|
|
76
76
|
evaluation_criteria: Optional[str] = Field(default=None, alias="evaluationCriteria")
|
|
77
77
|
questions: List[DatasetQuestion] = Field(default_factory=list)
|
|
78
|
+
# Custom code scorers attached to this dataset - [{ id, name, code, enabled }]. Retrievable,
|
|
79
|
+
# so a fetched dataset round-trips them (import_dataset copies them to the new dataset).
|
|
80
|
+
code_scorers: Optional[List[Dict[str, Any]]] = Field(default=None, alias="codeScorers")
|
|
78
81
|
status: str = "published"
|
|
79
82
|
version_id: Optional[str] = Field(default=None, alias="versionId")
|
|
80
83
|
# Sovereignty & Portability - models selected to compare on this dataset.
|
|
@@ -108,8 +111,10 @@ class EvaluationSettings(BaseModel):
|
|
|
108
111
|
acceptance_criteria: Optional[str] = Field(default=None, alias="acceptanceCriteria")
|
|
109
112
|
rejection_criteria: Optional[str] = Field(default=None, alias="rejectionCriteria")
|
|
110
113
|
evaluation_criteria: Optional[str] = Field(default=None, alias="evaluationCriteria")
|
|
111
|
-
#
|
|
112
|
-
|
|
114
|
+
# Custom code scorers on this scorer's offline profile - [{ id, name, code, enabled }].
|
|
115
|
+
code_scorers: Optional[List[Dict[str, Any]]] = Field(default=None, alias="codeScorers")
|
|
116
|
+
# LLM-as-judge overrides. None means "use the server default" (raw prompt template /
|
|
117
|
+
# gpt-5.6-luna). See client.evaluations.settings.builder(judge_prompt=..., judge_model=...).
|
|
113
118
|
judge_prompt: Optional[str] = Field(default=None, alias="judgePrompt")
|
|
114
119
|
judge_model: Optional[str] = Field(default=None, alias="judgeModel")
|
|
115
120
|
status: str = "published"
|
|
@@ -435,6 +440,8 @@ class RunResultRow(BaseModel):
|
|
|
435
440
|
bleu_score: Optional[float] = Field(default=None, alias="bleuScore")
|
|
436
441
|
rouge_score: Optional[float] = Field(default=None, alias="rougeScore")
|
|
437
442
|
code_scorer_results: Optional[List[Dict[str, Any]]] = Field(default=None, alias="codeScorerResults")
|
|
443
|
+
# Verdicts from the run's ADDITIONAL judge scorers: [{scorerId, name, rating, justification}].
|
|
444
|
+
judge_scorer_results: Optional[List[Dict[str, Any]]] = Field(default=None, alias="judgeScorerResults")
|
|
438
445
|
raw: Dict[str, Any] = Field(default_factory=dict)
|
|
439
446
|
|
|
440
447
|
class Config:
|
|
@@ -63,9 +63,6 @@ _ANALYSIS_LEVEL_LABELS = {
|
|
|
63
63
|
"l4_final_reduce": "writing final report",
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
_DEFAULT_JUDGE_MODEL = "gpt-5.5"
|
|
67
|
-
|
|
68
|
-
|
|
69
66
|
class GateResult:
|
|
70
67
|
"""Wire result of the CI gate (GET /runs/:id/gate) with attribute access for the fields a
|
|
71
68
|
CI script actually branches on."""
|
|
@@ -77,6 +74,9 @@ class GateResult:
|
|
|
77
74
|
self.baseline_average: Optional[float] = data.get("baselineAverage")
|
|
78
75
|
self.baseline_run_id: Optional[str] = data.get("baselineRunId")
|
|
79
76
|
self.checks: List[Dict[str, Any]] = data.get("checks", [])
|
|
77
|
+
# Multi-judge runs: {"id", "name"} of the additional scorer being gated when the gate
|
|
78
|
+
# ran with scorer=..., None when gating the primary.
|
|
79
|
+
self.gated_scorer: Optional[Dict[str, Any]] = data.get("gatedScorer")
|
|
80
80
|
|
|
81
81
|
@property
|
|
82
82
|
def exit_code(self) -> int:
|
|
@@ -354,13 +354,16 @@ class EvaluationRunContext:
|
|
|
354
354
|
no_regression: bool = False,
|
|
355
355
|
tolerance: Optional[float] = None,
|
|
356
356
|
caller: str = "sdk",
|
|
357
|
+
scorer: Optional[str] = None,
|
|
357
358
|
) -> "GateResult":
|
|
358
359
|
"""CI gate (self-host): pass/fail this finalized run so a CI job can block a merge.
|
|
359
360
|
|
|
360
361
|
``fail_under`` fails the gate when the run's average rating is below the floor;
|
|
361
362
|
``no_regression=True`` fails it when the average dropped more than ``tolerance``
|
|
362
363
|
(default 0.5, judge scores are noisy) below the dataset's previous completed run.
|
|
363
|
-
At least one check is required.
|
|
364
|
+
At least one check is required. On a multi-judge run, ``scorer`` (an additional
|
|
365
|
+
scorer's id or name, e.g. ``scorer="Safety"``) gates that scorer's own average
|
|
366
|
+
instead of the primary's - "fail if Safety is low even when the average looks fine". Prints a CI-log-friendly verdict and returns a
|
|
364
367
|
:class:`GateResult` - the caller decides the exit code::
|
|
365
368
|
|
|
366
369
|
report = client.evaluations.run(...).execute(my_agent).finalize()
|
|
@@ -374,6 +377,7 @@ class EvaluationRunContext:
|
|
|
374
377
|
no_regression=no_regression,
|
|
375
378
|
tolerance=tolerance,
|
|
376
379
|
caller=caller,
|
|
380
|
+
scorer=scorer,
|
|
377
381
|
)
|
|
378
382
|
result = GateResult(data)
|
|
379
383
|
_say()
|
|
@@ -449,16 +453,16 @@ class EvaluationRunContext:
|
|
|
449
453
|
Args:
|
|
450
454
|
mode: "auto" (default), "sync", or "batch" - how item scoring executes server-side.
|
|
451
455
|
quality_mode: "quality_first" or "balanced" - how many items get a second/third judge.
|
|
452
|
-
judges: 1-3 model ids
|
|
453
|
-
|
|
454
|
-
|
|
456
|
+
judges: 1-3 model ids from ``client.evaluations.list_models()``, e.g.
|
|
457
|
+
``["gpt-5.6-luna", "claude-opus-4-8"]``. Omit to let the engine score with its
|
|
458
|
+
platform default model (a single judge, rather than the dashboard's 3-judge
|
|
459
|
+
default - SDK runs are typically lighter-weight, quick-start evaluations).
|
|
455
460
|
poll_interval: seconds between status checks while waiting.
|
|
456
461
|
timeout: give up waiting after this many seconds (the job keeps running server-side;
|
|
457
462
|
call ``get_report()`` later to check on it).
|
|
458
463
|
"""
|
|
459
464
|
if judges is not None and not (1 <= len(judges) <= 3):
|
|
460
465
|
raise ValueError("judges must contain 1-3 model ids")
|
|
461
|
-
resolved_judges = judges if judges is not None else [_DEFAULT_JUDGE_MODEL]
|
|
462
466
|
|
|
463
467
|
_say()
|
|
464
468
|
with Spinner("Analyzing - AI is reviewing your results") as spinner:
|
|
@@ -467,7 +471,7 @@ class EvaluationRunContext:
|
|
|
467
471
|
self._run.run_id,
|
|
468
472
|
mode=mode,
|
|
469
473
|
quality_mode=quality_mode,
|
|
470
|
-
judges=
|
|
474
|
+
judges=judges,
|
|
471
475
|
)
|
|
472
476
|
deadline = time.monotonic() + timeout
|
|
473
477
|
status = self._client.get_analysis_status(self._run.run_id)
|
|
@@ -589,6 +593,7 @@ class EvaluationsRunner:
|
|
|
589
593
|
tolerance: Optional[float] = None,
|
|
590
594
|
record: bool = True,
|
|
591
595
|
caller: Optional[str] = "sdk",
|
|
596
|
+
scorer: Optional[str] = None,
|
|
592
597
|
) -> GateResult:
|
|
593
598
|
"""CI-gate any finalized run by id - the standalone form of
|
|
594
599
|
``EvaluationRunContext.gate()``, for gating a run created elsewhere or
|
|
@@ -603,6 +608,7 @@ class EvaluationsRunner:
|
|
|
603
608
|
tolerance=tolerance,
|
|
604
609
|
record=record,
|
|
605
610
|
caller=caller,
|
|
611
|
+
scorer=scorer,
|
|
606
612
|
)
|
|
607
613
|
)
|
|
608
614
|
|
|
@@ -613,6 +619,8 @@ class EvaluationsRunner:
|
|
|
613
619
|
scorer_id: Optional[str] = None,
|
|
614
620
|
evaluation_settings_id: Optional[str] = None,
|
|
615
621
|
split: Optional[str] = None,
|
|
622
|
+
additional_scorer_ids: Optional[List[str]] = None,
|
|
623
|
+
scorer_group_id: Optional[str] = None,
|
|
616
624
|
) -> EvaluationRunContext:
|
|
617
625
|
"""Start a run of ``dataset_id`` against ``subject``. Pass ``scorer_id`` (an LLM Judge
|
|
618
626
|
Scorer's id, e.g. from ``client.monitor.judge_scorers``) to grade with a specific
|
|
@@ -632,7 +640,14 @@ class EvaluationsRunner:
|
|
|
632
640
|
evaluation_settings = (
|
|
633
641
|
self._client.get_evaluation_settings(grader_id) if grader_id else None
|
|
634
642
|
)
|
|
635
|
-
run = self._client.init_run(
|
|
643
|
+
run = self._client.init_run(
|
|
644
|
+
dataset_id,
|
|
645
|
+
subject,
|
|
646
|
+
scorer_id=grader_id,
|
|
647
|
+
split=split,
|
|
648
|
+
additional_scorer_ids=additional_scorer_ids,
|
|
649
|
+
scorer_group_id=scorer_group_id,
|
|
650
|
+
)
|
|
636
651
|
case_count = (
|
|
637
652
|
sum(1 for q in dataset.questions if split in (q.main_question.splits or []))
|
|
638
653
|
if split
|
|
@@ -147,6 +147,11 @@ class MonitorClient:
|
|
|
147
147
|
# surface that matches the product; evaluations.settings and online_evaluators below
|
|
148
148
|
# remain as its profile-level views.
|
|
149
149
|
self.judge_scorers = JudgeScorersClient(api_key=api_key, base_url=self._api_root())
|
|
150
|
+
from agentx.monitor.scorer_groups import ScorerGroupsClient
|
|
151
|
+
|
|
152
|
+
# Scorer groups: mixed-kind scorers composed into one 0-10 score (weights + must-pass
|
|
153
|
+
# gates) - a group grades dataset runs (scorer_group_id) and, when online, live traffic.
|
|
154
|
+
self.scorer_groups = ScorerGroupsClient(api_key=api_key, base_url=self._api_root())
|
|
150
155
|
from agentx.monitor.improvement_groups import ImprovementGroupsClient
|
|
151
156
|
|
|
152
157
|
# Auto-improve: confirmed production failures -> improvement report -> code fix (via
|
|
@@ -424,6 +429,24 @@ class MonitorClient:
|
|
|
424
429
|
data = self._request("GET", f"/ingest/sessions/{session_id}/spans", base=self._api_root())
|
|
425
430
|
return data.get("spans", []) if isinstance(data, dict) else data
|
|
426
431
|
|
|
432
|
+
def list_session_scores(self, session_id: str) -> List[dict]:
|
|
433
|
+
"""Every session-level verdict on the session, newest first: session-scoped online
|
|
434
|
+
evaluators (kind ``online-eval:<id>``), session-scoped scorer groups
|
|
435
|
+
(``scorer-group:<id>``), and legacy coherence rows."""
|
|
436
|
+
data = self._request(
|
|
437
|
+
"GET", f"/agent-monitoring/sessions/{session_id}/scores", base=self._api_root()
|
|
438
|
+
)
|
|
439
|
+
return data.get("scores", []) if isinstance(data, dict) else data
|
|
440
|
+
|
|
441
|
+
def run_session_sweep(self) -> dict:
|
|
442
|
+
"""Run the idle-session sweep once, now - the tick that scores quiet multi-turn
|
|
443
|
+
sessions with every enabled session-scoped evaluator and scorer group. Production
|
|
444
|
+
engines run this automatically every minute; the manual trigger exists for demos,
|
|
445
|
+
tests, and backfills. Returns ``{"judged": n}``."""
|
|
446
|
+
return self._request(
|
|
447
|
+
"POST", "/agent-monitoring/session-sweep/run", base=self._api_root(), timeout=300
|
|
448
|
+
)
|
|
449
|
+
|
|
427
450
|
# ------------------------------------------------------------------
|
|
428
451
|
# Model portability (self-host): replay a trace's input against other models
|
|
429
452
|
# ------------------------------------------------------------------
|
|
@@ -36,6 +36,11 @@ class JudgeScorer(dict):
|
|
|
36
36
|
def offline(self) -> Dict[str, Any]:
|
|
37
37
|
return self.get("offline", {})
|
|
38
38
|
|
|
39
|
+
@property
|
|
40
|
+
def code_scorers(self) -> List[Dict[str, Any]]:
|
|
41
|
+
"""Custom code scorers on the offline profile - [{ id, name, code, enabled }]."""
|
|
42
|
+
return list(self.offline.get("codeScorers") or [])
|
|
43
|
+
|
|
39
44
|
@property
|
|
40
45
|
def online(self) -> Optional[Dict[str, Any]]:
|
|
41
46
|
return self.get("online")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Scorer groups (self-host): scorers of any kind - LLM judges, patterns, custom code/external
|
|
2
|
+
scorers - composed into ONE 0-10 score via per-member weights and optional must-pass gates.
|
|
3
|
+
Members are references: ``{"kind": "judge" | "pattern" | "custom", "refId": ..., "weight": ...,
|
|
4
|
+
"gate": ...}``. Grade a dataset run with a group by passing its id as ``scorer_group_id`` to
|
|
5
|
+
``client.evaluations.run(...)``; give it an ``online`` profile to score sampled live traffic and
|
|
6
|
+
raise Signals below the alert threshold."""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentXScorerGroupsError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ScorerGroup(dict):
|
|
18
|
+
"""Wire object (dict subclass so unknown fields round-trip)."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def id(self) -> str:
|
|
22
|
+
return self["_id"]
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def name(self) -> str:
|
|
26
|
+
return self["name"]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def members(self) -> List[Dict[str, Any]]:
|
|
30
|
+
return list(self.get("members") or [])
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def online(self) -> Optional[Dict[str, Any]]:
|
|
34
|
+
return self.get("online")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ScorerGroupsClient:
|
|
38
|
+
def __init__(self, api_key: str, base_url: str):
|
|
39
|
+
self._api_key = api_key
|
|
40
|
+
self._base = base_url.rstrip("/") + "/agent-monitoring/scorer-groups"
|
|
41
|
+
|
|
42
|
+
def _request(self, method: str, url: str, json: Optional[Dict[str, Any]] = None) -> Any:
|
|
43
|
+
response = requests.request(
|
|
44
|
+
method,
|
|
45
|
+
url,
|
|
46
|
+
headers={"x-api-key": self._api_key, "content-type": "application/json"},
|
|
47
|
+
json=json,
|
|
48
|
+
timeout=30,
|
|
49
|
+
)
|
|
50
|
+
if response.status_code >= 400:
|
|
51
|
+
raise AgentXScorerGroupsError(f"HTTP {response.status_code}: {response.text}")
|
|
52
|
+
return response.json()
|
|
53
|
+
|
|
54
|
+
def list(self) -> List[ScorerGroup]:
|
|
55
|
+
return [ScorerGroup(g) for g in self._request("GET", self._base).get("scorerGroups", [])]
|
|
56
|
+
|
|
57
|
+
def get(self, group_id: str) -> ScorerGroup:
|
|
58
|
+
return ScorerGroup(self._request("GET", f"{self._base}/{group_id}")["scorerGroup"])
|
|
59
|
+
|
|
60
|
+
def create(
|
|
61
|
+
self,
|
|
62
|
+
name: str,
|
|
63
|
+
members: List[Dict[str, Any]],
|
|
64
|
+
description: Optional[str] = None,
|
|
65
|
+
online: Optional[Dict[str, Any]] = None,
|
|
66
|
+
) -> ScorerGroup:
|
|
67
|
+
"""``members``: [{"kind": "judge"|"pattern"|"custom", "refId": ..., "weight": 1, "gate": False}].
|
|
68
|
+
``online``: {"enabled": True, "sampleRate": 0.1, "alertThreshold": 5, "severity": "medium"}.
|
|
69
|
+
Add ``"scope": "session", "idleSeconds": 120`` to score whole multi-turn sessions once
|
|
70
|
+
idle, instead of each sampled trace.
|
|
71
|
+
or None for offline-only."""
|
|
72
|
+
payload: Dict[str, Any] = {"name": name, "members": members}
|
|
73
|
+
if description is not None:
|
|
74
|
+
payload["description"] = description
|
|
75
|
+
if online is not None:
|
|
76
|
+
payload["online"] = online
|
|
77
|
+
return ScorerGroup(self._request("POST", self._base, json=payload)["scorerGroup"])
|
|
78
|
+
|
|
79
|
+
def update(self, group_id: str, **fields: Any) -> ScorerGroup:
|
|
80
|
+
"""Sparse update - pass any of name/description/members/online (online=None detaches
|
|
81
|
+
live scoring)."""
|
|
82
|
+
return ScorerGroup(self._request("PUT", f"{self._base}/{group_id}", json=fields)["scorerGroup"])
|
|
83
|
+
|
|
84
|
+
def delete(self, group_id: str) -> None:
|
|
85
|
+
self._request("DELETE", f"{self._base}/{group_id}")
|
|
86
|
+
|
|
87
|
+
def ratings(self, group_id: str, window: str = "7d") -> Dict[str, Any]:
|
|
88
|
+
"""Live score history for a group - ``{"window", "points": [{ts, averageRating, count}]}``,
|
|
89
|
+
the same shape online-evaluator ratings use. ``window``: "24h" | "7d" | "30d"."""
|
|
90
|
+
return self._request("GET", f"{self._base}/{group_id}/ratings?window={window}")
|
|
@@ -19,3 +19,15 @@ class MonitorSessionClient:
|
|
|
19
19
|
def spans(self, session_id: str) -> List[dict]:
|
|
20
20
|
"""Every span in the session (roots and children), oldest first."""
|
|
21
21
|
return self._client.list_session_spans(session_id)
|
|
22
|
+
|
|
23
|
+
def scores(self, session_id: str) -> List[dict]:
|
|
24
|
+
"""Session-level verdicts, newest first. ``kind`` says who scored: a session-scoped
|
|
25
|
+
online evaluator (``online-eval:<id>``) or a session-scoped scorer group
|
|
26
|
+
(``scorer-group:<id>``)."""
|
|
27
|
+
return self._client.list_session_scores(session_id)
|
|
28
|
+
|
|
29
|
+
def run_sweep(self) -> dict:
|
|
30
|
+
"""Trigger the idle-session sweep once (normally automatic, every minute) - scores
|
|
31
|
+
idle multi-turn sessions with every enabled session-scoped evaluator and scorer
|
|
32
|
+
group. Returns ``{"judged": n}``."""
|
|
33
|
+
return self._client.run_session_sweep()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
VERSION = "0.8.
|
|
1
|
+
VERSION = "0.8.17"
|
|
2
2
|
|
|
3
3
|
# The AgentX-trace-eval release this SDK version is tested against - what `agentx-trace-eval`
|
|
4
4
|
# installs and converges to (see agentx/cli.py). Bump together with VERSION when releasing, so
|
|
5
5
|
# every published SDK names a known-good engine+dashboard pair. Users can override with
|
|
6
6
|
# AGENTX_TRACE_EVAL_VERSION=<tag|latest>.
|
|
7
|
-
ENGINE_VERSION = "v0.3.
|
|
7
|
+
ENGINE_VERSION = "v0.3.16"
|
|
@@ -55,6 +55,7 @@ agentx/monitor/patterns.py
|
|
|
55
55
|
agentx/monitor/profile.py
|
|
56
56
|
agentx/monitor/review_queue.py
|
|
57
57
|
agentx/monitor/rules.py
|
|
58
|
+
agentx/monitor/scorer_groups.py
|
|
58
59
|
agentx/monitor/scorers.py
|
|
59
60
|
agentx/monitor/sessions.py
|
|
60
61
|
agentx/monitor/signals.py
|
|
@@ -83,6 +84,7 @@ tests/test_framework_detect.py
|
|
|
83
84
|
tests/test_integration.py
|
|
84
85
|
tests/test_integrations.py
|
|
85
86
|
tests/test_judge_scorers.py
|
|
87
|
+
tests/test_multi_judge.py
|
|
86
88
|
tests/test_pairwise.py
|
|
87
89
|
tests/test_review_queue.py
|
|
88
90
|
tests/test_runner_features.py
|
|
@@ -599,7 +599,9 @@ def test_crewai_captures_real_per_task_timing_via_event_bus():
|
|
|
599
599
|
durations are provably unequal — the old "divide latency evenly across
|
|
600
600
|
tasks" approximation would have reported them as identical.
|
|
601
601
|
"""
|
|
602
|
-
|
|
602
|
+
# exc_type=Exception: on older Pythons crewai can raise TypeError (PEP 604 syntax) at
|
|
603
|
+
# import time, and a broken optional integration should skip this test, not fail it.
|
|
604
|
+
crewai = pytest.importorskip("crewai", exc_type=Exception)
|
|
603
605
|
from crewai.events.event_bus import crewai_event_bus
|
|
604
606
|
from crewai.events.types.task_events import TaskCompletedEvent, TaskStartedEvent
|
|
605
607
|
from crewai.tasks.task_output import TaskOutput
|
|
@@ -268,3 +268,34 @@ def test_validate_and_publish_send_criteria_at_top_level(monkeypatch):
|
|
|
268
268
|
assert captured["validate"]["window"] == "24h"
|
|
269
269
|
assert "criteria" not in captured["validate"]
|
|
270
270
|
assert captured["publish"]["acceptanceCriteria"] == "a"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_code_scorers_are_retrievable_from_the_wire_object():
|
|
274
|
+
"""The wire rows may lack ids (SDK-created scorers) - retrieval must hand them back as-is."""
|
|
275
|
+
from agentx.monitor.judge_scorers import JudgeScorer
|
|
276
|
+
|
|
277
|
+
scorer = JudgeScorer(
|
|
278
|
+
{
|
|
279
|
+
"_id": "s1",
|
|
280
|
+
"name": "Blend",
|
|
281
|
+
"offline": {"codeScorers": [{"name": "Final score", "code": "return 1;", "enabled": True}]},
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
assert scorer.code_scorers == [{"name": "Final score", "code": "return 1;", "enabled": True}]
|
|
285
|
+
# And an offline profile without any stays an empty list, not a KeyError.
|
|
286
|
+
assert JudgeScorer({"_id": "s2", "name": "Plain", "offline": {}}).code_scorers == []
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_dataset_model_round_trips_code_scorers():
|
|
290
|
+
"""extra="ignore" used to silently drop codeScorers on read - import_dataset lost them."""
|
|
291
|
+
from agentx.evaluations.models import Dataset
|
|
292
|
+
|
|
293
|
+
wire = {
|
|
294
|
+
"_id": "d1",
|
|
295
|
+
"name": "Guarded",
|
|
296
|
+
"questions": [],
|
|
297
|
+
"codeScorers": [{"id": "cs1", "name": "gate", "code": "return 0;", "enabled": True}],
|
|
298
|
+
}
|
|
299
|
+
parsed = Dataset(**wire)
|
|
300
|
+
assert parsed.code_scorers == wire["codeScorers"]
|
|
301
|
+
assert parsed.model_dump(by_alias=True)["codeScorers"] == wire["codeScorers"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Wire-level tests for multi-judge dataset runs: additional_scorer_ids on run creation and
|
|
2
|
+
the named-scorer CI gate (gate_run(scorer=...)). The session/HTTP layer is monkeypatched; the
|
|
3
|
+
engine-side behavior is pinned by the engine's multiJudge.integration.test.ts."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from agentx.evaluations.client import EvaluationsClient
|
|
10
|
+
from agentx.evaluations.models import EvaluationSubject
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FakeResponse:
|
|
14
|
+
def __init__(self, payload: Dict[str, Any], status_code: int = 200):
|
|
15
|
+
self._payload = payload
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.ok = status_code < 400
|
|
18
|
+
self.text = "x"
|
|
19
|
+
|
|
20
|
+
def json(self) -> Dict[str, Any]:
|
|
21
|
+
return self._payload
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture()
|
|
25
|
+
def recorded(monkeypatch):
|
|
26
|
+
calls: List[Dict[str, Any]] = []
|
|
27
|
+
|
|
28
|
+
def fake_request(method, url, timeout=None, **kwargs):
|
|
29
|
+
calls.append({"method": method, "url": url, **kwargs})
|
|
30
|
+
return FakeResponse(
|
|
31
|
+
{
|
|
32
|
+
"runId": "r1",
|
|
33
|
+
"datasetId": "ds1",
|
|
34
|
+
"status": "in_progress",
|
|
35
|
+
"passed": True,
|
|
36
|
+
"checks": [],
|
|
37
|
+
"gatedScorer": None,
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
client = EvaluationsClient(api_key="k", base_url="http://engine:4700/api/v1")
|
|
42
|
+
monkeypatch.setattr(client._session, "request", fake_request)
|
|
43
|
+
return client, calls
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_init_run_sends_additional_scorer_ids_camel_case(recorded):
|
|
47
|
+
client, calls = recorded
|
|
48
|
+
client.init_run(
|
|
49
|
+
"ds1",
|
|
50
|
+
EvaluationSubject(kind="custom_agent"),
|
|
51
|
+
scorer_id="primary",
|
|
52
|
+
additional_scorer_ids=["safety", "tone"],
|
|
53
|
+
)
|
|
54
|
+
payload = calls[0]["json"]
|
|
55
|
+
assert payload["evaluationSettingsId"] == "primary"
|
|
56
|
+
assert payload["additionalScorerIds"] == ["safety", "tone"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_init_run_omits_the_key_when_no_additional_scorers(recorded):
|
|
60
|
+
client, calls = recorded
|
|
61
|
+
client.init_run("ds1", EvaluationSubject(kind="custom_agent"), scorer_id="primary")
|
|
62
|
+
assert "additionalScorerIds" not in calls[0]["json"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_gate_run_forwards_the_named_scorer(recorded):
|
|
66
|
+
client, calls = recorded
|
|
67
|
+
client.gate_run("r1", fail_under=5, scorer="Safety")
|
|
68
|
+
params = calls[0]["params"]
|
|
69
|
+
assert params["failUnder"] == 5
|
|
70
|
+
assert params["scorer"] == "Safety"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_gate_run_leaves_scorer_off_for_primary_gates(recorded):
|
|
74
|
+
client, calls = recorded
|
|
75
|
+
client.gate_run("r1", fail_under=5)
|
|
76
|
+
assert "scorer" not in calls[0]["params"]
|
|
@@ -210,12 +210,12 @@ def test_the_fallback_request_gets_the_long_analysis_timeout():
|
|
|
210
210
|
{("POST", f"{API_ROOT}/evaluate/analyze/{RUN}"): FakeResponse(200, {"status": "completed"})}
|
|
211
211
|
)
|
|
212
212
|
|
|
213
|
-
client.analyze_run(RUN, judges=["gpt-5.
|
|
213
|
+
client.analyze_run(RUN, judges=["gpt-5.6-luna"])
|
|
214
214
|
|
|
215
215
|
method, url, kwargs = session.calls[-1]
|
|
216
216
|
assert url == f"{API_ROOT}/evaluate/analyze/{RUN}"
|
|
217
217
|
assert kwargs["timeout"] > 60, "a synchronous judge pass needs more than the 30s default"
|
|
218
|
-
assert kwargs["json"]["judges"] == [{"model": "gpt-5.
|
|
218
|
+
assert kwargs["json"]["judges"] == [{"model": "gpt-5.6-luna"}]
|
|
219
219
|
|
|
220
220
|
|
|
221
221
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|