Spaces:
Running
Running
File size: 9,402 Bytes
3f0377e e2e01e0 3f0377e e2e01e0 3f0377e e2e01e0 3f0377e e2e01e0 3f0377e e2e01e0 3f0377e e2e01e0 3f0377e 921a78a 3f0377e 921a78a 3f0377e 921a78a 3f0377e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
"""
標準化 MCP 工具基類
定義統一的輸入輸出格式和錯誤處理
"""
import json
import logging
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional, List
from datetime import datetime
try:
import jsonschema
except ImportError:
jsonschema = None
logger = logging.getLogger("mcp.tools.base")
class ToolError(Exception):
"""工具錯誤基類"""
def __init__(self, code: str, message: str, details: Optional[Dict[str, Any]] = None):
self.code = code
self.message = message
self.details = details or {}
super().__init__(message)
class ValidationError(ToolError):
"""參數驗證錯誤"""
def __init__(self, field: str, message: str):
super().__init__(
code="VALIDATION_ERROR",
message=f"參數 '{field}' 驗證失敗: {message}",
details={"field": field}
)
class ExecutionError(ToolError):
"""執行錯誤"""
def __init__(self, message: str, cause: Optional[Exception] = None):
super().__init__(
code="EXECUTION_ERROR",
message=message,
details={"cause": str(cause) if cause else None}
)
class MCPTool(ABC):
"""標準化 MCP 工具基類"""
# 工具基本信息
NAME: str = ""
DESCRIPTION: str = ""
DESCRIPTION_SHORT: str = "" # 簡短描述(用於 Intent Detection,10-20 tokens)
CATEGORY: str = "general"
TAGS: List[str] = []
KEYWORDS: List[str] = [] # 用於快速意圖匹配的關鍵字
USAGE_TIPS: List[str] = []
IS_COMPLEX: bool = False # 標記是否為複雜工具(需要兩階段參數填充)
@classmethod
def get_summary(cls) -> Dict[str, Any]:
"""獲取工具摘要(用於 Intent Detection,減少 token 消耗)"""
# 自動生成簡短描述(截取前 50 字元或使用自訂)
short_desc = cls.DESCRIPTION_SHORT or (
cls.DESCRIPTION[:47] + "..." if len(cls.DESCRIPTION) > 50 else cls.DESCRIPTION
)
summary = {
"name": cls.NAME,
"description": short_desc,
"category": cls.CATEGORY,
"keywords": cls.KEYWORDS,
"is_complex": cls.IS_COMPLEX
}
# 簡單工具:提供簡化參數列表(只有參數名,不含詳細 schema)
if not cls.IS_COMPLEX:
try:
schema = cls.get_input_schema()
summary["params"] = list(schema.get("properties", {}).keys())
except:
summary["params"] = []
return summary
@classmethod
def get_full_definition(cls) -> Dict[str, Any]:
"""獲取完整工具定義(用於實際調用,包含完整 schema)"""
return {
"name": cls.NAME,
"description": cls.DESCRIPTION,
"metadata": {
"category": cls.CATEGORY,
"tags": cls.TAGS,
"keywords": cls.KEYWORDS,
"is_complex": cls.IS_COMPLEX,
"usage_tips": cls.USAGE_TIPS
},
"inputSchema": cls.get_input_schema(),
"outputSchema": cls.get_output_schema()
}
@classmethod
def get_definition(cls) -> Dict[str, Any]:
"""獲取工具定義(向後兼容,使用完整定義)"""
return cls.get_full_definition()
@classmethod
@abstractmethod
def get_input_schema(cls) -> Dict[str, Any]:
"""獲取輸入參數模式"""
pass
@classmethod
@abstractmethod
def get_output_schema(cls) -> Dict[str, Any]:
"""獲取輸出結果模式"""
pass
@classmethod
def validate_input(cls, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""驗證和標準化輸入參數"""
try:
# 修復:確保 arguments 是 dict 類型
if not isinstance(arguments, dict):
logger.error(f"工具 {cls.NAME} 收到非 dict 類型的參數: {type(arguments).__name__} = {arguments}")
raise ValidationError("arguments", f"參數必須是 dict 類型,但收到: {type(arguments).__name__}")
# 使用 JSON Schema 進行驗證
import jsonschema
schema = cls.get_input_schema()
jsonschema.validate(arguments, schema)
# 應用默認值
validated_args = cls._apply_defaults(arguments, schema)
return validated_args
except jsonschema.ValidationError as e:
raise ValidationError(e.absolute_path[0] if e.absolute_path else "unknown", str(e))
except Exception as e:
raise ValidationError("unknown", f"輸入驗證失敗: {str(e)}")
@classmethod
def _apply_defaults(cls, arguments: Dict[str, Any], schema: Dict[str, Any]) -> Dict[str, Any]:
"""應用默認值"""
result = arguments.copy()
properties = schema.get("properties", {})
for prop_name, prop_schema in properties.items():
if prop_name not in result and "default" in prop_schema:
result[prop_name] = prop_schema["default"]
return result
@classmethod
async def execute_safe(cls, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""安全執行工具,包含錯誤處理"""
try:
# 驗證輸入
validated_args = cls.validate_input(arguments)
# 執行工具
start_time = datetime.now()
result = await cls.execute(validated_args)
end_time = datetime.now()
# 驗證輸出
validated_result = cls.validate_output(result)
# 添加元數據
validated_result["metadata"] = validated_result.get("metadata", {})
validated_result["metadata"].update({
"tool_name": cls.NAME,
"execution_time": (end_time - start_time).total_seconds(),
"timestamp": end_time.isoformat()
})
return validated_result
except ToolError:
raise # 重新拋出工具錯誤
except Exception as e:
import traceback
logger.error(f"工具 {cls.NAME} 執行失敗: {e}")
logger.error(f"完整 traceback:\n{traceback.format_exc()}")
raise ExecutionError(f"工具執行失敗: {str(e)}", e)
@classmethod
def validate_output(cls, result: Dict[str, Any]) -> Dict[str, Any]:
"""驗證輸出結果"""
if jsonschema is None:
logger.warning(f"jsonschema 未安裝,跳過輸出驗證")
return result
try:
schema = cls.get_output_schema()
jsonschema.validate(result, schema)
return result
except jsonschema.ValidationError as e:
logger.warning(f"工具 {cls.NAME} 輸出格式不符合規範: {e}")
# 不拋出錯誤,只記錄警告,允許繼續執行
return result
except Exception as e:
logger.warning(f"工具 {cls.NAME} 輸出驗證失敗: {e}")
return result
@classmethod
@abstractmethod
async def execute(cls, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""執行工具邏輯"""
pass
@classmethod
def create_success_response(cls, content: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""創建成功的回應"""
response = {
"success": True,
"content": content,
"metadata": {
"tool_name": cls.NAME,
"category": cls.CATEGORY
}
}
if data:
response.update(data)
return response
@classmethod
def create_error_response(cls, error: str, code: str = "UNKNOWN_ERROR") -> Dict[str, Any]:
"""創建錯誤的回應"""
return {
"success": False,
"error": error,
"error_code": code,
"metadata": {
"tool_name": cls.NAME,
"category": cls.CATEGORY
}
}
class StandardToolSchemas:
"""標準工具模式定義"""
@staticmethod
def create_input_schema(properties: Dict[str, Any], required: Optional[List[str]] = None) -> Dict[str, Any]:
"""創建標準輸入模式"""
schema = {
"type": "object",
"properties": properties
}
if required:
schema["required"] = required
return schema
@staticmethod
def create_output_schema() -> Dict[str, Any]:
"""創建標準輸出模式"""
return {
"type": "object",
"properties": {
"success": {"type": "boolean"},
"content": {"type": "string"},
"error": {"type": ["string", "null"]},
"error_code": {"type": ["string", "null"]},
"metadata": {
"type": "object",
"properties": {
"tool_name": {"type": "string"},
"category": {"type": "string"},
"execution_time": {"type": "number"},
"timestamp": {"type": "string"}
}
}
},
"required": ["success"]
} |