Skip to content

Commit 84d0102

Browse files
authored
Merge pull request #103 from yuvalk/logo
feat: add icon_url to agent card
2 parents f409610 + 2ebea5e commit 84d0102

File tree

3 files changed

+45
-4
lines changed

3 files changed

+45
-4
lines changed

src/lightspeed_agent/api/app.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
"""
1010

1111
import logging
12+
import pathlib
1213
from collections.abc import AsyncIterator
1314
from contextlib import asynccontextmanager
1415
from typing import Any
1516

1617
from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPI
18+
from fastapi import Request
1719
from fastapi.middleware.cors import CORSMiddleware
20+
from starlette.responses import FileResponse
1821

1922
from lightspeed_agent.api.a2a.a2a_setup import setup_a2a_routes
2023
from lightspeed_agent.api.a2a.agent_card import get_agent_card_dict
@@ -24,9 +27,18 @@
2427
from lightspeed_agent.ratelimit import RateLimitMiddleware, get_redis_rate_limiter
2528
from lightspeed_agent.security import RequestBodyLimitMiddleware, SecurityHeadersMiddleware
2629

30+
_LOGO_PATH = pathlib.Path(__file__).parent.parent / "static" / "logo.png"
31+
2732
logger = logging.getLogger(__name__)
2833

2934

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+
3042
@asynccontextmanager
3143
async def lifespan(app: A2AFastAPI) -> AsyncIterator[None]:
3244
"""Application lifespan manager for startup/shutdown events."""
@@ -134,19 +146,32 @@ def create_app() -> A2AFastAPI:
134146
lifespan=lifespan,
135147
)
136148

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+
137162
# Set up A2A protocol routes using ADK's built-in integration
138163
# This provides:
139-
# - GET /.well-known/agent.json - AgentCard
164+
# - GET /.well-known/agent.json - AgentCard (overridden above)
140165
# - POST / - JSON-RPC 2.0 endpoint for message/send, message/stream, etc.
141166
# The ADK integration handles SSE streaming, task management, and
142167
# event conversion automatically.
143168
setup_a2a_routes(app)
144169

145170
# Alias for agent card (some clients use agent-card.json)
146171
@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]:
148173
"""AgentCard endpoint (alias for agent.json)."""
149-
return get_agent_card_dict()
174+
return _agent_card_response(request)
150175

151176
# Add authentication middleware for A2A endpoint (innermost layer)
152177
# Validates Red Hat SSO JWT tokens on POST / requests
5.66 KB
Loading

tests/test_a2a.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def test_get_agent_card_dict(self):
136136
assert "protocolVersion" in card_dict # aliased field
137137
assert "securitySchemes" in card_dict # aliased field
138138
assert "defaultInputModes" in card_dict # aliased field
139+
assert "iconUrl" not in card_dict # injected dynamically at the endpoint level
139140

140141
def test_agent_card_all_fields(self):
141142
"""Validate structural correctness of every AgentCard field."""
@@ -329,14 +330,29 @@ def client(self):
329330
rl_mod._rate_limiter = None
330331

331332
def test_agent_card_endpoint(self, client):
332-
"""Test /.well-known/agent.json endpoint."""
333+
"""Test /.well-known/agent.json endpoint returns card with dynamic iconUrl."""
333334
response = client.get("/.well-known/agent.json")
334335

335336
assert response.status_code == 200
336337
data = response.json()
337338
assert "name" in data
338339
assert "skills" in data
339340
assert "securitySchemes" in data
341+
assert data["iconUrl"] == "http://testserver/static/logo.png"
342+
343+
def test_agent_card_alias_matches(self, client):
344+
"""Test /.well-known/agent-card.json returns the same card as agent.json."""
345+
main = client.get("/.well-known/agent.json").json()
346+
alias = client.get("/.well-known/agent-card.json").json()
347+
348+
assert alias["iconUrl"] == main["iconUrl"]
349+
350+
def test_logo_endpoint(self, client):
351+
"""Test GET /static/logo.png serves an image/png response."""
352+
response = client.get("/static/logo.png")
353+
354+
assert response.status_code == 200
355+
assert response.headers["content-type"] == "image/png"
340356

341357
def test_send_message_jsonrpc(self, client):
342358
"""Test / endpoint with JSON-RPC message/send."""

0 commit comments

Comments
 (0)