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.
@@ -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
+