"""对话域 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