mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-02 07:42:40 +00:00
Fix issue #2379: Implement timeout mechanism for max_execution_time
Co-Authored-By: Joe Moura <joao@crewai.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import concurrent.futures
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -234,48 +235,118 @@ class Agent(BaseAgent):
|
|||||||
else:
|
else:
|
||||||
task_prompt = self._use_trained_data(task_prompt=task_prompt)
|
task_prompt = self._use_trained_data(task_prompt=task_prompt)
|
||||||
|
|
||||||
try:
|
# Prepare the invoke parameters
|
||||||
crewai_event_bus.emit(
|
invoke_params = {
|
||||||
self,
|
"input": task_prompt,
|
||||||
event=AgentExecutionStartedEvent(
|
"tool_names": self.agent_executor.tools_names,
|
||||||
agent=self,
|
"tools": self.agent_executor.tools_description,
|
||||||
tools=self.tools,
|
"ask_for_human_input": task.human_input,
|
||||||
task_prompt=task_prompt,
|
}
|
||||||
task=task,
|
|
||||||
),
|
# Emit the execution started event
|
||||||
)
|
crewai_event_bus.emit(
|
||||||
result = self.agent_executor.invoke(
|
self,
|
||||||
{
|
event=AgentExecutionStartedEvent(
|
||||||
"input": task_prompt,
|
agent=self,
|
||||||
"tool_names": self.agent_executor.tools_names,
|
tools=self.tools,
|
||||||
"tools": self.agent_executor.tools_description,
|
task_prompt=task_prompt,
|
||||||
"ask_for_human_input": task.human_input,
|
task=task,
|
||||||
}
|
),
|
||||||
)["output"]
|
)
|
||||||
except Exception as e:
|
|
||||||
if e.__class__.__module__.startswith("litellm"):
|
# If max_execution_time is set, use ThreadPoolExecutor with timeout
|
||||||
# Do not retry on litellm errors
|
if self.max_execution_time is not None:
|
||||||
crewai_event_bus.emit(
|
try:
|
||||||
self,
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
event=AgentExecutionErrorEvent(
|
future = executor.submit(self.agent_executor.invoke, invoke_params)
|
||||||
agent=self,
|
try:
|
||||||
task=task,
|
result = future.result(timeout=self.max_execution_time)["output"]
|
||||||
error=str(e),
|
except concurrent.futures.TimeoutError:
|
||||||
),
|
# Cancel the future to stop the execution
|
||||||
)
|
future.cancel()
|
||||||
raise e
|
# Define the timeout error message
|
||||||
self._times_executed += 1
|
error_message = f"Agent execution exceeded maximum time of {self.max_execution_time} seconds"
|
||||||
if self._times_executed > self.max_retry_limit:
|
# Emit the timeout error event
|
||||||
crewai_event_bus.emit(
|
crewai_event_bus.emit(
|
||||||
self,
|
self,
|
||||||
event=AgentExecutionErrorEvent(
|
event=AgentExecutionErrorEvent(
|
||||||
agent=self,
|
agent=self,
|
||||||
task=task,
|
task=task,
|
||||||
error=str(e),
|
error=error_message,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
raise e
|
# Raise a standard Exception with the timeout message
|
||||||
result = self.execute_task(task, context, tools)
|
# This avoids circular import issues while still providing the expected error message
|
||||||
|
raise Exception(f"Timeout Error: {error_message}")
|
||||||
|
except Exception as e:
|
||||||
|
# Re-raise any exceptions
|
||||||
|
if "Timeout Error:" in str(e):
|
||||||
|
raise
|
||||||
|
# For other exceptions, follow the normal retry logic
|
||||||
|
self._times_executed += 1
|
||||||
|
if self._times_executed > self.max_retry_limit:
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=AgentExecutionErrorEvent(
|
||||||
|
agent=self,
|
||||||
|
task=task,
|
||||||
|
error=str(e),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
return self.execute_task(task, context, tools)
|
||||||
|
except Exception as e:
|
||||||
|
if e.__class__.__module__.startswith("litellm"):
|
||||||
|
# Do not retry on litellm errors
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=AgentExecutionErrorEvent(
|
||||||
|
agent=self,
|
||||||
|
task=task,
|
||||||
|
error=str(e),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
self._times_executed += 1
|
||||||
|
if self._times_executed > self.max_retry_limit:
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=AgentExecutionErrorEvent(
|
||||||
|
agent=self,
|
||||||
|
task=task,
|
||||||
|
error=str(e),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
result = self.execute_task(task, context, tools)
|
||||||
|
else:
|
||||||
|
# No timeout, execute normally
|
||||||
|
try:
|
||||||
|
result = self.agent_executor.invoke(invoke_params)["output"]
|
||||||
|
except Exception as e:
|
||||||
|
if e.__class__.__module__.startswith("litellm"):
|
||||||
|
# Do not retry on litellm errors
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=AgentExecutionErrorEvent(
|
||||||
|
agent=self,
|
||||||
|
task=task,
|
||||||
|
error=str(e),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
self._times_executed += 1
|
||||||
|
if self._times_executed > self.max_retry_limit:
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=AgentExecutionErrorEvent(
|
||||||
|
agent=self,
|
||||||
|
task=task,
|
||||||
|
error=str(e),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
result = self.execute_task(task, context, tools)
|
||||||
|
|
||||||
if self.max_rpm and self._rpm_controller:
|
if self.max_rpm and self._rpm_controller:
|
||||||
self._rpm_controller.stop_rpm_counter()
|
self._rpm_controller.stop_rpm_counter()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from .rpm_controller import RPMController
|
|||||||
from .exceptions.context_window_exceeding_exception import (
|
from .exceptions.context_window_exceeding_exception import (
|
||||||
LLMContextLengthExceededException,
|
LLMContextLengthExceededException,
|
||||||
)
|
)
|
||||||
|
from .exceptions.agent_execution_timeout_error import AgentExecutionTimeoutError
|
||||||
from .embedding_configurator import EmbeddingConfigurator
|
from .embedding_configurator import EmbeddingConfigurator
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -24,5 +25,6 @@ __all__ = [
|
|||||||
"RPMController",
|
"RPMController",
|
||||||
"YamlParser",
|
"YamlParser",
|
||||||
"LLMContextLengthExceededException",
|
"LLMContextLengthExceededException",
|
||||||
|
"AgentExecutionTimeoutError",
|
||||||
"EmbeddingConfigurator",
|
"EmbeddingConfigurator",
|
||||||
]
|
]
|
||||||
|
|||||||
1
src/crewai/utilities/exceptions/__init__.py
Normal file
1
src/crewai/utilities/exceptions/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# This file is intentionally left empty to make the directory a Python package
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class AgentExecutionTimeoutError(Exception):
|
||||||
|
"""Exception raised when an agent execution exceeds the maximum allowed time."""
|
||||||
|
|
||||||
|
def __init__(self, max_execution_time: int, message: str = None):
|
||||||
|
self.max_execution_time = max_execution_time
|
||||||
|
self.message = message or f"Agent execution exceeded maximum allowed time of {max_execution_time} seconds"
|
||||||
|
super().__init__(self.message)
|
||||||
1
tests/test_timeout/__init__.py
Normal file
1
tests/test_timeout/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# This file is intentionally left empty to make the directory a Python package
|
||||||
58
tests/test_timeout/test_agent_timeout.py
Normal file
58
tests/test_timeout/test_agent_timeout.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from crewai import Agent, Task
|
||||||
|
|
||||||
|
def test_agent_max_execution_time():
|
||||||
|
"""Test that max_execution_time parameter is enforced."""
|
||||||
|
# Create a simple test function that will be used to simulate a long-running task
|
||||||
|
def test_timeout():
|
||||||
|
# Create an agent with a 1-second timeout
|
||||||
|
with patch('crewai.agent.Agent.create_agent_executor'):
|
||||||
|
agent = Agent(
|
||||||
|
role="Test Agent",
|
||||||
|
goal="Test timeout functionality",
|
||||||
|
backstory="I am testing the timeout functionality",
|
||||||
|
max_execution_time=1,
|
||||||
|
verbose=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a task that will take longer than 1 second
|
||||||
|
task = Task(
|
||||||
|
description="Sleep for 5 seconds and then return a result",
|
||||||
|
expected_output="The result after sleeping",
|
||||||
|
agent=agent
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the agent_executor to simulate a long-running task
|
||||||
|
mock_executor = MagicMock()
|
||||||
|
def side_effect(*args, **kwargs):
|
||||||
|
# Sleep for longer than the timeout to trigger the timeout mechanism
|
||||||
|
time.sleep(2)
|
||||||
|
return {"output": "This should never be returned due to timeout"}
|
||||||
|
|
||||||
|
mock_executor.invoke.side_effect = side_effect
|
||||||
|
mock_executor.tools_names = []
|
||||||
|
mock_executor.tools_description = []
|
||||||
|
|
||||||
|
# Replace the agent's executor with our mock
|
||||||
|
agent.agent_executor = mock_executor
|
||||||
|
|
||||||
|
# Mock the event bus to avoid any real event emissions
|
||||||
|
with patch('crewai.agent.crewai_event_bus'):
|
||||||
|
# Execute the task and measure the time
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# We expect an Exception to be raised due to timeout
|
||||||
|
with pytest.raises(Exception) as excinfo:
|
||||||
|
agent.execute_task(task)
|
||||||
|
|
||||||
|
# Check that the execution time is close to 1 second (the timeout)
|
||||||
|
execution_time = time.time() - start_time
|
||||||
|
assert execution_time <= 2.1, f"Execution took {execution_time:.2f} seconds, expected ~1 second"
|
||||||
|
|
||||||
|
# Check that the exception message mentions timeout
|
||||||
|
assert "timeout" in str(excinfo.value).lower() or "execution time" in str(excinfo.value).lower()
|
||||||
|
|
||||||
|
# Run the test function
|
||||||
|
test_timeout()
|
||||||
Reference in New Issue
Block a user