agent-observability-trace-cli 0.1.2__py3-none-any.whl

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 (51) hide show
  1. agent_observability_trace_cli-0.1.2.dist-info/METADATA +336 -0
  2. agent_observability_trace_cli-0.1.2.dist-info/RECORD +51 -0
  3. agent_observability_trace_cli-0.1.2.dist-info/WHEEL +4 -0
  4. agent_observability_trace_cli-0.1.2.dist-info/entry_points.txt +2 -0
  5. agent_observability_trace_cli-0.1.2.dist-info/licenses/LICENSE +198 -0
  6. agent_trace/__init__.py +1182 -0
  7. agent_trace/_cli.py +1377 -0
  8. agent_trace/_inspect.py +2020 -0
  9. agent_trace/_replay/__init__.py +1 -0
  10. agent_trace/_replay/engine.py +268 -0
  11. agent_trace/_replay/fixture.py +868 -0
  12. agent_trace/core/__init__.py +1 -0
  13. agent_trace/core/clock.py +91 -0
  14. agent_trace/core/exceptions.py +51 -0
  15. agent_trace/core/span.py +271 -0
  16. agent_trace/core/trace.py +92 -0
  17. agent_trace/exporters/__init__.py +1 -0
  18. agent_trace/exporters/file.py +80 -0
  19. agent_trace/exporters/otlp.py +199 -0
  20. agent_trace/exporters/remote_fixture.py +307 -0
  21. agent_trace/exporters/stdout.py +258 -0
  22. agent_trace/integrations/__init__.py +69 -0
  23. agent_trace/integrations/agno.py +489 -0
  24. agent_trace/integrations/autogen.py +581 -0
  25. agent_trace/integrations/crewai.py +486 -0
  26. agent_trace/integrations/google_genai.py +731 -0
  27. agent_trace/integrations/haystack.py +254 -0
  28. agent_trace/integrations/langchain_core.py +361 -0
  29. agent_trace/integrations/langgraph.py +2093 -0
  30. agent_trace/integrations/langgraph_checkpoint.py +763 -0
  31. agent_trace/integrations/langgraph_state_diff.py +345 -0
  32. agent_trace/integrations/langgraph_stream_debug.py +202 -0
  33. agent_trace/integrations/llama_index.py +472 -0
  34. agent_trace/integrations/mcp.py +281 -0
  35. agent_trace/integrations/openai_agents.py +811 -0
  36. agent_trace/integrations/pydantic_ai.py +504 -0
  37. agent_trace/integrations/streaming.py +218 -0
  38. agent_trace/interceptor/__init__.py +64 -0
  39. agent_trace/interceptor/aiohttp_hook.py +152 -0
  40. agent_trace/interceptor/botocore_hook.py +223 -0
  41. agent_trace/interceptor/grpc_hook.py +651 -0
  42. agent_trace/interceptor/httpx_hook.py +866 -0
  43. agent_trace/interceptor/logging_hook.py +137 -0
  44. agent_trace/interceptor/requests_patch.py +184 -0
  45. agent_trace/interceptor/sse.py +176 -0
  46. agent_trace/interceptor/stdio_hook.py +245 -0
  47. agent_trace/interceptor/warnings_hook.py +132 -0
  48. agent_trace/interceptor/websocket_hook.py +323 -0
  49. agent_trace/plugins/__init__.py +35 -0
  50. agent_trace/plugins/base.py +111 -0
  51. agent_trace/py.typed +0 -0
@@ -0,0 +1,258 @@
1
+ """
2
+ Console exporter — prints a trace as a colored span tree using rich.
3
+ Falls back to plain text if rich is not installed.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ from agent_trace import Span, Trace
13
+
14
+ __all__ = [
15
+ "StdoutExporter",
16
+ "export",
17
+ ]
18
+
19
+ # Colour map: SpanStatus value -> rich colour name
20
+ _STATUS_COLOUR: dict[str, str] = {
21
+ "OK": "green",
22
+ "ERROR": "red",
23
+ "UNSET": "yellow",
24
+ }
25
+
26
+ # Plain-text symbol map
27
+ _STATUS_SYMBOL: dict[str, str] = {
28
+ "OK": "[OK]",
29
+ "ERROR": "[ERR]",
30
+ "UNSET": "[---]",
31
+ }
32
+
33
+
34
+ # Span attributes worth surfacing inline in the tree view — the two numbers
35
+ # needed to check "did latency grow along with prompt size across turns"
36
+ # (#2920) are otherwise only reachable by manually opening trace.json and
37
+ # cross-referencing two separate JSON keys per span.
38
+ _INLINE_ATTRIBUTE_KEYS: tuple[str, ...] = (
39
+ "llm.model",
40
+ "llm.usage.prompt_tokens",
41
+ "llm.usage.completion_tokens",
42
+ "llm.usage.total_tokens",
43
+ )
44
+
45
+
46
+ def _inline_attributes_suffix(attributes: Mapping[str, object]) -> str:
47
+ """Render a compact ``key=value`` suffix for the subset of *attributes*
48
+ worth showing inline, or "" if none of them are present."""
49
+ parts = [
50
+ f"{key}={attributes[key]}"
51
+ for key in _INLINE_ATTRIBUTE_KEYS
52
+ if key in attributes
53
+ ]
54
+ return f" ({', '.join(parts)})" if parts else ""
55
+
56
+
57
+ def _exception_message(span: Span) -> str | None:
58
+ """Return the exception.message text captured on *span* via
59
+ Span.record_exception(), or None if it has no recorded exception event
60
+ — the data Span.record_exception() (core/span.py) already captures on
61
+ every ERROR span, but which was previously invisible in this tree view
62
+ (a developer saw "[ERR] llm:<model>" with no indication of why)."""
63
+ for event in span.events:
64
+ if event.name == "exception" and "exception.message" in event.attributes:
65
+ return str(event.attributes["exception.message"])
66
+ return None
67
+
68
+
69
+ def _unaccounted_ms(span: Span, children: list[Span] | None) -> float | None:
70
+ """Return *span*'s duration_ms minus the sum of its direct children's
71
+ duration_ms, or None when it can't be computed (span or any child is
72
+ still open, or there are no children — nothing to subtract).
73
+
74
+ Surfaces the gap a developer previously had to manually eyeball —
75
+ "this node took far longer than the LLM call inside it did" — e.g.
76
+ #2920's reporters posting screenshots of exactly this pattern with no
77
+ quantified figure to point at.
78
+ """
79
+ if span.duration_ms is None or not children:
80
+ return None
81
+ child_total = 0.0
82
+ for child in children:
83
+ if child.duration_ms is None:
84
+ return None # a still-open child makes the subtraction meaningless
85
+ child_total += child.duration_ms
86
+ return span.duration_ms - child_total
87
+
88
+
89
+ def _unaccounted_suffix(span: Span, children: list[Span] | None) -> str:
90
+ unaccounted = _unaccounted_ms(span, children)
91
+ if unaccounted is None:
92
+ return ""
93
+ return f" [{unaccounted:.1f}ms unaccounted of {span.duration_ms:.1f}ms]"
94
+
95
+
96
+ def _exception_http_detail(span: Span) -> str | None:
97
+ """Return "HTTP <status>: <body preview>" when *span*'s recorded
98
+ exception carried an HTTP error response body (exception.http_
99
+ response_body/exception.http_status_code, set by Span.record_exception
100
+ for requests.exceptions.HTTPError/httpx.HTTPStatusError-shaped
101
+ exceptions) — the actual provider/proxy error text (#4940), which
102
+ str(exc) alone (all `_exception_message` surfaces) typically omits."""
103
+ for event in span.events:
104
+ if event.name != "exception":
105
+ continue
106
+ body = event.attributes.get("exception.http_response_body")
107
+ if not body:
108
+ continue
109
+ status = event.attributes.get("exception.http_status_code")
110
+ prefix = f"HTTP {status}: " if status is not None else "HTTP: "
111
+ return f"{prefix}{body}"
112
+ return None
113
+
114
+
115
+ def _trace_header_info(trace: Trace) -> tuple[str, str]:
116
+ """Return (duration_string, display_name) for trace header lines."""
117
+ ended = [s.end_time for s in trace.spans if s.end_time is not None]
118
+ started = [s.start_time for s in trace.spans]
119
+ dur_str = ""
120
+ if started and ended:
121
+ total_ms = (max(ended) - min(started)) * 1_000
122
+ dur_str = f" ({total_ms:.1f} ms total)"
123
+ display_name = str(trace.metadata.get("name", trace.run_id))
124
+ return dur_str, display_name
125
+
126
+
127
+ class StdoutExporter:
128
+ """Export a :class:`~agent_trace.Trace` as a human-readable span tree.
129
+
130
+ Uses ``rich`` for coloured output when available; otherwise falls back to
131
+ plain indented ASCII.
132
+ """
133
+
134
+ def export(self, trace: Trace) -> None:
135
+ """Print the full span tree for *trace* to stdout."""
136
+ try:
137
+ self._export_rich(trace)
138
+ except ImportError:
139
+ self._export_plain(trace)
140
+
141
+ def export_span(
142
+ self, span: Span, depth: int = 0, children: list[Span] | None = None
143
+ ) -> None:
144
+ """Export a single span at the given indent depth (plain-text only).
145
+
146
+ *children*, when supplied, enables the "N ms unaccounted of M ms
147
+ total" suffix (duration_ms minus the sum of direct children's
148
+ duration_ms) — omitted entirely when not supplied, so existing
149
+ single-span callers are unaffected.
150
+ """
151
+ indent = " " * depth
152
+ sym = _STATUS_SYMBOL.get(span.status.value, "[---]")
153
+ dur = f" ({span.duration_ms:.1f} ms)" if span.duration_ms is not None else ""
154
+ attrs_suffix = _inline_attributes_suffix(span.attributes)
155
+ unaccounted_suffix = _unaccounted_suffix(span, children)
156
+ print(f"{indent}{sym} {span.name}{dur}{attrs_suffix}{unaccounted_suffix}")
157
+ if span.status.value == "ERROR":
158
+ message = _exception_message(span)
159
+ if message:
160
+ print(f"{indent} ! {message}")
161
+ http_detail = _exception_http_detail(span)
162
+ if http_detail:
163
+ print(f"{indent} ! {http_detail}")
164
+
165
+ # ------------------------------------------------------------------
166
+ # Rich implementation
167
+ # ------------------------------------------------------------------
168
+
169
+ def _export_rich(self, trace: Trace) -> None:
170
+ from rich.console import Console
171
+ from rich.tree import Tree
172
+
173
+ console = Console()
174
+ dur_str, display_name = _trace_header_info(trace)
175
+ rich_dur = f" [dim]({dur_str.strip()})[/dim]" if dur_str else ""
176
+
177
+ root_label = (
178
+ f"[bold cyan]Trace:[/bold cyan] {display_name}"
179
+ f" [dim]{trace.run_id}[/dim]{rich_dur}"
180
+ )
181
+ tree = Tree(root_label)
182
+
183
+ # Build parent->children map first so tree construction is order-independent.
184
+ children_map: dict[str | None, list[Any]] = {}
185
+ for span in trace.spans:
186
+ children_map.setdefault(span.parent_id, []).append(span)
187
+
188
+ def _add_children(parent_node: Any, parent_id: str | None) -> None:
189
+ for span in children_map.get(parent_id, []):
190
+ colour = _STATUS_COLOUR.get(span.status.value, "yellow")
191
+ dur = (
192
+ f" [dim]({span.duration_ms:.1f} ms)[/dim]"
193
+ if span.duration_ms is not None
194
+ else ""
195
+ )
196
+ attrs_suffix = _inline_attributes_suffix(span.attributes)
197
+ dim_attrs = (
198
+ f" [dim]{attrs_suffix.strip()}[/dim]" if attrs_suffix else ""
199
+ )
200
+ unaccounted_suffix = _unaccounted_suffix(
201
+ span, children_map.get(span.span_id)
202
+ )
203
+ dim_unaccounted = (
204
+ f" [dim]{unaccounted_suffix.strip()}[/dim]"
205
+ if unaccounted_suffix
206
+ else ""
207
+ )
208
+ label = (
209
+ f"[{colour}]{span.name}[/{colour}]"
210
+ f" [{colour}]{span.status.value}[/{colour}]"
211
+ f"{dur}{dim_attrs}{dim_unaccounted}"
212
+ )
213
+ if span.status.value == "ERROR":
214
+ message = _exception_message(span)
215
+ if message:
216
+ label += f"\n [red]! {message}[/red]"
217
+ http_detail = _exception_http_detail(span)
218
+ if http_detail:
219
+ label += f"\n [red]! {http_detail}[/red]"
220
+ node = parent_node.add(label)
221
+ _add_children(node, span.span_id)
222
+
223
+ _add_children(tree, None)
224
+ console.print(tree)
225
+
226
+ # ------------------------------------------------------------------
227
+ # Plain-text fallback
228
+ # ------------------------------------------------------------------
229
+
230
+ def _export_plain(self, trace: Trace) -> None:
231
+ dur_str, display_name = _trace_header_info(trace)
232
+ print(f"Trace: {display_name} [{trace.run_id}]{dur_str}")
233
+
234
+ # Build parent->children mapping for tree printing
235
+ children: dict[str | None, list[Span]] = {}
236
+ for span in trace.spans:
237
+ children.setdefault(span.parent_id, []).append(span)
238
+
239
+ def _print_subtree(parent_id: str | None, depth: int) -> None:
240
+ for span in children.get(parent_id, []):
241
+ self.export_span(
242
+ span, depth=depth, children=children.get(span.span_id)
243
+ )
244
+ _print_subtree(span.span_id, depth + 1)
245
+
246
+ _print_subtree(None, depth=1)
247
+
248
+
249
+ # ---------------------------------------------------------------------------
250
+ # Module-level convenience
251
+ # ---------------------------------------------------------------------------
252
+
253
+ _default_exporter: StdoutExporter = StdoutExporter()
254
+
255
+
256
+ def export(trace: Trace) -> None:
257
+ """Export *trace* to stdout using the default :class:`StdoutExporter`."""
258
+ _default_exporter.export(trace)
@@ -0,0 +1,69 @@
1
+ """
2
+ agent-trace framework integrations.
3
+
4
+ Each submodule wraps a specific agent framework's callback/event-hook
5
+ surface to emit agent-trace spans (e.g. ``langgraph.py`` for LangGraph,
6
+ ``openai_agents.py`` for the OpenAI Agents SDK, ``crewai.py`` for crewAI).
7
+
8
+ Submodules are exported lazily here (PEP 562 module ``__getattr__``) so that
9
+ ``import agent_trace.integrations`` never fails regardless of which optional
10
+ framework SDKs happen to be installed — none of the submodules themselves
11
+ import their target SDK at module import time either (see langgraph.py /
12
+ openai_agents.py, which defer the real ``import langgraph`` / ``import
13
+ agents`` to call time inside the class/function that needs it). Accessing a
14
+ specific submodule (e.g. ``agent_trace.integrations.crewai`` or ``from
15
+ agent_trace.integrations import crewai``) triggers that submodule's own
16
+ import; only *calling* something inside it that actually needs the SDK
17
+ raises a helpful ``ImportError`` with an install hint if the SDK is
18
+ missing.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import importlib
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ if TYPE_CHECKING:
27
+ from agent_trace.integrations import (
28
+ agno,
29
+ autogen,
30
+ crewai,
31
+ google_genai,
32
+ haystack,
33
+ langgraph,
34
+ langgraph_checkpoint,
35
+ langgraph_state_diff,
36
+ langgraph_stream_debug,
37
+ llama_index,
38
+ mcp,
39
+ openai_agents,
40
+ pydantic_ai,
41
+ streaming,
42
+ )
43
+
44
+ __all__ = [
45
+ "agno",
46
+ "autogen",
47
+ "crewai",
48
+ "google_genai",
49
+ "haystack",
50
+ "langgraph",
51
+ "langgraph_checkpoint",
52
+ "langgraph_state_diff",
53
+ "langgraph_stream_debug",
54
+ "llama_index",
55
+ "mcp",
56
+ "openai_agents",
57
+ "pydantic_ai",
58
+ "streaming",
59
+ ]
60
+
61
+
62
+ def __getattr__(name: str) -> Any:
63
+ if name in __all__:
64
+ return importlib.import_module(f"agent_trace.integrations.{name}")
65
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
66
+
67
+
68
+ def __dir__() -> list[str]:
69
+ return sorted(__all__)