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 +151 -0
- a2a_lite/agent.py +453 -0
- a2a_lite/auth.py +344 -0
- a2a_lite/cli.py +336 -0
- a2a_lite/decorators.py +32 -0
- a2a_lite/discovery.py +148 -0
- a2a_lite/executor.py +317 -0
- a2a_lite/human_loop.py +284 -0
- a2a_lite/middleware.py +193 -0
- a2a_lite/parts.py +218 -0
- a2a_lite/streaming.py +89 -0
- a2a_lite/tasks.py +221 -0
- a2a_lite/testing.py +268 -0
- a2a_lite/utils.py +117 -0
- a2a_lite/webhooks.py +232 -0
- a2a_lite-0.1.0.dist-info/METADATA +383 -0
- a2a_lite-0.1.0.dist-info/RECORD +19 -0
- a2a_lite-0.1.0.dist-info/WHEEL +4 -0
- a2a_lite-0.1.0.dist-info/entry_points.txt +2 -0
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()
|