traia-iatp 0.1.1__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 +30 -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/grpc_a2a_tools.py +349 -0
- traia_iatp/client/root_path_a2a_client.py +1 -0
- traia_iatp/core/__init__.py +43 -0
- traia_iatp/core/models.py +161 -0
- traia_iatp/mcp/__init__.py +15 -0
- traia_iatp/mcp/client.py +201 -0
- traia_iatp/mcp/mcp_agent_template.py +422 -0
- traia_iatp/mcp/templates/Dockerfile.j2 +56 -0
- traia_iatp/mcp/templates/README.md.j2 +212 -0
- traia_iatp/mcp/templates/cursor-rules.md.j2 +326 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
- traia_iatp/mcp/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/mcp/templates/dockerignore.j2 +47 -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 +26 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +94 -0
- traia_iatp/mcp/templates/server.py.j2 +240 -0
- traia_iatp/mcp/traia_mcp_adapter.py +381 -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 +839 -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/server/__init__.py +15 -0
- traia_iatp/server/a2a_server.py +215 -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/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 +450 -0
- traia_iatp/server/templates/agent.py.j2 +80 -0
- traia_iatp/server/templates/agent_config.json.j2 +22 -0
- traia_iatp/server/templates/agent_executor.py.j2 +264 -0
- traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/server/templates/env.example.j2 +67 -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 +76 -0
- traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
- traia_iatp/server/templates/server.py.j2 +190 -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.1.dist-info/METADATA +414 -0
- traia_iatp-0.1.1.dist-info/RECORD +72 -0
- traia_iatp-0.1.1.dist-info/WHEEL +5 -0
- traia_iatp-0.1.1.dist-info/entry_points.txt +2 -0
- traia_iatp-0.1.1.dist-info/licenses/LICENSE +21 -0
- traia_iatp-0.1.1.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")
|
|
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)
|