F1: add reusable task backend resources and migrate task tools
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""Public task backend configuration; runtime credentials stay server-side."""
|
||||
|
||||
from typing import Any, Literal
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from pydantic import ConfigDict, Field, field_validator, model_validator
|
||||
from schemas import CamelModel, ToolBinding
|
||||
|
||||
|
||||
class TaskBackendConfig(CamelModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
instructions: str = Field(default="", max_length=16000)
|
||||
timeout_seconds: int = Field(default=600, ge=30, le=7200)
|
||||
max_concurrency: Literal[1] = 1
|
||||
|
||||
|
||||
class TaskSkill(CamelModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
content: str = Field(min_length=1, max_length=16000)
|
||||
|
||||
@field_validator("name", "content")
|
||||
@classmethod
|
||||
def require_nonempty(cls, value: str) -> str:
|
||||
if not value.strip():
|
||||
raise ValueError("技能名称和正文不能为空")
|
||||
return value.strip()
|
||||
|
||||
|
||||
class ReasonerBackendConfig(TaskBackendConfig):
|
||||
model_resource_id: str = Field(min_length=1, max_length=40)
|
||||
reasoning_effort: Literal["provider_default", "low", "medium", "high"] = "provider_default"
|
||||
tool_bindings: list[ToolBinding] = Field(default_factory=list, max_length=50)
|
||||
skills: list[TaskSkill] = Field(default_factory=list, max_length=20)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def unique_bindings_and_skills(self):
|
||||
ids = [binding.tool_id for binding in self.tool_bindings]
|
||||
if len(ids) != len(set(ids)):
|
||||
raise ValueError("Reasoner 工具绑定不能重复")
|
||||
names = [skill.name for skill in self.skills]
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("技能名称不能重复")
|
||||
return self
|
||||
|
||||
|
||||
class OpenCodeBackendConfig(TaskBackendConfig):
|
||||
url: str = Field(min_length=1, max_length=2048)
|
||||
directory: str = Field(min_length=1, max_length=2048)
|
||||
username: str = Field(default="opencode", max_length=128)
|
||||
agent: str = Field(default="", max_length=128)
|
||||
model_provider_id: str = Field(default="", max_length=128)
|
||||
model_id: str = Field(default="", max_length=256)
|
||||
skills_note: str = Field(default="", max_length=4000)
|
||||
|
||||
@field_validator("url")
|
||||
@classmethod
|
||||
def validate_url(cls, value: str) -> str:
|
||||
parsed = urlsplit(value)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
|
||||
raise ValueError("任务服务须使用不含凭证、查询参数的 HTTP(S) 地址")
|
||||
return value.rstrip("/")
|
||||
|
||||
@field_validator("directory")
|
||||
@classmethod
|
||||
def require_directory(cls, value: str) -> str:
|
||||
if not value.strip():
|
||||
raise ValueError("必须填写远端工作目录")
|
||||
return value.strip()
|
||||
|
||||
@model_validator(mode="after")
|
||||
def require_model_pair(self):
|
||||
if bool(self.model_provider_id) != bool(self.model_id):
|
||||
raise ValueError("远端模型的 provider ID 和 model ID 必须同时填写")
|
||||
return self
|
||||
|
||||
|
||||
class TaskBackendUpsert(CamelModel):
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
type: Literal["reasoner", "opencode"]
|
||||
description: str = Field(default="", max_length=2048)
|
||||
config: ReasonerBackendConfig | OpenCodeBackendConfig
|
||||
secrets: dict[str, Any] = Field(default_factory=dict)
|
||||
enabled: bool = True
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def select_config(cls, value):
|
||||
if isinstance(value, dict) and value.get("type") in {"reasoner", "opencode"}:
|
||||
value = dict(value)
|
||||
config_class = ReasonerBackendConfig if value["type"] == "reasoner" else OpenCodeBackendConfig
|
||||
raw = value.get("config") or {}
|
||||
if isinstance(raw, CamelModel):
|
||||
raw = raw.model_dump()
|
||||
value["config"] = config_class.model_validate(raw)
|
||||
return value
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def require_name(cls, value: str) -> str:
|
||||
if not value.strip():
|
||||
raise ValueError("后端名称不能为空")
|
||||
return value.strip()
|
||||
|
||||
|
||||
class TaskBackendOut(TaskBackendUpsert):
|
||||
id: str
|
||||
updated_at: str | None = None
|
||||
Reference in New Issue
Block a user