Skip to content

Commit fbc577d

Browse files
authored
Merge branch '1.0-dev' into release-please--branches--1.0-dev
2 parents 7d9d4a7 + 7a429b8 commit fbc577d

8 files changed

Lines changed: 373 additions & 2311 deletions

File tree

.github/actions/spelling/excludes.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,4 @@ CHANGELOG.md
9393
^tests/
9494
.pre-commit-config.yaml
9595
(?:^|/)a2a\.json$
96-
96+
release-please-config.json

buf.gen.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,3 @@ plugins:
2929
# Generates *_pb2.pyi files.
3030
- remote: buf.build/protocolbuffers/pyi
3131
out: src/a2a/types
32-
# Generates a2a.swagger.json (OpenAPI v2)
33-
- remote: buf.build/grpc-ecosystem/openapiv2
34-
out: src/a2a/types
35-
opt: json_names_for_fields=true

release-please-config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"prerelease": true,
44
"last-release-sha": "5268218c1ad6671552b7cbad34703f3abbb4fcce",
55
"prerelease-type": "alpha",
6+
"draft": true,
67
"packages": {
78
".": {}
89
}

samples/__init__.py

Whitespace-only changes.

samples/cli.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import argparse
2+
import asyncio
3+
import os
4+
import signal
5+
import uuid
6+
7+
from typing import Any
8+
9+
import grpc
10+
import httpx
11+
12+
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
13+
from a2a.types import Message, Part, Role, SendMessageRequest, TaskState
14+
15+
16+
async def _handle_stream(
17+
stream: Any, current_task_id: str | None
18+
) -> str | None:
19+
async for event, task in stream:
20+
if not task:
21+
continue
22+
if not current_task_id:
23+
current_task_id = task.id
24+
25+
if event:
26+
if event.HasField('status_update'):
27+
state_name = TaskState.Name(event.status_update.status.state)
28+
print(f'TaskStatusUpdate [state={state_name}]:', end=' ')
29+
if event.status_update.status.HasField('message'):
30+
for part in event.status_update.status.message.parts:
31+
if part.text:
32+
print(part.text, end=' ')
33+
print()
34+
35+
if (
36+
event.status_update.status.state
37+
== TaskState.TASK_STATE_COMPLETED
38+
):
39+
current_task_id = None
40+
print('--- Task Completed ---')
41+
42+
elif event.HasField('artifact_update'):
43+
print(
44+
f'TaskArtifactUpdate [name={event.artifact_update.artifact.name}]:',
45+
end=' ',
46+
)
47+
for part in event.artifact_update.artifact.parts:
48+
if part.text:
49+
print(part.text, end=' ')
50+
print()
51+
52+
return current_task_id
53+
54+
55+
async def main() -> None:
56+
"""Run the A2A terminal client."""
57+
parser = argparse.ArgumentParser(description='A2A Terminal Client')
58+
parser.add_argument(
59+
'--url', default='http://127.0.0.1:41241', help='Agent base URL'
60+
)
61+
parser.add_argument(
62+
'--transport',
63+
default=None,
64+
help='Preferred transport (JSONRPC, HTTP+JSON, GRPC)',
65+
)
66+
args = parser.parse_args()
67+
68+
config = ClientConfig()
69+
if args.transport:
70+
config.supported_protocol_bindings = [args.transport]
71+
72+
print(
73+
f'Connecting to {args.url} (preferred transport: {args.transport or "Any"})'
74+
)
75+
76+
async with httpx.AsyncClient() as httpx_client:
77+
resolver = A2ACardResolver(httpx_client, args.url)
78+
card = await resolver.get_agent_card()
79+
print('\n✓ Agent Card Found:')
80+
print(f' Name: {card.name}')
81+
82+
client = await ClientFactory.connect(card, client_config=config)
83+
84+
actual_transport = getattr(client, '_transport', client)
85+
print(f' Picked Transport: {actual_transport.__class__.__name__}')
86+
87+
print('\nConnected! Send a message or type /quit to exit.')
88+
89+
current_task_id = None
90+
current_context_id = str(uuid.uuid4())
91+
92+
while True:
93+
try:
94+
loop = asyncio.get_running_loop()
95+
user_input = await loop.run_in_executor(None, input, 'You: ')
96+
except KeyboardInterrupt:
97+
break
98+
99+
if user_input.lower() in ('/quit', '/exit'):
100+
break
101+
if not user_input.strip():
102+
continue
103+
104+
message = Message(
105+
role=Role.ROLE_USER,
106+
message_id=str(uuid.uuid4()),
107+
parts=[Part(text=user_input)],
108+
task_id=current_task_id,
109+
context_id=current_context_id,
110+
)
111+
112+
request = SendMessageRequest(message=message)
113+
114+
try:
115+
stream = client.send_message(request)
116+
current_task_id = await _handle_stream(stream, current_task_id)
117+
except (httpx.RequestError, grpc.RpcError) as e:
118+
print(f'Error communicating with agent: {e}')
119+
120+
await client.close()
121+
122+
123+
if __name__ == '__main__':
124+
signal.signal(signal.SIGINT, lambda sig, frame: os._exit(0))
125+
asyncio.run(main())

samples/hello_world_agent.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import asyncio
2+
import contextlib
3+
import logging
4+
5+
import grpc
6+
import uvicorn
7+
8+
from fastapi import FastAPI
9+
10+
from a2a.compat.v0_3 import a2a_v0_3_pb2_grpc
11+
from a2a.compat.v0_3.grpc_handler import CompatGrpcHandler
12+
from a2a.server.agent_execution.agent_executor import AgentExecutor
13+
from a2a.server.agent_execution.context import RequestContext
14+
from a2a.server.apps import A2AFastAPIApplication, A2ARESTFastAPIApplication
15+
from a2a.server.events.event_queue import EventQueue
16+
from a2a.server.request_handlers import GrpcHandler
17+
from a2a.server.request_handlers.default_request_handler import (
18+
DefaultRequestHandler,
19+
)
20+
from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore
21+
from a2a.server.tasks.task_updater import TaskUpdater
22+
from a2a.types import (
23+
AgentCapabilities,
24+
AgentCard,
25+
AgentInterface,
26+
AgentProvider,
27+
AgentSkill,
28+
Part,
29+
a2a_pb2_grpc,
30+
)
31+
32+
33+
logger = logging.getLogger(__name__)
34+
35+
36+
class SampleAgentExecutor(AgentExecutor):
37+
"""Sample agent executor logic similar to the a2a-js sample."""
38+
39+
def __init__(self) -> None:
40+
self.running_tasks: set[str] = set()
41+
42+
async def cancel(
43+
self, context: RequestContext, event_queue: EventQueue
44+
) -> None:
45+
"""Cancels a task."""
46+
task_id = context.task_id
47+
if task_id in self.running_tasks:
48+
self.running_tasks.remove(task_id)
49+
50+
updater = TaskUpdater(
51+
event_queue=event_queue,
52+
task_id=task_id or '',
53+
context_id=context.context_id or '',
54+
)
55+
await updater.cancel()
56+
57+
async def execute(
58+
self, context: RequestContext, event_queue: EventQueue
59+
) -> None:
60+
"""Executes a task inline."""
61+
user_message = context.message
62+
task_id = context.task_id
63+
context_id = context.context_id
64+
65+
if not user_message or not task_id or not context_id:
66+
return
67+
68+
self.running_tasks.add(task_id)
69+
70+
logger.info(
71+
'[SampleAgentExecutor] Processing message %s for task %s (context: %s)',
72+
user_message.message_id,
73+
task_id,
74+
context_id,
75+
)
76+
77+
updater = TaskUpdater(
78+
event_queue=event_queue,
79+
task_id=task_id,
80+
context_id=context_id,
81+
)
82+
83+
working_message = updater.new_agent_message(
84+
parts=[Part(text='Processing your question...')]
85+
)
86+
await updater.start_work(message=working_message)
87+
88+
query = context.get_user_input()
89+
90+
agent_reply_text = self._parse_input(query)
91+
await asyncio.sleep(1)
92+
93+
if task_id not in self.running_tasks:
94+
return
95+
96+
await updater.add_artifact(
97+
parts=[Part(text=agent_reply_text)],
98+
name='response',
99+
last_chunk=True,
100+
)
101+
await updater.complete()
102+
103+
logger.info(
104+
'[SampleAgentExecutor] Task %s finished with state: completed',
105+
task_id,
106+
)
107+
108+
def _parse_input(self, query: str) -> str:
109+
if not query:
110+
return 'Hello! Please provide a message for me to respond to.'
111+
112+
ql = query.lower()
113+
if 'hello' in ql or 'hi' in ql:
114+
return 'Hello World! Nice to meet you!'
115+
if 'how are you' in ql:
116+
return (
117+
"I'm doing great! Thanks for asking. How can I help you today?"
118+
)
119+
if 'goodbye' in ql or 'bye' in ql:
120+
return 'Goodbye! Have a wonderful day!'
121+
return f"Hello World! You said: '{query}'. Thanks for your message!"
122+
123+
124+
async def serve(
125+
host: str = '127.0.0.1',
126+
port: int = 41241,
127+
grpc_port: int = 50051,
128+
compat_grpc_port: int = 50052,
129+
) -> None:
130+
"""Run the Sample Agent server with mounted JSON-RPC, HTTP+JSON and gRPC transports."""
131+
agent_card = AgentCard(
132+
name='Sample Agent',
133+
description='A sample agent to test the stream functionality.',
134+
provider=AgentProvider(
135+
organization='A2A Samples', url='https://example.com'
136+
),
137+
version='1.0.0',
138+
capabilities=AgentCapabilities(
139+
streaming=True, push_notifications=False
140+
),
141+
default_input_modes=['text'],
142+
default_output_modes=['text', 'task-status'],
143+
skills=[
144+
AgentSkill(
145+
id='sample_agent',
146+
name='Sample Agent',
147+
description='Say hi.',
148+
tags=['sample'],
149+
examples=['hi'],
150+
input_modes=['text'],
151+
output_modes=['text', 'task-status'],
152+
)
153+
],
154+
supported_interfaces=[
155+
AgentInterface(
156+
protocol_binding='GRPC',
157+
protocol_version='1.0',
158+
url=f'{host}:{grpc_port}',
159+
),
160+
AgentInterface(
161+
protocol_binding='GRPC',
162+
protocol_version='0.3',
163+
url=f'{host}:{compat_grpc_port}',
164+
),
165+
AgentInterface(
166+
protocol_binding='JSONRPC',
167+
protocol_version='1.0',
168+
url=f'http://{host}:{port}/a2a/jsonrpc/',
169+
),
170+
AgentInterface(
171+
protocol_binding='JSONRPC',
172+
protocol_version='0.3',
173+
url=f'http://{host}:{port}/a2a/jsonrpc/',
174+
),
175+
AgentInterface(
176+
protocol_binding='HTTP+JSON',
177+
protocol_version='1.0',
178+
url=f'http://{host}:{port}/a2a/rest/',
179+
),
180+
AgentInterface(
181+
protocol_binding='HTTP+JSON',
182+
protocol_version='0.3',
183+
url=f'http://{host}:{port}/a2a/rest/',
184+
),
185+
],
186+
)
187+
188+
task_store = InMemoryTaskStore()
189+
request_handler = DefaultRequestHandler(
190+
agent_executor=SampleAgentExecutor(), task_store=task_store
191+
)
192+
193+
rest_app_builder = A2ARESTFastAPIApplication(
194+
agent_card=agent_card,
195+
http_handler=request_handler,
196+
enable_v0_3_compat=True,
197+
)
198+
rest_app = rest_app_builder.build()
199+
200+
jsonrpc_app_builder = A2AFastAPIApplication(
201+
agent_card=agent_card,
202+
http_handler=request_handler,
203+
enable_v0_3_compat=True,
204+
)
205+
206+
app = FastAPI()
207+
jsonrpc_app_builder.add_routes_to_app(app, rpc_url='/a2a/jsonrpc/')
208+
app.mount('/a2a/rest', rest_app)
209+
210+
grpc_server = grpc.aio.server()
211+
grpc_server.add_insecure_port(f'{host}:{grpc_port}')
212+
servicer = GrpcHandler(agent_card, request_handler)
213+
a2a_pb2_grpc.add_A2AServiceServicer_to_server(servicer, grpc_server)
214+
215+
compat_grpc_server = grpc.aio.server()
216+
compat_grpc_server.add_insecure_port(f'{host}:{compat_grpc_port}')
217+
compat_servicer = CompatGrpcHandler(agent_card, request_handler)
218+
a2a_v0_3_pb2_grpc.add_A2AServiceServicer_to_server(
219+
compat_servicer, compat_grpc_server
220+
)
221+
222+
config = uvicorn.Config(app, host=host, port=port)
223+
uvicorn_server = uvicorn.Server(config)
224+
225+
logger.info('Starting Sample Agent servers:')
226+
logger.info(' - HTTP on http://%s:%s', host, port)
227+
logger.info(' - gRPC on %s:%s', host, grpc_port)
228+
logger.info(' - gRPC (v0.3 compat) on %s:%s', host, compat_grpc_port)
229+
logger.info(
230+
'Agent Card available at http://%s:%s/.well-known/agent-card.json',
231+
host,
232+
port,
233+
)
234+
235+
await asyncio.gather(
236+
grpc_server.start(),
237+
compat_grpc_server.start(),
238+
uvicorn_server.serve(),
239+
)
240+
241+
242+
if __name__ == '__main__':
243+
logging.basicConfig(level=logging.INFO)
244+
with contextlib.suppress(KeyboardInterrupt):
245+
asyncio.run(serve())

0 commit comments

Comments
 (0)