feat(a2a): add agent card and rpc skeleton
parent
d1238f186c
commit
9603483648
@ -0,0 +1,3 @@
|
||||
from app.a2a.router import router as a2a_router
|
||||
|
||||
__all__ = ["a2a_router"]
|
||||
@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
def build_agent_card(settings: Settings, request: Request) -> dict[str, Any]:
|
||||
base_url = _resolve_base_url(settings=settings, request=request)
|
||||
return {
|
||||
"name": settings.a2a_agent_name,
|
||||
"description": settings.a2a_agent_description,
|
||||
"url": f"{base_url}/a2a/rpc",
|
||||
"version": "0.1.0",
|
||||
"protocolVersion": "1.0",
|
||||
"defaultInputModes": ["application/json"],
|
||||
"defaultOutputModes": ["application/json"],
|
||||
"capabilities": {
|
||||
"streaming": False,
|
||||
"pushNotifications": False,
|
||||
"stateTransitionHistory": False,
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"description": "Use the same Bearer/API key auth as the REST API.",
|
||||
}
|
||||
},
|
||||
"security": [{"bearerAuth": []}],
|
||||
"skills": [
|
||||
{
|
||||
"id": "availability.query",
|
||||
"name": "Check Availability",
|
||||
"description": "Checks Google Calendar availability for a given time range.",
|
||||
"tags": ["calendar", "availability", "scheduling"],
|
||||
"examples": [
|
||||
"Is calendar primary free from 2026-03-10T09:00:00+01:00 to 2026-03-10T10:00:00+01:00?"
|
||||
],
|
||||
"inputModes": ["application/json"],
|
||||
"outputModes": ["application/json"],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _resolve_base_url(*, settings: Settings, request: Request) -> str:
|
||||
if settings.a2a_public_base_url:
|
||||
return settings.a2a_public_base_url.rstrip("/")
|
||||
return str(request.base_url).rstrip("/")
|
||||
@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class A2ARpcRequest(BaseModel):
|
||||
jsonrpc: str = "2.0"
|
||||
id: str | int | None = None
|
||||
method: str
|
||||
params: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class A2ARpcError(BaseModel):
|
||||
code: int
|
||||
message: str
|
||||
data: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2ARpcResponse(BaseModel):
|
||||
jsonrpc: str = "2.0"
|
||||
id: str | int | None = None
|
||||
result: dict[str, Any] | None = None
|
||||
error: A2ARpcError | None = None
|
||||
@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
|
||||
from app.a2a.agent_card import build_agent_card
|
||||
from app.a2a.models import A2ARpcError, A2ARpcRequest, A2ARpcResponse
|
||||
from app.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
router = APIRouter(tags=["a2a"])
|
||||
|
||||
|
||||
@router.get("/.well-known/agent-card.json")
|
||||
def get_agent_card(request: Request, response: Response) -> dict[str, Any]:
|
||||
response.headers["A2A-Version"] = "1.0"
|
||||
return build_agent_card(settings=settings, request=request)
|
||||
|
||||
|
||||
@router.post("/a2a/rpc", response_model=A2ARpcResponse)
|
||||
def a2a_rpc(payload: A2ARpcRequest, response: Response) -> A2ARpcResponse:
|
||||
response.headers["A2A-Version"] = "1.0"
|
||||
if payload.jsonrpc != "2.0":
|
||||
return _error_response(
|
||||
request_id=payload.id,
|
||||
code=-32600,
|
||||
message="Invalid Request: jsonrpc must be '2.0'.",
|
||||
)
|
||||
|
||||
if payload.method in {"ping", "health.ping", "health/ping"}:
|
||||
return A2ARpcResponse(
|
||||
id=payload.id,
|
||||
result={"status": "ok", "agent": settings.a2a_agent_name},
|
||||
)
|
||||
|
||||
return _error_response(
|
||||
request_id=payload.id,
|
||||
code=-32601,
|
||||
message=f"Method '{payload.method}' is not implemented yet.",
|
||||
)
|
||||
|
||||
|
||||
def _error_response(request_id: str | int | None, code: int, message: str) -> A2ARpcResponse:
|
||||
return A2ARpcResponse(
|
||||
id=request_id,
|
||||
error=A2ARpcError(code=code, message=message),
|
||||
)
|
||||
Loading…
Reference in New Issue