a2a-lite 0.2.2__py3-none-any.whl → 0.2.4__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,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: a2a-lite
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Simplified wrapper for Google's A2A Protocol SDK
5
5
  Author: A2A Lite Contributors
6
- License-Expression: Apache-2.0
6
+ License-Expression: MIT
7
7
  Keywords: a2a,agents,ai,protocol,sdk
8
8
  Classifier: Development Status :: 3 - Alpha
9
9
  Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
@@ -21,8 +21,6 @@ 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'
@@ -33,7 +31,7 @@ Description-Content-Type: text/markdown
33
31
 
34
32
  # A2A Lite - Python
35
33
 
36
- **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.**
37
35
 
38
36
  Wraps the official [A2A Python SDK](https://github.com/a2aproject/a2a-python) with a simple, intuitive API.
39
37
 
@@ -149,7 +147,6 @@ class User(BaseModel):
149
147
 
150
148
  @agent.skill("create_user")
151
149
  async def create_user(user: User) -> dict:
152
- # 'user' is already a User instance — auto-converted from dict!
153
150
  return {"id": 1, "name": user.name}
154
151
  ```
155
152
 
@@ -188,28 +185,13 @@ Built-in middleware:
188
185
  ```python
189
186
  from a2a_lite import logging_middleware, timing_middleware, retry_middleware, rate_limit_middleware
190
187
 
191
- agent.use(logging_middleware)
192
- agent.use(timing_middleware)
193
- agent.use(rate_limit_middleware(max_per_minute=60))
194
- 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))
195
192
  ```
196
193
 
197
- ### Level 5: Human-in-the-Loop
198
-
199
- ```python
200
- from a2a_lite import InteractionContext
201
-
202
- @agent.skill("wizard")
203
- async def wizard(ctx: InteractionContext) -> dict:
204
- name = await ctx.ask("What's your name?")
205
- role = await ctx.ask("Role?", options=["Dev", "Manager"])
206
-
207
- if await ctx.confirm(f"Create {name} as {role}?"):
208
- return {"created": name}
209
- return {"cancelled": True}
210
- ```
211
-
212
- ### Level 6: File Handling
194
+ ### Level 5: File Handling
213
195
 
214
196
  ```python
215
197
  from a2a_lite import FilePart
@@ -220,7 +202,7 @@ async def summarize(doc: FilePart) -> str:
220
202
  return f"Summary: {content[:100]}..."
221
203
  ```
222
204
 
223
- ### Level 7: Task Tracking
205
+ ### Level 6: Task Tracking
224
206
 
225
207
  ```python
226
208
  from a2a_lite import TaskContext
@@ -237,7 +219,7 @@ async def process(data: str, task: TaskContext) -> str:
237
219
  return "Done!"
238
220
  ```
239
221
 
240
- ### Level 8: Authentication
222
+ ### Level 7: Authentication
241
223
 
242
224
  ```python
243
225
  from a2a_lite import Agent, APIKeyAuth
@@ -262,14 +244,24 @@ agent = Agent(
262
244
  auth=BearerAuth(secret="your-jwt-secret"),
263
245
  )
264
246
 
265
- # OAuth2
247
+ # OAuth2 (requires: pip install a2a-lite[oauth])
266
248
  agent = Agent(
267
249
  name="Bot", description="A bot",
268
250
  auth=OAuth2Auth(issuer="https://auth.example.com", audience="my-api"),
269
251
  )
270
252
  ```
271
253
 
272
- ### 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
273
265
 
274
266
  ```python
275
267
  agent = Agent(
@@ -280,11 +272,11 @@ agent = Agent(
280
272
  )
281
273
  ```
282
274
 
283
- ### Level 10: Webhooks
275
+ ### Level 9: Completion Hooks
284
276
 
285
277
  ```python
286
278
  @agent.on_complete
287
- async def notify(skill_name, result):
279
+ async def notify(skill_name, result, ctx):
288
280
  print(f"Skill {skill_name} completed with: {result}")
289
281
  ```
290
282
 
@@ -313,25 +305,16 @@ async def info(name: str, age: int) -> dict:
313
305
  def test_simple_result():
314
306
  client = AgentTestClient(agent)
315
307
  result = client.call("greet", name="World")
316
- # Simple values support direct equality
317
308
  assert result == "Hello, World!"
318
309
 
319
310
 
320
311
  def test_dict_result():
321
312
  client = AgentTestClient(agent)
322
313
  result = client.call("info", name="Alice", age=30)
323
- # Access dict results via .data
324
314
  assert result.data["name"] == "Alice"
325
315
  assert result.data["age"] == 30
326
316
 
327
317
 
328
- def test_text_access():
329
- client = AgentTestClient(agent)
330
- result = client.call("greet", name="World")
331
- # .text gives the raw string
332
- assert result.text == '"Hello, World!"'
333
-
334
-
335
318
  def test_list_skills():
336
319
  client = AgentTestClient(agent)
337
320
  skills = client.list_skills()
@@ -379,59 +362,19 @@ def test_streaming():
379
362
 
380
363
  ---
381
364
 
382
- ## Task Store
383
-
384
- The `TaskStore` provides async-safe task lifecycle management:
385
-
386
- ```python
387
- from a2a_lite.tasks import TaskStore, TaskStatus
388
-
389
- store = TaskStore()
390
-
391
- # All operations are async and thread-safe
392
- task = await store.create(task_id="task-1", skill="process")
393
- task = await store.get("task-1")
394
- await store.update("task-1", status=TaskStatus.WORKING, progress=0.5)
395
- tasks = await store.list()
396
- await store.delete("task-1")
397
- ```
398
-
399
- ---
400
-
401
- ## Agent Discovery
402
-
403
- Find agents on your local network via mDNS:
404
-
405
- ```python
406
- from a2a_lite import AgentDiscovery
407
-
408
- # Advertise your agent
409
- agent.run(port=8787, enable_discovery=True)
410
-
411
- # Discover other agents
412
- discovery = AgentDiscovery()
413
- agents = await discovery.discover(timeout=5.0)
414
- for a in agents:
415
- print(f"{a.name} at {a.url}")
416
- ```
417
-
418
- ---
419
-
420
365
  ## CLI
421
366
 
422
367
  ```bash
423
368
  a2a-lite init my-agent # Create new project
424
369
  a2a-lite serve agent.py # Run agent from file
425
- a2a-lite serve agent.py -r # Run with hot reload
426
370
  a2a-lite inspect http://... # View agent capabilities
427
371
  a2a-lite test http://... skill # Test a skill
428
- a2a-lite discover # Find local agents
429
372
  a2a-lite version # Show version
430
373
  ```
431
374
 
432
375
  ---
433
376
 
434
- ## Full API Reference
377
+ ## API Reference
435
378
 
436
379
  ### Agent
437
380
 
@@ -445,7 +388,6 @@ Agent(
445
388
  task_store: str | TaskStore = None, # "memory" or custom TaskStore
446
389
  cors_origins: List[str] = None, # CORS allowed origins
447
390
  production: bool = False, # Enable production warnings
448
- enable_discovery: bool = False, # mDNS discovery
449
391
  )
450
392
  ```
451
393
 
@@ -455,8 +397,11 @@ Agent(
455
397
  |--------|-------------|
456
398
  | `@agent.skill(name, **config)` | Register a skill via decorator |
457
399
  | `@agent.middleware` | Register middleware via decorator |
458
- | `agent.use(middleware)` | Register middleware function |
400
+ | `agent.add_middleware(fn)` | Register middleware function |
459
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 |
460
405
  | `agent.run(port=8787)` | Start the server |
461
406
  | `agent.get_app()` | Get the ASGI app (for custom deployment) |
462
407
 
@@ -464,7 +409,7 @@ Agent(
464
409
 
465
410
  ```python
466
411
  @agent.skill(
467
- name: str, # Skill name (required)
412
+ name: str = None, # Skill name (defaults to function name)
468
413
  description: str = None, # Human-readable description
469
414
  tags: List[str] = None, # Categorization tags
470
415
  streaming: bool = False, # Enable streaming
@@ -477,18 +422,19 @@ Agent(
477
422
  |----------|-------|
478
423
  | `APIKeyAuth(keys=[...])` | API key auth (keys hashed with SHA-256) |
479
424
  | `BearerAuth(secret=...)` | JWT/Bearer token auth |
480
- | `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth |
425
+ | `OAuth2Auth(issuer=..., audience=...)` | OAuth2 auth (requires `a2a-lite[oauth]`) |
481
426
  | `NoAuth()` | No auth (default) |
482
427
 
483
428
  ### Special Parameter Types
484
429
 
485
- These are auto-injected when detected in skill signatures:
430
+ These are auto-injected when detected in skill function signatures:
486
431
 
487
432
  | Type | Description |
488
433
  |------|-------------|
489
434
  | `TaskContext` | Task lifecycle management (requires `task_store`) |
490
- | `InteractionContext` | Human-in-the-loop interactions |
435
+ | `AuthResult` | Authentication result injection |
491
436
  | `FilePart` | File upload handling |
437
+ | `DataPart` | Structured data handling |
492
438
 
493
439
  ---
494
440
 
@@ -501,10 +447,9 @@ These are auto-injected when detected in skill signatures:
501
447
  | [06_pydantic_models.py](examples/06_pydantic_models.py) | Auto Pydantic conversion |
502
448
  | [08_streaming.py](examples/08_streaming.py) | Streaming responses |
503
449
  | [09_testing.py](examples/09_testing.py) | Testing your agents |
504
- | [11_human_in_the_loop.py](examples/11_human_in_the_loop.py) | Ask user questions |
505
- | [12_file_handling.py](examples/12_file_handling.py) | Handle files |
506
- | [13_task_tracking.py](examples/13_task_tracking.py) | Progress updates |
507
- | [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 |
508
453
 
509
454
  ---
510
455
 
@@ -516,13 +461,13 @@ A2A Lite wraps the official A2A Python SDK. Every feature maps to real A2A proto
516
461
  |----------|--------------|
517
462
  | `@agent.skill()` | Agent Skills |
518
463
  | `streaming=True` | SSE Streaming |
519
- | `InteractionContext.ask()` | `input-required` state |
520
464
  | `TaskContext.update()` | Task lifecycle states |
521
465
  | `FilePart` | A2A File parts |
466
+ | `DataPart` | A2A Data parts |
522
467
  | `APIKeyAuth` / `BearerAuth` | Security schemes |
523
468
 
524
469
  ---
525
470
 
526
471
  ## License
527
472
 
528
- Apache 2.0
473
+ MIT
@@ -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.4.dist-info/METADATA,sha256=qR6P0NOhPdbfavyIb66t7PvvjqcCapTsVdZMsLOsEmA,11366
14
+ a2a_lite-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ a2a_lite-0.2.4.dist-info/entry_points.txt,sha256=BONfFqZbCntNal2iwlTJAE09gCUvurfvqslMYVYh4is,46
16
+ a2a_lite-0.2.4.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)