traceAI-langchain 0.1.6__1-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.
@@ -0,0 +1,156 @@
1
+ import logging
2
+ from typing import Any, Callable, Collection, List, Optional
3
+ from uuid import UUID
4
+
5
+ import langchain_core
6
+ from fi_instrumentation import FITracer, TraceConfig
7
+ from langchain_core.callbacks import BaseCallbackManager
8
+ from langchain_core.runnables.config import var_child_runnable_config # noqa F401
9
+ from opentelemetry import trace as trace_api
10
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore
11
+ from opentelemetry.trace import Span
12
+ from traceai_langchain._tracer import FiTracer
13
+ from traceai_langchain.package import _instruments
14
+ from traceai_langchain.version import __version__
15
+ from wrapt import wrap_function_wrapper # type: ignore
16
+ from fi_instrumentation.instrumentation._protect_wrapper import GuardrailProtectWrapper
17
+ logger = logging.getLogger(__name__)
18
+ logger.addHandler(logging.NullHandler())
19
+
20
+ _MODULE = "langchain_core"
21
+
22
+
23
+ class LangChainInstrumentor(BaseInstrumentor): # type: ignore
24
+ """
25
+ An instrumentor for LangChain
26
+ """
27
+
28
+ def instrumentation_dependencies(self) -> Collection[str]:
29
+ return _instruments
30
+
31
+ def _instrument(self, **kwargs: Any) -> None:
32
+ if not (tracer_provider := kwargs.get("tracer_provider")):
33
+ tracer_provider = trace_api.get_tracer_provider()
34
+ if not (config := kwargs.get("config")):
35
+ config = TraceConfig()
36
+ else:
37
+ assert isinstance(config, TraceConfig)
38
+
39
+ from traceai_langchain._tracer import FiTracer as LangChainFiTracer
40
+ from fi.evals import ProtectClient
41
+ from fi_instrumentation.instrumentation._tracers import FITracer
42
+
43
+ self.fi_tracer = FITracer(
44
+ trace_api.get_tracer(__name__, __version__, tracer_provider),
45
+ config=config,
46
+ )
47
+ self._tracer: Optional[LangChainFiTracer] = LangChainFiTracer(self.fi_tracer)
48
+ self._original_callback_manager_init = BaseCallbackManager.__init__
49
+ wrap_function_wrapper(
50
+ module="langchain_core.callbacks",
51
+ name="BaseCallbackManager.__init__",
52
+ wrapper=_BaseCallbackManagerInit(self._tracer),
53
+ )
54
+ self._original_protect = ProtectClient.protect
55
+ wrap_function_wrapper(
56
+ module="fi.evals",
57
+ name="ProtectClient.protect",
58
+ wrapper=GuardrailProtectWrapper(self._tracer),
59
+ )
60
+
61
+
62
+ def _uninstrument(self, **kwargs: Any) -> None:
63
+ langchain_core.callbacks.BaseCallbackManager.__init__ = self._original_callback_manager_init # type: ignore
64
+ self._original_callback_manager_init = None # type: ignore
65
+ self._tracer = None
66
+ self.fi_tracer = None
67
+
68
+ def get_span(self, run_id: UUID) -> Optional[Span]:
69
+ return self._tracer.get_span(run_id) if self._tracer else None
70
+
71
+ def get_ancestors(self, run_id: UUID) -> List[Span]:
72
+ ancestors: List[Span] = []
73
+ tracer = self._tracer
74
+ assert tracer
75
+
76
+ run = tracer.run_map.get(str(run_id))
77
+ if not run:
78
+ return ancestors
79
+
80
+ ancestor_run_id = run.parent_run_id # start with the first ancestor
81
+
82
+ while ancestor_run_id:
83
+ span = self.get_span(ancestor_run_id)
84
+ if span:
85
+ ancestors.append(span)
86
+
87
+ run = tracer.run_map.get(str(ancestor_run_id))
88
+ if not run:
89
+ break
90
+ ancestor_run_id = run.parent_run_id
91
+ return ancestors
92
+
93
+
94
+ class _BaseCallbackManagerInit:
95
+ __slots__ = ("_tracer",)
96
+
97
+ def __init__(self, tracer: "FiTracer"):
98
+ self._tracer = tracer
99
+
100
+ def __call__(
101
+ self,
102
+ wrapped: Callable[..., None],
103
+ instance: "BaseCallbackManager",
104
+ args: Any,
105
+ kwargs: Any,
106
+ ) -> None:
107
+ wrapped(*args, **kwargs)
108
+ for handler in instance.inheritable_handlers:
109
+ # Handlers may be copied when new managers are created, so we
110
+ # don't want to keep adding. E.g. see the following location.
111
+ # https://github.com/langchain-ai/langchain/blob/5c2538b9f7fb64afed2a918b621d9d8681c7ae32/libs/core/langchain_core/callbacks/manager.py#L1876 # noqa: E501
112
+ if isinstance(handler, type(self._tracer)):
113
+ break
114
+ else:
115
+ instance.add_handler(self._tracer, True)
116
+
117
+
118
+ def get_current_span() -> Optional[Span]:
119
+ import langchain_core
120
+
121
+ run_id: Optional[UUID] = None
122
+ config = langchain_core.runnables.config.var_child_runnable_config.get()
123
+ if not isinstance(config, dict):
124
+ return None
125
+ for v in config.values():
126
+ if not isinstance(v, langchain_core.callbacks.BaseCallbackManager):
127
+ continue
128
+ if run_id := v.parent_run_id:
129
+ break
130
+ if not run_id:
131
+ return None
132
+ return LangChainInstrumentor().get_span(run_id)
133
+
134
+
135
+ def get_ancestor_spans() -> List[Span]:
136
+ """
137
+ Retrieve the ancestor spans for the current LangChain run.
138
+
139
+ This function traverses the LangChain run tree from the current run's parent up to the root,
140
+ collecting the spans associated with each ancestor run. The list is ordered from the immediate
141
+ parent of the current run to the root of the run tree.
142
+ """
143
+ import langchain_core
144
+
145
+ run_id: Optional[UUID] = None
146
+ config = langchain_core.runnables.config.var_child_runnable_config.get()
147
+ if not isinstance(config, dict):
148
+ return None
149
+ for v in config.values():
150
+ if not isinstance(v, langchain_core.callbacks.BaseCallbackManager):
151
+ continue
152
+ if run_id := v.parent_run_id:
153
+ break
154
+ if not run_id:
155
+ return []
156
+ return LangChainInstrumentor().get_ancestors(run_id)
@@ -0,0 +1,1118 @@
1
+ import json
2
+ import logging
3
+ import math
4
+ import time
5
+ import traceback
6
+ from copy import deepcopy
7
+ from datetime import datetime, timezone
8
+ from enum import Enum
9
+ from itertools import chain
10
+ from threading import RLock
11
+ from typing import (
12
+ TYPE_CHECKING,
13
+ Any,
14
+ Callable,
15
+ Dict,
16
+ Generic,
17
+ Hashable,
18
+ Iterable,
19
+ Iterator,
20
+ List,
21
+ Mapping,
22
+ Optional,
23
+ Sequence,
24
+ Tuple,
25
+ TypeVar,
26
+ cast,
27
+ )
28
+ from uuid import UUID
29
+
30
+ import wrapt # type: ignore
31
+ from fi_instrumentation import get_attributes_from_context, safe_json_dumps
32
+ from fi_instrumentation.fi_types import (
33
+ DocumentAttributes,
34
+ EmbeddingAttributes,
35
+ FiMimeTypeValues,
36
+ FiSpanKindValues,
37
+ ImageAttributes,
38
+ MessageAttributes,
39
+ MessageContentAttributes,
40
+ RerankerAttributes,
41
+ SpanAttributes,
42
+ ToolCallAttributes,
43
+ )
44
+ from langchain_core.messages import BaseMessage
45
+ from langchain_core.messages.human import HumanMessage
46
+ from langchain_core.tracers import BaseTracer, LangChainTracer
47
+ from langchain_core.tracers.schemas import Run
48
+ from opentelemetry import context as context_api
49
+ from opentelemetry import trace as trace_api
50
+ from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY
51
+ from opentelemetry.semconv.trace import SpanAttributes as OTELSpanAttributes
52
+ from opentelemetry.trace import Span
53
+ from opentelemetry.util.types import AttributeValue
54
+ from wrapt import ObjectProxy
55
+
56
+ logger = logging.getLogger(__name__)
57
+ logger.addHandler(logging.NullHandler())
58
+
59
+ _AUDIT_TIMING = False
60
+
61
+
62
+ @wrapt.decorator # type: ignore
63
+ def audit_timing(wrapped: Any, _: Any, args: Any, kwargs: Any) -> Any:
64
+ if not _AUDIT_TIMING:
65
+ return wrapped(*args, **kwargs)
66
+ start_time = time.perf_counter()
67
+ try:
68
+ return wrapped(*args, **kwargs)
69
+ finally:
70
+ latency_ms = (time.perf_counter() - start_time) * 1000
71
+
72
+
73
+ K = TypeVar("K", bound=Hashable)
74
+ V = TypeVar("V")
75
+
76
+
77
+ class _DictWithLock(ObjectProxy, Generic[K, V]): # type: ignore
78
+ """
79
+ A wrapped dictionary with lock
80
+ """
81
+
82
+ def __init__(self, wrapped: Optional[Dict[str, V]] = None) -> None:
83
+ super().__init__(wrapped or {})
84
+ self._self_lock = RLock()
85
+
86
+ def get(self, key: K) -> Optional[V]:
87
+ with self._self_lock:
88
+ return cast(Optional[V], self.__wrapped__.get(key))
89
+
90
+ def pop(self, key: K, *args: Any) -> Optional[V]:
91
+ with self._self_lock:
92
+ return cast(Optional[V], self.__wrapped__.pop(key, *args))
93
+
94
+ def __getitem__(self, key: K) -> V:
95
+ with self._self_lock:
96
+ return cast(V, super().__getitem__(key))
97
+
98
+ def __setitem__(self, key: K, value: V) -> None:
99
+ with self._self_lock:
100
+ super().__setitem__(key, value)
101
+
102
+ def __delitem__(self, key: K) -> None:
103
+ with self._self_lock:
104
+ super().__delitem__(key)
105
+
106
+
107
+ class FiTracer(BaseTracer):
108
+ __slots__ = ("_tracer", "_spans_by_run")
109
+
110
+ def _extract_content(self, data: Any) -> Any:
111
+ """Extract clean content from various message formats"""
112
+ if isinstance(data, dict):
113
+ # Handle dictionary format
114
+ if "content" in data:
115
+ return data["content"]
116
+ if "generations" in data and isinstance(data["generations"], list):
117
+ for gen in data["generations"][0]: # Take first generation
118
+ if "text" in gen:
119
+ return gen["text"]
120
+ if "message" in gen and isinstance(gen["message"], dict):
121
+ return gen["message"].get("content")
122
+ if "messages" in data and isinstance(data["messages"], list):
123
+ messages_content = []
124
+ for msg_list in data["messages"]:
125
+ if isinstance(msg_list, list):
126
+ for msg in msg_list:
127
+ if isinstance(msg, dict) and "kwargs" in msg:
128
+ content = msg["kwargs"].get("content")
129
+ if content:
130
+ messages_content.append(content)
131
+ elif isinstance(msg_list, HumanMessage):
132
+ messages_content.append(msg_list.content)
133
+ return (
134
+ messages_content[0]
135
+ if len(messages_content) == 1
136
+ else messages_content
137
+ )
138
+ if "input" in data:
139
+ return data["input"]
140
+ if "query" in data:
141
+ return data["query"]
142
+ return data
143
+ elif hasattr(data, "content"):
144
+ # Handle message objects
145
+ return data.content
146
+ elif isinstance(data, (list, tuple)):
147
+ # Handle list format
148
+ if len(data) == 1:
149
+ return self._extract_content(data[0])
150
+ return [self._extract_content(item) for item in data]
151
+ return data
152
+
153
+ def _send_span_data_to_api(
154
+ self, span: Span, input_data: Any, output_data: Any
155
+ ) -> None:
156
+ """Send span data to API and add to span attributes"""
157
+ try:
158
+ # cleaned_input = self._extract_content(input_data)
159
+ cleaned_output = self._extract_content(output_data)
160
+
161
+ span_data = {
162
+ "span_id": span.context.span_id,
163
+ # "input": cleaned_input,
164
+ "output": cleaned_output,
165
+ }
166
+
167
+ # if cleaned_input is not None:
168
+ # span.set_attributes({"fi.llm.input": str(cleaned_input)})
169
+ if cleaned_output is not None:
170
+ span.set_attributes({"fi.llm.output": str(cleaned_output)})
171
+
172
+ except Exception as e:
173
+ logger.exception(f"Error sending span data to API: {e}")
174
+
175
+ def __init__(self, tracer: trace_api.Tracer, *args: Any, **kwargs: Any) -> None:
176
+ super().__init__(*args, **kwargs)
177
+ if TYPE_CHECKING:
178
+ # check that `run_map` still exists in parent class
179
+ assert self.run_map
180
+ self.run_map = _DictWithLock[str, Run](self.run_map)
181
+ self._tracer = tracer
182
+ self._spans_by_run: Dict[UUID, Span] = _DictWithLock[UUID, Span]()
183
+ self._lock = RLock() # handlers may be run in a thread by langchain
184
+
185
+ def get_span(self, run_id: UUID) -> Optional[Span]:
186
+ return self._spans_by_run.get(run_id)
187
+
188
+ @audit_timing # type: ignore
189
+ def _start_trace(self, run: Run) -> None:
190
+ self.run_map[str(run.id)] = run
191
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
192
+ return
193
+ with self._lock:
194
+ parent_context = (
195
+ trace_api.set_span_in_context(parent)
196
+ if (parent_run_id := run.parent_run_id)
197
+ and (parent := self._spans_by_run.get(parent_run_id))
198
+ else None
199
+ )
200
+ # We can't use real time because the handler may be
201
+ # called in a background thread.
202
+ start_time_utc_nano = _as_utc_nano(run.start_time)
203
+ span = self._tracer.start_span(
204
+ name=run.name,
205
+ context=parent_context,
206
+ start_time=start_time_utc_nano,
207
+ )
208
+
209
+ # The following line of code is commented out to serve as a reminder that in a system
210
+ # of callbacks, attaching the context can be hazardous because there is no guarantee
211
+ # that the context will be detached. An error could happen between callbacks leaving
212
+ # the context attached forever, and all future spans will use it as parent. What's
213
+ # worse is that the error could have also prevented the span from being exported,
214
+ # leaving all future spans as orphans. That is a very bad scenario.
215
+ # token = context_api.attach(context)
216
+ with self._lock:
217
+ self._spans_by_run[run.id] = span
218
+
219
+ # Send initial span data with input
220
+ input_data = None
221
+ if run.inputs:
222
+ if isinstance(run.inputs, dict):
223
+ input_data = (
224
+ run.inputs.get("input") or run.inputs.get("query") or run.inputs
225
+ )
226
+ else:
227
+ input_data = run.inputs
228
+
229
+ # self._send_span_data_to_api(
230
+ # span_id=format(span.context.span_id, "016x"),
231
+ # input_data=input_data,
232
+ # output_data=None # Output not available at start
233
+ # )
234
+
235
+ @audit_timing # type: ignore
236
+ def _end_trace(self, run: Run) -> None:
237
+ self.run_map.pop(str(run.id), None)
238
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
239
+ return
240
+ span = self._spans_by_run.pop(run.id, None)
241
+ if span:
242
+ try:
243
+ _update_span(span, run)
244
+
245
+ # Send final span data with both input and output
246
+ output_data = None
247
+ if run.outputs:
248
+ if isinstance(run.outputs, dict):
249
+ output_data = (
250
+ run.outputs.get("output")
251
+ or run.outputs.get("result")
252
+ or run.outputs
253
+ )
254
+ else:
255
+ output_data = run.outputs
256
+
257
+ self._send_span_data_to_api(
258
+ span=span, input_data=run.inputs, output_data=output_data
259
+ )
260
+
261
+ except Exception:
262
+ logger.exception("Failed to update span with run data.")
263
+ # We can't use real time because the handler may be
264
+ # called in a background thread.
265
+ end_time_utc_nano = _as_utc_nano(run.end_time) if run.end_time else None
266
+ span.end(end_time=end_time_utc_nano)
267
+
268
+ def _persist_run(self, run: Run) -> None:
269
+ pass
270
+
271
+ def on_llm_error(
272
+ self, error: BaseException, *args: Any, run_id: UUID, **kwargs: Any
273
+ ) -> Run:
274
+ if span := self._spans_by_run.get(run_id):
275
+ _record_exception(span, error)
276
+ # Send error span data
277
+ self._send_span_data_to_api(
278
+ span=span,
279
+ input_data=kwargs.get("inputs"),
280
+ output_data={"error": str(error)},
281
+ )
282
+ return super().on_llm_error(error, *args, run_id=run_id, **kwargs)
283
+
284
+ def on_chain_error(
285
+ self, error: BaseException, *args: Any, run_id: UUID, **kwargs: Any
286
+ ) -> Run:
287
+ if span := self._spans_by_run.get(run_id):
288
+ _record_exception(span, error)
289
+ # Send error span data
290
+ self._send_span_data_to_api(
291
+ span=span,
292
+ input_data=kwargs.get("inputs"),
293
+ output_data={"error": str(error)},
294
+ )
295
+ return super().on_chain_error(error, *args, run_id=run_id, **kwargs)
296
+
297
+ def on_retriever_error(
298
+ self, error: BaseException, *args: Any, run_id: UUID, **kwargs: Any
299
+ ) -> Run:
300
+ if span := self._spans_by_run.get(run_id):
301
+ _record_exception(span, error)
302
+ # Send error span data
303
+ self._send_span_data_to_api(
304
+ span=span,
305
+ input_data=kwargs.get("inputs"),
306
+ output_data={"error": str(error)},
307
+ )
308
+ return super().on_retriever_error(error, *args, run_id=run_id, **kwargs)
309
+
310
+ def on_tool_error(
311
+ self, error: BaseException, *args: Any, run_id: UUID, **kwargs: Any
312
+ ) -> Run:
313
+ if span := self._spans_by_run.get(run_id):
314
+ _record_exception(span, error)
315
+ # Send error span data
316
+ self._send_span_data_to_api(
317
+ span=span,
318
+ input_data=kwargs.get("inputs"),
319
+ output_data={"error": str(error)},
320
+ )
321
+ return super().on_tool_error(error, *args, run_id=run_id, **kwargs)
322
+
323
+ def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:
324
+ """
325
+ This emulates the behavior of the LangChainTracer.
326
+ https://github.com/langchain-ai/langchain/blob/c01467b1f4f9beae8f1edb105b17aa4f36bf6573/libs/core/langchain_core/tracers/langchain.py#L115
327
+
328
+ Although this method exists on the parent class, i.e. `BaseTracer`,
329
+ it requires setting `self._schema_format = "original+chat"`.
330
+ https://github.com/langchain-ai/langchain/blob/c01467b1f4f9beae8f1edb105b17aa4f36bf6573/libs/core/langchain_core/tracers/base.py#L170
331
+
332
+ But currently self._schema_format is marked for internal use.
333
+ https://github.com/langchain-ai/langchain/blob/c01467b1f4f9beae8f1edb105b17aa4f36bf6573/libs/core/langchain_core/tracers/base.py#L60
334
+ """ # noqa: E501
335
+ return LangChainTracer.on_chat_model_start(self, *args, **kwargs) # type: ignore
336
+
337
+
338
+ @audit_timing # type: ignore
339
+ def _record_exception(span: Span, error: BaseException) -> None:
340
+ if isinstance(error, Exception):
341
+ span.record_exception(error)
342
+ return
343
+ exception_type = error.__class__.__name__
344
+ exception_message = str(error)
345
+ if not exception_message:
346
+ exception_message = repr(error)
347
+ attributes: Dict[str, AttributeValue] = {
348
+ OTELSpanAttributes.EXCEPTION_TYPE: exception_type,
349
+ OTELSpanAttributes.EXCEPTION_MESSAGE: exception_message,
350
+ OTELSpanAttributes.EXCEPTION_ESCAPED: False,
351
+ }
352
+ try:
353
+ # See e.g. https://github.com/open-telemetry/opentelemetry-python/blob/e9c7c7529993cd13b4af661e2e3ddac3189a34d0/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py#L967 # noqa: E501
354
+ attributes[OTELSpanAttributes.EXCEPTION_STACKTRACE] = traceback.format_exc()
355
+ except Exception:
356
+ logger.exception("Failed to record exception stacktrace.")
357
+ span.add_event(name="exception", attributes=attributes)
358
+
359
+
360
+ @audit_timing # type: ignore
361
+ def _update_span(span: Span, run: Run) -> None:
362
+ if run.error is None:
363
+ span.set_status(trace_api.StatusCode.OK)
364
+ else:
365
+ span.set_status(trace_api.Status(trace_api.StatusCode.ERROR, run.error))
366
+ span_kind = (
367
+ FiSpanKindValues.AGENT
368
+ if "agent" in run.name.lower()
369
+ else _langchain_run_type_to_span_kind(run.run_type)
370
+ )
371
+ span.set_attribute(FI_SPAN_KIND, span_kind.value)
372
+ span.set_attributes(dict(get_attributes_from_context()))
373
+ filtered_inputs, images, eval_input, query = _filter_images(
374
+ run.inputs.get("messages", [])
375
+ )
376
+
377
+ if not filtered_inputs:
378
+ if isinstance(run.inputs, dict):
379
+ if input_value := run.inputs.get("input"):
380
+ filtered_inputs = str(input_value)
381
+ elif query := run.inputs.get("query"):
382
+ filtered_inputs = str(query)
383
+ elif question := run.inputs.get("question"):
384
+ filtered_inputs = str(question)
385
+ else:
386
+ filtered_inputs = None
387
+
388
+ span.set_attributes(
389
+ dict(
390
+ _flatten(
391
+ chain(
392
+ _as_span_input(filtered_inputs),
393
+ _as_output(_convert_io(run.outputs)),
394
+ _as_input_images(images),
395
+ _as_eval_input(eval_input),
396
+ _prompts(run.inputs),
397
+ _input_messages(run.inputs),
398
+ _output_messages(run.outputs),
399
+ _prompt_template(run),
400
+ _invocation_parameters(run),
401
+ _model_name(run.extra),
402
+ _token_counts(run.outputs),
403
+ _function_calls(run.outputs),
404
+ _tools(run),
405
+ _retrieval_documents(run),
406
+ _metadata(run),
407
+ _as_raw_input_output(run),
408
+ _as_query(query),
409
+ )
410
+ )
411
+ )
412
+ )
413
+
414
+
415
+ def _langchain_run_type_to_span_kind(run_type: str) -> FiSpanKindValues:
416
+ try:
417
+ return FiSpanKindValues(run_type.upper())
418
+ except ValueError:
419
+ return FiSpanKindValues.UNKNOWN
420
+
421
+
422
+ def stop_on_exception(
423
+ wrapped: Callable[..., Iterator[Tuple[str, Any]]],
424
+ ) -> Callable[..., Iterator[Tuple[str, Any]]]:
425
+ def wrapper(*args: Any, **kwargs: Any) -> Iterator[Tuple[str, Any]]:
426
+ start_time = time.perf_counter()
427
+ try:
428
+ yield from wrapped(*args, **kwargs)
429
+ except Exception:
430
+ logger.exception("Failed to get attribute.")
431
+ finally:
432
+ if _AUDIT_TIMING:
433
+ latency_ms = (time.perf_counter() - start_time) * 1000
434
+
435
+ return wrapper
436
+
437
+
438
+ @stop_on_exception
439
+ def _flatten(
440
+ key_values: Iterable[Tuple[str, Any]]
441
+ ) -> Iterator[Tuple[str, AttributeValue]]:
442
+ for key, value in key_values:
443
+ if value is None:
444
+ continue
445
+ if isinstance(value, Mapping):
446
+ for sub_key, sub_value in _flatten(value.items()):
447
+ yield f"{key}.{sub_key}", sub_value
448
+ elif isinstance(value, List) and any(
449
+ isinstance(item, Mapping) for item in value
450
+ ):
451
+ for index, sub_mapping in enumerate(value):
452
+ for sub_key, sub_value in _flatten(sub_mapping.items()):
453
+ yield f"{key}.{index}.{sub_key}", sub_value
454
+ else:
455
+ if isinstance(value, Enum):
456
+ value = value.value
457
+ yield key, value
458
+
459
+
460
+ @stop_on_exception
461
+ def _as_input(values: Iterable[str]) -> Iterator[Tuple[str, str]]:
462
+ return zip((INPUT_VALUE, INPUT_MIME_TYPE), values)
463
+
464
+
465
+ def _as_span_input(input: List[str]) -> Iterator[Tuple[str, Any]]:
466
+ yield INPUT_VALUE, json.dumps(input)
467
+
468
+
469
+ @stop_on_exception
470
+ def _as_output(values: Iterable[str]) -> Iterator[Tuple[str, str]]:
471
+ return zip((OUTPUT_VALUE, OUTPUT_MIME_TYPE), values)
472
+
473
+
474
+ def _as_input_images(image_urls: List[str]) -> Iterator[Tuple[str, Any]]:
475
+ if image_urls:
476
+ yield INPUT_IMAGES, json.dumps(image_urls)
477
+
478
+
479
+ def _as_eval_input(values: Iterable[str]) -> Iterator[Tuple[str, str]]:
480
+ yield EVAL_INPUT, json.dumps(values)
481
+
482
+
483
+ def _as_raw_input_output(run: Run) -> Iterator[Tuple[str, Any]]:
484
+ if raw_input := run.inputs:
485
+ if isinstance(raw_input, dict):
486
+ yield RAW_INPUT, safe_json_dumps(raw_input)
487
+ elif hasattr(raw_input, "__dict__"):
488
+ yield RAW_INPUT, safe_json_dumps(raw_input.__dict__)
489
+ if raw_output := run.outputs:
490
+ if isinstance(raw_output, dict):
491
+ yield RAW_OUTPUT, safe_json_dumps(raw_output)
492
+ elif hasattr(raw_output, "__dict__"):
493
+ yield RAW_OUTPUT, safe_json_dumps(raw_output.__dict__)
494
+
495
+
496
+ def _as_query(values: Iterable[str]) -> Iterator[Tuple[str, str]]:
497
+ if values:
498
+ yield QUERY, safe_json_dumps(values)
499
+
500
+
501
+ def _convert_io(obj: Optional[Mapping[str, Any]]) -> Iterator[str]:
502
+ if not obj:
503
+ return
504
+ assert isinstance(obj, dict), f"expected dict, found {type(obj)}"
505
+ if len(obj) == 1 and isinstance(value := next(iter(obj.values())), str):
506
+ yield value
507
+ else:
508
+ obj = dict(_replace_nan(obj))
509
+ yield safe_json_dumps(obj)
510
+ yield FiMimeTypeValues.JSON.value
511
+
512
+
513
+ def _replace_nan(obj: Mapping[str, Any]) -> Iterator[Tuple[str, Any]]:
514
+ for k, v in obj.items():
515
+ if isinstance(v, float) and not math.isfinite(v):
516
+ yield k, None
517
+ else:
518
+ yield k, v
519
+
520
+
521
+ @stop_on_exception
522
+ def _prompts(inputs: Optional[Mapping[str, Any]]) -> Iterator[Tuple[str, List[str]]]:
523
+ """Yields prompts if present."""
524
+ if not inputs:
525
+ return
526
+ assert hasattr(inputs, "get"), f"expected Mapping, found {type(inputs)}"
527
+ if prompts := inputs.get("prompts"):
528
+ yield LLM_PROMPTS, prompts
529
+
530
+
531
+ @stop_on_exception
532
+ def _input_messages(
533
+ inputs: Optional[Mapping[str, Any]],
534
+ ) -> Iterator[Tuple[str, List[Dict[str, Any]]]]:
535
+ """Yields chat messages if present."""
536
+ if not inputs:
537
+ return
538
+ assert hasattr(inputs, "get"), f"expected Mapping, found {type(inputs)}"
539
+ # There may be more than one set of messages. We'll use just the first set.
540
+ if not (multiple_messages := inputs.get("messages")):
541
+ return
542
+ assert isinstance(
543
+ multiple_messages, Iterable
544
+ ), f"expected Iterable, found {type(multiple_messages)}"
545
+ # This will only get the first set of messages.
546
+ if not (first_messages := next(iter(multiple_messages), None)):
547
+ return
548
+ parsed_messages = []
549
+ if isinstance(first_messages, list):
550
+ for message_data in first_messages:
551
+ if isinstance(message_data, BaseMessage):
552
+ parsed_messages.append(
553
+ dict(_parse_message_data(message_data.to_json()))
554
+ )
555
+ elif hasattr(message_data, "get"):
556
+ parsed_messages.append(dict(_parse_message_data(message_data)))
557
+ else:
558
+ raise ValueError(
559
+ f"failed to parse message of type {type(message_data)}"
560
+ )
561
+ elif isinstance(first_messages, BaseMessage):
562
+ parsed_messages.append(dict(_parse_message_data(first_messages.to_json())))
563
+ elif hasattr(first_messages, "get"):
564
+ parsed_messages.append(dict(_parse_message_data(first_messages)))
565
+ else:
566
+ raise ValueError(f"failed to parse messages of type {type(first_messages)}")
567
+ if parsed_messages:
568
+ yield LLM_INPUT_MESSAGES, parsed_messages
569
+
570
+
571
+ @stop_on_exception
572
+ def _output_messages(
573
+ outputs: Optional[Mapping[str, Any]],
574
+ ) -> Iterator[Tuple[str, List[Dict[str, Any]]]]:
575
+ """Yields chat messages if present."""
576
+ if not outputs:
577
+ return
578
+ assert hasattr(outputs, "get"), f"expected Mapping, found {type(outputs)}"
579
+ # There may be more than one set of generations. We'll use just the first set.
580
+ if not (multiple_generations := outputs.get("generations")):
581
+ return
582
+ assert isinstance(
583
+ multiple_generations, Iterable
584
+ ), f"expected Iterable, found {type(multiple_generations)}"
585
+ # This will only get the first set of generations.
586
+ if not (first_generations := next(iter(multiple_generations), None)):
587
+ return
588
+ assert isinstance(
589
+ first_generations, Iterable
590
+ ), f"expected Iterable, found {type(first_generations)}"
591
+ parsed_messages = []
592
+ for generation in first_generations:
593
+ assert hasattr(generation, "get"), f"expected Mapping, found {type(generation)}"
594
+ if message_data := generation.get("message"):
595
+ if isinstance(message_data, BaseMessage):
596
+ parsed_messages.append(
597
+ dict(_parse_message_data(message_data.to_json()))
598
+ )
599
+ elif hasattr(message_data, "get"):
600
+ parsed_messages.append(dict(_parse_message_data(message_data)))
601
+ else:
602
+ raise ValueError(f"fail to parse message of type {type(message_data)}")
603
+ if parsed_messages:
604
+ yield LLM_OUTPUT_MESSAGES, parsed_messages
605
+
606
+
607
+ @stop_on_exception
608
+ def _parse_message_data(
609
+ message_data: Optional[Mapping[str, Any]]
610
+ ) -> Iterator[Tuple[str, Any]]:
611
+ """Parses message data to grab message role, content, etc."""
612
+ if not message_data:
613
+ return
614
+ assert hasattr(message_data, "get"), f"expected Mapping, found {type(message_data)}"
615
+ id_ = message_data.get("id")
616
+ assert isinstance(id_, List), f"expected list, found {type(id_)}"
617
+ message_class_name = id_[-1]
618
+ if message_class_name.startswith("HumanMessage"):
619
+ role = "user"
620
+ elif message_class_name.startswith("AIMessage"):
621
+ role = "assistant"
622
+ elif message_class_name.startswith("SystemMessage"):
623
+ role = "system"
624
+ elif message_class_name.startswith("FunctionMessage"):
625
+ role = "function"
626
+ elif message_class_name.startswith("ToolMessage"):
627
+ role = "tool"
628
+ elif message_class_name.startswith("ChatMessage"):
629
+ role = message_data["kwargs"]["role"]
630
+ else:
631
+ raise ValueError(f"Cannot parse message of type: {message_class_name}")
632
+ yield MESSAGE_ROLE, role
633
+ if kwargs := message_data.get("kwargs"):
634
+ assert hasattr(kwargs, "get"), f"expected Mapping, found {type(kwargs)}"
635
+ if content := kwargs.get("content"):
636
+ if isinstance(content, str):
637
+ yield MESSAGE_CONTENT, content
638
+ elif isinstance(content, list):
639
+ for i, obj in enumerate(content):
640
+ assert hasattr(obj, "get"), f"expected Mapping, found {type(obj)}"
641
+ for k, v in _get_attributes_from_message_content(obj):
642
+ yield f"{MESSAGE_CONTENTS}.{i}.{k}", v
643
+ if additional_kwargs := kwargs.get("additional_kwargs"):
644
+ assert hasattr(
645
+ additional_kwargs, "get"
646
+ ), f"expected Mapping, found {type(additional_kwargs)}"
647
+ if function_call := additional_kwargs.get("function_call"):
648
+ assert hasattr(
649
+ function_call, "get"
650
+ ), f"expected Mapping, found {type(function_call)}"
651
+ if name := function_call.get("name"):
652
+ assert isinstance(name, str), f"expected str, found {type(name)}"
653
+ yield MESSAGE_FUNCTION_CALL_NAME, name
654
+ if arguments := function_call.get("arguments"):
655
+ assert isinstance(
656
+ arguments, str
657
+ ), f"expected str, found {type(arguments)}"
658
+ yield MESSAGE_FUNCTION_CALL_ARGUMENTS_JSON, arguments
659
+ if tool_calls := additional_kwargs.get("tool_calls"):
660
+ assert isinstance(
661
+ tool_calls, Iterable
662
+ ), f"expected Iterable, found {type(tool_calls)}"
663
+ message_tool_calls = []
664
+ for tool_call in tool_calls:
665
+ if message_tool_call := dict(_get_tool_call(tool_call)):
666
+ message_tool_calls.append(message_tool_call)
667
+ if message_tool_calls:
668
+ yield MESSAGE_TOOL_CALLS, message_tool_calls
669
+
670
+ # Add handling for tool_outputs
671
+ if tool_outputs := additional_kwargs.get("tool_outputs"):
672
+ assert isinstance(
673
+ tool_outputs, Iterable
674
+ ), f"expected Iterable, found {type(tool_outputs)}"
675
+ yield "tool_outputs", safe_json_dumps(tool_outputs)
676
+
677
+ if audio := additional_kwargs.get("audio"):
678
+ yield MessageContentAttributes.MESSAGE_CONTENT_TYPE, "audio"
679
+ if audio_data := audio.get("data"):
680
+ yield MessageContentAttributes.MESSAGE_CONTENT_AUDIO, audio_data
681
+ if transcript := audio.get("transcript"):
682
+ yield MessageContentAttributes.MESSAGE_AUDIO_TRANSCRIPT, transcript
683
+
684
+
685
+ @stop_on_exception
686
+ def _get_tool_call(tool_call: Optional[Mapping[str, Any]]) -> Iterator[Tuple[str, Any]]:
687
+ if not tool_call:
688
+ return
689
+ assert hasattr(tool_call, "get"), f"expected Mapping, found {type(tool_call)}"
690
+ if function := tool_call.get("function"):
691
+ assert hasattr(function, "get"), f"expected Mapping, found {type(function)}"
692
+ if name := function.get("name"):
693
+ assert isinstance(name, str), f"expected str, found {type(name)}"
694
+ yield TOOL_CALL_FUNCTION_NAME, name
695
+ if arguments := function.get("arguments"):
696
+ assert isinstance(arguments, str), f"expected str, found {type(arguments)}"
697
+ yield TOOL_CALL_FUNCTION_ARGUMENTS_JSON, arguments
698
+
699
+
700
+ @stop_on_exception
701
+ def _prompt_template(run: Run) -> Iterator[Tuple[str, AttributeValue]]:
702
+ yield from _parse_prompt_template(run.inputs, run.serialized)
703
+
704
+
705
+ @stop_on_exception
706
+ def _parse_prompt_template(
707
+ inputs: Mapping[str, str],
708
+ serialized: Optional[Mapping[str, Any]],
709
+ ) -> Iterator[Tuple[str, AttributeValue]]:
710
+ if (
711
+ not serialized
712
+ or not isinstance(serialized, Mapping)
713
+ or not (kwargs := serialized.get("kwargs"))
714
+ or not isinstance(kwargs, Mapping)
715
+ ):
716
+ return
717
+ if _get_cls_name(prompt := kwargs.get("prompt")).endswith("PromptTemplate"):
718
+ yield from _parse_prompt_template(inputs, prompt)
719
+ elif _get_cls_name(serialized).endswith("ChatPromptTemplate"):
720
+ messages = kwargs.get("messages")
721
+ assert isinstance(messages, Sequence), f"expected list, found {type(messages)}"
722
+ # FIXME: Multiple templates are possible (and the templated messages can also be
723
+ # interleaved with user massages), but we only have room for one template.
724
+ message = messages[0]
725
+ assert isinstance(message, Mapping), f"expected dict, found {type(message)}"
726
+ if partial_variables := kwargs.get("partial_variables"):
727
+ assert isinstance(
728
+ partial_variables, Mapping
729
+ ), f"expected dict, found {type(partial_variables)}"
730
+ inputs = {**partial_variables, **inputs}
731
+ yield from _parse_prompt_template(inputs, message)
732
+ elif _get_cls_name(serialized).endswith("PromptTemplate") and isinstance(
733
+ (template := kwargs.get("template")), str
734
+ ):
735
+ yield LLM_PROMPT_TEMPLATE, template
736
+ if input_variables := kwargs.get("input_variables"):
737
+ assert isinstance(
738
+ input_variables, list
739
+ ), f"expected list, found {type(input_variables)}"
740
+ template_variables = {}
741
+ for variable in input_variables:
742
+ if (value := inputs.get(variable)) is not None:
743
+ template_variables[variable] = value
744
+ if template_variables:
745
+ yield LLM_PROMPT_TEMPLATE_VARIABLES, safe_json_dumps(template_variables)
746
+
747
+
748
+ @stop_on_exception
749
+ def _invocation_parameters(run: Run) -> Iterator[Tuple[str, str]]:
750
+ """Yields invocation parameters if present."""
751
+ if run.run_type.lower() != "llm":
752
+ return
753
+ if not (extra := run.extra):
754
+ return
755
+ assert hasattr(extra, "get"), f"expected Mapping, found {type(extra)}"
756
+ if invocation_parameters := extra.get("invocation_params"):
757
+ assert isinstance(
758
+ invocation_parameters, Mapping
759
+ ), f"expected Mapping, found {type(invocation_parameters)}"
760
+ yield LLM_INVOCATION_PARAMETERS, safe_json_dumps(invocation_parameters)
761
+
762
+
763
+ @stop_on_exception
764
+ def _model_name(extra: Optional[Mapping[str, Any]]) -> Iterator[Tuple[str, str]]:
765
+ """Yields model name if present."""
766
+ if not extra:
767
+ return
768
+ assert hasattr(extra, "get"), f"expected Mapping, found {type(extra)}"
769
+ if not (invocation_params := extra.get("invocation_params")):
770
+ return
771
+ for key in ["model_name", "model"]:
772
+ if name := invocation_params.get(key):
773
+ yield LLM_MODEL_NAME, name
774
+ return
775
+
776
+
777
+ @stop_on_exception
778
+ def _token_counts(outputs: Optional[Mapping[str, Any]]) -> Iterator[Tuple[str, int]]:
779
+ """Yields token count information if present."""
780
+ if not (
781
+ token_usage := (
782
+ _parse_token_usage_for_non_streaming_outputs(outputs)
783
+ or _parse_token_usage_for_streaming_outputs(outputs)
784
+ )
785
+ ):
786
+ return
787
+ for attribute_name, keys in [
788
+ (
789
+ LLM_TOKEN_COUNT_PROMPT,
790
+ (
791
+ "prompt_tokens",
792
+ "input_tokens", # Anthropic-specific key
793
+ ),
794
+ ),
795
+ (
796
+ LLM_TOKEN_COUNT_COMPLETION,
797
+ (
798
+ "completion_tokens",
799
+ "output_tokens", # Anthropic-specific key
800
+ ),
801
+ ),
802
+ (LLM_TOKEN_COUNT_TOTAL, ("total_tokens",)),
803
+ ]:
804
+ if (token_count := _get_first_value(token_usage, keys)) is not None:
805
+ yield attribute_name, token_count
806
+
807
+
808
+ def _parse_token_usage_for_non_streaming_outputs(
809
+ outputs: Optional[Mapping[str, Any]],
810
+ ) -> Any:
811
+ """
812
+ Parses output to get token usage information for non-streaming LLMs, i.e.,
813
+ when `stream_usage` is set to false.
814
+ """
815
+ if (
816
+ outputs
817
+ and hasattr(outputs, "get")
818
+ and (llm_output := outputs.get("llm_output"))
819
+ and hasattr(llm_output, "get")
820
+ and (
821
+ token_usage := _get_first_value(
822
+ llm_output,
823
+ (
824
+ "token_usage",
825
+ "usage", # Anthropic-specific key
826
+ ),
827
+ )
828
+ )
829
+ ):
830
+ return token_usage
831
+ return None
832
+
833
+
834
+ def _parse_token_usage_for_streaming_outputs(
835
+ outputs: Optional[Mapping[str, Any]],
836
+ ) -> Any:
837
+ """
838
+ Parses output to get token usage information for streaming LLMs, i.e., when
839
+ `stream_usage` is set to true.
840
+ """
841
+ if (
842
+ outputs
843
+ and hasattr(outputs, "get")
844
+ and (generations := outputs.get("generations"))
845
+ and hasattr(generations, "__getitem__")
846
+ and generations[0]
847
+ and hasattr(generations[0], "__getitem__")
848
+ and (generation := generations[0][0])
849
+ and hasattr(generation, "get")
850
+ and (message := generation.get("message"))
851
+ and hasattr(message, "get")
852
+ and (kwargs := message.get("kwargs"))
853
+ and hasattr(kwargs, "get")
854
+ and (token_usage := kwargs.get("usage_metadata"))
855
+ ):
856
+ return token_usage
857
+ return None
858
+
859
+
860
+ @stop_on_exception
861
+ def _function_calls(outputs: Optional[Mapping[str, Any]]) -> Iterator[Tuple[str, str]]:
862
+ """Yields function call information if present."""
863
+ if not outputs:
864
+ return
865
+ assert hasattr(outputs, "get"), f"expected Mapping, found {type(outputs)}"
866
+ try:
867
+ function_call_data = deepcopy(
868
+ outputs["generations"][0][0]["message"]["kwargs"]["additional_kwargs"][
869
+ "function_call"
870
+ ]
871
+ )
872
+ function_call_data["arguments"] = json.loads(function_call_data["arguments"])
873
+ yield LLM_FUNCTION_CALL, safe_json_dumps(function_call_data)
874
+ except Exception:
875
+ pass
876
+
877
+
878
+ @stop_on_exception
879
+ def _tools(run: Run) -> Iterator[Tuple[str, str]]:
880
+ """Yields tool attributes if present."""
881
+ if run.run_type.lower() != "tool":
882
+ return
883
+ if not (serialized := run.serialized):
884
+ return
885
+ assert hasattr(serialized, "get"), f"expected Mapping, found {type(serialized)}"
886
+ if name := serialized.get("name"):
887
+ yield TOOL_NAME, name
888
+ if description := serialized.get("description"):
889
+ yield TOOL_DESCRIPTION, description
890
+
891
+
892
+ @stop_on_exception
893
+ def _retrieval_documents(run: Run) -> Iterator[Tuple[str, List[Mapping[str, Any]]]]:
894
+ if run.run_type.lower() != "retriever":
895
+ return
896
+ if not (outputs := run.outputs):
897
+ return
898
+ assert hasattr(outputs, "get"), f"expected Mapping, found {type(outputs)}"
899
+ documents = outputs.get("documents")
900
+ assert isinstance(
901
+ documents, Iterable
902
+ ), f"expected Iterable, found {type(documents)}"
903
+ yield RETRIEVAL_DOCUMENTS, [dict(_as_document(document)) for document in documents]
904
+
905
+
906
+ @stop_on_exception
907
+ def _metadata(run: Run) -> Iterator[Tuple[str, str]]:
908
+ """
909
+ Takes the LangChain chain metadata and adds it to the trace
910
+ """
911
+ if not run.extra or not (metadata := run.extra.get("metadata")):
912
+ return
913
+ assert isinstance(metadata, Mapping), f"expected Mapping, found {type(metadata)}"
914
+ if session_id := (
915
+ metadata.get(LANGCHAIN_SESSION_ID)
916
+ or metadata.get(LANGCHAIN_CONVERSATION_ID)
917
+ or metadata.get(LANGCHAIN_THREAD_ID)
918
+ ):
919
+ yield SESSION_ID, session_id
920
+ yield METADATA, safe_json_dumps(metadata)
921
+
922
+
923
+ @stop_on_exception
924
+ def _as_document(document: Any) -> Iterator[Tuple[str, Any]]:
925
+ if page_content := getattr(document, "page_content", None):
926
+ assert isinstance(
927
+ page_content, str
928
+ ), f"expected str, found {type(page_content)}"
929
+ yield DOCUMENT_CONTENT, page_content
930
+ if metadata := getattr(document, "metadata", None):
931
+ assert isinstance(
932
+ metadata, Mapping
933
+ ), f"expected Mapping, found {type(metadata)}"
934
+ yield DOCUMENT_METADATA, safe_json_dumps(metadata)
935
+
936
+
937
+ def _as_utc_nano(dt: datetime) -> int:
938
+ return int(dt.astimezone(timezone.utc).timestamp() * 1_000_000_000)
939
+
940
+
941
+ def _get_cls_name(serialized: Optional[Mapping[str, Any]]) -> str:
942
+ """
943
+ For a `Serializable` object, its class name, i.e. `cls.__name__`, is the last element of
944
+ its `lc_id`. See https://github.com/langchain-ai/langchain/blob/9e4a0e76f6aa9796ad7baa7f623ba98274676c6f/libs/core/langchain_core/load/serializable.py#L159
945
+
946
+ For example, for the class `langchain.llms.openai.OpenAI`, the id is
947
+ ["langchain", "llms", "openai", "OpenAI"], and `cls.__name__` is "OpenAI".
948
+ """ # noqa E501
949
+ if serialized is None or not hasattr(serialized, "get"):
950
+ return ""
951
+ if (
952
+ (id_ := serialized.get("id"))
953
+ and isinstance(id_, list)
954
+ and isinstance(id_[-1], str)
955
+ ):
956
+ return id_[-1]
957
+ return ""
958
+
959
+
960
+ KeyType = TypeVar("KeyType")
961
+ ValueType = TypeVar("ValueType")
962
+
963
+
964
+ def _get_first_value(
965
+ mapping: Mapping[KeyType, ValueType], keys: Iterable[KeyType]
966
+ ) -> Optional[ValueType]:
967
+ """
968
+ Returns the first non-null value corresponding to an input key, or None if
969
+ no non-null value is found.
970
+ """
971
+ if not hasattr(mapping, "get"):
972
+ return None
973
+ return next(
974
+ (value for key in keys if (value := mapping.get(key)) is not None),
975
+ None,
976
+ )
977
+
978
+
979
+ def _get_attributes_from_message_content(
980
+ content: Mapping[str, Any],
981
+ ) -> Iterator[Tuple[str, AttributeValue]]:
982
+ content = dict(content)
983
+ type_ = content.pop("type")
984
+ if type_ == "text":
985
+ yield f"{MESSAGE_CONTENT_TYPE}", "text"
986
+ if text := content.pop("text"):
987
+ yield f"{MESSAGE_CONTENT_TEXT}", text
988
+ elif type_ == "image_url":
989
+ yield f"{MESSAGE_CONTENT_TYPE}", "image"
990
+ if image := content.pop("image_url"):
991
+ for key, value in _get_attributes_from_image(image):
992
+ yield f"{MESSAGE_CONTENT_IMAGE}.{key}", value
993
+ elif type_ == "input_image":
994
+ yield f"{MESSAGE_CONTENT_TYPE}", "image"
995
+ if image_url := content.pop("image_url", None):
996
+ # Handle direct URL string for input_image type
997
+ yield f"{MESSAGE_CONTENT_IMAGE}", image_url
998
+ elif type_ == "input_audio":
999
+ if audio := content.pop("input_audio"):
1000
+ yield f"{MESSAGE_CONTENT_TYPE}", "audio"
1001
+ yield f"{MESSAGE_CONTENT_AUDIO}", audio.get("data", "")
1002
+
1003
+
1004
+ def _get_attributes_from_image(
1005
+ image: Mapping[str, Any],
1006
+ ) -> Iterator[Tuple[str, AttributeValue]]:
1007
+ image = dict(image)
1008
+ if url := image.pop("url"):
1009
+ yield f"{IMAGE_URL}", url
1010
+
1011
+
1012
+ def _filter_images(messages: Any) -> Tuple[Any, List[str]]:
1013
+ filtered_data = []
1014
+ images = []
1015
+ eval_input = []
1016
+ query = ""
1017
+
1018
+ if not messages or not isinstance(messages, list):
1019
+ return messages, [], "", ""
1020
+
1021
+ for message_group in messages:
1022
+ if not isinstance(message_group, list):
1023
+ continue
1024
+
1025
+ for message in message_group:
1026
+ if isinstance(message, dict):
1027
+ kwargs = message.get("kwargs", {})
1028
+ if not isinstance(kwargs, dict):
1029
+ continue
1030
+
1031
+ content = kwargs.get("content")
1032
+ if isinstance(content, str):
1033
+ filtered_data.append({"type": "text", "text": content})
1034
+ eval_input.append(content)
1035
+ elif isinstance(content, list):
1036
+ for obj in content:
1037
+ if not isinstance(obj, dict):
1038
+ continue
1039
+
1040
+ if obj.get("type") == "text":
1041
+ filtered_data.append(obj)
1042
+ text = obj.get("text")
1043
+ if text:
1044
+ eval_input.append(str(text))
1045
+ elif obj.get("type") == "image_url":
1046
+ image_url = obj.get("image_url", {}).get("url")
1047
+ if image_url:
1048
+ images.append(image_url)
1049
+ filtered_data.append(
1050
+ {"type": "text", "text": f"[Image: {image_url}]"}
1051
+ )
1052
+
1053
+ if eval_input and len(eval_input) > 0:
1054
+ query = eval_input[0]
1055
+ eval_input = " | ".join(map(str, eval_input))
1056
+ return filtered_data, images, eval_input, query
1057
+
1058
+
1059
+ LANGCHAIN_SESSION_ID = "session_id"
1060
+ LANGCHAIN_CONVERSATION_ID = "conversation_id"
1061
+ LANGCHAIN_THREAD_ID = "thread_id"
1062
+
1063
+ DOCUMENT_CONTENT = DocumentAttributes.DOCUMENT_CONTENT
1064
+ DOCUMENT_ID = DocumentAttributes.DOCUMENT_ID
1065
+ DOCUMENT_METADATA = DocumentAttributes.DOCUMENT_METADATA
1066
+ DOCUMENT_SCORE = DocumentAttributes.DOCUMENT_SCORE
1067
+ EMBEDDING_EMBEDDINGS = SpanAttributes.EMBEDDING_EMBEDDINGS
1068
+ EMBEDDING_MODEL_NAME = SpanAttributes.EMBEDDING_MODEL_NAME
1069
+ EMBEDDING_TEXT = EmbeddingAttributes.EMBEDDING_TEXT
1070
+ EMBEDDING_VECTOR = EmbeddingAttributes.EMBEDDING_VECTOR
1071
+ IMAGE_URL = ImageAttributes.IMAGE_URL
1072
+ INPUT_MIME_TYPE = SpanAttributes.INPUT_MIME_TYPE
1073
+ INPUT_VALUE = SpanAttributes.INPUT_VALUE
1074
+ INPUT_IMAGES = SpanAttributes.INPUT_IMAGES
1075
+ EVAL_INPUT = SpanAttributes.EVAL_INPUT
1076
+ RAW_INPUT = SpanAttributes.RAW_INPUT
1077
+ RAW_OUTPUT = SpanAttributes.RAW_OUTPUT
1078
+ QUERY = SpanAttributes.QUERY
1079
+ LLM_FUNCTION_CALL = SpanAttributes.LLM_FUNCTION_CALL
1080
+ LLM_INPUT_MESSAGES = SpanAttributes.LLM_INPUT_MESSAGES
1081
+ LLM_INVOCATION_PARAMETERS = SpanAttributes.LLM_INVOCATION_PARAMETERS
1082
+ LLM_MODEL_NAME = SpanAttributes.LLM_MODEL_NAME
1083
+ LLM_OUTPUT_MESSAGES = SpanAttributes.LLM_OUTPUT_MESSAGES
1084
+ LLM_PROMPTS = SpanAttributes.LLM_PROMPTS
1085
+ LLM_PROMPT_TEMPLATE = SpanAttributes.LLM_PROMPT_TEMPLATE
1086
+ LLM_PROMPT_TEMPLATE_VARIABLES = SpanAttributes.LLM_PROMPT_TEMPLATE_VARIABLES
1087
+ LLM_TOKEN_COUNT_COMPLETION = SpanAttributes.LLM_TOKEN_COUNT_COMPLETION
1088
+ LLM_TOKEN_COUNT_PROMPT = SpanAttributes.LLM_TOKEN_COUNT_PROMPT
1089
+ LLM_TOKEN_COUNT_TOTAL = SpanAttributes.LLM_TOKEN_COUNT_TOTAL
1090
+ MESSAGE_CONTENT = MessageAttributes.MESSAGE_CONTENT
1091
+ MESSAGE_CONTENTS = MessageAttributes.MESSAGE_CONTENTS
1092
+ MESSAGE_CONTENT_IMAGE = MessageContentAttributes.MESSAGE_CONTENT_IMAGE
1093
+ MESSAGE_CONTENT_AUDIO = MessageContentAttributes.MESSAGE_CONTENT_AUDIO
1094
+ MESSAGE_CONTENT_TEXT = MessageContentAttributes.MESSAGE_CONTENT_TEXT
1095
+ MESSAGE_CONTENT_TYPE = MessageContentAttributes.MESSAGE_CONTENT_TYPE
1096
+ MESSAGE_FUNCTION_CALL_ARGUMENTS_JSON = (
1097
+ MessageAttributes.MESSAGE_FUNCTION_CALL_ARGUMENTS_JSON
1098
+ )
1099
+ MESSAGE_FUNCTION_CALL_NAME = MessageAttributes.MESSAGE_FUNCTION_CALL_NAME
1100
+ MESSAGE_NAME = MessageAttributes.MESSAGE_NAME
1101
+ MESSAGE_ROLE = MessageAttributes.MESSAGE_ROLE
1102
+ MESSAGE_TOOL_CALLS = MessageAttributes.MESSAGE_TOOL_CALLS
1103
+ METADATA = SpanAttributes.METADATA
1104
+ FI_SPAN_KIND = SpanAttributes.FI_SPAN_KIND
1105
+ OUTPUT_MIME_TYPE = SpanAttributes.OUTPUT_MIME_TYPE
1106
+ OUTPUT_VALUE = SpanAttributes.OUTPUT_VALUE
1107
+ RERANKER_INPUT_DOCUMENTS = RerankerAttributes.RERANKER_INPUT_DOCUMENTS
1108
+ RERANKER_MODEL_NAME = RerankerAttributes.RERANKER_MODEL_NAME
1109
+ RERANKER_OUTPUT_DOCUMENTS = RerankerAttributes.RERANKER_OUTPUT_DOCUMENTS
1110
+ RERANKER_QUERY = RerankerAttributes.RERANKER_QUERY
1111
+ RERANKER_TOP_K = RerankerAttributes.RERANKER_TOP_K
1112
+ RETRIEVAL_DOCUMENTS = SpanAttributes.RETRIEVAL_DOCUMENTS
1113
+ SESSION_ID = SpanAttributes.SESSION_ID
1114
+ TOOL_CALL_FUNCTION_ARGUMENTS_JSON = ToolCallAttributes.TOOL_CALL_FUNCTION_ARGUMENTS_JSON
1115
+ TOOL_CALL_FUNCTION_NAME = ToolCallAttributes.TOOL_CALL_FUNCTION_NAME
1116
+ TOOL_DESCRIPTION = SpanAttributes.TOOL_DESCRIPTION
1117
+ TOOL_NAME = SpanAttributes.TOOL_NAME
1118
+ TOOL_PARAMETERS = SpanAttributes.TOOL_PARAMETERS
@@ -0,0 +1,2 @@
1
+ _instruments = ("langchain_core >= 0.1.0", "futureagi >= 0.0.1")
2
+ _supports_metrics = False
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.3
2
+ Name: traceAI-langchain
3
+ Version: 0.1.6
4
+ Summary: OpenTelemetry instrumentation for Langchain
5
+ Author: Future AGI
6
+ Author-email: no-reply@futureagi.com
7
+ Requires-Python: >3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: fi-instrumentation-otel (==0.1.5)
14
+ Requires-Dist: langchain-community[all] (>=0.3.9,<0.4.0)
15
+ Requires-Dist: langchain-core (>=0.2.43)
16
+ Requires-Dist: langchain[all] (>=0.3.9,<0.4.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # LangChain OpenTelemetry Integration
20
+
21
+ ## Overview
22
+ This integration provides support for using OpenTelemetry with the LangChain framework. It enables tracing and monitoring of applications built with LangChain.
23
+
24
+ ## Installation
25
+
26
+ 1. **Install traceAI LangChain**
27
+
28
+ ```bash
29
+ pip install traceAI-langchain
30
+ ```
31
+
32
+ 2. **Install LangChain OpenAI**
33
+
34
+ ```bash
35
+ pip install langchain-openai
36
+ ```
37
+
38
+
39
+ ### Set Environment Variables
40
+ Set up your environment variables to authenticate with FutureAGI.
41
+
42
+ ```python
43
+ import os
44
+
45
+ os.environ["FI_API_KEY"] = FI_API_KEY
46
+ os.environ["FI_SECRET_KEY"] = FI_SECRET_KEY
47
+ os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
48
+ ```
49
+
50
+ ## Quickstart
51
+
52
+ ### Register Tracer Provider
53
+ Set up the trace provider to establish the observability pipeline. The trace provider:
54
+
55
+ ```python
56
+ from fi_instrumentation import register
57
+ from fi_instrumentation.fi_types import ProjectType
58
+
59
+ trace_provider = register(
60
+ project_type=ProjectType.OBSERVE,
61
+ project_name="langchain_app",
62
+ project_version_name="v1",
63
+ session_name="chat-bot"
64
+ )
65
+ ```
66
+
67
+ ### Configure LangChain Instrumentation
68
+ Instrument the LangChain client to enable telemetry collection. This step ensures that all interactions with the LangChain SDK are tracked and monitored.
69
+
70
+ ```python
71
+ from traceai_langchain import LangChainInstrumentor
72
+
73
+ LangChainInstrumentor().instrument(tracer_provider=trace_provider)
74
+ ```
75
+
76
+ ### Create LangChain Components
77
+ Set up your LangChain client with built-in observability.
78
+
79
+ ```python
80
+ from langchain_openai import ChatOpenAI
81
+ from langchain_core.prompts import ChatPromptTemplate
82
+
83
+ prompt = ChatPromptTemplate.from_template("{x} {y} {z}?").partial(x="why is", z="blue")
84
+ chain = prompt | ChatOpenAI(model_name="gpt-3.5-turbo")
85
+
86
+ def run_chain():
87
+ try:
88
+ result = chain.invoke({"y": "sky"})
89
+ print(f"Response: {result}")
90
+ except Exception as e:
91
+ print(f"Error executing chain: {e}")
92
+
93
+ if __name__ == "__main__":
94
+ run_chain()
95
+ ```
96
+
97
+
@@ -0,0 +1,7 @@
1
+ traceai_langchain/__init__.py,sha256=fxcF5wHzaAze6BMY-Jk-ledaKI5s4trLg2V3SoFlKSg,5625
2
+ traceai_langchain/_tracer.py,sha256=_-i9xf-A9rYU8X7pKRmBkDl0yO8bv1lJ7KeZGjzmlaM,43040
3
+ traceai_langchain/package.py,sha256=aLEX_fPTzLHD-Pcsfaq-dW1NM3aV3LJbtire7jciE6k,91
4
+ traceai_langchain/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
5
+ traceai_langchain-0.1.6.dist-info/METADATA,sha256=wygxnoVmHMbSknOQNwiDVIrWrD-D7Hup9Ys5GP6g4iU,2619
6
+ traceai_langchain-0.1.6.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
7
+ traceai_langchain-0.1.6.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.0.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any