初始化仅前端版本

This commit is contained in:
2026-03-16 21:47:06 +08:00
parent 43adf1a7c4
commit 5a78b7b392
25 changed files with 699 additions and 153 deletions

View File

@@ -4,15 +4,21 @@ FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 复制依赖文件并安装
# 复制依赖文件
# 注意:这里的 requirements.txt 在 backend/ 目录下
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 安装依赖
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
# 复制所有代码
COPY app/ ./app/
# 关键修改:把 backend/ 目录下的内容复制到 /app/backend/ 下
# 这样镜像内的结构就是 /app/backend/app/...
COPY . ./backend/
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# 关键修改:路径改为 backend.app.api.route
CMD ["uvicorn", "backend.app.api.route:app", "--host", "0.0.0.0", "--port", "8000"]

18
backend/api/route.py Normal file
View File

@@ -0,0 +1,18 @@
from fastapi import FastAPI
# 假设这些函数已经在其他地方定义
from backend.core.items import ChatRequest
from backend.tools.get_all_role_and_chat import get_all_role_and_chat as get_chat_file
app = FastAPI()
# 1. 将输入内容持久化存储到本地jsonl方便前端读
@app.post("/generate_reply")
async def save_input_to_json(chat_request: ChatRequest):
return 0
# 2. 从本地jsonl中读取历史对话
@app.get("/get_all_role_and_chat")
def get_all_role_and_chat():
# 直接调用导入的函数
result = get_chat_file()
return result

View File

@@ -1,46 +0,0 @@
import base64
from typing import Any, Dict
from IPython.core.magic_arguments import defaults
from .. import nodes
class StartNode():
name = "开始节点"
inputs = {
"user_input": "string", # 用户输入文本
"stream": "boolean", # 是否流式输出
"img_switch": "boolean", # 是否处理图片
"table_switch": "boolean", # 是否处理表格
"role_name": "string", # 角色名称
"chat_name": "string" # 会话名称
}
async def run(self, text: str = None, image: bytes = None, **kwargs) -> Dict[str, Any]:
# 查空:文本不能为空字符串
if not text or text.strip() == "":
raise ValueError("文本输入不能为空")
# 查空:图片数据不能为空
if image is None or len(image) == 0:
raise ValueError("图片输入不能为空")
# 将图片字节转换为 Base64 字符串,便于在节点间传递
image_base64 = base64.b64encode(image).decode('utf-8')
return {
"text": text,
"image": image_base64
}
async def run(is_user,floor_number,mes: str = None, stream: bool = False, img_switch: bool = False,name = "default",
table_switch: bool = False, role_name: str = None, chat_name: str = None,preset: str = None):
# 将输入内容持久化存储到本地json方便前端读
nodes.save_input_to_json(mes=mes, role_name=role_name, chat_name=chat_name, name=name, is_user=is_user, floor_number=floor_number)
# 对上一条输入内容已确定不变的内容调用向量化根据role和chat嵌入到对应本地数据库
embed_input(user_input, role_name, chat_name)
# 根据role和chat去读取绑定的世界书
# 读取预设,进行拼接
# 调用模型,返回结果
# 将结果持久化存储到本地json方便前端读用JSONL
# 如果img_switch是开的那么异步调用生图并存储到目标文件夹里
# 如果table_switch是开的那么异步调用表格生成并存储到目标文件夹里
# 将结果返回给前端

0
backend/core/__init__.py Normal file
View File

47
backend/core/config.py Normal file
View File

@@ -0,0 +1,47 @@
import os
from pathlib import Path
from dotenv import load_dotenv
# 1. 动态计算项目根目录
# 假设 config.py 位于 backend/ 目录下
# __file__ 指向本文件的绝对路径
# .parent 指向 backend/ 目录
# .parent.parent 指向项目根目录 (即包含 backend/ 和 frontend/ 的目录)
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent # 修改这里,添加一个 .parent
# 2. 加载 .env 文件
# 假设 .env 文件位于项目根目录下
load_dotenv(PROJECT_ROOT / ".env")
class Settings:
# --- 主模型配置 ---
MAIN_LLM_API_KEY = os.getenv("MAIN_LLM_API_KEY")
MAIN_LLM_MODEL = os.getenv("MAIN_LLM_MODEL", "gpt-3.5-turbo")
MAIN_LLM_BASE_URL = os.getenv("MAIN_LLM_BASE_URL", "https://api.openai.com/v1")
MAIN_LLM_MAX_TOKENS = int(os.getenv("MAIN_LLM_MAX_TOKENS", "4096"))
MAIN_LLM_STREAM = os.getenv("MAIN_LLM_STREAM", "true").lower() == "true"
# --- 路径配置 (核心修改) ---
# 强制使用计算出的项目根目录,不再依赖 .env 中的 BASE_PATH
BASE_PATH = PROJECT_ROOT
# 数据目录:固定为根目录下的 data 文件夹
# 即使 .env 里写了 DATA_PATH=/data这里也会强制指向项目根目录下的 data
DATA_PATH = BASE_PATH / "data"
# 其他文件路径:基于 DATA_PATH 拼接
STATE_FILE = DATA_PATH / "state.json"
SCHEMA_FILE = DATA_PATH / "schema.json"
PRESETS_FILE = DATA_PATH / "presets.json"
REGEX_FILE = DATA_PATH / "regex_rules.json"
VECTORSTORE_PATH = DATA_PATH / "vectorstore"
settings = Settings()
if __name__ == '__main__':
# 实例化配置对象
settings = Settings()
print(settings.BASE_PATH)

24
backend/core/items.py Normal file
View File

@@ -0,0 +1,24 @@
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

12
backend/main.py Normal file
View File

@@ -0,0 +1,12 @@
# backend/app/main.py
from fastapi import FastAPI
from .api.routes import router
app = FastAPI(title="LLM Workflow Engine")
# 注册路由
app.include_router(router, prefix="/api")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

View File

View File

@@ -0,0 +1,46 @@
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

View File

@@ -1,33 +1,37 @@
import json
from typing import Dict, Any
from datetime import datetime
import config as cfg
from backend.core import config as cfg
from pathlib import Path
def save_input_to_json(
mes: str,
role_name: str,
chat_name: str,
name: str,
is_user: bool,
floor_number: int = 0
) -> Dict[str, Any]:
# 假设 ChatRequest 定义在这里或者从其他地方导入
# from backend.app.core.items import ChatRequest
async def save_input_to_json(chat_request: ChatRequest):
"""
保存消息到JSONL文件或处理重roll请求
参数:
mes: 消息内容
role_name: 角色名称
chat_name: 对话名称
name: 发送者名称
is_user: 是否为用户消息
floor_number: 楼层号(对话中的第几次回复)用于判断是否为重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"
# 确保目录存在
@@ -115,31 +119,34 @@ def save_input_to_json(
if __name__ == '__main__':
# 测试普通消息保存
# save_input_to_json(
# mes="你好",
# role_name="test",
# chat_name="111",
# name="用户",
# is_user=True,
# floor_number=0
# )
#
# save_input_to_json(
# mes="你好我是AI助手",
# role_name="test",
# chat_name="111",
# name="AI",
# is_user=False,
# floor_number=1
# )
# 注意:为了在本地运行测试,你需要手动构造一个 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消息
save_input_to_json(
mes="这是重roll后的新回复2",
role_name="test",
chat_name="111",
name="AI",
is_user=False,
floor_number=2 # 与当前楼层号相同表示重roll
)
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())

View File

View File

@@ -0,0 +1,201 @@
# 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