F2: persist native job projections and expose task controls
This commit is contained in:
@@ -242,6 +242,11 @@ 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, "每个助手最多绑定一个后台任务工具")
|
||||
reserved = sorted(
|
||||
tool.function_name
|
||||
for tool in tools.values()
|
||||
@@ -282,6 +287,8 @@ async def _validate_tool_bindings(
|
||||
type=tool.type,
|
||||
definition=tool.definition or {},
|
||||
)
|
||||
if tool.type == "task" and binding.result_assignments:
|
||||
raise HTTPException(400, "后台任务结果通过任务通知返回,不支持结果变量映射")
|
||||
properties, required = ToolExecutor.input_schema(runtime_tool)
|
||||
unknown = sorted(set(binding.argument_bindings) - set(properties))
|
||||
if unknown:
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Administrator task projections and controls, backed by native Jobs."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import Field
|
||||
from schemas import CamelModel
|
||||
from db.session import get_session
|
||||
from services.auth import require_admin
|
||||
from services.background_tasks.service import task_service
|
||||
from services.config_resolver import resolve_runtime_config
|
||||
|
||||
router = APIRouter(prefix="/api/background-tasks", tags=["background-tasks"], dependencies=[Depends(require_admin)])
|
||||
|
||||
|
||||
class SubmitTask(CamelModel):
|
||||
assistant_id: str
|
||||
tool_id: str
|
||||
instruction: str = Field(min_length=1, max_length=12000)
|
||||
request_id: str = Field(min_length=1, max_length=80)
|
||||
|
||||
|
||||
class TaskInput(CamelModel):
|
||||
reply: str | None = None
|
||||
answers: list[list[str]] | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_tasks(assistant_id: str):
|
||||
return [record.public() for record in await task_service.repository.list(assistant_id)]
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def submit_task(body: SubmitTask, db=Depends(get_session)):
|
||||
cfg = await resolve_runtime_config(db, body.assistant_id)
|
||||
if cfg.type != "prompt" or cfg.runtimeMode != "pipeline":
|
||||
raise HTTPException(400, "后台任务仅支持提示词 Pipeline")
|
||||
bound = {binding.tool_id for binding in cfg.tool_bindings}
|
||||
tool = next((tool for tool in cfg.tools if tool.id == body.tool_id and tool.id in bound and tool.type == "task"), None)
|
||||
if not tool:
|
||||
raise HTTPException(400, "助手未绑定该任务工具")
|
||||
try:
|
||||
return await task_service.submit(assistant_id=body.assistant_id, conversation_id="web_" + body.assistant_id, tool=tool, instruction=body.instruction, invocation_id=body.request_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/{task_id}")
|
||||
async def get_task(task_id: str):
|
||||
try:
|
||||
return (await task_service.get(task_id)).public()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(404, str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/{task_id}/cancel")
|
||||
async def cancel_task(task_id: str):
|
||||
try:
|
||||
return await task_service.cancel(task_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(404, str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/{task_id}/input")
|
||||
async def send_input(task_id: str, body: TaskInput):
|
||||
try:
|
||||
return await task_service.send_input(task_id, body.model_dump(exclude_none=True))
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, "远端暂未接受回复,请稍后重试") from exc
|
||||
@@ -100,7 +100,7 @@ def _payload(
|
||||
_normalize_http_template_inputs(definition)
|
||||
secrets = (
|
||||
merge_secrets(body.secrets, stored_secrets or {})
|
||||
if definition["type"] == "http"
|
||||
if definition["type"] in {"http", "task"}
|
||||
else {}
|
||||
)
|
||||
secret_variables = (secrets.get("dynamic_variables") or {}) if secrets else {}
|
||||
@@ -154,7 +154,7 @@ async def list_tools(session: AsyncSession = Depends(get_session)):
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(Tool)
|
||||
.where(Tool.type.in_(("http", "mcp", "client")))
|
||||
.where(Tool.type.in_(("http", "mcp", "client", "task")))
|
||||
.order_by(Tool.updated_at.desc())
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
Reference in New Issue
Block a user