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.
@@ -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"