Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions fasta2a/applications.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations as _annotations

import html
import json
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from pathlib import Path
Expand All @@ -8,7 +10,7 @@
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import FileResponse, Response, StreamingResponse
from starlette.responses import Response, StreamingResponse
from starlette.routing import Route
from starlette.types import ExceptionHandler, Lifespan, Receive, Scope, Send

Expand Down Expand Up @@ -112,7 +114,11 @@ async def _agent_card_endpoint(self, request: Request) -> Response:
async def _docs_endpoint(self, request: Request) -> Response:
"""Serve the documentation interface."""
docs_path = Path(__file__).parent / 'static' / 'docs.html'
return FileResponse(docs_path, media_type='text/html')
root_path = request.scope.get('root_path', '').rstrip('/')
content = docs_path.read_text()
content = content.replace('__FASTA2A_API_ROOT_JSON__', json.dumps(root_path))
content = content.replace('__FASTA2A_API_ROOT__', html.escape(root_path, quote=True))
return Response(content=content, media_type='text/html')

async def _agent_run_endpoint(self, request: Request) -> Response:
"""This is the main endpoint for the A2A server.
Expand Down
13 changes: 9 additions & 4 deletions fasta2a/static/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ <h2>📋 Agent Information</h2>
<div class="loading">Loading agent information...</div>
</div>
<div class="agent-card-link">
<a href="/.well-known/agent-card.json" target="_blank" rel="noopener noreferrer">
<a href="__FASTA2A_API_ROOT__/.well-known/agent-card.json" target="_blank" rel="noopener noreferrer">
/.well-known/agent-card.json
</a>
</div>
Expand Down Expand Up @@ -526,11 +526,16 @@ <h2>💬 Chat Interface</h2>
let agentCard = null;
let currentTaskId = null;
let contextId = null;
const apiRoot = __FASTA2A_API_ROOT_JSON__;

function apiUrl(path) {
return `${apiRoot}${path}`;
}

// Load agent card information
async function loadAgentCard() {
try {
const response = await fetch('/.well-known/agent-card.json');
const response = await fetch(apiUrl('/.well-known/agent-card.json'));
if (!response.ok) throw new Error('Failed to load agent card');

agentCard = await response.json();
Expand Down Expand Up @@ -596,7 +601,7 @@ <h4>Skills:</h4>

try {
// Send message to A2A endpoint
const response = await fetch('/', {
const response = await fetch(apiUrl('/'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -660,7 +665,7 @@ <h4>Skills:</h4>
if (!currentTaskId) return;

try {
const response = await fetch('/', {
const response = await fetch(apiUrl('/'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
26 changes: 26 additions & 0 deletions tests/test_applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
from asgi_lifespan import LifespanManager
from inline_snapshot import snapshot
from starlette.applications import Starlette

from fasta2a.applications import FastA2A
from fasta2a.broker import InMemoryBroker
Expand Down Expand Up @@ -56,13 +57,38 @@ async def test_docs_endpoint_default(self):
async with create_test_client(app) as client:
response = await client.get('/docs')
assert response.status_code == 200
assert '__FASTA2A_API_ROOT__' not in response.text
assert 'const apiRoot = "";' in response.text

async def test_docs_endpoint_custom_url(self):
app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker(), docs_url='/custom-docs')
async with create_test_client(app) as client:
response = await client.get('/custom-docs')
assert response.status_code == 200

async def test_docs_endpoint_mounted_app_uses_root_path(self):
a2a_app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker())

@asynccontextmanager
async def lifespan(_app: Starlette):
async with a2a_app.router.lifespan_context(a2a_app):
yield

app = Starlette(lifespan=lifespan)
app.mount('/agent', a2a_app)

async with LifespanManager(app=app) as manager:
transport = httpx.ASGITransport(app=manager.app)
async with httpx.AsyncClient(transport=transport, base_url='http://testclient') as client:
response = await client.get('/agent/docs')
assert response.status_code == 200
assert '__FASTA2A_API_ROOT__' not in response.text
assert 'const apiRoot = "/agent";' in response.text
assert 'href="/agent/.well-known/agent-card.json"' in response.text

response = await client.get('/agent/.well-known/agent-card.json')
assert response.status_code == 200

async def test_docs_endpoint_disabled(self):
app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker(), docs_url=None)
async with create_test_client(app) as client:
Expand Down
Loading