Bloom_Ware / core /tool_registry.py
LawrenceBai's picture
feat: overhaul MCP architecture with structured tool schemas, comprehensive care-mode skill definitions, and enhanced test coverage for pipelines and service integration.
79df050
Raw
History Blame Contribute Delete
11.4 kB
"""
工具註冊中心
統一管理 MCP 工具的 OpenAI Function Calling Schema
2025 最佳實踐:讓 GPT 原生選擇工具,不需要自定義意圖檢測 Prompt
重構版本:整合 Pydantic Schema 自動生成
"""
import inspect
from typing import Dict, List, Any, Optional, Callable, Type
from dataclasses import dataclass, field
from core.logging import get_logger
from core.tool_schema import (
ToolSchema,
ToolMetadata,
ToolSchemaRegistry,
tool_schema_registry,
extract_schema_from_mcp_tool,
)
from features.mcp.tools.base_tool import MCPTool
logger = get_logger("core.tool_registry")
@dataclass
class ToolDefinition:
"""工具定義(向後兼容)"""
name: str
description: str
parameters: Dict[str, Any]
handler: Optional[Callable] = None
category: str = "general"
requires_auth: bool = False
requires_location: bool = False
keywords: List[str] = field(default_factory=list)
examples: List[str] = field(default_factory=list)
class ToolRegistry:
"""
工具註冊中心(重構版)
功能:
1. 統一註冊所有 MCP 工具
2. 自動從 MCPTool 類別生成 OpenAI Function Calling Schema
3. 支援工具分類和過濾
4. 動態啟用/停用工具
5. 整合 ToolSchemaRegistry 提供 Pydantic 支援
"""
def __init__(self):
self._tools: Dict[str, ToolDefinition] = {}
self._disabled_tools: set = set()
# 整合新的 Schema Registry
self._schema_registry = tool_schema_registry
def register(
self,
name: str,
description: str,
parameters: Dict[str, Any],
handler: Optional[Callable] = None,
category: str = "general",
requires_auth: bool = False,
requires_location: bool = False,
keywords: Optional[List[str]] = None,
examples: Optional[List[str]] = None,
) -> None:
"""註冊工具(向後兼容 + 自動同步到 Schema Registry)"""
self._tools[name] = ToolDefinition(
name=name,
description=description,
parameters=parameters,
handler=handler,
category=category,
requires_auth=requires_auth,
requires_location=requires_location,
keywords=keywords or [],
examples=examples or [],
)
# 同步到 Schema Registry
schema = ToolSchema(
metadata=ToolMetadata(
name=name,
description=description,
category=category,
keywords=keywords or [],
examples=examples or [],
requires_location=requires_location,
requires_auth=requires_auth,
),
input_schema=parameters,
handler=handler,
)
self._schema_registry.register(schema)
logger.debug(f"註冊工具: {name}")
def register_mcp_tool(self, tool_class: Type) -> bool:
"""
從 MCPTool 類別自動註冊工具
Args:
tool_class: MCPTool 子類別
Returns:
是否註冊成功
"""
schema = extract_schema_from_mcp_tool(tool_class)
if not schema:
return False
# 註冊到 Schema Registry
self._schema_registry.register(schema)
# 同步到舊的 _tools(向後兼容)
self._tools[schema.metadata.name] = ToolDefinition(
name=schema.metadata.name,
description=schema.metadata.description,
parameters=schema.input_schema,
handler=schema.handler,
category=schema.metadata.category,
requires_auth=schema.metadata.requires_auth,
requires_location=schema.metadata.requires_location,
keywords=schema.metadata.keywords,
examples=schema.metadata.examples,
)
logger.debug(f"從 MCPTool 註冊工具: {schema.metadata.name}")
return True
def unregister(self, name: str) -> bool:
"""取消註冊工具"""
if name in self._tools:
del self._tools[name]
self._schema_registry.unregister(name)
return True
return False
def disable(self, name: str) -> None:
"""停用工具"""
self._disabled_tools.add(name)
self._schema_registry.disable(name)
def enable(self, name: str) -> None:
"""啟用工具"""
self._disabled_tools.discard(name)
self._schema_registry.enable(name)
def get_tool(self, name: str) -> Optional[ToolDefinition]:
"""取得工具定義"""
if name in self._disabled_tools:
return None
return self._tools.get(name)
def get_openai_tools(
self,
categories: Optional[List[str]] = None,
include_location_tools: bool = True,
strict: bool = True,
) -> List[Dict[str, Any]]:
"""
生成 OpenAI Function Calling 格式的工具列表
Args:
categories: 只包含指定分類的工具
include_location_tools: 是否包含需要位置的工具
strict: 是否啟用 strict mode(確保輸出符合 schema)
Returns:
OpenAI tools 格式的列表
"""
# 優先使用 Schema Registry(支援 strict mode)
return self._schema_registry.get_openai_tools(
categories=categories,
include_location_tools=include_location_tools,
strict=strict,
)
def get_openai_tools_legacy(
self,
categories: Optional[List[str]] = None,
include_location_tools: bool = True,
) -> List[Dict[str, Any]]:
"""
生成 OpenAI Function Calling 格式的工具列表(舊版,不支援 strict mode)
"""
tools = []
for name, tool in self._tools.items():
# 跳過停用的工具
if name in self._disabled_tools:
continue
# 分類過濾
if categories and tool.category not in categories:
continue
# 位置過濾
if not include_location_tools and tool.requires_location:
continue
tools.append({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters,
}
})
return tools
def get_tool_names(self) -> List[str]:
"""取得所有已註冊的工具名稱"""
return [
name for name in self._tools.keys()
if name not in self._disabled_tools
]
def get_stats(self) -> Dict[str, Any]:
"""取得統計資訊"""
return self._schema_registry.get_stats()
def get_summaries(self) -> List[Dict[str, Any]]:
"""取得所有工具摘要(用於快速意圖匹配)"""
return self._schema_registry.get_summaries()
# 全域單例
tool_registry = ToolRegistry()
def register_mcp_tools_to_registry(mcp_server) -> int:
"""
從 MCP Server 自動註冊工具到 Registry
2025 重構版:優先使用 MCPTool 類別自動提取 Schema
Args:
mcp_server: MCPServer 實例
Returns:
註冊的工具數量
"""
count = 0
for tool_name, tool in mcp_server.tools.items():
# 1. 嘗試獲取工具類別
tool_class = None
if hasattr(tool, 'handler') and hasattr(tool.handler, '__self__'):
tool_class = type(tool.handler.__self__)
elif hasattr(tool, 'handler') and hasattr(tool.handler, '__closure__') and tool.handler.__closure__:
# 嘗試從閉包中找 (例如 classmethod_wrapper or instance_wrapper)
for cell in tool.handler.__closure__:
try:
contents = cell.cell_contents
# 檢查是否為 MCPTool 類別或實例 (使用鴨子類型,避免模組導入路徑不一致問題)
if inspect.isclass(contents) and hasattr(contents, 'get_input_schema') and hasattr(contents, 'NAME'):
tool_class = contents
break
elif not inspect.isclass(contents) and hasattr(contents, 'get_input_schema') and hasattr(contents, 'NAME'):
tool_class = type(contents)
break
except:
continue
# 2. 如果能找到類別,使用 register_mcp_tool (這會處理 rich description)
if tool_class and tool_registry.register_mcp_tool(tool_class):
count += 1
continue
# 3. 降級:手動提取並註冊
description = getattr(tool, 'description', f'{tool_name} 工具')
parameters = getattr(tool, 'inputSchema', {"type": "object", "properties": {}, "required": []})
output_schema = getattr(tool, 'outputSchema', None)
# 嘗試從 handler 閉包中找 tool_class (如果有的話)
# 或者從 tool.metadata 找
keywords = []
examples = []
if hasattr(tool, 'metadata') and tool.metadata:
keywords = tool.metadata.get('keywords', [])
examples = tool.metadata.get('usage_tips', []) or tool.metadata.get('examples', [])
category = tool.metadata.get('category', 'general')
else:
category = _infer_category(tool_name)
# 判斷是否需要位置
requires_location = _requires_location(tool_name, parameters)
tool_registry.register(
name=tool_name,
description=description,
parameters=parameters,
handler=getattr(tool, 'handler', None),
category=category,
requires_location=requires_location,
keywords=keywords,
examples=examples,
)
count += 1
logger.info(f"從 MCP Server 註冊了 {count} 個工具")
return count
def _infer_category(tool_name: str) -> str:
"""推斷工具分類"""
name_lower = tool_name.lower()
if any(k in name_lower for k in ['weather', 'forecast']):
return "weather"
if any(k in name_lower for k in ['bus', 'train', 'metro', 'thsr', 'youbike', 'parking']):
return "transportation"
if any(k in name_lower for k in ['geocode', 'directions', 'location']):
return "location"
if any(k in name_lower for k in ['news']):
return "information"
if any(k in name_lower for k in ['exchange', 'currency']):
return "finance"
if any(k in name_lower for k in ['health', 'heart', 'sleep', 'step']):
return "health"
return "general"
def _requires_location(tool_name: str, parameters: Dict) -> bool:
"""判斷工具是否需要位置資訊"""
# 檢查參數中是否有 lat/lon
props = parameters.get("properties", {})
if "lat" in props or "lon" in props or "latitude" in props or "longitude" in props:
return True
# 檢查工具名稱
location_tools = [
'reverse_geocode', 'directions', 'tdx_bus_arrival',
'tdx_youbike', 'tdx_metro', 'tdx_parking', 'tdx_train', 'tdx_thsr'
]
return tool_name in location_tools