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,254 @@
1
+ """
2
+ Haystack integration — implements Haystack's native ``tracing.Tracer`` /
3
+ ``tracing.Span`` interface so ``Pipeline.run()`` and ``Component.run()``
4
+ execution is captured as agent-trace spans.
5
+
6
+ Haystack 2.x ships its own instrumentation surface
7
+ (``haystack.tracing.Tracer`` / ``haystack.tracing.Span``, see
8
+ ``haystack/tracing/tracer.py``) that ``Pipeline.run()``
9
+ (``haystack/core/pipeline/pipeline.py``) and ``PipelineBase._run_component``
10
+ (``haystack/core/pipeline/base.py``) call directly — there is no callback
11
+ list to pass in, unlike LangGraph. A tracer implementation is registered
12
+ globally via ``haystack.tracing.enable_tracing(...)`` and every pipeline/
13
+ component run after that point is traced through it until
14
+ ``haystack.tracing.disable_tracing()`` is called.
15
+
16
+ Two spans are produced per pipeline run:
17
+
18
+ - ``haystack.pipeline.run`` — one span for the whole ``Pipeline.run()`` call,
19
+ tagged with the pipeline's input/output data and metadata.
20
+ - ``haystack.component.run`` — one span per component invocation (nested
21
+ under the pipeline span), tagged with the component's name, type, and I/O
22
+ socket spec. When Haystack's own content tracing is enabled (see below)
23
+ the component's *actual received arguments* and *actual returned output*
24
+ are attached too — this is the exact capability gap issue #4574 exposes:
25
+ a caller-to-callee argument-propagation bug that occurs entirely
26
+ in-process, before any HTTP request, so agent-trace's httpx/requests
27
+ interceptor is structurally the wrong layer to catch it.
28
+
29
+ Haystack gates raw input/output content behind its own
30
+ ``HAYSTACK_CONTENT_TRACING_ENABLED`` environment variable (default
31
+ ``false``) because pipeline inputs/outputs can contain arbitrary user
32
+ content — set it to ``"true"`` (or call
33
+ ``haystack.tracing.tracer.tracer.is_content_tracing_enabled = True``) to
34
+ capture that content in agent-trace spans as well.
35
+
36
+ Usage::
37
+
38
+ import haystack.tracing
39
+ from agent_trace import Tracer
40
+ from agent_trace.integrations.haystack import HaystackTracer
41
+
42
+ t = Tracer()
43
+ with t.start_trace("my_pipeline", record=True) as trace:
44
+ haystack.tracing.enable_tracing(HaystackTracer(tracer=t, trace=trace))
45
+ try:
46
+ result = pipeline.run({"component_name": {"value": "hello"}})
47
+ finally:
48
+ haystack.tracing.disable_tracing()
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import contextlib
54
+ import logging
55
+ import threading
56
+ from typing import TYPE_CHECKING, Any
57
+
58
+ from agent_trace.core.span import Span, SpanStatus
59
+
60
+ if TYPE_CHECKING:
61
+ from collections.abc import Iterator
62
+
63
+ from agent_trace import Trace, Tracer
64
+
65
+ __all__ = ["HaystackTracer"]
66
+
67
+ logger = logging.getLogger(__name__)
68
+
69
+ _INSTALL_HINT = (
70
+ "HaystackTracer requires haystack-ai.\n"
71
+ "Install it with:\n\n"
72
+ " pip install haystack-ai\n"
73
+ )
74
+
75
+ # agent_trace.core.span.Span attributes only accept str | int | float | bool;
76
+ # Haystack tags/content-tags are frequently dicts, lists, dataclasses, or
77
+ # Haystack domain objects (Document, ChatMessage, ...). Truncate the
78
+ # stringified form so one component's raw output can't blow up trace.json.
79
+ _MAX_TAG_LEN = 4000
80
+
81
+
82
+ def _require_haystack_tracing() -> Any:
83
+ """Lazy import guard — raises a clear error if haystack-ai is absent."""
84
+ try:
85
+ import haystack.tracing
86
+
87
+ return haystack.tracing
88
+ except ImportError as exc:
89
+ raise ImportError(_INSTALL_HINT) from exc
90
+
91
+
92
+ def _coerce_tag_value(value: Any) -> str | int | float | bool:
93
+ """Coerce an arbitrary Haystack tag value to a Span-attribute-safe primitive."""
94
+ if isinstance(value, str | int | float | bool):
95
+ return value
96
+ try:
97
+ text = repr(value)
98
+ except Exception:
99
+ text = f"<unrepr-able {type(value).__name__}>"
100
+ if len(text) > _MAX_TAG_LEN:
101
+ text = text[:_MAX_TAG_LEN] + "...<truncated>"
102
+ return text
103
+
104
+
105
+ # Module-level singletons: the concrete Tracer/Span implementations are built
106
+ # once (the first time _get_classes() is called) so that Haystack's
107
+ # tracing.Tracer / tracing.Span are real bases at class-definition time
108
+ # rather than spliced in later.
109
+ _HaystackTracerImplClass: type | None = None
110
+ _HaystackSpanImplClass: type | None = None
111
+ _classes_lock: threading.Lock = threading.Lock()
112
+
113
+
114
+ def _get_classes() -> tuple[type, type]:
115
+ """Return (and lazily build) the concrete Tracer/Span implementations."""
116
+ global _HaystackTracerImplClass, _HaystackSpanImplClass # noqa: PLW0603
117
+ if _HaystackTracerImplClass is not None and _HaystackSpanImplClass is not None:
118
+ return _HaystackTracerImplClass, _HaystackSpanImplClass
119
+
120
+ with _classes_lock:
121
+ if _HaystackTracerImplClass is not None and _HaystackSpanImplClass is not None:
122
+ return _HaystackTracerImplClass, _HaystackSpanImplClass
123
+
124
+ htracing = _require_haystack_tracing()
125
+ base_span_cls: type = htracing.Span
126
+ base_tracer_cls: type = htracing.Tracer
127
+
128
+ class _AgentTraceHaystackSpan(base_span_cls): # type: ignore[misc]
129
+ """Wraps an agent_trace Span behind Haystack's Span interface."""
130
+
131
+ def __init__(self, agent_span: Span) -> None:
132
+ self._agent_span = agent_span
133
+
134
+ def set_tag(self, key: str, value: Any) -> None:
135
+ self._agent_span.set_attribute(key, _coerce_tag_value(value))
136
+
137
+ def raw_span(self) -> Any:
138
+ """Expose the underlying agent_trace Span for direct access."""
139
+ return self._agent_span
140
+
141
+ class _AgentTraceHaystackTracerImpl(base_tracer_cls): # type: ignore[misc]
142
+ """Concrete implementation — see HaystackTracer for public docs."""
143
+
144
+ def __init__(self, tracer: Tracer, trace: Trace) -> None:
145
+ self._tracer: Tracer = tracer
146
+ self._trace: Trace = trace
147
+ # LIFO stack of open agent_trace spans, used to infer the
148
+ # parent span when Haystack doesn't pass one explicitly.
149
+ self._span_stack: list[Span] = []
150
+ self._lock: threading.Lock = threading.Lock()
151
+
152
+ @contextlib.contextmanager
153
+ def trace(
154
+ self,
155
+ operation_name: str,
156
+ tags: dict[str, Any] | None = None,
157
+ parent_span: Any | None = None,
158
+ ) -> Iterator[Any]:
159
+ """Open a span for *operation_name*, yield it, then close it.
160
+
161
+ Honors an explicit *parent_span* (Haystack passes the
162
+ pipeline-level span in when tracing a component run — see
163
+ ``PipelineBase._create_component_span``); falls back to the
164
+ innermost currently-open span on this tracer otherwise.
165
+ """
166
+ parent_id: str | None = None
167
+ if parent_span is not None:
168
+ raw = getattr(parent_span, "raw_span", lambda: None)()
169
+ if isinstance(raw, Span):
170
+ parent_id = raw.span_id
171
+ else:
172
+ with self._lock:
173
+ if self._span_stack:
174
+ parent_id = self._span_stack[-1].span_id
175
+
176
+ agent_span = self._tracer.start_span(
177
+ operation_name, parent_id=parent_id
178
+ )
179
+ wrapped = _AgentTraceHaystackSpan(agent_span)
180
+ if tags:
181
+ wrapped.set_tags(tags)
182
+
183
+ with self._lock:
184
+ self._span_stack.append(agent_span)
185
+ try:
186
+ yield wrapped
187
+ except Exception as exc:
188
+ agent_span.record_exception(exc)
189
+ if agent_span.end_time is None:
190
+ agent_span.end(SpanStatus.ERROR)
191
+ raise
192
+ else:
193
+ if agent_span.end_time is None:
194
+ agent_span.end(SpanStatus.OK)
195
+ finally:
196
+ with self._lock:
197
+ if self._span_stack and self._span_stack[-1] is agent_span:
198
+ self._span_stack.pop()
199
+ else:
200
+ # Defensive: out-of-order close (shouldn't happen
201
+ # with well-behaved context managers) — drop it
202
+ # wherever it is rather than corrupting the stack.
203
+ with contextlib.suppress(ValueError):
204
+ self._span_stack.remove(agent_span)
205
+
206
+ def current_span(self) -> Any | None:
207
+ """Return the innermost currently-open span, or None."""
208
+ with self._lock:
209
+ if not self._span_stack:
210
+ return None
211
+ return _AgentTraceHaystackSpan(self._span_stack[-1])
212
+
213
+ _HaystackTracerImplClass = _AgentTraceHaystackTracerImpl
214
+ _HaystackSpanImplClass = _AgentTraceHaystackSpan
215
+ return _HaystackTracerImplClass, _HaystackSpanImplClass
216
+
217
+
218
+ class HaystackTracer:
219
+ """Haystack ``tracing.Tracer`` implementation that emits agent-trace spans.
220
+
221
+ Register an instance globally with ``haystack.tracing.enable_tracing(...)``
222
+ before calling ``pipeline.run()`` / ``pipeline.run_async()``; every
223
+ ``haystack.pipeline.run`` and ``haystack.component.run`` span Haystack
224
+ creates internally is forwarded to agent-trace for the lifetime of the
225
+ registration. Call ``haystack.tracing.disable_tracing()`` (or register a
226
+ fresh ``NullTracer``) when done so later pipeline runs outside the
227
+ ``with tracer.start_trace(...)`` block aren't captured into a closed
228
+ trace.
229
+
230
+ ``haystack-ai`` is imported lazily — importing this module succeeds even
231
+ when it is not installed.
232
+
233
+ Parameters
234
+ ----------
235
+ tracer:
236
+ The active :class:`~agent_trace.Tracer` instance.
237
+ trace:
238
+ The :class:`~agent_trace.Trace` that spans will be registered on.
239
+ """
240
+
241
+ def __new__(cls, tracer: Tracer, trace: Trace) -> HaystackTracer:
242
+ # Construct the concrete impl directly so Python's normal
243
+ # type.__call__ runs _AgentTraceHaystackTracerImpl.__init__
244
+ # automatically. See LangGraphTracer.__new__ for why this two-step
245
+ # __new__/__init__ split is required rather than mutating __bases__.
246
+ impl_cls, _ = _get_classes()
247
+ return impl_cls(tracer, trace) # type: ignore[no-any-return]
248
+
249
+ def __init__(self, tracer: Tracer, trace: Trace) -> None:
250
+ # __init__ is called on the instance whose __class__ is already the
251
+ # concrete impl class (set by __new__). This path is only reached if
252
+ # someone subclasses HaystackTracer directly; normal construction
253
+ # goes through the impl class __init__.
254
+ pass # pragma: no cover
@@ -0,0 +1,361 @@
1
+ """
2
+ Generic, framework-agnostic LangChain callback integration.
3
+
4
+ ``LangGraphTracer`` (``agent_trace.integrations.langgraph``) only attaches
5
+ to a compiled LangGraph ``StateGraph`` — it hard-codes LangGraph-specific
6
+ span-naming conventions (``node:<name>``, ``branch:dispatch``, checkpointer
7
+ tracking, ...) and is documented as a LangGraph integration. The much
8
+ larger population of plain-LangChain users — anyone calling
9
+ ``Runnable.invoke()``/``.batch()``/``.ainvoke()`` directly (a document
10
+ compressor pipeline, a bare `Chain`, a `Retriever`, ...) with no LangGraph
11
+ graph anywhere in sight — has had *no* chain-level integration at all: only
12
+ the raw HTTP interceptor sees anything, and an application-level exception
13
+ raised inside a Runnable's own Python code (not during an HTTP call) is
14
+ invisible everywhere, confirmed via reproduction against issue #31192 (a
15
+ `DocumentCompressorPipeline`'s `IndexError` in `_parse_ranking` attaches to
16
+ nothing — ``Tracer.start_trace``'s exception handler only records onto
17
+ spans already present in ``trace.spans``, and with no chain-level
18
+ integration wired in, none exist).
19
+
20
+ ``LangChainTracer`` closes that gap: a plain ``BaseCallbackHandler``
21
+ implementation that works with *any* LangChain `Runnable`, not just a
22
+ LangGraph graph — pass it into ``config={"callbacks": [...]}`` on any
23
+ ``.invoke()``/``.batch()``/``.ainvoke()`` call. On error, it calls
24
+ ``Span.record_exception()`` on a span it opens itself for that Runnable
25
+ invocation, the same mechanism LangGraphTracer uses, but without any
26
+ LangGraph-specific assumptions about span naming or graph structure.
27
+
28
+ Usage::
29
+
30
+ from agent_trace import Tracer
31
+ from agent_trace.integrations.langchain_core import LangChainTracer
32
+
33
+ t = Tracer(trace_dir=...)
34
+ with t.start_trace("my-runnable", record=True) as trace:
35
+ cb = LangChainTracer(tracer=t, trace=trace)
36
+ result = my_runnable.invoke(input, config={"callbacks": [cb]})
37
+
38
+ Requires ``langchain-core`` (``pip install agent-trace[langchain]``) —
39
+ notably *not* ``langgraph``, unlike ``LangGraphTracer``.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import json
45
+ import logging
46
+ import threading
47
+ import uuid
48
+ from typing import TYPE_CHECKING, Any
49
+
50
+ from agent_trace.core.span import Span, SpanStatus
51
+
52
+ if TYPE_CHECKING:
53
+ from agent_trace import Trace, Tracer
54
+
55
+ __all__ = ["LangChainTracer"]
56
+
57
+ logger = logging.getLogger(__name__)
58
+
59
+ _INSTALL_HINT = (
60
+ "LangChainTracer requires langchain-core.\n"
61
+ "Install it with:\n\n"
62
+ " pip install agent-trace[langchain]\n"
63
+ )
64
+
65
+ _MAX_ATTR_LEN = 8_000
66
+
67
+
68
+ def _to_attr_string(value: Any, *, max_len: int = _MAX_ATTR_LEN) -> str:
69
+ """Best-effort, bounded string form of an arbitrary LangChain value
70
+ (inputs/outputs dicts, message lists, ...) — mirrors
71
+ agent_trace.integrations.langgraph._to_attr_string, duplicated (not
72
+ imported) so this module has zero dependency on langgraph.py and,
73
+ transitively, on the ``langgraph`` package itself."""
74
+ try:
75
+ text = json.dumps(value, default=str, ensure_ascii=False)
76
+ except Exception:
77
+ text = str(value)
78
+ if len(text) > max_len:
79
+ text = text[:max_len] + "...<truncated>"
80
+ return text
81
+
82
+
83
+ def _require_langchain_core() -> Any:
84
+ try:
85
+ import langchain_core
86
+
87
+ return langchain_core
88
+ except ImportError as exc:
89
+ raise ImportError(_INSTALL_HINT) from exc
90
+
91
+
92
+ def _base_callback_handler() -> Any:
93
+ _require_langchain_core()
94
+ from langchain_core.callbacks import BaseCallbackHandler
95
+
96
+ return BaseCallbackHandler
97
+
98
+
99
+ _LangChainTracerClass: type | None = None
100
+ _tracer_class_lock: threading.Lock = threading.Lock()
101
+
102
+
103
+ def _get_tracer_class() -> type:
104
+ """Return (and lazily build) the concrete LangChainTracer
105
+ implementation, with BaseCallbackHandler as a genuine base class at
106
+ definition time — same pattern as
107
+ agent_trace.integrations.langgraph._get_tracer_class."""
108
+ global _LangChainTracerClass # noqa: PLW0603
109
+ if _LangChainTracerClass is not None:
110
+ return _LangChainTracerClass
111
+
112
+ with _tracer_class_lock:
113
+ if _LangChainTracerClass is not None:
114
+ return _LangChainTracerClass
115
+
116
+ BaseCallbackHandler = _base_callback_handler()
117
+
118
+ class _LangChainTracerImpl(BaseCallbackHandler): # type: ignore[misc,valid-type]
119
+ """Generic BaseCallbackHandler — see module docstring."""
120
+
121
+ def __init__(self, tracer: Tracer, trace: Trace) -> None:
122
+ self._tracer = tracer
123
+ self._trace = trace
124
+ self._spans: dict[str, Span] = {}
125
+ self._lock: threading.Lock = threading.Lock()
126
+
127
+ # -- span lifecycle -------------------------------------------
128
+
129
+ def _open_span(
130
+ self,
131
+ run_id: uuid.UUID | str,
132
+ name: str,
133
+ parent_run_id: uuid.UUID | str | None = None,
134
+ ) -> Span:
135
+ run_key = str(run_id)
136
+ parent_span_id: str | None = None
137
+ if parent_run_id is not None:
138
+ with self._lock:
139
+ parent_span = self._spans.get(str(parent_run_id))
140
+ parent_span_id = (
141
+ parent_span.span_id if parent_span is not None else None
142
+ )
143
+ span = self._tracer.start_span(name, parent_id=parent_span_id)
144
+ with self._lock:
145
+ self._spans[run_key] = span
146
+ return span
147
+
148
+ def _close_span(
149
+ self, run_id: uuid.UUID | str, status: SpanStatus = SpanStatus.OK
150
+ ) -> Span | None:
151
+ run_key = str(run_id)
152
+ with self._lock:
153
+ span = self._spans.pop(run_key, None)
154
+ if span is not None and span.end_time is None:
155
+ span.end(status)
156
+ return span
157
+
158
+ def _close_span_with_exception(
159
+ self, run_id: uuid.UUID | str, error: BaseException
160
+ ) -> None:
161
+ run_key = str(run_id)
162
+ with self._lock:
163
+ span = self._spans.pop(run_key, None)
164
+ if span is None:
165
+ return
166
+ span.record_exception(error)
167
+ if span.end_time is None:
168
+ span.end(SpanStatus.ERROR)
169
+
170
+ # -- chain callbacks -------------------------------------------
171
+
172
+ def on_chain_start(
173
+ self,
174
+ serialized: dict[str, Any] | None,
175
+ inputs: dict[str, Any],
176
+ *,
177
+ run_id: uuid.UUID,
178
+ parent_run_id: uuid.UUID | None = None,
179
+ tags: list[str] | None = None,
180
+ metadata: dict[str, Any] | None = None,
181
+ **kwargs: Any,
182
+ ) -> None:
183
+ ser = serialized or {}
184
+ name = (
185
+ kwargs.get("name")
186
+ or ser.get("name")
187
+ or (ser.get("id") or [None])[-1]
188
+ or "chain"
189
+ )
190
+ span = self._open_span(run_id, f"chain:{name}", parent_run_id)
191
+ if tags:
192
+ span.set_attribute("chain.tags", ",".join(tags))
193
+ if metadata:
194
+ span.set_attribute("chain.metadata", _to_attr_string(metadata))
195
+ if inputs:
196
+ span.set_attribute("chain.inputs", _to_attr_string(inputs))
197
+
198
+ def on_chain_end(
199
+ self,
200
+ outputs: Any,
201
+ *,
202
+ run_id: uuid.UUID,
203
+ parent_run_id: uuid.UUID | None = None,
204
+ **kwargs: Any,
205
+ ) -> None:
206
+ run_key = str(run_id)
207
+ with self._lock:
208
+ span = self._spans.get(run_key)
209
+ if span is not None and outputs:
210
+ try:
211
+ span.set_attribute("chain.outputs", _to_attr_string(outputs))
212
+ except Exception:
213
+ logger.debug(
214
+ "agent-trace: failed to record chain outputs for "
215
+ "run %r",
216
+ run_key,
217
+ exc_info=True,
218
+ )
219
+ self._close_span(run_id, SpanStatus.OK)
220
+
221
+ def on_chain_error(
222
+ self,
223
+ error: BaseException,
224
+ *,
225
+ run_id: uuid.UUID,
226
+ parent_run_id: uuid.UUID | None = None,
227
+ **kwargs: Any,
228
+ ) -> None:
229
+ self._close_span_with_exception(run_id, error)
230
+
231
+ # -- LLM callbacks -----------------------------------------------
232
+
233
+ def on_llm_start(
234
+ self,
235
+ serialized: dict[str, Any] | None,
236
+ prompts: list[str],
237
+ *,
238
+ run_id: uuid.UUID,
239
+ parent_run_id: uuid.UUID | None = None,
240
+ **kwargs: Any,
241
+ ) -> None:
242
+ ser = serialized or {}
243
+ model_name = (
244
+ (ser.get("kwargs") or {}).get("model_name")
245
+ or (ser.get("kwargs") or {}).get("model")
246
+ or ser.get("name")
247
+ or "llm"
248
+ )
249
+ span = self._open_span(run_id, f"llm:{model_name}", parent_run_id)
250
+ span.set_attribute("llm.model", str(model_name))
251
+
252
+ def on_chat_model_start(
253
+ self,
254
+ serialized: dict[str, Any] | None,
255
+ messages: list[list[Any]],
256
+ *,
257
+ run_id: uuid.UUID,
258
+ parent_run_id: uuid.UUID | None = None,
259
+ **kwargs: Any,
260
+ ) -> None:
261
+ ser = serialized or {}
262
+ model_name = (
263
+ (ser.get("kwargs") or {}).get("model_name")
264
+ or (ser.get("kwargs") or {}).get("model")
265
+ or ser.get("name")
266
+ or "llm"
267
+ )
268
+ span = self._open_span(run_id, f"llm:{model_name}", parent_run_id)
269
+ span.set_attribute("llm.model", str(model_name))
270
+ try:
271
+ span.set_attribute("llm.messages", _to_attr_string(messages))
272
+ except Exception:
273
+ logger.debug(
274
+ "agent-trace: failed to record chat model messages "
275
+ "for run %r",
276
+ str(run_id),
277
+ exc_info=True,
278
+ )
279
+
280
+ def on_llm_end(
281
+ self,
282
+ response: Any,
283
+ *,
284
+ run_id: uuid.UUID,
285
+ parent_run_id: uuid.UUID | None = None,
286
+ **kwargs: Any,
287
+ ) -> None:
288
+ self._close_span(run_id, SpanStatus.OK)
289
+
290
+ def on_llm_error(
291
+ self,
292
+ error: BaseException,
293
+ *,
294
+ run_id: uuid.UUID,
295
+ parent_run_id: uuid.UUID | None = None,
296
+ **kwargs: Any,
297
+ ) -> None:
298
+ self._close_span_with_exception(run_id, error)
299
+
300
+ # -- tool callbacks -------------------------------------------
301
+
302
+ def on_tool_start(
303
+ self,
304
+ serialized: dict[str, Any] | None,
305
+ input_str: str,
306
+ *,
307
+ run_id: uuid.UUID,
308
+ parent_run_id: uuid.UUID | None = None,
309
+ **kwargs: Any,
310
+ ) -> None:
311
+ ser = serialized or {}
312
+ tool_name = kwargs.get("name") or ser.get("name") or "tool"
313
+ span = self._open_span(run_id, f"tool:{tool_name}", parent_run_id)
314
+ span.set_attribute("tool.name", str(tool_name))
315
+ if input_str:
316
+ span.set_attribute("tool.input", _to_attr_string(input_str))
317
+
318
+ def on_tool_end(
319
+ self,
320
+ output: Any,
321
+ *,
322
+ run_id: uuid.UUID,
323
+ parent_run_id: uuid.UUID | None = None,
324
+ **kwargs: Any,
325
+ ) -> None:
326
+ run_key = str(run_id)
327
+ with self._lock:
328
+ span = self._spans.get(run_key)
329
+ if span is not None and output is not None:
330
+ try:
331
+ span.set_attribute("tool.output", _to_attr_string(output))
332
+ except Exception:
333
+ logger.debug(
334
+ "agent-trace: failed to record tool output for "
335
+ "run %r",
336
+ run_key,
337
+ exc_info=True,
338
+ )
339
+ self._close_span(run_id, SpanStatus.OK)
340
+
341
+ def on_tool_error(
342
+ self,
343
+ error: BaseException,
344
+ *,
345
+ run_id: uuid.UUID,
346
+ parent_run_id: uuid.UUID | None = None,
347
+ **kwargs: Any,
348
+ ) -> None:
349
+ self._close_span_with_exception(run_id, error)
350
+
351
+ _LangChainTracerClass = _LangChainTracerImpl
352
+ return _LangChainTracerClass
353
+
354
+
355
+ def LangChainTracer(tracer: Tracer, trace: Trace) -> Any: # noqa: N802
356
+ """Construct a generic LangChain ``BaseCallbackHandler`` — see module
357
+ docstring for usage. Works with any ``Runnable``, not just a compiled
358
+ LangGraph graph (that's ``LangGraphTracer``, a separate, LangGraph-
359
+ specific integration)."""
360
+ cls = _get_tracer_class()
361
+ return cls(tracer=tracer, trace=trace)