import re import hashlib from typing import Iterable, List, Tuple, Any TOOL_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]{1,64}$") class ToolValidationError(Exception): pass def sanitize_tool_name(name: str, max_len: int = 64) -> str: safe = re.sub(r"[^a-zA-Z0-9_-]", "_", name or "tool") if len(safe) > max_len: suffix = hashlib.sha1(safe.encode()).hexdigest()[:8] safe = safe[: max_len - 9] + "_" + suffix return safe or "tool" def _find_illegal_chars(name: str) -> List[str]: return sorted(list(set(re.findall(r"[^a-zA-Z0-9_-]", name)))) def _get_name(obj: Any) -> str: return getattr(obj, "name", None) or getattr(obj, "tool_name", None) or getattr(obj, "__name__", "tool") def validate_tool_names( tools: Iterable[Any], *, strict: bool = True, max_len: int = 64 ) -> Tuple[bool, List[str]]: """ Validate that tool names conform to OpenAI-compatible constraints: - Allowed chars: a-z, A-Z, 0-9, underscore, hyphen - Max length: 64 Returns (ok, messages). If strict=True and invalid tools exist, raises ToolValidationError with a detailed report. """ messages: List[str] = [] errors: List[str] = [] for idx, t in enumerate(tools): name = _get_name(t) if TOOL_NAME_PATTERN.match(name or ""): continue parts: List[str] = [f"第{idx}个工具名称不合法: '{name}'"] if not name: parts.append("原因: 为空") else: illegal = _find_illegal_chars(name) if illegal: parts.append(f"包含非法字符: {''.join(illegal)} (仅允许[a-zA-Z0-9_-])") if len(name) > max_len: parts.append(f"长度超限: {len(name)} > {max_len}") suggestion = sanitize_tool_name(name or "tool", max_len=max_len) parts.append(f"建议名称: '{suggestion}'") errors.append("; ".join(parts)) if errors: report = "\n".join(errors) messages.append(report) if strict: raise ToolValidationError(report) return False, messages return True, messages