Compare commits

...

3 Commits

Author SHA1 Message Date
Devin AI
da4b610d55 fix: correct type: ignore comments for mypy - keep import-untyped, remove unused assignment ignore
Co-Authored-By: João <joao@crewai.com>
2026-04-07 22:06:21 +00:00
Devin AI
baac6668ab fix: remove unused type: ignore comment to satisfy mypy
Co-Authored-By: João <joao@crewai.com>
2026-04-07 22:02:21 +00:00
Devin AI
7bec692878 fix: make lancedb an optional dependency under [memory] extras
Moves lancedb from core dependencies to optional [memory] extras group
in pyproject.toml. This unblocks installation on Intel Macs where
lancedb>=0.29.2 has no macOS x86_64 wheels.

- Add try/except around lancedb import in lancedb_storage.py
- Raise clear ImportError with install instructions when lancedb is absent
- Add 6 tests covering optional dependency behavior
- Existing memory tests continue to pass

Closes #5327

Co-Authored-By: João <joao@crewai.com>
2026-04-07 21:55:36 +00:00
4 changed files with 159 additions and 6 deletions

View File

@@ -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",

View File

@@ -12,7 +12,10 @@ 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
from crewai.memory.types import MemoryRecord, ScopeInfo
from crewai.utilities.lock_store import lock as store_lock
@@ -63,6 +66,12 @@ class LanceDBStorage:
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:

View File

@@ -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"
)

10
uv.lock generated
View File

@@ -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"