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>
171 lines
5.8 KiB
Python
171 lines
5.8 KiB
Python
"""
|
||
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()
|