1
- from crewai import Agent , Task , Crew
2
- from crewai .tools import tool
3
- from unittest .mock import patch
4
-
5
- @tool ("Simple Echo" )
6
- def echo_tool ():
7
- """Clear description for what this tool is useful for, your agent will need this information to use it."""
8
- return "TOOL_OUTPUT_SHOULD_NOT_BE_CHANGED"
9
-
10
- def test_tool_result_as_answer_bypasses_formatting ():
11
- with patch ("crewai.llms.base_llm.BaseLLM.call" ) as mock_call :
12
- mock_call .return_value = "Final Answer: TOOL_OUTPUT_SHOULD_NOT_BE_CHANGED"
13
-
14
- agent = Agent (
15
- role = "tester" ,
16
- goal = "test result_as_answer" ,
17
- backstory = "You're just here to echo things." ,
18
- tools = [echo_tool ],
19
- verbose = False
20
- )
21
-
22
- task = Task (
23
- description = "Echo something" ,
24
- agent = agent ,
25
- expected_output = "TOOL_OUTPUT_SHOULD_NOT_BE_CHANGED" ,
26
- result_as_answer = True
27
- )
28
-
29
- # crew = Crew(tasks=[task])
30
- result = echo_tool .run ()
31
-
32
- assert result == "TOOL_OUTPUT_SHOULD_NOT_BE_CHANGED"
1
+ from unittest .mock import patch , MagicMock
2
+ from crewai .agent import Agent
3
+ from crewai .crew import Crew
4
+ from crewai .task import Task
5
+ from crewai .tools .base_tool import BaseTool
6
+ from crewai .tools .tool_types import ToolAnswerResult
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class outputModel (BaseModel ):
11
+ message : str
12
+ status : str
13
+
14
+
15
+ class MockTool (BaseTool ):
16
+ name : str = "mock_tool"
17
+ description : str = "A mock tool for testing"
18
+ result_as_answer : bool = False
19
+
20
+ def _run (self , * args , ** kwargs ) -> str :
21
+ return "Mock tool output"
22
+
23
+
24
+ class MockLLM :
25
+ def call (self , messages , ** kwargs ):
26
+ return "LLM processed output"
27
+
28
+ def __call__ (self , messages , ** kwargs ):
29
+ return self .call (messages , ** kwargs )
30
+
31
+ def test_tool_with_result_as_answer_true_bypasses_conversion ():
32
+ """Test that tools with result_as_answer=True return output without conversion."""
33
+ tool = MockTool ()
34
+ tool .result_as_answer = True
35
+
36
+ agent = Agent (
37
+ role = "test_agent" ,
38
+ goal = "test goal" ,
39
+ backstory = "test backstory" ,
40
+ llm = MockLLM (),
41
+ tools = [tool ]
42
+ )
43
+
44
+ task = Task (
45
+ description = "Test task" ,
46
+ expected_output = "Test output" ,
47
+ agent = agent ,
48
+ output_pydantic = outputModel
49
+ )
50
+
51
+ agent .tools_results = [
52
+ {
53
+ "tool" : "mock_tool" ,
54
+ "result" : "Plain string output that should not be converted" ,
55
+ "result_as_answer" : True
56
+ }
57
+ ]
58
+
59
+
60
+ with patch .object (Agent , "execute_task" , return_value = ToolAnswerResult (
61
+ "Plain string output that should not be converted"
62
+ )):
63
+ result = task .execute_sync ()
64
+
65
+ assert result .raw == "Plain string output that should not be converted"
66
+ assert result .pydantic is None
67
+ assert result .json_dict is None
68
+
69
+
70
+ def test_tool_with_result_as_answer_false_applies_conversion ():
71
+ """Test that tools with result_as_answer=False still apply conversion when output_pydantic is set."""
72
+ tool = MockTool ()
73
+ tool .result_as_answer = False
74
+
75
+ agent = Agent (
76
+ role = "test_agent" ,
77
+ goal = "test goal" ,
78
+ backstory = "test backstory" ,
79
+ llm = MockLLM (),
80
+ tools = [tool ]
81
+ )
82
+
83
+ task = Task (
84
+ description = "Test task" ,
85
+ expected_output = "Test output" ,
86
+ agent = agent ,
87
+ output_pydantic = outputModel
88
+ )
89
+
90
+ with patch .object (agent , 'execute_task' ) as mock_execute :
91
+ mock_execute .return_value = '{"message": "test", "status": "success"}'
92
+
93
+ with patch ('crewai.task.convert_to_model' ) as mock_convert :
94
+ mock_convert .return_value = outputModel (message = "test" , status = "success" )
95
+
96
+ result = task .execute_sync ()
97
+
98
+ assert mock_convert .called
99
+ assert result .pydantic is not None
100
+ assert isinstance (result .pydantic , outputModel )
101
+
102
+
103
+ def test_multiple_tools_last_result_as_answer_wins ():
104
+ """Test that when multiple tools are used, the last one with result_as_answer=True is used."""
105
+ agent = Agent (
106
+ role = "test_agent" ,
107
+ goal = "test goal" ,
108
+ backstory = "test backstory" ,
109
+ llm = MockLLM ()
110
+ )
111
+
112
+ task = Task (
113
+ description = "Test task" ,
114
+ expected_output = "Test output" ,
115
+ agent = agent
116
+ )
117
+
118
+ agent .tools_results = [
119
+ {
120
+ "tool" : "tool1" ,
121
+ "result" : "First tool output" ,
122
+ "result_as_answer" : False
123
+ },
124
+ {
125
+ "tool" : "tool2" ,
126
+ "result" : "Second tool output" ,
127
+ "result_as_answer" : True
128
+ },
129
+ {
130
+ "tool" : "tool3" ,
131
+ "result" : "Third tool output" ,
132
+ "result_as_answer" : False
133
+ },
134
+ {
135
+ "tool" : "tool4" ,
136
+ "result" : "Final tool output that should be used" ,
137
+ "result_as_answer" : True
138
+ }
139
+ ]
140
+
141
+ with patch .object (agent , 'execute_task' ) as mock_execute :
142
+ mock_execute .return_value = ToolAnswerResult ("Final tool output that should be used" )
143
+
144
+ result = task .execute_sync ()
145
+
146
+ assert result .raw == "Final tool output that should be used"
147
+
148
+
149
+ def test_tool_answer_result_wrapper ():
150
+ """Test the ToolAnswerResult wrapper class."""
151
+ result = ToolAnswerResult ("test output" )
152
+
153
+ assert str (result ) == "test output"
154
+
155
+ assert result .result == "test output"
156
+
157
+
158
+ def test_reproduction_of_issue_3335 ():
159
+ """Reproduction test for GitHub issue #3335."""
160
+
161
+ tool = MockTool ()
162
+ tool .result_as_answer = True
163
+
164
+ def mock_tool_run (* args , ** kwargs ):
165
+ return "This is a plain string that should not be converted to JSON"
166
+
167
+ tool ._run = mock_tool_run
168
+
169
+ agent = Agent (
170
+ role = "test_agent" ,
171
+ goal = "test goal" ,
172
+ backstory = "test backstory" ,
173
+ llm = MockLLM (),
174
+ tools = [tool ]
175
+ )
176
+
177
+ task = Task (
178
+ description = "Test task" ,
179
+ expected_output = "Test output" ,
180
+ agent = agent ,
181
+ output_pydantic = outputModel
182
+ )
183
+
184
+ agent .tools_results = [
185
+ {
186
+ "tool" : "mock_tool" ,
187
+ "result" : "This is a plain string that should not be converted to JSON" ,
188
+ "result_as_answer" : True
189
+ }
190
+ ]
191
+
192
+ with patch .object (agent , 'execute_task' ) as mock_execute :
193
+ mock_execute .return_value = ToolAnswerResult ("This is a plain string that should not be converted to JSON" )
194
+
195
+ result = task .execute_sync ()
196
+
197
+ assert result .raw == "This is a plain string that should not be converted to JSON"
198
+ assert result .pydantic is None
199
+ assert result .json_dict is None
200
+
201
+
202
+ def test_edge_case_complex_tool_output ():
203
+ """Test edge case with complex tool output that should be preserved."""
204
+ complex_output = """
205
+ This is a multi-line output
206
+ with special characters: !@#$%^&*()
207
+ and some JSON-like content: {"key": "value"}
208
+ but it should be preserved as-is when result_as_answer=True
209
+ """
210
+
211
+ agent = Agent (
212
+ role = "test_agent" ,
213
+ goal = "test goal" ,
214
+ backstory = "test backstory" ,
215
+ llm = MockLLM ()
216
+ )
217
+
218
+ task = Task (
219
+ description = "Test task" ,
220
+ expected_output = "Test output" ,
221
+ agent = agent ,
222
+ output_pydantic = outputModel
223
+ )
224
+
225
+ agent .tools_results = [
226
+ {
227
+ "tool" : "complex_tool" ,
228
+ "result" : complex_output ,
229
+ "result_as_answer" : True
230
+ }
231
+ ]
232
+
233
+ with patch .object (agent , 'execute_task' ) as mock_execute :
234
+ mock_execute .return_value = ToolAnswerResult (complex_output )
235
+
236
+ result = task .execute_sync ()
237
+
238
+ assert result .raw == complex_output
239
+ assert result .pydantic is None
240
+ assert result .json_dict is None
241
+
0 commit comments