agnt5 0.1.0__cp39-abi3-macosx_11_0_arm64.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.
Files changed (49) hide show
  1. agnt5/__init__.py +307 -0
  2. agnt5/__pycache__/__init__.cpython-311.pyc +0 -0
  3. agnt5/__pycache__/agent.cpython-311.pyc +0 -0
  4. agnt5/__pycache__/context.cpython-311.pyc +0 -0
  5. agnt5/__pycache__/durable.cpython-311.pyc +0 -0
  6. agnt5/__pycache__/extraction.cpython-311.pyc +0 -0
  7. agnt5/__pycache__/memory.cpython-311.pyc +0 -0
  8. agnt5/__pycache__/reflection.cpython-311.pyc +0 -0
  9. agnt5/__pycache__/runtime.cpython-311.pyc +0 -0
  10. agnt5/__pycache__/task.cpython-311.pyc +0 -0
  11. agnt5/__pycache__/tool.cpython-311.pyc +0 -0
  12. agnt5/__pycache__/tracing.cpython-311.pyc +0 -0
  13. agnt5/__pycache__/types.cpython-311.pyc +0 -0
  14. agnt5/__pycache__/workflow.cpython-311.pyc +0 -0
  15. agnt5/_core.abi3.so +0 -0
  16. agnt5/agent.py +1086 -0
  17. agnt5/context.py +406 -0
  18. agnt5/durable.py +1050 -0
  19. agnt5/extraction.py +410 -0
  20. agnt5/llm/__init__.py +179 -0
  21. agnt5/llm/__pycache__/__init__.cpython-311.pyc +0 -0
  22. agnt5/llm/__pycache__/anthropic.cpython-311.pyc +0 -0
  23. agnt5/llm/__pycache__/azure.cpython-311.pyc +0 -0
  24. agnt5/llm/__pycache__/base.cpython-311.pyc +0 -0
  25. agnt5/llm/__pycache__/google.cpython-311.pyc +0 -0
  26. agnt5/llm/__pycache__/mistral.cpython-311.pyc +0 -0
  27. agnt5/llm/__pycache__/openai.cpython-311.pyc +0 -0
  28. agnt5/llm/__pycache__/together.cpython-311.pyc +0 -0
  29. agnt5/llm/anthropic.py +319 -0
  30. agnt5/llm/azure.py +348 -0
  31. agnt5/llm/base.py +315 -0
  32. agnt5/llm/google.py +373 -0
  33. agnt5/llm/mistral.py +330 -0
  34. agnt5/llm/model_registry.py +467 -0
  35. agnt5/llm/models.json +227 -0
  36. agnt5/llm/openai.py +334 -0
  37. agnt5/llm/together.py +377 -0
  38. agnt5/memory.py +746 -0
  39. agnt5/reflection.py +514 -0
  40. agnt5/runtime.py +699 -0
  41. agnt5/task.py +476 -0
  42. agnt5/testing.py +451 -0
  43. agnt5/tool.py +516 -0
  44. agnt5/tracing.py +624 -0
  45. agnt5/types.py +210 -0
  46. agnt5/workflow.py +897 -0
  47. agnt5-0.1.0.dist-info/METADATA +93 -0
  48. agnt5-0.1.0.dist-info/RECORD +49 -0
  49. agnt5-0.1.0.dist-info/WHEEL +4 -0
agnt5/runtime.py ADDED
@@ -0,0 +1,699 @@
1
+ """
2
+ Runtime integration bridge for AGNT5 Python SDK.
3
+
4
+ This module provides the bridge between Python durable functions and the
5
+ SDK-Core (Rust) that communicates with the AGNT5 Runtime via gRPC.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import os
12
+ import signal
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ import traceback
17
+ from dataclasses import asdict, dataclass
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Dict, List, Optional
20
+
21
+ import aiohttp
22
+
23
+ from .durable import (
24
+ InvocationRequest,
25
+ InvocationResponse,
26
+ _function_registry,
27
+ _service_registry,
28
+ _object_registry,
29
+ get_service_registration_data,
30
+ handle_invocation_from_runtime,
31
+ set_runtime_client,
32
+ DurableObject,
33
+ )
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ @dataclass
39
+ class RuntimeConfig:
40
+ """Configuration for runtime connection."""
41
+
42
+ runtime_endpoint: str = "http://localhost:8081"
43
+ service_name: str = "python-service"
44
+ service_version: str = "1.0.0"
45
+ reconnect_attempts: int = 5
46
+ reconnect_delay: float = 2.0
47
+
48
+
49
+ class PythonRuntimeBridge:
50
+ """
51
+ Bridge between Python SDK and SDK-Core.
52
+
53
+ Manages:
54
+ - Service registration with SDK-Core
55
+ - Function invocation handling
56
+ - Error handling and retries
57
+ - Graceful shutdown
58
+ """
59
+
60
+ def __init__(self, config: RuntimeConfig):
61
+ self.config = config
62
+ self.running = False
63
+ self._shutdown_event = asyncio.Event()
64
+ self._sdk_core_process: Optional[asyncio.subprocess.Process] = None
65
+ self._sdk_core_module = None
66
+ self._sdk_core_worker = None
67
+ self._message_queue = asyncio.Queue()
68
+ self._shutdown_in_progress = False
69
+
70
+ # Set up signal handlers for graceful shutdown
71
+ signal.signal(signal.SIGINT, self._signal_handler)
72
+ signal.signal(signal.SIGTERM, self._signal_handler)
73
+
74
+ def _signal_handler(self, signum, _):
75
+ """Handle shutdown signals."""
76
+ if self._shutdown_in_progress:
77
+ logger.warning(f"Shutdown already in progress, ignoring signal {signum}")
78
+ return
79
+
80
+ logger.info(f"Received signal {signum}, initiating graceful shutdown...")
81
+ self._shutdown_in_progress = True
82
+
83
+ # Set the shutdown event to break the main loop
84
+ if self._shutdown_event and not self._shutdown_event.is_set():
85
+ self._shutdown_event.set()
86
+
87
+ async def start(self) -> None:
88
+ """Start the runtime bridge and register services."""
89
+ logger.info("Starting Python runtime bridge...")
90
+
91
+ try:
92
+ # Start SDK-Core process
93
+ await self._start_sdk_core()
94
+
95
+ # Register services
96
+ await self._register_services()
97
+
98
+ # Start message loop
99
+ self.running = True
100
+ logger.info("Python runtime bridge started successfully")
101
+
102
+ # Keep running until shutdown
103
+ await self._shutdown_event.wait()
104
+
105
+ # After shutdown event, perform cleanup
106
+ logger.info("Shutdown event received, cleaning up...")
107
+
108
+ except KeyboardInterrupt:
109
+ logger.info("Keyboard interrupt received, shutting down...")
110
+ except Exception as e:
111
+ logger.error(f"Failed to start runtime bridge: {e}")
112
+ raise
113
+ finally:
114
+ if self.running or not self._shutdown_in_progress:
115
+ await self.shutdown()
116
+
117
+ async def _start_sdk_core(self) -> None:
118
+ """Start the SDK-Core process via Python extension."""
119
+ try:
120
+ logger.info("Initializing SDK-Core integration...")
121
+
122
+ # Try to import the Python extension module
123
+ try:
124
+ import agnt5._core as sdk_core
125
+
126
+ self._sdk_core_module = sdk_core
127
+ logger.info("SDK-Core Python extension loaded successfully")
128
+ except ImportError as e:
129
+ logger.warning(f"SDK-Core extension not available: {e}")
130
+ logger.info("Falling back to direct HTTP communication")
131
+ self._sdk_core_module = None
132
+ return
133
+
134
+ # Create worker configuration
135
+ worker_config = sdk_core.create_config(
136
+ worker_id=f"python-worker-{os.getpid()}",
137
+ service_name=self.config.service_name,
138
+ version=self.config.service_version,
139
+ max_concurrent_invocations=10,
140
+ heartbeat_interval_seconds=30,
141
+ )
142
+
143
+ # Create the durable worker
144
+ self._sdk_core_worker = sdk_core.create_worker(
145
+ worker_id=worker_config.worker_id, service_name=worker_config.service_name, version=worker_config.version, coordinator_endpoint=self.config.runtime_endpoint
146
+ )
147
+ assert self._sdk_core_worker is not None
148
+
149
+ # Set up message handlers
150
+ self._setup_message_handlers()
151
+
152
+ logger.info("SDK-Core worker initialized")
153
+
154
+ except Exception as e:
155
+ logger.error(f"Failed to start SDK-Core: {e}")
156
+ # Fall back to HTTP communication
157
+ self._sdk_core_module = None
158
+ self._sdk_core_worker = None
159
+ logger.info("Continuing with HTTP fallback mode")
160
+
161
+ async def _register_services(self) -> None:
162
+ """Register all durable functions and objects with SDK-Core."""
163
+ try:
164
+ # Get service registration data
165
+ registration_data = get_service_registration_data()
166
+
167
+ logger.info(f"Registering {len(registration_data['functions'])} functions " f"and {len(registration_data['objects'])} objects")
168
+
169
+ # Send registration to SDK-Core
170
+ await self._send_to_sdk_core(
171
+ {"type": "service_registration", "data": {"service_name": self.config.service_name, "service_version": self.config.service_version, **registration_data}}
172
+ )
173
+
174
+ logger.info("Service registration completed")
175
+
176
+ except Exception as e:
177
+ logger.error(f"Failed to register services: {e}")
178
+ raise
179
+
180
+ async def _send_to_sdk_core(self, message: Dict[str, Any]) -> None:
181
+ """Send a message to SDK-Core."""
182
+ if self._sdk_core_worker:
183
+ # Use the Rust SDK-Core worker
184
+ try:
185
+ message_type = message.get("type")
186
+ data = message.get("data", {})
187
+
188
+ if message_type == "service_registration":
189
+ # Register functions with the worker
190
+ await self._register_functions_with_worker(data)
191
+ elif message_type == "invocation_response":
192
+ # Handle invocation response
193
+ logger.debug(f"Invocation response: {data.get('invocation_id')}")
194
+ elif message_type == "service_deregistration":
195
+ # Handle deregistration
196
+ logger.debug(f"Deregistering service: {data.get('service_name')}")
197
+ else:
198
+ logger.warning(f"Unknown message type: {message_type}")
199
+
200
+ except Exception as e:
201
+ logger.error(f"Error sending message to SDK-Core: {e}")
202
+ raise
203
+ else:
204
+ # Fall back to HTTP communication
205
+ await self._send_http_message(message)
206
+
207
+ logger.debug(f"Sent to SDK-Core: {message['type']}")
208
+
209
+ async def _receive_from_sdk_core(self) -> Optional[Dict[str, Any]]:
210
+ """Receive a message from SDK-Core."""
211
+ if self._sdk_core_worker:
212
+ # Use the Rust SDK-Core worker
213
+ try:
214
+ # In the Rust implementation, messages are handled via callbacks
215
+ # This method is primarily for the HTTP fallback mode
216
+ await asyncio.sleep(0.1)
217
+ return None
218
+ except Exception as e:
219
+ logger.error(f"Error receiving from SDK-Core: {e}")
220
+ return None
221
+ else:
222
+ # Fall back to HTTP communication
223
+ return await self._receive_http_message()
224
+
225
+ async def handle_object_invocation(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
226
+ """
227
+ Handle durable object method invocation from SDK-Core.
228
+
229
+ This handles method calls on durable objects.
230
+ """
231
+ try:
232
+ object_class_name = request_data.get("object_class")
233
+ object_id = request_data.get("object_id")
234
+ method_name = request_data.get("method_name")
235
+ args = request_data.get("args", [])
236
+ kwargs = request_data.get("kwargs", {})
237
+
238
+ if not all([object_class_name, object_id, method_name]):
239
+ raise ValueError("Missing required object invocation parameters")
240
+
241
+ # Find the object class
242
+ if object_class_name not in _object_registry:
243
+ raise ValueError(f"Object class '{object_class_name}' not found")
244
+
245
+ object_class = _object_registry[object_class_name]
246
+
247
+ # Get or create the object instance
248
+ obj = await object_class.get_or_create(object_id)
249
+
250
+ # Invoke the method
251
+ result = await obj.invoke_method(method_name, args, kwargs)
252
+
253
+ # Send response back to SDK-Core
254
+ response_data = {
255
+ "invocation_id": request_data.get("invocation_id", "unknown"),
256
+ "success": True,
257
+ "result": result,
258
+ }
259
+
260
+ await self._send_to_sdk_core({"type": "object_invocation_response", "data": response_data})
261
+
262
+ return response_data
263
+
264
+ except Exception as e:
265
+ logger.error(f"Error handling object invocation: {e}")
266
+ error_response = {
267
+ "invocation_id": request_data.get("invocation_id", "unknown"),
268
+ "success": False,
269
+ "error": str(e),
270
+ }
271
+
272
+ await self._send_to_sdk_core({"type": "object_invocation_response", "data": error_response})
273
+
274
+ return error_response
275
+
276
+ async def handle_invocation(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
277
+ """
278
+ Handle function invocation request from SDK-Core.
279
+
280
+ This is the main entry point for function execution.
281
+ """
282
+ try:
283
+ # Delegate to the durable module
284
+ response_data = await handle_invocation_from_runtime(request_data)
285
+
286
+ # Send response back to SDK-Core
287
+ await self._send_to_sdk_core({"type": "invocation_response", "data": response_data})
288
+
289
+ return response_data
290
+
291
+ except Exception as e:
292
+ logger.error(f"Error handling invocation: {e}")
293
+ error_response = {
294
+ "invocation_id": request_data.get("invocation_id", "unknown"),
295
+ "success": False,
296
+ "error": str(e),
297
+ }
298
+
299
+ await self._send_to_sdk_core({"type": "invocation_response", "data": error_response})
300
+
301
+ return error_response
302
+
303
+ async def shutdown(self) -> None:
304
+ """Gracefully shutdown the runtime bridge."""
305
+ if not self.running and self._shutdown_in_progress:
306
+ logger.debug("Shutdown already completed")
307
+ return
308
+
309
+ logger.info("Shutting down Python runtime bridge...")
310
+ self.running = False
311
+ self._shutdown_in_progress = True
312
+
313
+ try:
314
+ # Send deregistration message to SDK-Core
315
+ if self._sdk_core_worker:
316
+ await self._send_to_sdk_core({"type": "service_deregistration", "data": {"service_name": self.config.service_name}})
317
+
318
+ # Stop SDK-Core process
319
+ if self._sdk_core_process:
320
+ self._sdk_core_process.terminate()
321
+ await self._sdk_core_process.wait()
322
+
323
+ logger.info("Python runtime bridge shutdown completed")
324
+
325
+ except Exception as e:
326
+ logger.error(f"Error during shutdown: {e}")
327
+ finally:
328
+ if not self._shutdown_event.is_set():
329
+ self._shutdown_event.set()
330
+
331
+ def _setup_message_handlers(self) -> None:
332
+ """Bridge Python function calls with the Rust SDK-Core worker by configuring callback mechanisms for bidirectional communication.."""
333
+ if not self._sdk_core_worker:
334
+ return
335
+
336
+ # Register Python functions with the Rust worker
337
+ # This will be called during service registration
338
+ logger.debug("Message handlers configured for SDK-Core integration")
339
+
340
+ async def _register_functions_with_worker(self, registration_data: Dict[str, Any]) -> None:
341
+ """Register Python functions with the SDK-Core worker."""
342
+ if not self._sdk_core_worker:
343
+ return
344
+
345
+ try:
346
+ functions = registration_data.get("functions", [])
347
+
348
+ for func_info in functions:
349
+ func_name = func_info.get("name")
350
+ if not func_name:
351
+ logger.warning(f"Function info missing name: {func_info}")
352
+ continue
353
+
354
+ # Create a wrapper function that can be called from Rust
355
+ # Note: This function will be registered with the Rust extension in future implementation
356
+ async def _python_handler_wrapper(invocation_id: str, input_data: bytes) -> bytes:
357
+ try:
358
+ # Deserialize input
359
+ input_dict = json.loads(input_data.decode("utf-8"))
360
+
361
+ # Call the Python function
362
+ from .durable import handle_invocation_from_runtime
363
+
364
+ request_data = {"invocation_id": invocation_id, "function_name": func_name, "args": input_dict.get("args", []), "kwargs": input_dict.get("kwargs", {})}
365
+
366
+ response = await handle_invocation_from_runtime(request_data)
367
+
368
+ # Serialize response
369
+ return json.dumps(response).encode("utf-8")
370
+
371
+ except Exception as e:
372
+ logger.error(f"Error in Python handler {func_name}: {e}")
373
+ error_response = {"success": False, "error": str(e), "invocation_id": invocation_id}
374
+ return json.dumps(error_response).encode("utf-8")
375
+
376
+ # Register the handler with the worker
377
+ # Note: This would need to be implemented in the Rust extension
378
+ logger.debug(f"Registered function {func_name} with SDK-Core worker")
379
+
380
+ except Exception as e:
381
+ logger.error(f"Error registering functions with worker: {e}")
382
+ raise
383
+
384
+ async def _send_http_message(self, message: Dict[str, Any]) -> None:
385
+ """Fall back to HTTP communication when SDK-Core is not available."""
386
+ try:
387
+ async with aiohttp.ClientSession() as session:
388
+ url = f"{self.config.runtime_endpoint}/messages"
389
+ async with session.post(url, json=message) as response:
390
+ if response.status != 200:
391
+ logger.warning(f"HTTP message failed: {response.status}")
392
+ else:
393
+ logger.debug(f"HTTP message sent successfully: {message['type']}")
394
+ except Exception as e:
395
+ logger.error(f"HTTP communication error: {e}")
396
+
397
+ async def _receive_http_message(self) -> Optional[Dict[str, Any]]:
398
+ """Receive messages via HTTP polling (fallback mode)."""
399
+ try:
400
+ async with aiohttp.ClientSession() as session:
401
+ url = f"{self.config.runtime_endpoint}/messages"
402
+ async with session.get(url) as response:
403
+ if response.status == 200:
404
+ return await response.json()
405
+ else:
406
+ return None
407
+ except Exception as e:
408
+ logger.debug(f"HTTP receive error (expected in polling): {e}")
409
+ return None
410
+
411
+
412
+ class RuntimeClient:
413
+ """
414
+ Client for interacting with the AGNT5 Runtime.
415
+
416
+ This is set as the global runtime client for durable functions.
417
+ """
418
+
419
+ def __init__(self, bridge: PythonRuntimeBridge):
420
+ self.bridge = bridge
421
+
422
+ async def call_service(self, service: str, method: str, *args, **kwargs) -> Any:
423
+ """Make a durable service call via the runtime."""
424
+ if self.bridge._sdk_core_worker:
425
+ # Use SDK-Core for service calls
426
+ try:
427
+ call_data = {"service": service, "method": method, "args": args, "kwargs": kwargs}
428
+
429
+ # Send service call message to SDK-Core
430
+ await self.bridge._send_to_sdk_core({"type": "service_call", "data": call_data})
431
+
432
+ # For now, return a placeholder response
433
+ # In full implementation, this would wait for the response
434
+ logger.info(f"Service call via SDK-Core: {service}.{method}")
435
+ return f"sdk_core_response_from_{service}_{method}"
436
+
437
+ except Exception as e:
438
+ logger.error(f"SDK-Core service call failed: {e}")
439
+ # Fall back to mock response
440
+ return f"fallback_response_from_{service}_{method}"
441
+ else:
442
+ # Fall back to HTTP/mock implementation
443
+ logger.info(f"Service call (fallback): {service}.{method}")
444
+ return f"http_response_from_{service}_{method}"
445
+
446
+ async def get_object(self, object_class: type, object_id: str) -> Any:
447
+ """Get or create a durable object instance via the runtime."""
448
+ if self.bridge._sdk_core_worker:
449
+ # Use SDK-Core for object management
450
+ try:
451
+ object_data = {
452
+ "object_class": object_class.__name__,
453
+ "object_id": object_id
454
+ }
455
+
456
+ # Send object request message to SDK-Core
457
+ await self.bridge._send_to_sdk_core({"type": "get_object", "data": object_data})
458
+
459
+ # For now, return a mock object instance
460
+ # In full implementation, this would wait for the response
461
+ logger.info(f"Object request via SDK-Core: {object_class.__name__}({object_id})")
462
+
463
+ # Create a local instance for fallback
464
+ if hasattr(object_class, 'get_or_create'):
465
+ return await object_class.get_or_create(object_id)
466
+ else:
467
+ return object_class(object_id)
468
+
469
+ except Exception as e:
470
+ logger.error(f"SDK-Core object request failed: {e}")
471
+ # Fall back to local object creation
472
+ if hasattr(object_class, 'get_or_create'):
473
+ return await object_class.get_or_create(object_id)
474
+ else:
475
+ return object_class(object_id)
476
+ else:
477
+ # Fall back to local object management
478
+ logger.info(f"Object request (fallback): {object_class.__name__}({object_id})")
479
+ if hasattr(object_class, 'get_or_create'):
480
+ return await object_class.get_or_create(object_id)
481
+ else:
482
+ return object_class(object_id)
483
+
484
+ async def durable_call(self, service: str, method: str, *args, **kwargs) -> Any:
485
+ """Make a durable call (alias for call_service)."""
486
+ return await self.call_service(service, method, *args, **kwargs)
487
+
488
+ async def durable_sleep(self, seconds: float) -> None:
489
+ """Durable sleep via the runtime."""
490
+ if self.bridge._sdk_core_worker:
491
+ try:
492
+ sleep_data = {"seconds": seconds}
493
+ await self.bridge._send_to_sdk_core({"type": "durable_sleep", "data": sleep_data})
494
+ logger.info(f"Durable sleep via SDK-Core: {seconds} seconds")
495
+
496
+ # For fallback, use regular sleep
497
+ await asyncio.sleep(seconds)
498
+
499
+ except Exception as e:
500
+ logger.error(f"SDK-Core durable sleep failed: {e}")
501
+ await asyncio.sleep(seconds)
502
+ else:
503
+ # Fallback to regular sleep
504
+ logger.info(f"Durable sleep (fallback): {seconds} seconds")
505
+ await asyncio.sleep(seconds)
506
+
507
+ async def get_state(self, key: str, default: Any = None) -> Any:
508
+ """Get state value from durable storage."""
509
+ if self.bridge._sdk_core_worker:
510
+ try:
511
+ state_data = {"key": key, "default": default}
512
+ await self.bridge._send_to_sdk_core({"type": "get_state", "data": state_data})
513
+
514
+ # For now, return default as placeholder
515
+ # In full implementation, this would wait for the response
516
+ logger.debug(f"Get state via SDK-Core: {key}")
517
+ return default
518
+
519
+ except Exception as e:
520
+ logger.error(f"SDK-Core get state failed: {e}")
521
+ return default
522
+ else:
523
+ # Fallback: return default
524
+ logger.debug(f"Get state (fallback): {key}")
525
+ return default
526
+
527
+ async def set_state(self, key: str, value: Any) -> None:
528
+ """Set state value in durable storage."""
529
+ if self.bridge._sdk_core_worker:
530
+ try:
531
+ state_data = {"key": key, "value": value}
532
+ await self.bridge._send_to_sdk_core({"type": "set_state", "data": state_data})
533
+ logger.debug(f"Set state via SDK-Core: {key}")
534
+
535
+ except Exception as e:
536
+ logger.error(f"SDK-Core set state failed: {e}")
537
+ else:
538
+ # Fallback: no-op
539
+ logger.debug(f"Set state (fallback): {key}")
540
+
541
+ async def delete_state(self, key: str) -> None:
542
+ """Delete state value from durable storage."""
543
+ if self.bridge._sdk_core_worker:
544
+ try:
545
+ state_data = {"key": key}
546
+ await self.bridge._send_to_sdk_core({"type": "delete_state", "data": state_data})
547
+ logger.debug(f"Delete state via SDK-Core: {key}")
548
+
549
+ except Exception as e:
550
+ logger.error(f"SDK-Core delete state failed: {e}")
551
+ else:
552
+ # Fallback: no-op
553
+ logger.debug(f"Delete state (fallback): {key}")
554
+
555
+ async def schedule_timer(self, delay: float, callback: Callable) -> str:
556
+ """Schedule a durable timer."""
557
+ timer_id = f"timer_{asyncio.get_event_loop().time()}_{delay}"
558
+
559
+ if self.bridge._sdk_core_worker:
560
+ # Use SDK-Core for timer scheduling
561
+ try:
562
+ timer_data = {"timer_id": timer_id, "delay": delay, "callback_name": getattr(callback, "__name__", "anonymous")}
563
+
564
+ await self.bridge._send_to_sdk_core({"type": "schedule_timer", "data": timer_data})
565
+
566
+ logger.info(f"Scheduled timer {timer_id} via SDK-Core for {delay} seconds")
567
+
568
+ except Exception as e:
569
+ logger.error(f"SDK-Core timer scheduling failed: {e}")
570
+ # Fall back to simple timer
571
+ asyncio.create_task(self._fallback_timer(delay, callback))
572
+ else:
573
+ # Fall back to simple asyncio timer
574
+ asyncio.create_task(self._fallback_timer(delay, callback))
575
+ logger.info(f"Scheduled fallback timer {timer_id} for {delay} seconds")
576
+
577
+ return timer_id
578
+
579
+ async def _fallback_timer(self, delay: float, callback: Callable) -> None:
580
+ """Simple fallback timer implementation."""
581
+ await asyncio.sleep(delay)
582
+ try:
583
+ if asyncio.iscoroutinefunction(callback):
584
+ await callback()
585
+ else:
586
+ callback()
587
+ except Exception as e:
588
+ logger.error(f"Timer callback error: {e}")
589
+
590
+ async def save_object_state(self, object_id: str, state: Dict[str, Any]) -> None:
591
+ """Save durable object state."""
592
+ if self.bridge._sdk_core_worker:
593
+ try:
594
+ state_data = {"object_id": object_id, "state": state}
595
+
596
+ await self.bridge._send_to_sdk_core({"type": "save_state", "data": state_data})
597
+
598
+ logger.debug(f"Saved state for object {object_id} via SDK-Core")
599
+
600
+ except Exception as e:
601
+ logger.error(f"SDK-Core state save failed: {e}")
602
+ else:
603
+ # Fall back to logging (no persistent storage in fallback mode)
604
+ logger.debug(f"Saving state for object {object_id} (fallback mode - not persistent)")
605
+
606
+ async def load_object_state(self, object_id: str) -> Optional[Dict[str, Any]]:
607
+ """Load durable object state."""
608
+ if self.bridge._sdk_core_worker:
609
+ try:
610
+ await self.bridge._send_to_sdk_core({"type": "load_state", "data": {"object_id": object_id}})
611
+
612
+ # In full implementation, this would wait for response
613
+ # For now, return None as placeholder
614
+ logger.debug(f"Requested state for object {object_id} via SDK-Core")
615
+ return None
616
+
617
+ except Exception as e:
618
+ logger.error(f"SDK-Core state load failed: {e}")
619
+ return None
620
+ else:
621
+ # Fall back mode - no persistent storage
622
+ logger.debug(f"Loading state for object {object_id} (fallback mode - no state available)")
623
+ return None
624
+
625
+
626
+ async def run_service(
627
+ service_name: str = "python-service",
628
+ runtime_endpoint: str = "http://localhost:8081",
629
+ service_version: str = "1.0.0",
630
+ ) -> None:
631
+ """
632
+ Run the Python service with durable functions.
633
+
634
+ This is the main entry point for Python services that use durable functions.
635
+
636
+ Args:
637
+ service_name: Name of the service
638
+ runtime_endpoint: AGNT5 Runtime endpoint
639
+ service_version: Version of the service
640
+ """
641
+ config = RuntimeConfig(
642
+ service_name=service_name,
643
+ runtime_endpoint=runtime_endpoint,
644
+ service_version=service_version,
645
+ )
646
+
647
+ bridge = PythonRuntimeBridge(config)
648
+ client = RuntimeClient(bridge)
649
+
650
+ # Set the runtime client for durable functions
651
+ set_runtime_client(client)
652
+
653
+ # Start the service
654
+ await bridge.start()
655
+
656
+
657
+ def main():
658
+ """CLI entry point for running Python services."""
659
+ import argparse
660
+
661
+ parser = argparse.ArgumentParser(description="Run AGNT5 Python service")
662
+ parser.add_argument("--service-name", default="python-service", help="Name of the service")
663
+ parser.add_argument("--runtime-endpoint", default="http://localhost:8081", help="AGNT5 Runtime endpoint")
664
+ parser.add_argument("--service-version", default="1.0.0", help="Version of the service")
665
+ parser.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Log level")
666
+
667
+ args = parser.parse_args()
668
+
669
+ # Configure logging
670
+ logging.basicConfig(level=getattr(logging, args.log_level), format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
671
+
672
+ # Print service info
673
+ print(f"Starting AGNT5 Python Service: {args.service_name}")
674
+ print(f"Runtime endpoint: {args.runtime_endpoint}")
675
+ print(f"Registered functions: {len(_function_registry)}")
676
+ print(f"Registered flows: {len([f for f in _function_registry.values() if hasattr(f, '_is_flow')])}")
677
+ print(f"Registered objects: {len(_object_registry)}")
678
+ print(f"Registered services: {len(_service_registry)}")
679
+
680
+ # Run the service
681
+ try:
682
+ asyncio.run(
683
+ run_service(
684
+ service_name=args.service_name,
685
+ runtime_endpoint=args.runtime_endpoint,
686
+ service_version=args.service_version,
687
+ )
688
+ )
689
+ except KeyboardInterrupt:
690
+ print("\nāœ… Service stopped by user")
691
+ except Exception as e:
692
+ print(f"āŒ Service failed: {e}")
693
+ sys.exit(1)
694
+ else:
695
+ print("āœ… Service completed successfully")
696
+
697
+
698
+ if __name__ == "__main__":
699
+ main()