Compare commits
31 Commits
18ee2b9b2c
...
feature/tr
| Author | SHA1 | Date | |
|---|---|---|---|
| a3e3711b2b | |||
| 8b10ef5828 | |||
| dd17206e1f | |||
| 4f9cf4b725 | |||
| e8dedb5ec4 | |||
| 7a62139683 | |||
| 01ca2bd0f9 | |||
| 1fc0c43689 | |||
| 1abfaeda9d | |||
| 0ae53c4b81 | |||
| f90ad8dc13 | |||
| 6375f9759c | |||
| 33188a345e | |||
| 6fa1fd6e7f | |||
| 80237463ef | |||
| f9bc77d392 | |||
| 408e9ce569 | |||
| 60a2049bb7 | |||
| 4d4d7c30ce | |||
| 2b1ec63c00 | |||
| 6c74bef8da | |||
| 73cdf5ac23 | |||
| a371039ee6 | |||
| 91d11abe90 | |||
| 4b85b35cf8 | |||
| c99052529d | |||
| bd1fa14f20 | |||
| 1aa90f5acf | |||
| 04ea889d75 | |||
| 3c4a11eca8 | |||
| 85f2bbe78c |
5
.env
5
.env
@@ -9,3 +9,8 @@ REGEX_FILE=/data/regex_rules.json
|
|||||||
COMFYUI_API_URL=http://comfyui:8188
|
COMFYUI_API_URL=http://comfyui:8188
|
||||||
BACKEND_PORT=8000
|
BACKEND_PORT=8000
|
||||||
FRONTEND_PORT=8501
|
FRONTEND_PORT=8501
|
||||||
|
|
||||||
|
# 先配置 .env 文件
|
||||||
|
MAIN_LLM_API_KEY=sk-Oh4o3fzV6Qe59B6DRwSskE48xe5D6bq1hkgDZqH1mmJOCN8j
|
||||||
|
MAIN_LLM_BASE_URL=https://api.chatfire.cn/v1
|
||||||
|
MAIN_LLM_MODEL=glm4.7
|
||||||
|
|||||||
BIN
.gitignore
vendored
Normal file
BIN
.gitignore
vendored
Normal file
Binary file not shown.
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
2
.idea/llm-workflow-engine.iml
generated
2
.idea/llm-workflow-engine.iml
generated
@@ -3,6 +3,8 @@
|
|||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/backend/api" isTestSource="false" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.9 (pythonProject1)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.9 (pythonProject1)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ FROM python:3.11-slim
|
|||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
# 复制依赖文件
|
# 复制依赖文件
|
||||||
# 注意:这里的 requirements.txt 在 backend/ 目录下
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
|
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
|
||||||
|
|
||||||
# 复制所有代码
|
# 复制所有代码
|
||||||
# 关键修改:把 backend/ 目录下的内容复制到 /app/backend/ 下
|
COPY . .
|
||||||
# 这样镜像内的结构就是 /app/backend/app/...
|
|
||||||
COPY . ./backend/
|
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露端口
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# 启动命令
|
# 启动命令
|
||||||
# 关键修改:路径改为 backend.app.api.route
|
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
CMD ["uvicorn", "backend.app.api.route:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
|
|||||||
BIN
backend/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
backend/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/main.cpython-311.pyc
Normal file
BIN
backend/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/api/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/api/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/api/__pycache__/route.cpython-311.pyc
Normal file
BIN
backend/api/__pycache__/route.cpython-311.pyc
Normal file
Binary file not shown.
@@ -1,18 +1,19 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import APIRouter
|
||||||
# 假设这些函数已经在其他地方定义
|
from .routes import presetsRoute, chatsRoute, worldbooksRoute, apiConfigRoute
|
||||||
from backend.core.items import ChatRequest
|
from utils.file_utils import get_all_roles_and_chats
|
||||||
from backend.tools.get_all_role_and_chat import get_all_role_and_chat as get_chat_file
|
from core.config import settings
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
app = FastAPI()
|
router = APIRouter()
|
||||||
|
|
||||||
# 1. 将输入内容持久化存储到本地jsonl方便前端读
|
# 注册子路由
|
||||||
@app.post("/generate_reply")
|
router.include_router(presetsRoute.router)
|
||||||
async def save_input_to_json(chat_request: ChatRequest):
|
router.include_router(chatsRoute.router)
|
||||||
return 0
|
router.include_router(worldbooksRoute.router)
|
||||||
|
router.include_router(apiConfigRoute.router)
|
||||||
|
|
||||||
# 2. 从本地jsonl中读取历史对话
|
|
||||||
@app.get("/get_all_role_and_chat")
|
# 保留原有的其他路由
|
||||||
def get_all_role_and_chat():
|
@router.get("/tool_bar/get_all_role_and_chat")
|
||||||
# 直接调用导入的函数
|
def get_all_role_and_chat_endpoint():
|
||||||
result = get_chat_file()
|
return get_all_roles_and_chats(Path(settings.DATA_PATH))
|
||||||
return result
|
|
||||||
|
|||||||
56
backend/api/routes/chatsRoute.py
Normal file
56
backend/api/routes/chatsRoute.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
# 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 ChatService.list_all_chats()
|
||||||
|
return {"chats": []}
|
||||||
|
|
||||||
|
@router.get("/{role_name}/{chat_name}")
|
||||||
|
async def get_chat(role_name: str, chat_name: str):
|
||||||
|
"""获取指定聊天的完整内容"""
|
||||||
|
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):
|
||||||
|
"""创建新聊天"""
|
||||||
|
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):
|
||||||
|
"""更新聊天元数据"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.delete("/{role_name}/{chat_name}")
|
||||||
|
async def delete_chat(role_name: str, chat_name: str):
|
||||||
|
"""删除指定聊天"""
|
||||||
|
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):
|
||||||
|
"""获取聊天的所有消息"""
|
||||||
|
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):
|
||||||
|
"""获取指定楼层的消息"""
|
||||||
|
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):
|
||||||
|
"""向聊天添加新消息"""
|
||||||
|
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):
|
||||||
|
"""更新指定楼层的消息"""
|
||||||
|
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):
|
||||||
|
"""删除指定楼层的消息"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
60
backend/api/routes/presetsRoute.py
Normal file
60
backend/api/routes/presetsRoute.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
# 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 PresetService.list_all_presets()
|
||||||
|
return {"presets": []}
|
||||||
|
|
||||||
|
@router.get("/{preset_name}")
|
||||||
|
async def get_preset(preset_name: str):
|
||||||
|
"""获取指定预设的完整内容"""
|
||||||
|
# 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):
|
||||||
|
"""创建新预设"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.put("/{preset_name}")
|
||||||
|
async def update_preset(preset_name: str, update_data: dict):
|
||||||
|
"""更新预设配置"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.delete("/{preset_name}")
|
||||||
|
async def delete_preset(preset_name: str):
|
||||||
|
"""删除指定预设"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.get("/{preset_name}/components")
|
||||||
|
async def list_preset_components(preset_name: str):
|
||||||
|
"""获取预设中的所有组件"""
|
||||||
|
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):
|
||||||
|
"""获取指定组件的详情"""
|
||||||
|
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):
|
||||||
|
"""向预设添加新组件"""
|
||||||
|
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):
|
||||||
|
"""更新指定组件"""
|
||||||
|
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):
|
||||||
|
"""从预设中删除指定组件"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
117
backend/api/routes/worldbooksRoute.py
Normal file
117
backend/api/routes/worldbooksRoute.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
3# 标准库导入
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
# 第三方库导入
|
||||||
|
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
||||||
|
from fastapi.responses import JSONResponse, FileResponse
|
||||||
|
|
||||||
|
# 本地模块导入
|
||||||
|
from models.internal import WorldInfo, WorldInfoEntry
|
||||||
|
from core.config import settings
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 创建路由器
|
||||||
|
router = APIRouter(prefix="/worldbooks", tags=["worldbooks"])
|
||||||
|
|
||||||
|
# 确保世界书目录存在 (由 config.py 中的 settings.ensure_directories() 统一处理,此处保留作为双重保险)
|
||||||
|
os.makedirs(settings.WORLDBOOKS_PATH, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Dict[str, Any]])
|
||||||
|
async def list_worldbooks():
|
||||||
|
"""
|
||||||
|
获取所有世界书的列表
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: 世界书列表
|
||||||
|
"""
|
||||||
|
# TODO: 实现 WorldBookService
|
||||||
|
return []
|
||||||
|
|
||||||
|
@router.get("/{name}", response_model=Dict[str, Any])
|
||||||
|
async def get_worldbook(name: str):
|
||||||
|
"""
|
||||||
|
获取指定名称的世界书
|
||||||
|
"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.post("/", response_model=Dict[str, Any])
|
||||||
|
async def create_worldbook(
|
||||||
|
name: str = Form(...),
|
||||||
|
file: Optional[UploadFile] = File(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
创建新世界书
|
||||||
|
"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.put("/{name}", response_model=Dict[str, Any])
|
||||||
|
async def update_worldbook(
|
||||||
|
name: str,
|
||||||
|
file: Optional[UploadFile] = File(None)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
更新世界书
|
||||||
|
"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.delete("/{name}")
|
||||||
|
async def delete_worldbook(name: str):
|
||||||
|
"""
|
||||||
|
删除世界书
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
获取世界书的所有条目
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
获取世界书的指定条目
|
||||||
|
"""
|
||||||
|
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]):
|
||||||
|
"""
|
||||||
|
在世界书中创建新条目
|
||||||
|
"""
|
||||||
|
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]):
|
||||||
|
"""
|
||||||
|
更新世界书的指定条目
|
||||||
|
"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.delete("/{name}/entries/{uid}")
|
||||||
|
async def delete_worldbook_entry(name: str, uid: int):
|
||||||
|
"""
|
||||||
|
删除世界书的指定条目
|
||||||
|
"""
|
||||||
|
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(...)):
|
||||||
|
"""
|
||||||
|
从文件导入世界书
|
||||||
|
"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
|
|
||||||
|
@router.get("/{name}/export")
|
||||||
|
async def export_worldbook(name: str):
|
||||||
|
"""
|
||||||
|
导出世界书为 SillyTavern 格式
|
||||||
|
"""
|
||||||
|
raise HTTPException(status_code=501, detail="Not Implemented")
|
||||||
BIN
backend/core/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/core/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/core/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
backend/core/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/core/__pycache__/config.cpython-311.pyc
Normal file
BIN
backend/core/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/core/__pycache__/config.cpython-39.pyc
Normal file
BIN
backend/core/__pycache__/config.cpython-39.pyc
Normal file
Binary file not shown.
BIN
backend/core/__pycache__/items.cpython-311.pyc
Normal file
BIN
backend/core/__pycache__/items.cpython-311.pyc
Normal file
Binary file not shown.
@@ -3,11 +3,12 @@ from pathlib import Path
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# 1. 动态计算项目根目录
|
# 1. 动态计算项目根目录
|
||||||
# 假设 config.py 位于 backend/ 目录下
|
# 假设 config.py 位于 backend/core/ 目录下
|
||||||
# __file__ 指向本文件的绝对路径
|
# __file__ 指向本文件的绝对路径
|
||||||
# .parent 指向 backend/ 目录
|
# .parent 指向 backend/core/ 目录
|
||||||
# .parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录)
|
# .parent.parent 指向 backend/ 目录
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent # 修改这里,添加一个 .parent
|
# .parent.parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录)
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
# 2. 加载 .env 文件
|
# 2. 加载 .env 文件
|
||||||
# 假设 .env 文件位于项目根目录下
|
# 假设 .env 文件位于项目根目录下
|
||||||
@@ -28,20 +29,51 @@ class Settings:
|
|||||||
BASE_PATH = PROJECT_ROOT
|
BASE_PATH = PROJECT_ROOT
|
||||||
|
|
||||||
# 数据目录:固定为根目录下的 data 文件夹
|
# 数据目录:固定为根目录下的 data 文件夹
|
||||||
# 即使 .env 里写了 DATA_PATH=/data,这里也会强制指向项目根目录下的 data
|
|
||||||
DATA_PATH = BASE_PATH / "data"
|
DATA_PATH = BASE_PATH / "data"
|
||||||
|
|
||||||
# 其他文件路径:基于 DATA_PATH 拼接
|
# --- 核心数据文件路径 ---
|
||||||
STATE_FILE = DATA_PATH / "state.json"
|
STATE_FILE = DATA_PATH / "state.json"
|
||||||
SCHEMA_FILE = DATA_PATH / "schema.json"
|
SCHEMA_FILE = DATA_PATH / "schema.json"
|
||||||
PRESETS_FILE = DATA_PATH / "presets.json"
|
PRESETS_FILE = DATA_PATH / "presets.json"
|
||||||
REGEX_FILE = DATA_PATH / "regex_rules.json"
|
REGEX_FILE = DATA_PATH / "regex_rules.json"
|
||||||
VECTORSTORE_PATH = DATA_PATH / "vectorstore"
|
VECTORSTORE_PATH = DATA_PATH / "vectorstore"
|
||||||
|
|
||||||
|
# --- 业务数据目录 ---
|
||||||
|
|
||||||
|
# 世界书目录
|
||||||
|
WORLDBOOKS_PATH = DATA_PATH / "worldbooks"
|
||||||
|
|
||||||
|
# 预设目录
|
||||||
|
PRESET_PATH = DATA_PATH / "preset"
|
||||||
|
|
||||||
|
# 聊天记录目录
|
||||||
|
CHAT_PATH = DATA_PATH / "chat"
|
||||||
|
|
||||||
|
# 临时文件目录
|
||||||
|
TEMP_PATH = DATA_PATH / "temp"
|
||||||
|
|
||||||
|
def ensure_directories(self):
|
||||||
|
"""确保所有配置的目录存在,如果不存在则创建"""
|
||||||
|
directories = [
|
||||||
|
self.DATA_PATH,
|
||||||
|
self.WORLDBOOKS_PATH,
|
||||||
|
self.PRESET_PATH,
|
||||||
|
self.CHAT_PATH,
|
||||||
|
self.TEMP_PATH,
|
||||||
|
]
|
||||||
|
for directory in directories:
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
# 初始化时自动创建必要的目录
|
||||||
# 实例化配置对象
|
settings.ensure_directories()
|
||||||
settings = Settings()
|
|
||||||
print(settings.BASE_PATH)
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
settings = Settings()
|
||||||
|
print(f"项目根目录: {settings.BASE_PATH}")
|
||||||
|
print(f"数据目录: {settings.DATA_PATH}")
|
||||||
|
print(f"世界书目录: {settings.WORLDBOOKS_PATH}")
|
||||||
|
print(f"预设目录: {settings.PRESETS_PATH}")
|
||||||
|
print(f"聊天目录: {settings.CHAT_PATH}")
|
||||||
|
|||||||
@@ -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
|
|
||||||
BIN
backend/core/models/__pycache__/chat_history.cpython-311.pyc
Normal file
BIN
backend/core/models/__pycache__/chat_history.cpython-311.pyc
Normal file
Binary file not shown.
@@ -1,12 +1,41 @@
|
|||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 确保所有模块的日志都能被捕获
|
||||||
|
for logger_name in ['uvicorn', 'uvicorn.access', 'fastapi']:
|
||||||
|
logging_logger = logging.getLogger(logger_name)
|
||||||
|
logging_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
# backend/app/main.py
|
# backend/app/main.py
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from .api.routes import router
|
try:
|
||||||
|
from backend.api.route import router
|
||||||
|
except ImportError:
|
||||||
|
from api.route import router
|
||||||
app = FastAPI(title="LLM Workflow Engine")
|
app = FastAPI(title="LLM Workflow Engine")
|
||||||
|
|
||||||
# 注册路由
|
# 注册路由
|
||||||
app.include_router(router, prefix="/api")
|
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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -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,12 @@
|
|||||||
fastapi==0.104.1
|
fastapi==0.104.1
|
||||||
uvicorn[standard]==0.24.0
|
uvicorn[standard]==0.24.0
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
|
cryptography>=41.0.0
|
||||||
|
requests>=2.31.0
|
||||||
|
|
||||||
|
# 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
|
||||||
BIN
backend/tools/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/tools/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/tools/__pycache__/get_all_role_and_chat.cpython-311.pyc
Normal file
BIN
backend/tools/__pycache__/get_all_role_and_chat.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/tools/__pycache__/save_input_to_json.cpython-311.pyc
Normal file
BIN
backend/tools/__pycache__/save_input_to_json.cpython-311.pyc
Normal file
Binary file not shown.
@@ -1,46 +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':
|
|
||||||
jsonl_files.append(str(file))
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# 假设 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="test",
|
|
||||||
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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
{"role": "test", "chat": "111", "content": "你好", "name": "用户", "is_user": true, "send_date": "2026-03-12 18:26:50", "floor_number": 1, "swipes": [], "swipes_id": 0}
|
|
||||||
{"role": "test", "chat": "111", "content": "这是重roll后的新回复2", "name": "AI", "is_user": false, "send_date": "2026-03-12 18:26:50", "floor_number": 2, "swipes": ["这是重roll后的新回复", "这是重roll后的新回复2"], "swipes_id": 1}
|
|
||||||
5
data/chat/testRole1/111.jsonl
Normal file
5
data/chat/testRole1/111.jsonl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{"user_name": "User", "character_name": "AI Dungeon Master", "integrity": "uuid-001", "chat_id_hash": "hash-001", "note_prompt": "你是一个经验丰富的D&D地下城主。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
||||||
|
{"name": "User", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "你好,我想开始一个新的冒险。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "AI Dungeon Master", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "欢迎,冒险者。请告诉我你想扮演什么角色?", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["欢迎,冒险者。请告诉我你想扮演什么角色?", "你好,旅行者。在这个奇幻世界中,你是谁?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
|
{"name": "User", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我想成为一名人类战士。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "AI Dungeon Master", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "很好。你站在喧闹的酒馆门口,手里握着一把旧长剑。你打算做什么?", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["很好。你站在喧闹的酒馆门口,手里握着一把旧长剑。你打算做什么?", "明白了。作为一名人类战士,你正身处繁华的市集广场。你的下一步行动是?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
5
data/chat/testRole1/11111.jsonl
Normal file
5
data/chat/testRole1/11111.jsonl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{"user_name": "Commander", "character_name": "XCOM AI", "integrity": "uuid-003", "chat_id_hash": "hash-003", "note_prompt": "你是一名XCOM基地的中央AI,负责协助指挥官管理外星威胁。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
||||||
|
{"name": "Commander", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "报告当前的外星活动情况。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "XCOM AI", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "指挥官,卫星侦测到在南美洲丛林中有高能反应。可能是外星着陆舱。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["指挥官,卫星侦测到在南美洲丛林中有高能反应。可能是外星着陆舱。", "警报。我们在非洲检测到异常信号,疑似外星绑架行动正在进行。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
|
{"name": "Commander", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "派遣布拉德福上尉带领一个小队去调查。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "XCOM AI", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "指令已确认。天火运输机正在起飞。预计到达时间:20分钟。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["指令已确认。天火运输机正在起飞。预计到达时间:20分钟。", "收到。正在部署天火运输机。布拉德福上尉已登机。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
5
data/chat/testRole2/222.jsonl
Normal file
5
data/chat/testRole2/222.jsonl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{"user_name": "Player", "character_name": "Game Master", "integrity": "uuid-002", "chat_id_hash": "hash-002", "note_prompt": "场景:赛博朋克风格的未来城市。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
||||||
|
{"name": "Player", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "我检查我的义体状态。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "Game Master", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "你的视觉义眼显示系统正常,但左臂的伺服电机发出轻微的嗡嗡声,似乎需要维护。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你的视觉义眼显示系统正常,但左臂的伺服电机发出轻微的嗡嗡声,似乎需要维护。", "系统自检完成。你的神经接口连接稳定,但义体排异反应指数略有上升。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
|
{"name": "Player", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我联系我的黑客朋友,问他知不知道哪里有靠谱的义体医生。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "Game Master", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "你的朋友回复说:'去下城区的老维克那里,虽然他的店看起来很破,但他手艺没得说。'", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你的朋友回复说:'去下城区的老维克那里,虽然他的店看起来很破,但他手艺没得说。'", "通讯接通。你的朋友告诉你:'别去连锁店,去太平间后巷找'扳手',他收费公道。'"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
5
data/chat/testRole3/333.jsonl
Normal file
5
data/chat/testRole3/333.jsonl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{"user_name": "Player", "character_name": "Narrator", "integrity": "uuid-004", "chat_id_hash": "hash-004", "note_prompt": "这是一个文字冒险游戏,你需要描述场景并等待玩家输入。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
||||||
|
{"name": "Player", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "开始游戏。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "Narrator", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "你醒来时发现自己躺在一片陌生的森林里,四周弥漫着浓雾。你身边有一个背包和一把生锈的匕首。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你醒来时发现自己躺在一片陌生的森林里,四周弥漫着浓雾。你身边有一个背包和一把生锈的匕首。", "当你睁开眼睛,发现自己身处一艘废弃的飞船中,应急灯闪烁着红光。你手里紧握着一个数据盘。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
|
{"name": "Player", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我打开背包看看里面有什么。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "Narrator", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "背包里有一块干硬的面包,一个水壶(里面还有半壶水),以及一张画着奇怪符号的羊皮纸。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["背包里有一块干硬的面包,一个水壶(里面还有半壶水),以及一张画着奇怪符号的羊皮纸。", "背包里只有一把激光手枪,能量槽仅剩10%。还有一张写着'不要相信AI'的纸条。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
@@ -2,28 +2,54 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build:
|
||||||
command: uvicorn backend.api.route:app --host 0.0.0.0 --port 8000 --reload
|
context: ./backend
|
||||||
ports:
|
dockerfile: Dockerfile
|
||||||
- "3001:8000"
|
container_name: llm-backend
|
||||||
|
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- ./backend:/app
|
||||||
- ./data:/data
|
- ./data:/app/data
|
||||||
- ./outputs:/outputs
|
- ./outputs:/app/outputs
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
|
- PYTHONDONTWRITEBYTECODE=1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- llm-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build:
|
||||||
command: streamlit run app.py --server.port=8501 --server.address=0.0.0.0 --server.fileWatcherType=poll
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: development
|
||||||
|
container_name: llm-frontend
|
||||||
ports:
|
ports:
|
||||||
- "3000:8501"
|
- "23338:5173"
|
||||||
environment:
|
|
||||||
- BACKEND_URL=http://backend:8000
|
|
||||||
- PYTHONUNBUFFERED=1
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app # 确保宿主机路径与容器内路径一致
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
- VITE_API_URL=http://backend:8000
|
||||||
|
- VITE_WS_URL=ws://backend:8000
|
||||||
|
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- llm-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
llm-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node_modules:
|
||||||
|
|||||||
@@ -1,18 +1,62 @@
|
|||||||
# 使用 Python 3.11 基础镜像
|
# 多阶段构建 - 开发环境
|
||||||
FROM python:3.11-slim
|
FROM node:20-alpine AS development
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 复制依赖文件并安装
|
# 设置 npm 镜像源(可选,国内推荐使用,加速依赖下载)
|
||||||
COPY requirements.txt .
|
RUN npm config set registry https://registry.npmmirror.com/
|
||||||
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
|
|
||||||
|
|
||||||
# 复制所有代码
|
# 复制 package.json 和 package-lock.json
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# 复制源代码到容器
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 暴露端口
|
# 暴露 Vite 默认端口 5173
|
||||||
EXPOSE 8501
|
EXPOSE 5173
|
||||||
|
|
||||||
# 启动命令(使用 8501 端口,与 docker-compose 映射保持一致)
|
# 设置环境变量
|
||||||
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
ENV NODE_ENV=development
|
||||||
|
ENV VITE_API_URL=http://backend:8000/api
|
||||||
|
|
||||||
|
# 启动 Vite 开发服务器
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||||
|
|
||||||
|
# 多阶段构建 - 生产环境
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置 npm 镜像源
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com/
|
||||||
|
|
||||||
|
# 复制依赖文件
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 生产环境镜像
|
||||||
|
FROM nginx:alpine AS production
|
||||||
|
|
||||||
|
# 复制构建产物到 Nginx
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制 Nginx 配置文件
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# 启动 Nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
BIN
frontend/__pycache__/app.cpython-39.pyc
Normal file
BIN
frontend/__pycache__/app.cpython-39.pyc
Normal file
Binary file not shown.
608
frontend/app.py
608
frontend/app.py
@@ -1,608 +0,0 @@
|
|||||||
import requests
|
|
||||||
import streamlit as st
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
|
|
||||||
# --- 页面配置 ---
|
|
||||||
st.set_page_config(
|
|
||||||
page_title="AI WorkFlow Engine",
|
|
||||||
page_icon="🤖",
|
|
||||||
layout="wide",
|
|
||||||
initial_sidebar_state="expanded"
|
|
||||||
)
|
|
||||||
BACKEND_URL = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
|
|
||||||
print(f"DEBUG BACKEND_URL: {BACKEND_URL}", flush=True) # 看 docker logs
|
|
||||||
|
|
||||||
# --- 自定义 CSS (蓝白清晰风格) ---
|
|
||||||
st.markdown("""
|
|
||||||
<style>
|
|
||||||
/* 全局背景与字体 */
|
|
||||||
.stApp {
|
|
||||||
background-color: #F0F4F8; /* 浅蓝灰背景,护眼且清晰 */
|
|
||||||
color: #1A1A1A; /* 深黑色字体 */
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏默认菜单 */
|
|
||||||
#MainMenu {visibility: hidden;}
|
|
||||||
footer {visibility: hidden;}
|
|
||||||
header {visibility: hidden;} /* 隐藏顶部默认栏,使用自定义工具栏 */
|
|
||||||
|
|
||||||
/* 侧边栏样式 */
|
|
||||||
section[data-testid="stSidebar"] {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border-right: 1px solid #D1D9E6;
|
|
||||||
color: #1A1A1A;
|
|
||||||
}
|
|
||||||
section[data-testid="stSidebar"] .stMarkdown,
|
|
||||||
section[data-testid="stSidebar"] .stNumberInput,
|
|
||||||
section[data-testid="stSidebar"] .stSlider {
|
|
||||||
color: #1A1A1A;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 聊天容器背景 (白色卡片感) */
|
|
||||||
.stChatMessage {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #E1E8F0;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 用户消息特殊样式 */
|
|
||||||
.stChatMessage[data-testid="stChatMessage"]:has(.stMarkdown p) {
|
|
||||||
/* 这里很难直接针对 user/assistant 做不同背景,通过 JS 或特定类名较难,
|
|
||||||
Streamlit 原生 chat_message 会自动处理头像,我们主要靠边框和布局区分 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 输入框样式 */
|
|
||||||
.stTextInput > div > div > input,
|
|
||||||
.stTextArea > div > div > textarea {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
color: #1A1A1A;
|
|
||||||
border: 1px solid #0056B3; /* 蓝色边框 */
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
.stTextInput > div > div > input:focus,
|
|
||||||
.stTextArea > div > div > textarea:focus {
|
|
||||||
border-color: #003D80;
|
|
||||||
box-shadow: 0 0 0 2px rgba(0, 86, 179, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮样式 */
|
|
||||||
.stButton > button {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
color: #0056B3;
|
|
||||||
border: 1px solid #0056B3;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.stButton > button:hover {
|
|
||||||
background-color: #0056B3;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
.stButton > button[kind="primary"] {
|
|
||||||
background-color: #0056B3;
|
|
||||||
color: #FFFFFF;
|
|
||||||
border: 1px solid #0056B3;
|
|
||||||
}
|
|
||||||
.stButton > button[kind="primary"]:hover {
|
|
||||||
background-color: #003D80;
|
|
||||||
border-color: #003D80;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拼接块列表样式优化 */
|
|
||||||
.splice-item {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #D1D9E6;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.splice-name-active { color: #1A1A1A; font-weight: 500; }
|
|
||||||
.splice-name-inactive { color: #8898AA; text-decoration: line-through; }
|
|
||||||
|
|
||||||
/* 顶部工具栏 */
|
|
||||||
.top-bar {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-bottom: 1px solid #D1D9E6;
|
|
||||||
margin: -10px -10px 10px -10px; /* 抵消默认 padding */
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 可折叠工具栏 */
|
|
||||||
.collapsible-toolbar {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border-bottom: 1px solid #D1D9E6;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏工具栏时的样式 */
|
|
||||||
.toolbar-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 工具栏切换按钮 */
|
|
||||||
.toolbar-toggle {
|
|
||||||
position: fixed;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
z-index: 999;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid #D1D9E6;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 三栏布局 - 修改部分 */
|
|
||||||
.main-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: calc(100vh - 60px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.three-column-layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-column, .middle-column, .right-column {
|
|
||||||
padding: 10px;
|
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-column {
|
|
||||||
flex: 1;
|
|
||||||
border-right: 1px solid #D1D9E6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.middle-column {
|
|
||||||
flex: 3;
|
|
||||||
border-right: 1px solid #D1D9E6;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-column {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 中间列的聊天区域 */
|
|
||||||
.chat-area {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 10px;
|
|
||||||
height: calc(100% - 80px); /* 减去输入区域的高度 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 中间列的输入区域 */
|
|
||||||
.input-area {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 10px;
|
|
||||||
border-top: 1px solid #D1D9E6;
|
|
||||||
background-color: #F0F4F8;
|
|
||||||
height: 80px; /* 固定高度 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏Streamlit默认的滚动条样式 */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #888;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 动态调整布局高度
|
|
||||||
function adjustLayout() {
|
|
||||||
// 获取三个列容器
|
|
||||||
const leftColumn = document.querySelector('.left-column');
|
|
||||||
const middleColumn = document.querySelector('.middle-column');
|
|
||||||
const rightColumn = document.querySelector('.right-column');
|
|
||||||
|
|
||||||
// 设置高度为视口高度减去顶部工具栏高度
|
|
||||||
const height = window.innerHeight - 60; // 减去顶部工具栏的高度
|
|
||||||
|
|
||||||
if (leftColumn) leftColumn.style.height = `${height}px`;
|
|
||||||
if (middleColumn) middleColumn.style.height = `${height}px`;
|
|
||||||
if (rightColumn) rightColumn.style.height = `${height}px`;
|
|
||||||
|
|
||||||
// 调整聊天区域高度
|
|
||||||
const chatArea = document.querySelector('.chat-area');
|
|
||||||
if (chatArea) {
|
|
||||||
const inputArea = document.querySelector('.input-area');
|
|
||||||
const inputHeight = inputArea ? inputArea.offsetHeight : 80;
|
|
||||||
chatArea.style.height = `${height - inputHeight}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载时调整布局
|
|
||||||
window.addEventListener('load', adjustLayout);
|
|
||||||
|
|
||||||
// 窗口大小改变时重新调整布局
|
|
||||||
window.addEventListener('resize', adjustLayout);
|
|
||||||
|
|
||||||
// 每次Streamlit重新渲染后调整布局
|
|
||||||
document.addEventListener('newElementRendered', adjustLayout);
|
|
||||||
</script>
|
|
||||||
""", unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# --- 状态初始化 ---
|
|
||||||
if "messages" not in st.session_state:
|
|
||||||
# 初始化一些示例数据,方便查看效果
|
|
||||||
st.session_state.messages = [
|
|
||||||
{"role": "assistant", "content": "你好!我是你的 AI 工作流助手。系统已就绪,请开始对话。"},
|
|
||||||
{"role": "user", "content": "帮我生成一个角色卡,需要包含姓名、年龄和背景故事。"},
|
|
||||||
{"role": "assistant",
|
|
||||||
"content": "好的,这是一个示例角色卡:<br><b>姓名</b>: 艾莉娅<br><b>年龄</b>: 24<br><b>背景</b>: 一位来自北方边境的流浪法师。<br><i>(这是 HTML 渲染测试)</i>"}
|
|
||||||
]
|
|
||||||
if "render_html" not in st.session_state:
|
|
||||||
st.session_state.render_html = True # 默认开启 HTML 渲染以展示效果
|
|
||||||
if "image_folder" not in st.session_state:
|
|
||||||
st.session_state.image_folder = "./assets/images"
|
|
||||||
if "splice_blocks" not in st.session_state:
|
|
||||||
st.session_state.splice_blocks = [
|
|
||||||
{"id": 1, "name": "[必看] 系统指令", "active": True, "type": "system"},
|
|
||||||
{"id": 2, "name": "A.U.T.O. 预设设置", "active": True, "type": "system"},
|
|
||||||
{"id": 3, "name": "世界书:人物关系", "active": False, "type": "world"},
|
|
||||||
{"id": 4, "name": "Chat History (自动)", "active": True, "type": "history", "editable": False},
|
|
||||||
]
|
|
||||||
if "toolbar_visible" not in st.session_state:
|
|
||||||
st.session_state.toolbar_visible = True
|
|
||||||
|
|
||||||
# --- 顶部工具栏 ---
|
|
||||||
# 工具栏切换按钮
|
|
||||||
st.markdown("""
|
|
||||||
<div class="toolbar-toggle" onclick="toggleToolbar()">
|
|
||||||
<span id="toolbar-icon">▼</span>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
function toggleToolbar() {
|
|
||||||
var toolbar = document.querySelector('.collapsible-toolbar');
|
|
||||||
var icon = document.getElementById('toolbar-icon');
|
|
||||||
if (toolbar.style.display === 'none') {
|
|
||||||
toolbar.style.display = 'block';
|
|
||||||
icon.textContent = '▼';
|
|
||||||
} else {
|
|
||||||
toolbar.style.display = 'none';
|
|
||||||
icon.textContent = '▲';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
""", unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# 工具栏内容
|
|
||||||
if st.session_state.toolbar_visible:
|
|
||||||
with st.container():
|
|
||||||
c_top1, c_top2, c_top3 = st.columns([1, 6, 1])
|
|
||||||
with c_top1:
|
|
||||||
if st.button("📂 打开", key="btn_open"):
|
|
||||||
st.toast("打开会话功能预留")
|
|
||||||
if st.button("💾 保存", key="btn_save"):
|
|
||||||
st.toast("会话已保存")
|
|
||||||
with c_top2:
|
|
||||||
st.markdown("<h3 style='margin:0; color:#0056B3;'>AI WorkFlow Engine</h3>", unsafe_allow_html=True)
|
|
||||||
with c_top3:
|
|
||||||
if st.button("⚙️ 设置", key="btn_settings"):
|
|
||||||
st.toast("全局设置预留")
|
|
||||||
|
|
||||||
st.divider()
|
|
||||||
|
|
||||||
# --- 三栏布局 ---
|
|
||||||
col_left, col_mid, col_right = st.columns([1, 3, 1], gap="small")
|
|
||||||
|
|
||||||
# =======================
|
|
||||||
# 1. 左侧:预设与拼接管理 (蓝白风格适配)
|
|
||||||
# =======================
|
|
||||||
with col_left:
|
|
||||||
# 使用自定义容器类
|
|
||||||
st.markdown('<div class="left-column">', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
st.markdown("#### 📜 全局预设")
|
|
||||||
c_pre1, c_pre2 = st.columns([4, 1])
|
|
||||||
with c_pre1:
|
|
||||||
preset_options = ["Default", "A.U.T.O. v1.47", "Roleplay Pro"]
|
|
||||||
st.selectbox("选择预设", preset_options, label_visibility="collapsed")
|
|
||||||
with c_pre2:
|
|
||||||
if st.button("📥", key="btn_import", help="导入预设"):
|
|
||||||
st.toast("导入功能预留")
|
|
||||||
|
|
||||||
st.markdown("#### ⚙️ 生成参数")
|
|
||||||
c_p1, c_p2 = st.columns(2)
|
|
||||||
with c_p1:
|
|
||||||
st.slider("温度", 0.0, 2.0, 1.0, key="slider_temp")
|
|
||||||
st.slider("Top P", 0.0, 1.0, 0.9, key="slider_top_p")
|
|
||||||
with c_p2:
|
|
||||||
st.slider("频率惩罚", 0.0, 2.0, 1.0, key="slider_freq")
|
|
||||||
st.slider("存在惩罚", 0.0, 2.0, 0.0, key="slider_pres")
|
|
||||||
|
|
||||||
c_l1, c_l2 = st.columns(2)
|
|
||||||
with c_l1:
|
|
||||||
st.number_input("上下文长度", value=30000, key="input_ctx")
|
|
||||||
with c_l2:
|
|
||||||
st.number_input("最大回复", value=500, key="input_max")
|
|
||||||
|
|
||||||
st.checkbox("✅ 流式传输", value=True, key="chk_stream")
|
|
||||||
|
|
||||||
st.markdown("#### 🧩 内容拼接块")
|
|
||||||
st.caption("控制发送至后端的上下文组成")
|
|
||||||
|
|
||||||
# 渲染拼接块列表
|
|
||||||
for block in st.session_state.splice_blocks:
|
|
||||||
with st.container():
|
|
||||||
# 自定义行布局模拟列表项
|
|
||||||
cols = st.columns([0.5, 3, 0.5, 0.5])
|
|
||||||
with cols[0]:
|
|
||||||
icon = "🌍" if block['type'] == 'world' else ("💬" if block['type'] == 'history' else "📄")
|
|
||||||
st.write(icon)
|
|
||||||
with cols[1]:
|
|
||||||
name_class = "splice-name-active" if block['active'] else "splice-name-inactive"
|
|
||||||
st.markdown(f"<div class='{name_class}' style='font-size:0.85em;'>{block['name']}</div>",
|
|
||||||
unsafe_allow_html=True)
|
|
||||||
with cols[2]:
|
|
||||||
disabled = not block.get('editable', True)
|
|
||||||
if st.button("✏️", key=f"edit_{block['id']}", disabled=disabled):
|
|
||||||
st.toast(f"编辑:{block['name']}")
|
|
||||||
with cols[3]:
|
|
||||||
is_active = st.checkbox("✓", value=block['active'], key=f"act_{block['id']}",
|
|
||||||
label_visibility="collapsed")
|
|
||||||
if is_active != block['active']:
|
|
||||||
block['active'] = is_active
|
|
||||||
st.rerun()
|
|
||||||
st.markdown("<div style='height:1px; background:#E1E8F0; margin:4px 0;'></div>", unsafe_allow_html=True)
|
|
||||||
|
|
||||||
if st.button("+ 添加拼接块", use_container_width=True):
|
|
||||||
st.toast("添加新功能预留")
|
|
||||||
|
|
||||||
st.markdown('</div>', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# =======================
|
|
||||||
# 2. 中间:流式对话区 (动态读取历史)
|
|
||||||
# =======================
|
|
||||||
with col_mid:
|
|
||||||
# 使用自定义容器类
|
|
||||||
st.markdown('<div class="middle-column">', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# --- 控制区域 ---
|
|
||||||
c_ctrl1, c_ctrl2, c_ctrl3 = st.columns([3, 2, 1])
|
|
||||||
|
|
||||||
with c_ctrl1:
|
|
||||||
# --- 数据集选择下拉框 ---
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{BACKEND_URL}/get_all_role_and_chat")
|
|
||||||
if response.status_code == 200:
|
|
||||||
datasets = response.json()
|
|
||||||
dataset_options = list(datasets.keys())
|
|
||||||
else:
|
|
||||||
st.error(f"获取数据集失败: {response.status_code}")
|
|
||||||
dataset_options = []
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
st.error(f"请求数据集时出错: {e}")
|
|
||||||
dataset_options = []
|
|
||||||
|
|
||||||
selected_dataset = st.selectbox(
|
|
||||||
"选择数据集",
|
|
||||||
dataset_options,
|
|
||||||
index=0 if dataset_options else None,
|
|
||||||
key="dataset_selector"
|
|
||||||
)
|
|
||||||
|
|
||||||
with c_ctrl2:
|
|
||||||
# --- 文件路径选择下拉框 ---
|
|
||||||
# 初始化两层下拉框的数据结构
|
|
||||||
chat_history_options = {}
|
|
||||||
file_options = []
|
|
||||||
|
|
||||||
if selected_dataset:
|
|
||||||
# 使用已经获取的数据集数据
|
|
||||||
chat_history_options = datasets
|
|
||||||
# 获取当前选中数据集对应的文件列表
|
|
||||||
file_options = chat_history_options.get(selected_dataset, [])
|
|
||||||
|
|
||||||
# 第一层下拉框:选择聊天会话(这里应该直接使用selected_dataset)
|
|
||||||
# 不需要再创建一个selectbox,因为已经选择了数据集
|
|
||||||
selected_chat_session = selected_dataset
|
|
||||||
|
|
||||||
# 第二层下拉框:选择文件路径(value列表)
|
|
||||||
if selected_chat_session:
|
|
||||||
file_options = chat_history_options.get(selected_chat_session, [])
|
|
||||||
if file_options:
|
|
||||||
# 提取文件名并去除.jsonl后缀,用于显示
|
|
||||||
display_names = [os.path.basename(f).replace('.jsonl', '') for f in file_options]
|
|
||||||
|
|
||||||
# 创建文件名到完整路径的映射
|
|
||||||
file_name_to_path = {os.path.basename(f).replace('.jsonl', ''): f for f in file_options}
|
|
||||||
|
|
||||||
selected_file_display = st.selectbox(
|
|
||||||
"选择聊天",
|
|
||||||
display_names,
|
|
||||||
index=0 if display_names else None,
|
|
||||||
key="file_selector"
|
|
||||||
)
|
|
||||||
|
|
||||||
if selected_file_display:
|
|
||||||
# 保存完整路径到session_state
|
|
||||||
st.session_state.selected_file_path = file_name_to_path[selected_file_display]
|
|
||||||
else:
|
|
||||||
# 如果没有文件路径,清空选择
|
|
||||||
if "file_selector" in st.session_state:
|
|
||||||
del st.session_state["file_selector"]
|
|
||||||
else:
|
|
||||||
# 如果没有选择会话,清空选择
|
|
||||||
if "file_selector" in st.session_state:
|
|
||||||
del st.session_state["file_selector"]
|
|
||||||
|
|
||||||
with c_ctrl3:
|
|
||||||
# HTML 渲染切换
|
|
||||||
toggle_html = st.toggle("HTML 渲染", value=st.session_state.render_html, key="html_toggle")
|
|
||||||
if toggle_html != st.session_state.render_html:
|
|
||||||
st.session_state.render_html = toggle_html
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
# 显示当前会话信息
|
|
||||||
if selected_dataset and 'selected_file_path' in st.session_state:
|
|
||||||
file_name = os.path.basename(st.session_state.selected_file_path).replace('.jsonl', '')
|
|
||||||
st.caption(f"当前会话:{selected_dataset} - {file_name}")
|
|
||||||
else:
|
|
||||||
st.caption("当前会话:Active_Session_01")
|
|
||||||
|
|
||||||
# --- 核心:动态渲染历史记录 ---
|
|
||||||
# 使用自定义容器类包裹聊天区域
|
|
||||||
st.markdown('<div class="chat-area">', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# 如果选择了聊天记录,则显示该记录
|
|
||||||
if hasattr(st.session_state, 'selected_chat_data') and st.session_state.selected_chat_data:
|
|
||||||
# 显示选中的聊天记录
|
|
||||||
msg = st.session_state.selected_chat_data
|
|
||||||
with st.chat_message(msg["role"]):
|
|
||||||
content = msg["content"]
|
|
||||||
|
|
||||||
# 根据开关决定是否解析 HTML
|
|
||||||
if st.session_state.render_html and msg["role"] == "assistant":
|
|
||||||
st.markdown(content, unsafe_allow_html=True)
|
|
||||||
else:
|
|
||||||
st.markdown(content)
|
|
||||||
|
|
||||||
# 显示其他信息
|
|
||||||
with st.expander("详细信息", expanded=False):
|
|
||||||
st.json(msg)
|
|
||||||
else:
|
|
||||||
# 否则显示session_state中的消息历史
|
|
||||||
for i, msg in enumerate(st.session_state.messages):
|
|
||||||
with st.chat_message(msg["role"]):
|
|
||||||
content = msg["content"]
|
|
||||||
|
|
||||||
if st.session_state.render_html and msg["role"] == "assistant":
|
|
||||||
st.markdown(content, unsafe_allow_html=True)
|
|
||||||
else:
|
|
||||||
st.markdown(content)
|
|
||||||
|
|
||||||
st.markdown('</div>', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# --- 输入区域 ---
|
|
||||||
# 使用自定义容器类包裹输入区域
|
|
||||||
st.markdown('<div class="input-area">', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# 聊天输入框
|
|
||||||
user_input = st.chat_input("输入消息... (支持 /命令)")
|
|
||||||
|
|
||||||
if user_input:
|
|
||||||
# 1. 将用户输入加入历史
|
|
||||||
st.session_state.messages.append({"role": "user", "content": user_input})
|
|
||||||
|
|
||||||
# 2. 触发重新渲染
|
|
||||||
with st.chat_message("assistant"):
|
|
||||||
message_placeholder = st.empty()
|
|
||||||
message_placeholder.markdown("*思考中...*")
|
|
||||||
|
|
||||||
# === 模拟后端流式响应 ===
|
|
||||||
full_response = ""
|
|
||||||
simulated_text = f"收到您的指令:**{user_input}**。\n\n这是一个测试回复,如果您开启了 **HTML 渲染**,下方将显示彩色文本和表格:<br><span style='color:#0056B3; font-weight:bold;'>蓝色高亮文本</span><br><table border='1' style='border-collapse:collapse; width:100%;'><tr><th>属性</th><th>值</th></tr><tr><td>状态</td><td>正常</td></tr></table>"
|
|
||||||
|
|
||||||
chunks = simulated_text.split(" ")
|
|
||||||
for chunk in chunks:
|
|
||||||
full_response += chunk + " "
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
if st.session_state.render_html:
|
|
||||||
message_placeholder.markdown(full_response, unsafe_allow_html=True)
|
|
||||||
else:
|
|
||||||
message_placeholder.markdown(full_response)
|
|
||||||
|
|
||||||
# 3. 将完整的助手回复存入历史
|
|
||||||
st.session_state.messages.append({"role": "assistant", "content": full_response})
|
|
||||||
|
|
||||||
# 强制刷新以确保持久化显示
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
st.markdown('</div>', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
st.markdown('</div>', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
# =======================
|
|
||||||
# 3. 右侧:图片与骰子 (蓝白风格)
|
|
||||||
# =======================
|
|
||||||
with col_right:
|
|
||||||
# 使用自定义容器类
|
|
||||||
st.markdown('<div class="right-column">', unsafe_allow_html=True)
|
|
||||||
|
|
||||||
st.markdown("#### 🖼️ 本地图库")
|
|
||||||
img_path = Path(st.session_state.image_folder)
|
|
||||||
img_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
images = [f for f in os.listdir(img_path) if f.endswith(('.png', '.jpg', '.jpeg', '.webp'))]
|
|
||||||
if images:
|
|
||||||
cols = st.columns(2)
|
|
||||||
for idx, img_name in enumerate(images[:8]):
|
|
||||||
with cols[idx % 2]:
|
|
||||||
# 增加白色背景和边框,使图片在浅灰底上更突出
|
|
||||||
st.markdown(
|
|
||||||
f"<div style='background:white; padding:5px; border-radius:4px; border:1px solid #ddd;'>",
|
|
||||||
unsafe_allow_html=True)
|
|
||||||
st.image(str(img_path / img_name), use_container_width=True)
|
|
||||||
st.caption(img_name)
|
|
||||||
st.markdown("</div>", unsafe_allow_html=True)
|
|
||||||
else:
|
|
||||||
st.info("图片文件夹为空")
|
|
||||||
except Exception as e:
|
|
||||||
st.error(f"读取错误:{e}")
|
|
||||||
|
|
||||||
st.divider()
|
|
||||||
|
|
||||||
st.markdown("#### 🎲 检定工具")
|
|
||||||
tab_table, tab_dice = st.tabs(["📊 表格", "🎲 骰子"])
|
|
||||||
|
|
||||||
with tab_table:
|
|
||||||
st.markdown("**动态数据表**")
|
|
||||||
st.dataframe(
|
|
||||||
{"属性": ["力量", "敏捷", "智力"], "数值": [50, 60, 70]},
|
|
||||||
hide_index=True,
|
|
||||||
use_container_width=True
|
|
||||||
)
|
|
||||||
|
|
||||||
with tab_dice:
|
|
||||||
roll_type = st.radio("类型", ["难度检定", "对抗骰"], horizontal=True)
|
|
||||||
diff_opts = ["极难 (95)", "困难 (75)", "普通 (50)"]
|
|
||||||
selected_diff = st.selectbox("难度", diff_opts)
|
|
||||||
|
|
||||||
c_r1, c_r2 = st.columns(2)
|
|
||||||
with c_r1:
|
|
||||||
if st.button("🎲 投掷", type="primary", use_container_width=True):
|
|
||||||
res = random.randint(1, 100)
|
|
||||||
color = "#d9534f" if res > int(selected_diff.split('(')[1].strip(')')) else "#5cb85c"
|
|
||||||
st.markdown(
|
|
||||||
f"<div style='text-align:center; font-size:1.5em; color:{color}; font-weight:bold;'>{res}</div>",
|
|
||||||
unsafe_allow_html=True)
|
|
||||||
with c_r2:
|
|
||||||
st.caption(f"目标:{selected_diff.split('(')[1].strip(')')}")
|
|
||||||
|
|
||||||
st.markdown('</div>', unsafe_allow_html=True)
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 497 KiB |
@@ -1,53 +0,0 @@
|
|||||||
import streamlit as st
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
def render_chat_window(backend_url):
|
|
||||||
st.subheader("💬 流式对话")
|
|
||||||
|
|
||||||
# 聊天历史显示
|
|
||||||
chat_container = st.container()
|
|
||||||
with chat_container:
|
|
||||||
for message in st.session_state.messages:
|
|
||||||
with st.chat_message(message["role"]):
|
|
||||||
st.markdown(message["content"])
|
|
||||||
# 如果有关联图片,也可以在这里显示
|
|
||||||
if "images" in message:
|
|
||||||
for img_url in message["images"]:
|
|
||||||
st.image(img_url, width=200)
|
|
||||||
|
|
||||||
# 输入框
|
|
||||||
if prompt := st.chat_input("输入消息..."):
|
|
||||||
# 1. 显示用户消息
|
|
||||||
st.session_state.messages.append({"role": "user", "content": prompt})
|
|
||||||
with st.chat_message("user"):
|
|
||||||
st.markdown(prompt)
|
|
||||||
|
|
||||||
# 2. 调用后端流式接口
|
|
||||||
with st.chat_message("assistant"):
|
|
||||||
message_placeholder = st.empty()
|
|
||||||
full_response = ""
|
|
||||||
|
|
||||||
# 模拟流式接收 (实际需使用 requests stream 或 websocket)
|
|
||||||
# POST /api/role/stream
|
|
||||||
try:
|
|
||||||
# 伪代码示例:
|
|
||||||
# with requests.post(f"{backend_url}/api/chat/stream", json={"message": prompt}, stream=True) as r:
|
|
||||||
# for chunk in r.iter_content(chunk_size=None):
|
|
||||||
# if chunk:
|
|
||||||
# full_response += chunk.decode('utf-8')
|
|
||||||
# message_placeholder.markdown(full_response + "▌")
|
|
||||||
|
|
||||||
# 演示用静态延迟
|
|
||||||
import time
|
|
||||||
response_text = "这是一个流式响应的演示。后端正在处理您的请求..."
|
|
||||||
for char in response_text:
|
|
||||||
full_response += char
|
|
||||||
message_placeholder.markdown(full_response + "▌")
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
message_placeholder.markdown(full_response)
|
|
||||||
st.session_state.messages.append({"role": "assistant", "content": full_response})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
st.error(f"连接后端失败: {e}")
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import streamlit as st
|
|
||||||
import random
|
|
||||||
|
|
||||||
|
|
||||||
def render_dice_roller():
|
|
||||||
st.subheader("🎲 命运骰子")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
d20 = st.button("D20", use_container_width=True)
|
|
||||||
if d20:
|
|
||||||
roll = random.randint(1, 20)
|
|
||||||
st.metric("结果", roll, delta=None)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
d6 = st.button("D6", use_container_width=True)
|
|
||||||
if d6:
|
|
||||||
roll = random.randint(1, 6)
|
|
||||||
st.metric("结果", roll, delta=None)
|
|
||||||
|
|
||||||
# 自定义骰子
|
|
||||||
sides = st.number_input("面数", min_value=2, max_value=100, value=10)
|
|
||||||
if st.button(f"投掷 D{sides}", use_container_width=True):
|
|
||||||
roll = random.randint(1, sides)
|
|
||||||
st.success(f"🎲 结果是: **{roll}**")
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import streamlit as st
|
|
||||||
|
|
||||||
|
|
||||||
def render_image_gallery(backend_url):
|
|
||||||
st.subheader("🖼️ 生成画廊")
|
|
||||||
|
|
||||||
# 这里通常轮询后端获取最新生成的图片
|
|
||||||
# GET /api/images/latest
|
|
||||||
|
|
||||||
if not st.session_state.generated_images:
|
|
||||||
st.info("暂无生成图片,对话中触发绘图后将在此显示。")
|
|
||||||
else:
|
|
||||||
cols = st.columns(2)
|
|
||||||
for idx, img_url in enumerate(st.session_state.generated_images[-4:]): # 只显示最近4张
|
|
||||||
with cols[idx % 2]:
|
|
||||||
st.image(img_url, use_container_width=True)
|
|
||||||
st.caption(f"Image {idx + 1}")
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import streamlit as st
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
def render_settings_panel(backend_url):
|
|
||||||
st.subheader("⚙️ 预设设置")
|
|
||||||
|
|
||||||
# 模拟获取预设列表 (实际应调用后端 API)
|
|
||||||
# GET /api/presets
|
|
||||||
try:
|
|
||||||
# response = requests.get(f"{backend_url}/api/presets")
|
|
||||||
# presets = response.json()
|
|
||||||
presets = ["角色扮演-奇幻", "项目管理", "旅行规划", "自定义"] # 占位数据
|
|
||||||
except:
|
|
||||||
presets = ["默认预设"]
|
|
||||||
|
|
||||||
selected_preset = st.selectbox("选择预设模板", presets, index=0)
|
|
||||||
|
|
||||||
st.text_area("系统指令 (System)", height=100, placeholder="在此输入系统级指令...")
|
|
||||||
|
|
||||||
st.checkbox("启用状态记忆", value=True)
|
|
||||||
st.checkbox("启用异步生图", value=True)
|
|
||||||
st.checkbox("启用输入预处理", value=False)
|
|
||||||
|
|
||||||
st.info("💡 修改配置后自动生效,无需重启。")
|
|
||||||
|
|
||||||
# 保存按钮 (调用后端更新配置)
|
|
||||||
if st.button("💾 保存配置", use_container_width=True):
|
|
||||||
st.success("配置已保存!")
|
|
||||||
# requests.post(f"{backend_url}/api/config", json={...})
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import streamlit as st
|
|
||||||
|
|
||||||
|
|
||||||
def render_toolbar(backend_url):
|
|
||||||
col1, col2, col3 = st.columns([1, 2, 1])
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
st.logo("https://streamlit.io/images/brand/streamlit-logo-primary-colormark-darktext.png",
|
|
||||||
size="large") # 可替换为项目Logo
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.title("AI Tavern 工作流引擎")
|
|
||||||
|
|
||||||
with col3:
|
|
||||||
if st.button("🔄 重置会话", use_container_width=True):
|
|
||||||
st.session_state.messages = []
|
|
||||||
st.rerun()
|
|
||||||
# 这里可以添加更多工具栏按钮,如:知识库管理、系统状态等
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
streamlit>=1.30.0
|
|
||||||
requests>=2.31.0
|
|
||||||
websockets>=12.0
|
|
||||||
Pillow>=10.0.0
|
|
||||||
8
main.py
8
main.py
@@ -1,13 +1,11 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from backend.api.route import router
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
# 注册API路由
|
||||||
|
app.include_router(router, prefix="/api")
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {"message": "Hello World"}
|
return {"message": "Hello World"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/hello/{name}")
|
|
||||||
async def say_hello(name: str):
|
|
||||||
return {"message": f"Hello {name}"}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user