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.

Files changed (107) hide show
  1. traia_iatp/README.md +368 -0
  2. traia_iatp/__init__.py +54 -0
  3. traia_iatp/cli/__init__.py +5 -0
  4. traia_iatp/cli/main.py +483 -0
  5. traia_iatp/client/__init__.py +10 -0
  6. traia_iatp/client/a2a_client.py +274 -0
  7. traia_iatp/client/crewai_a2a_tools.py +335 -0
  8. traia_iatp/client/d402_a2a_client.py +293 -0
  9. traia_iatp/client/grpc_a2a_tools.py +349 -0
  10. traia_iatp/client/root_path_a2a_client.py +1 -0
  11. traia_iatp/contracts/__init__.py +12 -0
  12. traia_iatp/contracts/iatp_contracts_config.py +263 -0
  13. traia_iatp/contracts/wallet_creator.py +255 -0
  14. traia_iatp/core/__init__.py +43 -0
  15. traia_iatp/core/models.py +172 -0
  16. traia_iatp/d402/__init__.py +55 -0
  17. traia_iatp/d402/chains.py +102 -0
  18. traia_iatp/d402/client.py +150 -0
  19. traia_iatp/d402/clients/__init__.py +7 -0
  20. traia_iatp/d402/clients/base.py +218 -0
  21. traia_iatp/d402/clients/httpx.py +219 -0
  22. traia_iatp/d402/common.py +114 -0
  23. traia_iatp/d402/encoding.py +28 -0
  24. traia_iatp/d402/examples/client_example.py +197 -0
  25. traia_iatp/d402/examples/server_example.py +171 -0
  26. traia_iatp/d402/facilitator.py +453 -0
  27. traia_iatp/d402/fastapi_middleware/__init__.py +6 -0
  28. traia_iatp/d402/fastapi_middleware/middleware.py +225 -0
  29. traia_iatp/d402/fastmcp_middleware.py +147 -0
  30. traia_iatp/d402/mcp_middleware.py +434 -0
  31. traia_iatp/d402/middleware.py +193 -0
  32. traia_iatp/d402/models.py +116 -0
  33. traia_iatp/d402/networks.py +98 -0
  34. traia_iatp/d402/path.py +43 -0
  35. traia_iatp/d402/payment_introspection.py +104 -0
  36. traia_iatp/d402/payment_signing.py +178 -0
  37. traia_iatp/d402/paywall.py +119 -0
  38. traia_iatp/d402/starlette_middleware.py +326 -0
  39. traia_iatp/d402/template.py +1 -0
  40. traia_iatp/d402/types.py +300 -0
  41. traia_iatp/mcp/__init__.py +18 -0
  42. traia_iatp/mcp/client.py +201 -0
  43. traia_iatp/mcp/d402_mcp_tool_adapter.py +361 -0
  44. traia_iatp/mcp/mcp_agent_template.py +481 -0
  45. traia_iatp/mcp/templates/Dockerfile.j2 +80 -0
  46. traia_iatp/mcp/templates/README.md.j2 +310 -0
  47. traia_iatp/mcp/templates/cursor-rules.md.j2 +520 -0
  48. traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
  49. traia_iatp/mcp/templates/docker-compose.yml.j2 +32 -0
  50. traia_iatp/mcp/templates/dockerignore.j2 +47 -0
  51. traia_iatp/mcp/templates/env.example.j2 +57 -0
  52. traia_iatp/mcp/templates/gitignore.j2 +77 -0
  53. traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
  54. traia_iatp/mcp/templates/pyproject.toml.j2 +32 -0
  55. traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
  56. traia_iatp/mcp/templates/run_local_docker.sh.j2 +390 -0
  57. traia_iatp/mcp/templates/server.py.j2 +175 -0
  58. traia_iatp/mcp/traia_mcp_adapter.py +543 -0
  59. traia_iatp/preview_diagrams.html +181 -0
  60. traia_iatp/registry/__init__.py +26 -0
  61. traia_iatp/registry/atlas_search_indexes.json +280 -0
  62. traia_iatp/registry/embeddings.py +298 -0
  63. traia_iatp/registry/iatp_search_api.py +846 -0
  64. traia_iatp/registry/mongodb_registry.py +771 -0
  65. traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
  66. traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
  67. traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
  68. traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
  69. traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
  70. traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
  71. traia_iatp/registry/readmes/README.md +251 -0
  72. traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
  73. traia_iatp/scripts/__init__.py +2 -0
  74. traia_iatp/scripts/create_wallet.py +244 -0
  75. traia_iatp/server/__init__.py +15 -0
  76. traia_iatp/server/a2a_server.py +219 -0
  77. traia_iatp/server/example_template_usage.py +72 -0
  78. traia_iatp/server/iatp_server_agent_generator.py +237 -0
  79. traia_iatp/server/iatp_server_template_generator.py +235 -0
  80. traia_iatp/server/templates/.dockerignore.j2 +48 -0
  81. traia_iatp/server/templates/Dockerfile.j2 +49 -0
  82. traia_iatp/server/templates/README.md +137 -0
  83. traia_iatp/server/templates/README.md.j2 +425 -0
  84. traia_iatp/server/templates/__init__.py +1 -0
  85. traia_iatp/server/templates/__main__.py.j2 +565 -0
  86. traia_iatp/server/templates/agent.py.j2 +94 -0
  87. traia_iatp/server/templates/agent_config.json.j2 +22 -0
  88. traia_iatp/server/templates/agent_executor.py.j2 +279 -0
  89. traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
  90. traia_iatp/server/templates/env.example.j2 +84 -0
  91. traia_iatp/server/templates/gitignore.j2 +78 -0
  92. traia_iatp/server/templates/grpc_server.py.j2 +218 -0
  93. traia_iatp/server/templates/pyproject.toml.j2 +78 -0
  94. traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
  95. traia_iatp/server/templates/server.py.j2 +243 -0
  96. traia_iatp/special_agencies/__init__.py +4 -0
  97. traia_iatp/special_agencies/registry_search_agency.py +392 -0
  98. traia_iatp/utils/__init__.py +10 -0
  99. traia_iatp/utils/docker_utils.py +251 -0
  100. traia_iatp/utils/general.py +64 -0
  101. traia_iatp/utils/iatp_utils.py +126 -0
  102. traia_iatp-0.1.29.dist-info/METADATA +423 -0
  103. traia_iatp-0.1.29.dist-info/RECORD +107 -0
  104. traia_iatp-0.1.29.dist-info/WHEEL +5 -0
  105. traia_iatp-0.1.29.dist-info/entry_points.txt +2 -0
  106. traia_iatp-0.1.29.dist-info/licenses/LICENSE +21 -0
  107. 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
+ ]