fiddler-langgraph 0.1.0rc1__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.
- fiddler_langgraph/VERSION +1 -0
- fiddler_langgraph/__init__.py +11 -0
- fiddler_langgraph/core/__init__.py +1 -0
- fiddler_langgraph/core/attributes.py +87 -0
- fiddler_langgraph/core/client.py +318 -0
- fiddler_langgraph/core/span_processor.py +31 -0
- fiddler_langgraph/tracing/__init__.py +1 -0
- fiddler_langgraph/tracing/callback.py +795 -0
- fiddler_langgraph/tracing/instrumentation.py +264 -0
- fiddler_langgraph/tracing/jsonl_capture.py +185 -0
- fiddler_langgraph/tracing/util.py +83 -0
- fiddler_langgraph-0.1.0rc1.dist-info/METADATA +323 -0
- fiddler_langgraph-0.1.0rc1.dist-info/RECORD +15 -0
- fiddler_langgraph-0.1.0rc1.dist-info/WHEEL +5 -0
- fiddler_langgraph-0.1.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
"""Callback handler for LangGraph instrumentation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
11
|
+
from langchain_core.documents import Document
|
|
12
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
|
|
13
|
+
from langchain_core.outputs import ChatGeneration, LLMResult
|
|
14
|
+
from opentelemetry import trace
|
|
15
|
+
from opentelemetry.context.context import Context
|
|
16
|
+
|
|
17
|
+
from fiddler_langgraph.core.attributes import (
|
|
18
|
+
_CONVERSATION_ID,
|
|
19
|
+
FIDDLER_METADATA_KEY,
|
|
20
|
+
FIDDLER_USER_SPAN_ATTRIBUTE_TEMPLATE,
|
|
21
|
+
FiddlerSpanAttributes,
|
|
22
|
+
SpanType,
|
|
23
|
+
)
|
|
24
|
+
from fiddler_langgraph.tracing.util import _LanggraphJSONEncoder
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_agent_name(metadata: dict[str, Any]) -> str:
|
|
30
|
+
"""Get the agent name from the kwargs."""
|
|
31
|
+
agent_name = ''
|
|
32
|
+
checkpoint_ns = metadata.get('langgraph_checkpoint_ns', '')
|
|
33
|
+
if checkpoint_ns:
|
|
34
|
+
path = checkpoint_ns.split(':')
|
|
35
|
+
agent_name = path[0]
|
|
36
|
+
return agent_name
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _set_agent_name(span: trace.Span, metadata: dict[str, Any]) -> None:
|
|
40
|
+
"""Get the agent name from the kwargs."""
|
|
41
|
+
agent_name = _get_agent_name(metadata)
|
|
42
|
+
span.set_attribute(FiddlerSpanAttributes.AGENT_NAME, agent_name)
|
|
43
|
+
_set_agent_id(span, agent_name)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _set_agent_id(span: trace.Span, agent_name: str) -> None:
|
|
47
|
+
"""Set the agent ID on the span."""
|
|
48
|
+
trace_id = format(span.get_span_context().trace_id, '032x')
|
|
49
|
+
agent_id = str(trace_id) + ':' + agent_name
|
|
50
|
+
span.set_attribute(FiddlerSpanAttributes.AGENT_ID, agent_id)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _stringify_message_content(message: BaseMessage) -> str:
|
|
54
|
+
"""Stringify a message."""
|
|
55
|
+
if isinstance(message.content, str):
|
|
56
|
+
return message.content
|
|
57
|
+
return json.dumps(message.content, cls=_LanggraphJSONEncoder)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _set_model_attributes(span: trace.Span, metadata: dict[str, Any] | None = None) -> None:
|
|
61
|
+
"""Set model-related attributes on a span.
|
|
62
|
+
|
|
63
|
+
Extracts model name and provider from metadata.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
span : trace.Span
|
|
68
|
+
The OpenTelemetry span to set attributes on
|
|
69
|
+
metadata : dict[str, Any] | None
|
|
70
|
+
The metadata containing model information
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
if not metadata:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Extract model information
|
|
77
|
+
ls_model_name = metadata.get('ls_model_name')
|
|
78
|
+
ls_provider = metadata.get('ls_provider')
|
|
79
|
+
|
|
80
|
+
# Set model name attribute
|
|
81
|
+
if ls_model_name and isinstance(ls_model_name, str) and ls_model_name.strip():
|
|
82
|
+
span.set_attribute(FiddlerSpanAttributes.LLM_REQUEST_MODEL, ls_model_name.strip())
|
|
83
|
+
|
|
84
|
+
# Set provider attribute
|
|
85
|
+
if ls_provider and isinstance(ls_provider, str) and ls_provider.strip():
|
|
86
|
+
span.set_attribute(FiddlerSpanAttributes.LLM_SYSTEM, ls_provider.strip())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _set_token_usage_attributes(span: trace.Span, response: LLMResult) -> None:
|
|
90
|
+
"""Set token usage attributes on a span from LLMResult.
|
|
91
|
+
|
|
92
|
+
Extracts token usage information from the LLM response
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
span : trace.Span
|
|
97
|
+
The OpenTelemetry span to set attributes on
|
|
98
|
+
response : LLMResult
|
|
99
|
+
The LLM response containing token usage information
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
if not response.generations or not response.generations[0]:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
generation = response.generations[0][0]
|
|
107
|
+
|
|
108
|
+
if not (
|
|
109
|
+
isinstance(generation, ChatGeneration)
|
|
110
|
+
and hasattr(generation.message, 'usage_metadata')
|
|
111
|
+
and generation.message.usage_metadata
|
|
112
|
+
):
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
usage_metadata = generation.message.usage_metadata
|
|
116
|
+
|
|
117
|
+
# Set input tokens
|
|
118
|
+
input_tokens = usage_metadata.get('input_tokens')
|
|
119
|
+
if input_tokens is not None and isinstance(input_tokens, int):
|
|
120
|
+
span.set_attribute(FiddlerSpanAttributes.LLM_TOKEN_COUNT_INPUT, input_tokens)
|
|
121
|
+
|
|
122
|
+
# Set output tokens
|
|
123
|
+
output_tokens = usage_metadata.get('output_tokens')
|
|
124
|
+
if output_tokens is not None and isinstance(output_tokens, int):
|
|
125
|
+
span.set_attribute(FiddlerSpanAttributes.LLM_TOKEN_COUNT_OUTPUT, output_tokens)
|
|
126
|
+
|
|
127
|
+
# Set total tokens
|
|
128
|
+
total_tokens = usage_metadata.get('total_tokens')
|
|
129
|
+
if total_tokens is not None and isinstance(total_tokens, int):
|
|
130
|
+
span.set_attribute(FiddlerSpanAttributes.LLM_TOKEN_COUNT_TOTAL, total_tokens)
|
|
131
|
+
else:
|
|
132
|
+
# Calculate total if not provided
|
|
133
|
+
if input_tokens and output_tokens:
|
|
134
|
+
calculated_total = input_tokens + output_tokens
|
|
135
|
+
span.set_attribute(FiddlerSpanAttributes.LLM_TOKEN_COUNT_TOTAL, calculated_total)
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.warning('Failed to extract token usage: %s', e)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _CallbackHandler(BaseCallbackHandler):
|
|
142
|
+
"""A LangChain callback handler that creates OpenTelemetry spans for Fiddler.
|
|
143
|
+
|
|
144
|
+
This handler listens to events from LangGraph and creates corresponding
|
|
145
|
+
spans to trace the execution of chains, tools, and language models.
|
|
146
|
+
It is responsible for managing the lifecycle of these spans, including
|
|
147
|
+
their creation, activation, and completion.
|
|
148
|
+
|
|
149
|
+
Attributes:
|
|
150
|
+
_tracer (trace.Tracer): The OpenTelemetry tracer used to create spans.
|
|
151
|
+
_active_spans (dict[UUID, trace.Span]): A dictionary mapping run IDs
|
|
152
|
+
to active spans.
|
|
153
|
+
_root_span (trace.Span | None): The root span of the current trace.
|
|
154
|
+
session_id (str | None): The ID of the current conversation or session.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, tracer: trace.Tracer):
|
|
158
|
+
"""Initializes the callback handler.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
tracer: The OpenTelemetry tracer to use for creating and managing spans.
|
|
162
|
+
"""
|
|
163
|
+
self._active_spans: dict[UUID, trace.Span] = {}
|
|
164
|
+
self._tracer = tracer
|
|
165
|
+
self._root_span: trace.Span | None = None
|
|
166
|
+
# our callback handler needs to have its own context
|
|
167
|
+
# so that the spans created by the callback handler are not affected by the global context
|
|
168
|
+
# if the global context is set to a different trace, the spans created by the callback handler
|
|
169
|
+
# will be part of a different trace
|
|
170
|
+
self._context = Context()
|
|
171
|
+
|
|
172
|
+
def _start_new_trace(self, trace_name: str) -> trace.Span:
|
|
173
|
+
"""Start a new trace with the given name."""
|
|
174
|
+
span = self._tracer.start_span(
|
|
175
|
+
trace_name, kind=trace.SpanKind.CLIENT, context=self._context
|
|
176
|
+
)
|
|
177
|
+
span.set_attribute(FiddlerSpanAttributes.TYPE, SpanType.CHAIN)
|
|
178
|
+
# we don't know the agent name when the graph starts, so we set it to unknown
|
|
179
|
+
# we will update it when the second chain starts
|
|
180
|
+
span.set_attribute(FiddlerSpanAttributes.AGENT_NAME, 'unknown')
|
|
181
|
+
_set_agent_id(span, 'unknown')
|
|
182
|
+
self._set_session_id(span)
|
|
183
|
+
self._root_span = span
|
|
184
|
+
|
|
185
|
+
return span
|
|
186
|
+
|
|
187
|
+
def _add_span(self, span: trace.Span, run_id: UUID) -> None:
|
|
188
|
+
"""Adds a span to the active spans dictionary."""
|
|
189
|
+
self._active_spans[run_id] = span
|
|
190
|
+
|
|
191
|
+
def _get_span(self, run_id: UUID | None) -> trace.Span | None:
|
|
192
|
+
"""Retrieves a span from the active spans dictionary by its run ID."""
|
|
193
|
+
if run_id is None:
|
|
194
|
+
return None
|
|
195
|
+
return self._active_spans.get(run_id)
|
|
196
|
+
|
|
197
|
+
def _remove_span(self, run_id: UUID) -> None:
|
|
198
|
+
"""Removes a span from the active spans dictionary."""
|
|
199
|
+
del self._active_spans[run_id]
|
|
200
|
+
if len(self._active_spans) == 0:
|
|
201
|
+
# reset the root span if no active spans are left
|
|
202
|
+
self._root_span = None
|
|
203
|
+
|
|
204
|
+
def _set_fiddler_attributes_from_metadata(
|
|
205
|
+
self, span: trace.Span, metadata: dict[str, Any] | None
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Sets Fiddler-specific attributes on a span from metadata."""
|
|
208
|
+
if metadata is not None:
|
|
209
|
+
fiddler_attributes = metadata.get(FIDDLER_METADATA_KEY, {})
|
|
210
|
+
for key, value in fiddler_attributes.items():
|
|
211
|
+
fdl_key = FIDDLER_USER_SPAN_ATTRIBUTE_TEMPLATE.format(key=key)
|
|
212
|
+
span.set_attribute(fdl_key, value)
|
|
213
|
+
|
|
214
|
+
def _update_root_span_agent_name(self, agent_name: str) -> None:
|
|
215
|
+
"""Updates the agent name on the root span.
|
|
216
|
+
|
|
217
|
+
The root span is created without an agent name, so this method is
|
|
218
|
+
used to update it once the agent name becomes available.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
agent_name: The agent name to set on the root span.
|
|
222
|
+
"""
|
|
223
|
+
if self._root_span and self._root_span.is_recording():
|
|
224
|
+
self._root_span.set_attribute(FiddlerSpanAttributes.AGENT_NAME, agent_name)
|
|
225
|
+
_set_agent_id(self._root_span, agent_name)
|
|
226
|
+
|
|
227
|
+
def _start_trace(self, trace_name: str, run_id: UUID) -> None:
|
|
228
|
+
"""Starts a new trace and adds the root span to the active spans."""
|
|
229
|
+
span = self._start_new_trace(trace_name)
|
|
230
|
+
self._add_span(span, run_id)
|
|
231
|
+
|
|
232
|
+
@cached_property
|
|
233
|
+
def session_id(self) -> str:
|
|
234
|
+
"""Get the session id from the metadata."""
|
|
235
|
+
return _CONVERSATION_ID.get()
|
|
236
|
+
|
|
237
|
+
def _set_session_id(self, span: trace.Span) -> None:
|
|
238
|
+
"""Sets the session ID as an attribute on the given span."""
|
|
239
|
+
if self.session_id:
|
|
240
|
+
span.set_attribute(FiddlerSpanAttributes.CONVERSATION_ID, self.session_id)
|
|
241
|
+
|
|
242
|
+
def _create_child_span(self, parent_span: trace.Span, span_name: str) -> trace.Span:
|
|
243
|
+
"""Get a child span."""
|
|
244
|
+
parent_context = trace.set_span_in_context(parent_span, self._context)
|
|
245
|
+
return self._tracer.start_span(span_name, context=parent_context)
|
|
246
|
+
|
|
247
|
+
def on_chain_start(
|
|
248
|
+
self,
|
|
249
|
+
serialized: dict[str, Any],
|
|
250
|
+
inputs: dict[str, Any],
|
|
251
|
+
*,
|
|
252
|
+
run_id: UUID,
|
|
253
|
+
parent_run_id: UUID | None = None,
|
|
254
|
+
tags: list[str] | None = None,
|
|
255
|
+
metadata: dict[str, Any] | None = None,
|
|
256
|
+
**kwargs: Any,
|
|
257
|
+
) -> Any:
|
|
258
|
+
"""Called when a chain starts.
|
|
259
|
+
|
|
260
|
+
This method creates a new span for the chain execution. If this is the
|
|
261
|
+
first event in a trace, it creates a root span. Otherwise, it creates
|
|
262
|
+
a child span of the currently active span.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
serialized: The serialized representation of the chain.
|
|
266
|
+
inputs: The inputs to the chain.
|
|
267
|
+
run_id: The unique ID of the chain run.
|
|
268
|
+
parent_run_id: The ID of the parent run, if any.
|
|
269
|
+
tags: A list of tags for the chain.
|
|
270
|
+
metadata: A dictionary of metadata for the chain.
|
|
271
|
+
**kwargs: Additional keyword arguments.
|
|
272
|
+
"""
|
|
273
|
+
if not self._root_span:
|
|
274
|
+
trace_name = kwargs.get('name', 'unknown')
|
|
275
|
+
self._start_trace(trace_name, run_id)
|
|
276
|
+
return
|
|
277
|
+
agent_name = _get_agent_name(metadata) if metadata is not None else 'unknown'
|
|
278
|
+
parent_span = self._get_span(parent_run_id)
|
|
279
|
+
if parent_span is None:
|
|
280
|
+
# if for some reason the parent span is not found, we can just return - don't generate faulty child spans
|
|
281
|
+
logger.warning(
|
|
282
|
+
'on_chain_start no parent span for run_id %s , parent_run_id %s',
|
|
283
|
+
run_id,
|
|
284
|
+
parent_run_id,
|
|
285
|
+
)
|
|
286
|
+
return
|
|
287
|
+
child_span = self._create_child_span(parent_span, kwargs.get('name', 'unknown'))
|
|
288
|
+
|
|
289
|
+
child_span.set_attribute(FiddlerSpanAttributes.TYPE, SpanType.CHAIN)
|
|
290
|
+
child_span.set_attribute(FiddlerSpanAttributes.AGENT_NAME, agent_name)
|
|
291
|
+
_set_agent_id(child_span, agent_name)
|
|
292
|
+
self._update_root_span_agent_name(agent_name)
|
|
293
|
+
|
|
294
|
+
if metadata is not None:
|
|
295
|
+
self._set_fiddler_attributes_from_metadata(child_span, metadata)
|
|
296
|
+
|
|
297
|
+
self._set_session_id(child_span)
|
|
298
|
+
self._add_span(child_span, run_id)
|
|
299
|
+
|
|
300
|
+
def on_chain_end(
|
|
301
|
+
self,
|
|
302
|
+
outputs: dict[str, Any],
|
|
303
|
+
*,
|
|
304
|
+
run_id: UUID,
|
|
305
|
+
parent_run_id: UUID | None = None,
|
|
306
|
+
**kwargs: Any,
|
|
307
|
+
) -> Any:
|
|
308
|
+
"""Called when a chain ends.
|
|
309
|
+
|
|
310
|
+
This method finds the corresponding span for the chain run, sets its
|
|
311
|
+
status to OK, and ends the span.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
outputs: The outputs of the chain.
|
|
315
|
+
run_id: The unique ID of the chain run.
|
|
316
|
+
parent_run_id: The ID of the parent run, if any.
|
|
317
|
+
**kwargs: Additional keyword arguments.
|
|
318
|
+
"""
|
|
319
|
+
span = self._get_span(run_id)
|
|
320
|
+
if span:
|
|
321
|
+
span.set_status(trace.Status(trace.StatusCode.OK))
|
|
322
|
+
span.end()
|
|
323
|
+
self._remove_span(run_id)
|
|
324
|
+
else:
|
|
325
|
+
logger.warning('on_chain_end no active span: %s, %s', run_id, kwargs)
|
|
326
|
+
|
|
327
|
+
def on_chain_error(
|
|
328
|
+
self,
|
|
329
|
+
error: BaseException,
|
|
330
|
+
*,
|
|
331
|
+
run_id: UUID,
|
|
332
|
+
parent_run_id: UUID | None = None,
|
|
333
|
+
**kwargs: Any,
|
|
334
|
+
) -> Any:
|
|
335
|
+
"""Called when a chain encounters an error.
|
|
336
|
+
|
|
337
|
+
This method finds the corresponding span, records the exception, sets
|
|
338
|
+
the status to ERROR, and ends the span.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
error: The exception that occurred.
|
|
342
|
+
run_id: The unique ID of the chain run.
|
|
343
|
+
parent_run_id: The ID of the parent run, if any.
|
|
344
|
+
**kwargs: Additional keyword arguments.
|
|
345
|
+
"""
|
|
346
|
+
span = self._get_span(run_id)
|
|
347
|
+
if span:
|
|
348
|
+
span.record_exception(error)
|
|
349
|
+
# Use repr() for more complete error information, fallback to str() if repr() is empty
|
|
350
|
+
error_message = repr(error) if repr(error) else str(error)
|
|
351
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR, error_message))
|
|
352
|
+
span.end()
|
|
353
|
+
self._remove_span(run_id)
|
|
354
|
+
else:
|
|
355
|
+
logger.warning('on_chain_error no active span: %s, %s', run_id, kwargs)
|
|
356
|
+
|
|
357
|
+
def on_tool_start(
|
|
358
|
+
self,
|
|
359
|
+
serialized: dict[str, Any],
|
|
360
|
+
input_str: str,
|
|
361
|
+
*,
|
|
362
|
+
run_id: UUID,
|
|
363
|
+
parent_run_id: UUID | None = None,
|
|
364
|
+
tags: list[str] | None = None,
|
|
365
|
+
metadata: dict[str, Any] | None = None,
|
|
366
|
+
inputs: dict[str, Any] | None = None,
|
|
367
|
+
**kwargs: Any,
|
|
368
|
+
) -> Any:
|
|
369
|
+
"""Called when a tool starts.
|
|
370
|
+
|
|
371
|
+
This method creates a new span for the tool execution as a child of
|
|
372
|
+
the currently active span.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
serialized: The serialized representation of the tool.
|
|
376
|
+
input_str: The input to the tool.
|
|
377
|
+
run_id: The unique ID of the tool run.
|
|
378
|
+
parent_run_id: The ID of the parent run, if any.
|
|
379
|
+
tags: A list of tags for the tool.
|
|
380
|
+
metadata: A dictionary of metadata for the tool.
|
|
381
|
+
inputs: The inputs to the tool.
|
|
382
|
+
**kwargs: Additional keyword arguments.
|
|
383
|
+
"""
|
|
384
|
+
parent_span = self._get_span(parent_run_id)
|
|
385
|
+
if parent_span is None:
|
|
386
|
+
# if for some reason the parent span is not found, we can just return - don't generate faulty child spans
|
|
387
|
+
logger.warning(
|
|
388
|
+
'on_tool_start no parent span for run_id %s , parent_run_id %s',
|
|
389
|
+
run_id,
|
|
390
|
+
parent_run_id,
|
|
391
|
+
)
|
|
392
|
+
return
|
|
393
|
+
child_span = self._create_child_span(parent_span, serialized.get('name', 'unknown'))
|
|
394
|
+
span_input = json.dumps(inputs, cls=_LanggraphJSONEncoder) if inputs else input_str
|
|
395
|
+
child_span.set_attribute(FiddlerSpanAttributes.TYPE, SpanType.TOOL)
|
|
396
|
+
child_span.set_attribute(FiddlerSpanAttributes.TOOL_NAME, serialized.get('name', 'unknown'))
|
|
397
|
+
child_span.set_attribute(FiddlerSpanAttributes.TOOL_INPUT, span_input)
|
|
398
|
+
if metadata is not None:
|
|
399
|
+
_set_agent_name(child_span, metadata)
|
|
400
|
+
self._set_fiddler_attributes_from_metadata(child_span, metadata)
|
|
401
|
+
|
|
402
|
+
self._set_session_id(child_span)
|
|
403
|
+
self._add_span(child_span, run_id)
|
|
404
|
+
|
|
405
|
+
def on_tool_end(
|
|
406
|
+
self,
|
|
407
|
+
output: Any,
|
|
408
|
+
*,
|
|
409
|
+
run_id: UUID,
|
|
410
|
+
parent_run_id: UUID | None = None,
|
|
411
|
+
**kwargs: Any,
|
|
412
|
+
) -> Any:
|
|
413
|
+
"""Called when a tool ends.
|
|
414
|
+
|
|
415
|
+
This method finds the corresponding span, sets its status to OK, and
|
|
416
|
+
ends the span.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
output: The output of the tool.
|
|
420
|
+
run_id: The unique ID of the tool run.
|
|
421
|
+
parent_run_id: The ID of the parent run, if any.
|
|
422
|
+
**kwargs: Additional keyword arguments.
|
|
423
|
+
"""
|
|
424
|
+
span = self._get_span(run_id)
|
|
425
|
+
if span:
|
|
426
|
+
# limit the output to 100 characters for now - add formal limits later
|
|
427
|
+
span.set_attribute(
|
|
428
|
+
FiddlerSpanAttributes.TOOL_OUTPUT,
|
|
429
|
+
json.dumps(output, cls=_LanggraphJSONEncoder),
|
|
430
|
+
)
|
|
431
|
+
span.set_status(trace.Status(trace.StatusCode.OK))
|
|
432
|
+
span.end()
|
|
433
|
+
self._remove_span(run_id)
|
|
434
|
+
else:
|
|
435
|
+
logger.warning('on_tool_end no active span: %s, %s', run_id, kwargs)
|
|
436
|
+
|
|
437
|
+
def on_tool_error(
|
|
438
|
+
self,
|
|
439
|
+
error: BaseException,
|
|
440
|
+
*,
|
|
441
|
+
run_id: UUID,
|
|
442
|
+
parent_run_id: UUID | None = None,
|
|
443
|
+
**kwargs: Any,
|
|
444
|
+
) -> Any:
|
|
445
|
+
"""Called when a tool encounters an error.
|
|
446
|
+
|
|
447
|
+
This method finds the corresponding span, records the exception, sets
|
|
448
|
+
the status to ERROR, and ends the span.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
error: The exception that occurred.
|
|
452
|
+
run_id: The unique ID of the tool run.
|
|
453
|
+
parent_run_id: The ID of the parent run, if any.
|
|
454
|
+
**kwargs: Additional keyword arguments.
|
|
455
|
+
"""
|
|
456
|
+
span = self._get_span(run_id)
|
|
457
|
+
if span:
|
|
458
|
+
span.record_exception(error)
|
|
459
|
+
# Use repr() for more complete error information, fallback to str() if repr() is empty
|
|
460
|
+
error_message = repr(error) if repr(error) else str(error)
|
|
461
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR, error_message))
|
|
462
|
+
span.end()
|
|
463
|
+
self._remove_span(run_id)
|
|
464
|
+
else:
|
|
465
|
+
logger.warning('on_tool_error no active span: %s, %s', run_id, kwargs)
|
|
466
|
+
|
|
467
|
+
def on_retriever_start(
|
|
468
|
+
self,
|
|
469
|
+
serialized: dict[str, Any],
|
|
470
|
+
query: str,
|
|
471
|
+
*,
|
|
472
|
+
run_id: UUID,
|
|
473
|
+
parent_run_id: UUID | None = None,
|
|
474
|
+
tags: list[str] | None = None,
|
|
475
|
+
metadata: dict[str, Any] | None = None,
|
|
476
|
+
**kwargs: Any,
|
|
477
|
+
) -> Any:
|
|
478
|
+
"""Called when a retriever starts.
|
|
479
|
+
|
|
480
|
+
This method creates a new span for the retriever execution.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
serialized: The serialized representation of the retriever.
|
|
484
|
+
query: The query sent to the retriever.
|
|
485
|
+
run_id: The unique ID of the retriever run.
|
|
486
|
+
parent_run_id: The ID of the parent run, if any.
|
|
487
|
+
tags: A list of tags for the retriever.
|
|
488
|
+
metadata: A dictionary of metadata for the retriever.
|
|
489
|
+
**kwargs: Additional keyword arguments.
|
|
490
|
+
"""
|
|
491
|
+
parent_span = self._get_span(parent_run_id)
|
|
492
|
+
if parent_span is None:
|
|
493
|
+
# if for some reason the parent span is not found, we can just return - don't generate faulty child spans
|
|
494
|
+
logger.warning(
|
|
495
|
+
'on_retriever_start no parent span for run_id %s , parent_run_id %s',
|
|
496
|
+
run_id,
|
|
497
|
+
parent_run_id,
|
|
498
|
+
)
|
|
499
|
+
return
|
|
500
|
+
child_span = self._create_child_span(parent_span, kwargs.get('name', 'unknown'))
|
|
501
|
+
|
|
502
|
+
child_span.set_attribute(FiddlerSpanAttributes.TYPE, SpanType.TOOL)
|
|
503
|
+
child_span.set_attribute(FiddlerSpanAttributes.TOOL_INPUT, query)
|
|
504
|
+
|
|
505
|
+
if metadata is not None:
|
|
506
|
+
_set_agent_name(child_span, metadata)
|
|
507
|
+
self._set_fiddler_attributes_from_metadata(child_span, metadata)
|
|
508
|
+
|
|
509
|
+
# semantic convention attributes
|
|
510
|
+
child_span.set_attribute(
|
|
511
|
+
FiddlerSpanAttributes.TYPE, SpanType.TOOL
|
|
512
|
+
) # document retrieval is a tool
|
|
513
|
+
child_span.set_attribute(FiddlerSpanAttributes.TOOL_NAME, kwargs.get('name', 'unknown'))
|
|
514
|
+
child_span.set_attribute(FiddlerSpanAttributes.TOOL_INPUT, str(query))
|
|
515
|
+
self._set_session_id(child_span)
|
|
516
|
+
self._add_span(child_span, run_id)
|
|
517
|
+
|
|
518
|
+
def on_retriever_error(
|
|
519
|
+
self,
|
|
520
|
+
error: BaseException,
|
|
521
|
+
*,
|
|
522
|
+
run_id: UUID,
|
|
523
|
+
parent_run_id: UUID | None = None,
|
|
524
|
+
**kwargs: Any,
|
|
525
|
+
) -> Any:
|
|
526
|
+
"""Called when a retriever encounters an error.
|
|
527
|
+
|
|
528
|
+
This method finds the corresponding span, records the exception, sets
|
|
529
|
+
the status to ERROR, and ends the span.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
error: The exception that occurred.
|
|
533
|
+
run_id: The unique ID of the retriever run.
|
|
534
|
+
parent_run_id: The ID of the parent run, if any.
|
|
535
|
+
**kwargs: Additional keyword arguments.
|
|
536
|
+
"""
|
|
537
|
+
span = self._get_span(run_id)
|
|
538
|
+
if span:
|
|
539
|
+
span.record_exception(error)
|
|
540
|
+
# Use repr() for more complete error information, fallback to str() if repr() is empty
|
|
541
|
+
error_message = repr(error) if repr(error) else str(error)
|
|
542
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR, error_message))
|
|
543
|
+
span.end()
|
|
544
|
+
self._remove_span(run_id)
|
|
545
|
+
else:
|
|
546
|
+
logger.warning('on_retriever_error no active span: %s, %s', run_id, kwargs)
|
|
547
|
+
|
|
548
|
+
def on_retriever_end(
|
|
549
|
+
self,
|
|
550
|
+
documents: Sequence[Document],
|
|
551
|
+
*,
|
|
552
|
+
run_id: UUID,
|
|
553
|
+
parent_run_id: UUID | None = None,
|
|
554
|
+
**kwargs: Any,
|
|
555
|
+
) -> Any:
|
|
556
|
+
"""Called when a retriever ends.
|
|
557
|
+
|
|
558
|
+
This method finds the corresponding span, records the retrieved
|
|
559
|
+
documents as an event, sets the status to OK, and ends the span.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
documents: The documents retrieved by the retriever.
|
|
563
|
+
run_id: The unique ID of the retriever run.
|
|
564
|
+
parent_run_id: The ID of the parent run, if any.
|
|
565
|
+
**kwargs: Additional keyword arguments.
|
|
566
|
+
"""
|
|
567
|
+
span = self._get_span(run_id)
|
|
568
|
+
if span:
|
|
569
|
+
span.set_status(trace.Status(trace.StatusCode.OK))
|
|
570
|
+
span.set_attribute(
|
|
571
|
+
FiddlerSpanAttributes.TOOL_OUTPUT,
|
|
572
|
+
json.dumps(documents, cls=_LanggraphJSONEncoder),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
span.end()
|
|
576
|
+
self._remove_span(run_id)
|
|
577
|
+
else:
|
|
578
|
+
logger.warning('on_retriever_end no active span: %s, %s', run_id, kwargs)
|
|
579
|
+
|
|
580
|
+
def on_chat_model_start(
|
|
581
|
+
self,
|
|
582
|
+
serialized: dict[str, Any],
|
|
583
|
+
messages: list[list[BaseMessage]],
|
|
584
|
+
*,
|
|
585
|
+
run_id: UUID,
|
|
586
|
+
parent_run_id: UUID | None = None,
|
|
587
|
+
tags: list[str] | None = None,
|
|
588
|
+
metadata: dict[str, Any] | None = None,
|
|
589
|
+
**kwargs: Any,
|
|
590
|
+
) -> Any:
|
|
591
|
+
"""Called when a chat model starts.
|
|
592
|
+
|
|
593
|
+
This method creates a new span for the chat model execution and records
|
|
594
|
+
the input messages as events.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
serialized: The serialized representation of the chat model.
|
|
598
|
+
messages: The messages sent to the chat model.
|
|
599
|
+
run_id: The unique ID of the chat model run.
|
|
600
|
+
parent_run_id: The ID of the parent run, if any.
|
|
601
|
+
tags: A list of tags for the chat model.
|
|
602
|
+
metadata: A dictionary of metadata for the chat model.
|
|
603
|
+
**kwargs: Additional keyword arguments.
|
|
604
|
+
"""
|
|
605
|
+
parent_span = self._get_span(parent_run_id)
|
|
606
|
+
if parent_span is None:
|
|
607
|
+
# if for some reason the parent span is not found, we can just return - don't generate faulty child spans
|
|
608
|
+
logger.warning(
|
|
609
|
+
'on_llm_start no parent span for run_id %s , parent_run_id %s',
|
|
610
|
+
run_id,
|
|
611
|
+
parent_run_id,
|
|
612
|
+
)
|
|
613
|
+
return
|
|
614
|
+
parent_context = trace.set_span_in_context(parent_span, self._context)
|
|
615
|
+
child_span = self._tracer.start_span(
|
|
616
|
+
serialized.get('name', 'unknown'), context=parent_context
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# chat models are a special case of LLMs with Structure Inputs (messages)
|
|
620
|
+
# the ordering of messages is preserved over the lifecycle of an agent's invocation
|
|
621
|
+
# we are ignoring AIMessage, ToolMessage, FunctionMessage & ChatMessage
|
|
622
|
+
# see https://python.langchain.com/api_reference/core/messages.html#module-langchain_core.messages
|
|
623
|
+
system_message = []
|
|
624
|
+
user_message = []
|
|
625
|
+
if messages and messages[0]:
|
|
626
|
+
system_message = [m for m in messages[0] if isinstance(m, SystemMessage)]
|
|
627
|
+
user_message = [m for m in messages[0] if isinstance(m, HumanMessage)]
|
|
628
|
+
|
|
629
|
+
# breakpoint()
|
|
630
|
+
if metadata is not None:
|
|
631
|
+
_set_agent_name(child_span, metadata)
|
|
632
|
+
|
|
633
|
+
self._set_fiddler_attributes_from_metadata(child_span, metadata)
|
|
634
|
+
|
|
635
|
+
child_span.set_attribute(FiddlerSpanAttributes.TYPE, SpanType.LLM)
|
|
636
|
+
|
|
637
|
+
# Set model attributes
|
|
638
|
+
_set_model_attributes(child_span, metadata)
|
|
639
|
+
|
|
640
|
+
# We are only taking the 1st system message and 1st user message
|
|
641
|
+
# as we are not supporting multiple system messages or multiple user messages
|
|
642
|
+
# To support multiple system messages, we would need to add a new attribute with indexing
|
|
643
|
+
# or use event attributes
|
|
644
|
+
system_content = _stringify_message_content(system_message[-1]) if system_message else ''
|
|
645
|
+
user_content = _stringify_message_content(user_message[-1]) if user_message else ''
|
|
646
|
+
child_span.set_attribute(
|
|
647
|
+
FiddlerSpanAttributes.LLM_INPUT_SYSTEM,
|
|
648
|
+
system_content,
|
|
649
|
+
)
|
|
650
|
+
child_span.set_attribute(
|
|
651
|
+
FiddlerSpanAttributes.LLM_INPUT_USER,
|
|
652
|
+
user_content,
|
|
653
|
+
)
|
|
654
|
+
self._set_session_id(child_span)
|
|
655
|
+
self._add_span(child_span, run_id)
|
|
656
|
+
|
|
657
|
+
def on_llm_start(
|
|
658
|
+
self,
|
|
659
|
+
serialized: dict[str, Any],
|
|
660
|
+
prompts: list[str],
|
|
661
|
+
*,
|
|
662
|
+
run_id: UUID,
|
|
663
|
+
parent_run_id: UUID | None = None,
|
|
664
|
+
tags: list[str] | None = None,
|
|
665
|
+
metadata: dict[str, Any] | None = None,
|
|
666
|
+
**kwargs: Any,
|
|
667
|
+
) -> Any:
|
|
668
|
+
"""Called when a language model starts.
|
|
669
|
+
|
|
670
|
+
This method creates a new span for the language model execution and
|
|
671
|
+
records the prompts as attributes.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
serialized: The serialized representation of the language model.
|
|
675
|
+
prompts: The prompts sent to the language model.
|
|
676
|
+
run_id: The unique ID of the language model run.
|
|
677
|
+
parent_run_id: The ID of the parent run, if any.
|
|
678
|
+
tags: A list of tags for the language model.
|
|
679
|
+
metadata: A dictionary of metadata for the language model.
|
|
680
|
+
**kwargs: Additional keyword arguments.
|
|
681
|
+
"""
|
|
682
|
+
parent_span = self._get_span(parent_run_id)
|
|
683
|
+
if parent_span is None:
|
|
684
|
+
# if for some reason the parent span is not found, we can just return - don't generate faulty child spans
|
|
685
|
+
logger.warning(
|
|
686
|
+
'on_llm_start no parent span for run_id %s , parent_run_id %s',
|
|
687
|
+
run_id,
|
|
688
|
+
parent_run_id,
|
|
689
|
+
)
|
|
690
|
+
return
|
|
691
|
+
child_span = self._create_child_span(parent_span, serialized.get('name', 'unknown'))
|
|
692
|
+
|
|
693
|
+
child_span.set_attribute(FiddlerSpanAttributes.TYPE, SpanType.LLM)
|
|
694
|
+
|
|
695
|
+
if metadata is not None:
|
|
696
|
+
_set_agent_name(child_span, metadata)
|
|
697
|
+
self._set_fiddler_attributes_from_metadata(child_span, metadata)
|
|
698
|
+
|
|
699
|
+
# Set model attributes
|
|
700
|
+
_set_model_attributes(child_span, metadata)
|
|
701
|
+
|
|
702
|
+
# LLM model is more generic than a chat model, it only has a list on prompts
|
|
703
|
+
# we are using the first prompt as both the system message and the user message
|
|
704
|
+
# to capture all the prompts, we would need to add a new attribute with indexing
|
|
705
|
+
# or use event attributes
|
|
706
|
+
child_span.set_attribute(FiddlerSpanAttributes.LLM_INPUT_SYSTEM, prompts[0])
|
|
707
|
+
child_span.set_attribute(FiddlerSpanAttributes.LLM_INPUT_USER, prompts[0])
|
|
708
|
+
self._set_session_id(child_span)
|
|
709
|
+
self._add_span(child_span, run_id)
|
|
710
|
+
|
|
711
|
+
def on_llm_end(
|
|
712
|
+
self,
|
|
713
|
+
response: LLMResult,
|
|
714
|
+
*,
|
|
715
|
+
run_id: UUID,
|
|
716
|
+
parent_run_id: UUID | None = None,
|
|
717
|
+
**kwargs: Any,
|
|
718
|
+
) -> Any:
|
|
719
|
+
"""Called when a language model ends.
|
|
720
|
+
|
|
721
|
+
This method finds the corresponding span, records the model's
|
|
722
|
+
response, sets the status to OK, and ends the span.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
response: The response from the language model.
|
|
726
|
+
run_id: The unique ID of the language model run.
|
|
727
|
+
parent_run_id: The ID of the parent run, if any.
|
|
728
|
+
**kwargs: Additional keyword arguments.
|
|
729
|
+
"""
|
|
730
|
+
span = self._get_span(run_id)
|
|
731
|
+
if span:
|
|
732
|
+
span.set_status(trace.Status(trace.StatusCode.OK))
|
|
733
|
+
|
|
734
|
+
# assuming we are going to use the first generation for now
|
|
735
|
+
# we always get only one element in the list - even with batch mode
|
|
736
|
+
# Add safety checks to prevent index errors
|
|
737
|
+
output = ''
|
|
738
|
+
if (
|
|
739
|
+
response.generations
|
|
740
|
+
and len(response.generations) > 0
|
|
741
|
+
and response.generations[0]
|
|
742
|
+
and len(response.generations[0]) > 0
|
|
743
|
+
):
|
|
744
|
+
generation = response.generations[0][0]
|
|
745
|
+
output = generation.text
|
|
746
|
+
if (
|
|
747
|
+
output == ''
|
|
748
|
+
and isinstance(generation, ChatGeneration)
|
|
749
|
+
and isinstance(generation.message, AIMessage)
|
|
750
|
+
and hasattr(generation.message, 'tool_calls')
|
|
751
|
+
):
|
|
752
|
+
# if llm returns an empty string, it means it used a tool
|
|
753
|
+
# we are using the tool calls to get the output
|
|
754
|
+
output = json.dumps(generation.message.tool_calls, cls=_LanggraphJSONEncoder)
|
|
755
|
+
|
|
756
|
+
span.set_attribute(FiddlerSpanAttributes.LLM_OUTPUT, output)
|
|
757
|
+
|
|
758
|
+
# Extract and set token usage information
|
|
759
|
+
_set_token_usage_attributes(span, response)
|
|
760
|
+
|
|
761
|
+
span.end()
|
|
762
|
+
self._remove_span(run_id)
|
|
763
|
+
else:
|
|
764
|
+
logger.warning('on_llm_end no active span: %s, %s', run_id, kwargs)
|
|
765
|
+
|
|
766
|
+
def on_llm_error(
|
|
767
|
+
self,
|
|
768
|
+
error: BaseException,
|
|
769
|
+
*,
|
|
770
|
+
run_id: UUID,
|
|
771
|
+
parent_run_id: UUID | None = None,
|
|
772
|
+
**kwargs: Any,
|
|
773
|
+
) -> Any:
|
|
774
|
+
"""Called when a language model encounters an error.
|
|
775
|
+
|
|
776
|
+
This method finds the corresponding span, records the exception, sets
|
|
777
|
+
the status to ERROR, and ends the span.
|
|
778
|
+
|
|
779
|
+
Args:
|
|
780
|
+
error: The exception that occurred.
|
|
781
|
+
run_id: The unique ID of the language model run.
|
|
782
|
+
parent_run_id: The ID of the parent run, if any.
|
|
783
|
+
**kwargs: Additional keyword arguments.
|
|
784
|
+
"""
|
|
785
|
+
span = self._get_span(run_id)
|
|
786
|
+
if span:
|
|
787
|
+
span.record_exception(error)
|
|
788
|
+
# Use repr() for more complete error information, fallback to str() if repr() is empty
|
|
789
|
+
error_message = repr(error) if repr(error) else str(error)
|
|
790
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR, error_message))
|
|
791
|
+
span.set_attribute('error_kwargs', str(kwargs))
|
|
792
|
+
span.end()
|
|
793
|
+
self._remove_span(run_id)
|
|
794
|
+
else:
|
|
795
|
+
logger.warning('on_llm_error no active span: %s, %s', run_id, kwargs)
|