a2a-lite 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- a2a_lite/__init__.py +7 -6
- a2a_lite/agent.py +75 -13
- a2a_lite/auth.py +27 -8
- a2a_lite/cli.py +1 -1
- a2a_lite/decorators.py +3 -1
- a2a_lite/discovery.py +4 -0
- a2a_lite/executor.py +49 -14
- a2a_lite/middleware.py +6 -1
- a2a_lite/tasks.py +43 -34
- a2a_lite/testing.py +57 -15
- a2a_lite/utils.py +16 -0
- a2a_lite-0.2.1.dist-info/METADATA +526 -0
- a2a_lite-0.2.1.dist-info/RECORD +19 -0
- a2a_lite-0.1.0.dist-info/METADATA +0 -383
- a2a_lite-0.1.0.dist-info/RECORD +0 -19
- {a2a_lite-0.1.0.dist-info → a2a_lite-0.2.1.dist-info}/WHEEL +0 -0
- {a2a_lite-0.1.0.dist-info → a2a_lite-0.2.1.dist-info}/entry_points.txt +0 -0
a2a_lite/testing.py
CHANGED
|
@@ -3,21 +3,61 @@ Testing utilities for A2A Lite agents.
|
|
|
3
3
|
|
|
4
4
|
Makes testing agents as simple as:
|
|
5
5
|
|
|
6
|
-
from a2a_lite.testing import
|
|
6
|
+
from a2a_lite.testing import AgentTestClient
|
|
7
7
|
|
|
8
8
|
def test_my_agent():
|
|
9
|
-
client =
|
|
9
|
+
client = AgentTestClient(agent)
|
|
10
10
|
result = client.call("greet", name="World")
|
|
11
11
|
assert result == "Hello, World!"
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
+
from dataclasses import dataclass
|
|
16
17
|
from typing import Any, Dict, Optional
|
|
17
18
|
from uuid import uuid4
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
@dataclass
|
|
22
|
+
class TestResult:
|
|
23
|
+
"""
|
|
24
|
+
Structured result from a test client call.
|
|
25
|
+
|
|
26
|
+
Provides multiple ways to access the result:
|
|
27
|
+
- .data — parsed Python object (dict, list, int, str, etc.)
|
|
28
|
+
- .text — raw text string
|
|
29
|
+
- .json() — parse text as JSON (raises on invalid JSON)
|
|
30
|
+
- .raw_response — the full A2A response dict
|
|
31
|
+
"""
|
|
32
|
+
_data: Any
|
|
33
|
+
_text: str
|
|
34
|
+
raw_response: Dict[str, Any]
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def data(self) -> Any:
|
|
38
|
+
"""The parsed result value."""
|
|
39
|
+
return self._data
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def text(self) -> str:
|
|
43
|
+
"""The raw text representation."""
|
|
44
|
+
return self._text
|
|
45
|
+
|
|
46
|
+
def json(self) -> Any:
|
|
47
|
+
"""Parse the text as JSON."""
|
|
48
|
+
return json.loads(self._text)
|
|
49
|
+
|
|
50
|
+
def __eq__(self, other: Any) -> bool:
|
|
51
|
+
"""Allow direct comparison with the data value for convenience."""
|
|
52
|
+
if isinstance(other, TestResult):
|
|
53
|
+
return self._data == other._data
|
|
54
|
+
return self._data == other
|
|
55
|
+
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
return f"TestResult(data={self._data!r})"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AgentTestClient:
|
|
21
61
|
"""
|
|
22
62
|
Simple test client for A2A Lite agents.
|
|
23
63
|
|
|
@@ -29,7 +69,7 @@ class TestClient:
|
|
|
29
69
|
return a + b
|
|
30
70
|
|
|
31
71
|
# In your test
|
|
32
|
-
client =
|
|
72
|
+
client = AgentTestClient(agent)
|
|
33
73
|
assert client.call("add", a=2, b=3) == 5
|
|
34
74
|
"""
|
|
35
75
|
|
|
@@ -89,7 +129,7 @@ class TestClient:
|
|
|
89
129
|
# Extract the actual result from A2A response
|
|
90
130
|
return self._extract_result(data)
|
|
91
131
|
|
|
92
|
-
def _extract_result(self, response: Dict) ->
|
|
132
|
+
def _extract_result(self, response: Dict) -> TestResult:
|
|
93
133
|
"""Extract the skill result from A2A response."""
|
|
94
134
|
if "error" in response:
|
|
95
135
|
raise TestClientError(response["error"])
|
|
@@ -103,11 +143,12 @@ class TestClient:
|
|
|
103
143
|
text = part.get("text", "")
|
|
104
144
|
# Try to parse as JSON
|
|
105
145
|
try:
|
|
106
|
-
|
|
146
|
+
data = json.loads(text)
|
|
107
147
|
except json.JSONDecodeError:
|
|
108
|
-
|
|
148
|
+
data = text
|
|
149
|
+
return TestResult(_data=data, _text=text, raw_response=response)
|
|
109
150
|
|
|
110
|
-
return result
|
|
151
|
+
return TestResult(_data=result, _text=json.dumps(result), raw_response=response)
|
|
111
152
|
|
|
112
153
|
def get_agent_card(self) -> Dict[str, Any]:
|
|
113
154
|
"""
|
|
@@ -173,7 +214,7 @@ class TestClient:
|
|
|
173
214
|
result = await gen
|
|
174
215
|
results.append(result)
|
|
175
216
|
|
|
176
|
-
asyncio.
|
|
217
|
+
asyncio.run(run_handler())
|
|
177
218
|
return results
|
|
178
219
|
|
|
179
220
|
|
|
@@ -183,13 +224,13 @@ class TestClientError(Exception):
|
|
|
183
224
|
|
|
184
225
|
|
|
185
226
|
# Async version for async tests
|
|
186
|
-
class
|
|
227
|
+
class AsyncAgentTestClient:
|
|
187
228
|
"""
|
|
188
229
|
Async test client for A2A Lite agents.
|
|
189
230
|
|
|
190
231
|
Example:
|
|
191
232
|
async def test_my_agent():
|
|
192
|
-
client =
|
|
233
|
+
client = AsyncAgentTestClient(agent)
|
|
193
234
|
result = await client.call("greet", name="World")
|
|
194
235
|
assert result == "Hello, World!"
|
|
195
236
|
"""
|
|
@@ -243,7 +284,7 @@ class AsyncTestClient:
|
|
|
243
284
|
|
|
244
285
|
return self._extract_result(data)
|
|
245
286
|
|
|
246
|
-
def _extract_result(self, response: Dict) ->
|
|
287
|
+
def _extract_result(self, response: Dict) -> TestResult:
|
|
247
288
|
"""Extract the skill result from A2A response."""
|
|
248
289
|
if "error" in response:
|
|
249
290
|
raise TestClientError(response["error"])
|
|
@@ -255,11 +296,12 @@ class AsyncTestClient:
|
|
|
255
296
|
if part.get("kind") == "text" or part.get("type") == "text":
|
|
256
297
|
text = part.get("text", "")
|
|
257
298
|
try:
|
|
258
|
-
|
|
299
|
+
data = json.loads(text)
|
|
259
300
|
except json.JSONDecodeError:
|
|
260
|
-
|
|
301
|
+
data = text
|
|
302
|
+
return TestResult(_data=data, _text=text, raw_response=response)
|
|
261
303
|
|
|
262
|
-
return result
|
|
304
|
+
return TestResult(_data=result, _text=json.dumps(result), raw_response=response)
|
|
263
305
|
|
|
264
306
|
async def close(self):
|
|
265
307
|
"""Close the client."""
|
a2a_lite/utils.py
CHANGED
|
@@ -5,6 +5,22 @@ from typing import Any, Dict, Type, get_origin, get_args, Union
|
|
|
5
5
|
import inspect
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def _is_or_subclass(hint: Any, target_class: Type) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Check if a type hint is, or is a subclass of, the target class.
|
|
11
|
+
|
|
12
|
+
Works with raw classes and string annotations.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
if hint is target_class:
|
|
16
|
+
return True
|
|
17
|
+
if isinstance(hint, type) and issubclass(hint, target_class):
|
|
18
|
+
return True
|
|
19
|
+
except TypeError:
|
|
20
|
+
pass
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
8
24
|
def type_to_json_schema(python_type: Type) -> Dict[str, Any]:
|
|
9
25
|
"""
|
|
10
26
|
Convert Python type to JSON Schema.
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: a2a-lite
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Simplified wrapper for Google's A2A Protocol SDK
|
|
5
|
+
Author: A2A Lite Contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: a2a,agents,ai,protocol,sdk
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: a2a-sdk[http-server]>=0.2.6
|
|
18
|
+
Requires-Dist: httpx>=0.25.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: rich>=13.0
|
|
21
|
+
Requires-Dist: starlette>=0.40.0
|
|
22
|
+
Requires-Dist: typer>=0.9.0
|
|
23
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
24
|
+
Requires-Dist: watchfiles>=0.20.0
|
|
25
|
+
Requires-Dist: zeroconf>=0.80.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: httpx>=0.25; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# A2A Lite - Python
|
|
33
|
+
|
|
34
|
+
**Build A2A agents in 8 lines. Add enterprise features when you need them.**
|
|
35
|
+
|
|
36
|
+
Wraps the official [A2A Python SDK](https://github.com/a2aproject/a2a-python) with a simple, intuitive API.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from a2a_lite import Agent
|
|
40
|
+
|
|
41
|
+
agent = Agent(name="Bot", description="My bot")
|
|
42
|
+
|
|
43
|
+
@agent.skill("greet")
|
|
44
|
+
async def greet(name: str) -> str:
|
|
45
|
+
return f"Hello, {name}!"
|
|
46
|
+
|
|
47
|
+
agent.run()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install a2a-lite
|
|
56
|
+
# or
|
|
57
|
+
uv add a2a-lite
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Requirements:** Python 3.10+
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
### 1. Create an agent
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from a2a_lite import Agent
|
|
70
|
+
|
|
71
|
+
agent = Agent(name="Calculator", description="Does math")
|
|
72
|
+
|
|
73
|
+
@agent.skill("add")
|
|
74
|
+
async def add(a: int, b: int) -> int:
|
|
75
|
+
return a + b
|
|
76
|
+
|
|
77
|
+
@agent.skill("multiply")
|
|
78
|
+
async def multiply(a: int, b: int) -> int:
|
|
79
|
+
return a * b
|
|
80
|
+
|
|
81
|
+
agent.run(port=8787)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Test it
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from a2a_lite import Agent, AgentTestClient
|
|
88
|
+
|
|
89
|
+
agent = Agent(name="Calculator", description="Does math")
|
|
90
|
+
|
|
91
|
+
@agent.skill("add")
|
|
92
|
+
async def add(a: int, b: int) -> int:
|
|
93
|
+
return a + b
|
|
94
|
+
|
|
95
|
+
client = AgentTestClient(agent)
|
|
96
|
+
result = client.call("add", a=2, b=3)
|
|
97
|
+
assert result == 5
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. Call it
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
curl -X POST http://localhost:8787/ \
|
|
104
|
+
-H "Content-Type: application/json" \
|
|
105
|
+
-d '{
|
|
106
|
+
"jsonrpc": "2.0",
|
|
107
|
+
"method": "message/send",
|
|
108
|
+
"id": "1",
|
|
109
|
+
"params": {
|
|
110
|
+
"message": {
|
|
111
|
+
"role": "user",
|
|
112
|
+
"parts": [{"type": "text", "text": "{\"skill\": \"add\", \"params\": {\"a\": 2, \"b\": 3}}"}],
|
|
113
|
+
"messageId": "msg-1"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Progressive Complexity
|
|
122
|
+
|
|
123
|
+
### Level 1: Basic Skills
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from a2a_lite import Agent
|
|
127
|
+
|
|
128
|
+
agent = Agent(name="Bot", description="A bot")
|
|
129
|
+
|
|
130
|
+
@agent.skill("greet")
|
|
131
|
+
async def greet(name: str) -> str:
|
|
132
|
+
return f"Hello, {name}!"
|
|
133
|
+
|
|
134
|
+
agent.run()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Level 2: Pydantic Models (Just Works)
|
|
138
|
+
|
|
139
|
+
Pass dicts from callers — they're auto-converted to Pydantic models:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from pydantic import BaseModel
|
|
143
|
+
|
|
144
|
+
class User(BaseModel):
|
|
145
|
+
name: str
|
|
146
|
+
email: str
|
|
147
|
+
|
|
148
|
+
@agent.skill("create_user")
|
|
149
|
+
async def create_user(user: User) -> dict:
|
|
150
|
+
# 'user' is already a User instance — auto-converted from dict!
|
|
151
|
+
return {"id": 1, "name": user.name}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Lists of models work too:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from typing import List
|
|
158
|
+
|
|
159
|
+
@agent.skill("count_users")
|
|
160
|
+
async def count_users(users: List[User]) -> int:
|
|
161
|
+
return len(users)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Level 3: Streaming (Just Yield)
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
@agent.skill("chat", streaming=True)
|
|
168
|
+
async def chat(message: str):
|
|
169
|
+
for word in message.split():
|
|
170
|
+
yield word + " "
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Level 4: Middleware
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
@agent.middleware
|
|
177
|
+
async def log_requests(ctx, next):
|
|
178
|
+
print(f"Calling: {ctx.skill}")
|
|
179
|
+
result = await next()
|
|
180
|
+
print(f"Result: {result}")
|
|
181
|
+
return result
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Built-in middleware:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from a2a_lite import logging_middleware, timing_middleware, retry_middleware, rate_limit_middleware
|
|
188
|
+
|
|
189
|
+
agent.use(logging_middleware)
|
|
190
|
+
agent.use(timing_middleware)
|
|
191
|
+
agent.use(rate_limit_middleware(max_per_minute=60))
|
|
192
|
+
agent.use(retry_middleware(max_retries=3))
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Level 5: Human-in-the-Loop
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from a2a_lite import InteractionContext
|
|
199
|
+
|
|
200
|
+
@agent.skill("wizard")
|
|
201
|
+
async def wizard(ctx: InteractionContext) -> dict:
|
|
202
|
+
name = await ctx.ask("What's your name?")
|
|
203
|
+
role = await ctx.ask("Role?", options=["Dev", "Manager"])
|
|
204
|
+
|
|
205
|
+
if await ctx.confirm(f"Create {name} as {role}?"):
|
|
206
|
+
return {"created": name}
|
|
207
|
+
return {"cancelled": True}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Level 6: File Handling
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from a2a_lite import FilePart
|
|
214
|
+
|
|
215
|
+
@agent.skill("summarize")
|
|
216
|
+
async def summarize(doc: FilePart) -> str:
|
|
217
|
+
content = await doc.read_text()
|
|
218
|
+
return f"Summary: {content[:100]}..."
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Level 7: Task Tracking
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from a2a_lite import TaskContext
|
|
225
|
+
|
|
226
|
+
agent = Agent(name="Bot", description="A bot", task_store="memory")
|
|
227
|
+
|
|
228
|
+
@agent.skill("process")
|
|
229
|
+
async def process(data: str, task: TaskContext) -> str:
|
|
230
|
+
await task.update("working", "Starting...", progress=0.0)
|
|
231
|
+
|
|
232
|
+
for i in range(10):
|
|
233
|
+
await task.update("working", f"Step {i}/10", progress=i/10)
|
|
234
|
+
|
|
235
|
+
return "Done!"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Level 8: Authentication
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from a2a_lite import Agent, APIKeyAuth
|
|
242
|
+
|
|
243
|
+
agent = Agent(
|
|
244
|
+
name="SecureBot",
|
|
245
|
+
description="A secure bot",
|
|
246
|
+
auth=APIKeyAuth(keys=["secret-key-1", "secret-key-2"]),
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
API keys are hashed in memory using SHA-256 — plaintext keys are never stored.
|
|
251
|
+
|
|
252
|
+
Other auth providers:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from a2a_lite.auth import BearerAuth, OAuth2Auth
|
|
256
|
+
|
|
257
|
+
# Bearer/JWT
|
|
258
|
+
agent = Agent(
|
|
259
|
+
name="Bot", description="A bot",
|
|
260
|
+
auth=BearerAuth(secret="your-jwt-secret"),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# OAuth2
|
|
264
|
+
agent = Agent(
|
|
265
|
+
name="Bot", description="A bot",
|
|
266
|
+
auth=OAuth2Auth(issuer="https://auth.example.com", audience="my-api"),
|
|
267
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Level 9: CORS and Production Mode
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
agent = Agent(
|
|
274
|
+
name="Bot",
|
|
275
|
+
description="A bot",
|
|
276
|
+
cors_origins=["https://myapp.com", "https://admin.myapp.com"],
|
|
277
|
+
production=True, # Warns if running over HTTP
|
|
278
|
+
)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Level 10: Webhooks
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
@agent.on_complete
|
|
285
|
+
async def notify(skill_name, result):
|
|
286
|
+
print(f"Skill {skill_name} completed with: {result}")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Testing
|
|
292
|
+
|
|
293
|
+
### AgentTestClient
|
|
294
|
+
|
|
295
|
+
The synchronous test client for use with pytest:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from a2a_lite import Agent, AgentTestClient
|
|
299
|
+
|
|
300
|
+
agent = Agent(name="Bot", description="Test")
|
|
301
|
+
|
|
302
|
+
@agent.skill("greet")
|
|
303
|
+
async def greet(name: str) -> str:
|
|
304
|
+
return f"Hello, {name}!"
|
|
305
|
+
|
|
306
|
+
@agent.skill("info")
|
|
307
|
+
async def info(name: str, age: int) -> dict:
|
|
308
|
+
return {"name": name, "age": age}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_simple_result():
|
|
312
|
+
client = AgentTestClient(agent)
|
|
313
|
+
result = client.call("greet", name="World")
|
|
314
|
+
# Simple values support direct equality
|
|
315
|
+
assert result == "Hello, World!"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def test_dict_result():
|
|
319
|
+
client = AgentTestClient(agent)
|
|
320
|
+
result = client.call("info", name="Alice", age=30)
|
|
321
|
+
# Access dict results via .data
|
|
322
|
+
assert result.data["name"] == "Alice"
|
|
323
|
+
assert result.data["age"] == 30
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_text_access():
|
|
327
|
+
client = AgentTestClient(agent)
|
|
328
|
+
result = client.call("greet", name="World")
|
|
329
|
+
# .text gives the raw string
|
|
330
|
+
assert result.text == '"Hello, World!"'
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_list_skills():
|
|
334
|
+
client = AgentTestClient(agent)
|
|
335
|
+
skills = client.list_skills()
|
|
336
|
+
assert "greet" in skills
|
|
337
|
+
assert "info" in skills
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### TestResult
|
|
341
|
+
|
|
342
|
+
Every `client.call()` returns a `TestResult` with:
|
|
343
|
+
|
|
344
|
+
| Property | Description |
|
|
345
|
+
|----------|-------------|
|
|
346
|
+
| `.data` | Parsed Python object (dict, list, int, str, etc.) |
|
|
347
|
+
| `.text` | Raw text string from the response |
|
|
348
|
+
| `.json()` | Parse text as JSON (raises on invalid JSON) |
|
|
349
|
+
| `.raw_response` | Full A2A response dict |
|
|
350
|
+
|
|
351
|
+
`TestResult` supports direct equality comparison for simple values (`result == 5`), but use `.data` for subscripting (`result.data["key"]`).
|
|
352
|
+
|
|
353
|
+
### AsyncAgentTestClient
|
|
354
|
+
|
|
355
|
+
For async test frameworks:
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
import pytest
|
|
359
|
+
from a2a_lite import AsyncAgentTestClient
|
|
360
|
+
|
|
361
|
+
@pytest.mark.asyncio
|
|
362
|
+
async def test_async():
|
|
363
|
+
client = AsyncAgentTestClient(agent)
|
|
364
|
+
result = await client.call("greet", name="World")
|
|
365
|
+
assert result == "Hello, World!"
|
|
366
|
+
await client.close()
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Streaming Tests
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
def test_streaming():
|
|
373
|
+
client = AgentTestClient(agent)
|
|
374
|
+
results = client.stream("chat", message="hello world")
|
|
375
|
+
assert len(results) == 2
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Task Store
|
|
381
|
+
|
|
382
|
+
The `TaskStore` provides async-safe task lifecycle management:
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
from a2a_lite.tasks import TaskStore, TaskStatus
|
|
386
|
+
|
|
387
|
+
store = TaskStore()
|
|
388
|
+
|
|
389
|
+
# All operations are async and thread-safe
|
|
390
|
+
task = await store.create(task_id="task-1", skill="process")
|
|
391
|
+
task = await store.get("task-1")
|
|
392
|
+
await store.update("task-1", status=TaskStatus.WORKING, progress=0.5)
|
|
393
|
+
tasks = await store.list()
|
|
394
|
+
await store.delete("task-1")
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Agent Discovery
|
|
400
|
+
|
|
401
|
+
Find agents on your local network via mDNS:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
from a2a_lite import AgentDiscovery
|
|
405
|
+
|
|
406
|
+
# Advertise your agent
|
|
407
|
+
agent.run(port=8787, enable_discovery=True)
|
|
408
|
+
|
|
409
|
+
# Discover other agents
|
|
410
|
+
discovery = AgentDiscovery()
|
|
411
|
+
agents = await discovery.discover(timeout=5.0)
|
|
412
|
+
for a in agents:
|
|
413
|
+
print(f"{a.name} at {a.url}")
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## CLI
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
a2a-lite init my-agent # Create new project
|
|
422
|
+
a2a-lite serve agent.py # Run agent from file
|
|
423
|
+
a2a-lite serve agent.py -r # Run with hot reload
|
|
424
|
+
a2a-lite inspect http://... # View agent capabilities
|
|
425
|
+
a2a-lite test http://... skill # Test a skill
|
|
426
|
+
a2a-lite discover # Find local agents
|
|
427
|
+
a2a-lite version # Show version
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Full API Reference
|
|
433
|
+
|
|
434
|
+
### Agent
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
Agent(
|
|
438
|
+
name: str, # Required
|
|
439
|
+
description: str, # Required
|
|
440
|
+
version: str = "1.0.0",
|
|
441
|
+
url: str = None, # Override auto-detected URL
|
|
442
|
+
auth: AuthProvider = None, # Authentication provider
|
|
443
|
+
task_store: str | TaskStore = None, # "memory" or custom TaskStore
|
|
444
|
+
cors_origins: List[str] = None, # CORS allowed origins
|
|
445
|
+
production: bool = False, # Enable production warnings
|
|
446
|
+
enable_discovery: bool = False, # mDNS discovery
|
|
447
|
+
)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Methods:**
|
|
451
|
+
|
|
452
|
+
| Method | Description |
|
|
453
|
+
|--------|-------------|
|
|
454
|
+
| `@agent.skill(name, **config)` | Register a skill via decorator |
|
|
455
|
+
| `@agent.middleware` | Register middleware via decorator |
|
|
456
|
+
| `agent.use(middleware)` | Register middleware function |
|
|
457
|
+
| `@agent.on_complete` | Register completion hook |
|
|
458
|
+
| `agent.run(port=8787)` | Start the server |
|
|
459
|
+
| `agent.get_app()` | Get the ASGI app (for custom deployment) |
|
|
460
|
+
|
|
461
|
+
### Skill Decorator
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
@agent.skill(
|
|
465
|
+
name: str, # Skill name (required)
|
|
466
|
+
description: str = None, # Human-readable description
|
|
467
|
+
tags: List[str] = None, # Categorization tags
|
|
468
|
+
streaming: bool = False, # Enable streaming
|
|
469
|
+
)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Auth Providers
|
|
473
|
+
|
|
474
|
+
| Provider | Usage |
|
|
475
|
+
|----------|-------|
|
|
476
|
+
| `APIKeyAuth(keys=[...])` | API key auth (keys hashed with SHA-256) |
|
|
477
|
+
| `BearerAuth(secret=...)` | JWT/Bearer token auth |
|
|
478
|
+
| `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth |
|
|
479
|
+
| `NoAuth()` | No auth (default) |
|
|
480
|
+
|
|
481
|
+
### Special Parameter Types
|
|
482
|
+
|
|
483
|
+
These are auto-injected when detected in skill signatures:
|
|
484
|
+
|
|
485
|
+
| Type | Description |
|
|
486
|
+
|------|-------------|
|
|
487
|
+
| `TaskContext` | Task lifecycle management (requires `task_store`) |
|
|
488
|
+
| `InteractionContext` | Human-in-the-loop interactions |
|
|
489
|
+
| `FilePart` | File upload handling |
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Examples
|
|
494
|
+
|
|
495
|
+
| Example | What it shows |
|
|
496
|
+
|---------|---------------|
|
|
497
|
+
| [01_hello_world.py](examples/01_hello_world.py) | Simplest agent (8 lines) |
|
|
498
|
+
| [02_calculator.py](examples/02_calculator.py) | Multiple skills |
|
|
499
|
+
| [06_pydantic_models.py](examples/06_pydantic_models.py) | Auto Pydantic conversion |
|
|
500
|
+
| [08_streaming.py](examples/08_streaming.py) | Streaming responses |
|
|
501
|
+
| [09_testing.py](examples/09_testing.py) | Testing your agents |
|
|
502
|
+
| [11_human_in_the_loop.py](examples/11_human_in_the_loop.py) | Ask user questions |
|
|
503
|
+
| [12_file_handling.py](examples/12_file_handling.py) | Handle files |
|
|
504
|
+
| [13_task_tracking.py](examples/13_task_tracking.py) | Progress updates |
|
|
505
|
+
| [14_with_auth.py](examples/14_with_auth.py) | Authentication |
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 100% A2A Protocol Compatible
|
|
510
|
+
|
|
511
|
+
A2A Lite wraps the official A2A Python SDK. Every feature maps to real A2A protocol concepts:
|
|
512
|
+
|
|
513
|
+
| A2A Lite | A2A Protocol |
|
|
514
|
+
|----------|--------------|
|
|
515
|
+
| `@agent.skill()` | Agent Skills |
|
|
516
|
+
| `streaming=True` | SSE Streaming |
|
|
517
|
+
| `InteractionContext.ask()` | `input-required` state |
|
|
518
|
+
| `TaskContext.update()` | Task lifecycle states |
|
|
519
|
+
| `FilePart` | A2A File parts |
|
|
520
|
+
| `APIKeyAuth` / `BearerAuth` | Security schemes |
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## License
|
|
525
|
+
|
|
526
|
+
Apache 2.0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
a2a_lite/__init__.py,sha256=5bFZw7LapphdqfvMvyKfyEthe_kH4iigeGChd5h3TI4,3520
|
|
2
|
+
a2a_lite/agent.py,sha256=WyHe9aA_BYkzIXRjN5byoRc-vItCyL6YLDiyfX5gfPU,17053
|
|
3
|
+
a2a_lite/auth.py,sha256=bjPu_xnAxmfK5_dkqYz5mHouAjHqJFWlfYMqn-S1-A4,10177
|
|
4
|
+
a2a_lite/cli.py,sha256=D46mGduJb8AcZz8WRczNx5OzvIGxqZ7H-4fLCy2LpX8,9043
|
|
5
|
+
a2a_lite/decorators.py,sha256=VdinkddmF61IS8TkjdSfHGdn3nDKwepcIjWZPZBKg3w,1050
|
|
6
|
+
a2a_lite/discovery.py,sha256=BxpiJAUDxIyI2gvsLhjmHte5c9ax5Qf1hbBQnyAmxLQ,4508
|
|
7
|
+
a2a_lite/executor.py,sha256=zkTotenRNa7wlHh6pnIcS546fx5laSgoVIxZizE7SQI,13218
|
|
8
|
+
a2a_lite/human_loop.py,sha256=XAqxp-k8I7TNyuLqqNmLEqABHqcAUiKYCL8n3W5StaY,8685
|
|
9
|
+
a2a_lite/middleware.py,sha256=c6jb9aFfyTf-JY6KjqaSgFJmpzqbHLC6Q1h9NNteqzo,5545
|
|
10
|
+
a2a_lite/parts.py,sha256=qVRiD-H9_NlMPk-R0gTUiGVQ77E2poiuBWAUyAyAoTI,6177
|
|
11
|
+
a2a_lite/streaming.py,sha256=RFv9EJYnhwkT0h1Wovkj4EXwFzCgHdaA-h7WpPaaONo,2329
|
|
12
|
+
a2a_lite/tasks.py,sha256=UpmDP-VGIQ1LodBNq4zx2pJElQ31gOJOAduHFBVyxOA,7039
|
|
13
|
+
a2a_lite/testing.py,sha256=blugOpPKNThlbFTTSCyBVHb7tgQH79RH8z7Vo45hGbg,8953
|
|
14
|
+
a2a_lite/utils.py,sha256=CnkO6HH9oKEXymbJG2ohdt1ESxldQ3fkmcYVO-o9R_k,3841
|
|
15
|
+
a2a_lite/webhooks.py,sha256=t6ebT3jVBEKFpjhBnPI-nuQWIUKQUbJm24phXOBnNKA,6158
|
|
16
|
+
a2a_lite-0.2.1.dist-info/METADATA,sha256=5QLYvcVJh3qwri1zR3Gg-so5YLaAXXAIr-TvvhAg9UM,12583
|
|
17
|
+
a2a_lite-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
a2a_lite-0.2.1.dist-info/entry_points.txt,sha256=BONfFqZbCntNal2iwlTJAE09gCUvurfvqslMYVYh4is,46
|
|
19
|
+
a2a_lite-0.2.1.dist-info/RECORD,,
|