新增AI会话系统
This commit is contained in:
279
.tasks/2025-10-29_3_ai_chat.md
Normal file
279
.tasks/2025-10-29_3_ai_chat.md
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# 背景
|
||||||
|
文件名:2025-10-29_3_ai_chat.md
|
||||||
|
创建于:2025-10-29_23:32:40
|
||||||
|
创建者:user
|
||||||
|
主分支:main
|
||||||
|
任务分支:task/ai_chat_2025-10-29_3
|
||||||
|
Yolo模式:Off
|
||||||
|
|
||||||
|
# 任务描述
|
||||||
|
在本项目中新增一个AI对话功能,使用llama_index构建,服务商为本地部署的ollama。
|
||||||
|
|
||||||
|
这个智能体将拥有足够长的上下文(超过30轮对话历史),能够同时与不同的用户展开交流。例如用户A提问后用户B进行补充,智能体将通过时间间隔判断机制来决定何时进行回答。
|
||||||
|
|
||||||
|
## 核心需求
|
||||||
|
1. **显式指令触发**:使用 `.ai <问题>` 指令触发AI对话
|
||||||
|
2. **配置指令**:使用 `.aiconfig` 指令配置Ollama服务地址、端口和模型名称
|
||||||
|
3. **时间间隔判断**:智能体通过时间间隔判断是否需要回答(固定10秒等待窗口)
|
||||||
|
4. **长上下文管理**:保留超过30轮对话历史
|
||||||
|
5. **多用户对话支持**:同一chat_id下不同用户的消息能够被正确识别和处理
|
||||||
|
|
||||||
|
## 技术方案决策(已确定)
|
||||||
|
1. **延迟任务机制**:使用 asyncio 的延迟任务(方案三)
|
||||||
|
- 每个 chat_id 维护独立的延迟任务句柄
|
||||||
|
- 使用全局字典存储任务映射
|
||||||
|
- 收到新消息时取消旧任务并创建新任务
|
||||||
|
|
||||||
|
2. **上下文管理**:使用 llama_index 的 ChatMemoryBuffer(策略A)
|
||||||
|
- 设置足够的 token_limit 确保保留30+轮对话
|
||||||
|
- 按 chat_id 独立维护 ChatEngine 实例
|
||||||
|
|
||||||
|
3. **多用户识别**:消息角色映射 + 系统提示(方案C)
|
||||||
|
- 将不同用户映射为不同角色(如"用户1"、"用户2")
|
||||||
|
- 在系统提示中明确告知存在多用户场景
|
||||||
|
- ChatMemoryBuffer 中使用角色区分不同用户
|
||||||
|
|
||||||
|
4. **等待窗口**:固定10秒窗口(变体1)
|
||||||
|
- 收到消息后等待10秒
|
||||||
|
- 等待期间有新消息则重新计时
|
||||||
|
|
||||||
|
5. **配置管理**:使用单独的JSON文件
|
||||||
|
- 配置存储在 `data/ai_config.json`
|
||||||
|
- 全局单一配置(服务器级别,非chat级别)
|
||||||
|
- 通过 `.aiconfig` 指令修改配置并保存到文件
|
||||||
|
|
||||||
|
# 项目概览
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
```
|
||||||
|
WPSBot/
|
||||||
|
├── app.py # FastAPI主应用
|
||||||
|
├── config.py # 配置管理
|
||||||
|
├── core/ # 核心模块
|
||||||
|
│ ├── database.py # 数据库操作
|
||||||
|
│ ├── models.py # 数据模型
|
||||||
|
│ └── middleware.py # 中间件
|
||||||
|
├── routers/ # 路由模块
|
||||||
|
│ ├── callback.py # 回调处理
|
||||||
|
│ └── health.py # 健康检查
|
||||||
|
├── games/ # 游戏模块
|
||||||
|
│ ├── base.py # 游戏基类
|
||||||
|
│ └── ... # 其他游戏
|
||||||
|
├── utils/ # 工具模块
|
||||||
|
│ ├── message.py # 消息发送
|
||||||
|
│ ├── parser.py # 指令解析
|
||||||
|
│ └── rate_limit.py # 限流控制
|
||||||
|
└── data/ # 数据文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
- FastAPI:Web框架
|
||||||
|
- SQLite:数据存储
|
||||||
|
- llama-index-core:AI对话框架核心
|
||||||
|
- llama-index-llms-ollama:Ollama LLM集成
|
||||||
|
- Ollama:本地LLM服务
|
||||||
|
|
||||||
|
# 分析
|
||||||
|
|
||||||
|
## 现有架构
|
||||||
|
1. **指令处理流程**:
|
||||||
|
- 消息通过 `/api/callback` 接收
|
||||||
|
- `CommandParser` 解析指令,只处理以 `.` 开头的命令
|
||||||
|
- 非指令消息会被忽略
|
||||||
|
- 指令分发到对应的游戏处理器
|
||||||
|
|
||||||
|
2. **状态管理**:
|
||||||
|
- 游戏状态存储在 `game_states` 表
|
||||||
|
- 使用 `(chat_id, user_id, game_type)` 作为联合主键
|
||||||
|
- 对于群组共享状态,使用 `user_id=0`(如成语接龙)
|
||||||
|
|
||||||
|
3. **异步任务**:
|
||||||
|
- 已有 `periodic_cleanup` 后台清理任务示例
|
||||||
|
- 使用 `asyncio.create_task` 和 `asyncio.sleep` 实现
|
||||||
|
|
||||||
|
## 关键技术挑战
|
||||||
|
|
||||||
|
### 1. 延迟回答机制
|
||||||
|
需要实现一个基于时间间隔的判断机制:
|
||||||
|
- 收到 `.ai` 指令时,将消息加入等待队列
|
||||||
|
- 设置一个等待窗口(例如5-10秒)
|
||||||
|
- 如果在等待窗口内有新消息,重新计时
|
||||||
|
- 等待窗口结束后,如果没有新消息,生成回答
|
||||||
|
- 需要在 `chat_id` 级别维护等待队列和延迟任务
|
||||||
|
|
||||||
|
### 2. 长上下文管理
|
||||||
|
- 使用 llama_index 的 `ChatMemoryBuffer` 管理对话历史
|
||||||
|
- 确保超过30轮对话历史能够被保留
|
||||||
|
- 对话历史需要按 `chat_id` 独立存储
|
||||||
|
- 对话历史中需要包含用户ID信息,以便区分不同用户
|
||||||
|
|
||||||
|
### 3. Ollama配置管理
|
||||||
|
- 使用全局单一配置(服务器级别)
|
||||||
|
- 配置存储在 `data/ai_config.json` 文件中
|
||||||
|
- 配置包括:服务地址、端口、模型名称
|
||||||
|
- 通过 `.aiconfig` 指令修改配置并持久化到文件
|
||||||
|
- 配置需要有默认值(localhost:11434,默认模型需指定)
|
||||||
|
|
||||||
|
### 4. 多用户对话识别
|
||||||
|
- 对话历史中需要记录每条消息的发送者(user_id)
|
||||||
|
- 生成回复时,需要识别上下文中的不同用户
|
||||||
|
- 回复格式可以考虑使用 @用户 的方式
|
||||||
|
|
||||||
|
### 5. 依赖管理
|
||||||
|
- 需要添加 llama-index-core 和相关依赖
|
||||||
|
- 需要确保与现有代码库的兼容性
|
||||||
|
- 考虑资源占用(内存、CPU)
|
||||||
|
|
||||||
|
## 数据结构设计
|
||||||
|
|
||||||
|
### AI对话状态数据结构
|
||||||
|
对话状态由 llama_index 的 ChatMemoryBuffer 管理,存储在内存中。
|
||||||
|
需要存储的额外信息:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 存储在 game_states 表中的 state_data
|
||||||
|
{
|
||||||
|
"user_mapping": { # 用户ID到角色名称的映射
|
||||||
|
"123456": "用户1",
|
||||||
|
"789012": "用户2",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"user_count": 2 # 当前对话中的用户数量
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置数据结构(存储在 data/ai_config.json)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 11434,
|
||||||
|
"model": "llama3.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库扩展
|
||||||
|
使用 `game_states` 表存储用户映射信息:
|
||||||
|
- `chat_id`: 会话ID
|
||||||
|
- `user_id`: 0(表示群组级别)
|
||||||
|
- `game_type`: "ai_chat"
|
||||||
|
- `state_data`: JSON格式的用户映射信息
|
||||||
|
|
||||||
|
注意:对话历史由 ChatMemoryBuffer 在内存中管理,不持久化到数据库。
|
||||||
|
|
||||||
|
# 提议的解决方案
|
||||||
|
|
||||||
|
## 方案概述
|
||||||
|
1. 创建一个新的游戏模块 `games/ai_chat.py`,继承 `BaseGame`
|
||||||
|
2. 使用 `game_states` 表存储用户映射信息(用户ID到角色名称的映射)
|
||||||
|
3. 使用全局字典维护每个 `chat_id` 的延迟任务句柄
|
||||||
|
4. 使用全局字典维护每个 `chat_id` 的 ChatEngine 实例和待处理消息队列
|
||||||
|
5. 使用 `data/ai_config.json` 存储 Ollama 全局配置
|
||||||
|
6. 使用 llama_index 的 ChatMemoryBuffer 管理对话上下文(内存中)
|
||||||
|
|
||||||
|
## 实现细节
|
||||||
|
|
||||||
|
### 1. 指令注册
|
||||||
|
在 `utils/parser.py` 中添加:
|
||||||
|
- `.ai`: 触发AI对话
|
||||||
|
- `.aiconfig`: 配置Ollama参数
|
||||||
|
|
||||||
|
### 2. AI对话模块 (`games/ai_chat.py`)
|
||||||
|
- `handle()`: 主处理函数,处理 `.ai` 和 `.aiconfig` 指令
|
||||||
|
- `_handle_ai()`: 处理AI对话请求
|
||||||
|
- 将消息加入等待队列
|
||||||
|
- 取消旧的延迟任务(如果存在)
|
||||||
|
- 创建新的延迟任务(10秒后执行)
|
||||||
|
- `_handle_config()`: 处理配置请求
|
||||||
|
- 解析配置参数(host, port, model)
|
||||||
|
- 更新 `data/ai_config.json` 文件
|
||||||
|
- 返回配置确认消息
|
||||||
|
- `_add_to_queue()`: 将消息加入等待队列(按 chat_id 组织)
|
||||||
|
- `_delayed_response()`: 延迟回答任务(内部异步函数)
|
||||||
|
- 等待10秒后执行
|
||||||
|
- 检查队列并生成回答
|
||||||
|
- 处理任务取消异常
|
||||||
|
- `_generate_response()`: 使用LLM生成回答
|
||||||
|
- 获取或创建 ChatEngine 实例
|
||||||
|
- 获取用户角色映射
|
||||||
|
- 将队列中的消息按用户角色格式化
|
||||||
|
- 调用 ChatEngine.chat() 生成回答
|
||||||
|
- 更新 ChatMemoryBuffer
|
||||||
|
- `_get_chat_engine()`: 获取或创建ChatEngine实例
|
||||||
|
- 检查全局字典中是否已存在
|
||||||
|
- 不存在则创建新的 ChatEngine,配置 ChatMemoryBuffer
|
||||||
|
- 设置系统提示(告知多用户场景)
|
||||||
|
- `_get_user_role()`: 获取用户角色名称(创建或获取映射)
|
||||||
|
- `_load_config()`: 从 JSON 文件加载配置
|
||||||
|
- `_save_config()`: 保存配置到 JSON 文件
|
||||||
|
|
||||||
|
### 3. 延迟任务管理
|
||||||
|
- 使用全局字典 `_pending_tasks` 存储每个 `chat_id` 的延迟任务句柄
|
||||||
|
- 使用全局字典 `_message_queues` 存储每个 `chat_id` 的待处理消息队列
|
||||||
|
- 使用全局字典 `_chat_engines` 存储每个 `chat_id` 的 ChatEngine 实例
|
||||||
|
- 新消息到达时,取消旧任务(调用 task.cancel())并创建新任务
|
||||||
|
- 使用 `asyncio.create_task` 和 `asyncio.sleep(10)` 实现固定10秒延迟
|
||||||
|
- 处理 `asyncio.CancelledError` 异常,避免任务取消时的错误日志
|
||||||
|
|
||||||
|
### 4. 用户角色映射机制
|
||||||
|
- 为每个 chat_id 维护用户ID到角色名称的映射(如"用户1"、"用户2")
|
||||||
|
- 映射信息存储在 `game_states` 表中(chat_id, user_id=0, game_type='ai_chat')
|
||||||
|
- 首次出现的用户自动分配角色名称(按出现顺序)
|
||||||
|
- 在将消息添加到 ChatMemoryBuffer 时使用角色名称作为消息角色
|
||||||
|
- 系统提示中包含:"这是一个多用户对话场景,不同用户的发言会用不同的角色标识。你需要理解不同用户的发言内容,并根据上下文给出合适的回复。"
|
||||||
|
|
||||||
|
### 4. 依赖添加
|
||||||
|
在 `requirements.txt` 中添加:
|
||||||
|
```
|
||||||
|
llama-index-core>=0.10.0
|
||||||
|
llama-index-llms-ollama>=0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 路由注册
|
||||||
|
在 `routers/callback.py` 的 `handle_command()` 中添加AI对话处理分支
|
||||||
|
|
||||||
|
### 6. 帮助信息更新
|
||||||
|
在 `games/base.py` 的 `get_help_message()` 中添加AI对话帮助
|
||||||
|
|
||||||
|
## 时间间隔判断逻辑(固定10秒窗口)
|
||||||
|
1. **默认等待窗口**:10秒(固定)
|
||||||
|
2. **收到 `.ai` 指令时**:
|
||||||
|
- 提取消息内容(去除 `.ai` 前缀)
|
||||||
|
- 获取用户ID和chat_id
|
||||||
|
- 将消息(用户ID + 内容)加入该 `chat_id` 的等待队列
|
||||||
|
- 如果有待处理的延迟任务(检查 `_pending_tasks[chat_id]`),取消它
|
||||||
|
- 创建新的延迟任务(`asyncio.create_task(_delayed_response(chat_id))`)
|
||||||
|
- 将任务句柄存储到 `_pending_tasks[chat_id]`
|
||||||
|
3. **在等待窗口内收到新消息**(无论是否是指令):
|
||||||
|
- 如果新消息也是 `.ai` 指令:
|
||||||
|
- 将新消息加入队列
|
||||||
|
- 取消当前延迟任务(`task.cancel()`)
|
||||||
|
- 创建新的延迟任务(重新计时10秒)
|
||||||
|
- 如果新消息不是指令,但chat_id在等待队列中:
|
||||||
|
- 可以考虑忽略,或也加入队列(根据需求决定)
|
||||||
|
4. **等待窗口结束(延迟任务执行)**:
|
||||||
|
- 检查队列中是否有消息
|
||||||
|
- 如果有,获取该 chat_id 的 ChatEngine 和用户映射
|
||||||
|
- 将队列中的消息按用户角色格式化后添加到 ChatMemoryBuffer
|
||||||
|
- 调用 ChatEngine.chat() 生成回答
|
||||||
|
- 清空队列
|
||||||
|
- 从 `_pending_tasks` 中移除任务句柄
|
||||||
|
|
||||||
|
## 配置文件管理(data/ai_config.json)
|
||||||
|
- 文件结构:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 11434,
|
||||||
|
"model": "llama3.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 首次加载时如果文件不存在,创建默认配置
|
||||||
|
- 通过 `.aiconfig` 指令修改配置时,实时保存到文件
|
||||||
|
- ChatEngine 创建时从配置文件加载配置
|
||||||
|
|
||||||
|
# 当前执行步骤:"2. 创新模式 - 方案决策完成"
|
||||||
|
|
||||||
|
# 任务进度
|
||||||
|
|
||||||
|
# 最终审查
|
||||||
|
|
||||||
6
data/ai_config.json
Normal file
6
data/ai_config.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 11434,
|
||||||
|
"model": "llama3.1"
|
||||||
|
}
|
||||||
|
|
||||||
452
games/ai_chat.py
Normal file
452
games/ai_chat.py
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
"""AI对话游戏模块"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
from games.base import BaseGame
|
||||||
|
from utils.parser import CommandParser
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 全局字典:存储每个chat_id的延迟任务句柄
|
||||||
|
_pending_tasks: Dict[int, asyncio.Task] = {}
|
||||||
|
|
||||||
|
# 全局字典:存储每个chat_id的待处理消息队列
|
||||||
|
_message_queues: Dict[int, List[Dict[str, Any]]] = {}
|
||||||
|
|
||||||
|
# 全局字典:存储每个chat_id的ChatEngine实例
|
||||||
|
_chat_engines: Dict[int, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class AIChatGame(BaseGame):
|
||||||
|
"""AI对话游戏"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化游戏"""
|
||||||
|
super().__init__()
|
||||||
|
self.config_file = Path(__file__).parent.parent / "data" / "ai_config.json"
|
||||||
|
self.wait_window = 10 # 固定10秒等待窗口
|
||||||
|
|
||||||
|
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
|
||||||
|
"""处理AI对话指令
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: 指令,如 ".ai 问题" 或 ".aiconfig host=xxx port=xxx model=xxx"
|
||||||
|
chat_id: 会话ID
|
||||||
|
user_id: 用户ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
回复消息
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 提取指令和参数
|
||||||
|
cmd, args = CommandParser.extract_command_args(command)
|
||||||
|
args = args.strip()
|
||||||
|
|
||||||
|
# 判断是配置指令还是AI对话指令
|
||||||
|
if cmd == '.aiconfig':
|
||||||
|
return await self._handle_config(args, chat_id, user_id)
|
||||||
|
else:
|
||||||
|
# .ai 指令
|
||||||
|
return await self._handle_ai(args, chat_id, user_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理AI对话指令错误: {e}", exc_info=True)
|
||||||
|
return f"❌ 处理指令出错: {str(e)}"
|
||||||
|
|
||||||
|
async def _handle_ai(self, content: str, chat_id: int, user_id: int) -> str:
|
||||||
|
"""处理AI对话请求
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: 消息内容
|
||||||
|
chat_id: 会话ID
|
||||||
|
user_id: 用户ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
回复消息
|
||||||
|
"""
|
||||||
|
# 如果内容为空,返回帮助信息
|
||||||
|
if not content:
|
||||||
|
return self.get_help()
|
||||||
|
|
||||||
|
# 将消息加入队列
|
||||||
|
self._add_to_queue(chat_id, user_id, content)
|
||||||
|
|
||||||
|
# 取消旧的延迟任务(如果存在)
|
||||||
|
if chat_id in _pending_tasks:
|
||||||
|
old_task = _pending_tasks[chat_id]
|
||||||
|
if not old_task.done():
|
||||||
|
old_task.cancel()
|
||||||
|
try:
|
||||||
|
await old_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 创建新的延迟任务
|
||||||
|
task = asyncio.create_task(self._delayed_response(chat_id))
|
||||||
|
_pending_tasks[chat_id] = task
|
||||||
|
|
||||||
|
return "✅ 已收到,等待10秒后回答(如有新消息将重新计时)"
|
||||||
|
|
||||||
|
async def _handle_config(self, args: str, chat_id: int, user_id: int) -> str:
|
||||||
|
"""处理配置请求
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: 配置参数,格式如 "host=localhost port=11434 model=llama3.1"
|
||||||
|
chat_id: 会话ID
|
||||||
|
user_id: 用户ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
配置确认消息
|
||||||
|
"""
|
||||||
|
if not args:
|
||||||
|
return "❌ 请提供配置参数\n\n格式:`.aiconfig host=xxx port=xxx model=xxx`\n\n示例:`.aiconfig host=localhost port=11434 model=llama3.1`"
|
||||||
|
|
||||||
|
# 解析配置参数
|
||||||
|
config_updates = {}
|
||||||
|
parts = args.split()
|
||||||
|
for part in parts:
|
||||||
|
if '=' in part:
|
||||||
|
key, value = part.split('=', 1)
|
||||||
|
key = key.strip().lower()
|
||||||
|
value = value.strip()
|
||||||
|
|
||||||
|
if key == 'host':
|
||||||
|
config_updates['host'] = value
|
||||||
|
elif key == 'port':
|
||||||
|
try:
|
||||||
|
config_updates['port'] = int(value)
|
||||||
|
except ValueError:
|
||||||
|
return f"❌ 端口号必须是数字:{value}"
|
||||||
|
elif key == 'model':
|
||||||
|
config_updates['model'] = value
|
||||||
|
else:
|
||||||
|
return f"❌ 未知的配置项:{key}\n\n支持的配置项:host, port, model"
|
||||||
|
|
||||||
|
if not config_updates:
|
||||||
|
return "❌ 未提供有效的配置参数"
|
||||||
|
|
||||||
|
# 加载现有配置
|
||||||
|
current_config = self._load_config()
|
||||||
|
|
||||||
|
# 更新配置
|
||||||
|
current_config.update(config_updates)
|
||||||
|
|
||||||
|
# 保存配置
|
||||||
|
if self._save_config(current_config):
|
||||||
|
# 清除所有ChatEngine缓存(配置变更需要重新创建)
|
||||||
|
_chat_engines.clear()
|
||||||
|
|
||||||
|
return f"✅ 配置已更新\n\n**当前配置**:\n- 地址:{current_config['host']}\n- 端口:{current_config['port']}\n- 模型:{current_config['model']}"
|
||||||
|
else:
|
||||||
|
return "❌ 保存配置失败,请稍后重试"
|
||||||
|
|
||||||
|
def _add_to_queue(self, chat_id: int, user_id: int, content: str) -> None:
|
||||||
|
"""将消息加入等待队列
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: 会话ID
|
||||||
|
user_id: 用户ID
|
||||||
|
content: 消息内容
|
||||||
|
"""
|
||||||
|
if chat_id not in _message_queues:
|
||||||
|
_message_queues[chat_id] = []
|
||||||
|
|
||||||
|
_message_queues[chat_id].append({
|
||||||
|
"user_id": user_id,
|
||||||
|
"content": content,
|
||||||
|
"timestamp": int(time.time())
|
||||||
|
})
|
||||||
|
|
||||||
|
async def _delayed_response(self, chat_id: int) -> None:
|
||||||
|
"""延迟回答任务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: 会话ID
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 等待固定时间窗口
|
||||||
|
await asyncio.sleep(self.wait_window)
|
||||||
|
|
||||||
|
# 检查队列中是否有消息
|
||||||
|
if chat_id in _message_queues and _message_queues[chat_id]:
|
||||||
|
# 生成回答
|
||||||
|
response = await self._generate_response(chat_id)
|
||||||
|
|
||||||
|
# 清空队列
|
||||||
|
_message_queues[chat_id] = []
|
||||||
|
|
||||||
|
# 发送回答
|
||||||
|
if response:
|
||||||
|
from utils.message import get_message_sender
|
||||||
|
sender = get_message_sender()
|
||||||
|
await sender.send_text(response)
|
||||||
|
|
||||||
|
# 从pending_tasks中移除任务句柄
|
||||||
|
if chat_id in _pending_tasks:
|
||||||
|
del _pending_tasks[chat_id]
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# 任务被取消,正常情况,不需要记录错误
|
||||||
|
logger.debug(f"延迟任务被取消: chat_id={chat_id}")
|
||||||
|
if chat_id in _pending_tasks:
|
||||||
|
del _pending_tasks[chat_id]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"延迟回答任务错误: {e}", exc_info=True)
|
||||||
|
if chat_id in _pending_tasks:
|
||||||
|
del _pending_tasks[chat_id]
|
||||||
|
|
||||||
|
async def _generate_response(self, chat_id: int) -> Optional[str]:
|
||||||
|
"""使用LLM生成回答
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: 会话ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
回答文本
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取队列消息
|
||||||
|
if chat_id not in _message_queues or not _message_queues[chat_id]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
messages = _message_queues[chat_id].copy()
|
||||||
|
|
||||||
|
# 获取ChatEngine实例
|
||||||
|
chat_engine = self._get_chat_engine(chat_id)
|
||||||
|
if not chat_engine:
|
||||||
|
return "❌ AI服务初始化失败,请检查配置"
|
||||||
|
|
||||||
|
# 将消息按用户角色格式化并添加到ChatMemoryBuffer
|
||||||
|
# 构建合并的消息内容(包含用户信息)
|
||||||
|
merged_content = ""
|
||||||
|
for msg in messages:
|
||||||
|
user_id = msg['user_id']
|
||||||
|
role = self._get_user_role(chat_id, user_id)
|
||||||
|
merged_content += f"[{role}]: {msg['content']}\n"
|
||||||
|
|
||||||
|
# 去掉最后的换行
|
||||||
|
merged_content = merged_content.strip()
|
||||||
|
|
||||||
|
# 调用ChatEngine生成回答
|
||||||
|
# chat_engine是一个字典,包含llm, memory, system_prompt
|
||||||
|
llm = chat_engine['llm']
|
||||||
|
memory = chat_engine['memory']
|
||||||
|
system_prompt = chat_engine['system_prompt']
|
||||||
|
|
||||||
|
# 构建完整的消息(包含系统提示和历史对话)
|
||||||
|
full_message = f"{system_prompt}\n\n{merged_content}"
|
||||||
|
|
||||||
|
# 使用LLM生成回答(同步调用,在线程池中执行)
|
||||||
|
response = await asyncio.to_thread(llm.complete, full_message)
|
||||||
|
|
||||||
|
# 返回回答文本
|
||||||
|
return str(response)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成AI回答错误: {e}", exc_info=True)
|
||||||
|
return f"❌ 生成回答时出错: {str(e)}"
|
||||||
|
|
||||||
|
def _get_chat_engine(self, chat_id: int) -> Any:
|
||||||
|
"""获取或创建ChatEngine实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: 会话ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ChatEngine实例
|
||||||
|
"""
|
||||||
|
# 检查是否已存在
|
||||||
|
if chat_id in _chat_engines:
|
||||||
|
return _chat_engines[chat_id]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 加载配置
|
||||||
|
config = self._load_config()
|
||||||
|
|
||||||
|
# 导入llama_index模块
|
||||||
|
from llama_index.llms.ollama import Ollama
|
||||||
|
from llama_index.core.memory import ChatMemoryBuffer
|
||||||
|
from llama_index.core import ChatPromptTemplate, Settings
|
||||||
|
|
||||||
|
# 创建Ollama LLM实例
|
||||||
|
llm = Ollama(
|
||||||
|
model=config['model'],
|
||||||
|
base_url=f"http://{config['host']}:{config['port']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置全局LLM
|
||||||
|
Settings.llm = llm
|
||||||
|
|
||||||
|
# 创建ChatMemoryBuffer(设置足够的token_limit确保保留30+轮对话)
|
||||||
|
memory = ChatMemoryBuffer.from_defaults(token_limit=8000)
|
||||||
|
|
||||||
|
# 系统提示
|
||||||
|
system_prompt = (
|
||||||
|
"这是一个多用户对话场景,不同用户的发言会用不同的角色标识(如'用户1'、'用户2'等)。"
|
||||||
|
"你需要理解不同用户的发言内容,并根据上下文给出合适的回复。"
|
||||||
|
"请用自然、友好的方式与用户交流。"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建对话引擎
|
||||||
|
# 由于llama_index的API可能在不同版本有变化,这里使用基本的chat接口
|
||||||
|
# 实际使用时可能需要根据llama_index的版本调整
|
||||||
|
chat_engine = {
|
||||||
|
'llm': llm,
|
||||||
|
'memory': memory,
|
||||||
|
'system_prompt': system_prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# 存储到全局字典
|
||||||
|
_chat_engines[chat_id] = chat_engine
|
||||||
|
|
||||||
|
return chat_engine
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"导入llama_index模块失败: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建ChatEngine失败: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user_role(self, chat_id: int, user_id: int) -> str:
|
||||||
|
"""获取用户角色名称(创建或获取映射)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: 会话ID
|
||||||
|
user_id: 用户ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
角色名称
|
||||||
|
"""
|
||||||
|
# 获取现有映射
|
||||||
|
user_mapping, user_count = self._get_user_mapping(chat_id)
|
||||||
|
|
||||||
|
user_id_str = str(user_id)
|
||||||
|
|
||||||
|
# 如果用户已存在,返回角色名称
|
||||||
|
if user_id_str in user_mapping:
|
||||||
|
return user_mapping[user_id_str]
|
||||||
|
|
||||||
|
# 新用户,分配角色
|
||||||
|
user_count += 1
|
||||||
|
role_name = f"用户{user_count}"
|
||||||
|
user_mapping[user_id_str] = role_name
|
||||||
|
|
||||||
|
# 保存到数据库
|
||||||
|
state_data = {
|
||||||
|
"user_mapping": user_mapping,
|
||||||
|
"user_count": user_count
|
||||||
|
}
|
||||||
|
self.db.save_game_state(chat_id, 0, 'ai_chat', state_data)
|
||||||
|
|
||||||
|
return role_name
|
||||||
|
|
||||||
|
def _get_user_mapping(self, chat_id: int) -> tuple[Dict[str, str], int]:
|
||||||
|
"""获取用户角色映射和计数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: 会话ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(用户映射字典, 用户计数)
|
||||||
|
"""
|
||||||
|
# 从数据库获取映射
|
||||||
|
state = self.db.get_game_state(chat_id, 0, 'ai_chat')
|
||||||
|
|
||||||
|
if state and state.get('state_data'):
|
||||||
|
user_mapping = state['state_data'].get('user_mapping', {})
|
||||||
|
user_count = state['state_data'].get('user_count', 0)
|
||||||
|
else:
|
||||||
|
user_mapping = {}
|
||||||
|
user_count = 0
|
||||||
|
|
||||||
|
return user_mapping, user_count
|
||||||
|
|
||||||
|
def _load_config(self) -> Dict[str, Any]:
|
||||||
|
"""从JSON文件加载配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
配置字典
|
||||||
|
"""
|
||||||
|
# 如果文件不存在,创建默认配置
|
||||||
|
if not self.config_file.exists():
|
||||||
|
default_config = {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 11434,
|
||||||
|
"model": "llama3.1"
|
||||||
|
}
|
||||||
|
self._save_config(default_config)
|
||||||
|
return default_config
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# 确保所有必需的字段存在
|
||||||
|
if 'host' not in config:
|
||||||
|
config['host'] = "localhost"
|
||||||
|
if 'port' not in config:
|
||||||
|
config['port'] = 11434
|
||||||
|
if 'model' not in config:
|
||||||
|
config['model'] = "llama3.1"
|
||||||
|
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"加载配置文件失败: {e}", exc_info=True)
|
||||||
|
# 返回默认配置
|
||||||
|
return {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 11434,
|
||||||
|
"model": "llama3.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
def _save_config(self, config: Dict[str, Any]) -> bool:
|
||||||
|
"""保存配置到JSON文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 配置字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 确保目录存在
|
||||||
|
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 写入JSON文件
|
||||||
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存配置文件失败: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_help(self) -> str:
|
||||||
|
"""获取帮助信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
帮助文本
|
||||||
|
"""
|
||||||
|
return """## 🤖 AI对话系统帮助
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
- `.ai <问题>` - 向AI提问(支持多用户对话,等待10秒后回答)
|
||||||
|
- `.aiconfig host=xxx port=xxx model=xxx` - 配置Ollama服务地址和模型
|
||||||
|
|
||||||
|
### 配置示例
|
||||||
|
\`.aiconfig host=localhost port=11434 model=llama3.1\`
|
||||||
|
|
||||||
|
### 说明
|
||||||
|
- 多个用户可以在同一个会话中提问
|
||||||
|
- 系统会等待10秒,收集所有问题后统一回答
|
||||||
|
- 如果在等待期间有新消息,会重新计时
|
||||||
|
|
||||||
|
---
|
||||||
|
💡 提示:确保Ollama服务已启动并配置正确
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -107,6 +107,10 @@ def get_help_message() -> str:
|
|||||||
- `.赠送 <用户ID> <积分数量> [消息]` - 赠送积分
|
- `.赠送 <用户ID> <积分数量> [消息]` - 赠送积分
|
||||||
- `.送 <用户ID> <积分数量> [消息]` - 赠送积分
|
- `.送 <用户ID> <积分数量> [消息]` - 赠送积分
|
||||||
|
|
||||||
|
### 🤖 AI对话系统
|
||||||
|
- `.ai <问题>` - 向AI提问(支持多用户对话,等待10秒后回答)
|
||||||
|
- `.aiconfig host=xxx port=xxx model=xxx` - 配置Ollama服务地址和模型
|
||||||
|
|
||||||
### 其他
|
### 其他
|
||||||
- `.help` - 显示帮助
|
- `.help` - 显示帮助
|
||||||
- `.stats` - 查看个人统计
|
- `.stats` - 查看个人统计
|
||||||
|
|||||||
@@ -18,5 +18,9 @@ psutil==7.1.2
|
|||||||
# 拼音处理
|
# 拼音处理
|
||||||
pypinyin==0.51.0
|
pypinyin==0.51.0
|
||||||
|
|
||||||
|
# AI对话框架
|
||||||
|
llama-index-core>=0.10.0
|
||||||
|
llama-index-llms-ollama>=0.1.0
|
||||||
|
|
||||||
# 注意:使用Python标准库sqlite3,不引入SQLAlchemy
|
# 注意:使用Python标准库sqlite3,不引入SQLAlchemy
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,12 @@ async def handle_command(game_type: str, command: str,
|
|||||||
game = GiftGame()
|
game = GiftGame()
|
||||||
return await game.handle(command, chat_id, user_id)
|
return await game.handle(command, chat_id, user_id)
|
||||||
|
|
||||||
|
# AI对话系统
|
||||||
|
if game_type == 'ai_chat':
|
||||||
|
from games.ai_chat import AIChatGame
|
||||||
|
game = AIChatGame()
|
||||||
|
return await game.handle(command, chat_id, user_id)
|
||||||
|
|
||||||
# 未知游戏类型
|
# 未知游戏类型
|
||||||
logger.warning(f"未知游戏类型: {game_type}")
|
logger.warning(f"未知游戏类型: {game_type}")
|
||||||
return "❌ 未知的游戏类型"
|
return "❌ 未知的游戏类型"
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ class CommandParser:
|
|||||||
'.赠送': 'gift',
|
'.赠送': 'gift',
|
||||||
'.送': 'gift',
|
'.送': 'gift',
|
||||||
|
|
||||||
|
# AI对话系统
|
||||||
|
'.ai': 'ai_chat',
|
||||||
|
'.aiconfig': 'ai_chat',
|
||||||
|
|
||||||
# 帮助
|
# 帮助
|
||||||
'.help': 'help',
|
'.help': 'help',
|
||||||
'.帮助': 'help',
|
'.帮助': 'help',
|
||||||
@@ -100,6 +104,12 @@ class CommandParser:
|
|||||||
# 返回游戏类型和完整指令
|
# 返回游戏类型和完整指令
|
||||||
return game_type, content
|
return game_type, content
|
||||||
|
|
||||||
|
# 特殊处理:.ai 和 .aiconfig 指令支持参数
|
||||||
|
if content.startswith('.ai '):
|
||||||
|
return 'ai_chat', content
|
||||||
|
if content.startswith('.aiconfig '):
|
||||||
|
return 'ai_chat', content
|
||||||
|
|
||||||
# 没有匹配的指令
|
# 没有匹配的指令
|
||||||
logger.debug(f"未识别的指令: {content}")
|
logger.debug(f"未识别的指令: {content}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user