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:
2026-05-31 02:30:43 +08:00
parent d6745b45a5
commit bc130d98f4
24 changed files with 1164 additions and 614 deletions

View File

@@ -0,0 +1,170 @@
"""
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()