sunholo 0.143.1__py3-none-any.whl → 0.143.3__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.
- sunholo/a2a/__init__.py +27 -0
- sunholo/a2a/agent_card.py +345 -0
- sunholo/a2a/task_manager.py +480 -0
- sunholo/a2a/vac_a2a_agent.py +383 -0
- sunholo/agents/flask/vac_routes.py +247 -2
- {sunholo-0.143.1.dist-info → sunholo-0.143.3.dist-info}/METADATA +4 -2
- {sunholo-0.143.1.dist-info → sunholo-0.143.3.dist-info}/RECORD +11 -7
- {sunholo-0.143.1.dist-info → sunholo-0.143.3.dist-info}/WHEEL +0 -0
- {sunholo-0.143.1.dist-info → sunholo-0.143.3.dist-info}/entry_points.txt +0 -0
- {sunholo-0.143.1.dist-info → sunholo-0.143.3.dist-info}/licenses/LICENSE.txt +0 -0
- {sunholo-0.143.1.dist-info → sunholo-0.143.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,383 @@
|
|
1
|
+
# Copyright [2024] [Holosun ApS]
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""
|
16
|
+
A2A Agent wrapper for VAC functionality.
|
17
|
+
Implements the Agent-to-Agent protocol for Sunholo VACs.
|
18
|
+
"""
|
19
|
+
|
20
|
+
import asyncio
|
21
|
+
import json
|
22
|
+
from typing import Dict, List, Any, Optional, Callable, AsyncGenerator
|
23
|
+
from ..custom_logging import log
|
24
|
+
from .agent_card import AgentCardGenerator
|
25
|
+
from .task_manager import A2ATaskManager, A2ATask
|
26
|
+
|
27
|
+
try:
|
28
|
+
# Import A2A Python SDK components when available
|
29
|
+
from a2a import Agent, Task, Message
|
30
|
+
A2A_AVAILABLE = True
|
31
|
+
except ImportError:
|
32
|
+
# Create placeholder classes for development
|
33
|
+
Agent = None
|
34
|
+
Task = None
|
35
|
+
Message = None
|
36
|
+
A2A_AVAILABLE = False
|
37
|
+
|
38
|
+
|
39
|
+
class VACA2AAgent:
|
40
|
+
"""
|
41
|
+
A2A Agent implementation for Sunholo VACs.
|
42
|
+
|
43
|
+
This class wraps VAC functionality to be compatible with the
|
44
|
+
Agent-to-Agent protocol, allowing VACs to participate in A2A ecosystems.
|
45
|
+
"""
|
46
|
+
|
47
|
+
def __init__(self,
|
48
|
+
base_url: str,
|
49
|
+
stream_interpreter: Callable,
|
50
|
+
vac_interpreter: Optional[Callable] = None,
|
51
|
+
vac_names: Optional[List[str]] = None,
|
52
|
+
agent_config: Optional[Dict[str, Any]] = None):
|
53
|
+
"""
|
54
|
+
Initialize the A2A agent.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
base_url: Base URL where the agent is hosted
|
58
|
+
stream_interpreter: Function for streaming VAC interactions
|
59
|
+
vac_interpreter: Function for static VAC interactions (optional)
|
60
|
+
vac_names: List of VAC names to expose (discovers all if None)
|
61
|
+
agent_config: Additional agent configuration
|
62
|
+
"""
|
63
|
+
if not A2A_AVAILABLE:
|
64
|
+
log.warning("A2A Python SDK not available. Install with: pip install a2a-python")
|
65
|
+
|
66
|
+
self.base_url = base_url.rstrip('/')
|
67
|
+
self.stream_interpreter = stream_interpreter
|
68
|
+
self.vac_interpreter = vac_interpreter
|
69
|
+
self.agent_config = agent_config or {}
|
70
|
+
|
71
|
+
# Initialize components
|
72
|
+
self.agent_card_generator = AgentCardGenerator(self.base_url)
|
73
|
+
self.task_manager = A2ATaskManager(stream_interpreter, vac_interpreter)
|
74
|
+
|
75
|
+
# Discover VACs
|
76
|
+
self.vac_names = vac_names or self.agent_card_generator._discover_vacs()
|
77
|
+
if not self.vac_names:
|
78
|
+
log.warning("No VACs discovered. Agent will have no skills.")
|
79
|
+
|
80
|
+
# Generate agent card
|
81
|
+
self.agent_card = self.agent_card_generator.generate_agent_card(self.vac_names)
|
82
|
+
|
83
|
+
log.info(f"Initialized A2A agent with {len(self.vac_names)} VACs: {self.vac_names}")
|
84
|
+
|
85
|
+
def get_agent_card(self) -> Dict[str, Any]:
|
86
|
+
"""
|
87
|
+
Get the agent card for A2A discovery.
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
Agent card dictionary
|
91
|
+
"""
|
92
|
+
return self.agent_card
|
93
|
+
|
94
|
+
def get_discovery_endpoints(self) -> Dict[str, str]:
|
95
|
+
"""
|
96
|
+
Get the A2A discovery endpoint paths.
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
Dictionary mapping endpoint names to their paths
|
100
|
+
"""
|
101
|
+
return self.agent_card_generator.generate_discovery_endpoints()
|
102
|
+
|
103
|
+
async def handle_task_send(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
104
|
+
"""
|
105
|
+
Handle A2A task send request.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
request_data: JSON-RPC request data
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
JSON-RPC response data
|
112
|
+
"""
|
113
|
+
try:
|
114
|
+
# Extract parameters from JSON-RPC request
|
115
|
+
params = request_data.get("params", {})
|
116
|
+
skill_name = params.get("skillName")
|
117
|
+
input_data = params.get("input", {})
|
118
|
+
client_metadata = params.get("clientMetadata", {})
|
119
|
+
|
120
|
+
if not skill_name:
|
121
|
+
return self._create_error_response(
|
122
|
+
request_data.get("id"),
|
123
|
+
-32602, # Invalid params
|
124
|
+
"Missing required parameter: skillName"
|
125
|
+
)
|
126
|
+
|
127
|
+
# Validate skill exists
|
128
|
+
available_skills = [skill["name"] for skill in self.agent_card["skills"]]
|
129
|
+
if skill_name not in available_skills:
|
130
|
+
return self._create_error_response(
|
131
|
+
request_data.get("id"),
|
132
|
+
-32601, # Method not found
|
133
|
+
f"Unknown skill: {skill_name}. Available skills: {available_skills}"
|
134
|
+
)
|
135
|
+
|
136
|
+
# Create and start task
|
137
|
+
task = await self.task_manager.create_task(skill_name, input_data, client_metadata)
|
138
|
+
|
139
|
+
# Return task creation response
|
140
|
+
return {
|
141
|
+
"jsonrpc": "2.0",
|
142
|
+
"result": {
|
143
|
+
"taskId": task.task_id,
|
144
|
+
"state": task.state.value,
|
145
|
+
"createdAt": task.created_at.isoformat(),
|
146
|
+
"estimatedDuration": self._estimate_task_duration(skill_name, input_data)
|
147
|
+
},
|
148
|
+
"id": request_data.get("id")
|
149
|
+
}
|
150
|
+
|
151
|
+
except Exception as e:
|
152
|
+
log.error(f"Error handling task send: {e}")
|
153
|
+
return self._create_error_response(
|
154
|
+
request_data.get("id"),
|
155
|
+
-32603, # Internal error
|
156
|
+
f"Internal error: {str(e)}"
|
157
|
+
)
|
158
|
+
|
159
|
+
async def handle_task_send_subscribe(self, request_data: Dict[str, Any]) -> AsyncGenerator[str, None]:
|
160
|
+
"""
|
161
|
+
Handle A2A task send with subscription (SSE).
|
162
|
+
|
163
|
+
Args:
|
164
|
+
request_data: JSON-RPC request data
|
165
|
+
|
166
|
+
Yields:
|
167
|
+
Server-sent event data strings
|
168
|
+
"""
|
169
|
+
try:
|
170
|
+
# First, send the task
|
171
|
+
response = await self.handle_task_send(request_data)
|
172
|
+
|
173
|
+
# Send initial response
|
174
|
+
yield f"data: {json.dumps(response)}\n\n"
|
175
|
+
|
176
|
+
# If task creation failed, stop here
|
177
|
+
if "error" in response:
|
178
|
+
return
|
179
|
+
|
180
|
+
# Get task ID and subscribe to updates
|
181
|
+
task_id = response["result"]["taskId"]
|
182
|
+
|
183
|
+
# Subscribe to task updates
|
184
|
+
async for update in self.task_manager.subscribe_to_task(task_id):
|
185
|
+
if update is None:
|
186
|
+
break
|
187
|
+
|
188
|
+
# Format as SSE
|
189
|
+
sse_data = {
|
190
|
+
"type": "task_update",
|
191
|
+
"taskId": task_id,
|
192
|
+
"data": update
|
193
|
+
}
|
194
|
+
|
195
|
+
yield f"data: {json.dumps(sse_data)}\n\n"
|
196
|
+
|
197
|
+
# Stop if task is complete
|
198
|
+
if update.get("state") in ["completed", "failed", "canceled"]:
|
199
|
+
break
|
200
|
+
|
201
|
+
except Exception as e:
|
202
|
+
log.error(f"Error in task send subscribe: {e}")
|
203
|
+
error_event = {
|
204
|
+
"type": "error",
|
205
|
+
"error": {
|
206
|
+
"code": -32603,
|
207
|
+
"message": f"Internal error: {str(e)}"
|
208
|
+
}
|
209
|
+
}
|
210
|
+
yield f"data: {json.dumps(error_event)}\n\n"
|
211
|
+
|
212
|
+
async def handle_task_get(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
213
|
+
"""
|
214
|
+
Handle A2A task get request.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
request_data: JSON-RPC request data
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
JSON-RPC response data
|
221
|
+
"""
|
222
|
+
try:
|
223
|
+
params = request_data.get("params", {})
|
224
|
+
task_id = params.get("taskId")
|
225
|
+
|
226
|
+
if not task_id:
|
227
|
+
return self._create_error_response(
|
228
|
+
request_data.get("id"),
|
229
|
+
-32602, # Invalid params
|
230
|
+
"Missing required parameter: taskId"
|
231
|
+
)
|
232
|
+
|
233
|
+
task = await self.task_manager.get_task(task_id)
|
234
|
+
if not task:
|
235
|
+
return self._create_error_response(
|
236
|
+
request_data.get("id"),
|
237
|
+
-32602, # Invalid params
|
238
|
+
f"Task not found: {task_id}"
|
239
|
+
)
|
240
|
+
|
241
|
+
return {
|
242
|
+
"jsonrpc": "2.0",
|
243
|
+
"result": task.to_dict(),
|
244
|
+
"id": request_data.get("id")
|
245
|
+
}
|
246
|
+
|
247
|
+
except Exception as e:
|
248
|
+
log.error(f"Error handling task get: {e}")
|
249
|
+
return self._create_error_response(
|
250
|
+
request_data.get("id"),
|
251
|
+
-32603, # Internal error
|
252
|
+
f"Internal error: {str(e)}"
|
253
|
+
)
|
254
|
+
|
255
|
+
async def handle_task_cancel(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
256
|
+
"""
|
257
|
+
Handle A2A task cancel request.
|
258
|
+
|
259
|
+
Args:
|
260
|
+
request_data: JSON-RPC request data
|
261
|
+
|
262
|
+
Returns:
|
263
|
+
JSON-RPC response data
|
264
|
+
"""
|
265
|
+
try:
|
266
|
+
params = request_data.get("params", {})
|
267
|
+
task_id = params.get("taskId")
|
268
|
+
|
269
|
+
if not task_id:
|
270
|
+
return self._create_error_response(
|
271
|
+
request_data.get("id"),
|
272
|
+
-32602, # Invalid params
|
273
|
+
"Missing required parameter: taskId"
|
274
|
+
)
|
275
|
+
|
276
|
+
success = await self.task_manager.cancel_task(task_id)
|
277
|
+
|
278
|
+
if not success:
|
279
|
+
return self._create_error_response(
|
280
|
+
request_data.get("id"),
|
281
|
+
-32602, # Invalid params
|
282
|
+
f"Cannot cancel task: {task_id} (not found or already completed)"
|
283
|
+
)
|
284
|
+
|
285
|
+
task = await self.task_manager.get_task(task_id)
|
286
|
+
|
287
|
+
return {
|
288
|
+
"jsonrpc": "2.0",
|
289
|
+
"result": {
|
290
|
+
"taskId": task_id,
|
291
|
+
"state": task.state.value if task else "canceled",
|
292
|
+
"canceledAt": task.updated_at.isoformat() if task else None
|
293
|
+
},
|
294
|
+
"id": request_data.get("id")
|
295
|
+
}
|
296
|
+
|
297
|
+
except Exception as e:
|
298
|
+
log.error(f"Error handling task cancel: {e}")
|
299
|
+
return self._create_error_response(
|
300
|
+
request_data.get("id"),
|
301
|
+
-32603, # Internal error
|
302
|
+
f"Internal error: {str(e)}"
|
303
|
+
)
|
304
|
+
|
305
|
+
async def handle_push_notification_set(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
306
|
+
"""
|
307
|
+
Handle A2A push notification settings.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
request_data: JSON-RPC request data
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
JSON-RPC response data
|
314
|
+
"""
|
315
|
+
# For now, this is a placeholder
|
316
|
+
# In a full implementation, this would configure push notifications
|
317
|
+
|
318
|
+
return {
|
319
|
+
"jsonrpc": "2.0",
|
320
|
+
"result": {
|
321
|
+
"status": "not_implemented",
|
322
|
+
"message": "Push notifications not yet implemented"
|
323
|
+
},
|
324
|
+
"id": request_data.get("id")
|
325
|
+
}
|
326
|
+
|
327
|
+
def _create_error_response(self, request_id: Any, error_code: int, error_message: str) -> Dict[str, Any]:
|
328
|
+
"""
|
329
|
+
Create a JSON-RPC error response.
|
330
|
+
|
331
|
+
Args:
|
332
|
+
request_id: The request ID
|
333
|
+
error_code: JSON-RPC error code
|
334
|
+
error_message: Error message
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
JSON-RPC error response
|
338
|
+
"""
|
339
|
+
return {
|
340
|
+
"jsonrpc": "2.0",
|
341
|
+
"error": {
|
342
|
+
"code": error_code,
|
343
|
+
"message": error_message
|
344
|
+
},
|
345
|
+
"id": request_id
|
346
|
+
}
|
347
|
+
|
348
|
+
def _estimate_task_duration(self, skill_name: str, input_data: Dict[str, Any]) -> float:
|
349
|
+
"""
|
350
|
+
Estimate task duration in seconds.
|
351
|
+
|
352
|
+
Args:
|
353
|
+
skill_name: Name of the skill
|
354
|
+
input_data: Input parameters
|
355
|
+
|
356
|
+
Returns:
|
357
|
+
Estimated duration in seconds
|
358
|
+
"""
|
359
|
+
# Simple heuristic based on skill type
|
360
|
+
if "stream" in skill_name:
|
361
|
+
return 30.0 # Streaming typically takes longer
|
362
|
+
elif "memory_search" in skill_name:
|
363
|
+
return 5.0 # Memory searches are usually quick
|
364
|
+
else:
|
365
|
+
return 15.0 # Default for queries
|
366
|
+
|
367
|
+
def get_stats(self) -> Dict[str, Any]:
|
368
|
+
"""
|
369
|
+
Get agent statistics.
|
370
|
+
|
371
|
+
Returns:
|
372
|
+
Dictionary with agent statistics
|
373
|
+
"""
|
374
|
+
return {
|
375
|
+
"agent_name": self.agent_card["name"],
|
376
|
+
"vac_count": len(self.vac_names),
|
377
|
+
"skill_count": len(self.agent_card["skills"]),
|
378
|
+
"active_tasks": len([t for t in self.task_manager.tasks.values()
|
379
|
+
if t.state.value in ["submitted", "working"]]),
|
380
|
+
"total_tasks": len(self.task_manager.tasks),
|
381
|
+
"base_url": self.base_url,
|
382
|
+
"a2a_available": A2A_AVAILABLE
|
383
|
+
}
|
@@ -45,6 +45,11 @@ except ImportError:
|
|
45
45
|
InitializationOptions = None
|
46
46
|
JSONRPCMessage = None
|
47
47
|
|
48
|
+
try:
|
49
|
+
from ...a2a.vac_a2a_agent import VACA2AAgent
|
50
|
+
except (ImportError, SyntaxError):
|
51
|
+
VACA2AAgent = None
|
52
|
+
|
48
53
|
|
49
54
|
# Cache dictionary to store validated API keys
|
50
55
|
api_key_cache = {}
|
@@ -81,7 +86,9 @@ if __name__ == "__main__":
|
|
81
86
|
mcp_servers: List[Dict[str, Any]] = None,
|
82
87
|
async_stream:bool=False,
|
83
88
|
add_langfuse_eval:bool=True,
|
84
|
-
enable_mcp_server:bool=False
|
89
|
+
enable_mcp_server:bool=False,
|
90
|
+
enable_a2a_agent:bool=False,
|
91
|
+
a2a_vac_names: List[str] = None):
|
85
92
|
self.app = app
|
86
93
|
self.stream_interpreter = stream_interpreter
|
87
94
|
self.vac_interpreter = vac_interpreter or partial(self.vac_interpreter_default)
|
@@ -102,6 +109,15 @@ if __name__ == "__main__":
|
|
102
109
|
vac_interpreter=self.vac_interpreter
|
103
110
|
)
|
104
111
|
|
112
|
+
# A2A agent initialization
|
113
|
+
self.enable_a2a_agent = enable_a2a_agent
|
114
|
+
self.vac_a2a_agent = None
|
115
|
+
self.a2a_vac_names = a2a_vac_names
|
116
|
+
if self.enable_a2a_agent and VACA2AAgent:
|
117
|
+
# Extract base URL from request context during route handling
|
118
|
+
# For now, initialize with placeholder - will be updated in route handlers
|
119
|
+
self.vac_a2a_agent = None # Initialized lazily in route handlers
|
120
|
+
|
105
121
|
self.additional_routes = additional_routes if additional_routes is not None else []
|
106
122
|
self.async_stream = async_stream
|
107
123
|
self.add_langfuse_eval = add_langfuse_eval
|
@@ -177,6 +193,15 @@ if __name__ == "__main__":
|
|
177
193
|
# MCP server endpoint
|
178
194
|
if self.enable_mcp_server and self.vac_mcp_server:
|
179
195
|
self.app.route('/mcp', methods=['POST', 'GET'])(self.handle_mcp_server)
|
196
|
+
|
197
|
+
# A2A agent endpoints
|
198
|
+
if self.enable_a2a_agent:
|
199
|
+
self.app.route('/.well-known/agent.json', methods=['GET'])(self.handle_a2a_agent_card)
|
200
|
+
self.app.route('/a2a/tasks/send', methods=['POST'])(self.handle_a2a_task_send)
|
201
|
+
self.app.route('/a2a/tasks/sendSubscribe', methods=['POST'])(self.handle_a2a_task_send_subscribe)
|
202
|
+
self.app.route('/a2a/tasks/get', methods=['POST'])(self.handle_a2a_task_get)
|
203
|
+
self.app.route('/a2a/tasks/cancel', methods=['POST'])(self.handle_a2a_task_cancel)
|
204
|
+
self.app.route('/a2a/tasks/pushNotification/set', methods=['POST'])(self.handle_a2a_push_notification)
|
180
205
|
|
181
206
|
self.register_additional_routes()
|
182
207
|
|
@@ -1101,4 +1126,224 @@ if __name__ == "__main__":
|
|
1101
1126
|
"transport": "http",
|
1102
1127
|
"endpoint": "/mcp",
|
1103
1128
|
"tools": ["vac_stream", "vac_query"] if self.vac_interpreter else ["vac_stream"]
|
1104
|
-
})
|
1129
|
+
})
|
1130
|
+
|
1131
|
+
def _get_or_create_a2a_agent(self):
|
1132
|
+
"""Get or create the A2A agent instance with current request context."""
|
1133
|
+
if not self.enable_a2a_agent or not VACA2AAgent:
|
1134
|
+
return None
|
1135
|
+
|
1136
|
+
if self.vac_a2a_agent is None:
|
1137
|
+
# Extract base URL from current request
|
1138
|
+
base_url = request.url_root.rstrip('/')
|
1139
|
+
|
1140
|
+
self.vac_a2a_agent = VACA2AAgent(
|
1141
|
+
base_url=base_url,
|
1142
|
+
stream_interpreter=self.stream_interpreter,
|
1143
|
+
vac_interpreter=self.vac_interpreter,
|
1144
|
+
vac_names=self.a2a_vac_names
|
1145
|
+
)
|
1146
|
+
|
1147
|
+
return self.vac_a2a_agent
|
1148
|
+
|
1149
|
+
def handle_a2a_agent_card(self):
|
1150
|
+
"""Handle A2A agent card discovery request."""
|
1151
|
+
agent = self._get_or_create_a2a_agent()
|
1152
|
+
if not agent:
|
1153
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
1154
|
+
|
1155
|
+
return jsonify(agent.get_agent_card())
|
1156
|
+
|
1157
|
+
def handle_a2a_task_send(self):
|
1158
|
+
"""Handle A2A task send request."""
|
1159
|
+
agent = self._get_or_create_a2a_agent()
|
1160
|
+
if not agent:
|
1161
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
1162
|
+
|
1163
|
+
try:
|
1164
|
+
data = request.get_json()
|
1165
|
+
if not data:
|
1166
|
+
return jsonify({
|
1167
|
+
"jsonrpc": "2.0",
|
1168
|
+
"error": {
|
1169
|
+
"code": -32700,
|
1170
|
+
"message": "Parse error: Invalid JSON"
|
1171
|
+
},
|
1172
|
+
"id": None
|
1173
|
+
}), 400
|
1174
|
+
|
1175
|
+
# Run async handler
|
1176
|
+
loop = asyncio.new_event_loop()
|
1177
|
+
asyncio.set_event_loop(loop)
|
1178
|
+
try:
|
1179
|
+
response = loop.run_until_complete(agent.handle_task_send(data))
|
1180
|
+
return jsonify(response)
|
1181
|
+
finally:
|
1182
|
+
loop.close()
|
1183
|
+
|
1184
|
+
except Exception as e:
|
1185
|
+
log.error(f"A2A task send error: {e}")
|
1186
|
+
return jsonify({
|
1187
|
+
"jsonrpc": "2.0",
|
1188
|
+
"error": {
|
1189
|
+
"code": -32603,
|
1190
|
+
"message": f"Internal error: {str(e)}"
|
1191
|
+
},
|
1192
|
+
"id": data.get("id") if 'data' in locals() else None
|
1193
|
+
}), 500
|
1194
|
+
|
1195
|
+
def handle_a2a_task_send_subscribe(self):
|
1196
|
+
"""Handle A2A task send with subscription (SSE)."""
|
1197
|
+
agent = self._get_or_create_a2a_agent()
|
1198
|
+
if not agent:
|
1199
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
1200
|
+
|
1201
|
+
try:
|
1202
|
+
data = request.get_json()
|
1203
|
+
if not data:
|
1204
|
+
def error_generator():
|
1205
|
+
yield "data: {\"error\": \"Parse error: Invalid JSON\"}\n\n"
|
1206
|
+
|
1207
|
+
return Response(error_generator(), content_type='text/event-stream')
|
1208
|
+
|
1209
|
+
# Create async generator for SSE
|
1210
|
+
async def sse_generator():
|
1211
|
+
async for chunk in agent.handle_task_send_subscribe(data):
|
1212
|
+
yield chunk
|
1213
|
+
|
1214
|
+
def sync_generator():
|
1215
|
+
loop = asyncio.new_event_loop()
|
1216
|
+
asyncio.set_event_loop(loop)
|
1217
|
+
try:
|
1218
|
+
async_gen = sse_generator()
|
1219
|
+
while True:
|
1220
|
+
try:
|
1221
|
+
chunk = loop.run_until_complete(async_gen.__anext__())
|
1222
|
+
yield chunk
|
1223
|
+
except StopAsyncIteration:
|
1224
|
+
break
|
1225
|
+
finally:
|
1226
|
+
loop.close()
|
1227
|
+
|
1228
|
+
return Response(sync_generator(), content_type='text/event-stream')
|
1229
|
+
|
1230
|
+
except Exception as e:
|
1231
|
+
log.error(f"A2A task send subscribe error: {e}")
|
1232
|
+
def error_generator(err):
|
1233
|
+
yield f"data: {{\"error\": \"Internal error: {str(err)}\"}}\n\n"
|
1234
|
+
|
1235
|
+
return Response(error_generator(e), content_type='text/event-stream')
|
1236
|
+
|
1237
|
+
def handle_a2a_task_get(self):
|
1238
|
+
"""Handle A2A task get request."""
|
1239
|
+
agent = self._get_or_create_a2a_agent()
|
1240
|
+
if not agent:
|
1241
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
1242
|
+
|
1243
|
+
try:
|
1244
|
+
data = request.get_json()
|
1245
|
+
if not data:
|
1246
|
+
return jsonify({
|
1247
|
+
"jsonrpc": "2.0",
|
1248
|
+
"error": {
|
1249
|
+
"code": -32700,
|
1250
|
+
"message": "Parse error: Invalid JSON"
|
1251
|
+
},
|
1252
|
+
"id": None
|
1253
|
+
}), 400
|
1254
|
+
|
1255
|
+
# Run async handler
|
1256
|
+
loop = asyncio.new_event_loop()
|
1257
|
+
asyncio.set_event_loop(loop)
|
1258
|
+
try:
|
1259
|
+
response = loop.run_until_complete(agent.handle_task_get(data))
|
1260
|
+
return jsonify(response)
|
1261
|
+
finally:
|
1262
|
+
loop.close()
|
1263
|
+
|
1264
|
+
except Exception as e:
|
1265
|
+
log.error(f"A2A task get error: {e}")
|
1266
|
+
return jsonify({
|
1267
|
+
"jsonrpc": "2.0",
|
1268
|
+
"error": {
|
1269
|
+
"code": -32603,
|
1270
|
+
"message": f"Internal error: {str(e)}"
|
1271
|
+
},
|
1272
|
+
"id": data.get("id") if 'data' in locals() else None
|
1273
|
+
}), 500
|
1274
|
+
|
1275
|
+
def handle_a2a_task_cancel(self):
|
1276
|
+
"""Handle A2A task cancel request."""
|
1277
|
+
agent = self._get_or_create_a2a_agent()
|
1278
|
+
if not agent:
|
1279
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
1280
|
+
|
1281
|
+
try:
|
1282
|
+
data = request.get_json()
|
1283
|
+
if not data:
|
1284
|
+
return jsonify({
|
1285
|
+
"jsonrpc": "2.0",
|
1286
|
+
"error": {
|
1287
|
+
"code": -32700,
|
1288
|
+
"message": "Parse error: Invalid JSON"
|
1289
|
+
},
|
1290
|
+
"id": None
|
1291
|
+
}), 400
|
1292
|
+
|
1293
|
+
# Run async handler
|
1294
|
+
loop = asyncio.new_event_loop()
|
1295
|
+
asyncio.set_event_loop(loop)
|
1296
|
+
try:
|
1297
|
+
response = loop.run_until_complete(agent.handle_task_cancel(data))
|
1298
|
+
return jsonify(response)
|
1299
|
+
finally:
|
1300
|
+
loop.close()
|
1301
|
+
|
1302
|
+
except Exception as e:
|
1303
|
+
log.error(f"A2A task cancel error: {e}")
|
1304
|
+
return jsonify({
|
1305
|
+
"jsonrpc": "2.0",
|
1306
|
+
"error": {
|
1307
|
+
"code": -32603,
|
1308
|
+
"message": f"Internal error: {str(e)}"
|
1309
|
+
},
|
1310
|
+
"id": data.get("id") if 'data' in locals() else None
|
1311
|
+
}), 500
|
1312
|
+
|
1313
|
+
def handle_a2a_push_notification(self):
|
1314
|
+
"""Handle A2A push notification settings."""
|
1315
|
+
agent = self._get_or_create_a2a_agent()
|
1316
|
+
if not agent:
|
1317
|
+
return jsonify({"error": "A2A agent not enabled"}), 501
|
1318
|
+
|
1319
|
+
try:
|
1320
|
+
data = request.get_json()
|
1321
|
+
if not data:
|
1322
|
+
return jsonify({
|
1323
|
+
"jsonrpc": "2.0",
|
1324
|
+
"error": {
|
1325
|
+
"code": -32700,
|
1326
|
+
"message": "Parse error: Invalid JSON"
|
1327
|
+
},
|
1328
|
+
"id": None
|
1329
|
+
}), 400
|
1330
|
+
|
1331
|
+
# Run async handler
|
1332
|
+
loop = asyncio.new_event_loop()
|
1333
|
+
asyncio.set_event_loop(loop)
|
1334
|
+
try:
|
1335
|
+
response = loop.run_until_complete(agent.handle_push_notification_set(data))
|
1336
|
+
return jsonify(response)
|
1337
|
+
finally:
|
1338
|
+
loop.close()
|
1339
|
+
|
1340
|
+
except Exception as e:
|
1341
|
+
log.error(f"A2A push notification error: {e}")
|
1342
|
+
return jsonify({
|
1343
|
+
"jsonrpc": "2.0",
|
1344
|
+
"error": {
|
1345
|
+
"code": -32603,
|
1346
|
+
"message": f"Internal error: {str(e)}"
|
1347
|
+
},
|
1348
|
+
"id": data.get("id") if 'data' in locals() else None
|
1349
|
+
}), 500
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sunholo
|
3
|
-
Version: 0.143.
|
3
|
+
Version: 0.143.3
|
4
4
|
Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
|
5
5
|
Author-email: Holosun ApS <multivac@sunholo.com>
|
6
6
|
License: Apache License, Version 2.0
|
@@ -15,9 +15,10 @@ Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
18
|
-
Requires-Python: >=3.
|
18
|
+
Requires-Python: >=3.11
|
19
19
|
Description-Content-Type: text/markdown
|
20
20
|
License-File: LICENSE.txt
|
21
|
+
Requires-Dist: a2a-python>=0.0.1
|
21
22
|
Requires-Dist: aiohttp
|
22
23
|
Requires-Dist: flask>=3.1.0
|
23
24
|
Requires-Dist: google-auth
|
@@ -126,6 +127,7 @@ Requires-Dist: pytesseract; extra == "pipeline"
|
|
126
127
|
Requires-Dist: tabulate; extra == "pipeline"
|
127
128
|
Requires-Dist: unstructured[all-docs,local-inference]; extra == "pipeline"
|
128
129
|
Provides-Extra: gcp
|
130
|
+
Requires-Dist: a2a-python; extra == "gcp"
|
129
131
|
Requires-Dist: aiofiles; extra == "gcp"
|
130
132
|
Requires-Dist: anthropic[vertex]; extra == "gcp"
|
131
133
|
Requires-Dist: google-api-python-client; extra == "gcp"
|