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
build_search
¶
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
¶
MockHeaders
¶
MockURL
¶
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.
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
¶
PostData
¶
Bases: TypedDict
Complete post_data structure for SWML requests