a2a-lite 0.2.1__py3-none-any.whl → 0.2.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: a2a-lite
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Simplified wrapper for Google's A2A Protocol SDK
5
5
  Author: A2A Lite Contributors
6
6
  License-Expression: Apache-2.0
@@ -21,17 +21,17 @@ Requires-Dist: rich>=13.0
21
21
  Requires-Dist: starlette>=0.40.0
22
22
  Requires-Dist: typer>=0.9.0
23
23
  Requires-Dist: uvicorn>=0.30.0
24
- Requires-Dist: watchfiles>=0.20.0
25
- Requires-Dist: zeroconf>=0.80.0
26
24
  Provides-Extra: dev
27
25
  Requires-Dist: httpx>=0.25; extra == 'dev'
28
26
  Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
29
27
  Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Provides-Extra: oauth
29
+ Requires-Dist: pyjwt[crypto]>=2.0; extra == 'oauth'
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  # A2A Lite - Python
33
33
 
34
- **Build A2A agents in 8 lines. Add enterprise features when you need them.**
34
+ **Build A2A agents in 8 lines. Add features when you need them.**
35
35
 
36
36
  Wraps the official [A2A Python SDK](https://github.com/a2aproject/a2a-python) with a simple, intuitive API.
37
37
 
@@ -147,7 +147,6 @@ class User(BaseModel):
147
147
 
148
148
  @agent.skill("create_user")
149
149
  async def create_user(user: User) -> dict:
150
- # 'user' is already a User instance — auto-converted from dict!
151
150
  return {"id": 1, "name": user.name}
152
151
  ```
153
152
 
@@ -186,28 +185,13 @@ Built-in middleware:
186
185
  ```python
187
186
  from a2a_lite import logging_middleware, timing_middleware, retry_middleware, rate_limit_middleware
188
187
 
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))
188
+ agent.add_middleware(logging_middleware)
189
+ agent.add_middleware(timing_middleware)
190
+ agent.add_middleware(rate_limit_middleware(max_per_minute=60))
191
+ agent.add_middleware(retry_middleware(max_retries=3))
193
192
  ```
194
193
 
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
194
+ ### Level 5: File Handling
211
195
 
212
196
  ```python
213
197
  from a2a_lite import FilePart
@@ -218,7 +202,7 @@ async def summarize(doc: FilePart) -> str:
218
202
  return f"Summary: {content[:100]}..."
219
203
  ```
220
204
 
221
- ### Level 7: Task Tracking
205
+ ### Level 6: Task Tracking
222
206
 
223
207
  ```python
224
208
  from a2a_lite import TaskContext
@@ -235,7 +219,7 @@ async def process(data: str, task: TaskContext) -> str:
235
219
  return "Done!"
236
220
  ```
237
221
 
238
- ### Level 8: Authentication
222
+ ### Level 7: Authentication
239
223
 
240
224
  ```python
241
225
  from a2a_lite import Agent, APIKeyAuth
@@ -260,14 +244,24 @@ agent = Agent(
260
244
  auth=BearerAuth(secret="your-jwt-secret"),
261
245
  )
262
246
 
263
- # OAuth2
247
+ # OAuth2 (requires: pip install a2a-lite[oauth])
264
248
  agent = Agent(
265
249
  name="Bot", description="A bot",
266
250
  auth=OAuth2Auth(issuer="https://auth.example.com", audience="my-api"),
267
251
  )
268
252
  ```
269
253
 
270
- ### Level 9: CORS and Production Mode
254
+ Skills can receive auth results by type-hinting a parameter as `AuthResult`:
255
+
256
+ ```python
257
+ from a2a_lite.auth import AuthResult
258
+
259
+ @agent.skill("whoami")
260
+ async def whoami(auth: AuthResult) -> dict:
261
+ return {"user": auth.identity, "scheme": auth.scheme}
262
+ ```
263
+
264
+ ### Level 8: CORS and Production Mode
271
265
 
272
266
  ```python
273
267
  agent = Agent(
@@ -278,11 +272,11 @@ agent = Agent(
278
272
  )
279
273
  ```
280
274
 
281
- ### Level 10: Webhooks
275
+ ### Level 9: Completion Hooks
282
276
 
283
277
  ```python
284
278
  @agent.on_complete
285
- async def notify(skill_name, result):
279
+ async def notify(skill_name, result, ctx):
286
280
  print(f"Skill {skill_name} completed with: {result}")
287
281
  ```
288
282
 
@@ -311,25 +305,16 @@ async def info(name: str, age: int) -> dict:
311
305
  def test_simple_result():
312
306
  client = AgentTestClient(agent)
313
307
  result = client.call("greet", name="World")
314
- # Simple values support direct equality
315
308
  assert result == "Hello, World!"
316
309
 
317
310
 
318
311
  def test_dict_result():
319
312
  client = AgentTestClient(agent)
320
313
  result = client.call("info", name="Alice", age=30)
321
- # Access dict results via .data
322
314
  assert result.data["name"] == "Alice"
323
315
  assert result.data["age"] == 30
324
316
 
325
317
 
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
318
  def test_list_skills():
334
319
  client = AgentTestClient(agent)
335
320
  skills = client.list_skills()
@@ -377,59 +362,19 @@ def test_streaming():
377
362
 
378
363
  ---
379
364
 
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
365
  ## CLI
419
366
 
420
367
  ```bash
421
368
  a2a-lite init my-agent # Create new project
422
369
  a2a-lite serve agent.py # Run agent from file
423
- a2a-lite serve agent.py -r # Run with hot reload
424
370
  a2a-lite inspect http://... # View agent capabilities
425
371
  a2a-lite test http://... skill # Test a skill
426
- a2a-lite discover # Find local agents
427
372
  a2a-lite version # Show version
428
373
  ```
429
374
 
430
375
  ---
431
376
 
432
- ## Full API Reference
377
+ ## API Reference
433
378
 
434
379
  ### Agent
435
380
 
@@ -443,7 +388,6 @@ Agent(
443
388
  task_store: str | TaskStore = None, # "memory" or custom TaskStore
444
389
  cors_origins: List[str] = None, # CORS allowed origins
445
390
  production: bool = False, # Enable production warnings
446
- enable_discovery: bool = False, # mDNS discovery
447
391
  )
448
392
  ```
449
393
 
@@ -453,8 +397,11 @@ Agent(
453
397
  |--------|-------------|
454
398
  | `@agent.skill(name, **config)` | Register a skill via decorator |
455
399
  | `@agent.middleware` | Register middleware via decorator |
456
- | `agent.use(middleware)` | Register middleware function |
400
+ | `agent.add_middleware(fn)` | Register middleware function |
457
401
  | `@agent.on_complete` | Register completion hook |
402
+ | `@agent.on_startup` | Register startup hook |
403
+ | `@agent.on_shutdown` | Register shutdown hook |
404
+ | `@agent.on_error` | Register error handler |
458
405
  | `agent.run(port=8787)` | Start the server |
459
406
  | `agent.get_app()` | Get the ASGI app (for custom deployment) |
460
407
 
@@ -462,7 +409,7 @@ Agent(
462
409
 
463
410
  ```python
464
411
  @agent.skill(
465
- name: str, # Skill name (required)
412
+ name: str = None, # Skill name (defaults to function name)
466
413
  description: str = None, # Human-readable description
467
414
  tags: List[str] = None, # Categorization tags
468
415
  streaming: bool = False, # Enable streaming
@@ -475,18 +422,19 @@ Agent(
475
422
  |----------|-------|
476
423
  | `APIKeyAuth(keys=[...])` | API key auth (keys hashed with SHA-256) |
477
424
  | `BearerAuth(secret=...)` | JWT/Bearer token auth |
478
- | `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth |
425
+ | `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth (requires `a2a-lite[oauth]`) |
479
426
  | `NoAuth()` | No auth (default) |
480
427
 
481
428
  ### Special Parameter Types
482
429
 
483
- These are auto-injected when detected in skill signatures:
430
+ These are auto-injected when detected in skill function signatures:
484
431
 
485
432
  | Type | Description |
486
433
  |------|-------------|
487
434
  | `TaskContext` | Task lifecycle management (requires `task_store`) |
488
- | `InteractionContext` | Human-in-the-loop interactions |
435
+ | `AuthResult` | Authentication result injection |
489
436
  | `FilePart` | File upload handling |
437
+ | `DataPart` | Structured data handling |
490
438
 
491
439
  ---
492
440
 
@@ -499,10 +447,9 @@ These are auto-injected when detected in skill signatures:
499
447
  | [06_pydantic_models.py](examples/06_pydantic_models.py) | Auto Pydantic conversion |
500
448
  | [08_streaming.py](examples/08_streaming.py) | Streaming responses |
501
449
  | [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 |
450
+ | [10_file_handling.py](examples/10_file_handling.py) | Handle files |
451
+ | [11_task_tracking.py](examples/11_task_tracking.py) | Progress updates |
452
+ | [12_with_auth.py](examples/12_with_auth.py) | Authentication |
506
453
 
507
454
  ---
508
455
 
@@ -514,9 +461,9 @@ A2A Lite wraps the official A2A Python SDK. Every feature maps to real A2A proto
514
461
  |----------|--------------|
515
462
  | `@agent.skill()` | Agent Skills |
516
463
  | `streaming=True` | SSE Streaming |
517
- | `InteractionContext.ask()` | `input-required` state |
518
464
  | `TaskContext.update()` | Task lifecycle states |
519
465
  | `FilePart` | A2A File parts |
466
+ | `DataPart` | A2A Data parts |
520
467
  | `APIKeyAuth` / `BearerAuth` | Security schemes |
521
468
 
522
469
  ---
@@ -0,0 +1,16 @@
1
+ a2a_lite/__init__.py,sha256=92WTqzqJV2FTBV0peiuLUdB9y5BZe37jvCT7E2k_vNQ,2642
2
+ a2a_lite/agent.py,sha256=2x4t4ysh10Qwjwp4S8pQehf8O3sINGoJIvq9YN9G-q0,15321
3
+ a2a_lite/auth.py,sha256=A1AMncM0cuWEAcysjumAhjd0lI_jMLmJpeYWuAPj30A,10181
4
+ a2a_lite/cli.py,sha256=5oAwZLR59ebxhuJjIYSHd_AskZqToQLRz45jAAlEBp0,7680
5
+ a2a_lite/decorators.py,sha256=rRzpeVW6mZTAFpNzZYDdraShbDdq7S32c4Z16edD4xw,979
6
+ a2a_lite/executor.py,sha256=pZx_3_JV34qWgbNDkzCWmT1qJRuBAxhrmz4luU7bDBc,13163
7
+ a2a_lite/middleware.py,sha256=c6jb9aFfyTf-JY6KjqaSgFJmpzqbHLC6Q1h9NNteqzo,5545
8
+ a2a_lite/parts.py,sha256=qVRiD-H9_NlMPk-R0gTUiGVQ77E2poiuBWAUyAyAoTI,6177
9
+ a2a_lite/streaming.py,sha256=6qLMUlbk61HBhtotxhSz5Yh4wL9OHeGt24o_J--RJvI,1626
10
+ a2a_lite/tasks.py,sha256=UpmDP-VGIQ1LodBNq4zx2pJElQ31gOJOAduHFBVyxOA,7039
11
+ a2a_lite/testing.py,sha256=M9IbLA6oUz1DokJ9Sc_r0gK43NNkU78IVkiBRuDFFCU,9393
12
+ a2a_lite/utils.py,sha256=AFLYQ4J-F7H_HeYWAeg8H3p9EOdDv4dOpju_ebrU5PI,3934
13
+ a2a_lite-0.2.3.dist-info/METADATA,sha256=x9DLlTzprZMk8qT210WM4dMxHiDaWjX5h9GFnbVxGrY,11392
14
+ a2a_lite-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ a2a_lite-0.2.3.dist-info/entry_points.txt,sha256=BONfFqZbCntNal2iwlTJAE09gCUvurfvqslMYVYh4is,46
16
+ a2a_lite-0.2.3.dist-info/RECORD,,
a2a_lite/discovery.py DELETED
@@ -1,152 +0,0 @@
1
- """
2
- mDNS-based local agent discovery using Zeroconf.
3
- """
4
- from __future__ import annotations
5
-
6
- import asyncio
7
- import logging
8
- import socket
9
- from typing import Dict, List, Optional
10
- from dataclasses import dataclass
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
- from zeroconf import ServiceBrowser, ServiceListener, Zeroconf, ServiceInfo
15
- from zeroconf.asyncio import AsyncZeroconf, AsyncServiceBrowser
16
-
17
-
18
- SERVICE_TYPE = "_a2a-agent._tcp.local."
19
-
20
-
21
- @dataclass
22
- class DiscoveredAgent:
23
- """Information about a discovered A2A agent."""
24
- name: str
25
- host: str
26
- port: int
27
- url: str
28
- properties: Dict[str, str]
29
-
30
-
31
- class AgentDiscovery:
32
- """
33
- Discover A2A agents on the local network using mDNS.
34
- """
35
-
36
- def __init__(self):
37
- self._discovered: Dict[str, DiscoveredAgent] = {}
38
- self._zeroconf: Optional[Zeroconf] = None
39
- self._service_info: Optional[ServiceInfo] = None
40
-
41
- async def discover(self, timeout: float = 5.0) -> List[DiscoveredAgent]:
42
- """
43
- Discover agents on the local network.
44
-
45
- Args:
46
- timeout: How long to wait for discoveries
47
-
48
- Returns:
49
- List of discovered agents
50
- """
51
- self._discovered.clear()
52
-
53
- async with AsyncZeroconf() as azc:
54
- listener = _DiscoveryListener(self._discovered)
55
- browser = AsyncServiceBrowser(
56
- azc.zeroconf, SERVICE_TYPE, listener
57
- )
58
-
59
- await asyncio.sleep(timeout)
60
- await browser.async_cancel()
61
-
62
- return list(self._discovered.values())
63
-
64
- def register(
65
- self,
66
- name: str,
67
- port: int,
68
- properties: Optional[Dict[str, str]] = None,
69
- ) -> ServiceInfo:
70
- """
71
- Register this agent for discovery.
72
-
73
- Args:
74
- name: Agent name
75
- port: Port the agent is running on
76
- properties: Additional properties to advertise
77
-
78
- Returns:
79
- ServiceInfo for unregistration
80
- """
81
- hostname = socket.gethostname()
82
- local_ip = self._get_local_ip()
83
-
84
- # Sanitize name for mDNS (remove spaces and special chars)
85
- safe_name = "".join(c if c.isalnum() or c == "-" else "-" for c in name)
86
-
87
- info = ServiceInfo(
88
- SERVICE_TYPE,
89
- f"{safe_name}.{SERVICE_TYPE}",
90
- addresses=[socket.inet_aton(local_ip)],
91
- port=port,
92
- properties=properties or {},
93
- server=f"{hostname}.local.",
94
- )
95
-
96
- self._zeroconf = Zeroconf()
97
- self._zeroconf.register_service(info)
98
- self._service_info = info
99
-
100
- return info
101
-
102
- def unregister(self) -> None:
103
- """Unregister this agent from discovery."""
104
- if self._zeroconf and self._service_info:
105
- self._zeroconf.unregister_service(self._service_info)
106
- self._zeroconf.close()
107
- self._zeroconf = None
108
- self._service_info = None
109
-
110
- def _get_local_ip(self) -> str:
111
- """Get local IP address."""
112
- try:
113
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
114
- s.connect(("8.8.8.8", 80))
115
- ip = s.getsockname()[0]
116
- s.close()
117
- return ip
118
- except Exception:
119
- logger.debug("Could not detect local IP, falling back to 127.0.0.1")
120
- return "127.0.0.1"
121
-
122
-
123
- class _DiscoveryListener(ServiceListener):
124
- """Internal listener for mDNS service discovery."""
125
-
126
- def __init__(self, discovered: Dict[str, DiscoveredAgent]):
127
- self.discovered = discovered
128
-
129
- def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:
130
- info = zc.get_service_info(type_, name)
131
- if info:
132
- agent_name = name.replace(f".{SERVICE_TYPE}", "")
133
- host = socket.inet_ntoa(info.addresses[0]) if info.addresses else "localhost"
134
- port = info.port
135
-
136
- self.discovered[name] = DiscoveredAgent(
137
- name=agent_name,
138
- host=host,
139
- port=port,
140
- url=f"http://{host}:{port}",
141
- properties={
142
- k.decode() if isinstance(k, bytes) else k:
143
- v.decode() if isinstance(v, bytes) else str(v)
144
- for k, v in info.properties.items()
145
- },
146
- )
147
-
148
- def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:
149
- self.discovered.pop(name, None)
150
-
151
- def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:
152
- self.add_service(zc, type_, name)