From 6bfc98e960fd6bd78978d9131005d2eb2b5cb7e5 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 4 Feb 2026 15:40:22 -0500 Subject: [PATCH 1/9] refactor: extract hitl to provider pattern * refactor: extract hitl to provider pattern - add humaninputprovider protocol with setup_messages and handle_feedback - move sync hitl logic from executor to synchuman inputprovider - add _passthrough_exceptions extension point in agent/core.py - create crewai.core.providers module for extensible components - remove _ask_human_input from base_agent_executor_mixin --- lib/crewai/src/crewai/agent/core.py | 6 + .../base_agent_executor_mixin.py | 50 --- .../src/crewai/agents/crew_agent_executor.py | 121 ++----- lib/crewai/src/crewai/core/__init__.py | 1 + .../src/crewai/core/providers/__init__.py | 1 + .../core/providers/content_processor.py | 78 +++++ .../src/crewai/core/providers/human_input.py | 304 ++++++++++++++++++ lib/crewai/src/crewai/task.py | 20 +- lib/crewai/tests/agents/test_agent.py | 18 +- .../test_flow_human_input_integration.py | 57 ++-- 10 files changed, 465 insertions(+), 191 deletions(-) create mode 100644 lib/crewai/src/crewai/core/__init__.py create mode 100644 lib/crewai/src/crewai/core/providers/__init__.py create mode 100644 lib/crewai/src/crewai/core/providers/content_processor.py create mode 100644 lib/crewai/src/crewai/core/providers/human_input.py diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index 6c2626a28..47eb841b4 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -118,6 +118,8 @@ MCP_TOOL_EXECUTION_TIMEOUT: Final[int] = 30 MCP_DISCOVERY_TIMEOUT: Final[int] = 15 MCP_MAX_RETRIES: Final[int] = 3 +_passthrough_exceptions: tuple[type[Exception], ...] = () + # Simple in-memory cache for MCP tool schemas (duration: 5 minutes) _mcp_schema_cache: dict[str, Any] = {} _cache_ttl: Final[int] = 300 # 5 minutes @@ -479,6 +481,8 @@ class Agent(BaseAgent): ), ) raise e + if isinstance(e, _passthrough_exceptions): + raise self._times_executed += 1 if self._times_executed > self.max_retry_limit: crewai_event_bus.emit( @@ -711,6 +715,8 @@ class Agent(BaseAgent): ), ) raise e + if isinstance(e, _passthrough_exceptions): + raise self._times_executed += 1 if self._times_executed > self.max_retry_limit: crewai_event_bus.emit( diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py index c9dceaa84..03787c802 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py +++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py @@ -4,7 +4,6 @@ import time from typing import TYPE_CHECKING from crewai.agents.parser import AgentFinish -from crewai.events.event_listener import event_listener from crewai.memory.entity.entity_memory_item import EntityMemoryItem from crewai.memory.long_term.long_term_memory_item import LongTermMemoryItem from crewai.utilities.converter import ConverterError @@ -138,52 +137,3 @@ class CrewAgentExecutorMixin: content="Long term memory is enabled, but entity memory is not enabled. Please configure entity memory or set memory=True to automatically enable it.", color="bold_yellow", ) - - def _ask_human_input(self, final_answer: str) -> str: - """Prompt human input with mode-appropriate messaging. - - Note: The final answer is already displayed via the AgentLogsExecutionEvent - panel, so we only show the feedback prompt here. - """ - from rich.panel import Panel - from rich.text import Text - - formatter = event_listener.formatter - formatter.pause_live_updates() - - try: - # Training mode prompt (single iteration) - if self.crew and getattr(self.crew, "_train", False): - prompt_text = ( - "TRAINING MODE: Provide feedback to improve the agent's performance.\n\n" - "This will be used to train better versions of the agent.\n" - "Please provide detailed feedback about the result quality and reasoning process." - ) - title = "🎓 Training Feedback Required" - # Regular human-in-the-loop prompt (multiple iterations) - else: - prompt_text = ( - "Provide feedback on the Final Result above.\n\n" - "• If you are happy with the result, simply hit Enter without typing anything.\n" - "• Otherwise, provide specific improvement requests.\n" - "• You can provide multiple rounds of feedback until satisfied." - ) - title = "💬 Human Feedback Required" - - content = Text() - content.append(prompt_text, style="yellow") - - prompt_panel = Panel( - content, - title=title, - border_style="yellow", - padding=(1, 2), - ) - formatter.console.print(prompt_panel) - - response = input() - if response.strip() != "": - formatter.console.print("\n[cyan]Processing your feedback...[/cyan]") - return response - finally: - formatter.resume_live_updates() diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 1218ceae8..c0f24516a 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -19,6 +19,7 @@ from crewai.agents.parser import ( AgentFinish, OutputParserError, ) +from crewai.core.providers.human_input import ExecutorContext, get_provider from crewai.events.event_bus import crewai_event_bus from crewai.events.types.logging_events import ( AgentLogsExecutionEvent, @@ -175,15 +176,16 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): """ return self.llm.supports_stop_words() if self.llm else False - def invoke(self, inputs: dict[str, Any]) -> dict[str, Any]: - """Execute the agent with given inputs. + def _setup_messages(self, inputs: dict[str, Any]) -> None: + """Set up messages for the agent execution. Args: inputs: Input dictionary containing prompt variables. - - Returns: - Dictionary with agent output. """ + provider = get_provider() + if provider.setup_messages(cast(ExecutorContext, cast(object, self))): + return + if "system" in self.prompt: system_prompt = self._format_prompt( cast(str, self.prompt.get("system", "")), inputs @@ -197,6 +199,19 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): user_prompt = self._format_prompt(self.prompt.get("prompt", ""), inputs) self.messages.append(format_message_for_llm(user_prompt)) + provider.post_setup_messages(cast(ExecutorContext, cast(object, self))) + + def invoke(self, inputs: dict[str, Any]) -> dict[str, Any]: + """Execute the agent with given inputs. + + Args: + inputs: Input dictionary containing prompt variables. + + Returns: + Dictionary with agent output. + """ + self._setup_messages(inputs) + self._inject_multimodal_files(inputs) self._show_start_logs() @@ -970,18 +985,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): Returns: Dictionary with agent output. """ - if "system" in self.prompt: - system_prompt = self._format_prompt( - cast(str, self.prompt.get("system", "")), inputs - ) - user_prompt = self._format_prompt( - cast(str, self.prompt.get("user", "")), inputs - ) - self.messages.append(format_message_for_llm(system_prompt, role="system")) - self.messages.append(format_message_for_llm(user_prompt)) - else: - user_prompt = self._format_prompt(self.prompt.get("prompt", ""), inputs) - self.messages.append(format_message_for_llm(user_prompt)) + self._setup_messages(inputs) await self._ainject_multimodal_files(inputs) @@ -1491,7 +1495,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): return prompt.replace("{tools}", inputs["tools"]) def _handle_human_feedback(self, formatted_answer: AgentFinish) -> AgentFinish: - """Process human feedback. + """Process human feedback via the configured provider. Args: formatted_answer: Initial agent result. @@ -1499,17 +1503,8 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): Returns: Final answer after feedback. """ - output_str = ( - formatted_answer.output - if isinstance(formatted_answer.output, str) - else formatted_answer.output.model_dump_json() - ) - human_feedback = self._ask_human_input(output_str) - - if self._is_training_mode(): - return self._handle_training_feedback(formatted_answer, human_feedback) - - return self._handle_regular_feedback(formatted_answer, human_feedback) + provider = get_provider() + return provider.handle_feedback(formatted_answer, self) def _is_training_mode(self) -> bool: """Check if training mode is active. @@ -1519,74 +1514,18 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): """ return bool(self.crew and self.crew._train) - def _handle_training_feedback( - self, initial_answer: AgentFinish, feedback: str - ) -> AgentFinish: - """Process training feedback. + def _format_feedback_message(self, feedback: str) -> LLMMessage: + """Format feedback as a message for the LLM. Args: - initial_answer: Initial agent output. - feedback: Training feedback. + feedback: User feedback string. Returns: - Improved answer. + Formatted message dict. """ - self._handle_crew_training_output(initial_answer, feedback) - self.messages.append( - format_message_for_llm( - self._i18n.slice("feedback_instructions").format(feedback=feedback) - ) + return format_message_for_llm( + self._i18n.slice("feedback_instructions").format(feedback=feedback) ) - improved_answer = self._invoke_loop() - self._handle_crew_training_output(improved_answer) - self.ask_for_human_input = False - return improved_answer - - def _handle_regular_feedback( - self, current_answer: AgentFinish, initial_feedback: str - ) -> AgentFinish: - """Process regular feedback iteratively. - - Args: - current_answer: Current agent output. - initial_feedback: Initial user feedback. - - Returns: - Final answer after iterations. - """ - feedback = initial_feedback - answer = current_answer - - while self.ask_for_human_input: - # If the user provides a blank response, assume they are happy with the result - if feedback.strip() == "": - self.ask_for_human_input = False - else: - answer = self._process_feedback_iteration(feedback) - output_str = ( - answer.output - if isinstance(answer.output, str) - else answer.output.model_dump_json() - ) - feedback = self._ask_human_input(output_str) - - return answer - - def _process_feedback_iteration(self, feedback: str) -> AgentFinish: - """Process single feedback iteration. - - Args: - feedback: User feedback. - - Returns: - Updated agent response. - """ - self.messages.append( - format_message_for_llm( - self._i18n.slice("feedback_instructions").format(feedback=feedback) - ) - ) - return self._invoke_loop() @classmethod def __get_pydantic_core_schema__( diff --git a/lib/crewai/src/crewai/core/__init__.py b/lib/crewai/src/crewai/core/__init__.py new file mode 100644 index 000000000..714ed9161 --- /dev/null +++ b/lib/crewai/src/crewai/core/__init__.py @@ -0,0 +1 @@ +"""Core crewAI components and interfaces.""" diff --git a/lib/crewai/src/crewai/core/providers/__init__.py b/lib/crewai/src/crewai/core/providers/__init__.py new file mode 100644 index 000000000..fc0a9ed4b --- /dev/null +++ b/lib/crewai/src/crewai/core/providers/__init__.py @@ -0,0 +1 @@ +"""Provider interfaces for extensible crewAI components.""" diff --git a/lib/crewai/src/crewai/core/providers/content_processor.py b/lib/crewai/src/crewai/core/providers/content_processor.py new file mode 100644 index 000000000..828a7e311 --- /dev/null +++ b/lib/crewai/src/crewai/core/providers/content_processor.py @@ -0,0 +1,78 @@ +"""Content processor provider for extensible content processing.""" + +from contextvars import ContextVar +from typing import Any, Protocol, runtime_checkable + + +@runtime_checkable +class ContentProcessorProvider(Protocol): + """Protocol for content processing during task execution.""" + + def process(self, content: str, context: dict[str, Any] | None = None) -> str: + """Process content before use. + + Args: + content: The content to process. + context: Optional context information. + + Returns: + The processed content. + """ + ... + + +class NoOpContentProcessor: + """Default processor that returns content unchanged.""" + + def process(self, content: str, context: dict[str, Any] | None = None) -> str: + """Return content unchanged. + + Args: + content: The content to process. + context: Optional context information (unused). + + Returns: + The original content unchanged. + """ + return content + + +_content_processor: ContextVar[ContentProcessorProvider | None] = ContextVar( + "_content_processor", default=None +) + +_default_processor = NoOpContentProcessor() + + +def get_processor() -> ContentProcessorProvider: + """Get the current content processor. + + Returns: + The registered content processor or the default no-op processor. + """ + processor = _content_processor.get() + if processor is not None: + return processor + return _default_processor + + +def set_processor(processor: ContentProcessorProvider) -> None: + """Set the content processor for the current context. + + Args: + processor: The content processor to use. + """ + _content_processor.set(processor) + + +def process_content(content: str, context: dict[str, Any] | None = None) -> str: + """Process content using the registered processor. + + Args: + content: The content to process. + context: Optional context information. + + Returns: + The processed content. + """ + return get_processor().process(content, context) diff --git a/lib/crewai/src/crewai/core/providers/human_input.py b/lib/crewai/src/crewai/core/providers/human_input.py new file mode 100644 index 000000000..4062e6bb9 --- /dev/null +++ b/lib/crewai/src/crewai/core/providers/human_input.py @@ -0,0 +1,304 @@ +"""Human input provider for HITL (Human-in-the-Loop) flows.""" + +from __future__ import annotations + +from contextvars import ContextVar, Token +from typing import TYPE_CHECKING, Protocol, runtime_checkable + + +if TYPE_CHECKING: + from crewai.agent.core import Agent + from crewai.agents.parser import AgentFinish + from crewai.crew import Crew + from crewai.llms.base_llm import BaseLLM + from crewai.task import Task + from crewai.utilities.types import LLMMessage + + +class ExecutorContext(Protocol): + """Context interface for human input providers to interact with executor.""" + + task: Task | None + crew: Crew | None + messages: list[LLMMessage] + ask_for_human_input: bool + llm: BaseLLM + agent: Agent + + def _invoke_loop(self) -> AgentFinish: + """Invoke the agent loop and return the result.""" + ... + + def _is_training_mode(self) -> bool: + """Check if training mode is active.""" + ... + + def _handle_crew_training_output( + self, + result: AgentFinish, + human_feedback: str | None = None, + ) -> None: + """Handle training output.""" + ... + + def _format_feedback_message(self, feedback: str) -> LLMMessage: + """Format feedback as a message.""" + ... + + +@runtime_checkable +class HumanInputProvider(Protocol): + """Protocol for human input handling. + + Implementations handle the full feedback flow: + - Sync: prompt user, loop until satisfied + - Async: raise exception for external handling + """ + + def setup_messages(self, context: ExecutorContext) -> bool: + """Set up messages for execution. + + Called before standard message setup. Allows providers to handle + conversation resumption or other custom message initialization. + + Args: + context: Executor context with messages list to modify. + + Returns: + True if messages were set up (skip standard setup), + False to use standard setup. + """ + ... + + def post_setup_messages(self, context: ExecutorContext) -> None: + """Called after standard message setup. + + Allows providers to modify messages after standard setup completes. + Only called when setup_messages returned False. + + Args: + context: Executor context with messages list to modify. + """ + ... + + def handle_feedback( + self, + formatted_answer: AgentFinish, + context: ExecutorContext, + ) -> AgentFinish: + """Handle the full human feedback flow. + + Args: + formatted_answer: The agent's current answer. + context: Executor context for callbacks. + + Returns: + The final answer after feedback processing. + + Raises: + Exception: Async implementations may raise to signal external handling. + """ + ... + + @staticmethod + def _get_output_string(answer: AgentFinish) -> str: + """Extract output string from answer. + + Args: + answer: The agent's finished answer. + + Returns: + String representation of the output. + """ + if isinstance(answer.output, str): + return answer.output + return answer.output.model_dump_json() + + +class SyncHumanInputProvider(HumanInputProvider): + """Default synchronous human input via terminal.""" + + def setup_messages(self, context: ExecutorContext) -> bool: + """Use standard message setup. + + Args: + context: Executor context (unused). + + Returns: + False to use standard setup. + """ + return False + + def post_setup_messages(self, context: ExecutorContext) -> None: + """No-op for sync provider. + + Args: + context: Executor context (unused). + """ + + def handle_feedback( + self, + formatted_answer: AgentFinish, + context: ExecutorContext, + ) -> AgentFinish: + """Handle feedback synchronously with terminal prompts. + + Args: + formatted_answer: The agent's current answer. + context: Executor context for callbacks. + + Returns: + The final answer after feedback processing. + """ + feedback = self._prompt_input(context.crew) + + if context._is_training_mode(): + return self._handle_training_feedback(formatted_answer, feedback, context) + + return self._handle_regular_feedback(formatted_answer, feedback, context) + + @staticmethod + def _handle_training_feedback( + initial_answer: AgentFinish, + feedback: str, + context: ExecutorContext, + ) -> AgentFinish: + """Process training feedback (single iteration). + + Args: + initial_answer: The agent's initial answer. + feedback: Human feedback string. + context: Executor context for callbacks. + + Returns: + Improved answer after processing feedback. + """ + context._handle_crew_training_output(initial_answer, feedback) + context.messages.append(context._format_feedback_message(feedback)) + improved_answer = context._invoke_loop() + context._handle_crew_training_output(improved_answer) + context.ask_for_human_input = False + return improved_answer + + def _handle_regular_feedback( + self, + current_answer: AgentFinish, + initial_feedback: str, + context: ExecutorContext, + ) -> AgentFinish: + """Process regular feedback with iteration loop. + + Args: + current_answer: The agent's current answer. + initial_feedback: Initial human feedback string. + context: Executor context for callbacks. + + Returns: + Final answer after all feedback iterations. + """ + feedback = initial_feedback + answer = current_answer + + while context.ask_for_human_input: + if feedback.strip() == "": + context.ask_for_human_input = False + else: + context.messages.append(context._format_feedback_message(feedback)) + answer = context._invoke_loop() + feedback = self._prompt_input(context.crew) + + return answer + + @staticmethod + def _prompt_input(crew: Crew | None) -> str: + """Show rich panel and prompt for input. + + Args: + crew: The crew instance for context. + + Returns: + User input string from terminal. + """ + from rich.panel import Panel + from rich.text import Text + + from crewai.events.event_listener import event_listener + + formatter = event_listener.formatter + formatter.pause_live_updates() + + try: + if crew and getattr(crew, "_train", False): + prompt_text = ( + "TRAINING MODE: Provide feedback to improve the agent's performance.\n\n" + "This will be used to train better versions of the agent.\n" + "Please provide detailed feedback about the result quality and reasoning process." + ) + title = "🎓 Training Feedback Required" + else: + prompt_text = ( + "Provide feedback on the Final Result above.\n\n" + "• If you are happy with the result, simply hit Enter without typing anything.\n" + "• Otherwise, provide specific improvement requests.\n" + "• You can provide multiple rounds of feedback until satisfied." + ) + title = "💬 Human Feedback Required" + + content = Text() + content.append(prompt_text, style="yellow") + + prompt_panel = Panel( + content, + title=title, + border_style="yellow", + padding=(1, 2), + ) + formatter.console.print(prompt_panel) + + response = input() + if response.strip() != "": + formatter.console.print("\n[cyan]Processing your feedback...[/cyan]") + return response + finally: + formatter.resume_live_updates() + + +_provider: ContextVar[HumanInputProvider | None] = ContextVar( + "human_input_provider", + default=None, +) + + +def get_provider() -> HumanInputProvider: + """Get the current human input provider. + + Returns: + The current provider, or a new SyncHumanInputProvider if none set. + """ + provider = _provider.get() + if provider is None: + initialized_provider = SyncHumanInputProvider() + set_provider(initialized_provider) + return initialized_provider + return provider + + +def set_provider(provider: HumanInputProvider) -> Token[HumanInputProvider | None]: + """Set the human input provider for the current context. + + Args: + provider: The provider to use. + + Returns: + Token that can be used to reset to previous value. + """ + return _provider.set(provider) + + +def reset_provider(token: Token[HumanInputProvider | None]) -> None: + """Reset the provider to its previous value. + + Args: + token: Token returned from set_provider. + """ + _provider.reset(token) diff --git a/lib/crewai/src/crewai/task.py b/lib/crewai/src/crewai/task.py index 77056f0ca..d73c3d919 100644 --- a/lib/crewai/src/crewai/task.py +++ b/lib/crewai/src/crewai/task.py @@ -31,6 +31,7 @@ from pydantic_core import PydanticCustomError from typing_extensions import Self from crewai.agents.agent_builder.base_agent import BaseAgent +from crewai.core.providers.content_processor import process_content from crewai.events.event_bus import crewai_event_bus from crewai.events.types.task_events import ( TaskCompletedEvent, @@ -496,6 +497,7 @@ class Task(BaseModel): tools: list[BaseTool] | None = None, ) -> TaskOutput: """Execute the task synchronously.""" + self.start_time = datetime.datetime.now() return self._execute_core(agent, context, tools) @property @@ -536,6 +538,7 @@ class Task(BaseModel): ) -> None: """Execute the task asynchronously with context handling.""" try: + self.start_time = datetime.datetime.now() result = self._execute_core(agent, context, tools) future.set_result(result) except Exception as e: @@ -548,6 +551,7 @@ class Task(BaseModel): tools: list[BaseTool] | None = None, ) -> TaskOutput: """Execute the task asynchronously using native async/await.""" + self.start_time = datetime.datetime.now() return await self._aexecute_core(agent, context, tools) async def _aexecute_core( @@ -566,8 +570,6 @@ class Task(BaseModel): f"The task '{self.description}' has no agent assigned, therefore it can't be executed directly and should be executed in a Crew using a specific process that support that, like hierarchical." ) - self.start_time = datetime.datetime.now() - self.prompt_context = context tools = tools or self.tools or [] @@ -579,6 +581,8 @@ class Task(BaseModel): tools=tools, ) + self._post_agent_execution(agent) + if not self._guardrails and not self._guardrail: pydantic_output, json_output = self._export_output(result) else: @@ -661,8 +665,6 @@ class Task(BaseModel): f"The task '{self.description}' has no agent assigned, therefore it can't be executed directly and should be executed in a Crew using a specific process that support that, like hierarchical." ) - self.start_time = datetime.datetime.now() - self.prompt_context = context tools = tools or self.tools or [] @@ -674,6 +676,8 @@ class Task(BaseModel): tools=tools, ) + self._post_agent_execution(agent) + if not self._guardrails and not self._guardrail: pydantic_output, json_output = self._export_output(result) else: @@ -741,6 +745,9 @@ class Task(BaseModel): finally: clear_task_files(self.id) + def _post_agent_execution(self, agent: BaseAgent) -> None: + pass + def prompt(self) -> str: """Generates the task prompt with optional markdown formatting. @@ -863,6 +870,11 @@ Follow these guidelines: except ValueError as e: raise ValueError(f"Error interpolating description: {e!s}") from e + self.description = process_content(self.description, {"task": self}) + self._original_expected_output = process_content( + self._original_expected_output, {"task": self} + ) + try: self.expected_output = interpolate_only( input_string=self._original_expected_output, inputs=inputs diff --git a/lib/crewai/tests/agents/test_agent.py b/lib/crewai/tests/agents/test_agent.py index 32130f900..025bfd334 100644 --- a/lib/crewai/tests/agents/test_agent.py +++ b/lib/crewai/tests/agents/test_agent.py @@ -703,6 +703,8 @@ def test_agent_definition_based_on_dict(): # test for human input @pytest.mark.vcr() def test_agent_human_input(): + from crewai.core.providers.human_input import SyncHumanInputProvider + # Agent configuration config = { "role": "test role", @@ -720,7 +722,7 @@ def test_agent_human_input(): human_input=True, ) - # Side effect function for _ask_human_input to simulate multiple feedback iterations + # Side effect function for _prompt_input to simulate multiple feedback iterations feedback_responses = iter( [ "Don't say hi, say Hello instead!", # First feedback: instruct change @@ -728,16 +730,16 @@ def test_agent_human_input(): ] ) - def ask_human_input_side_effect(*args, **kwargs): + def prompt_input_side_effect(*args, **kwargs): return next(feedback_responses) - # Patch both _ask_human_input and _invoke_loop to avoid real API/network calls. + # Patch both _prompt_input on provider and _invoke_loop to avoid real API/network calls. with ( patch.object( - CrewAgentExecutor, - "_ask_human_input", - side_effect=ask_human_input_side_effect, - ) as mock_human_input, + SyncHumanInputProvider, + "_prompt_input", + side_effect=prompt_input_side_effect, + ) as mock_prompt_input, patch.object( CrewAgentExecutor, "_invoke_loop", @@ -749,7 +751,7 @@ def test_agent_human_input(): # Assertions to ensure the agent behaves correctly. # It should have requested feedback twice. - assert mock_human_input.call_count == 2 + assert mock_prompt_input.call_count == 2 # The final result should be processed to "Hello" assert output.strip().lower() == "hello" diff --git a/lib/crewai/tests/test_flow_human_input_integration.py b/lib/crewai/tests/test_flow_human_input_integration.py index e60cfe514..3ce4ebbd7 100644 --- a/lib/crewai/tests/test_flow_human_input_integration.py +++ b/lib/crewai/tests/test_flow_human_input_integration.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch import pytest from crewai.events.event_listener import event_listener +from crewai.core.providers.human_input import SyncHumanInputProvider class TestFlowHumanInputIntegration: @@ -24,14 +25,9 @@ class TestFlowHumanInputIntegration: @patch("builtins.input", return_value="") def test_human_input_pauses_flow_updates(self, mock_input): """Test that human input pauses Flow status updates.""" - from crewai.agents.agent_builder.base_agent_executor_mixin import ( - CrewAgentExecutorMixin, - ) - - executor = CrewAgentExecutorMixin() - executor.crew = MagicMock() - executor.crew._train = False - executor._printer = MagicMock() + provider = SyncHumanInputProvider() + crew = MagicMock() + crew._train = False formatter = event_listener.formatter @@ -39,7 +35,7 @@ class TestFlowHumanInputIntegration: patch.object(formatter, "pause_live_updates") as mock_pause, patch.object(formatter, "resume_live_updates") as mock_resume, ): - result = executor._ask_human_input("Test result") + result = provider._prompt_input(crew) mock_pause.assert_called_once() mock_resume.assert_called_once() @@ -49,14 +45,9 @@ class TestFlowHumanInputIntegration: @patch("builtins.input", side_effect=["feedback", ""]) def test_multiple_human_input_rounds(self, mock_input): """Test multiple rounds of human input with Flow status management.""" - from crewai.agents.agent_builder.base_agent_executor_mixin import ( - CrewAgentExecutorMixin, - ) - - executor = CrewAgentExecutorMixin() - executor.crew = MagicMock() - executor.crew._train = False - executor._printer = MagicMock() + provider = SyncHumanInputProvider() + crew = MagicMock() + crew._train = False formatter = event_listener.formatter @@ -75,10 +66,10 @@ class TestFlowHumanInputIntegration: formatter, "resume_live_updates", side_effect=track_resume ), ): - result1 = executor._ask_human_input("Test result 1") + result1 = provider._prompt_input(crew) assert result1 == "feedback" - result2 = executor._ask_human_input("Test result 2") + result2 = provider._prompt_input(crew) assert result2 == "" assert len(pause_calls) == 2 @@ -103,14 +94,9 @@ class TestFlowHumanInputIntegration: def test_pause_resume_exception_handling(self): """Test that resume is called even if exception occurs during human input.""" - from crewai.agents.agent_builder.base_agent_executor_mixin import ( - CrewAgentExecutorMixin, - ) - - executor = CrewAgentExecutorMixin() - executor.crew = MagicMock() - executor.crew._train = False - executor._printer = MagicMock() + provider = SyncHumanInputProvider() + crew = MagicMock() + crew._train = False formatter = event_listener.formatter @@ -122,21 +108,16 @@ class TestFlowHumanInputIntegration: ), ): with pytest.raises(KeyboardInterrupt): - executor._ask_human_input("Test result") + provider._prompt_input(crew) mock_pause.assert_called_once() mock_resume.assert_called_once() def test_training_mode_human_input(self): """Test human input in training mode.""" - from crewai.agents.agent_builder.base_agent_executor_mixin import ( - CrewAgentExecutorMixin, - ) - - executor = CrewAgentExecutorMixin() - executor.crew = MagicMock() - executor.crew._train = True - executor._printer = MagicMock() + provider = SyncHumanInputProvider() + crew = MagicMock() + crew._train = True formatter = event_listener.formatter @@ -146,7 +127,7 @@ class TestFlowHumanInputIntegration: patch.object(formatter.console, "print") as mock_console_print, patch("builtins.input", return_value="training feedback"), ): - result = executor._ask_human_input("Test result") + result = provider._prompt_input(crew) mock_pause.assert_called_once() mock_resume.assert_called_once() @@ -161,4 +142,4 @@ class TestFlowHumanInputIntegration: for call in call_args if call[0] ) - assert training_panel_found + assert training_panel_found \ No newline at end of file From d86d43d3e0918fe2c17001edd920c438487813b8 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 4 Feb 2026 16:05:21 -0500 Subject: [PATCH 2/9] chore: refactor crew to provider Enable dynamic extension exports and small behavior fixes across events and flow modules: - events/__init__.py: Added _extension_exports and extended __getattr__ to lazily resolve registered extension values or import paths. - events/event_bus.py: Implemented off() to unregister sync/async handlers, clean handler dependencies, and invalidate execution plan cache. - events/listeners/tracing/utils.py: Added Callable import and _first_time_trace_hook to allow overriding first-time trace auto-collection behavior. - events/types/tool_usage_events.py: Changed ToolUsageEvent.run_attempts default from None to 0 to avoid nullable handling. - events/utils/console_formatter.py: Respect CREWAI_DISABLE_VERSION_CHECK env var to skip version checks in CI-like flows. - flow/async_feedback/__init__.py: Added typing.Any import, _extension_exports and __getattr__ to support extensions via attribute lookup. These changes add extension points and safer defaults, and provide a way to unregister event handlers. --- lib/crewai/src/crewai/crew.py | 19 ++++- lib/crewai/src/crewai/events/__init__.py | 18 ++++- lib/crewai/src/crewai/events/event_bus.py | 33 +++++++++ .../crewai/events/listeners/tracing/utils.py | 7 +- .../crewai/events/types/tool_usage_events.py | 8 +-- .../crewai/events/utils/console_formatter.py | 3 + .../crewai/flow/async_feedback/__init__.py | 13 ++++ .../crewai/knowledge/source/utils/__init__.py | 1 + .../knowledge/source/utils/source_helper.py | 70 +++++++++++++++++++ .../evaluators/crew_evaluator_handler.py | 16 +++-- 10 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 lib/crewai/src/crewai/knowledge/source/utils/__init__.py create mode 100644 lib/crewai/src/crewai/knowledge/source/utils/source_helper.py diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index f22dd4b72..baabce190 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -751,6 +751,8 @@ class Crew(FlowTrackable, BaseModel): for after_callback in self.after_kickoff_callbacks: result = after_callback(result) + result = self._post_kickoff(result) + self.usage_metrics = self.calculate_usage_metrics() return result @@ -764,6 +766,9 @@ class Crew(FlowTrackable, BaseModel): clear_files(self.id) detach(token) + def _post_kickoff(self, result: CrewOutput) -> CrewOutput: + return result + def kickoff_for_each( self, inputs: list[dict[str, Any]], @@ -936,6 +941,8 @@ class Crew(FlowTrackable, BaseModel): for after_callback in self.after_kickoff_callbacks: result = after_callback(result) + result = self._post_kickoff(result) + self.usage_metrics = self.calculate_usage_metrics() return result @@ -1181,6 +1188,9 @@ class Crew(FlowTrackable, BaseModel): self.manager_agent = manager manager.crew = self + def _get_execution_start_index(self, tasks: list[Task]) -> int | None: + return None + def _execute_tasks( self, tasks: list[Task], @@ -1197,6 +1207,9 @@ class Crew(FlowTrackable, BaseModel): Returns: CrewOutput: Final output of the crew """ + custom_start = self._get_execution_start_index(tasks) + if custom_start is not None: + start_index = custom_start task_outputs: list[TaskOutput] = [] futures: list[tuple[Task, Future[TaskOutput], int]] = [] @@ -1305,8 +1318,10 @@ class Crew(FlowTrackable, BaseModel): if files: supported_types: list[str] = [] if agent and agent.llm and agent.llm.supports_multimodal(): - provider = getattr(agent.llm, "provider", None) or getattr( - agent.llm, "model", "openai" + provider = ( + getattr(agent.llm, "provider", None) + or getattr(agent.llm, "model", None) + or "openai" ) api = getattr(agent.llm, "api", None) supported_types = get_supported_content_types(provider, api) diff --git a/lib/crewai/src/crewai/events/__init__.py b/lib/crewai/src/crewai/events/__init__.py index 61c0ec380..a6f213a54 100644 --- a/lib/crewai/src/crewai/events/__init__.py +++ b/lib/crewai/src/crewai/events/__init__.py @@ -195,6 +195,7 @@ __all__ = [ "ToolUsageFinishedEvent", "ToolUsageStartedEvent", "ToolValidateInputErrorEvent", + "_extension_exports", "crewai_event_bus", ] @@ -210,14 +211,29 @@ _AGENT_EVENT_MAPPING = { "LiteAgentExecutionStartedEvent": "crewai.events.types.agent_events", } +_extension_exports: dict[str, Any] = {} + def __getattr__(name: str) -> Any: - """Lazy import for agent events to avoid circular imports.""" + """Lazy import for agent events and registered extensions.""" if name in _AGENT_EVENT_MAPPING: import importlib module_path = _AGENT_EVENT_MAPPING[name] module = importlib.import_module(module_path) return getattr(module, name) + + if name in _extension_exports: + import importlib + + value = _extension_exports[name] + if isinstance(value, str): + module_path, _, attr_name = value.rpartition(".") + if module_path: + module = importlib.import_module(module_path) + return getattr(module, attr_name) + return importlib.import_module(value) + return value + msg = f"module {__name__!r} has no attribute {name!r}" raise AttributeError(msg) diff --git a/lib/crewai/src/crewai/events/event_bus.py b/lib/crewai/src/crewai/events/event_bus.py index 5c4bec58f..d0aaa4455 100644 --- a/lib/crewai/src/crewai/events/event_bus.py +++ b/lib/crewai/src/crewai/events/event_bus.py @@ -227,6 +227,39 @@ class CrewAIEventsBus: return decorator + def off( + self, + event_type: type[BaseEvent], + handler: Callable[..., Any], + ) -> None: + """Unregister an event handler for a specific event type. + + Args: + event_type: The event class to stop listening for + handler: The handler function to unregister + """ + with self._rwlock.w_locked(): + if event_type in self._sync_handlers: + existing_sync = self._sync_handlers[event_type] + if handler in existing_sync: + self._sync_handlers[event_type] = existing_sync - {handler} + if not self._sync_handlers[event_type]: + del self._sync_handlers[event_type] + + if event_type in self._async_handlers: + existing_async = self._async_handlers[event_type] + if handler in existing_async: + self._async_handlers[event_type] = existing_async - {handler} + if not self._async_handlers[event_type]: + del self._async_handlers[event_type] + + if event_type in self._handler_dependencies: + self._handler_dependencies[event_type].pop(handler, None) + if not self._handler_dependencies[event_type]: + del self._handler_dependencies[event_type] + + self._execution_plan_cache.pop(event_type, None) + def _call_handlers( self, source: Any, diff --git a/lib/crewai/src/crewai/events/listeners/tracing/utils.py b/lib/crewai/src/crewai/events/listeners/tracing/utils.py index 13e26dacb..b6bf1026b 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/utils.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/utils.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from contextvars import ContextVar, Token from datetime import datetime import getpass @@ -26,6 +27,8 @@ logger = logging.getLogger(__name__) _tracing_enabled: ContextVar[bool | None] = ContextVar("_tracing_enabled", default=None) +_first_time_trace_hook: Callable[[], bool] | None = None + def should_enable_tracing(*, override: bool | None = None) -> bool: """Determine if tracing should be enabled. @@ -407,10 +410,12 @@ def truncate_messages( def should_auto_collect_first_time_traces() -> bool: """True if we should auto-collect traces for first-time user. - Returns: True if first-time user AND telemetry not disabled AND tracing not explicitly enabled, False otherwise. """ + if _first_time_trace_hook is not None: + return _first_time_trace_hook() + if _is_test_environment(): return False diff --git a/lib/crewai/src/crewai/events/types/tool_usage_events.py b/lib/crewai/src/crewai/events/types/tool_usage_events.py index 7fe9b897f..c4e681546 100644 --- a/lib/crewai/src/crewai/events/types/tool_usage_events.py +++ b/lib/crewai/src/crewai/events/types/tool_usage_events.py @@ -16,7 +16,7 @@ class ToolUsageEvent(BaseEvent): tool_name: str tool_args: dict[str, Any] | str tool_class: str | None = None - run_attempts: int | None = None + run_attempts: int = 0 delegations: int | None = None agent: Any | None = None task_name: str | None = None @@ -26,7 +26,7 @@ class ToolUsageEvent(BaseEvent): model_config = ConfigDict(arbitrary_types_allowed=True) - def __init__(self, **data): + def __init__(self, **data: Any) -> None: if data.get("from_task"): task = data["from_task"] data["task_id"] = str(task.id) @@ -96,10 +96,10 @@ class ToolExecutionErrorEvent(BaseEvent): type: str = "tool_execution_error" tool_name: str tool_args: dict[str, Any] - tool_class: Callable + tool_class: Callable[..., Any] agent: Any | None = None - def __init__(self, **data): + def __init__(self, **data: Any) -> None: super().__init__(**data) # Set fingerprint data from the agent if self.agent and hasattr(self.agent, "fingerprint") and self.agent.fingerprint: diff --git a/lib/crewai/src/crewai/events/utils/console_formatter.py b/lib/crewai/src/crewai/events/utils/console_formatter.py index ac6caabcf..eaecc0e74 100644 --- a/lib/crewai/src/crewai/events/utils/console_formatter.py +++ b/lib/crewai/src/crewai/events/utils/console_formatter.py @@ -49,6 +49,9 @@ class ConsoleFormatter: if os.getenv("CI", "").lower() in ("true", "1"): return + if os.getenv("CREWAI_DISABLE_VERSION_CHECK", "").lower() in ("true", "1"): + return + try: is_newer, current, latest = is_newer_version_available() if is_newer and latest: diff --git a/lib/crewai/src/crewai/flow/async_feedback/__init__.py b/lib/crewai/src/crewai/flow/async_feedback/__init__.py index 286fdaa8d..02590a785 100644 --- a/lib/crewai/src/crewai/flow/async_feedback/__init__.py +++ b/lib/crewai/src/crewai/flow/async_feedback/__init__.py @@ -28,6 +28,8 @@ Example: ``` """ +from typing import Any + from crewai.flow.async_feedback.providers import ConsoleProvider from crewai.flow.async_feedback.types import ( HumanFeedbackPending, @@ -41,4 +43,15 @@ __all__ = [ "HumanFeedbackPending", "HumanFeedbackProvider", "PendingFeedbackContext", + "_extension_exports", ] + +_extension_exports: dict[str, Any] = {} + + +def __getattr__(name: str) -> Any: + """Support extensions via dynamic attribute lookup.""" + if name in _extension_exports: + return _extension_exports[name] + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) diff --git a/lib/crewai/src/crewai/knowledge/source/utils/__init__.py b/lib/crewai/src/crewai/knowledge/source/utils/__init__.py new file mode 100644 index 000000000..0f7c5142c --- /dev/null +++ b/lib/crewai/src/crewai/knowledge/source/utils/__init__.py @@ -0,0 +1 @@ +"""Knowledge source utilities.""" diff --git a/lib/crewai/src/crewai/knowledge/source/utils/source_helper.py b/lib/crewai/src/crewai/knowledge/source/utils/source_helper.py new file mode 100644 index 000000000..9ab41cd30 --- /dev/null +++ b/lib/crewai/src/crewai/knowledge/source/utils/source_helper.py @@ -0,0 +1,70 @@ +"""Helper utilities for knowledge sources.""" + +from typing import Any, ClassVar + +from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource +from crewai.knowledge.source.csv_knowledge_source import CSVKnowledgeSource +from crewai.knowledge.source.excel_knowledge_source import ExcelKnowledgeSource +from crewai.knowledge.source.json_knowledge_source import JSONKnowledgeSource +from crewai.knowledge.source.pdf_knowledge_source import PDFKnowledgeSource +from crewai.knowledge.source.text_file_knowledge_source import TextFileKnowledgeSource + + +class SourceHelper: + """Helper class for creating and managing knowledge sources.""" + + SUPPORTED_FILE_TYPES: ClassVar[list[str]] = [ + ".csv", + ".pdf", + ".json", + ".txt", + ".xlsx", + ".xls", + ] + + _FILE_TYPE_MAP: ClassVar[dict[str, type[BaseKnowledgeSource]]] = { + ".csv": CSVKnowledgeSource, + ".pdf": PDFKnowledgeSource, + ".json": JSONKnowledgeSource, + ".txt": TextFileKnowledgeSource, + ".xlsx": ExcelKnowledgeSource, + ".xls": ExcelKnowledgeSource, + } + + @classmethod + def is_supported_file(cls, file_path: str) -> bool: + """Check if a file type is supported. + + Args: + file_path: Path to the file. + + Returns: + True if the file type is supported. + """ + return file_path.lower().endswith(tuple(cls.SUPPORTED_FILE_TYPES)) + + @classmethod + def get_source( + cls, file_path: str, metadata: dict[str, Any] | None = None + ) -> BaseKnowledgeSource: + """Create appropriate KnowledgeSource based on file extension. + + Args: + file_path: Path to the file. + metadata: Optional metadata to attach to the source. + + Returns: + The appropriate KnowledgeSource instance. + + Raises: + ValueError: If the file type is not supported. + """ + if not cls.is_supported_file(file_path): + raise ValueError(f"Unsupported file type: {file_path}") + + lower_path = file_path.lower() + for ext, source_cls in cls._FILE_TYPE_MAP.items(): + if lower_path.endswith(ext): + return source_cls(file_path=[file_path], metadata=metadata) + + raise ValueError(f"Unsupported file type: {file_path}") diff --git a/lib/crewai/src/crewai/utilities/evaluators/crew_evaluator_handler.py b/lib/crewai/src/crewai/utilities/evaluators/crew_evaluator_handler.py index 9c9cac0c6..32b847d73 100644 --- a/lib/crewai/src/crewai/utilities/evaluators/crew_evaluator_handler.py +++ b/lib/crewai/src/crewai/utilities/evaluators/crew_evaluator_handler.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections import defaultdict -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from pydantic import BaseModel, Field, InstanceOf from rich.box import HEAVY_EDGE @@ -36,7 +36,13 @@ class CrewEvaluator: iteration: The current iteration of the evaluation. """ - def __init__(self, crew: Crew, eval_llm: InstanceOf[BaseLLM]) -> None: + def __init__( + self, + crew: Crew, + eval_llm: InstanceOf[BaseLLM] | str | None = None, + openai_model_name: str | None = None, + llm: InstanceOf[BaseLLM] | str | None = None, + ) -> None: self.crew = crew self.llm = eval_llm self.tasks_scores: defaultdict[int, list[float]] = defaultdict(list) @@ -86,7 +92,9 @@ class CrewEvaluator: """ self.iteration = iteration - def print_crew_evaluation_result(self) -> None: + def print_crew_evaluation_result( + self, token_usage: list[dict[str, Any]] | None = None + ) -> None: """ Prints the evaluation result of the crew in a table. A Crew with 2 tasks using the command crewai test -n 3 @@ -204,7 +212,7 @@ class CrewEvaluator: CrewTestResultEvent( quality=quality_score, execution_duration=current_task.execution_duration, - model=self.llm.model, + model=getattr(self.llm, "model", str(self.llm)), crew_name=self.crew.name, crew=self.crew, ), From 76b5f72e81844e7ee592db87317a96f2e05636ca Mon Sep 17 00:00:00 2001 From: Vini Brasil Date: Wed, 4 Feb 2026 20:34:08 -0300 Subject: [PATCH 3/9] Fix tool error causing double event scope pop (#4373) When a tool raises an error, both ToolUsageErrorEvent and ToolUsageFinishedEvent were being emitted. Since both events pop the event scope stack, this caused the agent scope to be incorrectly popped along with the tool scope. --- .../src/crewai/agents/crew_agent_executor.py | 30 ++++---- .../src/crewai/experimental/agent_executor.py | 30 ++++---- lib/crewai/src/crewai/tools/tool_usage.py | 8 +- lib/crewai/tests/events/test_event_context.py | 38 ++++++++- lib/crewai/tests/tools/test_tool_usage.py | 77 +++++++++++++++++++ 5 files changed, 152 insertions(+), 31 deletions(-) diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index c0f24516a..647596f2a 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -814,6 +814,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): agent_key=agent_key, ), ) + error_event_emitted = False track_delegation_if_needed(func_name, args_dict, self.task) @@ -896,6 +897,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): error=e, ), ) + error_event_emitted = True elif max_usage_reached and original_tool: # Return error message when max usage limit is reached result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore." @@ -923,20 +925,20 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): color="red", ) - # Emit tool usage finished event - crewai_event_bus.emit( - self, - event=ToolUsageFinishedEvent( - output=result, - tool_name=func_name, - tool_args=args_dict, - from_agent=self.agent, - from_task=self.task, - agent_key=agent_key, - started_at=started_at, - finished_at=datetime.now(), - ), - ) + if not error_event_emitted: + crewai_event_bus.emit( + self, + event=ToolUsageFinishedEvent( + output=result, + tool_name=func_name, + tool_args=args_dict, + from_agent=self.agent, + from_task=self.task, + agent_key=agent_key, + started_at=started_at, + finished_at=datetime.now(), + ), + ) # Append tool result message tool_message: LLMMessage = { diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index b9d8adccc..98e8b5bdb 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -689,6 +689,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): agent_key=agent_key, ), ) + error_event_emitted = False track_delegation_if_needed(func_name, args_dict, self.task) @@ -764,6 +765,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): error=e, ), ) + error_event_emitted = True elif max_usage_reached and original_tool: # Return error message when max usage limit is reached result = f"Tool '{func_name}' has reached its usage limit of {original_tool.max_usage_count} times and cannot be used anymore." @@ -792,20 +794,20 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): color="red", ) - # Emit tool usage finished event - crewai_event_bus.emit( - self, - event=ToolUsageFinishedEvent( - output=result, - tool_name=func_name, - tool_args=args_dict, - from_agent=self.agent, - from_task=self.task, - agent_key=agent_key, - started_at=started_at, - finished_at=datetime.now(), - ), - ) + if not error_event_emitted: + crewai_event_bus.emit( + self, + event=ToolUsageFinishedEvent( + output=result, + tool_name=func_name, + tool_args=args_dict, + from_agent=self.agent, + from_task=self.task, + agent_key=agent_key, + started_at=started_at, + finished_at=datetime.now(), + ), + ) # Append tool result message tool_message: LLMMessage = { diff --git a/lib/crewai/src/crewai/tools/tool_usage.py b/lib/crewai/src/crewai/tools/tool_usage.py index e5a9e6154..b6ce5adb6 100644 --- a/lib/crewai/src/crewai/tools/tool_usage.py +++ b/lib/crewai/src/crewai/tools/tool_usage.py @@ -270,6 +270,7 @@ class ToolUsage: result = None # type: ignore should_retry = False available_tool = None + error_event_emitted = False try: if self.tools_handler and self.tools_handler.cache: @@ -408,6 +409,7 @@ class ToolUsage: except Exception as e: self.on_tool_error(tool=tool, tool_calling=calling, e=e) + error_event_emitted = True self._run_attempts += 1 if self._run_attempts > self._max_parsing_attempts: self._telemetry.tool_usage_error(llm=self.function_calling_llm) @@ -435,7 +437,7 @@ class ToolUsage: result = self._format_result(result=result) finally: - if started_event_emitted: + if started_event_emitted and not error_event_emitted: self.on_tool_use_finished( tool=tool, tool_calling=calling, @@ -500,6 +502,7 @@ class ToolUsage: result = None # type: ignore should_retry = False available_tool = None + error_event_emitted = False try: if self.tools_handler and self.tools_handler.cache: @@ -638,6 +641,7 @@ class ToolUsage: except Exception as e: self.on_tool_error(tool=tool, tool_calling=calling, e=e) + error_event_emitted = True self._run_attempts += 1 if self._run_attempts > self._max_parsing_attempts: self._telemetry.tool_usage_error(llm=self.function_calling_llm) @@ -665,7 +669,7 @@ class ToolUsage: result = self._format_result(result=result) finally: - if started_event_emitted: + if started_event_emitted and not error_event_emitted: self.on_tool_use_finished( tool=tool, tool_calling=calling, diff --git a/lib/crewai/tests/events/test_event_context.py b/lib/crewai/tests/events/test_event_context.py index 071e1a34d..2a69ca1ee 100644 --- a/lib/crewai/tests/events/test_event_context.py +++ b/lib/crewai/tests/events/test_event_context.py @@ -177,4 +177,40 @@ class TestTriggeredByScope: raise ValueError("test error") except ValueError: pass - assert get_triggering_event_id() is None \ No newline at end of file + assert get_triggering_event_id() is None + + +def test_agent_scope_preserved_after_tool_error_event() -> None: + from crewai.events import crewai_event_bus + from crewai.events.types.tool_usage_events import ( + ToolUsageErrorEvent, + ToolUsageStartedEvent, + ) + + push_event_scope("crew-1", "crew_kickoff_started") + push_event_scope("task-1", "task_started") + push_event_scope("agent-1", "agent_execution_started") + + crewai_event_bus.emit( + None, + ToolUsageStartedEvent( + tool_name="test_tool", + tool_args={}, + agent_key="test_agent", + ) + ) + + crewai_event_bus.emit( + None, + ToolUsageErrorEvent( + tool_name="test_tool", + tool_args={}, + agent_key="test_agent", + error=ValueError("test error"), + ) + ) + + crewai_event_bus.flush() + + assert get_current_parent_id() == "agent-1" + diff --git a/lib/crewai/tests/tools/test_tool_usage.py b/lib/crewai/tests/tools/test_tool_usage.py index c6dfad58b..b68a41666 100644 --- a/lib/crewai/tests/tools/test_tool_usage.py +++ b/lib/crewai/tests/tools/test_tool_usage.py @@ -10,7 +10,9 @@ from crewai import Agent, Task from crewai.events.event_bus import crewai_event_bus from crewai.events.types.tool_usage_events import ( ToolSelectionErrorEvent, + ToolUsageErrorEvent, ToolUsageFinishedEvent, + ToolUsageStartedEvent, ToolValidateInputErrorEvent, ) from crewai.tools import BaseTool @@ -744,3 +746,78 @@ def test_tool_usage_finished_event_with_cached_result(): assert isinstance(event.started_at, datetime.datetime) assert isinstance(event.finished_at, datetime.datetime) assert event.type == "tool_usage_finished" + + +def test_tool_error_does_not_emit_finished_event(): + from crewai.tools.tool_calling import ToolCalling + + class FailingTool(BaseTool): + name: str = "Failing Tool" + description: str = "A tool that always fails" + + def _run(self, **kwargs) -> str: + raise ValueError("Intentional failure") + + failing_tool = FailingTool().to_structured_tool() + + mock_agent = MagicMock() + mock_agent.key = "test_agent_key" + mock_agent.role = "test_agent_role" + mock_agent._original_role = "test_agent_role" + mock_agent.verbose = False + mock_agent.fingerprint = None + mock_agent.i18n.tools.return_value = {"name": "Add Image"} + mock_agent.i18n.errors.return_value = "Error: {error}" + mock_agent.i18n.slice.return_value = "Available tools: {tool_names}" + + mock_task = MagicMock() + mock_task.delegations = 0 + mock_task.name = "Test Task" + mock_task.description = "A test task" + mock_task.id = "test-task-id" + + mock_action = MagicMock() + mock_action.tool = "failing_tool" + mock_action.tool_input = "{}" + + tool_usage = ToolUsage( + tools_handler=MagicMock(cache=None, last_used_tool=None), + tools=[failing_tool], + task=mock_task, + function_calling_llm=None, + agent=mock_agent, + action=mock_action, + ) + + started_events = [] + error_events = [] + finished_events = [] + error_received = threading.Event() + + @crewai_event_bus.on(ToolUsageStartedEvent) + def on_started(source, event): + if event.tool_name == "failing_tool": + started_events.append(event) + + @crewai_event_bus.on(ToolUsageErrorEvent) + def on_error(source, event): + if event.tool_name == "failing_tool": + error_events.append(event) + error_received.set() + + @crewai_event_bus.on(ToolUsageFinishedEvent) + def on_finished(source, event): + if event.tool_name == "failing_tool": + finished_events.append(event) + + tool_calling = ToolCalling(tool_name="failing_tool", arguments={}) + tool_usage.use(calling=tool_calling, tool_string="Action: failing_tool") + + assert error_received.wait(timeout=5), "Timeout waiting for error event" + crewai_event_bus.flush() + + assert len(started_events) >= 1, "Expected at least one ToolUsageStartedEvent" + assert len(error_events) >= 1, "Expected at least one ToolUsageErrorEvent" + assert len(finished_events) == 0, ( + "ToolUsageFinishedEvent should NOT be emitted after ToolUsageErrorEvent" + ) From 711e7171e1d01ef5a00c5f4f863463c670650e92 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 4 Feb 2026 21:16:20 -0500 Subject: [PATCH 4/9] chore: improve hook typing and registration Allow hook registration to accept both typed hook types and plain callables by importing and using After*/Before*CallHookCallable types; add explicit LLMCallHookContext and ToolCallHookContext typing in crew_base. Introduce a post-initialize crew hook list and invoke hooks after Crew instance initialization. Refactor filtered hook factory functions to include precise typing and clearer local names (before_llm_hook/after_llm_hook/before_tool_hook/after_tool_hook) and register those with the instance. Update CrewInstance protocol to include _registered_hook_functions and _hooks_being_registered fields. --- lib/crewai/src/crewai/hooks/llm_hooks.py | 25 ++++--- lib/crewai/src/crewai/hooks/tool_hooks.py | 27 +++++--- lib/crewai/src/crewai/project/crew_base.py | 77 +++++++++++++++------- lib/crewai/src/crewai/project/wrappers.py | 2 + 4 files changed, 89 insertions(+), 42 deletions(-) diff --git a/lib/crewai/src/crewai/hooks/llm_hooks.py b/lib/crewai/src/crewai/hooks/llm_hooks.py index 2f5462fe0..3a6abbedf 100644 --- a/lib/crewai/src/crewai/hooks/llm_hooks.py +++ b/lib/crewai/src/crewai/hooks/llm_hooks.py @@ -3,7 +3,12 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, cast from crewai.events.event_listener import event_listener -from crewai.hooks.types import AfterLLMCallHookType, BeforeLLMCallHookType +from crewai.hooks.types import ( + AfterLLMCallHookCallable, + AfterLLMCallHookType, + BeforeLLMCallHookCallable, + BeforeLLMCallHookType, +) from crewai.utilities.printer import Printer @@ -149,12 +154,12 @@ class LLMCallHookContext: event_listener.formatter.resume_live_updates() -_before_llm_call_hooks: list[BeforeLLMCallHookType] = [] -_after_llm_call_hooks: list[AfterLLMCallHookType] = [] +_before_llm_call_hooks: list[BeforeLLMCallHookType | BeforeLLMCallHookCallable] = [] +_after_llm_call_hooks: list[AfterLLMCallHookType | AfterLLMCallHookCallable] = [] def register_before_llm_call_hook( - hook: BeforeLLMCallHookType, + hook: BeforeLLMCallHookType | BeforeLLMCallHookCallable, ) -> None: """Register a global before_llm_call hook. @@ -190,7 +195,7 @@ def register_before_llm_call_hook( def register_after_llm_call_hook( - hook: AfterLLMCallHookType, + hook: AfterLLMCallHookType | AfterLLMCallHookCallable, ) -> None: """Register a global after_llm_call hook. @@ -217,7 +222,9 @@ def register_after_llm_call_hook( _after_llm_call_hooks.append(hook) -def get_before_llm_call_hooks() -> list[BeforeLLMCallHookType]: +def get_before_llm_call_hooks() -> list[ + BeforeLLMCallHookType | BeforeLLMCallHookCallable +]: """Get all registered global before_llm_call hooks. Returns: @@ -226,7 +233,7 @@ def get_before_llm_call_hooks() -> list[BeforeLLMCallHookType]: return _before_llm_call_hooks.copy() -def get_after_llm_call_hooks() -> list[AfterLLMCallHookType]: +def get_after_llm_call_hooks() -> list[AfterLLMCallHookType | AfterLLMCallHookCallable]: """Get all registered global after_llm_call hooks. Returns: @@ -236,7 +243,7 @@ def get_after_llm_call_hooks() -> list[AfterLLMCallHookType]: def unregister_before_llm_call_hook( - hook: BeforeLLMCallHookType, + hook: BeforeLLMCallHookType | BeforeLLMCallHookCallable, ) -> bool: """Unregister a specific global before_llm_call hook. @@ -262,7 +269,7 @@ def unregister_before_llm_call_hook( def unregister_after_llm_call_hook( - hook: AfterLLMCallHookType, + hook: AfterLLMCallHookType | AfterLLMCallHookCallable, ) -> bool: """Unregister a specific global after_llm_call hook. diff --git a/lib/crewai/src/crewai/hooks/tool_hooks.py b/lib/crewai/src/crewai/hooks/tool_hooks.py index 6ee0ab033..ac7f5c362 100644 --- a/lib/crewai/src/crewai/hooks/tool_hooks.py +++ b/lib/crewai/src/crewai/hooks/tool_hooks.py @@ -3,7 +3,12 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any from crewai.events.event_listener import event_listener -from crewai.hooks.types import AfterToolCallHookType, BeforeToolCallHookType +from crewai.hooks.types import ( + AfterToolCallHookCallable, + AfterToolCallHookType, + BeforeToolCallHookCallable, + BeforeToolCallHookType, +) from crewai.utilities.printer import Printer @@ -112,12 +117,12 @@ class ToolCallHookContext: # Global hook registries -_before_tool_call_hooks: list[BeforeToolCallHookType] = [] -_after_tool_call_hooks: list[AfterToolCallHookType] = [] +_before_tool_call_hooks: list[BeforeToolCallHookType | BeforeToolCallHookCallable] = [] +_after_tool_call_hooks: list[AfterToolCallHookType | AfterToolCallHookCallable] = [] def register_before_tool_call_hook( - hook: BeforeToolCallHookType, + hook: BeforeToolCallHookType | BeforeToolCallHookCallable, ) -> None: """Register a global before_tool_call hook. @@ -154,7 +159,7 @@ def register_before_tool_call_hook( def register_after_tool_call_hook( - hook: AfterToolCallHookType, + hook: AfterToolCallHookType | AfterToolCallHookCallable, ) -> None: """Register a global after_tool_call hook. @@ -184,7 +189,9 @@ def register_after_tool_call_hook( _after_tool_call_hooks.append(hook) -def get_before_tool_call_hooks() -> list[BeforeToolCallHookType]: +def get_before_tool_call_hooks() -> list[ + BeforeToolCallHookType | BeforeToolCallHookCallable +]: """Get all registered global before_tool_call hooks. Returns: @@ -193,7 +200,9 @@ def get_before_tool_call_hooks() -> list[BeforeToolCallHookType]: return _before_tool_call_hooks.copy() -def get_after_tool_call_hooks() -> list[AfterToolCallHookType]: +def get_after_tool_call_hooks() -> list[ + AfterToolCallHookType | AfterToolCallHookCallable +]: """Get all registered global after_tool_call hooks. Returns: @@ -203,7 +212,7 @@ def get_after_tool_call_hooks() -> list[AfterToolCallHookType]: def unregister_before_tool_call_hook( - hook: BeforeToolCallHookType, + hook: BeforeToolCallHookType | BeforeToolCallHookCallable, ) -> bool: """Unregister a specific global before_tool_call hook. @@ -229,7 +238,7 @@ def unregister_before_tool_call_hook( def unregister_after_tool_call_hook( - hook: AfterToolCallHookType, + hook: AfterToolCallHookType | AfterToolCallHookCallable, ) -> bool: """Unregister a specific global after_tool_call hook. diff --git a/lib/crewai/src/crewai/project/crew_base.py b/lib/crewai/src/crewai/project/crew_base.py index 202d98898..323450b13 100644 --- a/lib/crewai/src/crewai/project/crew_base.py +++ b/lib/crewai/src/crewai/project/crew_base.py @@ -27,6 +27,8 @@ if TYPE_CHECKING: from crewai import Agent, Task from crewai.agents.cache.cache_handler import CacheHandler from crewai.crews.crew_output import CrewOutput + from crewai.hooks.llm_hooks import LLMCallHookContext + from crewai.hooks.tool_hooks import ToolCallHookContext from crewai.project.wrappers import ( CrewInstance, OutputJsonClass, @@ -34,6 +36,8 @@ if TYPE_CHECKING: ) from crewai.tasks.task_output import TaskOutput +_post_initialize_crew_hooks: list[Callable[[Any], None]] = [] + class AgentConfig(TypedDict, total=False): """Type definition for agent configuration dictionary. @@ -266,6 +270,9 @@ class CrewBaseMeta(type): instance.map_all_agent_variables() instance.map_all_task_variables() + for hook in _post_initialize_crew_hooks: + hook(instance) + original_methods = { name: method for name, method in cls.__dict__.items() @@ -485,47 +492,61 @@ def _register_crew_hooks(instance: CrewInstance, cls: type) -> None: if has_agent_filter: agents_filter = hook_method._filter_agents - def make_filtered_before_llm(bound_fn, agents_list): - def filtered(context): + def make_filtered_before_llm( + bound_fn: Callable[[LLMCallHookContext], bool | None], + agents_list: list[str], + ) -> Callable[[LLMCallHookContext], bool | None]: + def filtered(context: LLMCallHookContext) -> bool | None: if context.agent and context.agent.role not in agents_list: return None return bound_fn(context) return filtered - final_hook = make_filtered_before_llm(bound_hook, agents_filter) + before_llm_hook = make_filtered_before_llm(bound_hook, agents_filter) else: - final_hook = bound_hook + before_llm_hook = bound_hook - register_before_llm_call_hook(final_hook) - instance._registered_hook_functions.append(("before_llm_call", final_hook)) + register_before_llm_call_hook(before_llm_hook) + instance._registered_hook_functions.append( + ("before_llm_call", before_llm_hook) + ) if hasattr(hook_method, "is_after_llm_call_hook"): if has_agent_filter: agents_filter = hook_method._filter_agents - def make_filtered_after_llm(bound_fn, agents_list): - def filtered(context): + def make_filtered_after_llm( + bound_fn: Callable[[LLMCallHookContext], str | None], + agents_list: list[str], + ) -> Callable[[LLMCallHookContext], str | None]: + def filtered(context: LLMCallHookContext) -> str | None: if context.agent and context.agent.role not in agents_list: return None return bound_fn(context) return filtered - final_hook = make_filtered_after_llm(bound_hook, agents_filter) + after_llm_hook = make_filtered_after_llm(bound_hook, agents_filter) else: - final_hook = bound_hook + after_llm_hook = bound_hook - register_after_llm_call_hook(final_hook) - instance._registered_hook_functions.append(("after_llm_call", final_hook)) + register_after_llm_call_hook(after_llm_hook) + instance._registered_hook_functions.append( + ("after_llm_call", after_llm_hook) + ) if hasattr(hook_method, "is_before_tool_call_hook"): if has_tool_filter or has_agent_filter: tools_filter = getattr(hook_method, "_filter_tools", None) agents_filter = getattr(hook_method, "_filter_agents", None) - def make_filtered_before_tool(bound_fn, tools_list, agents_list): - def filtered(context): + def make_filtered_before_tool( + bound_fn: Callable[[ToolCallHookContext], bool | None], + tools_list: list[str] | None, + agents_list: list[str] | None, + ) -> Callable[[ToolCallHookContext], bool | None]: + def filtered(context: ToolCallHookContext) -> bool | None: if tools_list and context.tool_name not in tools_list: return None if ( @@ -538,22 +559,28 @@ def _register_crew_hooks(instance: CrewInstance, cls: type) -> None: return filtered - final_hook = make_filtered_before_tool( + before_tool_hook = make_filtered_before_tool( bound_hook, tools_filter, agents_filter ) else: - final_hook = bound_hook + before_tool_hook = bound_hook - register_before_tool_call_hook(final_hook) - instance._registered_hook_functions.append(("before_tool_call", final_hook)) + register_before_tool_call_hook(before_tool_hook) + instance._registered_hook_functions.append( + ("before_tool_call", before_tool_hook) + ) if hasattr(hook_method, "is_after_tool_call_hook"): if has_tool_filter or has_agent_filter: tools_filter = getattr(hook_method, "_filter_tools", None) agents_filter = getattr(hook_method, "_filter_agents", None) - def make_filtered_after_tool(bound_fn, tools_list, agents_list): - def filtered(context): + def make_filtered_after_tool( + bound_fn: Callable[[ToolCallHookContext], str | None], + tools_list: list[str] | None, + agents_list: list[str] | None, + ) -> Callable[[ToolCallHookContext], str | None]: + def filtered(context: ToolCallHookContext) -> str | None: if tools_list and context.tool_name not in tools_list: return None if ( @@ -566,14 +593,16 @@ def _register_crew_hooks(instance: CrewInstance, cls: type) -> None: return filtered - final_hook = make_filtered_after_tool( + after_tool_hook = make_filtered_after_tool( bound_hook, tools_filter, agents_filter ) else: - final_hook = bound_hook + after_tool_hook = bound_hook - register_after_tool_call_hook(final_hook) - instance._registered_hook_functions.append(("after_tool_call", final_hook)) + register_after_tool_call_hook(after_tool_hook) + instance._registered_hook_functions.append( + ("after_tool_call", after_tool_hook) + ) instance._hooks_being_registered = False diff --git a/lib/crewai/src/crewai/project/wrappers.py b/lib/crewai/src/crewai/project/wrappers.py index 28cd39525..3d570b6f0 100644 --- a/lib/crewai/src/crewai/project/wrappers.py +++ b/lib/crewai/src/crewai/project/wrappers.py @@ -72,6 +72,8 @@ class CrewInstance(Protocol): __crew_metadata__: CrewMetadata _mcp_server_adapter: Any _all_methods: dict[str, Callable[..., Any]] + _registered_hook_functions: list[tuple[str, Callable[..., Any]]] + _hooks_being_registered: bool agents: list[Agent] tasks: list[Task] base_directory: Path From fe2a4b4e40dac46db8ceec45dfc58dbefb8c39c3 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 4 Feb 2026 21:21:54 -0500 Subject: [PATCH 5/9] chore: bug fixes and more refactor Refactor agent executor to delegate human interactions to a provider: add messages and ask_for_human_input properties, implement _invoke_loop and _format_feedback_message, and replace the internal iterative/training feedback logic with a call to get_provider().handle_feedback. Make LLMGuardrail kickoff coroutine-aware by detecting coroutines and running them via asyncio.run so both sync and async agents are supported. Make telemetry more robust by safely handling missing task.output (use empty string) and returning early if span is None before setting attributes. Improve serialization to detect circular references via an _ancestors set, propagate it through recursive calls, and pass exclude/max_depth/_current_depth consistently to prevent infinite recursion and produce stable serializable output. --- .../src/crewai/experimental/agent_executor.py | 179 ++++++------------ lib/crewai/src/crewai/tasks/llm_guardrail.py | 16 +- lib/crewai/src/crewai/telemetry/telemetry.py | 5 +- .../src/crewai/utilities/serialization.py | 24 ++- 4 files changed, 101 insertions(+), 123 deletions(-) diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 98e8b5bdb..87f9e83d4 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -18,6 +18,7 @@ from crewai.agents.parser import ( AgentFinish, OutputParserError, ) +from crewai.core.providers.human_input import get_provider from crewai.events.event_bus import crewai_event_bus from crewai.events.listeners.tracing.utils import ( is_tracing_enabled_in_context, @@ -41,7 +42,12 @@ from crewai.hooks.tool_hooks import ( get_after_tool_call_hooks, get_before_tool_call_hooks, ) -from crewai.hooks.types import AfterLLMCallHookType, BeforeLLMCallHookType +from crewai.hooks.types import ( + AfterLLMCallHookCallable, + AfterLLMCallHookType, + BeforeLLMCallHookCallable, + BeforeLLMCallHookType, +) from crewai.utilities.agent_utils import ( convert_tools_to_openai_schema, enforce_rpm_limit, @@ -191,8 +197,12 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): self._instance_id = str(uuid4())[:8] - self.before_llm_call_hooks: list[BeforeLLMCallHookType] = [] - self.after_llm_call_hooks: list[AfterLLMCallHookType] = [] + self.before_llm_call_hooks: list[ + BeforeLLMCallHookType | BeforeLLMCallHookCallable + ] = [] + self.after_llm_call_hooks: list[ + AfterLLMCallHookType | AfterLLMCallHookCallable + ] = [] self.before_llm_call_hooks.extend(get_before_llm_call_hooks()) self.after_llm_call_hooks.extend(get_after_llm_call_hooks()) @@ -207,6 +217,51 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): ) self._state = AgentReActState() + @property + def messages(self) -> list[LLMMessage]: + """Delegate to state for ExecutorContext conformance.""" + return self._state.messages + + @messages.setter + def messages(self, value: list[LLMMessage]) -> None: + """Delegate to state for ExecutorContext conformance.""" + self._state.messages = value + + @property + def ask_for_human_input(self) -> bool: + """Delegate to state for ExecutorContext conformance.""" + return self._state.ask_for_human_input + + @ask_for_human_input.setter + def ask_for_human_input(self, value: bool) -> None: + """Delegate to state for ExecutorContext conformance.""" + self._state.ask_for_human_input = value + + def _invoke_loop(self) -> AgentFinish: + """Invoke the agent loop and return the result. + + Required by ExecutorContext protocol. + """ + self._state.iterations = 0 + self._state.is_finished = False + self._state.current_answer = None + + self.kickoff() + + answer = self._state.current_answer + if not isinstance(answer, AgentFinish): + raise RuntimeError("Agent loop did not produce a final answer") + return answer + + def _format_feedback_message(self, feedback: str) -> LLMMessage: + """Format feedback as a message for the LLM. + + Required by ExecutorContext protocol. + """ + return format_message_for_llm( + self._i18n.slice("feedback_instructions").format(feedback=feedback) + ) + def _ensure_flow_initialized(self) -> None: """Ensure Flow.__init__() has been called. @@ -300,16 +355,6 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): """ return self._state - @property - def messages(self) -> list[LLMMessage]: - """Compatibility property for mixin - returns state messages.""" - return self._state.messages - - @messages.setter - def messages(self, value: list[LLMMessage]) -> None: - """Set state messages.""" - self._state.messages = value - @property def iterations(self) -> int: """Compatibility property for mixin - returns state iterations.""" @@ -1321,17 +1366,8 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): Returns: Final answer after feedback. """ - output_str = ( - str(formatted_answer.output) - if isinstance(formatted_answer.output, BaseModel) - else formatted_answer.output - ) - human_feedback = self._ask_human_input(output_str) - - if self._is_training_mode(): - return self._handle_training_feedback(formatted_answer, human_feedback) - - return self._handle_regular_feedback(formatted_answer, human_feedback) + provider = get_provider() + return provider.handle_feedback(formatted_answer, self) def _is_training_mode(self) -> bool: """Check if training mode is active. @@ -1341,101 +1377,6 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): """ return bool(self.crew and self.crew._train) - def _handle_training_feedback( - self, initial_answer: AgentFinish, feedback: str - ) -> AgentFinish: - """Process training feedback and generate improved answer. - - Args: - initial_answer: Initial agent output. - feedback: Training feedback. - - Returns: - Improved answer. - """ - self._handle_crew_training_output(initial_answer, feedback) - self.state.messages.append( - format_message_for_llm( - self._i18n.slice("feedback_instructions").format(feedback=feedback) - ) - ) - - # Re-run flow for improved answer - self.state.iterations = 0 - self.state.is_finished = False - self.state.current_answer = None - - self.kickoff() - - # Get improved answer from state - improved_answer = self.state.current_answer - if not isinstance(improved_answer, AgentFinish): - raise RuntimeError( - "Training feedback iteration did not produce final answer" - ) - - self._handle_crew_training_output(improved_answer) - self.state.ask_for_human_input = False - return improved_answer - - def _handle_regular_feedback( - self, current_answer: AgentFinish, initial_feedback: str - ) -> AgentFinish: - """Process regular feedback iteratively until user is satisfied. - - Args: - current_answer: Current agent output. - initial_feedback: Initial user feedback. - - Returns: - Final answer after iterations. - """ - feedback = initial_feedback - answer = current_answer - - while self.state.ask_for_human_input: - if feedback.strip() == "": - self.state.ask_for_human_input = False - else: - answer = self._process_feedback_iteration(feedback) - output_str = ( - str(answer.output) - if isinstance(answer.output, BaseModel) - else answer.output - ) - feedback = self._ask_human_input(output_str) - - return answer - - def _process_feedback_iteration(self, feedback: str) -> AgentFinish: - """Process a single feedback iteration and generate updated response. - - Args: - feedback: User feedback. - - Returns: - Updated agent response. - """ - self.state.messages.append( - format_message_for_llm( - self._i18n.slice("feedback_instructions").format(feedback=feedback) - ) - ) - - # Re-run flow - self.state.iterations = 0 - self.state.is_finished = False - self.state.current_answer = None - - self.kickoff() - - # Get answer from state - answer = self.state.current_answer - if not isinstance(answer, AgentFinish): - raise RuntimeError("Feedback iteration did not produce final answer") - - return answer - @classmethod def __get_pydantic_core_schema__( cls, _source_type: Any, _handler: GetCoreSchemaHandler diff --git a/lib/crewai/src/crewai/tasks/llm_guardrail.py b/lib/crewai/src/crewai/tasks/llm_guardrail.py index 803b2d749..3729e8084 100644 --- a/lib/crewai/src/crewai/tasks/llm_guardrail.py +++ b/lib/crewai/src/crewai/tasks/llm_guardrail.py @@ -1,6 +1,10 @@ +import asyncio +from collections.abc import Coroutine +import inspect from typing import Any from pydantic import BaseModel, Field +from typing_extensions import TypeIs from crewai.agent import Agent from crewai.lite_agent_output import LiteAgentOutput @@ -8,6 +12,13 @@ from crewai.llms.base_llm import BaseLLM from crewai.tasks.task_output import TaskOutput +def _is_coroutine( + obj: LiteAgentOutput | Coroutine[Any, Any, LiteAgentOutput], +) -> TypeIs[Coroutine[Any, Any, LiteAgentOutput]]: + """Check if obj is a coroutine for type narrowing.""" + return inspect.iscoroutine(obj) + + class LLMGuardrailResult(BaseModel): valid: bool = Field( description="Whether the task output complies with the guardrail" @@ -62,7 +73,10 @@ class LLMGuardrail: - If the Task result complies with the guardrail, saying that is valid """ - return agent.kickoff(query, response_format=LLMGuardrailResult) + kickoff_result = agent.kickoff(query, response_format=LLMGuardrailResult) + if _is_coroutine(kickoff_result): + return asyncio.run(kickoff_result) + return kickoff_result def __call__(self, task_output: TaskOutput) -> tuple[bool, Any]: """Validates the output of a task based on specified criteria. diff --git a/lib/crewai/src/crewai/telemetry/telemetry.py b/lib/crewai/src/crewai/telemetry/telemetry.py index 27d5d6090..04303fc3d 100644 --- a/lib/crewai/src/crewai/telemetry/telemetry.py +++ b/lib/crewai/src/crewai/telemetry/telemetry.py @@ -903,7 +903,7 @@ class Telemetry: { "id": str(task.id), "description": task.description, - "output": task.output.raw_output, + "output": task.output.raw if task.output else "", } for task in crew.tasks ] @@ -923,6 +923,9 @@ class Telemetry: value: The attribute value. """ + if span is None: + return + def _operation() -> None: return span.set_attribute(key, value) diff --git a/lib/crewai/src/crewai/utilities/serialization.py b/lib/crewai/src/crewai/utilities/serialization.py index a6b871d3d..0207e80ab 100644 --- a/lib/crewai/src/crewai/utilities/serialization.py +++ b/lib/crewai/src/crewai/utilities/serialization.py @@ -19,6 +19,7 @@ def to_serializable( exclude: set[str] | None = None, max_depth: int = 5, _current_depth: int = 0, + _ancestors: set[int] | None = None, ) -> Serializable: """Converts a Python object into a JSON-compatible representation. @@ -31,6 +32,7 @@ def to_serializable( exclude: Set of keys to exclude from the result. max_depth: Maximum recursion depth. Defaults to 5. _current_depth: Current recursion depth (for internal use). + _ancestors: Set of ancestor object ids for cycle detection (for internal use). Returns: Serializable: A JSON-compatible structure. @@ -41,16 +43,29 @@ def to_serializable( if exclude is None: exclude = set() + if _ancestors is None: + _ancestors = set() + if isinstance(obj, (str, int, float, bool, type(None))): return obj if isinstance(obj, uuid.UUID): return str(obj) if isinstance(obj, (date, datetime)): return obj.isoformat() + + object_id = id(obj) + if object_id in _ancestors: + return f"" + new_ancestors = _ancestors | {object_id} + if isinstance(obj, (list, tuple, set)): return [ to_serializable( - item, max_depth=max_depth, _current_depth=_current_depth + 1 + item, + exclude=exclude, + max_depth=max_depth, + _current_depth=_current_depth + 1, + _ancestors=new_ancestors, ) for item in obj ] @@ -61,6 +76,7 @@ def to_serializable( exclude=exclude, max_depth=max_depth, _current_depth=_current_depth + 1, + _ancestors=new_ancestors, ) for key, value in obj.items() if key not in exclude @@ -71,12 +87,16 @@ def to_serializable( obj=obj.model_dump(exclude=exclude), max_depth=max_depth, _current_depth=_current_depth + 1, + _ancestors=new_ancestors, ) except Exception: try: return { _to_serializable_key(k): to_serializable( - v, max_depth=max_depth, _current_depth=_current_depth + 1 + v, + max_depth=max_depth, + _current_depth=_current_depth + 1, + _ancestors=new_ancestors, ) for k, v in obj.__dict__.items() if k not in (exclude or set()) From 6bb1b178a10139160575cccc4ce1be62800b28c0 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 5 Feb 2026 12:49:54 -0500 Subject: [PATCH 6/9] chore: extension points Introduce ContextVar-backed hooks and small API/behavior changes to improve extensibility and testability. Changes include: - agents: mark configure_structured_output as abstract and change its parameter to task to reflect use of task metadata. - tracing: convert _first_time_trace_hook to a ContextVar and call .get() to safely retrieve the hook. - console formatter: add _disable_version_check ContextVar and skip version checks when set (avoids noisy checks in certain contexts). - flow: use current_triggering_event_id variable when scheduling listener tasks to keep naming consistent. - hallucination guardrail: make context optional, add _validate_output_hook to allow custom validation hooks, update examples and return contract to allow hooks to override behavior. - agent utilities: add _create_plus_client_hook for injecting a Plus client (used in tests/alternate flows), ensure structured tools have current_usage_count initialized and propagate to original tool, and fall back to creating PlusAPI client when no hook is provided. --- .../agent_adapters/base_agent_adapter.py | 5 ++- lib/crewai/src/crewai/crew.py | 8 +++- .../listeners/tracing/trace_listener.py | 8 +++- .../crewai/events/listeners/tracing/utils.py | 37 ++++++++++++++-- .../crewai/events/utils/console_formatter.py | 43 +++++++++++++++++++ lib/crewai/src/crewai/flow/flow.py | 9 +++- .../crewai/tasks/hallucination_guardrail.py | 31 +++++++------ .../src/crewai/utilities/agent_utils.py | 17 ++++++-- lib/crewai/src/crewai/utilities/printer.py | 4 ++ .../agent_adapters/test_base_agent_adapter.py | 4 ++ 10 files changed, 141 insertions(+), 25 deletions(-) diff --git a/lib/crewai/src/crewai/agents/agent_adapters/base_agent_adapter.py b/lib/crewai/src/crewai/agents/agent_adapters/base_agent_adapter.py index 8001628ed..12dfcf43c 100644 --- a/lib/crewai/src/crewai/agents/agent_adapters/base_agent_adapter.py +++ b/lib/crewai/src/crewai/agents/agent_adapters/base_agent_adapter.py @@ -37,9 +37,10 @@ class BaseAgentAdapter(BaseAgent, ABC): tools: Optional list of BaseTool instances to be configured """ - def configure_structured_output(self, structured_output: Any) -> None: + @abstractmethod + def configure_structured_output(self, task: Any) -> None: """Configure the structured output for the specific agent implementation. Args: - structured_output: The structured output to be configured + task: The task object containing output format specifications. """ diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index baabce190..09a103eba 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -2026,7 +2026,13 @@ class Crew(FlowTrackable, BaseModel): @staticmethod def _show_tracing_disabled_message() -> None: """Show a message when tracing is disabled.""" - from crewai.events.listeners.tracing.utils import has_user_declined_tracing + from crewai.events.listeners.tracing.utils import ( + has_user_declined_tracing, + should_suppress_tracing_messages, + ) + + if should_suppress_tracing_messages(): + return console = Console() diff --git a/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py b/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py index ee337d7fd..0e4d7d8a2 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py @@ -797,7 +797,13 @@ class TraceCollectionListener(BaseEventListener): from rich.console import Console from rich.panel import Panel - from crewai.events.listeners.tracing.utils import has_user_declined_tracing + from crewai.events.listeners.tracing.utils import ( + has_user_declined_tracing, + should_suppress_tracing_messages, + ) + + if should_suppress_tracing_messages(): + return console = Console() diff --git a/lib/crewai/src/crewai/events/listeners/tracing/utils.py b/lib/crewai/src/crewai/events/listeners/tracing/utils.py index b6bf1026b..a98142619 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/utils.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/utils.py @@ -27,7 +27,34 @@ logger = logging.getLogger(__name__) _tracing_enabled: ContextVar[bool | None] = ContextVar("_tracing_enabled", default=None) -_first_time_trace_hook: Callable[[], bool] | None = None +_first_time_trace_hook: ContextVar[Callable[[], bool] | None] = ContextVar( + "_first_time_trace_hook", default=None +) + +_suppress_tracing_messages: ContextVar[bool] = ContextVar( + "_suppress_tracing_messages", default=False +) + + +def set_suppress_tracing_messages(suppress: bool) -> object: + """Set whether to suppress tracing-related console messages. + + Args: + suppress: True to suppress messages, False to show them. + + Returns: + A token that can be used to restore the previous value. + """ + return _suppress_tracing_messages.set(suppress) + + +def should_suppress_tracing_messages() -> bool: + """Check if tracing messages should be suppressed. + + Returns: + True if messages should be suppressed, False otherwise. + """ + return _suppress_tracing_messages.get() def should_enable_tracing(*, override: bool | None = None) -> bool: @@ -413,8 +440,9 @@ def should_auto_collect_first_time_traces() -> bool: Returns: True if first-time user AND telemetry not disabled AND tracing not explicitly enabled, False otherwise. """ - if _first_time_trace_hook is not None: - return _first_time_trace_hook() + hook = _first_time_trace_hook.get() + if hook is not None: + return hook() if _is_test_environment(): return False @@ -437,6 +465,9 @@ def prompt_user_for_trace_viewing(timeout_seconds: int = 20) -> bool: if _is_test_environment(): return False + if should_suppress_tracing_messages(): + return False + try: import threading diff --git a/lib/crewai/src/crewai/events/utils/console_formatter.py b/lib/crewai/src/crewai/events/utils/console_formatter.py index eaecc0e74..ee466c344 100644 --- a/lib/crewai/src/crewai/events/utils/console_formatter.py +++ b/lib/crewai/src/crewai/events/utils/console_formatter.py @@ -1,3 +1,4 @@ +from contextvars import ContextVar import os import threading from typing import Any, ClassVar, cast @@ -10,6 +11,36 @@ from rich.text import Text from crewai.cli.version import is_newer_version_available +_disable_version_check: ContextVar[bool] = ContextVar( + "_disable_version_check", default=False +) + +_suppress_console_output: ContextVar[bool] = ContextVar( + "_suppress_console_output", default=False +) + + +def set_suppress_console_output(suppress: bool) -> object: + """Set whether to suppress all console output. + + Args: + suppress: True to suppress output, False to show it. + + Returns: + A token that can be used to restore the previous value. + """ + return _suppress_console_output.set(suppress) + + +def should_suppress_console_output() -> bool: + """Check if console output should be suppressed. + + Returns: + True if output should be suppressed, False otherwise. + """ + return _suppress_console_output.get() + + class ConsoleFormatter: tool_usage_counts: ClassVar[dict[str, int]] = {} @@ -46,6 +77,9 @@ class ConsoleFormatter: if not self.verbose: return + if _disable_version_check.get(): + return + if os.getenv("CI", "").lower() in ("true", "1"): return @@ -79,8 +113,12 @@ To update, run: uv sync --upgrade-package crewai""" from crewai.events.listeners.tracing.utils import ( has_user_declined_tracing, is_tracing_enabled_in_context, + should_suppress_tracing_messages, ) + if should_suppress_tracing_messages(): + return + if not is_tracing_enabled_in_context(): if has_user_declined_tracing(): message = """Info: Tracing is disabled. @@ -132,6 +170,8 @@ To enable tracing, do any one of these: def print(self, *args: Any, **kwargs: Any) -> None: """Print to console. Simplified to only handle panel-based output.""" + if should_suppress_console_output(): + return # Skip blank lines during streaming if len(args) == 0 and self._is_streaming: return @@ -488,6 +528,9 @@ To enable tracing, do any one of these: if not self.verbose: return + if should_suppress_console_output(): + return + self._is_streaming = True self._last_stream_call_type = call_type diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index 1b4665620..64bd86d53 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -45,6 +45,7 @@ from crewai.events.listeners.tracing.utils import ( has_user_declined_tracing, set_tracing_enabled, should_enable_tracing, + should_suppress_tracing_messages, ) from crewai.events.types.flow_events import ( FlowCreatedEvent, @@ -2074,12 +2075,14 @@ class Flow(Generic[T], metaclass=FlowMeta): racing_members, other_listeners, listener_result, - triggering_event_id, + current_triggering_event_id, ) else: tasks = [ self._execute_single_listener( - listener_name, listener_result, triggering_event_id + listener_name, + listener_result, + current_triggering_event_id, ) for listener_name in listeners_triggered ] @@ -2626,6 +2629,8 @@ class Flow(Generic[T], metaclass=FlowMeta): @staticmethod def _show_tracing_disabled_message() -> None: """Show a message when tracing is disabled.""" + if should_suppress_tracing_messages(): + return console = Console() diff --git a/lib/crewai/src/crewai/tasks/hallucination_guardrail.py b/lib/crewai/src/crewai/tasks/hallucination_guardrail.py index dd000a83c..c48b890a8 100644 --- a/lib/crewai/src/crewai/tasks/hallucination_guardrail.py +++ b/lib/crewai/src/crewai/tasks/hallucination_guardrail.py @@ -6,6 +6,7 @@ Classes: HallucinationGuardrail: Placeholder guardrail that validates task outputs. """ +from collections.abc import Callable from typing import Any from crewai.llm import LLM @@ -13,32 +14,36 @@ from crewai.tasks.task_output import TaskOutput from crewai.utilities.logger import Logger +_validate_output_hook: Callable[..., tuple[bool, Any]] | None = None + + class HallucinationGuardrail: """Placeholder for the HallucinationGuardrail feature. Attributes: - context: The reference context that outputs would be checked against. + context: Optional reference context that outputs would be checked against. llm: The language model that would be used for evaluation. threshold: Optional minimum faithfulness score that would be required to pass. tool_response: Optional tool response information that would be used in evaluation. Examples: - >>> # Basic usage with default verdict logic + >>> # Basic usage without context (uses task expected_output as context) + >>> guardrail = HallucinationGuardrail(llm=agent.llm) + + >>> # With context for reference >>> guardrail = HallucinationGuardrail( - ... context="AI helps with various tasks including analysis and generation.", ... llm=agent.llm, + ... context="AI helps with various tasks including analysis and generation.", ... ) >>> # With custom threshold for stricter validation >>> strict_guardrail = HallucinationGuardrail( - ... context="Quantum computing uses qubits in superposition.", ... llm=agent.llm, - ... threshold=8.0, # Would require score >= 8 to pass in enterprise version + ... threshold=8.0, # Require score >= 8 to pass ... ) >>> # With tool response for additional context >>> guardrail_with_tools = HallucinationGuardrail( - ... context="The current weather data", ... llm=agent.llm, ... tool_response="Weather API returned: Temperature 22°C, Humidity 65%", ... ) @@ -46,16 +51,17 @@ class HallucinationGuardrail: def __init__( self, - context: str, llm: LLM, + context: str | None = None, threshold: float | None = None, tool_response: str = "", ): """Initialize the HallucinationGuardrail placeholder. Args: - context: The reference context that outputs would be checked against. llm: The language model that would be used for evaluation. + context: Optional reference context that outputs would be checked against. + If not provided, the task's expected_output will be used as context. threshold: Optional minimum faithfulness score that would be required to pass. tool_response: Optional tool response information that would be used in evaluation. """ @@ -78,16 +84,17 @@ class HallucinationGuardrail: def __call__(self, task_output: TaskOutput) -> tuple[bool, Any]: """Validate a task output against hallucination criteria. - In the open source, this method always returns that the output is valid. - Args: task_output: The output to be validated. Returns: A tuple containing: - - True - - The raw task output + - True if validation passed, False otherwise + - The raw task output if valid, or error feedback if invalid """ + if callable(_validate_output_hook): + return _validate_output_hook(self, task_output) + self._logger.log( "warning", "Premium hallucination detection skipped (use for free at https://app.crewai.com)\n", diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py index 1ede7f07d..ee76dc53f 100644 --- a/lib/crewai/src/crewai/utilities/agent_utils.py +++ b/lib/crewai/src/crewai/utilities/agent_utils.py @@ -42,6 +42,8 @@ if TYPE_CHECKING: from crewai.llm import LLM from crewai.task import Task +_create_plus_client_hook: Callable[[], Any] | None = None + class SummaryContent(TypedDict): """Structure for summary content entries. @@ -91,7 +93,11 @@ def parse_tools(tools: list[BaseTool]) -> list[CrewStructuredTool]: for tool in tools: if isinstance(tool, CrewAITool): - tools_list.append(tool.to_structured_tool()) + structured_tool = tool.to_structured_tool() + structured_tool.current_usage_count = 0 + if structured_tool._original_tool: + structured_tool._original_tool.current_usage_count = 0 + tools_list.append(structured_tool) else: raise ValueError("Tool is not a CrewStructuredTool or BaseTool") @@ -818,10 +824,13 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]: if from_repository: import importlib - from crewai.cli.authentication.token import get_auth_token - from crewai.cli.plus_api import PlusAPI + if callable(_create_plus_client_hook): + client = _create_plus_client_hook() + else: + from crewai.cli.authentication.token import get_auth_token + from crewai.cli.plus_api import PlusAPI - client = PlusAPI(api_key=get_auth_token()) + client = PlusAPI(api_key=get_auth_token()) _print_current_organization() response = client.get_agent(from_repository) if response.status_code == 404: diff --git a/lib/crewai/src/crewai/utilities/printer.py b/lib/crewai/src/crewai/utilities/printer.py index c40de684e..949da543a 100644 --- a/lib/crewai/src/crewai/utilities/printer.py +++ b/lib/crewai/src/crewai/utilities/printer.py @@ -4,6 +4,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Final, Literal, NamedTuple +from crewai.events.utils.console_formatter import should_suppress_console_output + if TYPE_CHECKING: from _typeshed import SupportsWrite @@ -77,6 +79,8 @@ class Printer: file: A file-like object (stream); defaults to the current sys.stdout. flush: Whether to forcibly flush the stream. """ + if should_suppress_console_output(): + return if isinstance(content, str): content = [ColoredText(content, color)] print( diff --git a/lib/crewai/tests/agents/agent_adapters/test_base_agent_adapter.py b/lib/crewai/tests/agents/agent_adapters/test_base_agent_adapter.py index 6ed42b5d1..78320d187 100644 --- a/lib/crewai/tests/agents/agent_adapters/test_base_agent_adapter.py +++ b/lib/crewai/tests/agents/agent_adapters/test_base_agent_adapter.py @@ -51,6 +51,10 @@ class ConcreteAgentAdapter(BaseAgentAdapter): # Dummy implementation for MCP tools return [] + def configure_structured_output(self, task: Any) -> None: + # Dummy implementation for structured output + pass + async def aexecute_task( self, task: Any, From 1308bdee63f52e000f5cf201c5a97fa96c825bff Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 5 Feb 2026 21:28:23 -0500 Subject: [PATCH 7/9] feat: add started_event_id and set in eventbus * feat: add started_event_id and set in eventbus * chore: update additional test assumption * fix: restore event bus handlers on context exit fix rollback in crewai events bus so that exiting the context restores the previous _sync_handlers, _async_handlers, _handler_dependencies, and _execution_plan_cache by assigning shallow copies of the saved dicts. previously these were set to empty dicts on exit, which caused registered handlers and cached execution plans to be lost. --- lib/crewai/src/crewai/events/base_events.py | 1 + lib/crewai/src/crewai/events/event_bus.py | 59 ++++++++++++++----- .../tests/memory/test_external_memory.py | 4 ++ .../tests/memory/test_long_term_memory.py | 4 ++ .../tests/memory/test_short_term_memory.py | 4 ++ 5 files changed, 57 insertions(+), 15 deletions(-) diff --git a/lib/crewai/src/crewai/events/base_events.py b/lib/crewai/src/crewai/events/base_events.py index 7148e5e1d..6eeaa06e8 100644 --- a/lib/crewai/src/crewai/events/base_events.py +++ b/lib/crewai/src/crewai/events/base_events.py @@ -63,6 +63,7 @@ class BaseEvent(BaseModel): parent_event_id: str | None = None previous_event_id: str | None = None triggered_by_event_id: str | None = None + started_event_id: str | None = None emission_sequence: int | None = None def to_json(self, exclude: set[str] | None = None) -> Serializable: diff --git a/lib/crewai/src/crewai/events/event_bus.py b/lib/crewai/src/crewai/events/event_bus.py index d0aaa4455..b30d469b9 100644 --- a/lib/crewai/src/crewai/events/event_bus.py +++ b/lib/crewai/src/crewai/events/event_bus.py @@ -407,7 +407,8 @@ class CrewAIEventsBus: if popped is None: handle_empty_pop(event_type_name) else: - _, popped_type = popped + popped_event_id, popped_type = popped + event.started_event_id = popped_event_id expected_start = VALID_EVENT_PAIRS.get(event_type_name) if expected_start and popped_type and popped_type != expected_start: handle_mismatch(event_type_name, popped_type, expected_start) @@ -569,24 +570,52 @@ class CrewAIEventsBus: ... # Do stuff... ... # Handlers are cleared after the context """ - with self._rwlock.w_locked(): - prev_sync = self._sync_handlers - prev_async = self._async_handlers - prev_deps = self._handler_dependencies - prev_cache = self._execution_plan_cache - self._sync_handlers = {} - self._async_handlers = {} - self._handler_dependencies = {} - self._execution_plan_cache = {} + with self._rwlock.r_locked(): + saved_sync: dict[type[BaseEvent], frozenset[SyncHandler]] = dict( + self._sync_handlers + ) + saved_async: dict[type[BaseEvent], frozenset[AsyncHandler]] = dict( + self._async_handlers + ) + saved_deps: dict[type[BaseEvent], dict[Handler, list[Depends[Any]]]] = { + event_type: dict(handlers) + for event_type, handlers in self._handler_dependencies.items() + } + + for event_type, sync_handlers in saved_sync.items(): + for sync_handler in sync_handlers: + self.off(event_type, sync_handler) + + for event_type, async_handlers in saved_async.items(): + for async_handler in async_handlers: + self.off(event_type, async_handler) try: yield finally: - with self._rwlock.w_locked(): - self._sync_handlers = prev_sync - self._async_handlers = prev_async - self._handler_dependencies = prev_deps - self._execution_plan_cache = prev_cache + with self._rwlock.r_locked(): + current_sync = dict(self._sync_handlers) + current_async = dict(self._async_handlers) + + for event_type, cur_sync in current_sync.items(): + orig_sync = saved_sync.get(event_type, frozenset()) + for new_handler in cur_sync - orig_sync: + self.off(event_type, new_handler) + + for event_type, cur_async in current_async.items(): + orig_async = saved_async.get(event_type, frozenset()) + for new_async_handler in cur_async - orig_async: + self.off(event_type, new_async_handler) + + for event_type, sync_handlers in saved_sync.items(): + for sync_handler in sync_handlers: + deps = saved_deps.get(event_type, {}).get(sync_handler) + self._register_handler(event_type, sync_handler, deps) + + for event_type, async_handlers in saved_async.items(): + for async_handler in async_handlers: + deps = saved_deps.get(event_type, {}).get(async_handler) + self._register_handler(event_type, async_handler, deps) def shutdown(self, wait: bool = True) -> None: """Gracefully shutdown the event loop and wait for all tasks to finish. diff --git a/lib/crewai/tests/memory/test_external_memory.py b/lib/crewai/tests/memory/test_external_memory.py index 8718c5aca..1872bc0af 100644 --- a/lib/crewai/tests/memory/test_external_memory.py +++ b/lib/crewai/tests/memory/test_external_memory.py @@ -308,6 +308,7 @@ def test_external_memory_search_events( "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "query": "test value", "limit": 3, @@ -330,6 +331,7 @@ def test_external_memory_search_events( "parent_event_id": ANY, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "query": "test value", "results": [], @@ -390,6 +392,7 @@ def test_external_memory_save_events( "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "value": "saving value", "metadata": {"task": "test_task"}, @@ -411,6 +414,7 @@ def test_external_memory_save_events( "parent_event_id": ANY, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "value": "saving value", "metadata": {"task": "test_task"}, diff --git a/lib/crewai/tests/memory/test_long_term_memory.py b/lib/crewai/tests/memory/test_long_term_memory.py index c33e4469b..500fab169 100644 --- a/lib/crewai/tests/memory/test_long_term_memory.py +++ b/lib/crewai/tests/memory/test_long_term_memory.py @@ -74,6 +74,7 @@ def test_long_term_memory_save_events(long_term_memory): "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "value": "test_task", "metadata": {"task": "test_task", "quality": 0.5}, @@ -94,6 +95,7 @@ def test_long_term_memory_save_events(long_term_memory): "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "value": "test_task", "metadata": { @@ -153,6 +155,7 @@ def test_long_term_memory_search_events(long_term_memory): "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "query": "test query", "limit": 5, @@ -175,6 +178,7 @@ def test_long_term_memory_search_events(long_term_memory): "parent_event_id": ANY, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "query": "test query", "results": None, diff --git a/lib/crewai/tests/memory/test_short_term_memory.py b/lib/crewai/tests/memory/test_short_term_memory.py index 8ea64553a..5e74b688d 100644 --- a/lib/crewai/tests/memory/test_short_term_memory.py +++ b/lib/crewai/tests/memory/test_short_term_memory.py @@ -85,6 +85,7 @@ def test_short_term_memory_search_events(short_term_memory): "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "query": "test value", "limit": 3, @@ -107,6 +108,7 @@ def test_short_term_memory_search_events(short_term_memory): "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "query": "test value", "results": [], @@ -164,6 +166,7 @@ def test_short_term_memory_save_events(short_term_memory): "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "value": "test value", "metadata": {"task": "test_task"}, @@ -185,6 +188,7 @@ def test_short_term_memory_save_events(short_term_memory): "parent_event_id": None, "previous_event_id": ANY, "triggered_by_event_id": None, + "started_event_id": ANY, "emission_sequence": ANY, "value": "test value", "metadata": {"task": "test_task"}, From 7d498b29beaf626a19ae3ef61900594b41f8fcef Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Fri, 6 Feb 2026 14:02:43 -0500 Subject: [PATCH 8/9] fix: event ordering; flow state locks, routing * fix: add current task id context and flow updates introduce a context var for the current task id in `crewai.context` to track task scope. update `Flow._execute_single_listener` to return `(result, event_id)` and adjust callers to unpack it and append `FlowMethodName(str(result))` to `router_results`. set/reset the current task id at the start/end of task execution (async + sync) with minor import and call-site tweaks. * fix: await event futures and flush event bus call `crewai_event_bus.flush()` after crew kickoff. in `Flow`, await event handler futures instead of just collecting them: await pending `_event_futures` before finishing, await emitted futures immediately with try/except to log failures, then clear `_event_futures`. ensures handlers complete and errors surface. * fix: continue iteration on tool completion events expand the loop bridge listener to also trigger on tool completion events (`tool_completed` and `native_tool_completed`) so agent iteration resumes after tools finish. add a `requests.post` mock and response fixture in the liteagent test to simulate platform tool execution. refresh and sanitize vcr cassettes (updated model responses, timestamps, and header placeholders) to reflect tool-call flows and new recordings. * fix: thread-safe state proxies & native routing add thread-safe state proxies and refactor native tool routing. * introduce `LockedListProxy` and `LockedDictProxy` in `flow.py` and update `StateProxy` to return them for list/dict attrs so mutations are protected by the flow lock. * update `AgentExecutor` to use `StateProxy` on flow init, guard the messages setter with the state lock, and return a `StateProxy` from the temp state accessor. * convert `call_llm_native_tools` into a listener (no direct routing return) and add `route_native_tool_result` to route based on state (pending tool calls, final answer, or context error). * minor cleanup in `continue_iteration` to drop orphan listeners on init. * update test cassettes for new native tool call responses, timestamps, and ids. improves concurrency safety for shared state and makes native tool routing explicit. * chore: regen cassettes * chore: regen cassettes, remove duplicate listener call path --- lib/crewai/src/crewai/context.py | 20 + lib/crewai/src/crewai/crew.py | 1 + .../src/crewai/experimental/agent_executor.py | 51 +- lib/crewai/src/crewai/flow/flow.py | 229 ++++++--- lib/crewai/src/crewai/task.py | 5 + lib/crewai/tests/agents/test_lite_agent.py | 12 +- ...est_agent_kickoff_with_platform_tools.yaml | 193 ++++++-- .../test_guardrail_reached_attempt_limit.yaml | 460 ++++++++++-------- .../test_lite_agent_inside_flow_sync.yaml | 37 +- ...est_lite_agent_standalone_still_works.yaml | 37 +- .../test_multiple_agents_in_same_flow.yaml | 77 +-- ...st_native_tool_calling_error_handling.yaml | 65 ++- ..._kickoff_structured_output_with_tools.yaml | 12 +- ...ckoff_structured_output_without_tools.yaml | 21 +- 14 files changed, 745 insertions(+), 475 deletions(-) diff --git a/lib/crewai/src/crewai/context.py b/lib/crewai/src/crewai/context.py index 8edc4fdfb..bf73a221c 100644 --- a/lib/crewai/src/crewai/context.py +++ b/lib/crewai/src/crewai/context.py @@ -43,3 +43,23 @@ def platform_context(integration_token: str) -> Generator[None, Any, None]: yield finally: _platform_integration_token.reset(token) + + +_current_task_id: contextvars.ContextVar[str | None] = contextvars.ContextVar( + "current_task_id", default=None +) + + +def set_current_task_id(task_id: str | None) -> contextvars.Token[str | None]: + """Set the current task ID in the context. Returns a token for reset.""" + return _current_task_id.set(task_id) + + +def reset_current_task_id(token: contextvars.Token[str | None]) -> None: + """Reset the current task ID to its previous value.""" + _current_task_id.reset(token) + + +def get_current_task_id() -> str | None: + """Get the current task ID from the context.""" + return _current_task_id.get() diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index 09a103eba..c69dae65a 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -1517,6 +1517,7 @@ class Crew(FlowTrackable, BaseModel): final_string_output = final_task_output.raw self._finish_execution(final_string_output) self.token_usage = self.calculate_usage_metrics() + crewai_event_bus.flush() crewai_event_bus.emit( self, CrewKickoffCompletedEvent( diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 87f9e83d4..037df6793 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -32,7 +32,8 @@ from crewai.events.types.tool_usage_events import ( ToolUsageFinishedEvent, ToolUsageStartedEvent, ) -from crewai.flow.flow import Flow, listen, or_, router, start +from crewai.flow.flow import Flow, StateProxy, listen, or_, router, start +from crewai.flow.types import FlowMethodName from crewai.hooks.llm_hooks import ( get_after_llm_call_hooks, get_before_llm_call_hooks, @@ -225,7 +226,11 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): @messages.setter def messages(self, value: list[LLMMessage]) -> None: """Delegate to state for ExecutorContext conformance.""" - self._state.messages = value + if self._flow_initialized and hasattr(self, "_state_lock"): + with self._state_lock: + self._state.messages = value + else: + self._state.messages = value @property def ask_for_human_input(self) -> bool: @@ -353,6 +358,8 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): Flow initialization is deferred to prevent event emission during agent setup. Returns the temporary state until invoke() is called. """ + if self._flow_initialized and hasattr(self, "_state_lock"): + return StateProxy(self._state, self._state_lock) # type: ignore[return-value] return self._state @property @@ -461,15 +468,14 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): raise @listen("continue_reasoning_native") - def call_llm_native_tools( - self, - ) -> Literal["native_tool_calls", "native_finished", "context_error"]: + def call_llm_native_tools(self) -> None: """Execute LLM call with native function calling. Always calls the LLM so it can read reflection prompts and decide whether to provide a final answer or request more tools. - Returns routing decision based on whether tool calls or final answer. + Note: This is a listener, not a router. The route_native_tool_result + router fires after this to determine the next step based on state. """ try: # Clear pending tools - LLM will decide what to do next after reading @@ -499,8 +505,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): if isinstance(answer, list) and answer and self._is_tool_call_list(answer): # Store tool calls for sequential processing self.state.pending_tool_calls = list(answer) - - return "native_tool_calls" + return # Router will check pending_tool_calls if isinstance(answer, BaseModel): self.state.current_answer = AgentFinish( @@ -510,7 +515,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): ) self._invoke_step_callback(self.state.current_answer) self._append_message_to_state(answer.model_dump_json()) - return "native_finished" + return # Router will check current_answer # Text response - this is the final answer if isinstance(answer, str): @@ -521,8 +526,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): ) self._invoke_step_callback(self.state.current_answer) self._append_message_to_state(answer) - - return "native_finished" + return # Router will check current_answer # Unexpected response type, treat as final answer self.state.current_answer = AgentFinish( @@ -532,13 +536,12 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): ) self._invoke_step_callback(self.state.current_answer) self._append_message_to_state(str(answer)) - - return "native_finished" + # Router will check current_answer except Exception as e: if is_context_length_exceeded(e): self._last_context_error = e - return "context_error" + return # Router will check _last_context_error if e.__class__.__module__.startswith("litellm"): raise e handle_unknown_error(self._printer, e, verbose=self.agent.verbose) @@ -551,6 +554,22 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): return "execute_tool" return "agent_finished" + @router(call_llm_native_tools) + def route_native_tool_result( + self, + ) -> Literal["native_tool_calls", "native_finished", "context_error"]: + """Route based on LLM response for native tool calling. + + Checks state set by call_llm_native_tools to determine next step. + This router is needed because only router return values trigger + downstream listeners. + """ + if self._last_context_error is not None: + return "context_error" + if self.state.pending_tool_calls: + return "native_tool_calls" + return "native_finished" + @listen("execute_tool") def execute_tool_action(self) -> Literal["tool_completed", "tool_result_is_final"]: """Execute the tool action and handle the result.""" @@ -908,9 +927,11 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): self.state.iterations += 1 return "initialized" - @listen("initialized") + @listen(or_("initialized", "tool_completed", "native_tool_completed")) def continue_iteration(self) -> Literal["check_iteration"]: """Bridge listener that connects iteration loop back to iteration check.""" + if self._flow_initialized: + self._discard_or_listener(FlowMethodName("continue_iteration")) return "check_iteration" @router(or_(initialize_reasoning, continue_iteration)) diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index 64bd86d53..5a6ac4557 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -7,7 +7,14 @@ for building event-driven workflows with conditional execution and routing. from __future__ import annotations import asyncio -from collections.abc import Callable, Sequence +from collections.abc import ( + Callable, + ItemsView, + Iterator, + KeysView, + Sequence, + ValuesView, +) from concurrent.futures import Future import copy import inspect @@ -409,6 +416,132 @@ def and_(*conditions: str | FlowCondition | Callable[..., Any]) -> FlowCondition return {"type": AND_CONDITION, "conditions": processed_conditions} +class LockedListProxy(Generic[T]): + """Thread-safe proxy for list operations. + + Wraps a list and uses a lock for all mutating operations. + """ + + def __init__(self, lst: list[T], lock: threading.Lock) -> None: + self._list = lst + self._lock = lock + + def append(self, item: T) -> None: + with self._lock: + self._list.append(item) + + def extend(self, items: list[T]) -> None: + with self._lock: + self._list.extend(items) + + def insert(self, index: int, item: T) -> None: + with self._lock: + self._list.insert(index, item) + + def remove(self, item: T) -> None: + with self._lock: + self._list.remove(item) + + def pop(self, index: int = -1) -> T: + with self._lock: + return self._list.pop(index) + + def clear(self) -> None: + with self._lock: + self._list.clear() + + def __setitem__(self, index: int, value: T) -> None: + with self._lock: + self._list[index] = value + + def __delitem__(self, index: int) -> None: + with self._lock: + del self._list[index] + + def __getitem__(self, index: int) -> T: + return self._list[index] + + def __len__(self) -> int: + return len(self._list) + + def __iter__(self) -> Iterator[T]: + return iter(self._list) + + def __contains__(self, item: object) -> bool: + return item in self._list + + def __repr__(self) -> str: + return repr(self._list) + + def __bool__(self) -> bool: + return bool(self._list) + + +class LockedDictProxy(Generic[T]): + """Thread-safe proxy for dict operations. + + Wraps a dict and uses a lock for all mutating operations. + """ + + def __init__(self, d: dict[str, T], lock: threading.Lock) -> None: + self._dict = d + self._lock = lock + + def __setitem__(self, key: str, value: T) -> None: + with self._lock: + self._dict[key] = value + + def __delitem__(self, key: str) -> None: + with self._lock: + del self._dict[key] + + def pop(self, key: str, *default: T) -> T: + with self._lock: + return self._dict.pop(key, *default) + + def update(self, other: dict[str, T]) -> None: + with self._lock: + self._dict.update(other) + + def clear(self) -> None: + with self._lock: + self._dict.clear() + + def setdefault(self, key: str, default: T) -> T: + with self._lock: + return self._dict.setdefault(key, default) + + def __getitem__(self, key: str) -> T: + return self._dict[key] + + def __len__(self) -> int: + return len(self._dict) + + def __iter__(self) -> Iterator[str]: + return iter(self._dict) + + def __contains__(self, key: object) -> bool: + return key in self._dict + + def keys(self) -> KeysView[str]: + return self._dict.keys() + + def values(self) -> ValuesView[T]: + return self._dict.values() + + def items(self) -> ItemsView[str, T]: + return self._dict.items() + + def get(self, key: str, default: T | None = None) -> T | None: + return self._dict.get(key, default) + + def __repr__(self) -> str: + return repr(self._dict) + + def __bool__(self) -> bool: + return bool(self._dict) + + class StateProxy(Generic[T]): """Proxy that provides thread-safe access to flow state. @@ -423,7 +556,13 @@ class StateProxy(Generic[T]): object.__setattr__(self, "_proxy_lock", lock) def __getattr__(self, name: str) -> Any: - return getattr(object.__getattribute__(self, "_proxy_state"), name) + value = getattr(object.__getattribute__(self, "_proxy_state"), name) + lock = object.__getattribute__(self, "_proxy_lock") + if isinstance(value, list): + return LockedListProxy(value, lock) + if isinstance(value, dict): + return LockedDictProxy(value, lock) + return value def __setattr__(self, name: str, value: Any) -> None: if name in ("_proxy_state", "_proxy_lock"): @@ -1593,7 +1732,6 @@ class Flow(Generic[T], metaclass=FlowMeta): reset_emission_counter() reset_last_event_id() - # Emit FlowStartedEvent and log the start of the flow. if not self.suppress_flow_events: future = crewai_event_bus.emit( self, @@ -1604,7 +1742,10 @@ class Flow(Generic[T], metaclass=FlowMeta): ), ) if future: - self._event_futures.append(future) + try: + await asyncio.wrap_future(future) + except Exception: + logger.warning("FlowStartedEvent handler failed", exc_info=True) self._log_flow_event( f"Flow started with ID: {self.flow_id}", color="bold magenta" ) @@ -1696,6 +1837,12 @@ class Flow(Generic[T], metaclass=FlowMeta): final_output = self._method_outputs[-1] if self._method_outputs else None + if self._event_futures: + await asyncio.gather( + *[asyncio.wrap_future(f) for f in self._event_futures] + ) + self._event_futures.clear() + if not self.suppress_flow_events: future = crewai_event_bus.emit( self, @@ -1707,13 +1854,12 @@ class Flow(Generic[T], metaclass=FlowMeta): ), ) if future: - self._event_futures.append(future) - - if self._event_futures: - await asyncio.gather( - *[asyncio.wrap_future(f) for f in self._event_futures] - ) - self._event_futures.clear() + try: + await asyncio.wrap_future(future) + except Exception: + logger.warning( + "FlowFinishedEvent handler failed", exc_info=True + ) if not self.suppress_flow_events: trace_listener = TraceCollectionListener() @@ -2027,15 +2173,14 @@ class Flow(Generic[T], metaclass=FlowMeta): router_input = router_result_to_feedback.get( str(current_trigger), current_result ) - current_triggering_event_id = await self._execute_single_listener( + ( + router_result, + current_triggering_event_id, + ) = await self._execute_single_listener( router_name, router_input, current_triggering_event_id ) - # After executing router, the router's result is the path - router_result = ( - self._method_outputs[-1] if self._method_outputs else None - ) if router_result: # Only add non-None results - router_results.append(router_result) + router_results.append(FlowMethodName(str(router_result))) # If this was a human_feedback router, map the outcome to the feedback if self.last_human_feedback is not None: router_result_to_feedback[str(router_result)] = ( @@ -2265,7 +2410,7 @@ class Flow(Generic[T], metaclass=FlowMeta): listener_name: FlowMethodName, result: Any, triggering_event_id: str | None = None, - ) -> str | None: + ) -> tuple[Any, str | None]: """Executes a single listener method with proper event handling. This internal method manages the execution of an individual listener, @@ -2278,8 +2423,9 @@ class Flow(Generic[T], metaclass=FlowMeta): used for causal chain tracking. Returns: - The event_id of the MethodExecutionFinishedEvent emitted by this listener, - or None if events are suppressed. + A tuple of (listener_result, event_id) where listener_result is the return + value of the listener method and event_id is the MethodExecutionFinishedEvent + id, or (None, None) if skipped during resumption. Note: - Inspects method signature to determine if it accepts the trigger result @@ -2305,7 +2451,7 @@ class Flow(Generic[T], metaclass=FlowMeta): ): # This conditional start was executed, continue its chain await self._execute_start_method(start_method_name) - return None + return (None, None) # For cyclic flows, clear from completed to allow re-execution self._completed_methods.discard(listener_name) # Also clear from fired OR listeners for cyclic flows @@ -2343,46 +2489,7 @@ class Flow(Generic[T], metaclass=FlowMeta): listener_name, listener_result, finished_event_id ) - # If this listener is also a router (e.g., has @human_feedback with emit), - # we need to trigger listeners for the router result as well - if listener_name in self._routers and listener_result is not None: - router_result_trigger = FlowMethodName(str(listener_result)) - listeners_for_result = self._find_triggered_methods( - router_result_trigger, router_only=False - ) - if listeners_for_result: - # Pass the HumanFeedbackResult if available - feedback_result = ( - self.last_human_feedback - if self.last_human_feedback is not None - else listener_result - ) - racing_group = self._get_racing_group_for_listeners( - listeners_for_result - ) - if racing_group: - racing_members, _ = racing_group - other_listeners = [ - name - for name in listeners_for_result - if name not in racing_members - ] - await self._execute_racing_listeners( - racing_members, - other_listeners, - feedback_result, - finished_event_id, - ) - else: - tasks = [ - self._execute_single_listener( - name, feedback_result, finished_event_id - ) - for name in listeners_for_result - ] - await asyncio.gather(*tasks) - - return finished_event_id + return (listener_result, finished_event_id) except Exception as e: # Don't log HumanFeedbackPending as an error - it's expected control flow diff --git a/lib/crewai/src/crewai/task.py b/lib/crewai/src/crewai/task.py index d73c3d919..eac42f956 100644 --- a/lib/crewai/src/crewai/task.py +++ b/lib/crewai/src/crewai/task.py @@ -31,6 +31,7 @@ from pydantic_core import PydanticCustomError from typing_extensions import Self from crewai.agents.agent_builder.base_agent import BaseAgent +from crewai.context import reset_current_task_id, set_current_task_id from crewai.core.providers.content_processor import process_content from crewai.events.event_bus import crewai_event_bus from crewai.events.types.task_events import ( @@ -561,6 +562,7 @@ class Task(BaseModel): tools: list[Any] | None, ) -> TaskOutput: """Run the core execution logic of the task asynchronously.""" + task_id_token = set_current_task_id(str(self.id)) self._store_input_files() try: agent = agent or self.agent @@ -648,6 +650,7 @@ class Task(BaseModel): raise e # Re-raise the exception after emitting the event finally: clear_task_files(self.id) + reset_current_task_id(task_id_token) def _execute_core( self, @@ -656,6 +659,7 @@ class Task(BaseModel): tools: list[Any] | None, ) -> TaskOutput: """Run the core execution logic of the task.""" + task_id_token = set_current_task_id(str(self.id)) self._store_input_files() try: agent = agent or self.agent @@ -744,6 +748,7 @@ class Task(BaseModel): raise e # Re-raise the exception after emitting the event finally: clear_task_files(self.id) + reset_current_task_id(task_id_token) def _post_agent_execution(self, agent: BaseAgent) -> None: pass diff --git a/lib/crewai/tests/agents/test_lite_agent.py b/lib/crewai/tests/agents/test_lite_agent.py index 32a7c0ef1..6f989a27c 100644 --- a/lib/crewai/tests/agents/test_lite_agent.py +++ b/lib/crewai/tests/agents/test_lite_agent.py @@ -606,9 +606,10 @@ def test_lite_agent_with_invalid_llm(): @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) +@patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") @pytest.mark.vcr() -def test_agent_kickoff_with_platform_tools(mock_get): +def test_agent_kickoff_with_platform_tools(mock_get, mock_post): """Test that Agent.kickoff() properly integrates platform tools with LiteAgent""" mock_response = Mock() mock_response.raise_for_status.return_value = None @@ -632,6 +633,15 @@ def test_agent_kickoff_with_platform_tools(mock_get): } mock_get.return_value = mock_response + # Mock the platform tool execution + mock_post_response = Mock() + mock_post_response.ok = True + mock_post_response.json.return_value = { + "success": True, + "issue_url": "https://github.com/test/repo/issues/1" + } + mock_post.return_value = mock_post_response + agent = Agent( role="Test Agent", goal="Test goal", diff --git a/lib/crewai/tests/cassettes/agents/test_agent_kickoff_with_platform_tools.yaml b/lib/crewai/tests/cassettes/agents/test_agent_kickoff_with_platform_tools.yaml index 53b4c57e2..72c629c70 100644 --- a/lib/crewai/tests/cassettes/agents/test_agent_kickoff_with_platform_tools.yaml +++ b/lib/crewai/tests/cassettes/agents/test_agent_kickoff_with_platform_tools.yaml @@ -1,98 +1,227 @@ interactions: - request: - body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour personal goal is: Test goal\n\nYou ONLY have access to the following tools, and should NEVER make up tools that are not listed here:\n\nTool Name: create_issue\nTool Arguments: {''title'': {''description'': ''Issue title'', ''type'': ''str''}, ''body'': {''description'': ''Issue body'', ''type'': ''Union[str, NoneType]''}}\nTool Description: Create a GitHub issue\nDetailed Parameter Structure:\nObject with properties:\n - title: Issue title (required)\n - body: Issue body (optional)\n\nIMPORTANT: Use the following format in your response:\n\n```\nThought: you should always think about what to do\nAction: the action to take, only one name of [create_issue], just the name, exactly as it''s written.\nAction Input: the input to the action, just a simple JSON object, enclosed in curly braces, using \" to wrap keys and values.\nObservation: the result of the action\n```\n\nOnce all necessary information - is gathered, return the following format:\n\n```\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n```"}, {"role": "user", "content": "Create a GitHub issue"}], "model": "gpt-3.5-turbo", "stream": false}' + body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour + personal goal is: Test goal"},{"role":"user","content":"\nCurrent Task: Create + a GitHub issue"}],"model":"gpt-3.5-turbo","tool_choice":"auto","tools":[{"type":"function","function":{"name":"create_issue","description":"Create + a GitHub issue","strict":true,"parameters":{"additionalProperties":false,"properties":{"title":{"description":"Issue + title","title":"Title","type":"string"},"body":{"default":null,"description":"Issue + body","title":"Body","type":"string"}},"required":["title","body"],"type":"object"}}}]}' headers: + User-Agent: + - X-USER-AGENT-XXX accept: - application/json accept-encoding: - - gzip, deflate + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX connection: - keep-alive content-length: - - '1233' + - '596' content-type: - application/json host: - api.openai.com - user-agent: - - OpenAI/Python 1.109.1 x-stainless-arch: - - arm64 + - X-STAINLESS-ARCH-XXX x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - - MacOS + - X-STAINLESS-OS-XXX x-stainless-package-version: - - 1.109.1 + - 1.83.0 x-stainless-read-timeout: - - '600' + - X-STAINLESS-READ-TIMEOUT-XXX x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-CULxKTEIB85AVItcEQ09z4Xi0JCID\",\n \"object\": \"chat.completion\",\n \"created\": 1761350274,\n \"model\": \"gpt-3.5-turbo-0125\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"I will need more specific information to create a GitHub issue. Could you please provide more details such as the title and body of the issue you would like to create?\",\n \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 255,\n \"completion_tokens\": 33,\n \"total_tokens\": 288,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n \ - \ }\n },\n \"service_tier\": \"default\",\n \"system_fingerprint\": null\n}\n" + string: "{\n \"id\": \"chatcmpl-D6L3fqygkUIZ3bN4wvSpAhdaSk7MF\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403287,\n \"model\": \"gpt-3.5-turbo-0125\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_RuWuYzjzgRL3byVGhLlPi0rq\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"create_issue\",\n + \ \"arguments\": \"{\\\"title\\\":\\\"Test issue\\\",\\\"body\\\":\\\"This + is a test issue created for testing purposes.\\\"}\"\n }\n }\n + \ ],\n \"refusal\": null,\n \"annotations\": []\n },\n + \ \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n }\n + \ ],\n \"usage\": {\n \"prompt_tokens\": 93,\n \"completion_tokens\": + 28,\n \"total_tokens\": 121,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": null\n}\n" headers: CF-RAY: - - 993d6b4be9862379-SJC + - CF-RAY-XXX Connection: - keep-alive Content-Type: - application/json Date: - - Fri, 24 Oct 2025 23:57:54 GMT + - Fri, 06 Feb 2026 18:41:28 GMT Server: - cloudflare Set-Cookie: - - __cf_bm=WY9bgemMDI_hUYISAPlQ2a.DBGeZfM6AjVEa3SKNg1c-1761350274-1.0.1.1-K3Qm2cl6IlDAgmocoKZ8IMUTmue6Q81hH9stECprUq_SM8LF8rR9d1sHktvRCN3.jEM.twEuFFYDNpBnN8NBRJFZcea1yvpm8Uo0G_UhyDs; path=/; expires=Sat, 25-Oct-25 00:27:54 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=JklLS4i3hBGELpS9cz1KMpTbj72hCwP41LyXDSxWIv8-1761350274521-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + - SET-COOKIE-XXX Strict-Transport-Security: - - max-age=31536000; includeSubDomains; preload + - STS-XXX Transfer-Encoding: - chunked X-Content-Type-Options: - - nosniff + - X-CONTENT-TYPE-XXX access-control-expose-headers: - - X-Request-ID + - ACCESS-CONTROL-XXX alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC openai-organization: - - crewai-iuxna1 + - OPENAI-ORG-XXX openai-processing-ms: - - '487' + - '1406' openai-project: - - proj_xitITlrFeen7zjNSzML82h9x + - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - x-envoy-upstream-service-time: - - '526' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: - - '10000' + - X-RATELIMIT-LIMIT-REQUESTS-XXX x-ratelimit-limit-tokens: - - '50000000' + - X-RATELIMIT-LIMIT-TOKENS-XXX x-ratelimit-remaining-requests: - - '9999' + - X-RATELIMIT-REMAINING-REQUESTS-XXX x-ratelimit-remaining-tokens: - - '49999727' + - X-RATELIMIT-REMAINING-TOKENS-XXX x-ratelimit-reset-requests: - - 6ms + - X-RATELIMIT-RESET-REQUESTS-XXX x-ratelimit-reset-tokens: - - 0s + - X-RATELIMIT-RESET-TOKENS-XXX x-request-id: - - req_1708dc0928c64882aaa5bc2c168c140f + - X-REQUEST-ID-XXX + status: + code: 200 + message: OK +- request: + body: '{"messages":[{"role":"system","content":"You are Test Agent. Test backstory\nYour + personal goal is: Test goal"},{"role":"user","content":"\nCurrent Task: Create + a GitHub issue"},{"role":"assistant","content":null,"tool_calls":[{"id":"call_RuWuYzjzgRL3byVGhLlPi0rq","type":"function","function":{"name":"create_issue","arguments":"{\"title\":\"Test + issue\",\"body\":\"This is a test issue created for testing purposes.\"}"}}]},{"role":"tool","tool_call_id":"call_RuWuYzjzgRL3byVGhLlPi0rq","name":"create_issue","content":"{\n \"success\": + true,\n \"issue_url\": \"https://github.com/test/repo/issues/1\"\n}"}],"model":"gpt-3.5-turbo","tool_choice":"auto","tools":[{"type":"function","function":{"name":"create_issue","description":"Create + a GitHub issue","strict":true,"parameters":{"additionalProperties":false,"properties":{"title":{"description":"Issue + title","title":"Title","type":"string"},"body":{"default":null,"description":"Issue + body","title":"Body","type":"string"}},"required":["title","body"],"type":"object"}}}]}' + headers: + User-Agent: + - X-USER-AGENT-XXX + accept: + - application/json + accept-encoding: + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX + connection: + - keep-alive + content-length: + - '1028' + content-type: + - application/json + cookie: + - COOKIE-XXX + host: + - api.openai.com + x-stainless-arch: + - X-STAINLESS-ARCH-XXX + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - X-STAINLESS-OS-XXX + x-stainless-package-version: + - 1.83.0 + x-stainless-read-timeout: + - X-STAINLESS-READ-TIMEOUT-XXX + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.13.5 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-D6L3hfuBxk36LIb3ekD1IVwFD5VVL\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403289,\n \"model\": \"gpt-3.5-turbo-0125\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"I have successfully created a GitHub + issue for testing purposes. You can view the issue at this URL: [Test issue](https://github.com/test/repo/issues/1)\",\n + \ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": + null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": + 154,\n \"completion_tokens\": 36,\n \"total_tokens\": 190,\n \"prompt_tokens_details\": + {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": null\n}\n" + headers: + CF-RAY: + - CF-RAY-XXX + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 06 Feb 2026 18:41:29 GMT + Server: + - cloudflare + Strict-Transport-Security: + - STS-XXX + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - X-CONTENT-TYPE-XXX + access-control-expose-headers: + - ACCESS-CONTROL-XXX + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - OPENAI-ORG-XXX + openai-processing-ms: + - '888' + openai-project: + - OPENAI-PROJECT-XXX + openai-version: + - '2020-10-01' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - X-RATELIMIT-LIMIT-REQUESTS-XXX + x-ratelimit-limit-tokens: + - X-RATELIMIT-LIMIT-TOKENS-XXX + x-ratelimit-remaining-requests: + - X-RATELIMIT-REMAINING-REQUESTS-XXX + x-ratelimit-remaining-tokens: + - X-RATELIMIT-REMAINING-TOKENS-XXX + x-ratelimit-reset-requests: + - X-RATELIMIT-RESET-REQUESTS-XXX + x-ratelimit-reset-tokens: + - X-RATELIMIT-RESET-TOKENS-XXX + x-request-id: + - X-REQUEST-ID-XXX status: code: 200 message: OK diff --git a/lib/crewai/tests/cassettes/agents/test_guardrail_reached_attempt_limit.yaml b/lib/crewai/tests/cassettes/agents/test_guardrail_reached_attempt_limit.yaml index e5e104fef..fb04df412 100644 --- a/lib/crewai/tests/cassettes/agents/test_guardrail_reached_attempt_limit.yaml +++ b/lib/crewai/tests/cassettes/agents/test_guardrail_reached_attempt_limit.yaml @@ -1,400 +1,428 @@ interactions: - request: - body: '{"messages": [{"role": "system", "content": "You are Sports Analyst. You are an expert at gathering and organizing information. You carefully collect details and present them in a structured way.\nYour personal goal is: Gather information about the best soccer players\n\nTo give my best complete final answer to the task respond using the exact following format:\n\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user", "content": "Top 10 best players in the world?"}], "model": "gpt-4o-mini", "stop": ["\nObservation:"]}' + body: '{"messages":[{"role":"system","content":"You are Sports Analyst. You are + an expert at gathering and organizing information. You carefully collect details + and present them in a structured way.\nYour personal goal is: Gather information + about the best soccer players"},{"role":"user","content":"\nCurrent Task: Top + 10 best players in the world?\n\nProvide your complete response:"}],"model":"gpt-4.1-mini"}' headers: + User-Agent: + - X-USER-AGENT-XXX accept: - application/json accept-encoding: - - gzip, deflate, zstd + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX connection: - keep-alive content-length: - - '694' + - '404' content-type: - application/json host: - api.openai.com - user-agent: - - OpenAI/Python 1.78.0 x-stainless-arch: - - arm64 + - X-STAINLESS-ARCH-XXX x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - - MacOS + - X-STAINLESS-OS-XXX x-stainless-package-version: - - 1.78.0 - x-stainless-raw-response: - - 'true' + - 1.83.0 x-stainless-read-timeout: - - '600.0' + - X-STAINLESS-READ-TIMEOUT-XXX x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.12.9 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-BgufUtDqGzvqPZx2NmkqqxdW4G8rQ\",\n \"object\": \"chat.completion\",\n \"created\": 1749567308,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Thought: I now can give a great answer \\nFinal Answer: The top 10 best soccer players in the world, as of October 2023, can be identified based on their recent performances, skills, impact on games, and overall contributions to their teams. Here is the structured list:\\n\\n1. **Lionel Messi (Inter Miami CF)**\\n - Position: Forward\\n - Key Attributes: Dribbling, vision, goal-scoring ability.\\n - Achievements: Multiple Ballon d'Or winner, Copa America champion, World Cup champion (2022).\\n\\n2. **Kylian Mbappé (Paris Saint-Germain)**\\n - Position: Forward\\n - Key Attributes: Speed, technique, finishing.\\n - Achievements: FIFA World Cup champion (2018), Ligue 1 titles, multiple\ - \ domestic cups.\\n\\n3. **Erling Haaland (Manchester City)**\\n - Position: Forward\\n - Key Attributes: Power, speed, goal-scoring instinct.\\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\\n\\n4. **Kevin De Bruyne (Manchester City)**\\n - Position: Midfielder\\n - Key Attributes: Passing, vision, creativity.\\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\\n\\n5. **Karim Benzema (Al-Ittihad)**\\n - Position: Forward\\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\\n - Achievements: 2022 Ballon d'Or winner, multiple Champions Leagues with Real Madrid.\\n\\n6. **Neymar Jr. (Al Hilal)**\\n - Position: Forward\\n - Key Attributes: Flair, dribbling, creativity.\\n - Achievements: Multiple domestic league titles, Champions League runner-up.\\n\\n7. **Robert Lewandowski (FC Barcelona)**\\n - Position: Forward\\n - Key Attributes: Finishing,\ - \ positioning, aerial ability.\\n - Achievements: FIFA Best Men's Player, multiple Bundesliga titles, La Liga champion (2023).\\n\\n8. **Mohamed Salah (Liverpool)**\\n - Position: Forward\\n - Key Attributes: Speed, finishing, dribbling.\\n - Achievements: Premier League champion, FA Cup, UEFA Champions League winner.\\n\\n9. **Vinícius Júnior (Real Madrid)**\\n - Position: Forward\\n - Key Attributes: Speed, dribbling, creativity.\\n - Achievements: UEFA Champions League winner (2022), La Liga champion (2023).\\n\\n10. **Luka Modrić (Real Madrid)**\\n - Position: Midfielder\\n - Key Attributes: Passing, vision, tactical intelligence.\\n - Achievements: Multiple Champions League titles, Ballon d'Or winner (2018).\\n\\nThis list is compiled based on their current form, past performances, and contributions to their respective teams in both domestic and international competitions. Player rankings can vary based on personal opinion and specific criteria used for\ - \ evaluation, but these players have consistently been regarded as some of the best in the world as of October 2023.\",\n \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 122,\n \"completion_tokens\": 643,\n \"total_tokens\": 765,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": \"default\",\n \"system_fingerprint\": \"fp_34a54ae93c\"\n}\n" + string: "{\n \"id\": \"chatcmpl-D6L3hzoRVVEa07HZsM9wpi2RVRKQp\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403289,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"Here is a structured list of the top + 10 best soccer players in the world as of 2024, based on recent performances, + awards, and overall impact on the game:\\n\\n1. **Kylian Mbapp\xE9** \\n + \ - Nationality: French \\n - Club: Paris Saint-Germain (PSG) \\n - + Position: Forward \\n - Key Highlights: Multiple Ligue 1 titles, World + Cup winner (2018), known for speed, dribbling, and scoring prowess.\\n\\n2. + **Erling Haaland** \\n - Nationality: Norwegian \\n - Club: Manchester + City \\n - Position: Striker \\n - Key Highlights: Premier League Golden + Boot winner, incredible goal-scoring record, physical presence, and finishing + skills.\\n\\n3. **Lionel Messi** \\n - Nationality: Argentine \\n - + Club: Inter Miami \\n - Position: Forward/Attacking Midfielder \\n - + Key Highlights: Seven Ballon d\u2019Or awards, World Cup winner (2022), exceptional + playmaking and dribbling ability.\\n\\n4. **Kevin De Bruyne** \\n - Nationality: + Belgian \\n - Club: Manchester City \\n - Position: Midfielder \\n + \ - Key Highlights: One of the best playmakers globally, assists leader, + consistent high-level performance in the Premier League.\\n\\n5. **Robert + Lewandowski** \\n - Nationality: Polish \\n - Club: FC Barcelona \\n + \ - Position: Striker \\n - Key Highlights: Exceptional goal-scoring record, + multiple Bundesliga top scorer awards, key figure in Bayern Munich\u2019s + dominance before transferring.\\n\\n6. **Karim Benzema** \\n - Nationality: + French \\n - Club: Al-Ittihad \\n - Position: Striker \\n - Key Highlights: + Ballon d\u2019Or winner (2022), excellent technical skills, leadership at + Real Madrid before recent transfer.\\n\\n7. **Mohamed Salah** \\n - Nationality: + Egyptian \\n - Club: Liverpool \\n - Position: Forward \\n - Key + Highlights: Premier League Golden Boot winner, known for speed, dribbling, + and goal-scoring consistency.\\n\\n8. **Vin\xEDcius J\xFAnior** \\n - Nationality: + Brazilian \\n - Club: Real Madrid \\n - Position: Winger \\n - Key + Highlights: Key player for Real Madrid, exceptional dribbling and pace, rising + star in world football.\\n\\n9. **Jude Bellingham** \\n - Nationality: + English \\n - Club: Real Madrid \\n - Position: Midfielder \\n - + Key Highlights: Young talent with maturity beyond years, influential midfielder + with great vision and work rate.\\n\\n10. **Thibaut Courtois** \\n - Nationality: + Belgian \\n - Club: Real Madrid \\n - Position: Goalkeeper \\n - + Key Highlights: One of the best goalkeepers globally, crucial performances + in La Liga and Champions League.\\n\\nThese rankings consider individual talent, + recent achievements, influence on matches, and overall contribution to club + and country.\",\n \"refusal\": null,\n \"annotations\": []\n + \ },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n + \ ],\n \"usage\": {\n \"prompt_tokens\": 68,\n \"completion_tokens\": + 621,\n \"total_tokens\": 689,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_75546bd1a7\"\n}\n" headers: CF-RAY: - - 94d9b5400dcd624b-GRU + - CF-RAY-XXX Connection: - keep-alive Content-Type: - application/json Date: - - Tue, 10 Jun 2025 14:55:42 GMT + - Fri, 06 Feb 2026 18:41:40 GMT Server: - cloudflare Set-Cookie: - - __cf_bm=8Yv8F0ZCFAo2lf.qoqxao70yxyjVvIV90zQqVF6bVzQ-1749567342-1.0.1.1-fZgnv3RDfunvCO1koxwwFJrHnxSx_rwS_FHvQ6xxDPpKHwYr7dTqIQLZrNgSX5twGyK4F22rUmkuiS6KMVogcinChk8lmHtJBTUVTFjr2KU; path=/; expires=Tue, 10-Jun-25 15:25:42 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=wzh8YnmXvLq1G0RcIVijtzboQtCZyIe2uZiochkBLqE-1749567342267-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + - SET-COOKIE-XXX + Strict-Transport-Security: + - STS-XXX Transfer-Encoding: - chunked X-Content-Type-Options: - - nosniff + - X-CONTENT-TYPE-XXX access-control-expose-headers: - - X-Request-ID + - ACCESS-CONTROL-XXX alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC openai-organization: - - crewai-iuxna1 + - OPENAI-ORG-XXX openai-processing-ms: - - '33288' + - '10634' + openai-project: + - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '33292' + x-openai-proxy-wasm: + - v0.1 x-ratelimit-limit-requests: - - '30000' + - X-RATELIMIT-LIMIT-REQUESTS-XXX x-ratelimit-limit-tokens: - - '150000000' + - X-RATELIMIT-LIMIT-TOKENS-XXX x-ratelimit-remaining-requests: - - '29999' + - X-RATELIMIT-REMAINING-REQUESTS-XXX x-ratelimit-remaining-tokens: - - '149999859' + - X-RATELIMIT-REMAINING-TOKENS-XXX x-ratelimit-reset-requests: - - 2ms + - X-RATELIMIT-RESET-REQUESTS-XXX x-ratelimit-reset-tokens: - - 0s + - X-RATELIMIT-RESET-TOKENS-XXX x-request-id: - - req_6a587ea22edef774ecdada790a320cab + - X-REQUEST-ID-XXX status: code: 200 message: OK - request: - body: '{"messages": [{"role": "system", "content": "You are Sports Analyst. You are an expert at gathering and organizing information. You carefully collect details and present them in a structured way.\nYour personal goal is: Gather information about the best soccer players\n\nTo give my best complete final answer to the task respond using the exact following format:\n\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user", "content": "Top 10 best players in the world?"}, {"role": "assistant", "content": "Thought: I now can give a great answer \nFinal Answer: The top 10 best soccer players in the world, as of October 2023, can be identified based on their recent performances, skills, impact on games, and overall contributions to their teams. Here is the structured list:\n\n1. **Lionel Messi (Inter Miami CF)**\n - - Position: Forward\n - Key Attributes: Dribbling, vision, goal-scoring ability.\n - Achievements: Multiple Ballon d''Or winner, Copa America champion, World Cup champion (2022).\n\n2. **Kylian Mbapp\u00e9 (Paris Saint-Germain)**\n - Position: Forward\n - Key Attributes: Speed, technique, finishing.\n - Achievements: FIFA World Cup champion (2018), Ligue 1 titles, multiple domestic cups.\n\n3. **Erling Haaland (Manchester City)**\n - Position: Forward\n - Key Attributes: Power, speed, goal-scoring instinct.\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\n\n4. **Kevin De Bruyne (Manchester City)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, creativity.\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\n\n5. **Karim Benzema (Al-Ittihad)**\n - Position: Forward\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\n - Achievements: - 2022 Ballon d''Or winner, multiple Champions Leagues with Real Madrid.\n\n6. **Neymar Jr. (Al Hilal)**\n - Position: Forward\n - Key Attributes: Flair, dribbling, creativity.\n - Achievements: Multiple domestic league titles, Champions League runner-up.\n\n7. **Robert Lewandowski (FC Barcelona)**\n - Position: Forward\n - Key Attributes: Finishing, positioning, aerial ability.\n - Achievements: FIFA Best Men''s Player, multiple Bundesliga titles, La Liga champion (2023).\n\n8. **Mohamed Salah (Liverpool)**\n - Position: Forward\n - Key Attributes: Speed, finishing, dribbling.\n - Achievements: Premier League champion, FA Cup, UEFA Champions League winner.\n\n9. **Vin\u00edcius J\u00fanior (Real Madrid)**\n - Position: Forward\n - Key Attributes: Speed, dribbling, creativity.\n - Achievements: UEFA Champions League winner (2022), La Liga champion (2023).\n\n10. **Luka Modri\u0107 (Real Madrid)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, - tactical intelligence.\n - Achievements: Multiple Champions League titles, Ballon d''Or winner (2018).\n\nThis list is compiled based on their current form, past performances, and contributions to their respective teams in both domestic and international competitions. Player rankings can vary based on personal opinion and specific criteria used for evaluation, but these players have consistently been regarded as some of the best in the world as of October 2023."}, {"role": "user", "content": "You are not allowed to include Brazilian players"}], "model": "gpt-4o-mini", "stop": ["\nObservation:"]}' + body: '{"messages":[{"role":"system","content":"You are Sports Analyst. You are + an expert at gathering and organizing information. You carefully collect details + and present them in a structured way.\nYour personal goal is: Gather information + about the best soccer players"},{"role":"user","content":"\nCurrent Task: Top + 10 best players in the world?\n\nProvide your complete response:"}],"model":"gpt-4.1-mini"}' headers: + User-Agent: + - X-USER-AGENT-XXX accept: - application/json accept-encoding: - - gzip, deflate, zstd + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX connection: - keep-alive content-length: - - '3594' + - '404' content-type: - application/json cookie: - - __cf_bm=8Yv8F0ZCFAo2lf.qoqxao70yxyjVvIV90zQqVF6bVzQ-1749567342-1.0.1.1-fZgnv3RDfunvCO1koxwwFJrHnxSx_rwS_FHvQ6xxDPpKHwYr7dTqIQLZrNgSX5twGyK4F22rUmkuiS6KMVogcinChk8lmHtJBTUVTFjr2KU; _cfuvid=wzh8YnmXvLq1G0RcIVijtzboQtCZyIe2uZiochkBLqE-1749567342267-0.0.1.1-604800000 + - COOKIE-XXX host: - api.openai.com - user-agent: - - OpenAI/Python 1.78.0 x-stainless-arch: - - arm64 + - X-STAINLESS-ARCH-XXX x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - - MacOS + - X-STAINLESS-OS-XXX x-stainless-package-version: - - 1.78.0 - x-stainless-raw-response: - - 'true' + - 1.83.0 x-stainless-read-timeout: - - '600.0' + - X-STAINLESS-READ-TIMEOUT-XXX x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.12.9 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-BgugJkCDtB2EfvAMiIFK0reeLKFBl\",\n \"object\": \"chat.completion\",\n \"created\": 1749567359,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Thought: I now can give a great answer \\nFinal Answer: Here is an updated list of the top 10 best soccer players in the world as of October 2023, excluding Brazilian players:\\n\\n1. **Lionel Messi (Inter Miami CF)**\\n - Position: Forward\\n - Key Attributes: Dribbling, vision, goal-scoring ability.\\n - Achievements: Multiple Ballon d'Or winner, Copa America champion, World Cup champion (2022).\\n\\n2. **Kylian Mbappé (Paris Saint-Germain)**\\n - Position: Forward\\n - Key Attributes: Speed, technique, finishing.\\n - Achievements: FIFA World Cup champion (2018), Ligue 1 titles, multiple domestic cups.\\n\\n3. **Erling Haaland (Manchester City)**\\n - Position: Forward\\\ - n - Key Attributes: Power, speed, goal-scoring instinct.\\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\\n\\n4. **Kevin De Bruyne (Manchester City)**\\n - Position: Midfielder\\n - Key Attributes: Passing, vision, creativity.\\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\\n\\n5. **Karim Benzema (Al-Ittihad)**\\n - Position: Forward\\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\\n - Achievements: 2022 Ballon d'Or winner, multiple Champions Leagues with Real Madrid.\\n\\n6. **Robert Lewandowski (FC Barcelona)**\\n - Position: Forward\\n - Key Attributes: Finishing, positioning, aerial ability.\\n - Achievements: FIFA Best Men's Player, multiple Bundesliga titles, La Liga champion (2023).\\n\\n7. **Mohamed Salah (Liverpool)**\\n - Position: Forward\\n - Key Attributes: Speed, finishing, dribbling.\\n - Achievements: Premier League\ - \ champion, FA Cup, UEFA Champions League winner.\\n\\n8. **Vinícius Júnior (Real Madrid)**\\n - Position: Forward\\n - Key Attributes: Speed, dribbling, creativity.\\n - Achievements: UEFA Champions League winner (2022), La Liga champion (2023).\\n\\n9. **Luka Modrić (Real Madrid)**\\n - Position: Midfielder\\n - Key Attributes: Passing, vision, tactical intelligence.\\n - Achievements: Multiple Champions League titles, Ballon d'Or winner (2018).\\n\\n10. **Harry Kane (Bayern Munich)**\\n - Position: Forward\\n - Key Attributes: Goal-scoring, technique, playmaking.\\n - Achievements: Golden Boot winner, Premier League titles, UEFA European Championship runner-up.\\n\\nThis list has been adjusted to exclude Brazilian players and focuses on those who have made significant impacts in their clubs and on the international stage as of October 2023. Each player is recognized for their exceptional skills, performances, and achievements.\",\n \"refusal\": null,\n\ - \ \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 781,\n \"completion_tokens\": 610,\n \"total_tokens\": 1391,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": \"default\",\n \"system_fingerprint\": \"fp_34a54ae93c\"\n}\n" + string: "{\n \"id\": \"chatcmpl-D6L3sn9nSnGGOMKrS88avliVF7XTv\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403300,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"Certainly! Here's a structured list + of the top 10 best soccer players in the world as of 2024, considering their + performance, skills, achievements, and impact in recent seasons:\\n\\n### + Top 10 Best Soccer Players in the World (2024)\\n\\n| Rank | Player Name | + Nationality | Club (2023/24 Season) | Position | Key Attributes + \ | Recent Achievements |\\n|-------|---------------------|-------------|----------------------------|------------------|---------------------------------|------------------------------------|\\n| + 1 | Lionel Messi | Argentina | Paris Saint-Germain (PSG) | + Forward/Playmaker| Dribbling, Vision, Free kicks | 2023 World Cup Golden + Ball, Club Successes |\\n| 2 | Kylian Mbapp\xE9 | France | + Paris Saint-Germain (PSG) | Forward | Speed, Finishing, Dribbling + \ | Ligue 1 Top Scorer, World Cup Winner 2018|\\n| 3 | Erling Haaland + \ | Norway | Manchester City | Striker | Strength, + Finishing, Positioning| Premier League Golden Boot, Champions League Impact|\\n| + 4 | Kevin De Bruyne | Belgium | Manchester City | + Midfielder | Passing, Vision, Creativity | Premier League Titles, + Key Playmaker|\\n| 5 | Robert Lewandowski | Poland | FC Barcelona + \ | Striker | Finishing, Positioning, Composure| La + Liga Top Scorer, Consistent Scorer|\\n| 6 | Neymar Jr. | Brazil + \ | Al-Hilal | Forward/Winger | Dribbling, Creativity, + Flair | Copa America Titles, Club Success |\\n| 7 | Mohamed Salah | + Egypt | Liverpool | Forward/Winger | Pace, Finishing, + Work Rate | Premier League Golden Boot, Champions League Winner|\\n| + 8 | Vin\xEDcius Jr. | Brazil | Real Madrid | + Winger | Speed, Dribbling, Crossing | La Liga Titles, UEFA Champions + League Winner|\\n| 9 | Luka Modri\u0107 | Croatia | Real Madrid + \ | Midfielder | Passing, Control, Experience | Ballon + d\u2019Or 2018, Multiple Champions League Titles|\\n| 10 | Karim Benzema + \ | France | Al-Ittihad | Striker | Finishing, + Link-up Play, Movements| Ballon d\u2019Or 2022, UEFA Champions League Top + Scorer |\\n\\n### Notes:\\n- The rankings reflect a combination of individual + skill, recent performance, consistency, and influence on the game.\\n- Players\u2019 + clubs are based on the 2023/24 season affiliations.\\n- Achievements highlight + recent titles, awards, or standout contributions.\\n\\nIf you would like me + to focus on specific leagues, historical players, or emerging talents, just + let me know!\",\n \"refusal\": null,\n \"annotations\": []\n + \ },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n + \ ],\n \"usage\": {\n \"prompt_tokens\": 68,\n \"completion_tokens\": + 605,\n \"total_tokens\": 673,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_75546bd1a7\"\n}\n" headers: CF-RAY: - - 94d9b6782db84d3b-GRU + - CF-RAY-XXX Connection: - keep-alive Content-Type: - application/json Date: - - Tue, 10 Jun 2025 14:56:30 GMT + - Fri, 06 Feb 2026 18:41:49 GMT Server: - cloudflare + Strict-Transport-Security: + - STS-XXX Transfer-Encoding: - chunked X-Content-Type-Options: - - nosniff + - X-CONTENT-TYPE-XXX access-control-expose-headers: - - X-Request-ID + - ACCESS-CONTROL-XXX alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC openai-organization: - - crewai-iuxna1 + - OPENAI-ORG-XXX openai-processing-ms: - - '31484' + - '9044' + openai-project: + - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '31490' + x-openai-proxy-wasm: + - v0.1 x-ratelimit-limit-requests: - - '30000' + - X-RATELIMIT-LIMIT-REQUESTS-XXX x-ratelimit-limit-tokens: - - '150000000' + - X-RATELIMIT-LIMIT-TOKENS-XXX x-ratelimit-remaining-requests: - - '29999' + - X-RATELIMIT-REMAINING-REQUESTS-XXX x-ratelimit-remaining-tokens: - - '149999166' + - X-RATELIMIT-REMAINING-TOKENS-XXX x-ratelimit-reset-requests: - - 2ms + - X-RATELIMIT-RESET-REQUESTS-XXX x-ratelimit-reset-tokens: - - 0s + - X-RATELIMIT-RESET-TOKENS-XXX x-request-id: - - req_aa737cf40bb76af9f458bfd35f7a77a1 + - X-REQUEST-ID-XXX status: code: 200 message: OK - request: - body: '{"messages": [{"role": "system", "content": "You are Sports Analyst. You are an expert at gathering and organizing information. You carefully collect details and present them in a structured way.\nYour personal goal is: Gather information about the best soccer players\n\nTo give my best complete final answer to the task respond using the exact following format:\n\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user", "content": "Top 10 best players in the world?"}, {"role": "assistant", "content": "Thought: I now can give a great answer \nFinal Answer: The top 10 best soccer players in the world, as of October 2023, can be identified based on their recent performances, skills, impact on games, and overall contributions to their teams. Here is the structured list:\n\n1. **Lionel Messi (Inter Miami CF)**\n - - Position: Forward\n - Key Attributes: Dribbling, vision, goal-scoring ability.\n - Achievements: Multiple Ballon d''Or winner, Copa America champion, World Cup champion (2022).\n\n2. **Kylian Mbapp\u00e9 (Paris Saint-Germain)**\n - Position: Forward\n - Key Attributes: Speed, technique, finishing.\n - Achievements: FIFA World Cup champion (2018), Ligue 1 titles, multiple domestic cups.\n\n3. **Erling Haaland (Manchester City)**\n - Position: Forward\n - Key Attributes: Power, speed, goal-scoring instinct.\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\n\n4. **Kevin De Bruyne (Manchester City)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, creativity.\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\n\n5. **Karim Benzema (Al-Ittihad)**\n - Position: Forward\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\n - Achievements: - 2022 Ballon d''Or winner, multiple Champions Leagues with Real Madrid.\n\n6. **Neymar Jr. (Al Hilal)**\n - Position: Forward\n - Key Attributes: Flair, dribbling, creativity.\n - Achievements: Multiple domestic league titles, Champions League runner-up.\n\n7. **Robert Lewandowski (FC Barcelona)**\n - Position: Forward\n - Key Attributes: Finishing, positioning, aerial ability.\n - Achievements: FIFA Best Men''s Player, multiple Bundesliga titles, La Liga champion (2023).\n\n8. **Mohamed Salah (Liverpool)**\n - Position: Forward\n - Key Attributes: Speed, finishing, dribbling.\n - Achievements: Premier League champion, FA Cup, UEFA Champions League winner.\n\n9. **Vin\u00edcius J\u00fanior (Real Madrid)**\n - Position: Forward\n - Key Attributes: Speed, dribbling, creativity.\n - Achievements: UEFA Champions League winner (2022), La Liga champion (2023).\n\n10. **Luka Modri\u0107 (Real Madrid)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, - tactical intelligence.\n - Achievements: Multiple Champions League titles, Ballon d''Or winner (2018).\n\nThis list is compiled based on their current form, past performances, and contributions to their respective teams in both domestic and international competitions. Player rankings can vary based on personal opinion and specific criteria used for evaluation, but these players have consistently been regarded as some of the best in the world as of October 2023."}, {"role": "user", "content": "You are not allowed to include Brazilian players"}, {"role": "assistant", "content": "Thought: I now can give a great answer \nFinal Answer: Here is an updated list of the top 10 best soccer players in the world as of October 2023, excluding Brazilian players:\n\n1. **Lionel Messi (Inter Miami CF)**\n - Position: Forward\n - Key Attributes: Dribbling, vision, goal-scoring ability.\n - Achievements: Multiple Ballon d''Or winner, Copa America champion, World Cup champion (2022).\n\n2. - **Kylian Mbapp\u00e9 (Paris Saint-Germain)**\n - Position: Forward\n - Key Attributes: Speed, technique, finishing.\n - Achievements: FIFA World Cup champion (2018), Ligue 1 titles, multiple domestic cups.\n\n3. **Erling Haaland (Manchester City)**\n - Position: Forward\n - Key Attributes: Power, speed, goal-scoring instinct.\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\n\n4. **Kevin De Bruyne (Manchester City)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, creativity.\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\n\n5. **Karim Benzema (Al-Ittihad)**\n - Position: Forward\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\n - Achievements: 2022 Ballon d''Or winner, multiple Champions Leagues with Real Madrid.\n\n6. **Robert Lewandowski (FC Barcelona)**\n - Position: Forward\n - Key Attributes: Finishing, positioning, - aerial ability.\n - Achievements: FIFA Best Men''s Player, multiple Bundesliga titles, La Liga champion (2023).\n\n7. **Mohamed Salah (Liverpool)**\n - Position: Forward\n - Key Attributes: Speed, finishing, dribbling.\n - Achievements: Premier League champion, FA Cup, UEFA Champions League winner.\n\n8. **Vin\u00edcius J\u00fanior (Real Madrid)**\n - Position: Forward\n - Key Attributes: Speed, dribbling, creativity.\n - Achievements: UEFA Champions League winner (2022), La Liga champion (2023).\n\n9. **Luka Modri\u0107 (Real Madrid)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, tactical intelligence.\n - Achievements: Multiple Champions League titles, Ballon d''Or winner (2018).\n\n10. **Harry Kane (Bayern Munich)**\n - Position: Forward\n - Key Attributes: Goal-scoring, technique, playmaking.\n - Achievements: Golden Boot winner, Premier League titles, UEFA European Championship runner-up.\n\nThis list has been adjusted to exclude Brazilian - players and focuses on those who have made significant impacts in their clubs and on the international stage as of October 2023. Each player is recognized for their exceptional skills, performances, and achievements."}, {"role": "user", "content": "You are not allowed to include Brazilian players"}], "model": "gpt-4o-mini", "stop": ["\nObservation:"]}' + body: '{"messages":[{"role":"system","content":"You are Sports Analyst. You are + an expert at gathering and organizing information. You carefully collect details + and present them in a structured way.\nYour personal goal is: Gather information + about the best soccer players"},{"role":"user","content":"\nCurrent Task: Top + 10 best players in the world?\n\nProvide your complete response:"}],"model":"gpt-4.1-mini"}' headers: + User-Agent: + - X-USER-AGENT-XXX accept: - application/json accept-encoding: - - gzip, deflate, zstd + - ACCEPT-ENCODING-XXX + authorization: + - AUTHORIZATION-XXX connection: - keep-alive content-length: - - '6337' + - '404' content-type: - application/json cookie: - - __cf_bm=8Yv8F0ZCFAo2lf.qoqxao70yxyjVvIV90zQqVF6bVzQ-1749567342-1.0.1.1-fZgnv3RDfunvCO1koxwwFJrHnxSx_rwS_FHvQ6xxDPpKHwYr7dTqIQLZrNgSX5twGyK4F22rUmkuiS6KMVogcinChk8lmHtJBTUVTFjr2KU; _cfuvid=wzh8YnmXvLq1G0RcIVijtzboQtCZyIe2uZiochkBLqE-1749567342267-0.0.1.1-604800000 + - COOKIE-XXX host: - api.openai.com - user-agent: - - OpenAI/Python 1.78.0 x-stainless-arch: - - arm64 + - X-STAINLESS-ARCH-XXX x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - - MacOS + - X-STAINLESS-OS-XXX x-stainless-package-version: - - 1.78.0 - x-stainless-raw-response: - - 'true' + - 1.83.0 x-stainless-read-timeout: - - '600.0' + - X-STAINLESS-READ-TIMEOUT-XXX x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.12.9 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-BgugsAmyI50uQ6SpCp89ZZY4eD1Pz\",\n \"object\": \"chat.completion\",\n \"created\": 1749567394,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Thought: I now can give a great answer \\nFinal Answer: Here is the revised list of the top 10 best soccer players in the world as of October 2023, explicitly excluding Brazilian players:\\n\\n1. **Lionel Messi (Inter Miami CF)**\\n - Position: Forward\\n - Key Attributes: Dribbling, vision, goal-scoring ability.\\n - Achievements: Multiple Ballon d'Or winner, Copa America champion, World Cup champion (2022).\\n\\n2. **Kylian Mbappé (Paris Saint-Germain)**\\n - Position: Forward\\n - Key Attributes: Speed, technique, finishing.\\n - Achievements: FIFA World Cup champion (2018), multiple Ligue 1 titles, and various domestic cups.\\n\\n3. **Erling Haaland (Manchester City)**\\n\ - \ - Position: Forward\\n - Key Attributes: Power, speed, goal-scoring instinct.\\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\\n\\n4. **Kevin De Bruyne (Manchester City)**\\n - Position: Midfielder\\n - Key Attributes: Passing, vision, creativity.\\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\\n\\n5. **Karim Benzema (Al-Ittihad)**\\n - Position: Forward\\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\\n - Achievements: 2022 Ballon d'Or winner, multiple Champions Leagues with Real Madrid.\\n\\n6. **Robert Lewandowski (FC Barcelona)**\\n - Position: Forward\\n - Key Attributes: Finishing, positioning, aerial ability.\\n - Achievements: FIFA Best Men's Player, multiple Bundesliga titles, La Liga champion (2023).\\n\\n7. **Mohamed Salah (Liverpool)**\\n - Position: Forward\\n - Key Attributes: Speed, finishing, dribbling.\\n -\ - \ Achievements: Premier League champion, FA Cup, UEFA Champions League winner.\\n\\n8. **Luka Modrić (Real Madrid)**\\n - Position: Midfielder\\n - Key Attributes: Passing, vision, tactical intelligence.\\n - Achievements: Multiple Champions League titles, Ballon d'Or winner (2018).\\n\\n9. **Harry Kane (Bayern Munich)**\\n - Position: Forward\\n - Key Attributes: Goal-scoring, technique, playmaking.\\n - Achievements: Golden Boot winner, Premier League titles, UEFA European Championship runner-up.\\n\\n10. **Rodri (Manchester City)**\\n - Position: Midfielder\\n - Key Attributes: Defensive skills, passing, positional awareness.\\n - Achievements: Premier League titles, UEFA Champions League winner (2023).\\n\\nThis list is curated while adhering to the restriction of excluding Brazilian players. Each player included has demonstrated exceptional skills and remarkable performances, solidifying their status as some of the best in the world as of October 2023.\"\ - ,\n \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 1407,\n \"completion_tokens\": 605,\n \"total_tokens\": 2012,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": \"default\",\n \"system_fingerprint\": \"fp_62a23a81ef\"\n}\n" + string: "{\n \"id\": \"chatcmpl-D6L4102eMwTEPeHxfyN9Kh7rjBoX6\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403309,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"Certainly! Here is a list of the top + 10 best soccer players in the world as of 2024, considering their recent performances, + skills, impact, and accolades:\\n\\n1. **Lionel Messi** \\n - Nationality: + Argentine \\n - Position: Forward \\n - Key Achievements: 7 Ballon d'Or + awards, led Argentina to 2021 Copa Am\xE9rica victory and 2022 FIFA World + Cup triumph, exceptional dribbling and playmaking skills.\\n\\n2. **Kylian + Mbapp\xE9** \\n - Nationality: French \\n - Position: Forward \\n - + Key Achievements: FIFA World Cup winner (2018), multiple Ligue 1 titles, known + for incredible speed, finishing, and consistency.\\n\\n3. **Erling Haaland** + \ \\n - Nationality: Norwegian \\n - Position: Striker \\n - Key Achievements: + Premier League Golden Boot winner (2022-23), prolific goal scorer, physical + presence, and finishing ability.\\n\\n4. **Karim Benzema** \\n - Nationality: + French \\n - Position: Forward \\n - Key Achievements: 2022 Ballon d'Or + winner, key player for Real Madrid\u2019s recent Champions League victories, + excellent technical skills and leadership.\\n\\n5. **Kevin De Bruyne** \\n + \ - Nationality: Belgian \\n - Position: Midfielder \\n - Key Achievements: + Premier League playmaker, known for vision, passing accuracy, and creativity.\\n\\n6. + **Robert Lewandowski** \\n - Nationality: Polish \\n - Position: Striker + \ \\n - Key Achievements: Multiple Bundesliga top scorer titles, consistent + goal scorer, known for positioning and finishing.\\n\\n7. **Neymar Jr.** \\n + \ - Nationality: Brazilian \\n - Position: Forward \\n - Key Achievements: + Exceptional dribbling, creativity, and flair; multiple domestic titles and + Copa Libertadores winner.\\n\\n8. **Mohamed Salah** \\n - Nationality: + Egyptian \\n - Position: Forward \\n - Key Achievements: Premier League + Golden Boot, consistent goal scoring with Liverpool, known for speed and finishing.\\n\\n9. + **Luka Modri\u0107** \\n - Nationality: Croatian \\n - Position: Midfielder + \ \\n - Key Achievements: 2018 Ballon d\u2019Or winner, pivotal midfield + maestro, excellent passing and control.\\n\\n10. **Thibaut Courtois** \\n + \ - Nationality: Belgian \\n - Position: Goalkeeper \\n - Key Achievements: + Exceptional shot-stopper, key player in Real Madrid's recent successes.\\n\\nThis + list includes a blend of forwards, midfielders, and a goalkeeper, showcasing + the best talents in various positions worldwide. The rankings may vary slightly + depending on current form and opinions, but these players consistently rank + among the best globally.\",\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 68,\n \"completion_tokens\": + 575,\n \"total_tokens\": 643,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_75546bd1a7\"\n}\n" headers: CF-RAY: - - 94d9b7561f204d3b-GRU + - CF-RAY-XXX Connection: - keep-alive Content-Type: - application/json Date: - - Tue, 10 Jun 2025 14:56:46 GMT + - Fri, 06 Feb 2026 18:41:57 GMT Server: - cloudflare + Strict-Transport-Security: + - STS-XXX Transfer-Encoding: - chunked X-Content-Type-Options: - - nosniff + - X-CONTENT-TYPE-XXX access-control-expose-headers: - - X-Request-ID + - ACCESS-CONTROL-XXX alt-svc: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC openai-organization: - - crewai-iuxna1 + - OPENAI-ORG-XXX openai-processing-ms: - - '12189' + - '7948' + openai-project: + - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '12193' + x-openai-proxy-wasm: + - v0.1 x-ratelimit-limit-requests: - - '30000' + - X-RATELIMIT-LIMIT-REQUESTS-XXX x-ratelimit-limit-tokens: - - '150000000' + - X-RATELIMIT-LIMIT-TOKENS-XXX x-ratelimit-remaining-requests: - - '29999' + - X-RATELIMIT-REMAINING-REQUESTS-XXX x-ratelimit-remaining-tokens: - - '149998513' + - X-RATELIMIT-REMAINING-TOKENS-XXX x-ratelimit-reset-requests: - - 2ms + - X-RATELIMIT-RESET-REQUESTS-XXX x-ratelimit-reset-tokens: - - 0s + - X-RATELIMIT-RESET-TOKENS-XXX x-request-id: - - req_1098f5a5384f4a26aecf0c9e4e4d1fc0 - status: - code: 200 - message: OK -- request: - body: '{"messages": [{"role": "system", "content": "You are Sports Analyst. You are an expert at gathering and organizing information. You carefully collect details and present them in a structured way.\nYour personal goal is: Gather information about the best soccer players\n\nTo give my best complete final answer to the task respond using the exact following format:\n\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user", "content": "Top 10 best players in the world?"}, {"role": "assistant", "content": "Thought: I now can give a great answer \nFinal Answer: The top 10 best soccer players in the world, as of October 2023, can be identified based on their recent performances, skills, impact on games, and overall contributions to their teams. Here is the structured list:\n\n1. **Lionel Messi (Inter Miami CF)**\n - - Position: Forward\n - Key Attributes: Dribbling, vision, goal-scoring ability.\n - Achievements: Multiple Ballon d''Or winner, Copa America champion, World Cup champion (2022).\n\n2. **Kylian Mbapp\u00e9 (Paris Saint-Germain)**\n - Position: Forward\n - Key Attributes: Speed, technique, finishing.\n - Achievements: FIFA World Cup champion (2018), Ligue 1 titles, multiple domestic cups.\n\n3. **Erling Haaland (Manchester City)**\n - Position: Forward\n - Key Attributes: Power, speed, goal-scoring instinct.\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\n\n4. **Kevin De Bruyne (Manchester City)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, creativity.\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\n\n5. **Karim Benzema (Al-Ittihad)**\n - Position: Forward\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\n - Achievements: - 2022 Ballon d''Or winner, multiple Champions Leagues with Real Madrid.\n\n6. **Neymar Jr. (Al Hilal)**\n - Position: Forward\n - Key Attributes: Flair, dribbling, creativity.\n - Achievements: Multiple domestic league titles, Champions League runner-up.\n\n7. **Robert Lewandowski (FC Barcelona)**\n - Position: Forward\n - Key Attributes: Finishing, positioning, aerial ability.\n - Achievements: FIFA Best Men''s Player, multiple Bundesliga titles, La Liga champion (2023).\n\n8. **Mohamed Salah (Liverpool)**\n - Position: Forward\n - Key Attributes: Speed, finishing, dribbling.\n - Achievements: Premier League champion, FA Cup, UEFA Champions League winner.\n\n9. **Vin\u00edcius J\u00fanior (Real Madrid)**\n - Position: Forward\n - Key Attributes: Speed, dribbling, creativity.\n - Achievements: UEFA Champions League winner (2022), La Liga champion (2023).\n\n10. **Luka Modri\u0107 (Real Madrid)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, - tactical intelligence.\n - Achievements: Multiple Champions League titles, Ballon d''Or winner (2018).\n\nThis list is compiled based on their current form, past performances, and contributions to their respective teams in both domestic and international competitions. Player rankings can vary based on personal opinion and specific criteria used for evaluation, but these players have consistently been regarded as some of the best in the world as of October 2023."}, {"role": "user", "content": "You are not allowed to include Brazilian players"}, {"role": "assistant", "content": "Thought: I now can give a great answer \nFinal Answer: Here is an updated list of the top 10 best soccer players in the world as of October 2023, excluding Brazilian players:\n\n1. **Lionel Messi (Inter Miami CF)**\n - Position: Forward\n - Key Attributes: Dribbling, vision, goal-scoring ability.\n - Achievements: Multiple Ballon d''Or winner, Copa America champion, World Cup champion (2022).\n\n2. - **Kylian Mbapp\u00e9 (Paris Saint-Germain)**\n - Position: Forward\n - Key Attributes: Speed, technique, finishing.\n - Achievements: FIFA World Cup champion (2018), Ligue 1 titles, multiple domestic cups.\n\n3. **Erling Haaland (Manchester City)**\n - Position: Forward\n - Key Attributes: Power, speed, goal-scoring instinct.\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\n\n4. **Kevin De Bruyne (Manchester City)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, creativity.\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\n\n5. **Karim Benzema (Al-Ittihad)**\n - Position: Forward\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\n - Achievements: 2022 Ballon d''Or winner, multiple Champions Leagues with Real Madrid.\n\n6. **Robert Lewandowski (FC Barcelona)**\n - Position: Forward\n - Key Attributes: Finishing, positioning, - aerial ability.\n - Achievements: FIFA Best Men''s Player, multiple Bundesliga titles, La Liga champion (2023).\n\n7. **Mohamed Salah (Liverpool)**\n - Position: Forward\n - Key Attributes: Speed, finishing, dribbling.\n - Achievements: Premier League champion, FA Cup, UEFA Champions League winner.\n\n8. **Vin\u00edcius J\u00fanior (Real Madrid)**\n - Position: Forward\n - Key Attributes: Speed, dribbling, creativity.\n - Achievements: UEFA Champions League winner (2022), La Liga champion (2023).\n\n9. **Luka Modri\u0107 (Real Madrid)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, tactical intelligence.\n - Achievements: Multiple Champions League titles, Ballon d''Or winner (2018).\n\n10. **Harry Kane (Bayern Munich)**\n - Position: Forward\n - Key Attributes: Goal-scoring, technique, playmaking.\n - Achievements: Golden Boot winner, Premier League titles, UEFA European Championship runner-up.\n\nThis list has been adjusted to exclude Brazilian - players and focuses on those who have made significant impacts in their clubs and on the international stage as of October 2023. Each player is recognized for their exceptional skills, performances, and achievements."}, {"role": "user", "content": "You are not allowed to include Brazilian players"}, {"role": "assistant", "content": "Thought: I now can give a great answer \nFinal Answer: Here is the revised list of the top 10 best soccer players in the world as of October 2023, explicitly excluding Brazilian players:\n\n1. **Lionel Messi (Inter Miami CF)**\n - Position: Forward\n - Key Attributes: Dribbling, vision, goal-scoring ability.\n - Achievements: Multiple Ballon d''Or winner, Copa America champion, World Cup champion (2022).\n\n2. **Kylian Mbapp\u00e9 (Paris Saint-Germain)**\n - Position: Forward\n - Key Attributes: Speed, technique, finishing.\n - Achievements: FIFA World Cup champion (2018), multiple Ligue 1 titles, and various domestic cups.\n\n3. **Erling Haaland - (Manchester City)**\n - Position: Forward\n - Key Attributes: Power, speed, goal-scoring instinct.\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\n\n4. **Kevin De Bruyne (Manchester City)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, creativity.\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\n\n5. **Karim Benzema (Al-Ittihad)**\n - Position: Forward\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\n - Achievements: 2022 Ballon d''Or winner, multiple Champions Leagues with Real Madrid.\n\n6. **Robert Lewandowski (FC Barcelona)**\n - Position: Forward\n - Key Attributes: Finishing, positioning, aerial ability.\n - Achievements: FIFA Best Men''s Player, multiple Bundesliga titles, La Liga champion (2023).\n\n7. **Mohamed Salah (Liverpool)**\n - Position: Forward\n - Key Attributes: Speed, finishing, dribbling.\n - Achievements: - Premier League champion, FA Cup, UEFA Champions League winner.\n\n8. **Luka Modri\u0107 (Real Madrid)**\n - Position: Midfielder\n - Key Attributes: Passing, vision, tactical intelligence.\n - Achievements: Multiple Champions League titles, Ballon d''Or winner (2018).\n\n9. **Harry Kane (Bayern Munich)**\n - Position: Forward\n - Key Attributes: Goal-scoring, technique, playmaking.\n - Achievements: Golden Boot winner, Premier League titles, UEFA European Championship runner-up.\n\n10. **Rodri (Manchester City)**\n - Position: Midfielder\n - Key Attributes: Defensive skills, passing, positional awareness.\n - Achievements: Premier League titles, UEFA Champions League winner (2023).\n\nThis list is curated while adhering to the restriction of excluding Brazilian players. Each player included has demonstrated exceptional skills and remarkable performances, solidifying their status as some of the best in the world as of October 2023."}, {"role": "user", "content": - "You are not allowed to include Brazilian players"}], "model": "gpt-4o-mini", "stop": ["\nObservation:"]}' - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '9093' - content-type: - - application/json - cookie: - - __cf_bm=8Yv8F0ZCFAo2lf.qoqxao70yxyjVvIV90zQqVF6bVzQ-1749567342-1.0.1.1-fZgnv3RDfunvCO1koxwwFJrHnxSx_rwS_FHvQ6xxDPpKHwYr7dTqIQLZrNgSX5twGyK4F22rUmkuiS6KMVogcinChk8lmHtJBTUVTFjr2KU; _cfuvid=wzh8YnmXvLq1G0RcIVijtzboQtCZyIe2uZiochkBLqE-1749567342267-0.0.1.1-604800000 - host: - - api.openai.com - user-agent: - - OpenAI/Python 1.78.0 - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 1.78.0 - x-stainless-raw-response: - - 'true' - x-stainless-read-timeout: - - '600.0' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.12.9 - method: POST - uri: https://api.openai.com/v1/chat/completions - response: - body: - string: "{\n \"id\": \"chatcmpl-BguhCefN1bN2OeYRo5ChhUqNBLUda\",\n \"object\": \"chat.completion\",\n \"created\": 1749567414,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Thought: I now can give a great answer \\nFinal Answer: Here is a refined list of the top 10 best soccer players in the world as of October 2023, ensuring that no Brazilian players are included:\\n\\n1. **Lionel Messi (Inter Miami CF)**\\n - Position: Forward\\n - Key Attributes: Dribbling, vision, goal-scoring ability.\\n - Achievements: Multiple Ballon d'Or winner, Copa America champion, World Cup champion (2022).\\n\\n2. **Kylian Mbappé (Paris Saint-Germain)**\\n - Position: Forward\\n - Key Attributes: Speed, technique, finishing.\\n - Achievements: FIFA World Cup champion (2018), multiple Ligue 1 titles, various domestic cups.\\n\\n3. **Erling Haaland (Manchester City)**\\\ - n - Position: Forward\\n - Key Attributes: Power, speed, goal-scoring instinct.\\n - Achievements: Bundesliga top scorer, UEFA Champions League winner (2023), Premier League titles.\\n\\n4. **Kevin De Bruyne (Manchester City)**\\n - Position: Midfielder\\n - Key Attributes: Passing, vision, creativity.\\n - Achievements: Multiple Premier League titles, FA Cups, UEFA Champions League winner (2023).\\n\\n5. **Karim Benzema (Al-Ittihad)**\\n - Position: Forward\\n - Key Attributes: Goal-scoring, playmaking, tactical intelligence.\\n - Achievements: 2022 Ballon d'Or winner, multiple Champions Leagues with Real Madrid.\\n\\n6. **Robert Lewandowski (FC Barcelona)**\\n - Position: Forward\\n - Key Attributes: Finishing, positioning, aerial ability.\\n - Achievements: FIFA Best Men's Player, multiple Bundesliga titles, La Liga champion (2023).\\n\\n7. **Mohamed Salah (Liverpool)**\\n - Position: Forward\\n - Key Attributes: Speed, finishing, dribbling.\\n -\ - \ Achievements: Premier League champion, FA Cup, UEFA Champions League winner.\\n\\n8. **Luka Modrić (Real Madrid)**\\n - Position: Midfielder\\n - Key Attributes: Passing, vision, tactical intelligence.\\n - Achievements: Multiple Champions League titles, Ballon d'Or winner (2018).\\n\\n9. **Harry Kane (Bayern Munich)**\\n - Position: Forward\\n - Key Attributes: Goal-scoring, technique, playmaking.\\n - Achievements: Golden Boot winner, multiple Premier League titles, UEFA European Championship runner-up.\\n\\n10. **Son Heung-min (Tottenham Hotspur)**\\n - Position: Forward\\n - Key Attributes: Speed, finishing, playmaking.\\n - Achievements: Premier League Golden Boot winner, multiple domestic cup titles.\\n\\nThis list has been carefully revised to exclude all Brazilian players while highlighting some of the most talented individuals in soccer as of October 2023. Each player has showcased remarkable effectiveness and skill, contributing significantly to their\ - \ teams on both domestic and international stages.\",\n \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 2028,\n \"completion_tokens\": 614,\n \"total_tokens\": 2642,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 1280,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": \"default\",\n \"system_fingerprint\": \"fp_34a54ae93c\"\n}\n" - headers: - CF-RAY: - - 94d9b7d24d991d2c-GRU - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Tue, 10 Jun 2025 14:57:29 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Content-Type-Options: - - nosniff - access-control-expose-headers: - - X-Request-ID - alt-svc: - - h3=":443"; ma=86400 - cf-cache-status: - - DYNAMIC - openai-organization: - - crewai-iuxna1 - openai-processing-ms: - - '35291' - openai-version: - - '2020-10-01' - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '35294' - x-ratelimit-limit-requests: - - '30000' - x-ratelimit-limit-tokens: - - '150000000' - x-ratelimit-remaining-requests: - - '29999' - x-ratelimit-remaining-tokens: - - '149997855' - x-ratelimit-reset-requests: - - 2ms - x-ratelimit-reset-tokens: - - 0s - x-request-id: - - req_4676152d4227ac1825d1240ddef231d6 + - X-REQUEST-ID-XXX status: code: 200 message: OK diff --git a/lib/crewai/tests/cassettes/agents/test_lite_agent_inside_flow_sync.yaml b/lib/crewai/tests/cassettes/agents/test_lite_agent_inside_flow_sync.yaml index 89749c490..10a5cfcaa 100644 --- a/lib/crewai/tests/cassettes/agents/test_lite_agent_inside_flow_sync.yaml +++ b/lib/crewai/tests/cassettes/agents/test_lite_agent_inside_flow_sync.yaml @@ -1,14 +1,8 @@ interactions: - request: body: '{"messages":[{"role":"system","content":"You are Test Agent. A helpful - test assistant\nYour personal goal is: Answer questions\nTo give my best complete - final answer to the task respond using the exact following format:\n\nThought: - I now can give a great answer\nFinal Answer: Your final answer must be the great - and the most complete as possible, it must be outcome described.\n\nI MUST use - these formats, my job depends on it!"},{"role":"user","content":"\nCurrent Task: - What is 2+2? Reply with just the number.\n\nBegin! This is VERY important to - you, use the tools available and give your best Final Answer, your job depends - on it!\n\nThought:"}],"model":"gpt-4o-mini"}' + test assistant\nYour personal goal is: Answer questions"},{"role":"user","content":"\nCurrent + Task: What is 2+2? Reply with just the number.\n\nProvide your complete response:"}],"model":"gpt-4o-mini"}' headers: User-Agent: - X-USER-AGENT-XXX @@ -21,7 +15,7 @@ interactions: connection: - keep-alive content-length: - - '673' + - '272' content-type: - application/json host: @@ -43,23 +37,22 @@ interactions: x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-Cy7b0HjL79y39EkUcMLrRhPFe3XGj\",\n \"object\": - \"chat.completion\",\n \"created\": 1768444914,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + string: "{\n \"id\": \"chatcmpl-D6L4AzMHXLXDfyclWS6fJSwS0cvOl\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403318,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal - Answer: 4\",\n \"refusal\": null,\n \"annotations\": []\n },\n - \ \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n - \ \"usage\": {\n \"prompt_tokens\": 136,\n \"completion_tokens\": 13,\n - \ \"total_tokens\": 149,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + \"assistant\",\n \"content\": \"4\",\n \"refusal\": null,\n + \ \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": + \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 50,\n \"completion_tokens\": + 1,\n \"total_tokens\": 51,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": \"fp_8bbc38b4db\"\n}\n" + \"default\",\n \"system_fingerprint\": \"fp_f4ae844694\"\n}\n" headers: CF-RAY: - CF-RAY-XXX @@ -68,7 +61,7 @@ interactions: Content-Type: - application/json Date: - - Thu, 15 Jan 2026 02:41:55 GMT + - Fri, 06 Feb 2026 18:41:58 GMT Server: - cloudflare Set-Cookie: @@ -85,18 +78,14 @@ interactions: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC - content-length: - - '857' openai-organization: - OPENAI-ORG-XXX openai-processing-ms: - - '341' + - '264' openai-project: - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - x-envoy-upstream-service-time: - - '358' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: diff --git a/lib/crewai/tests/cassettes/agents/test_lite_agent_standalone_still_works.yaml b/lib/crewai/tests/cassettes/agents/test_lite_agent_standalone_still_works.yaml index 83bec39ce..d3f8bb9e5 100644 --- a/lib/crewai/tests/cassettes/agents/test_lite_agent_standalone_still_works.yaml +++ b/lib/crewai/tests/cassettes/agents/test_lite_agent_standalone_still_works.yaml @@ -1,14 +1,8 @@ interactions: - request: body: '{"messages":[{"role":"system","content":"You are Standalone Agent. A helpful - assistant\nYour personal goal is: Answer questions\nTo give my best complete - final answer to the task respond using the exact following format:\n\nThought: - I now can give a great answer\nFinal Answer: Your final answer must be the great - and the most complete as possible, it must be outcome described.\n\nI MUST use - these formats, my job depends on it!"},{"role":"user","content":"\nCurrent Task: - What is 5+5? Reply with just the number.\n\nBegin! This is VERY important to - you, use the tools available and give your best Final Answer, your job depends - on it!\n\nThought:"}],"model":"gpt-4o-mini"}' + assistant\nYour personal goal is: Answer questions"},{"role":"user","content":"\nCurrent + Task: What is 5+5? Reply with just the number.\n\nProvide your complete response:"}],"model":"gpt-4o-mini"}' headers: User-Agent: - X-USER-AGENT-XXX @@ -21,7 +15,7 @@ interactions: connection: - keep-alive content-length: - - '674' + - '273' content-type: - application/json host: @@ -43,23 +37,22 @@ interactions: x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-Cy7azhPwUHQ0p5tdhxSAmLPoE8UgC\",\n \"object\": - \"chat.completion\",\n \"created\": 1768444913,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + string: "{\n \"id\": \"chatcmpl-D6L3cLs2ndBaXV2wnqYCdi6X1ykvv\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403284,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal - Answer: 10\",\n \"refusal\": null,\n \"annotations\": []\n },\n - \ \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n - \ \"usage\": {\n \"prompt_tokens\": 136,\n \"completion_tokens\": 13,\n - \ \"total_tokens\": 149,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + \"assistant\",\n \"content\": \"10\",\n \"refusal\": null,\n + \ \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": + \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 50,\n \"completion_tokens\": + 1,\n \"total_tokens\": 51,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": \"fp_29330a9688\"\n}\n" + \"default\",\n \"system_fingerprint\": \"fp_f4ae844694\"\n}\n" headers: CF-RAY: - CF-RAY-XXX @@ -68,7 +61,7 @@ interactions: Content-Type: - application/json Date: - - Thu, 15 Jan 2026 02:41:54 GMT + - Fri, 06 Feb 2026 18:41:25 GMT Server: - cloudflare Set-Cookie: @@ -85,18 +78,14 @@ interactions: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC - content-length: - - '858' openai-organization: - OPENAI-ORG-XXX openai-processing-ms: - - '455' + - '270' openai-project: - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - x-envoy-upstream-service-time: - - '583' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: diff --git a/lib/crewai/tests/cassettes/agents/test_multiple_agents_in_same_flow.yaml b/lib/crewai/tests/cassettes/agents/test_multiple_agents_in_same_flow.yaml index 46ba712c1..e66c25d99 100644 --- a/lib/crewai/tests/cassettes/agents/test_multiple_agents_in_same_flow.yaml +++ b/lib/crewai/tests/cassettes/agents/test_multiple_agents_in_same_flow.yaml @@ -1,13 +1,8 @@ interactions: - request: body: '{"messages":[{"role":"system","content":"You are First Agent. A friendly - greeter\nYour personal goal is: Greet users\nTo give my best complete final - answer to the task respond using the exact following format:\n\nThought: I now - can give a great answer\nFinal Answer: Your final answer must be the great and - the most complete as possible, it must be outcome described.\n\nI MUST use these - formats, my job depends on it!"},{"role":"user","content":"\nCurrent Task: Say - hello\n\nBegin! This is VERY important to you, use the tools available and give - your best Final Answer, your job depends on it!\n\nThought:"}],"model":"gpt-4o-mini"}' + greeter\nYour personal goal is: Greet users"},{"role":"user","content":"\nCurrent + Task: Say hello\n\nProvide your complete response:"}],"model":"gpt-4o-mini"}' headers: User-Agent: - X-USER-AGENT-XXX @@ -20,7 +15,7 @@ interactions: connection: - keep-alive content-length: - - '632' + - '231' content-type: - application/json host: @@ -42,24 +37,22 @@ interactions: x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-CyRKzgODZ9yn3F9OkaXsscLk2Ln3N\",\n \"object\": - \"chat.completion\",\n \"created\": 1768520801,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + string: "{\n \"id\": \"chatcmpl-D6L4A8Aad6P1YUxWjQpvyltn8GaKT\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403318,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal - Answer: Hello! Welcome! I'm so glad to see you here. If you need any assistance - or have any questions, feel free to ask. Have a wonderful day!\",\n \"refusal\": - null,\n \"annotations\": []\n },\n \"logprobs\": null,\n - \ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": - 127,\n \"completion_tokens\": 43,\n \"total_tokens\": 170,\n \"prompt_tokens_details\": + \"assistant\",\n \"content\": \"Hello! \U0001F60A How are you today?\",\n + \ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": + null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": + 41,\n \"completion_tokens\": 8,\n \"total_tokens\": 49,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": \"fp_c4585b5b9c\"\n}\n" + \"default\",\n \"system_fingerprint\": \"fp_f4ae844694\"\n}\n" headers: CF-RAY: - CF-RAY-XXX @@ -68,7 +61,7 @@ interactions: Content-Type: - application/json Date: - - Thu, 15 Jan 2026 23:46:42 GMT + - Fri, 06 Feb 2026 18:41:58 GMT Server: - cloudflare Set-Cookie: @@ -85,18 +78,14 @@ interactions: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC - content-length: - - '990' openai-organization: - OPENAI-ORG-XXX openai-processing-ms: - - '880' + - '325' openai-project: - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - x-envoy-upstream-service-time: - - '1160' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: @@ -118,13 +107,8 @@ interactions: message: OK - request: body: '{"messages":[{"role":"system","content":"You are Second Agent. A polite - farewell agent\nYour personal goal is: Say goodbye\nTo give my best complete - final answer to the task respond using the exact following format:\n\nThought: - I now can give a great answer\nFinal Answer: Your final answer must be the great - and the most complete as possible, it must be outcome described.\n\nI MUST use - these formats, my job depends on it!"},{"role":"user","content":"\nCurrent Task: - Say goodbye\n\nBegin! This is VERY important to you, use the tools available - and give your best Final Answer, your job depends on it!\n\nThought:"}],"model":"gpt-4o-mini"}' + farewell agent\nYour personal goal is: Say goodbye"},{"role":"user","content":"\nCurrent + Task: Say goodbye\n\nProvide your complete response:"}],"model":"gpt-4o-mini"}' headers: User-Agent: - X-USER-AGENT-XXX @@ -137,7 +121,7 @@ interactions: connection: - keep-alive content-length: - - '640' + - '239' content-type: - application/json host: @@ -159,27 +143,24 @@ interactions: x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-CyRL1Ua2PkK5xXPp3KeF0AnGAk3JP\",\n \"object\": - \"chat.completion\",\n \"created\": 1768520803,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + string: "{\n \"id\": \"chatcmpl-D6L4BLMYC3ODccwbKfBIdtrEyd3no\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403319,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal - Answer: As we reach the end of our conversation, I want to express my gratitude - for the time we've shared. It's been a pleasure assisting you, and I hope - you found our interaction helpful and enjoyable. Remember, whenever you need - assistance, I'm just a message away. Wishing you all the best in your future - endeavors. Goodbye and take care!\",\n \"refusal\": null,\n \"annotations\": + \"assistant\",\n \"content\": \"Thank you for the time we've spent + together! I wish you all the best in your future endeavors. Take care, and + until we meet again, goodbye!\",\n \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n - \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 126,\n \"completion_tokens\": - 79,\n \"total_tokens\": 205,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 40,\n \"completion_tokens\": + 31,\n \"total_tokens\": 71,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": \"fp_29330a9688\"\n}\n" + \"default\",\n \"system_fingerprint\": \"fp_f4ae844694\"\n}\n" headers: CF-RAY: - CF-RAY-XXX @@ -188,7 +169,7 @@ interactions: Content-Type: - application/json Date: - - Thu, 15 Jan 2026 23:46:44 GMT + - Fri, 06 Feb 2026 18:41:59 GMT Server: - cloudflare Set-Cookie: @@ -205,18 +186,14 @@ interactions: - h3=":443"; ma=86400 cf-cache-status: - DYNAMIC - content-length: - - '1189' openai-organization: - OPENAI-ORG-XXX openai-processing-ms: - - '1363' + - '726' openai-project: - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - x-envoy-upstream-service-time: - - '1605' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: diff --git a/lib/crewai/tests/cassettes/agents/test_native_tool_calling_error_handling.yaml b/lib/crewai/tests/cassettes/agents/test_native_tool_calling_error_handling.yaml index f56e36c04..c61d2c034 100644 --- a/lib/crewai/tests/cassettes/agents/test_native_tool_calling_error_handling.yaml +++ b/lib/crewai/tests/cassettes/agents/test_native_tool_calling_error_handling.yaml @@ -2,9 +2,8 @@ interactions: - request: body: '{"messages":[{"role":"system","content":"You are Calculator. You calculate things.\nYour personal goal is: Perform calculations efficiently"},{"role":"user","content":"\nCurrent - Task: Use the failing_tool to do something.\n\nThis is VERY important to you, - your job depends on it!"}],"model":"gpt-4o-mini","tool_choice":"auto","tools":[{"type":"function","function":{"name":"failing_tool","description":"This - tool always fails","parameters":{"properties":{},"type":"object"}}}]}' + Task: Use the failing_tool to do something."}],"model":"gpt-4o-mini","tool_choice":"auto","tools":[{"type":"function","function":{"name":"failing_tool","description":"This + tool always fails","strict":true,"parameters":{"properties":{},"type":"object","additionalProperties":false,"required":[]}}}]}' headers: User-Agent: - X-USER-AGENT-XXX @@ -17,7 +16,7 @@ interactions: connection: - keep-alive content-length: - - '477' + - '476' content-type: - application/json host: @@ -39,26 +38,26 @@ interactions: x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-D0vm2JDsOmy0czXPAr4vnw3wvuqYZ\",\n \"object\": - \"chat.completion\",\n \"created\": 1769114454,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + string: "{\n \"id\": \"chatcmpl-D6L3dV6acwapgRyxmnzGfuOXemtjJ\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403285,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n - \ \"id\": \"call_8xr8rPUDWzLfQ3LOWPHtBUjK\",\n \"type\": + \ \"id\": \"call_GCdaOdo32pr1sSk4RzO0tiB9\",\n \"type\": \"function\",\n \"function\": {\n \"name\": \"failing_tool\",\n \ \"arguments\": \"{}\"\n }\n }\n ],\n \ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n }\n ],\n \"usage\": - {\n \"prompt_tokens\": 78,\n \"completion_tokens\": 11,\n \"total_tokens\": - 89,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": + {\n \"prompt_tokens\": 65,\n \"completion_tokens\": 11,\n \"total_tokens\": + 76,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \ \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": \"fp_c4585b5b9c\"\n}\n" + \"default\",\n \"system_fingerprint\": \"fp_6c0d1490cb\"\n}\n" headers: CF-RAY: - CF-RAY-XXX @@ -67,7 +66,7 @@ interactions: Content-Type: - application/json Date: - - Thu, 22 Jan 2026 20:40:54 GMT + - Fri, 06 Feb 2026 18:41:25 GMT Server: - cloudflare Set-Cookie: @@ -87,13 +86,11 @@ interactions: openai-organization: - OPENAI-ORG-XXX openai-processing-ms: - - '593' + - '436' openai-project: - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - x-envoy-upstream-service-time: - - '621' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: @@ -116,12 +113,9 @@ interactions: - request: body: '{"messages":[{"role":"system","content":"You are Calculator. You calculate things.\nYour personal goal is: Perform calculations efficiently"},{"role":"user","content":"\nCurrent - Task: Use the failing_tool to do something.\n\nThis is VERY important to you, - your job depends on it!"},{"role":"assistant","content":null,"tool_calls":[{"id":"call_8xr8rPUDWzLfQ3LOWPHtBUjK","type":"function","function":{"name":"failing_tool","arguments":"{}"}}]},{"role":"tool","tool_call_id":"call_8xr8rPUDWzLfQ3LOWPHtBUjK","content":"Error - executing tool: This tool always fails"},{"role":"user","content":"Analyze the - tool result. If requirements are met, provide the Final Answer. Otherwise, call - the next tool. Deliver only the answer without meta-commentary."}],"model":"gpt-4o-mini","tool_choice":"auto","tools":[{"type":"function","function":{"name":"failing_tool","description":"This - tool always fails","parameters":{"properties":{},"type":"object"}}}]}' + Task: Use the failing_tool to do something."},{"role":"assistant","content":null,"tool_calls":[{"id":"call_GCdaOdo32pr1sSk4RzO0tiB9","type":"function","function":{"name":"failing_tool","arguments":"{}"}}]},{"role":"tool","tool_call_id":"call_GCdaOdo32pr1sSk4RzO0tiB9","name":"failing_tool","content":"Error + executing tool: This tool always fails"}],"model":"gpt-4o-mini","tool_choice":"auto","tools":[{"type":"function","function":{"name":"failing_tool","description":"This + tool always fails","strict":true,"parameters":{"properties":{},"type":"object","additionalProperties":false,"required":[]}}}]}' headers: User-Agent: - X-USER-AGENT-XXX @@ -134,7 +128,7 @@ interactions: connection: - keep-alive content-length: - - '941' + - '778' content-type: - application/json cookie: @@ -158,22 +152,25 @@ interactions: x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 method: POST uri: https://api.openai.com/v1/chat/completions response: body: - string: "{\n \"id\": \"chatcmpl-D0vm3xcywoKBW75bhBXfkGJNim6Th\",\n \"object\": - \"chat.completion\",\n \"created\": 1769114455,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + string: "{\n \"id\": \"chatcmpl-D6L3dhjDZOoihHvXvRpbJD3ReGu0z\",\n \"object\": + \"chat.completion\",\n \"created\": 1770403285,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": - \"assistant\",\n \"content\": \"Error: This tool always fails.\",\n - \ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\": - null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": - 141,\n \"completion_tokens\": 8,\n \"total_tokens\": 149,\n \"prompt_tokens_details\": - {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + \"assistant\",\n \"content\": \"The attempt to use the failing tool + resulted in an error, as expected since it is designed to always fail. If + there's anything else you would like to calculate or explore, please let me + know!\",\n \"refusal\": null,\n \"annotations\": []\n },\n + \ \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n + \ \"usage\": {\n \"prompt_tokens\": 93,\n \"completion_tokens\": 40,\n + \ \"total_tokens\": 133,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": - \"default\",\n \"system_fingerprint\": \"fp_c4585b5b9c\"\n}\n" + \"default\",\n \"system_fingerprint\": \"fp_6c0d1490cb\"\n}\n" headers: CF-RAY: - CF-RAY-XXX @@ -182,7 +179,7 @@ interactions: Content-Type: - application/json Date: - - Thu, 22 Jan 2026 20:40:55 GMT + - Fri, 06 Feb 2026 18:41:26 GMT Server: - cloudflare Strict-Transport-Security: @@ -200,13 +197,11 @@ interactions: openai-organization: - OPENAI-ORG-XXX openai-processing-ms: - - '420' + - '776' openai-project: - OPENAI-PROJECT-XXX openai-version: - '2020-10-01' - x-envoy-upstream-service-time: - - '436' x-openai-proxy-wasm: - v0.1 x-ratelimit-limit-requests: diff --git a/lib/crewai/tests/cassettes/llms/anthropic/test_anthropic_agent_kickoff_structured_output_with_tools.yaml b/lib/crewai/tests/cassettes/llms/anthropic/test_anthropic_agent_kickoff_structured_output_with_tools.yaml index 879eea9e6..31124d09c 100644 --- a/lib/crewai/tests/cassettes/llms/anthropic/test_anthropic_agent_kickoff_structured_output_with_tools.yaml +++ b/lib/crewai/tests/cassettes/llms/anthropic/test_anthropic_agent_kickoff_structured_output_with_tools.yaml @@ -43,15 +43,15 @@ interactions: x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 x-stainless-timeout: - NOT_GIVEN method: POST uri: https://api.anthropic.com/v1/messages response: body: - string: '{"model":"claude-3-5-haiku-20241022","id":"msg_0149zKBgM47utdBdrfJjM6YZ","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_011jnBYLgtzXqdmSi7JDyQHj","name":"structured_output","input":{"operation":"Addition","result":42,"explanation":"Adding - 15 and 27 together results in 42"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":573,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":79,"service_tier":"standard"}}' + string: '{"model":"claude-3-5-haiku-20241022","id":"msg_01A41GpDoJbZLUhR8dQzUcUX","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UNPdzpayoWyqDYVE7fR5oA","name":"structured_output","input":{"operation":"Addition","result":42,"explanation":"Added + 15 and 27 together"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":573,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":75,"service_tier":"standard","inference_geo":"not_available"}}' headers: CF-RAY: - CF-RAY-XXX @@ -62,7 +62,7 @@ interactions: Content-Type: - application/json Date: - - Fri, 30 Jan 2026 18:56:15 GMT + - Fri, 06 Feb 2026 18:41:25 GMT Server: - cloudflare Transfer-Encoding: @@ -88,7 +88,7 @@ interactions: anthropic-ratelimit-requests-remaining: - '3999' anthropic-ratelimit-requests-reset: - - '2026-01-30T18:56:14Z' + - '2026-02-06T18:41:24Z' anthropic-ratelimit-tokens-limit: - ANTHROPIC-RATELIMIT-TOKENS-LIMIT-XXX anthropic-ratelimit-tokens-remaining: @@ -102,7 +102,7 @@ interactions: strict-transport-security: - STS-XXX x-envoy-upstream-service-time: - - '1473' + - '1247' status: code: 200 message: OK diff --git a/lib/crewai/tests/cassettes/llms/anthropic/test_anthropic_agent_kickoff_structured_output_without_tools.yaml b/lib/crewai/tests/cassettes/llms/anthropic/test_anthropic_agent_kickoff_structured_output_without_tools.yaml index 02739fef1..70478203b 100644 --- a/lib/crewai/tests/cassettes/llms/anthropic/test_anthropic_agent_kickoff_structured_output_without_tools.yaml +++ b/lib/crewai/tests/cassettes/llms/anthropic/test_anthropic_agent_kickoff_structured_output_without_tools.yaml @@ -44,21 +44,20 @@ interactions: x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.13.3 + - 3.13.5 x-stainless-timeout: - NOT_GIVEN method: POST uri: https://api.anthropic.com/v1/messages response: body: - string: '{"model":"claude-3-5-haiku-20241022","id":"msg_013iHkpmto99iyH5kDvn8uER","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01Kpda2DzHBqWq9a2FS2Bdw6","name":"structured_output","input":{"topic":"Benefits + string: '{"model":"claude-3-5-haiku-20241022","id":"msg_016wrV83wm3FLYD4JoTy2Piw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01V6Pzr7eGfuG4Q3mc25ZXwN","name":"structured_output","input":{"topic":"Benefits of Remote Work","summary":"Remote work offers significant advantages for both - employees and employers, transforming traditional work paradigms by providing - flexibility, increased productivity, and cost savings.","key_points":["Increased - employee flexibility and work-life balance","Reduced commuting time and associated - stress","Cost savings for companies on office infrastructure","Access to a - global talent pool","Higher employee productivity and job satisfaction","Lower - carbon footprint due to reduced travel"]}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":589,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":153,"service_tier":"standard"}}' + employees and employers, transforming traditional workplace dynamics.","key_points":["Increased + flexibility in work schedule","Reduced commute time and transportation costs","Improved + work-life balance","Higher productivity for many employees","Cost savings + for companies on office infrastructure","Expanded talent pool for hiring","Enhanced + employee job satisfaction"]}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":589,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":142,"service_tier":"standard","inference_geo":"not_available"}}' headers: CF-RAY: - CF-RAY-XXX @@ -69,7 +68,7 @@ interactions: Content-Type: - application/json Date: - - Fri, 30 Jan 2026 18:56:19 GMT + - Fri, 06 Feb 2026 18:41:28 GMT Server: - cloudflare Transfer-Encoding: @@ -95,7 +94,7 @@ interactions: anthropic-ratelimit-requests-remaining: - '3999' anthropic-ratelimit-requests-reset: - - '2026-01-30T18:56:16Z' + - '2026-02-06T18:41:26Z' anthropic-ratelimit-tokens-limit: - ANTHROPIC-RATELIMIT-TOKENS-LIMIT-XXX anthropic-ratelimit-tokens-remaining: @@ -109,7 +108,7 @@ interactions: strict-transport-security: - STS-XXX x-envoy-upstream-service-time: - - '3107' + - '2650' status: code: 200 message: OK From f6fa04528ab67230320bd581ea673d2900e1aaba Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Fri, 6 Feb 2026 16:29:27 -0500 Subject: [PATCH 9/9] fix: add async HITL support and chained-router tests asynchronous human-in-the-loop handling and related fixes. - Extend human_input provider with async support: AsyncExecutorContext, handle_feedback_async, async prompt helpers (_prompt_input_async, _async_readline), and async training/regular feedback loops in SyncHumanInputProvider. - Add async handler methods in CrewAgentExecutor and AgentExecutor (_ahandle_human_feedback, _ainvoke_loop) to integrate async provider flows. - Change PlusAPI.get_agent to an async httpx call and adapt caller in agent_utils to run it via asyncio.run. - Simplify listener execution in flow.Flow to correctly pass HumanFeedbackResult to listeners and unify execution path for router outcomes. - Remove deprecated types/hitl.py definitions. - Add tests covering chained router feedback, rejected paths, and mixed router/non-router listeners to prevent regressions. --- .../src/crewai/agents/crew_agent_executor.py | 16 +- lib/crewai/src/crewai/cli/plus_api.py | 16 +- .../src/crewai/core/providers/human_input.py | 191 +++++++++++++++++- .../src/crewai/experimental/agent_executor.py | 32 ++- lib/crewai/src/crewai/flow/flow.py | 40 +--- lib/crewai/src/crewai/types/hitl.py | 37 ---- .../src/crewai/utilities/agent_utils.py | 2 +- lib/crewai/tests/cli/test_plus_api.py | 81 +++++--- .../tests/test_human_feedback_integration.py | 170 ++++++++++++++++ 9 files changed, 473 insertions(+), 112 deletions(-) delete mode 100644 lib/crewai/src/crewai/types/hitl.py diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 647596f2a..c7adcbe09 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -1009,7 +1009,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): raise if self.ask_for_human_input: - formatted_answer = self._handle_human_feedback(formatted_answer) + formatted_answer = await self._ahandle_human_feedback(formatted_answer) self._create_short_term_memory(formatted_answer) self._create_long_term_memory(formatted_answer) @@ -1508,6 +1508,20 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): provider = get_provider() return provider.handle_feedback(formatted_answer, self) + async def _ahandle_human_feedback( + self, formatted_answer: AgentFinish + ) -> AgentFinish: + """Process human feedback asynchronously via the configured provider. + + Args: + formatted_answer: Initial agent result. + + Returns: + Final answer after feedback. + """ + provider = get_provider() + return await provider.handle_feedback_async(formatted_answer, self) + def _is_training_mode(self) -> bool: """Check if training mode is active. diff --git a/lib/crewai/src/crewai/cli/plus_api.py b/lib/crewai/src/crewai/cli/plus_api.py index 62f34095b..e07d44d10 100644 --- a/lib/crewai/src/crewai/cli/plus_api.py +++ b/lib/crewai/src/crewai/cli/plus_api.py @@ -1,6 +1,8 @@ +import os from typing import Any from urllib.parse import urljoin -import os + +import httpx import requests from crewai.cli.config import Settings @@ -33,7 +35,11 @@ class PlusAPI: if settings.org_uuid: self.headers["X-Crewai-Organization-Id"] = settings.org_uuid - self.base_url = os.getenv("CREWAI_PLUS_URL") or str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL + self.base_url = ( + os.getenv("CREWAI_PLUS_URL") + or str(settings.enterprise_base_url) + or DEFAULT_CREWAI_ENTERPRISE_URL + ) def _make_request( self, method: str, endpoint: str, **kwargs: Any @@ -49,8 +55,10 @@ class PlusAPI: def get_tool(self, handle: str) -> requests.Response: return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}") - def get_agent(self, handle: str) -> requests.Response: - return self._make_request("GET", f"{self.AGENTS_RESOURCE}/{handle}") + async def get_agent(self, handle: str) -> httpx.Response: + url = urljoin(self.base_url, f"{self.AGENTS_RESOURCE}/{handle}") + async with httpx.AsyncClient() as client: + return await client.get(url, headers=self.headers) def publish_tool( self, diff --git a/lib/crewai/src/crewai/core/providers/human_input.py b/lib/crewai/src/crewai/core/providers/human_input.py index 4062e6bb9..ecbc09a41 100644 --- a/lib/crewai/src/crewai/core/providers/human_input.py +++ b/lib/crewai/src/crewai/core/providers/human_input.py @@ -2,7 +2,9 @@ from __future__ import annotations +import asyncio from contextvars import ContextVar, Token +import sys from typing import TYPE_CHECKING, Protocol, runtime_checkable @@ -46,13 +48,21 @@ class ExecutorContext(Protocol): ... +class AsyncExecutorContext(ExecutorContext, Protocol): + """Extended context for executors that support async invocation.""" + + async def _ainvoke_loop(self) -> AgentFinish: + """Invoke the agent loop asynchronously and return the result.""" + ... + + @runtime_checkable class HumanInputProvider(Protocol): """Protocol for human input handling. Implementations handle the full feedback flow: - Sync: prompt user, loop until satisfied - - Async: raise exception for external handling + - Async: use non-blocking I/O and async invoke loop """ def setup_messages(self, context: ExecutorContext) -> bool: @@ -86,7 +96,7 @@ class HumanInputProvider(Protocol): formatted_answer: AgentFinish, context: ExecutorContext, ) -> AgentFinish: - """Handle the full human feedback flow. + """Handle the full human feedback flow synchronously. Args: formatted_answer: The agent's current answer. @@ -100,6 +110,25 @@ class HumanInputProvider(Protocol): """ ... + async def handle_feedback_async( + self, + formatted_answer: AgentFinish, + context: AsyncExecutorContext, + ) -> AgentFinish: + """Handle the full human feedback flow asynchronously. + + Uses non-blocking I/O for user prompts and async invoke loop + for agent re-execution. + + Args: + formatted_answer: The agent's current answer. + context: Async executor context for callbacks. + + Returns: + The final answer after feedback processing. + """ + ... + @staticmethod def _get_output_string(answer: AgentFinish) -> str: """Extract output string from answer. @@ -116,7 +145,7 @@ class HumanInputProvider(Protocol): class SyncHumanInputProvider(HumanInputProvider): - """Default synchronous human input via terminal.""" + """Default human input provider with sync and async support.""" def setup_messages(self, context: ExecutorContext) -> bool: """Use standard message setup. @@ -157,6 +186,33 @@ class SyncHumanInputProvider(HumanInputProvider): return self._handle_regular_feedback(formatted_answer, feedback, context) + async def handle_feedback_async( + self, + formatted_answer: AgentFinish, + context: AsyncExecutorContext, + ) -> AgentFinish: + """Handle feedback asynchronously without blocking the event loop. + + Args: + formatted_answer: The agent's current answer. + context: Async executor context for callbacks. + + Returns: + The final answer after feedback processing. + """ + feedback = await self._prompt_input_async(context.crew) + + if context._is_training_mode(): + return await self._handle_training_feedback_async( + formatted_answer, feedback, context + ) + + return await self._handle_regular_feedback_async( + formatted_answer, feedback, context + ) + + # ── Sync helpers ────────────────────────────────────────────────── + @staticmethod def _handle_training_feedback( initial_answer: AgentFinish, @@ -209,6 +265,62 @@ class SyncHumanInputProvider(HumanInputProvider): return answer + # ── Async helpers ───────────────────────────────────────────────── + + @staticmethod + async def _handle_training_feedback_async( + initial_answer: AgentFinish, + feedback: str, + context: AsyncExecutorContext, + ) -> AgentFinish: + """Process training feedback asynchronously (single iteration). + + Args: + initial_answer: The agent's initial answer. + feedback: Human feedback string. + context: Async executor context for callbacks. + + Returns: + Improved answer after processing feedback. + """ + context._handle_crew_training_output(initial_answer, feedback) + context.messages.append(context._format_feedback_message(feedback)) + improved_answer = await context._ainvoke_loop() + context._handle_crew_training_output(improved_answer) + context.ask_for_human_input = False + return improved_answer + + async def _handle_regular_feedback_async( + self, + current_answer: AgentFinish, + initial_feedback: str, + context: AsyncExecutorContext, + ) -> AgentFinish: + """Process regular feedback with async iteration loop. + + Args: + current_answer: The agent's current answer. + initial_feedback: Initial human feedback string. + context: Async executor context for callbacks. + + Returns: + Final answer after all feedback iterations. + """ + feedback = initial_feedback + answer = current_answer + + while context.ask_for_human_input: + if feedback.strip() == "": + context.ask_for_human_input = False + else: + context.messages.append(context._format_feedback_message(feedback)) + answer = await context._ainvoke_loop() + feedback = await self._prompt_input_async(context.crew) + + return answer + + # ── I/O ─────────────────────────────────────────────────────────── + @staticmethod def _prompt_input(crew: Crew | None) -> str: """Show rich panel and prompt for input. @@ -262,6 +374,79 @@ class SyncHumanInputProvider(HumanInputProvider): finally: formatter.resume_live_updates() + @staticmethod + async def _prompt_input_async(crew: Crew | None) -> str: + """Show rich panel and prompt for input without blocking the event loop. + + Args: + crew: The crew instance for context. + + Returns: + User input string from terminal. + """ + from rich.panel import Panel + from rich.text import Text + + from crewai.events.event_listener import event_listener + + formatter = event_listener.formatter + formatter.pause_live_updates() + + try: + if crew and getattr(crew, "_train", False): + prompt_text = ( + "TRAINING MODE: Provide feedback to improve the agent's performance.\n\n" + "This will be used to train better versions of the agent.\n" + "Please provide detailed feedback about the result quality and reasoning process." + ) + title = "🎓 Training Feedback Required" + else: + prompt_text = ( + "Provide feedback on the Final Result above.\n\n" + "• If you are happy with the result, simply hit Enter without typing anything.\n" + "• Otherwise, provide specific improvement requests.\n" + "• You can provide multiple rounds of feedback until satisfied." + ) + title = "💬 Human Feedback Required" + + content = Text() + content.append(prompt_text, style="yellow") + + prompt_panel = Panel( + content, + title=title, + border_style="yellow", + padding=(1, 2), + ) + formatter.console.print(prompt_panel) + + response = await _async_readline() + if response.strip() != "": + formatter.console.print("\n[cyan]Processing your feedback...[/cyan]") + return response + finally: + formatter.resume_live_updates() + + +async def _async_readline() -> str: + """Read a line from stdin using the event loop's native I/O. + + Falls back to asyncio.to_thread on platforms where piping stdin + is unsupported. + + Returns: + The line read from stdin, with trailing newline stripped. + """ + loop = asyncio.get_running_loop() + try: + reader = asyncio.StreamReader() + protocol = asyncio.StreamReaderProtocol(reader) + await loop.connect_read_pipe(lambda: protocol, sys.stdin) + raw = await reader.readline() + return raw.decode().rstrip("\n") + except (OSError, NotImplementedError, ValueError): + return await asyncio.to_thread(input) + _provider: ContextVar[HumanInputProvider | None] = ContextVar( "human_input_provider", diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 037df6793..9f2fecb25 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -258,6 +258,22 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): raise RuntimeError("Agent loop did not produce a final answer") return answer + async def _ainvoke_loop(self) -> AgentFinish: + """Invoke the agent loop asynchronously and return the result. + + Required by AsyncExecutorContext protocol. + """ + self._state.iterations = 0 + self._state.is_finished = False + self._state.current_answer = None + + await self.akickoff() + + answer = self._state.current_answer + if not isinstance(answer, AgentFinish): + raise RuntimeError("Agent loop did not produce a final answer") + return answer + def _format_feedback_message(self, feedback: str) -> LLMMessage: """Format feedback as a message for the LLM. @@ -1173,7 +1189,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): ) if self.state.ask_for_human_input: - formatted_answer = self._handle_human_feedback(formatted_answer) + formatted_answer = await self._ahandle_human_feedback(formatted_answer) self._create_short_term_memory(formatted_answer) self._create_long_term_memory(formatted_answer) @@ -1390,6 +1406,20 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): provider = get_provider() return provider.handle_feedback(formatted_answer, self) + async def _ahandle_human_feedback( + self, formatted_answer: AgentFinish + ) -> AgentFinish: + """Process human feedback asynchronously and refine answer. + + Args: + formatted_answer: Initial agent result. + + Returns: + Final answer after feedback. + """ + provider = get_provider() + return await provider.handle_feedback_async(formatted_answer, self) + def _is_training_mode(self) -> bool: """Check if training mode is active. diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index 5a6ac4557..f9f6843aa 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -1934,40 +1934,14 @@ class Flow(Generic[T], metaclass=FlowMeta): await self._execute_listeners(start_method_name, result, finished_event_id) # Then execute listeners for the router result (e.g., "approved") router_result_trigger = FlowMethodName(str(result)) - listeners_for_result = self._find_triggered_methods( - router_result_trigger, router_only=False + listener_result = ( + self.last_human_feedback + if self.last_human_feedback is not None + else result + ) + await self._execute_listeners( + router_result_trigger, listener_result, finished_event_id ) - if listeners_for_result: - # Pass the HumanFeedbackResult if available - listener_result = ( - self.last_human_feedback - if self.last_human_feedback is not None - else result - ) - racing_group = self._get_racing_group_for_listeners( - listeners_for_result - ) - if racing_group: - racing_members, _ = racing_group - other_listeners = [ - name - for name in listeners_for_result - if name not in racing_members - ] - await self._execute_racing_listeners( - racing_members, - other_listeners, - listener_result, - finished_event_id, - ) - else: - tasks = [ - self._execute_single_listener( - listener_name, listener_result, finished_event_id - ) - for listener_name in listeners_for_result - ] - await asyncio.gather(*tasks) else: await self._execute_listeners(start_method_name, result, finished_event_id) diff --git a/lib/crewai/src/crewai/types/hitl.py b/lib/crewai/src/crewai/types/hitl.py deleted file mode 100644 index d5fd33e14..000000000 --- a/lib/crewai/src/crewai/types/hitl.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Human-in-the-loop (HITL) type definitions. - -This module provides type definitions for human-in-the-loop interactions -in crew executions. -""" - -from typing import TypedDict - - -class HITLResumeInfo(TypedDict, total=False): - """HITL resume information passed from flow to crew. - - Attributes: - task_id: Unique identifier for the task. - crew_execution_id: Unique identifier for the crew execution. - task_key: Key identifying the specific task. - task_output: Output from the task before human intervention. - human_feedback: Feedback provided by the human. - previous_messages: History of messages in the conversation. - """ - - task_id: str - crew_execution_id: str - task_key: str - task_output: str - human_feedback: str - previous_messages: list[dict[str, str]] - - -class CrewInputsWithHITL(TypedDict, total=False): - """Crew inputs that may contain HITL resume information. - - Attributes: - _hitl_resume: Optional HITL resume information for continuing execution. - """ - - _hitl_resume: HITLResumeInfo diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py index ee76dc53f..4ded93505 100644 --- a/lib/crewai/src/crewai/utilities/agent_utils.py +++ b/lib/crewai/src/crewai/utilities/agent_utils.py @@ -832,7 +832,7 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]: client = PlusAPI(api_key=get_auth_token()) _print_current_organization() - response = client.get_agent(from_repository) + response = asyncio.run(client.get_agent(from_repository)) if response.status_code == 404: raise AgentRepositoryError( f"Agent {from_repository} does not exist, make sure the name is correct or the agent is available on your organization." diff --git a/lib/crewai/tests/cli/test_plus_api.py b/lib/crewai/tests/cli/test_plus_api.py index 0a8946c2b..70eff917e 100644 --- a/lib/crewai/tests/cli/test_plus_api.py +++ b/lib/crewai/tests/cli/test_plus_api.py @@ -1,6 +1,8 @@ import os import unittest -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, patch + +import pytest from crewai.cli.plus_api import PlusAPI @@ -68,37 +70,6 @@ class TestPlusAPI(unittest.TestCase): ) self.assertEqual(response, mock_response) - @patch("crewai.cli.plus_api.PlusAPI._make_request") - def test_get_agent(self, mock_make_request): - mock_response = MagicMock() - mock_make_request.return_value = mock_response - - response = self.api.get_agent("test_agent_handle") - mock_make_request.assert_called_once_with( - "GET", "/crewai_plus/api/v1/agents/test_agent_handle" - ) - self.assertEqual(response, mock_response) - - @patch("crewai.cli.plus_api.Settings") - @patch("requests.Session.request") - def test_get_agent_with_org_uuid(self, mock_make_request, mock_settings_class): - mock_settings = MagicMock() - mock_settings.org_uuid = self.org_uuid - mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL') - mock_settings_class.return_value = mock_settings - # re-initialize Client - self.api = PlusAPI(self.api_key) - - mock_response = MagicMock() - mock_make_request.return_value = mock_response - - response = self.api.get_agent("test_agent_handle") - - self.assert_request_with_org_id( - mock_make_request, "GET", "/crewai_plus/api/v1/agents/test_agent_handle" - ) - self.assertEqual(response, mock_response) - @patch("crewai.cli.plus_api.PlusAPI._make_request") def test_get_tool(self, mock_make_request): mock_response = MagicMock() @@ -338,3 +309,49 @@ class TestPlusAPI(unittest.TestCase): custom_api.base_url, "https://custom-url-from-env.com", ) + + +@pytest.mark.asyncio +@patch("httpx.AsyncClient") +async def test_get_agent(mock_async_client_class): + api = PlusAPI("test_api_key") + mock_response = MagicMock() + mock_client_instance = AsyncMock() + mock_client_instance.get.return_value = mock_response + mock_async_client_class.return_value.__aenter__.return_value = mock_client_instance + + response = await api.get_agent("test_agent_handle") + + mock_client_instance.get.assert_called_once_with( + f"{api.base_url}/crewai_plus/api/v1/agents/test_agent_handle", + headers=api.headers, + ) + assert response == mock_response + + +@pytest.mark.asyncio +@patch("httpx.AsyncClient") +@patch("crewai.cli.plus_api.Settings") +async def test_get_agent_with_org_uuid(mock_settings_class, mock_async_client_class): + org_uuid = "test-org-uuid" + mock_settings = MagicMock() + mock_settings.org_uuid = org_uuid + mock_settings.enterprise_base_url = os.getenv("CREWAI_PLUS_URL") + mock_settings_class.return_value = mock_settings + + api = PlusAPI("test_api_key") + + mock_response = MagicMock() + mock_client_instance = AsyncMock() + mock_client_instance.get.return_value = mock_response + mock_async_client_class.return_value.__aenter__.return_value = mock_client_instance + + response = await api.get_agent("test_agent_handle") + + mock_client_instance.get.assert_called_once_with( + f"{api.base_url}/crewai_plus/api/v1/agents/test_agent_handle", + headers=api.headers, + ) + assert "X-Crewai-Organization-Id" in api.headers + assert api.headers["X-Crewai-Organization-Id"] == org_uuid + assert response == mock_response diff --git a/lib/crewai/tests/test_human_feedback_integration.py b/lib/crewai/tests/test_human_feedback_integration.py index dd21724b4..d2d6a6f31 100644 --- a/lib/crewai/tests/test_human_feedback_integration.py +++ b/lib/crewai/tests/test_human_feedback_integration.py @@ -157,6 +157,176 @@ class TestMultiStepFlows: assert execution_order == ["generate", "review", "finalize"] + def test_chained_router_feedback_steps(self): + """Test that a router outcome can trigger another router method. + + Regression test: @listen("outcome") combined with @human_feedback(emit=...) + creates a method that is both a listener and a router. The flow must find + and execute it when the upstream router emits the matching outcome. + """ + execution_order: list[str] = [] + + class ChainedRouterFlow(Flow): + @start() + @human_feedback( + message="First review:", + emit=["approved", "rejected"], + llm="gpt-4o-mini", + ) + def draft(self): + execution_order.append("draft") + return "draft content" + + @listen("approved") + @human_feedback( + message="Final review:", + emit=["publish", "revise"], + llm="gpt-4o-mini", + ) + def final_review(self, prev: HumanFeedbackResult): + execution_order.append("final_review") + return "final content" + + @listen("rejected") + def on_rejected(self, prev: HumanFeedbackResult): + execution_order.append("on_rejected") + return "rejected" + + @listen("publish") + def on_publish(self, prev: HumanFeedbackResult): + execution_order.append("on_publish") + return "published" + + @listen("revise") + def on_revise(self, prev: HumanFeedbackResult): + execution_order.append("on_revise") + return "revised" + + flow = ChainedRouterFlow() + + with ( + patch.object( + flow, + "_request_human_feedback", + side_effect=["looks good", "ship it"], + ), + patch.object( + flow, + "_collapse_to_outcome", + side_effect=["approved", "publish"], + ), + ): + result = flow.kickoff() + + assert execution_order == ["draft", "final_review", "on_publish"] + assert result == "published" + assert len(flow.human_feedback_history) == 2 + assert flow.human_feedback_history[0].outcome == "approved" + assert flow.human_feedback_history[1].outcome == "publish" + + def test_chained_router_rejected_path(self): + """Test that a start-router outcome routes to a non-router listener.""" + execution_order: list[str] = [] + + class ChainedRouterFlow(Flow): + @start() + @human_feedback( + message="Review:", + emit=["approved", "rejected"], + llm="gpt-4o-mini", + ) + def draft(self): + execution_order.append("draft") + return "draft" + + @listen("approved") + @human_feedback( + message="Final:", + emit=["publish", "revise"], + llm="gpt-4o-mini", + ) + def final_review(self, prev: HumanFeedbackResult): + execution_order.append("final_review") + return "final" + + @listen("rejected") + def on_rejected(self, prev: HumanFeedbackResult): + execution_order.append("on_rejected") + return "rejected" + + flow = ChainedRouterFlow() + + with ( + patch.object( + flow, "_request_human_feedback", return_value="bad" + ), + patch.object( + flow, "_collapse_to_outcome", return_value="rejected" + ), + ): + result = flow.kickoff() + + assert execution_order == ["draft", "on_rejected"] + assert result == "rejected" + assert len(flow.human_feedback_history) == 1 + assert flow.human_feedback_history[0].outcome == "rejected" + + def test_router_and_non_router_listeners_for_same_outcome(self): + """Test that both router and non-router listeners fire for the same outcome.""" + execution_order: list[str] = [] + + class MixedListenerFlow(Flow): + @start() + @human_feedback( + message="Review:", + emit=["approved", "rejected"], + llm="gpt-4o-mini", + ) + def draft(self): + execution_order.append("draft") + return "draft" + + @listen("approved") + @human_feedback( + message="Final:", + emit=["publish", "revise"], + llm="gpt-4o-mini", + ) + def router_listener(self, prev: HumanFeedbackResult): + execution_order.append("router_listener") + return "final" + + @listen("approved") + def plain_listener(self, prev: HumanFeedbackResult): + execution_order.append("plain_listener") + return "logged" + + @listen("publish") + def on_publish(self, prev: HumanFeedbackResult): + execution_order.append("on_publish") + return "published" + + flow = MixedListenerFlow() + + with ( + patch.object( + flow, + "_request_human_feedback", + side_effect=["approve it", "publish it"], + ), + patch.object( + flow, + "_collapse_to_outcome", + side_effect=["approved", "publish"], + ), + ): + flow.kickoff() + + assert "draft" in execution_order + assert "router_listener" in execution_order + assert "plain_listener" in execution_order + assert "on_publish" in execution_order + class TestStateManagement: """Tests for state management with human feedback."""