Compare commits

...

3 Commits

Author SHA1 Message Date
Devin AI
34a33fc287 fix: handle Pydantic V1/V2 compatibility in ChromaDBConfig
Fixes #4095

When users pass a chromadb.config.Settings object (Pydantic V1 model) to
ChromaDBConfig (Pydantic V2 dataclass), Pydantic V2 would attempt to
validate it and fail with: TypeError: BaseModel.validate() takes 2
positional arguments but 3 were given

This fix:
- Uses SkipValidation to prevent Pydantic V2 from validating the V1 Settings object
- Uses BeforeValidator to handle dict-to-Settings conversion
- Adds arbitrary_types_allowed=True to the config for extra safety

The settings field now accepts either:
- A chromadb.config.Settings instance (passed through unchanged)
- A dictionary of settings parameters (converted to Settings internally)

Co-Authored-By: João <joao@crewai.com>
2025-12-16 09:18:33 +00:00
Lorenze Jay
88d3c0fa97 feat: bump versions to 1.7.1 (#4092)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
* feat: bump versions to 1.7.1

* bump projects
2025-12-15 21:51:53 -08:00
Matt Aitchison
75ff7dce0c feat: add --no-commit flag to bump command (#4087)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Allows updating version files without creating a commit, branch, or PR.
2025-12-15 15:32:37 -06:00
10 changed files with 238 additions and 55 deletions

View File

@@ -12,7 +12,7 @@ dependencies = [
"pytube~=15.0.0",
"requests~=2.32.5",
"docker~=7.1.0",
"crewai==1.7.0",
"crewai==1.7.1",
"lancedb~=0.5.4",
"tiktoken~=0.8.0",
"beautifulsoup4~=4.13.4",

View File

@@ -291,4 +291,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.7.0"
__version__ = "1.7.1"

View File

@@ -49,7 +49,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.7.0",
"crewai-tools==1.7.1",
]
embeddings = [
"tiktoken~=0.8.0"

View File

@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.7.0"
__version__ = "1.7.1"
_telemetry_submitted = False

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.7.0"
"crewai[tools]==1.7.1"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.7.0"
"crewai[tools]==1.7.1"
]
[project.scripts]

View File

@@ -2,10 +2,11 @@
from dataclasses import field
import os
from typing import Literal, cast
from typing import Annotated, Any, Literal, cast
import warnings
from chromadb.config import Settings
from pydantic import BeforeValidator, ConfigDict, SkipValidation
from pydantic.dataclasses import dataclass as pyd_dataclass
from crewai.rag.chromadb.constants import (
@@ -31,6 +32,40 @@ warnings.filterwarnings(
)
def _coerce_settings(value: Any) -> Settings:
"""Coerce input value to a chromadb.config.Settings instance.
This validator handles the Pydantic V1/V2 compatibility issue by:
- Passing through existing Settings objects without validation
- Converting dict inputs to Settings objects
Args:
value: Either a Settings instance or a dict of settings parameters.
Returns:
A chromadb.config.Settings instance.
Raises:
TypeError: If value is neither a Settings instance nor a dict.
"""
if isinstance(value, Settings):
return value
if isinstance(value, dict):
return Settings(**value)
raise TypeError(
f"settings must be a chromadb.config.Settings instance or a dict, "
f"got {type(value).__name__}"
)
# Type alias that skips Pydantic V2 validation for chromadb Settings (Pydantic V1 model)
# and uses a before validator to handle dict-to-Settings conversion
ChromaSettings = Annotated[
SkipValidation[Settings],
BeforeValidator(_coerce_settings),
]
def _default_settings() -> Settings:
"""Create default ChromaDB settings.
@@ -64,14 +99,19 @@ def _default_embedding_function() -> ChromaEmbeddingFunctionWrapper:
)
@pyd_dataclass(frozen=True)
@pyd_dataclass(frozen=True, config=ConfigDict(arbitrary_types_allowed=True))
class ChromaDBConfig(BaseRagConfig):
"""Configuration for ChromaDB client."""
"""Configuration for ChromaDB client.
The settings field accepts either a chromadb.config.Settings instance
or a dictionary of settings parameters. This handles the Pydantic V1/V2
compatibility issue where ChromaDB uses Pydantic V1 for its Settings class.
"""
provider: Literal["chromadb"] = field(default="chromadb", init=False)
tenant: str = DEFAULT_TENANT
database: str = DEFAULT_DATABASE
settings: Settings = field(default_factory=_default_settings)
settings: ChromaSettings = field(default_factory=_default_settings)
embedding_function: ChromaEmbeddingFunctionWrapper = field(
default_factory=_default_embedding_function
)

View File

@@ -0,0 +1,130 @@
"""Tests for ChromaDBConfig Pydantic V1/V2 compatibility."""
import pytest
from chromadb.config import Settings
from crewai.rag.chromadb.config import ChromaDBConfig, _coerce_settings
class TestCoerceSettings:
"""Test suite for _coerce_settings validator function."""
def test_coerce_settings_passes_through_settings_instance(self):
"""Test that existing Settings instances are passed through unchanged."""
settings = Settings(
persist_directory="./test_db",
allow_reset=True,
is_persistent=False,
)
result = _coerce_settings(settings)
assert result is settings
assert result.persist_directory == "./test_db"
assert result.allow_reset is True
assert result.is_persistent is False
def test_coerce_settings_converts_dict_to_settings(self):
"""Test that dict inputs are converted to Settings instances."""
settings_dict = {
"persist_directory": "./my_custom_db",
"allow_reset": True,
"is_persistent": True,
}
result = _coerce_settings(settings_dict)
assert isinstance(result, Settings)
assert result.persist_directory == "./my_custom_db"
assert result.allow_reset is True
assert result.is_persistent is True
def test_coerce_settings_raises_type_error_for_invalid_input(self):
"""Test that invalid input types raise TypeError."""
with pytest.raises(TypeError, match="settings must be a chromadb.config.Settings"):
_coerce_settings("invalid_string")
with pytest.raises(TypeError, match="settings must be a chromadb.config.Settings"):
_coerce_settings(123)
with pytest.raises(TypeError, match="settings must be a chromadb.config.Settings"):
_coerce_settings(["list", "of", "items"])
class TestChromaDBConfigPydanticCompatibility:
"""Test suite for ChromaDBConfig Pydantic V1/V2 compatibility.
These tests verify the fix for GitHub issue #4095:
Pydantic V1/V2 Compatibility Crash in RagTool when passing custom ChromaDB Settings.
"""
def test_chromadb_config_accepts_settings_instance(self):
"""Test that ChromaDBConfig accepts a chromadb.config.Settings instance.
This is the main regression test for issue #4095 where passing a Settings
instance would cause: TypeError: BaseModel.validate() takes 2 positional
arguments but 3 were given
"""
custom_settings = Settings(
persist_directory="./my_db",
allow_reset=True,
is_persistent=False,
)
config = ChromaDBConfig(settings=custom_settings)
assert config.settings is custom_settings
assert config.settings.persist_directory == "./my_db"
assert config.settings.allow_reset is True
assert config.settings.is_persistent is False
def test_chromadb_config_accepts_settings_dict(self):
"""Test that ChromaDBConfig accepts a dict for settings and converts it."""
settings_dict = {
"persist_directory": "./dict_db",
"allow_reset": False,
"is_persistent": True,
}
config = ChromaDBConfig(settings=settings_dict)
assert isinstance(config.settings, Settings)
assert config.settings.persist_directory == "./dict_db"
assert config.settings.allow_reset is False
assert config.settings.is_persistent is True
def test_chromadb_config_uses_default_settings_when_not_provided(self):
"""Test that ChromaDBConfig uses default settings when none provided."""
config = ChromaDBConfig()
assert isinstance(config.settings, Settings)
assert config.settings.allow_reset is True
assert config.settings.is_persistent is True
def test_chromadb_config_with_all_parameters(self):
"""Test ChromaDBConfig with all parameters including custom settings."""
custom_settings = Settings(
persist_directory="./full_test_db",
allow_reset=True,
is_persistent=True,
)
config = ChromaDBConfig(
tenant="test_tenant",
database="test_database",
settings=custom_settings,
limit=10,
score_threshold=0.8,
batch_size=50,
)
assert config.tenant == "test_tenant"
assert config.database == "test_database"
assert config.settings is custom_settings
assert config.limit == 10
assert config.score_threshold == 0.8
assert config.batch_size == 50
def test_chromadb_config_provider_is_chromadb(self):
"""Test that provider field is always 'chromadb'."""
config = ChromaDBConfig()
assert config.provider == "chromadb"
def test_chromadb_config_is_frozen(self):
"""Test that ChromaDBConfig is immutable (frozen)."""
config = ChromaDBConfig()
with pytest.raises(AttributeError):
config.tenant = "new_tenant"

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.7.0"
__version__ = "1.7.1"

View File

@@ -323,13 +323,17 @@ def cli() -> None:
"--dry-run", is_flag=True, help="Show what would be done without making changes"
)
@click.option("--no-push", is_flag=True, help="Don't push changes to remote")
def bump(version: str, dry_run: bool, no_push: bool) -> None:
@click.option(
"--no-commit", is_flag=True, help="Don't commit changes (just update files)"
)
def bump(version: str, dry_run: bool, no_push: bool, no_commit: bool) -> None:
"""Bump version across all packages in lib/.
Args:
version: New version to set (e.g., 1.0.0, 1.0.0a1).
dry_run: Show what would be done without making changes.
no_push: Don't push changes to remote.
no_commit: Don't commit changes (just update files).
"""
try:
# Check prerequisites
@@ -397,51 +401,60 @@ def bump(version: str, dry_run: bool, no_push: bool) -> None:
else:
console.print("[dim][DRY RUN][/dim] Would run: uv sync")
branch_name = f"feat/bump-version-{version}"
if not dry_run:
console.print(f"\nCreating branch {branch_name}...")
run_command(["git", "checkout", "-b", branch_name])
console.print("[green]✓[/green] Branch created")
console.print("\nCommitting changes...")
run_command(["git", "add", "."])
run_command(["git", "commit", "-m", f"feat: bump versions to {version}"])
console.print("[green]✓[/green] Changes committed")
if not no_push:
console.print("\nPushing branch...")
run_command(["git", "push", "-u", "origin", branch_name])
console.print("[green]✓[/green] Branch pushed")
if no_commit:
console.print("\nSkipping git operations (--no-commit flag set)")
else:
console.print(f"[dim][DRY RUN][/dim] Would create branch: {branch_name}")
console.print(
f"[dim][DRY RUN][/dim] Would commit: feat: bump versions to {version}"
)
if not no_push:
console.print(f"[dim][DRY RUN][/dim] Would push branch: {branch_name}")
branch_name = f"feat/bump-version-{version}"
if not dry_run:
console.print(f"\nCreating branch {branch_name}...")
run_command(["git", "checkout", "-b", branch_name])
console.print("[green]✓[/green] Branch created")
if not dry_run and not no_push:
console.print("\nCreating pull request...")
run_command(
[
"gh",
"pr",
"create",
"--base",
"main",
"--title",
f"feat: bump versions to {version}",
"--body",
"",
]
)
console.print("[green]✓[/green] Pull request created")
elif dry_run:
console.print(
f"[dim][DRY RUN][/dim] Would create PR: feat: bump versions to {version}"
)
else:
console.print("\nSkipping PR creation (--no-push flag set)")
console.print("\nCommitting changes...")
run_command(["git", "add", "."])
run_command(
["git", "commit", "-m", f"feat: bump versions to {version}"]
)
console.print("[green]✓[/green] Changes committed")
if not no_push:
console.print("\nPushing branch...")
run_command(["git", "push", "-u", "origin", branch_name])
console.print("[green]✓[/green] Branch pushed")
else:
console.print(
f"[dim][DRY RUN][/dim] Would create branch: {branch_name}"
)
console.print(
f"[dim][DRY RUN][/dim] Would commit: feat: bump versions to {version}"
)
if not no_push:
console.print(
f"[dim][DRY RUN][/dim] Would push branch: {branch_name}"
)
if not dry_run and not no_push:
console.print("\nCreating pull request...")
run_command(
[
"gh",
"pr",
"create",
"--base",
"main",
"--title",
f"feat: bump versions to {version}",
"--body",
"",
]
)
console.print("[green]✓[/green] Pull request created")
elif dry_run:
console.print(
f"[dim][DRY RUN][/dim] Would create PR: feat: bump versions to {version}"
)
else:
console.print("\nSkipping PR creation (--no-push flag set)")
console.print(f"\n[green]✓[/green] Version bump to {version} complete!")