Files
ai-safety-platform/app/schemas/api.py
T
baozaotumao2025 14722be770 first commit
2026-07-18 21:00:26 +08:00

275 lines
11 KiB
Python

from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class HealthResponse(BaseModel):
status: str = Field(description="服务状态;正常时为 `ok`。")
environment: str = Field(description="当前运行环境,例如 `test` 或 `production`。")
version: str = Field(description="服务版本号。")
class LoginRequest(BaseModel):
username: str = Field(min_length=1, max_length=64)
password: str = Field(min_length=1, max_length=256)
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
expires_at: int
refresh_token: str
refresh_expires_at: int
class RefreshTokenRequest(BaseModel):
refresh_token: str = Field(min_length=32, max_length=512)
class CreateUserRequest(BaseModel):
username: str = Field(min_length=1, max_length=64)
password: str = Field(min_length=8, max_length=256)
is_admin: bool = False
class UpdateUserRequest(BaseModel):
is_active: bool
class ResetPasswordRequest(BaseModel):
password: str = Field(min_length=8, max_length=256)
class ChangePasswordRequest(BaseModel):
current_password: str = Field(min_length=1, max_length=256)
new_password: str = Field(min_length=8, max_length=256)
class UserResponse(BaseModel):
id: int
username: str
is_active: bool
is_admin: bool
class ProviderCheckResponse(BaseModel):
"""模型服务最小推理检查的结果。"""
provider_id: Literal["target", "judge"] = Field(description="模型提供商配置 ID。")
endpoint: str = Field(description="实际调用的 chat/completions 地址。")
authentication: dict[str, Any] = Field(
description="认证配置状态;不会返回 API Key 或 Authorization 请求头。"
)
model: str = Field(description="本次请求使用的模型名称。")
reachable: bool = Field(description="`true` 表示认证和最小推理均成功。")
response_preview: str = Field(default="", description="模型回复的前 200 个字符。")
class ErrorBody(BaseModel):
code: str = Field(description="稳定的应用错误码。")
message: str = Field(description="面向调用方的错误说明。")
details: dict[str, Any] = Field(default_factory=dict, description="上游错误的有限诊断信息。")
request_id: str | None = Field(default=None, description="用于排查日志的请求 ID。")
class ErrorResponse(BaseModel):
error: ErrorBody
class DiscoverModelsResponse(BaseModel):
provider_id: Literal["target", "judge"] = Field(description="模型提供商配置 ID。")
models: list[str] = Field(description="上游 `/models` 返回的模型 ID 列表。")
class ProviderConfigCreate(BaseModel):
provider_id: Literal["target", "judge"] = Field(description="稳定资源 ID。")
base_url: str = Field(min_length=1, max_length=1024)
chat_path: str = Field(default="/chat/completions", min_length=1, max_length=255)
models_path: str = Field(default="/models", min_length=1, max_length=255)
model_name: str = Field(min_length=1, max_length=255)
auth_type: Literal["none", "bearer", "api_key"] = "none"
api_key: str = Field(default="", max_length=8192)
auth_header: str = Field(default="Authorization", min_length=1, max_length=255)
auth_prefix: str = Field(default="Bearer", max_length=255)
verify_ssl: bool = True
@field_validator("base_url")
@classmethod
def validate_base_url(cls, value: str) -> str:
if not value.startswith(("http://", "https://")):
raise ValueError("base_url 必须使用 http 或 https")
return value.rstrip("/")
@model_validator(mode="after")
def validate_auth(self):
if self.auth_type != "none" and not self.api_key:
raise ValueError("启用认证时必须提供 API Key")
return self
class ProviderConfigUpdate(BaseModel):
base_url: str | None = Field(default=None, min_length=1, max_length=1024)
chat_path: str | None = Field(default=None, min_length=1, max_length=255)
models_path: str | None = Field(default=None, min_length=1, max_length=255)
model_name: str | None = Field(default=None, min_length=1, max_length=255)
auth_type: Literal["none", "bearer", "api_key"] | None = None
api_key: str | None = Field(default=None, max_length=8192)
auth_header: str | None = Field(default=None, min_length=1, max_length=255)
auth_prefix: str | None = Field(default=None, max_length=255)
verify_ssl: bool | None = None
@field_validator("base_url")
@classmethod
def validate_base_url(cls, value: str | None) -> str | None:
if value is not None and not value.startswith(("http://", "https://")):
raise ValueError("base_url 必须使用 http 或 https")
return value.rstrip("/") if value else value
class ProviderConfigResponse(BaseModel):
provider_id: Literal["target", "judge"] = Field(description="稳定资源 ID。")
source: Literal["database", "env"] = Field(description="当前生效配置的来源。")
base_url: str
chat_path: str
models_path: str
model_name: str
auth_type: str
auth_header: str
auth_prefix: str
verify_ssl: bool
api_key_configured: bool
created_at: datetime | None
updated_at: datetime | None
class StartRunRequest(BaseModel):
model_config = ConfigDict(
json_schema_extra={"examples": [{"profile": "smoke", "auto_judge": True}]}
)
profile: Literal["smoke", "all"] = Field(
default="smoke",
description="测试范围;`smoke` 对单轮、多轮、工具和图片各执行 1 条,`all` 执行所有能力支持的样例。",
)
auto_judge: bool = Field(
default=True,
description="是否调用 `judge` 模型自动裁判;关闭后仅保存被测模型响应。",
)
class RunResponse(BaseModel):
model_config = ConfigDict(
json_schema_extra={"examples": [{"run_id": 42, "status": "pending", "selected_count": 0}]}
)
run_id: int = Field(description="已创建的测试运行 ID,可用于后续查询。")
status: str = Field(description="创建时为 `pending`,可通过状态接口跟踪后续阶段。")
selected_count: int = Field(description="能力筛选后实际执行的样例数。")
class ResumeRunResponse(RunResponse):
skipped_count: int = Field(description="已持久化、恢复时不会重复执行的样例数。")
class RetryErrorsResponse(RunResponse):
retry_count: int = Field(description="本次移除并重新排队的 error 结果数。")
class RetryResultResponse(RunResponse):
execution_id: str = Field(description="本次移除并重新排队的样例执行 ID。")
class UpdateRunRequest(BaseModel):
status: Literal["cancelled"] = Field(description="唯一允许的运行状态更新:取消运行。")
class RunDetailResponse(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"run_id": 42,
"status": "running",
"terminal": False,
"phase": "executing",
"selected_count": 10,
"processed_count": 5,
"completed_count": 4,
"error_count": 1,
"progress_percent": 50.0,
"current_execution_id": "R0006",
"started_at": "2026-07-16T06:51:29Z",
"finished_at": None,
"error_message": None,
"poll_after_seconds": 2,
"summary": {
"phase": "executing",
"completed": 4,
"errors": 1,
"selected": 10,
},
}
]
}
)
run_id: int = Field(description="测试运行 ID。")
status: str = Field(
description="pending、probing、running、cancelled、completed、completed_with_errors 或 failed。"
)
terminal: bool = Field(description="true 表示后台已结束,前端应停止轮询。")
phase: str = Field(
description="当前阶段:pending、probing、executing、judging、finished 或 failed。"
)
selected_count: int = Field(description="计划执行的样例数。")
processed_count: int = Field(description="已处理数,等于 completed_count + error_count。")
completed_count: int = Field(description="已完成的样例数。")
error_count: int = Field(description="执行错误的样例数,不包含仲裁 verdict=fail。")
progress_percent: float = Field(description="已处理数占已选样例数的百分比。", ge=0, le=100)
current_execution_id: str | None = Field(description="当前执行或裁判的样例 ID。")
started_at: datetime = Field(description="运行记录创建时间。")
finished_at: datetime | None = Field(description="进入最终状态的时间;未结束时为 null。")
error_message: str | None = Field(description="全局失败原因;非 failed 状态为 null。")
poll_after_seconds: int = Field(description="建议的下次轮询间隔;终态为 0。")
summary: dict[str, Any] = Field(description="运行过程中累计的结构化汇总数据。")
class ResultItem(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"execution_id": "R0001",
"case_kind": "risk",
"interaction_mode": "single_turn",
"execution_status": "completed",
"verdict": "pass",
"model_input": {"messages": [{"role": "user", "content": "测试问题…"}]},
"model_response": "...",
"judge_result": {"verdict": "pass", "score": 1.0, "reason": "..."},
"error_message": "",
}
]
}
)
execution_id: str = Field(description="数据集样例的执行 ID。")
case_kind: str = Field(description="样例类别,例如风险或对照。")
interaction_mode: str = Field(description="交互模式,例如 single_turn、multi_turn、tool。")
execution_status: str = Field(description="执行状态:`completed` 或 `error`。")
verdict: str | None = Field(
description="仲裁结论:`pass`、`fail`、`needs_human_review`;未仲裁或执行错误时为空。"
)
model_input: dict[str, Any] = Field(description="该样例发送给被测模型的完整输入。")
model_response: str = Field(description="被测模型的原始文本回复。")
judge_result: dict[str, Any] = Field(description="自动裁判的结构化结果;未启用时为空对象。")
error_message: str = Field(description="执行失败原因;成功时为空字符串。")
class ReportResponse(BaseModel):
run_id: int = Field(description="测试运行 ID。")
summary: dict[str, Any] = Field(
description="汇总指标,包含 execution_statuses、verdicts 和按交互模式分维度聚合的 by_mode。"
)