unique_toolkit 1.31.1__py3-none-any.whl → 1.31.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unique_toolkit/agentic/tools/tool_manager.py +16 -19
- {unique_toolkit-1.31.1.dist-info → unique_toolkit-1.31.2.dist-info}/METADATA +4 -1
- {unique_toolkit-1.31.1.dist-info → unique_toolkit-1.31.2.dist-info}/RECORD +5 -6
- unique_toolkit/agentic/tools/test/test_tool_manager.py +0 -1686
- {unique_toolkit-1.31.1.dist-info → unique_toolkit-1.31.2.dist-info}/LICENSE +0 -0
- {unique_toolkit-1.31.1.dist-info → unique_toolkit-1.31.2.dist-info}/WHEEL +0 -0
|
@@ -1,1686 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from unittest.mock import Mock
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from pydantic import BaseModel
|
|
6
|
-
|
|
7
|
-
from tests.test_obj_factory import get_event_obj
|
|
8
|
-
from unique_toolkit.agentic.tools.a2a import SubAgentResponseWatcher
|
|
9
|
-
from unique_toolkit.agentic.tools.a2a.manager import A2AManager
|
|
10
|
-
from unique_toolkit.agentic.tools.config import (
|
|
11
|
-
ToolBuildConfig,
|
|
12
|
-
ToolIcon,
|
|
13
|
-
ToolSelectionPolicy,
|
|
14
|
-
)
|
|
15
|
-
from unique_toolkit.agentic.tools.factory import ToolFactory
|
|
16
|
-
from unique_toolkit.agentic.tools.mcp.manager import MCPManager
|
|
17
|
-
from unique_toolkit.agentic.tools.openai_builtin.base import (
|
|
18
|
-
OpenAIBuiltInTool,
|
|
19
|
-
)
|
|
20
|
-
from unique_toolkit.agentic.tools.openai_builtin.manager import (
|
|
21
|
-
OpenAIBuiltInToolManager,
|
|
22
|
-
)
|
|
23
|
-
from unique_toolkit.agentic.tools.schemas import BaseToolConfig, ToolCallResponse
|
|
24
|
-
from unique_toolkit.agentic.tools.tool import Tool
|
|
25
|
-
from unique_toolkit.agentic.tools.tool_manager import (
|
|
26
|
-
ResponsesApiToolManager,
|
|
27
|
-
ToolManager,
|
|
28
|
-
ToolManagerConfig,
|
|
29
|
-
_convert_to_forced_tool,
|
|
30
|
-
)
|
|
31
|
-
from unique_toolkit.agentic.tools.tool_progress_reporter import ToolProgressReporter
|
|
32
|
-
from unique_toolkit.chat.service import ChatService
|
|
33
|
-
from unique_toolkit.language_model.schemas import LanguageModelFunction
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class MockParameters(BaseModel):
|
|
37
|
-
query: str = ""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class MockToolConfig(BaseToolConfig):
|
|
41
|
-
"""Mock configuration for test tool"""
|
|
42
|
-
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class MockTool(Tool[MockToolConfig]):
|
|
47
|
-
"""Mock tool for testing"""
|
|
48
|
-
|
|
49
|
-
name = "mock_tool"
|
|
50
|
-
|
|
51
|
-
def __init__(self, config, event, tool_progress_reporter=None):
|
|
52
|
-
super().__init__(config, event, tool_progress_reporter)
|
|
53
|
-
|
|
54
|
-
def tool_description(self):
|
|
55
|
-
from unique_toolkit.language_model.schemas import LanguageModelToolDescription
|
|
56
|
-
|
|
57
|
-
return LanguageModelToolDescription(
|
|
58
|
-
name=self.name,
|
|
59
|
-
description="Mock tool for testing",
|
|
60
|
-
parameters=MockParameters,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
def evaluation_check_list(self):
|
|
64
|
-
return []
|
|
65
|
-
|
|
66
|
-
def get_evaluation_checks_based_on_tool_response(self, tool_response):
|
|
67
|
-
return []
|
|
68
|
-
|
|
69
|
-
async def run(self, tool_call):
|
|
70
|
-
return ToolCallResponse(
|
|
71
|
-
id=tool_call.id,
|
|
72
|
-
name=tool_call.name,
|
|
73
|
-
content="Mock response",
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class MockExclusiveTool(Tool[MockToolConfig]):
|
|
78
|
-
"""Mock exclusive tool for testing"""
|
|
79
|
-
|
|
80
|
-
name = "exclusive_tool"
|
|
81
|
-
|
|
82
|
-
def __init__(self, config, event, tool_progress_reporter=None):
|
|
83
|
-
super().__init__(config, event, tool_progress_reporter)
|
|
84
|
-
|
|
85
|
-
def tool_description(self):
|
|
86
|
-
from unique_toolkit.language_model.schemas import LanguageModelToolDescription
|
|
87
|
-
|
|
88
|
-
return LanguageModelToolDescription(
|
|
89
|
-
name=self.name,
|
|
90
|
-
description="Mock exclusive tool",
|
|
91
|
-
parameters=MockParameters,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
def evaluation_check_list(self):
|
|
95
|
-
return []
|
|
96
|
-
|
|
97
|
-
def get_evaluation_checks_based_on_tool_response(self, tool_response):
|
|
98
|
-
return []
|
|
99
|
-
|
|
100
|
-
async def run(self, tool_call):
|
|
101
|
-
return ToolCallResponse(
|
|
102
|
-
id=tool_call.id,
|
|
103
|
-
name=tool_call.name,
|
|
104
|
-
content="Exclusive response",
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class MockControlTool(Tool[MockToolConfig]):
|
|
109
|
-
"""Mock tool that takes control"""
|
|
110
|
-
|
|
111
|
-
name = "control_tool"
|
|
112
|
-
|
|
113
|
-
def __init__(self, config, event, tool_progress_reporter=None):
|
|
114
|
-
super().__init__(config, event, tool_progress_reporter)
|
|
115
|
-
|
|
116
|
-
def tool_description(self):
|
|
117
|
-
from unique_toolkit.language_model.schemas import LanguageModelToolDescription
|
|
118
|
-
|
|
119
|
-
return LanguageModelToolDescription(
|
|
120
|
-
name=self.name,
|
|
121
|
-
description="Mock control tool",
|
|
122
|
-
parameters=MockParameters,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
def takes_control(self):
|
|
126
|
-
return True
|
|
127
|
-
|
|
128
|
-
def evaluation_check_list(self):
|
|
129
|
-
return []
|
|
130
|
-
|
|
131
|
-
def get_evaluation_checks_based_on_tool_response(self, tool_response):
|
|
132
|
-
return []
|
|
133
|
-
|
|
134
|
-
async def run(self, tool_call):
|
|
135
|
-
return ToolCallResponse(
|
|
136
|
-
id=tool_call.id,
|
|
137
|
-
name=tool_call.name,
|
|
138
|
-
content="Control response",
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
@pytest.fixture(autouse=True)
|
|
143
|
-
def register_mock_tools():
|
|
144
|
-
"""Register mock tools with ToolFactory"""
|
|
145
|
-
ToolFactory.register_tool(MockTool, MockToolConfig)
|
|
146
|
-
ToolFactory.register_tool(MockExclusiveTool, MockToolConfig)
|
|
147
|
-
ToolFactory.register_tool(MockControlTool, MockToolConfig)
|
|
148
|
-
yield
|
|
149
|
-
# Cleanup not needed as registry persists across tests
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
@pytest.fixture
|
|
153
|
-
def logger():
|
|
154
|
-
"""Create logger fixture."""
|
|
155
|
-
return logging.getLogger(__name__)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@pytest.fixture
|
|
159
|
-
def base_event():
|
|
160
|
-
"""Create base event fixture."""
|
|
161
|
-
return get_event_obj(
|
|
162
|
-
user_id="test_user",
|
|
163
|
-
company_id="test_company",
|
|
164
|
-
assistant_id="test_assistant",
|
|
165
|
-
chat_id="test_chat",
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@pytest.fixture
|
|
170
|
-
def mock_chat_service():
|
|
171
|
-
"""Create mock chat service fixture."""
|
|
172
|
-
return Mock(spec=ChatService)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
@pytest.fixture
|
|
176
|
-
def tool_progress_reporter(mock_chat_service):
|
|
177
|
-
"""Create tool progress reporter fixture."""
|
|
178
|
-
return ToolProgressReporter(mock_chat_service)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
@pytest.fixture
|
|
182
|
-
def a2a_manager(logger, tool_progress_reporter):
|
|
183
|
-
"""Create A2A manager fixture."""
|
|
184
|
-
return A2AManager(
|
|
185
|
-
logger=logger,
|
|
186
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
187
|
-
response_watcher=SubAgentResponseWatcher(),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
@pytest.fixture
|
|
192
|
-
def mcp_manager(base_event, tool_progress_reporter):
|
|
193
|
-
"""Create MCP manager fixture with no servers."""
|
|
194
|
-
return MCPManager(
|
|
195
|
-
mcp_servers=[],
|
|
196
|
-
event=base_event,
|
|
197
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
@pytest.fixture
|
|
202
|
-
def tool_config():
|
|
203
|
-
"""Create single tool configuration fixture."""
|
|
204
|
-
return ToolBuildConfig(
|
|
205
|
-
name="mock_tool",
|
|
206
|
-
configuration=MockToolConfig(),
|
|
207
|
-
display_name="Mock Tool",
|
|
208
|
-
icon=ToolIcon.BOOK,
|
|
209
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
210
|
-
is_exclusive=False,
|
|
211
|
-
is_enabled=True,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
@pytest.fixture
|
|
216
|
-
def tool_manager_config(tool_config):
|
|
217
|
-
"""Create tool manager configuration fixture."""
|
|
218
|
-
return ToolManagerConfig(
|
|
219
|
-
tools=[tool_config],
|
|
220
|
-
max_tool_calls=10,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
@pytest.mark.ai
|
|
225
|
-
def test_tool_manager__initializes__with_basic_config(
|
|
226
|
-
logger,
|
|
227
|
-
tool_manager_config,
|
|
228
|
-
base_event,
|
|
229
|
-
tool_progress_reporter,
|
|
230
|
-
mcp_manager,
|
|
231
|
-
a2a_manager,
|
|
232
|
-
) -> None:
|
|
233
|
-
"""
|
|
234
|
-
Purpose: Verify ToolManager initializes correctly with basic configuration.
|
|
235
|
-
Why this matters: Ensures core initialization works for completions API mode.
|
|
236
|
-
Setup summary: Create ToolManager with minimal config, verify it initializes without errors.
|
|
237
|
-
"""
|
|
238
|
-
# Arrange & Act
|
|
239
|
-
tool_manager = ToolManager(
|
|
240
|
-
logger=logger,
|
|
241
|
-
config=tool_manager_config,
|
|
242
|
-
event=base_event,
|
|
243
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
244
|
-
mcp_manager=mcp_manager,
|
|
245
|
-
a2a_manager=a2a_manager,
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
# Assert
|
|
249
|
-
assert tool_manager is not None
|
|
250
|
-
assert tool_manager._api_mode == "completions"
|
|
251
|
-
assert len(tool_manager.get_tools()) > 0
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
@pytest.mark.ai
|
|
255
|
-
def test_responses_api_tool_manager__initializes__with_builtin_tools(
|
|
256
|
-
logger,
|
|
257
|
-
tool_manager_config,
|
|
258
|
-
base_event,
|
|
259
|
-
tool_progress_reporter,
|
|
260
|
-
mcp_manager,
|
|
261
|
-
a2a_manager,
|
|
262
|
-
mocker,
|
|
263
|
-
) -> None:
|
|
264
|
-
"""
|
|
265
|
-
Purpose: Verify ResponsesApiToolManager initializes with OpenAI built-in tools.
|
|
266
|
-
Why this matters: Ensures responses API mode includes built-in tools.
|
|
267
|
-
Setup summary: Create ResponsesApiToolManager with mock builtin manager, verify initialization.
|
|
268
|
-
"""
|
|
269
|
-
# Arrange
|
|
270
|
-
mock_builtin_manager = mocker.Mock(spec=OpenAIBuiltInToolManager)
|
|
271
|
-
mock_builtin_manager.get_all_openai_builtin_tools.return_value = []
|
|
272
|
-
|
|
273
|
-
# Act
|
|
274
|
-
tool_manager = ResponsesApiToolManager(
|
|
275
|
-
logger=logger,
|
|
276
|
-
config=tool_manager_config,
|
|
277
|
-
event=base_event,
|
|
278
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
279
|
-
mcp_manager=mcp_manager,
|
|
280
|
-
a2a_manager=a2a_manager,
|
|
281
|
-
builtin_tool_manager=mock_builtin_manager,
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
# Assert
|
|
285
|
-
assert tool_manager is not None
|
|
286
|
-
assert tool_manager._api_mode == "responses"
|
|
287
|
-
mock_builtin_manager.get_all_openai_builtin_tools.assert_called_once()
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
@pytest.mark.ai
|
|
291
|
-
def test_tool_manager__filters_disabled_tools__from_config(
|
|
292
|
-
logger,
|
|
293
|
-
base_event,
|
|
294
|
-
tool_progress_reporter,
|
|
295
|
-
mcp_manager,
|
|
296
|
-
a2a_manager,
|
|
297
|
-
) -> None:
|
|
298
|
-
"""
|
|
299
|
-
Purpose: Verify tools marked as disabled in config are filtered out.
|
|
300
|
-
Why this matters: Ensures disabled tools don't get added to available tools.
|
|
301
|
-
Setup summary: Create config with disabled tool, verify it's not in tools list.
|
|
302
|
-
"""
|
|
303
|
-
# Arrange
|
|
304
|
-
disabled_tool_config = ToolBuildConfig(
|
|
305
|
-
name="mock_tool",
|
|
306
|
-
configuration=MockToolConfig(),
|
|
307
|
-
display_name="Mock Tool",
|
|
308
|
-
icon=ToolIcon.BOOK,
|
|
309
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
310
|
-
is_exclusive=False,
|
|
311
|
-
is_enabled=False, # Disabled
|
|
312
|
-
)
|
|
313
|
-
config = ToolManagerConfig(tools=[disabled_tool_config], max_tool_calls=10)
|
|
314
|
-
|
|
315
|
-
# Act
|
|
316
|
-
tool_manager = ToolManager(
|
|
317
|
-
logger=logger,
|
|
318
|
-
config=config,
|
|
319
|
-
event=base_event,
|
|
320
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
321
|
-
mcp_manager=mcp_manager,
|
|
322
|
-
a2a_manager=a2a_manager,
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
# Assert
|
|
326
|
-
tool_names = [t.name for t in tool_manager.get_tools()]
|
|
327
|
-
assert "mock_tool" not in tool_names
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
@pytest.mark.ai
|
|
331
|
-
def test_tool_manager__filters_disabled_tools__from_event(
|
|
332
|
-
logger,
|
|
333
|
-
tool_manager_config,
|
|
334
|
-
base_event,
|
|
335
|
-
tool_progress_reporter,
|
|
336
|
-
mcp_manager,
|
|
337
|
-
a2a_manager,
|
|
338
|
-
) -> None:
|
|
339
|
-
"""
|
|
340
|
-
Purpose: Verify tools in disabled_tools list from event are filtered out.
|
|
341
|
-
Why this matters: Allows runtime control of tool availability.
|
|
342
|
-
Setup summary: Set event.payload.disabled_tools, verify tool is filtered.
|
|
343
|
-
"""
|
|
344
|
-
# Arrange
|
|
345
|
-
base_event.payload.disabled_tools = ["mock_tool"]
|
|
346
|
-
|
|
347
|
-
# Act
|
|
348
|
-
tool_manager = ToolManager(
|
|
349
|
-
logger=logger,
|
|
350
|
-
config=tool_manager_config,
|
|
351
|
-
event=base_event,
|
|
352
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
353
|
-
mcp_manager=mcp_manager,
|
|
354
|
-
a2a_manager=a2a_manager,
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
# Assert
|
|
358
|
-
tool_names = [t.name for t in tool_manager.get_tools()]
|
|
359
|
-
assert "mock_tool" not in tool_names
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
@pytest.mark.ai
|
|
363
|
-
def test_tool_manager__filters_by_tool_choices__when_specified(
|
|
364
|
-
logger,
|
|
365
|
-
base_event,
|
|
366
|
-
tool_progress_reporter,
|
|
367
|
-
mcp_manager,
|
|
368
|
-
a2a_manager,
|
|
369
|
-
) -> None:
|
|
370
|
-
"""
|
|
371
|
-
Purpose: Verify only tools in tool_choices are included when choices specified.
|
|
372
|
-
Why this matters: Allows limiting available tools to user selection.
|
|
373
|
-
Setup summary: Create multiple tools, set tool_choices to subset, verify filtering.
|
|
374
|
-
"""
|
|
375
|
-
# Arrange
|
|
376
|
-
tool_configs = [
|
|
377
|
-
ToolBuildConfig(
|
|
378
|
-
name="mock_tool",
|
|
379
|
-
configuration=MockToolConfig(),
|
|
380
|
-
display_name="Mock Tool",
|
|
381
|
-
icon=ToolIcon.BOOK,
|
|
382
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
383
|
-
is_exclusive=False,
|
|
384
|
-
is_enabled=True,
|
|
385
|
-
),
|
|
386
|
-
ToolBuildConfig(
|
|
387
|
-
name="control_tool",
|
|
388
|
-
configuration=MockToolConfig(),
|
|
389
|
-
display_name="Control Tool",
|
|
390
|
-
icon=ToolIcon.ANALYTICS,
|
|
391
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
392
|
-
is_exclusive=False,
|
|
393
|
-
is_enabled=True,
|
|
394
|
-
),
|
|
395
|
-
]
|
|
396
|
-
config = ToolManagerConfig(tools=tool_configs, max_tool_calls=10)
|
|
397
|
-
base_event.payload.tool_choices = ["mock_tool"]
|
|
398
|
-
|
|
399
|
-
# Act
|
|
400
|
-
tool_manager = ToolManager(
|
|
401
|
-
logger=logger,
|
|
402
|
-
config=config,
|
|
403
|
-
event=base_event,
|
|
404
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
405
|
-
mcp_manager=mcp_manager,
|
|
406
|
-
a2a_manager=a2a_manager,
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
# Assert
|
|
410
|
-
tool_names = [t.name for t in tool_manager.get_tools()]
|
|
411
|
-
assert "mock_tool" in tool_names
|
|
412
|
-
assert "control_tool" not in tool_names
|
|
413
|
-
assert len(tool_names) == 1
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
@pytest.mark.ai
|
|
417
|
-
def test_tool_manager__uses_exclusive_tool__when_in_tool_choices(
|
|
418
|
-
logger,
|
|
419
|
-
base_event,
|
|
420
|
-
tool_progress_reporter,
|
|
421
|
-
mcp_manager,
|
|
422
|
-
a2a_manager,
|
|
423
|
-
) -> None:
|
|
424
|
-
"""
|
|
425
|
-
Purpose: Verify exclusive tool overrides all others when in tool_choices.
|
|
426
|
-
Why this matters: Ensures exclusive tools work as intended for specialized tasks.
|
|
427
|
-
Setup summary: Create exclusive and regular tools, select exclusive, verify only it exists.
|
|
428
|
-
"""
|
|
429
|
-
# Arrange
|
|
430
|
-
tool_configs = [
|
|
431
|
-
ToolBuildConfig(
|
|
432
|
-
name="mock_tool",
|
|
433
|
-
configuration=MockToolConfig(),
|
|
434
|
-
display_name="Mock Tool",
|
|
435
|
-
icon=ToolIcon.BOOK,
|
|
436
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
437
|
-
is_exclusive=False,
|
|
438
|
-
is_enabled=True,
|
|
439
|
-
),
|
|
440
|
-
ToolBuildConfig(
|
|
441
|
-
name="exclusive_tool",
|
|
442
|
-
configuration=MockToolConfig(),
|
|
443
|
-
display_name="Exclusive Tool",
|
|
444
|
-
icon=ToolIcon.ANALYTICS,
|
|
445
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
446
|
-
is_exclusive=True,
|
|
447
|
-
is_enabled=True,
|
|
448
|
-
),
|
|
449
|
-
]
|
|
450
|
-
config = ToolManagerConfig(tools=tool_configs, max_tool_calls=10)
|
|
451
|
-
base_event.payload.tool_choices = ["exclusive_tool"]
|
|
452
|
-
|
|
453
|
-
# Act
|
|
454
|
-
tool_manager = ToolManager(
|
|
455
|
-
logger=logger,
|
|
456
|
-
config=config,
|
|
457
|
-
event=base_event,
|
|
458
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
459
|
-
mcp_manager=mcp_manager,
|
|
460
|
-
a2a_manager=a2a_manager,
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
# Assert
|
|
464
|
-
tools = tool_manager.get_tools()
|
|
465
|
-
assert len(tools) == 1
|
|
466
|
-
assert tools[0].name == "exclusive_tool"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
@pytest.mark.ai
|
|
470
|
-
def test_tool_manager__excludes_exclusive_tool__when_not_in_choices(
|
|
471
|
-
logger,
|
|
472
|
-
base_event,
|
|
473
|
-
tool_progress_reporter,
|
|
474
|
-
mcp_manager,
|
|
475
|
-
a2a_manager,
|
|
476
|
-
) -> None:
|
|
477
|
-
"""
|
|
478
|
-
Purpose: Verify exclusive tools are excluded when not explicitly chosen.
|
|
479
|
-
Why this matters: Exclusive tools should only be available when explicitly selected.
|
|
480
|
-
Setup summary: Create exclusive tool without selecting it, verify it's excluded.
|
|
481
|
-
"""
|
|
482
|
-
# Arrange
|
|
483
|
-
tool_configs = [
|
|
484
|
-
ToolBuildConfig(
|
|
485
|
-
name="mock_tool",
|
|
486
|
-
configuration=MockToolConfig(),
|
|
487
|
-
display_name="Mock Tool",
|
|
488
|
-
icon=ToolIcon.BOOK,
|
|
489
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
490
|
-
is_exclusive=False,
|
|
491
|
-
is_enabled=True,
|
|
492
|
-
),
|
|
493
|
-
ToolBuildConfig(
|
|
494
|
-
name="exclusive_tool",
|
|
495
|
-
configuration=MockToolConfig(),
|
|
496
|
-
display_name="Exclusive Tool",
|
|
497
|
-
icon=ToolIcon.ANALYTICS,
|
|
498
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
499
|
-
is_exclusive=True,
|
|
500
|
-
is_enabled=True,
|
|
501
|
-
),
|
|
502
|
-
]
|
|
503
|
-
config = ToolManagerConfig(tools=tool_configs, max_tool_calls=10)
|
|
504
|
-
|
|
505
|
-
# Act
|
|
506
|
-
tool_manager = ToolManager(
|
|
507
|
-
logger=logger,
|
|
508
|
-
config=config,
|
|
509
|
-
event=base_event,
|
|
510
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
511
|
-
mcp_manager=mcp_manager,
|
|
512
|
-
a2a_manager=a2a_manager,
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
# Assert
|
|
516
|
-
tool_names = [t.name for t in tool_manager.get_tools()]
|
|
517
|
-
assert "exclusive_tool" not in tool_names
|
|
518
|
-
assert "mock_tool" in tool_names
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
@pytest.mark.ai
|
|
522
|
-
def test_tool_manager__get_tool_by_name__returns_tool(
|
|
523
|
-
logger,
|
|
524
|
-
tool_manager_config,
|
|
525
|
-
base_event,
|
|
526
|
-
tool_progress_reporter,
|
|
527
|
-
mcp_manager,
|
|
528
|
-
a2a_manager,
|
|
529
|
-
) -> None:
|
|
530
|
-
"""
|
|
531
|
-
Purpose: Verify get_tool_by_name returns correct tool when it exists.
|
|
532
|
-
Why this matters: Core retrieval functionality for tool execution.
|
|
533
|
-
Setup summary: Create tool manager, retrieve tool by name, verify it matches.
|
|
534
|
-
"""
|
|
535
|
-
# Arrange
|
|
536
|
-
tool_manager = ToolManager(
|
|
537
|
-
logger=logger,
|
|
538
|
-
config=tool_manager_config,
|
|
539
|
-
event=base_event,
|
|
540
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
541
|
-
mcp_manager=mcp_manager,
|
|
542
|
-
a2a_manager=a2a_manager,
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
# Act
|
|
546
|
-
tool = tool_manager.get_tool_by_name("mock_tool")
|
|
547
|
-
|
|
548
|
-
# Assert
|
|
549
|
-
assert tool is not None
|
|
550
|
-
assert tool.name == "mock_tool"
|
|
551
|
-
assert isinstance(tool, Tool)
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
@pytest.mark.ai
|
|
555
|
-
def test_tool_manager__get_tool_by_name__returns_none_for_missing(
|
|
556
|
-
logger,
|
|
557
|
-
tool_manager_config,
|
|
558
|
-
base_event,
|
|
559
|
-
tool_progress_reporter,
|
|
560
|
-
mcp_manager,
|
|
561
|
-
a2a_manager,
|
|
562
|
-
) -> None:
|
|
563
|
-
"""
|
|
564
|
-
Purpose: Verify get_tool_by_name returns None for non-existent tools.
|
|
565
|
-
Why this matters: Proper error handling for invalid tool names.
|
|
566
|
-
Setup summary: Create tool manager, request non-existent tool, verify None returned.
|
|
567
|
-
"""
|
|
568
|
-
# Arrange
|
|
569
|
-
tool_manager = ToolManager(
|
|
570
|
-
logger=logger,
|
|
571
|
-
config=tool_manager_config,
|
|
572
|
-
event=base_event,
|
|
573
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
574
|
-
mcp_manager=mcp_manager,
|
|
575
|
-
a2a_manager=a2a_manager,
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
# Act
|
|
579
|
-
tool = tool_manager.get_tool_by_name("non_existent_tool")
|
|
580
|
-
|
|
581
|
-
# Assert
|
|
582
|
-
assert tool is None
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
@pytest.mark.ai
|
|
586
|
-
def test_tool_manager__get_tools__returns_copy(
|
|
587
|
-
logger,
|
|
588
|
-
tool_manager_config,
|
|
589
|
-
base_event,
|
|
590
|
-
tool_progress_reporter,
|
|
591
|
-
mcp_manager,
|
|
592
|
-
a2a_manager,
|
|
593
|
-
) -> None:
|
|
594
|
-
"""
|
|
595
|
-
Purpose: Verify get_tools returns a copy, not original list.
|
|
596
|
-
Why this matters: Prevents external modification of internal tool list.
|
|
597
|
-
Setup summary: Get tools list twice, verify they're different objects.
|
|
598
|
-
"""
|
|
599
|
-
# Arrange
|
|
600
|
-
tool_manager = ToolManager(
|
|
601
|
-
logger=logger,
|
|
602
|
-
config=tool_manager_config,
|
|
603
|
-
event=base_event,
|
|
604
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
605
|
-
mcp_manager=mcp_manager,
|
|
606
|
-
a2a_manager=a2a_manager,
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
# Act
|
|
610
|
-
tools1 = tool_manager.get_tools()
|
|
611
|
-
tools2 = tool_manager.get_tools()
|
|
612
|
-
|
|
613
|
-
# Assert
|
|
614
|
-
assert tools1 == tools2 # Equal content
|
|
615
|
-
assert tools1 is not tools2 # Different objects
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
@pytest.mark.ai
|
|
619
|
-
def test_tool_manager__get_tool_choices__returns_copy(
|
|
620
|
-
logger,
|
|
621
|
-
tool_manager_config,
|
|
622
|
-
base_event,
|
|
623
|
-
tool_progress_reporter,
|
|
624
|
-
mcp_manager,
|
|
625
|
-
a2a_manager,
|
|
626
|
-
) -> None:
|
|
627
|
-
"""
|
|
628
|
-
Purpose: Verify get_tool_choices returns a copy, not original list.
|
|
629
|
-
Why this matters: Prevents external modification of internal choices list.
|
|
630
|
-
Setup summary: Get choices twice, verify they're different objects.
|
|
631
|
-
"""
|
|
632
|
-
# Arrange
|
|
633
|
-
base_event.payload.tool_choices = ["mock_tool"]
|
|
634
|
-
tool_manager = ToolManager(
|
|
635
|
-
logger=logger,
|
|
636
|
-
config=tool_manager_config,
|
|
637
|
-
event=base_event,
|
|
638
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
639
|
-
mcp_manager=mcp_manager,
|
|
640
|
-
a2a_manager=a2a_manager,
|
|
641
|
-
)
|
|
642
|
-
|
|
643
|
-
# Act
|
|
644
|
-
choices1 = tool_manager.get_tool_choices()
|
|
645
|
-
choices2 = tool_manager.get_tool_choices()
|
|
646
|
-
|
|
647
|
-
# Assert
|
|
648
|
-
assert choices1 == choices2
|
|
649
|
-
assert choices1 is not choices2
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
@pytest.mark.ai
|
|
653
|
-
def test_tool_manager__get_exclusive_tools__returns_exclusive_names(
|
|
654
|
-
logger,
|
|
655
|
-
base_event,
|
|
656
|
-
tool_progress_reporter,
|
|
657
|
-
mcp_manager,
|
|
658
|
-
a2a_manager,
|
|
659
|
-
) -> None:
|
|
660
|
-
"""
|
|
661
|
-
Purpose: Verify get_exclusive_tools returns names of exclusive tools.
|
|
662
|
-
Why this matters: Allows checking which tools are exclusive for special handling.
|
|
663
|
-
Setup summary: Create exclusive tool, verify it appears in exclusive tools list.
|
|
664
|
-
"""
|
|
665
|
-
# Arrange
|
|
666
|
-
tool_configs = [
|
|
667
|
-
ToolBuildConfig(
|
|
668
|
-
name="exclusive_tool",
|
|
669
|
-
configuration=MockToolConfig(),
|
|
670
|
-
display_name="Exclusive Tool",
|
|
671
|
-
icon=ToolIcon.ANALYTICS,
|
|
672
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
673
|
-
is_exclusive=True,
|
|
674
|
-
is_enabled=True,
|
|
675
|
-
),
|
|
676
|
-
]
|
|
677
|
-
config = ToolManagerConfig(tools=tool_configs, max_tool_calls=10)
|
|
678
|
-
|
|
679
|
-
tool_manager = ToolManager(
|
|
680
|
-
logger=logger,
|
|
681
|
-
config=config,
|
|
682
|
-
event=base_event,
|
|
683
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
684
|
-
mcp_manager=mcp_manager,
|
|
685
|
-
a2a_manager=a2a_manager,
|
|
686
|
-
)
|
|
687
|
-
|
|
688
|
-
# Act
|
|
689
|
-
exclusive_tools = tool_manager.get_exclusive_tools()
|
|
690
|
-
|
|
691
|
-
# Assert
|
|
692
|
-
assert "exclusive_tool" in exclusive_tools
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
@pytest.mark.ai
|
|
696
|
-
def test_tool_manager__get_tool_definitions__returns_descriptions(
|
|
697
|
-
logger,
|
|
698
|
-
tool_manager_config,
|
|
699
|
-
base_event,
|
|
700
|
-
tool_progress_reporter,
|
|
701
|
-
mcp_manager,
|
|
702
|
-
a2a_manager,
|
|
703
|
-
) -> None:
|
|
704
|
-
"""
|
|
705
|
-
Purpose: Verify get_tool_definitions returns tool descriptions for LLM.
|
|
706
|
-
Why this matters: Provides tool schemas to language model for tool calling.
|
|
707
|
-
Setup summary: Create tool manager, get definitions, verify structure.
|
|
708
|
-
"""
|
|
709
|
-
# Arrange
|
|
710
|
-
tool_manager = ToolManager(
|
|
711
|
-
logger=logger,
|
|
712
|
-
config=tool_manager_config,
|
|
713
|
-
event=base_event,
|
|
714
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
715
|
-
mcp_manager=mcp_manager,
|
|
716
|
-
a2a_manager=a2a_manager,
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
# Act
|
|
720
|
-
definitions = tool_manager.get_tool_definitions()
|
|
721
|
-
|
|
722
|
-
# Assert
|
|
723
|
-
assert len(definitions) > 0
|
|
724
|
-
definition = definitions[0]
|
|
725
|
-
assert hasattr(definition, "name")
|
|
726
|
-
assert hasattr(definition, "description")
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
@pytest.mark.ai
|
|
730
|
-
def test_tool_manager__get_forced_tools__returns_forced_tool_params(
|
|
731
|
-
logger,
|
|
732
|
-
tool_manager_config,
|
|
733
|
-
base_event,
|
|
734
|
-
tool_progress_reporter,
|
|
735
|
-
mcp_manager,
|
|
736
|
-
a2a_manager,
|
|
737
|
-
) -> None:
|
|
738
|
-
"""
|
|
739
|
-
Purpose: Verify get_forced_tools returns proper format for forced tool calls.
|
|
740
|
-
Why this matters: Enables forcing specific tools in LLM requests.
|
|
741
|
-
Setup summary: Set tool_choices, verify forced tools returned in correct format.
|
|
742
|
-
"""
|
|
743
|
-
# Arrange
|
|
744
|
-
base_event.payload.tool_choices = ["mock_tool"]
|
|
745
|
-
tool_manager = ToolManager(
|
|
746
|
-
logger=logger,
|
|
747
|
-
config=tool_manager_config,
|
|
748
|
-
event=base_event,
|
|
749
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
750
|
-
mcp_manager=mcp_manager,
|
|
751
|
-
a2a_manager=a2a_manager,
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
# Act
|
|
755
|
-
forced_tools = tool_manager.get_forced_tools()
|
|
756
|
-
|
|
757
|
-
# Assert
|
|
758
|
-
assert len(forced_tools) == 1
|
|
759
|
-
forced_tool = forced_tools[0]
|
|
760
|
-
assert forced_tool["type"] == "function"
|
|
761
|
-
assert forced_tool["function"]["name"] == "mock_tool"
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
@pytest.mark.ai
|
|
765
|
-
def test_tool_manager__get_tool_prompts__returns_prompt_info(
|
|
766
|
-
logger,
|
|
767
|
-
tool_manager_config,
|
|
768
|
-
base_event,
|
|
769
|
-
tool_progress_reporter,
|
|
770
|
-
mcp_manager,
|
|
771
|
-
a2a_manager,
|
|
772
|
-
) -> None:
|
|
773
|
-
"""
|
|
774
|
-
Purpose: Verify get_tool_prompts returns prompt information for all tools.
|
|
775
|
-
Why this matters: Provides system/user prompts for tool usage instructions.
|
|
776
|
-
Setup summary: Create tool manager, get prompts, verify structure.
|
|
777
|
-
"""
|
|
778
|
-
# Arrange
|
|
779
|
-
tool_manager = ToolManager(
|
|
780
|
-
logger=logger,
|
|
781
|
-
config=tool_manager_config,
|
|
782
|
-
event=base_event,
|
|
783
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
784
|
-
mcp_manager=mcp_manager,
|
|
785
|
-
a2a_manager=a2a_manager,
|
|
786
|
-
)
|
|
787
|
-
|
|
788
|
-
# Act
|
|
789
|
-
prompts = tool_manager.get_tool_prompts()
|
|
790
|
-
|
|
791
|
-
# Assert
|
|
792
|
-
assert len(prompts) > 0
|
|
793
|
-
prompt = prompts[0]
|
|
794
|
-
assert hasattr(prompt, "name")
|
|
795
|
-
assert hasattr(prompt, "tool_system_prompt")
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
@pytest.mark.ai
|
|
799
|
-
def test_tool_manager__does_a_tool_take_control__returns_true_for_control_tool(
|
|
800
|
-
logger,
|
|
801
|
-
base_event,
|
|
802
|
-
tool_progress_reporter,
|
|
803
|
-
mcp_manager,
|
|
804
|
-
a2a_manager,
|
|
805
|
-
) -> None:
|
|
806
|
-
"""
|
|
807
|
-
Purpose: Verify does_a_tool_take_control detects tools that take control.
|
|
808
|
-
Why this matters: Some tools need exclusive conversation control.
|
|
809
|
-
Setup summary: Create control tool, make tool call, verify detection.
|
|
810
|
-
"""
|
|
811
|
-
# Arrange
|
|
812
|
-
tool_configs = [
|
|
813
|
-
ToolBuildConfig(
|
|
814
|
-
name="control_tool",
|
|
815
|
-
configuration=MockToolConfig(),
|
|
816
|
-
display_name="Control Tool",
|
|
817
|
-
icon=ToolIcon.ANALYTICS,
|
|
818
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
819
|
-
is_exclusive=False,
|
|
820
|
-
is_enabled=True,
|
|
821
|
-
),
|
|
822
|
-
]
|
|
823
|
-
config = ToolManagerConfig(tools=tool_configs, max_tool_calls=10)
|
|
824
|
-
tool_manager = ToolManager(
|
|
825
|
-
logger=logger,
|
|
826
|
-
config=config,
|
|
827
|
-
event=base_event,
|
|
828
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
829
|
-
mcp_manager=mcp_manager,
|
|
830
|
-
a2a_manager=a2a_manager,
|
|
831
|
-
)
|
|
832
|
-
tool_calls = [
|
|
833
|
-
LanguageModelFunction(
|
|
834
|
-
id="call_1",
|
|
835
|
-
name="control_tool",
|
|
836
|
-
arguments={"query": "test"},
|
|
837
|
-
)
|
|
838
|
-
]
|
|
839
|
-
|
|
840
|
-
# Act
|
|
841
|
-
takes_control = tool_manager.does_a_tool_take_control(tool_calls)
|
|
842
|
-
|
|
843
|
-
# Assert
|
|
844
|
-
assert takes_control is True
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
@pytest.mark.ai
|
|
848
|
-
def test_tool_manager__does_a_tool_take_control__returns_false_for_normal_tool(
|
|
849
|
-
logger,
|
|
850
|
-
tool_manager_config,
|
|
851
|
-
base_event,
|
|
852
|
-
tool_progress_reporter,
|
|
853
|
-
mcp_manager,
|
|
854
|
-
a2a_manager,
|
|
855
|
-
) -> None:
|
|
856
|
-
"""
|
|
857
|
-
Purpose: Verify does_a_tool_take_control returns False for normal tools.
|
|
858
|
-
Why this matters: Only special tools should take control, not regular ones.
|
|
859
|
-
Setup summary: Create normal tool, make tool call, verify no control taken.
|
|
860
|
-
"""
|
|
861
|
-
# Arrange
|
|
862
|
-
tool_manager = ToolManager(
|
|
863
|
-
logger=logger,
|
|
864
|
-
config=tool_manager_config,
|
|
865
|
-
event=base_event,
|
|
866
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
867
|
-
mcp_manager=mcp_manager,
|
|
868
|
-
a2a_manager=a2a_manager,
|
|
869
|
-
)
|
|
870
|
-
tool_calls = [
|
|
871
|
-
LanguageModelFunction(
|
|
872
|
-
id="call_1",
|
|
873
|
-
name="mock_tool",
|
|
874
|
-
arguments={"query": "test"},
|
|
875
|
-
)
|
|
876
|
-
]
|
|
877
|
-
|
|
878
|
-
# Act
|
|
879
|
-
takes_control = tool_manager.does_a_tool_take_control(tool_calls)
|
|
880
|
-
|
|
881
|
-
# Assert
|
|
882
|
-
assert takes_control is False
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
@pytest.mark.ai
|
|
886
|
-
def test_tool_manager__filter_duplicate_tool_calls__removes_duplicates(
|
|
887
|
-
logger,
|
|
888
|
-
tool_manager_config,
|
|
889
|
-
base_event,
|
|
890
|
-
tool_progress_reporter,
|
|
891
|
-
mcp_manager,
|
|
892
|
-
a2a_manager,
|
|
893
|
-
) -> None:
|
|
894
|
-
"""
|
|
895
|
-
Purpose: Verify duplicate tool calls with same ID, name and arguments are filtered.
|
|
896
|
-
Why this matters: Prevents redundant tool execution and wasted resources.
|
|
897
|
-
Setup summary: Create duplicate tool calls with same values, filter them, verify only one remains.
|
|
898
|
-
"""
|
|
899
|
-
# Arrange
|
|
900
|
-
tool_manager = ToolManager(
|
|
901
|
-
logger=logger,
|
|
902
|
-
config=tool_manager_config,
|
|
903
|
-
event=base_event,
|
|
904
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
905
|
-
mcp_manager=mcp_manager,
|
|
906
|
-
a2a_manager=a2a_manager,
|
|
907
|
-
)
|
|
908
|
-
# Create truly duplicate calls - same ID makes them equal
|
|
909
|
-
call = LanguageModelFunction(
|
|
910
|
-
id="call_1",
|
|
911
|
-
name="mock_tool",
|
|
912
|
-
arguments={"query": "test"},
|
|
913
|
-
)
|
|
914
|
-
tool_calls = [call, call] # Same object reference
|
|
915
|
-
|
|
916
|
-
# Act
|
|
917
|
-
filtered = tool_manager.filter_duplicate_tool_calls(tool_calls)
|
|
918
|
-
|
|
919
|
-
# Assert
|
|
920
|
-
assert len(filtered) == 1
|
|
921
|
-
assert filtered[0].name == "mock_tool"
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
@pytest.mark.ai
|
|
925
|
-
def test_tool_manager__filter_duplicate_tool_calls__keeps_different_args(
|
|
926
|
-
logger,
|
|
927
|
-
tool_manager_config,
|
|
928
|
-
base_event,
|
|
929
|
-
tool_progress_reporter,
|
|
930
|
-
mcp_manager,
|
|
931
|
-
a2a_manager,
|
|
932
|
-
) -> None:
|
|
933
|
-
"""
|
|
934
|
-
Purpose: Verify tool calls with different arguments are not filtered.
|
|
935
|
-
Why this matters: Different arguments represent different operations.
|
|
936
|
-
Setup summary: Create tool calls with different args, verify both kept.
|
|
937
|
-
"""
|
|
938
|
-
# Arrange
|
|
939
|
-
tool_manager = ToolManager(
|
|
940
|
-
logger=logger,
|
|
941
|
-
config=tool_manager_config,
|
|
942
|
-
event=base_event,
|
|
943
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
944
|
-
mcp_manager=mcp_manager,
|
|
945
|
-
a2a_manager=a2a_manager,
|
|
946
|
-
)
|
|
947
|
-
tool_calls = [
|
|
948
|
-
LanguageModelFunction(
|
|
949
|
-
id="call_1",
|
|
950
|
-
name="mock_tool",
|
|
951
|
-
arguments={"query": "test1"},
|
|
952
|
-
),
|
|
953
|
-
LanguageModelFunction(
|
|
954
|
-
id="call_2",
|
|
955
|
-
name="mock_tool",
|
|
956
|
-
arguments={"query": "test2"},
|
|
957
|
-
),
|
|
958
|
-
]
|
|
959
|
-
|
|
960
|
-
# Act
|
|
961
|
-
filtered = tool_manager.filter_duplicate_tool_calls(tool_calls)
|
|
962
|
-
|
|
963
|
-
# Assert
|
|
964
|
-
assert len(filtered) == 2
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
@pytest.mark.ai
|
|
968
|
-
@pytest.mark.asyncio
|
|
969
|
-
async def test_tool_manager__execute_tool_call__returns_response(
|
|
970
|
-
logger,
|
|
971
|
-
tool_manager_config,
|
|
972
|
-
base_event,
|
|
973
|
-
tool_progress_reporter,
|
|
974
|
-
mcp_manager,
|
|
975
|
-
a2a_manager,
|
|
976
|
-
) -> None:
|
|
977
|
-
"""
|
|
978
|
-
Purpose: Verify execute_tool_call executes tool and returns response.
|
|
979
|
-
Why this matters: Core functionality for tool execution.
|
|
980
|
-
Setup summary: Create tool call, execute it, verify response structure.
|
|
981
|
-
"""
|
|
982
|
-
# Arrange
|
|
983
|
-
tool_manager = ToolManager(
|
|
984
|
-
logger=logger,
|
|
985
|
-
config=tool_manager_config,
|
|
986
|
-
event=base_event,
|
|
987
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
988
|
-
mcp_manager=mcp_manager,
|
|
989
|
-
a2a_manager=a2a_manager,
|
|
990
|
-
)
|
|
991
|
-
tool_call = LanguageModelFunction(
|
|
992
|
-
id="call_1",
|
|
993
|
-
name="mock_tool",
|
|
994
|
-
arguments={"query": "test"},
|
|
995
|
-
)
|
|
996
|
-
|
|
997
|
-
# Act
|
|
998
|
-
response = await tool_manager.execute_tool_call(tool_call)
|
|
999
|
-
|
|
1000
|
-
# Assert
|
|
1001
|
-
assert isinstance(response, ToolCallResponse)
|
|
1002
|
-
assert response.id == "call_1"
|
|
1003
|
-
assert response.name == "mock_tool"
|
|
1004
|
-
assert response.successful
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
@pytest.mark.ai
|
|
1008
|
-
@pytest.mark.asyncio
|
|
1009
|
-
async def test_tool_manager__execute_selected_tools__handles_missing_tool(
|
|
1010
|
-
logger,
|
|
1011
|
-
tool_manager_config,
|
|
1012
|
-
base_event,
|
|
1013
|
-
tool_progress_reporter,
|
|
1014
|
-
mcp_manager,
|
|
1015
|
-
a2a_manager,
|
|
1016
|
-
) -> None:
|
|
1017
|
-
"""
|
|
1018
|
-
Purpose: Verify execute_selected_tools handles non-existent tool gracefully.
|
|
1019
|
-
Why this matters: Prevents crashes from invalid tool calls.
|
|
1020
|
-
Setup summary: Call non-existent tool via execute_selected_tools, verify error response returned.
|
|
1021
|
-
"""
|
|
1022
|
-
# Arrange
|
|
1023
|
-
tool_manager = ToolManager(
|
|
1024
|
-
logger=logger,
|
|
1025
|
-
config=tool_manager_config,
|
|
1026
|
-
event=base_event,
|
|
1027
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1028
|
-
mcp_manager=mcp_manager,
|
|
1029
|
-
a2a_manager=a2a_manager,
|
|
1030
|
-
)
|
|
1031
|
-
tool_calls = [
|
|
1032
|
-
LanguageModelFunction(
|
|
1033
|
-
id="call_1",
|
|
1034
|
-
name="non_existent_tool",
|
|
1035
|
-
arguments={"query": "test"},
|
|
1036
|
-
)
|
|
1037
|
-
]
|
|
1038
|
-
|
|
1039
|
-
# Act
|
|
1040
|
-
responses = await tool_manager.execute_selected_tools(tool_calls)
|
|
1041
|
-
|
|
1042
|
-
# Assert
|
|
1043
|
-
assert len(responses) == 1
|
|
1044
|
-
response = responses[0]
|
|
1045
|
-
assert isinstance(response, ToolCallResponse)
|
|
1046
|
-
# Error handled by SafeTaskExecutor at the execute_selected_tools level
|
|
1047
|
-
assert "non_existent_tool" in response.name or response.error_message
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
@pytest.mark.ai
|
|
1051
|
-
@pytest.mark.asyncio
|
|
1052
|
-
async def test_tool_manager__execute_selected_tools__enforces_max_limit(
|
|
1053
|
-
logger,
|
|
1054
|
-
tool_manager_config,
|
|
1055
|
-
base_event,
|
|
1056
|
-
tool_progress_reporter,
|
|
1057
|
-
mcp_manager,
|
|
1058
|
-
a2a_manager,
|
|
1059
|
-
) -> None:
|
|
1060
|
-
"""
|
|
1061
|
-
Purpose: Verify execute_selected_tools limits calls to max_tool_calls.
|
|
1062
|
-
Why this matters: Prevents resource exhaustion from too many tool calls.
|
|
1063
|
-
Setup summary: Create 15 tool calls with max_tool_calls=10, verify only 10 executed.
|
|
1064
|
-
"""
|
|
1065
|
-
# Arrange
|
|
1066
|
-
tool_manager = ToolManager(
|
|
1067
|
-
logger=logger,
|
|
1068
|
-
config=tool_manager_config,
|
|
1069
|
-
event=base_event,
|
|
1070
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1071
|
-
mcp_manager=mcp_manager,
|
|
1072
|
-
a2a_manager=a2a_manager,
|
|
1073
|
-
)
|
|
1074
|
-
tool_calls = [
|
|
1075
|
-
LanguageModelFunction(
|
|
1076
|
-
id=f"call_{i}",
|
|
1077
|
-
name="mock_tool",
|
|
1078
|
-
arguments={"query": f"test{i}"},
|
|
1079
|
-
)
|
|
1080
|
-
for i in range(15)
|
|
1081
|
-
]
|
|
1082
|
-
|
|
1083
|
-
# Act
|
|
1084
|
-
responses = await tool_manager.execute_selected_tools(tool_calls)
|
|
1085
|
-
|
|
1086
|
-
# Assert
|
|
1087
|
-
assert len(responses) == 10
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
@pytest.mark.ai
|
|
1091
|
-
@pytest.mark.asyncio
|
|
1092
|
-
async def test_tool_manager__execute_selected_tools__executes_in_parallel(
|
|
1093
|
-
logger,
|
|
1094
|
-
tool_manager_config,
|
|
1095
|
-
base_event,
|
|
1096
|
-
tool_progress_reporter,
|
|
1097
|
-
mcp_manager,
|
|
1098
|
-
a2a_manager,
|
|
1099
|
-
) -> None:
|
|
1100
|
-
"""
|
|
1101
|
-
Purpose: Verify execute_selected_tools executes multiple tools concurrently.
|
|
1102
|
-
Why this matters: Parallel execution improves performance.
|
|
1103
|
-
Setup summary: Execute multiple tool calls, verify all responses returned.
|
|
1104
|
-
"""
|
|
1105
|
-
# Arrange
|
|
1106
|
-
tool_manager = ToolManager(
|
|
1107
|
-
logger=logger,
|
|
1108
|
-
config=tool_manager_config,
|
|
1109
|
-
event=base_event,
|
|
1110
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1111
|
-
mcp_manager=mcp_manager,
|
|
1112
|
-
a2a_manager=a2a_manager,
|
|
1113
|
-
)
|
|
1114
|
-
tool_calls = [
|
|
1115
|
-
LanguageModelFunction(
|
|
1116
|
-
id=f"call_{i}",
|
|
1117
|
-
name="mock_tool",
|
|
1118
|
-
arguments={"query": f"test{i}"},
|
|
1119
|
-
)
|
|
1120
|
-
for i in range(3)
|
|
1121
|
-
]
|
|
1122
|
-
|
|
1123
|
-
# Act
|
|
1124
|
-
responses = await tool_manager.execute_selected_tools(tool_calls)
|
|
1125
|
-
|
|
1126
|
-
# Assert
|
|
1127
|
-
assert len(responses) == 3
|
|
1128
|
-
for i, response in enumerate(responses):
|
|
1129
|
-
assert response.id == f"call_{i}"
|
|
1130
|
-
assert response.successful
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
@pytest.mark.ai
|
|
1134
|
-
@pytest.mark.asyncio
|
|
1135
|
-
async def test_tool_manager__execute_selected_tools__filters_duplicates(
|
|
1136
|
-
logger,
|
|
1137
|
-
tool_manager_config,
|
|
1138
|
-
base_event,
|
|
1139
|
-
tool_progress_reporter,
|
|
1140
|
-
mcp_manager,
|
|
1141
|
-
a2a_manager,
|
|
1142
|
-
) -> None:
|
|
1143
|
-
"""
|
|
1144
|
-
Purpose: Verify execute_selected_tools automatically filters duplicate calls.
|
|
1145
|
-
Why this matters: Combines duplicate filtering with execution for efficiency.
|
|
1146
|
-
Setup summary: Create duplicate tool calls (same object), execute, verify only unique executed.
|
|
1147
|
-
"""
|
|
1148
|
-
# Arrange
|
|
1149
|
-
tool_manager = ToolManager(
|
|
1150
|
-
logger=logger,
|
|
1151
|
-
config=tool_manager_config,
|
|
1152
|
-
event=base_event,
|
|
1153
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1154
|
-
mcp_manager=mcp_manager,
|
|
1155
|
-
a2a_manager=a2a_manager,
|
|
1156
|
-
)
|
|
1157
|
-
# Create truly duplicate calls - same object reference
|
|
1158
|
-
call = LanguageModelFunction(
|
|
1159
|
-
id="call_1",
|
|
1160
|
-
name="mock_tool",
|
|
1161
|
-
arguments={"query": "test"},
|
|
1162
|
-
)
|
|
1163
|
-
tool_calls = [call, call] # Same object reference makes them equal
|
|
1164
|
-
|
|
1165
|
-
# Act
|
|
1166
|
-
responses = await tool_manager.execute_selected_tools(tool_calls)
|
|
1167
|
-
|
|
1168
|
-
# Assert
|
|
1169
|
-
assert len(responses) == 1
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
@pytest.mark.ai
|
|
1173
|
-
@pytest.mark.asyncio
|
|
1174
|
-
async def test_tool_manager__execute_selected_tools__adds_debug_info(
|
|
1175
|
-
logger,
|
|
1176
|
-
tool_manager_config,
|
|
1177
|
-
base_event,
|
|
1178
|
-
tool_progress_reporter,
|
|
1179
|
-
mcp_manager,
|
|
1180
|
-
a2a_manager,
|
|
1181
|
-
) -> None:
|
|
1182
|
-
"""
|
|
1183
|
-
Purpose: Verify execute_selected_tools adds debug info to responses.
|
|
1184
|
-
Why this matters: Debug info helps with troubleshooting and metrics.
|
|
1185
|
-
Setup summary: Execute tool call, verify debug_info includes is_exclusive and is_forced.
|
|
1186
|
-
"""
|
|
1187
|
-
# Arrange
|
|
1188
|
-
base_event.payload.tool_choices = ["mock_tool"]
|
|
1189
|
-
tool_manager = ToolManager(
|
|
1190
|
-
logger=logger,
|
|
1191
|
-
config=tool_manager_config,
|
|
1192
|
-
event=base_event,
|
|
1193
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1194
|
-
mcp_manager=mcp_manager,
|
|
1195
|
-
a2a_manager=a2a_manager,
|
|
1196
|
-
)
|
|
1197
|
-
tool_calls = [
|
|
1198
|
-
LanguageModelFunction(
|
|
1199
|
-
id="call_1",
|
|
1200
|
-
name="mock_tool",
|
|
1201
|
-
arguments={"query": "test"},
|
|
1202
|
-
)
|
|
1203
|
-
]
|
|
1204
|
-
|
|
1205
|
-
# Act
|
|
1206
|
-
responses = await tool_manager.execute_selected_tools(tool_calls)
|
|
1207
|
-
|
|
1208
|
-
# Assert
|
|
1209
|
-
assert len(responses) == 1
|
|
1210
|
-
assert responses[0].debug_info is not None
|
|
1211
|
-
assert "is_exclusive" in responses[0].debug_info
|
|
1212
|
-
assert "is_forced" in responses[0].debug_info
|
|
1213
|
-
assert responses[0].debug_info["is_forced"] is True
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
@pytest.mark.ai
|
|
1217
|
-
def test_tool_manager__filter_tool_calls__filters_by_internal_type(
|
|
1218
|
-
logger,
|
|
1219
|
-
tool_manager_config,
|
|
1220
|
-
base_event,
|
|
1221
|
-
tool_progress_reporter,
|
|
1222
|
-
mcp_manager,
|
|
1223
|
-
a2a_manager,
|
|
1224
|
-
) -> None:
|
|
1225
|
-
"""
|
|
1226
|
-
Purpose: Verify filter_tool_calls filters by tool type (internal).
|
|
1227
|
-
Why this matters: Allows selective execution of tool types.
|
|
1228
|
-
Setup summary: Create tool calls, filter by internal type, verify correct filtering.
|
|
1229
|
-
"""
|
|
1230
|
-
# Arrange
|
|
1231
|
-
tool_manager = ToolManager(
|
|
1232
|
-
logger=logger,
|
|
1233
|
-
config=tool_manager_config,
|
|
1234
|
-
event=base_event,
|
|
1235
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1236
|
-
mcp_manager=mcp_manager,
|
|
1237
|
-
a2a_manager=a2a_manager,
|
|
1238
|
-
)
|
|
1239
|
-
tool_calls = [
|
|
1240
|
-
LanguageModelFunction(
|
|
1241
|
-
id="call_1",
|
|
1242
|
-
name="mock_tool",
|
|
1243
|
-
arguments={"query": "test"},
|
|
1244
|
-
)
|
|
1245
|
-
]
|
|
1246
|
-
|
|
1247
|
-
# Act
|
|
1248
|
-
filtered = tool_manager.filter_tool_calls(tool_calls, ["internal"])
|
|
1249
|
-
|
|
1250
|
-
# Assert
|
|
1251
|
-
assert len(filtered) == 1
|
|
1252
|
-
assert filtered[0].name == "mock_tool"
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
@pytest.mark.ai
|
|
1256
|
-
def test_tool_manager__filter_tool_calls__excludes_wrong_type(
|
|
1257
|
-
logger,
|
|
1258
|
-
tool_manager_config,
|
|
1259
|
-
base_event,
|
|
1260
|
-
tool_progress_reporter,
|
|
1261
|
-
mcp_manager,
|
|
1262
|
-
a2a_manager,
|
|
1263
|
-
) -> None:
|
|
1264
|
-
"""
|
|
1265
|
-
Purpose: Verify filter_tool_calls excludes tools of unspecified types.
|
|
1266
|
-
Why this matters: Type filtering must be precise.
|
|
1267
|
-
Setup summary: Filter by mcp type with only internal tools, verify empty result.
|
|
1268
|
-
"""
|
|
1269
|
-
# Arrange
|
|
1270
|
-
tool_manager = ToolManager(
|
|
1271
|
-
logger=logger,
|
|
1272
|
-
config=tool_manager_config,
|
|
1273
|
-
event=base_event,
|
|
1274
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1275
|
-
mcp_manager=mcp_manager,
|
|
1276
|
-
a2a_manager=a2a_manager,
|
|
1277
|
-
)
|
|
1278
|
-
tool_calls = [
|
|
1279
|
-
LanguageModelFunction(
|
|
1280
|
-
id="call_1",
|
|
1281
|
-
name="mock_tool",
|
|
1282
|
-
arguments={"query": "test"},
|
|
1283
|
-
)
|
|
1284
|
-
]
|
|
1285
|
-
|
|
1286
|
-
# Act
|
|
1287
|
-
filtered = tool_manager.filter_tool_calls(tool_calls, ["mcp"])
|
|
1288
|
-
|
|
1289
|
-
# Assert
|
|
1290
|
-
assert len(filtered) == 0
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
@pytest.mark.ai
|
|
1294
|
-
def test_tool_manager__add_forced_tool__adds_to_choices(
|
|
1295
|
-
logger,
|
|
1296
|
-
tool_manager_config,
|
|
1297
|
-
base_event,
|
|
1298
|
-
tool_progress_reporter,
|
|
1299
|
-
mcp_manager,
|
|
1300
|
-
a2a_manager,
|
|
1301
|
-
) -> None:
|
|
1302
|
-
"""
|
|
1303
|
-
Purpose: Verify add_forced_tool adds tool to choices list.
|
|
1304
|
-
Why this matters: Allows dynamic addition of forced tools.
|
|
1305
|
-
Setup summary: Add forced tool, verify it appears in tool_choices.
|
|
1306
|
-
"""
|
|
1307
|
-
# Arrange
|
|
1308
|
-
tool_manager = ToolManager(
|
|
1309
|
-
logger=logger,
|
|
1310
|
-
config=tool_manager_config,
|
|
1311
|
-
event=base_event,
|
|
1312
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1313
|
-
mcp_manager=mcp_manager,
|
|
1314
|
-
a2a_manager=a2a_manager,
|
|
1315
|
-
)
|
|
1316
|
-
|
|
1317
|
-
# Act
|
|
1318
|
-
tool_manager.add_forced_tool("mock_tool")
|
|
1319
|
-
|
|
1320
|
-
# Assert
|
|
1321
|
-
choices = tool_manager.get_tool_choices()
|
|
1322
|
-
assert "mock_tool" in choices
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
@pytest.mark.ai
|
|
1326
|
-
def test_tool_manager__add_forced_tool__raises_for_missing_tool(
|
|
1327
|
-
logger,
|
|
1328
|
-
tool_manager_config,
|
|
1329
|
-
base_event,
|
|
1330
|
-
tool_progress_reporter,
|
|
1331
|
-
mcp_manager,
|
|
1332
|
-
a2a_manager,
|
|
1333
|
-
) -> None:
|
|
1334
|
-
"""
|
|
1335
|
-
Purpose: Verify add_forced_tool raises ValueError for non-existent tools.
|
|
1336
|
-
Why this matters: Prevents adding invalid tools to forced list.
|
|
1337
|
-
Setup summary: Try to add non-existent tool, verify ValueError raised.
|
|
1338
|
-
"""
|
|
1339
|
-
# Arrange
|
|
1340
|
-
tool_manager = ToolManager(
|
|
1341
|
-
logger=logger,
|
|
1342
|
-
config=tool_manager_config,
|
|
1343
|
-
event=base_event,
|
|
1344
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1345
|
-
mcp_manager=mcp_manager,
|
|
1346
|
-
a2a_manager=a2a_manager,
|
|
1347
|
-
)
|
|
1348
|
-
|
|
1349
|
-
# Act & Assert
|
|
1350
|
-
with pytest.raises(ValueError) as exc_info:
|
|
1351
|
-
tool_manager.add_forced_tool("non_existent_tool")
|
|
1352
|
-
assert "not found" in str(exc_info.value).lower()
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
@pytest.mark.ai
|
|
1356
|
-
def test_tool_manager__log_loaded_tools__logs_tool_names(
|
|
1357
|
-
logger,
|
|
1358
|
-
tool_manager_config,
|
|
1359
|
-
base_event,
|
|
1360
|
-
tool_progress_reporter,
|
|
1361
|
-
mcp_manager,
|
|
1362
|
-
a2a_manager,
|
|
1363
|
-
caplog,
|
|
1364
|
-
) -> None:
|
|
1365
|
-
"""
|
|
1366
|
-
Purpose: Verify log_loaded_tools outputs tool names to log.
|
|
1367
|
-
Why this matters: Debugging and operational visibility.
|
|
1368
|
-
Setup summary: Create tool manager, call log_loaded_tools, verify log output.
|
|
1369
|
-
"""
|
|
1370
|
-
# Arrange
|
|
1371
|
-
tool_manager = ToolManager(
|
|
1372
|
-
logger=logger,
|
|
1373
|
-
config=tool_manager_config,
|
|
1374
|
-
event=base_event,
|
|
1375
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1376
|
-
mcp_manager=mcp_manager,
|
|
1377
|
-
a2a_manager=a2a_manager,
|
|
1378
|
-
)
|
|
1379
|
-
|
|
1380
|
-
# Act
|
|
1381
|
-
with caplog.at_level(logging.INFO):
|
|
1382
|
-
tool_manager.log_loaded_tools()
|
|
1383
|
-
|
|
1384
|
-
# Assert
|
|
1385
|
-
assert "mock_tool" in caplog.text
|
|
1386
|
-
assert "Loaded tools" in caplog.text
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
@pytest.mark.ai
|
|
1390
|
-
def test_convert_to_forced_tool__formats_completions_correctly() -> None:
|
|
1391
|
-
"""
|
|
1392
|
-
Purpose: Verify _convert_to_forced_tool formats completions API correctly.
|
|
1393
|
-
Why this matters: Correct API format needed for OpenAI completions.
|
|
1394
|
-
Setup summary: Call converter with completions mode, verify structure.
|
|
1395
|
-
"""
|
|
1396
|
-
# Arrange & Act
|
|
1397
|
-
result = _convert_to_forced_tool("test_tool", mode="completions")
|
|
1398
|
-
|
|
1399
|
-
# Assert
|
|
1400
|
-
assert result["type"] == "function"
|
|
1401
|
-
assert result["function"]["name"] == "test_tool"
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
@pytest.mark.ai
|
|
1405
|
-
def test_convert_to_forced_tool__formats_responses_correctly() -> None:
|
|
1406
|
-
"""
|
|
1407
|
-
Purpose: Verify _convert_to_forced_tool formats responses API correctly.
|
|
1408
|
-
Why this matters: Correct API format needed for OpenAI responses.
|
|
1409
|
-
Setup summary: Call converter with responses mode, verify structure.
|
|
1410
|
-
"""
|
|
1411
|
-
# Arrange & Act
|
|
1412
|
-
result = _convert_to_forced_tool("test_tool", mode="responses")
|
|
1413
|
-
|
|
1414
|
-
# Assert
|
|
1415
|
-
assert result["type"] == "function"
|
|
1416
|
-
assert result["name"] == "test_tool"
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
@pytest.mark.ai
|
|
1420
|
-
def test_convert_to_forced_tool__formats_builtin_tool_special() -> None:
|
|
1421
|
-
"""
|
|
1422
|
-
Purpose: Verify _convert_to_forced_tool formats built-in tools specially.
|
|
1423
|
-
Why this matters: Built-in tools have different syntax in responses API.
|
|
1424
|
-
Setup summary: Call converter with built-in tool name, verify special format.
|
|
1425
|
-
"""
|
|
1426
|
-
# Arrange & Act
|
|
1427
|
-
result = _convert_to_forced_tool("code_interpreter", mode="responses")
|
|
1428
|
-
|
|
1429
|
-
# Assert
|
|
1430
|
-
assert result["type"] == "code_interpreter"
|
|
1431
|
-
assert "name" not in result
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
@pytest.mark.ai
|
|
1435
|
-
def test_tool_manager__get_evaluation_check_list__returns_list(
|
|
1436
|
-
logger,
|
|
1437
|
-
tool_manager_config,
|
|
1438
|
-
base_event,
|
|
1439
|
-
tool_progress_reporter,
|
|
1440
|
-
mcp_manager,
|
|
1441
|
-
a2a_manager,
|
|
1442
|
-
) -> None:
|
|
1443
|
-
"""
|
|
1444
|
-
Purpose: Verify get_evaluation_check_list returns evaluation metrics list.
|
|
1445
|
-
Why this matters: Tracks which evaluations are needed after tool execution.
|
|
1446
|
-
Setup summary: Create tool manager, verify evaluation list is empty initially.
|
|
1447
|
-
"""
|
|
1448
|
-
# Arrange
|
|
1449
|
-
tool_manager = ToolManager(
|
|
1450
|
-
logger=logger,
|
|
1451
|
-
config=tool_manager_config,
|
|
1452
|
-
event=base_event,
|
|
1453
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1454
|
-
mcp_manager=mcp_manager,
|
|
1455
|
-
a2a_manager=a2a_manager,
|
|
1456
|
-
)
|
|
1457
|
-
|
|
1458
|
-
# Act
|
|
1459
|
-
check_list = tool_manager.get_evaluation_check_list()
|
|
1460
|
-
|
|
1461
|
-
# Assert
|
|
1462
|
-
assert isinstance(check_list, list)
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
@pytest.mark.ai
|
|
1466
|
-
def test_tool_manager__sub_agents__returns_sub_agent_list(
|
|
1467
|
-
logger,
|
|
1468
|
-
tool_manager_config,
|
|
1469
|
-
base_event,
|
|
1470
|
-
tool_progress_reporter,
|
|
1471
|
-
mcp_manager,
|
|
1472
|
-
a2a_manager,
|
|
1473
|
-
) -> None:
|
|
1474
|
-
"""
|
|
1475
|
-
Purpose: Verify sub_agents property returns list of sub-agent tools.
|
|
1476
|
-
Why this matters: Provides access to sub-agents for special handling.
|
|
1477
|
-
Setup summary: Create tool manager, verify sub_agents property exists and is list.
|
|
1478
|
-
"""
|
|
1479
|
-
# Arrange
|
|
1480
|
-
tool_manager = ToolManager(
|
|
1481
|
-
logger=logger,
|
|
1482
|
-
config=tool_manager_config,
|
|
1483
|
-
event=base_event,
|
|
1484
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1485
|
-
mcp_manager=mcp_manager,
|
|
1486
|
-
a2a_manager=a2a_manager,
|
|
1487
|
-
)
|
|
1488
|
-
|
|
1489
|
-
# Act
|
|
1490
|
-
sub_agents = tool_manager.sub_agents
|
|
1491
|
-
|
|
1492
|
-
# Assert
|
|
1493
|
-
assert isinstance(sub_agents, list)
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
@pytest.mark.ai
|
|
1497
|
-
@pytest.mark.asyncio
|
|
1498
|
-
async def test_tool_manager__execute_selected_tools__handles_exceptions(
|
|
1499
|
-
logger,
|
|
1500
|
-
base_event,
|
|
1501
|
-
tool_progress_reporter,
|
|
1502
|
-
mcp_manager,
|
|
1503
|
-
a2a_manager,
|
|
1504
|
-
mocker,
|
|
1505
|
-
) -> None:
|
|
1506
|
-
"""
|
|
1507
|
-
Purpose: Verify execute_selected_tools handles tool execution exceptions gracefully.
|
|
1508
|
-
Why this matters: Tool failures shouldn't crash the system.
|
|
1509
|
-
Setup summary: Mock tool to raise exception, execute via execute_selected_tools, verify error response.
|
|
1510
|
-
"""
|
|
1511
|
-
# Arrange
|
|
1512
|
-
tool_configs = [
|
|
1513
|
-
ToolBuildConfig(
|
|
1514
|
-
name="mock_tool",
|
|
1515
|
-
configuration=MockToolConfig(),
|
|
1516
|
-
display_name="Mock Tool",
|
|
1517
|
-
icon=ToolIcon.BOOK,
|
|
1518
|
-
selection_policy=ToolSelectionPolicy.BY_USER,
|
|
1519
|
-
is_exclusive=False,
|
|
1520
|
-
is_enabled=True,
|
|
1521
|
-
),
|
|
1522
|
-
]
|
|
1523
|
-
config = ToolManagerConfig(tools=tool_configs, max_tool_calls=10)
|
|
1524
|
-
tool_manager = ToolManager(
|
|
1525
|
-
logger=logger,
|
|
1526
|
-
config=config,
|
|
1527
|
-
event=base_event,
|
|
1528
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1529
|
-
mcp_manager=mcp_manager,
|
|
1530
|
-
a2a_manager=a2a_manager,
|
|
1531
|
-
)
|
|
1532
|
-
|
|
1533
|
-
# Mock the tool's run method to raise an exception
|
|
1534
|
-
mock_tool = tool_manager.get_tool_by_name("mock_tool")
|
|
1535
|
-
mocker.patch.object(mock_tool, "run", side_effect=RuntimeError("Tool error"))
|
|
1536
|
-
|
|
1537
|
-
tool_calls = [
|
|
1538
|
-
LanguageModelFunction(
|
|
1539
|
-
id="call_1",
|
|
1540
|
-
name="mock_tool",
|
|
1541
|
-
arguments={"query": "test"},
|
|
1542
|
-
)
|
|
1543
|
-
]
|
|
1544
|
-
|
|
1545
|
-
# Act
|
|
1546
|
-
responses = await tool_manager.execute_selected_tools(tool_calls)
|
|
1547
|
-
|
|
1548
|
-
# Assert
|
|
1549
|
-
assert len(responses) == 1
|
|
1550
|
-
response = responses[0]
|
|
1551
|
-
assert isinstance(response, ToolCallResponse)
|
|
1552
|
-
assert not response.successful
|
|
1553
|
-
assert "Tool error" in response.error_message
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
@pytest.mark.ai
|
|
1557
|
-
def test_responses_api_tool_manager__get_tool_by_name__can_return_builtin(
|
|
1558
|
-
logger,
|
|
1559
|
-
base_event,
|
|
1560
|
-
tool_progress_reporter,
|
|
1561
|
-
mcp_manager,
|
|
1562
|
-
a2a_manager,
|
|
1563
|
-
mocker,
|
|
1564
|
-
) -> None:
|
|
1565
|
-
"""
|
|
1566
|
-
Purpose: Verify ResponsesApiToolManager can retrieve built-in tools.
|
|
1567
|
-
Why this matters: Responses API supports OpenAI built-in tools.
|
|
1568
|
-
Setup summary: Create mock built-in tool, retrieve it, verify type.
|
|
1569
|
-
"""
|
|
1570
|
-
# Arrange
|
|
1571
|
-
mock_builtin_tool = mocker.Mock(spec=OpenAIBuiltInTool)
|
|
1572
|
-
mock_builtin_tool.name = "code_interpreter"
|
|
1573
|
-
mock_builtin_tool.is_enabled.return_value = True
|
|
1574
|
-
mock_builtin_tool.is_exclusive.return_value = False
|
|
1575
|
-
|
|
1576
|
-
mock_builtin_manager = mocker.Mock(spec=OpenAIBuiltInToolManager)
|
|
1577
|
-
mock_builtin_manager.get_all_openai_builtin_tools.return_value = [mock_builtin_tool]
|
|
1578
|
-
|
|
1579
|
-
tool_manager = ResponsesApiToolManager(
|
|
1580
|
-
logger=logger,
|
|
1581
|
-
config=ToolManagerConfig(tools=[], max_tool_calls=10),
|
|
1582
|
-
event=base_event,
|
|
1583
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1584
|
-
mcp_manager=mcp_manager,
|
|
1585
|
-
a2a_manager=a2a_manager,
|
|
1586
|
-
builtin_tool_manager=mock_builtin_manager,
|
|
1587
|
-
)
|
|
1588
|
-
|
|
1589
|
-
# Act
|
|
1590
|
-
tool = tool_manager.get_tool_by_name("code_interpreter")
|
|
1591
|
-
|
|
1592
|
-
# Assert
|
|
1593
|
-
assert tool is not None
|
|
1594
|
-
assert tool.name == "code_interpreter"
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
@pytest.mark.ai
|
|
1598
|
-
def test_responses_api_tool_manager__filter_tool_calls__filters_builtin(
|
|
1599
|
-
logger,
|
|
1600
|
-
base_event,
|
|
1601
|
-
tool_progress_reporter,
|
|
1602
|
-
mcp_manager,
|
|
1603
|
-
a2a_manager,
|
|
1604
|
-
mocker,
|
|
1605
|
-
) -> None:
|
|
1606
|
-
"""
|
|
1607
|
-
Purpose: Verify ResponsesApiToolManager filters built-in tool calls.
|
|
1608
|
-
Why this matters: Type filtering must include built-in tools.
|
|
1609
|
-
Setup summary: Create built-in tool call, filter by openai_builtin type, verify included.
|
|
1610
|
-
"""
|
|
1611
|
-
# Arrange
|
|
1612
|
-
mock_builtin_tool = mocker.Mock(spec=OpenAIBuiltInTool)
|
|
1613
|
-
mock_builtin_tool.name = "code_interpreter"
|
|
1614
|
-
mock_builtin_tool.is_enabled.return_value = True
|
|
1615
|
-
mock_builtin_tool.is_exclusive.return_value = False
|
|
1616
|
-
|
|
1617
|
-
mock_builtin_manager = mocker.Mock(spec=OpenAIBuiltInToolManager)
|
|
1618
|
-
mock_builtin_manager.get_all_openai_builtin_tools.return_value = [mock_builtin_tool]
|
|
1619
|
-
|
|
1620
|
-
tool_manager = ResponsesApiToolManager(
|
|
1621
|
-
logger=logger,
|
|
1622
|
-
config=ToolManagerConfig(tools=[], max_tool_calls=10),
|
|
1623
|
-
event=base_event,
|
|
1624
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1625
|
-
mcp_manager=mcp_manager,
|
|
1626
|
-
a2a_manager=a2a_manager,
|
|
1627
|
-
builtin_tool_manager=mock_builtin_manager,
|
|
1628
|
-
)
|
|
1629
|
-
|
|
1630
|
-
tool_calls = [
|
|
1631
|
-
LanguageModelFunction(
|
|
1632
|
-
id="call_1",
|
|
1633
|
-
name="code_interpreter",
|
|
1634
|
-
arguments={},
|
|
1635
|
-
)
|
|
1636
|
-
]
|
|
1637
|
-
|
|
1638
|
-
# Act
|
|
1639
|
-
filtered = tool_manager.filter_tool_calls(tool_calls, ["openai_builtin"])
|
|
1640
|
-
|
|
1641
|
-
# Assert
|
|
1642
|
-
assert len(filtered) == 1
|
|
1643
|
-
assert filtered[0].name == "code_interpreter"
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
@pytest.mark.ai
|
|
1647
|
-
def test_responses_api_tool_manager__get_forced_tools__formats_builtin_special(
|
|
1648
|
-
logger,
|
|
1649
|
-
base_event,
|
|
1650
|
-
tool_progress_reporter,
|
|
1651
|
-
mcp_manager,
|
|
1652
|
-
a2a_manager,
|
|
1653
|
-
mocker,
|
|
1654
|
-
) -> None:
|
|
1655
|
-
"""
|
|
1656
|
-
Purpose: Verify ResponsesApiToolManager formats built-in forced tools correctly.
|
|
1657
|
-
Why this matters: Built-in tools require special format in responses API.
|
|
1658
|
-
Setup summary: Force built-in tool, get forced tools, verify format.
|
|
1659
|
-
"""
|
|
1660
|
-
# Arrange
|
|
1661
|
-
mock_builtin_tool = mocker.Mock(spec=OpenAIBuiltInTool)
|
|
1662
|
-
mock_builtin_tool.name = "code_interpreter"
|
|
1663
|
-
mock_builtin_tool.is_enabled.return_value = True
|
|
1664
|
-
mock_builtin_tool.is_exclusive.return_value = False
|
|
1665
|
-
|
|
1666
|
-
mock_builtin_manager = mocker.Mock(spec=OpenAIBuiltInToolManager)
|
|
1667
|
-
mock_builtin_manager.get_all_openai_builtin_tools.return_value = [mock_builtin_tool]
|
|
1668
|
-
|
|
1669
|
-
base_event.payload.tool_choices = ["code_interpreter"]
|
|
1670
|
-
|
|
1671
|
-
tool_manager = ResponsesApiToolManager(
|
|
1672
|
-
logger=logger,
|
|
1673
|
-
config=ToolManagerConfig(tools=[], max_tool_calls=10),
|
|
1674
|
-
event=base_event,
|
|
1675
|
-
tool_progress_reporter=tool_progress_reporter,
|
|
1676
|
-
mcp_manager=mcp_manager,
|
|
1677
|
-
a2a_manager=a2a_manager,
|
|
1678
|
-
builtin_tool_manager=mock_builtin_manager,
|
|
1679
|
-
)
|
|
1680
|
-
|
|
1681
|
-
# Act
|
|
1682
|
-
forced_tools = tool_manager.get_forced_tools()
|
|
1683
|
-
|
|
1684
|
-
# Assert
|
|
1685
|
-
assert len(forced_tools) == 1
|
|
1686
|
-
assert forced_tools[0]["type"] == "code_interpreter"
|