agentx-python 0.8.19__tar.gz → 0.8.20__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.19/agentx_python.egg-info → agentx_python-0.8.20}/PKG-INFO +1 -1
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/__init__.py +4 -5
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/client.py +28 -12
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/datasets.py +14 -2
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/reporting.py +4 -2
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/runner.py +8 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/anthropic.py +1 -1
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/autogen.py +3 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/crewai.py +18 -3
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/langchain.py +34 -21
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/llamaindex.py +43 -5
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/openai_agents.py +14 -1
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/client.py +13 -8
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/scorer_groups.py +2 -1
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/scorers.py +9 -2
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/sessions.py +17 -1
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/tracing/ci_types.py +17 -11
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/tracing/ingest_client.py +48 -21
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/tracing/tracer.py +57 -15
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/version.py +2 -2
- {agentx_python-0.8.19 → agentx_python-0.8.20/agentx_python.egg-info}/PKG-INFO +1 -1
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_span_tree.py +41 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/LICENSE +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/README.md +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/agentx.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/cli.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/evaluation_settings.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/models.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/results.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/exceptions.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/export.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/feedback.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/__init__.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/agents.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/improvement_groups.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/judge_scorers.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/models.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/profile.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/review_queue.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/rules.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/outcomes.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/projects.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/py.typed +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/resources/agent.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/testing.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/traces.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/tracing/eval_scope.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/tracing/framework_detect.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx/util.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx_python.egg-info/SOURCES.txt +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/setup.cfg +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/setup.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_cli_launcher.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_deep_dive_fixes.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_docs_match_sdk.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_eval_scope.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_framework_detect.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_integration.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_integrations.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_judge_scorers.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_multi_judge.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_pairwise.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_review_queue.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_runner_features.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_selfhost_analysis_fallback.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_selfhost_compat.py +0 -0
- {agentx_python-0.8.19 → agentx_python-0.8.20}/tests/test_testing.py +0 -0
|
@@ -12,11 +12,10 @@ from agentx.exceptions import (
|
|
|
12
12
|
CIGateFailure,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
logging.basicConfig
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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",
|
|
@@ -348,7 +348,9 @@ class EvaluationsClient:
|
|
|
348
348
|
payload["scorerGroupId"] = scorer_group_id
|
|
349
349
|
if split:
|
|
350
350
|
payload["split"] = split
|
|
351
|
-
|
|
351
|
+
# Server-side write: a timeout after the run row was created would be
|
|
352
|
+
# retried into a duplicate run, so no transport retry.
|
|
353
|
+
data = self._request("POST", "/runs", json=self._with_workspace(payload), retry=False)
|
|
352
354
|
return EvaluationRun(**data)
|
|
353
355
|
|
|
354
356
|
def append_results(
|
|
@@ -432,8 +434,12 @@ class EvaluationsClient:
|
|
|
432
434
|
|
|
433
435
|
if not self._analysis_on_dashboard_router:
|
|
434
436
|
try:
|
|
437
|
+
# The self-host route runs the analysis SYNCHRONOUSLY (engine
|
|
438
|
+
# routes/evaluations.ts) - a short timeout with retries re-billed the whole
|
|
439
|
+
# multi-judge analysis up to 4x while the first was still running. Full
|
|
440
|
+
# analysis timeout, no transport retry.
|
|
435
441
|
return self._request(
|
|
436
|
-
"POST", f"/runs/{run_id}/analyze", json=payload, timeout=
|
|
442
|
+
"POST", f"/runs/{run_id}/analyze", json=payload, timeout=1800, retry=False
|
|
437
443
|
)
|
|
438
444
|
except AgentXEvaluationsError as exc:
|
|
439
445
|
if not self._note_missing_analysis_route(exc, "analyze"):
|
|
@@ -554,13 +560,17 @@ class EvaluationsClient:
|
|
|
554
560
|
if isinstance(dataset_id, dict): # populated reference, not a bare id
|
|
555
561
|
dataset_id = dataset_id.get("_id") or dataset_id.get("id")
|
|
556
562
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
563
|
+
# Built as one merged dict (explicit keys last, so they win) - passing the
|
|
564
|
+
# explicit keys as keyword arguments alongside **body raises "got multiple
|
|
565
|
+
# values" whenever the analysis body itself carries runId/datasetId/status/
|
|
566
|
+
# statistics.
|
|
567
|
+
return Report(**{
|
|
562
568
|
**body,
|
|
563
|
-
|
|
569
|
+
"runId": run_id,
|
|
570
|
+
"datasetId": dataset_id or "",
|
|
571
|
+
"status": envelope.get("status") or "completed",
|
|
572
|
+
"statistics": envelope.get("statistics"),
|
|
573
|
+
})
|
|
564
574
|
|
|
565
575
|
# ------------------------------------------------------------------
|
|
566
576
|
# Prompt improvement loop (examples -> propose -> publish). These ride the engine's
|
|
@@ -589,8 +599,11 @@ class EvaluationsClient:
|
|
|
589
599
|
payload["reasoning"] = reasoning
|
|
590
600
|
if based_on_version is not None:
|
|
591
601
|
payload["basedOnVersion"] = based_on_version
|
|
602
|
+
# Server-side write: a timeout after the version was stored would be
|
|
603
|
+
# retried into a duplicate version, so no transport retry.
|
|
592
604
|
return self._request(
|
|
593
|
-
"POST", f"/evaluate/prompts/{prompt_id}/versions", base=self._api_root, json=payload
|
|
605
|
+
"POST", f"/evaluate/prompts/{prompt_id}/versions", base=self._api_root, json=payload,
|
|
606
|
+
retry=False,
|
|
594
607
|
)
|
|
595
608
|
|
|
596
609
|
# ------------------------------------------------------------------
|
|
@@ -628,7 +641,7 @@ class EvaluationsClient:
|
|
|
628
641
|
payload["judgeModel"] = judge_model
|
|
629
642
|
if both_orders:
|
|
630
643
|
payload["bothOrders"] = True
|
|
631
|
-
response = self._request("POST", "/evaluate/runs/pairwise", json=payload, base=self._api_root)
|
|
644
|
+
response = self._request("POST", "/evaluate/runs/pairwise", json=payload, base=self._api_root, timeout=900, retry=False,)
|
|
632
645
|
return PairwiseComparison(**response["comparison"])
|
|
633
646
|
|
|
634
647
|
def get_pairwise(self, batch_id: str) -> PairwiseComparison:
|
|
@@ -662,7 +675,8 @@ class EvaluationsClient:
|
|
|
662
675
|
payload: dict = {"name": name, "definition": definition}
|
|
663
676
|
if description is not None:
|
|
664
677
|
payload["description"] = description
|
|
665
|
-
|
|
678
|
+
# Server-side write - no transport retry (see init_run's comment).
|
|
679
|
+
return self._request("POST", "/evaluate/tool-schemas", base=self._api_root, json=payload, retry=False)
|
|
666
680
|
|
|
667
681
|
def get_tool_schema_examples(self, tool_schema_id: str, window: Optional[str] = None) -> dict:
|
|
668
682
|
params = {"window": window} if window else None
|
|
@@ -686,8 +700,10 @@ class EvaluationsClient:
|
|
|
686
700
|
payload["reasoning"] = reasoning
|
|
687
701
|
if based_on_version is not None:
|
|
688
702
|
payload["basedOnVersion"] = based_on_version
|
|
703
|
+
# Server-side write - no transport retry (see init_run's comment).
|
|
689
704
|
return self._request(
|
|
690
|
-
"POST", f"/evaluate/tool-schemas/{tool_schema_id}/versions", base=self._api_root, json=payload
|
|
705
|
+
"POST", f"/evaluate/tool-schemas/{tool_schema_id}/versions", base=self._api_root, json=payload,
|
|
706
|
+
retry=False,
|
|
691
707
|
)
|
|
692
708
|
|
|
693
709
|
# ------------------------------------------------------------------
|
|
@@ -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:
|
|
@@ -84,7 +92,8 @@ class DatasetBuilder:
|
|
|
84
92
|
if rouge_score:
|
|
85
93
|
self._payload["rougeScore"] = {"enabled": True}
|
|
86
94
|
# Sovereignty & Portability - the models to compare on this dataset (use
|
|
87
|
-
# client.evaluations.list_models() to discover valid ids).
|
|
95
|
+
# client.evaluations.list_models() to discover valid ids). Self-host: accepted on
|
|
96
|
+
# the wire but not acted on by the engine (see class docstring).
|
|
88
97
|
if sovereignty_models:
|
|
89
98
|
self._payload["sovereigntyIndex"] = {
|
|
90
99
|
"enabled": True,
|
|
@@ -354,10 +363,13 @@ class DatasetClient:
|
|
|
354
363
|
"acceptanceCriteria",
|
|
355
364
|
"rejectionCriteria",
|
|
356
365
|
"evaluationCriteria",
|
|
366
|
+
"judgePrompt",
|
|
367
|
+
"judgeModel",
|
|
357
368
|
"vectorSimilarity",
|
|
358
369
|
"jaccardSimilarity",
|
|
359
370
|
"bleuScore",
|
|
360
371
|
"rougeScore",
|
|
372
|
+
"sovereigntyIndex",
|
|
361
373
|
"codeScorers",
|
|
362
374
|
):
|
|
363
375
|
if wire.get(key) is not None:
|
|
@@ -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
|
|
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
|
|
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:
|
|
@@ -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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
|
@@ -339,6 +339,16 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
339
339
|
self._retrieval_starts.pop(run_id, None)
|
|
340
340
|
self._parents.pop(run_id, None)
|
|
341
341
|
|
|
342
|
+
# Pre-run retrieval steps waiting for a top-level chain that never came
|
|
343
|
+
# (e.g. retriever.invoke() called but agent.invoke() aborted before
|
|
344
|
+
# on_chain_start). Each step carries its own start_time, so drop the
|
|
345
|
+
# pre-cutoff ones just like the run_id-keyed structures above.
|
|
346
|
+
with self._state_lock:
|
|
347
|
+
if self._pending_retrieval_steps:
|
|
348
|
+
self._pending_retrieval_steps[:] = [
|
|
349
|
+
step for step in self._pending_retrieval_steps if step.get("start_time", 0) >= cutoff
|
|
350
|
+
]
|
|
351
|
+
|
|
342
352
|
# ------------------------------------------------------------------
|
|
343
353
|
# Chain lifecycle
|
|
344
354
|
# ------------------------------------------------------------------
|
|
@@ -359,8 +369,9 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
359
369
|
self._prune_stale_entries()
|
|
360
370
|
# Consume any retrieval steps that ran before this chain started
|
|
361
371
|
# (pre-run RAG: retriever.invoke() called before agent.invoke())
|
|
362
|
-
|
|
363
|
-
|
|
372
|
+
with self._state_lock:
|
|
373
|
+
pending = self._pending_retrieval_steps[:]
|
|
374
|
+
self._pending_retrieval_steps.clear()
|
|
364
375
|
self._runs[run_id] = {
|
|
365
376
|
"start": time.time(),
|
|
366
377
|
"input": _extract_input(inputs),
|
|
@@ -670,7 +681,7 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
670
681
|
"input": _extract_llm_input(prompts=prompts, messages=messages),
|
|
671
682
|
}
|
|
672
683
|
top = self._find_top_ancestor(parent_run_id)
|
|
673
|
-
if top and not self._runs[top].get("model") and model:
|
|
684
|
+
if top and top in self._runs and not self._runs[top].get("model") and model:
|
|
674
685
|
self._runs[top]["model"] = model
|
|
675
686
|
|
|
676
687
|
def on_llm_start(
|
|
@@ -882,15 +893,16 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
882
893
|
step["output"] = "\n\n---\n\n".join(contents)
|
|
883
894
|
|
|
884
895
|
top = self._find_top_ancestor(parent_run_id)
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
896
|
+
with self._state_lock:
|
|
897
|
+
if top and top in self._runs:
|
|
898
|
+
# Retriever ran inside an active chain - attach directly
|
|
899
|
+
retrievals = self._runs[top]["retrieval_steps"]
|
|
900
|
+
step["name"] = f"Retrieval {len(retrievals) + 1}"
|
|
901
|
+
retrievals.append(step)
|
|
902
|
+
else:
|
|
903
|
+
# Retriever ran before the chain started (pre-run RAG pattern)
|
|
904
|
+
step["name"] = f"Retrieval {len(self._pending_retrieval_steps) + 1}"
|
|
905
|
+
self._pending_retrieval_steps.append(step)
|
|
894
906
|
|
|
895
907
|
def on_retriever_error(
|
|
896
908
|
self,
|
|
@@ -918,15 +930,16 @@ class AgentXCallbackHandler(BaseCallbackHandler):
|
|
|
918
930
|
step["query"] = query
|
|
919
931
|
|
|
920
932
|
top = self._find_top_ancestor(parent_run_id)
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
933
|
+
with self._state_lock:
|
|
934
|
+
if top and top in self._runs:
|
|
935
|
+
# Retriever ran inside an active chain - attach directly
|
|
936
|
+
retrievals = self._runs[top]["retrieval_steps"]
|
|
937
|
+
step["name"] = f"Retrieval {len(retrievals) + 1}"
|
|
938
|
+
retrievals.append(step)
|
|
939
|
+
else:
|
|
940
|
+
# Retriever ran before the chain started (pre-run RAG pattern)
|
|
941
|
+
step["name"] = f"Retrieval {len(self._pending_retrieval_steps) + 1}"
|
|
942
|
+
self._pending_retrieval_steps.append(step)
|
|
930
943
|
|
|
931
944
|
# ------------------------------------------------------------------
|
|
932
945
|
# Helpers
|
|
@@ -107,18 +107,51 @@ class AgentXLlamaIndexHandler(BaseCallbackHandler):
|
|
|
107
107
|
name: str = "llamaindex-agent",
|
|
108
108
|
metadata: Optional[Dict[str, Any]] = None,
|
|
109
109
|
session_id: Optional[str] = None,
|
|
110
|
+
max_run_age_seconds: float = 900.0,
|
|
110
111
|
) -> None:
|
|
111
112
|
super().__init__(event_starts_to_ignore=[], event_ends_to_ignore=[])
|
|
112
113
|
self._tracer = tracer
|
|
113
114
|
self._name = name
|
|
114
115
|
self._metadata = metadata
|
|
115
116
|
self._session_id = session_id
|
|
117
|
+
# Safety net mirroring langchain.py's _prune_stale_entries: state is
|
|
118
|
+
# normally popped in on_event_end, but an event whose end callback never
|
|
119
|
+
# fires (hard crash, integration bug) would leak forever in this
|
|
120
|
+
# long-lived singleton handler. Entries older than this are swept out
|
|
121
|
+
# at the top of on_event_start.
|
|
122
|
+
self._max_run_age_seconds = max_run_age_seconds
|
|
116
123
|
|
|
117
124
|
self._parents: Dict[str, Optional[str]] = {}
|
|
118
125
|
self._roots: Dict[str, bool] = {}
|
|
119
126
|
self._runs: Dict[str, Dict[str, Any]] = {}
|
|
120
127
|
self._starts: Dict[str, Dict[str, Any]] = {}
|
|
121
128
|
|
|
129
|
+
def _prune_stale_entries(self) -> None:
|
|
130
|
+
"""Sweep out event_id entries older than max_run_age_seconds - see __init__'s comment."""
|
|
131
|
+
cutoff = time.time() - self._max_run_age_seconds
|
|
132
|
+
|
|
133
|
+
# Every live event_id has a _starts entry (set in on_event_start and
|
|
134
|
+
# popped with _parents/_roots in on_event_end), each carrying its own
|
|
135
|
+
# start timestamp.
|
|
136
|
+
stale_event_ids = [
|
|
137
|
+
event_id for event_id, info in self._starts.items() if info.get("start", 0) < cutoff
|
|
138
|
+
]
|
|
139
|
+
for event_id in stale_event_ids:
|
|
140
|
+
self._starts.pop(event_id, None)
|
|
141
|
+
self._parents.pop(event_id, None)
|
|
142
|
+
self._roots.pop(event_id, None)
|
|
143
|
+
self._runs.pop(event_id, None)
|
|
144
|
+
|
|
145
|
+
# Root runs outlive their own _starts entry until the root's end event
|
|
146
|
+
# fires - sweep those by the run state's own start timestamp.
|
|
147
|
+
stale_run_ids = [
|
|
148
|
+
event_id for event_id, state in self._runs.items() if state.get("start", 0) < cutoff
|
|
149
|
+
]
|
|
150
|
+
for event_id in stale_run_ids:
|
|
151
|
+
self._runs.pop(event_id, None)
|
|
152
|
+
self._roots.pop(event_id, None)
|
|
153
|
+
self._parents.pop(event_id, None)
|
|
154
|
+
|
|
122
155
|
# ------------------------------------------------------------------
|
|
123
156
|
# BaseCallbackHandler protocol
|
|
124
157
|
# ------------------------------------------------------------------
|
|
@@ -138,6 +171,7 @@ class AgentXLlamaIndexHandler(BaseCallbackHandler):
|
|
|
138
171
|
**kwargs: Any,
|
|
139
172
|
) -> str:
|
|
140
173
|
payload = payload or {}
|
|
174
|
+
self._prune_stale_entries()
|
|
141
175
|
self._parents[event_id] = parent_id
|
|
142
176
|
|
|
143
177
|
root_id = self._find_root(parent_id)
|
|
@@ -251,11 +285,16 @@ class AgentXLlamaIndexHandler(BaseCallbackHandler):
|
|
|
251
285
|
tool_output = payload.get(EventPayload.FUNCTION_OUTPUT)
|
|
252
286
|
state["tool_call_steps"].append({
|
|
253
287
|
"name": tool_name,
|
|
254
|
-
|
|
288
|
+
# tracer._merge_child_run's tool_calls loop reads "latency_ms"
|
|
289
|
+
# (not "duration_ms" like execution/retrieval steps).
|
|
290
|
+
"latency_ms": int((end_t - start_t) * 1000),
|
|
255
291
|
"start_time": start_t,
|
|
256
292
|
"end_time": end_t,
|
|
257
293
|
"input": _safe_serialize(tool_input) if tool_input is not None else None,
|
|
258
294
|
"output": f"ERROR: {exception}" if exception else (str(tool_output) if tool_output is not None else None),
|
|
295
|
+
# The engine's failure test is success === false; without this a
|
|
296
|
+
# failed tool call would read as passing.
|
|
297
|
+
"success": exception is None,
|
|
259
298
|
})
|
|
260
299
|
|
|
261
300
|
if is_root:
|
|
@@ -277,10 +316,9 @@ class AgentXLlamaIndexHandler(BaseCallbackHandler):
|
|
|
277
316
|
return None
|
|
278
317
|
|
|
279
318
|
def _send_trace(self, state: Dict[str, Any]) -> None:
|
|
280
|
-
# tool_call_steps entries carry start_time/end_time
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
# call still positions correctly in the tree panel instead of defaulting to offset 0.
|
|
319
|
+
# tool_call_steps entries carry latency_ms AND start_time/end_time - _merge_child_run's
|
|
320
|
+
# tool_calls loop reads latency_ms for duration and the timestamps for position, so each
|
|
321
|
+
# tool call lands correctly in the tree panel instead of defaulting to offset 0.
|
|
284
322
|
with self._tracer.trace(
|
|
285
323
|
self._name, metadata=self._metadata, session_id=self._session_id, framework="llamaindex"
|
|
286
324
|
) as span:
|
|
@@ -16,6 +16,8 @@ Requires: ``pip install "agentx-python[openai-agents]"``
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
from datetime import datetime, timezone
|
|
19
|
+
import time
|
|
20
|
+
from uuid import uuid4
|
|
19
21
|
from typing import Any, Dict, List, Optional
|
|
20
22
|
|
|
21
23
|
from agentx.tracing.tracer import Tracer, _safe_serialize
|
|
@@ -145,7 +147,15 @@ class AgentXTracingProcessor:
|
|
|
145
147
|
metadata=self._metadata,
|
|
146
148
|
session_id=self._session_id,
|
|
147
149
|
)
|
|
148
|
-
root_span.__enter__()
|
|
150
|
+
# Deliberately NOT root_span.__enter__(): enter pushes onto the CALLING thread's
|
|
151
|
+
# active-span stack, but the Agents SDK fires on_trace_end on whatever thread it
|
|
152
|
+
# likes - the pop then no-ops there, the entry never drains, and every later
|
|
153
|
+
# unrelated trace on this thread is mis-filed as a child of this dead run (and
|
|
154
|
+
# inherits its session). Start time and session are set by hand instead; on_span_end
|
|
155
|
+
# already parents via child_span() on this exact reference, no stack involved.
|
|
156
|
+
root_span._start = time.time()
|
|
157
|
+
if root_span._session_id is None:
|
|
158
|
+
root_span._session_id = f"sdk_{uuid4().hex}"
|
|
149
159
|
self._spans[trace_id] = {
|
|
150
160
|
"root_span": root_span,
|
|
151
161
|
"llm_call_count": 0,
|
|
@@ -173,6 +183,9 @@ class AgentXTracingProcessor:
|
|
|
173
183
|
root_span._output_tokens = state["output_tokens"]
|
|
174
184
|
if state.get("error"):
|
|
175
185
|
root_span.set_error(state["error"])
|
|
186
|
+
# Close WITHOUT touching the thread-local stack (see on_trace_start). __exit__'s only
|
|
187
|
+
# stack interaction is the pop, which is a no-op for a never-pushed span - safe to call
|
|
188
|
+
# directly for its send/flush behavior.
|
|
176
189
|
root_span.__exit__(None, None, None)
|
|
177
190
|
|
|
178
191
|
def on_span_start(self, span: Any) -> None:
|
|
@@ -28,7 +28,12 @@ _RETRY_BACKOFF = [1.0, 2.0, 4.0]
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class AgentXMonitorError(Exception):
|
|
31
|
-
|
|
31
|
+
"""``status_code`` carries the HTTP status when the error came from a server
|
|
32
|
+
response; it is ``None`` for transport-level failures and retry exhaustion."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str, status_code: Optional[int] = None) -> None:
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.status_code = status_code
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
class AgentXAuthError(AgentXMonitorError):
|
|
@@ -208,17 +213,17 @@ class MonitorClient:
|
|
|
208
213
|
continue
|
|
209
214
|
|
|
210
215
|
if resp.status_code == 401:
|
|
211
|
-
raise AgentXAuthError("Invalid or missing API key")
|
|
216
|
+
raise AgentXAuthError("Invalid or missing API key", status_code=401)
|
|
212
217
|
if resp.status_code == 422:
|
|
213
|
-
raise AgentXValidationError(resp.text)
|
|
218
|
+
raise AgentXValidationError(resp.text, status_code=422)
|
|
214
219
|
if retry and resp.status_code in _RETRYABLE_STATUS and attempt < _MAX_RETRIES - 1:
|
|
215
220
|
logger.debug(
|
|
216
221
|
"Retryable status %d (attempt %d)", resp.status_code, attempt + 1
|
|
217
222
|
)
|
|
218
|
-
last_exc = AgentXMonitorError(f"HTTP {resp.status_code}")
|
|
223
|
+
last_exc = AgentXMonitorError(f"HTTP {resp.status_code}", status_code=resp.status_code)
|
|
219
224
|
continue
|
|
220
225
|
if not resp.ok:
|
|
221
|
-
raise AgentXMonitorError(f"HTTP {resp.status_code}: {resp.text}")
|
|
226
|
+
raise AgentXMonitorError(f"HTTP {resp.status_code}: {resp.text}", status_code=resp.status_code)
|
|
222
227
|
try:
|
|
223
228
|
return resp.json()
|
|
224
229
|
except Exception:
|
|
@@ -481,7 +486,7 @@ class MonitorClient:
|
|
|
481
486
|
def propose_online_evaluator_tuning(self, evaluator_id: str, window: str = "7d") -> dict:
|
|
482
487
|
data = self._request(
|
|
483
488
|
"POST", f"/agent-monitoring/online-evaluators/{evaluator_id}/tune",
|
|
484
|
-
base=self._api_root(), json={"window": window}, timeout=300,
|
|
489
|
+
base=self._api_root(), json={"window": window}, timeout=300, retry=False,
|
|
485
490
|
)
|
|
486
491
|
return data.get("proposal", data) if isinstance(data, dict) else data
|
|
487
492
|
|
|
@@ -490,7 +495,7 @@ class MonitorClient:
|
|
|
490
495
|
) -> dict:
|
|
491
496
|
return self._request(
|
|
492
497
|
"POST", f"/agent-monitoring/online-evaluators/{evaluator_id}/tune/validate",
|
|
493
|
-
base=self._api_root(), json={**criteria, "window": window}, timeout=600,
|
|
498
|
+
base=self._api_root(), json={**criteria, "window": window}, timeout=600, retry=False,
|
|
494
499
|
)
|
|
495
500
|
|
|
496
501
|
def publish_online_evaluator_tuning(
|
|
@@ -505,7 +510,7 @@ class MonitorClient:
|
|
|
505
510
|
payload["force"] = True
|
|
506
511
|
return self._request(
|
|
507
512
|
"POST", f"/agent-monitoring/online-evaluators/{evaluator_id}/tune/publish",
|
|
508
|
-
base=self._api_root(), json=payload, timeout=60,
|
|
513
|
+
base=self._api_root(), json=payload, timeout=60, retry=False,
|
|
509
514
|
)
|
|
510
515
|
|
|
511
516
|
def update_profile(self, agent_id: str, payload: dict) -> MonitorProfile:
|
|
@@ -49,7 +49,8 @@ class ScorerGroupsClient:
|
|
|
49
49
|
)
|
|
50
50
|
if response.status_code >= 400:
|
|
51
51
|
raise AgentXScorerGroupsError(f"HTTP {response.status_code}: {response.text}")
|
|
52
|
-
|
|
52
|
+
# DELETE (and any other empty 2xx) has no body - .json() on it raises.
|
|
53
|
+
return response.json() if response.text else {}
|
|
53
54
|
|
|
54
55
|
def list(self) -> List[ScorerGroup]:
|
|
55
56
|
return [ScorerGroup(g) for g in self._request("GET", self._base).get("scorerGroups", [])]
|
|
@@ -65,11 +65,18 @@ class ScorersClient:
|
|
|
65
65
|
return [p for p in patterns if p.get("source") == "builtIn"]
|
|
66
66
|
|
|
67
67
|
def _enabled_template_keys(self) -> List[str]:
|
|
68
|
-
|
|
68
|
+
# .get("key"): defensive against a template row missing its key (the wire owns this
|
|
69
|
+
# shape, not the SDK) - a keyless row is skipped rather than KeyError-ing the sweep.
|
|
70
|
+
return [p.get("key") for p in self.templates() if p.get("enabled") and p.get("key")]
|
|
69
71
|
|
|
70
72
|
def enable(self, keys: Sequence[str]) -> List[str]:
|
|
71
73
|
"""Enable template scorers by key (e.g. ``["pii-in-response"]``), preserving what is
|
|
72
|
-
already on. Returns the resulting enabled-key list.
|
|
74
|
+
already on. Returns the resulting enabled-key list.
|
|
75
|
+
|
|
76
|
+
Note: enable()/disable() are a read-modify-write over the project's single
|
|
77
|
+
enabledBuiltinPatterns list - two concurrent callers (or a dashboard edit racing an
|
|
78
|
+
SDK call) can lose one side's change. There is no engine-side merge; serialize
|
|
79
|
+
catalog edits if that matters."""
|
|
73
80
|
merged = sorted(set(self._enabled_template_keys()) | set(keys))
|
|
74
81
|
self._request("PUT", "/settings/monitoring-defaults", json={"enabledBuiltinPatterns": merged})
|
|
75
82
|
return merged
|
|
@@ -27,8 +27,24 @@ class MonitorSessionClient:
|
|
|
27
27
|
Baseline Judge existed - branch defensively on unknown kinds."""
|
|
28
28
|
return self._client.list_session_scores(session_id)
|
|
29
29
|
|
|
30
|
+
def judge(self, session_id: str, evaluator_id: str, *, if_stale: bool = False) -> dict:
|
|
31
|
+
"""Judge one session with one session-scoped evaluator, now (one judge call).
|
|
32
|
+
``if_stale=True`` skips re-judging a session that was already scored since its
|
|
33
|
+
last activity - the engine then answers ``{"skipped": True}`` without spending
|
|
34
|
+
another judge call. Returns the score row (or that skip marker)."""
|
|
35
|
+
data = self._client._request(
|
|
36
|
+
"POST",
|
|
37
|
+
f"/agent-monitoring/sessions/{session_id}/judge/{evaluator_id}",
|
|
38
|
+
base=self._client._api_root(),
|
|
39
|
+
timeout=120,
|
|
40
|
+
retry=False,
|
|
41
|
+
params={"ifStale": "true"} if if_stale else None,
|
|
42
|
+
)
|
|
43
|
+
return data.get("score", data) if isinstance(data, dict) else data
|
|
44
|
+
|
|
30
45
|
def run_sweep(self) -> dict:
|
|
31
46
|
"""Trigger the idle-session sweep once (normally automatic, every minute) - scores
|
|
32
47
|
idle multi-turn sessions with every enabled session-scoped evaluator and scorer
|
|
33
|
-
group. Returns ``{"judged": n}
|
|
48
|
+
group. Returns ``{"judged": n}``; the response may instead carry ``skipped: true``
|
|
49
|
+
when another sweep is already in flight."""
|
|
34
50
|
return self._client.run_session_sweep()
|