signalwire-agents 0.1.13__py3-none-any.whl → 1.0.7__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 +176 -23
- 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/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 +1225 -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 +825 -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 +287 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +368 -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 +1134 -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 +167 -200
- 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 +752 -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 +10 -1
- 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.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/METADATA +344 -215
- signalwire_agents-1.0.7.dist-info/RECORD +142 -0
- signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -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.7.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1225 @@
|
|
|
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
|
+
# ANSI colors
|
|
22
|
+
class Colors:
|
|
23
|
+
RED = '\033[0;31m'
|
|
24
|
+
GREEN = '\033[0;32m'
|
|
25
|
+
YELLOW = '\033[1;33m'
|
|
26
|
+
BLUE = '\033[0;34m'
|
|
27
|
+
CYAN = '\033[0;36m'
|
|
28
|
+
BOLD = '\033[1m'
|
|
29
|
+
DIM = '\033[2m'
|
|
30
|
+
NC = '\033[0m' # No Color
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def print_step(msg: str):
|
|
34
|
+
print(f"{Colors.BLUE}==>{Colors.NC} {msg}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_success(msg: str):
|
|
38
|
+
print(f"{Colors.GREEN}✓{Colors.NC} {msg}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def print_warning(msg: str):
|
|
42
|
+
print(f"{Colors.YELLOW}!{Colors.NC} {msg}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def print_error(msg: str):
|
|
46
|
+
print(f"{Colors.RED}✗{Colors.NC} {msg}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def prompt(question: str, default: str = "") -> str:
|
|
50
|
+
"""Prompt user for input with optional default."""
|
|
51
|
+
if default:
|
|
52
|
+
result = input(f"{question} [{default}]: ").strip()
|
|
53
|
+
return result if result else default
|
|
54
|
+
else:
|
|
55
|
+
return input(f"{question}: ").strip()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def prompt_yes_no(question: str, default: bool = True) -> bool:
|
|
59
|
+
"""Prompt user for yes/no answer."""
|
|
60
|
+
hint = "Y/n" if default else "y/N"
|
|
61
|
+
result = input(f"{question} [{hint}]: ").strip().lower()
|
|
62
|
+
if not result:
|
|
63
|
+
return default
|
|
64
|
+
return result in ('y', 'yes')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def prompt_select(question: str, options: List[str], default: int = 1) -> int:
|
|
68
|
+
"""Prompt user to select from numbered options. Returns 1-based index."""
|
|
69
|
+
print(f"\n{question}")
|
|
70
|
+
for i, opt in enumerate(options, 1):
|
|
71
|
+
print(f" {i}) {opt}")
|
|
72
|
+
while True:
|
|
73
|
+
result = input(f"Select [{default}]: ").strip()
|
|
74
|
+
if not result:
|
|
75
|
+
return default
|
|
76
|
+
try:
|
|
77
|
+
idx = int(result)
|
|
78
|
+
if 1 <= idx <= len(options):
|
|
79
|
+
return idx
|
|
80
|
+
except ValueError:
|
|
81
|
+
pass
|
|
82
|
+
print(f"Please enter a number between 1 and {len(options)}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def prompt_multiselect(question: str, options: List[str], defaults: List[bool]) -> List[bool]:
|
|
86
|
+
"""Prompt user to toggle multiple options. Returns list of booleans."""
|
|
87
|
+
selected = defaults.copy()
|
|
88
|
+
|
|
89
|
+
while True:
|
|
90
|
+
print(f"\n{question}")
|
|
91
|
+
for i, (opt, sel) in enumerate(zip(options, selected), 1):
|
|
92
|
+
marker = "x" if sel else " "
|
|
93
|
+
print(f" {i}) [{marker}] {opt}")
|
|
94
|
+
print(f" Enter number to toggle, or press Enter to continue")
|
|
95
|
+
|
|
96
|
+
result = input("Toggle: ").strip()
|
|
97
|
+
if not result:
|
|
98
|
+
return selected
|
|
99
|
+
try:
|
|
100
|
+
idx = int(result)
|
|
101
|
+
if 1 <= idx <= len(options):
|
|
102
|
+
selected[idx - 1] = not selected[idx - 1]
|
|
103
|
+
except ValueError:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def mask_token(token: str) -> str:
|
|
108
|
+
"""Mask a token showing only first 4 and last 3 characters."""
|
|
109
|
+
if len(token) <= 10:
|
|
110
|
+
return "*" * len(token)
|
|
111
|
+
return f"{token[:4]}...{token[-3:]}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_env_credentials() -> Dict[str, str]:
|
|
115
|
+
"""Get SignalWire credentials from environment variables."""
|
|
116
|
+
return {
|
|
117
|
+
'space': os.environ.get('SIGNALWIRE_SPACE_NAME', ''),
|
|
118
|
+
'project': os.environ.get('SIGNALWIRE_PROJECT_ID', ''),
|
|
119
|
+
'token': os.environ.get('SIGNALWIRE_TOKEN', ''),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def generate_password(length: int = 32) -> str:
|
|
124
|
+
"""Generate a secure random password."""
|
|
125
|
+
return secrets.token_urlsafe(length)[:length]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# =============================================================================
|
|
129
|
+
# Templates
|
|
130
|
+
# =============================================================================
|
|
131
|
+
|
|
132
|
+
TEMPLATE_AGENTS_INIT = '''from .main_agent import MainAgent
|
|
133
|
+
|
|
134
|
+
__all__ = ["MainAgent"]
|
|
135
|
+
'''
|
|
136
|
+
|
|
137
|
+
TEMPLATE_SKILLS_INIT = '''"""Skills module - Add reusable agent skills here."""
|
|
138
|
+
'''
|
|
139
|
+
|
|
140
|
+
TEMPLATE_TESTS_INIT = '''"""Test package."""
|
|
141
|
+
'''
|
|
142
|
+
|
|
143
|
+
TEMPLATE_GITIGNORE = '''# Environment
|
|
144
|
+
.env
|
|
145
|
+
.venv/
|
|
146
|
+
venv/
|
|
147
|
+
__pycache__/
|
|
148
|
+
*.pyc
|
|
149
|
+
*.pyo
|
|
150
|
+
|
|
151
|
+
# IDE
|
|
152
|
+
.vscode/
|
|
153
|
+
.idea/
|
|
154
|
+
*.swp
|
|
155
|
+
*.swo
|
|
156
|
+
|
|
157
|
+
# Testing
|
|
158
|
+
.pytest_cache/
|
|
159
|
+
.coverage
|
|
160
|
+
htmlcov/
|
|
161
|
+
|
|
162
|
+
# Build
|
|
163
|
+
dist/
|
|
164
|
+
build/
|
|
165
|
+
*.egg-info/
|
|
166
|
+
|
|
167
|
+
# Logs
|
|
168
|
+
*.log
|
|
169
|
+
|
|
170
|
+
# OS
|
|
171
|
+
.DS_Store
|
|
172
|
+
Thumbs.db
|
|
173
|
+
'''
|
|
174
|
+
|
|
175
|
+
TEMPLATE_ENV_EXAMPLE = '''# SignalWire Credentials
|
|
176
|
+
SIGNALWIRE_SPACE_NAME=your-space
|
|
177
|
+
SIGNALWIRE_PROJECT_ID=your-project-id
|
|
178
|
+
SIGNALWIRE_TOKEN=your-api-token
|
|
179
|
+
|
|
180
|
+
# Agent Server Configuration
|
|
181
|
+
HOST=0.0.0.0
|
|
182
|
+
PORT=5000
|
|
183
|
+
|
|
184
|
+
# Agent name (used for SWML handler - keeps the same handler across restarts)
|
|
185
|
+
AGENT_NAME=myagent
|
|
186
|
+
|
|
187
|
+
# Basic Auth for SWML webhooks (optional)
|
|
188
|
+
SWML_BASIC_AUTH_USER=signalwire
|
|
189
|
+
SWML_BASIC_AUTH_PASSWORD=your-secure-password
|
|
190
|
+
|
|
191
|
+
# Public URL (ngrok tunnel or production domain)
|
|
192
|
+
SWML_PROXY_URL_BASE=https://your-domain.ngrok.io
|
|
193
|
+
|
|
194
|
+
# Debug settings (0=off, 1=basic, 2=verbose)
|
|
195
|
+
DEBUG_WEBHOOK_LEVEL=1
|
|
196
|
+
'''
|
|
197
|
+
|
|
198
|
+
TEMPLATE_REQUIREMENTS = '''signalwire-agents>=1.0.6
|
|
199
|
+
python-dotenv>=1.0.0
|
|
200
|
+
requests>=2.28.0
|
|
201
|
+
pytest>=7.0.0
|
|
202
|
+
'''
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_agent_template(agent_type: str, features: Dict[str, bool]) -> str:
|
|
206
|
+
"""Generate the main agent template based on type and features."""
|
|
207
|
+
|
|
208
|
+
has_tool = features.get('example_tool', True)
|
|
209
|
+
has_debug = features.get('debug_webhooks', False)
|
|
210
|
+
has_auth = features.get('basic_auth', False)
|
|
211
|
+
|
|
212
|
+
imports = ['from signalwire_agents import AgentBase']
|
|
213
|
+
if has_tool:
|
|
214
|
+
imports.append('from signalwire_agents import SwaigFunctionResult')
|
|
215
|
+
|
|
216
|
+
imports_str = '\n'.join(imports)
|
|
217
|
+
|
|
218
|
+
# Build the __init__ method
|
|
219
|
+
init_parts = []
|
|
220
|
+
|
|
221
|
+
if has_auth:
|
|
222
|
+
init_parts.append('''
|
|
223
|
+
# Set basic auth if configured
|
|
224
|
+
user = os.getenv("SWML_BASIC_AUTH_USER")
|
|
225
|
+
password = os.getenv("SWML_BASIC_AUTH_PASSWORD")
|
|
226
|
+
if user and password:
|
|
227
|
+
self.set_params({
|
|
228
|
+
"swml_basic_auth_user": user,
|
|
229
|
+
"swml_basic_auth_password": password,
|
|
230
|
+
})''')
|
|
231
|
+
|
|
232
|
+
init_parts.append('''
|
|
233
|
+
self._configure_voice()
|
|
234
|
+
self._configure_prompts()''')
|
|
235
|
+
|
|
236
|
+
if has_debug:
|
|
237
|
+
init_parts.append('''
|
|
238
|
+
self._configure_debug_webhooks()''')
|
|
239
|
+
|
|
240
|
+
init_body = ''.join(init_parts)
|
|
241
|
+
|
|
242
|
+
# Build optional methods
|
|
243
|
+
extra_methods = []
|
|
244
|
+
|
|
245
|
+
if has_debug:
|
|
246
|
+
extra_methods.append('''
|
|
247
|
+
|
|
248
|
+
def _configure_debug_webhooks(self):
|
|
249
|
+
"""Set up debug and post-prompt webhooks."""
|
|
250
|
+
proxy_url = os.getenv("SWML_PROXY_URL_BASE", "")
|
|
251
|
+
debug_level = int(os.getenv("DEBUG_WEBHOOK_LEVEL", "1"))
|
|
252
|
+
auth_user = os.getenv("SWML_BASIC_AUTH_USER", "")
|
|
253
|
+
auth_pass = os.getenv("SWML_BASIC_AUTH_PASSWORD", "")
|
|
254
|
+
|
|
255
|
+
if proxy_url and debug_level > 0:
|
|
256
|
+
# Build URL with basic auth credentials
|
|
257
|
+
if auth_user and auth_pass:
|
|
258
|
+
if "://" in proxy_url:
|
|
259
|
+
scheme, rest = proxy_url.split("://", 1)
|
|
260
|
+
auth_proxy_url = f"{scheme}://{auth_user}:{auth_pass}@{rest}"
|
|
261
|
+
else:
|
|
262
|
+
auth_proxy_url = f"{auth_user}:{auth_pass}@{proxy_url}"
|
|
263
|
+
else:
|
|
264
|
+
auth_proxy_url = proxy_url
|
|
265
|
+
|
|
266
|
+
self.set_params({
|
|
267
|
+
"debug_webhook_url": f"{auth_proxy_url}/debug",
|
|
268
|
+
"debug_webhook_level": debug_level,
|
|
269
|
+
})
|
|
270
|
+
self.set_post_prompt(
|
|
271
|
+
"Summarize the conversation including: "
|
|
272
|
+
"the caller's main request, any actions taken, "
|
|
273
|
+
"and the outcome of the call."
|
|
274
|
+
)
|
|
275
|
+
self.set_post_prompt_url(f"{auth_proxy_url}/post_prompt")
|
|
276
|
+
|
|
277
|
+
def on_summary(self, summary):
|
|
278
|
+
"""Handle call summary."""
|
|
279
|
+
print(f"Call summary: {summary}")
|
|
280
|
+
''')
|
|
281
|
+
|
|
282
|
+
if has_tool:
|
|
283
|
+
extra_methods.append('''
|
|
284
|
+
|
|
285
|
+
@AgentBase.tool(
|
|
286
|
+
name="get_info",
|
|
287
|
+
description="Get information about a topic",
|
|
288
|
+
parameters={
|
|
289
|
+
"type": "object",
|
|
290
|
+
"properties": {
|
|
291
|
+
"topic": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"description": "The topic to get information about"
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
"required": ["topic"]
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
def get_info(self, args, raw_data):
|
|
300
|
+
"""Get information about a topic."""
|
|
301
|
+
topic = args.get("topic", "")
|
|
302
|
+
# TODO: Implement your logic here
|
|
303
|
+
return SwaigFunctionResult(f"Information about {topic}: This is a placeholder response.")''')
|
|
304
|
+
|
|
305
|
+
extra_methods_str = ''.join(extra_methods)
|
|
306
|
+
|
|
307
|
+
# Need os import if auth or debug
|
|
308
|
+
os_import = 'import os\n' if (has_auth or has_debug) else ''
|
|
309
|
+
|
|
310
|
+
return f'''#!/usr/bin/env python3
|
|
311
|
+
"""Main Agent - SignalWire AI Agent"""
|
|
312
|
+
|
|
313
|
+
{os_import}{imports_str}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class MainAgent(AgentBase):
|
|
317
|
+
"""Main voice AI agent."""
|
|
318
|
+
|
|
319
|
+
def __init__(self):
|
|
320
|
+
super().__init__(
|
|
321
|
+
name="main-agent",
|
|
322
|
+
route="/swml"
|
|
323
|
+
)
|
|
324
|
+
{init_body}
|
|
325
|
+
|
|
326
|
+
def _configure_voice(self):
|
|
327
|
+
"""Set up voice and language."""
|
|
328
|
+
self.add_language("English", "en-US", "rime.spore")
|
|
329
|
+
|
|
330
|
+
self.set_params({{
|
|
331
|
+
"end_of_speech_timeout": 500,
|
|
332
|
+
"attention_timeout": 15000,
|
|
333
|
+
}})
|
|
334
|
+
|
|
335
|
+
def _configure_prompts(self):
|
|
336
|
+
"""Set up AI prompts."""
|
|
337
|
+
self.prompt_add_section(
|
|
338
|
+
"Role",
|
|
339
|
+
"You are a helpful AI assistant. "
|
|
340
|
+
"Help callers with their questions and requests."
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
self.prompt_add_section(
|
|
344
|
+
"Guidelines",
|
|
345
|
+
body="Follow these guidelines:",
|
|
346
|
+
bullets=[
|
|
347
|
+
"Be professional and courteous",
|
|
348
|
+
"Ask clarifying questions when needed",
|
|
349
|
+
"Keep responses concise and helpful",
|
|
350
|
+
"If you cannot help, offer to transfer to a human"
|
|
351
|
+
]
|
|
352
|
+
)
|
|
353
|
+
{extra_methods_str}
|
|
354
|
+
'''
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def get_app_template(features: Dict[str, bool]) -> str:
|
|
358
|
+
"""Generate the app.py template based on features."""
|
|
359
|
+
|
|
360
|
+
has_debug = features.get('debug_webhooks', False)
|
|
361
|
+
has_web_ui = features.get('web_ui', False)
|
|
362
|
+
|
|
363
|
+
# Base imports
|
|
364
|
+
imports = [
|
|
365
|
+
'import os',
|
|
366
|
+
'from pathlib import Path',
|
|
367
|
+
'from dotenv import load_dotenv',
|
|
368
|
+
'',
|
|
369
|
+
'# Load environment variables from .env file',
|
|
370
|
+
'load_dotenv()',
|
|
371
|
+
'',
|
|
372
|
+
'from signalwire_agents import AgentServer',
|
|
373
|
+
'from agents import MainAgent',
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
if has_debug:
|
|
377
|
+
imports.insert(1, 'import sys')
|
|
378
|
+
imports.insert(2, 'import json')
|
|
379
|
+
imports.insert(3, 'from datetime import datetime')
|
|
380
|
+
imports.insert(4, 'from starlette.requests import Request')
|
|
381
|
+
|
|
382
|
+
imports_str = '\n'.join(imports)
|
|
383
|
+
|
|
384
|
+
# Debug webhook code
|
|
385
|
+
debug_code = ''
|
|
386
|
+
if has_debug:
|
|
387
|
+
debug_code = '''
|
|
388
|
+
|
|
389
|
+
# ANSI colors for console output
|
|
390
|
+
RESET = "\\033[0m"
|
|
391
|
+
BOLD = "\\033[1m"
|
|
392
|
+
DIM = "\\033[2m"
|
|
393
|
+
CYAN = "\\033[36m"
|
|
394
|
+
GREEN = "\\033[32m"
|
|
395
|
+
YELLOW = "\\033[33m"
|
|
396
|
+
MAGENTA = "\\033[35m"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def print_separator(char="-", width=80):
|
|
400
|
+
print(f"{DIM}{char * width}{RESET}")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def print_debug_data(data):
|
|
404
|
+
"""Pretty print debug webhook data."""
|
|
405
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
406
|
+
print()
|
|
407
|
+
print_separator("=")
|
|
408
|
+
print(f"{BOLD}{CYAN}> DEBUG WEBHOOK{RESET}")
|
|
409
|
+
print(f"{DIM}{timestamp}{RESET}")
|
|
410
|
+
print_separator()
|
|
411
|
+
|
|
412
|
+
if isinstance(data, dict):
|
|
413
|
+
event_type = data.get("event_type", data.get("type", "unknown"))
|
|
414
|
+
print(f"{YELLOW}Event:{RESET} {event_type}")
|
|
415
|
+
|
|
416
|
+
call_id = data.get("call_id", data.get("CallSid", ""))
|
|
417
|
+
if call_id:
|
|
418
|
+
print(f"{YELLOW}Call ID:{RESET} {call_id}")
|
|
419
|
+
|
|
420
|
+
if "conversation" in data:
|
|
421
|
+
print(f"\\n{BOLD}{YELLOW}Conversation:{RESET}")
|
|
422
|
+
for msg in data.get("conversation", [])[-5:]:
|
|
423
|
+
role = msg.get("role", "?")
|
|
424
|
+
content = msg.get("content", "")[:100]
|
|
425
|
+
color = GREEN if role == "assistant" else MAGENTA
|
|
426
|
+
print(f" {color}{role}:{RESET} {content}")
|
|
427
|
+
|
|
428
|
+
debug_level = int(os.getenv("DEBUG_WEBHOOK_LEVEL", "1"))
|
|
429
|
+
if debug_level >= 2:
|
|
430
|
+
print(f"\\n{BOLD}{YELLOW}Full Data:{RESET}")
|
|
431
|
+
formatted = json.dumps(data, indent=2)
|
|
432
|
+
for line in formatted.split('\\n')[:50]:
|
|
433
|
+
print(f" {DIM}{line}{RESET}")
|
|
434
|
+
else:
|
|
435
|
+
print(f"{DIM}{data}{RESET}")
|
|
436
|
+
|
|
437
|
+
print_separator("=")
|
|
438
|
+
print()
|
|
439
|
+
sys.stdout.flush()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def print_post_prompt_data(data):
|
|
443
|
+
"""Pretty print post-prompt summary data."""
|
|
444
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
445
|
+
print()
|
|
446
|
+
print_separator("=")
|
|
447
|
+
print(f"{BOLD}{YELLOW}> POST-PROMPT SUMMARY{RESET}")
|
|
448
|
+
print(f"{DIM}{timestamp}{RESET}")
|
|
449
|
+
print_separator()
|
|
450
|
+
|
|
451
|
+
if isinstance(data, dict):
|
|
452
|
+
summary = data.get("post_prompt_data", {})
|
|
453
|
+
if isinstance(summary, dict):
|
|
454
|
+
for key, value in summary.items():
|
|
455
|
+
print(f"{GREEN}{key}:{RESET} {value}")
|
|
456
|
+
elif summary:
|
|
457
|
+
print(f"{GREEN}Summary:{RESET} {summary}")
|
|
458
|
+
|
|
459
|
+
raw = data.get("raw_response", data.get("response", ""))
|
|
460
|
+
if raw:
|
|
461
|
+
print(f"\\n{BOLD}{YELLOW}Response:{RESET}")
|
|
462
|
+
print(f" {MAGENTA}{raw}{RESET}")
|
|
463
|
+
|
|
464
|
+
call_id = data.get("call_id", "")
|
|
465
|
+
if call_id:
|
|
466
|
+
print(f"\\n{DIM}Call ID: {call_id}{RESET}")
|
|
467
|
+
else:
|
|
468
|
+
print(f"{DIM}{data}{RESET}")
|
|
469
|
+
|
|
470
|
+
print_separator("=")
|
|
471
|
+
print()
|
|
472
|
+
sys.stdout.flush()
|
|
473
|
+
'''
|
|
474
|
+
|
|
475
|
+
# Main function
|
|
476
|
+
main_body_parts = ['''
|
|
477
|
+
def main():
|
|
478
|
+
host = os.getenv("HOST", "0.0.0.0")
|
|
479
|
+
port = int(os.getenv("PORT", "5000"))
|
|
480
|
+
|
|
481
|
+
# Create server and register agent
|
|
482
|
+
server = AgentServer(host=host, port=port)
|
|
483
|
+
server.register(MainAgent())
|
|
484
|
+
''']
|
|
485
|
+
|
|
486
|
+
if has_web_ui:
|
|
487
|
+
main_body_parts.append('''
|
|
488
|
+
# Serve static files from web/ directory
|
|
489
|
+
web_dir = Path(__file__).parent / "web"
|
|
490
|
+
if web_dir.exists():
|
|
491
|
+
server.serve_static_files(str(web_dir))
|
|
492
|
+
''')
|
|
493
|
+
|
|
494
|
+
if has_debug:
|
|
495
|
+
main_body_parts.append('''
|
|
496
|
+
# Add debug webhook endpoint
|
|
497
|
+
@server.app.post('/debug')
|
|
498
|
+
async def debug_webhook(request: Request):
|
|
499
|
+
"""Receive and display debug webhook data."""
|
|
500
|
+
try:
|
|
501
|
+
data = await request.json()
|
|
502
|
+
except:
|
|
503
|
+
body = await request.body()
|
|
504
|
+
data = body.decode('utf-8', errors='ignore')
|
|
505
|
+
print_debug_data(data)
|
|
506
|
+
return {'status': 'received'}
|
|
507
|
+
|
|
508
|
+
# Add post-prompt webhook endpoint
|
|
509
|
+
@server.app.post('/post_prompt')
|
|
510
|
+
async def post_prompt_webhook(request: Request):
|
|
511
|
+
"""Receive and display post-prompt summary data."""
|
|
512
|
+
try:
|
|
513
|
+
data = await request.json()
|
|
514
|
+
except:
|
|
515
|
+
body = await request.body()
|
|
516
|
+
data = body.decode('utf-8', errors='ignore')
|
|
517
|
+
print_post_prompt_data(data)
|
|
518
|
+
return {'status': 'received'}
|
|
519
|
+
''')
|
|
520
|
+
|
|
521
|
+
main_body_parts.append('''
|
|
522
|
+
# Print startup info
|
|
523
|
+
print(f"\\nSignalWire Agent Server")
|
|
524
|
+
print(f"SWML endpoint: http://{host}:{port}/swml")
|
|
525
|
+
print(f"SWAIG endpoint: http://{host}:{port}/swml/swaig/")
|
|
526
|
+
print()
|
|
527
|
+
|
|
528
|
+
server.run()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
if __name__ == "__main__":
|
|
532
|
+
main()
|
|
533
|
+
''')
|
|
534
|
+
|
|
535
|
+
main_body = ''.join(main_body_parts)
|
|
536
|
+
|
|
537
|
+
return f'''#!/usr/bin/env python3
|
|
538
|
+
"""Main entry point for the agent server."""
|
|
539
|
+
|
|
540
|
+
{imports_str}
|
|
541
|
+
{debug_code}
|
|
542
|
+
{main_body}'''
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def get_test_template(has_tool: bool) -> str:
|
|
546
|
+
"""Generate test template."""
|
|
547
|
+
|
|
548
|
+
tool_tests = ''
|
|
549
|
+
if has_tool:
|
|
550
|
+
tool_tests = '''
|
|
551
|
+
|
|
552
|
+
class TestFunctionExecution:
|
|
553
|
+
"""Test that SWAIG functions can be executed."""
|
|
554
|
+
|
|
555
|
+
def test_get_info_function(self):
|
|
556
|
+
"""Test get_info function executes successfully."""
|
|
557
|
+
returncode, stdout, stderr = run_swaig_test(
|
|
558
|
+
"--exec", "get_info", "--topic", "SignalWire"
|
|
559
|
+
)
|
|
560
|
+
assert returncode == 0, f"Function execution failed: {stderr}"
|
|
561
|
+
assert "SignalWire" in stdout, f"Expected 'SignalWire' in output: {stdout}"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class TestDirectImport:
|
|
565
|
+
"""Test direct Python import of the agent."""
|
|
566
|
+
|
|
567
|
+
def test_agent_creation(self):
|
|
568
|
+
"""Test that agent can be instantiated."""
|
|
569
|
+
from agents import MainAgent
|
|
570
|
+
agent = MainAgent()
|
|
571
|
+
assert agent is not None
|
|
572
|
+
assert agent.name == "main-agent"
|
|
573
|
+
|
|
574
|
+
def test_get_info_tool_direct(self):
|
|
575
|
+
"""Test the get_info tool via direct call."""
|
|
576
|
+
from agents import MainAgent
|
|
577
|
+
agent = MainAgent()
|
|
578
|
+
result = agent.get_info({"topic": "test"}, {})
|
|
579
|
+
assert "test" in result.response
|
|
580
|
+
'''
|
|
581
|
+
|
|
582
|
+
tool_check = '''
|
|
583
|
+
def test_agent_has_tools(self):
|
|
584
|
+
"""Test agent has expected tools defined."""
|
|
585
|
+
tools = list_tools()
|
|
586
|
+
assert "get_info" in tools, f"Missing get_info tool. Found: {tools}"
|
|
587
|
+
''' if has_tool else ''
|
|
588
|
+
|
|
589
|
+
return f'''#!/usr/bin/env python3
|
|
590
|
+
"""
|
|
591
|
+
Tests for the main agent using swaig-test.
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
import subprocess
|
|
595
|
+
import json
|
|
596
|
+
import sys
|
|
597
|
+
from pathlib import Path
|
|
598
|
+
|
|
599
|
+
import pytest
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
AGENT_FILE = Path(__file__).parent.parent / "agents" / "main_agent.py"
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def run_swaig_test(*args) -> tuple:
|
|
606
|
+
"""Run swaig-test on the agent and return (returncode, stdout, stderr)."""
|
|
607
|
+
cmd = [sys.executable, "-m", "signalwire_agents.cli.swaig_test_wrapper", str(AGENT_FILE)] + list(args)
|
|
608
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
609
|
+
return result.returncode, result.stdout, result.stderr
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def get_swml_json() -> dict:
|
|
613
|
+
"""Get SWML JSON output from the agent."""
|
|
614
|
+
returncode, stdout, stderr = run_swaig_test("--dump-swml", "--raw")
|
|
615
|
+
if returncode != 0:
|
|
616
|
+
pytest.fail(f"swaig-test failed:\\nstderr: {{stderr}}\\nstdout: {{stdout}}")
|
|
617
|
+
try:
|
|
618
|
+
return json.loads(stdout)
|
|
619
|
+
except json.JSONDecodeError as e:
|
|
620
|
+
pytest.fail(f"Invalid JSON: {{e}}\\nOutput: {{stdout}}")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def list_tools() -> list:
|
|
624
|
+
"""List tools available in the agent."""
|
|
625
|
+
returncode, stdout, stderr = run_swaig_test("--list-tools")
|
|
626
|
+
if returncode != 0:
|
|
627
|
+
return []
|
|
628
|
+
tools = []
|
|
629
|
+
for line in stdout.split('\\n'):
|
|
630
|
+
line = line.strip()
|
|
631
|
+
if ' - ' in line and not line.startswith('Parameters:'):
|
|
632
|
+
parts = line.split(' - ')
|
|
633
|
+
if parts:
|
|
634
|
+
tool_name = parts[0].strip()
|
|
635
|
+
if tool_name and not tool_name.startswith('('):
|
|
636
|
+
tools.append(tool_name)
|
|
637
|
+
return tools
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class TestAgentLoading:
|
|
641
|
+
"""Test that the agent can be loaded without errors."""
|
|
642
|
+
|
|
643
|
+
def test_agent_loads(self):
|
|
644
|
+
"""Test agent file can be loaded by swaig-test."""
|
|
645
|
+
returncode, stdout, stderr = run_swaig_test("--list-tools")
|
|
646
|
+
assert returncode == 0, f"Failed to load agent: {{stderr}}"
|
|
647
|
+
{tool_check}
|
|
648
|
+
|
|
649
|
+
class TestSWMLGeneration:
|
|
650
|
+
"""Test that the agent generates valid SWML documents."""
|
|
651
|
+
|
|
652
|
+
def test_swml_structure(self):
|
|
653
|
+
"""Test SWML has required structure."""
|
|
654
|
+
swml = get_swml_json()
|
|
655
|
+
assert "version" in swml, "SWML missing 'version'"
|
|
656
|
+
assert "sections" in swml, "SWML missing 'sections'"
|
|
657
|
+
assert "main" in swml["sections"], "SWML missing 'sections.main'"
|
|
658
|
+
|
|
659
|
+
def test_swml_has_ai_section(self):
|
|
660
|
+
"""Test SWML has AI configuration."""
|
|
661
|
+
swml = get_swml_json()
|
|
662
|
+
main_section = swml.get("sections", {{}}).get("main", [])
|
|
663
|
+
ai_found = any("ai" in verb for verb in main_section)
|
|
664
|
+
assert ai_found, "SWML missing 'ai' verb"
|
|
665
|
+
{tool_tests}
|
|
666
|
+
|
|
667
|
+
if __name__ == "__main__":
|
|
668
|
+
pytest.main([__file__, "-v"])
|
|
669
|
+
'''
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def get_readme_template(project_name: str, features: Dict[str, bool]) -> str:
|
|
673
|
+
"""Generate README template."""
|
|
674
|
+
|
|
675
|
+
endpoints = [
|
|
676
|
+
"| `/swml` | POST | Main SWML endpoint - point your SignalWire phone number here |",
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
if features.get('debug_webhooks'):
|
|
680
|
+
endpoints.append("| `/debug` | POST | Debug webhook - receives real-time call data |")
|
|
681
|
+
endpoints.append("| `/post_prompt` | POST | Post-prompt webhook - receives call summaries |")
|
|
682
|
+
|
|
683
|
+
if features.get('web_ui'):
|
|
684
|
+
endpoints.append("| `/` | GET | Static files from `web/` directory |")
|
|
685
|
+
|
|
686
|
+
endpoints_table = '\n'.join(endpoints)
|
|
687
|
+
|
|
688
|
+
return f'''# {project_name}
|
|
689
|
+
|
|
690
|
+
A SignalWire AI Agent built with signalwire-agents.
|
|
691
|
+
|
|
692
|
+
## Quick Start
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
cd {project_name}
|
|
696
|
+
source .venv/bin/activate # If using virtual environment
|
|
697
|
+
python app.py
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
## Endpoints
|
|
701
|
+
|
|
702
|
+
| Endpoint | Method | Description |
|
|
703
|
+
|----------|--------|-------------|
|
|
704
|
+
{endpoints_table}
|
|
705
|
+
|
|
706
|
+
## Project Structure
|
|
707
|
+
|
|
708
|
+
```
|
|
709
|
+
{project_name}/
|
|
710
|
+
├── agents/
|
|
711
|
+
│ ├── __init__.py
|
|
712
|
+
│ └── main_agent.py # Main agent implementation
|
|
713
|
+
├── skills/
|
|
714
|
+
│ └── __init__.py # Reusable skills
|
|
715
|
+
├── tests/
|
|
716
|
+
│ └── test_agent.py # Test suite
|
|
717
|
+
├── web/ # Static files
|
|
718
|
+
├── app.py # Entry point
|
|
719
|
+
├── .env # Environment configuration
|
|
720
|
+
└── requirements.txt # Python dependencies
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## Configuration
|
|
724
|
+
|
|
725
|
+
Edit `.env` to configure:
|
|
726
|
+
|
|
727
|
+
| Variable | Description |
|
|
728
|
+
|----------|-------------|
|
|
729
|
+
| `SIGNALWIRE_SPACE_NAME` | Your SignalWire space name |
|
|
730
|
+
| `SIGNALWIRE_PROJECT_ID` | Your SignalWire project ID |
|
|
731
|
+
| `SIGNALWIRE_TOKEN` | Your SignalWire API token |
|
|
732
|
+
| `HOST` | Server host (default: 0.0.0.0) |
|
|
733
|
+
| `PORT` | Server port (default: 5000) |
|
|
734
|
+
|
|
735
|
+
## Testing
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
pytest tests/ -v
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
## Adding Tools
|
|
742
|
+
|
|
743
|
+
Add new tools to your agent using the `@AgentBase.tool` decorator:
|
|
744
|
+
|
|
745
|
+
```python
|
|
746
|
+
@AgentBase.tool(
|
|
747
|
+
name="my_tool",
|
|
748
|
+
description="What this tool does",
|
|
749
|
+
parameters={{
|
|
750
|
+
"type": "object",
|
|
751
|
+
"properties": {{
|
|
752
|
+
"param1": {{"type": "string", "description": "Parameter description"}}
|
|
753
|
+
}},
|
|
754
|
+
"required": ["param1"]
|
|
755
|
+
}}
|
|
756
|
+
)
|
|
757
|
+
def my_tool(self, args, raw_data):
|
|
758
|
+
param1 = args.get("param1")
|
|
759
|
+
return SwaigFunctionResult(f"Result: {{param1}}")
|
|
760
|
+
```
|
|
761
|
+
'''
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def get_web_index_template() -> str:
|
|
765
|
+
"""Generate a simple web UI template."""
|
|
766
|
+
return '''<!DOCTYPE html>
|
|
767
|
+
<html>
|
|
768
|
+
<head>
|
|
769
|
+
<title>SignalWire Agent</title>
|
|
770
|
+
<style>
|
|
771
|
+
body {
|
|
772
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
773
|
+
max-width: 800px;
|
|
774
|
+
margin: 0 auto;
|
|
775
|
+
padding: 40px 20px;
|
|
776
|
+
background: #f8f9fa;
|
|
777
|
+
color: #333;
|
|
778
|
+
}
|
|
779
|
+
h1 {
|
|
780
|
+
color: #044cf6;
|
|
781
|
+
border-bottom: 3px solid #044cf6;
|
|
782
|
+
padding-bottom: 10px;
|
|
783
|
+
}
|
|
784
|
+
.status {
|
|
785
|
+
background: #d4edda;
|
|
786
|
+
border: 1px solid #c3e6cb;
|
|
787
|
+
color: #155724;
|
|
788
|
+
padding: 15px;
|
|
789
|
+
border-radius: 8px;
|
|
790
|
+
margin: 20px 0;
|
|
791
|
+
}
|
|
792
|
+
.endpoint {
|
|
793
|
+
background: white;
|
|
794
|
+
border: 1px solid #ddd;
|
|
795
|
+
border-radius: 8px;
|
|
796
|
+
padding: 20px;
|
|
797
|
+
margin: 15px 0;
|
|
798
|
+
}
|
|
799
|
+
.endpoint h3 {
|
|
800
|
+
margin-top: 0;
|
|
801
|
+
color: #044cf6;
|
|
802
|
+
}
|
|
803
|
+
.method {
|
|
804
|
+
display: inline-block;
|
|
805
|
+
padding: 4px 10px;
|
|
806
|
+
border-radius: 4px;
|
|
807
|
+
font-weight: bold;
|
|
808
|
+
font-size: 12px;
|
|
809
|
+
margin-right: 10px;
|
|
810
|
+
}
|
|
811
|
+
.method.post { background: #49cc90; color: white; }
|
|
812
|
+
code {
|
|
813
|
+
background: #e9ecef;
|
|
814
|
+
padding: 2px 6px;
|
|
815
|
+
border-radius: 4px;
|
|
816
|
+
font-size: 14px;
|
|
817
|
+
}
|
|
818
|
+
pre {
|
|
819
|
+
background: #1e1e1e;
|
|
820
|
+
color: #d4d4d4;
|
|
821
|
+
padding: 15px;
|
|
822
|
+
border-radius: 8px;
|
|
823
|
+
overflow-x: auto;
|
|
824
|
+
}
|
|
825
|
+
</style>
|
|
826
|
+
</head>
|
|
827
|
+
<body>
|
|
828
|
+
<h1>SignalWire Agent</h1>
|
|
829
|
+
|
|
830
|
+
<div class="status">
|
|
831
|
+
Your agent is running and ready to receive calls!
|
|
832
|
+
</div>
|
|
833
|
+
|
|
834
|
+
<h2>Endpoints</h2>
|
|
835
|
+
|
|
836
|
+
<div class="endpoint">
|
|
837
|
+
<h3><span class="method post">POST</span> /swml</h3>
|
|
838
|
+
<p>Main SWML endpoint. Point your SignalWire phone number here.</p>
|
|
839
|
+
<pre>curl -X POST http://localhost:5000/swml \\
|
|
840
|
+
-H "Content-Type: application/json" \\
|
|
841
|
+
-d '{}'</pre>
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
<div class="endpoint">
|
|
845
|
+
<h3><span class="method post">POST</span> /swml/swaig/</h3>
|
|
846
|
+
<p>SWAIG function endpoint for tool calls.</p>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<h2>Quick Start</h2>
|
|
850
|
+
<pre># Test the SWML endpoint
|
|
851
|
+
curl -X POST http://localhost:5000/swml -H "Content-Type: application/json" -d '{}'
|
|
852
|
+
|
|
853
|
+
# Run tests
|
|
854
|
+
pytest tests/ -v</pre>
|
|
855
|
+
|
|
856
|
+
<p>Powered by <a href="https://signalwire.com">SignalWire</a> and the
|
|
857
|
+
<a href="https://github.com/signalwire/signalwire-agents">SignalWire Agents SDK</a></p>
|
|
858
|
+
</body>
|
|
859
|
+
</html>
|
|
860
|
+
'''
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
# =============================================================================
|
|
864
|
+
# Project Generator
|
|
865
|
+
# =============================================================================
|
|
866
|
+
|
|
867
|
+
class ProjectGenerator:
|
|
868
|
+
"""Generates a new SignalWire agent project."""
|
|
869
|
+
|
|
870
|
+
def __init__(self, config: Dict[str, Any]):
|
|
871
|
+
self.config = config
|
|
872
|
+
self.project_dir = Path(config['project_dir'])
|
|
873
|
+
self.project_name = config['project_name']
|
|
874
|
+
self.features = config['features']
|
|
875
|
+
self.credentials = config['credentials']
|
|
876
|
+
|
|
877
|
+
def generate(self) -> bool:
|
|
878
|
+
"""Generate the project. Returns True on success."""
|
|
879
|
+
try:
|
|
880
|
+
self._create_directories()
|
|
881
|
+
self._create_agent_files()
|
|
882
|
+
self._create_app_file()
|
|
883
|
+
self._create_config_files()
|
|
884
|
+
|
|
885
|
+
if self.features.get('tests'):
|
|
886
|
+
self._create_test_files()
|
|
887
|
+
|
|
888
|
+
if self.features.get('web_ui'):
|
|
889
|
+
self._create_web_files()
|
|
890
|
+
|
|
891
|
+
self._create_readme()
|
|
892
|
+
|
|
893
|
+
if self.config.get('create_venv'):
|
|
894
|
+
self._create_virtualenv()
|
|
895
|
+
|
|
896
|
+
return True
|
|
897
|
+
except Exception as e:
|
|
898
|
+
print_error(f"Failed to generate project: {e}")
|
|
899
|
+
return False
|
|
900
|
+
|
|
901
|
+
def _create_directories(self):
|
|
902
|
+
"""Create project directory structure."""
|
|
903
|
+
dirs = ['agents', 'skills']
|
|
904
|
+
if self.features.get('tests'):
|
|
905
|
+
dirs.append('tests')
|
|
906
|
+
if self.features.get('web_ui'):
|
|
907
|
+
dirs.append('web')
|
|
908
|
+
|
|
909
|
+
self.project_dir.mkdir(parents=True, exist_ok=True)
|
|
910
|
+
print_success(f"Created {self.project_dir}/")
|
|
911
|
+
|
|
912
|
+
for d in dirs:
|
|
913
|
+
(self.project_dir / d).mkdir(exist_ok=True)
|
|
914
|
+
|
|
915
|
+
def _create_agent_files(self):
|
|
916
|
+
"""Create agent module files."""
|
|
917
|
+
agents_dir = self.project_dir / 'agents'
|
|
918
|
+
|
|
919
|
+
# __init__.py
|
|
920
|
+
(agents_dir / '__init__.py').write_text(TEMPLATE_AGENTS_INIT)
|
|
921
|
+
print_success("Created agents/__init__.py")
|
|
922
|
+
|
|
923
|
+
# main_agent.py
|
|
924
|
+
agent_code = get_agent_template(self.config.get('agent_type', 'basic'), self.features)
|
|
925
|
+
(agents_dir / 'main_agent.py').write_text(agent_code)
|
|
926
|
+
print_success("Created agents/main_agent.py")
|
|
927
|
+
|
|
928
|
+
# skills/__init__.py
|
|
929
|
+
(self.project_dir / 'skills' / '__init__.py').write_text(TEMPLATE_SKILLS_INIT)
|
|
930
|
+
print_success("Created skills/__init__.py")
|
|
931
|
+
|
|
932
|
+
def _create_app_file(self):
|
|
933
|
+
"""Create main app.py entry point."""
|
|
934
|
+
app_code = get_app_template(self.features)
|
|
935
|
+
(self.project_dir / 'app.py').write_text(app_code)
|
|
936
|
+
print_success("Created app.py")
|
|
937
|
+
|
|
938
|
+
def _create_config_files(self):
|
|
939
|
+
"""Create configuration files."""
|
|
940
|
+
# .env
|
|
941
|
+
env_content = f'''# SignalWire Credentials
|
|
942
|
+
SIGNALWIRE_SPACE_NAME={self.credentials.get('space', '')}
|
|
943
|
+
SIGNALWIRE_PROJECT_ID={self.credentials.get('project', '')}
|
|
944
|
+
SIGNALWIRE_TOKEN={self.credentials.get('token', '')}
|
|
945
|
+
|
|
946
|
+
# Agent Server Configuration
|
|
947
|
+
HOST=0.0.0.0
|
|
948
|
+
PORT=5000
|
|
949
|
+
|
|
950
|
+
# Agent name
|
|
951
|
+
AGENT_NAME={self.project_name}
|
|
952
|
+
'''
|
|
953
|
+
if self.features.get('basic_auth'):
|
|
954
|
+
env_content += f'''
|
|
955
|
+
# Basic Auth for SWML webhooks
|
|
956
|
+
SWML_BASIC_AUTH_USER=signalwire
|
|
957
|
+
SWML_BASIC_AUTH_PASSWORD={generate_password()}
|
|
958
|
+
'''
|
|
959
|
+
|
|
960
|
+
if self.features.get('debug_webhooks'):
|
|
961
|
+
env_content += '''
|
|
962
|
+
# Public URL (ngrok tunnel or production domain)
|
|
963
|
+
SWML_PROXY_URL_BASE=
|
|
964
|
+
|
|
965
|
+
# Debug settings (0=off, 1=basic, 2=verbose)
|
|
966
|
+
DEBUG_WEBHOOK_LEVEL=1
|
|
967
|
+
'''
|
|
968
|
+
|
|
969
|
+
(self.project_dir / '.env').write_text(env_content)
|
|
970
|
+
print_success("Created .env")
|
|
971
|
+
|
|
972
|
+
# .env.example
|
|
973
|
+
(self.project_dir / '.env.example').write_text(TEMPLATE_ENV_EXAMPLE)
|
|
974
|
+
print_success("Created .env.example")
|
|
975
|
+
|
|
976
|
+
# .gitignore
|
|
977
|
+
(self.project_dir / '.gitignore').write_text(TEMPLATE_GITIGNORE)
|
|
978
|
+
print_success("Created .gitignore")
|
|
979
|
+
|
|
980
|
+
# requirements.txt
|
|
981
|
+
(self.project_dir / 'requirements.txt').write_text(TEMPLATE_REQUIREMENTS)
|
|
982
|
+
print_success("Created requirements.txt")
|
|
983
|
+
|
|
984
|
+
def _create_test_files(self):
|
|
985
|
+
"""Create test files."""
|
|
986
|
+
tests_dir = self.project_dir / 'tests'
|
|
987
|
+
|
|
988
|
+
(tests_dir / '__init__.py').write_text(TEMPLATE_TESTS_INIT)
|
|
989
|
+
print_success("Created tests/__init__.py")
|
|
990
|
+
|
|
991
|
+
test_code = get_test_template(self.features.get('example_tool', True))
|
|
992
|
+
(tests_dir / 'test_agent.py').write_text(test_code)
|
|
993
|
+
print_success("Created tests/test_agent.py")
|
|
994
|
+
|
|
995
|
+
def _create_web_files(self):
|
|
996
|
+
"""Create web UI files."""
|
|
997
|
+
web_dir = self.project_dir / 'web'
|
|
998
|
+
|
|
999
|
+
(web_dir / 'index.html').write_text(get_web_index_template())
|
|
1000
|
+
print_success("Created web/index.html")
|
|
1001
|
+
|
|
1002
|
+
def _create_readme(self):
|
|
1003
|
+
"""Create README.md."""
|
|
1004
|
+
readme = get_readme_template(self.project_name, self.features)
|
|
1005
|
+
(self.project_dir / 'README.md').write_text(readme)
|
|
1006
|
+
print_success("Created README.md")
|
|
1007
|
+
|
|
1008
|
+
def _create_virtualenv(self):
|
|
1009
|
+
"""Create and set up virtual environment."""
|
|
1010
|
+
venv_dir = self.project_dir / '.venv'
|
|
1011
|
+
|
|
1012
|
+
print_step("Creating virtual environment...")
|
|
1013
|
+
try:
|
|
1014
|
+
subprocess.run(
|
|
1015
|
+
[sys.executable, '-m', 'venv', str(venv_dir)],
|
|
1016
|
+
check=True,
|
|
1017
|
+
capture_output=True
|
|
1018
|
+
)
|
|
1019
|
+
print_success("Created virtual environment")
|
|
1020
|
+
|
|
1021
|
+
# Install dependencies
|
|
1022
|
+
print_step("Installing dependencies...")
|
|
1023
|
+
pip_path = venv_dir / 'bin' / 'pip'
|
|
1024
|
+
if sys.platform == 'win32':
|
|
1025
|
+
pip_path = venv_dir / 'Scripts' / 'pip.exe'
|
|
1026
|
+
|
|
1027
|
+
subprocess.run(
|
|
1028
|
+
[str(pip_path), 'install', '-q', '-r', str(self.project_dir / 'requirements.txt')],
|
|
1029
|
+
check=True,
|
|
1030
|
+
capture_output=True
|
|
1031
|
+
)
|
|
1032
|
+
print_success("Installed dependencies")
|
|
1033
|
+
|
|
1034
|
+
except subprocess.CalledProcessError as e:
|
|
1035
|
+
print_warning(f"Failed to set up virtual environment: {e}")
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
# =============================================================================
|
|
1039
|
+
# CLI Entry Point
|
|
1040
|
+
# =============================================================================
|
|
1041
|
+
|
|
1042
|
+
def run_interactive() -> Dict[str, Any]:
|
|
1043
|
+
"""Run interactive prompts and return configuration."""
|
|
1044
|
+
print(f"\n{Colors.BOLD}{Colors.CYAN}Welcome to SignalWire Agent Init!{Colors.NC}\n")
|
|
1045
|
+
|
|
1046
|
+
# Project name
|
|
1047
|
+
default_name = "my-agent"
|
|
1048
|
+
project_name = prompt("Project name", default_name)
|
|
1049
|
+
|
|
1050
|
+
# Project directory
|
|
1051
|
+
default_dir = f"./{project_name}"
|
|
1052
|
+
project_dir = prompt("Project directory", default_dir)
|
|
1053
|
+
project_dir = os.path.abspath(os.path.expanduser(project_dir))
|
|
1054
|
+
|
|
1055
|
+
# Check if directory exists
|
|
1056
|
+
if os.path.exists(project_dir):
|
|
1057
|
+
if not prompt_yes_no(f"Directory {project_dir} exists. Overwrite?", default=False):
|
|
1058
|
+
print("Aborted.")
|
|
1059
|
+
sys.exit(0)
|
|
1060
|
+
|
|
1061
|
+
# Agent type
|
|
1062
|
+
agent_types = [
|
|
1063
|
+
"Basic - Minimal agent with example tool",
|
|
1064
|
+
"Full - Debug webhooks, web UI, all features",
|
|
1065
|
+
]
|
|
1066
|
+
agent_type_idx = prompt_select("Agent type:", agent_types, default=1)
|
|
1067
|
+
agent_type = 'basic' if agent_type_idx == 1 else 'full'
|
|
1068
|
+
|
|
1069
|
+
# Set default features based on type
|
|
1070
|
+
if agent_type == 'full':
|
|
1071
|
+
default_features = [True, True, True, True, True, True]
|
|
1072
|
+
else:
|
|
1073
|
+
default_features = [False, False, False, True, True, False]
|
|
1074
|
+
|
|
1075
|
+
# Feature selection
|
|
1076
|
+
feature_names = [
|
|
1077
|
+
"Debug webhooks (console output)",
|
|
1078
|
+
"Post-prompt summary",
|
|
1079
|
+
"Web UI",
|
|
1080
|
+
"Example SWAIG tool",
|
|
1081
|
+
"Test scaffolding (pytest)",
|
|
1082
|
+
"Basic authentication",
|
|
1083
|
+
]
|
|
1084
|
+
selected = prompt_multiselect("Include features:", feature_names, default_features)
|
|
1085
|
+
|
|
1086
|
+
features = {
|
|
1087
|
+
'debug_webhooks': selected[0],
|
|
1088
|
+
'post_prompt': selected[1],
|
|
1089
|
+
'web_ui': selected[2],
|
|
1090
|
+
'example_tool': selected[3],
|
|
1091
|
+
'tests': selected[4],
|
|
1092
|
+
'basic_auth': selected[5],
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
# Credentials
|
|
1096
|
+
env_creds = get_env_credentials()
|
|
1097
|
+
credentials = {'space': '', 'project': '', 'token': ''}
|
|
1098
|
+
|
|
1099
|
+
if env_creds['space'] or env_creds['project'] or env_creds['token']:
|
|
1100
|
+
print(f"\n{Colors.GREEN}SignalWire credentials found in environment:{Colors.NC}")
|
|
1101
|
+
if env_creds['space']:
|
|
1102
|
+
print(f" Space: {env_creds['space']}")
|
|
1103
|
+
if env_creds['project']:
|
|
1104
|
+
print(f" Project: {env_creds['project'][:12]}...{env_creds['project'][-4:]}")
|
|
1105
|
+
if env_creds['token']:
|
|
1106
|
+
print(f" Token: {mask_token(env_creds['token'])}")
|
|
1107
|
+
|
|
1108
|
+
if prompt_yes_no("Use these credentials?", default=True):
|
|
1109
|
+
credentials = env_creds
|
|
1110
|
+
else:
|
|
1111
|
+
credentials['space'] = prompt("Space name", env_creds['space'])
|
|
1112
|
+
credentials['project'] = prompt("Project ID", env_creds['project'])
|
|
1113
|
+
credentials['token'] = prompt("Token", env_creds['token'])
|
|
1114
|
+
else:
|
|
1115
|
+
print(f"\n{Colors.YELLOW}No SignalWire credentials found in environment.{Colors.NC}")
|
|
1116
|
+
if prompt_yes_no("Enter credentials now?", default=False):
|
|
1117
|
+
credentials['space'] = prompt("Space name")
|
|
1118
|
+
credentials['project'] = prompt("Project ID")
|
|
1119
|
+
credentials['token'] = prompt("Token")
|
|
1120
|
+
|
|
1121
|
+
# Virtual environment
|
|
1122
|
+
create_venv = prompt_yes_no("\nCreate virtual environment?", default=True)
|
|
1123
|
+
|
|
1124
|
+
return {
|
|
1125
|
+
'project_name': project_name,
|
|
1126
|
+
'project_dir': project_dir,
|
|
1127
|
+
'agent_type': agent_type,
|
|
1128
|
+
'features': features,
|
|
1129
|
+
'credentials': credentials,
|
|
1130
|
+
'create_venv': create_venv,
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def run_quick(project_name: str, args: Any) -> Dict[str, Any]:
|
|
1135
|
+
"""Run in quick mode with minimal prompts."""
|
|
1136
|
+
project_dir = os.path.abspath(os.path.join('.', project_name))
|
|
1137
|
+
|
|
1138
|
+
# Determine features from args
|
|
1139
|
+
agent_type = getattr(args, 'type', 'basic') or 'basic'
|
|
1140
|
+
|
|
1141
|
+
if agent_type == 'full':
|
|
1142
|
+
features = {
|
|
1143
|
+
'debug_webhooks': True,
|
|
1144
|
+
'post_prompt': True,
|
|
1145
|
+
'web_ui': True,
|
|
1146
|
+
'example_tool': True,
|
|
1147
|
+
'tests': True,
|
|
1148
|
+
'basic_auth': True,
|
|
1149
|
+
}
|
|
1150
|
+
else:
|
|
1151
|
+
features = {
|
|
1152
|
+
'debug_webhooks': False,
|
|
1153
|
+
'post_prompt': False,
|
|
1154
|
+
'web_ui': False,
|
|
1155
|
+
'example_tool': True,
|
|
1156
|
+
'tests': True,
|
|
1157
|
+
'basic_auth': False,
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
# Get credentials from environment
|
|
1161
|
+
credentials = get_env_credentials()
|
|
1162
|
+
|
|
1163
|
+
return {
|
|
1164
|
+
'project_name': project_name,
|
|
1165
|
+
'project_dir': project_dir,
|
|
1166
|
+
'agent_type': agent_type,
|
|
1167
|
+
'features': features,
|
|
1168
|
+
'credentials': credentials,
|
|
1169
|
+
'create_venv': not getattr(args, 'no_venv', False),
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def main():
|
|
1174
|
+
"""Main entry point."""
|
|
1175
|
+
import argparse
|
|
1176
|
+
|
|
1177
|
+
parser = argparse.ArgumentParser(
|
|
1178
|
+
description='Create a new SignalWire agent project',
|
|
1179
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1180
|
+
epilog='''
|
|
1181
|
+
Examples:
|
|
1182
|
+
sw-agent-init Interactive mode
|
|
1183
|
+
sw-agent-init myagent Quick mode with defaults
|
|
1184
|
+
sw-agent-init myagent --type full
|
|
1185
|
+
sw-agent-init myagent --no-venv
|
|
1186
|
+
'''
|
|
1187
|
+
)
|
|
1188
|
+
parser.add_argument('name', nargs='?', help='Project name')
|
|
1189
|
+
parser.add_argument('--type', choices=['basic', 'full'], default='basic',
|
|
1190
|
+
help='Agent type (default: basic)')
|
|
1191
|
+
parser.add_argument('--no-venv', action='store_true',
|
|
1192
|
+
help='Skip virtual environment creation')
|
|
1193
|
+
parser.add_argument('--dir', help='Parent directory for project')
|
|
1194
|
+
|
|
1195
|
+
args = parser.parse_args()
|
|
1196
|
+
|
|
1197
|
+
# Run interactive or quick mode
|
|
1198
|
+
if args.name:
|
|
1199
|
+
config = run_quick(args.name, args)
|
|
1200
|
+
if args.dir:
|
|
1201
|
+
config['project_dir'] = os.path.abspath(os.path.join(args.dir, args.name))
|
|
1202
|
+
else:
|
|
1203
|
+
config = run_interactive()
|
|
1204
|
+
|
|
1205
|
+
# Generate project
|
|
1206
|
+
print(f"\n{Colors.BOLD}Creating project '{config['project_name']}'...{Colors.NC}\n")
|
|
1207
|
+
|
|
1208
|
+
generator = ProjectGenerator(config)
|
|
1209
|
+
if generator.generate():
|
|
1210
|
+
print(f"\n{Colors.GREEN}{Colors.BOLD}Project created successfully!{Colors.NC}\n")
|
|
1211
|
+
print("To start your agent:\n")
|
|
1212
|
+
print(f" cd {config['project_dir']}")
|
|
1213
|
+
if config.get('create_venv'):
|
|
1214
|
+
if sys.platform == 'win32':
|
|
1215
|
+
print(" .venv\\Scripts\\activate")
|
|
1216
|
+
else:
|
|
1217
|
+
print(" source .venv/bin/activate")
|
|
1218
|
+
print(" python app.py")
|
|
1219
|
+
print()
|
|
1220
|
+
else:
|
|
1221
|
+
sys.exit(1)
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
if __name__ == '__main__':
|
|
1225
|
+
main()
|