agentx-python 0.8.11__tar.gz → 0.8.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. {agentx_python-0.8.11 → agentx_python-0.8.13}/PKG-INFO +7 -2
  2. {agentx_python-0.8.11 → agentx_python-0.8.13}/README.md +6 -1
  3. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/_traced_call.py +3 -0
  4. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/client.py +22 -0
  5. agentx_python-0.8.13/agentx/tracing/framework_detect.py +64 -0
  6. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/tracing/ingest_client.py +9 -0
  7. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/tracing/tracer.py +39 -9
  8. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/version.py +2 -2
  9. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx_python.egg-info/PKG-INFO +7 -2
  10. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx_python.egg-info/SOURCES.txt +2 -0
  11. agentx_python-0.8.13/tests/test_framework_detect.py +125 -0
  12. {agentx_python-0.8.11 → agentx_python-0.8.13}/LICENSE +0 -0
  13. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/__init__.py +0 -0
  14. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/agentx.py +0 -0
  15. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/cli.py +0 -0
  16. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/__init__.py +0 -0
  17. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/_term.py +0 -0
  18. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/adapters/__init__.py +0 -0
  19. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/adapters/http_endpoint.py +0 -0
  20. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/adapters/precomputed.py +0 -0
  21. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/adapters/raw.py +0 -0
  22. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/client.py +0 -0
  23. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/datasets.py +0 -0
  24. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/evaluation_settings.py +0 -0
  25. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/models.py +0 -0
  26. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/prompts.py +0 -0
  27. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/reporting.py +0 -0
  28. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/results.py +0 -0
  29. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/runner.py +0 -0
  30. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/tool_schemas.py +0 -0
  31. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/evaluations/tracing.py +0 -0
  32. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/exceptions.py +0 -0
  33. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/export.py +0 -0
  34. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/feedback.py +0 -0
  35. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/__init__.py +0 -0
  36. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/anthropic.py +0 -0
  37. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/autogen.py +0 -0
  38. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/crewai.py +0 -0
  39. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/databricks.py +0 -0
  40. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/google_adk.py +0 -0
  41. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/google_genai.py +0 -0
  42. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/langchain.py +0 -0
  43. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/litellm.py +0 -0
  44. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/llamaindex.py +0 -0
  45. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/moveworks.py +0 -0
  46. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/openai.py +0 -0
  47. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/integrations/openai_agents.py +0 -0
  48. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/__init__.py +0 -0
  49. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/agents.py +0 -0
  50. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/judge_scorers.py +0 -0
  51. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/models.py +0 -0
  52. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/online_evaluators.py +0 -0
  53. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/patterns.py +0 -0
  54. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/profile.py +0 -0
  55. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/review_queue.py +0 -0
  56. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/rules.py +0 -0
  57. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/scorers.py +0 -0
  58. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/sessions.py +0 -0
  59. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/monitor/signals.py +0 -0
  60. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/outcomes.py +0 -0
  61. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/projects.py +0 -0
  62. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/py.typed +0 -0
  63. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/resources/__init__.py +0 -0
  64. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/resources/agent.py +0 -0
  65. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/resources/conversation.py +0 -0
  66. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/resources/workforce.py +0 -0
  67. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/testing.py +0 -0
  68. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/traces.py +0 -0
  69. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/tracing/__init__.py +0 -0
  70. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/tracing/ci_types.py +0 -0
  71. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/tracing/eval_scope.py +0 -0
  72. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx/util.py +0 -0
  73. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx_python.egg-info/dependency_links.txt +0 -0
  74. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx_python.egg-info/entry_points.txt +0 -0
  75. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx_python.egg-info/not-zip-safe +0 -0
  76. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx_python.egg-info/requires.txt +0 -0
  77. {agentx_python-0.8.11 → agentx_python-0.8.13}/agentx_python.egg-info/top_level.txt +0 -0
  78. {agentx_python-0.8.11 → agentx_python-0.8.13}/setup.cfg +0 -0
  79. {agentx_python-0.8.11 → agentx_python-0.8.13}/setup.py +0 -0
  80. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_cli_launcher.py +0 -0
  81. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_deep_dive_fixes.py +0 -0
  82. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_docs_match_sdk.py +0 -0
  83. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_eval_scope.py +0 -0
  84. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_integration.py +0 -0
  85. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_integrations.py +0 -0
  86. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_judge_scorers.py +0 -0
  87. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_pairwise.py +0 -0
  88. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_review_queue.py +0 -0
  89. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_runner_features.py +0 -0
  90. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_selfhost_analysis_fallback.py +0 -0
  91. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_span_tree.py +0 -0
  92. {agentx_python-0.8.11 → agentx_python-0.8.13}/tests/test_testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.11
3
+ Version: 0.8.13
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
@@ -252,7 +252,12 @@ extra:
252
252
  | LlamaIndex | `pip install "agentx-python[llamaindex]"` | `AgentXLlamaIndexHandler` |
253
253
  | AutoGen | `pip install "agentx-python[autogen]"` | `AgentXAutoGenObserver` |
254
254
 
255
- Or plain Python - wrap any function with `@tracer.trace(...)` and it just works, no framework required.
255
+ Or plain Python - wrap any function with `@tracer.trace(...)` and it just works, no framework
256
+ required. Tracing is **platform agnostic**: each integration stamps its platform label
257
+ automatically, a plain trace auto-detects the one orchestration framework imported in the
258
+ process, and `framework="any-name"` labels platforms AgentX has never heard of - the label
259
+ drives the dashboard's framework filter and Monitor's Platforms chart. See
260
+ [Platform detection](TRACING.md#platform-detection).
256
261
 
257
262
  Running specialist agents in parallel with a `ThreadPoolExecutor`? Wrap each worker body in `tracer.use_span(span)` so their steps land on the parent trace instead of becoming independent traces - see [TRACING.md](TRACING.md) for the full pattern.
258
263
 
@@ -188,7 +188,12 @@ extra:
188
188
  | LlamaIndex | `pip install "agentx-python[llamaindex]"` | `AgentXLlamaIndexHandler` |
189
189
  | AutoGen | `pip install "agentx-python[autogen]"` | `AgentXAutoGenObserver` |
190
190
 
191
- Or plain Python - wrap any function with `@tracer.trace(...)` and it just works, no framework required.
191
+ Or plain Python - wrap any function with `@tracer.trace(...)` and it just works, no framework
192
+ required. Tracing is **platform agnostic**: each integration stamps its platform label
193
+ automatically, a plain trace auto-detects the one orchestration framework imported in the
194
+ process, and `framework="any-name"` labels platforms AgentX has never heard of - the label
195
+ drives the dashboard's framework filter and Monitor's Platforms chart. See
196
+ [Platform detection](TRACING.md#platform-detection).
192
197
 
193
198
  Running specialist agents in parallel with a `ThreadPoolExecutor`? Wrap each worker body in `tracer.use_span(span)` so their steps land on the parent trace instead of becoming independent traces - see [TRACING.md](TRACING.md) for the full pattern.
194
199
 
@@ -143,6 +143,9 @@ def finish_llm_call(
143
143
  input=input_repr,
144
144
  output=output,
145
145
  model=model,
146
+ # Stamp the provider literal on a span opened without one (adoption keeps an
147
+ # explicit framework= or a framework integration's label winning over this).
148
+ framework=framework,
146
149
  input_tokens=input_tokens,
147
150
  output_tokens=output_tokens,
148
151
  cache_read_tokens=cache_read_tokens,
@@ -268,6 +268,28 @@ class MonitorClient:
268
268
  plus deltas vs the prior window and the run-outcome breakdown."""
269
269
  return self._request("GET", "/kpis", params={"window": window})
270
270
 
271
+ def metrics(
272
+ self,
273
+ window: str = "1d",
274
+ *,
275
+ agent: Optional[str] = None,
276
+ model: Optional[str] = None,
277
+ tool: Optional[str] = None,
278
+ framework: Optional[str] = None,
279
+ status: Optional[str] = None,
280
+ ) -> dict:
281
+ """The Monitor metrics grid's data over a window ("1h".."90d") - bucketed spans by kind,
282
+ latency percentiles, tokens/cost, tool executions and failures, and platform attribution
283
+ (`frameworks` window totals + per-bucket `byFramework` - the Platforms chart). Optional
284
+ filters scope every number the way the dashboard's filter chips do; `framework` matches
285
+ the platform label traces carry (see TRACING.md's Platform detection), with "other"
286
+ selecting unlabeled traffic."""
287
+ params = {"window": window}
288
+ for key, value in (("agent", agent), ("model", model), ("tool", tool), ("framework", framework), ("status", status)):
289
+ if value is not None:
290
+ params[key] = value
291
+ return self._request("GET", "/metrics", params=params)
292
+
271
293
  def topics(self, window: str = "7d") -> dict:
272
294
  """The Topics view's data over a window ("24h", "7d", "30d"): LLM-classified themes of
273
295
  sampled production traffic with per-topic counts and sentiment. Empty until Topics is
@@ -0,0 +1,64 @@
1
+ """Best-effort agent-framework auto-detection (the "platform agnostic" story).
2
+
3
+ A span whose framework was neither passed explicitly (``tracer.trace(...,
4
+ framework="...")``) nor adopted from a framework integration (callback handler,
5
+ observer, patched client - see ``_TraceSpan._captured_framework``) gets labeled
6
+ by looking at which known ORCHESTRATION framework is actually imported in this
7
+ process. ``sys.modules`` is the signal - imported, not merely installed - so a
8
+ machine with ten frameworks pip-installed but one in use still resolves.
9
+
10
+ Only unambiguous answers are given: zero or more than one known framework
11
+ loaded means ``None``, and the span goes out unlabeled rather than mislabeled.
12
+ The user's explicit ``framework=`` always wins, including totally custom names
13
+ for platforms this table has never heard of.
14
+
15
+ Raw provider SDKs (openai, anthropic, google-genai, ...) are deliberately NOT
16
+ in this table: they are transitive dependencies of nearly every framework, so
17
+ their presence says nothing about what orchestrates the agent - and their
18
+ patched-client integrations already stamp the provider literal on the spans
19
+ they create.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import sys
25
+ from typing import Optional
26
+
27
+ # Top-level module name -> the wire literal the matching integration emits.
28
+ # Multiple modules may map to one literal (langgraph is the LangChain family).
29
+ _ORCHESTRATOR_MODULES = {
30
+ "langchain": "langchain",
31
+ "langchain_core": "langchain",
32
+ "langgraph": "langchain",
33
+ "crewai": "crewai",
34
+ "llama_index": "llamaindex",
35
+ "autogen": "autogen",
36
+ "autogen_agentchat": "autogen",
37
+ "agents": "openai-agents", # the OpenAI Agents SDK's import name
38
+ "google.adk": "google-adk",
39
+ "semantic_kernel": "semantic-kernel",
40
+ "haystack": "haystack",
41
+ "pydantic_ai": "pydantic-ai",
42
+ "smolagents": "smolagents",
43
+ "dspy": "dspy",
44
+ }
45
+
46
+
47
+ def _looks_like_openai_agents_sdk() -> bool:
48
+ # "agents" is a name any user package could claim - only trust it when the
49
+ # OpenAI Agents SDK's own submodules are loaded alongside it.
50
+ return "agents.run" in sys.modules or "agents.tracing" in sys.modules
51
+
52
+
53
+ def detect_framework() -> Optional[str]:
54
+ """The single unambiguous orchestration framework imported right now, else None."""
55
+ found: set = set()
56
+ for module, literal in _ORCHESTRATOR_MODULES.items():
57
+ if module not in sys.modules:
58
+ continue
59
+ if module == "agents" and not _looks_like_openai_agents_sdk():
60
+ continue
61
+ found.add(literal)
62
+ if len(found) > 1:
63
+ return None
64
+ return found.pop() if len(found) == 1 else None
@@ -428,6 +428,15 @@ class IngestClient:
428
428
  continue
429
429
 
430
430
  if resp.status_code in _RETRYABLE_STATUS and attempt < _MAX_RETRIES - 1:
431
+ # 429 = the engine's bounded ingest queue shedding load (its ADR-0005): honor
432
+ # Retry-After exactly instead of the generic backoff schedule, so the SDK backs
433
+ # off in step with the server's own flush cadence.
434
+ retry_after = resp.headers.get("Retry-After")
435
+ if resp.status_code == 429 and retry_after:
436
+ try:
437
+ time.sleep(min(30.0, float(retry_after)))
438
+ except ValueError:
439
+ pass
431
440
  last_exc = Exception(f"HTTP {resp.status_code}")
432
441
  continue
433
442
  if not resp.ok:
@@ -14,6 +14,7 @@ from agentx.exceptions import CIGateFailure
14
14
  from agentx.tracing.ingest_client import IngestClient
15
15
  from agentx.tracing.ci_types import CIRun, CIRunResult, CIRunStatus, CIQuestionScore
16
16
  from agentx.tracing.eval_scope import EVAL_RUN_SOURCE, current_eval_run_id
17
+ from agentx.tracing.framework_detect import detect_framework
17
18
 
18
19
  F = TypeVar("F", bound=Callable[..., Any])
19
20
 
@@ -125,6 +126,10 @@ class _TraceSpan:
125
126
  # Adopted from a merged child run (e.g. AgentXCallbackHandler) when this span itself
126
127
  # wasn't opened with an explicit framework= - see _merge_child_run below.
127
128
  self._captured_framework: Optional[str] = None
129
+ # Best-effort auto-detection (framework_detect.py), resolved once at span open so child
130
+ # spans emitted mid-flight carry the same label the root will. Lowest precedence:
131
+ # explicit framework= > integration adoption > this. None when ambiguous.
132
+ self._detected_framework: Optional[str] = None if framework else detect_framework()
128
133
  self._input_tokens: int = 0
129
134
  self._output_tokens: int = 0
130
135
  # Subsets of _input_tokens (not additional tokens) - a prompt-caching write/read, when the
@@ -191,7 +196,7 @@ class _TraceSpan:
191
196
  latency_ms=latency_ms,
192
197
  error=self._error,
193
198
  metadata=metadata,
194
- framework=self._framework or self._captured_framework,
199
+ framework=self._framework or self._captured_framework or self._detected_framework or detect_framework(),
195
200
  model=self._model or self._captured_model,
196
201
  tool_calls=self.tool_calls or None,
197
202
  session_id=self._session_id,
@@ -235,14 +240,18 @@ class _TraceSpan:
235
240
  input: Any = None,
236
241
  output: Any = None,
237
242
  model: Optional[str] = None,
243
+ framework: Optional[str] = None,
238
244
  input_tokens: Optional[int] = None,
239
245
  output_tokens: Optional[int] = None,
240
246
  cache_read_tokens: Optional[int] = None,
241
247
  cache_write_tokens: Optional[int] = None,
242
248
  ) -> None:
243
249
  """Record one LLM-call child span (e.g. one patched Anthropic call) under this span -
244
- name left unset so _merge_child_run auto-numbers it "LLM Call N"."""
250
+ name left unset so _merge_child_run auto-numbers it "LLM Call N". ``framework`` lets the
251
+ patched client stamp its provider literal on a span the user opened without one - the
252
+ adoption in _merge_child_run keeps explicit/integration labels winning."""
245
253
  self._merge_child_run(
254
+ framework=framework,
246
255
  execution_steps=[{
247
256
  "duration_ms": duration_ms,
248
257
  "start_time": start_time,
@@ -301,7 +310,7 @@ class _TraceSpan:
301
310
  child = _TraceSpan(
302
311
  tracer=self._tracer,
303
312
  name=name,
304
- framework=framework or self._framework or self._captured_framework,
313
+ framework=framework or self._framework or self._captured_framework or self._detected_framework,
305
314
  model=model,
306
315
  session_id=self._session_id,
307
316
  )
@@ -397,6 +406,13 @@ class _TraceSpan:
397
406
  under this span).
398
407
  """
399
408
  with self._merge_lock:
409
+ # Adopt framework/model BEFORE emitting child spans: child_span resolves its
410
+ # framework from this span's fields, so adopting after the emission loops used to
411
+ # send every CrewAI/AutoGen child out unlabeled while only the root got stamped.
412
+ if model and not self._captured_model:
413
+ self._captured_model = model
414
+ if framework and not self._captured_framework:
415
+ self._captured_framework = framework
400
416
  for step in [] if not emit_steps else (execution_steps or []):
401
417
  self._child_span_count += 1
402
418
  self.child_span(
@@ -463,10 +479,6 @@ class _TraceSpan:
463
479
  self.input = input
464
480
  if output is not None:
465
481
  self.output = output
466
- if model and not self._captured_model:
467
- self._captured_model = model
468
- if framework and not self._captured_framework:
469
- self._captured_framework = framework
470
482
  if input_tokens:
471
483
  self._input_tokens += input_tokens
472
484
  if output_tokens:
@@ -520,7 +532,8 @@ class _TraceSpan:
520
532
  # to be called from inside another active span.
521
533
  span = self._tracer.trace(
522
534
  self.name, metadata=self._metadata, framework=self._framework, model=self._model,
523
- session_id=self._session_id,
535
+ session_id=self._session_id, sync=self._sync, monitor=self._monitor,
536
+ pattern_ids=self._pattern_ids, agent_id=self._agent_id, span_kind=self._span_kind,
524
537
  )
525
538
  span.__enter__()
526
539
  try:
@@ -542,7 +555,8 @@ class _TraceSpan:
542
555
  # See _wrap_sync's comment - same "fresh span per call" reasoning applies here.
543
556
  span = self._tracer.trace(
544
557
  self.name, metadata=self._metadata, framework=self._framework, model=self._model,
545
- session_id=self._session_id,
558
+ session_id=self._session_id, sync=self._sync, monitor=self._monitor,
559
+ pattern_ids=self._pattern_ids, agent_id=self._agent_id, span_kind=self._span_kind,
546
560
  )
547
561
  span.__enter__()
548
562
  try:
@@ -911,6 +925,22 @@ class Tracer:
911
925
 
912
926
  with client.tracer.trace("support-agent", agent_id="ag_123", sync=True) as span:
913
927
  span.output = call_llm(...)
928
+
929
+ ``framework`` names the platform the agent runs on - tracing is platform agnostic, and
930
+ this label is how the dashboard's framework filter and Monitor's Platforms chart group
931
+ traffic. Three ways it gets set, strongest first:
932
+
933
+ 1. **Explicit**: ``framework="langchain"`` - any string works, including platforms
934
+ AgentX has no integration for (``framework="my-inhouse-runner"``).
935
+ 2. **Integration**: every integration stamps its own literal automatically -
936
+ ``langchain``, ``crewai``, ``openai-agents``, ``openai``, ``anthropic``,
937
+ ``google-genai``, ``google-adk``, ``litellm``, ``llamaindex``, ``autogen``,
938
+ ``moveworks``, ``databricks``.
939
+ 3. **Auto-detection**: with neither of the above, the SDK labels the span with the one
940
+ known orchestration framework imported in the process (LangChain/LangGraph, CrewAI,
941
+ LlamaIndex, AutoGen, OpenAI Agents SDK, Google ADK, Semantic Kernel, Haystack,
942
+ Pydantic AI, smolagents, DSPy). Ambiguous (several imported) or unknown -> the span
943
+ goes out unlabeled rather than mislabeled.
914
944
  """
915
945
  return _TraceSpan(
916
946
  tracer=self,
@@ -1,7 +1,7 @@
1
- VERSION = "0.8.11"
1
+ VERSION = "0.8.13"
2
2
 
3
3
  # The AgentX-trace-eval release this SDK version is tested against - what `agentx-trace-eval`
4
4
  # installs and converges to (see agentx/cli.py). Bump together with VERSION when releasing, so
5
5
  # every published SDK names a known-good engine+dashboard pair. Users can override with
6
6
  # AGENTX_TRACE_EVAL_VERSION=<tag|latest>.
7
- ENGINE_VERSION = "v0.3.2"
7
+ ENGINE_VERSION = "v0.3.7"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-python
3
- Version: 0.8.11
3
+ Version: 0.8.13
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
@@ -252,7 +252,12 @@ extra:
252
252
  | LlamaIndex | `pip install "agentx-python[llamaindex]"` | `AgentXLlamaIndexHandler` |
253
253
  | AutoGen | `pip install "agentx-python[autogen]"` | `AgentXAutoGenObserver` |
254
254
 
255
- Or plain Python - wrap any function with `@tracer.trace(...)` and it just works, no framework required.
255
+ Or plain Python - wrap any function with `@tracer.trace(...)` and it just works, no framework
256
+ required. Tracing is **platform agnostic**: each integration stamps its platform label
257
+ automatically, a plain trace auto-detects the one orchestration framework imported in the
258
+ process, and `framework="any-name"` labels platforms AgentX has never heard of - the label
259
+ drives the dashboard's framework filter and Monitor's Platforms chart. See
260
+ [Platform detection](TRACING.md#platform-detection).
256
261
 
257
262
  Running specialist agents in parallel with a `ThreadPoolExecutor`? Wrap each worker body in `tracer.use_span(span)` so their steps land on the parent trace instead of becoming independent traces - see [TRACING.md](TRACING.md) for the full pattern.
258
263
 
@@ -64,6 +64,7 @@ agentx/resources/workforce.py
64
64
  agentx/tracing/__init__.py
65
65
  agentx/tracing/ci_types.py
66
66
  agentx/tracing/eval_scope.py
67
+ agentx/tracing/framework_detect.py
67
68
  agentx/tracing/ingest_client.py
68
69
  agentx/tracing/tracer.py
69
70
  agentx_python.egg-info/PKG-INFO
@@ -77,6 +78,7 @@ tests/test_cli_launcher.py
77
78
  tests/test_deep_dive_fixes.py
78
79
  tests/test_docs_match_sdk.py
79
80
  tests/test_eval_scope.py
81
+ tests/test_framework_detect.py
80
82
  tests/test_integration.py
81
83
  tests/test_integrations.py
82
84
  tests/test_judge_scorers.py
@@ -0,0 +1,125 @@
1
+ """
2
+ Platform-agnostic framework capture: explicit framework= always wins, integrations stamp their
3
+ literal, and - new - the SDK auto-detects the single unambiguous orchestration framework
4
+ imported in the process (agentx/tracing/framework_detect.py). Also pins the two capture-gap
5
+ fixes: _merge_child_run adopts the framework BEFORE emitting child spans (CrewAI/AutoGen
6
+ children used to go out unlabeled), and _record_llm_call forwards the patched client's provider
7
+ literal onto a user-opened span that has no label of its own.
8
+
9
+ Same harness as test_span_tree.py: only the ingest_client boundary is mocked, so the real
10
+ _send/_dispatch/child_span paths run and every wire dict is inspectable.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ import types
16
+ from unittest.mock import MagicMock
17
+
18
+ import pytest
19
+
20
+ from agentx.tracing.framework_detect import _ORCHESTRATOR_MODULES, detect_framework
21
+ from agentx.tracing.tracer import Tracer
22
+
23
+
24
+ def make_tracer() -> Tracer:
25
+ return Tracer(ingest_client=MagicMock())
26
+
27
+
28
+ def enqueued_wires(tracer: Tracer) -> list:
29
+ return [call.args[0] for call in tracer._client.enqueue.call_args_list]
30
+
31
+
32
+ @pytest.fixture()
33
+ def clean_modules(monkeypatch):
34
+ """Remove every known orchestrator from sys.modules so each test states its own world."""
35
+ for module in list(_ORCHESTRATOR_MODULES) + ["agents.run", "agents.tracing"]:
36
+ monkeypatch.delitem(sys.modules, module, raising=False)
37
+ return monkeypatch
38
+
39
+
40
+ def fake_import(monkeypatch, name: str) -> None:
41
+ monkeypatch.setitem(sys.modules, name, types.ModuleType(name))
42
+
43
+
44
+ def test_detects_single_imported_orchestrator(clean_modules):
45
+ fake_import(clean_modules, "crewai")
46
+ assert detect_framework() == "crewai"
47
+ tracer = make_tracer()
48
+ with tracer.trace("agent"):
49
+ pass
50
+ assert enqueued_wires(tracer)[0]["framework"] == "crewai"
51
+
52
+
53
+ def test_no_orchestrator_means_no_label(clean_modules):
54
+ assert detect_framework() is None
55
+ tracer = make_tracer()
56
+ with tracer.trace("agent"):
57
+ pass
58
+ assert "framework" not in enqueued_wires(tracer)[0]
59
+
60
+
61
+ def test_ambiguous_imports_stay_unlabeled(clean_modules):
62
+ fake_import(clean_modules, "crewai")
63
+ fake_import(clean_modules, "llama_index")
64
+ assert detect_framework() is None
65
+
66
+
67
+ def test_langchain_family_collapses_to_one_literal(clean_modules):
68
+ # langgraph + langchain_core together are ONE framework, not an ambiguity.
69
+ fake_import(clean_modules, "langchain_core")
70
+ fake_import(clean_modules, "langgraph")
71
+ assert detect_framework() == "langchain"
72
+
73
+
74
+ def test_agents_module_needs_sdk_submodules(clean_modules):
75
+ fake_import(clean_modules, "agents") # could be anyone's package named "agents"
76
+ assert detect_framework() is None
77
+ fake_import(clean_modules, "agents.run")
78
+ assert detect_framework() == "openai-agents"
79
+
80
+
81
+ def test_explicit_framework_beats_detection(clean_modules):
82
+ fake_import(clean_modules, "crewai")
83
+ tracer = make_tracer()
84
+ with tracer.trace("agent", framework="my-inhouse-runner"):
85
+ pass
86
+ assert enqueued_wires(tracer)[0]["framework"] == "my-inhouse-runner"
87
+
88
+
89
+ def test_children_inherit_detected_framework(clean_modules):
90
+ fake_import(clean_modules, "llama_index")
91
+ tracer = make_tracer()
92
+ with tracer.trace("agent") as span:
93
+ span.child_span("step", duration_ms=5)
94
+ wires = enqueued_wires(tracer)
95
+ assert [w.get("framework") for w in wires] == ["llamaindex", "llamaindex"]
96
+
97
+
98
+ def test_merge_child_run_adopts_before_emitting_children(clean_modules):
99
+ # The CrewAI/AutoGen shape: root opened with no framework, the merged sub-run carries it.
100
+ # Children must go out labeled too - adoption used to happen after emission.
101
+ tracer = make_tracer()
102
+ with tracer.trace("crew") as span:
103
+ span._merge_child_run(
104
+ framework="crewai",
105
+ execution_steps=[{"duration_ms": 10, "input": "q", "output": "a"}],
106
+ )
107
+ wires = enqueued_wires(tracer)
108
+ assert len(wires) == 2
109
+ assert all(w.get("framework") == "crewai" for w in wires)
110
+
111
+
112
+ def test_record_llm_call_stamps_provider_on_unlabeled_span(clean_modules):
113
+ tracer = make_tracer()
114
+ with tracer.trace("agent") as span:
115
+ span._record_llm_call(duration_ms=7, model="claude-x", framework="anthropic")
116
+ root = enqueued_wires(tracer)[-1]
117
+ assert root["framework"] == "anthropic"
118
+
119
+
120
+ def test_record_llm_call_never_overrides_explicit_framework(clean_modules):
121
+ tracer = make_tracer()
122
+ with tracer.trace("agent", framework="langchain") as span:
123
+ span._record_llm_call(duration_ms=7, model="gpt-x", framework="openai")
124
+ root = enqueued_wires(tracer)[-1]
125
+ assert root["framework"] == "langchain"
File without changes
File without changes
File without changes