- 新增 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
291 lines
10 KiB
Python
291 lines
10 KiB
Python
"""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)
|