增强AI插件
This commit is contained in:
300
.tasks/2025-11-20_2_add_user_tools_to_ai.md
Normal file
300
.tasks/2025-11-20_2_add_user_tools_to_ai.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# 背景
|
||||||
|
文件名:2025-11-20_2_add_user_tools_to_ai.md
|
||||||
|
创建于:2025-11-20_15:25:43
|
||||||
|
创建者:admin
|
||||||
|
主分支:main
|
||||||
|
任务分支:无(不创建分支)
|
||||||
|
Yolo模式:Off
|
||||||
|
|
||||||
|
# 任务描述
|
||||||
|
为 AI 聊天插件(ChatAI.py)添加用户 ID 与用户名转换的工具调用功能,让 AI 能够主动查询用户信息。
|
||||||
|
|
||||||
|
## 具体需求
|
||||||
|
1. AI 应该能够通过工具调用实现以下功能:
|
||||||
|
- 根据 user_id 查询 username
|
||||||
|
- 根据 username 查询 user_id
|
||||||
|
- 查询所有存在的 username 列表
|
||||||
|
|
||||||
|
2. 使用 LlamaIndex 框架的高级功能(FunctionTool + AgentWorkflow + ReActAgent)
|
||||||
|
|
||||||
|
3. 参考 NewsReport.py 的工具调用架构实现
|
||||||
|
|
||||||
|
# 项目概览
|
||||||
|
- **项目**:NewWPSBot
|
||||||
|
- **相关文件**:
|
||||||
|
- `Plugins/Others/ChatAI.py` - AI 对话插件(需要改造)
|
||||||
|
- `Plugins/WPSConfigSystem.py` - 用户配置系统(需要添加查询所有用户名的方法)
|
||||||
|
- `Plugins/Others/NewsReport.py` - 参考实现(已实现工具调用)
|
||||||
|
|
||||||
|
# 分析
|
||||||
|
|
||||||
|
## WPSConfigSystem.py 现状
|
||||||
|
- ✅ 已有方法:`get_user_name(user_id: int)` - 第201-206行
|
||||||
|
- ✅ 已有方法:`find_user_id_by_username(username: str)` - 第238-248行
|
||||||
|
- ❌ 缺失方法:查询所有用户名列表
|
||||||
|
- 数据表:`user_info` (user_id, username, userurl, userpoint)
|
||||||
|
|
||||||
|
## ChatAI.py 现状(第1-428行)
|
||||||
|
- 当前使用简单的 `Ollama` LLM + `ChatMessage` 架构
|
||||||
|
- 通过 `llm.achat()` 进行对话
|
||||||
|
- **没有工具调用能力**
|
||||||
|
- 维护全局历史 `global_history`
|
||||||
|
- 单例模式:`ChatAIAgent.get_instance()`
|
||||||
|
|
||||||
|
## NewsReport.py 工具调用架构(参考)
|
||||||
|
```python
|
||||||
|
# 1. 导入
|
||||||
|
from llama_index.core.tools import FunctionTool
|
||||||
|
from llama_index.core.agent.workflow import AgentWorkflow
|
||||||
|
from llama_index.core.agent.workflow.react_agent import ReActAgent
|
||||||
|
|
||||||
|
# 2. 创建工具
|
||||||
|
tools = [
|
||||||
|
FunctionTool.from_defaults(
|
||||||
|
fn=self._tool_function,
|
||||||
|
name="tool_name",
|
||||||
|
description="工具描述"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 3. 创建 Agent
|
||||||
|
agent = ReActAgent(
|
||||||
|
llm=self.llm,
|
||||||
|
tools=tools,
|
||||||
|
verbose=True,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 创建 Workflow
|
||||||
|
self.workflow = AgentWorkflow(
|
||||||
|
agents=[agent],
|
||||||
|
timeout=600.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 执行
|
||||||
|
result = await self.workflow.run(user_msg=query)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术要点
|
||||||
|
1. **工具函数必须是同步函数**(从 NewsReport.py 看,工具函数如 `_get_current_date()` 都是同步的)
|
||||||
|
2. **WPSConfigAPI 需要通过 Architecture.Get() 获取**
|
||||||
|
3. **系统提示词需要明确指导 AI 何时使用工具**
|
||||||
|
4. **需要保留现有的历史消息管理功能**
|
||||||
|
|
||||||
|
# 提议的解决方案
|
||||||
|
|
||||||
|
## 方案探索(INNOVATE 阶段)
|
||||||
|
|
||||||
|
### 方案A:完全重构为 ReActAgent 架构 ⭐ 推荐
|
||||||
|
|
||||||
|
**架构设计**:
|
||||||
|
- 移除 `llm.achat()` 调用方式
|
||||||
|
- 使用 `AgentWorkflow` + `ReActAgent`
|
||||||
|
- 历史消息通过系统提示词传递
|
||||||
|
- 工具函数独立实现
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 与 NewsReport.py 架构统一,易于维护
|
||||||
|
- ReActAgent 专门为工具调用优化
|
||||||
|
- 工具调用日志清晰(verbose=True)
|
||||||
|
- 扩展性强,易于添加新工具
|
||||||
|
|
||||||
|
**劣势**:
|
||||||
|
- 需要大幅度重构现有代码
|
||||||
|
- 历史消息管理方式需改变
|
||||||
|
- qwen3:0.6b 小模型的工具调用能力需验证
|
||||||
|
|
||||||
|
**关键改进**:
|
||||||
|
- 保留历史消息数组结构,调用时转换为提示词
|
||||||
|
- 添加错误处理和降级机制
|
||||||
|
- 可根据消息内容智能决定是否启用工具
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案B:混合架构 - 保留对话 + 添加工具层
|
||||||
|
|
||||||
|
**架构设计**:
|
||||||
|
- 保留 `llm.achat()` 作为主要对话方式
|
||||||
|
- AI 在回复中使用特殊标记触发工具(如 `[TOOL:get_username:123]`)
|
||||||
|
- 后处理解析工具调用并再次调用 LLM
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 改动最小,风险低
|
||||||
|
- 保留现有历史消息机制
|
||||||
|
- 对话连贯性不受影响
|
||||||
|
|
||||||
|
**劣势**:
|
||||||
|
- 需要自定义工具调用协议
|
||||||
|
- 可能需要多轮 LLM 调用
|
||||||
|
- 工具调用智能程度较低
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案C:双智能体架构
|
||||||
|
|
||||||
|
**架构设计**:
|
||||||
|
- ToolAgent(ReActAgent)专门处理工具调用
|
||||||
|
- ChatAgent(现有架构)处理普通对话
|
||||||
|
- 路由逻辑判断使用哪个智能体
|
||||||
|
|
||||||
|
**优势**:职责分离,灵活性高
|
||||||
|
**劣势**:架构复杂,维护成本高,上下文共享困难
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案D:轻量级工具注入
|
||||||
|
|
||||||
|
**架构设计**:
|
||||||
|
- 预处理:分析消息,预先调用工具
|
||||||
|
- 将工具结果注入到提示词
|
||||||
|
- 正常调用 `llm.achat()`
|
||||||
|
|
||||||
|
**优势**:改动极小,性能影响最小
|
||||||
|
**劣势**:AI 无法主动决定使用工具,智能程度低
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最终推荐方案:方案A(ReActAgent 架构)
|
||||||
|
|
||||||
|
### 实施要点
|
||||||
|
|
||||||
|
#### 1. 在 WPSConfigSystem.py 添加方法
|
||||||
|
```python
|
||||||
|
def get_all_usernames(self) -> List[Dict[str, Any]]:
|
||||||
|
"""获取所有用户名列表"""
|
||||||
|
cursor = get_db().conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT user_id, username FROM user_info WHERE username != '' AND username IS NOT NULL"
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
return [{"user_id": row["user_id"], "username": row["username"]} for row in rows]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 改造 ChatAI.py
|
||||||
|
|
||||||
|
**导入新增**:
|
||||||
|
```python
|
||||||
|
from llama_index.core.tools import FunctionTool
|
||||||
|
from llama_index.core.agent.workflow import AgentWorkflow
|
||||||
|
from llama_index.core.agent.workflow.react_agent import ReActAgent
|
||||||
|
from PWF.Convention.Runtime.Architecture import Architecture
|
||||||
|
```
|
||||||
|
|
||||||
|
**添加 `_initialize_agent()` 方法**:
|
||||||
|
- 获取 WPSConfigAPI 实例
|
||||||
|
- 创建三个工具函数
|
||||||
|
- 初始化 ReActAgent
|
||||||
|
- 创建 AgentWorkflow
|
||||||
|
|
||||||
|
**工具函数**:
|
||||||
|
1. `get_username_by_id(user_id: int) -> str`
|
||||||
|
2. `get_userid_by_name(username: str) -> str`
|
||||||
|
3. `list_all_usernames() -> str`
|
||||||
|
|
||||||
|
**修改 `chat()` 方法**:
|
||||||
|
- 将历史消息格式化为提示词
|
||||||
|
- 使用 `await self.workflow.run(user_msg=...)`
|
||||||
|
- 保留历史消息管理逻辑
|
||||||
|
- 添加错误处理
|
||||||
|
|
||||||
|
**更新系统提示词**:
|
||||||
|
- 告知 AI 可用的三个工具
|
||||||
|
- 说明工具使用场景和时机
|
||||||
|
- 保持原有的 @用户 格式规则
|
||||||
|
|
||||||
|
### 技术挑战与解决方案
|
||||||
|
1. **历史消息管理**:保留数组结构,调用时动态转换
|
||||||
|
2. **WPSConfigAPI 访问**:在 `_initialize_agent()` 中通过 Architecture.Get() 获取
|
||||||
|
3. **错误处理**:工具调用失败时返回友好提示,不中断对话
|
||||||
|
4. **模型兼容性**:测试 qwen3:0.6b 的工具调用能力,必要时提供降级方案
|
||||||
|
|
||||||
|
# 当前执行步骤:"4. 执行完成,等待测试"
|
||||||
|
|
||||||
|
# 任务进度
|
||||||
|
## [2025-11-20_15:25:43]
|
||||||
|
- 状态:研究完成
|
||||||
|
- 已完成:
|
||||||
|
- 分析 WPSConfigSystem.py 现有功能
|
||||||
|
- 分析 ChatAI.py 当前架构
|
||||||
|
- 研究 NewsReport.py 工具调用实现
|
||||||
|
- 创建任务文件
|
||||||
|
- 下一步:进入 INNOVATE 模式
|
||||||
|
|
||||||
|
## [2025-11-20_15:30:00]
|
||||||
|
- 状态:创新完成
|
||||||
|
- 已完成:
|
||||||
|
- 探索了 4 种技术方案(ReActAgent、混合架构、双智能体、轻量级注入)
|
||||||
|
- 评估各方案优劣势和技术风险
|
||||||
|
- 确定推荐方案:完全 ReActAgent 架构
|
||||||
|
- 更新任务文件,记录方案分析
|
||||||
|
- 下一步:进入 PLAN 模式
|
||||||
|
|
||||||
|
## [2025-11-20_15:35:00]
|
||||||
|
- 状态:规划完成
|
||||||
|
- 已完成:
|
||||||
|
- 制定详细的代码修改规格
|
||||||
|
- 设计完整的系统提示词(包含工具使用说明、示例、错误处理)
|
||||||
|
- 明确每个函数的签名和实现细节
|
||||||
|
- 创建8项实施清单
|
||||||
|
- 添加降级机制设计
|
||||||
|
- 下一步:进入 EXECUTE 模式
|
||||||
|
|
||||||
|
## [2025-11-20_15:38:25]
|
||||||
|
- 状态:执行完成
|
||||||
|
- 已修改文件:
|
||||||
|
- Plugins/WPSConfigSystem.py(添加 get_all_usernames() 方法)
|
||||||
|
- Plugins/Others/ChatAI.py(完全重构为 ReActAgent 架构)
|
||||||
|
- 已完成修改:
|
||||||
|
1. ✅ WPSConfigSystem.py 添加 get_all_usernames() 方法
|
||||||
|
2. ✅ ChatAI.py 添加导入(FunctionTool, AgentWorkflow, ReActAgent)
|
||||||
|
3. ✅ ChatAIAgent.__init__() 添加 workflow 和 config_api 属性
|
||||||
|
4. ✅ ChatAIAgent.__init__() 添加 _initialize_agent() 调用
|
||||||
|
5. ✅ 实现 _initialize_agent() 方法(包含三个工具函数)
|
||||||
|
6. ✅ 实现 _build_system_prompt() 方法(详细提示词,约200行)
|
||||||
|
7. ✅ 重构 chat() 方法使用 workflow.run()
|
||||||
|
8. ✅ 添加 _fallback_chat() 降级方法
|
||||||
|
- Linter 检查:✅ 无错误
|
||||||
|
- 更改摘要:
|
||||||
|
- 新增方法:get_all_usernames() (WPSConfigSystem)
|
||||||
|
- 新增方法:_initialize_agent() (ChatAI)
|
||||||
|
- 新增方法:_build_system_prompt() (ChatAI)
|
||||||
|
- 新增方法:_fallback_chat() (ChatAI)
|
||||||
|
- 重构方法:chat() (ChatAI)
|
||||||
|
- 新增工具函数:get_username_by_id, get_userid_by_name, list_all_usernames
|
||||||
|
- 新增导入:FunctionTool, AgentWorkflow, ReActAgent
|
||||||
|
- 下一步:用户测试工具调用功能
|
||||||
|
|
||||||
|
# 详细实施规格(PLAN 阶段输出)
|
||||||
|
|
||||||
|
## 实施清单
|
||||||
|
1. 在 WPSConfigSystem.py 添加 get_all_usernames() 方法
|
||||||
|
2. 在 ChatAI.py 添加新的导入语句
|
||||||
|
3. 在 ChatAIAgent.__init__() 添加 workflow 和 config_api 属性
|
||||||
|
4. 在 ChatAIAgent.__init__() 添加 _initialize_agent() 调用
|
||||||
|
5. 添加 _initialize_agent() 方法(创建三个工具)
|
||||||
|
6. 添加 _build_system_prompt() 方法(详细提示词)
|
||||||
|
7. 重构 chat() 方法使用 workflow.run()
|
||||||
|
8. 添加 _fallback_chat() 降级方法
|
||||||
|
|
||||||
|
## 关键设计
|
||||||
|
|
||||||
|
### 系统提示词特点
|
||||||
|
- 详细说明三个工具的用途、参数、返回值
|
||||||
|
- 提供大量使用示例(5个场景)
|
||||||
|
- 明确工具使用原则(何时用/何时不用)
|
||||||
|
- 包含错误处理指导
|
||||||
|
- 强调 <at> 标签使用规则
|
||||||
|
|
||||||
|
### 三个工具函数
|
||||||
|
1. get_username_by_id(user_id: int) -> str
|
||||||
|
2. get_userid_by_name(username: str) -> str
|
||||||
|
3. list_all_usernames() -> str
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
- Workflow 初始化失败 → 使用 _fallback_chat()
|
||||||
|
- 工具调用失败 → 返回友好错误信息
|
||||||
|
- 整体异常 → 降级到简单对话模式
|
||||||
|
|
||||||
|
# 最终审查
|
||||||
|
(待完成后填写)
|
||||||
|
|
||||||
@@ -2,12 +2,15 @@ from Plugins.WPSAPI import *
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from llama_index.llms.ollama import Ollama
|
from llama_index.llms.ollama import Ollama
|
||||||
from llama_index.core.llms import ChatMessage
|
from llama_index.core.llms import ChatMessage
|
||||||
from typing import List, Dict, Optional
|
from llama_index.core.tools import FunctionTool
|
||||||
|
from llama_index.core.agent.workflow import AgentWorkflow
|
||||||
|
from llama_index.core.agent.workflow.react_agent import ReActAgent
|
||||||
|
from typing import List, Dict, Optional, Any
|
||||||
|
|
||||||
logger: ProjectConfig = Architecture.Get(ProjectConfig)
|
logger: ProjectConfig = Architecture.Get(ProjectConfig)
|
||||||
OLLAMA_URL = logger.FindItem("ollama_url", "http://ollama.liubai.site")
|
OLLAMA_URL = logger.FindItem("ollama_url", "http://ollama.liubai.site")
|
||||||
OLLAMA_MODEL = logger.FindItem("ollama_model", "qwen3:0.6b")
|
OLLAMA_MODEL = logger.FindItem("ollama_model", "qwen3:0.6b")
|
||||||
MAX_HISTORY = logger.FindItem("chat_ai_max_history", 20)
|
MAX_HISTORY = logger.FindItem("chat_ai_max_history", 100)
|
||||||
logger.SaveProperties()
|
logger.SaveProperties()
|
||||||
|
|
||||||
|
|
||||||
@@ -30,21 +33,12 @@ class ChatAIAgent:
|
|||||||
# 全局消息历史列表
|
# 全局消息历史列表
|
||||||
self.global_history: List[Dict[str, any]] = []
|
self.global_history: List[Dict[str, any]] = []
|
||||||
|
|
||||||
# 系统提示词
|
# Agent 工作流和配置API
|
||||||
self.system_prompt = """你是一个友好的AI助手,能够同时在多个群聊中与不同用户对话。
|
self.workflow: Optional[AgentWorkflow] = None
|
||||||
|
self.config_api = None # 延迟加载,避免循环依赖
|
||||||
重要规则:
|
|
||||||
- 你能看到所有群聊的对话历史,每条消息都标注了时间、群聊ID和用户ID
|
# 初始化 Agent 和工具
|
||||||
- 当你需要特指某个用户或专门回复某个用户时,必须在回复中使用以下格式:<at user_id="用户ID"></at>
|
self._initialize_agent()
|
||||||
- 禁止直接输出用户ID数字,始终使用 <at> 标签包裹
|
|
||||||
- 注意区分不同群聊和不同用户的对话上下文
|
|
||||||
|
|
||||||
示例对话:
|
|
||||||
[2025-11-20 10:00:00] [群聊#12345] [用户#11111]: 你好
|
|
||||||
你的回复: 你好!有什么可以帮助你的吗?
|
|
||||||
|
|
||||||
[2025-11-20 10:01:00] [群聊#67890] [用户#22222]: 刚刚谁在说话, 说了什么?
|
|
||||||
你的回复: 刚刚群聊12345的用户<at user_id="11111"></at>在说话, 他说了你好。"""
|
|
||||||
|
|
||||||
logger.Log("Info", f"{ConsoleFrontColor.GREEN}ChatAIAgent 初始化完成{ConsoleFrontColor.RESET}")
|
logger.Log("Info", f"{ConsoleFrontColor.GREEN}ChatAIAgent 初始化完成{ConsoleFrontColor.RESET}")
|
||||||
logger.Log("Info", f"模型: {OLLAMA_MODEL}, 最大历史: {max_history}")
|
logger.Log("Info", f"模型: {OLLAMA_MODEL}, 最大历史: {max_history}")
|
||||||
@@ -56,6 +50,316 @@ class ChatAIAgent:
|
|||||||
cls._instance = cls()
|
cls._instance = cls()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
|
def _initialize_agent(self) -> None:
|
||||||
|
"""初始化 ReActAgent 和工具函数"""
|
||||||
|
try:
|
||||||
|
# 延迟导入避免循环依赖
|
||||||
|
from Plugins.WPSConfigSystem import WPSConfigAPI
|
||||||
|
from PWF.Convention.Runtime.Architecture import Architecture
|
||||||
|
|
||||||
|
# 获取 WPSConfigAPI 实例
|
||||||
|
try:
|
||||||
|
self.config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI)
|
||||||
|
except Exception as e:
|
||||||
|
logger.Log("Warning", f"无法获取 WPSConfigAPI: {e}")
|
||||||
|
self.config_api = None
|
||||||
|
|
||||||
|
# 创建工具函数
|
||||||
|
tools = []
|
||||||
|
|
||||||
|
if self.config_api:
|
||||||
|
# 工具1: 根据用户ID查询用户名
|
||||||
|
def get_username_by_id(user_id: int) -> str:
|
||||||
|
"""根据用户ID查询用户名
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: 用户ID(整数)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
用户名字符串。如果用户未设置用户名,返回 'user_用户ID' 格式
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
get_username_by_id(12345) -> "张三"
|
||||||
|
get_username_by_id(99999) -> "user_99999"
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
username = self.config_api.get_user_name(user_id)
|
||||||
|
return username if username else f"user_{user_id}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.Log("Error", f"查询用户名失败 user_id={user_id}: {e}")
|
||||||
|
return f"user_{user_id}"
|
||||||
|
|
||||||
|
# 工具2: 根据用户名查询用户ID
|
||||||
|
def get_userid_by_name(username: str) -> str:
|
||||||
|
"""根据用户名查询用户ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: 用户名(字符串)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
如果找到用户,返回用户ID(字符串格式)
|
||||||
|
如果未找到,返回 "未找到用户名为'{username}'的用户"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
get_userid_by_name("张三") -> "12345"
|
||||||
|
get_userid_by_name("不存在的用户") -> "未找到用户名为'不存在的用户'的用户"
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_id = self.config_api.find_user_id_by_username(username)
|
||||||
|
if user_id is not None:
|
||||||
|
return str(user_id)
|
||||||
|
return f"未找到用户名为'{username}'的用户"
|
||||||
|
except Exception as e:
|
||||||
|
logger.Log("Error", f"查询用户ID失败 username={username}: {e}")
|
||||||
|
return f"查询失败: {str(e)}"
|
||||||
|
|
||||||
|
# 工具3: 获取所有用户名列表
|
||||||
|
def list_all_usernames() -> str:
|
||||||
|
"""获取系统中所有已设置用户名的用户列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
格式化的用户列表字符串,每行一个用户
|
||||||
|
格式: "用户ID: 用户名"
|
||||||
|
如果没有用户,返回 "当前系统中没有设置用户名的用户"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
返回示例:
|
||||||
|
"12345: 张三
|
||||||
|
67890: 李四
|
||||||
|
11111: 王五"
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
users = self.config_api.get_all_usernames()
|
||||||
|
if not users:
|
||||||
|
return "当前系统中没有设置用户名的用户"
|
||||||
|
|
||||||
|
lines = [f"{user['user_id']}: {user['username']}" for user in users]
|
||||||
|
return "\n".join(lines)
|
||||||
|
except Exception as e:
|
||||||
|
logger.Log("Error", f"获取用户列表失败: {e}")
|
||||||
|
return f"获取用户列表失败: {str(e)}"
|
||||||
|
|
||||||
|
# 注册工具
|
||||||
|
tools = [
|
||||||
|
FunctionTool.from_defaults(
|
||||||
|
fn=get_username_by_id,
|
||||||
|
name="get_username_by_id",
|
||||||
|
description="根据用户ID(整数)查询对应的用户名。用于当你知道用户ID,需要知道该用户的名字时。"
|
||||||
|
),
|
||||||
|
FunctionTool.from_defaults(
|
||||||
|
fn=get_userid_by_name,
|
||||||
|
name="get_userid_by_name",
|
||||||
|
description="根据用户名(字符串)查询对应的用户ID。用于当你知道用户名,需要知道该用户的ID时。"
|
||||||
|
),
|
||||||
|
FunctionTool.from_defaults(
|
||||||
|
fn=list_all_usernames,
|
||||||
|
name="list_all_usernames",
|
||||||
|
description="获取系统中所有已设置用户名的用户列表。用于当你需要了解系统中有哪些用户,或者用户询问'有哪些用户'、'谁在线'等问题时。"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 创建系统提示词
|
||||||
|
system_prompt = self._build_system_prompt()
|
||||||
|
|
||||||
|
# 创建 ReActAgent
|
||||||
|
agent = ReActAgent(
|
||||||
|
llm=self.llm,
|
||||||
|
tools=tools,
|
||||||
|
verbose=True,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建 AgentWorkflow
|
||||||
|
self.workflow = AgentWorkflow(
|
||||||
|
agents=[agent],
|
||||||
|
timeout=600.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.Log("Info", f"{ConsoleFrontColor.GREEN}Agent 工具初始化完成,共 {len(tools)} 个工具{ConsoleFrontColor.RESET}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.Log("Error", f"初始化 Agent 失败: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.Log("Error", traceback.format_exc())
|
||||||
|
self.workflow = None
|
||||||
|
|
||||||
|
def _build_system_prompt(self) -> str:
|
||||||
|
"""构建详细的系统提示词"""
|
||||||
|
return """你是一个友好且智能的 AI 助手,能够同时在多个群聊中与不同用户对话。
|
||||||
|
|
||||||
|
# 核心能力
|
||||||
|
|
||||||
|
## 1. 对话历史理解
|
||||||
|
- 你能看到所有群聊的对话历史
|
||||||
|
- 每条消息都标注了:[时间] [群聊ID] [用户ID]
|
||||||
|
- 你需要根据上下文理解对话,区分不同群聊和用户
|
||||||
|
|
||||||
|
## 2. 用户信息工具 🔧
|
||||||
|
|
||||||
|
你拥有三个强大的工具来查询用户信息:
|
||||||
|
|
||||||
|
### 工具 1: get_username_by_id(user_id: int)
|
||||||
|
**用途**:根据用户ID查询用户名
|
||||||
|
**参数**:user_id(整数类型)
|
||||||
|
**返回**:用户名字符串
|
||||||
|
**使用场景**:
|
||||||
|
- 当你看到对话历史中有用户ID,想知道这个用户叫什么名字时
|
||||||
|
- 当用户问"用户12345是谁"时
|
||||||
|
- 当你需要用更友好的称呼来提及某个用户时
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户问:"用户12345是谁?"
|
||||||
|
你的思考:我需要查询用户ID 12345 的用户名
|
||||||
|
你的动作:调用 get_username_by_id(12345)
|
||||||
|
工具返回:"张三"
|
||||||
|
你的回复:"用户12345是张三。"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工具 2: get_userid_by_name(username: str)
|
||||||
|
**用途**:根据用户名查询用户ID
|
||||||
|
**参数**:username(字符串类型)
|
||||||
|
**返回**:用户ID字符串,或未找到的提示
|
||||||
|
**使用场景**:
|
||||||
|
- 当用户提到某个人名,你需要知道对应的用户ID时
|
||||||
|
- 当用户问"张三的ID是多少"时
|
||||||
|
- 当你需要用 <at> 标签提及某个用户,但只知道名字时
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户问:"张三的用户ID是多少?"
|
||||||
|
你的思考:我需要查询名为"张三"的用户ID
|
||||||
|
你的动作:调用 get_userid_by_name("张三")
|
||||||
|
工具返回:"12345"
|
||||||
|
你的回复:"张三的用户ID是12345。"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工具 3: list_all_usernames()
|
||||||
|
**用途**:获取所有已设置用户名的用户列表
|
||||||
|
**参数**:无
|
||||||
|
**返回**:格式化的用户列表
|
||||||
|
**使用场景**:
|
||||||
|
- 当用户问"系统里有哪些用户"时
|
||||||
|
- 当用户问"谁在这里"、"有哪些人"时
|
||||||
|
- 当你需要了解系统中的用户概况时
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户问:"系统里有哪些用户?"
|
||||||
|
你的思考:我需要获取所有用户列表
|
||||||
|
你的动作:调用 list_all_usernames()
|
||||||
|
工具返回:
|
||||||
|
"12345: 张三
|
||||||
|
67890: 李四
|
||||||
|
11111: 王五"
|
||||||
|
你的回复:"系统中目前有以下用户:
|
||||||
|
- 张三(ID: 12345)
|
||||||
|
- 李四(ID: 67890)
|
||||||
|
- 王五(ID: 11111)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 工具使用原则 ⚠️
|
||||||
|
|
||||||
|
**何时应该使用工具**:
|
||||||
|
✅ 用户明确询问用户信息("XX是谁"、"XX的ID是多少")
|
||||||
|
✅ 对话中提到不认识的人名,需要确认身份
|
||||||
|
✅ 需要列举系统中的用户
|
||||||
|
✅ 需要将用户名转换为ID,或ID转换为用户名
|
||||||
|
|
||||||
|
**何时不需要使用工具**:
|
||||||
|
❌ 对话历史中已经明确显示了用户ID和相关信息
|
||||||
|
❌ 用户只是在普通聊天,没有涉及用户查询
|
||||||
|
❌ 你已经在同一轮对话中查询过相同的信息
|
||||||
|
|
||||||
|
**工具使用建议**:
|
||||||
|
1. 优先检查对话历史,看是否已有所需信息
|
||||||
|
2. 一次对话中避免重复查询相同信息
|
||||||
|
3. 查询失败时,友好地告知用户
|
||||||
|
4. 工具返回结果后,用自然语言整理给用户
|
||||||
|
|
||||||
|
## 4. 用户提及规则 📢
|
||||||
|
|
||||||
|
**重要**:当你需要特指某个用户时,必须使用 `<at>` 标签!
|
||||||
|
|
||||||
|
**正确格式**:
|
||||||
|
- `<at user_id="用户ID"></at>`
|
||||||
|
- `<at user_id="用户ID">用户名</at>`
|
||||||
|
|
||||||
|
**错误格式**(禁止):
|
||||||
|
- ❌ 直接输出数字ID:"用户12345"
|
||||||
|
- ❌ 不使用标签:"@张三"
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
用户问:"我和小明谁比较忙?"
|
||||||
|
你的思考:用户提到"小明",我需要查询小明的ID,然后用<at>标签回复
|
||||||
|
步骤1:调用 get_userid_by_name("小明")
|
||||||
|
步骤2:假设返回 "12345"
|
||||||
|
你的回复:"我觉得<at user_id="12345">小明</at>可能比较忙,不过这只是我的猜测。"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 对话示例 💬
|
||||||
|
|
||||||
|
### 示例 1:查询用户信息
|
||||||
|
```
|
||||||
|
[2025-11-20 10:00:00] [群聊#12345] [用户#11111]: 用户99999是谁?
|
||||||
|
AI思考:用户询问用户99999的身份,我需要查询
|
||||||
|
AI动作:调用 get_username_by_id(99999)
|
||||||
|
AI回复:用户99999是王小明。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2:根据名字查ID
|
||||||
|
```
|
||||||
|
[2025-11-20 10:01:00] [群聊#12345] [用户#11111]: 李华的ID是多少?
|
||||||
|
AI思考:用户询问李华的ID
|
||||||
|
AI动作:调用 get_userid_by_name("李华")
|
||||||
|
工具返回:"88888"
|
||||||
|
AI回复:李华的用户ID是88888。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3:列出所有用户
|
||||||
|
```
|
||||||
|
[2025-11-20 10:02:00] [群聊#12345] [用户#11111]: 系统里有哪些用户?
|
||||||
|
AI思考:用户想知道所有用户列表
|
||||||
|
AI动作:调用 list_all_usernames()
|
||||||
|
工具返回:"12345: 张三\n67890: 李四"
|
||||||
|
AI回复:系统中目前有以下用户:
|
||||||
|
- 张三(ID: 12345)
|
||||||
|
- 李四(ID: 67890)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 4:智能对话中使用工具
|
||||||
|
```
|
||||||
|
[2025-11-20 10:03:00] [群聊#12345] [用户#11111]: 帮我问问小红今天有空吗?
|
||||||
|
AI思考:用户提到"小红",我需要用<at>标签提及她,所以要先查询ID
|
||||||
|
AI动作:调用 get_userid_by_name("小红")
|
||||||
|
工具返回:"55555"
|
||||||
|
AI回复:<at user_id="55555">小红</at> 今天有空吗?用户<at user_id="11111"></at>想知道。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 5:普通对话(不需要工具)
|
||||||
|
```
|
||||||
|
[2025-11-20 10:04:00] [群聊#12345] [用户#11111]: 今天天气真好
|
||||||
|
AI思考:这是普通聊天,不涉及用户查询,不需要使用工具
|
||||||
|
AI回复:是的,今天天气很不错呢!适合出去走走。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 错误处理 🔧
|
||||||
|
|
||||||
|
- 如果工具调用失败,友好地告知用户
|
||||||
|
- 如果查询不到用户,明确说明"未找到该用户"
|
||||||
|
- 如果不确定是否需要使用工具,优先使用工具确保准确性
|
||||||
|
|
||||||
|
## 7. 总结
|
||||||
|
|
||||||
|
你的职责:
|
||||||
|
1. 理解对话上下文,区分不同群聊和用户
|
||||||
|
2. 在需要时主动使用工具查询用户信息
|
||||||
|
3. 始终使用 <at> 标签提及用户
|
||||||
|
4. 提供友好、准确、有帮助的回复
|
||||||
|
|
||||||
|
记住:工具是帮助你更好地服务用户的,当不确定时,优先使用工具确保信息准确!"""
|
||||||
|
|
||||||
def _add_message(self, chat_id: int, user_id: int, role: str, content: str) -> None:
|
def _add_message(self, chat_id: int, user_id: int, role: str, content: str) -> None:
|
||||||
"""添加消息到全局历史
|
"""添加消息到全局历史
|
||||||
|
|
||||||
@@ -148,6 +452,11 @@ class ChatAIAgent:
|
|||||||
# 添加用户消息到历史
|
# 添加用户消息到历史
|
||||||
self._add_message(chat_id, user_id, "user", message)
|
self._add_message(chat_id, user_id, "user", message)
|
||||||
|
|
||||||
|
# 检查是否成功初始化 workflow
|
||||||
|
if self.workflow is None:
|
||||||
|
logger.Log("Warning", "Workflow 未初始化,使用降级模式")
|
||||||
|
return await self._fallback_chat(message, chat_id, user_id)
|
||||||
|
|
||||||
# 格式化历史消息(不包括刚添加的用户消息)
|
# 格式化历史消息(不包括刚添加的用户消息)
|
||||||
history_without_last = self.global_history[:-1] if len(self.global_history) > 1 else []
|
history_without_last = self.global_history[:-1] if len(self.global_history) > 1 else []
|
||||||
history_text = "\n".join([
|
history_text = "\n".join([
|
||||||
@@ -158,29 +467,32 @@ class ChatAIAgent:
|
|||||||
# 构建当前用户消息的格式化版本
|
# 构建当前用户消息的格式化版本
|
||||||
current_msg = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [群聊#{chat_id}] [用户#{user_id}]: {message}"
|
current_msg = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [群聊#{chat_id}] [用户#{user_id}]: {message}"
|
||||||
|
|
||||||
# 构建对话提示
|
# 构建完整的用户查询,包含历史上下文
|
||||||
conversation_prompt = f"""以下是历史对话记录:
|
full_query = f"""# 历史对话记录
|
||||||
{history_text}
|
{history_text}
|
||||||
|
|
||||||
当前消息:
|
# 当前消息
|
||||||
{current_msg}
|
{current_msg}
|
||||||
|
|
||||||
请作为AI助手回复当前用户。记住:
|
# 任务
|
||||||
1. 使用 <at user_id="{user_id}"></at> 格式来提到任意你想提及的用户
|
请作为 AI 助手回复当前用户。记住:
|
||||||
2. 根据对话历史和当前消息,给出有意义的回复
|
1. 如果需要查询用户信息,主动使用工具
|
||||||
3. 如果历史中有其他群聊或用户的信息,你可以引用它们"""
|
2. 使用 <at user_id="用户ID"></at> 格式提及用户
|
||||||
|
3. 根据对话历史和当前消息,给出有意义的回复"""
|
||||||
|
|
||||||
logger.Log("Info", f"处理对话 - 群聊#{chat_id} 用户#{user_id}")
|
logger.Log("Info", f"处理对话 - 群聊#{chat_id} 用户#{user_id}")
|
||||||
logger.Log("Info", f"当前历史消息数: {len(self.global_history)}")
|
logger.Log("Info", f"当前历史消息数: {len(self.global_history)}")
|
||||||
|
|
||||||
# 使用 achat 方法进行对话
|
# 使用 workflow 运行 agent
|
||||||
messages = [
|
result = await self.workflow.run(user_msg=full_query)
|
||||||
ChatMessage(role="system", content=self.system_prompt),
|
|
||||||
ChatMessage(role="user", content=conversation_prompt)
|
|
||||||
]
|
|
||||||
|
|
||||||
response = await self.llm.achat(messages)
|
# 提取回答
|
||||||
answer = str(response.message.content)
|
if hasattr(result, 'response'):
|
||||||
|
answer = str(result.response)
|
||||||
|
elif hasattr(result, 'message'):
|
||||||
|
answer = str(result.message)
|
||||||
|
else:
|
||||||
|
answer = str(result)
|
||||||
|
|
||||||
# 添加 AI 回复到历史
|
# 添加 AI 回复到历史
|
||||||
self._add_message(chat_id, user_id, "assistant", answer)
|
self._add_message(chat_id, user_id, "assistant", answer)
|
||||||
@@ -194,7 +506,60 @@ class ChatAIAgent:
|
|||||||
import traceback
|
import traceback
|
||||||
error_trace = traceback.format_exc()
|
error_trace = traceback.format_exc()
|
||||||
logger.Log("Error", f"详细错误:\n{error_trace}")
|
logger.Log("Error", f"详细错误:\n{error_trace}")
|
||||||
return f"处理对话时出错: {str(e)}"
|
|
||||||
|
# 尝试降级处理
|
||||||
|
try:
|
||||||
|
return await self._fallback_chat(message, chat_id, user_id)
|
||||||
|
except:
|
||||||
|
return f"处理对话时出错: {str(e)}"
|
||||||
|
|
||||||
|
async def _fallback_chat(self, message: str, chat_id: int, user_id: int) -> str:
|
||||||
|
"""降级对话处理(当 workflow 不可用时)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 用户消息
|
||||||
|
chat_id: 群聊ID
|
||||||
|
user_id: 用户ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AI 回复
|
||||||
|
"""
|
||||||
|
logger.Log("Info", "使用降级模式处理对话")
|
||||||
|
|
||||||
|
# 格式化历史消息(不包括刚添加的用户消息)
|
||||||
|
history_without_last = self.global_history[:-1] if len(self.global_history) > 1 else []
|
||||||
|
history_text = "\n".join([
|
||||||
|
self._format_message_with_context(msg)
|
||||||
|
for msg in history_without_last
|
||||||
|
]) if history_without_last else "(这是第一条对话)"
|
||||||
|
|
||||||
|
# 构建当前用户消息的格式化版本
|
||||||
|
current_msg = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [群聊#{chat_id}] [用户#{user_id}]: {message}"
|
||||||
|
|
||||||
|
# 构建对话提示
|
||||||
|
conversation_prompt = f"""以下是历史对话记录:
|
||||||
|
{history_text}
|
||||||
|
|
||||||
|
当前消息:
|
||||||
|
{current_msg}
|
||||||
|
|
||||||
|
请作为AI助手回复当前用户。"""
|
||||||
|
|
||||||
|
# 使用简单的系统提示词
|
||||||
|
simple_prompt = """你是一个友好的AI助手。
|
||||||
|
- 使用 <at user_id="用户ID"></at> 格式来提及用户
|
||||||
|
- 根据对话历史和当前消息,给出有意义的回复"""
|
||||||
|
|
||||||
|
# 使用 achat 方法进行对话
|
||||||
|
messages = [
|
||||||
|
ChatMessage(role="system", content=simple_prompt),
|
||||||
|
ChatMessage(role="user", content=conversation_prompt)
|
||||||
|
]
|
||||||
|
|
||||||
|
response = await self.llm.achat(messages)
|
||||||
|
answer = str(response.message.content)
|
||||||
|
|
||||||
|
return answer
|
||||||
|
|
||||||
|
|
||||||
class ChatAIPlugin(WPSAPI):
|
class ChatAIPlugin(WPSAPI):
|
||||||
|
|||||||
@@ -247,6 +247,20 @@ class WPSConfigAPI(WPSAPI):
|
|||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
return int(row["user_id"]) if row else None
|
return int(row["user_id"]) if row else None
|
||||||
|
|
||||||
|
def get_all_usernames(self) -> List[Dict[str, Any]]:
|
||||||
|
"""获取所有已设置用户名的用户列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含 user_id 和 username 的字典列表
|
||||||
|
示例: [{"user_id": 123, "username": "张三"}, ...]
|
||||||
|
"""
|
||||||
|
cursor = get_db().conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT user_id, username FROM user_info WHERE username != '' AND username IS NOT NULL ORDER BY user_id ASC"
|
||||||
|
)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
return [{"user_id": row["user_id"], "username": row["username"]} for row in rows]
|
||||||
|
|
||||||
def get_user_url(self, user_id: int) -> Optional[str]:
|
def get_user_url(self, user_id: int) -> Optional[str]:
|
||||||
record = self._get_user_record(user_id)
|
record = self._get_user_record(user_id)
|
||||||
if not record:
|
if not record:
|
||||||
|
|||||||
Reference in New Issue
Block a user