Skip to content

Commit 51cad9f

Browse files
authored
Merge branch 'main' into main
2 parents aaff26d + 2cc2a0d commit 51cad9f

File tree

17 files changed

+868
-330
lines changed

17 files changed

+868
-330
lines changed

.github/actions/spelling/excludes.txt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-excludes
2+
(?:^|/)(?i).gitignore\E$
3+
(?:^|/)(?i)CODE_OF_CONDUCT.md\E$
24
(?:^|/)(?i)COPYRIGHT
35
(?:^|/)(?i)LICEN[CS]E
4-
(?:^|/)(?i)CODE_OF_CONDUCT.md\E$
5-
(?:^|/)(?i).gitignore\E$
66
(?:^|/)3rdparty/
77
(?:^|/)go\.sum$
88
(?:^|/)package(?:-lock|)\.json$
@@ -33,6 +33,7 @@
3333
\.gif$
3434
\.git-blame-ignore-revs$
3535
\.gitattributes$
36+
\.gitignore\E$
3637
\.gitkeep$
3738
\.graffle$
3839
\.gz$
@@ -62,6 +63,7 @@
6263
\.pyc$
6364
\.pylintrc$
6465
\.qm$
66+
\.ruff.toml$
6567
\.s$
6668
\.sig$
6769
\.so$
@@ -71,6 +73,7 @@
7173
\.tgz$
7274
\.tiff?$
7375
\.ttf$
76+
\.vscode/
7477
\.wav$
7578
\.webm$
7679
\.webp$
@@ -82,8 +85,7 @@
8285
\.zip$
8386
^\.github/actions/spelling/
8487
^\.github/workflows/
85-
\.gitignore\E$
86-
\.vscode/
87-
noxfile.py
88-
\.ruff.toml$
88+
^\Qsrc/a2a/auth/__init__.py\E$
89+
^\Qsrc/a2a/server/request_handlers/context.py\E$
8990
CHANGELOG.md
91+
noxfile.py

.github/actions/spelling/expect.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
AUser
12
excinfo
23
GVsb
34
notif

examples/helloworld/__main__.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from a2a.server.apps import A2AStarletteApplication
66
from a2a.server.request_handlers import DefaultRequestHandler
77
from a2a.server.tasks import InMemoryTaskStore
8-
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
8+
from a2a.types import (
9+
AgentCapabilities,
10+
AgentCard,
11+
AgentSkill,
12+
)
913

1014

1115
if __name__ == '__main__':
@@ -17,25 +21,48 @@
1721
examples=['hi', 'hello world'],
1822
)
1923

20-
agent_card = AgentCard(
24+
extended_skill = AgentSkill(
25+
id='super_hello_world',
26+
name='Returns a SUPER Hello World',
27+
description='A more enthusiastic greeting, only for authenticated users.',
28+
tags=['hello world', 'super', 'extended'],
29+
examples=['super hi', 'give me a super hello'],
30+
)
31+
32+
# This will be the public-facing agent card
33+
public_agent_card = AgentCard(
2134
name='Hello World Agent',
2235
description='Just a hello world agent',
2336
url='http://localhost:9999/',
2437
version='1.0.0',
2538
defaultInputModes=['text'],
2639
defaultOutputModes=['text'],
2740
capabilities=AgentCapabilities(streaming=True),
28-
skills=[skill],
41+
skills=[skill], # Only the basic skill for the public card
42+
supportsAuthenticatedExtendedCard=True,
43+
)
44+
45+
# This will be the authenticated extended agent card
46+
# It includes the additional 'extended_skill'
47+
specific_extended_agent_card = public_agent_card.model_copy(
48+
update={
49+
'name': 'Hello World Agent - Extended Edition', # Different name for clarity
50+
'description': 'The full-featured hello world agent for authenticated users.',
51+
'version': '1.0.1', # Could even be a different version
52+
# Capabilities and other fields like url, defaultInputModes, defaultOutputModes,
53+
# supportsAuthenticatedExtendedCard are inherited from public_agent_card unless specified here.
54+
'skills': [skill, extended_skill], # Both skills for the extended card
55+
}
2956
)
3057

3158
request_handler = DefaultRequestHandler(
3259
agent_executor=HelloWorldAgentExecutor(),
3360
task_store=InMemoryTaskStore(),
3461
)
3562

36-
server = A2AStarletteApplication(
37-
agent_card=agent_card, http_handler=request_handler
38-
)
63+
server = A2AStarletteApplication(agent_card=public_agent_card,
64+
http_handler=request_handler,
65+
extended_agent_card=specific_extended_agent_card)
3966
import uvicorn
4067

4168
uvicorn.run(server.build(), host='0.0.0.0', port=9999)

examples/helloworld/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "helloworld"
33
version = "0.1.0"
44
description = "HelloWorld agent example that only returns Messages"
55
readme = "README.md"
6-
requires-python = ">=3.13"
6+
requires-python = ">=3.12"
77
dependencies = [
88
"a2a-sdk",
99
"click>=8.1.8",

examples/helloworld/test_client.py

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,70 @@
1-
from a2a.client import A2AClient
1+
import logging # Import the logging module
22
from typing import Any
3-
import httpx
43
from uuid import uuid4
5-
from a2a.types import (
6-
SendMessageRequest,
7-
MessageSendParams,
8-
SendStreamingMessageRequest,
9-
)
4+
5+
import httpx
6+
7+
from a2a.client import A2ACardResolver, A2AClient
8+
from a2a.types import (AgentCard, MessageSendParams, SendMessageRequest,
9+
SendStreamingMessageRequest)
1010

1111

1212
async def main() -> None:
13+
PUBLIC_AGENT_CARD_PATH = "/.well-known/agent.json"
14+
EXTENDED_AGENT_CARD_PATH = "/agent/authenticatedExtendedCard"
15+
16+
# Configure logging to show INFO level messages
17+
logging.basicConfig(level=logging.INFO)
18+
logger = logging.getLogger(__name__) # Get a logger instance
19+
20+
base_url = 'http://localhost:9999'
21+
1322
async with httpx.AsyncClient() as httpx_client:
14-
client = await A2AClient.get_client_from_agent_card_url(
15-
httpx_client, 'http://localhost:9999'
23+
# Initialize A2ACardResolver
24+
resolver = A2ACardResolver(
25+
httpx_client=httpx_client,
26+
base_url=base_url,
27+
# agent_card_path uses default, extended_agent_card_path also uses default
28+
)
29+
30+
# Fetch Public Agent Card and Initialize Client
31+
final_agent_card_to_use: AgentCard | None = None
32+
33+
try:
34+
logger.info(f"Attempting to fetch public agent card from: {base_url}{PUBLIC_AGENT_CARD_PATH}")
35+
_public_card = await resolver.get_agent_card() # Fetches from default public path
36+
logger.info("Successfully fetched public agent card:")
37+
logger.info(_public_card.model_dump_json(indent=2, exclude_none=True))
38+
final_agent_card_to_use = _public_card
39+
logger.info("\nUsing PUBLIC agent card for client initialization (default).")
40+
41+
if _public_card.supportsAuthenticatedExtendedCard:
42+
try:
43+
logger.info(f"\nPublic card supports authenticated extended card. Attempting to fetch from: {base_url}{EXTENDED_AGENT_CARD_PATH}")
44+
auth_headers_dict = {"Authorization": "Bearer dummy-token-for-extended-card"}
45+
_extended_card = await resolver.get_agent_card(
46+
relative_card_path=EXTENDED_AGENT_CARD_PATH,
47+
http_kwargs={"headers": auth_headers_dict}
48+
)
49+
logger.info("Successfully fetched authenticated extended agent card:")
50+
logger.info(_extended_card.model_dump_json(indent=2, exclude_none=True))
51+
final_agent_card_to_use = _extended_card # Update to use the extended card
52+
logger.info("\nUsing AUTHENTICATED EXTENDED agent card for client initialization.")
53+
except Exception as e_extended:
54+
logger.warning(f"Failed to fetch extended agent card: {e_extended}. Will proceed with public card.", exc_info=True)
55+
elif _public_card: # supportsAuthenticatedExtendedCard is False or None
56+
logger.info("\nPublic card does not indicate support for an extended card. Using public card.")
57+
58+
except Exception as e:
59+
logger.error(f"Critical error fetching public agent card: {e}", exc_info=True)
60+
raise RuntimeError("Failed to fetch the public agent card. Cannot continue.") from e
61+
62+
63+
client = A2AClient(
64+
httpx_client=httpx_client, agent_card=final_agent_card_to_use
1665
)
66+
logger.info("A2AClient initialized.")
67+
1768
send_message_payload: dict[str, Any] = {
1869
'message': {
1970
'role': 'user',
@@ -29,14 +80,19 @@ async def main() -> None:
2980

3081
response = await client.send_message(request)
3182
print(response.model_dump(mode='json', exclude_none=True))
83+
# The HelloWorld agent doesn't support streaming requests. Validate
84+
# this throws an error.
85+
try:
86+
streaming_request = SendStreamingMessageRequest(
87+
params=MessageSendParams(**send_message_payload)
88+
)
3289

33-
streaming_request = SendStreamingMessageRequest(
34-
params=MessageSendParams(**send_message_payload)
35-
)
36-
37-
stream_response = client.send_message_streaming(streaming_request)
38-
async for chunk in stream_response:
39-
print(chunk.model_dump(mode='json', exclude_none=True))
90+
stream_response = client.send_message_streaming(streaming_request)
91+
print("Got an unexpected response:")
92+
async for chunk in stream_response:
93+
print(chunk.model_dump(mode='json', exclude_none=True))
94+
except Exception:
95+
pass
4096

4197

4298
if __name__ == '__main__':

examples/langgraph/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "langgraph-example"
33
version = "0.1.0"
44
description = "Currency conversion agent example"
55
readme = "README.md"
6-
requires-python = ">=3.13"
6+
requires-python = ">=3.12"
77
dependencies = [
88
"a2a-sdk",
99
"click>=8.1.8",

src/a2a/auth/__init__.py

Whitespace-only changes.

src/a2a/auth/user.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Authenticated user information."""
2+
3+
from abc import ABC, abstractmethod
4+
5+
6+
class User(ABC):
7+
"""A representation of an authenticated user."""
8+
9+
@property
10+
@abstractmethod
11+
def is_authenticated(self) -> bool:
12+
"""Returns whether the current user is authenticated."""
13+
14+
@property
15+
@abstractmethod
16+
def user_name(self) -> str:
17+
"""Returns the user name of the current user."""
18+
19+
20+
class UnauthenticatedUser(User):
21+
"""A representation that no user has been authenticated in the request."""
22+
23+
@property
24+
def is_authenticated(self):
25+
return False
26+
27+
@property
28+
def user_name(self) -> str:
29+
return ''

0 commit comments

Comments
 (0)