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