fix: address review comments - unused import, FC+no-tools routing

- Remove unused asyncio import from test file
- Route FC-capable LLMs with no tools + response_model to
  _invoke_loop_native_no_tools (preserves structured output)
- FC-capable LLMs with no tools and no response_model still
  fall through to ReAct path (no regression)
- Add tests for both FC+no-tools routing scenarios

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2026-03-04 12:32:30 +00:00
parent 20be4ae62b
commit 7e60321945
2 changed files with 62 additions and 7 deletions

View File

@@ -311,16 +311,21 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
Final answer from the agent.
"""
# Check if model supports native function calling
use_native_tools = (
supports_fc = (
hasattr(self.llm, "supports_function_calling")
and callable(getattr(self.llm, "supports_function_calling", None))
and self.llm.supports_function_calling()
and self.original_tools
)
if use_native_tools:
if supports_fc and self.original_tools:
return self._invoke_loop_native_tools()
# FC-capable LLM with no tools but with response_model: use simple
# native call path which correctly passes response_model for structured
# output instead of dropping it in the ReAct path.
if supports_fc and not self.original_tools and self.response_model:
return self._invoke_loop_native_no_tools()
# Fall back to ReAct text-based pattern
return self._invoke_loop_react()
@@ -1132,16 +1137,21 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
Final answer from the agent.
"""
# Check if model supports native function calling
use_native_tools = (
supports_fc = (
hasattr(self.llm, "supports_function_calling")
and callable(getattr(self.llm, "supports_function_calling", None))
and self.llm.supports_function_calling()
and self.original_tools
)
if use_native_tools:
if supports_fc and self.original_tools:
return await self._ainvoke_loop_native_tools()
# FC-capable LLM with no tools but with response_model: use simple
# native call path which correctly passes response_model for structured
# output instead of dropping it in the ReAct path.
if supports_fc and not self.original_tools and self.response_model:
return await self._ainvoke_loop_native_no_tools()
# Fall back to ReAct text-based pattern
return await self._ainvoke_loop_react()

View File

@@ -13,7 +13,6 @@ conversion to pydantic/json should happen in task._export_output() after the ReA
from __future__ import annotations
import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
@@ -180,6 +179,52 @@ class TestReActFlowDoesNotPassResponseModel:
mock_native.assert_called_once()
mock_react.assert_not_called()
def test_invoke_loop_routes_to_native_no_tools_when_fc_no_tools_with_response_model(
self,
) -> None:
"""When LLM supports FC, has no tools, but HAS a response_model,
route to _invoke_loop_native_no_tools (which correctly passes
response_model) instead of falling through to the ReAct path."""
llm = _make_llm(supports_fc=True)
executor = _make_executor(llm, response_model=PersonInfo)
# No tools
executor.original_tools = []
with patch.object(
executor,
"_invoke_loop_native_no_tools",
return_value=AgentFinish(thought="done", output="test", text="Final Answer: test"),
) as mock_native_no_tools:
with patch.object(executor, "_invoke_loop_react") as mock_react:
with patch.object(executor, "_invoke_loop_native_tools") as mock_native:
executor._invoke_loop()
mock_native_no_tools.assert_called_once()
mock_react.assert_not_called()
mock_native.assert_not_called()
def test_invoke_loop_routes_to_react_when_fc_no_tools_no_response_model(
self,
) -> None:
"""When LLM supports FC, has no tools, and NO response_model,
fall through to ReAct path (no structured output to preserve)."""
llm = _make_llm(supports_fc=True)
executor = _make_executor(llm, response_model=None)
executor.original_tools = []
with patch.object(
executor,
"_invoke_loop_react",
return_value=AgentFinish(thought="done", output="test", text="Final Answer: test"),
) as mock_react:
with patch.object(executor, "_invoke_loop_native_no_tools") as mock_native_no_tools:
with patch.object(executor, "_invoke_loop_native_tools") as mock_native:
executor._invoke_loop()
mock_react.assert_called_once()
mock_native_no_tools.assert_not_called()
mock_native.assert_not_called()
def test_react_flow_still_works_with_tool_usage(self) -> None:
"""Verify the ReAct loop still processes Action/Observation cycles
correctly even when output_pydantic is set."""