From 56ec9bc2241336156d223063c5250662de19ee84 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:20:16 -0300 Subject: [PATCH 01/10] fix: handle multiple task outputs correctly in conditional tasks (#1937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: handle multiple task outputs correctly in conditional tasks - Fix IndexError in _handle_conditional_task by using first output - Modify _execute_tasks to accumulate task outputs instead of resetting - Update _create_crew_output to handle multiple outputs correctly - Add tests for multiple tasks with conditional and multiple conditional tasks Co-Authored-By: brandon@crewai.com * feat: validate at least one non-conditional task and refine task outputs Co-Authored-By: brandon@crewai.com * Revert to single output in _create_crew_output; remove redundant empty task check Co-Authored-By: brandon@crewai.com * Address PR feedback: use last output in conditional tasks, add validation test Co-Authored-By: brandon@crewai.com * Address PR feedback: updated conditional tasks tests and indexing Co-Authored-By: brandon@crewai.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: brandon@crewai.com Co-authored-by: Brandon Hancock Co-authored-by: Brandon Hancock (bhancock_ai) <109994880+bhancockio@users.noreply.github.com> Co-authored-by: João Moura --- src/crewai/crew.py | 39 ++++++-- tests/crew_test.py | 222 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 8 deletions(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index f36df2a03..9ae9ce2c0 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -380,6 +380,22 @@ class Crew(BaseModel): return self + @model_validator(mode="after") + def validate_must_have_non_conditional_task(self) -> "Crew": + """Ensure that a crew has at least one non-conditional task.""" + if not self.tasks: + return self + non_conditional_count = sum( + 1 for task in self.tasks if not isinstance(task, ConditionalTask) + ) + if non_conditional_count == 0: + raise PydanticCustomError( + "only_conditional_tasks", + "Crew must include at least one non-conditional task", + {}, + ) + return self + @model_validator(mode="after") def validate_first_task(self) -> "Crew": """Ensure the first task is not a ConditionalTask.""" @@ -439,6 +455,8 @@ class Crew(BaseModel): ) return self + + @property def key(self) -> str: source = [agent.key for agent in self.agents] + [ @@ -741,6 +759,7 @@ class Crew(BaseModel): task, task_outputs, futures, task_index, was_replayed ) if skipped_task_output: + task_outputs.append(skipped_task_output) continue if task.async_execution: @@ -764,7 +783,7 @@ class Crew(BaseModel): context=context, tools=tools_for_task, ) - task_outputs = [task_output] + task_outputs.append(task_output) self._process_task_result(task, task_output) self._store_execution_log(task, task_output, task_index, was_replayed) @@ -785,7 +804,7 @@ class Crew(BaseModel): task_outputs = self._process_async_tasks(futures, was_replayed) futures.clear() - previous_output = task_outputs[task_index - 1] if task_outputs else None + previous_output = task_outputs[-1] if task_outputs else None if previous_output is not None and not task.should_execute(previous_output): self._logger.log( "debug", @@ -907,11 +926,15 @@ class Crew(BaseModel): ) def _create_crew_output(self, task_outputs: List[TaskOutput]) -> CrewOutput: - if len(task_outputs) != 1: - raise ValueError( - "Something went wrong. Kickoff should return only one task output." - ) - final_task_output = task_outputs[0] + if not task_outputs: + raise ValueError("No task outputs available to create crew output.") + + # Filter out empty outputs and get the last valid one as the main output + valid_outputs = [t for t in task_outputs if t.raw] + if not valid_outputs: + raise ValueError("No valid task outputs available to create crew output.") + final_task_output = valid_outputs[-1] + final_string_output = final_task_output.raw self._finish_execution(final_string_output) token_usage = self.calculate_usage_metrics() @@ -920,7 +943,7 @@ class Crew(BaseModel): raw=final_task_output.raw, pydantic=final_task_output.pydantic, json_dict=final_task_output.json_dict, - tasks_output=[task.output for task in self.tasks if task.output], + tasks_output=task_outputs, token_usage=token_usage, ) diff --git a/tests/crew_test.py b/tests/crew_test.py index 4812ab93f..e69c71315 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -49,6 +49,39 @@ writer = Agent( ) +def test_crew_with_only_conditional_tasks_raises_error(): + """Test that creating a crew with only conditional tasks raises an error.""" + def condition_func(task_output: TaskOutput) -> bool: + return True + + conditional1 = ConditionalTask( + description="Conditional task 1", + expected_output="Output 1", + agent=researcher, + condition=condition_func, + ) + conditional2 = ConditionalTask( + description="Conditional task 2", + expected_output="Output 2", + agent=researcher, + condition=condition_func, + ) + conditional3 = ConditionalTask( + description="Conditional task 3", + expected_output="Output 3", + agent=researcher, + condition=condition_func, + ) + + with pytest.raises( + pydantic_core._pydantic_core.ValidationError, + match="Crew must include at least one non-conditional task", + ): + Crew( + agents=[researcher], + tasks=[conditional1, conditional2, conditional3], + ) + def test_crew_config_conditional_requirement(): with pytest.raises(ValueError): Crew(process=Process.sequential) @@ -2060,6 +2093,195 @@ def test_tools_with_custom_caching(): assert result.raw == "3" +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_conditional_task_uses_last_output(): + """Test that conditional tasks use the last task output for condition evaluation.""" + task1 = Task( + description="First task", + expected_output="First output", + agent=researcher, + ) + def condition_fails(task_output: TaskOutput) -> bool: + # This condition will never be met + return "never matches" in task_output.raw.lower() + + def condition_succeeds(task_output: TaskOutput) -> bool: + # This condition will match first task's output + return "first success" in task_output.raw.lower() + + conditional_task1 = ConditionalTask( + description="Second task - conditional that fails condition", + expected_output="Second output", + agent=researcher, + condition=condition_fails, + ) + + conditional_task2 = ConditionalTask( + description="Third task - conditional that succeeds using first task output", + expected_output="Third output", + agent=writer, + condition=condition_succeeds, + ) + + crew = Crew( + agents=[researcher, writer], + tasks=[task1, conditional_task1, conditional_task2], + ) + + # Mock outputs for tasks + mock_first = TaskOutput( + description="First task output", + raw="First success output", # Will be used by third task's condition + agent=researcher.role, + ) + mock_skipped = TaskOutput( + description="Second task output", + raw="", # Empty output since condition fails + agent=researcher.role, + ) + mock_third = TaskOutput( + description="Third task output", + raw="Third task executed", # Output when condition succeeds using first task output + agent=writer.role, + ) + + # Set up mocks for task execution and conditional logic + with patch.object(ConditionalTask, "should_execute") as mock_should_execute: + # First conditional fails, second succeeds + mock_should_execute.side_effect = [False, True] + + with patch.object(Task, "execute_sync") as mock_execute: + mock_execute.side_effect = [mock_first, mock_third] + result = crew.kickoff() + + # Verify execution behavior + assert mock_execute.call_count == 2 # Only first and third tasks execute + assert mock_should_execute.call_count == 2 # Both conditionals checked + + # Verify outputs collection + assert len(result.tasks_output) == 3 + assert result.tasks_output[0].raw == "First success output" # First task succeeded + assert result.tasks_output[1].raw == "" # Second task skipped (condition failed) + assert result.tasks_output[2].raw == "Third task executed" # Third task used first task's output + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_conditional_tasks_result_collection(): + """Test that task outputs are properly collected based on execution status.""" + task1 = Task( + description="Normal task that always executes", + expected_output="First output", + agent=researcher, + ) + + def condition_never_met(task_output: TaskOutput) -> bool: + return "never matches" in task_output.raw.lower() + + def condition_always_met(task_output: TaskOutput) -> bool: + return "success" in task_output.raw.lower() + + task2 = ConditionalTask( + description="Conditional task that never executes", + expected_output="Second output", + agent=researcher, + condition=condition_never_met, + ) + + task3 = ConditionalTask( + description="Conditional task that always executes", + expected_output="Third output", + agent=writer, + condition=condition_always_met, + ) + + crew = Crew( + agents=[researcher, writer], + tasks=[task1, task2, task3], + ) + + # Mock outputs for different execution paths + mock_success = TaskOutput( + description="Success output", + raw="Success output", # Triggers third task's condition + agent=researcher.role, + ) + mock_skipped = TaskOutput( + description="Skipped output", + raw="", # Empty output for skipped task + agent=researcher.role, + ) + mock_conditional = TaskOutput( + description="Conditional output", + raw="Conditional task executed", + agent=writer.role, + ) + + # Set up mocks for task execution and conditional logic + with patch.object(ConditionalTask, "should_execute") as mock_should_execute: + # First conditional fails, second succeeds + mock_should_execute.side_effect = [False, True] + + with patch.object(Task, "execute_sync") as mock_execute: + mock_execute.side_effect = [mock_success, mock_conditional] + result = crew.kickoff() + + # Verify execution behavior + assert mock_execute.call_count == 2 # Only first and third tasks execute + assert mock_should_execute.call_count == 2 # Both conditionals checked + + # Verify task output collection + assert len(result.tasks_output) == 3 + assert result.tasks_output[0].raw == "Success output" # Normal task executed + assert result.tasks_output[1].raw == "" # Second task skipped + assert result.tasks_output[2].raw == "Conditional task executed" # Third task executed + +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_multiple_conditional_tasks(): + """Test that having multiple conditional tasks in sequence works correctly.""" + task1 = Task( + description="Initial research task", + expected_output="Research output", + agent=researcher, + ) + + def condition1(task_output: TaskOutput) -> bool: + return "success" in task_output.raw.lower() + + def condition2(task_output: TaskOutput) -> bool: + return "proceed" in task_output.raw.lower() + + task2 = ConditionalTask( + description="First conditional task", + expected_output="Conditional output 1", + agent=writer, + condition=condition1, + ) + + task3 = ConditionalTask( + description="Second conditional task", + expected_output="Conditional output 2", + agent=writer, + condition=condition2, + ) + + crew = Crew( + agents=[researcher, writer], + tasks=[task1, task2, task3], + ) + + # Mock different task outputs to test conditional logic + mock_success = TaskOutput( + description="Mock success", + raw="Success and proceed output", + agent=researcher.role, + ) + + # Set up mocks for task execution + with patch.object(Task, "execute_sync", return_value=mock_success) as mock_execute: + result = crew.kickoff() + # Verify all tasks were executed (no IndexError) + assert mock_execute.call_count == 3 + assert len(result.tasks_output) == 3 + @pytest.mark.vcr(filter_headers=["authorization"]) def test_using_contextual_memory(): from unittest.mock import patch From a79d77dfd78c878eb0e7d55855cd39b98076a5d7 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:21:56 -0300 Subject: [PATCH 02/10] docs: document FileWriterTool as solution for file writing issues (#2039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add FileWriterTool recommendation for file writing issues - Add FileWriterTool recommendation in _save_file docstring - Update error message to suggest using FileWriterTool for cross-platform compatibility - Resolves #2015 Co-Authored-By: Joe Moura * docs: enhance FileWriterTool documentation - Add cross-platform compatibility details - Highlight UTF-8 encoding support - Emphasize Windows compatibility - Add recommendation for users experiencing file writing issues Part of #2015 Co-Authored-By: Joe Moura * refactor: improve _save_file type hints and error messages Co-Authored-By: Joe Moura --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Joe Moura Co-authored-by: João Moura --- docs/tools/filewritetool.mdx | 12 +++++++----- src/crewai/task.py | 24 +++++++++++++++++++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/docs/tools/filewritetool.mdx b/docs/tools/filewritetool.mdx index f5dffb2ad..5e00801b7 100644 --- a/docs/tools/filewritetool.mdx +++ b/docs/tools/filewritetool.mdx @@ -8,9 +8,9 @@ icon: file-pen ## Description -The `FileWriterTool` is a component of the crewai_tools package, designed to simplify the process of writing content to files. +The `FileWriterTool` is a component of the crewai_tools package, designed to simplify the process of writing content to files with cross-platform compatibility (Windows, Linux, macOS). It is particularly useful in scenarios such as generating reports, saving logs, creating configuration files, and more. -This tool supports creating new directories if they don't exist, making it easier to organize your output. +This tool handles path differences across operating systems, supports UTF-8 encoding, and automatically creates directories if they don't exist, making it easier to organize your output reliably across different platforms. ## Installation @@ -43,6 +43,8 @@ print(result) ## Conclusion -By integrating the `FileWriterTool` into your crews, the agents can execute the process of writing content to files and creating directories. -This tool is essential for tasks that require saving output data, creating structured file systems, and more. By adhering to the setup and usage guidelines provided, -incorporating this tool into projects is straightforward and efficient. \ No newline at end of file +By integrating the `FileWriterTool` into your crews, the agents can reliably write content to files across different operating systems. +This tool is essential for tasks that require saving output data, creating structured file systems, and handling cross-platform file operations. +It's particularly recommended for Windows users who may encounter file writing issues with standard Python file operations. + +By adhering to the setup and usage guidelines provided, incorporating this tool into projects is straightforward and ensures consistent file writing behavior across all platforms. diff --git a/src/crewai/task.py b/src/crewai/task.py index a3ee8aa14..4088c3fb0 100644 --- a/src/crewai/task.py +++ b/src/crewai/task.py @@ -674,19 +674,32 @@ class Task(BaseModel): return OutputFormat.PYDANTIC return OutputFormat.RAW - def _save_file(self, result: Any) -> None: + def _save_file(self, result: Union[Dict, str, Any]) -> None: """Save task output to a file. + Note: + For cross-platform file writing, especially on Windows, consider using FileWriterTool + from the crewai_tools package: + pip install 'crewai[tools]' + from crewai_tools import FileWriterTool + Args: result: The result to save to the file. Can be a dict or any stringifiable object. Raises: ValueError: If output_file is not set - RuntimeError: If there is an error writing to the file + RuntimeError: If there is an error writing to the file. For cross-platform + compatibility, especially on Windows, use FileWriterTool from crewai_tools + package. """ if self.output_file is None: raise ValueError("output_file is not set.") + FILEWRITER_RECOMMENDATION = ( + "For cross-platform file writing, especially on Windows, " + "use FileWriterTool from crewai_tools package." + ) + try: resolved_path = Path(self.output_file).expanduser().resolve() directory = resolved_path.parent @@ -702,7 +715,12 @@ class Task(BaseModel): else: file.write(str(result)) except (OSError, IOError) as e: - raise RuntimeError(f"Failed to save output file: {e}") + raise RuntimeError( + "\n".join([ + f"Failed to save output file: {e}", + FILEWRITER_RECOMMENDATION + ]) + ) return None def __repr__(self): From e0600e3bb96199a7050054cb5887c48187986ae4 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:35:52 -0300 Subject: [PATCH 03/10] fix: ensure proper message formatting for Anthropic models (#2063) * fix: ensure proper message formatting for Anthropic models - Add Anthropic-specific message formatting - Add placeholder user message when required - Add test case for Anthropic message formatting Fixes #1869 Co-Authored-By: Joe Moura * refactor: improve Anthropic model handling - Add robust model detection with _is_anthropic_model - Enhance message formatting with better edge cases - Add type hints and improve documentation - Improve test structure with fixtures - Add edge case tests Addresses review feedback on #2063 Co-Authored-By: Joe Moura --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Joe Moura --- src/crewai/llm.py | 116 ++++++++++++++++++++++++++++++++++------------ tests/llm_test.py | 73 +++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 30 deletions(-) diff --git a/src/crewai/llm.py b/src/crewai/llm.py index d6be4b588..ada5c9bf3 100644 --- a/src/crewai/llm.py +++ b/src/crewai/llm.py @@ -164,6 +164,7 @@ class LLM: self.context_window_size = 0 self.reasoning_effort = reasoning_effort self.additional_params = kwargs + self.is_anthropic = self._is_anthropic_model(model) litellm.drop_params = True @@ -178,42 +179,62 @@ class LLM: self.set_callbacks(callbacks) self.set_env_callbacks() + def _is_anthropic_model(self, model: str) -> bool: + """Determine if the model is from Anthropic provider. + + Args: + model: The model identifier string. + + Returns: + bool: True if the model is from Anthropic, False otherwise. + """ + ANTHROPIC_PREFIXES = ('anthropic/', 'claude-', 'claude/') + return any(prefix in model.lower() for prefix in ANTHROPIC_PREFIXES) + def call( self, messages: Union[str, List[Dict[str, str]]], tools: Optional[List[dict]] = None, callbacks: Optional[List[Any]] = None, available_functions: Optional[Dict[str, Any]] = None, - ) -> str: - """ - High-level llm call method that: - 1) Accepts either a string or a list of messages - 2) Converts string input to the required message format - 3) Calls litellm.completion - 4) Handles function/tool calls if any - 5) Returns the final text response or tool result - - Parameters: - - messages (Union[str, List[Dict[str, str]]]): The input messages for the LLM. - - If a string is provided, it will be converted into a message list with a single entry. - - If a list of dictionaries is provided, each dictionary should have 'role' and 'content' keys. - - tools (Optional[List[dict]]): A list of tool schemas for function calling. - - callbacks (Optional[List[Any]]): A list of callback functions to be executed. - - available_functions (Optional[Dict[str, Any]]): A dictionary mapping function names to actual Python functions. - + ) -> Union[str, Any]: + """High-level LLM call method. + + Args: + messages: Input messages for the LLM. + Can be a string or list of message dictionaries. + If string, it will be converted to a single user message. + If list, each dict must have 'role' and 'content' keys. + tools: Optional list of tool schemas for function calling. + Each tool should define its name, description, and parameters. + callbacks: Optional list of callback functions to be executed + during and after the LLM call. + available_functions: Optional dict mapping function names to callables + that can be invoked by the LLM. + Returns: - - str: The final text response from the LLM or the result of a tool function call. - + Union[str, Any]: Either a text response from the LLM (str) or + the result of a tool function call (Any). + + Raises: + TypeError: If messages format is invalid + ValueError: If response format is not supported + LLMContextLengthExceededException: If input exceeds model's context limit + Examples: - --------- - # Example 1: Using a string input - response = llm.call("Return the name of a random city in the world.") - print(response) - - # Example 2: Using a list of messages - messages = [{"role": "user", "content": "What is the capital of France?"}] - response = llm.call(messages) - print(response) + # Example 1: Simple string input + >>> response = llm.call("Return the name of a random city.") + >>> print(response) + "Paris" + + # Example 2: Message list with system and user messages + >>> messages = [ + ... {"role": "system", "content": "You are a geography expert"}, + ... {"role": "user", "content": "What is France's capital?"} + ... ] + >>> response = llm.call(messages) + >>> print(response) + "The capital of France is Paris." """ # Validate parameters before proceeding with the call. self._validate_call_params() @@ -233,10 +254,13 @@ class LLM: self.set_callbacks(callbacks) try: - # --- 1) Prepare the parameters for the completion call + # --- 1) Format messages according to provider requirements + formatted_messages = self._format_messages_for_provider(messages) + + # --- 2) Prepare the parameters for the completion call params = { "model": self.model, - "messages": messages, + "messages": formatted_messages, "timeout": self.timeout, "temperature": self.temperature, "top_p": self.top_p, @@ -324,6 +348,38 @@ class LLM: logging.error(f"LiteLLM call failed: {str(e)}") raise + def _format_messages_for_provider(self, messages: List[Dict[str, str]]) -> List[Dict[str, str]]: + """Format messages according to provider requirements. + + Args: + messages: List of message dictionaries with 'role' and 'content' keys. + Can be empty or None. + + Returns: + List of formatted messages according to provider requirements. + For Anthropic models, ensures first message has 'user' role. + + Raises: + TypeError: If messages is None or contains invalid message format. + """ + if messages is None: + raise TypeError("Messages cannot be None") + + # Validate message format first + for msg in messages: + if not isinstance(msg, dict) or "role" not in msg or "content" not in msg: + raise TypeError("Invalid message format. Each message must be a dict with 'role' and 'content' keys") + + if not self.is_anthropic: + return messages + + # Anthropic requires messages to start with 'user' role + if not messages or messages[0]["role"] == "system": + # If first message is system or empty, add a placeholder user message + return [{"role": "user", "content": "."}, *messages] + + return messages + def _get_custom_llm_provider(self) -> str: """ Derives the custom_llm_provider from the model string. diff --git a/tests/llm_test.py b/tests/llm_test.py index d64639dca..2e5faf774 100644 --- a/tests/llm_test.py +++ b/tests/llm_test.py @@ -286,6 +286,79 @@ def test_o3_mini_reasoning_effort_medium(): @pytest.mark.vcr(filter_headers=["authorization"]) +@pytest.fixture +def anthropic_llm(): + """Fixture providing an Anthropic LLM instance.""" + return LLM(model="anthropic/claude-3-sonnet") + +@pytest.fixture +def system_message(): + """Fixture providing a system message.""" + return {"role": "system", "content": "test"} + +@pytest.fixture +def user_message(): + """Fixture providing a user message.""" + return {"role": "user", "content": "test"} + +def test_anthropic_message_formatting_edge_cases(anthropic_llm): + """Test edge cases for Anthropic message formatting.""" + # Test None messages + with pytest.raises(TypeError, match="Messages cannot be None"): + anthropic_llm._format_messages_for_provider(None) + + # Test empty message list + formatted = anthropic_llm._format_messages_for_provider([]) + assert len(formatted) == 1 + assert formatted[0]["role"] == "user" + assert formatted[0]["content"] == "." + + # Test invalid message format + with pytest.raises(TypeError, match="Invalid message format"): + anthropic_llm._format_messages_for_provider([{"invalid": "message"}]) + +def test_anthropic_model_detection(): + """Test Anthropic model detection with various formats.""" + models = [ + ("anthropic/claude-3", True), + ("claude-instant", True), + ("claude/v1", True), + ("gpt-4", False), + ("", False), + ("anthropomorphic", False), # Should not match partial words + ] + + for model, expected in models: + llm = LLM(model=model) + assert llm.is_anthropic == expected, f"Failed for model: {model}" + +def test_anthropic_message_formatting(anthropic_llm, system_message, user_message): + """Test Anthropic message formatting with fixtures.""" + # Test when first message is system + formatted = anthropic_llm._format_messages_for_provider([system_message]) + assert len(formatted) == 2 + assert formatted[0]["role"] == "user" + assert formatted[0]["content"] == "." + assert formatted[1] == system_message + + # Test when first message is already user + formatted = anthropic_llm._format_messages_for_provider([user_message]) + assert len(formatted) == 1 + assert formatted[0] == user_message + + # Test with empty message list + formatted = anthropic_llm._format_messages_for_provider([]) + assert len(formatted) == 1 + assert formatted[0]["role"] == "user" + assert formatted[0]["content"] == "." + + # Test with non-Anthropic model (should not modify messages) + non_anthropic_llm = LLM(model="gpt-4") + formatted = non_anthropic_llm._format_messages_for_provider([system_message]) + assert len(formatted) == 1 + assert formatted[0] == system_message + + def test_deepseek_r1_with_open_router(): if not os.getenv("OPEN_ROUTER_API_KEY"): pytest.skip("OPEN_ROUTER_API_KEY not set; skipping test.") From d6d98ee969503cf8b8e7d2d62d5d78eee331bd36 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:47:31 -0300 Subject: [PATCH 04/10] docs: fix long term memory class name in examples (#2049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: fix long term memory class name in examples - Replace EnhanceLongTermMemory with LongTermMemory to match actual implementation - Update code examples to show correct usage - Fixes #2026 Co-Authored-By: Joe Moura * docs: improve memory examples with imports, types and security - Add proper import statements - Add type hints for better readability - Add descriptive comments for each memory type - Add security considerations section - Add configuration examples section - Use environment variables for storage paths Co-Authored-By: Joe Moura * Update memory.mdx * Update memory.mdx --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Joe Moura Co-authored-by: João Moura --- docs/concepts/memory.mdx | 112 +++++++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 23 deletions(-) diff --git a/docs/concepts/memory.mdx b/docs/concepts/memory.mdx index 33df47b82..8db1fe33a 100644 --- a/docs/concepts/memory.mdx +++ b/docs/concepts/memory.mdx @@ -58,41 +58,107 @@ my_crew = Crew( ### Example: Use Custom Memory Instances e.g FAISS as the VectorDB ```python Code -from crewai import Crew, Agent, Task, Process +from crewai import Crew, Process +from crewai.memory import LongTermMemory, ShortTermMemory, EntityMemory +from crewai.memory.storage import LTMSQLiteStorage, RAGStorage +from typing import List, Optional # Assemble your crew with memory capabilities -my_crew = Crew( - agents=[...], - tasks=[...], - process="Process.sequential", - memory=True, - long_term_memory=EnhanceLongTermMemory( +my_crew: Crew = Crew( + agents = [...], + tasks = [...], + process = Process.sequential, + memory = True, + # Long-term memory for persistent storage across sessions + long_term_memory = LongTermMemory( storage=LTMSQLiteStorage( - db_path="/my_data_dir/my_crew1/long_term_memory_storage.db" + db_path="/my_crew1/long_term_memory_storage.db" ) ), - short_term_memory=EnhanceShortTermMemory( - storage=CustomRAGStorage( - crew_name="my_crew", - storage_type="short_term", - data_dir="//my_data_dir", - model=embedder["model"], - dimension=embedder["dimension"], + # Short-term memory for current context using RAG + short_term_memory = ShortTermMemory( + storage = RAGStorage( + embedder_config={ + "provider": "openai", + "config": { + "model": 'text-embedding-3-small' + } + }, + type="short_term", + path="/my_crew1/" + ) ), ), - entity_memory=EnhanceEntityMemory( - storage=CustomRAGStorage( - crew_name="my_crew", - storage_type="entities", - data_dir="//my_data_dir", - model=embedder["model"], - dimension=embedder["dimension"], - ), + # Entity memory for tracking key information about entities + entity_memory = EntityMemory( + storage=RAGStorage( + embedder_config={ + "provider": "openai", + "config": { + "model": 'text-embedding-3-small' + } + }, + type="short_term", + path="/my_crew1/" + ) ), verbose=True, ) ``` +## Security Considerations + +When configuring memory storage: +- Use environment variables for storage paths (e.g., `CREWAI_STORAGE_DIR`) +- Never hardcode sensitive information like database credentials +- Consider access permissions for storage directories +- Use relative paths when possible to maintain portability + +Example using environment variables: +```python +import os +from crewai import Crew +from crewai.memory import LongTermMemory +from crewai.memory.storage import LTMSQLiteStorage + +# Configure storage path using environment variable +storage_path = os.getenv("CREWAI_STORAGE_DIR", "./storage") +crew = Crew( + memory=True, + long_term_memory=LongTermMemory( + storage=LTMSQLiteStorage( + db_path="{storage_path}/memory.db".format(storage_path=storage_path) + ) + ) +) +``` + +## Configuration Examples + +### Basic Memory Configuration +```python +from crewai import Crew +from crewai.memory import LongTermMemory + +# Simple memory configuration +crew = Crew(memory=True) # Uses default storage locations +``` + +### Custom Storage Configuration +```python +from crewai import Crew +from crewai.memory import LongTermMemory +from crewai.memory.storage import LTMSQLiteStorage + +# Configure custom storage paths +crew = Crew( + memory=True, + long_term_memory=LongTermMemory( + storage=LTMSQLiteStorage(db_path="./memory.db") + ) +) +``` + ## Integrating Mem0 for Enhanced User Memory [Mem0](https://mem0.ai/) is a self-improving memory layer for LLM applications, enabling personalized AI experiences. From 17e25fb84251003306ea79076a968c14016ca853 Mon Sep 17 00:00:00 2001 From: Bradley Goodyear Date: Sun, 9 Feb 2025 18:23:52 -0500 Subject: [PATCH 05/10] Fix a typo in the Task Guardrails section (#2043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Moura --- docs/concepts/tasks.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/tasks.mdx b/docs/concepts/tasks.mdx index 4aa47c416..120f5d547 100644 --- a/docs/concepts/tasks.mdx +++ b/docs/concepts/tasks.mdx @@ -268,7 +268,7 @@ analysis_task = Task( Task guardrails provide a way to validate and transform task outputs before they are passed to the next task. This feature helps ensure data quality and provides -efeedback to agents when their output doesn't meet specific criteria. +feedback to agents when their output doesn't meet specific criteria. ### Using Task Guardrails From fbd0e015d5dc2b4fa744b1422f666db586554613 Mon Sep 17 00:00:00 2001 From: Nicolas Lorin Date: Mon, 10 Feb 2025 00:25:33 +0100 Subject: [PATCH 06/10] doc: use the corresponding source depending on filetype (#2038) Co-authored-by: Brandon Hancock (bhancock_ai) <109994880+bhancockio@users.noreply.github.com> --- docs/concepts/knowledge.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/concepts/knowledge.mdx b/docs/concepts/knowledge.mdx index 78443ecab..b5827551a 100644 --- a/docs/concepts/knowledge.mdx +++ b/docs/concepts/knowledge.mdx @@ -91,7 +91,7 @@ result = crew.kickoff(inputs={"question": "What city does John live in and how o ``` -Here's another example with the `CrewDoclingSource`. The CrewDoclingSource is actually quite versatile and can handle multiple file formats including TXT, PDF, DOCX, HTML, and more. +Here's another example with the `CrewDoclingSource`. The CrewDoclingSource is actually quite versatile and can handle multiple file formats including MD, PDF, DOCX, HTML, and more. You need to install `docling` for the following example to work: `uv add docling` @@ -152,10 +152,10 @@ Here are examples of how to use different types of knowledge sources: ### Text File Knowledge Source ```python -from crewai.knowledge.source.crew_docling_source import CrewDoclingSource +from crewai.knowledge.source.text_file_knowledge_source import TextFileKnowledgeSource # Create a text file knowledge source -text_source = CrewDoclingSource( +text_source = TextFileKnowledgeSource( file_paths=["document.txt", "another.txt"] ) From 90b3145e922fcb8f4b1a1e6938225dc07f6d464c Mon Sep 17 00:00:00 2001 From: Kevin King <64164523+Kking112@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:56:32 -0500 Subject: [PATCH 07/10] Updated excel_knowledge_source.py to account for excel files with multiple tabs. (#1921) * Updated excel_knowledge_source.py to account for excel sheets that have multiple tabs. The old implementation contained a single df=pd.read_excel(excel_file_path), which only reads the first or most recently used excel sheet. The updated functionality reads all sheets in the excel workbook. * updated load_content() function in excel_knowledge_source.py to reduce memory usage and provide better documentation * accidentally didn't delete the old load_content() function in last commit - corrected this * Added an override for the content field from the inheritted BaseFileKnowledgeSource to account for the change in the load_content method to support excel files with multiple tabs/sheets. This change should ensure it passes the type check test, as it failed before since content was assigned a different type in BaseFileKnowledgeSource * Now removed the commented out imports in _import_dependencies, as requested * Updated excel_knowledge_source to fix linter errors and type errors. Changed inheritence from basefileknowledgesource to baseknowledgesource because basefileknowledgesource's types conflicted (in particular the load_content function and the content class variable. --------- Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> --- .../source/excel_knowledge_source.py | 142 ++++++++++++++++-- 1 file changed, 128 insertions(+), 14 deletions(-) diff --git a/src/crewai/knowledge/source/excel_knowledge_source.py b/src/crewai/knowledge/source/excel_knowledge_source.py index d379c2763..a73afb1df 100644 --- a/src/crewai/knowledge/source/excel_knowledge_source.py +++ b/src/crewai/knowledge/source/excel_knowledge_source.py @@ -1,28 +1,138 @@ from pathlib import Path -from typing import Dict, List +from typing import Dict, Iterator, List, Optional, Union +from urllib.parse import urlparse -from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource +from pydantic import Field, field_validator + +from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource +from crewai.utilities.constants import KNOWLEDGE_DIRECTORY +from crewai.utilities.logger import Logger -class ExcelKnowledgeSource(BaseFileKnowledgeSource): +class ExcelKnowledgeSource(BaseKnowledgeSource): """A knowledge source that stores and queries Excel file content using embeddings.""" - def load_content(self) -> Dict[Path, str]: - """Load and preprocess Excel file content.""" - pd = self._import_dependencies() + # override content to be a dict of file paths to sheet names to csv content + _logger: Logger = Logger(verbose=True) + + file_path: Optional[Union[Path, List[Path], str, List[str]]] = Field( + default=None, + description="[Deprecated] The path to the file. Use file_paths instead.", + ) + file_paths: Optional[Union[Path, List[Path], str, List[str]]] = Field( + default_factory=list, description="The path to the file" + ) + chunks: List[str] = Field(default_factory=list) + content: Dict[Path, Dict[str, str]] = Field(default_factory=dict) + safe_file_paths: List[Path] = Field(default_factory=list) + + @field_validator("file_path", "file_paths", mode="before") + def validate_file_path(cls, v, info): + """Validate that at least one of file_path or file_paths is provided.""" + # Single check if both are None, O(1) instead of nested conditions + if ( + v is None + and info.data.get( + "file_path" if info.field_name == "file_paths" else "file_paths" + ) + is None + ): + raise ValueError("Either file_path or file_paths must be provided") + return v + + def _process_file_paths(self) -> List[Path]: + """Convert file_path to a list of Path objects.""" + + if hasattr(self, "file_path") and self.file_path is not None: + self._logger.log( + "warning", + "The 'file_path' attribute is deprecated and will be removed in a future version. Please use 'file_paths' instead.", + color="yellow", + ) + self.file_paths = self.file_path + + if self.file_paths is None: + raise ValueError("Your source must be provided with a file_paths: []") + + # Convert single path to list + path_list: List[Union[Path, str]] = ( + [self.file_paths] + if isinstance(self.file_paths, (str, Path)) + else list(self.file_paths) + if isinstance(self.file_paths, list) + else [] + ) + + if not path_list: + raise ValueError( + "file_path/file_paths must be a Path, str, or a list of these types" + ) + + return [self.convert_to_path(path) for path in path_list] + + def validate_content(self): + """Validate the paths.""" + for path in self.safe_file_paths: + if not path.exists(): + self._logger.log( + "error", + f"File not found: {path}. Try adding sources to the knowledge directory. If it's inside the knowledge directory, use the relative path.", + color="red", + ) + raise FileNotFoundError(f"File not found: {path}") + if not path.is_file(): + self._logger.log( + "error", + f"Path is not a file: {path}", + color="red", + ) + + def model_post_init(self, _) -> None: + if self.file_path: + self._logger.log( + "warning", + "The 'file_path' attribute is deprecated and will be removed in a future version. Please use 'file_paths' instead.", + color="yellow", + ) + self.file_paths = self.file_path + self.safe_file_paths = self._process_file_paths() + self.validate_content() + self.content = self._load_content() + + def _load_content(self) -> Dict[Path, Dict[str, str]]: + """Load and preprocess Excel file content from multiple sheets. + + Each sheet's content is converted to CSV format and stored. + + Returns: + Dict[Path, Dict[str, str]]: A mapping of file paths to their respective sheet contents. + + Raises: + ImportError: If required dependencies are missing. + FileNotFoundError: If the specified Excel file cannot be opened. + """ + pd = self._import_dependencies() content_dict = {} for file_path in self.safe_file_paths: file_path = self.convert_to_path(file_path) - df = pd.read_excel(file_path) - content = df.to_csv(index=False) - content_dict[file_path] = content + with pd.ExcelFile(file_path) as xl: + sheet_dict = { + str(sheet_name): str( + pd.read_excel(xl, sheet_name).to_csv(index=False) + ) + for sheet_name in xl.sheet_names + } + content_dict[file_path] = sheet_dict return content_dict + def convert_to_path(self, path: Union[Path, str]) -> Path: + """Convert a path to a Path object.""" + return Path(KNOWLEDGE_DIRECTORY + "/" + path) if isinstance(path, str) else path + def _import_dependencies(self): """Dynamically import dependencies.""" try: - import openpyxl # noqa import pandas as pd return pd @@ -38,10 +148,14 @@ class ExcelKnowledgeSource(BaseFileKnowledgeSource): and save the embeddings. """ # Convert dictionary values to a single string if content is a dictionary - if isinstance(self.content, dict): - content_str = "\n".join(str(value) for value in self.content.values()) - else: - content_str = str(self.content) + # Updated to account for .xlsx workbooks with multiple tabs/sheets + content_str = "" + for value in self.content.values(): + if isinstance(value, dict): + for sheet_value in value.values(): + content_str += str(sheet_value) + "\n" + else: + content_str += str(value) + "\n" new_chunks = self._chunk_text(content_str) self.chunks.extend(new_chunks) From c408368267de192f153cc608a7be5bca5be4871a Mon Sep 17 00:00:00 2001 From: "Brandon Hancock (bhancock_ai)" <109994880+bhancockio@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:10:53 -0500 Subject: [PATCH 08/10] fix linting issues in new tests (#2089) Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> --- tests/crew_test.py | 182 +++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 82 deletions(-) diff --git a/tests/crew_test.py b/tests/crew_test.py index e69c71315..0539ea347 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -51,6 +51,7 @@ writer = Agent( def test_crew_with_only_conditional_tasks_raises_error(): """Test that creating a crew with only conditional tasks raises an error.""" + def condition_func(task_output: TaskOutput) -> bool: return True @@ -82,6 +83,7 @@ def test_crew_with_only_conditional_tasks_raises_error(): tasks=[conditional1, conditional2, conditional3], ) + def test_crew_config_conditional_requirement(): with pytest.raises(ValueError): Crew(process=Process.sequential) @@ -589,12 +591,12 @@ def test_crew_with_delegating_agents_should_not_override_task_tools(): _, kwargs = mock_execute_sync.call_args tools = kwargs["tools"] - assert any(isinstance(tool, TestTool) for tool in tools), ( - "TestTool should be present" - ) - assert any("delegate" in tool.name.lower() for tool in tools), ( - "Delegation tool should be present" - ) + assert any( + isinstance(tool, TestTool) for tool in tools + ), "TestTool should be present" + assert any( + "delegate" in tool.name.lower() for tool in tools + ), "Delegation tool should be present" @pytest.mark.vcr(filter_headers=["authorization"]) @@ -653,12 +655,12 @@ def test_crew_with_delegating_agents_should_not_override_agent_tools(): _, kwargs = mock_execute_sync.call_args tools = kwargs["tools"] - assert any(isinstance(tool, TestTool) for tool in new_ceo.tools), ( - "TestTool should be present" - ) - assert any("delegate" in tool.name.lower() for tool in tools), ( - "Delegation tool should be present" - ) + assert any( + isinstance(tool, TestTool) for tool in new_ceo.tools + ), "TestTool should be present" + assert any( + "delegate" in tool.name.lower() for tool in tools + ), "Delegation tool should be present" @pytest.mark.vcr(filter_headers=["authorization"]) @@ -782,17 +784,17 @@ def test_task_tools_override_agent_tools_with_allow_delegation(): used_tools = kwargs["tools"] # Confirm AnotherTestTool is present but TestTool is not - assert any(isinstance(tool, AnotherTestTool) for tool in used_tools), ( - "AnotherTestTool should be present" - ) - assert not any(isinstance(tool, TestTool) for tool in used_tools), ( - "TestTool should not be present among used tools" - ) + assert any( + isinstance(tool, AnotherTestTool) for tool in used_tools + ), "AnotherTestTool should be present" + assert not any( + isinstance(tool, TestTool) for tool in used_tools + ), "TestTool should not be present among used tools" # Confirm delegation tool(s) are present - assert any("delegate" in tool.name.lower() for tool in used_tools), ( - "Delegation tool should be present" - ) + assert any( + "delegate" in tool.name.lower() for tool in used_tools + ), "Delegation tool should be present" # Finally, make sure the agent's original tools remain unchanged assert len(researcher_with_delegation.tools) == 1 @@ -1593,9 +1595,9 @@ def test_code_execution_flag_adds_code_tool_upon_kickoff(): # Verify that exactly one tool was used and it was a CodeInterpreterTool assert len(used_tools) == 1, "Should have exactly one tool" - assert isinstance(used_tools[0], CodeInterpreterTool), ( - "Tool should be CodeInterpreterTool" - ) + assert isinstance( + used_tools[0], CodeInterpreterTool + ), "Tool should be CodeInterpreterTool" @pytest.mark.vcr(filter_headers=["authorization"]) @@ -1952,6 +1954,7 @@ def test_task_callback_on_crew(): def test_task_callback_both_on_task_and_crew(): from unittest.mock import MagicMock, patch + mock_callback_on_task = MagicMock() mock_callback_on_crew = MagicMock() @@ -2101,21 +2104,22 @@ def test_conditional_task_uses_last_output(): expected_output="First output", agent=researcher, ) + def condition_fails(task_output: TaskOutput) -> bool: # This condition will never be met return "never matches" in task_output.raw.lower() - + def condition_succeeds(task_output: TaskOutput) -> bool: # This condition will match first task's output return "first success" in task_output.raw.lower() - + conditional_task1 = ConditionalTask( description="Second task - conditional that fails condition", expected_output="Second output", agent=researcher, condition=condition_fails, ) - + conditional_task2 = ConditionalTask( description="Third task - conditional that succeeds using first task output", expected_output="Third output", @@ -2134,35 +2138,37 @@ def test_conditional_task_uses_last_output(): raw="First success output", # Will be used by third task's condition agent=researcher.role, ) - mock_skipped = TaskOutput( - description="Second task output", - raw="", # Empty output since condition fails - agent=researcher.role, - ) mock_third = TaskOutput( description="Third task output", raw="Third task executed", # Output when condition succeeds using first task output agent=writer.role, ) - + # Set up mocks for task execution and conditional logic with patch.object(ConditionalTask, "should_execute") as mock_should_execute: # First conditional fails, second succeeds mock_should_execute.side_effect = [False, True] - with patch.object(Task, "execute_sync") as mock_execute: mock_execute.side_effect = [mock_first, mock_third] result = crew.kickoff() - + # Verify execution behavior assert mock_execute.call_count == 2 # Only first and third tasks execute assert mock_should_execute.call_count == 2 # Both conditionals checked - - # Verify outputs collection + + # Verify outputs collection: + # First executed task output, followed by an automatically generated (skipped) output, then the conditional execution assert len(result.tasks_output) == 3 - assert result.tasks_output[0].raw == "First success output" # First task succeeded - assert result.tasks_output[1].raw == "" # Second task skipped (condition failed) - assert result.tasks_output[2].raw == "Third task executed" # Third task used first task's output + assert ( + result.tasks_output[0].raw == "First success output" + ) # First task succeeded + assert ( + result.tasks_output[1].raw == "" + ) # Second task skipped (condition failed) + assert ( + result.tasks_output[2].raw == "Third task executed" + ) # Third task used first task's output + @pytest.mark.vcr(filter_headers=["authorization"]) def test_conditional_tasks_result_collection(): @@ -2172,20 +2178,20 @@ def test_conditional_tasks_result_collection(): expected_output="First output", agent=researcher, ) - + def condition_never_met(task_output: TaskOutput) -> bool: return "never matches" in task_output.raw.lower() - + def condition_always_met(task_output: TaskOutput) -> bool: return "success" in task_output.raw.lower() - + task2 = ConditionalTask( description="Conditional task that never executes", expected_output="Second output", agent=researcher, condition=condition_never_met, ) - + task3 = ConditionalTask( description="Conditional task that always executes", expected_output="Third output", @@ -2204,35 +2210,46 @@ def test_conditional_tasks_result_collection(): raw="Success output", # Triggers third task's condition agent=researcher.role, ) - mock_skipped = TaskOutput( - description="Skipped output", - raw="", # Empty output for skipped task - agent=researcher.role, - ) mock_conditional = TaskOutput( description="Conditional output", raw="Conditional task executed", agent=writer.role, ) - + # Set up mocks for task execution and conditional logic with patch.object(ConditionalTask, "should_execute") as mock_should_execute: # First conditional fails, second succeeds mock_should_execute.side_effect = [False, True] - with patch.object(Task, "execute_sync") as mock_execute: mock_execute.side_effect = [mock_success, mock_conditional] result = crew.kickoff() - + # Verify execution behavior assert mock_execute.call_count == 2 # Only first and third tasks execute assert mock_should_execute.call_count == 2 # Both conditionals checked - + + # Verify task output collection: + # There should be three outputs: normal task, skipped conditional task (empty output), + # and the conditional task that executed. + assert len(result.tasks_output) == 3 + assert ( + result.tasks_output[0].raw == "Success output" + ) # Normal task executed + assert result.tasks_output[1].raw == "" # Second task skipped + assert ( + result.tasks_output[2].raw == "Conditional task executed" + ) # Third task executed + # Verify task output collection assert len(result.tasks_output) == 3 - assert result.tasks_output[0].raw == "Success output" # Normal task executed - assert result.tasks_output[1].raw == "" # Second task skipped - assert result.tasks_output[2].raw == "Conditional task executed" # Third task executed + assert ( + result.tasks_output[0].raw == "Success output" + ) # Normal task executed + assert result.tasks_output[1].raw == "" # Second task skipped + assert ( + result.tasks_output[2].raw == "Conditional task executed" + ) # Third task executed + @pytest.mark.vcr(filter_headers=["authorization"]) def test_multiple_conditional_tasks(): @@ -2242,20 +2259,20 @@ def test_multiple_conditional_tasks(): expected_output="Research output", agent=researcher, ) - + def condition1(task_output: TaskOutput) -> bool: return "success" in task_output.raw.lower() - + def condition2(task_output: TaskOutput) -> bool: return "proceed" in task_output.raw.lower() - + task2 = ConditionalTask( description="First conditional task", expected_output="Conditional output 1", agent=writer, condition=condition1, ) - + task3 = ConditionalTask( description="Second conditional task", expected_output="Conditional output 2", @@ -2274,7 +2291,7 @@ def test_multiple_conditional_tasks(): raw="Success and proceed output", agent=researcher.role, ) - + # Set up mocks for task execution with patch.object(Task, "execute_sync", return_value=mock_success) as mock_execute: result = crew.kickoff() @@ -2282,6 +2299,7 @@ def test_multiple_conditional_tasks(): assert mock_execute.call_count == 3 assert len(result.tasks_output) == 3 + @pytest.mark.vcr(filter_headers=["authorization"]) def test_using_contextual_memory(): from unittest.mock import patch @@ -3400,9 +3418,9 @@ def test_fetch_inputs(): expected_placeholders = {"role_detail", "topic", "field"} actual_placeholders = crew.fetch_inputs() - assert actual_placeholders == expected_placeholders, ( - f"Expected {expected_placeholders}, but got {actual_placeholders}" - ) + assert ( + actual_placeholders == expected_placeholders + ), f"Expected {expected_placeholders}, but got {actual_placeholders}" def test_task_tools_preserve_code_execution_tools(): @@ -3475,20 +3493,20 @@ def test_task_tools_preserve_code_execution_tools(): used_tools = kwargs["tools"] # Verify all expected tools are present - assert any(isinstance(tool, TestTool) for tool in used_tools), ( - "Task's TestTool should be present" - ) - assert any(isinstance(tool, CodeInterpreterTool) for tool in used_tools), ( - "CodeInterpreterTool should be present" - ) - assert any("delegate" in tool.name.lower() for tool in used_tools), ( - "Delegation tool should be present" - ) + assert any( + isinstance(tool, TestTool) for tool in used_tools + ), "Task's TestTool should be present" + assert any( + isinstance(tool, CodeInterpreterTool) for tool in used_tools + ), "CodeInterpreterTool should be present" + assert any( + "delegate" in tool.name.lower() for tool in used_tools + ), "Delegation tool should be present" # Verify the total number of tools (TestTool + CodeInterpreter + 2 delegation tools) - assert len(used_tools) == 4, ( - "Should have TestTool, CodeInterpreter, and 2 delegation tools" - ) + assert ( + len(used_tools) == 4 + ), "Should have TestTool, CodeInterpreter, and 2 delegation tools" @pytest.mark.vcr(filter_headers=["authorization"]) @@ -3532,9 +3550,9 @@ def test_multimodal_flag_adds_multimodal_tools(): used_tools = kwargs["tools"] # Check that the multimodal tool was added - assert any(isinstance(tool, AddImageTool) for tool in used_tools), ( - "AddImageTool should be present when agent is multimodal" - ) + assert any( + isinstance(tool, AddImageTool) for tool in used_tools + ), "AddImageTool should be present when agent is multimodal" # Verify we have exactly one tool (just the AddImageTool) assert len(used_tools) == 1, "Should only have the AddImageTool" @@ -3760,9 +3778,9 @@ def test_crew_guardrail_feedback_in_context(): assert len(execution_contexts) > 1, "Task should have been executed multiple times" # Verify that the second execution included the guardrail feedback - assert "Output must contain the keyword 'IMPORTANT'" in execution_contexts[1], ( - "Guardrail feedback should be included in retry context" - ) + assert ( + "Output must contain the keyword 'IMPORTANT'" in execution_contexts[1] + ), "Guardrail feedback should be included in retry context" # Verify final output meets guardrail requirements assert "IMPORTANT" in result.raw, "Final output should contain required keyword" From 9b10fd47b0b5e699e671d9027f0b4ef4e1f8fe3a Mon Sep 17 00:00:00 2001 From: "Brandon Hancock (bhancock_ai)" <109994880+bhancockio@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:17:41 -0500 Subject: [PATCH 09/10] incorporate Small update in memory.mdx, fixing Google AI parameters #2008 (#2087) --- docs/concepts/memory.mdx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/concepts/memory.mdx b/docs/concepts/memory.mdx index 8db1fe33a..cdb1745ee 100644 --- a/docs/concepts/memory.mdx +++ b/docs/concepts/memory.mdx @@ -282,6 +282,19 @@ my_crew = Crew( ### Using Google AI embeddings +#### Prerequisites +Before using Google AI embeddings, ensure you have: +- Access to the Gemini API +- The necessary API keys and permissions + +You will need to update your *pyproject.toml* dependencies: +```YAML +dependencies = [ + "google-generativeai>=0.8.4", #main version in January/2025 - crewai v.0.100.0 and crewai-tools 0.33.0 + "crewai[tools]>=0.100.0,<1.0.0" +] +``` + ```python Code from crewai import Crew, Agent, Task, Process From 47818f4f41b6d7dbe31c4fab3e312870bec79709 Mon Sep 17 00:00:00 2001 From: "Brandon Hancock (bhancock_ai)" <109994880+bhancockio@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:48:12 -0500 Subject: [PATCH 10/10] updating bedrock docs (#2088) Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> --- docs/concepts/memory.mdx | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/concepts/memory.mdx b/docs/concepts/memory.mdx index cdb1745ee..ae65db290 100644 --- a/docs/concepts/memory.mdx +++ b/docs/concepts/memory.mdx @@ -447,6 +447,38 @@ my_crew = Crew( ) ``` +### Using Amazon Bedrock embeddings + +```python Code +# Note: Ensure you have installed `boto3` for Bedrock embeddings to work. + +import os +import boto3 +from crewai import Crew, Agent, Task, Process + +boto3_session = boto3.Session( + region_name=os.environ.get("AWS_REGION_NAME"), + aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY") +) + +my_crew = Crew( + agents=[...], + tasks=[...], + process=Process.sequential, + memory=True, + embedder={ + "provider": "bedrock", + "config":{ + "session": boto3_session, + "model": "amazon.titan-embed-text-v2:0", + "vector_dimension": 1024 + } + } + verbose=True +) +``` + ### Adding Custom Embedding Function ```python Code