Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions lib/crewai/src/crewai/knowledge/storage/knowledge_storage.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
16 changes: 12 additions & 4 deletions lib/crewai/src/crewai/rag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand All @@ -27,14 +24,25 @@ 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:
name: Attribute name.
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.")

Expand Down
81 changes: 81 additions & 0 deletions lib/crewai/tests/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,84 @@ 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
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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_rag_config_types_still_resolve():
"""The provider config union still exposes the real config classes."""
import typing

from crewai.rag.config.types import SupportedProviderConfig

names = sorted(m.__name__ for m in typing.get_args(SupportedProviderConfig))
assert names == ["ChromaDBConfig", "QdrantConfig"]


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"