codetether 1.2.2__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.
- a2a_server/__init__.py +29 -0
- a2a_server/a2a_agent_card.py +365 -0
- a2a_server/a2a_errors.py +1133 -0
- a2a_server/a2a_executor.py +926 -0
- a2a_server/a2a_router.py +1033 -0
- a2a_server/a2a_types.py +344 -0
- a2a_server/agent_card.py +408 -0
- a2a_server/agents_server.py +271 -0
- a2a_server/auth_api.py +349 -0
- a2a_server/billing_api.py +638 -0
- a2a_server/billing_service.py +712 -0
- a2a_server/billing_webhooks.py +501 -0
- a2a_server/config.py +96 -0
- a2a_server/database.py +2165 -0
- a2a_server/email_inbound.py +398 -0
- a2a_server/email_notifications.py +486 -0
- a2a_server/enhanced_agents.py +919 -0
- a2a_server/enhanced_server.py +160 -0
- a2a_server/hosted_worker.py +1049 -0
- a2a_server/integrated_agents_server.py +347 -0
- a2a_server/keycloak_auth.py +750 -0
- a2a_server/livekit_bridge.py +439 -0
- a2a_server/marketing_tools.py +1364 -0
- a2a_server/mcp_client.py +196 -0
- a2a_server/mcp_http_server.py +2256 -0
- a2a_server/mcp_server.py +191 -0
- a2a_server/message_broker.py +725 -0
- a2a_server/mock_mcp.py +273 -0
- a2a_server/models.py +494 -0
- a2a_server/monitor_api.py +5904 -0
- a2a_server/opencode_bridge.py +1594 -0
- a2a_server/redis_task_manager.py +518 -0
- a2a_server/server.py +726 -0
- a2a_server/task_manager.py +668 -0
- a2a_server/task_queue.py +742 -0
- a2a_server/tenant_api.py +333 -0
- a2a_server/tenant_middleware.py +219 -0
- a2a_server/tenant_service.py +760 -0
- a2a_server/user_auth.py +721 -0
- a2a_server/vault_client.py +576 -0
- a2a_server/worker_sse.py +873 -0
- agent_worker/__init__.py +8 -0
- agent_worker/worker.py +4877 -0
- codetether/__init__.py +10 -0
- codetether/__main__.py +4 -0
- codetether/cli.py +112 -0
- codetether/worker_cli.py +57 -0
- codetether-1.2.2.dist-info/METADATA +570 -0
- codetether-1.2.2.dist-info/RECORD +66 -0
- codetether-1.2.2.dist-info/WHEEL +5 -0
- codetether-1.2.2.dist-info/entry_points.txt +4 -0
- codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
- codetether-1.2.2.dist-info/top_level.txt +5 -0
- codetether_voice_agent/__init__.py +6 -0
- codetether_voice_agent/agent.py +445 -0
- codetether_voice_agent/codetether_mcp.py +345 -0
- codetether_voice_agent/config.py +16 -0
- codetether_voice_agent/functiongemma_caller.py +380 -0
- codetether_voice_agent/session_playback.py +247 -0
- codetether_voice_agent/tools/__init__.py +21 -0
- codetether_voice_agent/tools/definitions.py +135 -0
- codetether_voice_agent/tools/handlers.py +380 -0
- run_server.py +314 -0
- ui/monitor-tailwind.html +1790 -0
- ui/monitor.html +1775 -0
- ui/monitor.js +2662 -0
a2a_server/server.py
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A2A Protocol Server Implementation
|
|
3
|
+
|
|
4
|
+
Main server implementation providing JSON-RPC 2.0 over HTTP(S) with support for
|
|
5
|
+
streaming, task management, and agent discovery.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, Any, Optional, Callable, List
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
|
16
|
+
from fastapi.responses import StreamingResponse, JSONResponse
|
|
17
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
18
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
19
|
+
import uvicorn
|
|
20
|
+
|
|
21
|
+
from .models import (
|
|
22
|
+
JSONRPCRequest,
|
|
23
|
+
JSONRPCResponse,
|
|
24
|
+
JSONRPCError,
|
|
25
|
+
SendMessageRequest,
|
|
26
|
+
SendMessageResponse,
|
|
27
|
+
GetTaskRequest,
|
|
28
|
+
GetTaskResponse,
|
|
29
|
+
CancelTaskRequest,
|
|
30
|
+
CancelTaskResponse,
|
|
31
|
+
StreamMessageRequest,
|
|
32
|
+
TaskStatusUpdateEvent,
|
|
33
|
+
Task,
|
|
34
|
+
TaskStatus,
|
|
35
|
+
Message,
|
|
36
|
+
Part,
|
|
37
|
+
LiveKitTokenRequest,
|
|
38
|
+
LiveKitTokenResponse,
|
|
39
|
+
)
|
|
40
|
+
from .task_manager import TaskManager, PersistentTaskManager
|
|
41
|
+
from .database import DATABASE_URL
|
|
42
|
+
from .message_broker import MessageBroker, InMemoryMessageBroker
|
|
43
|
+
from .agent_card import AgentCard
|
|
44
|
+
from .monitor_api import (
|
|
45
|
+
monitor_router,
|
|
46
|
+
opencode_router,
|
|
47
|
+
voice_router,
|
|
48
|
+
auth_router,
|
|
49
|
+
nextauth_router,
|
|
50
|
+
monitoring_service,
|
|
51
|
+
log_agent_message,
|
|
52
|
+
)
|
|
53
|
+
from .worker_sse import worker_sse_router
|
|
54
|
+
from .email_inbound import email_router
|
|
55
|
+
from .email_api import email_api_router
|
|
56
|
+
from .tenant_middleware import TenantContextMiddleware
|
|
57
|
+
from .tenant_api import router as tenant_router
|
|
58
|
+
from .billing_api import router as billing_router
|
|
59
|
+
from .billing_webhooks import billing_webhook_router
|
|
60
|
+
from .a2a_agent_card import a2a_agent_card_router
|
|
61
|
+
|
|
62
|
+
# Import A2A protocol router for standards-compliant agent communication
|
|
63
|
+
try:
|
|
64
|
+
from .a2a_router import create_a2a_router
|
|
65
|
+
from .a2a_executor import CodetetherExecutor, create_codetether_executor
|
|
66
|
+
|
|
67
|
+
A2A_ROUTER_AVAILABLE = True
|
|
68
|
+
except ImportError as e:
|
|
69
|
+
A2A_ROUTER_AVAILABLE = False
|
|
70
|
+
create_a2a_router = None
|
|
71
|
+
CodetetherExecutor = None
|
|
72
|
+
create_codetether_executor = None
|
|
73
|
+
logger.warning(f'A2A router not available: {e}')
|
|
74
|
+
|
|
75
|
+
# Import queue API router for operational visibility (mid-market)
|
|
76
|
+
try:
|
|
77
|
+
from .queue_api import router as queue_api_router
|
|
78
|
+
|
|
79
|
+
QUEUE_API_AVAILABLE = True
|
|
80
|
+
except ImportError:
|
|
81
|
+
QUEUE_API_AVAILABLE = False
|
|
82
|
+
queue_api_router = None
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
from .livekit_bridge import create_livekit_bridge, LiveKitBridge
|
|
86
|
+
|
|
87
|
+
LIVEKIT_AVAILABLE = True
|
|
88
|
+
except ImportError:
|
|
89
|
+
LIVEKIT_AVAILABLE = False
|
|
90
|
+
create_livekit_bridge = None
|
|
91
|
+
LiveKitBridge = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
logger = logging.getLogger(__name__)
|
|
95
|
+
security = HTTPBearer(auto_error=False)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class A2AServer:
|
|
99
|
+
"""Main A2A Protocol Server implementation."""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
agent_card: AgentCard,
|
|
104
|
+
task_manager: Optional[TaskManager] = None,
|
|
105
|
+
message_broker: Optional[MessageBroker] = None,
|
|
106
|
+
auth_callback: Optional[Callable[[str], bool]] = None,
|
|
107
|
+
):
|
|
108
|
+
self.agent_card = agent_card
|
|
109
|
+
self.task_manager = task_manager or PersistentTaskManager(DATABASE_URL)
|
|
110
|
+
self.message_broker = message_broker or InMemoryMessageBroker()
|
|
111
|
+
self.auth_callback = auth_callback
|
|
112
|
+
|
|
113
|
+
# Initialize LiveKit bridge if available
|
|
114
|
+
self.livekit_bridge = None
|
|
115
|
+
if LIVEKIT_AVAILABLE and create_livekit_bridge:
|
|
116
|
+
try:
|
|
117
|
+
self.livekit_bridge = create_livekit_bridge()
|
|
118
|
+
if self.livekit_bridge:
|
|
119
|
+
logger.info('LiveKit bridge initialized successfully')
|
|
120
|
+
else:
|
|
121
|
+
logger.info(
|
|
122
|
+
'LiveKit bridge not configured - media features disabled'
|
|
123
|
+
)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.warning(f'Failed to initialize LiveKit bridge: {e}')
|
|
126
|
+
else:
|
|
127
|
+
logger.info('LiveKit not available - media features disabled')
|
|
128
|
+
|
|
129
|
+
# Create FastAPI app
|
|
130
|
+
self.app = FastAPI(
|
|
131
|
+
title=f'A2A Server - {agent_card.card.name}',
|
|
132
|
+
description=agent_card.card.description,
|
|
133
|
+
version=agent_card.card.version,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Add CORS middleware
|
|
137
|
+
self.app.add_middleware(
|
|
138
|
+
CORSMiddleware,
|
|
139
|
+
allow_origins=['*'],
|
|
140
|
+
allow_credentials=True,
|
|
141
|
+
allow_methods=['*'],
|
|
142
|
+
allow_headers=['*'],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Add tenant context middleware (extracts tenant from JWT)
|
|
146
|
+
self.app.add_middleware(TenantContextMiddleware)
|
|
147
|
+
|
|
148
|
+
# Method handlers
|
|
149
|
+
self._method_handlers: Dict[str, Callable] = {
|
|
150
|
+
'message/send': self._handle_send_message,
|
|
151
|
+
'message/stream': self._handle_stream_message,
|
|
152
|
+
'tasks/get': self._handle_get_task,
|
|
153
|
+
'tasks/cancel': self._handle_cancel_task,
|
|
154
|
+
'tasks/resubscribe': self._handle_resubscribe_task,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Active streaming connections
|
|
158
|
+
self._streaming_connections: Dict[str, List[asyncio.Queue]] = {}
|
|
159
|
+
|
|
160
|
+
# Store agent info for deferred registration
|
|
161
|
+
self.agent_id = str(uuid.uuid4())
|
|
162
|
+
self._agent_name = agent_card.card.name
|
|
163
|
+
|
|
164
|
+
self._setup_routes()
|
|
165
|
+
|
|
166
|
+
# Register agent on startup (async)
|
|
167
|
+
@self.app.on_event('startup')
|
|
168
|
+
async def register_with_monitoring():
|
|
169
|
+
await monitoring_service.register_agent(
|
|
170
|
+
self.agent_id, self._agent_name
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _setup_routes(self) -> None:
|
|
174
|
+
"""Setup FastAPI routes."""
|
|
175
|
+
|
|
176
|
+
@self.app.get('/.well-known/agent-card.json')
|
|
177
|
+
async def get_agent_card():
|
|
178
|
+
"""Serve the agent card for discovery."""
|
|
179
|
+
return JSONResponse(content=self.agent_card.to_dict())
|
|
180
|
+
|
|
181
|
+
@self.app.post('/')
|
|
182
|
+
async def handle_jsonrpc(
|
|
183
|
+
request: Request,
|
|
184
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(
|
|
185
|
+
security
|
|
186
|
+
),
|
|
187
|
+
):
|
|
188
|
+
"""Handle JSON-RPC 2.0 requests."""
|
|
189
|
+
return await self._handle_jsonrpc_request(request, credentials)
|
|
190
|
+
|
|
191
|
+
# Backwards/Docs compatibility: accept JSON-RPC at a versioned path.
|
|
192
|
+
# Historically, docs and clients used /v1/a2a.
|
|
193
|
+
@self.app.post('/v1/a2a')
|
|
194
|
+
async def handle_jsonrpc_v1(
|
|
195
|
+
request: Request,
|
|
196
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(
|
|
197
|
+
security
|
|
198
|
+
),
|
|
199
|
+
):
|
|
200
|
+
"""Handle JSON-RPC 2.0 requests (alias for `/`)."""
|
|
201
|
+
return await self._handle_jsonrpc_request(request, credentials)
|
|
202
|
+
|
|
203
|
+
@self.app.get('/health')
|
|
204
|
+
async def health_check():
|
|
205
|
+
"""Health check endpoint."""
|
|
206
|
+
return {
|
|
207
|
+
'status': 'healthy',
|
|
208
|
+
'timestamp': datetime.utcnow().isoformat(),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@self.app.get('/agents')
|
|
212
|
+
async def discover_agents():
|
|
213
|
+
"""Discover other agents through the message broker."""
|
|
214
|
+
agents = await self.message_broker.discover_agents()
|
|
215
|
+
return [agent.model_dump() for agent in agents]
|
|
216
|
+
|
|
217
|
+
@self.app.post('/v1/livekit/token', response_model=LiveKitTokenResponse)
|
|
218
|
+
async def get_livekit_token(
|
|
219
|
+
token_request: LiveKitTokenRequest,
|
|
220
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(
|
|
221
|
+
security
|
|
222
|
+
),
|
|
223
|
+
):
|
|
224
|
+
"""Get a LiveKit access token for media sessions."""
|
|
225
|
+
return await self._handle_livekit_token_request(
|
|
226
|
+
token_request, credentials
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Include monitoring API routes
|
|
230
|
+
self.app.include_router(monitor_router)
|
|
231
|
+
|
|
232
|
+
# Include OpenCode integration routes
|
|
233
|
+
self.app.include_router(opencode_router)
|
|
234
|
+
|
|
235
|
+
# Include authentication routes
|
|
236
|
+
self.app.include_router(auth_router)
|
|
237
|
+
|
|
238
|
+
# Include NextAuth compatibility routes for Cypress
|
|
239
|
+
self.app.include_router(nextauth_router)
|
|
240
|
+
|
|
241
|
+
# Include voice session routes
|
|
242
|
+
self.app.include_router(voice_router)
|
|
243
|
+
|
|
244
|
+
# Include worker SSE routes for push-based task distribution
|
|
245
|
+
self.app.include_router(worker_sse_router)
|
|
246
|
+
|
|
247
|
+
# Include email inbound webhook routes for reply-based task continuation
|
|
248
|
+
self.app.include_router(email_router)
|
|
249
|
+
|
|
250
|
+
# Include email debugging and testing routes
|
|
251
|
+
self.app.include_router(email_api_router)
|
|
252
|
+
|
|
253
|
+
# Include tenant management routes
|
|
254
|
+
self.app.include_router(tenant_router)
|
|
255
|
+
|
|
256
|
+
# Include billing API routes for subscription management
|
|
257
|
+
self.app.include_router(billing_router)
|
|
258
|
+
|
|
259
|
+
# Include billing webhook routes for Stripe
|
|
260
|
+
self.app.include_router(billing_webhook_router)
|
|
261
|
+
|
|
262
|
+
# Include queue API routes for operational visibility (mid-market)
|
|
263
|
+
if QUEUE_API_AVAILABLE and queue_api_router:
|
|
264
|
+
self.app.include_router(queue_api_router)
|
|
265
|
+
|
|
266
|
+
# Include A2A protocol compliant agent card endpoint
|
|
267
|
+
# This serves the standard /.well-known/agent-card.json for discovery
|
|
268
|
+
self.app.include_router(a2a_agent_card_router)
|
|
269
|
+
|
|
270
|
+
# Include A2A protocol router for standards-compliant agent communication
|
|
271
|
+
# This provides JSON-RPC and REST bindings at /a2a/*
|
|
272
|
+
if A2A_ROUTER_AVAILABLE and create_a2a_router:
|
|
273
|
+
try:
|
|
274
|
+
a2a_router = create_a2a_router(
|
|
275
|
+
executor=create_codetether_executor(
|
|
276
|
+
task_queue=None, # Will be initialized lazily
|
|
277
|
+
database=None,
|
|
278
|
+
)
|
|
279
|
+
if create_codetether_executor
|
|
280
|
+
else None,
|
|
281
|
+
database_url=DATABASE_URL,
|
|
282
|
+
require_authentication=False, # Allow public access, auth per-endpoint
|
|
283
|
+
)
|
|
284
|
+
self.app.include_router(a2a_router)
|
|
285
|
+
logger.info('A2A protocol router mounted at /a2a')
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.warning(f'Failed to mount A2A router: {e}')
|
|
288
|
+
|
|
289
|
+
async def _handle_jsonrpc_request(
|
|
290
|
+
self,
|
|
291
|
+
request: Request,
|
|
292
|
+
credentials: Optional[HTTPAuthorizationCredentials],
|
|
293
|
+
) -> Response:
|
|
294
|
+
"""Handle incoming JSON-RPC request."""
|
|
295
|
+
try:
|
|
296
|
+
# Parse request body
|
|
297
|
+
body = await request.body()
|
|
298
|
+
try:
|
|
299
|
+
request_data = json.loads(body)
|
|
300
|
+
except json.JSONDecodeError:
|
|
301
|
+
return self._create_error_response(None, -32700, 'Parse error')
|
|
302
|
+
|
|
303
|
+
# Validate JSON-RPC structure
|
|
304
|
+
try:
|
|
305
|
+
rpc_request = JSONRPCRequest.model_validate(request_data)
|
|
306
|
+
except Exception:
|
|
307
|
+
return self._create_error_response(
|
|
308
|
+
request_data.get('id'), -32600, 'Invalid Request'
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Check authentication if required
|
|
312
|
+
if self.agent_card.card.authentication and self.auth_callback:
|
|
313
|
+
if not credentials or not self.auth_callback(
|
|
314
|
+
credentials.credentials
|
|
315
|
+
):
|
|
316
|
+
return self._create_error_response(
|
|
317
|
+
rpc_request.id, -32001, 'Authentication failed'
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Handle method
|
|
321
|
+
method_handler = self._method_handlers.get(rpc_request.method)
|
|
322
|
+
if not method_handler:
|
|
323
|
+
return self._create_error_response(
|
|
324
|
+
rpc_request.id, -32601, 'Method not found'
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
result = await method_handler(rpc_request.params or {})
|
|
329
|
+
|
|
330
|
+
# Check if this is a streaming response
|
|
331
|
+
if isinstance(result, StreamingResponse):
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
return self._create_success_response(rpc_request.id, result)
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
logger.error(f'Error handling method {rpc_request.method}: {e}')
|
|
338
|
+
return self._create_error_response(
|
|
339
|
+
rpc_request.id, -32603, f'Internal error: {str(e)}'
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error(f'Error processing JSON-RPC request: {e}')
|
|
344
|
+
return self._create_error_response(None, -32603, 'Internal error')
|
|
345
|
+
|
|
346
|
+
def _create_success_response(
|
|
347
|
+
self, request_id: Any, result: Any
|
|
348
|
+
) -> JSONResponse:
|
|
349
|
+
"""Create a successful JSON-RPC response."""
|
|
350
|
+
response = JSONRPCResponse(id=request_id, result=result)
|
|
351
|
+
return JSONResponse(content=response.model_dump(exclude_none=True))
|
|
352
|
+
|
|
353
|
+
def _create_error_response(
|
|
354
|
+
self, request_id: Any, code: int, message: str
|
|
355
|
+
) -> JSONResponse:
|
|
356
|
+
"""Create an error JSON-RPC response."""
|
|
357
|
+
error = JSONRPCError(code=code, message=message)
|
|
358
|
+
response = JSONRPCResponse(id=request_id, error=error.model_dump())
|
|
359
|
+
return JSONResponse(
|
|
360
|
+
content=response.model_dump(exclude_none=True),
|
|
361
|
+
status_code=400 if code != -32603 else 500,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
async def _handle_send_message(
|
|
365
|
+
self, params: Dict[str, Any]
|
|
366
|
+
) -> Dict[str, Any]:
|
|
367
|
+
"""Handle message/send method."""
|
|
368
|
+
start_time = datetime.now()
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
request = SendMessageRequest.model_validate(params)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
raise ValueError(f'Invalid parameters: {e}')
|
|
374
|
+
|
|
375
|
+
# Log incoming message to monitoring
|
|
376
|
+
message_text = ' '.join(
|
|
377
|
+
[p.content for p in request.message.parts if p.type == 'text']
|
|
378
|
+
)
|
|
379
|
+
await log_agent_message(
|
|
380
|
+
agent_name='External Client',
|
|
381
|
+
content=message_text,
|
|
382
|
+
message_type='human',
|
|
383
|
+
metadata={'task_id': request.task_id, 'skill_id': request.skill_id},
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Create or get task
|
|
387
|
+
if request.task_id:
|
|
388
|
+
task = await self.task_manager.get_task(request.task_id)
|
|
389
|
+
if not task:
|
|
390
|
+
raise ValueError(f'Task not found: {request.task_id}')
|
|
391
|
+
else:
|
|
392
|
+
task = await self.task_manager.create_task(
|
|
393
|
+
title='Message processing',
|
|
394
|
+
description='Processing incoming message',
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Update task status
|
|
398
|
+
await self.task_manager.update_task_status(
|
|
399
|
+
task.id, TaskStatus.WORKING, request.message
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Process the message (this would be implemented by specific agents)
|
|
403
|
+
response_message = await self._process_message(
|
|
404
|
+
request.message, request.skill_id
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Calculate response time
|
|
408
|
+
response_time = (datetime.now() - start_time).total_seconds() * 1000
|
|
409
|
+
|
|
410
|
+
# Log agent response to monitoring
|
|
411
|
+
response_text = ' '.join(
|
|
412
|
+
[p.content for p in response_message.parts if p.type == 'text']
|
|
413
|
+
)
|
|
414
|
+
await log_agent_message(
|
|
415
|
+
agent_name=self.agent_card.card.name,
|
|
416
|
+
content=response_text,
|
|
417
|
+
message_type='agent',
|
|
418
|
+
metadata={'task_id': task.id, 'skill_id': request.skill_id},
|
|
419
|
+
response_time=response_time,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Update task as completed
|
|
423
|
+
await self.task_manager.update_task_status(
|
|
424
|
+
task.id, TaskStatus.COMPLETED, response_message, final=True
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Publish message event
|
|
428
|
+
await self.message_broker.publish_message(
|
|
429
|
+
'external', self.agent_card.card.name, request.message
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
response = SendMessageResponse(task=task, message=response_message)
|
|
433
|
+
return response.model_dump(mode='json')
|
|
434
|
+
|
|
435
|
+
async def _handle_stream_message(
|
|
436
|
+
self, params: Dict[str, Any]
|
|
437
|
+
) -> StreamingResponse:
|
|
438
|
+
"""Handle message/stream method."""
|
|
439
|
+
try:
|
|
440
|
+
request = StreamMessageRequest.model_validate(params)
|
|
441
|
+
except Exception as e:
|
|
442
|
+
raise ValueError(f'Invalid parameters: {e}')
|
|
443
|
+
|
|
444
|
+
# Check if streaming is supported
|
|
445
|
+
if not (
|
|
446
|
+
self.agent_card.card.capabilities
|
|
447
|
+
and self.agent_card.card.capabilities.streaming
|
|
448
|
+
):
|
|
449
|
+
raise ValueError('Streaming not supported')
|
|
450
|
+
|
|
451
|
+
# Create task
|
|
452
|
+
task = await self.task_manager.create_task(
|
|
453
|
+
title='Streaming message processing',
|
|
454
|
+
description='Processing streaming message',
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Create event queue for this connection
|
|
458
|
+
event_queue = asyncio.Queue()
|
|
459
|
+
task_id = task.id
|
|
460
|
+
if task_id not in self._streaming_connections:
|
|
461
|
+
self._streaming_connections[task_id] = []
|
|
462
|
+
self._streaming_connections[task_id].append(event_queue)
|
|
463
|
+
|
|
464
|
+
# Register task update handler
|
|
465
|
+
async def task_update_handler(event: TaskStatusUpdateEvent):
|
|
466
|
+
await event_queue.put(event)
|
|
467
|
+
|
|
468
|
+
await self.task_manager.register_update_handler(
|
|
469
|
+
task_id, task_update_handler
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# Start processing in background
|
|
473
|
+
asyncio.create_task(self._process_streaming_message(request, task))
|
|
474
|
+
|
|
475
|
+
# Return streaming response
|
|
476
|
+
async def generate_events():
|
|
477
|
+
try:
|
|
478
|
+
while True:
|
|
479
|
+
try:
|
|
480
|
+
# Wait for next event with timeout
|
|
481
|
+
event = await asyncio.wait_for(
|
|
482
|
+
event_queue.get(), timeout=30.0
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Format as Server-Sent Event
|
|
486
|
+
event_data = {
|
|
487
|
+
'jsonrpc': '2.0',
|
|
488
|
+
'id': task_id,
|
|
489
|
+
'result': {'event': event.model_dump(mode='json')},
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
yield f'data: {json.dumps(event_data)}\n\n'
|
|
493
|
+
|
|
494
|
+
# Break if this is the final event
|
|
495
|
+
if event.final:
|
|
496
|
+
break
|
|
497
|
+
|
|
498
|
+
except asyncio.TimeoutError:
|
|
499
|
+
# Send keepalive
|
|
500
|
+
yield 'data: {}\n\n'
|
|
501
|
+
|
|
502
|
+
finally:
|
|
503
|
+
# Cleanup
|
|
504
|
+
if task_id in self._streaming_connections:
|
|
505
|
+
try:
|
|
506
|
+
self._streaming_connections[task_id].remove(event_queue)
|
|
507
|
+
if not self._streaming_connections[task_id]:
|
|
508
|
+
del self._streaming_connections[task_id]
|
|
509
|
+
except ValueError:
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
await self.task_manager.unregister_update_handler(
|
|
513
|
+
task_id, task_update_handler
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return StreamingResponse(
|
|
517
|
+
generate_events(),
|
|
518
|
+
media_type='text/event-stream',
|
|
519
|
+
headers={
|
|
520
|
+
'Cache-Control': 'no-cache',
|
|
521
|
+
'Connection': 'keep-alive',
|
|
522
|
+
},
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
async def _handle_get_task(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
526
|
+
"""Handle tasks/get method."""
|
|
527
|
+
try:
|
|
528
|
+
request = GetTaskRequest.model_validate(params)
|
|
529
|
+
except Exception as e:
|
|
530
|
+
raise ValueError(f'Invalid parameters: {e}')
|
|
531
|
+
|
|
532
|
+
task = await self.task_manager.get_task(request.task_id)
|
|
533
|
+
if not task:
|
|
534
|
+
raise ValueError(f'Task not found: {request.task_id}')
|
|
535
|
+
|
|
536
|
+
response = GetTaskResponse(task=task)
|
|
537
|
+
return response.model_dump(mode='json')
|
|
538
|
+
|
|
539
|
+
async def _handle_cancel_task(
|
|
540
|
+
self, params: Dict[str, Any]
|
|
541
|
+
) -> Dict[str, Any]:
|
|
542
|
+
"""Handle tasks/cancel method."""
|
|
543
|
+
try:
|
|
544
|
+
request = CancelTaskRequest.model_validate(params)
|
|
545
|
+
except Exception as e:
|
|
546
|
+
raise ValueError(f'Invalid parameters: {e}')
|
|
547
|
+
|
|
548
|
+
task = await self.task_manager.cancel_task(request.task_id)
|
|
549
|
+
if not task:
|
|
550
|
+
raise ValueError(f'Task not found: {request.task_id}')
|
|
551
|
+
|
|
552
|
+
response = CancelTaskResponse(task=task)
|
|
553
|
+
return response.model_dump(mode='json')
|
|
554
|
+
|
|
555
|
+
async def _handle_resubscribe_task(
|
|
556
|
+
self, params: Dict[str, Any]
|
|
557
|
+
) -> StreamingResponse:
|
|
558
|
+
"""Handle tasks/resubscribe method."""
|
|
559
|
+
task_id = params.get('task_id')
|
|
560
|
+
if not task_id:
|
|
561
|
+
raise ValueError('task_id is required')
|
|
562
|
+
|
|
563
|
+
task = await self.task_manager.get_task(task_id)
|
|
564
|
+
if not task:
|
|
565
|
+
raise ValueError(f'Task not found: {task_id}')
|
|
566
|
+
|
|
567
|
+
# This is a simplified implementation - in a real system you'd
|
|
568
|
+
# need to handle reconnection to existing streams
|
|
569
|
+
return await self._handle_stream_message(params)
|
|
570
|
+
|
|
571
|
+
async def _process_message(
|
|
572
|
+
self, message: Message, skill_id: Optional[str] = None
|
|
573
|
+
) -> Message:
|
|
574
|
+
"""Process an incoming message. Override this in subclasses."""
|
|
575
|
+
# Default implementation - echo the message back
|
|
576
|
+
response_parts = []
|
|
577
|
+
for part in message.parts:
|
|
578
|
+
if part.type == 'text':
|
|
579
|
+
response_parts.append(
|
|
580
|
+
Part(type='text', content=f'Received: {part.content}')
|
|
581
|
+
)
|
|
582
|
+
else:
|
|
583
|
+
response_parts.append(part)
|
|
584
|
+
|
|
585
|
+
return Message(parts=response_parts)
|
|
586
|
+
|
|
587
|
+
async def _process_streaming_message(
|
|
588
|
+
self, request: StreamMessageRequest, task: Task
|
|
589
|
+
) -> None:
|
|
590
|
+
"""Process a streaming message with periodic updates."""
|
|
591
|
+
try:
|
|
592
|
+
# Update task status to working
|
|
593
|
+
await self.task_manager.update_task_status(
|
|
594
|
+
task.id, TaskStatus.WORKING, request.message
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Simulate processing with progress updates
|
|
598
|
+
for i in range(5):
|
|
599
|
+
await asyncio.sleep(1) # Simulate work
|
|
600
|
+
progress = (i + 1) / 5
|
|
601
|
+
await self.task_manager.update_task_status(
|
|
602
|
+
task.id, TaskStatus.WORKING, progress=progress
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Generate final response
|
|
606
|
+
response_message = await self._process_message(
|
|
607
|
+
request.message, request.skill_id
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Complete the task
|
|
611
|
+
await self.task_manager.update_task_status(
|
|
612
|
+
task.id, TaskStatus.COMPLETED, response_message, final=True
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
except Exception as e:
|
|
616
|
+
logger.error(f'Error processing streaming message: {e}')
|
|
617
|
+
await self.task_manager.update_task_status(
|
|
618
|
+
task.id, TaskStatus.FAILED, final=True
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
def _validate_auth(
|
|
622
|
+
self, credentials: Optional[HTTPAuthorizationCredentials]
|
|
623
|
+
) -> bool:
|
|
624
|
+
"""Validate authentication credentials."""
|
|
625
|
+
if self.agent_card.card.authentication and self.auth_callback:
|
|
626
|
+
if not credentials or not self.auth_callback(
|
|
627
|
+
credentials.credentials
|
|
628
|
+
):
|
|
629
|
+
return False
|
|
630
|
+
return True
|
|
631
|
+
|
|
632
|
+
async def _handle_livekit_token_request(
|
|
633
|
+
self,
|
|
634
|
+
token_request: LiveKitTokenRequest,
|
|
635
|
+
credentials: Optional[HTTPAuthorizationCredentials],
|
|
636
|
+
) -> LiveKitTokenResponse:
|
|
637
|
+
"""Handle LiveKit token request with A2A authentication."""
|
|
638
|
+
# Validate authentication
|
|
639
|
+
if not self._validate_auth(credentials):
|
|
640
|
+
raise HTTPException(
|
|
641
|
+
status_code=401, detail='Authentication required'
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Check if LiveKit bridge is available
|
|
645
|
+
if not self.livekit_bridge:
|
|
646
|
+
raise HTTPException(
|
|
647
|
+
status_code=503,
|
|
648
|
+
detail='LiveKit functionality not available - bridge not configured',
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
# Mint access token using LiveKit bridge
|
|
653
|
+
access_token = self.livekit_bridge.mint_access_token(
|
|
654
|
+
identity=token_request.identity,
|
|
655
|
+
room_name=token_request.room_name,
|
|
656
|
+
a2a_role=token_request.role,
|
|
657
|
+
metadata=token_request.metadata,
|
|
658
|
+
ttl_minutes=token_request.ttl_minutes,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# Generate join URL
|
|
662
|
+
join_url = self.livekit_bridge.generate_join_url(
|
|
663
|
+
token_request.room_name, access_token
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Calculate expiration time
|
|
667
|
+
expires_at = datetime.now() + timedelta(
|
|
668
|
+
minutes=token_request.ttl_minutes
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
logger.info(
|
|
672
|
+
f'Minted LiveKit token for {token_request.identity} in room {token_request.room_name}'
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
return LiveKitTokenResponse(
|
|
676
|
+
access_token=access_token,
|
|
677
|
+
join_url=join_url,
|
|
678
|
+
expires_at=expires_at,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
except Exception as e:
|
|
682
|
+
logger.error(f'Failed to mint LiveKit token: {e}')
|
|
683
|
+
raise HTTPException(
|
|
684
|
+
status_code=500, detail=f'Failed to generate token: {str(e)}'
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
async def start(self, host: str = '0.0.0.0', port: int = 8000) -> None:
|
|
688
|
+
"""Start the A2A server."""
|
|
689
|
+
# Start message broker
|
|
690
|
+
await self.message_broker.start()
|
|
691
|
+
|
|
692
|
+
# Register this agent
|
|
693
|
+
await self.message_broker.register_agent(self.agent_card.card)
|
|
694
|
+
|
|
695
|
+
logger.info(f'Starting A2A server for {self.agent_card.card.name}')
|
|
696
|
+
logger.info(
|
|
697
|
+
f'Agent card available at: http://{host}:{port}/.well-known/agent-card.json'
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# Start the server
|
|
701
|
+
config = uvicorn.Config(
|
|
702
|
+
self.app, host=host, port=port, log_level='info'
|
|
703
|
+
)
|
|
704
|
+
server = uvicorn.Server(config)
|
|
705
|
+
await server.serve()
|
|
706
|
+
|
|
707
|
+
async def stop(self) -> None:
|
|
708
|
+
"""Stop the A2A server."""
|
|
709
|
+
# Unregister this agent
|
|
710
|
+
await self.message_broker.unregister_agent(self.agent_card.card.name)
|
|
711
|
+
|
|
712
|
+
# Stop message broker
|
|
713
|
+
await self.message_broker.stop()
|
|
714
|
+
|
|
715
|
+
logger.info(f'Stopped A2A server for {self.agent_card.card.name}')
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
# Custom agent implementations would inherit from this
|
|
719
|
+
class CustomA2AAgent(A2AServer):
|
|
720
|
+
"""Base class for custom A2A agent implementations."""
|
|
721
|
+
|
|
722
|
+
async def _process_message(
|
|
723
|
+
self, message: Message, skill_id: Optional[str] = None
|
|
724
|
+
) -> Message:
|
|
725
|
+
"""Override this method to implement custom message processing logic."""
|
|
726
|
+
raise NotImplementedError('Subclasses must implement _process_message')
|