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.
- agnt5/__init__.py +23 -0
- agnt5/_compat.py +15 -0
- agnt5/_core.pyd +0 -0
- agnt5/components.py +278 -0
- agnt5/decorators.py +240 -0
- agnt5/logging.py +140 -0
- agnt5/runtimes/__init__.py +13 -0
- agnt5/runtimes/asgi.py +270 -0
- agnt5/runtimes/base.py +78 -0
- agnt5/runtimes/worker.py +77 -0
- agnt5/version.py +23 -0
- agnt5/worker.py +261 -0
- agnt5-0.1.3.dist-info/METADATA +20 -0
- agnt5-0.1.3.dist-info/RECORD +15 -0
- agnt5-0.1.3.dist-info/WHEEL +4 -0
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
|
+
...
|
agnt5/runtimes/worker.py
ADDED
|
@@ -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"
|