traia-iatp 0.1.29__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.
Potentially problematic release.
This version of traia-iatp might be problematic. Click here for more details.
- traia_iatp/README.md +368 -0
- traia_iatp/__init__.py +54 -0
- traia_iatp/cli/__init__.py +5 -0
- traia_iatp/cli/main.py +483 -0
- traia_iatp/client/__init__.py +10 -0
- traia_iatp/client/a2a_client.py +274 -0
- traia_iatp/client/crewai_a2a_tools.py +335 -0
- traia_iatp/client/d402_a2a_client.py +293 -0
- traia_iatp/client/grpc_a2a_tools.py +349 -0
- traia_iatp/client/root_path_a2a_client.py +1 -0
- traia_iatp/contracts/__init__.py +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +255 -0
- traia_iatp/core/__init__.py +43 -0
- traia_iatp/core/models.py +172 -0
- traia_iatp/d402/__init__.py +55 -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 +219 -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 +453 -0
- traia_iatp/d402/fastapi_middleware/__init__.py +6 -0
- traia_iatp/d402/fastapi_middleware/middleware.py +225 -0
- traia_iatp/d402/fastmcp_middleware.py +147 -0
- traia_iatp/d402/mcp_middleware.py +434 -0
- traia_iatp/d402/middleware.py +193 -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 +104 -0
- traia_iatp/d402/payment_signing.py +178 -0
- traia_iatp/d402/paywall.py +119 -0
- traia_iatp/d402/starlette_middleware.py +326 -0
- traia_iatp/d402/template.py +1 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/__init__.py +18 -0
- traia_iatp/mcp/client.py +201 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +361 -0
- traia_iatp/mcp/mcp_agent_template.py +481 -0
- traia_iatp/mcp/templates/Dockerfile.j2 +80 -0
- traia_iatp/mcp/templates/README.md.j2 +310 -0
- traia_iatp/mcp/templates/cursor-rules.md.j2 +520 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
- traia_iatp/mcp/templates/docker-compose.yml.j2 +32 -0
- traia_iatp/mcp/templates/dockerignore.j2 +47 -0
- traia_iatp/mcp/templates/env.example.j2 +57 -0
- traia_iatp/mcp/templates/gitignore.j2 +77 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
- traia_iatp/mcp/templates/pyproject.toml.j2 +32 -0
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +390 -0
- traia_iatp/mcp/templates/server.py.j2 +175 -0
- traia_iatp/mcp/traia_mcp_adapter.py +543 -0
- traia_iatp/preview_diagrams.html +181 -0
- traia_iatp/registry/__init__.py +26 -0
- traia_iatp/registry/atlas_search_indexes.json +280 -0
- traia_iatp/registry/embeddings.py +298 -0
- traia_iatp/registry/iatp_search_api.py +846 -0
- traia_iatp/registry/mongodb_registry.py +771 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
- traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
- traia_iatp/registry/readmes/README.md +251 -0
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/__init__.py +15 -0
- traia_iatp/server/a2a_server.py +219 -0
- traia_iatp/server/example_template_usage.py +72 -0
- traia_iatp/server/iatp_server_agent_generator.py +237 -0
- traia_iatp/server/iatp_server_template_generator.py +235 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +49 -0
- traia_iatp/server/templates/README.md +137 -0
- traia_iatp/server/templates/README.md.j2 +425 -0
- traia_iatp/server/templates/__init__.py +1 -0
- traia_iatp/server/templates/__main__.py.j2 +565 -0
- traia_iatp/server/templates/agent.py.j2 +94 -0
- traia_iatp/server/templates/agent_config.json.j2 +22 -0
- traia_iatp/server/templates/agent_executor.py.j2 +279 -0
- traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/server/templates/env.example.j2 +84 -0
- traia_iatp/server/templates/gitignore.j2 +78 -0
- traia_iatp/server/templates/grpc_server.py.j2 +218 -0
- traia_iatp/server/templates/pyproject.toml.j2 +78 -0
- traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
- traia_iatp/server/templates/server.py.j2 +243 -0
- traia_iatp/special_agencies/__init__.py +4 -0
- traia_iatp/special_agencies/registry_search_agency.py +392 -0
- traia_iatp/utils/__init__.py +10 -0
- traia_iatp/utils/docker_utils.py +251 -0
- traia_iatp/utils/general.py +64 -0
- traia_iatp/utils/iatp_utils.py +126 -0
- traia_iatp-0.1.29.dist-info/METADATA +423 -0
- traia_iatp-0.1.29.dist-info/RECORD +107 -0
- traia_iatp-0.1.29.dist-info/WHEEL +5 -0
- traia_iatp-0.1.29.dist-info/entry_points.txt +2 -0
- traia_iatp-0.1.29.dist-info/licenses/LICENSE +21 -0
- traia_iatp-0.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""D402-enabled A2A client for IATP with payment support."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
import base64
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
from a2a.client import A2AClient, A2ACardResolver
|
|
9
|
+
from a2a.types import Message, TextPart, TaskState
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from ..d402.client import D402IATPClient, create_iatp_payment_client
|
|
13
|
+
from ..d402.models import D402PaymentInfo
|
|
14
|
+
from ..d402.types import d402PaymentRequiredResponse, PaymentRequirements
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class D402A2AClient:
|
|
20
|
+
"""A2A client with d402 payment support for IATP.
|
|
21
|
+
|
|
22
|
+
This client automatically handles d402 payments when communicating with
|
|
23
|
+
utility agents that require payment.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
agent_endpoint: str,
|
|
29
|
+
payment_client: Optional[D402IATPClient] = None,
|
|
30
|
+
max_payment_usd: Optional[float] = None
|
|
31
|
+
):
|
|
32
|
+
"""Initialize the d402-enabled A2A client.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
agent_endpoint: URL of the utility agent
|
|
36
|
+
payment_client: Optional pre-configured D402IATPClient
|
|
37
|
+
max_payment_usd: Optional maximum payment amount per request
|
|
38
|
+
"""
|
|
39
|
+
self.agent_endpoint = agent_endpoint
|
|
40
|
+
self.payment_client = payment_client
|
|
41
|
+
self.max_payment_usd = max_payment_usd
|
|
42
|
+
|
|
43
|
+
# Resolve agent card
|
|
44
|
+
self.card_resolver = A2ACardResolver(agent_endpoint)
|
|
45
|
+
self.agent_card = self.card_resolver.get_agent_card()
|
|
46
|
+
|
|
47
|
+
# Extract d402 payment information if available
|
|
48
|
+
self.d402_info = self._extract_d402_info()
|
|
49
|
+
|
|
50
|
+
# Initialize A2A client
|
|
51
|
+
self.a2a_client = A2AClient(
|
|
52
|
+
agent_card=self.agent_card,
|
|
53
|
+
credentials=None # d402 handles payment authorization
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _extract_d402_info(self) -> Optional[D402PaymentInfo]:
|
|
57
|
+
"""Extract d402 payment information from agent card."""
|
|
58
|
+
metadata = self.agent_card.metadata or {}
|
|
59
|
+
d402_data = metadata.get("d402")
|
|
60
|
+
|
|
61
|
+
if d402_data and d402_data.get("enabled"):
|
|
62
|
+
return D402PaymentInfo(**d402_data)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
async def send_message_with_payment(
|
|
66
|
+
self,
|
|
67
|
+
message: str,
|
|
68
|
+
skill_id: Optional[str] = None
|
|
69
|
+
) -> str:
|
|
70
|
+
"""Send a message to the agent, automatically handling d402 payment if required.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
message: The message to send to the agent
|
|
74
|
+
skill_id: Optional specific skill to invoke
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Agent's response text
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: If payment is required but no payment client is configured
|
|
81
|
+
RuntimeError: If task execution fails
|
|
82
|
+
"""
|
|
83
|
+
# Prepare the message
|
|
84
|
+
msg = Message(
|
|
85
|
+
role="user",
|
|
86
|
+
parts=[TextPart(text=message)]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Prepare headers (will add payment header if needed)
|
|
90
|
+
headers = {}
|
|
91
|
+
|
|
92
|
+
# Try to send the request
|
|
93
|
+
async with httpx.AsyncClient(timeout=300.0) as http_client:
|
|
94
|
+
# First attempt without payment to see if it's required
|
|
95
|
+
try:
|
|
96
|
+
# Use A2A client's send_task method
|
|
97
|
+
task = await self.a2a_client.send_task(
|
|
98
|
+
id=str(asyncio.get_event_loop().time()),
|
|
99
|
+
message=msg
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Extract response
|
|
103
|
+
return self._extract_response(task)
|
|
104
|
+
|
|
105
|
+
except httpx.HTTPStatusError as e:
|
|
106
|
+
if e.response.status_code == 402:
|
|
107
|
+
# Payment required - handle d402 flow
|
|
108
|
+
return await self._handle_payment_required(
|
|
109
|
+
http_client, e.response, message, skill_id
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
async def _handle_payment_required(
|
|
115
|
+
self,
|
|
116
|
+
http_client: httpx.AsyncClient,
|
|
117
|
+
response: httpx.Response,
|
|
118
|
+
message: str,
|
|
119
|
+
skill_id: Optional[str]
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Handle 402 Payment Required response.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
http_client: HTTP client for making requests
|
|
125
|
+
response: 402 response with payment requirements
|
|
126
|
+
message: Original message to send
|
|
127
|
+
skill_id: Optional skill ID
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Agent's response after successful payment
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
ValueError: If payment client not configured or requirements not met
|
|
134
|
+
"""
|
|
135
|
+
if not self.payment_client:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
"Payment is required but no payment client configured. "
|
|
138
|
+
"Please provide a payment_client when initializing D402A2AClient."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Parse payment requirements
|
|
142
|
+
try:
|
|
143
|
+
payment_response = d402PaymentRequiredResponse(**response.json())
|
|
144
|
+
except Exception as e:
|
|
145
|
+
raise ValueError(f"Invalid payment requirements: {e}")
|
|
146
|
+
|
|
147
|
+
# Select payment requirements
|
|
148
|
+
try:
|
|
149
|
+
selected_requirements = self.payment_client.select_payment_requirements(
|
|
150
|
+
accepts=payment_response.accepts,
|
|
151
|
+
network_filter=None, # Accept any network
|
|
152
|
+
scheme_filter="exact" # Only support exact scheme
|
|
153
|
+
)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise ValueError(f"Cannot satisfy payment requirements: {e}")
|
|
156
|
+
|
|
157
|
+
# Check if amount is within maximum
|
|
158
|
+
if self.max_payment_usd:
|
|
159
|
+
# Convert to USD (assuming USDC with 6 decimals)
|
|
160
|
+
amount_usd = int(selected_requirements.max_amount_required) / 1_000_000
|
|
161
|
+
if amount_usd > self.max_payment_usd:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"Payment amount ${amount_usd:.2f} exceeds maximum ${self.max_payment_usd:.2f}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Create payment header
|
|
167
|
+
payment_header = self.payment_client.create_payment_header(
|
|
168
|
+
payment_requirements=selected_requirements,
|
|
169
|
+
d402_version=1
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Retry request with payment header
|
|
173
|
+
headers = {"X-PAYMENT": payment_header}
|
|
174
|
+
|
|
175
|
+
# Make the A2A request with payment
|
|
176
|
+
msg = Message(
|
|
177
|
+
role="user",
|
|
178
|
+
parts=[TextPart(text=message)]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Send task with payment (we need to make raw HTTP request with headers)
|
|
182
|
+
# Since A2AClient doesn't support custom headers, we'll make the request directly
|
|
183
|
+
request_data = {
|
|
184
|
+
"jsonrpc": "2.0",
|
|
185
|
+
"id": str(asyncio.get_event_loop().time()),
|
|
186
|
+
"method": "message/send",
|
|
187
|
+
"params": {
|
|
188
|
+
"message": msg.model_dump()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
response = await http_client.post(
|
|
193
|
+
self.agent_endpoint,
|
|
194
|
+
json=request_data,
|
|
195
|
+
headers={"Content-Type": "application/json", **headers}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if response.status_code == 402:
|
|
199
|
+
raise RuntimeError(f"Payment failed: {response.json().get('error', 'Unknown error')}")
|
|
200
|
+
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
|
|
203
|
+
# Parse JSON-RPC response
|
|
204
|
+
result = response.json()
|
|
205
|
+
if "error" in result:
|
|
206
|
+
raise RuntimeError(f"Agent error: {result['error']}")
|
|
207
|
+
|
|
208
|
+
# Extract the response text
|
|
209
|
+
task_result = result.get("result", {})
|
|
210
|
+
if isinstance(task_result, dict):
|
|
211
|
+
messages = task_result.get("messages", [])
|
|
212
|
+
for msg in messages:
|
|
213
|
+
if msg.get("role") == "agent":
|
|
214
|
+
parts = msg.get("parts", [])
|
|
215
|
+
return " ".join(
|
|
216
|
+
part.get("text", "")
|
|
217
|
+
for part in parts
|
|
218
|
+
if part.get("type") == "text"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return str(task_result)
|
|
222
|
+
|
|
223
|
+
def _extract_response(self, task) -> str:
|
|
224
|
+
"""Extract text response from task result."""
|
|
225
|
+
if task.status.state == TaskState.COMPLETED:
|
|
226
|
+
# Look for agent's response in messages
|
|
227
|
+
for msg in task.messages:
|
|
228
|
+
if msg.role == "agent":
|
|
229
|
+
response_text = ""
|
|
230
|
+
for part in msg.parts:
|
|
231
|
+
if hasattr(part, 'text'):
|
|
232
|
+
response_text += part.text
|
|
233
|
+
return response_text
|
|
234
|
+
|
|
235
|
+
# If no agent message, check artifacts
|
|
236
|
+
if task.artifacts:
|
|
237
|
+
response_text = ""
|
|
238
|
+
for artifact in task.artifacts:
|
|
239
|
+
for part in artifact.parts:
|
|
240
|
+
if hasattr(part, 'text'):
|
|
241
|
+
response_text += part.text
|
|
242
|
+
return response_text
|
|
243
|
+
|
|
244
|
+
return "Task completed but no response found"
|
|
245
|
+
else:
|
|
246
|
+
raise RuntimeError(f"Task failed with state: {task.status.state}")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def create_d402_a2a_client(
|
|
250
|
+
agent_endpoint: str,
|
|
251
|
+
payment_private_key: Optional[str] = None,
|
|
252
|
+
max_payment_usd: Optional[float] = 10.0,
|
|
253
|
+
agent_contract_address: Optional[str] = None
|
|
254
|
+
) -> D402A2AClient:
|
|
255
|
+
"""Convenience function to create an d402-enabled A2A client.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
agent_endpoint: URL of the utility agent
|
|
259
|
+
payment_private_key: Optional private key for payments (hex encoded)
|
|
260
|
+
max_payment_usd: Maximum payment per request in USD
|
|
261
|
+
agent_contract_address: Optional client agent contract address
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Configured D402A2AClient
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
# Create client with payment support
|
|
268
|
+
client = create_d402_a2a_client(
|
|
269
|
+
agent_endpoint="https://agent.example.com",
|
|
270
|
+
payment_private_key="0x...",
|
|
271
|
+
max_payment_usd=5.0
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Send message (automatically handles payment if required)
|
|
275
|
+
response = await client.send_message_with_payment(
|
|
276
|
+
"Analyze sentiment of: 'Stock prices are rising'"
|
|
277
|
+
)
|
|
278
|
+
print(response)
|
|
279
|
+
"""
|
|
280
|
+
payment_client = None
|
|
281
|
+
if payment_private_key:
|
|
282
|
+
payment_client = create_iatp_payment_client(
|
|
283
|
+
private_key=payment_private_key,
|
|
284
|
+
max_value_usd=max_payment_usd,
|
|
285
|
+
agent_contract_address=agent_contract_address
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return D402A2AClient(
|
|
289
|
+
agent_endpoint=agent_endpoint,
|
|
290
|
+
payment_client=payment_client,
|
|
291
|
+
max_payment_usd=max_payment_usd
|
|
292
|
+
)
|
|
293
|
+
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""gRPC-based A2A client for high-performance agent communication."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict, Any, Optional, List, Union, AsyncIterator
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import json
|
|
8
|
+
import uuid
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
|
|
11
|
+
import grpc
|
|
12
|
+
from grpc import aio
|
|
13
|
+
from crewai.tools import BaseTool
|
|
14
|
+
from pydantic import Field, BaseModel
|
|
15
|
+
|
|
16
|
+
# Note: These imports assume the A2A proto files have been compiled
|
|
17
|
+
# In practice, you'd need to generate these from the A2A .proto definitions
|
|
18
|
+
# from a2a.proto import a2a_pb2, a2a_pb2_grpc
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GrpcA2AConfig(BaseModel):
|
|
24
|
+
"""Configuration for gRPC A2A tools."""
|
|
25
|
+
endpoint: str = Field(description="The gRPC endpoint (host:port)")
|
|
26
|
+
agency_name: str = Field(description="Name of the utility agency")
|
|
27
|
+
agency_description: str = Field(description="Description of what the agency does")
|
|
28
|
+
timeout: int = Field(default=300, description="Timeout in seconds for gRPC calls")
|
|
29
|
+
retry_attempts: int = Field(default=3, description="Number of retry attempts")
|
|
30
|
+
|
|
31
|
+
# gRPC specific settings
|
|
32
|
+
max_message_length: int = Field(default=4 * 1024 * 1024, description="Max message size (4MB default)")
|
|
33
|
+
keepalive_time_ms: int = Field(default=10000, description="Keepalive ping interval")
|
|
34
|
+
keepalive_timeout_ms: int = Field(default=5000, description="Keepalive timeout")
|
|
35
|
+
max_concurrent_streams: int = Field(default=100, description="Max concurrent gRPC streams")
|
|
36
|
+
|
|
37
|
+
# Connection pool settings
|
|
38
|
+
pool_size: int = Field(default=10, description="Number of gRPC channels in pool")
|
|
39
|
+
use_tls: bool = Field(default=False, description="Whether to use TLS")
|
|
40
|
+
tls_cert_path: Optional[str] = Field(default=None, description="Path to TLS certificate")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GrpcChannelPool:
|
|
44
|
+
"""Manages a pool of gRPC channels for load distribution."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: GrpcA2AConfig):
|
|
47
|
+
self.config = config
|
|
48
|
+
self.channels: List[aio.Channel] = []
|
|
49
|
+
self.current_index = 0
|
|
50
|
+
self._lock = asyncio.Lock()
|
|
51
|
+
|
|
52
|
+
async def initialize(self):
|
|
53
|
+
"""Initialize the channel pool."""
|
|
54
|
+
options = [
|
|
55
|
+
('grpc.max_send_message_length', self.config.max_message_length),
|
|
56
|
+
('grpc.max_receive_message_length', self.config.max_message_length),
|
|
57
|
+
('grpc.keepalive_time_ms', self.config.keepalive_time_ms),
|
|
58
|
+
('grpc.keepalive_timeout_ms', self.config.keepalive_timeout_ms),
|
|
59
|
+
('grpc.http2.max_concurrent_streams', self.config.max_concurrent_streams),
|
|
60
|
+
('grpc.enable_http_proxy', 0), # Disable proxy for direct connection
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
for _ in range(self.config.pool_size):
|
|
64
|
+
if self.config.use_tls:
|
|
65
|
+
# Load TLS credentials
|
|
66
|
+
if self.config.tls_cert_path:
|
|
67
|
+
with open(self.config.tls_cert_path, 'rb') as f:
|
|
68
|
+
credentials = grpc.ssl_channel_credentials(f.read())
|
|
69
|
+
else:
|
|
70
|
+
credentials = grpc.ssl_channel_credentials()
|
|
71
|
+
|
|
72
|
+
channel = aio.secure_channel(
|
|
73
|
+
self.config.endpoint,
|
|
74
|
+
credentials,
|
|
75
|
+
options=options
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
channel = aio.insecure_channel(
|
|
79
|
+
self.config.endpoint,
|
|
80
|
+
options=options
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.channels.append(channel)
|
|
84
|
+
|
|
85
|
+
logger.info(f"Initialized gRPC channel pool with {self.config.pool_size} channels")
|
|
86
|
+
|
|
87
|
+
async def get_channel(self) -> aio.Channel:
|
|
88
|
+
"""Get the next available channel (round-robin)."""
|
|
89
|
+
async with self._lock:
|
|
90
|
+
channel = self.channels[self.current_index]
|
|
91
|
+
self.current_index = (self.current_index + 1) % len(self.channels)
|
|
92
|
+
return channel
|
|
93
|
+
|
|
94
|
+
async def close(self):
|
|
95
|
+
"""Close all channels in the pool."""
|
|
96
|
+
for channel in self.channels:
|
|
97
|
+
await channel.close()
|
|
98
|
+
self.channels.clear()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class GrpcA2ATool(BaseTool):
|
|
102
|
+
"""gRPC-based A2A tool for high-performance agent communication."""
|
|
103
|
+
|
|
104
|
+
name: str
|
|
105
|
+
description: str
|
|
106
|
+
config: GrpcA2AConfig
|
|
107
|
+
_channel_pool: Optional[GrpcChannelPool] = None
|
|
108
|
+
_initialized: bool = False
|
|
109
|
+
|
|
110
|
+
def __init__(self, config: GrpcA2AConfig):
|
|
111
|
+
"""Initialize the gRPC A2A tool."""
|
|
112
|
+
tool_name = f"grpc_a2a_{config.agency_name.replace(' ', '_').replace('-', '_').lower()}"
|
|
113
|
+
|
|
114
|
+
super().__init__(
|
|
115
|
+
name=tool_name,
|
|
116
|
+
description=f"Use {config.agency_name} via gRPC A2A: {config.agency_description}",
|
|
117
|
+
config=config
|
|
118
|
+
)
|
|
119
|
+
self._channel_pool = None
|
|
120
|
+
self._initialized = False
|
|
121
|
+
|
|
122
|
+
async def _ensure_initialized(self):
|
|
123
|
+
"""Ensure the gRPC channel pool is initialized."""
|
|
124
|
+
if not self._initialized:
|
|
125
|
+
self._channel_pool = GrpcChannelPool(self.config)
|
|
126
|
+
await self._channel_pool.initialize()
|
|
127
|
+
self._initialized = True
|
|
128
|
+
|
|
129
|
+
async def _execute_unary(self, request: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
130
|
+
"""Execute a unary (request-response) gRPC call."""
|
|
131
|
+
await self._ensure_initialized()
|
|
132
|
+
|
|
133
|
+
# Get a channel from the pool
|
|
134
|
+
channel = await self._channel_pool.get_channel()
|
|
135
|
+
|
|
136
|
+
# Create stub (lightweight, can be created per request)
|
|
137
|
+
# stub = a2a_pb2_grpc.A2AServiceStub(channel)
|
|
138
|
+
|
|
139
|
+
# For now, return a placeholder since we don't have the actual proto files
|
|
140
|
+
# In a real implementation, you would:
|
|
141
|
+
# 1. Create the request message
|
|
142
|
+
# 2. Call the appropriate gRPC method
|
|
143
|
+
# 3. Handle the response
|
|
144
|
+
|
|
145
|
+
return f"gRPC call to {self.config.agency_name} with request: {request}"
|
|
146
|
+
|
|
147
|
+
async def _execute_streaming(self, request: str, context: Optional[Dict[str, Any]] = None) -> AsyncIterator[str]:
|
|
148
|
+
"""Execute a server-streaming gRPC call."""
|
|
149
|
+
await self._ensure_initialized()
|
|
150
|
+
|
|
151
|
+
# Get a channel from the pool
|
|
152
|
+
channel = await self._channel_pool.get_channel()
|
|
153
|
+
|
|
154
|
+
# Create stub
|
|
155
|
+
# stub = a2a_pb2_grpc.A2AServiceStub(channel)
|
|
156
|
+
|
|
157
|
+
# For demonstration, yield simulated chunks
|
|
158
|
+
for i in range(5):
|
|
159
|
+
yield f"Chunk {i} from {self.config.agency_name}"
|
|
160
|
+
await asyncio.sleep(0.1)
|
|
161
|
+
|
|
162
|
+
async def _execute_with_retry(self, request: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
163
|
+
"""Execute request with retry logic."""
|
|
164
|
+
last_error = None
|
|
165
|
+
|
|
166
|
+
for attempt in range(self.config.retry_attempts):
|
|
167
|
+
try:
|
|
168
|
+
return await self._execute_unary(request, context)
|
|
169
|
+
|
|
170
|
+
except grpc.RpcError as e:
|
|
171
|
+
last_error = f"gRPC error: {e.code()}: {e.details()}"
|
|
172
|
+
logger.warning(f"Attempt {attempt + 1} failed: {last_error}")
|
|
173
|
+
|
|
174
|
+
# Check if retryable
|
|
175
|
+
if e.code() in [
|
|
176
|
+
grpc.StatusCode.UNAVAILABLE,
|
|
177
|
+
grpc.StatusCode.DEADLINE_EXCEEDED,
|
|
178
|
+
grpc.StatusCode.INTERNAL
|
|
179
|
+
]:
|
|
180
|
+
# Wait before retry with exponential backoff
|
|
181
|
+
if attempt < self.config.retry_attempts - 1:
|
|
182
|
+
await asyncio.sleep(2 ** attempt)
|
|
183
|
+
else:
|
|
184
|
+
# Non-retryable error
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
last_error = str(e)
|
|
189
|
+
logger.warning(f"Attempt {attempt + 1} failed: {e}")
|
|
190
|
+
|
|
191
|
+
if attempt < self.config.retry_attempts - 1:
|
|
192
|
+
await asyncio.sleep(2 ** attempt)
|
|
193
|
+
|
|
194
|
+
return f"Failed after {self.config.retry_attempts} attempts. Last error: {last_error}"
|
|
195
|
+
|
|
196
|
+
async def _arun(self, request: str, **kwargs) -> str:
|
|
197
|
+
"""Async execution of the tool."""
|
|
198
|
+
try:
|
|
199
|
+
context = kwargs.get('context', {})
|
|
200
|
+
return await self._execute_with_retry(request, context)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(f"Error in gRPC A2A tool {self.name}: {e}")
|
|
203
|
+
return f"Error: {str(e)}"
|
|
204
|
+
|
|
205
|
+
def _run(self, request: str, **kwargs) -> str:
|
|
206
|
+
"""Sync execution of the tool."""
|
|
207
|
+
try:
|
|
208
|
+
loop = asyncio.get_running_loop()
|
|
209
|
+
return asyncio.run_coroutine_threadsafe(
|
|
210
|
+
self._arun(request, **kwargs),
|
|
211
|
+
loop
|
|
212
|
+
).result()
|
|
213
|
+
except RuntimeError:
|
|
214
|
+
return asyncio.run(self._arun(request, **kwargs))
|
|
215
|
+
|
|
216
|
+
async def stream(self, request: str, **kwargs) -> AsyncIterator[str]:
|
|
217
|
+
"""Stream responses using server-streaming gRPC."""
|
|
218
|
+
try:
|
|
219
|
+
context = kwargs.get('context', {})
|
|
220
|
+
async for chunk in self._execute_streaming(request, context):
|
|
221
|
+
yield chunk
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error(f"Error in gRPC streaming: {e}")
|
|
224
|
+
yield f"Streaming error: {str(e)}"
|
|
225
|
+
|
|
226
|
+
async def close(self):
|
|
227
|
+
"""Close the gRPC channel pool."""
|
|
228
|
+
if self._channel_pool:
|
|
229
|
+
await self._channel_pool.close()
|
|
230
|
+
self._channel_pool = None
|
|
231
|
+
self._initialized = False
|
|
232
|
+
logger.info(f"Closed gRPC connections for {self.name}")
|
|
233
|
+
|
|
234
|
+
def __del__(self):
|
|
235
|
+
"""Cleanup gRPC channels when tool is destroyed."""
|
|
236
|
+
if self._channel_pool:
|
|
237
|
+
try:
|
|
238
|
+
loop = asyncio.get_running_loop()
|
|
239
|
+
asyncio.create_task(self.close())
|
|
240
|
+
except RuntimeError:
|
|
241
|
+
loop = asyncio.new_event_loop()
|
|
242
|
+
loop.run_until_complete(self.close())
|
|
243
|
+
loop.close()
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class GrpcA2AToolkit:
|
|
249
|
+
"""Toolkit for creating gRPC-based A2A tools."""
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def create_tool_from_endpoint(
|
|
253
|
+
endpoint: str,
|
|
254
|
+
name: str,
|
|
255
|
+
description: str,
|
|
256
|
+
timeout: int = 300,
|
|
257
|
+
retry_attempts: int = 3,
|
|
258
|
+
pool_size: int = 10,
|
|
259
|
+
use_tls: bool = False,
|
|
260
|
+
tls_cert_path: Optional[str] = None
|
|
261
|
+
) -> GrpcA2ATool:
|
|
262
|
+
"""Create a gRPC A2A tool from an endpoint."""
|
|
263
|
+
config = GrpcA2AConfig(
|
|
264
|
+
endpoint=endpoint,
|
|
265
|
+
agency_name=name,
|
|
266
|
+
agency_description=description,
|
|
267
|
+
timeout=timeout,
|
|
268
|
+
retry_attempts=retry_attempts,
|
|
269
|
+
pool_size=pool_size,
|
|
270
|
+
use_tls=use_tls,
|
|
271
|
+
tls_cert_path=tls_cert_path
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return GrpcA2ATool(config)
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def create_tools_from_endpoints(
|
|
278
|
+
endpoints: List[Dict[str, Any]],
|
|
279
|
+
default_timeout: int = 300,
|
|
280
|
+
default_retry_attempts: int = 3,
|
|
281
|
+
default_pool_size: int = 10
|
|
282
|
+
) -> List[GrpcA2ATool]:
|
|
283
|
+
"""Create multiple gRPC A2A tools."""
|
|
284
|
+
tools = []
|
|
285
|
+
|
|
286
|
+
for ep_config in endpoints:
|
|
287
|
+
tool = GrpcA2AToolkit.create_tool_from_endpoint(
|
|
288
|
+
endpoint=ep_config["endpoint"],
|
|
289
|
+
name=ep_config["name"],
|
|
290
|
+
description=ep_config["description"],
|
|
291
|
+
timeout=ep_config.get("timeout", default_timeout),
|
|
292
|
+
retry_attempts=ep_config.get("retry_attempts", default_retry_attempts),
|
|
293
|
+
pool_size=ep_config.get("pool_size", default_pool_size),
|
|
294
|
+
use_tls=ep_config.get("use_tls", False),
|
|
295
|
+
tls_cert_path=ep_config.get("tls_cert_path")
|
|
296
|
+
)
|
|
297
|
+
tools.append(tool)
|
|
298
|
+
logger.info(f"Created gRPC A2A tool: {tool.name}")
|
|
299
|
+
|
|
300
|
+
return tools
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# Example usage with both HTTP/2 and gRPC
|
|
304
|
+
async def compare_protocols():
|
|
305
|
+
"""Compare HTTP/2 and gRPC performance."""
|
|
306
|
+
from .crewai_a2a_tools import A2AToolkit as HttpA2AToolkit
|
|
307
|
+
|
|
308
|
+
# Create HTTP/2 tool
|
|
309
|
+
http_tool = HttpA2AToolkit.create_tool_from_endpoint(
|
|
310
|
+
endpoint="http://localhost:8000",
|
|
311
|
+
name="Trading Agency HTTP",
|
|
312
|
+
description="Trading via HTTP/2"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Create gRPC tool
|
|
316
|
+
grpc_tool = GrpcA2AToolkit.create_tool_from_endpoint(
|
|
317
|
+
endpoint="localhost:50051",
|
|
318
|
+
name="Trading Agency gRPC",
|
|
319
|
+
description="Trading via gRPC",
|
|
320
|
+
pool_size=20 # More channels for high load
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Run parallel requests
|
|
324
|
+
import time
|
|
325
|
+
|
|
326
|
+
# HTTP/2 test
|
|
327
|
+
start = time.time()
|
|
328
|
+
http_tasks = [
|
|
329
|
+
http_tool._arun(f"Get price for BTC {i}")
|
|
330
|
+
for i in range(100)
|
|
331
|
+
]
|
|
332
|
+
await asyncio.gather(*http_tasks)
|
|
333
|
+
http_time = time.time() - start
|
|
334
|
+
|
|
335
|
+
# gRPC test
|
|
336
|
+
start = time.time()
|
|
337
|
+
grpc_tasks = [
|
|
338
|
+
grpc_tool._arun(f"Get price for BTC {i}")
|
|
339
|
+
for i in range(100)
|
|
340
|
+
]
|
|
341
|
+
await asyncio.gather(*grpc_tasks)
|
|
342
|
+
grpc_time = time.time() - start
|
|
343
|
+
|
|
344
|
+
print(f"HTTP/2 time: {http_time:.2f}s")
|
|
345
|
+
print(f"gRPC time: {grpc_time:.2f}s")
|
|
346
|
+
|
|
347
|
+
# Cleanup
|
|
348
|
+
await http_tool.close()
|
|
349
|
+
await grpc_tool.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""IATP Contracts utilities."""
|
|
2
|
+
|
|
3
|
+
from .wallet_creator import create_wallet, get_contract_config
|
|
4
|
+
from .iatp_contracts_config import get_contract_address, get_contract_abi, get_rpc_url
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"create_wallet",
|
|
8
|
+
"get_contract_config",
|
|
9
|
+
"get_contract_address",
|
|
10
|
+
"get_contract_abi",
|
|
11
|
+
"get_rpc_url"
|
|
12
|
+
]
|