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.
Files changed (66) hide show
  1. a2a_server/__init__.py +29 -0
  2. a2a_server/a2a_agent_card.py +365 -0
  3. a2a_server/a2a_errors.py +1133 -0
  4. a2a_server/a2a_executor.py +926 -0
  5. a2a_server/a2a_router.py +1033 -0
  6. a2a_server/a2a_types.py +344 -0
  7. a2a_server/agent_card.py +408 -0
  8. a2a_server/agents_server.py +271 -0
  9. a2a_server/auth_api.py +349 -0
  10. a2a_server/billing_api.py +638 -0
  11. a2a_server/billing_service.py +712 -0
  12. a2a_server/billing_webhooks.py +501 -0
  13. a2a_server/config.py +96 -0
  14. a2a_server/database.py +2165 -0
  15. a2a_server/email_inbound.py +398 -0
  16. a2a_server/email_notifications.py +486 -0
  17. a2a_server/enhanced_agents.py +919 -0
  18. a2a_server/enhanced_server.py +160 -0
  19. a2a_server/hosted_worker.py +1049 -0
  20. a2a_server/integrated_agents_server.py +347 -0
  21. a2a_server/keycloak_auth.py +750 -0
  22. a2a_server/livekit_bridge.py +439 -0
  23. a2a_server/marketing_tools.py +1364 -0
  24. a2a_server/mcp_client.py +196 -0
  25. a2a_server/mcp_http_server.py +2256 -0
  26. a2a_server/mcp_server.py +191 -0
  27. a2a_server/message_broker.py +725 -0
  28. a2a_server/mock_mcp.py +273 -0
  29. a2a_server/models.py +494 -0
  30. a2a_server/monitor_api.py +5904 -0
  31. a2a_server/opencode_bridge.py +1594 -0
  32. a2a_server/redis_task_manager.py +518 -0
  33. a2a_server/server.py +726 -0
  34. a2a_server/task_manager.py +668 -0
  35. a2a_server/task_queue.py +742 -0
  36. a2a_server/tenant_api.py +333 -0
  37. a2a_server/tenant_middleware.py +219 -0
  38. a2a_server/tenant_service.py +760 -0
  39. a2a_server/user_auth.py +721 -0
  40. a2a_server/vault_client.py +576 -0
  41. a2a_server/worker_sse.py +873 -0
  42. agent_worker/__init__.py +8 -0
  43. agent_worker/worker.py +4877 -0
  44. codetether/__init__.py +10 -0
  45. codetether/__main__.py +4 -0
  46. codetether/cli.py +112 -0
  47. codetether/worker_cli.py +57 -0
  48. codetether-1.2.2.dist-info/METADATA +570 -0
  49. codetether-1.2.2.dist-info/RECORD +66 -0
  50. codetether-1.2.2.dist-info/WHEEL +5 -0
  51. codetether-1.2.2.dist-info/entry_points.txt +4 -0
  52. codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
  53. codetether-1.2.2.dist-info/top_level.txt +5 -0
  54. codetether_voice_agent/__init__.py +6 -0
  55. codetether_voice_agent/agent.py +445 -0
  56. codetether_voice_agent/codetether_mcp.py +345 -0
  57. codetether_voice_agent/config.py +16 -0
  58. codetether_voice_agent/functiongemma_caller.py +380 -0
  59. codetether_voice_agent/session_playback.py +247 -0
  60. codetether_voice_agent/tools/__init__.py +21 -0
  61. codetether_voice_agent/tools/definitions.py +135 -0
  62. codetether_voice_agent/tools/handlers.py +380 -0
  63. run_server.py +314 -0
  64. ui/monitor-tailwind.html +1790 -0
  65. ui/monitor.html +1775 -0
  66. 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')