Files
ai-video-fullstack/backend/routes/assistants.py
T

796 lines
31 KiB
Python
Raw 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.
"""Assistant CRUD backed by capability-to-model-resource bindings."""
import uuid
from models import VISION_PROMPT_MODES, RuntimeTool, RuntimeToolBinding
from db.models import (
Assistant,
AssistantModelBinding,
AssistantToolBinding,
KnowledgeBase,
ModelResource,
Tool,
InterfaceDefinition,
)
from db.session import get_session
from fastapi import APIRouter, Depends, HTTPException
from schemas import (
AssistantOut,
AssistantUpsert,
)
from services.auth import require_admin
from services.masking import mask, resolve_incoming_key
from services.node_specs import copy_graph, graph_references, validate_graph
from services.state_expressions import StateExpressionError, validate_update_rules
from services.system_tools import RESERVED_TOOL_NAMES
from services.tool_policy import policy_for_tool
from services.tool_executor import ToolExecutor
from services.workflow_engine import WorkflowEngine
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(
prefix="/api/assistants",
tags=["assistants"],
dependencies=[Depends(require_admin)],
)
CAPABILITIES = ("LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent")
async def _validate_prompt_realtime_tool_capabilities(
session: AsyncSession,
body: AssistantUpsert,
) -> None:
"""Gate Prompt tools by the selected realtime interface definition."""
if body.type != "prompt" or body.runtime_mode != "realtime":
return
resource_id = str(body.model_resource_ids.get("Realtime") or "")
resource = await session.get(ModelResource, resource_id) if resource_id else None
if not resource or not resource.enabled or resource.capability != "Realtime":
raise HTTPException(400, "Prompt Realtime 必须选择可用的 Realtime 模型")
definition = await session.get(InterfaceDefinition, resource.interface_type)
features = (
((definition.field_schema or {}).get("features") or {}).get(
"realtimePromptTools"
)
if definition and definition.enabled
else None
) or {}
external_types = set(features.get("externalTypes") or [])
built_ins = set(features.get("builtIns") or [])
tool_ids = [binding.tool_id for binding in body.tool_bindings]
rows = (
(
await session.execute(select(Tool).where(Tool.id.in_(tool_ids)))
).scalars().all()
if tool_ids
else []
)
unsupported_external = sorted(
{tool.type for tool in rows if tool.type not in external_types}
)
requested_built_ins = {
key
for key, value in body.built_in_tools.model_dump(
by_alias=True,
exclude_none=True,
).items()
if value is not None
}
unsupported_built_ins = sorted(requested_built_ins - built_ins)
if unsupported_external or unsupported_built_ins:
details = []
if unsupported_external:
details.append("外部工具:" + ",".join(unsupported_external))
if unsupported_built_ins:
details.append("内建工具:" + ",".join(unsupported_built_ins))
raise HTTPException(
400,
f"Realtime 接口 {resource.interface_type} 不支持 " + ";".join(details),
)
def _validate_workflow(body: AssistantUpsert) -> None:
"""workflow 类型:保存前校验图结构,不通过则 400。其他类型跳过。"""
if body.type != "workflow":
return
if body.graph and str(body.graph.get("specVersion") or "") != "4":
raise HTTPException(
400,
"Workflow API 仅接受当前 specVersion=4 图结构",
)
body.graph = copy_graph(body.graph or {})
errors = validate_graph(body.graph)
settings = body.graph.get("settings") or {}
graph_runtime_mode = str(settings.get("runtimeMode") or "pipeline")
if graph_runtime_mode != body.runtime_mode:
errors.append("工作流全局运行模式与 Assistant runtimeMode 不一致")
graph_realtime_id = str(settings.get("defaultRealtimeResourceId") or "")
bound_realtime_id = str(body.model_resource_ids.get("Realtime") or "")
if graph_realtime_id != bound_realtime_id:
errors.append("工作流 Realtime 模型与 Assistant Realtime 绑定不一致")
if errors:
raise HTTPException(400, "工作流校验失败:" + ";".join(errors))
# Graph settings are the source of truth. The flat flag is only a session
# capability summary used by clients to decide whether to open the camera.
body.vision_enabled = WorkflowEngine(body.graph).uses_vision()
body.vision_model_resource_id = None
body.vision_prompt_mode = "runtime"
body.vision_prompt = ""
async def _validate_workflow_references(
session: AsyncSession, body: AssistantUpsert
) -> None:
if body.type != "workflow" or not body.graph.get("nodes"):
return
graph = body.graph
engine = WorkflowEngine(graph)
settings = graph.get("settings") or {}
runtime_mode = str(settings.get("runtimeMode") or "pipeline")
resource_expectations: dict[str, str] = {}
vision_resource_ids: set[str] = set()
for key, capability in (
("defaultRealtimeResourceId", "Realtime"),
("defaultLlmResourceId", "LLM"),
("defaultAsrResourceId", "ASR"),
("defaultTtsResourceId", "TTS"),
):
if settings.get(key):
resource_expectations[str(settings[key])] = capability
knowledge_ids: set[str] = (
{str(settings["knowledgeBaseId"])}
if settings.get("knowledgeBaseId")
else set()
)
for node in graph.get("nodes") or []:
data = node.get("data") or {}
if node.get("type") == "agent" and data.get("inheritGlobalConfig", True):
continue
if data.get("llmResourceId"):
resource_expectations[str(data["llmResourceId"])] = "LLM"
if data.get("asrResourceId"):
resource_expectations[str(data["asrResourceId"])] = "ASR"
if data.get("ttsResourceId"):
resource_expectations[str(data["ttsResourceId"])] = "TTS"
if data.get("knowledgeBaseId"):
knowledge_ids.add(str(data["knowledgeBaseId"]))
for node_id, node in engine.nodes.items():
if node.get("type") != "agent":
continue
stage = engine.agent_stage_config(node_id)
if not stage.vision_enabled:
continue
if stage.vision_model_resource_id:
resource_id = stage.vision_model_resource_id
capability = "LLM"
elif runtime_mode == "realtime":
resource_id = str(settings.get("defaultRealtimeResourceId") or "")
capability = "Realtime"
else:
resource_id = stage.llm_resource_id
capability = "LLM"
if not resource_id:
raise HTTPException(
400,
f"Agent 节点 {node_id} 开启视觉理解时必须选择"
"支持图片输入的当前模型或独立视觉模型",
)
resource_expectations[resource_id] = capability
vision_resource_ids.add(resource_id)
for resource_id, capability in resource_expectations.items():
resource = await session.get(ModelResource, resource_id)
if not resource or not resource.enabled or resource.capability != capability:
raise HTTPException(400, f"Workflow 引用了无效的 {capability} 资源:{resource_id}")
if resource_id in vision_resource_ids and not resource.support_image_input:
raise HTTPException(
400,
f"Workflow 视觉模型必须支持图片输入:{resource_id}",
)
for knowledge_id in knowledge_ids:
knowledge = await session.get(KnowledgeBase, knowledge_id)
if not knowledge or knowledge.status != "active":
raise HTTPException(400, f"Workflow 引用了无效知识库:{knowledge_id}")
async def _validate_tool_bindings(
session: AsyncSession,
body: AssistantUpsert,
) -> None:
"""Validate reusable resources and every usage-specific binding."""
raw_bindings: list[tuple[str, dict]] = []
if body.type == "prompt":
raw_bindings.extend(
("Prompt", binding.model_dump()) for binding in body.tool_bindings
)
for action in body.startup.actions:
raw_bindings.append(
(f"Startup {action.id}", action.tool_binding.model_dump())
)
elif body.type == "workflow":
settings = body.graph.get("settings") or {}
raw_bindings.extend(
("Workflow 全局", binding)
for binding in settings.get("toolBindings") or []
)
for node in body.graph.get("nodes") or []:
data = node.get("data") or {}
if node.get("type") == "agent" and not data.get(
"inheritGlobalConfig", True
):
raw_bindings.extend(
(f"Agent {node.get('id')}", binding)
for binding in data.get("toolBindings") or []
)
elif node.get("type") == "action":
raw_bindings.append(
(f"Action {node.get('id')}", data.get("toolBinding") or {})
)
tool_ids = {
str(binding.get("tool_id") or binding.get("toolId") or "")
for _, binding in raw_bindings
if isinstance(binding, dict)
}
tool_ids.discard("")
rows = (
await session.execute(select(Tool).where(Tool.id.in_(tool_ids)))
).scalars().all() if tool_ids else []
tools = {tool.id: tool for tool in rows if tool.status == "active"}
missing = sorted(tool_ids - set(tools))
if missing:
raise HTTPException(400, "工具不存在或未启用: " + ", ".join(missing))
reserved = sorted(
tool.function_name
for tool in tools.values()
if tool.function_name in RESERVED_TOOL_NAMES
)
if reserved:
raise HTTPException(
400,
"外部工具使用了平台保留函数名: " + ", ".join(reserved),
)
declared_variables = set(body.dynamic_variable_definitions)
system_variables = {
"system__agent_id",
"system__conversation_id",
"system__time",
"system__time_utc",
"system__timezone",
"system__agent_turns",
"system__conversation_history",
"system__last_action_status",
"system__last_action_invocation_id",
"system__last_action_duration_ms",
"system__last_action_error",
"system__last_action_error_code",
"system__handoff_status",
}
for scope, raw in raw_bindings:
if not isinstance(raw, dict):
raise HTTPException(400, "工具绑定必须是对象")
binding = RuntimeToolBinding(**raw)
tool = tools.get(binding.tool_id)
if tool is None:
continue
runtime_tool = RuntimeTool(
id=tool.id,
name=tool.name,
function_name=tool.function_name,
type=tool.type,
definition=tool.definition or {},
)
properties, required = ToolExecutor.input_schema(runtime_tool)
unknown = sorted(set(binding.argument_bindings) - set(properties))
if unknown:
raise HTTPException(400, "工具绑定引用了未知参数: " + ", ".join(unknown))
absent = sorted(set(required) - set(binding.argument_bindings))
if absent:
raise HTTPException(400, "工具绑定缺少必填参数: " + ", ".join(absent))
if scope.startswith(("Action ", "Startup ")):
model_parameters = sorted(
name
for name, source in binding.argument_bindings.items()
if str((source or {}).get("source") or "") == "model"
)
if model_parameters:
raise HTTPException(
400,
f"{scope} 是确定性调用,参数不能来自模型: "
+ ", ".join(model_parameters),
)
variable_sources = {
str(source.get("variable_name") or source.get("variableName") or "")
for source in [
*binding.argument_bindings.values(),
*binding.template_bindings.values(),
]
if str((source or {}).get("source") or "") == "variable"
}
invalid_sources = sorted(variable_sources - declared_variables - system_variables)
if invalid_sources:
raise HTTPException(
400,
f"{scope} 引用了未声明动态变量: " + ", ".join(invalid_sources),
)
config = (tool.definition or {}).get("config") or {}
if tool.type == "client" and not config.get(
"wait_for_response", config.get("waitForResponse", True)
) and binding.result_assignments:
raise HTTPException(
400,
f"{scope} 的 fire-and-forget Client 工具不能配置结果映射",
)
template_inputs = config.get("template_inputs") or []
template_names = {
str(item.get("name")) for item in template_inputs if item.get("name")
}
unknown_templates = sorted(set(binding.template_bindings) - template_names)
missing_templates = sorted(
{
str(item.get("name"))
for item in template_inputs
if item.get("name") and item.get("required", True)
}
- set(binding.template_bindings)
)
if unknown_templates:
raise HTTPException(
400,
"工具绑定引用了未知模板输入: " + ", ".join(unknown_templates),
)
if missing_templates:
raise HTTPException(
400,
"工具绑定缺少模板输入: " + ", ".join(missing_templates),
)
undeclared = sorted(
set(binding.result_assignments) - set(body.dynamic_variable_definitions)
)
if undeclared:
raise HTTPException(400, "结果映射目标变量未声明: " + ", ".join(undeclared))
def _validate_update_state_config(body: AssistantUpsert) -> None:
"""Validate Prompt and Workflow built-ins against declared variables."""
configs: list[tuple[str, dict]] = []
if body.type == "prompt":
configs.append(("Prompt", body.built_in_tools.model_dump()))
elif body.type == "workflow":
settings = body.graph.get("settings") or {}
configs.append(("Workflow 全局", settings.get("builtInTools") or {}))
for node in body.graph.get("nodes") or []:
data = node.get("data") or {}
if node.get("type") == "agent" and not data.get(
"inheritGlobalConfig", True
):
configs.append(
(f"Agent {node.get('id')}", data.get("builtInTools") or {})
)
elif node.get("type") == "update_state":
try:
validate_update_rules(
data.get("updates") or [],
body.dynamic_variable_definitions,
allow_llm=False,
)
except StateExpressionError as exc:
raise HTTPException(400, f"Update State 节点配置无效: {exc}") from exc
for scope, config in configs:
update_state = config.get("update_state") or config.get("updateState")
if not update_state:
continue
try:
validate_update_rules(
update_state.get("updates") or [],
body.dynamic_variable_definitions,
allow_llm=True,
)
except StateExpressionError as exc:
raise HTTPException(400, f"{scope} 更新状态配置无效: {exc}") from exc
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
for action in actions:
binding = action.tool_binding
tool = await session.get(Tool, binding.tool_id)
if not tool or tool.status != "active":
raise HTTPException(400, f"启动 Action 引用了无效工具:{binding.tool_id}")
runtime_tool = RuntimeTool(
id=tool.id,
name=tool.name,
function_name=tool.function_name,
type=tool.type,
definition=tool.definition or {},
)
if action.phase == "preflight" and runtime_tool.type not in {"http", "mcp"}:
raise HTTPException(400, "preflight Action 仅支持 HTTP 或 MCP 工具")
if action.phase == "opening" and runtime_tool.type not in {
"http",
"mcp",
"client",
}:
raise HTTPException(400, "opening Action 不支持该工具类型")
if runtime_tool.type != "client":
continue
policy = policy_for_tool(runtime_tool)
if action.required and not policy.wait_for_response:
raise HTTPException(400, "必需的 Client 启动 Action 必须等待客户端响应")
if runtime_tool.function_name != "show_message":
continue
if policy.response_wait_mode != "session":
raise HTTPException(400, "show_message 启动 Action 必须使用会话内等待")
literal_arguments = {
name: source.value
for name, source in binding.argument_bindings.items()
if getattr(source, "source", "") == "literal"
}
message = str(literal_arguments.get("message") or "").strip()
buttons = literal_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 literal_arguments.get("dismissible") is not False:
raise HTTPException(400, "必需的 show_message 启动 Action 不允许跳过确认")
async def _validate_vision_model(
session: AsyncSession, body: AssistantUpsert
) -> None:
# Workflow 的视觉模型属于图中的全局/Agent 配置,由上面的图引用校验处理。
if body.type == "workflow":
return
if body.vision_enabled:
if body.vision_prompt_mode not in VISION_PROMPT_MODES:
raise HTTPException(400, "视觉提示词模式无效")
if (
body.vision_prompt_mode in {"fixed", "guided"}
and not body.vision_prompt.strip()
):
raise HTTPException(400, "固定或引导模式必须填写视觉提示词")
if body.vision_model_resource_id:
resource = await session.get(ModelResource, body.vision_model_resource_id)
else:
resource_id = body.model_resource_ids.get("LLM")
resource = (
await session.get(ModelResource, resource_id) if resource_id else None
)
if not resource or resource.capability != "LLM":
raise HTTPException(400, "视觉模型必须引用 LLM 模型资源")
if not resource.support_image_input:
raise HTTPException(400, "视觉模型必须支持图片输入")
async def _validate_analysis_model(
session: AsyncSession, body: AssistantUpsert
) -> None:
config = body.analysis_config
if not config.enabled:
return
resource = await session.get(ModelResource, config.model_resource_id)
if not resource or not resource.enabled or resource.capability != "LLM":
raise HTTPException(400, "分析模型必须引用已启用的 LLM 模型资源")
async def _validate_knowledge_base(session: AsyncSession, body: AssistantUpsert) -> None:
if body.runtime_mode != "pipeline" or body.type not in {"prompt", "workflow"}:
body.knowledge_base_id = None
return
if body.knowledge_base_id and not await session.get(KnowledgeBase, body.knowledge_base_id):
raise HTTPException(400, "知识库不存在")
async def _sync_bindings(
session: AsyncSession, assistant_id: str, resource_ids: dict[str, str]
) -> None:
for capability in CAPABILITIES:
resource_id = resource_ids.get(capability)
binding = await session.get(AssistantModelBinding, (assistant_id, capability))
if not resource_id:
if binding:
await session.delete(binding)
continue
resource = await session.get(ModelResource, resource_id)
if not resource or resource.capability != capability:
raise HTTPException(400, f"{capability} 绑定必须引用同能力的模型资源")
if binding:
binding.model_resource_id = resource_id
else:
session.add(
AssistantModelBinding(
assistant_id=assistant_id,
capability=capability,
model_resource_id=resource_id,
config={},
)
)
async def _resource_ids(session: AsyncSession, assistant_id: str) -> dict[str, str]:
bindings = (
await session.execute(
select(AssistantModelBinding).where(
AssistantModelBinding.assistant_id == assistant_id
)
)
).scalars().all()
return {binding.capability: binding.model_resource_id for binding in bindings}
async def _sync_tool_bindings(
session: AsyncSession,
assistant_id: str,
assistant_type: str,
tool_bindings,
graph: dict | None = None,
) -> None:
requested_configs = {
binding.tool_id: {
"argument_bindings": {
name: source.model_dump()
for name, source in binding.argument_bindings.items()
},
"template_bindings": {
name: source.model_dump()
for name, source in binding.template_bindings.items()
},
"result_assignments": dict(binding.result_assignments),
}
for binding in tool_bindings
} if assistant_type == "prompt" else {}
if assistant_type == "workflow":
refs = graph_references(graph or {})
requested_configs = {tool_id: {} for tool_id in refs["tools"]}
requested = list(requested_configs)
if requested:
tools = (
await session.execute(select(Tool).where(Tool.id.in_(requested)))
).scalars().all()
found = {tool.id for tool in tools if tool.status == "active"}
missing = [tool_id for tool_id in requested if tool_id not in found]
if missing:
raise HTTPException(400, f"工具不存在或未启用: {', '.join(missing)}")
existing = (
await session.execute(
select(AssistantToolBinding).where(
AssistantToolBinding.assistant_id == assistant_id
)
)
).scalars().all()
existing_by_id = {binding.tool_id: binding for binding in existing}
for tool_id, binding in existing_by_id.items():
if tool_id not in requested:
await session.delete(binding)
for tool_id in requested:
if tool_id in existing_by_id:
existing_by_id[tool_id].config = requested_configs[tool_id]
else:
session.add(
AssistantToolBinding(
assistant_id=assistant_id,
tool_id=tool_id,
config=requested_configs[tool_id],
)
)
async def _tool_bindings(session: AsyncSession, assistant_id: str) -> list[dict]:
rows = (
await session.execute(
select(AssistantToolBinding)
.where(AssistantToolBinding.assistant_id == assistant_id)
.order_by(AssistantToolBinding.created_at)
)
).scalars().all()
return [
{"tool_id": row.tool_id, **(row.config or {})}
for row in rows
if row.config
]
async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
return AssistantOut(
id=assistant.id,
name=assistant.name,
type=assistant.type, # type: ignore[arg-type]
runtime_mode=assistant.runtime_mode, # type: ignore[arg-type]
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,
vision_prompt_mode=assistant.vision_prompt_mode or "runtime",
vision_prompt=assistant.vision_prompt or "",
model_resource_ids=await _resource_ids(session, assistant.id),
knowledge_base_id=assistant.knowledge_base_id,
knowledge_retrieval_config=assistant.knowledge_retrieval_config or {},
tool_bindings=(
await _tool_bindings(session, assistant.id)
if assistant.type == "prompt"
else []
),
built_in_tools=assistant.built_in_tools or {},
prompt=assistant.prompt,
dynamic_variable_definitions=assistant.dynamic_variable_definitions or {},
analysis_config=assistant.analysis_config or {},
api_url=assistant.api_url,
api_key=mask(assistant.api_key),
app_id=assistant.app_id,
graph=(
copy_graph(assistant.graph or {})
if assistant.type == "workflow"
else {}
),
updated_at=assistant.updated_at.isoformat() if assistant.updated_at else None,
)
@router.get("", response_model=list[AssistantOut])
async def list_assistants(session: AsyncSession = Depends(get_session)):
rows = (
await session.execute(select(Assistant).order_by(Assistant.updated_at.desc()))
).scalars().all()
return [await _to_out(session, assistant) for assistant in rows]
@router.post("", response_model=AssistantOut)
async def create_assistant(
body: AssistantUpsert, session: AsyncSession = Depends(get_session)
):
_validate_workflow(body)
await _validate_workflow_references(session, body)
_validate_update_state_config(body)
await _validate_tool_bindings(session, body)
await _validate_prompt_realtime_tool_capabilities(session, body)
await _validate_startup_actions(session, body)
await _validate_vision_model(session, body)
await _validate_analysis_model(session, body)
await _validate_knowledge_base(session, body)
data = body.model_dump()
resource_ids = data.pop("model_resource_ids")
tool_bindings = body.tool_bindings
data.pop("tool_bindings")
data["built_in_tools"] = body.built_in_tools.model_dump(exclude_none=True)
assistant = Assistant(id=f"asst_{uuid.uuid4().hex[:12]}", **data)
session.add(assistant)
await session.flush()
await _sync_bindings(session, assistant.id, resource_ids)
await _sync_tool_bindings(
session,
assistant.id,
assistant.type,
tool_bindings,
body.graph,
)
await session.commit()
await session.refresh(assistant)
return await _to_out(session, assistant)
@router.get("/{assistant_id}", response_model=AssistantOut)
async def get_assistant(
assistant_id: str, session: AsyncSession = Depends(get_session)
):
assistant = await session.get(Assistant, assistant_id)
if not assistant:
raise HTTPException(404, "助手不存在")
return await _to_out(session, assistant)
@router.post("/{assistant_id}/duplicate", response_model=AssistantOut)
async def duplicate_assistant(
assistant_id: str, session: AsyncSession = Depends(get_session)
):
source = await session.get(Assistant, assistant_id)
if not source:
raise HTTPException(404, "助手不存在")
assistant = Assistant(
id=f"asst_{uuid.uuid4().hex[:12]}",
name=f"{source.name} 副本",
type=source.type,
runtime_mode=source.runtime_mode,
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,
vision_prompt_mode=source.vision_prompt_mode or "runtime",
vision_prompt=source.vision_prompt or "",
knowledge_base_id=source.knowledge_base_id,
knowledge_retrieval_config=dict(source.knowledge_retrieval_config or {}),
prompt=source.prompt,
dynamic_variable_definitions=dict(source.dynamic_variable_definitions or {}),
built_in_tools=dict(source.built_in_tools or {}),
analysis_config=dict(source.analysis_config or {}),
api_url=source.api_url,
api_key=source.api_key,
app_id=source.app_id,
graph=dict(source.graph or {}),
)
session.add(assistant)
await session.flush()
await _sync_bindings(session, assistant.id, await _resource_ids(session, source.id))
source_bindings = await _tool_bindings(session, source.id)
from schemas import ToolBinding
await _sync_tool_bindings(
session,
assistant.id,
assistant.type,
[ToolBinding(**binding) for binding in source_bindings],
assistant.graph,
)
await session.commit()
await session.refresh(assistant)
return await _to_out(session, assistant)
@router.put("/{assistant_id}", response_model=AssistantOut)
async def update_assistant(
assistant_id: str,
body: AssistantUpsert,
session: AsyncSession = Depends(get_session),
):
assistant = await session.get(Assistant, assistant_id)
if not assistant:
raise HTTPException(404, "助手不存在")
_validate_workflow(body)
await _validate_workflow_references(session, body)
_validate_update_state_config(body)
await _validate_tool_bindings(session, body)
await _validate_prompt_realtime_tool_capabilities(session, body)
await _validate_startup_actions(session, body)
await _validate_vision_model(session, body)
await _validate_analysis_model(session, body)
await _validate_knowledge_base(session, body)
data = body.model_dump()
resource_ids = data.pop("model_resource_ids")
tool_bindings = body.tool_bindings
data.pop("tool_bindings")
data["built_in_tools"] = body.built_in_tools.model_dump(exclude_none=True)
data["api_key"] = resolve_incoming_key(data["api_key"], assistant.api_key)
for key, value in data.items():
setattr(assistant, key, value)
await _sync_bindings(session, assistant.id, resource_ids)
await _sync_tool_bindings(
session,
assistant.id,
assistant.type,
tool_bindings,
body.graph,
)
await session.commit()
await session.refresh(assistant)
return await _to_out(session, assistant)
@router.delete("/{assistant_id}")
async def delete_assistant(
assistant_id: str, session: AsyncSession = Depends(get_session)
):
assistant = await session.get(Assistant, assistant_id)
if not assistant:
raise HTTPException(404, "助手不存在")
await session.delete(assistant)
await session.commit()
return {"ok": True}