f88031b0c9
* Refactor: using sqlmodel(sqlchemy+pydantic) as ORM framework and switch to async-based sqlite operation (#2294) * stage * stage * refactor: using sqlchemy as ORM framework, switch to async-based sqlite operation - using sqlmodel as ORM(based on sqlchemy and pydantic) - add Persona, Preference, PlatformMessageHistory table * fix: conversation * fix: remove redundant explicit session.commit, and fix some type error * fix: conversation context issue * chore: remove comments * chore: remove exclude_content param * Fix: 当多个相同消息平台实例部署时上下文可能混乱(共享) (#2298) * perf: update astrbot event session format, using platfrom id to ensure uniqueness fixes: #1000 * fix: 更新 MessageSession 类以使用 platform_id 作为唯一标识符,并调整相关方法以确保一致性 * fix: 更新 MessageSession 文档以明确 platform_id 的赋值规则,并调整 get_platform 和 get_platform_inst 方法的返回类型 * Improve: 引入全新的人格管理模式以及重构函数工具管理器 (#2305) * feat: add persona management * refactor: 重构函数工具管理器,引入 ToolSet,并让 Persona 支持绑定 Tools * feat: 更新 Persona 工具选择逻辑,支持全选和指定工具的切换 * feat: 更新 BaseDatabase 中的 persona 方法返回类型,支持返回 None * fix: platform id * feat: add support to sync mcp servers from ModelScope (#2313) * fix: 修复访问令牌的空格问题 * chore: 移除 MCP 市场相关逻辑 (#2314) * chore: 移除 MCP 市场相关路由 * Refactor: 重构配置文件管理,以支持更灵活的、会话粒度的(基于 umo part)配置文件隔离 (#2328) * refactor: 重构配置文件管理,以支持更灵活的、基于 umo part 的配置文件隔离 * Refactor: 重构配置前端页面,新增数个配置项 (#2331) * refactor: 重构配置前端页面,新增数个配置项 * feat: 完善多配置文件结构 * perf: 系统配置入口 * fix: normal config item list not display * fix: 修复 axios 请求中的上下文引用问题 * chore: remove status checking in chat page * fix: 修复 stage 在不同 pipeline 中被重复使用的问题和 persona 相关问题 * Feature: 增加图片转述提供商配置、支持用户自定义模型模态能力 (#2422) * feat: 增加图片转述提供商配置、支持用户自定义模型模态能力 * fix: 修复 LLMRequestSubStage 中会话管理方法参数不一致的问题,简化方法调用 * Feature: 优化 WebSearch 的爬取网页速度并且支持使用 Tavily 作为搜索引擎 (#2427) * feat: 优化了 websearch 的速度;支持 Tavily 作为搜索引擎 * fix: 优化日志记录格式,修复搜索结果处理中的索引和内容显示问题 * feat: 添加对话选中状态管理,优化默认对话加载逻辑 * feat: 支持通过解析URL 的方式导入网页数据到知识库 (#2280) * feat:为webchat页面添加一个手动上传文件按钮(目前只处理图片) * fix:上传后清空value,允许触发change事件以多次上传同一张图片 * perf:webchat页面消息发送后清空图片预览缩略图,维持与文本信息行为一致 * perf:将文件输入的值重置为空字符串以提升浏览器兼容性 * feat:webchat文件上传按钮支持多选文件上传 * fix:释放blob URL以防止内存泄漏 * perf:并行化sendMessage中的图片获取逻辑 * feat:完成从url获取部分的UI * feat: 添加从URL导入功能的组件 * fix: 优化导入结果处理,添加整体摘要和主题摘要的文件命名 * perf: 更新url导入选项添加默认值 * perf: 在导入url的部分配置项未启用时隐藏暂不使用的下拉框选项 * feat: 添加上传前提提示信息至导入url至知识库功能 * feat: 更新导入功能提示信息,添加上传状态通知 * fix: 优化url转知识库错误处理 * feat: 合并知识库的上传文件和 URL 标签页 * feat: 删除导入URL至知识库功能的相关组件 --------- Co-authored-by: Soulter <905617992@qq.com> * feat: 添加条件显示逻辑以优化插件配置项的可见性管理 (#2433) * Feature: 支持在 WebUI 配置文件页中配置默认知识库 (#2437) * feat: 支持配置默认知识库 * chore: clean code * refactor: 重构 Function Tool 管理并初步引入 Multi Agent 及 Agent Handsoff 机制 (#2454) * stage * refactor: 重构 Function Tool 管理并引入 multi agent handsoff 机制 - Updated `star_request.py` to use the global `call_handler` instead of context-specific calls. - Modified `entities.py` to remove the dependency on `FunctionToolManager` and streamline the function tool handling. - Refactored `func_tool_manager.py` to simplify the `FunctionTool` class and its methods, removing deprecated code and enhancing clarity. - Adjusted `provider.py` to align with the new function tool structure, removing unnecessary type unions. - Enhanced `star_handler.py` to support agent registration and tool association, introducing `RegisteringAgent` for better encapsulation. - Updated `star_manager.py` to handle tool registration for agents, ensuring proper binding of handlers. - Revised `main.py` in the web searcher package to utilize the new agent registration system for web search tools. * chore: websearch * perf: 减少嵌套 * chore: 移除未使用的 mcp 导入 * feat: 添加 WebUI 迁移助手以及相关迁移方法 (#2477) * fix: 修复迁移对话时的一些问题 * feat: 增加工具使用模型能力选项 * feat: 添加知识库插件更新检查和更新功能 * perf: 调整 WebUI sidebar 顺序 * refactor: 重构 SharedPreference 类并采用数据库存储替换 json 存储 (#2482) * perf: 使用 run_coroutine_threadsafe Co-authored-by: Raven95676 <raven95676@gmail.com> * Feature: 支持配置重排序模型(vLLM API 格式)用于 score 任务 (#2496) * feat: 支持添加重排序模型(vLLM API 格式)用于 score 任务 * fix: update rerank API base URL to use localhost * feat: 知识库支持配置重排序模型 * fix: remove debug print statement for reranked results in FaissVecDB * fix: 移除知识库中的提示文本 * Feature: 支持在配置文件配置可用的插件组 (#2505) * feat: 增加可用插件集合配置项 * remove: 旧版平台可用性配置 已经基于多配置文件实现。 * feat: 应用配置文件插件可用性配置 * perf: hoist if from if * feat: llm_tool 装饰器返回值支持返回 mcp 库中 tool 的返回值类型(mcp.type.CallToolResult) (#2507) * fix: add type definition for migrationDialog and ensure open method exists before calling * chore: update project version to 4.0.0 * feat: 多 t2i 服务的随机负载均衡 (#2529) * fix: bugfixes * Improve: 扩大配置文件生效范围的自定义程度到会话粒度 (#2532) * feat: 扩大配置文件生效范围的自定义程度 * perf: 冲突检测 * refactor: simplify config form validation and improve conflict message clarity * chore: clean code * feat: 插件配置支持多个快捷魔法配置项 * chore: 修复当自动更新 webchat title 时,history 被重置的问题 * bugfixes * feat: add custom T2I template editor (#2581) * perf: add option to clear provider selection in ProviderSelector component * 📦 release: bump verstion to v4.0.0-beta.1 * chore: delete uv.lock --------- Co-authored-by: RC-CHN <67079377+RC-CHN@users.noreply.github.com> Co-authored-by: Raven95676 <raven95676@gmail.com>
494 lines
15 KiB
Python
494 lines
15 KiB
Python
import sqlite3
|
|
import time
|
|
from astrbot.core.db.po import Platform, Stats
|
|
from typing import Tuple, List, Dict, Any
|
|
from dataclasses import dataclass
|
|
|
|
@dataclass
|
|
class Conversation:
|
|
"""LLM 对话存储
|
|
|
|
对于网页聊天,history 存储了包括指令、回复、图片等在内的所有消息。
|
|
对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。
|
|
"""
|
|
|
|
user_id: str
|
|
cid: str
|
|
history: str = ""
|
|
"""字符串格式的列表。"""
|
|
created_at: int = 0
|
|
updated_at: int = 0
|
|
title: str = ""
|
|
persona_id: str = ""
|
|
|
|
|
|
INIT_SQL = """
|
|
CREATE TABLE IF NOT EXISTS platform(
|
|
name VARCHAR(32),
|
|
count INTEGER,
|
|
timestamp INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS llm(
|
|
name VARCHAR(32),
|
|
count INTEGER,
|
|
timestamp INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS plugin(
|
|
name VARCHAR(32),
|
|
count INTEGER,
|
|
timestamp INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS command(
|
|
name VARCHAR(32),
|
|
count INTEGER,
|
|
timestamp INTEGER
|
|
);
|
|
CREATE TABLE IF NOT EXISTS llm_history(
|
|
provider_type VARCHAR(32),
|
|
session_id VARCHAR(32),
|
|
content TEXT
|
|
);
|
|
|
|
-- ATRI
|
|
CREATE TABLE IF NOT EXISTS atri_vision(
|
|
id TEXT,
|
|
url_or_path TEXT,
|
|
caption TEXT,
|
|
is_meme BOOLEAN,
|
|
keywords TEXT,
|
|
platform_name VARCHAR(32),
|
|
session_id VARCHAR(32),
|
|
sender_nickname VARCHAR(32),
|
|
timestamp INTEGER
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS webchat_conversation(
|
|
user_id TEXT, -- 会话 id
|
|
cid TEXT, -- 对话 id
|
|
history TEXT,
|
|
created_at INTEGER,
|
|
updated_at INTEGER,
|
|
title TEXT,
|
|
persona_id TEXT
|
|
);
|
|
|
|
PRAGMA encoding = 'UTF-8';
|
|
"""
|
|
|
|
|
|
class SQLiteDatabase():
|
|
def __init__(self, db_path: str) -> None:
|
|
super().__init__()
|
|
self.db_path = db_path
|
|
|
|
sql = INIT_SQL
|
|
|
|
# 初始化数据库
|
|
self.conn = self._get_conn(self.db_path)
|
|
c = self.conn.cursor()
|
|
c.executescript(sql)
|
|
self.conn.commit()
|
|
|
|
# 检查 webchat_conversation 的 title 字段是否存在
|
|
c.execute(
|
|
"""
|
|
PRAGMA table_info(webchat_conversation)
|
|
"""
|
|
)
|
|
res = c.fetchall()
|
|
has_title = False
|
|
has_persona_id = False
|
|
for row in res:
|
|
if row[1] == "title":
|
|
has_title = True
|
|
if row[1] == "persona_id":
|
|
has_persona_id = True
|
|
if not has_title:
|
|
c.execute(
|
|
"""
|
|
ALTER TABLE webchat_conversation ADD COLUMN title TEXT;
|
|
"""
|
|
)
|
|
self.conn.commit()
|
|
if not has_persona_id:
|
|
c.execute(
|
|
"""
|
|
ALTER TABLE webchat_conversation ADD COLUMN persona_id TEXT;
|
|
"""
|
|
)
|
|
self.conn.commit()
|
|
|
|
c.close()
|
|
|
|
def _get_conn(self, db_path: str) -> sqlite3.Connection:
|
|
conn = sqlite3.connect(self.db_path)
|
|
conn.text_factory = str
|
|
return conn
|
|
|
|
def _exec_sql(self, sql: str, params: Tuple = None):
|
|
conn = self.conn
|
|
try:
|
|
c = self.conn.cursor()
|
|
except sqlite3.ProgrammingError:
|
|
conn = self._get_conn(self.db_path)
|
|
c = conn.cursor()
|
|
|
|
if params:
|
|
c.execute(sql, params)
|
|
c.close()
|
|
else:
|
|
c.execute(sql)
|
|
c.close()
|
|
|
|
conn.commit()
|
|
|
|
def insert_platform_metrics(self, metrics: dict):
|
|
for k, v in metrics.items():
|
|
self._exec_sql(
|
|
"""
|
|
INSERT INTO platform(name, count, timestamp) VALUES (?, ?, ?)
|
|
""",
|
|
(k, v, int(time.time())),
|
|
)
|
|
|
|
def insert_llm_metrics(self, metrics: dict):
|
|
for k, v in metrics.items():
|
|
self._exec_sql(
|
|
"""
|
|
INSERT INTO llm(name, count, timestamp) VALUES (?, ?, ?)
|
|
""",
|
|
(k, v, int(time.time())),
|
|
)
|
|
|
|
def get_base_stats(self, offset_sec: int = 86400) -> Stats:
|
|
"""获取 offset_sec 秒前到现在的基础统计数据"""
|
|
where_clause = f" WHERE timestamp >= {int(time.time()) - offset_sec}"
|
|
|
|
try:
|
|
c = self.conn.cursor()
|
|
except sqlite3.ProgrammingError:
|
|
c = self._get_conn(self.db_path).cursor()
|
|
|
|
c.execute(
|
|
"""
|
|
SELECT * FROM platform
|
|
"""
|
|
+ where_clause
|
|
)
|
|
|
|
platform = []
|
|
for row in c.fetchall():
|
|
platform.append(Platform(*row))
|
|
|
|
c.close()
|
|
|
|
return Stats(platform=platform)
|
|
|
|
def get_total_message_count(self) -> int:
|
|
try:
|
|
c = self.conn.cursor()
|
|
except sqlite3.ProgrammingError:
|
|
c = self._get_conn(self.db_path).cursor()
|
|
|
|
c.execute(
|
|
"""
|
|
SELECT SUM(count) FROM platform
|
|
"""
|
|
)
|
|
res = c.fetchone()
|
|
c.close()
|
|
return res[0]
|
|
|
|
def get_grouped_base_stats(self, offset_sec: int = 86400) -> Stats:
|
|
"""获取 offset_sec 秒前到现在的基础统计数据(合并)"""
|
|
where_clause = f" WHERE timestamp >= {int(time.time()) - offset_sec}"
|
|
|
|
try:
|
|
c = self.conn.cursor()
|
|
except sqlite3.ProgrammingError:
|
|
c = self._get_conn(self.db_path).cursor()
|
|
|
|
c.execute(
|
|
"""
|
|
SELECT name, SUM(count), timestamp FROM platform
|
|
"""
|
|
+ where_clause
|
|
+ " GROUP BY name"
|
|
)
|
|
|
|
platform = []
|
|
for row in c.fetchall():
|
|
platform.append(Platform(*row))
|
|
|
|
c.close()
|
|
|
|
return Stats(platform, [], [])
|
|
|
|
def get_conversation_by_user_id(self, user_id: str, cid: str) -> Conversation:
|
|
try:
|
|
c = self.conn.cursor()
|
|
except sqlite3.ProgrammingError:
|
|
c = self._get_conn(self.db_path).cursor()
|
|
|
|
c.execute(
|
|
"""
|
|
SELECT * FROM webchat_conversation WHERE user_id = ? AND cid = ?
|
|
""",
|
|
(user_id, cid),
|
|
)
|
|
|
|
res = c.fetchone()
|
|
c.close()
|
|
|
|
if not res:
|
|
return
|
|
|
|
return Conversation(*res)
|
|
|
|
def new_conversation(self, user_id: str, cid: str):
|
|
history = "[]"
|
|
updated_at = int(time.time())
|
|
created_at = updated_at
|
|
self._exec_sql(
|
|
"""
|
|
INSERT INTO webchat_conversation(user_id, cid, history, updated_at, created_at) VALUES (?, ?, ?, ?, ?)
|
|
""",
|
|
(user_id, cid, history, updated_at, created_at),
|
|
)
|
|
|
|
def get_conversations(self, user_id: str) -> Tuple:
|
|
try:
|
|
c = self.conn.cursor()
|
|
except sqlite3.ProgrammingError:
|
|
c = self._get_conn(self.db_path).cursor()
|
|
|
|
c.execute(
|
|
"""
|
|
SELECT cid, created_at, updated_at, title, persona_id FROM webchat_conversation WHERE user_id = ? ORDER BY updated_at DESC
|
|
""",
|
|
(user_id,),
|
|
)
|
|
|
|
res = c.fetchall()
|
|
c.close()
|
|
conversations = []
|
|
for row in res:
|
|
cid = row[0]
|
|
created_at = row[1]
|
|
updated_at = row[2]
|
|
title = row[3]
|
|
persona_id = row[4]
|
|
conversations.append(
|
|
Conversation("", cid, "[]", created_at, updated_at, title, persona_id)
|
|
)
|
|
return conversations
|
|
|
|
def update_conversation(self, user_id: str, cid: str, history: str):
|
|
"""更新对话,并且同时更新时间"""
|
|
updated_at = int(time.time())
|
|
self._exec_sql(
|
|
"""
|
|
UPDATE webchat_conversation SET history = ?, updated_at = ? WHERE user_id = ? AND cid = ?
|
|
""",
|
|
(history, updated_at, user_id, cid),
|
|
)
|
|
|
|
def update_conversation_title(self, user_id: str, cid: str, title: str):
|
|
self._exec_sql(
|
|
"""
|
|
UPDATE webchat_conversation SET title = ? WHERE user_id = ? AND cid = ?
|
|
""",
|
|
(title, user_id, cid),
|
|
)
|
|
|
|
def update_conversation_persona_id(self, user_id: str, cid: str, persona_id: str):
|
|
self._exec_sql(
|
|
"""
|
|
UPDATE webchat_conversation SET persona_id = ? WHERE user_id = ? AND cid = ?
|
|
""",
|
|
(persona_id, user_id, cid),
|
|
)
|
|
|
|
def delete_conversation(self, user_id: str, cid: str):
|
|
self._exec_sql(
|
|
"""
|
|
DELETE FROM webchat_conversation WHERE user_id = ? AND cid = ?
|
|
""",
|
|
(user_id, cid),
|
|
)
|
|
|
|
def get_all_conversations(
|
|
self, page: int = 1, page_size: int = 20
|
|
) -> Tuple[List[Dict[str, Any]], int]:
|
|
"""获取所有对话,支持分页,按更新时间降序排序"""
|
|
try:
|
|
c = self.conn.cursor()
|
|
except sqlite3.ProgrammingError:
|
|
c = self._get_conn(self.db_path).cursor()
|
|
|
|
try:
|
|
# 获取总记录数
|
|
c.execute("""
|
|
SELECT COUNT(*) FROM webchat_conversation
|
|
""")
|
|
total_count = c.fetchone()[0]
|
|
|
|
# 计算偏移量
|
|
offset = (page - 1) * page_size
|
|
|
|
# 获取分页数据,按更新时间降序排序
|
|
c.execute(
|
|
"""
|
|
SELECT user_id, cid, created_at, updated_at, title, persona_id
|
|
FROM webchat_conversation
|
|
ORDER BY updated_at DESC
|
|
LIMIT ? OFFSET ?
|
|
""",
|
|
(page_size, offset),
|
|
)
|
|
|
|
rows = c.fetchall()
|
|
|
|
conversations = []
|
|
|
|
for row in rows:
|
|
user_id, cid, created_at, updated_at, title, persona_id = row
|
|
# 确保 cid 是字符串类型且至少有8个字符,否则使用一个默认值
|
|
safe_cid = str(cid) if cid else "unknown"
|
|
display_cid = safe_cid[:8] if len(safe_cid) >= 8 else safe_cid
|
|
|
|
conversations.append(
|
|
{
|
|
"user_id": user_id or "",
|
|
"cid": safe_cid,
|
|
"title": title or f"对话 {display_cid}",
|
|
"persona_id": persona_id or "",
|
|
"created_at": created_at or 0,
|
|
"updated_at": updated_at or 0,
|
|
}
|
|
)
|
|
|
|
return conversations, total_count
|
|
|
|
except Exception as _:
|
|
# 返回空列表和0,确保即使出错也有有效的返回值
|
|
return [], 0
|
|
finally:
|
|
c.close()
|
|
|
|
def get_filtered_conversations(
|
|
self,
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
platforms: List[str] = None,
|
|
message_types: List[str] = None,
|
|
search_query: str = None,
|
|
exclude_ids: List[str] = None,
|
|
exclude_platforms: List[str] = None,
|
|
) -> Tuple[List[Dict[str, Any]], int]:
|
|
"""获取筛选后的对话列表"""
|
|
try:
|
|
c = self.conn.cursor()
|
|
except sqlite3.ProgrammingError:
|
|
c = self._get_conn(self.db_path).cursor()
|
|
|
|
try:
|
|
# 构建查询条件
|
|
where_clauses = []
|
|
params = []
|
|
|
|
# 平台筛选
|
|
if platforms and len(platforms) > 0:
|
|
platform_conditions = []
|
|
for platform in platforms:
|
|
platform_conditions.append("user_id LIKE ?")
|
|
params.append(f"{platform}:%")
|
|
|
|
if platform_conditions:
|
|
where_clauses.append(f"({' OR '.join(platform_conditions)})")
|
|
|
|
# 消息类型筛选
|
|
if message_types and len(message_types) > 0:
|
|
message_type_conditions = []
|
|
for msg_type in message_types:
|
|
message_type_conditions.append("user_id LIKE ?")
|
|
params.append(f"%:{msg_type}:%")
|
|
|
|
if message_type_conditions:
|
|
where_clauses.append(f"({' OR '.join(message_type_conditions)})")
|
|
|
|
# 搜索关键词
|
|
if search_query:
|
|
search_query = search_query.encode("unicode_escape").decode("utf-8")
|
|
where_clauses.append(
|
|
"(title LIKE ? OR user_id LIKE ? OR cid LIKE ? OR history LIKE ?)"
|
|
)
|
|
search_param = f"%{search_query}%"
|
|
params.extend([search_param, search_param, search_param, search_param])
|
|
|
|
# 排除特定用户ID
|
|
if exclude_ids and len(exclude_ids) > 0:
|
|
for exclude_id in exclude_ids:
|
|
where_clauses.append("user_id NOT LIKE ?")
|
|
params.append(f"{exclude_id}%")
|
|
|
|
# 排除特定平台
|
|
if exclude_platforms and len(exclude_platforms) > 0:
|
|
for exclude_platform in exclude_platforms:
|
|
where_clauses.append("user_id NOT LIKE ?")
|
|
params.append(f"{exclude_platform}:%")
|
|
|
|
# 构建完整的 WHERE 子句
|
|
where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
|
|
|
|
# 构建计数查询
|
|
count_sql = f"SELECT COUNT(*) FROM webchat_conversation{where_sql}"
|
|
|
|
# 获取总记录数
|
|
c.execute(count_sql, params)
|
|
total_count = c.fetchone()[0]
|
|
|
|
# 计算偏移量
|
|
offset = (page - 1) * page_size
|
|
|
|
# 构建分页数据查询
|
|
data_sql = f"""
|
|
SELECT user_id, cid, created_at, updated_at, title, persona_id
|
|
FROM webchat_conversation
|
|
{where_sql}
|
|
ORDER BY updated_at DESC
|
|
LIMIT ? OFFSET ?
|
|
"""
|
|
query_params = params + [page_size, offset]
|
|
|
|
# 获取分页数据
|
|
c.execute(data_sql, query_params)
|
|
rows = c.fetchall()
|
|
|
|
conversations = []
|
|
|
|
for row in rows:
|
|
user_id, cid, created_at, updated_at, title, persona_id = row
|
|
# 确保 cid 是字符串类型,否则使用一个默认值
|
|
safe_cid = str(cid) if cid else "unknown"
|
|
display_cid = safe_cid[:8] if len(safe_cid) >= 8 else safe_cid
|
|
|
|
conversations.append(
|
|
{
|
|
"user_id": user_id or "",
|
|
"cid": safe_cid,
|
|
"title": title or f"对话 {display_cid}",
|
|
"persona_id": persona_id or "",
|
|
"created_at": created_at or 0,
|
|
"updated_at": updated_at or 0,
|
|
}
|
|
)
|
|
|
|
return conversations, total_count
|
|
|
|
except Exception as _:
|
|
# 返回空列表和0,确保即使出错也有有效的返回值
|
|
return [], 0
|
|
finally:
|
|
c.close()
|