- 新增 pam_deploy_graph 包,包含 agent、action router、runner、parser 和配置加载能力 - 支持 hybrid_node_mcp 路由策略:PAM_HOME 走脚本 action,PAM_NODE 走 MCP - 新增 fake runner 和 CLI 预演/全局流程验证入口 - 新增路由、输出解析、配置加载、脚本命令构造、Skill 策略加载测试 - 在 README 中记录当前代码骨架、实现进度、使用方式和下一步建议
141 lines
5.5 KiB
Python
141 lines
5.5 KiB
Python
"""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()
|