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.
- agnt5/__init__.py +307 -0
- agnt5/__pycache__/__init__.cpython-311.pyc +0 -0
- agnt5/__pycache__/agent.cpython-311.pyc +0 -0
- agnt5/__pycache__/context.cpython-311.pyc +0 -0
- agnt5/__pycache__/durable.cpython-311.pyc +0 -0
- agnt5/__pycache__/extraction.cpython-311.pyc +0 -0
- agnt5/__pycache__/memory.cpython-311.pyc +0 -0
- agnt5/__pycache__/reflection.cpython-311.pyc +0 -0
- agnt5/__pycache__/runtime.cpython-311.pyc +0 -0
- agnt5/__pycache__/task.cpython-311.pyc +0 -0
- agnt5/__pycache__/tool.cpython-311.pyc +0 -0
- agnt5/__pycache__/tracing.cpython-311.pyc +0 -0
- agnt5/__pycache__/types.cpython-311.pyc +0 -0
- agnt5/__pycache__/workflow.cpython-311.pyc +0 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/agent.py +1086 -0
- agnt5/context.py +406 -0
- agnt5/durable.py +1050 -0
- agnt5/extraction.py +410 -0
- agnt5/llm/__init__.py +179 -0
- agnt5/llm/__pycache__/__init__.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/anthropic.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/azure.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/base.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/google.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/mistral.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/openai.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/together.cpython-311.pyc +0 -0
- agnt5/llm/anthropic.py +319 -0
- agnt5/llm/azure.py +348 -0
- agnt5/llm/base.py +315 -0
- agnt5/llm/google.py +373 -0
- agnt5/llm/mistral.py +330 -0
- agnt5/llm/model_registry.py +467 -0
- agnt5/llm/models.json +227 -0
- agnt5/llm/openai.py +334 -0
- agnt5/llm/together.py +377 -0
- agnt5/memory.py +746 -0
- agnt5/reflection.py +514 -0
- agnt5/runtime.py +699 -0
- agnt5/task.py +476 -0
- agnt5/testing.py +451 -0
- agnt5/tool.py +516 -0
- agnt5/tracing.py +624 -0
- agnt5/types.py +210 -0
- agnt5/workflow.py +897 -0
- agnt5-0.1.0.dist-info/METADATA +93 -0
- agnt5-0.1.0.dist-info/RECORD +49 -0
- 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()
|