nexus-a2a 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nexus_a2a-0.1.0/.gitignore +29 -0
- nexus_a2a-0.1.0/.python-version +1 -0
- nexus_a2a-0.1.0/PKG-INFO +56 -0
- nexus_a2a-0.1.0/README.md +23 -0
- nexus_a2a-0.1.0/main.py +6 -0
- nexus_a2a-0.1.0/nexus_a2a/__init__.py +60 -0
- nexus_a2a-0.1.0/nexus_a2a/adapters/__init__.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/adapters/base.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/adapters/crewai.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/adapters/langgraph.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/core/__init__.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/core/orchestrator.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/core/registry.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/core/task_manager.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/decorators.py +210 -0
- nexus_a2a-0.1.0/nexus_a2a/models/__init__.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/models/agent.py +192 -0
- nexus_a2a-0.1.0/nexus_a2a/models/task.py +292 -0
- nexus_a2a-0.1.0/nexus_a2a/security/__init__.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/security/auth.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/security/rate_limiter.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/security/trust.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/security/validator.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/storage/__init__.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/storage/audit_logger.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/storage/metrics.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/storage/redis_store.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/storage/task_store.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/transport/__init__.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/transport/http_client.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/transport/sse.py +0 -0
- nexus_a2a-0.1.0/nexus_a2a/transport/webhook.py +0 -0
- nexus_a2a-0.1.0/pyproject.toml +90 -0
- nexus_a2a-0.1.0/tests/__init__.py +0 -0
- nexus_a2a-0.1.0/tests/test_decorator.py +210 -0
- nexus_a2a-0.1.0/tests/test_models.py +226 -0
- nexus_a2a-0.1.0/uv.lock +427 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
.Python
|
|
6
|
+
|
|
7
|
+
# Virtual environment (uv creates this)
|
|
8
|
+
.venv/
|
|
9
|
+
|
|
10
|
+
# Build artifacts
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
|
|
15
|
+
# Type checking cache
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
|
|
19
|
+
# Test coverage
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# IDE
|
|
24
|
+
.vscode/settings.json
|
|
25
|
+
.idea/
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
nexus_a2a-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexus-a2a
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A developer-friendly Python package for building AI agent-to-agent (A2A) communication with ease.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yourusername/nexus-a2a
|
|
6
|
+
Project-URL: Documentation, https://github.com/yourusername/nexus-a2a#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/yourusername/nexus-a2a/issues
|
|
8
|
+
Author-email: Your Name <you@example.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: a2a,agent,ai,llm,multi-agent,protocol
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Requires-Dist: asyncpg>=0.29; extra == 'all'
|
|
21
|
+
Requires-Dist: redis>=5.0; extra == 'all'
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
24
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Provides-Extra: postgres
|
|
29
|
+
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
|
|
30
|
+
Provides-Extra: redis
|
|
31
|
+
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# nexus-a2a
|
|
35
|
+
|
|
36
|
+
A developer-friendly Python package for building AI agent-to-agent (A2A) communication with ease.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install nexus-a2a
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from nexus_a2a import agent, AgentNetwork
|
|
48
|
+
|
|
49
|
+
@agent(name="researcher", skills=["search"])
|
|
50
|
+
class ResearchAgent:
|
|
51
|
+
async def run(self, task):
|
|
52
|
+
return f"Result for: {task}"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Status
|
|
56
|
+
🚧 Active development — v0.1.0 alpha
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# nexus-a2a
|
|
2
|
+
|
|
3
|
+
A developer-friendly Python package for building AI agent-to-agent (A2A) communication with ease.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install nexus-a2a
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from nexus_a2a import agent, AgentNetwork
|
|
15
|
+
|
|
16
|
+
@agent(name="researcher", skills=["search"])
|
|
17
|
+
class ResearchAgent:
|
|
18
|
+
async def run(self, task):
|
|
19
|
+
return f"Result for: {task}"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Status
|
|
23
|
+
🚧 Active development — v0.1.0 alpha
|
nexus_a2a-0.1.0/main.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nexus_a2a — Developer-friendly A2A multi-agent communication for Python.
|
|
3
|
+
|
|
4
|
+
Public API for Phase 1. Import everything you need from here:
|
|
5
|
+
|
|
6
|
+
from nexus_a2a import agent, get_card
|
|
7
|
+
from nexus_a2a import AgentCard, AgentSkill, AgentCapabilities
|
|
8
|
+
from nexus_a2a import Task, TaskState, Message, Artifact, Part
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# ── Version ───────────────────────────────────────────────────────────────────
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
# ── Decorator — the primary developer entry point ─────────────────────────────
|
|
15
|
+
from nexus_a2a.decorators import agent, get_card
|
|
16
|
+
|
|
17
|
+
# ── Agent models ──────────────────────────────────────────────────────────────
|
|
18
|
+
from nexus_a2a.models.agent import (
|
|
19
|
+
AgentAuthentication,
|
|
20
|
+
AgentCapabilities,
|
|
21
|
+
AgentCard,
|
|
22
|
+
AgentSkill,
|
|
23
|
+
AuthScheme,
|
|
24
|
+
InputMode,
|
|
25
|
+
OutputMode,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ── Task models ───────────────────────────────────────────────────────────────
|
|
29
|
+
from nexus_a2a.models.task import (
|
|
30
|
+
Artifact,
|
|
31
|
+
Message,
|
|
32
|
+
MessageRole,
|
|
33
|
+
Part,
|
|
34
|
+
PartType,
|
|
35
|
+
Task,
|
|
36
|
+
TaskState,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# ── What gets exported when someone does: from nexus_a2a import * ─────────────
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Decorator
|
|
42
|
+
"agent",
|
|
43
|
+
"get_card",
|
|
44
|
+
# Agent models
|
|
45
|
+
"AgentCard",
|
|
46
|
+
"AgentSkill",
|
|
47
|
+
"AgentCapabilities",
|
|
48
|
+
"AgentAuthentication",
|
|
49
|
+
"AuthScheme",
|
|
50
|
+
"InputMode",
|
|
51
|
+
"OutputMode",
|
|
52
|
+
# Task models
|
|
53
|
+
"Task",
|
|
54
|
+
"TaskState",
|
|
55
|
+
"Message",
|
|
56
|
+
"MessageRole",
|
|
57
|
+
"Part",
|
|
58
|
+
"PartType",
|
|
59
|
+
"Artifact",
|
|
60
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nexus_a2a/decorators.py
|
|
3
|
+
|
|
4
|
+
The @agent decorator — the primary entry point for developers.
|
|
5
|
+
Wraps a class and automatically generates an AgentCard from its metadata.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
@agent(
|
|
9
|
+
name="ResearchAgent",
|
|
10
|
+
description="Searches the web and summarises results.",
|
|
11
|
+
skills=[{"id": "search", "name": "Web search", "description": "..."}],
|
|
12
|
+
url="http://localhost:8001",
|
|
13
|
+
)
|
|
14
|
+
class ResearchAgent:
|
|
15
|
+
async def run(self, task: Task) -> str:
|
|
16
|
+
return "result"
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import functools
|
|
22
|
+
import inspect
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from typing import Any, Callable, TypeVar, overload, cast
|
|
25
|
+
|
|
26
|
+
from nexus_a2a.models.agent import (
|
|
27
|
+
AgentAuthentication,
|
|
28
|
+
AgentCapabilities,
|
|
29
|
+
AgentCard,
|
|
30
|
+
AgentSkill,
|
|
31
|
+
AuthScheme,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# The decorated class can be any type
|
|
35
|
+
C = TypeVar("C", bound=type)
|
|
36
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
37
|
+
|
|
38
|
+
# Attribute name we attach to the class so other layers can read the card
|
|
39
|
+
_AGENT_CARD_ATTR = "__nexus_agent_card__"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Internal helpers ──────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
def _build_skill(raw: dict[str, Any] | AgentSkill) -> AgentSkill:
|
|
45
|
+
"""Accept either an AgentSkill instance or a plain dict and return AgentSkill."""
|
|
46
|
+
if isinstance(raw, AgentSkill):
|
|
47
|
+
return raw
|
|
48
|
+
return AgentSkill(**raw)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _has_async_run(cls: type) -> bool:
|
|
52
|
+
"""Return True if the class has an async method named 'run'."""
|
|
53
|
+
method = getattr(cls, "run", None)
|
|
54
|
+
return method is not None and inspect.iscoroutinefunction(method)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Public decorator ──────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
@overload
|
|
60
|
+
def agent(cls: C) -> C: ... # called as @agent (no parentheses)
|
|
61
|
+
|
|
62
|
+
@overload
|
|
63
|
+
def agent(
|
|
64
|
+
*,
|
|
65
|
+
name: str | None = None,
|
|
66
|
+
description: str | None = None,
|
|
67
|
+
version: str = "0.1.0",
|
|
68
|
+
url: str = "http://localhost:8000",
|
|
69
|
+
skills: list[dict[str, Any] | AgentSkill] | None = None,
|
|
70
|
+
streaming: bool = False,
|
|
71
|
+
push_notifications: bool = False,
|
|
72
|
+
auth_scheme: AuthScheme = AuthScheme.NONE,
|
|
73
|
+
) -> Callable[[C], C]: ... # called as @agent(...) with arguments
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def agent(
|
|
77
|
+
cls: C | None = None,
|
|
78
|
+
*,
|
|
79
|
+
name: str | None = None,
|
|
80
|
+
description: str | None = None,
|
|
81
|
+
version: str = "0.1.0",
|
|
82
|
+
url: str = "http://localhost:8000",
|
|
83
|
+
skills: list[dict[str, Any] | AgentSkill] | None = None,
|
|
84
|
+
streaming: bool = False,
|
|
85
|
+
push_notifications: bool = False,
|
|
86
|
+
auth_scheme: AuthScheme = AuthScheme.NONE,
|
|
87
|
+
) -> C | Callable[[C], C]:
|
|
88
|
+
"""
|
|
89
|
+
Class decorator that registers a class as an A2A-compatible agent
|
|
90
|
+
and automatically generates its AgentCard.
|
|
91
|
+
|
|
92
|
+
Can be used in two ways:
|
|
93
|
+
|
|
94
|
+
1. With arguments (recommended):
|
|
95
|
+
@agent(
|
|
96
|
+
name="Summariser",
|
|
97
|
+
description="Summarises long documents.",
|
|
98
|
+
skills=[{"id": "summarise", "name": "Summarise", "description": "..."}],
|
|
99
|
+
url="http://localhost:8001",
|
|
100
|
+
)
|
|
101
|
+
class SummariserAgent:
|
|
102
|
+
async def run(self, task): ...
|
|
103
|
+
|
|
104
|
+
2. Without arguments (uses class name and docstring as defaults):
|
|
105
|
+
@agent
|
|
106
|
+
class SummariserAgent:
|
|
107
|
+
\"\"\"Summarises long documents.\"\"\"
|
|
108
|
+
async def run(self, task): ...
|
|
109
|
+
|
|
110
|
+
After decoration, the class gains:
|
|
111
|
+
- SummariserAgent.__nexus_agent_card__ → AgentCard instance
|
|
112
|
+
- SummariserAgent.get_agent_card() → returns the AgentCard
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
TypeError: If the class does not have an async `run` method.
|
|
116
|
+
ValueError: If name or description cannot be resolved.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def decorator(klass: C) -> C:
|
|
120
|
+
# ── Resolve metadata ──────────────────────────────────────────────────
|
|
121
|
+
resolved_name = name or klass.__name__
|
|
122
|
+
resolved_desc = (
|
|
123
|
+
description
|
|
124
|
+
or (inspect.getdoc(klass)) # use docstring if available
|
|
125
|
+
or f"A2A agent: {resolved_name}" # fallback
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not resolved_desc.strip():
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"@agent on '{resolved_name}': provide a description= "
|
|
131
|
+
"or add a docstring to the class."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# ── Validate the class has async run() ───────────────────────────────
|
|
135
|
+
if not _has_async_run(klass):
|
|
136
|
+
raise TypeError(
|
|
137
|
+
f"@agent on '{resolved_name}': the class must define "
|
|
138
|
+
"'async def run(self, task)' method."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# ── Build skills list ─────────────────────────────────────────────────
|
|
142
|
+
built_skills: list[AgentSkill] = [
|
|
143
|
+
_build_skill(s) for s in (skills or [])
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
# ── Build AgentCard ───────────────────────────────────────────────────
|
|
147
|
+
card = AgentCard(
|
|
148
|
+
name=resolved_name,
|
|
149
|
+
description=resolved_desc,
|
|
150
|
+
version=version,
|
|
151
|
+
url=url, # type: ignore[arg-type] # Pydantic coerces str → HttpUrl
|
|
152
|
+
skills=built_skills,
|
|
153
|
+
capabilities=AgentCapabilities(
|
|
154
|
+
streaming=streaming,
|
|
155
|
+
push_notifications=push_notifications,
|
|
156
|
+
),
|
|
157
|
+
authentication=AgentAuthentication(scheme=auth_scheme),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# ── Attach card to the class ──────────────────────────────────────────
|
|
161
|
+
setattr(klass, _AGENT_CARD_ATTR, card)
|
|
162
|
+
|
|
163
|
+
# ── Add helper class method ───────────────────────────────────────────
|
|
164
|
+
@classmethod # type: ignore[misc]
|
|
165
|
+
def get_agent_card(klass_inner: type) -> AgentCard:
|
|
166
|
+
"""Return the AgentCard generated by @agent."""
|
|
167
|
+
return cast(AgentCard, getattr(klass_inner, _AGENT_CARD_ATTR))
|
|
168
|
+
|
|
169
|
+
klass.get_agent_card = get_agent_card # type: ignore[attr-defined]
|
|
170
|
+
|
|
171
|
+
# ── Preserve original class metadata ──────────────────────────────────
|
|
172
|
+
functools.update_wrapper(klass, klass, updated=[])
|
|
173
|
+
|
|
174
|
+
return klass
|
|
175
|
+
|
|
176
|
+
# Handle both @agent and @agent(...) call styles
|
|
177
|
+
if cls is not None:
|
|
178
|
+
# Called as @agent with no parentheses
|
|
179
|
+
return decorator(cls)
|
|
180
|
+
|
|
181
|
+
# Called as @agent(...) with arguments
|
|
182
|
+
return decorator
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── Public helper ─────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def get_card(agent_cls: type) -> AgentCard:
|
|
188
|
+
"""
|
|
189
|
+
Retrieve the AgentCard from a decorated agent class.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
agent_cls: A class decorated with @agent.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The AgentCard attached to the class.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
TypeError: If the class was not decorated with @agent.
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
card = get_card(ResearchAgent)
|
|
202
|
+
print(card.name)
|
|
203
|
+
"""
|
|
204
|
+
card = getattr(agent_cls, _AGENT_CARD_ATTR, None)
|
|
205
|
+
if card is None:
|
|
206
|
+
raise TypeError(
|
|
207
|
+
f"'{agent_cls.__name__}' is not decorated with @agent. "
|
|
208
|
+
"Apply @agent before calling get_card()."
|
|
209
|
+
)
|
|
210
|
+
return cast(AgentCard, card)
|
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nexus_a2a/models/agent.py
|
|
3
|
+
|
|
4
|
+
Pydantic models that describe an AI agent's identity, capabilities,
|
|
5
|
+
and skills — mirroring the A2A protocol's AgentCard specification.
|
|
6
|
+
|
|
7
|
+
These models are the single source of truth for agent metadata
|
|
8
|
+
across the entire package.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Annotated, Any
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, HttpUrl, field_validator
|
|
17
|
+
|
|
18
|
+
# ── Enums ─────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
class AuthScheme(str, Enum):
|
|
21
|
+
"""Supported authentication schemes for agent-to-agent calls."""
|
|
22
|
+
NONE = "none"
|
|
23
|
+
API_KEY = "api_key"
|
|
24
|
+
JWT = "jwt"
|
|
25
|
+
OAUTH2 = "oauth2"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InputMode(str, Enum):
|
|
29
|
+
"""MIME types an agent can accept as input."""
|
|
30
|
+
TEXT = "text/plain"
|
|
31
|
+
JSON = "application/json"
|
|
32
|
+
MULTIPART = "multipart/form-data"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OutputMode(str, Enum):
|
|
36
|
+
"""MIME types an agent can produce as output."""
|
|
37
|
+
TEXT = "text/plain"
|
|
38
|
+
JSON = "application/json"
|
|
39
|
+
MARKDOWN = "text/markdown"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Sub-models ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
class AgentAuthentication(BaseModel):
|
|
45
|
+
"""
|
|
46
|
+
Describes how a client must authenticate when calling this agent.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
AgentAuthentication(scheme=AuthScheme.JWT, token_url="https://auth.example.com/token")
|
|
50
|
+
"""
|
|
51
|
+
scheme: AuthScheme = AuthScheme.NONE
|
|
52
|
+
token_url: HttpUrl | None = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
description="URL to obtain a token — required for JWT and OAuth2 schemes.",
|
|
55
|
+
)
|
|
56
|
+
header_name: str | None = Field(
|
|
57
|
+
default=None,
|
|
58
|
+
description="Custom header name for API key auth. Defaults to 'X-API-Key'.",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
model_config = {"use_enum_values": True}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AgentCapabilities(BaseModel):
|
|
65
|
+
"""
|
|
66
|
+
Flags that describe what protocol features this agent supports.
|
|
67
|
+
The client uses these to decide how to interact with the agent.
|
|
68
|
+
"""
|
|
69
|
+
streaming: bool = Field(
|
|
70
|
+
default=False,
|
|
71
|
+
description="Agent supports Server-Sent Events for real-time task updates.",
|
|
72
|
+
)
|
|
73
|
+
push_notifications: bool = Field(
|
|
74
|
+
default=False,
|
|
75
|
+
description="Agent can POST task updates to a client-provided webhook URL.",
|
|
76
|
+
)
|
|
77
|
+
multi_turn: bool = Field(
|
|
78
|
+
default=True,
|
|
79
|
+
description="Agent supports back-and-forth conversations within a single task.",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AgentSkill(BaseModel):
|
|
84
|
+
"""
|
|
85
|
+
A single capability that an agent advertises.
|
|
86
|
+
Think of this as one entry in the agent's 'menu of services'.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
AgentSkill(
|
|
90
|
+
id="web_search",
|
|
91
|
+
name="Web search",
|
|
92
|
+
description="Searches the web and returns summarised results.",
|
|
93
|
+
tags=["search", "web", "research"],
|
|
94
|
+
examples=["Search for latest AI papers", "Find Python docs for asyncio"],
|
|
95
|
+
)
|
|
96
|
+
"""
|
|
97
|
+
id: Annotated[str, Field(min_length=1, max_length=64)]
|
|
98
|
+
name: Annotated[str, Field(min_length=1, max_length=128)]
|
|
99
|
+
description: Annotated[str, Field(min_length=1, max_length=1024)]
|
|
100
|
+
tags: list[str] = Field(
|
|
101
|
+
default_factory=list,
|
|
102
|
+
description="Keywords that help agents discover this skill.",
|
|
103
|
+
)
|
|
104
|
+
examples: list[str] = Field(
|
|
105
|
+
default_factory=list,
|
|
106
|
+
description="Sample inputs that demonstrate this skill.",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@field_validator("tags", "examples", mode="before")
|
|
110
|
+
@classmethod
|
|
111
|
+
def _no_empty_strings(cls, v: list[str]) -> list[str]:
|
|
112
|
+
"""Strip whitespace and drop empty strings from lists."""
|
|
113
|
+
return [item.strip() for item in v if item.strip()]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── Primary model ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
class AgentCard(BaseModel):
|
|
119
|
+
"""
|
|
120
|
+
The complete identity document for an agent — exposed at the
|
|
121
|
+
well-known endpoint: GET /.well-known/agent-card.json
|
|
122
|
+
|
|
123
|
+
This is what other agents read during discovery to learn:
|
|
124
|
+
- What the agent can do (skills)
|
|
125
|
+
- How to reach it (url)
|
|
126
|
+
- How to authenticate (authentication)
|
|
127
|
+
- What formats it speaks (input_modes, output_modes)
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
AgentCard(
|
|
131
|
+
name="ResearchAgent",
|
|
132
|
+
description="Searches the web and summarises findings.",
|
|
133
|
+
version="1.0.0",
|
|
134
|
+
url="https://research-agent.example.com",
|
|
135
|
+
skills=[AgentSkill(id="search", name="Search", description="Web search")],
|
|
136
|
+
)
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
# Identity
|
|
140
|
+
name: Annotated[str, Field(min_length=1, max_length=128)]
|
|
141
|
+
description: Annotated[str, Field(min_length=1, max_length=1024)]
|
|
142
|
+
version: str = Field(
|
|
143
|
+
default="0.1.0",
|
|
144
|
+
description="Semantic version of this agent. Bump when skills change.",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Network
|
|
148
|
+
url: HttpUrl = Field(
|
|
149
|
+
description="Base URL where this agent's A2A server is reachable.",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Protocol
|
|
153
|
+
capabilities: AgentCapabilities = Field(default_factory=AgentCapabilities)
|
|
154
|
+
authentication: AgentAuthentication = Field(default_factory=AgentAuthentication)
|
|
155
|
+
|
|
156
|
+
# Skills
|
|
157
|
+
skills: list[AgentSkill] = Field(
|
|
158
|
+
default_factory=list,
|
|
159
|
+
description="List of capabilities this agent offers.",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Communication formats
|
|
163
|
+
input_modes: list[InputMode] = Field(
|
|
164
|
+
default_factory=lambda: [InputMode.TEXT, InputMode.JSON],
|
|
165
|
+
)
|
|
166
|
+
output_modes: list[OutputMode] = Field(
|
|
167
|
+
default_factory=lambda: [OutputMode.TEXT, OutputMode.JSON],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
model_config = {
|
|
171
|
+
"use_enum_values": True,
|
|
172
|
+
# Allows: AgentCard(**a2a_sdk_agent_card_dict) without extra fields
|
|
173
|
+
# causing an error — useful when parsing cards from external agents.
|
|
174
|
+
"extra": "ignore",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
def has_skill(self, skill_id: str) -> bool:
|
|
180
|
+
"""Return True if this agent advertises the given skill id."""
|
|
181
|
+
return any(s.id == skill_id for s in self.skills)
|
|
182
|
+
|
|
183
|
+
def skill_ids(self) -> list[str]:
|
|
184
|
+
"""Return a flat list of all advertised skill IDs."""
|
|
185
|
+
return [s.id for s in self.skills]
|
|
186
|
+
|
|
187
|
+
def to_well_known_dict(self) -> dict[str, Any] :
|
|
188
|
+
"""
|
|
189
|
+
Serialise to the dict served at /.well-known/agent-card.json.
|
|
190
|
+
URLs are converted to plain strings so JSON serialisation works.
|
|
191
|
+
"""
|
|
192
|
+
return self.model_dump(mode="json")
|