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 = ` -