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,565 @@
|
|
|
1
|
+
"""
|
|
2
|
+
{{ agent_name }} - A2A Server Main Entry Point
|
|
3
|
+
|
|
4
|
+
This module initializes and starts the A2A server for {{ agent_name }}.
|
|
5
|
+
Auto-generated for the {{ agent_name }} utility agent.
|
|
6
|
+
Supports HTTP/2 multiplexing and SSE streaming.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
import ssl
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import AsyncIterator, Dict, Any, Optional
|
|
15
|
+
import asyncio
|
|
16
|
+
import uuid
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
import time
|
|
19
|
+
import tempfile
|
|
20
|
+
import subprocess
|
|
21
|
+
|
|
22
|
+
from a2a.server.apps import A2AStarletteApplication
|
|
23
|
+
from a2a.types import AgentCard, AgentSkill, AgentCapabilities
|
|
24
|
+
from a2a.server.tasks import InMemoryTaskStore
|
|
25
|
+
from a2a.server.request_handlers import DefaultRequestHandler
|
|
26
|
+
from a2a.server.events.event_queue import EventQueue
|
|
27
|
+
from hypercorn.asyncio import serve
|
|
28
|
+
from hypercorn.config import Config
|
|
29
|
+
from starlette.responses import StreamingResponse
|
|
30
|
+
from starlette.requests import Request
|
|
31
|
+
from starlette.routing import Route
|
|
32
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
33
|
+
from starlette.responses import Response
|
|
34
|
+
|
|
35
|
+
# Import AgentOps for monitoring and observability
|
|
36
|
+
try:
|
|
37
|
+
import agentops
|
|
38
|
+
AGENTOPS_AVAILABLE = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
AGENTOPS_AVAILABLE = False
|
|
41
|
+
agentops = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
import sys
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
# Add parent directory to path to import mcp_agent_template
|
|
47
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
48
|
+
from traia_iatp.mcp import MCPServerConfig
|
|
49
|
+
from .agent_executor import {{ class_name }}AgentExecutor
|
|
50
|
+
|
|
51
|
+
# Configure logging FIRST (before any logger usage)
|
|
52
|
+
logging.basicConfig(
|
|
53
|
+
level=logging.INFO,
|
|
54
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
55
|
+
)
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
# Import D402 payment support
|
|
59
|
+
try:
|
|
60
|
+
from traia_iatp.x402 import (
|
|
61
|
+
D402Config,
|
|
62
|
+
D402ServicePrice,
|
|
63
|
+
require_iatp_payment,
|
|
64
|
+
add_x402_info_to_agent_card
|
|
65
|
+
)
|
|
66
|
+
D402_AVAILABLE = True
|
|
67
|
+
except ImportError:
|
|
68
|
+
logger.warning("D402 payment support not available")
|
|
69
|
+
D402_AVAILABLE = False
|
|
70
|
+
|
|
71
|
+
# Enable debug logging for HTTP/2 and connection events if requested
|
|
72
|
+
if os.environ.get("DEBUG_PROTOCOL", "false").lower() == "true":
|
|
73
|
+
logging.getLogger("hypercorn.access").setLevel(logging.DEBUG)
|
|
74
|
+
logging.getLogger("hypercorn.error").setLevel(logging.DEBUG)
|
|
75
|
+
logging.getLogger("httpcore").setLevel(logging.DEBUG)
|
|
76
|
+
logging.getLogger("httpx").setLevel(logging.DEBUG)
|
|
77
|
+
logging.getLogger("h2").setLevel(logging.DEBUG)
|
|
78
|
+
logging.getLogger("a2a").setLevel(logging.DEBUG)
|
|
79
|
+
logger.setLevel(logging.DEBUG)
|
|
80
|
+
logger.info("Protocol-level debug logging enabled")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ProtocolLoggingMiddleware(BaseHTTPMiddleware):
|
|
84
|
+
"""Middleware to log HTTP protocol details for debugging."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, app):
|
|
87
|
+
super().__init__(app)
|
|
88
|
+
self.request_counter = 0
|
|
89
|
+
|
|
90
|
+
async def dispatch(self, request: Request, call_next):
|
|
91
|
+
self.request_counter += 1
|
|
92
|
+
request_id = self.request_counter
|
|
93
|
+
|
|
94
|
+
# Log request details
|
|
95
|
+
logger.info(f"[Request {request_id}] {request.method} {request.url.path}")
|
|
96
|
+
logger.info(f"[Request {request_id}] Client: {request.client}")
|
|
97
|
+
logger.info(f"[Request {request_id}] Headers: {dict(request.headers)}")
|
|
98
|
+
|
|
99
|
+
# Check HTTP version
|
|
100
|
+
http_version = request.scope.get("http_version", "unknown")
|
|
101
|
+
logger.info(f"[Request {request_id}] HTTP Version: {http_version}")
|
|
102
|
+
|
|
103
|
+
# Check if it's HTTP/2
|
|
104
|
+
if http_version == "2.0":
|
|
105
|
+
logger.info(f"[Request {request_id}] ✅ HTTP/2 connection detected")
|
|
106
|
+
stream_id = request.scope.get("stream_id", "unknown")
|
|
107
|
+
logger.info(f"[Request {request_id}] HTTP/2 Stream ID: {stream_id}")
|
|
108
|
+
else:
|
|
109
|
+
logger.info(f"[Request {request_id}] ⚠️ HTTP/1.1 connection (not HTTP/2)")
|
|
110
|
+
|
|
111
|
+
# Time the request
|
|
112
|
+
start_time = time.time()
|
|
113
|
+
|
|
114
|
+
# Process the request
|
|
115
|
+
response = await call_next(request)
|
|
116
|
+
|
|
117
|
+
# Log response details
|
|
118
|
+
process_time = time.time() - start_time
|
|
119
|
+
logger.info(f"[Request {request_id}] Response Status: {response.status_code}")
|
|
120
|
+
logger.info(f"[Request {request_id}] Process Time: {process_time:.3f}s")
|
|
121
|
+
|
|
122
|
+
return response
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class StreamingRequestHandler(DefaultRequestHandler):
|
|
126
|
+
"""Extended request handler with SSE streaming support."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, agent_executor, task_store):
|
|
129
|
+
super().__init__(agent_executor, task_store)
|
|
130
|
+
self._active_streams: Dict[str, EventQueue] = {}
|
|
131
|
+
|
|
132
|
+
async def handle_subscribe(self, request: Dict[str, Any]) -> StreamingResponse:
|
|
133
|
+
"""Handle tasks/sendSubscribe requests for SSE streaming."""
|
|
134
|
+
params = request.get("params", {})
|
|
135
|
+
task_id = params.get("id")
|
|
136
|
+
history_length = params.get("historyLength", 0)
|
|
137
|
+
|
|
138
|
+
if not task_id:
|
|
139
|
+
return {"error": {"code": -32602, "message": "Missing task ID"}}
|
|
140
|
+
|
|
141
|
+
# Get or create event queue for this task
|
|
142
|
+
event_queue = self._active_streams.get(task_id)
|
|
143
|
+
if not event_queue:
|
|
144
|
+
event_queue = EventQueue()
|
|
145
|
+
self._active_streams[task_id] = event_queue
|
|
146
|
+
|
|
147
|
+
async def event_generator():
|
|
148
|
+
"""Generate SSE events."""
|
|
149
|
+
try:
|
|
150
|
+
# Send initial connection event
|
|
151
|
+
yield f"data: {json.dumps({'type': 'connection', 'status': 'connected', 'task_id': task_id})}\n\n"
|
|
152
|
+
|
|
153
|
+
# Send history if requested
|
|
154
|
+
if history_length > 0:
|
|
155
|
+
# TODO: Implement history retrieval from task store
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# Stream events
|
|
159
|
+
event_count = 0
|
|
160
|
+
while True:
|
|
161
|
+
try:
|
|
162
|
+
# Wait for events with timeout
|
|
163
|
+
event = await asyncio.wait_for(
|
|
164
|
+
event_queue.dequeue_event(),
|
|
165
|
+
timeout=30.0 # 30 second timeout
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if event:
|
|
169
|
+
event_count += 1
|
|
170
|
+
# Handle different event types
|
|
171
|
+
if hasattr(event, 'type') and hasattr(event, 'data'):
|
|
172
|
+
event_data = {
|
|
173
|
+
"type": event.type,
|
|
174
|
+
"sequence": event_count,
|
|
175
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
176
|
+
"data": event.data
|
|
177
|
+
}
|
|
178
|
+
else:
|
|
179
|
+
# Fallback for simple message events
|
|
180
|
+
event_data = {
|
|
181
|
+
"type": "message",
|
|
182
|
+
"sequence": event_count,
|
|
183
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
184
|
+
"data": str(event)
|
|
185
|
+
}
|
|
186
|
+
yield f"data: {json.dumps(event_data)}\n\n"
|
|
187
|
+
|
|
188
|
+
# Check for completion
|
|
189
|
+
if hasattr(event, 'type') and event.type == "status" and hasattr(event, 'data') and event.data.get("state") in ["COMPLETED", "FAILED"]:
|
|
190
|
+
yield "data: [DONE]\n\n"
|
|
191
|
+
break
|
|
192
|
+
except asyncio.TimeoutError:
|
|
193
|
+
# Send keepalive
|
|
194
|
+
yield f"data: {json.dumps({'type': 'keepalive', 'timestamp': datetime.utcnow().isoformat()})}\n\n"
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(f"Error in SSE stream: {e}")
|
|
198
|
+
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
|
199
|
+
finally:
|
|
200
|
+
# Cleanup
|
|
201
|
+
if task_id in self._active_streams:
|
|
202
|
+
del self._active_streams[task_id]
|
|
203
|
+
|
|
204
|
+
return StreamingResponse(
|
|
205
|
+
event_generator(),
|
|
206
|
+
media_type="text/event-stream",
|
|
207
|
+
headers={
|
|
208
|
+
"Cache-Control": "no-cache",
|
|
209
|
+
"Connection": "keep-alive",
|
|
210
|
+
"X-Accel-Buffering": "no" # Disable nginx buffering
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
async def handle_resubscribe(self, request: Dict[str, Any]) -> StreamingResponse:
|
|
215
|
+
"""Handle tasks/resubscribe requests to resume SSE streaming."""
|
|
216
|
+
# Resubscribe uses the same logic as subscribe
|
|
217
|
+
# but may start from a different history point
|
|
218
|
+
return await self.handle_subscribe(request)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def create_app():
|
|
222
|
+
"""Create and configure the A2A application with SSE support."""
|
|
223
|
+
|
|
224
|
+
# Load agent configuration if it exists
|
|
225
|
+
config_path = "agent_config.json"
|
|
226
|
+
if os.path.exists(config_path):
|
|
227
|
+
with open(config_path, "r") as f:
|
|
228
|
+
config_data = json.load(f)
|
|
229
|
+
mcp_data = config_data.get("mcp_server", {})
|
|
230
|
+
else:
|
|
231
|
+
# Use template variables directly
|
|
232
|
+
mcp_data = {
|
|
233
|
+
"name": "{{ mcp_server_name }}",
|
|
234
|
+
"url": "{{ mcp_server_url }}",
|
|
235
|
+
"description": "{{ mcp_server_description }}",
|
|
236
|
+
"server_type": "{{ mcp_server_type }}",
|
|
237
|
+
"capabilities": {{ mcp_server_capabilities | tojson }},
|
|
238
|
+
"metadata": {{ mcp_server_metadata | tojson }}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Create MCP server configuration
|
|
242
|
+
mcp_config = MCPServerConfig(
|
|
243
|
+
name=mcp_data["name"],
|
|
244
|
+
url=mcp_data["url"],
|
|
245
|
+
description=mcp_data["description"],
|
|
246
|
+
server_type=mcp_data.get("server_type", "streamable-http"),
|
|
247
|
+
capabilities=mcp_data.get("capabilities", []),
|
|
248
|
+
metadata=mcp_data.get("metadata", {})
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Check if MCP server supports streaming
|
|
252
|
+
supports_streaming = mcp_data.get("server_type") == "streamable-http" or \
|
|
253
|
+
"stream" in mcp_data.get("capabilities", [])
|
|
254
|
+
|
|
255
|
+
# D402 Configuration (if enabled)
|
|
256
|
+
x402_enabled = D402_AVAILABLE and os.getenv("D402_ENABLED", "false").lower() == "true"
|
|
257
|
+
x402_config = None
|
|
258
|
+
|
|
259
|
+
if x402_enabled:
|
|
260
|
+
logger.info("D402 payments enabled")
|
|
261
|
+
|
|
262
|
+
contract_address = os.getenv("UTILITY_AGENT_CONTRACT_ADDRESS")
|
|
263
|
+
if not contract_address:
|
|
264
|
+
logger.error("D402 enabled but UTILITY_AGENT_CONTRACT_ADDRESS not set!")
|
|
265
|
+
x402_enabled = False
|
|
266
|
+
else:
|
|
267
|
+
x402_config = D402Config(
|
|
268
|
+
enabled=True,
|
|
269
|
+
pay_to_address=contract_address,
|
|
270
|
+
default_price=D402ServicePrice(
|
|
271
|
+
usd_amount=os.getenv("D402_PRICE_USD", "0.01"),
|
|
272
|
+
network=os.getenv("D402_NETWORK", "sepolia"),
|
|
273
|
+
asset_address=os.getenv("D402_TOKEN_ADDRESS", ""),
|
|
274
|
+
max_timeout_seconds=300
|
|
275
|
+
),
|
|
276
|
+
facilitator_url=os.getenv("D402_FACILITATOR_URL", "http://localhost:8080"),
|
|
277
|
+
service_description="{{ agent_description }}",
|
|
278
|
+
protected_paths=["*"] # Protect all paths except .well-known
|
|
279
|
+
)
|
|
280
|
+
logger.info(f"D402 configured: ${x402_config.default_price.usd_amount} {os.getenv('D402_TOKEN', 'USDC')} per request")
|
|
281
|
+
logger.info(f" Pay to: {contract_address}")
|
|
282
|
+
logger.info(f" Network: {x402_config.default_price.network}")
|
|
283
|
+
logger.info(f" Facilitator: {x402_config.facilitator_url}")
|
|
284
|
+
|
|
285
|
+
# Create agent skills based on MCP capabilities
|
|
286
|
+
skills = []
|
|
287
|
+
|
|
288
|
+
# Add main processing skill
|
|
289
|
+
main_skill = AgentSkill(
|
|
290
|
+
id="process_request",
|
|
291
|
+
name=f"Process request using {{ agent_name }}",
|
|
292
|
+
description="{{ agent_description }}",
|
|
293
|
+
examples=[
|
|
294
|
+
{% for example in skill_examples %}
|
|
295
|
+
"{{ example }}",
|
|
296
|
+
{% endfor %}
|
|
297
|
+
],
|
|
298
|
+
inputModes=["text", "text/plain"],
|
|
299
|
+
outputModes=["text", "text/plain", "text/event-stream"] if supports_streaming else ["text", "text/plain"],
|
|
300
|
+
tags=[
|
|
301
|
+
"mcp", "{{ mcp_server_name }}", "utility",
|
|
302
|
+
{% if mcp_server_metadata.tags %}
|
|
303
|
+
{% for tag in mcp_server_metadata.tags %}
|
|
304
|
+
"{{ tag }}",
|
|
305
|
+
{% endfor %}
|
|
306
|
+
{% endif %}
|
|
307
|
+
{% for capability in mcp_server_capabilities[:5] %}
|
|
308
|
+
"{{ capability }}",
|
|
309
|
+
{% endfor %}
|
|
310
|
+
]
|
|
311
|
+
)
|
|
312
|
+
skills.append(main_skill)
|
|
313
|
+
|
|
314
|
+
{% if expose_individual_tools %}
|
|
315
|
+
# Add individual MCP tool skills
|
|
316
|
+
{% for capability in mcp_server_capabilities %}
|
|
317
|
+
skill_{{ loop.index }} = AgentSkill(
|
|
318
|
+
id="mcp_{{ capability }}",
|
|
319
|
+
name="Execute {{ capability }}",
|
|
320
|
+
description="Execute {{ capability }} tool on MCP server",
|
|
321
|
+
examples=[f"Run {{ capability }} with these parameters"],
|
|
322
|
+
inputModes=["text", "text/plain"],
|
|
323
|
+
outputModes=["text", "text/plain", "text/event-stream"] if supports_streaming else ["text", "text/plain"],
|
|
324
|
+
tags=["mcp", "{{ mcp_server_name }}", "{{ capability }}"]
|
|
325
|
+
)
|
|
326
|
+
skills.append(skill_{{ loop.index }})
|
|
327
|
+
{% endfor %}
|
|
328
|
+
{% endif %}
|
|
329
|
+
|
|
330
|
+
# Create capabilities with streaming support if available
|
|
331
|
+
capabilities = AgentCapabilities(
|
|
332
|
+
streaming=supports_streaming, # Enable streaming if MCP server supports it
|
|
333
|
+
pushNotifications=False, # Can be extended to support push notifications
|
|
334
|
+
stateTransitionHistory=True # Enable for SSE history support
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Authentication can be added here if needed in the future
|
|
338
|
+
# Currently the A2A protocol handles authentication at a different layer
|
|
339
|
+
|
|
340
|
+
# Create agent card
|
|
341
|
+
agent_card_dict = {
|
|
342
|
+
"name": "{{ agent_id }}",
|
|
343
|
+
"description": "{{ agent_description }}",
|
|
344
|
+
"url": f"http://0.0.0.0:{os.environ.get('PORT', 8000)}",
|
|
345
|
+
"version": "{{ agent_version }}",
|
|
346
|
+
"capabilities": capabilities.model_dump(),
|
|
347
|
+
"skills": [s.model_dump() for s in skills],
|
|
348
|
+
"defaultInputModes": ["text", "text/plain"],
|
|
349
|
+
"defaultOutputModes": ["text", "text/plain", "text/event-stream"] if supports_streaming else ["text", "text/plain"]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Add x402 payment info to agent card if enabled
|
|
353
|
+
if x402_enabled and x402_config:
|
|
354
|
+
logger.info("Adding x402 payment information to agent card")
|
|
355
|
+
agent_card_dict = asyncio.run(add_x402_info_to_agent_card(agent_card_dict, x402_config))
|
|
356
|
+
|
|
357
|
+
agent_card = AgentCard(**agent_card_dict)
|
|
358
|
+
|
|
359
|
+
# Create executor with MCP config
|
|
360
|
+
executor = {{ class_name }}AgentExecutor(mcp_config, supports_streaming=supports_streaming)
|
|
361
|
+
|
|
362
|
+
# Create task store and request handler with streaming support
|
|
363
|
+
task_store = InMemoryTaskStore()
|
|
364
|
+
request_handler = StreamingRequestHandler(
|
|
365
|
+
agent_executor=executor,
|
|
366
|
+
task_store=task_store
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Create the A2A application
|
|
370
|
+
app = A2AStarletteApplication(
|
|
371
|
+
agent_card=agent_card,
|
|
372
|
+
http_handler=request_handler
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Build the Starlette app - this should add all necessary routes including JSON-RPC endpoint
|
|
376
|
+
starlette_app = app.build()
|
|
377
|
+
|
|
378
|
+
# Add x402 payment middleware if enabled
|
|
379
|
+
if x402_enabled and x402_config:
|
|
380
|
+
logger.info("Adding x402 payment middleware")
|
|
381
|
+
|
|
382
|
+
@starlette_app.middleware("http")
|
|
383
|
+
async def payment_middleware(request: Request, call_next):
|
|
384
|
+
middleware = require_iatp_payment(x402_config)
|
|
385
|
+
return await middleware(request, call_next)
|
|
386
|
+
|
|
387
|
+
# Add protocol logging middleware if debug mode is enabled
|
|
388
|
+
if os.environ.get("DEBUG_PROTOCOL", "false").lower() == "true":
|
|
389
|
+
starlette_app.add_middleware(ProtocolLoggingMiddleware)
|
|
390
|
+
logger.info("Added protocol logging middleware")
|
|
391
|
+
|
|
392
|
+
# Log all routes that were created
|
|
393
|
+
logger.info("Routes created by A2A application:")
|
|
394
|
+
for route in starlette_app.routes:
|
|
395
|
+
if hasattr(route, 'path') and hasattr(route, 'methods'):
|
|
396
|
+
logger.info(f" {route.methods} {route.path}")
|
|
397
|
+
else:
|
|
398
|
+
logger.info(f" {route}")
|
|
399
|
+
|
|
400
|
+
# The A2AStarletteApplication.build() should have already added the JSON-RPC endpoint
|
|
401
|
+
# We only need to add custom SSE endpoints if they're not already included
|
|
402
|
+
|
|
403
|
+
# Add SSE endpoints using Starlette's routing
|
|
404
|
+
async def handle_subscribe_endpoint(request: Request):
|
|
405
|
+
"""Handle SSE subscription requests."""
|
|
406
|
+
data = await request.json()
|
|
407
|
+
return await request_handler.handle_subscribe(data)
|
|
408
|
+
|
|
409
|
+
async def handle_resubscribe_endpoint(request: Request):
|
|
410
|
+
"""Handle SSE resubscription requests."""
|
|
411
|
+
data = await request.json()
|
|
412
|
+
return await request_handler.handle_resubscribe(data)
|
|
413
|
+
|
|
414
|
+
# Add custom SSE routes (these might not be included by default)
|
|
415
|
+
starlette_app.routes.append(
|
|
416
|
+
Route("/a2a/tasks/subscribe", handle_subscribe_endpoint, methods=["POST"])
|
|
417
|
+
)
|
|
418
|
+
starlette_app.routes.append(
|
|
419
|
+
Route("/a2a/tasks/resubscribe", handle_resubscribe_endpoint, methods=["POST"])
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return starlette_app
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def generate_self_signed_cert(cert_path: str = "cert.pem", key_path: str = "key.pem") -> None:
|
|
426
|
+
"""Generate self-signed certificate for local development."""
|
|
427
|
+
if Path(cert_path).exists() and Path(key_path).exists():
|
|
428
|
+
logger.info("TLS certificates already exist")
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
logger.info("Generating self-signed certificates for local development...")
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
# Generate self-signed certificate using openssl
|
|
435
|
+
subprocess.run([
|
|
436
|
+
"openssl", "req", "-x509", "-newkey", "rsa:4096",
|
|
437
|
+
"-keyout", key_path, "-out", cert_path,
|
|
438
|
+
"-days", "365", "-nodes",
|
|
439
|
+
"-subj", "/CN=localhost/O=A2A Development/C=US"
|
|
440
|
+
], check=True, capture_output=True)
|
|
441
|
+
|
|
442
|
+
logger.info(f"Generated self-signed certificates: {cert_path}, {key_path}")
|
|
443
|
+
except subprocess.CalledProcessError as e:
|
|
444
|
+
logger.error(f"Failed to generate certificates: {e}")
|
|
445
|
+
logger.error(f"Stdout: {e.stdout}")
|
|
446
|
+
logger.error(f"Stderr: {e.stderr}")
|
|
447
|
+
raise
|
|
448
|
+
except FileNotFoundError:
|
|
449
|
+
logger.error("OpenSSL not found. Please install OpenSSL to generate certificates.")
|
|
450
|
+
raise
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
async def main():
|
|
454
|
+
"""Main function to start the A2A server with HTTP/2 support."""
|
|
455
|
+
|
|
456
|
+
# Initialize AgentOps for monitoring and observability
|
|
457
|
+
agentops_session_id = None
|
|
458
|
+
if AGENTOPS_AVAILABLE:
|
|
459
|
+
agentops_api_key = os.environ.get("AGENTOPS_API_KEY")
|
|
460
|
+
if agentops_api_key:
|
|
461
|
+
try:
|
|
462
|
+
# Initialize AgentOps with CrewAI-friendly settings
|
|
463
|
+
agentops_session_id = agentops.init(
|
|
464
|
+
api_key=agentops_api_key,
|
|
465
|
+
skip_auto_end_session=True, # Let CrewAI handle session lifecycle
|
|
466
|
+
tags=["{{ agent_name }}", "{{ mcp_server_name }}", "IATP", "A2A"],
|
|
467
|
+
auto_start_session=True
|
|
468
|
+
)
|
|
469
|
+
logger.info(f"✅ AgentOps initialized successfully - Session ID: {agentops_session_id}")
|
|
470
|
+
logger.info("📊 View session at: https://app.agentops.ai")
|
|
471
|
+
except Exception as e:
|
|
472
|
+
logger.warning(f"⚠️ Failed to initialize AgentOps: {e}")
|
|
473
|
+
logger.warning(" Continuing without AgentOps monitoring...")
|
|
474
|
+
else:
|
|
475
|
+
logger.info("ℹ️ AGENTOPS_API_KEY not set - AgentOps monitoring disabled")
|
|
476
|
+
logger.info(" Set AGENTOPS_API_KEY environment variable to enable monitoring")
|
|
477
|
+
else:
|
|
478
|
+
logger.info("ℹ️ AgentOps not installed - monitoring disabled")
|
|
479
|
+
logger.info(" Install with: pip install agentops")
|
|
480
|
+
|
|
481
|
+
# Get configuration from environment
|
|
482
|
+
host = os.environ.get("HOST", "0.0.0.0")
|
|
483
|
+
port = int(os.environ.get("PORT", 8000))
|
|
484
|
+
|
|
485
|
+
# Create the application
|
|
486
|
+
app = create_app()
|
|
487
|
+
|
|
488
|
+
# Configure Hypercorn for HTTP/2 support
|
|
489
|
+
config = Config()
|
|
490
|
+
config.bind = [f"{host}:{port}"]
|
|
491
|
+
config.alpn_protocols = ["h2", "http/1.1"] # Support both HTTP/2 and HTTP/1.1
|
|
492
|
+
|
|
493
|
+
# Enable access logging if debug mode is on
|
|
494
|
+
if os.environ.get("DEBUG_PROTOCOL", "false").lower() == "true":
|
|
495
|
+
config.accesslog = "-" # Log to stdout
|
|
496
|
+
config.errorlog = "-" # Log errors to stdout
|
|
497
|
+
config.loglevel = "DEBUG"
|
|
498
|
+
logger.info("Hypercorn access logging enabled")
|
|
499
|
+
|
|
500
|
+
# Enable HTTP/2
|
|
501
|
+
config.h2_max_concurrent_streams = 100
|
|
502
|
+
config.h2_max_header_list_size = 8192
|
|
503
|
+
config.h2_max_inbound_frame_size = 16384
|
|
504
|
+
config.h2_initial_connection_window_size = 65536
|
|
505
|
+
|
|
506
|
+
# Connection settings for high performance
|
|
507
|
+
config.keep_alive_timeout = 300 # 5 minutes
|
|
508
|
+
config.max_requests = 10000 # Max requests per connection
|
|
509
|
+
config.max_requests_jitter = 1000 # Add jitter to prevent thundering herd
|
|
510
|
+
|
|
511
|
+
# SSL/TLS configuration for production (optional)
|
|
512
|
+
if os.environ.get("USE_TLS", "false").lower() == "true":
|
|
513
|
+
cert_path = os.environ.get("TLS_CERT_PATH", "cert.pem")
|
|
514
|
+
key_path = os.environ.get("TLS_KEY_PATH", "key.pem")
|
|
515
|
+
|
|
516
|
+
# Generate self-signed certificates for local development if needed
|
|
517
|
+
if os.environ.get("GENERATE_CERTS", "true").lower() == "true":
|
|
518
|
+
generate_self_signed_cert(cert_path, key_path)
|
|
519
|
+
|
|
520
|
+
if Path(cert_path).exists() and Path(key_path).exists():
|
|
521
|
+
config.certfile = cert_path
|
|
522
|
+
config.keyfile = key_path
|
|
523
|
+
config.alpn_protocols = ["h2", "http/1.1"]
|
|
524
|
+
logger.info("TLS enabled with HTTP/2 support")
|
|
525
|
+
logger.info("Using HTTPS - connect to https://localhost:8000")
|
|
526
|
+
else:
|
|
527
|
+
logger.warning("TLS requested but certificates not found")
|
|
528
|
+
|
|
529
|
+
# Log startup information
|
|
530
|
+
logger.info(f"Starting {{ agent_name }} A2A Server with HTTP/2 support")
|
|
531
|
+
logger.info(f"MCP Server: {{ mcp_server_name }}")
|
|
532
|
+
logger.info(f"Listening on {host}:{port}")
|
|
533
|
+
logger.info(f"Agent Card available at: http://{host}:{port}/.well-known/agent.json")
|
|
534
|
+
logger.info(f"SSE endpoints available at: /a2a/tasks/subscribe and /a2a/tasks/resubscribe")
|
|
535
|
+
logger.info(f"HTTP/2 multiplexing enabled with max {config.h2_max_concurrent_streams} concurrent streams")
|
|
536
|
+
|
|
537
|
+
if agentops_session_id:
|
|
538
|
+
logger.info(f"📊 AgentOps Session ID: {agentops_session_id}")
|
|
539
|
+
logger.info("📊 Monitor agent performance at: https://app.agentops.ai")
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
# Run the server with Hypercorn
|
|
543
|
+
await serve(app, config)
|
|
544
|
+
except KeyboardInterrupt:
|
|
545
|
+
logger.info("🛑 Server shutdown requested")
|
|
546
|
+
if AGENTOPS_AVAILABLE and agentops_session_id:
|
|
547
|
+
try:
|
|
548
|
+
agentops.end_session('Success')
|
|
549
|
+
logger.info("✅ AgentOps session ended successfully")
|
|
550
|
+
except Exception as e:
|
|
551
|
+
logger.warning(f"⚠️ Failed to end AgentOps session: {e}")
|
|
552
|
+
except Exception as e:
|
|
553
|
+
logger.error(f"❌ Server error: {e}")
|
|
554
|
+
if AGENTOPS_AVAILABLE and agentops_session_id:
|
|
555
|
+
try:
|
|
556
|
+
agentops.end_session('Fail', end_state_reason=str(e))
|
|
557
|
+
logger.info("📊 AgentOps session ended with failure status")
|
|
558
|
+
except Exception as ae:
|
|
559
|
+
logger.warning(f"⚠️ Failed to end AgentOps session: {ae}")
|
|
560
|
+
raise
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
if __name__ == "__main__":
|
|
564
|
+
import asyncio
|
|
565
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
{{ agent_name }} - CrewAI Agent Implementation
|
|
3
|
+
|
|
4
|
+
This module defines the CrewAI agent that wraps the {{ mcp_server_name }} MCP server.
|
|
5
|
+
Auto-generated for the {{ agent_name }} utility agent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import datetime
|
|
10
|
+
from typing import List, Dict, Any, Optional
|
|
11
|
+
from crewai import Agent, Task, Crew, LLM
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
# Import MCP integration from traia_iatp.mcp
|
|
15
|
+
from traia_iatp.mcp import MCPServerConfig, MCPAgentBuilder, run_with_mcp_tools, MCPServerInfo
|
|
16
|
+
|
|
17
|
+
# Import AgentOps for operation tracking
|
|
18
|
+
import agentops
|
|
19
|
+
|
|
20
|
+
DEFAULT_LLM = LLM(
|
|
21
|
+
model=os.getenv("LLM_MODEL", "gpt-4.1-nano"), # Using environment variable with fallback
|
|
22
|
+
temperature=float(os.getenv("LLM_MODEL_TEMPERATURE", "0.1")),
|
|
23
|
+
api_key=os.getenv("OPENAI_API_KEY")
|
|
24
|
+
)
|
|
25
|
+
current_time = datetime.datetime.utcnow()
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
logger.info(f"Current LLM model used: {os.getenv("LLM_MODEL", "gpt-4.1-nano")}")
|
|
30
|
+
|
|
31
|
+
class {{ class_name }}Agent:
|
|
32
|
+
"""{{ agent_name }} agent that processes requests using {{ mcp_server_name }}."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, mcp_config: MCPServerConfig):
|
|
35
|
+
self.mcp_config = mcp_config
|
|
36
|
+
self.mcp_server_info = self._create_server_info()
|
|
37
|
+
|
|
38
|
+
def _create_server_info(self) -> MCPServerInfo:
|
|
39
|
+
"""Create MCPServerInfo from config."""
|
|
40
|
+
return MCPServerInfo(
|
|
41
|
+
id="", # Not needed for direct usage
|
|
42
|
+
name=self.mcp_config.name,
|
|
43
|
+
url=self.mcp_config.url,
|
|
44
|
+
description=self.mcp_config.description,
|
|
45
|
+
server_type=self.mcp_config.server_type,
|
|
46
|
+
capabilities=self.mcp_config.capabilities,
|
|
47
|
+
metadata=self.mcp_config.metadata,
|
|
48
|
+
tags=self.mcp_config.metadata.get("tags", [])
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def create_agent(self, tools_subset: Optional[List[str]] = None) -> Agent:
|
|
52
|
+
"""Create a CrewAI agent for this MCP server."""
|
|
53
|
+
# Use MCPAgentBuilder to create the agent
|
|
54
|
+
return MCPAgentBuilder.create_agent(
|
|
55
|
+
role="{{ agent_name }} Specialist",
|
|
56
|
+
goal="Process requests using {{ mcp_server_name }} capabilities to provide accurate and helpful responses",
|
|
57
|
+
backstory=(
|
|
58
|
+
"You are an expert at using {{ mcp_server_name }}. "
|
|
59
|
+
"{{ mcp_server_description }} "
|
|
60
|
+
"You excel at understanding user requests and utilizing the available tools to provide comprehensive solutions."
|
|
61
|
+
),
|
|
62
|
+
verbose=True,
|
|
63
|
+
allow_delegation=False,
|
|
64
|
+
llm=DEFAULT_LLM,
|
|
65
|
+
tools_subset=tools_subset
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
agentops.init(trace_name=f"Utility Agent for MCP: {self.mcp_config.name}", default_tags=[f"current time: {current_time}"])
|
|
69
|
+
def process_request(self, request: str, context: Dict[str, Any] = None) -> str:
|
|
70
|
+
"""Process a request using the MCP server capabilities."""
|
|
71
|
+
try:
|
|
72
|
+
# Create an agent for this request
|
|
73
|
+
agent = self.create_agent()
|
|
74
|
+
|
|
75
|
+
# Create a task
|
|
76
|
+
task = Task(
|
|
77
|
+
description=request,
|
|
78
|
+
expected_output="A comprehensive response based on {{ mcp_server_name }} capabilities",
|
|
79
|
+
agent=agent
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Run with MCP tools
|
|
83
|
+
result = run_with_mcp_tools(
|
|
84
|
+
tasks=[task],
|
|
85
|
+
mcp_server=self.mcp_server_info,
|
|
86
|
+
inputs=context or {},
|
|
87
|
+
skip_health_check=True # Skip for production usage
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return str(result)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Error processing request: {e}")
|
|
94
|
+
raise
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{ agent_name }}",
|
|
3
|
+
"description": "{{ agent_description }}",
|
|
4
|
+
"version": "{{ agent_version }}",
|
|
5
|
+
"agent_id": "{{ agent_id }}",
|
|
6
|
+
"mcp_server": {
|
|
7
|
+
"name": "{{ mcp_server_name }}",
|
|
8
|
+
"url": "{{ mcp_server_url }}",
|
|
9
|
+
"description": "{{ mcp_server_description }}",
|
|
10
|
+
"server_type": "{{ mcp_server_type }}",
|
|
11
|
+
"capabilities": {{ mcp_server_capabilities | tojson }},
|
|
12
|
+
"metadata": {{ mcp_server_metadata | tojson }}
|
|
13
|
+
},
|
|
14
|
+
"a2a_config": {
|
|
15
|
+
"expose_individual_tools": {{ expose_individual_tools | lower }},
|
|
16
|
+
"auth_required": {{ auth_required | lower }},
|
|
17
|
+
{% if auth_required %}
|
|
18
|
+
"auth_schemes": {{ auth_schemes | tojson }},
|
|
19
|
+
{% endif %}
|
|
20
|
+
"skill_examples": {{ skill_examples | tojson }}
|
|
21
|
+
}
|
|
22
|
+
}
|