agentx-python 0.6.34__tar.gz → 0.6.35__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.
Potentially problematic release.
This version of agentx-python might be problematic. Click here for more details.
- {agentx_python-0.6.34 → agentx_python-0.6.35}/PKG-INFO +1 -1
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/agentx.py +17 -15
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/export.py +5 -3
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/feedback.py +5 -3
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/client.py +3 -1
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/scorers.py +4 -2
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/outcomes.py +4 -2
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/projects.py +5 -2
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/traces.py +4 -2
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/tracing/ingest_client.py +46 -6
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/tracing/tracer.py +5 -3
- agentx_python-0.6.35/agentx/version.py +1 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/PKG-INFO +1 -1
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/SOURCES.txt +1 -0
- agentx_python-0.6.35/tests/test_deep_dive_fixes.py +85 -0
- agentx_python-0.6.34/agentx/version.py +0 -1
- {agentx_python-0.6.34 → agentx_python-0.6.35}/LICENSE +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/README.md +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/__init__.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/cli.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/__init__.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/_term.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/adapters/__init__.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/adapters/http_endpoint.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/adapters/precomputed.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/adapters/raw.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/client.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/datasets.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/evaluation_settings.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/models.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/prompts.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/reporting.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/results.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/runner.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/tool_schemas.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/tracing.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/exceptions.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/__init__.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/_traced_call.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/anthropic.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/autogen.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/crewai.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/databricks.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/google_adk.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/google_genai.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/langchain.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/litellm.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/llamaindex.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/moveworks.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/openai.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/openai_agents.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/__init__.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/agents.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/models.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/online_evaluators.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/patterns.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/profile.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/sessions.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/signals.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/py.typed +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/resources/__init__.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/resources/agent.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/resources/conversation.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/resources/workforce.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/tracing/__init__.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/tracing/ci_types.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/util.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/dependency_links.txt +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/entry_points.txt +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/not-zip-safe +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/requires.txt +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/top_level.txt +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/setup.cfg +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/setup.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/tests/test_integration.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/tests/test_integrations.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/tests/test_selfhost_analysis_fallback.py +0 -0
- {agentx_python-0.6.34 → agentx_python-0.6.35}/tests/test_span_tree.py +0 -0
|
@@ -20,10 +20,12 @@ class AgentX:
|
|
|
20
20
|
if self.api_key and not os.getenv("AGENTX_API_KEY"):
|
|
21
21
|
os.environ["AGENTX_API_KEY"] = self.api_key
|
|
22
22
|
|
|
23
|
-
# base_url overrides AGENTX_API_BASE_URL env var (and the SDK default)
|
|
23
|
+
# base_url overrides AGENTX_API_BASE_URL env var (and the SDK default). It is
|
|
24
|
+
# deliberately NOT written back into os.environ: the constructor used to do that, which
|
|
25
|
+
# made the last-constructed client silently re-point every other client in the process
|
|
26
|
+
# (deep-dive round 3, bug #1). Each sub-client below receives this value explicitly and
|
|
27
|
+
# captures it at construction instead.
|
|
24
28
|
self.base_url = base_url or os.getenv("AGENTX_API_BASE_URL")
|
|
25
|
-
if self.base_url:
|
|
26
|
-
os.environ["AGENTX_API_BASE_URL"] = self.base_url
|
|
27
29
|
|
|
28
30
|
self.workspace_id = workspace_id or os.getenv("AGENTX_WORKSPACE_ID")
|
|
29
31
|
|
|
@@ -58,29 +60,29 @@ class AgentX:
|
|
|
58
60
|
|
|
59
61
|
# Report real, after-the-fact outcomes ("the ticket got reopened") against traces - the
|
|
60
62
|
# ground truth behind the dashboard's Judge Calibration card. Self-host only.
|
|
61
|
-
self.outcomes = OutcomesClient(api_key=self.api_key)
|
|
63
|
+
self.outcomes = OutcomesClient(api_key=self.api_key, base_url=self.base_url)
|
|
62
64
|
|
|
63
65
|
from agentx.projects import ProjectsClient
|
|
64
66
|
|
|
65
67
|
# Project CRUD (self-host): isolated tenants with their own API keys (P1.1).
|
|
66
|
-
self.projects = ProjectsClient(api_key=self.api_key)
|
|
68
|
+
self.projects = ProjectsClient(api_key=self.api_key, base_url=self.base_url)
|
|
67
69
|
|
|
68
70
|
from agentx.traces import TracesClient
|
|
69
71
|
|
|
70
72
|
# The read side of tracing: trace-by-id detail and paginated listing (P1.2).
|
|
71
|
-
self.traces = TracesClient(api_key=self.api_key)
|
|
73
|
+
self.traces = TracesClient(api_key=self.api_key, base_url=self.base_url)
|
|
72
74
|
|
|
73
75
|
from agentx.export import ExportClient
|
|
74
76
|
|
|
75
77
|
# Bulk NDJSON egress for backup/migration (P2.1): manifest, per-entity streaming, and
|
|
76
78
|
# directory dumps. Self-host only.
|
|
77
|
-
self.export = ExportClient(api_key=self.api_key)
|
|
79
|
+
self.export = ExportClient(api_key=self.api_key, base_url=self.base_url)
|
|
78
80
|
|
|
79
81
|
from agentx.feedback import FeedbackClient
|
|
80
82
|
|
|
81
83
|
# Forward end-user votes ("up"/"down") on traced responses - a "down" raises a signal
|
|
82
84
|
# directly, and every vote feeds Judge Calibration alongside outcomes. Self-host only.
|
|
83
|
-
self.feedback = FeedbackClient(api_key=self.api_key)
|
|
85
|
+
self.feedback = FeedbackClient(api_key=self.api_key, base_url=self.base_url)
|
|
84
86
|
|
|
85
87
|
_ingest_client = IngestClient(
|
|
86
88
|
api_key=self.api_key,
|
|
@@ -96,9 +98,9 @@ class AgentX:
|
|
|
96
98
|
return cls()
|
|
97
99
|
|
|
98
100
|
def get_agent(self, id: str) -> Agent:
|
|
99
|
-
url = f"{api_base()}/access/agents/{id}"
|
|
101
|
+
url = f"{self.base_url or api_base()}/access/agents/{id}"
|
|
100
102
|
# Make a GET request to the AgentX API
|
|
101
|
-
response = requests.get(url, headers=get_headers())
|
|
103
|
+
response = requests.get(url, headers=get_headers(self.api_key))
|
|
102
104
|
# Check if response was successful
|
|
103
105
|
if response.status_code == 200:
|
|
104
106
|
return Agent(**response.json())
|
|
@@ -106,9 +108,9 @@ class AgentX:
|
|
|
106
108
|
raise Exception(f"Failed to retrieve agent: {response.reason}")
|
|
107
109
|
|
|
108
110
|
def list_agents(self) -> List[Agent]:
|
|
109
|
-
url = f"{api_base()}/access/agents"
|
|
111
|
+
url = f"{self.base_url or api_base()}/access/agents"
|
|
110
112
|
# Make a GET request to the AgentX API
|
|
111
|
-
response = requests.get(url, headers=get_headers())
|
|
113
|
+
response = requests.get(url, headers=get_headers(self.api_key))
|
|
112
114
|
# Check if response was successful
|
|
113
115
|
if response.status_code == 200:
|
|
114
116
|
return [Agent(**agent) for agent in response.json()]
|
|
@@ -146,7 +148,7 @@ class AgentX:
|
|
|
146
148
|
"""
|
|
147
149
|
from agentx.exceptions import AgentXAPIError, AgentXAuthError, AgentXConnectionError
|
|
148
150
|
|
|
149
|
-
base = api_base()
|
|
151
|
+
base = (self.base_url or api_base()).rstrip("/")
|
|
150
152
|
# /monitor/patterns: the cheapest key-authenticated endpoint that exists on both the
|
|
151
153
|
# hosted API and the self-host engine's SDK-facing router.
|
|
152
154
|
url = f"{base}/monitor/patterns"
|
|
@@ -173,8 +175,8 @@ class AgentX:
|
|
|
173
175
|
|
|
174
176
|
def get_profile(self):
|
|
175
177
|
"""Get the current user's profile information."""
|
|
176
|
-
url = f"{api_base()}/access/getProfile"
|
|
177
|
-
response = requests.get(url, headers=get_headers())
|
|
178
|
+
url = f"{self.base_url or api_base()}/access/getProfile"
|
|
179
|
+
response = requests.get(url, headers=get_headers(self.api_key))
|
|
178
180
|
if response.status_code == 200:
|
|
179
181
|
return response.json()
|
|
180
182
|
else:
|
|
@@ -36,13 +36,15 @@ class ExportClient:
|
|
|
36
36
|
(``pg_dump`` / SQLite file copy). There is deliberately no blind row-import endpoint.
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
39
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
40
40
|
self._api_key = api_key
|
|
41
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
42
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
41
43
|
|
|
42
44
|
def manifest(self) -> List[Dict[str, Any]]:
|
|
43
45
|
"""The exportable entities with live row counts: ``[{entity, rows, path}, ...]``."""
|
|
44
46
|
resp = requests.get(
|
|
45
|
-
f"{
|
|
47
|
+
f"{self._base_url}/export", headers=get_headers(self._api_key), timeout=30
|
|
46
48
|
)
|
|
47
49
|
if resp.status_code >= 400:
|
|
48
50
|
raise AgentXExportError(f"Export manifest failed ({resp.status_code}): {resp.text[:200]}")
|
|
@@ -54,7 +56,7 @@ class ExportClient:
|
|
|
54
56
|
timestamp column, e.g. ``createdAt`` for traces, ``lastSeenAt`` for signals)."""
|
|
55
57
|
params = {"since": since} if since else None
|
|
56
58
|
resp = requests.get(
|
|
57
|
-
f"{
|
|
59
|
+
f"{self._base_url}/export/{entity}",
|
|
58
60
|
headers=get_headers(self._api_key),
|
|
59
61
|
params=params,
|
|
60
62
|
stream=True,
|
|
@@ -26,8 +26,10 @@ class FeedbackClient:
|
|
|
26
26
|
reopened", reported by a workflow); feedback is a human vote with up/down semantics.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
29
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
30
30
|
self._api_key = api_key
|
|
31
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
32
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
31
33
|
|
|
32
34
|
def report(
|
|
33
35
|
self,
|
|
@@ -61,7 +63,7 @@ class FeedbackClient:
|
|
|
61
63
|
payload["endUserId"] = end_user_id
|
|
62
64
|
|
|
63
65
|
resp = requests.post(
|
|
64
|
-
f"{
|
|
66
|
+
f"{self._base_url}/feedback",
|
|
65
67
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
66
68
|
json=payload,
|
|
67
69
|
timeout=10,
|
|
@@ -79,7 +81,7 @@ class FeedbackClient:
|
|
|
79
81
|
"""All votes recorded on one trace, oldest first (``GET /feedback/trace/:traceId``) -
|
|
80
82
|
the same rows the dashboard's trace dialog shows as up/down chips."""
|
|
81
83
|
resp = requests.get(
|
|
82
|
-
f"{
|
|
84
|
+
f"{self._base_url}/feedback/trace/{trace_id}",
|
|
83
85
|
headers=get_headers(self._api_key),
|
|
84
86
|
timeout=10,
|
|
85
87
|
)
|
|
@@ -85,7 +85,9 @@ class MonitorClient:
|
|
|
85
85
|
from agentx.monitor.scorers import ScorersClient
|
|
86
86
|
# Scorers-catalog administration as code: template enable/disable, code/external scorer
|
|
87
87
|
# CRUD and dry runs - full parity with the dashboard's Scorers page (P1.3).
|
|
88
|
-
|
|
88
|
+
# Handed this client's own resolved API root, never the process-global default, so a
|
|
89
|
+
# second AgentX() with a different base_url can't re-point it (deep-dive bug #1).
|
|
90
|
+
self.scorers = ScorersClient(api_key=api_key, base_url=self._api_root())
|
|
89
91
|
self.profile = MonitorProfileClient(self)
|
|
90
92
|
self.online_evaluators = MonitorOnlineEvaluatorClient(self)
|
|
91
93
|
from agentx.monitor.sessions import MonitorSessionClient
|
|
@@ -33,13 +33,15 @@ class ScorersClient:
|
|
|
33
33
|
wire.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
36
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
37
37
|
self._api_key = api_key
|
|
38
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
39
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
38
40
|
|
|
39
41
|
def _request(self, method: str, path: str, json: Any = None, params: Any = None) -> Any:
|
|
40
42
|
resp = requests.request(
|
|
41
43
|
method,
|
|
42
|
-
f"{
|
|
44
|
+
f"{self._base_url}/agent-monitoring{path}",
|
|
43
45
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
44
46
|
json=json,
|
|
45
47
|
params=params,
|
|
@@ -26,8 +26,10 @@ class OutcomesClient:
|
|
|
26
26
|
tracker's webhook, a CRM workflow), and this method is the SDK's way to be that caller.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
29
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
30
30
|
self._api_key = api_key
|
|
31
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
32
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
31
33
|
|
|
32
34
|
def report(
|
|
33
35
|
self,
|
|
@@ -69,7 +71,7 @@ class OutcomesClient:
|
|
|
69
71
|
payload["reportedBy"] = reported_by
|
|
70
72
|
|
|
71
73
|
resp = requests.post(
|
|
72
|
-
f"{
|
|
74
|
+
f"{self._base_url}/outcomes",
|
|
73
75
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
74
76
|
json=payload,
|
|
75
77
|
timeout=10,
|
|
@@ -25,13 +25,16 @@ class ProjectsClient:
|
|
|
25
25
|
users; this client covers the default self-host (auth-disabled) mode.
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
28
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
29
29
|
self._api_key = api_key
|
|
30
|
+
# Captured once at construction so two clients with different bases can coexist
|
|
31
|
+
# (deep-dive round 3, bug #1) - never read from process-global state per call.
|
|
32
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
30
33
|
|
|
31
34
|
def _request(self, method: str, path: str, json: Any = None) -> Any:
|
|
32
35
|
resp = requests.request(
|
|
33
36
|
method,
|
|
34
|
-
f"{
|
|
37
|
+
f"{self._base_url}{path}",
|
|
35
38
|
headers={**get_headers(self._api_key), "Content-Type": "application/json"},
|
|
36
39
|
json=json,
|
|
37
40
|
timeout=15,
|
|
@@ -24,12 +24,14 @@ class TracesClient:
|
|
|
24
24
|
span-tree read.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
def __init__(self, api_key: Optional[str] = None):
|
|
27
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
28
28
|
self._api_key = api_key
|
|
29
|
+
# Captured once at construction (deep-dive round 3, bug #1).
|
|
30
|
+
self._base_url = (base_url or api_base()).rstrip("/")
|
|
29
31
|
|
|
30
32
|
def _request(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
31
33
|
resp = requests.get(
|
|
32
|
-
f"{
|
|
34
|
+
f"{self._base_url}{path}",
|
|
33
35
|
headers=get_headers(self._api_key),
|
|
34
36
|
params=params or {},
|
|
35
37
|
timeout=15,
|
|
@@ -72,6 +72,11 @@ class IngestClient:
|
|
|
72
72
|
# failures stay at DEBUG to avoid log spam. client.ping() is the fail-fast startup check.
|
|
73
73
|
self._delivery_warning_emitted = False
|
|
74
74
|
|
|
75
|
+
# Dropped-trace accounting (deep-dive round 3, bug #5): fire-and-forget may drop traces
|
|
76
|
+
# (queue overflow, retries exhausted while the engine is down), but it must never drop
|
|
77
|
+
# them SILENTLY. First drop warns, then every 50th, with the cumulative count.
|
|
78
|
+
self._dropped = 0
|
|
79
|
+
|
|
75
80
|
self._queue: queue.Queue[Optional[Dict[str, Any]]] = queue.Queue(maxsize=_QUEUE_MAX)
|
|
76
81
|
self._worker = threading.Thread(target=self._drain, daemon=True, name="agentx-ingest")
|
|
77
82
|
self._worker.start()
|
|
@@ -90,11 +95,44 @@ class IngestClient:
|
|
|
90
95
|
try:
|
|
91
96
|
self._queue.put_nowait(payload)
|
|
92
97
|
except queue.Full:
|
|
93
|
-
|
|
98
|
+
self._record_drop("queue full")
|
|
99
|
+
|
|
100
|
+
def flush(self, timeout: float = 5.0) -> bool:
|
|
101
|
+
"""Block until all queued traces have been sent, or until ``timeout`` seconds elapse.
|
|
94
102
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
Returns ``True`` when the queue fully drained, ``False`` on deadline - undelivered
|
|
104
|
+
traces stay queued and keep sending in the background. The old implementation called
|
|
105
|
+
``queue.join()``, which has no deadline at all: with the engine down, each queued item
|
|
106
|
+
burned the full retry backoff and a ``flush(timeout=3)`` measurably blocked for over
|
|
107
|
+
two minutes (deep-dive round 3, bug #5). The timeout is now a real wall-clock bound.
|
|
108
|
+
"""
|
|
109
|
+
deadline = time.time() + timeout
|
|
110
|
+
with self._queue.all_tasks_done:
|
|
111
|
+
while self._queue.unfinished_tasks:
|
|
112
|
+
remaining = deadline - time.time()
|
|
113
|
+
if remaining <= 0:
|
|
114
|
+
logger.warning(
|
|
115
|
+
"agentx flush(%.1fs) timed out with %d trace(s) still undelivered - "
|
|
116
|
+
"they remain queued and keep sending in the background",
|
|
117
|
+
timeout,
|
|
118
|
+
self._queue.unfinished_tasks,
|
|
119
|
+
)
|
|
120
|
+
return False
|
|
121
|
+
self._queue.all_tasks_done.wait(remaining)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def _record_drop(self, reason: str) -> None:
|
|
125
|
+
self._dropped += 1
|
|
126
|
+
if self._dropped == 1 or self._dropped % 50 == 0:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"agentx dropped a trace (%s) - %d dropped total this process. "
|
|
129
|
+
"Tracing is fire-and-forget: traces that cannot be delivered are not persisted "
|
|
130
|
+
"locally. Use client.ping() at startup to fail fast on a bad endpoint.",
|
|
131
|
+
reason,
|
|
132
|
+
self._dropped,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
logger.debug("agentx dropped a trace (%s), %d total", reason, self._dropped)
|
|
98
136
|
|
|
99
137
|
def send_trace_sync(self, payload: Dict[str, Any]) -> Optional[str]:
|
|
100
138
|
"""
|
|
@@ -372,7 +410,9 @@ class IngestClient:
|
|
|
372
410
|
try:
|
|
373
411
|
self._send(payload)
|
|
374
412
|
except Exception as exc:
|
|
375
|
-
|
|
413
|
+
# Anything _send didn't classify still cost us this payload - count it as a
|
|
414
|
+
# drop rather than whispering at DEBUG (bug #5's silent half).
|
|
415
|
+
self._record_drop(f"unexpected error: {exc}")
|
|
376
416
|
finally:
|
|
377
417
|
self._queue.task_done()
|
|
378
418
|
|
|
@@ -397,4 +437,4 @@ class IngestClient:
|
|
|
397
437
|
return
|
|
398
438
|
|
|
399
439
|
self._warn_delivery(str(last_exc))
|
|
400
|
-
|
|
440
|
+
self._record_drop(f"retries exhausted: {last_exc}")
|
|
@@ -890,9 +890,11 @@ class Tracer:
|
|
|
890
890
|
agent_id=agent_id,
|
|
891
891
|
)
|
|
892
892
|
|
|
893
|
-
def flush(self, timeout: float = 5.0) ->
|
|
894
|
-
"""Block until all queued traces have been delivered
|
|
895
|
-
|
|
893
|
+
def flush(self, timeout: float = 5.0) -> bool:
|
|
894
|
+
"""Block until all queued traces have been delivered, or until ``timeout`` seconds
|
|
895
|
+
elapse. Returns ``True`` when everything drained, ``False`` on deadline (a warning is
|
|
896
|
+
logged and undelivered traces keep sending in the background)."""
|
|
897
|
+
return self._client.flush(timeout)
|
|
896
898
|
|
|
897
899
|
# ------------------------------------------------------------------
|
|
898
900
|
# CI/CD evaluation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.6.35"
|
|
@@ -68,6 +68,7 @@ agentx_python.egg-info/entry_points.txt
|
|
|
68
68
|
agentx_python.egg-info/not-zip-safe
|
|
69
69
|
agentx_python.egg-info/requires.txt
|
|
70
70
|
agentx_python.egg-info/top_level.txt
|
|
71
|
+
tests/test_deep_dive_fixes.py
|
|
71
72
|
tests/test_integration.py
|
|
72
73
|
tests/test_integrations.py
|
|
73
74
|
tests/test_selfhost_analysis_fallback.py
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Regressions for deep-dive round 3's SDK bugs (sample-scripts/eval_framework_deep_dive/
|
|
2
|
+
DEEP_DIVE_REPORT.md): bug #1 (base_url was process-global via os.environ, so the
|
|
3
|
+
last-constructed client silently re-pointed every other client) and bug #5 (flush(timeout)
|
|
4
|
+
called queue.join() with no deadline and dropped traces silently)."""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from agentx import AgentX
|
|
14
|
+
from agentx.tracing.ingest_client import IngestClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def clean_env(monkeypatch):
|
|
19
|
+
monkeypatch.delenv("AGENTX_API_BASE_URL", raising=False)
|
|
20
|
+
monkeypatch.setenv("AGENTX_API_KEY", "agtx_test_key")
|
|
21
|
+
yield
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_second_client_does_not_repoint_the_first():
|
|
25
|
+
a = AgentX(api_key="k1", base_url="http://engine-a:1111/api/v1")
|
|
26
|
+
b = AgentX(api_key="k2", base_url="http://engine-b:2222/api/v1")
|
|
27
|
+
|
|
28
|
+
# The constructor must not leak its base into process-global state...
|
|
29
|
+
assert os.environ.get("AGENTX_API_BASE_URL") is None
|
|
30
|
+
|
|
31
|
+
# ...and every per-call-resolving surface of client A must still point at engine A.
|
|
32
|
+
for sub in (a.projects, a.traces, a.export, a.feedback, a.outcomes, a.monitor.scorers):
|
|
33
|
+
assert "engine-a:1111" in sub._base_url, type(sub).__name__
|
|
34
|
+
for sub in (b.projects, b.traces, b.export, b.feedback, b.outcomes, b.monitor.scorers):
|
|
35
|
+
assert "engine-b:2222" in sub._base_url, type(sub).__name__
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_env_var_still_works_as_a_default(monkeypatch):
|
|
39
|
+
monkeypatch.setenv("AGENTX_API_BASE_URL", "http://from-env:3333/api/v1")
|
|
40
|
+
c = AgentX(api_key="k")
|
|
41
|
+
assert "from-env:3333" in c.projects._base_url
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _stalled_client(monkeypatch, per_call_delay: float) -> IngestClient:
|
|
45
|
+
client = IngestClient(api_key="k", sdk_version="test", base_url="http://localhost:9/api/v1")
|
|
46
|
+
|
|
47
|
+
def slow_post(*args, **kwargs):
|
|
48
|
+
time.sleep(per_call_delay)
|
|
49
|
+
raise ConnectionError("engine down")
|
|
50
|
+
|
|
51
|
+
monkeypatch.setattr(client._session, "post", slow_post)
|
|
52
|
+
return client
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_flush_timeout_is_a_wall_clock_bound(monkeypatch):
|
|
56
|
+
client = _stalled_client(monkeypatch, per_call_delay=2.0)
|
|
57
|
+
for i in range(5):
|
|
58
|
+
client.enqueue({"name": f"t{i}"})
|
|
59
|
+
|
|
60
|
+
t0 = time.time()
|
|
61
|
+
drained = client.flush(timeout=1.0)
|
|
62
|
+
elapsed = time.time() - t0
|
|
63
|
+
|
|
64
|
+
assert drained is False
|
|
65
|
+
# The old queue.join() implementation blocked for the full retry schedule of every queued
|
|
66
|
+
# item (measured 140s for 20 items); the deadline must now bind within a small margin.
|
|
67
|
+
assert elapsed < 3.0, f"flush(timeout=1.0) blocked for {elapsed:.1f}s"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_dropped_traces_warn_instead_of_vanishing(monkeypatch, caplog):
|
|
71
|
+
client = IngestClient(api_key="k", sdk_version="test", base_url="http://localhost:9/api/v1")
|
|
72
|
+
|
|
73
|
+
def refuse(*args, **kwargs):
|
|
74
|
+
raise requests.exceptions.ConnectionError("connection refused")
|
|
75
|
+
|
|
76
|
+
monkeypatch.setattr(client._session, "post", refuse)
|
|
77
|
+
with caplog.at_level(logging.WARNING, logger="agentx.tracing.ingest_client"):
|
|
78
|
+
client.enqueue({"name": "doomed"})
|
|
79
|
+
for _ in range(200): # wait out the retry backoff
|
|
80
|
+
if client._dropped >= 1:
|
|
81
|
+
break
|
|
82
|
+
time.sleep(0.1)
|
|
83
|
+
|
|
84
|
+
assert client._dropped >= 1
|
|
85
|
+
assert any("dropped a trace" in rec.message for rec in caplog.records)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "0.6.34"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|