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.
a2a_lite/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- A2A Lite - Simple by default, powerful when needed.
2
+ A2A Lite - Build A2A agents in 8 lines. Add enterprise features when you need them.
3
3
 
4
4
  SIMPLE (8 lines):
5
5
  from a2a_lite import Agent
@@ -43,14 +43,6 @@ WITH TASK TRACKING (opt-in):
43
43
  await task.update("working", progress=0.5)
44
44
  return "done"
45
45
 
46
- WITH HUMAN-IN-THE-LOOP (opt-in):
47
- from a2a_lite.human_loop import InteractionContext
48
-
49
- @agent.skill("wizard")
50
- async def wizard(ctx: InteractionContext) -> str:
51
- name = await ctx.ask("What's your name?")
52
- return f"Hello, {name}!"
53
-
54
46
  WITH FILES (opt-in):
55
47
  from a2a_lite.parts import FilePart
56
48
 
@@ -60,13 +52,12 @@ WITH FILES (opt-in):
60
52
  return summarize(text)
61
53
  """
62
54
 
63
- # Core - always available
55
+ # Core
64
56
  from .agent import Agent
65
57
  from .decorators import SkillDefinition
66
- from .discovery import AgentDiscovery, DiscoveredAgent
67
58
  from .testing import AgentTestClient, AsyncAgentTestClient, TestResult
68
59
 
69
- # Middleware - always available
60
+ # Middleware
70
61
  from .middleware import (
71
62
  MiddlewareContext,
72
63
  MiddlewareChain,
@@ -76,22 +67,13 @@ from .middleware import (
76
67
  rate_limit_middleware,
77
68
  )
78
69
 
79
- # Webhooks - always available
80
- from .webhooks import WebhookClient, WebhookConfig, NotificationManager
81
-
82
- # Streaming - always available
83
- from .streaming import StreamingResponse
84
-
85
- # Parts - opt-in for multi-modal
70
+ # Parts (multi-modal)
86
71
  from .parts import TextPart, FilePart, DataPart, Artifact
87
72
 
88
- # Tasks - opt-in for task lifecycle
73
+ # Tasks
89
74
  from .tasks import TaskContext, TaskState, TaskStatus, Task, TaskStore
90
75
 
91
- # Human-in-the-loop - opt-in
92
- from .human_loop import InteractionContext, ConversationMemory
93
-
94
- # Auth - opt-in
76
+ # Auth
95
77
  from .auth import (
96
78
  AuthProvider,
97
79
  AuthResult,
@@ -102,14 +84,12 @@ from .auth import (
102
84
  require_auth,
103
85
  )
104
86
 
105
- __version__ = "0.2.1"
87
+ __version__ = "0.2.3"
106
88
 
107
89
  __all__ = [
108
90
  # Core
109
91
  "Agent",
110
92
  "SkillDefinition",
111
- "AgentDiscovery",
112
- "DiscoveredAgent",
113
93
  # Testing
114
94
  "AgentTestClient",
115
95
  "AsyncAgentTestClient",
@@ -121,12 +101,6 @@ __all__ = [
121
101
  "timing_middleware",
122
102
  "retry_middleware",
123
103
  "rate_limit_middleware",
124
- # Webhooks
125
- "WebhookClient",
126
- "WebhookConfig",
127
- "NotificationManager",
128
- # Streaming
129
- "StreamingResponse",
130
104
  # Parts (multi-modal)
131
105
  "TextPart",
132
106
  "FilePart",
@@ -138,9 +112,6 @@ __all__ = [
138
112
  "TaskStatus",
139
113
  "Task",
140
114
  "TaskStore",
141
- # Human-in-the-loop
142
- "InteractionContext",
143
- "ConversationMemory",
144
115
  # Auth
145
116
  "AuthProvider",
146
117
  "AuthResult",
a2a_lite/agent.py CHANGED
@@ -29,7 +29,6 @@ from .decorators import SkillDefinition
29
29
  from .utils import type_to_json_schema, extract_function_schemas, _is_or_subclass
30
30
  from .middleware import MiddlewareChain, MiddlewareContext
31
31
  from .streaming import is_generator_function
32
- from .webhooks import NotificationManager, WebhookClient
33
32
 
34
33
 
35
34
  @dataclass
@@ -73,12 +72,6 @@ class Agent:
73
72
  async def process(data: str, task: TaskContext) -> str:
74
73
  await task.update("working", progress=0.5)
75
74
  return "done"
76
-
77
- WITH HUMAN-IN-THE-LOOP (optional):
78
- @agent.skill("wizard")
79
- async def wizard(ctx: InteractionContext) -> str:
80
- name = await ctx.ask("What's your name?")
81
- return f"Hello, {name}!"
82
75
  """
83
76
  name: str
84
77
  description: str
@@ -98,10 +91,7 @@ class Agent:
98
91
  self._on_startup: List[Callable] = []
99
92
  self._on_shutdown: List[Callable] = []
100
93
  self._on_complete: List[Callable] = []
101
- self._discovery = None
102
94
  self._middleware = MiddlewareChain()
103
- self._notifications = NotificationManager()
104
- self._webhook = WebhookClient()
105
95
  self._has_streaming = False
106
96
 
107
97
  # Setup optional task store
@@ -146,12 +136,6 @@ class Agent:
146
136
  async def process(data: str, task: TaskContext) -> str:
147
137
  await task.update("working", progress=0.5)
148
138
  return "done"
149
-
150
- With human-in-the-loop (opt-in):
151
- @agent.skill("wizard")
152
- async def wizard(ctx: InteractionContext) -> str:
153
- name = await ctx.ask("What's your name?")
154
- return f"Hello, {name}!"
155
139
  """
156
140
  def decorator(func: Callable) -> Callable:
157
141
  skill_name = name or func.__name__
@@ -169,12 +153,12 @@ class Agent:
169
153
  # Detect special parameter types using proper type introspection
170
154
  import typing
171
155
  from .tasks import TaskContext as _TaskContext
172
- from .human_loop import InteractionContext as _InteractionContext
156
+ from .auth import AuthResult as _AuthResult
173
157
 
174
158
  needs_task_context = False
175
- needs_interaction = False
159
+ needs_auth = False
176
160
  task_context_param: str | None = None
177
- interaction_param: str | None = None
161
+ auth_param: str | None = None
178
162
 
179
163
  try:
180
164
  resolved_hints = typing.get_type_hints(func)
@@ -187,9 +171,14 @@ class Agent:
187
171
  if _is_or_subclass(hint, _TaskContext):
188
172
  needs_task_context = True
189
173
  task_context_param = param_name
190
- elif _is_or_subclass(hint, _InteractionContext):
191
- needs_interaction = True
192
- interaction_param = param_name
174
+ elif _is_or_subclass(hint, _AuthResult):
175
+ needs_auth = True
176
+ auth_param = param_name
177
+
178
+ # Also detect require_auth decorator
179
+ if getattr(func, '__requires_auth__', False) and not needs_auth:
180
+ needs_auth = True
181
+ auth_param = auth_param or "auth"
193
182
 
194
183
  # Extract schemas
195
184
  input_schema, output_schema = extract_function_schemas(func)
@@ -204,9 +193,9 @@ class Agent:
204
193
  is_async=asyncio.iscoroutinefunction(func) or is_streaming,
205
194
  is_streaming=is_streaming,
206
195
  needs_task_context=needs_task_context,
207
- needs_interaction=needs_interaction,
196
+ needs_auth=needs_auth,
208
197
  task_context_param=task_context_param,
209
- interaction_param=interaction_param,
198
+ auth_param=auth_param,
210
199
  )
211
200
 
212
201
  self._skills[skill_name] = skill_def
@@ -251,16 +240,6 @@ class Agent:
251
240
  self._on_complete.append(func)
252
241
  return func
253
242
 
254
- @property
255
- def webhook(self) -> WebhookClient:
256
- """Get the webhook client for sending notifications."""
257
- return self._webhook
258
-
259
- @property
260
- def notifications(self) -> NotificationManager:
261
- """Get the notification manager for push notifications."""
262
- return self._notifications
263
-
264
243
  def build_agent_card(self, host: str = "localhost", port: int = 8787) -> AgentCard:
265
244
  """Generate A2A-compliant Agent Card from registered skills."""
266
245
  skills = []
@@ -278,12 +257,6 @@ class Agent:
278
257
 
279
258
  url = self.url or f"http://{host}:{port}"
280
259
 
281
- # Check if any skills need human-in-the-loop
282
- has_input_required = any(
283
- getattr(s, 'needs_interaction', False)
284
- for s in self._skills.values()
285
- )
286
-
287
260
  return AgentCard(
288
261
  name=self.name,
289
262
  description=self.description,
@@ -302,9 +275,7 @@ class Agent:
302
275
  self,
303
276
  host: str = "0.0.0.0",
304
277
  port: int = 8787,
305
- reload: bool = False,
306
278
  log_level: str = "info",
307
- enable_discovery: bool = False,
308
279
  ) -> None:
309
280
  """
310
281
  Start the A2A server.
@@ -313,7 +284,7 @@ class Agent:
313
284
  agent.run()
314
285
 
315
286
  With options:
316
- agent.run(port=9000, enable_discovery=True)
287
+ agent.run(port=9000)
317
288
  """
318
289
  from rich.console import Console
319
290
  from rich.panel import Panel
@@ -349,8 +320,7 @@ class Agent:
349
320
  # Build display info
350
321
  skills_list = "\n".join([
351
322
  f" • {s.name}: {s.description}" +
352
- (" [streaming]" if getattr(s, 'is_streaming', False) else "") +
353
- (" [interactive]" if getattr(s, 'needs_interaction', False) else "")
323
+ (" [streaming]" if getattr(s, 'is_streaming', False) else "")
354
324
  for s in self._skills.values()
355
325
  ])
356
326
  if not skills_list:
@@ -362,8 +332,6 @@ class Agent:
362
332
  features.append(f"{len(self._middleware._middlewares)} middleware")
363
333
  if self._has_streaming:
364
334
  features.append("streaming")
365
- if self._on_complete:
366
- features.append("webhooks")
367
335
  if self.auth:
368
336
  features.append("auth")
369
337
  if self._task_store:
@@ -383,22 +351,14 @@ class Agent:
383
351
  ))
384
352
 
385
353
  # Run startup hooks
386
- for hook in self._on_startup:
387
- if asyncio.iscoroutinefunction(hook):
388
- asyncio.get_event_loop().run_until_complete(hook())
389
- else:
390
- hook()
391
-
392
- # Enable discovery if requested
393
- if enable_discovery:
394
- from .discovery import AgentDiscovery
395
- self._discovery = AgentDiscovery()
396
- self._discovery.register(
397
- name=self.name,
398
- port=port,
399
- properties={"version": self.version},
400
- )
401
- console.print(f"[dim]mDNS discovery enabled for {self.name}[/]")
354
+ async def _run_startup():
355
+ for hook in self._on_startup:
356
+ if asyncio.iscoroutinefunction(hook):
357
+ await hook()
358
+ else:
359
+ hook()
360
+ if self._on_startup:
361
+ asyncio.run(_run_startup())
402
362
 
403
363
  # Production mode warning
404
364
  if self.production:
@@ -415,8 +375,6 @@ class Agent:
415
375
  # Add CORS middleware if configured
416
376
  if self.cors_origins is not None:
417
377
  from starlette.middleware.cors import CORSMiddleware
418
- from starlette.middleware import Middleware as StarletteMiddleware
419
- # Wrap the existing app with CORS
420
378
  app.add_middleware(
421
379
  CORSMiddleware,
422
380
  allow_origins=self.cors_origins,
@@ -434,15 +392,14 @@ class Agent:
434
392
  )
435
393
  finally:
436
394
  # Run shutdown hooks
437
- for hook in self._on_shutdown:
438
- if asyncio.iscoroutinefunction(hook):
439
- asyncio.get_event_loop().run_until_complete(hook())
440
- else:
441
- hook()
442
-
443
- # Unregister discovery
444
- if self._discovery:
445
- self._discovery.unregister()
395
+ async def _run_shutdown():
396
+ for hook in self._on_shutdown:
397
+ if asyncio.iscoroutinefunction(hook):
398
+ await hook()
399
+ else:
400
+ hook()
401
+ if self._on_shutdown:
402
+ asyncio.run(_run_shutdown())
446
403
 
447
404
  async def call_remote(
448
405
  self,
a2a_lite/auth.py CHANGED
@@ -222,7 +222,7 @@ class OAuth2Auth(AuthProvider):
222
222
  audience="my-agent",
223
223
  )
224
224
 
225
- Requires: pip install pyjwt[crypto]
225
+ Requires: pip install a2a-lite[oauth]
226
226
  """
227
227
 
228
228
  def __init__(
@@ -277,7 +277,7 @@ class OAuth2Auth(AuthProvider):
277
277
 
278
278
  except ImportError:
279
279
  return AuthResult.failure(
280
- "OAuth2 requires pyjwt: pip install pyjwt[crypto]"
280
+ "OAuth2 requires pyjwt: pip install a2a-lite[oauth]"
281
281
  )
282
282
  except Exception as e:
283
283
  return AuthResult.failure(f"Token validation failed: {str(e)}")
a2a_lite/cli.py CHANGED
@@ -75,7 +75,7 @@ version = "0.1.0"
75
75
  description = "A2A Agent: {name}"
76
76
  requires-python = ">=3.10"
77
77
  dependencies = [
78
- "a2a-lite>=0.2.1",
78
+ "a2a-lite>=0.2.3",
79
79
  ]
80
80
  '''
81
81
  (project_path / "pyproject.toml").write_text(pyproject)
@@ -232,51 +232,10 @@ def test(
232
232
  raise typer.Exit(1)
233
233
 
234
234
 
235
- @app.command()
236
- def discover(
237
- timeout: float = typer.Option(5.0, help="Discovery timeout in seconds"),
238
- ):
239
- """
240
- Discover A2A agents on the local network.
241
-
242
- Uses mDNS to find agents advertising themselves.
243
- """
244
- from .discovery import AgentDiscovery
245
-
246
- async def _discover():
247
- console.print("[dim]Scanning local network...[/]\n")
248
-
249
- discovery = AgentDiscovery()
250
- agents = await discovery.discover(timeout=timeout)
251
-
252
- if not agents:
253
- console.print("[yellow]No agents found.[/]")
254
- console.print("[dim]Make sure agents are running with discovery enabled (enable_discovery=True).[/]")
255
- return
256
-
257
- table = Table(title=f"🔍 Found {len(agents)} Agent(s)")
258
- table.add_column("Name", style="cyan")
259
- table.add_column("URL", style="blue")
260
- table.add_column("Properties", style="dim")
261
-
262
- for agent in agents:
263
- table.add_row(
264
- agent.name,
265
- agent.url,
266
- json.dumps(agent.properties) if agent.properties else "-",
267
- )
268
-
269
- console.print(table)
270
-
271
- asyncio.run(_discover())
272
-
273
-
274
235
  @app.command()
275
236
  def serve(
276
237
  file: Path = typer.Argument(..., help="Python file containing the agent"),
277
238
  port: int = typer.Option(8787, help="Port to run on"),
278
- reload: bool = typer.Option(False, "--reload", "-r", help="Enable hot reload"),
279
- discovery: bool = typer.Option(False, "--discovery", "-d", help="Enable mDNS discovery"),
280
239
  ):
281
240
  """
282
241
  Run an agent from a Python file.
@@ -312,7 +271,7 @@ def serve(
312
271
  raise typer.Exit(1)
313
272
 
314
273
  agent = module.agent
315
- agent.run(port=port, reload=reload, enable_discovery=discovery)
274
+ agent.run(port=port)
316
275
 
317
276
 
318
277
  @app.command()
a2a_lite/decorators.py CHANGED
@@ -17,9 +17,9 @@ class SkillDefinition:
17
17
  is_async: bool = False
18
18
  is_streaming: bool = False
19
19
  needs_task_context: bool = False
20
- needs_interaction: bool = False
20
+ needs_auth: bool = False
21
21
  task_context_param: Optional[str] = None
22
- interaction_param: Optional[str] = None
22
+ auth_param: Optional[str] = None
23
23
 
24
24
  def to_dict(self) -> Dict[str, Any]:
25
25
  """Convert to dictionary for serialization."""
@@ -30,5 +30,4 @@ class SkillDefinition:
30
30
  "input_schema": self.input_schema,
31
31
  "output_schema": self.output_schema,
32
32
  "is_streaming": self.is_streaming,
33
- "needs_interaction": self.needs_interaction,
34
33
  }
a2a_lite/executor.py CHANGED
@@ -59,21 +59,22 @@ class LiteAgentExecutor(AgentExecutor):
59
59
  from a2a.utils import new_agent_text_message
60
60
 
61
61
  try:
62
- # Authenticate the request
62
+ # Authenticate the request (always run to produce auth_result for injection)
63
+ auth_result = None
63
64
  if self.auth_provider:
64
65
  from .auth import AuthRequest, NoAuth
65
- if not isinstance(self.auth_provider, NoAuth):
66
- headers = {}
67
- if context.call_context and context.call_context.state:
68
- headers = context.call_context.state.get('headers', {})
69
- auth_request = AuthRequest(headers=headers)
70
- auth_result = await self.auth_provider.authenticate(auth_request)
71
- if not auth_result.authenticated:
72
- error_msg = json.dumps({
73
- "error": auth_result.error or "Authentication failed",
74
- })
75
- await event_queue.enqueue_event(new_agent_text_message(error_msg))
76
- return
66
+ headers = {}
67
+ if context.call_context and context.call_context.state:
68
+ headers = context.call_context.state.get('headers', {})
69
+ auth_request = AuthRequest(headers=headers)
70
+ auth_result = await self.auth_provider.authenticate(auth_request)
71
+ # Reject unauthenticated requests (unless NoAuth)
72
+ if not isinstance(self.auth_provider, NoAuth) and not auth_result.authenticated:
73
+ error_msg = json.dumps({
74
+ "error": auth_result.error or "Authentication failed",
75
+ })
76
+ await event_queue.enqueue_event(new_agent_text_message(error_msg))
77
+ return
77
78
 
78
79
  # Extract message and parts
79
80
  message, parts = self._extract_message_and_parts(context)
@@ -88,9 +89,10 @@ class LiteAgentExecutor(AgentExecutor):
88
89
  message=message,
89
90
  )
90
91
 
91
- # Store parts in metadata for skill access
92
+ # Store parts and auth result in metadata for skill access
92
93
  ctx.metadata["parts"] = parts
93
94
  ctx.metadata["event_queue"] = event_queue
95
+ ctx.metadata["auth_result"] = auth_result
94
96
 
95
97
  # Define final handler
96
98
  async def final_handler(ctx: MiddlewareContext) -> Any:
@@ -166,12 +168,9 @@ class LiteAgentExecutor(AgentExecutor):
166
168
  param_name = skill_def.task_context_param or "task"
167
169
  params[param_name] = task_ctx
168
170
 
169
- if skill_def.needs_interaction:
170
- from .human_loop import InteractionContext
171
- task_id = metadata.get("task_id", "unknown")
172
- interaction_ctx = InteractionContext(task_id, event_queue)
173
- param_name = skill_def.interaction_param or "ctx"
174
- params[param_name] = interaction_ctx
171
+ if skill_def.needs_auth:
172
+ param_name = skill_def.auth_param or "auth"
173
+ params[param_name] = metadata.get("auth_result")
175
174
 
176
175
  # Call the handler
177
176
  handler = skill_def.handler
@@ -211,8 +210,8 @@ class LiteAgentExecutor(AgentExecutor):
211
210
 
212
211
  # Skip special context types
213
212
  from .tasks import TaskContext as _TaskContext
214
- from .human_loop import InteractionContext as _InteractionContext
215
- if _is_or_subclass(param_type, _TaskContext) or _is_or_subclass(param_type, _InteractionContext):
213
+ from .auth import AuthResult as _AuthResult
214
+ if _is_or_subclass(param_type, _TaskContext) or _is_or_subclass(param_type, _AuthResult):
216
215
  continue
217
216
 
218
217
  # Convert FilePart
@@ -345,7 +344,7 @@ class LiteAgentExecutor(AgentExecutor):
345
344
  if asyncio.iscoroutinefunction(handler):
346
345
  return await handler(*args, **kwargs)
347
346
  else:
348
- loop = asyncio.get_event_loop()
347
+ loop = asyncio.get_running_loop()
349
348
  return await loop.run_in_executor(
350
349
  None,
351
350
  lambda: handler(*args, **kwargs)
a2a_lite/streaming.py CHANGED
@@ -58,32 +58,3 @@ async def stream_generator(
58
58
  await event_queue.enqueue_event(new_agent_text_message(text))
59
59
 
60
60
 
61
- class StreamingResponse:
62
- """
63
- Helper class for building streaming responses.
64
-
65
- Example:
66
- @agent.skill("count")
67
- async def count(n: int):
68
- stream = StreamingResponse()
69
- for i in range(n):
70
- stream.write(f"Count: {i}")
71
- return stream
72
- """
73
-
74
- def __init__(self):
75
- self._chunks: list[str] = []
76
-
77
- def write(self, chunk: str) -> None:
78
- """Add a chunk to the stream."""
79
- self._chunks.append(chunk)
80
-
81
- def __iter__(self):
82
- return iter(self._chunks)
83
-
84
- def __aiter__(self):
85
- return self._async_iter()
86
-
87
- async def _async_iter(self):
88
- for chunk in self._chunks:
89
- yield chunk
a2a_lite/testing.py CHANGED
@@ -214,7 +214,16 @@ class AgentTestClient:
214
214
  result = await gen
215
215
  results.append(result)
216
216
 
217
- asyncio.run(run_handler())
217
+ # Handle both sync and async calling contexts
218
+ try:
219
+ asyncio.get_running_loop()
220
+ # Already in an async context — run in a separate thread
221
+ import concurrent.futures
222
+ with concurrent.futures.ThreadPoolExecutor(1) as pool:
223
+ pool.submit(asyncio.run, run_handler()).result()
224
+ except RuntimeError:
225
+ # No running loop — safe to use asyncio.run()
226
+ asyncio.run(run_handler())
218
227
  return results
219
228
 
220
229
 
a2a_lite/utils.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Helper functions for A2A Lite.
3
3
  """
4
+ import typing
4
5
  from typing import Any, Dict, Type, get_origin, get_args, Union
5
6
  import inspect
6
7
 
@@ -103,7 +104,10 @@ def extract_function_schemas(func) -> tuple[Dict[str, Any], Dict[str, Any]]:
103
104
  Tuple of (input_schema, output_schema)
104
105
  """
105
106
  sig = inspect.signature(func)
106
- hints = getattr(func, '__annotations__', {})
107
+ try:
108
+ hints = typing.get_type_hints(func)
109
+ except Exception:
110
+ hints = getattr(func, '__annotations__', {})
107
111
 
108
112
  # Build input schema from parameters
109
113
  properties = {}