gns3-server/docs/ai-chat-api-design.md
YueGuobin 4a017b1a7f feat(chat): add session pinning feature with database migration
- Add `pinned` column to chat_sessions table with default FALSE
- Implement database migration for existing installations using PRAGMA table_info
- Create composite index for pinned + updated_at sorting
- Add pin/unpin API endpoints (PUT/DELETE /sessions/{id}/pin)
- Update session listing to sort by pinned status then updated_at
- Extend ChatSessionsRepository with pin_session method
- Update API documentation to reflect new pinning functionality

The feature allows users to pin important chat sessions to the top of the list. Sessions are sorted with pinned sessions first (by updated_at), followed by regular sessions (by updated_at). Database migration ensures backward compatibility with existing installations.
2026-03-04 23:18:30 +08:00

21 KiB
Raw Blame History

GNS3 Copilot Agent Chat API 设计文档

概述

本文档描述 GNS3 Copilot Chat API 的架构设计和实现方案。该 API 使客户端能够通过 RESTful 接口与 GNS3 Copilot Agent 进行交互,提供流式对话、会话管理、项目拓扑查询等功能。

核心特性

  • 项目级隔离:每个 GNS3 项目拥有独立的 Agent 实例和会话存储
  • 流式响应:使用 Server-Sent Events (SSE) 实现实时流式输出
  • 会话管理:支持会话列表、重命名、删除、历史记录查询
  • 统计追踪自动记录消息数量、LLM 调用次数、Token 使用量
  • 用户隔离:每个用户拥有独立的 LLM 配置和会话空间

架构设计

整体架构

Frontend (Web UI)
    │
    │ SSE Streaming
    ▼
FastAPI Chat API Routes
    │
    │ Project-level Agent Management
    ▼
AgentService (per project)
    │
    ├─ SQLite Checkpointer (project_dir/gns3-copilot/)
    │   ├─ checkpoints table (LangGraph state)
    │   └─ chat_sessions table (session metadata)
    │
    └─ LangGraph Agent
        ├─ llm_call node
        ├─ should_continue node
        └─ tool_node (GNS3 tools)

项目级 Checkpoint 设计

每个 GNS3 项目在项目目录下创建 gns3-copilot/copilot_checkpoints.db SQLite 数据库,包含两张表:

  1. checkpoints 表LangGraph 自动管理):存储 Agent 的对话状态和记忆
  2. chat_sessions 表(自定义):存储会话元数据和统计信息

目录结构

{project.path}/
├── gns3-copilot/
│   └── copilot_checkpoints.db
├── project-files/
└── project.gns3

设计优势

  • 项目删除时自动清理所有相关数据
  • 实现项目级别的会话隔离
  • 便于备份和迁移

用户认证信息传递

背景需求

GNS3 Copilot Agent 需要以下信息才能正常工作:

  1. user_id:获取用户专属的 LLM 配置
  2. jwt_token:调用 GNS3 API 时进行身份验证
  3. llm_config:包含 provider、model、api_key 等配置

ContextVars 方案

使用 Python 的 contextvars.ContextVar 在请求作用域内传递数据,避免敏感信息持久化到 checkpoint。

数据流

1. API 层获取用户信息
   ├─ 从 FastAPI get_current_active_user 获取 user_id
   ├─ 从 Authorization header 提取 jwt_token
   └─ 从数据库查询 LLM 配置(已解密 API key

2. 设置 ContextVars内存临时存储
   ├─ set_current_jwt_token(jwt_token)
   └─ set_current_llm_config(llm_config)

3. 构建安全的 LangGraph config仅包含非敏感标识符
   {
     "configurable": {
       "thread_id": session_id,
       "project_id": project_id
     },
     "metadata": {
       "user_id": user_id
     }
   }

4. LLM 节点从 ContextVars 获取配置
   ├─ get_current_jwt_token()
   └─ get_current_llm_config()

方案优势

  • 敏感数据JWT token、API key仅存储在内存中
  • 请求结束后自动清理,不会持久化到数据库
  • 避免序列化/反序列化开销
  • 实现请求级别的数据隔离

会话管理

chat_sessions 表结构

字段 类型 说明
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

索引

  • idx_thread_idthread_id 唯一索引
  • idx_user_projectuser_id + project_id 复合索引
  • idx_pinned_updatedpinned + updated_at 复合索引(用于置顶排序)

数据库迁移

实现位置agent_service.py_create_chat_sessions_table 方法

迁移策略

  • 使用 PRAGMA table_info(chat_sessions) 检查列是否存在
  • 如果 pinned 列不存在,执行 ALTER TABLE ADD COLUMN 添加该列
  • 确保列存在后再创建索引

代码示例

# Check if pinned column exists, add it if not (migration for existing databases)
cursor = await conn.execute("PRAGMA table_info(chat_sessions)")
columns = await cursor.fetchall()
column_names = [col[1] for col in columns]

if "pinned" not in column_names:
    log.debug("Adding pinned column to existing chat_sessions table")
    await conn.execute("ALTER TABLE chat_sessions ADD COLUMN pinned BOOLEAN DEFAULT FALSE")
    await conn.commit()

# Create pinned index (after column is guaranteed to exist)
await conn.execute("CREATE INDEX IF NOT EXISTS idx_pinned_updated ON chat_sessions(pinned DESC, updated_at DESC)")

优势

  • 向后兼容:现有数据库自动升级,无需手动干预
  • 幂等性:重复执行不会报错
  • 零停机:迁移在初始化时自动完成

ChatSessionsRepository

提供会话的 CRUD 操作:

  • create_session:创建新会话
  • get_session_by_thread:根据 thread_id 查询会话
  • list_sessions:列出用户的会话(支持过滤和分页,按 pinned 和 updated_at 排序)
  • update_session:更新会话(支持增量更新计数器)
  • delete_session:删除会话及其 checkpoints
  • delete_all_sessions:删除项目的所有会话
  • pin_session:置顶或取消置顶会话

统计信息自动收集

统计信息在对话过程中实时收集,流结束后一次性更新到 chat_sessions 表。

实现位置agent_service.pystream_chat 方法

统计逻辑

  1. message_count消息数量

    • 初始值1用户消息
    • on_chat_model_end 事件:+1AI 完整回复,不是每个 chunk
    • on_tool_end 事件:+1每个工具执行结果
  2. llm_calls_countLLM 调用次数)

    • 监听 on_chat_model_start 事件
    • 每次 LLM 开始生成时 +1
  3. input_tokens输入 token

    • on_chat_model_end 事件的 usage_metadata 中提取
    • 重要LangGraph 返回的 input_tokens 已包含对话历史,每次 LLM 调用都会累加之前的对话内容
    • 示例第1次 input=8674第2次 input=9421包含第1次对话 8674+675+系统提示词增量)
  4. output_tokens输出 token

    • on_chat_model_end 事件的 usage_metadata 中提取
    • 重要LangGraph 返回的 output_tokens 也是累加值,包含所有 LLM 调用的输出
    • 示例第1次实际输出=675第2次实际输出=9累加后 output=684675+9
  5. total_tokens总 token

    • 计算公式input_tokens + output_tokens
    • 取最后一次 LLM 调用的累加值进行计算

统计示例(真实数据):

  • 第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 已自动累加,代码直接取最后一次值即可

注意事项

  • message_count 统计的是完整消息,不是流式 chunks
  • Token 数据依赖 LLM 返回的 usage_metadata,某些模型可能不支持
  • 统计数据在流结束后通过 update_session 方法增量更新到数据库
  • LangGraph 已自动处理 input 和 output 的历史累加,代码使用最后一次 LLM 调用的值
  • 消息 ID 处理:创建初始消息时分配 IDHumanMessage(id=str(uuid4()))),从 checkpoint 读取的消息如果没有 ID 也会自动生成
  • 格式转换:使用 message_converters.py 模块处理 LangChain 和 OpenAI 格式之间的转换,确保 tool_calls 格式符合 OpenAI 规范

Title 自动同步

会话标题由 generate_title 节点自动生成,保存在 LangGraph checkpoint 的 conversation_title 字段中。

同步机制

  1. 流式 Chat 完成后,从 checkpoint 读取最终 state
  2. 检查 conversation_title 是否有变化
  3. 如果有变化,更新到 chat_sessions

优势

  • 避免在节点中直接访问数据库(防止循环依赖)
  • 所有数据库更新集中在流结束后
  • 逻辑清晰,易于维护

SSE 消息格式

Chat API 使用 Server-Sent Events (SSE) 进行流式传输。

消息类型

type 说明 包含字段
content AI 文本内容(流式) content
tool_call 工具调用请求 tool_call (id, name, arguments)
tool_start 工具开始执行 tool_name
tool_end 工具执行完成 tool_name, tool_output
error 错误信息 error
done 流结束 session_id
heartbeat 心跳保活 session_id

消息示例

// AI 文本流式输出
{"type": "content", "content": "Hello! How can I help"}

// 工具调用
{"type": "tool_call", "tool_call": {"id": "call_123", "function": {"name": "GNS3TopologyTool", "arguments": {"project_id": "xxx"}}}}

// 工具开始
{"type": "tool_start", "tool_name": "GNS3TopologyTool", "session_id": "xxx"}

// 工具完成
{"type": "tool_end", "tool_name": "GNS3TopologyTool", "tool_output": "{...}", "session_id": "xxx"}

// 完成
{"type": "done", "session_id": "xxx"}

// 错误
{"type": "error", "error": "Project not found", "session_id": "xxx"}

心跳机制

作用:防止代理服务器/负载均衡器因超时断开 SSE 连接。

实现:使用 asyncio.wait 设置超时,超时后发送 heartbeat 消息,然后继续等待下一个事件。

前端处理:收到 heartbeat 消息时直接忽略,不渲染任何内容。

API 端点

所有端点都在 /v3/projects/{project_id}/chat/ 路径下。

方法 端点 说明
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 取消置顶会话

POST /v3/projects/{project_id}/chat/stream

功能:流式对话接口

请求参数

  • message: 用户消息内容
  • session_id: 会话 ID可选不提供则自动创建新会话
  • stream: 是否启用流式响应(默认 true
  • mode: 交互模式(当前仅支持 "text"

响应SSE 流,包含多种类型的消息(见上文消息格式)

项目状态检查:只允许项目状态为 "opened" 时进行对话

GET /v3/projects/{project_id}/chat/sessions

功能:列出项目的所有会话

响应会话列表包含统计信息消息数、token 使用量等),按置顶状态和更新时间排序

GET /v3/projects/{project_id}/chat/sessions/{session_id}/history

功能:获取会话的完整历史记录

参数

  • session_id: 会话 ID
  • limit: 最大消息数量(默认 100

响应

  • thread_id: 会话 ID
  • title: 会话标题
  • messages: 消息列表OpenAI 格式)
  • llm_calls: LLM 调用次数

PATCH /v3/projects/{project_id}/chat/sessions/{session_id}

功能:重命名会话

请求参数

  • title: 新标题1-255 字符)

响应:更新后的会话信息

DELETE /v3/projects/{project_id}/chat/sessions/{session_id}

功能:删除会话及其所有 checkpoint 数据

响应204 No Content

PUT /v3/projects/{project_id}/chat/sessions/{session_id}/pin

功能:置顶会话到列表顶部

响应:更新后的会话信息(包含 pinned=true

DELETE /v3/projects/{project_id}/chat/sessions/{session_id}/pin

功能:取消置顶会话

响应:更新后的会话信息(包含 pinned=false

排序规则

  • 置顶会话pinned=true排在最前面
  • 置顶会话之间按 updated_at 降序排列
  • 普通会话按 updated_at 降序排列

数据模型

ChatRequest

  • message: str - 用户消息内容
  • session_id: Optional[str] - 会话 ID可选
  • stream: bool - 是否流式响应(默认 true
  • mode: Literal["text"] - 交互模式

ChatSession

会话模型,存储会话元数据和统计信息。

基础字段

  • id: 数据库自增 ID
  • thread_id: LangGraph thread_id会话唯一标识
  • user_id: 用户 ID
  • project_id: GNS3 项目 ID
  • title: 会话标题(自动生成或用户修改)

统计字段

  • message_count: 完整消息数量(用户消息 + AI 回复 + 工具结果)
  • llm_calls_count: LLM 总调用次数
  • input_tokens: 输入 token 总数(累加所有 LLM 调用)
  • output_tokens: 输出 token 总数(累加所有 LLM 调用)
  • total_tokens: 总 token 数input_tokens + output_tokens

时间字段

  • last_message_at: 最后一条消息的时间戳
  • created_at: 会话创建时间
  • updated_at: 会话最后更新时间

预留字段

  • metadata: 元数据 JSON 字符串(存储 mode、status、tags 等)
  • stats: 额外统计 JSON 字符串(存储工具调用次数等)

会话管理

  • pinned: 是否置顶到列表顶部(默认 false

ConversationHistory

  • thread_id: str - 会话 ID
  • title: str - 会话标题
  • messages: List[OpenAIMessage] - 消息列表
  • created_at: Optional[str] - 创建时间
  • updated_at: Optional[str] - 更新时间
  • llm_calls: int - LLM 调用次数

OpenAIMessage

OpenAI 兼容的消息模型。

基础字段

  • id: str - 消息唯一标识符(自动生成或从 LangChain 消息继承)
  • role: Literal["user", "assistant", "system", "tool"] - 消息角色
  • content: str - 消息内容支持文本、JSON 字符串)
  • created_at: str - 创建时间ISO 8601

工具相关字段

  • name: Optional[str] - 工具消息名称tool 消息)
  • tool_call_id: Optional[str] - 关联的工具调用 IDtool 消息)
  • tool_calls: Optional[List[OpenAIToolCall]] - 工具调用列表assistant 消息)
    • id: str - 工具调用 ID
    • type: Literal["function"] - 固定为 "function"
    • function: Dict - 包含 name 和 argumentsdict 或 JSON 字符串)

元数据

  • metadata: Optional[Dict] - 额外的消息元数据

核心组件

Message Converters消息格式转换

文件gns3server/agent/gns3_copilot/utils/message_converters.py

职责:在 LangChain 消息格式和 OpenAI 兼容格式之间进行转换

主要函数

  • convert_langchain_to_openai()LangChain → OpenAI 格式
  • convert_openai_to_langchain()OpenAI → LangChain 格式
  • convert_stream_event_to_openai():流事件 → OpenAI SSE 格式

关键转换逻辑

  1. 消息 ID 处理

    • 如果消息没有 ID自动生成 UUID
    • 确保所有返回的消息都有唯一标识符
  2. Tool Calls 格式转换

    • LangChain 格式:{'name': 'xxx', 'args': {...}, 'id': 'yyy', 'type': 'tool_call'}
    • OpenAI 格式:{'id': 'yyy', 'type': 'function', 'function': {'name': 'xxx', 'arguments': '{...}'}}
    • 自动将 args 对象转换为 JSON 字符串(如需要)
  3. Content 类型处理

    • 支持 string、dict、list 类型
    • 非 string 类型自动转换为 JSON 字符串

实现位置utils/message_converters.py

AgentService

职责:项目级的 Agent 管理服务

主要方法

  • stream_chat:流式对话,自动管理会话和统计
  • get_history:获取会话历史
  • list_sessions:列出会话
  • delete_session:删除会话
  • rename_session:重命名会话
  • close:关闭数据库连接

核心流程stream_chat

  1. 初始化 checkpointer 连接(如果未连接)
  2. 获取或创建 chat sessionchat_sessions 表)
  3. 设置 ContextVarsJWT token、LLM config
  4. 构建 LangGraph config
  5. 创建带 ID 的初始消息:HumanMessage(content=message, id=str(uuid4()))
  6. 流式执行 Agent同时收集统计信息
  7. 流结束后更新会话统计到数据库
  8. 同步 auto-generated title

统计收集机制(在 stream_chat 中):

  • 监听 LangGraph 的 astream_events 事件流
  • 在事件循环中实时收集统计数据
  • 统计逻辑不依赖转换后的 SSE chunk直接从原始事件获取

关键事件处理

  • on_chat_model_startLLM 调用次数 +1
  • on_chat_model_end:提取 token 使用量(从 output.usage_metadataAI 消息计数 +1
  • on_tool_end:工具消息计数 +1

实现位置agent_service.py

ProjectAgentManager

职责:全局单例,管理所有项目的 AgentService 实例

方法

  • get_agent(project_id, project_path):获取或创建项目的 AgentService
  • remove_agent(project_id):移除项目的 AgentService
  • close_all:关闭所有 AgentService

Chat API Routes

文件gns3server/api/routes/controller/chat.py

路由注册

router.include_router(
    chat.router,
    prefix="/{project_id}/chat",
    tags=["Chat"]
)

主要端点实现

  • 所有端点都需要用户认证(get_current_active_user
  • 所有端点都检查项目状态是否为 "opened"
  • stream 端点使用 StreamingResponse 返回 SSE 流

项目生命周期集成

项目打开时

创建或获取 AgentService 实例:

agent_manager = await get_project_agent_manager()
agent_service = await agent_manager.get_agent(project_id, project.path)

项目关闭时

移除 AgentService 实例,释放资源:

agent_manager.remove_agent(project_id)

项目删除时

  1. 调用 delete_all_sessions(project_id) 删除所有会话和 checkpoint 数据
  2. 移除 AgentService 实例
  3. 项目目录被删除,数据库文件也被删除

前端集成

useChat Hook

根据 SSE 消息的 type 字段进行不同处理:

type 处理逻辑
content 追加到当前 AI 消息内容
tool_call 创建 tool_call 类型消息,显示工具调用信息
tool_start 可选:显示工具开始执行状态
tool_end 创建 tool_result 类型消息,显示工具执行结果
error 显示错误信息
done 标记流结束,停止加载状态
heartbeat 忽略(保活信号)

错误处理

  • 网络错误:显示重试选项
  • LLM 错误:显示错误消息
  • 项目未打开:提示用户打开项目
  • LLM 未配置:引导用户配置 LLM

安全考虑

用户隔离

  • 每个用户只能访问自己的会话
  • user_id 存储在 config.metadata 中
  • 所有数据库查询都带 user_id 过滤

项目访问控制

  • 只允许访问用户有权限的项目
  • 项目状态检查:只允许 "opened" 状态的项目使用 Chat

LLM 配置安全

  • API key 加密存储在数据库
  • 使用 ContextVars 传递,不持久化到 checkpoint
  • 请求结束后自动清理内存中的敏感信息

性能优化

数据库连接管理

  • 使用 WAL 模式提升并发写入性能
  • 项目级连接复用
  • 项目切换时自动关闭旧连接

Checkpoint 优化

  • LangGraph 自动管理 checkpoints 表
  • 定期清理旧 checkpoint可选
  • 使用索引加速查询thread_id, user_id + project_id

统计信息收集与更新

收集机制(在内存中进行):

  • 在 SSE 流式传输过程中同步收集统计数据
  • 监听 LangGraph 事件流,不增加额外网络开销
  • 使用临时变量累加统计值,避免频繁数据库访问

更新策略(流结束后批量写入):

  • 流式 Chat 完成后,一次性更新 chat_sessions
  • 使用 SQL 增量更新语法:message_count = message_count + ?
  • 单次数据库事务,提交所有统计更新

优势

  • 减少数据库写入次数N 次事件 → 1 次更新)
  • 降低数据库锁竞争
  • 提升流式响应的实时性

实现位置agent_service.py 第 283-294 行

依赖项

  • langchain >= 0.3.0
  • langgraph >= 0.2.0
  • langchain-core
  • langgraph-checkpoint-sqlite >= 3.0.1
  • aiosqlite

扩展性

预留字段

  • metadataTEXT JSON存储会话级别的元数据
  • statsTEXT JSON存储额外的统计信息

未来可能的扩展

  • 多模态支持(图片、文件)
  • 语音输入/输出
  • 多人协作会话
  • 会话分享和导出
  • 自定义工具注册

参考资料