diff --git a/docs/ai-chat-api-design.md b/docs/gns3-copilot/ai-chat-api-design.md similarity index 100% rename from docs/ai-chat-api-design.md rename to docs/gns3-copilot/ai-chat-api-design.md diff --git a/docs/gns3-copilot/hitl-implementation-plan.md b/docs/gns3-copilot/hitl-implementation-plan.md new file mode 100644 index 000000000..64d763c46 --- /dev/null +++ b/docs/gns3-copilot/hitl-implementation-plan.md @@ -0,0 +1,1402 @@ +# GNS3 Copilot HITL 功能实现方案 + +## 概述 + +本文档详细描述了 GNS3 Copilot 中 Human-in-the-Loop (HITL) 功能的完整实现方案。HITL 功能允许在执行敏感操作(如设备配置)前要求用户确认,提高系统安全性。 + +## 目标 + +- 在执行配置工具前要求用户确认 +- 支持单个和批量工具确认 +- 提供清晰的工具执行预览 +- 保持现有功能完全兼容 + +## 核心设计 + +### 流程图 + +``` +用户消息 → LLM → 工具调用决策 + ↓ + 检测是否需要确认 + ↙ ↘ + 需要 HITL 直接执行 + ↓ ↓ + 暂停,等待前端 执行工具 + ↓ ↓ + 用户确认/拒绝 返回结果 + ↓ + 执行已确认的工具 + ↓ + 返回结果 +``` + +### 架构分层 + +``` +┌─────────────────────────────────────────┐ +│ Frontend (Web UI) │ +│ - 显示确认对话框 │ +│ - 列出待确认的工具 │ +│ - 处理用户确认/拒绝操作 │ +└─────────────────────────────────────────┘ + ↕ SSE/HTTP +┌─────────────────────────────────────────┐ +│ API Layer (FastAPI) │ +│ - /hitl/status: 获取待确认工具 │ +│ - /hitl/confirm: 确认执行 │ +│ - /hitl/reject: 拒绝执行 │ +└─────────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────────┐ +│ AgentService (状态管理) │ +│ - 管理 HITL 状态 │ +│ - 处理确认/拒绝逻辑 │ +│ - checkpoint 持久化 │ +└─────────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────────┐ +│ LangGraph Agent (工作流) │ +│ - llm_call: LLM 调用 │ +│ - check_hitl: 检查是否需要确认 │ +│ - hitl_confirmation: 等待确认 │ +│ - conditional_execution: 条件执行 │ +└─────────────────────────────────────────┘ +``` + +## 实现改动 + +### 1. LangGraph Agent 层 + +#### 文件:`gns3server/agent/gns3_copilot/agent/gns3_copilot.py` + +##### 改动 1.1:扩展 MessagesState + +**位置**:第 98-124 行 + +```python +# 扩展状态定义,添加 HITL 相关字段 +class MessagesState(TypedDict): + """GNS3-Copilot 对话状态管理类""" + + messages: Annotated[list[AnyMessage], operator.add] + llm_calls: int + remaining_steps: RemainingSteps + conversation_title: str | None + topology_info: dict | None + + # 新增:HITL 相关字段 + pending_tool_calls: list[dict] # 等待确认的工具调用列表 + hitl_confirmation_required: bool # 是否需要用户确认 + hitl_session_id: str | None # HITL 会话唯一标识 + confirmed_tool_calls: list[dict] # 用户已确认的工具调用 + rejected_tool_calls: list[dict] # 用户拒绝的工具调用 +``` + +**改动影响**: +- checkpoint 数据库中新增 5 个字段 +- 现有状态读取代码需要兼容新字段 + +##### 改动 1.2:添加 HITL 检测节点 + +**位置**:在 `generate_title` 函数后添加(约第 310 行) + +```python +# 需要确认的工具列表 +HITL_TOOLS = { + "execute_multiple_device_config_commands", + # 可根据需要添加: + # "delete_node", + # "start_gns3_node", +} + +DANGEROUS_PATTERNS = [ + "reload", "reboot", "write erase", "erase startup-config", + "factory reset", "format", "delete", "no ip routing" +] + + +def _is_dangerous_config(tool_name: str, tool_args: dict) -> bool: + """检测配置是否包含危险命令""" + if tool_name == "execute_multiple_device_config_commands": + device_configs = tool_args.get("device_configs", []) + for device in device_configs: + commands = device.get("config_commands", []) + for cmd in commands: + cmd_lower = cmd.lower() + if any(pattern in cmd_lower for pattern in DANGEROUS_PATTERNS): + return True + return False + + +def check_hitl_requirement(state: MessagesState) -> dict: + """ + 检查工具调用是否需要用户确认 (HITL) + + Returns: + dict: 包含 pending_tool_calls 和 hitl_confirmation_required 的状态更新 + """ + last_message = state["messages"][-1] + + # 如果最后一条消息没有工具调用,直接返回 + if not hasattr(last_message, 'tool_calls') or not last_message.tool_calls: + return {"hitl_confirmation_required": False} + + pending_tools = [] + + for tool_call in last_message.tool_calls: + tool_name = tool_call["name"] + tool_args = tool_call["args"] + + # 只处理需要 HITL 的工具 + if tool_name in HITL_TOOLS: + # 检查是否为危险操作 + is_dangerous = _is_dangerous_config(tool_name, tool_args) + + tool_info = { + "tool_call_id": tool_call["id"], + "tool_name": tool_name, + "tool_args": tool_args, + "danger_level": "high" if is_dangerous else "medium" + } + + # 添加描述信息 + if tool_name == "execute_multiple_device_config_commands": + device_count = len(tool_args.get("device_configs", [])) + tool_info["description"] = f"配置 {device_count} 个设备" + + pending_tools.append(tool_info) + logger.info("HITL: Tool '%s' requires confirmation (danger_level=%s)", + tool_name, tool_info["danger_level"]) + + if pending_tools: + logger.info("HITL: %d tools require confirmation", len(pending_tools)) + return { + "pending_tool_calls": pending_tools, + "hitl_confirmation_required": True, + "hitl_session_id": str(uuid4()) + } + + return {"hitl_confirmation_required": False} +``` + +##### 改动 1.3:修改 should_continue 路由 + +**位置**:第 337-370 行 + +```python +def should_continue( + state: MessagesState, +) -> Literal["conditional_tool_execution", "hitl_confirmation", "title_generator_node", END]: + """ + LLM 响应后的路由决策 + + Returns: + Literal: 路由到下一个节点 + """ + last_message = state["messages"][-1] + current_title = state.get("conversation_title") + + # LLM 请求工具调用 + if last_message.tool_calls: + # 检查是否需要 HITL 确认 + if state.get("hitl_confirmation_required"): + logger.info("Routing to hitl_confirmation node") + return "hitl_confirmation" + + logger.info("Routing to conditional_tool_execution node") + return "conditional_tool_execution" + + # 首次交互完成,生成标题 + if current_title in [None, "New Conversation"]: + return "title_generator_node" + + return END +``` + +**关键变更**: +- 原路由 `"tool_node"` 改为 `"conditional_tool_execution"` +- 新增 `"hitl_confirmation"` 路由 +- 路由名称变更,需要同步更新所有引用 + +##### 改动 1.4:添加 HITL 确认等待节点 + +**位置**:在 `should_continue` 函数后添加 + +```python +def hitl_confirmation_node(state: MessagesState) -> dict: + """ + HITL 确认节点 - 暂停执行,等待用户确认 + + 这是一个特殊的节点,它不执行任何操作,只是保持状态。 + 实际的确认流程由 API 层处理。 + + 工作原理: + 1. 节点被调用时,状态中包含 pending_tool_calls + 2. LangGraph 保存状态到 checkpoint + 3. 执行流程暂停,等待外部状态更新 + 4. 用户通过 API 确认/拒绝后,状态被更新 + 5. 从 checkpoint 恢复并继续执行 + + Returns: + dict: 空字典,保持状态不变 + """ + logger.info("HITL: Pausing for user confirmation (session_id=%s)", + state.get("hitl_session_id")) + + # 返回空字典,保持状态不变 + # 状态将在用户确认后通过 API 更新 + return {} +``` + +##### 改动 1.5:添加条件执行节点 + +**位置**:在 `hitl_confirmation_node` 函数后添加 + +```python +def conditional_tool_execution(state: MessagesState) -> dict: + """ + 条件工具执行节点 - 根据用户确认决定是否执行工具 + + 这个节点替代原来的 tool_node,增加了: + 1. 只执行用户确认的工具 + 2. 处理用户拒绝的工具 + 3. 更新 HITL 状态 + + Returns: + dict: 包含执行结果和状态更新的字典 + """ + confirmed_calls = state.get("confirmed_tool_calls", []) + rejected_calls = state.get("rejected_tool_calls", []) + + logger.info("HITL: Executing %d confirmed tools, %d rejected", + len(confirmed_calls), len(rejected_calls)) + + # 处理用户拒绝的工具 + if rejected_calls: + rejected_names = [c["tool_name"] for c in rejected_calls] + rejection_msg = ( + f"用户拒绝了以下操作: {', '.join(rejected_names)}。\n" + f"请提供替代方案或解释不执行这些操作的原因。" + ) + + # 添加系统消息通知 LLM + return { + "messages": [SystemMessage(content=rejection_msg)], + "pending_tool_calls": [], + "hitl_confirmation_required": False, + "confirmed_tool_calls": [], + "rejected_tool_calls": [] + } + + # 执行已确认的工具 + if not confirmed_calls: + logger.warning("HITL: No confirmed tools to execute") + return { + "pending_tool_calls": [], + "hitl_confirmation_required": False + } + + results = [] + for tool_call_dict in confirmed_calls: + tool_call_id = tool_call_dict["tool_call_id"] + tool_name = tool_call_dict["tool_name"] + tool_args = tool_call_dict["tool_args"] + + logger.info("Executing confirmed tool: %s", tool_name) + + tool = tools_by_name[tool_name] + try: + observation = tool.invoke(tool_args) + results.append(ToolMessage( + content=observation, + tool_call_id=tool_call_id, + name=tool_name + )) + logger.debug("Tool %s completed successfully", tool_name) + except Exception as e: + logger.error("Tool %s failed: %s", tool_name, e, exc_info=True) + results.append(ToolMessage( + content=f"Error: {str(e)}", + tool_call_id=tool_call_id, + name=tool_name + )) + + # 清理 HITL 状态 + return { + "messages": results, + "pending_tool_calls": [], + "hitl_confirmation_required": False, + "confirmed_tool_calls": [], + "rejected_tool_calls": [] + } +``` + +**关键变更**: +- 替代原 `tool_node` 函数 +- 增加确认逻辑处理 +- 支持部分确认、部分拒绝 + +##### 改动 1.6:更新 LangGraph 构建流程 + +**位置**:第 393-427 行 + +```python +# 构建工作流 +agent_builder = StateGraph(MessagesState) + +# 添加节点 +agent_builder.add_node("llm_call", llm_call) +agent_builder.add_node("hitl_confirmation", hitl_confirmation_node) +agent_builder.add_node("conditional_tool_execution", conditional_tool_execution) +agent_builder.add_node("title_generator_node", generate_title) + +# 添加边:START → llm_call +agent_builder.add_edge(START, "llm_call") + +# 添加边:LLM 后的条件路由 +agent_builder.add_conditional_edges( + "llm_call", + should_continue, + { + "hitl_confirmation": "hitl_confirmation", # 需要 HITL 确认 + "conditional_tool_execution": "conditional_tool_execution", # 直接执行 + "title_generator_node": "title_generator_node", # 生成标题 + END: END # 结束对话 + }, +) + +# HITL 确认节点是特殊的,它需要等待外部状态更新 +# 状态更新后,下一轮调用将从 checkpoint 恢复并继续 +# 这个循环通过 API 触发新的 graph.ainvoke() 调用完成 + +# 添加边:条件执行后继续 LLM 调用 +agent_builder.add_conditional_edges( + "conditional_tool_execution", + recursion_limit_continue, + { + "llm_call": "llm_call", + END: END + }, +) + +# 添加边:标题生成后结束 +agent_builder.add_edge("title_generator_node", END) +``` + +**工作流图**: +``` +START → llm_call → should_continue + ↓ + ┌────────────┼────────────┐ + ↓ ↓ ↓ +hitl_confirmation conditional title_generator +(等待 API) _execution ↓ + └────────────┴────────────→ END +``` + +#### 文件:`gns3server/agent/gns3_copilot/agent_service.py` + +##### 改动 2.1:添加 HITL 事件处理 + +**位置**:第 333-376 行的 `_convert_event_to_chunk` 函数 + +```python +def _convert_event_to_chunk(self, event: Dict[str, Any], session_id: str) -> Optional[Dict[str, Any]]: + """ + 转换 LangGraph 事件为 API 响应块 + + 支持 HITL 事件类型 + """ + event_type = event.get("event", "") + data = event.get("data", {}) + + if event_type == "on_chat_model_stream": + chunk = data.get("chunk", {}) + content = getattr(chunk, "content", "") + if content: + return {"type": "content", "content": content} + + elif event_type == "on_tool_start": + return { + "type": "tool_start", + "tool_name": event.get("name", ""), + "session_id": session_id + } + + elif event_type == "on_tool_end": + output = data.get("output", "") + if not isinstance(output, str): + output = str(output) + return { + "type": "tool_end", + "tool_name": event.get("name", ""), + "tool_output": output, + "session_id": session_id + } + + # 新增:HITL 确认要求事件 + elif event_type == "hitl_required": + return { + "type": "hitl_required", + "pending_tools": data.get("pending_tool_calls", []), + "hitl_session_id": data.get("hitl_session_id"), + "timeout": 300, # 5分钟超时 + "session_id": session_id + } + + return None +``` + +**注意**:实际实现中,HITL 事件不是通过 LangGraph 的 `astream_events` 触发的,而是通过状态查询实现的。因此,此函数主要用于处理工具执行事件。 + +--- + +### 2. API 层 + +#### 文件:`gns3server/api/routes/controller/chat.py` + +##### 改动 2.1:添加 HITL 端点 + +**位置**:在文件末尾添加(约第 325 行后) + +```python +from gns3server import schemas +from typing import List + + +# ============================================================================= +# HITL (Human-in-the-Loop) 端点 +# ============================================================================= + +@router.get( + "/sessions/{session_id}/hitl-status", + response_model=schemas.HITLStatusResponse, + summary="获取 HITL 状态", + description="获取当前会话中待确认的工具列表" +) +async def get_hitl_status( + session_id: str, + project: Project = Depends(dep_project), + current_user: schemas.User = Depends(get_current_active_user), +) -> schemas.HITLStatusResponse: + """ + 获取 HITL 状态 + + 返回当前会话中等待用户确认的工具调用列表。 + 前端应定期轮询此端点以检查是否有新的待确认工具。 + """ + if project.status != "opened": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Project must be opened. Current status: {project.status}" + ) + + agent_manager = await get_project_agent_manager() + agent_service = await agent_manager.get_agent(str(project.id), project.path) + + # 从 checkpoint 获取状态 + config = {"configurable": {"thread_id": session_id}} + state = await agent_service._graph.aget_state(config) + + if not state or not state.values: + return schemas.HITLStatusResponse( + status="idle", + pending_tools=[], + hitl_session_id=None, + session_id=session_id + ) + + values = state.values + pending_tools = values.get("pending_tool_calls", []) + hitl_session_id = values.get("hitl_session_id") + + # 转换为 Schema 格式 + pending_tool_schemas = [] + for tool in pending_tools: + pending_tool_schemas.append(schemas.PendingTool( + tool_call_id=tool["tool_call_id"], + tool_name=tool["tool_name"], + tool_args=tool["tool_args"], + danger_level=tool.get("danger_level", "medium"), + description=tool.get("description", "") + )) + + return schemas.HITLStatusResponse( + status="waiting" if pending_tool_schemas else "idle", + pending_tools=pending_tool_schemas, + hitl_session_id=hitl_session_id, + session_id=session_id + ) + + +@router.post( + "/sessions/{session_id}/hitl/confirm", + response_model=schemas.HITLConfirmationResponse, + summary="确认执行工具", + description="用户确认执行一个或多个待确认的工具" +) +async def confirm_tool_execution( + session_id: str, + request: schemas.HITLConfirmationRequest, + project: Project = Depends(dep_project), + current_user: schemas.User = Depends(get_current_active_user), +) -> schemas.HITLConfirmationResponse: + """ + 确认执行工具 + + 用户可以选择: + - confirm_all=true: 确认所有待确认的工具 + - tool_call_ids=[...]: 确认指定的工具 + + 确认后,工具将被执行,结果将通过 SSE 流返回。 + """ + if project.status != "opened": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Project must be opened. Current status: {project.status}" + ) + + agent_manager = await get_project_agent_manager() + agent_service = await agent_manager.get_agent(str(project.id), project.path) + + config = {"configurable": {"thread_id": session_id}} + state = await agent_service._graph.aget_state(config) + + if not state or not state.values: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Session '{session_id}' not found" + ) + + values = state.values + pending_tools = values.get("pending_tool_calls", []) + + if not pending_tools: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No pending tools to confirm" + ) + + # 根据请求选择要确认的工具 + confirmed_tools = [] + if request.confirm_all: + confirmed_tools = pending_tools + elif request.tool_call_ids: + confirmed_tools = [t for t in pending_tools if t["tool_call_id"] in request.tool_call_ids] + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Must specify either confirm_all=true or tool_call_ids" + ) + + if not confirmed_tools: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No tools matched the confirmation criteria" + ) + + # 更新状态:标记为已确认 + await agent_service._graph.aupdate_state( + config, + { + "confirmed_tool_calls": confirmed_tools, + "pending_tool_calls": [], + "hitl_confirmation_required": False + } + ) + + # 继续执行流程 + try: + new_state = await agent_service._graph.ainvoke(None, config) + except Exception as e: + logger.error("Error continuing graph execution: %s", e, exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error executing tools: {str(e)}" + ) + + return schemas.HITLConfirmationResponse( + status="confirmed", + confirmed_count=len(confirmed_tools), + message=f"已确认 {len(confirmed_tools)} 个工具执行" + ) + + +@router.post( + "/sessions/{session_id}/hitl/reject", + response_model=schemas.HITLConfirmationResponse, + summary="拒绝执行工具", + description="用户拒绝执行一个或多个待确认的工具" +) +async def reject_tool_execution( + session_id: str, + request: schemas.HITLRejectionRequest, + project: Project = Depends(dep_project), + current_user: schemas.User = Depends(get_current_active_user), +) -> schemas.HITLConfirmationResponse: + """ + 拒绝执行工具 + + 用户可以选择: + - reject_all=true: 拒绝所有待确认的工具 + - tool_call_ids=[...]: 拒绝指定的工具 + - reason: 拒绝原因(将反馈给 LLM) + + 拒绝后,LLM 将收到通知并可以提供替代方案。 + """ + if project.status != "opened": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Project must be opened. Current status: {project.status}" + ) + + agent_manager = await get_project_agent_manager() + agent_service = await agent_manager.get_agent(str(project.id), project.path) + + config = {"configurable": {"thread_id": session_id}} + + rejected_tools = [] + if request.reject_all: + state = await agent_service._graph.aget_state(config) + if state and state.values: + rejected_tools = state.values.get("pending_tool_calls", []) + elif request.tool_call_ids: + state = await agent_service._graph.aget_state(config) + if state and state.values: + pending = state.values.get("pending_tool_calls", []) + rejected_tools = [t for t in pending if t["tool_call_id"] in request.tool_call_ids] + + if not rejected_tools: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No pending tools to reject" + ) + + # 更新状态:标记为已拒绝 + await agent_service._graph.aupdate_state( + config, + { + "rejected_tool_calls": rejected_tools, + "pending_tool_calls": [], + "hitl_confirmation_required": False + } + ) + + # 继续执行流程,LLM 将收到拒绝通知 + try: + new_state = await agent_service._graph.ainvoke(None, config) + except Exception as e: + logger.error("Error continuing graph execution: %s", e, exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error processing rejection: {str(e)}" + ) + + rejected_names = [t["tool_name"] for t in rejected_tools] + return schemas.HITLConfirmationResponse( + status="rejected", + confirmed_count=0, + message=f"已拒绝 {len(rejected_tools)} 个工具执行: {', '.join(rejected_names)}" + ) +``` + +--- + +### 3. Schema 层 + +#### 文件:`gns3server/schemas/controller/chat.py` + +##### 改动 3.1:添加 HITL 相关的 Pydantic 模型 + +**位置**:在文件末尾添加(约第 112 行后) + +```python +class PendingTool(BaseModel): + """待确认的工具信息""" + tool_call_id: str = Field(..., description="工具调用的唯一 ID") + tool_name: str = Field(..., description="工具名称") + tool_args: Dict[str, Any] = Field(..., description="工具参数") + danger_level: Literal["low", "medium", "high"] = Field( + default="medium", + description="危险等级" + ) + description: Optional[str] = Field(None, description="工具执行的描述") + + +class HITLStatusResponse(BaseModel): + """HITL 状态响应""" + status: Literal["idle", "waiting", "confirmed", "rejected"] = Field( + ..., + description="当前状态" + ) + pending_tools: List[PendingTool] = Field( + default_factory=list, + description="待确认的工具列表" + ) + hitl_session_id: Optional[str] = Field( + None, + description="HITL 会话 ID" + ) + session_id: str = Field(..., description="聊天会话 ID") + + +class HITLConfirmationRequest(BaseModel): + """HITL 确认请求""" + confirm_all: bool = Field( + default=False, + description="是否确认所有待确认的工具" + ) + tool_call_ids: Optional[List[str]] = Field( + None, + description="要确认的工具调用 ID 列表" + ) + + +class HITLRejectionRequest(BaseModel): + """HITL 拒绝请求""" + reject_all: bool = Field( + default=False, + description="是否拒绝所有待确认的工具" + ) + tool_call_ids: Optional[List[str]] = Field( + None, + description="要拒绝的工具调用 ID 列表" + ) + reason: Optional[str] = Field( + None, + description="拒绝原因(将反馈给 LLM)" + ) + + +class HITLConfirmationResponse(BaseModel): + """HITL 确认响应""" + status: Literal["confirmed", "rejected"] = Field( + ..., + description="操作状态" + ) + confirmed_count: int = Field( + ..., + description="已确认的工具数量" + ) + message: str = Field(..., description="响应消息") +``` + +**同时更新 `__init__.py` 导出**: + +```python +# 文件:gns3server/schemas/__init__.py + +# 添加到导入列表 +from .controller.chat import ( + # ... 现有导入 ... + PendingTool, + HITLStatusResponse, + HITLConfirmationRequest, + HITLRejectionRequest, + HITLConfirmationResponse, +) +``` + +--- + +### 4. 文件改动汇总 + +| 文件路径 | 改动类型 | 行数变化 | 风险等级 | +|---------|---------|----------|----------| +| `gns3_copilot.py` | 修改/新增 | +200 行 | 🟡 中 | +| `agent_service.py` | 修改 | +10 行 | 🟢 低 | +| `chat.py` (API) | 新增 | +150 行 | 🟢 低 | +| `chat.py` (Schema) | 新增 | +50 行 | 🟢 低 | +| **总计** | - | **+410 行** | - | + +--- + +## 数据库变更 + +### Checkpoint 表结构 + +**表名**:`checkpoints` (LangGraph 自动管理) + +**新增字段**(通过 MessagesState 扩展自动添加): + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `pending_tool_calls` | TEXT (JSON) | 待确认的工具调用列表 | +| `hitl_confirmation_required` | BOOLEAN | 是否需要 HITL 确认 | +| `hitl_session_id` | TEXT | HITL 会话 ID | +| `confirmed_tool_calls` | TEXT (JSON) | 已确认的工具调用 | +| `rejected_tool_calls` | TEXT (JSON) | 已拒绝的工具调用 | + +**迁移说明**: +- LangGraph 自动处理新的 state 字段 +- 无需手动执行数据库迁移 +- 现有 checkpoint 向后兼容 + +--- + +## API 规范 + +### 1. 获取 HITL 状态 + +**端点**:`GET /v3/projects/{project_id}/chat/sessions/{session_id}/hitl-status` + +**响应示例**: +```json +{ + "status": "waiting", + "pending_tools": [ + { + "tool_call_id": "call_abc123", + "tool_name": "execute_multiple_device_config_commands", + "tool_args": { + "project_id": "xxx", + "device_configs": [ + { + "device_name": "R1", + "config_commands": ["interface gig0/0", "ip address 10.0.0.1/24"] + } + ] + }, + "danger_level": "medium", + "description": "配置 1 个设备" + } + ], + "hitl_session_id": "hitl_12345", + "session_id": "chat_session_id" +} +``` + +### 2. 确认执行 + +**端点**:`POST /v3/projects/{project_id}/chat/sessions/{session_id}/hitl/confirm` + +**请求体**: +```json +{ + "confirm_all": true, + "tool_call_ids": null +} +``` + +**或指定工具**: +```json +{ + "confirm_all": false, + "tool_call_ids": ["call_abc123", "call_def456"] +} +``` + +**响应示例**: +```json +{ + "status": "confirmed", + "confirmed_count": 2, + "message": "已确认 2 个工具执行" +} +``` + +### 3. 拒绝执行 + +**端点**:`POST /v3/projects/{project_id}/chat/sessions/{session_id}/hitl/reject` + +**请求体**: +```json +{ + "reject_all": true, + "reason": "配置有误,需要重新规划" +} +``` + +**响应示例**: +```json +{ + "status": "rejected", + "confirmed_count": 0, + "message": "已拒绝 1 个工具执行: execute_multiple_device_config_commands" +} +``` + +--- + +## 前端集成指南 + +### 1. 检测 HITL 状态 + +```javascript +// 定期轮询 HITL 状态 +async function pollHITLStatus(sessionId) { + const response = await fetch( + `/api/v3/projects/${projectId}/chat/sessions/${sessionId}/hitl-status` + ); + const status = await response.json(); + + if (status.status === 'waiting' && status.pending_tools.length > 0) { + showConfirmationDialog(status); + } +} + +// 每 2 秒轮询一次 +setInterval(() => pollHITLStatus(sessionId), 2000); +``` + +### 2. 显示确认对话框 + +```javascript +function showConfirmationDialog(hitlStatus) { + const { pending_tools, hitl_session_id } = hitlStatus; + + const dialog = document.createElement('div'); + dialog.className = 'hitl-confirmation-dialog'; + + let html = ` +