From 216b3d72199e6eca00afebf0f8b15aa0ce47863c Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 6 Mar 2026 01:14:52 +0800 Subject: [PATCH] docs: translate AI chat API design document to English Translate the GNS3 Copilot Agent Chat API design document from Chinese to English to improve accessibility for international contributors and align with project documentation standards. The translation covers all sections including overview, core features, architecture design, API endpoints, and response formats. --- docs/gns3-copilot/ai-chat-api-design.md | 814 +++++++++--------- .../gns3-copilot/context-window-management.md | 250 +++--- .../todo/hitl-implementation-plan.md | 804 ++++++++--------- .../todo/orphan-tool-calls-recovery.md | 302 +++---- .../todo/tool-response-format-standard.md | 162 ++-- .../force-kill-residual-processes.md | 120 +-- 6 files changed, 1226 insertions(+), 1226 deletions(-) diff --git a/docs/gns3-copilot/ai-chat-api-design.md b/docs/gns3-copilot/ai-chat-api-design.md index a3e64d987..3ccbbd7ed 100644 --- a/docs/gns3-copilot/ai-chat-api-design.md +++ b/docs/gns3-copilot/ai-chat-api-design.md @@ -1,20 +1,20 @@ -# GNS3 Copilot Agent Chat API 设计文档 +# GNS3 Copilot Agent Chat API Design Document -## 概述 +## Overview -本文档描述 GNS3 Copilot Chat API 的架构设计和实现方案。该 API 使客户端能够通过 RESTful 接口与 GNS3 Copilot Agent 进行交互,提供流式对话、会话管理、项目拓扑查询等功能。 +This document describes the architectural design and implementation plan for the GNS3 Copilot Chat API. This API enables clients to interact with the GNS3 Copilot Agent through a RESTful interface, providing streaming conversations, session management, and project topology queries. -## 核心特性 +## Core Features -- **项目级隔离**:每个 GNS3 项目拥有独立的 Agent 实例和会话存储 -- **流式响应**:使用 Server-Sent Events (SSE) 实现实时流式输出 -- **会话管理**:支持会话列表、重命名、删除、历史记录查询 -- **统计追踪**:自动记录消息数量、LLM 调用次数、Token 使用量 -- **用户隔离**:每个用户拥有独立的 LLM 配置和会话空间 +- **Project-level Isolation**: Each GNS3 project has its own Agent instance and session storage +- **Streaming Responses**: Uses Server-Sent Events (SSE) for real-time streaming output +- **Session Management**: Supports session listing, renaming, deletion, and history queries +- **Statistics Tracking**: Automatically records message counts, LLM call counts, and token usage +- **User Isolation**: Each user has independent LLM configurations and session spaces -## 架构设计 +## Architecture Design -### 整体架构 +### Overall Architecture ``` Frontend (Web UI) @@ -37,14 +37,14 @@ AgentService (per project) └─ tool_node (GNS3 tools) ``` -### 项目级 Checkpoint 设计 +### Project-level Checkpoint Design -每个 GNS3 项目在项目目录下创建 `gns3-copilot/copilot_checkpoints.db` SQLite 数据库,包含两张表: +Each GNS3 project creates a `gns3-copilot/copilot_checkpoints.db` SQLite database in the project directory, containing two tables: -1. **checkpoints 表**(LangGraph 自动管理):存储 Agent 的对话状态和记忆 -2. **chat_sessions 表**(自定义):存储会话元数据和统计信息 +1. **checkpoints table** (managed by LangGraph): stores Agent conversation state and memory +2. **chat_sessions table** (custom): stores session metadata and statistics -**目录结构**: +**Directory Structure**: ``` {project.path}/ ├── gns3-copilot/ @@ -53,36 +53,36 @@ AgentService (per project) └── project.gns3 ``` -**设计优势**: -- 项目删除时自动清理所有相关数据 -- 实现项目级别的会话隔离 -- 便于备份和迁移 +**Design Advantages**: +- All related data is automatically cleaned up when the project is deleted +- Achieves project-level session isolation +- Facilitates backup and migration -## 用户认证信息传递 +## User Authentication Information Passing -### 背景需求 +### Background Requirements -GNS3 Copilot Agent 需要以下信息才能正常工作: -1. **user_id**:获取用户专属的 LLM 配置 -2. **jwt_token**:调用 GNS3 API 时进行身份验证 -3. **llm_config**:包含 provider、model、api_key 等配置 +GNS3 Copilot Agent requires the following information to work properly: +1. **user_id**: Get user-specific LLM configuration +2. **jwt_token**: Authenticate when calling GNS3 API +3. **llm_config**: Contains provider, model, api_key, etc. -### ContextVars 方案 +### ContextVars Solution -使用 Python 的 `contextvars.ContextVar` 在请求作用域内传递数据,避免敏感信息持久化到 checkpoint。 +Uses Python's `contextvars.ContextVar` to pass data within request scope, avoiding persisting sensitive information to checkpoint. -**数据流**: +**Data Flow**: ``` -1. API 层获取用户信息 - ├─ 从 FastAPI get_current_active_user 获取 user_id - ├─ 从 Authorization header 提取 jwt_token - └─ 从数据库查询 LLM 配置(已解密 API key) +1. API layer gets user information + ├─ Get user_id from FastAPI get_current_active_user + ├─ Extract jwt_token from Authorization header + └─ Query LLM configuration from database (API key already decrypted) -2. 设置 ContextVars(内存临时存储) +2. Set ContextVars (temporary in-memory storage) ├─ set_current_jwt_token(jwt_token) └─ set_current_llm_config(llm_config) -3. 构建安全的 LangGraph config(仅包含非敏感标识符) +3. Build secure LangGraph config (only contains non-sensitive identifiers) { "configurable": { "thread_id": session_id, @@ -93,55 +93,55 @@ GNS3 Copilot Agent 需要以下信息才能正常工作: } } -4. LLM 节点从 ContextVars 获取配置 +4. LLM node gets configuration from ContextVars ├─ get_current_jwt_token() └─ get_current_llm_config() ``` -**方案优势**: -- 敏感数据(JWT token、API key)仅存储在内存中 -- 请求结束后自动清理,不会持久化到数据库 -- 避免序列化/反序列化开销 -- 实现请求级别的数据隔离 +**Solution Advantages**: +- Sensitive data (JWT token, API key) only stored in memory +- Automatically cleared after request ends, not persisted to database +- Avoids serialization/deserialization overhead +- Achieves request-level data isolation -## 会话管理 +## Session Management -### chat_sessions 表结构 +### chat_sessions Table Structure -| 字段 | 类型 | 说明 | -|------|------|------| -| id | INTEGER | 主键(自增) | -| thread_id | TEXT | LangGraph thread_id(唯一) | -| user_id | TEXT | 用户 ID | -| project_id | TEXT | GNS3 项目 ID | -| title | TEXT | 会话标题 | -| message_count | INTEGER | 消息数量 | -| llm_calls_count | INTEGER | LLM 调用次数 | -| input_tokens | INTEGER | 输入 token 总数 | -| output_tokens | INTEGER | 输出 token 总数 | -| total_tokens | INTEGER | 总 token 数 | -| last_message_at | TIMESTAMP | 最后消息时间 | -| created_at | TIMESTAMP | 创建时间 | -| updated_at | TIMESTAMP | 更新时间 | -| metadata | TEXT | 预留元数据(JSON) | -| stats | TEXT | 额外统计信息(JSON) | -| pinned | BOOLEAN | 是否置顶(默认 FALSE) | +| Field | Type | Description | +|-------|------|-------------| +| id | INTEGER | Primary key (auto-increment) | +| thread_id | TEXT | LangGraph thread_id (unique) | +| user_id | TEXT | User ID | +| project_id | TEXT | GNS3 project ID | +| title | TEXT | Session title | +| message_count | INTEGER | Number of messages | +| llm_calls_count | INTEGER | Number of LLM calls | +| input_tokens | INTEGER | Total input tokens | +| output_tokens | INTEGER | Total output tokens | +| total_tokens | INTEGER | Total tokens | +| last_message_at | TIMESTAMP | Last message time | +| created_at | TIMESTAMP | Creation time | +| updated_at | TIMESTAMP | Update time | +| metadata | TEXT | Reserved metadata (JSON) | +| stats | TEXT | Additional statistics (JSON) | +| pinned | BOOLEAN | Whether pinned (default FALSE) | -**索引**: -- `idx_thread_id`:thread_id 唯一索引 -- `idx_user_project`:user_id + project_id 复合索引 -- `idx_pinned_updated`:pinned + updated_at 复合索引(用于置顶排序) +**Indexes**: +- `idx_thread_id`: thread_id unique index +- `idx_user_project`: user_id + project_id composite index +- `idx_pinned_updated`: pinned + updated_at composite index (for pin sorting) -### 数据库迁移 +### Database Migration -**实现位置**:`agent_service.py` 的 `_create_chat_sessions_table` 方法 +**Implementation Location**: `_create_chat_sessions_table` method in `agent_service.py` -**迁移策略**: -- 使用 `PRAGMA table_info(chat_sessions)` 检查列是否存在 -- 如果 `pinned` 列不存在,执行 `ALTER TABLE ADD COLUMN` 添加该列 -- 确保列存在后再创建索引 +**Migration Strategy**: +- Use `PRAGMA table_info(chat_sessions)` to check if columns exist +- If `pinned` column doesn't exist, execute `ALTER TABLE ADD COLUMN` to add it +- Ensure column exists before creating index -**代码示例**: +**Code Example**: ```python # Check if pinned column exists, add it if not (migration for existing databases) cursor = await conn.execute("PRAGMA table_info(chat_sessions)") @@ -157,107 +157,107 @@ if "pinned" not in column_names: await conn.execute("CREATE INDEX IF NOT EXISTS idx_pinned_updated ON chat_sessions(pinned DESC, updated_at DESC)") ``` -**优势**: -- 向后兼容:现有数据库自动升级,无需手动干预 -- 幂等性:重复执行不会报错 -- 零停机:迁移在初始化时自动完成 +**Advantages**: +- Backward compatible: existing databases automatically upgraded without manual intervention +- Idempotent: repeated execution won't cause errors +- Zero downtime: migration happens automatically during initialization ### ChatSessionsRepository -提供会话的 CRUD 操作: +Provides CRUD operations for sessions: -- **create_session**:创建新会话 -- **get_session_by_thread**:根据 thread_id 查询会话 -- **list_sessions**:列出用户的会话(支持过滤和分页,按 pinned 和 updated_at 排序) -- **update_session**:更新会话(支持增量更新计数器) -- **delete_session**:删除会话及其 checkpoints -- **delete_all_sessions**:删除项目的所有会话 -- **pin_session**:置顶或取消置顶会话 +- **create_session**: Create new session +- **get_session_by_thread**: Query session by thread_id +- **list_sessions**: List user's sessions (supports filtering and pagination, sorted by pinned and updated_at) +- **update_session**: Update session (supports incremental counter updates) +- **delete_session**: Delete session and its checkpoints +- **delete_all_sessions**: Delete all sessions in project +- **pin_session**: Pin or unpin session -### 统计信息自动收集 +### Automatic Statistics Collection -统计信息在对话过程中实时收集,流结束后一次性更新到 `chat_sessions` 表。 +Statistics are collected in real-time during conversation, and updated to `chat_sessions` table in one batch after streaming ends. -**实现位置**:`agent_service.py` 的 `stream_chat` 方法 +**Implementation Location**: `stream_chat` method in `agent_service.py` -**统计逻辑**: +**Statistics Logic**: -1. **message_count(消息数量)** - - 初始值:1(用户消息) - - `on_chat_model_end` 事件:+1(AI 完整回复,不是每个 chunk) - - `on_tool_end` 事件:+1(每个工具执行结果) +1. **message_count (number of messages)** + - Initial value: 1 (user message) + - `on_chat_model_end` event: +1 (AI complete reply, not each chunk) + - `on_tool_end` event: +1 (each tool execution result) -2. **llm_calls_count(LLM 调用次数)** - - 监听 `on_chat_model_start` 事件 - - 每次 LLM 开始生成时 +1 +2. **llm_calls_count (number of LLM calls)** + - Listen to `on_chat_model_start` event + - +1 each time LLM starts generation -3. **input_tokens(输入 token)** - - 从 `on_chat_model_end` 事件的 `usage_metadata` 中提取 - - **重要**:LangGraph 返回的 input_tokens 已包含对话历史,每次 LLM 调用都会累加之前的对话内容 - - 示例:第1次 input=8674,第2次 input=9421(包含第1次对话 8674+675+系统提示词增量) +3. **input_tokens (input tokens)** + - Extracted from `usage_metadata` in `on_chat_model_end` event + - **Important**: input_tokens returned by LangGraph already includes conversation history, accumulates previous conversation content on each LLM call + - Example: 1st call input=8674, 2nd call input=9421 (includes 1st conversation 8674+675+system prompt increment) -4. **output_tokens(输出 token)** - - 从 `on_chat_model_end` 事件的 `usage_metadata` 中提取 - - **重要**:LangGraph 返回的 output_tokens 也是累加值,包含所有 LLM 调用的输出 - - 示例:第1次实际输出=675,第2次实际输出=9,累加后 output=684(675+9) +4. **output_tokens (output tokens)** + - Extracted from `usage_metadata` in `on_chat_model_end` event + - **Important**: output_tokens returned by LangGraph is also accumulated value, includes output from all LLM calls + - Example: 1st actual output=675, 2nd actual output=9, accumulated output=684 (675+9) -5. **total_tokens(总 token)** - - 计算公式:input_tokens + output_tokens - - 取最后一次 LLM 调用的累加值进行计算 +5. **total_tokens (total tokens)** + - Calculation formula: input_tokens + output_tokens + - Take the accumulated value from the last LLM call for calculation -**统计示例**(真实数据): -- 第1次 LLM 调用(AI 回复):input=8674, output=675 -- 第2次 LLM 调用(生成标题):input=9421, output=684(累加值:675+9) -- 最终存储:input_tokens=9421, output_tokens=684, total_tokens=10105 -- 说明:LangGraph 已自动累加,代码直接取最后一次值即可 +**Statistics Example** (real data): +- 1st LLM call (AI reply): input=8674, output=675 +- 2nd LLM call (generate title): input=9421, output=684 (accumulated value: 675+9) +- Final storage: input_tokens=9421, output_tokens=684, total_tokens=10105 +- Note: LangGraph automatically accumulates, code can directly take the last value -**注意事项**: -- message_count 统计的是**完整消息**,不是流式 chunks -- Token 数据依赖 LLM 返回的 `usage_metadata`,某些模型可能不支持 -- 统计数据在流结束后通过 `update_session` 方法增量更新到数据库 -- LangGraph 已自动处理 input 和 output 的历史累加,代码使用最后一次 LLM 调用的值 -- **消息 ID 处理**:创建初始消息时分配 ID(`HumanMessage(id=str(uuid4()))`),从 checkpoint 读取的消息如果没有 ID 也会自动生成 -- **格式转换**:使用 `message_converters.py` 模块处理 LangChain 和 OpenAI 格式之间的转换,确保 tool_calls 格式符合 OpenAI 规范 +**Notes**: +- message_count counts **complete messages**, not streaming chunks +- Token data depends on LLM's returned `usage_metadata`, some models may not support +- Statistics are incrementally updated to database via `update_session` method after stream ends +- LangGraph automatically handles input and output history accumulation, code uses the last LLM call value +- **Message ID handling**: Assign ID when creating initial message (`HumanMessage(id=str(uuid4()))`), messages read from checkpoint without ID are also automatically generated +- **Format conversion**: Use `message_converters.py` module to handle conversion between LangChain and OpenAI formats, ensuring tool_calls format conforms to OpenAI specification -### Title 自动同步 +### Automatic Title Synchronization -会话标题由 `title_generator_node` 节点自动生成,保存在 LangGraph checkpoint 的 `conversation_title` 字段中。 +Session title is automatically generated by `title_generator_node` node, saved in `conversation_title` field in LangGraph checkpoint. -**同步机制**: -1. 流式 Chat 完成后,从 checkpoint 读取最终 state -2. 检查 `conversation_title` 是否有变化 -3. 如果有变化,更新到 `chat_sessions` 表 +**Synchronization Mechanism**: +1. After streaming Chat completes, read final state from checkpoint +2. Check if `conversation_title` has changed +3. If changed, update to `chat_sessions` table -**优势**: -- 避免在节点中直接访问数据库(防止循环依赖) -- 所有数据库更新集中在流结束后 -- 逻辑清晰,易于维护 +**Advantages**: +- Avoids accessing database directly in nodes (prevents circular dependencies) +- All database updates concentrated after stream ends +- Clear logic, easy to maintain -## SSE 消息格式 +## SSE Message Format -Chat API 使用 Server-Sent Events (SSE) 进行流式传输。 +Chat API uses Server-Sent Events (SSE) for streaming transmission. -### 消息类型 +### Message Types -| type | 说明 | 包含字段 | -|------|------|----------| -| content | AI 文本内容(流式) | content, message_id (可选) | -| tool_call | LLM 决定调用工具(流式,参数逐次累积) | tool_call (对象, 包含 id, type, function), session_id, message_id (可选) | -| tool_start | 工具开始执行 | tool_name, tool_call_id, session_id | -| tool_end | 工具执行完成 | tool_name, tool_output, session_id | -| error | 错误信息 | error, session_id | -| done | 流结束 | session_id | -| heartbeat | 心跳保活 | session_id | +| type | Description | Included Fields | +|------|-------------|------------------| +| content | AI text content (streaming) | content, message_id (optional) | +| tool_call | LLM decides to call tool (streaming, parameters accumulated gradually) | tool_call (object, includes id, type, function), session_id, message_id (optional) | +| tool_start | Tool starts execution | tool_name, tool_call_id, session_id | +| tool_end | Tool execution complete | tool_name, tool_output, session_id | +| error | Error message | error, session_id | +| done | Stream end | session_id | +| heartbeat | Heartbeat keepalive | session_id | -### 消息示例 +### Message Examples ```json -// AI 文本流式输出 +// AI text streaming output {"type": "content", "content": "Hello! How can I help"} -// LLM 决定调用工具(流式传输,参数逐次累积) +// LLM decides to call tool (streaming transmission, parameters accumulated gradually) -// 第 1 个 chunk:工具调用开始(参数为空) +// 1st chunk: tool call starts (parameters empty) { "type": "tool_call", "tool_call": { @@ -268,7 +268,7 @@ Chat API 使用 Server-Sent Events (SSE) 进行流式传输。 "session_id": "xxx" } -// 第 2 个 chunk:参数累积中 +// 2nd chunk: parameters accumulating { "type": "tool_call", "tool_call": { @@ -279,7 +279,7 @@ Chat API 使用 Server-Sent Events (SSE) 进行流式传输。 "session_id": "xxx" } -// 第 3 个 chunk:参数累积中 +// 3rd chunk: parameters accumulating { "type": "tool_call", "tool_call": { @@ -290,7 +290,7 @@ Chat API 使用 Server-Sent Events (SSE) 进行流式传输。 "session_id": "xxx" } -// 第 4 个 chunk:参数完整(标记 complete=true) +// 4th chunk: parameters complete (mark complete=true) { "type": "tool_call", "tool_call": { @@ -305,7 +305,7 @@ Chat API 使用 Server-Sent Events (SSE) 进行流式传输。 "session_id": "xxx" } -// 工具开始执行(通过 tool_call_id 关联) +// Tool starts execution (associated via tool_call_id) { "type": "tool_start", "tool_name": "execute_multiple_device_commands", @@ -313,7 +313,7 @@ Chat API 使用 Server-Sent Events (SSE) 进行流式传输。 "session_id": "xxx" } -// 工具执行完成 +// Tool execution complete { "type": "tool_end", "tool_name": "execute_multiple_device_commands", @@ -321,41 +321,41 @@ Chat API 使用 Server-Sent Events (SSE) 进行流式传输。 "session_id": "xxx" } -// 流结束 +// Stream end {"type": "done", "session_id": "xxx"} -// 错误 +// Error {"type": "error", "error": "Project not found", "session_id": "xxx"} ``` -### 流式工具调用机制 +### Streaming Tool Call Mechanism -**背景**:LLM 生成工具调用参数时是逐字符流式输出的,就像文本内容一样。 +**Background**: When LLM generates tool call parameters, it outputs character by character like text content. -**实现**:使用 `ToolCallStreamAccumulator` 类维护状态,处理三个阶段: +**Implementation**: Use `ToolCallStreamAccumulator` class to maintain state, handling three phases: -1. **初始化阶段**:从 `tool_calls` 获取工具 ID 和名称,发送初始 `tool_call` 事件(参数为空) +1. **Initialization Phase**: Get tool ID and name from `tool_calls`, send initial `tool_call` event (parameters empty) -2. **累积阶段**:从 `tool_call_chunks` 逐个获取参数片段,通过字符串拼接累积完整参数,每次累积后发送更新的 `tool_call` 事件 +2. **Accumulation Phase**: Get parameter fragments from `tool_call_chunks`, accumulate complete parameters via string concatenation, send updated `tool_call` event after each accumulation -3. **完成阶段**:检测 `finish_reason == "tool_calls"` 或 `"stop"`,发送最终 `tool_call` 事件(标记 `complete: true`) +3. **Completion Phase**: Detect `finish_reason == "tool_calls"` or `"stop"`, send final `tool_call` event (mark `complete: true`) -**前端处理**: -- 收到 `tool_call` 事件时,根据 `tool_call.id` 判断是否为新工具调用 -- 同一个 ID 的后续事件用于更新参数显示 -- 当 `function.complete: true` 时,参数已完整,可以执行工具 -- `tool_start` 事件包含 `tool_call_id`,可以关联到之前的 `tool_call` 事件 +**Frontend Handling**: +- When receiving `tool_call` event, determine if it's a new tool call based on `tool_call.id` +- Subsequent events with same ID are used to update parameter display +- When `function.complete: true`, parameters are complete, tool can be executed +- `tool_start` event contains `tool_call_id`, can associate with previous `tool_call` event -**示例代码**(前端): +**Example Code** (frontend): ```javascript -// 维护当前工具调用状态 +// Maintain current tool call state let currentToolCall = null; function handleToolCallEvent(chunk) { const toolCall = chunk.tool_call; if (!currentToolCall || currentToolCall.id !== toolCall.id) { - // 新工具调用 + // New tool call currentToolCall = { id: toolCall.id, name: toolCall.function.name, @@ -364,257 +364,257 @@ function handleToolCallEvent(chunk) { }; displayToolCallStarted(currentToolCall); } else { - // 更新现有工具调用的参数 + // Update existing tool call parameters currentToolCall.arguments = toolCall.function.arguments; currentToolCall.complete = toolCall.function.complete || false; updateToolCallArguments(currentToolCall); } if (currentToolCall.complete) { - // 参数完整,准备执行工具 + // Parameters complete, ready to execute tool displayToolCallReady(currentToolCall); } } ``` -### 心跳机制 +### Heartbeat Mechanism -**作用**:防止代理服务器/负载均衡器因超时断开 SSE 连接。 +**Purpose**: Prevent proxy server/load balancer from disconnecting SSE connection due to timeout. -**实现**:使用 `asyncio.wait` 设置超时,超时后发送 `heartbeat` 消息,然后继续等待下一个事件。 +**Implementation**: Use `asyncio.wait` to set timeout, send `heartbeat` message after timeout, then continue waiting for next event. -**前端处理**:收到 `heartbeat` 消息时直接忽略,不渲染任何内容。 +**Frontend Handling**: When receiving `heartbeat` message, ignore it directly, don't render anything. -## API 端点 +## API Endpoints -所有端点都在 `/v3/projects/{project_id}/chat/` 路径下。 +All endpoints are under `/v3/projects/{project_id}/chat/` path. -| 方法 | 端点 | 说明 | -|------|------|------| -| POST | `/stream` | 流式 Chat(主要接口) | -| GET | `/sessions` | 列出会话(按置顶和更新时间排序) | -| GET | `/sessions/{session_id}/history` | 获取会话历史 | -| PATCH | `/sessions/{session_id}` | 重命名会话 | -| DELETE | `/sessions/{session_id}` | 删除会话 | -| PUT | `/sessions/{session_id}/pin` | 置顶会话 | -| DELETE | `/sessions/{session_id}/pin` | 取消置顶会话 | +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/stream` | Streaming Chat (main interface) | +| GET | `/sessions` | List sessions (sorted by pin and update time) | +| GET | `/sessions/{session_id}/history` | Get session history | +| PATCH | `/sessions/{session_id}` | Rename session | +| DELETE | `/sessions/{session_id}` | Delete session | +| PUT | `/sessions/{session_id}/pin` | Pin session | +| DELETE | `/sessions/{session_id}/pin` | Unpin session | ### POST /v3/projects/{project_id}/chat/stream -**功能**:流式对话接口 +**Function**: Streaming conversation interface -**请求参数**: -- message: 用户消息内容 -- session_id: 会话 ID(可选,不提供则自动创建新会话) -- stream: 是否启用流式响应(默认 true) -- temperature: LLM temperature 参数(注意:当前未使用,保留以备将来实现。实际 temperature 从用户的数据库 LLM 配置中读取) -- mode: 交互模式(当前仅支持 "text") +**Request Parameters**: +- message: User message content +- session_id: Session ID (optional, creates new session if not provided) +- stream: Enable streaming response (default true) +- temperature: LLM temperature parameter (Note: currently unused, reserved for future implementation. Actual temperature is read from user's database LLM configuration) +- mode: Interaction mode (currently only supports "text") -**响应**:SSE 流,包含多种类型的消息(见上文消息格式) +**Response**: SSE stream, contains multiple types of messages (see message format above) -**项目状态检查**:只允许项目状态为 "opened" 时进行对话 +**Project Status Check**: Only allows conversation when project status is "opened" ### GET /v3/projects/{project_id}/chat/sessions -**功能**:列出项目的所有会话 +**Function**: List all sessions in a project -**响应**:会话列表,包含统计信息(消息数、token 使用量等),按置顶状态和更新时间排序 +**Response**: Session list, includes statistics (message count, token usage, etc.), sorted by pin status and update time ### GET /v3/projects/{project_id}/chat/sessions/{session_id}/history -**功能**:获取会话的完整历史记录 +**Function**: Get complete history of a session -**参数**: -- session_id: 会话 ID -- limit: 最大消息数量(默认 100) +**Parameters**: +- session_id: Session ID +- limit: Maximum number of messages (default 100) -**响应**: -- thread_id: 会话 ID -- title: 会话标题 -- messages: 消息列表(OpenAI 格式) -- llm_calls: LLM 调用次数 +**Response**: +- thread_id: Session ID +- title: Session title +- messages: Message list (OpenAI format) +- llm_calls: Number of LLM calls ### PATCH /v3/projects/{project_id}/chat/sessions/{session_id} -**功能**:重命名会话 +**Function**: Rename session -**请求参数**: -- title: 新标题(1-255 字符) +**Request Parameters**: +- title: New title (1-255 characters) -**响应**:更新后的会话信息 +**Response**: Updated session information ### DELETE /v3/projects/{project_id}/chat/sessions/{session_id} -**功能**:删除会话及其所有 checkpoint 数据 +**Function**: Delete session and all its checkpoint data -**响应**:204 No Content +**Response**: 204 No Content ### PUT /v3/projects/{project_id}/chat/sessions/{session_id}/pin -**功能**:置顶会话到列表顶部 +**Function**: Pin session to top of list -**响应**:更新后的会话信息(包含 pinned=true) +**Response**: Updated session information (includes pinned=true) ### DELETE /v3/projects/{project_id}/chat/sessions/{session_id}/pin -**功能**:取消置顶会话 +**Function**: Unpin session -**响应**:更新后的会话信息(包含 pinned=false) +**Response**: Updated session information (includes pinned=false) -**排序规则**: -- 置顶会话(pinned=true)排在最前面 -- 置顶会话之间按 updated_at 降序排列 -- 普通会话按 updated_at 降序排列 +**Sorting Rules**: +- Pinned sessions (pinned=true) appear at the front +- Among pinned sessions, sort by updated_at descending +- Normal sessions sort by updated_at descending -## 数据模型 +## Data Models ### ChatRequest -- message: str - 用户消息内容 -- session_id: Optional[str] - 会话 ID(可选) -- stream: bool - 是否流式响应(默认 true) -- temperature: Optional[float] - LLM temperature 参数(注意:当前未使用,保留以备将来实现运行时覆盖。当前 temperature 从用户的数据库 LLM 配置中读取) -- mode: Literal["text"] - 交互模式 +- message: str - User message content +- session_id: Optional[str] - Session ID (optional) +- stream: bool - Enable streaming response (default true) +- temperature: Optional[float] - LLM temperature parameter (Note: currently unused, reserved for future runtime override implementation. Current temperature is read from user's database LLM configuration) +- mode: Literal["text"] - Interaction mode ### ChatSession -会话模型,存储会话元数据和统计信息。 +Session model, stores session metadata and statistics. -**基础字段**: -- id: 数据库自增 ID -- thread_id: LangGraph thread_id(会话唯一标识) -- user_id: 用户 ID -- project_id: GNS3 项目 ID -- title: 会话标题(自动生成或用户修改) +**Base Fields**: +- id: Database auto-increment ID +- thread_id: LangGraph thread_id (session unique identifier) +- user_id: User ID +- project_id: GNS3 project ID +- title: Session title (auto-generated or user-modified) -**统计字段**: -- message_count: 完整消息数量(用户消息 + AI 回复 + 工具结果) -- llm_calls_count: LLM 总调用次数 -- input_tokens: 输入 token 总数(累加所有 LLM 调用) -- output_tokens: 输出 token 总数(累加所有 LLM 调用) -- total_tokens: 总 token 数(input_tokens + output_tokens) +**Statistics Fields**: +- message_count: Complete message count (user messages + AI replies + tool results) +- llm_calls_count: Total LLM call count +- input_tokens: Total input tokens (accumulated across all LLM calls) +- output_tokens: Total output tokens (accumulated across all LLM calls) +- total_tokens: Total tokens (input_tokens + output_tokens) -**时间字段**: -- last_message_at: 最后一条消息的时间戳 -- created_at: 会话创建时间 -- updated_at: 会话最后更新时间 +**Time Fields**: +- last_message_at: Timestamp of last message +- created_at: Session creation time +- updated_at: Session last update time -**预留字段**: -- metadata: 元数据 JSON 字符串(存储 mode、status、tags 等) -- stats: 额外统计 JSON 字符串(存储工具调用次数等) +**Reserved Fields**: +- metadata: Metadata JSON string (stores mode, status, tags, etc.) +- stats: Additional statistics JSON string (stores tool call counts, etc.) -**会话管理**: -- pinned: 是否置顶到列表顶部(默认 false) +**Session Management**: +- pinned: Whether pinned to top of list (default false) ### ConversationHistory -- thread_id: str - 会话 ID -- title: str - 会话标题 -- messages: List[OpenAIMessage] - 消息列表 -- created_at: Optional[str] - 创建时间 -- updated_at: Optional[str] - 更新时间 -- llm_calls: int - LLM 调用次数 +- thread_id: str - Session ID +- title: str - Session title +- messages: List[OpenAIMessage] - Message list +- created_at: Optional[str] - Creation time +- updated_at: Optional[str] - Update time +- llm_calls: int - Number of LLM calls ### OpenAIMessage -OpenAI 兼容的消息模型。 +OpenAI-compatible message model. -**基础字段**: -- id: str - 消息唯一标识符(自动生成或从 LangChain 消息继承) -- role: Literal["user", "assistant", "system", "tool"] - 消息角色 -- content: str - 消息内容(支持文本、JSON 字符串) -- created_at: str - 创建时间(ISO 8601) +**Base Fields**: +- id: str - Message unique identifier (auto-generated or inherited from LangChain message) +- role: Literal["user", "assistant", "system", "tool"] - Message role +- content: str - Message content (supports text, JSON string) +- created_at: str - Creation time (ISO 8601) -**工具相关字段**: -- name: Optional[str] - 工具消息名称(tool 消息) -- tool_call_id: Optional[str] - 关联的工具调用 ID(tool 消息) -- tool_calls: Optional[List[OpenAIToolCall]] - 工具调用列表(assistant 消息) - - id: str - 工具调用 ID - - type: Literal["function"] - 固定为 "function" - - function: Dict - 包含 name 和 arguments(dict 或 JSON 字符串) +**Tool-related Fields**: +- name: Optional[str] - Tool message name (tool message) +- tool_call_id: Optional[str] - Associated tool call ID (tool message) +- tool_calls: Optional[List[OpenAIToolCall]] - Tool call list (assistant message) + - id: str - Tool call ID + - type: Literal["function"] - Fixed as "function" + - function: Dict - Contains name and arguments (dict or JSON string) -**元数据**: -- metadata: Optional[Dict] - 额外的消息元数据 +**Metadata**: +- metadata: Optional[Dict] - Additional message metadata -## 核心组件 +## Core Components -### Message Converters(消息格式转换) +### Message Converters (Message Format Conversion) -**文件**:`gns3server/agent/gns3_copilot/utils/message_converters.py` +**File**: `gns3server/agent/gns3_copilot/utils/message_converters.py` -**职责**:在 LangChain 消息格式和 OpenAI 兼容格式之间进行转换 +**Responsibility**: Convert between LangChain message format and OpenAI-compatible format -**主要函数**: -- `convert_langchain_to_openai()`:LangChain → OpenAI 格式 -- `convert_openai_to_langchain()`:OpenAI → LangChain 格式 -- `convert_stream_event_to_openai()`:流事件 → OpenAI SSE 格式 +**Main Functions**: +- `convert_langchain_to_openai()`: LangChain → OpenAI format +- `convert_openai_to_langchain()`: OpenAI → LangChain format +- `convert_stream_event_to_openai()`: Stream event → OpenAI SSE format -**关键转换逻辑**: +**Key Conversion Logic**: -1. **消息 ID 处理** - - 如果消息没有 ID,自动生成 UUID - - 确保所有返回的消息都有唯一标识符 +1. **Message ID Handling** + - Auto-generate UUID if message has no ID + - Ensure all returned messages have unique identifier -2. **Tool Calls 格式转换** - - LangChain 格式:`{'name': 'xxx', 'args': {...}, 'id': 'yyy', 'type': 'tool_call'}` - - OpenAI 格式:`{'id': 'yyy', 'type': 'function', 'function': {'name': 'xxx', 'arguments': '{...}'}}` - - 自动将 `args` 对象转换为 JSON 字符串(如需要) +2. **Tool Calls Format Conversion** + - LangChain format: `{'name': 'xxx', 'args': {...}, 'id': 'yyy', 'type': 'tool_call'}` + - OpenAI format: `{'id': 'yyy', 'type': 'function', 'function': {'name': 'xxx', 'arguments': '{...}'}}` + - Automatically convert `args` object to JSON string (if needed) -3. **Content 类型处理** - - 支持 string、dict、list 类型 - - 非 string 类型自动转换为 JSON 字符串 +3. **Content Type Handling** + - Supports string, dict, list types + - Non-string types automatically converted to JSON string -**实现位置**:`utils/message_converters.py` +**Implementation Location**: `utils/message_converters.py` ### AgentService -**职责**:项目级的 Agent 管理服务 +**Responsibility**: Project-level Agent management service -**主要方法**: -- `stream_chat`:流式对话,自动管理会话和统计 -- `get_history`:获取会话历史 -- `list_sessions`:列出会话 -- `delete_session`:删除会话 -- `rename_session`:重命名会话 -- `close`:关闭数据库连接 +**Main Methods**: +- `stream_chat`: Streaming conversation, automatically manages sessions and statistics +- `get_history`: Get session history +- `list_sessions`: List sessions +- `delete_session`: Delete session +- `rename_session`: Rename session +- `close`: Close database connection -**核心流程**(stream_chat): -1. 初始化 checkpointer 连接(如果未连接) -2. 获取或创建 chat session(从 `chat_sessions` 表) -3. 设置 ContextVars(JWT token、LLM config) -4. 构建 LangGraph config -5. 创建带 ID 的初始消息:`HumanMessage(content=message, id=str(uuid4()))` -6. 流式执行 Agent,同时收集统计信息 -7. 流结束后更新会话统计到数据库 -8. 同步 auto-generated title +**Core Flow** (stream_chat): +1. Initialize checkpointer connection (if not connected) +2. Get or create chat session (from `chat_sessions` table) +3. Set ContextVars (JWT token, LLM config) +4. Build LangGraph config +5. Create initial message with ID: `HumanMessage(content=message, id=str(uuid4()))` +6. Stream Agent execution, collecting statistics simultaneously +7. Update session statistics to database after stream ends +8. Sync auto-generated title -**统计收集机制**(在 `stream_chat` 中): +**Statistics Collection Mechanism** (in `stream_chat`): -- 监听 LangGraph 的 `astream_events` 事件流 -- 在事件循环中实时收集统计数据 -- 统计逻辑不依赖转换后的 SSE chunk,直接从原始事件获取 +- Listen to LangGraph's `astream_events` event stream +- Collect statistics in real-time during event loop +- Statistics logic doesn't depend on converted SSE chunk, gets directly from original events -**关键事件处理**: -- `on_chat_model_start`:LLM 调用次数 +1 -- `on_chat_model_end`:提取 token 使用量(从 `output.usage_metadata`),AI 消息计数 +1 -- `on_tool_end`:工具消息计数 +1 +**Key Event Handling**: +- `on_chat_model_start`: LLM call count +1 +- `on_chat_model_end`: Extract token usage (from `output.usage_metadata`), AI message count +1 +- `on_tool_end`: Tool message count +1 -**实现位置**:`agent_service.py` +**Implementation Location**: `agent_service.py` ### ProjectAgentManager -**职责**:全局单例,管理所有项目的 AgentService 实例 +**Responsibility**: Global singleton, manages AgentService instances for all projects -**方法**: -- `get_agent(project_id, project_path)`:获取或创建项目的 AgentService -- `remove_agent(project_id)`:移除项目的 AgentService -- `close_all`:关闭所有 AgentService +**Methods**: +- `get_agent(project_id, project_path)`: Get or create project's AgentService +- `remove_agent(project_id)`: Remove project's AgentService +- `close_all`: Close all AgentService ### Chat API Routes -**文件**:`gns3server/api/routes/controller/chat.py` +**File**: `gns3server/api/routes/controller/chat.py` -**路由注册**: +**Route Registration**: ```python router.include_router( chat.router, @@ -623,110 +623,110 @@ router.include_router( ) ``` -**主要端点实现**: -- 所有端点都需要用户认证(`get_current_active_user`) -- 所有端点都检查项目状态是否为 "opened" -- stream 端点使用 `StreamingResponse` 返回 SSE 流 +**Main Endpoint Implementation**: +- All endpoints require user authentication (`get_current_active_user`) +- All endpoints check if project status is "opened" +- stream endpoint uses `StreamingResponse` to return SSE stream -## 项目生命周期集成 +## Project Lifecycle Integration -### 项目打开时 +### When Project Opens -创建或获取 AgentService 实例: +Create or get AgentService instance: ```python agent_manager = await get_project_agent_manager() agent_service = await agent_manager.get_agent(project_id, project.path) ``` -### 项目关闭时 +### When Project Closes -移除 AgentService 实例,释放资源: +Remove AgentService instance, release resources: ```python agent_manager.remove_agent(project_id) ``` -### 项目删除时 +### When Project Deletes -1. 调用 `delete_all_sessions(project_id)` 删除所有会话和 checkpoint 数据 -2. 移除 AgentService 实例 -3. 项目目录被删除,数据库文件也被删除 +1. Call `delete_all_sessions(project_id)` to delete all sessions and checkpoint data +2. Remove AgentService instance +3. Project directory is deleted, database file is also deleted -## 前端集成 +## Frontend Integration ### useChat Hook -根据 SSE 消息的 `type` 字段进行不同处理: +Handle different types based on SSE message's `type` field: -| type | 处理逻辑 | -|------|----------| -| content | 追加到当前 AI 消息内容 | -| tool_call | 创建 tool_call 类型消息,显示工具调用信息 | -| tool_start | 可选:显示工具开始执行状态 | -| tool_end | 创建 tool_result 类型消息,显示工具执行结果 | -| error | 显示错误信息 | -| done | 标记流结束,停止加载状态 | -| heartbeat | 忽略(保活信号) | +| type | Handling Logic | +|------|----------------| +| content | Append to current AI message content | +| tool_call | Create tool_call type message, display tool call information | +| tool_start | Optional: show tool start execution status | +| tool_end | Create tool_result type message, display tool execution result | +| error | Display error message | +| done | Mark stream end, stop loading state | +| heartbeat | Ignore (keepalive signal) | -### 错误处理 +### Error Handling -- 网络错误:显示重试选项 -- LLM 错误:显示错误消息 -- 项目未打开:提示用户打开项目 -- LLM 未配置:引导用户配置 LLM +- Network error: Show retry option +- LLM error: Show error message +- Project not opened: Prompt user to open project +- LLM not configured: Guide user to configure LLM -## 安全考虑 +## Security Considerations -### 用户隔离 +### User Isolation -- 每个用户只能访问自己的会话 -- user_id 存储在 config.metadata 中 -- 所有数据库查询都带 user_id 过滤 +- Each user can only access their own sessions +- user_id stored in config.metadata +- All database queries filtered by user_id -### 项目访问控制 +### Project Access Control -- 只允许访问用户有权限的项目 -- 项目状态检查:只允许 "opened" 状态的项目使用 Chat +- Only allow access to projects user has permission for +- Project status check: only allow "opened" status projects to use Chat -### LLM 配置安全 +### LLM Configuration Security -- API key 加密存储在数据库 -- 使用 ContextVars 传递,不持久化到 checkpoint -- 请求结束后自动清理内存中的敏感信息 +- API key encrypted storage in database +- Pass via ContextVars, not persisted to checkpoint +- Automatically clear sensitive information in memory after request ends -## 性能优化 +## Performance Optimization -### 数据库连接管理 +### Database Connection Management -- 使用 WAL 模式提升并发写入性能 -- 项目级连接复用 -- 项目切换时自动关闭旧连接 +- Use WAL mode to improve concurrent write performance +- Project-level connection reuse +- Automatically close old connections when switching projects -### Checkpoint 优化 +### Checkpoint Optimization -- LangGraph 自动管理 checkpoints 表 -- 定期清理旧 checkpoint(可选) -- 使用索引加速查询(thread_id, user_id + project_id) +- LangGraph automatically manages checkpoints table +- Periodically clean old checkpoints (optional) +- Use indexes to accelerate queries (thread_id, user_id + project_id) -### 统计信息收集与更新 +### Statistics Collection and Update -**收集机制**(在内存中进行): -- 在 SSE 流式传输过程中同步收集统计数据 -- 监听 LangGraph 事件流,不增加额外网络开销 -- 使用临时变量累加统计值,避免频繁数据库访问 +**Collection Mechanism** (in-memory): +- Collect statistics synchronously during SSE streaming transmission +- Listen to LangGraph event stream, no additional network overhead +- Use temporary variables to accumulate statistics, avoid frequent database access -**更新策略**(流结束后批量写入): -- 流式 Chat 完成后,一次性更新 `chat_sessions` 表 -- 使用 SQL 增量更新语法:`message_count = message_count + ?` -- 单次数据库事务,提交所有统计更新 +**Update Strategy** (batch write after stream ends): +- After streaming Chat completes, update `chat_sessions` table in one batch +- Use SQL incremental update syntax: `message_count = message_count + ?` +- Single database transaction, commit all statistic updates -**优势**: -- 减少数据库写入次数(N 次事件 → 1 次更新) -- 降低数据库锁竞争 -- 提升流式响应的实时性 +**Advantages**: +- Reduce database write count (N events → 1 update) +- Lower database lock contention +- Improve real-time performance of streaming response -**实现位置**:`agent_service.py` 第 283-294 行 +**Implementation Location**: `agent_service.py` lines 283-294 -## 依赖项 +## Dependencies - `langchain` >= 0.3.0 - `langgraph` >= 0.2.0 @@ -734,47 +734,47 @@ agent_manager.remove_agent(project_id) - `langgraph-checkpoint-sqlite` >= 3.0.1 - `aiosqlite` -## 扩展性 +## Extensibility -### 预留字段 +### Reserved Fields -- `metadata`(TEXT JSON):存储会话级别的元数据 -- `stats`(TEXT JSON):存储额外的统计信息 +- `metadata` (TEXT JSON): Store session-level metadata +- `stats` (TEXT JSON): Store additional statistics -### 未来可能的扩展 +### Future Possible Extensions -#### 运行时 LLM 参数覆盖 +#### Runtime LLM Parameter Override -当前 LLM 配置(包括 temperature、max_tokens 等)从用户的数据库配置中读取。将来可以支持在请求时覆盖这些参数: +Current LLM configuration (including temperature, max_tokens, etc.) is read from user's database configuration. Future support for overriding these parameters at request time: -**实现方案**: +**Implementation Plan**: ```python -# 在 chat.py 的 stream_chat 函数中 +# In chat.py's stream_chat function if request.temperature is not None: llm_config["temperature"] = str(request.temperature) if request.max_tokens is not None: llm_config["max_tokens"] = str(request.max_tokens) ``` -**当前状态**: -- `temperature` 参数已添加到 ChatRequest schema,但未实现覆盖逻辑 -- 参数保留在 API 中以保持向后兼容性 -- 代码中已添加 TODO 注释标记实现位置 +**Current Status**: +- `temperature` parameter already added to ChatRequest schema, but override logic not implemented +- Parameter reserved in API for backward compatibility +- TODO comments added in code to mark implementation location -**注意事项**: -- 需要验证参数范围(如 temperature: 0.0-2.0) -- 需要考虑是否记录覆盖值到统计信息 -- 需要在前端 UI 中提供相应的设置选项 +**Notes**: +- Need to validate parameter ranges (e.g., temperature: 0.0-2.0) +- Need to consider whether to record override values to statistics +- Need to provide corresponding settings in frontend UI -#### 其他扩展方向 +#### Other Extension Directions -- 多模态支持(图片、文件) -- 语音输入/输出 -- 多人协作会话 -- 会话分享和导出 -- 自定义工具注册 +- Multi-modal support (images, files) +- Voice input/output +- Multi-user collaboration sessions +- Session sharing and export +- Custom tool registration -## 参考资料 +## References - [LangGraph Checkpoint Documentation](https://langchain-ai.github.io/langgraph/how-tos/checkpointers/) - [Server-Sent Events (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) diff --git a/docs/gns3-copilot/context-window-management.md b/docs/gns3-copilot/context-window-management.md index 97a4770e2..2515fe72a 100644 --- a/docs/gns3-copilot/context-window-management.md +++ b/docs/gns3-copilot/context-window-management.md @@ -1,152 +1,152 @@ -# LLM 上下文窗口管理实现文档 +# LLM Context Window Management Implementation Document -## 概述 +## Overview -本文档说明 GNS3 Copilot 的上下文窗口管理实现机制,包括消息裁剪、Token 计数和配置验证。 +This document explains the context window management implementation mechanism for GNS3 Copilot, including message trimming, token counting, and configuration validation. -## 实现架构 +## Implementation Architecture -### 1. 核心模块 +### 1. Core Modules -**文件位置**: `gns3server/agent/gns3_copilot/agent/context_manager.py` +**File Location**: `gns3server/agent/gns3_copilot/agent/context_manager.py` -#### Token 计数策略 +#### Token Counting Strategy -系统使用 **tiktoken** 进行 Token 计数(context_manager.py:60): +The system uses **tiktoken** for token counting (context_manager.py:60): ```python _tiktoken_encoding = tiktoken.get_encoding("cl100k_base") ``` -**必需依赖**: +**Required Dependency**: ```bash pip install tiktoken>=0.8.0 ``` -如果未安装 tiktoken,系统将在启动时抛出 `ModuleNotFoundError`。 +If tiktoken is not installed, the system will throw a `ModuleNotFoundError` at startup. -#### 关键函数 +#### Key Functions **`count_tokens(text: str) -> int`** (context_manager.py:84-100) -- 使用 tiktoken 准确计数文本的 token 数 -- 使用 `cl100k_base` 编码 -- 返回精确的 token 数 +- Uses tiktoken to accurately count tokens in text +- Uses `cl100k_base` encoding +- Returns the exact token count **`estimate_tool_tokens(tools: list) -> int`** (context_manager.py:103-169) -- 序列化工具 schema 为 JSON -- 使用 tiktoken 计数工具定义的 token 消耗 -- 支持 Pydantic v1/v2 兼容性 -- 失败时使用 1000 tokens 的回退值 +- Serializes tool schema to JSON +- Uses tiktoken to count token consumption of tool definitions +- Supports Pydantic v1/v2 compatibility +- Falls back to 1000 tokens on failure **`create_pre_model_hook(...)`** (context_manager.py:195-402) -- 创建预处理函数(pre_model_hook) -- 在每次 LLM 调用前自动执行: - 1. 注入 topology 信息到 system prompt - 2. 估算工具定义的 token 消耗 - 3. 裁剪消息历史以适应上下文限制 -- 返回一个可调用的函数,用于准备消息 +- Creates a preprocessing function (pre_model_hook) +- Automatically executes before each LLM call: + 1. Injects topology information into system prompt + 2. Estimates token consumption of tool definitions + 3. Trims message history to fit context limits +- Returns a callable function for preparing messages -### 2. 裁剪逻辑详解 +### 2. Detailed Trimming Logic -#### 2.1 Token 预算分配 +#### 2.1 Token Budget Allocation -当调用 LLM 时,发送的内容包含两部分: +When calling the LLM, the content sent consists of two parts: ``` -发送给 LLM 的完整请求: +Complete request sent to LLM: ┌─────────────────────────────────────────────────────────────┐ -│ 1. Messages (我们管理的) │ -│ ├─ SystemMessage: system prompt + topology (模板注入) │ -│ └─ HumanMessage/AIMessage: 用户消息 / 历史消息 │ +│ 1. Messages (managed by us) │ +│ ├─ SystemMessage: system prompt + topology (template injection) │ +│ └─ HumanMessage/AIMessage: user messages / history messages │ ├─────────────────────────────────────────────────────────────┤ -│ 2. Tool Definitions (LangChain 自动添加,不在消息中) │ +│ 2. Tool Definitions (LangChain adds automatically, not in messages) │ │ ├─ Tool 1 schema (name, description, parameters) │ │ ├─ Tool 2 schema │ -│ └─ ... (约 500-1500 tokens per tool) │ +│ └─ ... (about 500-1500 tokens per tool) │ └─────────────────────────────────────────────────────────────┘ ``` -**System Message 结构**: -- 使用模板变量 `{{topology_info}}` 动态注入 topology -- System prompt 包含占位符:`"### CURRENT TOPOLOGY\n{{topology_info}}"` -- 如果有 topology,替换为实际内容 -- 如果没有 topology,替换为 `"(No topology information available)"` +**System Message Structure**: +- Uses template variable `{{topology_info}}` to dynamically inject topology +- System prompt contains placeholder: `"### CURRENT TOPOLOGY\n{{topology_info}}"` +- If topology exists, replaces with actual content +- If no topology, replaces with `"(No topology information available)"` -#### 2.2 裁剪流程 +#### 2.2 Trimming Process ``` -第一步:计算输入预算 +Step 1: Calculate Input Budget ┌─────────────────────────────────────────────────────────────┐ │ context_limit: 128,000 tokens (128K) │ │ strategy: balanced (75%) │ │ │ -│ 输入预算 = 128 × 1000 × 0.75 = 96,000 tokens │ +│ Input budget = 128 × 1000 × 0.75 = 96,000 tokens │ └─────────────────────────────────────────────────────────────┘ ↓ -第二步:减去工具定义 +Step 2: Subtract Tool Definitions ┌─────────────────────────────────────────────────────────────┐ -│ 输入预算: 96,000 tokens │ -│ 工具定义: 1,725 tokens │ +│ Input budget: 96,000 tokens │ +│ Tool definitions: 1,725 tokens │ │ │ -│ 可用于消息 = 96,000 - 1,725 = 94,275 tokens │ +│ Available for messages = 96,000 - 1,725 = 94,275 tokens │ └─────────────────────────────────────────────────────────────┘ ↓ -第三步:trim_messages 处理 +Step 3: trim_messages Processing ┌─────────────────────────────────────────────────────────────┐ -│ 调用 LangChain 的 trim_messages: │ -│ - max_tokens = 94,275 (包含 system message) │ -│ - strategy = "last" (保留最新消息) │ -│ - token_counter = tiktoken 计数函数 │ -│ - include_system = True (始终保留 system) │ +│ Call LangChain's trim_messages: │ +│ - max_tokens = 94,275 (includes system message) │ +│ - strategy = "last" (keep latest messages) │ +│ - token_counter = tiktoken counting function │ +│ - include_system = True (always keep system) │ │ │ -│ trim_messages 会: │ -│ 1. 保留 SystemMessage (system + topology) │ -│ 2. 从最新消息开始,保留尽可能多的历史 │ -│ 3. 超出限制时,丢弃最旧的消息 │ +│ trim_messages will: │ +│ 1. Keep SystemMessage (system + topology) │ +│ 2. Starting from latest messages, keep as much history │ +│ 3. When exceeding limit, discard oldest messages │ └─────────────────────────────────────────────────────────────┘ ``` -#### 2.3 裁剪优先级 +#### 2.3 Trimming Priority -系统按以下优先级保留内容: +The system preserves content in the following priority order: -| 优先级 | 内容 | 说明 | -|--------|------|------| -| 1️⃣ | System Message (system prompt + topology) | 永远保留 | -| 2️⃣ | 最新用户消息 | 至少保留最后1条 | -| 3️⃣ | 旧对话历史 | 按时间顺序丢弃 | +| Priority | Content | Description | +|----------|---------|-------------| +| 1️⃣ | System Message (system prompt + topology) | Never removed | +| 2️⃣ | Latest user message | Keep at least the last 1 | +| 3️⃣ | Old conversation history | Discarded in chronological order | -**注意**:System prompt 和 topology info 通过模板变量合并为一个 SystemMessage,无法单独分离。 +**Note**: System prompt and topology info are merged into one SystemMessage via template variable and cannot be separated. -#### 2.4 边界情况处理 +#### 2.4 Edge Case Handling -| 情况 | 处理方式 | -|------|----------| -| System (包含 topology) > 预算 | 保留完整的 System Message(无法分离 system 和 topology) | -| Tools > 预算 | ERROR 日志,建议增加 context_limit 或减少工具数量 | -| 历史全被裁剪 | 保留最后1条用户消息 | +| Scenario | Handling | +|----------|----------| +| System (including topology) > budget | Keep complete SystemMessage (cannot separate system and topology) | +| Tools > budget | ERROR log, suggest increasing context_limit or reducing tool count | +| All history trimmed | Keep last 1 user message | -**重要提示**: -- 当 system + topology 超出可用预算时,**两者都会被保留** -- 无法只丢弃 topology 而保留 system prompt(因为已合并) +**Important Notes**: +- When system + topology exceed available budget, **both are preserved** +- Cannot discard only topology while keeping system prompt (already merged) -### 3. 集成到 GNS3 Copilot +### 3. Integration with GNS3 Copilot -**文件位置**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` +**File Location**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` -#### 实现方式 +#### Implementation Method -**关键点**:系统使用**自定义 StateGraph**,不是 LangGraph 的预构建 agent。 +**Key Point**: The system uses a **custom StateGraph**, not LangGraph's pre-built agent. -因此,`pre_model_hook` 不能通过 `model.invoke(config={"configurable": {"pre_model_hook": ...}})` 传递。 +Therefore, `pre_model_hook` cannot be passed via `model.invoke(config={"configurable": {"pre_model_hook": ...}})`. -**正确的使用方式**:**直接调用** `pre_hook` 函数准备消息。 +**Correct Usage**: **Directly call** the `pre_hook` function to prepare messages. ```python def llm_call(state: dict, config: RunnableConfig | None = None): """LLM decides whether to call a tool or not.""" - # 1. 获取 topology 信息 + # 1. Get topology information project_id = config["configurable"].get("project_id") topology_info = None if project_id: @@ -155,7 +155,7 @@ def llm_call(state: dict, config: RunnableConfig | None = None): if topology and "error" not in topology: topology_info = topology - # 2. 创建 pre_model_hook + # 2. Create pre_model_hook system_prompt = load_system_prompt() pre_hook = create_pre_model_hook( system_prompt=system_prompt, @@ -164,65 +164,65 @@ def llm_call(state: dict, config: RunnableConfig | None = None): get_tools_func=lambda: tools, ) - # 3. 创建 model with tools + # 3. Create model with tools model_with_tools = create_base_model_with_tools(tools, llm_config=llm_config) - # 4. ⭐ 关键:直接调用 pre_hook 准备消息 + # 4. ⭐ Key: directly call pre_hook to prepare messages logger.info("Calling pre_hook to prepare %d messages", len(messages)) prepared_state = pre_hook({"messages": messages, "topology_info": topology_info}) prepared_messages = prepared_state["messages"] - # 5. 使用准备好的消息调用 LLM + # 5. Use prepared messages to call LLM response = model_with_tools.invoke(prepared_messages) return {"messages": [response], ...} ``` -#### 为什么不通过 config 传递? +#### Why Not Pass via Config? -LangGraph 的 `pre_model_hook` 参数仅适用于**预构建的 agent**,不适用于自定义 StateGraph。 +LangGraph's `pre_model_hook` parameter only applies to **pre-built agents**, not custom StateGraphs. -| Agent 类型 | pre_model_hook 支持方式 | +| Agent Type | pre_model_hook Support | |------------|------------------------| -| `create_react_agent` | ✅ 通过 `pre_model_hook` 参数 | -| `chat_agent_executor` | ✅ 通过 `pre_model_hook` 参数 | -| **自定义 StateGraph** | ❌ **不支持**,需要直接调用 | +| `create_react_agent` | ✅ Via `pre_model_hook` parameter | +| `chat_agent_executor` | ✅ Via `pre_model_hook` parameter | +| **Custom StateGraph** | ❌ **Not supported**, need to call directly | -我们的实现使用的是自定义 StateGraph(`agent_builder = StateGraph(MessagesState)`),所以必须直接调用 `pre_hook`。 +Our implementation uses a custom StateGraph (`agent_builder = StateGraph(MessagesState)`), so we must call `pre_hook` directly. -### 4. 执行流程 +### 4. Execution Flow ``` -用户发送消息 +User sends message ↓ -llm_call 节点被调用 +llm_call node is called ↓ -获取 project_id (从 config["configurable"]) +Get project_id (from config["configurable"]) ↓ -调用 GNS3TopologyTool._run(project_id) 获取 topology +Call GNS3TopologyTool._run(project_id) to get topology ↓ -存储 topology_info 到 state +Store topology_info to state ↓ -创建 pre_model_hook (通过 create_pre_model_hook()) +Create pre_model_hook (via create_pre_model_hook()) ↓ -【关键】直接调用 pre_hook({"messages": messages, "topology_info": topology_info}) - ├─ 1. 注入 topology 到 system prompt - ├─ 2. 估算工具定义 tokens - ├─ 3. 调用 trim_messages() 裁剪消息 - └─ 4. 返回准备好的消息列表 +[Key] Directly call pre_hook({"messages": messages, "topology_info": topology_info}) + ├─ 1. Inject topology into system prompt + ├─ 2. Estimate tool definitions tokens + ├─ 3. Call trim_messages() to trim messages + └─ 4. Return prepared message list ↓ -使用准备好的消息调用 model.invoke() +Call model.invoke() with prepared messages ↓ -返回 LLM 响应 +Return LLM response ``` --- -## 策略实现 +## Strategy Implementation ### Context Strategy Ratios -**定义**(context_manager.py:68-72): +**Definition** (context_manager.py:68-72): ```python CONTEXT_STRATEGY_RATIOS = { @@ -232,24 +232,24 @@ CONTEXT_STRATEGY_RATIOS = { } ``` -**默认值**(context_manager.py:74): +**Default Value** (context_manager.py:74): ```python DEFAULT_CONTEXT_STRATEGY = "balanced" ``` -### 策略对比 +### Strategy Comparison -| 策略 | 输入比例 | 输出预留 | 计算公式 | -|------|---------|---------|---------| +| Strategy | Input Ratio | Output Reserved | Calculation Formula | +|----------|-------------|-----------------|---------------------| | Conservative | 60% | 40% | `context_limit × 1000 × 0.60` | | Balanced | 75% | 25% | `context_limit × 1000 × 0.75` | | Aggressive | 85% | 15% | `context_limit × 1000 × 0.85` | --- -## 日志输出 +## Log Output -### 正常情况(topology 成功注入) +### Normal Case (topology successfully injected) ``` INFO: Calling pre_hook to prepare 1 messages @@ -259,7 +259,7 @@ INFO: Messages prepared: 1 → 2 INFO: LLM call completed: tool_calls=0 ``` -### 发生裁剪时 +### When Trimming Occurs ``` INFO: Calling pre_hook to prepare 50 messages @@ -268,7 +268,7 @@ INFO: Messages trimmed: 50 → 25 msgs. Total: ~82000 tokens + 1725 tools = 8372 INFO: Messages prepared: 50 → 25 ``` -### topology 为 None 时 +### When topology is None ``` INFO: Calling pre_hook to prepare 1 messages @@ -278,24 +278,24 @@ INFO: Context ready: 2 msgs, ~800 tokens + 1725 tools = 2525 / 128K (2.0%), stra --- -## 错误处理 +## Error Handling -### tiktoken 未安装 +### tiktoken Not Installed -如果 tiktoken 未安装,系统将在启动时抛出错误: +If tiktoken is not installed, the system will throw an error at startup: ```python ModuleNotFoundError: No module named 'tiktoken' ``` -**解决方法**: +**Solution**: ```bash pip install tiktoken>=0.8.0 ``` -### context_limit 缺失或无效 +### context_limit Missing or Invalid -如果 LLM 配置中没有 `context_limit` 或值无效(context_manager.py:285-295): +If there is no `context_limit` in the LLM configuration or the value is invalid (context_manager.py:285-295): ```python if "context_limit" not in llm_config: @@ -306,7 +306,7 @@ if not isinstance(limit, int) or limit <= 0: raise ValueError(f"Invalid context_limit: {limit}") ``` -### 裁剪失败 +### Trimming Failure ```python try: @@ -319,8 +319,8 @@ except Exception as e: --- -## 相关源文件 +## Related Source Files -- `gns3server/agent/gns3_copilot/agent/context_manager.py` - 上下文管理核心逻辑 -- `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` - LLM 调用节点(StateGraph) -- `gns3server/agent/gns3_copilot/agent/model_factory.py` - 模型创建和工具绑定 +- `gns3server/agent/gns3_copilot/agent/context_manager.py` - Context management core logic +- `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` - LLM call node (StateGraph) +- `gns3server/agent/gns3_copilot/agent/model_factory.py` - Model creation and tool binding diff --git a/docs/gns3-copilot/todo/hitl-implementation-plan.md b/docs/gns3-copilot/todo/hitl-implementation-plan.md index 64d763c46..9fc86a148 100644 --- a/docs/gns3-copilot/todo/hitl-implementation-plan.md +++ b/docs/gns3-copilot/todo/hitl-implementation-plan.md @@ -1,83 +1,83 @@ -# GNS3 Copilot HITL 功能实现方案 +# GNS3 Copilot HITL Feature Implementation Plan -## 概述 +## Overview -本文档详细描述了 GNS3 Copilot 中 Human-in-the-Loop (HITL) 功能的完整实现方案。HITL 功能允许在执行敏感操作(如设备配置)前要求用户确认,提高系统安全性。 +This document details the complete implementation plan for the Human-in-the-Loop (HITL) feature in GNS3 Copilot. The HITL feature allows requiring user confirmation before executing sensitive operations (such as device configuration), improving system security. -## 目标 +## Goals -- 在执行配置工具前要求用户确认 -- 支持单个和批量工具确认 -- 提供清晰的工具执行预览 -- 保持现有功能完全兼容 +- Require user confirmation before executing configuration tools +- Support single and batch tool confirmation +- Provide clear tool execution preview +- Maintain complete compatibility with existing functionality -## 核心设计 +## Core Design -### 流程图 +### Flow Chart ``` -用户消息 → LLM → 工具调用决策 +User Message → LLM → Tool Call Decision ↓ - 检测是否需要确认 + Check if confirmation needed ↙ ↘ - 需要 HITL 直接执行 + Need HITL Direct Execution ↓ ↓ - 暂停,等待前端 执行工具 + Pause, wait for frontend Execute Tool ↓ ↓ - 用户确认/拒绝 返回结果 + User Confirm/Reject Return Result ↓ - 执行已确认的工具 + Execute Confirmed Tools ↓ - 返回结果 + Return Result ``` -### 架构分层 +### Architecture Layers ``` ┌─────────────────────────────────────────┐ │ Frontend (Web UI) │ -│ - 显示确认对话框 │ -│ - 列出待确认的工具 │ -│ - 处理用户确认/拒绝操作 │ +│ - Display confirmation dialog │ +│ - List pending tools │ +│ - Handle user confirm/reject actions │ └─────────────────────────────────────────┘ ↕ SSE/HTTP ┌─────────────────────────────────────────┐ │ API Layer (FastAPI) │ -│ - /hitl/status: 获取待确认工具 │ -│ - /hitl/confirm: 确认执行 │ -│ - /hitl/reject: 拒绝执行 │ +│ - /hitl/status: Get pending tools │ +│ - /hitl/confirm: Confirm execution │ +│ - /hitl/reject: Reject execution │ └─────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────┐ -│ AgentService (状态管理) │ -│ - 管理 HITL 状态 │ -│ - 处理确认/拒绝逻辑 │ -│ - checkpoint 持久化 │ +│ AgentService (State Management) │ +│ - Manage HITL state │ +│ - Handle confirm/reject logic │ +│ - checkpoint persistence │ └─────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────┐ -│ LangGraph Agent (工作流) │ -│ - llm_call: LLM 调用 │ -│ - check_hitl: 检查是否需要确认 │ -│ - hitl_confirmation: 等待确认 │ -│ - conditional_execution: 条件执行 │ +│ LangGraph Agent (Workflow) │ +│ - llm_call: LLM invocation │ +│ - check_hitl: Check if confirmation needed │ +│ - hitl_confirmation: Wait for confirmation │ +│ - conditional_execution: Conditional execution │ └─────────────────────────────────────────┘ ``` -## 实现改动 +## Implementation Changes -### 1. LangGraph Agent 层 +### 1. LangGraph Agent Layer -#### 文件:`gns3server/agent/gns3_copilot/agent/gns3_copilot.py` +#### File: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` -##### 改动 1.1:扩展 MessagesState +##### Change 1.1: Extend MessagesState -**位置**:第 98-124 行 +**Location**: Lines 98-124 ```python -# 扩展状态定义,添加 HITL 相关字段 +# Extend state definition, add HITL-related fields class MessagesState(TypedDict): - """GNS3-Copilot 对话状态管理类""" + """GNS3-Copilot Conversation State Management Class""" messages: Annotated[list[AnyMessage], operator.add] llm_calls: int @@ -85,27 +85,27 @@ class MessagesState(TypedDict): 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] # 用户拒绝的工具调用 + # New: HITL-related fields + pending_tool_calls: list[dict] # List of tool calls awaiting confirmation + hitl_confirmation_required: bool # Whether user confirmation is needed + hitl_session_id: str | None # HITL session unique identifier + confirmed_tool_calls: list[dict] # User-confirmed tool calls + rejected_tool_calls: list[dict] # User-rejected tool calls ``` -**改动影响**: -- checkpoint 数据库中新增 5 个字段 -- 现有状态读取代码需要兼容新字段 +**Change Impact**: +- 5 new fields in checkpoint database +- Existing state reading code needs to be compatible with new fields -##### 改动 1.2:添加 HITL 检测节点 +##### Change 1.2: Add HITL Detection Node -**位置**:在 `generate_title` 函数后添加(约第 310 行) +**Location**: Add after `generate_title` function (around line 310) ```python -# 需要确认的工具列表 +# List of tools requiring confirmation HITL_TOOLS = { "execute_multiple_device_config_commands", - # 可根据需要添加: + # Can be added as needed: # "delete_node", # "start_gns3_node", } @@ -117,7 +117,7 @@ DANGEROUS_PATTERNS = [ def _is_dangerous_config(tool_name: str, tool_args: dict) -> bool: - """检测配置是否包含危险命令""" + """Check if configuration contains dangerous commands""" if tool_name == "execute_multiple_device_config_commands": device_configs = tool_args.get("device_configs", []) for device in device_configs: @@ -131,14 +131,14 @@ def _is_dangerous_config(tool_name: str, tool_args: dict) -> bool: def check_hitl_requirement(state: MessagesState) -> dict: """ - 检查工具调用是否需要用户确认 (HITL) + Check if tool calls require user confirmation (HITL) Returns: - dict: 包含 pending_tool_calls 和 hitl_confirmation_required 的状态更新 + dict: State update containing pending_tool_calls and hitl_confirmation_required """ last_message = state["messages"][-1] - # 如果最后一条消息没有工具调用,直接返回 + # If last message has no tool calls, return directly if not hasattr(last_message, 'tool_calls') or not last_message.tool_calls: return {"hitl_confirmation_required": False} @@ -148,9 +148,9 @@ def check_hitl_requirement(state: MessagesState) -> dict: tool_name = tool_call["name"] tool_args = tool_call["args"] - # 只处理需要 HITL 的工具 + # Only process tools that need HITL if tool_name in HITL_TOOLS: - # 检查是否为危险操作 + # Check if it's a dangerous operation is_dangerous = _is_dangerous_config(tool_name, tool_args) tool_info = { @@ -160,10 +160,10 @@ def check_hitl_requirement(state: MessagesState) -> dict: "danger_level": "high" if is_dangerous else "medium" } - # 添加描述信息 + # Add description information if tool_name == "execute_multiple_device_config_commands": device_count = len(tool_args.get("device_configs", [])) - tool_info["description"] = f"配置 {device_count} 个设备" + tool_info["description"] = f"Configure {device_count} device(s)" pending_tools.append(tool_info) logger.info("HITL: Tool '%s' requires confirmation (danger_level=%s)", @@ -180,26 +180,26 @@ def check_hitl_requirement(state: MessagesState) -> dict: return {"hitl_confirmation_required": False} ``` -##### 改动 1.3:修改 should_continue 路由 +##### Change 1.3: Modify should_continue Route -**位置**:第 337-370 行 +**Location**: Lines 337-370 ```python def should_continue( state: MessagesState, ) -> Literal["conditional_tool_execution", "hitl_confirmation", "title_generator_node", END]: """ - LLM 响应后的路由决策 + Routing decision after LLM response Returns: - Literal: 路由到下一个节点 + Literal: Route to next node """ last_message = state["messages"][-1] current_title = state.get("conversation_title") - # LLM 请求工具调用 + # LLM requests tool call if last_message.tool_calls: - # 检查是否需要 HITL 确认 + # Check if HITL confirmation is needed if state.get("hitl_confirmation_required"): logger.info("Routing to hitl_confirmation node") return "hitl_confirmation" @@ -207,64 +207,64 @@ def should_continue( logger.info("Routing to conditional_tool_execution node") return "conditional_tool_execution" - # 首次交互完成,生成标题 + # First interaction completed, generate title if current_title in [None, "New Conversation"]: return "title_generator_node" return END ``` -**关键变更**: -- 原路由 `"tool_node"` 改为 `"conditional_tool_execution"` -- 新增 `"hitl_confirmation"` 路由 -- 路由名称变更,需要同步更新所有引用 +**Key Changes**: +- Original route `"tool_node"` changed to `"conditional_tool_execution"` +- New `"hitl_confirmation"` route added +- Route name changed, all references need to be updated synchronously -##### 改动 1.4:添加 HITL 确认等待节点 +##### Change 1.4: Add HITL Confirmation Wait Node -**位置**:在 `should_continue` 函数后添加 +**Location**: Add after `should_continue` function ```python def hitl_confirmation_node(state: MessagesState) -> dict: """ - HITL 确认节点 - 暂停执行,等待用户确认 + HITL confirmation node - Pause execution, wait for user confirmation - 这是一个特殊的节点,它不执行任何操作,只是保持状态。 - 实际的确认流程由 API 层处理。 + This is a special node that performs no operations, only maintains state. + Actual confirmation flow is handled by the API layer. - 工作原理: - 1. 节点被调用时,状态中包含 pending_tool_calls - 2. LangGraph 保存状态到 checkpoint - 3. 执行流程暂停,等待外部状态更新 - 4. 用户通过 API 确认/拒绝后,状态被更新 - 5. 从 checkpoint 恢复并继续执行 + Working principle: + 1. When node is called, state contains pending_tool_calls + 2. LangGraph saves state to checkpoint + 3. Execution flow pauses, waits for external state update + 4. After user confirms via API, state is updated + 5. Resume execution from checkpoint Returns: - dict: 空字典,保持状态不变 + dict: Empty dictionary, maintains state unchanged """ logger.info("HITL: Pausing for user confirmation (session_id=%s)", state.get("hitl_session_id")) - # 返回空字典,保持状态不变 - # 状态将在用户确认后通过 API 更新 + # Return empty dictionary, keep state unchanged + # State will be updated via API after user confirmation return {} ``` -##### 改动 1.5:添加条件执行节点 +##### Change 1.5: Add Conditional Execution Node -**位置**:在 `hitl_confirmation_node` 函数后添加 +**Location**: Add after `hitl_confirmation_node` function ```python def conditional_tool_execution(state: MessagesState) -> dict: """ - 条件工具执行节点 - 根据用户确认决定是否执行工具 + Conditional tool execution node - Decide whether to execute tools based on user confirmation - 这个节点替代原来的 tool_node,增加了: - 1. 只执行用户确认的工具 - 2. 处理用户拒绝的工具 - 3. 更新 HITL 状态 + This node replaces the original tool_node, adding: + 1. Only execute user-confirmed tools + 2. Handle user-rejected tools + 3. Update HITL state Returns: - dict: 包含执行结果和状态更新的字典 + dict: Contains execution results and state updates """ confirmed_calls = state.get("confirmed_tool_calls", []) rejected_calls = state.get("rejected_tool_calls", []) @@ -272,15 +272,15 @@ def conditional_tool_execution(state: MessagesState) -> dict: logger.info("HITL: Executing %d confirmed tools, %d rejected", len(confirmed_calls), len(rejected_calls)) - # 处理用户拒绝的工具 + # Handle user-rejected tools if rejected_calls: rejected_names = [c["tool_name"] for c in rejected_calls] rejection_msg = ( - f"用户拒绝了以下操作: {', '.join(rejected_names)}。\n" - f"请提供替代方案或解释不执行这些操作的原因。" + f"User rejected the following operations: {', '.join(rejected_names)}.\n" + f"Please provide an alternative solution or explain why these operations should not be performed." ) - # 添加系统消息通知 LLM + # Add system message to notify LLM return { "messages": [SystemMessage(content=rejection_msg)], "pending_tool_calls": [], @@ -289,7 +289,7 @@ def conditional_tool_execution(state: MessagesState) -> dict: "rejected_tool_calls": [] } - # 执行已确认的工具 + # Execute confirmed tools if not confirmed_calls: logger.warning("HITL: No confirmed tools to execute") return { @@ -322,7 +322,7 @@ def conditional_tool_execution(state: MessagesState) -> dict: name=tool_name )) - # 清理 HITL 状态 + # Clean up HITL state return { "messages": results, "pending_tool_calls": [], @@ -332,45 +332,45 @@ def conditional_tool_execution(state: MessagesState) -> dict: } ``` -**关键变更**: -- 替代原 `tool_node` 函数 -- 增加确认逻辑处理 -- 支持部分确认、部分拒绝 +**Key Changes**: +- Replaces original `tool_node` function +- Adds confirmation logic handling +- Supports partial confirmation, partial rejection -##### 改动 1.6:更新 LangGraph 构建流程 +##### Change 1.6: Update LangGraph Build Process -**位置**:第 393-427 行 +**Location**: Lines 393-427 ```python -# 构建工作流 +# Build workflow agent_builder = StateGraph(MessagesState) -# 添加节点 +# Add nodes 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 +# Add edges: START → llm_call agent_builder.add_edge(START, "llm_call") -# 添加边:LLM 后的条件路由 +# Add edges: conditional routing after 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_confirmation": "hitl_confirmation", # Needs HITL confirmation + "conditional_tool_execution": "conditional_tool_execution", # Direct execution + "title_generator_node": "title_generator_node", # Generate title + END: END # End conversation }, ) -# HITL 确认节点是特殊的,它需要等待外部状态更新 -# 状态更新后,下一轮调用将从 checkpoint 恢复并继续 -# 这个循环通过 API 触发新的 graph.ainvoke() 调用完成 +# HITL confirmation node is special, needs to wait for external state update +# State is updated after checkpoint saved, next round of invocation resumes from checkpoint +# This cycle is completed by API triggering new graph.ainvoke() call -# 添加边:条件执行后继续 LLM 调用 +# Add edges: continue LLM call after conditional execution agent_builder.add_conditional_edges( "conditional_tool_execution", recursion_limit_continue, @@ -380,33 +380,33 @@ agent_builder.add_conditional_edges( }, ) -# 添加边:标题生成后结束 +# Add edges: end after title generation agent_builder.add_edge("title_generator_node", END) ``` -**工作流图**: +**Workflow Diagram**: ``` START → llm_call → should_continue ↓ ┌────────────┼────────────┐ ↓ ↓ ↓ hitl_confirmation conditional title_generator -(等待 API) _execution ↓ +(wait for API) _execution ↓ └────────────┴────────────→ END ``` -#### 文件:`gns3server/agent/gns3_copilot/agent_service.py` +#### File: `gns3server/agent/gns3_copilot/agent_service.py` -##### 改动 2.1:添加 HITL 事件处理 +##### Change 2.1: Add HITL Event Handling -**位置**:第 333-376 行的 `_convert_event_to_chunk` 函数 +**Location**: `_convert_event_to_chunk` function at lines 333-376 ```python def _convert_event_to_chunk(self, event: Dict[str, Any], session_id: str) -> Optional[Dict[str, Any]]: """ - 转换 LangGraph 事件为 API 响应块 + Convert LangGraph events to API response chunks - 支持 HITL 事件类型 + Supports HITL event types """ event_type = event.get("event", "") data = event.get("data", {}) @@ -435,30 +435,30 @@ def _convert_event_to_chunk(self, event: Dict[str, Any], session_id: str) -> Opt "session_id": session_id } - # 新增:HITL 确认要求事件 + # New: HITL confirmation required event 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分钟超时 + "timeout": 300, # 5 minute timeout "session_id": session_id } return None ``` -**注意**:实际实现中,HITL 事件不是通过 LangGraph 的 `astream_events` 触发的,而是通过状态查询实现的。因此,此函数主要用于处理工具执行事件。 +**Note**: In actual implementation, HITL events are not triggered via LangGraph's `astream_events`, but achieved through state queries. Therefore, this function is mainly used to handle tool execution events. --- -### 2. API 层 +### 2. API Layer -#### 文件:`gns3server/api/routes/controller/chat.py` +#### File: `gns3server/api/routes/controller/chat.py` -##### 改动 2.1:添加 HITL 端点 +##### Change 2.1: Add HITL Endpoints -**位置**:在文件末尾添加(约第 325 行后) +**Location**: Add at end of file (after line 325) ```python from gns3server import schemas @@ -466,14 +466,14 @@ from typing import List # ============================================================================= -# HITL (Human-in-the-Loop) 端点 +# HITL (Human-in-the-Loop) Endpoints # ============================================================================= @router.get( "/sessions/{session_id}/hitl-status", response_model=schemas.HITLStatusResponse, - summary="获取 HITL 状态", - description="获取当前会话中待确认的工具列表" + summary="Get HITL status", + description="Get list of pending tools in current session" ) async def get_hitl_status( session_id: str, @@ -481,10 +481,10 @@ async def get_hitl_status( current_user: schemas.User = Depends(get_current_active_user), ) -> schemas.HITLStatusResponse: """ - 获取 HITL 状态 + Get HITL status - 返回当前会话中等待用户确认的工具调用列表。 - 前端应定期轮询此端点以检查是否有新的待确认工具。 + Returns the list of tool calls waiting for user confirmation in the current session. + Frontend should poll this endpoint periodically to check for new pending tools. """ if project.status != "opened": raise HTTPException( @@ -495,7 +495,7 @@ async def get_hitl_status( agent_manager = await get_project_agent_manager() agent_service = await agent_manager.get_agent(str(project.id), project.path) - # 从 checkpoint 获取状态 + # Get state from checkpoint config = {"configurable": {"thread_id": session_id}} state = await agent_service._graph.aget_state(config) @@ -511,7 +511,7 @@ async def get_hitl_status( pending_tools = values.get("pending_tool_calls", []) hitl_session_id = values.get("hitl_session_id") - # 转换为 Schema 格式 + # Convert to Schema format pending_tool_schemas = [] for tool in pending_tools: pending_tool_schemas.append(schemas.PendingTool( @@ -533,8 +533,8 @@ async def get_hitl_status( @router.post( "/sessions/{session_id}/hitl/confirm", response_model=schemas.HITLConfirmationResponse, - summary="确认执行工具", - description="用户确认执行一个或多个待确认的工具" + summary="Confirm tool execution", + description="User confirms to execute one or more pending tools" ) async def confirm_tool_execution( session_id: str, @@ -543,13 +543,13 @@ async def confirm_tool_execution( current_user: schemas.User = Depends(get_current_active_user), ) -> schemas.HITLConfirmationResponse: """ - 确认执行工具 + Confirm tool execution - 用户可以选择: - - confirm_all=true: 确认所有待确认的工具 - - tool_call_ids=[...]: 确认指定的工具 + User can choose: + - confirm_all=true: Confirm all pending tools + - tool_call_ids=[...]: Confirm specified tools - 确认后,工具将被执行,结果将通过 SSE 流返回。 + After confirmation, tools will be executed and results returned via SSE stream. """ if project.status != "opened": raise HTTPException( @@ -578,7 +578,7 @@ async def confirm_tool_execution( detail="No pending tools to confirm" ) - # 根据请求选择要确认的工具 + # Select tools to confirm based on request confirmed_tools = [] if request.confirm_all: confirmed_tools = pending_tools @@ -596,7 +596,7 @@ async def confirm_tool_execution( detail="No tools matched the confirmation criteria" ) - # 更新状态:标记为已确认 + # Update state: mark as confirmed await agent_service._graph.aupdate_state( config, { @@ -606,7 +606,7 @@ async def confirm_tool_execution( } ) - # 继续执行流程 + # Continue execution flow try: new_state = await agent_service._graph.ainvoke(None, config) except Exception as e: @@ -619,15 +619,15 @@ async def confirm_tool_execution( return schemas.HITLConfirmationResponse( status="confirmed", confirmed_count=len(confirmed_tools), - message=f"已确认 {len(confirmed_tools)} 个工具执行" + message=f"Confirmed {len(confirmed_tools)} tool(s) for execution" ) @router.post( "/sessions/{session_id}/hitl/reject", response_model=schemas.HITLConfirmationResponse, - summary="拒绝执行工具", - description="用户拒绝执行一个或多个待确认的工具" + summary="Reject tool execution", + description="User rejects to execute one or more pending tools" ) async def reject_tool_execution( session_id: str, @@ -636,14 +636,14 @@ async def reject_tool_execution( current_user: schemas.User = Depends(get_current_active_user), ) -> schemas.HITLConfirmationResponse: """ - 拒绝执行工具 + Reject tool execution - 用户可以选择: - - reject_all=true: 拒绝所有待确认的工具 - - tool_call_ids=[...]: 拒绝指定的工具 - - reason: 拒绝原因(将反馈给 LLM) + User can choose: + - reject_all=true: Reject all pending tools + - tool_call_ids=[...]: Reject specified tools + - reason: Rejection reason (will be fed back to LLM) - 拒绝后,LLM 将收到通知并可以提供替代方案。 + After rejection, LLM will be notified and can provide alternative solution. """ if project.status != "opened": raise HTTPException( @@ -673,7 +673,7 @@ async def reject_tool_execution( detail="No pending tools to reject" ) - # 更新状态:标记为已拒绝 + # Update state: mark as rejected await agent_service._graph.aupdate_state( config, { @@ -683,7 +683,7 @@ async def reject_tool_execution( } ) - # 继续执行流程,LLM 将收到拒绝通知 + # Continue execution flow, LLM will receive rejection notification try: new_state = await agent_service._graph.ainvoke(None, config) except Exception as e: @@ -697,99 +697,99 @@ async def reject_tool_execution( return schemas.HITLConfirmationResponse( status="rejected", confirmed_count=0, - message=f"已拒绝 {len(rejected_tools)} 个工具执行: {', '.join(rejected_names)}" + message=f"Rejected {len(rejected_tools)} tool(s): {', '.join(rejected_names)}" ) ``` --- -### 3. Schema 层 +### 3. Schema Layer -#### 文件:`gns3server/schemas/controller/chat.py` +#### File: `gns3server/schemas/controller/chat.py` -##### 改动 3.1:添加 HITL 相关的 Pydantic 模型 +##### Change 3.1: Add HITL-related Pydantic Models -**位置**:在文件末尾添加(约第 112 行后) +**Location**: Add at end of file (after line 112) ```python class PendingTool(BaseModel): - """待确认的工具信息""" - tool_call_id: str = Field(..., description="工具调用的唯一 ID") - tool_name: str = Field(..., description="工具名称") - tool_args: Dict[str, Any] = Field(..., description="工具参数") + """Information about pending tool""" + tool_call_id: str = Field(..., description="Unique ID of the tool call") + tool_name: str = Field(..., description="Tool name") + tool_args: Dict[str, Any] = Field(..., description="Tool parameters") danger_level: Literal["low", "medium", "high"] = Field( default="medium", - description="危险等级" + description="Danger level" ) - description: Optional[str] = Field(None, description="工具执行的描述") + description: Optional[str] = Field(None, description="Tool execution description") class HITLStatusResponse(BaseModel): - """HITL 状态响应""" + """HITL status response""" status: Literal["idle", "waiting", "confirmed", "rejected"] = Field( ..., - description="当前状态" + description="Current status" ) pending_tools: List[PendingTool] = Field( default_factory=list, - description="待确认的工具列表" + description="List of pending tools" ) hitl_session_id: Optional[str] = Field( None, - description="HITL 会话 ID" + description="HITL session ID" ) - session_id: str = Field(..., description="聊天会话 ID") + session_id: str = Field(..., description="Chat session ID") class HITLConfirmationRequest(BaseModel): - """HITL 确认请求""" + """HITL confirmation request""" confirm_all: bool = Field( default=False, - description="是否确认所有待确认的工具" + description="Whether to confirm all pending tools" ) tool_call_ids: Optional[List[str]] = Field( None, - description="要确认的工具调用 ID 列表" + description="List of tool call IDs to confirm" ) class HITLRejectionRequest(BaseModel): - """HITL 拒绝请求""" + """HITL rejection request""" reject_all: bool = Field( default=False, - description="是否拒绝所有待确认的工具" + description="Whether to reject all pending tools" ) tool_call_ids: Optional[List[str]] = Field( None, - description="要拒绝的工具调用 ID 列表" + description="List of tool call IDs to reject" ) reason: Optional[str] = Field( None, - description="拒绝原因(将反馈给 LLM)" + description="Rejection reason (will be fed back to LLM)" ) class HITLConfirmationResponse(BaseModel): - """HITL 确认响应""" + """HITL confirmation response""" status: Literal["confirmed", "rejected"] = Field( ..., - description="操作状态" + description="Operation status" ) confirmed_count: int = Field( ..., - description="已确认的工具数量" + description="Number of confirmed tools" ) - message: str = Field(..., description="响应消息") + message: str = Field(..., description="Response message") ``` -**同时更新 `__init__.py` 导出**: +**Also update `__init__.py` exports**: ```python -# 文件:gns3server/schemas/__init__.py +# File: gns3server/schemas/__init__.py -# 添加到导入列表 +# Add to import list from .controller.chat import ( - # ... 现有导入 ... + # ... existing imports ... PendingTool, HITLStatusResponse, HITLConfirmationRequest, @@ -800,48 +800,48 @@ from .controller.chat import ( --- -### 4. 文件改动汇总 +### 4. File Changes Summary -| 文件路径 | 改动类型 | 行数变化 | 风险等级 | -|---------|---------|----------|----------| -| `gns3_copilot.py` | 修改/新增 | +200 行 | 🟡 中 | -| `agent_service.py` | 修改 | +10 行 | 🟢 低 | -| `chat.py` (API) | 新增 | +150 行 | 🟢 低 | -| `chat.py` (Schema) | 新增 | +50 行 | 🟢 低 | -| **总计** | - | **+410 行** | - | +| File Path | Change Type | Line Changes | Risk Level | +|-----------|-------------|--------------|------------| +| `gns3_copilot.py` | Modify/Add | +200 lines | 🟡 Medium | +| `agent_service.py` | Modify | +10 lines | 🟢 Low | +| `chat.py` (API) | Add | +150 lines | 🟢 Low | +| `chat.py` (Schema) | Add | +50 lines | 🟢 Low | +| **Total** | - | **+410 lines** | - | --- -## 数据库变更 +## Database Changes -### Checkpoint 表结构 +### Checkpoint Table Structure -**表名**:`checkpoints` (LangGraph 自动管理) +**Table Name**: `checkpoints` (managed by LangGraph) -**新增字段**(通过 MessagesState 扩展自动添加): +**New Fields** (automatically added via MessagesState extension): -| 字段名 | 类型 | 说明 | -|--------|------|------| -| `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) | 已拒绝的工具调用 | +| Field Name | Type | Description | +|------------|------|-------------| +| `pending_tool_calls` | TEXT (JSON) | List of pending tool calls | +| `hitl_confirmation_required` | BOOLEAN | Whether HITL confirmation is needed | +| `hitl_session_id` | TEXT | HITL session ID | +| `confirmed_tool_calls` | TEXT (JSON) | Confirmed tool calls | +| `rejected_tool_calls` | TEXT (JSON) | Rejected tool calls | -**迁移说明**: -- LangGraph 自动处理新的 state 字段 -- 无需手动执行数据库迁移 -- 现有 checkpoint 向后兼容 +**Migration Notes**: +- LangGraph automatically handles new state fields +- No manual database migration required +- Existing checkpoints are backward compatible --- -## API 规范 +## API Specification -### 1. 获取 HITL 状态 +### 1. Get HITL Status -**端点**:`GET /v3/projects/{project_id}/chat/sessions/{session_id}/hitl-status` +**Endpoint**: `GET /v3/projects/{project_id}/chat/sessions/{session_id}/hitl-status` -**响应示例**: +**Response Example**: ```json { "status": "waiting", @@ -859,7 +859,7 @@ from .controller.chat import ( ] }, "danger_level": "medium", - "description": "配置 1 个设备" + "description": "Configure 1 device" } ], "hitl_session_id": "hitl_12345", @@ -867,11 +867,11 @@ from .controller.chat import ( } ``` -### 2. 确认执行 +### 2. Confirm Execution -**端点**:`POST /v3/projects/{project_id}/chat/sessions/{session_id}/hitl/confirm` +**Endpoint**: `POST /v3/projects/{project_id}/chat/sessions/{session_id}/hitl/confirm` -**请求体**: +**Request Body**: ```json { "confirm_all": true, @@ -879,7 +879,7 @@ from .controller.chat import ( } ``` -**或指定工具**: +**Or specify tools**: ```json { "confirm_all": false, @@ -887,44 +887,44 @@ from .controller.chat import ( } ``` -**响应示例**: +**Response Example**: ```json { "status": "confirmed", "confirmed_count": 2, - "message": "已确认 2 个工具执行" + "message": "Confirmed 2 tool(s) for execution" } ``` -### 3. 拒绝执行 +### 3. Reject Execution -**端点**:`POST /v3/projects/{project_id}/chat/sessions/{session_id}/hitl/reject` +**Endpoint**: `POST /v3/projects/{project_id}/chat/sessions/{session_id}/hitl/reject` -**请求体**: +**Request Body**: ```json { "reject_all": true, - "reason": "配置有误,需要重新规划" + "reason": "Configuration has errors, needs re-planning" } ``` -**响应示例**: +**Response Example**: ```json { "status": "rejected", "confirmed_count": 0, - "message": "已拒绝 1 个工具执行: execute_multiple_device_config_commands" + "message": "Rejected 1 tool(s): execute_multiple_device_config_commands" } ``` --- -## 前端集成指南 +## Frontend Integration Guide -### 1. 检测 HITL 状态 +### 1. Detect HITL Status ```javascript -// 定期轮询 HITL 状态 +// Periodically poll HITL status async function pollHITLStatus(sessionId) { const response = await fetch( `/api/v3/projects/${projectId}/chat/sessions/${sessionId}/hitl-status` @@ -936,11 +936,11 @@ async function pollHITLStatus(sessionId) { } } -// 每 2 秒轮询一次 +// Poll every 2 seconds setInterval(() => pollHITLStatus(sessionId), 2000); ``` -### 2. 显示确认对话框 +### 2. Show Confirmation Dialog ```javascript function showConfirmationDialog(hitlStatus) { @@ -950,7 +950,7 @@ function showConfirmationDialog(hitlStatus) { dialog.className = 'hitl-confirmation-dialog'; let html = ` -

⚠️ 需要确认以下操作

+

⚠️ Please Confirm the Following Operations

`; @@ -968,8 +968,8 @@ function showConfirmationDialog(hitlStatus) { html += `
- - + +
`; @@ -989,7 +989,7 @@ async function confirmAll(sessionId) { if (response.ok) { closeDialog(); - // 继续监听 SSE 流以获取执行结果 + // Continue listening to SSE stream to get execution results } } @@ -999,7 +999,7 @@ async function rejectAll(sessionId) { { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ reject_all: true, reason: '用户取消操作' }) + body: JSON.stringify({ reject_all: true, reason: 'User cancelled operation' }) } ); @@ -1011,9 +1011,9 @@ async function rejectAll(sessionId) { --- -## 测试计划 +## Test Plan -### 单元测试 +### Unit Tests ```python import pytest @@ -1023,7 +1023,7 @@ from gns3server.agent.gns3_copilot.agent.gns3_copilot import ( ) def test_dangerous_config_detection(): - """测试危险命令检测""" + """Test dangerous command detection""" tool_args = { "device_configs": [{ "config_commands": ["reload", "write erase"] @@ -1033,7 +1033,7 @@ def test_dangerous_config_detection(): assert _is_dangerous_config("execute_multiple_device_config_commands", tool_args) == True def test_safe_config_detection(): - """测试安全命令检测""" + """Test safe command detection""" tool_args = { "device_configs": [{ "config_commands": ["interface gig0/0", "ip address 10.0.0.1/24"] @@ -1043,10 +1043,10 @@ def test_safe_config_detection(): assert _is_dangerous_config("execute_multiple_device_config_commands", tool_args) == False def test_hitl_requirement_check(): - """测试 HITL 需求检查""" + """Test HITL requirement check""" state = { "messages": [ - HumanMessage(content="配置路由器"), + HumanMessage(content="Configure router"), AIMessage( content="", tool_calls=[{ @@ -1064,28 +1064,28 @@ def test_hitl_requirement_check(): assert len(result["pending_tool_calls"]) == 1 ``` -### 集成测试 +### Integration Tests ```python import pytest from fastapi.testclient import TestClient def test_hitl_flow(): - """测试完整的 HITL 流程""" + """Test complete HITL flow""" client = TestClient(app) - # 1. 开始聊天 + # 1. Start chat response = client.post( f"/v3/projects/{project_id}/chat/stream", - json={"message": "配置所有路由器"} + json={"message": "Configure all routers"} ) - # 2. 检查 HITL 状态 + # 2. Check HITL status status = client.get(f"/v3/projects/{project_id}/chat/sessions/{session_id}/hitl-status") assert status.json()["status"] == "waiting" assert len(status.json()["pending_tools"]) > 0 - # 3. 确认执行 + # 3. Confirm execution confirm = client.post( f"/v3/projects/{project_id}/chat/sessions/{session_id}/hitl/confirm", json={"confirm_all": True} @@ -1095,85 +1095,85 @@ def test_hitl_flow(): --- -## 部署步骤 +## Deployment Steps -### 第一阶段:基础架构(1-2 天) +### Phase 1: Basic Infrastructure (1-2 days) -1. ✅ 扩展 MessagesState -2. ✅ 实现 check_hitl_requirement 节点 -3. ✅ 实现 hitl_confirmation_node 和 conditional_tool_execution -4. ✅ 更新 LangGraph 工作流 -5. ✅ 单元测试 +1. ✅ Extend MessagesState +2. ✅ Implement check_hitl_requirement node +3. ✅ Implement hitl_confirmation_node and conditional_tool_execution +4. ✅ Update LangGraph workflow +5. ✅ Unit tests -**验证**:现有功能不受影响 +**Verification**: Existing functionality unaffected -### 第二阶段:API 端点(1 天) +### Phase 2: API Endpoints (1 day) -1. ✅ 添加 HITL 状态查询端点 -2. ✅ 添加确认/拒绝端点 -3. ✅ 添加 Schema 定义 -4. ✅ API 测试 +1. ✅ Add HITL status query endpoint +2. ✅ Add confirm/reject endpoints +3. ✅ Add Schema definitions +4. ✅ API tests -**验证**:API 可正常调用 +**Verification**: API can be called normally -### 第三阶段:前端集成(2-3 天) +### Phase 3: Frontend Integration (2-3 days) -1. ✅ 实现轮询逻辑 -2. ✅ 显示确认对话框 -3. ✅ 处理确认/拒绝操作 -4. ✅ 显示执行结果 +1. ✅ Implement polling logic +2. ✅ Show confirmation dialog +3. ✅ Handle confirm/reject operations +4. ✅ Display execution results -**验证**:端到端流程可用 +**Verification**: End-to-end flow available -### 第四阶段:优化和增强(1-2 天) +### Phase 4: Optimization and Enhancement (1-2 days) -1. ✅ 添加危险命令分级 -2. ✅ 实现超时处理 -3. ✅ 添加操作日志 -4. ✅ 性能优化 +1. ✅ Add dangerous command classification +2. ✅ Implement timeout handling +3. ✅ Add operation logging +4. ✅ Performance optimization -**验证**:生产就绪 +**Verification**: Production ready --- -## 参数修改功能(扩展) +## Parameter Modification Feature (Extension) -### 功能概述 +### Feature Overview -除了"确认/拒绝"外,HITL 还支持用户修改即将执行的命令参数,然后将修改反馈给 LLM,由 LLM 理解并按新参数执行。 +In addition to "Confirm/Reject", HITL also supports users modifying command parameters about to be executed, then feeding the modifications back to the LLM, which understands and executes with the new parameters. -### 完整流程 +### Complete Flow ``` -LLM 生成命令 A +LLM generates command A ↓ - HITL 确认暂停 + HITL confirmation pause ↓ - 显示给用户 + Display to user ↓ ┌─────────┼─────────┐ │ │ │ -直接确认 拒绝 修改为B +Direct Confirm Reject Modify to B │ │ │ -执行A 反馈LLM 反馈(A→B)给LLM +Execute A Feedback LLM Feedback (A→B) to LLM ↓ - LLM理解修改 + LLM understands modification ↓ - 生成新tool call + Generate new tool call ↓ - 执行B + Execute B ``` -### 对话示例 +### Conversation Example ``` -用户: 配置 R1 的 gig0/0 接口为 10.0.0.1/24 +User: Configure R1's gig0/0 interface to 10.0.0.1/24 -LLM: 我将为您配置 R1 的接口。 +LLM: I will configure R1's interface for you. -HITL: ⚠️ 需要确认以下操作 - 工具: execute_multiple_device_config_commands - 参数: { +HITL: ⚠️ Please confirm the following operations + Tool: execute_multiple_device_config_commands + Parameters: { "device_configs": [{ "device_name": "R1", "config_commands": [ @@ -1183,35 +1183,35 @@ HITL: ⚠️ 需要确认以下操作 }] } -用户: [修改参数] +User: [Modify parameters] ip address 10.0.0.1 255.255.255.0 → ip address 192.168.1.1 255.255.255.0 - [保存并执行] + [Save and Execute] -系统反馈给 LLM: - 用户修改了即将执行的命令参数: - 工具: execute_multiple_device_config_commands +System feedback to LLM: + User modified the command parameters about to be executed: + Tool: execute_multiple_device_config_commands R1: - 原命令: ['interface gig0/0', 'ip address 10.0.0.1 255.255.255.0'] - 修改为: ['interface gig0/0', 'ip address 192.168.1.1 255.255.255.0'] - 请按照修改后的参数执行。 + Original commands: ['interface gig0/0', 'ip address 10.0.0.1 255.255.255.0'] + Modified to: ['interface gig0/0', 'ip address 192.168.1.1 255.255.255.0'] + Please execute according to the modified parameters. -LLM: 明白,我将使用修改后的 IP 地址 192.168.1.1/24 来配置 R1 的 gig0/0 接口。 +LLM: Understood, I will use the modified IP address 192.168.1.1/24 to configure R1's gig0/0 interface. -[工具执行 execute_multiple_device_config_commands with modified args] +[Tool execution execute_multiple_device_config_commands with modified args] -LLM: 已完成配置,R1 的 gig0/0 接口已配置为 192.168.1.1/24。 +LLM: Configuration completed, R1's gig0/0 interface configured to 192.168.1.1/24. ``` -### 实现要点 +### Implementation Points -#### 1. 状态扩展 +#### 1. State Extension -在 `MessagesState` 中添加: +Add to `MessagesState`: ```python -# 用户修改的字段 -user_modified_args: dict | None # 结构: +# User-modified fields +user_modified_args: dict | None # Structure: # { # "tool_call_id": str, # "original_args": dict, @@ -1219,92 +1219,92 @@ user_modified_args: dict | None # 结构: # } ``` -#### 2. API 扩展 +#### 2. API Extension -新增端点:`POST /v3/projects/{project_id}/chat/sessions/{session_id}/hitl/modify` +New endpoint: `POST /v3/projects/{project_id}/chat/sessions/{session_id}/hitl/modify` -**请求体**: +**Request Body**: ```python { "tool_call_id": "call_abc123", "modified_args": { - # 修改后的完整参数 + # Modified complete parameters } } ``` -**响应**: +**Response**: ```python { "status": "modified", - "modification_summary": "显示参数差异", - "message": "已将修改反馈给 AI" + "modification_summary": "Show parameter differences", + "message": "Feedback sent to AI" } ``` -#### 3. 后端处理流程 +#### 3. Backend Processing Flow -1. 接收修改后的参数 -2. 生成参数差异摘要 -3. 添加 HumanMessage 到对话,说明用户的修改 -4. 清除 `pending_tool_calls` 和 `hitl_confirmation_required` -5. 设置 `user_modified_args`(用于触发 LLM 重新生成) -6. 继续执行,LLM 看到用户的修改后,生成新的 tool call -7. 执行新的 tool call +1. Receive modified parameters +2. Generate parameter difference summary +3. Add HumanMessage to conversation, explaining user's modification +4. Clear `pending_tool_calls` and `hitl_confirmation_required` +5. Set `user_modified_args` (used to trigger LLM regeneration) +6. Continue execution, LLM sees user's modification and generates new tool call +7. Execute new tool call -#### 4. 前端实现要点 +#### 4. Frontend Implementation Points -**界面组件**: -- 显示原始参数和编辑区域 -- 提供参数差异高亮(原值 vs 新值) -- 支持 JSON 格式验证 -- 保存修改并执行按钮 +**UI Components**: +- Display original parameters and editing area +- Provide parameter difference highlighting (original vs new values) +- Support JSON format validation +- Save modification and execute button -**交互流程**: -1. 用户点击"修改参数"按钮 -2. 展开参数编辑区域,显示原始 JSON -3. 用户在文本框中编辑 JSON -4. 实时验证 JSON 格式 -5. 点击"保存并执行"提交修改 -6. 系统反馈修改摘要并继续执行 +**Interaction Flow**: +1. User clicks "Modify Parameters" button +2. Expand parameter editing area, display original JSON +3. User edits JSON in text box +4. Real-time JSON format validation +5. Click "Save and Execute" to submit modification +6. System shows modification summary and continues execution -**用户体验**: -- 对于配置工具,可以提供更友好的命令行界面而非纯 JSON -- 高亮显示修改的部分(红色删除线、绿色新增) -- 提供参数预设模板 -- 显示修改前后的对比视图 +**User Experience**: +- For configuration tools, can provide more friendly command-line interface instead of pure JSON +- Highlight modified parts (red strikethrough, green addition) +- Provide parameter preset templates +- Show before/after comparison view -### 关键代码位置 +### Key Code Locations -**文件**:`gns3_copilot.py` +**File**: `gns3_copilot.py` -增强 `conditional_tool_execution` 函数,检测 `user_modified_args`: +Enhance `conditional_tool_execution` function to detect `user_modified_args`: ```python def conditional_tool_execution(state: MessagesState) -> dict: - """条件工具执行节点(支持用户修改)""" + """Conditional tool execution node (supports user modification)""" - # 处理用户修改的情况 + # Handle user modification case if state.get("user_modified_args"): - # LLM 已通过 HumanMessage 收到用户修改 - # 清除标记,让 LLM 重新生成 tool call + # LLM already received user modification via HumanMessage + # Clear marker, let LLM regenerate tool call return { "user_modified_args": None, "pending_tool_calls": [], "hitl_confirmation_required": False } - # ... 其他处理逻辑 + # ... other processing logic ``` -**参数差异生成**: +**Parameter Difference Generation**: ```python def _generate_modification_summary(tool_name: str, original: dict, modified: dict) -> str: - """生成参数修改摘要""" + """Generate parameter modification summary""" if tool_name == "execute_multiple_device_config_commands": - # 特殊处理配置工具,逐命令对比 + # Special handling for configuration tools, compare command by command orig_devices = original.get("device_configs", []) mod_devices = modified.get("device_configs", []) @@ -1321,82 +1321,82 @@ def _generate_modification_summary(tool_name: str, original: dict, modified: dic summary.append(f" - {oc}") summary.append(f" + {mc}") - return "\n".join(summary) if summary else "无修改" + return "\n".join(summary) if summary else "No modifications" - # 其他工具的通用处理 + # Other tools' generic handling # ... ``` -### 安全考虑 +### Security Considerations -#### 参数验证 +#### Parameter Validation -- 验证修改后的参数结构是否完整 -- 检查必填字段是否存在 -- 验证参数值是否在合法范围内 +- Validate modified parameter structure is complete +- Check required fields exist +- Validate parameter values are within legal range -#### 危险命令二次确认 +#### Dangerous Command Secondary Confirmation -即使修改后,某些命令仍需二次确认: +Even after modification, certain commands still require secondary confirmation: - `erase startup-config` - `reload` - `format flash:` -### 测试用例 +### Test Cases -**场景**:用户修改配置命令 +**Scenario**: User modifies configuration command -1. LLM 生成配置命令:`ip address 10.0.0.1 255.255.255.0` -2. 用户修改为:`ip address 192.168.1.1 255.255.255.0` -3. 系统反馈修改摘要 -4. LLM 理解并确认使用新 IP -5. 执行工具,使用修改后的参数 -6. 验证配置结果 +1. LLM generates configuration command: `ip address 10.0.0.1 255.255.255.0` +2. User modifies to: `ip address 192.168.1.1 255.255.255.0` +3. System shows modification summary +4. LLM understands and confirms using new IP +5. Execute tool with modified parameters +6. Verify configuration result --- -## 风险评估 +## Risk Assessment -| 风险 | 概率 | 影响 | 缓解措施 | -|------|------|------|----------| -| 破坏现有功能 | 低 | 高 | 完整的回归测试 | -| 状态不一致 | 中 | 中 | checkpoint 验证 | -| 性能影响 | 低 | 低 | 异步处理 | -| 前端集成问题 | 中 | 中 | 详细的前端文档 | +| Risk | Probability | Impact | Mitigation Measures | +|------|-------------|--------|---------------------| +| Breaking existing functionality | Low | High | Complete regression testing | +| State inconsistency | Medium | Medium | Checkpoint validation | +| Performance impact | Low | Low | Asynchronous processing | +| Frontend integration issues | Medium | Medium | Detailed frontend documentation | --- -## 回滚计划 +## Rollback Plan -如需回滚: +If rollback is needed: -1. 移除 HITL 相关节点 -2. 恢复原始 `should_continue` 和 `tool_node` -3. 删除新增的 API 端点 -4. checkpoint 中的新字段会被自动忽略 +1. Remove HITL-related nodes +2. Restore original `should_continue` and `tool_node` +3. Delete new API endpoints +4. New fields in checkpoint are automatically ignored -**回滚时间**:约 30 分钟 +**Rollback Time**: Approximately 30 minutes --- -## 后续增强 +## Future Enhancements -1. **批量操作优化**:支持选择性确认部分工具 -2. **操作历史**:记录所有 HITL 操作 -3. **自动审批**:对低风险操作设置自动审批规则 -4. **多用户协作**:支持多人审批流程 -5. **模板管理**:保存常用配置为模板 +1. **Batch operation optimization**: Support selective confirmation of some tools +2. **Operation history**: Record all HITL operations +3. **Automatic approval**: Set automatic approval rules for low-risk operations +4. **Multi-user collaboration**: Support multi-person approval process +5. **Template management**: Save commonly used configurations as templates --- -## 参考文档 +## Reference Documentation - [LangGraph Interrupts](https://langchain-ai.github.io/langgraph/concepts/low_level/#interruption) -- [GNS3 Copilot 架构](./ai-chat-api-design.md) -- [工具响应格式标准](./tool-response-format-standard.md) +- [GNS3 Copilot Architecture](./ai-chat-api-design.md) +- [Tool Response Format Standard](./tool-response-format-standard.md) --- -**文档版本**:v1.0 -**创建日期**:2026-03-04 -**作者**:GNS3 Development Team +**Document Version**: v1.0 +**Created Date**: 2026-03-04 +**Author**: GNS3 Development Team diff --git a/docs/gns3-copilot/todo/orphan-tool-calls-recovery.md b/docs/gns3-copilot/todo/orphan-tool-calls-recovery.md index 7859b59b7..057144ad0 100644 --- a/docs/gns3-copilot/todo/orphan-tool-calls-recovery.md +++ b/docs/gns3-copilot/todo/orphan-tool-calls-recovery.md @@ -1,42 +1,42 @@ -# TODO: 修复孤儿 Tool Calls 导致的 Checkpoint 状态不一致 +# TODO: Fix Orphan Tool Calls Causing Checkpoint State Inconsistency -## 问题描述 +## Problem Description -当 LangGraph agent 在执行过程中异常终止(如服务被强制关闭、进程崩溃等),可能导致 checkpoint 中保存了包含 `tool_calls` 的 `AIMessage`,但没有对应的 `ToolMessage`。这种状态不一致会导致后续对话恢复时出现错误。 +When the LangGraph agent terminates abnormally during execution (such as forced service shutdown, process crash, etc.), it may result in a checkpoint containing an `AIMessage` with `tool_calls` but no corresponding `ToolMessage`. This state inconsistency can cause errors during subsequent conversation recovery. -### 术语说明 +### Terminology -- **孤儿 tool_calls**:`AIMessage` 中包含 `tool_calls` 字段,但消息列表中没有对应的 `ToolMessage` -- **Checkpoint**:LangGraph 用于持久化对话状态的机制 -- **状态不一致**:checkpoint 中的消息状态不符合预期的消息对(AIMessage + ToolMessage) +- **Orphan tool_calls**: `AIMessage` contains `tool_calls` field, but there's no corresponding `ToolMessage` in the message list +- **Checkpoint**: LangGraph's mechanism for persisting conversation state +- **State inconsistency**: Message state in checkpoint doesn't match expected message pairs (AIMessage + ToolMessage) --- -## 触发场景 +## Trigger Scenarios -### 场景 1:进程异常终止(主要问题) +### Scenario 1: Process Abnormal Termination (Primary Issue) ``` -执行流程: -用户消息 → llm_call → AIMessage(tool_calls) → [Checkpoint 保存] +Execution flow: +User message → llm_call → AIMessage(tool_calls) → [Checkpoint saved] ↓ - [进程崩溃/服务关闭] + [Process crash/service shutdown] ↓ - tool_node 未执行 + tool_node not executed ↓ - Checkpoint 中: - - AIMessage (有 tool_calls) ✅ - - ToolMessage ❌ 缺失 + Checkpoint contains: + - AIMessage (has tool_calls) ✅ + - ToolMessage ❌ missing ``` -**触发条件:** -- LLM 返回包含 tool_calls 的响应 -- Checkpoint 已保存 AIMessage -- 在 tool_node 执行前服务被关闭(kill -9、Ctrl+C、崩溃等) +**Trigger conditions:** +- LLM returns a response containing tool_calls +- Checkpoint has saved AIMessage +- Service is shut down before tool_node execution (kill -9, Ctrl+C, crash, etc.) -### 场景 2:达到最大调用次数(已处理) +### Scenario 2: Maximum Call Count Reached (Already Handled) -当前代码通过 `recursion_limit_continue` 函数在 tool_node **执行后**检查剩余步数: +Current code checks remaining steps after tool_node execution via the `recursion_limit_continue` function: ```python def recursion_limit_continue(state: MessagesState) -> Literal["llm_call", END]: @@ -48,59 +48,59 @@ def recursion_limit_continue(state: MessagesState) -> Literal["llm_call", END]: return END ``` -**执行流程:** +**Execution flow:** ``` remaining_steps = 5 llm_call → AIMessage(tool_calls) → remaining_steps = 4 ↓ - should_continue → tool_node(因为有 tool_calls) + should_continue → tool_node (because there are tool_calls) ↓ tool_node → ToolMessage → remaining_steps = 3 ↓ recursion_limit_continue → remaining_steps < 4 → END ✅ ``` -**结论:** 场景 2 不会产生孤儿 tool_calls,因为 tool_node 总是会执行并生成 ToolMessage。 +**Conclusion:** Scenario 2 won't produce orphan tool_calls because tool_node always executes and generates a ToolMessage. --- -## 修复方案 +## Fix Solution -### 核心思路 +### Core Idea -在 `stream_chat` 开始时,对于已存在的会话,检测并修复孤儿 tool_calls。 +At the start of `stream_chat`, for existing sessions, detect and fix orphan tool_calls. -### 修复策略 +### Fix Strategy -**策略 A:清除 tool_calls(推荐)** +**Strategy A: Clear tool_calls (Recommended)** -创建一个新的 `AIMessage`,内容与原消息相同,但不包含 `tool_calls` 字段。 +Create a new `AIMessage` with the same content as the original message but without the `tool_calls` field. -**优点:** -- 简单、干净 -- 不会影响后续对话 -- 用户可以重新提问 +**Advantages:** +- Simple and clean +- Won't affect subsequent conversation +- User can ask the question again -**缺点:** -- 丢失了 LLM 原本的意图(但已经崩溃了,无法恢复) +**Disadvantages:** +- Loses LLM's original intent (but it already crashed, can't be recovered) --- -## 实现代码 +## Implementation Code -### 1. 添加修复方法(`agent_service.py`) +### 1. Add Fix Method (`agent_service.py`) ```python async def _fix_orphan_tool_calls(self, graph, config: dict, session_id: str): """ - 检测并修复孤儿 tool_calls(AIMessage有tool_calls但没有对应ToolMessage)。 + Detect and fix orphan tool_calls (AIMessage has tool_calls but no corresponding ToolMessage). - 当进程在 tool_node 执行前崩溃时会产生孤儿 tool_calls。 + Orphan tool_calls occur when the process crashes before tool_node execution. - 使用 LangGraph 的 aupdate_state API,安全地创建新的 checkpoint 版本。 + Uses LangGraph's aupdate_state API to safely create a new checkpoint version. """ try: - # 1. 读取当前状态 + # 1. Read current state state = await graph.aget_state(config) if not state or not state.values.get("messages"): return @@ -108,37 +108,37 @@ async def _fix_orphan_tool_calls(self, graph, config: dict, session_id: str): messages = state.values["messages"] last_message = messages[-1] - # 2. 检测孤儿 tool_calls + # 2. Detect orphan tool_calls if not (hasattr(last_message, "tool_calls") and last_message.tool_calls): return - # 检查是否有对应的 ToolMessage + # Check if there's a corresponding ToolMessage has_tool_message = any(isinstance(m, ToolMessage) for m in messages) if has_tool_message: return - log.warning("检测到孤儿 tool_calls: session=%s, 将清除", session_id) + log.warning("Detected orphan tool_calls: session=%s, will clear", session_id) - # 3. 创建修复后的消息(不含 tool_calls) + # 3. Create fixed message (without tool_calls) from langchain.messages import AIMessage fixed_message = AIMessage( content=last_message.content, id=getattr(last_message, "id", None) ) - # 4. 使用 LangGraph API 更新状态(创建新 checkpoint) + # 4. Use LangGraph API to update state (create new checkpoint) await graph.aupdate_state(config, {"messages": [fixed_message]}) - log.info("孤儿 tool_calls 已修复: session=%s", session_id) + log.info("Orphan tool_calls fixed: session=%s", session_id) except Exception as e: - log.error("修复孤儿 tool_calls 失败: %s", e, exc_info=True) + log.error("Failed to fix orphan tool_calls: %s", e, exc_info=True) ``` -### 2. 在 `stream_chat` 中调用(`agent_service.py`) +### 2. Call in `stream_chat` (`agent_service.py`) -在获取 graph 之后、开始 stream 之前添加修复逻辑: +Add fix logic after getting the graph and before starting the stream: ```python async def stream_chat( @@ -151,7 +151,7 @@ async def stream_chat( mode: str = "text", llm_config: Optional[Dict[str, Any]] = None, ) -> AsyncGenerator[Dict[str, Any], None]: - # ... 现有代码 ... + # ... existing code ... # Get or create chat session repo = ChatSessionsRepository(self._checkpointer_conn) @@ -163,7 +163,7 @@ async def stream_chat( session = await repo.create_session(...) log.debug("Created new chat session: thread_id=%s", session_id) - # ... 设置 context variables ... + # ... set context variables ... # Build config config = { @@ -187,100 +187,100 @@ async def stream_chat( # Get the compiled graph graph = await self._get_graph() - # 🔧 修复状态:对于已存在的会话,检查并修复孤儿 tool_calls + # 🔧 Fix state: for existing sessions, check and fix orphan tool_calls if not is_new_session: await self._fix_orphan_tool_calls(graph, config, session_id) log.debug("LangGraph graph obtained, starting stream") - # ... 继续现有代码 ... + # ... continue existing code ... ``` -### 3. 需要添加的 import +### 3. Required Imports -确保 `agent_service.py` 中有以下 import: +Ensure `agent_service.py` has the following import: ```python -from langchain.messages import ToolMessage # 用于检测 ToolMessage 类型 +from langchain.messages import ToolMessage # For detecting ToolMessage type ``` --- -## 对 Checkpoint 数据库的影响 +## Impact on Checkpoint Database -### LangGraph Checkpoint 机制 +### LangGraph Checkpoint Mechanism -LangGraph 的 checkpoint 是**版本化**的,每次状态更新会创建新记录: +LangGraph checkpoints are **versioned** - each state update creates a new record: ``` -checkpoints 表结构: +checkpoints table structure: - thread_id -- checkpoint_id (递增的版本号) -- checkpoint (序列化的状态数据) +- checkpoint_id (incrementing version number) +- checkpoint (serialized state data) - metadata - ... ``` -### 安全性分析 +### Security Analysis -| 方面 | 影响 | 说明 | -|------|------|------| -| **原始数据** | 保留不变 | `aupdate_state` 创建新版本,不覆盖历史 | -| **数据库结构** | 完全兼容 | 使用 LangGraph 原生 API,不会破坏结构 | -| **并发安全** | 内置保护 | LangGraph 有锁机制处理并发访问 | -| **存储开销** | 很小 | 只增加一条 checkpoint 记录(约几 KB) | -| **可回滚性** | 支持 | 可回滚到修复前的任何版本 | +| Aspect | Impact | Description | +|--------|--------|-------------| +| **Original data** | Preserved unchanged | `aupdate_state` creates new version, doesn't overwrite history | +| **Database structure** | Fully compatible | Uses LangGraph native API, won't break structure | +| **Concurrency safety** | Built-in protection | LangGraph has locking mechanism for concurrent access | +| **Storage overhead** | Minimal | Only adds one checkpoint record (about a few KB) | +| **Revertibility** | Supported | Can roll back to any version before fix | -### 为什么不直接操作数据库 +### Not Direct Database Manipulation -**❌ 危险方式:** +**❌ Dangerous approach:** ```python -# 直接修改数据库 - 破坏性强 +# Direct database modification - destructive await conn.execute( "UPDATE checkpoints SET checkpoint = ? WHERE ...", [modified_json] ) ``` -**问题:** -- 可能破坏序列化格式 -- 不创建新版本,覆盖历史 -- 可能导致数据库锁定或损坏 -- 违反 LangGraph 的设计原则 +**Problems:** +- May break serialization format +- Doesn't create new version, overwrites history +- May cause database locking or corruption +- Violates LangGraph design principles -**✅ 安全方式:** +**✅ Safe approach:** ```python -# 使用 LangGraph 的 aupdate_state +# Use LangGraph's aupdate_state await graph.aupdate_state(config, {"messages": [fixed_message]}) ``` --- -## 测试方法 +## Testing Methods -### 方法 1:模拟崩溃测试(推荐) +### Method 1: Simulated Crash Test (Recommended) -利用强制关闭服务来模拟崩溃场景: +Simulate crash scenarios by forcibly shutting down the service: ``` -步骤: -1. 启动 GNS3 服务 -2. 发送一个会触发 tool_calls 的消息(例如查询拓扑) -3. 观察日志,等待看到 AIMessage 返回(有 tool_calls) -4. 在 tool_node 执行完成前,强制关闭服务: - - 方式 1: kill -9 - - 方式 2: Ctrl+C (如果支持) -5. 重启 GNS3 服务 -6. 使用同一个 session_id 继续对话 -7. 观察日志,应该看到: - - "检测到孤儿 tool_calls: session=xxx, 将清除" - - "孤儿 tool_calls 已修复: session=xxx" -8. 验证对话可以正常进行 +Steps: +1. Start GNS3 service +2. Send a message that triggers tool_calls (e.g., query topology) +3. Observe logs, wait for AIMessage return (with tool_calls) +4. Force shutdown service before tool_node completes: + - Method 1: kill -9 + - Method 2: Ctrl+C (if supported) +5. Restart GNS3 service +6. Continue conversation using same session_id +7. Observe logs, should see: + - "Detected orphan tool_calls: session=xxx, will clear" + - "Orphan tool_calls fixed: session=xxx" +8. Verify conversation can proceed normally ``` -### 方法 2:单元测试 +### Method 2: Unit Tests -直接构造孤儿 tool_calls 状态来测试修复逻辑: +Directly construct orphan tool_calls state to test fix logic: ```python # tests/test_agent_service.py @@ -290,24 +290,24 @@ from langchain.messages import AIMessage, HumanMessage, ToolMessage @pytest.mark.asyncio async def test_fix_orphan_tool_calls(): - """测试孤儿 tool_calls 修复逻辑""" + """Test orphan tool_calls fix logic""" from gns3server.agent.gns3_copilot.agent_service import AgentService - # 创建测试用的 agent service + # Create test agent service service = AgentService("/tmp/test_project") await service._get_checkpointer() graph = await service._get_graph() config = {"configurable": {"thread_id": "test_session"}} - # 构造孤儿状态:先添加正常消息 + # Construct orphan state: add normal messages first await graph.aupdate_state( config, { "messages": [ - HumanMessage(content="测试消息", id="msg_1"), + HumanMessage(content="Test message", id="msg_1"), AIMessage( - content="让我帮你查一下", + content="Let me check for you", id="msg_2", tool_calls=[{ "id": "call_123", @@ -315,30 +315,30 @@ async def test_fix_orphan_tool_calls(): "args": {"project_id": "test"} }] ) - # 注意:没有对应的 ToolMessage + # Note: No corresponding ToolMessage ], "llm_calls": 1, "remaining_steps": 20 } ) - # 调用修复逻辑 + # Call fix logic await service._fix_orphan_tool_calls(graph, config, "test_session") - # 验证修复结果 + # Verify fix result state = await graph.aget_state(config) last_message = state.values["messages"][-1] - # 应该不再有 tool_calls + # Should no longer have tool_calls assert not hasattr(last_message, "tool_calls") or not last_message.tool_calls - assert last_message.content == "让我帮你查一下" + assert last_message.content == "Let me check for you" - # 清理 + # Cleanup await service.close() @pytest.mark.asyncio async def test_no_fix_when_normal(): - """测试正常状态不会被误修复""" + """Test that normal state isn't incorrectly fixed""" from gns3server.agent.gns3_copilot.agent_service import AgentService service = AgentService("/tmp/test_project") @@ -347,14 +347,14 @@ async def test_no_fix_when_normal(): graph = await service._get_graph() config = {"configurable": {"thread_id": "test_session_2"}} - # 构造正常状态:有完整的 AIMessage + ToolMessage 对 + # Construct normal state: complete AIMessage + ToolMessage pair await graph.aupdate_state( config, { "messages": [ - HumanMessage(content="测试消息", id="msg_1"), + HumanMessage(content="Test message", id="msg_1"), AIMessage( - content="让我帮你查一下", + content="Let me check for you", id="msg_2", tool_calls=[{ "id": "call_123", @@ -363,7 +363,7 @@ async def test_no_fix_when_normal(): }] ), ToolMessage( - content="拓扑信息:...", + content="Topology info: ...", tool_call_id="call_123", name="get_topology", id="msg_3" @@ -374,78 +374,78 @@ async def test_no_fix_when_normal(): } ) - # 记录原始消息数量 + # Record original message count state_before = await graph.aget_state(config) msg_count_before = len(state_before.values["messages"]) - # 调用修复逻辑 + # Call fix logic await service._fix_orphan_tool_calls(graph, config, "test_session_2") - # 验证状态未改变 + # Verify state unchanged state_after = await graph.aget_state(config) msg_count_after = len(state_after.values["messages"]) - assert msg_count_before == msg_count_after # 不应该添加新消息 + assert msg_count_before == msg_count_after # Should not add new messages last_message = state_after.values["messages"][-1] - assert isinstance(last_message, ToolMessage) # 最后一条还是 ToolMessage + assert isinstance(last_message, ToolMessage) # Last is still ToolMessage - # 清理 + # Cleanup await service.close() ``` -### 方法 3:增强日志和监控 +### Method 3: Enhanced Logging and Monitoring -即使无法主动触发,也可以在生产环境验证修复逻辑是否生效: +Even without active triggering, you can verify fix logic works in production: ```python -# 在 _fix_orphan_tool_calls 中添加详细日志 -log.warning("检测到孤儿 tool_calls: session=%s", session_id) -log.info("原始消息: tool_calls=%d, content=%s", +# Add detailed logging in _fix_orphan_tool_calls +log.warning("Detected orphan tool_calls: session=%s", session_id) +log.info("Original message: tool_calls=%d, content=%s", len(last_message.tool_calls), last_message.content[:100]) -log.info("修复后: tool_calls=%d", +log.info("After fix: tool_calls=%d", len(fixed_message.tool_calls) if hasattr(fixed_message, "tool_calls") else 0) ``` --- -## 文件修改清单 +## File Modification Checklist -### 需要修改的文件 +### Files to Modify 1. **`gns3server/agent/gns3_copilot/agent_service.py`** - - 添加 `_fix_orphan_tool_calls` 方法 - - 在 `stream_chat` 方法中调用修复逻辑 + - Add `_fix_orphan_tool_calls` method + - Call fix logic in `stream_chat` method -### 需要添加的测试文件(可选) +### Test Files to Add (Optional) -2. **`tests/test_agent_service.py`**(新建或添加到现有测试文件) - - `test_fix_orphan_tool_calls()` - 测试孤儿 tool_calls 修复 - - `test_no_fix_when_normal()` - 测试正常状态不被误修复 +2. **`tests/test_agent_service.py`** (create new or add to existing test file) + - `test_fix_orphan_tool_calls()` - Test orphan tool_calls fix + - `test_no_fix_when_normal()` - Test normal state isn't incorrectly fixed --- -## 实施步骤 +## Implementation Steps -1. ✅ 创建待办文档(当前文档) -2. ⬜ 在 `agent_service.py` 中添加 `_fix_orphan_tool_calls` 方法 -3. ⬜ 在 `stream_chat` 中调用修复逻辑 -4. ⬜ 使用模拟崩溃方法测试修复效果 -5. ⬜ 添加单元测试(可选) -6. ⬜ 更新相关文档(如有必要) +1. ✅ Create TODO document (current document) +2. ⬜ Add `_fix_orphan_tool_calls` method in `agent_service.py` +3. ⬜ Call fix logic in `stream_chat` +4. ⬜ Test fix effect using simulated crash method +5. ⬜ Add unit tests (optional) +6. ⬜ Update related documentation (if necessary) --- -## 相关代码文件 +## Related Code Files -- **主要修改文件**: `gns3server/agent/gns3_copilot/agent_service.py` -- **相关文件**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` -- **测试文件**: `tests/test_agent_service.py` (待创建) +- **Main modification file**: `gns3server/agent/gns3_copilot/agent_service.py` +- **Related file**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` +- **Test file**: `tests/test_agent_service.py` (to be created) --- -## 参考文档 +## Reference Documentation -- [LangGraph Checkpointer 文档](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer) +- [LangGraph Checkpointer Documentation](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer) - [LangGraph State Management](https://langchain-ai.github.io/langgraph/concepts/low_level/#state) -- [GNS3-Copilot AI Chat API 设计](../ai-chat-api-design.md) +- [GNS3-Copilot AI Chat API Design](../ai-chat-api-design.md) diff --git a/docs/gns3-copilot/todo/tool-response-format-standard.md b/docs/gns3-copilot/todo/tool-response-format-standard.md index 9ca78eb01..ba891e925 100644 --- a/docs/gns3-copilot/todo/tool-response-format-standard.md +++ b/docs/gns3-copilot/todo/tool-response-format-standard.md @@ -1,69 +1,69 @@ # GNS3 Copilot Tool Response Format Standard -## 概述 +## Overview -本文档定义了 GNS3 Copilot 工具的标准响应格式,确保所有工具返回统一的数据结构,便于前端处理和美化显示。 +This document defines the standard response format for GNS3 Copilot tools, ensuring all tools return a unified data structure for easy frontend processing and display. -## 标准响应格式 +## Standard Response Format -### 顶层结构 +### Top-level Structure -所有工具应返回以下标准格式: +All tools should return the following standard format: ```python { - "success": bool, # 整体操作是否成功 - "total": int, # 总操作数量 - "successful": int, # 成功数量 - "failed": int, # 失败数量 - "data": list[dict], # 详细结果列表 - "error": str, # 全局错误信息(可选,操作完全失败时) - "metadata": dict # 元数据(可选) + "success": bool, # Whether the overall operation succeeded + "total": int, # Total number of operations + "successful": int, # Number of successful operations + "failed": int, # Number of failed operations + "data": list[dict], # Detailed result list + "error": str, # Global error message (optional, when operation completely fails) + "metadata": dict # Metadata (optional) } ``` -**字段说明**: +**Field Descriptions**: -| 字段 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `success` | `bool` | 是 | 整体操作是否成功(`failed == 0` 时为 `True`) | -| `total` | `int` | 是 | 处理的项目总数 | -| `successful` | `int` | 是 | 成功的项目数量 | -| `failed` | `int` | 是 | 失败的项目数量 | -| `data` | `list[dict]` | 是 | 每个项目的详细结果 | -| `error` | `str` | 否 | 全局错误消息(当整个操作失败时) | -| `metadata` | `dict` | 否 | 元数据(时间戳、执行时间等) | +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `success` | `bool` | Yes | Whether the overall operation succeeded (True when `failed == 0`) | +| `total` | `int` | Yes | Total number of items processed | +| `successful` | `int` | Yes | Number of successful items | +| `failed` | `int` | Yes | Number of failed items | +| `data` | `list[dict]` | Yes | Detailed results for each item | +| `error` | `str` | No | Global error message (when entire operation fails) | +| `metadata` | `dict` | No | Metadata (timestamp, execution time, etc.) | -### 单个项目格式 +### Single Item Format -`data` 数组中的每个项目应遵循以下格式: +Each item in the `data` array should follow this format: ```python { - "id": str, # 设备/节点/链接 ID - "name": str, # 人类可读的名称 - "status": "success" | "failed", # 项目状态 - "result": str, # 成功时的结果或输出 - "error": str # 失败时的错误信息 + "id": str, # Device/node/link ID + "name": str, # Human-readable name + "status": "success" | "failed", # Item status + "result": str, # Result or output on success + "error": str # Error message on failure } ``` -**字段说明**: +**Field Descriptions**: -| 字段 | 类型 | 必需 | 说明 | -|------|------|------|------| -| `id` | `str` | 是 | 设备/节点/链接的唯一标识符 | -| `name` | `str` | 是 | 人类可读的名称 | -| `status` | `str` | 是 | `"success"` 或 `"failed"` | -| `result` | `str` | 条件 | 状态为 `success` 时的输出 | -| `error` | `str` | 条件 | 状态为 `failed` 时的错误信息 | +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | `str` | Yes | Unique identifier for device/node/link | +| `name` | `str` | Yes | Human-readable name | +| `status` | `str` | Yes | `"success"` or `"failed"` | +| `result` | `str` | Conditional | Output when status is `success` | +| `error` | `str` | Conditional | Error message when status is `failed` | -## 示例 +## Examples -### 成功响应示例 +### Success Response Example ```python -# 执行多个设备的显示命令 +# Execute display commands on multiple devices { "success": True, "total": 3, @@ -96,10 +96,10 @@ } ``` -### 完全失败示例 +### Complete Failure Example ```python -# 整个操作失败(如参数错误) +# Entire operation failed (e.g., parameter error) { "success": False, "total": 0, @@ -113,10 +113,10 @@ } ``` -### 单个设备操作示例 +### Single Device Operation Example ```python -# 操作单个设备 +# Operate on a single device { "success": True, "total": 1, @@ -134,56 +134,56 @@ } ``` -## 使用标准化函数 +## Using the Standardization Function -在 `gns3server.agent.gns3_copilot.utils` 模块中提供了 `normalize_tool_response` 函数,用于将各种格式转换为标准格式: +The `normalize_tool_response` function is provided in the `gns3server.agent.gns3_copilot.utils` module to convert various formats to the standard format: ```python from gns3server.agent.gns3_copilot.utils import normalize_tool_response -# 标准化工具响应 +# Normalize tool response normalized = normalize_tool_response(raw_response, tool_name="my_tool") ``` -该函数支持: -- 列表格式(`[{...}, {...}]`) -- 字典格式(`{"nodes": [...]}`) -- 字符串格式(自动解析 JSON/Python literal) -- 混合格式(兼容旧工具) +This function supports: +- List format (`[{...}, {...}]`) +- Dict format (`{"nodes": [...]}`) +- String format (automatically parses JSON/Python literal) +- Mixed format (compatible with legacy tools) -## 兼容性 +## Compatibility -### 向后兼容 +### Backward Compatibility -`normalize_tool_response` 函数设计为向后兼容,可以处理现有工具的各种格式: +The `normalize_tool_response` function is designed to be backward compatible and can handle various formats from existing tools: -- `status` / `error` 字段 -- `output` / `result` 字段 -- `device_name` / `name` 字段 -- `total_nodes` / `total` 字段 +- `status` / `error` fields +- `output` / `result` fields +- `device_name` / `name` fields +- `total_nodes` / `total` fields -### 推荐的迁移策略 +### Recommended Migration Strategy -1. **新工具**:直接返回标准格式 -2. **现有工具**:保持不变,使用 `normalize_tool_response` 标准化 -3. **前端**:依赖标准格式处理显示 +1. **New Tools**: Return standard format directly +2. **Existing Tools**: Keep unchanged, use `normalize_tool_response` to standardize +3. **Frontend**: Rely on standard format for display processing -## 前端集成建议 +## Frontend Integration Recommendations -### 渲染逻辑 +### Rendering Logic ```javascript function renderToolResponse(response) { if (!response.success) { - // 显示全局错误 + // Show global error showError(response.error); return; } - // 显示统计摘要 + // Show statistics summary showSummary(response.total, response.successful, response.failed); - // 渲染每个项目 + // Render each item response.data.forEach(item => { if (item.status === 'success') { showSuccess(item.name, item.result); @@ -194,22 +194,22 @@ function renderToolResponse(response) { } ``` -### 状态图标 +### Status Icons -| 状态 | 图标建议 | 颜色 | -|------|----------|------| -| `success` | ✓ 绿色 | 绿色 | -| `failed` | ✗ 红色 | 红色 | -| `unknown` | ? 灰色 | 灰色 | +| Status | Icon Suggestion | Color | +|--------|----------------|-------| +| `success` | ✓ Green | Green | +| `failed` | ✗ Red | Red | +| `unknown` | ? Gray | Gray | -## 版本控制 +## Version Control -当前标准版本:`v1.0` +Current standard version: `v1.0` -格式变更时,应更新 `metadata.version` 字段,前端据此适配。 +When the format changes, update the `metadata.version` field, and the frontend adapts accordingly. -## 参考 +## References -- 实现:`gns3server/agent/gns3_copilot/utils/parse_tool_content.py` -- 消息转换:`gns3server/agent/gns3_copilot/utils/message_converters.py` -- 工具示例:`gns3server/agent/gns3_copilot/tools_v2/` +- Implementation: `gns3server/agent/gns3_copilot/utils/parse_tool_content.py` +- Message conversion: `gns3server/agent/gns3_copilot/utils/message_converters.py` +- Tool examples: `gns3server/agent/gns3_copilot/tools_v2/` diff --git a/docs/gns3-copilot/troubleshooting/force-kill-residual-processes.md b/docs/gns3-copilot/troubleshooting/force-kill-residual-processes.md index 17c43b59b..217d2d539 100644 --- a/docs/gns3-copilot/troubleshooting/force-kill-residual-processes.md +++ b/docs/gns3-copilot/troubleshooting/force-kill-residual-processes.md @@ -1,150 +1,150 @@ -# Force Kill (kill -9) 导致残留进程问题 +# Force Kill (kill -9) Causing Residual Processes Issue -## 问题描述 +## Problem Description -当使用 `kill -9` 强制关闭 gns3server 进程后,重新启动 gns3server 会出现以下错误: +When using `kill -9` to forcibly close the gns3server process, restarting gns3server results in the following errors: -### 1. Dynamips VM 创建失败 +### 1. Dynamips VM Creation Failure ``` ERROR gns3server.api.routes.compute:133 Compute node error: Dynamips error when running command 'vm create "R1" 1 c7200 ': unable to create VM instance 'R1' ``` -### 2. project_id 为 "undefined" 的验证错误 +### 2. Validation Error with project_id as "undefined" ``` ERROR gns3server.api.server:208 Request validation error in /v3/projects/undefined/nodes/{node_id} (PUT): 1 validation error: {'type': 'uuid_parsing', 'loc': ('path', 'project_id'), 'msg': 'Input should be a valid UUID, invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `u` at 1', 'input': 'undefined', ...} ``` -### 3. TCP 端口仍在使用的警告 +### 3. TCP Port Still in Use Warning ``` WARNING gns3server.compute.project:355 Project d672144c-4de9-4a97-a23d-307ddc3ab9b1 has TCP ports still in use: {5001} ``` -## 根本原因 +## Root Cause -使用 `kill -9` 强制终止 gns3server 进序时,gns3server 没有机会正确清理其启动的子进程,导致以下残留进程仍在运行: +When using `kill -9` to forcibly terminate the gns3server process, gns3server has no opportunity to properly clean up its spawned child processes, leaving the following residual processes running: -- **Dynamips hypervisor 进程** (dynamips) -- **VPCS 虚拟 PC 进程** (vpcs) -- **Docker 容器**(虽然在日志中显示被移除,但可能有些状态未清理) -- **其他模拟器进程** +- **Dynamips hypervisor processes** (dynamips) +- **VPCS virtual PC processes** (vpcs) +- **Docker containers** (although shown as removed in logs, some state may not be cleaned up) +- **Other emulator processes** -这些残留进程会: -1. 占用相同的端口号和资源 ID -2. 保持旧的 socket 连接 -3. 导致新启动的 gns3server 无法重新分配相同资源 +These residual processes will: +1. Occupy the same port numbers and resource IDs +2. Maintain old socket connections +3. Cause the newly started gns3server to be unable to reallocate the same resources -## 解决方案 +## Solutions -### 方法 1:手动清理残留进程(推荐) +### Method 1: Manual Cleanup of Residual Processes (Recommended) -在强制关闭 gns3server 后,查找并清理残留进程: +After forcibly closing gns3server, find and clean up residual processes: ```bash -# 查找 dynamips 进程 +# Find dynamips processes ps aux | grep dynamips -# 查找 vpcs 进程 +# Find vpcs processes ps aux | grep vpcs -# 终止残留进程 +# Terminate residual processes killall dynamips killall vpcs ``` -### 方法 2:使用 pkill 清理相关进程 +### Method 2: Use pkill to Clean Related Processes ```bash -# 清理所有 GNS3 相关进程 +# Clean all GNS3 related processes pkill -9 dynamips pkill -9 vpcs -pkill -6 ubridge +pkill -9 ubridge ``` -### 方法 3:重启前检查 +### Method 3: Check Before Restart -在重新启动 gns3server 之前,确保没有残留进程: +Before restarting gns3server, ensure there are no residual processes: ```bash -# 检查是否有残留的 GNS3 进程 +# Check if there are residual GNS3 processes ps aux | grep -E "(dynamips|vpcs|ubridge|gns3)" | grep -v grep ``` -## 预防措施 +## Preventive Measures -### 1. 使用正确的关闭方法 +### 1. Use Proper Shutdown Methods -优先使用以下方法关闭 gns3server,而不是 `kill -9`: +Prefer the following methods to close gns3server instead of `kill -9`: ```bash -# 如果使用 systemd +# If using systemd sudo systemctl stop gns3server -# 如果直接运行 -# 按 Ctrl+C 或使用正常的 kill 信号 +# If running directly +# Press Ctrl+C or use normal kill signal kill ``` -### 2. 使用 SIGTERM 而不是 SIGKILL +### 2. Use SIGTERM Instead of SIGKILL ```bash -# 先尝试正常终止(允许进程清理) +# Try normal termination first (allows process to clean up) kill -15 -# 等待几秒,如果进程仍在运行,再使用 kill -9 +# Wait a few seconds, if process is still running, then use kill -9 sleep 3 if ps -p > /dev/null; then kill -9 fi ``` -### 3. 实现自动清理脚本 +### 3. Implement Automatic Cleanup Script -可以创建一个启动脚本来检查并清理残留进程: +You can create a startup script to check and clean up residual processes: ```bash #!/bin/bash # cleanup_before_start.sh -# 检查并清理残留的 dynamips 进程 +# Check and clean residual dynamips processes if pgrep -f dynamips > /dev/null; then - echo "发现残留的 dynamips 进程,正在清理..." + echo "Found residual dynamips processes, cleaning up..." killall -9 dynamips fi -# 检查并清理残留的 vpcs 进程 +# Check and clean residual vpcs processes if pgrep -f vpcs > /dev/null; then - echo "发现残留的 vpcs 进程,正在清理..." + echo "Found residual vpcs processes, cleaning up..." killall -9 vpcs fi -# 等待端口释放 +# Wait for ports to be released sleep 1 -# 启动 gns3server +# Start gns3server gns3server ``` -## 技术细节 +## Technical Details -### 为什么 kill -9 会导致这个问题? +### Why Does kill -9 Cause This Problem? -1. **SIGKILL 信号无法被捕获**:进程无法捕获或忽略 SIGKILL 信号,因此没有机会执行清理代码 -2. **子进程成为孤儿进程**:父进程被强制终止后,子进程被 init/PID 1 接管,但它们不知道父进程已死 -3. **资源未释放**:socket、端口、文件锁等资源没有被正确释放 -4. **状态不一致**:gns3server 的内部状态(如端口分配、ID 分配)被清除,但实际资源仍被占用 +1. **SIGKILL signal cannot be captured**: Processes cannot capture or ignore the SIGKILL signal, so there's no chance to execute cleanup code +2. **Child processes become orphans**: After the parent process is forcibly terminated, child processes are adopted by init/PID 1, but they don't know the parent has died +3. **Resources not released**: Sockets, ports, file locks, and other resources are not properly released +4. **State inconsistency**: gns3server's internal state (such as port allocation, ID allocation) is cleared, but actual resources are still occupied -### 涉及的代码位置 +### Code Locations Involved -- **Dynamips 进程管理**:`gns3server/compute/dynamips/` -- **端口分配和跟踪**:`gns3server/compute/project.py:355` -- **节点更新 API**:`gns3server/api/routes/controller/nodes.py:230` +- **Dynamips Process Management**: `gns3server/compute/dynamips/` +- **Port Allocation and Tracking**: `gns3server/compute/project.py:355` +- **Node Update API**: `gns3server/api/routes/controller/nodes.py:230` -## 相关问题 +## Related Issues -- [ ] 考虑在 gns3server 启动时自动检测并清理残留进程 -- [ ] 添加进程健康检查机制 -- [ ] 实现更健壮的端口和 ID 重用逻辑 -- [ ] 添加残留进程检测和警告 +- [ ] Consider automatically detecting and cleaning up residual processes at gns3server startup +- [ ] Add process health check mechanism +- [ ] Implement more robust port and ID reuse logic +- [ ] Add residual process detection and warnings