feat: add prompt startup actions and shared vision config

This commit is contained in:
Xin Wang
2026-08-01 23:31:14 +08:00
parent b747144ff1
commit 0331f8cd07
22 changed files with 1238 additions and 344 deletions

View File

@@ -5,9 +5,7 @@ from __future__ import annotations
import asyncio
from copy import deepcopy
from dataclasses import replace
from time import monotonic
from typing import Any
from uuid import uuid4
from loguru import logger
from models import AssistantConfig, RuntimeTool
@@ -36,19 +34,18 @@ from services.brains.base import (
BrainSpec,
SessionVariableUpdate,
)
from services.action_runtime import (
ActionInvocationCancelled,
ActionOutcome,
ActionRunner,
ActionStatus,
)
from services.knowledge import search as search_knowledge
from services.runtime_variables import DynamicVariableStore
from services.tool_executor import ToolExecutionError, ToolExecutor
from services.tool_policy import policy_for_tool
from services.workflow.agent import WorkflowAgentStage
from services.workflow.models import (
ActionError,
ActionOutcome,
ActionStatus,
RouteStatus,
WorkflowRuntimeState,
WorkflowStatus,
)
from services.workflow.models import RouteStatus, WorkflowRuntimeState, WorkflowStatus
from services.workflow.output import WorkflowOutput
from services.workflow.routing import WorkflowEdgeEvaluator
from services.workflow_engine import WorkflowEngine
@@ -58,23 +55,6 @@ from services.workflow_router import WorkflowLLMRouter
MAX_AUTOMATIC_HOPS = 50
class _ActionFailure(RuntimeError):
"""Internal adapter from heterogeneous tool failures to ActionOutcome."""
def __init__(
self,
message: str,
*,
code: str,
retryable: bool = False,
result: dict[str, Any] | None = None,
) -> None:
super().__init__(message)
self.code = code
self.retryable = retryable
self.result = result
class ConfiguredFlowManager(FlowManager):
"""Preserve Flow transitions while suppressing late async-tool replies."""
@@ -129,6 +109,7 @@ class WorkflowBrain(BaseBrain):
self._cfg = cfg
self._store = DynamicVariableStore.from_config(cfg or AssistantConfig(type="workflow"))
self._tools = ToolExecutor(self._store)
self._actions = ActionRunner(self._tools)
self._tool_by_id: dict[str, RuntimeTool] = {
tool.id: tool for tool in (cfg.tools if cfg else [])
}
@@ -166,6 +147,10 @@ class WorkflowBrain(BaseBrain):
self._runtime = runtime
self._store = DynamicVariableStore.from_config(cfg)
self._tools = ToolExecutor(self._store, client_tools=runtime.client_tools)
self._actions = ActionRunner(
self._tools,
is_session_ending=lambda: runtime.call_end.ending,
)
self._tool_by_id = {tool.id: tool for tool in cfg.tools}
self._router = WorkflowLLMRouter(cfg)
self._edge_evaluator = WorkflowEdgeEvaluator(
@@ -749,8 +734,7 @@ class WorkflowBrain(BaseBrain):
await self._emit_node_active(node_id)
data = self._engine.data(node_id)
runtime = self._require_runtime()
invocation_id = f"act_{uuid4().hex[:20]}"
started_at = monotonic()
invocation_id = self._actions.new_invocation_id()
block_user_input = data.get("userInputPolicy") == "block"
if block_user_input and runtime.set_input_enabled:
# Blocking only suppresses new audio/text input while the Action
@@ -760,7 +744,6 @@ class WorkflowBrain(BaseBrain):
runtime.set_input_enabled(False)
tool_id = str(data.get("toolId") or "")
tool = self._tool_by_id.get(tool_id)
result: dict[str, Any] | None = None
try:
await self._emit_trace(
"action_started",
@@ -769,82 +752,24 @@ class WorkflowBrain(BaseBrain):
toolId=tool_id,
toolType=tool.type if tool else None,
)
if not tool:
raise _ActionFailure(
f"工具不存在:{tool_id}",
code="tool_not_found",
)
arguments = self._store.render_data(data.get("arguments") or {})
result = await self._tools.execute(
outcome = await self._actions.execute(
tool,
arguments,
data.get("arguments") or {},
result_assignments=self._action_result_assignments(data),
invocation_id=invocation_id,
)
if result.get("status") != "ok":
returned_status = str(result.get("status") or "error")
raise _ActionFailure(
str(result.get("message") or "工具返回执行失败状态"),
code=str(result.get("code") or f"tool_{returned_status}"),
retryable=bool(result.get("retryable", False)),
result=result,
)
updated_variables = list(result.get("updated_variables") or [])
updated_variables = list(outcome.updated_variables)
if updated_variables:
await self._emit_variables(
reason="action",
node_id=node_id,
changed=updated_variables,
)
outcome = ActionOutcome(
invocation_id=invocation_id,
status=ActionStatus.SUCCESS,
duration_ms=self._elapsed_ms(started_at),
result=result,
updated_variables=tuple(str(name) for name in updated_variables),
)
except asyncio.CancelledError:
outcome = ActionOutcome(
invocation_id=invocation_id,
status=ActionStatus.CANCELLED,
duration_ms=self._elapsed_ms(started_at),
result=result,
error=ActionError(
code="action_cancelled",
message="Action 随当前任务取消",
),
)
except ActionInvocationCancelled as exc:
outcome = exc.outcome
self._set_last_action(outcome)
await self._emit_action_outcome(node_id, outcome)
raise
except (_ActionFailure, ToolExecutionError, ValueError) as exc:
cancelled = self._action_was_cancelled(exc, runtime)
outcome = ActionOutcome(
invocation_id=invocation_id,
status=(
ActionStatus.CANCELLED if cancelled else ActionStatus.FAILURE
),
duration_ms=self._elapsed_ms(started_at),
result=(exc.result if isinstance(exc, _ActionFailure) else result),
error=ActionError(
code=(
"session_ended"
if cancelled
else (
exc.code
if isinstance(exc, _ActionFailure)
else (
"invalid_action_configuration"
if isinstance(exc, ValueError)
else "tool_execution_error"
)
)
),
message=str(exc)[:2048],
retryable=(
exc.retryable if isinstance(exc, _ActionFailure) else False
),
),
)
finally:
if block_user_input and runtime.set_input_enabled:
runtime.set_input_enabled(True)
@@ -852,26 +777,6 @@ class WorkflowBrain(BaseBrain):
await self._emit_action_outcome(node_id, outcome)
return outcome
@staticmethod
def _elapsed_ms(started_at: float) -> int:
return max(0, round((monotonic() - started_at) * 1000))
@staticmethod
def _action_was_cancelled(exc: Exception, runtime: BrainRuntime) -> bool:
if getattr(runtime.call_end, "ending", False):
return True
message = str(exc)
return any(
marker in message
for marker in (
"会话已结束",
"会话已取消",
"管线已停止",
"通道已关闭",
"连接已断开",
)
)
def _set_last_action(self, outcome: ActionOutcome) -> None:
legacy_status = {
ActionStatus.SUCCESS: "ok",