fast_a2a_app 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_a2a_app-0.1.0/CHANGELOG.md +26 -0
- fast_a2a_app-0.1.0/PKG-INFO +511 -0
- fast_a2a_app-0.1.0/README.md +482 -0
- fast_a2a_app-0.1.0/examples/.env.example +27 -0
- fast_a2a_app-0.1.0/examples/echo_agent/agent.py +24 -0
- fast_a2a_app-0.1.0/examples/echo_agent/main.py +68 -0
- fast_a2a_app-0.1.0/examples/echo_agent/requirements.txt +9 -0
- fast_a2a_app-0.1.0/examples/holiday_planner/agent.py +283 -0
- fast_a2a_app-0.1.0/examples/holiday_planner/main.py +140 -0
- fast_a2a_app-0.1.0/examples/holiday_planner/requirements.txt +12 -0
- fast_a2a_app-0.1.0/examples/joke_agent/agent.py +101 -0
- fast_a2a_app-0.1.0/examples/joke_agent/main.py +82 -0
- fast_a2a_app-0.1.0/examples/joke_agent/requirements.txt +11 -0
- fast_a2a_app-0.1.0/pyproject.toml +59 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/__init__.py +61 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/py.typed +0 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/server/__init__.py +29 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/server/route.py +656 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/server/task_store.py +188 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/server/utils.py +31 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/ui/__init__.py +4 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/ui/index.html +1229 -0
- fast_a2a_app-0.1.0/src/fast_a2a_app/ui/route.py +18 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-05-05
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `fast_a2a_app.server` — A2A protocol adapter extracted and cleaned from ppt-agent
|
|
10
|
+
- `build_a2a_app()` factory assembling a Starlette ASGI app
|
|
11
|
+
- `build_agent_invoke()` / `build_agent_stream_invoke()` wrappers for pydantic-ai
|
|
12
|
+
- `report_progress()` for live tool status updates via ContextVar
|
|
13
|
+
- `RedisTaskStore` with context-id indexing and cross-instance cancel signals
|
|
14
|
+
- `ConfigurableAgentExecutor` with `on_task_start` / `on_task_cancel` hooks
|
|
15
|
+
- `ContextAwareRequestContextBuilder` for multi-turn history injection
|
|
16
|
+
- `debug` parameter for controlling error verbosity
|
|
17
|
+
- `redis_url` parameter replacing hard-coded config import
|
|
18
|
+
- `fast_a2a_app.ui` — Self-contained browser chat UI (no build step, no npm)
|
|
19
|
+
- Streaming SSE with real-time progress indicator
|
|
20
|
+
- localStorage persistence (context ID, transcript, active task)
|
|
21
|
+
- Page-reload recovery via `tasks/resubscribe` and `tasks/get` fallback
|
|
22
|
+
- Markdown rendering with DOMPurify sanitisation
|
|
23
|
+
- Collapsible agent card panel
|
|
24
|
+
- `examples/holiday_planner/` — Full example application
|
|
25
|
+
- Holiday planning agent with 4 tools: destinations, itinerary, budget, essentials
|
|
26
|
+
- FastAPI app wiring agent + fast_a2a_app server + fast_a2a_app UI
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fast_a2a_app
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: fast_a2a_app — Drop-in A2A server and chat UI for any AI agent
|
|
5
|
+
Project-URL: Homepage, https://github.com/boegeg/fast_a2a_app
|
|
6
|
+
Project-URL: Repository, https://github.com/boegeg/fast_a2a_app
|
|
7
|
+
Project-URL: Issues, https://github.com/boegeg/fast_a2a_app/issues
|
|
8
|
+
Author-email: Georg Boegerl <georg.boegerl@henkel.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: a2a,agent,ai,chat,fastapi,llm,server
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: a2a-sdk>=0.3.0
|
|
23
|
+
Requires-Dist: fastapi>=0.115.0
|
|
24
|
+
Requires-Dist: redis>=7.4.0
|
|
25
|
+
Requires-Dist: sse-starlette>=2.1.3
|
|
26
|
+
Requires-Dist: starlette>=0.41.0
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.34.0
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# fast_a2a_app
|
|
31
|
+
|
|
32
|
+
**Drop-in A2A server and chat UI for any FastAPI application that runs ai agents — installable from PyPI.**
|
|
33
|
+
|
|
34
|
+
fast_a2a_app packages the battle-tested A2A protocol adapter and self-contained browser chat UI into a standalone pip-installable library. Get a fully
|
|
35
|
+
spec-compliant A2A server plus a ready-to-use chat interface in under 20 lines.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
pip install fast_a2a_app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Why fast_a2a_app?
|
|
44
|
+
|
|
45
|
+
[Pydantic AI](https://ai.pydantic.dev/) ships its own `FastA2A` integration, which is excellent if you are already inside the Pydantic AI ecosystem. fast_a2a_app exists for a different set of needs:
|
|
46
|
+
|
|
47
|
+
- **Mount point, not a framework.** fast_a2a_app is a plain Starlette app you mount into an existing FastAPI application at any path prefix. Everything outside that prefix — authentication middleware, custom routes, dependency injection, observability — is yours to own and compose however you like.
|
|
48
|
+
- **Framework-agnostic.** The library has zero dependency on Pydantic AI. Wire in any agent: raw Anthropic/OpenAI API calls, LangChain, LlamaIndex, or plain Python — as long as it exposes an `async (str) -> str` function or an async generator.
|
|
49
|
+
- **Separation of concerns.** Your FastAPI application stays in charge of the HTTP layer (auth, rate limiting, CORS, health checks). fast_a2a_app only handles the A2A protocol inside its mounted prefix, keeping agent logic cleanly decoupled from transport concerns.
|
|
50
|
+
|
|
51
|
+
We hope this contributes to a composable AI agent architecture where protocol adapters, agent frameworks, and application infrastructure are independent choices.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## What's inside
|
|
56
|
+
|
|
57
|
+
| Module | What it does |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `fast_a2a_app.server` | A2A JSON-RPC server (streaming SSE, multi-turn history, cross-instance cancel) |
|
|
60
|
+
| `fast_a2a_app.ui` | Self-contained browser chat UI — no build step, no npm |
|
|
61
|
+
|
|
62
|
+
### Protocol features (via `a2a-sdk`)
|
|
63
|
+
|
|
64
|
+
- `message/stream` — streaming SSE responses
|
|
65
|
+
- `tasks/cancel` — immediate or cross-replica cancellation
|
|
66
|
+
- `tasks/resubscribe` — reconnect to an in-flight stream after a network blip
|
|
67
|
+
- `tasks/get` — snapshot fallback for page-reload recovery
|
|
68
|
+
- `.well-known/agent-card.json` — agent discovery
|
|
69
|
+
|
|
70
|
+
### Server features
|
|
71
|
+
|
|
72
|
+
- **Multi-turn history** — every turn is stored in Redis and injected as a "Conversation so far:" prefix, giving the agent continuity without client-side replay
|
|
73
|
+
- **Cross-instance cancellation** — cancel signals flow through Redis so any replica can stop a task running on another replica
|
|
74
|
+
- **Live progress updates** — call `report_progress("step 2/5…")` from any tool and the chat UI spinner updates in real time
|
|
75
|
+
- **Lifecycle hooks** — `on_task_start` / `on_task_cancel` callbacks for metrics, locks, or state resets
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Storage
|
|
80
|
+
|
|
81
|
+
fast_a2a_app currently uses **Redis** for all server-side state:
|
|
82
|
+
|
|
83
|
+
| What is stored | Key pattern | TTL |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| Task JSON (full A2A task object) | `a2a:task:{id}` | 24 h |
|
|
86
|
+
| Conversation index (task_id → sequence) | `a2a:context:{cid}:tasks` | 24 h |
|
|
87
|
+
| Cross-instance cancel signal | `a2a:cancel:{id}` | 5 min |
|
|
88
|
+
|
|
89
|
+
Start a local Redis instance before running any example:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or point `REDIS_URL` at any managed Redis-compatible service (Redis Cloud, AWS ElastiCache, Azure Cache for Redis, etc.).
|
|
96
|
+
|
|
97
|
+
> **Roadmap** — pluggable storage backends (MongoDB, PostgreSQL) are planned.
|
|
98
|
+
> The `RedisTaskStore` already implements the `A2ATaskStore` Protocol, so a
|
|
99
|
+
> Mongo or Postgres backend can be swapped in by passing a custom
|
|
100
|
+
> `a2a_task_store` to `build_a2a_app()` without any library changes.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Framework-agnostic design
|
|
105
|
+
|
|
106
|
+
fast_a2a_app has **no dependency on any AI framework**. `build_a2a_app` accepts two
|
|
107
|
+
plain callables that you implement however you like:
|
|
108
|
+
|
|
109
|
+
| Callable | Signature |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `invoke` | `async (prompt: str) -> str` |
|
|
112
|
+
| `stream_invoke` | `async (prompt: str) -> AsyncIterable[str]` |
|
|
113
|
+
|
|
114
|
+
Wrap them with the two helpers and pass to `build_a2a_app`:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
invoke=build_invoke(my_async_fn)
|
|
118
|
+
stream_invoke=build_stream_invoke(my_async_generator_fn)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`build_stream_invoke` automatically sets up the `report_progress()` ContextVar,
|
|
122
|
+
so any code called during streaming can push live status updates to the chat UI —
|
|
123
|
+
regardless of which framework (or none) your agent uses.
|
|
124
|
+
|
|
125
|
+
You can also implement `invoke` and `stream_invoke` directly as bare callables
|
|
126
|
+
and pass them straight to `build_a2a_app` — no wrapper needed.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Quickstart
|
|
131
|
+
|
|
132
|
+
### 1. Install
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install fast_a2a_app
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 2. Implement your agent
|
|
139
|
+
|
|
140
|
+
Any `async (str) -> str` function works as the non-streaming invoke.
|
|
141
|
+
Any `async (str) -> AsyncIterable[str]` generator works for streaming.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# agent.py — Azure OpenAI chat completions, no framework needed
|
|
145
|
+
import os
|
|
146
|
+
from collections.abc import AsyncIterable
|
|
147
|
+
from openai import AsyncOpenAI
|
|
148
|
+
|
|
149
|
+
AZURE_AI_ENDPOINT = os.environ.get("AZURE_AI_ENDPOINT", "")
|
|
150
|
+
AZURE_AI_DEPLOYMENT = os.environ.get("AZURE_AI_DEPLOYMENT", "gpt-4o")
|
|
151
|
+
AZURE_AI_KEY = os.environ.get("AZURE_AI_KEY", "")
|
|
152
|
+
AZURE_AI_BASE_URL = os.environ.get(
|
|
153
|
+
"AZURE_AI_BASE_URL",
|
|
154
|
+
f"{AZURE_AI_ENDPOINT.rstrip('/')}/" if AZURE_AI_ENDPOINT else "",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if AZURE_AI_KEY:
|
|
158
|
+
api_key = AZURE_AI_KEY
|
|
159
|
+
else:
|
|
160
|
+
from azure.identity import AzureCliCredential, get_bearer_token_provider
|
|
161
|
+
api_key = get_bearer_token_provider(
|
|
162
|
+
AzureCliCredential(), "https://ai.azure.com/.default"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
client = AsyncOpenAI(base_url=AZURE_AI_BASE_URL, api_key=api_key)
|
|
166
|
+
|
|
167
|
+
async def invoke(prompt: str) -> str:
|
|
168
|
+
resp = await client.chat.completions.create(
|
|
169
|
+
model=AZURE_AI_DEPLOYMENT, max_tokens=1024,
|
|
170
|
+
messages=[{"role": "user", "content": prompt}],
|
|
171
|
+
)
|
|
172
|
+
return (resp.choices[0].message.content or "").strip()
|
|
173
|
+
|
|
174
|
+
async def stream_invoke(prompt: str) -> AsyncIterable[str]:
|
|
175
|
+
stream = await client.chat.completions.create(
|
|
176
|
+
model=AZURE_AI_DEPLOYMENT, max_tokens=1024,
|
|
177
|
+
messages=[{"role": "user", "content": prompt}],
|
|
178
|
+
stream=True,
|
|
179
|
+
)
|
|
180
|
+
async for chunk in stream:
|
|
181
|
+
text = chunk.choices[0].delta.content or ""
|
|
182
|
+
if text:
|
|
183
|
+
yield text
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 3. Wire up the server
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# main.py
|
|
190
|
+
from fastapi import FastAPI
|
|
191
|
+
from a2a.types import AgentCapabilities, AgentCard
|
|
192
|
+
from fast_a2a_app import a2a_ui, build_a2a_app, build_invoke, build_stream_invoke
|
|
193
|
+
from agent import invoke, stream_invoke
|
|
194
|
+
|
|
195
|
+
app = FastAPI()
|
|
196
|
+
|
|
197
|
+
agent_card = AgentCard(
|
|
198
|
+
name="My Agent",
|
|
199
|
+
description="Does cool things",
|
|
200
|
+
version="1.0.0",
|
|
201
|
+
url="http://localhost:8000/a2a/",
|
|
202
|
+
capabilities=AgentCapabilities(streaming=True),
|
|
203
|
+
default_input_modes=["text"],
|
|
204
|
+
default_output_modes=["text"],
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
app.mount("/a2a", build_a2a_app(
|
|
208
|
+
agent_card=agent_card,
|
|
209
|
+
invoke=build_invoke(invoke),
|
|
210
|
+
stream_invoke=build_stream_invoke(stream_invoke),
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
app.mount("/", a2a_ui) # built-in chat UI at http://localhost:8000/
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 4. Run
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Start Redis (required for conversation history)
|
|
220
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
221
|
+
|
|
222
|
+
# Run the app
|
|
223
|
+
uvicorn main:app --reload
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Open `http://localhost:8000/` — you're chatting.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## API reference
|
|
231
|
+
|
|
232
|
+
### `build_a2a_app(...)`
|
|
233
|
+
|
|
234
|
+
Assembles a Starlette ASGI app. Mount it at any path prefix.
|
|
235
|
+
|
|
236
|
+
| Parameter | Type | Default | Description |
|
|
237
|
+
|---|---|---|---|
|
|
238
|
+
| `agent_card` | `AgentCard` | required | Pre-built A2A agent card (name, description, version, url, skills, capabilities) |
|
|
239
|
+
| `invoke` | `Callable` | required | Non-streaming callable — use `build_invoke()` to wrap |
|
|
240
|
+
| `stream_invoke` | `Callable \| None` | `None` | Streaming callable — use `build_stream_invoke()` to wrap |
|
|
241
|
+
| `prompt_builder` | `Callable \| None` | history+cid prefix | Custom prompt assembly function |
|
|
242
|
+
| `on_task_start` | `Callable[[str], Awaitable] \| None` | `None` | Called before each task |
|
|
243
|
+
| `on_task_cancel` | `Callable[[str], Awaitable] \| None` | `None` | Called on cancel |
|
|
244
|
+
| `a2a_task_store` | `A2ATaskStore \| None` | auto | Custom task store |
|
|
245
|
+
| `redis_client` | `aioredis.Redis \| None` | auto | Custom Redis client |
|
|
246
|
+
| `redis_url` | `str` | `"redis://localhost:6379"` | Redis connection string |
|
|
247
|
+
| `debug` | `bool` | `False` | Include exception details in failure messages |
|
|
248
|
+
|
|
249
|
+
### `build_invoke(run)`
|
|
250
|
+
|
|
251
|
+
Wraps any `async (prompt: str) -> str` function as a non-streaming A2A invoke.
|
|
252
|
+
Works with any AI framework or plain API call.
|
|
253
|
+
|
|
254
|
+
### `build_stream_invoke(run)`
|
|
255
|
+
|
|
256
|
+
Wraps any `async (prompt: str) -> AsyncIterable[str]` generator as a streaming A2A invoke.
|
|
257
|
+
Also sets up the `report_progress()` ContextVar so live progress updates work
|
|
258
|
+
out of the box — call `report_progress("step 2/5…")` anywhere during execution
|
|
259
|
+
and it will appear as a working-status event in the chat UI.
|
|
260
|
+
|
|
261
|
+
### `report_progress(message)`
|
|
262
|
+
|
|
263
|
+
Call from any agent tool to push a status string to the chat UI spinner.
|
|
264
|
+
Has no effect outside a streaming context (safe to call unconditionally).
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
@agent.tool_plain
|
|
268
|
+
async def long_computation(n: int) -> str:
|
|
269
|
+
report_progress(f"Computing step 1/{n}…")
|
|
270
|
+
# …
|
|
271
|
+
report_progress(f"Computing step 2/{n}…")
|
|
272
|
+
return result
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### `a2a_ui`
|
|
276
|
+
|
|
277
|
+
A Starlette ASGI app serving a self-contained single-page chat interface.
|
|
278
|
+
No build step, no npm. Mount it at `"/"` to serve the UI.
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
app.mount("/", a2a_ui)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The UI reads the agent card from `/a2a/.well-known/agent-card.json` to populate
|
|
285
|
+
the header name and the collapsible info panel.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Example: Holiday Planner
|
|
290
|
+
|
|
291
|
+
`examples/holiday_planner/` is a complete example showing how to build a
|
|
292
|
+
domain-specific agent on fast_a2a_app.
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
examples/holiday_planner/
|
|
296
|
+
├── agent.py # pydantic-ai agent with 4 tools
|
|
297
|
+
├── main.py # FastAPI app + fast_a2a_app wiring
|
|
298
|
+
└── requirements.txt
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Running the example
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# One-time: create your .env from the shared template
|
|
305
|
+
cp examples/.env.example examples/.env
|
|
306
|
+
# edit examples/.env — set AZURE_AI_ENDPOINT and AZURE_AI_DEPLOYMENT
|
|
307
|
+
|
|
308
|
+
cd examples/holiday_planner
|
|
309
|
+
pip install -e ../../ # install fast_a2a_app from repo root
|
|
310
|
+
pip install -r requirements.txt
|
|
311
|
+
|
|
312
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
313
|
+
|
|
314
|
+
uvicorn main:app --reload
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Open `http://localhost:8000/` and ask:
|
|
318
|
+
|
|
319
|
+
> *"I want to plan a 10-day trip somewhere in Southeast Asia in September,
|
|
320
|
+
> moderate budget, interested in food, temples, and nature. Can you help?"*
|
|
321
|
+
|
|
322
|
+
The agent will ask follow-up questions, then use its tools to recommend
|
|
323
|
+
destinations, build a day-by-day itinerary, estimate costs, and provide
|
|
324
|
+
travel essentials — all with live progress updates in the UI.
|
|
325
|
+
|
|
326
|
+
### Holiday planner tools
|
|
327
|
+
|
|
328
|
+
| Tool | Description |
|
|
329
|
+
|---|---|
|
|
330
|
+
| `recommend_destinations` | 2-3 tailored destination suggestions with pros/cons |
|
|
331
|
+
| `create_itinerary` | Day-by-day plan with restaurants and local tips |
|
|
332
|
+
| `estimate_budget` | Cost breakdown table per person per day |
|
|
333
|
+
| `get_travel_essentials` | Visa, health, weather, and packing guide |
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Example: Echo Agent (no LLM, no external dependencies)
|
|
338
|
+
|
|
339
|
+
`examples/echo_agent/` is the minimal fast_a2a_app integration — pure Python, no API key, no AI framework.
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
examples/echo_agent/
|
|
343
|
+
├── agent.py # Two plain async functions, zero external imports
|
|
344
|
+
├── main.py # FastAPI app
|
|
345
|
+
└── requirements.txt # fast_a2a_app only
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
# agent.py
|
|
350
|
+
async def invoke(prompt: str) -> str:
|
|
351
|
+
return f"Echo: {prompt}"
|
|
352
|
+
|
|
353
|
+
async def stream_invoke(prompt: str) -> AsyncIterable[str]:
|
|
354
|
+
for i, word in enumerate(f"Echo: {prompt}".split()):
|
|
355
|
+
yield word if i == len(words) - 1 else word + " "
|
|
356
|
+
await asyncio.sleep(0.05) # makes streaming visible in the UI
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Running the echo agent
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
cd examples/echo_agent
|
|
363
|
+
pip install -e ../../
|
|
364
|
+
pip install -r requirements.txt
|
|
365
|
+
|
|
366
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
367
|
+
uvicorn main:app --reload
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
> No `.env` needed — the echo agent requires no API key. A `REDIS_URL` can
|
|
371
|
+
> be set via `examples/.env` if you need a non-default Redis address.
|
|
372
|
+
|
|
373
|
+
No API key needed. Open `http://localhost:8000/` and type anything.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Example: Joke Agent (raw chat completions, no agent framework)
|
|
378
|
+
|
|
379
|
+
`examples/joke_agent/` shows fast_a2a_app wired to plain Azure OpenAI chat completions — no agent framework at all.
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
examples/joke_agent/
|
|
383
|
+
├── agent.py # Two plain async functions: run_joke_agent + stream_joke_agent
|
|
384
|
+
├── main.py # FastAPI app using build_invoke / build_stream_invoke
|
|
385
|
+
└── requirements.txt
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
`agent.py` defines two callables that satisfy the fast_a2a_app contract:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
# Non-streaming: async (str) -> str
|
|
392
|
+
async def run_joke_agent(prompt: str) -> str:
|
|
393
|
+
response = await client.chat.completions.create(model=..., messages=[...])
|
|
394
|
+
return (response.choices[0].message.content or "").strip()
|
|
395
|
+
|
|
396
|
+
# Streaming: async (str) -> AsyncIterable[str]
|
|
397
|
+
async def stream_joke_agent(prompt: str) -> AsyncIterable[str]:
|
|
398
|
+
stream = await client.chat.completions.create(model=..., messages=[...], stream=True)
|
|
399
|
+
async for chunk in stream:
|
|
400
|
+
text = chunk.choices[0].delta.content or ""
|
|
401
|
+
if text:
|
|
402
|
+
yield text
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`main.py` wires them in with the helpers:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
|
|
409
|
+
from fast_a2a_app import build_a2a_app, build_invoke, build_stream_invoke, a2a_ui
|
|
410
|
+
from agent import run_joke_agent, stream_joke_agent
|
|
411
|
+
|
|
412
|
+
agent_card = AgentCard(
|
|
413
|
+
name="Joke Agent",
|
|
414
|
+
description="Your AI stand-up comedian.",
|
|
415
|
+
version="0.1.0",
|
|
416
|
+
url="http://localhost:8000/a2a/",
|
|
417
|
+
capabilities=AgentCapabilities(streaming=True),
|
|
418
|
+
default_input_modes=["text"],
|
|
419
|
+
default_output_modes=["text"],
|
|
420
|
+
skills=[AgentSkill(id="tell_joke", name="Tell a joke", description="Tells a joke on any topic.")],
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
app.mount("/a2a", build_a2a_app(
|
|
424
|
+
agent_card=agent_card,
|
|
425
|
+
invoke=build_invoke(run_joke_agent),
|
|
426
|
+
stream_invoke=build_stream_invoke(stream_joke_agent),
|
|
427
|
+
))
|
|
428
|
+
app.mount("/", a2a_ui)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Running the joke agent
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
# One-time: create your .env from the shared template
|
|
435
|
+
cp examples/.env.example examples/.env
|
|
436
|
+
# edit examples/.env — set AZURE_AI_ENDPOINT and AZURE_AI_DEPLOYMENT
|
|
437
|
+
|
|
438
|
+
cd examples/joke_agent
|
|
439
|
+
pip install -e ../../
|
|
440
|
+
pip install -r requirements.txt
|
|
441
|
+
|
|
442
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
443
|
+
uvicorn main:app --reload
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Open `http://localhost:8000/` and try:
|
|
447
|
+
|
|
448
|
+
> *"Tell me a programming joke"* or *"Give me your best dad joke"*
|
|
449
|
+
|
|
450
|
+
Tokens stream directly from the Azure OpenAI API to the browser as they arrive.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Architecture
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
FastAPI app
|
|
458
|
+
├── /a2a ← Starlette ASGI app (build_a2a_app)
|
|
459
|
+
│ ├── POST / message/stream, tasks/cancel, …
|
|
460
|
+
│ └── GET /.well-known/agent-card.json
|
|
461
|
+
└── / ← a2a_ui (Starlette, single HTML file)
|
|
462
|
+
|
|
463
|
+
Redis
|
|
464
|
+
├── a2a:task:{id} Task JSON (24 h TTL)
|
|
465
|
+
├── a2a:context:{cid}:tasks Context index (task_id → sequence)
|
|
466
|
+
├── a2a:context:{cid}:sequence Sequence counter
|
|
467
|
+
└── a2a:cancel:{id} Cancel signal (5 min TTL)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Conversation history injection
|
|
471
|
+
|
|
472
|
+
Each A2A task has a `context_id` shared across all turns of a conversation.
|
|
473
|
+
`ContextAwareRequestContextBuilder` fetches all prior tasks for the same
|
|
474
|
+
`context_id` from Redis, extracts the last 12 lines of dialogue, and
|
|
475
|
+
prepends them as `"Conversation so far:\n…"` to each new prompt.
|
|
476
|
+
|
|
477
|
+
The agent therefore sees recent history without the client needing to
|
|
478
|
+
replay it — multi-turn conversation just works.
|
|
479
|
+
|
|
480
|
+
### How streaming works
|
|
481
|
+
|
|
482
|
+
`build_stream_invoke` wraps your generator in an `asyncio.Queue`-based
|
|
483
|
+
relay. Before starting the generator it sets a `ContextVar` callback that
|
|
484
|
+
`report_progress()` reads. Strings from `report_progress()` are placed in
|
|
485
|
+
the queue with a sentinel prefix; `ConfigurableAgentExecutor` routes them
|
|
486
|
+
to non-final `working` SSE events. All other yielded strings become
|
|
487
|
+
`artifact-update` events — the streaming text the user sees.
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Publishing to PyPI
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
pip install hatch
|
|
495
|
+
hatch build
|
|
496
|
+
hatch publish
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Or with `twine`:
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
pip install build twine
|
|
503
|
+
python -m build
|
|
504
|
+
twine upload dist/*
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## License
|
|
510
|
+
|
|
511
|
+
MIT
|