- ContentBlock 全 21 类型 Pydantic 判别联合(与 openapi 契约一一对齐,extra=forbid);系统产生类 block(tool_call/tool_result/agent_status/approval_request/artifact)拒绝客户端伪造(契约 3.5) - 迁移 4:run_event 表((run_id, sequence) 唯一、事件类型检查约束)+ conversation.current_branch_id - Run 事件持久化:状态事件、每 block completed、message.completed 与权威状态同事务写入;sequence 单调从 0;SSE 改为重放持久化事件(不再现场构造),delta/progress 留待真实模型流(WP-05),不伪造 - GET messages:opaque cursor 分页((created_at,id) 复合键、base64 编码、limit 1-200、has_more) - branches:列表 + 当前路径切换(采用≠归真);跨会话/不存在分支 422 fail-closed - 测试 36 项全绿:21 类型接受、runtime block 拒绝、事件重放序号单调、分页无重叠、cursor 非法拒绝、分支切换与跨域
276 lines
6.1 KiB
Python
276 lines
6.1 KiB
Python
"""对话域 schema:统一 ContentBlock 判别联合(21 类型)。
|
|
|
|
与 openapi/openapi.yaml 的 components.schemas 一一对齐;此为服务端
|
|
Pydantic 边界(AGENTS 8.2),字段语义以契约 docs/contracts/content-blocks-and-streaming.md 为准。
|
|
markdown block 在服务端经灵台引擎规范化(契约第 4 节)。
|
|
"""
|
|
|
|
from typing import Annotated, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class BlockBase(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
id: str | None = None # 创建请求可省略,由服务端生成
|
|
status: Literal["pending", "streaming", "completed", "failed", "cancelled"] | None = None
|
|
|
|
|
|
# ===== 文本与结构 =====
|
|
|
|
|
|
class TextBlock(BlockBase):
|
|
type: Literal["text"]
|
|
text: str
|
|
language: str | None = None
|
|
|
|
|
|
class MarkdownBlock(BlockBase):
|
|
type: Literal["markdown"]
|
|
text: str
|
|
flavor: Literal["hunjing-gfm-v1"] = "hunjing-gfm-v1"
|
|
|
|
|
|
class CodeBlock(BlockBase):
|
|
type: Literal["code"]
|
|
source: str
|
|
language: str | None = None
|
|
filename: str | None = None
|
|
|
|
|
|
class QuoteBlock(BlockBase):
|
|
type: Literal["quote"]
|
|
content: list["ContentBlock"]
|
|
source_ref: str | None = None
|
|
|
|
|
|
class TableBlock(BlockBase):
|
|
type: Literal["table"]
|
|
columns: list[str]
|
|
rows: list[list[object]]
|
|
caption: str | None = None
|
|
|
|
|
|
class CalloutBlock(BlockBase):
|
|
type: Literal["callout"]
|
|
kind: Literal["info", "warning", "danger", "success"]
|
|
title: str | None = None
|
|
content: list["ContentBlock"]
|
|
|
|
|
|
# ===== 数学与图 =====
|
|
|
|
|
|
class MathBlock(BlockBase):
|
|
type: Literal["math"]
|
|
notation: Literal["latex", "asciimath"]
|
|
display: Literal["inline", "block"]
|
|
source: str
|
|
fallback_text: str | None = None
|
|
label: str | None = None
|
|
|
|
|
|
class MermaidBlock(BlockBase):
|
|
type: Literal["mermaid"]
|
|
source: str
|
|
title: str | None = None
|
|
caption: str | None = None
|
|
fallback_code_language: str = "mermaid"
|
|
|
|
|
|
# ===== 媒体与文件 =====
|
|
|
|
|
|
class ImageBlock(BlockBase):
|
|
type: Literal["image"]
|
|
file_id: str | None = None
|
|
url_ref: str | None = None
|
|
alt: str
|
|
width: int | None = Field(default=None, ge=1)
|
|
height: int | None = Field(default=None, ge=1)
|
|
caption: str | None = None
|
|
|
|
|
|
class VideoBlock(BlockBase):
|
|
type: Literal["video"]
|
|
file_id: str
|
|
poster_file_id: str | None = None
|
|
caption: str | None = None
|
|
transcript_file_id: str | None = None
|
|
duration_ms: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
class AudioBlock(BlockBase):
|
|
type: Literal["audio"]
|
|
file_id: str
|
|
transcript: str | None = None
|
|
duration_ms: int | None = Field(default=None, ge=0)
|
|
waveform: str | None = None
|
|
|
|
|
|
class FileBlock(BlockBase):
|
|
type: Literal["file"]
|
|
file_id: str
|
|
filename: str
|
|
media_type: str
|
|
size_bytes: int = Field(ge=0)
|
|
description: str | None = None
|
|
|
|
|
|
class GalleryItem(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
type: Literal["image", "video"]
|
|
file_id: str
|
|
alt: str | None = None
|
|
caption: str | None = None
|
|
|
|
|
|
class GalleryBlock(BlockBase):
|
|
type: Literal["gallery"]
|
|
items: list[GalleryItem]
|
|
layout: str | None = None
|
|
|
|
|
|
class AvatarBlock(BlockBase):
|
|
type: Literal["avatar"]
|
|
asset_id: str
|
|
format: Literal["live2d", "vrm", "gltf"]
|
|
preview_file_id: str | None = None
|
|
|
|
|
|
# ===== Agent 与工具 =====
|
|
|
|
|
|
class ToolCallBlock(BlockBase):
|
|
type: Literal["tool_call"]
|
|
tool_name: str
|
|
call_id: str
|
|
arguments_redacted: dict[str, object] | None = None
|
|
approval_id: str | None = None
|
|
|
|
|
|
class ToolResultBlock(BlockBase):
|
|
type: Literal["tool_result"]
|
|
call_id: str
|
|
summary: dict[str, object] | None = None
|
|
file_ids: list[str] | None = None
|
|
is_error: bool | None = None
|
|
|
|
|
|
class AgentStatusBlock(BlockBase):
|
|
type: Literal["agent_status"]
|
|
agent_id: str | None = None
|
|
child_run_id: str | None = None
|
|
stage: str
|
|
progress: dict[str, object] | None = None
|
|
waiting_reason: str | None = None
|
|
|
|
|
|
class ApprovalRequestBlock(BlockBase):
|
|
type: Literal["approval_request"]
|
|
approval_id: str
|
|
action: str
|
|
impact: str | None = None
|
|
arguments_summary: dict[str, object] | None = None
|
|
expires_at: str | None = None
|
|
|
|
|
|
class ArtifactBlock(BlockBase):
|
|
type: Literal["artifact"]
|
|
artifact_id: str
|
|
artifact_type: str
|
|
version: int | None = None
|
|
|
|
|
|
class CitationBlock(BlockBase):
|
|
type: Literal["citation"]
|
|
source_id: str
|
|
title: str
|
|
quoted_text: str
|
|
locator: dict[str, object] | None = None
|
|
|
|
|
|
class ErrorBlock(BlockBase):
|
|
type: Literal["error"]
|
|
message: str
|
|
retryable: bool | None = None
|
|
fallback: str | None = None
|
|
|
|
|
|
_BlockTypes = (
|
|
TextBlock
|
|
| MarkdownBlock
|
|
| CodeBlock
|
|
| QuoteBlock
|
|
| TableBlock
|
|
| CalloutBlock
|
|
| MathBlock
|
|
| MermaidBlock
|
|
| ImageBlock
|
|
| VideoBlock
|
|
| AudioBlock
|
|
| FileBlock
|
|
| GalleryBlock
|
|
| AvatarBlock
|
|
| ToolCallBlock
|
|
| ToolResultBlock
|
|
| AgentStatusBlock
|
|
| ApprovalRequestBlock
|
|
| ArtifactBlock
|
|
| CitationBlock
|
|
| ErrorBlock
|
|
)
|
|
|
|
ContentBlock = Annotated[_BlockTypes, Field(discriminator="type")]
|
|
|
|
# 系统产生类 block:普通 Message 请求不允许客户端伪造(契约 3.5 节)
|
|
RUNTIME_BLOCK_TYPES = frozenset(
|
|
{"tool_call", "tool_result", "agent_status", "approval_request", "artifact"}
|
|
)
|
|
|
|
|
|
class MessageIn(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
content: list[ContentBlock] = Field(min_length=1, max_length=64)
|
|
start_run: bool = True
|
|
|
|
|
|
class MessageOut(BaseModel):
|
|
id: str
|
|
conversation_id: str
|
|
branch_id: str
|
|
role: str
|
|
content: list[dict[str, object]]
|
|
parser_version: str | None = None
|
|
created_at: str
|
|
|
|
|
|
class RunOut(BaseModel):
|
|
id: str
|
|
conversation_id: str | None
|
|
status: str
|
|
created_at: str
|
|
completed_at: str | None = None
|
|
|
|
|
|
class MessageCreatedOut(BaseModel):
|
|
message: MessageOut
|
|
run: RunOut | None
|
|
|
|
|
|
class BranchOut(BaseModel):
|
|
id: str
|
|
conversation_id: str
|
|
title: str
|
|
created_at: str
|
|
|
|
|
|
class ConversationOut(BaseModel):
|
|
id: str
|
|
title: str
|
|
current_branch_id: str | None = None
|
|
created_at: str
|