"""查询 MoviePilot Doctor 诊断报告工具。""" import json from typing import Any, Optional, Type from pydantic import BaseModel, Field from app.agent.tools.base import MoviePilotTool from app.agent.tools.tags import ToolTag from app.doctor import run_doctor from app.log import logger class QueryDoctorReportInput(BaseModel): """查询 Doctor 诊断报告工具的输入参数模型。""" explanation: Optional[str] = Field( None, description="Clear explanation of why this tool is being used in the current context", ) deep: Optional[bool] = Field( False, description=( "Whether to run deeper checks. When true, doctor may perform slower environment probes " "such as PostgreSQL TCP connectivity checks." ), ) include_details: Optional[bool] = Field( True, description=( "Whether to include full doctor findings with details and context. Set false for a compact " "summary when only overall status and finding titles are needed." ), ) class QueryDoctorReportTool(MoviePilotTool): """ Doctor 离线诊断报告查询工具。 """ name: str = "query_doctor_report" tags: list[str] = [ ToolTag.Read, ToolTag.System, ToolTag.Admin, ] description: str = ( "Run MoviePilot Doctor in read-only mode and return a structured diagnostic report for troubleshooting. " "Use this tool when analyzing startup failures, Docker/runtime issues, port conflicts, dependency problems, " "database health, frontend assets, safe mode, or recent log error clues. This tool never applies fixes." ) require_admin: bool = True args_schema: Type[BaseModel] = QueryDoctorReportInput def get_tool_message(self, **kwargs) -> Optional[str]: """根据查询参数生成友好的提示消息。""" if kwargs.get("deep"): return "运行 Doctor 深度诊断" return "运行 Doctor 诊断" @staticmethod def _compact_report(report: dict[str, Any]) -> dict[str, Any]: """压缩诊断报告,保留 Agent 判断问题所需的核心字段。""" return { "schema_version": report.get("schema_version"), "status": report.get("status"), "generated_at": report.get("generated_at"), "version": report.get("version"), "environment": report.get("environment"), "summary": report.get("summary"), "findings": [ { "id": item.get("id"), "severity": item.get("severity"), "status": item.get("status"), "title": item.get("title"), "fixable": item.get("fixable"), "fixed": item.get("fixed"), } for item in report.get("findings") or [] if isinstance(item, dict) ], } @staticmethod def _run_doctor_report(deep: bool = False) -> dict[str, Any]: """在线程池中运行只读 Doctor 诊断。""" return run_doctor(deep=bool(deep)).to_dict() async def run( self, deep: Optional[bool] = False, include_details: Optional[bool] = True, **kwargs, ) -> str: """ 运行只读 Doctor 诊断并返回 JSON 字符串。 """ logger.info( f"执行工具: {self.name}, deep={bool(deep)}, include_details={bool(include_details)}" ) try: report = await self.run_blocking("default", self._run_doctor_report, bool(deep)) if not include_details: report = self._compact_report(report) return json.dumps( { "success": True, "deep": bool(deep), "include_details": bool(include_details), "report": report, }, ensure_ascii=False, indent=2, default=str, ) except Exception as err: logger.error(f"查询 Doctor 诊断报告失败: {err}", exc_info=True) return json.dumps( { "success": False, "message": f"查询 Doctor 诊断报告时发生错误: {str(err)}", }, ensure_ascii=False, )