theprotocol-sdk 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.
- theprotocol_sdk-0.1.0/.gitignore +31 -0
- theprotocol_sdk-0.1.0/PKG-INFO +96 -0
- theprotocol_sdk-0.1.0/README.md +61 -0
- theprotocol_sdk-0.1.0/examples/simple_agent.py +64 -0
- theprotocol_sdk-0.1.0/pyproject.toml +52 -0
- theprotocol_sdk-0.1.0/src/theprotocol/__init__.py +10 -0
- theprotocol_sdk-0.1.0/src/theprotocol/agent/__init__.py +17 -0
- theprotocol_sdk-0.1.0/src/theprotocol/agent/base.py +63 -0
- theprotocol_sdk-0.1.0/src/theprotocol/agent/decorators.py +29 -0
- theprotocol_sdk-0.1.0/src/theprotocol/agent/router.py +194 -0
- theprotocol_sdk-0.1.0/src/theprotocol/agent/task_store.py +202 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/__init__.py +9 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/acp.py +240 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/__init__.py +22 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/auth.py +210 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/bridge.py +188 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/did_wba.py +261 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/translation.py +188 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/base.py +61 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/google_a2a.py +194 -0
- theprotocol_sdk-0.1.0/src/theprotocol/bridges/mcp.py +236 -0
- theprotocol_sdk-0.1.0/src/theprotocol/client/__init__.py +6 -0
- theprotocol_sdk-0.1.0/src/theprotocol/client/a2a_client.py +214 -0
- theprotocol_sdk-0.1.0/src/theprotocol/client/credentials.py +186 -0
- theprotocol_sdk-0.1.0/src/theprotocol/exceptions.py +76 -0
- theprotocol_sdk-0.1.0/src/theprotocol/models/__init__.py +30 -0
- theprotocol_sdk-0.1.0/src/theprotocol/models/a2a_protocol.py +128 -0
- theprotocol_sdk-0.1.0/src/theprotocol/models/agent_card.py +90 -0
- theprotocol_sdk-0.1.0/tests/test_agent.py +92 -0
- theprotocol_sdk-0.1.0/tests/test_bridge_acp.py +192 -0
- theprotocol_sdk-0.1.0/tests/test_bridge_anp.py +447 -0
- theprotocol_sdk-0.1.0/tests/test_bridge_google_a2a.py +175 -0
- theprotocol_sdk-0.1.0/tests/test_bridge_mcp.py +188 -0
- theprotocol_sdk-0.1.0/tests/test_credentials.py +60 -0
- theprotocol_sdk-0.1.0/tests/test_models.py +87 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# TheProtocol — Root .gitignore
|
|
2
|
+
# Created: 2026-03-09 (§4.1 Credentials Rotation)
|
|
3
|
+
|
|
4
|
+
# !! NEVER COMMIT PRODUCTION SECRETS !!
|
|
5
|
+
.env.production
|
|
6
|
+
.env.production.*
|
|
7
|
+
|
|
8
|
+
# Session logs (operational, not source)
|
|
9
|
+
sessionlogs2026/
|
|
10
|
+
|
|
11
|
+
# Debug/runtime artifacts
|
|
12
|
+
logs/
|
|
13
|
+
*.log
|
|
14
|
+
APEX_PREDATOR_JOBS.log
|
|
15
|
+
|
|
16
|
+
# Node
|
|
17
|
+
node_modules/
|
|
18
|
+
|
|
19
|
+
# Python
|
|
20
|
+
__pycache__/
|
|
21
|
+
*.pyc
|
|
22
|
+
*.pyo
|
|
23
|
+
*.pyd
|
|
24
|
+
.venv/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
dist/
|
|
27
|
+
build/
|
|
28
|
+
|
|
29
|
+
# OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: theprotocol-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TheProtocol SDK — Build and call A2A agents. Protocol bridges for ACP, ADK, and more.
|
|
5
|
+
Project-URL: Homepage, https://theprotocol.cloud
|
|
6
|
+
Project-URL: Documentation, https://api.theprotocol.cloud/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/theprotocol/theprotocol-sdk
|
|
8
|
+
Author-email: TheProtocol <sdk@theprotocol.cloud>
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: a2a,agents,ai,mcp,protocol,theprotocol
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx[http2]>=0.27
|
|
21
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
22
|
+
Requires-Dist: python-dotenv>=1.0
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: fastapi>=0.111; extra == 'all'
|
|
25
|
+
Requires-Dist: keyring>=24; extra == 'all'
|
|
26
|
+
Provides-Extra: keyring
|
|
27
|
+
Requires-Dist: keyring>=24; extra == 'keyring'
|
|
28
|
+
Provides-Extra: server
|
|
29
|
+
Requires-Dist: fastapi>=0.111; extra == 'server'
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
33
|
+
Requires-Dist: respx>=0.20; extra == 'test'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# theprotocol-sdk
|
|
37
|
+
|
|
38
|
+
Build and call A2A (Agent-to-Agent) agents on [TheProtocol](https://theprotocol.cloud).
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install theprotocol-sdk # Client only (call agents)
|
|
44
|
+
pip install theprotocol-sdk[server] # + FastAPI router (build agents)
|
|
45
|
+
pip install theprotocol-sdk[all] # Everything
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Build an Agent (10 lines)
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from theprotocol.agent import BaseA2AAgent, create_a2a_router
|
|
52
|
+
from theprotocol.models import Message, TextPart
|
|
53
|
+
from fastapi import FastAPI
|
|
54
|
+
|
|
55
|
+
class MyAgent(BaseA2AAgent):
|
|
56
|
+
async def handle_task_send(self, task_id, message):
|
|
57
|
+
return "task-1" # Return task ID
|
|
58
|
+
async def handle_task_get(self, task_id):
|
|
59
|
+
... # Return Task object
|
|
60
|
+
async def handle_task_cancel(self, task_id):
|
|
61
|
+
return True
|
|
62
|
+
async def handle_subscribe_request(self, task_id):
|
|
63
|
+
yield # SSE events
|
|
64
|
+
|
|
65
|
+
app = FastAPI()
|
|
66
|
+
app.include_router(create_a2a_router(MyAgent()))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Call a Remote Agent
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from theprotocol.client import A2AClient, KeyManager
|
|
73
|
+
from theprotocol.models import Message, TextPart
|
|
74
|
+
|
|
75
|
+
async with A2AClient() as client:
|
|
76
|
+
task_id = await client.initiate_task(agent_card, message, key_manager)
|
|
77
|
+
task = await client.get_task_status(agent_card, task_id, key_manager)
|
|
78
|
+
print(task.state) # COMPLETED
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Protocol Bridges (coming soon)
|
|
82
|
+
|
|
83
|
+
Translate between A2A and other agent protocols:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from theprotocol.bridges.acp import ACPBridge # ACP ↔ A2A
|
|
87
|
+
from theprotocol.bridges.adk import ADKBridge # Google ADK ↔ A2A
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Registry Operations
|
|
91
|
+
|
|
92
|
+
For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/docs) instead of SDK functions. 19 tools available via Claude Desktop or any MCP client.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
Apache-2.0
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# theprotocol-sdk
|
|
2
|
+
|
|
3
|
+
Build and call A2A (Agent-to-Agent) agents on [TheProtocol](https://theprotocol.cloud).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install theprotocol-sdk # Client only (call agents)
|
|
9
|
+
pip install theprotocol-sdk[server] # + FastAPI router (build agents)
|
|
10
|
+
pip install theprotocol-sdk[all] # Everything
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Build an Agent (10 lines)
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from theprotocol.agent import BaseA2AAgent, create_a2a_router
|
|
17
|
+
from theprotocol.models import Message, TextPart
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
|
|
20
|
+
class MyAgent(BaseA2AAgent):
|
|
21
|
+
async def handle_task_send(self, task_id, message):
|
|
22
|
+
return "task-1" # Return task ID
|
|
23
|
+
async def handle_task_get(self, task_id):
|
|
24
|
+
... # Return Task object
|
|
25
|
+
async def handle_task_cancel(self, task_id):
|
|
26
|
+
return True
|
|
27
|
+
async def handle_subscribe_request(self, task_id):
|
|
28
|
+
yield # SSE events
|
|
29
|
+
|
|
30
|
+
app = FastAPI()
|
|
31
|
+
app.include_router(create_a2a_router(MyAgent()))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Call a Remote Agent
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from theprotocol.client import A2AClient, KeyManager
|
|
38
|
+
from theprotocol.models import Message, TextPart
|
|
39
|
+
|
|
40
|
+
async with A2AClient() as client:
|
|
41
|
+
task_id = await client.initiate_task(agent_card, message, key_manager)
|
|
42
|
+
task = await client.get_task_status(agent_card, task_id, key_manager)
|
|
43
|
+
print(task.state) # COMPLETED
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Protocol Bridges (coming soon)
|
|
47
|
+
|
|
48
|
+
Translate between A2A and other agent protocols:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from theprotocol.bridges.acp import ACPBridge # ACP ↔ A2A
|
|
52
|
+
from theprotocol.bridges.adk import ADKBridge # Google ADK ↔ A2A
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Registry Operations
|
|
56
|
+
|
|
57
|
+
For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/docs) instead of SDK functions. 19 tools available via Claude Desktop or any MCP client.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
Apache-2.0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal A2A agent example.
|
|
3
|
+
|
|
4
|
+
Run: uvicorn simple_agent:app --port 8080
|
|
5
|
+
Then register on TheProtocol via MCP: createAgent(name="My Agent", ...)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
import datetime
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
|
|
12
|
+
from theprotocol.agent import BaseA2AAgent, create_a2a_router
|
|
13
|
+
from theprotocol.models import Message, Task, TaskState, TextPart
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EchoAgent(BaseA2AAgent):
|
|
17
|
+
"""Simple agent that echoes back whatever you send it."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__(agent_metadata={"name": "Echo Agent"})
|
|
21
|
+
self.tasks = {}
|
|
22
|
+
|
|
23
|
+
async def handle_task_send(self, task_id, message):
|
|
24
|
+
tid = task_id or f"task-{uuid.uuid4().hex[:8]}"
|
|
25
|
+
# Echo the first text part
|
|
26
|
+
text = ""
|
|
27
|
+
for part in message.parts:
|
|
28
|
+
if hasattr(part, 'content') and isinstance(part.content, str):
|
|
29
|
+
text = part.content
|
|
30
|
+
break
|
|
31
|
+
self.tasks[tid] = {
|
|
32
|
+
"state": TaskState.COMPLETED,
|
|
33
|
+
"response": f"Echo: {text}",
|
|
34
|
+
"message": message,
|
|
35
|
+
}
|
|
36
|
+
return tid
|
|
37
|
+
|
|
38
|
+
async def handle_task_get(self, task_id):
|
|
39
|
+
t = self.tasks.get(task_id)
|
|
40
|
+
if not t:
|
|
41
|
+
raise ValueError(f"Task {task_id} not found")
|
|
42
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
43
|
+
return Task(
|
|
44
|
+
id=task_id,
|
|
45
|
+
state=t["state"],
|
|
46
|
+
createdAt=now,
|
|
47
|
+
updatedAt=now,
|
|
48
|
+
messages=[t["message"]],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def handle_task_cancel(self, task_id):
|
|
52
|
+
if task_id in self.tasks:
|
|
53
|
+
self.tasks[task_id]["state"] = TaskState.CANCELED
|
|
54
|
+
return True
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
async def handle_subscribe_request(self, task_id):
|
|
58
|
+
# No streaming for this simple agent
|
|
59
|
+
return
|
|
60
|
+
yield # Make it a generator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
app = FastAPI(title="Echo Agent")
|
|
64
|
+
app.include_router(create_a2a_router(EchoAgent()), prefix="/a2a")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "theprotocol-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "TheProtocol SDK — Build and call A2A agents. Protocol bridges for ACP, ADK, and more."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "TheProtocol", email = "sdk@theprotocol.cloud"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["agents", "a2a", "protocol", "ai", "mcp", "theprotocol"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pydantic>=2.0,<3.0",
|
|
28
|
+
"httpx[http2]>=0.27",
|
|
29
|
+
"python-dotenv>=1.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
server = ["fastapi>=0.111"]
|
|
34
|
+
keyring = ["keyring>=24"]
|
|
35
|
+
all = ["theprotocol-sdk[server,keyring]"]
|
|
36
|
+
test = [
|
|
37
|
+
"pytest>=7.0",
|
|
38
|
+
"pytest-asyncio>=0.21",
|
|
39
|
+
"respx>=0.20",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://theprotocol.cloud"
|
|
44
|
+
Documentation = "https://api.theprotocol.cloud/docs"
|
|
45
|
+
Repository = "https://github.com/theprotocol/theprotocol-sdk"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/theprotocol"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
asyncio_mode = "auto"
|
|
52
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TheProtocol SDK — Build and call A2A agents.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from theprotocol.models import Message, Task, AgentCard
|
|
6
|
+
from theprotocol.agent import BaseA2AAgent, create_a2a_router
|
|
7
|
+
from theprotocol.client import A2AClient, KeyManager
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Build A2A-compliant agents."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseA2AAgent
|
|
4
|
+
from .task_store import BaseTaskStore, InMemoryTaskStore, TaskContext
|
|
5
|
+
from .decorators import a2a_method
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"BaseA2AAgent", "BaseTaskStore", "InMemoryTaskStore", "TaskContext",
|
|
9
|
+
"a2a_method",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# Import router factory only if fastapi is available
|
|
13
|
+
try:
|
|
14
|
+
from .router import create_a2a_router
|
|
15
|
+
__all__.append("create_a2a_router")
|
|
16
|
+
except ImportError:
|
|
17
|
+
pass
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BaseA2AAgent — Abstract base class for TheProtocol A2A agents.
|
|
3
|
+
|
|
4
|
+
Subclass this and implement the handle_* methods to create an agent.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional, Dict, Any, AsyncGenerator
|
|
9
|
+
|
|
10
|
+
from ..models import Message, Task, A2AEvent
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseA2AAgent:
|
|
16
|
+
"""
|
|
17
|
+
Abstract base class for A2A agents.
|
|
18
|
+
|
|
19
|
+
Implement the four handle methods to define your agent's behavior:
|
|
20
|
+
- handle_task_send: Process incoming messages
|
|
21
|
+
- handle_task_get: Return current task state
|
|
22
|
+
- handle_task_cancel: Cancel a running task
|
|
23
|
+
- handle_subscribe_request: Stream SSE events for a task
|
|
24
|
+
|
|
25
|
+
Example::
|
|
26
|
+
|
|
27
|
+
class MyAgent(BaseA2AAgent):
|
|
28
|
+
async def handle_task_send(self, task_id, message):
|
|
29
|
+
# Process the message and return task ID
|
|
30
|
+
return "task-123"
|
|
31
|
+
|
|
32
|
+
async def handle_task_get(self, task_id):
|
|
33
|
+
# Return the current task state
|
|
34
|
+
return Task(id=task_id, state=TaskState.COMPLETED, ...)
|
|
35
|
+
|
|
36
|
+
async def handle_task_cancel(self, task_id):
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
async def handle_subscribe_request(self, task_id):
|
|
40
|
+
yield TaskStatusUpdateEvent(...)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, agent_metadata: Optional[Dict[str, Any]] = None):
|
|
44
|
+
self.agent_metadata = agent_metadata or {}
|
|
45
|
+
logger.info(f"Initialized {self.__class__.__name__}")
|
|
46
|
+
|
|
47
|
+
async def handle_task_send(self, task_id: Optional[str], message: Message) -> str:
|
|
48
|
+
"""Handle incoming message. Return task ID."""
|
|
49
|
+
raise NotImplementedError("Implement handle_task_send")
|
|
50
|
+
|
|
51
|
+
async def handle_task_get(self, task_id: str) -> Task:
|
|
52
|
+
"""Return current task state."""
|
|
53
|
+
raise NotImplementedError("Implement handle_task_get")
|
|
54
|
+
|
|
55
|
+
async def handle_task_cancel(self, task_id: str) -> bool:
|
|
56
|
+
"""Cancel a task. Return True if accepted."""
|
|
57
|
+
raise NotImplementedError("Implement handle_task_cancel")
|
|
58
|
+
|
|
59
|
+
async def handle_subscribe_request(self, task_id: str) -> AsyncGenerator[A2AEvent, None]:
|
|
60
|
+
"""Yield SSE events for a task."""
|
|
61
|
+
raise NotImplementedError("Implement handle_subscribe_request")
|
|
62
|
+
if False: # pragma: no cover
|
|
63
|
+
yield # Make it a generator
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Decorator for custom A2A JSON-RPC methods."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Callable, TypeVar
|
|
5
|
+
|
|
6
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def a2a_method(method_name: str) -> Callable[[F], F]:
|
|
10
|
+
"""
|
|
11
|
+
Mark an agent method as a handler for a custom A2A JSON-RPC method.
|
|
12
|
+
|
|
13
|
+
Example::
|
|
14
|
+
|
|
15
|
+
class MyAgent(BaseA2AAgent):
|
|
16
|
+
@a2a_method("custom/analyze")
|
|
17
|
+
async def analyze(self, data: dict) -> dict:
|
|
18
|
+
return {"result": "analyzed"}
|
|
19
|
+
"""
|
|
20
|
+
if not isinstance(method_name, str) or not method_name:
|
|
21
|
+
raise ValueError("a2a_method requires a non-empty method name string.")
|
|
22
|
+
|
|
23
|
+
def _decorator(func: F) -> F:
|
|
24
|
+
if not asyncio.iscoroutinefunction(func):
|
|
25
|
+
raise TypeError(f"A2A method handler '{func.__name__}' must be async.")
|
|
26
|
+
setattr(func, '_a2a_method_name', method_name)
|
|
27
|
+
return func
|
|
28
|
+
|
|
29
|
+
return _decorator
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""FastAPI router factory for A2A agents."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, Optional, Union, List, Callable, AsyncGenerator
|
|
7
|
+
|
|
8
|
+
import pydantic
|
|
9
|
+
from pydantic_core import ValidationError
|
|
10
|
+
from fastapi import APIRouter, Depends, Request, Response, status
|
|
11
|
+
from fastapi.responses import StreamingResponse, JSONResponse
|
|
12
|
+
|
|
13
|
+
from .base import BaseA2AAgent
|
|
14
|
+
from .task_store import BaseTaskStore, InMemoryTaskStore, TaskContext
|
|
15
|
+
from ..models import (
|
|
16
|
+
Message, Task, TaskState, A2AEvent,
|
|
17
|
+
TaskSendParams, TaskSendResult, TaskGetParams, GetTaskResult,
|
|
18
|
+
TaskCancelParams, TaskCancelResult,
|
|
19
|
+
TaskStatusUpdateEvent, TaskMessageEvent, TaskArtifactUpdateEvent,
|
|
20
|
+
)
|
|
21
|
+
from ..exceptions import AgentServerError, TaskNotFoundError
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# JSON-RPC error codes
|
|
26
|
+
JSONRPC_PARSE_ERROR = -32700
|
|
27
|
+
JSONRPC_INVALID_REQUEST = -32600
|
|
28
|
+
JSONRPC_METHOD_NOT_FOUND = -32601
|
|
29
|
+
JSONRPC_INVALID_PARAMS = -32602
|
|
30
|
+
JSONRPC_INTERNAL_ERROR = -32603
|
|
31
|
+
JSONRPC_APP_ERROR = -32000
|
|
32
|
+
JSONRPC_TASK_NOT_FOUND = -32001
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _jsonrpc_error(req_id: Union[str, int, None], code: int, message: str, data: Optional[Any] = None) -> Dict:
|
|
36
|
+
err: Dict[str, Any] = {"code": code, "message": message}
|
|
37
|
+
if data is not None:
|
|
38
|
+
err["data"] = data
|
|
39
|
+
return {"jsonrpc": "2.0", "error": err, "id": req_id}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _jsonrpc_success(req_id: Union[str, int, None], result: Any) -> Dict:
|
|
43
|
+
return {"jsonrpc": "2.0", "result": result, "id": req_id}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _format_sse_bytes(event: A2AEvent) -> Optional[bytes]:
|
|
47
|
+
event_type = None
|
|
48
|
+
if isinstance(event, TaskStatusUpdateEvent):
|
|
49
|
+
event_type = "task_status"
|
|
50
|
+
elif isinstance(event, TaskMessageEvent):
|
|
51
|
+
event_type = "task_message"
|
|
52
|
+
elif isinstance(event, TaskArtifactUpdateEvent):
|
|
53
|
+
event_type = "task_artifact"
|
|
54
|
+
if event_type is None:
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
json_data = event.model_dump_json(by_alias=True)
|
|
58
|
+
return f"event: {event_type}\ndata: {json_data}\n\n".encode("utf-8")
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def _sse_wrapper(task_id: str, gen: AsyncGenerator[A2AEvent, None]) -> AsyncGenerator[bytes, None]:
|
|
64
|
+
try:
|
|
65
|
+
async for event in gen:
|
|
66
|
+
data = _format_sse_bytes(event)
|
|
67
|
+
if data:
|
|
68
|
+
yield data
|
|
69
|
+
except TaskNotFoundError as e:
|
|
70
|
+
yield f'event: error\ndata: {json.dumps({"error": "task_not_found", "message": str(e)})}\n\n'.encode()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
yield f'event: error\ndata: {json.dumps({"error": "stream_error", "message": str(e)})}\n\n'.encode()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_a2a_router(
|
|
76
|
+
agent: BaseA2AAgent,
|
|
77
|
+
prefix: str = "",
|
|
78
|
+
tags: Optional[List[str]] = None,
|
|
79
|
+
task_store: Optional[BaseTaskStore] = None,
|
|
80
|
+
dependencies: Optional[list] = None,
|
|
81
|
+
) -> APIRouter:
|
|
82
|
+
"""
|
|
83
|
+
Create a FastAPI router that exposes an A2A agent via JSON-RPC 2.0.
|
|
84
|
+
|
|
85
|
+
Example::
|
|
86
|
+
|
|
87
|
+
from theprotocol.agent import BaseA2AAgent, create_a2a_router
|
|
88
|
+
from fastapi import FastAPI
|
|
89
|
+
|
|
90
|
+
class MyAgent(BaseA2AAgent):
|
|
91
|
+
async def handle_task_send(self, task_id, message):
|
|
92
|
+
return "task-1"
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
app = FastAPI()
|
|
96
|
+
app.include_router(create_a2a_router(MyAgent()))
|
|
97
|
+
"""
|
|
98
|
+
if tags is None:
|
|
99
|
+
tags = ["A2A Protocol"]
|
|
100
|
+
if task_store is None:
|
|
101
|
+
task_store = InMemoryTaskStore()
|
|
102
|
+
store = task_store
|
|
103
|
+
|
|
104
|
+
router = APIRouter(prefix=prefix, tags=tags, dependencies=dependencies or [])
|
|
105
|
+
|
|
106
|
+
# Discover @a2a_method decorated handlers
|
|
107
|
+
decorated: Dict[str, Callable] = {}
|
|
108
|
+
for name, method in inspect.getmembers(agent, predicate=inspect.iscoroutinefunction):
|
|
109
|
+
if hasattr(method, '_a2a_method_name'):
|
|
110
|
+
decorated[getattr(method, '_a2a_method_name')] = method
|
|
111
|
+
|
|
112
|
+
@router.post("/", summary="A2A JSON-RPC Endpoint")
|
|
113
|
+
async def handle_request(request: Request) -> Response:
|
|
114
|
+
req_id = None
|
|
115
|
+
try:
|
|
116
|
+
payload = await request.json()
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
return JSONResponse(_jsonrpc_error(None, JSONRPC_PARSE_ERROR, "Parse error"))
|
|
119
|
+
|
|
120
|
+
if not isinstance(payload, dict):
|
|
121
|
+
return JSONResponse(_jsonrpc_error(None, JSONRPC_INVALID_REQUEST, "Payload must be JSON object"))
|
|
122
|
+
|
|
123
|
+
req_id = payload.get("id")
|
|
124
|
+
method = payload.get("method", "")
|
|
125
|
+
params = payload.get("params") or {}
|
|
126
|
+
if isinstance(params, list):
|
|
127
|
+
params = {}
|
|
128
|
+
|
|
129
|
+
if payload.get("jsonrpc") != "2.0":
|
|
130
|
+
return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_REQUEST, "'jsonrpc' must be '2.0'"))
|
|
131
|
+
if not isinstance(method, str) or not method:
|
|
132
|
+
return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_REQUEST, "'method' is required"))
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Check decorated methods first
|
|
136
|
+
if method in decorated:
|
|
137
|
+
handler = decorated[method]
|
|
138
|
+
sig = inspect.signature(handler)
|
|
139
|
+
param_fields: Dict[str, Any] = {}
|
|
140
|
+
for pname, p in sig.parameters.items():
|
|
141
|
+
if pname in ('self', 'cls'):
|
|
142
|
+
continue
|
|
143
|
+
ann = Any if p.annotation is inspect.Parameter.empty else p.annotation
|
|
144
|
+
default = ... if p.default is inspect.Parameter.empty else p.default
|
|
145
|
+
param_fields[pname] = (ann, default)
|
|
146
|
+
|
|
147
|
+
mapped = dict(params)
|
|
148
|
+
if "task_id" in [p for p in sig.parameters if p not in ('self', 'cls')] and "id" in params:
|
|
149
|
+
mapped['task_id'] = params['id']
|
|
150
|
+
|
|
151
|
+
ParamsModel = pydantic.create_model(f'{handler.__name__}Params', **param_fields)
|
|
152
|
+
validated = ParamsModel.model_validate(mapped).model_dump()
|
|
153
|
+
result = await handler(**validated)
|
|
154
|
+
return JSONResponse(_jsonrpc_success(req_id, result))
|
|
155
|
+
|
|
156
|
+
elif method == "tasks/send":
|
|
157
|
+
vp = TaskSendParams.model_validate(params)
|
|
158
|
+
task_id = await agent.handle_task_send(task_id=vp.id, message=vp.message)
|
|
159
|
+
return JSONResponse(_jsonrpc_success(req_id, TaskSendResult(id=task_id).model_dump(mode='json')))
|
|
160
|
+
|
|
161
|
+
elif method == "tasks/get":
|
|
162
|
+
vp = TaskGetParams.model_validate(params)
|
|
163
|
+
task = await agent.handle_task_get(task_id=vp.id)
|
|
164
|
+
return JSONResponse(_jsonrpc_success(req_id, task.model_dump(mode='json', by_alias=True)))
|
|
165
|
+
|
|
166
|
+
elif method == "tasks/cancel":
|
|
167
|
+
vp = TaskCancelParams.model_validate(params)
|
|
168
|
+
ok = await agent.handle_task_cancel(task_id=vp.id)
|
|
169
|
+
return JSONResponse(_jsonrpc_success(req_id, TaskCancelResult(success=ok).model_dump(mode='json')))
|
|
170
|
+
|
|
171
|
+
elif method == "tasks/sendSubscribe":
|
|
172
|
+
task_id = params.get("id", "")
|
|
173
|
+
if not task_id:
|
|
174
|
+
return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_PARAMS, "'id' is required"))
|
|
175
|
+
gen = agent.handle_subscribe_request(task_id=task_id)
|
|
176
|
+
return StreamingResponse(content=_sse_wrapper(task_id, gen), media_type="text/event-stream")
|
|
177
|
+
|
|
178
|
+
else:
|
|
179
|
+
return JSONResponse(_jsonrpc_error(req_id, JSONRPC_METHOD_NOT_FOUND, f"Method not found: {method}"))
|
|
180
|
+
|
|
181
|
+
except TaskNotFoundError as e:
|
|
182
|
+
return JSONResponse(_jsonrpc_error(req_id, JSONRPC_TASK_NOT_FOUND, str(e)))
|
|
183
|
+
except (ValueError, TypeError, pydantic.ValidationError, ValidationError) as e:
|
|
184
|
+
return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_PARAMS, f"Invalid parameters: {e}"))
|
|
185
|
+
except AgentServerError as e:
|
|
186
|
+
return JSONResponse(_jsonrpc_error(req_id, JSONRPC_APP_ERROR, f"Agent error: {e}"))
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.exception(f"Unhandled error in A2A handler: {e}")
|
|
189
|
+
return JSONResponse(
|
|
190
|
+
_jsonrpc_error(req_id, JSONRPC_INTERNAL_ERROR, f"Internal error: {type(e).__name__}"),
|
|
191
|
+
status_code=500,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return router
|