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.
- agent_observability_trace_cli-0.1.2.dist-info/METADATA +336 -0
- agent_observability_trace_cli-0.1.2.dist-info/RECORD +51 -0
- agent_observability_trace_cli-0.1.2.dist-info/WHEEL +4 -0
- agent_observability_trace_cli-0.1.2.dist-info/entry_points.txt +2 -0
- agent_observability_trace_cli-0.1.2.dist-info/licenses/LICENSE +198 -0
- agent_trace/__init__.py +1182 -0
- agent_trace/_cli.py +1377 -0
- agent_trace/_inspect.py +2020 -0
- agent_trace/_replay/__init__.py +1 -0
- agent_trace/_replay/engine.py +268 -0
- agent_trace/_replay/fixture.py +868 -0
- agent_trace/core/__init__.py +1 -0
- agent_trace/core/clock.py +91 -0
- agent_trace/core/exceptions.py +51 -0
- agent_trace/core/span.py +271 -0
- agent_trace/core/trace.py +92 -0
- agent_trace/exporters/__init__.py +1 -0
- agent_trace/exporters/file.py +80 -0
- agent_trace/exporters/otlp.py +199 -0
- agent_trace/exporters/remote_fixture.py +307 -0
- agent_trace/exporters/stdout.py +258 -0
- agent_trace/integrations/__init__.py +69 -0
- agent_trace/integrations/agno.py +489 -0
- agent_trace/integrations/autogen.py +581 -0
- agent_trace/integrations/crewai.py +486 -0
- agent_trace/integrations/google_genai.py +731 -0
- agent_trace/integrations/haystack.py +254 -0
- agent_trace/integrations/langchain_core.py +361 -0
- agent_trace/integrations/langgraph.py +2093 -0
- agent_trace/integrations/langgraph_checkpoint.py +763 -0
- agent_trace/integrations/langgraph_state_diff.py +345 -0
- agent_trace/integrations/langgraph_stream_debug.py +202 -0
- agent_trace/integrations/llama_index.py +472 -0
- agent_trace/integrations/mcp.py +281 -0
- agent_trace/integrations/openai_agents.py +811 -0
- agent_trace/integrations/pydantic_ai.py +504 -0
- agent_trace/integrations/streaming.py +218 -0
- agent_trace/interceptor/__init__.py +64 -0
- agent_trace/interceptor/aiohttp_hook.py +152 -0
- agent_trace/interceptor/botocore_hook.py +223 -0
- agent_trace/interceptor/grpc_hook.py +651 -0
- agent_trace/interceptor/httpx_hook.py +866 -0
- agent_trace/interceptor/logging_hook.py +137 -0
- agent_trace/interceptor/requests_patch.py +184 -0
- agent_trace/interceptor/sse.py +176 -0
- agent_trace/interceptor/stdio_hook.py +245 -0
- agent_trace/interceptor/warnings_hook.py +132 -0
- agent_trace/interceptor/websocket_hook.py +323 -0
- agent_trace/plugins/__init__.py +35 -0
- agent_trace/plugins/base.py +111 -0
- 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)
|