diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index e883035c19..36690af587 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -44,7 +44,6 @@ dependencies = [ "aiosqlite~=0.21.0", "pyyaml~=6.0", "aiofiles~=24.1.0", - "lancedb>=0.29.2,<0.30.1", ] [project.urls] @@ -66,6 +65,9 @@ pandas = [ openpyxl = [ "openpyxl~=3.1.5", ] +memory = [ + "lancedb>=0.29.2,<0.30.1", +] mem0 = ["mem0ai~=0.1.94"] docling = [ "docling~=2.75.0", diff --git a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py index a7a2d3956d..9f7cf6afd8 100644 --- a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py +++ b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py @@ -12,7 +12,10 @@ import time from typing import Any -import lancedb # type: ignore[import-untyped] +try: + import lancedb # type: ignore[import-untyped] +except ImportError: + lancedb = None from crewai.memory.types import MemoryRecord, ScopeInfo from crewai.utilities.lock_store import lock as store_lock @@ -63,6 +66,12 @@ def __init__( fragment file; compaction merges them, keeping query performance consistent. Set to 0 to disable. """ + if lancedb is None: + raise ImportError( + "lancedb is required for LanceDB memory storage but is not installed.\n" + "Install it with: pip install 'crewai[memory]'\n" + "Or directly: pip install 'lancedb>=0.29.2,<0.30.1'" + ) if path is None: storage_dir = os.environ.get("CREWAI_STORAGE_DIR") if storage_dir: diff --git a/lib/crewai/tests/memory/test_lancedb_optional.py b/lib/crewai/tests/memory/test_lancedb_optional.py new file mode 100644 index 0000000000..92aba52b48 --- /dev/null +++ b/lib/crewai/tests/memory/test_lancedb_optional.py @@ -0,0 +1,140 @@ +"""Tests that lancedb is an optional dependency. + +These tests verify that: +1. The lancedb_storage module handles a missing lancedb gracefully. +2. Memory falls back with a clear error when lancedb is not installed. +3. Importing crewai itself does not require lancedb. +""" + +from __future__ import annotations + +import sys +from unittest.mock import MagicMock, patch + +import pytest + + +def test_lancedb_storage_raises_import_error_when_lancedb_missing(tmp_path): + """LanceDBStorage.__init__ raises ImportError with install instructions when lancedb is absent.""" + with patch.dict(sys.modules, {"lancedb": None}): + # Force reload so the module picks up the patched sys.modules + import importlib + + import crewai.memory.storage.lancedb_storage as mod + + importlib.reload(mod) + + with pytest.raises(ImportError, match="pip install 'crewai\\[memory\\]'"): + mod.LanceDBStorage(path=str(tmp_path / "mem")) + + # Restore the module to its original state + importlib.reload(mod) + + +def test_memory_default_storage_raises_when_lancedb_missing(tmp_path): + """Memory(storage='lancedb') raises ImportError when lancedb is not installed.""" + with patch.dict(sys.modules, {"lancedb": None}): + import importlib + + import crewai.memory.storage.lancedb_storage as mod + + importlib.reload(mod) + + try: + from crewai.memory.unified_memory import Memory + + with pytest.raises(ImportError, match="pip install 'crewai\\[memory\\]'"): + Memory( + storage="lancedb", + llm=MagicMock(), + embedder=MagicMock(), + ) + finally: + importlib.reload(mod) + + +def test_memory_with_path_string_raises_when_lancedb_missing(tmp_path): + """Memory(storage='/some/path') also uses LanceDBStorage and raises when lancedb is missing.""" + with patch.dict(sys.modules, {"lancedb": None}): + import importlib + + import crewai.memory.storage.lancedb_storage as mod + + importlib.reload(mod) + + try: + from crewai.memory.unified_memory import Memory + + with pytest.raises(ImportError, match="pip install 'crewai\\[memory\\]'"): + Memory( + storage=str(tmp_path / "custom_path"), + llm=MagicMock(), + embedder=MagicMock(), + ) + finally: + importlib.reload(mod) + + +def test_crewai_import_does_not_require_lancedb(): + """Importing crewai should work even if lancedb is not installed. + + The Memory class is lazily imported in crewai/__init__.py, so lancedb + should never be pulled in at import time. + """ + # This test verifies the lazy import mechanism by checking that the + # crewai module is importable and that Memory is listed in __all__ + # but not yet resolved in the module globals until accessed. + import crewai + + assert "Memory" in crewai.__all__ + # Memory should be accessible (lazy import triggers on access) + assert hasattr(crewai, "Memory") + + +def test_memory_with_custom_storage_backend_does_not_need_lancedb(tmp_path): + """When a custom StorageBackend is passed, lancedb is never needed.""" + with patch.dict(sys.modules, {"lancedb": None}): + import importlib + + import crewai.memory.storage.lancedb_storage as mod + + importlib.reload(mod) + + try: + from crewai.memory.unified_memory import Memory + + mock_storage = MagicMock() + # Should not raise, since we're providing a custom storage backend + mem = Memory( + storage=mock_storage, + llm=MagicMock(), + embedder=MagicMock(), + ) + assert mem._storage is mock_storage + finally: + importlib.reload(mod) + + +def test_lancedb_in_optional_dependencies(): + """Verify lancedb is listed under optional [memory] dependencies, not core.""" + import tomli + from pathlib import Path + + pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml" + with open(pyproject_path, "rb") as f: + data = tomli.load(f) + + core_deps = data["project"]["dependencies"] + optional_deps = data["project"]["optional-dependencies"] + + # lancedb should NOT be in core dependencies + assert not any("lancedb" in dep for dep in core_deps), ( + "lancedb should not be a core dependency" + ) + + # lancedb SHOULD be in optional [memory] dependencies + assert "memory" in optional_deps, "Missing [memory] optional dependency group" + memory_deps = optional_deps["memory"] + assert any("lancedb" in dep for dep in memory_deps), ( + "lancedb should be in the [memory] optional dependency group" + ) diff --git a/uv.lock b/uv.lock index 2f09221736..e415d568a4 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-04T15:11:41.651093Z" +exclude-newer = "2026-04-04T21:52:55.380648312Z" exclude-newer-span = "P3D" [manifest] @@ -1209,7 +1209,6 @@ dependencies = [ { name = "json-repair" }, { name = "json5" }, { name = "jsonref" }, - { name = "lancedb" }, { name = "mcp" }, { name = "openai" }, { name = "openpyxl" }, @@ -1269,6 +1268,9 @@ litellm = [ mem0 = [ { name = "mem0ai" }, ] +memory = [ + { name = "lancedb" }, +] openpyxl = [ { name = "openpyxl" }, ] @@ -1317,7 +1319,7 @@ requires-dist = [ { name = "json-repair", specifier = "~=0.25.2" }, { name = "json5", specifier = "~=0.10.0" }, { name = "jsonref", specifier = "~=1.1.0" }, - { name = "lancedb", specifier = ">=0.29.2,<0.30.1" }, + { name = "lancedb", marker = "extra == 'memory'", specifier = ">=0.29.2,<0.30.1" }, { name = "litellm", marker = "extra == 'litellm'", specifier = "~=1.83.0" }, { name = "mcp", specifier = "~=1.26.0" }, { name = "mem0ai", marker = "extra == 'mem0'", specifier = "~=0.1.94" }, @@ -1346,7 +1348,7 @@ requires-dist = [ { name = "uv", specifier = "~=0.9.13" }, { name = "voyageai", marker = "extra == 'voyageai'", specifier = "~=0.3.5" }, ] -provides-extras = ["a2a", "anthropic", "aws", "azure-ai-inference", "bedrock", "docling", "embeddings", "file-processing", "google-genai", "litellm", "mem0", "openpyxl", "pandas", "qdrant", "qdrant-edge", "tools", "voyageai", "watson"] +provides-extras = ["a2a", "anthropic", "aws", "azure-ai-inference", "bedrock", "docling", "embeddings", "file-processing", "google-genai", "litellm", "mem0", "memory", "openpyxl", "pandas", "qdrant", "qdrant-edge", "tools", "voyageai", "watson"] [[package]] name = "crewai-devtools"