增强AI插件

This commit is contained in:
2025-11-20 15:40:03 +08:00
parent b879a70325
commit 544b8071da
3 changed files with 711 additions and 32 deletions

View File

@@ -2,12 +2,15 @@ from Plugins.WPSAPI import *
from datetime import datetime
from llama_index.llms.ollama import Ollama
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)
OLLAMA_URL = logger.FindItem("ollama_url", "http://ollama.liubai.site")
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()
@@ -30,21 +33,12 @@ class ChatAIAgent:
# 全局消息历史列表
self.global_history: List[Dict[str, any]] = []
# 系统提示词
self.system_prompt = """你是一个友好的AI助手能够同时在多个群聊中与不同用户对话。
重要规则:
- 你能看到所有群聊的对话历史每条消息都标注了时间、群聊ID和用户ID
- 当你需要特指某个用户或专门回复某个用户时,必须在回复中使用以下格式:<at user_id="用户ID"></at>
- 禁止直接输出用户ID数字始终使用 <at> 标签包裹
- 注意区分不同群聊和不同用户的对话上下文
示例对话:
[2025-11-20 10:00:00] [群聊#12345] [用户#11111]: 你好
你的回复: 你好!有什么可以帮助你的吗?
[2025-11-20 10:01:00] [群聊#67890] [用户#22222]: 刚刚谁在说话, 说了什么?
你的回复: 刚刚群聊12345的用户<at user_id="11111"></at>在说话, 他说了你好。"""
# Agent 工作流和配置API
self.workflow: Optional[AgentWorkflow] = None
self.config_api = None # 延迟加载,避免循环依赖
# 初始化 Agent 和工具
self._initialize_agent()
logger.Log("Info", f"{ConsoleFrontColor.GREEN}ChatAIAgent 初始化完成{ConsoleFrontColor.RESET}")
logger.Log("Info", f"模型: {OLLAMA_MODEL}, 最大历史: {max_history}")
@@ -56,6 +50,316 @@ class ChatAIAgent:
cls._instance = cls()
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:
"""添加消息到全局历史
@@ -148,6 +452,11 @@ class ChatAIAgent:
# 添加用户消息到历史
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_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}"
# 构建对话提示
conversation_prompt = f"""以下是历史对话记录
# 构建完整的用户查询,包含历史上下文
full_query = f"""# 历史对话记录
{history_text}
当前消息
# 当前消息
{current_msg}
请作为AI助手回复当前用户。记住
1. 使用 <at user_id="{user_id}"></at> 格式来提到任意你想提及的用户
2. 根据对话历史和当前消息,给出有意义的回复
3. 如果历史中有其他群聊或用户的信息,你可以引用它们"""
# 任务
请作为 AI 助手回复当前用户。记住:
1. 如果需要查询用户信息,主动使用工具
2. 使用 <at user_id="用户ID"></at> 格式提及用户
3. 根据对话历史和当前消息,给出有意义的回复"""
logger.Log("Info", f"处理对话 - 群聊#{chat_id} 用户#{user_id}")
logger.Log("Info", f"当前历史消息数: {len(self.global_history)}")
# 使用 achat 方法进行对话
messages = [
ChatMessage(role="system", content=self.system_prompt),
ChatMessage(role="user", content=conversation_prompt)
]
# 使用 workflow 运行 agent
result = await self.workflow.run(user_msg=full_query)
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 回复到历史
self._add_message(chat_id, user_id, "assistant", answer)
@@ -194,7 +506,60 @@ class ChatAIAgent:
import traceback
error_trace = traceback.format_exc()
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):

View File

@@ -247,6 +247,20 @@ class WPSConfigAPI(WPSAPI):
row = cursor.fetchone()
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]:
record = self._get_user_record(user_id)
if not record: