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/a2a_router.py
ADDED
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A2A Protocol Router Integration for FastAPI.
|
|
3
|
+
|
|
4
|
+
Provides a factory function to create an A2A protocol router using the official
|
|
5
|
+
A2A SDK's FastAPI integration. This router handles JSON-RPC and REST bindings
|
|
6
|
+
for the A2A protocol.
|
|
7
|
+
|
|
8
|
+
Endpoints:
|
|
9
|
+
POST /a2a/jsonrpc - JSON-RPC 2.0 endpoint
|
|
10
|
+
POST /a2a/rest/message:send - REST binding for sending messages
|
|
11
|
+
POST /a2a/rest/message:stream - REST binding for streaming messages
|
|
12
|
+
GET /a2a/rest/tasks/{id} - REST binding for getting task status
|
|
13
|
+
GET /a2a/.well-known/agent.json - Agent card discovery
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import uuid
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Union
|
|
24
|
+
|
|
25
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
26
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
27
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
28
|
+
from pydantic import BaseModel, Field
|
|
29
|
+
|
|
30
|
+
from .agent_card import AgentCard
|
|
31
|
+
from .keycloak_auth import (
|
|
32
|
+
UserSession,
|
|
33
|
+
get_current_user,
|
|
34
|
+
keycloak_auth,
|
|
35
|
+
require_auth,
|
|
36
|
+
)
|
|
37
|
+
from .models import Message, Part, Task, TaskStatus
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
# Security scheme for Bearer token authentication
|
|
42
|
+
security = HTTPBearer(auto_error=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# =============================================================================
|
|
46
|
+
# A2A SDK Compatible Types
|
|
47
|
+
# =============================================================================
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TaskState(str, Enum):
|
|
51
|
+
"""Task states as defined by A2A protocol."""
|
|
52
|
+
|
|
53
|
+
PENDING = 'pending'
|
|
54
|
+
WORKING = 'working'
|
|
55
|
+
INPUT_REQUIRED = 'input-required'
|
|
56
|
+
COMPLETED = 'completed'
|
|
57
|
+
CANCELLED = 'cancelled'
|
|
58
|
+
FAILED = 'failed'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class A2AMessage(BaseModel):
|
|
62
|
+
"""A2A protocol message."""
|
|
63
|
+
|
|
64
|
+
role: str = Field(..., description='Message role (user/assistant)')
|
|
65
|
+
parts: List[Dict[str, Any]] = Field(default_factory=list)
|
|
66
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class A2ATask(BaseModel):
|
|
70
|
+
"""A2A protocol task representation."""
|
|
71
|
+
|
|
72
|
+
id: str
|
|
73
|
+
contextId: Optional[str] = None
|
|
74
|
+
status: Dict[str, Any]
|
|
75
|
+
artifacts: Optional[List[Dict[str, Any]]] = None
|
|
76
|
+
history: Optional[List[Dict[str, Any]]] = None
|
|
77
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SendMessageParams(BaseModel):
|
|
81
|
+
"""Parameters for message/send method."""
|
|
82
|
+
|
|
83
|
+
message: A2AMessage
|
|
84
|
+
configuration: Optional[Dict[str, Any]] = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class GetTaskParams(BaseModel):
|
|
88
|
+
"""Parameters for tasks/get method."""
|
|
89
|
+
|
|
90
|
+
id: str
|
|
91
|
+
historyLength: Optional[int] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CancelTaskParams(BaseModel):
|
|
95
|
+
"""Parameters for tasks/cancel method."""
|
|
96
|
+
|
|
97
|
+
id: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class JSONRPCRequest(BaseModel):
|
|
101
|
+
"""JSON-RPC 2.0 request."""
|
|
102
|
+
|
|
103
|
+
jsonrpc: str = '2.0'
|
|
104
|
+
id: Optional[Union[str, int]] = None
|
|
105
|
+
method: str
|
|
106
|
+
params: Optional[Dict[str, Any]] = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class JSONRPCResponse(BaseModel):
|
|
110
|
+
"""JSON-RPC 2.0 response."""
|
|
111
|
+
|
|
112
|
+
jsonrpc: str = '2.0'
|
|
113
|
+
id: Optional[Union[str, int]] = None
|
|
114
|
+
result: Optional[Any] = None
|
|
115
|
+
error: Optional[Dict[str, Any]] = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class JSONRPCError(BaseModel):
|
|
119
|
+
"""JSON-RPC 2.0 error."""
|
|
120
|
+
|
|
121
|
+
code: int
|
|
122
|
+
message: str
|
|
123
|
+
data: Optional[Any] = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# =============================================================================
|
|
127
|
+
# Task Store Abstraction
|
|
128
|
+
# =============================================================================
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TaskStore(ABC):
|
|
132
|
+
"""Abstract base class for task storage."""
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
async def create_task(
|
|
136
|
+
self,
|
|
137
|
+
task_id: Optional[str] = None,
|
|
138
|
+
title: Optional[str] = None,
|
|
139
|
+
prompt: Optional[str] = None,
|
|
140
|
+
) -> A2ATask:
|
|
141
|
+
"""Create a new task."""
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
async def get_task(self, task_id: str) -> Optional[A2ATask]:
|
|
146
|
+
"""Get a task by ID."""
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
async def update_task(
|
|
151
|
+
self,
|
|
152
|
+
task_id: str,
|
|
153
|
+
status: Optional[Dict[str, Any]] = None,
|
|
154
|
+
artifacts: Optional[List[Dict[str, Any]]] = None,
|
|
155
|
+
) -> Optional[A2ATask]:
|
|
156
|
+
"""Update a task."""
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
@abstractmethod
|
|
160
|
+
async def cancel_task(self, task_id: str) -> Optional[A2ATask]:
|
|
161
|
+
"""Cancel a task."""
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class InMemoryTaskStore(TaskStore):
|
|
166
|
+
"""In-memory task store implementation."""
|
|
167
|
+
|
|
168
|
+
def __init__(self):
|
|
169
|
+
self._tasks: Dict[str, A2ATask] = {}
|
|
170
|
+
self._lock = asyncio.Lock()
|
|
171
|
+
|
|
172
|
+
async def create_task(
|
|
173
|
+
self,
|
|
174
|
+
task_id: Optional[str] = None,
|
|
175
|
+
title: Optional[str] = None,
|
|
176
|
+
prompt: Optional[str] = None,
|
|
177
|
+
) -> A2ATask:
|
|
178
|
+
"""Create a new task."""
|
|
179
|
+
if task_id is None:
|
|
180
|
+
task_id = str(uuid.uuid4())
|
|
181
|
+
|
|
182
|
+
task = A2ATask(
|
|
183
|
+
id=task_id,
|
|
184
|
+
status={'state': TaskState.PENDING.value},
|
|
185
|
+
artifacts=[],
|
|
186
|
+
history=[],
|
|
187
|
+
metadata={'title': title, 'prompt': prompt},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async with self._lock:
|
|
191
|
+
self._tasks[task_id] = task
|
|
192
|
+
|
|
193
|
+
return task
|
|
194
|
+
|
|
195
|
+
async def get_task(self, task_id: str) -> Optional[A2ATask]:
|
|
196
|
+
"""Get a task by ID."""
|
|
197
|
+
async with self._lock:
|
|
198
|
+
return self._tasks.get(task_id)
|
|
199
|
+
|
|
200
|
+
async def update_task(
|
|
201
|
+
self,
|
|
202
|
+
task_id: str,
|
|
203
|
+
status: Optional[Dict[str, Any]] = None,
|
|
204
|
+
artifacts: Optional[List[Dict[str, Any]]] = None,
|
|
205
|
+
) -> Optional[A2ATask]:
|
|
206
|
+
"""Update a task."""
|
|
207
|
+
async with self._lock:
|
|
208
|
+
task = self._tasks.get(task_id)
|
|
209
|
+
if not task:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
if status:
|
|
213
|
+
task.status = status
|
|
214
|
+
if artifacts is not None:
|
|
215
|
+
task.artifacts = artifacts
|
|
216
|
+
|
|
217
|
+
return task
|
|
218
|
+
|
|
219
|
+
async def cancel_task(self, task_id: str) -> Optional[A2ATask]:
|
|
220
|
+
"""Cancel a task."""
|
|
221
|
+
return await self.update_task(
|
|
222
|
+
task_id, status={'state': TaskState.CANCELLED.value}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class DatabaseTaskStore(TaskStore):
|
|
227
|
+
"""Database-backed task store using the existing database module."""
|
|
228
|
+
|
|
229
|
+
def __init__(self, database_url: str):
|
|
230
|
+
self._database_url = database_url
|
|
231
|
+
self._initialized = False
|
|
232
|
+
|
|
233
|
+
async def _ensure_initialized(self):
|
|
234
|
+
"""Ensure database connection is ready."""
|
|
235
|
+
if self._initialized:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
from . import database as db
|
|
239
|
+
|
|
240
|
+
await db.get_pool()
|
|
241
|
+
self._initialized = True
|
|
242
|
+
|
|
243
|
+
async def create_task(
|
|
244
|
+
self,
|
|
245
|
+
task_id: Optional[str] = None,
|
|
246
|
+
title: Optional[str] = None,
|
|
247
|
+
prompt: Optional[str] = None,
|
|
248
|
+
) -> A2ATask:
|
|
249
|
+
"""Create a new task in the database."""
|
|
250
|
+
await self._ensure_initialized()
|
|
251
|
+
from . import database as db
|
|
252
|
+
|
|
253
|
+
if task_id is None:
|
|
254
|
+
task_id = str(uuid.uuid4())
|
|
255
|
+
|
|
256
|
+
if title is None:
|
|
257
|
+
title = f'A2A Task {task_id[:8]}'
|
|
258
|
+
|
|
259
|
+
if prompt is None:
|
|
260
|
+
prompt = 'A2A Protocol message'
|
|
261
|
+
|
|
262
|
+
now = datetime.utcnow()
|
|
263
|
+
pool = await db.get_pool()
|
|
264
|
+
if pool:
|
|
265
|
+
async with pool.acquire() as conn:
|
|
266
|
+
await conn.execute(
|
|
267
|
+
"""
|
|
268
|
+
INSERT INTO tasks (id, title, prompt, status, created_at, updated_at)
|
|
269
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
270
|
+
ON CONFLICT (id) DO NOTHING
|
|
271
|
+
""",
|
|
272
|
+
task_id,
|
|
273
|
+
title,
|
|
274
|
+
prompt,
|
|
275
|
+
TaskState.PENDING.value,
|
|
276
|
+
now,
|
|
277
|
+
now,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return A2ATask(
|
|
281
|
+
id=task_id,
|
|
282
|
+
status={'state': TaskState.PENDING.value},
|
|
283
|
+
artifacts=[],
|
|
284
|
+
history=[],
|
|
285
|
+
metadata={'title': title},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
async def get_task(self, task_id: str) -> Optional[A2ATask]:
|
|
289
|
+
"""Get a task from the database."""
|
|
290
|
+
await self._ensure_initialized()
|
|
291
|
+
from . import database as db
|
|
292
|
+
|
|
293
|
+
pool = await db.get_pool()
|
|
294
|
+
if not pool:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
async with pool.acquire() as conn:
|
|
298
|
+
row = await conn.fetchrow(
|
|
299
|
+
'SELECT * FROM tasks WHERE id = $1', task_id
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if not row:
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
return A2ATask(
|
|
306
|
+
id=row['id'],
|
|
307
|
+
status={'state': row.get('status', TaskState.PENDING.value)},
|
|
308
|
+
artifacts=[],
|
|
309
|
+
history=[],
|
|
310
|
+
metadata={
|
|
311
|
+
'title': row.get('title'),
|
|
312
|
+
'description': row.get('description'),
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
async def update_task(
|
|
317
|
+
self,
|
|
318
|
+
task_id: str,
|
|
319
|
+
status: Optional[Dict[str, Any]] = None,
|
|
320
|
+
artifacts: Optional[List[Dict[str, Any]]] = None,
|
|
321
|
+
) -> Optional[A2ATask]:
|
|
322
|
+
"""Update a task in the database."""
|
|
323
|
+
await self._ensure_initialized()
|
|
324
|
+
from . import database as db
|
|
325
|
+
|
|
326
|
+
pool = await db.get_pool()
|
|
327
|
+
if not pool:
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
now = datetime.utcnow()
|
|
331
|
+
async with pool.acquire() as conn:
|
|
332
|
+
if status:
|
|
333
|
+
await conn.execute(
|
|
334
|
+
"""
|
|
335
|
+
UPDATE tasks SET status = $1, updated_at = $2 WHERE id = $3
|
|
336
|
+
""",
|
|
337
|
+
status.get('state', TaskState.WORKING.value),
|
|
338
|
+
now,
|
|
339
|
+
task_id,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return await self.get_task(task_id)
|
|
343
|
+
|
|
344
|
+
async def cancel_task(self, task_id: str) -> Optional[A2ATask]:
|
|
345
|
+
"""Cancel a task in the database."""
|
|
346
|
+
return await self.update_task(
|
|
347
|
+
task_id, status={'state': TaskState.CANCELLED.value}
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# =============================================================================
|
|
352
|
+
# Agent Executor Abstraction
|
|
353
|
+
# =============================================================================
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class RequestContext:
|
|
357
|
+
"""Context for an A2A request."""
|
|
358
|
+
|
|
359
|
+
def __init__(
|
|
360
|
+
self,
|
|
361
|
+
task_id: Optional[str] = None,
|
|
362
|
+
message: Optional[A2AMessage] = None,
|
|
363
|
+
configuration: Optional[Dict[str, Any]] = None,
|
|
364
|
+
user: Optional[UserSession] = None,
|
|
365
|
+
):
|
|
366
|
+
self.task_id = task_id
|
|
367
|
+
self.message = message
|
|
368
|
+
self.configuration = configuration or {}
|
|
369
|
+
self.user = user
|
|
370
|
+
self.metadata: Dict[str, Any] = {}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class EventQueue:
|
|
374
|
+
"""Queue for streaming events back to the client."""
|
|
375
|
+
|
|
376
|
+
def __init__(self):
|
|
377
|
+
self._queue: asyncio.Queue = asyncio.Queue()
|
|
378
|
+
self._closed = False
|
|
379
|
+
|
|
380
|
+
async def put(self, event: Dict[str, Any]):
|
|
381
|
+
"""Put an event on the queue."""
|
|
382
|
+
if not self._closed:
|
|
383
|
+
await self._queue.put(event)
|
|
384
|
+
|
|
385
|
+
async def get(self) -> Optional[Dict[str, Any]]:
|
|
386
|
+
"""Get an event from the queue."""
|
|
387
|
+
if self._closed:
|
|
388
|
+
return None
|
|
389
|
+
try:
|
|
390
|
+
return await asyncio.wait_for(self._queue.get(), timeout=30.0)
|
|
391
|
+
except asyncio.TimeoutError:
|
|
392
|
+
return {'type': 'keepalive'}
|
|
393
|
+
|
|
394
|
+
def close(self):
|
|
395
|
+
"""Close the queue."""
|
|
396
|
+
self._closed = True
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def is_closed(self) -> bool:
|
|
400
|
+
return self._closed
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class AgentExecutor(ABC):
|
|
404
|
+
"""
|
|
405
|
+
Abstract base class for agent execution logic.
|
|
406
|
+
|
|
407
|
+
Implement this interface to bridge your agent's logic with the A2A protocol.
|
|
408
|
+
The executor receives context about the request and uses an event queue to
|
|
409
|
+
communicate results or updates back.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
@abstractmethod
|
|
413
|
+
async def execute(
|
|
414
|
+
self,
|
|
415
|
+
context: RequestContext,
|
|
416
|
+
event_queue: EventQueue,
|
|
417
|
+
) -> None:
|
|
418
|
+
"""
|
|
419
|
+
Execute agent logic for a request.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
context: Request context containing the message and configuration
|
|
423
|
+
event_queue: Queue to push events (status updates, artifacts, etc.)
|
|
424
|
+
"""
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
async def cancel(self, task_id: str) -> bool:
|
|
428
|
+
"""
|
|
429
|
+
Cancel an in-progress task.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
task_id: The ID of the task to cancel
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
True if cancellation was successful, False otherwise
|
|
436
|
+
"""
|
|
437
|
+
return True
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class DefaultAgentExecutor(AgentExecutor):
|
|
441
|
+
"""Default agent executor that echoes messages back."""
|
|
442
|
+
|
|
443
|
+
async def execute(
|
|
444
|
+
self,
|
|
445
|
+
context: RequestContext,
|
|
446
|
+
event_queue: EventQueue,
|
|
447
|
+
) -> None:
|
|
448
|
+
"""Execute default echo behavior."""
|
|
449
|
+
# Send working status
|
|
450
|
+
await event_queue.put(
|
|
451
|
+
{
|
|
452
|
+
'type': 'status',
|
|
453
|
+
'status': {'state': TaskState.WORKING.value},
|
|
454
|
+
}
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Echo the message content
|
|
458
|
+
response_text = 'Echo: '
|
|
459
|
+
if context.message:
|
|
460
|
+
for part in context.message.parts:
|
|
461
|
+
if part.get('type') == 'text':
|
|
462
|
+
response_text += part.get('text', '')
|
|
463
|
+
|
|
464
|
+
# Send artifact with response
|
|
465
|
+
await event_queue.put(
|
|
466
|
+
{
|
|
467
|
+
'type': 'artifact',
|
|
468
|
+
'artifact': {
|
|
469
|
+
'parts': [{'type': 'text', 'text': response_text}],
|
|
470
|
+
},
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Send completed status
|
|
475
|
+
await event_queue.put(
|
|
476
|
+
{
|
|
477
|
+
'type': 'status',
|
|
478
|
+
'status': {'state': TaskState.COMPLETED.value},
|
|
479
|
+
'final': True,
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# =============================================================================
|
|
485
|
+
# Request Handler
|
|
486
|
+
# =============================================================================
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class DefaultRequestHandler:
|
|
490
|
+
"""
|
|
491
|
+
Default request handler that bridges the A2A protocol with agent executors.
|
|
492
|
+
|
|
493
|
+
Handles JSON-RPC methods and delegates to the appropriate executor methods.
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
def __init__(
|
|
497
|
+
self,
|
|
498
|
+
executor: AgentExecutor,
|
|
499
|
+
task_store: TaskStore,
|
|
500
|
+
):
|
|
501
|
+
self.executor = executor
|
|
502
|
+
self.task_store = task_store
|
|
503
|
+
self._active_tasks: Dict[str, asyncio.Task] = {}
|
|
504
|
+
|
|
505
|
+
async def handle_send_message(
|
|
506
|
+
self,
|
|
507
|
+
params: Dict[str, Any],
|
|
508
|
+
user: Optional[UserSession] = None,
|
|
509
|
+
) -> Dict[str, Any]:
|
|
510
|
+
"""Handle message/send method."""
|
|
511
|
+
message_data = params.get('message', {})
|
|
512
|
+
message = A2AMessage(
|
|
513
|
+
role=message_data.get('role', 'user'),
|
|
514
|
+
parts=message_data.get('parts', []),
|
|
515
|
+
metadata=message_data.get('metadata'),
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Extract text from message parts for prompt
|
|
519
|
+
prompt_text = ''
|
|
520
|
+
for part in message.parts:
|
|
521
|
+
if isinstance(part, dict) and part.get('text'):
|
|
522
|
+
prompt_text += part.get('text', '')
|
|
523
|
+
|
|
524
|
+
# Create task with message as prompt
|
|
525
|
+
task = await self.task_store.create_task(
|
|
526
|
+
prompt=prompt_text or 'A2A message'
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Create context and event queue
|
|
530
|
+
context = RequestContext(
|
|
531
|
+
task_id=task.id,
|
|
532
|
+
message=message,
|
|
533
|
+
configuration=params.get('configuration'),
|
|
534
|
+
user=user,
|
|
535
|
+
)
|
|
536
|
+
event_queue = EventQueue()
|
|
537
|
+
|
|
538
|
+
# Execute in background and collect final result
|
|
539
|
+
final_result = {'task': task.model_dump()}
|
|
540
|
+
|
|
541
|
+
async def run_executor():
|
|
542
|
+
try:
|
|
543
|
+
await self.executor.execute(context, event_queue)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
logger.error(f'Executor error: {e}')
|
|
546
|
+
await event_queue.put(
|
|
547
|
+
{
|
|
548
|
+
'type': 'status',
|
|
549
|
+
'status': {
|
|
550
|
+
'state': TaskState.FAILED.value,
|
|
551
|
+
'message': str(e),
|
|
552
|
+
},
|
|
553
|
+
'final': True,
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
finally:
|
|
557
|
+
event_queue.close()
|
|
558
|
+
|
|
559
|
+
# Run executor
|
|
560
|
+
exec_task = asyncio.create_task(run_executor())
|
|
561
|
+
self._active_tasks[task.id] = exec_task
|
|
562
|
+
|
|
563
|
+
# Collect events until final
|
|
564
|
+
artifacts = []
|
|
565
|
+
final_status = {'state': TaskState.PENDING.value}
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
while not event_queue.is_closed:
|
|
569
|
+
event = await event_queue.get()
|
|
570
|
+
if event is None:
|
|
571
|
+
break
|
|
572
|
+
|
|
573
|
+
event_type = event.get('type')
|
|
574
|
+
if event_type == 'status':
|
|
575
|
+
final_status = event.get('status', final_status)
|
|
576
|
+
await self.task_store.update_task(
|
|
577
|
+
task.id, status=final_status
|
|
578
|
+
)
|
|
579
|
+
elif event_type == 'artifact':
|
|
580
|
+
artifacts.append(event.get('artifact', {}))
|
|
581
|
+
|
|
582
|
+
if event.get('final'):
|
|
583
|
+
break
|
|
584
|
+
finally:
|
|
585
|
+
self._active_tasks.pop(task.id, None)
|
|
586
|
+
|
|
587
|
+
# Update final task state
|
|
588
|
+
updated_task = await self.task_store.update_task(
|
|
589
|
+
task.id, status=final_status, artifacts=artifacts
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
'task': (updated_task or task).model_dump(),
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async def handle_stream_message(
|
|
597
|
+
self,
|
|
598
|
+
params: Dict[str, Any],
|
|
599
|
+
user: Optional[UserSession] = None,
|
|
600
|
+
) -> AsyncGenerator[str, None]:
|
|
601
|
+
"""Handle message/stream method with SSE."""
|
|
602
|
+
message_data = params.get('message', {})
|
|
603
|
+
message = A2AMessage(
|
|
604
|
+
role=message_data.get('role', 'user'),
|
|
605
|
+
parts=message_data.get('parts', []),
|
|
606
|
+
metadata=message_data.get('metadata'),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Create task
|
|
610
|
+
task = await self.task_store.create_task()
|
|
611
|
+
|
|
612
|
+
# Create context and event queue
|
|
613
|
+
context = RequestContext(
|
|
614
|
+
task_id=task.id,
|
|
615
|
+
message=message,
|
|
616
|
+
configuration=params.get('configuration'),
|
|
617
|
+
user=user,
|
|
618
|
+
)
|
|
619
|
+
event_queue = EventQueue()
|
|
620
|
+
|
|
621
|
+
# Execute in background
|
|
622
|
+
async def run_executor():
|
|
623
|
+
try:
|
|
624
|
+
await self.executor.execute(context, event_queue)
|
|
625
|
+
except Exception as e:
|
|
626
|
+
logger.error(f'Executor error: {e}')
|
|
627
|
+
await event_queue.put(
|
|
628
|
+
{
|
|
629
|
+
'type': 'status',
|
|
630
|
+
'status': {
|
|
631
|
+
'state': TaskState.FAILED.value,
|
|
632
|
+
'message': str(e),
|
|
633
|
+
},
|
|
634
|
+
'final': True,
|
|
635
|
+
}
|
|
636
|
+
)
|
|
637
|
+
finally:
|
|
638
|
+
event_queue.close()
|
|
639
|
+
|
|
640
|
+
exec_task = asyncio.create_task(run_executor())
|
|
641
|
+
self._active_tasks[task.id] = exec_task
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
# Yield initial task event
|
|
645
|
+
yield f'data: {json.dumps({"type": "task", "task": task.model_dump()})}\n\n'
|
|
646
|
+
|
|
647
|
+
while not event_queue.is_closed:
|
|
648
|
+
event = await event_queue.get()
|
|
649
|
+
if event is None:
|
|
650
|
+
break
|
|
651
|
+
|
|
652
|
+
if event.get('type') == 'keepalive':
|
|
653
|
+
yield ':\n\n' # SSE comment for keepalive
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
yield f'data: {json.dumps(event)}\n\n'
|
|
657
|
+
|
|
658
|
+
if event.get('final'):
|
|
659
|
+
break
|
|
660
|
+
|
|
661
|
+
finally:
|
|
662
|
+
self._active_tasks.pop(task.id, None)
|
|
663
|
+
|
|
664
|
+
async def handle_get_task(
|
|
665
|
+
self,
|
|
666
|
+
params: Dict[str, Any],
|
|
667
|
+
user: Optional[UserSession] = None,
|
|
668
|
+
) -> Dict[str, Any]:
|
|
669
|
+
"""Handle tasks/get method."""
|
|
670
|
+
task_id = params.get('id')
|
|
671
|
+
if not task_id:
|
|
672
|
+
raise ValueError('Task ID is required')
|
|
673
|
+
|
|
674
|
+
task = await self.task_store.get_task(task_id)
|
|
675
|
+
if not task:
|
|
676
|
+
raise ValueError(f'Task not found: {task_id}')
|
|
677
|
+
|
|
678
|
+
return {'task': task.model_dump()}
|
|
679
|
+
|
|
680
|
+
async def handle_cancel_task(
|
|
681
|
+
self,
|
|
682
|
+
params: Dict[str, Any],
|
|
683
|
+
user: Optional[UserSession] = None,
|
|
684
|
+
) -> Dict[str, Any]:
|
|
685
|
+
"""Handle tasks/cancel method."""
|
|
686
|
+
task_id = params.get('id')
|
|
687
|
+
if not task_id:
|
|
688
|
+
raise ValueError('Task ID is required')
|
|
689
|
+
|
|
690
|
+
# Cancel the executor if running
|
|
691
|
+
exec_task = self._active_tasks.get(task_id)
|
|
692
|
+
if exec_task:
|
|
693
|
+
exec_task.cancel()
|
|
694
|
+
self._active_tasks.pop(task_id, None)
|
|
695
|
+
|
|
696
|
+
# Try to cancel via executor
|
|
697
|
+
await self.executor.cancel(task_id)
|
|
698
|
+
|
|
699
|
+
# Update task status
|
|
700
|
+
task = await self.task_store.cancel_task(task_id)
|
|
701
|
+
if not task:
|
|
702
|
+
raise ValueError(f'Task not found: {task_id}')
|
|
703
|
+
|
|
704
|
+
return {'task': task.model_dump()}
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# =============================================================================
|
|
708
|
+
# Router Factory
|
|
709
|
+
# =============================================================================
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def create_a2a_router(
|
|
713
|
+
executor: Optional[AgentExecutor] = None,
|
|
714
|
+
task_store: Optional[TaskStore] = None,
|
|
715
|
+
agent_card: Optional[AgentCard] = None,
|
|
716
|
+
database_url: Optional[str] = None,
|
|
717
|
+
require_authentication: bool = False,
|
|
718
|
+
auth_callback: Optional[Callable[[str], bool]] = None,
|
|
719
|
+
) -> APIRouter:
|
|
720
|
+
"""
|
|
721
|
+
Create an A2A protocol router with the provided executor.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
executor: AgentExecutor implementation for handling requests.
|
|
725
|
+
Defaults to DefaultAgentExecutor.
|
|
726
|
+
task_store: TaskStore implementation for persisting tasks.
|
|
727
|
+
Defaults to InMemoryTaskStore or DatabaseTaskStore if database_url is provided.
|
|
728
|
+
agent_card: AgentCard for discovery. Optional.
|
|
729
|
+
database_url: Database URL for DatabaseTaskStore. Optional.
|
|
730
|
+
require_authentication: Whether to require authentication for all endpoints.
|
|
731
|
+
auth_callback: Custom authentication callback (token) -> bool.
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
FastAPI APIRouter with A2A protocol endpoints.
|
|
735
|
+
"""
|
|
736
|
+
# Initialize executor
|
|
737
|
+
if executor is None:
|
|
738
|
+
executor = DefaultAgentExecutor()
|
|
739
|
+
|
|
740
|
+
# Initialize task store
|
|
741
|
+
if task_store is None:
|
|
742
|
+
if database_url:
|
|
743
|
+
task_store = DatabaseTaskStore(database_url)
|
|
744
|
+
else:
|
|
745
|
+
task_store = InMemoryTaskStore()
|
|
746
|
+
|
|
747
|
+
# Create request handler
|
|
748
|
+
handler = DefaultRequestHandler(executor, task_store)
|
|
749
|
+
|
|
750
|
+
# Create router
|
|
751
|
+
router = APIRouter(prefix='/a2a', tags=['a2a'])
|
|
752
|
+
|
|
753
|
+
# Authentication dependency
|
|
754
|
+
async def get_authenticated_user(
|
|
755
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
756
|
+
) -> Optional[UserSession]:
|
|
757
|
+
"""Get authenticated user if credentials provided."""
|
|
758
|
+
if not credentials:
|
|
759
|
+
if require_authentication:
|
|
760
|
+
raise HTTPException(
|
|
761
|
+
status_code=401, detail='Authentication required'
|
|
762
|
+
)
|
|
763
|
+
return None
|
|
764
|
+
|
|
765
|
+
token = credentials.credentials
|
|
766
|
+
|
|
767
|
+
# Try custom auth callback first
|
|
768
|
+
if auth_callback:
|
|
769
|
+
if not auth_callback(token):
|
|
770
|
+
raise HTTPException(status_code=401, detail='Invalid token')
|
|
771
|
+
return None # Custom auth doesn't provide user session
|
|
772
|
+
|
|
773
|
+
# Try Keycloak auth
|
|
774
|
+
try:
|
|
775
|
+
user = await get_current_user(credentials)
|
|
776
|
+
if user:
|
|
777
|
+
return user
|
|
778
|
+
except HTTPException:
|
|
779
|
+
if require_authentication:
|
|
780
|
+
raise
|
|
781
|
+
|
|
782
|
+
if require_authentication:
|
|
783
|
+
raise HTTPException(status_code=401, detail='Invalid token')
|
|
784
|
+
|
|
785
|
+
return None
|
|
786
|
+
|
|
787
|
+
# JSON-RPC endpoint
|
|
788
|
+
@router.post('/jsonrpc')
|
|
789
|
+
async def handle_jsonrpc(
|
|
790
|
+
request: Request,
|
|
791
|
+
user: Optional[UserSession] = Depends(get_authenticated_user),
|
|
792
|
+
) -> Response:
|
|
793
|
+
"""Handle JSON-RPC 2.0 requests."""
|
|
794
|
+
try:
|
|
795
|
+
body = await request.body()
|
|
796
|
+
try:
|
|
797
|
+
request_data = json.loads(body)
|
|
798
|
+
except json.JSONDecodeError:
|
|
799
|
+
return JSONResponse(
|
|
800
|
+
content=JSONRPCResponse(
|
|
801
|
+
id=None,
|
|
802
|
+
error={'code': -32700, 'message': 'Parse error'},
|
|
803
|
+
).model_dump(exclude_none=True),
|
|
804
|
+
status_code=400,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
# Validate JSON-RPC structure
|
|
808
|
+
try:
|
|
809
|
+
rpc_request = JSONRPCRequest.model_validate(request_data)
|
|
810
|
+
except Exception:
|
|
811
|
+
return JSONResponse(
|
|
812
|
+
content=JSONRPCResponse(
|
|
813
|
+
id=request_data.get('id'),
|
|
814
|
+
error={'code': -32600, 'message': 'Invalid Request'},
|
|
815
|
+
).model_dump(exclude_none=True),
|
|
816
|
+
status_code=400,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Route methods
|
|
820
|
+
method = rpc_request.method
|
|
821
|
+
params = rpc_request.params or {}
|
|
822
|
+
|
|
823
|
+
try:
|
|
824
|
+
if method == 'message/send':
|
|
825
|
+
result = await handler.handle_send_message(params, user)
|
|
826
|
+
elif method == 'message/stream':
|
|
827
|
+
# Return streaming response
|
|
828
|
+
return StreamingResponse(
|
|
829
|
+
handler.handle_stream_message(params, user),
|
|
830
|
+
media_type='text/event-stream',
|
|
831
|
+
headers={
|
|
832
|
+
'Cache-Control': 'no-cache',
|
|
833
|
+
'Connection': 'keep-alive',
|
|
834
|
+
'X-Accel-Buffering': 'no',
|
|
835
|
+
},
|
|
836
|
+
)
|
|
837
|
+
elif method == 'tasks/get':
|
|
838
|
+
result = await handler.handle_get_task(params, user)
|
|
839
|
+
elif method == 'tasks/cancel':
|
|
840
|
+
result = await handler.handle_cancel_task(params, user)
|
|
841
|
+
else:
|
|
842
|
+
return JSONResponse(
|
|
843
|
+
content=JSONRPCResponse(
|
|
844
|
+
id=rpc_request.id,
|
|
845
|
+
error={
|
|
846
|
+
'code': -32601,
|
|
847
|
+
'message': 'Method not found',
|
|
848
|
+
},
|
|
849
|
+
).model_dump(exclude_none=True),
|
|
850
|
+
status_code=400,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
return JSONResponse(
|
|
854
|
+
content=JSONRPCResponse(
|
|
855
|
+
id=rpc_request.id,
|
|
856
|
+
result=result,
|
|
857
|
+
).model_dump(exclude_none=True),
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
except ValueError as e:
|
|
861
|
+
return JSONResponse(
|
|
862
|
+
content=JSONRPCResponse(
|
|
863
|
+
id=rpc_request.id,
|
|
864
|
+
error={'code': -32602, 'message': str(e)},
|
|
865
|
+
).model_dump(exclude_none=True),
|
|
866
|
+
status_code=400,
|
|
867
|
+
)
|
|
868
|
+
except Exception as e:
|
|
869
|
+
logger.error(f'Error handling method {method}: {e}')
|
|
870
|
+
return JSONResponse(
|
|
871
|
+
content=JSONRPCResponse(
|
|
872
|
+
id=rpc_request.id,
|
|
873
|
+
error={
|
|
874
|
+
'code': -32603,
|
|
875
|
+
'message': f'Internal error: {str(e)}',
|
|
876
|
+
},
|
|
877
|
+
).model_dump(exclude_none=True),
|
|
878
|
+
status_code=500,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
except Exception as e:
|
|
882
|
+
logger.error(f'Error processing JSON-RPC request: {e}')
|
|
883
|
+
return JSONResponse(
|
|
884
|
+
content=JSONRPCResponse(
|
|
885
|
+
id=None,
|
|
886
|
+
error={'code': -32603, 'message': 'Internal error'},
|
|
887
|
+
).model_dump(exclude_none=True),
|
|
888
|
+
status_code=500,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# REST binding: Send message
|
|
892
|
+
@router.post('/rest/message:send')
|
|
893
|
+
async def rest_send_message(
|
|
894
|
+
request: Request,
|
|
895
|
+
user: Optional[UserSession] = Depends(get_authenticated_user),
|
|
896
|
+
) -> JSONResponse:
|
|
897
|
+
"""REST binding for sending messages."""
|
|
898
|
+
try:
|
|
899
|
+
body = await request.json()
|
|
900
|
+
result = await handler.handle_send_message(body, user)
|
|
901
|
+
return JSONResponse(content=result)
|
|
902
|
+
except ValueError as e:
|
|
903
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
904
|
+
except Exception as e:
|
|
905
|
+
logger.error(f'Error in REST send message: {e}')
|
|
906
|
+
raise HTTPException(status_code=500, detail='Internal server error')
|
|
907
|
+
|
|
908
|
+
# REST binding: Stream message
|
|
909
|
+
@router.post('/rest/message:stream')
|
|
910
|
+
async def rest_stream_message(
|
|
911
|
+
request: Request,
|
|
912
|
+
user: Optional[UserSession] = Depends(get_authenticated_user),
|
|
913
|
+
) -> StreamingResponse:
|
|
914
|
+
"""REST binding for streaming messages."""
|
|
915
|
+
try:
|
|
916
|
+
body = await request.json()
|
|
917
|
+
return StreamingResponse(
|
|
918
|
+
handler.handle_stream_message(body, user),
|
|
919
|
+
media_type='text/event-stream',
|
|
920
|
+
headers={
|
|
921
|
+
'Cache-Control': 'no-cache',
|
|
922
|
+
'Connection': 'keep-alive',
|
|
923
|
+
'X-Accel-Buffering': 'no',
|
|
924
|
+
},
|
|
925
|
+
)
|
|
926
|
+
except ValueError as e:
|
|
927
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
928
|
+
except Exception as e:
|
|
929
|
+
logger.error(f'Error in REST stream message: {e}')
|
|
930
|
+
raise HTTPException(status_code=500, detail='Internal server error')
|
|
931
|
+
|
|
932
|
+
# REST binding: Get task
|
|
933
|
+
@router.get('/rest/tasks/{task_id}')
|
|
934
|
+
async def rest_get_task(
|
|
935
|
+
task_id: str,
|
|
936
|
+
user: Optional[UserSession] = Depends(get_authenticated_user),
|
|
937
|
+
) -> JSONResponse:
|
|
938
|
+
"""REST binding for getting task status."""
|
|
939
|
+
try:
|
|
940
|
+
result = await handler.handle_get_task({'id': task_id}, user)
|
|
941
|
+
return JSONResponse(content=result)
|
|
942
|
+
except ValueError as e:
|
|
943
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
944
|
+
except Exception as e:
|
|
945
|
+
logger.error(f'Error in REST get task: {e}')
|
|
946
|
+
raise HTTPException(status_code=500, detail='Internal server error')
|
|
947
|
+
|
|
948
|
+
# REST binding: Cancel task
|
|
949
|
+
@router.post('/rest/tasks/{task_id}:cancel')
|
|
950
|
+
async def rest_cancel_task(
|
|
951
|
+
task_id: str,
|
|
952
|
+
user: Optional[UserSession] = Depends(get_authenticated_user),
|
|
953
|
+
) -> JSONResponse:
|
|
954
|
+
"""REST binding for cancelling a task."""
|
|
955
|
+
try:
|
|
956
|
+
result = await handler.handle_cancel_task({'id': task_id}, user)
|
|
957
|
+
return JSONResponse(content=result)
|
|
958
|
+
except ValueError as e:
|
|
959
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
960
|
+
except Exception as e:
|
|
961
|
+
logger.error(f'Error in REST cancel task: {e}')
|
|
962
|
+
raise HTTPException(status_code=500, detail='Internal server error')
|
|
963
|
+
|
|
964
|
+
# Agent card discovery endpoint
|
|
965
|
+
if agent_card:
|
|
966
|
+
|
|
967
|
+
@router.get('/.well-known/agent.json')
|
|
968
|
+
async def get_agent_card_endpoint() -> JSONResponse:
|
|
969
|
+
"""Get the agent card for discovery."""
|
|
970
|
+
return JSONResponse(content=agent_card.to_dict())
|
|
971
|
+
|
|
972
|
+
return router
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
# =============================================================================
|
|
976
|
+
# Convenience function for quick setup
|
|
977
|
+
# =============================================================================
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def setup_a2a_routes(
|
|
981
|
+
app,
|
|
982
|
+
executor: Optional[AgentExecutor] = None,
|
|
983
|
+
agent_card: Optional[AgentCard] = None,
|
|
984
|
+
database_url: Optional[str] = None,
|
|
985
|
+
require_authentication: bool = False,
|
|
986
|
+
) -> None:
|
|
987
|
+
"""
|
|
988
|
+
Convenience function to setup A2A routes on a FastAPI app.
|
|
989
|
+
|
|
990
|
+
Args:
|
|
991
|
+
app: FastAPI application instance
|
|
992
|
+
executor: AgentExecutor implementation
|
|
993
|
+
agent_card: AgentCard for discovery
|
|
994
|
+
database_url: Database URL for task persistence
|
|
995
|
+
require_authentication: Whether to require auth for all endpoints
|
|
996
|
+
"""
|
|
997
|
+
router = create_a2a_router(
|
|
998
|
+
executor=executor,
|
|
999
|
+
agent_card=agent_card,
|
|
1000
|
+
database_url=database_url,
|
|
1001
|
+
require_authentication=require_authentication,
|
|
1002
|
+
)
|
|
1003
|
+
app.include_router(router)
|
|
1004
|
+
logger.info('A2A protocol routes mounted at /a2a')
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
# =============================================================================
|
|
1008
|
+
# Export public API
|
|
1009
|
+
# =============================================================================
|
|
1010
|
+
|
|
1011
|
+
__all__ = [
|
|
1012
|
+
# Core types
|
|
1013
|
+
'TaskState',
|
|
1014
|
+
'A2AMessage',
|
|
1015
|
+
'A2ATask',
|
|
1016
|
+
'JSONRPCRequest',
|
|
1017
|
+
'JSONRPCResponse',
|
|
1018
|
+
'JSONRPCError',
|
|
1019
|
+
# Task stores
|
|
1020
|
+
'TaskStore',
|
|
1021
|
+
'InMemoryTaskStore',
|
|
1022
|
+
'DatabaseTaskStore',
|
|
1023
|
+
# Executor
|
|
1024
|
+
'AgentExecutor',
|
|
1025
|
+
'DefaultAgentExecutor',
|
|
1026
|
+
'RequestContext',
|
|
1027
|
+
'EventQueue',
|
|
1028
|
+
# Request handler
|
|
1029
|
+
'DefaultRequestHandler',
|
|
1030
|
+
# Router factory
|
|
1031
|
+
'create_a2a_router',
|
|
1032
|
+
'setup_a2a_routes',
|
|
1033
|
+
]
|