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,191 @@
1
+ # Registry and Search API Refactoring Summary
2
+
3
+ ## Overview
4
+
5
+ This refactoring separates the concerns between registry management (write operations) and search/query operations (read operations) to eliminate duplication and improve maintainability.
6
+
7
+ ## Changes Made
8
+
9
+ ### 1. `mongodb_registry.py` - Write Operations Only
10
+
11
+ **Removed Methods:**
12
+ - `query_agents()` - Moved to search API
13
+ - `atlas_search()` - Moved to search API
14
+ - `vector_search()` - Moved to search API
15
+ - `vector_search_text()` - Moved to search API
16
+ - `query_mcp_servers()` - Moved to search API
17
+ - `atlas_search()` (MCP) - Moved to search API
18
+ - `vector_search()` (MCP) - Moved to search API
19
+ - `vector_search_text()` (MCP) - Moved to search API
20
+
21
+ **Kept Methods:**
22
+ - `add_utility_agent()` - Registry management
23
+ - `update_health_status()` - Registry management
24
+ - `update_agent_base_url()` - Registry management
25
+ - `add_tags()` - Registry management
26
+ - `remove_agent()` - Registry management
27
+ - `update_utility_agent()` - Registry management
28
+ - `get_statistics()` - Registry statistics (read but specific to registry)
29
+ - `add_mcp_server()` - Registry management
30
+ - `get_mcp_server()` - Simple lookup (kept for backward compatibility)
31
+
32
+ **Updated Documentation:**
33
+ - Updated module docstring to clarify write-only purpose
34
+ - Updated class docstrings to indicate write operations only
35
+ - Added references to `iatp_search_api.py` for search operations
36
+
37
+ ### 2. `iatp_search_api.py` - Search and Query Operations
38
+
39
+ **Existing Methods (Unchanged):**
40
+ - `find_utility_agent()` - Search by name, capability, tag, or query
41
+ - `list_utility_agents()` - List agents with filters
42
+ - `search_utility_agents()` - Vector search for agents
43
+ - `find_mcp_server()` - Search MCP servers
44
+ - `list_mcp_servers()` - List MCP servers
45
+ - `search_mcp_servers()` - Vector search for MCP servers
46
+ - `get_mcp_server()` - Get MCP server by name
47
+
48
+ **Features:**
49
+ - Atlas Search integration
50
+ - Vector search with embeddings
51
+ - Fallback to Atlas Search when vector search fails
52
+ - Read-only operations (no MongoDB write access needed)
53
+ - Cached connections for better performance
54
+
55
+ ### 3. Updated Test Files
56
+
57
+ **`test_discovery_and_usage.py`:**
58
+ - Updated imports to use `iatp_search_api` instead of `mongodb_registry`
59
+ - Modified `discover_hyperliquid_agent()` to use `search_utility_agents()`
60
+ - Updated `create_tools_from_discovered_agent()` to work with `UtilityAgentInfo` structure
61
+ - Removed registry connection management
62
+
63
+ **`test_refactoring.py`:**
64
+ - New test script to verify refactoring works correctly
65
+ - Tests registry write operations
66
+ - Tests search API operations
67
+ - Tests discovery test imports
68
+
69
+ ## Benefits
70
+
71
+ ### 1. Clear Separation of Concerns
72
+ - **Registry**: Handles data persistence and lifecycle management
73
+ - **Search API**: Handles discovery and querying
74
+
75
+ ### 2. Reduced Duplication
76
+ - Eliminated duplicate search methods between files
77
+ - Single source of truth for search logic
78
+
79
+ ### 3. Better Performance
80
+ - Search API uses read-only connections
81
+ - Cached connections for repeated queries
82
+ - Optimized for search operations
83
+
84
+ ### 4. Improved Maintainability
85
+ - Easier to modify search logic in one place
86
+ - Clearer responsibilities for each module
87
+ - Better testability
88
+
89
+ ### 5. Enhanced Security
90
+ - Search API can use read-only credentials
91
+ - Registry operations require write permissions
92
+ - Reduced attack surface
93
+
94
+ ## Usage Examples
95
+
96
+ ### Registry Operations (Write)
97
+ ```python
98
+ from traia_iatp.registry.mongodb_registry import UtilityAgentRegistry
99
+
100
+ # Add new agent
101
+ registry = UtilityAgentRegistry()
102
+ await registry.add_utility_agent(agent, tags=["trading", "hyperliquid"])
103
+
104
+ # Update agent
105
+ await registry.update_health_status(agent_id, is_healthy=True)
106
+ await registry.update_agent_base_url(agent_id, new_url)
107
+ ```
108
+
109
+ ### Search Operations (Read)
110
+ ```python
111
+ from traia_iatp.registry.iatp_search_api import search_utility_agents, find_utility_agent
112
+
113
+ # Search for agents
114
+ agents = await search_utility_agents("hyperliquid trading", limit=5)
115
+
116
+ # Find specific agent
117
+ agent = find_utility_agent(capability="market_data")
118
+ ```
119
+
120
+ ### Discovery Test
121
+ ```python
122
+ # The discovery test now uses the search API
123
+ from traia_iatp.registry.iatp_search_api import search_utility_agents
124
+
125
+ # Search for Hyperliquid agent
126
+ search_results = await search_utility_agents(
127
+ query="hyperliquid trading",
128
+ limit=5,
129
+ active_only=True
130
+ )
131
+ ```
132
+
133
+ ## Migration Guide
134
+
135
+ ### For Existing Code
136
+
137
+ 1. **If using registry for search/query:**
138
+ ```python
139
+ # OLD
140
+ from traia_iatp.registry.mongodb_registry import UtilityAgentRegistry
141
+ registry = UtilityAgentRegistry()
142
+ agents = await registry.query_agents(query="trading")
143
+
144
+ # NEW
145
+ from traia_iatp.registry.iatp_search_api import search_utility_agents
146
+ agents = await search_utility_agents("trading")
147
+ ```
148
+
149
+ 2. **If using registry for write operations:**
150
+ ```python
151
+ # No changes needed - these methods are still available
152
+ from traia_iatp.registry.mongodb_registry import UtilityAgentRegistry
153
+ registry = UtilityAgentRegistry()
154
+ await registry.add_utility_agent(agent)
155
+ ```
156
+
157
+ ### Environment Variables
158
+
159
+ No changes to environment variables are required. Both modules use the same authentication methods:
160
+ - `MONGODB_CONNECTION_STRING`
161
+ - `MONGODB_USER` + `MONGODB_PASSWORD`
162
+ - `MONGODB_X509_CERT_FILE`
163
+
164
+ ## Testing
165
+
166
+ Run the refactoring test to verify everything works:
167
+
168
+ ```bash
169
+ cd traia-centralized-backend
170
+ python test_refactoring.py
171
+ ```
172
+
173
+ This will test:
174
+ - Registry write operations
175
+ - Search API operations
176
+ - Discovery test imports
177
+
178
+ ## Future Improvements
179
+
180
+ 1. **Connection Pooling**: Implement connection pooling for better performance
181
+ 2. **Caching**: Add Redis caching for frequently accessed data
182
+ 3. **Rate Limiting**: Add rate limiting for search operations
183
+ 4. **Metrics**: Add metrics collection for search performance
184
+ 5. **API Versioning**: Consider API versioning for future changes
185
+
186
+ ## Backward Compatibility
187
+
188
+ - All existing registry write operations remain unchanged
189
+ - Search operations now use the dedicated search API
190
+ - No breaking changes to existing functionality
191
+ - Clear migration path provided
@@ -0,0 +1,2 @@
1
+ """IATP utility scripts."""
2
+
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env python3
2
+ """Create an IATPWallet for a user.
3
+
4
+ This script:
5
+ 1. Takes an owner private key as input
6
+ 2. Generates a new operator keypair
7
+ 3. Calls IATPWalletFactory.createWallet() to deploy an IATPWallet
8
+ 4. Returns the operator keys and wallet address
9
+
10
+ Usage:
11
+ uv run python -m traia_iatp.scripts.create_wallet \\
12
+ --owner-key 0x... \\
13
+ --network sepolia \\
14
+ [--wait-confirmations 2]
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import logging
20
+ import sys
21
+ from eth_account import Account
22
+ from web3 import Web3
23
+
24
+ from traia_iatp.contracts import (
25
+ get_contract_address,
26
+ load_contract,
27
+ ContractName
28
+ )
29
+
30
+ logging.basicConfig(
31
+ level=logging.INFO,
32
+ format='%(asctime)s - %(levelname)s - %(message)s'
33
+ )
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ def create_wallet(
38
+ owner_private_key: str,
39
+ network: str = "sepolia",
40
+ wait_confirmations: int = 2
41
+ ) -> dict:
42
+ """Create an IATP Wallet for an owner.
43
+
44
+ Args:
45
+ owner_private_key: Owner's private key (0x...)
46
+ network: Network name (default: "sepolia")
47
+ wait_confirmations: Number of confirmations to wait (default: 2)
48
+
49
+ Returns:
50
+ Dict with operator keys and wallet address:
51
+ {
52
+ "owner_address": "0x...",
53
+ "operator_address": "0x...",
54
+ "operator_private_key": "0x...",
55
+ "wallet_address": "0x...",
56
+ "transaction_hash": "0x...",
57
+ "network": "sepolia"
58
+ }
59
+ """
60
+ logger.info(f"Creating IATP Wallet on {network}")
61
+
62
+ # Create owner account
63
+ if not owner_private_key.startswith("0x"):
64
+ owner_private_key = f"0x{owner_private_key}"
65
+
66
+ owner_account = Account.from_key(owner_private_key)
67
+ logger.info(f"Owner address: {owner_account.address}")
68
+
69
+ # Generate new operator keypair
70
+ operator_account = Account.create()
71
+ logger.info(f"Generated operator address: {operator_account.address}")
72
+ logger.info(f"Operator private key: {operator_account.key.hex()}")
73
+
74
+ # Load IATPWalletFactory contract
75
+ factory_contract, w3 = load_contract(
76
+ ContractName.IATP_WALLET_FACTORY,
77
+ network=network
78
+ )
79
+
80
+ factory_address = factory_contract.address
81
+ logger.info(f"IATPWalletFactory address: {factory_address}")
82
+
83
+ # Check owner balance
84
+ owner_balance = w3.eth.get_balance(owner_account.address)
85
+ logger.info(f"Owner balance: {w3.from_wei(owner_balance, 'ether')} ETH")
86
+
87
+ if owner_balance == 0:
88
+ logger.error("Owner has no ETH for gas. Please fund the account.")
89
+ raise ValueError("Insufficient balance for gas")
90
+
91
+ # Build transaction to create wallet
92
+ logger.info("Building createWallet transaction...")
93
+
94
+ tx = factory_contract.functions.createWallet(
95
+ operator_account.address
96
+ ).build_transaction({
97
+ 'from': owner_account.address,
98
+ 'nonce': w3.eth.get_transaction_count(owner_account.address),
99
+ 'gas': 2000000, # Estimate gas
100
+ 'gasPrice': w3.eth.gas_price,
101
+ 'chainId': w3.eth.chain_id
102
+ })
103
+
104
+ # Sign transaction
105
+ signed_tx = owner_account.sign_transaction(tx)
106
+
107
+ # Send transaction
108
+ logger.info("Sending transaction...")
109
+ tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
110
+ logger.info(f"Transaction hash: {tx_hash.hex()}")
111
+
112
+ # Wait for confirmation
113
+ logger.info(f"Waiting for {wait_confirmations} confirmation(s)...")
114
+ tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
115
+
116
+ if tx_receipt['status'] != 1:
117
+ logger.error("Transaction failed!")
118
+ raise Exception(f"Transaction failed: {tx_hash.hex()}")
119
+
120
+ logger.info(f"Transaction confirmed in block {tx_receipt['blockNumber']}")
121
+
122
+ # Parse WalletCreated event to get wallet address
123
+ wallet_address = None
124
+ for log in tx_receipt['logs']:
125
+ try:
126
+ event = factory_contract.events.WalletCreated().process_log(log)
127
+ wallet_address = event['args']['wallet']
128
+ logger.info(f"✅ IATPWallet created: {wallet_address}")
129
+ break
130
+ except:
131
+ continue
132
+
133
+ if not wallet_address:
134
+ logger.error("Could not find WalletCreated event in logs")
135
+ raise Exception("Wallet address not found in transaction logs")
136
+
137
+ # Return result
138
+ result = {
139
+ "owner_address": owner_account.address,
140
+ "operator_address": operator_account.address,
141
+ "operator_private_key": operator_account.key.hex(),
142
+ "wallet_address": wallet_address,
143
+ "transaction_hash": tx_hash.hex(),
144
+ "block_number": tx_receipt['blockNumber'],
145
+ "network": network,
146
+ "chain_id": w3.eth.chain_id
147
+ }
148
+
149
+ return result
150
+
151
+
152
+ def main():
153
+ """Main CLI entry point."""
154
+ parser = argparse.ArgumentParser(
155
+ description="Create an IATP Wallet via IATPWalletFactory",
156
+ formatter_class=argparse.RawDescriptionHelpFormatter,
157
+ epilog="""
158
+ Examples:
159
+ # Create wallet on Sepolia
160
+ uv run python -m traia_iatp.scripts.create_wallet \\
161
+ --owner-key 0x0cdda2d9d744d6d2a3ba4b30ac51227590f0e8f44cf5251bb1ed0941d677c0a4 \\
162
+ --network sepolia
163
+
164
+ # Create wallet with output to JSON file
165
+ uv run python -m traia_iatp.scripts.create_wallet \\
166
+ --owner-key 0x... \\
167
+ --network sepolia \\
168
+ --output wallet_config.json
169
+ """
170
+ )
171
+
172
+ parser.add_argument(
173
+ "--owner-key",
174
+ required=True,
175
+ help="Owner's private key (0x...)"
176
+ )
177
+
178
+ parser.add_argument(
179
+ "--network",
180
+ default="sepolia",
181
+ choices=["sepolia", "base-sepolia", "arbitrum-sepolia", "localhost"],
182
+ help="Network to deploy on (default: sepolia)"
183
+ )
184
+
185
+ parser.add_argument(
186
+ "--wait-confirmations",
187
+ type=int,
188
+ default=2,
189
+ help="Number of confirmations to wait (default: 2)"
190
+ )
191
+
192
+ parser.add_argument(
193
+ "--output",
194
+ "-o",
195
+ type=str,
196
+ help="Output JSON file path (prints to stdout if not specified)"
197
+ )
198
+
199
+ parser.add_argument(
200
+ "--verbose",
201
+ "-v",
202
+ action="store_true",
203
+ help="Enable verbose logging"
204
+ )
205
+
206
+ args = parser.parse_args()
207
+
208
+ if args.verbose:
209
+ logging.getLogger().setLevel(logging.DEBUG)
210
+
211
+ try:
212
+ # Create wallet
213
+ result = create_wallet(
214
+ owner_private_key=args.owner_key,
215
+ network=args.network,
216
+ wait_confirmations=args.wait_confirmations
217
+ )
218
+
219
+ # Output result
220
+ output_json = json.dumps(result, indent=2)
221
+
222
+ if args.output:
223
+ with open(args.output, 'w') as f:
224
+ f.write(output_json)
225
+ logger.info(f"✅ Configuration saved to: {args.output}")
226
+ else:
227
+ print("\n" + "=" * 80)
228
+ print("IATP Wallet Created Successfully!")
229
+ print("=" * 80)
230
+ print(output_json)
231
+ print("=" * 80)
232
+
233
+ return 0
234
+
235
+ except Exception as e:
236
+ logger.error(f"Failed to create wallet: {e}")
237
+ import traceback
238
+ traceback.print_exc()
239
+ return 1
240
+
241
+
242
+ if __name__ == "__main__":
243
+ sys.exit(main())
244
+
@@ -0,0 +1,15 @@
1
+ """IATP server module for creating A2A servers from utility agents."""
2
+
3
+ from .a2a_server import (
4
+ UtilityAgencyExecutor,
5
+ create_a2a_server,
6
+ create_and_start_a2a_server
7
+ )
8
+ from .iatp_server_agent_generator import IATPServerAgentGenerator
9
+
10
+ __all__ = [
11
+ "UtilityAgencyExecutor",
12
+ "IATPServerAgentGenerator",
13
+ "create_a2a_server",
14
+ "create_and_start_a2a_server",
15
+ ]
@@ -0,0 +1,219 @@
1
+ """A2A server implementation for utility agencies using the official a2a-sdk."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Dict, Any, Optional, List
6
+ from a2a.server.apps import A2AStarletteApplication
7
+ from a2a.server.agent_execution import AgentExecutor, RequestContext
8
+ from a2a.server.events.event_queue import EventQueue
9
+ from a2a.server.tasks import InMemoryTaskStore
10
+ from a2a.server.request_handlers import DefaultRequestHandler
11
+ from a2a.types import AgentCard, AgentSkill, AgentCapabilities
12
+ from a2a.utils import new_agent_text_message, new_text_artifact
13
+ from dataclasses import dataclass
14
+ import uvicorn
15
+
16
+ from ..core.models import UtilityAgent
17
+ from .iatp_server_agent_generator import IATPServerAgentGenerator
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class UtilityAgencyExecutor(AgentExecutor):
23
+ """Agent executor that wraps a utility agency and executes tasks via CrewAI."""
24
+
25
+ def __init__(self, agency: UtilityAgent, agency_generator: IATPServerAgentGenerator):
26
+ self.agency = agency
27
+ self.agency_generator = agency_generator
28
+ self._crew = None
29
+
30
+ async def _get_crew(self):
31
+ """Lazily build the crew from agency config."""
32
+ if self._crew is None:
33
+ self._crew = await self.agency_generator.build_crew_from_agency(self.agency)
34
+ return self._crew
35
+
36
+ async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
37
+ """Execute a task using the utility agency crew."""
38
+ try:
39
+ # Get the user's request from context
40
+ user_message = context.get_last_user_message()
41
+ if not user_message:
42
+ await event_queue.enqueue_event(
43
+ new_agent_text_message("No user message provided")
44
+ )
45
+ return
46
+
47
+ # Extract text from message parts
48
+ user_text = ""
49
+ for part in user_message.parts:
50
+ if hasattr(part, 'text'):
51
+ user_text += part.text
52
+
53
+ if not user_text:
54
+ event_queue.enqueue_event(
55
+ new_agent_text_message("No text content in user message")
56
+ )
57
+ return
58
+
59
+ # Build crew and execute
60
+ crew = await self._get_crew()
61
+
62
+ # Run the crew with the request
63
+ result = crew.kickoff(inputs={"request": user_text})
64
+
65
+ # Send the result as agent message
66
+ event_queue.enqueue_event(
67
+ new_agent_text_message(str(result))
68
+ )
69
+
70
+ except Exception as e:
71
+ logger.error(f"Error executing task: {e}")
72
+ event_queue.enqueue_event(
73
+ new_agent_text_message(f"Error processing request: {str(e)}")
74
+ )
75
+
76
+ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
77
+ """Cancel all tasks."""
78
+ pass
79
+
80
+
81
+ class MCPToolExecutor(AgentExecutor):
82
+ """Agent executor for individual MCP tool execution."""
83
+
84
+ def __init__(self, agency: UtilityAgent, tool_name: str):
85
+ self.agency = agency
86
+ self.tool_name = tool_name
87
+
88
+ async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
89
+ """Execute a specific MCP tool."""
90
+ try:
91
+ from ..mcp.client import MCPClient
92
+
93
+ # Get tool arguments from context
94
+ user_message = context.get_last_user_message()
95
+ if not user_message:
96
+ await event_queue.enqueue_event(
97
+ new_agent_text_message("No arguments provided")
98
+ )
99
+ return
100
+
101
+ # Extract arguments (assuming they're passed as JSON in text)
102
+ import json
103
+ arguments = {}
104
+ for part in user_message.parts:
105
+ if hasattr(part, 'text'):
106
+ try:
107
+ arguments = json.loads(part.text)
108
+ except:
109
+ arguments = {"query": part.text} # Fallback to simple text
110
+
111
+ # Connect to MCP server and execute tool
112
+ mcp_client = MCPClient(self.agency.mcp_server)
113
+ await mcp_client.connect()
114
+
115
+ try:
116
+ result = await mcp_client.call_tool(self.tool_name, arguments)
117
+ await event_queue.enqueue_event(
118
+ new_agent_text_message(str(result))
119
+ )
120
+ finally:
121
+ await mcp_client.disconnect()
122
+
123
+ except Exception as e:
124
+ logger.error(f"Error executing MCP tool {self.tool_name}: {e}")
125
+ await event_queue.enqueue_event(
126
+ new_agent_text_message(f"Error executing tool: {str(e)}")
127
+ )
128
+
129
+
130
+ def create_a2a_server(
131
+ agency: UtilityAgent,
132
+ agency_generator: IATPServerAgentGenerator,
133
+ host: str = "0.0.0.0",
134
+ port: int = 8000
135
+ ) -> A2AStarletteApplication:
136
+ """Create an A2A server for a utility agency."""
137
+
138
+ # Create skills from agency capabilities
139
+ skills = []
140
+
141
+ # Add main processing skill
142
+ main_skill = AgentSkill(
143
+ id="process_request",
144
+ name=f"Process request using {agency.name}",
145
+ description=f"Process a request using {agency.name} capabilities. {agency.description}",
146
+ examples=[
147
+ f"Help me with {cap}" for cap in agency.mcp_server.capabilities[:2]
148
+ ] if agency.mcp_server.capabilities else ["Process this request for me"]
149
+ )
150
+ skills.append(main_skill)
151
+
152
+ # Add individual MCP tool skills
153
+ for capability in agency.mcp_server.capabilities:
154
+ skill = AgentSkill(
155
+ id=f"mcp_{capability}",
156
+ name=f"Execute {capability}",
157
+ description=f"Execute {capability} tool on MCP server",
158
+ examples=[f"Run {capability} with these parameters"]
159
+ )
160
+ skills.append(skill)
161
+
162
+ # Create capabilities
163
+ capabilities = AgentCapabilities(
164
+ streaming=False, # Not implementing streaming for now
165
+ pushNotifications=False, # Not implementing push notifications
166
+ stateTransitionHistory=False
167
+ )
168
+
169
+ # Create agent card
170
+ agent_card = AgentCard(
171
+ name=agency.name.replace(" ", "_").lower(),
172
+ description=agency.description,
173
+ url=f"http://{host}:{port}",
174
+ version="1.0.0",
175
+ capabilities=capabilities,
176
+ skills=skills,
177
+ # TODO: Add authentication when AgentAuthentication is available
178
+ # authentication=AgentAuthentication(schemes=["Bearer"]) if agency.auth_type else None
179
+ )
180
+
181
+ # Create executor mapping
182
+ executors = {
183
+ "process_request": UtilityAgencyExecutor(agency, agency_generator)
184
+ }
185
+
186
+ # Add MCP tool executors
187
+ for capability in agency.mcp_server.capabilities:
188
+ executors[f"mcp_{capability}"] = MCPToolExecutor(agency, capability)
189
+
190
+ # Create task store and request handler
191
+ task_store = InMemoryTaskStore()
192
+ request_handler = DefaultRequestHandler(
193
+ agent_card=agent_card,
194
+ executors=executors,
195
+ task_store=task_store
196
+ )
197
+
198
+ # Create the A2A application
199
+ app = A2AStarletteApplication(
200
+ agent_card=agent_card,
201
+ http_handler=request_handler
202
+ )
203
+
204
+ return app
205
+
206
+
207
+ async def create_and_start_a2a_server(
208
+ agency: UtilityAgent,
209
+ agency_generator: IATPServerAgentGenerator,
210
+ host: str = "0.0.0.0",
211
+ port: int = 8000
212
+ ):
213
+ """Create and start an A2A server for a utility agency."""
214
+ app = create_a2a_server(agency, agency_generator, host, port)
215
+
216
+ logger.info(f"Starting A2A server for {agency.name} on {host}:{port}")
217
+ config = uvicorn.Config(app, host=host, port=port, log_level="info")
218
+ server = uvicorn.Server(config)
219
+ await server.serve()