feat: 修改数据库结构
This commit is contained in:
parent
ab9ac965c8
commit
234c74dbf0
351
assets/API.md
351
assets/API.md
|
|
@ -2,8 +2,14 @@
|
||||||
|
|
||||||
## 认证 `/api/auth`
|
## 认证 `/api/auth`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/auth/register` | 用户注册 |
|
||||||
|
| POST | `/api/auth/login` | 用户登录 |
|
||||||
|
| POST | `/api/auth/logout` | 用户登出 |
|
||||||
|
| GET | `/api/auth/me` | 获取当前用户信息 |
|
||||||
|
|
||||||
### POST /api/auth/register
|
### POST /api/auth/register
|
||||||
用户注册
|
|
||||||
|
|
||||||
**请求体:**
|
**请求体:**
|
||||||
```json
|
```json
|
||||||
|
|
@ -14,20 +20,7 @@
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "注册成功",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"username": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /api/auth/login
|
### POST /api/auth/login
|
||||||
用户登录
|
|
||||||
|
|
||||||
**请求体:**
|
**请求体:**
|
||||||
```json
|
```json
|
||||||
|
|
@ -37,88 +30,20 @@
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "登录成功",
|
|
||||||
"data": {
|
|
||||||
"access_token": "eyJ...",
|
|
||||||
"token_type": "bearer",
|
|
||||||
"user": {
|
|
||||||
"id": 1,
|
|
||||||
"username": "string",
|
|
||||||
"role": "user"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /api/auth/logout
|
|
||||||
用户登出
|
|
||||||
|
|
||||||
**请求头:** `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
**响应:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "登出成功"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GET /api/auth/me
|
|
||||||
获取当前用户信息
|
|
||||||
|
|
||||||
**请求头:** `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
**响应:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"username": "string",
|
|
||||||
"email": "user@example.com",
|
|
||||||
"role": "user",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 会话 `/api/conversations`
|
## 会话 `/api/conversations`
|
||||||
|
|
||||||
### GET /api/conversations/
|
| 方法 | 路径 | 说明 |
|
||||||
获取会话列表
|
|------|------|------|
|
||||||
|
| GET | `/api/conversations/` | 获取会话列表 |
|
||||||
|
| POST | `/api/conversations/` | 创建会话 |
|
||||||
|
| GET | `/api/conversations/{id}` | 获取会话详情 |
|
||||||
|
| PUT | `/api/conversations/{id}` | 更新会话 |
|
||||||
|
| DELETE | `/api/conversations/{id}` | 删除会话 |
|
||||||
|
|
||||||
**查询参数:**
|
### 创建会话请求体
|
||||||
- `project_id` (可选): 项目ID
|
|
||||||
- `page` (可选): 页码,默认1
|
|
||||||
- `page_size` (可选): 每页数量,默认20
|
|
||||||
|
|
||||||
**请求头:** `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
**响应:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"items": [...],
|
|
||||||
"total": 100,
|
|
||||||
"page": 1,
|
|
||||||
"page_size": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /api/conversations/
|
|
||||||
创建会话
|
|
||||||
|
|
||||||
**请求头:** `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
**请求体:**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"project_id": "string (可选)",
|
"project_id": "string (可选)",
|
||||||
|
|
@ -131,56 +56,19 @@
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": "会话创建成功",
|
|
||||||
"data": {
|
|
||||||
"id": "conv_xxx",
|
|
||||||
"user_id": 1,
|
|
||||||
"title": "新会话",
|
|
||||||
"model": "glm-5",
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GET /api/conversations/{id}
|
|
||||||
获取会话详情
|
|
||||||
|
|
||||||
**路径参数:**
|
|
||||||
- `id`: 会话ID
|
|
||||||
|
|
||||||
**请求头:** `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
### PUT /api/conversations/{id}
|
|
||||||
更新会话
|
|
||||||
|
|
||||||
### DELETE /api/conversations/{id}
|
|
||||||
删除会话
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 消息 `/api/messages`
|
## 消息 `/api/messages`
|
||||||
|
|
||||||
### GET /api/messages/{conversation_id}
|
| 方法 | 路径 | 说明 |
|
||||||
获取消息列表
|
|------|------|------|
|
||||||
|
| GET | `/api/messages/{conversation_id}` | 获取消息列表 |
|
||||||
|
| POST | `/api/messages/` | 发送消息(非流式) |
|
||||||
|
| POST | `/api/messages/stream` | 发送消息(流式 SSE) |
|
||||||
|
| DELETE | `/api/messages/{id}` | 删除消息 |
|
||||||
|
|
||||||
**路径参数:**
|
### 发送消息请求体
|
||||||
- `conversation_id`: 会话ID
|
|
||||||
|
|
||||||
**查询参数:**
|
|
||||||
- `limit` (可选): 返回数量,默认100
|
|
||||||
|
|
||||||
**请求头:** `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
### POST /api/messages/
|
|
||||||
发送消息(非流式)
|
|
||||||
|
|
||||||
**请求头:** `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
**请求体:**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"conversation_id": "conv_xxx",
|
"conversation_id": "conv_xxx",
|
||||||
|
|
@ -189,45 +77,122 @@
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应:**
|
### SSE 事件类型
|
||||||
|
|
||||||
|
| 事件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `text` | 文本增量 |
|
||||||
|
| `tool_call` | 工具调用 |
|
||||||
|
| `tool_result` | 工具结果 |
|
||||||
|
| `done` | 完成 |
|
||||||
|
| `error` | 错误 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 聊天室 `/api/chat-rooms`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/chat-rooms/` | 获取聊天室列表 |
|
||||||
|
| POST | `/api/chat-rooms/` | 创建聊天室 |
|
||||||
|
| GET | `/api/chat-rooms/{room_id}` | 获取聊天室详情 |
|
||||||
|
| PUT | `/api/chat-rooms/{room_id}` | 更新聊天室 |
|
||||||
|
| DELETE | `/api/chat-rooms/{room_id}` | 删除聊天室 |
|
||||||
|
| GET | `/api/chat-rooms/{room_id}/agents` | 获取聊天室中的 Agent 列表 |
|
||||||
|
| POST | `/api/chat-rooms/{room_id}/agents` | 添加 Agent 到聊天室 |
|
||||||
|
| DELETE | `/api/chat-rooms/{room_id}/agents/{agent_id}` | 从聊天室移除 Agent |
|
||||||
|
| GET | `/api/chat-rooms/{room_id}/messages` | 获取聊天室消息历史 |
|
||||||
|
| POST | `/api/chat-rooms/{room_id}/messages` | 发送消息到聊天室(SSE 流式响应) |
|
||||||
|
|
||||||
|
### 创建聊天室请求体
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"name": "聊天室名称",
|
||||||
"data": {
|
"description": "描述(可选)",
|
||||||
"user_message": {...},
|
"agent_ids": ["agent-1", "agent-2"]
|
||||||
"assistant_message": {...}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### POST /api/messages/stream
|
### 添加 Agent 请求体
|
||||||
发送消息(流式响应)
|
|
||||||
|
|
||||||
使用 Server-Sent Events (SSE) 返回流式响应。
|
```json
|
||||||
|
{
|
||||||
|
"agent_id": "agent-1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**事件类型:**
|
---
|
||||||
- `text`: 文本增量
|
|
||||||
- `tool_call`: 工具调用
|
|
||||||
- `tool_result`: 工具结果
|
|
||||||
- `done`: 完成
|
|
||||||
- `error`: 错误
|
|
||||||
|
|
||||||
### DELETE /api/messages/{id}
|
## WebSocket `/ws/chat-room/{room_id}`
|
||||||
删除消息
|
|
||||||
|
### 连接参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 | 示例 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `participant_type` | string | `user` 或 `agent` | `user` |
|
||||||
|
| `participant_id` | string | 用户 ID 或 Agent ID | `1` |
|
||||||
|
| `participant_name` | string | 显示名称 | `John` |
|
||||||
|
|
||||||
|
### 连接示例
|
||||||
|
|
||||||
|
**用户连接:**
|
||||||
|
```
|
||||||
|
ws://host/ws/chat-room/room-123?participant_type=user&participant_id=1&participant_name=John
|
||||||
|
```
|
||||||
|
|
||||||
|
**Agent 连接:**
|
||||||
|
```
|
||||||
|
ws://host/ws/chat-room/room-123?participant_type=agent&participant_id=agent-1&participant_name=Assistant
|
||||||
|
```
|
||||||
|
|
||||||
|
### 发送消息
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "send_message",
|
||||||
|
"content": "Hello everyone!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 接收事件
|
||||||
|
|
||||||
|
| 事件 | 说明 | 数据示例 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `connected` | 连接成功 | `{"room_id": "xxx", "type": "user"}` |
|
||||||
|
| `history` | 历史消息 | `{"messages": [...]}` |
|
||||||
|
| `agents` | Agent 列表 | `{"agents": [...]}` |
|
||||||
|
| `message` | 新消息 | 消息对象 |
|
||||||
|
| `typing` | 打字状态 | `{"agent_id": "xxx", "is_typing": true}` |
|
||||||
|
| `process_step` | 处理步骤 | 步骤对象 |
|
||||||
|
| `done` | 完成 | `{"message_id": "xxx", "token_count": 100}` |
|
||||||
|
| `error` | 错误 | `{"content": "error message"}` |
|
||||||
|
| `system` | 系统消息 | `{"content": "John joined", "type": "user_join"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent `/api/agents`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/agents/` | 获取 Agent 列表 |
|
||||||
|
| POST | `/api/agents/` | 创建 Agent |
|
||||||
|
| GET | `/api/agents/{id}` | 获取 Agent 详情 |
|
||||||
|
| PUT | `/api/agents/{id}` | 更新 Agent |
|
||||||
|
| DELETE | `/api/agents/{id}` | 删除 Agent |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 工具 `/api/tools`
|
## 工具 `/api/tools`
|
||||||
|
|
||||||
### GET /api/tools/
|
| 方法 | 路径 | 说明 |
|
||||||
获取可用工具列表
|
|------|------|------|
|
||||||
|
| GET | `/api/tools/` | 获取可用工具列表 |
|
||||||
|
| GET | `/api/tools/{name}` | 获取工具详情 |
|
||||||
|
| POST | `/api/tools/{name}/execute` | 手动执行工具 |
|
||||||
|
|
||||||
**查询参数:**
|
### 工具列表响应
|
||||||
- `category` (可选): 工具分类
|
|
||||||
|
|
||||||
**请求头:** `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
**响应:**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
|
|
@ -236,24 +201,78 @@
|
||||||
"categorized": {
|
"categorized": {
|
||||||
"crawler": [...],
|
"crawler": [...],
|
||||||
"code": [...],
|
"code": [...],
|
||||||
"data": [...],
|
"data": [...]
|
||||||
"weather": [...]
|
|
||||||
},
|
},
|
||||||
"total": 11
|
"total": 11
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### GET /api/tools/{name}
|
### 执行工具请求体
|
||||||
获取工具详情
|
|
||||||
|
|
||||||
### POST /api/tools/{name}/execute
|
|
||||||
手动执行工具
|
|
||||||
|
|
||||||
**请求体:**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"arg1": "value1",
|
"arg1": "value1",
|
||||||
"arg2": "value2"
|
"arg2": "value2"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLM 提供商 `/api/providers`
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/providers/` | 获取提供商列表 |
|
||||||
|
| POST | `/api/providers/` | 创建提供商 |
|
||||||
|
| GET | `/api/providers/{id}` | 获取提供商详情 |
|
||||||
|
| PUT | `/api/providers/{id}` | 更新提供商 |
|
||||||
|
| DELETE | `/api/providers/{id}` | 删除提供商 |
|
||||||
|
| POST | `/api/providers/{id}/test` | 测试提供商连接 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 健康检查
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/health` | 健康检查 |
|
||||||
|
| GET | `/` | 服务信息 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 通用响应格式
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "操作成功",
|
||||||
|
"data": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "错误信息",
|
||||||
|
"code": 404
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分页响应
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"items": [...],
|
||||||
|
"total": 100,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -17,35 +17,57 @@
|
||||||
|
|
||||||
```
|
```
|
||||||
luxx/
|
luxx/
|
||||||
├── __init__.py # FastAPI 应用工厂
|
├── __init__.py # FastAPI 应用工厂
|
||||||
├── config.py # 配置管理(YAML)
|
├── config.py # 配置管理(YAML)
|
||||||
├── database.py # 数据库连接
|
├── core/
|
||||||
├── models.py # ORM 模型
|
│ ├── __init__.py
|
||||||
├── routes/ # API 路由层
|
│ ├── config.py # 配置管理
|
||||||
│ ├── __init__.py # 路由聚合
|
│ └── database.py # 数据库连接
|
||||||
│ ├── auth.py # 认证 (登录/注册)
|
├── models/ # ORM 模型
|
||||||
│ ├── conversations.py # 会话管理 (CRUD)
|
│ ├── __init__.py
|
||||||
│ ├── messages.py # 消息处理 (流式/同步)
|
│ ├── user.py # User, LLMProvider, Project
|
||||||
│ ├── providers.py # LLM 提供商管理
|
│ ├── chat.py # Conversation, Message
|
||||||
│ └── tools.py # 工具管理
|
│ ├── room.py # ChatRoom, Agent, RoomParticipant, ChatRoomMessage
|
||||||
├── services/ # 服务层
|
│ └── participant.py # Participant (统一参与者抽象)
|
||||||
│ ├── chat.py # 聊天服务 (Agentic Loop)
|
├── services/ # 服务层
|
||||||
│ └── llm_client.py # LLM 客户端
|
│ ├── __init__.py
|
||||||
├── tools/ # 工具系统
|
│ ├── chat.py # 聊天服务 (Agentic Loop)
|
||||||
│ ├── core.py # 核心类 (ToolRegistry, ToolDefinition, ToolResult)
|
│ ├── room.py # 聊天室服务 (多 Agent 编排)
|
||||||
│ ├── factory.py # @tool 装饰器
|
│ ├── participant.py # 统一参与者服务
|
||||||
│ ├── executor.py # 工具执行器 (缓存/并行)
|
│ ├── room_ws.py # WebSocket 处理
|
||||||
│ ├── services.py # 工具服务层
|
│ ├── llm_service.py # LLM 服务
|
||||||
│ └── builtin/ # 内置工具
|
│ ├── message_service.py # 消息服务
|
||||||
│ ├── __init__.py # 工具注册入口
|
│ ├── stream_service.py # 流式响应服务
|
||||||
│ ├── code.py # 代码执行 (python_execute, python_eval)
|
│ ├── task.py # 任务服务
|
||||||
│ ├── crawler.py # 网页爬虫 (web_search, web_fetch, batch_fetch)
|
│ └── agent.py # Agent 管理
|
||||||
│ └── data.py # 数据处理 (process_data)
|
├── agents/ # Agent 系统
|
||||||
└── utils/ # 工具函数
|
│ ├── __init__.py
|
||||||
└── helpers.py # 密码哈希、ID生成、响应封装
|
│ ├── base.py # BaseAgent 抽象类
|
||||||
|
│ └── registry.py # Agent 注册表
|
||||||
|
├── api/ # API 路由层
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── auth.py # 认证 (登录/注册)
|
||||||
|
│ ├── conversations.py # 会话管理 (CRUD)
|
||||||
|
│ ├── messages.py # 消息处理
|
||||||
|
│ ├── providers.py # LLM 提供商管理
|
||||||
|
│ ├── tools.py # 工具管理
|
||||||
|
│ ├── agents.py # Agent 管理
|
||||||
|
│ └── rooms.py # 聊天室管理
|
||||||
|
├── tools/ # 工具系统
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── core.py # 核心类 (ToolRegistry, ToolDefinition, ToolResult)
|
||||||
|
│ ├── factory.py # @tool 装饰器
|
||||||
|
│ ├── executor.py # 工具执行器
|
||||||
|
│ └── builtin/ # 内置工具
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── code.py # 代码执行
|
||||||
|
│ ├── crawler.py # 网页爬虫
|
||||||
|
│ └── data.py # 数据处理
|
||||||
|
└── utils/ # 工具函数
|
||||||
|
└── helpers.py # 密码哈希、ID生成、响应封装
|
||||||
|
|
||||||
run.py # 应用入口文件
|
run.py # 应用入口文件
|
||||||
config.yaml # 配置文件
|
config.yaml # 配置文件
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心组件
|
## 核心组件
|
||||||
|
|
@ -55,144 +77,70 @@ FastAPI 应用入口,使用 lifespan 管理生命周期:
|
||||||
- 启动:初始化数据库、注册内置工具、创建默认管理员用户
|
- 启动:初始化数据库、注册内置工具、创建默认管理员用户
|
||||||
- 关闭:清理资源
|
- 关闭:清理资源
|
||||||
|
|
||||||
```python
|
### 2. 配置管理 (`core/config.py`)
|
||||||
# 默认管理员账号
|
|
||||||
username: admin
|
|
||||||
password: admin123
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 配置管理 (`config.py`)
|
|
||||||
使用 YAML 文件管理配置:
|
使用 YAML 文件管理配置:
|
||||||
- 配置文件:`config.yaml`
|
- 配置文件:`config.yaml`
|
||||||
- 环境变量替换:`${VAR_NAME}`
|
- 环境变量替换:`${VAR_NAME}`
|
||||||
- 单例模式全局访问
|
- 单例模式全局访问
|
||||||
- 默认值支持
|
|
||||||
|
|
||||||
```yaml
|
### 3. 数据库 (`core/database.py`)
|
||||||
# config.yaml 示例
|
|
||||||
app:
|
|
||||||
secret_key: ${APP_SECRET_KEY}
|
|
||||||
debug: true
|
|
||||||
|
|
||||||
database:
|
|
||||||
type: sqlite
|
|
||||||
url: sqlite:///./chat.db
|
|
||||||
|
|
||||||
llm:
|
|
||||||
provider: deepseek
|
|
||||||
api_key: ${DEEPSEEK_API_KEY}
|
|
||||||
api_url: https://api.deepseek.com/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 数据库 (`database.py`)
|
|
||||||
- SQLAlchemy 同步支持
|
- SQLAlchemy 同步支持
|
||||||
- SQLite 默认数据库
|
- SQLite 默认数据库
|
||||||
- 依赖注入获取会话
|
- 依赖注入获取会话
|
||||||
|
|
||||||
### 4. ORM 模型 (`models.py`)
|
### 4. ORM 模型
|
||||||
|
|
||||||
|
#### 统一参与者架构
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
erDiagram
|
classDiagram
|
||||||
USER {
|
class Participant {
|
||||||
int id PK
|
+str participant_id
|
||||||
string username UK
|
+str name
|
||||||
string email UK
|
+ParticipantType participant_type
|
||||||
string password_hash
|
+from_user(User) Participant
|
||||||
string role
|
+from_agent(...) Participant
|
||||||
boolean is_active
|
+from_participant_id(str) Participant
|
||||||
datetime created_at
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PROJECT {
|
class ParticipantType {
|
||||||
string id PK
|
<<enumeration>>
|
||||||
int user_id FK
|
USER = "user"
|
||||||
string name
|
AGENT = "agent"
|
||||||
text description
|
|
||||||
datetime created_at
|
|
||||||
datetime updated_at
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CONVERSATION {
|
class RoomParticipant {
|
||||||
string id PK
|
+str room_id
|
||||||
int user_id FK
|
+str agent_id
|
||||||
int provider_id FK "optional"
|
+int user_id
|
||||||
string project_id FK "optional"
|
+str role
|
||||||
string title
|
+participant_id property
|
||||||
string model
|
+participant_type property
|
||||||
text system_prompt
|
|
||||||
float temperature
|
|
||||||
int max_tokens
|
|
||||||
boolean thinking_enabled
|
|
||||||
datetime created_at
|
|
||||||
datetime updated_at
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MESSAGE {
|
class ChatRoomMessage {
|
||||||
string id PK
|
+str participant_id
|
||||||
string conversation_id FK
|
+sender_type property
|
||||||
string role
|
+sender_id property
|
||||||
longtext content "JSON 格式"
|
|
||||||
int token_count
|
|
||||||
text usage "JSON 格式"
|
|
||||||
datetime created_at
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LLM_PROVIDER {
|
Participant --> ParticipantType
|
||||||
int id PK
|
RoomParticipant --> Participant
|
||||||
int user_id FK
|
ChatRoomMessage --> Participant
|
||||||
string name
|
|
||||||
string provider_type
|
|
||||||
string base_url
|
|
||||||
string api_key
|
|
||||||
string default_model
|
|
||||||
int max_tokens
|
|
||||||
boolean is_default
|
|
||||||
boolean enabled
|
|
||||||
datetime created_at
|
|
||||||
datetime updated_at
|
|
||||||
}
|
|
||||||
|
|
||||||
USER ||--o{ PROJECT : "has"
|
|
||||||
USER ||--o{ CONVERSATION : "has"
|
|
||||||
USER ||--o{ LLM_PROVIDER : "configures"
|
|
||||||
PROJECT ||--o{ CONVERSATION : "contains"
|
|
||||||
LLM_PROVIDER ||--o{ CONVERSATION : "uses"
|
|
||||||
CONVERSATION ||--o{ MESSAGE : "has"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Message Content JSON 结构
|
### 5. 聊天室系统
|
||||||
|
|
||||||
`content` 字段统一使用 JSON 格式存储:
|
#### WebSocket 端点
|
||||||
|
- `/ws/chat-room/{room_id}?participant_type=user&participant_id=123&participant_name=John`
|
||||||
|
- `/ws/chat-room/{room_id}?participant_type=agent&participant_id=agent-1&participant_name=Assistant`
|
||||||
|
|
||||||
**User 消息:**
|
#### ParticipantService
|
||||||
|
统一处理用户和 Agent 的消息:
|
||||||
|
- `process_message()` - 处理来自任何参与者的消息
|
||||||
|
- `send_message()` - 主动发送消息
|
||||||
|
|
||||||
```json
|
### 6. 工具系统
|
||||||
{
|
|
||||||
"text": "用户输入的文本内容",
|
|
||||||
"attachments": [
|
|
||||||
{"name": "utils.py", "extension": "py", "content": "..."}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Assistant 消息:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"text": "AI 回复的文本内容",
|
|
||||||
"tool_calls": [...],
|
|
||||||
"steps": [
|
|
||||||
{"id": "step-0", "index": 0, "type": "thinking", "content": "..."},
|
|
||||||
{"id": "step-1", "index": 1, "type": "text", "content": "..."},
|
|
||||||
{"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_xxx", "name": "...", "arguments": "..."},
|
|
||||||
{"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_xxx", "name": "...", "content": "...", "success": true}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`steps` 字段是**渲染顺序的唯一数据源**,按 `index` 顺序排列。thinking、text、tool_call、tool_result 可以在多轮迭代中穿插出现。
|
|
||||||
|
|
||||||
### 5. 工具系统
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
|
|
@ -219,21 +167,13 @@ classDiagram
|
||||||
+register(tool) void
|
+register(tool) void
|
||||||
+get(name) ToolDefinition?
|
+get(name) ToolDefinition?
|
||||||
+list_all() List~dict~
|
+list_all() List~dict~
|
||||||
+list_by_category(category) List~dict~
|
|
||||||
+execute(name, arguments) dict
|
+execute(name, arguments) dict
|
||||||
+remove(name) bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToolExecutor {
|
class ToolExecutor {
|
||||||
+enable_cache: bool
|
+enable_cache: bool
|
||||||
+cache_ttl: int
|
|
||||||
+max_workers: int
|
|
||||||
+_cache: Dict
|
|
||||||
+_call_history: List
|
|
||||||
+process_tool_calls(tool_calls, context) list
|
+process_tool_calls(tool_calls, context) list
|
||||||
+process_tool_calls_parallel(tool_calls, context) list
|
+process_tool_calls_parallel(tool_calls, context) list
|
||||||
+clear_cache() void
|
|
||||||
+get_history(limit) List
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -250,8 +190,6 @@ classDiagram
|
||||||
|
|
||||||
#### 工具开发规范
|
#### 工具开发规范
|
||||||
|
|
||||||
所有工具必须遵循统一的开发规范,确保错误处理和返回格式一致。
|
|
||||||
|
|
||||||
**核心原则:装饰器自动处理一切,工具函数只写业务逻辑**
|
**核心原则:装饰器自动处理一切,工具函数只写业务逻辑**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
@ -279,7 +217,7 @@ def my_tool(arguments: dict):
|
||||||
2. 所有异常捕获和转换
|
2. 所有异常捕获和转换
|
||||||
3. 结果格式统一包装
|
3. 结果格式统一包装
|
||||||
|
|
||||||
**返回格式转换**
|
**返回格式转换:**
|
||||||
|
|
||||||
| 工具函数返回/抛出 | 装饰器转换为 |
|
| 工具函数返回/抛出 | 装饰器转换为 |
|
||||||
|-------------------|-------------|
|
|-------------------|-------------|
|
||||||
|
|
@ -287,73 +225,52 @@ def my_tool(arguments: dict):
|
||||||
| `raise ValueError("msg")` | `{"success": false, "data": null, "error": "ValueError: msg"}` |
|
| `raise ValueError("msg")` | `{"success": false, "data": null, "error": "ValueError: msg"}` |
|
||||||
| `raise Exception()` | `{"success": false, "data": null, "error": "..."}` |
|
| `raise Exception()` | `{"success": false, "data": null, "error": "..."}` |
|
||||||
|
|
||||||
**工具调用流程**
|
#### 工具调用流程
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
LLM 请求
|
flowchart TD
|
||||||
↓
|
A[LLM 请求] --> B{ToolRegistry.execute}
|
||||||
ToolRegistry.execute(name, args)
|
B --> C[@tool 装饰器]
|
||||||
↓
|
C --> D{验证 required_params}
|
||||||
@tool 装饰器
|
D -->|失败| E[返回 error]
|
||||||
├─ 验证 required_params
|
D -->|成功| F[执行工具函数]
|
||||||
├─ 执行工具函数 (try-except 包裹)
|
F --> G{try-except}
|
||||||
├─ 捕获异常 → 转换为 error
|
G -->|成功| H[包装 ToolResult]
|
||||||
├─ 包装返回格式
|
G -->|异常| I[捕获并转换 error]
|
||||||
└─ 返回 ToolResult
|
I --> H
|
||||||
↓
|
H --> J[返回 ToolResult]
|
||||||
ToolExecutor 返回结果
|
J --> K[前端 ProcessBlock 显示]
|
||||||
↓
|
|
||||||
前端 ProcessBlock 显示
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 服务层
|
#### Message Content JSON 结构
|
||||||
|
|
||||||
#### ChatService (`services/chat.py`)
|
`content` 字段统一使用 JSON 格式存储:
|
||||||
核心聊天服务:
|
|
||||||
- Agentic Loop 迭代执行(最多 10 轮)
|
|
||||||
- 流式 SSE 响应
|
|
||||||
- 工具调用编排(并行执行)
|
|
||||||
- 消息历史管理
|
|
||||||
- 自动重试机制
|
|
||||||
- 支持 thinking_content 提取
|
|
||||||
- Token 用量追踪
|
|
||||||
|
|
||||||
#### LLMClient (`services/llm_client.py`)
|
**User 消息:**
|
||||||
LLM API 客户端:
|
```json
|
||||||
- 多提供商:DeepSeek、GLM、OpenAI
|
{
|
||||||
- 流式/同步调用
|
"text": "用户输入的文本内容",
|
||||||
- 错误处理和重试
|
"attachments": [
|
||||||
- Token 计数
|
{"name": "utils.py", "extension": "py", "content": "..."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 7. 认证系统 (`routes/auth.py`)
|
**Assistant 消息:**
|
||||||
- JWT Bearer Token
|
```json
|
||||||
- Bcrypt 密码哈希
|
{
|
||||||
- 用户注册/登录
|
"text": "AI 回复的文本内容",
|
||||||
|
"tool_calls": [...],
|
||||||
|
"steps": [
|
||||||
|
{"id": "step-0", "index": 0, "type": "thinking", "content": "..."},
|
||||||
|
{"id": "step-1", "index": 1, "type": "text", "content": "..."},
|
||||||
|
{"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_xxx", "name": "...", "arguments": "..."},
|
||||||
|
{"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_xxx", "name": "...", "content": "...", "success": true}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 8. API 路由
|
**steps 字段是渲染顺序的唯一数据源**,按 `index` 顺序排列。
|
||||||
|
|
||||||
| 路由 | 方法 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `/auth/register` | POST | 用户注册 |
|
|
||||||
| `/auth/login` | POST | 用户登录 |
|
|
||||||
| `/conversations` | GET | 会话列表(分页) |
|
|
||||||
| `/conversations` | POST | 创建会话 |
|
|
||||||
| `/conversations/{id}` | GET | 会话详情 |
|
|
||||||
| `/conversations/{id}` | PUT | 更新会话 |
|
|
||||||
| `/conversations/{id}` | DELETE | 删除会话 |
|
|
||||||
| `/messages` | GET | 消息列表 |
|
|
||||||
| `/messages` | POST | 发送消息(同步) |
|
|
||||||
| `/messages/stream` | POST | 发送消息(流式 SSE) |
|
|
||||||
| `/messages/{id}` | DELETE | 删除消息 |
|
|
||||||
| `/providers` | GET | LLM 提供商列表 |
|
|
||||||
| `/providers` | POST | 创建提供商 |
|
|
||||||
| `/providers/{id}` | GET | 提供商详情 |
|
|
||||||
| `/providers/{id}` | PUT | 更新提供商 |
|
|
||||||
| `/providers/{id}` | DELETE | 删除提供商 |
|
|
||||||
| `/providers/{id}/test` | POST | 测试提供商连接 |
|
|
||||||
| `/tools` | GET | 可用工具列表 |
|
|
||||||
| `/health` | GET | 健康检查 |
|
|
||||||
| `/` | GET | 服务信息 |
|
|
||||||
|
|
||||||
## 数据流
|
## 数据流
|
||||||
|
|
||||||
|
|
@ -362,63 +279,43 @@ LLM API 客户端:
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Client
|
participant Client
|
||||||
participant API as POST /messages/stream
|
participant API as WebSocket/API
|
||||||
participant CS as ChatService
|
participant PS as ParticipantService
|
||||||
|
participant RS as ChatRoomService
|
||||||
participant LLM as LLM API
|
participant LLM as LLM API
|
||||||
participant TE as ToolExecutor
|
participant TE as ToolExecutor
|
||||||
|
|
||||||
Client->>API: POST {content, tools, thinking_enabled}
|
Client->>API: send_message(content)
|
||||||
API->>CS: stream_response()
|
API->>PS: process_message()
|
||||||
|
PS->>RS: save_message()
|
||||||
loop MAX_ITERATIONS (10)
|
PS->>RS: dispatch to agents
|
||||||
CS->>LLM: call(messages, tools)
|
|
||||||
LLM-->>CS: SSE Stream
|
|
||||||
|
|
||||||
|
loop Agentic Loop
|
||||||
|
RS->>LLM: call(messages, tools)
|
||||||
|
LLM-->>RS: SSE Stream
|
||||||
alt tool_calls
|
alt tool_calls
|
||||||
CS->>TE: process_tool_calls_parallel()
|
RS->>TE: process_tool_calls_parallel()
|
||||||
TE-->>CS: tool_results
|
TE-->>RS: tool_results
|
||||||
CS->>CS: 追加到 messages
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
CS->>CS: _save_message()
|
RS-->>PS: process_step events
|
||||||
CS->>API: SSE Stream
|
PS-->>Client: SSE Stream
|
||||||
API-->>Client: 流式响应
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## SSE 事件
|
## WebSocket 事件
|
||||||
|
|
||||||
| 事件 | 说明 |
|
| 事件 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `process_step` | 结构化步骤(thinking/text/tool_call/tool_result),携带 `id`、`index` 确保渲染顺序 |
|
| `connected` | 连接成功 |
|
||||||
| `done` | 响应完成,携带 message_id、token_count、usage |
|
| `history` | 历史消息 |
|
||||||
|
| `agents` | 聊天室 Agent 列表 |
|
||||||
|
| `message` | 新消息 |
|
||||||
|
| `typing` | 打字状态 |
|
||||||
|
| `process_step` | Agent 处理步骤 |
|
||||||
|
| `done` | 处理完成 |
|
||||||
| `error` | 错误信息 |
|
| `error` | 错误信息 |
|
||||||
|
| `system` | 系统消息 (join/leave) |
|
||||||
### process_step 事件格式
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"type": "process_step", "step": {"id": "step-0", "index": 0, "type": "thinking", "content": "..."}}
|
|
||||||
{"type": "process_step", "step": {"id": "step-1", "index": 1, "type": "text", "content": "回复文本..."}}
|
|
||||||
{"type": "process_step", "step": {"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_abc", "name": "web_search", "arguments": "{\"query\": \"...\"}"}}
|
|
||||||
{"type": "process_step", "step": {"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_abc", "name": "web_search", "content": "{...}", "success": true}}
|
|
||||||
```
|
|
||||||
|
|
||||||
| 字段 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `id` | 步骤唯一标识(格式 `step-{index}`) |
|
|
||||||
| `index` | 步骤序号,确保按正确顺序显示 |
|
|
||||||
| `type` | 步骤类型:`thinking` / `text` / `tool_call` / `tool_result` |
|
|
||||||
| `id_ref` | 工具调用引用 ID(仅 tool_call/tool_result) |
|
|
||||||
| `name` | 工具名称(仅 tool_call/tool_result) |
|
|
||||||
| `arguments` | 工具调用参数 JSON 字符串(仅 tool_call) |
|
|
||||||
| `content` | 内容(thinking 的思考内容、text 的文本、tool_result 的返回结果) |
|
|
||||||
| `success` | 工具执行是否成功(仅 tool_result) |
|
|
||||||
|
|
||||||
### done 事件格式
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"type": "done", "message_id": "uuid", "token_count": 1234, "usage": {"prompt_tokens": 100, "completion_tokens": 200, "total_tokens": 300}}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置示例
|
## 配置示例
|
||||||
|
|
||||||
|
|
@ -454,35 +351,3 @@ tools:
|
||||||
| `APP_SECRET_KEY` | 应用密钥 | `your-secret-key` |
|
| `APP_SECRET_KEY` | 应用密钥 | `your-secret-key` |
|
||||||
| `DEEPSEEK_API_KEY` | DeepSeek API | `sk-xxxx` |
|
| `DEEPSEEK_API_KEY` | DeepSeek API | `sk-xxxx` |
|
||||||
| `DATABASE_URL` | 数据库连接 | `sqlite:///./chat.db` |
|
| `DATABASE_URL` | 数据库连接 | `sqlite:///./chat.db` |
|
||||||
|
|
||||||
## 项目结构说明
|
|
||||||
|
|
||||||
### 入口文件
|
|
||||||
|
|
||||||
- `run.py` - 启动 Uvicorn 服务器
|
|
||||||
|
|
||||||
### 响应格式
|
|
||||||
|
|
||||||
所有 API 统一使用响应封装:
|
|
||||||
|
|
||||||
```json
|
|
||||||
// 成功
|
|
||||||
{"success": true, "data": {...}, "message": "操作成功"}
|
|
||||||
|
|
||||||
// 错误
|
|
||||||
{"success": false, "error": "错误信息", "code": 404}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 工具缓存机制
|
|
||||||
|
|
||||||
ToolExecutor 支持结果缓存:
|
|
||||||
- TTL: 5 分钟(可配置)
|
|
||||||
- 缓存 Key: `{tool_name}:{sorted_arguments_json}`
|
|
||||||
- 调用历史记录最近 1000 条
|
|
||||||
|
|
||||||
### 流式响应特点
|
|
||||||
|
|
||||||
1. 实时返回 thinking_content(模型思考过程)
|
|
||||||
2. 实时返回 text 增量更新
|
|
||||||
3. 工具调用串行执行,结果批量返回
|
|
||||||
4. 最终 `done` 事件包含完整 message_id 和 token 用量
|
|
||||||
|
|
|
||||||
|
|
@ -90,21 +90,13 @@ erDiagram
|
||||||
int owner_id FK
|
int owner_id FK
|
||||||
bool is_active
|
bool is_active
|
||||||
datetime created_at
|
datetime created_at
|
||||||
}
|
datetime updated_at
|
||||||
|
|
||||||
CHAT_ROOM_AGENT {
|
|
||||||
string id PK
|
|
||||||
string chat_room_id FK
|
|
||||||
string agent_id FK
|
|
||||||
bool is_active
|
|
||||||
datetime joined_at
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CHAT_ROOM_MESSAGE {
|
CHAT_ROOM_MESSAGE {
|
||||||
string id PK
|
string id PK
|
||||||
string room_id FK
|
string room_id FK
|
||||||
string sender_type
|
string participant_id
|
||||||
string sender_id
|
|
||||||
string sender_name
|
string sender_name
|
||||||
text content
|
text content
|
||||||
string mentions
|
string mentions
|
||||||
|
|
@ -122,8 +114,6 @@ erDiagram
|
||||||
LLM_PROVIDER ||--o{ AGENT : configures
|
LLM_PROVIDER ||--o{ AGENT : configures
|
||||||
CONVERSATION ||--o{ MESSAGE : contains
|
CONVERSATION ||--o{ MESSAGE : contains
|
||||||
CHAT_ROOM ||--o{ CHAT_ROOM_MESSAGE : contains
|
CHAT_ROOM ||--o{ CHAT_ROOM_MESSAGE : contains
|
||||||
CHAT_ROOM ||--o{ CHAT_ROOM_AGENT : has
|
|
||||||
AGENT ||--o{ CHAT_ROOM_AGENT : joins
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. 表设计
|
## 3. 表设计
|
||||||
|
|
@ -227,24 +217,13 @@ erDiagram
|
||||||
| is_active | BOOLEAN | DEFAULT 1 | 是否启用 |
|
| is_active | BOOLEAN | DEFAULT 1 | 是否启用 |
|
||||||
| created_at | DATETIME | - | 创建时间 |
|
| created_at | DATETIME | - | 创建时间 |
|
||||||
|
|
||||||
### chat_room_agents 聊天室成员表
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | VARCHAR(64) | PK | 关联 ID |
|
|
||||||
| chat_room_id | VARCHAR(64) | FK(chat_rooms.id) | 聊天室 ID |
|
|
||||||
| agent_id | VARCHAR(64) | FK(agents.id) | Agent ID |
|
|
||||||
| is_active | BOOLEAN | DEFAULT 1 | 是否启用 |
|
|
||||||
| joined_at | DATETIME | - | 加入时间 |
|
|
||||||
|
|
||||||
### chat_room_messages 聊天室消息表
|
### chat_room_messages 聊天室消息表
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| id | VARCHAR(64) | PK | 消息 ID |
|
| id | VARCHAR(64) | PK | 消息 ID |
|
||||||
| room_id | VARCHAR(64) | FK(chat_rooms.id) | 聊天室 ID |
|
| room_id | VARCHAR(64) | FK(chat_rooms.id) | 聊天室 ID |
|
||||||
| sender_type | VARCHAR(16) | NOT NULL | user/agent/system |
|
| participant_id | VARCHAR(64) | NOT NULL | 统一参与者 ID (格式: "user:123" 或 "agent:abc") |
|
||||||
| sender_id | VARCHAR(64) | NOT NULL | 发送者 ID |
|
|
||||||
| sender_name | VARCHAR(50) | NOT NULL | 发送者名称 |
|
| sender_name | VARCHAR(50) | NOT NULL | 发送者名称 |
|
||||||
| content | TEXT | NOT NULL | 消息内容 |
|
| content | TEXT | NOT NULL | 消息内容 |
|
||||||
| mentions | TEXT | - | @ 提及列表(JSON) |
|
| mentions | TEXT | - | @ 提及列表(JSON) |
|
||||||
|
|
@ -252,32 +231,41 @@ erDiagram
|
||||||
| token_count | INTEGER | DEFAULT 0 | Token 数 |
|
| token_count | INTEGER | DEFAULT 0 | Token 数 |
|
||||||
| created_at | DATETIME | - | 创建时间 |
|
| created_at | DATETIME | - | 创建时间 |
|
||||||
|
|
||||||
|
**participant_id 格式:**
|
||||||
|
- 用户: `user:{user_id}` (如 `user:123`)
|
||||||
|
- Agent: `agent:{agent_id}` (如 `agent:abc-001`)
|
||||||
|
|
||||||
## 4. 流程图
|
## 4. 流程图
|
||||||
|
|
||||||
### 消息流
|
### 消息流(统一参与者)
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant U as 用户
|
participant U as User
|
||||||
participant WS as WebSocket
|
participant WS as WebSocket
|
||||||
participant R as ChatRoomService
|
participant PS as ParticipantService
|
||||||
|
participant RS as ChatRoomService
|
||||||
participant A as Agent
|
participant A as Agent
|
||||||
participant LLM as LLM
|
participant LLM as LLM
|
||||||
|
|
||||||
U->>WS: 发送消息
|
U->>WS: 发送消息
|
||||||
WS->>R: dispatch
|
WS->>PS: process_message()
|
||||||
R->>A: 触发 Agent
|
PS->>RS: save_message()
|
||||||
|
RS->>RS: 创建 participant_id
|
||||||
|
RS-->>PS: 消息保存成功
|
||||||
|
PS->>A: 触发 Agent
|
||||||
A->>LLM: stream_call
|
A->>LLM: stream_call
|
||||||
LLM-->>A: SSE 流
|
LLM-->>A: SSE 流
|
||||||
A-->>WS: process_step
|
A-->>PS: process_step
|
||||||
WS-->>U: 实时推送
|
PS-->>WS: 实时推送
|
||||||
|
WS-->>U: 显示
|
||||||
```
|
```
|
||||||
|
|
||||||
### Agent 调度
|
### Agent 调度
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A[用户消息] --> B{有 @ 提及?}
|
A[用户/Agent 消息] --> B{有 @ 提及?}
|
||||||
|
|
||||||
B -->|是| C[触发指定 Agent]
|
B -->|是| C[触发指定 Agent]
|
||||||
B -->|否| D[触发 auto_response Agent]
|
B -->|否| D[触发 auto_response Agent]
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ async def lifespan(app: FastAPI):
|
||||||
# Import all models to ensure they are registered with Base
|
# Import all models to ensure they are registered with Base
|
||||||
from luxx.models.user import User, LLMProvider, Project
|
from luxx.models.user import User, LLMProvider, Project
|
||||||
from luxx.models.chat import Conversation, Message
|
from luxx.models.chat import Conversation, Message
|
||||||
from luxx.models.room import ChatRoom, Agent, ChatRoomAgent, ChatRoomMessage
|
from luxx.models.room import ChatRoom, Agent
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
# Create default test user if not exists
|
# Create default test user if not exists
|
||||||
|
|
@ -71,7 +71,25 @@ def create_app() -> FastAPI:
|
||||||
from luxx.services.room_ws import websocket_handler
|
from luxx.services.room_ws import websocket_handler
|
||||||
|
|
||||||
@app.websocket("/ws/chat-room/{room_id}")
|
@app.websocket("/ws/chat-room/{room_id}")
|
||||||
async def chat_room_websocket(websocket: WebSocket, room_id: str):
|
async def chat_room_websocket(
|
||||||
|
websocket: WebSocket,
|
||||||
|
room_id: str,
|
||||||
|
participant_id: str = None,
|
||||||
|
participant_type: str = "user",
|
||||||
|
participant_name: str = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
WebSocket endpoint for chat room participation.
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
- participant_id: ID of the user or agent connecting
|
||||||
|
- participant_type: "user" or "agent" (default: "user")
|
||||||
|
- participant_name: Display name for the participant
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- User: ws://host/ws/chat-room/{room_id}?participant_id=123&participant_type=user&participant_name=John
|
||||||
|
- Agent: ws://host/ws/chat-room/{room_id}?participant_id=agent-1&participant_type=agent&participant_name=Assistant
|
||||||
|
"""
|
||||||
await websocket_handler(websocket, room_id)
|
await websocket_handler(websocket, room_id)
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# Builtins package - user-defined agent templates can be placed here
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
"""Models package"""
|
"""Models package"""
|
||||||
from luxx.models.user import User, LLMProvider, Project
|
from luxx.models.user import User, LLMProvider, Project
|
||||||
from luxx.models.chat import Conversation, Message
|
from luxx.models.chat import Conversation, Message
|
||||||
from luxx.models.room import ChatRoom, Agent, ChatRoomAgent, ChatRoomMessage
|
from luxx.models.room import ChatRoom, Agent
|
||||||
|
from luxx.models.participant import Participant, ParticipantType
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"User", "LLMProvider", "Project",
|
"User", "LLMProvider", "Project",
|
||||||
"Conversation", "Message",
|
"Conversation", "Message",
|
||||||
"ChatRoom", "Agent", "ChatRoomAgent", "ChatRoomMessage"
|
"ChatRoom", "Agent",
|
||||||
|
"Participant", "ParticipantType",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Chat-related models"""
|
"""Chat-related models"""
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from sqlalchemy import String, Integer, Boolean, Float, Text, DateTime, ForeignKey
|
from sqlalchemy import String, Integer, Boolean, Float, Text, DateTime, ForeignKey
|
||||||
|
|
@ -18,17 +19,16 @@ class Conversation(Base):
|
||||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True)
|
provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True)
|
||||||
project_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("projects.id"), nullable=True)
|
project_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
||||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
model: Mapped[str] = mapped_column(String(64), nullable=False, default="deepseek-chat")
|
model: Mapped[str] = mapped_column(String(64), nullable=False, default="deepseek-chat")
|
||||||
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are a helpful assistant.")
|
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are helpful.")
|
||||||
temperature: Mapped[float] = mapped_column(Float, default=0.7)
|
temperature: Mapped[float] = mapped_column(Float, default=0.7)
|
||||||
max_tokens: Mapped[int] = mapped_column(Integer, default=2000)
|
max_tokens: Mapped[int] = mapped_column(Integer, default=2000)
|
||||||
thinking_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
thinking_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||||
|
|
||||||
# Relationships
|
|
||||||
user: Mapped["User"] = relationship("User", back_populates="conversations")
|
user: Mapped["User"] = relationship("User", back_populates="conversations")
|
||||||
provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider")
|
provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider")
|
||||||
messages: Mapped[List["Message"]] = relationship(
|
messages: Mapped[List["Message"]] = relationship(
|
||||||
|
|
@ -53,31 +53,43 @@ class Conversation(Base):
|
||||||
|
|
||||||
|
|
||||||
class Message(Base):
|
class Message(Base):
|
||||||
"""Message model.
|
"""Unified Message model for Conversation and ChatRoom.
|
||||||
|
|
||||||
content 字段统一使用 JSON 格式存储:
|
role: user/assistant/system/tool
|
||||||
|
content: JSON format with text, attachments, tool_calls, steps
|
||||||
"""
|
"""
|
||||||
__tablename__ = "messages"
|
__tablename__ = "messages"
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||||
conversation_id: Mapped[str] = mapped_column(String(64), ForeignKey("conversations.id"), nullable=False)
|
conversation_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("conversations.id"), nullable=True)
|
||||||
|
room_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
||||||
role: Mapped[str] = mapped_column(String(16), nullable=False)
|
role: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
content: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||||
|
sender_name: Mapped[str] = mapped_column(String(50), nullable=False, default="")
|
||||||
|
mentions: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
token_count: Mapped[int] = mapped_column(Integer, default=0)
|
token_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
usage: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
usage: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
|
conversation: Mapped[Optional["Conversation"]] = relationship("Conversation", back_populates="messages")
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||||
|
|
||||||
# Relationships
|
@property
|
||||||
conversation: Mapped["Conversation"] = relationship("Conversation", back_populates="messages")
|
def target_type(self) -> str:
|
||||||
|
return "conversation" if self.conversation_id else "room"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_id(self) -> str:
|
||||||
|
return self.conversation_id or self.room_id or ""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""Convert to dictionary, extracting process_steps for frontend"""
|
|
||||||
import json
|
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"conversation_id": self.conversation_id,
|
"conversation_id": self.conversation_id,
|
||||||
|
"room_id": self.room_id,
|
||||||
|
"target_type": self.target_type,
|
||||||
|
"target_id": self.target_id,
|
||||||
"role": self.role,
|
"role": self.role,
|
||||||
|
"sender_name": self.sender_name,
|
||||||
"token_count": self.token_count,
|
"token_count": self.token_count,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||||
}
|
}
|
||||||
|
|
@ -89,11 +101,19 @@ class Message(Base):
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
result["usage"] = None
|
result["usage"] = None
|
||||||
|
|
||||||
|
# Parse mentions JSON
|
||||||
|
if self.mentions:
|
||||||
|
try:
|
||||||
|
result["mentions"] = json.loads(self.mentions)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
result["mentions"] = []
|
||||||
|
else:
|
||||||
|
result["mentions"] = []
|
||||||
|
|
||||||
# Parse content JSON
|
# Parse content JSON
|
||||||
try:
|
try:
|
||||||
content_obj = json.loads(self.content) if self.content else {}
|
content_obj = json.loads(self.content) if self.content else {}
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
result["content"] = self.content
|
|
||||||
result["text"] = self.content
|
result["text"] = self.content
|
||||||
result["attachments"] = []
|
result["attachments"] = []
|
||||||
result["tool_calls"] = []
|
result["tool_calls"] = []
|
||||||
|
|
@ -105,7 +125,7 @@ class Message(Base):
|
||||||
result["tool_calls"] = content_obj.get("tool_calls", [])
|
result["tool_calls"] = content_obj.get("tool_calls", [])
|
||||||
result["process_steps"] = content_obj.get("steps", [])
|
result["process_steps"] = content_obj.get("steps", [])
|
||||||
|
|
||||||
if "content" not in result:
|
if "content" not in content_obj:
|
||||||
result["content"] = result["text"]
|
result["content"] = result["text"]
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
"""Participant model - unified participant for users and agents."""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Dict, Any, TYPE_CHECKING
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from luxx.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantType(Enum):
|
||||||
|
USER = "user"
|
||||||
|
AGENT = "agent"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Participant:
|
||||||
|
"""Unified participant abstraction for users and agents."""
|
||||||
|
participant_id: str
|
||||||
|
name: str
|
||||||
|
participant_type: ParticipantType
|
||||||
|
avatar: Optional[str] = None
|
||||||
|
role: Optional[str] = None # ChatRoom role: owner/admin/member
|
||||||
|
permission_level: int = 1
|
||||||
|
is_active: bool = True
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_agent(self) -> bool:
|
||||||
|
return self.participant_type == ParticipantType.AGENT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_user(self) -> bool:
|
||||||
|
return self.participant_type == ParticipantType.USER
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": self.participant_id,
|
||||||
|
"name": self.name,
|
||||||
|
"type": self.participant_type.value,
|
||||||
|
"avatar": self.avatar,
|
||||||
|
"role": self.role,
|
||||||
|
"permission_level": self.permission_level,
|
||||||
|
"is_active": self.is_active,
|
||||||
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_user(cls, user: "User") -> "Participant":
|
||||||
|
return cls(
|
||||||
|
participant_id=f"user:{user.id}",
|
||||||
|
name=user.username,
|
||||||
|
participant_type=ParticipantType.USER,
|
||||||
|
permission_level=user.permission_level,
|
||||||
|
is_active=user.is_active,
|
||||||
|
created_at=user.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_agent(
|
||||||
|
cls, agent_id: str, name: str, role: str = None,
|
||||||
|
avatar: str = None, permission_level: int = 1, is_active: bool = True
|
||||||
|
) -> "Participant":
|
||||||
|
return cls(
|
||||||
|
participant_id=f"agent:{agent_id}",
|
||||||
|
name=name,
|
||||||
|
participant_type=ParticipantType.AGENT,
|
||||||
|
role=role,
|
||||||
|
avatar=avatar,
|
||||||
|
permission_level=permission_level,
|
||||||
|
is_active=is_active
|
||||||
|
)
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
"""ChatRoom models"""
|
"""ChatRoom models - unified participant architecture"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List
|
from typing import Optional, List, TYPE_CHECKING
|
||||||
from sqlalchemy import String, Integer, Boolean, Text, DateTime, ForeignKey
|
from sqlalchemy import String, Integer, Boolean, Text, DateTime, ForeignKey
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from luxx.core.database import Base
|
from luxx.core.database import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from luxx.models.user import User
|
||||||
|
|
||||||
|
|
||||||
def local_now():
|
def local_now():
|
||||||
return datetime.now()
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
class ChatRoom(Base):
|
class ChatRoom(Base):
|
||||||
"""Chat Room model - like a group chat for multiple agents"""
|
"""Chat Room - group chat for multiple participants"""
|
||||||
__tablename__ = "chat_rooms"
|
__tablename__ = "chat_rooms"
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||||
|
|
@ -23,39 +26,26 @@ class ChatRoom(Base):
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||||
|
|
||||||
# Relationships
|
|
||||||
owner: Mapped["User"] = relationship("User", backref="chat_rooms")
|
owner: Mapped["User"] = relationship("User", backref="chat_rooms")
|
||||||
agents: Mapped[List["ChatRoomAgent"]] = relationship(
|
|
||||||
"ChatRoomAgent", back_populates="chat_room", cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
messages: Mapped[List["ChatRoomMessage"]] = relationship(
|
|
||||||
"ChatRoomMessage", back_populates="chat_room", cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self, include_agents: bool = False):
|
def to_dict(self):
|
||||||
result = {
|
return {
|
||||||
"id": self.id,
|
"id": self.id, "name": self.name, "description": self.description,
|
||||||
"name": self.name,
|
"owner_id": self.owner_id, "is_active": self.is_active,
|
||||||
"description": self.description,
|
|
||||||
"owner_id": self.owner_id,
|
|
||||||
"is_active": self.is_active,
|
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||||
}
|
}
|
||||||
if include_agents and self.agents:
|
|
||||||
result["agents"] = [ca.to_dict() for ca in self.agents]
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class Agent(Base):
|
class Agent(Base):
|
||||||
"""Agent model - defines an AI agent with specific role"""
|
"""Agent model - defines an AI agent"""
|
||||||
__tablename__ = "agents"
|
__tablename__ = "agents"
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
role: Mapped[str] = mapped_column(String(50), nullable=False, default="helper")
|
role: Mapped[str] = mapped_column(String(50), nullable=False, default="helper")
|
||||||
avatar: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
avatar: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||||
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are a helpful assistant.")
|
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are helpful.")
|
||||||
provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True)
|
provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True)
|
||||||
model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||||
tools: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
tools: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
|
|
@ -68,105 +58,24 @@ class Agent(Base):
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||||
|
|
||||||
# Relationships
|
|
||||||
provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider")
|
provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider")
|
||||||
chat_rooms: Mapped[List["ChatRoomAgent"]] = relationship(
|
|
||||||
"ChatRoomAgent", back_populates="agent", cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self, include_secrets: bool = False):
|
def to_dict(self, include_secrets: bool = False):
|
||||||
import json
|
import json
|
||||||
result = {
|
result = {
|
||||||
"id": self.id,
|
"id": self.id, "name": self.name, "role": self.role, "avatar": self.avatar,
|
||||||
"name": self.name,
|
"system_prompt": self.system_prompt, "provider_id": self.provider_id, "model": self.model,
|
||||||
"role": self.role,
|
"is_active": self.is_active, "priority": self.priority,
|
||||||
"avatar": self.avatar,
|
"auto_response": self.auto_response, "mention_trigger": self.mention_trigger,
|
||||||
"system_prompt": self.system_prompt,
|
|
||||||
"provider_id": self.provider_id,
|
|
||||||
"model": self.model,
|
|
||||||
"is_active": self.is_active,
|
|
||||||
"priority": self.priority,
|
|
||||||
"auto_response": self.auto_response,
|
|
||||||
"mention_trigger": self.mention_trigger,
|
|
||||||
"temperature": float(self.temperature) if self.temperature else 0.7,
|
"temperature": float(self.temperature) if self.temperature else 0.7,
|
||||||
"max_tokens": self.max_tokens,
|
"max_tokens": self.max_tokens,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||||
}
|
}
|
||||||
if self.tools:
|
try:
|
||||||
try:
|
result["tools"] = json.loads(self.tools) if self.tools else []
|
||||||
result["tools"] = json.loads(self.tools)
|
except json.JSONDecodeError:
|
||||||
except json.JSONDecodeError:
|
|
||||||
result["tools"] = []
|
|
||||||
else:
|
|
||||||
result["tools"] = []
|
result["tools"] = []
|
||||||
|
|
||||||
if include_secrets and self.provider:
|
if include_secrets and self.provider:
|
||||||
result["provider"] = self.provider.to_dict(include_key=True)
|
result["provider"] = self.provider.to_dict(include_key=True)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ChatRoomAgent(Base):
|
|
||||||
"""Association table for ChatRoom and Agent"""
|
|
||||||
__tablename__ = "chat_room_agents"
|
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
||||||
chat_room_id: Mapped[str] = mapped_column(String(64), ForeignKey("chat_rooms.id"), nullable=False)
|
|
||||||
agent_id: Mapped[str] = mapped_column(String(64), ForeignKey("agents.id"), nullable=False)
|
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
||||||
joined_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
chat_room: Mapped["ChatRoom"] = relationship("ChatRoom", back_populates="agents")
|
|
||||||
agent: Mapped["Agent"] = relationship("Agent", back_populates="chat_rooms")
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
"id": self.id,
|
|
||||||
"chat_room_id": self.chat_room_id,
|
|
||||||
"agent_id": self.agent_id,
|
|
||||||
"is_active": self.is_active,
|
|
||||||
"joined_at": self.joined_at.isoformat() if self.joined_at else None,
|
|
||||||
"agent": self.agent.to_dict() if self.agent else None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ChatRoomMessage(Base):
|
|
||||||
"""Chat Room Message model"""
|
|
||||||
__tablename__ = "chat_room_messages"
|
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
||||||
room_id: Mapped[str] = mapped_column(String(64), ForeignKey("chat_rooms.id"), nullable=False)
|
|
||||||
sender_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
||||||
sender_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
sender_name: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
|
||||||
mentions: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
||||||
parent_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
|
||||||
token_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
|
||||||
|
|
||||||
# Relationships
|
|
||||||
chat_room: Mapped["ChatRoom"] = relationship("ChatRoom", back_populates="messages")
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
import json
|
|
||||||
result = {
|
|
||||||
"id": self.id,
|
|
||||||
"room_id": self.room_id,
|
|
||||||
"sender_type": self.sender_type,
|
|
||||||
"sender_id": self.sender_id,
|
|
||||||
"sender_name": self.sender_name,
|
|
||||||
"content": self.content,
|
|
||||||
"parent_id": self.parent_id,
|
|
||||||
"token_count": self.token_count,
|
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
|
||||||
}
|
|
||||||
if self.mentions:
|
|
||||||
try:
|
|
||||||
result["mentions"] = json.loads(self.mentions)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
result["mentions"] = []
|
|
||||||
else:
|
|
||||||
result["mentions"] = []
|
|
||||||
return result
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
"""User-related models"""
|
"""User-related models"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List
|
from typing import Optional, List, TYPE_CHECKING
|
||||||
from sqlalchemy import String, Integer, Boolean, Text, DateTime, ForeignKey
|
from sqlalchemy import String, Integer, Boolean, Text, DateTime, ForeignKey
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from luxx.core.database import Base
|
from luxx.core.database import Base
|
||||||
|
|
||||||
|
# Avoid circular import at runtime
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from luxx.models.chat import Conversation
|
||||||
|
|
||||||
|
|
||||||
def local_now():
|
def local_now():
|
||||||
return datetime.now()
|
return datetime.now()
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,14 @@ from luxx.services.agent import agent_manager
|
||||||
from luxx.services.llm_service import llm_service, LLMService
|
from luxx.services.llm_service import llm_service, LLMService
|
||||||
from luxx.services.message_service import message_service, MessageService
|
from luxx.services.message_service import message_service, MessageService
|
||||||
from luxx.services.stream_service import stream_service, StreamService
|
from luxx.services.stream_service import stream_service, StreamService
|
||||||
|
from luxx.services.participant import participant_service, ParticipantService
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"chat_service",
|
"chat_service",
|
||||||
"chat_room_service",
|
"chat_room_service",
|
||||||
"agent_manager",
|
"agent_manager",
|
||||||
|
"participant_service",
|
||||||
|
"ParticipantService",
|
||||||
"ChatService",
|
"ChatService",
|
||||||
"llm_service",
|
"llm_service",
|
||||||
"LLMService",
|
"LLMService",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
"""Participant Service - unified service for users and agents in chat rooms."""
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any, Optional, AsyncGenerator
|
||||||
|
|
||||||
|
from luxx.agents.base import BaseAgent
|
||||||
|
from luxx.agents.registry import agent_registry
|
||||||
|
from luxx.models.participant import Participant
|
||||||
|
from luxx.services.room import chat_room_service
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantService:
|
||||||
|
"""Unified service for managing participants in chat rooms."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._active_agents: Dict[str, BaseAgent] = {}
|
||||||
|
self._active_users: Dict[int, Any] = {}
|
||||||
|
|
||||||
|
def _cm(self):
|
||||||
|
"""Lazy import connection manager."""
|
||||||
|
from luxx.services.room_ws import connection_manager
|
||||||
|
return connection_manager
|
||||||
|
|
||||||
|
# ==================== Agent ====================
|
||||||
|
|
||||||
|
def register_agent(self, agent: BaseAgent) -> Participant:
|
||||||
|
self._active_agents[agent.agent_id] = agent
|
||||||
|
agent_registry.register(agent)
|
||||||
|
return Participant.from_agent(
|
||||||
|
agent.agent_id, agent.name, agent.role, agent.avatar,
|
||||||
|
agent.auto_response, agent.mention_trigger, agent.priority
|
||||||
|
)
|
||||||
|
|
||||||
|
def unregister_agent(self, agent_id: str) -> bool:
|
||||||
|
if agent_id in self._active_agents:
|
||||||
|
del self._active_agents[agent_id]
|
||||||
|
agent_registry.unregister(agent_id)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_agent_participant(self, agent_id: str) -> Optional[Participant]:
|
||||||
|
agent = self._active_agents.get(agent_id) or chat_room_service.get_agent(agent_id)
|
||||||
|
if agent:
|
||||||
|
if agent_id not in self._active_agents:
|
||||||
|
self._active_agents[agent_id] = agent
|
||||||
|
return Participant.from_agent(
|
||||||
|
agent.agent_id, agent.name, agent.role, agent.avatar,
|
||||||
|
agent.auto_response, agent.mention_trigger, agent.priority
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ==================== User ====================
|
||||||
|
|
||||||
|
def register_user(self, user) -> Participant:
|
||||||
|
self._active_users[user.id] = user
|
||||||
|
return Participant.from_user(user)
|
||||||
|
|
||||||
|
def get_user_participant(self, user_id: int) -> Optional[Participant]:
|
||||||
|
user = self._active_users.get(user_id)
|
||||||
|
if not user:
|
||||||
|
from luxx.core.database import SessionLocal
|
||||||
|
from luxx.models.user import User
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if user:
|
||||||
|
self._active_users[user_id] = user
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
return Participant.from_user(user) if user else None
|
||||||
|
|
||||||
|
# ==================== Unified ====================
|
||||||
|
|
||||||
|
def get_participant(self, participant_id: str, ptype: str = "user") -> Optional[Participant]:
|
||||||
|
if ptype == "agent":
|
||||||
|
return self.get_agent_participant(participant_id)
|
||||||
|
try:
|
||||||
|
return self.get_user_participant(int(participant_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ==================== Messages ====================
|
||||||
|
|
||||||
|
async def process_message(
|
||||||
|
self, room_id: str, content: str, sender_id: str,
|
||||||
|
sender_name: str, sender_type: str = "user", context: Dict = None
|
||||||
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||||
|
cm = self._cm()
|
||||||
|
|
||||||
|
msg = chat_room_service.save_message(room_id, sender_type, sender_id, sender_name, content)
|
||||||
|
await cm.broadcast_to_room(room_id, {"event": "message", "data": msg})
|
||||||
|
|
||||||
|
room_agents = chat_room_service.get_room_agents(room_id)
|
||||||
|
if sender_type == "agent":
|
||||||
|
room_agents = [a for a in room_agents if a.agent_id != sender_id]
|
||||||
|
|
||||||
|
for agent in room_agents:
|
||||||
|
await cm.broadcast_to_room(room_id, {
|
||||||
|
"event": "typing",
|
||||||
|
"data": {"agent_id": agent.agent_id, "agent_name": agent.name, "is_typing": True}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx = (context or {})
|
||||||
|
ctx.update({"sender_type": sender_type, "sender_id": sender_id, "username": sender_name})
|
||||||
|
|
||||||
|
async for event in chat_room_service.process_message(room_id, content, sender_id, sender_name, ctx):
|
||||||
|
yield event
|
||||||
|
|
||||||
|
for agent in room_agents:
|
||||||
|
await cm.broadcast_to_room(room_id, {
|
||||||
|
"event": "typing",
|
||||||
|
"data": {"agent_id": agent.agent_id, "agent_name": agent.name, "is_typing": False}
|
||||||
|
})
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
self, room_id: str, participant_id: str,
|
||||||
|
participant_type: str, participant_name: str, content: str
|
||||||
|
):
|
||||||
|
cm = self._cm()
|
||||||
|
msg = chat_room_service.save_message(
|
||||||
|
room_id, participant_type, participant_id, participant_name, content
|
||||||
|
)
|
||||||
|
await cm.broadcast_to_room(room_id, {"event": "message", "data": msg})
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
participant_service = ParticipantService()
|
||||||
|
|
@ -1,177 +1,19 @@
|
||||||
"""Chat Room Service - orchestrates multi-agent chat"""
|
"""Chat Room Service - orchestrates multi-agent chat"""
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
import uuid
|
import uuid
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Any, Optional, AsyncGenerator
|
from typing import List, Dict, Any, Optional, AsyncGenerator
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from luxx.core.database import SessionLocal
|
from luxx.core.database import SessionLocal
|
||||||
from luxx.models.room import ChatRoom, Agent, ChatRoomAgent, ChatRoomMessage
|
from luxx.models.room import ChatRoom, Agent
|
||||||
|
from luxx.models.chat import Message
|
||||||
from luxx.agents.base import BaseAgent
|
from luxx.agents.base import BaseAgent
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# ==================== Dispatcher ====================
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DispatchResult:
|
|
||||||
"""Result of message dispatch"""
|
|
||||||
triggered_agents: List[BaseAgent]
|
|
||||||
mentions: List[str]
|
|
||||||
should_respond: bool
|
|
||||||
|
|
||||||
|
|
||||||
class MessageDispatcher:
|
|
||||||
"""Dispatcher for routing messages to agents"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_mentions(content: str) -> List[str]:
|
|
||||||
"""Parse @mentions from message content"""
|
|
||||||
pattern = r'@(\w+)'
|
|
||||||
return re.findall(pattern, content)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_agents_by_names(names: List[str], room_agents: List[BaseAgent]) -> List[BaseAgent]:
|
|
||||||
"""Get agents by their names (case-insensitive)"""
|
|
||||||
name_lower_map = {a.name.lower(): a for a in room_agents}
|
|
||||||
matched = []
|
|
||||||
for name in names:
|
|
||||||
agent = name_lower_map.get(name.lower())
|
|
||||||
if agent:
|
|
||||||
matched.append(agent)
|
|
||||||
return matched
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_agents_by_ids(agent_ids: List[str], room_agents: List[BaseAgent]) -> List[BaseAgent]:
|
|
||||||
"""Get agents by their IDs"""
|
|
||||||
id_set = set(agent_ids)
|
|
||||||
return [a for a in room_agents if a.agent_id in id_set]
|
|
||||||
|
|
||||||
def dispatch(self, content: str, room_agents: List[BaseAgent], sender_id: str, sender_type: str = "user") -> DispatchResult:
|
|
||||||
"""Dispatch a message to appropriate agents."""
|
|
||||||
available_agents = [a for a in room_agents if a.agent_id != sender_id]
|
|
||||||
mentions = self.parse_mentions(content)
|
|
||||||
|
|
||||||
if mentions:
|
|
||||||
triggered = self.get_agents_by_names(mentions, available_agents)
|
|
||||||
logger.info(f"Message with mentions: {mentions} -> triggered: {[a.name for a in triggered]}")
|
|
||||||
return DispatchResult(triggered_agents=triggered, mentions=mentions, should_respond=len(triggered) > 0)
|
|
||||||
|
|
||||||
auto_agents = [a for a in available_agents if a.auto_response]
|
|
||||||
auto_agents.sort(key=lambda a: a.priority)
|
|
||||||
logger.info(f"Auto-response agents triggered: {[a.name for a in auto_agents]}")
|
|
||||||
return DispatchResult(triggered_agents=auto_agents, mentions=[], should_respond=len(auto_agents) > 0)
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Aggregator ====================
|
|
||||||
|
|
||||||
class ResponseAggregator:
|
|
||||||
"""Aggregates responses from multiple agents"""
|
|
||||||
|
|
||||||
def __init__(self, room_id: str):
|
|
||||||
self.room_id = room_id
|
|
||||||
self._agent_responses: Dict[str, Dict[str, Any]] = {}
|
|
||||||
|
|
||||||
async def aggregate_stream(self, agent_streams: Dict[str, AsyncGenerator]) -> AsyncGenerator[Dict[str, Any], None]:
|
|
||||||
"""Aggregate streaming responses from multiple agents."""
|
|
||||||
if not agent_streams:
|
|
||||||
return
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
def parse_sse(event_str: str) -> Dict[str, Any]:
|
|
||||||
"""Parse SSE string to dict."""
|
|
||||||
lines = event_str.strip().split('\n')
|
|
||||||
result = {"event": None, "data": {}}
|
|
||||||
for line in lines:
|
|
||||||
if line.startswith('event: '):
|
|
||||||
result["event"] = line[7:].strip()
|
|
||||||
elif line.startswith('data: '):
|
|
||||||
try:
|
|
||||||
result["data"] = json.loads(line[6:].strip())
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
result["data"] = {"content": line[6:].strip()}
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def collect_agent_stream(agent_id: str, stream):
|
|
||||||
"""Collect all events from a single agent stream."""
|
|
||||||
try:
|
|
||||||
async for event in stream:
|
|
||||||
# Event is SSE string from BaseAgent
|
|
||||||
parsed = parse_sse(event)
|
|
||||||
parsed["agent_id"] = agent_id
|
|
||||||
yield parsed
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Agent {agent_id} stream error: {e}")
|
|
||||||
yield {"event": "error", "agent_id": agent_id, "data": {"content": str(e)}}
|
|
||||||
|
|
||||||
# Use a queue-based approach for merging
|
|
||||||
queue = asyncio.Queue()
|
|
||||||
|
|
||||||
async def producer(agent_id: str, stream):
|
|
||||||
try:
|
|
||||||
async for event in stream:
|
|
||||||
# Parse SSE string to dict if needed
|
|
||||||
if isinstance(event, str):
|
|
||||||
parsed = parse_sse(event)
|
|
||||||
parsed["agent_id"] = agent_id
|
|
||||||
await queue.put((agent_id, parsed))
|
|
||||||
else:
|
|
||||||
# Already a dict, just add agent_id
|
|
||||||
if isinstance(event, dict):
|
|
||||||
event["agent_id"] = agent_id
|
|
||||||
await queue.put((agent_id, event))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Agent {agent_id} stream error: {e}")
|
|
||||||
await queue.put((agent_id, {"event": "error", "agent_id": agent_id, "data": {"content": str(e)}}))
|
|
||||||
finally:
|
|
||||||
await queue.put((agent_id, None)) # Signal done
|
|
||||||
|
|
||||||
# Start all producers
|
|
||||||
producers = [
|
|
||||||
asyncio.create_task(producer(agent_id, stream))
|
|
||||||
for agent_id, stream in agent_streams.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
active = len(producers)
|
|
||||||
while active > 0:
|
|
||||||
agent_id, event = await queue.get()
|
|
||||||
if event is None:
|
|
||||||
active -= 1
|
|
||||||
else:
|
|
||||||
yield event
|
|
||||||
|
|
||||||
# Wait for all producers to complete
|
|
||||||
await asyncio.gather(*producers, return_exceptions=True)
|
|
||||||
|
|
||||||
def aggregate_final(self, responses: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
||||||
"""Aggregate final responses from agents."""
|
|
||||||
results = []
|
|
||||||
for agent_id, response in responses.items():
|
|
||||||
if response.get("event") == "done":
|
|
||||||
results.append({
|
|
||||||
"agent_id": agent_id,
|
|
||||||
"agent_name": response.get("agent_name"),
|
|
||||||
"message_id": response.get("message_id"),
|
|
||||||
"content": response.get("content"),
|
|
||||||
"token_count": response.get("token_count", 0)
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Chat Room Service ====================
|
|
||||||
|
|
||||||
class ChatRoomService:
|
class ChatRoomService:
|
||||||
"""Service for managing chat rooms with multi-agent support"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.dispatcher = MessageDispatcher()
|
|
||||||
|
|
||||||
def get_room(self, room_id: str) -> Optional[ChatRoom]:
|
def get_room(self, room_id: str) -> Optional[ChatRoom]:
|
||||||
"""Get a chat room by ID"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
return db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
return db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
||||||
|
|
@ -179,50 +21,60 @@ class ChatRoomService:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def get_room_agents(self, room_id: str) -> List[BaseAgent]:
|
def get_room_agents(self, room_id: str) -> List[BaseAgent]:
|
||||||
"""Get all active agents in a chat room"""
|
"""Get active agents in a room from Message table"""
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
room_agents = db.query(ChatRoomAgent).filter(
|
# Query distinct agent records from messages
|
||||||
ChatRoomAgent.chat_room_id == room_id,
|
messages = db.query(Message).filter(
|
||||||
ChatRoomAgent.is_active == True
|
Message.room_id == room_id,
|
||||||
).all()
|
Message.role == "agent"
|
||||||
|
).distinct().all()
|
||||||
|
|
||||||
|
agent_ids = []
|
||||||
|
seen = set()
|
||||||
|
for msg in messages:
|
||||||
|
# Extract agent_id from content JSON
|
||||||
|
try:
|
||||||
|
content = json.loads(msg.content) if msg.content else {}
|
||||||
|
agent_id = content.get("agent_id")
|
||||||
|
if agent_id and agent_id not in seen:
|
||||||
|
seen.add(agent_id)
|
||||||
|
agent_ids.append(agent_id)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
agents = []
|
agents = []
|
||||||
for ra in room_agents:
|
for agent_id in agent_ids:
|
||||||
agent_db = db.query(Agent).filter(Agent.id == ra.agent_id, Agent.is_active == True).first()
|
agent_db = db.query(Agent).filter(
|
||||||
|
Agent.id == agent_id,
|
||||||
|
Agent.is_active == True
|
||||||
|
).first()
|
||||||
if agent_db:
|
if agent_db:
|
||||||
agents.append(BaseAgent.from_model(agent_db))
|
agents.append(BaseAgent.from_model(agent_db))
|
||||||
|
|
||||||
agents.sort(key=lambda a: a.priority)
|
return sorted(agents, key=lambda a: a.priority)
|
||||||
return agents
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def get_agent(self, agent_id: str) -> Optional[BaseAgent]:
|
def get_agent(self, agent_id: str) -> Optional[BaseAgent]:
|
||||||
"""Get an agent by ID"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
agent_db = db.query(Agent).filter(Agent.id == agent_id).first()
|
agent_db = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||||
if agent_db:
|
return BaseAgent.from_model(agent_db) if agent_db else None
|
||||||
return BaseAgent.from_model(agent_db)
|
|
||||||
return None
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def list_rooms(self, user_id: int = None, include_agents: bool = True) -> List[Dict]:
|
def list_rooms(self, user_id: int = None) -> List[Dict]:
|
||||||
"""List all chat rooms"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
query = db.query(ChatRoom)
|
q = db.query(ChatRoom)
|
||||||
if user_id:
|
if user_id:
|
||||||
query = query.filter(ChatRoom.owner_id == user_id)
|
q = q.filter(ChatRoom.owner_id == user_id)
|
||||||
rooms = query.order_by(ChatRoom.updated_at.desc()).all()
|
return [r.to_dict() for r in q.order_by(ChatRoom.updated_at.desc()).all()]
|
||||||
return [r.to_dict(include_agents=include_agents) for r in rooms]
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def create_room(self, name: str, owner_id: int, description: str = None, agent_ids: List[str] = None) -> Dict:
|
def create_room(self, name: str, owner_id: int, description: str = None, agent_ids: List[str] = None) -> Dict:
|
||||||
"""Create a new chat room"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
room = ChatRoom(
|
room = ChatRoom(
|
||||||
|
|
@ -233,132 +85,116 @@ class ChatRoomService:
|
||||||
)
|
)
|
||||||
db.add(room)
|
db.add(room)
|
||||||
|
|
||||||
if agent_ids:
|
# Record agents as join messages
|
||||||
for agent_id in agent_ids:
|
for agent_id in (agent_ids or []):
|
||||||
room_agent = ChatRoomAgent(
|
msg = Message(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
chat_room_id=room.id,
|
room_id=room.id,
|
||||||
agent_id=agent_id
|
role="agent",
|
||||||
)
|
content=json.dumps({"type": "join", "agent_id": agent_id}),
|
||||||
db.add(room_agent)
|
sender_name=agent_id
|
||||||
|
)
|
||||||
|
db.add(msg)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return room.to_dict(include_agents=True)
|
return room.to_dict()
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def update_room(self, room_id: str, name: str = None, description: str = None, is_active: bool = None) -> Optional[Dict]:
|
def update_room(self, room_id: str, **kwargs) -> Optional[Dict]:
|
||||||
"""Update a chat room"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
||||||
if not room:
|
if not room:
|
||||||
return None
|
return None
|
||||||
|
for key, value in kwargs.items():
|
||||||
if name is not None:
|
if value is not None and hasattr(room, key):
|
||||||
room.name = name
|
setattr(room, key, value)
|
||||||
if description is not None:
|
|
||||||
room.description = description
|
|
||||||
if is_active is not None:
|
|
||||||
room.is_active = is_active
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return room.to_dict(include_agents=True)
|
return room.to_dict()
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def delete_room(self, room_id: str) -> bool:
|
def delete_room(self, room_id: str) -> bool:
|
||||||
"""Delete a chat room"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
||||||
if not room:
|
if room:
|
||||||
return False
|
db.delete(room)
|
||||||
db.delete(room)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def add_agent_to_room(self, room_id: str, agent_id: str) -> bool:
|
|
||||||
"""Add an agent to a chat room"""
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
|
||||||
existing = db.query(ChatRoomAgent).filter(
|
|
||||||
ChatRoomAgent.chat_room_id == room_id,
|
|
||||||
ChatRoomAgent.agent_id == agent_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
existing.is_active = True
|
|
||||||
else:
|
|
||||||
room_agent = ChatRoomAgent(
|
|
||||||
id=str(uuid.uuid4()),
|
|
||||||
chat_room_id=room_id,
|
|
||||||
agent_id=agent_id
|
|
||||||
)
|
|
||||||
db.add(room_agent)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def remove_agent_from_room(self, room_id: str, agent_id: str) -> bool:
|
|
||||||
"""Remove an agent from a chat room"""
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
|
||||||
room_agent = db.query(ChatRoomAgent).filter(
|
|
||||||
ChatRoomAgent.chat_room_id == room_id,
|
|
||||||
ChatRoomAgent.agent_id == agent_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if room_agent:
|
|
||||||
room_agent.is_active = False
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def get_messages(self, room_id: str, limit: int = 50, before_id: str = None) -> List[Dict]:
|
def add_participant(
|
||||||
"""Get messages from a chat room"""
|
self, room_id: str, agent_id: str = None, user_id: int = None
|
||||||
|
) -> bool:
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
query = db.query(ChatRoomMessage).filter(
|
role = "agent" if agent_id else "user"
|
||||||
ChatRoomMessage.room_id == room_id
|
sender_name = agent_id or f"user_{user_id}"
|
||||||
).order_by(ChatRoomMessage.created_at.desc())
|
content_data = {"type": "join", "agent_id": agent_id} if agent_id else {"type": "join", "user_id": user_id}
|
||||||
|
|
||||||
if before_id:
|
msg = Message(
|
||||||
before_msg = db.query(ChatRoomMessage).filter(
|
id=str(uuid.uuid4()),
|
||||||
ChatRoomMessage.id == before_id
|
room_id=room_id,
|
||||||
).first()
|
role=role,
|
||||||
if before_msg:
|
sender_name=sender_name,
|
||||||
query = query.filter(ChatRoomMessage.created_at < before_msg.created_at)
|
content=json.dumps(content_data)
|
||||||
|
)
|
||||||
messages = query.limit(limit).all()
|
db.add(msg)
|
||||||
return [m.to_dict() for m in reversed(messages)]
|
db.commit()
|
||||||
|
return True
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def save_message(self, room_id: str, sender_type: str, sender_id: str, sender_name: str, content: str,
|
def remove_participant(self, room_id: str, participant_id: str) -> bool:
|
||||||
mentions: List[str] = None, parent_id: str = None, token_count: int = 0) -> Dict:
|
|
||||||
"""Save a message to a chat room"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
msg = ChatRoomMessage(
|
db.query(Message).filter(
|
||||||
|
Message.id == participant_id
|
||||||
|
).delete()
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def get_messages(self, room_id: str, limit: int = 50, before_id: str = None) -> List[Dict]:
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
q = db.query(Message).filter(Message.room_id == room_id).order_by(Message.created_at.desc())
|
||||||
|
if before_id:
|
||||||
|
before = db.query(Message).filter(Message.id == before_id).first()
|
||||||
|
if before:
|
||||||
|
q = q.filter(Message.created_at < before.created_at)
|
||||||
|
return [m.to_dict() for m in reversed(q.limit(limit).all())]
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def save_message(
|
||||||
|
self,
|
||||||
|
room_id: str,
|
||||||
|
role: str,
|
||||||
|
sender_name: str,
|
||||||
|
content: str,
|
||||||
|
mentions: List[str] = None,
|
||||||
|
token_count: int = 0
|
||||||
|
) -> Dict:
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
msg = Message(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
sender_type=sender_type,
|
role=role,
|
||||||
sender_id=sender_id,
|
|
||||||
sender_name=sender_name,
|
sender_name=sender_name,
|
||||||
content=content,
|
content=content,
|
||||||
mentions=json.dumps(mentions) if mentions else None,
|
mentions=json.dumps(mentions) if mentions else None,
|
||||||
parent_id=parent_id,
|
|
||||||
token_count=token_count
|
token_count=token_count
|
||||||
)
|
)
|
||||||
db.add(msg)
|
db.add(msg)
|
||||||
|
|
||||||
|
# Update room updated_at
|
||||||
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
||||||
if room:
|
if room:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -369,47 +205,54 @@ class ChatRoomService:
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
async def process_message(self, room_id: str, user_message: str, user_id: str, user_name: str, context: Dict = None) -> AsyncGenerator[Dict[str, Any], None]:
|
async def process_message(
|
||||||
"""Process a user message and dispatch to appropriate agents."""
|
self, room_id: str, user_message: str, sender_id: str, sender_name: str = None
|
||||||
|
):
|
||||||
room = self.get_room(room_id)
|
room = self.get_room(room_id)
|
||||||
if not room:
|
if not room:
|
||||||
yield {"event": "error", "data": {"content": "Chat room not found"}}
|
yield {"event": "error", "data": {"content": "Chat room not found"}}
|
||||||
return
|
return
|
||||||
|
|
||||||
room_agents = self.get_room_agents(room_id)
|
agents = self.get_room_agents(room_id)
|
||||||
if not room_agents:
|
if not agents:
|
||||||
yield {"event": "error", "data": {"content": "No agents available in this room"}}
|
yield {"event": "error", "data": {"content": "No agents available"}}
|
||||||
return
|
return
|
||||||
|
|
||||||
dispatch_result = self.dispatcher.dispatch(
|
# Check if sender is agent
|
||||||
content=user_message,
|
from luxx.agents.registry import agent_registry
|
||||||
room_agents=room_agents,
|
sender_is_agent = agent_registry.get(sender_id) is not None
|
||||||
sender_id=user_id,
|
|
||||||
sender_type="user"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not dispatch_result.should_respond:
|
# Filter out sender if agent
|
||||||
|
if sender_is_agent:
|
||||||
|
agents = [a for a in agents if a.agent_id != sender_id]
|
||||||
|
|
||||||
|
# Check mentions
|
||||||
|
import re
|
||||||
|
mentions = re.findall(r'@(\w+)', user_message)
|
||||||
|
triggered = []
|
||||||
|
|
||||||
|
if mentions:
|
||||||
|
name_map = {a.name.lower(): a for a in agents}
|
||||||
|
triggered = [name_map[n.lower()] for n in mentions if n.lower() in name_map]
|
||||||
|
|
||||||
|
if not triggered:
|
||||||
|
triggered = [a for a in agents if a.auto_response]
|
||||||
|
triggered.sort(key=lambda a: a.priority)
|
||||||
|
|
||||||
|
if not triggered:
|
||||||
yield {"event": "no_response", "data": {"message": "No agents triggered"}}
|
yield {"event": "no_response", "data": {"message": "No agents triggered"}}
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Get history
|
||||||
messages = self.get_messages(room_id, limit=20)
|
messages = self.get_messages(room_id, limit=20)
|
||||||
|
|
||||||
agent_streams = {}
|
# Stream responses
|
||||||
for agent in dispatch_result.triggered_agents:
|
for agent in triggered:
|
||||||
stream = agent.stream_response(
|
async for event in agent.stream_response(user_message, messages):
|
||||||
user_message=user_message,
|
yield event
|
||||||
conversation_history=messages,
|
|
||||||
context=context
|
|
||||||
)
|
|
||||||
agent_streams[agent.agent_id] = stream
|
|
||||||
|
|
||||||
aggregator = ResponseAggregator(room_id)
|
self.save_message(room_id, "user", sender_id, sender_name, user_message)
|
||||||
async for event in aggregator.aggregate_stream(agent_streams):
|
|
||||||
yield event
|
|
||||||
|
|
||||||
|
|
||||||
# Global service instance
|
# Global instance
|
||||||
chat_room_service = ChatRoomService()
|
chat_room_service = ChatRoomService()
|
||||||
|
|
||||||
# Export for backward compatibility
|
|
||||||
dispatcher = chat_room_service.dispatcher
|
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,82 @@
|
||||||
"""WebSocket handler for Chat Rooms"""
|
"""WebSocket handler for Chat Rooms - unified user and agent participants."""
|
||||||
import json
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Set
|
from typing import Dict, Set
|
||||||
from fastapi import WebSocket, WebSocketDisconnect
|
from fastapi import WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
from luxx.services.room import chat_room_service
|
from luxx.services.room import chat_room_service
|
||||||
|
from luxx.services.participant import participant_service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChatRoomConnectionManager:
|
class ConnectionManager:
|
||||||
"""Manages WebSocket connections for chat rooms"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._room_connections: Dict[str, Set[WebSocket]] = {}
|
self._rooms: Dict[str, Set[WebSocket]] = {}
|
||||||
self._connection_rooms: Dict[WebSocket, str] = {}
|
self._info: Dict[WebSocket, Dict] = {}
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket, room_id: str):
|
async def connect(self, ws: WebSocket, room_id: str, ptype: str, pid: str, pname: str):
|
||||||
"""Connect a WebSocket to a chat room"""
|
await ws.accept()
|
||||||
await websocket.accept()
|
self._rooms.setdefault(room_id, set()).add(ws)
|
||||||
|
self._info[ws] = {"type": ptype, "id": pid, "name": pname}
|
||||||
|
await ws.send_json({"event": "connected", "data": {"room_id": room_id, "type": ptype}})
|
||||||
|
|
||||||
if room_id not in self._room_connections:
|
def disconnect(self, ws: WebSocket):
|
||||||
self._room_connections[room_id] = set()
|
info = self._info.pop(ws, {})
|
||||||
|
room = self._rooms.get(self._info.get(ws, {}).get("id"))
|
||||||
|
if room:
|
||||||
|
room.discard(ws)
|
||||||
|
if not room:
|
||||||
|
del self._rooms[room]
|
||||||
|
return info
|
||||||
|
|
||||||
self._room_connections[room_id].add(websocket)
|
async def broadcast(self, room_id: str, msg: dict, exclude: WebSocket = None):
|
||||||
self._connection_rooms[websocket] = room_id
|
for ws in self._rooms.get(room_id, set()):
|
||||||
|
if ws != exclude:
|
||||||
|
try:
|
||||||
|
await ws.send_json(msg)
|
||||||
|
except:
|
||||||
|
self.disconnect(ws)
|
||||||
|
|
||||||
logger.info(f"WebSocket connected to room: {room_id}")
|
def size(self, room_id: str) -> int:
|
||||||
|
return len(self._rooms.get(room_id, set()))
|
||||||
await websocket.send_json({
|
|
||||||
"event": "connected",
|
|
||||||
"data": {"room_id": room_id, "message": "Connected to chat room"}
|
|
||||||
})
|
|
||||||
|
|
||||||
def disconnect(self, websocket: WebSocket):
|
|
||||||
"""Disconnect a WebSocket from its room"""
|
|
||||||
room_id = self._connection_rooms.pop(websocket, None)
|
|
||||||
if room_id and room_id in self._room_connections:
|
|
||||||
self._room_connections[room_id].discard(websocket)
|
|
||||||
if not self._room_connections[room_id]:
|
|
||||||
del self._room_connections[room_id]
|
|
||||||
logger.info(f"WebSocket disconnected from room: {room_id}")
|
|
||||||
|
|
||||||
async def broadcast_to_room(self, room_id: str, message: dict, exclude: WebSocket = None):
|
|
||||||
"""Broadcast a message to all connections in a room"""
|
|
||||||
if room_id not in self._room_connections:
|
|
||||||
return
|
|
||||||
|
|
||||||
disconnected = []
|
|
||||||
for connection in self._room_connections[room_id]:
|
|
||||||
if connection == exclude:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
await connection.send_json(message)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to send to connection: {e}")
|
|
||||||
disconnected.append(connection)
|
|
||||||
|
|
||||||
for conn in disconnected:
|
|
||||||
self.disconnect(conn)
|
|
||||||
|
|
||||||
async def send_to_room(self, room_id: str, message: dict):
|
|
||||||
"""Send a message to all connections in a room"""
|
|
||||||
await self.broadcast_to_room(room_id, message)
|
|
||||||
|
|
||||||
def get_room_size(self, room_id: str) -> int:
|
|
||||||
"""Get the number of connections in a room"""
|
|
||||||
return len(self._room_connections.get(room_id, set()))
|
|
||||||
|
|
||||||
|
|
||||||
# Global connection manager
|
cm = ConnectionManager()
|
||||||
connection_manager = ChatRoomConnectionManager()
|
|
||||||
|
|
||||||
|
|
||||||
async def websocket_handler(websocket: WebSocket, room_id: str):
|
async def websocket_handler(ws: WebSocket, room_id: str):
|
||||||
"""Handle WebSocket connection for a chat room."""
|
params = dict(ws.query_params)
|
||||||
await connection_manager.connect(websocket, room_id)
|
ptype = params.get("participant_type", "user")
|
||||||
|
pid = params.get("participant_id", "")
|
||||||
|
pname = params.get("participant_name", "Anonymous")
|
||||||
|
|
||||||
|
await cm.connect(ws, room_id, ptype, pid, pname)
|
||||||
|
|
||||||
room = chat_room_service.get_room(room_id)
|
room = chat_room_service.get_room(room_id)
|
||||||
if not room:
|
if not room:
|
||||||
await websocket.send_json({"event": "error", "data": {"content": "Chat room not found"}})
|
await ws.send_json({"event": "error", "data": {"content": "Room not found"}})
|
||||||
await websocket.close()
|
await ws.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if ptype == "agent" and pid:
|
||||||
|
agent = chat_room_service.get_agent(pid)
|
||||||
|
if agent:
|
||||||
|
participant_service.register_agent(agent)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
messages = chat_room_service.get_messages(room_id, limit=50)
|
# Send history
|
||||||
await websocket.send_json({"event": "history", "data": {"messages": messages}})
|
await ws.send_json({"event": "history", "data": {"messages": chat_room_service.get_messages(room_id)}})
|
||||||
|
await ws.send_json({"event": "agents", "data": {
|
||||||
|
"agents": [a.to_dict() for a in chat_room_service.get_room_agents(room_id)]
|
||||||
|
}})
|
||||||
|
|
||||||
agents = chat_room_service.get_room_agents(room_id)
|
await cm.broadcast(room_id, {
|
||||||
await websocket.send_json({"event": "agents", "data": {"agents": [a.to_dict() for a in agents]}})
|
|
||||||
|
|
||||||
await connection_manager.broadcast_to_room(room_id, {
|
|
||||||
"event": "system",
|
"event": "system",
|
||||||
"data": {"content": "User joined the room", "type": "join"}
|
"data": {"content": f"{pname} joined", "type": f"{ptype}_join"}
|
||||||
}, exclude=websocket)
|
}, exclude=ws)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
data = await websocket.receive_json()
|
data = await ws.receive_json()
|
||||||
action = data.get("action")
|
action = data.get("action")
|
||||||
|
|
||||||
if action == "send_message":
|
if action == "send_message":
|
||||||
|
|
@ -105,87 +84,29 @@ async def websocket_handler(websocket: WebSocket, room_id: str):
|
||||||
if not content:
|
if not content:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
user_id = str(data.get("user_id", "anonymous"))
|
sid = pid if ptype == "agent" else str(data.get("user_id", pid or "anonymous"))
|
||||||
user_name = data.get("user_name", "Anonymous")
|
sname = pname if ptype == "agent" else data.get("user_name", pname or "Anonymous")
|
||||||
|
|
||||||
msg = chat_room_service.save_message(
|
async for event in participant_service.process_message(
|
||||||
room_id=room_id, sender_type="user", sender_id=user_id,
|
room_id, content, sid, sname, ptype
|
||||||
sender_name=user_name, content=content
|
|
||||||
)
|
|
||||||
|
|
||||||
await connection_manager.broadcast_to_room(room_id, {"event": "message", "data": msg})
|
|
||||||
|
|
||||||
agents = chat_room_service.get_room_agents(room_id)
|
|
||||||
for agent in agents:
|
|
||||||
await connection_manager.broadcast_to_room(room_id, {
|
|
||||||
"event": "typing",
|
|
||||||
"data": {"agent_id": agent.agent_id, "agent_name": agent.name, "is_typing": True}
|
|
||||||
})
|
|
||||||
|
|
||||||
context = {"user_id": user_id, "username": user_name}
|
|
||||||
logger.info(f"[ROOM_WS] Starting process_message, agents count: {len(agents)}")
|
|
||||||
|
|
||||||
event_count = 0
|
|
||||||
async for event in chat_room_service.process_message(
|
|
||||||
room_id=room_id, user_message=content, user_id=user_id,
|
|
||||||
user_name=user_name, context=context
|
|
||||||
):
|
):
|
||||||
event_count += 1
|
|
||||||
logger.info(f"[ROOM_WS] Received event {event_count}: {event.get('event')}")
|
|
||||||
|
|
||||||
if event.get("event") in ["process_step", "done", "error"]:
|
if event.get("event") in ["process_step", "done", "error"]:
|
||||||
# Find agent_name from agents list
|
await cm.broadcast(room_id, {
|
||||||
agent_name = None
|
"event": event["event"],
|
||||||
for agent in agents:
|
|
||||||
if agent.agent_id == event.get("agent_id"):
|
|
||||||
agent_name = agent.name
|
|
||||||
break
|
|
||||||
await connection_manager.broadcast_to_room(room_id, {
|
|
||||||
"event": event.get("event"),
|
|
||||||
"data": event.get("data", {}),
|
"data": event.get("data", {}),
|
||||||
"agent_id": event.get("agent_id"),
|
"agent_id": event.get("agent_id")
|
||||||
"agent_name": agent_name
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if event.get("event") == "done":
|
|
||||||
# Find agent_name from agents list
|
|
||||||
agent_name = None
|
|
||||||
for agent in agents:
|
|
||||||
if agent.agent_id == event.get("agent_id"):
|
|
||||||
agent_name = agent.name
|
|
||||||
break
|
|
||||||
chat_room_service.save_message(
|
|
||||||
room_id=room_id, sender_type="agent",
|
|
||||||
sender_id=event.get("agent_id"),
|
|
||||||
sender_name=agent_name,
|
|
||||||
content=event.get("data", {}).get("content", "") if isinstance(event.get("data"), dict) else "",
|
|
||||||
token_count=event.get("data", {}).get("token_count", 0) if isinstance(event.get("data"), dict) else 0
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info(f"[ROOM_WS] Skipping event: {event.get('event')}")
|
|
||||||
|
|
||||||
logger.info(f"[ROOM_WS] process_message completed, total events: {event_count}")
|
|
||||||
|
|
||||||
for agent in agents:
|
|
||||||
await connection_manager.broadcast_to_room(room_id, {
|
|
||||||
"event": "typing",
|
|
||||||
"data": {"agent_id": agent.agent_id, "agent_name": agent.name, "is_typing": False}
|
|
||||||
})
|
|
||||||
|
|
||||||
elif action == "ping":
|
elif action == "ping":
|
||||||
await websocket.send_json({"event": "pong", "data": {}})
|
await ws.send_json({"event": "pong", "data": {}})
|
||||||
|
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
logger.info(f"WebSocket disconnected from room: {room_id}")
|
await cm.broadcast(room_id, {"event": "system", "data": {"content": f"{pname} left", "type": "leave"}})
|
||||||
connection_manager.disconnect(websocket)
|
|
||||||
await connection_manager.broadcast_to_room(room_id, {
|
|
||||||
"event": "system",
|
|
||||||
"data": {"content": "User left the room", "type": "leave"}
|
|
||||||
})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"WebSocket error in room {room_id}: {e}")
|
logger.error(f"WebSocket error: {e}")
|
||||||
connection_manager.disconnect(websocket)
|
await cm.broadcast(room_id, {"event": "error", "data": {"content": str(e)}})
|
||||||
await connection_manager.broadcast_to_room(room_id, {
|
finally:
|
||||||
"event": "error",
|
cm.disconnect(ws)
|
||||||
"data": {"content": str(e)}
|
|
||||||
})
|
|
||||||
|
connection_manager = cm
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue