重构后端成功
This commit is contained in:
@@ -4,6 +4,10 @@ FROM python:3.11-slim
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
@@ -11,13 +15,10 @@ COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
|
||||
|
||||
# 复制所有代码
|
||||
# 修改点:把当前目录(即 backend/)的内容复制到 /app/backend/ 下
|
||||
# 这样镜像内的结构就是 /app/backend/api/route.py
|
||||
COPY . /app/backend/
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动命令
|
||||
# 修改点:路径改为 backend.api.route (对应 /app/backend/api/route.py)
|
||||
CMD ["uvicorn", "backend.api.route:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
from .routes import presetsRoute, chatsRoute, worldbooksRoute
|
||||
from utils.file_utils import get_all_roles_and_chats
|
||||
from core.config import settings
|
||||
from pathlib import Path
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -12,5 +15,4 @@ router.include_router(worldbooksRoute.router)
|
||||
# 保留原有的其他路由
|
||||
@router.get("/tool_bar/get_all_role_and_chat")
|
||||
def get_all_role_and_chat_endpoint():
|
||||
from ..tools.get_all_role_and_chat import get_all_role_and_chat
|
||||
return get_all_role_and_chat()
|
||||
return get_all_roles_and_chats(Path(settings.DATA_PATH))
|
||||
|
||||
@@ -1,97 +1,56 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from backend.core.models.chat_history import ChatHistory, Message
|
||||
# TODO: 实现 ChatService 来替代旧的 ChatHistory 逻辑
|
||||
# from services.chat_service import ChatService
|
||||
|
||||
router = APIRouter(prefix="/chat", tags=["chat"])
|
||||
|
||||
|
||||
# ========== 聊天历史基础路由 ==========
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
async def list_all_chats():
|
||||
"""获取所有角色的所有聊天列表"""
|
||||
return await ChatHistory.list_all_chats()
|
||||
|
||||
# return await ChatService.list_all_chats()
|
||||
return {"chats": []}
|
||||
|
||||
@router.get("/{role_name}/{chat_name}")
|
||||
async def get_chat(role_name: str, chat_name: str):
|
||||
"""获取指定聊天的完整内容"""
|
||||
try:
|
||||
return await ChatHistory.get_chat(role_name, chat_name)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.post("/{role_name}", status_code=status.HTTP_201_CREATED)
|
||||
async def create_chat(role_name: str, chat_name: str, metadata: dict = None):
|
||||
"""创建新聊天"""
|
||||
try:
|
||||
return await ChatHistory.create_chat(role_name, chat_name, metadata)
|
||||
except FileExistsError:
|
||||
raise HTTPException(status_code=400, detail="Chat already exists")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.put("/{role_name}/{chat_name}")
|
||||
async def update_chat(role_name: str, chat_name: str, update_data: dict):
|
||||
"""更新聊天元数据"""
|
||||
try:
|
||||
return await ChatHistory.update_chat(role_name, chat_name, update_data)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.delete("/{role_name}/{chat_name}")
|
||||
async def delete_chat(role_name: str, chat_name: str):
|
||||
"""删除指定聊天"""
|
||||
try:
|
||||
return await ChatHistory.delete_chat(role_name, chat_name)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
|
||||
|
||||
# ========== 聊天消息路由 ==========
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{role_name}/{chat_name}/messages")
|
||||
async def list_messages(role_name: str, chat_name: str):
|
||||
"""获取聊天的所有消息"""
|
||||
try:
|
||||
return await ChatHistory.list_messages(role_name, chat_name)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{role_name}/{chat_name}/messages/{floor}")
|
||||
async def get_message(role_name: str, chat_name: str, floor: int):
|
||||
"""获取指定楼层的消息"""
|
||||
try:
|
||||
return await ChatHistory.get_message(role_name, chat_name, floor)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.post("/{role_name}/{chat_name}/messages", status_code=status.HTTP_201_CREATED)
|
||||
async def add_message(role_name: str, chat_name: str, message_data: dict):
|
||||
"""向聊天添加新消息"""
|
||||
try:
|
||||
return await ChatHistory.add_message(role_name, chat_name, message_data)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.put("/{role_name}/{chat_name}/messages/{floor}")
|
||||
async def update_message(role_name: str, chat_name: str, floor: int, update_data: dict):
|
||||
"""更新指定楼层的消息"""
|
||||
try:
|
||||
return await ChatHistory.update_message(role_name, chat_name, floor, update_data)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.delete("/{role_name}/{chat_name}/messages/{floor}")
|
||||
async def delete_message(role_name: str, chat_name: str, floor: int):
|
||||
"""删除指定楼层的消息"""
|
||||
try:
|
||||
return await ChatHistory.delete_message(role_name, chat_name, floor)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Chat not found")
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@@ -1,98 +1,60 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from backend.core.models.PromptList import AIDesignSpec
|
||||
from backend.core.models.PromptComponent import PromptComponent
|
||||
# TODO: 实现 PresetService 来替代旧的 AIDesignSpec 逻辑
|
||||
# from services.preset_service import PresetService
|
||||
|
||||
router = APIRouter(prefix="/presets", tags=["presets"])
|
||||
|
||||
|
||||
# ========== 预设基础路由 ==========
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
async def list_presets():
|
||||
"""获取所有预设列表及其基本信息"""
|
||||
return await AIDesignSpec.list_all_presets()
|
||||
|
||||
# return await PresetService.list_all_presets()
|
||||
return {"presets": []}
|
||||
|
||||
@router.get("/{preset_name}")
|
||||
async def get_preset(preset_name: str):
|
||||
"""获取指定预设的完整内容"""
|
||||
try:
|
||||
return await AIDesignSpec.get_preset(preset_name)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
|
||||
# try:
|
||||
# return await PresetService.get_preset(preset_name)
|
||||
# except FileNotFoundError:
|
||||
# raise HTTPException(status_code=404, detail="Preset not found")
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def create_preset(preset_name: str, preset_data: dict):
|
||||
"""创建新预设"""
|
||||
try:
|
||||
return await AIDesignSpec.create_preset(preset_name, preset_data)
|
||||
except FileExistsError:
|
||||
raise HTTPException(status_code=400, detail="Preset already exists")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.put("/{preset_name}")
|
||||
async def update_preset(preset_name: str, update_data: dict):
|
||||
"""更新预设配置"""
|
||||
try:
|
||||
return await AIDesignSpec.update_preset(preset_name, update_data)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.delete("/{preset_name}")
|
||||
async def delete_preset(preset_name: str):
|
||||
"""删除指定预设"""
|
||||
try:
|
||||
return await AIDesignSpec.delete_preset(preset_name)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
|
||||
|
||||
# ========== 预设组件路由 ==========
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{preset_name}/components")
|
||||
async def list_preset_components(preset_name: str):
|
||||
"""获取预设中的所有组件"""
|
||||
try:
|
||||
return await AIDesignSpec.list_components(preset_name)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{preset_name}/components/{component_id}")
|
||||
async def get_preset_component(preset_name: str, component_id: str):
|
||||
"""获取指定组件的详情"""
|
||||
try:
|
||||
return await AIDesignSpec.get_component(preset_name, component_id)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.post("/{preset_name}/components", status_code=status.HTTP_201_CREATED)
|
||||
async def add_preset_component(preset_name: str, component_data: dict):
|
||||
"""向预设添加新组件"""
|
||||
try:
|
||||
return await AIDesignSpec.add_component_to_preset(preset_name, component_data)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.put("/{preset_name}/components/{component_id}")
|
||||
async def update_preset_component(preset_name: str, component_id: str, update_data: dict):
|
||||
"""更新指定组件"""
|
||||
try:
|
||||
return await AIDesignSpec.update_component_in_preset(preset_name, component_id, update_data)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.delete("/{preset_name}/components/{component_id}")
|
||||
async def delete_preset_component(preset_name: str, component_id: str):
|
||||
"""从预设中删除指定组件"""
|
||||
try:
|
||||
return await AIDesignSpec.delete_component_from_preset(preset_name, component_id)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 标准库导入
|
||||
3# 标准库导入
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
@@ -10,18 +10,8 @@ from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
|
||||
# 本地模块导入
|
||||
# 本地模块导入
|
||||
from backend.core.models.WorldBook import WorldBook
|
||||
from backend.core.models.WorldItem import (
|
||||
WorldItem,
|
||||
TriggerConfig,
|
||||
KeywordTriggerConfig,
|
||||
RAGTriggerConfig,
|
||||
ConditionTriggerConfig,
|
||||
TriggerStrategy
|
||||
)
|
||||
|
||||
from backend.core.config import settings
|
||||
from models.internal import WorldInfo, WorldInfoEntry
|
||||
from core.config import settings
|
||||
|
||||
# 配置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -40,58 +30,15 @@ async def list_worldbooks():
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 世界书列表
|
||||
"""
|
||||
try:
|
||||
worldbooks = []
|
||||
search_dir = settings.WORLDBOOKS_PATH
|
||||
|
||||
# 检查目录是否存在
|
||||
if not os.path.exists(search_dir):
|
||||
logger.warning(f"目录不存在: {search_dir}")
|
||||
return []
|
||||
|
||||
for filename in os.listdir(search_dir):
|
||||
if filename.endswith(".json"):
|
||||
file_path = os.path.join(search_dir, filename)
|
||||
try:
|
||||
# 加载世界书基本信息
|
||||
# 传入文件名(不带扩展名)
|
||||
world_book = WorldBook.load(Path(file_path).stem)
|
||||
worldbooks.append(world_book.to_summary_dict())
|
||||
except Exception as e:
|
||||
logger.warning(f"加载世界书 {filename} 失败: {str(e)}")
|
||||
continue
|
||||
|
||||
logger.info(f"获取世界书列表: 共 {len(worldbooks)} 个")
|
||||
return worldbooks
|
||||
except Exception as e:
|
||||
logger.error(f"获取世界书列表失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取世界书列表失败: {str(e)}")
|
||||
|
||||
# TODO: 实现 WorldBookService
|
||||
return []
|
||||
|
||||
@router.get("/{name}", response_model=Dict[str, Any])
|
||||
async def get_worldbook(name: str):
|
||||
"""
|
||||
获取指定名称的世界书
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 世界书数据
|
||||
"""
|
||||
try:
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
world_book = WorldBook.load(name)
|
||||
logger.info(f"获取世界书: {name}")
|
||||
return world_book.to_dict()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取世界书 {name} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取世界书失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.post("/", response_model=Dict[str, Any])
|
||||
async def create_worldbook(
|
||||
@@ -100,47 +47,8 @@ async def create_worldbook(
|
||||
):
|
||||
"""
|
||||
创建新世界书
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
description: 世界书描述
|
||||
file: 可选的上传文件(SillyTavern 格式)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 创建的世界书数据
|
||||
"""
|
||||
try:
|
||||
# 如果上传了文件,从文件导入
|
||||
if file:
|
||||
# 保存临时文件
|
||||
temp_path = os.path.join(settings.WORLDBOOKS_PATH, f"temp_{file.filename}")
|
||||
with open(temp_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
try:
|
||||
# 从文件加载世界书
|
||||
world_book = WorldBook.load(Path(temp_path).stem)
|
||||
# 更新名称和描述
|
||||
world_book.name = name
|
||||
# 保存世界书
|
||||
world_book.save()
|
||||
logger.info(f"从文件创建世界书: {name}")
|
||||
finally:
|
||||
# 删除临时文件
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
else:
|
||||
# 创建空世界书
|
||||
world_book = WorldBook.create_empty(name)
|
||||
logger.info(f"创建空世界书: {name}")
|
||||
|
||||
return world_book.to_dict()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"创建世界书 {name} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"创建世界书失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.put("/{name}", response_model=Dict[str, Any])
|
||||
async def update_worldbook(
|
||||
@@ -149,417 +57,61 @@ async def update_worldbook(
|
||||
):
|
||||
"""
|
||||
更新世界书
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
description: 世界书描述(可选)
|
||||
file: 可选的上传文件(SillyTavern 格式)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 更新后的世界书数据
|
||||
"""
|
||||
try:
|
||||
# 检查世界书是否存在
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
# 加载世界书
|
||||
world_book = WorldBook.load(name)
|
||||
|
||||
# 如果上传了文件,从文件导入并合并
|
||||
if file:
|
||||
# 保存临时文件
|
||||
temp_path = os.path.join(settings.WORLDBOOKS_PATH, f"temp_{file.filename}")
|
||||
with open(temp_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
try:
|
||||
# 从文件加载世界书
|
||||
imported_book = WorldBook.load(Path(temp_path).stem)
|
||||
# 合并条目
|
||||
world_book.merge_from_book(imported_book)
|
||||
logger.info(f"从文件更新世界书: {name}")
|
||||
finally:
|
||||
# 删除临时文件
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
# 保存世界书
|
||||
world_book.save()
|
||||
logger.info(f"更新世界书: {name}")
|
||||
|
||||
return world_book.to_dict()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"更新世界书 {name} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"更新世界书失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.delete("/{name}")
|
||||
async def delete_worldbook(name: str):
|
||||
"""
|
||||
删除世界书
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 删除结果
|
||||
"""
|
||||
try:
|
||||
# 检查世界书是否存在
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
# 获取文件路径
|
||||
file_path = WorldBook.get_file_path(name)
|
||||
|
||||
# 删除文件
|
||||
os.remove(file_path)
|
||||
|
||||
logger.info(f"删除世界书: {name}")
|
||||
return {"success": True, "message": f"世界书 '{name}' 已删除"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"删除世界书 {name} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"删除世界书失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{name}/entries", response_model=List[Dict[str, Any]])
|
||||
async def list_worldbook_entries(name: str):
|
||||
"""
|
||||
获取世界书的所有条目(包括已禁用的条目)
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 条目列表
|
||||
获取世界书的所有条目
|
||||
"""
|
||||
try:
|
||||
# 检查世界书是否存在
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
# 加载世界书
|
||||
world_book = WorldBook.load(name)
|
||||
|
||||
# 获取所有条目的核心信息
|
||||
entries = world_book.get_all_entries()
|
||||
|
||||
logger.info(f"获取世界书 {name} 的所有条目: 共 {len(entries)} 个")
|
||||
return entries
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取世界书 {name} 的条目失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取世界书条目失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{name}/entries/{uid}", response_model=Dict[str, Any])
|
||||
async def get_worldbook_entry(name: str, uid: int):
|
||||
"""
|
||||
获取世界书的指定条目
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
uid: 条目 UID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 条目数据
|
||||
"""
|
||||
try:
|
||||
# 检查世界书是否存在
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
# 加载世界书
|
||||
world_book = WorldBook.load(name)
|
||||
|
||||
# 获取条目
|
||||
entry = world_book.get_entry(uid)
|
||||
if entry is None:
|
||||
raise HTTPException(status_code=404, detail=f"条目 UID {uid} 不存在")
|
||||
|
||||
logger.info(f"获取世界书 {name} 的条目: UID={uid}")
|
||||
return entry.to_dict()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取世界书 {name} 的条目 {uid} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取世界书条目失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.post("/{name}/entries", response_model=Dict[str, Any])
|
||||
async def create_worldbook_entry(name: str, entry_data: Dict[str, Any]):
|
||||
"""
|
||||
在世界书中创建新条目
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
entry_data: 条目数据
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 创建的条目数据
|
||||
"""
|
||||
try:
|
||||
# 检查世界书是否存在
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
# 加载世界书
|
||||
world_book = WorldBook.load(name)
|
||||
|
||||
# 处理触发配置数据
|
||||
trigger_data = entry_data.pop("trigger_config", None)
|
||||
if trigger_data and "triggers" in trigger_data:
|
||||
# 创建新的触发配置对象
|
||||
trigger_config = TriggerConfig()
|
||||
|
||||
# 处理每个触发策略
|
||||
for strategy_str, trigger_info in trigger_data["triggers"].items():
|
||||
try:
|
||||
strategy = TriggerStrategy(strategy_str)
|
||||
enabled = trigger_info[0] if isinstance(trigger_info, list) and len(trigger_info) > 0 else False
|
||||
config_data = trigger_info[1] if isinstance(trigger_info, list) and len(trigger_info) > 1 else None
|
||||
|
||||
# 根据触发策略创建对应的配置对象
|
||||
if strategy == TriggerStrategy.KEYWORD and config_data:
|
||||
config = KeywordTriggerConfig(**config_data)
|
||||
elif strategy == TriggerStrategy.RAG and config_data:
|
||||
config = RAGTriggerConfig(**config_data)
|
||||
elif strategy == TriggerStrategy.CONDITION and config_data:
|
||||
config = ConditionTriggerConfig(**config_data)
|
||||
else:
|
||||
config = None
|
||||
|
||||
# 设置触发策略
|
||||
trigger_config.set_trigger(strategy, enabled, config)
|
||||
except Exception as e:
|
||||
logger.warning(f"处理触发策略 {strategy_str} 失败: {str(e)}")
|
||||
continue
|
||||
|
||||
# 设置触发配置
|
||||
entry_data["trigger_config"] = trigger_config
|
||||
|
||||
# 创建条目
|
||||
entry = WorldItem.Entry(**entry_data)
|
||||
|
||||
# 添加条目
|
||||
world_book.add_entry(entry)
|
||||
|
||||
# 保存世界书
|
||||
world_book.save()
|
||||
|
||||
logger.info(f"在世界书 {name} 中创建条目: UID={entry.uid}")
|
||||
return entry.to_dict()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"在世界书 {name} 中创建条目失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"创建世界书条目失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.put("/{name}/entries/{uid}", response_model=Dict[str, Any])
|
||||
async def update_worldbook_entry(name: str, uid: int, entry_data: Dict[str, Any]):
|
||||
"""
|
||||
更新世界书的指定条目
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
uid: 条目 UID
|
||||
entry_data: 条目数据
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 更新后的条目数据
|
||||
"""
|
||||
try:
|
||||
# 检查世界书是否存在
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
# 加载世界书
|
||||
world_book = WorldBook.load(name)
|
||||
|
||||
# 检查条目是否存在
|
||||
if world_book.get_entry(uid) is None:
|
||||
raise HTTPException(status_code=404, detail=f"条目 UID {uid} 不存在")
|
||||
|
||||
# 处理触发配置数据
|
||||
trigger_data = entry_data.pop("trigger_config", None)
|
||||
if trigger_data and "triggers" in trigger_data:
|
||||
# 创建新的触发配置对象
|
||||
trigger_config = TriggerConfig()
|
||||
|
||||
# 处理每个触发策略
|
||||
for strategy_str, trigger_info in trigger_data["triggers"].items():
|
||||
try:
|
||||
strategy = TriggerStrategy(strategy_str)
|
||||
enabled = trigger_info[0] if isinstance(trigger_info, list) and len(trigger_info) > 0 else False
|
||||
config_data = trigger_info[1] if isinstance(trigger_info, list) and len(trigger_info) > 1 else None
|
||||
|
||||
# 根据触发策略创建对应的配置对象
|
||||
if strategy == TriggerStrategy.KEYWORD and config_data:
|
||||
config = KeywordTriggerConfig(**config_data)
|
||||
elif strategy == TriggerStrategy.RAG and config_data:
|
||||
config = RAGTriggerConfig(**config_data)
|
||||
elif strategy == TriggerStrategy.CONDITION and config_data:
|
||||
config = ConditionTriggerConfig(**config_data)
|
||||
else:
|
||||
config = None
|
||||
|
||||
# 设置触发策略
|
||||
trigger_config.set_trigger(strategy, enabled, config)
|
||||
except Exception as e:
|
||||
logger.warning(f"处理触发策略 {strategy_str} 失败: {str(e)}")
|
||||
continue
|
||||
|
||||
# 设置触发配置
|
||||
entry_data["trigger_config"] = trigger_config
|
||||
|
||||
valid_fields = WorldItem.Entry.model_fields.keys()
|
||||
filtered_data = {k: v for k, v in entry_data.items() if k in valid_fields}
|
||||
|
||||
# 更新条目
|
||||
success = world_book.update_entry(uid, **filtered_data)
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="更新条目失败")
|
||||
|
||||
# 保存世界书
|
||||
world_book.save()
|
||||
|
||||
# 获取更新后的条目
|
||||
entry = world_book.get_entry(uid)
|
||||
|
||||
logger.info(f"更新世界书 {name} 的条目: UID={uid}")
|
||||
return entry.to_dict()
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"更新世界书 {name} 的条目 {uid} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"更新世界书条目失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.delete("/{name}/entries/{uid}")
|
||||
async def delete_worldbook_entry(name: str, uid: int):
|
||||
"""
|
||||
删除世界书的指定条目
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
uid: 条目 UID
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 删除结果
|
||||
"""
|
||||
try:
|
||||
# 检查世界书是否存在
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
# 加载世界书
|
||||
world_book = WorldBook.load(name)
|
||||
|
||||
# 删除条目
|
||||
success = world_book.remove_entry(uid)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail=f"条目 UID {uid} 不存在")
|
||||
|
||||
# 保存世界书
|
||||
world_book.save()
|
||||
|
||||
logger.info(f"删除世界书 {name} 的条目: UID={uid}")
|
||||
return {"success": True, "message": f"条目 UID {uid} 已删除"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"删除世界书 {name} 的条目 {uid} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"删除世界书条目失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.post("/{name}/import", response_model=Dict[str, Any])
|
||||
async def import_worldbook(name: str, file: UploadFile = File(...)):
|
||||
"""
|
||||
从文件导入世界书
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
file: 上传的文件(SillyTavern 格式)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 导入的世界书数据
|
||||
"""
|
||||
try:
|
||||
# 保存临时文件
|
||||
temp_path = os.path.join(settings.WORLDBOOKS_PATH, f"temp_{file.filename}")
|
||||
with open(temp_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
try:
|
||||
# 从文件加载世界书
|
||||
world_book = WorldBook.load(Path(temp_path).stem)
|
||||
|
||||
# 如果世界书已存在,合并条目
|
||||
if WorldBook.exists(name):
|
||||
existing_book = WorldBook.load(name)
|
||||
existing_book.merge_from_book(world_book)
|
||||
# 保存合并后的世界书
|
||||
existing_book.save()
|
||||
world_book = existing_book
|
||||
logger.info(f"导入并合并世界书: {name}")
|
||||
else:
|
||||
# 设置名称并保存
|
||||
world_book.name = name
|
||||
world_book.save()
|
||||
logger.info(f"导入新世界书: {name}")
|
||||
|
||||
return world_book.to_dict()
|
||||
finally:
|
||||
# 删除临时文件
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
except Exception as e:
|
||||
logger.error(f"导入世界书 {name} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"导入世界书失败: {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@router.get("/{name}/export")
|
||||
async def export_worldbook(name: str):
|
||||
"""
|
||||
导出世界书为 SillyTavern 格式
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
|
||||
Returns:
|
||||
FileResponse: 导出的文件
|
||||
"""
|
||||
try:
|
||||
# 检查世界书是否存在
|
||||
if not WorldBook.exists(name):
|
||||
raise HTTPException(status_code=404, detail=f"世界书 '{name}' 不存在")
|
||||
|
||||
# 加载世界书
|
||||
world_book = WorldBook.load(name)
|
||||
|
||||
# 创建导出文件路径
|
||||
export_path = os.path.join(settings.WORLDBOOKS_PATH, f"export_{name}.json")
|
||||
|
||||
# 导出为 SillyTavern 格式
|
||||
world_book.to_sillytavern_json(export_path)
|
||||
|
||||
logger.info(f"导出世界书: {name}")
|
||||
|
||||
# 返回文件
|
||||
return FileResponse(
|
||||
path=export_path,
|
||||
filename=f"{name}.json",
|
||||
media_type="application/json"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"导出世界书 {name} 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"导出世界书失败: {str(e)}")
|
||||
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
# 1. 定义请求体模型
|
||||
class ChatRequest(BaseModel):
|
||||
# --- 基础信息 ---
|
||||
mes: str = Field(..., description="用户输入的消息内容")
|
||||
is_user: bool = Field(..., description="标识发送者是否为用户(True为用户,False为AI)")
|
||||
floor_number: int = Field(..., description="当前对话的楼层号,用于判断是否为重试(Regenerate)请求")
|
||||
|
||||
# --- 身份与会话 ---
|
||||
name: str = Field("default", description="发送者的显示名称,默认为'default'")
|
||||
role_name: Optional[str] = Field(None, description="当前绑定的角色名称")
|
||||
chat_name: Optional[str] = Field(None, description="当前会话的标识名称")
|
||||
preset: Optional[str] = Field(None, description="预设的提示词或系统指令")
|
||||
|
||||
# --- 功能开关 ---
|
||||
stream: bool = Field(False, description="是否开启流式输出")
|
||||
img_switch: bool = Field(False, description="是否开启图片生成功能")
|
||||
table_switch: bool = Field(False, description="是否开启表格生成功能")
|
||||
|
||||
# 其他可能需要的参数,比如历史记录,可以在这里加
|
||||
# history: Optional[List[Dict]] = None
|
||||
@@ -1,65 +0,0 @@
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class PromptComponent(BaseModel):
|
||||
"""预设组件类,代表一个独立的提示词模块"""
|
||||
|
||||
identifier: str = Field(..., description="唯一标识符,用于引用和定位组件")
|
||||
name: str = Field(..., description="组件显示名称")
|
||||
content: str = Field("", description="组件内容文本")
|
||||
# 0:System,1:User,2:Assistant
|
||||
role: int = Field(0, description="角色身份(0:System,1:User,2:Assistant)")
|
||||
system_prompt: bool = Field(False, description="是否强制作为系统提示词处理")
|
||||
marker: bool = Field(False, description="是否为动态插入点占位符")
|
||||
|
||||
@validator('role')
|
||||
def validate_role(cls, v):
|
||||
"""验证角色值是否在有效范围内"""
|
||||
if not isinstance(v, int) or v not in [0, 1, 2]:
|
||||
raise ValueError("角色值必须是0(System)、1(User)或2(Assistant)")
|
||||
return v
|
||||
|
||||
def update(self, **kwargs) -> None:
|
||||
"""
|
||||
更新组件属性
|
||||
|
||||
参数:
|
||||
**kwargs: 要更新的字段和值
|
||||
|
||||
异常:
|
||||
ValueError: 当尝试更新identifier时抛出
|
||||
"""
|
||||
if 'identifier' in kwargs:
|
||||
raise ValueError("组件标识符不可修改")
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
将组件转换为字典
|
||||
|
||||
返回:
|
||||
Dict[str, Any]: 组件的字典表示
|
||||
"""
|
||||
return self.dict()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'PromptComponent':
|
||||
"""
|
||||
从字典创建组件实例,自动处理role字段的类型转换
|
||||
|
||||
参数:
|
||||
data: 包含组件数据的字典
|
||||
|
||||
返回:
|
||||
PromptComponent: 组件实例
|
||||
"""
|
||||
# 处理role字段,将字符串转换为整数
|
||||
if 'role' in data and isinstance(data['role'], str):
|
||||
role_map = {'system': 0, 'user': 1, 'assistant': 2}
|
||||
data['role'] = role_map.get(data['role'].lower(), 0)
|
||||
|
||||
return cls(**data)
|
||||
@@ -1,591 +0,0 @@
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
import json
|
||||
from .PromptComponent import PromptComponent
|
||||
|
||||
|
||||
class AIDesignSpec(BaseModel):
|
||||
"""AI设计规范类,包含模型生成的核心参数和动态结构配置"""
|
||||
|
||||
# [Base] 基础核心参数
|
||||
temperature: float = Field(1.0, description="生成温度,控制随机性(0-2)")
|
||||
frequency_penalty: float = Field(0.0, description="频率惩罚,降低重复token概率")
|
||||
presence_penalty: float = Field(0.0, description="存在惩罚,鼓励谈论新话题")
|
||||
top_p: float = Field(1.0, description="核采样,控制词汇选择范围")
|
||||
top_k: int = Field(0, description="随机采样范围,从概率最高的K个词中选择")
|
||||
top_a: float = Field(0.0, description="基于平方概率分布的采样")
|
||||
min_p: float = Field(0.0, description="最小概率阈值")
|
||||
repetition_penalty: float = Field(1.0, description="重复惩罚系数(1.0-1.2)")
|
||||
max_context: int = Field(2048, description="上下文窗口大小(Token上限)")
|
||||
max_tokens: int = Field(250, description="单次回复的最大长度")
|
||||
max_context_unlocked: bool = Field(False, description="是否允许超出限制的上下文")
|
||||
names_behavior: int = Field(0, description="名字处理行为(0=默认,1=始终包含,2=仅角色)")
|
||||
send_if_empty: str = Field("", description="用户发送空消息时自动填充的内容")
|
||||
impersonation_prompt: str = Field("", description="模仿模式下使用的提示词")
|
||||
new_chat_prompt: str = Field("", description="开启新聊天时自动发送的系统提示")
|
||||
new_group_chat_prompt: str = Field("", description="开启新群组聊天时的提示")
|
||||
new_example_chat_prompt: str = Field("", description="新示例聊天的提示")
|
||||
continue_nudge_prompt: str = Field("", description="续写功能触发的提示词")
|
||||
bias_preset_selected: str = Field("", description="选用的偏见预设")
|
||||
wi_format: str = Field("{0}", description="世界书条目的格式化字符串")
|
||||
scenario_format: str = Field("{{scenario}}", description="场景描述的格式化字符串")
|
||||
personality_format: str = Field("", description="角色性格的格式化字符串")
|
||||
group_nudge_prompt: str = Field("", description="群组聊天中提示AI仅以特定角色回复的提示词")
|
||||
stream: bool = Field(True, description="是否使用流式输出")
|
||||
assistant_prefill: str = Field("", description="强制AI回复的开头内容")
|
||||
assistant_impersonation: str = Field("", description="模仿模式下强制AI回复的开头内容")
|
||||
use_sysprompt: bool = Field(True, description="是否强制将提示词注入系统层")
|
||||
squash_system_messages: bool = Field(False, description="是否压缩系统消息")
|
||||
media_inlining: bool = Field(False, description="是否内联媒体描述")
|
||||
continue_prefill: bool = Field(True, description="续写时是否预填充内容")
|
||||
continue_postfix: str = Field(" ", description="续写时添加的后缀")
|
||||
seed: int = Field(-1, description="随机种子(-1为随机)")
|
||||
n: int = Field(1, description="生成回复的数量")
|
||||
|
||||
# [Dynamic] 动态结构
|
||||
prompts: List[PromptComponent] = Field(
|
||||
default_factory=list,
|
||||
description="组件库,定义所有可用的积木块"
|
||||
)
|
||||
prompt_order: List[str] = Field(
|
||||
default_factory=list,
|
||||
description="组装说明书,定义构建最终提示词的顺序"
|
||||
)
|
||||
|
||||
@validator('prompts')
|
||||
def validate_prompts_unique_identifier(cls, v):
|
||||
"""验证组件标识符唯一性"""
|
||||
identifiers = [comp.identifier for comp in v]
|
||||
if len(identifiers) != len(set(identifiers)):
|
||||
raise ValueError("组件标识符必须唯一")
|
||||
return v
|
||||
|
||||
@validator('prompt_order')
|
||||
def validate_prompt_order_exists(cls, v, values):
|
||||
"""验证prompt_order中的组件ID是否存在于prompts中"""
|
||||
if 'prompts' in values:
|
||||
prompt_ids = {comp.identifier for comp in values['prompts']}
|
||||
invalid_ids = set(v) - prompt_ids
|
||||
if invalid_ids:
|
||||
raise ValueError(f"prompt_order中包含不存在的组件ID: {invalid_ids}")
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def get_preset_dir(cls) -> Path:
|
||||
"""获取预设目录路径"""
|
||||
try:
|
||||
from backend.core.config import settings
|
||||
preset_dir = settings.DATA_PATH / "preset"
|
||||
# 如果路径不存在,尝试使用相对路径
|
||||
if not preset_dir.exists():
|
||||
# 尝试从当前工作目录构建路径
|
||||
cwd_preset_dir = Path.cwd() / "data" / "preset"
|
||||
if cwd_preset_dir.exists():
|
||||
return cwd_preset_dir
|
||||
# 尝试从脚本所在目录构建路径
|
||||
script_dir = Path(__file__).resolve().parent.parent.parent
|
||||
script_preset_dir = script_dir / "data" / "preset"
|
||||
if script_preset_dir.exists():
|
||||
return script_preset_dir
|
||||
# 如果都不存在,返回默认路径
|
||||
return Path("data/preset")
|
||||
return preset_dir
|
||||
except ImportError:
|
||||
# 如果无法导入settings,尝试使用相对路径
|
||||
cwd_preset_dir = Path.cwd() / "data" / "preset"
|
||||
if cwd_preset_dir.exists():
|
||||
return cwd_preset_dir
|
||||
# 尝试从脚本所在目录构建路径
|
||||
script_dir = Path(__file__).resolve().parent.parent.parent
|
||||
script_preset_dir = script_dir / "data" / "preset"
|
||||
if script_preset_dir.exists():
|
||||
return script_preset_dir
|
||||
# 如果都不存在,返回默认路径
|
||||
return Path("data/preset")
|
||||
|
||||
@classmethod
|
||||
async def list_all_presets(cls) -> Dict[str, List[Dict]]:
|
||||
"""获取所有预设列表及其基本信息"""
|
||||
preset_dir = cls.get_preset_dir()
|
||||
if not preset_dir.exists():
|
||||
return {"presets": []}
|
||||
|
||||
presets = []
|
||||
for preset_file in preset_dir.glob("*.json"):
|
||||
try:
|
||||
with open(preset_file, 'r', encoding='utf-8') as f:
|
||||
preset_data = json.load(f)
|
||||
presets.append({
|
||||
"name": preset_file.stem,
|
||||
"description": preset_data.get("description", ""),
|
||||
"component_count": len(preset_data.get("prompts", [])),
|
||||
"temperature": preset_data.get("temperature", 1.0)
|
||||
})
|
||||
except Exception:
|
||||
continue # 跳过损坏的预设文件
|
||||
return {"presets": presets}
|
||||
|
||||
@classmethod
|
||||
async def get_preset(cls, preset_name: str) -> Dict[str, Any]:
|
||||
"""获取指定预设的完整内容"""
|
||||
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
|
||||
if not preset_path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {preset_name}")
|
||||
|
||||
try:
|
||||
with open(preset_path, 'r', encoding='utf-8') as f:
|
||||
preset_data = json.load(f)
|
||||
|
||||
# 处理prompt_order,简化为单角色配置
|
||||
if 'prompt_order' in preset_data and isinstance(preset_data['prompt_order'], list) and len(
|
||||
preset_data['prompt_order']) > 0:
|
||||
# 检查第一个元素是否为字典(多角色配置)
|
||||
first_item = preset_data['prompt_order'][0]
|
||||
if isinstance(first_item, dict) and 'order' in first_item:
|
||||
# 提取第一个角色的order配置
|
||||
first_role_order = first_item
|
||||
if isinstance(first_role_order['order'], list):
|
||||
# 简化为只包含enabled为True的identifier列表
|
||||
simplified_order = [
|
||||
item.get('identifier')
|
||||
for item in first_role_order['order']
|
||||
if item.get('enabled', True)
|
||||
]
|
||||
preset_data['prompt_order'] = simplified_order
|
||||
|
||||
# 转换为AIDesignSpec对象进行验证
|
||||
ai_design_spec = cls.from_dict(preset_data)
|
||||
|
||||
# 构建返回数据,确保格式与前端期望的一致
|
||||
result = {
|
||||
# 基础参数
|
||||
"temperature": ai_design_spec.temperature,
|
||||
"frequency_penalty": ai_design_spec.frequency_penalty,
|
||||
"presence_penalty": ai_design_spec.presence_penalty,
|
||||
"top_p": ai_design_spec.top_p,
|
||||
"top_k": ai_design_spec.top_k,
|
||||
"max_context": ai_design_spec.max_context,
|
||||
"max_tokens": ai_design_spec.max_tokens,
|
||||
"max_context_unlocked": ai_design_spec.max_context_unlocked,
|
||||
"stream_openai": ai_design_spec.stream,
|
||||
"seed": ai_design_spec.seed,
|
||||
"n": ai_design_spec.n,
|
||||
|
||||
# 兼容旧格式
|
||||
"openai_max_context": ai_design_spec.max_context,
|
||||
"openai_max_tokens": ai_design_spec.max_tokens,
|
||||
|
||||
# 其他参数
|
||||
"top_a": ai_design_spec.top_a,
|
||||
"min_p": ai_design_spec.min_p,
|
||||
"repetition_penalty": ai_design_spec.repetition_penalty,
|
||||
"names_behavior": ai_design_spec.names_behavior,
|
||||
"send_if_empty": ai_design_spec.send_if_empty,
|
||||
"impersonation_prompt": ai_design_spec.impersonation_prompt,
|
||||
"new_chat_prompt": ai_design_spec.new_chat_prompt,
|
||||
"new_group_chat_prompt": ai_design_spec.new_group_chat_prompt,
|
||||
"new_example_chat_prompt": ai_design_spec.new_example_chat_prompt,
|
||||
"continue_nudge_prompt": ai_design_spec.continue_nudge_prompt,
|
||||
"bias_preset_selected": ai_design_spec.bias_preset_selected,
|
||||
"wi_format": ai_design_spec.wi_format,
|
||||
"scenario_format": ai_design_spec.scenario_format,
|
||||
"personality_format": ai_design_spec.personality_format,
|
||||
"group_nudge_prompt": ai_design_spec.group_nudge_prompt,
|
||||
"assistant_prefill": ai_design_spec.assistant_prefill,
|
||||
"assistant_impersonation": ai_design_spec.assistant_impersonation,
|
||||
"use_sysprompt": ai_design_spec.use_sysprompt,
|
||||
"squash_system_messages": ai_design_spec.squash_system_messages,
|
||||
"media_inlining": ai_design_spec.media_inlining,
|
||||
"continue_prefill": ai_design_spec.continue_prefill,
|
||||
"continue_postfix": ai_design_spec.continue_postfix,
|
||||
|
||||
# 处理组件
|
||||
"prompts": []
|
||||
}
|
||||
|
||||
# 处理组件列表
|
||||
if ai_design_spec.prompts:
|
||||
# 获取当前角色的prompt_order(简化后的字符串列表)
|
||||
current_order = ai_design_spec.prompt_order if ai_design_spec.prompt_order else []
|
||||
|
||||
# 构建组件列表
|
||||
for prompt in ai_design_spec.prompts:
|
||||
# 检查组件是否在order中
|
||||
is_in_order = prompt.identifier in current_order
|
||||
|
||||
# 构建组件对象
|
||||
component = {
|
||||
"identifier": prompt.identifier,
|
||||
"name": prompt.name,
|
||||
"content": prompt.content if hasattr(prompt, 'content') else "",
|
||||
"role": prompt.role if hasattr(prompt, 'role') else (0 if prompt.system_prompt else 1),
|
||||
"system_prompt": prompt.system_prompt,
|
||||
"marker": prompt.marker,
|
||||
"enabled": is_in_order if current_order else True
|
||||
}
|
||||
|
||||
result["prompts"].append(component)
|
||||
|
||||
# 按照order排序组件
|
||||
if current_order:
|
||||
result["prompts"].sort(
|
||||
key=lambda x: current_order.index(x["identifier"]) if x[
|
||||
"identifier"] in current_order else len(
|
||||
current_order))
|
||||
|
||||
# 添加prompt_order
|
||||
result["prompt_order"] = ai_design_spec.prompt_order if ai_design_spec.prompt_order else []
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to load preset: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def create_preset(cls, preset_name: str, preset_data: Dict) -> Dict[str, str]:
|
||||
"""创建新预设"""
|
||||
preset_dir = cls.get_preset_dir()
|
||||
preset_dir.mkdir(parents=True, exist_ok=True)
|
||||
preset_path = preset_dir / f"{preset_name}.json"
|
||||
|
||||
if preset_path.exists():
|
||||
raise FileExistsError(f"Preset already exists: {preset_name}")
|
||||
|
||||
try:
|
||||
# 验证并转换为AIDesignSpec对象
|
||||
ai_design_spec = cls.from_dict(preset_data)
|
||||
|
||||
# 保存到文件
|
||||
with open(preset_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(ai_design_spec.dict(), f, ensure_ascii=False, indent=2)
|
||||
|
||||
return {"message": "Preset created successfully", "name": preset_name}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to create preset: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def update_preset(cls, preset_name: str, update_data: Dict) -> Dict[str, str]:
|
||||
"""更新预设配置"""
|
||||
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
|
||||
if not preset_path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {preset_name}")
|
||||
|
||||
try:
|
||||
# 加载现有预设
|
||||
with open(preset_path, 'r', encoding='utf-8') as f:
|
||||
preset_data = json.load(f)
|
||||
|
||||
# 更新字段
|
||||
for key, value in update_data.items():
|
||||
preset_data[key] = value
|
||||
|
||||
# 验证并转换为AIDesignSpec对象
|
||||
ai_design_spec = cls.from_dict(preset_data)
|
||||
|
||||
# 保存更新
|
||||
with open(preset_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(ai_design_spec.dict(), f, ensure_ascii=False, indent=2)
|
||||
|
||||
return {"message": "Preset updated successfully"}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to update preset: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def delete_preset(cls, preset_name: str) -> Dict[str, str]:
|
||||
"""删除指定预设"""
|
||||
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
|
||||
if not preset_path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {preset_name}")
|
||||
|
||||
try:
|
||||
preset_path.unlink()
|
||||
return {"message": "Preset deleted successfully"}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to delete preset: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def list_components(cls, preset_name: str) -> Dict[str, List[Dict]]:
|
||||
"""获取预设中的所有组件"""
|
||||
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
|
||||
if not preset_path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {preset_name}")
|
||||
|
||||
try:
|
||||
with open(preset_path, 'r', encoding='utf-8') as f:
|
||||
preset_data = json.load(f)
|
||||
|
||||
# 获取组件列表
|
||||
components = preset_data.get("prompts", [])
|
||||
return {"components": components}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to load components: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def get_component(cls, preset_name: str, component_id: str) -> Dict[str, Any]:
|
||||
"""获取指定组件的详情"""
|
||||
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
|
||||
if not preset_path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {preset_name}")
|
||||
|
||||
try:
|
||||
with open(preset_path, 'r', encoding='utf-8') as f:
|
||||
preset_data = json.load(f)
|
||||
|
||||
# 查找组件
|
||||
components = preset_data.get("prompts", [])
|
||||
component = next((c for c in components if c.get("identifier") == component_id), None)
|
||||
|
||||
if not component:
|
||||
raise FileNotFoundError(f"Component not found: {component_id}")
|
||||
|
||||
return component
|
||||
except FileNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to load component: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def add_component_to_preset(cls, preset_name: str, component_data: Dict) -> Dict[str, str]:
|
||||
"""向预设添加新组件"""
|
||||
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
|
||||
if not preset_path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {preset_name}")
|
||||
|
||||
try:
|
||||
# 加载预设数据
|
||||
with open(preset_path, 'r', encoding='utf-8') as f:
|
||||
preset_data = json.load(f)
|
||||
|
||||
# 验证组件数据
|
||||
component = PromptComponent(**component_data)
|
||||
|
||||
# 检查组件ID是否已存在
|
||||
components = preset_data.get("prompts", [])
|
||||
if any(c.get("identifier") == component.identifier for c in components):
|
||||
raise ValueError(f"Component identifier already exists: {component.identifier}")
|
||||
|
||||
# 添加组件
|
||||
components.append(component.dict())
|
||||
preset_data["prompts"] = components
|
||||
|
||||
# 保存更新
|
||||
with open(preset_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(preset_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return {"message": "Component added successfully", "identifier": component.identifier}
|
||||
except (FileNotFoundError, ValueError):
|
||||
raise
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to add component: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def update_component_in_preset(cls, preset_name: str, component_id: str, update_data: Dict) -> Dict[str, str]:
|
||||
"""更新指定组件"""
|
||||
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
|
||||
if not preset_path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {preset_name}")
|
||||
|
||||
try:
|
||||
# 加载预设数据
|
||||
with open(preset_path, 'r', encoding='utf-8') as f:
|
||||
preset_data = json.load(f)
|
||||
|
||||
# 查找并更新组件
|
||||
components = preset_data.get("prompts", [])
|
||||
component_index = next((i for i, c in enumerate(components) if c.get("identifier") == component_id), None)
|
||||
|
||||
if component_index is None:
|
||||
raise FileNotFoundError(f"Component not found: {component_id}")
|
||||
|
||||
# 更新组件字段
|
||||
for key, value in update_data.items():
|
||||
components[component_index][key] = value
|
||||
|
||||
# 保存更新
|
||||
with open(preset_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(preset_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return {"message": "Component updated successfully"}
|
||||
except FileNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to update component: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def delete_component_from_preset(cls, preset_name: str, component_id: str) -> Dict[str, str]:
|
||||
"""从预设中删除指定组件"""
|
||||
preset_path = cls.get_preset_dir() / f"{preset_name}.json"
|
||||
if not preset_path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {preset_name}")
|
||||
|
||||
try:
|
||||
# 加载预设数据
|
||||
with open(preset_path, 'r', encoding='utf-8') as f:
|
||||
preset_data = json.load(f)
|
||||
|
||||
# 查找并删除组件
|
||||
components = preset_data.get("prompts", [])
|
||||
original_length = len(components)
|
||||
components = [c for c in components if c.get("identifier") != component_id]
|
||||
|
||||
if len(components) == original_length:
|
||||
raise FileNotFoundError(f"Component not found: {component_id}")
|
||||
|
||||
# 更新预设数据
|
||||
preset_data["prompts"] = components
|
||||
|
||||
# 保存更新
|
||||
with open(preset_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(preset_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return {"message": "Component deleted successfully"}
|
||||
except FileNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to delete component: {str(e)}")
|
||||
|
||||
# ========== 组件管理方法 ==========
|
||||
|
||||
def add_component(self, component: PromptComponent) -> None:
|
||||
"""
|
||||
添加新组件
|
||||
|
||||
参数:
|
||||
component: 要添加的组件
|
||||
|
||||
异常:
|
||||
ValueError: 当组件标识符已存在时抛出
|
||||
"""
|
||||
if any(c.identifier == component.identifier for c in self.prompts):
|
||||
raise ValueError(f"组件标识符 {component.identifier} 已存在")
|
||||
self.prompts.append(component)
|
||||
|
||||
def remove_component(self, identifier: str) -> bool:
|
||||
"""
|
||||
移除指定组件
|
||||
|
||||
参数:
|
||||
identifier: 组件标识符
|
||||
|
||||
返回:
|
||||
bool: 是否成功移除
|
||||
"""
|
||||
original_length = len(self.prompts)
|
||||
self.prompts = [c for c in self.prompts if c.identifier != identifier]
|
||||
|
||||
# 同时从prompt_order中移除
|
||||
self.prompt_order = [id for id in self.prompt_order if id != identifier]
|
||||
|
||||
return len(self.prompts) < original_length
|
||||
|
||||
def get_component(self, identifier: str) -> Optional[PromptComponent]:
|
||||
"""
|
||||
获取指定组件
|
||||
|
||||
参数:
|
||||
identifier: 组件标识符
|
||||
|
||||
返回:
|
||||
Optional[PromptComponent]: 找到的组件,未找到返回None
|
||||
"""
|
||||
for component in self.prompts:
|
||||
if component.identifier == identifier:
|
||||
return component
|
||||
return None
|
||||
|
||||
def update_component(self, identifier: str, **kwargs) -> bool:
|
||||
"""
|
||||
更新指定组件
|
||||
|
||||
参数:
|
||||
identifier: 组件标识符
|
||||
**kwargs: 要更新的字段
|
||||
|
||||
返回:
|
||||
bool: 是否成功更新
|
||||
"""
|
||||
component = self.get_component(identifier)
|
||||
if component is None:
|
||||
return False
|
||||
|
||||
component.update(**kwargs)
|
||||
return True
|
||||
|
||||
def list_components(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
列出所有组件
|
||||
|
||||
返回:
|
||||
List[Dict[str, Any]]: 组件字典列表
|
||||
"""
|
||||
return [component.to_dict() for component in self.prompts]
|
||||
|
||||
def reorder_components(self, new_order: List[str]) -> None:
|
||||
"""
|
||||
重新排序组件
|
||||
|
||||
参数:
|
||||
new_order: 新的组件标识符顺序
|
||||
|
||||
异常:
|
||||
ValueError: 当包含不存在的组件ID时抛出
|
||||
"""
|
||||
# 验证所有ID都存在
|
||||
existing_ids = {c.identifier for c in self.prompts}
|
||||
invalid_ids = set(new_order) - existing_ids
|
||||
|
||||
if invalid_ids:
|
||||
raise ValueError(f"包含不存在的组件ID: {invalid_ids}")
|
||||
|
||||
self.prompt_order = new_order
|
||||
|
||||
def get_ordered_components(self) -> List[PromptComponent]:
|
||||
"""
|
||||
获取按prompt_order排序的组件列表
|
||||
|
||||
返回:
|
||||
List[PromptComponent]: 排序后的组件列表
|
||||
"""
|
||||
component_map = {c.identifier: c for c in self.prompts}
|
||||
ordered_components = []
|
||||
|
||||
for identifier in self.prompt_order:
|
||||
if identifier in component_map:
|
||||
ordered_components.append(component_map[identifier])
|
||||
|
||||
# 添加未在prompt_order中的组件
|
||||
ordered_components.extend([
|
||||
c for c in self.prompts
|
||||
if c.identifier not in self.prompt_order
|
||||
])
|
||||
|
||||
return ordered_components
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
将设计规范转换为字典
|
||||
|
||||
返回:
|
||||
Dict[str, Any]: 设计规范的字典表示
|
||||
"""
|
||||
return self.dict()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'AIDesignSpec':
|
||||
"""
|
||||
从字典创建设计规范实例
|
||||
|
||||
参数:
|
||||
data: 包含设计规范数据的字典
|
||||
|
||||
返回:
|
||||
AIDesignSpec: 设计规范实例
|
||||
"""
|
||||
# 处理prompts字段
|
||||
if 'prompts' in data:
|
||||
data['prompts'] = [
|
||||
PromptComponent.from_dict(comp) if isinstance(comp, dict) else comp
|
||||
for comp in data['prompts']
|
||||
]
|
||||
|
||||
return cls(**data)
|
||||
@@ -1,438 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from .WorldItem import WorldItem, TriggerStrategy
|
||||
from backend.core.config import settings
|
||||
|
||||
# 配置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorldBook(BaseModel):
|
||||
"""
|
||||
世界书集合模型
|
||||
管理多个世界书条目,支持导入导出 SillyTavern 格式
|
||||
"""
|
||||
# 世界书基本信息
|
||||
name: str = Field(..., description="世界书名称")
|
||||
|
||||
# 条目集合
|
||||
entries: Dict[str, WorldItem.Entry] = Field(
|
||||
default_factory=dict,
|
||||
description="世界书条目字典对象 (Key-Value Map)"
|
||||
)
|
||||
|
||||
@field_validator('entries')
|
||||
@classmethod
|
||||
def validate_entries_unique_uid(cls, v):
|
||||
"""验证条目 UID 的唯一性"""
|
||||
uids = [entry.uid for entry in v.values()]
|
||||
if len(uids) != len(set(uids)):
|
||||
logger.error("验证失败: 条目 UID 必须唯一")
|
||||
raise ValueError("条目 UID 必须唯一")
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def get_file_path(cls, name: str) -> str:
|
||||
"""
|
||||
根据世界书名称获取文件路径
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
|
||||
Returns:
|
||||
str: 完整的文件路径
|
||||
"""
|
||||
# 使用配置中的 WORLDBOOKS_PATH
|
||||
return str(settings.WORLDBOOKS_PATH / f"{name}.json")
|
||||
|
||||
@classmethod
|
||||
def exists(cls, name: str) -> bool:
|
||||
"""
|
||||
检查指定名称的世界书文件是否存在
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
|
||||
Returns:
|
||||
bool: 文件是否存在
|
||||
"""
|
||||
file_path = cls.get_file_path(name)
|
||||
return os.path.exists(file_path)
|
||||
|
||||
@classmethod
|
||||
def create_empty(cls, name: str) -> 'WorldBook':
|
||||
"""
|
||||
创建并保存一个空白的世界书
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
Returns:
|
||||
WorldBook: 创建的世界书对象
|
||||
|
||||
Raises:
|
||||
ValueError: 世界书已存在
|
||||
IOError: 文件写入失败
|
||||
"""
|
||||
# 检查世界书是否已存在
|
||||
if cls.exists(name):
|
||||
raise ValueError(f"世界书 '{name}' 已存在")
|
||||
|
||||
# 创建空白世界书对象
|
||||
world_book = cls(
|
||||
name=name,
|
||||
)
|
||||
|
||||
# 保存世界书
|
||||
world_book.save()
|
||||
|
||||
logger.info(f"创建空白世界书: {name}")
|
||||
return world_book
|
||||
|
||||
def add_entry(self, entry: WorldItem.Entry) -> None:
|
||||
"""
|
||||
添加世界书条目
|
||||
|
||||
Args:
|
||||
entry: 世界书条目对象
|
||||
|
||||
Raises:
|
||||
ValueError: 条目 UID 已存在
|
||||
"""
|
||||
entry_key = str(entry.uid)
|
||||
if entry_key in self.entries:
|
||||
error_msg = f"添加条目失败: 条目 UID {entry.uid} 已存在于世界书 {self.name}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
self.entries[entry_key] = entry
|
||||
logger.debug(f"已添加条目: UID={entry.uid}, 世界书={self.name}")
|
||||
|
||||
def remove_entry(self, uid: int) -> bool:
|
||||
"""
|
||||
移除世界书条目
|
||||
|
||||
Args:
|
||||
uid: 条目 UID
|
||||
|
||||
Returns:
|
||||
bool: 是否成功移除
|
||||
"""
|
||||
entry_key = str(uid)
|
||||
if entry_key in self.entries:
|
||||
del self.entries[entry_key]
|
||||
logger.info(f"已从世界书 {self.name} 移除条目: UID={uid}")
|
||||
return True
|
||||
logger.warning(f"尝试移除不存在的条目: 世界书 {self.name} 中未找到 UID={uid}")
|
||||
return False
|
||||
|
||||
def get_entry(self, uid: int) -> Optional[WorldItem.Entry]:
|
||||
"""
|
||||
获取指定 UID 的世界书条目
|
||||
|
||||
Args:
|
||||
uid: 条目 UID
|
||||
|
||||
Returns:
|
||||
Optional[WorldItem.Entry]: 找到的条目,未找到返回 None
|
||||
"""
|
||||
entry_key = str(uid)
|
||||
entry = self.entries.get(entry_key)
|
||||
if entry:
|
||||
logger.debug(f"从世界书 {self.name} 获取条目: UID={uid}")
|
||||
else:
|
||||
logger.debug(f"在世界书 {self.name} 中未找到条目: UID={uid}")
|
||||
return entry
|
||||
|
||||
def update_entry(self, uid: int, **kwargs) -> bool:
|
||||
"""
|
||||
更新世界书条目
|
||||
|
||||
Args:
|
||||
uid: 条目 UID
|
||||
**kwargs: 要更新的字段
|
||||
|
||||
Returns:
|
||||
bool: 是否成功更新
|
||||
"""
|
||||
entry = self.get_entry(uid)
|
||||
if entry is None:
|
||||
logger.warning(f"更新条目失败: 在世界书 {self.name} 中未找到 UID={uid}")
|
||||
return False
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(entry, key):
|
||||
setattr(entry, key, value)
|
||||
logger.info(f"已更新世界书 {self.name} 中的条目: UID={uid}, 更新字段={list(kwargs.keys())}")
|
||||
return True
|
||||
|
||||
def filter_by_position(self, position: int) -> List[WorldItem.Entry]:
|
||||
"""
|
||||
根据位置筛选条目
|
||||
|
||||
Args:
|
||||
position: 位置值
|
||||
|
||||
Returns:
|
||||
List[WorldItem.Entry]: 筛选后的条目列表
|
||||
"""
|
||||
filtered_entries = [
|
||||
entry for entry in self.entries.values()
|
||||
if entry.position == position
|
||||
]
|
||||
logger.debug(
|
||||
f"在世界书 {self.name} 中按位置筛选: 值={position}, 结果数量={len(filtered_entries)}")
|
||||
return filtered_entries
|
||||
|
||||
def get_summary(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取世界书概要信息
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 概要信息字典
|
||||
"""
|
||||
summary = {
|
||||
"name": self.name,
|
||||
"entry_count": len(self.entries),
|
||||
"trigger_strategies": {
|
||||
strategy.value: sum(1 for e in self.entries.values()
|
||||
if strategy in e.trigger_config.get_enabled_triggers())
|
||||
for strategy in TriggerStrategy
|
||||
}
|
||||
}
|
||||
logger.debug(f"获取世界书 {self.name} 的概要信息")
|
||||
return summary
|
||||
|
||||
def to_summary_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
生成世界书摘要信息,用于列表显示
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 包含基本信息的字典
|
||||
"""
|
||||
summary = {
|
||||
"name": self.name,
|
||||
"entry_count": len(self.entries),
|
||||
}
|
||||
logger.debug(f"生成世界书 {self.name} 的摘要信息")
|
||||
return summary
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""
|
||||
将 WorldBook 转换为字典
|
||||
|
||||
Returns:
|
||||
Dict: 世界书数据字典
|
||||
"""
|
||||
result = {
|
||||
'name': self.name,
|
||||
'entries': {uid: entry.model_dump() for uid, entry in self.entries.items()}
|
||||
}
|
||||
logger.debug(f"将世界书 {self.name} 转换为字典")
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def load(cls, name: str) -> 'WorldBook':
|
||||
"""
|
||||
从文件加载世界书(只有 entries 字段的格式)
|
||||
|
||||
Args:
|
||||
name: 世界书名称
|
||||
|
||||
Returns:
|
||||
WorldBook: 世界书对象
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 文件不存在
|
||||
ValueError: 格式不符合标准
|
||||
json.JSONDecodeError: JSON 解析错误
|
||||
"""
|
||||
file_path = cls.get_file_path(name)
|
||||
if not os.path.exists(file_path):
|
||||
error_msg = f"世界书文件未找到: {file_path}"
|
||||
logger.error(error_msg)
|
||||
raise FileNotFoundError(error_msg)
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
raw_data = json.load(f)
|
||||
|
||||
# 世界书名称始终使用文件名(不包括后缀名)
|
||||
world_name = name
|
||||
|
||||
# 直接使用 entries 字段
|
||||
entries_dict = raw_data.get("entries", {})
|
||||
if not isinstance(entries_dict, dict):
|
||||
error_msg = "无效的世界书格式:'entries' 字段必须是一个字典。"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# 创建世界书对象
|
||||
world_book = cls(
|
||||
name=world_name,
|
||||
)
|
||||
|
||||
# 转换标准格式的条目
|
||||
for uid, entry_data in entries_dict.items():
|
||||
try:
|
||||
# 先使用 WorldItem 解析数据
|
||||
world_item = WorldItem.from_sillytavern_data(entry_data)
|
||||
# 然后转换为 Entry
|
||||
world_entry = world_item.to_entry()
|
||||
world_book.add_entry(world_entry)
|
||||
except Exception as e:
|
||||
logger.warning(f"跳过条目 {uid},解析失败: {e}")
|
||||
|
||||
logger.info(
|
||||
f"从文件加载世界书: 文件={file_path}, 名称={world_name}, 条目数={len(world_book.entries)}")
|
||||
|
||||
return world_book
|
||||
except json.JSONDecodeError as e:
|
||||
error_msg = f"JSON 解析错误: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
except Exception as e:
|
||||
error_msg = f"从文件加载世界书失败: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
保存世界书到文件(只有 entries 字段的格式)
|
||||
如果文件不存在,会创建新文件;如果文件存在,会更新现有文件
|
||||
|
||||
Raises:
|
||||
IOError: 文件写入失败
|
||||
"""
|
||||
file_path = self.get_file_path(self.name)
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs(Path(file_path).parent, exist_ok=True)
|
||||
|
||||
try:
|
||||
# 转换为标准格式
|
||||
entries_dict = {}
|
||||
for uid, entry in self.entries.items():
|
||||
entries_dict[uid] = entry.to_sillytavern_dict()
|
||||
|
||||
output_data = {
|
||||
"entries": entries_dict
|
||||
}
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(output_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(
|
||||
f"世界书已保存: 文件={file_path}, 名称={self.name}, 条目数={len(self.entries)}")
|
||||
except Exception as e:
|
||||
error_msg = f"保存世界书失败: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
raise IOError(error_msg)
|
||||
|
||||
def list_triggers_and_content(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
提取所有条目的触发关键词和内容,用于快速构建向量数据库或索引
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 包含 trigger (key) 和 content 的列表
|
||||
"""
|
||||
result = []
|
||||
for entry in self.entries.values():
|
||||
entry_dict = entry.to_dict()
|
||||
# 添加额外的触发相关信息
|
||||
enabled_triggers = entry.trigger_config.get_enabled_triggers()
|
||||
keyword_enabled, keyword_config = entry.trigger_config.get_trigger(TriggerStrategy.KEYWORD)
|
||||
constant_enabled, _ = entry.trigger_config.get_trigger(TriggerStrategy.CONSTANT)
|
||||
|
||||
entry_dict.update({
|
||||
"triggers": keyword_config.key if keyword_enabled and keyword_config else [],
|
||||
"constant": constant_enabled,
|
||||
"trigger_strategies": [strategy.value for strategy in enabled_triggers]
|
||||
})
|
||||
result.append(entry_dict)
|
||||
|
||||
logger.debug(f"列出世界书 {self.name} 的触发词和内容: 条目数={len(result)}")
|
||||
return result
|
||||
|
||||
def get_all_entries(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取所有条目的核心信息(包括已禁用的条目)
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 包含核心信息的条目列表
|
||||
"""
|
||||
result = [entry.to_dict() for entry in self.entries.values()]
|
||||
logger.debug(f"获取世界书 {self.name} 的所有条目: 条目数={len(result)}")
|
||||
return result
|
||||
|
||||
def merge_from_book(self, other_book: 'WorldBook') -> None:
|
||||
"""
|
||||
从另一个世界书合并条目
|
||||
|
||||
Args:
|
||||
other_book: 要合并的世界书对象
|
||||
"""
|
||||
for uid, entry in other_book.entries.items():
|
||||
if uid in self.entries:
|
||||
# 更新现有条目
|
||||
for key, value in entry.dict().items():
|
||||
if key != 'uid': # 不更新 UID
|
||||
setattr(self.entries[uid], key, value)
|
||||
else:
|
||||
# 添加新条目
|
||||
self.add_entry(entry)
|
||||
logger.info(f"合并世界书: 从 {other_book.name} 合并到 {self.name}")
|
||||
|
||||
def to_sillytavern_json(self, file_path: str) -> None:
|
||||
"""
|
||||
导出为 SillyTavern 格式的 JSON 文件
|
||||
|
||||
Args:
|
||||
file_path: 导出文件路径
|
||||
"""
|
||||
# 转换为 SillyTavern 格式
|
||||
entries_dict = {}
|
||||
for uid, entry in self.entries.items():
|
||||
entries_dict[uid] = entry.to_sillytavern_dict()
|
||||
|
||||
output_data = {
|
||||
"entries": entries_dict,
|
||||
"name": self.name
|
||||
}
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(output_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"导出世界书为 SillyTavern 格式: 文件={file_path}")
|
||||
|
||||
|
||||
# --- 使用示例 ---
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# 创建空白世界书
|
||||
world_book = WorldBook.create_empty("test_worldbook")
|
||||
|
||||
# 加载世界书
|
||||
world_book = WorldBook.load("test_worldbook")
|
||||
|
||||
# 打印概要
|
||||
summary = world_book.get_summary()
|
||||
print(f"世界书名称: {summary['name']}")
|
||||
print(f"条目数量: {summary['entry_count']}")
|
||||
print(f"触发策略分布: {summary['trigger_strategies']}")
|
||||
|
||||
# 列出所有条目的触发词和内容预览
|
||||
print("\n--- 条目预览 ---")
|
||||
for item in world_book.list_triggers_and_content():
|
||||
triggers = item['triggers'] if item['triggers'] else ['(无关键词 - 常驻)']
|
||||
content_preview = item['content'][:50].replace('\n', ' ') + "..."
|
||||
print(f"[{item['position']}] TRIGGERS: {triggers} -> CONTENT: {content_preview}")
|
||||
|
||||
# 保存世界书
|
||||
world_book.save()
|
||||
print(f"\n✅ 世界书已保存")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 错误: {e}")
|
||||
@@ -1,826 +0,0 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||
|
||||
# 配置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorldInfoPosition(Enum):
|
||||
"""
|
||||
SillyTavern 世界书条目插入位置枚举
|
||||
|
||||
注意:枚举值的顺序(0-4)并不完全代表物理顺序!
|
||||
以下是按照 Prompt 从上到下的真实物理顺序排列的:
|
||||
"""
|
||||
|
||||
# --- 1. 顶部区域 ---
|
||||
# (System Prompt 在这里,不可插入)
|
||||
|
||||
# --- 2. 核心指令区 (Position 4 实际上在这里) ---
|
||||
SYSTEM_PROMPT = 4
|
||||
"""
|
||||
物理位置:紧跟在系统提示词之后,角色定义之前。
|
||||
语境:最高优先级的规则。
|
||||
用途:作者注释、核心系统规则。AI 在读人设前就会先读到这个。
|
||||
"""
|
||||
|
||||
# --- 3. 角色人设区 (Position 0 实际上在这里) ---
|
||||
# (Character Definition 在这里)
|
||||
|
||||
CHAR_AFTER = 0
|
||||
"""
|
||||
物理位置:紧跟在角色定义之后。
|
||||
语境:角色固有属性。
|
||||
用途:性格、外貌、长期设定。
|
||||
"""
|
||||
|
||||
# --- 4. 示例对话区 ---
|
||||
EXAMPLE_BEFORE = 2
|
||||
"""
|
||||
物理位置:在示例对话块之前。
|
||||
"""
|
||||
|
||||
EXAMPLE_AFTER = 3
|
||||
"""
|
||||
物理位置:在示例对话块之后。
|
||||
"""
|
||||
|
||||
# --- 5. 底部区域 ---
|
||||
# (Chat History 在这里)
|
||||
# (User Input 在这里 - 最新输入)
|
||||
|
||||
# --- 6. 动态深度区 (Depth / d0-d99) ---
|
||||
# 这是你强调的"第 6 个插入区"
|
||||
# 它不是一个固定的物理点,而是一个动态区域
|
||||
|
||||
DEPTH_HISTORY = 4
|
||||
"""
|
||||
物理位置:
|
||||
- d0: 在 [用户最新输入] 之前,[AI 回复] 之前。
|
||||
- d0~d99: 在 [Chat History] 内部,倒数第 N 条消息之前。
|
||||
|
||||
语境:
|
||||
- d0: 即时状态("现在正在发生")。
|
||||
- d1+: 历史背景("当时就在那里")。
|
||||
|
||||
用途:
|
||||
这是最灵活的插入区,利用 Depth 字段来精确控制条目在对话流中的位置。
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_description(cls, position: int) -> str:
|
||||
"""
|
||||
获取位置描述
|
||||
|
||||
Args:
|
||||
position: 位置值
|
||||
|
||||
Returns:
|
||||
str: 位置描述
|
||||
"""
|
||||
position_map = {
|
||||
0: "角色定义之后",
|
||||
1: "角色定义之后 (最常用)",
|
||||
2: "示例对话之前",
|
||||
3: "示例对话之后",
|
||||
4: "系统提示 / 作者注释 (底部) 或 历史记录深度插入"
|
||||
}
|
||||
return position_map.get(position, "未知位置")
|
||||
|
||||
@classmethod
|
||||
def is_depth_position(cls, position: int) -> bool:
|
||||
"""
|
||||
判断是否为深度插入位置
|
||||
|
||||
Args:
|
||||
position: 位置值
|
||||
|
||||
Returns:
|
||||
bool: 是否为深度插入位置
|
||||
"""
|
||||
return position == cls.DEPTH_HISTORY.value
|
||||
|
||||
|
||||
class TriggerStrategy(str, Enum):
|
||||
"""
|
||||
触发策略枚举
|
||||
"""
|
||||
CONSTANT = "constant" # 永久触发
|
||||
KEYWORD = "keyword" # 关键词匹配触发
|
||||
RAG = "rag" # 向量检索触发
|
||||
CONDITION = "condition" # 逻辑条件触发
|
||||
|
||||
|
||||
class RAGTriggerConfig(BaseModel):
|
||||
"""
|
||||
RAG触发配置
|
||||
"""
|
||||
threshold: float = Field(0.75, description="RAG 相似度阈值")
|
||||
top_k: int = Field(5, description="返回的匹配条目数")
|
||||
query_template: Optional[str] = Field(None, description="检索用的查询模板")
|
||||
|
||||
|
||||
class KeywordTriggerConfig(BaseModel):
|
||||
"""
|
||||
关键词触发配置
|
||||
"""
|
||||
key: List[str] = Field(default_factory=list, description="主关键词数组")
|
||||
keysecondary: List[str] = Field(default_factory=list, description="次要关键词数组")
|
||||
selective: bool = Field(True, description="是否开启选择性匹配")
|
||||
selectiveLogic: int = Field(0, description="逻辑模式 (0=OR, 1=AND)")
|
||||
matchWholeWords: bool = Field(False, description="是否全词匹配")
|
||||
caseSensitive: bool = Field(False, description="是否区分大小写")
|
||||
|
||||
|
||||
class ConditionTriggerConfig(BaseModel):
|
||||
"""
|
||||
条件触发配置
|
||||
"""
|
||||
variable_a: str = Field(..., description="变量a")
|
||||
operator: str = Field(..., description="运算符 (>, <, =, >=, <=, !=)")
|
||||
variable_b: str = Field(..., description="变量b")
|
||||
|
||||
|
||||
class TriggerConfig(BaseModel):
|
||||
"""
|
||||
触发配置
|
||||
使用字典结构,键为触发策略,值为[是否启用, 对应配置]的列表
|
||||
"""
|
||||
triggers: Dict[TriggerStrategy, List[
|
||||
Union[bool, Optional[Union[KeywordTriggerConfig, RAGTriggerConfig, ConditionTriggerConfig]]]]] = Field(
|
||||
default_factory=lambda: {
|
||||
TriggerStrategy.CONSTANT: [True, None],
|
||||
TriggerStrategy.KEYWORD: [False, None],
|
||||
TriggerStrategy.RAG: [False, None],
|
||||
TriggerStrategy.CONDITION: [False, None]
|
||||
},
|
||||
description="触发配置字典,键为触发策略,值为[是否启用, 对应配置]"
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
def set_trigger(self, strategy: TriggerStrategy, enabled: bool,
|
||||
config: Optional[Union[KeywordTriggerConfig, RAGTriggerConfig, ConditionTriggerConfig]] = None
|
||||
):
|
||||
"""
|
||||
设置触发策略
|
||||
|
||||
Args:
|
||||
strategy: 触发策略
|
||||
enabled: 是否启用
|
||||
config: 对应的配置对象
|
||||
"""
|
||||
self.triggers[strategy] = [enabled, config]
|
||||
|
||||
def get_trigger(self, strategy: TriggerStrategy) -> List[
|
||||
Union[bool, Optional[Union[KeywordTriggerConfig, RAGTriggerConfig, ConditionTriggerConfig]]]]:
|
||||
"""
|
||||
获取触发策略
|
||||
|
||||
Args:
|
||||
strategy: 触发策略
|
||||
|
||||
Returns:
|
||||
List: [是否启用, 对应配置]
|
||||
"""
|
||||
return self.triggers.get(strategy, [False, None])
|
||||
|
||||
def get_enabled_triggers(self) -> List[TriggerStrategy]:
|
||||
"""
|
||||
获取所有启用的触发策略
|
||||
|
||||
Returns:
|
||||
List[TriggerStrategy]: 启用的触发策略列表
|
||||
"""
|
||||
return [strategy for strategy, (enabled, _) in self.triggers.items() if enabled]
|
||||
|
||||
|
||||
class WorldItem(BaseModel):
|
||||
"""
|
||||
世界书条目完整模型
|
||||
包含所有 SillyTavern 世界书条目属性,用于导入导出
|
||||
"""
|
||||
|
||||
class Entry(BaseModel):
|
||||
"""
|
||||
世界书条目模型
|
||||
精简版,只包含必要字段,用于实际使用
|
||||
"""
|
||||
# 基础定义
|
||||
uid: int = Field(..., description="唯一标识符")
|
||||
content: str = Field(..., description="注入到 Prompt 的实际文本内容")
|
||||
comment: str = Field("", description="条目名、备注")
|
||||
|
||||
# 注入与排序
|
||||
position: int = Field(0,
|
||||
description="插入位置 (0=角色定义之前, 1=角色定义之后, 2=示例对话之前, 3=示例对话之后, 4=系统提示/作者注释)")
|
||||
order: int = Field(100, description="注入顺序权重,数字越小优先级越高")
|
||||
depth: int = Field(4, description="扫描深度,0为最深/最高,4为标准")
|
||||
|
||||
# 触发配置
|
||||
trigger_config: Optional[TriggerConfig] = Field(
|
||||
default_factory=TriggerConfig,
|
||||
description="触发配置,为空表示无需触发配置"
|
||||
)
|
||||
# 角色匹配
|
||||
role: int = Field(0, description="角色匹配 (0=Both, 1=User, 2=Assistant)")
|
||||
|
||||
# 条目启用状态
|
||||
enabled: bool = Field(True, description="条目是否启用(启用才会被插入到LLM)")
|
||||
|
||||
@field_validator('position')
|
||||
@classmethod
|
||||
def validate_position(cls, v):
|
||||
"""验证 position 值是否在有效范围内"""
|
||||
if v not in [0, 1, 2, 3, 4]:
|
||||
logger.warning(f"无效的 position 值: {v},将使用默认值 1")
|
||||
return 1
|
||||
return v
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
转换为字典
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 字典数据
|
||||
"""
|
||||
return self.dict()
|
||||
|
||||
def get_trigger_params(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取触发策略所需的参数
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 触发参数字典
|
||||
"""
|
||||
params = {}
|
||||
|
||||
try:
|
||||
# 获取所有启用的触发策略
|
||||
enabled_triggers = self.trigger_config.get_enabled_triggers()
|
||||
|
||||
# 处理 RAG 触发
|
||||
if TriggerStrategy.RAG in enabled_triggers:
|
||||
_, rag_config = self.trigger_config.get_trigger(TriggerStrategy.RAG)
|
||||
if rag_config:
|
||||
params["threshold"] = rag_config.threshold
|
||||
params["top_k"] = rag_config.top_k
|
||||
params["query_template"] = rag_config.query_template
|
||||
params["vectorized"] = True
|
||||
|
||||
# 处理关键词触发
|
||||
if TriggerStrategy.KEYWORD in enabled_triggers:
|
||||
_, keyword_config = self.trigger_config.get_trigger(TriggerStrategy.KEYWORD)
|
||||
if keyword_config:
|
||||
params["key"] = keyword_config.key
|
||||
params["keysecondary"] = keyword_config.keysecondary
|
||||
params["selective"] = keyword_config.selective
|
||||
params["selectiveLogic"] = keyword_config.selectiveLogic
|
||||
params["matchWholeWords"] = keyword_config.matchWholeWords
|
||||
params["caseSensitive"] = keyword_config.caseSensitive
|
||||
|
||||
# 处理条件触发
|
||||
if TriggerStrategy.CONDITION in enabled_triggers:
|
||||
_, condition_config = self.trigger_config.get_trigger(TriggerStrategy.CONDITION)
|
||||
if condition_config:
|
||||
params["variable_a"] = condition_config.variable_a
|
||||
params["operator"] = condition_config.operator
|
||||
params["variable_b"] = condition_config.variable_b
|
||||
except Exception as e:
|
||||
# 如果获取触发参数失败,返回空字典,表示使用默认的永久触发
|
||||
logger.warning(f"条目 {self.uid} 的触发参数获取失败: {str(e)},使用默认的永久触发")
|
||||
|
||||
return params
|
||||
|
||||
# 基础定义
|
||||
uid: int = Field(..., description="唯一标识符")
|
||||
content: str = Field(..., description="注入到 Prompt 的实际文本内容")
|
||||
comment: str = Field("", description="条目名、备注")
|
||||
|
||||
# 注入与排序
|
||||
position: int = Field(0,
|
||||
description="插入位置 (0=角色定义之前, 1=角色定义之后, 2=示例对话之前, 3=示例对话之后, 4=系统提示/作者注释)")
|
||||
order: int = Field(100, description="注入顺序权重,数字越小优先级越高")
|
||||
depth: int = Field(4, description="扫描深度,0为最深/最高,4为标准")
|
||||
|
||||
# 触发配置
|
||||
trigger_config: TriggerConfig = Field(
|
||||
default_factory=TriggerConfig,
|
||||
description="触发配置"
|
||||
)
|
||||
|
||||
# 角色匹配
|
||||
role: int = Field(0, description="角色匹配 (0=Both, 1=User, 2=Assistant)")
|
||||
|
||||
# 条目启用状态
|
||||
enabled: bool = Field(True, description="条目是否启用(启用才会被插入到LLM)")
|
||||
|
||||
# 触发相关属性
|
||||
vectorized: bool = Field(False, description="是否使用向量检索(RAG触发)")
|
||||
selective: bool = Field(True, description="是否开启选择性匹配(关键词触发)")
|
||||
selectiveLogic: int = Field(0, description="逻辑模式 (0=OR, 1=AND)")
|
||||
constant: bool = Field(False, description="是否永久触发")
|
||||
|
||||
# 关键词相关
|
||||
key: List[str] = Field(default_factory=list, description="主关键词数组")
|
||||
keysecondary: List[str] = Field(default_factory=list, description="次要关键词数组")
|
||||
matchWholeWords: Optional[bool] = Field(None, description="是否全词匹配")
|
||||
caseSensitive: Optional[bool] = Field(None, description="是否区分大小写")
|
||||
|
||||
# RAG相关
|
||||
rag_threshold: Optional[float] = Field(None, description="RAG 相似度阈值")
|
||||
top_k: Optional[int] = Field(None, description="返回的匹配条目数")
|
||||
query_template: Optional[str] = Field(None, description="检索用的查询模板")
|
||||
|
||||
# 条目控制
|
||||
addMemo: bool = Field(True, description="是否添加备忘")
|
||||
disable: bool = Field(False, description="是否禁用")
|
||||
ignoreBudget: bool = Field(False, description="是否忽略预算")
|
||||
excludeRecursion: bool = Field(True, description="是否排除递归")
|
||||
preventRecursion: bool = Field(True, description="是否阻止递归")
|
||||
matchPersonaDescription: bool = Field(False, description="是否匹配人设描述")
|
||||
matchCharacterDescription: bool = Field(False, description="是否匹配角色描述")
|
||||
matchCharacterPersonality: bool = Field(False, description="是否匹配角色性格")
|
||||
matchCharacterDepthPrompt: bool = Field(False, description="是否匹配深度提示")
|
||||
matchScenario: bool = Field(False, description="是否匹配场景")
|
||||
matchCreatorNotes: bool = Field(False, description="是否匹配作者笔记")
|
||||
delayUntilRecursion: bool = Field(False, description="是否延迟递归")
|
||||
|
||||
# 概率相关
|
||||
probability: int = Field(100, description="触发概率 (0-100)")
|
||||
useProbability: bool = Field(True, description="是否使用概率")
|
||||
|
||||
# 分组相关
|
||||
group: str = Field("", description="分组名称")
|
||||
groupOverride: bool = Field(False, description="是否覆盖分组")
|
||||
groupWeight: int = Field(100, description="分组权重")
|
||||
useGroupScoring: bool = Field(False, description="是否使用分组评分")
|
||||
|
||||
# 其他属性
|
||||
scanDepth: Optional[int] = Field(None, description="扫描深度")
|
||||
automationId: str = Field("", description="自动化ID")
|
||||
sticky: int = Field(0, description="粘性")
|
||||
cooldown: int = Field(0, description="冷却时间(秒)")
|
||||
delay: int = Field(0, description="延迟时间(秒)")
|
||||
displayIndex: int = Field(0, description="显示索引")
|
||||
|
||||
# 角色过滤器
|
||||
characterFilter: Dict[str, Any] = Field(
|
||||
default_factory=lambda: {"isExclude": False, "names": [], "tags": []},
|
||||
description="角色过滤器"
|
||||
)
|
||||
|
||||
# 验证器
|
||||
@field_validator('position')
|
||||
@classmethod
|
||||
def validate_position(cls, v):
|
||||
"""验证 position 值是否在有效范围内"""
|
||||
if v not in [0, 1, 2, 3, 4]:
|
||||
logger.warning(f"无效的 position 值: {v},将使用默认值 1")
|
||||
return 1
|
||||
return v
|
||||
|
||||
@field_validator('role')
|
||||
@classmethod
|
||||
def validate_role(cls, v):
|
||||
"""验证 role 值是否在有效范围内"""
|
||||
if v not in [0, 1, 2]:
|
||||
logger.warning(f"无效的 role 值: {v},将使用默认值 2")
|
||||
return 2
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'WorldItem':
|
||||
"""
|
||||
从字典创建 WorldItem 对象
|
||||
|
||||
Args:
|
||||
data: 字典数据
|
||||
|
||||
Returns:
|
||||
WorldItem: WorldItem 对象
|
||||
"""
|
||||
return cls(**data)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
转换为字典
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 字典数据
|
||||
"""
|
||||
return self.dict()
|
||||
|
||||
def to_entry(self) -> Entry:
|
||||
"""
|
||||
转换为 Entry 对象
|
||||
|
||||
Returns:
|
||||
Entry: Entry 对象
|
||||
"""
|
||||
# 转换为 SillyTavern 格式的字典
|
||||
sillytavern_dict = self.to_sillytavern_dict()
|
||||
# 创建 Entry 对象
|
||||
return self.Entry(**sillytavern_dict)
|
||||
|
||||
def to_sillytavern_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
转换为 SillyTavern 格式的字典
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: SillyTavern 格式的条目数据
|
||||
"""
|
||||
result = {
|
||||
"uid": self.uid,
|
||||
"content": self.content,
|
||||
"comment": self.comment,
|
||||
"position": self.position,
|
||||
"order": self.order,
|
||||
"depth": self.depth,
|
||||
"role": self.role,
|
||||
"enabled": self.enabled,
|
||||
"vectorized": self.vectorized,
|
||||
"selective": self.selective,
|
||||
"selectiveLogic": self.selectiveLogic,
|
||||
"constant": self.constant,
|
||||
"key": self.key,
|
||||
"keysecondary": self.keysecondary,
|
||||
"matchWholeWords": self.matchWholeWords,
|
||||
"caseSensitive": self.caseSensitive,
|
||||
"addMemo": self.addMemo,
|
||||
"disable": self.disable,
|
||||
"ignoreBudget": self.ignoreBudget,
|
||||
"excludeRecursion": self.excludeRecursion,
|
||||
"preventRecursion": self.preventRecursion,
|
||||
"matchPersonaDescription": self.matchPersonaDescription,
|
||||
"matchCharacterDescription": self.matchCharacterDescription,
|
||||
"matchCharacterPersonality": self.matchCharacterPersonality,
|
||||
"matchCharacterDepthPrompt": self.matchCharacterDepthPrompt,
|
||||
"matchScenario": self.matchScenario,
|
||||
"matchCreatorNotes": self.matchCreatorNotes,
|
||||
"delayUntilRecursion": self.delayUntilRecursion,
|
||||
"probability": self.probability,
|
||||
"useProbability": self.useProbability,
|
||||
"group": self.group,
|
||||
"groupOverride": self.groupOverride,
|
||||
"groupWeight": self.groupWeight,
|
||||
"scanDepth": self.scanDepth,
|
||||
"automationId": self.automationId,
|
||||
"sticky": self.sticky,
|
||||
"cooldown": self.cooldown,
|
||||
"delay": self.delay,
|
||||
"displayIndex": self.displayIndex,
|
||||
"characterFilter": self.characterFilter
|
||||
}
|
||||
|
||||
# 添加 RAG 相关字段
|
||||
if self.vectorized:
|
||||
result["rag_threshold"] = self.rag_threshold
|
||||
result["top_k"] = self.top_k
|
||||
result["query_template"] = self.query_template
|
||||
|
||||
# 添加条件触发相关字段
|
||||
if TriggerStrategy.CONDITION in self.trigger_config.get_enabled_triggers():
|
||||
condition_config = self.trigger_config.get_trigger(TriggerStrategy.CONDITION)[1]
|
||||
if condition_config:
|
||||
result["variable_a"] = condition_config.variable_a
|
||||
result["operator"] = condition_config.operator
|
||||
result["variable_b"] = condition_config.variable_b
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_sillytavern_data(cls, data: Dict[str, Any]) -> 'WorldItem':
|
||||
"""
|
||||
从 SillyTavern 格式的数据创建 WorldItem 对象
|
||||
|
||||
Args:
|
||||
data: SillyTavern 格式的条目数据
|
||||
|
||||
Returns:
|
||||
WorldItem: WorldItem 对象
|
||||
"""
|
||||
|
||||
constant = data.get("constant", False)
|
||||
if isinstance(constant, str):
|
||||
constant = constant.lower() in ('true', '1', 'yes')
|
||||
|
||||
enabled = data.get("enabled", True)
|
||||
if isinstance(enabled, str):
|
||||
enabled = enabled.lower() in ('true', '1', 'yes')
|
||||
|
||||
try:
|
||||
# 提取必要字段
|
||||
uid = int(data.get("uid", data.get("id", 0)))
|
||||
content = data.get("content", "")
|
||||
comment = data.get("comment", "")
|
||||
position = data.get("position", 0)
|
||||
order = data.get("order", 100)
|
||||
depth = data.get("depth", 4)
|
||||
role = data.get("role", 0)
|
||||
enabled = data.get("enabled", True)
|
||||
|
||||
# 处理 position 字段,确保为整数类型
|
||||
if isinstance(position, str):
|
||||
try:
|
||||
position = int(position)
|
||||
except ValueError:
|
||||
logger.warning(f"条目 {uid} 的 position 字段值 '{position}' 无法转换为整数,使用默认值 0")
|
||||
position = 0
|
||||
|
||||
# 初始化触发配置
|
||||
trigger_config = TriggerConfig()
|
||||
|
||||
# 读取触发相关字段,并进行类型转换
|
||||
vectorized = data.get("vectorized", False)
|
||||
if isinstance(vectorized, str):
|
||||
vectorized = vectorized.lower() in ('true', '1', 'yes')
|
||||
|
||||
selective = data.get("selective", True)
|
||||
if isinstance(selective, str):
|
||||
selective = selective.lower() in ('true', '1', 'yes')
|
||||
|
||||
constant = data.get("constant", False)
|
||||
if isinstance(constant, str):
|
||||
constant = constant.lower() in ('true', '1', 'yes')
|
||||
|
||||
# 初始化变量,确保它们始终有值
|
||||
key = []
|
||||
keysecondary = []
|
||||
selectiveLogic = 0
|
||||
matchWholeWords = False
|
||||
caseSensitive = False
|
||||
|
||||
# 判断触发策略并设置对应的触发配置
|
||||
# 优先级:vectorized > constant > selective
|
||||
if vectorized:
|
||||
# RAG 触发
|
||||
rag_config = RAGTriggerConfig(
|
||||
threshold=float(data.get("rag_threshold", 0.75)),
|
||||
top_k=int(data.get("top_k", 5)),
|
||||
query_template=data.get("query_template", None)
|
||||
)
|
||||
trigger_config.set_trigger(TriggerStrategy.RAG, True, rag_config)
|
||||
elif constant:
|
||||
# 永久触发
|
||||
trigger_config.set_trigger(TriggerStrategy.CONSTANT, True)
|
||||
elif selective:
|
||||
# 关键词触发
|
||||
key = data.get("key", [])
|
||||
keysecondary = data.get("keysecondary", data.get("secondary_keys", []))
|
||||
selectiveLogic = int(data.get("selectiveLogic", 0))
|
||||
|
||||
# 处理 matchWholeWords 字段
|
||||
matchWholeWords = data.get("matchWholeWords", False)
|
||||
if matchWholeWords is None:
|
||||
matchWholeWords = False
|
||||
elif isinstance(matchWholeWords, str):
|
||||
matchWholeWords = matchWholeWords.lower() in ('true', '1', 'yes')
|
||||
|
||||
# 处理 caseSensitive 字段
|
||||
caseSensitive = data.get("caseSensitive", False)
|
||||
if caseSensitive is None:
|
||||
caseSensitive = False
|
||||
elif isinstance(caseSensitive, str):
|
||||
caseSensitive = caseSensitive.lower() in ('true', '1', 'yes')
|
||||
|
||||
keyword_config = KeywordTriggerConfig(
|
||||
key=key,
|
||||
keysecondary=keysecondary,
|
||||
selective=selective,
|
||||
selectiveLogic=selectiveLogic,
|
||||
matchWholeWords=matchWholeWords,
|
||||
caseSensitive=caseSensitive
|
||||
)
|
||||
trigger_config.set_trigger(TriggerStrategy.KEYWORD, True, keyword_config)
|
||||
else:
|
||||
# 默认使用永久触发
|
||||
trigger_config.set_trigger(TriggerStrategy.CONSTANT, True)
|
||||
|
||||
# 检查是否有条件触发(虽然 JSON 中没有对应字段,但需要保留兼容性)
|
||||
if "variable_a" in data and "operator" in data and "variable_b" in data:
|
||||
condition_config = ConditionTriggerConfig(
|
||||
variable_a=data.get("variable_a", ""),
|
||||
operator=data.get("operator", "="),
|
||||
variable_b=data.get("variable_b", "")
|
||||
)
|
||||
trigger_config.set_trigger(TriggerStrategy.CONDITION, True, condition_config)
|
||||
|
||||
# 创建 WorldItem 对象
|
||||
return cls(
|
||||
uid=uid,
|
||||
content=content,
|
||||
comment=comment,
|
||||
position=position,
|
||||
order=order,
|
||||
depth=depth,
|
||||
trigger_config=trigger_config,
|
||||
role=role,
|
||||
enabled=enabled,
|
||||
vectorized=vectorized,
|
||||
selective=selective,
|
||||
selectiveLogic=selectiveLogic,
|
||||
constant=constant,
|
||||
key=key,
|
||||
keysecondary=keysecondary,
|
||||
matchWholeWords=matchWholeWords,
|
||||
caseSensitive=caseSensitive,
|
||||
rag_threshold=float(data.get("rag_threshold", None)) if vectorized else None,
|
||||
top_k=int(data.get("top_k", None)) if vectorized else None,
|
||||
query_template=data.get("query_template", None),
|
||||
addMemo=data.get("addMemo", True),
|
||||
disable=data.get("disable", False),
|
||||
ignoreBudget=data.get("ignoreBudget", False),
|
||||
excludeRecursion=data.get("excludeRecursion", True),
|
||||
preventRecursion=data.get("preventRecursion", True),
|
||||
matchPersonaDescription=data.get("matchPersonaDescription", False),
|
||||
matchCharacterDescription=data.get("matchCharacterDescription", False),
|
||||
matchCharacterPersonality=data.get("matchCharacterPersonality", False),
|
||||
matchCharacterDepthPrompt=data.get("matchCharacterDepthPrompt", False),
|
||||
matchScenario=data.get("matchScenario", False),
|
||||
matchCreatorNotes=data.get("matchCreatorNotes", False),
|
||||
delayUntilRecursion=data.get("delayUntilRecursion", False),
|
||||
probability=data.get("probability", 100),
|
||||
useProbability=data.get("useProbability", True),
|
||||
group=data.get("group", ""),
|
||||
groupOverride=data.get("groupOverride", False),
|
||||
groupWeight=data.get("groupWeight", 100),
|
||||
useGroupScoring=data.get("useGroupScoring", False),
|
||||
scanDepth=data.get("scanDepth", None),
|
||||
automationId=data.get("automationId", ""),
|
||||
sticky=data.get("sticky", 0),
|
||||
cooldown=data.get("cooldown", 0),
|
||||
delay=data.get("delay", 0),
|
||||
displayIndex=data.get("displayIndex", 0),
|
||||
characterFilter=data.get("characterFilter", {"isExclude": False, "names": [], "tags": []})
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"从 SillyTavern 数据创建 WorldItem 失败: {str(e)}")
|
||||
raise ValueError(f"从 SillyTavern 数据创建 WorldItem 失败: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
测试入口:用于调试 WorldItem 的解析和转换功能
|
||||
可以像断点调试一样查看内部执行过程
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 测试用例1:基本条目
|
||||
test_data_1 = {
|
||||
"uid": 0,
|
||||
"content": "测试内容",
|
||||
"comment": "测试条目",
|
||||
"position": 0,
|
||||
"order": 100,
|
||||
"depth": 4,
|
||||
"role": 0,
|
||||
"vectorized": False,
|
||||
"selective": True,
|
||||
"selectiveLogic": 0,
|
||||
"constant": False,
|
||||
"key": ["测试关键词"],
|
||||
"keysecondary": [],
|
||||
"matchWholeWords": False,
|
||||
"caseSensitive": False,
|
||||
"addMemo": True,
|
||||
"disable": False,
|
||||
"ignoreBudget": False,
|
||||
"excludeRecursion": True,
|
||||
"preventRecursion": True
|
||||
}
|
||||
|
||||
# 测试用例2:RAG触发
|
||||
test_data_2 = {
|
||||
"uid": 1,
|
||||
"content": "RAG测试内容",
|
||||
"comment": "RAG测试条目",
|
||||
"position": 4,
|
||||
"order": 50,
|
||||
"depth": 0,
|
||||
"role": 0,
|
||||
"vectorized": True,
|
||||
"rag_threshold": 0.8,
|
||||
"top_k": 10,
|
||||
"query_template": "测试模板"
|
||||
}
|
||||
|
||||
# 测试用例3:条件触发
|
||||
test_data_3 = {
|
||||
"uid": 2,
|
||||
"content": "条件触发测试",
|
||||
"comment": "条件触发条目",
|
||||
"position": 1,
|
||||
"order": 75,
|
||||
"depth": 2,
|
||||
"role": 0,
|
||||
"variable_a": "好感度",
|
||||
"operator": ">",
|
||||
"variable_b": "50"
|
||||
}
|
||||
|
||||
print("=" * 60)
|
||||
print("开始测试 WorldItem 解析功能")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# 测试1:解析基本条目
|
||||
print("\n【测试1】解析基本条目...")
|
||||
item1 = WorldItem.from_sillytavern_data(test_data_1)
|
||||
print(f"✓ 解析成功: {item1.comment}")
|
||||
print(f" - UID: {item1.uid}")
|
||||
print(f" - Position: {item1.position}")
|
||||
print(f" - 触发策略和配置:")
|
||||
for strategy in item1.trigger_config.get_enabled_triggers():
|
||||
enabled, config = item1.trigger_config.get_trigger(strategy)
|
||||
print(f" * {strategy.value}: 启用={enabled}, 配置={config}")
|
||||
|
||||
# 测试2:解析RAG触发条目
|
||||
print("\n【测试2】解析RAG触发条目...")
|
||||
item2 = WorldItem.from_sillytavern_data(test_data_2)
|
||||
print(f"✓ 解析成功: {item2.comment}")
|
||||
print(f" - UID: {item2.uid}")
|
||||
print(f" - Position: {item2.position}")
|
||||
print(f" - 触发策略和配置:")
|
||||
for strategy in item2.trigger_config.get_enabled_triggers():
|
||||
enabled, config = item2.trigger_config.get_trigger(strategy)
|
||||
print(f" * {strategy.value}: 启用={enabled}, 配置={config}")
|
||||
if item2.rag_threshold:
|
||||
print(f" - RAG阈值: {item2.rag_threshold}")
|
||||
|
||||
# 测试3:解析条件触发条目
|
||||
print("\n【测试3】解析条件触发条目...")
|
||||
item3 = WorldItem.from_sillytavern_data(test_data_3)
|
||||
print(f"✓ 解析成功: {item3.comment}")
|
||||
print(f" - UID: {item3.uid}")
|
||||
print(f" - Position: {item3.position}")
|
||||
print(f" - 触发策略和配置:")
|
||||
for strategy in item3.trigger_config.get_enabled_triggers():
|
||||
enabled, config = item3.trigger_config.get_trigger(strategy)
|
||||
print(f" * {strategy.value}: 启用={enabled}, 配置={config}")
|
||||
|
||||
# 测试4:从文件读取实际数据
|
||||
print("\n【测试4】从实际JSON文件读取...")
|
||||
# 从当前文件位置向上查找项目根目录
|
||||
current_file = Path(__file__).resolve()
|
||||
project_root = current_file
|
||||
while project_root.name != "llm_workflow_engine" and project_root.parent != project_root:
|
||||
project_root = project_root.parent
|
||||
|
||||
# 构建正确的文件路径
|
||||
json_path = project_root / "data" / "worldbooks" / "卡立创-v5.json"
|
||||
print(f"查找文件路径: {json_path}")
|
||||
|
||||
if json_path.exists():
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
worldbook_data = json.load(f)
|
||||
entries = worldbook_data.get('entries', {})
|
||||
print(f"找到 {len(entries)} 个条目")
|
||||
|
||||
# 只测试前3个条目
|
||||
for uid, entry_data in list(entries.items())[:3]:
|
||||
try:
|
||||
item = WorldItem.from_sillytavern_data(entry_data)
|
||||
print(f"\n✓ 条目 {uid} 解析成功:")
|
||||
print(f" - 备注: {item.comment}")
|
||||
print(f" - UID: {item.uid}")
|
||||
print(f" - Position: {item.position}")
|
||||
print(f" - 触发策略和配置:")
|
||||
for strategy in item.trigger_config.get_enabled_triggers():
|
||||
enabled, config = item.trigger_config.get_trigger(strategy)
|
||||
print(f" * {strategy.value}: 启用={enabled}, 配置={config}")
|
||||
except Exception as e:
|
||||
print(f"\n✗ 条目 {uid} 解析失败: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print(f"⚠ 文件不存在: {json_path}")
|
||||
print(f"请确认文件路径是否正确")
|
||||
# 列出可能的文件位置
|
||||
possible_paths = [
|
||||
project_root / "data" / "worldbooks",
|
||||
project_root / "backend" / "data" / "worldbooks",
|
||||
current_file.parent.parent.parent / "data" / "worldbooks"
|
||||
]
|
||||
print("\n可能的文件位置:")
|
||||
for path in possible_paths:
|
||||
if path.exists():
|
||||
print(f" ✓ {path}")
|
||||
for file in path.glob("*.json"):
|
||||
print(f" - {file.name}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有测试完成!")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ 测试失败: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -1,443 +0,0 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field
|
||||
import json
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""消息类,代表JSONL文件中的一行消息内容"""
|
||||
name: str = Field(..., description="发送者名称")
|
||||
is_user: bool = Field(..., description="是否为用户消息")
|
||||
is_system: bool = Field(False, description="是否为系统消息")
|
||||
send_date: str = Field(
|
||||
default_factory=lambda: str(int(datetime.now().timestamp() * 1000)),
|
||||
description="消息发送时间戳"
|
||||
)
|
||||
floor: int = Field(0, description="对话楼层数")
|
||||
swipes: List[str] = Field(
|
||||
default_factory=list,
|
||||
description="历史版本列表。用户消息:存编辑过的不同版本。AI消息:存重roll生成的不同版本"
|
||||
)
|
||||
swipe_id: int = Field(
|
||||
0,
|
||||
description="当前指针。指示当前显示的是 swipes 数组中的第几个(从 0 开始)"
|
||||
)
|
||||
mes: str = Field(..., description="消息内容文本")
|
||||
extra: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="额外信息,包含推理内容、API、模型等"
|
||||
)
|
||||
force_avatar: Optional[str] = Field(None, description="强制头像URL")
|
||||
variables: List[Any] = Field(default_factory=list, description="消息变量列表")
|
||||
variables_initialized: List[bool] = Field(default_factory=list, description="变量初始化状态数组")
|
||||
is_ejs_processed: List[bool] = Field(default_factory=list, description="EJS处理状态数组")
|
||||
|
||||
# 以下属性仅在is_user为False时有值
|
||||
api: Optional[str] = Field(None, description="使用的API提供商")
|
||||
model: Optional[str] = Field(None, description="使用的AI模型")
|
||||
reasoning: Optional[str] = Field(None, description="推理内容")
|
||||
reasoning_duration: Optional[float] = Field(None, description="推理耗时")
|
||||
reasoning_signature: Optional[str] = Field(None, description="推理签名")
|
||||
time_to_first_token: Optional[float] = Field(None, description="首Token响应时间")
|
||||
bias: Optional[float] = Field(None, description="偏差值")
|
||||
|
||||
|
||||
class ChatMetadata(BaseModel):
|
||||
"""聊天元数据类,包含整个聊天的共享属性"""
|
||||
user_name: str = Field("User", description="用户名称")
|
||||
character_name: str = Field("Assistant", description="角色名称")
|
||||
|
||||
# 完整性校验相关
|
||||
integrity: str = Field("", description="完整性校验值")
|
||||
chat_id_hash: str = Field("", description="聊天ID哈希值")
|
||||
|
||||
# 笔记相关
|
||||
note_prompt: str = Field("", description="作者笔记提示词")
|
||||
note_interval: int = Field(0, description="笔记插入间隔数")
|
||||
note_position: int = Field(0, description="笔记插入位置")
|
||||
note_depth: int = Field(0, description="笔记插入深度")
|
||||
# 0:System,1:User,2:Assistant
|
||||
note_role: int = Field("", description="笔记使用角色类型")
|
||||
|
||||
# 扩展信息
|
||||
extensions: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="扩展信息,如LittleWhiteBox等"
|
||||
)
|
||||
# 世界信息
|
||||
timedWorldInfo: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="定时世界信息"
|
||||
)
|
||||
# 变量
|
||||
variables: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="变量字典"
|
||||
)
|
||||
# 状态标记
|
||||
tainted: bool = Field(False, description="是否被修改标记")
|
||||
lastInContextMessageId: int = Field(-1, description="最后上下文消息ID")
|
||||
|
||||
|
||||
class ChatHistory(BaseModel):
|
||||
"""聊天文件类,包含完整的聊天记录"""
|
||||
chat_metadata: ChatMetadata = Field(..., description="聊天元数据,包含基本信息和配置")
|
||||
messages: List[Message] = Field(default_factory=list, description="消息列表")
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@classmethod
|
||||
def get_data_path(cls) -> Path:
|
||||
"""获取数据目录路径"""
|
||||
try:
|
||||
from backend.core.config import settings
|
||||
return settings.DATA_PATH / "chat"
|
||||
except ImportError:
|
||||
return Path("data")
|
||||
|
||||
@classmethod
|
||||
async def list_all_chats(cls) -> Dict[str, List[Dict]]:
|
||||
"""获取所有角色的所有聊天列表"""
|
||||
data_dir = cls.get_data_path()
|
||||
if not data_dir.exists():
|
||||
return {"chat": []}
|
||||
|
||||
chats = []
|
||||
for role_dir in data_dir.iterdir():
|
||||
if role_dir.is_dir():
|
||||
for chat_file in role_dir.glob("*.jsonl"):
|
||||
try:
|
||||
with open(chat_file, 'r', encoding='utf-8') as f:
|
||||
# 读取第一行获取元数据
|
||||
first_line = f.readline()
|
||||
metadata = json.loads(first_line)
|
||||
chats.append({
|
||||
"role_name": role_dir.name,
|
||||
"chat_name": chat_file.stem,
|
||||
"user_name": metadata.get("user_name", "User"),
|
||||
"character_name": metadata.get("character_name", "Assistant"),
|
||||
"last_modified": metadata.get("last_modified", ""),
|
||||
"message_count": sum(1 for _ in f) # 统计剩余行数(消息数)
|
||||
})
|
||||
except Exception:
|
||||
continue # 跳过损坏的聊天文件
|
||||
return {"chat": chats}
|
||||
|
||||
@classmethod
|
||||
async def get_chat(cls, role_name: str, chat_name: str) -> Dict[str, Any]:
|
||||
"""获取指定聊天的完整内容"""
|
||||
chat_history = cls.load_from_file(role_name, chat_name)
|
||||
return {
|
||||
"metadata": chat_history.chat_metadata.dict(),
|
||||
"messages": chat_history.to_chatbox_format()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def create_chat(cls, role_name: str, chat_name: str, metadata: Optional[Dict] = None) -> Dict[str, str]:
|
||||
"""创建新聊天"""
|
||||
base_path = cls.get_data_path()
|
||||
role_dir = base_path / role_name
|
||||
role_dir.mkdir(parents=True, exist_ok=True)
|
||||
chat_path = role_dir / f"{chat_name}.jsonl"
|
||||
|
||||
if chat_path.exists():
|
||||
raise FileExistsError(f"Chat already exists: {chat_path}")
|
||||
|
||||
# 创建聊天历史对象
|
||||
chat_history = cls(
|
||||
chat_metadata=ChatMetadata(**(metadata or {})),
|
||||
messages=[]
|
||||
)
|
||||
|
||||
# 保存到文件
|
||||
chat_history.save_to_file(role_name, chat_name, base_path)
|
||||
return {"message": "Chat created successfully"}
|
||||
|
||||
@classmethod
|
||||
async def update_chat(cls, role_name: str, chat_name: str, update_data: Dict) -> Dict[str, str]:
|
||||
"""更新聊天元数据"""
|
||||
chat_history = cls.load_from_file(role_name, chat_name)
|
||||
|
||||
# 更新元数据
|
||||
if "metadata" in update_data:
|
||||
for key, value in update_data["metadata"].items():
|
||||
if hasattr(chat_history.chat_metadata, key):
|
||||
setattr(chat_history.chat_metadata, key, value)
|
||||
|
||||
# 保存更改
|
||||
base_path = cls.get_data_path()
|
||||
chat_history.save_to_file(role_name, chat_name, base_path)
|
||||
return {"message": "Chat metadata updated successfully"}
|
||||
|
||||
@classmethod
|
||||
async def delete_chat(cls, role_name: str, chat_name: str) -> Dict[str, str]:
|
||||
"""删除指定聊天"""
|
||||
base_path = cls.get_data_path()
|
||||
chat_path = base_path / role_name / f"{chat_name}.jsonl"
|
||||
|
||||
if not chat_path.exists():
|
||||
raise FileNotFoundError(f"Chat not found: {chat_path}")
|
||||
|
||||
chat_path.unlink()
|
||||
return {"message": "Chat deleted successfully"}
|
||||
|
||||
@classmethod
|
||||
async def list_messages(cls, role_name: str, chat_name: str) -> Dict[str, List[Dict]]:
|
||||
"""获取聊天的所有消息"""
|
||||
chat_history = cls.load_from_file(role_name, chat_name)
|
||||
return {"messages": chat_history.to_chatbox_format()}
|
||||
|
||||
@classmethod
|
||||
async def get_message(cls, role_name: str, chat_name: str, floor: int) -> Dict[str, Any]:
|
||||
"""获取指定楼层的消息"""
|
||||
chat_history = cls.load_from_file(role_name, chat_name)
|
||||
message = next((msg for msg in chat_history.messages if msg.floor == floor), None)
|
||||
|
||||
if not message:
|
||||
raise FileNotFoundError(f"Message not found: floor {floor}")
|
||||
|
||||
return message.dict()
|
||||
|
||||
@classmethod
|
||||
async def add_message(cls, role_name: str, chat_name: str, message_data: Dict) -> Dict[str, Any]:
|
||||
"""向聊天添加新消息"""
|
||||
chat_history = cls.load_from_file(role_name, chat_name)
|
||||
|
||||
# 创建消息对象
|
||||
message = Message(**message_data)
|
||||
|
||||
# 检查楼层是否已存在
|
||||
if any(msg.floor == message.floor for msg in chat_history.messages):
|
||||
raise ValueError(f"Message floor already exists: {message.floor}")
|
||||
|
||||
# 添加消息
|
||||
chat_history.messages.append(message)
|
||||
|
||||
# 保存更改
|
||||
base_path = cls.get_data_path()
|
||||
chat_history.save_to_file(role_name, chat_name, base_path)
|
||||
return {"message": "Message added successfully", "floor": message.floor}
|
||||
|
||||
@classmethod
|
||||
async def update_message(cls, role_name: str, chat_name: str, floor: int, update_data: Dict) -> Dict[str, str]:
|
||||
"""更新指定楼层的消息"""
|
||||
chat_history = cls.load_from_file(role_name, chat_name)
|
||||
message = next((msg for msg in chat_history.messages if msg.floor == floor), None)
|
||||
|
||||
if not message:
|
||||
raise FileNotFoundError(f"Message not found: floor {floor}")
|
||||
|
||||
# 更新消息字段
|
||||
for key, value in update_data.items():
|
||||
if hasattr(message, key):
|
||||
setattr(message, key, value)
|
||||
|
||||
# 保存更改
|
||||
base_path = cls.get_data_path()
|
||||
chat_history.save_to_file(role_name, chat_name, base_path)
|
||||
return {"message": "Message updated successfully"}
|
||||
|
||||
@classmethod
|
||||
async def delete_message(cls, role_name: str, chat_name: str, floor: int) -> Dict[str, str]:
|
||||
"""删除指定楼层的消息"""
|
||||
chat_history = cls.load_from_file(role_name, chat_name)
|
||||
|
||||
# 查找并删除消息
|
||||
original_length = len(chat_history.messages)
|
||||
chat_history.messages = [msg for msg in chat_history.messages if msg.floor != floor]
|
||||
|
||||
if len(chat_history.messages) == original_length:
|
||||
raise FileNotFoundError(f"Message not found: floor {floor}")
|
||||
|
||||
# 保存更改
|
||||
base_path = cls.get_data_path()
|
||||
chat_history.save_to_file(role_name, chat_name, base_path)
|
||||
return {"message": "Message deleted successfully"}
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls, role_name: str, chat_name: str, base_path: Path = None) -> 'ChatHistory':
|
||||
"""
|
||||
从JSONL文件加载聊天历史
|
||||
|
||||
参数:
|
||||
role_name: 角色名称(文件夹名)
|
||||
chat_name: 聊天名称(文件名,不含扩展名)
|
||||
base_path: 基础路径,默认为配置中的DATA_PATH/chat
|
||||
|
||||
返回:
|
||||
ChatHistory: 加载的聊天历史对象
|
||||
|
||||
异常:
|
||||
FileNotFoundError: 当文件不存在时抛出
|
||||
json.JSONDecodeError: 当JSON解析失败时抛出
|
||||
"""
|
||||
# 设置默认基础路径
|
||||
if base_path is None:
|
||||
base_path = cls.get_data_path()
|
||||
|
||||
# 构建文件路径
|
||||
file_path = base_path / role_name / f"{chat_name}.jsonl"
|
||||
|
||||
# 检查文件是否存在
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"聊天文件不存在: {file_path}")
|
||||
|
||||
# 初始化结果数据
|
||||
messages = []
|
||||
metadata = None
|
||||
|
||||
# 读取文件内容
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f):
|
||||
try:
|
||||
line_data = json.loads(line.strip())
|
||||
|
||||
# 第一行是元数据
|
||||
if line_num == 0:
|
||||
metadata = ChatMetadata(**line_data)
|
||||
else:
|
||||
# 后续行是消息
|
||||
messages.append(Message(**line_data))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# 创建并返回ChatHistory对象
|
||||
return cls(
|
||||
chat_metadata=metadata or ChatMetadata(),
|
||||
messages=messages
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load_from_jsonl(cls, file_path: Path) -> 'ChatHistory':
|
||||
"""
|
||||
从JSONL文件加载聊天历史
|
||||
|
||||
参数:
|
||||
file_path: JSONL文件路径
|
||||
|
||||
返回:
|
||||
ChatHistory: 加载的聊天历史对象
|
||||
|
||||
异常:
|
||||
FileNotFoundError: 当文件不存在时抛出
|
||||
json.JSONDecodeError: 当JSON解析失败时抛出
|
||||
"""
|
||||
# 检查文件是否存在
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"聊天文件不存在: {file_path}")
|
||||
|
||||
# 初始化结果数据
|
||||
messages = []
|
||||
metadata = None
|
||||
|
||||
# 读取文件内容
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f):
|
||||
try:
|
||||
line_data = json.loads(line.strip())
|
||||
|
||||
# 第一行是元数据
|
||||
if line_num == 0:
|
||||
# 处理元数据中的嵌套结构
|
||||
if 'chat_metadata' in line_data:
|
||||
metadata_dict = line_data['chat_metadata']
|
||||
# 合并顶层字段和chat_metadata中的字段
|
||||
metadata_dict.update(line_data)
|
||||
metadata = ChatMetadata(**metadata_dict)
|
||||
else:
|
||||
metadata = ChatMetadata(**line_data)
|
||||
else:
|
||||
# 后续行是消息
|
||||
# 处理extra字段中的内容
|
||||
extra_data = line_data.get('extra', {})
|
||||
|
||||
# 如果是AI消息(is_user=False),将extra中的某些字段提升到顶层
|
||||
if not line_data.get('is_user', True):
|
||||
ai_fields = ['api', 'model', 'reasoning', 'reasoning_duration',
|
||||
'reasoning_signature', 'time_to_first_token', 'bias']
|
||||
for field in ai_fields:
|
||||
if field in extra_data:
|
||||
line_data[field] = extra_data.pop(field)
|
||||
|
||||
# 创建Message实例
|
||||
message = Message(**line_data)
|
||||
# 将剩余的extra数据保存回extra字段
|
||||
message.extra = extra_data
|
||||
messages.append(message)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# 创建并返回ChatHistory对象
|
||||
return cls(
|
||||
chat_metadata=metadata or ChatMetadata(),
|
||||
messages=messages
|
||||
)
|
||||
|
||||
def to_chatbox_format(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
将聊天历史转换为适合前端chatbox显示的格式
|
||||
|
||||
返回:
|
||||
List[Dict[str, Any]]: 按floor排序的消息字典列表,每个字典包含:
|
||||
{
|
||||
"name": str,
|
||||
"is_user": bool,
|
||||
"floor": int,
|
||||
"mes": str,
|
||||
"swipes": List[str],
|
||||
"swipe_id": int
|
||||
}
|
||||
"""
|
||||
# 创建消息字典列表
|
||||
messages_list = []
|
||||
for msg in self.messages:
|
||||
# 获取当前消息内容:优先从swipes数组中获取,如果不存在则使用mes
|
||||
current_mes = msg.mes
|
||||
if msg.swipes and 0 <= msg.swipe_id < len(msg.swipes):
|
||||
current_mes = msg.swipes[msg.swipe_id]
|
||||
|
||||
msg_dict = {
|
||||
"name": msg.name,
|
||||
"is_user": msg.is_user,
|
||||
"floor": msg.floor,
|
||||
"mes": current_mes,
|
||||
"swipes": msg.swipes,
|
||||
"swipe_id": msg.swipe_id
|
||||
}
|
||||
messages_list.append(msg_dict)
|
||||
|
||||
# 按floor排序
|
||||
messages_list.sort(key=lambda x: x["floor"])
|
||||
|
||||
return messages_list
|
||||
|
||||
def save_to_file(self, role_name: str, chat_name: str, base_path: Path = None) -> None:
|
||||
"""
|
||||
将聊天历史保存到JSONL文件
|
||||
|
||||
参数:
|
||||
role_name: 角色名称(文件夹名)
|
||||
chat_name: 聊天名称(文件名,不含扩展名)
|
||||
base_path: 基础路径,默认为data/chat
|
||||
"""
|
||||
# 设置默认基础路径
|
||||
if base_path is None:
|
||||
base_path = self.get_data_path()
|
||||
|
||||
# 构建文件路径
|
||||
file_path = base_path / role_name / f"{chat_name}.jsonl"
|
||||
|
||||
# 确保目录存在
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 写入文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
# 写入元数据
|
||||
f.write(json.dumps(self.chat_metadata.dict(), ensure_ascii=False) + '\n')
|
||||
|
||||
# 写入消息
|
||||
for message in self.messages:
|
||||
f.write(json.dumps(message.dict(), ensure_ascii=False) + '\n')
|
||||
@@ -17,12 +17,25 @@ for logger_name in ['uvicorn', 'uvicorn.access', 'fastapi']:
|
||||
|
||||
# backend/app/main.py
|
||||
from fastapi import FastAPI
|
||||
from .api.route import router
|
||||
try:
|
||||
from backend.api.route import router
|
||||
except ImportError:
|
||||
from api.route import router
|
||||
app = FastAPI(title="LLM Workflow Engine")
|
||||
|
||||
# 注册路由
|
||||
app.include_router(router, prefix="/api")
|
||||
|
||||
# 添加健康检查端点
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
# 添加根路径
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "LLM Workflow Engine", "status": "running"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from backend.core.models.PromptList import AIDesignSpec
|
||||
from backend.core.models.PromptComponent import PromptComponent
|
||||
from enum import Enum
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SpecialIdentifier(str, Enum):
|
||||
"""
|
||||
特殊组件标识符枚举
|
||||
定义所有提示词组件的类型及其在最终 Prompt 中的默认物理流向
|
||||
顺序大致遵循:系统层 -> 角色层 -> 动态层 -> 历史层 -> 尾部指令
|
||||
"""
|
||||
|
||||
WORLD_INFO_BEFORE = "worldInfoBefore"
|
||||
"""前置世界书:通常用于全局设定(如物理法则),紧接在 Main Prompt 之后,拥有最高优先级"""
|
||||
|
||||
PERSONA_DESCRIPTION = "personaDescription"
|
||||
"""用户设定:告诉 AI {{user}} 是谁,通常放在场景之后,完成“谁在对谁说话”的闭环"""
|
||||
|
||||
ENHANCE_DEFINITIONS = "enhanceDefinitions"
|
||||
"""增强定义:通常是 "If you have more knowledge...",用于补充 AI 的知识库,这里用rag获取"""
|
||||
|
||||
WORLD_INFO_AFTER = "worldInfoAfter"
|
||||
"""后置世界书:通常用于特定场景规则,位于中间层底部,用于覆盖或补充前面的全局设定"""
|
||||
|
||||
CHAT_HISTORY = "chatHistory"
|
||||
"""聊天历史:包含用户与 AI 的过往对话,占据提示词的下半部分"""
|
||||
|
||||
JAILBREAK = "jailbreak"
|
||||
"""后置指令/注释,也即d0层:通常位于聊天记录之后、AI 生成之前,用于最后时刻的强调(如“不要重复”)"""
|
||||
|
||||
class PresetAssemblyNode(BaseModel):
|
||||
"""预设组装节点类,负责根据组装指令动态组装提示词内容"""
|
||||
|
||||
# 输入数据
|
||||
design_spec: AIDesignSpec = Field(
|
||||
...,
|
||||
description="AI设计规范,包含组件库和组装顺序"
|
||||
)
|
||||
target_character_id: int = Field(
|
||||
...,
|
||||
description="目标角色ID,用于选择对应的组装指令"
|
||||
)
|
||||
|
||||
# 内部状态(不参与序列化)
|
||||
_component_map: Dict[str, PromptComponent] = Field(
|
||||
default_factory=dict,
|
||||
description="组件标识符到组件对象的映射"
|
||||
)
|
||||
|
||||
def __init__(self, **data):
|
||||
"""初始化方法,构建组件映射"""
|
||||
super().__init__(**data)
|
||||
# 构建组件映射字典,提高查找效率
|
||||
self._component_map = {
|
||||
comp.identifier: comp
|
||||
for comp in self.design_spec.prompts
|
||||
}
|
||||
|
||||
def _process_special_component(self, component: PromptComponent) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
处理特殊组件(marker为True的组件)
|
||||
|
||||
参数:
|
||||
component: 要处理的组件
|
||||
|
||||
返回:
|
||||
Optional[Dict[str, Any]]: 处理后的消息,如果组件无法处理则返回None
|
||||
"""
|
||||
try:
|
||||
# 尝试将标识符转换为枚举
|
||||
special_id = SpecialIdentifier(component.identifier)
|
||||
|
||||
# 根据不同标识符执行不同处理逻辑
|
||||
if special_id == SpecialIdentifier.CHAT_HISTORY:
|
||||
return self._handle_chat_history(component)
|
||||
elif special_id == SpecialIdentifier.WORLD_INFO_BEFORE:
|
||||
return self._handle_world_info_before(component)
|
||||
elif special_id == SpecialIdentifier.WORLD_INFO_AFTER:
|
||||
return self._handle_world_info_after(component)
|
||||
elif special_id == SpecialIdentifier.CHAR_DESCRIPTION:
|
||||
return self._handle_char_description(component)
|
||||
else:
|
||||
# 未知特殊组件,使用默认处理
|
||||
return self._process_regular_component(component)
|
||||
except ValueError:
|
||||
# 不是特殊标识符,使用默认处理
|
||||
return self._process_regular_component(component)
|
||||
|
||||
def _process_special_component(self, component: PromptComponent) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
处理特殊组件(marker为True的组件)
|
||||
|
||||
参数:
|
||||
component: 要处理的组件
|
||||
|
||||
返回:
|
||||
Optional[Dict[str, Any]]: 处理后的消息,如果组件无法处理则返回None
|
||||
"""
|
||||
try:
|
||||
# 尝试将标识符转换为枚举
|
||||
special_id = SpecialIdentifier(component.identifier)
|
||||
|
||||
# 根据不同标识符执行不同处理逻辑
|
||||
if special_id == SpecialIdentifier.CHAT_HISTORY:
|
||||
return self._handle_chat_history(component)
|
||||
elif special_id == SpecialIdentifier.WORLD_INFO_BEFORE:
|
||||
return self._handle_world_info_before(component)
|
||||
elif special_id == SpecialIdentifier.WORLD_INFO_AFTER:
|
||||
return self._handle_world_info_after(component)
|
||||
elif special_id == SpecialIdentifier.DIALOGUE_EXAMPLES:
|
||||
return self._handle_dialogue_examples(component)
|
||||
elif special_id == SpecialIdentifier.CHAR_DESCRIPTION:
|
||||
return self._handle_char_description(component)
|
||||
elif special_id == SpecialIdentifier.CHAR_PERSONALITY:
|
||||
return self._handle_char_personality(component)
|
||||
elif special_id == SpecialIdentifier.SCENARIO:
|
||||
return self._handle_scenario(component)
|
||||
elif special_id == SpecialIdentifier.PERSONA_DESCRIPTION:
|
||||
return self._handle_persona_description(component)
|
||||
else:
|
||||
# 未知特殊组件,使用默认处理
|
||||
return self._process_regular_component(component)
|
||||
except ValueError:
|
||||
# 不是特殊标识符,使用默认处理
|
||||
return self._process_regular_component(component)
|
||||
|
||||
def _process_regular_component(self, component: PromptComponent) -> Dict[str, Any]:
|
||||
"""
|
||||
处理普通组件(marker为False的组件)
|
||||
|
||||
参数:
|
||||
component: 要处理的组件
|
||||
|
||||
返回:
|
||||
Dict[str, Any]: 处理后的消息
|
||||
"""
|
||||
# 角色映射表
|
||||
role_map = {0: "system", 1: "user", 2: "assistant"}
|
||||
|
||||
# 构建消息
|
||||
message = {
|
||||
"role": role_map.get(component.role, "system"),
|
||||
"content": component.content
|
||||
}
|
||||
|
||||
# 添加系统提示词标记
|
||||
if component.system_prompt:
|
||||
message["system_prompt"] = True
|
||||
|
||||
return message
|
||||
|
||||
# 以下为特殊组件处理方法
|
||||
|
||||
def _handle_chat_history(self, component: PromptComponent) -> Dict[str, Any]:
|
||||
"""
|
||||
处理聊天历史组件
|
||||
|
||||
参数:
|
||||
component: 聊天历史组件
|
||||
|
||||
返回:
|
||||
Dict[str, Any]: 处理后的消息
|
||||
"""
|
||||
# 这里应该从外部获取实际的聊天历史
|
||||
# 示例实现,实际需要根据业务逻辑调整
|
||||
return {
|
||||
"role": "system",
|
||||
"content": "聊天历史内容...",
|
||||
"marker": True,
|
||||
"type": "chat_history"
|
||||
}
|
||||
|
||||
def _handle_world_info_before(self, component: PromptComponent) -> Dict[str, Any]:
|
||||
"""
|
||||
处理前置世界信息组件
|
||||
|
||||
参数:
|
||||
component: 世界信息组件
|
||||
|
||||
返回:
|
||||
Dict[str, Any]: 处理后的消息
|
||||
"""
|
||||
# 这里应该从外部获取实际的世界信息
|
||||
return {
|
||||
"role": "system",
|
||||
"content": "前置世界信息...",
|
||||
"marker": True,
|
||||
"type": "world_info_before"
|
||||
}
|
||||
|
||||
def _handle_world_info_after(self, component: PromptComponent) -> Dict[str, Any]:
|
||||
"""
|
||||
处理后置世界信息组件
|
||||
|
||||
参数:
|
||||
component: 世界信息组件
|
||||
|
||||
返回:
|
||||
Dict[str, Any]: 处理后的消息
|
||||
"""
|
||||
# 这里应该从外部获取实际的世界信息
|
||||
return {
|
||||
"role": "system",
|
||||
"content": "后置世界信息...",
|
||||
"marker": True,
|
||||
"type": "world_info_after"
|
||||
}
|
||||
|
||||
|
||||
def _handle_char_description(self, component: PromptComponent) -> Dict[str, Any]:
|
||||
"""
|
||||
处理角色描述组件
|
||||
|
||||
参数:
|
||||
component: 角色描述组件
|
||||
|
||||
返回:
|
||||
Dict[str, Any]: 处理后的消息
|
||||
"""
|
||||
# 这里应该从外部获取实际的角色描述
|
||||
return {
|
||||
"role": "system",
|
||||
"content": "角色描述内容...",
|
||||
"marker": True,
|
||||
"type": "char_description"
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import re
|
||||
from core.node_base import BaseNode
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class TextSplitterNode(BaseNode):
|
||||
name = "文本分割节点"
|
||||
inputs = {"text": "string"}
|
||||
outputs = {
|
||||
"outline": "list", # 大纲部分列表
|
||||
"requirement": "list", # 要求部分列表
|
||||
"dialogue": "list", # 对话部分列表
|
||||
"weak_guidance": "list" # 弱指引部分列表
|
||||
}
|
||||
|
||||
async def run(self, text: str) -> Dict[str, List[str]]:
|
||||
# 正则匹配三种括号内的内容
|
||||
# 注意:此正则假设括号不嵌套,且没有转义字符
|
||||
pattern = r'\{([^{}]*)\}|\(([^()]*)\)|“([^”]*)”'
|
||||
|
||||
outline = []
|
||||
requirement = []
|
||||
dialogue = []
|
||||
weak_guidance = []
|
||||
|
||||
pos = 0
|
||||
for match in re.finditer(pattern, text):
|
||||
start, end = match.span()
|
||||
# 处理匹配前的普通文本(弱指引)
|
||||
if start > pos:
|
||||
weak_part = text[pos:start].strip()
|
||||
if weak_part:
|
||||
weak_guidance.append(weak_part)
|
||||
|
||||
# 根据捕获组确定类型
|
||||
if match.group(1) is not None: # 大括号
|
||||
outline.append(match.group(1).strip())
|
||||
elif match.group(2) is not None: # 小括号
|
||||
requirement.append(match.group(2).strip())
|
||||
elif match.group(3) is not None: # 中文引号
|
||||
dialogue.append(match.group(3).strip())
|
||||
|
||||
pos = end
|
||||
|
||||
# 处理剩余的普通文本
|
||||
if pos < len(text):
|
||||
weak_part = text[pos:].strip()
|
||||
if weak_part:
|
||||
weak_guidance.append(weak_part)
|
||||
|
||||
return {
|
||||
"outline": outline,
|
||||
"requirement": requirement,
|
||||
"dialogue": dialogue,
|
||||
"weak_guidance": weak_guidance
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
python-multipart==0.0.6
|
||||
python-multipart==0.0.6
|
||||
|
||||
# LangChain for LLM integration (让 pip 自动解析兼容版本)
|
||||
langchain>=0.1.0
|
||||
langchain-openai>=0.0.5
|
||||
langchain-anthropic>=0.1.1
|
||||
openai>=1.12.0
|
||||
anthropic>=0.23.0
|
||||
@@ -1,47 +0,0 @@
|
||||
from ..core import config
|
||||
from typing import Dict, List
|
||||
|
||||
# 使用配置中的 DATA_PATH 并添加 "chat" 子目录
|
||||
ROOT_DIR = config.settings.DATA_PATH / "chat"
|
||||
|
||||
def get_all_role_and_chat() -> Dict[str, List[str]]:
|
||||
"""
|
||||
读取配置目录下的所有子文件夹,并收集每个子文件夹中的 JSONL 文件
|
||||
|
||||
返回:
|
||||
dict: 字典结构,键是文件夹名称,值是该文件夹中的 JSONL 文件列表(仅文件名,无路径和后缀)
|
||||
"""
|
||||
result = {}
|
||||
|
||||
# 确保目标目录存在
|
||||
if not ROOT_DIR.exists():
|
||||
print(f"警告: 目录 {ROOT_DIR} 不存在")
|
||||
return result
|
||||
|
||||
# 打印根目录路径和内容(调试用)
|
||||
print(f"正在扫描目录: {ROOT_DIR}")
|
||||
print(f"根目录内容: {list(ROOT_DIR.iterdir())}")
|
||||
|
||||
# 遍历根目录下的所有条目
|
||||
for entry in ROOT_DIR.iterdir():
|
||||
try:
|
||||
# 只处理文件夹
|
||||
if entry.is_dir():
|
||||
print(f"处理文件夹: {entry.name}") # 调试信息
|
||||
jsonl_files = []
|
||||
|
||||
# 遍历子文件夹中的所有文件
|
||||
for file in entry.iterdir():
|
||||
if file.is_file() and file.suffix == '.jsonl':
|
||||
# 使用 file.stem 获取不带后缀的文件名
|
||||
jsonl_files.append(file.stem)
|
||||
print(f" 找到文件: {file.name}") # 调试信息
|
||||
|
||||
# 如果该文件夹中有 JSONL 文件,则添加到结果中
|
||||
if jsonl_files:
|
||||
result[entry.name] = jsonl_files
|
||||
except Exception as e:
|
||||
print(f"处理文件夹 {entry.name} 时出错: {str(e)}")
|
||||
continue
|
||||
|
||||
return result
|
||||
@@ -1,152 +0,0 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from backend.core import config as cfg
|
||||
from pathlib import Path
|
||||
from ..core.items import ChatRequest
|
||||
|
||||
# 假设 ChatRequest 定义在这里或者从其他地方导入
|
||||
# from backend.app.core.items import ChatRequest
|
||||
|
||||
async def save_input_to_json(chat_request: ChatRequest):
|
||||
"""
|
||||
保存消息到JSONL文件或处理重roll请求
|
||||
|
||||
参数:
|
||||
chat_request: 包含消息详情的请求对象
|
||||
"""
|
||||
# 1. 从对象中提取属性
|
||||
mes = chat_request.mes
|
||||
role_name = chat_request.role_name
|
||||
chat_name = chat_request.chat_name
|
||||
name = chat_request.name
|
||||
is_user = chat_request.is_user
|
||||
floor_number = chat_request.floor_number
|
||||
# stream, img_switch, table_switch 等虽然在这个函数逻辑中没用到,
|
||||
# 但如果 ChatRequest 中有,也可以提取出来备用
|
||||
# stream = chat_request.stream
|
||||
# ...
|
||||
|
||||
config = cfg.settings
|
||||
# 注意:这里要确保 role_name 和 chat_name 不为 None,否则路径拼接会报错
|
||||
# 建议在函数入口处增加校验,或者在 Pydantic 模型中设置为必填项
|
||||
if not role_name or not chat_name:
|
||||
raise ValueError("role_name and chat_name cannot be empty")
|
||||
|
||||
file_path = config.BASE_PATH / "data" / "chat" / role_name / f"{chat_name}.jsonl"
|
||||
|
||||
# 确保目录存在
|
||||
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 读取文件内容
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
except FileNotFoundError:
|
||||
lines = []
|
||||
|
||||
# 判断是否为重roll请求
|
||||
is_regenerate = False
|
||||
target_index = -1
|
||||
|
||||
if lines and floor_number > 0:
|
||||
# 计算当前楼层号
|
||||
current_floor = len(lines)
|
||||
|
||||
# 如果floor_number与当前楼层号相同,则为重roll请求
|
||||
if floor_number == current_floor:
|
||||
# 找到最后一条非用户消息
|
||||
for i in range(len(lines) - 1, -1, -1):
|
||||
try:
|
||||
line_data = json.loads(lines[i])
|
||||
if not line_data.get('is_user', False):
|
||||
is_regenerate = True
|
||||
target_index = i
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# 处理重roll逻辑
|
||||
if is_regenerate:
|
||||
# 解析目标消息
|
||||
try:
|
||||
target_message = json.loads(lines[target_index])
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"无法解析楼层 {floor_number} 的JSON数据")
|
||||
|
||||
# 初始化swipes数组
|
||||
if target_message.get('swipes') is None:
|
||||
target_message['swipes'] = []
|
||||
|
||||
# 将新回复添加到swipes数组
|
||||
target_message['swipes'].append(mes)
|
||||
|
||||
# 更新swipe_id和content
|
||||
target_message['swipes_id'] = len(target_message['swipes']) - 1
|
||||
target_message['content'] = mes
|
||||
|
||||
# 更新文件内容
|
||||
lines[target_index] = json.dumps(target_message, ensure_ascii=False) + '\n'
|
||||
|
||||
# 写回文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(lines)
|
||||
|
||||
return target_message
|
||||
|
||||
# 处理普通消息保存逻辑
|
||||
else:
|
||||
# 获取当前时间
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 构建消息对象
|
||||
message = {
|
||||
"role": role_name,
|
||||
"chat": chat_name,
|
||||
"content": mes,
|
||||
"name": name,
|
||||
"is_user": is_user,
|
||||
"send_date": current_time,
|
||||
"floor_number": len(lines) + 1, # 记录楼层号
|
||||
"swipes": [],
|
||||
"swipes_id": 0
|
||||
}
|
||||
|
||||
# 追加到文件
|
||||
with open(file_path, 'a', encoding='utf-8') as f:
|
||||
f.write(json.dumps(message, ensure_ascii=False) + '\n')
|
||||
|
||||
return message
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 注意:为了在本地运行测试,你需要手动构造一个 ChatRequest 对象
|
||||
# 或者临时修改函数签名以便直接传参测试
|
||||
|
||||
# 示例:假设 ChatRequest 是一个简单的类或 Pydantic 模型
|
||||
class MockChatRequest:
|
||||
def __init__(self, **kwargs):
|
||||
self.mes = kwargs.get('mes')
|
||||
self.role_name = kwargs.get('role_name')
|
||||
self.chat_name = kwargs.get('chat_name')
|
||||
self.name = kwargs.get('name')
|
||||
self.is_user = kwargs.get('is_user')
|
||||
self.floor_number = kwargs.get('floor_number')
|
||||
|
||||
|
||||
# 测试重roll最后一条AI消息
|
||||
import asyncio
|
||||
|
||||
|
||||
async def test():
|
||||
req = MockChatRequest(
|
||||
mes="这是重roll后的新回复2",
|
||||
role_name="testRole1",
|
||||
chat_name="111",
|
||||
name="AI",
|
||||
is_user=False,
|
||||
floor_number=2
|
||||
)
|
||||
await save_input_to_json(req)
|
||||
|
||||
|
||||
asyncio.run(test())
|
||||
@@ -1,201 +0,0 @@
|
||||
# backend/app/workflows/llm_workflow.py
|
||||
from typing import Dict, Any, List, Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class WorkflowStatus(Enum):
|
||||
"""工作流状态枚举"""
|
||||
INITIALIZED = "initialized"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
PAUSED = "paused"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowContext:
|
||||
"""工作流上下文"""
|
||||
data: Dict[str, Any]
|
||||
status: WorkflowStatus = WorkflowStatus.INITIALIZED
|
||||
metadata: Dict[str, Any] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.metadata is None:
|
||||
self.metadata = {}
|
||||
|
||||
|
||||
class WorkflowNode:
|
||||
"""工作流节点声明"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
handler: Callable,
|
||||
enabled: bool = True,
|
||||
config: Dict[str, Any] = None
|
||||
):
|
||||
self.name = name # 节点的唯一标识符,用于区分不同的节点。
|
||||
self.handler = handler # 一个可调用对象(函数或方法),这是节点实际执行的处理逻辑。
|
||||
self.enabled = enabled # 布尔值,控制节点是否启用。默认为 True,如果设置为 False,节点将被跳过。
|
||||
self.config = config or {} # 一个字典,用于存储节点的配置信息。默认为空字典。
|
||||
self.next_nodes: List['WorkflowNode'] = [] # 一个节点列表,用于指定当前节点执行完成后应跳转到的下一个节点。默认为空列表,可能指向多分支。
|
||||
|
||||
def execute(self, context: WorkflowContext) -> WorkflowContext:
|
||||
"""执行节点处理"""
|
||||
if not self.enabled:
|
||||
return context
|
||||
|
||||
try:
|
||||
context = self.handler(context, self.config)
|
||||
return context
|
||||
except Exception as e:
|
||||
context.status = WorkflowStatus.FAILED
|
||||
context.metadata["error"] = str(e)
|
||||
raise
|
||||
|
||||
|
||||
class LLMWorkflow:
|
||||
"""LLM工作流声明"""
|
||||
|
||||
def __init__(self):
|
||||
self.nodes: List[WorkflowNode] = []
|
||||
self._initialize_workflow()
|
||||
|
||||
def _initialize_workflow(self):
|
||||
"""初始化工作流节点(仅声明,不实现)"""
|
||||
# 输入节点
|
||||
input_node = WorkflowNode(
|
||||
name="input",
|
||||
handler=self._input_handler
|
||||
)
|
||||
|
||||
# 输入预处理节点(可开关)
|
||||
preprocessing_node = WorkflowNode(
|
||||
name="preprocessing",
|
||||
handler=self._preprocessing_handler,
|
||||
enabled=False
|
||||
)
|
||||
|
||||
# RAG处理节点
|
||||
rag_node = WorkflowNode(
|
||||
name="rag",
|
||||
handler=self._rag_handler
|
||||
)
|
||||
|
||||
# 提示词组装节点
|
||||
prompt_assembly_node = WorkflowNode(
|
||||
name="prompt_assembly",
|
||||
handler=self._prompt_assembly_handler
|
||||
)
|
||||
|
||||
# LLM请求节点
|
||||
llm_request_node = WorkflowNode(
|
||||
name="llm_request",
|
||||
handler=self._llm_request_handler
|
||||
)
|
||||
|
||||
# 图像生成节点(可开关)
|
||||
image_generation_node = WorkflowNode(
|
||||
name="image_generation",
|
||||
handler=self._image_generation_handler,
|
||||
enabled=False
|
||||
)
|
||||
|
||||
# 动态表格更新节点(可开关)
|
||||
dynamic_table_node = WorkflowNode(
|
||||
name="dynamic_table",
|
||||
handler=self._dynamic_table_handler,
|
||||
enabled=False
|
||||
)
|
||||
|
||||
# 输出过滤节点
|
||||
output_filter_node = WorkflowNode(
|
||||
name="output_filter",
|
||||
handler=self._output_filter_handler
|
||||
)
|
||||
|
||||
# 输出节点
|
||||
output_node = WorkflowNode(
|
||||
name="output",
|
||||
handler=self._output_handler
|
||||
)
|
||||
|
||||
# 设置节点顺序(构建工作流)
|
||||
self.nodes = [
|
||||
input_node,
|
||||
preprocessing_node,
|
||||
rag_node,
|
||||
prompt_assembly_node,
|
||||
llm_request_node,
|
||||
image_generation_node,
|
||||
dynamic_table_node,
|
||||
output_filter_node,
|
||||
output_node
|
||||
]
|
||||
|
||||
def execute(self, context: WorkflowContext) -> WorkflowContext:
|
||||
"""执行工作流"""
|
||||
context.status = WorkflowStatus.RUNNING
|
||||
|
||||
for node in self.nodes:
|
||||
try:
|
||||
context = node.execute(context)
|
||||
|
||||
# 如果工作流失败,停止执行
|
||||
if context.status == WorkflowStatus.FAILED:
|
||||
break
|
||||
except Exception as e:
|
||||
context.status = WorkflowStatus.FAILED
|
||||
context.metadata["error"] = str(e)
|
||||
break
|
||||
|
||||
if context.status != WorkflowStatus.FAILED:
|
||||
context.status = WorkflowStatus.COMPLETED
|
||||
|
||||
return context
|
||||
|
||||
def enable_node(self, node_name: str, enabled: bool = True):
|
||||
"""启用或禁用特定节点"""
|
||||
for node in self.nodes:
|
||||
if node.name == node_name:
|
||||
node.enabled = enabled
|
||||
return True
|
||||
return False
|
||||
|
||||
# 以下是节点处理函数声明(仅声明,不实现)
|
||||
def _input_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""输入节点处理函数"""
|
||||
pass
|
||||
|
||||
def _preprocessing_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""输入预处理节点处理函数"""
|
||||
pass
|
||||
|
||||
def _rag_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""RAG处理节点处理函数"""
|
||||
pass
|
||||
|
||||
def _prompt_assembly_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""提示词组装节点处理函数"""
|
||||
pass
|
||||
|
||||
def _llm_request_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""LLM请求节点处理函数"""
|
||||
pass
|
||||
|
||||
def _image_generation_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""图像生成节点处理函数"""
|
||||
pass
|
||||
|
||||
def _dynamic_table_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""动态表格更新节点处理函数"""
|
||||
pass
|
||||
|
||||
def _output_filter_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""输出过滤节点处理函数"""
|
||||
pass
|
||||
|
||||
def _output_handler(self, context: WorkflowContext, config: Dict[str, Any]) -> WorkflowContext:
|
||||
"""输出节点处理函数"""
|
||||
pass
|
||||
Reference in New Issue
Block a user