ironshell 1.5.1__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.
- ironshell/__init__.py +682 -0
- ironshell/cloud.py +967 -0
- ironshell/distributed.py +691 -0
- ironshell/langchain.py +1074 -0
- ironshell/metrics.py +603 -0
- ironshell/py.typed +0 -0
- ironshell/workflows.py +539 -0
- ironshell-1.5.1.dist-info/METADATA +349 -0
- ironshell-1.5.1.dist-info/RECORD +11 -0
- ironshell-1.5.1.dist-info/WHEEL +5 -0
- ironshell-1.5.1.dist-info/top_level.txt +1 -0
ironshell/cloud.py
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IronShell Cloud Platform
|
|
3
|
+
|
|
4
|
+
Deploy IronShell agents as serverless functions:
|
|
5
|
+
- AWS Lambda
|
|
6
|
+
- Azure Functions
|
|
7
|
+
- Google Cloud Functions
|
|
8
|
+
- Kubernetes (via Knative)
|
|
9
|
+
|
|
10
|
+
Provides infrastructure-as-code templates and runtime adapters.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"CloudProvider",
|
|
24
|
+
"FunctionConfig",
|
|
25
|
+
"DeploymentConfig",
|
|
26
|
+
"CloudRuntime",
|
|
27
|
+
"LambdaRuntime",
|
|
28
|
+
"AzureFunctionsRuntime",
|
|
29
|
+
"GCPFunctionsRuntime",
|
|
30
|
+
"KnativeRuntime",
|
|
31
|
+
"deploy_agent",
|
|
32
|
+
"create_handler",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CloudProvider(Enum):
|
|
39
|
+
"""Supported cloud providers"""
|
|
40
|
+
AWS_LAMBDA = "aws_lambda"
|
|
41
|
+
AZURE_FUNCTIONS = "azure_functions"
|
|
42
|
+
GCP_FUNCTIONS = "gcp_functions"
|
|
43
|
+
KNATIVE = "knative"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class FunctionConfig:
|
|
48
|
+
"""Configuration for a serverless function"""
|
|
49
|
+
name: str
|
|
50
|
+
runtime: str = "python3.11"
|
|
51
|
+
memory_mb: int = 256
|
|
52
|
+
timeout_seconds: int = 30
|
|
53
|
+
environment: Dict[str, str] = field(default_factory=dict)
|
|
54
|
+
vpc_config: Optional[Dict[str, Any]] = None
|
|
55
|
+
concurrency: Optional[int] = None
|
|
56
|
+
layers: List[str] = field(default_factory=list)
|
|
57
|
+
tags: Dict[str, str] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class DeploymentConfig:
|
|
62
|
+
"""Deployment configuration"""
|
|
63
|
+
provider: CloudProvider
|
|
64
|
+
region: str
|
|
65
|
+
function_config: FunctionConfig
|
|
66
|
+
api_gateway: bool = True
|
|
67
|
+
cors_enabled: bool = True
|
|
68
|
+
custom_domain: Optional[str] = None
|
|
69
|
+
stage: str = "prod"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CloudRuntime(ABC):
|
|
73
|
+
"""Base class for cloud runtime adapters"""
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def create_handler(self, agent_code: str) -> str:
|
|
77
|
+
"""Generate handler code for the cloud platform"""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def generate_deployment(self, config: DeploymentConfig) -> Dict[str, str]:
|
|
82
|
+
"""Generate deployment configuration files"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def get_requirements(self) -> List[str]:
|
|
87
|
+
"""Get Python requirements for the runtime"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class LambdaRuntime(CloudRuntime):
|
|
92
|
+
"""AWS Lambda runtime adapter"""
|
|
93
|
+
|
|
94
|
+
def create_handler(self, agent_code: str) -> str:
|
|
95
|
+
"""Generate Lambda handler"""
|
|
96
|
+
return f'''"""
|
|
97
|
+
IronShell Lambda Handler
|
|
98
|
+
Auto-generated - do not edit directly
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
import json
|
|
102
|
+
import os
|
|
103
|
+
import asyncio
|
|
104
|
+
from ironshell import IronShellRuntime, Agent
|
|
105
|
+
|
|
106
|
+
# Initialize runtime (reused across invocations)
|
|
107
|
+
runtime = IronShellRuntime()
|
|
108
|
+
|
|
109
|
+
{agent_code}
|
|
110
|
+
|
|
111
|
+
def lambda_handler(event, context):
|
|
112
|
+
"""
|
|
113
|
+
AWS Lambda entry point.
|
|
114
|
+
|
|
115
|
+
Supports:
|
|
116
|
+
- API Gateway (REST and HTTP API)
|
|
117
|
+
- Direct invocation
|
|
118
|
+
- SQS triggers
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
# Parse input based on trigger type
|
|
122
|
+
if "body" in event:
|
|
123
|
+
# API Gateway
|
|
124
|
+
body = json.loads(event.get("body", "{{}}"))
|
|
125
|
+
goal = body.get("goal", "")
|
|
126
|
+
params = body.get("params", {{}})
|
|
127
|
+
elif "Records" in event:
|
|
128
|
+
# SQS
|
|
129
|
+
record = event["Records"][0]
|
|
130
|
+
body = json.loads(record.get("body", "{{}}"))
|
|
131
|
+
goal = body.get("goal", "")
|
|
132
|
+
params = body.get("params", {{}})
|
|
133
|
+
else:
|
|
134
|
+
# Direct invocation
|
|
135
|
+
goal = event.get("goal", "")
|
|
136
|
+
params = event.get("params", {{}})
|
|
137
|
+
|
|
138
|
+
# Run agent
|
|
139
|
+
result = asyncio.run(run_agent(goal, params))
|
|
140
|
+
|
|
141
|
+
# Format response for API Gateway
|
|
142
|
+
return {{
|
|
143
|
+
"statusCode": 200,
|
|
144
|
+
"headers": {{
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
"Access-Control-Allow-Origin": "*",
|
|
147
|
+
}},
|
|
148
|
+
"body": json.dumps(result, default=str),
|
|
149
|
+
}}
|
|
150
|
+
except Exception as e:
|
|
151
|
+
return {{
|
|
152
|
+
"statusCode": 500,
|
|
153
|
+
"headers": {{"Content-Type": "application/json"}},
|
|
154
|
+
"body": json.dumps({{"error": str(e)}}),
|
|
155
|
+
}}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def run_agent(goal: str, params: dict):
|
|
159
|
+
"""Execute the agent with given goal"""
|
|
160
|
+
agent = create_agent(runtime)
|
|
161
|
+
result = await agent.run(goal)
|
|
162
|
+
return {{
|
|
163
|
+
"success": result.success if hasattr(result, "success") else True,
|
|
164
|
+
"result": result.result if hasattr(result, "result") else result,
|
|
165
|
+
}}
|
|
166
|
+
'''
|
|
167
|
+
|
|
168
|
+
def generate_deployment(self, config: DeploymentConfig) -> Dict[str, str]:
|
|
169
|
+
"""Generate AWS SAM template and supporting files"""
|
|
170
|
+
fc = config.function_config
|
|
171
|
+
|
|
172
|
+
sam_template = f'''AWSTemplateFormatVersion: '2010-09-09'
|
|
173
|
+
Transform: AWS::Serverless-2016-10-31
|
|
174
|
+
Description: IronShell Agent - {fc.name}
|
|
175
|
+
|
|
176
|
+
Globals:
|
|
177
|
+
Function:
|
|
178
|
+
Timeout: {fc.timeout_seconds}
|
|
179
|
+
MemorySize: {fc.memory_mb}
|
|
180
|
+
Runtime: {fc.runtime}
|
|
181
|
+
Environment:
|
|
182
|
+
Variables:
|
|
183
|
+
IRONSHELL_AI: "{{{{resolve:ssm:/ironshell/ai_provider}}}}"
|
|
184
|
+
OPENAI_API_KEY: "{{{{resolve:ssm:/ironshell/openai_key}}}}"
|
|
185
|
+
{self._format_env_vars(fc.environment, 8)}
|
|
186
|
+
|
|
187
|
+
Resources:
|
|
188
|
+
AgentFunction:
|
|
189
|
+
Type: AWS::Serverless::Function
|
|
190
|
+
Properties:
|
|
191
|
+
FunctionName: {fc.name}
|
|
192
|
+
CodeUri: ./
|
|
193
|
+
Handler: handler.lambda_handler
|
|
194
|
+
Description: IronShell agent function
|
|
195
|
+
{self._format_tags(fc.tags, 6)}
|
|
196
|
+
Events:
|
|
197
|
+
ApiEvent:
|
|
198
|
+
Type: HttpApi
|
|
199
|
+
Properties:
|
|
200
|
+
Path: /agent
|
|
201
|
+
Method: POST
|
|
202
|
+
ApiId: !Ref AgentApi
|
|
203
|
+
{self._format_vpc_config(fc.vpc_config, 6) if fc.vpc_config else ""}
|
|
204
|
+
|
|
205
|
+
AgentApi:
|
|
206
|
+
Type: AWS::Serverless::HttpApi
|
|
207
|
+
Properties:
|
|
208
|
+
StageName: {config.stage}
|
|
209
|
+
CorsConfiguration:
|
|
210
|
+
AllowMethods:
|
|
211
|
+
- POST
|
|
212
|
+
- OPTIONS
|
|
213
|
+
AllowHeaders:
|
|
214
|
+
- Content-Type
|
|
215
|
+
- Authorization
|
|
216
|
+
AllowOrigins:
|
|
217
|
+
- "*"
|
|
218
|
+
|
|
219
|
+
Outputs:
|
|
220
|
+
ApiEndpoint:
|
|
221
|
+
Description: API Gateway endpoint URL
|
|
222
|
+
Value: !Sub "https://${{AgentApi}}.execute-api.${{AWS::Region}}.amazonaws.com/{config.stage}/agent"
|
|
223
|
+
FunctionArn:
|
|
224
|
+
Description: Lambda function ARN
|
|
225
|
+
Value: !GetAtt AgentFunction.Arn
|
|
226
|
+
'''
|
|
227
|
+
|
|
228
|
+
samconfig = f'''version = 0.1
|
|
229
|
+
[default.deploy.parameters]
|
|
230
|
+
stack_name = "ironshell-{fc.name}"
|
|
231
|
+
resolve_s3 = true
|
|
232
|
+
s3_prefix = "ironshell-{fc.name}"
|
|
233
|
+
region = "{config.region}"
|
|
234
|
+
confirm_changeset = false
|
|
235
|
+
capabilities = "CAPABILITY_IAM"
|
|
236
|
+
'''
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"template.yaml": sam_template,
|
|
240
|
+
"samconfig.toml": samconfig,
|
|
241
|
+
"requirements.txt": "\n".join(self.get_requirements()),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def get_requirements(self) -> List[str]:
|
|
245
|
+
return [
|
|
246
|
+
"ironshell>=0.3.0",
|
|
247
|
+
"boto3>=1.28.0",
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
def _format_env_vars(self, env: Dict[str, str], indent: int) -> str:
|
|
251
|
+
if not env:
|
|
252
|
+
return ""
|
|
253
|
+
spaces = " " * indent
|
|
254
|
+
lines = [f"{spaces}{k}: {v}" for k, v in env.items()]
|
|
255
|
+
return "\n".join(lines)
|
|
256
|
+
|
|
257
|
+
def _format_tags(self, tags: Dict[str, str], indent: int) -> str:
|
|
258
|
+
if not tags:
|
|
259
|
+
return ""
|
|
260
|
+
spaces = " " * indent
|
|
261
|
+
lines = [f"{spaces}Tags:"]
|
|
262
|
+
for k, v in tags.items():
|
|
263
|
+
lines.append(f"{spaces} {k}: {v}")
|
|
264
|
+
return "\n".join(lines)
|
|
265
|
+
|
|
266
|
+
def _format_vpc_config(self, vpc: Dict[str, Any], indent: int) -> str:
|
|
267
|
+
if not vpc:
|
|
268
|
+
return ""
|
|
269
|
+
spaces = " " * indent
|
|
270
|
+
return f'''{spaces}VpcConfig:
|
|
271
|
+
{spaces} SubnetIds: {vpc.get("subnet_ids", [])}
|
|
272
|
+
{spaces} SecurityGroupIds: {vpc.get("security_group_ids", [])}'''
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class AzureFunctionsRuntime(CloudRuntime):
|
|
276
|
+
"""Azure Functions runtime adapter"""
|
|
277
|
+
|
|
278
|
+
def create_handler(self, agent_code: str) -> str:
|
|
279
|
+
"""Generate Azure Functions handler"""
|
|
280
|
+
return f'''"""
|
|
281
|
+
IronShell Azure Functions Handler
|
|
282
|
+
Auto-generated - do not edit directly
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
import json
|
|
286
|
+
import logging
|
|
287
|
+
import asyncio
|
|
288
|
+
import azure.functions as func
|
|
289
|
+
from ironshell import IronShellRuntime, Agent
|
|
290
|
+
|
|
291
|
+
# Initialize runtime
|
|
292
|
+
runtime = IronShellRuntime()
|
|
293
|
+
|
|
294
|
+
{agent_code}
|
|
295
|
+
|
|
296
|
+
async def main(req: func.HttpRequest) -> func.HttpResponse:
|
|
297
|
+
"""
|
|
298
|
+
Azure Functions HTTP trigger entry point.
|
|
299
|
+
"""
|
|
300
|
+
logging.info('IronShell agent invoked')
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Parse request
|
|
304
|
+
try:
|
|
305
|
+
body = req.get_json()
|
|
306
|
+
except ValueError:
|
|
307
|
+
body = {{}}
|
|
308
|
+
|
|
309
|
+
goal = body.get("goal", "")
|
|
310
|
+
params = body.get("params", {{}})
|
|
311
|
+
|
|
312
|
+
# Run agent
|
|
313
|
+
result = await run_agent(goal, params)
|
|
314
|
+
|
|
315
|
+
return func.HttpResponse(
|
|
316
|
+
json.dumps(result, default=str),
|
|
317
|
+
status_code=200,
|
|
318
|
+
mimetype="application/json",
|
|
319
|
+
headers={{"Access-Control-Allow-Origin": "*"}},
|
|
320
|
+
)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logging.error(f"Agent error: {{e}}")
|
|
323
|
+
return func.HttpResponse(
|
|
324
|
+
json.dumps({{"error": str(e)}}),
|
|
325
|
+
status_code=500,
|
|
326
|
+
mimetype="application/json",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
async def run_agent(goal: str, params: dict):
|
|
331
|
+
"""Execute the agent"""
|
|
332
|
+
agent = create_agent(runtime)
|
|
333
|
+
result = await agent.run(goal)
|
|
334
|
+
return {{
|
|
335
|
+
"success": getattr(result, "success", True),
|
|
336
|
+
"result": getattr(result, "result", result),
|
|
337
|
+
}}
|
|
338
|
+
'''
|
|
339
|
+
|
|
340
|
+
def generate_deployment(self, config: DeploymentConfig) -> Dict[str, str]:
|
|
341
|
+
"""Generate Azure Functions configuration"""
|
|
342
|
+
fc = config.function_config
|
|
343
|
+
|
|
344
|
+
function_json = json.dumps({
|
|
345
|
+
"bindings": [
|
|
346
|
+
{
|
|
347
|
+
"authLevel": "function",
|
|
348
|
+
"type": "httpTrigger",
|
|
349
|
+
"direction": "in",
|
|
350
|
+
"name": "req",
|
|
351
|
+
"methods": ["post", "options"],
|
|
352
|
+
"route": "agent",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"type": "http",
|
|
356
|
+
"direction": "out",
|
|
357
|
+
"name": "$return",
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
}, indent=2)
|
|
361
|
+
|
|
362
|
+
host_json = json.dumps({
|
|
363
|
+
"version": "2.0",
|
|
364
|
+
"logging": {
|
|
365
|
+
"applicationInsights": {
|
|
366
|
+
"samplingSettings": {
|
|
367
|
+
"isEnabled": True,
|
|
368
|
+
"excludedTypes": "Request"
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
"extensionBundle": {
|
|
373
|
+
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
|
374
|
+
"version": "[3.*, 4.0.0)"
|
|
375
|
+
},
|
|
376
|
+
"functionTimeout": f"00:{fc.timeout_seconds // 60:02d}:{fc.timeout_seconds % 60:02d}",
|
|
377
|
+
}, indent=2)
|
|
378
|
+
|
|
379
|
+
local_settings = json.dumps({
|
|
380
|
+
"IsEncrypted": False,
|
|
381
|
+
"Values": {
|
|
382
|
+
"AzureWebJobsStorage": "",
|
|
383
|
+
"FUNCTIONS_WORKER_RUNTIME": "python",
|
|
384
|
+
**fc.environment,
|
|
385
|
+
}
|
|
386
|
+
}, indent=2)
|
|
387
|
+
|
|
388
|
+
bicep = f'''@description('Location for all resources')
|
|
389
|
+
param location string = resourceGroup().location
|
|
390
|
+
|
|
391
|
+
@description('Function app name')
|
|
392
|
+
param functionAppName string = '{fc.name}'
|
|
393
|
+
|
|
394
|
+
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = {{
|
|
395
|
+
name: '${{functionAppName}}storage'
|
|
396
|
+
location: location
|
|
397
|
+
sku: {{
|
|
398
|
+
name: 'Standard_LRS'
|
|
399
|
+
}}
|
|
400
|
+
kind: 'StorageV2'
|
|
401
|
+
}}
|
|
402
|
+
|
|
403
|
+
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {{
|
|
404
|
+
name: '${{functionAppName}}-plan'
|
|
405
|
+
location: location
|
|
406
|
+
sku: {{
|
|
407
|
+
name: 'Y1'
|
|
408
|
+
tier: 'Dynamic'
|
|
409
|
+
}}
|
|
410
|
+
properties: {{
|
|
411
|
+
reserved: true
|
|
412
|
+
}}
|
|
413
|
+
}}
|
|
414
|
+
|
|
415
|
+
resource functionApp 'Microsoft.Web/sites@2022-03-01' = {{
|
|
416
|
+
name: functionAppName
|
|
417
|
+
location: location
|
|
418
|
+
kind: 'functionapp,linux'
|
|
419
|
+
properties: {{
|
|
420
|
+
serverFarmId: appServicePlan.id
|
|
421
|
+
siteConfig: {{
|
|
422
|
+
pythonVersion: '3.11'
|
|
423
|
+
linuxFxVersion: 'PYTHON|3.11'
|
|
424
|
+
appSettings: [
|
|
425
|
+
{{
|
|
426
|
+
name: 'AzureWebJobsStorage'
|
|
427
|
+
value: 'DefaultEndpointsProtocol=https;AccountName=${{storageAccount.name}};AccountKey=${{storageAccount.listKeys().keys[0].value}}'
|
|
428
|
+
}}
|
|
429
|
+
{{
|
|
430
|
+
name: 'FUNCTIONS_WORKER_RUNTIME'
|
|
431
|
+
value: 'python'
|
|
432
|
+
}}
|
|
433
|
+
{{
|
|
434
|
+
name: 'WEBSITE_RUN_FROM_PACKAGE'
|
|
435
|
+
value: '1'
|
|
436
|
+
}}
|
|
437
|
+
]
|
|
438
|
+
}}
|
|
439
|
+
}}
|
|
440
|
+
}}
|
|
441
|
+
|
|
442
|
+
output functionAppUrl string = 'https://${{functionApp.properties.defaultHostName}}/api/agent'
|
|
443
|
+
'''
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
"function_app/__init__.py": self.create_handler(""),
|
|
447
|
+
"function_app/function.json": function_json,
|
|
448
|
+
"host.json": host_json,
|
|
449
|
+
"local.settings.json": local_settings,
|
|
450
|
+
"deploy.bicep": bicep,
|
|
451
|
+
"requirements.txt": "\n".join(self.get_requirements()),
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
def get_requirements(self) -> List[str]:
|
|
455
|
+
return [
|
|
456
|
+
"ironshell>=0.3.0",
|
|
457
|
+
"azure-functions>=1.15.0",
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class GCPFunctionsRuntime(CloudRuntime):
|
|
462
|
+
"""Google Cloud Functions runtime adapter"""
|
|
463
|
+
|
|
464
|
+
def create_handler(self, agent_code: str) -> str:
|
|
465
|
+
"""Generate GCP Cloud Functions handler"""
|
|
466
|
+
return f'''"""
|
|
467
|
+
IronShell GCP Cloud Functions Handler
|
|
468
|
+
Auto-generated - do not edit directly
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
import json
|
|
472
|
+
import asyncio
|
|
473
|
+
import functions_framework
|
|
474
|
+
from flask import jsonify
|
|
475
|
+
from ironshell import IronShellRuntime, Agent
|
|
476
|
+
|
|
477
|
+
# Initialize runtime
|
|
478
|
+
runtime = IronShellRuntime()
|
|
479
|
+
|
|
480
|
+
{agent_code}
|
|
481
|
+
|
|
482
|
+
@functions_framework.http
|
|
483
|
+
def agent_handler(request):
|
|
484
|
+
"""
|
|
485
|
+
GCP Cloud Functions HTTP entry point.
|
|
486
|
+
"""
|
|
487
|
+
# Handle CORS preflight
|
|
488
|
+
if request.method == "OPTIONS":
|
|
489
|
+
headers = {{
|
|
490
|
+
"Access-Control-Allow-Origin": "*",
|
|
491
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
492
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
493
|
+
"Access-Control-Max-Age": "3600",
|
|
494
|
+
}}
|
|
495
|
+
return ("", 204, headers)
|
|
496
|
+
|
|
497
|
+
headers = {{"Access-Control-Allow-Origin": "*"}}
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
# Parse request
|
|
501
|
+
body = request.get_json(silent=True) or {{}}
|
|
502
|
+
goal = body.get("goal", "")
|
|
503
|
+
params = body.get("params", {{}})
|
|
504
|
+
|
|
505
|
+
# Run agent
|
|
506
|
+
result = asyncio.run(run_agent(goal, params))
|
|
507
|
+
|
|
508
|
+
return (jsonify(result), 200, headers)
|
|
509
|
+
except Exception as e:
|
|
510
|
+
return (jsonify({{"error": str(e)}}), 500, headers)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
async def run_agent(goal: str, params: dict):
|
|
514
|
+
"""Execute the agent"""
|
|
515
|
+
agent = create_agent(runtime)
|
|
516
|
+
result = await agent.run(goal)
|
|
517
|
+
return {{
|
|
518
|
+
"success": getattr(result, "success", True),
|
|
519
|
+
"result": getattr(result, "result", result),
|
|
520
|
+
}}
|
|
521
|
+
'''
|
|
522
|
+
|
|
523
|
+
def generate_deployment(self, config: DeploymentConfig) -> Dict[str, str]:
|
|
524
|
+
"""Generate GCP deployment configuration"""
|
|
525
|
+
fc = config.function_config
|
|
526
|
+
|
|
527
|
+
cloudbuild_yaml = f'''steps:
|
|
528
|
+
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
|
529
|
+
entrypoint: 'gcloud'
|
|
530
|
+
args:
|
|
531
|
+
- 'functions'
|
|
532
|
+
- 'deploy'
|
|
533
|
+
- '{fc.name}'
|
|
534
|
+
- '--gen2'
|
|
535
|
+
- '--runtime=python311'
|
|
536
|
+
- '--region={config.region}'
|
|
537
|
+
- '--source=.'
|
|
538
|
+
- '--entry-point=agent_handler'
|
|
539
|
+
- '--trigger-http'
|
|
540
|
+
- '--allow-unauthenticated'
|
|
541
|
+
- '--memory={fc.memory_mb}MB'
|
|
542
|
+
- '--timeout={fc.timeout_seconds}s'
|
|
543
|
+
{self._format_env_flags(fc.environment)}
|
|
544
|
+
'''
|
|
545
|
+
|
|
546
|
+
terraform = f'''terraform {{
|
|
547
|
+
required_providers {{
|
|
548
|
+
google = {{
|
|
549
|
+
source = "hashicorp/google"
|
|
550
|
+
version = "~> 4.0"
|
|
551
|
+
}}
|
|
552
|
+
}}
|
|
553
|
+
}}
|
|
554
|
+
|
|
555
|
+
provider "google" {{
|
|
556
|
+
project = var.project_id
|
|
557
|
+
region = "{config.region}"
|
|
558
|
+
}}
|
|
559
|
+
|
|
560
|
+
variable "project_id" {{
|
|
561
|
+
description = "GCP project ID"
|
|
562
|
+
type = string
|
|
563
|
+
}}
|
|
564
|
+
|
|
565
|
+
resource "google_storage_bucket" "function_bucket" {{
|
|
566
|
+
name = "${{var.project_id}}-{fc.name}-source"
|
|
567
|
+
location = "{config.region}"
|
|
568
|
+
}}
|
|
569
|
+
|
|
570
|
+
resource "google_storage_bucket_object" "function_source" {{
|
|
571
|
+
name = "function-source.zip"
|
|
572
|
+
bucket = google_storage_bucket.function_bucket.name
|
|
573
|
+
source = "function-source.zip"
|
|
574
|
+
}}
|
|
575
|
+
|
|
576
|
+
resource "google_cloudfunctions2_function" "agent" {{
|
|
577
|
+
name = "{fc.name}"
|
|
578
|
+
location = "{config.region}"
|
|
579
|
+
description = "IronShell agent function"
|
|
580
|
+
|
|
581
|
+
build_config {{
|
|
582
|
+
runtime = "python311"
|
|
583
|
+
entry_point = "agent_handler"
|
|
584
|
+
source {{
|
|
585
|
+
storage_source {{
|
|
586
|
+
bucket = google_storage_bucket.function_bucket.name
|
|
587
|
+
object = google_storage_bucket_object.function_source.name
|
|
588
|
+
}}
|
|
589
|
+
}}
|
|
590
|
+
}}
|
|
591
|
+
|
|
592
|
+
service_config {{
|
|
593
|
+
max_instance_count = {fc.concurrency or 100}
|
|
594
|
+
available_memory = "{fc.memory_mb}M"
|
|
595
|
+
timeout_seconds = {fc.timeout_seconds}
|
|
596
|
+
{self._format_tf_env_vars(fc.environment, 4)}
|
|
597
|
+
}}
|
|
598
|
+
}}
|
|
599
|
+
|
|
600
|
+
resource "google_cloud_run_service_iam_member" "invoker" {{
|
|
601
|
+
location = google_cloudfunctions2_function.agent.location
|
|
602
|
+
service = google_cloudfunctions2_function.agent.name
|
|
603
|
+
role = "roles/run.invoker"
|
|
604
|
+
member = "allUsers"
|
|
605
|
+
}}
|
|
606
|
+
|
|
607
|
+
output "function_url" {{
|
|
608
|
+
value = google_cloudfunctions2_function.agent.service_config[0].uri
|
|
609
|
+
}}
|
|
610
|
+
'''
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
"main.py": self.create_handler(""),
|
|
614
|
+
"cloudbuild.yaml": cloudbuild_yaml,
|
|
615
|
+
"main.tf": terraform,
|
|
616
|
+
"requirements.txt": "\n".join(self.get_requirements()),
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
def get_requirements(self) -> List[str]:
|
|
620
|
+
return [
|
|
621
|
+
"ironshell>=0.3.0",
|
|
622
|
+
"functions-framework>=3.0.0",
|
|
623
|
+
"flask>=2.0.0",
|
|
624
|
+
]
|
|
625
|
+
|
|
626
|
+
def _format_env_flags(self, env: Dict[str, str]) -> str:
|
|
627
|
+
if not env:
|
|
628
|
+
return ""
|
|
629
|
+
flags = [f" - '--set-env-vars={k}={v}'" for k, v in env.items()]
|
|
630
|
+
return "\n".join(flags)
|
|
631
|
+
|
|
632
|
+
def _format_tf_env_vars(self, env: Dict[str, str], indent: int) -> str:
|
|
633
|
+
if not env:
|
|
634
|
+
return ""
|
|
635
|
+
spaces = " " * indent
|
|
636
|
+
lines = [f"{spaces}environment_variables = {{"]
|
|
637
|
+
for k, v in env.items():
|
|
638
|
+
lines.append(f'{spaces} {k} = "{v}"')
|
|
639
|
+
lines.append(f"{spaces}}}")
|
|
640
|
+
return "\n".join(lines)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class KnativeRuntime(CloudRuntime):
|
|
644
|
+
"""Knative/Kubernetes runtime adapter"""
|
|
645
|
+
|
|
646
|
+
def create_handler(self, agent_code: str) -> str:
|
|
647
|
+
"""Generate Knative handler (FastAPI-based)"""
|
|
648
|
+
return f'''"""
|
|
649
|
+
IronShell Knative Handler
|
|
650
|
+
Auto-generated - do not edit directly
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
import json
|
|
654
|
+
import asyncio
|
|
655
|
+
from contextlib import asynccontextmanager
|
|
656
|
+
from fastapi import FastAPI, HTTPException
|
|
657
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
658
|
+
from pydantic import BaseModel
|
|
659
|
+
from typing import Dict, Any, Optional
|
|
660
|
+
from ironshell import IronShellRuntime, Agent
|
|
661
|
+
|
|
662
|
+
# Initialize runtime
|
|
663
|
+
runtime = IronShellRuntime()
|
|
664
|
+
|
|
665
|
+
{agent_code}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class AgentRequest(BaseModel):
|
|
669
|
+
goal: str
|
|
670
|
+
params: Optional[Dict[str, Any]] = {{}}
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class AgentResponse(BaseModel):
|
|
674
|
+
success: bool
|
|
675
|
+
result: Any
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@asynccontextmanager
|
|
679
|
+
async def lifespan(app: FastAPI):
|
|
680
|
+
# Startup: warm up the runtime
|
|
681
|
+
runtime.evaluate("1 + 1")
|
|
682
|
+
yield
|
|
683
|
+
# Shutdown
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
app = FastAPI(
|
|
687
|
+
title="IronShell Agent",
|
|
688
|
+
description="AI-powered agent service",
|
|
689
|
+
version="0.3.0",
|
|
690
|
+
lifespan=lifespan,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
app.add_middleware(
|
|
694
|
+
CORSMiddleware,
|
|
695
|
+
allow_origins=["*"],
|
|
696
|
+
allow_credentials=True,
|
|
697
|
+
allow_methods=["*"],
|
|
698
|
+
allow_headers=["*"],
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@app.get("/health")
|
|
703
|
+
async def health():
|
|
704
|
+
"""Health check endpoint"""
|
|
705
|
+
return {{"status": "healthy"}}
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@app.post("/agent", response_model=AgentResponse)
|
|
709
|
+
async def run_agent_endpoint(request: AgentRequest):
|
|
710
|
+
"""Run the agent with a goal"""
|
|
711
|
+
try:
|
|
712
|
+
agent = create_agent(runtime)
|
|
713
|
+
result = await agent.run(request.goal)
|
|
714
|
+
return AgentResponse(
|
|
715
|
+
success=getattr(result, "success", True),
|
|
716
|
+
result=getattr(result, "result", result),
|
|
717
|
+
)
|
|
718
|
+
except Exception as e:
|
|
719
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
if __name__ == "__main__":
|
|
723
|
+
import uvicorn
|
|
724
|
+
uvicorn.run(app, host="0.0.0.0", port=8080)
|
|
725
|
+
'''
|
|
726
|
+
|
|
727
|
+
def generate_deployment(self, config: DeploymentConfig) -> Dict[str, str]:
|
|
728
|
+
"""Generate Kubernetes/Knative manifests"""
|
|
729
|
+
fc = config.function_config
|
|
730
|
+
|
|
731
|
+
dockerfile = f'''FROM python:3.11-slim
|
|
732
|
+
|
|
733
|
+
WORKDIR /app
|
|
734
|
+
|
|
735
|
+
COPY requirements.txt .
|
|
736
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
737
|
+
|
|
738
|
+
COPY . .
|
|
739
|
+
|
|
740
|
+
ENV PORT=8080
|
|
741
|
+
EXPOSE 8080
|
|
742
|
+
|
|
743
|
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
|
|
744
|
+
'''
|
|
745
|
+
|
|
746
|
+
knative_service = f'''apiVersion: serving.knative.dev/v1
|
|
747
|
+
kind: Service
|
|
748
|
+
metadata:
|
|
749
|
+
name: {fc.name}
|
|
750
|
+
labels:
|
|
751
|
+
app: ironshell-agent
|
|
752
|
+
spec:
|
|
753
|
+
template:
|
|
754
|
+
metadata:
|
|
755
|
+
annotations:
|
|
756
|
+
autoscaling.knative.dev/minScale: "0"
|
|
757
|
+
autoscaling.knative.dev/maxScale: "{fc.concurrency or 10}"
|
|
758
|
+
spec:
|
|
759
|
+
containerConcurrency: 10
|
|
760
|
+
timeoutSeconds: {fc.timeout_seconds}
|
|
761
|
+
containers:
|
|
762
|
+
- image: ${{IMAGE_URL}}
|
|
763
|
+
ports:
|
|
764
|
+
- containerPort: 8080
|
|
765
|
+
resources:
|
|
766
|
+
limits:
|
|
767
|
+
memory: {fc.memory_mb}Mi
|
|
768
|
+
cpu: "1"
|
|
769
|
+
env:
|
|
770
|
+
{self._format_k8s_env(fc.environment, 12)}
|
|
771
|
+
readinessProbe:
|
|
772
|
+
httpGet:
|
|
773
|
+
path: /health
|
|
774
|
+
initialDelaySeconds: 5
|
|
775
|
+
'''
|
|
776
|
+
|
|
777
|
+
k8s_deployment = f'''apiVersion: apps/v1
|
|
778
|
+
kind: Deployment
|
|
779
|
+
metadata:
|
|
780
|
+
name: {fc.name}
|
|
781
|
+
labels:
|
|
782
|
+
app: ironshell-agent
|
|
783
|
+
spec:
|
|
784
|
+
replicas: 1
|
|
785
|
+
selector:
|
|
786
|
+
matchLabels:
|
|
787
|
+
app: {fc.name}
|
|
788
|
+
template:
|
|
789
|
+
metadata:
|
|
790
|
+
labels:
|
|
791
|
+
app: {fc.name}
|
|
792
|
+
spec:
|
|
793
|
+
containers:
|
|
794
|
+
- name: agent
|
|
795
|
+
image: ${{IMAGE_URL}}
|
|
796
|
+
ports:
|
|
797
|
+
- containerPort: 8080
|
|
798
|
+
resources:
|
|
799
|
+
requests:
|
|
800
|
+
memory: "{fc.memory_mb // 2}Mi"
|
|
801
|
+
cpu: "250m"
|
|
802
|
+
limits:
|
|
803
|
+
memory: "{fc.memory_mb}Mi"
|
|
804
|
+
cpu: "1"
|
|
805
|
+
env:
|
|
806
|
+
{self._format_k8s_env(fc.environment, 12)}
|
|
807
|
+
livenessProbe:
|
|
808
|
+
httpGet:
|
|
809
|
+
path: /health
|
|
810
|
+
port: 8080
|
|
811
|
+
initialDelaySeconds: 10
|
|
812
|
+
readinessProbe:
|
|
813
|
+
httpGet:
|
|
814
|
+
path: /health
|
|
815
|
+
port: 8080
|
|
816
|
+
initialDelaySeconds: 5
|
|
817
|
+
---
|
|
818
|
+
apiVersion: v1
|
|
819
|
+
kind: Service
|
|
820
|
+
metadata:
|
|
821
|
+
name: {fc.name}
|
|
822
|
+
spec:
|
|
823
|
+
selector:
|
|
824
|
+
app: {fc.name}
|
|
825
|
+
ports:
|
|
826
|
+
- port: 80
|
|
827
|
+
targetPort: 8080
|
|
828
|
+
type: ClusterIP
|
|
829
|
+
---
|
|
830
|
+
apiVersion: networking.k8s.io/v1
|
|
831
|
+
kind: Ingress
|
|
832
|
+
metadata:
|
|
833
|
+
name: {fc.name}
|
|
834
|
+
annotations:
|
|
835
|
+
kubernetes.io/ingress.class: nginx
|
|
836
|
+
spec:
|
|
837
|
+
rules:
|
|
838
|
+
- host: {config.custom_domain or f"{fc.name}.example.com"}
|
|
839
|
+
http:
|
|
840
|
+
paths:
|
|
841
|
+
- path: /
|
|
842
|
+
pathType: Prefix
|
|
843
|
+
backend:
|
|
844
|
+
service:
|
|
845
|
+
name: {fc.name}
|
|
846
|
+
port:
|
|
847
|
+
number: 80
|
|
848
|
+
'''
|
|
849
|
+
|
|
850
|
+
skaffold = f'''apiVersion: skaffold/v4beta6
|
|
851
|
+
kind: Config
|
|
852
|
+
metadata:
|
|
853
|
+
name: {fc.name}
|
|
854
|
+
build:
|
|
855
|
+
artifacts:
|
|
856
|
+
- image: {fc.name}
|
|
857
|
+
docker:
|
|
858
|
+
dockerfile: Dockerfile
|
|
859
|
+
deploy:
|
|
860
|
+
kubectl:
|
|
861
|
+
manifests:
|
|
862
|
+
- k8s/*.yaml
|
|
863
|
+
'''
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
"main.py": self.create_handler(""),
|
|
867
|
+
"Dockerfile": dockerfile,
|
|
868
|
+
"knative/service.yaml": knative_service,
|
|
869
|
+
"k8s/deployment.yaml": k8s_deployment,
|
|
870
|
+
"skaffold.yaml": skaffold,
|
|
871
|
+
"requirements.txt": "\n".join(self.get_requirements()),
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
def get_requirements(self) -> List[str]:
|
|
875
|
+
return [
|
|
876
|
+
"ironshell>=0.3.0",
|
|
877
|
+
"fastapi>=0.100.0",
|
|
878
|
+
"uvicorn>=0.23.0",
|
|
879
|
+
"pydantic>=2.0.0",
|
|
880
|
+
]
|
|
881
|
+
|
|
882
|
+
def _format_k8s_env(self, env: Dict[str, str], indent: int) -> str:
|
|
883
|
+
if not env:
|
|
884
|
+
spaces = " " * indent
|
|
885
|
+
return f"{spaces}- name: IRONSHELL_AI\n{spaces} value: openai"
|
|
886
|
+
|
|
887
|
+
lines = []
|
|
888
|
+
spaces = " " * indent
|
|
889
|
+
for k, v in env.items():
|
|
890
|
+
lines.append(f"{spaces}- name: {k}")
|
|
891
|
+
lines.append(f"{spaces} value: \"{v}\"")
|
|
892
|
+
return "\n".join(lines)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
# ============================================================================
|
|
896
|
+
# Factory Functions
|
|
897
|
+
# ============================================================================
|
|
898
|
+
|
|
899
|
+
def get_runtime(provider: CloudProvider) -> CloudRuntime:
|
|
900
|
+
"""Get the appropriate runtime for a cloud provider"""
|
|
901
|
+
runtimes = {
|
|
902
|
+
CloudProvider.AWS_LAMBDA: LambdaRuntime(),
|
|
903
|
+
CloudProvider.AZURE_FUNCTIONS: AzureFunctionsRuntime(),
|
|
904
|
+
CloudProvider.GCP_FUNCTIONS: GCPFunctionsRuntime(),
|
|
905
|
+
CloudProvider.KNATIVE: KnativeRuntime(),
|
|
906
|
+
}
|
|
907
|
+
return runtimes[provider]
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def create_handler(
|
|
911
|
+
provider: CloudProvider,
|
|
912
|
+
agent_code: str,
|
|
913
|
+
) -> str:
|
|
914
|
+
"""
|
|
915
|
+
Create a cloud function handler for an agent.
|
|
916
|
+
|
|
917
|
+
Args:
|
|
918
|
+
provider: Target cloud provider
|
|
919
|
+
agent_code: Agent creation code (should define create_agent function)
|
|
920
|
+
|
|
921
|
+
Returns:
|
|
922
|
+
Handler code as string
|
|
923
|
+
"""
|
|
924
|
+
runtime = get_runtime(provider)
|
|
925
|
+
return runtime.create_handler(agent_code)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def deploy_agent(
|
|
929
|
+
config: DeploymentConfig,
|
|
930
|
+
agent_code: str,
|
|
931
|
+
output_dir: str = ".",
|
|
932
|
+
) -> Dict[str, str]:
|
|
933
|
+
"""
|
|
934
|
+
Generate deployment files for an agent.
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
config: Deployment configuration
|
|
938
|
+
agent_code: Agent creation code
|
|
939
|
+
output_dir: Directory to write files
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
Dictionary of filename -> content
|
|
943
|
+
"""
|
|
944
|
+
import os
|
|
945
|
+
|
|
946
|
+
runtime = get_runtime(config.provider)
|
|
947
|
+
files = runtime.generate_deployment(config)
|
|
948
|
+
|
|
949
|
+
# Add the handler with agent code
|
|
950
|
+
if config.provider == CloudProvider.AWS_LAMBDA:
|
|
951
|
+
files["handler.py"] = runtime.create_handler(agent_code)
|
|
952
|
+
elif config.provider == CloudProvider.AZURE_FUNCTIONS:
|
|
953
|
+
files["function_app/__init__.py"] = runtime.create_handler(agent_code)
|
|
954
|
+
elif config.provider == CloudProvider.GCP_FUNCTIONS:
|
|
955
|
+
files["main.py"] = runtime.create_handler(agent_code)
|
|
956
|
+
elif config.provider == CloudProvider.KNATIVE:
|
|
957
|
+
files["main.py"] = runtime.create_handler(agent_code)
|
|
958
|
+
|
|
959
|
+
# Write files if output_dir specified
|
|
960
|
+
if output_dir:
|
|
961
|
+
for filename, content in files.items():
|
|
962
|
+
filepath = os.path.join(output_dir, filename)
|
|
963
|
+
os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True)
|
|
964
|
+
with open(filepath, "w") as f:
|
|
965
|
+
f.write(content)
|
|
966
|
+
|
|
967
|
+
return files
|