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,472 @@
|
|
|
1
|
+
"""
|
|
2
|
+
llama_index integration — Dispatcher-based span/event capture.
|
|
3
|
+
|
|
4
|
+
llama_index does not accept a per-call ``callbacks=[...]`` list the way
|
|
5
|
+
LangGraph or the OpenAI Agents SDK do. Instead it exposes a global
|
|
6
|
+
instrumentation surface, ``llama_index.core.instrumentation``, built around a
|
|
7
|
+
tree of ``Dispatcher`` objects (one per module) that fan out to attached
|
|
8
|
+
``BaseSpanHandler``/``BaseEventHandler`` instances. Every method decorated
|
|
9
|
+
with ``@dispatcher.span`` — which covers essentially all public entry points
|
|
10
|
+
on ``BaseQueryEngine``, ``BaseRetriever``, ``BaseChatEngine``, ``BaseLLM``,
|
|
11
|
+
``BaseTool``, agent workers/workflows, etc., via the ``DispatcherSpanMixin`` —
|
|
12
|
+
fires ``span_enter``/``span_exit``/``span_drop`` on every handler reachable
|
|
13
|
+
from the dispatcher tree's root. Semantic events (``LLMChatStartEvent``,
|
|
14
|
+
``AgentToolCallEvent``, ``ExceptionEvent``, ...) are fired independently and
|
|
15
|
+
carry the ``span_id`` of whatever span was active when they were dispatched,
|
|
16
|
+
so a ``BaseEventHandler`` can attribute them back to the span that produced
|
|
17
|
+
them.
|
|
18
|
+
|
|
19
|
+
``LlamaIndexTracer`` implements both interfaces and, once installed on a
|
|
20
|
+
dispatcher (the root dispatcher by default — every leaf dispatcher's parent
|
|
21
|
+
chain terminates there, so this covers the whole library), turns every
|
|
22
|
+
llama_index span into an agent-trace ``Span`` with the correct parent/child
|
|
23
|
+
nesting, and enriches those spans with chat-history / tool-call / agent-step
|
|
24
|
+
data pulled out of the semantic events.
|
|
25
|
+
|
|
26
|
+
Usage (context manager — recommended, scopes install/uninstall to the block):
|
|
27
|
+
|
|
28
|
+
from agent_trace import tracer
|
|
29
|
+
from agent_trace.integrations.llama_index import LlamaIndexTracer
|
|
30
|
+
|
|
31
|
+
with tracer.start_trace("my_query_engine", record=True) as trace:
|
|
32
|
+
with LlamaIndexTracer(tracer=tracer, trace=trace):
|
|
33
|
+
response = query_engine.query("What is agent-trace?")
|
|
34
|
+
|
|
35
|
+
Usage (manual install/uninstall — e.g. a long-lived process):
|
|
36
|
+
|
|
37
|
+
li_tracer = LlamaIndexTracer(tracer=tracer, trace=trace)
|
|
38
|
+
li_tracer.install()
|
|
39
|
+
...
|
|
40
|
+
li_tracer.uninstall()
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import logging
|
|
46
|
+
import re
|
|
47
|
+
import threading
|
|
48
|
+
from typing import TYPE_CHECKING, Any
|
|
49
|
+
|
|
50
|
+
from agent_trace.core.span import Span, SpanStatus
|
|
51
|
+
from agent_trace.interceptor.httpx_hook import pop_correlation_id, push_correlation_id
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from agent_trace import Trace, Tracer
|
|
55
|
+
|
|
56
|
+
__all__ = ["LlamaIndexTracer"]
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
_INSTALL_HINT = (
|
|
61
|
+
"LlamaIndexTracer requires llama-index-core.\n"
|
|
62
|
+
"Install it with:\n\n"
|
|
63
|
+
" pip install llama-index-core\n"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# ``Dispatcher.span()`` mints ids as "{ClassName}.{method_name}-{uuid4}" for
|
|
67
|
+
# instance methods, or "{func.__qualname__}-{uuid4}" for plain functions
|
|
68
|
+
# (see llama_index_instrumentation.dispatcher.Dispatcher.span). Stripping the
|
|
69
|
+
# trailing uuid4 gives a stable, human-readable span name.
|
|
70
|
+
_UUID_SUFFIX_RE = re.compile(
|
|
71
|
+
r"-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
72
|
+
re.IGNORECASE,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
_TRUNCATE_LEN = 2000
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _span_name(id_: str) -> str:
|
|
79
|
+
"""Derive a human-readable span name from a dispatcher span id."""
|
|
80
|
+
stripped = _UUID_SUFFIX_RE.sub("", id_)
|
|
81
|
+
return stripped or id_
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _truncate(value: Any) -> str:
|
|
85
|
+
"""Stringify and bound *value* so a single field can't blow up a span."""
|
|
86
|
+
text = str(value)
|
|
87
|
+
if len(text) <= _TRUNCATE_LEN:
|
|
88
|
+
return text
|
|
89
|
+
return text[:_TRUNCATE_LEN] + "...<truncated>"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _require_llama_index() -> Any:
|
|
93
|
+
"""Lazy import guard — raises a clear error if llama-index-core is absent."""
|
|
94
|
+
try:
|
|
95
|
+
import llama_index.core
|
|
96
|
+
|
|
97
|
+
return llama_index.core
|
|
98
|
+
except ImportError as exc:
|
|
99
|
+
raise ImportError(_INSTALL_HINT) from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _apply_llm_chat_start(span: Span, event: Any) -> None:
|
|
103
|
+
messages = getattr(event, "messages", None) or []
|
|
104
|
+
span.set_attribute("llm.messages_count", len(messages))
|
|
105
|
+
model_dict = getattr(event, "model_dict", None) or {}
|
|
106
|
+
model = model_dict.get("model") or model_dict.get("model_name")
|
|
107
|
+
if model:
|
|
108
|
+
span.set_attribute("llm.model", str(model))
|
|
109
|
+
if messages:
|
|
110
|
+
last = messages[-1]
|
|
111
|
+
span.set_attribute("llm.last_message_role", str(getattr(last, "role", "")))
|
|
112
|
+
span.set_attribute(
|
|
113
|
+
"llm.last_message_content", _truncate(getattr(last, "content", ""))
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _apply_llm_chat_end(span: Span, event: Any) -> None:
|
|
118
|
+
response = getattr(event, "response", None)
|
|
119
|
+
message = getattr(response, "message", None) if response is not None else None
|
|
120
|
+
if message is None:
|
|
121
|
+
return
|
|
122
|
+
span.set_attribute(
|
|
123
|
+
"llm.response_content", _truncate(getattr(message, "content", ""))
|
|
124
|
+
)
|
|
125
|
+
additional_kwargs = getattr(message, "additional_kwargs", None) or {}
|
|
126
|
+
span.set_attribute("llm.has_tool_calls", bool(additional_kwargs.get("tool_calls")))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _apply_llm_completion_start(span: Span, event: Any) -> None:
|
|
130
|
+
prompt = getattr(event, "prompt", None)
|
|
131
|
+
if prompt is not None:
|
|
132
|
+
span.set_attribute("llm.prompt", _truncate(prompt))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _apply_llm_completion_end(span: Span, event: Any) -> None:
|
|
136
|
+
response = getattr(event, "response", None)
|
|
137
|
+
if response is not None:
|
|
138
|
+
span.set_attribute(
|
|
139
|
+
"llm.response_content", _truncate(getattr(response, "text", ""))
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _apply_agent_tool_call(span: Span, event: Any) -> None:
|
|
144
|
+
tool = getattr(event, "tool", None)
|
|
145
|
+
tool_name = getattr(tool, "name", None) if tool is not None else None
|
|
146
|
+
span.add_event(
|
|
147
|
+
"tool_call",
|
|
148
|
+
attributes={
|
|
149
|
+
"tool.name": str(tool_name) if tool_name else "unknown",
|
|
150
|
+
"tool.arguments": _truncate(getattr(event, "arguments", "")),
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _apply_agent_run_step_start(span: Span, event: Any) -> None:
|
|
156
|
+
task_id = getattr(event, "task_id", None)
|
|
157
|
+
step_input = getattr(event, "input", None)
|
|
158
|
+
if task_id is not None:
|
|
159
|
+
span.set_attribute("agent.task_id", str(task_id))
|
|
160
|
+
if step_input is not None:
|
|
161
|
+
span.set_attribute("agent.step_input", _truncate(step_input))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _apply_agent_run_step_end(span: Span, event: Any) -> None:
|
|
165
|
+
step_output = getattr(event, "step_output", None)
|
|
166
|
+
if step_output is not None:
|
|
167
|
+
span.set_attribute("agent.step_output", _truncate(step_output))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _apply_exception(span: Span, event: Any) -> None:
|
|
171
|
+
exc = getattr(event, "exception", None)
|
|
172
|
+
if isinstance(exc, BaseException):
|
|
173
|
+
span.record_exception(exc)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Dispatch on event.class_name() (a plain string every BaseEvent subclass
|
|
177
|
+
# implements) rather than importing each concrete event class — this keeps
|
|
178
|
+
# the handler resilient to llama_index moving event classes between modules
|
|
179
|
+
# across versions, since only the shape (attribute names), not the import
|
|
180
|
+
# path, is relied upon.
|
|
181
|
+
_EVENT_HANDLERS: dict[str, Any] = {
|
|
182
|
+
"LLMChatStartEvent": _apply_llm_chat_start,
|
|
183
|
+
"LLMChatEndEvent": _apply_llm_chat_end,
|
|
184
|
+
"LLMCompletionStartEvent": _apply_llm_completion_start,
|
|
185
|
+
"LLMCompletionEndEvent": _apply_llm_completion_end,
|
|
186
|
+
"AgentToolCallEvent": _apply_agent_tool_call,
|
|
187
|
+
"AgentRunStepStartEvent": _apply_agent_run_step_start,
|
|
188
|
+
"AgentRunStepEndEvent": _apply_agent_run_step_end,
|
|
189
|
+
"ExceptionEvent": _apply_exception,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _apply_event(span: Span, event: Any) -> None:
|
|
194
|
+
"""Enrich *span* with data pulled out of a dispatched llama_index event."""
|
|
195
|
+
handler = _EVENT_HANDLERS.get(event.class_name())
|
|
196
|
+
if handler is not None:
|
|
197
|
+
handler(span, event)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# Lazily-built concrete handler classes
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
#
|
|
204
|
+
# BaseSpanHandler / BaseEventHandler / BaseSpan are pydantic BaseModel
|
|
205
|
+
# subclasses defined by llama_index_instrumentation. Subclassing them at
|
|
206
|
+
# module import time would require llama-index-core to be installed just to
|
|
207
|
+
# `import agent_trace.integrations.llama_index`. Instead — same pattern as
|
|
208
|
+
# integrations/langgraph.py's _get_tracer_class() — the concrete impl classes
|
|
209
|
+
# are built exactly once, the first time a LlamaIndexTracer is constructed,
|
|
210
|
+
# with the real base classes as genuine bases.
|
|
211
|
+
|
|
212
|
+
_SpanHandlerClass: type | None = None
|
|
213
|
+
_EventHandlerClass: type | None = None
|
|
214
|
+
_classes_lock: threading.Lock = threading.Lock()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _get_handler_classes() -> tuple[type, type]:
|
|
218
|
+
"""Return (and lazily build) the concrete span/event handler classes."""
|
|
219
|
+
global _SpanHandlerClass, _EventHandlerClass # noqa: PLW0603
|
|
220
|
+
if _SpanHandlerClass is not None and _EventHandlerClass is not None:
|
|
221
|
+
return _SpanHandlerClass, _EventHandlerClass
|
|
222
|
+
|
|
223
|
+
with _classes_lock:
|
|
224
|
+
if _SpanHandlerClass is not None and _EventHandlerClass is not None:
|
|
225
|
+
return _SpanHandlerClass, _EventHandlerClass
|
|
226
|
+
|
|
227
|
+
_require_llama_index()
|
|
228
|
+
from llama_index.core.instrumentation.event_handlers import BaseEventHandler
|
|
229
|
+
from llama_index.core.instrumentation.span.base import BaseSpan
|
|
230
|
+
from llama_index.core.instrumentation.span_handlers import BaseSpanHandler
|
|
231
|
+
from pydantic import PrivateAttr
|
|
232
|
+
|
|
233
|
+
class _AgentTraceSpan(BaseSpan): # type: ignore[misc]
|
|
234
|
+
"""Minimal BaseSpan — the real agent-trace Span lives in the
|
|
235
|
+
handler's own registry (keyed by the same dispatcher span id),
|
|
236
|
+
not on this pydantic model."""
|
|
237
|
+
|
|
238
|
+
class _AgentTraceSpanHandlerImpl(BaseSpanHandler[_AgentTraceSpan]): # type: ignore[misc]
|
|
239
|
+
"""Turns dispatcher span_enter/span_exit/span_drop into agent-trace
|
|
240
|
+
Spans, using the dispatcher-supplied parent_span_id for nesting."""
|
|
241
|
+
|
|
242
|
+
_tracer: Any = PrivateAttr(default=None)
|
|
243
|
+
_registry: dict[str, Span] = PrivateAttr(default_factory=dict)
|
|
244
|
+
_registry_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
|
245
|
+
# Per-span correlation-id contextvar tokens (#13449): every
|
|
246
|
+
# span new_span() creates pushes its own span_id as the active
|
|
247
|
+
# httpx_hook correlation id for the duration it's open, so any
|
|
248
|
+
# HTTP exchange made anywhere inside it is recoverable
|
|
249
|
+
# afterwards via Fixture.exchanges_for_correlation_id(span_id)
|
|
250
|
+
# — the same mechanism LangGraphTracer uses for #6037. Same
|
|
251
|
+
# documented scope/limitation: reliable for synchronous
|
|
252
|
+
# dispatch, not guaranteed to propagate across llama_index's
|
|
253
|
+
# own async/concurrent execution paths.
|
|
254
|
+
_correlation_tokens: dict[str, Any] = PrivateAttr(default_factory=dict)
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def class_name(cls) -> str:
|
|
258
|
+
return "AgentTraceSpanHandler"
|
|
259
|
+
|
|
260
|
+
def new_span(
|
|
261
|
+
self,
|
|
262
|
+
id_: str,
|
|
263
|
+
bound_args: Any,
|
|
264
|
+
instance: Any | None = None,
|
|
265
|
+
parent_span_id: str | None = None,
|
|
266
|
+
tags: dict[str, Any] | None = None,
|
|
267
|
+
**kwargs: Any,
|
|
268
|
+
) -> _AgentTraceSpan:
|
|
269
|
+
parent_id: str | None = None
|
|
270
|
+
if parent_span_id is not None:
|
|
271
|
+
with self._registry_lock:
|
|
272
|
+
parent_span = self._registry.get(parent_span_id)
|
|
273
|
+
if parent_span is not None:
|
|
274
|
+
parent_id = parent_span.span_id
|
|
275
|
+
|
|
276
|
+
span = self._tracer.start_span(_span_name(id_), parent_id=parent_id)
|
|
277
|
+
span.set_attribute("llama_index.span_id", id_)
|
|
278
|
+
if instance is not None:
|
|
279
|
+
span.set_attribute("llama_index.class", type(instance).__name__)
|
|
280
|
+
with self._registry_lock:
|
|
281
|
+
self._registry[id_] = span
|
|
282
|
+
try:
|
|
283
|
+
self._correlation_tokens[id_] = push_correlation_id(
|
|
284
|
+
span.span_id
|
|
285
|
+
)
|
|
286
|
+
except Exception:
|
|
287
|
+
logger.debug(
|
|
288
|
+
"agent-trace: failed to push correlation id for "
|
|
289
|
+
"llama_index span %r",
|
|
290
|
+
id_,
|
|
291
|
+
exc_info=True,
|
|
292
|
+
)
|
|
293
|
+
return _AgentTraceSpan(
|
|
294
|
+
id_=id_, parent_id=parent_span_id, tags=tags or {}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _pop_correlation_token(self, id_: str) -> None:
|
|
298
|
+
with self._registry_lock:
|
|
299
|
+
token = self._correlation_tokens.pop(id_, None)
|
|
300
|
+
if token is None:
|
|
301
|
+
return
|
|
302
|
+
try:
|
|
303
|
+
pop_correlation_id(token)
|
|
304
|
+
except Exception:
|
|
305
|
+
logger.debug(
|
|
306
|
+
"agent-trace: failed to pop correlation id for "
|
|
307
|
+
"llama_index span %r",
|
|
308
|
+
id_,
|
|
309
|
+
exc_info=True,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def prepare_to_exit_span(
|
|
313
|
+
self,
|
|
314
|
+
id_: str,
|
|
315
|
+
bound_args: Any,
|
|
316
|
+
instance: Any | None = None,
|
|
317
|
+
result: Any | None = None,
|
|
318
|
+
**kwargs: Any,
|
|
319
|
+
) -> _AgentTraceSpan | None:
|
|
320
|
+
with self._registry_lock:
|
|
321
|
+
span = self._registry.pop(id_, None)
|
|
322
|
+
self._pop_correlation_token(id_)
|
|
323
|
+
if span is not None and span.end_time is None:
|
|
324
|
+
span.end(SpanStatus.OK)
|
|
325
|
+
return self.open_spans.get(id_) # type: ignore[no-any-return]
|
|
326
|
+
|
|
327
|
+
def prepare_to_drop_span(
|
|
328
|
+
self,
|
|
329
|
+
id_: str,
|
|
330
|
+
bound_args: Any,
|
|
331
|
+
instance: Any | None = None,
|
|
332
|
+
err: BaseException | None = None,
|
|
333
|
+
**kwargs: Any,
|
|
334
|
+
) -> _AgentTraceSpan | None:
|
|
335
|
+
if id_ not in self.open_spans:
|
|
336
|
+
return None
|
|
337
|
+
with self._registry_lock:
|
|
338
|
+
span = self._registry.pop(id_, None)
|
|
339
|
+
self._pop_correlation_token(id_)
|
|
340
|
+
if span is not None:
|
|
341
|
+
try:
|
|
342
|
+
if err is not None:
|
|
343
|
+
span.record_exception(err)
|
|
344
|
+
if span.end_time is None:
|
|
345
|
+
span.end(
|
|
346
|
+
SpanStatus.ERROR if err is not None else SpanStatus.OK
|
|
347
|
+
)
|
|
348
|
+
except Exception:
|
|
349
|
+
logger.debug(
|
|
350
|
+
"agent-trace: failed to close dropped llama_index span %r",
|
|
351
|
+
id_,
|
|
352
|
+
exc_info=True,
|
|
353
|
+
)
|
|
354
|
+
return self.open_spans.get(id_) # type: ignore[no-any-return]
|
|
355
|
+
|
|
356
|
+
class _AgentTraceEventHandlerImpl(BaseEventHandler): # type: ignore[misc]
|
|
357
|
+
"""Routes semantic llama_index events onto the currently-open
|
|
358
|
+
agent-trace span they belong to (matched via event.span_id)."""
|
|
359
|
+
|
|
360
|
+
_registry: dict[str, Span] = PrivateAttr(default_factory=dict)
|
|
361
|
+
_registry_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def class_name(cls) -> str:
|
|
365
|
+
return "AgentTraceEventHandler"
|
|
366
|
+
|
|
367
|
+
def handle(self, event: Any, **kwargs: Any) -> None:
|
|
368
|
+
span_id = getattr(event, "span_id", None)
|
|
369
|
+
if not span_id:
|
|
370
|
+
return
|
|
371
|
+
with self._registry_lock:
|
|
372
|
+
span = self._registry.get(span_id)
|
|
373
|
+
if span is None:
|
|
374
|
+
return
|
|
375
|
+
try:
|
|
376
|
+
_apply_event(span, event)
|
|
377
|
+
except Exception:
|
|
378
|
+
logger.debug(
|
|
379
|
+
"agent-trace: failed to apply llama_index event %r "
|
|
380
|
+
"onto span %r",
|
|
381
|
+
getattr(event, "class_name", lambda: "?")(),
|
|
382
|
+
span_id,
|
|
383
|
+
exc_info=True,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
_SpanHandlerClass = _AgentTraceSpanHandlerImpl
|
|
387
|
+
_EventHandlerClass = _AgentTraceEventHandlerImpl
|
|
388
|
+
return _SpanHandlerClass, _EventHandlerClass
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class LlamaIndexTracer:
|
|
392
|
+
"""Installs agent-trace span/event capture onto a llama_index Dispatcher.
|
|
393
|
+
|
|
394
|
+
Unlike ``LangGraphTracer``/``AgentTraceHook``, this is not passed into a
|
|
395
|
+
single call — llama_index's instrumentation is dispatcher-global, so this
|
|
396
|
+
object is *installed* onto a dispatcher (the root dispatcher, by default)
|
|
397
|
+
for the duration of the work you want captured, then *uninstalled*.
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
tracer:
|
|
402
|
+
The active :class:`~agent_trace.Tracer` instance.
|
|
403
|
+
trace:
|
|
404
|
+
The :class:`~agent_trace.Trace` that spans will be registered on.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
def __init__(self, tracer: Tracer, trace: Trace) -> None:
|
|
408
|
+
span_handler_cls, event_handler_cls = _get_handler_classes()
|
|
409
|
+
|
|
410
|
+
self._tracer: Tracer = tracer
|
|
411
|
+
self._trace: Trace = trace
|
|
412
|
+
# Shared registry: llama_index dispatcher span id -> open agent-trace
|
|
413
|
+
# Span. The span handler populates/pops it; the event handler only
|
|
414
|
+
# reads it, so events can be attributed to the span that produced
|
|
415
|
+
# them (event.span_id matches the dispatcher span id verbatim).
|
|
416
|
+
self._registry: dict[str, Span] = {}
|
|
417
|
+
self._registry_lock: threading.Lock = threading.Lock()
|
|
418
|
+
|
|
419
|
+
self._span_handler: Any = span_handler_cls()
|
|
420
|
+
self._span_handler._tracer = tracer
|
|
421
|
+
self._span_handler._registry = self._registry
|
|
422
|
+
self._span_handler._registry_lock = self._registry_lock
|
|
423
|
+
|
|
424
|
+
self._event_handler: Any = event_handler_cls()
|
|
425
|
+
self._event_handler._registry = self._registry
|
|
426
|
+
self._event_handler._registry_lock = self._registry_lock
|
|
427
|
+
|
|
428
|
+
self._installed_on: Any = None
|
|
429
|
+
|
|
430
|
+
# ------------------------------------------------------------------
|
|
431
|
+
# Install / uninstall
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
def install(self, dispatcher: Any = None) -> None:
|
|
435
|
+
"""Attach this tracer's handlers to *dispatcher* (root by default).
|
|
436
|
+
|
|
437
|
+
The root dispatcher's parent chain is the terminus for every other
|
|
438
|
+
dispatcher in the process (``Dispatcher.span_enter``/``event`` walk
|
|
439
|
+
up ``c.parent`` until a non-propagating dispatcher is hit, and the
|
|
440
|
+
root dispatcher is created with ``propagate=False``), so handlers
|
|
441
|
+
attached to root see every span/event fired anywhere in llama_index.
|
|
442
|
+
"""
|
|
443
|
+
if dispatcher is None:
|
|
444
|
+
_require_llama_index()
|
|
445
|
+
from llama_index.core.instrumentation import get_dispatcher
|
|
446
|
+
|
|
447
|
+
dispatcher = get_dispatcher()
|
|
448
|
+
|
|
449
|
+
dispatcher.add_span_handler(self._span_handler)
|
|
450
|
+
dispatcher.add_event_handler(self._event_handler)
|
|
451
|
+
self._installed_on = dispatcher
|
|
452
|
+
|
|
453
|
+
def uninstall(self) -> None:
|
|
454
|
+
"""Detach this tracer's handlers from whatever dispatcher they were
|
|
455
|
+
installed on. Safe to call even if never installed."""
|
|
456
|
+
dispatcher = self._installed_on
|
|
457
|
+
if dispatcher is None:
|
|
458
|
+
return
|
|
459
|
+
dispatcher.span_handlers = [
|
|
460
|
+
h for h in dispatcher.span_handlers if h is not self._span_handler
|
|
461
|
+
]
|
|
462
|
+
dispatcher.event_handlers = [
|
|
463
|
+
h for h in dispatcher.event_handlers if h is not self._event_handler
|
|
464
|
+
]
|
|
465
|
+
self._installed_on = None
|
|
466
|
+
|
|
467
|
+
def __enter__(self) -> LlamaIndexTracer:
|
|
468
|
+
self.install()
|
|
469
|
+
return self
|
|
470
|
+
|
|
471
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
472
|
+
self.uninstall()
|