Add agent workflow engine foundation and theme-style page switcher.
Introduce WorkflowEngine with state machine, tool registry, and builtin chat template; migrate stream chat to engine callbacks. Move page mode switching to TopBar actions cluster as a ThemeToggle-style dropdown (聊天/工作室/爽文/房间). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,7 @@ try:
|
||||
from backend.utils.llm_client import LLMClient
|
||||
from backend.services.task_queue_manager import task_queue_manager, TaskType
|
||||
from backend.core.config import settings
|
||||
from backend.services.workflow_engine import workflow_engine
|
||||
except ImportError:
|
||||
from services.character_service import CharacterService
|
||||
from services.worldbook_service import WorldBookService
|
||||
@@ -24,6 +25,7 @@ except ImportError:
|
||||
from utils.llm_client import LLMClient
|
||||
from services.task_queue_manager import task_queue_manager, TaskType
|
||||
from core.config import settings
|
||||
from services.workflow_engine import workflow_engine
|
||||
|
||||
|
||||
class ChatWorkflowService:
|
||||
@@ -128,201 +130,17 @@ class ChatWorkflowService:
|
||||
self,
|
||||
request_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
处理聊天请求的核心工作流
|
||||
|
||||
Args:
|
||||
request_data: 前端发送的完整数据
|
||||
|
||||
Returns:
|
||||
{
|
||||
"success": bool,
|
||||
"content": str, # 生成的回复内容
|
||||
"error": str | None
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# === 第1步:解析请求数据 ===
|
||||
current_role = request_data.get("currentRole")
|
||||
current_chat = request_data.get("currentChat")
|
||||
user_message = request_data.get("mes", "")
|
||||
|
||||
if not current_role or not user_message:
|
||||
return {
|
||||
"success": False,
|
||||
"content": "",
|
||||
"error": "缺少必要的参数:currentRole 或 mes"
|
||||
}
|
||||
|
||||
print(f"[ChatWorkflow] 开始处理请求: role={current_role}, chat={current_chat}")
|
||||
|
||||
# ✅ 获取预设名称(用于加载预设绑定的正则规则)
|
||||
preset_config = request_data.get("presetConfig", {})
|
||||
preset_name = preset_config.get("selectedPreset")
|
||||
|
||||
# ✅ 第1.5步:应用用户输入的正则规则
|
||||
from services.regex_service import regex_service
|
||||
from models.regex_rules import RegexPlacement
|
||||
|
||||
processed_user_message = regex_service.apply_rules_by_placement(
|
||||
text=user_message,
|
||||
placement=RegexPlacement.USER_INPUT.value,
|
||||
character_name=current_role,
|
||||
preset_name=preset_name,
|
||||
message_depth=0,
|
||||
is_for_llm=True, # ✅ 用户输入会发送给LLM
|
||||
is_markdown_rendered=False
|
||||
)
|
||||
|
||||
if processed_user_message != user_message:
|
||||
print(f"[Regex] ✅ 已应用用户输入正则规则")
|
||||
|
||||
user_message = processed_user_message
|
||||
|
||||
# === 第2步:加载角色卡 ===
|
||||
character_data = request_data.get("characterData")
|
||||
if not character_data:
|
||||
# 如果没有提供角色卡数据,从后端加载
|
||||
character = self.character_service.get_character_by_name(current_role)
|
||||
if not character:
|
||||
return {
|
||||
"success": False,
|
||||
"content": "",
|
||||
"error": f"角色 '{current_role}' 不存在"
|
||||
}
|
||||
else:
|
||||
# 使用前端提供的角色卡数据(可能包含用户修改)
|
||||
try:
|
||||
from backend.models.internal import CharacterCard
|
||||
except ImportError:
|
||||
from models.internal import CharacterCard
|
||||
character = CharacterCard(**character_data)
|
||||
|
||||
print(f"[ChatWorkflow] 已加载角色卡: {character.name}")
|
||||
|
||||
# === 第3步:收集并激活世界书条目 ===
|
||||
active_entries = await self._collect_and_activate_worldbooks(
|
||||
request_data,
|
||||
character
|
||||
)
|
||||
print(f"[ChatWorkflow] 激活了 {len(active_entries)} 个世界书条目")
|
||||
|
||||
# === 第4步:加载聊天历史 ===
|
||||
chat_history = await self._load_chat_history(current_role, current_chat)
|
||||
print(f"[ChatWorkflow] 加载了 {len(chat_history)} 条历史消息")
|
||||
|
||||
# === 第5步:组装提示词 ===
|
||||
prompt_messages = self._assemble_prompt(
|
||||
character,
|
||||
chat_history,
|
||||
user_message,
|
||||
active_entries,
|
||||
request_data
|
||||
)
|
||||
print(f"[ChatWorkflow] 组装了 {len(prompt_messages)} 条提示消息")
|
||||
|
||||
# === 第6步:调用LLM生成回复 ===
|
||||
api_config = request_data.get("apiConfig", {})
|
||||
preset_config = request_data.get("presetConfig", {})
|
||||
stream_output = request_data.get("stream", False)
|
||||
|
||||
result = await self._generate_response(
|
||||
prompt_messages,
|
||||
api_config,
|
||||
preset_config,
|
||||
stream_output
|
||||
)
|
||||
|
||||
generated_content = result["content"]
|
||||
token_usage = result.get("usage", {})
|
||||
duration = result.get("duration")
|
||||
|
||||
print(f"[ChatWorkflow] 生成完成,内容长度: {len(generated_content)}")
|
||||
print(f"[ChatWorkflow] Token 使用: {token_usage}")
|
||||
|
||||
# ✅ 第6.5步:应用 AI 输出的正则规则
|
||||
processed_ai_output = regex_service.apply_rules_by_placement(
|
||||
text=generated_content,
|
||||
placement=RegexPlacement.AI_OUTPUT.value,
|
||||
character_name=current_role,
|
||||
preset_name=preset_name,
|
||||
message_depth=0,
|
||||
is_for_llm=False, # ✅ AI输出是显示给用户的
|
||||
is_markdown_rendered=False
|
||||
)
|
||||
|
||||
if processed_ai_output != generated_content:
|
||||
print(f"[Regex] ✅ 已应用 AI 输出正则规则")
|
||||
generated_content = processed_ai_output
|
||||
|
||||
# === 第7步:记录 Token 使用 ===
|
||||
chat_id = f"{current_role}/{request_data.get('currentChat', '')}"
|
||||
floor = request_data.get("floor", 0)
|
||||
|
||||
try:
|
||||
try:
|
||||
from backend.services.token_usage_service import token_usage_service
|
||||
from backend.models.internal import TokenUsageStatus
|
||||
except ImportError:
|
||||
from services.token_usage_service import token_usage_service
|
||||
from models.internal import TokenUsageStatus
|
||||
|
||||
await token_usage_service.record_usage(
|
||||
chat_id=chat_id,
|
||||
role_name=current_role,
|
||||
chat_name=request_data.get('currentChat', ''),
|
||||
prompt_tokens=token_usage.get("prompt_tokens", 0),
|
||||
completion_tokens=token_usage.get("completion_tokens", 0),
|
||||
total_tokens=token_usage.get("total_tokens", 0),
|
||||
status=TokenUsageStatus.COMPLETED,
|
||||
floor=floor + 1, # AI 回复的楼层
|
||||
duration=duration,
|
||||
model=api_config.get("model"),
|
||||
api_provider="openai", # TODO: 从 API URL 检测提供商
|
||||
api_url=api_config.get("api_url") # ✅ 记录 API URL
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[ChatWorkflow] 记录 Token 使用失败: {e}")
|
||||
|
||||
# === 第8步:启动异步并行任务 ===
|
||||
|
||||
# 创建任务ID
|
||||
image_task_id = None
|
||||
table_task_id = None
|
||||
|
||||
options = request_data.get("options", {})
|
||||
if options.get("imageWorkflow", False):
|
||||
import uuid
|
||||
image_task_id = f"img_{uuid.uuid4().hex[:8]}"
|
||||
await task_queue_manager.add_task(image_task_id, TaskType.IMAGE_WORKFLOW, chat_id)
|
||||
|
||||
if options.get("dynamicTable", False):
|
||||
import uuid
|
||||
table_task_id = f"tbl_{uuid.uuid4().hex[:8]}"
|
||||
await task_queue_manager.add_task(table_task_id, TaskType.DYNAMIC_TABLE, chat_id)
|
||||
|
||||
await self._start_parallel_tasks(request_data, generated_content, image_task_id, table_task_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"content": generated_content,
|
||||
"error": None,
|
||||
"activeEntries": active_entries,
|
||||
"taskIds": {
|
||||
"imageWorkflow": image_task_id,
|
||||
"dynamicTable": table_task_id
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ChatWorkflow] 错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"success": False,
|
||||
"content": "",
|
||||
"error": f"工作流执行失败: {str(e)}"
|
||||
}
|
||||
"""处理聊天请求 – delegates to WorkflowEngine.run_turn."""
|
||||
result = await workflow_engine.run_turn(request_data, stream=False)
|
||||
return {
|
||||
"success": result.success,
|
||||
"content": result.content,
|
||||
"error": result.error,
|
||||
"activeEntries": result.active_entries,
|
||||
"taskIds": result.task_ids,
|
||||
"engineRunId": result.run_id,
|
||||
"workflowTemplateId": result.workflow_template_id,
|
||||
}
|
||||
|
||||
def _normalize_worldbook_entry(self, entry_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -1211,280 +1029,24 @@ class ChatWorkflowService:
|
||||
async def process_chat_request_stream(
|
||||
self,
|
||||
request_data: Dict[str, Any],
|
||||
on_chunk
|
||||
on_chunk,
|
||||
on_worldbook_active=None,
|
||||
on_tasks_created=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
处理流式聊天请求
|
||||
|
||||
Args:
|
||||
request_data: 前端发送的完整数据
|
||||
on_chunk: 回调函数,每次收到 chunk 时调用
|
||||
|
||||
Returns:
|
||||
{
|
||||
"success": bool,
|
||||
"content": str, # 完整的生成内容
|
||||
"error": str | None,
|
||||
"activeEntries": List,
|
||||
"taskIds": Dict
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# === 第1步:解析请求数据 ===
|
||||
current_role = request_data.get("currentRole")
|
||||
current_chat = request_data.get("currentChat")
|
||||
user_message = request_data.get("mes", "")
|
||||
|
||||
if not current_role or not user_message:
|
||||
return {
|
||||
"success": False,
|
||||
"content": "",
|
||||
"error": "缺少必要的参数:currentRole 或 mes"
|
||||
}
|
||||
|
||||
print(f"\n{'#'*80}")
|
||||
print(f"[ChatWorkflow-Stream] 🚀 开始处理请求")
|
||||
print(f" - Role: {current_role}")
|
||||
print(f" - Chat: {current_chat}")
|
||||
print(f" - Message Length: {len(user_message)}")
|
||||
print(f"{'#'*80}\n")
|
||||
|
||||
# ✅ 获取预设名称(用于加载预设绑定的正则规则)
|
||||
preset_config = request_data.get("presetConfig", {})
|
||||
preset_name = preset_config.get("selectedPreset")
|
||||
|
||||
# ✅ 第1.5步:应用用户输入的正则规则
|
||||
from services.regex_service import regex_service
|
||||
from models.regex_rules import RegexPlacement
|
||||
|
||||
processed_user_message = regex_service.apply_rules_by_placement(
|
||||
text=user_message,
|
||||
placement=RegexPlacement.USER_INPUT.value,
|
||||
character_name=current_role,
|
||||
preset_name=preset_name,
|
||||
message_depth=0,
|
||||
is_for_llm=True, # ✅ 用户输入会发送给LLM
|
||||
is_markdown_rendered=False
|
||||
)
|
||||
|
||||
if processed_user_message != user_message:
|
||||
print(f"[Regex] ✅ 已应用用户输入正则规则")
|
||||
|
||||
user_message = processed_user_message
|
||||
|
||||
# === 第2步:加载角色卡 ===
|
||||
character_data = request_data.get("characterData")
|
||||
if not character_data:
|
||||
character = self.character_service.get_character_by_name(current_role)
|
||||
if not character:
|
||||
return {
|
||||
"success": False,
|
||||
"content": "",
|
||||
"error": f"角色 '{current_role}' 不存在"
|
||||
}
|
||||
else:
|
||||
try:
|
||||
from backend.models.internal import CharacterCard
|
||||
except ImportError:
|
||||
from models.internal import CharacterCard
|
||||
character = CharacterCard(**character_data)
|
||||
|
||||
print(f"[ChatWorkflow-Stream] ✅ 已加载角色卡: {character.name}")
|
||||
|
||||
# === 第3步:收集并激活世界书条目 ===
|
||||
active_entries = await self._collect_and_activate_worldbooks(
|
||||
request_data,
|
||||
character
|
||||
)
|
||||
print(f"\n[ChatWorkflow-Stream] 📚 世界书激活结果: {len(active_entries)} 个条目")
|
||||
for i, entry in enumerate(active_entries, 1):
|
||||
print(f" {i}. {getattr(entry, 'name', 'N/A')} (UID: {getattr(entry, 'uid', 'N/A')})")
|
||||
|
||||
# === 第4步:加载聊天历史 ===
|
||||
chat_history = await self._load_chat_history(current_role, current_chat)
|
||||
print(f"[ChatWorkflow-Stream] 💬 聊天历史加载结果: {len(chat_history)} 条消息")
|
||||
|
||||
# === 第5步:组装提示词 ===
|
||||
prompt_messages = self._assemble_prompt(
|
||||
character,
|
||||
chat_history,
|
||||
user_message,
|
||||
active_entries,
|
||||
request_data
|
||||
)
|
||||
print(f"[ChatWorkflow-Stream] 📝 提示词组装结果: {len(prompt_messages)} 条消息")
|
||||
for i, msg in enumerate(prompt_messages, 1):
|
||||
role = getattr(msg, 'role', 'unknown')
|
||||
content_preview = str(getattr(msg, 'content', ''))[:50]
|
||||
print(f" {i}. [{role}] {content_preview}...")
|
||||
|
||||
# === 第6步:流式调用LLM生成回复 ===
|
||||
api_config = request_data.get("apiConfig", {})
|
||||
preset_config = request_data.get("presetConfig", {})
|
||||
|
||||
print(f"\n[ChatWorkflow-Stream] 🤖 开始流式调用 LLM")
|
||||
print(f" - Model: {api_config.get('model', 'N/A')}")
|
||||
print(f" - API URL: {api_config.get('api_url', 'N/A')[:50]}...")
|
||||
print(f" - API Key: {'已设置' if api_config.get('api_key') else '⚠️ 未设置'}")
|
||||
print(f" - Temperature: {preset_config.get('parameters', {}).get('temperature', 1.0)}")
|
||||
print(f" - Max Tokens: {preset_config.get('parameters', {}).get('max_tokens', 30000)}")
|
||||
print(f" - Request Timeout: {preset_config.get('parameters', {}).get('request_timeout', 60)}s")
|
||||
print(f"{'~'*80}")
|
||||
|
||||
# ✅ 验证 API Key
|
||||
if not api_config.get('api_key'):
|
||||
print(f"[ChatWorkflow-Stream] ❌ 错误: API Key 为空!")
|
||||
print(f" - 请检查配置文件中是否保存了 API Key")
|
||||
print(f" - Profile ID: {request_data.get('currentProfile', {}).get('id', 'N/A')}")
|
||||
raise Exception("API Key 未配置,请先在 API 配置页面保存密钥")
|
||||
|
||||
generated_content = ""
|
||||
token_usage = {}
|
||||
duration = 0
|
||||
chunk_count = 0
|
||||
start_time = time.time()
|
||||
|
||||
print(f"[ChatWorkflow-Stream] 📡 正在调用 llm_client.stream_chat()...")
|
||||
|
||||
try:
|
||||
async for chunk_dict in self.llm_client.stream_chat(
|
||||
messages=prompt_messages,
|
||||
api_url=api_config.get("api_url", ""),
|
||||
api_key=api_config.get("api_key", ""),
|
||||
model=api_config.get("model", ""),
|
||||
temperature=preset_config.get("parameters", {}).get("temperature", 1.0),
|
||||
max_tokens=preset_config.get("parameters", {}).get("max_tokens", 30000),
|
||||
request_timeout=preset_config.get("parameters", {}).get("request_timeout", 60) # ✅ 传递超时时间
|
||||
):
|
||||
# ✅ 处理 LLM 返回的字典格式数据
|
||||
if isinstance(chunk_dict, dict):
|
||||
if chunk_dict.get("type") == "chunk":
|
||||
chunk_content = chunk_dict.get("content", "")
|
||||
elif chunk_dict.get("type") == "usage":
|
||||
# 跳过 usage 信息,不拼接到内容中
|
||||
continue
|
||||
else:
|
||||
# 兼容旧格式或未知类型,尝试直接获取 content
|
||||
chunk_content = chunk_dict.get("content", str(chunk_dict))
|
||||
else:
|
||||
# 如果直接返回字符串(兼容情况)
|
||||
chunk_content = str(chunk_dict)
|
||||
|
||||
# ✅ 拼接纯文本内容
|
||||
generated_content += chunk_content
|
||||
chunk_count += 1
|
||||
|
||||
# 第一个 chunk 到达时记录
|
||||
if chunk_count == 1:
|
||||
first_chunk_time = time.time()
|
||||
print(f"[ChatWorkflow-Stream] ✨ 收到第一个 chunk (耗时: {first_chunk_time - start_time:.2f}s)")
|
||||
|
||||
# 每20个chunk记录一次进度
|
||||
if chunk_count % 20 == 0:
|
||||
elapsed = time.time() - start_time
|
||||
print(f"[ChatWorkflow-Stream] 📊 已接收 {chunk_count} 个 chunks, 当前长度: {len(generated_content)}, 耗时: {elapsed:.2f}s")
|
||||
|
||||
# ✅ 调用回调函数发送纯文本 chunk
|
||||
await on_chunk(chunk_content)
|
||||
|
||||
except Exception as stream_error:
|
||||
elapsed = time.time() - start_time
|
||||
print(f"[ChatWorkflow-Stream] ❌ 流式调用失败 (耗时: {elapsed:.2f}s)")
|
||||
print(f" - 错误类型: {type(stream_error).__name__}")
|
||||
print(f" - 错误信息: {str(stream_error)}")
|
||||
raise
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"{'~'*80}")
|
||||
print(f"[ChatWorkflow-Stream] ✅ LLM 流式调用完成")
|
||||
print(f" - 总 Chunks: {chunk_count}")
|
||||
print(f" - 内容长度: {len(generated_content)}")
|
||||
print(f" - 总耗时: {elapsed:.2f}s")
|
||||
print(f" - 平均速度: {len(generated_content)/elapsed if elapsed > 0 else 0:.0f} chars/s")
|
||||
print(f"{'#'*80}\n")
|
||||
|
||||
# ✅ 第6.5步:应用 AI 输出的正则规则
|
||||
processed_ai_output = regex_service.apply_rules_by_placement(
|
||||
text=generated_content,
|
||||
placement=RegexPlacement.AI_OUTPUT.value,
|
||||
character_name=current_role,
|
||||
preset_name=preset_name,
|
||||
message_depth=0,
|
||||
is_for_llm=False, # ✅ AI输出是显示给用户的
|
||||
is_markdown_rendered=False
|
||||
)
|
||||
|
||||
if processed_ai_output != generated_content:
|
||||
print(f"[Regex] ✅ 已应用 AI 输出正则规则")
|
||||
generated_content = processed_ai_output
|
||||
|
||||
# === 第7步:记录 Token 使用(估算) ===
|
||||
chat_id = f"{current_role}/{request_data.get('currentChat', '')}"
|
||||
floor = request_data.get("floor", 0)
|
||||
|
||||
try:
|
||||
try:
|
||||
from backend.services.token_usage_service import token_usage_service
|
||||
from backend.models.internal import TokenUsageStatus
|
||||
except ImportError:
|
||||
from services.token_usage_service import token_usage_service
|
||||
from models.internal import TokenUsageStatus
|
||||
|
||||
# 估算 token 数量
|
||||
prompt_tokens = len(str(prompt_messages)) // 4
|
||||
completion_tokens = len(generated_content) // 4
|
||||
|
||||
await token_usage_service.record_usage(
|
||||
chat_id=chat_id,
|
||||
role_name=current_role,
|
||||
chat_name=request_data.get('currentChat', ''),
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=prompt_tokens + completion_tokens,
|
||||
status=TokenUsageStatus.COMPLETED,
|
||||
floor=floor + 1,
|
||||
duration=duration,
|
||||
model=api_config.get("model"),
|
||||
api_provider="openai",
|
||||
api_url=api_config.get("api_url") # ✅ 记录 API URL
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[ChatWorkflow-Stream] 记录 Token 使用失败: {e}")
|
||||
|
||||
# === 第8步:启动异步并行任务 ===
|
||||
image_task_id = None
|
||||
table_task_id = None
|
||||
|
||||
options = request_data.get("options", {})
|
||||
if options.get("imageWorkflow", False):
|
||||
import uuid
|
||||
image_task_id = f"img_{uuid.uuid4().hex[:8]}"
|
||||
await task_queue_manager.add_task(image_task_id, TaskType.IMAGE_WORKFLOW, chat_id)
|
||||
|
||||
if options.get("dynamicTable", False):
|
||||
import uuid
|
||||
table_task_id = f"tbl_{uuid.uuid4().hex[:8]}"
|
||||
await task_queue_manager.add_task(table_task_id, TaskType.DYNAMIC_TABLE, chat_id)
|
||||
|
||||
await self._start_parallel_tasks(request_data, generated_content, image_task_id, table_task_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"content": generated_content,
|
||||
"error": None,
|
||||
"activeEntries": active_entries,
|
||||
"taskIds": {
|
||||
"imageWorkflow": image_task_id,
|
||||
"dynamicTable": table_task_id
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ChatWorkflow-Stream] 错误: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"success": False,
|
||||
"content": "",
|
||||
"error": f"工作流执行失败: {str(e)}"
|
||||
}
|
||||
"""处理流式聊天请求 – delegates to WorkflowEngine.run_turn with callbacks."""
|
||||
result = await workflow_engine.run_turn(
|
||||
request_data,
|
||||
stream=True,
|
||||
on_chunk=on_chunk,
|
||||
on_worldbook_active=on_worldbook_active,
|
||||
on_tasks_created=on_tasks_created,
|
||||
)
|
||||
return {
|
||||
"success": result.success,
|
||||
"content": result.content,
|
||||
"error": result.error,
|
||||
"activeEntries": result.active_entries,
|
||||
"taskIds": result.task_ids,
|
||||
"engineRunId": result.run_id,
|
||||
"workflowTemplateId": result.workflow_template_id,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user