Introduce WorkflowEngine with state machine, tool registry, and builtin chat template; migrate stream chat to engine callbacks. Move page mode switching to TopBar actions cluster as a ThemeToggle-style dropdown (聊天/工作室/爽文/房间). Co-authored-by: Cursor <cursoragent@cursor.com>
440 lines
21 KiB
Python
440 lines
21 KiB
Python
"""
|
||
项目内部数据结构定义
|
||
|
||
这是本项目真正使用的核心数据模型,所有业务逻辑都基于这些类型。
|
||
与 sillytavern.py 不同,这里的模型不参与导入导出兼容,而是专注于:
|
||
- 内部业务逻辑处理
|
||
- API 响应数据结构
|
||
- 数据存储格式
|
||
- 工作流引擎数据交换
|
||
|
||
所有从 SillyTavern 导入的数据都会转换为这些内部模型进行处理,
|
||
导出时再从内部模型转换回 SillyTavern 格式。
|
||
"""
|
||
from enum import Enum
|
||
from typing import List, Optional, Dict, Any
|
||
from pydantic import BaseModel, Field
|
||
from datetime import datetime
|
||
|
||
|
||
# ==================== 世界书 (World Info) ====================
|
||
|
||
class ActivationType(str, Enum):
|
||
"""
|
||
自定义激活方式类型(4种枚举)
|
||
|
||
这是项目的核心创新点之一,相比 SillyTavern 的简单 constant/selective 标志,
|
||
我们提供了更灵活的激活机制。
|
||
"""
|
||
PERMANENT = 'permanent' # 永久激活 - 始终包含在上下文中
|
||
KEYWORD = 'keyword' # 关键词触发 - 匹配关键词时激活
|
||
RAG = 'rag' # RAG 检索激活 - 基于向量相似度检索
|
||
LOGIC = 'logic' # 逻辑表达式激活 - 基于变量条件判断
|
||
|
||
|
||
class LogicOperator(str, Enum):
|
||
"""逻辑运算符(用于 LOGIC 激活类型)"""
|
||
EQUALS = 'equals' # 等于
|
||
NOT_EQUALS = 'not_equals' # 不等于
|
||
CONTAINS = 'contains' # 包含
|
||
NOT_CONTAINS = 'not_contains' # 不包含
|
||
GREATER = 'greater' # 大于
|
||
LESS = 'less' # 小于
|
||
|
||
|
||
class LogicExpression(BaseModel):
|
||
"""
|
||
逻辑表达式结构(用于 LOGIC 激活类型)
|
||
|
||
示例: variable1="mood", operator="equals", variable2="happy"
|
||
表示当 mood 变量等于 happy 时激活该条目
|
||
"""
|
||
variable1: str = Field(..., description="第一个变量名")
|
||
operator: LogicOperator = Field(..., description="比较运算符")
|
||
variable2: str = Field(..., description="第二个变量名或值")
|
||
|
||
|
||
class RAGConfig(BaseModel):
|
||
"""
|
||
RAG 配置(用于 RAG 激活类型)
|
||
|
||
控制如何从向量数据库中检索相关内容
|
||
"""
|
||
libraryId: str = Field(..., description="绑定的 RAG 库 ID")
|
||
threshold: Optional[float] = Field(0.7, ge=0, le=1, description="相似度阈值 (0-1)")
|
||
maxEntries: Optional[int] = Field(5, gt=0, description="最大返回条目数")
|
||
|
||
|
||
class WorldInfoEntry(BaseModel):
|
||
"""
|
||
项目内部世界书条目结构
|
||
|
||
这是世界书的核心单元,每个条目代表一段可以被动态注入到对话上下文中的知识。
|
||
相比 SillyTavern,我们添加了 activationType、logicExpression、ragConfig 等高级功能。
|
||
"""
|
||
uid: str = Field(..., description="条目唯一标识符 (UUID)")
|
||
key: Optional[List[str]] = Field(None, description="主关键词列表 (用于 KEYWORD 激活)")
|
||
keysecondary: Optional[List[str]] = Field(None, description="次要关键词列表 (可选过滤)")
|
||
content: str = Field(..., description="条目内容 - 激活时注入的文本")
|
||
activationType: ActivationType = Field(..., description="激活方式")
|
||
logicExpression: Optional[LogicExpression] = Field(None, description="逻辑表达式 (LOGIC 类型使用)")
|
||
ragConfig: Optional[RAGConfig] = Field(None, description="RAG 配置 (RAG 类型使用)")
|
||
order: int = Field(0, description="插入顺序 - 数值越大越靠近末尾")
|
||
position: Optional[str] = Field('after_char', description="插入位置")
|
||
depth: Optional[int] = Field(None, description="插入深度 (当 position='at_depth' 时使用)")
|
||
probability: Optional[float] = Field(100, ge=0, le=100, description="激活概率 (0-100)")
|
||
group: Optional[List[str]] = Field(None, description="所属组标签")
|
||
disable: bool = Field(False, description="是否禁用")
|
||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|
||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||
|
||
|
||
class WorldInfo(BaseModel):
|
||
"""
|
||
项目内部世界书结构
|
||
|
||
世界书是角色知识的集合,可以绑定到角色卡上,在对话中动态提供背景信息。
|
||
"""
|
||
id: str = Field(..., description="世界书唯一标识符 (UUID)")
|
||
name: str = Field(..., description="世界书名称")
|
||
description: Optional[str] = Field(None, description="世界书描述")
|
||
entries: List[WorldInfoEntry] = Field(default_factory=list, description="条目数组")
|
||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|
||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||
version: int = Field(1, description="版本号 (用于数据迁移)")
|
||
|
||
|
||
# ==================== 角色卡 (Character Card) ====================
|
||
|
||
class OutputSchemaField(BaseModel):
|
||
"""
|
||
Vercel AI SDK Output.object() 的表头定义
|
||
|
||
用于结构化输出,让 LLM 按照指定格式返回数据。
|
||
这是项目的特色功能,支持动态表格生成。
|
||
"""
|
||
name: str = Field(..., description="字段名称")
|
||
type: str = Field(..., description="字段类型 (string/number/boolean/array/object)")
|
||
description: str = Field(..., description="字段描述")
|
||
required: Optional[bool] = Field(None, description="是否必需")
|
||
enum: Optional[List[str]] = Field(None, description="枚举值 (字符串固定选项)")
|
||
fields: Optional[List['OutputSchemaField']] = Field(None, description="嵌套字段 (object 类型)")
|
||
|
||
|
||
class CharacterCard(BaseModel):
|
||
"""
|
||
项目内部角色卡结构
|
||
|
||
角色卡是对话 AI 的核心定义,包含人设、场景、开场白等。
|
||
相比 SillyTavern,我们添加了 categories、outputSchema、worldInfoId 等功能。
|
||
"""
|
||
id: str = Field(..., description="角色唯一标识符 (UUID)")
|
||
name: str = Field(..., description="角色名称")
|
||
description: str = Field(..., description="角色详细描述")
|
||
personality: str = Field(..., description="角色性格特征")
|
||
scenario: str = Field(..., description="场景设定")
|
||
first_mes: str = Field(..., description="首条开场消息")
|
||
mes_example: str = Field(..., description="对话示例")
|
||
categories: List[str] = Field(default_factory=list, description="分类标签 (用于前端筛选)")
|
||
tags: Optional[List[str]] = Field(None, description="动态表格标签数组 (SillyTavern 关键字机制)")
|
||
worldInfoId: Optional[str] = Field(None, description="绑定的世界书 ID")
|
||
outputSchema: Optional[List[OutputSchemaField]] = Field(None, description="输出 schema 定义 (结构化输出)")
|
||
avatarPath: Optional[str] = Field(None, description="角色头像路径")
|
||
alternate_greetings: Optional[List[str]] = Field(None, description="替代问候语数组")
|
||
|
||
# TODO: 拓展提示词设置(插件/拓展系统预留接口)
|
||
# - tableMaintenancePrompt: 用于指导 AI 维护动态表格(RPG状态、任务追踪等)
|
||
# - imageGenerationPrompt: 用于指导 AI 生成图片描述提示词
|
||
# 当前状态:字段已定义,默认值为 None,等待插件系统实现
|
||
tableMaintenancePrompt: Optional[str] = Field(None, description="动态表格维护提示词 - 指导 AI 如何更新表格数据")
|
||
imageGenerationPrompt: Optional[str] = Field(None, description="生图提示词模板 - 指导 AI 如何生成图片描述")
|
||
|
||
# ✅ 动态表格数据(SillyTavern 关键字机制扩展)
|
||
tableHeaders: Optional[List[str]] = Field(None, description="动态表格表头数组")
|
||
tableDefaults: Optional[Dict[str, Any]] = Field(None, description="动态表格默认值对象")
|
||
|
||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|
||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||
lastChatAt: Optional[int] = Field(None, description="最后聊天时间戳")
|
||
isFavorite: bool = Field(False, description="收藏状态")
|
||
version: int = Field(1, description="版本号")
|
||
|
||
|
||
# ==================== 聊天记录 (Chat Log) ====================
|
||
|
||
# 历史记录模式枚举
|
||
class HistoryMode(str, Enum):
|
||
"""
|
||
历史记录处理模式
|
||
|
||
- FULL: 全量模式,保留所有消息(需经正则处理)
|
||
- SUMMARY: 总结模式,定期用LLM总结历史消息
|
||
- RAG: RAG模式,基于向量检索(暂不实现)
|
||
"""
|
||
FULL = 'full' # 全量模式
|
||
SUMMARY = 'summary' # 总结模式
|
||
RAG = 'rag' # RAG模式(预留)
|
||
|
||
|
||
class SummaryConfig(BaseModel):
|
||
"""
|
||
总结配置
|
||
|
||
用于控制历史消息的总结行为
|
||
"""
|
||
enabled: bool = Field(True, description="是否启用总结")
|
||
interval: int = Field(10, ge=2, description="总结间隔(每隔多少条消息总结一次)")
|
||
includeUserInput: bool = Field(True, description="总结时是否包含用户输入")
|
||
summaryPrompt: str = Field(
|
||
"请总结以下对话内容,保留关键信息和上下文。用简洁的语言概括主要事件、人物状态和重要细节。",
|
||
description="总结提示词"
|
||
)
|
||
maxSummaryLength: int = Field(500, ge=100, description="总结文本的最大长度(字符数)")
|
||
|
||
|
||
class ChatHeader(BaseModel):
|
||
"""
|
||
项目内部聊天记录头
|
||
|
||
包含聊天的元数据,如参与角色、创建时间等。
|
||
"""
|
||
id: str = Field(..., description="聊天唯一标识符 (UUID)")
|
||
displayName: str = Field(..., description="显示名称 (聊天标题)")
|
||
characterId: str = Field(..., description="关联的角色卡 ID")
|
||
userName: str = Field("User", description="用户角色名")
|
||
characterName: str = Field(..., description="AI 角色名称")
|
||
tags: Optional[List[str]] = Field(None, description="动态表格标签数组 (从角色卡继承)")
|
||
|
||
# ✅ 历史记录模式配置
|
||
historyMode: HistoryMode = Field(HistoryMode.FULL, description="历史记录处理模式 (full/summary/rag)")
|
||
summaryConfig: Optional[SummaryConfig] = Field(None, description="总结配置 (当 historyMode='summary' 时使用)")
|
||
summaryCounter: int = Field(0, ge=0, description="总结计数器(独立于楼层,用于跟踪需要总结的消息数)")
|
||
|
||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|
||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||
messageCount: int = Field(0, description="消息数量")
|
||
ragLibraryId: Optional[str] = Field(None, description="关联的 RAG 历史消息库 ID")
|
||
|
||
# Agent workflow engine (optional, backward compatible)
|
||
workflowTemplateId: Optional[str] = Field(None, description="工作流模板 ID")
|
||
engineRunId: Optional[str] = Field(None, description="最近一次引擎运行 ID")
|
||
|
||
|
||
class ChatMessage(BaseModel):
|
||
"""
|
||
项目内部聊天消息
|
||
|
||
单条对话消息,支持多版本 (swipes)、token 统计、历史记录总结等功能。
|
||
"""
|
||
id: str = Field(..., description="消息唯一标识符 (UUID)")
|
||
name: str = Field(..., description="发送者名称")
|
||
is_user: bool = Field(..., description="是否为用户消息")
|
||
is_system: Optional[bool] = Field(None, description="是否为系统消息")
|
||
sendDate: str = Field(..., description="发送日期 ISO 字符串")
|
||
mes: str = Field(..., description="消息内容文本")
|
||
chatId: str = Field(..., description="关联的聊天 ID")
|
||
swipes: Optional[List[str]] = Field(None, description="替换回答数组 (多版本)")
|
||
swipe_id: Optional[int] = Field(0, description="当前选择的版本索引")
|
||
tokenCount: Optional[int] = Field(None, description="Token 数量 (用于统计)")
|
||
isTemporary: Optional[bool] = Field(None, description="是否为临时消息 (未保存)")
|
||
|
||
# ✅ 历史记录总结相关字段
|
||
is_summarized: bool = Field(False, description="是否已被总结(中间楼层,内容为空)")
|
||
is_summary: bool = Field(False, description="是否是总结消息(包含总结文本的楼层)")
|
||
summary_range: Optional[str] = Field(None, description="总结范围描述(如 'L1-L8',仅在 is_summary=True 时有值)")
|
||
|
||
|
||
class ChatLog(BaseModel):
|
||
"""
|
||
项目内部完整聊天记录
|
||
|
||
包含聊天头和所有消息,是完整的对话历史。
|
||
"""
|
||
header: ChatHeader = Field(..., description="聊天头")
|
||
messages: List[ChatMessage] = Field(default_factory=list, description="消息列表")
|
||
|
||
|
||
# ==================== 预设 (Preset) ====================
|
||
|
||
class GenerationPreset(BaseModel):
|
||
"""
|
||
项目内部采样参数预设
|
||
|
||
控制 LLM 生成的参数配置,如温度、top_p 等。
|
||
"""
|
||
id: str = Field(..., description="预设唯一标识符 (UUID)")
|
||
name: str = Field(..., description="预设名称")
|
||
temperature: float = Field(1.0, ge=0, le=2, description="温度 (控制随机性)")
|
||
topP: float = Field(1.0, ge=0, le=1, description="Top P (核采样)")
|
||
topK: int = Field(0, ge=0, description="Top K")
|
||
repetitionPenalty: float = Field(1.0, ge=0, description="重复惩罚")
|
||
frequencyPenalty: Optional[float] = Field(None, description="频率惩罚")
|
||
presencePenalty: Optional[float] = Field(None, description="存在惩罚")
|
||
maxLength: Optional[int] = Field(None, gt=0, description="最大生成长度")
|
||
isDefault: bool = Field(False, description="是否为默认预设")
|
||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|
||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||
|
||
|
||
# ==================== 提示词预设 (Prompt Preset) ====================
|
||
|
||
class PromptRole(str, Enum):
|
||
"""
|
||
Prompt 角色类型
|
||
|
||
内部业务层只保留三种角色,简化了 SillyTavern 的复杂角色系统。
|
||
"""
|
||
SYSTEM = 'system' # 系统指令
|
||
AI = 'ai' # AI 助手
|
||
USER = 'user' # 用户
|
||
|
||
|
||
class PromptEntry(BaseModel):
|
||
"""
|
||
内部业务层 - Prompt 条目
|
||
|
||
提示词模板的基本单元,可以组合成完整的提示词预设。
|
||
这是基于某个 character_id 生成的"当前视图"。
|
||
"""
|
||
identifier: str = Field(..., description="稳定关联键 (用于回写)")
|
||
name: str = Field(..., description="条目名 (前端显示)")
|
||
enabled: bool = Field(True, description="是否启用 (当前作用域下的业务状态)")
|
||
content: str = Field(..., description="条目内容 (静态内容视图)")
|
||
order: int = Field(..., description="条目顺序 (前端展示和拖拽排序)")
|
||
role: PromptRole = Field(..., description="角色类型")
|
||
tokenCount: int = Field(0, description="总 token 数 (派生显示字段)")
|
||
isSystemNode: bool = Field(False, description="是否固有节点 (不可删除)")
|
||
|
||
|
||
class PromptPresetView(BaseModel):
|
||
"""
|
||
内部业务层 - Prompt 预设视图
|
||
|
||
基于某个 character_id 的"当前视图",包含已排序、已过滤的条目列表。
|
||
"""
|
||
characterId: str = Field(..., description="关联的角色 ID")
|
||
entries: List[PromptEntry] = Field(default_factory=list, description="当前视图的条目列表")
|
||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||
version: int = Field(1, description="版本号")
|
||
|
||
|
||
# ==================== RAG 配置 ====================
|
||
|
||
class RAGSearchConfig(BaseModel):
|
||
"""RAG 搜索配置"""
|
||
topK: int = Field(5, gt=0, description="每次检索返回的结果数")
|
||
threshold: float = Field(0.7, ge=0, le=1, description="相似度阈值 (0-1)")
|
||
maxContextLength: int = Field(2000, gt=0, description="最大上下文长度 (字符数)")
|
||
|
||
|
||
class CharacterRAGConfig(BaseModel):
|
||
"""
|
||
角色卡 RAG 世界书库配置
|
||
|
||
记录角色卡关联的 RAG 知识库,用于动态检索相关知识。
|
||
"""
|
||
characterId: str = Field(..., description="角色卡ID")
|
||
ragLibraryIds: List[str] = Field(default_factory=list, description="关联的RAG库ID列表")
|
||
enabled: bool = Field(True, description="是否启用")
|
||
searchConfig: Optional[RAGSearchConfig] = Field(None, description="搜索配置")
|
||
position: str = Field('after_char', description="RAG内容插入位置")
|
||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|
||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||
|
||
|
||
class ChatRAGConfig(BaseModel):
|
||
"""
|
||
聊天会话 RAG 历史消息配置
|
||
|
||
记录聊天会话关联的 RAG 历史消息库,用于智能检索历史对话。
|
||
"""
|
||
chatId: str = Field(..., description="聊天会话ID")
|
||
ragLibraryId: Optional[str] = Field(None, description="关联的RAG历史消息库ID")
|
||
enabled: bool = Field(True, description="是否启用")
|
||
searchConfig: Optional[Dict[str, Any]] = Field(None, description="搜索配置")
|
||
autoIndex: bool = Field(True, description="是否自动索引新消息")
|
||
indexConfig: Optional[Dict[str, Any]] = Field(None, description="索引配置")
|
||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|
||
updatedAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="最后更新时间戳")
|
||
|
||
|
||
# ==================== Token 统计 ====================
|
||
|
||
class TokenUsageStatus(str, Enum):
|
||
"""Token 使用状态"""
|
||
COMPLETED = 'completed' # 成功完成
|
||
INTERRUPTED = 'interrupted' # 被用户中断
|
||
FAILED = 'failed' # 请求失败(API错误等)
|
||
|
||
|
||
class TokenUsageRecord(BaseModel):
|
||
"""
|
||
Token 使用记录
|
||
|
||
记录每次 LLM 调用的 token 使用情况,支持按时间、角色、聊天维度统计
|
||
"""
|
||
id: str = Field(..., description="记录唯一标识符 (UUID)")
|
||
chatId: str = Field(..., description="聊天ID (role_name/chat_name)")
|
||
roleName: str = Field(..., description="角色名称")
|
||
chatName: str = Field(..., description="聊天名称")
|
||
messageId: Optional[str] = Field(None, description="关联的消息ID")
|
||
floor: Optional[int] = Field(None, description="楼层号")
|
||
|
||
# Token 统计
|
||
promptTokens: int = Field(0, description="输入 token 数")
|
||
completionTokens: int = Field(0, description="输出 token 数")
|
||
totalTokens: int = Field(0, description="总 token 数")
|
||
|
||
# 状态信息
|
||
status: TokenUsageStatus = Field(TokenUsageStatus.COMPLETED, description="请求状态")
|
||
errorMessage: Optional[str] = Field(None, description="错误信息(如果失败)")
|
||
|
||
# 时间信息
|
||
timestamp: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="请求时间戳")
|
||
duration: Optional[float] = Field(None, description="请求耗时(秒)")
|
||
|
||
# API 信息
|
||
model: Optional[str] = Field(None, description="使用的模型")
|
||
apiProvider: Optional[str] = Field(None, description="API 提供商")
|
||
apiUrl: Optional[str] = Field(None, description="API URL地址")
|
||
|
||
|
||
# ==================== 图片元数据 ====================
|
||
|
||
class ImageMetadata(BaseModel):
|
||
"""
|
||
图片元数据
|
||
|
||
记录生成的图片信息,绑定到角色/聊天的特定楼层
|
||
"""
|
||
id: str = Field(..., description="图片唯一标识符 (UUID)")
|
||
chatId: str = Field(..., description="聊天ID (role_name/chat_name)")
|
||
roleName: str = Field(..., description="角色名称")
|
||
chatName: str = Field(..., description="聊天名称")
|
||
floor: int = Field(..., description="楼层号")
|
||
|
||
# 图片信息
|
||
filename: str = Field(..., description="文件名")
|
||
filepath: str = Field(..., description="文件相对路径")
|
||
width: Optional[int] = Field(None, description="图片宽度")
|
||
height: Optional[int] = Field(None, description="图片高度")
|
||
fileSize: Optional[int] = Field(None, description="文件大小(字节)")
|
||
|
||
# Swipe 支持
|
||
swipeIndex: int = Field(0, description="Swipe 索引(同一楼层多张图片)")
|
||
isCurrentSwipe: bool = Field(True, description="是否为当前显示的 swipe")
|
||
|
||
# 生成信息
|
||
prompt: Optional[str] = Field(None, description="生成使用的提示词")
|
||
negativePrompt: Optional[str] = Field(None, description="负面提示词")
|
||
seed: Optional[int] = Field(None, description="随机种子")
|
||
model: Optional[str] = Field(None, description="使用的模型/checkpoint")
|
||
workflowName: Optional[str] = Field(None, description="使用的工作流名称")
|
||
|
||
# 任务信息
|
||
taskId: Optional[str] = Field(None, description="关联的任务ID")
|
||
generationTime: Optional[float] = Field(None, description="生成耗时(秒)")
|
||
|
||
# 时间信息
|
||
createdAt: int = Field(default_factory=lambda: int(datetime.now().timestamp()), description="创建时间戳")
|