chore: 迁移到 social-app 架构,集成 Supabase 和 taskiq worker
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
from schemas.agent.forwarded_props import (
|
||||
ClientTimeContext,
|
||||
ForwardedPropsPayload,
|
||||
parse_forwarded_props_client_time,
|
||||
parse_forwarded_props_runtime_mode,
|
||||
)
|
||||
from schemas.agent.forwarded_props import RuntimeMode
|
||||
from schemas.agent.runtime_models import (
|
||||
AgentOutput,
|
||||
ConstraintItem,
|
||||
ExecutionMode,
|
||||
KeyEntity,
|
||||
NormalizedTaskInput,
|
||||
ResultTyping,
|
||||
ResultType,
|
||||
RouterAgentOutput,
|
||||
RunStatus,
|
||||
TaskType,
|
||||
TaskTyping,
|
||||
ToolAgentOutput,
|
||||
ToolStatus,
|
||||
WorkerAgentOutputLite,
|
||||
WorkerAgentOutputRich,
|
||||
resolve_worker_output_model,
|
||||
)
|
||||
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
|
||||
from schemas.agent.visibility import SystemVisibilityBit, VisibilityMask, bit_mask
|
||||
from schemas.agent.ui_hints import (
|
||||
UiHintAction,
|
||||
UiHintIntent,
|
||||
UiHintSection,
|
||||
UiHintStatus,
|
||||
UiHintsPayload,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AgentType",
|
||||
"AgentOutput",
|
||||
"ConstraintItem",
|
||||
"ExecutionMode",
|
||||
"ForwardedPropsPayload",
|
||||
"KeyEntity",
|
||||
"NormalizedTaskInput",
|
||||
"ResultTyping",
|
||||
"ClientTimeContext",
|
||||
"ResultType",
|
||||
"RouterAgentOutput",
|
||||
"RunStatus",
|
||||
"RuntimeMode",
|
||||
"TaskType",
|
||||
"TaskTyping",
|
||||
"SystemAgentLLMConfig",
|
||||
"SystemVisibilityBit",
|
||||
"ToolAgentOutput",
|
||||
"ToolStatus",
|
||||
"UiHintAction",
|
||||
"UiHintIntent",
|
||||
"UiHintSection",
|
||||
"UiHintStatus",
|
||||
"UiHintsPayload",
|
||||
"VisibilityMask",
|
||||
"WorkerAgentOutputLite",
|
||||
"WorkerAgentOutputRich",
|
||||
"bit_mask",
|
||||
"parse_forwarded_props_client_time",
|
||||
"parse_forwarded_props_runtime_mode",
|
||||
"resolve_worker_output_model",
|
||||
]
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import re
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
StrictInt,
|
||||
ValidationError,
|
||||
field_validator,
|
||||
)
|
||||
|
||||
_RFC3339_WITH_TZ_PATTERN = re.compile(
|
||||
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$"
|
||||
)
|
||||
|
||||
|
||||
class ClientTimeContext(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
device_timezone: str = Field(
|
||||
...,
|
||||
description="IANA timezone from client device, e.g. America/Los_Angeles.",
|
||||
)
|
||||
client_now_iso: str = Field(
|
||||
...,
|
||||
description="RFC3339 datetime with timezone offset from client device.",
|
||||
)
|
||||
client_epoch_ms: StrictInt = Field(
|
||||
...,
|
||||
ge=0,
|
||||
description="Unix epoch milliseconds from client device.",
|
||||
)
|
||||
|
||||
@field_validator("device_timezone")
|
||||
@classmethod
|
||||
def validate_device_timezone(cls, value: str) -> str:
|
||||
try:
|
||||
ZoneInfo(value)
|
||||
except ZoneInfoNotFoundError as exc:
|
||||
raise ValueError("invalid client_time.device_timezone") from exc
|
||||
return value
|
||||
|
||||
@field_validator("client_now_iso")
|
||||
@classmethod
|
||||
def validate_client_now_iso(cls, value: str) -> str:
|
||||
if not _RFC3339_WITH_TZ_PATTERN.fullmatch(value):
|
||||
raise ValueError("invalid client_time.client_now_iso")
|
||||
normalized = value.replace("Z", "+00:00")
|
||||
try:
|
||||
parsed = datetime.fromisoformat(normalized)
|
||||
except ValueError as exc:
|
||||
raise ValueError("invalid client_time.client_now_iso") from exc
|
||||
if parsed.tzinfo is None:
|
||||
raise ValueError("invalid client_time.client_now_iso")
|
||||
return value
|
||||
|
||||
|
||||
class RuntimeMode(str, Enum):
|
||||
CHAT = "chat"
|
||||
AUTOMATION = "automation"
|
||||
|
||||
|
||||
class ForwardedPropsPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
runtime_mode: RuntimeMode
|
||||
client_time: ClientTimeContext | None = None
|
||||
|
||||
|
||||
def parse_forwarded_props(forwarded_props: object) -> ForwardedPropsPayload:
|
||||
if not isinstance(forwarded_props, dict):
|
||||
raise ValueError("invalid RunAgentInput.forwardedProps")
|
||||
try:
|
||||
return ForwardedPropsPayload.model_validate(forwarded_props)
|
||||
except ValidationError as exc:
|
||||
raise ValueError("invalid RunAgentInput.forwardedProps") from exc
|
||||
|
||||
|
||||
def parse_forwarded_props_client_time(
|
||||
forwarded_props: object,
|
||||
) -> ClientTimeContext | None:
|
||||
payload = parse_forwarded_props(forwarded_props)
|
||||
return payload.client_time
|
||||
|
||||
|
||||
def parse_forwarded_props_runtime_mode(forwarded_props: object) -> RuntimeMode:
|
||||
payload = parse_forwarded_props(forwarded_props)
|
||||
return payload.runtime_mode
|
||||
@@ -0,0 +1,177 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from schemas.agent.ui_hints import UiHintsPayload
|
||||
|
||||
|
||||
class TaskType(str, Enum):
|
||||
KNOWLEDGE = "knowledge"
|
||||
RECOMMENDATION = "recommendation"
|
||||
PLANNING = "planning"
|
||||
SCHEDULING = "scheduling"
|
||||
REMINDER_MANAGEMENT = "reminder_management"
|
||||
TODO_MANAGEMENT = "todo_management"
|
||||
COMMUNICATION_DRAFTING = "communication_drafting"
|
||||
INFORMATION_ORGANIZATION = "information_organization"
|
||||
STATUS_TRACKING = "status_tracking"
|
||||
TRANSACTION_ASSIST = "transaction_assist"
|
||||
ACTION_EXECUTION = "action_execution"
|
||||
TROUBLESHOOTING = "troubleshooting"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class ResultType(str, Enum):
|
||||
DIRECT_ANSWER = "direct_answer"
|
||||
OPTIONS_WITH_RECOMMENDATION = "options_with_recommendation"
|
||||
ACTION_PLAN = "action_plan"
|
||||
SCHEDULE_PROPOSAL = "schedule_proposal"
|
||||
TODO_LIST = "todo_list"
|
||||
DRAFT_MESSAGE = "draft_message"
|
||||
SUMMARY = "summary"
|
||||
PROGRESS_SUMMARY = "progress_summary"
|
||||
DIAGNOSIS_REPORT = "diagnosis_report"
|
||||
STRUCTURED_PAYLOAD = "structured_payload"
|
||||
EXECUTION_REPORT = "execution_report"
|
||||
CLARIFICATION_REQUEST = "clarification_request"
|
||||
SAFETY_BLOCK = "safety_block"
|
||||
ERROR_REPORT = "error_report"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class TaskTyping(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
primary: TaskType
|
||||
secondary: list[TaskType] = Field(default_factory=list, max_length=3)
|
||||
|
||||
|
||||
class ResultTyping(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
primary: ResultType
|
||||
secondary: list[ResultType] = Field(default_factory=list, max_length=3)
|
||||
|
||||
|
||||
class ExecutionMode(str, Enum):
|
||||
ONESTEP = "onestep"
|
||||
TOOL_ASSISTED = "tool_assisted"
|
||||
MULTISTEP = "multistep"
|
||||
|
||||
|
||||
class RunStatus(str, Enum):
|
||||
SUCCESS = "success"
|
||||
PARTIAL_SUCCESS = "partial_success"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class ToolStatus(str, Enum):
|
||||
SUCCESS = "success"
|
||||
FAILURE = "failure"
|
||||
PARTIAL = "partial"
|
||||
|
||||
|
||||
class KeyEntity(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str
|
||||
type: str
|
||||
value: str | None = None
|
||||
|
||||
@field_validator("value", mode="before")
|
||||
@classmethod
|
||||
def normalize_value(cls, value: object) -> object:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, bool | int | float):
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
|
||||
class ConstraintItem(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
key: str
|
||||
value: str
|
||||
required: bool = True
|
||||
|
||||
@field_validator("value", mode="before")
|
||||
@classmethod
|
||||
def normalize_value(cls, value: object) -> object:
|
||||
if isinstance(value, bool | int | float):
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
|
||||
class NormalizedTaskInput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
user_text: str
|
||||
multimodal_summary: list[str] = Field(default_factory=list)
|
||||
context_summary: str = Field(default="", max_length=2000)
|
||||
|
||||
|
||||
class RouterAgentOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
normalized_task_input: NormalizedTaskInput
|
||||
key_entities: list[KeyEntity] = Field(default_factory=list)
|
||||
constraints: list[ConstraintItem] = Field(default_factory=list)
|
||||
task_typing: TaskTyping
|
||||
execution_mode: ExecutionMode
|
||||
result_typing: ResultTyping
|
||||
|
||||
|
||||
class ErrorInfo(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
code: str
|
||||
message: str
|
||||
retryable: bool = False
|
||||
details: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ToolAgentOutput(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
tool_name: str
|
||||
tool_call_id: str
|
||||
tool_call_args: dict[str, Any] | None = None
|
||||
status: ToolStatus
|
||||
result: str
|
||||
error: ErrorInfo | None = None
|
||||
|
||||
|
||||
class WorkerAgentOutputLite(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
status: RunStatus = RunStatus.SUCCESS
|
||||
answer: str
|
||||
key_points: list[str] = Field(default_factory=list)
|
||||
result_type: ResultType = ResultType.UNKNOWN
|
||||
suggested_actions: list[str] = Field(default_factory=list)
|
||||
error: ErrorInfo | None = None
|
||||
|
||||
|
||||
class WorkerAgentOutputRich(WorkerAgentOutputLite):
|
||||
ui_hints: UiHintsPayload | None = None
|
||||
|
||||
|
||||
class AgentOutput(WorkerAgentOutputRich):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
|
||||
|
||||
|
||||
def resolve_worker_output_model(
|
||||
execution_mode: ExecutionMode,
|
||||
) -> type[WorkerAgentOutputLite]:
|
||||
if execution_mode == ExecutionMode.ONESTEP:
|
||||
return WorkerAgentOutputLite
|
||||
return WorkerAgentOutputRich
|
||||
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AgentType(str, Enum):
|
||||
ROUTER = "router"
|
||||
WORKER = "worker"
|
||||
|
||||
|
||||
class ContextBuildStrategy(str, Enum):
|
||||
DAY = "day"
|
||||
NUMBER = "number"
|
||||
|
||||
|
||||
class ContextMessagesConfig(BaseModel):
|
||||
mode: ContextBuildStrategy = ContextBuildStrategy.NUMBER
|
||||
count: int = Field(default=20, ge=1, le=200)
|
||||
|
||||
|
||||
class SystemAgentLLMConfig(BaseModel):
|
||||
temperature: float | None = Field(default=None, ge=0.0, le=2.0)
|
||||
max_tokens: int | None = Field(default=None, ge=1)
|
||||
timeout_seconds: float | None = Field(default=30.0, gt=0.0, le=300.0)
|
||||
context_messages: ContextMessagesConfig = Field(
|
||||
default_factory=ContextMessagesConfig
|
||||
)
|
||||
enabled_tools: list[str] = Field(default_factory=list, max_length=32)
|
||||
@@ -0,0 +1,349 @@
|
||||
"""
|
||||
UiHints - 描述性 UI 提示
|
||||
|
||||
设计原则:
|
||||
- 描述性而非渲染性: 告诉编译器“要展示什么”,而不是“如何渲染”
|
||||
- 最小化 token: 保持字段简洁
|
||||
- 可编译: 可机械转换为 UiSchemaRenderer
|
||||
- 尽量无损: hints 中的主要内容字段应尽量被保留到 renderer 中
|
||||
|
||||
Version: 2.1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
import re
|
||||
from typing import Any, ClassVar, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import field_validator
|
||||
|
||||
_NAVIGATION_PATH_PATTERN = re.compile(r"^/[A-Za-z0-9/_-]*$")
|
||||
_NAVIGATION_PARAM_KEY_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_]{0,31}$")
|
||||
_MAX_NAVIGATION_PARAMS = 8
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Enums
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiHintStatus(str, Enum):
|
||||
INFO = "info"
|
||||
SUCCESS = "success"
|
||||
WARNING = "warning"
|
||||
ERROR = "error"
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
class UiHintIntent(str, Enum):
|
||||
"""主要展示意图(弱提示,不应决定字段生死)"""
|
||||
|
||||
MESSAGE = "message" # 普通消息/说明
|
||||
DATA = "data" # 数据/结果摘要
|
||||
LIST = "list" # 列表为主
|
||||
STATUS = "status" # 状态结果为主
|
||||
FORM = "form" # 结构化内容(当前不表示真实输入表单)
|
||||
MIXED = "mixed" # 混合内容
|
||||
|
||||
|
||||
class UiHintActionStyle(str, Enum):
|
||||
PRIMARY = "primary"
|
||||
SECONDARY = "secondary"
|
||||
GHOST = "ghost"
|
||||
DANGER = "danger"
|
||||
|
||||
|
||||
class UiHintTextFormat(str, Enum):
|
||||
PLAIN = "plain"
|
||||
MARKDOWN = "markdown"
|
||||
|
||||
|
||||
class UiHintActionType(str, Enum):
|
||||
NAVIGATION = "navigation"
|
||||
URL = "url"
|
||||
EVENT = "event"
|
||||
TOOL = "tool"
|
||||
COPY = "copy"
|
||||
PAYLOAD = "payload"
|
||||
|
||||
|
||||
class UiHintIconSource(str, Enum):
|
||||
ICON = "icon"
|
||||
EMOJI = "emoji"
|
||||
URL = "url"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Base Config
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiHintBaseModel(BaseModel):
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(
|
||||
extra="forbid",
|
||||
populate_by_name=True,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Action Targets
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiHintActionNavigation(UiHintBaseModel):
|
||||
type: Literal["navigation"]
|
||||
path: str = Field(..., description="Internal route path.")
|
||||
params: dict[str, Any] | None = Field(default=None, description="Route params.")
|
||||
|
||||
@field_validator("path")
|
||||
@classmethod
|
||||
def validate_navigation_path(cls, value: str) -> str:
|
||||
path = value.strip()
|
||||
if not path:
|
||||
raise ValueError("navigation path must not be empty")
|
||||
if len(path) > 256:
|
||||
raise ValueError("navigation path is too long")
|
||||
if path.startswith("//") or "://" in path:
|
||||
raise ValueError("navigation path must be internal")
|
||||
if "?" in path or "#" in path:
|
||||
raise ValueError("navigation path must not contain query or fragment")
|
||||
if ":" in path:
|
||||
raise ValueError("navigation path must be concrete without placeholders")
|
||||
if _NAVIGATION_PATH_PATTERN.fullmatch(path) is None:
|
||||
raise ValueError("navigation path contains unsupported characters")
|
||||
return path
|
||||
|
||||
@field_validator("params")
|
||||
@classmethod
|
||||
def validate_navigation_params(
|
||||
cls, value: dict[str, Any] | None
|
||||
) -> dict[str, Any] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if len(value) > _MAX_NAVIGATION_PARAMS:
|
||||
raise ValueError("navigation params exceed limit")
|
||||
|
||||
normalized: dict[str, Any] = {}
|
||||
for key, param_value in value.items():
|
||||
if _NAVIGATION_PARAM_KEY_PATTERN.fullmatch(key) is None:
|
||||
raise ValueError("navigation param key is invalid")
|
||||
if isinstance(param_value, (str, int, float, bool)):
|
||||
normalized[key] = param_value
|
||||
continue
|
||||
raise ValueError("navigation params must be scalar")
|
||||
return normalized
|
||||
|
||||
|
||||
class UiHintActionUrl(UiHintBaseModel):
|
||||
type: Literal["url"]
|
||||
url: str = Field(..., description="External URL.")
|
||||
target: Literal["_self", "_blank"] | None = Field(default=None)
|
||||
|
||||
|
||||
class UiHintActionEvent(UiHintBaseModel):
|
||||
type: Literal["event"]
|
||||
event: str = Field(..., description="Frontend event name.")
|
||||
payload: dict[str, Any] | None = Field(default=None)
|
||||
|
||||
|
||||
class UiHintActionTool(UiHintBaseModel):
|
||||
type: Literal["tool"]
|
||||
tool_id: str = Field(alias="toolId", description="Tool identifier.")
|
||||
params: dict[str, Any] | None = Field(default=None)
|
||||
|
||||
|
||||
class UiHintActionCopy(UiHintBaseModel):
|
||||
type: Literal["copy"]
|
||||
content: str = Field(..., description="Content to copy.")
|
||||
success_message: str | None = Field(alias="successMessage", default=None)
|
||||
|
||||
|
||||
class UiHintActionPayload(UiHintBaseModel):
|
||||
type: Literal["payload"]
|
||||
payload: dict[str, Any] = Field(..., description="Structured payload.")
|
||||
submit_to: str | None = Field(alias="submitTo", default=None)
|
||||
|
||||
|
||||
UiHintActionTarget = (
|
||||
UiHintActionNavigation
|
||||
| UiHintActionUrl
|
||||
| UiHintActionEvent
|
||||
| UiHintActionTool
|
||||
| UiHintActionCopy
|
||||
| UiHintActionPayload
|
||||
)
|
||||
|
||||
|
||||
class UiHintAction(UiHintBaseModel):
|
||||
label: str = Field(..., description="Button label.")
|
||||
style: UiHintActionStyle | None = Field(default=None, description="Button style.")
|
||||
disabled: bool = Field(default=False, description="Disabled state.")
|
||||
action: UiHintActionTarget = Field(..., description="Action to execute.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Small Descriptive Models
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiHintIcon(UiHintBaseModel):
|
||||
source: UiHintIconSource = Field(default=UiHintIconSource.ICON)
|
||||
value: str = Field(..., description="Icon identifier / emoji / url.")
|
||||
color: str | None = Field(default=None)
|
||||
size: int | None = Field(default=None)
|
||||
|
||||
|
||||
class UiHintKvItem(UiHintBaseModel):
|
||||
key: str = Field(..., description="Key identifier.")
|
||||
label: str | None = Field(default=None, description="Display label.")
|
||||
value: Any = Field(default=None, description="Value.")
|
||||
copyable: bool = Field(default=False, description="Allow copy.")
|
||||
|
||||
|
||||
class UiHintListItem(UiHintBaseModel):
|
||||
id: str | None = Field(default=None)
|
||||
title: str = Field(..., description="Item title.")
|
||||
subtitle: str | None = Field(default=None)
|
||||
description: str | None = Field(default=None)
|
||||
icon: UiHintIcon | None = Field(default=None)
|
||||
status: UiHintStatus | None = Field(default=None)
|
||||
actions: list[UiHintAction] = Field(default_factory=list)
|
||||
|
||||
@field_validator("status", mode="before")
|
||||
@classmethod
|
||||
def normalize_status(cls, value: object) -> object:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
status_type = value.get("type")
|
||||
if isinstance(status_type, str):
|
||||
return status_type
|
||||
status_value = value.get("status")
|
||||
if isinstance(status_value, str):
|
||||
return status_value
|
||||
return value
|
||||
|
||||
|
||||
class UiHintSection(UiHintBaseModel):
|
||||
title: str | None = Field(default=None, description="Section title.")
|
||||
description: str | None = Field(default=None, description="Section description.")
|
||||
icon: UiHintIcon | None = Field(default=None, description="Section icon.")
|
||||
|
||||
content: str | None = Field(default=None, description="Main text content.")
|
||||
content_format: UiHintTextFormat = Field(
|
||||
default=UiHintTextFormat.PLAIN,
|
||||
alias="contentFormat",
|
||||
description="Section content text format.",
|
||||
)
|
||||
|
||||
items: list[UiHintKvItem] = Field(default_factory=list, description="KV items.")
|
||||
list_items: list[UiHintListItem] = Field(
|
||||
default_factory=list,
|
||||
alias="listItems",
|
||||
description="List items.",
|
||||
)
|
||||
actions: list[UiHintAction] = Field(default_factory=list, description="Actions.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Root Payload
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiHintsPayload(UiHintBaseModel):
|
||||
"""
|
||||
描述性 UI 提示
|
||||
|
||||
设计目标:
|
||||
- agent 输出尽可能短
|
||||
- 不表达布局细节
|
||||
- 编译器负责转换为完整 UiSchemaRenderer
|
||||
"""
|
||||
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(
|
||||
extra="forbid",
|
||||
populate_by_name=True,
|
||||
json_schema_extra={
|
||||
"examples": [
|
||||
{
|
||||
"intent": "status",
|
||||
"status": "success",
|
||||
"title": "日程已创建",
|
||||
"body": "本次创建已成功完成。",
|
||||
"items": [
|
||||
{"key": "title", "label": "主题", "value": "Q1 规划会议"},
|
||||
{"key": "time", "label": "时间", "value": "2026-03-15 14:00"},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"label": "查看详情",
|
||||
"style": "primary",
|
||||
"action": {
|
||||
"type": "navigation",
|
||||
"path": "/calendar/evt_123",
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "删除",
|
||||
"style": "danger",
|
||||
"action": {
|
||||
"type": "tool",
|
||||
"toolId": "calendar.delete",
|
||||
"params": {"eventId": "evt_123"},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
version: str = Field(default="2.1")
|
||||
|
||||
intent: UiHintIntent = Field(
|
||||
default=UiHintIntent.MESSAGE,
|
||||
description="Primary display intent.",
|
||||
)
|
||||
status: UiHintStatus = Field(
|
||||
default=UiHintStatus.INFO,
|
||||
description="Overall status.",
|
||||
)
|
||||
|
||||
title: str | None = Field(default=None, description="Top-level title.")
|
||||
description: str | None = Field(default=None, description="Top-level description.")
|
||||
|
||||
body: str | None = Field(default=None, description="Top-level main body text.")
|
||||
body_format: UiHintTextFormat = Field(
|
||||
default=UiHintTextFormat.PLAIN,
|
||||
alias="bodyFormat",
|
||||
description="Body text format.",
|
||||
)
|
||||
|
||||
items: list[UiHintKvItem] = Field(
|
||||
default_factory=list,
|
||||
description="Top-level key-value items.",
|
||||
)
|
||||
list_items: list[UiHintListItem] = Field(
|
||||
default_factory=list,
|
||||
alias="listItems",
|
||||
description="Top-level list items.",
|
||||
)
|
||||
sections: list[UiHintSection] = Field(
|
||||
default_factory=list,
|
||||
description="Grouped sections.",
|
||||
)
|
||||
actions: list[UiHintAction] = Field(
|
||||
default_factory=list,
|
||||
description="Top-level actions.",
|
||||
)
|
||||
|
||||
icon: UiHintIcon | None = Field(
|
||||
default=None,
|
||||
description="Top-level icon.",
|
||||
)
|
||||
meta: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Extra meta, e.g. requestId/toolId/traceId/userId.",
|
||||
)
|
||||
@@ -0,0 +1,628 @@
|
||||
"""
|
||||
UI Schema Renderer Protocol
|
||||
|
||||
目标:
|
||||
- 只保留“基础组件 + 布局容器”
|
||||
- 最终返回一个 UiSchemaRenderer
|
||||
- 前端只需要递归渲染 root 布局树即可
|
||||
|
||||
Version: 2.0
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Literal, TypedDict, Union
|
||||
|
||||
# ============================================================
|
||||
# Enums
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiStatus(str, Enum):
|
||||
INFO = "info"
|
||||
SUCCESS = "success"
|
||||
WARNING = "warning"
|
||||
ERROR = "error"
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
class IconSource(str, Enum):
|
||||
ICON = "icon"
|
||||
EMOJI = "emoji"
|
||||
URL = "url"
|
||||
|
||||
|
||||
class TextFormat(str, Enum):
|
||||
PLAIN = "plain"
|
||||
MARKDOWN = "markdown"
|
||||
|
||||
|
||||
class TextRole(str, Enum):
|
||||
TITLE = "title"
|
||||
SUBTITLE = "subtitle"
|
||||
BODY = "body"
|
||||
CAPTION = "caption"
|
||||
CODE = "code"
|
||||
|
||||
|
||||
class ButtonStyle(str, Enum):
|
||||
PRIMARY = "primary"
|
||||
SECONDARY = "secondary"
|
||||
GHOST = "ghost"
|
||||
DANGER = "danger"
|
||||
|
||||
|
||||
class LayoutDirection(str, Enum):
|
||||
VERTICAL = "vertical"
|
||||
HORIZONTAL = "horizontal"
|
||||
|
||||
|
||||
class LayoutAppearance(str, Enum):
|
||||
PLAIN = "plain"
|
||||
CARD = "card"
|
||||
SECTION = "section"
|
||||
|
||||
|
||||
class LayoutAlign(str, Enum):
|
||||
START = "start"
|
||||
CENTER = "center"
|
||||
END = "end"
|
||||
STRETCH = "stretch"
|
||||
|
||||
|
||||
class LayoutJustify(str, Enum):
|
||||
START = "start"
|
||||
CENTER = "center"
|
||||
END = "end"
|
||||
SPACE_BETWEEN = "space-between"
|
||||
|
||||
|
||||
class RendererTheme(str, Enum):
|
||||
DEFAULT = "default"
|
||||
LIGHT = "light"
|
||||
DARK = "dark"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Meta
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiMeta(TypedDict, total=False):
|
||||
requestId: str
|
||||
toolId: str
|
||||
traceId: str
|
||||
userId: str
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Action Payloads
|
||||
# ============================================================
|
||||
|
||||
|
||||
class NavigateAction(TypedDict, total=False):
|
||||
type: Literal["navigation"]
|
||||
path: str
|
||||
params: dict[str, Any]
|
||||
|
||||
|
||||
class UrlAction(TypedDict, total=False):
|
||||
type: Literal["url"]
|
||||
url: str
|
||||
target: Literal["_self", "_blank"]
|
||||
|
||||
|
||||
class EventAction(TypedDict, total=False):
|
||||
type: Literal["event"]
|
||||
event: str
|
||||
payload: dict[str, Any]
|
||||
|
||||
|
||||
class ToolAction(TypedDict, total=False):
|
||||
type: Literal["tool"]
|
||||
toolId: str
|
||||
params: dict[str, Any]
|
||||
|
||||
|
||||
class CopyAction(TypedDict, total=False):
|
||||
type: Literal["copy"]
|
||||
content: str
|
||||
successMessage: str
|
||||
|
||||
|
||||
class PayloadAction(TypedDict, total=False):
|
||||
type: Literal["payload"]
|
||||
payload: dict[str, Any]
|
||||
submitTo: str
|
||||
|
||||
|
||||
UiActionPayload = Union[
|
||||
NavigateAction,
|
||||
UrlAction,
|
||||
EventAction,
|
||||
ToolAction,
|
||||
CopyAction,
|
||||
PayloadAction,
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Shared Small Types
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiIconSpec(TypedDict, total=False):
|
||||
source: str
|
||||
value: str
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
||||
class UiKvItem(TypedDict, total=False):
|
||||
key: str
|
||||
label: str
|
||||
value: Any
|
||||
copyable: bool
|
||||
|
||||
|
||||
class UiBaseNode(TypedDict, total=False):
|
||||
id: str
|
||||
visible: bool
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Primitive Components
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiTextNode(UiBaseNode, total=False):
|
||||
type: Literal["text"]
|
||||
content: str
|
||||
format: str # TextFormat
|
||||
role: str # TextRole
|
||||
status: str # UiStatus
|
||||
maxLines: int
|
||||
|
||||
|
||||
class UiIconNode(UiBaseNode, total=False):
|
||||
type: Literal["icon"]
|
||||
source: str # IconSource
|
||||
value: str
|
||||
color: str
|
||||
size: int
|
||||
|
||||
|
||||
class UiBadgeNode(UiBaseNode, total=False):
|
||||
type: Literal["badge"]
|
||||
label: str
|
||||
status: str # UiStatus
|
||||
|
||||
|
||||
class UiButtonNode(UiBaseNode, total=False):
|
||||
type: Literal["button"]
|
||||
label: str
|
||||
style: str # ButtonStyle
|
||||
disabled: bool
|
||||
icon: UiIconSpec
|
||||
action: UiActionPayload
|
||||
|
||||
|
||||
class UiKvNode(UiBaseNode, total=False):
|
||||
type: Literal["kv"]
|
||||
items: list[UiKvItem]
|
||||
columns: int
|
||||
|
||||
|
||||
class UiDividerNode(UiBaseNode, total=False):
|
||||
type: Literal["divider"]
|
||||
inset: int
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Layout Containers
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiStackNode(UiBaseNode, total=False):
|
||||
type: Literal["stack"]
|
||||
direction: str # LayoutDirection
|
||||
gap: int
|
||||
appearance: str # LayoutAppearance
|
||||
status: str # UiStatus
|
||||
align: str # LayoutAlign
|
||||
justify: str # LayoutJustify
|
||||
wrap: bool
|
||||
children: list["UiNode"]
|
||||
|
||||
|
||||
class UiGridNode(UiBaseNode, total=False):
|
||||
type: Literal["grid"]
|
||||
columns: int
|
||||
gap: int
|
||||
appearance: str # LayoutAppearance
|
||||
status: str # UiStatus
|
||||
children: list["UiNode"]
|
||||
|
||||
|
||||
UiNode = Union[
|
||||
UiTextNode,
|
||||
UiIconNode,
|
||||
UiBadgeNode,
|
||||
UiButtonNode,
|
||||
UiKvNode,
|
||||
UiDividerNode,
|
||||
UiStackNode,
|
||||
UiGridNode,
|
||||
]
|
||||
|
||||
UiLayoutNode = Union[UiStackNode, UiGridNode]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Root Renderer
|
||||
# ============================================================
|
||||
|
||||
|
||||
class UiSchemaRenderer(TypedDict, total=False):
|
||||
version: str
|
||||
locale: str
|
||||
status: str # UiStatus
|
||||
theme: str # RendererTheme
|
||||
meta: UiMeta
|
||||
root: UiLayoutNode
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Root Builder
|
||||
# ============================================================
|
||||
|
||||
|
||||
def build_renderer(
|
||||
root: UiLayoutNode,
|
||||
*,
|
||||
version: str = "2.0",
|
||||
locale: str = "zh-CN",
|
||||
status: UiStatus = UiStatus.INFO,
|
||||
theme: RendererTheme = RendererTheme.DEFAULT,
|
||||
meta: UiMeta | None = None,
|
||||
) -> UiSchemaRenderer:
|
||||
renderer: UiSchemaRenderer = {
|
||||
"version": version,
|
||||
"locale": locale,
|
||||
"status": status.value,
|
||||
"theme": theme.value,
|
||||
"root": root,
|
||||
}
|
||||
if meta:
|
||||
renderer["meta"] = meta
|
||||
return renderer
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Primitive Builders
|
||||
# ============================================================
|
||||
|
||||
|
||||
def build_text(
|
||||
content: str,
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
format: TextFormat = TextFormat.PLAIN,
|
||||
role: TextRole = TextRole.BODY,
|
||||
status: UiStatus | None = None,
|
||||
max_lines: int | None = None,
|
||||
visible: bool = True,
|
||||
) -> UiTextNode:
|
||||
node: UiTextNode = {
|
||||
"type": "text",
|
||||
"content": content,
|
||||
"format": format.value,
|
||||
"role": role.value,
|
||||
"visible": visible,
|
||||
}
|
||||
if node_id:
|
||||
node["id"] = node_id
|
||||
if status:
|
||||
node["status"] = status.value
|
||||
if max_lines is not None:
|
||||
node["maxLines"] = max_lines
|
||||
return node
|
||||
|
||||
|
||||
def build_icon(
|
||||
source: IconSource,
|
||||
value: str,
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
color: str | None = None,
|
||||
size: int | None = None,
|
||||
visible: bool = True,
|
||||
) -> UiIconNode:
|
||||
node: UiIconNode = {
|
||||
"type": "icon",
|
||||
"source": source.value,
|
||||
"value": value,
|
||||
"visible": visible,
|
||||
}
|
||||
if node_id:
|
||||
node["id"] = node_id
|
||||
if color:
|
||||
node["color"] = color
|
||||
if size is not None:
|
||||
node["size"] = size
|
||||
return node
|
||||
|
||||
|
||||
def build_badge(
|
||||
label: str,
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
status: UiStatus = UiStatus.INFO,
|
||||
visible: bool = True,
|
||||
) -> UiBadgeNode:
|
||||
node: UiBadgeNode = {
|
||||
"type": "badge",
|
||||
"label": label,
|
||||
"status": status.value,
|
||||
"visible": visible,
|
||||
}
|
||||
if node_id:
|
||||
node["id"] = node_id
|
||||
return node
|
||||
|
||||
|
||||
def build_button(
|
||||
label: str,
|
||||
action: UiActionPayload,
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
style: ButtonStyle = ButtonStyle.PRIMARY,
|
||||
disabled: bool = False,
|
||||
icon: UiIconSpec | None = None,
|
||||
visible: bool = True,
|
||||
) -> UiButtonNode:
|
||||
node: UiButtonNode = {
|
||||
"type": "button",
|
||||
"label": label,
|
||||
"style": style.value,
|
||||
"disabled": disabled,
|
||||
"action": action,
|
||||
"visible": visible,
|
||||
}
|
||||
if node_id:
|
||||
node["id"] = node_id
|
||||
if icon:
|
||||
node["icon"] = icon
|
||||
return node
|
||||
|
||||
|
||||
def build_kv(
|
||||
items: list[UiKvItem],
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
columns: int = 1,
|
||||
visible: bool = True,
|
||||
) -> UiKvNode:
|
||||
node: UiKvNode = {
|
||||
"type": "kv",
|
||||
"items": items,
|
||||
"columns": columns,
|
||||
"visible": visible,
|
||||
}
|
||||
if node_id:
|
||||
node["id"] = node_id
|
||||
return node
|
||||
|
||||
|
||||
def build_divider(
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
inset: int = 0,
|
||||
visible: bool = True,
|
||||
) -> UiDividerNode:
|
||||
node: UiDividerNode = {
|
||||
"type": "divider",
|
||||
"inset": inset,
|
||||
"visible": visible,
|
||||
}
|
||||
if node_id:
|
||||
node["id"] = node_id
|
||||
return node
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Layout Builders
|
||||
# ============================================================
|
||||
|
||||
|
||||
def build_stack(
|
||||
children: list[UiNode],
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
direction: LayoutDirection = LayoutDirection.VERTICAL,
|
||||
gap: int = 12,
|
||||
appearance: LayoutAppearance = LayoutAppearance.PLAIN,
|
||||
status: UiStatus | None = None,
|
||||
align: LayoutAlign = LayoutAlign.START,
|
||||
justify: LayoutJustify = LayoutJustify.START,
|
||||
wrap: bool = False,
|
||||
visible: bool = True,
|
||||
) -> UiStackNode:
|
||||
node: UiStackNode = {
|
||||
"type": "stack",
|
||||
"direction": direction.value,
|
||||
"gap": gap,
|
||||
"appearance": appearance.value,
|
||||
"align": align.value,
|
||||
"justify": justify.value,
|
||||
"wrap": wrap,
|
||||
"children": children,
|
||||
"visible": visible,
|
||||
}
|
||||
if node_id:
|
||||
node["id"] = node_id
|
||||
if status:
|
||||
node["status"] = status.value
|
||||
return node
|
||||
|
||||
|
||||
def build_grid(
|
||||
children: list[UiNode],
|
||||
*,
|
||||
columns: int,
|
||||
node_id: str | None = None,
|
||||
gap: int = 12,
|
||||
appearance: LayoutAppearance = LayoutAppearance.PLAIN,
|
||||
status: UiStatus | None = None,
|
||||
visible: bool = True,
|
||||
) -> UiGridNode:
|
||||
node: UiGridNode = {
|
||||
"type": "grid",
|
||||
"columns": columns,
|
||||
"gap": gap,
|
||||
"appearance": appearance.value,
|
||||
"children": children,
|
||||
"visible": visible,
|
||||
}
|
||||
if node_id:
|
||||
node["id"] = node_id
|
||||
if status:
|
||||
node["status"] = status.value
|
||||
return node
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Small Action Builders
|
||||
# ============================================================
|
||||
|
||||
|
||||
def action_navigation(
|
||||
path: str, params: dict[str, Any] | None = None
|
||||
) -> NavigateAction:
|
||||
action: NavigateAction = {"type": "navigation", "path": path}
|
||||
if params:
|
||||
action["params"] = params
|
||||
return action
|
||||
|
||||
|
||||
def action_url(url: str, target: Literal["_self", "_blank"] = "_blank") -> UrlAction:
|
||||
return {"type": "url", "url": url, "target": target}
|
||||
|
||||
|
||||
def action_event(event: str, payload: dict[str, Any] | None = None) -> EventAction:
|
||||
action: EventAction = {"type": "event", "event": event}
|
||||
if payload:
|
||||
action["payload"] = payload
|
||||
return action
|
||||
|
||||
|
||||
def action_tool(tool_id: str, params: dict[str, Any] | None = None) -> ToolAction:
|
||||
action: ToolAction = {"type": "tool", "toolId": tool_id}
|
||||
if params:
|
||||
action["params"] = params
|
||||
return action
|
||||
|
||||
|
||||
def action_copy(content: str, success_message: str | None = None) -> CopyAction:
|
||||
action: CopyAction = {"type": "copy", "content": content}
|
||||
if success_message:
|
||||
action["successMessage"] = success_message
|
||||
return action
|
||||
|
||||
|
||||
def action_payload(
|
||||
payload: dict[str, Any], submit_to: str | None = None
|
||||
) -> PayloadAction:
|
||||
action: PayloadAction = {"type": "payload", "payload": payload}
|
||||
if submit_to:
|
||||
action["submitTo"] = submit_to
|
||||
return action
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Derived Helpers (协议外的便捷封装,不是基础原语)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def build_card(
|
||||
children: list[UiNode],
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
gap: int = 12,
|
||||
status: UiStatus | None = None,
|
||||
) -> UiStackNode:
|
||||
return build_stack(
|
||||
children,
|
||||
node_id=node_id,
|
||||
direction=LayoutDirection.VERTICAL,
|
||||
gap=gap,
|
||||
appearance=LayoutAppearance.CARD,
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
def build_section(
|
||||
title: str,
|
||||
children: list[UiNode],
|
||||
*,
|
||||
node_id: str | None = None,
|
||||
description: str | None = None,
|
||||
status: UiStatus | None = None,
|
||||
gap: int = 12,
|
||||
) -> UiStackNode:
|
||||
header_nodes: list[UiNode] = [build_text(title, role=TextRole.TITLE)]
|
||||
if description:
|
||||
header_nodes.append(build_text(description, role=TextRole.CAPTION))
|
||||
|
||||
all_children = header_nodes + children
|
||||
return build_stack(
|
||||
all_children,
|
||||
node_id=node_id,
|
||||
direction=LayoutDirection.VERTICAL,
|
||||
gap=gap,
|
||||
appearance=LayoutAppearance.SECTION,
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
def build_status_panel(
|
||||
title: str,
|
||||
message: str,
|
||||
*,
|
||||
status: UiStatus,
|
||||
primary_button: UiButtonNode | None = None,
|
||||
secondary_button: UiButtonNode | None = None,
|
||||
node_id: str | None = None,
|
||||
) -> UiStackNode:
|
||||
status_label = f"ui.status.{status.value}"
|
||||
children: list[UiNode] = [
|
||||
build_stack(
|
||||
[
|
||||
build_text(title, role=TextRole.TITLE),
|
||||
build_badge(label=status_label, status=status),
|
||||
],
|
||||
direction=LayoutDirection.HORIZONTAL,
|
||||
gap=8,
|
||||
align=LayoutAlign.CENTER,
|
||||
justify=LayoutJustify.SPACE_BETWEEN,
|
||||
),
|
||||
build_text(message, role=TextRole.BODY, status=status),
|
||||
]
|
||||
|
||||
actions: list[UiNode] = []
|
||||
if primary_button:
|
||||
actions.append(primary_button)
|
||||
if secondary_button:
|
||||
actions.append(secondary_button)
|
||||
|
||||
if actions:
|
||||
children.append(
|
||||
build_stack(
|
||||
actions,
|
||||
direction=LayoutDirection.HORIZONTAL,
|
||||
gap=8,
|
||||
)
|
||||
)
|
||||
|
||||
return build_card(children, node_id=node_id, status=status)
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class SystemVisibilityBit(IntEnum):
|
||||
UI_HISTORY = 0
|
||||
CONTEXT_ASSEMBLY = 1
|
||||
|
||||
|
||||
class VisibilityMask(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
value: int = Field(..., ge=0, le=(1 << 63) - 1)
|
||||
|
||||
@classmethod
|
||||
def from_bits(cls, *, bits: list[int]) -> "VisibilityMask":
|
||||
mask = 0
|
||||
for bit in bits:
|
||||
validate_visibility_bit(bit=bit)
|
||||
mask |= 1 << bit
|
||||
return cls(value=mask)
|
||||
|
||||
def contains(self, *, bit: int) -> bool:
|
||||
validate_visibility_bit(bit=bit)
|
||||
return bool(self.value & (1 << bit))
|
||||
|
||||
|
||||
class VisibilityBitRef(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
bit: int = Field(..., ge=0, le=63)
|
||||
|
||||
@field_validator("bit")
|
||||
@classmethod
|
||||
def _validate_bit(cls, value: int) -> int:
|
||||
validate_visibility_bit(bit=value)
|
||||
return value
|
||||
|
||||
|
||||
def validate_visibility_bit(*, bit: int) -> None:
|
||||
if bit < 0 or bit > 63:
|
||||
raise ValueError("visibility bit must be in range [0, 63]")
|
||||
|
||||
|
||||
def bit_mask(*, bit: int) -> int:
|
||||
validate_visibility_bit(bit=bit)
|
||||
return 1 << bit
|
||||
Reference in New Issue
Block a user