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/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