unique_toolkit 0.8.21__tar.gz → 0.8.23__tar.gz

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.
Files changed (119) hide show
  1. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/CHANGELOG.md +6 -0
  2. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/PKG-INFO +7 -1
  3. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/pyproject.toml +1 -1
  4. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/infos.py +67 -0
  5. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/config.py +24 -0
  6. unique_toolkit-0.8.23/unique_toolkit/tools/mcp/__init__.py +4 -0
  7. unique_toolkit-0.8.23/unique_toolkit/tools/mcp/manager.py +82 -0
  8. unique_toolkit-0.8.23/unique_toolkit/tools/mcp/models.py +38 -0
  9. unique_toolkit-0.8.23/unique_toolkit/tools/mcp/tool_wrapper.py +278 -0
  10. unique_toolkit-0.8.23/unique_toolkit/tools/test/test_mcp_manager.py +399 -0
  11. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/tool_manager.py +21 -2
  12. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/LICENSE +0 -0
  13. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/README.md +0 -0
  14. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/__init__.py +0 -0
  15. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/_common/_base_service.py +0 -0
  16. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/_common/_time_utils.py +0 -0
  17. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/_common/default_language_model.py +0 -0
  18. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/_common/exception.py +0 -0
  19. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/_common/token/image_token_counting.py +0 -0
  20. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/_common/token/token_counting.py +0 -0
  21. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/_common/validate_required_values.py +0 -0
  22. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/_common/validators.py +0 -0
  23. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/__init__.py +0 -0
  24. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/dev_util.py +0 -0
  25. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/init_logging.py +0 -0
  26. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/init_sdk.py +0 -0
  27. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/performance/async_tasks.py +0 -0
  28. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/performance/async_wrapper.py +0 -0
  29. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/schemas.py +0 -0
  30. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/unique_settings.py +0 -0
  31. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/app/verification.py +0 -0
  32. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/chat/__init__.py +0 -0
  33. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/chat/constants.py +0 -0
  34. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/chat/functions.py +0 -0
  35. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/chat/schemas.py +0 -0
  36. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/chat/service.py +0 -0
  37. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/chat/state.py +0 -0
  38. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/chat/utils.py +0 -0
  39. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/content/__init__.py +0 -0
  40. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/content/constants.py +0 -0
  41. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/content/functions.py +0 -0
  42. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/content/schemas.py +0 -0
  43. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/content/service.py +0 -0
  44. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/content/utils.py +0 -0
  45. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/debug_info_manager/debug_info_manager.py +0 -0
  46. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/embedding/__init__.py +0 -0
  47. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/embedding/constants.py +0 -0
  48. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/embedding/functions.py +0 -0
  49. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/embedding/schemas.py +0 -0
  50. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/embedding/service.py +0 -0
  51. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/embedding/utils.py +0 -0
  52. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/config.py +0 -0
  53. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/context_relevancy/prompts.py +0 -0
  54. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/context_relevancy/schema.py +0 -0
  55. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/context_relevancy/service.py +0 -0
  56. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/evaluation_manager.py +0 -0
  57. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/exception.py +0 -0
  58. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/hallucination/constants.py +0 -0
  59. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/hallucination/hallucination_evaluation.py +0 -0
  60. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/hallucination/prompts.py +0 -0
  61. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/hallucination/service.py +0 -0
  62. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/hallucination/utils.py +0 -0
  63. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/output_parser.py +0 -0
  64. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/schemas.py +0 -0
  65. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/tests/test_context_relevancy_service.py +0 -0
  66. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evals/tests/test_output_parser.py +0 -0
  67. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/__init__.py +0 -0
  68. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/config.py +0 -0
  69. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/constants.py +0 -0
  70. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/context_relevancy/constants.py +0 -0
  71. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/context_relevancy/prompts.py +0 -0
  72. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/context_relevancy/service.py +0 -0
  73. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/context_relevancy/utils.py +0 -0
  74. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/exception.py +0 -0
  75. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/hallucination/constants.py +0 -0
  76. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/hallucination/prompts.py +0 -0
  77. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/hallucination/service.py +0 -0
  78. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/hallucination/utils.py +0 -0
  79. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/output_parser.py +0 -0
  80. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/evaluators/schemas.py +0 -0
  81. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/framework_utilities/langchain/client.py +0 -0
  82. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/framework_utilities/langchain/history.py +0 -0
  83. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/framework_utilities/openai/client.py +0 -0
  84. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/framework_utilities/openai/message_builder.py +0 -0
  85. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/framework_utilities/utils.py +0 -0
  86. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/history_manager/history_construction_with_contents.py +0 -0
  87. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/history_manager/history_manager.py +0 -0
  88. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/history_manager/loop_token_reducer.py +0 -0
  89. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/history_manager/utils.py +0 -0
  90. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/__init__.py +0 -0
  91. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/builder.py +0 -0
  92. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/constants.py +0 -0
  93. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/functions.py +0 -0
  94. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/prompt.py +0 -0
  95. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/reference.py +0 -0
  96. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/schemas.py +0 -0
  97. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/service.py +0 -0
  98. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/language_model/utils.py +0 -0
  99. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/postprocessor/postprocessor_manager.py +0 -0
  100. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/protocols/support.py +0 -0
  101. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/reference_manager/reference_manager.py +0 -0
  102. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/short_term_memory/__init__.py +0 -0
  103. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/short_term_memory/constants.py +0 -0
  104. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/short_term_memory/functions.py +0 -0
  105. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/short_term_memory/persistent_short_term_memory_manager.py +0 -0
  106. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/short_term_memory/schemas.py +0 -0
  107. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/short_term_memory/service.py +0 -0
  108. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/smart_rules/__init__.py +0 -0
  109. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/smart_rules/compile.py +0 -0
  110. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/thinking_manager/thinking_manager.py +0 -0
  111. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/factory.py +0 -0
  112. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/schemas.py +0 -0
  113. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/test/test_tool_progress_reporter.py +0 -0
  114. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/tool.py +0 -0
  115. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/tool_progress_reporter.py +0 -0
  116. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/utils/execution/execution.py +0 -0
  117. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/utils/source_handling/schema.py +0 -0
  118. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/utils/source_handling/source_formatting.py +0 -0
  119. {unique_toolkit-0.8.21 → unique_toolkit-0.8.23}/unique_toolkit/tools/utils/source_handling/tests/test_source_formatting.py +0 -0
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.23] - 2025-08-27
9
+ - Add MCP manager that handles MCP related logic
10
+
11
+ ## [0.8.22] - 2025-08-25
12
+ - Add DeepSeek-R1, DeepSeek-V3.1, Qwen3-235B-A22B and Qwen3-235B-A22B-Thinking-2507 to supported model list
13
+
8
14
  ## [0.8.21] - 2025-08-26
9
15
  - Fixed old (not used) function "create_async" in language_model.functions : The function always returns "Unauthorized" --> Added "user_id" argument to fix this.
10
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 0.8.21
3
+ Version: 0.8.23
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Martin Fadler
@@ -114,6 +114,12 @@ All notable changes to this project will be documented in this file.
114
114
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
115
115
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
116
116
 
117
+ ## [0.8.23] - 2025-08-27
118
+ - Add MCP manager that handles MCP related logic
119
+
120
+ ## [0.8.22] - 2025-08-25
121
+ - Add DeepSeek-R1, DeepSeek-V3.1, Qwen3-235B-A22B and Qwen3-235B-A22B-Thinking-2507 to supported model list
122
+
117
123
  ## [0.8.21] - 2025-08-26
118
124
  - Fixed old (not used) function "create_async" in language_model.functions : The function always returns "Unauthorized" --> Added "user_id" argument to fix this.
119
125
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "unique_toolkit"
3
- version = "0.8.21"
3
+ version = "0.8.23"
4
4
  description = ""
5
5
  authors = [
6
6
  "Martin Fadler <martin.fadler@unique.ch>",
@@ -48,6 +48,10 @@ class LanguageModelName(StrEnum):
48
48
  LITELLM_OPENAI_GPT_5_MINI = "litellm:openai-gpt-5-mini"
49
49
  LITELLM_OPENAI_GPT_5_NANO = "litellm:openai-gpt-5-nano"
50
50
  LITELLM_OPENAI_GPT_5_CHAT = "litellm:openai-gpt-5-chat"
51
+ LITELLM_DEEPSEEK_R1 = "litellm:deepseek-r1"
52
+ LITELLM_DEEPSEEK_V3 = "litellm:deepseek-v3-1"
53
+ LITELLM_QWEN_3 = "litellm:qwen-3-235B-A22B"
54
+ LITELLM_QWEN_3_THINKING = "litellm:qwen-3-235B-A22B-thinking"
51
55
 
52
56
 
53
57
  class EncoderName(StrEnum):
@@ -875,6 +879,69 @@ class LanguageModelInfo(BaseModel):
875
879
  deprecated_at=date(2026, 8, 7),
876
880
  retirement_at=date(2026, 8, 7),
877
881
  )
882
+ case LanguageModelName.LITELLM_DEEPSEEK_R1:
883
+ return cls(
884
+ name=model_name,
885
+ provider=LanguageModelProvider.LITELLM,
886
+ version="deepseek-r1",
887
+ capabilities=[
888
+ ModelCapabilities.FUNCTION_CALLING,
889
+ ModelCapabilities.STRUCTURED_OUTPUT,
890
+ ModelCapabilities.STREAMING,
891
+ ModelCapabilities.REASONING,
892
+ ],
893
+ token_limits=LanguageModelTokenLimits(
894
+ token_limit_input=64_000, token_limit_output=4_000
895
+ ),
896
+ published_at=date(2025, 1, 25),
897
+ )
898
+ case LanguageModelName.LITELLM_DEEPSEEK_V3:
899
+ return cls(
900
+ name=model_name,
901
+ provider=LanguageModelProvider.LITELLM,
902
+ version="deepseek-v3-1",
903
+ capabilities=[
904
+ ModelCapabilities.FUNCTION_CALLING,
905
+ ModelCapabilities.STRUCTURED_OUTPUT,
906
+ ModelCapabilities.REASONING,
907
+ ],
908
+ token_limits=LanguageModelTokenLimits(
909
+ token_limit_input=128_000, token_limit_output=4_000
910
+ ),
911
+ published_at=date(2025, 8, 1),
912
+ )
913
+ case LanguageModelName.LITELLM_QWEN_3:
914
+ return cls(
915
+ name=model_name,
916
+ provider=LanguageModelProvider.LITELLM,
917
+ version="qwen-3",
918
+ capabilities=[
919
+ ModelCapabilities.FUNCTION_CALLING,
920
+ ModelCapabilities.STRUCTURED_OUTPUT,
921
+ ModelCapabilities.STREAMING,
922
+ ModelCapabilities.REASONING,
923
+ ],
924
+ published_at=date(2025, 4, 29),
925
+ token_limits=LanguageModelTokenLimits(
926
+ token_limit_input=256_000, token_limit_output=32_768
927
+ ),
928
+ )
929
+ case LanguageModelName.LITELLM_QWEN_3_THINKING:
930
+ return cls(
931
+ name=model_name,
932
+ provider=LanguageModelProvider.LITELLM,
933
+ version="qwen-3-thinking",
934
+ capabilities=[
935
+ ModelCapabilities.FUNCTION_CALLING,
936
+ ModelCapabilities.STRUCTURED_OUTPUT,
937
+ ModelCapabilities.STREAMING,
938
+ ModelCapabilities.REASONING,
939
+ ],
940
+ token_limits=LanguageModelTokenLimits(
941
+ token_limit_input=256_000, token_limit_output=32_768
942
+ ),
943
+ published_at=date(2025, 7, 25),
944
+ )
878
945
 
879
946
  case _:
880
947
  if isinstance(model_name, LanguageModelName):
@@ -85,6 +85,22 @@ class ToolBuildConfig(BaseModel):
85
85
  if not isinstance(value, dict):
86
86
  return value
87
87
 
88
+ is_mcp_tool = value.get("mcp_source_id", "") != ""
89
+ mcp_configuration = value.get("configuration", {})
90
+
91
+ # Import at runtime to avoid circular imports
92
+ from unique_toolkit.tools.mcp.models import MCPToolConfig
93
+
94
+ if (
95
+ isinstance(mcp_configuration, MCPToolConfig)
96
+ and mcp_configuration.mcp_source_id
97
+ ):
98
+ return value
99
+ if is_mcp_tool:
100
+ # For MCP tools, skip ToolFactory validation
101
+ # Configuration can remain as a dict
102
+ return value
103
+
88
104
  configuration = value.get("configuration", {})
89
105
  if isinstance(configuration, dict):
90
106
  # Local import to avoid circular import at module import time
@@ -105,3 +121,11 @@ class ToolBuildConfig(BaseModel):
105
121
  config = configuration
106
122
  value["configuration"] = config
107
123
  return value
124
+
125
+
126
+ def _rebuild_config_model():
127
+ """Rebuild the ToolBuildConfig model to resolve forward references."""
128
+ # Import here to avoid circular imports
129
+ from unique_toolkit.tools.schemas import BaseToolConfig # noqa: F401
130
+
131
+ ToolBuildConfig.model_rebuild()
@@ -0,0 +1,4 @@
1
+ from .models import EnrichedMCPTool, MCPToolConfig
2
+ from .tool_wrapper import MCPToolWrapper
3
+
4
+ __all__ = ["MCPToolWrapper", "MCPToolConfig", "EnrichedMCPTool"]
@@ -0,0 +1,82 @@
1
+ import logging
2
+
3
+ from unique_toolkit.app.schemas import ChatEvent, McpServer, McpTool
4
+ from unique_toolkit.tools.mcp.models import EnrichedMCPTool, MCPToolConfig
5
+ from unique_toolkit.tools.mcp.tool_wrapper import MCPToolWrapper
6
+ from unique_toolkit.tools.tool_progress_reporter import ToolProgressReporter
7
+
8
+
9
+ class MCPManager:
10
+ def __init__(
11
+ self,
12
+ mcp_servers: list[McpServer],
13
+ event: ChatEvent,
14
+ tool_progress_reporter: ToolProgressReporter,
15
+ ):
16
+ self._mcp_servers = mcp_servers
17
+ self._event = event
18
+ self._tool_progress_reporter = tool_progress_reporter
19
+
20
+ def get_mcp_servers(self):
21
+ return self._mcp_servers
22
+
23
+ def get_mcp_server_by_id(self, id: str):
24
+ return next((server for server in self._mcp_servers if server.id == id), None)
25
+
26
+ def _enrich_tool_with_mcp_info(
27
+ self, mcp_tool: McpTool, server: McpServer
28
+ ) -> EnrichedMCPTool:
29
+ enriched_tool = type("EnrichedMcpTool", (), {})()
30
+
31
+ # Copy all attributes from the original tool
32
+ for attr in dir(mcp_tool):
33
+ if not attr.startswith("_"):
34
+ setattr(enriched_tool, attr, getattr(mcp_tool, attr))
35
+
36
+ # Add server-specific attributes
37
+ enriched_tool.server_id = server.id
38
+ enriched_tool.server_name = server.name
39
+ enriched_tool.server_system_prompt = getattr(server, "system_prompt", None)
40
+ enriched_tool.server_user_prompt = getattr(server, "user_prompt", None)
41
+ enriched_tool.mcp_source_id = server.id
42
+
43
+ return enriched_tool
44
+
45
+ def create_mcp_tool_wrapper(
46
+ self, mcp_tool: EnrichedMCPTool, tool_progress_reporter: ToolProgressReporter
47
+ ) -> MCPToolWrapper:
48
+ """Create MCP tool wrapper that behave like internal tools"""
49
+ try:
50
+ config = MCPToolConfig(
51
+ server_id=mcp_tool.server_id,
52
+ server_name=mcp_tool.server_name,
53
+ server_system_prompt=mcp_tool.server_system_prompt,
54
+ server_user_prompt=mcp_tool.server_user_prompt,
55
+ mcp_source_id=mcp_tool.mcp_source_id,
56
+ )
57
+ wrapper = MCPToolWrapper(
58
+ mcp_tool=mcp_tool,
59
+ config=config,
60
+ event=self._event,
61
+ tool_progress_reporter=tool_progress_reporter,
62
+ )
63
+ return wrapper
64
+ except Exception as e:
65
+ import traceback
66
+
67
+ logging.error(f"Error creating MCP tool wrapper for {mcp_tool.name}: {e}")
68
+ logging.error(f"Full traceback: {traceback.format_exc()}")
69
+ return None
70
+
71
+ def get_all_mcp_tools(self, selected_by_user: list[str]) -> list[MCPToolWrapper]:
72
+ selected_tools = []
73
+ for server in self._mcp_servers:
74
+ if hasattr(server, "tools") and server.tools:
75
+ for tool in server.tools:
76
+ enriched_tool = self._enrich_tool_with_mcp_info(tool, server)
77
+ wrapper = self.create_mcp_tool_wrapper(
78
+ enriched_tool, self._tool_progress_reporter
79
+ )
80
+ if wrapper is not None:
81
+ selected_tools.append(wrapper)
82
+ return selected_tools
@@ -0,0 +1,38 @@
1
+ from typing import Any, Dict, Optional, Protocol
2
+
3
+ from unique_toolkit.tools.schemas import BaseToolConfig
4
+
5
+
6
+ class MCPTool(Protocol):
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 EnrichedMCPTool(MCPTool, Protocol):
22
+ """Protocol for MCP tools enriched with server information."""
23
+
24
+ server_id: str
25
+ server_name: str
26
+ server_system_prompt: Optional[str]
27
+ server_user_prompt: Optional[str]
28
+ mcp_source_id: str
29
+
30
+
31
+ class MCPToolConfig(BaseToolConfig):
32
+ """Configuration for MCP tools"""
33
+
34
+ server_id: str
35
+ server_name: str
36
+ server_system_prompt: Optional[str] = None
37
+ server_user_prompt: Optional[str] = None
38
+ mcp_source_id: str
@@ -0,0 +1,278 @@
1
+ import json
2
+ from typing import Any, Dict
3
+
4
+ import unique_sdk
5
+ from pydantic import BaseModel, Field, create_model
6
+
7
+ from unique_toolkit.app.schemas import ChatEvent
8
+ from unique_toolkit.evals.schemas import EvaluationMetricName
9
+ from unique_toolkit.language_model import LanguageModelMessage
10
+ from unique_toolkit.language_model.schemas import (
11
+ LanguageModelFunction,
12
+ LanguageModelToolDescription,
13
+ LanguageModelToolMessage,
14
+ )
15
+ from unique_toolkit.tools.mcp.models import EnrichedMCPTool, MCPToolConfig
16
+ from unique_toolkit.tools.schemas import ToolCallResponse
17
+ from unique_toolkit.tools.tool import Tool
18
+ from unique_toolkit.tools.tool_progress_reporter import (
19
+ ProgressState,
20
+ ToolProgressReporter,
21
+ )
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_tool: EnrichedMCPTool,
30
+ config: MCPToolConfig,
31
+ event: ChatEvent,
32
+ tool_progress_reporter: ToolProgressReporter | None = None,
33
+ ):
34
+ self.name = mcp_tool.name
35
+ super().__init__(config, event, tool_progress_reporter)
36
+ self.mcp_tool = mcp_tool
37
+ self._tool_description = mcp_tool.description or ""
38
+ self._parameters_schema = mcp_tool.input_schema
39
+
40
+ # Set the display name for user-facing messages
41
+ # Priority: title > annotations.title > name
42
+ self.display_name = (
43
+ getattr(mcp_tool, "title", None)
44
+ or (getattr(mcp_tool, "annotations", {}) or {}).get("title")
45
+ or mcp_tool.name
46
+ )
47
+
48
+ def tool_description(self) -> LanguageModelToolDescription:
49
+ """Convert MCP tool schema to LanguageModelToolDescription"""
50
+ # Create a Pydantic model from the MCP tool's input schema
51
+ parameters_model = self._create_parameters_model()
52
+
53
+ return LanguageModelToolDescription(
54
+ name=self.name,
55
+ description=self._tool_description,
56
+ parameters=parameters_model,
57
+ )
58
+
59
+ def _create_parameters_model(self) -> type[BaseModel]:
60
+ """Create a Pydantic model from MCP tool's input schema"""
61
+ properties = self._parameters_schema.get("properties", {})
62
+ required_fields = self._parameters_schema.get("required", [])
63
+
64
+ # Convert JSON schema properties to Pydantic fields
65
+ fields = {}
66
+ for prop_name, prop_schema in properties.items():
67
+ field_type = self._json_schema_to_python_type(prop_schema)
68
+ field_description = prop_schema.get("description", "")
69
+
70
+ if prop_name in required_fields:
71
+ fields[prop_name] = (
72
+ field_type,
73
+ Field(description=field_description),
74
+ )
75
+ else:
76
+ fields[prop_name] = (
77
+ field_type,
78
+ Field(default=None, description=field_description),
79
+ )
80
+
81
+ # Create dynamic model
82
+ return create_model(f"{self.name}Parameters", **fields)
83
+
84
+ def _json_schema_to_python_type(self, schema: Dict[str, Any]) -> type:
85
+ """Convert JSON schema type to Python type"""
86
+ json_type = schema.get("type", "string")
87
+
88
+ type_mapping = {
89
+ "string": str,
90
+ "integer": int,
91
+ "number": float,
92
+ "boolean": bool,
93
+ "array": list,
94
+ "object": dict,
95
+ }
96
+
97
+ return type_mapping.get(json_type, str)
98
+
99
+ def display_name(self) -> str:
100
+ """The display name of the tool."""
101
+ return self._display_name
102
+
103
+ def tool_description_for_system_prompt(self) -> str:
104
+ """Return tool description for system prompt"""
105
+ return self._tool_description
106
+
107
+ def tool_format_information_for_system_prompt(self) -> str:
108
+ """Return formatting information for system prompt"""
109
+ return f"Use this MCP tool to {self._tool_description.lower()}"
110
+
111
+ def evaluation_check_list(self) -> list[EvaluationMetricName]:
112
+ """Return evaluation check list - empty for MCP tools for now"""
113
+ return []
114
+
115
+ def get_evaluation_checks_based_on_tool_response(
116
+ self,
117
+ tool_response: ToolCallResponse,
118
+ ) -> list[EvaluationMetricName]:
119
+ """Return evaluation checks based on tool response"""
120
+ return []
121
+
122
+ def get_tool_call_result_for_loop_history(
123
+ self,
124
+ tool_response: ToolCallResponse,
125
+ ) -> LanguageModelMessage:
126
+ """Convert tool response to message for loop history"""
127
+ # Convert the tool response to a message for the conversation history
128
+ content = (
129
+ tool_response.error_message
130
+ if tool_response.error_message
131
+ else "Tool executed successfully"
132
+ )
133
+
134
+ if hasattr(tool_response, "content") and tool_response.content:
135
+ content = str(tool_response.content)
136
+ elif tool_response.debug_info:
137
+ content = json.dumps(tool_response.debug_info)
138
+
139
+ return LanguageModelToolMessage(
140
+ content=content,
141
+ tool_call_id=tool_response.id,
142
+ name=tool_response.name,
143
+ )
144
+
145
+ async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
146
+ """Execute the MCP tool using SDK to call public API"""
147
+ self.logger.info(f"Running MCP tool: {self.name}")
148
+
149
+ # Notify progress if reporter is available
150
+ if self.tool_progress_reporter:
151
+ await self.tool_progress_reporter.notify_from_tool_call(
152
+ tool_call=tool_call,
153
+ name=f"**{self.display_name}**",
154
+ message=f"Executing MCP tool: {self.display_name}",
155
+ state=ProgressState.RUNNING,
156
+ )
157
+
158
+ try:
159
+ # Robust argument extraction and validation
160
+ arguments = self._extract_and_validate_arguments(tool_call)
161
+
162
+ # Use SDK to call the public API
163
+ result = await self._call_mcp_tool_via_sdk(arguments)
164
+
165
+ # Create successful response
166
+ tool_response = ToolCallResponse(
167
+ id=tool_call.id or "",
168
+ name=self.name,
169
+ debug_info={
170
+ "mcp_tool": self.name,
171
+ "arguments": arguments,
172
+ "result": result,
173
+ },
174
+ error_message="",
175
+ )
176
+
177
+ # Notify completion
178
+ if self.tool_progress_reporter:
179
+ await self.tool_progress_reporter.notify_from_tool_call(
180
+ tool_call=tool_call,
181
+ name=f"**{self.display_name}**",
182
+ message=f"MCP tool completed: {self.display_name}",
183
+ state=ProgressState.FINISHED,
184
+ )
185
+
186
+ return tool_response
187
+
188
+ except Exception as e:
189
+ self.logger.error(f"Error executing MCP tool {self.name}: {e}")
190
+
191
+ # Notify failure
192
+ if self.tool_progress_reporter:
193
+ await self.tool_progress_reporter.notify_from_tool_call(
194
+ tool_call=tool_call,
195
+ name=f"**{self.display_name}**",
196
+ message=f"MCP tool failed: {str(e)}",
197
+ state=ProgressState.FAILED,
198
+ )
199
+
200
+ return ToolCallResponse(
201
+ id=tool_call.id or "",
202
+ name=self.name,
203
+ debug_info={
204
+ "mcp_tool": self.name,
205
+ "error": str(e),
206
+ "original_arguments": getattr(tool_call, "arguments", None),
207
+ },
208
+ error_message=str(e),
209
+ )
210
+
211
+ def _extract_and_validate_arguments(
212
+ self, tool_call: LanguageModelFunction
213
+ ) -> Dict[str, Any]:
214
+ """
215
+ Extract and validate arguments from tool call, handling various formats robustly.
216
+
217
+ The arguments field can come in different formats:
218
+ 1. As a JSON string (expected format from OpenAI API)
219
+ 2. As a dictionary (from internal processing)
220
+ 3. As None or empty (edge cases)
221
+ """
222
+ raw_arguments = tool_call.arguments
223
+
224
+ # Handle None or empty arguments
225
+ if not raw_arguments:
226
+ self.logger.warning(f"MCP tool {self.name} called with empty arguments")
227
+ return {}
228
+
229
+ # Handle string arguments (JSON format)
230
+ if isinstance(raw_arguments, str):
231
+ try:
232
+ parsed_arguments = json.loads(raw_arguments)
233
+ if not isinstance(parsed_arguments, dict):
234
+ self.logger.warning(
235
+ f"MCP tool {self.name}: arguments JSON parsed to non-dict: {type(parsed_arguments)}"
236
+ )
237
+ return {}
238
+ return parsed_arguments
239
+ except json.JSONDecodeError as e:
240
+ self.logger.error(
241
+ f"MCP tool {self.name}: failed to parse arguments JSON '{raw_arguments}': {e}"
242
+ )
243
+ raise ValueError(
244
+ f"Invalid JSON arguments for MCP tool {self.name}: {e}"
245
+ )
246
+
247
+ # Handle dictionary arguments (already parsed)
248
+ if isinstance(raw_arguments, dict):
249
+ self.logger.debug(f"MCP tool {self.name}: arguments already in dict format")
250
+ return raw_arguments
251
+
252
+ # Handle unexpected argument types
253
+ self.logger.error(
254
+ f"MCP tool {self.name}: unexpected arguments type {type(raw_arguments)}: {raw_arguments}"
255
+ )
256
+ raise ValueError(
257
+ f"Unexpected arguments type for MCP tool {self.name}: {type(raw_arguments)}"
258
+ )
259
+
260
+ async def _call_mcp_tool_via_sdk(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
261
+ """Call MCP tool via SDK to public API"""
262
+ try:
263
+ result = unique_sdk.MCP.call_tool(
264
+ user_id=self.event.user_id,
265
+ company_id=self.event.company_id,
266
+ name=self.name,
267
+ arguments=arguments,
268
+ )
269
+
270
+ self.logger.info(
271
+ f"Calling MCP tool {self.name} with arguments: {arguments}"
272
+ )
273
+ self.logger.debug(f"Result: {result}")
274
+
275
+ return result
276
+ except Exception as e:
277
+ self.logger.error(f"SDK call failed for MCP tool {self.name}: {e}")
278
+ raise