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

@@ -2,6 +2,7 @@
import uuid
from models import RuntimeTool
from db.models import (
Assistant,
AssistantModelBinding,
@@ -16,6 +17,7 @@ from schemas import AssistantOut, AssistantUpsert
from services.auth import require_admin
from services.masking import mask, resolve_incoming_key
from services.node_specs import graph_references, normalize_graph, validate_graph
from services.tool_policy import policy_for_tool
from services.workflow_engine import WorkflowEngine
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -110,6 +112,57 @@ async def _validate_workflow_references(
raise HTTPException(400, f"Workflow 引用了无效知识库:{knowledge_id}")
async def _validate_startup_actions(
session: AsyncSession,
body: AssistantUpsert,
) -> None:
"""Keep startup deterministic and reject unsupported lifecycle/tool pairs."""
actions = body.startup.actions
if not actions:
return
bound_tool_ids = set(body.tool_ids)
for action in actions:
if action.tool_id not in bound_tool_ids:
raise HTTPException(400, f"启动 Action 必须先绑定工具:{action.tool_id}")
tool = await session.get(Tool, action.tool_id)
if not tool or tool.status != "active":
raise HTTPException(400, f"启动 Action 引用了无效工具:{action.tool_id}")
if action.phase == "preflight" and tool.type not in {"http", "mcp"}:
raise HTTPException(400, "preflight Action 仅支持 HTTP 或 MCP 工具")
if action.phase == "opening" and tool.type not in {"http", "mcp", "client"}:
raise HTTPException(400, "opening Action 不支持该工具类型")
if tool.type != "client":
continue
runtime_tool = RuntimeTool(
id=tool.id,
name=tool.name,
function_name=tool.function_name,
type=tool.type,
definition=tool.definition or {},
)
policy = policy_for_tool(runtime_tool)
if action.required and not policy.wait_for_response:
raise HTTPException(400, "必需的 Client 启动 Action 必须等待客户端响应")
if tool.function_name != "show_message":
continue
if policy.response_wait_mode != "session":
raise HTTPException(400, "show_message 启动 Action 必须使用会话内等待")
message = str(action.arguments.get("message") or "").strip()
buttons = action.arguments.get("actions")
if not message or len(message) > 2000:
raise HTTPException(400, "show_message 重要信息必须为 1-2000 个字符")
if not isinstance(buttons, list) or not buttons:
raise HTTPException(400, "show_message 至少需要一个确认按钮")
first_button = buttons[0] if isinstance(buttons[0], dict) else {}
if not str(first_button.get("id") or "").strip() or not str(
first_button.get("label") or ""
).strip():
raise HTTPException(400, "show_message 确认按钮必须配置 id 和文字")
if action.required and action.arguments.get("dismissible") is not False:
raise HTTPException(400, "必需的 show_message 启动 Action 不允许跳过确认")
async def _validate_vision_model(
session: AsyncSession, body: AssistantUpsert
) -> None:
@@ -228,6 +281,7 @@ async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
greeting=assistant.greeting,
enable_interrupt=assistant.enable_interrupt,
turn_config=assistant.turn_config or {},
startup=assistant.startup or {},
vision_enabled=assistant.vision_enabled,
vision_model_resource_id=assistant.vision_model_resource_id,
model_resource_ids=await _resource_ids(session, assistant.id),
@@ -262,6 +316,7 @@ async def create_assistant(
):
_validate_workflow(body)
await _validate_workflow_references(session, body)
await _validate_startup_actions(session, body)
await _validate_vision_model(session, body)
await _validate_knowledge_base(session, body)
data = body.model_dump()
@@ -302,6 +357,7 @@ async def duplicate_assistant(
greeting=source.greeting,
enable_interrupt=source.enable_interrupt,
turn_config=dict(source.turn_config or {}),
startup=dict(source.startup or {}),
vision_enabled=source.vision_enabled,
vision_model_resource_id=source.vision_model_resource_id,
knowledge_base_id=source.knowledge_base_id,
@@ -335,6 +391,7 @@ async def update_assistant(
raise HTTPException(404, "助手不存在")
_validate_workflow(body)
await _validate_workflow_references(session, body)
await _validate_startup_actions(session, body)
await _validate_vision_model(session, body)
await _validate_knowledge_base(session, body)
data = body.model_dump()