Coverage for merco/tools/errors.py: 57%
61 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 14:04 +0800
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 14:04 +0800
1"""工具执行错误处理 — 把异常转为 LLM 可消费的结构化 dict。
3边界:错误分类 + 公共消息脱敏。LLM 错误(APIStatusError 等)由 core.llm.errors 处理。
4"""
5from __future__ import annotations
7import logging
8from typing import Any
10logger = logging.getLogger("merco.tools.errors")
13ERROR_CATEGORIES = {
14 "param_mismatch": "参数不匹配",
15 "tool_not_found": "工具不存在",
16 "timeout": "执行超时",
17 "permission": "权限不足",
18 "network": "网络错误",
19 "resource_not_found": "资源不存在",
20 "internal": "内部错误",
21 "unknown": "未知错误",
22}
25def classify_error(exc: Exception) -> str:
26 """将异常映射到错误类别"""
27 if isinstance(exc, TypeError):
28 return "param_mismatch"
29 if isinstance(exc, TimeoutError):
30 return "timeout"
31 if isinstance(exc, PermissionError):
32 return "permission"
33 if isinstance(exc, FileNotFoundError):
34 return "resource_not_found"
35 if isinstance(exc, ConnectionError):
36 return "network"
37 if isinstance(exc, OSError):
38 return "network"
39 if isinstance(exc, ValueError):
40 return "param_mismatch"
41 return "unknown"
44def tool_error(
45 exc: Exception,
46 tool_name: str,
47 tool_schema: dict | None = None,
48) -> dict:
49 """将工具执行异常转为 LLM 可读的结构化错误"""
50 category = classify_error(exc)
51 label = ERROR_CATEGORIES.get(category, "未知错误")
52 result: dict[str, Any] = {
53 "error": f"[{label}] {tool_name}: {_public_message(exc)}",
54 "category": category,
55 "tool": tool_name,
56 }
57 if category == "param_mismatch":
58 hint = _params_hint(tool_schema)
59 result["suggestion"] = f"参数类型或值不正确。{hint}"
60 if tool_schema:
61 result["available_params"] = _param_names(tool_schema)
62 elif category == "tool_not_found":
63 result["suggestion"] = "该工具不可用。请使用其他可用工具完成用户请求。"
64 elif category == "timeout":
65 result["suggestion"] = "操作超时。可尝试减少数据量、拆分请求,或使用其他工具。"
66 elif category == "permission":
67 result["suggestion"] = "权限不足。请检查文件权限,或使用其他路径/工具。"
68 elif category == "network":
69 result["suggestion"] = "网络请求失败。可重试,或检查 URL 是否正确。"
70 elif category == "resource_not_found":
71 result["suggestion"] = "资源(文件/路径)不存在。请检查路径拼写,或搜索确认位置。"
72 else:
73 result["suggestion"] = "执行时发生意外错误。请尝试其他方式完成用户请求。"
74 logger.warning("工具 %s 未知异常", tool_name, exc_info=True)
75 return result
78def empty_response() -> dict:
79 """空回复错误 — 回调 LLM 让它产出实际内容"""
80 return {
81 "error": "[空回复] 你既没有回复用户也没有调用工具。"
82 "请直接回答用户,或使用工具推进任务。",
83 "category": "empty_response",
84 "suggestion": "请直接回复用户,或调用工具获取信息。",
85 }
88def _public_message(exc: Exception) -> str:
89 msg = str(exc)
90 if len(msg) > 300:
91 msg = msg[:300] + "..."
92 return msg
95def _params_hint(schema: dict | None) -> str:
96 if not schema:
97 return "请检查工具调用参数。"
98 names = _param_names(schema)
99 required = schema.get("required", [])
100 if required:
101 return f"必需: {', '.join(required)}。可用: {', '.join(names)}。"
102 return f"可用参数: {', '.join(names)}。"
105def _param_names(schema: dict) -> list[str]:
106 props = schema.get("properties", {})
107 return list(props.keys()) if isinstance(props, dict) else []