"""PAM deploy Agent runtime. This is intentionally runnable without langgraph installed. The same nodes can be wired into LangGraph later via pam_deploy_graph.graph. """ from __future__ import annotations import time from pathlib import Path from typing import Any from .action_router import ActionRouter, build_action_backends from .config_writer import write_config from .constants import DEFAULT_PARAMS, GLOBAL_ACTION_SEQUENCE, REQUIRED_PARAMS from .fake_runner import FakeActionRunner from .mcp_runner import McpActionRunner from .models import AgentState, ExecutionStrategy from .script_runner import ScriptActionRunner, select_script_entry from .skill_policy import load_skill_policy class PamDeployAgent: def __init__( self, *, skill_path: str | Path = "doc_scripts/PAM_AUTO_DEPLY_SKILL.md", script_base_dir: str | Path = "doc_scripts", mcp_runner: McpActionRunner | None = None, fake_runner: FakeActionRunner | None = None, ) -> None: self.skill_policy = load_skill_policy(skill_path) self.script_base_dir = Path(script_base_dir) self.script_runner = ScriptActionRunner(self.script_base_dir) self.fake_runner = fake_runner or FakeActionRunner() self.mcp_runner = mcp_runner self.router = ActionRouter( script_runner=self.script_runner, mcp_runner=mcp_runner, fake_runner=self.fake_runner, ) def normalize_params(self, params: dict[str, Any]) -> dict[str, Any]: normalized = {**DEFAULT_PARAMS, **params} missing = [key for key in REQUIRED_PARAMS if not normalized.get(key)] if missing: raise ValueError(f"Missing required params: {', '.join(missing)}") return normalized def create_state( self, *, params: dict[str, Any], execution_strategy: ExecutionStrategy = "hybrid_node_mcp", run_id: str | None = None, script_entry: str | None = None, config_path: str | None = None, trace_file_path: str | None = None, ) -> AgentState: normalized = self.normalize_params(params) actual_run_id = run_id or time.strftime("%Y%m%d_%H%M%S") actual_script_entry = script_entry or select_script_entry() runtime_dir = Path("runtime") actual_config_path = config_path or str(runtime_dir / f"config_{actual_run_id}.txt") actual_trace_path = trace_file_path or str(Path("logs") / f"api_trace_{actual_run_id}.log") write_config(normalized, actual_config_path) return AgentState( run_id=actual_run_id, params=normalized, execution_strategy=execution_strategy, action_backends=build_action_backends(execution_strategy), script_entry=actual_script_entry, script_base_dir=str(self.script_base_dir), config_path=actual_config_path, trace_file_path=actual_trace_path, ) def preview(self, params: dict[str, Any], strategy: ExecutionStrategy = "hybrid_node_mcp") -> str: normalized = self.normalize_params(params) routes = build_action_backends(strategy) if strategy == "hybrid_node_mcp": home_backend = "脚本 action" node_backend = "MCP" elif strategy == "script_only": home_backend = "脚本 action" node_backend = "脚本 action" else: home_backend = "fake" node_backend = "fake" lines = [ "## PAM 部署预演", "", f"- 执行策略: {strategy}", f"- PAM_HOME: {home_backend}", f"- PAM_NODE: {node_backend}", f"- 机场: {normalized['AIRPORT_CODE']}", f"- 应用: {normalized['APP_NAME']}", f"- 模块: {normalized['MODULE_NAME']}", f"- 版本: {normalized['VERSION_NUMBER']}", "", "| action | backend |", "| --- | --- |", ] for action in GLOBAL_ACTION_SEQUENCE: lines.append(f"| `{action}` | `{routes[action]}` |") return "\n".join(lines) def run_global_flow(self, state: AgentState) -> AgentState: for action in GLOBAL_ACTION_SEQUENCE: kwargs: dict[str, Any] = {} if action == "publish-version": kwargs["hash_code"] = state.hash_code result = self.router.run_action(state, action, **kwargs) state.events.append( { "type": "ACTION_DONE" if result.ok else "ACTION_FAIL", "stage": action, "backend": result.backend, "message": result.error_summary or "ok", } ) if not result.ok: state.last_failed_step = action raise RuntimeError(f"{action} failed: {result.error_summary}") self._apply_result(state, action, result.values) state.completed_global_steps.append(action) state.last_success_step = action return state def _apply_result(self, state: AgentState, action: str, values: dict[str, Any]) -> None: if "HASH_CODE" in values: state.hash_code = str(values["HASH_CODE"]) if "NODE_URL" in values: state.node_url = str(values["NODE_URL"]) if action == "get-online-ips": ips = values.get("IP", []) if isinstance(ips, str): ips = [ips] state.online_ips = list(ips) state.target_ips = state.target_ips or state.online_ips.copy()