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.

@@ -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, record_token_usage
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.integrations import DidNotEnable, Integration
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 Any, List, Callable, Dict, Union, Optional
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 langchain_core.messages import BaseMessage
23
- from langchain_core.outputs import LLMResult
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.agents import AgentAction, AgentFinish
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
- "temperature": SPANDATA.AI_TEMPERATURE,
37
- "top_p": SPANDATA.AI_TOP_P,
38
- "top_k": SPANDATA.AI_TOP_K,
39
- "function_call": SPANDATA.AI_FUNCTION_CALL,
40
- "tool_calls": SPANDATA.AI_TOOL_CALLS,
41
- "tools": SPANDATA.AI_TOOLS,
42
- "response_format": SPANDATA.AI_RESPONSE_FORMAT,
43
- "logit_bias": SPANDATA.AI_LOGIT_BIAS,
44
- "tags": SPANDATA.AI_TAGS,
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
- num_completion_tokens: int = 0
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
- """Base callback handler that can be used to handle callbacks from langchain."""
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
- if not run_id or run_id not in self.span_map:
122
- return
123
-
124
- span_data = self.span_map[run_id]
125
- if not span_data:
126
- return
127
- sentry_sdk.capture_exception(error)
128
- span_data.span.set_status(SPANSTATUS.INTERNAL_ERROR)
129
- span_data.span.finish()
130
- del self.span_map[run_id]
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 = {"content": message.content, "role": message.type}
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
- kwargs.get("parent_run_id"),
200
- op=OP.LANGCHAIN_RUN,
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.AI_INPUT_MESSAGES, prompts)
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.LANGCHAIN_CHAT_COMPLETIONS_CREATE,
229
- name=kwargs.get("name") or "Langchain Chat Model",
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
- if not model and "anthropic" in all_params.get("_type"):
241
- model = "claude-2"
240
+ span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
242
241
  if model:
243
- span.set_attribute(SPANDATA.AI_MODEL_ID, model)
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.AI_INPUT_MESSAGES,
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 on_llm_end(
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 LLM ends running."""
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
- if not span_data:
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
- span_data.span,
298
- SPANDATA.AI_RESPONSES,
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
- if not span_data.no_collect_tokens:
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 on_llm_error(
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
- outputs: Dict[str, Any],
291
+ response: LLMResult,
360
292
  *,
361
293
  run_id: UUID,
362
294
  **kwargs: Any,
363
295
  ) -> Any:
364
- """Run when chain ends running."""
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
- if not span_data:
371
- return
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 on_chain_error(
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 chain errors."""
355
+ """Run when LLM errors."""
382
356
  self._handle_error(run_id, error)
383
357
 
384
- def on_agent_action(
358
+ def on_chat_model_error(
385
359
  self: SentryLangchainCallback,
386
- action: AgentAction,
360
+ error: Union[Exception, KeyboardInterrupt],
387
361
  *,
388
362
  run_id: UUID,
389
363
  **kwargs: Any,
390
364
  ) -> Any:
391
- with capture_internal_exceptions():
392
- if not run_id:
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
- if not span_data:
419
- return
380
+ span = span_data.span
381
+
420
382
  if should_send_default_pii() and self.include_prompts:
421
383
  set_data_normalized(
422
- span_data.span, SPANDATA.AI_RESPONSES, finish.return_values.items()
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.LANGCHAIN_TOOL,
442
- name=serialized.get("name") or kwargs.get("name") or "AI tool usage",
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
- watched_span.span,
448
- SPANDATA.AI_INPUT_MESSAGES,
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
- if not span_data:
466
- return
438
+ span = span_data.span
439
+
467
440
  if should_send_default_pii() and self.include_prompts:
468
- set_data_normalized(span_data.span, SPANDATA.AI_RESPONSES, output)
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