""" Tool trace models for Mid Platform. [AC-IDMP-15] 工具调用结构化记录 Reference: spec/intent-driven-mid-platform/openapi.provider.yaml - ToolCallTrace """ import hashlib import json from dataclasses import dataclass, field from datetime import datetime from enum import Enum from typing import Any class ToolCallStatus(str, Enum): """工具调用状态""" OK = "ok" TIMEOUT = "timeout" ERROR = "error" REJECTED = "rejected" class ToolType(str, Enum): """工具类型""" INTERNAL = "internal" MCP = "mcp" @dataclass class ToolCallTrace: """ [AC-IDMP-15] 工具调用追踪记录 Reference: openapi.provider.yaml - ToolCallTrace 记录字段: - tool_name: 工具名称 - tool_type: 工具类型 (internal | mcp) - registry_version: 注册表版本 - auth_applied: 是否应用鉴权 - duration_ms: 调用耗时(毫秒) - status: 调用状态 (ok | timeout | error | rejected) - error_code: 错误码 - args_digest: 参数摘要(脱敏) - result_digest: 结果摘要 - arguments: 完整参数 - result: 完整结果 """ tool_name: str duration_ms: int status: ToolCallStatus tool_type: ToolType = ToolType.INTERNAL registry_version: str | None = None auth_applied: bool = False error_code: str | None = None args_digest: str | None = None result_digest: str | None = None arguments: dict[str, Any] | None = None result: Any = None started_at: datetime = field(default_factory=datetime.utcnow) completed_at: datetime | None = None def to_dict(self) -> dict[str, Any]: result = { "tool_name": self.tool_name, "duration_ms": self.duration_ms, "status": self.status.value, } if self.tool_type != ToolType.INTERNAL: result["tool_type"] = self.tool_type.value if self.registry_version: result["registry_version"] = self.registry_version if self.auth_applied: result["auth_applied"] = self.auth_applied if self.error_code: result["error_code"] = self.error_code if self.args_digest: result["args_digest"] = self.args_digest if self.result_digest: result["result_digest"] = self.result_digest if self.arguments: result["arguments"] = self.arguments if self.result is not None: result["result"] = self.result return result @staticmethod def compute_digest(data: Any, max_length: int = 64) -> str: """ 计算数据摘要(用于脱敏记录) Args: data: 原始数据 max_length: 最大长度限制 Returns: 摘要字符串 """ if data is None: return "" if isinstance(data, (dict, list)): data_str = json.dumps(data, ensure_ascii=False, sort_keys=True) else: data_str = str(data) if len(data_str) <= max_length: return data_str hash_value = hashlib.sha256(data_str.encode("utf-8")).hexdigest()[:16] preview = data_str[:32] return f"{preview}...[hash:{hash_value}]" @dataclass class ToolCallBuilder: """ [AC-IDMP-15] 工具调用记录构建器 用于在工具执行过程中逐步构建追踪记录 """ tool_name: str tool_type: ToolType = ToolType.INTERNAL registry_version: str | None = None auth_applied: bool = False _started_at: datetime = field(default_factory=datetime.utcnow) _args: Any = None _result: Any = None _error: Exception | None = None _status: ToolCallStatus = ToolCallStatus.OK _error_code: str | None = None def with_args(self, args: Any) -> "ToolCallBuilder": """设置调用参数""" self._args = args return self def with_registry_info(self, version: str, auth_applied: bool) -> "ToolCallBuilder": """设置注册表信息""" self.registry_version = version self.auth_applied = auth_applied return self def with_result(self, result: Any) -> "ToolCallBuilder": """设置调用结果""" self._result = result self._status = ToolCallStatus.OK return self def with_error(self, error: Exception, error_code: str | None = None) -> "ToolCallBuilder": """设置错误信息""" self._error = error self._error_code = error_code if isinstance(error, TimeoutError): self._status = ToolCallStatus.TIMEOUT else: self._status = ToolCallStatus.ERROR return self def with_rejected(self, reason: str) -> "ToolCallBuilder": """设置拒绝状态""" self._status = ToolCallStatus.REJECTED self._error_code = reason return self def build(self) -> ToolCallTrace: """构建追踪记录""" completed_at = datetime.utcnow() duration_ms = int((completed_at - self._started_at).total_seconds() * 1000) return ToolCallTrace( tool_name=self.tool_name, tool_type=self.tool_type, registry_version=self.registry_version, auth_applied=self.auth_applied, duration_ms=duration_ms, status=self._status, error_code=self._error_code, args_digest=ToolCallTrace.compute_digest(self._args) if self._args else None, result_digest=ToolCallTrace.compute_digest(self._result) if self._result else None, started_at=self._started_at, completed_at=completed_at, )