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.
- signalwire_agents/__init__.py +99 -15
- signalwire_agents/agent_server.py +248 -60
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/dokku.py +2320 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +2636 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +845 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +280 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +460 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1142 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +171 -203
- signalwire_agents/mcp_gateway/__init__.py +29 -0
- signalwire_agents/mcp_gateway/gateway_service.py +564 -0
- signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
- signalwire_agents/mcp_gateway/session_manager.py +218 -0
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +748 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +14 -2
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
- signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
- signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
- {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()
|