sentry-sdk 3.0.0a5__py2.py3-none-any.whl → 3.0.0a6__py2.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.
Potentially problematic release.
This version of sentry-sdk might be problematic. Click here for more details.
- sentry_sdk/ai/utils.py +7 -8
- sentry_sdk/api.py +13 -2
- sentry_sdk/client.py +93 -17
- sentry_sdk/consts.py +14 -6
- sentry_sdk/crons/api.py +5 -0
- sentry_sdk/integrations/anthropic.py +133 -73
- sentry_sdk/integrations/asgi.py +10 -9
- sentry_sdk/integrations/asyncio.py +85 -20
- sentry_sdk/integrations/clickhouse_driver.py +55 -28
- sentry_sdk/integrations/fastapi.py +1 -7
- sentry_sdk/integrations/gnu_backtrace.py +6 -3
- sentry_sdk/integrations/langchain.py +462 -218
- sentry_sdk/integrations/litestar.py +1 -1
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +0 -2
- sentry_sdk/integrations/openai_agents/patches/runner.py +18 -15
- sentry_sdk/integrations/quart.py +1 -1
- sentry_sdk/integrations/starlette.py +1 -5
- sentry_sdk/integrations/starlite.py +2 -2
- sentry_sdk/scope.py +11 -11
- sentry_sdk/tracing.py +94 -17
- sentry_sdk/tracing_utils.py +330 -33
- sentry_sdk/transport.py +357 -62
- sentry_sdk/utils.py +23 -5
- sentry_sdk/worker.py +197 -3
- {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a6.dist-info}/METADATA +3 -1
- {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a6.dist-info}/RECORD +30 -30
- {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a6.dist-info}/WHEEL +0 -0
- {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a6.dist-info}/entry_points.txt +0 -0
- {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a6.dist-info}/licenses/LICENSE +0 -0
- {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a6.dist-info}/top_level.txt +0 -0
|
@@ -4,55 +4,59 @@ from collections import OrderedDict
|
|
|
4
4
|
from functools import wraps
|
|
5
5
|
|
|
6
6
|
import sentry_sdk
|
|
7
|
-
from sentry_sdk.ai.monitoring import set_ai_pipeline_name
|
|
8
|
-
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
|
|
7
|
+
from sentry_sdk.ai.monitoring import set_ai_pipeline_name
|
|
9
8
|
from sentry_sdk.ai.utils import set_data_normalized
|
|
9
|
+
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
|
|
10
|
+
from sentry_sdk.integrations import DidNotEnable, Integration
|
|
10
11
|
from sentry_sdk.scope import should_send_default_pii
|
|
11
12
|
from sentry_sdk.tracing import Span
|
|
12
|
-
from sentry_sdk.
|
|
13
|
+
from sentry_sdk.tracing_utils import _get_value
|
|
13
14
|
from sentry_sdk.utils import logger, capture_internal_exceptions
|
|
14
15
|
|
|
15
16
|
from typing import TYPE_CHECKING
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
|
-
from typing import
|
|
19
|
+
from typing import (
|
|
20
|
+
Any,
|
|
21
|
+
AsyncIterator,
|
|
22
|
+
Callable,
|
|
23
|
+
Dict,
|
|
24
|
+
Iterator,
|
|
25
|
+
List,
|
|
26
|
+
Optional,
|
|
27
|
+
Union,
|
|
28
|
+
)
|
|
19
29
|
from uuid import UUID
|
|
20
30
|
|
|
31
|
+
|
|
21
32
|
try:
|
|
22
|
-
from
|
|
23
|
-
from langchain_core.
|
|
33
|
+
from langchain.agents import AgentExecutor
|
|
34
|
+
from langchain_core.agents import AgentFinish
|
|
24
35
|
from langchain_core.callbacks import (
|
|
25
|
-
manager,
|
|
26
36
|
BaseCallbackHandler,
|
|
27
37
|
BaseCallbackManager,
|
|
28
38
|
Callbacks,
|
|
39
|
+
manager,
|
|
29
40
|
)
|
|
30
|
-
from langchain_core.
|
|
41
|
+
from langchain_core.messages import BaseMessage
|
|
42
|
+
from langchain_core.outputs import LLMResult
|
|
43
|
+
|
|
31
44
|
except ImportError:
|
|
32
45
|
raise DidNotEnable("langchain not installed")
|
|
33
46
|
|
|
34
47
|
|
|
35
48
|
DATA_FIELDS = {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
49
|
+
"frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY,
|
|
50
|
+
"function_call": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS,
|
|
51
|
+
"max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS,
|
|
52
|
+
"presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY,
|
|
53
|
+
"temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE,
|
|
54
|
+
"tool_calls": SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS,
|
|
55
|
+
"tools": SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS,
|
|
56
|
+
"top_k": SPANDATA.GEN_AI_REQUEST_TOP_K,
|
|
57
|
+
"top_p": SPANDATA.GEN_AI_REQUEST_TOP_P,
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
# To avoid double collecting tokens, we do *not* measure
|
|
48
|
-
# token counts for models for which we have an explicit integration
|
|
49
|
-
NO_COLLECT_TOKEN_MODELS = [
|
|
50
|
-
"openai-chat",
|
|
51
|
-
"anthropic-chat",
|
|
52
|
-
"cohere-chat",
|
|
53
|
-
"huggingface_endpoint",
|
|
54
|
-
]
|
|
55
|
-
|
|
56
60
|
|
|
57
61
|
class LangchainIntegration(Integration):
|
|
58
62
|
identifier = "langchain"
|
|
@@ -62,24 +66,22 @@ class LangchainIntegration(Integration):
|
|
|
62
66
|
max_spans = 1024
|
|
63
67
|
|
|
64
68
|
def __init__(
|
|
65
|
-
self: LangchainIntegration,
|
|
66
|
-
include_prompts: bool = True,
|
|
67
|
-
max_spans: int = 1024,
|
|
68
|
-
tiktoken_encoding_name: Optional[str] = None,
|
|
69
|
+
self: LangchainIntegration, include_prompts: bool = True, max_spans: int = 1024
|
|
69
70
|
) -> None:
|
|
70
71
|
self.include_prompts = include_prompts
|
|
71
72
|
self.max_spans = max_spans
|
|
72
|
-
self.tiktoken_encoding_name = tiktoken_encoding_name
|
|
73
73
|
|
|
74
74
|
@staticmethod
|
|
75
75
|
def setup_once() -> None:
|
|
76
76
|
manager._configure = _wrap_configure(manager._configure)
|
|
77
77
|
|
|
78
|
+
if AgentExecutor is not None:
|
|
79
|
+
AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke)
|
|
80
|
+
AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream)
|
|
81
|
+
|
|
78
82
|
|
|
79
83
|
class WatchedSpan:
|
|
80
|
-
|
|
81
|
-
num_prompt_tokens: int = 0
|
|
82
|
-
no_collect_tokens: bool = False
|
|
84
|
+
span: Span
|
|
83
85
|
children: List[WatchedSpan] = []
|
|
84
86
|
is_pipeline: bool = False
|
|
85
87
|
|
|
@@ -88,29 +90,13 @@ class WatchedSpan:
|
|
|
88
90
|
|
|
89
91
|
|
|
90
92
|
class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
91
|
-
"""
|
|
93
|
+
"""Callback handler that creates Sentry spans."""
|
|
92
94
|
|
|
93
|
-
def __init__(
|
|
94
|
-
self,
|
|
95
|
-
max_span_map_size: int,
|
|
96
|
-
include_prompts: bool,
|
|
97
|
-
tiktoken_encoding_name: Optional[str] = None,
|
|
98
|
-
) -> None:
|
|
95
|
+
def __init__(self, max_span_map_size: int, include_prompts: bool) -> None:
|
|
99
96
|
self.span_map: OrderedDict[UUID, WatchedSpan] = OrderedDict()
|
|
100
97
|
self.max_span_map_size = max_span_map_size
|
|
101
98
|
self.include_prompts = include_prompts
|
|
102
99
|
|
|
103
|
-
self.tiktoken_encoding = None
|
|
104
|
-
if tiktoken_encoding_name is not None:
|
|
105
|
-
import tiktoken # type: ignore
|
|
106
|
-
|
|
107
|
-
self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name)
|
|
108
|
-
|
|
109
|
-
def count_tokens(self, s: str) -> int:
|
|
110
|
-
if self.tiktoken_encoding is not None:
|
|
111
|
-
return len(self.tiktoken_encoding.encode_ordinary(s))
|
|
112
|
-
return 0
|
|
113
|
-
|
|
114
100
|
def gc_span_map(self) -> None:
|
|
115
101
|
|
|
116
102
|
while len(self.span_map) > self.max_span_map_size:
|
|
@@ -118,19 +104,20 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
118
104
|
self._exit_span(watched_span, run_id)
|
|
119
105
|
|
|
120
106
|
def _handle_error(self, run_id: UUID, error: Any) -> None:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
107
|
+
with capture_internal_exceptions():
|
|
108
|
+
if not run_id or run_id not in self.span_map:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
sentry_sdk.capture_exception(error)
|
|
112
|
+
|
|
113
|
+
span_data = self.span_map[run_id]
|
|
114
|
+
span = span_data.span
|
|
115
|
+
span.set_status(SPANSTATUS.INTERNAL_ERROR)
|
|
116
|
+
span.finish()
|
|
117
|
+
del self.span_map[run_id]
|
|
131
118
|
|
|
132
119
|
def _normalize_langchain_message(self, message: BaseMessage) -> Any:
|
|
133
|
-
parsed = {"
|
|
120
|
+
parsed = {"role": message.type, "content": message.content}
|
|
134
121
|
parsed.update(message.additional_kwargs)
|
|
135
122
|
return parsed
|
|
136
123
|
|
|
@@ -140,7 +127,6 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
140
127
|
parent_id: Optional[Any],
|
|
141
128
|
**kwargs: Any,
|
|
142
129
|
) -> WatchedSpan:
|
|
143
|
-
|
|
144
130
|
parent_watched_span = self.span_map.get(parent_id) if parent_id else None
|
|
145
131
|
sentry_span = sentry_sdk.start_span(
|
|
146
132
|
parent_span=parent_watched_span.span if parent_watched_span else None,
|
|
@@ -151,17 +137,6 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
151
137
|
if parent_watched_span:
|
|
152
138
|
parent_watched_span.children.append(watched_span)
|
|
153
139
|
|
|
154
|
-
if kwargs.get("op", "").startswith("ai.pipeline."):
|
|
155
|
-
if kwargs.get("name"):
|
|
156
|
-
set_ai_pipeline_name(kwargs.get("name"))
|
|
157
|
-
watched_span.is_pipeline = True
|
|
158
|
-
|
|
159
|
-
# the same run_id is reused for the pipeline it seems
|
|
160
|
-
# so we need to end the older span to avoid orphan spans
|
|
161
|
-
existing_span_data = self.span_map.get(run_id)
|
|
162
|
-
if existing_span_data is not None:
|
|
163
|
-
self._exit_span(existing_span_data, run_id)
|
|
164
|
-
|
|
165
140
|
self.span_map[run_id] = watched_span
|
|
166
141
|
self.gc_span_map()
|
|
167
142
|
return watched_span
|
|
@@ -169,7 +144,6 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
169
144
|
def _exit_span(
|
|
170
145
|
self: SentryLangchainCallback, span_data: WatchedSpan, run_id: UUID
|
|
171
146
|
) -> None:
|
|
172
|
-
|
|
173
147
|
if span_data.is_pipeline:
|
|
174
148
|
set_ai_pipeline_name(None)
|
|
175
149
|
|
|
@@ -192,21 +166,44 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
192
166
|
with capture_internal_exceptions():
|
|
193
167
|
if not run_id:
|
|
194
168
|
return
|
|
169
|
+
|
|
195
170
|
all_params = kwargs.get("invocation_params", {})
|
|
196
171
|
all_params.update(serialized.get("kwargs", {}))
|
|
172
|
+
|
|
173
|
+
model = (
|
|
174
|
+
all_params.get("model")
|
|
175
|
+
or all_params.get("model_name")
|
|
176
|
+
or all_params.get("model_id")
|
|
177
|
+
or ""
|
|
178
|
+
)
|
|
179
|
+
|
|
197
180
|
watched_span = self._create_span(
|
|
198
181
|
run_id,
|
|
199
|
-
|
|
200
|
-
op=OP.
|
|
182
|
+
parent_run_id,
|
|
183
|
+
op=OP.GEN_AI_PIPELINE,
|
|
201
184
|
name=kwargs.get("name") or "Langchain LLM call",
|
|
202
185
|
origin=LangchainIntegration.origin,
|
|
203
186
|
)
|
|
204
187
|
span = watched_span.span
|
|
188
|
+
|
|
189
|
+
if model:
|
|
190
|
+
span.set_data(
|
|
191
|
+
SPANDATA.GEN_AI_REQUEST_MODEL,
|
|
192
|
+
model,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
ai_type = all_params.get("_type", "")
|
|
196
|
+
if "anthropic" in ai_type:
|
|
197
|
+
span.set_data(SPANDATA.GEN_AI_SYSTEM, "anthropic")
|
|
198
|
+
elif "openai" in ai_type:
|
|
199
|
+
span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai")
|
|
200
|
+
|
|
201
|
+
for key, attribute in DATA_FIELDS.items():
|
|
202
|
+
if key in all_params and all_params[key] is not None:
|
|
203
|
+
set_data_normalized(span, attribute, all_params[key], unpack=False)
|
|
204
|
+
|
|
205
205
|
if should_send_default_pii() and self.include_prompts:
|
|
206
|
-
set_data_normalized(span, SPANDATA.
|
|
207
|
-
for k, v in DATA_FIELDS.items():
|
|
208
|
-
if k in all_params:
|
|
209
|
-
set_data_normalized(span, v, all_params[k])
|
|
206
|
+
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompts)
|
|
210
207
|
|
|
211
208
|
def on_chat_model_start(
|
|
212
209
|
self: SentryLangchainCallback,
|
|
@@ -220,188 +217,153 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
220
217
|
with capture_internal_exceptions():
|
|
221
218
|
if not run_id:
|
|
222
219
|
return
|
|
220
|
+
|
|
223
221
|
all_params = kwargs.get("invocation_params", {})
|
|
224
222
|
all_params.update(serialized.get("kwargs", {}))
|
|
223
|
+
|
|
224
|
+
model = (
|
|
225
|
+
all_params.get("model")
|
|
226
|
+
or all_params.get("model_name")
|
|
227
|
+
or all_params.get("model_id")
|
|
228
|
+
or ""
|
|
229
|
+
)
|
|
230
|
+
|
|
225
231
|
watched_span = self._create_span(
|
|
226
232
|
run_id,
|
|
227
233
|
kwargs.get("parent_run_id"),
|
|
228
|
-
op=OP.
|
|
229
|
-
name=
|
|
234
|
+
op=OP.GEN_AI_CHAT,
|
|
235
|
+
name=f"chat {model}".strip(),
|
|
230
236
|
origin=LangchainIntegration.origin,
|
|
231
237
|
)
|
|
232
238
|
span = watched_span.span
|
|
233
|
-
model = all_params.get(
|
|
234
|
-
"model", all_params.get("model_name", all_params.get("model_id"))
|
|
235
|
-
)
|
|
236
|
-
watched_span.no_collect_tokens = any(
|
|
237
|
-
x in all_params.get("_type", "") for x in NO_COLLECT_TOKEN_MODELS
|
|
238
|
-
)
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
model = "claude-2"
|
|
240
|
+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
|
|
242
241
|
if model:
|
|
243
|
-
span.
|
|
242
|
+
span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model)
|
|
243
|
+
|
|
244
|
+
ai_type = all_params.get("_type", "")
|
|
245
|
+
if "anthropic" in ai_type:
|
|
246
|
+
span.set_data(SPANDATA.GEN_AI_SYSTEM, "anthropic")
|
|
247
|
+
elif "openai" in ai_type:
|
|
248
|
+
span.set_data(SPANDATA.GEN_AI_SYSTEM, "openai")
|
|
249
|
+
|
|
250
|
+
for key, attribute in DATA_FIELDS.items():
|
|
251
|
+
if key in all_params and all_params[key] is not None:
|
|
252
|
+
set_data_normalized(span, attribute, all_params[key], unpack=False)
|
|
253
|
+
|
|
244
254
|
if should_send_default_pii() and self.include_prompts:
|
|
245
255
|
set_data_normalized(
|
|
246
256
|
span,
|
|
247
|
-
SPANDATA.
|
|
257
|
+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
|
|
248
258
|
[
|
|
249
259
|
[self._normalize_langchain_message(x) for x in list_]
|
|
250
260
|
for list_ in messages
|
|
251
261
|
],
|
|
252
262
|
)
|
|
253
|
-
for k, v in DATA_FIELDS.items():
|
|
254
|
-
if k in all_params:
|
|
255
|
-
set_data_normalized(span, v, all_params[k])
|
|
256
|
-
if not watched_span.no_collect_tokens:
|
|
257
|
-
for list_ in messages:
|
|
258
|
-
for message in list_:
|
|
259
|
-
self.span_map[run_id].num_prompt_tokens += self.count_tokens(
|
|
260
|
-
message.content
|
|
261
|
-
) + self.count_tokens(message.type)
|
|
262
|
-
|
|
263
|
-
def on_llm_new_token(
|
|
264
|
-
self: SentryLangchainCallback, token: str, *, run_id: UUID, **kwargs: Any
|
|
265
|
-
) -> Any:
|
|
266
|
-
"""Run on new LLM token. Only available when streaming is enabled."""
|
|
267
|
-
with capture_internal_exceptions():
|
|
268
|
-
if not run_id or run_id not in self.span_map:
|
|
269
|
-
return
|
|
270
|
-
span_data = self.span_map[run_id]
|
|
271
|
-
if not span_data or span_data.no_collect_tokens:
|
|
272
|
-
return
|
|
273
|
-
span_data.num_completion_tokens += self.count_tokens(token)
|
|
274
263
|
|
|
275
|
-
def
|
|
264
|
+
def on_chat_model_end(
|
|
276
265
|
self: SentryLangchainCallback,
|
|
277
266
|
response: LLMResult,
|
|
278
267
|
*,
|
|
279
268
|
run_id: UUID,
|
|
280
269
|
**kwargs: Any,
|
|
281
270
|
) -> Any:
|
|
282
|
-
"""Run when
|
|
271
|
+
"""Run when Chat Model ends running."""
|
|
283
272
|
with capture_internal_exceptions():
|
|
284
|
-
if not run_id:
|
|
273
|
+
if not run_id or run_id not in self.span_map:
|
|
285
274
|
return
|
|
286
275
|
|
|
287
|
-
token_usage = (
|
|
288
|
-
response.llm_output.get("token_usage") if response.llm_output else None
|
|
289
|
-
)
|
|
290
|
-
|
|
291
276
|
span_data = self.span_map[run_id]
|
|
292
|
-
|
|
293
|
-
return
|
|
277
|
+
span = span_data.span
|
|
294
278
|
|
|
295
279
|
if should_send_default_pii() and self.include_prompts:
|
|
296
280
|
set_data_normalized(
|
|
297
|
-
|
|
298
|
-
SPANDATA.
|
|
281
|
+
span,
|
|
282
|
+
SPANDATA.GEN_AI_RESPONSE_TEXT,
|
|
299
283
|
[[x.text for x in list_] for list_ in response.generations],
|
|
300
284
|
)
|
|
301
285
|
|
|
302
|
-
|
|
303
|
-
if token_usage:
|
|
304
|
-
record_token_usage(
|
|
305
|
-
span_data.span,
|
|
306
|
-
input_tokens=token_usage.get("prompt_tokens"),
|
|
307
|
-
output_tokens=token_usage.get("completion_tokens"),
|
|
308
|
-
total_tokens=token_usage.get("total_tokens"),
|
|
309
|
-
)
|
|
310
|
-
else:
|
|
311
|
-
record_token_usage(
|
|
312
|
-
span_data.span,
|
|
313
|
-
input_tokens=span_data.num_prompt_tokens,
|
|
314
|
-
output_tokens=span_data.num_completion_tokens,
|
|
315
|
-
)
|
|
316
|
-
|
|
286
|
+
_record_token_usage(span, response)
|
|
317
287
|
self._exit_span(span_data, run_id)
|
|
318
288
|
|
|
319
|
-
def
|
|
320
|
-
self: SentryLangchainCallback,
|
|
321
|
-
error: Union[Exception, KeyboardInterrupt],
|
|
322
|
-
*,
|
|
323
|
-
run_id: UUID,
|
|
324
|
-
**kwargs: Any,
|
|
325
|
-
) -> Any:
|
|
326
|
-
"""Run when LLM errors."""
|
|
327
|
-
with capture_internal_exceptions():
|
|
328
|
-
self._handle_error(run_id, error)
|
|
329
|
-
|
|
330
|
-
def on_chain_start(
|
|
331
|
-
self: SentryLangchainCallback,
|
|
332
|
-
serialized: Dict[str, Any],
|
|
333
|
-
inputs: Dict[str, Any],
|
|
334
|
-
*,
|
|
335
|
-
run_id: UUID,
|
|
336
|
-
**kwargs: Any,
|
|
337
|
-
) -> Any:
|
|
338
|
-
"""Run when chain starts running."""
|
|
339
|
-
with capture_internal_exceptions():
|
|
340
|
-
if not run_id:
|
|
341
|
-
return
|
|
342
|
-
watched_span = self._create_span(
|
|
343
|
-
run_id,
|
|
344
|
-
kwargs.get("parent_run_id"),
|
|
345
|
-
op=(
|
|
346
|
-
OP.LANGCHAIN_RUN
|
|
347
|
-
if kwargs.get("parent_run_id") is not None
|
|
348
|
-
else OP.LANGCHAIN_PIPELINE
|
|
349
|
-
),
|
|
350
|
-
name=kwargs.get("name") or "Chain execution",
|
|
351
|
-
origin=LangchainIntegration.origin,
|
|
352
|
-
)
|
|
353
|
-
metadata = kwargs.get("metadata")
|
|
354
|
-
if metadata:
|
|
355
|
-
set_data_normalized(watched_span.span, SPANDATA.AI_METADATA, metadata)
|
|
356
|
-
|
|
357
|
-
def on_chain_end(
|
|
289
|
+
def on_llm_end(
|
|
358
290
|
self: SentryLangchainCallback,
|
|
359
|
-
|
|
291
|
+
response: LLMResult,
|
|
360
292
|
*,
|
|
361
293
|
run_id: UUID,
|
|
362
294
|
**kwargs: Any,
|
|
363
295
|
) -> Any:
|
|
364
|
-
"""Run when
|
|
296
|
+
"""Run when LLM ends running."""
|
|
365
297
|
with capture_internal_exceptions():
|
|
366
298
|
if not run_id or run_id not in self.span_map:
|
|
367
299
|
return
|
|
368
300
|
|
|
369
301
|
span_data = self.span_map[run_id]
|
|
370
|
-
|
|
371
|
-
|
|
302
|
+
span = span_data.span
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
generation = response.generations[0][0]
|
|
306
|
+
except IndexError:
|
|
307
|
+
generation = None
|
|
308
|
+
|
|
309
|
+
if generation is not None:
|
|
310
|
+
try:
|
|
311
|
+
response_model = generation.generation_info.get("model_name")
|
|
312
|
+
if response_model is not None:
|
|
313
|
+
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
|
|
314
|
+
except AttributeError:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
finish_reason = generation.generation_info.get("finish_reason")
|
|
319
|
+
if finish_reason is not None:
|
|
320
|
+
span.set_data(
|
|
321
|
+
SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reason
|
|
322
|
+
)
|
|
323
|
+
except AttributeError:
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
tool_calls = getattr(generation.message, "tool_calls", None)
|
|
328
|
+
if tool_calls is not None and tool_calls != []:
|
|
329
|
+
set_data_normalized(
|
|
330
|
+
span,
|
|
331
|
+
SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS,
|
|
332
|
+
tool_calls,
|
|
333
|
+
unpack=False,
|
|
334
|
+
)
|
|
335
|
+
except AttributeError:
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
if should_send_default_pii() and self.include_prompts:
|
|
339
|
+
set_data_normalized(
|
|
340
|
+
span,
|
|
341
|
+
SPANDATA.GEN_AI_RESPONSE_TEXT,
|
|
342
|
+
[[x.text for x in list_] for list_ in response.generations],
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
_record_token_usage(span, response)
|
|
372
346
|
self._exit_span(span_data, run_id)
|
|
373
347
|
|
|
374
|
-
def
|
|
348
|
+
def on_llm_error(
|
|
375
349
|
self: SentryLangchainCallback,
|
|
376
350
|
error: Union[Exception, KeyboardInterrupt],
|
|
377
351
|
*,
|
|
378
352
|
run_id: UUID,
|
|
379
353
|
**kwargs: Any,
|
|
380
354
|
) -> Any:
|
|
381
|
-
"""Run when
|
|
355
|
+
"""Run when LLM errors."""
|
|
382
356
|
self._handle_error(run_id, error)
|
|
383
357
|
|
|
384
|
-
def
|
|
358
|
+
def on_chat_model_error(
|
|
385
359
|
self: SentryLangchainCallback,
|
|
386
|
-
|
|
360
|
+
error: Union[Exception, KeyboardInterrupt],
|
|
387
361
|
*,
|
|
388
362
|
run_id: UUID,
|
|
389
363
|
**kwargs: Any,
|
|
390
364
|
) -> Any:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return
|
|
394
|
-
watched_span = self._create_span(
|
|
395
|
-
run_id,
|
|
396
|
-
kwargs.get("parent_run_id"),
|
|
397
|
-
op=OP.LANGCHAIN_AGENT,
|
|
398
|
-
name=action.tool or "AI tool usage",
|
|
399
|
-
origin=LangchainIntegration.origin,
|
|
400
|
-
)
|
|
401
|
-
if action.tool_input and should_send_default_pii() and self.include_prompts:
|
|
402
|
-
set_data_normalized(
|
|
403
|
-
watched_span.span, SPANDATA.AI_INPUT_MESSAGES, action.tool_input
|
|
404
|
-
)
|
|
365
|
+
"""Run when Chat Model errors."""
|
|
366
|
+
self._handle_error(run_id, error)
|
|
405
367
|
|
|
406
368
|
def on_agent_finish(
|
|
407
369
|
self: SentryLangchainCallback,
|
|
@@ -411,16 +373,19 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
411
373
|
**kwargs: Any,
|
|
412
374
|
) -> Any:
|
|
413
375
|
with capture_internal_exceptions():
|
|
414
|
-
if not run_id:
|
|
376
|
+
if not run_id or run_id not in self.span_map:
|
|
415
377
|
return
|
|
416
378
|
|
|
417
379
|
span_data = self.span_map[run_id]
|
|
418
|
-
|
|
419
|
-
|
|
380
|
+
span = span_data.span
|
|
381
|
+
|
|
420
382
|
if should_send_default_pii() and self.include_prompts:
|
|
421
383
|
set_data_normalized(
|
|
422
|
-
|
|
384
|
+
span,
|
|
385
|
+
SPANDATA.GEN_AI_RESPONSE_TEXT,
|
|
386
|
+
finish.return_values.items(),
|
|
423
387
|
)
|
|
388
|
+
|
|
424
389
|
self._exit_span(span_data, run_id)
|
|
425
390
|
|
|
426
391
|
def on_tool_start(
|
|
@@ -435,23 +400,31 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
435
400
|
with capture_internal_exceptions():
|
|
436
401
|
if not run_id:
|
|
437
402
|
return
|
|
403
|
+
|
|
404
|
+
tool_name = serialized.get("name") or kwargs.get("name") or ""
|
|
405
|
+
|
|
438
406
|
watched_span = self._create_span(
|
|
439
407
|
run_id,
|
|
440
408
|
kwargs.get("parent_run_id"),
|
|
441
|
-
op=OP.
|
|
442
|
-
name=
|
|
409
|
+
op=OP.GEN_AI_EXECUTE_TOOL,
|
|
410
|
+
name=f"execute_tool {tool_name}".strip(),
|
|
443
411
|
origin=LangchainIntegration.origin,
|
|
444
412
|
)
|
|
413
|
+
span = watched_span.span
|
|
414
|
+
|
|
415
|
+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "execute_tool")
|
|
416
|
+
span.set_data(SPANDATA.GEN_AI_TOOL_NAME, tool_name)
|
|
417
|
+
|
|
418
|
+
tool_description = serialized.get("description")
|
|
419
|
+
if tool_description is not None:
|
|
420
|
+
span.set_data(SPANDATA.GEN_AI_TOOL_DESCRIPTION, tool_description)
|
|
421
|
+
|
|
445
422
|
if should_send_default_pii() and self.include_prompts:
|
|
446
423
|
set_data_normalized(
|
|
447
|
-
|
|
448
|
-
SPANDATA.
|
|
424
|
+
span,
|
|
425
|
+
SPANDATA.GEN_AI_TOOL_INPUT,
|
|
449
426
|
kwargs.get("inputs", [input_str]),
|
|
450
427
|
)
|
|
451
|
-
if kwargs.get("metadata"):
|
|
452
|
-
set_data_normalized(
|
|
453
|
-
watched_span.span, SPANDATA.AI_METADATA, kwargs.get("metadata")
|
|
454
|
-
)
|
|
455
428
|
|
|
456
429
|
def on_tool_end(
|
|
457
430
|
self: SentryLangchainCallback, output: str, *, run_id: UUID, **kwargs: Any
|
|
@@ -462,10 +435,11 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
462
435
|
return
|
|
463
436
|
|
|
464
437
|
span_data = self.span_map[run_id]
|
|
465
|
-
|
|
466
|
-
|
|
438
|
+
span = span_data.span
|
|
439
|
+
|
|
467
440
|
if should_send_default_pii() and self.include_prompts:
|
|
468
|
-
set_data_normalized(
|
|
441
|
+
set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output)
|
|
442
|
+
|
|
469
443
|
self._exit_span(span_data, run_id)
|
|
470
444
|
|
|
471
445
|
def on_tool_error(
|
|
@@ -479,6 +453,127 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
|
|
|
479
453
|
self._handle_error(run_id, error)
|
|
480
454
|
|
|
481
455
|
|
|
456
|
+
def _extract_tokens(
|
|
457
|
+
token_usage: Any,
|
|
458
|
+
) -> tuple[Optional[int], Optional[int], Optional[int]]:
|
|
459
|
+
if not token_usage:
|
|
460
|
+
return None, None, None
|
|
461
|
+
|
|
462
|
+
input_tokens = _get_value(token_usage, "prompt_tokens") or _get_value(
|
|
463
|
+
token_usage, "input_tokens"
|
|
464
|
+
)
|
|
465
|
+
output_tokens = _get_value(token_usage, "completion_tokens") or _get_value(
|
|
466
|
+
token_usage, "output_tokens"
|
|
467
|
+
)
|
|
468
|
+
total_tokens = _get_value(token_usage, "total_tokens")
|
|
469
|
+
|
|
470
|
+
return input_tokens, output_tokens, total_tokens
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _extract_tokens_from_generations(
|
|
474
|
+
generations: Any,
|
|
475
|
+
) -> tuple[Optional[int], Optional[int], Optional[int]]:
|
|
476
|
+
"""Extract token usage from response.generations structure."""
|
|
477
|
+
if not generations:
|
|
478
|
+
return None, None, None
|
|
479
|
+
|
|
480
|
+
total_input = 0
|
|
481
|
+
total_output = 0
|
|
482
|
+
total_total = 0
|
|
483
|
+
|
|
484
|
+
for gen_list in generations:
|
|
485
|
+
for gen in gen_list:
|
|
486
|
+
token_usage = _get_token_usage(gen)
|
|
487
|
+
input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage)
|
|
488
|
+
total_input += input_tokens if input_tokens is not None else 0
|
|
489
|
+
total_output += output_tokens if output_tokens is not None else 0
|
|
490
|
+
total_total += total_tokens if total_tokens is not None else 0
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
total_input if total_input > 0 else None,
|
|
494
|
+
total_output if total_output > 0 else None,
|
|
495
|
+
total_total if total_total > 0 else None,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _get_token_usage(obj: Any) -> Optional[Dict[str, Any]]:
|
|
500
|
+
"""
|
|
501
|
+
Check multiple paths to extract token usage from different objects.
|
|
502
|
+
"""
|
|
503
|
+
possible_names = ("usage", "token_usage", "usage_metadata")
|
|
504
|
+
|
|
505
|
+
message = _get_value(obj, "message")
|
|
506
|
+
if message is not None:
|
|
507
|
+
for name in possible_names:
|
|
508
|
+
usage = _get_value(message, name)
|
|
509
|
+
if usage is not None:
|
|
510
|
+
return usage
|
|
511
|
+
|
|
512
|
+
llm_output = _get_value(obj, "llm_output")
|
|
513
|
+
if llm_output is not None:
|
|
514
|
+
for name in possible_names:
|
|
515
|
+
usage = _get_value(llm_output, name)
|
|
516
|
+
if usage is not None:
|
|
517
|
+
return usage
|
|
518
|
+
|
|
519
|
+
# check for usage in the object itself
|
|
520
|
+
for name in possible_names:
|
|
521
|
+
usage = _get_value(obj, name)
|
|
522
|
+
if usage is not None:
|
|
523
|
+
return usage
|
|
524
|
+
|
|
525
|
+
# no usage found anywhere
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _record_token_usage(span: Span, response: Any) -> None:
|
|
530
|
+
token_usage = _get_token_usage(response)
|
|
531
|
+
if token_usage:
|
|
532
|
+
input_tokens, output_tokens, total_tokens = _extract_tokens(token_usage)
|
|
533
|
+
else:
|
|
534
|
+
input_tokens, output_tokens, total_tokens = _extract_tokens_from_generations(
|
|
535
|
+
response.generations
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if input_tokens is not None:
|
|
539
|
+
span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens)
|
|
540
|
+
|
|
541
|
+
if output_tokens is not None:
|
|
542
|
+
span.set_data(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens)
|
|
543
|
+
|
|
544
|
+
if total_tokens is not None:
|
|
545
|
+
span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _get_request_data(
|
|
549
|
+
obj: Any, args: Any, kwargs: Any
|
|
550
|
+
) -> tuple[Optional[str], Optional[List[Any]]]:
|
|
551
|
+
"""
|
|
552
|
+
Get the agent name and available tools for the agent.
|
|
553
|
+
"""
|
|
554
|
+
agent = getattr(obj, "agent", None)
|
|
555
|
+
runnable = getattr(agent, "runnable", None)
|
|
556
|
+
runnable_config = getattr(runnable, "config", {})
|
|
557
|
+
tools = (
|
|
558
|
+
getattr(obj, "tools", None)
|
|
559
|
+
or getattr(agent, "tools", None)
|
|
560
|
+
or runnable_config.get("tools")
|
|
561
|
+
or runnable_config.get("available_tools")
|
|
562
|
+
)
|
|
563
|
+
tools = tools if tools and len(tools) > 0 else None
|
|
564
|
+
|
|
565
|
+
try:
|
|
566
|
+
agent_name = None
|
|
567
|
+
if len(args) > 1:
|
|
568
|
+
agent_name = args[1].get("run_name")
|
|
569
|
+
if agent_name is None:
|
|
570
|
+
agent_name = runnable_config.get("run_name")
|
|
571
|
+
except Exception:
|
|
572
|
+
pass
|
|
573
|
+
|
|
574
|
+
return (agent_name, tools)
|
|
575
|
+
|
|
576
|
+
|
|
482
577
|
def _wrap_configure(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
483
578
|
|
|
484
579
|
@wraps(f)
|
|
@@ -538,7 +633,6 @@ def _wrap_configure(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
538
633
|
sentry_handler = SentryLangchainCallback(
|
|
539
634
|
integration.max_spans,
|
|
540
635
|
integration.include_prompts,
|
|
541
|
-
integration.tiktoken_encoding_name,
|
|
542
636
|
)
|
|
543
637
|
if isinstance(local_callbacks, BaseCallbackManager):
|
|
544
638
|
local_callbacks = local_callbacks.copy()
|
|
@@ -560,3 +654,153 @@ def _wrap_configure(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
560
654
|
)
|
|
561
655
|
|
|
562
656
|
return new_configure
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _wrap_agent_executor_invoke(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
660
|
+
|
|
661
|
+
@wraps(f)
|
|
662
|
+
def new_invoke(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
663
|
+
integration = sentry_sdk.get_client().get_integration(LangchainIntegration)
|
|
664
|
+
if integration is None:
|
|
665
|
+
return f(self, *args, **kwargs)
|
|
666
|
+
|
|
667
|
+
agent_name, tools = _get_request_data(self, args, kwargs)
|
|
668
|
+
|
|
669
|
+
with sentry_sdk.start_span(
|
|
670
|
+
op=OP.GEN_AI_INVOKE_AGENT,
|
|
671
|
+
name=f"invoke_agent {agent_name}" if agent_name else "invoke_agent",
|
|
672
|
+
origin=LangchainIntegration.origin,
|
|
673
|
+
) as span:
|
|
674
|
+
if agent_name:
|
|
675
|
+
span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name)
|
|
676
|
+
|
|
677
|
+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent")
|
|
678
|
+
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False)
|
|
679
|
+
|
|
680
|
+
if tools:
|
|
681
|
+
set_data_normalized(
|
|
682
|
+
span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools, unpack=False
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Run the agent
|
|
686
|
+
result = f(self, *args, **kwargs)
|
|
687
|
+
|
|
688
|
+
input = result.get("input")
|
|
689
|
+
if (
|
|
690
|
+
input is not None
|
|
691
|
+
and should_send_default_pii()
|
|
692
|
+
and integration.include_prompts
|
|
693
|
+
):
|
|
694
|
+
set_data_normalized(
|
|
695
|
+
span,
|
|
696
|
+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
|
|
697
|
+
[
|
|
698
|
+
input,
|
|
699
|
+
],
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
output = result.get("output")
|
|
703
|
+
if (
|
|
704
|
+
output is not None
|
|
705
|
+
and should_send_default_pii()
|
|
706
|
+
and integration.include_prompts
|
|
707
|
+
):
|
|
708
|
+
span.set_data(SPANDATA.GEN_AI_RESPONSE_TEXT, output)
|
|
709
|
+
|
|
710
|
+
return result
|
|
711
|
+
|
|
712
|
+
return new_invoke
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _wrap_agent_executor_stream(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
716
|
+
|
|
717
|
+
@wraps(f)
|
|
718
|
+
def new_stream(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
719
|
+
integration = sentry_sdk.get_client().get_integration(LangchainIntegration)
|
|
720
|
+
if integration is None:
|
|
721
|
+
return f(self, *args, **kwargs)
|
|
722
|
+
|
|
723
|
+
agent_name, tools = _get_request_data(self, args, kwargs)
|
|
724
|
+
|
|
725
|
+
span = sentry_sdk.start_span(
|
|
726
|
+
op=OP.GEN_AI_INVOKE_AGENT,
|
|
727
|
+
name=f"invoke_agent {agent_name}".strip(),
|
|
728
|
+
origin=LangchainIntegration.origin,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
if agent_name:
|
|
732
|
+
span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name)
|
|
733
|
+
|
|
734
|
+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent")
|
|
735
|
+
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
|
|
736
|
+
|
|
737
|
+
if tools:
|
|
738
|
+
set_data_normalized(
|
|
739
|
+
span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, tools, unpack=False
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
input = args[0].get("input") if len(args) >= 1 else None
|
|
743
|
+
if (
|
|
744
|
+
input is not None
|
|
745
|
+
and should_send_default_pii()
|
|
746
|
+
and integration.include_prompts
|
|
747
|
+
):
|
|
748
|
+
set_data_normalized(
|
|
749
|
+
span,
|
|
750
|
+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
|
|
751
|
+
[
|
|
752
|
+
input,
|
|
753
|
+
],
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
# Run the agent
|
|
757
|
+
result = f(self, *args, **kwargs)
|
|
758
|
+
|
|
759
|
+
old_iterator = result
|
|
760
|
+
|
|
761
|
+
def new_iterator() -> Iterator[Any]:
|
|
762
|
+
for event in old_iterator:
|
|
763
|
+
yield event
|
|
764
|
+
|
|
765
|
+
try:
|
|
766
|
+
output = event.get("output")
|
|
767
|
+
except Exception:
|
|
768
|
+
output = None
|
|
769
|
+
|
|
770
|
+
if (
|
|
771
|
+
output is not None
|
|
772
|
+
and should_send_default_pii()
|
|
773
|
+
and integration.include_prompts
|
|
774
|
+
):
|
|
775
|
+
span.set_data(SPANDATA.GEN_AI_RESPONSE_TEXT, output)
|
|
776
|
+
|
|
777
|
+
span.set_status(SPANSTATUS.OK)
|
|
778
|
+
span.finish()
|
|
779
|
+
|
|
780
|
+
async def new_iterator_async() -> AsyncIterator[Any]:
|
|
781
|
+
async for event in old_iterator:
|
|
782
|
+
yield event
|
|
783
|
+
|
|
784
|
+
try:
|
|
785
|
+
output = event.get("output")
|
|
786
|
+
except Exception:
|
|
787
|
+
output = None
|
|
788
|
+
|
|
789
|
+
if (
|
|
790
|
+
output is not None
|
|
791
|
+
and should_send_default_pii()
|
|
792
|
+
and integration.include_prompts
|
|
793
|
+
):
|
|
794
|
+
span.set_data(SPANDATA.GEN_AI_RESPONSE_TEXT, output)
|
|
795
|
+
|
|
796
|
+
span.set_status(SPANSTATUS.OK)
|
|
797
|
+
span.finish()
|
|
798
|
+
|
|
799
|
+
if str(type(result)) == "<class 'async_generator'>":
|
|
800
|
+
result = new_iterator_async()
|
|
801
|
+
else:
|
|
802
|
+
result = new_iterator()
|
|
803
|
+
|
|
804
|
+
return result
|
|
805
|
+
|
|
806
|
+
return new_stream
|