From 0fbebf2d45b5c29a617639046be9982fcebd7d1d Mon Sep 17 00:00:00 2001 From: Mikhail Shavliuk <6589665+mshavliuk@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:44:44 +0300 Subject: [PATCH] fix(integrations): make `langchain` package optional for `LangchainIntegration` --- sentry_sdk/integrations/langchain.py | 6 ++-- .../integrations/langchain/test_langchain.py | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index a53115a2a9..5a61c3bf68 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -29,7 +29,6 @@ try: - from langchain.agents import AgentExecutor from langchain_core.agents import AgentFinish from langchain_core.callbacks import ( BaseCallbackHandler, @@ -72,10 +71,13 @@ def __init__(self, include_prompts=True, max_spans=1024): def setup_once(): # type: () -> None manager._configure = _wrap_configure(manager._configure) + try: + from langchain.agents import AgentExecutor - if AgentExecutor is not None: AgentExecutor.invoke = _wrap_agent_executor_invoke(AgentExecutor.invoke) AgentExecutor.stream = _wrap_agent_executor_stream(AgentExecutor.stream) + except ImportError: + pass class WatchedSpan: diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index 99dc5f4e37..df8822a0de 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -1,3 +1,6 @@ +import importlib +import re +import sys from typing import List, Optional, Any, Iterator from unittest import mock from unittest.mock import Mock @@ -382,6 +385,34 @@ def test_span_origin(sentry_init, capture_events): assert span["origin"] == "auto.ai.langchain" +def test_langchain_module_is_optional(monkeypatch): + """ + Test that the LangchainIntegration can be imported and set up + even if the langchain module is not installed. + """ + + lc_re = re.compile(r"^langchain(\..*)?$") + + stripped_modules = { + name: mod for name, mod in sys.modules.items() if not lc_re.match(name) + } + + class HideModuleImportHook(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path, target=..., /): + if lc_re.match(fullname): + raise ModuleNotFoundError(f"No module named '{fullname}'") + return None + + with ( + mock.patch.dict(sys.modules, stripped_modules, clear=True), + mock.patch.object(sys, "meta_path", [HideModuleImportHook(), *sys.meta_path]), + mock.patch("sentry_sdk.integrations.langchain.manager"), + ): + from sentry_sdk.integrations.langchain import LangchainIntegration + + LangchainIntegration().setup_once() + + def test_manual_callback_no_duplication(sentry_init): """ Test that when a user manually provides a SentryLangchainCallback,