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