Skip to content

Commit 5b7771c

Browse files
committed
fix(langchain): capture exceptions within calls to the LLM
1 parent f99a17b commit 5b7771c

File tree

2 files changed

+300
-3
lines changed

2 files changed

+300
-3
lines changed

sentry_sdk/integrations/langchain.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,12 @@ def new_invoke(self, *args, **kwargs):
749749
_set_tools_on_span(span, tools)
750750

751751
# Run the agent
752-
result = f(self, *args, **kwargs)
752+
try:
753+
result = f(self, *args, **kwargs)
754+
except Exception as e:
755+
run_id = kwargs.get("run_id")
756+
self._handle_error(run_id, e)
757+
raise e
753758

754759
input = result.get("input")
755760
if (
@@ -820,8 +825,12 @@ def new_stream(self, *args, **kwargs):
820825
unpack=False,
821826
)
822827

823-
# Run the agent
824-
result = f(self, *args, **kwargs)
828+
try:
829+
result = f(self, *args, **kwargs)
830+
except Exception as e:
831+
run_id = kwargs.get("run_id")
832+
self._handle_error(run_id, e)
833+
raise e
825834

826835
old_iterator = result
827836

tests/integrations/langchain/test_langchain.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,3 +958,291 @@ def test_langchain_message_role_normalization_units():
958958
assert normalized[3]["role"] == "system" # system unchanged
959959
assert "role" not in normalized[4] # Message without role unchanged
960960
assert normalized[5] == "string message" # String message unchanged
961+
962+
963+
def test_langchain_llm_exception_captured(sentry_init, capture_events):
964+
"""Test that exceptions during LLM execution are properly captured with full context."""
965+
global llm_type
966+
llm_type = "openai-chat"
967+
968+
sentry_init(
969+
integrations=[LangchainIntegration(include_prompts=True)],
970+
traces_sample_rate=1.0,
971+
send_default_pii=True,
972+
)
973+
events = capture_events()
974+
975+
prompt = ChatPromptTemplate.from_messages(
976+
[
977+
("system", "You are a helpful assistant"),
978+
("user", "{input}"),
979+
MessagesPlaceholder(variable_name="agent_scratchpad"),
980+
]
981+
)
982+
983+
global stream_result_mock
984+
stream_result_mock = Mock(side_effect=RuntimeError("LLM service unavailable"))
985+
986+
llm = MockOpenAI(
987+
model_name="gpt-3.5-turbo",
988+
temperature=0,
989+
openai_api_key="badkey",
990+
)
991+
agent = create_openai_tools_agent(llm, [get_word_length], prompt)
992+
agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True)
993+
994+
with start_transaction(name="test_llm_exception"):
995+
with pytest.raises(RuntimeError):
996+
list(agent_executor.stream({"input": "Test input"}))
997+
998+
(error_event, transaction_event) = events
999+
1000+
assert error_event["level"] == "error"
1001+
assert "exception" in error_event
1002+
assert len(error_event["exception"]["values"]) > 0
1003+
1004+
exception = error_event["exception"]["values"][0]
1005+
assert exception["type"] == "RuntimeError"
1006+
assert exception["value"] == "LLM service unavailable"
1007+
assert "stacktrace" in exception
1008+
1009+
assert transaction_event["type"] == "transaction"
1010+
assert transaction_event["transaction"] == "test_llm_exception"
1011+
assert transaction_event["contexts"]["trace"]["status"] == "error"
1012+
1013+
1014+
def test_langchain_different_exception_types(sentry_init, capture_events):
1015+
"""Test that different exception types are properly captured."""
1016+
global llm_type
1017+
llm_type = "openai-chat"
1018+
1019+
exception_types = [
1020+
(ValueError, "Invalid parameter"),
1021+
(TypeError, "Type mismatch"),
1022+
(RuntimeError, "Runtime error occurred"),
1023+
(Exception, "Generic exception"),
1024+
]
1025+
1026+
for exception_class, exception_message in exception_types:
1027+
sentry_init(
1028+
integrations=[LangchainIntegration(include_prompts=False)],
1029+
traces_sample_rate=1.0,
1030+
)
1031+
events = capture_events()
1032+
1033+
prompt = ChatPromptTemplate.from_messages(
1034+
[
1035+
("system", "You are a helpful assistant"),
1036+
("user", "{input}"),
1037+
MessagesPlaceholder(variable_name="agent_scratchpad"),
1038+
]
1039+
)
1040+
1041+
global stream_result_mock
1042+
stream_result_mock = Mock(side_effect=exception_class(exception_message))
1043+
1044+
llm = MockOpenAI(
1045+
model_name="gpt-3.5-turbo",
1046+
temperature=0,
1047+
openai_api_key="badkey",
1048+
)
1049+
agent = create_openai_tools_agent(llm, [get_word_length], prompt)
1050+
agent_executor = AgentExecutor(
1051+
agent=agent, tools=[get_word_length], verbose=True
1052+
)
1053+
1054+
with start_transaction():
1055+
with pytest.raises(exception_class):
1056+
list(agent_executor.stream({"input": "Test"}))
1057+
1058+
assert len(events) >= 1
1059+
error_event = events[0]
1060+
assert error_event["level"] == "error"
1061+
1062+
exception = error_event["exception"]["values"][0]
1063+
assert exception["type"] == exception_class.__name__
1064+
assert exception["value"] == exception_message
1065+
1066+
1067+
def test_langchain_exception_with_span_context(sentry_init, capture_events):
1068+
"""Test that exception events include proper span context."""
1069+
global llm_type
1070+
llm_type = "openai-chat"
1071+
1072+
sentry_init(
1073+
integrations=[LangchainIntegration(include_prompts=True)],
1074+
traces_sample_rate=1.0,
1075+
send_default_pii=True,
1076+
)
1077+
events = capture_events()
1078+
1079+
prompt = ChatPromptTemplate.from_messages(
1080+
[
1081+
("system", "You are a helpful assistant"),
1082+
("user", "{input}"),
1083+
MessagesPlaceholder(variable_name="agent_scratchpad"),
1084+
]
1085+
)
1086+
1087+
global stream_result_mock
1088+
stream_result_mock = Mock(side_effect=ValueError("Model error"))
1089+
1090+
llm = MockOpenAI(
1091+
model_name="gpt-4",
1092+
temperature=0.7,
1093+
openai_api_key="badkey",
1094+
)
1095+
agent = create_openai_tools_agent(llm, [get_word_length], prompt)
1096+
agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True)
1097+
1098+
with start_transaction(name="llm_with_error"):
1099+
with pytest.raises(ValueError):
1100+
list(agent_executor.stream({"input": "Cause an error"}))
1101+
1102+
error_event, transaction_event = events
1103+
1104+
assert "contexts" in error_event
1105+
assert "trace" in error_event["contexts"]
1106+
1107+
error_trace_id = error_event["contexts"]["trace"].get("trace_id")
1108+
transaction_trace_id = transaction_event["contexts"]["trace"]["trace_id"]
1109+
1110+
assert error_trace_id == transaction_trace_id
1111+
1112+
gen_ai_spans = [
1113+
span
1114+
for span in transaction_event.get("spans", [])
1115+
if span.get("op", "").startswith("gen_ai")
1116+
]
1117+
assert len(gen_ai_spans) > 0
1118+
1119+
for span in gen_ai_spans:
1120+
if span.get("tags", {}).get("status") == "error":
1121+
assert "span_id" in span
1122+
1123+
1124+
def test_langchain_tool_execution_error(sentry_init, capture_events):
1125+
"""Test that exceptions during tool execution are properly captured."""
1126+
global llm_type
1127+
llm_type = "openai-chat"
1128+
1129+
sentry_init(
1130+
integrations=[LangchainIntegration(include_prompts=True)],
1131+
traces_sample_rate=1.0,
1132+
send_default_pii=True,
1133+
)
1134+
events = capture_events()
1135+
1136+
@tool
1137+
def failing_tool(word: str) -> int:
1138+
"""A tool that always fails."""
1139+
raise RuntimeError("Tool execution failed")
1140+
1141+
prompt = ChatPromptTemplate.from_messages(
1142+
[
1143+
("system", "You are a helpful assistant"),
1144+
("user", "{input}"),
1145+
MessagesPlaceholder(variable_name="agent_scratchpad"),
1146+
]
1147+
)
1148+
1149+
global stream_result_mock
1150+
stream_result_mock = Mock(
1151+
side_effect=[
1152+
[
1153+
ChatGenerationChunk(
1154+
type="ChatGenerationChunk",
1155+
message=AIMessageChunk(
1156+
content="",
1157+
additional_kwargs={
1158+
"tool_calls": [
1159+
{
1160+
"index": 0,
1161+
"id": "call_test",
1162+
"function": {
1163+
"arguments": '{"word": "test"}',
1164+
"name": "failing_tool",
1165+
},
1166+
"type": "function",
1167+
}
1168+
]
1169+
},
1170+
),
1171+
),
1172+
]
1173+
]
1174+
)
1175+
1176+
llm = MockOpenAI(
1177+
model_name="gpt-3.5-turbo",
1178+
temperature=0,
1179+
openai_api_key="badkey",
1180+
)
1181+
agent = create_openai_tools_agent(llm, [failing_tool], prompt)
1182+
agent_executor = AgentExecutor(agent=agent, tools=[failing_tool], verbose=True)
1183+
1184+
with start_transaction():
1185+
with pytest.raises(RuntimeError):
1186+
list(agent_executor.stream({"input": "Use the failing tool"}))
1187+
1188+
assert len(events) >= 1
1189+
1190+
error_events = [e for e in events if e.get("level") == "error"]
1191+
assert len(error_events) > 0
1192+
1193+
error_event = error_events[0]
1194+
exception = error_event["exception"]["values"][0]
1195+
assert exception["type"] == "RuntimeError"
1196+
assert "Tool execution failed" in exception["value"]
1197+
1198+
1199+
def test_langchain_exception_span_cleanup(sentry_init, capture_events):
1200+
"""Test that spans are properly cleaned up even when exceptions occur."""
1201+
global llm_type
1202+
llm_type = "openai-chat"
1203+
1204+
sentry_init(
1205+
integrations=[LangchainIntegration(include_prompts=True)],
1206+
traces_sample_rate=1.0,
1207+
)
1208+
events = capture_events()
1209+
1210+
prompt = ChatPromptTemplate.from_messages(
1211+
[
1212+
("system", "You are a helpful assistant"),
1213+
("user", "{input}"),
1214+
MessagesPlaceholder(variable_name="agent_scratchpad"),
1215+
]
1216+
)
1217+
1218+
global stream_result_mock
1219+
stream_result_mock = Mock(side_effect=ValueError("Test error"))
1220+
1221+
llm = MockOpenAI(
1222+
model_name="gpt-3.5-turbo",
1223+
temperature=0,
1224+
openai_api_key="badkey",
1225+
)
1226+
agent = create_openai_tools_agent(llm, [get_word_length], prompt)
1227+
agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True)
1228+
1229+
with start_transaction():
1230+
with pytest.raises(ValueError):
1231+
list(agent_executor.stream({"input": "Test"}))
1232+
1233+
transaction_event = next(
1234+
(e for e in events if e.get("type") == "transaction"), None
1235+
)
1236+
assert transaction_event is not None
1237+
1238+
errored_spans = [
1239+
span
1240+
for span in transaction_event.get("spans", [])
1241+
if span.get("tags", {}).get("status") == "error"
1242+
]
1243+
1244+
assert len(errored_spans) > 0
1245+
1246+
for span in errored_spans:
1247+
assert "timestamp" in span
1248+
assert span["timestamp"] > span.get("start_timestamp", 0)

0 commit comments

Comments
 (0)