agentx-python 0.8.19__tar.gz → 0.8.21__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.19/agentx_python.egg-info → agentx_python-0.8.21}/PKG-INFO +1 -1
  2. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/__init__.py +4 -5
  3. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/client.py +53 -20
  4. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/datasets.py +16 -3
  5. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/evaluation_settings.py +2 -1
  6. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/models.py +4 -0
  7. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/prompts.py +3 -3
  8. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/reporting.py +4 -2
  9. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/runner.py +13 -0
  10. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/anthropic.py +1 -1
  11. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/autogen.py +3 -0
  12. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/crewai.py +21 -3
  13. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/google_adk.py +6 -2
  14. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/langchain.py +38 -21
  15. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/llamaindex.py +43 -5
  16. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/openai_agents.py +17 -1
  17. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/client.py +20 -10
  18. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/models.py +7 -0
  19. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/profile.py +1 -1
  20. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/scorer_groups.py +2 -1
  21. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/scorers.py +9 -2
  22. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/sessions.py +17 -1
  23. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/tracing/ci_types.py +17 -11
  24. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/tracing/ingest_client.py +48 -21
  25. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/tracing/tracer.py +80 -24
  26. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/version.py +2 -2
  27. {agentx_python-0.8.19 → agentx_python-0.8.21/agentx_python.egg-info}/PKG-INFO +1 -1
  28. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_runner_features.py +16 -0
  29. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_selfhost_analysis_fallback.py +25 -1
  30. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_span_tree.py +93 -5
  31. {agentx_python-0.8.19 → agentx_python-0.8.21}/LICENSE +0 -0
  32. {agentx_python-0.8.19 → agentx_python-0.8.21}/README.md +0 -0
  33. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/agentx.py +0 -0
  34. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/cli.py +0 -0
  35. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/__init__.py +0 -0
  36. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/_term.py +0 -0
  37. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/adapters/__init__.py +0 -0
  38. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  39. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/adapters/precomputed.py +0 -0
  40. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/adapters/raw.py +0 -0
  41. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/results.py +0 -0
  42. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/tool_schemas.py +0 -0
  43. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/evaluations/tracing.py +0 -0
  44. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/exceptions.py +0 -0
  45. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/export.py +0 -0
  46. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/feedback.py +0 -0
  47. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/__init__.py +0 -0
  48. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/_traced_call.py +0 -0
  49. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/databricks.py +0 -0
  50. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/google_genai.py +0 -0
  51. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/litellm.py +0 -0
  52. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/moveworks.py +0 -0
  53. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/integrations/openai.py +0 -0
  54. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/__init__.py +0 -0
  55. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/agents.py +0 -0
  56. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/improvement_groups.py +0 -0
  57. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/judge_scorers.py +0 -0
  58. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/online_evaluators.py +0 -0
  59. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/patterns.py +0 -0
  60. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/review_queue.py +0 -0
  61. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/rules.py +0 -0
  62. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/monitor/signals.py +0 -0
  63. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/outcomes.py +0 -0
  64. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/projects.py +0 -0
  65. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/py.typed +0 -0
  66. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/resources/__init__.py +0 -0
  67. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/resources/agent.py +0 -0
  68. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/resources/conversation.py +0 -0
  69. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/resources/workforce.py +0 -0
  70. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/testing.py +0 -0
  71. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/traces.py +0 -0
  72. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/tracing/__init__.py +0 -0
  73. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/tracing/eval_scope.py +0 -0
  74. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/tracing/framework_detect.py +0 -0
  75. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx/util.py +0 -0
  76. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx_python.egg-info/SOURCES.txt +0 -0
  77. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx_python.egg-info/dependency_links.txt +0 -0
  78. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx_python.egg-info/entry_points.txt +0 -0
  79. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx_python.egg-info/not-zip-safe +0 -0
  80. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx_python.egg-info/requires.txt +0 -0
  81. {agentx_python-0.8.19 → agentx_python-0.8.21}/agentx_python.egg-info/top_level.txt +0 -0
  82. {agentx_python-0.8.19 → agentx_python-0.8.21}/setup.cfg +0 -0
  83. {agentx_python-0.8.19 → agentx_python-0.8.21}/setup.py +0 -0
  84. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_cli_launcher.py +0 -0
  85. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_deep_dive_fixes.py +0 -0
  86. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_docs_match_sdk.py +0 -0
  87. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_eval_scope.py +0 -0
  88. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_framework_detect.py +0 -0
  89. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_integration.py +0 -0
  90. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_integrations.py +0 -0
  91. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_judge_scorers.py +0 -0
  92. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_multi_judge.py +0 -0
  93. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_pairwise.py +0 -0
  94. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_review_queue.py +0 -0
  95. {agentx_python-0.8.19 → agentx_python-0.8.21}/tests/test_selfhost_compat.py +0 -0
  96. {agentx_python-0.8.19 → agentx_python-0.8.21}/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.19
3
+ Version: 0.8.21
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
@@ -12,11 +12,10 @@ from agentx.exceptions import (
12
12
  CIGateFailure,
13
13
  )
14
14
 
15
- logging.basicConfig(
16
- level=logging.INFO,
17
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18
- datefmt="%Y-%m-%d %H:%M:%S %Z",
19
- )
15
+ # Library logging hygiene: a library must never call logging.basicConfig - it hijacks the
16
+ # host application's root logger (format AND level) and turns the app's own later basicConfig
17
+ # into a no-op. Consumers opt into our logs with logging.getLogger("agentx").setLevel(...).
18
+ logging.getLogger("agentx").addHandler(logging.NullHandler())
20
19
 
21
20
  __all__ = [
22
21
  "AgentX",
@@ -207,6 +207,12 @@ class EvaluationsClient:
207
207
  return resp.json()
208
208
  except Exception:
209
209
  return resp.text
210
+ # A timeout keeps its type: runner._flush_batch catches requests.Timeout specifically
211
+ # (the engine may still be scoring the batch - a blind retry double-bills every judge
212
+ # call), and wrapping it in AgentXEvaluationsError here made that guard unreachable.
213
+ # Applies to retry=False calls too, where the single attempt lands straight here.
214
+ if isinstance(last_exc, requests.Timeout):
215
+ raise last_exc
210
216
  raise AgentXEvaluationsError(f"Request failed after retries: {last_exc}")
211
217
 
212
218
  # ------------------------------------------------------------------
@@ -227,7 +233,9 @@ class EvaluationsClient:
227
233
  # ------------------------------------------------------------------
228
234
 
229
235
  def create_dataset(self, payload: dict) -> Dataset:
230
- data = self._request("POST", "/datasets", json=self._with_workspace(payload))
236
+ # Server-side write: a timeout after the dataset row was created would be
237
+ # retried into a duplicate dataset, so no transport retry.
238
+ data = self._request("POST", "/datasets", json=self._with_workspace(payload), retry=False)
231
239
  return Dataset(**data)
232
240
 
233
241
  def delete_dataset(self, dataset_id: str) -> None:
@@ -255,8 +263,9 @@ class EvaluationsClient:
255
263
  # ------------------------------------------------------------------
256
264
 
257
265
  def create_evaluation_settings(self, payload: dict) -> EvaluationSettings:
266
+ # Server-side write - no transport retry (see init_run's comment).
258
267
  data = self._request(
259
- "POST", "/evaluation-settings", json=self._with_workspace(payload)
268
+ "POST", "/evaluation-settings", json=self._with_workspace(payload), retry=False
260
269
  )
261
270
  return EvaluationSettings(**data)
262
271
 
@@ -281,12 +290,14 @@ class EvaluationsClient:
281
290
 
282
291
  # ------------------------------------------------------------------
283
292
  # Prompt registry endpoints - see agentx.evaluations.prompts.PromptClient for the concept
284
- # (the external-agent analog to native autotune). Deliberately read-mostly: no publish here,
285
- # a new version only ever comes from the dashboard's human-approved propose/publish flow.
293
+ # (the external-agent analog to native autotune). propose_prompt never publishes;
294
+ # publish_prompt_version below IS the explicit approval step - call it only after a human
295
+ # reviewed the proposal.
286
296
  # ------------------------------------------------------------------
287
297
 
288
298
  def create_prompt(self, payload: dict) -> Prompt:
289
- data = self._request("POST", "/prompts", json=self._with_workspace(payload))
299
+ # Server-side write - no transport retry (see init_run's comment).
300
+ data = self._request("POST", "/prompts", json=self._with_workspace(payload), retry=False)
290
301
  return Prompt(**data)
291
302
 
292
303
  def list_prompts(self) -> List[Prompt]:
@@ -322,7 +333,9 @@ class EvaluationsClient:
322
333
  alias and keeps working. ``split`` records the named case subset this run covers.
323
334
  ``additional_scorer_ids`` (self-host): extra judge scorers that each pass their own
324
335
  verdict on every result from the same single agent execution - verdicts land in each
325
- result row's ``judgeScorerResults`` and the run's ``scorerBreakdown``."""
336
+ result row's ``judgeScorerResults`` and the run's ``scorerBreakdown``. When
337
+ ``scorer_group_id`` is set, the engine nulls ``additionalScorerIds`` on the run too -
338
+ the group is the whole grading story, not a layer on top of extra scorers."""
326
339
  from agentx.version import VERSION
327
340
 
328
341
  grader_id = _resolve_scorer_id(scorer_id, evaluation_settings_id)
@@ -343,12 +356,15 @@ class EvaluationsClient:
343
356
  if additional_scorer_ids:
344
357
  payload["additionalScorerIds"] = additional_scorer_ids
345
358
  # 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).
359
+ # column and member verdicts land per row. Mutually exclusive with scorer_id (group
360
+ # wins), and the engine also nulls additionalScorerIds when a group grades the run.
347
361
  if scorer_group_id:
348
362
  payload["scorerGroupId"] = scorer_group_id
349
363
  if split:
350
364
  payload["split"] = split
351
- data = self._request("POST", "/runs", json=self._with_workspace(payload))
365
+ # Server-side write: a timeout after the run row was created would be
366
+ # retried into a duplicate run, so no transport retry.
367
+ data = self._request("POST", "/runs", json=self._with_workspace(payload), retry=False)
352
368
  return EvaluationRun(**data)
353
369
 
354
370
  def append_results(
@@ -406,7 +422,10 @@ class EvaluationsClient:
406
422
  # scorer's own per-result verdicts. Unknown names are a hard 400 from the engine.
407
423
  if scorer:
408
424
  params["scorer"] = scorer
409
- return self._request("GET", f"/runs/{run_id}/gate", params=params)
425
+ # record=True is a server-side write despite the GET verb (it persists a gate-history
426
+ # row): a timeout after the row was stored would be retried into a duplicate verdict,
427
+ # so no transport retry - same precedent as init_run.
428
+ return self._request("GET", f"/runs/{run_id}/gate", params=params, retry=not record)
410
429
 
411
430
  def analyze_run(
412
431
  self,
@@ -432,8 +451,12 @@ class EvaluationsClient:
432
451
 
433
452
  if not self._analysis_on_dashboard_router:
434
453
  try:
454
+ # The self-host route runs the analysis SYNCHRONOUSLY (engine
455
+ # routes/evaluations.ts) - a short timeout with retries re-billed the whole
456
+ # multi-judge analysis up to 4x while the first was still running. Full
457
+ # analysis timeout, no transport retry.
435
458
  return self._request(
436
- "POST", f"/runs/{run_id}/analyze", json=payload, timeout=30
459
+ "POST", f"/runs/{run_id}/analyze", json=payload, timeout=1800, retry=False
437
460
  )
438
461
  except AgentXEvaluationsError as exc:
439
462
  if not self._note_missing_analysis_route(exc, "analyze"):
@@ -554,13 +577,17 @@ class EvaluationsClient:
554
577
  if isinstance(dataset_id, dict): # populated reference, not a bare id
555
578
  dataset_id = dataset_id.get("_id") or dataset_id.get("id")
556
579
 
557
- return Report(
558
- runId=run_id,
559
- datasetId=dataset_id or "",
560
- status=envelope.get("status") or "completed",
561
- statistics=envelope.get("statistics"),
580
+ # Built as one merged dict (explicit keys last, so they win) - passing the
581
+ # explicit keys as keyword arguments alongside **body raises "got multiple
582
+ # values" whenever the analysis body itself carries runId/datasetId/status/
583
+ # statistics.
584
+ return Report(**{
562
585
  **body,
563
- )
586
+ "runId": run_id,
587
+ "datasetId": dataset_id or "",
588
+ "status": envelope.get("status") or "completed",
589
+ "statistics": envelope.get("statistics"),
590
+ })
564
591
 
565
592
  # ------------------------------------------------------------------
566
593
  # Prompt improvement loop (examples -> propose -> publish). These ride the engine's
@@ -589,8 +616,11 @@ class EvaluationsClient:
589
616
  payload["reasoning"] = reasoning
590
617
  if based_on_version is not None:
591
618
  payload["basedOnVersion"] = based_on_version
619
+ # Server-side write: a timeout after the version was stored would be
620
+ # retried into a duplicate version, so no transport retry.
592
621
  return self._request(
593
- "POST", f"/evaluate/prompts/{prompt_id}/versions", base=self._api_root, json=payload
622
+ "POST", f"/evaluate/prompts/{prompt_id}/versions", base=self._api_root, json=payload,
623
+ retry=False,
594
624
  )
595
625
 
596
626
  # ------------------------------------------------------------------
@@ -628,7 +658,7 @@ class EvaluationsClient:
628
658
  payload["judgeModel"] = judge_model
629
659
  if both_orders:
630
660
  payload["bothOrders"] = True
631
- response = self._request("POST", "/evaluate/runs/pairwise", json=payload, base=self._api_root)
661
+ response = self._request("POST", "/evaluate/runs/pairwise", json=payload, base=self._api_root, timeout=900, retry=False,)
632
662
  return PairwiseComparison(**response["comparison"])
633
663
 
634
664
  def get_pairwise(self, batch_id: str) -> PairwiseComparison:
@@ -662,7 +692,8 @@ class EvaluationsClient:
662
692
  payload: dict = {"name": name, "definition": definition}
663
693
  if description is not None:
664
694
  payload["description"] = description
665
- return self._request("POST", "/evaluate/tool-schemas", base=self._api_root, json=payload)
695
+ # Server-side write - no transport retry (see init_run's comment).
696
+ return self._request("POST", "/evaluate/tool-schemas", base=self._api_root, json=payload, retry=False)
666
697
 
667
698
  def get_tool_schema_examples(self, tool_schema_id: str, window: Optional[str] = None) -> dict:
668
699
  params = {"window": window} if window else None
@@ -686,8 +717,10 @@ class EvaluationsClient:
686
717
  payload["reasoning"] = reasoning
687
718
  if based_on_version is not None:
688
719
  payload["basedOnVersion"] = based_on_version
720
+ # Server-side write - no transport retry (see init_run's comment).
689
721
  return self._request(
690
- "POST", f"/evaluate/tool-schemas/{tool_schema_id}/versions", base=self._api_root, json=payload
722
+ "POST", f"/evaluate/tool-schemas/{tool_schema_id}/versions", base=self._api_root, json=payload,
723
+ retry=False,
691
724
  )
692
725
 
693
726
  # ------------------------------------------------------------------
@@ -16,7 +16,13 @@ _REQUIRED_CSV_COLS = {"query"}
16
16
 
17
17
 
18
18
  class DatasetBuilder:
19
- """Fluent builder for creating a Custom Agent Evaluations dataset."""
19
+ """Fluent builder for creating a Custom Agent Evaluations dataset.
20
+
21
+ ``judge_prompt``/``judge_model`` are LLM-as-judge overrides for the dataset's grading
22
+ config. NOTE (self-host): the engine's dataset-create route currently ignores both -
23
+ set them on a judge scorer / the evaluation settings instead. ``sovereignty_models``
24
+ is accepted on the wire but not acted on by the self-host engine.
25
+ """
20
26
 
21
27
  def __init__(
22
28
  self,
@@ -50,6 +56,8 @@ class DatasetBuilder:
50
56
  # LLM-as-judge overrides for this dataset's own grading config. Omit either to keep the
51
57
  # server default (raw prompt template / gpt-5.6-luna, see EVALUATIONS.md). judge_model
52
58
  # must be one of client.evaluations.list_models() (OpenAI or Anthropic).
59
+ # Self-host: the dataset-create route currently IGNORES judgePrompt/judgeModel - set
60
+ # them on a judge scorer / the evaluation settings instead (see class docstring).
53
61
  if judge_prompt is not None:
54
62
  self._payload["judgePrompt"] = judge_prompt
55
63
  if judge_model is not None:
@@ -73,7 +81,8 @@ class DatasetBuilder:
73
81
  self._payload["codeScorers"] = [
74
82
  {
75
83
  "id": scorer.get("id") or _uuid.uuid4().hex[:12],
76
- "name": scorer["name"],
84
+ # Name may be omitted - the engine defaults it, so don't KeyError here.
85
+ "name": scorer.get("name"),
77
86
  "code": scorer["code"],
78
87
  "enabled": scorer.get("enabled", True),
79
88
  }
@@ -84,7 +93,8 @@ class DatasetBuilder:
84
93
  if rouge_score:
85
94
  self._payload["rougeScore"] = {"enabled": True}
86
95
  # Sovereignty & Portability - the models to compare on this dataset (use
87
- # client.evaluations.list_models() to discover valid ids).
96
+ # client.evaluations.list_models() to discover valid ids). Self-host: accepted on
97
+ # the wire but not acted on by the engine (see class docstring).
88
98
  if sovereignty_models:
89
99
  self._payload["sovereigntyIndex"] = {
90
100
  "enabled": True,
@@ -354,10 +364,13 @@ class DatasetClient:
354
364
  "acceptanceCriteria",
355
365
  "rejectionCriteria",
356
366
  "evaluationCriteria",
367
+ "judgePrompt",
368
+ "judgeModel",
357
369
  "vectorSimilarity",
358
370
  "jaccardSimilarity",
359
371
  "bleuScore",
360
372
  "rougeScore",
373
+ "sovereigntyIndex",
361
374
  "codeScorers",
362
375
  ):
363
376
  if wire.get(key) is not None:
@@ -64,7 +64,8 @@ class EvaluationSettingsBuilder:
64
64
  if rouge_score:
65
65
  self._payload["rougeScore"] = {"enabled": True}
66
66
  # Sovereignty & Portability - the models to compare when this config runs
67
- # (use client.evaluations.list_models() to discover valid ids).
67
+ # (use client.evaluations.list_models() to discover valid ids). Self-host: accepted
68
+ # on the wire but not acted on by the engine (same caveat as DatasetBuilder's).
68
69
  if sovereignty_models:
69
70
  self._payload["sovereigntyIndex"] = {
70
71
  "enabled": True,
@@ -248,6 +248,10 @@ class LiveStatistics(BaseModel):
248
248
  min_rating: Optional[float] = Field(default=None, alias="minRating")
249
249
  max_rating: Optional[float] = Field(default=None, alias="maxRating")
250
250
  rated_count: int = Field(default=0, alias="ratedCount")
251
+ # Rows the judge could not score / rows submitted with an error - the difference between
252
+ # "everything rated 8" and "half the run never got a verdict".
253
+ skipped_count: int = Field(default=0, alias="skippedCount")
254
+ failed_count: int = Field(default=0, alias="failedCount")
251
255
 
252
256
  class Config:
253
257
  populate_by_name = True
@@ -17,9 +17,9 @@ class PromptClient:
17
17
  existing version-comparison view (``client.evaluations`` run comparisons on a dataset) can
18
18
  tell you which published version actually scored higher.
19
19
 
20
- Deliberately read-mostly from here: there is no ``publish`` on this client. A prompt only
21
- gets a new version through the dashboard's human-approved propose/publish flow, so a
22
- rewritten prompt never reaches your running agent without someone explicitly approving it.
20
+ ``propose()`` never publishes anything; ``publish_version()`` IS the explicit approval
21
+ step - call it only after a human reviewed the proposal, since a published version is
22
+ what your running agent pulls as its live prompt.
23
23
 
24
24
  Example::
25
25
 
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import Optional
4
+
3
5
  from agentx.evaluations.models import Report
4
6
  from agentx.evaluations._term import (
5
7
  bold,
@@ -21,14 +23,14 @@ _RATING_ICONS = {"high": "●", "medium": "◑", "low": "○"}
21
23
  _PRI_COLORS = {"high": red, "medium": yellow, "low": dim}
22
24
 
23
25
 
24
- def _rating_badge(rating: str | None) -> str:
26
+ def _rating_badge(rating: Optional[str]) -> str:
25
27
  icon = _RATING_ICONS.get(rating or "", "·")
26
28
  color = _RATING_COLORS.get(rating or "", dim)
27
29
  label = (rating or "").upper()
28
30
  return color(f"{icon} {label}") if label else dim(icon)
29
31
 
30
32
 
31
- def _section(title: str, rating: str | None = None) -> None:
33
+ def _section(title: str, rating: Optional[str] = None) -> None:
32
34
  badge = f" {_rating_badge(rating)}" if rating else ""
33
35
  print(f"\n{bold(title)}{badge}")
34
36
  print(dim(_THIN))
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import logging
4
4
  import os
5
5
  import time
6
+
7
+ import requests
6
8
  import uuid
7
9
  from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Union
8
10
 
@@ -304,6 +306,12 @@ class EvaluationRunContext:
304
306
  resp.failed_validation,
305
307
  )
306
308
  return
309
+ except requests.Timeout as exc:
310
+ # A read timeout means the engine may STILL be scoring this batch - a
311
+ # retry re-POSTs it and double-bills every judge call (idempotency keys
312
+ # protect rows already inserted, not judge work mid-flight). Fail loud.
313
+ last_exc = exc
314
+ break
307
315
  except Exception as exc:
308
316
  last_exc = exc
309
317
  if attempt == 1:
@@ -334,6 +342,10 @@ class EvaluationRunContext:
334
342
  # ------------------------------------------------------------------
335
343
 
336
344
  def finalize(self) -> "EvaluationRunContext":
345
+ """Mark the run completed server-side. A failed finalize is raised, not swallowed
346
+ (same fail-loud posture as _flush_batch): it leaves the run in_progress - a state a
347
+ CI pipeline MUST treat as a failure, since gates and baselines only consider
348
+ completed runs."""
337
349
  _say()
338
350
  with Spinner("Finalizing - submitting results"):
339
351
  try:
@@ -345,6 +357,7 @@ class EvaluationRunContext:
345
357
  except Exception as exc:
346
358
  _say(f" {red('✗')} Finalize failed: {dim(str(exc))}")
347
359
  logger.error("Finalize failed: %s", exc)
360
+ raise
348
361
  return self
349
362
 
350
363
  def gate(
@@ -274,7 +274,7 @@ def _patch_stream(
274
274
  return iter(ctx)
275
275
 
276
276
  def __aiter__(self_inner):
277
- return aiter(ctx)
277
+ return ctx.__aiter__() # aiter() builtin is 3.10+; python_requires is >=3.9
278
278
 
279
279
  def __getattr__(self_inner, item):
280
280
  return getattr(ctx, item)
@@ -167,6 +167,9 @@ class AgentXAutoGenObserver:
167
167
  "end_time": end_t,
168
168
  "input": pending["input"] if pending else None,
169
169
  "output": f"ERROR: {output}" if is_error else (str(output) if output is not None else None),
170
+ # The engine's failure test is success === false; without
171
+ # this a failed tool call would read as passing.
172
+ "success": not is_error,
170
173
  })
171
174
  continue
172
175
 
@@ -127,6 +127,16 @@ class AgentXCrewObserver:
127
127
  except ImportError:
128
128
  return task_timings, lambda: None
129
129
 
130
+ # Double-instrumentation guard (bus-keyed latch, the same idea as the
131
+ # other integrations' _agentx_patched flag): the event bus is a global
132
+ # singleton, so a notebook re-run or an overlapping kickoff that
133
+ # already has AgentX listeners registered would otherwise get a second
134
+ # set and duplicate every task span. When already attached, this
135
+ # kickoff just falls back to the evenly-divided timing approximation.
136
+ if getattr(crewai_event_bus, "_agentx_attached", False):
137
+ return task_timings, lambda: None
138
+ crewai_event_bus._agentx_attached = True
139
+
130
140
  def on_task_started(source: Any, event: Any) -> None:
131
141
  task_id = getattr(event, "task_id", None)
132
142
  if task_id is None:
@@ -154,9 +164,14 @@ class AgentXCrewObserver:
154
164
  crewai_event_bus.on(TaskFailedEvent)(on_task_failed)
155
165
 
156
166
  def unregister() -> None:
157
- crewai_event_bus.off(TaskStartedEvent, on_task_started)
158
- crewai_event_bus.off(TaskCompletedEvent, on_task_completed)
159
- crewai_event_bus.off(TaskFailedEvent, on_task_failed)
167
+ try:
168
+ crewai_event_bus.off(TaskStartedEvent, on_task_started)
169
+ crewai_event_bus.off(TaskCompletedEvent, on_task_completed)
170
+ crewai_event_bus.off(TaskFailedEvent, on_task_failed)
171
+ finally:
172
+ # Clear the latch even if .off() raises, so a later kickoff
173
+ # can re-attach instead of being locked out forever.
174
+ crewai_event_bus._agentx_attached = False
160
175
 
161
176
  return task_timings, unregister
162
177
 
@@ -197,6 +212,9 @@ class AgentXCrewObserver:
197
212
  "end_time": end,
198
213
  "input": description,
199
214
  "output": output_text,
215
+ # A CrewAI task is an agent turn, not a model call - without this,
216
+ # _merge_child_run's default stamped every task span "llm".
217
+ "kind": "agent",
200
218
  })
201
219
  if description is not None or task_output_text is not None:
202
220
  tool_calls.append({"name": name, "input": description, "output": task_output_text})
@@ -231,6 +231,7 @@ class AgentXADKPlugin(BasePlugin):
231
231
  model=call_start.get("model") if call_start else None,
232
232
  input_tokens=call_input_tokens,
233
233
  output_tokens=call_output_tokens,
234
+ span_kind="llm",
234
235
  )
235
236
 
236
237
  async def on_model_error_callback(
@@ -261,6 +262,7 @@ class AgentXADKPlugin(BasePlugin):
261
262
  output=f"ERROR: {error}",
262
263
  model=call_start.get("model") if call_start else None,
263
264
  error=str(error),
265
+ span_kind="llm",
264
266
  )
265
267
 
266
268
  # ------------------------------------------------------------------
@@ -290,7 +292,8 @@ class AgentXADKPlugin(BasePlugin):
290
292
  tool_input = _safe_serialize(tool_args)
291
293
  tool_output = str(result) if result is not None else None
292
294
  state["root_span"].child_span(
293
- tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output
295
+ tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output,
296
+ span_kind="tool",
294
297
  )
295
298
 
296
299
  async def on_tool_error_callback(
@@ -311,5 +314,6 @@ class AgentXADKPlugin(BasePlugin):
311
314
  tool_input = _safe_serialize(tool_args)
312
315
  tool_output = f"ERROR: {error}"
313
316
  state["root_span"].child_span(
314
- tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output, error=str(error)
317
+ tool_name, start_time=start_t, end_time=end_t, input=tool_input, output=tool_output, error=str(error),
318
+ span_kind="tool",
315
319
  )
@@ -277,6 +277,10 @@ class AgentXCallbackHandler(BaseCallbackHandler):
277
277
  become child spans, and each LLM call / tool call / retrieval becomes a span parented under
278
278
  the node that ran it - so the engine's Execution Timeline shows the actual graph trajectory
279
279
  (which nodes ran, in what order, and what each did), not a flat step list.
280
+
281
+ Retriever runs are stamped ``retrieval`` - LangChain cannot distinguish memory-backed
282
+ retrievers, so a Mem0/Zep-style store exposed as a retriever classifies as retrieval too.
283
+ Use ``tracer.trace_memory`` for lookups that should classify as memory.
280
284
  """
281
285
 
282
286
  def __init__(
@@ -339,6 +343,16 @@ class AgentXCallbackHandler(BaseCallbackHandler):
339
343
  self._retrieval_starts.pop(run_id, None)
340
344
  self._parents.pop(run_id, None)
341
345
 
346
+ # Pre-run retrieval steps waiting for a top-level chain that never came
347
+ # (e.g. retriever.invoke() called but agent.invoke() aborted before
348
+ # on_chain_start). Each step carries its own start_time, so drop the
349
+ # pre-cutoff ones just like the run_id-keyed structures above.
350
+ with self._state_lock:
351
+ if self._pending_retrieval_steps:
352
+ self._pending_retrieval_steps[:] = [
353
+ step for step in self._pending_retrieval_steps if step.get("start_time", 0) >= cutoff
354
+ ]
355
+
342
356
  # ------------------------------------------------------------------
343
357
  # Chain lifecycle
344
358
  # ------------------------------------------------------------------
@@ -359,8 +373,9 @@ class AgentXCallbackHandler(BaseCallbackHandler):
359
373
  self._prune_stale_entries()
360
374
  # Consume any retrieval steps that ran before this chain started
361
375
  # (pre-run RAG: retriever.invoke() called before agent.invoke())
362
- pending = self._pending_retrieval_steps[:]
363
- self._pending_retrieval_steps.clear()
376
+ with self._state_lock:
377
+ pending = self._pending_retrieval_steps[:]
378
+ self._pending_retrieval_steps.clear()
364
379
  self._runs[run_id] = {
365
380
  "start": time.time(),
366
381
  "input": _extract_input(inputs),
@@ -670,7 +685,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
670
685
  "input": _extract_llm_input(prompts=prompts, messages=messages),
671
686
  }
672
687
  top = self._find_top_ancestor(parent_run_id)
673
- if top and not self._runs[top].get("model") and model:
688
+ if top and top in self._runs and not self._runs[top].get("model") and model:
674
689
  self._runs[top]["model"] = model
675
690
 
676
691
  def on_llm_start(
@@ -882,15 +897,16 @@ class AgentXCallbackHandler(BaseCallbackHandler):
882
897
  step["output"] = "\n\n---\n\n".join(contents)
883
898
 
884
899
  top = self._find_top_ancestor(parent_run_id)
885
- if top and top in self._runs:
886
- # Retriever ran inside an active chain - attach directly
887
- retrievals = self._runs[top]["retrieval_steps"]
888
- step["name"] = f"Retrieval {len(retrievals) + 1}"
889
- retrievals.append(step)
890
- else:
891
- # Retriever ran before the chain started (pre-run RAG pattern)
892
- step["name"] = f"Retrieval {len(self._pending_retrieval_steps) + 1}"
893
- self._pending_retrieval_steps.append(step)
900
+ with self._state_lock:
901
+ if top and top in self._runs:
902
+ # Retriever ran inside an active chain - attach directly
903
+ retrievals = self._runs[top]["retrieval_steps"]
904
+ step["name"] = f"Retrieval {len(retrievals) + 1}"
905
+ retrievals.append(step)
906
+ else:
907
+ # Retriever ran before the chain started (pre-run RAG pattern)
908
+ step["name"] = f"Retrieval {len(self._pending_retrieval_steps) + 1}"
909
+ self._pending_retrieval_steps.append(step)
894
910
 
895
911
  def on_retriever_error(
896
912
  self,
@@ -918,15 +934,16 @@ class AgentXCallbackHandler(BaseCallbackHandler):
918
934
  step["query"] = query
919
935
 
920
936
  top = self._find_top_ancestor(parent_run_id)
921
- if top and top in self._runs:
922
- # Retriever ran inside an active chain - attach directly
923
- retrievals = self._runs[top]["retrieval_steps"]
924
- step["name"] = f"Retrieval {len(retrievals) + 1}"
925
- retrievals.append(step)
926
- else:
927
- # Retriever ran before the chain started (pre-run RAG pattern)
928
- step["name"] = f"Retrieval {len(self._pending_retrieval_steps) + 1}"
929
- self._pending_retrieval_steps.append(step)
937
+ with self._state_lock:
938
+ if top and top in self._runs:
939
+ # Retriever ran inside an active chain - attach directly
940
+ retrievals = self._runs[top]["retrieval_steps"]
941
+ step["name"] = f"Retrieval {len(retrievals) + 1}"
942
+ retrievals.append(step)
943
+ else:
944
+ # Retriever ran before the chain started (pre-run RAG pattern)
945
+ step["name"] = f"Retrieval {len(self._pending_retrieval_steps) + 1}"
946
+ self._pending_retrieval_steps.append(step)
930
947
 
931
948
  # ------------------------------------------------------------------
932
949
  # Helpers