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,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
+ }