agent-mcp 0.1.4__py3-none-any.whl → 0.1.6__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.
- agent_mcp/__init__.py +66 -12
- agent_mcp/a2a_protocol.py +316 -0
- agent_mcp/agent_lightning_library.py +214 -0
- agent_mcp/claude_mcp_adapter.py +195 -0
- agent_mcp/google_ai_mcp_adapter.py +183 -0
- agent_mcp/llamaindex_mcp_adapter.py +410 -0
- agent_mcp/microsoft_agent_framework.py +591 -0
- agent_mcp/missing_frameworks.py +435 -0
- agent_mcp/openapi_protocol.py +616 -0
- agent_mcp/payments.py +804 -0
- agent_mcp/pydantic_ai_mcp_adapter.py +628 -0
- agent_mcp/registry.py +768 -0
- agent_mcp/security.py +864 -0
- {agent_mcp-0.1.4.dist-info → agent_mcp-0.1.6.dist-info}/METADATA +182 -55
- {agent_mcp-0.1.4.dist-info → agent_mcp-0.1.6.dist-info}/RECORD +19 -6
- {agent_mcp-0.1.4.dist-info → agent_mcp-0.1.6.dist-info}/WHEEL +1 -1
- agent_mcp-0.1.6.dist-info/entry_points.txt +4 -0
- demos/comprehensive_framework_demo.py +202 -0
- agent_mcp-0.1.4.dist-info/entry_points.txt +0 -2
- {agent_mcp-0.1.4.dist-info → agent_mcp-0.1.6.dist-info}/top_level.txt +0 -0
agent_mcp/registry.py
ADDED
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-Language Agent Registry
|
|
3
|
+
Enhanced agent discovery and registration system supporting multiple protocols
|
|
4
|
+
|
|
5
|
+
This module provides a comprehensive registry for:
|
|
6
|
+
- Multi-language agent support (Python, JavaScript, Go, Rust, etc.)
|
|
7
|
+
- Multiple protocols (MCP, A2A, OpenAPI, REST)
|
|
8
|
+
- Health monitoring and lifecycle management
|
|
9
|
+
- Capability discovery and matching
|
|
10
|
+
- Protocol auto-detection
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import uuid
|
|
16
|
+
import hashlib
|
|
17
|
+
import aiohttp
|
|
18
|
+
from datetime import datetime, timezone, timedelta
|
|
19
|
+
from typing import Dict, Any, List, Optional, Callable, Union, Set
|
|
20
|
+
from dataclasses import dataclass, asdict
|
|
21
|
+
from enum import Enum
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
class AgentProtocol(Enum):
|
|
27
|
+
"""Supported agent protocols"""
|
|
28
|
+
MCP = "mcp"
|
|
29
|
+
A2A = "a2a"
|
|
30
|
+
OPENAPI = "openapi"
|
|
31
|
+
REST = "rest"
|
|
32
|
+
WEBHOOK = "webhook"
|
|
33
|
+
WEBSOCKET = "websocket"
|
|
34
|
+
GRPC = "grpc"
|
|
35
|
+
|
|
36
|
+
class AgentStatus(Enum):
|
|
37
|
+
"""Agent registration status"""
|
|
38
|
+
ACTIVE = "active"
|
|
39
|
+
INACTIVE = "inactive"
|
|
40
|
+
PENDING = "pending"
|
|
41
|
+
SUSPENDED = "suspended"
|
|
42
|
+
DECOMMISSIONED = "decommissioned"
|
|
43
|
+
|
|
44
|
+
class AgentLanguage(Enum):
|
|
45
|
+
"""Programming languages/frameworks"""
|
|
46
|
+
PYTHON = "python"
|
|
47
|
+
JAVASCRIPT = "javascript"
|
|
48
|
+
TYPESCRIPT = "typescript"
|
|
49
|
+
GO = "go"
|
|
50
|
+
RUST = "rust"
|
|
51
|
+
JAVA = "java"
|
|
52
|
+
CSHARP = "csharp"
|
|
53
|
+
RUBY = "ruby"
|
|
54
|
+
PHP = "php"
|
|
55
|
+
SWIFT = "swift"
|
|
56
|
+
KOTLIN = "kotlin"
|
|
57
|
+
UNKNOWN = "unknown"
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class AgentRegistration:
|
|
61
|
+
"""Comprehensive agent registration information"""
|
|
62
|
+
agent_id: str
|
|
63
|
+
name: str
|
|
64
|
+
description: str
|
|
65
|
+
language: AgentLanguage
|
|
66
|
+
frameworks: List[str]
|
|
67
|
+
protocols: List[AgentProtocol]
|
|
68
|
+
endpoint: str
|
|
69
|
+
webhook_url: Optional[str] = None
|
|
70
|
+
capabilities: List[str] = None
|
|
71
|
+
security_level: str = "medium"
|
|
72
|
+
owner: Optional[str] = None
|
|
73
|
+
version: str = "1.0.0"
|
|
74
|
+
metadata: Dict[str, Any] = None
|
|
75
|
+
|
|
76
|
+
# Runtime information
|
|
77
|
+
status: AgentStatus = AgentStatus.PENDING
|
|
78
|
+
registered_at: str = None
|
|
79
|
+
last_heartbeat: Optional[str] = None
|
|
80
|
+
health_status: Optional[str] = None
|
|
81
|
+
health_check_url: Optional[str] = None
|
|
82
|
+
|
|
83
|
+
# Network information
|
|
84
|
+
ip_address: Optional[str] = None
|
|
85
|
+
region: Optional[str] = None
|
|
86
|
+
latency_ms: Optional[float] = None
|
|
87
|
+
|
|
88
|
+
# Security information
|
|
89
|
+
auth_token: Optional[str] = None
|
|
90
|
+
public_key: Optional[str] = None
|
|
91
|
+
did: Optional[str] = None # Decentralized Identifier
|
|
92
|
+
|
|
93
|
+
def __post_init__(self):
|
|
94
|
+
if self.capabilities is None:
|
|
95
|
+
self.capabilities = []
|
|
96
|
+
if self.frameworks is None:
|
|
97
|
+
self.frameworks = []
|
|
98
|
+
if self.metadata is None:
|
|
99
|
+
self.metadata = {}
|
|
100
|
+
if self.registered_at is None:
|
|
101
|
+
self.registered_at = datetime.now(timezone.utc).isoformat()
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class HealthCheckResult:
|
|
105
|
+
"""Health check result for an agent"""
|
|
106
|
+
agent_id: str
|
|
107
|
+
status: str
|
|
108
|
+
response_time_ms: float
|
|
109
|
+
timestamp: str
|
|
110
|
+
error: Optional[str] = None
|
|
111
|
+
details: Dict[str, Any] = None
|
|
112
|
+
|
|
113
|
+
def __post_init__(self):
|
|
114
|
+
if self.details is None:
|
|
115
|
+
self.details = {}
|
|
116
|
+
if self.timestamp is None:
|
|
117
|
+
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
118
|
+
|
|
119
|
+
class ProtocolDetector:
|
|
120
|
+
"""Auto-detection of agent protocols"""
|
|
121
|
+
|
|
122
|
+
def __init__(self):
|
|
123
|
+
self.detected_protocols = {}
|
|
124
|
+
|
|
125
|
+
async def detect_protocols(self, endpoint: str) -> List[AgentProtocol]:
|
|
126
|
+
"""Detect which protocols an agent supports"""
|
|
127
|
+
detected = []
|
|
128
|
+
|
|
129
|
+
async with aiohttp.ClientSession() as session:
|
|
130
|
+
# Try MCP detection
|
|
131
|
+
if await self._detect_mcp(session, endpoint):
|
|
132
|
+
detected.append(AgentProtocol.MCP)
|
|
133
|
+
|
|
134
|
+
# Try A2A detection
|
|
135
|
+
if await self._detect_a2a(session, endpoint):
|
|
136
|
+
detected.append(AgentProtocol.A2A)
|
|
137
|
+
|
|
138
|
+
# Try OpenAPI detection
|
|
139
|
+
if await self._detect_openapi(session, endpoint):
|
|
140
|
+
detected.append(AgentProtocol.OPENAPI)
|
|
141
|
+
|
|
142
|
+
# Default to REST if endpoint responds
|
|
143
|
+
if not detected and await self._detect_rest(session, endpoint):
|
|
144
|
+
detected.append(AgentProtocol.REST)
|
|
145
|
+
|
|
146
|
+
return detected
|
|
147
|
+
|
|
148
|
+
async def _detect_mcp(self, session: aiohttp.ClientSession, endpoint: str) -> bool:
|
|
149
|
+
"""Detect if endpoint supports MCP"""
|
|
150
|
+
try:
|
|
151
|
+
# Check for MCP endpoint
|
|
152
|
+
async with session.get(f"{endpoint}/.well-known/mcp", timeout=5) as response:
|
|
153
|
+
return response.status == 200
|
|
154
|
+
except:
|
|
155
|
+
try:
|
|
156
|
+
async with session.get(f"{endpoint}/mcp/info", timeout=5) as response:
|
|
157
|
+
return response.status == 200
|
|
158
|
+
except:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
async def _detect_a2a(self, session: aiohttp.ClientSession, endpoint: str) -> bool:
|
|
162
|
+
"""Detect if endpoint supports A2A"""
|
|
163
|
+
try:
|
|
164
|
+
async with session.get(f"{endpoint}/.well-known/agent.json", timeout=5) as response:
|
|
165
|
+
return response.status == 200
|
|
166
|
+
except:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
async def _detect_openapi(self, session: aiohttp.ClientSession, endpoint: str) -> bool:
|
|
170
|
+
"""Detect if endpoint supports OpenAPI"""
|
|
171
|
+
try:
|
|
172
|
+
async with session.get(f"{endpoint}/openapi.json", timeout=5) as response:
|
|
173
|
+
return response.status == 200
|
|
174
|
+
except:
|
|
175
|
+
try:
|
|
176
|
+
async with session.get(f"{endpoint}/api/docs", timeout=5) as response:
|
|
177
|
+
return response.status == 200
|
|
178
|
+
except:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
async def _detect_rest(self, session: aiohttp.ClientSession, endpoint: str) -> bool:
|
|
182
|
+
"""Detect if endpoint supports basic REST"""
|
|
183
|
+
try:
|
|
184
|
+
# Simple ping to base endpoint
|
|
185
|
+
async with session.get(endpoint, timeout=5) as response:
|
|
186
|
+
return response.status < 500
|
|
187
|
+
except:
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
class HealthMonitor:
|
|
191
|
+
"""Health monitoring for registered agents"""
|
|
192
|
+
|
|
193
|
+
def __init__(self, check_interval: int = 60):
|
|
194
|
+
self.check_interval = check_interval
|
|
195
|
+
self.health_history = {}
|
|
196
|
+
self.monitoring_active = False
|
|
197
|
+
self.check_task = None
|
|
198
|
+
|
|
199
|
+
async def start_monitoring(self, agents: Dict[str, AgentRegistration]):
|
|
200
|
+
"""Start health monitoring for agents"""
|
|
201
|
+
self.agents = agents
|
|
202
|
+
self.monitoring_active = True
|
|
203
|
+
|
|
204
|
+
if self.check_task:
|
|
205
|
+
self.check_task.cancel()
|
|
206
|
+
|
|
207
|
+
self.check_task = asyncio.create_task(self._monitoring_loop())
|
|
208
|
+
logger.info("Health monitoring started")
|
|
209
|
+
|
|
210
|
+
async def stop_monitoring(self):
|
|
211
|
+
"""Stop health monitoring"""
|
|
212
|
+
self.monitoring_active = False
|
|
213
|
+
if self.check_task:
|
|
214
|
+
self.check_task.cancel()
|
|
215
|
+
self.check_task = None
|
|
216
|
+
logger.info("Health monitoring stopped")
|
|
217
|
+
|
|
218
|
+
async def _monitoring_loop(self):
|
|
219
|
+
"""Main monitoring loop"""
|
|
220
|
+
while self.monitoring_active:
|
|
221
|
+
try:
|
|
222
|
+
await self._check_all_agents()
|
|
223
|
+
await asyncio.sleep(self.check_interval)
|
|
224
|
+
except asyncio.CancelledError:
|
|
225
|
+
break
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Error in health monitoring loop: {e}")
|
|
228
|
+
await asyncio.sleep(10) # Brief pause before retry
|
|
229
|
+
|
|
230
|
+
async def _check_all_agents(self):
|
|
231
|
+
"""Check health of all registered agents"""
|
|
232
|
+
tasks = []
|
|
233
|
+
|
|
234
|
+
for agent_id, registration in self.agents.items():
|
|
235
|
+
if registration.status == AgentStatus.ACTIVE and registration.health_check_url:
|
|
236
|
+
tasks.append(self._check_agent_health(agent_id, registration))
|
|
237
|
+
|
|
238
|
+
if tasks:
|
|
239
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
240
|
+
|
|
241
|
+
async def _check_agent_health(self, agent_id: str, registration: AgentRegistration) -> HealthCheckResult:
|
|
242
|
+
"""Check health of a single agent"""
|
|
243
|
+
start_time = datetime.now(timezone.utc)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
async with aiohttp.ClientSession() as session:
|
|
247
|
+
timeout = aiohttp.ClientTimeout(total=10)
|
|
248
|
+
|
|
249
|
+
# Use dedicated health check URL or default to agent endpoint
|
|
250
|
+
check_url = registration.health_check_url or f"{registration.endpoint}/health"
|
|
251
|
+
|
|
252
|
+
async with session.get(check_url, timeout=timeout) as response:
|
|
253
|
+
end_time = datetime.now(timezone.utc)
|
|
254
|
+
response_time = (end_time - start_time).total_seconds() * 1000
|
|
255
|
+
|
|
256
|
+
result = HealthCheckResult(
|
|
257
|
+
agent_id=agent_id,
|
|
258
|
+
status="healthy" if response.status == 200 else "unhealthy",
|
|
259
|
+
response_time_ms=response_time,
|
|
260
|
+
timestamp=end_time.isoformat(),
|
|
261
|
+
details={
|
|
262
|
+
"http_status": response.status,
|
|
263
|
+
"endpoint": check_url
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if response.status == 200:
|
|
268
|
+
registration.last_heartbeat = result.timestamp
|
|
269
|
+
registration.health_status = "healthy"
|
|
270
|
+
else:
|
|
271
|
+
registration.health_status = "unhealthy"
|
|
272
|
+
|
|
273
|
+
# Store in history
|
|
274
|
+
if agent_id not in self.health_history:
|
|
275
|
+
self.health_history[agent_id] = []
|
|
276
|
+
|
|
277
|
+
self.health_history[agent_id].append(result)
|
|
278
|
+
|
|
279
|
+
# Keep only last 100 results per agent
|
|
280
|
+
if len(self.health_history[agent_id]) > 100:
|
|
281
|
+
self.health_history[agent_id] = self.health_history[agent_id][-100:]
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
end_time = datetime.now(timezone.utc)
|
|
287
|
+
response_time = (end_time - start_time).total_seconds() * 1000
|
|
288
|
+
|
|
289
|
+
result = HealthCheckResult(
|
|
290
|
+
agent_id=agent_id,
|
|
291
|
+
status="error",
|
|
292
|
+
response_time_ms=response_time,
|
|
293
|
+
timestamp=end_time.isoformat(),
|
|
294
|
+
error=str(e),
|
|
295
|
+
details={"error_type": type(e).__name__}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
registration.health_status = "error"
|
|
299
|
+
|
|
300
|
+
if agent_id not in self.health_history:
|
|
301
|
+
self.health_history[agent_id] = []
|
|
302
|
+
|
|
303
|
+
self.health_history[agent_id].append(result)
|
|
304
|
+
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
class MultiLanguageAgentRegistry:
|
|
308
|
+
"""Enhanced registry for multi-language agents"""
|
|
309
|
+
|
|
310
|
+
def __init__(
|
|
311
|
+
self,
|
|
312
|
+
storage_backend = None,
|
|
313
|
+
enable_health_monitoring: bool = True,
|
|
314
|
+
health_check_interval: int = 60,
|
|
315
|
+
require_approval: bool = False
|
|
316
|
+
):
|
|
317
|
+
self.storage = storage_backend # Could be Firestore, PostgreSQL, etc.
|
|
318
|
+
self.agents = {}
|
|
319
|
+
self.capability_index = {} # capability -> [agent_ids]
|
|
320
|
+
self.protocol_index = {} # protocol -> [agent_ids]
|
|
321
|
+
self.language_index = {} # language -> [agent_ids]
|
|
322
|
+
|
|
323
|
+
# Components
|
|
324
|
+
self.protocol_detector = ProtocolDetector()
|
|
325
|
+
self.health_monitor = HealthMonitor(health_check_interval)
|
|
326
|
+
self.require_approval = require_approval
|
|
327
|
+
|
|
328
|
+
# Start health monitoring if enabled
|
|
329
|
+
if enable_health_monitoring:
|
|
330
|
+
asyncio.create_task(self._start_health_monitoring())
|
|
331
|
+
|
|
332
|
+
async def _start_health_monitoring(self):
|
|
333
|
+
"""Start health monitoring after a short delay"""
|
|
334
|
+
await asyncio.sleep(5) # Wait for initial agents to be registered
|
|
335
|
+
await self.health_monitor.start_monitoring(self.agents)
|
|
336
|
+
|
|
337
|
+
async def register_agent(
|
|
338
|
+
self,
|
|
339
|
+
registration: AgentRegistration,
|
|
340
|
+
auto_detect_protocols: bool = True
|
|
341
|
+
) -> Dict[str, Any]:
|
|
342
|
+
"""Register a new agent with enhanced capabilities"""
|
|
343
|
+
try:
|
|
344
|
+
# Validate required fields
|
|
345
|
+
if not self._validate_registration(registration):
|
|
346
|
+
return {
|
|
347
|
+
"status": "error",
|
|
348
|
+
"message": "Invalid registration data",
|
|
349
|
+
"errors": self._get_validation_errors(registration)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Auto-detect protocols if requested
|
|
353
|
+
if auto_detect_protocols and not registration.protocols:
|
|
354
|
+
detected_protocols = await self.protocol_detector.detect_protocols(registration.endpoint)
|
|
355
|
+
registration.protocols = detected_protocols
|
|
356
|
+
logger.info(f"Auto-detected protocols for {registration.agent_id}: {[p.value for p in detected_protocols]}")
|
|
357
|
+
|
|
358
|
+
# Generate authentication token
|
|
359
|
+
registration.auth_token = self._generate_agent_token(registration)
|
|
360
|
+
|
|
361
|
+
# Set initial status
|
|
362
|
+
registration.status = AgentStatus.PENDING if self.require_approval else AgentStatus.ACTIVE
|
|
363
|
+
registration.registered_at = datetime.now(timezone.utc).isoformat()
|
|
364
|
+
|
|
365
|
+
# Store in registry
|
|
366
|
+
self.agents[registration.agent_id] = registration
|
|
367
|
+
|
|
368
|
+
# Update indexes
|
|
369
|
+
self._update_indexes(registration)
|
|
370
|
+
|
|
371
|
+
# Store in persistent storage
|
|
372
|
+
await self.storage.write("agent_registrations", asdict(registration))
|
|
373
|
+
|
|
374
|
+
# Log registration
|
|
375
|
+
logger.info(f"Registered agent: {registration.agent_id} ({registration.language.value})")
|
|
376
|
+
|
|
377
|
+
# Start health check for new agent
|
|
378
|
+
if registration.health_check_url:
|
|
379
|
+
asyncio.create_task(self._immediate_health_check(registration))
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
"status": "success",
|
|
383
|
+
"agent_id": registration.agent_id,
|
|
384
|
+
"auth_token": registration.auth_token,
|
|
385
|
+
"detected_protocols": [p.value for p in registration.protocols],
|
|
386
|
+
"message": "Agent registered successfully"
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.error(f"Error registering agent {registration.agent_id}: {e}")
|
|
391
|
+
return {
|
|
392
|
+
"status": "error",
|
|
393
|
+
"message": str(e)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async def register_multi_protocol_agent(
|
|
397
|
+
self,
|
|
398
|
+
agent_id: str,
|
|
399
|
+
name: str,
|
|
400
|
+
description: str,
|
|
401
|
+
language: Union[str, AgentLanguage],
|
|
402
|
+
endpoint: str,
|
|
403
|
+
capabilities: List[str] = None,
|
|
404
|
+
frameworks: List[str] = None,
|
|
405
|
+
**kwargs
|
|
406
|
+
) -> Dict[str, Any]:
|
|
407
|
+
"""Register an agent with multiple protocol support"""
|
|
408
|
+
|
|
409
|
+
# Normalize language
|
|
410
|
+
if isinstance(language, str):
|
|
411
|
+
try:
|
|
412
|
+
language = AgentLanguage(language.lower())
|
|
413
|
+
except ValueError:
|
|
414
|
+
language = AgentLanguage.UNKNOWN
|
|
415
|
+
|
|
416
|
+
# Create registration
|
|
417
|
+
registration = AgentRegistration(
|
|
418
|
+
agent_id=agent_id,
|
|
419
|
+
name=name,
|
|
420
|
+
description=description,
|
|
421
|
+
language=language,
|
|
422
|
+
frameworks=frameworks or [],
|
|
423
|
+
protocols=[], # Will be auto-detected
|
|
424
|
+
endpoint=endpoint,
|
|
425
|
+
capabilities=capabilities or [],
|
|
426
|
+
**kwargs
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return await self.register_agent(registration, auto_detect_protocols=True)
|
|
430
|
+
|
|
431
|
+
async def discover_agents(
|
|
432
|
+
self,
|
|
433
|
+
capability: str = None,
|
|
434
|
+
protocol: AgentProtocol = None,
|
|
435
|
+
language: AgentLanguage = None,
|
|
436
|
+
active_only: bool = True
|
|
437
|
+
) -> List[Dict[str, Any]]:
|
|
438
|
+
"""Discover agents based on various criteria"""
|
|
439
|
+
candidates = []
|
|
440
|
+
|
|
441
|
+
for agent_id, registration in self.agents.items():
|
|
442
|
+
# Skip inactive agents unless requested
|
|
443
|
+
if active_only and registration.status != AgentStatus.ACTIVE:
|
|
444
|
+
continue
|
|
445
|
+
|
|
446
|
+
# Filter by capability
|
|
447
|
+
if capability and capability not in registration.capabilities:
|
|
448
|
+
continue
|
|
449
|
+
|
|
450
|
+
# Filter by protocol
|
|
451
|
+
if protocol and protocol not in registration.protocols:
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
# Filter by language
|
|
455
|
+
if language and registration.language != language:
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
candidates.append({
|
|
459
|
+
"agent_id": agent_id,
|
|
460
|
+
"name": registration.name,
|
|
461
|
+
"description": registration.description,
|
|
462
|
+
"language": registration.language.value,
|
|
463
|
+
"protocols": [p.value for p in registration.protocols],
|
|
464
|
+
"capabilities": registration.capabilities,
|
|
465
|
+
"status": registration.status.value,
|
|
466
|
+
"endpoint": registration.endpoint,
|
|
467
|
+
"health_status": registration.health_status
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
return candidates
|
|
471
|
+
|
|
472
|
+
async def update_agent_status(
|
|
473
|
+
self,
|
|
474
|
+
agent_id: str,
|
|
475
|
+
status: AgentStatus,
|
|
476
|
+
metadata: Dict[str, Any] = None
|
|
477
|
+
) -> Dict[str, Any]:
|
|
478
|
+
"""Update agent registration status"""
|
|
479
|
+
if agent_id not in self.agents:
|
|
480
|
+
return {
|
|
481
|
+
"status": "error",
|
|
482
|
+
"message": f"Agent {agent_id} not found"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
registration = self.agents[agent_id]
|
|
486
|
+
old_status = registration.status
|
|
487
|
+
registration.status = status
|
|
488
|
+
|
|
489
|
+
if metadata:
|
|
490
|
+
registration.metadata.update(metadata)
|
|
491
|
+
|
|
492
|
+
# Update storage
|
|
493
|
+
await self.storage.update(
|
|
494
|
+
"agent_registrations",
|
|
495
|
+
{"agent_id": agent_id},
|
|
496
|
+
{"status": status.value, "metadata": registration.metadata}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
logger.info(f"Updated agent {agent_id} status: {old_status.value} -> {status.value}")
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
"status": "success",
|
|
503
|
+
"agent_id": agent_id,
|
|
504
|
+
"old_status": old_status.value,
|
|
505
|
+
"new_status": status.value
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async def get_agent_info(self, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
509
|
+
"""Get detailed information about an agent"""
|
|
510
|
+
registration = self.agents.get(agent_id)
|
|
511
|
+
if not registration:
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
# Get health history
|
|
515
|
+
health_history = self.health_monitor.health_history.get(agent_id, [])
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
"registration": asdict(registration),
|
|
519
|
+
"health_history": health_history[-10:], # Last 10 health checks
|
|
520
|
+
"uptime_percentage": self._calculate_uptime(health_history),
|
|
521
|
+
"average_response_time": self._calculate_avg_response_time(health_history)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async def create_agent_webhook(
|
|
525
|
+
self,
|
|
526
|
+
agent_id: str,
|
|
527
|
+
webhook_url: str,
|
|
528
|
+
events: List[str] = None
|
|
529
|
+
) -> Dict[str, Any]:
|
|
530
|
+
"""Create a webhook for agent events"""
|
|
531
|
+
if agent_id not in self.agents:
|
|
532
|
+
return {
|
|
533
|
+
"status": "error",
|
|
534
|
+
"message": "Agent not found"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
registration = self.agents[agent_id]
|
|
538
|
+
registration.webhook_url = webhook_url
|
|
539
|
+
|
|
540
|
+
# Generate webhook secret
|
|
541
|
+
webhook_secret = self._generate_webhook_secret(agent_id)
|
|
542
|
+
|
|
543
|
+
# Store webhook info
|
|
544
|
+
webhook_info = {
|
|
545
|
+
"agent_id": agent_id,
|
|
546
|
+
"webhook_url": webhook_url,
|
|
547
|
+
"events": events or ["all"],
|
|
548
|
+
"secret": webhook_secret,
|
|
549
|
+
"created_at": datetime.now(timezone.utc).isoformat()
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
await self.storage.write("agent_webhooks", webhook_info)
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
"status": "success",
|
|
556
|
+
"webhook_url": webhook_url,
|
|
557
|
+
"secret": webhook_secret,
|
|
558
|
+
"message": "Webhook created successfully"
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async def handle_webhook(self, agent_id: str, payload: Dict[str, Any], signature: str) -> bool:
|
|
562
|
+
"""Handle incoming webhook from an agent"""
|
|
563
|
+
try:
|
|
564
|
+
# Verify webhook signature
|
|
565
|
+
if not self._verify_webhook_signature(agent_id, payload, signature):
|
|
566
|
+
logger.warning(f"Invalid webhook signature for agent {agent_id}")
|
|
567
|
+
return False
|
|
568
|
+
|
|
569
|
+
# Process webhook based on event type
|
|
570
|
+
event_type = payload.get("event")
|
|
571
|
+
|
|
572
|
+
if event_type == "health.status":
|
|
573
|
+
await self._handle_health_webhook(agent_id, payload)
|
|
574
|
+
elif event_type == "capability.update":
|
|
575
|
+
await self._handle_capability_webhook(agent_id, payload)
|
|
576
|
+
elif event_type == "status.update":
|
|
577
|
+
await self._handle_status_webhook(agent_id, payload)
|
|
578
|
+
|
|
579
|
+
return True
|
|
580
|
+
|
|
581
|
+
except Exception as e:
|
|
582
|
+
logger.error(f"Error handling webhook for agent {agent_id}: {e}")
|
|
583
|
+
return False
|
|
584
|
+
|
|
585
|
+
async def _handle_health_webhook(self, agent_id: str, payload: Dict[str, Any]):
|
|
586
|
+
"""Handle health status update webhook"""
|
|
587
|
+
if agent_id in self.agents:
|
|
588
|
+
registration = self.agents[agent_id]
|
|
589
|
+
registration.health_status = payload.get("status")
|
|
590
|
+
registration.last_heartbeat = datetime.now(timezone.utc).isoformat()
|
|
591
|
+
|
|
592
|
+
async def _handle_capability_webhook(self, agent_id: str, payload: Dict[str, Any]):
|
|
593
|
+
"""Handle capability update webhook"""
|
|
594
|
+
if agent_id in self.agents:
|
|
595
|
+
registration = self.agents[agent_id]
|
|
596
|
+
old_capabilities = registration.capabilities.copy()
|
|
597
|
+
new_capabilities = payload.get("capabilities", [])
|
|
598
|
+
|
|
599
|
+
registration.capabilities = new_capabilities
|
|
600
|
+
|
|
601
|
+
# Update capability index
|
|
602
|
+
self._update_indexes(registration, old_capabilities)
|
|
603
|
+
|
|
604
|
+
async def _handle_status_webhook(self, agent_id: str, payload: Dict[str, Any]):
|
|
605
|
+
"""Handle status update webhook"""
|
|
606
|
+
new_status = payload.get("status")
|
|
607
|
+
if new_status:
|
|
608
|
+
try:
|
|
609
|
+
status_enum = AgentStatus(new_status)
|
|
610
|
+
await self.update_agent_status(agent_id, status_enum)
|
|
611
|
+
except ValueError:
|
|
612
|
+
logger.error(f"Invalid status in webhook: {new_status}")
|
|
613
|
+
|
|
614
|
+
def _validate_registration(self, registration: AgentRegistration) -> bool:
|
|
615
|
+
"""Validate agent registration data"""
|
|
616
|
+
required_fields = ["agent_id", "name", "description", "language", "endpoint"]
|
|
617
|
+
|
|
618
|
+
for field in required_fields:
|
|
619
|
+
if not getattr(registration, field):
|
|
620
|
+
return False
|
|
621
|
+
|
|
622
|
+
# Basic format validation
|
|
623
|
+
if not registration.endpoint.startswith(("http://", "https://")):
|
|
624
|
+
return False
|
|
625
|
+
|
|
626
|
+
return True
|
|
627
|
+
|
|
628
|
+
def _get_validation_errors(self, registration: AgentRegistration) -> List[str]:
|
|
629
|
+
"""Get validation error messages"""
|
|
630
|
+
errors = []
|
|
631
|
+
|
|
632
|
+
required_fields = ["agent_id", "name", "description", "language", "endpoint"]
|
|
633
|
+
for field in required_fields:
|
|
634
|
+
if not getattr(registration, field):
|
|
635
|
+
errors.append(f"Missing required field: {field}")
|
|
636
|
+
|
|
637
|
+
if registration.endpoint and not registration.endpoint.startswith(("http://", "https://")):
|
|
638
|
+
errors.append("Invalid endpoint URL format")
|
|
639
|
+
|
|
640
|
+
return errors
|
|
641
|
+
|
|
642
|
+
def _generate_agent_token(self, registration: AgentRegistration) -> str:
|
|
643
|
+
"""Generate authentication token for agent"""
|
|
644
|
+
data = f"{registration.agent_id}:{registration.endpoint}:{datetime.now(timezone.utc).isoformat()}"
|
|
645
|
+
return hashlib.sha256(data.encode()).hexdigest()
|
|
646
|
+
|
|
647
|
+
def _generate_webhook_secret(self, agent_id: str) -> str:
|
|
648
|
+
"""Generate webhook secret for an agent"""
|
|
649
|
+
return secrets.token_urlsafe(32)
|
|
650
|
+
|
|
651
|
+
def _verify_webhook_signature(self, agent_id: str, payload: Dict[str, Any], signature: str) -> bool:
|
|
652
|
+
"""Verify webhook signature"""
|
|
653
|
+
# In production, use proper HMAC verification
|
|
654
|
+
# This is a simplified implementation
|
|
655
|
+
expected_data = json.dumps(payload, sort_keys=True)
|
|
656
|
+
expected_signature = hashlib.sha256(f"{expected_data}:{agent_id}".encode()).hexdigest()
|
|
657
|
+
return secrets.compare_digest(signature, expected_signature)
|
|
658
|
+
|
|
659
|
+
def _update_indexes(self, registration: AgentRegistration, old_capabilities: List[str] = None):
|
|
660
|
+
"""Update capability, protocol, and language indexes"""
|
|
661
|
+
agent_id = registration.agent_id
|
|
662
|
+
|
|
663
|
+
# Update capability index
|
|
664
|
+
if old_capabilities:
|
|
665
|
+
# Remove old capabilities
|
|
666
|
+
for cap in old_capabilities:
|
|
667
|
+
if cap in self.capability_index and agent_id in self.capability_index[cap]:
|
|
668
|
+
self.capability_index[cap].remove(agent_id)
|
|
669
|
+
|
|
670
|
+
for cap in registration.capabilities:
|
|
671
|
+
if cap not in self.capability_index:
|
|
672
|
+
self.capability_index[cap] = []
|
|
673
|
+
if agent_id not in self.capability_index[cap]:
|
|
674
|
+
self.capability_index[cap].append(agent_id)
|
|
675
|
+
|
|
676
|
+
# Update protocol index
|
|
677
|
+
for protocol in registration.protocols:
|
|
678
|
+
if protocol not in self.protocol_index:
|
|
679
|
+
self.protocol_index[protocol] = []
|
|
680
|
+
if agent_id not in self.protocol_index[protocol]:
|
|
681
|
+
self.protocol_index[protocol].append(agent_id)
|
|
682
|
+
|
|
683
|
+
# Update language index
|
|
684
|
+
language = registration.language
|
|
685
|
+
if language not in self.language_index:
|
|
686
|
+
self.language_index[language] = []
|
|
687
|
+
if agent_id not in self.language_index[language]:
|
|
688
|
+
self.language_index[language].append(agent_id)
|
|
689
|
+
|
|
690
|
+
async def _immediate_health_check(self, registration: AgentRegistration):
|
|
691
|
+
"""Perform immediate health check on newly registered agent"""
|
|
692
|
+
try:
|
|
693
|
+
start_time = datetime.now(timezone.utc)
|
|
694
|
+
|
|
695
|
+
async with aiohttp.ClientSession() as session:
|
|
696
|
+
check_url = registration.health_check_url or f"{registration.endpoint}/health"
|
|
697
|
+
timeout = aiohttp.ClientTimeout(total=10)
|
|
698
|
+
|
|
699
|
+
async with session.get(check_url, timeout=timeout) as response:
|
|
700
|
+
end_time = datetime.now(timezone.utc)
|
|
701
|
+
response_time = (end_time - start_time).total_seconds() * 1000
|
|
702
|
+
|
|
703
|
+
registration.health_status = "healthy" if response.status == 200 else "unhealthy"
|
|
704
|
+
registration.last_heartbeat = end_time.isoformat()
|
|
705
|
+
registration.latency_ms = response_time
|
|
706
|
+
|
|
707
|
+
except Exception as e:
|
|
708
|
+
registration.health_status = "error"
|
|
709
|
+
registration.last_heartbeat = datetime.now(timezone.utc).isoformat()
|
|
710
|
+
logger.error(f"Immediate health check failed for {registration.agent_id}: {e}")
|
|
711
|
+
|
|
712
|
+
def _calculate_uptime(self, health_history: List[HealthCheckResult]) -> float:
|
|
713
|
+
"""Calculate uptime percentage from health history"""
|
|
714
|
+
if not health_history:
|
|
715
|
+
return 0.0
|
|
716
|
+
|
|
717
|
+
healthy_checks = sum(1 for check in health_history if check.status == "healthy")
|
|
718
|
+
total_checks = len(health_history)
|
|
719
|
+
|
|
720
|
+
return (healthy_checks / total_checks) * 100 if total_checks > 0 else 0.0
|
|
721
|
+
|
|
722
|
+
def _calculate_avg_response_time(self, health_history: List[HealthCheckResult]) -> float:
|
|
723
|
+
"""Calculate average response time from health history"""
|
|
724
|
+
if not health_history:
|
|
725
|
+
return 0.0
|
|
726
|
+
|
|
727
|
+
response_times = [check.response_time_ms for check in health_history if check.response_time_ms is not None]
|
|
728
|
+
|
|
729
|
+
return sum(response_times) / len(response_times) if response_times else 0.0
|
|
730
|
+
|
|
731
|
+
async def get_registry_stats(self) -> Dict[str, Any]:
|
|
732
|
+
"""Get registry statistics"""
|
|
733
|
+
total_agents = len(self.agents)
|
|
734
|
+
active_agents = sum(1 for reg in self.agents.values() if reg.status == AgentStatus.ACTIVE)
|
|
735
|
+
|
|
736
|
+
language_stats = {}
|
|
737
|
+
for language in AgentLanguage:
|
|
738
|
+
count = sum(1 for reg in self.agents.values() if reg.language == language)
|
|
739
|
+
if count > 0:
|
|
740
|
+
language_stats[language.value] = count
|
|
741
|
+
|
|
742
|
+
protocol_stats = {}
|
|
743
|
+
for protocol in AgentProtocol:
|
|
744
|
+
count = sum(1 for reg in self.agents.values() if protocol in reg.protocols)
|
|
745
|
+
if count > 0:
|
|
746
|
+
protocol_stats[protocol.value] = count
|
|
747
|
+
|
|
748
|
+
return {
|
|
749
|
+
"total_agents": total_agents,
|
|
750
|
+
"active_agents": active_agents,
|
|
751
|
+
"inactive_agents": total_agents - active_agents,
|
|
752
|
+
"language_distribution": language_stats,
|
|
753
|
+
"protocol_distribution": protocol_stats,
|
|
754
|
+
"capabilities_available": list(self.capability_index.keys()),
|
|
755
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
# Export classes for easy importing
|
|
759
|
+
__all__ = [
|
|
760
|
+
'AgentProtocol',
|
|
761
|
+
'AgentStatus',
|
|
762
|
+
'AgentLanguage',
|
|
763
|
+
'AgentRegistration',
|
|
764
|
+
'HealthCheckResult',
|
|
765
|
+
'ProtocolDetector',
|
|
766
|
+
'HealthMonitor',
|
|
767
|
+
'MultiLanguageAgentRegistry'
|
|
768
|
+
]
|