mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-06 22:58:30 +00:00
Compare commits
2 Commits
devin/1757
...
devin/1755
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fffd401fd1 | ||
|
|
9d16fde9bf |
@@ -477,7 +477,8 @@ class Agent(BaseAgent):
|
||||
# result_as_answer set to True
|
||||
for tool_result in self.tools_results: # type: ignore # Item "None" of "list[Any] | None" has no attribute "__iter__" (not iterable)
|
||||
if tool_result.get("result_as_answer", False):
|
||||
result = tool_result["result"]
|
||||
from crewai.tools.tool_types import ToolAnswerResult
|
||||
result = ToolAnswerResult(tool_result["result"]) # type: ignore
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=AgentExecutionCompletedEvent(agent=self, task=task, output=result),
|
||||
|
||||
@@ -38,6 +38,7 @@ from crewai.security import Fingerprint, SecurityConfig
|
||||
from crewai.tasks.output_format import OutputFormat
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.tool_types import ToolAnswerResult
|
||||
from crewai.utilities.config import process_config
|
||||
from crewai.utilities.constants import NOT_SPECIFIED, _NotSpecified
|
||||
from crewai.utilities.guardrail import process_guardrail, GuardrailResult
|
||||
@@ -424,11 +425,14 @@ class Task(BaseModel):
|
||||
)
|
||||
|
||||
pydantic_output, json_output = self._export_output(result)
|
||||
|
||||
raw_result = result.result if hasattr(result, 'result') else result
|
||||
|
||||
task_output = TaskOutput(
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
expected_output=self.expected_output,
|
||||
raw=result,
|
||||
raw=raw_result,
|
||||
pydantic=pydantic_output,
|
||||
json_dict=json_output,
|
||||
agent=agent.role,
|
||||
@@ -695,8 +699,11 @@ Follow these guidelines:
|
||||
return copied_task
|
||||
|
||||
def _export_output(
|
||||
self, result: str
|
||||
self, result: Union[str, ToolAnswerResult]
|
||||
) -> Tuple[Optional[BaseModel], Optional[Dict[str, Any]]]:
|
||||
if isinstance(result, ToolAnswerResult):
|
||||
return None, None
|
||||
|
||||
pydantic_output: Optional[BaseModel] = None
|
||||
json_output: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
@@ -7,3 +7,13 @@ class ToolResult:
|
||||
|
||||
result: str
|
||||
result_as_answer: bool = False
|
||||
|
||||
|
||||
class ToolAnswerResult:
|
||||
"""Wrapper for tool results that should be used as final answers without conversion."""
|
||||
|
||||
def __init__(self, result: str):
|
||||
self.result = result
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.result
|
||||
|
||||
239
tests/test_result_as_answer_fix.py
Normal file
239
tests/test_result_as_answer_fix.py
Normal file
@@ -0,0 +1,239 @@
|
||||
from unittest.mock import patch
|
||||
from crewai.agent import Agent
|
||||
from crewai.task import Task
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.tool_types import ToolAnswerResult
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TestOutputModel(BaseModel):
|
||||
message: str
|
||||
status: str
|
||||
|
||||
|
||||
class MockTool(BaseTool):
|
||||
name: str = "mock_tool"
|
||||
description: str = "A mock tool for testing"
|
||||
result_as_answer: bool = False
|
||||
|
||||
def _run(self, *args, **kwargs) -> str:
|
||||
return "Mock tool output"
|
||||
|
||||
|
||||
class MockLLM:
|
||||
def call(self, messages, **kwargs):
|
||||
return "LLM processed output"
|
||||
|
||||
def __call__(self, messages, **kwargs):
|
||||
return self.call(messages, **kwargs)
|
||||
|
||||
|
||||
def test_tool_with_result_as_answer_true_bypasses_conversion():
|
||||
"""Test that tools with result_as_answer=True return output without conversion."""
|
||||
tool = MockTool()
|
||||
tool.result_as_answer = True
|
||||
|
||||
agent = Agent(
|
||||
role="test_agent",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
llm=MockLLM(),
|
||||
tools=[tool]
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Test output",
|
||||
agent=agent,
|
||||
output_pydantic=TestOutputModel
|
||||
)
|
||||
|
||||
agent.tools_results = [
|
||||
{
|
||||
"tool": "mock_tool",
|
||||
"result": "Plain string output that should not be converted",
|
||||
"result_as_answer": True
|
||||
}
|
||||
]
|
||||
|
||||
with patch.object(agent, 'execute_task') as mock_execute:
|
||||
mock_execute.return_value = ToolAnswerResult("Plain string output that should not be converted")
|
||||
|
||||
result = task.execute_sync()
|
||||
|
||||
assert result.raw == "Plain string output that should not be converted"
|
||||
assert result.pydantic is None
|
||||
assert result.json_dict is None
|
||||
|
||||
|
||||
def test_tool_with_result_as_answer_false_applies_conversion():
|
||||
"""Test that tools with result_as_answer=False still apply conversion when output_pydantic is set."""
|
||||
tool = MockTool()
|
||||
tool.result_as_answer = False
|
||||
|
||||
agent = Agent(
|
||||
role="test_agent",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
llm=MockLLM(),
|
||||
tools=[tool]
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Test output",
|
||||
agent=agent,
|
||||
output_pydantic=TestOutputModel
|
||||
)
|
||||
|
||||
with patch.object(agent, 'execute_task') as mock_execute:
|
||||
mock_execute.return_value = '{"message": "test", "status": "success"}'
|
||||
|
||||
with patch('crewai.task.convert_to_model') as mock_convert:
|
||||
mock_convert.return_value = TestOutputModel(message="test", status="success")
|
||||
|
||||
result = task.execute_sync()
|
||||
|
||||
assert mock_convert.called
|
||||
assert result.pydantic is not None
|
||||
assert isinstance(result.pydantic, TestOutputModel)
|
||||
|
||||
|
||||
def test_multiple_tools_last_result_as_answer_wins():
|
||||
"""Test that when multiple tools are used, the last one with result_as_answer=True is used."""
|
||||
agent = Agent(
|
||||
role="test_agent",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
llm=MockLLM()
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Test output",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
agent.tools_results = [
|
||||
{
|
||||
"tool": "tool1",
|
||||
"result": "First tool output",
|
||||
"result_as_answer": False
|
||||
},
|
||||
{
|
||||
"tool": "tool2",
|
||||
"result": "Second tool output",
|
||||
"result_as_answer": True
|
||||
},
|
||||
{
|
||||
"tool": "tool3",
|
||||
"result": "Third tool output",
|
||||
"result_as_answer": False
|
||||
},
|
||||
{
|
||||
"tool": "tool4",
|
||||
"result": "Final tool output that should be used",
|
||||
"result_as_answer": True
|
||||
}
|
||||
]
|
||||
|
||||
with patch.object(agent, 'execute_task') as mock_execute:
|
||||
mock_execute.return_value = ToolAnswerResult("Final tool output that should be used")
|
||||
|
||||
result = task.execute_sync()
|
||||
|
||||
assert result.raw == "Final tool output that should be used"
|
||||
|
||||
|
||||
def test_tool_answer_result_wrapper():
|
||||
"""Test the ToolAnswerResult wrapper class."""
|
||||
result = ToolAnswerResult("test output")
|
||||
|
||||
assert str(result) == "test output"
|
||||
|
||||
assert result.result == "test output"
|
||||
|
||||
|
||||
def test_reproduction_of_issue_3335():
|
||||
"""Reproduction test for GitHub issue #3335."""
|
||||
|
||||
tool = MockTool()
|
||||
tool.result_as_answer = True
|
||||
|
||||
def mock_tool_run(*args, **kwargs):
|
||||
return "This is a plain string that should not be converted to JSON"
|
||||
|
||||
tool._run = mock_tool_run
|
||||
|
||||
agent = Agent(
|
||||
role="test_agent",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
llm=MockLLM(),
|
||||
tools=[tool]
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Test output",
|
||||
agent=agent,
|
||||
output_pydantic=TestOutputModel
|
||||
)
|
||||
|
||||
agent.tools_results = [
|
||||
{
|
||||
"tool": "mock_tool",
|
||||
"result": "This is a plain string that should not be converted to JSON",
|
||||
"result_as_answer": True
|
||||
}
|
||||
]
|
||||
|
||||
with patch.object(agent, 'execute_task') as mock_execute:
|
||||
mock_execute.return_value = ToolAnswerResult("This is a plain string that should not be converted to JSON")
|
||||
|
||||
result = task.execute_sync()
|
||||
|
||||
assert result.raw == "This is a plain string that should not be converted to JSON"
|
||||
assert result.pydantic is None
|
||||
assert result.json_dict is None
|
||||
|
||||
|
||||
def test_edge_case_complex_tool_output():
|
||||
"""Test edge case with complex tool output that should be preserved."""
|
||||
complex_output = """
|
||||
This is a multi-line output
|
||||
with special characters: !@#$%^&*()
|
||||
and some JSON-like content: {"key": "value"}
|
||||
but it should be preserved as-is when result_as_answer=True
|
||||
"""
|
||||
|
||||
agent = Agent(
|
||||
role="test_agent",
|
||||
goal="test goal",
|
||||
backstory="test backstory",
|
||||
llm=MockLLM()
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Test output",
|
||||
agent=agent,
|
||||
output_pydantic=TestOutputModel
|
||||
)
|
||||
|
||||
agent.tools_results = [
|
||||
{
|
||||
"tool": "complex_tool",
|
||||
"result": complex_output,
|
||||
"result_as_answer": True
|
||||
}
|
||||
]
|
||||
|
||||
with patch.object(agent, 'execute_task') as mock_execute:
|
||||
mock_execute.return_value = ToolAnswerResult(complex_output)
|
||||
|
||||
result = task.execute_sync()
|
||||
|
||||
assert result.raw == complex_output
|
||||
assert result.pydantic is None
|
||||
assert result.json_dict is None
|
||||
Reference in New Issue
Block a user