signalwire-agents 1.0.7__py3-none-any.whl → 1.0.17.dev4__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.
Files changed (24) hide show
  1. signalwire_agents/__init__.py +1 -1
  2. signalwire_agents/agent_server.py +103 -68
  3. signalwire_agents/cli/dokku.py +2320 -0
  4. signalwire_agents/cli/init_project.py +1503 -92
  5. signalwire_agents/core/agent_base.py +25 -5
  6. signalwire_agents/core/mixins/auth_mixin.py +6 -13
  7. signalwire_agents/core/mixins/serverless_mixin.py +204 -112
  8. signalwire_agents/core/mixins/web_mixin.py +14 -6
  9. signalwire_agents/core/swml_service.py +4 -3
  10. signalwire_agents/mcp_gateway/__init__.py +29 -0
  11. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  12. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  13. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  14. signalwire_agents/search/pgvector_backend.py +10 -14
  15. signalwire_agents/skills/__init__.py +4 -1
  16. {signalwire_agents-1.0.7.data → signalwire_agents-1.0.17.dev4.data}/data/share/man/man1/sw-agent-init.1 +107 -14
  17. {signalwire_agents-1.0.7.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +4 -1
  18. {signalwire_agents-1.0.7.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/RECORD +24 -19
  19. {signalwire_agents-1.0.7.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/entry_points.txt +2 -0
  20. {signalwire_agents-1.0.7.data → signalwire_agents-1.0.17.dev4.data}/data/share/man/man1/sw-search.1 +0 -0
  21. {signalwire_agents-1.0.7.data → signalwire_agents-1.0.17.dev4.data}/data/share/man/man1/swaig-test.1 +0 -0
  22. {signalwire_agents-1.0.7.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  23. {signalwire_agents-1.0.7.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  24. {signalwire_agents-1.0.7.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,22 @@ from pathlib import Path
18
18
  from typing import Optional, Dict, List, Any
19
19
 
20
20
 
21
+ # Cloud platform options
22
+ CLOUD_PLATFORMS = {
23
+ "local": "Local Agent (FastAPI/uvicorn server)",
24
+ "aws": "AWS Lambda Function",
25
+ "gcp": "Google Cloud Function",
26
+ "azure": "Azure Function",
27
+ }
28
+
29
+ # Default regions per platform
30
+ DEFAULT_REGIONS = {
31
+ "aws": "us-east-1",
32
+ "gcp": "us-central1",
33
+ "azure": "eastus",
34
+ }
35
+
36
+
21
37
  # ANSI colors
22
38
  class Colors:
23
39
  RED = '\033[0;31m'
@@ -195,12 +211,968 @@ SWML_PROXY_URL_BASE=https://your-domain.ngrok.io
195
211
  DEBUG_WEBHOOK_LEVEL=1
196
212
  '''
197
213
 
198
- TEMPLATE_REQUIREMENTS = '''signalwire-agents>=1.0.6
214
+ TEMPLATE_REQUIREMENTS = '''signalwire-agents>=1.0.10
199
215
  python-dotenv>=1.0.0
200
216
  requests>=2.28.0
201
217
  pytest>=7.0.0
202
218
  '''
203
219
 
220
+ # =============================================================================
221
+ # Cloud Function Templates
222
+ # =============================================================================
223
+
224
+ # AWS Lambda Templates
225
+ AWS_REQUIREMENTS_TEMPLATE = '''signalwire-agents>=1.0.10
226
+ h11>=0.13,<0.15
227
+ fastapi
228
+ mangum
229
+ uvicorn
230
+ '''
231
+
232
+ AWS_HANDLER_TEMPLATE = '''#!/usr/bin/env python3
233
+ """AWS Lambda handler for {agent_name} agent.
234
+
235
+ This demonstrates deploying a SignalWire AI Agent to AWS Lambda
236
+ with SWAIG functions and SWML output.
237
+
238
+ Environment variables:
239
+ SWML_BASIC_AUTH_USER: Basic auth username (optional)
240
+ SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)
241
+ """
242
+
243
+ import os
244
+ from signalwire_agents import AgentBase, SwaigFunctionResult
245
+
246
+
247
+ class {agent_class}(AgentBase):
248
+ """{agent_name} agent for AWS Lambda deployment."""
249
+
250
+ def __init__(self):
251
+ super().__init__(name="{agent_name_slug}")
252
+
253
+ self._configure_prompts()
254
+ self.add_language("English", "en-US", "rime.spore")
255
+ self._setup_functions()
256
+
257
+ def _configure_prompts(self):
258
+ self.prompt_add_section(
259
+ "Role",
260
+ "You are a helpful AI assistant deployed on AWS Lambda."
261
+ )
262
+
263
+ self.prompt_add_section(
264
+ "Guidelines",
265
+ bullets=[
266
+ "Be professional and courteous",
267
+ "Ask clarifying questions when needed",
268
+ "Keep responses concise and helpful"
269
+ ]
270
+ )
271
+
272
+ def _setup_functions(self):
273
+ @self.tool(
274
+ description="Get information about a topic",
275
+ parameters={{
276
+ "type": "object",
277
+ "properties": {{
278
+ "topic": {{
279
+ "type": "string",
280
+ "description": "The topic to get information about"
281
+ }}
282
+ }},
283
+ "required": ["topic"]
284
+ }}
285
+ )
286
+ def get_info(args, raw_data):
287
+ topic = args.get("topic", "")
288
+ return SwaigFunctionResult(
289
+ f"Information about {{topic}}: This is a placeholder response."
290
+ )
291
+
292
+ @self.tool(description="Get AWS Lambda deployment information")
293
+ def get_platform_info(args, raw_data):
294
+ region = os.getenv("AWS_REGION", "unknown")
295
+ function_name = os.getenv("AWS_LAMBDA_FUNCTION_NAME", "unknown")
296
+ memory = os.getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "unknown")
297
+ runtime = os.getenv("AWS_EXECUTION_ENV", "unknown")
298
+
299
+ return SwaigFunctionResult(
300
+ f"Running on AWS Lambda. "
301
+ f"Function: {{function_name}}, Region: {{region}}, "
302
+ f"Memory: {{memory}}MB, Runtime: {{runtime}}."
303
+ )
304
+
305
+
306
+ # Create agent instance outside handler for warm starts
307
+ agent = {agent_class}()
308
+
309
+
310
+ def lambda_handler(event, context):
311
+ """AWS Lambda entry point.
312
+
313
+ Args:
314
+ event: Lambda event (API Gateway request)
315
+ context: Lambda context with runtime info
316
+
317
+ Returns:
318
+ API Gateway response dict
319
+ """
320
+ return agent.run(event, context)
321
+ '''
322
+
323
+ AWS_DEPLOY_TEMPLATE = '''#!/bin/bash
324
+ # AWS Lambda deployment script for {agent_name} agent
325
+ #
326
+ # Prerequisites:
327
+ # - AWS CLI configured with appropriate credentials
328
+ # - Docker installed and running (for building Lambda-compatible packages)
329
+ #
330
+ # Usage:
331
+ # ./deploy.sh # Deploy with defaults
332
+ # ./deploy.sh my-function # Deploy with custom function name
333
+ # ./deploy.sh my-function us-west-2 # Custom function and region
334
+
335
+ set -e
336
+
337
+ # Configuration
338
+ FUNCTION_NAME="${{1:-{function_name}}}"
339
+ REGION="${{2:-{region}}}"
340
+ RUNTIME="python3.11"
341
+ HANDLER="handler.lambda_handler"
342
+ MEMORY_SIZE=512
343
+ TIMEOUT=30
344
+ ROLE_NAME="${{FUNCTION_NAME}}-role"
345
+
346
+ # Default credentials (change these or set via environment)
347
+ AUTH_USER="${{SWML_BASIC_AUTH_USER:-{auth_user}}}"
348
+ AUTH_PASS="${{SWML_BASIC_AUTH_PASSWORD:-{auth_password}}}"
349
+
350
+ echo "=== {agent_name} - AWS Lambda Deployment ==="
351
+ echo "Function: $FUNCTION_NAME"
352
+ echo "Region: $REGION"
353
+ echo ""
354
+
355
+ # Check for Docker
356
+ if ! command -v docker &> /dev/null; then
357
+ echo "ERROR: Docker is required but not installed."
358
+ echo "Please install Docker: https://docs.docker.com/get-docker/"
359
+ exit 1
360
+ fi
361
+
362
+ if ! docker info &> /dev/null; then
363
+ echo "ERROR: Docker is not running. Please start Docker."
364
+ exit 1
365
+ fi
366
+
367
+ # Get AWS account ID
368
+ ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
369
+ ROLE_ARN="arn:aws:iam::${{ACCOUNT_ID}}:role/${{ROLE_NAME}}"
370
+
371
+ # Step 1: Create IAM role if it doesn't exist
372
+ echo "Step 1: Setting up IAM role..."
373
+
374
+ TRUST_POLICY='{{
375
+ "Version": "2012-10-17",
376
+ "Statement": [
377
+ {{
378
+ "Effect": "Allow",
379
+ "Principal": {{
380
+ "Service": "lambda.amazonaws.com"
381
+ }},
382
+ "Action": "sts:AssumeRole"
383
+ }}
384
+ ]
385
+ }}'
386
+
387
+ if ! aws iam get-role --role-name "$ROLE_NAME" --region "$REGION" 2>/dev/null; then
388
+ echo "Creating IAM role: $ROLE_NAME"
389
+ aws iam create-role \\
390
+ --role-name "$ROLE_NAME" \\
391
+ --assume-role-policy-document "$TRUST_POLICY" \\
392
+ --region "$REGION"
393
+
394
+ # Attach basic Lambda execution policy
395
+ aws iam attach-role-policy \\
396
+ --role-name "$ROLE_NAME" \\
397
+ --policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" \\
398
+ --region "$REGION"
399
+
400
+ echo "Waiting for role to propagate..."
401
+ sleep 10
402
+ else
403
+ echo "IAM role already exists: $ROLE_NAME"
404
+ fi
405
+
406
+ # Step 2: Package the function using Docker
407
+ echo ""
408
+ echo "Step 2: Packaging function with Docker (linux/amd64)..."
409
+
410
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
411
+ BUILD_DIR=$(mktemp -d)
412
+ PACKAGE_DIR="$BUILD_DIR/package"
413
+ ZIP_FILE="$BUILD_DIR/function.zip"
414
+
415
+ mkdir -p "$PACKAGE_DIR"
416
+
417
+ # Build dependencies using Lambda Python image for correct architecture
418
+ echo "Installing dependencies via Docker..."
419
+ docker run --rm \\
420
+ --platform linux/amd64 \\
421
+ --entrypoint "" \\
422
+ -v "$SCRIPT_DIR:/var/task:ro" \\
423
+ -v "$PACKAGE_DIR:/var/output" \\
424
+ -w /var/task \\
425
+ public.ecr.aws/lambda/python:3.11 \\
426
+ bash -c "pip install -r requirements.txt -t /var/output --quiet && cp handler.py /var/output/"
427
+
428
+ # Create zip
429
+ echo "Creating deployment package..."
430
+ cd "$PACKAGE_DIR"
431
+ zip -r "$ZIP_FILE" . -q
432
+ cd - > /dev/null
433
+
434
+ PACKAGE_SIZE=$(du -h "$ZIP_FILE" | cut -f1)
435
+ echo "Package size: $PACKAGE_SIZE"
436
+
437
+ # Step 3: Create or update Lambda function
438
+ echo ""
439
+ echo "Step 3: Deploying Lambda function..."
440
+
441
+ if aws lambda get-function --function-name "$FUNCTION_NAME" --region "$REGION" 2>/dev/null; then
442
+ echo "Updating existing function..."
443
+ aws lambda update-function-code \\
444
+ --function-name "$FUNCTION_NAME" \\
445
+ --zip-file "fileb://$ZIP_FILE" \\
446
+ --region "$REGION" \\
447
+ --cli-read-timeout 300 \\
448
+ --output text --query 'FunctionArn'
449
+ else
450
+ echo "Creating new function..."
451
+ aws lambda create-function \\
452
+ --function-name "$FUNCTION_NAME" \\
453
+ --runtime "$RUNTIME" \\
454
+ --role "$ROLE_ARN" \\
455
+ --handler "$HANDLER" \\
456
+ --zip-file "fileb://$ZIP_FILE" \\
457
+ --memory-size "$MEMORY_SIZE" \\
458
+ --timeout "$TIMEOUT" \\
459
+ --region "$REGION" \\
460
+ --cli-read-timeout 300 \\
461
+ --environment "Variables={{SWML_BASIC_AUTH_USER=$AUTH_USER,SWML_BASIC_AUTH_PASSWORD=$AUTH_PASS}}" \\
462
+ --output text --query 'FunctionArn'
463
+ fi
464
+
465
+ # Wait for function to be active
466
+ echo "Waiting for function to be active..."
467
+ aws lambda wait function-active --function-name "$FUNCTION_NAME" --region "$REGION"
468
+
469
+ # Step 4: Create or get API Gateway
470
+ echo ""
471
+ echo "Step 4: Setting up API Gateway..."
472
+
473
+ API_NAME="${{FUNCTION_NAME}}-api"
474
+
475
+ # Check if API exists
476
+ API_ID=$(aws apigatewayv2 get-apis --region "$REGION" \\
477
+ --query "Items[?Name=='$API_NAME'].ApiId" --output text)
478
+
479
+ if [ -z "$API_ID" ] || [ "$API_ID" == "None" ]; then
480
+ echo "Creating HTTP API..."
481
+ API_ID=$(aws apigatewayv2 create-api \\
482
+ --name "$API_NAME" \\
483
+ --protocol-type HTTP \\
484
+ --region "$REGION" \\
485
+ --output text --query 'ApiId')
486
+ fi
487
+
488
+ echo "API ID: $API_ID"
489
+
490
+ # Step 5: Create Lambda integration
491
+ echo ""
492
+ echo "Step 5: Creating Lambda integration..."
493
+
494
+ LAMBDA_ARN="arn:aws:lambda:${{REGION}}:${{ACCOUNT_ID}}:function:${{FUNCTION_NAME}}"
495
+
496
+ # Check for existing integration
497
+ INTEGRATION_ID=$(aws apigatewayv2 get-integrations \\
498
+ --api-id "$API_ID" \\
499
+ --region "$REGION" \\
500
+ --query "Items[?IntegrationUri=='${{LAMBDA_ARN}}'].IntegrationId" \\
501
+ --output text 2>/dev/null || echo "")
502
+
503
+ if [ -z "$INTEGRATION_ID" ] || [ "$INTEGRATION_ID" == "None" ]; then
504
+ echo "Creating integration..."
505
+ INTEGRATION_ID=$(aws apigatewayv2 create-integration \\
506
+ --api-id "$API_ID" \\
507
+ --integration-type AWS_PROXY \\
508
+ --integration-uri "$LAMBDA_ARN" \\
509
+ --payload-format-version "2.0" \\
510
+ --region "$REGION" \\
511
+ --output text --query 'IntegrationId')
512
+ fi
513
+
514
+ echo "Integration ID: $INTEGRATION_ID"
515
+
516
+ # Step 6: Create routes
517
+ echo ""
518
+ echo "Step 6: Creating routes..."
519
+
520
+ create_route() {{
521
+ local route_key="$1"
522
+ local existing=$(aws apigatewayv2 get-routes \\
523
+ --api-id "$API_ID" \\
524
+ --region "$REGION" \\
525
+ --query "Items[?RouteKey=='$route_key'].RouteId" \\
526
+ --output text 2>/dev/null || echo "")
527
+
528
+ if [ -z "$existing" ] || [ "$existing" == "None" ]; then
529
+ echo "Creating route: $route_key"
530
+ aws apigatewayv2 create-route \\
531
+ --api-id "$API_ID" \\
532
+ --route-key "$route_key" \\
533
+ --target "integrations/$INTEGRATION_ID" \\
534
+ --region "$REGION" \\
535
+ --output text --query 'RouteId'
536
+ else
537
+ echo "Route exists: $route_key"
538
+ fi
539
+ }}
540
+
541
+ # Create routes for SWML and SWAIG
542
+ create_route "GET /"
543
+ create_route "POST /"
544
+ create_route "POST /swaig"
545
+ create_route "ANY /{{proxy+}}"
546
+
547
+ # Step 7: Create/update stage
548
+ echo ""
549
+ echo "Step 7: Deploying stage..."
550
+
551
+ STAGE_NAME="\\$default"
552
+
553
+ if ! aws apigatewayv2 get-stage --api-id "$API_ID" --stage-name "$STAGE_NAME" --region "$REGION" 2>/dev/null; then
554
+ aws apigatewayv2 create-stage \\
555
+ --api-id "$API_ID" \\
556
+ --stage-name "$STAGE_NAME" \\
557
+ --auto-deploy \\
558
+ --region "$REGION" > /dev/null
559
+ fi
560
+
561
+ # Step 8: Add Lambda permission for API Gateway
562
+ echo ""
563
+ echo "Step 8: Configuring permissions..."
564
+
565
+ STATEMENT_ID="${{API_NAME}}-invoke"
566
+
567
+ # Remove existing permission if it exists (ignore errors)
568
+ aws lambda remove-permission \\
569
+ --function-name "$FUNCTION_NAME" \\
570
+ --statement-id "$STATEMENT_ID" \\
571
+ --region "$REGION" 2>/dev/null || true
572
+
573
+ # Add permission
574
+ aws lambda add-permission \\
575
+ --function-name "$FUNCTION_NAME" \\
576
+ --statement-id "$STATEMENT_ID" \\
577
+ --action lambda:InvokeFunction \\
578
+ --principal apigateway.amazonaws.com \\
579
+ --source-arn "arn:aws:execute-api:${{REGION}}:${{ACCOUNT_ID}}:${{API_ID}}/*" \\
580
+ --region "$REGION" > /dev/null
581
+
582
+ # Get the endpoint URL
583
+ ENDPOINT="https://${{API_ID}}.execute-api.${{REGION}}.amazonaws.com"
584
+
585
+ # Cleanup
586
+ rm -rf "$BUILD_DIR"
587
+
588
+ echo ""
589
+ echo "=== Deployment Complete ==="
590
+ echo ""
591
+ echo "Endpoint URL: $ENDPOINT"
592
+ echo ""
593
+ echo "Authentication:"
594
+ echo " Username: $AUTH_USER"
595
+ echo " Password: $AUTH_PASS"
596
+ echo ""
597
+ echo "Test SWML output:"
598
+ echo " curl -u $AUTH_USER:$AUTH_PASS $ENDPOINT/"
599
+ echo ""
600
+ echo "Test SWAIG function:"
601
+ echo " curl -u $AUTH_USER:$AUTH_PASS -X POST $ENDPOINT/swaig \\\\"
602
+ echo " -H 'Content-Type: application/json' \\\\"
603
+ echo " -d '{{\\\"function\\\": \\\"get_info\\\", \\\"argument\\\": {{\\\"parsed\\\": [{{\\\"topic\\\": \\\"test\\\"}}]}}}}'"
604
+ echo ""
605
+ echo "Configure SignalWire:"
606
+ echo " Set your phone number's SWML URL to: https://$AUTH_USER:$AUTH_PASS@${{API_ID}}.execute-api.${{REGION}}.amazonaws.com/"
607
+ echo ""
608
+ '''
609
+
610
+ # GCP Cloud Function Templates
611
+ GCP_REQUIREMENTS_TEMPLATE = '''signalwire-agents>=1.0.10
612
+ functions-framework>=3.0.0
613
+ '''
614
+
615
+ GCP_MAIN_TEMPLATE = '''#!/usr/bin/env python3
616
+ """Google Cloud Functions handler for {agent_name} agent.
617
+
618
+ This demonstrates deploying a SignalWire AI Agent to Google Cloud Functions
619
+ with SWAIG functions and SWML output.
620
+
621
+ Environment variables:
622
+ SWML_BASIC_AUTH_USER: Basic auth username (optional)
623
+ SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)
624
+ """
625
+
626
+ import os
627
+ from signalwire_agents import AgentBase, SwaigFunctionResult
628
+
629
+
630
+ class {agent_class}(AgentBase):
631
+ """{agent_name} agent for Google Cloud Functions deployment."""
632
+
633
+ def __init__(self):
634
+ super().__init__(name="{agent_name_slug}")
635
+
636
+ self._configure_prompts()
637
+ self.add_language("English", "en-US", "rime.spore")
638
+ self._setup_functions()
639
+
640
+ def _configure_prompts(self):
641
+ self.prompt_add_section(
642
+ "Role",
643
+ "You are a helpful AI assistant deployed on Google Cloud Functions."
644
+ )
645
+
646
+ self.prompt_add_section(
647
+ "Guidelines",
648
+ bullets=[
649
+ "Be professional and courteous",
650
+ "Ask clarifying questions when needed",
651
+ "Keep responses concise and helpful"
652
+ ]
653
+ )
654
+
655
+ def _setup_functions(self):
656
+ @self.tool(
657
+ description="Get information about a topic",
658
+ parameters={{
659
+ "type": "object",
660
+ "properties": {{
661
+ "topic": {{
662
+ "type": "string",
663
+ "description": "The topic to get information about"
664
+ }}
665
+ }},
666
+ "required": ["topic"]
667
+ }}
668
+ )
669
+ def get_info(args, raw_data):
670
+ topic = args.get("topic", "")
671
+ return SwaigFunctionResult(
672
+ f"Information about {{topic}}: This is a placeholder response."
673
+ )
674
+
675
+ @self.tool(description="Get Google Cloud deployment information")
676
+ def get_platform_info(args, raw_data):
677
+ import urllib.request
678
+
679
+ # Gen 2 Cloud Functions run on Cloud Run with these env vars
680
+ service = os.getenv("K_SERVICE", "unknown")
681
+ revision = os.getenv("K_REVISION", "unknown")
682
+
683
+ # Query metadata server for project ID
684
+ project = os.getenv("GOOGLE_CLOUD_PROJECT", "unknown")
685
+ if project == "unknown":
686
+ try:
687
+ req = urllib.request.Request(
688
+ "http://metadata.google.internal/computeMetadata/v1/project/project-id",
689
+ headers={{"Metadata-Flavor": "Google"}}
690
+ )
691
+ with urllib.request.urlopen(req, timeout=2) as resp:
692
+ project = resp.read().decode()
693
+ except Exception:
694
+ pass
695
+
696
+ return SwaigFunctionResult(
697
+ f"Running on Google Cloud Functions Gen 2. "
698
+ f"Service: {{service}}, Revision: {{revision}}, "
699
+ f"Project: {{project}}."
700
+ )
701
+
702
+
703
+ # Create agent instance outside handler for warm starts
704
+ agent = {agent_class}()
705
+
706
+
707
+ def main(request):
708
+ """Google Cloud Functions entry point.
709
+
710
+ Args:
711
+ request: Flask request object
712
+
713
+ Returns:
714
+ Flask response
715
+ """
716
+ return agent.run(request)
717
+ '''
718
+
719
+ GCP_DEPLOY_TEMPLATE = '''#!/bin/bash
720
+ # Google Cloud Functions deployment script for {agent_name} agent
721
+ #
722
+ # Prerequisites:
723
+ # - gcloud CLI installed and authenticated
724
+ # - A Google Cloud project with Cloud Functions API enabled
725
+ #
726
+ # Usage:
727
+ # ./deploy.sh # Deploy with defaults
728
+ # ./deploy.sh my-function # Custom function name
729
+ # ./deploy.sh my-function us-central1 # Custom function and region
730
+
731
+ set -e
732
+
733
+ # Configuration
734
+ FUNCTION_NAME="${{1:-{function_name}}}"
735
+ REGION="${{2:-{region}}}"
736
+ RUNTIME="python311"
737
+ ENTRY_POINT="main"
738
+ MEMORY="512MB"
739
+ TIMEOUT="60s"
740
+ MIN_INSTANCES=0
741
+ MAX_INSTANCES=10
742
+
743
+ # Directory containing this script
744
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
745
+
746
+ echo "=== {agent_name} - Google Cloud Functions Deployment ==="
747
+ echo "Function: $FUNCTION_NAME"
748
+ echo "Region: $REGION"
749
+ echo ""
750
+
751
+ # Get current project
752
+ PROJECT=$(gcloud config get-value project 2>/dev/null)
753
+ if [ -z "$PROJECT" ]; then
754
+ echo "Error: No project set. Run: gcloud config set project <project-id>"
755
+ exit 1
756
+ fi
757
+ echo "Project: $PROJECT"
758
+ echo ""
759
+
760
+ # Step 1: Enable required APIs
761
+ echo "Step 1: Enabling required APIs..."
762
+ gcloud services enable cloudfunctions.googleapis.com --quiet
763
+ gcloud services enable cloudbuild.googleapis.com --quiet
764
+ gcloud services enable artifactregistry.googleapis.com --quiet
765
+
766
+ # Step 2: Create deployment package
767
+ echo ""
768
+ echo "Step 2: Creating deployment package..."
769
+
770
+ # Create a temporary deployment directory
771
+ DEPLOY_DIR=$(mktemp -d)
772
+ trap "rm -rf $DEPLOY_DIR" EXIT
773
+
774
+ # Copy the main files
775
+ cp "$SCRIPT_DIR/main.py" "$DEPLOY_DIR/"
776
+ cp "$SCRIPT_DIR/requirements.txt" "$DEPLOY_DIR/"
777
+
778
+ echo "Deployment package contents:"
779
+ ls -la "$DEPLOY_DIR/"
780
+
781
+ # Step 3: Deploy function
782
+ echo ""
783
+ echo "Step 3: Deploying Cloud Function..."
784
+
785
+ # Check if function exists (Gen 2 vs Gen 1)
786
+ EXISTING_GEN2=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --gen2 2>/dev/null && echo "yes" || echo "no")
787
+
788
+ if [ "$EXISTING_GEN2" == "yes" ]; then
789
+ echo "Updating existing Gen 2 function..."
790
+ else
791
+ echo "Creating new Gen 2 function..."
792
+ fi
793
+
794
+ gcloud functions deploy "$FUNCTION_NAME" \\
795
+ --gen2 \\
796
+ --region="$REGION" \\
797
+ --runtime="$RUNTIME" \\
798
+ --source="$DEPLOY_DIR" \\
799
+ --entry-point="$ENTRY_POINT" \\
800
+ --trigger-http \\
801
+ --allow-unauthenticated \\
802
+ --memory="$MEMORY" \\
803
+ --timeout="$TIMEOUT" \\
804
+ --min-instances="$MIN_INSTANCES" \\
805
+ --max-instances="$MAX_INSTANCES" \\
806
+ --quiet
807
+
808
+ # Step 4: Get the endpoint URL
809
+ echo ""
810
+ echo "Step 4: Getting endpoint URL..."
811
+
812
+ ENDPOINT=$(gcloud functions describe "$FUNCTION_NAME" \\
813
+ --region="$REGION" \\
814
+ --gen2 \\
815
+ --format="value(serviceConfig.uri)")
816
+
817
+ echo ""
818
+ echo "=== Deployment Complete ==="
819
+ echo ""
820
+ echo "Endpoint URL: $ENDPOINT"
821
+ echo ""
822
+ echo "Test SWML output:"
823
+ echo " curl $ENDPOINT"
824
+ echo ""
825
+ echo "Test SWAIG function:"
826
+ echo " curl -X POST $ENDPOINT/swaig \\\\"
827
+ echo " -H 'Content-Type: application/json' \\\\"
828
+ echo " -d '{{\\\"function\\\": \\\"get_info\\\", \\\"argument\\\": {{\\\"parsed\\\": [{{\\\"topic\\\": \\\"test\\\"}}]}}}}'"
829
+ echo ""
830
+ echo "Configure SignalWire:"
831
+ echo " Set your phone number's SWML URL to: $ENDPOINT"
832
+ echo ""
833
+ echo "To set environment variables (optional):"
834
+ echo " gcloud functions deploy $FUNCTION_NAME \\\\"
835
+ echo " --region=$REGION \\\\"
836
+ echo " --gen2 \\\\"
837
+ echo " --update-env-vars SWML_BASIC_AUTH_USER=myuser,SWML_BASIC_AUTH_PASSWORD=mypass"
838
+ echo ""
839
+ '''
840
+
841
+ # Azure Function Templates
842
+ AZURE_REQUIREMENTS_TEMPLATE = '''azure-functions>=1.17.0
843
+ signalwire-agents>=1.0.10
844
+ '''
845
+
846
+ AZURE_INIT_TEMPLATE = '''#!/usr/bin/env python3
847
+ """Azure Functions handler for {agent_name} agent.
848
+
849
+ This demonstrates deploying a SignalWire AI Agent to Azure Functions
850
+ with SWAIG functions and SWML output.
851
+
852
+ Environment variables:
853
+ SWML_BASIC_AUTH_USER: Basic auth username (optional)
854
+ SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)
855
+ """
856
+
857
+ import os
858
+ import azure.functions as func
859
+ from signalwire_agents import AgentBase, SwaigFunctionResult
860
+
861
+
862
+ class {agent_class}(AgentBase):
863
+ """{agent_name} agent for Azure Functions deployment."""
864
+
865
+ def __init__(self):
866
+ super().__init__(name="{agent_name_slug}")
867
+
868
+ self._configure_prompts()
869
+ self.add_language("English", "en-US", "rime.spore")
870
+ self._setup_functions()
871
+
872
+ def _configure_prompts(self):
873
+ self.prompt_add_section(
874
+ "Role",
875
+ "You are a helpful AI assistant deployed on Azure Functions."
876
+ )
877
+
878
+ self.prompt_add_section(
879
+ "Guidelines",
880
+ bullets=[
881
+ "Be professional and courteous",
882
+ "Ask clarifying questions when needed",
883
+ "Keep responses concise and helpful"
884
+ ]
885
+ )
886
+
887
+ def _setup_functions(self):
888
+ @self.tool(
889
+ description="Get information about a topic",
890
+ parameters={{
891
+ "type": "object",
892
+ "properties": {{
893
+ "topic": {{
894
+ "type": "string",
895
+ "description": "The topic to get information about"
896
+ }}
897
+ }},
898
+ "required": ["topic"]
899
+ }}
900
+ )
901
+ def get_info(args, raw_data):
902
+ topic = args.get("topic", "")
903
+ return SwaigFunctionResult(
904
+ f"Information about {{topic}}: This is a placeholder response."
905
+ )
906
+
907
+ @self.tool(description="Get Azure Functions deployment information")
908
+ def get_platform_info(args, raw_data):
909
+ function_name = os.getenv("WEBSITE_SITE_NAME", "unknown")
910
+ region = os.getenv("REGION_NAME", "unknown")
911
+ runtime = os.getenv("FUNCTIONS_WORKER_RUNTIME", "unknown")
912
+ version = os.getenv("FUNCTIONS_EXTENSION_VERSION", "unknown")
913
+
914
+ return SwaigFunctionResult(
915
+ f"Running on Azure Functions. "
916
+ f"App: {{function_name}}, Region: {{region}}, "
917
+ f"Runtime: {{runtime}}, Version: {{version}}."
918
+ )
919
+
920
+
921
+ # Create agent instance outside handler for warm starts
922
+ agent = {agent_class}()
923
+
924
+
925
+ def main(req: func.HttpRequest) -> func.HttpResponse:
926
+ """Azure Functions entry point.
927
+
928
+ Args:
929
+ req: Azure Functions HTTP request object
930
+
931
+ Returns:
932
+ Azure Functions HTTP response
933
+ """
934
+ return agent.run(req)
935
+ '''
936
+
937
+ AZURE_FUNCTION_JSON_TEMPLATE = '''{{
938
+ "scriptFile": "__init__.py",
939
+ "bindings": [
940
+ {{
941
+ "authLevel": "anonymous",
942
+ "type": "httpTrigger",
943
+ "direction": "in",
944
+ "name": "req",
945
+ "methods": ["get", "post"],
946
+ "route": "{{*path}}"
947
+ }},
948
+ {{
949
+ "type": "http",
950
+ "direction": "out",
951
+ "name": "$return"
952
+ }}
953
+ ]
954
+ }}
955
+ '''
956
+
957
+ AZURE_HOST_JSON_TEMPLATE = '''{{
958
+ "version": "2.0",
959
+ "logging": {{
960
+ "applicationInsights": {{
961
+ "samplingSettings": {{
962
+ "isEnabled": true,
963
+ "excludedTypes": "Request"
964
+ }}
965
+ }}
966
+ }},
967
+ "extensionBundle": {{
968
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
969
+ "version": "[4.*, 5.0.0)"
970
+ }},
971
+ "extensions": {{
972
+ "http": {{
973
+ "routePrefix": ""
974
+ }}
975
+ }}
976
+ }}
977
+ '''
978
+
979
+ AZURE_LOCAL_SETTINGS_TEMPLATE = '''{{
980
+ "IsEncrypted": false,
981
+ "Values": {{
982
+ "FUNCTIONS_WORKER_RUNTIME": "python",
983
+ "AzureWebJobsStorage": ""
984
+ }}
985
+ }}
986
+ '''
987
+
988
+ AZURE_DEPLOY_TEMPLATE = '''#!/bin/bash
989
+ # Azure Functions deployment script for {agent_name} agent
990
+ #
991
+ # Prerequisites:
992
+ # - Azure CLI installed and authenticated (az login)
993
+ # - Docker installed and running (for building correct architecture)
994
+ #
995
+ # Usage:
996
+ # ./deploy.sh # Deploy with defaults
997
+ # ./deploy.sh my-app # Custom app name
998
+ # ./deploy.sh my-app eastus my-rg # Custom app, region, and resource group
999
+
1000
+ set -e
1001
+
1002
+ # Configuration
1003
+ APP_NAME="${{1:-{function_name}}}"
1004
+ LOCATION="${{2:-{region}}}"
1005
+ RESOURCE_GROUP="${{3:-{resource_group}}}"
1006
+ STORAGE_ACCOUNT="${{APP_NAME//-/}}storage" # Remove hyphens for storage account
1007
+ RUNTIME="python"
1008
+ RUNTIME_VERSION="3.11"
1009
+ FUNCTIONS_VERSION="4"
1010
+
1011
+ # Truncate storage account name to 24 chars (Azure limit)
1012
+ STORAGE_ACCOUNT="${{STORAGE_ACCOUNT:0:24}}"
1013
+
1014
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
1015
+
1016
+ echo "=== {agent_name} - Azure Functions Deployment ==="
1017
+ echo "App Name: $APP_NAME"
1018
+ echo "Location: $LOCATION"
1019
+ echo "Resource Group: $RESOURCE_GROUP"
1020
+ echo "Storage Account: $STORAGE_ACCOUNT"
1021
+ echo ""
1022
+
1023
+ # Check for Docker
1024
+ if ! command -v docker &> /dev/null; then
1025
+ echo "ERROR: Docker is required but not installed."
1026
+ echo "Please install Docker: https://docs.docker.com/get-docker/"
1027
+ exit 1
1028
+ fi
1029
+
1030
+ if ! docker info &> /dev/null; then
1031
+ echo "ERROR: Docker is not running. Please start Docker."
1032
+ exit 1
1033
+ fi
1034
+
1035
+ # Step 1: Login check
1036
+ echo "Step 1: Checking Azure login..."
1037
+ if ! az account show &>/dev/null; then
1038
+ echo "Not logged in. Running: az login"
1039
+ az login
1040
+ fi
1041
+
1042
+ SUBSCRIPTION=$(az account show --query name -o tsv)
1043
+ echo "Subscription: $SUBSCRIPTION"
1044
+ echo ""
1045
+
1046
+ # Step 2: Create resource group
1047
+ echo "Step 2: Creating resource group..."
1048
+ if ! az group show --name "$RESOURCE_GROUP" &>/dev/null; then
1049
+ az group create \\
1050
+ --name "$RESOURCE_GROUP" \\
1051
+ --location "$LOCATION" \\
1052
+ --output none
1053
+ echo "Created resource group: $RESOURCE_GROUP"
1054
+ else
1055
+ echo "Resource group exists: $RESOURCE_GROUP"
1056
+ fi
1057
+
1058
+ # Step 3: Create storage account
1059
+ echo ""
1060
+ echo "Step 3: Creating storage account..."
1061
+ if ! az storage account show --name "$STORAGE_ACCOUNT" --resource-group "$RESOURCE_GROUP" &>/dev/null; then
1062
+ az storage account create \\
1063
+ --name "$STORAGE_ACCOUNT" \\
1064
+ --resource-group "$RESOURCE_GROUP" \\
1065
+ --location "$LOCATION" \\
1066
+ --sku Standard_LRS \\
1067
+ --output none
1068
+ echo "Created storage account: $STORAGE_ACCOUNT"
1069
+ echo "Waiting for storage account to propagate..."
1070
+ sleep 10
1071
+ else
1072
+ echo "Storage account exists: $STORAGE_ACCOUNT"
1073
+ fi
1074
+
1075
+ # Step 4: Create Function App
1076
+ echo ""
1077
+ echo "Step 4: Creating Function App..."
1078
+ if ! az functionapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then
1079
+ az functionapp create \\
1080
+ --name "$APP_NAME" \\
1081
+ --resource-group "$RESOURCE_GROUP" \\
1082
+ --storage-account "$STORAGE_ACCOUNT" \\
1083
+ --consumption-plan-location "$LOCATION" \\
1084
+ --runtime "$RUNTIME" \\
1085
+ --runtime-version "$RUNTIME_VERSION" \\
1086
+ --functions-version "$FUNCTIONS_VERSION" \\
1087
+ --os-type Linux \\
1088
+ --output none
1089
+ echo "Created Function App: $APP_NAME"
1090
+
1091
+ # Wait for app to be ready
1092
+ echo "Waiting for Function App to be ready..."
1093
+ sleep 30
1094
+ else
1095
+ echo "Function App exists: $APP_NAME"
1096
+ fi
1097
+
1098
+ # Step 5: Build and deploy the function using Docker
1099
+ echo ""
1100
+ echo "Step 5: Building function with Docker (linux/amd64)..."
1101
+
1102
+ DEPLOY_DIR=$(mktemp -d)
1103
+
1104
+ # Copy function files
1105
+ cp -r "$SCRIPT_DIR/function_app" "$DEPLOY_DIR/"
1106
+ cp "$SCRIPT_DIR/host.json" "$DEPLOY_DIR/"
1107
+ cp "$SCRIPT_DIR/requirements.txt" "$DEPLOY_DIR/"
1108
+ cp "$SCRIPT_DIR/local.settings.json" "$DEPLOY_DIR/" 2>/dev/null || true
1109
+
1110
+ # Build dependencies using Docker for correct architecture
1111
+ echo "Installing dependencies via Docker..."
1112
+ docker run --rm \\
1113
+ --platform linux/amd64 \\
1114
+ --entrypoint "" \\
1115
+ -v "$DEPLOY_DIR:/var/task" \\
1116
+ -w /var/task \\
1117
+ mcr.microsoft.com/azure-functions/python:4-python3.11 \\
1118
+ bash -c "pip install -r requirements.txt -t .python_packages/lib/site-packages --quiet"
1119
+
1120
+ # Create zip for deployment
1121
+ echo "Creating deployment package..."
1122
+ ZIP_FILE="$DEPLOY_DIR/deploy.zip"
1123
+ cd "$DEPLOY_DIR"
1124
+ zip -r "$ZIP_FILE" . -x "*.pyc" -q
1125
+ cd - > /dev/null
1126
+
1127
+ PACKAGE_SIZE=$(du -h "$ZIP_FILE" | cut -f1)
1128
+ echo "Package size: $PACKAGE_SIZE"
1129
+
1130
+ # Deploy using zip deployment
1131
+ echo ""
1132
+ echo "Step 6: Deploying to Azure..."
1133
+ az functionapp deployment source config-zip \\
1134
+ --name "$APP_NAME" \\
1135
+ --resource-group "$RESOURCE_GROUP" \\
1136
+ --src "$ZIP_FILE" \\
1137
+ --output none
1138
+
1139
+ echo "Deployment complete"
1140
+
1141
+ # Cleanup
1142
+ rm -rf "$DEPLOY_DIR"
1143
+
1144
+ # Step 7: Get the endpoint URL
1145
+ echo ""
1146
+ echo "Step 7: Getting endpoint URL..."
1147
+
1148
+ ENDPOINT="https://${{APP_NAME}}.azurewebsites.net"
1149
+
1150
+ # Verify deployment
1151
+ echo "Waiting for deployment to propagate..."
1152
+ sleep 10
1153
+
1154
+ echo ""
1155
+ echo "=== Deployment Complete ==="
1156
+ echo ""
1157
+ echo "Endpoint URL: $ENDPOINT/api/function_app"
1158
+ echo ""
1159
+ echo "Test SWML output:"
1160
+ echo " curl $ENDPOINT/api/function_app"
1161
+ echo ""
1162
+ echo "Test SWAIG function:"
1163
+ echo " curl -X POST $ENDPOINT/api/function_app/swaig \\\\"
1164
+ echo " -H 'Content-Type: application/json' \\\\"
1165
+ echo " -d '{{\\\"function\\\": \\\"get_info\\\", \\\"argument\\\": {{\\\"parsed\\\": [{{\\\"topic\\\": \\\"test\\\"}}]}}}}'"
1166
+ echo ""
1167
+ echo "Configure SignalWire:"
1168
+ echo " Set your phone number's SWML URL to: $ENDPOINT/api/function_app"
1169
+ echo ""
1170
+ echo "To set environment variables (optional):"
1171
+ echo " az functionapp config appsettings set --name $APP_NAME --resource-group $RESOURCE_GROUP \\\\"
1172
+ echo " --settings SWML_BASIC_AUTH_USER=myuser SWML_BASIC_AUTH_PASSWORD=mypass"
1173
+ echo ""
1174
+ '''
1175
+
204
1176
 
205
1177
  def get_agent_template(agent_type: str, features: Dict[str, bool]) -> str:
206
1178
  """Generate the main agent template based on type and features."""
@@ -381,6 +1353,13 @@ def get_app_template(features: Dict[str, bool]) -> str:
381
1353
 
382
1354
  imports_str = '\n'.join(imports)
383
1355
 
1356
+ # Create agent at module level for swaig-test compatibility
1357
+ agent_instance = '''
1358
+
1359
+ # Create agent instance at module level for swaig-test compatibility
1360
+ agent = MainAgent()
1361
+ '''
1362
+
384
1363
  # Debug webhook code
385
1364
  debug_code = ''
386
1365
  if has_debug:
@@ -480,7 +1459,7 @@ def main():
480
1459
 
481
1460
  # Create server and register agent
482
1461
  server = AgentServer(host=host, port=port)
483
- server.register(MainAgent())
1462
+ server.register(agent)
484
1463
  ''']
485
1464
 
486
1465
  if has_web_ui:
@@ -538,6 +1517,7 @@ if __name__ == "__main__":
538
1517
  """Main entry point for the agent server."""
539
1518
 
540
1519
  {imports_str}
1520
+ {agent_instance}
541
1521
  {debug_code}
542
1522
  {main_body}'''
543
1523
 
@@ -872,31 +1852,397 @@ class ProjectGenerator:
872
1852
  self.project_dir = Path(config['project_dir'])
873
1853
  self.project_name = config['project_name']
874
1854
  self.features = config['features']
875
- self.credentials = config['credentials']
1855
+ self.credentials = config.get('credentials', {})
1856
+ self.platform = config.get('platform', 'local')
1857
+ self.cloud_config = config.get('cloud_config', {})
876
1858
 
877
1859
  def generate(self) -> bool:
878
1860
  """Generate the project. Returns True on success."""
879
1861
  try:
880
- self._create_directories()
881
- self._create_agent_files()
882
- self._create_app_file()
883
- self._create_config_files()
1862
+ if self.platform == 'local':
1863
+ return self._generate_local()
1864
+ elif self.platform == 'aws':
1865
+ return self._generate_aws()
1866
+ elif self.platform == 'gcp':
1867
+ return self._generate_gcp()
1868
+ elif self.platform == 'azure':
1869
+ return self._generate_azure()
1870
+ else:
1871
+ print_error(f"Unknown platform: {self.platform}")
1872
+ return False
1873
+ except Exception as e:
1874
+ print_error(f"Failed to generate project: {e}")
1875
+ return False
884
1876
 
885
- if self.features.get('tests'):
886
- self._create_test_files()
1877
+ def _generate_local(self) -> bool:
1878
+ """Generate a local agent project."""
1879
+ self._create_directories()
1880
+ self._create_agent_files()
1881
+ self._create_app_file()
1882
+ self._create_config_files()
887
1883
 
888
- if self.features.get('web_ui'):
889
- self._create_web_files()
1884
+ if self.features.get('tests'):
1885
+ self._create_test_files()
890
1886
 
891
- self._create_readme()
1887
+ if self.features.get('web_ui'):
1888
+ self._create_web_files()
892
1889
 
893
- if self.config.get('create_venv'):
894
- self._create_virtualenv()
1890
+ self._create_readme()
895
1891
 
896
- return True
897
- except Exception as e:
898
- print_error(f"Failed to generate project: {e}")
899
- return False
1892
+ if self.config.get('create_venv'):
1893
+ self._create_virtualenv()
1894
+
1895
+ return True
1896
+
1897
+ def _generate_aws(self) -> bool:
1898
+ """Generate an AWS Lambda project."""
1899
+ self.project_dir.mkdir(parents=True, exist_ok=True)
1900
+ print_success(f"Created {self.project_dir}/")
1901
+
1902
+ # Get template variables
1903
+ template_vars = self._get_template_vars()
1904
+
1905
+ # handler.py
1906
+ handler_code = AWS_HANDLER_TEMPLATE.format(**template_vars)
1907
+ (self.project_dir / 'handler.py').write_text(handler_code)
1908
+ print_success("Created handler.py")
1909
+
1910
+ # requirements.txt
1911
+ (self.project_dir / 'requirements.txt').write_text(AWS_REQUIREMENTS_TEMPLATE)
1912
+ print_success("Created requirements.txt")
1913
+
1914
+ # deploy.sh
1915
+ deploy_code = AWS_DEPLOY_TEMPLATE.format(**template_vars)
1916
+ deploy_path = self.project_dir / 'deploy.sh'
1917
+ deploy_path.write_text(deploy_code)
1918
+ deploy_path.chmod(0o755)
1919
+ print_success("Created deploy.sh")
1920
+
1921
+ # .env.example
1922
+ self._create_cloud_env_example('aws')
1923
+
1924
+ # .gitignore
1925
+ (self.project_dir / '.gitignore').write_text(TEMPLATE_GITIGNORE)
1926
+ print_success("Created .gitignore")
1927
+
1928
+ # README.md
1929
+ self._create_cloud_readme('aws')
1930
+
1931
+ return True
1932
+
1933
+ def _generate_gcp(self) -> bool:
1934
+ """Generate a Google Cloud Function project."""
1935
+ self.project_dir.mkdir(parents=True, exist_ok=True)
1936
+ print_success(f"Created {self.project_dir}/")
1937
+
1938
+ # Get template variables
1939
+ template_vars = self._get_template_vars()
1940
+
1941
+ # main.py
1942
+ main_code = GCP_MAIN_TEMPLATE.format(**template_vars)
1943
+ (self.project_dir / 'main.py').write_text(main_code)
1944
+ print_success("Created main.py")
1945
+
1946
+ # requirements.txt
1947
+ (self.project_dir / 'requirements.txt').write_text(GCP_REQUIREMENTS_TEMPLATE)
1948
+ print_success("Created requirements.txt")
1949
+
1950
+ # deploy.sh
1951
+ deploy_code = GCP_DEPLOY_TEMPLATE.format(**template_vars)
1952
+ deploy_path = self.project_dir / 'deploy.sh'
1953
+ deploy_path.write_text(deploy_code)
1954
+ deploy_path.chmod(0o755)
1955
+ print_success("Created deploy.sh")
1956
+
1957
+ # .env.example
1958
+ self._create_cloud_env_example('gcp')
1959
+
1960
+ # .gitignore
1961
+ (self.project_dir / '.gitignore').write_text(TEMPLATE_GITIGNORE)
1962
+ print_success("Created .gitignore")
1963
+
1964
+ # README.md
1965
+ self._create_cloud_readme('gcp')
1966
+
1967
+ return True
1968
+
1969
+ def _generate_azure(self) -> bool:
1970
+ """Generate an Azure Functions project."""
1971
+ self.project_dir.mkdir(parents=True, exist_ok=True)
1972
+ print_success(f"Created {self.project_dir}/")
1973
+
1974
+ # Create function_app directory
1975
+ function_dir = self.project_dir / 'function_app'
1976
+ function_dir.mkdir(exist_ok=True)
1977
+
1978
+ # Get template variables
1979
+ template_vars = self._get_template_vars()
1980
+
1981
+ # function_app/__init__.py
1982
+ init_code = AZURE_INIT_TEMPLATE.format(**template_vars)
1983
+ (function_dir / '__init__.py').write_text(init_code)
1984
+ print_success("Created function_app/__init__.py")
1985
+
1986
+ # function_app/function.json
1987
+ (function_dir / 'function.json').write_text(AZURE_FUNCTION_JSON_TEMPLATE)
1988
+ print_success("Created function_app/function.json")
1989
+
1990
+ # host.json
1991
+ (self.project_dir / 'host.json').write_text(AZURE_HOST_JSON_TEMPLATE)
1992
+ print_success("Created host.json")
1993
+
1994
+ # local.settings.json
1995
+ (self.project_dir / 'local.settings.json').write_text(AZURE_LOCAL_SETTINGS_TEMPLATE)
1996
+ print_success("Created local.settings.json")
1997
+
1998
+ # requirements.txt
1999
+ (self.project_dir / 'requirements.txt').write_text(AZURE_REQUIREMENTS_TEMPLATE)
2000
+ print_success("Created requirements.txt")
2001
+
2002
+ # deploy.sh
2003
+ deploy_code = AZURE_DEPLOY_TEMPLATE.format(**template_vars)
2004
+ deploy_path = self.project_dir / 'deploy.sh'
2005
+ deploy_path.write_text(deploy_code)
2006
+ deploy_path.chmod(0o755)
2007
+ print_success("Created deploy.sh")
2008
+
2009
+ # .env.example
2010
+ self._create_cloud_env_example('azure')
2011
+
2012
+ # .gitignore
2013
+ (self.project_dir / '.gitignore').write_text(TEMPLATE_GITIGNORE)
2014
+ print_success("Created .gitignore")
2015
+
2016
+ # README.md
2017
+ self._create_cloud_readme('azure')
2018
+
2019
+ return True
2020
+
2021
+ def _get_template_vars(self) -> Dict[str, str]:
2022
+ """Get template variables for cloud function templates."""
2023
+ # Convert project name to various formats
2024
+ agent_name = self.project_name
2025
+ agent_name_slug = self.project_name.lower().replace(' ', '-').replace('_', '-')
2026
+ agent_class = ''.join(word.capitalize() for word in self.project_name.replace('-', ' ').replace('_', ' ').split()) + 'Agent'
2027
+ function_name = agent_name_slug
2028
+
2029
+ # Auth credentials
2030
+ auth_user = 'admin'
2031
+ auth_password = generate_password(16)
2032
+
2033
+ return {
2034
+ 'agent_name': agent_name,
2035
+ 'agent_name_slug': agent_name_slug,
2036
+ 'agent_class': agent_class,
2037
+ 'function_name': function_name,
2038
+ 'region': self.cloud_config.get('region', DEFAULT_REGIONS.get(self.platform, '')),
2039
+ 'resource_group': self.cloud_config.get('resource_group', f'{function_name}-rg'),
2040
+ 'auth_user': auth_user,
2041
+ 'auth_password': auth_password,
2042
+ }
2043
+
2044
+ def _create_cloud_env_example(self, platform: str):
2045
+ """Create .env.example for cloud platforms."""
2046
+ env_content = '''# Optional: Basic authentication credentials
2047
+ # If not set, the SDK will auto-generate secure credentials
2048
+ SWML_BASIC_AUTH_USER=admin
2049
+ SWML_BASIC_AUTH_PASSWORD=your-secure-password
2050
+ '''
2051
+ (self.project_dir / '.env.example').write_text(env_content)
2052
+ print_success("Created .env.example")
2053
+
2054
+ def _create_cloud_readme(self, platform: str):
2055
+ """Create README.md for cloud platforms."""
2056
+ template_vars = self._get_template_vars()
2057
+ platform_names = {'aws': 'AWS Lambda', 'gcp': 'Google Cloud Functions', 'azure': 'Azure Functions'}
2058
+ platform_name = platform_names.get(platform, platform)
2059
+
2060
+ if platform == 'aws':
2061
+ readme = f'''# {self.project_name}
2062
+
2063
+ A SignalWire AI Agent deployed on {platform_name}.
2064
+
2065
+ ## Prerequisites
2066
+
2067
+ - AWS CLI configured with appropriate credentials
2068
+ - Docker installed and running
2069
+
2070
+ ## Quick Start
2071
+
2072
+ ```bash
2073
+ cd {self.project_name}
2074
+ ./deploy.sh
2075
+ ```
2076
+
2077
+ ## Deployment Options
2078
+
2079
+ ```bash
2080
+ # Deploy with defaults
2081
+ ./deploy.sh
2082
+
2083
+ # Custom function name
2084
+ ./deploy.sh my-function
2085
+
2086
+ # Custom function and region
2087
+ ./deploy.sh my-function us-west-2
2088
+ ```
2089
+
2090
+ ## Project Structure
2091
+
2092
+ ```
2093
+ {self.project_name}/
2094
+ ├── handler.py # Lambda handler with agent
2095
+ ├── requirements.txt # Python dependencies
2096
+ ├── deploy.sh # Deployment script
2097
+ ├── .env.example # Environment template
2098
+ └── README.md
2099
+ ```
2100
+
2101
+ ## Testing
2102
+
2103
+ After deployment, test your function:
2104
+
2105
+ ```bash
2106
+ # Test SWML output
2107
+ curl -u admin:password https://YOUR-API-ID.execute-api.REGION.amazonaws.com/
2108
+
2109
+ # Test SWAIG function
2110
+ curl -u admin:password -X POST https://YOUR-API-ID.execute-api.REGION.amazonaws.com/swaig \\
2111
+ -H 'Content-Type: application/json' \\
2112
+ -d '{{"function": "get_info", "argument": {{"parsed": [{{"topic": "test"}}]}}}}'
2113
+ ```
2114
+
2115
+ ## Configure SignalWire
2116
+
2117
+ Set your phone number's SWML URL to the endpoint URL shown after deployment.
2118
+ '''
2119
+
2120
+ elif platform == 'gcp':
2121
+ readme = f'''# {self.project_name}
2122
+
2123
+ A SignalWire AI Agent deployed on {platform_name}.
2124
+
2125
+ ## Prerequisites
2126
+
2127
+ - gcloud CLI installed and authenticated
2128
+ - A Google Cloud project with Cloud Functions API enabled
2129
+
2130
+ ## Quick Start
2131
+
2132
+ ```bash
2133
+ cd {self.project_name}
2134
+ ./deploy.sh
2135
+ ```
2136
+
2137
+ ## Deployment Options
2138
+
2139
+ ```bash
2140
+ # Deploy with defaults
2141
+ ./deploy.sh
2142
+
2143
+ # Custom function name
2144
+ ./deploy.sh my-function
2145
+
2146
+ # Custom function and region
2147
+ ./deploy.sh my-function us-central1
2148
+ ```
2149
+
2150
+ ## Project Structure
2151
+
2152
+ ```
2153
+ {self.project_name}/
2154
+ ├── main.py # Cloud Function entry point
2155
+ ├── requirements.txt # Python dependencies
2156
+ ├── deploy.sh # Deployment script
2157
+ ├── .env.example # Environment template
2158
+ └── README.md
2159
+ ```
2160
+
2161
+ ## Testing
2162
+
2163
+ After deployment, test your function:
2164
+
2165
+ ```bash
2166
+ # Test SWML output
2167
+ curl https://YOUR-FUNCTION-URL
2168
+
2169
+ # Test SWAIG function
2170
+ curl -X POST https://YOUR-FUNCTION-URL/swaig \\
2171
+ -H 'Content-Type: application/json' \\
2172
+ -d '{{"function": "get_info", "argument": {{"parsed": [{{"topic": "test"}}]}}}}'
2173
+ ```
2174
+
2175
+ ## Configure SignalWire
2176
+
2177
+ Set your phone number's SWML URL to the endpoint URL shown after deployment.
2178
+ '''
2179
+
2180
+ else: # azure
2181
+ readme = f'''# {self.project_name}
2182
+
2183
+ A SignalWire AI Agent deployed on {platform_name}.
2184
+
2185
+ ## Prerequisites
2186
+
2187
+ - Azure CLI installed and authenticated (`az login`)
2188
+ - Docker installed and running
2189
+
2190
+ ## Quick Start
2191
+
2192
+ ```bash
2193
+ cd {self.project_name}
2194
+ ./deploy.sh
2195
+ ```
2196
+
2197
+ ## Deployment Options
2198
+
2199
+ ```bash
2200
+ # Deploy with defaults
2201
+ ./deploy.sh
2202
+
2203
+ # Custom app name
2204
+ ./deploy.sh my-app
2205
+
2206
+ # Custom app, region, and resource group
2207
+ ./deploy.sh my-app eastus my-resource-group
2208
+ ```
2209
+
2210
+ ## Project Structure
2211
+
2212
+ ```
2213
+ {self.project_name}/
2214
+ ├── function_app/
2215
+ │ ├── __init__.py # Azure Function handler
2216
+ │ └── function.json # HTTP trigger config
2217
+ ├── host.json # Host configuration
2218
+ ├── local.settings.json # Local dev settings
2219
+ ├── requirements.txt # Python dependencies
2220
+ ├── deploy.sh # Deployment script
2221
+ ├── .env.example # Environment template
2222
+ └── README.md
2223
+ ```
2224
+
2225
+ ## Testing
2226
+
2227
+ After deployment, test your function:
2228
+
2229
+ ```bash
2230
+ # Test SWML output
2231
+ curl https://YOUR-APP.azurewebsites.net/api/function_app
2232
+
2233
+ # Test SWAIG function
2234
+ curl -X POST https://YOUR-APP.azurewebsites.net/api/function_app/swaig \\
2235
+ -H 'Content-Type: application/json' \\
2236
+ -d '{{"function": "get_info", "argument": {{"parsed": [{{"topic": "test"}}]}}}}'
2237
+ ```
2238
+
2239
+ ## Configure SignalWire
2240
+
2241
+ Set your phone number's SWML URL to the endpoint URL shown after deployment.
2242
+ '''
2243
+
2244
+ (self.project_dir / 'README.md').write_text(readme)
2245
+ print_success("Created README.md")
900
2246
 
901
2247
  def _create_directories(self):
902
2248
  """Create project directory structure."""
@@ -1043,6 +2389,11 @@ def run_interactive() -> Dict[str, Any]:
1043
2389
  """Run interactive prompts and return configuration."""
1044
2390
  print(f"\n{Colors.BOLD}{Colors.CYAN}Welcome to SignalWire Agent Init!{Colors.NC}\n")
1045
2391
 
2392
+ # Platform selection
2393
+ platform_options = list(CLOUD_PLATFORMS.values())
2394
+ platform_idx = prompt_select("Target platform:", platform_options, default=1)
2395
+ platform = list(CLOUD_PLATFORMS.keys())[platform_idx - 1]
2396
+
1046
2397
  # Project name
1047
2398
  default_name = "my-agent"
1048
2399
  project_name = prompt("Project name", default_name)
@@ -1058,76 +2409,102 @@ def run_interactive() -> Dict[str, Any]:
1058
2409
  print("Aborted.")
1059
2410
  sys.exit(0)
1060
2411
 
1061
- # Agent type
1062
- agent_types = [
1063
- "Basic - Minimal agent with example tool",
1064
- "Full - Debug webhooks, web UI, all features",
1065
- ]
1066
- agent_type_idx = prompt_select("Agent type:", agent_types, default=1)
1067
- agent_type = 'basic' if agent_type_idx == 1 else 'full'
2412
+ # Cloud-specific configuration
2413
+ cloud_config = {}
2414
+ if platform != 'local':
2415
+ default_region = DEFAULT_REGIONS.get(platform, '')
2416
+ cloud_config['region'] = prompt("Region", default_region)
2417
+
2418
+ if platform == 'azure':
2419
+ cloud_config['resource_group'] = prompt("Resource group", f"{project_name}-rg")
2420
+
2421
+ # Agent type (for local) or simplified for cloud
2422
+ if platform == 'local':
2423
+ agent_types = [
2424
+ "Basic - Minimal agent with example tool",
2425
+ "Full - Debug webhooks, web UI, all features",
2426
+ ]
2427
+ agent_type_idx = prompt_select("Agent type:", agent_types, default=1)
2428
+ agent_type = 'basic' if agent_type_idx == 1 else 'full'
2429
+
2430
+ # Set default features based on type
2431
+ if agent_type == 'full':
2432
+ default_features = [True, True, True, True, True, True]
2433
+ else:
2434
+ default_features = [False, False, False, True, True, False]
2435
+
2436
+ # Feature selection
2437
+ feature_names = [
2438
+ "Debug webhooks (console output)",
2439
+ "Post-prompt summary",
2440
+ "Web UI",
2441
+ "Example SWAIG tool",
2442
+ "Test scaffolding (pytest)",
2443
+ "Basic authentication",
2444
+ ]
2445
+ selected = prompt_multiselect("Include features:", feature_names, default_features)
1068
2446
 
1069
- # Set default features based on type
1070
- if agent_type == 'full':
1071
- default_features = [True, True, True, True, True, True]
2447
+ features = {
2448
+ 'debug_webhooks': selected[0],
2449
+ 'post_prompt': selected[1],
2450
+ 'web_ui': selected[2],
2451
+ 'example_tool': selected[3],
2452
+ 'tests': selected[4],
2453
+ 'basic_auth': selected[5],
2454
+ }
1072
2455
  else:
1073
- default_features = [False, False, False, True, True, False]
1074
-
1075
- # Feature selection
1076
- feature_names = [
1077
- "Debug webhooks (console output)",
1078
- "Post-prompt summary",
1079
- "Web UI",
1080
- "Example SWAIG tool",
1081
- "Test scaffolding (pytest)",
1082
- "Basic authentication",
1083
- ]
1084
- selected = prompt_multiselect("Include features:", feature_names, default_features)
1085
-
1086
- features = {
1087
- 'debug_webhooks': selected[0],
1088
- 'post_prompt': selected[1],
1089
- 'web_ui': selected[2],
1090
- 'example_tool': selected[3],
1091
- 'tests': selected[4],
1092
- 'basic_auth': selected[5],
1093
- }
2456
+ # Cloud platforms have simplified features
2457
+ agent_type = 'basic'
2458
+ features = {
2459
+ 'debug_webhooks': False,
2460
+ 'post_prompt': False,
2461
+ 'web_ui': False,
2462
+ 'example_tool': True,
2463
+ 'tests': False,
2464
+ 'basic_auth': prompt_yes_no("Enable basic authentication?", default=True),
2465
+ }
1094
2466
 
1095
- # Credentials
1096
- env_creds = get_env_credentials()
2467
+ # Credentials (only for local)
1097
2468
  credentials = {'space': '', 'project': '', 'token': ''}
1098
-
1099
- if env_creds['space'] or env_creds['project'] or env_creds['token']:
1100
- print(f"\n{Colors.GREEN}SignalWire credentials found in environment:{Colors.NC}")
1101
- if env_creds['space']:
1102
- print(f" Space: {env_creds['space']}")
1103
- if env_creds['project']:
1104
- print(f" Project: {env_creds['project'][:12]}...{env_creds['project'][-4:]}")
1105
- if env_creds['token']:
1106
- print(f" Token: {mask_token(env_creds['token'])}")
1107
-
1108
- if prompt_yes_no("Use these credentials?", default=True):
1109
- credentials = env_creds
2469
+ if platform == 'local':
2470
+ env_creds = get_env_credentials()
2471
+
2472
+ if env_creds['space'] or env_creds['project'] or env_creds['token']:
2473
+ print(f"\n{Colors.GREEN}SignalWire credentials found in environment:{Colors.NC}")
2474
+ if env_creds['space']:
2475
+ print(f" Space: {env_creds['space']}")
2476
+ if env_creds['project']:
2477
+ print(f" Project: {env_creds['project'][:12]}...{env_creds['project'][-4:]}")
2478
+ if env_creds['token']:
2479
+ print(f" Token: {mask_token(env_creds['token'])}")
2480
+
2481
+ if prompt_yes_no("Use these credentials?", default=True):
2482
+ credentials = env_creds
2483
+ else:
2484
+ credentials['space'] = prompt("Space name", env_creds['space'])
2485
+ credentials['project'] = prompt("Project ID", env_creds['project'])
2486
+ credentials['token'] = prompt("Token", env_creds['token'])
1110
2487
  else:
1111
- credentials['space'] = prompt("Space name", env_creds['space'])
1112
- credentials['project'] = prompt("Project ID", env_creds['project'])
1113
- credentials['token'] = prompt("Token", env_creds['token'])
1114
- else:
1115
- print(f"\n{Colors.YELLOW}No SignalWire credentials found in environment.{Colors.NC}")
1116
- if prompt_yes_no("Enter credentials now?", default=False):
1117
- credentials['space'] = prompt("Space name")
1118
- credentials['project'] = prompt("Project ID")
1119
- credentials['token'] = prompt("Token")
2488
+ print(f"\n{Colors.YELLOW}No SignalWire credentials found in environment.{Colors.NC}")
2489
+ if prompt_yes_no("Enter credentials now?", default=False):
2490
+ credentials['space'] = prompt("Space name")
2491
+ credentials['project'] = prompt("Project ID")
2492
+ credentials['token'] = prompt("Token")
1120
2493
 
1121
- # Virtual environment
1122
- create_venv = prompt_yes_no("\nCreate virtual environment?", default=True)
2494
+ # Virtual environment (only for local)
2495
+ create_venv = False
2496
+ if platform == 'local':
2497
+ create_venv = prompt_yes_no("\nCreate virtual environment?", default=True)
1123
2498
 
1124
2499
  return {
1125
2500
  'project_name': project_name,
1126
2501
  'project_dir': project_dir,
2502
+ 'platform': platform,
1127
2503
  'agent_type': agent_type,
1128
2504
  'features': features,
1129
2505
  'credentials': credentials,
1130
2506
  'create_venv': create_venv,
2507
+ 'cloud_config': cloud_config,
1131
2508
  }
1132
2509
 
1133
2510
 
@@ -1135,38 +2512,66 @@ def run_quick(project_name: str, args: Any) -> Dict[str, Any]:
1135
2512
  """Run in quick mode with minimal prompts."""
1136
2513
  project_dir = os.path.abspath(os.path.join('.', project_name))
1137
2514
 
2515
+ # Get platform from args
2516
+ platform = getattr(args, 'platform', 'local') or 'local'
2517
+
2518
+ # Get region from args or use default
2519
+ region = getattr(args, 'region', None)
2520
+ if not region and platform != 'local':
2521
+ region = DEFAULT_REGIONS.get(platform, '')
2522
+
2523
+ # Build cloud config
2524
+ cloud_config = {}
2525
+ if platform != 'local':
2526
+ cloud_config['region'] = region
2527
+ if platform == 'azure':
2528
+ cloud_config['resource_group'] = f"{project_name}-rg"
2529
+
1138
2530
  # Determine features from args
1139
2531
  agent_type = getattr(args, 'type', 'basic') or 'basic'
1140
2532
 
1141
- if agent_type == 'full':
1142
- features = {
1143
- 'debug_webhooks': True,
1144
- 'post_prompt': True,
1145
- 'web_ui': True,
1146
- 'example_tool': True,
1147
- 'tests': True,
1148
- 'basic_auth': True,
1149
- }
2533
+ if platform == 'local':
2534
+ if agent_type == 'full':
2535
+ features = {
2536
+ 'debug_webhooks': True,
2537
+ 'post_prompt': True,
2538
+ 'web_ui': True,
2539
+ 'example_tool': True,
2540
+ 'tests': True,
2541
+ 'basic_auth': True,
2542
+ }
2543
+ else:
2544
+ features = {
2545
+ 'debug_webhooks': False,
2546
+ 'post_prompt': False,
2547
+ 'web_ui': False,
2548
+ 'example_tool': True,
2549
+ 'tests': True,
2550
+ 'basic_auth': False,
2551
+ }
1150
2552
  else:
2553
+ # Cloud platforms have simplified features
1151
2554
  features = {
1152
2555
  'debug_webhooks': False,
1153
2556
  'post_prompt': False,
1154
2557
  'web_ui': False,
1155
2558
  'example_tool': True,
1156
- 'tests': True,
1157
- 'basic_auth': False,
2559
+ 'tests': False,
2560
+ 'basic_auth': True,
1158
2561
  }
1159
2562
 
1160
- # Get credentials from environment
1161
- credentials = get_env_credentials()
2563
+ # Get credentials from environment (only for local)
2564
+ credentials = get_env_credentials() if platform == 'local' else {'space': '', 'project': '', 'token': ''}
1162
2565
 
1163
2566
  return {
1164
2567
  'project_name': project_name,
1165
2568
  'project_dir': project_dir,
2569
+ 'platform': platform,
1166
2570
  'agent_type': agent_type,
1167
2571
  'features': features,
1168
2572
  'credentials': credentials,
1169
- 'create_venv': not getattr(args, 'no_venv', False),
2573
+ 'create_venv': not getattr(args, 'no_venv', False) and platform == 'local',
2574
+ 'cloud_config': cloud_config,
1170
2575
  }
1171
2576
 
1172
2577
 
@@ -1179,15 +2584,21 @@ def main():
1179
2584
  formatter_class=argparse.RawDescriptionHelpFormatter,
1180
2585
  epilog='''
1181
2586
  Examples:
1182
- sw-agent-init Interactive mode
1183
- sw-agent-init myagent Quick mode with defaults
1184
- sw-agent-init myagent --type full
1185
- sw-agent-init myagent --no-venv
2587
+ sw-agent-init Interactive mode
2588
+ sw-agent-init myagent Quick mode with defaults
2589
+ sw-agent-init myagent --type full Full-featured local agent
2590
+ sw-agent-init myagent -p aws AWS Lambda function
2591
+ sw-agent-init myagent -p gcp Google Cloud Function
2592
+ sw-agent-init myagent -p azure Azure Function
2593
+ sw-agent-init myagent -p aws -r us-west-2 Custom region
1186
2594
  '''
1187
2595
  )
1188
2596
  parser.add_argument('name', nargs='?', help='Project name')
1189
2597
  parser.add_argument('--type', choices=['basic', 'full'], default='basic',
1190
2598
  help='Agent type (default: basic)')
2599
+ parser.add_argument('--platform', '-p', choices=['local', 'aws', 'gcp', 'azure'],
2600
+ default='local', help='Target platform (default: local)')
2601
+ parser.add_argument('--region', '-r', help='Cloud region (e.g., us-east-1, us-central1, eastus)')
1191
2602
  parser.add_argument('--no-venv', action='store_true',
1192
2603
  help='Skip virtual environment creation')
1193
2604
  parser.add_argument('--dir', help='Parent directory for project')