unique_toolkit 0.7.7__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 (166) hide show
  1. unique_toolkit/__init__.py +28 -1
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +343 -0
  3. unique_toolkit/_common/base_model_type_attribute.py +303 -0
  4. unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
  5. unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
  6. unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
  7. unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
  8. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
  9. unique_toolkit/_common/default_language_model.py +12 -0
  10. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  11. unique_toolkit/_common/docx_generator/config.py +12 -0
  12. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  13. unique_toolkit/_common/docx_generator/service.py +252 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +305 -0
  16. unique_toolkit/_common/endpoint_requestor.py +430 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/feature_flags/schema.py +9 -0
  19. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  20. unique_toolkit/_common/pydantic_helpers.py +154 -0
  21. unique_toolkit/_common/referencing.py +53 -0
  22. unique_toolkit/_common/string_utilities.py +140 -0
  23. unique_toolkit/_common/tests/test_referencing.py +521 -0
  24. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  25. unique_toolkit/_common/token/image_token_counting.py +67 -0
  26. unique_toolkit/_common/token/token_counting.py +204 -0
  27. unique_toolkit/_common/utils/__init__.py +1 -0
  28. unique_toolkit/_common/utils/files.py +43 -0
  29. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  30. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  31. unique_toolkit/_common/utils/write_configuration.py +51 -0
  32. unique_toolkit/_common/validators.py +101 -4
  33. unique_toolkit/agentic/__init__.py +1 -0
  34. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  35. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  36. unique_toolkit/agentic/evaluation/config.py +36 -0
  37. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  38. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  39. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  40. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  41. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  42. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
  43. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  44. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
  45. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
  46. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  47. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  48. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  49. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  50. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
  51. unique_toolkit/agentic/history_manager/history_manager.py +242 -0
  52. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  53. unique_toolkit/agentic/history_manager/utils.py +96 -0
  54. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  55. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  56. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  57. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  58. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  59. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  60. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  61. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  62. unique_toolkit/agentic/tools/__init__.py +1 -0
  63. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  64. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  65. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  66. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  67. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  68. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  69. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  70. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  71. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  72. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  73. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  74. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
  75. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  76. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  77. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  78. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  79. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  80. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  81. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  82. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  83. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  84. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  85. unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
  86. unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
  87. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  88. unique_toolkit/agentic/tools/config.py +167 -0
  89. unique_toolkit/agentic/tools/factory.py +44 -0
  90. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  91. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  92. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  93. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  94. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  95. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  96. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  97. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  98. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  99. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  100. unique_toolkit/agentic/tools/schemas.py +141 -0
  101. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  102. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  103. unique_toolkit/agentic/tools/tool.py +183 -0
  104. unique_toolkit/agentic/tools/tool_manager.py +523 -0
  105. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  106. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  107. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  108. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  109. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  110. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  111. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  112. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  113. unique_toolkit/app/__init__.py +6 -0
  114. unique_toolkit/app/dev_util.py +180 -0
  115. unique_toolkit/app/init_sdk.py +32 -1
  116. unique_toolkit/app/schemas.py +198 -31
  117. unique_toolkit/app/unique_settings.py +367 -0
  118. unique_toolkit/chat/__init__.py +8 -1
  119. unique_toolkit/chat/deprecated/service.py +232 -0
  120. unique_toolkit/chat/functions.py +642 -77
  121. unique_toolkit/chat/rendering.py +34 -0
  122. unique_toolkit/chat/responses_api.py +461 -0
  123. unique_toolkit/chat/schemas.py +133 -2
  124. unique_toolkit/chat/service.py +115 -767
  125. unique_toolkit/content/functions.py +153 -4
  126. unique_toolkit/content/schemas.py +122 -15
  127. unique_toolkit/content/service.py +278 -44
  128. unique_toolkit/content/smart_rules.py +301 -0
  129. unique_toolkit/content/utils.py +8 -3
  130. unique_toolkit/embedding/service.py +102 -11
  131. unique_toolkit/framework_utilities/__init__.py +1 -0
  132. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  133. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  134. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  135. unique_toolkit/framework_utilities/openai/client.py +83 -0
  136. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  137. unique_toolkit/framework_utilities/utils.py +23 -0
  138. unique_toolkit/language_model/__init__.py +3 -0
  139. unique_toolkit/language_model/builder.py +27 -11
  140. unique_toolkit/language_model/default_language_model.py +3 -0
  141. unique_toolkit/language_model/functions.py +327 -43
  142. unique_toolkit/language_model/infos.py +992 -50
  143. unique_toolkit/language_model/reference.py +242 -0
  144. unique_toolkit/language_model/schemas.py +475 -48
  145. unique_toolkit/language_model/service.py +228 -27
  146. unique_toolkit/protocols/support.py +145 -0
  147. unique_toolkit/services/__init__.py +7 -0
  148. unique_toolkit/services/chat_service.py +1630 -0
  149. unique_toolkit/services/knowledge_base.py +861 -0
  150. unique_toolkit/short_term_memory/service.py +178 -41
  151. unique_toolkit/smart_rules/__init__.py +0 -0
  152. unique_toolkit/smart_rules/compile.py +56 -0
  153. unique_toolkit/test_utilities/events.py +197 -0
  154. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
  155. unique_toolkit-1.23.0.dist-info/RECORD +182 -0
  156. unique_toolkit/evaluators/__init__.py +0 -1
  157. unique_toolkit/evaluators/config.py +0 -35
  158. unique_toolkit/evaluators/constants.py +0 -1
  159. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  160. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  161. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  162. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  163. unique_toolkit-0.7.7.dist-info/RECORD +0 -64
  164. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  165. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  166. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,167 @@
1
+ import json
2
+ from enum import StrEnum
3
+ from typing import Annotated, Any, Dict
4
+
5
+ from pydantic import (
6
+ BaseModel,
7
+ BeforeValidator,
8
+ Field,
9
+ ValidationInfo,
10
+ model_validator,
11
+ )
12
+
13
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
14
+ from unique_toolkit.agentic.tools.schemas import BaseToolConfig
15
+
16
+
17
+ class ToolIcon(StrEnum):
18
+ ANALYTICS = "IconAnalytics"
19
+ BOOK = "IconBook"
20
+ FOLDERDATA = "IconFolderData"
21
+ INTEGRATION = "IconIntegration"
22
+ TEXT_COMPARE = "IconTextCompare"
23
+ WORLD = "IconWorld"
24
+ QUICK_REPLY = "IconQuickReply"
25
+ CHAT_PLUS = "IconChatPlus"
26
+ TELESCOPE = "IconTelescope"
27
+
28
+
29
+ class ToolSelectionPolicy(StrEnum):
30
+ """Determine the usage policy of tools."""
31
+
32
+ FORCED_BY_DEFAULT = "ForcedByDefault"
33
+ ON_BY_DEFAULT = "OnByDefault"
34
+ BY_USER = "ByUser"
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
+
47
+ class ToolBuildConfig(BaseModel):
48
+ model_config = get_configuration_dict()
49
+ """Main tool configuration"""
50
+
51
+ name: str
52
+ configuration: BaseToolConfig
53
+ display_name: str = ""
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
+ )
58
+ selection_policy: ToolSelectionPolicy = Field(
59
+ default=ToolSelectionPolicy.BY_USER,
60
+ )
61
+ is_exclusive: bool = Field(
62
+ default=False,
63
+ description="This tool must be chosen by the user and no other tools are used for this iteration.",
64
+ )
65
+ is_sub_agent: bool = False
66
+
67
+ is_enabled: bool = Field(default=True)
68
+
69
+ @model_validator(mode="before")
70
+ def initialize_config_based_on_tool_name(
71
+ cls,
72
+ value: Any,
73
+ info: ValidationInfo,
74
+ ) -> Any:
75
+ """Check the given values for."""
76
+ if not isinstance(value, dict):
77
+ return value
78
+
79
+ is_mcp_tool = value.get("mcp_source_id", "") != ""
80
+ mcp_configuration = value.get("configuration", {})
81
+
82
+ # Import at runtime to avoid circular imports
83
+ from unique_toolkit.agentic.tools.mcp.models import MCPToolConfig
84
+
85
+ if (
86
+ isinstance(mcp_configuration, MCPToolConfig)
87
+ and mcp_configuration.mcp_source_id
88
+ ):
89
+ return value
90
+ if is_mcp_tool:
91
+ # For MCP tools, skip ToolFactory validation
92
+ # Configuration can remain as a dict
93
+ return value
94
+
95
+ is_sub_agent_tool = (
96
+ value.get("is_sub_agent") or value.get("isSubAgent") or False
97
+ )
98
+
99
+ configuration = value.get("configuration", {})
100
+
101
+ if is_sub_agent_tool:
102
+ from unique_toolkit.agentic.tools.a2a import ExtendedSubAgentToolConfig
103
+
104
+ config = ExtendedSubAgentToolConfig.model_validate(configuration)
105
+ elif isinstance(configuration, dict):
106
+ # Local import to avoid circular import at module import time
107
+ from unique_toolkit.agentic.tools.factory import ToolFactory
108
+
109
+ config = ToolFactory.build_tool_config(
110
+ value["name"],
111
+ **configuration,
112
+ )
113
+ else:
114
+ # Check that the type of config matches the tool name
115
+ from unique_toolkit.agentic.tools.factory import ToolFactory
116
+
117
+ assert isinstance(
118
+ configuration,
119
+ ToolFactory.tool_config_map[value["name"]], # type: ignore
120
+ )
121
+ config = configuration
122
+ value["configuration"] = config
123
+ return value
124
+
125
+ def model_dump(self) -> Dict[str, Any]:
126
+ """
127
+ Returns a dict representation of the tool config that preserves
128
+ subclass fields from `configuration` by delegating to its own
129
+ model_dump. This prevents `{}` when `configuration` is typed
130
+ as `BaseToolConfig` but holds a subclass instance.
131
+ """
132
+ data: Dict[str, Any] = {
133
+ "name": self.name,
134
+ "configuration": self.configuration.model_dump()
135
+ if self.configuration
136
+ else None,
137
+ "display_name": self.display_name,
138
+ "icon": self.icon,
139
+ "selection_policy": self.selection_policy,
140
+ "is_exclusive": self.is_exclusive,
141
+ "is_sub_agent": self.is_sub_agent,
142
+ "is_enabled": self.is_enabled,
143
+ }
144
+ return data
145
+
146
+ def model_dump_json(self) -> str:
147
+ """
148
+ Returns a JSON string representation of the tool config.
149
+ Ensures `configuration` is fully serialized by using the
150
+ subclass's `model_dump_json()` when available.
151
+ """
152
+ config_json = (
153
+ self.configuration.model_dump_json() if self.configuration else None
154
+ )
155
+ config = json.loads(config_json) if config_json else None
156
+
157
+ data: Dict[str, Any] = {
158
+ "name": self.name,
159
+ "configuration": config,
160
+ "display_name": self.display_name,
161
+ "icon": self.icon,
162
+ "selection_policy": self.selection_policy,
163
+ "is_exclusive": self.is_exclusive,
164
+ "is_sub_agent": self.is_sub_agent,
165
+ "is_enabled": self.is_enabled,
166
+ }
167
+ return json.dumps(data)
@@ -0,0 +1,44 @@
1
+ from typing import TYPE_CHECKING, Callable
2
+
3
+ from unique_toolkit.agentic.tools.schemas import BaseToolConfig
4
+ from unique_toolkit.agentic.tools.tool import Tool
5
+
6
+ if TYPE_CHECKING:
7
+ from unique_toolkit.agentic.tools.config import ToolBuildConfig
8
+
9
+
10
+ class ToolFactory:
11
+ tool_map: dict[str, type[Tool]] = {}
12
+ tool_config_map: dict[str, Callable] = {}
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
+
18
+ @classmethod
19
+ def register_tool(
20
+ cls,
21
+ tool: type[Tool],
22
+ tool_config: type[BaseToolConfig],
23
+ ):
24
+ cls.tool_map[tool.name] = tool
25
+ cls.tool_config_map[tool.name] = tool_config
26
+
27
+ @classmethod
28
+ def build_tool(cls, tool_name: str, *args, **kwargs) -> Tool[BaseToolConfig]:
29
+ tool = cls.tool_map[tool_name](*args, **kwargs)
30
+ return tool
31
+
32
+ @classmethod
33
+ def build_tool_with_settings(
34
+ cls, tool_name: str, settings: "ToolBuildConfig", *args, **kwargs
35
+ ) -> Tool[BaseToolConfig]:
36
+ tool = cls.tool_map[tool_name](*args, **kwargs)
37
+ tool.settings = settings
38
+ return tool
39
+
40
+ @classmethod
41
+ def build_tool_config(cls, tool_name: str, **kwargs) -> BaseToolConfig:
42
+ if tool_name not in cls.tool_config_map:
43
+ raise ValueError(f"Tool {tool_name} not found")
44
+ return cls.tool_config_map[tool_name](**kwargs)
@@ -0,0 +1,4 @@
1
+ from .models import MCPToolConfig
2
+ from .tool_wrapper import MCPToolWrapper
3
+
4
+ __all__ = ["MCPToolWrapper", "MCPToolConfig"]
@@ -0,0 +1,71 @@
1
+ import logging
2
+
3
+ from unique_toolkit.agentic.tools.config import (
4
+ ToolBuildConfig,
5
+ ToolIcon,
6
+ ToolSelectionPolicy,
7
+ )
8
+ from unique_toolkit.agentic.tools.mcp.models import MCPToolConfig
9
+ from unique_toolkit.agentic.tools.mcp.tool_wrapper import MCPToolWrapper
10
+ from unique_toolkit.agentic.tools.schemas import BaseToolConfig
11
+ from unique_toolkit.agentic.tools.tool import Tool
12
+ from unique_toolkit.agentic.tools.tool_progress_reporter import ToolProgressReporter
13
+ from unique_toolkit.app.schemas import ChatEvent, McpServer
14
+
15
+
16
+ class MCPManager:
17
+ def __init__(
18
+ self,
19
+ mcp_servers: list[McpServer],
20
+ event: ChatEvent,
21
+ tool_progress_reporter: ToolProgressReporter,
22
+ ):
23
+ self._mcp_servers = mcp_servers
24
+ self._event = event
25
+ self._tool_progress_reporter = tool_progress_reporter
26
+
27
+ def get_mcp_servers(self):
28
+ return self._mcp_servers
29
+
30
+ def get_mcp_server_by_id(self, id: str):
31
+ return next((server for server in self._mcp_servers if server.id == id), None)
32
+
33
+ def get_all_mcp_tools(self) -> list[Tool[BaseToolConfig]]:
34
+ selected_tools = []
35
+ for server in self._mcp_servers:
36
+ if not hasattr(server, "tools"):
37
+ continue
38
+ if not server.tools:
39
+ continue
40
+
41
+ for tool in server.tools:
42
+ try:
43
+ config = MCPToolConfig(
44
+ server_id=server.id,
45
+ server_name=server.name,
46
+ server_system_prompt=server.system_prompt,
47
+ server_user_prompt=server.user_prompt,
48
+ mcp_source_id=server.id,
49
+ )
50
+ wrapper = MCPToolWrapper(
51
+ mcp_server=server,
52
+ mcp_tool=tool,
53
+ config=config,
54
+ event=self._event,
55
+ tool_progress_reporter=self._tool_progress_reporter,
56
+ )
57
+ wrapper.settings = ToolBuildConfig( # TODO: this must be refactored to behave like the other tools.
58
+ name=tool.name,
59
+ configuration=config,
60
+ display_name=tool.title or tool.name,
61
+ is_exclusive=False,
62
+ is_enabled=True,
63
+ icon=ToolIcon.BOOK,
64
+ selection_policy=ToolSelectionPolicy.BY_USER,
65
+ )
66
+ selected_tools.append(wrapper)
67
+ except Exception as e:
68
+ logging.error(
69
+ f"Error creating MCP tool wrapper for {tool.name}: {e}"
70
+ )
71
+ return selected_tools
@@ -0,0 +1,28 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ from unique_toolkit.agentic.tools.schemas import BaseToolConfig
4
+
5
+
6
+ class MCPTool:
7
+ """Protocol defining the expected structure of an MCP tool."""
8
+
9
+ name: str
10
+ description: Optional[str]
11
+ input_schema: Dict[str, Any]
12
+ output_schema: Optional[Dict[str, Any]]
13
+ annotations: Optional[Dict[str, Any]]
14
+ title: Optional[str]
15
+ icon: Optional[str]
16
+ system_prompt: Optional[str]
17
+ user_prompt: Optional[str]
18
+ is_connected: bool
19
+
20
+
21
+ class MCPToolConfig(BaseToolConfig):
22
+ """Configuration for MCP tools"""
23
+
24
+ server_id: str
25
+ server_name: str
26
+ server_system_prompt: Optional[str] = None
27
+ server_user_prompt: Optional[str] = None
28
+ mcp_source_id: str
@@ -0,0 +1,234 @@
1
+ import json
2
+ import logging
3
+ from typing import Any, Dict
4
+
5
+ import unique_sdk
6
+
7
+ from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
8
+ from unique_toolkit.agentic.tools.mcp.models import MCPToolConfig
9
+ from unique_toolkit.agentic.tools.schemas import ToolCallResponse
10
+ from unique_toolkit.agentic.tools.tool import Tool
11
+ from unique_toolkit.agentic.tools.tool_progress_reporter import (
12
+ ProgressState,
13
+ ToolProgressReporter,
14
+ )
15
+ from unique_toolkit.app.schemas import ChatEvent, McpServer, McpTool
16
+ from unique_toolkit.language_model.schemas import (
17
+ LanguageModelFunction,
18
+ LanguageModelToolDescription,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class MCPToolWrapper(Tool[MCPToolConfig]):
25
+ """Wrapper class for MCP tools that implements the Tool interface"""
26
+
27
+ def __init__(
28
+ self,
29
+ mcp_server: McpServer,
30
+ mcp_tool: McpTool,
31
+ config: MCPToolConfig,
32
+ event: ChatEvent,
33
+ tool_progress_reporter: ToolProgressReporter | None = None,
34
+ ):
35
+ self.name = mcp_tool.name
36
+ super().__init__(config, event, tool_progress_reporter)
37
+ self._mcp_tool = mcp_tool
38
+ self._mcp_server = mcp_server
39
+
40
+ def tool_description(self) -> LanguageModelToolDescription:
41
+ """Convert MCP tool schema to LanguageModelToolDescription"""
42
+ # Create a Pydantic model from the MCP tool's input schema
43
+ logger.info(
44
+ "MCP tool %s schema %s", self._mcp_tool.name, self._mcp_tool.input_schema
45
+ )
46
+
47
+ return LanguageModelToolDescription(
48
+ name=self.name,
49
+ description=self._mcp_tool.description or "",
50
+ parameters=self._mcp_tool.input_schema,
51
+ )
52
+
53
+ def _json_schema_to_python_type(self, schema: Dict[str, Any]) -> type:
54
+ """Convert JSON schema type to Python type"""
55
+ json_type = schema.get("type", "string")
56
+
57
+ type_mapping = {
58
+ "string": str,
59
+ "integer": int,
60
+ "number": float,
61
+ "boolean": bool,
62
+ "array": list,
63
+ "object": dict,
64
+ }
65
+
66
+ return type_mapping.get(json_type, str)
67
+
68
+ def tool_description_for_system_prompt(self) -> str:
69
+ """Return tool description for system prompt"""
70
+ # Not using jinja here to keep it simple and not import new packages.
71
+ description = (
72
+ f"**MCP Server**: {self._mcp_server.name}\n"
73
+ f"**Tool Name**: {self.name}\n"
74
+ f"{self._mcp_tool.system_prompt}"
75
+ )
76
+
77
+ return description
78
+
79
+ def tool_description_for_user_prompt(self) -> str:
80
+ return self._mcp_tool.user_prompt or ""
81
+
82
+ def tool_format_information_for_user_prompt(self) -> str:
83
+ return ""
84
+
85
+ def tool_format_information_for_system_prompt(self) -> str:
86
+ """Return formatting information for system prompt"""
87
+ return "" # this is empty for now as it requires to add this to the MCP model of the backend.
88
+
89
+ def evaluation_check_list(self) -> list[EvaluationMetricName]:
90
+ """Return evaluation check list - empty for MCP tools for now"""
91
+ # TODO: this is empty for now as it requires a setting in the backend for choosing a suitable validator.
92
+ return []
93
+
94
+ def get_evaluation_checks_based_on_tool_response(
95
+ self,
96
+ tool_response: ToolCallResponse,
97
+ ) -> list[EvaluationMetricName]:
98
+ """Return evaluation checks based on tool response"""
99
+ return []
100
+
101
+ async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
102
+ """Execute the MCP tool using SDK to call public API"""
103
+ self.logger.info(f"Running MCP tool: {self.name}")
104
+
105
+ # Notify progress if reporter is available
106
+ if self._tool_progress_reporter:
107
+ await self._tool_progress_reporter.notify_from_tool_call(
108
+ tool_call=tool_call,
109
+ name=f"**{self.display_name()}**",
110
+ message=f"Executing MCP tool: {self.display_name()}",
111
+ state=ProgressState.RUNNING,
112
+ )
113
+
114
+ try:
115
+ # Robust argument extraction and validation
116
+ arguments = self._extract_and_validate_arguments(tool_call)
117
+
118
+ # Use SDK to call the public API
119
+ result = await self._call_mcp_tool_via_sdk(arguments)
120
+
121
+ # Create successful response
122
+ tool_response = ToolCallResponse( # TODO: Why result here not applied directly to the body of the tool_response? like so how does it know the results in the history?
123
+ id=tool_call.id or "",
124
+ name=self.name,
125
+ debug_info={
126
+ "mcp_tool": self.name,
127
+ "arguments": arguments,
128
+ },
129
+ error_message="",
130
+ content=json.dumps(result),
131
+ )
132
+
133
+ # Notify completion
134
+ if self._tool_progress_reporter:
135
+ await self._tool_progress_reporter.notify_from_tool_call(
136
+ tool_call=tool_call,
137
+ name=f"**{self.display_name()}**",
138
+ message=f"MCP tool completed: {self.display_name()}",
139
+ state=ProgressState.FINISHED,
140
+ )
141
+
142
+ return tool_response
143
+
144
+ except Exception as e:
145
+ self.logger.error(f"Error executing MCP tool {self.name}: {e}")
146
+
147
+ # Notify failure
148
+ if self._tool_progress_reporter:
149
+ await self._tool_progress_reporter.notify_from_tool_call(
150
+ tool_call=tool_call,
151
+ name=f"**{self.display_name()}**",
152
+ message=f"MCP tool failed: {str(e)}",
153
+ state=ProgressState.FAILED,
154
+ )
155
+
156
+ return ToolCallResponse(
157
+ id=tool_call.id or "",
158
+ name=self.name,
159
+ debug_info={
160
+ "mcp_tool": self.name,
161
+ "error": str(e),
162
+ "original_arguments": getattr(tool_call, "arguments", None),
163
+ },
164
+ error_message=str(e),
165
+ )
166
+
167
+ def _extract_and_validate_arguments(
168
+ self, tool_call: LanguageModelFunction
169
+ ) -> Dict[str, Any]:
170
+ """
171
+ Extract and validate arguments from tool call, handling various formats robustly.
172
+
173
+ The arguments field can come in different formats:
174
+ 1. As a JSON string (expected format from OpenAI API)
175
+ 2. As a dictionary (from internal processing)
176
+ 3. As None or empty (edge cases)
177
+ """
178
+ raw_arguments = tool_call.arguments
179
+
180
+ # Handle None or empty arguments
181
+ if not raw_arguments:
182
+ self.logger.warning(f"MCP tool {self.name} called with empty arguments")
183
+ return {}
184
+
185
+ # Handle string arguments (JSON format)
186
+ if isinstance(raw_arguments, str):
187
+ try:
188
+ parsed_arguments = json.loads(raw_arguments)
189
+ if not isinstance(parsed_arguments, dict):
190
+ self.logger.warning(
191
+ f"MCP tool {self.name}: arguments JSON parsed to non-dict: {type(parsed_arguments)}"
192
+ )
193
+ return {}
194
+ return parsed_arguments
195
+ except json.JSONDecodeError as e:
196
+ self.logger.error(
197
+ f"MCP tool {self.name}: failed to parse arguments JSON '{raw_arguments}': {e}"
198
+ )
199
+ raise ValueError(
200
+ f"Invalid JSON arguments for MCP tool {self.name}: {e}"
201
+ )
202
+
203
+ # Handle dictionary arguments (already parsed)
204
+ if isinstance(raw_arguments, dict):
205
+ self.logger.debug(f"MCP tool {self.name}: arguments already in dict format")
206
+ return raw_arguments
207
+
208
+ # Handle unexpected argument types
209
+ self.logger.error(
210
+ f"MCP tool {self.name}: unexpected arguments type {type(raw_arguments)}: {raw_arguments}"
211
+ )
212
+ raise ValueError(
213
+ f"Unexpected arguments type for MCP tool {self.name}: {type(raw_arguments)}"
214
+ )
215
+
216
+ async def _call_mcp_tool_via_sdk(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
217
+ """Call MCP tool via SDK to public API"""
218
+ try:
219
+ result = unique_sdk.MCP.call_tool(
220
+ user_id=self.event.user_id,
221
+ company_id=self.event.company_id,
222
+ name=self.name,
223
+ arguments=arguments,
224
+ )
225
+
226
+ self.logger.info(
227
+ f"Calling MCP tool {self.name} with arguments: {arguments}"
228
+ )
229
+ self.logger.debug(f"Result: {result}")
230
+
231
+ return result
232
+ except Exception as e:
233
+ self.logger.error(f"SDK call failed for MCP tool {self.name}: {e}")
234
+ raise
@@ -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
+ )