Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
40 changes: 34 additions & 6 deletions examples/helloworld/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
from a2a.types import (
AgentCapabilities,
AgentCard,
AgentSkill,
)


if __name__ == '__main__':
Expand All @@ -17,25 +21,49 @@
examples=['hi', 'hello world'],
)

agent_card = AgentCard(
extended_skill = AgentSkill(
id='super_hello_world',
name='Returns a SUPER Hello World',
description='A more enthusiastic greeting, only for authenticated users.',
tags=['hello world', 'super', 'extended'],
examples=['super hi', 'give me a super hello'],
)

# This will be the public-facing agent card
public_agent_card = AgentCard(
name='Hello World Agent',
description='Just a hello world agent',
url='http://localhost:9999/',
version='1.0.0',
defaultInputModes=['text'],
defaultOutputModes=['text'],
capabilities=AgentCapabilities(streaming=True),
skills=[skill],
skills=[skill], # Only the basic skill for the public card
supportsAuthenticatedExtendedCard=True,
)

# This will be the authenticated extended agent card
# It includes the additional 'extended_skill'
specific_extended_agent_card = AgentCard(
name='Hello World Agent - Extended Edition', # Different name for clarity
description='The full-featured hello world agent for authenticated users.',
url='http://localhost:9999/',
version='1.0.1', # Could even be a different version
defaultInputModes=['text'],
defaultOutputModes=['text'],
capabilities=AgentCapabilities(streaming=True), # Could have different capabilities
skills=[skill, extended_skill], # Both skills for the extended card
supportsAuthenticatedExtendedCard=True,
)

request_handler = DefaultRequestHandler(
agent_executor=HelloWorldAgentExecutor(),
task_store=InMemoryTaskStore(),
)

server = A2AStarletteApplication(
agent_card=agent_card, http_handler=request_handler
)
server = A2AStarletteApplication(agent_card=public_agent_card,
http_handler=request_handler,
extended_agent_card=specific_extended_agent_card)
import uvicorn

uvicorn.run(server.build(), host='0.0.0.0', port=9999)
10 changes: 8 additions & 2 deletions examples/helloworld/test_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from a2a.client import A2AClient
import logging # Import the logging module
from typing import Any
import httpx
from uuid import uuid4

import httpx

from a2a.client import A2AClient
from a2a.types import (
SendMessageRequest,
MessageSendParams,
Expand All @@ -10,6 +13,9 @@


async def main() -> None:
# Configure logging to show INFO level messages
logging.basicConfig(level=logging.INFO)

async with httpx.AsyncClient() as httpx_client:
client = await A2AClient.get_client_from_agent_card_url(
httpx_client, 'http://localhost:9999'
Expand Down
112 changes: 90 additions & 22 deletions src/a2a/client/client.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
import json

import logging
from collections.abc import AsyncGenerator
from typing import Any
from uuid import uuid4

import httpx

from httpx_sse import SSEError, aconnect_sse
from pydantic import ValidationError

from a2a.client.errors import A2AClientHTTPError, A2AClientJSONError
from a2a.types import (
AgentCard,
CancelTaskRequest,
CancelTaskResponse,
GetTaskPushNotificationConfigRequest,
GetTaskPushNotificationConfigResponse,
GetTaskRequest,
GetTaskResponse,
SendMessageRequest,
SendMessageResponse,
SendStreamingMessageRequest,
SendStreamingMessageResponse,
SetTaskPushNotificationConfigRequest,
SetTaskPushNotificationConfigResponse,
)
from a2a.types import (AgentCard, CancelTaskRequest, CancelTaskResponse,
GetTaskPushNotificationConfigRequest,
GetTaskPushNotificationConfigResponse, GetTaskRequest,
GetTaskResponse, SendMessageRequest,
SendMessageResponse, SendStreamingMessageRequest,
SendStreamingMessageResponse,
SetTaskPushNotificationConfigRequest,
SetTaskPushNotificationConfigResponse)
from a2a.utils.telemetry import SpanKind, trace_class

logger = logging.getLogger(__name__)

class A2ACardResolver:
"""Agent Card resolver."""
Expand All @@ -35,6 +29,7 @@ def __init__(
httpx_client: httpx.AsyncClient,
base_url: str,
agent_card_path: str = '/.well-known/agent.json',
extended_agent_card_path: str = '/agent/authenticatedExtendedCard',
):
"""Initializes the A2ACardResolver.

Expand All @@ -45,11 +40,14 @@ def __init__(
"""
self.base_url = base_url.rstrip('/')
self.agent_card_path = agent_card_path.lstrip('/')
self.extended_agent_card_path = extended_agent_card_path.lstrip('/')
self.httpx_client = httpx_client

async def get_agent_card(
self, http_kwargs: dict[str, Any] | None = None
) -> AgentCard:
# Fetch the initial public agent card
public_card_url = f'{self.base_url}/{self.agent_card_path}'
"""Fetches the agent card from the specified URL.

Args:
Expand All @@ -66,20 +64,90 @@ async def get_agent_card(
"""
try:
response = await self.httpx_client.get(
f'{self.base_url}/{self.agent_card_path}',
public_card_url,
**(http_kwargs or {}),
)
response.raise_for_status()
return AgentCard.model_validate(response.json())
public_agent_card_data = response.json()
logger.info(
'Successfully fetched public agent card data: %s',
public_agent_card_data,
) # Added for verbosity
# print(f"DEBUG: Fetched public agent card data:\n{json.dumps(public_agent_card_data, indent=2)}") # Added for direct output
agent_card = AgentCard.model_validate(public_agent_card_data)
except httpx.HTTPStatusError as e:
raise A2AClientHTTPError(e.response.status_code, str(e)) from e
raise A2AClientHTTPError(
e.response.status_code,
f'Failed to fetch public agent card from {public_card_url}: {e}',
) from e
except json.JSONDecodeError as e:
raise A2AClientJSONError(str(e)) from e
raise A2AClientJSONError(
f'Failed to parse JSON for public agent card from {public_card_url}: {e}'
) from e
except httpx.RequestError as e:
raise A2AClientHTTPError(
503, f'Network communication error: {e}'
503,
f'Network communication error fetching public agent card from {public_card_url}: {e}',
) from e

# Check for supportsAuthenticatedExtendedCard
if agent_card.supportsAuthenticatedExtendedCard:
# Construct URL for the extended card.
# The extended card URL is relative to the agent's base URL specified *in* the agent card.
if not agent_card.url:
logger.warning(
'Agent card (from %s) indicates support for an extended card '
"but does not specify its own base 'url' field. "
'Cannot fetch extended card. Proceeding with public card.',
public_card_url,
)
return agent_card

extended_card_base_url = agent_card.url.rstrip('/')
full_extended_card_url = (
f'{extended_card_base_url}/{self.extended_agent_card_path}'
)

logger.info(
'Attempting to fetch extended agent card from %s',
full_extended_card_url,
)
try:
# Make another GET request for the extended card
# Note: Authentication headers will be added here when auth is implemented.
extended_response = await self.httpx_client.get(
full_extended_card_url,
**(http_kwargs or {}), # Passing original http_kwargs
)
extended_response.raise_for_status()
extended_agent_card_data = extended_response.json()
logger.info(
'Successfully fetched extended agent card data: %s',
extended_agent_card_data,
) # Added for verbosity

# This new card data replaces the old one entirely
agent_card = AgentCard.model_validate(extended_agent_card_data)
logger.info(
'Successfully fetched and using extended agent card from %s',
full_extended_card_url,
)
except (
httpx.HTTPStatusError,
httpx.RequestError,
json.JSONDecodeError,
ValidationError,
) as e:
logger.warning(
'Failed to fetch or parse extended agent card from %s. Error: %s. '
'Proceeding with the initially fetched public agent card.',
full_extended_card_url,
e,
)
# Fallback to the already parsed public_agent_card (which is 'agent_card' at this point)

return agent_card


@trace_class(kind=SpanKind.CLIENT)
class A2AClient:
Expand Down
Loading