diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 8fc69adf6..85691c278 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -43,7 +43,6 @@ dependencies = [ "uv~=0.9.13", "aiosqlite~=0.21.0", "pyyaml~=6.0", - "lancedb>=0.29.2", ] [project.urls] @@ -103,6 +102,9 @@ a2a = [ "httpx-sse~=0.4.0", "aiocache[redis,memcached]~=0.12.3", ] +memory-storage = [ + "lancedb>=0.29.2", +] file-processing = [ "crewai-files", ] diff --git a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py index a7a2d3956..08e9fa7b6 100644 --- a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py +++ b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py @@ -12,12 +12,28 @@ import threading import time from typing import Any -import lancedb # type: ignore[import-untyped] + +try: + import lancedb # type: ignore[import-untyped] +except ImportError: + lancedb = None # type: ignore[assignment] from crewai.memory.types import MemoryRecord, ScopeInfo from crewai.utilities.lock_store import lock as store_lock +_INSTALL_HINT = ( + "lancedb is required for the default memory storage backend but is not installed.\n\n" + "Install it with:\n" + " pip install crewai[memory-storage] # or: pip install lancedb\n\n" + "If lancedb does not provide wheels for your platform (e.g. Windows with " + "lancedb >=0.30), pin an older version:\n" + " pip install 'lancedb>=0.29.2,<0.30'\n\n" + "Alternatively, supply a custom StorageBackend to Memory(storage=...) to " + "bypass lancedb entirely." +) + + _logger = logging.getLogger(__name__) # Default embedding vector dimensionality (matches OpenAI text-embedding-3-small). @@ -63,6 +79,8 @@ class LanceDBStorage: fragment file; compaction merges them, keeping query performance consistent. Set to 0 to disable. """ + if lancedb is None: + raise ImportError(_INSTALL_HINT) if path is None: storage_dir = os.environ.get("CREWAI_STORAGE_DIR") if storage_dir: diff --git a/lib/crewai/src/crewai/memory/unified_memory.py b/lib/crewai/src/crewai/memory/unified_memory.py index 488e3c94a..e266d390a 100644 --- a/lib/crewai/src/crewai/memory/unified_memory.py +++ b/lib/crewai/src/crewai/memory/unified_memory.py @@ -173,7 +173,20 @@ class Memory(BaseModel): ) if isinstance(self.storage, str): - from crewai.memory.storage.lancedb_storage import LanceDBStorage + try: + from crewai.memory.storage.lancedb_storage import LanceDBStorage + except ImportError as exc: + raise ImportError( + "lancedb is required for the default memory storage backend " + "but is not installed.\n\n" + "Install it with:\n" + " pip install crewai[memory-storage] # or: pip install lancedb\n\n" + "If lancedb does not provide wheels for your platform " + "(e.g. Windows with lancedb >=0.30), pin an older version:\n" + " pip install 'lancedb>=0.29.2,<0.30'\n\n" + "Alternatively, supply a custom StorageBackend instance to " + "Memory(storage=my_backend) to bypass lancedb entirely." + ) from exc self._storage = ( LanceDBStorage() diff --git a/lib/crewai/tests/memory/test_unified_memory.py b/lib/crewai/tests/memory/test_unified_memory.py index 98a041086..00605cfc0 100644 --- a/lib/crewai/tests/memory/test_unified_memory.py +++ b/lib/crewai/tests/memory/test_unified_memory.py @@ -999,3 +999,62 @@ def test_close_drains_and_shuts_down(tmp_path: Path, mock_embedder: MagicMock) - mem.close() # After close, records should be persisted assert mem._storage.count() == 1 + + +# --- lancedb optional dependency --- + + +def test_lancedb_storage_import_error_when_missing(monkeypatch: pytest.MonkeyPatch) -> None: + """LanceDBStorage.__init__ raises ImportError with install hint when lancedb is absent.""" + import crewai.memory.storage.lancedb_storage as mod + + # Simulate lancedb not being installed + monkeypatch.setattr(mod, "lancedb", None) + with pytest.raises(ImportError, match="pip install crewai\\[memory-storage\\]"): + mod.LanceDBStorage(path="/tmp/fake") + + +def test_memory_raises_import_error_when_lancedb_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Memory(storage='lancedb') raises ImportError with install hint when lancedb is absent.""" + import importlib + + # Make the import of lancedb_storage raise ImportError + original_import = importlib.import_module + + def _patched_import(name: str, *args, **kwargs): # type: ignore[no-untyped-def] + if name == "crewai.memory.storage.lancedb_storage": + raise ImportError("No module named 'lancedb'") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(importlib, "import_module", _patched_import) + + # Also patch the direct import that model_post_init uses + import builtins + + original_builtins_import = builtins.__import__ + + def _patched_builtins_import(name, *args, **kwargs): # type: ignore[no-untyped-def] + if "lancedb_storage" in name: + raise ImportError("No module named 'lancedb'") + return original_builtins_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", _patched_builtins_import) + + from crewai.memory.unified_memory import Memory + + with pytest.raises(ImportError, match="pip install crewai\\[memory-storage\\]"): + Memory(storage="lancedb", llm=MagicMock(), embedder=MagicMock()) + + +def test_memory_works_with_custom_storage_when_lancedb_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Memory accepts a custom StorageBackend even when lancedb is not installed.""" + from crewai.memory.unified_memory import Memory + + mock_storage = MagicMock() + # Should not raise even if lancedb is missing — custom storage bypasses it + mem = Memory(storage=mock_storage, llm=MagicMock(), embedder=MagicMock()) + assert mem._storage is mock_storage diff --git a/uv.lock b/uv.lock index 265e72d7c..d4a815797 100644 --- a/uv.lock +++ b/uv.lock @@ -1136,7 +1136,6 @@ dependencies = [ { name = "json-repair" }, { name = "json5" }, { name = "jsonref" }, - { name = "lancedb" }, { name = "mcp" }, { name = "openai" }, { name = "openpyxl" }, @@ -1196,6 +1195,9 @@ litellm = [ mem0 = [ { name = "mem0ai" }, ] +memory-storage = [ + { name = "lancedb" }, +] openpyxl = [ { name = "openpyxl" }, ] @@ -1240,8 +1242,8 @@ 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" }, - { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.74.9,<3" }, + { name = "lancedb", marker = "extra == 'memory-storage'", specifier = ">=0.29.2" }, + { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.74.9,<=1.82.6" }, { name = "mcp", specifier = "~=1.26.0" }, { name = "mem0ai", marker = "extra == 'mem0'", specifier = "~=0.1.94" }, { name = "openai", specifier = ">=1.83.0,<3" }, @@ -1268,7 +1270,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", "tools", "voyageai", "watson"] +provides-extras = ["a2a", "anthropic", "aws", "azure-ai-inference", "bedrock", "docling", "embeddings", "file-processing", "google-genai", "litellm", "mem0", "memory-storage", "openpyxl", "pandas", "qdrant", "tools", "voyageai", "watson"] [[package]] name = "crewai-devtools"