Skip to content

Commit 80c772d

Browse files
Fix Langchain GEN_AI_AGENT_NAME regression by adding contextvar propagation
- Add CURRENT_LANGCHAIN_AGENT_NAME contextvar to track agent name across spans - Set agent name in agent executor wrappers (invoke/stream) - Propagate agent name to all child spans via _create_span - Add test to verify agent name is set on all spans
1 parent bd0b816 commit 80c772d

2 files changed

Lines changed: 153 additions & 43 deletions

File tree

sentry_sdk/integrations/langchain.py

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323
from sentry_sdk.tracing_utils import _get_value, set_span_errored
2424
from sentry_sdk.utils import capture_internal_exceptions, logger
2525

26+
CURRENT_LANGCHAIN_AGENT_NAME = contextvars.ContextVar("CURRENT_LANGCHAIN_AGENT_NAME", default=None)
27+
28+
29+
def _get_current_langchain_agent_name() -> "Optional[str]":
30+
return CURRENT_LANGCHAIN_AGENT_NAME.get(None)
31+
32+
2633
if TYPE_CHECKING:
2734
from typing import (
2835
Any,
@@ -290,6 +297,11 @@ def _create_span(
290297
watched_span = WatchedSpan(sentry_sdk.start_span(**kwargs))
291298

292299
watched_span.span.__enter__()
300+
301+
agent_name = _get_current_langchain_agent_name()
302+
if agent_name:
303+
watched_span.span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name)
304+
293305
self.span_map[run_id] = watched_span
294306
self.gc_span_map()
295307
return watched_span
@@ -933,53 +945,60 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
933945
return f(self, *args, **kwargs)
934946

935947
agent_name, tools = _get_request_data(self, args, kwargs)
948+
token = CURRENT_LANGCHAIN_AGENT_NAME.set(agent_name)
936949
start_span_function = get_start_span_function()
937950

938-
with start_span_function(
939-
op=OP.GEN_AI_INVOKE_AGENT,
940-
name=f"invoke_agent {agent_name}" if agent_name else "invoke_agent",
941-
origin=LangchainIntegration.origin,
942-
) as span:
943-
try:
944-
945-
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent")
946-
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False)
947-
948-
_set_tools_on_span(span, tools)
949-
950-
# Run the agent
951-
result = f(self, *args, **kwargs)
952-
953-
input = result.get("input")
954-
if (
955-
input is not None
956-
and should_send_default_pii()
957-
and integration.include_prompts
958-
):
959-
normalized_messages = normalize_message_roles([input])
960-
scope = sentry_sdk.get_current_scope()
961-
messages_data = truncate_and_annotate_messages(
962-
normalized_messages, span, scope
963-
)
964-
if messages_data is not None:
965-
set_data_normalized(
966-
span,
967-
SPANDATA.GEN_AI_REQUEST_MESSAGES,
968-
messages_data,
969-
unpack=False,
951+
try:
952+
with start_span_function(
953+
op=OP.GEN_AI_INVOKE_AGENT,
954+
name=f"invoke_agent {agent_name}" if agent_name else "invoke_agent",
955+
origin=LangchainIntegration.origin,
956+
) as span:
957+
try:
958+
if agent_name:
959+
span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name)
960+
961+
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent")
962+
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, False)
963+
964+
_set_tools_on_span(span, tools)
965+
966+
# Run the agent
967+
result = f(self, *args, **kwargs)
968+
969+
input = result.get("input")
970+
if (
971+
input is not None
972+
and should_send_default_pii()
973+
and integration.include_prompts
974+
):
975+
normalized_messages = normalize_message_roles([input])
976+
scope = sentry_sdk.get_current_scope()
977+
messages_data = truncate_and_annotate_messages(
978+
normalized_messages, span, scope
970979
)
980+
if messages_data is not None:
981+
set_data_normalized(
982+
span,
983+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
984+
messages_data,
985+
unpack=False,
986+
)
971987

972-
output = result.get("output")
973-
if (
974-
output is not None
975-
and should_send_default_pii()
976-
and integration.include_prompts
977-
):
978-
set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output)
979-
980-
return result
981-
finally:
982-
# Ensure agent is popped even if an exception occurs
988+
output = result.get("output")
989+
if (
990+
output is not None
991+
and should_send_default_pii()
992+
and integration.include_prompts
993+
):
994+
set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output)
995+
996+
return result
997+
finally:
998+
# Ensure agent is popped even if an exception occurs
999+
pass
1000+
finally:
1001+
CURRENT_LANGCHAIN_AGENT_NAME.reset(token)
9831002

9841003
return new_invoke
9851004

@@ -992,6 +1011,7 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
9921011
return f(self, *args, **kwargs)
9931012

9941013
agent_name, tools = _get_request_data(self, args, kwargs)
1014+
token = CURRENT_LANGCHAIN_AGENT_NAME.set(agent_name)
9951015
start_span_function = get_start_span_function()
9961016

9971017
span = start_span_function(
@@ -1001,6 +1021,8 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
10011021
)
10021022
span.__enter__()
10031023

1024+
if agent_name:
1025+
span.set_data(SPANDATA.GEN_AI_AGENT_NAME, agent_name)
10041026

10051027
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent")
10061028
span.set_data(SPANDATA.GEN_AI_RESPONSE_STREAMING, True)
@@ -1055,6 +1077,7 @@ def new_iterator() -> "Iterator[Any]":
10551077
finally:
10561078
# Ensure cleanup happens even if iterator is abandoned or fails
10571079
span.__exit__(*exc_info)
1080+
CURRENT_LANGCHAIN_AGENT_NAME.reset(token)
10581081

10591082
async def new_iterator_async() -> "AsyncIterator[Any]":
10601083
exc_info: "tuple[Any, Any, Any]" = (None, None, None)
@@ -1080,6 +1103,7 @@ async def new_iterator_async() -> "AsyncIterator[Any]":
10801103
finally:
10811104
# Ensure cleanup happens even if iterator is abandoned or fails
10821105
span.__exit__(*exc_info)
1106+
CURRENT_LANGCHAIN_AGENT_NAME.reset(token)
10831107

10841108
if str(type(result)) == "<class 'async_generator'>":
10851109
result = new_iterator_async()

tests/integrations/langchain/test_langchain.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,92 @@ def test_langchain_agent(
259259
},
260260
] == json.loads(chat_spans[0]["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS])
261261

262+
263+
def test_langchain_agent_name_propagation(sentry_init, capture_events, monkeypatch):
264+
monkeypatch.setattr(
265+
"sentry_sdk.integrations.langchain._get_request_data",
266+
lambda obj, args, kwargs: ("test_agent_name", None),
267+
)
268+
269+
sentry_init(
270+
integrations=[LangchainIntegration(include_prompts=True)],
271+
traces_sample_rate=1.0,
272+
send_default_pii=True,
273+
)
274+
events = capture_events()
275+
276+
global llm_type, stream_result_mock
277+
llm_type = "openai-chat"
278+
stream_result_mock = Mock(
279+
side_effect=[
280+
[
281+
ChatGenerationChunk(
282+
type="ChatGenerationChunk",
283+
message=AIMessageChunk(
284+
content="",
285+
additional_kwargs={
286+
"tool_calls": [
287+
{
288+
"index": 0,
289+
"id": "call_BbeyNhCKa6kYLYzrD40NGm3b",
290+
"function": {"arguments": "", "name": "get_word_length"},
291+
"type": "function",
292+
}
293+
]
294+
},
295+
),
296+
),
297+
ChatGenerationChunk(
298+
type="ChatGenerationChunk",
299+
message=AIMessageChunk(
300+
content="5",
301+
usage_metadata={
302+
"input_tokens": 142,
303+
"output_tokens": 50,
304+
"total_tokens": 192,
305+
"input_token_details": {"audio": 0, "cache_read": 0},
306+
"output_token_details": {"audio": 0, "reasoning": 0},
307+
},
308+
),
309+
generation_info={"finish_reason": "function_call"},
310+
),
311+
],
312+
[
313+
ChatGenerationChunk(
314+
text="The word eudca has 5 letters.",
315+
type="ChatGenerationChunk",
316+
message=AIMessageChunk(
317+
content="The word eudca has 5 letters.",
318+
usage_metadata={
319+
"input_tokens": 89,
320+
"output_tokens": 28,
321+
"total_tokens": 117,
322+
"input_token_details": {"audio": 0, "cache_read": 0},
323+
"output_token_details": {"audio": 0, "reasoning": 0},
324+
},
325+
),
326+
),
327+
ChatGenerationChunk(
328+
type="ChatGenerationChunk",
329+
generation_info={"finish_reason": "stop"},
330+
message=AIMessageChunk(content=""),
331+
),
332+
],
333+
]
334+
)
335+
336+
llm = MockOpenAI(model_name="gpt-3.5-turbo", temperature=0, openai_api_key="badkey")
337+
agent = create_openai_tools_agent(llm, [get_word_length], prompt=None)
338+
agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True)
339+
340+
with start_transaction():
341+
list(agent_executor.stream({"input": "How many letters in the word eudca"}))
342+
343+
tx = events[0]
344+
assert tx["type"] == "transaction"
345+
for span in tx["spans"]:
346+
assert span["data"].get(SPANDATA.GEN_AI_AGENT_NAME) == "test_agent_name"
347+
262348
assert "5" in chat_spans[1]["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
263349

264350
# Verify tool calls are recorded when PII is enabled

0 commit comments

Comments
 (0)