agent_deply/pam_deploy_graph/interactive.py
dark 1e74ae3cd6 feat: 增加 PAM 部署 Agent 交互式 CLI 与真实 LLM 配置
- 新增 OpenAI-compatible LLM client,支持 base_url、api_key、model 配置
- 固化意图识别、参数抽取、部署计划生成的结构化 JSON 提示词
- 增加 MCP client 配置读取和真实 session 接入说明
- 实现 checkpoint 自动保存、resume 断点续跑和已完成步骤跳过
- 实现人工确认流程,支持失败 IP 回滚 approve/reject
- 新增 chat 常驻式 CLI 对话框,支持自然语言分析、参数设置、执行确认、状态查看、回滚确认和续跑
- 同步 README,补充 LLM、MCP、checkpoint、confirm/resume、chat 使用方式
- 增加相关单元测试,覆盖 LLM client、MCP 配置、确认/续跑和交互式 CLI
2026-06-01 10:26:40 +08:00

291 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Interactive CLI session for the PAM deploy agent."""
from __future__ import annotations
import time
from dataclasses import asdict
from pathlib import Path
from typing import Any, Callable
from .agent import PamDeployAgent
from .checkpoint_store import load_agent_state, redact_mapping
from .models import AgentState, ExecutionStrategy
InputFunc = Callable[[str], str]
OutputFunc = Callable[[str], None]
COMMAND_HELP = """可用命令:
help 显示帮助
preview 查看当前参数和执行策略
analyze <需求> 只做理解和计划,不执行
set KEY=VALUE 修改当前会话参数
run 创建部署任务并执行
status 查看当前运行状态
approve 确认待处理回滚
reject [原因] 拒绝待处理回滚
resume 从当前 checkpoint 续跑
checkpoint 显示 checkpoint 路径
exit 退出
也可以直接输入自然语言需求Agent 会先分析并更新会话参数;执行仍需输入 run。
"""
class InteractiveCliSession:
def __init__(
self,
*,
agent: PamDeployAgent,
params: dict[str, Any],
strategy: ExecutionStrategy = "hybrid_node_mcp",
checkpoint_path: str | None = None,
target_ips: list[str] | None = None,
input_func: InputFunc = input,
output_func: OutputFunc = print,
) -> None:
self.agent = agent
self.params = dict(params)
self.strategy = strategy
self.checkpoint_path = checkpoint_path or _default_checkpoint_path()
self.target_ips = list(target_ips or [])
self.input = input_func
self.output = output_func
self.state: AgentState | None = None
self.last_analysis: dict[str, Any] | None = None
def run(self) -> None:
self.output("PAM Deploy Agent interactive session")
self.output("输入 help 查看命令,输入 exit 退出。")
self._load_existing_checkpoint_if_any()
while True:
try:
line = self.input("PAM> ")
except EOFError:
self.output("bye")
return
if not self.handle_line(line):
return
def handle_line(self, line: str) -> bool:
text = line.strip()
if not text:
return True
command, _, rest = text.partition(" ")
normalized = command.lower()
if normalized in ("exit", "quit", "q"):
self.output("bye")
return False
if normalized in ("help", "?"):
self.output(COMMAND_HELP.rstrip())
return True
if normalized == "preview":
self.output(self.agent.preview(self.params, self.strategy))
return True
if normalized == "analyze":
self._analyze(rest.strip())
return True
if normalized == "set":
self._set_param(rest.strip())
return True
if normalized in ("run", "deploy", "execute"):
self._run_deploy()
return True
if normalized == "resume":
self._resume()
return True
if normalized == "status":
self._status()
return True
if normalized == "approve":
self._confirm(approved=True, note=rest.strip())
return True
if normalized == "reject":
self._confirm(approved=False, note=rest.strip())
return True
if normalized == "checkpoint":
self.output(f"checkpoint: {self.checkpoint_path}")
return True
self._analyze(text)
return True
def _analyze(self, text: str) -> None:
if not text:
self.output("请输入要分析的自然语言需求例如analyze 请用 MCP 预演部署 HET。")
return
result = self.agent.analyze_request(text, self.params)
self.last_analysis = result
param_result = result["params"]
intent_result = result["intent"]
plan = result["plan"]
self.params = dict(param_result.extracted_params)
self.strategy = _choose_strategy(intent_result.strategy_preference, self.strategy)
user_ips = param_result.extracted_control.get("user_specified_ips")
if isinstance(user_ips, list):
self.target_ips = [str(item) for item in user_ips]
safe_payload = redact_mapping({key: asdict(value) for key, value in result.items()})
self.output("已生成结构化理解:")
self.output(f"- intent: {intent_result.intent}")
self.output(f"- strategy: {self.strategy}")
self.output(f"- summary: {plan.summary}")
if param_result.missing_required_params:
self.output("- missing: " + ", ".join(param_result.missing_required_params))
if self.target_ips:
self.output("- target_ips: " + ", ".join(self.target_ips))
self.output("执行请输 run查看完整 JSON 可用一次性 analyze 命令。")
self.output(_format_redacted_params(safe_payload["params"]["extracted_params"]))
def _set_param(self, assignment: str) -> None:
if "=" not in assignment:
self.output("格式set KEY=VALUE")
return
key, value = assignment.split("=", 1)
key = key.strip()
if not key:
self.output("参数名不能为空。")
return
self.params[key] = value.strip()
self.output(f"已设置 {key}")
def _run_deploy(self) -> None:
if self.state and self.state.pending_confirmation:
self._print_confirmation()
return
if not self._ask_yes_no("即将执行真实 action确认执行请输入 yes: "):
self.output("已取消执行。")
return
self.state = self.agent.create_state(
params=self.params,
execution_strategy=self.strategy,
checkpoint_path=self.checkpoint_path,
target_ips=self.target_ips,
)
self._execute_current_state()
def _resume(self) -> None:
if self.state is None:
checkpoint = Path(self.checkpoint_path)
if not checkpoint.exists():
self.output("当前没有可续跑的 checkpoint。")
return
self.state = load_agent_state(checkpoint)
self.state.checkpoint_path = self.state.checkpoint_path or str(checkpoint)
self._execute_current_state()
def _execute_current_state(self) -> None:
if self.state is None:
self.output("当前没有运行状态。")
return
self.state = self.agent.run_deploy_flow(self.state)
self.output(self.agent.render_report(self.state))
if self.state.pending_confirmation:
self._print_confirmation()
self.output(f"checkpoint: {self.state.checkpoint_path or self.checkpoint_path}")
def _status(self) -> None:
if self.state is None:
self.output("当前还没有运行状态。")
self.output(f"checkpoint: {self.checkpoint_path}")
return
self.output(self.agent.render_report(self.state))
if self.state.pending_confirmation:
self._print_confirmation()
def _confirm(self, *, approved: bool, note: str = "") -> None:
if self.state is None:
checkpoint = Path(self.checkpoint_path)
if checkpoint.exists():
self.state = load_agent_state(checkpoint)
self.state.checkpoint_path = self.state.checkpoint_path or str(checkpoint)
else:
self.output("当前没有待确认任务。")
return
if not self.state.pending_confirmation:
self.output("当前没有待确认任务。")
return
self.state = self.agent.confirm_pending(self.state, approved=approved, operator_note=note)
self.output(self.agent.render_report(self.state))
if self.state.pending_confirmation:
self._print_confirmation()
def _print_confirmation(self) -> None:
if self.state is None:
return
request = self.agent.build_confirmation_request(self.state)
if not request:
return
self.output("需要人工确认:")
self.output(f"- type: {request.get('type')}")
if request.get("ip"):
self.output(f"- ip: {request['ip']}")
if request.get("failed_stage"):
self.output(f"- failed_stage: {request['failed_stage']}")
if request.get("failure_reason"):
self.output(f"- reason: {request['failure_reason']}")
self.output("输入 approve 执行回滚,或 reject [原因] 拒绝回滚。")
def _ask_yes_no(self, prompt: str) -> bool:
try:
answer = self.input(prompt).strip().lower()
except EOFError:
return False
return answer in ("yes", "y")
def _load_existing_checkpoint_if_any(self) -> None:
checkpoint = Path(self.checkpoint_path)
if not checkpoint.exists():
return
self.state = load_agent_state(checkpoint)
self.state.checkpoint_path = self.state.checkpoint_path or str(checkpoint)
self.output(f"已加载 checkpoint: {checkpoint}")
if self.state.pending_confirmation:
self._print_confirmation()
def run_interactive_chat(
*,
agent: PamDeployAgent,
params: dict[str, Any],
strategy: ExecutionStrategy,
checkpoint_path: str | None = None,
target_ips: list[str] | None = None,
input_func: InputFunc = input,
output_func: OutputFunc = print,
) -> InteractiveCliSession:
session = InteractiveCliSession(
agent=agent,
params=params,
strategy=strategy,
checkpoint_path=checkpoint_path,
target_ips=target_ips,
input_func=input_func,
output_func=output_func,
)
session.run()
return session
def _default_checkpoint_path() -> str:
return str(Path("runtime") / "checkpoints" / f"chat_{time.strftime('%Y%m%d_%H%M%S')}.json")
def _choose_strategy(preference: str, default: ExecutionStrategy) -> ExecutionStrategy:
if preference in ("hybrid_node_mcp", "script_only", "fake"):
return preference # type: ignore[return-value]
return default
def _format_redacted_params(params: dict[str, Any]) -> str:
lines = ["当前参数:"]
for key in sorted(params):
lines.append(f"- {key}: {params[key]}")
return "\n".join(lines)