gns3-server/docs/ai-chat-api-design.md
YueGuobin e1f1bb7d9f feat(api): add AI chat API design document for GNS3 Copilot integration
This commit introduces a comprehensive design document outlining the implementation of AI Chat API for GNS3 Copilot Agent within GNS3 Server. The document provides:

- Overview and background on existing components including GNS3 Copilot Agent, LLM configuration management, and API framework
- Reference implementation details from FlowNet-Lab project
- Architecture design with clear component interactions between frontend clients and backend services
- RESTful API specifications for chat streaming, session management, and history retrieval
- Implementation details covering project-based agent management, SQLite checkpoint storage, and LangGraph integration
- Security considerations and deployment guidelines

The design enables clients to interact with GNS3 Copilot Agent through standardized APIs, supporting real-time chat streaming and persistent conversation sessions per project.
2026-03-04 12:08:47 +08:00

1173 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GNS3 Copilot Agent Chat API 实现方案
## 概述
本文档描述了如何在 GNS3 Server 中实现 AI Chat API使客户端能够通过 RESTful API 与 GNS3 Copilot Agent 进行交互。
## 背景
### 现有组件
- **GNS3 Copilot Agent**: 位于 `gns3server/agent/gns3_copilot/`,使用 LangGraph 实现的网络自动化助手
- **LLM 配置管理**: 已有 `llm_model_configs` 系统,支持用户/用户组的 LLM 配置
- **API 框架**: 使用 FastAPI已有的路由结构在 `gns3server/api/routes/controller/`
### 参考实现
FlowNet-Lab 项目 (`/home/yueguobin/myCode/GNS3/FlowNet-Lab`) 已有完整的 Chat API 实现,可作为参考:
- Backend: `backend/api/v1/chat.py`
- Agent Service: `backend/core/agent.py`
## 架构设计
### 整体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Client) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐│
│ │ ChatInput │ │ MessageList │ │ ConversationSidebar ││
│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘│
│ │ │ │ │
│ └─────────────────┼──────────────────────┘ │
│ ▼ │
│ chatService.ts │
│ (SSE Streaming) │
└─────────────────────────────┬───────────────────────────────────┘
HTTP POST /api/v1/chat/stream
┌─────────────────────────────────────────────────────────────────┐
│ GNS3 Server (Backend) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FastAPI Application │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Chat API Routes │ │ │
│ │ │ POST /v3/chat/stream │ │ │
│ │ │ GET /v3/chat/history/{session_id} │ │ │
│ │ │ GET /v3/chat/sessions │ │ │
│ │ │ POST /v3/chat/sessions │ │ │
│ │ │ DELETE /v3/chat/sessions/{session_id} │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └──────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ProjectAgentManager │ │
│ │ (管理每个项目的 Agent Service 实例) │ │
│ └──────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┼───────────────────────────────┐ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ AgentService │ │ │
│ │ │ - SQLiteSaver (项目级 checkpoint) │ │ │
│ │ │ - LangGraph Agent │ │ │
│ │ └─────────────────────┬───────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ LangGraph Agent (gns3_copilot) │ │ │
│ │ │ llm_call → should_continue → tool_node │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ GNS3 Project Directory │
│ │
│ {project.path}/ │
│ ├── .gns3-copilot/ │
│ │ └── checkpoint.db (SQLite - LangGraph 状态存储) │
│ ├── project-files/ │
│ │ ├── nodes/ │
│ │ └── captures/ │
│ └── project.gns3 │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 项目级 Checkpoint 设计
**核心思路**: 每个 GNS3 项目有独立的 checkpoint 数据库,实现项目级别的会话隔离。
```
project_path/.gns3-copilot/checkpoint.db
```
使用 LangGraph 的 `AsyncSqliteSaver` 作为 checkpointer推荐异步方式
### 生产级实现(推荐)
以下实现包含连接管理、项目切换、资源清理等完整功能:
```python
import os
import logging
from typing import Optional
import aiosqlite
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
log = logging.getLogger(__name__)
class CheckpointerManager:
"""Checkpointer 管理器(项目级)"""
def __init__(self, controller):
"""
Args:
controller: GNS3 Controller 实例
"""
self.controller = controller
self._checkpointer: Optional[AsyncSqliteSaver] = None
self._checkpointer_conn: Optional[aiosqlite.Connection] = None
self._project_checkpoint_path: Optional[str] = None
async def _get_checkpointer(self, project_id: str) -> AsyncSqliteSaver:
"""
获取或创建 SQLite checkpointer for a specific project.
Args:
project_id: GNS3 project ID
Returns:
AsyncSqliteSaver instance
"""
log.debug("Getting checkpointer for project %s", project_id)
# Get the project to find its directory
project = self.controller.get_project(project_id)
if not project:
log.error("Project %s not found in controller", project_id)
raise ValueError(f"Project {project_id} not found")
# Create checkpoint file in the project directory
checkpointer_path = os.path.join(project.path, ".gns3-copilot", "copilot_checkpoints.db")
os.makedirs(os.path.dirname(checkpointer_path), exist_ok=True)
log.debug("Checkpoint path: %s", checkpointer_path)
# Store the path for reference
self._project_checkpoint_path = checkpointer_path
# Check if we already created a checkpointer for this project
if self._checkpointer and self._project_checkpoint_path == checkpointer_path:
log.debug("Reusing existing checkpointer")
return self._checkpointer
# Create new checkpointer using AsyncSqliteSaver
log.debug("Creating new async checkpointer at %s", checkpointer_path)
# Close existing connection if switching projects
if self._checkpointer_conn:
try:
await self._checkpointer_conn.close()
log.debug("Closed previous checkpointer connection")
except Exception as e:
log.warning("Error closing old checkpointer connection: %s", e)
# Create new connection
conn = await aiosqlite.connect(checkpointer_path)
# Enable WAL mode for better concurrent performance
await conn.execute("PRAGMA journal_mode=WAL;")
self._checkpointer_conn = conn # Save connection reference to prevent GC
self._checkpointer = AsyncSqliteSaver(conn)
# CRITICAL: Initialize database schema
await self._checkpointer.setup()
log.info("Project async checkpointer created and initialized at %s", checkpointer_path)
return self._checkpointer
async def close(self):
"""关闭 checkpointer 连接"""
if self._checkpointer_conn:
try:
await self._checkpointer_conn.close()
log.debug("Checkpointer connection closed")
except Exception as e:
log.warning("Error closing checkpointer connection: %s", e)
finally:
self._checkpointer_conn = None
self._checkpointer = None
```
### 关键设计要点
| 功能 | 说明 |
|------|------|
| **连接复用** | 同项目复用已有 checkpointer避免重复创建 |
| **项目切换** | 切换项目时自动关闭旧连接,防止连接泄漏 |
| **GC 防护** | 保存 `self._checkpointer_conn` 引用,防止连接被垃圾回收 |
| **Schema 初始化** | 调用 `await self._checkpointer.setup()` 初始化数据库表结构 |
| **WAL 模式** | 启用 WAL 模式提升并发写入性能 |
| **资源清理** | `close()` 方法确保连接正确关闭 |
| **日志记录** | 完整的调试日志,便于问题排查 |
### 注意事项
- 需要安装 `aiosqlite`
- 必须在异步环境中使用
- 切换项目时自动关闭旧连接
- 应用关闭时应调用 `close()` 清理资源
**依赖**:
```
langgraph-checkpoint-sqlite>=3.0.1
aiosqlite
```
## 文件结构
需要创建/修改以下文件:
```
gns3server/
├── agent/gns3_copilot/
│ ├── agent_service.py # [新建] AgentService 封装
│ └── project_agent_manager.py # [新建] 项目级 Agent 管理器
├── api/routes/controller/
│ ├── chat.py # [新建] Chat API 路由
│ └── __init__.py # [修改] 注册 chat router
├── schemas/controller/
│ └── chat.py # [新建] Chat Request/Response 模型
└── docs/
└── ai-chat-api-design.md # [本文档]
```
## 用户认证信息传递机制
### 背景
GNS3 Copilot Agent 需要获取用户的 LLM 配置信息,这需要:
1. **user_id**: 用于从数据库获取用户专属的 LLM 配置
2. **jwt_token**: 用于调用 GNS3 API 时进行身份验证
`model_factory.py` 已支持这些参数:
```python
def create_base_model(
user_id: Optional[UUID] = None,
jwt_token: Optional[str] = None,
llm_config: Optional[dict[str, Any]] = None,
) -> Any:
# 优先级:
# 1. 提供的 llm_config 字典
# 2. 从数据库获取 (需要 user_id 和 jwt_token)
# 3. 环境变量 (回退)
```
### 传递机制
使用 **LangGraph 的 config 参数** 传递用户信息:
```
┌─────────────────────────────────────────────────────────────┐
│ LangGraph Config │
│ { │
│ "configurable": { │
│ "thread_id": "session-xxx", # 会话 ID │
│ "user_id": "user-uuid", # 用户 ID │
│ "jwt_token": "eyJxxx..." # JWT Token │
│ } │
│ } │
└─────────────────────────────────────────────────────────────┘
```
### 实现步骤
#### 1. 修改 gns3_copilot.py 中的 llm_call 函数
```python
# gns3server/agent/gns3_copilot/agent/gns3_copilot.py
def llm_call(state: dict, config: dict = None):
"""LLM decides whether to call a tool or not"""
# 从 config 中获取用户信息
configurable = config.get("configurable", {}) if config else {}
user_id = configurable.get("user_id")
jwt_token = configurable.get("jwt_token")
# ... 原有逻辑 ...
# 传递 user_id 和 jwt_token 给 model factory
model_with_tools = create_base_model_with_tools(
tools,
user_id=user_id,
jwt_token=jwt_token
)
return {
"messages": [model_with_tools.invoke(full_messages)],
"llm_calls": state.get("llm_calls", 0) + 1,
"topology_info": topology_info,
}
```
#### 2. 修改 generate_title 函数(同样需要传递)
```python
def generate_title(state: MessagesState, config: dict = None) -> dict:
"""Generate conversation title"""
configurable = config.get("configurable", {}) if config else {}
user_id = configurable.get("user_id")
jwt_token = configurable.get("jwt_token")
# ... 使用 user_id 和 jwt_token ...
```
#### 3. 在 AgentService.stream_chat 中构建 config
```python
# gns3server/agent/gns3_copilot/agent_service.py
async def stream_chat(
self,
message: str,
session_id: str,
project_id: Optional[str] = None,
user_id: Optional[str] = None,
jwt_token: Optional[str] = None,
mode: str = "text"
) -> AsyncGenerator[Dict[str, Any], None]:
# 构建包含用户信息的 config
config = {
"configurable": {
"thread_id": session_id,
"user_id": user_id,
"jwt_token": jwt_token,
}
}
# 流式处理时传递 config
async for event in self.graph.astream_events(inputs, config=config, version="v2"):
# ...
```
#### 4. 在 API 路由中获取并传递用户信息
```python
# gns3server/api/routes/controller/chat.py
from fastapi import Request
@router.post("/stream")
async def stream_chat(
request: ChatRequest,
current_user = Depends(get_current_active_user)
):
# 获取 JWT token
auth_header = request.headers.get("Authorization")
jwt_token = auth_header.replace("Bearer ", "") if auth_header else None
# 获取 user_id
user_id = str(current_user.user_id)
# 传递用户信息给 AgentService
agent_service = agent_manager.get_agent(request.project_id, project_path)
async def generate():
async for chunk in agent_service.stream_chat(
message=request.message,
session_id=session_id,
project_id=request.project_id,
user_id=user_id, # 传递 user_id
jwt_token=jwt_token, # 传递 jwt_token
mode=request.mode
):
# ...
```
### 完整数据流
```
1. 前端发起请求 (带 Authorization: Bearer <token>)
2. FastAPI get_current_active_user 验证并返回 User 对象
3. 从 Header 获取 JWT token
4. 构建 LangGraph config:
{
"configurable": {
"thread_id": session_id,
"user_id": user_id,
"jwt_token": jwt_token
}
}
5. AgentService.stream_chat() 传递 config 给 astream_events()
6. llm_call() / generate_title() 从 config 获取用户信息
7. create_base_model() 使用 user_id 从数据库获取 LLM 配置
```
## 消息格式定义 (参考 FlowNet-Lab)
### 概述
Chat API 使用 Server-Sent Events (SSE) 进行流式传输,消息格式分为:
- **请求格式** (ChatRequest)
- **响应格式** (ChatResponse)
- **消息类型** (Message Types)
### 1. 请求格式 (ChatRequest)
```python
class ChatRequest(BaseModel):
"""Chat 请求模型"""
message: str # 用户消息内容
session_id: Optional[str] = None # 会话 ID (可选,不提供则自动创建)
project_id: str # GNS3 项目 ID
stream: bool = True # 是否启用流式响应
temperature: Optional[float] = None # LLM 温度参数
mode: Literal["text"] = "text" # 交互模式
```
### 2. 响应格式 (ChatResponse)
```python
class OpenAIToolCall(BaseModel):
"""工具调用信息 (OpenAI 兼容格式)"""
id: str
type: Literal["function"] = "function"
function: Dict[str, Any] # {"name": "...", "arguments": {...}}
class ChatResponse(BaseModel):
"""流式响应模型"""
type: Literal[
"content", # AI 文本内容
"tool_call", # 工具调用请求
"tool_start", # 工具开始执行
"tool_end", # 工具执行完成
"error", # 错误信息
"done", # 流结束
"heartbeat" # 心跳保活
]
content: Optional[str] = None # 文本内容 (type=content)
message_id: Optional[str] = None # 消息 ID
tool_call: Optional[OpenAIToolCall] = None # 工具调用 (type=tool_call)
tool_name: Optional[str] = None # 工具名称 (type=tool_start/end)
tool_output: Optional[str] = None # 工具输出 (type=tool_end)
error: Optional[str] = None # 错误信息 (type=error)
session_id: Optional[str] = None # 会话 ID (type=heartbeat/done)
```
### 3. SSE 消息示例
#### 3.1 文本内容 (content)
```json
{"type": "content", "content": "Hello! How can I help you with your network today?"}
```
#### 3.2 工具调用 (tool_call)
```json
{
"type": "tool_call",
"tool_call": {
"id": "call_abc123",
"type": "function",
"function": {
"name": "GNS3TopologyTool",
"arguments": {"project_id": "550e8400-e29b-41d4-a716-446655440000"}
}
}
}
```
#### 3.3 工具开始执行 (tool_start)
```json
{"type": "tool_start", "tool_name": "GNS3TopologyTool", "session_id": "xxx"}
```
#### 3.4 工具执行完成 (tool_end)
```json
{
"type": "tool_end",
"tool_name": "GNS3TopologyTool",
"tool_output": "{\"nodes\": [{\"name\": \"Router1\", ...}], \"links\": [...]}"
}
```
#### 3.5 错误 (error)
```json
{"type": "error", "error": "Session not found", "session_id": "xxx"}
```
#### 3.6 完成 (done)
```json
{"type": "done", "session_id": "xxx"}
```
#### 3.7 心跳 (heartbeat)
```json
{"type": "heartbeat", "session_id": "xxx"}
```
**作用**: 保持 SSE 连接活跃,防止代理服务器/负载均衡器因超时断开连接。
**实现机制**:
```python
# 使用 asyncio.wait 实现超时检测
heartbeat_interval = 15.0 # 配置的心跳间隔(秒)
done, pending = await asyncio.wait(
[next_event_task],
timeout=heartbeat_interval
)
if done:
# 收到事件,正常处理
event = next_event_task.result()
# ...
else:
# 超时 - 发送心跳,保持连接
yield {"type": "heartbeat", "session_id": session_id}
# 继续等待下一个事件
```
**配置项** (可选):
```python
# 可通过配置控制
heartbeat_interval = 15.0 # 心跳间隔0 表示禁用
heartbeat_enabled = True # 是否启用
```
**前端处理**:
- 前端收到 `heartbeat` 类型消息时可以忽略
- 主要用于维持连接,不需要渲染任何内容
### 4. 前端处理逻辑
前端 (`useChat.ts`) 根据 `type` 字段进行不同处理:
| type | 处理逻辑 |
|------|----------|
| `content` | 追加到当前 AI 消息内容 |
| `tool_call` | 创建 tool_call 类型的消息,显示工具调用信息 |
| `tool_start` | 可选:显示工具开始执行的状态 |
| `tool_end` | 创建 tool_result 类型的消息,显示工具执行结果 |
| `error` | 显示错误信息 |
| `done` | 标记流结束 |
| `heartbeat` | 忽略(保活信号) |
### 5. 会话历史格式 (ConversationHistory)
```python
class OpenAIMessage(BaseModel):
"""消息模型 (用于历史记录)"""
id: str
role: Literal["user", "assistant", "system", "tool"]
content: str
name: Optional[str] = None # 工具消息的名称
tool_call_id: Optional[str] = None # 工具消息关联的 tool_call ID
tool_calls: Optional[List[OpenAIToolCall]] = None # 助手消息的工具调用
metadata: Dict[str, Any] = {}
created_at: str
class ConversationHistory(BaseModel):
"""会话历史模型"""
thread_id: str
title: str
messages: List[OpenAIMessage]
created_at: Optional[str] = None
updated_at: Optional[str] = None
llm_calls: int = 0
```
## 核心实现
### 1. Chat Schemas
**文件**: `gns3server/schemas/controller/chat.py`
```python
from pydantic import BaseModel
from typing import Optional, List, Dict, Any, Literal
class ChatRequest(BaseModel):
"""Chat 请求模型"""
message: str
session_id: Optional[str] = None
project_id: str
stream: bool = True
temperature: Optional[float] = None
mode: Literal["text"] = "text"
class ChatResponse(BaseModel):
"""Chat 流式响应模型"""
type: Literal["content", "tool_call", "tool_start", "tool_end", "error", "done", "heartbeat"]
content: Optional[str] = None
tool_call: Optional[Dict[str, Any]] = None
tool_name: Optional[str] = None
tool_output: Optional[str] = None
error: Optional[str] = None
session_id: Optional[str] = None
class ConversationHistory(BaseModel):
"""会话历史模型"""
thread_id: str
title: str
messages: List[Dict[str, Any]]
created_at: Optional[str] = None
updated_at: Optional[str] = None
class ChatSession(BaseModel):
"""会话模型"""
session_id: str
title: str
project_id: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
```
### 2. AgentService
**文件**: `gns3server/agent/gns3_copilot/agent_service.py`
```python
"""
GNS3 Copilot Agent Service
为每个项目提供独立的 Agent 实例,使用项目目录的 SQLite 作为 checkpoint异步版本
"""
import os
import uuid
import logging
from typing import AsyncGenerator, Dict, List, Any, Optional
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
import aiosqlite
from gns3_copilot.agent.gns3_copilot import agent_builder
log = logging.getLogger(__name__)
class AgentService:
"""项目级 Agent Service异步版本"""
def __init__(self, project_path: str, controller=None):
"""
Args:
project_path: GNS3 项目目录路径
controller: GNS3 Controller 实例(可选,用于获取项目信息)
"""
self.project_path = project_path
self.controller = controller
# 创建 checkpoint 目录
self.checkpoint_dir = os.path.join(project_path, ".gns3-copilot")
os.makedirs(self.checkpoint_dir, exist_ok=True)
self._checkpointer: Optional[AsyncSqliteSaver] = None
self._checkpointer_conn: Optional[aiosqlite.Connection] = None
self._project_checkpoint_path: Optional[str] = None
self._graph = None
self._model_with_tools = None
async def _get_checkpointer(self) -> AsyncSqliteSaver:
"""
获取或创建 SQLite checkpointer。
Returns:
AsyncSqliteSaver instance
"""
log.debug("Getting checkpointer for project at %s", self.project_path)
# Create checkpoint file in the project directory
checkpointer_path = os.path.join(self.checkpoint_dir, "copilot_checkpoints.db")
log.debug("Checkpoint path: %s", checkpointer_path)
# Store the path for reference
self._project_checkpoint_path = checkpointer_path
# Check if we already created a checkpointer for this project
if self._checkpointer and self._project_checkpoint_path == checkpointer_path:
log.debug("Reusing existing checkpointer")
return self._checkpointer
# Create new checkpointer using AsyncSqliteSaver
log.debug("Creating new async checkpointer at %s", checkpointer_path)
# Close existing connection if switching projects
if self._checkpointer_conn:
try:
await self._checkpointer_conn.close()
log.debug("Closed previous checkpointer connection")
except Exception as e:
log.warning("Error closing old checkpointer connection: %s", e)
# Create new connection
conn = await aiosqlite.connect(checkpointer_path)
# Enable WAL mode for better concurrent performance
await conn.execute("PRAGMA journal_mode=WAL;")
self._checkpointer_conn = conn # Save connection reference to prevent GC
self._checkpointer = AsyncSqliteSaver(conn)
# CRITICAL: Initialize database schema
await self._checkpointer.setup()
log.info("Project async checkpointer created and initialized at %s", checkpointer_path)
return self._checkpointer
def _get_model_with_tools(self):
"""
Get model with tools bound.
"""
if self._model_with_tools is None:
log.debug("Binding tools to model...")
from gns3server.agent.gns3_copilot.model_factory import create_base_model_with_tools
from gns3server.agent.gns3_copilot.tools import get_tools
model = self._create_model()
tools = get_tools()
self._model_with_tools = model.bind_tools(tools)
log.info("Model bound with %d tools", len(tools))
return self._model_with_tools
def _create_model(self):
"""创建基础模型"""
from gns3server.agent.gns3_copilot.model_factory import create_base_model
return create_base_model()
@property
async def checkpointer(self) -> AsyncSqliteSaver:
"""获取 SQLite checkpointer"""
if self._checkpointer is None:
return await self._get_checkpointer()
return self._checkpointer
@property
async def graph(self):
"""获取或编译 LangGraph"""
if self._graph is None:
checkpointer = await self.checkpointer
self._graph = agent_builder.compile(checkpointer=checkpointer)
return self._graph
async def close(self):
"""关闭连接"""
if self._checkpointer_conn:
try:
await self._checkpointer_conn.close()
log.debug("Checkpointer connection closed")
except Exception as e:
log.warning("Error closing checkpointer connection: %s", e)
finally:
self._checkpointer_conn = None
self._checkpointer = None
self._graph = None
self._model_with_tools = None
async def stream_chat(
self,
message: str,
session_id: str,
project_id: Optional[str] = None,
user_id: Optional[str] = None,
jwt_token: Optional[str] = None,
mode: str = "text"
) -> AsyncGenerator[Dict[str, Any], None]:
"""流式处理 chat 请求"""
# 构建 config包含用户认证信息
config = {
"configurable": {
"thread_id": session_id,
"user_id": user_id,
"jwt_token": jwt_token,
}
}
# 获取项目信息
project_info = None
if project_id:
try:
from gns3server.controller import Controller
controller = Controller.instance()
project = controller.projects.get(project_id)
if project:
project_info = (
project.name,
project.id,
len(project.nodes),
len(project.links),
project.status
)
except Exception:
pass
# 构建输入
inputs = {
"messages": [HumanMessage(content=message)],
"llm_calls": 0,
"remaining_steps": 20,
"mode": mode,
}
if project_info:
inputs["selected_project"] = project_info
# 流式处理
try:
async for event in self.graph.astream_events(inputs, config=config, version="v2"):
chunk = self._convert_event_to_chunk(event)
if chunk:
yield chunk
yield {"type": "done", "session_id": session_id}
except Exception as e:
yield {"type": "error", "error": str(e), "session_id": session_id}
def _convert_event_to_chunk(self, event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""将 LangGraph 事件转换为 API 响应块"""
event_type = event.get("event", "")
if event_type == "on_chat_model_stream":
content = event.get("data", {}).get("chunk", {}).get("content", "")
if content:
return {"type": "content", "content": content}
elif event_type == "on_tool_start":
return {
"type": "tool_start",
"tool_name": event.get("name", ""),
"session_id": self.session_id
}
elif event_type == "on_tool_end":
return {
"type": "tool_end",
"tool_name": event.get("name", ""),
"tool_output": event.get("data", {}).get("output", ""),
"session_id": self.session_id
}
return None
async def get_history(self, session_id: str, limit: int = 100) -> Dict[str, Any]:
"""获取会话历史"""
config = {"configurable": {"thread_id": session_id}}
try:
state = await self.graph.aget_state(config)
if state and "messages" in state.values:
messages = []
for msg in state.values["messages"][-limit:]:
messages.append({
"type": type(msg).__name__,
"content": msg.content if hasattr(msg, 'content') else str(msg)
})
title = state.values.get("conversation_title", "New Conversation")
return {
"thread_id": session_id,
"title": title,
"messages": messages
}
except Exception:
pass
return {
"thread_id": session_id,
"title": "New Conversation",
"messages": []
}
def close(self):
"""关闭连接"""
if self._checkpointer:
self._checkpointer.conn.close()
```
### 3. ProjectAgentManager
**文件**: `gns3server/agent/gns3_copilot/project_agent_manager.py`
```python
"""
Project Agent Manager
管理每个项目的 Agent Service 实例(单例模式)
"""
import os
from typing import Dict, Optional
from threading import Lock
from gns3server.agent.gns3_copilot.agent_service import AgentService
class ProjectAgentManager:
"""项目级 Agent 管理器"""
_instance = None
_lock = Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._agents: Dict[str, AgentService] = {}
return cls._instance
def get_agent(self, project_id: str, project_path: str) -> AgentService:
"""
获取或创建项目的 Agent Service
"""
key = project_id
with self._lock:
if key not in self._agents:
self._agents[key] = AgentService(project_path)
return self._agents[key]
def remove_agent(self, project_id: str):
"""移除项目的 Agent Service"""
key = project_id
with self._lock:
if key in self._agents:
self._agents[key].close()
del self._agents[key]
def close_all(self):
"""关闭所有 Agent"""
with self._lock:
for agent in self._agents.values():
agent.close()
self._agents.clear()
# 全局单例
_project_agent_manager: Optional[ProjectAgentManager] = None
def get_project_agent_manager() -> ProjectAgentManager:
"""获取项目 Agent 管理器"""
global _project_agent_manager
if _project_agent_manager is None:
_project_agent_manager = ProjectAgentManager()
return _project_agent_manager
```
### 4. Chat API Routes
**文件**: `gns3server/api/routes/controller/chat.py`
```python
"""
Chat API endpoints
"""
import json
import uuid
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends, status, Request
from fastapi.responses import StreamingResponse
from gns3server.schemas.controller.chat import (
ChatRequest, ChatResponse, ConversationHistory, ChatSession
)
from gns3server.agent.gns3_copilot.project_agent_manager import get_project_agent_manager
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerNotFoundError
from gns3server.api.routes.controller.dependencies.authentication import get_current_active_user
router = APIRouter()
@router.post("/stream")
async def stream_chat(
request: ChatRequest,
http_request: Request,
current_user = Depends(get_current_active_user)
):
"""流式 Chat API"""
# 验证项目
project_path = None
if request.project_id:
try:
controller = Controller.instance()
project = controller.projects.get(request.project_id)
if not project:
raise ControllerNotFoundError(f"Project '{request.project_id}' not found")
project_path = project.path
except ControllerNotFoundError:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid project: {e}")
if not project_path:
raise HTTPException(status_code=400, detail="project_id is required")
# 获取用户认证信息
user_id = str(current_user.user_id)
# 获取 JWT token (从 Authorization header)
auth_header = http_request.headers.get("Authorization", "")
jwt_token = auth_header.replace("Bearer ", "") if auth_header else None
# 获取 Agent Service
agent_manager = get_project_agent_manager()
agent_service = agent_manager.get_agent(request.project_id, project_path)
session_id = request.session_id or str(uuid.uuid4())
async def generate():
try:
async for chunk in agent_service.stream_chat(
message=request.message,
session_id=session_id,
project_id=request.project_id,
user_id=user_id,
jwt_token=jwt_token,
mode=request.mode
):
try:
validated = ChatResponse(**chunk)
yield f"data: {json.dumps(validated.model_dump(exclude_none=True), ensure_ascii=False)}\n\n"
except Exception:
pass
yield f"data: {json.dumps({'type': 'done', 'session_id': session_id})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no"
}
)
@router.get("/history/{session_id}")
async def get_history(
session_id: str,
project_id: str,
limit: int = 100,
current_user = Depends(get_current_active_user)
):
"""获取会话历史"""
controller = Controller.instance()
project = controller.projects.get(project_id)
if not project:
raise ControllerNotFoundError(f"Project '{project_id}' not found")
agent_manager = get_project_agent_manager()
agent_service = agent_manager.get_agent(project_id, project.path)
history = await agent_service.get_history(session_id, limit)
return history
```
### 5. 注册路由
**文件**: `gns3server/api/routes/controller/__init__.py`
添加 chat router 注册:
```python
from . import chat
# ... 其他 router ...
router.include_router(
chat.router,
prefix="/chat",
tags=["Chat"]
)
```
## API 端点
| 方法 | 端点 | 说明 |
|------|------|------|
| POST | `/v3/chat/stream` | 流式 Chat主要接口 |
| GET | `/v3/chat/history/{session_id}?project_id=xxx` | 获取会话历史 |
## SSE 消息格式
```json
// 内容块
{"type": "content", "content": "Hello!"}
// 工具开始
{"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": "Error message", "session_id": "xxx"}
```
## 项目生命周期集成
在项目打开/关闭时,需要管理 Agent Service 实例:
```python
# 项目打开时
project_agent_manager.get_agent(project_id, project.path)
# 项目关闭时
project_agent_manager.remove_agent(project_id)
```
可以监听项目事件或使用信号机制实现。
## 依赖项
确保以下包已安装:
- `langchain` >= 0.3.0
- `langgraph` >= 0.2.0
- `langchain-core`
- `sqlalchemy` (LangGraph 依赖)
## 参考资料
- [LangGraph Checkpoint Documentation](https://langchain-ai.github.io/langgraph/how-tos/checkpointers/)
- [FlowNet-Lab Chat API](file:///home/yueguobin/myCode/GNS3/FlowNet-Lab/backend/api/v1/chat.py)
- [FlowNet-Lab Agent Service](file:///home/yueguobin/myCode/GNS3/FlowNet-Lab/backend/core/agent.py)