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,91 @@
1
+ import datetime
2
+ import json
3
+ from typing import NamedTuple
4
+
5
+ import unique_sdk
6
+
7
+
8
+ def _clone_message(
9
+ message: unique_sdk.Space.Message,
10
+ ) -> unique_sdk.Space.Message:
11
+ # copy.deepcopy does not work for instances of UniqueObject
12
+ return json.loads(json.dumps(message))
13
+
14
+
15
+ class SubAgentResponse(NamedTuple):
16
+ assistant_id: str
17
+ name: str
18
+ sequence_number: int
19
+ message: unique_sdk.Space.Message
20
+ timestamp: datetime.datetime
21
+
22
+ def clone(self) -> "SubAgentResponse":
23
+ return SubAgentResponse(
24
+ assistant_id=self.assistant_id,
25
+ name=self.name,
26
+ sequence_number=self.sequence_number,
27
+ message=_clone_message(self.message),
28
+ timestamp=self.timestamp,
29
+ )
30
+
31
+
32
+ class SubAgentResponseWatcher:
33
+ """
34
+ Save and retrieve sub agent responses immutably.
35
+ """
36
+
37
+ def __init__(self) -> None:
38
+ self._response_registry: dict[str, list[SubAgentResponse]] = {}
39
+
40
+ def notify_response(
41
+ self,
42
+ assistant_id: str,
43
+ name: str,
44
+ sequence_number: int,
45
+ response: unique_sdk.Space.Message,
46
+ timestamp: datetime.datetime,
47
+ ) -> None:
48
+ if assistant_id not in self._response_registry:
49
+ self._response_registry[assistant_id] = []
50
+
51
+ response = _clone_message(response)
52
+
53
+ self._response_registry[assistant_id].append(
54
+ SubAgentResponse(
55
+ assistant_id=assistant_id,
56
+ name=name,
57
+ sequence_number=sequence_number,
58
+ message=response,
59
+ timestamp=timestamp,
60
+ )
61
+ )
62
+
63
+ def get_responses(self, assistant_id: str) -> list[SubAgentResponse]:
64
+ return _sort_responses( # Always return a consistent order
65
+ [
66
+ response.clone()
67
+ for response in self._response_registry.get(assistant_id, [])
68
+ ],
69
+ )
70
+
71
+ def get_all_responses(self) -> list[SubAgentResponse]:
72
+ return _sort_responses(
73
+ [
74
+ response.clone()
75
+ for sub_agent_responses in self._response_registry.values()
76
+ for response in sub_agent_responses
77
+ ],
78
+ )
79
+
80
+
81
+ def _sort_responses(
82
+ responses: list[SubAgentResponse],
83
+ ) -> list[SubAgentResponse]:
84
+ return sorted(
85
+ responses,
86
+ key=lambda response: (
87
+ response.timestamp,
88
+ response.assistant_id,
89
+ response.sequence_number,
90
+ ),
91
+ )
@@ -1,3 +1,5 @@
1
+ from typing import Literal
2
+
1
3
  from pydantic import Field
2
4
 
3
5
  from unique_toolkit._common.pydantic_helpers import get_configuration_dict
@@ -7,10 +9,6 @@ DEFAULT_PARAM_DESCRIPTION_SUB_AGENT_USER_MESSAGE = """
7
9
  This is the message that will be sent to the sub-agent.
8
10
  """.strip()
9
11
 
10
- DEFAULT_FORMAT_INFORMATION_SUB_AGENT_SYSTEM_MESSAGE = """
11
- NEVER mention any references from sub-agent answers in your response.
12
- """.strip()
13
-
14
12
 
15
13
  class SubAgentToolConfig(BaseToolConfig):
16
14
  model_config = get_configuration_dict()
@@ -27,6 +25,14 @@ class SubAgentToolConfig(BaseToolConfig):
27
25
  default=True,
28
26
  description="Whether to reuse the existing chat or create a new one for each sub-agent call.",
29
27
  )
28
+ use_sub_agent_references: bool = Field(
29
+ default=True,
30
+ description="Whether this sub agent's references should be used in the main agent's response.",
31
+ )
32
+ forced_tools: list[str] | None = Field(
33
+ default=None,
34
+ description="The list of tool names that will be forced to be called for this sub-agent.",
35
+ )
30
36
 
31
37
  tool_description_for_system_prompt: str = Field(
32
38
  default="",
@@ -41,7 +47,7 @@ class SubAgentToolConfig(BaseToolConfig):
41
47
  description="Description of the user message parameter that will be sent to the model.",
42
48
  )
43
49
  tool_format_information_for_system_prompt: str = Field(
44
- default=DEFAULT_FORMAT_INFORMATION_SUB_AGENT_SYSTEM_MESSAGE,
50
+ default="",
45
51
  description="Format information that will be included in the system prompt to guide response formatting.",
46
52
  )
47
53
  tool_description_for_user_prompt: str = Field(
@@ -61,3 +67,7 @@ class SubAgentToolConfig(BaseToolConfig):
61
67
  default=120.0,
62
68
  description="Maximum time in seconds to wait for the sub-agent response before timing out.",
63
69
  )
70
+ stop_condition: Literal["stoppedStreamingAt", "completedAt"] = Field(
71
+ default="completedAt",
72
+ description="The condition that will be used to stop the polling for the sub-agent response.",
73
+ )
@@ -1,12 +1,21 @@
1
1
  import asyncio
2
2
  import contextlib
3
- from typing import Protocol, override
3
+ import logging
4
+ import re
5
+ from datetime import datetime
6
+ from typing import override
4
7
 
5
8
  import unique_sdk
6
9
  from pydantic import Field, create_model
7
10
  from unique_sdk.utils.chat_in_space import send_message_and_wait_for_completion
8
11
 
12
+ from unique_toolkit._common.referencing import (
13
+ get_all_ref_numbers,
14
+ remove_all_refs,
15
+ replace_ref_number,
16
+ )
9
17
  from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
18
+ from unique_toolkit.agentic.tools.a2a.response_watcher import SubAgentResponseWatcher
10
19
  from unique_toolkit.agentic.tools.a2a.tool._memory import (
11
20
  get_sub_agent_short_term_memory_manager,
12
21
  )
@@ -17,7 +26,6 @@ from unique_toolkit.agentic.tools.a2a.tool._schema import (
17
26
  from unique_toolkit.agentic.tools.a2a.tool.config import (
18
27
  SubAgentToolConfig,
19
28
  )
20
- from unique_toolkit.agentic.tools.agent_chunks_hanlder import AgentChunksHandler
21
29
  from unique_toolkit.agentic.tools.factory import ToolFactory
22
30
  from unique_toolkit.agentic.tools.schemas import ToolCallResponse
23
31
  from unique_toolkit.agentic.tools.tool import Tool
@@ -30,22 +38,8 @@ from unique_toolkit.language_model import (
30
38
  LanguageModelFunction,
31
39
  LanguageModelToolDescription,
32
40
  )
33
- from unique_toolkit.language_model.schemas import LanguageModelMessage
34
41
 
35
-
36
- class SubAgentResponseSubscriber(Protocol):
37
- def notify_sub_agent_response(
38
- self,
39
- response: unique_sdk.Space.Message,
40
- sub_agent_assistant_id: str,
41
- sequence_number: int,
42
- ) -> None: ...
43
-
44
- """
45
- Notify the subscriber that a sub agent response has been received.
46
- Important: The subscriber should NOT modify the response in place.
47
- The sequence number is a 1-indexed counter that is incremented for each concurrent run of the same sub agent.
48
- """
42
+ logger = logging.getLogger(__name__)
49
43
 
50
44
 
51
45
  class SubAgentTool(Tool[SubAgentToolConfig]):
@@ -58,6 +52,7 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
58
52
  tool_progress_reporter: ToolProgressReporter | None = None,
59
53
  name: str = "SubAgentTool",
60
54
  display_name: str = "SubAgentTool",
55
+ response_watcher: SubAgentResponseWatcher | None = None,
61
56
  ):
62
57
  super().__init__(configuration, event, tool_progress_reporter)
63
58
  self._user_id = event.user_id
@@ -72,15 +67,25 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
72
67
  chat_id=event.payload.chat_id,
73
68
  assistant_id=self.config.assistant_id,
74
69
  )
75
- self._subscribers: list[SubAgentResponseSubscriber] = []
76
70
  self._should_run_evaluation = False
77
71
 
72
+ self._response_watcher = response_watcher
73
+
78
74
  # Synchronization state
79
75
  self._sequence_number = 1
80
76
  self._lock = asyncio.Lock()
81
77
 
82
- def subscribe(self, subscriber: SubAgentResponseSubscriber) -> None:
83
- self._subscribers.append(subscriber)
78
+ @staticmethod
79
+ def get_sub_agent_reference_format(
80
+ name: str, sequence_number: int, reference_number: int
81
+ ) -> str:
82
+ return f"<sup><name>{name} {sequence_number}</name>{reference_number}</sup>"
83
+
84
+ @staticmethod
85
+ def get_sub_agent_reference_re(
86
+ name: str, sequence_number: int, reference_number: int
87
+ ) -> str:
88
+ return rf"<sup>\s*<name>\s*{re.escape(name)}\s*{sequence_number}\s*</name>\s*{reference_number}\s*</sup>"
84
89
 
85
90
  @override
86
91
  def display_name(self) -> str:
@@ -132,6 +137,7 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
132
137
  @override
133
138
  async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
134
139
  tool_input = SubAgentToolInput.model_validate(tool_call.arguments)
140
+ timestamp = datetime.now()
135
141
 
136
142
  if self._lock.locked():
137
143
  await self._notify_progress(
@@ -167,7 +173,7 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
167
173
  response["assessment"] is not None and len(response["assessment"]) > 0
168
174
  ) # Run evaluation if any sub agent returned an assessment
169
175
 
170
- self._notify_subscribers(response, sequence_number)
176
+ self._notify_watcher(response, sequence_number, timestamp)
171
177
 
172
178
  if chat_id is None:
173
179
  await self._save_chat_id(response["chatId"])
@@ -175,6 +181,11 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
175
181
  if response["text"] is None:
176
182
  raise ValueError("No response returned from sub agent")
177
183
 
184
+ response_text_with_references = self._prepare_response_references(
185
+ response=response["text"],
186
+ sequence_number=sequence_number,
187
+ )
188
+
178
189
  await self._notify_progress(
179
190
  tool_call=tool_call,
180
191
  message=tool_input.user_message,
@@ -184,16 +195,9 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
184
195
  return ToolCallResponse(
185
196
  id=tool_call.id, # type: ignore
186
197
  name=tool_call.name,
187
- content=response["text"],
198
+ content=response_text_with_references,
188
199
  )
189
200
 
190
- @override
191
- def get_tool_call_result_for_loop_history(
192
- self,
193
- tool_response: ToolCallResponse,
194
- agent_chunks_handler: AgentChunksHandler,
195
- ) -> LanguageModelMessage: ... # Empty as method is deprecated
196
-
197
201
  async def _get_chat_id(self) -> str | None:
198
202
  if not self.config.reuse_chat:
199
203
  return None
@@ -209,6 +213,23 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
209
213
 
210
214
  return None
211
215
 
216
+ def _prepare_response_references(self, response: str, sequence_number: int) -> str:
217
+ if not self.config.use_sub_agent_references:
218
+ # Remove all references from the response
219
+ response = remove_all_refs(response)
220
+ return response
221
+
222
+ for ref_number in get_all_ref_numbers(response):
223
+ reference = self.get_sub_agent_reference_format(
224
+ name=self.name,
225
+ sequence_number=sequence_number,
226
+ reference_number=ref_number,
227
+ )
228
+ response = replace_ref_number(
229
+ text=response, ref_number=ref_number, replacement=reference
230
+ )
231
+ return response
232
+
212
233
  async def _save_chat_id(self, chat_id: str) -> None:
213
234
  if not self.config.reuse_chat:
214
235
  return
@@ -231,14 +252,25 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
231
252
  state=state,
232
253
  )
233
254
 
234
- def _notify_subscribers(
235
- self, response: unique_sdk.Space.Message, sequence_number: int
255
+ def _notify_watcher(
256
+ self,
257
+ response: unique_sdk.Space.Message,
258
+ sequence_number: int,
259
+ timestamp: datetime,
236
260
  ) -> None:
237
- for subsciber in self._subscribers:
238
- subsciber.notify_sub_agent_response(
239
- sub_agent_assistant_id=self.config.assistant_id,
240
- response=response,
261
+ if self._response_watcher is not None:
262
+ self._response_watcher.notify_response(
263
+ assistant_id=self.config.assistant_id,
264
+ name=self.name,
241
265
  sequence_number=sequence_number,
266
+ response=response,
267
+ timestamp=timestamp,
268
+ )
269
+ else:
270
+ logger.warning(
271
+ "No response watcher found for sub agent %s (assistant_id: %s)",
272
+ self.name,
273
+ self.config.assistant_id,
242
274
  )
243
275
 
244
276
  async def _execute_and_handle_timeout(
@@ -255,8 +287,9 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
255
287
  text=tool_user_message,
256
288
  chat_id=chat_id,
257
289
  poll_interval=self.config.poll_interval,
290
+ tool_choices=self.config.forced_tools,
258
291
  max_wait=self.config.max_wait,
259
- stop_condition="completedAt",
292
+ stop_condition=self.config.stop_condition,
260
293
  )
261
294
  except TimeoutError as e:
262
295
  await self._notify_progress(
@@ -1,9 +1,10 @@
1
1
  import json
2
2
  from enum import StrEnum
3
- from typing import Any, Dict
3
+ from typing import Annotated, Any, Dict
4
4
 
5
5
  from pydantic import (
6
6
  BaseModel,
7
+ BeforeValidator,
7
8
  Field,
8
9
  ValidationInfo,
9
10
  model_validator,
@@ -33,6 +34,16 @@ class ToolSelectionPolicy(StrEnum):
33
34
  BY_USER = "ByUser"
34
35
 
35
36
 
37
+ def handle_undefined_icon(value: Any) -> ToolIcon:
38
+ try:
39
+ if isinstance(value, str):
40
+ return ToolIcon(value)
41
+ else:
42
+ return ToolIcon.BOOK
43
+ except ValueError:
44
+ return ToolIcon.BOOK
45
+
46
+
36
47
  class ToolBuildConfig(BaseModel):
37
48
  model_config = get_configuration_dict()
38
49
  """Main tool configuration"""
@@ -40,7 +51,10 @@ class ToolBuildConfig(BaseModel):
40
51
  name: str
41
52
  configuration: BaseToolConfig
42
53
  display_name: str = ""
43
- icon: ToolIcon = ToolIcon.BOOK
54
+ icon: Annotated[ToolIcon, BeforeValidator(handle_undefined_icon)] = Field(
55
+ default=ToolIcon.BOOK,
56
+ description="The icon name that will be used to display the tool in the user interface.",
57
+ )
44
58
  selection_policy: ToolSelectionPolicy = Field(
45
59
  default=ToolSelectionPolicy.BY_USER,
46
60
  )
@@ -11,6 +11,10 @@ class ToolFactory:
11
11
  tool_map: dict[str, type[Tool]] = {}
12
12
  tool_config_map: dict[str, Callable] = {}
13
13
 
14
+ @classmethod
15
+ def register_tool_config(cls, tool_name: str, tool_config: type[BaseToolConfig]):
16
+ cls.tool_config_map[tool_name] = tool_config
17
+
14
18
  @classmethod
15
19
  def register_tool(
16
20
  cls,
@@ -1,8 +1,8 @@
1
1
  import json
2
+ import logging
2
3
  from typing import Any, Dict
3
4
 
4
5
  import unique_sdk
5
- from pydantic import BaseModel, Field, create_model
6
6
 
7
7
  from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
8
8
  from unique_toolkit.agentic.tools.mcp.models import MCPToolConfig
@@ -13,12 +13,13 @@ from unique_toolkit.agentic.tools.tool_progress_reporter import (
13
13
  ToolProgressReporter,
14
14
  )
15
15
  from unique_toolkit.app.schemas import ChatEvent, McpServer, McpTool
16
- from unique_toolkit.language_model import LanguageModelMessage
17
16
  from unique_toolkit.language_model.schemas import (
18
17
  LanguageModelFunction,
19
18
  LanguageModelToolDescription,
20
19
  )
21
20
 
21
+ logger = logging.getLogger(__name__)
22
+
22
23
 
23
24
  class MCPToolWrapper(Tool[MCPToolConfig]):
24
25
  """Wrapper class for MCP tools that implements the Tool interface"""
@@ -39,39 +40,16 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
39
40
  def tool_description(self) -> LanguageModelToolDescription:
40
41
  """Convert MCP tool schema to LanguageModelToolDescription"""
41
42
  # Create a Pydantic model from the MCP tool's input schema
42
- parameters_model = self._create_parameters_model()
43
+ logger.info(
44
+ "MCP tool %s schema %s", self._mcp_tool.name, self._mcp_tool.input_schema
45
+ )
43
46
 
44
47
  return LanguageModelToolDescription(
45
48
  name=self.name,
46
49
  description=self._mcp_tool.description or "",
47
- parameters=parameters_model,
50
+ parameters=self._mcp_tool.input_schema,
48
51
  )
49
52
 
50
- def _create_parameters_model(self) -> type[BaseModel]:
51
- """Create a Pydantic model from MCP tool's input schema"""
52
- properties = self._mcp_tool.input_schema.get("properties", {})
53
- required_fields = self._mcp_tool.input_schema.get("required", [])
54
-
55
- # Convert JSON schema properties to Pydantic fields
56
- fields = {}
57
- for prop_name, prop_schema in properties.items():
58
- field_type = self._json_schema_to_python_type(prop_schema)
59
- field_description = prop_schema.get("description", "")
60
-
61
- if prop_name in required_fields:
62
- fields[prop_name] = (
63
- field_type,
64
- Field(description=field_description),
65
- )
66
- else:
67
- fields[prop_name] = (
68
- field_type,
69
- Field(default=None, description=field_description),
70
- )
71
-
72
- # Create dynamic model
73
- return create_model(f"{self.name}Parameters", **fields)
74
-
75
53
  def _json_schema_to_python_type(self, schema: Dict[str, Any]) -> type:
76
54
  """Convert JSON schema type to Python type"""
77
55
  json_type = schema.get("type", "string")
@@ -120,12 +98,6 @@ class MCPToolWrapper(Tool[MCPToolConfig]):
120
98
  """Return evaluation checks based on tool response"""
121
99
  return []
122
100
 
123
- def get_tool_call_result_for_loop_history(
124
- self,
125
- tool_response: ToolCallResponse,
126
- ) -> LanguageModelMessage:
127
- raise NotImplementedError("function is not supported")
128
-
129
101
  async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
130
102
  """Execute the MCP tool using SDK to call public API"""
131
103
  self.logger.info(f"Running MCP tool: {self.name}")
@@ -0,0 +1,11 @@
1
+ from unique_toolkit.agentic.tools.openai_builtin.code_interpreter import (
2
+ OpenAICodeInterpreterConfig,
3
+ OpenAICodeInterpreterTool,
4
+ )
5
+ from unique_toolkit.agentic.tools.openai_builtin.manager import OpenAIBuiltInToolManager
6
+
7
+ __all__ = [
8
+ "OpenAIBuiltInToolManager",
9
+ "OpenAICodeInterpreterTool",
10
+ "OpenAICodeInterpreterConfig",
11
+ ]
@@ -0,0 +1,30 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import StrEnum
3
+ from typing import Generic, TypeVar
4
+
5
+ from openai.types.responses.tool_param import CodeInterpreter
6
+
7
+ from unique_toolkit.agentic.tools.schemas import ToolPrompts
8
+
9
+
10
+ class OpenAIBuiltInToolName(StrEnum):
11
+ CODE_INTERPRETER = "code_interpreter"
12
+
13
+
14
+ BuiltInToolType = CodeInterpreter # Add other tool types when needed
15
+ ToolType = TypeVar("ToolType", bound=BuiltInToolType)
16
+
17
+
18
+ class OpenAIBuiltInTool(ABC, Generic[ToolType]):
19
+ @property
20
+ @abstractmethod
21
+ def name(self) -> OpenAIBuiltInToolName:
22
+ raise NotImplementedError()
23
+
24
+ @abstractmethod
25
+ def tool_description(self) -> BuiltInToolType:
26
+ raise NotImplementedError()
27
+
28
+ @abstractmethod
29
+ def get_tool_prompts(self) -> ToolPrompts:
30
+ raise NotImplementedError()
@@ -0,0 +1,8 @@
1
+ from unique_toolkit.agentic.tools.openai_builtin.code_interpreter.config import (
2
+ OpenAICodeInterpreterConfig,
3
+ )
4
+ from unique_toolkit.agentic.tools.openai_builtin.code_interpreter.service import (
5
+ OpenAICodeInterpreterTool,
6
+ )
7
+
8
+ __all__ = ["OpenAICodeInterpreterConfig", "OpenAICodeInterpreterTool"]
@@ -0,0 +1,57 @@
1
+ from pydantic import Field
2
+
3
+ from unique_toolkit.agentic.tools.factory import ToolFactory
4
+ from unique_toolkit.agentic.tools.openai_builtin.base import (
5
+ OpenAIBuiltInToolName,
6
+ )
7
+ from unique_toolkit.agentic.tools.schemas import BaseToolConfig
8
+
9
+ DEFAULT_TOOL_DESCRIPTION = "Use this tool to run python code, e.g to generate plots, process excel files, perform calculations, etc."
10
+
11
+ DEFAULT_TOOL_DESCRIPTION_FOR_SYSTEM_PROMPT = """
12
+ Use this tool to run python code, e.g to generate plots, process excel files, perform calculations, etc.
13
+ Instructions:
14
+ - All files uploaded to the chat are available in the code interpreter under the path `/mnt/data/<filename>
15
+ - All files generated through code should be saved in the `/mnt/data` folder
16
+
17
+ Instructions for displaying images and files in the chat:
18
+ Once files are generated in the `/mnt/data` folder you MUST reference them in the chat using markdown syntax in order to display them in the chat.
19
+
20
+ - If you want to display an image, use the following syntax: `![Image Name](sandbox:/mnt/data/<filename>)`
21
+ - Images will be converted and shown in the chat.
22
+ - Do NOT display an extra download link for images a part from the markdown above.
23
+ - Not using markdown syntax will FAIL to show images to the user.
24
+ - YOU MUST use the syntax above to display images, otherwise the image will not be displayed in the chat.
25
+ - For displaying a link to a file, use the following syntax: `[filename](sandbox:/mnt/data/<filename>)`
26
+ - Files are converted to references the user can click on to download the file
27
+
28
+ You MUST always use this syntax, otherwise the files will not be displayed in the chat.
29
+ """.strip()
30
+
31
+ DEFAULT_TOOL_FORMAT_INFORMATION_FOR_SYSTEM_PROMPT = ""
32
+
33
+ DEFAULT_TOOL_FORMAT_INFORMATION_FOR_USER_PROMPT = ""
34
+
35
+ DEFAULT_TOOL_DESCRIPTION_FOR_USER_PROMPT = ""
36
+
37
+
38
+ class OpenAICodeInterpreterConfig(BaseToolConfig):
39
+ upload_files_in_chat: bool = Field(default=True)
40
+
41
+ tool_description: str = DEFAULT_TOOL_DESCRIPTION
42
+ tool_description_for_system_prompt: str = DEFAULT_TOOL_DESCRIPTION_FOR_SYSTEM_PROMPT
43
+ tool_format_information_for_system_prompt: str = (
44
+ DEFAULT_TOOL_FORMAT_INFORMATION_FOR_SYSTEM_PROMPT
45
+ )
46
+ tool_description_for_user_prompt: str = DEFAULT_TOOL_DESCRIPTION_FOR_USER_PROMPT
47
+ tool_format_information_for_user_prompt: str = (
48
+ DEFAULT_TOOL_FORMAT_INFORMATION_FOR_USER_PROMPT
49
+ )
50
+
51
+ expires_after_minutes: int = 20
52
+ use_auto_container: bool = False
53
+
54
+
55
+ ToolFactory.register_tool_config(
56
+ OpenAIBuiltInToolName.CODE_INTERPRETER, OpenAICodeInterpreterConfig
57
+ )