diff --git a/lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py b/lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py index 3c9615946f..337a7c9b1d 100644 --- a/lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py +++ b/lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py @@ -1,24 +1,47 @@ import logging import traceback -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import warnings from pydantic import Field, PrivateAttr, model_validator from typing_extensions import Self from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage -from crewai.rag.chromadb.config import ChromaDBConfig -from crewai.rag.chromadb.types import ChromaEmbeddingFunctionWrapper -from crewai.rag.config.utils import get_rag_client from crewai.rag.core.base_client import BaseClient from crewai.rag.core.base_embeddings_provider import BaseEmbeddingsProvider from crewai.rag.embeddings.factory import build_embedder from crewai.rag.embeddings.types import ProviderSpec -from crewai.rag.factory import create_client from crewai.rag.types import BaseRecord, SearchResult from crewai.utilities.logger import Logger +if TYPE_CHECKING: + from crewai.rag.chromadb.types import ChromaEmbeddingFunctionWrapper + + +def create_client(config: Any) -> BaseClient: + """Create a RAG client for the given config. + + Thin wrapper around crewai.rag.factory.create_client, imported lazily so + that `import crewai` does not pull in the chromadb/qdrant provider chain. + """ + from crewai.rag.factory import create_client as _create_client + + return _create_client(config) + + +def get_rag_client() -> BaseClient: + """Get the global RAG client. + + Thin wrapper around crewai.rag.config.utils.get_rag_client, imported + lazily so that `import crewai` does not pull in the chromadb/qdrant + provider chain. + """ + from crewai.rag.config.utils import get_rag_client as _get_rag_client + + return _get_rag_client() + + class KnowledgeStorage(BaseKnowledgeStorage): """ Extends Storage to handle embeddings for memory entries, improving @@ -43,10 +66,14 @@ def _init_client(self) -> Self: ) if self.embedder: + # Imported lazily so that `import crewai` does not pull in the + # chromadb provider chain at import time. + from crewai.rag.chromadb.config import ChromaDBConfig + embedding_function = build_embedder(self.embedder) # type: ignore[arg-type] config = ChromaDBConfig( embedding_function=cast( - ChromaEmbeddingFunctionWrapper, embedding_function + "ChromaEmbeddingFunctionWrapper", embedding_function ) ) self._client = create_client(config) diff --git a/lib/crewai/src/crewai/rag/__init__.py b/lib/crewai/src/crewai/rag/__init__.py index 865910b427..644228a2a3 100644 --- a/lib/crewai/src/crewai/rag/__init__.py +++ b/lib/crewai/src/crewai/rag/__init__.py @@ -5,9 +5,6 @@ from types import ModuleType from typing import Any -from crewai.rag.config.types import RagConfigType -from crewai.rag.config.utils import set_rag_config - _module_path = __path__ _module_file = __file__ @@ -27,7 +24,7 @@ def __init__(self, module_name: str): """ super().__init__(module_name) - def __setattr__(self, name: str, value: RagConfigType) -> None: + def __setattr__(self, name: str, value: Any) -> None: """Set module attributes. Args: @@ -35,6 +32,17 @@ def __setattr__(self, name: str, value: RagConfigType) -> None: value: Attribute value. """ if name == "config": + if isinstance(value, ModuleType) and value is sys.modules.get( + f"{self.__name__}.config" + ): + # importlib registers the crewai.rag.config submodule on the + # package when it is first imported; store it as a plain + # module attribute instead of treating it as a RAG config. + return super().__setattr__(name, value) + # Imported lazily so that `import crewai.rag` does not pull in the + # provider config chain (chromadb, qdrant) at import time. + from crewai.rag.config.utils import set_rag_config + return set_rag_config(value) raise AttributeError(f"Setting attribute '{name}' is not allowed.") diff --git a/lib/crewai/tests/test_imports.py b/lib/crewai/tests/test_imports.py index bbbc3bbdd6..f9acbe9ef5 100644 --- a/lib/crewai/tests/test_imports.py +++ b/lib/crewai/tests/test_imports.py @@ -13,3 +13,74 @@ def test_crew_output_import(): from crewai import CrewOutput assert CrewOutput is not None + + +def test_import_crewai_does_not_load_qdrant(): + """Importing crewai must not eagerly load the qdrant_client dependency chain. + + Regression test for lazy RAG config imports: the provider config union + (crewai.rag.config.types -> qdrant_client/chromadb) must only load when + the RAG config surface is actually used, not at `import crewai` time. + """ + import subprocess + import sys + + code = ( + "import sys; import crewai; " + "sys.exit(1 if 'qdrant_client' in sys.modules else 0)" + ) + result = subprocess.run( + [sys.executable, "-c", code], capture_output=True, text=True, timeout=30 + ) + assert result.returncode == 0, ( + "qdrant_client was imported eagerly by `import crewai`" + f"\nstderr: {result.stderr}" + ) + + +def test_rag_module_setattr_contract(): + """crewai.rag still routes `config` assignment and rejects other attributes.""" + from types import ModuleType + + import crewai.rag as rag_mod + import crewai.rag.config.utils as rag_config_utils + + # Non-config attributes are rejected. + try: + rag_mod.some_random_attribute = 1 + except AttributeError: + pass + else: + raise AssertionError("crewai.rag accepted an arbitrary attribute") + + # `config` assignment routes to set_rag_config. + recorded = [] + original = rag_config_utils.set_rag_config + rag_config_utils.set_rag_config = recorded.append + try: + sentinel = object() + rag_mod.config = sentinel + unrelated_module = ModuleType("unrelated") + rag_mod.config = unrelated_module + assert recorded == [sentinel, unrelated_module] + finally: + rag_config_utils.set_rag_config = original + + +def test_lazy_import_annotations_resolve(): + """Lazy imports must not leave unresolved runtime annotations.""" + import typing + + import crewai.rag as rag_mod + from crewai.knowledge.storage import knowledge_storage + + assert typing.get_type_hints(type(rag_mod).__setattr__)["value"] is typing.Any + assert typing.get_type_hints(knowledge_storage.create_client)["config"] is typing.Any + + +def test_knowledge_storage_instantiates(): + """KnowledgeStorage still builds its pydantic model with lazy rag imports.""" + from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage + + storage = KnowledgeStorage(collection_name="import-regression") + assert storage.collection_name == "import-regression"