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.
Files changed (96) hide show
  1. {agentx_python-0.8.15/agentx_python.egg-info → agentx_python-0.8.17}/PKG-INFO +1 -1
  2. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/client.py +18 -1
  3. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/datasets.py +1 -1
  4. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/evaluation_settings.py +1 -1
  5. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/models.py +9 -2
  6. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/runner.py +25 -10
  7. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/client.py +23 -0
  8. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/judge_scorers.py +5 -0
  9. agentx_python-0.8.17/agentx/monitor/scorer_groups.py +90 -0
  10. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/sessions.py +12 -0
  11. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/version.py +2 -2
  12. {agentx_python-0.8.15 → agentx_python-0.8.17/agentx_python.egg-info}/PKG-INFO +1 -1
  13. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/SOURCES.txt +2 -0
  14. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_integrations.py +3 -1
  15. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_judge_scorers.py +31 -0
  16. agentx_python-0.8.17/tests/test_multi_judge.py +76 -0
  17. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_selfhost_analysis_fallback.py +2 -2
  18. {agentx_python-0.8.15 → agentx_python-0.8.17}/LICENSE +0 -0
  19. {agentx_python-0.8.15 → agentx_python-0.8.17}/README.md +0 -0
  20. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/__init__.py +0 -0
  21. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/agentx.py +0 -0
  22. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/cli.py +0 -0
  23. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/__init__.py +0 -0
  24. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/_term.py +0 -0
  25. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/adapters/__init__.py +0 -0
  26. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  27. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/adapters/precomputed.py +0 -0
  28. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/adapters/raw.py +0 -0
  29. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/prompts.py +0 -0
  30. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/reporting.py +0 -0
  31. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/results.py +0 -0
  32. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/tool_schemas.py +0 -0
  33. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/evaluations/tracing.py +0 -0
  34. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/exceptions.py +0 -0
  35. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/export.py +0 -0
  36. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/feedback.py +0 -0
  37. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/__init__.py +0 -0
  38. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/_traced_call.py +0 -0
  39. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/anthropic.py +0 -0
  40. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/autogen.py +0 -0
  41. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/crewai.py +0 -0
  42. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/databricks.py +0 -0
  43. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/google_adk.py +0 -0
  44. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/google_genai.py +0 -0
  45. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/langchain.py +0 -0
  46. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/litellm.py +0 -0
  47. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/llamaindex.py +0 -0
  48. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/moveworks.py +0 -0
  49. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/openai.py +0 -0
  50. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/integrations/openai_agents.py +0 -0
  51. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/__init__.py +0 -0
  52. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/agents.py +0 -0
  53. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/improvement_groups.py +0 -0
  54. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/models.py +0 -0
  55. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/online_evaluators.py +0 -0
  56. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/patterns.py +0 -0
  57. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/profile.py +0 -0
  58. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/review_queue.py +0 -0
  59. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/rules.py +0 -0
  60. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/scorers.py +0 -0
  61. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/monitor/signals.py +0 -0
  62. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/outcomes.py +0 -0
  63. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/projects.py +0 -0
  64. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/py.typed +0 -0
  65. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/resources/__init__.py +0 -0
  66. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/resources/agent.py +0 -0
  67. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/resources/conversation.py +0 -0
  68. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/resources/workforce.py +0 -0
  69. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/testing.py +0 -0
  70. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/traces.py +0 -0
  71. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/__init__.py +0 -0
  72. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/ci_types.py +0 -0
  73. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/eval_scope.py +0 -0
  74. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/framework_detect.py +0 -0
  75. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/ingest_client.py +0 -0
  76. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/tracing/tracer.py +0 -0
  77. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx/util.py +0 -0
  78. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/dependency_links.txt +0 -0
  79. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/entry_points.txt +0 -0
  80. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/not-zip-safe +0 -0
  81. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/requires.txt +0 -0
  82. {agentx_python-0.8.15 → agentx_python-0.8.17}/agentx_python.egg-info/top_level.txt +0 -0
  83. {agentx_python-0.8.15 → agentx_python-0.8.17}/setup.cfg +0 -0
  84. {agentx_python-0.8.15 → agentx_python-0.8.17}/setup.py +0 -0
  85. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_cli_launcher.py +0 -0
  86. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_deep_dive_fixes.py +0 -0
  87. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_docs_match_sdk.py +0 -0
  88. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_eval_scope.py +0 -0
  89. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_framework_detect.py +0 -0
  90. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_integration.py +0 -0
  91. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_pairwise.py +0 -0
  92. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_review_queue.py +0 -0
  93. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_runner_features.py +0 -0
  94. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_selfhost_compat.py +0 -0
  95. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_span_tree.py +0 -0
  96. {agentx_python-0.8.15 → agentx_python-0.8.17}/tests/test_testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.15
3
+ Version: 0.8.17
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
@@ -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 / OpenAI gpt-5.5, see EVALUATIONS.md). judge_model
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
- # OpenAI gpt-5.5, see EVALUATIONS.md). judge_model must be one of
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
- # LLM-as-judge overrides. None means "use the server default" (raw prompt template / OpenAI
112
- # gpt-5.5). See client.evaluations.settings.builder(judge_prompt=..., judge_model=...).
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. Prints a CI-log-friendly verdict and returns a
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, e.g. ``["gpt-5.5", "claude-opus-4-8"]``. Defaults to a single
453
- judge, ``["gpt-5.5"]``, rather than the dashboard's 3-judge default - SDK runs are
454
- typically lighter-weight, quick-start evaluations.
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=resolved_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(dataset_id, subject, scorer_id=grader_id, split=split)
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.15"
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.11"
7
+ ENGINE_VERSION = "v0.3.16"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.15
3
+ Version: 0.8.17
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
@@ -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
- crewai = pytest.importorskip("crewai")
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.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.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