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 @@
|
|
|
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,,
|