Files
SillyTavern_replica/backend/services/workflow_engine.py
moranzhi bc130d98f4 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>
2026-05-31 02:30:43 +08:00

171 lines
5.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Workflow engine orchestrates template loading, state machine execution, and run persistence.
"""
from __future__ import annotations
import json
import uuid
from datetime import datetime
from pathlib import Path
from typing import Any, Awaitable, Callable, Dict, List, Optional
try:
from backend.core.config import settings
from backend.models.agent import (
ChatRunBinding,
ChatTurnResult,
RunEvent,
RunStatus,
TurnCallbacks,
TurnContext,
WorkflowRun,
WorkflowTemplate,
WorkflowTemplateKind,
)
from backend.services.state_machine_runner import StateMachineRunner
from backend.services.tool_registry import ToolRegistry, default_tool_registry
from backend.services.tools.chat_tools import register_chat_tools
except ImportError:
from core.config import settings
from models.agent import (
ChatRunBinding,
ChatTurnResult,
RunEvent,
RunStatus,
TurnCallbacks,
TurnContext,
WorkflowRun,
WorkflowTemplate,
WorkflowTemplateKind,
)
from services.state_machine_runner import StateMachineRunner
from services.tool_registry import ToolRegistry, default_tool_registry
from services.tools.chat_tools import register_chat_tools
class WorkflowEngine:
def __init__(self, registry: Optional[ToolRegistry] = None) -> None:
self.registry = registry or default_tool_registry
if not self.registry.list_specs():
register_chat_tools(self.registry)
def _template_dir(self, template_id: str) -> Path:
return settings.AGENT_TEMPLATES_PATH / template_id
def load_template(self, template_id: str = WorkflowTemplateKind.BUILTIN_CHAT.value) -> WorkflowTemplate:
template_path = self._template_dir(template_id) / "template.json"
with open(template_path, "r", encoding="utf-8") as f:
data = json.load(f)
return WorkflowTemplate(**data)
def _run_dir(self, role_name: str, chat_name: str) -> Path:
return settings.AGENT_RUNS_PATH / "chat" / role_name / chat_name
def _persist_run(self, run: WorkflowRun, events: List[RunEvent]) -> None:
run_dir = self._run_dir(run.binding.role_name, run.binding.chat_name)
run_dir.mkdir(parents=True, exist_ok=True)
run_file = run_dir / "run.json"
run.finished_at = datetime.now().isoformat()
with open(run_file, "w", encoding="utf-8") as f:
json.dump(run.model_dump(), f, ensure_ascii=False, indent=2)
events_file = run_dir / "events.jsonl"
with open(events_file, "a", encoding="utf-8") as f:
for event in events:
f.write(json.dumps(event.model_dump(), ensure_ascii=False) + "\n")
async def run_turn(
self,
request_data: Dict[str, Any],
*,
stream: bool = False,
on_chunk: Optional[Callable[[str], Awaitable[None]]] = None,
on_worldbook_active: Optional[Callable[[List[Any]], Awaitable[None]]] = None,
on_tasks_created: Optional[Callable[[Dict[str, Any]], Awaitable[None]]] = None,
template_id: str = WorkflowTemplateKind.BUILTIN_CHAT.value,
) -> ChatTurnResult:
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 ChatTurnResult(
success=False,
error="缺少必要的参数currentRole 或 mes",
workflow_template_id=template_id,
)
preset_config = request_data.get("presetConfig", {})
preset_name = preset_config.get("selectedPreset")
run_id = uuid.uuid4().hex
binding = ChatRunBinding(
role_name=current_role,
chat_name=current_chat or "",
template_id=template_id,
)
run = WorkflowRun(
id=run_id,
template_id=template_id,
binding=binding,
status=RunStatus.PENDING,
)
callbacks = TurnCallbacks(
on_chunk=on_chunk,
on_worldbook_active=on_worldbook_active,
on_tasks_created=on_tasks_created,
)
ctx = TurnContext(
request_data=request_data,
template_id=template_id,
run_id=run_id,
stream=stream,
callbacks=callbacks,
current_role=current_role,
current_chat=current_chat or "",
user_message=user_message,
preset_name=preset_name,
)
template = self.load_template(template_id)
sm_path = self._template_dir(template_id) / template.state_machine_path
runner = StateMachineRunner.from_file(sm_path, self.registry)
try:
events = await runner.run(run, ctx)
self._persist_run(run, events)
active_entries = [
entry.model_dump() if hasattr(entry, "model_dump") else entry
for entry in ctx.active_entries
]
return ChatTurnResult(
success=True,
content=ctx.generated_content,
active_entries=active_entries,
task_ids=ctx.task_ids,
run_id=run_id,
workflow_template_id=template_id,
)
except Exception as exc:
run.status = RunStatus.FAILED
run.error = str(exc)
try:
self._persist_run(run, [])
except Exception:
pass
return ChatTurnResult(
success=False,
error=f"工作流执行失败: {exc}",
run_id=run_id,
workflow_template_id=template_id,
)
# Module-level singleton
workflow_engine = WorkflowEngine()