a2a-lite 0.1.0__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 ADDED
@@ -0,0 +1,151 @@
1
+ """
2
+ A2A Lite - Simple by default, powerful when needed.
3
+
4
+ SIMPLE (8 lines):
5
+ from a2a_lite import Agent
6
+
7
+ agent = Agent(name="Bot", description="A bot")
8
+
9
+ @agent.skill("greet")
10
+ async def greet(name: str) -> str:
11
+ return f"Hello, {name}!"
12
+
13
+ agent.run()
14
+
15
+ TEST IT (3 lines):
16
+ from a2a_lite import TestClient
17
+ client = TestClient(agent)
18
+ assert client.call("greet", name="World") == "Hello, World!"
19
+
20
+ WITH PYDANTIC:
21
+ class User(BaseModel):
22
+ name: str
23
+
24
+ @agent.skill("create")
25
+ async def create(user: User) -> dict:
26
+ return {"created": user.name}
27
+
28
+ WITH STREAMING:
29
+ @agent.skill("chat", streaming=True)
30
+ async def chat(msg: str):
31
+ for word in msg.split():
32
+ yield word
33
+
34
+ WITH AUTH (opt-in):
35
+ from a2a_lite.auth import APIKeyAuth
36
+ agent = Agent(name="Bot", auth=APIKeyAuth(keys=["secret"]))
37
+
38
+ WITH TASK TRACKING (opt-in):
39
+ from a2a_lite.tasks import TaskContext
40
+
41
+ @agent.skill("process")
42
+ async def process(data: str, task: TaskContext) -> str:
43
+ await task.update("working", progress=0.5)
44
+ return "done"
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
+ WITH FILES (opt-in):
55
+ from a2a_lite.parts import FilePart
56
+
57
+ @agent.skill("summarize")
58
+ async def summarize(doc: FilePart) -> str:
59
+ text = await doc.read_text()
60
+ return summarize(text)
61
+ """
62
+
63
+ # Core - always available
64
+ from .agent import Agent
65
+ from .decorators import SkillDefinition
66
+ from .discovery import AgentDiscovery, DiscoveredAgent
67
+ from .testing import TestClient, AsyncTestClient
68
+
69
+ # Middleware - always available
70
+ from .middleware import (
71
+ MiddlewareContext,
72
+ MiddlewareChain,
73
+ logging_middleware,
74
+ timing_middleware,
75
+ retry_middleware,
76
+ rate_limit_middleware,
77
+ )
78
+
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
86
+ from .parts import TextPart, FilePart, DataPart, Artifact
87
+
88
+ # Tasks - opt-in for task lifecycle
89
+ from .tasks import TaskContext, TaskState, TaskStatus, Task, TaskStore
90
+
91
+ # Human-in-the-loop - opt-in
92
+ from .human_loop import InteractionContext, ConversationMemory
93
+
94
+ # Auth - opt-in
95
+ from .auth import (
96
+ AuthProvider,
97
+ AuthResult,
98
+ NoAuth,
99
+ APIKeyAuth,
100
+ BearerAuth,
101
+ OAuth2Auth,
102
+ require_auth,
103
+ )
104
+
105
+ __version__ = "0.1.0"
106
+
107
+ __all__ = [
108
+ # Core
109
+ "Agent",
110
+ "SkillDefinition",
111
+ "AgentDiscovery",
112
+ "DiscoveredAgent",
113
+ # Testing
114
+ "TestClient",
115
+ "AsyncTestClient",
116
+ # Middleware
117
+ "MiddlewareContext",
118
+ "MiddlewareChain",
119
+ "logging_middleware",
120
+ "timing_middleware",
121
+ "retry_middleware",
122
+ "rate_limit_middleware",
123
+ # Webhooks
124
+ "WebhookClient",
125
+ "WebhookConfig",
126
+ "NotificationManager",
127
+ # Streaming
128
+ "StreamingResponse",
129
+ # Parts (multi-modal)
130
+ "TextPart",
131
+ "FilePart",
132
+ "DataPart",
133
+ "Artifact",
134
+ # Tasks
135
+ "TaskContext",
136
+ "TaskState",
137
+ "TaskStatus",
138
+ "Task",
139
+ "TaskStore",
140
+ # Human-in-the-loop
141
+ "InteractionContext",
142
+ "ConversationMemory",
143
+ # Auth
144
+ "AuthProvider",
145
+ "AuthResult",
146
+ "NoAuth",
147
+ "APIKeyAuth",
148
+ "BearerAuth",
149
+ "OAuth2Auth",
150
+ "require_auth",
151
+ ]
a2a_lite/agent.py ADDED
@@ -0,0 +1,453 @@
1
+ """
2
+ Core Agent class that wraps the A2A SDK complexity.
3
+
4
+ Simple by default, powerful when needed.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import inspect
10
+ from typing import Any, Callable, Optional, Dict, List, Type, Union, get_origin, get_args
11
+ from dataclasses import dataclass, field
12
+
13
+ import uvicorn
14
+
15
+ from a2a.server.apps import A2AStarletteApplication
16
+ from a2a.server.request_handlers import DefaultRequestHandler
17
+ from a2a.server.tasks import InMemoryTaskStore
18
+ from a2a.types import (
19
+ AgentCard,
20
+ AgentSkill,
21
+ AgentCapabilities,
22
+ )
23
+
24
+ from .executor import LiteAgentExecutor
25
+ from .decorators import SkillDefinition
26
+ from .utils import type_to_json_schema, extract_function_schemas
27
+ from .middleware import MiddlewareChain, MiddlewareContext
28
+ from .streaming import is_generator_function
29
+ from .webhooks import NotificationManager, WebhookClient
30
+
31
+
32
+ @dataclass
33
+ class Agent:
34
+ """
35
+ Simplified A2A Agent - simple by default, powerful when needed.
36
+
37
+ SIMPLE (8 lines):
38
+ agent = Agent(name="Bot", description="A bot")
39
+
40
+ @agent.skill("greet")
41
+ async def greet(name: str) -> str:
42
+ return f"Hello, {name}!"
43
+
44
+ agent.run()
45
+
46
+ WITH PYDANTIC:
47
+ class User(BaseModel):
48
+ name: str
49
+
50
+ @agent.skill("create")
51
+ async def create(user: User) -> dict:
52
+ return {"created": user.name}
53
+
54
+ WITH STREAMING:
55
+ @agent.skill("chat", streaming=True)
56
+ async def chat(msg: str):
57
+ for word in msg.split():
58
+ yield word
59
+
60
+ WITH AUTH (optional):
61
+ from a2a_lite.auth import APIKeyAuth
62
+
63
+ agent = Agent(
64
+ name="SecureBot",
65
+ auth=APIKeyAuth(keys=["secret"]),
66
+ )
67
+
68
+ WITH TASK TRACKING (optional):
69
+ @agent.skill("process")
70
+ async def process(data: str, task: TaskContext) -> str:
71
+ await task.update("working", progress=0.5)
72
+ return "done"
73
+
74
+ WITH HUMAN-IN-THE-LOOP (optional):
75
+ @agent.skill("wizard")
76
+ async def wizard(ctx: InteractionContext) -> str:
77
+ name = await ctx.ask("What's your name?")
78
+ return f"Hello, {name}!"
79
+ """
80
+ name: str
81
+ description: str
82
+ version: str = "1.0.0"
83
+ url: Optional[str] = None
84
+
85
+ # Optional enterprise features
86
+ auth: Optional[Any] = None # AuthProvider
87
+ task_store: Optional[Any] = None # TaskStore or "memory"
88
+
89
+ def __post_init__(self):
90
+ # Internal state
91
+ self._skills: Dict[str, SkillDefinition] = {}
92
+ self._error_handler: Optional[Callable] = None
93
+ self._on_startup: List[Callable] = []
94
+ self._on_shutdown: List[Callable] = []
95
+ self._on_complete: List[Callable] = []
96
+ self._discovery = None
97
+ self._middleware = MiddlewareChain()
98
+ self._notifications = NotificationManager()
99
+ self._webhook = WebhookClient()
100
+ self._has_streaming = False
101
+
102
+ # Setup optional task store
103
+ if self.task_store == "memory":
104
+ from .tasks import TaskStore
105
+ self._task_store = TaskStore()
106
+ elif self.task_store:
107
+ self._task_store = self.task_store
108
+ else:
109
+ self._task_store = None
110
+
111
+ # Setup optional auth
112
+ if self.auth is None:
113
+ from .auth import NoAuth
114
+ self._auth = NoAuth()
115
+ else:
116
+ self._auth = self.auth
117
+
118
+ def skill(
119
+ self,
120
+ name: Optional[str] = None,
121
+ description: Optional[str] = None,
122
+ tags: Optional[List[str]] = None,
123
+ streaming: bool = False,
124
+ ) -> Callable:
125
+ """
126
+ Decorator to register a function as an agent skill.
127
+
128
+ Simple:
129
+ @agent.skill("greet")
130
+ async def greet(name: str) -> str:
131
+ return f"Hello, {name}!"
132
+
133
+ With streaming:
134
+ @agent.skill("chat", streaming=True)
135
+ async def chat(message: str):
136
+ for word in message.split():
137
+ yield word
138
+
139
+ With task context (opt-in):
140
+ @agent.skill("process")
141
+ async def process(data: str, task: TaskContext) -> str:
142
+ await task.update("working", progress=0.5)
143
+ return "done"
144
+
145
+ With human-in-the-loop (opt-in):
146
+ @agent.skill("wizard")
147
+ async def wizard(ctx: InteractionContext) -> str:
148
+ name = await ctx.ask("What's your name?")
149
+ return f"Hello, {name}!"
150
+ """
151
+ def decorator(func: Callable) -> Callable:
152
+ skill_name = name or func.__name__
153
+ skill_desc = description or func.__doc__ or f"Skill: {skill_name}"
154
+
155
+ # Clean up docstring
156
+ if skill_desc:
157
+ skill_desc = " ".join(skill_desc.split())
158
+
159
+ # Detect streaming
160
+ is_streaming = streaming or is_generator_function(func)
161
+ if is_streaming:
162
+ self._has_streaming = True
163
+
164
+ # Detect special parameter types
165
+ hints = getattr(func, '__annotations__', {})
166
+ needs_task_context = any(
167
+ str(h).endswith("TaskContext") or "TaskContext" in str(h)
168
+ for h in hints.values()
169
+ )
170
+ needs_interaction = any(
171
+ str(h).endswith("InteractionContext") or "InteractionContext" in str(h)
172
+ for h in hints.values()
173
+ )
174
+
175
+ # Extract schemas
176
+ input_schema, output_schema = extract_function_schemas(func)
177
+
178
+ skill_def = SkillDefinition(
179
+ name=skill_name,
180
+ description=skill_desc,
181
+ tags=tags or [],
182
+ handler=func,
183
+ input_schema=input_schema,
184
+ output_schema=output_schema,
185
+ is_async=asyncio.iscoroutinefunction(func) or is_streaming,
186
+ is_streaming=is_streaming,
187
+ needs_task_context=needs_task_context,
188
+ needs_interaction=needs_interaction,
189
+ )
190
+
191
+ self._skills[skill_name] = skill_def
192
+ return func
193
+
194
+ return decorator
195
+
196
+ def middleware(self, func: Callable) -> Callable:
197
+ """
198
+ Decorator to register middleware.
199
+
200
+ Example:
201
+ @agent.middleware
202
+ async def log_requests(ctx, next):
203
+ print(f"Calling: {ctx.skill}")
204
+ return await next()
205
+ """
206
+ self._middleware.add(func)
207
+ return func
208
+
209
+ def add_middleware(self, middleware: Callable) -> None:
210
+ """Add a middleware function (non-decorator version)."""
211
+ self._middleware.add(middleware)
212
+
213
+ def on_error(self, func: Callable) -> Callable:
214
+ """Decorator to register a global error handler."""
215
+ self._error_handler = func
216
+ return func
217
+
218
+ def on_startup(self, func: Callable) -> Callable:
219
+ """Decorator to register a startup hook."""
220
+ self._on_startup.append(func)
221
+ return func
222
+
223
+ def on_shutdown(self, func: Callable) -> Callable:
224
+ """Decorator to register a shutdown hook."""
225
+ self._on_shutdown.append(func)
226
+ return func
227
+
228
+ def on_complete(self, func: Callable) -> Callable:
229
+ """Decorator to register a task completion handler."""
230
+ self._on_complete.append(func)
231
+ return func
232
+
233
+ @property
234
+ def webhook(self) -> WebhookClient:
235
+ """Get the webhook client for sending notifications."""
236
+ return self._webhook
237
+
238
+ @property
239
+ def notifications(self) -> NotificationManager:
240
+ """Get the notification manager for push notifications."""
241
+ return self._notifications
242
+
243
+ def build_agent_card(self, host: str = "localhost", port: int = 8787) -> AgentCard:
244
+ """Generate A2A-compliant Agent Card from registered skills."""
245
+ skills = []
246
+
247
+ for skill_def in self._skills.values():
248
+ skill = AgentSkill(
249
+ id=skill_def.name,
250
+ name=skill_def.name,
251
+ description=skill_def.description,
252
+ tags=skill_def.tags,
253
+ inputModes=["application/json"],
254
+ outputModes=["application/json"],
255
+ )
256
+ skills.append(skill)
257
+
258
+ url = self.url or f"http://{host}:{port}"
259
+
260
+ # Check if any skills need human-in-the-loop
261
+ has_input_required = any(
262
+ getattr(s, 'needs_interaction', False)
263
+ for s in self._skills.values()
264
+ )
265
+
266
+ return AgentCard(
267
+ name=self.name,
268
+ description=self.description,
269
+ version=self.version,
270
+ url=url,
271
+ capabilities=AgentCapabilities(
272
+ streaming=self._has_streaming,
273
+ pushNotifications=bool(self._on_complete),
274
+ ),
275
+ defaultInputModes=["application/json"],
276
+ defaultOutputModes=["application/json"],
277
+ skills=skills,
278
+ )
279
+
280
+ def run(
281
+ self,
282
+ host: str = "0.0.0.0",
283
+ port: int = 8787,
284
+ reload: bool = False,
285
+ log_level: str = "info",
286
+ enable_discovery: bool = False,
287
+ ) -> None:
288
+ """
289
+ Start the A2A server.
290
+
291
+ Simple:
292
+ agent.run()
293
+
294
+ With options:
295
+ agent.run(port=9000, enable_discovery=True)
296
+ """
297
+ from rich.console import Console
298
+ from rich.panel import Panel
299
+
300
+ console = Console()
301
+
302
+ # Build components
303
+ display_host = "localhost" if host == "0.0.0.0" else host
304
+ agent_card = self.build_agent_card(display_host, port)
305
+ executor = LiteAgentExecutor(
306
+ skills=self._skills,
307
+ error_handler=self._error_handler,
308
+ middleware=self._middleware,
309
+ on_complete=self._on_complete,
310
+ auth_provider=self._auth,
311
+ task_store=self._task_store,
312
+ )
313
+
314
+ request_handler = DefaultRequestHandler(
315
+ agent_executor=executor,
316
+ task_store=InMemoryTaskStore(),
317
+ )
318
+
319
+ app_builder = A2AStarletteApplication(
320
+ agent_card=agent_card,
321
+ http_handler=request_handler,
322
+ )
323
+
324
+ # Build display info
325
+ skills_list = "\n".join([
326
+ f" • {s.name}: {s.description}" +
327
+ (" [streaming]" if getattr(s, 'is_streaming', False) else "") +
328
+ (" [interactive]" if getattr(s, 'needs_interaction', False) else "")
329
+ for s in self._skills.values()
330
+ ])
331
+ if not skills_list:
332
+ skills_list = " (no skills registered)"
333
+
334
+ # Collect enabled features
335
+ features = []
336
+ if len(self._middleware._middlewares):
337
+ features.append(f"{len(self._middleware._middlewares)} middleware")
338
+ if self._has_streaming:
339
+ features.append("streaming")
340
+ if self._on_complete:
341
+ features.append("webhooks")
342
+ if self.auth:
343
+ features.append("auth")
344
+ if self._task_store:
345
+ features.append("task-tracking")
346
+
347
+ features_str = f"\n\n[bold]Features:[/] {', '.join(features)}" if features else ""
348
+
349
+ console.print(Panel(
350
+ f"[bold green]{self.name}[/] v{self.version}\n\n"
351
+ f"[dim]{self.description}[/]\n\n"
352
+ f"[bold]Skills:[/]\n{skills_list}{features_str}\n\n"
353
+ f"[bold]Endpoints:[/]\n"
354
+ f" • Agent Card: http://{display_host}:{port}/.well-known/agent.json\n"
355
+ f" • API: http://{display_host}:{port}/",
356
+ title="🚀 A2A Lite Agent Started",
357
+ border_style="green",
358
+ ))
359
+
360
+ # Run startup hooks
361
+ for hook in self._on_startup:
362
+ if asyncio.iscoroutinefunction(hook):
363
+ asyncio.get_event_loop().run_until_complete(hook())
364
+ else:
365
+ hook()
366
+
367
+ # Enable discovery if requested
368
+ if enable_discovery:
369
+ from .discovery import AgentDiscovery
370
+ self._discovery = AgentDiscovery()
371
+ self._discovery.register(
372
+ name=self.name,
373
+ port=port,
374
+ properties={"version": self.version},
375
+ )
376
+ console.print(f"[dim]mDNS discovery enabled for {self.name}[/]")
377
+
378
+ # Start server
379
+ try:
380
+ uvicorn.run(
381
+ app_builder.build(),
382
+ host=host,
383
+ port=port,
384
+ log_level=log_level,
385
+ )
386
+ finally:
387
+ # Run shutdown hooks
388
+ for hook in self._on_shutdown:
389
+ if asyncio.iscoroutinefunction(hook):
390
+ asyncio.get_event_loop().run_until_complete(hook())
391
+ else:
392
+ hook()
393
+
394
+ # Unregister discovery
395
+ if self._discovery:
396
+ self._discovery.unregister()
397
+
398
+ async def call_remote(
399
+ self,
400
+ agent_url: str,
401
+ message: str,
402
+ timeout: float = 30.0,
403
+ ) -> Dict[str, Any]:
404
+ """Call a remote A2A agent."""
405
+ import httpx
406
+ from a2a.client import A2AClient
407
+ from a2a.types import MessageSendParams, SendMessageRequest
408
+ from uuid import uuid4
409
+
410
+ async with httpx.AsyncClient(timeout=timeout) as http_client:
411
+ card_url = f"{agent_url.rstrip('/')}/.well-known/agent.json"
412
+
413
+ client = await A2AClient.get_client_from_agent_card_url(
414
+ http_client, card_url
415
+ )
416
+
417
+ request = SendMessageRequest(
418
+ id=uuid4().hex,
419
+ params=MessageSendParams(
420
+ message={
421
+ "role": "user",
422
+ "parts": [{"type": "text", "text": message}],
423
+ "messageId": uuid4().hex,
424
+ }
425
+ )
426
+ )
427
+
428
+ response = await client.send_message(request)
429
+ return response.model_dump()
430
+
431
+ def get_app(self):
432
+ """Get the Starlette application without running it."""
433
+ agent_card = self.build_agent_card()
434
+ executor = LiteAgentExecutor(
435
+ skills=self._skills,
436
+ error_handler=self._error_handler,
437
+ middleware=self._middleware,
438
+ on_complete=self._on_complete,
439
+ auth_provider=self._auth,
440
+ task_store=self._task_store,
441
+ )
442
+
443
+ request_handler = DefaultRequestHandler(
444
+ agent_executor=executor,
445
+ task_store=InMemoryTaskStore(),
446
+ )
447
+
448
+ app_builder = A2AStarletteApplication(
449
+ agent_card=agent_card,
450
+ http_handler=request_handler,
451
+ )
452
+
453
+ return app_builder.build()