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,274 @@
1
+ """A2A client implementation for CrewAI integration using the official a2a-sdk."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Dict, Any, Optional, List
6
+ from a2a.client import A2AClient, A2ACardResolver
7
+ from a2a.types import Message, TextPart, TaskState
8
+ from crewai.tools import BaseTool
9
+ import os
10
+
11
+ from ..core.models import UtilityAgentRegistryEntry
12
+ from ..registry.mongodb_registry import UtilityAgentRegistry
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class UtilityAgencyTool(BaseTool):
18
+ """CrewAI tool wrapper for utility agencies accessed via A2A."""
19
+
20
+ name: str
21
+ description: str
22
+ endpoint: str
23
+ agency_id: str
24
+ capabilities: List[str]
25
+ _client: Optional[A2AClient] = None
26
+
27
+ def __init__(self, registry_entry: UtilityAgentRegistryEntry):
28
+ """Initialize from a registry entry."""
29
+ super().__init__(
30
+ name=f"utility_agency_{registry_entry.name.replace(' ', '_').lower()}",
31
+ description=registry_entry.description,
32
+ endpoint=str(registry_entry.endpoint),
33
+ agency_id=registry_entry.agency_id,
34
+ capabilities=registry_entry.capabilities
35
+ )
36
+ self._client = None
37
+
38
+ async def _get_client(self) -> A2AClient:
39
+ """Get or create the A2A client."""
40
+ if self._client is None:
41
+ # Resolve agent card
42
+ card_resolver = A2ACardResolver(self.endpoint)
43
+ agent_card = card_resolver.get_agent_card()
44
+
45
+ # Create client with authentication if needed
46
+ auth_credentials = None
47
+ if agent_card.authentication:
48
+ # Get credentials from environment or registry
49
+ # This is a simplified example - in production, use proper credential management
50
+ auth_credentials = os.getenv(f"{self.agency_id.upper()}_AUTH")
51
+
52
+ self._client = A2AClient(
53
+ agent_card=agent_card,
54
+ credentials=auth_credentials
55
+ )
56
+ return self._client
57
+
58
+ async def _arun(self, request: str, **kwargs) -> str:
59
+ """Async execution of the tool."""
60
+ try:
61
+ client = await self._get_client()
62
+
63
+ # Create task message
64
+ message = Message(
65
+ role="user",
66
+ parts=[TextPart(text=request)]
67
+ )
68
+
69
+ # Send task and wait for completion
70
+ # The send_task method expects id and message as parameters
71
+ task = await client.send_task(
72
+ id=str(asyncio.get_event_loop().time()), # Simple unique ID
73
+ message=message
74
+ )
75
+
76
+ # Extract response
77
+ if task.status.state == TaskState.COMPLETED:
78
+ # Look for agent's response in messages
79
+ for msg in task.messages:
80
+ if msg.role == "agent":
81
+ # Combine all text parts
82
+ response_text = ""
83
+ for part in msg.parts:
84
+ if hasattr(part, 'text'):
85
+ response_text += part.text
86
+ return response_text
87
+
88
+ # If no agent message, check artifacts
89
+ if task.artifacts:
90
+ response_text = ""
91
+ for artifact in task.artifacts:
92
+ for part in artifact.parts:
93
+ if hasattr(part, 'text'):
94
+ response_text += part.text
95
+ return response_text
96
+
97
+ return "Task completed but no response found"
98
+ else:
99
+ return f"Task failed with state: {task.status.state}"
100
+
101
+ except Exception as e:
102
+ logger.error(f"Error calling A2A agent: {e}")
103
+ return f"Error: {str(e)}"
104
+
105
+ def _run(self, request: str, **kwargs) -> str:
106
+ """Sync execution of the tool."""
107
+ return asyncio.run(self._arun(request, **kwargs))
108
+
109
+
110
+ class A2AAgencyDiscovery:
111
+ """Helper class to discover A2A-compatible utility agencies."""
112
+
113
+ @staticmethod
114
+ async def discover_agent(endpoint: str) -> Optional[Dict[str, Any]]:
115
+ """Discover an agent at the given endpoint."""
116
+ try:
117
+ card_resolver = A2ACardResolver(endpoint)
118
+ agent_card = card_resolver.get_agent_card()
119
+
120
+ # Test connectivity with a simple health check
121
+ client = A2AClient(agent_card=agent_card)
122
+ # The A2A protocol doesn't define a standard health check,
123
+ # but we can try to fetch the agent card as a connectivity test
124
+
125
+ return {
126
+ "name": agent_card.name,
127
+ "description": agent_card.description,
128
+ "skills": [
129
+ {"id": skill.id, "name": skill.name, "description": skill.description}
130
+ for skill in agent_card.skills
131
+ ],
132
+ "capabilities": agent_card.capabilities.model_dump(),
133
+ "authentication": agent_card.authentication.schemes if agent_card.authentication else None
134
+ }
135
+ except Exception as e:
136
+ logger.error(f"Failed to discover agent at {endpoint}: {e}")
137
+ return None
138
+
139
+
140
+ class UtilityAgencyFinder:
141
+ """Helper class to find and create tools from utility agencies."""
142
+
143
+ def __init__(self, registry: UtilityAgentRegistry):
144
+ self.registry = registry
145
+
146
+ async def find_tools(
147
+ self,
148
+ query: Optional[str] = None,
149
+ tags: Optional[List[str]] = None,
150
+ capabilities: Optional[List[str]] = None,
151
+ limit: int = 10
152
+ ) -> List[UtilityAgencyTool]:
153
+ """Find utility agencies and create CrewAI tools from them."""
154
+ # Search the registry
155
+ entries = await self.registry.query_agencies(
156
+ query=query,
157
+ tags=tags,
158
+ capabilities=capabilities,
159
+ active_only=True,
160
+ limit=limit
161
+ )
162
+
163
+ # Create tools from entries with discovery check
164
+ tools = []
165
+ for entry in entries:
166
+ # Verify the agency is discoverable
167
+ agent_info = await A2AAgencyDiscovery.discover_agent(str(entry.endpoint))
168
+ if agent_info:
169
+ tool = UtilityAgencyTool(entry)
170
+ tools.append(tool)
171
+ logger.info(f"Created tool for agency: {entry.name}")
172
+ else:
173
+ logger.warning(f"Agency {entry.name} is not discoverable")
174
+ # Update health status in registry
175
+ await self.registry.update_health_status(entry.agency_id, is_healthy=False)
176
+
177
+ return tools
178
+
179
+ async def get_tool_by_id(self, agency_id: str) -> Optional[UtilityAgencyTool]:
180
+ """Get a specific utility agency tool by ID."""
181
+ entry = await self.registry.get_agency_by_id(agency_id)
182
+ if entry:
183
+ agent_info = await A2AAgencyDiscovery.discover_agent(str(entry.endpoint))
184
+ if agent_info:
185
+ return UtilityAgencyTool(entry)
186
+ return None
187
+
188
+
189
+ def create_utility_agency_tools(
190
+ mongodb_uri: Optional[str] = None,
191
+ query: Optional[str] = None,
192
+ tags: Optional[List[str]] = None,
193
+ capabilities: Optional[List[str]] = None
194
+ ) -> List[UtilityAgencyTool]:
195
+ """Convenience function to create utility agency tools for CrewAI.
196
+
197
+ Args:
198
+ mongodb_uri: MongoDB connection string (uses MONGODB_URI env var if not provided)
199
+ query: Search query for agencies
200
+ tags: Filter by tags
201
+ capabilities: Filter by capabilities
202
+
203
+ Returns:
204
+ List of CrewAI-compatible tools
205
+ """
206
+ async def _create_tools():
207
+ # MongoDB URI is required
208
+ if not mongodb_uri and not os.getenv("MONGODB_URI"):
209
+ raise ValueError("MongoDB URI is required. Set MONGODB_URI environment variable or pass mongodb_uri parameter.")
210
+
211
+ registry = UtilityAgentRegistry(mongodb_uri)
212
+ finder = UtilityAgencyFinder(registry)
213
+
214
+ try:
215
+ tools = await finder.find_tools(
216
+ query=query,
217
+ tags=tags,
218
+ capabilities=capabilities
219
+ )
220
+ return tools
221
+ finally:
222
+ registry.close()
223
+
224
+ return asyncio.run(_create_tools())
225
+
226
+
227
+ # Example usage for direct A2A client without CrewAI
228
+ class SimpleA2AClient:
229
+ """Simple A2A client for direct agent communication."""
230
+
231
+ def __init__(self, agent_endpoint: str, credentials: Optional[str] = None):
232
+ self.agent_endpoint = agent_endpoint
233
+ self.credentials = credentials
234
+ self._client = None
235
+
236
+ async def connect(self):
237
+ """Connect to the A2A agent."""
238
+ card_resolver = A2ACardResolver(self.agent_endpoint)
239
+ agent_card = card_resolver.get_agent_card()
240
+
241
+ self._client = A2AClient(
242
+ agent_card=agent_card,
243
+ credentials=self.credentials
244
+ )
245
+ logger.info(f"Connected to agent: {agent_card.name}")
246
+
247
+ async def send_message(self, message: str) -> str:
248
+ """Send a message to the agent and get response."""
249
+ if not self._client:
250
+ await self.connect()
251
+
252
+ msg = Message(
253
+ role="user",
254
+ parts=[TextPart(text=message)]
255
+ )
256
+
257
+ task = await self._client.send_task(
258
+ id=str(asyncio.get_event_loop().time()),
259
+ message=msg
260
+ )
261
+
262
+ # Extract response
263
+ if task.status.state == TaskState.COMPLETED:
264
+ for msg in task.messages:
265
+ if msg.role == "agent":
266
+ return " ".join(part.text for part in msg.parts if hasattr(part, 'text'))
267
+ return "No response from agent"
268
+ else:
269
+ raise RuntimeError(f"Task failed: {task.status.state}")
270
+
271
+ async def close(self):
272
+ """Close the client connection."""
273
+ # A2A client doesn't have explicit close, but we can clean up
274
+ self._client = None
@@ -0,0 +1,335 @@
1
+ """Enhanced CrewAI tools for A2A integration with utility agencies."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import traceback
6
+ from typing import Dict, Any, Optional, List, Union, AsyncIterator
7
+ from datetime import datetime
8
+ import json
9
+ import httpx
10
+ import uuid
11
+ from contextlib import asynccontextmanager
12
+ import httpcore
13
+ from enum import Enum
14
+ import threading
15
+
16
+ from crewai.tools import BaseTool
17
+ from a2a.client import A2AClient, A2ACardResolver
18
+ from a2a.types import (
19
+ Message, TextPart, TaskState, SendMessageRequest, MessageSendParams,
20
+ GetTaskRequest, Task, TaskQueryParams, TaskResubscriptionRequest
21
+ )
22
+ from pydantic import Field, BaseModel
23
+
24
+ from ..utils import get_now_in_utc
25
+ from a2a.client.errors import A2AClientError
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Thread-local storage for httpx clients
30
+ _thread_local = threading.local()
31
+
32
+
33
+ class A2AToolSchema(BaseModel):
34
+ """Input schema for A2A tools."""
35
+ request: str = Field(description="The request or query to send to the A2A agent. Send only 'request' key when using this tool")
36
+
37
+
38
+ class A2AToolConfig(BaseModel):
39
+ """Configuration for A2A tools."""
40
+ endpoint: str = Field(description="The A2A endpoint URL")
41
+ agency_name: str = Field(description="Name of the utility agency")
42
+ agency_description: str = Field(description="Description of what the agency does")
43
+ timeout: int = Field(default=300, description="Timeout in seconds for A2A calls")
44
+ retry_attempts: int = Field(default=3, description="Number of retry attempts")
45
+ poll_interval: int = Field(default=2, description="Interval in seconds between status checks")
46
+ max_poll_attempts: int = Field(default=150, description="Maximum number of polling attempts")
47
+ authentication: Optional[Dict[str, str]] = Field(default=None, description="Authentication credentials if needed")
48
+ # Connection pool settings
49
+ max_keepalive_connections: int = Field(default=20, description="Max keepalive connections in pool")
50
+ max_connections: int = Field(default=100, description="Max total connections in pool")
51
+ keepalive_expiry: int = Field(default=300, description="Keepalive expiry in seconds")
52
+ # Streaming settings
53
+ supports_streaming: bool = Field(default=False, description="Whether the agency supports SSE streaming")
54
+ stream_timeout: int = Field(default=600, description="Timeout for streaming connections")
55
+ iatp_endpoint: Optional[str] = Field(default=None, description="Specific IATP endpoint")
56
+
57
+
58
+ class A2ATool(BaseTool):
59
+ """Enhanced CrewAI tool for A2A utility agencies."""
60
+
61
+ name: str
62
+ description: str
63
+ args_schema: type[BaseModel] = A2AToolSchema
64
+ config: A2AToolConfig
65
+
66
+ def __init__(self, config: A2AToolConfig):
67
+ """Initialize the A2A tool with configuration."""
68
+ # Create a tool name from agency name
69
+ tool_name = f"a2a_{config.agency_name.replace(' ', '_').replace('-', '_').lower()}"
70
+
71
+ super().__init__(
72
+ name=tool_name,
73
+ description=f"Use {config.agency_name} via A2A: {config.agency_description}",
74
+ config=config
75
+ )
76
+
77
+ async def _run(self, **kwargs) -> str:
78
+ """Async execution of the tool - main CrewAI entry point."""
79
+ # Extract request from kwargs
80
+ request = kwargs.get('request', kwargs.get('query', ''))
81
+
82
+ logger.info(f"A2ATool._run called with request: {str(request)[:200]}...")
83
+ logger.info(f"Tool name: {self.name}, Endpoint: {self.config.endpoint}")
84
+
85
+ try:
86
+ # Create a fresh httpx client for this request
87
+ async with httpx.AsyncClient(
88
+ timeout=httpx.Timeout(
89
+ connect=5.0,
90
+ read=self.config.timeout,
91
+ write=self.config.timeout,
92
+ pool=5.0
93
+ ),
94
+ http2=True
95
+ ) as httpx_client:
96
+
97
+ # Resolve agent card
98
+ card_resolver = A2ACardResolver(
99
+ httpx_client=httpx_client,
100
+ base_url=self.config.endpoint
101
+ )
102
+ agent_card = await card_resolver.get_agent_card()
103
+ logger.info(f"Resolved agent card for: {agent_card.name}")
104
+
105
+ # Patch the agent card URL to use the actual endpoint
106
+ if hasattr(agent_card, 'url'):
107
+ from a2a.types import AgentCard
108
+ agent_card_dict = agent_card.model_dump()
109
+ agent_card_dict['url'] = self.config.endpoint
110
+ agent_card = AgentCard(**agent_card_dict)
111
+
112
+ # Create A2A client
113
+ iatp_endpoint = self.config.iatp_endpoint or self.config.endpoint
114
+ a2a_client = A2AClient(
115
+ httpx_client=httpx_client,
116
+ agent_card=agent_card,
117
+ url=iatp_endpoint
118
+ )
119
+
120
+ # Create message
121
+ message_id = f"msg_{uuid.uuid4()}"
122
+ task_id = f"task_{uuid.uuid4()}"
123
+
124
+ message = Message(
125
+ messageId=message_id,
126
+ role="user",
127
+ parts=[TextPart(text=str(request))]
128
+ )
129
+
130
+ # Create send message request
131
+ send_request = SendMessageRequest(
132
+ id=task_id,
133
+ jsonrpc="2.0",
134
+ method="message/send",
135
+ params=MessageSendParams(
136
+ message=message,
137
+ configuration=None,
138
+ metadata=None
139
+ )
140
+ )
141
+
142
+ logger.info(f"Sending message with task ID: {task_id}")
143
+
144
+ # Send message and get response
145
+ response = await a2a_client.send_message(send_request)
146
+
147
+ # Extract response text
148
+ if hasattr(response, 'error') and response.error:
149
+ return f"A2A error: {response.error}"
150
+
151
+ # Handle response
152
+ if hasattr(response, 'root'):
153
+ result = response.root.result if hasattr(response.root, 'result') else response.root
154
+ else:
155
+ result = response.result if hasattr(response, 'result') else response
156
+
157
+ # If it's a task, wait for completion
158
+ if hasattr(result, 'id') and hasattr(result, 'status'):
159
+ # It's a task - for now just return that we started it
160
+ # In a real implementation you'd poll for completion
161
+ return f"Task {result.id} started with status: {result.status.state}"
162
+
163
+ # Extract text from message response
164
+ response_parts = []
165
+ if hasattr(result, 'parts') and result.parts:
166
+ for part in result.parts:
167
+ if hasattr(part, 'root') and hasattr(part.root, 'text'):
168
+ response_parts.append(part.root.text)
169
+ elif hasattr(part, 'text'):
170
+ response_parts.append(part.text)
171
+ elif hasattr(result, 'text'):
172
+ response_parts.append(result.text)
173
+ elif isinstance(result, dict) and 'text' in result:
174
+ response_parts.append(result['text'])
175
+ else:
176
+ response_parts.append(str(result))
177
+
178
+ return "\n".join(response_parts) if response_parts else "No response content"
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error in A2A tool {self.name}: {e}")
182
+ return f"Error: {str(e)}"
183
+
184
+
185
+ class A2AToolkit:
186
+ """Toolkit for creating and managing A2A tools for CrewAI."""
187
+
188
+ @staticmethod
189
+ async def discover_agent_async(endpoint: str) -> Optional[Dict[str, Any]]:
190
+ """Async method to discover agent information."""
191
+ # Create HTTP/2 enabled client for discovery
192
+ transport = httpx.AsyncHTTPTransport(http2=True)
193
+ httpx_client = httpx.AsyncClient(
194
+ timeout=httpx.Timeout(10.0),
195
+ transport=transport
196
+ )
197
+ try:
198
+ card_resolver = A2ACardResolver(
199
+ httpx_client=httpx_client,
200
+ base_url=endpoint
201
+ )
202
+ agent_card = await card_resolver.get_agent_card()
203
+
204
+ # Check capabilities
205
+ capabilities = {}
206
+ if hasattr(agent_card, 'capabilities'):
207
+ capabilities = {
208
+ "streaming": getattr(agent_card.capabilities, 'streaming', False),
209
+ "pushNotifications": getattr(agent_card.capabilities, 'pushNotifications', False),
210
+ "stateTransitionHistory": getattr(agent_card.capabilities, 'stateTransitionHistory', False)
211
+ }
212
+
213
+ return {
214
+ "name": agent_card.name,
215
+ "description": agent_card.description,
216
+ "version": agent_card.version,
217
+ "capabilities": capabilities
218
+ }
219
+ except Exception as e:
220
+ logger.warning(f"Could not discover agent info from {endpoint}: {e}")
221
+ return None
222
+ finally:
223
+ await httpx_client.aclose()
224
+
225
+ @staticmethod
226
+ def create_tool_from_endpoint(
227
+ endpoint: str,
228
+ name: Optional[str] = None,
229
+ description: Optional[str] = None,
230
+ timeout: int = 300,
231
+ retry_attempts: int = 3,
232
+ poll_interval: int = 2,
233
+ max_poll_attempts: int = 150,
234
+ authentication: Optional[Dict[str, str]] = None,
235
+ supports_streaming: Optional[bool] = None,
236
+ iatp_endpoint: Optional[str] = None
237
+ ) -> A2ATool:
238
+ """Create an A2A tool from an endpoint."""
239
+ # If name/description not provided, try to discover from endpoint
240
+ discovered_streaming = False
241
+ if not name or not description or supports_streaming is None:
242
+ try:
243
+ # Run async discovery in sync context
244
+ agent_info = asyncio.run(A2AToolkit.discover_agent_async(endpoint))
245
+ if agent_info:
246
+ name = name or agent_info["name"]
247
+ description = description or agent_info["description"]
248
+ discovered_streaming = agent_info.get("capabilities", {}).get("streaming", False)
249
+ else:
250
+ name = name or "Unknown Agency"
251
+ description = description or "A2A utility agency"
252
+ except Exception as e:
253
+ logger.warning(f"Could not discover agent info from {endpoint}: {e}")
254
+ name = name or "Unknown Agency"
255
+ description = description or "A2A utility agency"
256
+
257
+ config = A2AToolConfig(
258
+ endpoint=endpoint,
259
+ agency_name=name,
260
+ agency_description=description,
261
+ timeout=timeout,
262
+ retry_attempts=retry_attempts,
263
+ poll_interval=poll_interval,
264
+ max_poll_attempts=max_poll_attempts,
265
+ authentication=authentication,
266
+ supports_streaming=supports_streaming if supports_streaming is not None else discovered_streaming,
267
+ iatp_endpoint=iatp_endpoint or endpoint # Default to endpoint if not specified
268
+ )
269
+
270
+ return A2ATool(config)
271
+
272
+ @staticmethod
273
+ def create_tools_from_endpoints(
274
+ endpoints: List[Dict[str, Any]],
275
+ default_timeout: int = 300,
276
+ default_retry_attempts: int = 3,
277
+ default_poll_interval: int = 2,
278
+ default_max_poll_attempts: int = 150
279
+ ) -> List[A2ATool]:
280
+ """Create multiple A2A tools from a list of endpoint configurations."""
281
+ tools = []
282
+
283
+ for ep_config in endpoints:
284
+ tool = A2AToolkit.create_tool_from_endpoint(
285
+ endpoint=ep_config["endpoint"],
286
+ name=ep_config.get("name"),
287
+ description=ep_config.get("description"),
288
+ timeout=ep_config.get("timeout", default_timeout),
289
+ retry_attempts=ep_config.get("retry_attempts", default_retry_attempts),
290
+ poll_interval=ep_config.get("poll_interval", default_poll_interval),
291
+ max_poll_attempts=ep_config.get("max_poll_attempts", default_max_poll_attempts),
292
+ authentication=ep_config.get("authentication"),
293
+ supports_streaming=ep_config.get("supports_streaming"),
294
+ iatp_endpoint=ep_config.get("iatp_endpoint")
295
+ )
296
+ tools.append(tool)
297
+ logger.info(f"Created A2A tool: {tool.name}")
298
+
299
+ return tools
300
+
301
+
302
+ # Specialized tools for common trading operations
303
+ class TradingA2ATool(A2ATool):
304
+ """Specialized A2A tool for trading operations with structured prompts."""
305
+
306
+ def __init__(self, config: A2AToolConfig):
307
+ super().__init__(config)
308
+ self.description = f"Trading tool via {config.agency_name}: Execute trades, check positions, and analyze markets"
309
+
310
+ async def get_market_info(self, symbol: str) -> str:
311
+ """Get market information for a symbol."""
312
+ request = f"Get current market information for {symbol} including price, volume, and recent trends"
313
+ return await self._run(request=request)
314
+
315
+ async def check_positions(self) -> str:
316
+ """Check current trading positions."""
317
+ request = "List all current open positions with their P&L and status"
318
+ return await self._run(request=request)
319
+
320
+ async def execute_trade(self, action: str, symbol: str, amount: float, price: Optional[float] = None) -> str:
321
+ """Execute a trade."""
322
+ if price:
323
+ request = f"Execute {action} order for {amount} units of {symbol} at price {price}"
324
+ else:
325
+ request = f"Execute {action} market order for {amount} units of {symbol}"
326
+
327
+ context = {
328
+ "action": action,
329
+ "symbol": symbol,
330
+ "amount": amount,
331
+ "price": price,
332
+ "timestamp": datetime.utcnow().isoformat()
333
+ }
334
+
335
+ return await self._run(request=request, **context)