Compare commits

...

5 Commits

Author SHA1 Message Date
Devin AI
255758e429 update: refresh test cassettes after local test runs
- Update VCR cassettes for knowledge-related tests
- Ensures CI has consistent test recordings

Co-Authored-By: João <joao@crewai.com>
2025-06-27 10:06:35 +00:00
Devin AI
201a73289a fix: resolve type-checker and test isolation issues
- Remove hasattr/getattr calls that caused mypy type-checker errors
- Fix test mocking to use 'crewai.agent.Knowledge' for proper isolation
- Prevent network calls in tests by mocking Knowledge class constructor
- All knowledge-related tests now pass locally without API dependencies

Co-Authored-By: João <joao@crewai.com>
2025-06-27 10:06:22 +00:00
Devin AI
cd4b5b9ded fix: address CI failures - add type hints and fix tests
- Add proper PrivateAttr declarations for cache attributes to fix mypy errors
- Simplify tests to focus on set_knowledge method directly instead of full kickoff
- Remove network calls and invalid method mocking from tests
- All knowledge caching functionality verified working locally

Co-Authored-By: João <joao@crewai.com>
2025-06-27 10:01:12 +00:00
Devin AI
63914f38cb fix: cache agent knowledge to avoid reloading on every kickoff
- Add caching mechanism in Agent.set_knowledge to track loaded state
- Skip knowledge reloading when sources and embedder haven't changed
- Add reset_knowledge_cache method for explicit cache clearing
- Add comprehensive tests for caching behavior and edge cases
- Fixes issue #3076 performance overhead on repeated kickoffs

Co-Authored-By: João <joao@crewai.com>
2025-06-27 09:52:49 +00:00
Lucas Gomide
b35c3e8024 fix: ensure env-vars are written in upper case (#3072)
Some checks failed
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
When creating a Crew via the CLI and selecting the Azure provider, the generated .env file had environment variables in lowercase.
This commit ensures that all environment variables are written in uppercase.
2025-06-26 12:29:06 -04:00
10 changed files with 941 additions and 32 deletions

View File

@@ -71,6 +71,9 @@ class Agent(BaseAgent):
"""
_times_executed: int = PrivateAttr(default=0)
_knowledge_loaded: bool = PrivateAttr(default=False)
_last_embedder: Optional[Dict[str, Any]] = PrivateAttr(default=None)
_last_knowledge_sources: Optional[List[Any]] = PrivateAttr(default=None)
max_execution_time: Optional[int] = Field(
default=None,
description="Maximum execution time for an agent to execute a task",
@@ -194,6 +197,13 @@ class Agent(BaseAgent):
def set_knowledge(self, crew_embedder: Optional[Dict[str, Any]] = None):
try:
current_embedder = crew_embedder or self.embedder
if (self._knowledge_loaded and
self.knowledge is not None and
self._last_embedder == current_embedder and
self._last_knowledge_sources == self.knowledge_sources):
return
if self.embedder is None and crew_embedder:
self.embedder = crew_embedder
@@ -208,6 +218,10 @@ class Agent(BaseAgent):
storage=self.knowledge_storage or None,
)
self.knowledge.add_sources()
self._knowledge_loaded = True
self._last_embedder = current_embedder
self._last_knowledge_sources = self.knowledge_sources.copy() if self.knowledge_sources else None
except (TypeError, ValueError) as e:
raise ValueError(f"Invalid Knowledge Configuration: {str(e)}")
@@ -228,6 +242,13 @@ class Agent(BaseAgent):
return any(getattr(self.crew, attr) for attr in memory_attributes)
def reset_knowledge_cache(self) -> None:
"""Reset the knowledge cache to force reloading on next set_knowledge call."""
self._knowledge_loaded = False
self._last_embedder = None
self._last_knowledge_sources = None
self.knowledge = None
def execute_task(
self,
task: Task,

View File

@@ -252,7 +252,7 @@ def write_env_file(folder_path, env_vars):
env_file_path = folder_path / ".env"
with open(env_file_path, "w") as file:
for key, value in env_vars.items():
file.write(f"{key}={value}\n")
file.write(f"{key.upper()}={value}\n")
def get_crews(crew_path: str = "crew.py", require: bool = False) -> list[Crew]:

View File

@@ -2276,3 +2276,112 @@ def test_agent_from_repository_without_org_set(
"No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.",
style="yellow",
)
def test_agent_knowledge_caching_on_multiple_set_knowledge_calls():
"""Test that agent knowledge is only loaded once when set_knowledge is called multiple times."""
content = "Brandon's favorite color is blue and he likes Mexican food."
string_source = StringKnowledgeSource(content=content)
agent = Agent(
role="Researcher",
goal="Research about Brandon",
backstory="You are a researcher.",
knowledge_sources=[string_source],
llm="gpt-4o-mini"
)
with patch('crewai.agent.Knowledge') as mock_knowledge_class:
mock_knowledge_instance = MagicMock()
mock_knowledge_class.return_value = mock_knowledge_instance
agent.set_knowledge()
assert mock_knowledge_class.call_count == 1, "Knowledge should be created once on first call"
assert mock_knowledge_instance.add_sources.call_count == 1, "add_sources should be called once"
agent.set_knowledge()
assert mock_knowledge_class.call_count == 1, "Knowledge should not be recreated on second call"
assert mock_knowledge_instance.add_sources.call_count == 1, "add_sources should not be called again"
agent.set_knowledge()
assert mock_knowledge_class.call_count == 1, "Knowledge should not be recreated on third call"
assert mock_knowledge_instance.add_sources.call_count == 1, "add_sources should not be called again"
def test_agent_knowledge_reloads_when_sources_change():
"""Test that agent knowledge is reloaded when knowledge sources change."""
content1 = "Brandon's favorite color is blue."
content2 = "Brandon's favorite food is tacos."
string_source1 = StringKnowledgeSource(content=content1)
string_source2 = StringKnowledgeSource(content=content2)
agent = Agent(
role="Researcher",
goal="Research about Brandon",
backstory="You are a researcher.",
knowledge_sources=[string_source1],
llm="gpt-4o-mini"
)
with patch('crewai.agent.Knowledge') as mock_knowledge_class:
mock_knowledge_instance = MagicMock()
mock_knowledge_class.return_value = mock_knowledge_instance
agent.set_knowledge()
assert mock_knowledge_class.call_count == 1, "Knowledge should be created once on first call"
agent.knowledge_sources = [string_source2]
agent.set_knowledge()
assert mock_knowledge_class.call_count == 2, "Knowledge should be recreated when sources change"
def test_agent_knowledge_reloads_when_embedder_changes():
"""Test that agent knowledge is reloaded when embedder changes."""
content = "Brandon's favorite color is blue."
string_source = StringKnowledgeSource(content=content)
agent = Agent(
role="Researcher",
goal="Research about Brandon",
backstory="You are a researcher.",
knowledge_sources=[string_source],
llm="gpt-4o-mini"
)
embedder1 = {"provider": "openai", "model": "text-embedding-ada-002"}
embedder2 = {"provider": "openai", "model": "text-embedding-3-small"}
with patch('crewai.agent.Knowledge') as mock_knowledge_class:
mock_knowledge_instance = MagicMock()
mock_knowledge_class.return_value = mock_knowledge_instance
agent.set_knowledge(crew_embedder=embedder1)
assert mock_knowledge_class.call_count == 1, "Knowledge should be created once on first call"
agent.set_knowledge(crew_embedder=embedder2)
assert mock_knowledge_class.call_count == 2, "Knowledge should be recreated when embedder changes"
def test_agent_reset_knowledge_cache():
"""Test that reset_knowledge_cache forces knowledge reloading."""
content = "Brandon's favorite color is blue."
string_source = StringKnowledgeSource(content=content)
agent = Agent(
role="Researcher",
goal="Research about Brandon",
backstory="You are a researcher.",
knowledge_sources=[string_source]
)
agent._knowledge_loaded = True
agent._last_embedder = {"model": "test"}
agent._last_knowledge_sources = [string_source]
agent.reset_knowledge_cache()
assert not getattr(agent, '_knowledge_loaded', True)
assert agent._last_embedder is None
assert agent._last_knowledge_sources is None
assert agent.knowledge is None

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,6 @@ from click.testing import CliRunner
from crewai.cli.create_crew import create_crew, create_folder_structure
@pytest.fixture
def runner():
return CliRunner()
@@ -25,7 +24,7 @@ def temp_dir():
def test_create_folder_structure_strips_single_trailing_slash():
with tempfile.TemporaryDirectory() as temp_dir:
folder_path, folder_name, class_name = create_folder_structure("hello/", parent_folder=temp_dir)
assert folder_name == "hello"
assert class_name == "Hello"
assert folder_path.name == "hello"
@@ -36,7 +35,7 @@ def test_create_folder_structure_strips_single_trailing_slash():
def test_create_folder_structure_strips_multiple_trailing_slashes():
with tempfile.TemporaryDirectory() as temp_dir:
folder_path, folder_name, class_name = create_folder_structure("hello///", parent_folder=temp_dir)
assert folder_name == "hello"
assert class_name == "Hello"
assert folder_path.name == "hello"
@@ -47,7 +46,7 @@ def test_create_folder_structure_strips_multiple_trailing_slashes():
def test_create_folder_structure_handles_complex_name_with_trailing_slash():
with tempfile.TemporaryDirectory() as temp_dir:
folder_path, folder_name, class_name = create_folder_structure("my-awesome_project/", parent_folder=temp_dir)
assert folder_name == "my_awesome_project"
assert class_name == "MyAwesomeProject"
assert folder_path.name == "my_awesome_project"
@@ -58,7 +57,7 @@ def test_create_folder_structure_handles_complex_name_with_trailing_slash():
def test_create_folder_structure_normal_name_unchanged():
with tempfile.TemporaryDirectory() as temp_dir:
folder_path, folder_name, class_name = create_folder_structure("hello", parent_folder=temp_dir)
assert folder_name == "hello"
assert class_name == "Hello"
assert folder_path.name == "hello"
@@ -73,9 +72,9 @@ def test_create_folder_structure_with_parent_folder():
with tempfile.TemporaryDirectory() as temp_dir:
parent_path = Path(temp_dir) / "parent"
parent_path.mkdir()
folder_path, folder_name, class_name = create_folder_structure("child/", parent_folder=parent_path)
assert folder_name == "child"
assert class_name == "Child"
assert folder_path.name == "child"
@@ -88,18 +87,18 @@ def test_create_folder_structure_with_parent_folder():
@mock.patch("crewai.cli.create_crew.load_env_vars")
def test_create_crew_with_trailing_slash_creates_valid_project(mock_load_env, mock_write_env, mock_copy_template, temp_dir):
mock_load_env.return_value = {}
with tempfile.TemporaryDirectory() as work_dir:
with mock.patch("crewai.cli.create_crew.create_folder_structure") as mock_create_folder:
mock_folder_path = Path(work_dir) / "test_project"
mock_create_folder.return_value = (mock_folder_path, "test_project", "TestProject")
create_crew("test-project/", skip_provider=True)
mock_create_folder.assert_called_once_with("test-project/", None)
mock_copy_template.assert_called()
copy_calls = mock_copy_template.call_args_list
for call in copy_calls:
args = call[0]
if len(args) >= 5:
@@ -112,14 +111,14 @@ def test_create_crew_with_trailing_slash_creates_valid_project(mock_load_env, mo
@mock.patch("crewai.cli.create_crew.load_env_vars")
def test_create_crew_with_multiple_trailing_slashes(mock_load_env, mock_write_env, mock_copy_template, temp_dir):
mock_load_env.return_value = {}
with tempfile.TemporaryDirectory() as work_dir:
with mock.patch("crewai.cli.create_crew.create_folder_structure") as mock_create_folder:
mock_folder_path = Path(work_dir) / "test_project"
mock_create_folder.return_value = (mock_folder_path, "test_project", "TestProject")
create_crew("test-project///", skip_provider=True)
mock_create_folder.assert_called_once_with("test-project///", None)
@@ -128,21 +127,21 @@ def test_create_crew_with_multiple_trailing_slashes(mock_load_env, mock_write_en
@mock.patch("crewai.cli.create_crew.load_env_vars")
def test_create_crew_normal_name_still_works(mock_load_env, mock_write_env, mock_copy_template, temp_dir):
mock_load_env.return_value = {}
with tempfile.TemporaryDirectory() as work_dir:
with mock.patch("crewai.cli.create_crew.create_folder_structure") as mock_create_folder:
mock_folder_path = Path(work_dir) / "normal_project"
mock_create_folder.return_value = (mock_folder_path, "normal_project", "NormalProject")
create_crew("normal-project", skip_provider=True)
mock_create_folder.assert_called_once_with("normal-project", None)
def test_create_folder_structure_handles_spaces_and_dashes_with_slash():
with tempfile.TemporaryDirectory() as temp_dir:
folder_path, folder_name, class_name = create_folder_structure("My Cool-Project/", parent_folder=temp_dir)
assert folder_name == "my_cool_project"
assert class_name == "MyCoolProject"
assert folder_path.name == "my_cool_project"
@@ -155,7 +154,7 @@ def test_create_folder_structure_raises_error_for_invalid_names():
invalid_cases = [
("123project/", "cannot start with a digit"),
("True/", "reserved Python keyword"),
("False/", "reserved Python keyword"),
("False/", "reserved Python keyword"),
("None/", "reserved Python keyword"),
("class/", "reserved Python keyword"),
("def/", "reserved Python keyword"),
@@ -163,7 +162,7 @@ def test_create_folder_structure_raises_error_for_invalid_names():
("", "empty or contain only whitespace"),
("@#$/", "contains no valid characters"),
]
for invalid_name, expected_error in invalid_cases:
with pytest.raises(ValueError, match=expected_error):
create_folder_structure(invalid_name, parent_folder=temp_dir)
@@ -179,20 +178,20 @@ def test_create_folder_structure_validates_names():
("hello.world/", "helloworld", "HelloWorld"),
("hello@world/", "helloworld", "HelloWorld"),
]
for valid_name, expected_folder, expected_class in valid_cases:
folder_path, folder_name, class_name = create_folder_structure(valid_name, parent_folder=temp_dir)
assert folder_name == expected_folder
assert class_name == expected_class
assert folder_name.isidentifier(), f"folder_name '{folder_name}' should be valid Python identifier"
assert not keyword.iskeyword(folder_name), f"folder_name '{folder_name}' should not be Python keyword"
assert not folder_name[0].isdigit(), f"folder_name '{folder_name}' should not start with digit"
assert class_name.isidentifier(), f"class_name '{class_name}' should be valid Python identifier"
assert not keyword.iskeyword(class_name), f"class_name '{class_name}' should not be Python keyword"
assert folder_path.parent == Path(temp_dir)
if folder_path.exists():
shutil.rmtree(folder_path)
@@ -202,13 +201,13 @@ def test_create_folder_structure_validates_names():
@mock.patch("crewai.cli.create_crew.load_env_vars")
def test_create_crew_with_parent_folder_and_trailing_slash(mock_load_env, mock_write_env, mock_copy_template, temp_dir):
mock_load_env.return_value = {}
with tempfile.TemporaryDirectory() as work_dir:
parent_path = Path(work_dir) / "parent"
parent_path.mkdir()
create_crew("child-crew/", skip_provider=True, parent_folder=parent_path)
crew_path = parent_path / "child_crew"
assert crew_path.exists()
assert not (crew_path / "src").exists()
@@ -224,23 +223,56 @@ def test_create_folder_structure_folder_name_validation():
("for/", "reserved Python keyword"),
("@#$invalid/", "contains no valid characters.*Python module name"),
]
for invalid_name, expected_error in folder_invalid_cases:
with pytest.raises(ValueError, match=expected_error):
create_folder_structure(invalid_name, parent_folder=temp_dir)
valid_cases = [
("hello-world/", "hello_world"),
("my.project/", "myproject"),
("test@123/", "test123"),
("valid_name/", "valid_name"),
]
for valid_name, expected_folder in valid_cases:
folder_path, folder_name, class_name = create_folder_structure(valid_name, parent_folder=temp_dir)
assert folder_name == expected_folder
assert folder_name.isidentifier()
assert not keyword.iskeyword(folder_name)
if folder_path.exists():
shutil.rmtree(folder_path)
@mock.patch("crewai.cli.create_crew.create_folder_structure")
@mock.patch("crewai.cli.create_crew.copy_template")
@mock.patch("crewai.cli.create_crew.load_env_vars")
@mock.patch("crewai.cli.create_crew.get_provider_data")
@mock.patch("crewai.cli.create_crew.select_provider")
@mock.patch("crewai.cli.create_crew.select_model")
@mock.patch("click.prompt")
def test_env_vars_are_uppercased_in_env_file(
mock_prompt,
mock_select_model,
mock_select_provider,
mock_get_provider_data,
mock_load_env_vars,
mock_copy_template,
mock_create_folder_structure,
tmp_path
):
crew_path = tmp_path / "test_crew"
crew_path.mkdir()
mock_create_folder_structure.return_value = (crew_path, "test_crew", "TestCrew")
mock_load_env_vars.return_value = {}
mock_get_provider_data.return_value = {"openai": ["gpt-4"]}
mock_select_provider.return_value = "azure"
mock_select_model.return_value = "azure/openai"
mock_prompt.return_value = "fake-api-key"
create_crew("Test Crew")
env_file_path = crew_path / ".env"
content = env_file_path.read_text()
assert "MODEL=" in content