Skip to content

signalwire.cli

signalwire.cli

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

SignalWire Agents CLI Tools

This package contains command-line tools for working with SignalWire AI Agents.

__all__ = ['test_swaig_main'] module-attribute

test_swaig_main()

Main entry point for the CLI tool

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

main()

Main entry point for the build-search command

validate_command()

Validate an existing search index

search_command()

Search within an existing search index

migrate_command()

Migrate search indexes between backends

remote_command()

Search via remote API endpoint

console_entry_point()

Console script entry point for pip installation

config

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

DEFAULT_CALL_TYPE = 'webrtc' module-attribute

DEFAULT_CALL_DIRECTION = 'inbound' module-attribute

DEFAULT_CALL_STATE = 'created' module-attribute

DEFAULT_HTTP_METHOD = 'POST' module-attribute

DEFAULT_TOKEN_EXPIRY = 3600 module-attribute

DEFAULT_PROJECT_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' module-attribute

DEFAULT_SPACE_ID = 'zzzzzzzz-yyyy-xxxx-wwww-vvvvvvvvvvvv' module-attribute

DEFAULT_SPACE_NAME = 'example-space' module-attribute

DEFAULT_ENVIRONMENT = 'production' module-attribute

HTTP_REQUEST_TIMEOUT = 30 module-attribute

RESULT_LINE_SEP = '-' * 60 module-attribute

SERVERLESS_PLATFORMS = ['lambda', 'cgi', 'cloud_function', 'azure_function'] module-attribute

AWS_DEFAULT_REGION = 'us-east-1' module-attribute

AWS_DEFAULT_STAGE = 'prod' module-attribute

GCP_DEFAULT_REGION = 'us-central1' module-attribute

ERROR_MISSING_AGENT = 'Error: Missing required argument.' module-attribute

ERROR_MULTIPLE_AGENTS = 'Multiple agents found in file. Please specify --agent-class' module-attribute

ERROR_NO_AGENTS = 'No agents found in file: {file_path}' module-attribute

ERROR_AGENT_NOT_FOUND = "Agent class '{class_name}' not found in file: {file_path}" module-attribute

ERROR_FUNCTION_NOT_FOUND = "Function '{function_name}' not found in agent" module-attribute

ERROR_CGI_HOST_REQUIRED = 'CGI simulation requires --cgi-host' module-attribute

HELP_DESCRIPTION = 'Test SWAIG functions and generate SWML documents for SignalWire AI agents\n\nIMPORTANT: When using --exec, ALL options (like --call-id, --verbose, etc.) must come BEFORE --exec.\nEverything after --exec <function_name> is treated as arguments to the function.' module-attribute

HELP_EPILOG_SHORT = '\nexamples:\n # Execute a function\n %(prog)s agent.py --exec search --query "test" --limit 5\n \n # Execute with persistent session (--call-id MUST come BEFORE --exec)\n %(prog)s agent.py --call-id my-session --exec add_todo --text "Buy milk"\n \n # WRONG: This won\'t work! --call-id is treated as a function argument\n %(prog)s agent.py --exec add_todo --text "Buy milk" --call-id my-session\n \n # Generate SWML\n %(prog)s agent.py --dump-swml --raw | jq \'.\'\n \n # Test with specific agent\n %(prog)s multi_agent.py --agent-class MattiAgent --list-tools\n \n # Simulate serverless\n %(prog)s agent.py --simulate-serverless lambda --exec my_function\n\nFor platform-specific options: %(prog)s --help-platforms\nFor more examples: %(prog)s --help-examples\n' module-attribute

core

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

agent_loader

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Agent discovery and loading functionality

AGENT_BASE_AVAILABLE = True module-attribute
SWML_SERVICE_AVAILABLE = True module-attribute
NEW_LOADER_AVAILABLE = True module-attribute
discover_services_in_file(service_path)

Discover all available SWML services (including agents) in a Python file without instantiating them

Parameters:

Name Type Description Default
service_path str

Path to the Python file containing services

required

Returns:

Type Description
list[dict[str, Any]]

List of dictionaries with service information

Raises:

Type Description
ImportError

If the file cannot be imported

FileNotFoundError

If the file doesn't exist

discover_agents_in_file(agent_path)

Backward compatibility wrapper - discovers agents in a file

Parameters:

Name Type Description Default
agent_path str

Path to the Python file containing agents

required

Returns:

Type Description
list[dict[str, Any]]

List of dictionaries with agent information

load_service_from_file(service_path, service_identifier=None, prefer_route=True)

Load a SWML service from a Python file

Parameters:

Name Type Description Default
service_path str

Path to the Python file containing the service

required
service_identifier str | None

Optional service identifier - can be class name or route

None
prefer_route bool

If True, interpret identifier as route first, then class name

True

Returns:

Type Description
SWMLService

SWMLService instance (could be AgentBase or basic SWMLService)

Raises:

Type Description
ImportError

If the file cannot be imported

ValueError

If no service is found in the file

load_agent_from_file(agent_path, agent_class_name=None)

Load an agent from a Python file

Parameters:

Name Type Description Default
agent_path str

Path to the Python file containing the agent

required
agent_class_name str | None

Optional name of the agent class to instantiate

None

Returns:

Type Description
AgentBase

AgentBase instance

Raises:

Type Description
ImportError

If the file cannot be imported

ValueError

If no agent is found in the file

argparse_helpers

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Custom argument parsing and function argument parsing

CustomArgumentParser

Bases: ArgumentParser

Custom ArgumentParser with better error handling

__init__(*args, **kwargs)
error(message)

Override error method to provide user-friendly error messages

print_usage(file=None)

Override print_usage to suppress output when we want custom error handling

parse_args(args=None, namespace=None)

Override parse_args to provide custom error handling for missing arguments

parse_function_arguments(function_args_list, func_schema)

Parse function arguments from command line with type coercion based on schema

Parameters:

Name Type Description Default
function_args_list list[str]

List of command line arguments after --args

required
func_schema Any

Function schema with parameter definitions

required

Returns:

Type Description
dict[str, Any]

Dictionary of parsed function arguments

dynamic_config

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Apply dynamic configuration to agents

apply_dynamic_config(agent, mock_request=None, verbose=False)

Apply dynamic configuration callback if the agent has one

Parameters:

Name Type Description Default
agent AgentBase

The agent instance

required
mock_request Optional[MockRequest]

Optional mock request object

None
verbose bool

Whether to print verbose output

False

service_loader

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Service discovery and loading functionality - new simplified approach

DEPENDENCIES_AVAILABLE = True module-attribute
ServiceCapture

Captures SWMLService instances when they try to run/serve

captured_services = [] instance-attribute
original_methods = {} instance-attribute
__init__()
capture(service_path, suppress_output=False)

Execute a service file and capture any services that try to run

Parameters:

Name Type Description Default
service_path str

Path to the Python file

required
suppress_output bool

If True, suppress stdout during module execution

False

Returns:

Type Description
list[SWMLService]

List of captured SWMLService instances

simulate_request_to_service(service, method='POST', body=None, query_params=None, headers=None) async

Simulate a request to a SWMLService instance

Parameters:

Name Type Description Default
service SWMLService

The SWMLService instance

required
method str

HTTP method (GET or POST)

'POST'
body dict[str, Any] | None

Request body for POST requests

None
query_params dict[str, str] | None

Query parameters

None
headers dict[str, str] | None

Request headers

None

Returns:

Type Description
dict[str, Any]

The service's response as a dict

load_and_simulate_service(service_path, route=None, method='POST', body=None, query_params=None, headers=None, suppress_output=False)

Load a service file and simulate a request to it

This is the main entry point that combines loading and request simulation

Parameters:

Name Type Description Default
service_path str

Path to the service file

required
route str | None

Optional route to request (for multi-service files)

None
method str

HTTP method

'POST'
body dict[str, Any] | None

Request body

None
query_params dict[str, str] | None

Query parameters

None
headers dict[str, str] | None

Request headers

None

Returns:

Type Description
dict[str, Any]

The service's response

load_agent_from_file(agent_path, agent_class_name=None, suppress_output=False)

Backward compatibility wrapper

Note: This still uses the direct extraction approach for compatibility

discover_agents_in_file(agent_path)

Backward compatibility wrapper

dokku

SignalWire Agent Dokku Deployment Tool

CLI tool for deploying SignalWire agents to Dokku with support for: - Simple git push deployment - Full CI/CD with GitHub Actions - Service provisioning (PostgreSQL, Redis) - Preview environments for PRs

Usage

sw-agent-dokku init myagent # Simple mode sw-agent-dokku init myagent --cicd # With GitHub Actions CI/CD sw-agent-dokku deploy # Deploy current directory sw-agent-dokku logs # Tail logs sw-agent-dokku config set KEY=value # Set environment variables sw-agent-dokku scale web=2 # Scale processes

PROCFILE_TEMPLATE = 'web: gunicorn app:app --bind 0.0.0.0:$PORT --workers 2 --worker-class uvicorn.workers.UvicornWorker\n' module-attribute

RUNTIME_TEMPLATE = 'python-3.11\n' module-attribute

REQUIREMENTS_TEMPLATE = 'signalwire-agents>=1.0.16\ngunicorn>=21.0.0\nuvicorn>=0.24.0\npython-dotenv>=1.0.0\nrequests>=2.28.0\n' module-attribute

CHECKS_TEMPLATE = 'WAIT=5\nTIMEOUT=30\nATTEMPTS=5\n\n/health\n' module-attribute

GITIGNORE_TEMPLATE = '# Environment\n.env\n.venv/\nvenv/\n__pycache__/\n*.pyc\n*.pyo\n\n# IDE\n.vscode/\n.idea/\n*.swp\n*.swo\n\n# Testing\n.pytest_cache/\n.coverage\nhtmlcov/\n\n# Build\ndist/\nbuild/\n*.egg-info/\n\n# Logs\n*.log\n\n# OS\n.DS_Store\nThumbs.db\n' module-attribute

ENV_EXAMPLE_TEMPLATE = "# SignalWire Agent Configuration\n# =============================================================================\n\n# SignalWire Credentials (required for WebRTC calling)\nSIGNALWIRE_SPACE_NAME=your-space\nSIGNALWIRE_PROJECT_ID=your-project-id\nSIGNALWIRE_TOKEN=your-api-token\n\n# Public URL for SWML callbacks (required for WebRTC calling)\n# This should be your publicly accessible URL (e.g., ngrok, dokku domain)\nSWML_PROXY_URL_BASE=https://your-app.example.com\n\n# Basic Auth for SWML endpoints (password required, user defaults to 'signalwire')\n# SWML_BASIC_AUTH_USER=signalwire\nSWML_BASIC_AUTH_PASSWORD=your-secure-password\n\n# Agent Configuration\nAGENT_NAME={app_name}\n\n# App Configuration\nAPP_ENV=production\nAPP_NAME={app_name}\n\n# Optional: External Services\n# DATABASE_URL=postgres://user:pass@host:5432/db\n# REDIS_URL=redis://host:6379\n" module-attribute

APP_TEMPLATE = '#!/usr/bin/env python3\n"""\n{agent_name} - SignalWire AI Agent\n\nDeployed to Dokku with automatic health checks and SWAIG support.\n"""\n\nimport os\nfrom dotenv import load_dotenv\nfrom signalwire import AgentBase, FunctionResult\n\n# Load environment variables from .env file\nload_dotenv()\n\n\nclass {agent_class}(AgentBase):\n """{agent_name} agent for Dokku deployment."""\n\n def __init__(self):\n super().__init__(name="{agent_slug}")\n\n self._configure_prompts()\n self.add_language("English", "en-US", "rime.spore")\n self._setup_functions()\n\n def _configure_prompts(self):\n self.prompt_add_section(\n "Role",\n "You are a helpful AI assistant deployed on Dokku."\n )\n\n self.prompt_add_section(\n "Guidelines",\n bullets=[\n "Be professional and courteous",\n "Ask clarifying questions when needed",\n "Keep responses concise and helpful"\n ]\n )\n\n def _setup_functions(self):\n @self.tool(\n description="Get information about a topic",\n parameters={{\n "type": "object",\n "properties": {{\n "topic": {{\n "type": "string",\n "description": "The topic to get information about"\n }}\n }},\n "required": ["topic"]\n }}\n )\n def get_info(args, raw_data):\n topic = args.get("topic", "")\n return FunctionResult(\n f"Information about {{topic}}: This is a placeholder response."\n )\n\n @self.tool(description="Get deployment information")\n def get_deployment_info(args, raw_data):\n app_name = os.getenv("APP_NAME", "unknown")\n app_env = os.getenv("APP_ENV", "unknown")\n\n return FunctionResult(\n f"Running on Dokku. App: {{app_name}}, Environment: {{app_env}}."\n )\n\n\n# Create agent instance\nagent = {agent_class}()\n\n# Expose the FastAPI app for gunicorn/uvicorn\napp = agent.get_app()\n\nif __name__ == "__main__":\n agent.run()\n' module-attribute

APP_TEMPLATE_WITH_WEB = '#!/usr/bin/env python3\n"""\n{agent_name} - SignalWire AI Agent\n\nDeployed to Dokku with automatic health checks, SWAIG support, and web interface.\nIncludes WebRTC calling support with dynamic token generation.\n"""\n\nimport os\nimport time\nfrom pathlib import Path\nfrom dotenv import load_dotenv\nimport requests\nfrom starlette.responses import JSONResponse\nfrom signalwire import AgentBase, AgentServer, FunctionResult\n\n# Load environment variables from .env file\nload_dotenv()\n\n\nclass {agent_class}(AgentBase):\n """{agent_name} agent for Dokku deployment."""\n\n def __init__(self):\n super().__init__(name="{agent_slug}", route="/swml")\n\n self._configure_prompts()\n self.add_language("English", "en-US", "rime.spore")\n self._setup_functions()\n\n def _configure_prompts(self):\n self.prompt_add_section(\n "Role",\n "You are a helpful AI assistant deployed on Dokku."\n )\n\n self.prompt_add_section(\n "Guidelines",\n bullets=[\n "Be professional and courteous",\n "Ask clarifying questions when needed",\n "Keep responses concise and helpful"\n ]\n )\n\n def _setup_functions(self):\n @self.tool(\n description="Get information about a topic",\n parameters={{\n "type": "object",\n "properties": {{\n "topic": {{\n "type": "string",\n "description": "The topic to get information about"\n }}\n }},\n "required": ["topic"]\n }}\n )\n def get_info(args, raw_data):\n topic = args.get("topic", "")\n return FunctionResult(\n f"Information about {{topic}}: This is a placeholder response."\n )\n\n @self.tool(description="Get deployment information")\n def get_deployment_info(args, raw_data):\n app_name = os.getenv("APP_NAME", "unknown")\n app_env = os.getenv("APP_ENV", "unknown")\n\n return FunctionResult(\n f"Running on Dokku. App: {{app_name}}, Environment: {{app_env}}."\n )\n\n\n# =============================================================================\n# SignalWire SWML Handler Management\n# =============================================================================\n\ndef get_signalwire_host():\n """Get the full SignalWire host from space name."""\n space = os.getenv("SIGNALWIRE_SPACE_NAME", "")\n if not space:\n return None\n if "." in space:\n return space\n return f"{{space}}.signalwire.com"\n\n\ndef find_existing_handler(sw_host, auth, agent_name):\n """Find an existing SWML handler by name."""\n try:\n resp = requests.get(\n f"https://{{sw_host}}/api/fabric/resources/external_swml_handlers",\n auth=auth,\n headers={{"Accept": "application/json"}}\n )\n if resp.status_code != 200:\n return None\n\n handlers = resp.json().get("data", [])\n for handler in handlers:\n swml_webhook = handler.get("swml_webhook", {{}})\n handler_name = swml_webhook.get("name") or handler.get("display_name")\n if handler_name == agent_name:\n handler_id = handler.get("id")\n handler_url = swml_webhook.get("primary_request_url", "")\n addr_resp = requests.get(\n f"https://{{sw_host}}/api/fabric/resources/external_swml_handlers/{{handler_id}}/addresses",\n auth=auth,\n headers={{"Accept": "application/json"}}\n )\n if addr_resp.status_code == 200:\n addresses = addr_resp.json().get("data", [])\n if addresses:\n return {{\n "id": handler_id,\n "name": handler_name,\n "url": handler_url,\n "address_id": addresses[0]["id"],\n "address": addresses[0]["channels"]["audio"]\n }}\n except Exception as e:\n print(f"Error checking existing handlers: {{e}}")\n return None\n\n\n# Store SWML handler info\nswml_handler_info = {{"id": None, "address_id": None, "address": None}}\n\n\ndef setup_swml_handler():\n """Set up SWML handler on startup."""\n sw_host = get_signalwire_host()\n project = os.getenv("SIGNALWIRE_PROJECT_ID", "")\n token = os.getenv("SIGNALWIRE_TOKEN", "")\n agent_name = os.getenv("AGENT_NAME", "{agent_slug}")\n proxy_url = os.getenv("SWML_PROXY_URL_BASE", os.getenv("APP_URL", ""))\n auth_user = os.getenv("SWML_BASIC_AUTH_USER", "signalwire")\n auth_pass = os.getenv("SWML_BASIC_AUTH_PASSWORD", "")\n\n if not all([sw_host, project, token]):\n print("SignalWire credentials not configured - skipping SWML handler setup")\n return\n\n if not proxy_url:\n print("SWML_PROXY_URL_BASE not set - skipping SWML handler setup")\n return\n\n # Build SWML URL with basic auth (user defaults to \'signalwire\' if not set)\n if auth_pass and "://" in proxy_url:\n scheme, rest = proxy_url.split("://", 1)\n swml_url = f"{{scheme}}://{{auth_user}}:{{auth_pass}}@{{rest}}/swml"\n else:\n swml_url = proxy_url + "/swml"\n\n auth = (project, token)\n headers = {{"Content-Type": "application/json", "Accept": "application/json"}}\n\n existing = find_existing_handler(sw_host, auth, agent_name)\n if existing:\n swml_handler_info["id"] = existing["id"]\n swml_handler_info["address_id"] = existing["address_id"]\n swml_handler_info["address"] = existing["address"]\n\n # Always update the URL on startup to ensure credentials are current\n try:\n requests.put(\n f"https://{{sw_host}}/api/fabric/resources/external_swml_handlers/{{existing[\'id\']}}",\n json={{"primary_request_url": swml_url, "primary_request_method": "POST"}},\n auth=auth,\n headers=headers\n )\n print(f"Updated SWML handler: {{existing[\'name\']}}")\n except Exception as e:\n print(f"Failed to update handler URL: {{e}}")\n print(f"Call address: {{existing[\'address\']}}")\n else:\n try:\n handler_resp = requests.post(\n f"https://{{sw_host}}/api/fabric/resources/external_swml_handlers",\n json={{\n "name": agent_name,\n "used_for": "calling",\n "primary_request_url": swml_url,\n "primary_request_method": "POST"\n }},\n auth=auth,\n headers=headers\n )\n handler_resp.raise_for_status()\n handler_id = handler_resp.json().get("id")\n swml_handler_info["id"] = handler_id\n\n addr_resp = requests.get(\n f"https://{{sw_host}}/api/fabric/resources/external_swml_handlers/{{handler_id}}/addresses",\n auth=auth,\n headers={{"Accept": "application/json"}}\n )\n addr_resp.raise_for_status()\n addresses = addr_resp.json().get("data", [])\n if addresses:\n swml_handler_info["address_id"] = addresses[0]["id"]\n swml_handler_info["address"] = addresses[0]["channels"]["audio"]\n print(f"Created SWML handler: {{agent_name}}")\n print(f"Call address: {{swml_handler_info[\'address\']}}")\n except Exception as e:\n print(f"Failed to create SWML handler: {{e}}")\n\n\n# =============================================================================\n# Server Setup\n# =============================================================================\n\nserver = AgentServer(host="0.0.0.0", port=int(os.getenv("PORT", 3000)))\nserver.register({agent_class}())\n\n# Serve static files from web/ directory\nweb_dir = Path(__file__).parent / "web"\nif web_dir.exists():\n server.serve_static_files(str(web_dir))\n\n\n# =============================================================================\n# API Endpoints\n# =============================================================================\n\n@server.app.get("/get_token")\ndef get_token():\n """Get a guest token for WebRTC calls."""\n sw_host = get_signalwire_host()\n project = os.getenv("SIGNALWIRE_PROJECT_ID", "")\n token = os.getenv("SIGNALWIRE_TOKEN", "")\n\n if not all([sw_host, project, token]):\n return JSONResponse({{"error": "SignalWire credentials not configured"}}, status_code=500)\n\n if not swml_handler_info["address_id"]:\n return JSONResponse({{"error": "SWML handler not configured - check startup logs"}}, status_code=500)\n\n auth = (project, token)\n headers = {{"Content-Type": "application/json", "Accept": "application/json"}}\n\n try:\n expire_at = int(time.time()) + 3600 * 24 # 24 hours\n\n guest_resp = requests.post(\n f"https://{{sw_host}}/api/fabric/guests/tokens",\n json={{\n "allowed_addresses": [swml_handler_info["address_id"]],\n "expire_at": expire_at\n }},\n auth=auth,\n headers=headers\n )\n guest_resp.raise_for_status()\n guest_token = guest_resp.json().get("token", "")\n\n return {{\n "token": guest_token,\n "address": swml_handler_info["address"]\n }}\n\n except requests.exceptions.RequestException as e:\n print(f"Token request failed: {{e}}")\n return JSONResponse({{"error": str(e)}}, status_code=500)\n\n\n@server.app.get("/get_resource_info")\ndef get_resource_info():\n """Get SWML handler resource info for dashboard link."""\n sw_host = get_signalwire_host()\n space_name = os.getenv("SIGNALWIRE_SPACE_NAME", "")\n return {{\n "space_name": space_name,\n "resource_id": swml_handler_info["id"],\n "dashboard_url": f"https://{{sw_host}}/neon/resources/{{swml_handler_info[\'id\']}}/edit?t=addresses" if sw_host and swml_handler_info["id"] else None\n }}\n\n\n# =============================================================================\n# Static File Handling\n# =============================================================================\n\nfrom fastapi import Request, HTTPException\nfrom fastapi.responses import FileResponse, RedirectResponse\nimport mimetypes\n\n@server.app.api_route("/swml", methods=["GET", "POST"])\nasync def swml_redirect():\n return RedirectResponse(url="/swml/", status_code=307)\n\n@server.app.get("/{{full_path:path}}")\nasync def serve_static(request: Request, full_path: str):\n """Serve static files from web/ directory"""\n if not web_dir.exists():\n raise HTTPException(status_code=404, detail="Not Found")\n\n if not full_path or full_path == "/":\n full_path = "index.html"\n\n file_path = web_dir / full_path\n\n try:\n file_path = file_path.resolve()\n if not str(file_path).startswith(str(web_dir.resolve())):\n raise HTTPException(status_code=404, detail="Not Found")\n except Exception:\n raise HTTPException(status_code=404, detail="Not Found")\n\n if file_path.exists() and file_path.is_file():\n media_type, _ = mimetypes.guess_type(str(file_path))\n return FileResponse(file_path, media_type=media_type)\n\n if (web_dir / full_path / "index.html").exists():\n return FileResponse(web_dir / full_path / "index.html", media_type="text/html")\n\n raise HTTPException(status_code=404, detail="Not Found")\n\n\n# Set up SWML handler on startup\nsetup_swml_handler()\n\n# Expose ASGI app for gunicorn\napp = server.app\n\nif __name__ == "__main__":\n server.run()\n' module-attribute

WEB_INDEX_TEMPLATE = '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>{agent_name}</title>\n <style>\n * {{ box-sizing: border-box; }}\n body {{\n font-family: system-ui, -apple-system, sans-serif;\n max-width: 900px;\n margin: 0 auto;\n padding: 40px 20px;\n background: #f8f9fa;\n color: #333;\n }}\n h1 {{\n color: #044cf6;\n border-bottom: 3px solid #044cf6;\n padding-bottom: 10px;\n }}\n h2 {{\n color: #333;\n margin-top: 40px;\n border-bottom: 1px solid #ddd;\n padding-bottom: 8px;\n }}\n .status {{\n background: #d4edda;\n border: 1px solid #c3e6cb;\n color: #155724;\n padding: 15px;\n border-radius: 8px;\n margin: 20px 0;\n }}\n .call-section {{\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n border-radius: 12px;\n padding: 30px;\n margin: 20px 0;\n color: white;\n text-align: center;\n }}\n .call-section h2 {{\n color: white;\n border: none;\n margin-top: 0;\n }}\n .call-controls {{\n display: flex;\n gap: 15px;\n justify-content: center;\n align-items: center;\n flex-wrap: wrap;\n }}\n .call-btn {{\n padding: 15px 40px;\n font-size: 18px;\n font-weight: bold;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.3s ease;\n }}\n .call-btn:disabled {{\n opacity: 0.5;\n cursor: not-allowed;\n }}\n .call-btn.connect {{\n background: #10b981;\n color: white;\n }}\n .call-btn.connect:hover:not(:disabled) {{\n background: #059669;\n transform: translateY(-2px);\n box-shadow: 0 5px 15px rgba(16, 185, 129, 0.4);\n }}\n .call-btn.disconnect {{\n background: #ef4444;\n color: white;\n }}\n .call-btn.disconnect:hover:not(:disabled) {{\n background: #dc2626;\n }}\n .call-status {{\n margin-top: 15px;\n font-size: 14px;\n opacity: 0.9;\n }}\n .destination-input {{\n padding: 12px 15px;\n font-size: 14px;\n border: 2px solid rgba(255,255,255,0.3);\n border-radius: 8px;\n background: rgba(255,255,255,0.1);\n color: white;\n width: 250px;\n }}\n .destination-input::placeholder {{\n color: rgba(255,255,255,0.6);\n }}\n .destination-input:focus {{\n outline: none;\n border-color: rgba(255,255,255,0.6);\n background: rgba(255,255,255,0.2);\n }}\n .endpoint {{\n background: white;\n border: 1px solid #ddd;\n border-radius: 8px;\n padding: 20px;\n margin: 15px 0;\n }}\n .endpoint h3 {{\n margin-top: 0;\n color: #044cf6;\n }}\n .method {{\n display: inline-block;\n padding: 4px 10px;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n margin-right: 10px;\n }}\n .method.get {{ background: #61affe; color: white; }}\n .method.post {{ background: #49cc90; color: white; }}\n .path {{\n font-family: monospace;\n font-size: 16px;\n color: #333;\n }}\n code, pre {{\n font-family: \'SF Mono\', Monaco, \'Courier New\', monospace;\n }}\n code {{\n background: #e9ecef;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 14px;\n }}\n pre {{\n background: #1e1e1e;\n color: #d4d4d4;\n padding: 15px;\n border-radius: 8px;\n overflow-x: auto;\n font-size: 13px;\n line-height: 1.5;\n margin: 0;\n }}\n pre .comment {{ color: #6a9955; }}\n .tabs {{\n display: flex;\n gap: 0;\n margin-top: 15px;\n }}\n .tab {{\n padding: 8px 16px;\n background: #e9ecef;\n border: 1px solid #ddd;\n border-bottom: none;\n border-radius: 8px 8px 0 0;\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: #666;\n transition: all 0.2s;\n }}\n .tab:hover {{ background: #dee2e6; }}\n .tab.active {{\n background: #1e1e1e;\n color: #d4d4d4;\n border-color: #1e1e1e;\n }}\n .tab-content {{\n display: none;\n border-radius: 0 8px 8px 8px;\n }}\n .tab-content.active {{ display: block; }}\n .browser-panel {{\n background: #f8f9fa;\n border: 1px solid #ddd;\n border-radius: 0 8px 8px 8px;\n padding: 15px;\n }}\n .try-btn {{\n padding: 10px 20px;\n background: #044cf6;\n color: white;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n font-weight: 500;\n font-size: 14px;\n transition: all 0.2s;\n }}\n .try-btn:hover {{ background: #0339c2; }}\n .try-btn:disabled {{\n background: #ccc;\n cursor: not-allowed;\n }}\n .response-area {{\n margin-top: 15px;\n display: none;\n }}\n .response-area.visible {{ display: block; }}\n .response-header {{\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n }}\n .response-status {{\n font-size: 13px;\n font-weight: 500;\n }}\n .response-status.success {{ color: #10b981; }}\n .response-status.error {{ color: #ef4444; }}\n .response-time {{\n font-size: 12px;\n color: #666;\n }}\n .response-body {{\n background: #1e1e1e;\n color: #d4d4d4;\n padding: 15px;\n border-radius: 8px;\n font-family: \'SF Mono\', Monaco, monospace;\n font-size: 12px;\n max-height: 300px;\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n }}\n .curl-panel {{\n position: relative;\n }}\n .copy-btn {{\n position: absolute;\n top: 10px;\n right: 10px;\n padding: 5px 10px;\n background: rgba(255,255,255,0.1);\n color: #999;\n border: 1px solid rgba(255,255,255,0.2);\n border-radius: 4px;\n cursor: pointer;\n font-size: 11px;\n transition: all 0.2s;\n }}\n .copy-btn:hover {{\n background: rgba(255,255,255,0.2);\n color: #fff;\n }}\n .audio-settings {{\n display: flex;\n gap: 20px;\n justify-content: center;\n margin-top: 15px;\n flex-wrap: wrap;\n }}\n .audio-setting {{\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 13px;\n color: rgba(255,255,255,0.9);\n }}\n .audio-setting input[type="checkbox"] {{\n width: 18px;\n height: 18px;\n cursor: pointer;\n }}\n .audio-setting label {{\n cursor: pointer;\n }}\n .phone-info {{\n background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n border: 1px solid #044cf6;\n border-radius: 12px;\n padding: 20px 25px;\n margin: 30px 0;\n max-width: 800px;\n margin-left: auto;\n margin-right: auto;\n }}\n .phone-info h3 {{\n margin: 0 0 10px 0;\n color: #fff;\n font-size: 16px;\n }}\n .phone-info p {{\n margin: 0 0 15px 0;\n color: rgba(255,255,255,0.8);\n font-size: 14px;\n line-height: 1.5;\n }}\n .phone-info a {{\n display: inline-block;\n padding: 10px 20px;\n background: #044cf6;\n color: white;\n text-decoration: none;\n border-radius: 6px;\n font-weight: 500;\n font-size: 14px;\n transition: all 0.2s;\n }}\n .phone-info a:hover {{\n background: #0339c2;\n }}\n .phone-info.hidden {{\n display: none;\n }}\n .footer {{\n margin-top: 50px;\n padding-top: 20px;\n border-top: 1px solid #ddd;\n color: #666;\n font-size: 14px;\n }}\n </style>\n</head>\n<body>\n <h1>{agent_name}</h1>\n\n <div class="status">\n Your agent is running and ready to receive calls!\n </div>\n\n <div class="call-section">\n <h2>Call Your Agent</h2>\n <p>Test your agent directly from the browser using WebRTC.</p>\n <div class="call-controls">\n <input type="text" id="destination" class="destination-input" placeholder="Address will be auto-filled" />\n <button id="connectBtn" class="call-btn connect">Call Agent</button>\n <button id="disconnectBtn" class="call-btn disconnect" disabled>Hang Up</button>\n </div>\n <div class="audio-settings">\n <div class="audio-setting">\n <input type="checkbox" id="echoCancellation" checked>\n <label for="echoCancellation">Echo Cancellation</label>\n </div>\n <div class="audio-setting">\n <input type="checkbox" id="noiseSuppression">\n <label for="noiseSuppression">Noise Suppression</label>\n </div>\n <div class="audio-setting">\n <input type="checkbox" id="autoGainControl">\n <label for="autoGainControl">Auto Gain Control</label>\n </div>\n </div>\n <div id="callStatus" class="call-status"></div>\n </div>\n\n <div id="phoneInfo" class="phone-info hidden">\n <h3>Want to call from a phone number?</h3>\n <p>You can assign a SignalWire phone number to this agent. Click below to add a number in the dashboard.</p>\n <a id="dashboardLink" href="#" target="_blank">Add Phone Number in Dashboard</a>\n </div>\n\n <h2>Endpoints</h2>\n\n <div class="endpoint">\n <h3><span class="method post">POST</span> <span class="path">/swml</span></h3>\n <p>Main SWML endpoint for SignalWire to fetch agent configuration.</p>\n <div class="tabs">\n <div class="tab active" onclick="switchTab(this, \'swml-browser\')">Browser</div>\n <div class="tab" onclick="switchTab(this, \'swml-curl\')">curl</div>\n </div>\n <div id="swml-browser" class="tab-content active">\n <div class="browser-panel">\n <button class="try-btn" onclick="tryEndpoint(\'POST\', \'/swml\', {{}}, \'swml-response\', true)">Try it</button>\n <div id="swml-response" class="response-area"></div>\n </div>\n </div>\n <div id="swml-curl" class="tab-content">\n <div class="curl-panel">\n <button class="copy-btn" onclick="copyCode(this)">Copy</button>\n <pre><span class="comment"># Get the SWML configuration</span>\ncurl -X POST <span class="base-url"></span>/swml \\\n -u <span class="auth-creds"></span> \\\n -H "Content-Type: application/json" \\\n -d \'{{}}\'</pre>\n </div>\n </div>\n </div>\n\n <div class="endpoint">\n <h3><span class="method get">GET</span> <span class="path">/get_token</span></h3>\n <p>Get a guest token for WebRTC calls. Returns a token and call address.</p>\n <div class="tabs">\n <div class="tab active" onclick="switchTab(this, \'token-browser\')">Browser</div>\n <div class="tab" onclick="switchTab(this, \'token-curl\')">curl</div>\n </div>\n <div id="token-browser" class="tab-content active">\n <div class="browser-panel">\n <button class="try-btn" onclick="tryEndpoint(\'GET\', \'/get_token\', null, \'token-response\')">Try it</button>\n <div id="token-response" class="response-area"></div>\n </div>\n </div>\n <div id="token-curl" class="tab-content">\n <div class="curl-panel">\n <button class="copy-btn" onclick="copyCode(this)">Copy</button>\n <pre><span class="comment"># Get a guest token</span>\ncurl <span class="base-url"></span>/get_token</pre>\n </div>\n </div>\n </div>\n\n <div class="endpoint">\n <h3><span class="method post">POST</span> <span class="path">/swml/swaig/</span></h3>\n <p>SWAIG function endpoint. Test your agent\'s functions.</p>\n <div class="tabs">\n <div class="tab active" onclick="switchTab(this, \'swaig-browser\')">Browser</div>\n <div class="tab" onclick="switchTab(this, \'swaig-curl\')">curl</div>\n </div>\n <div id="swaig-browser" class="tab-content active">\n <div class="browser-panel">\n <button class="try-btn" onclick="tryEndpoint(\'POST\', \'/swml/swaig/\', {{function: \'get_info\', argument: {{parsed: [{{topic: \'SignalWire\'}}]}}}}, \'swaig-response\', true)">Try it</button>\n <div id="swaig-response" class="response-area"></div>\n </div>\n </div>\n <div id="swaig-curl" class="tab-content">\n <div class="curl-panel">\n <button class="copy-btn" onclick="copyCode(this)">Copy</button>\n <pre><span class="comment"># Call a SWAIG function</span>\ncurl -X POST <span class="base-url"></span>/swml/swaig/ \\\n -u <span class="auth-creds"></span> \\\n -H "Content-Type: application/json" \\\n -d \'{{"function": "get_info", "argument": {{"parsed": [{{"topic": "SignalWire"}}]}}}}\'</pre>\n </div>\n </div>\n </div>\n\n <div class="endpoint">\n <h3><span class="method get">GET</span> <span class="path">/health</span></h3>\n <p>Health check endpoint for load balancers and monitoring.</p>\n </div>\n\n <div class="footer">\n Powered by <a href="https://signalwire.com">SignalWire</a> and the\n <a href="https://github.com/signalwire/signalwire-agents">SignalWire Agents SDK</a>\n </div>\n\n <script src="https://cdn.signalwire.com/@signalwire/client"></script>\n <script>\n function switchTab(tabEl, contentId) {{\n const endpoint = tabEl.closest(\'.endpoint\');\n endpoint.querySelectorAll(\'.tab\').forEach(t => t.classList.remove(\'active\'));\n endpoint.querySelectorAll(\'.tab-content\').forEach(c => c.classList.remove(\'active\'));\n tabEl.classList.add(\'active\');\n document.getElementById(contentId).classList.add(\'active\');\n }}\n\n function copyCode(btn) {{\n const pre = btn.parentElement.querySelector(\'pre\');\n const text = pre.textContent;\n navigator.clipboard.writeText(text).then(() => {{\n const orig = btn.textContent;\n btn.textContent = \'Copied!\';\n setTimeout(() => btn.textContent = orig, 1500);\n }});\n }}\n\n async function tryEndpoint(method, path, body, responseId, requiresAuth) {{\n const responseArea = document.getElementById(responseId);\n responseArea.classList.add(\'visible\');\n responseArea.innerHTML = \'<div class="response-status">Loading...</div>\';\n\n const startTime = performance.now();\n try {{\n const options = {{\n method: method,\n headers: {{}}\n }};\n if (body) {{\n options.headers[\'Content-Type\'] = \'application/json\';\n options.body = JSON.stringify(body);\n }}\n if (requiresAuth) {{\n // Credentials are not exposed in the browser; use curl examples instead\n }}\n\n const resp = await fetch(path, options);\n const endTime = performance.now();\n const duration = Math.round(endTime - startTime);\n\n let data;\n const contentType = resp.headers.get(\'content-type\') || \'\';\n if (contentType.includes(\'json\')) {{\n data = await resp.json();\n data = JSON.stringify(data, null, 2);\n }} else {{\n data = await resp.text();\n }}\n\n const statusClass = resp.ok ? \'success\' : \'error\';\n responseArea.innerHTML = `\n <div class="response-header">\n <span class="response-status ${{statusClass}}">${{resp.status}} ${{resp.statusText}}</span>\n <span class="response-time">${{duration}}ms</span>\n </div>\n <div class="response-body">${{escapeHtml(data)}}</div>\n `;\n }} catch (err) {{\n responseArea.innerHTML = `\n <div class="response-header">\n <span class="response-status error">Error</span>\n </div>\n <div class="response-body">${{escapeHtml(err.message)}}</div>\n `;\n }}\n }}\n\n function escapeHtml(text) {{\n const div = document.createElement(\'div\');\n div.textContent = text;\n return div.innerHTML;\n }}\n\n // WebRTC calling - robust pattern from santa app.js\n let client = null;\n let roomSession = null;\n let currentToken = null;\n let currentDestination = null;\n\n const connectBtn = document.getElementById(\'connectBtn\');\n const disconnectBtn = document.getElementById(\'disconnectBtn\');\n const destinationInput = document.getElementById(\'destination\');\n const callStatus = document.getElementById(\'callStatus\');\n\n function updateCallStatus(message) {{\n callStatus.textContent = message;\n }}\n\n async function connect() {{\n // Debounce - prevent double-clicks\n if (connectBtn.disabled) {{\n console.log(\'Call already in progress\');\n return;\n }}\n connectBtn.disabled = true;\n connectBtn.textContent = \'Connecting...\';\n\n try {{\n updateCallStatus(\'Getting token...\');\n\n const tokenResp = await fetch(\'/get_token\');\n const tokenData = await tokenResp.json();\n\n if (tokenData.error) {{\n throw new Error(tokenData.error);\n }}\n\n currentToken = tokenData.token;\n currentDestination = tokenData.address;\n\n if (tokenData.address) {{\n destinationInput.value = tokenData.address;\n }}\n\n console.log(\'Got token, destination:\', currentDestination);\n updateCallStatus(\'Connecting...\');\n\n // Initialize SignalWire client\n if (window.SignalWire && typeof window.SignalWire.SignalWire === \'function\') {{\n console.log(\'Initializing SignalWire client...\');\n client = await window.SignalWire.SignalWire({{\n token: currentToken\n }});\n }} else {{\n console.error(\'SignalWire SDK structure:\', window.SignalWire);\n throw new Error(\'SignalWire.SignalWire function not found\');\n }}\n\n const destination = currentDestination || destinationInput.value;\n roomSession = await client.dial({{\n to: destination,\n audio: {{\n echoCancellation: document.getElementById(\'echoCancellation\').checked,\n noiseSuppression: document.getElementById(\'noiseSuppression\').checked,\n autoGainControl: document.getElementById(\'autoGainControl\').checked\n }},\n video: false\n }});\n\n console.log(\'Room session created:\', roomSession);\n\n roomSession.on(\'call.joined\', () => {{\n console.log(\'Call joined\');\n updateCallStatus(\'Connected\');\n disconnectBtn.disabled = false;\n }});\n\n roomSession.on(\'call.left\', () => {{\n console.log(\'Call left\');\n updateCallStatus(\'Call ended\');\n cleanup();\n }});\n\n roomSession.on(\'destroy\', () => {{\n console.log(\'Session destroyed\');\n updateCallStatus(\'Call ended\');\n cleanup();\n }});\n\n roomSession.on(\'room.left\', () => {{\n console.log(\'Room left\');\n cleanup();\n }});\n\n await roomSession.start();\n console.log(\'Call started successfully\');\n\n // Update UI\n connectBtn.style.display = \'none\';\n disconnectBtn.style.display = \'inline-block\';\n\n }} catch (err) {{\n console.error(\'Connection error:\', err);\n updateCallStatus(\'Error: \' + err.message);\n cleanup();\n }}\n }}\n\n async function disconnect() {{\n console.log(\'Disconnect called\');\n await hangup();\n }}\n\n async function hangup() {{\n try {{\n if (roomSession) {{\n console.log(\'Hanging up call...\');\n await roomSession.hangup();\n console.log(\'Call hung up successfully\');\n }}\n }} catch (err) {{\n console.error(\'Hangup error:\', err);\n }}\n cleanup();\n }}\n\n function cleanup() {{\n console.log(\'Cleanup called\');\n\n // Clean up local stream if it exists\n if (roomSession && roomSession.localStream) {{\n console.log(\'Stopping local stream tracks\');\n roomSession.localStream.getTracks().forEach(track => {{\n track.stop();\n }});\n }}\n\n roomSession = null;\n\n // Disconnect the client properly\n if (client) {{\n try {{\n console.log(\'Disconnecting client\');\n client.disconnect();\n }} catch (e) {{\n console.log(\'Client disconnect error:\', e);\n }}\n client = null;\n }}\n\n // Reset UI\n connectBtn.disabled = false;\n connectBtn.textContent = \'Call Agent\';\n connectBtn.style.display = \'inline-block\';\n disconnectBtn.disabled = true;\n disconnectBtn.style.display = \'none\';\n }}\n\n connectBtn.addEventListener(\'click\', connect);\n disconnectBtn.addEventListener(\'click\', disconnect);\n\n // Clean up on page unload\n window.addEventListener(\'beforeunload\', () => {{\n if (roomSession) {{\n hangup();\n }}\n }});\n\n // Initialize on load\n document.addEventListener(\'DOMContentLoaded\', async function() {{\n const baseUrl = window.location.origin;\n document.querySelectorAll(\'.base-url\').forEach(function(el) {{\n el.textContent = baseUrl;\n }});\n\n // Auth credentials are not exposed in the browser for security\n document.querySelectorAll(\'.auth-creds\').forEach(function(el) {{\n el.textContent = \'user:password\';\n }});\n\n // Fetch resource info for dashboard link\n try {{\n const resourceResp = await fetch(\'/get_resource_info\');\n if (resourceResp.ok) {{\n const resourceInfo = await resourceResp.json();\n if (resourceInfo.dashboard_url) {{\n document.getElementById(\'dashboardLink\').href = resourceInfo.dashboard_url;\n document.getElementById(\'phoneInfo\').classList.remove(\'hidden\');\n }}\n }}\n }} catch (e) {{\n console.log(\'Could not fetch resource info:\', e);\n }}\n }});\n </script>\n</body>\n</html>\n' module-attribute

APP_JSON_TEMPLATE = '{{\n "name": "{app_name}",\n "description": "SignalWire AI Agent with WebRTC calling support",\n "keywords": ["signalwire", "ai", "agent", "python", "webrtc"],\n "env": {{\n "APP_ENV": {{\n "description": "Application environment",\n "value": "production"\n }},\n "AGENT_NAME": {{\n "description": "Name for the SWML handler resource",\n "value": "{app_name}"\n }},\n "SIGNALWIRE_SPACE_NAME": {{\n "description": "SignalWire space name (e.g., \'myspace\' or \'myspace.signalwire.com\')",\n "required": true\n }},\n "SIGNALWIRE_PROJECT_ID": {{\n "description": "SignalWire project ID",\n "required": true\n }},\n "SIGNALWIRE_TOKEN": {{\n "description": "SignalWire API token",\n "required": true\n }},\n "SWML_PROXY_URL_BASE": {{\n "description": "Public URL base for SWML callbacks (e.g., \'https://myapp.example.com\')",\n "required": true\n }},\n "SWML_BASIC_AUTH_USER": {{\n "description": "Basic auth username for SWML endpoints",\n "required": true\n }},\n "SWML_BASIC_AUTH_PASSWORD": {{\n "description": "Basic auth password for SWML endpoints",\n "required": true\n }}\n }},\n "formation": {{\n "web": {{\n "quantity": 1\n }}\n }},\n "buildpacks": [\n {{\n "url": "heroku/python"\n }}\n ],\n "healthchecks": {{\n "web": [\n {{\n "type": "startup",\n "name": "port listening",\n "listening": true,\n "attempts": 10,\n "wait": 5,\n "timeout": 60\n }}\n ]\n }}\n}}\n' module-attribute

DEPLOY_SCRIPT_TEMPLATE = '#!/bin/bash\n# Dokku deployment helper for {app_name}\nset -e\n\nAPP_NAME="${{1:-{app_name}}}"\nDOKKU_HOST="${{2:-{dokku_host}}}"\n\necho "═══════════════════════════════════════════════════════════"\necho " Deploying $APP_NAME to $DOKKU_HOST"\necho "═══════════════════════════════════════════════════════════"\n\n# Initialize git if needed\nif [ ! -d .git ]; then\n echo "→ Initializing git repository..."\n git init\n git add .\n git commit -m "Initial commit"\nfi\n\n# Create app if it doesn\'t exist\necho "→ Creating app (if not exists)..."\nssh dokku@$DOKKU_HOST apps:create $APP_NAME 2>/dev/null || true\n\n# Set environment variables\necho "→ Setting environment variables..."\nAUTH_PASS=$(openssl rand -base64 24 | tr -d \'/+=\' | head -c 24)\nssh dokku@$DOKKU_HOST config:set --no-restart $APP_NAME \\\n APP_ENV=production \\\n APP_NAME=$APP_NAME \\\n SWML_BASIC_AUTH_USER=admin \\\n SWML_BASIC_AUTH_PASSWORD=$AUTH_PASS\n\n# Add dokku remote\necho "→ Configuring git remote..."\ngit remote add dokku dokku@$DOKKU_HOST:$APP_NAME 2>/dev/null || \\\ngit remote set-url dokku dokku@$DOKKU_HOST:$APP_NAME\n\n# Deploy\necho "→ Pushing to Dokku..."\ngit push dokku main --force\n\n# Enable SSL\necho "→ Enabling Let\'s Encrypt SSL..."\nssh dokku@$DOKKU_HOST letsencrypt:enable $APP_NAME 2>/dev/null || \\\necho " (SSL setup may require manual configuration)"\n\necho ""\necho "═══════════════════════════════════════════════════════════"\necho " ✅ Deployment complete!"\necho ""\necho " 🌐 URL: https://$APP_NAME.$DOKKU_HOST"\necho " 🔑 Auth: admin / $AUTH_PASS"\necho ""\necho " Configure SignalWire phone number SWML URL to:"\necho " https://admin:$AUTH_PASS@$APP_NAME.$DOKKU_HOST/{route}"\necho "═══════════════════════════════════════════════════════════"\n' module-attribute

README_SIMPLE_TEMPLATE = '# {app_name}\n\nA SignalWire AI Agent deployed to Dokku.\n\n## Quick Deploy\n\n```bash\n./deploy.sh {app_name} {dokku_host}\n```\n\n## Manual Deployment\n\n1. **Create the app:**\n ```bash\n ssh dokku@{dokku_host} apps:create {app_name}\n ```\n\n2. **Set environment variables:**\n ```bash\n ssh dokku@{dokku_host} config:set {app_name} \\\n SWML_BASIC_AUTH_USER=admin \\\n SWML_BASIC_AUTH_PASSWORD=secure-password \\\n APP_ENV=production\n ```\n\n3. **Add git remote and deploy:**\n ```bash\n git remote add dokku dokku@{dokku_host}:{app_name}\n git push dokku main\n ```\n\n4. **Enable SSL:**\n ```bash\n ssh dokku@{dokku_host} letsencrypt:enable {app_name}\n ```\n\n## Usage\n\nYour agent is available at: `https://{app_name}.{dokku_host_domain}`\n\nConfigure your SignalWire phone number:\n- **SWML URL:** `https://{app_name}.{dokku_host_domain}/{route}`\n- **Auth:** Basic auth with your configured credentials\n\n## Useful Commands\n\n```bash\n# View logs\nssh dokku@{dokku_host} logs {app_name} -t\n\n# Restart app\nssh dokku@{dokku_host} ps:restart {app_name}\n\n# View environment variables\nssh dokku@{dokku_host} config:show {app_name}\n\n# Scale workers\nssh dokku@{dokku_host} ps:scale {app_name} web=2\n\n# Rollback to previous release\nssh dokku@{dokku_host} releases:rollback {app_name}\n```\n\n## Local Development\n\n```bash\npip install -r requirements.txt\nuvicorn app:app --reload --port 8080\n```\n\nTest with swaig-test:\n```bash\nswaig-test app.py --list-tools\n```\n' module-attribute

DEPLOY_WORKFLOW_TEMPLATE = "# Deploy to Dokku - calls reusable workflow from dokku-deploy-system\nname: Deploy\n\non:\n workflow_dispatch:\n push:\n branches: [main, staging, develop]\n\npermissions:\n contents: read\n deployments: write\n\nconcurrency:\n group: deploy-${{{{ github.ref }}}}\n cancel-in-progress: true\n\njobs:\n deploy:\n uses: signalwire-demos/dokku-deploy-system/.github/workflows/deploy.yml@main\n secrets: inherit\n # Optional: customize health check path\n # with:\n # health_check_path: '/health'\n" module-attribute

PREVIEW_WORKFLOW_TEMPLATE = "# Preview environments for pull requests - calls reusable workflow from dokku-deploy-system\nname: Preview\n\non:\n pull_request:\n types: [opened, synchronize, reopened, closed]\n\nconcurrency:\n group: preview-${{{{ github.event.pull_request.number }}}}\n\njobs:\n preview:\n uses: signalwire-demos/dokku-deploy-system/.github/workflows/preview.yml@main\n secrets: inherit\n # Optional: customize memory limit for previews\n # with:\n # memory_limit: '256m'\n" module-attribute

DOKKU_CONFIG_TEMPLATE = '# ═══════════════════════════════════════════════════════════════════════════════\n# Dokku App Configuration\n# ═══════════════════════════════════════════════════════════════════════════════\n#\n# Configuration for your Dokku app deployment.\n# These settings are applied during the deployment workflow.\n#\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Resource Limits\n# ─────────────────────────────────────────────────────────────────────────────\n# Memory: 256m, 512m, 1g, 2g, etc.\n# CPU: Number of cores (can be fractional, e.g., 0.5)\nresources:\n memory: 512m\n cpu: 1\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Health Check\n# ─────────────────────────────────────────────────────────────────────────────\n# Path that returns 200 OK when app is healthy\nhealthcheck:\n path: /health\n timeout: 30\n attempts: 5\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Scaling\n# ─────────────────────────────────────────────────────────────────────────────\n# Number of web workers (dynos)\nscale:\n web: 1\n # worker: 1 # Uncomment for background workers\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Custom Domains\n# ─────────────────────────────────────────────────────────────────────────────\n# Additional domains for this app (requires DNS configuration)\n# custom_domains:\n# - www.example.com\n# - api.example.com\n\n# ─────────────────────────────────────────────────────────────────────────────\n# Environment-Specific Overrides\n# ─────────────────────────────────────────────────────────────────────────────\nenvironments:\n production:\n resources:\n memory: 1g\n cpu: 2\n scale:\n web: 2\n\n staging:\n resources:\n memory: 512m\n cpu: 1\n\n preview:\n resources:\n memory: 256m\n cpu: 0.5\n' module-attribute

SERVICES_TEMPLATE = '# ═══════════════════════════════════════════════════════════════════════════════\n# Dokku Services Configuration\n# ═══════════════════════════════════════════════════════════════════════════════\n#\n# Define which backing services your app needs.\n# Services are automatically provisioned and linked during deployment.\n#\n# When a service is linked, its connection URL is automatically\n# injected as an environment variable (e.g., DATABASE_URL, REDIS_URL).\n#\n# ═══════════════════════════════════════════════════════════════════════════════\n\nservices:\n # ─────────────────────────────────────────────────────────────────────────────\n # PostgreSQL Database\n # ─────────────────────────────────────────────────────────────────────────────\n # Environment variable: DATABASE_URL\n # Format: postgres://user:pass@host:5432/database\n postgres:\n enabled: false # Set to true to enable\n environments:\n production:\n # Production gets its own dedicated database\n dedicated: true\n staging:\n # Staging gets its own database\n dedicated: true\n preview:\n # All preview apps share a single database to save resources\n shared: true\n\n # ─────────────────────────────────────────────────────────────────────────────\n # Redis Cache/Queue\n # ─────────────────────────────────────────────────────────────────────────────\n # Environment variable: REDIS_URL\n # Format: redis://host:6379\n redis:\n enabled: false # Set to true to enable\n environments:\n production:\n dedicated: true\n staging:\n dedicated: true\n preview:\n shared: true\n\n # ─────────────────────────────────────────────────────────────────────────────\n # MySQL Database\n # ─────────────────────────────────────────────────────────────────────────────\n # Environment variable: DATABASE_URL\n # Format: mysql://user:pass@host:3306/database\n mysql:\n enabled: false\n environments:\n preview:\n shared: true\n\n # ─────────────────────────────────────────────────────────────────────────────\n # MongoDB\n # ─────────────────────────────────────────────────────────────────────────────\n # Environment variable: MONGO_URL\n # Format: mongodb://user:pass@host:27017/database\n mongo:\n enabled: false\n environments:\n preview:\n shared: true\n\n # ─────────────────────────────────────────────────────────────────────────────\n # RabbitMQ Message Queue\n # ─────────────────────────────────────────────────────────────────────────────\n # Environment variable: RABBITMQ_URL\n # Format: amqp://user:pass@host:5672\n rabbitmq:\n enabled: false\n environments:\n preview:\n # Don\'t provision RabbitMQ for previews (too expensive)\n enabled: false\n\n # ─────────────────────────────────────────────────────────────────────────────\n # Elasticsearch\n # ─────────────────────────────────────────────────────────────────────────────\n # Environment variable: ELASTICSEARCH_URL\n # Format: http://host:9200\n elasticsearch:\n enabled: false\n environments:\n preview:\n enabled: false\n\n# ═══════════════════════════════════════════════════════════════════════════════\n# External/Managed Services\n# ═══════════════════════════════════════════════════════════════════════════════\n#\n# For production, you may want to use managed services like AWS RDS,\n# ElastiCache, etc. Define the connection URLs here (reference GitHub secrets).\n#\n# These override the Dokku-managed services for the specified environment.\n#\n# ═══════════════════════════════════════════════════════════════════════════════\n\n# external_services:\n# production:\n# DATABASE_URL: "${secrets.PROD_DATABASE_URL}"\n# REDIS_URL: "${secrets.PROD_REDIS_URL}"\n# staging:\n# DATABASE_URL: "${secrets.STAGING_DATABASE_URL}"\n' module-attribute

README_CICD_TEMPLATE = "# {app_name}\n\nA SignalWire AI Agent with automated GitHub → Dokku deployments.\n\n## Features\n\n- ✅ Auto-deploy on push to main/staging/develop\n- ✅ Preview environments for pull requests\n- ✅ Automatic SSL via Let's Encrypt\n- ✅ Zero-downtime deployments\n- ✅ Multi-environment support\n\n## Setup\n\n### 1. GitHub Secrets\n\nAdd these secrets to your repository (Settings → Secrets → Actions):\n\n| Secret | Description |\n|--------|-------------|\n| `DOKKU_HOST` | Your Dokku server hostname |\n| `DOKKU_SSH_PRIVATE_KEY` | SSH private key for deployments |\n| `BASE_DOMAIN` | Base domain (e.g., `yourdomain.com`) |\n| `SWML_BASIC_AUTH_USER` | Basic auth username |\n| `SWML_BASIC_AUTH_PASSWORD` | Basic auth password |\n\n### 2. GitHub Environments\n\nCreate these environments (Settings → Environments):\n- `production` - Deploy from `main` branch\n- `staging` - Deploy from `staging` branch\n- `development` - Deploy from `develop` branch\n- `preview` - Deploy from pull requests\n\n### 3. Deploy\n\nJust push to a branch:\n\n```bash\ngit push origin main # → {app_name}.yourdomain.com\ngit push origin staging # → {app_name}-staging.yourdomain.com\ngit push origin develop # → {app_name}-dev.yourdomain.com\n```\n\nOr open a PR for a preview environment.\n\n## Branch → Environment Mapping\n\n| Branch | App Name | URL |\n|--------|----------|-----|\n| `main` | `{app_name}` | `{app_name}.yourdomain.com` |\n| `staging` | `{app_name}-staging` | `{app_name}-staging.yourdomain.com` |\n| `develop` | `{app_name}-dev` | `{app_name}-dev.yourdomain.com` |\n| PR #42 | `{app_name}-pr-42` | `{app_name}-pr-42.yourdomain.com` |\n\n## Manual Operations\n\n```bash\n# View logs\nssh dokku@server logs {app_name} -t\n\n# SSH into container\nssh dokku@server enter {app_name}\n\n# Restart\nssh dokku@server ps:restart {app_name}\n\n# Rollback\nssh dokku@server releases:rollback {app_name}\n\n# Scale\nssh dokku@server ps:scale {app_name} web=2\n```\n\n## Local Development\n\n```bash\npip install -r requirements.txt\nuvicorn app:app --reload --port 8080\n```\n\nTest with swaig-test:\n```bash\nswaig-test app.py --list-tools\n```\n" module-attribute

Colors

RED = '\x1b[0;31m' class-attribute instance-attribute
GREEN = '\x1b[0;32m' class-attribute instance-attribute
YELLOW = '\x1b[1;33m' class-attribute instance-attribute
BLUE = '\x1b[0;34m' class-attribute instance-attribute
CYAN = '\x1b[0;36m' class-attribute instance-attribute
MAGENTA = '\x1b[0;35m' class-attribute instance-attribute
BOLD = '\x1b[1m' class-attribute instance-attribute
DIM = '\x1b[2m' class-attribute instance-attribute
NC = '\x1b[0m' class-attribute instance-attribute

DokkuProjectGenerator

Generates Dokku deployment files for SignalWire agents.

app_name = app_name instance-attribute
options = options instance-attribute
project_dir = Path(options.get('project_dir', f'./{app_name}')) instance-attribute
agent_slug = app_name.lower().replace(' ', '-').replace('_', '-') instance-attribute
agent_class = ''.join((word.capitalize()) for word in (app_name.replace('-', ' ').replace('_', ' ').split())) + 'Agent' instance-attribute
__init__(app_name, options)
generate()

Generate the project files.

print_step(msg)

print_success(msg)

print_warning(msg)

print_error(msg)

print_header(msg)

prompt(question, default='')

prompt_yes_no(question, default=True)

generate_password(length=32)

cmd_init(args)

Initialize a new Dokku project.

cmd_deploy(args)

Deploy to Dokku.

cmd_logs(args)

Tail Dokku logs.

cmd_config(args)

Manage Dokku config.

cmd_scale(args)

Scale Dokku processes.

main()

execution

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

datamap_exec

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

DataMap function execution and template expansion

simple_template_expand(template, data)

Simple template expansion for DataMap testing Supports both ${key} and %{key} syntax with nested object access and array indexing

Parameters:

Name Type Description Default
template str

Template string with ${} or %{} variables

required
data dict[str, Any]

Data dictionary for expansion

required

Returns:

Type Description
str

Expanded string

execute_datamap_function(datamap_config, args, verbose=False)

Execute a DataMap function following the actual DataMap processing pipeline: 1. Expressions (pattern matching) 2. Webhooks (try each sequentially until one succeeds) 3. Foreach (within successful webhook) 4. Output (from successful webhook) 5. Fallback output (if all webhooks fail)

Parameters:

Name Type Description Default
datamap_config dict[str, Any]

DataMap configuration dictionary

required
args dict[str, Any]

Function arguments

required
verbose bool

Enable verbose output

False

Returns:

Type Description
Any

Function result (should be string or dict with 'response' key)

webhook_exec

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Webhook function execution (including external)

execute_external_webhook_function(func, function_name, function_args, post_data, verbose=False)

Execute an external webhook SWAIG function by making an HTTP request to the external service. This simulates what SignalWire would do when calling an external webhook function.

Parameters:

Name Type Description Default
func SWAIGFunction

The SWAIGFunction object with webhook_url

required
function_name str

Name of the function being called

required
function_args dict[str, Any]

Parsed function arguments

required
post_data dict[str, Any]

Complete post data to send to the webhook

required
verbose bool

Whether to show verbose output

False

Returns:

Type Description
dict[str, Any]

Response from the external webhook service

init_project

SignalWire Agent Project Generator

Interactive CLI tool to create new SignalWire agent projects with customizable features.

Usage

sw-agent-init # Interactive mode sw-agent-init myagent # Quick mode with project name sw-agent-init myagent --type full --no-venv

CLOUD_PLATFORMS = {'local': 'Local Agent (FastAPI/uvicorn server)', 'aws': 'AWS Lambda Function', 'gcp': 'Google Cloud Function', 'azure': 'Azure Function'} module-attribute

DEFAULT_REGIONS = {'aws': 'us-east-1', 'gcp': 'us-central1', 'azure': 'eastus'} module-attribute

TEMPLATE_AGENTS_INIT = 'from .main_agent import MainAgent\n\n__all__ = ["MainAgent"]\n' module-attribute

TEMPLATE_SKILLS_INIT = '"""Skills module - Add reusable agent skills here."""\n' module-attribute

TEMPLATE_TESTS_INIT = '"""Test package."""\n' module-attribute

TEMPLATE_GITIGNORE = '# Environment\n.env\n.venv/\nvenv/\n__pycache__/\n*.pyc\n*.pyo\n\n# IDE\n.vscode/\n.idea/\n*.swp\n*.swo\n\n# Testing\n.pytest_cache/\n.coverage\nhtmlcov/\n\n# Build\ndist/\nbuild/\n*.egg-info/\n\n# Logs\n*.log\n\n# OS\n.DS_Store\nThumbs.db\n' module-attribute

TEMPLATE_ENV_EXAMPLE = '# SignalWire Credentials\nSIGNALWIRE_SPACE_NAME=your-space\nSIGNALWIRE_PROJECT_ID=your-project-id\nSIGNALWIRE_TOKEN=your-api-token\n\n# Agent Server Configuration\nHOST=0.0.0.0\nPORT=5000\n\n# Agent name (used for SWML handler - keeps the same handler across restarts)\nAGENT_NAME=myagent\n\n# Basic Auth for SWML webhooks (optional)\nSWML_BASIC_AUTH_USER=signalwire\nSWML_BASIC_AUTH_PASSWORD=your-secure-password\n\n# Public URL (ngrok tunnel or production domain)\nSWML_PROXY_URL_BASE=https://your-domain.ngrok.io\n\n# Debug settings (0=off, 1=basic, 2=verbose)\nDEBUG_WEBHOOK_LEVEL=1\n' module-attribute

TEMPLATE_REQUIREMENTS = 'signalwire-agents>=1.0.10\npython-dotenv>=1.0.0\nrequests>=2.28.0\npytest>=7.0.0\n' module-attribute

AWS_REQUIREMENTS_TEMPLATE = 'signalwire-agents>=1.0.10\nh11>=0.13,<0.15\nfastapi\nmangum\nuvicorn\n' module-attribute

AWS_HANDLER_TEMPLATE = '#!/usr/bin/env python3\n"""AWS Lambda handler for {agent_name} agent.\n\nThis demonstrates deploying a SignalWire AI Agent to AWS Lambda\nwith SWAIG functions and SWML output.\n\nEnvironment variables:\n SWML_BASIC_AUTH_USER: Basic auth username (optional)\n SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)\n"""\n\nimport os\nfrom signalwire import AgentBase, FunctionResult\n\n\nclass {agent_class}(AgentBase):\n """{agent_name} agent for AWS Lambda deployment."""\n\n def __init__(self):\n super().__init__(name="{agent_name_slug}")\n\n self._configure_prompts()\n self.add_language("English", "en-US", "rime.spore")\n self._setup_functions()\n\n def _configure_prompts(self):\n self.prompt_add_section(\n "Role",\n "You are a helpful AI assistant deployed on AWS Lambda."\n )\n\n self.prompt_add_section(\n "Guidelines",\n bullets=[\n "Be professional and courteous",\n "Ask clarifying questions when needed",\n "Keep responses concise and helpful"\n ]\n )\n\n def _setup_functions(self):\n @self.tool(\n description="Get information about a topic",\n parameters={{\n "type": "object",\n "properties": {{\n "topic": {{\n "type": "string",\n "description": "The topic to get information about"\n }}\n }},\n "required": ["topic"]\n }}\n )\n def get_info(args, raw_data):\n topic = args.get("topic", "")\n return FunctionResult(\n f"Information about {{topic}}: This is a placeholder response."\n )\n\n @self.tool(description="Get AWS Lambda deployment information")\n def get_platform_info(args, raw_data):\n region = os.getenv("AWS_REGION", "unknown")\n function_name = os.getenv("AWS_LAMBDA_FUNCTION_NAME", "unknown")\n memory = os.getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "unknown")\n runtime = os.getenv("AWS_EXECUTION_ENV", "unknown")\n\n return FunctionResult(\n f"Running on AWS Lambda. "\n f"Function: {{function_name}}, Region: {{region}}, "\n f"Memory: {{memory}}MB, Runtime: {{runtime}}."\n )\n\n\n# Create agent instance outside handler for warm starts\nagent = {agent_class}()\n\n\ndef lambda_handler(event, context):\n """AWS Lambda entry point.\n\n Args:\n event: Lambda event (API Gateway request)\n context: Lambda context with runtime info\n\n Returns:\n API Gateway response dict\n """\n return agent.run(event, context)\n' module-attribute

AWS_DEPLOY_TEMPLATE = '#!/bin/bash\n# AWS Lambda deployment script for {agent_name} agent\n#\n# Prerequisites:\n# - AWS CLI configured with appropriate credentials\n# - Docker installed and running (for building Lambda-compatible packages)\n#\n# Usage:\n# ./deploy.sh # Deploy with defaults\n# ./deploy.sh my-function # Deploy with custom function name\n# ./deploy.sh my-function us-west-2 # Custom function and region\n\nset -e\n\n# Configuration\nFUNCTION_NAME="${{1:-{function_name}}}"\nREGION="${{2:-{region}}}"\nRUNTIME="python3.11"\nHANDLER="handler.lambda_handler"\nMEMORY_SIZE=512\nTIMEOUT=30\nROLE_NAME="${{FUNCTION_NAME}}-role"\n\n# Default credentials (change these or set via environment)\nAUTH_USER="${{SWML_BASIC_AUTH_USER:-{auth_user}}}"\nAUTH_PASS="${{SWML_BASIC_AUTH_PASSWORD:-{auth_password}}}"\n\necho "=== {agent_name} - AWS Lambda Deployment ==="\necho "Function: $FUNCTION_NAME"\necho "Region: $REGION"\necho ""\n\n# Check for Docker\nif ! command -v docker &> /dev/null; then\n echo "ERROR: Docker is required but not installed."\n echo "Please install Docker: https://docs.docker.com/get-docker/"\n exit 1\nfi\n\nif ! docker info &> /dev/null; then\n echo "ERROR: Docker is not running. Please start Docker."\n exit 1\nfi\n\n# Get AWS account ID\nACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)\nROLE_ARN="arn:aws:iam::${{ACCOUNT_ID}}:role/${{ROLE_NAME}}"\n\n# Step 1: Create IAM role if it doesn\'t exist\necho "Step 1: Setting up IAM role..."\n\nTRUST_POLICY=\'{{\n "Version": "2012-10-17",\n "Statement": [\n {{\n "Effect": "Allow",\n "Principal": {{\n "Service": "lambda.amazonaws.com"\n }},\n "Action": "sts:AssumeRole"\n }}\n ]\n}}\'\n\nif ! aws iam get-role --role-name "$ROLE_NAME" --region "$REGION" 2>/dev/null; then\n echo "Creating IAM role: $ROLE_NAME"\n aws iam create-role \\\n --role-name "$ROLE_NAME" \\\n --assume-role-policy-document "$TRUST_POLICY" \\\n --region "$REGION"\n\n # Attach basic Lambda execution policy\n aws iam attach-role-policy \\\n --role-name "$ROLE_NAME" \\\n --policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" \\\n --region "$REGION"\n\n echo "Waiting for role to propagate..."\n sleep 10\nelse\n echo "IAM role already exists: $ROLE_NAME"\nfi\n\n# Step 2: Package the function using Docker\necho ""\necho "Step 2: Packaging function with Docker (linux/amd64)..."\n\nSCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"\nBUILD_DIR=$(mktemp -d)\nPACKAGE_DIR="$BUILD_DIR/package"\nZIP_FILE="$BUILD_DIR/function.zip"\n\nmkdir -p "$PACKAGE_DIR"\n\n# Build dependencies using Lambda Python image for correct architecture\necho "Installing dependencies via Docker..."\ndocker run --rm \\\n --platform linux/amd64 \\\n --entrypoint "" \\\n -v "$SCRIPT_DIR:/var/task:ro" \\\n -v "$PACKAGE_DIR:/var/output" \\\n -w /var/task \\\n public.ecr.aws/lambda/python:3.11 \\\n bash -c "pip install -r requirements.txt -t /var/output --quiet && cp handler.py /var/output/"\n\n# Create zip\necho "Creating deployment package..."\ncd "$PACKAGE_DIR"\nzip -r "$ZIP_FILE" . -q\ncd - > /dev/null\n\nPACKAGE_SIZE=$(du -h "$ZIP_FILE" | cut -f1)\necho "Package size: $PACKAGE_SIZE"\n\n# Step 3: Create or update Lambda function\necho ""\necho "Step 3: Deploying Lambda function..."\n\nif aws lambda get-function --function-name "$FUNCTION_NAME" --region "$REGION" 2>/dev/null; then\n echo "Updating existing function..."\n aws lambda update-function-code \\\n --function-name "$FUNCTION_NAME" \\\n --zip-file "fileb://$ZIP_FILE" \\\n --region "$REGION" \\\n --cli-read-timeout 300 \\\n --output text --query \'FunctionArn\'\nelse\n echo "Creating new function..."\n aws lambda create-function \\\n --function-name "$FUNCTION_NAME" \\\n --runtime "$RUNTIME" \\\n --role "$ROLE_ARN" \\\n --handler "$HANDLER" \\\n --zip-file "fileb://$ZIP_FILE" \\\n --memory-size "$MEMORY_SIZE" \\\n --timeout "$TIMEOUT" \\\n --region "$REGION" \\\n --cli-read-timeout 300 \\\n --environment "Variables={{SWML_BASIC_AUTH_USER=$AUTH_USER,SWML_BASIC_AUTH_PASSWORD=$AUTH_PASS}}" \\\n --output text --query \'FunctionArn\'\nfi\n\n# Wait for function to be active\necho "Waiting for function to be active..."\naws lambda wait function-active --function-name "$FUNCTION_NAME" --region "$REGION"\n\n# Step 4: Create or get API Gateway\necho ""\necho "Step 4: Setting up API Gateway..."\n\nAPI_NAME="${{FUNCTION_NAME}}-api"\n\n# Check if API exists\nAPI_ID=$(aws apigatewayv2 get-apis --region "$REGION" \\\n --query "Items[?Name==\'$API_NAME\'].ApiId" --output text)\n\nif [ -z "$API_ID" ] || [ "$API_ID" == "None" ]; then\n echo "Creating HTTP API..."\n API_ID=$(aws apigatewayv2 create-api \\\n --name "$API_NAME" \\\n --protocol-type HTTP \\\n --region "$REGION" \\\n --output text --query \'ApiId\')\nfi\n\necho "API ID: $API_ID"\n\n# Step 5: Create Lambda integration\necho ""\necho "Step 5: Creating Lambda integration..."\n\nLAMBDA_ARN="arn:aws:lambda:${{REGION}}:${{ACCOUNT_ID}}:function:${{FUNCTION_NAME}}"\n\n# Check for existing integration\nINTEGRATION_ID=$(aws apigatewayv2 get-integrations \\\n --api-id "$API_ID" \\\n --region "$REGION" \\\n --query "Items[?IntegrationUri==\'${{LAMBDA_ARN}}\'].IntegrationId" \\\n --output text 2>/dev/null || echo "")\n\nif [ -z "$INTEGRATION_ID" ] || [ "$INTEGRATION_ID" == "None" ]; then\n echo "Creating integration..."\n INTEGRATION_ID=$(aws apigatewayv2 create-integration \\\n --api-id "$API_ID" \\\n --integration-type AWS_PROXY \\\n --integration-uri "$LAMBDA_ARN" \\\n --payload-format-version "2.0" \\\n --region "$REGION" \\\n --output text --query \'IntegrationId\')\nfi\n\necho "Integration ID: $INTEGRATION_ID"\n\n# Step 6: Create routes\necho ""\necho "Step 6: Creating routes..."\n\ncreate_route() {{\n local route_key="$1"\n local existing=$(aws apigatewayv2 get-routes \\\n --api-id "$API_ID" \\\n --region "$REGION" \\\n --query "Items[?RouteKey==\'$route_key\'].RouteId" \\\n --output text 2>/dev/null || echo "")\n\n if [ -z "$existing" ] || [ "$existing" == "None" ]; then\n echo "Creating route: $route_key"\n aws apigatewayv2 create-route \\\n --api-id "$API_ID" \\\n --route-key "$route_key" \\\n --target "integrations/$INTEGRATION_ID" \\\n --region "$REGION" \\\n --output text --query \'RouteId\'\n else\n echo "Route exists: $route_key"\n fi\n}}\n\n# Create routes for SWML and SWAIG\ncreate_route "GET /"\ncreate_route "POST /"\ncreate_route "POST /swaig"\ncreate_route "ANY /{{proxy+}}"\n\n# Step 7: Create/update stage\necho ""\necho "Step 7: Deploying stage..."\n\nSTAGE_NAME="\\$default"\n\nif ! aws apigatewayv2 get-stage --api-id "$API_ID" --stage-name "$STAGE_NAME" --region "$REGION" 2>/dev/null; then\n aws apigatewayv2 create-stage \\\n --api-id "$API_ID" \\\n --stage-name "$STAGE_NAME" \\\n --auto-deploy \\\n --region "$REGION" > /dev/null\nfi\n\n# Step 8: Add Lambda permission for API Gateway\necho ""\necho "Step 8: Configuring permissions..."\n\nSTATEMENT_ID="${{API_NAME}}-invoke"\n\n# Remove existing permission if it exists (ignore errors)\naws lambda remove-permission \\\n --function-name "$FUNCTION_NAME" \\\n --statement-id "$STATEMENT_ID" \\\n --region "$REGION" 2>/dev/null || true\n\n# Add permission\naws lambda add-permission \\\n --function-name "$FUNCTION_NAME" \\\n --statement-id "$STATEMENT_ID" \\\n --action lambda:InvokeFunction \\\n --principal apigateway.amazonaws.com \\\n --source-arn "arn:aws:execute-api:${{REGION}}:${{ACCOUNT_ID}}:${{API_ID}}/*" \\\n --region "$REGION" > /dev/null\n\n# Get the endpoint URL\nENDPOINT="https://${{API_ID}}.execute-api.${{REGION}}.amazonaws.com"\n\n# Cleanup\nrm -rf "$BUILD_DIR"\n\necho ""\necho "=== Deployment Complete ==="\necho ""\necho "Endpoint URL: $ENDPOINT"\necho ""\necho "Authentication:"\necho " Username: $AUTH_USER"\necho " Password: $AUTH_PASS"\necho ""\necho "Test SWML output:"\necho " curl -u $AUTH_USER:$AUTH_PASS $ENDPOINT/"\necho ""\necho "Test SWAIG function:"\necho " curl -u $AUTH_USER:$AUTH_PASS -X POST $ENDPOINT/swaig \\\\"\necho " -H \'Content-Type: application/json\' \\\\"\necho " -d \'{{\\"function\\": \\"get_info\\", \\"argument\\": {{\\"parsed\\": [{{\\"topic\\": \\"test\\"}}]}}}}\'"\necho ""\necho "Configure SignalWire:"\necho " Set your phone number\'s SWML URL to: https://$AUTH_USER:$AUTH_PASS@${{API_ID}}.execute-api.${{REGION}}.amazonaws.com/"\necho ""\n' module-attribute

GCP_REQUIREMENTS_TEMPLATE = 'signalwire-agents>=1.0.10\nfunctions-framework>=3.0.0\n' module-attribute

GCP_MAIN_TEMPLATE = '#!/usr/bin/env python3\n"""Google Cloud Functions handler for {agent_name} agent.\n\nThis demonstrates deploying a SignalWire AI Agent to Google Cloud Functions\nwith SWAIG functions and SWML output.\n\nEnvironment variables:\n SWML_BASIC_AUTH_USER: Basic auth username (optional)\n SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)\n"""\n\nimport os\nfrom signalwire import AgentBase, FunctionResult\n\n\nclass {agent_class}(AgentBase):\n """{agent_name} agent for Google Cloud Functions deployment."""\n\n def __init__(self):\n super().__init__(name="{agent_name_slug}")\n\n self._configure_prompts()\n self.add_language("English", "en-US", "rime.spore")\n self._setup_functions()\n\n def _configure_prompts(self):\n self.prompt_add_section(\n "Role",\n "You are a helpful AI assistant deployed on Google Cloud Functions."\n )\n\n self.prompt_add_section(\n "Guidelines",\n bullets=[\n "Be professional and courteous",\n "Ask clarifying questions when needed",\n "Keep responses concise and helpful"\n ]\n )\n\n def _setup_functions(self):\n @self.tool(\n description="Get information about a topic",\n parameters={{\n "type": "object",\n "properties": {{\n "topic": {{\n "type": "string",\n "description": "The topic to get information about"\n }}\n }},\n "required": ["topic"]\n }}\n )\n def get_info(args, raw_data):\n topic = args.get("topic", "")\n return FunctionResult(\n f"Information about {{topic}}: This is a placeholder response."\n )\n\n @self.tool(description="Get Google Cloud deployment information")\n def get_platform_info(args, raw_data):\n import urllib.request\n\n # Gen 2 Cloud Functions run on Cloud Run with these env vars\n service = os.getenv("K_SERVICE", "unknown")\n revision = os.getenv("K_REVISION", "unknown")\n\n # Query metadata server for project ID\n project = os.getenv("GOOGLE_CLOUD_PROJECT", "unknown")\n if project == "unknown":\n try:\n req = urllib.request.Request(\n "http://metadata.google.internal/computeMetadata/v1/project/project-id",\n headers={{"Metadata-Flavor": "Google"}}\n )\n with urllib.request.urlopen(req, timeout=2) as resp:\n project = resp.read().decode()\n except Exception:\n pass\n\n return FunctionResult(\n f"Running on Google Cloud Functions Gen 2. "\n f"Service: {{service}}, Revision: {{revision}}, "\n f"Project: {{project}}."\n )\n\n\n# Create agent instance outside handler for warm starts\nagent = {agent_class}()\n\n\ndef main(request):\n """Google Cloud Functions entry point.\n\n Args:\n request: Flask request object\n\n Returns:\n Flask response\n """\n return agent.run(request)\n' module-attribute

GCP_DEPLOY_TEMPLATE = '#!/bin/bash\n# Google Cloud Functions deployment script for {agent_name} agent\n#\n# Prerequisites:\n# - gcloud CLI installed and authenticated\n# - A Google Cloud project with Cloud Functions API enabled\n#\n# Usage:\n# ./deploy.sh # Deploy with defaults\n# ./deploy.sh my-function # Custom function name\n# ./deploy.sh my-function us-central1 # Custom function and region\n\nset -e\n\n# Configuration\nFUNCTION_NAME="${{1:-{function_name}}}"\nREGION="${{2:-{region}}}"\nRUNTIME="python311"\nENTRY_POINT="main"\nMEMORY="512MB"\nTIMEOUT="60s"\nMIN_INSTANCES=0\nMAX_INSTANCES=10\n\n# Directory containing this script\nSCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"\n\necho "=== {agent_name} - Google Cloud Functions Deployment ==="\necho "Function: $FUNCTION_NAME"\necho "Region: $REGION"\necho ""\n\n# Get current project\nPROJECT=$(gcloud config get-value project 2>/dev/null)\nif [ -z "$PROJECT" ]; then\n echo "Error: No project set. Run: gcloud config set project <project-id>"\n exit 1\nfi\necho "Project: $PROJECT"\necho ""\n\n# Step 1: Enable required APIs\necho "Step 1: Enabling required APIs..."\ngcloud services enable cloudfunctions.googleapis.com --quiet\ngcloud services enable cloudbuild.googleapis.com --quiet\ngcloud services enable artifactregistry.googleapis.com --quiet\n\n# Step 2: Create deployment package\necho ""\necho "Step 2: Creating deployment package..."\n\n# Create a temporary deployment directory\nDEPLOY_DIR=$(mktemp -d)\ntrap "rm -rf $DEPLOY_DIR" EXIT\n\n# Copy the main files\ncp "$SCRIPT_DIR/main.py" "$DEPLOY_DIR/"\ncp "$SCRIPT_DIR/requirements.txt" "$DEPLOY_DIR/"\n\necho "Deployment package contents:"\nls -la "$DEPLOY_DIR/"\n\n# Step 3: Deploy function\necho ""\necho "Step 3: Deploying Cloud Function..."\n\n# Check if function exists (Gen 2 vs Gen 1)\nEXISTING_GEN2=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --gen2 2>/dev/null && echo "yes" || echo "no")\n\nif [ "$EXISTING_GEN2" == "yes" ]; then\n echo "Updating existing Gen 2 function..."\nelse\n echo "Creating new Gen 2 function..."\nfi\n\ngcloud functions deploy "$FUNCTION_NAME" \\\n --gen2 \\\n --region="$REGION" \\\n --runtime="$RUNTIME" \\\n --source="$DEPLOY_DIR" \\\n --entry-point="$ENTRY_POINT" \\\n --trigger-http \\\n --allow-unauthenticated \\\n --memory="$MEMORY" \\\n --timeout="$TIMEOUT" \\\n --min-instances="$MIN_INSTANCES" \\\n --max-instances="$MAX_INSTANCES" \\\n --quiet\n\n# Step 4: Get the endpoint URL\necho ""\necho "Step 4: Getting endpoint URL..."\n\nENDPOINT=$(gcloud functions describe "$FUNCTION_NAME" \\\n --region="$REGION" \\\n --gen2 \\\n --format="value(serviceConfig.uri)")\n\necho ""\necho "=== Deployment Complete ==="\necho ""\necho "Endpoint URL: $ENDPOINT"\necho ""\necho "Test SWML output:"\necho " curl $ENDPOINT"\necho ""\necho "Test SWAIG function:"\necho " curl -X POST $ENDPOINT/swaig \\\\"\necho " -H \'Content-Type: application/json\' \\\\"\necho " -d \'{{\\"function\\": \\"get_info\\", \\"argument\\": {{\\"parsed\\": [{{\\"topic\\": \\"test\\"}}]}}}}\'"\necho ""\necho "Configure SignalWire:"\necho " Set your phone number\'s SWML URL to: $ENDPOINT"\necho ""\necho "To set environment variables (optional):"\necho " gcloud functions deploy $FUNCTION_NAME \\\\"\necho " --region=$REGION \\\\"\necho " --gen2 \\\\"\necho " --update-env-vars SWML_BASIC_AUTH_USER=myuser,SWML_BASIC_AUTH_PASSWORD=mypass"\necho ""\n' module-attribute

AZURE_REQUIREMENTS_TEMPLATE = 'azure-functions>=1.17.0\nsignalwire-agents>=1.0.10\n' module-attribute

AZURE_INIT_TEMPLATE = '#!/usr/bin/env python3\n"""Azure Functions handler for {agent_name} agent.\n\nThis demonstrates deploying a SignalWire AI Agent to Azure Functions\nwith SWAIG functions and SWML output.\n\nEnvironment variables:\n SWML_BASIC_AUTH_USER: Basic auth username (optional)\n SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)\n"""\n\nimport os\nimport azure.functions as func\nfrom signalwire import AgentBase, FunctionResult\n\n\nclass {agent_class}(AgentBase):\n """{agent_name} agent for Azure Functions deployment."""\n\n def __init__(self):\n super().__init__(name="{agent_name_slug}")\n\n self._configure_prompts()\n self.add_language("English", "en-US", "rime.spore")\n self._setup_functions()\n\n def _configure_prompts(self):\n self.prompt_add_section(\n "Role",\n "You are a helpful AI assistant deployed on Azure Functions."\n )\n\n self.prompt_add_section(\n "Guidelines",\n bullets=[\n "Be professional and courteous",\n "Ask clarifying questions when needed",\n "Keep responses concise and helpful"\n ]\n )\n\n def _setup_functions(self):\n @self.tool(\n description="Get information about a topic",\n parameters={{\n "type": "object",\n "properties": {{\n "topic": {{\n "type": "string",\n "description": "The topic to get information about"\n }}\n }},\n "required": ["topic"]\n }}\n )\n def get_info(args, raw_data):\n topic = args.get("topic", "")\n return FunctionResult(\n f"Information about {{topic}}: This is a placeholder response."\n )\n\n @self.tool(description="Get Azure Functions deployment information")\n def get_platform_info(args, raw_data):\n function_name = os.getenv("WEBSITE_SITE_NAME", "unknown")\n region = os.getenv("REGION_NAME", "unknown")\n runtime = os.getenv("FUNCTIONS_WORKER_RUNTIME", "unknown")\n version = os.getenv("FUNCTIONS_EXTENSION_VERSION", "unknown")\n\n return FunctionResult(\n f"Running on Azure Functions. "\n f"App: {{function_name}}, Region: {{region}}, "\n f"Runtime: {{runtime}}, Version: {{version}}."\n )\n\n\n# Create agent instance outside handler for warm starts\nagent = {agent_class}()\n\n\ndef main(req: func.HttpRequest) -> func.HttpResponse:\n """Azure Functions entry point.\n\n Args:\n req: Azure Functions HTTP request object\n\n Returns:\n Azure Functions HTTP response\n """\n return agent.run(req)\n' module-attribute

AZURE_FUNCTION_JSON_TEMPLATE = '{{\n "scriptFile": "__init__.py",\n "bindings": [\n {{\n "authLevel": "anonymous",\n "type": "httpTrigger",\n "direction": "in",\n "name": "req",\n "methods": ["get", "post"],\n "route": "{{*path}}"\n }},\n {{\n "type": "http",\n "direction": "out",\n "name": "$return"\n }}\n ]\n}}\n' module-attribute

AZURE_HOST_JSON_TEMPLATE = '{{\n "version": "2.0",\n "logging": {{\n "applicationInsights": {{\n "samplingSettings": {{\n "isEnabled": true,\n "excludedTypes": "Request"\n }}\n }}\n }},\n "extensionBundle": {{\n "id": "Microsoft.Azure.Functions.ExtensionBundle",\n "version": "[4.*, 5.0.0)"\n }},\n "extensions": {{\n "http": {{\n "routePrefix": ""\n }}\n }}\n}}\n' module-attribute

AZURE_LOCAL_SETTINGS_TEMPLATE = '{{\n "IsEncrypted": false,\n "Values": {{\n "FUNCTIONS_WORKER_RUNTIME": "python",\n "AzureWebJobsStorage": ""\n }}\n}}\n' module-attribute

AZURE_DEPLOY_TEMPLATE = '#!/bin/bash\n# Azure Functions deployment script for {agent_name} agent\n#\n# Prerequisites:\n# - Azure CLI installed and authenticated (az login)\n# - Docker installed and running (for building correct architecture)\n#\n# Usage:\n# ./deploy.sh # Deploy with defaults\n# ./deploy.sh my-app # Custom app name\n# ./deploy.sh my-app eastus my-rg # Custom app, region, and resource group\n\nset -e\n\n# Configuration\nAPP_NAME="${{1:-{function_name}}}"\nLOCATION="${{2:-{region}}}"\nRESOURCE_GROUP="${{3:-{resource_group}}}"\nSTORAGE_ACCOUNT="${{APP_NAME//-/}}storage" # Remove hyphens for storage account\nRUNTIME="python"\nRUNTIME_VERSION="3.11"\nFUNCTIONS_VERSION="4"\n\n# Truncate storage account name to 24 chars (Azure limit)\nSTORAGE_ACCOUNT="${{STORAGE_ACCOUNT:0:24}}"\n\nSCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"\n\necho "=== {agent_name} - Azure Functions Deployment ==="\necho "App Name: $APP_NAME"\necho "Location: $LOCATION"\necho "Resource Group: $RESOURCE_GROUP"\necho "Storage Account: $STORAGE_ACCOUNT"\necho ""\n\n# Check for Docker\nif ! command -v docker &> /dev/null; then\n echo "ERROR: Docker is required but not installed."\n echo "Please install Docker: https://docs.docker.com/get-docker/"\n exit 1\nfi\n\nif ! docker info &> /dev/null; then\n echo "ERROR: Docker is not running. Please start Docker."\n exit 1\nfi\n\n# Step 1: Login check\necho "Step 1: Checking Azure login..."\nif ! az account show &>/dev/null; then\n echo "Not logged in. Running: az login"\n az login\nfi\n\nSUBSCRIPTION=$(az account show --query name -o tsv)\necho "Subscription: $SUBSCRIPTION"\necho ""\n\n# Step 2: Create resource group\necho "Step 2: Creating resource group..."\nif ! az group show --name "$RESOURCE_GROUP" &>/dev/null; then\n az group create \\\n --name "$RESOURCE_GROUP" \\\n --location "$LOCATION" \\\n --output none\n echo "Created resource group: $RESOURCE_GROUP"\nelse\n echo "Resource group exists: $RESOURCE_GROUP"\nfi\n\n# Step 3: Create storage account\necho ""\necho "Step 3: Creating storage account..."\nif ! az storage account show --name "$STORAGE_ACCOUNT" --resource-group "$RESOURCE_GROUP" &>/dev/null; then\n az storage account create \\\n --name "$STORAGE_ACCOUNT" \\\n --resource-group "$RESOURCE_GROUP" \\\n --location "$LOCATION" \\\n --sku Standard_LRS \\\n --output none\n echo "Created storage account: $STORAGE_ACCOUNT"\n echo "Waiting for storage account to propagate..."\n sleep 10\nelse\n echo "Storage account exists: $STORAGE_ACCOUNT"\nfi\n\n# Step 4: Create Function App\necho ""\necho "Step 4: Creating Function App..."\nif ! az functionapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then\n az functionapp create \\\n --name "$APP_NAME" \\\n --resource-group "$RESOURCE_GROUP" \\\n --storage-account "$STORAGE_ACCOUNT" \\\n --consumption-plan-location "$LOCATION" \\\n --runtime "$RUNTIME" \\\n --runtime-version "$RUNTIME_VERSION" \\\n --functions-version "$FUNCTIONS_VERSION" \\\n --os-type Linux \\\n --output none\n echo "Created Function App: $APP_NAME"\n\n # Wait for app to be ready\n echo "Waiting for Function App to be ready..."\n sleep 30\nelse\n echo "Function App exists: $APP_NAME"\nfi\n\n# Step 5: Build and deploy the function using Docker\necho ""\necho "Step 5: Building function with Docker (linux/amd64)..."\n\nDEPLOY_DIR=$(mktemp -d)\n\n# Copy function files\ncp -r "$SCRIPT_DIR/function_app" "$DEPLOY_DIR/"\ncp "$SCRIPT_DIR/host.json" "$DEPLOY_DIR/"\ncp "$SCRIPT_DIR/requirements.txt" "$DEPLOY_DIR/"\ncp "$SCRIPT_DIR/local.settings.json" "$DEPLOY_DIR/" 2>/dev/null || true\n\n# Build dependencies using Docker for correct architecture\necho "Installing dependencies via Docker..."\ndocker run --rm \\\n --platform linux/amd64 \\\n --entrypoint "" \\\n -v "$DEPLOY_DIR:/var/task" \\\n -w /var/task \\\n mcr.microsoft.com/azure-functions/python:4-python3.11 \\\n bash -c "pip install -r requirements.txt -t .python_packages/lib/site-packages --quiet"\n\n# Create zip for deployment\necho "Creating deployment package..."\nZIP_FILE="$DEPLOY_DIR/deploy.zip"\ncd "$DEPLOY_DIR"\nzip -r "$ZIP_FILE" . -x "*.pyc" -q\ncd - > /dev/null\n\nPACKAGE_SIZE=$(du -h "$ZIP_FILE" | cut -f1)\necho "Package size: $PACKAGE_SIZE"\n\n# Deploy using zip deployment\necho ""\necho "Step 6: Deploying to Azure..."\naz functionapp deployment source config-zip \\\n --name "$APP_NAME" \\\n --resource-group "$RESOURCE_GROUP" \\\n --src "$ZIP_FILE" \\\n --output none\n\necho "Deployment complete"\n\n# Cleanup\nrm -rf "$DEPLOY_DIR"\n\n# Step 7: Get the endpoint URL\necho ""\necho "Step 7: Getting endpoint URL..."\n\nENDPOINT="https://${{APP_NAME}}.azurewebsites.net"\n\n# Verify deployment\necho "Waiting for deployment to propagate..."\nsleep 10\n\necho ""\necho "=== Deployment Complete ==="\necho ""\necho "Endpoint URL: $ENDPOINT/api/function_app"\necho ""\necho "Test SWML output:"\necho " curl $ENDPOINT/api/function_app"\necho ""\necho "Test SWAIG function:"\necho " curl -X POST $ENDPOINT/api/function_app/swaig \\\\"\necho " -H \'Content-Type: application/json\' \\\\"\necho " -d \'{{\\"function\\": \\"get_info\\", \\"argument\\": {{\\"parsed\\": [{{\\"topic\\": \\"test\\"}}]}}}}\'"\necho ""\necho "Configure SignalWire:"\necho " Set your phone number\'s SWML URL to: $ENDPOINT/api/function_app"\necho ""\necho "To set environment variables (optional):"\necho " az functionapp config appsettings set --name $APP_NAME --resource-group $RESOURCE_GROUP \\\\"\necho " --settings SWML_BASIC_AUTH_USER=myuser SWML_BASIC_AUTH_PASSWORD=mypass"\necho ""\n' module-attribute

Colors

RED = '\x1b[0;31m' class-attribute instance-attribute
GREEN = '\x1b[0;32m' class-attribute instance-attribute
YELLOW = '\x1b[1;33m' class-attribute instance-attribute
BLUE = '\x1b[0;34m' class-attribute instance-attribute
CYAN = '\x1b[0;36m' class-attribute instance-attribute
BOLD = '\x1b[1m' class-attribute instance-attribute
DIM = '\x1b[2m' class-attribute instance-attribute
NC = '\x1b[0m' class-attribute instance-attribute

ProjectGenerator

Generates a new SignalWire agent project.

config = config instance-attribute
project_dir = Path(config['project_dir']) instance-attribute
project_name = config['project_name'] instance-attribute
features = config['features'] instance-attribute
credentials = config.get('credentials', {}) instance-attribute
platform = config.get('platform', 'local') instance-attribute
cloud_config = config.get('cloud_config', {}) instance-attribute
__init__(config)
generate()

Generate the project. Returns True on success.

print_step(msg)

print_success(msg)

print_warning(msg)

print_error(msg)

prompt(question, default='')

Prompt user for input with optional default.

prompt_yes_no(question, default=True)

Prompt user for yes/no answer.

prompt_select(question, options, default=1)

Prompt user to select from numbered options. Returns 1-based index.

prompt_multiselect(question, options, defaults)

Prompt user to toggle multiple options. Returns list of booleans.

mask_token(token)

Mask a token showing only first 4 and last 3 characters.

get_env_credentials()

Get SignalWire credentials from environment variables.

generate_password(length=32)

Generate a secure random password.

get_agent_template(agent_type, features)

Generate the main agent template based on type and features.

get_app_template(features)

Generate the app.py template based on features.

get_test_template(has_tool)

Generate test template.

get_readme_template(project_name, features)

Generate README template.

get_web_index_template()

Generate a simple web UI template.

run_interactive()

Run interactive prompts and return configuration.

run_quick(project_name, args)

Run in quick mode with minimal prompts.

main()

Main entry point.

output

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

output_formatter

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Display agent/tools and format results

display_agent_tools(agent, verbose=False)

Display the available SWAIG functions for an agent

Parameters:

Name Type Description Default
agent AgentBase

The agent instance

required
verbose bool

Whether to show verbose details

False
format_result(result)

Format the result of a SWAIG function call for display

Parameters:

Name Type Description Default
result Any

The result from the SWAIG function

required

Returns:

Type Description
str

Formatted string representation

swml_dump

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Handle SWML document dumping

original_print = print module-attribute
setup_output_suppression()

Set up output suppression for SWML dumping

handle_dump_swml(agent, args)

Handle SWML dumping with fake post_data and mock request support

Parameters:

Name Type Description Default
agent AgentBase

The loaded agent instance

required
args Namespace

Parsed CLI arguments

required

Returns:

Type Description
int

Exit code (0 for success, 1 for error)

simulation

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

data_generation

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Generate fake SWML post_data and related helpers

generate_fake_uuid()

Generate a fake UUID for testing

generate_fake_node_id()

Generate a fake node ID for testing

generate_fake_sip_from(call_type)

Generate a fake 'from' address based on call type

generate_fake_sip_to(call_type)

Generate a fake 'to' address based on call type

adapt_for_call_type(call_data, call_type)

Adapt call data structure based on call type (sip vs webrtc)

Parameters:

Name Type Description Default
call_data dict[str, Any]

Base call data structure

required
call_type str

"sip" or "webrtc"

required

Returns:

Type Description
dict[str, Any]

Adapted call data with appropriate addresses and metadata

generate_fake_swml_post_data(call_type='webrtc', call_direction='inbound', call_state='created')

Generate fake SWML post_data that matches real SignalWire structure

Parameters:

Name Type Description Default
call_type str

"sip" or "webrtc" (default: webrtc)

'webrtc'
call_direction str

"inbound" or "outbound" (default: inbound)

'inbound'
call_state str

Call state (default: created)

'created'

Returns:

Type Description
dict[str, Any]

Fake post_data dict with call, vars, and envs structure

generate_comprehensive_post_data(function_name, args, custom_data=None)

Generate comprehensive post_data that matches what SignalWire would send

Parameters:

Name Type Description Default
function_name str

Name of the SWAIG function being called

required
args dict[str, Any]

Function arguments

required
custom_data dict[str, Any] | None

Optional custom data to override defaults

None

Returns:

Type Description
dict[str, Any]

Complete post_data dict with all possible keys

generate_minimal_post_data(function_name, args)

Generate minimal post_data with only essential keys

Parameters:

Name Type Description Default
function_name str

Name of the SWAIG function being called

required
args dict[str, Any]

Function arguments

required

Returns:

Type Description
dict[str, Any]

Minimal post_data dict

data_overrides

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Handle CLI overrides and mapping to nested data

set_nested_value(data, path, value)

Set a nested value using dot notation path

Parameters:

Name Type Description Default
data dict[str, Any]

Dictionary to modify

required
path str

Dot-notation path (e.g., "call.call_id" or "vars.userVariables.custom")

required
value Any

Value to set

required
parse_value(value_str)

Parse a string value into appropriate Python type

Parameters:

Name Type Description Default
value_str str

String representation of value

required

Returns:

Type Description
Any

Parsed value (str, int, float, bool, None, or JSON object)

apply_overrides(data, overrides, json_overrides)

Apply override values to data using dot notation paths

Parameters:

Name Type Description Default
data dict[str, Any]

Data dictionary to modify

required
overrides list[str]

List of "path=value" strings

required
json_overrides list[str]

List of "path=json_value" strings

required

Returns:

Type Description
dict[str, Any]

Modified data dictionary

apply_convenience_mappings(data, args)

Apply convenience CLI arguments to data structure

Parameters:

Name Type Description Default
data dict[str, Any]

Data dictionary to modify

required
args Namespace

Parsed CLI arguments

required

Returns:

Type Description
dict[str, Any]

Modified data dictionary

mock_env

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Mock environment and serverless simulation functionality

MockQueryParams

Mock FastAPI QueryParams (simple dict-like)

__init__(params=None)
get(key, default=None)
__getitem__(key)
__contains__(key)
items()
keys()
values()
MockHeaders

Mock FastAPI Headers (case-insensitive dict-like)

__init__(headers=None)
get(key, default=None)
__getitem__(key)
__contains__(key)
items()
keys()
values()
MockURL

Mock FastAPI URL object

query = query_string instance-attribute
path = url instance-attribute
netloc = rest.split('/', 1)[0] instance-attribute
scheme = 'http' instance-attribute
__init__(url='http://localhost:8080/swml')
__str__()
MockRequest

Mock FastAPI Request object for dynamic agent testing

method = method instance-attribute
url = MockURL(url) instance-attribute
headers = MockHeaders(headers) instance-attribute
query_params = MockQueryParams(query_params) instance-attribute
state = type('State', (), {})() instance-attribute
__init__(method='POST', url='http://localhost:8080/swml', headers=None, query_params=None, json_body=None)
json() async

Return the JSON body

body() async

Return the raw body bytes

client()

Mock client property

ServerlessSimulator

Manages serverless environment simulation for different platforms

PLATFORM_PRESETS = {'lambda': {'AWS_LAMBDA_FUNCTION_NAME': 'test-agent-function', 'AWS_LAMBDA_FUNCTION_URL': 'https://abc123.lambda-url.us-east-1.on.aws/', 'AWS_REGION': 'us-east-1', '_HANDLER': 'lambda_function.lambda_handler'}, 'cgi': {'GATEWAY_INTERFACE': 'CGI/1.1', 'HTTP_HOST': 'example.com', 'SCRIPT_NAME': '/cgi-bin/agent.cgi', 'HTTPS': 'on', 'SERVER_NAME': 'example.com'}, 'cloud_function': {'GOOGLE_CLOUD_PROJECT': 'test-project', 'FUNCTION_URL': 'https://my-function-abc123.cloudfunctions.net', 'GOOGLE_CLOUD_REGION': 'us-central1', 'K_SERVICE': 'agent'}, 'azure_function': {'AZURE_FUNCTIONS_ENVIRONMENT': 'Development', 'FUNCTIONS_WORKER_RUNTIME': 'python', 'WEBSITE_SITE_NAME': 'my-function-app'}} class-attribute
platform = platform instance-attribute
original_env = dict(os.environ) instance-attribute
preset_env = self.PLATFORM_PRESETS.get(platform, {}).copy() instance-attribute
overrides = overrides or {} instance-attribute
active = False instance-attribute
__init__(platform, overrides=None)
activate(verbose=False)

Apply serverless environment simulation

deactivate(verbose=False)

Restore original environment

add_override(key, value)

Add an environment variable override

get_current_env()

Get the current environment that would be applied

create_mock_request(method='POST', url='http://localhost:8080/swml', headers=None, query_params=None, body=None)

Factory function to create a mock FastAPI Request object

load_env_file(env_file_path)

Load environment variables from a file

swaig_test_wrapper

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Wrapper script for swaig-test that sets environment variables before importing any modules. This allows proper control of logging before the logging system is initialized.

main()

Main entry point for the swaig-test command

test_swaig

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

SWAIG Function CLI Testing Tool

This tool loads an agent application and calls SWAIG functions with comprehensive simulation of the SignalWire environment. It supports both webhook and DataMap functions.

print_help_platforms()

Print detailed help for serverless platform options

print_help_examples()

Print comprehensive usage examples

main()

Main entry point for the CLI tool

console_entry_point()

Console script entry point for pip installation

types

Copyright (c) 2025 SignalWire

This file is part of the SignalWire SDK.

Licensed under the MIT License. See LICENSE file in the project root for full license information.

Type definitions for the CLI tools

CallData

Bases: TypedDict

Call data structure for SWML post_data

id instance-attribute
node_id instance-attribute
state instance-attribute
type instance-attribute
direction instance-attribute
project_id instance-attribute
space_id instance-attribute
from_number instance-attribute
to_number instance-attribute
from_ instance-attribute
to instance-attribute
from_name instance-attribute
headers instance-attribute
timeout instance-attribute
tag instance-attribute

VarsData

Bases: TypedDict

Variables data structure for SWML post_data

userVariables instance-attribute
environment instance-attribute
call_data instance-attribute

PostData

Bases: TypedDict

Complete post_data structure for SWML requests

call_id instance-attribute
call instance-attribute
vars instance-attribute
params instance-attribute
project_id instance-attribute
space_id instance-attribute
meta_data instance-attribute
post_prompt_data instance-attribute
error instance-attribute
protocol_error instance-attribute
parse_error instance-attribute

DataMapConfig

Bases: TypedDict

DataMap function configuration

function instance-attribute
data_map instance-attribute
description instance-attribute
parameters instance-attribute

AgentInfo

Bases: TypedDict

Information about a discovered agent

class_name instance-attribute
file_path instance-attribute
is_instance instance-attribute
instance_name instance-attribute

FunctionInfo

Bases: TypedDict

Information about a SWAIG function

name instance-attribute
description instance-attribute
parameters instance-attribute
type instance-attribute
webhook_url instance-attribute