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 +7 -36
- a2a_lite/agent.py +31 -74
- a2a_lite/auth.py +2 -2
- a2a_lite/cli.py +2 -43
- a2a_lite/decorators.py +2 -3
- a2a_lite/executor.py +22 -23
- a2a_lite/streaming.py +0 -29
- a2a_lite/testing.py +10 -1
- a2a_lite/utils.py +5 -1
- {a2a_lite-0.2.1.dist-info → a2a_lite-0.2.3.dist-info}/METADATA +39 -92
- a2a_lite-0.2.3.dist-info/RECORD +16 -0
- a2a_lite/discovery.py +0 -152
- a2a_lite/human_loop.py +0 -284
- a2a_lite/webhooks.py +0 -232
- a2a_lite-0.2.1.dist-info/RECORD +0 -19
- {a2a_lite-0.2.1.dist-info → a2a_lite-0.2.3.dist-info}/WHEEL +0 -0
- {a2a_lite-0.2.1.dist-info → a2a_lite-0.2.3.dist-info}/entry_points.txt +0 -0
a2a_lite/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
A2A Lite -
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
73
|
+
# Tasks
|
|
89
74
|
from .tasks import TaskContext, TaskState, TaskStatus, Task, TaskStore
|
|
90
75
|
|
|
91
|
-
#
|
|
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.
|
|
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 .
|
|
156
|
+
from .auth import AuthResult as _AuthResult
|
|
173
157
|
|
|
174
158
|
needs_task_context = False
|
|
175
|
-
|
|
159
|
+
needs_auth = False
|
|
176
160
|
task_context_param: str | None = None
|
|
177
|
-
|
|
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,
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
196
|
+
needs_auth=needs_auth,
|
|
208
197
|
task_context_param=task_context_param,
|
|
209
|
-
|
|
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
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
asyncio.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
asyncio.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
20
|
+
needs_auth: bool = False
|
|
21
21
|
task_context_param: Optional[str] = None
|
|
22
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
170
|
-
|
|
171
|
-
|
|
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 .
|
|
215
|
-
if _is_or_subclass(param_type, _TaskContext) or _is_or_subclass(param_type,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 = {}
|