F1: add reusable task backend resources and migrate task tools

This commit is contained in:
Xin Wang
2026-10-23 23:45:50 +08:00
parent e958491201
commit 529250173d
17 changed files with 741 additions and 43 deletions
+19 -9
View File
@@ -26,6 +26,7 @@ from services.state_expressions import StateExpressionError, validate_update_rul
from services.system_tools import RESERVED_TOOL_NAMES
from services.tool_policy import policy_for_tool
from services.tool_executor import ToolExecutor
from services.task_backend_resources import validate_backend_binding
from services.workflow_engine import WorkflowEngine
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -242,11 +243,12 @@ async def _validate_tool_bindings(
missing = sorted(tool_ids - set(tools))
if missing:
raise HTTPException(400, "工具不存在或未启用: " + ", ".join(missing))
task_tools = [tool for tool in tools.values() if tool.type == "task"]
if task_tools and (body.type != "prompt" or body.runtime_mode != "pipeline"):
raise HTTPException(400, "后台任务工具仅支持提示词 Pipeline")
if len(task_tools) > 1:
raise HTTPException(400, "每个助手最多绑定一个后台任务工具")
if any(tool.type not in {"http", "mcp", "client"} for tool in tools.values()):
for scope, raw in raw_bindings:
tool = tools.get(raw.get("tool_id") or raw.get("toolId"))
if scope.startswith(("Action ", "Startup ")) and tool and tool.type == "task":
raise HTTPException(400, "任务后端不支持启动 Action")
raise HTTPException(400, "普通工具仅支持 HTTP/MCP/Client;任务请配置任务后端")
reserved = sorted(
tool.function_name
for tool in tools.values()
@@ -287,10 +289,6 @@ async def _validate_tool_bindings(
type=tool.type,
definition=tool.definition or {},
)
if tool.type == "task" and binding.result_assignments:
raise HTTPException(400, "后台任务结果通过任务通知返回,不支持结果变量映射")
if tool.type == "task" and scope.startswith(("Action ", "Startup ")):
raise HTTPException(400, "后台任务工具不支持启动 Action")
properties, required = ToolExecutor.input_schema(runtime_tool)
unknown = sorted(set(binding.argument_bindings) - set(properties))
if unknown:
@@ -506,6 +504,13 @@ async def _validate_knowledge_base(session: AsyncSession, body: AssistantUpsert)
raise HTTPException(400, "知识库不存在")
async def _validate_task_backend(session: AsyncSession, body: AssistantUpsert) -> None:
try:
await validate_backend_binding(session, body)
except ValueError as exc:
raise HTTPException(400, str(exc)) from exc
async def _sync_bindings(
session: AsyncSession, assistant_id: str, resource_ids: dict[str, str]
) -> None:
@@ -641,6 +646,7 @@ async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
else []
),
built_in_tools=assistant.built_in_tools or {},
task_backend=getattr(assistant, "task_backend", None),
prompt=assistant.prompt,
dynamic_variable_definitions=assistant.dynamic_variable_definitions or {},
analysis_config=assistant.analysis_config or {},
@@ -681,6 +687,7 @@ async def create_assistant(
await _validate_vision_model(session, body)
await _validate_analysis_model(session, body)
await _validate_knowledge_base(session, body)
await _validate_task_backend(session, body)
data = body.model_dump()
resource_ids = data.pop("model_resource_ids")
tool_bindings = body.tool_bindings
@@ -721,6 +728,7 @@ async def duplicate_assistant(
raise HTTPException(404, "助手不存在")
if source.type not in SUPPORTED_ASSISTANT_TYPES:
raise HTTPException(410, "该助手类型已下线")
await _validate_task_backend(session, source)
assistant = Assistant(
id=f"asst_{uuid.uuid4().hex[:12]}",
name=f"{source.name} 副本",
@@ -739,6 +747,7 @@ async def duplicate_assistant(
prompt=source.prompt,
dynamic_variable_definitions=dict(source.dynamic_variable_definitions or {}),
built_in_tools=dict(source.built_in_tools or {}),
task_backend=dict(source.task_backend) if source.task_backend else None,
analysis_config=dict(source.analysis_config or {}),
api_url=source.api_url,
api_key=source.api_key,
@@ -780,6 +789,7 @@ async def update_assistant(
await _validate_vision_model(session, body)
await _validate_analysis_model(session, body)
await _validate_knowledge_base(session, body)
await _validate_task_backend(session, body)
data = body.model_dump()
resource_ids = data.pop("model_resource_ids")
tool_bindings = body.tool_bindings