traia-iatp 0.1.2__py3-none-any.whl → 0.1.67__py3-none-any.whl
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.
- traia_iatp/__init__.py +105 -8
- traia_iatp/cli/main.py +85 -1
- traia_iatp/client/__init__.py +28 -3
- traia_iatp/client/crewai_a2a_tools.py +32 -12
- traia_iatp/client/d402_a2a_client.py +348 -0
- traia_iatp/contracts/__init__.py +11 -0
- traia_iatp/contracts/data/abis/contract-abis-localhost.json +4091 -0
- traia_iatp/contracts/data/abis/contract-abis-sepolia.json +4890 -0
- traia_iatp/contracts/data/addresses/contract-addresses.json +17 -0
- traia_iatp/contracts/data/addresses/contract-proxies.json +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +369 -0
- traia_iatp/core/models.py +17 -3
- traia_iatp/d402/MIDDLEWARE_ARCHITECTURE.md +205 -0
- traia_iatp/d402/PRICE_BUILDER_USAGE.md +249 -0
- traia_iatp/d402/README.md +489 -0
- traia_iatp/d402/__init__.py +54 -0
- traia_iatp/d402/asgi_wrapper.py +469 -0
- traia_iatp/d402/chains.py +102 -0
- traia_iatp/d402/client.py +150 -0
- traia_iatp/d402/clients/__init__.py +7 -0
- traia_iatp/d402/clients/base.py +218 -0
- traia_iatp/d402/clients/httpx.py +266 -0
- traia_iatp/d402/common.py +114 -0
- traia_iatp/d402/encoding.py +28 -0
- traia_iatp/d402/examples/client_example.py +197 -0
- traia_iatp/d402/examples/server_example.py +171 -0
- traia_iatp/d402/facilitator.py +481 -0
- traia_iatp/d402/mcp_middleware.py +296 -0
- traia_iatp/d402/models.py +116 -0
- traia_iatp/d402/networks.py +98 -0
- traia_iatp/d402/path.py +43 -0
- traia_iatp/d402/payment_introspection.py +126 -0
- traia_iatp/d402/payment_signing.py +183 -0
- traia_iatp/d402/price_builder.py +164 -0
- traia_iatp/d402/servers/__init__.py +61 -0
- traia_iatp/d402/servers/base.py +139 -0
- traia_iatp/d402/servers/example_general_server.py +140 -0
- traia_iatp/d402/servers/fastapi.py +253 -0
- traia_iatp/d402/servers/mcp.py +304 -0
- traia_iatp/d402/servers/starlette.py +878 -0
- traia_iatp/d402/starlette_middleware.py +529 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/D402_MCP_ADAPTER_FLOW.md +357 -0
- traia_iatp/mcp/__init__.py +3 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +526 -0
- traia_iatp/mcp/mcp_agent_template.py +78 -13
- traia_iatp/mcp/templates/Dockerfile.j2 +27 -4
- traia_iatp/mcp/templates/README.md.j2 +104 -8
- traia_iatp/mcp/templates/cursor-rules.md.j2 +194 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +1 -2
- traia_iatp/mcp/templates/docker-compose.yml.j2 +13 -3
- traia_iatp/mcp/templates/env.example.j2 +60 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +2 -2
- traia_iatp/mcp/templates/pyproject.toml.j2 +11 -5
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +320 -10
- traia_iatp/mcp/templates/server.py.j2 +174 -197
- traia_iatp/mcp/traia_mcp_adapter.py +182 -20
- traia_iatp/registry/__init__.py +47 -12
- traia_iatp/registry/atlas_search_indexes.json +108 -54
- traia_iatp/registry/iatp_search_api.py +169 -39
- traia_iatp/registry/mongodb_registry.py +241 -69
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +1 -1
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +8 -8
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +1 -1
- traia_iatp/registry/readmes/README.md +3 -3
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +6 -6
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/a2a_server.py +22 -7
- traia_iatp/server/iatp_server_template_generator.py +23 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +23 -1
- traia_iatp/server/templates/README.md +2 -2
- traia_iatp/server/templates/README.md.j2 +5 -5
- traia_iatp/server/templates/__main__.py.j2 +374 -66
- traia_iatp/server/templates/agent.py.j2 +12 -11
- traia_iatp/server/templates/agent_config.json.j2 +3 -3
- traia_iatp/server/templates/agent_executor.py.j2 +45 -27
- traia_iatp/server/templates/env.example.j2 +32 -4
- traia_iatp/server/templates/gitignore.j2 +7 -0
- traia_iatp/server/templates/pyproject.toml.j2 +13 -12
- traia_iatp/server/templates/run_local_docker.sh.j2 +143 -11
- traia_iatp/server/templates/server.py.j2 +197 -10
- traia_iatp/special_agencies/registry_search_agency.py +1 -1
- traia_iatp/utils/iatp_utils.py +6 -6
- traia_iatp-0.1.67.dist-info/METADATA +320 -0
- traia_iatp-0.1.67.dist-info/RECORD +117 -0
- traia_iatp-0.1.2.dist-info/METADATA +0 -414
- traia_iatp-0.1.2.dist-info/RECORD +0 -72
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/WHEEL +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/entry_points.txt +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/licenses/LICENSE +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""D402-enabled A2A client for IATP with payment support.
|
|
2
|
+
|
|
3
|
+
This client automatically handles D402 payments when communicating with
|
|
4
|
+
utility agents that require payment. It supports:
|
|
5
|
+
- Automatic payment creation for 402 responses
|
|
6
|
+
- Payment header injection
|
|
7
|
+
- Multiple payment schemes (exact, facilitator-verified)
|
|
8
|
+
- Network and token selection
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import json
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Optional, Dict, Any
|
|
16
|
+
from a2a.client import A2AClient, A2ACardResolver
|
|
17
|
+
from a2a.types import Message, TextPart, TaskState, SendMessageRequest, MessageSendParams
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from ..d402.client import D402IATPClient, create_iatp_payment_client
|
|
21
|
+
from ..d402.models import D402PaymentInfo
|
|
22
|
+
from ..d402.types import d402PaymentRequiredResponse, PaymentRequirements
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class D402A2AClient:
|
|
28
|
+
"""A2A client with d402 payment support for IATP.
|
|
29
|
+
|
|
30
|
+
This client automatically handles d402 payments when communicating with
|
|
31
|
+
utility agents that require payment.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
agent_endpoint: str,
|
|
37
|
+
agent_card: Any,
|
|
38
|
+
httpx_client: Any,
|
|
39
|
+
payment_client: Optional[D402IATPClient] = None,
|
|
40
|
+
max_payment_usd: Optional[float] = None
|
|
41
|
+
):
|
|
42
|
+
"""Private init - use create() classmethod instead."""
|
|
43
|
+
self.agent_endpoint = agent_endpoint
|
|
44
|
+
self.agent_card = agent_card
|
|
45
|
+
self._httpx_client = httpx_client
|
|
46
|
+
self.payment_client = payment_client
|
|
47
|
+
self.max_payment_usd = max_payment_usd
|
|
48
|
+
|
|
49
|
+
# Extract d402 payment information if available
|
|
50
|
+
self.d402_info = self._extract_d402_info()
|
|
51
|
+
|
|
52
|
+
# Initialize A2A client (a2a-sdk 0.3.x API)
|
|
53
|
+
self.a2a_client = A2AClient(
|
|
54
|
+
httpx_client=self._httpx_client,
|
|
55
|
+
agent_card=self.agent_card,
|
|
56
|
+
url=agent_endpoint
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
async def create(
|
|
61
|
+
cls,
|
|
62
|
+
agent_endpoint: str,
|
|
63
|
+
payment_client: Optional[D402IATPClient] = None,
|
|
64
|
+
max_payment_usd: Optional[float] = None
|
|
65
|
+
):
|
|
66
|
+
"""Create a D402A2AClient instance (async factory method).
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
agent_endpoint: URL of the utility agent
|
|
70
|
+
payment_client: Optional pre-configured D402IATPClient
|
|
71
|
+
max_payment_usd: Optional maximum payment amount per request
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Initialized D402A2AClient instance
|
|
75
|
+
"""
|
|
76
|
+
import httpx
|
|
77
|
+
|
|
78
|
+
# Create HTTP client and resolve agent card
|
|
79
|
+
httpx_client = httpx.AsyncClient(timeout=30.0)
|
|
80
|
+
card_resolver = A2ACardResolver(
|
|
81
|
+
httpx_client=httpx_client,
|
|
82
|
+
base_url=agent_endpoint,
|
|
83
|
+
agent_card_path='/.well-known/agent-card.json'
|
|
84
|
+
)
|
|
85
|
+
agent_card = await card_resolver.get_agent_card()
|
|
86
|
+
|
|
87
|
+
# Create instance
|
|
88
|
+
return cls(
|
|
89
|
+
agent_endpoint=agent_endpoint,
|
|
90
|
+
agent_card=agent_card,
|
|
91
|
+
httpx_client=httpx_client,
|
|
92
|
+
payment_client=payment_client,
|
|
93
|
+
max_payment_usd=max_payment_usd
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def aclose(self):
|
|
97
|
+
"""Close the HTTP client."""
|
|
98
|
+
if hasattr(self, '_httpx_client'):
|
|
99
|
+
await self._httpx_client.aclose()
|
|
100
|
+
|
|
101
|
+
async def __aenter__(self):
|
|
102
|
+
"""Async context manager entry."""
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
106
|
+
"""Async context manager exit."""
|
|
107
|
+
await self.aclose()
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def _extract_d402_info(self) -> Optional[D402PaymentInfo]:
|
|
111
|
+
"""Extract d402 payment information from agent card."""
|
|
112
|
+
# In a2a-sdk 0.3.x, AgentCard doesn't have metadata field
|
|
113
|
+
# D402 info will be discovered via 402 responses instead
|
|
114
|
+
# So we always assume payment might be required and handle 402 dynamically
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
async def send_message_with_payment(
|
|
118
|
+
self,
|
|
119
|
+
message: str,
|
|
120
|
+
skill_id: Optional[str] = None
|
|
121
|
+
) -> str:
|
|
122
|
+
"""Send a message to the agent, automatically handling d402 payment if required.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
message: The message to send to the agent
|
|
126
|
+
skill_id: Optional specific skill to invoke
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Agent's response text
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If payment is required but no payment client is configured
|
|
133
|
+
RuntimeError: If task execution fails
|
|
134
|
+
"""
|
|
135
|
+
# Prepare the message (a2a-sdk 0.3.x requires message_id)
|
|
136
|
+
import uuid
|
|
137
|
+
msg = Message(
|
|
138
|
+
message_id=str(uuid.uuid4()),
|
|
139
|
+
role="user",
|
|
140
|
+
parts=[TextPart(text=message)]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Prepare headers (will add payment header if needed)
|
|
144
|
+
headers = {}
|
|
145
|
+
|
|
146
|
+
# Try to send the request
|
|
147
|
+
async with httpx.AsyncClient(timeout=300.0) as http_client:
|
|
148
|
+
# First attempt without payment to see if it's required
|
|
149
|
+
try:
|
|
150
|
+
# Use A2A client's send_task method
|
|
151
|
+
task = await self.a2a_client.send_task(
|
|
152
|
+
id=str(asyncio.get_event_loop().time()),
|
|
153
|
+
message=msg
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Extract response
|
|
157
|
+
return self._extract_response(task)
|
|
158
|
+
|
|
159
|
+
except httpx.HTTPStatusError as e:
|
|
160
|
+
if e.response.status_code == 402:
|
|
161
|
+
# Payment required - handle d402 flow
|
|
162
|
+
return await self._handle_payment_required(
|
|
163
|
+
http_client, e.response, message, skill_id
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
raise
|
|
167
|
+
|
|
168
|
+
async def _handle_payment_required(
|
|
169
|
+
self,
|
|
170
|
+
http_client: httpx.AsyncClient,
|
|
171
|
+
response: httpx.Response,
|
|
172
|
+
message: str,
|
|
173
|
+
skill_id: Optional[str]
|
|
174
|
+
) -> str:
|
|
175
|
+
"""Handle 402 Payment Required response.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
http_client: HTTP client for making requests
|
|
179
|
+
response: 402 response with payment requirements
|
|
180
|
+
message: Original message to send
|
|
181
|
+
skill_id: Optional skill ID
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Agent's response after successful payment
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
ValueError: If payment client not configured or requirements not met
|
|
188
|
+
"""
|
|
189
|
+
if not self.payment_client:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"Payment is required but no payment client configured. "
|
|
192
|
+
"Please provide a payment_client when initializing D402A2AClient."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Parse payment requirements
|
|
196
|
+
try:
|
|
197
|
+
payment_response = d402PaymentRequiredResponse(**response.json())
|
|
198
|
+
except Exception as e:
|
|
199
|
+
raise ValueError(f"Invalid payment requirements: {e}")
|
|
200
|
+
|
|
201
|
+
# Select payment requirements
|
|
202
|
+
try:
|
|
203
|
+
selected_requirements = self.payment_client.select_payment_requirements(
|
|
204
|
+
accepts=payment_response.accepts,
|
|
205
|
+
network_filter=None, # Accept any network
|
|
206
|
+
scheme_filter="exact" # Only support exact scheme
|
|
207
|
+
)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
raise ValueError(f"Cannot satisfy payment requirements: {e}")
|
|
210
|
+
|
|
211
|
+
# Check if amount is within maximum
|
|
212
|
+
if self.max_payment_usd:
|
|
213
|
+
# Convert to USD (assuming USDC with 6 decimals)
|
|
214
|
+
amount_usd = int(selected_requirements.max_amount_required) / 1_000_000
|
|
215
|
+
if amount_usd > self.max_payment_usd:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"Payment amount ${amount_usd:.2f} exceeds maximum ${self.max_payment_usd:.2f}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Create payment header
|
|
221
|
+
payment_header = self.payment_client.create_payment_header(
|
|
222
|
+
payment_requirements=selected_requirements,
|
|
223
|
+
d402_version=1
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Retry request with payment header
|
|
227
|
+
headers = {"X-PAYMENT": payment_header}
|
|
228
|
+
|
|
229
|
+
# Make the A2A request with payment
|
|
230
|
+
msg = Message(
|
|
231
|
+
role="user",
|
|
232
|
+
parts=[TextPart(text=message)]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Send task with payment (we need to make raw HTTP request with headers)
|
|
236
|
+
# Since A2AClient doesn't support custom headers, we'll make the request directly
|
|
237
|
+
request_data = {
|
|
238
|
+
"jsonrpc": "2.0",
|
|
239
|
+
"id": str(asyncio.get_event_loop().time()),
|
|
240
|
+
"method": "message/send",
|
|
241
|
+
"params": {
|
|
242
|
+
"message": msg.model_dump()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
response = await http_client.post(
|
|
247
|
+
self.agent_endpoint,
|
|
248
|
+
json=request_data,
|
|
249
|
+
headers={"Content-Type": "application/json", **headers}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if response.status_code == 402:
|
|
253
|
+
raise RuntimeError(f"Payment failed: {response.json().get('error', 'Unknown error')}")
|
|
254
|
+
|
|
255
|
+
response.raise_for_status()
|
|
256
|
+
|
|
257
|
+
# Parse JSON-RPC response
|
|
258
|
+
result = response.json()
|
|
259
|
+
if "error" in result:
|
|
260
|
+
raise RuntimeError(f"Agent error: {result['error']}")
|
|
261
|
+
|
|
262
|
+
# Extract the response text
|
|
263
|
+
task_result = result.get("result", {})
|
|
264
|
+
if isinstance(task_result, dict):
|
|
265
|
+
messages = task_result.get("messages", [])
|
|
266
|
+
for msg in messages:
|
|
267
|
+
if msg.get("role") == "agent":
|
|
268
|
+
parts = msg.get("parts", [])
|
|
269
|
+
return " ".join(
|
|
270
|
+
part.get("text", "")
|
|
271
|
+
for part in parts
|
|
272
|
+
if part.get("type") == "text"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return str(task_result)
|
|
276
|
+
|
|
277
|
+
def _extract_response(self, task) -> str:
|
|
278
|
+
"""Extract text response from task result."""
|
|
279
|
+
if task.status.state == TaskState.COMPLETED:
|
|
280
|
+
# Look for agent's response in messages
|
|
281
|
+
for msg in task.messages:
|
|
282
|
+
if msg.role == "agent":
|
|
283
|
+
response_text = ""
|
|
284
|
+
for part in msg.parts:
|
|
285
|
+
if hasattr(part, 'text'):
|
|
286
|
+
response_text += part.text
|
|
287
|
+
return response_text
|
|
288
|
+
|
|
289
|
+
# If no agent message, check artifacts
|
|
290
|
+
if task.artifacts:
|
|
291
|
+
response_text = ""
|
|
292
|
+
for artifact in task.artifacts:
|
|
293
|
+
for part in artifact.parts:
|
|
294
|
+
if hasattr(part, 'text'):
|
|
295
|
+
response_text += part.text
|
|
296
|
+
return response_text
|
|
297
|
+
|
|
298
|
+
return "Task completed but no response found"
|
|
299
|
+
else:
|
|
300
|
+
raise RuntimeError(f"Task failed with state: {task.status.state}")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
async def create_d402_a2a_client(
|
|
304
|
+
agent_endpoint: str,
|
|
305
|
+
payment_private_key: Optional[str] = None,
|
|
306
|
+
max_payment_usd: Optional[float] = 10.0,
|
|
307
|
+
agent_contract_address: Optional[str] = None
|
|
308
|
+
) -> D402A2AClient:
|
|
309
|
+
"""Convenience function to create an d402-enabled A2A client (async).
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
agent_endpoint: URL of the utility agent
|
|
313
|
+
payment_private_key: Optional private key for payments (hex encoded)
|
|
314
|
+
max_payment_usd: Maximum payment per request in USD
|
|
315
|
+
agent_contract_address: Optional client agent contract address
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Configured D402A2AClient
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
# Create client with payment support
|
|
322
|
+
client = await create_d402_a2a_client(
|
|
323
|
+
agent_endpoint="https://agent.example.com",
|
|
324
|
+
payment_private_key="0x...",
|
|
325
|
+
max_payment_usd=5.0
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Send message (automatically handles payment if required)
|
|
329
|
+
response = await client.send_message_with_payment(
|
|
330
|
+
"Analyze sentiment of: 'Stock prices are rising'"
|
|
331
|
+
)
|
|
332
|
+
print(response)
|
|
333
|
+
"""
|
|
334
|
+
payment_client = None
|
|
335
|
+
if payment_private_key:
|
|
336
|
+
payment_client = create_iatp_payment_client(
|
|
337
|
+
private_key=payment_private_key,
|
|
338
|
+
max_value_usd=max_payment_usd,
|
|
339
|
+
agent_contract_address=agent_contract_address
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Use async factory method for a2a-sdk 0.3.x compatibility
|
|
343
|
+
return await D402A2AClient.create(
|
|
344
|
+
agent_endpoint=agent_endpoint,
|
|
345
|
+
payment_client=payment_client,
|
|
346
|
+
max_payment_usd=max_payment_usd
|
|
347
|
+
)
|
|
348
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""IATP Contracts utilities."""
|
|
2
|
+
|
|
3
|
+
from .wallet_creator import get_contract_config
|
|
4
|
+
from .iatp_contracts_config import get_contract_address, get_contract_abi, get_rpc_url
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"get_contract_config",
|
|
8
|
+
"get_contract_address",
|
|
9
|
+
"get_contract_abi",
|
|
10
|
+
"get_rpc_url"
|
|
11
|
+
]
|