sunholo 0.143.1__py3-none-any.whl → 0.143.7__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,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
+ }