signalwire-agents 0.1.6__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.
Files changed (140) hide show
  1. signalwire_agents/__init__.py +130 -4
  2. signalwire_agents/agent_server.py +438 -32
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +18 -0
  5. signalwire_agents/cli/build_search.py +1367 -0
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/execution/__init__.py +10 -0
  13. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  14. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  15. signalwire_agents/cli/init_project.py +1225 -0
  16. signalwire_agents/cli/output/__init__.py +10 -0
  17. signalwire_agents/cli/output/output_formatter.py +255 -0
  18. signalwire_agents/cli/output/swml_dump.py +186 -0
  19. signalwire_agents/cli/simulation/__init__.py +10 -0
  20. signalwire_agents/cli/simulation/data_generation.py +374 -0
  21. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  22. signalwire_agents/cli/simulation/mock_env.py +282 -0
  23. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  24. signalwire_agents/cli/test_swaig.py +809 -0
  25. signalwire_agents/cli/types.py +81 -0
  26. signalwire_agents/core/__init__.py +2 -2
  27. signalwire_agents/core/agent/__init__.py +12 -0
  28. signalwire_agents/core/agent/config/__init__.py +12 -0
  29. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  30. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  31. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  32. signalwire_agents/core/agent/prompt/manager.py +306 -0
  33. signalwire_agents/core/agent/routing/__init__.py +9 -0
  34. signalwire_agents/core/agent/security/__init__.py +9 -0
  35. signalwire_agents/core/agent/swml/__init__.py +9 -0
  36. signalwire_agents/core/agent/tools/__init__.py +15 -0
  37. signalwire_agents/core/agent/tools/decorator.py +97 -0
  38. signalwire_agents/core/agent/tools/registry.py +210 -0
  39. signalwire_agents/core/agent_base.py +959 -2166
  40. signalwire_agents/core/auth_handler.py +233 -0
  41. signalwire_agents/core/config_loader.py +259 -0
  42. signalwire_agents/core/contexts.py +707 -0
  43. signalwire_agents/core/data_map.py +487 -0
  44. signalwire_agents/core/function_result.py +1150 -1
  45. signalwire_agents/core/logging_config.py +376 -0
  46. signalwire_agents/core/mixins/__init__.py +28 -0
  47. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  48. signalwire_agents/core/mixins/auth_mixin.py +287 -0
  49. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  50. signalwire_agents/core/mixins/serverless_mixin.py +368 -0
  51. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  52. signalwire_agents/core/mixins/state_mixin.py +153 -0
  53. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  54. signalwire_agents/core/mixins/web_mixin.py +1134 -0
  55. signalwire_agents/core/security/session_manager.py +174 -86
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +200 -0
  58. signalwire_agents/core/skill_manager.py +244 -0
  59. signalwire_agents/core/swaig_function.py +33 -9
  60. signalwire_agents/core/swml_builder.py +212 -12
  61. signalwire_agents/core/swml_handler.py +43 -13
  62. signalwire_agents/core/swml_renderer.py +123 -297
  63. signalwire_agents/core/swml_service.py +277 -260
  64. signalwire_agents/prefabs/concierge.py +6 -2
  65. signalwire_agents/prefabs/info_gatherer.py +149 -33
  66. signalwire_agents/prefabs/receptionist.py +14 -22
  67. signalwire_agents/prefabs/survey.py +6 -2
  68. signalwire_agents/schema.json +9218 -5489
  69. signalwire_agents/search/__init__.py +137 -0
  70. signalwire_agents/search/document_processor.py +1223 -0
  71. signalwire_agents/search/index_builder.py +804 -0
  72. signalwire_agents/search/migration.py +418 -0
  73. signalwire_agents/search/models.py +30 -0
  74. signalwire_agents/search/pgvector_backend.py +752 -0
  75. signalwire_agents/search/query_processor.py +502 -0
  76. signalwire_agents/search/search_engine.py +1264 -0
  77. signalwire_agents/search/search_service.py +574 -0
  78. signalwire_agents/skills/README.md +452 -0
  79. signalwire_agents/skills/__init__.py +23 -0
  80. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  81. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  82. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  83. signalwire_agents/skills/datasphere/README.md +210 -0
  84. signalwire_agents/skills/datasphere/__init__.py +12 -0
  85. signalwire_agents/skills/datasphere/skill.py +310 -0
  86. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  87. signalwire_agents/skills/datasphere_serverless/__init__.py +10 -0
  88. signalwire_agents/skills/datasphere_serverless/skill.py +237 -0
  89. signalwire_agents/skills/datetime/README.md +132 -0
  90. signalwire_agents/skills/datetime/__init__.py +10 -0
  91. signalwire_agents/skills/datetime/skill.py +126 -0
  92. signalwire_agents/skills/joke/README.md +149 -0
  93. signalwire_agents/skills/joke/__init__.py +10 -0
  94. signalwire_agents/skills/joke/skill.py +109 -0
  95. signalwire_agents/skills/math/README.md +161 -0
  96. signalwire_agents/skills/math/__init__.py +10 -0
  97. signalwire_agents/skills/math/skill.py +105 -0
  98. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  99. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  100. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  101. signalwire_agents/skills/native_vector_search/README.md +210 -0
  102. signalwire_agents/skills/native_vector_search/__init__.py +10 -0
  103. signalwire_agents/skills/native_vector_search/skill.py +820 -0
  104. signalwire_agents/skills/play_background_file/README.md +218 -0
  105. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  106. signalwire_agents/skills/play_background_file/skill.py +242 -0
  107. signalwire_agents/skills/registry.py +459 -0
  108. signalwire_agents/skills/spider/README.md +236 -0
  109. signalwire_agents/skills/spider/__init__.py +13 -0
  110. signalwire_agents/skills/spider/skill.py +598 -0
  111. signalwire_agents/skills/swml_transfer/README.md +395 -0
  112. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  113. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  114. signalwire_agents/skills/weather_api/README.md +178 -0
  115. signalwire_agents/skills/weather_api/__init__.py +12 -0
  116. signalwire_agents/skills/weather_api/skill.py +191 -0
  117. signalwire_agents/skills/web_search/README.md +163 -0
  118. signalwire_agents/skills/web_search/__init__.py +10 -0
  119. signalwire_agents/skills/web_search/skill.py +739 -0
  120. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  121. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  122. signalwire_agents/skills/wikipedia_search/skill.py +210 -0
  123. signalwire_agents/utils/__init__.py +14 -0
  124. signalwire_agents/utils/schema_utils.py +111 -44
  125. signalwire_agents/web/__init__.py +17 -0
  126. signalwire_agents/web/web_service.py +559 -0
  127. signalwire_agents-1.0.7.data/data/share/man/man1/sw-agent-init.1 +307 -0
  128. signalwire_agents-1.0.7.data/data/share/man/man1/sw-search.1 +483 -0
  129. signalwire_agents-1.0.7.data/data/share/man/man1/swaig-test.1 +308 -0
  130. signalwire_agents-1.0.7.dist-info/METADATA +992 -0
  131. signalwire_agents-1.0.7.dist-info/RECORD +142 -0
  132. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/WHEEL +1 -1
  133. signalwire_agents-1.0.7.dist-info/entry_points.txt +4 -0
  134. signalwire_agents/core/state/file_state_manager.py +0 -219
  135. signalwire_agents/core/state/state_manager.py +0 -101
  136. signalwire_agents-0.1.6.data/data/schema.json +0 -5611
  137. signalwire_agents-0.1.6.dist-info/METADATA +0 -199
  138. signalwire_agents-0.1.6.dist-info/RECORD +0 -34
  139. {signalwire_agents-0.1.6.dist-info → signalwire_agents-1.0.7.dist-info}/licenses/LICENSE +0 -0
  140. {signalwire_agents-0.1.6.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()