From 33da3e1797943afb252df435934d696b44b9c1eb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 9 Feb 2025 23:45:32 +0000 Subject: [PATCH] feat: use force_final_answer prompt on timeout Co-Authored-By: Joe Moura --- src/crewai/agent.py | 23 +++++++++++++++-------- tests/task_test.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/crewai/agent.py b/src/crewai/agent.py index 01fde3fdf..aafac6105 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -66,6 +66,7 @@ class Agent(BaseAgent): """ _times_executed: int = PrivateAttr(default=0) + _have_forced_answer: bool = PrivateAttr(default=False) max_execution_time: Optional[int] = Field( default=None, description="Maximum execution time for an agent to execute a task", @@ -177,11 +178,10 @@ class Agent(BaseAgent): timeout: Maximum execution time in seconds (must be > 0) Returns: - The result of the task execution + The result of the task execution, with force_final_answer prompt appended on timeout Raises: ValueError: If timeout is not a positive integer - TimeoutError: If execution exceeds the timeout Exception: Any error that occurs during execution """ # Validate timeout before creating any resources @@ -216,8 +216,11 @@ class Agent(BaseAgent): self.agent_executor.llm = None # Release LLM resources if hasattr(self.agent_executor, 'close'): self.agent_executor.close() - - raise timeout_decorator.TimeoutError(f"Task execution timed out after {timeout} seconds") + + # Force final answer using the prompt + self._have_forced_answer = True + forced_answer = self.i18n.errors("force_final_answer") + return f"{result_container[0] if result_container[0] else ''}\n{forced_answer}" if error_container[0]: error = error_container[0] @@ -226,7 +229,7 @@ class Agent(BaseAgent): if result_container[0] is None: self._logger.log("warning", "Task execution completed but returned no result") - raise timeout_decorator.TimeoutError("Task execution completed but returned no result") + raise timeout_decorator.TimeoutError("Task execution completed but returned no result") # This is a different kind of failure than timeout return result_container[0] @@ -258,11 +261,15 @@ class Agent(BaseAgent): if hasattr(self.llm, 'timeout'): self.llm.timeout = self.max_execution_time - return self._execute_with_timeout(task, context, tools, self.max_execution_time) + result = self._execute_with_timeout(task, context, tools, self.max_execution_time) + if self._have_forced_answer: + self._logger.log("warning", f"Task '{task.description}' execution timed out after {self.max_execution_time} seconds. Using forced answer.") + return result except timeout_decorator.TimeoutError: + # This is a different kind of failure (e.g., no result at all) error_msg = ( - f"Task '{task.description}' execution timed out after {self.max_execution_time} seconds. " - f"Consider increasing max_execution_time or optimizing the task." + f"Task '{task.description}' execution timed out after {self.max_execution_time} seconds " + f"and produced no result. Consider increasing max_execution_time or optimizing the task." ) self._logger.log("error", error_msg) raise TimeoutError(error_msg) diff --git a/tests/task_test.py b/tests/task_test.py index 3936fd724..4b7fb48fe 100644 --- a/tests/task_test.py +++ b/tests/task_test.py @@ -1374,3 +1374,49 @@ def test_task_max_execution_time_zero(): with pytest.raises(TimeoutError) as excinfo: task.execute_sync(agent=researcher) assert "timed out after 1 seconds" in str(excinfo.value) + + +def test_task_force_final_answer_on_timeout(): + """Test that force_final_answer is used when task times out""" + researcher = Agent( + role="Researcher", + goal="Test goal", + backstory="Test backstory", + max_execution_time=1 # Very short timeout + ) + + task = Task( + description="Test task", + expected_output="Test output", + agent=researcher + ) + + # Mock the task execution to simulate a partial result before timeout + mock_i18n = MagicMock() + mock_i18n.errors.return_value = "MUST give your absolute best final answer" + researcher.i18n = mock_i18n + + class MockThread: + def __init__(self, target, *args, **kwargs): + self.target = target + self.daemon = kwargs.get('daemon', False) + self.args = args + self.kwargs = kwargs + + def start(self): + # Execute the target function to set the result + self.target() + + def join(self, timeout=None): + pass + + def mock_thread(*args, **kwargs): + return MockThread(*args, **kwargs) + + with patch('threading.Thread', side_effect=mock_thread), \ + patch('threading.Event.wait', return_value=False), \ + patch('litellm.completion'), \ + patch.object(Agent, '_execute_task_without_timeout', return_value="Partial result"): + result = task.execute_sync(agent=researcher) + assert "MUST give your absolute best final answer" in result.raw + assert "Partial result" in result.raw # Should include partial result