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.
@@ -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.1
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.10
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"