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.

Files changed (78) hide show
  1. {agentx_python-0.6.34 → agentx_python-0.6.35}/PKG-INFO +1 -1
  2. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/agentx.py +17 -15
  3. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/export.py +5 -3
  4. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/feedback.py +5 -3
  5. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/client.py +3 -1
  6. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/scorers.py +4 -2
  7. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/outcomes.py +4 -2
  8. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/projects.py +5 -2
  9. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/traces.py +4 -2
  10. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/tracing/ingest_client.py +46 -6
  11. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/tracing/tracer.py +5 -3
  12. agentx_python-0.6.35/agentx/version.py +1 -0
  13. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/PKG-INFO +1 -1
  14. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/SOURCES.txt +1 -0
  15. agentx_python-0.6.35/tests/test_deep_dive_fixes.py +85 -0
  16. agentx_python-0.6.34/agentx/version.py +0 -1
  17. {agentx_python-0.6.34 → agentx_python-0.6.35}/LICENSE +0 -0
  18. {agentx_python-0.6.34 → agentx_python-0.6.35}/README.md +0 -0
  19. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/__init__.py +0 -0
  20. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/cli.py +0 -0
  21. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/__init__.py +0 -0
  22. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/_term.py +0 -0
  23. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/adapters/__init__.py +0 -0
  24. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  25. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/adapters/precomputed.py +0 -0
  26. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/adapters/raw.py +0 -0
  27. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/client.py +0 -0
  28. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/datasets.py +0 -0
  29. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/evaluation_settings.py +0 -0
  30. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/models.py +0 -0
  31. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/prompts.py +0 -0
  32. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/reporting.py +0 -0
  33. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/results.py +0 -0
  34. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/runner.py +0 -0
  35. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/tool_schemas.py +0 -0
  36. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/evaluations/tracing.py +0 -0
  37. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/exceptions.py +0 -0
  38. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/__init__.py +0 -0
  39. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/_traced_call.py +0 -0
  40. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/anthropic.py +0 -0
  41. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/autogen.py +0 -0
  42. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/crewai.py +0 -0
  43. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/databricks.py +0 -0
  44. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/google_adk.py +0 -0
  45. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/google_genai.py +0 -0
  46. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/langchain.py +0 -0
  47. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/litellm.py +0 -0
  48. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/llamaindex.py +0 -0
  49. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/moveworks.py +0 -0
  50. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/openai.py +0 -0
  51. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/integrations/openai_agents.py +0 -0
  52. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/__init__.py +0 -0
  53. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/agents.py +0 -0
  54. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/models.py +0 -0
  55. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/online_evaluators.py +0 -0
  56. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/patterns.py +0 -0
  57. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/profile.py +0 -0
  58. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/sessions.py +0 -0
  59. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/monitor/signals.py +0 -0
  60. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/py.typed +0 -0
  61. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/resources/__init__.py +0 -0
  62. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/resources/agent.py +0 -0
  63. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/resources/conversation.py +0 -0
  64. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/resources/workforce.py +0 -0
  65. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/tracing/__init__.py +0 -0
  66. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/tracing/ci_types.py +0 -0
  67. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx/util.py +0 -0
  68. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/dependency_links.txt +0 -0
  69. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/entry_points.txt +0 -0
  70. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/not-zip-safe +0 -0
  71. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/requires.txt +0 -0
  72. {agentx_python-0.6.34 → agentx_python-0.6.35}/agentx_python.egg-info/top_level.txt +0 -0
  73. {agentx_python-0.6.34 → agentx_python-0.6.35}/setup.cfg +0 -0
  74. {agentx_python-0.6.34 → agentx_python-0.6.35}/setup.py +0 -0
  75. {agentx_python-0.6.34 → agentx_python-0.6.35}/tests/test_integration.py +0 -0
  76. {agentx_python-0.6.34 → agentx_python-0.6.35}/tests/test_integrations.py +0 -0
  77. {agentx_python-0.6.34 → agentx_python-0.6.35}/tests/test_selfhost_analysis_fallback.py +0 -0
  78. {agentx_python-0.6.34 → agentx_python-0.6.35}/tests/test_span_tree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.34
3
+ Version: 0.6.35
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
@@ -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"{api_base()}/export", headers=get_headers(self._api_key), timeout=30
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"{api_base()}/export/{entity}",
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"{api_base()}/feedback",
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"{api_base()}/feedback/trace/{trace_id}",
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
- self.scorers = ScorersClient(api_key=api_key)
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"{api_base()}/agent-monitoring{path}",
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"{api_base()}/outcomes",
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"{api_base()}{path}",
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"{api_base()}{path}",
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
- logger.debug("agentx ingest queue full - trace dropped")
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
- def flush(self, timeout: float = 5.0) -> None:
96
- """Block until all queued traces have been sent (or timeout elapses)."""
97
- self._queue.join()
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
- logger.debug("agentx ingest send error: %s", exc)
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
- logger.debug("agentx ingest failed after retries: %s", last_exc)
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) -> None:
894
- """Block until all queued traces have been delivered."""
895
- self._client.flush(timeout)
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.6.34
3
+ Version: 0.6.35
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
@@ -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