unique_toolkit 1.8.1__py3-none-any.whl → 1.23.0__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.

Potentially problematic release.


This version of unique_toolkit might be problematic. Click here for more details.

Files changed (105) hide show
  1. unique_toolkit/__init__.py +20 -0
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +121 -28
  3. unique_toolkit/_common/chunk_relevancy_sorter/config.py +3 -3
  4. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +2 -5
  5. unique_toolkit/_common/default_language_model.py +9 -3
  6. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  7. unique_toolkit/_common/docx_generator/config.py +12 -0
  8. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  9. unique_toolkit/_common/docx_generator/service.py +252 -0
  10. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  11. unique_toolkit/_common/endpoint_builder.py +138 -117
  12. unique_toolkit/_common/endpoint_requestor.py +240 -14
  13. unique_toolkit/_common/exception.py +20 -0
  14. unique_toolkit/_common/feature_flags/schema.py +1 -5
  15. unique_toolkit/_common/referencing.py +53 -0
  16. unique_toolkit/_common/string_utilities.py +52 -1
  17. unique_toolkit/_common/tests/test_referencing.py +521 -0
  18. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  19. unique_toolkit/_common/utils/files.py +43 -0
  20. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +16 -6
  21. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  22. unique_toolkit/agentic/evaluation/config.py +3 -2
  23. unique_toolkit/agentic/evaluation/context_relevancy/service.py +2 -2
  24. unique_toolkit/agentic/evaluation/evaluation_manager.py +9 -5
  25. unique_toolkit/agentic/evaluation/hallucination/constants.py +1 -1
  26. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +26 -3
  27. unique_toolkit/agentic/history_manager/history_manager.py +14 -11
  28. unique_toolkit/agentic/history_manager/loop_token_reducer.py +3 -4
  29. unique_toolkit/agentic/history_manager/utils.py +10 -87
  30. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +107 -16
  31. unique_toolkit/agentic/reference_manager/reference_manager.py +1 -1
  32. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  33. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  34. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  35. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  36. unique_toolkit/agentic/tools/a2a/__init__.py +18 -2
  37. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +2 -0
  38. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +3 -3
  39. unique_toolkit/agentic/tools/a2a/evaluation/config.py +1 -1
  40. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +143 -91
  41. unique_toolkit/agentic/tools/a2a/manager.py +7 -1
  42. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +11 -3
  43. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  44. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  45. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +21 -0
  46. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  47. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  48. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  49. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  50. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  51. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  52. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  53. unique_toolkit/agentic/tools/a2a/tool/config.py +15 -5
  54. unique_toolkit/agentic/tools/a2a/tool/service.py +69 -36
  55. unique_toolkit/agentic/tools/config.py +16 -2
  56. unique_toolkit/agentic/tools/factory.py +4 -0
  57. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +7 -35
  58. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  59. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  60. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  61. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  62. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  63. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  64. unique_toolkit/agentic/tools/test/test_mcp_manager.py +95 -7
  65. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +240 -0
  66. unique_toolkit/agentic/tools/tool.py +0 -11
  67. unique_toolkit/agentic/tools/tool_manager.py +337 -122
  68. unique_toolkit/agentic/tools/tool_progress_reporter.py +81 -15
  69. unique_toolkit/agentic/tools/utils/__init__.py +18 -0
  70. unique_toolkit/agentic/tools/utils/execution/execution.py +8 -4
  71. unique_toolkit/agentic/tools/utils/source_handling/schema.py +1 -1
  72. unique_toolkit/chat/__init__.py +8 -1
  73. unique_toolkit/chat/deprecated/service.py +232 -0
  74. unique_toolkit/chat/functions.py +54 -40
  75. unique_toolkit/chat/rendering.py +34 -0
  76. unique_toolkit/chat/responses_api.py +461 -0
  77. unique_toolkit/chat/schemas.py +1 -1
  78. unique_toolkit/chat/service.py +96 -1569
  79. unique_toolkit/content/functions.py +116 -1
  80. unique_toolkit/content/schemas.py +59 -0
  81. unique_toolkit/content/service.py +5 -37
  82. unique_toolkit/content/smart_rules.py +301 -0
  83. unique_toolkit/framework_utilities/langchain/client.py +27 -3
  84. unique_toolkit/framework_utilities/openai/client.py +12 -1
  85. unique_toolkit/framework_utilities/openai/message_builder.py +85 -1
  86. unique_toolkit/language_model/default_language_model.py +3 -0
  87. unique_toolkit/language_model/functions.py +25 -9
  88. unique_toolkit/language_model/infos.py +72 -4
  89. unique_toolkit/language_model/schemas.py +246 -40
  90. unique_toolkit/protocols/support.py +91 -9
  91. unique_toolkit/services/__init__.py +7 -0
  92. unique_toolkit/services/chat_service.py +1630 -0
  93. unique_toolkit/services/knowledge_base.py +861 -0
  94. unique_toolkit/smart_rules/compile.py +56 -301
  95. unique_toolkit/test_utilities/events.py +197 -0
  96. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +173 -3
  97. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/RECORD +99 -67
  98. unique_toolkit/agentic/tools/a2a/postprocessing/_display.py +0 -122
  99. unique_toolkit/agentic/tools/a2a/postprocessing/_utils.py +0 -19
  100. unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py +0 -230
  101. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_consolidate_references.py +0 -665
  102. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +0 -391
  103. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_postprocessor_reference_functions.py +0 -256
  104. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  105. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,230 @@
1
+ import logging
2
+ from typing import override
3
+
4
+ from openai import AsyncOpenAI, BaseModel, NotFoundError
5
+ from openai.types.responses.tool_param import CodeInterpreter
6
+
7
+ from unique_toolkit import ContentService, ShortTermMemoryService
8
+ from unique_toolkit.agentic.short_term_memory_manager.persistent_short_term_memory_manager import (
9
+ PersistentShortMemoryManager,
10
+ )
11
+ from unique_toolkit.agentic.tools.openai_builtin.base import (
12
+ OpenAIBuiltInTool,
13
+ OpenAIBuiltInToolName,
14
+ )
15
+ from unique_toolkit.agentic.tools.openai_builtin.code_interpreter.config import (
16
+ OpenAICodeInterpreterConfig,
17
+ )
18
+ from unique_toolkit.agentic.tools.schemas import ToolPrompts
19
+ from unique_toolkit.content.schemas import (
20
+ Content,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ _SHORT_TERM_MEMORY_NAME = "container_code_execution"
27
+
28
+
29
+ class CodeExecutionShortTermMemorySchema(BaseModel):
30
+ container_id: str | None = None
31
+ file_ids: dict[str, str] = {} # Mapping of unique file id to openai file id
32
+
33
+
34
+ CodeExecutionMemoryManager = PersistentShortMemoryManager[
35
+ CodeExecutionShortTermMemorySchema
36
+ ]
37
+
38
+
39
+ def _get_container_code_execution_short_term_memory_manager(
40
+ company_id: str, user_id: str, chat_id: str
41
+ ) -> CodeExecutionMemoryManager:
42
+ short_term_memory_service = ShortTermMemoryService(
43
+ company_id=company_id,
44
+ user_id=user_id,
45
+ chat_id=chat_id,
46
+ message_id=None,
47
+ )
48
+ short_term_memory_manager = PersistentShortMemoryManager(
49
+ short_term_memory_service=short_term_memory_service,
50
+ short_term_memory_schema=CodeExecutionShortTermMemorySchema,
51
+ short_term_memory_name=_SHORT_TERM_MEMORY_NAME,
52
+ )
53
+ return short_term_memory_manager
54
+
55
+
56
+ async def _create_container_if_not_exists(
57
+ client: AsyncOpenAI,
58
+ chat_id: str,
59
+ user_id: str,
60
+ company_id: str,
61
+ expires_after_minutes: int,
62
+ memory: CodeExecutionShortTermMemorySchema | None = None,
63
+ ) -> CodeExecutionShortTermMemorySchema:
64
+ if memory is not None:
65
+ logger.info("Container found in short term memory")
66
+ else:
67
+ logger.info("No Container in short term memory, creating a new container")
68
+ memory = CodeExecutionShortTermMemorySchema()
69
+
70
+ container_id = memory.container_id
71
+
72
+ if container_id is not None:
73
+ try:
74
+ container = await client.containers.retrieve(container_id)
75
+ if container.status not in ["active", "running"]:
76
+ logger.info(
77
+ "Container has status `%s`, recreating a new one", container.status
78
+ )
79
+ container_id = None
80
+ except NotFoundError:
81
+ container_id = None
82
+
83
+ if container_id is None:
84
+ memory = CodeExecutionShortTermMemorySchema()
85
+
86
+ container = await client.containers.create(
87
+ name=f"code_execution_{company_id}_{user_id}_{chat_id}",
88
+ expires_after={
89
+ "anchor": "last_active_at",
90
+ "minutes": expires_after_minutes,
91
+ },
92
+ )
93
+
94
+ memory.container_id = container.id
95
+
96
+ return memory
97
+
98
+
99
+ async def _upload_files_to_container(
100
+ client: AsyncOpenAI,
101
+ uploaded_files: list[Content],
102
+ memory: CodeExecutionShortTermMemorySchema,
103
+ content_service: ContentService,
104
+ chat_id: str,
105
+ ) -> CodeExecutionShortTermMemorySchema:
106
+ container_id = memory.container_id
107
+
108
+ assert container_id is not None
109
+
110
+ memory = memory.model_copy(deep=True)
111
+
112
+ for file in uploaded_files:
113
+ upload = True
114
+ if file.id in memory.file_ids:
115
+ try:
116
+ _ = await client.containers.files.retrieve(
117
+ container_id=container_id, file_id=memory.file_ids[file.id]
118
+ )
119
+ logger.info("File with id %s already uploaded to container", file.id)
120
+ upload = False
121
+ except NotFoundError:
122
+ upload = True
123
+
124
+ if upload:
125
+ logger.info(
126
+ "Uploding file %s to container %s", file.id, memory.container_id
127
+ )
128
+ file_content = content_service.download_content_to_bytes(
129
+ content_id=file.id, chat_id=chat_id
130
+ ) # TODO: Use async version when available
131
+
132
+ openai_file = await client.containers.files.create(
133
+ container_id=container_id,
134
+ file=(file.key, file_content),
135
+ )
136
+ memory.file_ids[file.id] = openai_file.id
137
+
138
+ return memory
139
+
140
+
141
+ class OpenAICodeInterpreterTool(OpenAIBuiltInTool[CodeInterpreter]):
142
+ DISPLAY_NAME = "Code Interpreter"
143
+
144
+ def __init__(
145
+ self,
146
+ config: OpenAICodeInterpreterConfig,
147
+ container_id: str | None,
148
+ ):
149
+ self._config = config
150
+
151
+ if not config.use_auto_container and container_id is None:
152
+ raise ValueError("`container_id` required when not using `auto` containers")
153
+
154
+ self._container_id = container_id
155
+
156
+ @property
157
+ @override
158
+ def name(self) -> OpenAIBuiltInToolName:
159
+ return OpenAIBuiltInToolName.CODE_INTERPRETER
160
+
161
+ @override
162
+ def tool_description(self) -> CodeInterpreter:
163
+ if self._config.use_auto_container:
164
+ return {"container": {"type": "auto"}, "type": "code_interpreter"}
165
+
166
+ return {
167
+ "container": self._container_id, # type: ignore
168
+ "type": "code_interpreter",
169
+ }
170
+
171
+ @classmethod
172
+ async def build_tool(
173
+ cls,
174
+ config: OpenAICodeInterpreterConfig,
175
+ uploaded_files: list[Content],
176
+ client: AsyncOpenAI,
177
+ content_service: ContentService,
178
+ company_id: str,
179
+ user_id: str,
180
+ chat_id: str,
181
+ ) -> "OpenAICodeInterpreterTool":
182
+ if config.use_auto_container:
183
+ logger.info("Using `auto` container setting")
184
+ return cls(config=config, container_id=None)
185
+
186
+ memory_manager = _get_container_code_execution_short_term_memory_manager(
187
+ company_id=company_id,
188
+ user_id=user_id,
189
+ chat_id=chat_id,
190
+ )
191
+
192
+ memory = await memory_manager.load_async()
193
+
194
+ memory = await _create_container_if_not_exists(
195
+ client=client,
196
+ memory=memory,
197
+ chat_id=chat_id,
198
+ user_id=user_id,
199
+ company_id=company_id,
200
+ expires_after_minutes=config.expires_after_minutes,
201
+ )
202
+
203
+ memory = await _upload_files_to_container(
204
+ client=client,
205
+ uploaded_files=uploaded_files,
206
+ content_service=content_service,
207
+ chat_id=chat_id,
208
+ memory=memory,
209
+ )
210
+
211
+ await memory_manager.save_async(memory)
212
+
213
+ assert memory.container_id is not None
214
+
215
+ return OpenAICodeInterpreterTool(
216
+ config=config, container_id=memory.container_id
217
+ )
218
+
219
+ @override
220
+ def get_tool_prompts(self) -> ToolPrompts:
221
+ return ToolPrompts(
222
+ name="the python tool", # https://platform.openai.com/docs/guides/tools-code-interpreter
223
+ display_name=self.DISPLAY_NAME,
224
+ tool_description=self._config.tool_description,
225
+ tool_system_prompt=self._config.tool_description_for_system_prompt,
226
+ tool_format_information_for_system_prompt=self._config.tool_format_information_for_system_prompt,
227
+ tool_user_prompt=self._config.tool_description_for_user_prompt,
228
+ tool_format_information_for_user_prompt=self._config.tool_format_information_for_user_prompt,
229
+ input_model={},
230
+ )
@@ -0,0 +1,62 @@
1
+ from openai import AsyncOpenAI
2
+
3
+ from unique_toolkit.agentic.tools.config import ToolBuildConfig
4
+ from unique_toolkit.agentic.tools.openai_builtin.base import (
5
+ OpenAIBuiltInTool,
6
+ OpenAIBuiltInToolName,
7
+ )
8
+ from unique_toolkit.agentic.tools.openai_builtin.code_interpreter import (
9
+ OpenAICodeInterpreterConfig,
10
+ OpenAICodeInterpreterTool,
11
+ )
12
+ from unique_toolkit.content.schemas import Content
13
+ from unique_toolkit.content.service import ContentService
14
+
15
+
16
+ class OpenAIBuiltInToolManager:
17
+ def __init__(
18
+ self,
19
+ uploaded_files: list[Content],
20
+ content_service: ContentService,
21
+ user_id: str,
22
+ company_id: str,
23
+ chat_id: str,
24
+ client: AsyncOpenAI,
25
+ ):
26
+ self._uploaded_files = uploaded_files
27
+ self._content_service = content_service
28
+ self._user_id = user_id
29
+ self._company_id = company_id
30
+ self._client = client
31
+ self._chat_id = chat_id
32
+
33
+ async def _build_tool(self, tool_config: ToolBuildConfig) -> OpenAIBuiltInTool:
34
+ if tool_config.name == OpenAIBuiltInToolName.CODE_INTERPRETER:
35
+ assert isinstance(tool_config.configuration, OpenAICodeInterpreterConfig)
36
+ tool = await OpenAICodeInterpreterTool.build_tool(
37
+ config=tool_config.configuration,
38
+ uploaded_files=self._uploaded_files,
39
+ user_id=self._user_id,
40
+ company_id=self._company_id,
41
+ chat_id=self._chat_id,
42
+ content_service=self._content_service,
43
+ client=self._client,
44
+ )
45
+ return tool
46
+ else:
47
+ raise ValueError(f"Unknown built-in tool name: {tool_config.name}")
48
+
49
+ async def get_all_openai_builtin_tools(
50
+ self, tool_configs: list[ToolBuildConfig]
51
+ ) -> tuple[list[ToolBuildConfig], list[OpenAIBuiltInTool]]:
52
+ openai_builtin_tools = []
53
+ filtered_tool_configs = []
54
+
55
+ for tool_config in tool_configs:
56
+ if tool_config.name not in OpenAIBuiltInToolName:
57
+ filtered_tool_configs.append(tool_config)
58
+ continue
59
+
60
+ openai_builtin_tools.append(await self._build_tool(tool_config))
61
+
62
+ return filtered_tool_configs, openai_builtin_tools
@@ -5,6 +5,7 @@ import pytest
5
5
  from pydantic import BaseModel
6
6
 
7
7
  from tests.test_obj_factory import get_event_obj
8
+ from unique_toolkit.agentic.tools.a2a import SubAgentResponseWatcher
8
9
  from unique_toolkit.agentic.tools.a2a.manager import A2AManager
9
10
  from unique_toolkit.agentic.tools.config import (
10
11
  ToolBuildConfig,
@@ -13,6 +14,8 @@ from unique_toolkit.agentic.tools.config import (
13
14
  )
14
15
  from unique_toolkit.agentic.tools.factory import ToolFactory
15
16
  from unique_toolkit.agentic.tools.mcp.manager import MCPManager
17
+ from unique_toolkit.agentic.tools.mcp.models import MCPToolConfig
18
+ from unique_toolkit.agentic.tools.mcp.tool_wrapper import MCPToolWrapper
16
19
  from unique_toolkit.agentic.tools.schemas import BaseToolConfig
17
20
  from unique_toolkit.agentic.tools.tool import Tool
18
21
  from unique_toolkit.agentic.tools.tool_manager import ToolManager, ToolManagerConfig
@@ -48,11 +51,6 @@ class MockInternalSearchTool(Tool[BaseToolConfig]):
48
51
  def tool_format_information_for_system_prompt(self) -> str:
49
52
  return "Use this tool to search for content"
50
53
 
51
- def get_tool_call_result_for_loop_history(self, tool_response):
52
- from unique_toolkit.language_model.schemas import LanguageModelMessage
53
-
54
- return LanguageModelMessage(role="tool", content="Mock search result")
55
-
56
54
  def evaluation_check_list(self):
57
55
  return []
58
56
 
@@ -114,11 +112,22 @@ class TestMCPManager:
114
112
  name="mcp_test_tool",
115
113
  description="Test MCP tool",
116
114
  input_schema={
115
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
117
116
  "type": "object",
118
117
  "properties": {
119
- "query": {"type": "string", "description": "Search query"}
118
+ "folder": {
119
+ "description": "Mail folder to read mails from",
120
+ "default": "inbox",
121
+ "type": "string",
122
+ },
123
+ "limit": {
124
+ "description": "Number of emails to retrieve",
125
+ "default": 10,
126
+ "type": "number",
127
+ "minimum": 1,
128
+ "maximum": 50,
129
+ },
120
130
  },
121
- "required": ["query"],
122
131
  },
123
132
  output_schema=None,
124
133
  annotations=None,
@@ -173,6 +182,7 @@ class TestMCPManager:
173
182
  return A2AManager(
174
183
  logger=self.logger,
175
184
  tool_progress_reporter=tool_progress_reporter,
185
+ response_watcher=SubAgentResponseWatcher(),
176
186
  )
177
187
 
178
188
  @pytest.fixture
@@ -277,6 +287,7 @@ class TestMCPManager:
277
287
  a2a_manager = A2AManager(
278
288
  logger=self.logger,
279
289
  tool_progress_reporter=tool_progress_reporter,
290
+ response_watcher=SubAgentResponseWatcher(),
280
291
  )
281
292
 
282
293
  tool_manager = ToolManager(
@@ -313,6 +324,7 @@ class TestMCPManager:
313
324
  a2a_manager = A2AManager(
314
325
  logger=self.logger,
315
326
  tool_progress_reporter=tool_progress_reporter,
327
+ response_watcher=SubAgentResponseWatcher(),
316
328
  )
317
329
 
318
330
  tool_manager = ToolManager(
@@ -349,6 +361,7 @@ class TestMCPManager:
349
361
  a2a_manager = A2AManager(
350
362
  logger=self.logger,
351
363
  tool_progress_reporter=tool_progress_reporter,
364
+ response_watcher=SubAgentResponseWatcher(),
352
365
  )
353
366
 
354
367
  tool_manager = ToolManager(
@@ -388,6 +401,7 @@ class TestMCPManager:
388
401
  a2a_manager = A2AManager(
389
402
  logger=self.logger,
390
403
  tool_progress_reporter=tool_progress_reporter,
404
+ response_watcher=SubAgentResponseWatcher(),
391
405
  )
392
406
 
393
407
  tool_manager = ToolManager(
@@ -429,6 +443,7 @@ class TestMCPManager:
429
443
  a2a_manager = A2AManager(
430
444
  logger=self.logger,
431
445
  tool_progress_reporter=tool_progress_reporter,
446
+ response_watcher=SubAgentResponseWatcher(),
432
447
  )
433
448
 
434
449
  tool_manager = ToolManager(
@@ -446,3 +461,76 @@ class TestMCPManager:
446
461
  assert "internal_search" not in tool_names
447
462
  assert "mcp_test_tool" in tool_names
448
463
  assert len(tools) == 1
464
+
465
+ def test_mcp_tool_wrapper_generates_correct_parameters(
466
+ self, mcp_tools, mcp_servers, tool_progress_reporter
467
+ ):
468
+ """Test that MCPToolWrapper correctly generates parameters field from MCP tool input schema"""
469
+ # Get the first MCP tool and server from fixtures
470
+ mcp_tool = mcp_tools[0]
471
+ mcp_server = mcp_servers[0]
472
+
473
+ # Create MCPToolConfig
474
+ config = MCPToolConfig(
475
+ server_id="test_server_id",
476
+ server_name="test_server",
477
+ mcp_source_id="test_server_id",
478
+ )
479
+
480
+ # Initialize the MCPToolWrapper
481
+ tool_wrapper = MCPToolWrapper(
482
+ mcp_server=mcp_server,
483
+ mcp_tool=mcp_tool,
484
+ config=config,
485
+ event=self.event,
486
+ tool_progress_reporter=tool_progress_reporter,
487
+ )
488
+
489
+ # Call tool_description to get the LanguageModelToolDescription
490
+ tool_description = tool_wrapper.tool_description()
491
+
492
+ # Verify the basic properties
493
+ assert tool_description.name == "mcp_test_tool"
494
+ assert tool_description.description == "Test MCP tool"
495
+
496
+ # Verify that parameters field is correctly set to the input_schema
497
+ expected_parameters = {
498
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
499
+ "type": "object",
500
+ "properties": {
501
+ "folder": {
502
+ "description": "Mail folder to read mails from",
503
+ "default": "inbox",
504
+ "type": "string",
505
+ },
506
+ "limit": {
507
+ "description": "Number of emails to retrieve",
508
+ "default": 10,
509
+ "type": "number",
510
+ "minimum": 1,
511
+ "maximum": 50,
512
+ },
513
+ },
514
+ }
515
+
516
+ assert tool_description.parameters == expected_parameters
517
+
518
+ # Verify specific parameter properties
519
+ parameters = tool_description.parameters
520
+ assert isinstance(parameters, dict)
521
+ assert parameters["type"] == "object"
522
+ assert "properties" in parameters
523
+
524
+ # Check folder parameter
525
+ folder_prop = parameters["properties"]["folder"]
526
+ assert folder_prop["type"] == "string"
527
+ assert folder_prop["description"] == "Mail folder to read mails from"
528
+ assert folder_prop["default"] == "inbox"
529
+
530
+ # Check limit parameter
531
+ limit_prop = parameters["properties"]["limit"]
532
+ assert limit_prop["type"] == "number"
533
+ assert limit_prop["description"] == "Number of emails to retrieve"
534
+ assert limit_prop["default"] == 10
535
+ assert limit_prop["minimum"] == 1
536
+ assert limit_prop["maximum"] == 50