agnt5 0.1.3__cp39-abi3-win_amd64.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.

Potentially problematic release.


This version of agnt5 might be problematic. Click here for more details.

@@ -0,0 +1,13 @@
1
+ """
2
+ Runtime adapters for different execution environments.
3
+ """
4
+
5
+ from .base import RuntimeAdapter
6
+ from .worker import WorkerRuntime
7
+ from .asgi import ASGIRuntime
8
+
9
+ __all__ = [
10
+ 'RuntimeAdapter',
11
+ 'WorkerRuntime',
12
+ 'ASGIRuntime',
13
+ ]
agnt5/runtimes/asgi.py ADDED
@@ -0,0 +1,270 @@
1
+ """
2
+ ASGI runtime adapter for web framework integration.
3
+
4
+ This adapter creates a pure ASGI application that can be run with any ASGI server
5
+ like uvicorn, hypercorn, or daphne without requiring FastAPI or other frameworks.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import uuid
11
+ from typing import Any, Dict, List, Tuple, Union
12
+ from urllib.parse import parse_qs
13
+
14
+ from .base import RuntimeAdapter, RuntimeContext, InvocationRequest, InvocationResponse
15
+ from ..decorators import invoke_function, get_registered_functions
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ASGIRuntime(RuntimeAdapter):
21
+ """Pure ASGI runtime adapter."""
22
+
23
+ def __init__(self, worker=None):
24
+ self.name = "asgi"
25
+ self.worker = worker
26
+ self._cors_enabled = True
27
+ self._cors_origins = ["*"]
28
+
29
+ def enable_cors(self, origins: List[str] = None):
30
+ """Enable CORS with specified origins."""
31
+ self._cors_enabled = True
32
+ self._cors_origins = origins or ["*"]
33
+
34
+ def disable_cors(self):
35
+ """Disable CORS."""
36
+ self._cors_enabled = False
37
+
38
+ async def __call__(self, scope: dict, receive, send):
39
+ """ASGI application entry point."""
40
+ assert scope['type'] == 'http'
41
+
42
+ # Parse request
43
+ method = scope['method']
44
+ path = scope['path']
45
+
46
+ # Handle CORS preflight
47
+ if method == 'OPTIONS' and self._cors_enabled:
48
+ await self._handle_cors_preflight(scope, receive, send)
49
+ return
50
+
51
+ # Route to appropriate handler
52
+ if path.startswith('/invoke/'):
53
+ await self._handle_invocation(scope, receive, send)
54
+ elif path == '/health':
55
+ await self._handle_health(scope, receive, send)
56
+ elif path == '/functions':
57
+ await self._handle_list_functions(scope, receive, send)
58
+ else:
59
+ await self._handle_not_found(scope, receive, send)
60
+
61
+ async def _handle_invocation(self, scope: dict, receive, send):
62
+ """Handle function invocation requests."""
63
+ path = scope['path']
64
+ method = scope['method']
65
+
66
+ if method != 'POST':
67
+ await self._send_error(send, 405, "Method not allowed")
68
+ return
69
+
70
+ # Extract function name from path: /invoke/{function_name}
71
+ path_parts = path.split('/')
72
+ if len(path_parts) < 3:
73
+ await self._send_error(send, 400, "Invalid path format")
74
+ return
75
+
76
+ function_name = path_parts[2]
77
+
78
+ try:
79
+ # Read request body
80
+ body = b''
81
+ while True:
82
+ message = await receive()
83
+ if message['type'] == 'http.request':
84
+ body += message.get('body', b'')
85
+ if not message.get('more_body', False):
86
+ break
87
+
88
+ # Parse JSON body
89
+ try:
90
+ if body:
91
+ input_data = json.loads(body.decode('utf-8'))
92
+ input_bytes = json.dumps(input_data).encode('utf-8')
93
+ else:
94
+ input_bytes = b'{}'
95
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
96
+ await self._send_error(send, 400, f"Invalid JSON: {str(e)}")
97
+ return
98
+
99
+ # Create invocation request
100
+ invocation_id = str(uuid.uuid4())
101
+ request = InvocationRequest(
102
+ invocation_id=invocation_id,
103
+ service_name=self.worker.service_name if self.worker else "unknown",
104
+ handler_name=function_name,
105
+ input_data=input_bytes
106
+ )
107
+
108
+ # Create runtime context
109
+ ctx = RuntimeContext(
110
+ invocation_id=invocation_id,
111
+ service_name=request.service_name,
112
+ component_name=function_name
113
+ )
114
+
115
+ # Handle the request
116
+ response = await self.handle_request(ctx, request)
117
+
118
+ if response.success:
119
+ # Parse output data back to JSON for HTTP response
120
+ try:
121
+ if response.output_data:
122
+ output = json.loads(response.output_data.decode('utf-8'))
123
+ else:
124
+ output = None
125
+
126
+ await self._send_json_response(send, 200, {
127
+ "result": output,
128
+ "invocation_id": response.invocation_id
129
+ })
130
+ except (json.JSONDecodeError, UnicodeDecodeError):
131
+ # If output is not JSON, send as raw bytes (base64 encoded)
132
+ import base64
133
+ await self._send_json_response(send, 200, {
134
+ "result": base64.b64encode(response.output_data).decode('utf-8'),
135
+ "invocation_id": response.invocation_id,
136
+ "encoding": "base64"
137
+ })
138
+ else:
139
+ await self._send_error(send, 500, response.error_message or "Function execution failed")
140
+
141
+ except Exception as e:
142
+ logger.exception(f"Error handling invocation for {function_name}")
143
+ await self._send_error(send, 500, f"Internal server error: {str(e)}")
144
+
145
+ async def _handle_health(self, scope: dict, receive, send):
146
+ """Handle health check requests."""
147
+ await self._send_json_response(send, 200, {
148
+ "status": "healthy",
149
+ "runtime": self.name,
150
+ "service": self.worker.service_name if self.worker else "unknown"
151
+ })
152
+
153
+ async def _handle_list_functions(self, scope: dict, receive, send):
154
+ """Handle function listing requests."""
155
+ functions = get_registered_functions()
156
+ function_info = []
157
+
158
+ for name, func in functions.items():
159
+ info = {
160
+ "name": name,
161
+ "type": "function"
162
+ }
163
+ if hasattr(func, '__doc__') and func.__doc__:
164
+ info["description"] = func.__doc__.strip()
165
+ function_info.append(info)
166
+
167
+ await self._send_json_response(send, 200, {
168
+ "functions": function_info,
169
+ "count": len(function_info)
170
+ })
171
+
172
+ async def _handle_not_found(self, scope: dict, receive, send):
173
+ """Handle 404 responses."""
174
+ await self._send_error(send, 404, "Not found")
175
+
176
+ async def _handle_cors_preflight(self, scope: dict, receive, send):
177
+ """Handle CORS preflight requests."""
178
+ headers = [
179
+ (b'access-control-allow-origin', b'*'),
180
+ (b'access-control-allow-methods', b'GET, POST, OPTIONS'),
181
+ (b'access-control-allow-headers', b'content-type'),
182
+ (b'access-control-max-age', b'3600'),
183
+ ]
184
+
185
+ await send({
186
+ 'type': 'http.response.start',
187
+ 'status': 200,
188
+ 'headers': headers
189
+ })
190
+ await send({
191
+ 'type': 'http.response.body',
192
+ 'body': b''
193
+ })
194
+
195
+ async def _send_json_response(self, send, status: int, data: dict):
196
+ """Send JSON response with optional CORS headers."""
197
+ headers = [(b'content-type', b'application/json')]
198
+
199
+ if self._cors_enabled:
200
+ headers.extend([
201
+ (b'access-control-allow-origin', b'*'),
202
+ (b'access-control-allow-methods', b'GET, POST, OPTIONS'),
203
+ (b'access-control-allow-headers', b'content-type'),
204
+ ])
205
+
206
+ body = json.dumps(data).encode('utf-8')
207
+
208
+ await send({
209
+ 'type': 'http.response.start',
210
+ 'status': status,
211
+ 'headers': headers
212
+ })
213
+ await send({
214
+ 'type': 'http.response.body',
215
+ 'body': body
216
+ })
217
+
218
+ async def _send_error(self, send, status: int, message: str):
219
+ """Send error response."""
220
+ await self._send_json_response(send, status, {
221
+ "error": message,
222
+ "status": status
223
+ })
224
+
225
+ async def handle_request(
226
+ self,
227
+ ctx: RuntimeContext,
228
+ request: InvocationRequest
229
+ ) -> InvocationResponse:
230
+ """
231
+ Handle function invocation using the decorator system.
232
+ """
233
+ logger.info(f"Handling ASGI invocation: {request.handler_name}")
234
+
235
+ try:
236
+ # Create context dict for the function
237
+ function_context = {
238
+ 'invocation_id': ctx.invocation_id,
239
+ 'service_name': ctx.service_name,
240
+ 'handler_name': request.handler_name,
241
+ 'tenant_id': ctx.tenant_id,
242
+ 'deployment_id': ctx.deployment_id,
243
+ 'metadata': {**ctx.metadata, **request.metadata}
244
+ }
245
+
246
+ # Call the function through the decorator system
247
+ result_data = invoke_function(
248
+ handler_name=request.handler_name,
249
+ input_data=request.input_data,
250
+ context=function_context
251
+ )
252
+
253
+ logger.info(f"ASGI invocation {request.handler_name} completed successfully")
254
+
255
+ return InvocationResponse(
256
+ invocation_id=request.invocation_id,
257
+ output_data=result_data,
258
+ success=True
259
+ )
260
+
261
+ except Exception as e:
262
+ error_msg = f"Function {request.handler_name} failed: {str(e)}"
263
+ logger.error(error_msg)
264
+
265
+ return InvocationResponse(
266
+ invocation_id=request.invocation_id,
267
+ output_data=b'',
268
+ success=False,
269
+ error_message=error_msg
270
+ )
agnt5/runtimes/base.py ADDED
@@ -0,0 +1,78 @@
1
+ """
2
+ Base runtime adapter class.
3
+ """
4
+
5
+ from typing import Any, Dict, Optional, Protocol
6
+
7
+
8
+ class InvocationRequest:
9
+ """Request for function invocation."""
10
+
11
+ def __init__(
12
+ self,
13
+ invocation_id: str,
14
+ service_name: str,
15
+ handler_name: str,
16
+ input_data: bytes,
17
+ metadata: Optional[Dict[str, str]] = None
18
+ ):
19
+ self.invocation_id = invocation_id
20
+ self.service_name = service_name
21
+ self.handler_name = handler_name
22
+ self.input_data = input_data
23
+ self.metadata = metadata or {}
24
+
25
+
26
+ class InvocationResponse:
27
+ """Response from function invocation."""
28
+
29
+ def __init__(
30
+ self,
31
+ invocation_id: str,
32
+ output_data: bytes,
33
+ success: bool = True,
34
+ error_message: Optional[str] = None,
35
+ metadata: Optional[Dict[str, str]] = None
36
+ ):
37
+ self.invocation_id = invocation_id
38
+ self.output_data = output_data
39
+ self.success = success
40
+ self.error_message = error_message
41
+ self.metadata = metadata or {}
42
+
43
+
44
+ class RuntimeContext:
45
+ """Runtime execution context."""
46
+
47
+ def __init__(
48
+ self,
49
+ invocation_id: str,
50
+ service_name: str,
51
+ component_name: str,
52
+ tenant_id: str = "default",
53
+ deployment_id: str = "default",
54
+ metadata: Optional[Dict[str, str]] = None
55
+ ):
56
+ self.invocation_id = invocation_id
57
+ self.service_name = service_name
58
+ self.component_name = component_name
59
+ self.tenant_id = tenant_id
60
+ self.deployment_id = deployment_id
61
+ self.metadata = metadata or {}
62
+
63
+
64
+ class RuntimeAdapter(Protocol):
65
+ """Protocol for runtime adapters.
66
+
67
+ Any class implementing this protocol can be used as a RuntimeAdapter.
68
+ The Protocol pattern uses duck typing - if an object has the required
69
+ methods with the correct signature, it satisfies the protocol.
70
+ """
71
+
72
+ async def handle_request(
73
+ self,
74
+ ctx: RuntimeContext,
75
+ request: InvocationRequest
76
+ ) -> InvocationResponse:
77
+ """Handle a function invocation request."""
78
+ ...
@@ -0,0 +1,77 @@
1
+ """
2
+ Standalone runtime adapter for direct worker execution.
3
+
4
+ This adapter is used when running workers directly with asyncio.run(main()).
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Dict
11
+
12
+ from .base import RuntimeAdapter, RuntimeContext, InvocationRequest, InvocationResponse
13
+ from ..decorators import invoke_function
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class WorkerRuntime(RuntimeAdapter):
19
+ """Runtime adapter for standalone worker execution."""
20
+
21
+ def __init__(self):
22
+ self.name = "standalone"
23
+
24
+ async def handle_request(
25
+ self,
26
+ ctx: RuntimeContext,
27
+ request: InvocationRequest
28
+ ) -> InvocationResponse:
29
+ """
30
+ Handle function invocation by directly calling the decorated function.
31
+
32
+ Args:
33
+ ctx: Runtime execution context
34
+ request: Invocation request with handler name and input data
35
+
36
+ Returns:
37
+ InvocationResponse with function result or error
38
+ """
39
+ logger.info(f"Handling standalone invocation: {request.handler_name}")
40
+
41
+ try:
42
+ # Create context dict for the function
43
+ function_context = {
44
+ 'invocation_id': ctx.invocation_id,
45
+ 'service_name': ctx.service_name,
46
+ 'handler_name': request.handler_name,
47
+ 'tenant_id': ctx.tenant_id,
48
+ 'deployment_id': ctx.deployment_id,
49
+ 'metadata': {**ctx.metadata, **request.metadata}
50
+ }
51
+
52
+ # Call the function through the decorator system
53
+ result_data = invoke_function(
54
+ handler_name=request.handler_name,
55
+ input_data=request.input_data,
56
+ context=function_context
57
+ )
58
+
59
+ logger.info(f"Standalone invocation {request.handler_name} completed successfully")
60
+
61
+ return InvocationResponse(
62
+ invocation_id=request.invocation_id,
63
+ output_data=result_data,
64
+ success=True
65
+ )
66
+
67
+ except Exception as e:
68
+ error_msg = f"Function {request.handler_name} failed: {str(e)}"
69
+ logger.error(error_msg)
70
+
71
+ # Return error response with empty output
72
+ return InvocationResponse(
73
+ invocation_id=request.invocation_id,
74
+ output_data=b'',
75
+ success=False,
76
+ error_message=error_msg
77
+ )
agnt5/version.py ADDED
@@ -0,0 +1,23 @@
1
+
2
+
3
+ # Read version from pyproject.toml to maintain single source of truth
4
+ def _get_version():
5
+ try:
6
+ import tomllib
7
+ except ImportError:
8
+ # Python < 3.11 fallback
9
+ try:
10
+ import tomli as tomllib
11
+ except ImportError:
12
+ # Final fallback if no toml library available
13
+ return "UNKNOWN"
14
+
15
+ try:
16
+ import pathlib
17
+ pyproject_path = pathlib.Path(__file__).parent.parent.parent / "pyproject.toml"
18
+ with open(pyproject_path, "rb") as f:
19
+ pyproject_data = tomllib.load(f)
20
+ return pyproject_data["project"]["version"]
21
+ except Exception:
22
+ # Fallback version if reading fails
23
+ return "UNKNOWN"