signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2636 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SignalWire Agent Project Generator
4
+
5
+ Interactive CLI tool to create new SignalWire agent projects with customizable features.
6
+
7
+ Usage:
8
+ sw-agent-init # Interactive mode
9
+ sw-agent-init myagent # Quick mode with project name
10
+ sw-agent-init myagent --type full --no-venv
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ import secrets
16
+ import subprocess
17
+ from pathlib import Path
18
+ from typing import Optional, Dict, List, Any
19
+
20
+
21
+ # Cloud platform options
22
+ CLOUD_PLATFORMS = {
23
+ "local": "Local Agent (FastAPI/uvicorn server)",
24
+ "aws": "AWS Lambda Function",
25
+ "gcp": "Google Cloud Function",
26
+ "azure": "Azure Function",
27
+ }
28
+
29
+ # Default regions per platform
30
+ DEFAULT_REGIONS = {
31
+ "aws": "us-east-1",
32
+ "gcp": "us-central1",
33
+ "azure": "eastus",
34
+ }
35
+
36
+
37
+ # ANSI colors
38
+ class Colors:
39
+ RED = '\033[0;31m'
40
+ GREEN = '\033[0;32m'
41
+ YELLOW = '\033[1;33m'
42
+ BLUE = '\033[0;34m'
43
+ CYAN = '\033[0;36m'
44
+ BOLD = '\033[1m'
45
+ DIM = '\033[2m'
46
+ NC = '\033[0m' # No Color
47
+
48
+
49
+ def print_step(msg: str):
50
+ print(f"{Colors.BLUE}==>{Colors.NC} {msg}")
51
+
52
+
53
+ def print_success(msg: str):
54
+ print(f"{Colors.GREEN}✓{Colors.NC} {msg}")
55
+
56
+
57
+ def print_warning(msg: str):
58
+ print(f"{Colors.YELLOW}!{Colors.NC} {msg}")
59
+
60
+
61
+ def print_error(msg: str):
62
+ print(f"{Colors.RED}✗{Colors.NC} {msg}")
63
+
64
+
65
+ def prompt(question: str, default: str = "") -> str:
66
+ """Prompt user for input with optional default."""
67
+ if default:
68
+ result = input(f"{question} [{default}]: ").strip()
69
+ return result if result else default
70
+ else:
71
+ return input(f"{question}: ").strip()
72
+
73
+
74
+ def prompt_yes_no(question: str, default: bool = True) -> bool:
75
+ """Prompt user for yes/no answer."""
76
+ hint = "Y/n" if default else "y/N"
77
+ result = input(f"{question} [{hint}]: ").strip().lower()
78
+ if not result:
79
+ return default
80
+ return result in ('y', 'yes')
81
+
82
+
83
+ def prompt_select(question: str, options: List[str], default: int = 1) -> int:
84
+ """Prompt user to select from numbered options. Returns 1-based index."""
85
+ print(f"\n{question}")
86
+ for i, opt in enumerate(options, 1):
87
+ print(f" {i}) {opt}")
88
+ while True:
89
+ result = input(f"Select [{default}]: ").strip()
90
+ if not result:
91
+ return default
92
+ try:
93
+ idx = int(result)
94
+ if 1 <= idx <= len(options):
95
+ return idx
96
+ except ValueError:
97
+ pass
98
+ print(f"Please enter a number between 1 and {len(options)}")
99
+
100
+
101
+ def prompt_multiselect(question: str, options: List[str], defaults: List[bool]) -> List[bool]:
102
+ """Prompt user to toggle multiple options. Returns list of booleans."""
103
+ selected = defaults.copy()
104
+
105
+ while True:
106
+ print(f"\n{question}")
107
+ for i, (opt, sel) in enumerate(zip(options, selected), 1):
108
+ marker = "x" if sel else " "
109
+ print(f" {i}) [{marker}] {opt}")
110
+ print(f" Enter number to toggle, or press Enter to continue")
111
+
112
+ result = input("Toggle: ").strip()
113
+ if not result:
114
+ return selected
115
+ try:
116
+ idx = int(result)
117
+ if 1 <= idx <= len(options):
118
+ selected[idx - 1] = not selected[idx - 1]
119
+ except ValueError:
120
+ pass
121
+
122
+
123
+ def mask_token(token: str) -> str:
124
+ """Mask a token showing only first 4 and last 3 characters."""
125
+ if len(token) <= 10:
126
+ return "*" * len(token)
127
+ return f"{token[:4]}...{token[-3:]}"
128
+
129
+
130
+ def get_env_credentials() -> Dict[str, str]:
131
+ """Get SignalWire credentials from environment variables."""
132
+ return {
133
+ 'space': os.environ.get('SIGNALWIRE_SPACE_NAME', ''),
134
+ 'project': os.environ.get('SIGNALWIRE_PROJECT_ID', ''),
135
+ 'token': os.environ.get('SIGNALWIRE_TOKEN', ''),
136
+ }
137
+
138
+
139
+ def generate_password(length: int = 32) -> str:
140
+ """Generate a secure random password."""
141
+ return secrets.token_urlsafe(length)[:length]
142
+
143
+
144
+ # =============================================================================
145
+ # Templates
146
+ # =============================================================================
147
+
148
+ TEMPLATE_AGENTS_INIT = '''from .main_agent import MainAgent
149
+
150
+ __all__ = ["MainAgent"]
151
+ '''
152
+
153
+ TEMPLATE_SKILLS_INIT = '''"""Skills module - Add reusable agent skills here."""
154
+ '''
155
+
156
+ TEMPLATE_TESTS_INIT = '''"""Test package."""
157
+ '''
158
+
159
+ TEMPLATE_GITIGNORE = '''# Environment
160
+ .env
161
+ .venv/
162
+ venv/
163
+ __pycache__/
164
+ *.pyc
165
+ *.pyo
166
+
167
+ # IDE
168
+ .vscode/
169
+ .idea/
170
+ *.swp
171
+ *.swo
172
+
173
+ # Testing
174
+ .pytest_cache/
175
+ .coverage
176
+ htmlcov/
177
+
178
+ # Build
179
+ dist/
180
+ build/
181
+ *.egg-info/
182
+
183
+ # Logs
184
+ *.log
185
+
186
+ # OS
187
+ .DS_Store
188
+ Thumbs.db
189
+ '''
190
+
191
+ TEMPLATE_ENV_EXAMPLE = '''# SignalWire Credentials
192
+ SIGNALWIRE_SPACE_NAME=your-space
193
+ SIGNALWIRE_PROJECT_ID=your-project-id
194
+ SIGNALWIRE_TOKEN=your-api-token
195
+
196
+ # Agent Server Configuration
197
+ HOST=0.0.0.0
198
+ PORT=5000
199
+
200
+ # Agent name (used for SWML handler - keeps the same handler across restarts)
201
+ AGENT_NAME=myagent
202
+
203
+ # Basic Auth for SWML webhooks (optional)
204
+ SWML_BASIC_AUTH_USER=signalwire
205
+ SWML_BASIC_AUTH_PASSWORD=your-secure-password
206
+
207
+ # Public URL (ngrok tunnel or production domain)
208
+ SWML_PROXY_URL_BASE=https://your-domain.ngrok.io
209
+
210
+ # Debug settings (0=off, 1=basic, 2=verbose)
211
+ DEBUG_WEBHOOK_LEVEL=1
212
+ '''
213
+
214
+ TEMPLATE_REQUIREMENTS = '''signalwire-agents>=1.0.10
215
+ python-dotenv>=1.0.0
216
+ requests>=2.28.0
217
+ pytest>=7.0.0
218
+ '''
219
+
220
+ # =============================================================================
221
+ # Cloud Function Templates
222
+ # =============================================================================
223
+
224
+ # AWS Lambda Templates
225
+ AWS_REQUIREMENTS_TEMPLATE = '''signalwire-agents>=1.0.10
226
+ h11>=0.13,<0.15
227
+ fastapi
228
+ mangum
229
+ uvicorn
230
+ '''
231
+
232
+ AWS_HANDLER_TEMPLATE = '''#!/usr/bin/env python3
233
+ """AWS Lambda handler for {agent_name} agent.
234
+
235
+ This demonstrates deploying a SignalWire AI Agent to AWS Lambda
236
+ with SWAIG functions and SWML output.
237
+
238
+ Environment variables:
239
+ SWML_BASIC_AUTH_USER: Basic auth username (optional)
240
+ SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)
241
+ """
242
+
243
+ import os
244
+ from signalwire_agents import AgentBase, SwaigFunctionResult
245
+
246
+
247
+ class {agent_class}(AgentBase):
248
+ """{agent_name} agent for AWS Lambda deployment."""
249
+
250
+ def __init__(self):
251
+ super().__init__(name="{agent_name_slug}")
252
+
253
+ self._configure_prompts()
254
+ self.add_language("English", "en-US", "rime.spore")
255
+ self._setup_functions()
256
+
257
+ def _configure_prompts(self):
258
+ self.prompt_add_section(
259
+ "Role",
260
+ "You are a helpful AI assistant deployed on AWS Lambda."
261
+ )
262
+
263
+ self.prompt_add_section(
264
+ "Guidelines",
265
+ bullets=[
266
+ "Be professional and courteous",
267
+ "Ask clarifying questions when needed",
268
+ "Keep responses concise and helpful"
269
+ ]
270
+ )
271
+
272
+ def _setup_functions(self):
273
+ @self.tool(
274
+ description="Get information about a topic",
275
+ parameters={{
276
+ "type": "object",
277
+ "properties": {{
278
+ "topic": {{
279
+ "type": "string",
280
+ "description": "The topic to get information about"
281
+ }}
282
+ }},
283
+ "required": ["topic"]
284
+ }}
285
+ )
286
+ def get_info(args, raw_data):
287
+ topic = args.get("topic", "")
288
+ return SwaigFunctionResult(
289
+ f"Information about {{topic}}: This is a placeholder response."
290
+ )
291
+
292
+ @self.tool(description="Get AWS Lambda deployment information")
293
+ def get_platform_info(args, raw_data):
294
+ region = os.getenv("AWS_REGION", "unknown")
295
+ function_name = os.getenv("AWS_LAMBDA_FUNCTION_NAME", "unknown")
296
+ memory = os.getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "unknown")
297
+ runtime = os.getenv("AWS_EXECUTION_ENV", "unknown")
298
+
299
+ return SwaigFunctionResult(
300
+ f"Running on AWS Lambda. "
301
+ f"Function: {{function_name}}, Region: {{region}}, "
302
+ f"Memory: {{memory}}MB, Runtime: {{runtime}}."
303
+ )
304
+
305
+
306
+ # Create agent instance outside handler for warm starts
307
+ agent = {agent_class}()
308
+
309
+
310
+ def lambda_handler(event, context):
311
+ """AWS Lambda entry point.
312
+
313
+ Args:
314
+ event: Lambda event (API Gateway request)
315
+ context: Lambda context with runtime info
316
+
317
+ Returns:
318
+ API Gateway response dict
319
+ """
320
+ return agent.run(event, context)
321
+ '''
322
+
323
+ AWS_DEPLOY_TEMPLATE = '''#!/bin/bash
324
+ # AWS Lambda deployment script for {agent_name} agent
325
+ #
326
+ # Prerequisites:
327
+ # - AWS CLI configured with appropriate credentials
328
+ # - Docker installed and running (for building Lambda-compatible packages)
329
+ #
330
+ # Usage:
331
+ # ./deploy.sh # Deploy with defaults
332
+ # ./deploy.sh my-function # Deploy with custom function name
333
+ # ./deploy.sh my-function us-west-2 # Custom function and region
334
+
335
+ set -e
336
+
337
+ # Configuration
338
+ FUNCTION_NAME="${{1:-{function_name}}}"
339
+ REGION="${{2:-{region}}}"
340
+ RUNTIME="python3.11"
341
+ HANDLER="handler.lambda_handler"
342
+ MEMORY_SIZE=512
343
+ TIMEOUT=30
344
+ ROLE_NAME="${{FUNCTION_NAME}}-role"
345
+
346
+ # Default credentials (change these or set via environment)
347
+ AUTH_USER="${{SWML_BASIC_AUTH_USER:-{auth_user}}}"
348
+ AUTH_PASS="${{SWML_BASIC_AUTH_PASSWORD:-{auth_password}}}"
349
+
350
+ echo "=== {agent_name} - AWS Lambda Deployment ==="
351
+ echo "Function: $FUNCTION_NAME"
352
+ echo "Region: $REGION"
353
+ echo ""
354
+
355
+ # Check for Docker
356
+ if ! command -v docker &> /dev/null; then
357
+ echo "ERROR: Docker is required but not installed."
358
+ echo "Please install Docker: https://docs.docker.com/get-docker/"
359
+ exit 1
360
+ fi
361
+
362
+ if ! docker info &> /dev/null; then
363
+ echo "ERROR: Docker is not running. Please start Docker."
364
+ exit 1
365
+ fi
366
+
367
+ # Get AWS account ID
368
+ ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
369
+ ROLE_ARN="arn:aws:iam::${{ACCOUNT_ID}}:role/${{ROLE_NAME}}"
370
+
371
+ # Step 1: Create IAM role if it doesn't exist
372
+ echo "Step 1: Setting up IAM role..."
373
+
374
+ TRUST_POLICY='{{
375
+ "Version": "2012-10-17",
376
+ "Statement": [
377
+ {{
378
+ "Effect": "Allow",
379
+ "Principal": {{
380
+ "Service": "lambda.amazonaws.com"
381
+ }},
382
+ "Action": "sts:AssumeRole"
383
+ }}
384
+ ]
385
+ }}'
386
+
387
+ if ! aws iam get-role --role-name "$ROLE_NAME" --region "$REGION" 2>/dev/null; then
388
+ echo "Creating IAM role: $ROLE_NAME"
389
+ aws iam create-role \\
390
+ --role-name "$ROLE_NAME" \\
391
+ --assume-role-policy-document "$TRUST_POLICY" \\
392
+ --region "$REGION"
393
+
394
+ # Attach basic Lambda execution policy
395
+ aws iam attach-role-policy \\
396
+ --role-name "$ROLE_NAME" \\
397
+ --policy-arn "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" \\
398
+ --region "$REGION"
399
+
400
+ echo "Waiting for role to propagate..."
401
+ sleep 10
402
+ else
403
+ echo "IAM role already exists: $ROLE_NAME"
404
+ fi
405
+
406
+ # Step 2: Package the function using Docker
407
+ echo ""
408
+ echo "Step 2: Packaging function with Docker (linux/amd64)..."
409
+
410
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
411
+ BUILD_DIR=$(mktemp -d)
412
+ PACKAGE_DIR="$BUILD_DIR/package"
413
+ ZIP_FILE="$BUILD_DIR/function.zip"
414
+
415
+ mkdir -p "$PACKAGE_DIR"
416
+
417
+ # Build dependencies using Lambda Python image for correct architecture
418
+ echo "Installing dependencies via Docker..."
419
+ docker run --rm \\
420
+ --platform linux/amd64 \\
421
+ --entrypoint "" \\
422
+ -v "$SCRIPT_DIR:/var/task:ro" \\
423
+ -v "$PACKAGE_DIR:/var/output" \\
424
+ -w /var/task \\
425
+ public.ecr.aws/lambda/python:3.11 \\
426
+ bash -c "pip install -r requirements.txt -t /var/output --quiet && cp handler.py /var/output/"
427
+
428
+ # Create zip
429
+ echo "Creating deployment package..."
430
+ cd "$PACKAGE_DIR"
431
+ zip -r "$ZIP_FILE" . -q
432
+ cd - > /dev/null
433
+
434
+ PACKAGE_SIZE=$(du -h "$ZIP_FILE" | cut -f1)
435
+ echo "Package size: $PACKAGE_SIZE"
436
+
437
+ # Step 3: Create or update Lambda function
438
+ echo ""
439
+ echo "Step 3: Deploying Lambda function..."
440
+
441
+ if aws lambda get-function --function-name "$FUNCTION_NAME" --region "$REGION" 2>/dev/null; then
442
+ echo "Updating existing function..."
443
+ aws lambda update-function-code \\
444
+ --function-name "$FUNCTION_NAME" \\
445
+ --zip-file "fileb://$ZIP_FILE" \\
446
+ --region "$REGION" \\
447
+ --cli-read-timeout 300 \\
448
+ --output text --query 'FunctionArn'
449
+ else
450
+ echo "Creating new function..."
451
+ aws lambda create-function \\
452
+ --function-name "$FUNCTION_NAME" \\
453
+ --runtime "$RUNTIME" \\
454
+ --role "$ROLE_ARN" \\
455
+ --handler "$HANDLER" \\
456
+ --zip-file "fileb://$ZIP_FILE" \\
457
+ --memory-size "$MEMORY_SIZE" \\
458
+ --timeout "$TIMEOUT" \\
459
+ --region "$REGION" \\
460
+ --cli-read-timeout 300 \\
461
+ --environment "Variables={{SWML_BASIC_AUTH_USER=$AUTH_USER,SWML_BASIC_AUTH_PASSWORD=$AUTH_PASS}}" \\
462
+ --output text --query 'FunctionArn'
463
+ fi
464
+
465
+ # Wait for function to be active
466
+ echo "Waiting for function to be active..."
467
+ aws lambda wait function-active --function-name "$FUNCTION_NAME" --region "$REGION"
468
+
469
+ # Step 4: Create or get API Gateway
470
+ echo ""
471
+ echo "Step 4: Setting up API Gateway..."
472
+
473
+ API_NAME="${{FUNCTION_NAME}}-api"
474
+
475
+ # Check if API exists
476
+ API_ID=$(aws apigatewayv2 get-apis --region "$REGION" \\
477
+ --query "Items[?Name=='$API_NAME'].ApiId" --output text)
478
+
479
+ if [ -z "$API_ID" ] || [ "$API_ID" == "None" ]; then
480
+ echo "Creating HTTP API..."
481
+ API_ID=$(aws apigatewayv2 create-api \\
482
+ --name "$API_NAME" \\
483
+ --protocol-type HTTP \\
484
+ --region "$REGION" \\
485
+ --output text --query 'ApiId')
486
+ fi
487
+
488
+ echo "API ID: $API_ID"
489
+
490
+ # Step 5: Create Lambda integration
491
+ echo ""
492
+ echo "Step 5: Creating Lambda integration..."
493
+
494
+ LAMBDA_ARN="arn:aws:lambda:${{REGION}}:${{ACCOUNT_ID}}:function:${{FUNCTION_NAME}}"
495
+
496
+ # Check for existing integration
497
+ INTEGRATION_ID=$(aws apigatewayv2 get-integrations \\
498
+ --api-id "$API_ID" \\
499
+ --region "$REGION" \\
500
+ --query "Items[?IntegrationUri=='${{LAMBDA_ARN}}'].IntegrationId" \\
501
+ --output text 2>/dev/null || echo "")
502
+
503
+ if [ -z "$INTEGRATION_ID" ] || [ "$INTEGRATION_ID" == "None" ]; then
504
+ echo "Creating integration..."
505
+ INTEGRATION_ID=$(aws apigatewayv2 create-integration \\
506
+ --api-id "$API_ID" \\
507
+ --integration-type AWS_PROXY \\
508
+ --integration-uri "$LAMBDA_ARN" \\
509
+ --payload-format-version "2.0" \\
510
+ --region "$REGION" \\
511
+ --output text --query 'IntegrationId')
512
+ fi
513
+
514
+ echo "Integration ID: $INTEGRATION_ID"
515
+
516
+ # Step 6: Create routes
517
+ echo ""
518
+ echo "Step 6: Creating routes..."
519
+
520
+ create_route() {{
521
+ local route_key="$1"
522
+ local existing=$(aws apigatewayv2 get-routes \\
523
+ --api-id "$API_ID" \\
524
+ --region "$REGION" \\
525
+ --query "Items[?RouteKey=='$route_key'].RouteId" \\
526
+ --output text 2>/dev/null || echo "")
527
+
528
+ if [ -z "$existing" ] || [ "$existing" == "None" ]; then
529
+ echo "Creating route: $route_key"
530
+ aws apigatewayv2 create-route \\
531
+ --api-id "$API_ID" \\
532
+ --route-key "$route_key" \\
533
+ --target "integrations/$INTEGRATION_ID" \\
534
+ --region "$REGION" \\
535
+ --output text --query 'RouteId'
536
+ else
537
+ echo "Route exists: $route_key"
538
+ fi
539
+ }}
540
+
541
+ # Create routes for SWML and SWAIG
542
+ create_route "GET /"
543
+ create_route "POST /"
544
+ create_route "POST /swaig"
545
+ create_route "ANY /{{proxy+}}"
546
+
547
+ # Step 7: Create/update stage
548
+ echo ""
549
+ echo "Step 7: Deploying stage..."
550
+
551
+ STAGE_NAME="\\$default"
552
+
553
+ if ! aws apigatewayv2 get-stage --api-id "$API_ID" --stage-name "$STAGE_NAME" --region "$REGION" 2>/dev/null; then
554
+ aws apigatewayv2 create-stage \\
555
+ --api-id "$API_ID" \\
556
+ --stage-name "$STAGE_NAME" \\
557
+ --auto-deploy \\
558
+ --region "$REGION" > /dev/null
559
+ fi
560
+
561
+ # Step 8: Add Lambda permission for API Gateway
562
+ echo ""
563
+ echo "Step 8: Configuring permissions..."
564
+
565
+ STATEMENT_ID="${{API_NAME}}-invoke"
566
+
567
+ # Remove existing permission if it exists (ignore errors)
568
+ aws lambda remove-permission \\
569
+ --function-name "$FUNCTION_NAME" \\
570
+ --statement-id "$STATEMENT_ID" \\
571
+ --region "$REGION" 2>/dev/null || true
572
+
573
+ # Add permission
574
+ aws lambda add-permission \\
575
+ --function-name "$FUNCTION_NAME" \\
576
+ --statement-id "$STATEMENT_ID" \\
577
+ --action lambda:InvokeFunction \\
578
+ --principal apigateway.amazonaws.com \\
579
+ --source-arn "arn:aws:execute-api:${{REGION}}:${{ACCOUNT_ID}}:${{API_ID}}/*" \\
580
+ --region "$REGION" > /dev/null
581
+
582
+ # Get the endpoint URL
583
+ ENDPOINT="https://${{API_ID}}.execute-api.${{REGION}}.amazonaws.com"
584
+
585
+ # Cleanup
586
+ rm -rf "$BUILD_DIR"
587
+
588
+ echo ""
589
+ echo "=== Deployment Complete ==="
590
+ echo ""
591
+ echo "Endpoint URL: $ENDPOINT"
592
+ echo ""
593
+ echo "Authentication:"
594
+ echo " Username: $AUTH_USER"
595
+ echo " Password: $AUTH_PASS"
596
+ echo ""
597
+ echo "Test SWML output:"
598
+ echo " curl -u $AUTH_USER:$AUTH_PASS $ENDPOINT/"
599
+ echo ""
600
+ echo "Test SWAIG function:"
601
+ echo " curl -u $AUTH_USER:$AUTH_PASS -X POST $ENDPOINT/swaig \\\\"
602
+ echo " -H 'Content-Type: application/json' \\\\"
603
+ echo " -d '{{\\\"function\\\": \\\"get_info\\\", \\\"argument\\\": {{\\\"parsed\\\": [{{\\\"topic\\\": \\\"test\\\"}}]}}}}'"
604
+ echo ""
605
+ echo "Configure SignalWire:"
606
+ echo " Set your phone number's SWML URL to: https://$AUTH_USER:$AUTH_PASS@${{API_ID}}.execute-api.${{REGION}}.amazonaws.com/"
607
+ echo ""
608
+ '''
609
+
610
+ # GCP Cloud Function Templates
611
+ GCP_REQUIREMENTS_TEMPLATE = '''signalwire-agents>=1.0.10
612
+ functions-framework>=3.0.0
613
+ '''
614
+
615
+ GCP_MAIN_TEMPLATE = '''#!/usr/bin/env python3
616
+ """Google Cloud Functions handler for {agent_name} agent.
617
+
618
+ This demonstrates deploying a SignalWire AI Agent to Google Cloud Functions
619
+ with SWAIG functions and SWML output.
620
+
621
+ Environment variables:
622
+ SWML_BASIC_AUTH_USER: Basic auth username (optional)
623
+ SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)
624
+ """
625
+
626
+ import os
627
+ from signalwire_agents import AgentBase, SwaigFunctionResult
628
+
629
+
630
+ class {agent_class}(AgentBase):
631
+ """{agent_name} agent for Google Cloud Functions deployment."""
632
+
633
+ def __init__(self):
634
+ super().__init__(name="{agent_name_slug}")
635
+
636
+ self._configure_prompts()
637
+ self.add_language("English", "en-US", "rime.spore")
638
+ self._setup_functions()
639
+
640
+ def _configure_prompts(self):
641
+ self.prompt_add_section(
642
+ "Role",
643
+ "You are a helpful AI assistant deployed on Google Cloud Functions."
644
+ )
645
+
646
+ self.prompt_add_section(
647
+ "Guidelines",
648
+ bullets=[
649
+ "Be professional and courteous",
650
+ "Ask clarifying questions when needed",
651
+ "Keep responses concise and helpful"
652
+ ]
653
+ )
654
+
655
+ def _setup_functions(self):
656
+ @self.tool(
657
+ description="Get information about a topic",
658
+ parameters={{
659
+ "type": "object",
660
+ "properties": {{
661
+ "topic": {{
662
+ "type": "string",
663
+ "description": "The topic to get information about"
664
+ }}
665
+ }},
666
+ "required": ["topic"]
667
+ }}
668
+ )
669
+ def get_info(args, raw_data):
670
+ topic = args.get("topic", "")
671
+ return SwaigFunctionResult(
672
+ f"Information about {{topic}}: This is a placeholder response."
673
+ )
674
+
675
+ @self.tool(description="Get Google Cloud deployment information")
676
+ def get_platform_info(args, raw_data):
677
+ import urllib.request
678
+
679
+ # Gen 2 Cloud Functions run on Cloud Run with these env vars
680
+ service = os.getenv("K_SERVICE", "unknown")
681
+ revision = os.getenv("K_REVISION", "unknown")
682
+
683
+ # Query metadata server for project ID
684
+ project = os.getenv("GOOGLE_CLOUD_PROJECT", "unknown")
685
+ if project == "unknown":
686
+ try:
687
+ req = urllib.request.Request(
688
+ "http://metadata.google.internal/computeMetadata/v1/project/project-id",
689
+ headers={{"Metadata-Flavor": "Google"}}
690
+ )
691
+ with urllib.request.urlopen(req, timeout=2) as resp:
692
+ project = resp.read().decode()
693
+ except Exception:
694
+ pass
695
+
696
+ return SwaigFunctionResult(
697
+ f"Running on Google Cloud Functions Gen 2. "
698
+ f"Service: {{service}}, Revision: {{revision}}, "
699
+ f"Project: {{project}}."
700
+ )
701
+
702
+
703
+ # Create agent instance outside handler for warm starts
704
+ agent = {agent_class}()
705
+
706
+
707
+ def main(request):
708
+ """Google Cloud Functions entry point.
709
+
710
+ Args:
711
+ request: Flask request object
712
+
713
+ Returns:
714
+ Flask response
715
+ """
716
+ return agent.run(request)
717
+ '''
718
+
719
+ GCP_DEPLOY_TEMPLATE = '''#!/bin/bash
720
+ # Google Cloud Functions deployment script for {agent_name} agent
721
+ #
722
+ # Prerequisites:
723
+ # - gcloud CLI installed and authenticated
724
+ # - A Google Cloud project with Cloud Functions API enabled
725
+ #
726
+ # Usage:
727
+ # ./deploy.sh # Deploy with defaults
728
+ # ./deploy.sh my-function # Custom function name
729
+ # ./deploy.sh my-function us-central1 # Custom function and region
730
+
731
+ set -e
732
+
733
+ # Configuration
734
+ FUNCTION_NAME="${{1:-{function_name}}}"
735
+ REGION="${{2:-{region}}}"
736
+ RUNTIME="python311"
737
+ ENTRY_POINT="main"
738
+ MEMORY="512MB"
739
+ TIMEOUT="60s"
740
+ MIN_INSTANCES=0
741
+ MAX_INSTANCES=10
742
+
743
+ # Directory containing this script
744
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
745
+
746
+ echo "=== {agent_name} - Google Cloud Functions Deployment ==="
747
+ echo "Function: $FUNCTION_NAME"
748
+ echo "Region: $REGION"
749
+ echo ""
750
+
751
+ # Get current project
752
+ PROJECT=$(gcloud config get-value project 2>/dev/null)
753
+ if [ -z "$PROJECT" ]; then
754
+ echo "Error: No project set. Run: gcloud config set project <project-id>"
755
+ exit 1
756
+ fi
757
+ echo "Project: $PROJECT"
758
+ echo ""
759
+
760
+ # Step 1: Enable required APIs
761
+ echo "Step 1: Enabling required APIs..."
762
+ gcloud services enable cloudfunctions.googleapis.com --quiet
763
+ gcloud services enable cloudbuild.googleapis.com --quiet
764
+ gcloud services enable artifactregistry.googleapis.com --quiet
765
+
766
+ # Step 2: Create deployment package
767
+ echo ""
768
+ echo "Step 2: Creating deployment package..."
769
+
770
+ # Create a temporary deployment directory
771
+ DEPLOY_DIR=$(mktemp -d)
772
+ trap "rm -rf $DEPLOY_DIR" EXIT
773
+
774
+ # Copy the main files
775
+ cp "$SCRIPT_DIR/main.py" "$DEPLOY_DIR/"
776
+ cp "$SCRIPT_DIR/requirements.txt" "$DEPLOY_DIR/"
777
+
778
+ echo "Deployment package contents:"
779
+ ls -la "$DEPLOY_DIR/"
780
+
781
+ # Step 3: Deploy function
782
+ echo ""
783
+ echo "Step 3: Deploying Cloud Function..."
784
+
785
+ # Check if function exists (Gen 2 vs Gen 1)
786
+ EXISTING_GEN2=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --gen2 2>/dev/null && echo "yes" || echo "no")
787
+
788
+ if [ "$EXISTING_GEN2" == "yes" ]; then
789
+ echo "Updating existing Gen 2 function..."
790
+ else
791
+ echo "Creating new Gen 2 function..."
792
+ fi
793
+
794
+ gcloud functions deploy "$FUNCTION_NAME" \\
795
+ --gen2 \\
796
+ --region="$REGION" \\
797
+ --runtime="$RUNTIME" \\
798
+ --source="$DEPLOY_DIR" \\
799
+ --entry-point="$ENTRY_POINT" \\
800
+ --trigger-http \\
801
+ --allow-unauthenticated \\
802
+ --memory="$MEMORY" \\
803
+ --timeout="$TIMEOUT" \\
804
+ --min-instances="$MIN_INSTANCES" \\
805
+ --max-instances="$MAX_INSTANCES" \\
806
+ --quiet
807
+
808
+ # Step 4: Get the endpoint URL
809
+ echo ""
810
+ echo "Step 4: Getting endpoint URL..."
811
+
812
+ ENDPOINT=$(gcloud functions describe "$FUNCTION_NAME" \\
813
+ --region="$REGION" \\
814
+ --gen2 \\
815
+ --format="value(serviceConfig.uri)")
816
+
817
+ echo ""
818
+ echo "=== Deployment Complete ==="
819
+ echo ""
820
+ echo "Endpoint URL: $ENDPOINT"
821
+ echo ""
822
+ echo "Test SWML output:"
823
+ echo " curl $ENDPOINT"
824
+ echo ""
825
+ echo "Test SWAIG function:"
826
+ echo " curl -X POST $ENDPOINT/swaig \\\\"
827
+ echo " -H 'Content-Type: application/json' \\\\"
828
+ echo " -d '{{\\\"function\\\": \\\"get_info\\\", \\\"argument\\\": {{\\\"parsed\\\": [{{\\\"topic\\\": \\\"test\\\"}}]}}}}'"
829
+ echo ""
830
+ echo "Configure SignalWire:"
831
+ echo " Set your phone number's SWML URL to: $ENDPOINT"
832
+ echo ""
833
+ echo "To set environment variables (optional):"
834
+ echo " gcloud functions deploy $FUNCTION_NAME \\\\"
835
+ echo " --region=$REGION \\\\"
836
+ echo " --gen2 \\\\"
837
+ echo " --update-env-vars SWML_BASIC_AUTH_USER=myuser,SWML_BASIC_AUTH_PASSWORD=mypass"
838
+ echo ""
839
+ '''
840
+
841
+ # Azure Function Templates
842
+ AZURE_REQUIREMENTS_TEMPLATE = '''azure-functions>=1.17.0
843
+ signalwire-agents>=1.0.10
844
+ '''
845
+
846
+ AZURE_INIT_TEMPLATE = '''#!/usr/bin/env python3
847
+ """Azure Functions handler for {agent_name} agent.
848
+
849
+ This demonstrates deploying a SignalWire AI Agent to Azure Functions
850
+ with SWAIG functions and SWML output.
851
+
852
+ Environment variables:
853
+ SWML_BASIC_AUTH_USER: Basic auth username (optional)
854
+ SWML_BASIC_AUTH_PASSWORD: Basic auth password (optional)
855
+ """
856
+
857
+ import os
858
+ import azure.functions as func
859
+ from signalwire_agents import AgentBase, SwaigFunctionResult
860
+
861
+
862
+ class {agent_class}(AgentBase):
863
+ """{agent_name} agent for Azure Functions deployment."""
864
+
865
+ def __init__(self):
866
+ super().__init__(name="{agent_name_slug}")
867
+
868
+ self._configure_prompts()
869
+ self.add_language("English", "en-US", "rime.spore")
870
+ self._setup_functions()
871
+
872
+ def _configure_prompts(self):
873
+ self.prompt_add_section(
874
+ "Role",
875
+ "You are a helpful AI assistant deployed on Azure Functions."
876
+ )
877
+
878
+ self.prompt_add_section(
879
+ "Guidelines",
880
+ bullets=[
881
+ "Be professional and courteous",
882
+ "Ask clarifying questions when needed",
883
+ "Keep responses concise and helpful"
884
+ ]
885
+ )
886
+
887
+ def _setup_functions(self):
888
+ @self.tool(
889
+ description="Get information about a topic",
890
+ parameters={{
891
+ "type": "object",
892
+ "properties": {{
893
+ "topic": {{
894
+ "type": "string",
895
+ "description": "The topic to get information about"
896
+ }}
897
+ }},
898
+ "required": ["topic"]
899
+ }}
900
+ )
901
+ def get_info(args, raw_data):
902
+ topic = args.get("topic", "")
903
+ return SwaigFunctionResult(
904
+ f"Information about {{topic}}: This is a placeholder response."
905
+ )
906
+
907
+ @self.tool(description="Get Azure Functions deployment information")
908
+ def get_platform_info(args, raw_data):
909
+ function_name = os.getenv("WEBSITE_SITE_NAME", "unknown")
910
+ region = os.getenv("REGION_NAME", "unknown")
911
+ runtime = os.getenv("FUNCTIONS_WORKER_RUNTIME", "unknown")
912
+ version = os.getenv("FUNCTIONS_EXTENSION_VERSION", "unknown")
913
+
914
+ return SwaigFunctionResult(
915
+ f"Running on Azure Functions. "
916
+ f"App: {{function_name}}, Region: {{region}}, "
917
+ f"Runtime: {{runtime}}, Version: {{version}}."
918
+ )
919
+
920
+
921
+ # Create agent instance outside handler for warm starts
922
+ agent = {agent_class}()
923
+
924
+
925
+ def main(req: func.HttpRequest) -> func.HttpResponse:
926
+ """Azure Functions entry point.
927
+
928
+ Args:
929
+ req: Azure Functions HTTP request object
930
+
931
+ Returns:
932
+ Azure Functions HTTP response
933
+ """
934
+ return agent.run(req)
935
+ '''
936
+
937
+ AZURE_FUNCTION_JSON_TEMPLATE = '''{{
938
+ "scriptFile": "__init__.py",
939
+ "bindings": [
940
+ {{
941
+ "authLevel": "anonymous",
942
+ "type": "httpTrigger",
943
+ "direction": "in",
944
+ "name": "req",
945
+ "methods": ["get", "post"],
946
+ "route": "{{*path}}"
947
+ }},
948
+ {{
949
+ "type": "http",
950
+ "direction": "out",
951
+ "name": "$return"
952
+ }}
953
+ ]
954
+ }}
955
+ '''
956
+
957
+ AZURE_HOST_JSON_TEMPLATE = '''{{
958
+ "version": "2.0",
959
+ "logging": {{
960
+ "applicationInsights": {{
961
+ "samplingSettings": {{
962
+ "isEnabled": true,
963
+ "excludedTypes": "Request"
964
+ }}
965
+ }}
966
+ }},
967
+ "extensionBundle": {{
968
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
969
+ "version": "[4.*, 5.0.0)"
970
+ }},
971
+ "extensions": {{
972
+ "http": {{
973
+ "routePrefix": ""
974
+ }}
975
+ }}
976
+ }}
977
+ '''
978
+
979
+ AZURE_LOCAL_SETTINGS_TEMPLATE = '''{{
980
+ "IsEncrypted": false,
981
+ "Values": {{
982
+ "FUNCTIONS_WORKER_RUNTIME": "python",
983
+ "AzureWebJobsStorage": ""
984
+ }}
985
+ }}
986
+ '''
987
+
988
+ AZURE_DEPLOY_TEMPLATE = '''#!/bin/bash
989
+ # Azure Functions deployment script for {agent_name} agent
990
+ #
991
+ # Prerequisites:
992
+ # - Azure CLI installed and authenticated (az login)
993
+ # - Docker installed and running (for building correct architecture)
994
+ #
995
+ # Usage:
996
+ # ./deploy.sh # Deploy with defaults
997
+ # ./deploy.sh my-app # Custom app name
998
+ # ./deploy.sh my-app eastus my-rg # Custom app, region, and resource group
999
+
1000
+ set -e
1001
+
1002
+ # Configuration
1003
+ APP_NAME="${{1:-{function_name}}}"
1004
+ LOCATION="${{2:-{region}}}"
1005
+ RESOURCE_GROUP="${{3:-{resource_group}}}"
1006
+ STORAGE_ACCOUNT="${{APP_NAME//-/}}storage" # Remove hyphens for storage account
1007
+ RUNTIME="python"
1008
+ RUNTIME_VERSION="3.11"
1009
+ FUNCTIONS_VERSION="4"
1010
+
1011
+ # Truncate storage account name to 24 chars (Azure limit)
1012
+ STORAGE_ACCOUNT="${{STORAGE_ACCOUNT:0:24}}"
1013
+
1014
+ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
1015
+
1016
+ echo "=== {agent_name} - Azure Functions Deployment ==="
1017
+ echo "App Name: $APP_NAME"
1018
+ echo "Location: $LOCATION"
1019
+ echo "Resource Group: $RESOURCE_GROUP"
1020
+ echo "Storage Account: $STORAGE_ACCOUNT"
1021
+ echo ""
1022
+
1023
+ # Check for Docker
1024
+ if ! command -v docker &> /dev/null; then
1025
+ echo "ERROR: Docker is required but not installed."
1026
+ echo "Please install Docker: https://docs.docker.com/get-docker/"
1027
+ exit 1
1028
+ fi
1029
+
1030
+ if ! docker info &> /dev/null; then
1031
+ echo "ERROR: Docker is not running. Please start Docker."
1032
+ exit 1
1033
+ fi
1034
+
1035
+ # Step 1: Login check
1036
+ echo "Step 1: Checking Azure login..."
1037
+ if ! az account show &>/dev/null; then
1038
+ echo "Not logged in. Running: az login"
1039
+ az login
1040
+ fi
1041
+
1042
+ SUBSCRIPTION=$(az account show --query name -o tsv)
1043
+ echo "Subscription: $SUBSCRIPTION"
1044
+ echo ""
1045
+
1046
+ # Step 2: Create resource group
1047
+ echo "Step 2: Creating resource group..."
1048
+ if ! az group show --name "$RESOURCE_GROUP" &>/dev/null; then
1049
+ az group create \\
1050
+ --name "$RESOURCE_GROUP" \\
1051
+ --location "$LOCATION" \\
1052
+ --output none
1053
+ echo "Created resource group: $RESOURCE_GROUP"
1054
+ else
1055
+ echo "Resource group exists: $RESOURCE_GROUP"
1056
+ fi
1057
+
1058
+ # Step 3: Create storage account
1059
+ echo ""
1060
+ echo "Step 3: Creating storage account..."
1061
+ if ! az storage account show --name "$STORAGE_ACCOUNT" --resource-group "$RESOURCE_GROUP" &>/dev/null; then
1062
+ az storage account create \\
1063
+ --name "$STORAGE_ACCOUNT" \\
1064
+ --resource-group "$RESOURCE_GROUP" \\
1065
+ --location "$LOCATION" \\
1066
+ --sku Standard_LRS \\
1067
+ --output none
1068
+ echo "Created storage account: $STORAGE_ACCOUNT"
1069
+ echo "Waiting for storage account to propagate..."
1070
+ sleep 10
1071
+ else
1072
+ echo "Storage account exists: $STORAGE_ACCOUNT"
1073
+ fi
1074
+
1075
+ # Step 4: Create Function App
1076
+ echo ""
1077
+ echo "Step 4: Creating Function App..."
1078
+ if ! az functionapp show --name "$APP_NAME" --resource-group "$RESOURCE_GROUP" &>/dev/null; then
1079
+ az functionapp create \\
1080
+ --name "$APP_NAME" \\
1081
+ --resource-group "$RESOURCE_GROUP" \\
1082
+ --storage-account "$STORAGE_ACCOUNT" \\
1083
+ --consumption-plan-location "$LOCATION" \\
1084
+ --runtime "$RUNTIME" \\
1085
+ --runtime-version "$RUNTIME_VERSION" \\
1086
+ --functions-version "$FUNCTIONS_VERSION" \\
1087
+ --os-type Linux \\
1088
+ --output none
1089
+ echo "Created Function App: $APP_NAME"
1090
+
1091
+ # Wait for app to be ready
1092
+ echo "Waiting for Function App to be ready..."
1093
+ sleep 30
1094
+ else
1095
+ echo "Function App exists: $APP_NAME"
1096
+ fi
1097
+
1098
+ # Step 5: Build and deploy the function using Docker
1099
+ echo ""
1100
+ echo "Step 5: Building function with Docker (linux/amd64)..."
1101
+
1102
+ DEPLOY_DIR=$(mktemp -d)
1103
+
1104
+ # Copy function files
1105
+ cp -r "$SCRIPT_DIR/function_app" "$DEPLOY_DIR/"
1106
+ cp "$SCRIPT_DIR/host.json" "$DEPLOY_DIR/"
1107
+ cp "$SCRIPT_DIR/requirements.txt" "$DEPLOY_DIR/"
1108
+ cp "$SCRIPT_DIR/local.settings.json" "$DEPLOY_DIR/" 2>/dev/null || true
1109
+
1110
+ # Build dependencies using Docker for correct architecture
1111
+ echo "Installing dependencies via Docker..."
1112
+ docker run --rm \\
1113
+ --platform linux/amd64 \\
1114
+ --entrypoint "" \\
1115
+ -v "$DEPLOY_DIR:/var/task" \\
1116
+ -w /var/task \\
1117
+ mcr.microsoft.com/azure-functions/python:4-python3.11 \\
1118
+ bash -c "pip install -r requirements.txt -t .python_packages/lib/site-packages --quiet"
1119
+
1120
+ # Create zip for deployment
1121
+ echo "Creating deployment package..."
1122
+ ZIP_FILE="$DEPLOY_DIR/deploy.zip"
1123
+ cd "$DEPLOY_DIR"
1124
+ zip -r "$ZIP_FILE" . -x "*.pyc" -q
1125
+ cd - > /dev/null
1126
+
1127
+ PACKAGE_SIZE=$(du -h "$ZIP_FILE" | cut -f1)
1128
+ echo "Package size: $PACKAGE_SIZE"
1129
+
1130
+ # Deploy using zip deployment
1131
+ echo ""
1132
+ echo "Step 6: Deploying to Azure..."
1133
+ az functionapp deployment source config-zip \\
1134
+ --name "$APP_NAME" \\
1135
+ --resource-group "$RESOURCE_GROUP" \\
1136
+ --src "$ZIP_FILE" \\
1137
+ --output none
1138
+
1139
+ echo "Deployment complete"
1140
+
1141
+ # Cleanup
1142
+ rm -rf "$DEPLOY_DIR"
1143
+
1144
+ # Step 7: Get the endpoint URL
1145
+ echo ""
1146
+ echo "Step 7: Getting endpoint URL..."
1147
+
1148
+ ENDPOINT="https://${{APP_NAME}}.azurewebsites.net"
1149
+
1150
+ # Verify deployment
1151
+ echo "Waiting for deployment to propagate..."
1152
+ sleep 10
1153
+
1154
+ echo ""
1155
+ echo "=== Deployment Complete ==="
1156
+ echo ""
1157
+ echo "Endpoint URL: $ENDPOINT/api/function_app"
1158
+ echo ""
1159
+ echo "Test SWML output:"
1160
+ echo " curl $ENDPOINT/api/function_app"
1161
+ echo ""
1162
+ echo "Test SWAIG function:"
1163
+ echo " curl -X POST $ENDPOINT/api/function_app/swaig \\\\"
1164
+ echo " -H 'Content-Type: application/json' \\\\"
1165
+ echo " -d '{{\\\"function\\\": \\\"get_info\\\", \\\"argument\\\": {{\\\"parsed\\\": [{{\\\"topic\\\": \\\"test\\\"}}]}}}}'"
1166
+ echo ""
1167
+ echo "Configure SignalWire:"
1168
+ echo " Set your phone number's SWML URL to: $ENDPOINT/api/function_app"
1169
+ echo ""
1170
+ echo "To set environment variables (optional):"
1171
+ echo " az functionapp config appsettings set --name $APP_NAME --resource-group $RESOURCE_GROUP \\\\"
1172
+ echo " --settings SWML_BASIC_AUTH_USER=myuser SWML_BASIC_AUTH_PASSWORD=mypass"
1173
+ echo ""
1174
+ '''
1175
+
1176
+
1177
+ def get_agent_template(agent_type: str, features: Dict[str, bool]) -> str:
1178
+ """Generate the main agent template based on type and features."""
1179
+
1180
+ has_tool = features.get('example_tool', True)
1181
+ has_debug = features.get('debug_webhooks', False)
1182
+ has_auth = features.get('basic_auth', False)
1183
+
1184
+ imports = ['from signalwire_agents import AgentBase']
1185
+ if has_tool:
1186
+ imports.append('from signalwire_agents import SwaigFunctionResult')
1187
+
1188
+ imports_str = '\n'.join(imports)
1189
+
1190
+ # Build the __init__ method
1191
+ init_parts = []
1192
+
1193
+ if has_auth:
1194
+ init_parts.append('''
1195
+ # Set basic auth if configured
1196
+ user = os.getenv("SWML_BASIC_AUTH_USER")
1197
+ password = os.getenv("SWML_BASIC_AUTH_PASSWORD")
1198
+ if user and password:
1199
+ self.set_params({
1200
+ "swml_basic_auth_user": user,
1201
+ "swml_basic_auth_password": password,
1202
+ })''')
1203
+
1204
+ init_parts.append('''
1205
+ self._configure_voice()
1206
+ self._configure_prompts()''')
1207
+
1208
+ if has_debug:
1209
+ init_parts.append('''
1210
+ self._configure_debug_webhooks()''')
1211
+
1212
+ init_body = ''.join(init_parts)
1213
+
1214
+ # Build optional methods
1215
+ extra_methods = []
1216
+
1217
+ if has_debug:
1218
+ extra_methods.append('''
1219
+
1220
+ def _configure_debug_webhooks(self):
1221
+ """Set up debug and post-prompt webhooks."""
1222
+ proxy_url = os.getenv("SWML_PROXY_URL_BASE", "")
1223
+ debug_level = int(os.getenv("DEBUG_WEBHOOK_LEVEL", "1"))
1224
+ auth_user = os.getenv("SWML_BASIC_AUTH_USER", "")
1225
+ auth_pass = os.getenv("SWML_BASIC_AUTH_PASSWORD", "")
1226
+
1227
+ if proxy_url and debug_level > 0:
1228
+ # Build URL with basic auth credentials
1229
+ if auth_user and auth_pass:
1230
+ if "://" in proxy_url:
1231
+ scheme, rest = proxy_url.split("://", 1)
1232
+ auth_proxy_url = f"{scheme}://{auth_user}:{auth_pass}@{rest}"
1233
+ else:
1234
+ auth_proxy_url = f"{auth_user}:{auth_pass}@{proxy_url}"
1235
+ else:
1236
+ auth_proxy_url = proxy_url
1237
+
1238
+ self.set_params({
1239
+ "debug_webhook_url": f"{auth_proxy_url}/debug",
1240
+ "debug_webhook_level": debug_level,
1241
+ })
1242
+ self.set_post_prompt(
1243
+ "Summarize the conversation including: "
1244
+ "the caller's main request, any actions taken, "
1245
+ "and the outcome of the call."
1246
+ )
1247
+ self.set_post_prompt_url(f"{auth_proxy_url}/post_prompt")
1248
+
1249
+ def on_summary(self, summary):
1250
+ """Handle call summary."""
1251
+ print(f"Call summary: {summary}")
1252
+ ''')
1253
+
1254
+ if has_tool:
1255
+ extra_methods.append('''
1256
+
1257
+ @AgentBase.tool(
1258
+ name="get_info",
1259
+ description="Get information about a topic",
1260
+ parameters={
1261
+ "type": "object",
1262
+ "properties": {
1263
+ "topic": {
1264
+ "type": "string",
1265
+ "description": "The topic to get information about"
1266
+ }
1267
+ },
1268
+ "required": ["topic"]
1269
+ }
1270
+ )
1271
+ def get_info(self, args, raw_data):
1272
+ """Get information about a topic."""
1273
+ topic = args.get("topic", "")
1274
+ # TODO: Implement your logic here
1275
+ return SwaigFunctionResult(f"Information about {topic}: This is a placeholder response.")''')
1276
+
1277
+ extra_methods_str = ''.join(extra_methods)
1278
+
1279
+ # Need os import if auth or debug
1280
+ os_import = 'import os\n' if (has_auth or has_debug) else ''
1281
+
1282
+ return f'''#!/usr/bin/env python3
1283
+ """Main Agent - SignalWire AI Agent"""
1284
+
1285
+ {os_import}{imports_str}
1286
+
1287
+
1288
+ class MainAgent(AgentBase):
1289
+ """Main voice AI agent."""
1290
+
1291
+ def __init__(self):
1292
+ super().__init__(
1293
+ name="main-agent",
1294
+ route="/swml"
1295
+ )
1296
+ {init_body}
1297
+
1298
+ def _configure_voice(self):
1299
+ """Set up voice and language."""
1300
+ self.add_language("English", "en-US", "rime.spore")
1301
+
1302
+ self.set_params({{
1303
+ "end_of_speech_timeout": 500,
1304
+ "attention_timeout": 15000,
1305
+ }})
1306
+
1307
+ def _configure_prompts(self):
1308
+ """Set up AI prompts."""
1309
+ self.prompt_add_section(
1310
+ "Role",
1311
+ "You are a helpful AI assistant. "
1312
+ "Help callers with their questions and requests."
1313
+ )
1314
+
1315
+ self.prompt_add_section(
1316
+ "Guidelines",
1317
+ body="Follow these guidelines:",
1318
+ bullets=[
1319
+ "Be professional and courteous",
1320
+ "Ask clarifying questions when needed",
1321
+ "Keep responses concise and helpful",
1322
+ "If you cannot help, offer to transfer to a human"
1323
+ ]
1324
+ )
1325
+ {extra_methods_str}
1326
+ '''
1327
+
1328
+
1329
+ def get_app_template(features: Dict[str, bool]) -> str:
1330
+ """Generate the app.py template based on features."""
1331
+
1332
+ has_debug = features.get('debug_webhooks', False)
1333
+ has_web_ui = features.get('web_ui', False)
1334
+
1335
+ # Base imports
1336
+ imports = [
1337
+ 'import os',
1338
+ 'from pathlib import Path',
1339
+ 'from dotenv import load_dotenv',
1340
+ '',
1341
+ '# Load environment variables from .env file',
1342
+ 'load_dotenv()',
1343
+ '',
1344
+ 'from signalwire_agents import AgentServer',
1345
+ 'from agents import MainAgent',
1346
+ ]
1347
+
1348
+ if has_debug:
1349
+ imports.insert(1, 'import sys')
1350
+ imports.insert(2, 'import json')
1351
+ imports.insert(3, 'from datetime import datetime')
1352
+ imports.insert(4, 'from starlette.requests import Request')
1353
+
1354
+ imports_str = '\n'.join(imports)
1355
+
1356
+ # Create agent at module level for swaig-test compatibility
1357
+ agent_instance = '''
1358
+
1359
+ # Create agent instance at module level for swaig-test compatibility
1360
+ agent = MainAgent()
1361
+ '''
1362
+
1363
+ # Debug webhook code
1364
+ debug_code = ''
1365
+ if has_debug:
1366
+ debug_code = '''
1367
+
1368
+ # ANSI colors for console output
1369
+ RESET = "\\033[0m"
1370
+ BOLD = "\\033[1m"
1371
+ DIM = "\\033[2m"
1372
+ CYAN = "\\033[36m"
1373
+ GREEN = "\\033[32m"
1374
+ YELLOW = "\\033[33m"
1375
+ MAGENTA = "\\033[35m"
1376
+
1377
+
1378
+ def print_separator(char="-", width=80):
1379
+ print(f"{DIM}{char * width}{RESET}")
1380
+
1381
+
1382
+ def print_debug_data(data):
1383
+ """Pretty print debug webhook data."""
1384
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1385
+ print()
1386
+ print_separator("=")
1387
+ print(f"{BOLD}{CYAN}> DEBUG WEBHOOK{RESET}")
1388
+ print(f"{DIM}{timestamp}{RESET}")
1389
+ print_separator()
1390
+
1391
+ if isinstance(data, dict):
1392
+ event_type = data.get("event_type", data.get("type", "unknown"))
1393
+ print(f"{YELLOW}Event:{RESET} {event_type}")
1394
+
1395
+ call_id = data.get("call_id", data.get("CallSid", ""))
1396
+ if call_id:
1397
+ print(f"{YELLOW}Call ID:{RESET} {call_id}")
1398
+
1399
+ if "conversation" in data:
1400
+ print(f"\\n{BOLD}{YELLOW}Conversation:{RESET}")
1401
+ for msg in data.get("conversation", [])[-5:]:
1402
+ role = msg.get("role", "?")
1403
+ content = msg.get("content", "")[:100]
1404
+ color = GREEN if role == "assistant" else MAGENTA
1405
+ print(f" {color}{role}:{RESET} {content}")
1406
+
1407
+ debug_level = int(os.getenv("DEBUG_WEBHOOK_LEVEL", "1"))
1408
+ if debug_level >= 2:
1409
+ print(f"\\n{BOLD}{YELLOW}Full Data:{RESET}")
1410
+ formatted = json.dumps(data, indent=2)
1411
+ for line in formatted.split('\\n')[:50]:
1412
+ print(f" {DIM}{line}{RESET}")
1413
+ else:
1414
+ print(f"{DIM}{data}{RESET}")
1415
+
1416
+ print_separator("=")
1417
+ print()
1418
+ sys.stdout.flush()
1419
+
1420
+
1421
+ def print_post_prompt_data(data):
1422
+ """Pretty print post-prompt summary data."""
1423
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1424
+ print()
1425
+ print_separator("=")
1426
+ print(f"{BOLD}{YELLOW}> POST-PROMPT SUMMARY{RESET}")
1427
+ print(f"{DIM}{timestamp}{RESET}")
1428
+ print_separator()
1429
+
1430
+ if isinstance(data, dict):
1431
+ summary = data.get("post_prompt_data", {})
1432
+ if isinstance(summary, dict):
1433
+ for key, value in summary.items():
1434
+ print(f"{GREEN}{key}:{RESET} {value}")
1435
+ elif summary:
1436
+ print(f"{GREEN}Summary:{RESET} {summary}")
1437
+
1438
+ raw = data.get("raw_response", data.get("response", ""))
1439
+ if raw:
1440
+ print(f"\\n{BOLD}{YELLOW}Response:{RESET}")
1441
+ print(f" {MAGENTA}{raw}{RESET}")
1442
+
1443
+ call_id = data.get("call_id", "")
1444
+ if call_id:
1445
+ print(f"\\n{DIM}Call ID: {call_id}{RESET}")
1446
+ else:
1447
+ print(f"{DIM}{data}{RESET}")
1448
+
1449
+ print_separator("=")
1450
+ print()
1451
+ sys.stdout.flush()
1452
+ '''
1453
+
1454
+ # Main function
1455
+ main_body_parts = ['''
1456
+ def main():
1457
+ host = os.getenv("HOST", "0.0.0.0")
1458
+ port = int(os.getenv("PORT", "5000"))
1459
+
1460
+ # Create server and register agent
1461
+ server = AgentServer(host=host, port=port)
1462
+ server.register(agent)
1463
+ ''']
1464
+
1465
+ if has_web_ui:
1466
+ main_body_parts.append('''
1467
+ # Serve static files from web/ directory
1468
+ web_dir = Path(__file__).parent / "web"
1469
+ if web_dir.exists():
1470
+ server.serve_static_files(str(web_dir))
1471
+ ''')
1472
+
1473
+ if has_debug:
1474
+ main_body_parts.append('''
1475
+ # Add debug webhook endpoint
1476
+ @server.app.post('/debug')
1477
+ async def debug_webhook(request: Request):
1478
+ """Receive and display debug webhook data."""
1479
+ try:
1480
+ data = await request.json()
1481
+ except:
1482
+ body = await request.body()
1483
+ data = body.decode('utf-8', errors='ignore')
1484
+ print_debug_data(data)
1485
+ return {'status': 'received'}
1486
+
1487
+ # Add post-prompt webhook endpoint
1488
+ @server.app.post('/post_prompt')
1489
+ async def post_prompt_webhook(request: Request):
1490
+ """Receive and display post-prompt summary data."""
1491
+ try:
1492
+ data = await request.json()
1493
+ except:
1494
+ body = await request.body()
1495
+ data = body.decode('utf-8', errors='ignore')
1496
+ print_post_prompt_data(data)
1497
+ return {'status': 'received'}
1498
+ ''')
1499
+
1500
+ main_body_parts.append('''
1501
+ # Print startup info
1502
+ print(f"\\nSignalWire Agent Server")
1503
+ print(f"SWML endpoint: http://{host}:{port}/swml")
1504
+ print(f"SWAIG endpoint: http://{host}:{port}/swml/swaig/")
1505
+ print()
1506
+
1507
+ server.run()
1508
+
1509
+
1510
+ if __name__ == "__main__":
1511
+ main()
1512
+ ''')
1513
+
1514
+ main_body = ''.join(main_body_parts)
1515
+
1516
+ return f'''#!/usr/bin/env python3
1517
+ """Main entry point for the agent server."""
1518
+
1519
+ {imports_str}
1520
+ {agent_instance}
1521
+ {debug_code}
1522
+ {main_body}'''
1523
+
1524
+
1525
+ def get_test_template(has_tool: bool) -> str:
1526
+ """Generate test template."""
1527
+
1528
+ tool_tests = ''
1529
+ if has_tool:
1530
+ tool_tests = '''
1531
+
1532
+ class TestFunctionExecution:
1533
+ """Test that SWAIG functions can be executed."""
1534
+
1535
+ def test_get_info_function(self):
1536
+ """Test get_info function executes successfully."""
1537
+ returncode, stdout, stderr = run_swaig_test(
1538
+ "--exec", "get_info", "--topic", "SignalWire"
1539
+ )
1540
+ assert returncode == 0, f"Function execution failed: {stderr}"
1541
+ assert "SignalWire" in stdout, f"Expected 'SignalWire' in output: {stdout}"
1542
+
1543
+
1544
+ class TestDirectImport:
1545
+ """Test direct Python import of the agent."""
1546
+
1547
+ def test_agent_creation(self):
1548
+ """Test that agent can be instantiated."""
1549
+ from agents import MainAgent
1550
+ agent = MainAgent()
1551
+ assert agent is not None
1552
+ assert agent.name == "main-agent"
1553
+
1554
+ def test_get_info_tool_direct(self):
1555
+ """Test the get_info tool via direct call."""
1556
+ from agents import MainAgent
1557
+ agent = MainAgent()
1558
+ result = agent.get_info({"topic": "test"}, {})
1559
+ assert "test" in result.response
1560
+ '''
1561
+
1562
+ tool_check = '''
1563
+ def test_agent_has_tools(self):
1564
+ """Test agent has expected tools defined."""
1565
+ tools = list_tools()
1566
+ assert "get_info" in tools, f"Missing get_info tool. Found: {tools}"
1567
+ ''' if has_tool else ''
1568
+
1569
+ return f'''#!/usr/bin/env python3
1570
+ """
1571
+ Tests for the main agent using swaig-test.
1572
+ """
1573
+
1574
+ import subprocess
1575
+ import json
1576
+ import sys
1577
+ from pathlib import Path
1578
+
1579
+ import pytest
1580
+
1581
+
1582
+ AGENT_FILE = Path(__file__).parent.parent / "agents" / "main_agent.py"
1583
+
1584
+
1585
+ def run_swaig_test(*args) -> tuple:
1586
+ """Run swaig-test on the agent and return (returncode, stdout, stderr)."""
1587
+ cmd = [sys.executable, "-m", "signalwire_agents.cli.swaig_test_wrapper", str(AGENT_FILE)] + list(args)
1588
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
1589
+ return result.returncode, result.stdout, result.stderr
1590
+
1591
+
1592
+ def get_swml_json() -> dict:
1593
+ """Get SWML JSON output from the agent."""
1594
+ returncode, stdout, stderr = run_swaig_test("--dump-swml", "--raw")
1595
+ if returncode != 0:
1596
+ pytest.fail(f"swaig-test failed:\\nstderr: {{stderr}}\\nstdout: {{stdout}}")
1597
+ try:
1598
+ return json.loads(stdout)
1599
+ except json.JSONDecodeError as e:
1600
+ pytest.fail(f"Invalid JSON: {{e}}\\nOutput: {{stdout}}")
1601
+
1602
+
1603
+ def list_tools() -> list:
1604
+ """List tools available in the agent."""
1605
+ returncode, stdout, stderr = run_swaig_test("--list-tools")
1606
+ if returncode != 0:
1607
+ return []
1608
+ tools = []
1609
+ for line in stdout.split('\\n'):
1610
+ line = line.strip()
1611
+ if ' - ' in line and not line.startswith('Parameters:'):
1612
+ parts = line.split(' - ')
1613
+ if parts:
1614
+ tool_name = parts[0].strip()
1615
+ if tool_name and not tool_name.startswith('('):
1616
+ tools.append(tool_name)
1617
+ return tools
1618
+
1619
+
1620
+ class TestAgentLoading:
1621
+ """Test that the agent can be loaded without errors."""
1622
+
1623
+ def test_agent_loads(self):
1624
+ """Test agent file can be loaded by swaig-test."""
1625
+ returncode, stdout, stderr = run_swaig_test("--list-tools")
1626
+ assert returncode == 0, f"Failed to load agent: {{stderr}}"
1627
+ {tool_check}
1628
+
1629
+ class TestSWMLGeneration:
1630
+ """Test that the agent generates valid SWML documents."""
1631
+
1632
+ def test_swml_structure(self):
1633
+ """Test SWML has required structure."""
1634
+ swml = get_swml_json()
1635
+ assert "version" in swml, "SWML missing 'version'"
1636
+ assert "sections" in swml, "SWML missing 'sections'"
1637
+ assert "main" in swml["sections"], "SWML missing 'sections.main'"
1638
+
1639
+ def test_swml_has_ai_section(self):
1640
+ """Test SWML has AI configuration."""
1641
+ swml = get_swml_json()
1642
+ main_section = swml.get("sections", {{}}).get("main", [])
1643
+ ai_found = any("ai" in verb for verb in main_section)
1644
+ assert ai_found, "SWML missing 'ai' verb"
1645
+ {tool_tests}
1646
+
1647
+ if __name__ == "__main__":
1648
+ pytest.main([__file__, "-v"])
1649
+ '''
1650
+
1651
+
1652
+ def get_readme_template(project_name: str, features: Dict[str, bool]) -> str:
1653
+ """Generate README template."""
1654
+
1655
+ endpoints = [
1656
+ "| `/swml` | POST | Main SWML endpoint - point your SignalWire phone number here |",
1657
+ ]
1658
+
1659
+ if features.get('debug_webhooks'):
1660
+ endpoints.append("| `/debug` | POST | Debug webhook - receives real-time call data |")
1661
+ endpoints.append("| `/post_prompt` | POST | Post-prompt webhook - receives call summaries |")
1662
+
1663
+ if features.get('web_ui'):
1664
+ endpoints.append("| `/` | GET | Static files from `web/` directory |")
1665
+
1666
+ endpoints_table = '\n'.join(endpoints)
1667
+
1668
+ return f'''# {project_name}
1669
+
1670
+ A SignalWire AI Agent built with signalwire-agents.
1671
+
1672
+ ## Quick Start
1673
+
1674
+ ```bash
1675
+ cd {project_name}
1676
+ source .venv/bin/activate # If using virtual environment
1677
+ python app.py
1678
+ ```
1679
+
1680
+ ## Endpoints
1681
+
1682
+ | Endpoint | Method | Description |
1683
+ |----------|--------|-------------|
1684
+ {endpoints_table}
1685
+
1686
+ ## Project Structure
1687
+
1688
+ ```
1689
+ {project_name}/
1690
+ ├── agents/
1691
+ │ ├── __init__.py
1692
+ │ └── main_agent.py # Main agent implementation
1693
+ ├── skills/
1694
+ │ └── __init__.py # Reusable skills
1695
+ ├── tests/
1696
+ │ └── test_agent.py # Test suite
1697
+ ├── web/ # Static files
1698
+ ├── app.py # Entry point
1699
+ ├── .env # Environment configuration
1700
+ └── requirements.txt # Python dependencies
1701
+ ```
1702
+
1703
+ ## Configuration
1704
+
1705
+ Edit `.env` to configure:
1706
+
1707
+ | Variable | Description |
1708
+ |----------|-------------|
1709
+ | `SIGNALWIRE_SPACE_NAME` | Your SignalWire space name |
1710
+ | `SIGNALWIRE_PROJECT_ID` | Your SignalWire project ID |
1711
+ | `SIGNALWIRE_TOKEN` | Your SignalWire API token |
1712
+ | `HOST` | Server host (default: 0.0.0.0) |
1713
+ | `PORT` | Server port (default: 5000) |
1714
+
1715
+ ## Testing
1716
+
1717
+ ```bash
1718
+ pytest tests/ -v
1719
+ ```
1720
+
1721
+ ## Adding Tools
1722
+
1723
+ Add new tools to your agent using the `@AgentBase.tool` decorator:
1724
+
1725
+ ```python
1726
+ @AgentBase.tool(
1727
+ name="my_tool",
1728
+ description="What this tool does",
1729
+ parameters={{
1730
+ "type": "object",
1731
+ "properties": {{
1732
+ "param1": {{"type": "string", "description": "Parameter description"}}
1733
+ }},
1734
+ "required": ["param1"]
1735
+ }}
1736
+ )
1737
+ def my_tool(self, args, raw_data):
1738
+ param1 = args.get("param1")
1739
+ return SwaigFunctionResult(f"Result: {{param1}}")
1740
+ ```
1741
+ '''
1742
+
1743
+
1744
+ def get_web_index_template() -> str:
1745
+ """Generate a simple web UI template."""
1746
+ return '''<!DOCTYPE html>
1747
+ <html>
1748
+ <head>
1749
+ <title>SignalWire Agent</title>
1750
+ <style>
1751
+ body {
1752
+ font-family: system-ui, -apple-system, sans-serif;
1753
+ max-width: 800px;
1754
+ margin: 0 auto;
1755
+ padding: 40px 20px;
1756
+ background: #f8f9fa;
1757
+ color: #333;
1758
+ }
1759
+ h1 {
1760
+ color: #044cf6;
1761
+ border-bottom: 3px solid #044cf6;
1762
+ padding-bottom: 10px;
1763
+ }
1764
+ .status {
1765
+ background: #d4edda;
1766
+ border: 1px solid #c3e6cb;
1767
+ color: #155724;
1768
+ padding: 15px;
1769
+ border-radius: 8px;
1770
+ margin: 20px 0;
1771
+ }
1772
+ .endpoint {
1773
+ background: white;
1774
+ border: 1px solid #ddd;
1775
+ border-radius: 8px;
1776
+ padding: 20px;
1777
+ margin: 15px 0;
1778
+ }
1779
+ .endpoint h3 {
1780
+ margin-top: 0;
1781
+ color: #044cf6;
1782
+ }
1783
+ .method {
1784
+ display: inline-block;
1785
+ padding: 4px 10px;
1786
+ border-radius: 4px;
1787
+ font-weight: bold;
1788
+ font-size: 12px;
1789
+ margin-right: 10px;
1790
+ }
1791
+ .method.post { background: #49cc90; color: white; }
1792
+ code {
1793
+ background: #e9ecef;
1794
+ padding: 2px 6px;
1795
+ border-radius: 4px;
1796
+ font-size: 14px;
1797
+ }
1798
+ pre {
1799
+ background: #1e1e1e;
1800
+ color: #d4d4d4;
1801
+ padding: 15px;
1802
+ border-radius: 8px;
1803
+ overflow-x: auto;
1804
+ }
1805
+ </style>
1806
+ </head>
1807
+ <body>
1808
+ <h1>SignalWire Agent</h1>
1809
+
1810
+ <div class="status">
1811
+ Your agent is running and ready to receive calls!
1812
+ </div>
1813
+
1814
+ <h2>Endpoints</h2>
1815
+
1816
+ <div class="endpoint">
1817
+ <h3><span class="method post">POST</span> /swml</h3>
1818
+ <p>Main SWML endpoint. Point your SignalWire phone number here.</p>
1819
+ <pre>curl -X POST http://localhost:5000/swml \\
1820
+ -H "Content-Type: application/json" \\
1821
+ -d '{}'</pre>
1822
+ </div>
1823
+
1824
+ <div class="endpoint">
1825
+ <h3><span class="method post">POST</span> /swml/swaig/</h3>
1826
+ <p>SWAIG function endpoint for tool calls.</p>
1827
+ </div>
1828
+
1829
+ <h2>Quick Start</h2>
1830
+ <pre># Test the SWML endpoint
1831
+ curl -X POST http://localhost:5000/swml -H "Content-Type: application/json" -d '{}'
1832
+
1833
+ # Run tests
1834
+ pytest tests/ -v</pre>
1835
+
1836
+ <p>Powered by <a href="https://signalwire.com">SignalWire</a> and the
1837
+ <a href="https://github.com/signalwire/signalwire-agents">SignalWire Agents SDK</a></p>
1838
+ </body>
1839
+ </html>
1840
+ '''
1841
+
1842
+
1843
+ # =============================================================================
1844
+ # Project Generator
1845
+ # =============================================================================
1846
+
1847
+ class ProjectGenerator:
1848
+ """Generates a new SignalWire agent project."""
1849
+
1850
+ def __init__(self, config: Dict[str, Any]):
1851
+ self.config = config
1852
+ self.project_dir = Path(config['project_dir'])
1853
+ self.project_name = config['project_name']
1854
+ self.features = config['features']
1855
+ self.credentials = config.get('credentials', {})
1856
+ self.platform = config.get('platform', 'local')
1857
+ self.cloud_config = config.get('cloud_config', {})
1858
+
1859
+ def generate(self) -> bool:
1860
+ """Generate the project. Returns True on success."""
1861
+ try:
1862
+ if self.platform == 'local':
1863
+ return self._generate_local()
1864
+ elif self.platform == 'aws':
1865
+ return self._generate_aws()
1866
+ elif self.platform == 'gcp':
1867
+ return self._generate_gcp()
1868
+ elif self.platform == 'azure':
1869
+ return self._generate_azure()
1870
+ else:
1871
+ print_error(f"Unknown platform: {self.platform}")
1872
+ return False
1873
+ except Exception as e:
1874
+ print_error(f"Failed to generate project: {e}")
1875
+ return False
1876
+
1877
+ def _generate_local(self) -> bool:
1878
+ """Generate a local agent project."""
1879
+ self._create_directories()
1880
+ self._create_agent_files()
1881
+ self._create_app_file()
1882
+ self._create_config_files()
1883
+
1884
+ if self.features.get('tests'):
1885
+ self._create_test_files()
1886
+
1887
+ if self.features.get('web_ui'):
1888
+ self._create_web_files()
1889
+
1890
+ self._create_readme()
1891
+
1892
+ if self.config.get('create_venv'):
1893
+ self._create_virtualenv()
1894
+
1895
+ return True
1896
+
1897
+ def _generate_aws(self) -> bool:
1898
+ """Generate an AWS Lambda project."""
1899
+ self.project_dir.mkdir(parents=True, exist_ok=True)
1900
+ print_success(f"Created {self.project_dir}/")
1901
+
1902
+ # Get template variables
1903
+ template_vars = self._get_template_vars()
1904
+
1905
+ # handler.py
1906
+ handler_code = AWS_HANDLER_TEMPLATE.format(**template_vars)
1907
+ (self.project_dir / 'handler.py').write_text(handler_code)
1908
+ print_success("Created handler.py")
1909
+
1910
+ # requirements.txt
1911
+ (self.project_dir / 'requirements.txt').write_text(AWS_REQUIREMENTS_TEMPLATE)
1912
+ print_success("Created requirements.txt")
1913
+
1914
+ # deploy.sh
1915
+ deploy_code = AWS_DEPLOY_TEMPLATE.format(**template_vars)
1916
+ deploy_path = self.project_dir / 'deploy.sh'
1917
+ deploy_path.write_text(deploy_code)
1918
+ deploy_path.chmod(0o755)
1919
+ print_success("Created deploy.sh")
1920
+
1921
+ # .env.example
1922
+ self._create_cloud_env_example('aws')
1923
+
1924
+ # .gitignore
1925
+ (self.project_dir / '.gitignore').write_text(TEMPLATE_GITIGNORE)
1926
+ print_success("Created .gitignore")
1927
+
1928
+ # README.md
1929
+ self._create_cloud_readme('aws')
1930
+
1931
+ return True
1932
+
1933
+ def _generate_gcp(self) -> bool:
1934
+ """Generate a Google Cloud Function project."""
1935
+ self.project_dir.mkdir(parents=True, exist_ok=True)
1936
+ print_success(f"Created {self.project_dir}/")
1937
+
1938
+ # Get template variables
1939
+ template_vars = self._get_template_vars()
1940
+
1941
+ # main.py
1942
+ main_code = GCP_MAIN_TEMPLATE.format(**template_vars)
1943
+ (self.project_dir / 'main.py').write_text(main_code)
1944
+ print_success("Created main.py")
1945
+
1946
+ # requirements.txt
1947
+ (self.project_dir / 'requirements.txt').write_text(GCP_REQUIREMENTS_TEMPLATE)
1948
+ print_success("Created requirements.txt")
1949
+
1950
+ # deploy.sh
1951
+ deploy_code = GCP_DEPLOY_TEMPLATE.format(**template_vars)
1952
+ deploy_path = self.project_dir / 'deploy.sh'
1953
+ deploy_path.write_text(deploy_code)
1954
+ deploy_path.chmod(0o755)
1955
+ print_success("Created deploy.sh")
1956
+
1957
+ # .env.example
1958
+ self._create_cloud_env_example('gcp')
1959
+
1960
+ # .gitignore
1961
+ (self.project_dir / '.gitignore').write_text(TEMPLATE_GITIGNORE)
1962
+ print_success("Created .gitignore")
1963
+
1964
+ # README.md
1965
+ self._create_cloud_readme('gcp')
1966
+
1967
+ return True
1968
+
1969
+ def _generate_azure(self) -> bool:
1970
+ """Generate an Azure Functions project."""
1971
+ self.project_dir.mkdir(parents=True, exist_ok=True)
1972
+ print_success(f"Created {self.project_dir}/")
1973
+
1974
+ # Create function_app directory
1975
+ function_dir = self.project_dir / 'function_app'
1976
+ function_dir.mkdir(exist_ok=True)
1977
+
1978
+ # Get template variables
1979
+ template_vars = self._get_template_vars()
1980
+
1981
+ # function_app/__init__.py
1982
+ init_code = AZURE_INIT_TEMPLATE.format(**template_vars)
1983
+ (function_dir / '__init__.py').write_text(init_code)
1984
+ print_success("Created function_app/__init__.py")
1985
+
1986
+ # function_app/function.json
1987
+ (function_dir / 'function.json').write_text(AZURE_FUNCTION_JSON_TEMPLATE)
1988
+ print_success("Created function_app/function.json")
1989
+
1990
+ # host.json
1991
+ (self.project_dir / 'host.json').write_text(AZURE_HOST_JSON_TEMPLATE)
1992
+ print_success("Created host.json")
1993
+
1994
+ # local.settings.json
1995
+ (self.project_dir / 'local.settings.json').write_text(AZURE_LOCAL_SETTINGS_TEMPLATE)
1996
+ print_success("Created local.settings.json")
1997
+
1998
+ # requirements.txt
1999
+ (self.project_dir / 'requirements.txt').write_text(AZURE_REQUIREMENTS_TEMPLATE)
2000
+ print_success("Created requirements.txt")
2001
+
2002
+ # deploy.sh
2003
+ deploy_code = AZURE_DEPLOY_TEMPLATE.format(**template_vars)
2004
+ deploy_path = self.project_dir / 'deploy.sh'
2005
+ deploy_path.write_text(deploy_code)
2006
+ deploy_path.chmod(0o755)
2007
+ print_success("Created deploy.sh")
2008
+
2009
+ # .env.example
2010
+ self._create_cloud_env_example('azure')
2011
+
2012
+ # .gitignore
2013
+ (self.project_dir / '.gitignore').write_text(TEMPLATE_GITIGNORE)
2014
+ print_success("Created .gitignore")
2015
+
2016
+ # README.md
2017
+ self._create_cloud_readme('azure')
2018
+
2019
+ return True
2020
+
2021
+ def _get_template_vars(self) -> Dict[str, str]:
2022
+ """Get template variables for cloud function templates."""
2023
+ # Convert project name to various formats
2024
+ agent_name = self.project_name
2025
+ agent_name_slug = self.project_name.lower().replace(' ', '-').replace('_', '-')
2026
+ agent_class = ''.join(word.capitalize() for word in self.project_name.replace('-', ' ').replace('_', ' ').split()) + 'Agent'
2027
+ function_name = agent_name_slug
2028
+
2029
+ # Auth credentials
2030
+ auth_user = 'admin'
2031
+ auth_password = generate_password(16)
2032
+
2033
+ return {
2034
+ 'agent_name': agent_name,
2035
+ 'agent_name_slug': agent_name_slug,
2036
+ 'agent_class': agent_class,
2037
+ 'function_name': function_name,
2038
+ 'region': self.cloud_config.get('region', DEFAULT_REGIONS.get(self.platform, '')),
2039
+ 'resource_group': self.cloud_config.get('resource_group', f'{function_name}-rg'),
2040
+ 'auth_user': auth_user,
2041
+ 'auth_password': auth_password,
2042
+ }
2043
+
2044
+ def _create_cloud_env_example(self, platform: str):
2045
+ """Create .env.example for cloud platforms."""
2046
+ env_content = '''# Optional: Basic authentication credentials
2047
+ # If not set, the SDK will auto-generate secure credentials
2048
+ SWML_BASIC_AUTH_USER=admin
2049
+ SWML_BASIC_AUTH_PASSWORD=your-secure-password
2050
+ '''
2051
+ (self.project_dir / '.env.example').write_text(env_content)
2052
+ print_success("Created .env.example")
2053
+
2054
+ def _create_cloud_readme(self, platform: str):
2055
+ """Create README.md for cloud platforms."""
2056
+ template_vars = self._get_template_vars()
2057
+ platform_names = {'aws': 'AWS Lambda', 'gcp': 'Google Cloud Functions', 'azure': 'Azure Functions'}
2058
+ platform_name = platform_names.get(platform, platform)
2059
+
2060
+ if platform == 'aws':
2061
+ readme = f'''# {self.project_name}
2062
+
2063
+ A SignalWire AI Agent deployed on {platform_name}.
2064
+
2065
+ ## Prerequisites
2066
+
2067
+ - AWS CLI configured with appropriate credentials
2068
+ - Docker installed and running
2069
+
2070
+ ## Quick Start
2071
+
2072
+ ```bash
2073
+ cd {self.project_name}
2074
+ ./deploy.sh
2075
+ ```
2076
+
2077
+ ## Deployment Options
2078
+
2079
+ ```bash
2080
+ # Deploy with defaults
2081
+ ./deploy.sh
2082
+
2083
+ # Custom function name
2084
+ ./deploy.sh my-function
2085
+
2086
+ # Custom function and region
2087
+ ./deploy.sh my-function us-west-2
2088
+ ```
2089
+
2090
+ ## Project Structure
2091
+
2092
+ ```
2093
+ {self.project_name}/
2094
+ ├── handler.py # Lambda handler with agent
2095
+ ├── requirements.txt # Python dependencies
2096
+ ├── deploy.sh # Deployment script
2097
+ ├── .env.example # Environment template
2098
+ └── README.md
2099
+ ```
2100
+
2101
+ ## Testing
2102
+
2103
+ After deployment, test your function:
2104
+
2105
+ ```bash
2106
+ # Test SWML output
2107
+ curl -u admin:password https://YOUR-API-ID.execute-api.REGION.amazonaws.com/
2108
+
2109
+ # Test SWAIG function
2110
+ curl -u admin:password -X POST https://YOUR-API-ID.execute-api.REGION.amazonaws.com/swaig \\
2111
+ -H 'Content-Type: application/json' \\
2112
+ -d '{{"function": "get_info", "argument": {{"parsed": [{{"topic": "test"}}]}}}}'
2113
+ ```
2114
+
2115
+ ## Configure SignalWire
2116
+
2117
+ Set your phone number's SWML URL to the endpoint URL shown after deployment.
2118
+ '''
2119
+
2120
+ elif platform == 'gcp':
2121
+ readme = f'''# {self.project_name}
2122
+
2123
+ A SignalWire AI Agent deployed on {platform_name}.
2124
+
2125
+ ## Prerequisites
2126
+
2127
+ - gcloud CLI installed and authenticated
2128
+ - A Google Cloud project with Cloud Functions API enabled
2129
+
2130
+ ## Quick Start
2131
+
2132
+ ```bash
2133
+ cd {self.project_name}
2134
+ ./deploy.sh
2135
+ ```
2136
+
2137
+ ## Deployment Options
2138
+
2139
+ ```bash
2140
+ # Deploy with defaults
2141
+ ./deploy.sh
2142
+
2143
+ # Custom function name
2144
+ ./deploy.sh my-function
2145
+
2146
+ # Custom function and region
2147
+ ./deploy.sh my-function us-central1
2148
+ ```
2149
+
2150
+ ## Project Structure
2151
+
2152
+ ```
2153
+ {self.project_name}/
2154
+ ├── main.py # Cloud Function entry point
2155
+ ├── requirements.txt # Python dependencies
2156
+ ├── deploy.sh # Deployment script
2157
+ ├── .env.example # Environment template
2158
+ └── README.md
2159
+ ```
2160
+
2161
+ ## Testing
2162
+
2163
+ After deployment, test your function:
2164
+
2165
+ ```bash
2166
+ # Test SWML output
2167
+ curl https://YOUR-FUNCTION-URL
2168
+
2169
+ # Test SWAIG function
2170
+ curl -X POST https://YOUR-FUNCTION-URL/swaig \\
2171
+ -H 'Content-Type: application/json' \\
2172
+ -d '{{"function": "get_info", "argument": {{"parsed": [{{"topic": "test"}}]}}}}'
2173
+ ```
2174
+
2175
+ ## Configure SignalWire
2176
+
2177
+ Set your phone number's SWML URL to the endpoint URL shown after deployment.
2178
+ '''
2179
+
2180
+ else: # azure
2181
+ readme = f'''# {self.project_name}
2182
+
2183
+ A SignalWire AI Agent deployed on {platform_name}.
2184
+
2185
+ ## Prerequisites
2186
+
2187
+ - Azure CLI installed and authenticated (`az login`)
2188
+ - Docker installed and running
2189
+
2190
+ ## Quick Start
2191
+
2192
+ ```bash
2193
+ cd {self.project_name}
2194
+ ./deploy.sh
2195
+ ```
2196
+
2197
+ ## Deployment Options
2198
+
2199
+ ```bash
2200
+ # Deploy with defaults
2201
+ ./deploy.sh
2202
+
2203
+ # Custom app name
2204
+ ./deploy.sh my-app
2205
+
2206
+ # Custom app, region, and resource group
2207
+ ./deploy.sh my-app eastus my-resource-group
2208
+ ```
2209
+
2210
+ ## Project Structure
2211
+
2212
+ ```
2213
+ {self.project_name}/
2214
+ ├── function_app/
2215
+ │ ├── __init__.py # Azure Function handler
2216
+ │ └── function.json # HTTP trigger config
2217
+ ├── host.json # Host configuration
2218
+ ├── local.settings.json # Local dev settings
2219
+ ├── requirements.txt # Python dependencies
2220
+ ├── deploy.sh # Deployment script
2221
+ ├── .env.example # Environment template
2222
+ └── README.md
2223
+ ```
2224
+
2225
+ ## Testing
2226
+
2227
+ After deployment, test your function:
2228
+
2229
+ ```bash
2230
+ # Test SWML output
2231
+ curl https://YOUR-APP.azurewebsites.net/api/function_app
2232
+
2233
+ # Test SWAIG function
2234
+ curl -X POST https://YOUR-APP.azurewebsites.net/api/function_app/swaig \\
2235
+ -H 'Content-Type: application/json' \\
2236
+ -d '{{"function": "get_info", "argument": {{"parsed": [{{"topic": "test"}}]}}}}'
2237
+ ```
2238
+
2239
+ ## Configure SignalWire
2240
+
2241
+ Set your phone number's SWML URL to the endpoint URL shown after deployment.
2242
+ '''
2243
+
2244
+ (self.project_dir / 'README.md').write_text(readme)
2245
+ print_success("Created README.md")
2246
+
2247
+ def _create_directories(self):
2248
+ """Create project directory structure."""
2249
+ dirs = ['agents', 'skills']
2250
+ if self.features.get('tests'):
2251
+ dirs.append('tests')
2252
+ if self.features.get('web_ui'):
2253
+ dirs.append('web')
2254
+
2255
+ self.project_dir.mkdir(parents=True, exist_ok=True)
2256
+ print_success(f"Created {self.project_dir}/")
2257
+
2258
+ for d in dirs:
2259
+ (self.project_dir / d).mkdir(exist_ok=True)
2260
+
2261
+ def _create_agent_files(self):
2262
+ """Create agent module files."""
2263
+ agents_dir = self.project_dir / 'agents'
2264
+
2265
+ # __init__.py
2266
+ (agents_dir / '__init__.py').write_text(TEMPLATE_AGENTS_INIT)
2267
+ print_success("Created agents/__init__.py")
2268
+
2269
+ # main_agent.py
2270
+ agent_code = get_agent_template(self.config.get('agent_type', 'basic'), self.features)
2271
+ (agents_dir / 'main_agent.py').write_text(agent_code)
2272
+ print_success("Created agents/main_agent.py")
2273
+
2274
+ # skills/__init__.py
2275
+ (self.project_dir / 'skills' / '__init__.py').write_text(TEMPLATE_SKILLS_INIT)
2276
+ print_success("Created skills/__init__.py")
2277
+
2278
+ def _create_app_file(self):
2279
+ """Create main app.py entry point."""
2280
+ app_code = get_app_template(self.features)
2281
+ (self.project_dir / 'app.py').write_text(app_code)
2282
+ print_success("Created app.py")
2283
+
2284
+ def _create_config_files(self):
2285
+ """Create configuration files."""
2286
+ # .env
2287
+ env_content = f'''# SignalWire Credentials
2288
+ SIGNALWIRE_SPACE_NAME={self.credentials.get('space', '')}
2289
+ SIGNALWIRE_PROJECT_ID={self.credentials.get('project', '')}
2290
+ SIGNALWIRE_TOKEN={self.credentials.get('token', '')}
2291
+
2292
+ # Agent Server Configuration
2293
+ HOST=0.0.0.0
2294
+ PORT=5000
2295
+
2296
+ # Agent name
2297
+ AGENT_NAME={self.project_name}
2298
+ '''
2299
+ if self.features.get('basic_auth'):
2300
+ env_content += f'''
2301
+ # Basic Auth for SWML webhooks
2302
+ SWML_BASIC_AUTH_USER=signalwire
2303
+ SWML_BASIC_AUTH_PASSWORD={generate_password()}
2304
+ '''
2305
+
2306
+ if self.features.get('debug_webhooks'):
2307
+ env_content += '''
2308
+ # Public URL (ngrok tunnel or production domain)
2309
+ SWML_PROXY_URL_BASE=
2310
+
2311
+ # Debug settings (0=off, 1=basic, 2=verbose)
2312
+ DEBUG_WEBHOOK_LEVEL=1
2313
+ '''
2314
+
2315
+ (self.project_dir / '.env').write_text(env_content)
2316
+ print_success("Created .env")
2317
+
2318
+ # .env.example
2319
+ (self.project_dir / '.env.example').write_text(TEMPLATE_ENV_EXAMPLE)
2320
+ print_success("Created .env.example")
2321
+
2322
+ # .gitignore
2323
+ (self.project_dir / '.gitignore').write_text(TEMPLATE_GITIGNORE)
2324
+ print_success("Created .gitignore")
2325
+
2326
+ # requirements.txt
2327
+ (self.project_dir / 'requirements.txt').write_text(TEMPLATE_REQUIREMENTS)
2328
+ print_success("Created requirements.txt")
2329
+
2330
+ def _create_test_files(self):
2331
+ """Create test files."""
2332
+ tests_dir = self.project_dir / 'tests'
2333
+
2334
+ (tests_dir / '__init__.py').write_text(TEMPLATE_TESTS_INIT)
2335
+ print_success("Created tests/__init__.py")
2336
+
2337
+ test_code = get_test_template(self.features.get('example_tool', True))
2338
+ (tests_dir / 'test_agent.py').write_text(test_code)
2339
+ print_success("Created tests/test_agent.py")
2340
+
2341
+ def _create_web_files(self):
2342
+ """Create web UI files."""
2343
+ web_dir = self.project_dir / 'web'
2344
+
2345
+ (web_dir / 'index.html').write_text(get_web_index_template())
2346
+ print_success("Created web/index.html")
2347
+
2348
+ def _create_readme(self):
2349
+ """Create README.md."""
2350
+ readme = get_readme_template(self.project_name, self.features)
2351
+ (self.project_dir / 'README.md').write_text(readme)
2352
+ print_success("Created README.md")
2353
+
2354
+ def _create_virtualenv(self):
2355
+ """Create and set up virtual environment."""
2356
+ venv_dir = self.project_dir / '.venv'
2357
+
2358
+ print_step("Creating virtual environment...")
2359
+ try:
2360
+ subprocess.run(
2361
+ [sys.executable, '-m', 'venv', str(venv_dir)],
2362
+ check=True,
2363
+ capture_output=True
2364
+ )
2365
+ print_success("Created virtual environment")
2366
+
2367
+ # Install dependencies
2368
+ print_step("Installing dependencies...")
2369
+ pip_path = venv_dir / 'bin' / 'pip'
2370
+ if sys.platform == 'win32':
2371
+ pip_path = venv_dir / 'Scripts' / 'pip.exe'
2372
+
2373
+ subprocess.run(
2374
+ [str(pip_path), 'install', '-q', '-r', str(self.project_dir / 'requirements.txt')],
2375
+ check=True,
2376
+ capture_output=True
2377
+ )
2378
+ print_success("Installed dependencies")
2379
+
2380
+ except subprocess.CalledProcessError as e:
2381
+ print_warning(f"Failed to set up virtual environment: {e}")
2382
+
2383
+
2384
+ # =============================================================================
2385
+ # CLI Entry Point
2386
+ # =============================================================================
2387
+
2388
+ def run_interactive() -> Dict[str, Any]:
2389
+ """Run interactive prompts and return configuration."""
2390
+ print(f"\n{Colors.BOLD}{Colors.CYAN}Welcome to SignalWire Agent Init!{Colors.NC}\n")
2391
+
2392
+ # Platform selection
2393
+ platform_options = list(CLOUD_PLATFORMS.values())
2394
+ platform_idx = prompt_select("Target platform:", platform_options, default=1)
2395
+ platform = list(CLOUD_PLATFORMS.keys())[platform_idx - 1]
2396
+
2397
+ # Project name
2398
+ default_name = "my-agent"
2399
+ project_name = prompt("Project name", default_name)
2400
+
2401
+ # Project directory
2402
+ default_dir = f"./{project_name}"
2403
+ project_dir = prompt("Project directory", default_dir)
2404
+ project_dir = os.path.abspath(os.path.expanduser(project_dir))
2405
+
2406
+ # Check if directory exists
2407
+ if os.path.exists(project_dir):
2408
+ if not prompt_yes_no(f"Directory {project_dir} exists. Overwrite?", default=False):
2409
+ print("Aborted.")
2410
+ sys.exit(0)
2411
+
2412
+ # Cloud-specific configuration
2413
+ cloud_config = {}
2414
+ if platform != 'local':
2415
+ default_region = DEFAULT_REGIONS.get(platform, '')
2416
+ cloud_config['region'] = prompt("Region", default_region)
2417
+
2418
+ if platform == 'azure':
2419
+ cloud_config['resource_group'] = prompt("Resource group", f"{project_name}-rg")
2420
+
2421
+ # Agent type (for local) or simplified for cloud
2422
+ if platform == 'local':
2423
+ agent_types = [
2424
+ "Basic - Minimal agent with example tool",
2425
+ "Full - Debug webhooks, web UI, all features",
2426
+ ]
2427
+ agent_type_idx = prompt_select("Agent type:", agent_types, default=1)
2428
+ agent_type = 'basic' if agent_type_idx == 1 else 'full'
2429
+
2430
+ # Set default features based on type
2431
+ if agent_type == 'full':
2432
+ default_features = [True, True, True, True, True, True]
2433
+ else:
2434
+ default_features = [False, False, False, True, True, False]
2435
+
2436
+ # Feature selection
2437
+ feature_names = [
2438
+ "Debug webhooks (console output)",
2439
+ "Post-prompt summary",
2440
+ "Web UI",
2441
+ "Example SWAIG tool",
2442
+ "Test scaffolding (pytest)",
2443
+ "Basic authentication",
2444
+ ]
2445
+ selected = prompt_multiselect("Include features:", feature_names, default_features)
2446
+
2447
+ features = {
2448
+ 'debug_webhooks': selected[0],
2449
+ 'post_prompt': selected[1],
2450
+ 'web_ui': selected[2],
2451
+ 'example_tool': selected[3],
2452
+ 'tests': selected[4],
2453
+ 'basic_auth': selected[5],
2454
+ }
2455
+ else:
2456
+ # Cloud platforms have simplified features
2457
+ agent_type = 'basic'
2458
+ features = {
2459
+ 'debug_webhooks': False,
2460
+ 'post_prompt': False,
2461
+ 'web_ui': False,
2462
+ 'example_tool': True,
2463
+ 'tests': False,
2464
+ 'basic_auth': prompt_yes_no("Enable basic authentication?", default=True),
2465
+ }
2466
+
2467
+ # Credentials (only for local)
2468
+ credentials = {'space': '', 'project': '', 'token': ''}
2469
+ if platform == 'local':
2470
+ env_creds = get_env_credentials()
2471
+
2472
+ if env_creds['space'] or env_creds['project'] or env_creds['token']:
2473
+ print(f"\n{Colors.GREEN}SignalWire credentials found in environment:{Colors.NC}")
2474
+ if env_creds['space']:
2475
+ print(f" Space: {env_creds['space']}")
2476
+ if env_creds['project']:
2477
+ print(f" Project: {env_creds['project'][:12]}...{env_creds['project'][-4:]}")
2478
+ if env_creds['token']:
2479
+ print(f" Token: {mask_token(env_creds['token'])}")
2480
+
2481
+ if prompt_yes_no("Use these credentials?", default=True):
2482
+ credentials = env_creds
2483
+ else:
2484
+ credentials['space'] = prompt("Space name", env_creds['space'])
2485
+ credentials['project'] = prompt("Project ID", env_creds['project'])
2486
+ credentials['token'] = prompt("Token", env_creds['token'])
2487
+ else:
2488
+ print(f"\n{Colors.YELLOW}No SignalWire credentials found in environment.{Colors.NC}")
2489
+ if prompt_yes_no("Enter credentials now?", default=False):
2490
+ credentials['space'] = prompt("Space name")
2491
+ credentials['project'] = prompt("Project ID")
2492
+ credentials['token'] = prompt("Token")
2493
+
2494
+ # Virtual environment (only for local)
2495
+ create_venv = False
2496
+ if platform == 'local':
2497
+ create_venv = prompt_yes_no("\nCreate virtual environment?", default=True)
2498
+
2499
+ return {
2500
+ 'project_name': project_name,
2501
+ 'project_dir': project_dir,
2502
+ 'platform': platform,
2503
+ 'agent_type': agent_type,
2504
+ 'features': features,
2505
+ 'credentials': credentials,
2506
+ 'create_venv': create_venv,
2507
+ 'cloud_config': cloud_config,
2508
+ }
2509
+
2510
+
2511
+ def run_quick(project_name: str, args: Any) -> Dict[str, Any]:
2512
+ """Run in quick mode with minimal prompts."""
2513
+ project_dir = os.path.abspath(os.path.join('.', project_name))
2514
+
2515
+ # Get platform from args
2516
+ platform = getattr(args, 'platform', 'local') or 'local'
2517
+
2518
+ # Get region from args or use default
2519
+ region = getattr(args, 'region', None)
2520
+ if not region and platform != 'local':
2521
+ region = DEFAULT_REGIONS.get(platform, '')
2522
+
2523
+ # Build cloud config
2524
+ cloud_config = {}
2525
+ if platform != 'local':
2526
+ cloud_config['region'] = region
2527
+ if platform == 'azure':
2528
+ cloud_config['resource_group'] = f"{project_name}-rg"
2529
+
2530
+ # Determine features from args
2531
+ agent_type = getattr(args, 'type', 'basic') or 'basic'
2532
+
2533
+ if platform == 'local':
2534
+ if agent_type == 'full':
2535
+ features = {
2536
+ 'debug_webhooks': True,
2537
+ 'post_prompt': True,
2538
+ 'web_ui': True,
2539
+ 'example_tool': True,
2540
+ 'tests': True,
2541
+ 'basic_auth': True,
2542
+ }
2543
+ else:
2544
+ features = {
2545
+ 'debug_webhooks': False,
2546
+ 'post_prompt': False,
2547
+ 'web_ui': False,
2548
+ 'example_tool': True,
2549
+ 'tests': True,
2550
+ 'basic_auth': False,
2551
+ }
2552
+ else:
2553
+ # Cloud platforms have simplified features
2554
+ features = {
2555
+ 'debug_webhooks': False,
2556
+ 'post_prompt': False,
2557
+ 'web_ui': False,
2558
+ 'example_tool': True,
2559
+ 'tests': False,
2560
+ 'basic_auth': True,
2561
+ }
2562
+
2563
+ # Get credentials from environment (only for local)
2564
+ credentials = get_env_credentials() if platform == 'local' else {'space': '', 'project': '', 'token': ''}
2565
+
2566
+ return {
2567
+ 'project_name': project_name,
2568
+ 'project_dir': project_dir,
2569
+ 'platform': platform,
2570
+ 'agent_type': agent_type,
2571
+ 'features': features,
2572
+ 'credentials': credentials,
2573
+ 'create_venv': not getattr(args, 'no_venv', False) and platform == 'local',
2574
+ 'cloud_config': cloud_config,
2575
+ }
2576
+
2577
+
2578
+ def main():
2579
+ """Main entry point."""
2580
+ import argparse
2581
+
2582
+ parser = argparse.ArgumentParser(
2583
+ description='Create a new SignalWire agent project',
2584
+ formatter_class=argparse.RawDescriptionHelpFormatter,
2585
+ epilog='''
2586
+ Examples:
2587
+ sw-agent-init Interactive mode
2588
+ sw-agent-init myagent Quick mode with defaults
2589
+ sw-agent-init myagent --type full Full-featured local agent
2590
+ sw-agent-init myagent -p aws AWS Lambda function
2591
+ sw-agent-init myagent -p gcp Google Cloud Function
2592
+ sw-agent-init myagent -p azure Azure Function
2593
+ sw-agent-init myagent -p aws -r us-west-2 Custom region
2594
+ '''
2595
+ )
2596
+ parser.add_argument('name', nargs='?', help='Project name')
2597
+ parser.add_argument('--type', choices=['basic', 'full'], default='basic',
2598
+ help='Agent type (default: basic)')
2599
+ parser.add_argument('--platform', '-p', choices=['local', 'aws', 'gcp', 'azure'],
2600
+ default='local', help='Target platform (default: local)')
2601
+ parser.add_argument('--region', '-r', help='Cloud region (e.g., us-east-1, us-central1, eastus)')
2602
+ parser.add_argument('--no-venv', action='store_true',
2603
+ help='Skip virtual environment creation')
2604
+ parser.add_argument('--dir', help='Parent directory for project')
2605
+
2606
+ args = parser.parse_args()
2607
+
2608
+ # Run interactive or quick mode
2609
+ if args.name:
2610
+ config = run_quick(args.name, args)
2611
+ if args.dir:
2612
+ config['project_dir'] = os.path.abspath(os.path.join(args.dir, args.name))
2613
+ else:
2614
+ config = run_interactive()
2615
+
2616
+ # Generate project
2617
+ print(f"\n{Colors.BOLD}Creating project '{config['project_name']}'...{Colors.NC}\n")
2618
+
2619
+ generator = ProjectGenerator(config)
2620
+ if generator.generate():
2621
+ print(f"\n{Colors.GREEN}{Colors.BOLD}Project created successfully!{Colors.NC}\n")
2622
+ print("To start your agent:\n")
2623
+ print(f" cd {config['project_dir']}")
2624
+ if config.get('create_venv'):
2625
+ if sys.platform == 'win32':
2626
+ print(" .venv\\Scripts\\activate")
2627
+ else:
2628
+ print(" source .venv/bin/activate")
2629
+ print(" python app.py")
2630
+ print()
2631
+ else:
2632
+ sys.exit(1)
2633
+
2634
+
2635
+ if __name__ == '__main__':
2636
+ main()