Coverage for agentos/protocols/agent_card.py: 42%
120 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 09:19 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 09:19 +0800
1"""
2AgentOS v1.2.0 — Agent Card 服务发现协议。
4基因来源: Google A2A (Agent-to-Agent) Agent Card 规范
6Agent Card 是标准化的 Agent 自描述卡片,支持:
7- 发布/发现: Agent 发布自身能力,其他 Agent 按需发现
8- 能力匹配: 按 domain / capability / keyword 搜索匹配
9- 本地+远程: 文件系统本地发现 + HTTP 端点远程发现
10- JSON 序列化: 完整的 export/import 往返,兼容 A2A 生态
11"""
13from __future__ import annotations
15import json
16from dataclasses import asdict, dataclass, field
17from typing import Any
19# ── AgentCard ───────────────────────────────────
22@dataclass
23class AgentCard:
24 """Agent 自描述卡片,A2A 兼容。
26 使用方式:
27 card = AgentCard(
28 name="data-analyzer",
29 description="数据分析Agent,支持SQL/Pandas/可视化",
30 version="1.0.0",
31 url="http://localhost:8000/agent",
32 capabilities=["analysis", "coding"],
33 skills=["sql-query", "pandas-transform", "chart-generate"],
34 input_schema={"type": "object", "properties": {"query": {"type": "string"}}},
35 output_schema={"type": "object", "properties": {"result": {"type": "string"}}},
36 )
37 """
39 name: str
40 description: str
41 version: str
42 url: str = ""
43 capabilities: list[str] = field(default_factory=list)
44 skills: list[str] = field(default_factory=list)
45 input_schema: dict[str, Any] = field(default_factory=dict)
46 output_schema: dict[str, Any] = field(default_factory=dict)
47 provider: str = ""
48 metadata: dict[str, Any] = field(default_factory=dict)
49 tags: list[str] = field(default_factory=list)
51 def to_dict(self) -> dict[str, Any]:
52 """导出为字典,保留所有字段(含空值)。"""
53 return asdict(self)
55 def to_json(self, indent: int = 2) -> str:
56 """导出为 JSON 字符串。"""
57 return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
59 @classmethod
60 def from_dict(cls, data: dict[str, Any]) -> AgentCard:
61 """从字典重建 AgentCard。"""
62 return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
64 @classmethod
65 def from_json(cls, json_str: str) -> AgentCard:
66 """从 JSON 字符串重建。"""
67 return cls.from_dict(json.loads(json_str))
69 def matches_query(self, query: str) -> bool:
70 """模糊匹配:检查 query 是否命中 name/description/skills/tags。"""
71 q = query.lower()
72 if q in self.name.lower():
73 return True
74 if q in self.description.lower():
75 return True
76 for skill in self.skills:
77 if q in skill.lower():
78 return True
79 for tag in self.tags:
80 if q in tag.lower():
81 return True
82 return False
84 def has_capability(self, capability: str) -> bool:
85 return capability in self.capabilities
87 def has_skill(self, skill: str) -> bool:
88 return skill in self.skills
90 def has_tag(self, tag: str) -> bool:
91 return tag in self.tags
94# ── AgentCardRegistry ───────────────────────────
97@dataclass
98class AgentCardRegistry:
99 """Agent Card 注册中心。
101 支持注册、注销、搜索、过滤。
102 """
104 cards: dict[str, AgentCard] = field(default_factory=dict)
106 def register(self, card: AgentCard) -> None:
107 """注册一张 Agent Card(同名覆盖)。"""
108 self.cards[card.name] = card
110 def unregister(self, name: str) -> AgentCard | None:
111 """注销并返回被移除的卡片,不存在返回 None。"""
112 return self.cards.pop(name, None)
114 def get(self, name: str) -> AgentCard | None:
115 """按名称查找。"""
116 return self.cards.get(name)
118 def list_all(self) -> list[AgentCard]:
119 """列出所有注册的卡片。"""
120 return list(self.cards.values())
122 def find_by_query(self, query: str) -> list[AgentCard]:
123 """按关键词搜索(匹配 name/description/skills/tags)。"""
124 return [c for c in self.cards.values() if c.matches_query(query)]
126 def find_by_capability(self, capability: str) -> list[AgentCard]:
127 """按能力关键词查找。"""
128 return [c for c in self.cards.values() if c.has_capability(capability)]
130 def find_by_skill(self, skill: str) -> list[AgentCard]:
131 """按技能关键词查找。"""
132 return [c for c in self.cards.values() if c.has_skill(skill)]
134 def find_by_tag(self, tag: str) -> list[AgentCard]:
135 """按标签查找。"""
136 return [c for c in self.cards.values() if c.has_tag(tag)]
138 def export_all(self, filepath: str) -> None:
139 """将所有卡片导出到 JSON 文件。"""
140 data = {name: card.to_dict() for name, card in self.cards.items()}
141 with open(filepath, "w", encoding="utf-8") as f:
142 json.dump(data, f, ensure_ascii=False, indent=2)
144 def import_from_file(self, filepath: str) -> int:
145 """从 JSON 文件导入卡片到注册中心,返回导入数量。"""
146 with open(filepath, encoding="utf-8") as f:
147 data = json.load(f)
148 count = 0
149 for name, card_data in data.items():
150 self.cards[name] = AgentCard.from_dict(card_data)
151 count += 1
152 return count
154 @classmethod
155 def from_file(cls, filepath: str) -> AgentCardRegistry:
156 """从 JSON 文件创建注册中心。"""
157 reg = cls()
158 reg.import_from_file(filepath)
159 return reg
161 def __len__(self) -> int:
162 return len(self.cards)
164 def __contains__(self, name: str) -> bool:
165 return name in self.cards
168# ── AgentCardDiscovery (远程发现) ───────────────
171class AgentCardDiscovery:
172 """Agent Card 远程发现器。
174 通过 HTTP GET 获取远程 Agent 的 /agent-card 端点。
175 """
177 @staticmethod
178 async def fetch(url: str, timeout: float = 10.0) -> AgentCard | None:
179 """从远程 URL 获取 AgentCard JSON 并解析。
181 默认期望端点返回 {"name":..., "description":..., ...}
183 Args:
184 url: Agent Card 端点 URL(如 http://host:8000/agent-card)
185 timeout: 请求超时(秒)
187 Returns:
188 AgentCard 实例,失败返回 None
189 """
190 try:
191 import httpx
193 async with httpx.AsyncClient(timeout=timeout) as client:
194 resp = await client.get(url)
195 resp.raise_for_status()
196 return AgentCard.from_json(resp.text)
197 except Exception:
198 return None
200 @staticmethod
201 async def fetch_all(urls: list[str], timeout: float = 10.0) -> dict[str, AgentCard | None]:
202 """并发获取多个 Agent Card。
204 Args:
205 urls: Agent Card 端点 URL 列表
206 timeout: 单个请求超时(秒)
208 Returns:
209 {url: AgentCard 或 None} 字典
210 """
211 import asyncio
213 results = await asyncio.gather(
214 *(AgentCardDiscovery.fetch(url, timeout) for url in urls),
215 return_exceptions=True,
216 )
217 return {url: (None if isinstance(r, Exception) else r) for url, r in zip(urls, results)}
220# ── 便捷函数 ───────────────────────────────────
223def create_card(
224 name: str,
225 description: str,
226 version: str = "1.0.0",
227 url: str = "",
228 capabilities: list[str] | None = None,
229 skills: list[str] | None = None,
230 **metadata,
231) -> AgentCard:
232 """快速创建 AgentCard 的便捷函数。"""
233 return AgentCard(
234 name=name,
235 description=description,
236 version=version,
237 url=url,
238 capabilities=capabilities or [],
239 skills=skills or [],
240 metadata=metadata,
241 )
244def discover_local(directory: str, pattern: str = "agent-card*.json") -> list[AgentCard]:
245 """从本地目录发现 AgentCard JSON 文件。
247 Args:
248 directory: 扫描目录
249 pattern: 文件名 glob pattern(仅支持简单前缀/后缀匹配)
251 Returns:
252 发现的 AgentCard 列表
253 """
254 import fnmatch
255 import os
257 cards: list[AgentCard] = []
258 try:
259 for fname in os.listdir(directory):
260 if fnmatch.fnmatch(fname, pattern):
261 fpath = os.path.join(directory, fname)
262 try:
263 with open(fpath, encoding="utf-8") as f:
264 cards.append(AgentCard.from_json(f.read()))
265 except Exception:
266 continue
267 except FileNotFoundError:
268 pass
269 return cards