a2a-adapter 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_adapter/__init__.py +42 -0
- a2a_adapter/adapter.py +185 -0
- a2a_adapter/client.py +236 -0
- a2a_adapter/integrations/__init__.py +33 -0
- a2a_adapter/integrations/callable.py +172 -0
- a2a_adapter/integrations/crewai.py +142 -0
- a2a_adapter/integrations/langchain.py +171 -0
- a2a_adapter/integrations/n8n.py +787 -0
- a2a_adapter/loader.py +131 -0
- a2a_adapter-0.1.0.dist-info/METADATA +604 -0
- a2a_adapter-0.1.0.dist-info/RECORD +14 -0
- a2a_adapter-0.1.0.dist-info/WHEEL +5 -0
- a2a_adapter-0.1.0.dist-info/licenses/LICENSE +201 -0
- a2a_adapter-0.1.0.dist-info/top_level.txt +1 -0
a2a_adapter/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A2A Adapters - Python SDK for A2A Protocol Integration.
|
|
3
|
+
|
|
4
|
+
This package provides adapters to integrate various agent frameworks with the
|
|
5
|
+
A2A (Agent-to-Agent) Protocol, enabling seamless inter-agent communication.
|
|
6
|
+
|
|
7
|
+
Main exports:
|
|
8
|
+
- load_a2a_agent: Factory function to create adapters from configuration
|
|
9
|
+
- build_agent_app: Build an ASGI app for serving an A2A agent
|
|
10
|
+
- serve_agent: Convenience function to start serving an A2A agent
|
|
11
|
+
- BaseAgentAdapter: Abstract base class for creating custom adapters
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> import asyncio
|
|
15
|
+
>>> from a2a_adapter import load_a2a_agent, serve_agent
|
|
16
|
+
>>> from a2a.types import AgentCard
|
|
17
|
+
>>>
|
|
18
|
+
>>> async def main():
|
|
19
|
+
... adapter = await load_a2a_agent({
|
|
20
|
+
... "adapter": "n8n",
|
|
21
|
+
... "webhook_url": "https://n8n.example.com/webhook"
|
|
22
|
+
... })
|
|
23
|
+
... card = AgentCard(name="My Agent", description="...")
|
|
24
|
+
... serve_agent(card, adapter, port=9000)
|
|
25
|
+
>>>
|
|
26
|
+
>>> asyncio.run(main())
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
from .adapter import BaseAgentAdapter
|
|
32
|
+
from .client import build_agent_app, serve_agent
|
|
33
|
+
from .loader import load_a2a_agent
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"__version__",
|
|
37
|
+
"BaseAgentAdapter",
|
|
38
|
+
"load_a2a_agent",
|
|
39
|
+
"build_agent_app",
|
|
40
|
+
"serve_agent",
|
|
41
|
+
]
|
|
42
|
+
|
a2a_adapter/adapter.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core adapter abstraction for A2A Protocol integration.
|
|
3
|
+
|
|
4
|
+
This module defines the BaseAgentAdapter abstract class that all framework-specific
|
|
5
|
+
adapters must implement.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, AsyncIterator, Dict
|
|
10
|
+
|
|
11
|
+
from a2a.types import Message, MessageSendParams, Task
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseAgentAdapter(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Abstract base class for adapting agent frameworks to the A2A Protocol.
|
|
17
|
+
|
|
18
|
+
This class provides the core interface for translating between A2A Protocol
|
|
19
|
+
messages and framework-specific inputs/outputs. Concrete implementations
|
|
20
|
+
handle the specifics of n8n, CrewAI, LangChain, etc.
|
|
21
|
+
|
|
22
|
+
The adapter follows a three-step process:
|
|
23
|
+
1. to_framework: Convert A2A MessageSendParams to framework input
|
|
24
|
+
2. call_framework: Execute the framework-specific logic
|
|
25
|
+
3. from_framework: Convert framework output back to A2A Message/Task
|
|
26
|
+
|
|
27
|
+
For adapters that support async task execution, the adapter can:
|
|
28
|
+
- Return a Task with state="submitted" or "working" immediately
|
|
29
|
+
- Run the actual work in the background
|
|
30
|
+
- Allow clients to poll for task status via get_task()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
async def handle(self, params: MessageSendParams) -> Message | Task:
|
|
34
|
+
"""
|
|
35
|
+
Handle a non-streaming A2A message request.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
params: A2A protocol message parameters
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A2A Message or Task response
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
Exception: If the underlying framework call fails
|
|
45
|
+
"""
|
|
46
|
+
framework_input = await self.to_framework(params)
|
|
47
|
+
framework_output = await self.call_framework(framework_input, params)
|
|
48
|
+
return await self.from_framework(framework_output, params)
|
|
49
|
+
|
|
50
|
+
async def handle_stream(
|
|
51
|
+
self, params: MessageSendParams
|
|
52
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
53
|
+
"""
|
|
54
|
+
Handle a streaming A2A message request.
|
|
55
|
+
|
|
56
|
+
Default implementation raises NotImplementedError. Override this method
|
|
57
|
+
in subclasses that support streaming responses.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
params: A2A protocol message parameters
|
|
61
|
+
|
|
62
|
+
Yields:
|
|
63
|
+
Server-Sent Events compatible dictionaries with 'event' and 'data' keys
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
NotImplementedError: If streaming is not supported by this adapter
|
|
67
|
+
"""
|
|
68
|
+
raise NotImplementedError(
|
|
69
|
+
f"{self.__class__.__name__} does not support streaming"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
async def to_framework(self, params: MessageSendParams) -> Any:
|
|
74
|
+
"""
|
|
75
|
+
Convert A2A message parameters to framework-specific input format.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
params: A2A protocol message parameters
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Framework-specific input (format varies by implementation)
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
async def call_framework(
|
|
87
|
+
self, framework_input: Any, params: MessageSendParams
|
|
88
|
+
) -> Any:
|
|
89
|
+
"""
|
|
90
|
+
Execute the underlying agent framework with the prepared input.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
framework_input: Framework-specific input from to_framework()
|
|
94
|
+
params: Original A2A message parameters (for context)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Framework-specific output
|
|
98
|
+
"""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
async def from_framework(
|
|
103
|
+
self, framework_output: Any, params: MessageSendParams
|
|
104
|
+
) -> Message | Task:
|
|
105
|
+
"""
|
|
106
|
+
Convert framework output to A2A Message or Task.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
framework_output: Output from call_framework()
|
|
110
|
+
params: Original A2A message parameters (for context)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A2A Message or Task response
|
|
114
|
+
"""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
def supports_streaming(self) -> bool:
|
|
118
|
+
"""
|
|
119
|
+
Check if this adapter supports streaming responses.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
True if streaming is supported, False otherwise
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
# Check if handle_stream is overridden
|
|
126
|
+
return (
|
|
127
|
+
self.__class__.handle_stream
|
|
128
|
+
!= BaseAgentAdapter.handle_stream
|
|
129
|
+
)
|
|
130
|
+
except AttributeError:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def supports_async_tasks(self) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Check if this adapter supports async task execution.
|
|
136
|
+
|
|
137
|
+
Async task execution allows the adapter to return a Task immediately
|
|
138
|
+
with state="working" and process the request in the background.
|
|
139
|
+
Clients can then poll for task status via get_task().
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if async tasks are supported, False otherwise
|
|
143
|
+
"""
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
async def get_task(self, task_id: str) -> Task | None:
|
|
147
|
+
"""
|
|
148
|
+
Get the current status of a task by ID.
|
|
149
|
+
|
|
150
|
+
This method is used for polling task status in async task execution mode.
|
|
151
|
+
Override this method in subclasses that support async tasks.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
task_id: The ID of the task to retrieve
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The Task object with current status, or None if not found
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
NotImplementedError: If async tasks are not supported by this adapter
|
|
161
|
+
"""
|
|
162
|
+
raise NotImplementedError(
|
|
163
|
+
f"{self.__class__.__name__} does not support async task execution"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
async def cancel_task(self, task_id: str) -> Task | None:
|
|
167
|
+
"""
|
|
168
|
+
Attempt to cancel a running task.
|
|
169
|
+
|
|
170
|
+
This method is used to cancel async tasks that are still in progress.
|
|
171
|
+
Override this method in subclasses that support async tasks.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
task_id: The ID of the task to cancel
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The updated Task object with state="canceled", or None if not found
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
NotImplementedError: If async tasks are not supported by this adapter
|
|
181
|
+
"""
|
|
182
|
+
raise NotImplementedError(
|
|
183
|
+
f"{self.__class__.__name__} does not support async task execution"
|
|
184
|
+
)
|
|
185
|
+
|
a2a_adapter/client.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Single-agent A2A server helpers.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for creating and serving A2A-compliant
|
|
5
|
+
agent servers using the official A2A.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import AsyncGenerator
|
|
9
|
+
|
|
10
|
+
import uvicorn
|
|
11
|
+
from a2a.server.apps import A2AStarletteApplication
|
|
12
|
+
from a2a.server.request_handlers.request_handler import RequestHandler
|
|
13
|
+
from a2a.types import UnsupportedOperationError
|
|
14
|
+
from a2a.utils.errors import ServerError
|
|
15
|
+
from a2a.server.context import ServerCallContext
|
|
16
|
+
from a2a.types import (
|
|
17
|
+
AgentCard,
|
|
18
|
+
CancelTaskRequest,
|
|
19
|
+
CancelTaskResponse,
|
|
20
|
+
DeleteTaskPushNotificationConfigParams,
|
|
21
|
+
DeleteTaskPushNotificationConfigResponse,
|
|
22
|
+
GetTaskPushNotificationConfigParams,
|
|
23
|
+
GetTaskPushNotificationConfigResponse,
|
|
24
|
+
GetTaskRequest,
|
|
25
|
+
GetTaskResponse,
|
|
26
|
+
ListTaskPushNotificationConfigParams,
|
|
27
|
+
ListTaskPushNotificationConfigResponse,
|
|
28
|
+
Message,
|
|
29
|
+
MessageSendParams,
|
|
30
|
+
SetTaskPushNotificationConfigRequest,
|
|
31
|
+
SetTaskPushNotificationConfigResponse,
|
|
32
|
+
Task,
|
|
33
|
+
TaskResubscriptionRequest,
|
|
34
|
+
TaskStatusUpdateEvent,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from .adapter import BaseAgentAdapter
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AdapterRequestHandler(RequestHandler):
|
|
41
|
+
"""
|
|
42
|
+
Wrapper that adapts BaseAgentAdapter to A2A's RequestHandler interface.
|
|
43
|
+
|
|
44
|
+
This class bridges the gap between our adapter abstraction and the
|
|
45
|
+
official A2A SDK's RequestHandler protocol.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, adapter: BaseAgentAdapter):
|
|
49
|
+
"""
|
|
50
|
+
Initialize the request handler with an adapter.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
adapter: The BaseAgentAdapter instance to wrap
|
|
54
|
+
"""
|
|
55
|
+
self.adapter = adapter
|
|
56
|
+
|
|
57
|
+
async def on_message_send(
|
|
58
|
+
self,
|
|
59
|
+
params: MessageSendParams,
|
|
60
|
+
context: ServerCallContext
|
|
61
|
+
) -> Message | Task:
|
|
62
|
+
"""
|
|
63
|
+
Handle a non-streaming message send request.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
params: A2A message parameters
|
|
67
|
+
context: Server call context
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A2A Message or Task response
|
|
71
|
+
"""
|
|
72
|
+
return await self.adapter.handle(params)
|
|
73
|
+
|
|
74
|
+
async def on_message_send_stream(
|
|
75
|
+
self,
|
|
76
|
+
params: MessageSendParams,
|
|
77
|
+
context: ServerCallContext
|
|
78
|
+
) -> AsyncGenerator[Message, None]:
|
|
79
|
+
"""
|
|
80
|
+
Handle a streaming message send request.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
params: A2A message parameters
|
|
84
|
+
context: Server call context
|
|
85
|
+
|
|
86
|
+
Yields:
|
|
87
|
+
A2A Message responses
|
|
88
|
+
"""
|
|
89
|
+
async for event in self.adapter.handle_stream(params):
|
|
90
|
+
yield event
|
|
91
|
+
|
|
92
|
+
# Task-related methods (not supported by default)
|
|
93
|
+
|
|
94
|
+
async def on_get_task(
|
|
95
|
+
self,
|
|
96
|
+
params: GetTaskRequest,
|
|
97
|
+
context: ServerCallContext
|
|
98
|
+
) -> GetTaskResponse:
|
|
99
|
+
"""Get task status - not supported."""
|
|
100
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
101
|
+
|
|
102
|
+
async def on_cancel_task(
|
|
103
|
+
self,
|
|
104
|
+
params: CancelTaskRequest,
|
|
105
|
+
context: ServerCallContext
|
|
106
|
+
) -> CancelTaskResponse:
|
|
107
|
+
"""Cancel task - not supported."""
|
|
108
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
109
|
+
|
|
110
|
+
async def on_resubscribe_to_task(
|
|
111
|
+
self,
|
|
112
|
+
params: TaskResubscriptionRequest,
|
|
113
|
+
context: ServerCallContext
|
|
114
|
+
) -> AsyncGenerator[TaskStatusUpdateEvent, None]:
|
|
115
|
+
"""Resubscribe to task - not supported."""
|
|
116
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
117
|
+
yield # Make this an async generator
|
|
118
|
+
|
|
119
|
+
# Push notification methods (not supported by default)
|
|
120
|
+
|
|
121
|
+
async def on_set_task_push_notification_config(
|
|
122
|
+
self,
|
|
123
|
+
params: SetTaskPushNotificationConfigRequest,
|
|
124
|
+
context: ServerCallContext
|
|
125
|
+
) -> SetTaskPushNotificationConfigResponse:
|
|
126
|
+
"""Set push notification config - not supported."""
|
|
127
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
128
|
+
|
|
129
|
+
async def on_get_task_push_notification_config(
|
|
130
|
+
self,
|
|
131
|
+
params: GetTaskPushNotificationConfigParams,
|
|
132
|
+
context: ServerCallContext
|
|
133
|
+
) -> GetTaskPushNotificationConfigResponse:
|
|
134
|
+
"""Get push notification config - not supported."""
|
|
135
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
136
|
+
|
|
137
|
+
async def on_list_task_push_notification_config(
|
|
138
|
+
self,
|
|
139
|
+
params: ListTaskPushNotificationConfigParams,
|
|
140
|
+
context: ServerCallContext
|
|
141
|
+
) -> ListTaskPushNotificationConfigResponse:
|
|
142
|
+
"""List push notification configs - not supported."""
|
|
143
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
144
|
+
|
|
145
|
+
async def on_delete_task_push_notification_config(
|
|
146
|
+
self,
|
|
147
|
+
params: DeleteTaskPushNotificationConfigParams,
|
|
148
|
+
context: ServerCallContext
|
|
149
|
+
) -> DeleteTaskPushNotificationConfigResponse:
|
|
150
|
+
"""Delete push notification config - not supported."""
|
|
151
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_agent_app(
|
|
155
|
+
agent_card: AgentCard,
|
|
156
|
+
adapter: BaseAgentAdapter,
|
|
157
|
+
):
|
|
158
|
+
"""
|
|
159
|
+
Build an ASGI application for a single A2A agent.
|
|
160
|
+
|
|
161
|
+
This function creates a complete A2A-compliant server application using
|
|
162
|
+
the official A2A SDK, configured with the provided agent card and adapter.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
agent_card: A2A AgentCard describing the agent's capabilities
|
|
166
|
+
adapter: BaseAgentAdapter implementation for the agent framework
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
ASGI application ready to be served
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> from a2a.types import AgentCard
|
|
173
|
+
>>> from a2a_adapter.integrations.n8n import N8nAgentAdapter
|
|
174
|
+
>>>
|
|
175
|
+
>>> card = AgentCard(
|
|
176
|
+
... name="Math Agent",
|
|
177
|
+
... description="Performs mathematical operations"
|
|
178
|
+
... )
|
|
179
|
+
>>> adapter = N8nAgentAdapter(webhook_url="https://n8n.example.com/webhook")
|
|
180
|
+
>>> app = build_agent_app(card, adapter)
|
|
181
|
+
"""
|
|
182
|
+
handler = AdapterRequestHandler(adapter)
|
|
183
|
+
|
|
184
|
+
app_builder = A2AStarletteApplication(
|
|
185
|
+
agent_card=agent_card,
|
|
186
|
+
http_handler=handler,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Build and return the actual ASGI application
|
|
190
|
+
return app_builder.build()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def serve_agent(
|
|
194
|
+
agent_card: AgentCard,
|
|
195
|
+
adapter: BaseAgentAdapter,
|
|
196
|
+
host: str = "0.0.0.0",
|
|
197
|
+
port: int = 9000,
|
|
198
|
+
log_level: str = "info",
|
|
199
|
+
**uvicorn_kwargs,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""
|
|
202
|
+
Start serving a single A2A agent.
|
|
203
|
+
|
|
204
|
+
This is a convenience function that builds the agent application and
|
|
205
|
+
starts a uvicorn server to serve it.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
agent_card: A2A AgentCard describing the agent's capabilities
|
|
209
|
+
adapter: BaseAgentAdapter implementation for the agent framework
|
|
210
|
+
host: Host address to bind to (default: "0.0.0.0")
|
|
211
|
+
port: Port to listen on (default: 9000)
|
|
212
|
+
log_level: Logging level (default: "info")
|
|
213
|
+
**uvicorn_kwargs: Additional arguments to pass to uvicorn.run()
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> from a2a.types import AgentCard
|
|
217
|
+
>>> from a2a_adapter import load_a2a_agent, serve_agent
|
|
218
|
+
>>>
|
|
219
|
+
>>> adapter = await load_a2a_agent({
|
|
220
|
+
... "adapter": "n8n",
|
|
221
|
+
... "webhook_url": "https://n8n.example.com/webhook"
|
|
222
|
+
... })
|
|
223
|
+
>>> card = AgentCard(name="My Agent", description="...")
|
|
224
|
+
>>> serve_agent(card, adapter, port=9000)
|
|
225
|
+
"""
|
|
226
|
+
app = build_agent_app(agent_card, adapter)
|
|
227
|
+
|
|
228
|
+
# Use uvicorn.run directly (not inside asyncio.run context)
|
|
229
|
+
uvicorn.run(
|
|
230
|
+
app,
|
|
231
|
+
host=host,
|
|
232
|
+
port=port,
|
|
233
|
+
log_level=log_level,
|
|
234
|
+
**uvicorn_kwargs,
|
|
235
|
+
)
|
|
236
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework-specific adapter implementations.
|
|
3
|
+
|
|
4
|
+
This package contains concrete adapter implementations for various agent frameworks:
|
|
5
|
+
- n8n: HTTP webhook-based workflows
|
|
6
|
+
- CrewAI: Multi-agent collaboration framework
|
|
7
|
+
- LangChain: LLM application framework with LCEL support
|
|
8
|
+
- Callable: Generic Python async function adapter
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"N8nAgentAdapter",
|
|
13
|
+
"CrewAIAgentAdapter",
|
|
14
|
+
"LangChainAgentAdapter",
|
|
15
|
+
"CallableAgentAdapter",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# Lazy imports to avoid requiring all optional dependencies
|
|
19
|
+
def __getattr__(name: str):
|
|
20
|
+
if name == "N8nAgentAdapter":
|
|
21
|
+
from .n8n import N8nAgentAdapter
|
|
22
|
+
return N8nAgentAdapter
|
|
23
|
+
elif name == "CrewAIAgentAdapter":
|
|
24
|
+
from .crewai import CrewAIAgentAdapter
|
|
25
|
+
return CrewAIAgentAdapter
|
|
26
|
+
elif name == "LangChainAgentAdapter":
|
|
27
|
+
from .langchain import LangChainAgentAdapter
|
|
28
|
+
return LangChainAgentAdapter
|
|
29
|
+
elif name == "CallableAgentAdapter":
|
|
30
|
+
from .callable import CallableAgentAdapter
|
|
31
|
+
return CallableAgentAdapter
|
|
32
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
33
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic callable adapter for A2A Protocol.
|
|
3
|
+
|
|
4
|
+
This adapter allows any async Python function to be exposed as an A2A-compliant
|
|
5
|
+
agent, providing maximum flexibility for custom implementations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, AsyncIterator, Callable, Dict
|
|
10
|
+
|
|
11
|
+
from a2a.types import Message, MessageSendParams, Task, TextPart
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CallableAgentAdapter:
|
|
15
|
+
"""
|
|
16
|
+
Adapter for integrating custom async functions as A2A agents.
|
|
17
|
+
|
|
18
|
+
This adapter wraps any async callable (function, coroutine) and handles
|
|
19
|
+
the A2A protocol translation. The callable should accept a dictionary
|
|
20
|
+
input and return either a string or dictionary output.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
func: Callable,
|
|
26
|
+
supports_streaming: bool = False,
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the callable adapter.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
func: An async callable that processes the agent logic.
|
|
33
|
+
For non-streaming: Should accept Dict[str, Any] and return str or Dict.
|
|
34
|
+
For streaming: Should be an async generator yielding str chunks.
|
|
35
|
+
supports_streaming: Whether the function supports streaming (default: False)
|
|
36
|
+
"""
|
|
37
|
+
self.func = func
|
|
38
|
+
self._supports_streaming = supports_streaming
|
|
39
|
+
|
|
40
|
+
async def handle(self, params: MessageSendParams) -> Message | Task:
|
|
41
|
+
"""Handle a non-streaming A2A message request."""
|
|
42
|
+
framework_input = await self.to_framework(params)
|
|
43
|
+
framework_output = await self.call_framework(framework_input, params)
|
|
44
|
+
return await self.from_framework(framework_output, params)
|
|
45
|
+
|
|
46
|
+
async def handle_stream(
|
|
47
|
+
self, params: MessageSendParams
|
|
48
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
49
|
+
"""
|
|
50
|
+
Handle a streaming A2A message request.
|
|
51
|
+
|
|
52
|
+
The wrapped function must be an async generator for streaming to work.
|
|
53
|
+
"""
|
|
54
|
+
if not self._supports_streaming:
|
|
55
|
+
raise NotImplementedError(
|
|
56
|
+
"CallableAgentAdapter: streaming not enabled for this function"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
framework_input = await self.to_framework(params)
|
|
60
|
+
|
|
61
|
+
# Call the async generator function
|
|
62
|
+
async for chunk in self.func(framework_input):
|
|
63
|
+
# Convert chunk to string if needed
|
|
64
|
+
text = str(chunk) if not isinstance(chunk, str) else chunk
|
|
65
|
+
|
|
66
|
+
# Yield SSE-compatible event
|
|
67
|
+
if text:
|
|
68
|
+
yield {
|
|
69
|
+
"event": "message",
|
|
70
|
+
"data": json.dumps({
|
|
71
|
+
"type": "content",
|
|
72
|
+
"content": text,
|
|
73
|
+
}),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Send completion event
|
|
77
|
+
yield {
|
|
78
|
+
"event": "done",
|
|
79
|
+
"data": json.dumps({"status": "completed"}),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def supports_streaming(self) -> bool:
|
|
83
|
+
"""Check if this adapter supports streaming."""
|
|
84
|
+
return self._supports_streaming
|
|
85
|
+
|
|
86
|
+
async def to_framework(self, params: MessageSendParams) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Convert A2A message parameters to a dictionary for the callable.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
params: A2A message parameters
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary with input data for the callable
|
|
95
|
+
"""
|
|
96
|
+
# Extract text from the last user message
|
|
97
|
+
user_message = ""
|
|
98
|
+
if params.messages:
|
|
99
|
+
last_message = params.messages[-1]
|
|
100
|
+
if hasattr(last_message, "content"):
|
|
101
|
+
if isinstance(last_message.content, list):
|
|
102
|
+
# Extract text from content blocks
|
|
103
|
+
text_parts = [
|
|
104
|
+
item.text
|
|
105
|
+
for item in last_message.content
|
|
106
|
+
if hasattr(item, "text")
|
|
107
|
+
]
|
|
108
|
+
user_message = " ".join(text_parts)
|
|
109
|
+
elif isinstance(last_message.content, str):
|
|
110
|
+
user_message = last_message.content
|
|
111
|
+
|
|
112
|
+
# Build input dictionary
|
|
113
|
+
return {
|
|
114
|
+
"message": user_message,
|
|
115
|
+
"messages": params.messages,
|
|
116
|
+
"session_id": getattr(params, "session_id", None),
|
|
117
|
+
"context": getattr(params, "context", None),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async def call_framework(
|
|
121
|
+
self, framework_input: Dict[str, Any], params: MessageSendParams
|
|
122
|
+
) -> Any:
|
|
123
|
+
"""
|
|
124
|
+
Execute the callable function with the provided input.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
framework_input: Input dictionary for the function
|
|
128
|
+
params: Original A2A parameters (for context)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Function execution output
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
Exception: If function execution fails
|
|
135
|
+
"""
|
|
136
|
+
result = await self.func(framework_input)
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
async def from_framework(
|
|
140
|
+
self, framework_output: Any, params: MessageSendParams
|
|
141
|
+
) -> Message | Task:
|
|
142
|
+
"""
|
|
143
|
+
Convert callable output to A2A Message.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
framework_output: Output from the callable
|
|
147
|
+
params: Original A2A parameters
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A2A Message with the function's response
|
|
151
|
+
"""
|
|
152
|
+
# Convert output to string
|
|
153
|
+
if isinstance(framework_output, dict):
|
|
154
|
+
# If output has a 'response' or 'output' key, use that
|
|
155
|
+
if "response" in framework_output:
|
|
156
|
+
response_text = str(framework_output["response"])
|
|
157
|
+
elif "output" in framework_output:
|
|
158
|
+
response_text = str(framework_output["output"])
|
|
159
|
+
else:
|
|
160
|
+
response_text = json.dumps(framework_output, indent=2)
|
|
161
|
+
else:
|
|
162
|
+
response_text = str(framework_output)
|
|
163
|
+
|
|
164
|
+
return Message(
|
|
165
|
+
role="assistant",
|
|
166
|
+
content=[TextPart(type="text", text=response_text)],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def supports_streaming(self) -> bool:
|
|
170
|
+
"""Check if this adapter supports streaming responses."""
|
|
171
|
+
return False
|
|
172
|
+
|