|
9 | 9 | """ |
10 | 10 |
|
11 | 11 | import logging |
| 12 | +import pathlib |
12 | 13 | from collections.abc import AsyncIterator |
13 | 14 | from contextlib import asynccontextmanager |
14 | 15 | from typing import Any |
15 | 16 |
|
16 | 17 | from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPI |
| 18 | +from fastapi import Request |
17 | 19 | from fastapi.middleware.cors import CORSMiddleware |
| 20 | +from starlette.responses import FileResponse |
18 | 21 |
|
19 | 22 | from lightspeed_agent.api.a2a.a2a_setup import setup_a2a_routes |
20 | 23 | from lightspeed_agent.api.a2a.agent_card import get_agent_card_dict |
|
24 | 27 | from lightspeed_agent.ratelimit import RateLimitMiddleware, get_redis_rate_limiter |
25 | 28 | from lightspeed_agent.security import RequestBodyLimitMiddleware, SecurityHeadersMiddleware |
26 | 29 |
|
| 30 | +_LOGO_PATH = pathlib.Path(__file__).parent.parent / "static" / "logo.png" |
| 31 | + |
27 | 32 | logger = logging.getLogger(__name__) |
28 | 33 |
|
29 | 34 |
|
| 35 | +def _agent_card_response(request: Request) -> dict[str, Any]: |
| 36 | + """Build agent card dict with a dynamic iconUrl derived from the request base URL.""" |
| 37 | + card = get_agent_card_dict() |
| 38 | + icon_url = f"{str(request.base_url).rstrip('/')}/static/logo.png" |
| 39 | + return {**card, "iconUrl": icon_url} |
| 40 | + |
| 41 | + |
30 | 42 | @asynccontextmanager |
31 | 43 | async def lifespan(app: A2AFastAPI) -> AsyncIterator[None]: |
32 | 44 | """Application lifespan manager for startup/shutdown events.""" |
@@ -134,19 +146,32 @@ def create_app() -> A2AFastAPI: |
134 | 146 | lifespan=lifespan, |
135 | 147 | ) |
136 | 148 |
|
| 149 | + # Serve the Red Hat logo for the agent card iconUrl |
| 150 | + @app.get("/static/logo.png") |
| 151 | + async def serve_logo() -> FileResponse: |
| 152 | + """Serve the agent logo image.""" |
| 153 | + return FileResponse(_LOGO_PATH, media_type="image/png") |
| 154 | + |
| 155 | + # Custom agent card endpoint registered BEFORE setup_a2a_routes so |
| 156 | + # FastAPI's first-match routing picks it up instead of the SDK default. |
| 157 | + @app.get("/.well-known/agent.json") |
| 158 | + async def agent_card_with_icon(request: Request) -> dict[str, Any]: |
| 159 | + """AgentCard endpoint with dynamic iconUrl.""" |
| 160 | + return _agent_card_response(request) |
| 161 | + |
137 | 162 | # Set up A2A protocol routes using ADK's built-in integration |
138 | 163 | # This provides: |
139 | | - # - GET /.well-known/agent.json - AgentCard |
| 164 | + # - GET /.well-known/agent.json - AgentCard (overridden above) |
140 | 165 | # - POST / - JSON-RPC 2.0 endpoint for message/send, message/stream, etc. |
141 | 166 | # The ADK integration handles SSE streaming, task management, and |
142 | 167 | # event conversion automatically. |
143 | 168 | setup_a2a_routes(app) |
144 | 169 |
|
145 | 170 | # Alias for agent card (some clients use agent-card.json) |
146 | 171 | @app.get("/.well-known/agent-card.json") |
147 | | - async def agent_card_alias() -> dict[str, Any]: |
| 172 | + async def agent_card_alias(request: Request) -> dict[str, Any]: |
148 | 173 | """AgentCard endpoint (alias for agent.json).""" |
149 | | - return get_agent_card_dict() |
| 174 | + return _agent_card_response(request) |
150 | 175 |
|
151 | 176 | # Add authentication middleware for A2A endpoint (innermost layer) |
152 | 177 | # Validates Red Hat SSO JWT tokens on POST / requests |
|
0 commit comments