Download src/agentcade/protocol.py from left0ver/WatcherRobot-com.left0ver.agentcade: direct link, hf CLI and curl.
- Browser
- Download file 5.89 kB
-
https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/protocol.py
- Command line
-
hf download hf://spaces/left0ver/WatcherRobot-com.left0ver.agentcade/src/agentcade/protocol.py
-
curl -L -o protocol.py https://huggingface.co/spaces/left0ver/WatcherRobot-com.left0ver.agentcade/resolve/main/src/agentcade/protocol.py
5.89 kB
| from typing import Literal, TypeVar, get_args | |
| from a2a.helpers import new_data_part | |
| from a2a.types import Part | |
| from google.protobuf.json_format import MessageToDict | |
| from pydantic import BaseModel, ConfigDict, Field, JsonValue, field_validator, model_validator | |
| # 主 Agent 和游戏之间的通信协议,写成“名称/版本”。只有不兼容的改动才升版本,见游戏开发指南的“协议版本”一节。 | |
| # Version 是主 Agent 支持的全部版本,新的在前;游戏的名片里写它支持的版本,主 Agent 用双方都支持的最新版本。 | |
| Version = Literal["agentcade/1"] | |
| SUPPORTED_PROTOCOLS: tuple[str, ...] = get_args(Version) | |
| PROTOCOL_VERSION = SUPPORTED_PROTOCOLS[0] | |
| # 出了新版本以后,旧版本至少再支持三个月:版本 → 停止支持的日期,check 会提醒还在用它的游戏。 | |
| RETIRING: dict[str, str] = {} | |
| class Model(BaseModel): | |
| model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) | |
| class Wire(BaseModel): | |
| """主 Agent 和游戏之间传的消息。不认识的字段放进 model_extra 后忽略,所以加可选字段不用升协议版本。""" | |
| model_config = ConfigDict(extra="allow", str_strip_whitespace=True) | |
| class GameDescriptor(Model): | |
| id: str = Field(pattern=r"^[a-z][a-z0-9_]{0,63}$") | |
| name: str = Field(min_length=1) | |
| description: str = Field(min_length=1) | |
| # 开机问候里跟在游戏名后面念的一句简短介绍,不带句末标点。 | |
| summary: str = Field(min_length=1, max_length=30, pattern=r"[^。!?.!?,,;;]$") | |
| aliases: list[str] = Field(default_factory=list) | |
| required_capabilities: list[str] = Field(default_factory=list) | |
| # 选填,写进名片,用户安装远程游戏时在确认页上看到。 | |
| version: str = Field(default="1.0.0", min_length=1, max_length=32) | |
| author: str = Field(default="", max_length=64) | |
| homepage: str = Field(default="", max_length=512) | |
| def catalog(self) -> dict: | |
| """给主 Agent 看的游戏目录条目,只包含选游戏和介绍玩法用得到的内容。""" | |
| return self.model_dump(include={"id", "name", "description", "summary", "aliases"}) | |
| def integral(value): | |
| """A2A 的结构化数据里数字都是浮点数,整数值还原成 int,SDK 要整数的参数才能通过。""" | |
| if isinstance(value, float) and value.is_integer(): | |
| return int(value) | |
| if isinstance(value, list): | |
| return [integral(item) for item in value] | |
| if isinstance(value, dict): | |
| return {key: integral(item) for key, item in value.items()} | |
| return value | |
| class SdkCall(Wire): | |
| """一次机器人 SDK 调用,例如 lights.set_color("#00FF00", brightness=0.8)。 | |
| call 是 SDK 里的“模块.方法”,参数和 SDK 一样;主 Agent 检查游戏能不能调用、有没有声明能力后, | |
| 在机器人上执行同名方法。 | |
| """ | |
| call: str = Field(pattern=r"^[a-z_]+\.[a-z_]+$", description="SDK 里的模块.方法,例如 lights.set_color") | |
| args: list[JsonValue] = Field(default_factory=list) | |
| kwargs: dict[str, JsonValue] = Field(default_factory=dict) | |
| def restore_integers(cls, value): | |
| return integral(value) | |
| class Presentation(Model): | |
| """主 Agent 的一条输出:一句话、一次游戏的 SDK 调用,或者一局结束后复位设备。""" | |
| text: str = Field(default="", max_length=500) | |
| call: SdkCall | None = None | |
| reset: bool = False | |
| class GameInput(Wire): | |
| """主 Agent 发给游戏 Agent 的一条用户输入。""" | |
| model_config = ConfigDict(extra="allow", str_strip_whitespace=False) | |
| protocol: Version = PROTOCOL_VERSION | |
| kind: Literal["start", "message", "end"] | |
| text: str = Field(default="", description="用户原话,保持字词和标点") | |
| reason: Literal["user", "shutdown"] | None = Field( | |
| default=None, description="end 的原因:user 是用户要求结束,shutdown 是程序退出", | |
| ) | |
| def validate_text(self): | |
| # 程序退出时发送的 end 没有用户原话。 | |
| if self.kind != "end" and not self.text.strip(): | |
| raise ValueError("开局和局内输入需要用户原话") | |
| if (self.kind == "end") != (self.reason is not None): | |
| raise ValueError("只有 end 带有结束原因,并且必须带") | |
| return self | |
| class GameReply(Wire): | |
| """游戏 Agent 回给主 Agent 的一条回复:要播报的话和设备指令,一次请求至少回复一条。 | |
| 设备指令是机器人 SDK 调用。中间回复可以只有设备指令;最后一条回复必须有要播报的话。 | |
| 主 Agent 先执行设备指令,再播报。 | |
| """ | |
| protocol: Version = PROTOCOL_VERSION | |
| text: str = Field(default="", max_length=500, description="主 Agent 原样播报的面向用户回复") | |
| directives: list[SdkCall] = Field( | |
| default_factory=list, max_length=16, description="按顺序执行的机器人 SDK 调用", | |
| ) | |
| session: Literal["active", "ended"] = Field( | |
| default="active", description="本局是否继续,只有一次请求的最后一条回复可以是 ended", | |
| ) | |
| def validate_content(self): | |
| if not self.text and not self.directives: | |
| raise ValueError("回复至少要有要播报的话或设备指令") | |
| return self | |
| Payload = TypeVar("Payload", GameInput, GameReply) | |
| def to_part(payload: GameInput | GameReply) -> Part: | |
| return new_data_part(payload.model_dump(mode="json")) | |
| def from_parts(model: type[Payload], parts) -> Payload: | |
| if len(parts) != 1 or not parts[0].HasField("data"): | |
| raise ValueError("游戏消息必须包含唯一一个结构化数据 Part") | |
| return model.model_validate(MessageToDict(parts[0].data)) | |