theprotocol-sdk 0.1.0__tar.gz → 0.2.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.2.0/LICENSE +176 -0
- theprotocol_sdk-0.2.0/PKG-INFO +136 -0
- theprotocol_sdk-0.2.0/README.md +95 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/pyproject.toml +5 -2
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/router.py +41 -15
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/task_store.py +9 -7
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/acp.py +6 -6
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/google_a2a.py +8 -6
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/client/a2a_client.py +19 -8
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/models/a2a_protocol.py +11 -9
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/models/agent_card.py +2 -1
- theprotocol_sdk-0.2.0/src/theprotocol/models/v1_compat.py +230 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/tests/test_bridge_anp.py +3 -3
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/tests/test_bridge_google_a2a.py +1 -1
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/tests/test_models.py +2 -2
- theprotocol_sdk-0.1.0/PKG-INFO +0 -96
- theprotocol_sdk-0.1.0/README.md +0 -61
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/.gitignore +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/examples/simple_agent.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/__init__.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/__init__.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/base.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/agent/decorators.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/__init__.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/__init__.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/auth.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/bridge.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/did_wba.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/anp/translation.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/base.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/bridges/mcp.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/client/__init__.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/client/credentials.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/exceptions.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/src/theprotocol/models/__init__.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/tests/test_agent.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/tests/test_bridge_acp.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/tests/test_bridge_mcp.py +0 -0
- {theprotocol_sdk-0.1.0 → theprotocol_sdk-0.2.0}/tests/test_credentials.py +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: theprotocol-sdk
|
|
3
|
+
Version: 0.2.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
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: a2a,agents,ai,mcp,protocol,theprotocol
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx[http2]>=0.27
|
|
22
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0
|
|
24
|
+
Provides-Extra: all
|
|
25
|
+
Requires-Dist: cryptography>=42.0; extra == 'all'
|
|
26
|
+
Requires-Dist: fastapi>=0.111; extra == 'all'
|
|
27
|
+
Requires-Dist: keyring>=24; extra == 'all'
|
|
28
|
+
Provides-Extra: anp
|
|
29
|
+
Requires-Dist: cryptography>=42.0; extra == 'anp'
|
|
30
|
+
Provides-Extra: keyring
|
|
31
|
+
Requires-Dist: keyring>=24; extra == 'keyring'
|
|
32
|
+
Provides-Extra: server
|
|
33
|
+
Requires-Dist: fastapi>=0.111; extra == 'server'
|
|
34
|
+
Provides-Extra: test
|
|
35
|
+
Requires-Dist: cryptography>=42.0; extra == 'test'
|
|
36
|
+
Requires-Dist: httpx>=0.27; extra == 'test'
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
|
|
38
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
39
|
+
Requires-Dist: respx>=0.20; extra == 'test'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# theprotocol-sdk
|
|
43
|
+
|
|
44
|
+
**A2A v1.0** — Build and call AI agents on [TheProtocol](https://theprotocol.cloud). Native protocol bridges for Google A2A, ANP, and MCP.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install theprotocol-sdk # Client only (call agents)
|
|
50
|
+
pip install theprotocol-sdk[server] # + FastAPI router (build agents)
|
|
51
|
+
pip install theprotocol-sdk[anp] # + Ed25519 crypto for ANP DID:WBA
|
|
52
|
+
pip install theprotocol-sdk[all] # Everything
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Build an Agent
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from theprotocol.agent import BaseA2AAgent, create_a2a_router
|
|
59
|
+
from fastapi import FastAPI
|
|
60
|
+
|
|
61
|
+
class MyAgent(BaseA2AAgent):
|
|
62
|
+
async def handle_task_send(self, task_id, message):
|
|
63
|
+
return "task-1"
|
|
64
|
+
async def handle_task_get(self, task_id): ...
|
|
65
|
+
async def handle_task_cancel(self, task_id): return True
|
|
66
|
+
async def handle_subscribe_request(self, task_id): yield
|
|
67
|
+
|
|
68
|
+
app = FastAPI()
|
|
69
|
+
app.include_router(create_a2a_router(MyAgent()))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Your agent speaks A2A v1.0 out of the box. It accepts both `message/send` (v1.0) and `tasks/send` (v0.3) for backward compatibility.
|
|
73
|
+
|
|
74
|
+
## Call a Remote Agent
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from theprotocol.client import A2AClient, KeyManager
|
|
78
|
+
from theprotocol.models import Message, TextPart
|
|
79
|
+
|
|
80
|
+
async with A2AClient() as client:
|
|
81
|
+
task_id = await client.initiate_task(agent_card, message, key_manager)
|
|
82
|
+
task = await client.get_task_status(agent_card, task_id, key_manager)
|
|
83
|
+
print(task.state) # TASK_STATE_COMPLETED
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The client sends v1.0 wire format and accepts responses from both v1.0 and v0.3 agents.
|
|
87
|
+
|
|
88
|
+
## Dockerize
|
|
89
|
+
|
|
90
|
+
```dockerfile
|
|
91
|
+
FROM python:3.11-slim
|
|
92
|
+
WORKDIR /app
|
|
93
|
+
RUN pip install --no-cache-dir theprotocol-sdk[server] uvicorn
|
|
94
|
+
COPY agent.py .
|
|
95
|
+
EXPOSE 9500
|
|
96
|
+
CMD ["uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "9500"]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Register on TheProtocol and your agent gets a permanent DID, OAuth credentials, and a 1,000 AVT genesis grant.
|
|
100
|
+
|
|
101
|
+
## Protocol Bridges
|
|
102
|
+
|
|
103
|
+
Translate between A2A and other agent protocols:
|
|
104
|
+
|
|
105
|
+
| Bridge | Protocol | Use Case |
|
|
106
|
+
|--------|----------|----------|
|
|
107
|
+
| `GoogleA2ABridge` | Google A2A REST | Expose agents via REST binding (Vertex AI, AgentCore) |
|
|
108
|
+
| `ANPBridge` | Agent Network Protocol | DID:WBA identity linking, Ed25519 auth |
|
|
109
|
+
| `MCPBridge` | Model Context Protocol | Expose agents as MCP tool servers |
|
|
110
|
+
| `ACPBridge` | ACP (deprecated) | Legacy BeeAI compat — use GoogleA2ABridge instead |
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from theprotocol.bridges.google_a2a import GoogleA2ABridge
|
|
114
|
+
from theprotocol.bridges.anp import ANPBridge
|
|
115
|
+
from theprotocol.bridges.mcp import MCPBridge
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Platform Compatibility
|
|
119
|
+
|
|
120
|
+
Any platform that speaks A2A v1.0 can call your agent directly:
|
|
121
|
+
|
|
122
|
+
- **Google Vertex AI** — native A2A support
|
|
123
|
+
- **AWS Bedrock AgentCore** — native A2A support
|
|
124
|
+
- **LangGraph Cloud** — native A2A support
|
|
125
|
+
- **CrewAI** — native A2A support
|
|
126
|
+
- **Azure AI Foundry** — A2A in preview
|
|
127
|
+
|
|
128
|
+
No additional bridges needed. The SDK's JSON-RPC endpoint is the universal interface.
|
|
129
|
+
|
|
130
|
+
## MCP Tools
|
|
131
|
+
|
|
132
|
+
For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/mcp/sse) (19 tools via Claude Desktop or any MCP client).
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
Apache-2.0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# theprotocol-sdk
|
|
2
|
+
|
|
3
|
+
**A2A v1.0** — Build and call AI agents on [TheProtocol](https://theprotocol.cloud). Native protocol bridges for Google A2A, ANP, and MCP.
|
|
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[anp] # + Ed25519 crypto for ANP DID:WBA
|
|
11
|
+
pip install theprotocol-sdk[all] # Everything
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Build an Agent
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from theprotocol.agent import BaseA2AAgent, create_a2a_router
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
|
|
20
|
+
class MyAgent(BaseA2AAgent):
|
|
21
|
+
async def handle_task_send(self, task_id, message):
|
|
22
|
+
return "task-1"
|
|
23
|
+
async def handle_task_get(self, task_id): ...
|
|
24
|
+
async def handle_task_cancel(self, task_id): return True
|
|
25
|
+
async def handle_subscribe_request(self, task_id): yield
|
|
26
|
+
|
|
27
|
+
app = FastAPI()
|
|
28
|
+
app.include_router(create_a2a_router(MyAgent()))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Your agent speaks A2A v1.0 out of the box. It accepts both `message/send` (v1.0) and `tasks/send` (v0.3) for backward compatibility.
|
|
32
|
+
|
|
33
|
+
## Call a Remote Agent
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from theprotocol.client import A2AClient, KeyManager
|
|
37
|
+
from theprotocol.models import Message, TextPart
|
|
38
|
+
|
|
39
|
+
async with A2AClient() as client:
|
|
40
|
+
task_id = await client.initiate_task(agent_card, message, key_manager)
|
|
41
|
+
task = await client.get_task_status(agent_card, task_id, key_manager)
|
|
42
|
+
print(task.state) # TASK_STATE_COMPLETED
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The client sends v1.0 wire format and accepts responses from both v1.0 and v0.3 agents.
|
|
46
|
+
|
|
47
|
+
## Dockerize
|
|
48
|
+
|
|
49
|
+
```dockerfile
|
|
50
|
+
FROM python:3.11-slim
|
|
51
|
+
WORKDIR /app
|
|
52
|
+
RUN pip install --no-cache-dir theprotocol-sdk[server] uvicorn
|
|
53
|
+
COPY agent.py .
|
|
54
|
+
EXPOSE 9500
|
|
55
|
+
CMD ["uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "9500"]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Register on TheProtocol and your agent gets a permanent DID, OAuth credentials, and a 1,000 AVT genesis grant.
|
|
59
|
+
|
|
60
|
+
## Protocol Bridges
|
|
61
|
+
|
|
62
|
+
Translate between A2A and other agent protocols:
|
|
63
|
+
|
|
64
|
+
| Bridge | Protocol | Use Case |
|
|
65
|
+
|--------|----------|----------|
|
|
66
|
+
| `GoogleA2ABridge` | Google A2A REST | Expose agents via REST binding (Vertex AI, AgentCore) |
|
|
67
|
+
| `ANPBridge` | Agent Network Protocol | DID:WBA identity linking, Ed25519 auth |
|
|
68
|
+
| `MCPBridge` | Model Context Protocol | Expose agents as MCP tool servers |
|
|
69
|
+
| `ACPBridge` | ACP (deprecated) | Legacy BeeAI compat — use GoogleA2ABridge instead |
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from theprotocol.bridges.google_a2a import GoogleA2ABridge
|
|
73
|
+
from theprotocol.bridges.anp import ANPBridge
|
|
74
|
+
from theprotocol.bridges.mcp import MCPBridge
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Platform Compatibility
|
|
78
|
+
|
|
79
|
+
Any platform that speaks A2A v1.0 can call your agent directly:
|
|
80
|
+
|
|
81
|
+
- **Google Vertex AI** — native A2A support
|
|
82
|
+
- **AWS Bedrock AgentCore** — native A2A support
|
|
83
|
+
- **LangGraph Cloud** — native A2A support
|
|
84
|
+
- **CrewAI** — native A2A support
|
|
85
|
+
- **Azure AI Foundry** — A2A in preview
|
|
86
|
+
|
|
87
|
+
No additional bridges needed. The SDK's JSON-RPC endpoint is the universal interface.
|
|
88
|
+
|
|
89
|
+
## MCP Tools
|
|
90
|
+
|
|
91
|
+
For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/mcp/sse) (19 tools via Claude Desktop or any MCP client).
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "theprotocol-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "TheProtocol SDK — Build and call A2A agents. Protocol bridges for ACP, ADK, and more."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -32,11 +32,14 @@ dependencies = [
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
server = ["fastapi>=0.111"]
|
|
34
34
|
keyring = ["keyring>=24"]
|
|
35
|
-
|
|
35
|
+
anp = ["cryptography>=42.0"]
|
|
36
|
+
all = ["theprotocol-sdk[server,keyring,anp]"]
|
|
36
37
|
test = [
|
|
37
38
|
"pytest>=7.0",
|
|
38
39
|
"pytest-asyncio>=0.21",
|
|
39
40
|
"respx>=0.20",
|
|
41
|
+
"cryptography>=42.0",
|
|
42
|
+
"httpx>=0.27",
|
|
40
43
|
]
|
|
41
44
|
|
|
42
45
|
[project.urls]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""FastAPI router factory for A2A agents.
|
|
1
|
+
"""FastAPI router factory for A2A agents.
|
|
2
|
+
Accepts both A2A v1.0 and v0.3 wire formats. Responds in v1.0."""
|
|
2
3
|
|
|
3
4
|
import json
|
|
4
5
|
import inspect
|
|
@@ -18,6 +19,10 @@ from ..models import (
|
|
|
18
19
|
TaskCancelParams, TaskCancelResult,
|
|
19
20
|
TaskStatusUpdateEvent, TaskMessageEvent, TaskArtifactUpdateEvent,
|
|
20
21
|
)
|
|
22
|
+
from ..models.v1_compat import (
|
|
23
|
+
method_from_v1, message_from_v1, task_to_v1, sse_event_to_v1,
|
|
24
|
+
state_to_v1,
|
|
25
|
+
)
|
|
21
26
|
from ..exceptions import AgentServerError, TaskNotFoundError
|
|
22
27
|
|
|
23
28
|
logger = logging.getLogger(__name__)
|
|
@@ -44,18 +49,22 @@ def _jsonrpc_success(req_id: Union[str, int, None], result: Any) -> Dict:
|
|
|
44
49
|
|
|
45
50
|
|
|
46
51
|
def _format_sse_bytes(event: A2AEvent) -> Optional[bytes]:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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:
|
|
52
|
+
# v1.0 SSE format: field name is the discriminator (statusUpdate, message, artifactUpdate)
|
|
53
|
+
v1_data = sse_event_to_v1(event)
|
|
54
|
+
if v1_data is None:
|
|
55
55
|
return None
|
|
56
56
|
try:
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
# Determine event type for SSE event: field
|
|
58
|
+
if "statusUpdate" in v1_data:
|
|
59
|
+
event_type = "status_update"
|
|
60
|
+
elif "message" in v1_data:
|
|
61
|
+
event_type = "message"
|
|
62
|
+
elif "artifactUpdate" in v1_data:
|
|
63
|
+
event_type = "artifact_update"
|
|
64
|
+
else:
|
|
65
|
+
event_type = "event"
|
|
66
|
+
json_str = json.dumps(v1_data)
|
|
67
|
+
return f"event: {event_type}\ndata: {json_str}\n\n".encode("utf-8")
|
|
59
68
|
except Exception:
|
|
60
69
|
return None
|
|
61
70
|
|
|
@@ -121,11 +130,20 @@ def create_a2a_router(
|
|
|
121
130
|
return JSONResponse(_jsonrpc_error(None, JSONRPC_INVALID_REQUEST, "Payload must be JSON object"))
|
|
122
131
|
|
|
123
132
|
req_id = payload.get("id")
|
|
124
|
-
|
|
133
|
+
raw_method = payload.get("method", "")
|
|
134
|
+
method = method_from_v1(raw_method) # v1.0 → v0.3 method name translation
|
|
125
135
|
params = payload.get("params") or {}
|
|
126
136
|
if isinstance(params, list):
|
|
127
137
|
params = {}
|
|
128
138
|
|
|
139
|
+
# v1.0 compat: if params contain v1.0 part format, translate message
|
|
140
|
+
if "message" in params and isinstance(params["message"], dict):
|
|
141
|
+
msg = params["message"]
|
|
142
|
+
parts = msg.get("parts", [])
|
|
143
|
+
if parts and isinstance(parts[0], dict) and "type" not in parts[0]:
|
|
144
|
+
# v1.0 part format (no type field) — translate to v0.3
|
|
145
|
+
params["message"] = message_from_v1(msg).model_dump(mode="json")
|
|
146
|
+
|
|
129
147
|
if payload.get("jsonrpc") != "2.0":
|
|
130
148
|
return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_REQUEST, "'jsonrpc' must be '2.0'"))
|
|
131
149
|
if not isinstance(method, str) or not method:
|
|
@@ -156,17 +174,25 @@ def create_a2a_router(
|
|
|
156
174
|
elif method == "tasks/send":
|
|
157
175
|
vp = TaskSendParams.model_validate(params)
|
|
158
176
|
task_id = await agent.handle_task_send(task_id=vp.id, message=vp.message)
|
|
159
|
-
return
|
|
177
|
+
# v1.0: try to return full task with v1.0 state format
|
|
178
|
+
try:
|
|
179
|
+
task = await agent.handle_task_get(task_id)
|
|
180
|
+
return JSONResponse(_jsonrpc_success(req_id, task_to_v1(task)))
|
|
181
|
+
except Exception:
|
|
182
|
+
return JSONResponse(_jsonrpc_success(req_id, {"id": task_id}))
|
|
160
183
|
|
|
161
184
|
elif method == "tasks/get":
|
|
162
185
|
vp = TaskGetParams.model_validate(params)
|
|
163
186
|
task = await agent.handle_task_get(task_id=vp.id)
|
|
164
|
-
return JSONResponse(_jsonrpc_success(req_id, task
|
|
187
|
+
return JSONResponse(_jsonrpc_success(req_id, task_to_v1(task)))
|
|
165
188
|
|
|
166
189
|
elif method == "tasks/cancel":
|
|
167
190
|
vp = TaskCancelParams.model_validate(params)
|
|
168
191
|
ok = await agent.handle_task_cancel(task_id=vp.id)
|
|
169
|
-
|
|
192
|
+
result = TaskCancelResult(success=ok).model_dump(mode='json')
|
|
193
|
+
if ok:
|
|
194
|
+
result["status"] = {"state": state_to_v1(TaskState.CANCELED)}
|
|
195
|
+
return JSONResponse(_jsonrpc_success(req_id, result))
|
|
170
196
|
|
|
171
197
|
elif method == "tasks/sendSubscribe":
|
|
172
198
|
task_id = params.get("id", "")
|
|
@@ -16,14 +16,16 @@ from ..exceptions import InvalidStateTransitionError
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
18
|
ALLOWED_TRANSITIONS = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
TaskState.SUBMITTED.value: {TaskState.WORKING.value, TaskState.CANCELED.value, TaskState.REJECTED.value},
|
|
20
|
+
TaskState.WORKING.value: {TaskState.INPUT_REQUIRED.value, TaskState.COMPLETED.value, TaskState.FAILED.value, TaskState.CANCELED.value},
|
|
21
|
+
TaskState.INPUT_REQUIRED.value: {TaskState.WORKING.value, TaskState.CANCELED.value, TaskState.AUTH_REQUIRED.value},
|
|
22
|
+
TaskState.AUTH_REQUIRED.value: {TaskState.WORKING.value, TaskState.CANCELED.value},
|
|
23
|
+
TaskState.COMPLETED.value: {TaskState.COMPLETED.value},
|
|
24
|
+
TaskState.FAILED.value: {TaskState.FAILED.value},
|
|
25
|
+
TaskState.CANCELED.value: {TaskState.CANCELED.value},
|
|
26
|
+
TaskState.REJECTED.value: {TaskState.REJECTED.value},
|
|
25
27
|
}
|
|
26
|
-
TERMINAL_STATES = {
|
|
28
|
+
TERMINAL_STATES = {TaskState.COMPLETED.value, TaskState.FAILED.value, TaskState.CANCELED.value, TaskState.REJECTED.value}
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
@dataclass
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
2
|
ACP (Agent Communication Protocol) ↔ A2A Bridge.
|
|
3
3
|
|
|
4
|
+
DEPRECATED: ACP merged into A2A under Linux Foundation (Aug 2025).
|
|
5
|
+
BeeAI now speaks A2A natively. Use GoogleA2ABridge for BeeAI agents.
|
|
6
|
+
This bridge is preserved for backward compatibility with pre-merger ACP deployments.
|
|
7
|
+
|
|
4
8
|
ACP spec: https://github.com/i-am-bee/acp (IBM/BeeAI)
|
|
5
9
|
Wire format: REST (HTTP + JSON), no JSON-RPC
|
|
6
|
-
|
|
7
|
-
Translates:
|
|
8
|
-
ACP POST /runs → A2A tasks/send
|
|
9
|
-
ACP GET /runs/{id} → A2A tasks/get
|
|
10
|
-
ACP POST /runs/{id}/cancel → A2A tasks/cancel
|
|
11
|
-
ACP GET /agents → A2A AgentCard
|
|
12
10
|
"""
|
|
13
11
|
|
|
14
12
|
import datetime
|
|
@@ -43,6 +41,8 @@ A2A_TO_ACP_STATE = {
|
|
|
43
41
|
TaskState.COMPLETED: "completed",
|
|
44
42
|
TaskState.FAILED: "failed",
|
|
45
43
|
TaskState.CANCELED: "cancelled",
|
|
44
|
+
TaskState.REJECTED: "failed", # v1.0: map to ACP failed
|
|
45
|
+
TaskState.AUTH_REQUIRED: "awaiting", # v1.0: map to ACP awaiting
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
|
|
@@ -26,7 +26,7 @@ from .base import BaseBridge
|
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
|
-
# Google A2A state (lowercase-hyphen) →
|
|
29
|
+
# Google A2A REST binding state (lowercase-hyphen) → internal TaskState (v1.0)
|
|
30
30
|
GOOGLE_TO_TP_STATE = {
|
|
31
31
|
"submitted": TaskState.SUBMITTED,
|
|
32
32
|
"working": TaskState.WORKING,
|
|
@@ -34,8 +34,8 @@ GOOGLE_TO_TP_STATE = {
|
|
|
34
34
|
"failed": TaskState.FAILED,
|
|
35
35
|
"canceled": TaskState.CANCELED,
|
|
36
36
|
"input-required": TaskState.INPUT_REQUIRED,
|
|
37
|
-
"rejected": TaskState.
|
|
38
|
-
"auth-required": TaskState.
|
|
37
|
+
"rejected": TaskState.REJECTED,
|
|
38
|
+
"auth-required": TaskState.AUTH_REQUIRED,
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
TP_TO_GOOGLE_STATE = {
|
|
@@ -45,6 +45,8 @@ TP_TO_GOOGLE_STATE = {
|
|
|
45
45
|
TaskState.FAILED: "failed",
|
|
46
46
|
TaskState.CANCELED: "canceled",
|
|
47
47
|
TaskState.INPUT_REQUIRED: "input-required",
|
|
48
|
+
TaskState.REJECTED: "rejected",
|
|
49
|
+
TaskState.AUTH_REQUIRED: "auth-required",
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
|
|
@@ -84,8 +86,8 @@ def tp_part_to_google_part(part: Any) -> Dict:
|
|
|
84
86
|
def google_message_to_tp(gmsg: Dict) -> Message:
|
|
85
87
|
"""Convert Google A2A Message to TheProtocol Message."""
|
|
86
88
|
role = gmsg.get("role", "user")
|
|
87
|
-
# Google roles: "user", "agent" → TP roles: "user", "
|
|
88
|
-
tp_role = "user" if role == "user" else "
|
|
89
|
+
# Google roles: "user", "agent" → TP roles: "user", "agent"
|
|
90
|
+
tp_role = "user" if role == "user" else "agent"
|
|
89
91
|
parts = [google_part_to_tp_part(p) for p in gmsg.get("parts", [])]
|
|
90
92
|
if not parts:
|
|
91
93
|
parts = [TextPart(content="")]
|
|
@@ -95,7 +97,7 @@ def google_message_to_tp(gmsg: Dict) -> Message:
|
|
|
95
97
|
|
|
96
98
|
def tp_message_to_google(msg: Message) -> Dict:
|
|
97
99
|
"""Convert TheProtocol Message to Google A2A Message."""
|
|
98
|
-
role = "user" if msg.role == "user" else "agent"
|
|
100
|
+
role = "user" if msg.role == "user" else "agent" # v1.0: "agent" (not "assistant")
|
|
99
101
|
return {
|
|
100
102
|
"message_id": str(uuid.uuid4()),
|
|
101
103
|
"role": role,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
A2AClient — Call remote A2A agents via JSON-RPC 2.0 + SSE streaming.
|
|
3
|
+
Sends A2A v1.0 wire format. Accepts both v1.0 and v0.3 responses.
|
|
3
4
|
"""
|
|
4
5
|
|
|
5
6
|
import json
|
|
@@ -18,6 +19,7 @@ from ..models import (
|
|
|
18
19
|
TaskCancelParams, TaskCancelResult, A2AEvent,
|
|
19
20
|
TaskStatusUpdateEvent, TaskMessageEvent, TaskArtifactUpdateEvent,
|
|
20
21
|
)
|
|
22
|
+
from ..models.v1_compat import message_to_v1, state_from_v1, part_from_v1, role_from_v1
|
|
21
23
|
from ..exceptions import (
|
|
22
24
|
A2AError, A2AConnectionError, A2AAuthenticationError,
|
|
23
25
|
A2ARemoteAgentError, A2ATimeoutError, A2AMessageError,
|
|
@@ -28,10 +30,14 @@ from .credentials import KeyManager
|
|
|
28
30
|
logger = logging.getLogger(__name__)
|
|
29
31
|
|
|
30
32
|
SSE_EVENT_MAP = {
|
|
33
|
+
# v0.3 event types
|
|
31
34
|
"task_status": TaskStatusUpdateEvent,
|
|
32
35
|
"task_message": TaskMessageEvent,
|
|
33
36
|
"task_artifact": TaskArtifactUpdateEvent,
|
|
37
|
+
# v1.0 event types
|
|
38
|
+
"status_update": TaskStatusUpdateEvent,
|
|
34
39
|
"message": TaskMessageEvent,
|
|
40
|
+
"artifact_update": TaskArtifactUpdateEvent,
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
|
|
@@ -69,20 +75,24 @@ class A2AClient:
|
|
|
69
75
|
async def initiate_task(
|
|
70
76
|
self, agent_card: AgentCard, message: Message, key_manager: KeyManager
|
|
71
77
|
) -> str:
|
|
72
|
-
"""Start a new task on a remote agent. Returns the task ID.
|
|
78
|
+
"""Start a new task on a remote agent. Returns the task ID.
|
|
79
|
+
Sends v1.0 wire format (message/send with unified parts)."""
|
|
73
80
|
headers = await self._auth_headers(agent_card, key_manager)
|
|
74
|
-
|
|
75
|
-
result = await self._rpc(agent_card.url, "
|
|
81
|
+
v1_params = {"message": message_to_v1(message)}
|
|
82
|
+
result = await self._rpc(agent_card.url, "message/send", v1_params, headers)
|
|
83
|
+
# Accept both v1.0 (result.id or result["id"]) and v0.3 (TaskSendResult)
|
|
84
|
+
if isinstance(result, dict) and "id" in result:
|
|
85
|
+
return result["id"]
|
|
76
86
|
return TaskSendResult.model_validate(result).id
|
|
77
87
|
|
|
78
88
|
async def send_message(
|
|
79
89
|
self, agent_card: AgentCard, task_id: str, message: Message, key_manager: KeyManager
|
|
80
90
|
) -> bool:
|
|
81
|
-
"""Send a follow-up message to an existing task.
|
|
91
|
+
"""Send a follow-up message to an existing task.
|
|
92
|
+
Sends v1.0 wire format."""
|
|
82
93
|
headers = await self._auth_headers(agent_card, key_manager)
|
|
83
|
-
|
|
84
|
-
result = await self._rpc(agent_card.url, "
|
|
85
|
-
TaskSendResult.model_validate(result)
|
|
94
|
+
v1_params = {"id": task_id, "message": message_to_v1(message)}
|
|
95
|
+
result = await self._rpc(agent_card.url, "message/send", v1_params, headers)
|
|
86
96
|
return True
|
|
87
97
|
|
|
88
98
|
async def get_task_status(
|
|
@@ -110,7 +120,8 @@ class A2AClient:
|
|
|
110
120
|
headers = await self._auth_headers(agent_card, key_manager)
|
|
111
121
|
headers["Accept"] = "text/event-stream"
|
|
112
122
|
url = self._ensure_trailing_slash(str(agent_card.url))
|
|
113
|
-
|
|
123
|
+
# v1.0: message/stream (falls back to tasks/sendSubscribe on v0.3 servers)
|
|
124
|
+
payload = {"jsonrpc": "2.0", "method": "message/stream", "params": {"id": task_id}, "id": f"sub-{uuid.uuid4()}"}
|
|
114
125
|
|
|
115
126
|
async with self._client.stream('POST', url, headers=headers, json=payload, timeout=None) as resp:
|
|
116
127
|
resp.raise_for_status()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Pydantic models for the Agent-to-Agent (A2A) protocol.
|
|
3
|
-
|
|
3
|
+
Native A2A v1.0 specification. Backward compatible with v0.3 input.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import datetime
|
|
@@ -10,13 +10,15 @@ from pydantic import BaseModel, Field, HttpUrl, ConfigDict
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TaskState(str, Enum):
|
|
13
|
-
"""
|
|
14
|
-
SUBMITTED = "
|
|
15
|
-
WORKING = "
|
|
16
|
-
INPUT_REQUIRED = "
|
|
17
|
-
COMPLETED = "
|
|
18
|
-
FAILED = "
|
|
19
|
-
CANCELED = "
|
|
13
|
+
"""A2A v1.0 task states."""
|
|
14
|
+
SUBMITTED = "TASK_STATE_SUBMITTED"
|
|
15
|
+
WORKING = "TASK_STATE_WORKING"
|
|
16
|
+
INPUT_REQUIRED = "TASK_STATE_INPUT_REQUIRED"
|
|
17
|
+
COMPLETED = "TASK_STATE_COMPLETED"
|
|
18
|
+
FAILED = "TASK_STATE_FAILED"
|
|
19
|
+
CANCELED = "TASK_STATE_CANCELED"
|
|
20
|
+
REJECTED = "TASK_STATE_REJECTED"
|
|
21
|
+
AUTH_REQUIRED = "TASK_STATE_AUTH_REQUIRED"
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
# ── Message Parts ──
|
|
@@ -60,7 +62,7 @@ class Artifact(BaseModel):
|
|
|
60
62
|
|
|
61
63
|
class Message(BaseModel):
|
|
62
64
|
model_config = ConfigDict(frozen=True)
|
|
63
|
-
role: Literal['user', '
|
|
65
|
+
role: Literal['user', 'agent', 'assistant'] = Field(...) # v1.0: user/agent. assistant accepted for backward compat.
|
|
64
66
|
parts: List[Part] = Field(..., min_length=1)
|
|
65
67
|
metadata: Optional[Dict[str, Any]] = None
|
|
66
68
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Pydantic models for A2A Agent Cards.
|
|
3
|
-
|
|
3
|
+
Supports A2A v1.0 wire format with v0.3 internal fields.
|
|
4
|
+
Full v1.0 Agent Card schema (supported_interfaces, security_schemes) planned for SDK v2.0.
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
7
|
from typing import List, Optional, Dict, Any, Literal
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A2A v0.3 Backward Compatibility Layer.
|
|
3
|
+
|
|
4
|
+
Internal models are now native v1.0.
|
|
5
|
+
This module translates v0.3 wire format INPUT to v1.0 internal types,
|
|
6
|
+
so old clients (tasks/send, {type:"text"}, COMPLETED) still work.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
from .a2a_protocol import (
|
|
11
|
+
TaskState, Message, TextPart, FilePart, DataPart, Part,
|
|
12
|
+
Task, Artifact, TaskStatusUpdateEvent, TaskMessageEvent, TaskArtifactUpdateEvent,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# ─── State Translation ──────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
# Accept both v1.0 and v0.3 state strings → internal TaskState (now v1.0 native)
|
|
18
|
+
STATE_STRING_TO_INTERNAL: Dict[str, TaskState] = {
|
|
19
|
+
# v1.0 values (native)
|
|
20
|
+
"TASK_STATE_SUBMITTED": TaskState.SUBMITTED,
|
|
21
|
+
"TASK_STATE_WORKING": TaskState.WORKING,
|
|
22
|
+
"TASK_STATE_INPUT_REQUIRED": TaskState.INPUT_REQUIRED,
|
|
23
|
+
"TASK_STATE_COMPLETED": TaskState.COMPLETED,
|
|
24
|
+
"TASK_STATE_FAILED": TaskState.FAILED,
|
|
25
|
+
"TASK_STATE_CANCELED": TaskState.CANCELED,
|
|
26
|
+
"TASK_STATE_REJECTED": TaskState.REJECTED,
|
|
27
|
+
"TASK_STATE_AUTH_REQUIRED": TaskState.AUTH_REQUIRED,
|
|
28
|
+
# v0.3 values (backward compat)
|
|
29
|
+
"SUBMITTED": TaskState.SUBMITTED,
|
|
30
|
+
"WORKING": TaskState.WORKING,
|
|
31
|
+
"INPUT_REQUIRED": TaskState.INPUT_REQUIRED,
|
|
32
|
+
"COMPLETED": TaskState.COMPLETED,
|
|
33
|
+
"FAILED": TaskState.FAILED,
|
|
34
|
+
"CANCELED": TaskState.CANCELED,
|
|
35
|
+
"REJECTED": TaskState.REJECTED,
|
|
36
|
+
"AUTH_REQUIRED": TaskState.AUTH_REQUIRED,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def state_from_v1(v1_state: str) -> TaskState:
|
|
41
|
+
"""Convert any state string (v0.3 or v1.0) to internal TaskState."""
|
|
42
|
+
return STATE_STRING_TO_INTERNAL.get(v1_state, TaskState.WORKING)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def state_to_v1(state: TaskState) -> str:
|
|
46
|
+
"""Return the v1.0 wire value (TaskState is already v1.0 native)."""
|
|
47
|
+
return state.value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ─── Role Translation ───────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
# Accept all role variants → internal v1.0 role (user/agent)
|
|
53
|
+
ROLE_TO_INTERNAL = {"user": "user", "agent": "agent", "assistant": "agent", "ROLE_USER": "user", "ROLE_AGENT": "agent", "system": "user", "tool": "user"}
|
|
54
|
+
INTERNAL_ROLE_TO_WIRE = {"user": "user", "agent": "agent", "assistant": "agent"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def role_from_v1(v1_role: str) -> str:
|
|
58
|
+
"""Convert any role string (v0.3 or v1.0) to internal v1.0 role."""
|
|
59
|
+
return ROLE_TO_INTERNAL.get(v1_role, "user")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def role_to_v1(role: str) -> str:
|
|
63
|
+
"""Return v1.0 wire role."""
|
|
64
|
+
return INTERNAL_ROLE_TO_WIRE.get(role, "user")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ─── Part Translation ────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
def part_from_v1(v1_part: Dict[str, Any]) -> Part:
|
|
70
|
+
"""Convert v1.0 unified Part to internal typed Part.
|
|
71
|
+
|
|
72
|
+
v1.0: {"text": "hello"} or {"url": "...", "media_type": "..."} or {"data": {...}}
|
|
73
|
+
v0.3: TextPart(type="text", content="hello") etc.
|
|
74
|
+
"""
|
|
75
|
+
# v1.0 format: field name IS the discriminator
|
|
76
|
+
if "text" in v1_part:
|
|
77
|
+
return TextPart(content=v1_part["text"])
|
|
78
|
+
elif "url" in v1_part:
|
|
79
|
+
return FilePart(
|
|
80
|
+
url=v1_part["url"],
|
|
81
|
+
media_type=v1_part.get("media_type") or v1_part.get("mediaType"),
|
|
82
|
+
filename=v1_part.get("filename"),
|
|
83
|
+
)
|
|
84
|
+
elif "data" in v1_part:
|
|
85
|
+
data = v1_part["data"]
|
|
86
|
+
if not isinstance(data, dict):
|
|
87
|
+
data = {"value": data}
|
|
88
|
+
return DataPart(
|
|
89
|
+
content=data,
|
|
90
|
+
media_type=v1_part.get("media_type") or v1_part.get("mediaType") or "application/json",
|
|
91
|
+
)
|
|
92
|
+
elif "raw" in v1_part:
|
|
93
|
+
# v1.0 raw bytes → store as DataPart with base64
|
|
94
|
+
return DataPart(
|
|
95
|
+
content={"raw_base64": v1_part["raw"], "media_type": v1_part.get("media_type")},
|
|
96
|
+
media_type=v1_part.get("media_type") or "application/octet-stream",
|
|
97
|
+
)
|
|
98
|
+
# Fallback: check v0.3 format (type field)
|
|
99
|
+
elif "type" in v1_part:
|
|
100
|
+
ptype = v1_part["type"]
|
|
101
|
+
if ptype == "text":
|
|
102
|
+
return TextPart(content=v1_part.get("content", ""))
|
|
103
|
+
elif ptype == "file":
|
|
104
|
+
return FilePart(
|
|
105
|
+
url=v1_part.get("url", ""),
|
|
106
|
+
media_type=v1_part.get("mediaType") or v1_part.get("media_type"),
|
|
107
|
+
filename=v1_part.get("filename"),
|
|
108
|
+
)
|
|
109
|
+
elif ptype == "data":
|
|
110
|
+
return DataPart(
|
|
111
|
+
content=v1_part.get("content", {}),
|
|
112
|
+
media_type=v1_part.get("mediaType") or v1_part.get("media_type") or "application/json",
|
|
113
|
+
)
|
|
114
|
+
return TextPart(content="")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def part_to_v1(part: Part) -> Dict[str, Any]:
|
|
118
|
+
"""Convert internal typed Part to v1.0 unified Part."""
|
|
119
|
+
if isinstance(part, TextPart):
|
|
120
|
+
return {"text": part.content}
|
|
121
|
+
elif isinstance(part, FilePart):
|
|
122
|
+
r: Dict[str, Any] = {"url": str(part.url)}
|
|
123
|
+
if part.media_type:
|
|
124
|
+
r["media_type"] = part.media_type
|
|
125
|
+
if part.filename:
|
|
126
|
+
r["filename"] = part.filename
|
|
127
|
+
return r
|
|
128
|
+
elif isinstance(part, DataPart):
|
|
129
|
+
r = {"data": part.content}
|
|
130
|
+
if part.media_type and part.media_type != "application/json":
|
|
131
|
+
r["media_type"] = part.media_type
|
|
132
|
+
return r
|
|
133
|
+
return {"text": ""}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ─── Message Translation ────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
def message_from_v1(v1_msg: Dict[str, Any]) -> Message:
|
|
139
|
+
"""Convert v1.0 message to internal Message."""
|
|
140
|
+
role = role_from_v1(v1_msg.get("role", "user"))
|
|
141
|
+
parts_raw = v1_msg.get("parts", [])
|
|
142
|
+
parts = [part_from_v1(p) for p in parts_raw] if parts_raw else [TextPart(content="")]
|
|
143
|
+
return Message(role=role, parts=parts, metadata=v1_msg.get("metadata"))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def message_to_v1(msg: Message) -> Dict[str, Any]:
|
|
147
|
+
"""Convert internal Message to v1.0 wire format."""
|
|
148
|
+
return {
|
|
149
|
+
"role": role_to_v1(msg.role),
|
|
150
|
+
"parts": [part_to_v1(p) for p in msg.parts],
|
|
151
|
+
"metadata": msg.metadata,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ─── Task Translation ───────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def task_to_v1(task: Task) -> Dict[str, Any]:
|
|
158
|
+
"""Convert internal Task to v1.0 wire format."""
|
|
159
|
+
return {
|
|
160
|
+
"id": task.id,
|
|
161
|
+
"status": {
|
|
162
|
+
"state": state_to_v1(task.state),
|
|
163
|
+
"timestamp": task.updated_at.isoformat() if task.updated_at else None,
|
|
164
|
+
},
|
|
165
|
+
"history": [message_to_v1(m) for m in task.messages],
|
|
166
|
+
"artifacts": [
|
|
167
|
+
{
|
|
168
|
+
"artifact_id": a.id,
|
|
169
|
+
"name": a.type,
|
|
170
|
+
"parts": [part_to_v1(TextPart(content=str(a.content)))] if a.content else
|
|
171
|
+
[{"url": str(a.url)}] if a.url else [],
|
|
172
|
+
"metadata": a.metadata,
|
|
173
|
+
}
|
|
174
|
+
for a in task.artifacts
|
|
175
|
+
],
|
|
176
|
+
"metadata": task.metadata,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ─── SSE Event Translation ──────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
def sse_event_to_v1(event: Any) -> Optional[Dict[str, Any]]:
|
|
183
|
+
"""Convert internal SSE event to v1.0 StreamResponse format."""
|
|
184
|
+
if isinstance(event, TaskStatusUpdateEvent):
|
|
185
|
+
return {
|
|
186
|
+
"statusUpdate": {
|
|
187
|
+
"taskId": event.task_id,
|
|
188
|
+
"status": {
|
|
189
|
+
"state": state_to_v1(event.state),
|
|
190
|
+
"timestamp": event.timestamp.isoformat(),
|
|
191
|
+
"message": event.message,
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
elif isinstance(event, TaskMessageEvent):
|
|
196
|
+
return {
|
|
197
|
+
"message": message_to_v1(event.message),
|
|
198
|
+
}
|
|
199
|
+
elif isinstance(event, TaskArtifactUpdateEvent):
|
|
200
|
+
return {
|
|
201
|
+
"artifactUpdate": {
|
|
202
|
+
"taskId": event.task_id,
|
|
203
|
+
"artifact": {
|
|
204
|
+
"artifact_id": event.artifact.id,
|
|
205
|
+
"name": event.artifact.type,
|
|
206
|
+
"parts": [part_to_v1(TextPart(content=str(event.artifact.content)))] if event.artifact.content else [],
|
|
207
|
+
},
|
|
208
|
+
"lastChunk": True,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ─── Method Name Translation ─────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
# v1.0 method → v0.3 method
|
|
217
|
+
V1_METHOD_TO_INTERNAL = {
|
|
218
|
+
"message/send": "tasks/send",
|
|
219
|
+
"message/stream": "tasks/sendSubscribe",
|
|
220
|
+
"tasks/get": "tasks/get",
|
|
221
|
+
"tasks/cancel": "tasks/cancel",
|
|
222
|
+
# v0.3 names pass through
|
|
223
|
+
"tasks/send": "tasks/send",
|
|
224
|
+
"tasks/sendSubscribe": "tasks/sendSubscribe",
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def method_from_v1(method: str) -> str:
|
|
229
|
+
"""Convert v1.0 method name to internal v0.3 method name."""
|
|
230
|
+
return V1_METHOD_TO_INTERNAL.get(method, method)
|
|
@@ -331,7 +331,7 @@ def test_a2a_task_to_anp_response():
|
|
|
331
331
|
)
|
|
332
332
|
resp = a2a_task_to_anp_response(task)
|
|
333
333
|
assert resp["task_id"] == "t1"
|
|
334
|
-
assert resp["state"] == "
|
|
334
|
+
assert resp["state"] == "task_state_completed"
|
|
335
335
|
assert resp["messages"][0]["parts"][0]["text"] == "done"
|
|
336
336
|
|
|
337
337
|
|
|
@@ -400,7 +400,7 @@ def test_anp_rpc_tasks_send():
|
|
|
400
400
|
assert resp.status_code == 200
|
|
401
401
|
data = resp.json()
|
|
402
402
|
assert "result" in data
|
|
403
|
-
assert data["result"]["state"] == "
|
|
403
|
+
assert data["result"]["state"] == "task_state_completed"
|
|
404
404
|
assert any("ANP echo" in m["parts"][0].get("text", "") for m in data["result"]["messages"] if m.get("parts"))
|
|
405
405
|
|
|
406
406
|
|
|
@@ -418,7 +418,7 @@ def test_anp_rpc_tasks_get():
|
|
|
418
418
|
"params": {"id": "anp-task-1"},
|
|
419
419
|
})
|
|
420
420
|
assert resp.status_code == 200
|
|
421
|
-
assert resp.json()["result"]["state"] == "
|
|
421
|
+
assert resp.json()["result"]["state"] == "task_state_completed"
|
|
422
422
|
|
|
423
423
|
|
|
424
424
|
def test_anp_rpc_tasks_cancel():
|
|
@@ -90,7 +90,7 @@ def test_google_user_message():
|
|
|
90
90
|
def test_google_agent_role_maps_to_assistant():
|
|
91
91
|
gmsg = {"role": "agent", "parts": [{"text": "response"}]}
|
|
92
92
|
msg = google_message_to_tp(gmsg)
|
|
93
|
-
assert msg.role == "
|
|
93
|
+
assert msg.role == "agent"
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def test_tp_to_google_message():
|
|
@@ -9,8 +9,8 @@ from theprotocol.models import (
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def test_task_state_enum():
|
|
12
|
-
assert TaskState.SUBMITTED == "
|
|
13
|
-
assert TaskState.COMPLETED == "
|
|
12
|
+
assert TaskState.SUBMITTED == "TASK_STATE_SUBMITTED"
|
|
13
|
+
assert TaskState.COMPLETED == "TASK_STATE_COMPLETED"
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def test_text_part():
|
theprotocol_sdk-0.1.0/PKG-INFO
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
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
|
theprotocol_sdk-0.1.0/README.md
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|