a2a-adapter 0.1.3__py3-none-any.whl → 0.1.5__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 +1 -1
- a2a_adapter/client.py +57 -13
- a2a_adapter/integrations/__init__.py +10 -1
- a2a_adapter/integrations/callable.py +204 -90
- a2a_adapter/integrations/crewai.py +489 -46
- a2a_adapter/integrations/langchain.py +248 -90
- a2a_adapter/integrations/langgraph.py +756 -0
- a2a_adapter/integrations/openclaw.py +1297 -0
- a2a_adapter/loader.py +101 -28
- {a2a_adapter-0.1.3.dist-info → a2a_adapter-0.1.5.dist-info}/METADATA +108 -28
- a2a_adapter-0.1.5.dist-info/RECORD +16 -0
- {a2a_adapter-0.1.3.dist-info → a2a_adapter-0.1.5.dist-info}/WHEEL +1 -1
- a2a_adapter-0.1.3.dist-info/RECORD +0 -14
- {a2a_adapter-0.1.3.dist-info → a2a_adapter-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {a2a_adapter-0.1.3.dist-info → a2a_adapter-0.1.5.dist-info}/top_level.txt +0 -0
a2a_adapter/__init__.py
CHANGED
a2a_adapter/client.py
CHANGED
|
@@ -27,6 +27,7 @@ from a2a.types import (
|
|
|
27
27
|
ListTaskPushNotificationConfigResponse,
|
|
28
28
|
Message,
|
|
29
29
|
MessageSendParams,
|
|
30
|
+
PushNotificationConfig,
|
|
30
31
|
SetTaskPushNotificationConfigRequest,
|
|
31
32
|
SetTaskPushNotificationConfigResponse,
|
|
32
33
|
Task,
|
|
@@ -43,6 +44,11 @@ class AdapterRequestHandler(RequestHandler):
|
|
|
43
44
|
|
|
44
45
|
This class bridges the gap between our adapter abstraction and the
|
|
45
46
|
official A2A SDK's RequestHandler protocol.
|
|
47
|
+
|
|
48
|
+
Supports:
|
|
49
|
+
- Basic message send (sync and async)
|
|
50
|
+
- Task get/cancel for async adapters
|
|
51
|
+
- Push notification configuration for adapters that support it
|
|
46
52
|
"""
|
|
47
53
|
|
|
48
54
|
def __init__(self, adapter: BaseAgentAdapter):
|
|
@@ -89,23 +95,37 @@ class AdapterRequestHandler(RequestHandler):
|
|
|
89
95
|
async for event in self.adapter.handle_stream(params):
|
|
90
96
|
yield event
|
|
91
97
|
|
|
92
|
-
# Task-related methods
|
|
98
|
+
# Task-related methods
|
|
93
99
|
|
|
94
100
|
async def on_get_task(
|
|
95
101
|
self,
|
|
96
102
|
params: GetTaskRequest,
|
|
97
103
|
context: ServerCallContext
|
|
98
104
|
) -> GetTaskResponse:
|
|
99
|
-
"""Get task status
|
|
100
|
-
|
|
105
|
+
"""Get task status."""
|
|
106
|
+
if not self.adapter.supports_async_tasks():
|
|
107
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
108
|
+
|
|
109
|
+
task = await self.adapter.get_task(params.id)
|
|
110
|
+
if task is None:
|
|
111
|
+
raise ServerError(error=UnsupportedOperationError(message=f"Task {params.id} not found"))
|
|
112
|
+
|
|
113
|
+
return GetTaskResponse(result=task)
|
|
101
114
|
|
|
102
115
|
async def on_cancel_task(
|
|
103
116
|
self,
|
|
104
117
|
params: CancelTaskRequest,
|
|
105
118
|
context: ServerCallContext
|
|
106
119
|
) -> CancelTaskResponse:
|
|
107
|
-
"""Cancel task
|
|
108
|
-
|
|
120
|
+
"""Cancel task."""
|
|
121
|
+
if not self.adapter.supports_async_tasks():
|
|
122
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
123
|
+
|
|
124
|
+
task = await self.adapter.cancel_task(params.id)
|
|
125
|
+
if task is None:
|
|
126
|
+
raise ServerError(error=UnsupportedOperationError(message=f"Task {params.id} not found"))
|
|
127
|
+
|
|
128
|
+
return CancelTaskResponse(result=task)
|
|
109
129
|
|
|
110
130
|
async def on_resubscribe_to_task(
|
|
111
131
|
self,
|
|
@@ -116,30 +136,47 @@ class AdapterRequestHandler(RequestHandler):
|
|
|
116
136
|
raise ServerError(error=UnsupportedOperationError())
|
|
117
137
|
yield # Make this an async generator
|
|
118
138
|
|
|
119
|
-
# Push notification methods
|
|
139
|
+
# Push notification methods
|
|
120
140
|
|
|
121
141
|
async def on_set_task_push_notification_config(
|
|
122
142
|
self,
|
|
123
143
|
params: SetTaskPushNotificationConfigRequest,
|
|
124
144
|
context: ServerCallContext
|
|
125
145
|
) -> SetTaskPushNotificationConfigResponse:
|
|
126
|
-
"""Set push notification config
|
|
127
|
-
|
|
146
|
+
"""Set push notification config."""
|
|
147
|
+
if not hasattr(self.adapter, 'supports_push_notifications') or not self.adapter.supports_push_notifications():
|
|
148
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
149
|
+
|
|
150
|
+
success = await self.adapter.set_push_notification_config(
|
|
151
|
+
params.taskId,
|
|
152
|
+
params.pushNotificationConfig
|
|
153
|
+
)
|
|
154
|
+
if not success:
|
|
155
|
+
raise ServerError(error=UnsupportedOperationError(message=f"Task {params.taskId} not found"))
|
|
156
|
+
|
|
157
|
+
return SetTaskPushNotificationConfigResponse(result=params.pushNotificationConfig)
|
|
128
158
|
|
|
129
159
|
async def on_get_task_push_notification_config(
|
|
130
160
|
self,
|
|
131
161
|
params: GetTaskPushNotificationConfigParams,
|
|
132
162
|
context: ServerCallContext
|
|
133
163
|
) -> GetTaskPushNotificationConfigResponse:
|
|
134
|
-
"""Get push notification config
|
|
135
|
-
|
|
164
|
+
"""Get push notification config."""
|
|
165
|
+
if not hasattr(self.adapter, 'supports_push_notifications') or not self.adapter.supports_push_notifications():
|
|
166
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
167
|
+
|
|
168
|
+
config = await self.adapter.get_push_notification_config(params.taskId)
|
|
169
|
+
if config is None:
|
|
170
|
+
raise ServerError(error=UnsupportedOperationError(message=f"No push config for task {params.taskId}"))
|
|
171
|
+
|
|
172
|
+
return GetTaskPushNotificationConfigResponse(result=config)
|
|
136
173
|
|
|
137
174
|
async def on_list_task_push_notification_config(
|
|
138
175
|
self,
|
|
139
176
|
params: ListTaskPushNotificationConfigParams,
|
|
140
177
|
context: ServerCallContext
|
|
141
178
|
) -> ListTaskPushNotificationConfigResponse:
|
|
142
|
-
"""List push notification configs - not supported."""
|
|
179
|
+
"""List push notification configs - not supported (would need to track all configs)."""
|
|
143
180
|
raise ServerError(error=UnsupportedOperationError())
|
|
144
181
|
|
|
145
182
|
async def on_delete_task_push_notification_config(
|
|
@@ -147,8 +184,15 @@ class AdapterRequestHandler(RequestHandler):
|
|
|
147
184
|
params: DeleteTaskPushNotificationConfigParams,
|
|
148
185
|
context: ServerCallContext
|
|
149
186
|
) -> DeleteTaskPushNotificationConfigResponse:
|
|
150
|
-
"""Delete push notification config
|
|
151
|
-
|
|
187
|
+
"""Delete push notification config."""
|
|
188
|
+
if not hasattr(self.adapter, 'supports_push_notifications') or not self.adapter.supports_push_notifications():
|
|
189
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
190
|
+
|
|
191
|
+
success = await self.adapter.delete_push_notification_config(params.taskId)
|
|
192
|
+
if not success:
|
|
193
|
+
raise ServerError(error=UnsupportedOperationError(message=f"No push config for task {params.taskId}"))
|
|
194
|
+
|
|
195
|
+
return DeleteTaskPushNotificationConfigResponse(result={})
|
|
152
196
|
|
|
153
197
|
|
|
154
198
|
def build_agent_app(
|
|
@@ -5,14 +5,18 @@ This package contains concrete adapter implementations for various agent framewo
|
|
|
5
5
|
- n8n: HTTP webhook-based workflows
|
|
6
6
|
- CrewAI: Multi-agent collaboration framework
|
|
7
7
|
- LangChain: LLM application framework with LCEL support
|
|
8
|
+
- LangGraph: Stateful workflow orchestration framework
|
|
8
9
|
- Callable: Generic Python async function adapter
|
|
10
|
+
- OpenClaw: Personal AI super agent CLI wrapper
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
__all__ = [
|
|
12
14
|
"N8nAgentAdapter",
|
|
13
15
|
"CrewAIAgentAdapter",
|
|
14
16
|
"LangChainAgentAdapter",
|
|
17
|
+
"LangGraphAgentAdapter",
|
|
15
18
|
"CallableAgentAdapter",
|
|
19
|
+
"OpenClawAgentAdapter",
|
|
16
20
|
]
|
|
17
21
|
|
|
18
22
|
# Lazy imports to avoid requiring all optional dependencies
|
|
@@ -26,8 +30,13 @@ def __getattr__(name: str):
|
|
|
26
30
|
elif name == "LangChainAgentAdapter":
|
|
27
31
|
from .langchain import LangChainAgentAdapter
|
|
28
32
|
return LangChainAgentAdapter
|
|
33
|
+
elif name == "LangGraphAgentAdapter":
|
|
34
|
+
from .langgraph import LangGraphAgentAdapter
|
|
35
|
+
return LangGraphAgentAdapter
|
|
29
36
|
elif name == "CallableAgentAdapter":
|
|
30
37
|
from .callable import CallableAgentAdapter
|
|
31
38
|
return CallableAgentAdapter
|
|
39
|
+
elif name == "OpenClawAgentAdapter":
|
|
40
|
+
from .openclaw import OpenClawAgentAdapter
|
|
41
|
+
return OpenClawAgentAdapter
|
|
32
42
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
33
|
-
|
|
@@ -6,18 +6,51 @@ agent, providing maximum flexibility for custom implementations.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
9
11
|
from typing import Any, AsyncIterator, Callable, Dict
|
|
10
12
|
|
|
11
|
-
from a2a.types import
|
|
13
|
+
from a2a.types import (
|
|
14
|
+
Message,
|
|
15
|
+
MessageSendParams,
|
|
16
|
+
Task,
|
|
17
|
+
TextPart,
|
|
18
|
+
Role,
|
|
19
|
+
Part,
|
|
20
|
+
)
|
|
21
|
+
from ..adapter import BaseAgentAdapter
|
|
12
22
|
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
|
|
26
|
+
class CallableAgentAdapter(BaseAgentAdapter):
|
|
15
27
|
"""
|
|
16
28
|
Adapter for integrating custom async functions as A2A agents.
|
|
17
|
-
|
|
29
|
+
|
|
18
30
|
This adapter wraps any async callable (function, coroutine) and handles
|
|
19
31
|
the A2A protocol translation. The callable should accept a dictionary
|
|
20
32
|
input and return either a string or dictionary output.
|
|
33
|
+
|
|
34
|
+
For streaming support, the callable should be an async generator that
|
|
35
|
+
yields string chunks.
|
|
36
|
+
|
|
37
|
+
Example (non-streaming):
|
|
38
|
+
>>> async def my_agent(inputs: dict) -> str:
|
|
39
|
+
... message = inputs["message"]
|
|
40
|
+
... return f"Processed: {message}"
|
|
41
|
+
>>>
|
|
42
|
+
>>> adapter = CallableAgentAdapter(func=my_agent)
|
|
43
|
+
|
|
44
|
+
Example (streaming):
|
|
45
|
+
>>> async def my_streaming_agent(inputs: dict):
|
|
46
|
+
... message = inputs["message"]
|
|
47
|
+
... for word in message.split():
|
|
48
|
+
... yield word + " "
|
|
49
|
+
>>>
|
|
50
|
+
>>> adapter = CallableAgentAdapter(
|
|
51
|
+
... func=my_streaming_agent,
|
|
52
|
+
... supports_streaming=True
|
|
53
|
+
... )
|
|
21
54
|
"""
|
|
22
55
|
|
|
23
56
|
def __init__(
|
|
@@ -27,7 +60,7 @@ class CallableAgentAdapter:
|
|
|
27
60
|
):
|
|
28
61
|
"""
|
|
29
62
|
Initialize the callable adapter.
|
|
30
|
-
|
|
63
|
+
|
|
31
64
|
Args:
|
|
32
65
|
func: An async callable that processes the agent logic.
|
|
33
66
|
For non-streaming: Should accept Dict[str, Any] and return str or Dict.
|
|
@@ -37,136 +70,217 @@ class CallableAgentAdapter:
|
|
|
37
70
|
self.func = func
|
|
38
71
|
self._supports_streaming = supports_streaming
|
|
39
72
|
|
|
40
|
-
|
|
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
|
|
73
|
+
# ---------- Input mapping ----------
|
|
85
74
|
|
|
86
75
|
async def to_framework(self, params: MessageSendParams) -> Dict[str, Any]:
|
|
87
76
|
"""
|
|
88
77
|
Convert A2A message parameters to a dictionary for the callable.
|
|
89
|
-
|
|
78
|
+
|
|
79
|
+
Extracts the user's message and relevant metadata.
|
|
80
|
+
|
|
90
81
|
Args:
|
|
91
82
|
params: A2A message parameters
|
|
92
|
-
|
|
83
|
+
|
|
93
84
|
Returns:
|
|
94
85
|
Dictionary with input data for the callable
|
|
95
86
|
"""
|
|
96
|
-
# Extract text from the last user message
|
|
97
87
|
user_message = ""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
88
|
+
|
|
89
|
+
# Extract message from A2A params (new format with message.parts)
|
|
90
|
+
if hasattr(params, "message") and params.message:
|
|
91
|
+
msg = params.message
|
|
92
|
+
if hasattr(msg, "parts") and msg.parts:
|
|
93
|
+
text_parts = []
|
|
94
|
+
for part in msg.parts:
|
|
95
|
+
# Handle Part(root=TextPart(...)) structure
|
|
96
|
+
if hasattr(part, "root") and hasattr(part.root, "text"):
|
|
97
|
+
text_parts.append(part.root.text)
|
|
98
|
+
# Handle direct TextPart
|
|
99
|
+
elif hasattr(part, "text"):
|
|
100
|
+
text_parts.append(part.text)
|
|
101
|
+
user_message = self._join_text_parts(text_parts)
|
|
102
|
+
|
|
103
|
+
# Legacy support for messages array (deprecated)
|
|
104
|
+
elif getattr(params, "messages", None):
|
|
105
|
+
last = params.messages[-1]
|
|
106
|
+
content = getattr(last, "content", "")
|
|
107
|
+
if isinstance(content, str):
|
|
108
|
+
user_message = content.strip()
|
|
109
|
+
elif isinstance(content, list):
|
|
110
|
+
text_parts = []
|
|
111
|
+
for item in content:
|
|
112
|
+
txt = getattr(item, "text", None)
|
|
113
|
+
if txt and isinstance(txt, str) and txt.strip():
|
|
114
|
+
text_parts.append(txt.strip())
|
|
115
|
+
user_message = self._join_text_parts(text_parts)
|
|
116
|
+
|
|
117
|
+
# Extract metadata
|
|
118
|
+
context_id = self._extract_context_id(params)
|
|
119
|
+
|
|
120
|
+
# Build input dictionary with useful fields
|
|
113
121
|
return {
|
|
114
122
|
"message": user_message,
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"context": getattr(params, "context", None),
|
|
123
|
+
"context_id": context_id,
|
|
124
|
+
"params": params, # Full params for advanced use cases
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _join_text_parts(parts: list[str]) -> str:
|
|
129
|
+
"""Join text parts into a single string."""
|
|
130
|
+
if not parts:
|
|
131
|
+
return ""
|
|
132
|
+
text = " ".join(p.strip() for p in parts if p)
|
|
133
|
+
return text.strip()
|
|
134
|
+
|
|
135
|
+
def _extract_context_id(self, params: MessageSendParams) -> str | None:
|
|
136
|
+
"""Extract context_id from MessageSendParams."""
|
|
137
|
+
if hasattr(params, "message") and params.message:
|
|
138
|
+
return getattr(params.message, "context_id", None)
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
# ---------- Framework call ----------
|
|
142
|
+
|
|
120
143
|
async def call_framework(
|
|
121
144
|
self, framework_input: Dict[str, Any], params: MessageSendParams
|
|
122
145
|
) -> Any:
|
|
123
146
|
"""
|
|
124
147
|
Execute the callable function with the provided input.
|
|
125
|
-
|
|
148
|
+
|
|
126
149
|
Args:
|
|
127
150
|
framework_input: Input dictionary for the function
|
|
128
151
|
params: Original A2A parameters (for context)
|
|
129
|
-
|
|
152
|
+
|
|
130
153
|
Returns:
|
|
131
154
|
Function execution output
|
|
132
|
-
|
|
155
|
+
|
|
133
156
|
Raises:
|
|
134
157
|
Exception: If function execution fails
|
|
135
158
|
"""
|
|
159
|
+
logger.debug("Invoking callable with input keys: %s", list(framework_input.keys()))
|
|
136
160
|
result = await self.func(framework_input)
|
|
161
|
+
logger.debug("Callable returned: %s", type(result).__name__)
|
|
137
162
|
return result
|
|
138
163
|
|
|
164
|
+
# ---------- Output mapping ----------
|
|
165
|
+
|
|
139
166
|
async def from_framework(
|
|
140
167
|
self, framework_output: Any, params: MessageSendParams
|
|
141
168
|
) -> Message | Task:
|
|
142
169
|
"""
|
|
143
170
|
Convert callable output to A2A Message.
|
|
144
|
-
|
|
171
|
+
|
|
145
172
|
Args:
|
|
146
173
|
framework_output: Output from the callable
|
|
147
174
|
params: Original A2A parameters
|
|
148
|
-
|
|
175
|
+
|
|
149
176
|
Returns:
|
|
150
177
|
A2A Message with the function's response
|
|
151
178
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
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)
|
|
179
|
+
response_text = self._extract_output_text(framework_output)
|
|
180
|
+
context_id = self._extract_context_id(params)
|
|
163
181
|
|
|
164
182
|
return Message(
|
|
165
|
-
role=
|
|
166
|
-
|
|
183
|
+
role=Role.agent,
|
|
184
|
+
message_id=str(uuid.uuid4()),
|
|
185
|
+
context_id=context_id,
|
|
186
|
+
parts=[Part(root=TextPart(text=response_text))],
|
|
167
187
|
)
|
|
168
188
|
|
|
189
|
+
def _extract_output_text(self, framework_output: Any) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Extract text content from callable output.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
framework_output: Output from the callable
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Extracted text string
|
|
198
|
+
"""
|
|
199
|
+
# Dictionary output
|
|
200
|
+
if isinstance(framework_output, dict):
|
|
201
|
+
# Try common output keys
|
|
202
|
+
for key in ["response", "output", "result", "answer", "text", "message"]:
|
|
203
|
+
if key in framework_output:
|
|
204
|
+
return str(framework_output[key])
|
|
205
|
+
# Fallback: serialize as JSON
|
|
206
|
+
return json.dumps(framework_output, indent=2)
|
|
207
|
+
|
|
208
|
+
# String or other type - convert to string
|
|
209
|
+
return str(framework_output)
|
|
210
|
+
|
|
211
|
+
# ---------- Streaming support ----------
|
|
212
|
+
|
|
213
|
+
async def handle_stream(
|
|
214
|
+
self, params: MessageSendParams
|
|
215
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
216
|
+
"""
|
|
217
|
+
Handle a streaming A2A message request.
|
|
218
|
+
|
|
219
|
+
The wrapped function must be an async generator for streaming to work.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
params: A2A message parameters
|
|
223
|
+
|
|
224
|
+
Yields:
|
|
225
|
+
Server-Sent Events compatible dictionaries with streaming chunks
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
NotImplementedError: If streaming is not enabled for this adapter
|
|
229
|
+
"""
|
|
230
|
+
if not self._supports_streaming:
|
|
231
|
+
raise NotImplementedError(
|
|
232
|
+
"CallableAgentAdapter: streaming not enabled for this function. "
|
|
233
|
+
"Initialize with supports_streaming=True and provide an async generator."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
framework_input = await self.to_framework(params)
|
|
237
|
+
context_id = self._extract_context_id(params)
|
|
238
|
+
message_id = str(uuid.uuid4())
|
|
239
|
+
|
|
240
|
+
logger.debug("Starting streaming call")
|
|
241
|
+
|
|
242
|
+
accumulated_text = ""
|
|
243
|
+
|
|
244
|
+
# Call the async generator function
|
|
245
|
+
async for chunk in self.func(framework_input):
|
|
246
|
+
# Convert chunk to string if needed
|
|
247
|
+
text = str(chunk) if not isinstance(chunk, str) else chunk
|
|
248
|
+
|
|
249
|
+
if text:
|
|
250
|
+
accumulated_text += text
|
|
251
|
+
# Yield SSE-compatible event
|
|
252
|
+
yield {
|
|
253
|
+
"event": "message",
|
|
254
|
+
"data": json.dumps({
|
|
255
|
+
"type": "content",
|
|
256
|
+
"content": text,
|
|
257
|
+
}),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Send final message with complete response
|
|
261
|
+
final_message = Message(
|
|
262
|
+
role=Role.agent,
|
|
263
|
+
message_id=message_id,
|
|
264
|
+
context_id=context_id,
|
|
265
|
+
parts=[Part(root=TextPart(text=accumulated_text))],
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Send completion event
|
|
269
|
+
yield {
|
|
270
|
+
"event": "done",
|
|
271
|
+
"data": json.dumps({
|
|
272
|
+
"status": "completed",
|
|
273
|
+
"message": final_message.model_dump() if hasattr(final_message, "model_dump") else str(final_message),
|
|
274
|
+
}),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
logger.debug("Streaming call completed")
|
|
278
|
+
|
|
169
279
|
def supports_streaming(self) -> bool:
|
|
170
|
-
"""
|
|
171
|
-
|
|
280
|
+
"""
|
|
281
|
+
Check if this adapter supports streaming.
|
|
172
282
|
|
|
283
|
+
Returns:
|
|
284
|
+
True if streaming is enabled, False otherwise
|
|
285
|
+
"""
|
|
286
|
+
return self._supports_streaming
|