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,809 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Copyright (c) 2025 SignalWire
4
+
5
+ This file is part of the SignalWire AI Agents SDK.
6
+
7
+ Licensed under the MIT License.
8
+ See LICENSE file in the project root for full license information.
9
+ """
10
+
11
+ """
12
+ SWAIG Function CLI Testing Tool
13
+
14
+ This tool loads an agent application and calls SWAIG functions with comprehensive
15
+ simulation of the SignalWire environment. It supports both webhook and DataMap functions.
16
+ """
17
+
18
+ # CRITICAL: Set environment variable BEFORE any imports to suppress logs for --raw and --dump-swml
19
+ import sys
20
+ import os
21
+ if "--raw" in sys.argv or "--dump-swml" in sys.argv:
22
+ os.environ['SIGNALWIRE_LOG_MODE'] = 'off'
23
+
24
+ import json
25
+ import argparse
26
+ import warnings
27
+ from pathlib import Path
28
+ from typing import Dict, Any, Optional
29
+
30
+ # Import submodules
31
+ from .config import (
32
+ ERROR_MISSING_AGENT, ERROR_MULTIPLE_AGENTS, ERROR_NO_AGENTS,
33
+ ERROR_AGENT_NOT_FOUND, ERROR_FUNCTION_NOT_FOUND, ERROR_CGI_HOST_REQUIRED,
34
+ HELP_DESCRIPTION, HELP_EPILOG_SHORT
35
+ )
36
+ from .core.argparse_helpers import CustomArgumentParser, parse_function_arguments
37
+ from .core.agent_loader import discover_agents_in_file, load_agent_from_file, load_service_from_file
38
+ from .core.dynamic_config import apply_dynamic_config
39
+ from .simulation.mock_env import ServerlessSimulator, create_mock_request, load_env_file
40
+ from .simulation.data_generation import (
41
+ generate_fake_swml_post_data, generate_comprehensive_post_data,
42
+ generate_minimal_post_data
43
+ )
44
+ from .simulation.data_overrides import apply_overrides, apply_convenience_mappings
45
+ from .execution.datamap_exec import execute_datamap_function
46
+ from .execution.webhook_exec import execute_external_webhook_function
47
+ from .output.swml_dump import handle_dump_swml, setup_output_suppression
48
+ from .output.output_formatter import display_agent_tools, format_result
49
+
50
+
51
+ def print_help_platforms():
52
+ """Print detailed help for serverless platform options"""
53
+ print("""
54
+ Serverless Platform Configuration Options
55
+ ========================================
56
+
57
+ AWS Lambda Configuration:
58
+ --aws-function-name NAME AWS Lambda function name (overrides default)
59
+ --aws-function-url URL AWS Lambda function URL (overrides default)
60
+ --aws-region REGION AWS region (overrides default)
61
+ --aws-api-gateway-id ID AWS API Gateway ID for API Gateway URLs
62
+ --aws-stage STAGE AWS API Gateway stage (default: prod)
63
+
64
+ CGI Configuration:
65
+ --cgi-host HOST CGI server hostname (required for CGI simulation)
66
+ --cgi-script-name NAME CGI script name/path (overrides default)
67
+ --cgi-https Use HTTPS for CGI URLs
68
+ --cgi-path-info PATH CGI PATH_INFO value
69
+
70
+ Google Cloud Platform Configuration:
71
+ --gcp-project ID Google Cloud project ID (overrides default)
72
+ --gcp-function-url URL Google Cloud Function URL (overrides default)
73
+ --gcp-region REGION Google Cloud region (overrides default)
74
+ --gcp-service NAME Google Cloud service name (overrides default)
75
+
76
+ Azure Functions Configuration:
77
+ --azure-env ENV Azure Functions environment (overrides default)
78
+ --azure-function-url URL Azure Function URL (overrides default)
79
+
80
+ Examples:
81
+ # AWS Lambda with custom configuration
82
+ swaig-test agent.py --simulate-serverless lambda \\
83
+ --aws-function-name prod-agent \\
84
+ --aws-region us-west-2 \\
85
+ --dump-swml
86
+
87
+ # CGI with HTTPS
88
+ swaig-test agent.py --simulate-serverless cgi \\
89
+ --cgi-host example.com \\
90
+ --cgi-https \\
91
+ --exec my_function
92
+ """)
93
+
94
+
95
+ def print_help_examples():
96
+ """Print comprehensive usage examples"""
97
+ print("""
98
+ Comprehensive Usage Examples
99
+ ===========================
100
+
101
+ Basic Function Testing
102
+ ---------------------
103
+ # Test a function with CLI-style arguments
104
+ swaig-test agent.py --exec search --query "AI" --limit 5
105
+
106
+ # Test with verbose output
107
+ swaig-test agent.py --verbose --exec search --query "test"
108
+
109
+ # Legacy JSON syntax (still supported)
110
+ swaig-test agent.py search '{"query":"test"}'
111
+
112
+ SWML Document Generation
113
+ -----------------------
114
+ # Generate basic SWML
115
+ swaig-test agent.py --dump-swml
116
+
117
+ # Generate SWML with raw JSON output (for piping)
118
+ swaig-test agent.py --dump-swml --raw | jq '.'
119
+
120
+ # Extract specific fields with jq
121
+ swaig-test agent.py --dump-swml --raw | jq '.sections.main[1].ai.SWAIG.functions'
122
+
123
+ # Generate SWML with comprehensive fake data
124
+ swaig-test agent.py --dump-swml --fake-full-data
125
+
126
+ # Customize call configuration
127
+ swaig-test agent.py --dump-swml --call-type sip --from-number +15551234567
128
+
129
+ Multi-Agent Files
130
+ ----------------
131
+ # List available agents
132
+ swaig-test multi_agent.py --list-agents
133
+
134
+ # Use specific agent
135
+ swaig-test multi_agent.py --agent-class MattiAgent --list-tools
136
+ swaig-test multi_agent.py --agent-class MattiAgent --exec transfer --name sigmond
137
+
138
+ Dynamic Agent Testing
139
+ --------------------
140
+ # Test with query parameters
141
+ swaig-test dynamic_agent.py --dump-swml --query-params '{"tier":"premium"}'
142
+
143
+ # Test with headers
144
+ swaig-test dynamic_agent.py --dump-swml --header "Authorization=Bearer token"
145
+
146
+ # Test with custom request body
147
+ swaig-test dynamic_agent.py --dump-swml --method POST --body '{"custom":"data"}'
148
+
149
+ # Combined dynamic configuration
150
+ swaig-test dynamic_agent.py --dump-swml \\
151
+ --query-params '{"tier":"premium","region":"eu"}' \\
152
+ --header "X-Customer-ID=12345" \\
153
+ --user-vars '{"preferences":{"language":"es"}}'
154
+
155
+ Serverless Environment Simulation
156
+ --------------------------------
157
+ # AWS Lambda simulation
158
+ swaig-test agent.py --simulate-serverless lambda --dump-swml
159
+ swaig-test agent.py --simulate-serverless lambda --exec my_function --param value
160
+
161
+ # With environment variables
162
+ swaig-test agent.py --simulate-serverless lambda \\
163
+ --env API_KEY=secret \\
164
+ --env DEBUG=1 \\
165
+ --exec my_function
166
+
167
+ # With environment file
168
+ swaig-test agent.py --simulate-serverless lambda \\
169
+ --env-file production.env \\
170
+ --exec my_function
171
+
172
+ # CGI simulation
173
+ swaig-test agent.py --simulate-serverless cgi \\
174
+ --cgi-host example.com \\
175
+ --cgi-https \\
176
+ --exec my_function
177
+
178
+ # Google Cloud Functions
179
+ swaig-test agent.py --simulate-serverless cloud_function \\
180
+ --gcp-project my-project \\
181
+ --exec my_function
182
+
183
+ # Azure Functions
184
+ swaig-test agent.py --simulate-serverless azure_function \\
185
+ --azure-env production \\
186
+ --exec my_function
187
+
188
+ Advanced Data Overrides
189
+ ----------------------
190
+ # Override specific values
191
+ swaig-test agent.py --dump-swml \\
192
+ --override call.state=answered \\
193
+ --override call.timeout=60
194
+
195
+ # Override with JSON values
196
+ swaig-test agent.py --dump-swml \\
197
+ --override-json vars.custom='{"key":"value","nested":{"data":true}}'
198
+
199
+ # Combine multiple override types
200
+ swaig-test agent.py --dump-swml \\
201
+ --call-type sip \\
202
+ --user-vars '{"vip":"true"}' \\
203
+ --header "X-Source=test" \\
204
+ --override call.project_id=my-project \\
205
+ --verbose
206
+
207
+ Cross-Platform Testing
208
+ ---------------------
209
+ # Test across all platforms
210
+ for platform in lambda cgi cloud_function azure_function; do
211
+ echo "Testing $platform..."
212
+ swaig-test agent.py --simulate-serverless $platform \\
213
+ --exec my_function --param value
214
+ done
215
+
216
+ # Compare webhook URLs across platforms
217
+ swaig-test agent.py --simulate-serverless lambda --dump-swml | grep web_hook_url
218
+ swaig-test agent.py --simulate-serverless cgi --cgi-host example.com --dump-swml | grep web_hook_url
219
+ """)
220
+
221
+
222
+ def main():
223
+ """Main entry point for the CLI tool"""
224
+ # Set up suppression early if we're dumping SWML
225
+ if "--dump-swml" in sys.argv:
226
+ setup_output_suppression()
227
+
228
+ # Check for help sections early
229
+ if "--help-platforms" in sys.argv:
230
+ print_help_platforms()
231
+ sys.exit(0)
232
+
233
+ if "--help-examples" in sys.argv:
234
+ print_help_examples()
235
+ sys.exit(0)
236
+
237
+ # Check for --exec and split arguments
238
+ cli_args = sys.argv[1:]
239
+ function_args_list = []
240
+ exec_function_name = None
241
+
242
+ if '--exec' in sys.argv:
243
+ exec_index = sys.argv.index('--exec')
244
+ if exec_index + 1 < len(sys.argv):
245
+ exec_function_name = sys.argv[exec_index + 1]
246
+ # CLI args: everything before --exec
247
+ cli_args = sys.argv[1:exec_index]
248
+ # Function args: everything after the function name
249
+ function_args_list = sys.argv[exec_index + 2:]
250
+ else:
251
+ print("Error: --exec requires a function name")
252
+ return 1
253
+
254
+ # Temporarily modify sys.argv for argparse (exclude --exec and its args)
255
+ original_argv = sys.argv[:]
256
+ sys.argv = [sys.argv[0]] + cli_args
257
+
258
+ parser = CustomArgumentParser(
259
+ description=HELP_DESCRIPTION,
260
+ formatter_class=argparse.RawDescriptionHelpFormatter,
261
+ usage="%(prog)s <agent_path> [options]",
262
+ epilog=HELP_EPILOG_SHORT
263
+ )
264
+
265
+ # Positional arguments
266
+ parser.add_argument(
267
+ "agent_path",
268
+ help="Path to Python file containing the agent"
269
+ )
270
+
271
+ # Common options
272
+ common = parser.add_argument_group('common options')
273
+ common.add_argument(
274
+ "-v", "--verbose",
275
+ action="store_true",
276
+ help="Enable verbose output"
277
+ )
278
+ common.add_argument(
279
+ "--raw",
280
+ action="store_true",
281
+ help="Output raw JSON only (for piping to jq)"
282
+ )
283
+ common.add_argument(
284
+ "--agent-class",
285
+ help="Specify agent class (required if file has multiple agents)"
286
+ )
287
+ common.add_argument(
288
+ "--route",
289
+ help="Specify service by route (e.g., /healthcare, /finance)"
290
+ )
291
+
292
+ # Actions (choose one)
293
+ actions = parser.add_argument_group('actions (choose one)')
294
+ actions.add_argument(
295
+ "--list-agents",
296
+ action="store_true",
297
+ help="List all agents in file"
298
+ )
299
+ actions.add_argument(
300
+ "--list-tools",
301
+ action="store_true",
302
+ help="List all tools in agent"
303
+ )
304
+ actions.add_argument(
305
+ "--dump-swml",
306
+ action="store_true",
307
+ help="Generate and output SWML document"
308
+ )
309
+ actions.add_argument(
310
+ "--exec",
311
+ metavar="FUNCTION",
312
+ help="Execute function with CLI args (e.g., --exec search --query 'AI')"
313
+ )
314
+
315
+ # Function execution options
316
+ func_group = parser.add_argument_group('function execution options')
317
+ func_group.add_argument(
318
+ "--custom-data",
319
+ help="JSON string with custom post_data overrides",
320
+ default="{}"
321
+ )
322
+ func_group.add_argument(
323
+ "--minimal",
324
+ action="store_true",
325
+ help="Use minimal post_data for function execution"
326
+ )
327
+ func_group.add_argument(
328
+ "--fake-full-data",
329
+ action="store_true",
330
+ help="Use comprehensive fake post_data"
331
+ )
332
+
333
+ # SWML generation options
334
+ swml_group = parser.add_argument_group('swml generation options')
335
+ swml_group.add_argument(
336
+ "--call-type",
337
+ choices=["sip", "webrtc"],
338
+ default="webrtc",
339
+ help="Call type (default: webrtc)"
340
+ )
341
+ swml_group.add_argument(
342
+ "--call-direction",
343
+ choices=["inbound", "outbound"],
344
+ default="inbound",
345
+ help="Call direction (default: inbound)"
346
+ )
347
+ swml_group.add_argument(
348
+ "--call-state",
349
+ default="created",
350
+ help="Call state (default: created)"
351
+ )
352
+ swml_group.add_argument(
353
+ "--from-number",
354
+ help="Override from number"
355
+ )
356
+ swml_group.add_argument(
357
+ "--to-extension",
358
+ help="Override to extension"
359
+ )
360
+
361
+ # Data customization
362
+ data_group = parser.add_argument_group('data customization')
363
+ data_group.add_argument(
364
+ "--user-vars",
365
+ help="JSON string for userVariables"
366
+ )
367
+ data_group.add_argument(
368
+ "--query-params",
369
+ help="JSON string for query parameters"
370
+ )
371
+ data_group.add_argument(
372
+ "--override",
373
+ action="append",
374
+ default=[],
375
+ help="Override value (e.g., --override call.state=answered)"
376
+ )
377
+ data_group.add_argument(
378
+ "--header",
379
+ action="append",
380
+ default=[],
381
+ help="Add HTTP header (e.g., --header Authorization=Bearer token)"
382
+ )
383
+
384
+ # Serverless simulation (basic)
385
+ serverless_group = parser.add_argument_group('serverless simulation (use --help-platforms for platform options)')
386
+ serverless_group.add_argument(
387
+ "--simulate-serverless",
388
+ choices=["lambda", "cgi", "cloud_function", "azure_function"],
389
+ help="Simulate serverless platform"
390
+ )
391
+ serverless_group.add_argument(
392
+ "--env",
393
+ action="append",
394
+ default=[],
395
+ help="Set environment variable (e.g., --env KEY=VALUE)"
396
+ )
397
+ serverless_group.add_argument(
398
+ "--env-file",
399
+ help="Load environment from file"
400
+ )
401
+
402
+ # Hidden/advanced options (not shown in main help)
403
+ parser.add_argument("--call-id", help=argparse.SUPPRESS)
404
+ parser.add_argument("--project-id", help=argparse.SUPPRESS)
405
+ parser.add_argument("--space-id", help=argparse.SUPPRESS)
406
+ parser.add_argument("--method", default="POST", help=argparse.SUPPRESS)
407
+ parser.add_argument("--body", help=argparse.SUPPRESS)
408
+ parser.add_argument("--override-json", action="append", default=[], help=argparse.SUPPRESS)
409
+
410
+ # Platform-specific options (hidden from main help)
411
+ parser.add_argument("--aws-function-name", help=argparse.SUPPRESS)
412
+ parser.add_argument("--aws-function-url", help=argparse.SUPPRESS)
413
+ parser.add_argument("--aws-region", help=argparse.SUPPRESS)
414
+ parser.add_argument("--aws-api-gateway-id", help=argparse.SUPPRESS)
415
+ parser.add_argument("--aws-stage", help=argparse.SUPPRESS)
416
+ parser.add_argument("--cgi-host", help=argparse.SUPPRESS)
417
+ parser.add_argument("--cgi-script-name", help=argparse.SUPPRESS)
418
+ parser.add_argument("--cgi-https", action="store_true", help=argparse.SUPPRESS)
419
+ parser.add_argument("--cgi-path-info", help=argparse.SUPPRESS)
420
+ parser.add_argument("--gcp-project", help=argparse.SUPPRESS)
421
+ parser.add_argument("--gcp-function-url", help=argparse.SUPPRESS)
422
+ parser.add_argument("--gcp-region", help=argparse.SUPPRESS)
423
+ parser.add_argument("--gcp-service", help=argparse.SUPPRESS)
424
+ parser.add_argument("--azure-env", help=argparse.SUPPRESS)
425
+ parser.add_argument("--azure-function-url", help=argparse.SUPPRESS)
426
+
427
+ # Help extension options
428
+ parser.add_argument(
429
+ "--help-platforms",
430
+ action="store_true",
431
+ help="Show platform-specific serverless options"
432
+ )
433
+ parser.add_argument(
434
+ "--help-examples",
435
+ action="store_true",
436
+ help="Show comprehensive usage examples"
437
+ )
438
+
439
+ args = parser.parse_args()
440
+
441
+ # Restore original sys.argv
442
+ sys.argv = original_argv
443
+
444
+ # Handle --exec vs positional tool_name
445
+ if exec_function_name:
446
+ args.tool_name = exec_function_name
447
+ else:
448
+ args.tool_name = None
449
+
450
+ # Validate arguments
451
+ if args.route and args.agent_class:
452
+ parser.error("Cannot specify both --route and --agent-class. Choose one.")
453
+
454
+ if not args.list_tools and not args.dump_swml and not args.list_agents:
455
+ if not args.tool_name:
456
+ # If no tool_name and no special flags, default to listing tools
457
+ args.list_tools = True
458
+
459
+ # ===== SERVERLESS SIMULATION SETUP =====
460
+ serverless_simulator = None
461
+
462
+ if args.simulate_serverless:
463
+ # Validate CGI requirements
464
+ if args.simulate_serverless == 'cgi' and not args.cgi_host:
465
+ parser.error(ERROR_CGI_HOST_REQUIRED)
466
+
467
+ # Collect environment variable overrides
468
+ env_overrides = {}
469
+
470
+ # Load from environment file first
471
+ if args.env_file:
472
+ try:
473
+ file_env = load_env_file(args.env_file)
474
+ env_overrides.update(file_env)
475
+ if args.verbose and not args.raw:
476
+ print(f"Loaded {len(file_env)} environment variables from {args.env_file}")
477
+ except FileNotFoundError as e:
478
+ print(f"Error: {e}")
479
+ return 1
480
+
481
+ # Apply individual env overrides
482
+ for env_var in args.env:
483
+ if '=' in env_var:
484
+ key, value = env_var.split('=', 1)
485
+ env_overrides[key] = value
486
+
487
+ # Apply platform-specific overrides
488
+ if args.simulate_serverless == 'lambda':
489
+ if args.aws_function_name:
490
+ env_overrides['AWS_LAMBDA_FUNCTION_NAME'] = args.aws_function_name
491
+ if args.aws_function_url:
492
+ env_overrides['AWS_LAMBDA_FUNCTION_URL'] = args.aws_function_url
493
+ if args.aws_region:
494
+ env_overrides['AWS_REGION'] = args.aws_region
495
+ elif args.simulate_serverless == 'cgi':
496
+ if args.cgi_host:
497
+ env_overrides['HTTP_HOST'] = args.cgi_host
498
+ env_overrides['SERVER_NAME'] = args.cgi_host
499
+ if args.cgi_script_name:
500
+ env_overrides['SCRIPT_NAME'] = args.cgi_script_name
501
+ if args.cgi_https:
502
+ env_overrides['HTTPS'] = 'on'
503
+ if args.cgi_path_info:
504
+ env_overrides['PATH_INFO'] = args.cgi_path_info
505
+ elif args.simulate_serverless == 'cloud_function':
506
+ if args.gcp_project:
507
+ env_overrides['GOOGLE_CLOUD_PROJECT'] = args.gcp_project
508
+ if args.gcp_function_url:
509
+ env_overrides['FUNCTION_URL'] = args.gcp_function_url
510
+ if args.gcp_region:
511
+ env_overrides['GOOGLE_CLOUD_REGION'] = args.gcp_region
512
+ if args.gcp_service:
513
+ env_overrides['K_SERVICE'] = args.gcp_service
514
+ elif args.simulate_serverless == 'azure_function':
515
+ if args.azure_env:
516
+ env_overrides['AZURE_FUNCTIONS_ENVIRONMENT'] = args.azure_env
517
+ if args.azure_function_url:
518
+ env_overrides['AZURE_FUNCTION_URL'] = args.azure_function_url
519
+
520
+ # Create and activate simulator
521
+ serverless_simulator = ServerlessSimulator(args.simulate_serverless, env_overrides)
522
+ serverless_simulator.activate(args.verbose and not args.raw)
523
+
524
+ # ===== MAIN EXECUTION =====
525
+ try:
526
+ # Check if agent file exists
527
+ agent_path = Path(args.agent_path)
528
+ if not agent_path.exists():
529
+ print(f"Error: Agent file not found: {args.agent_path}")
530
+ return 1
531
+
532
+ # Handle --list-agents
533
+ if args.list_agents:
534
+ try:
535
+ agents = discover_agents_in_file(args.agent_path)
536
+ if not agents:
537
+ print(ERROR_NO_AGENTS.format(file_path=args.agent_path))
538
+ return 1
539
+
540
+ print(f"\nAgents found in {args.agent_path}:")
541
+ for agent_info in agents:
542
+ agent_type = "instance" if agent_info['type'] == 'instance' else "class"
543
+ print(f" {agent_info['class_name']} ({agent_type})")
544
+ if agent_info['type'] == 'instance':
545
+ print(f" Name: {agent_info['agent_name']}")
546
+ print(f" Route: {agent_info['route']}")
547
+ if agent_info['description']:
548
+ # Clean up description
549
+ desc = agent_info['description'].strip()
550
+ if desc:
551
+ # Take first line only
552
+ desc_lines = desc.split('\n')
553
+ first_line = desc_lines[0].strip()
554
+ if first_line:
555
+ print(f" Description: {first_line}")
556
+ return 0
557
+ except Exception as e:
558
+ print(f"Error discovering agents: {e}")
559
+ if args.verbose:
560
+ import traceback
561
+ traceback.print_exc()
562
+ return 1
563
+
564
+ # Load the agent
565
+ try:
566
+ # Determine which identifier to use
567
+ service_identifier = args.route if args.route else args.agent_class
568
+ prefer_route = bool(args.route)
569
+
570
+ # Use load_service_from_file which handles both routes and class names
571
+ from signalwire_agents.cli.core.agent_loader import load_service_from_file
572
+ agent = load_service_from_file(args.agent_path, service_identifier, prefer_route)
573
+ except ValueError as e:
574
+ error_msg = str(e)
575
+ if "Multiple agent classes found" in error_msg and args.list_tools and not args.agent_class:
576
+ # When listing tools and multiple agents exist, show all agents with their tools
577
+ try:
578
+ agents = discover_agents_in_file(args.agent_path)
579
+ if agents:
580
+ print(f"\nMultiple agents found in {args.agent_path}:")
581
+ print("=" * 60)
582
+
583
+ for agent_info in agents:
584
+ if agent_info['type'] == 'class':
585
+ print(f"\n{agent_info['class_name']}:")
586
+ if agent_info['description']:
587
+ desc = agent_info['description'].strip().split('\n')[0]
588
+ if desc:
589
+ print(f" Description: {desc}")
590
+
591
+ # Try to load this specific agent and show its tools
592
+ try:
593
+ specific_agent = load_agent_from_file(args.agent_path, agent_info['class_name'])
594
+
595
+ # Apply dynamic configuration if the agent has it
596
+ # Create a basic mock request for dynamic config
597
+ try:
598
+ basic_mock_request = create_mock_request(
599
+ method="POST",
600
+ headers={},
601
+ query_params={},
602
+ body={}
603
+ )
604
+ apply_dynamic_config(specific_agent, basic_mock_request, verbose=False)
605
+ except Exception as dc_error:
606
+ if args.verbose:
607
+ print(f" (Warning: Dynamic config failed: {dc_error})")
608
+
609
+ functions = specific_agent._tool_registry.get_all_functions() if hasattr(specific_agent, '_tool_registry') else {}
610
+
611
+
612
+ if functions:
613
+ print(f" Tools:")
614
+ for name, func in functions.items():
615
+ if isinstance(func, dict):
616
+ description = func.get('description', 'DataMap function')
617
+ print(f" - {name}: {description}")
618
+ else:
619
+ print(f" - {name}: {func.description}")
620
+ else:
621
+ print(f" Tools: (none)")
622
+ except Exception as load_error:
623
+ print(f" Tools: (error loading agent: {load_error})")
624
+ if args.verbose:
625
+ import traceback
626
+ traceback.print_exc()
627
+
628
+ print("\n" + "=" * 60)
629
+ print(f"\nTo use a specific agent, run:")
630
+ print(f" swaig-test {args.agent_path} --agent-class <AgentClassName>")
631
+ print(f" swaig-test {args.agent_path} --route <route>")
632
+ return 0
633
+ except Exception as discover_error:
634
+ print(f"Error discovering agents: {discover_error}")
635
+ return 1
636
+ elif "Multiple agent classes found" in error_msg:
637
+ print(f"\n{ERROR_MULTIPLE_AGENTS}")
638
+ print(error_msg)
639
+ elif "not found" in error_msg and args.agent_class:
640
+ print(ERROR_AGENT_NOT_FOUND.format(
641
+ class_name=args.agent_class,
642
+ file_path=args.agent_path
643
+ ))
644
+ else:
645
+ print(f"Error: {error_msg}")
646
+ return 1
647
+
648
+ # Create mock request for dynamic configuration
649
+ headers = {}
650
+ for header in args.header:
651
+ if '=' in header:
652
+ key, value = header.split('=', 1)
653
+ headers[key] = value
654
+
655
+ query_params = {}
656
+ if args.query_params:
657
+ try:
658
+ query_params = json.loads(args.query_params)
659
+ except json.JSONDecodeError as e:
660
+ if not args.raw:
661
+ print(f"Warning: Invalid JSON in --query-params: {e}")
662
+
663
+ request_body = {}
664
+ if args.body:
665
+ try:
666
+ request_body = json.loads(args.body)
667
+ except json.JSONDecodeError as e:
668
+ if not args.raw:
669
+ print(f"Warning: Invalid JSON in --body: {e}")
670
+
671
+ mock_request = create_mock_request(
672
+ method=args.method,
673
+ headers=headers,
674
+ query_params=query_params,
675
+ body=request_body
676
+ )
677
+
678
+ # Apply dynamic configuration
679
+ apply_dynamic_config(agent, mock_request, verbose=args.verbose and not args.raw)
680
+
681
+ # Handle --list-tools
682
+ if args.list_tools:
683
+ display_agent_tools(agent, verbose=args.verbose)
684
+ return 0
685
+
686
+ # Handle --dump-swml
687
+ if args.dump_swml:
688
+ return handle_dump_swml(agent, args)
689
+
690
+ # Handle function execution
691
+ if args.tool_name:
692
+ # Get the function
693
+ functions = agent._tool_registry.get_all_functions() if hasattr(agent, '_tool_registry') else {}
694
+
695
+ if args.tool_name not in functions:
696
+ print(ERROR_FUNCTION_NOT_FOUND.format(function_name=args.tool_name))
697
+ display_agent_tools(agent, verbose=False)
698
+ return 1
699
+
700
+ func = functions[args.tool_name]
701
+
702
+ # Parse function arguments
703
+ try:
704
+ function_args = parse_function_arguments(function_args_list, func)
705
+ except ValueError as e:
706
+ print(f"Error parsing arguments: {e}")
707
+ return 1
708
+
709
+ # Check if this is a DataMap function
710
+ is_datamap = isinstance(func, dict) and 'data_map' in func
711
+
712
+ # Check if this is an external webhook function
713
+ is_external_webhook = (hasattr(func, 'webhook_url') and
714
+ func.webhook_url and
715
+ hasattr(func, 'is_external') and
716
+ func.is_external)
717
+
718
+ if is_datamap:
719
+ if args.verbose:
720
+ print(f"\nCalling DataMap function: {args.tool_name}")
721
+ print(f"Arguments: {json.dumps(function_args, indent=2)}")
722
+ print(f"Function type: DataMap (serverless)")
723
+ print("-" * 60)
724
+
725
+ # Execute DataMap function
726
+ result = execute_datamap_function(func, function_args, args.verbose)
727
+ print("RESULT:")
728
+ print(format_result(result))
729
+ else:
730
+ # Regular SWAIG function
731
+ if args.verbose:
732
+ print(f"\nCalling function: {args.tool_name}")
733
+ print(f"Arguments: {json.dumps(function_args, indent=2)}")
734
+ if is_external_webhook:
735
+ print(f"Function type: EXTERNAL webhook")
736
+ print(f"External URL: {func.webhook_url}")
737
+ else:
738
+ print(f"Function type: LOCAL webhook")
739
+
740
+ # Generate post_data based on options
741
+ if args.minimal:
742
+ post_data = generate_minimal_post_data(args.tool_name, function_args)
743
+ if args.custom_data:
744
+ custom_data = json.loads(args.custom_data)
745
+ post_data.update(custom_data)
746
+ elif args.fake_full_data or args.custom_data:
747
+ custom_data = json.loads(args.custom_data) if args.custom_data else None
748
+ post_data = generate_comprehensive_post_data(args.tool_name, function_args, custom_data)
749
+ else:
750
+ # Default behavior - minimal data
751
+ post_data = generate_minimal_post_data(args.tool_name, function_args)
752
+
753
+ # Apply convenience mappings from CLI args (e.g., --call-id)
754
+ post_data = apply_convenience_mappings(post_data, args)
755
+
756
+ # Apply explicit overrides
757
+ post_data = apply_overrides(post_data, args.override, args.override_json)
758
+
759
+ if args.verbose:
760
+ print(f"Post data: {json.dumps(post_data, indent=2)}")
761
+ print("-" * 60)
762
+
763
+ # Call the function
764
+ try:
765
+ if is_external_webhook:
766
+ # For external webhook functions, make HTTP request to external service
767
+ result = execute_external_webhook_function(func, args.tool_name, function_args, post_data, args.verbose)
768
+ else:
769
+ # For local webhook functions, call the agent's handler
770
+ result = agent.on_function_call(args.tool_name, function_args, post_data)
771
+
772
+ print("RESULT:")
773
+ print(format_result(result))
774
+
775
+ if args.verbose:
776
+ print(f"\nRaw result type: {type(result).__name__}")
777
+ print(f"Raw result: {repr(result)}")
778
+
779
+ except Exception as e:
780
+ print(f"Error calling function: {e}")
781
+ if args.verbose:
782
+ import traceback
783
+ traceback.print_exc()
784
+ return 1
785
+
786
+ except Exception as e:
787
+ print(f"Error: {e}")
788
+ if args.verbose:
789
+ import traceback
790
+ traceback.print_exc()
791
+ return 1
792
+ finally:
793
+ # Clean up serverless simulation
794
+ if serverless_simulator:
795
+ serverless_simulator.deactivate(args.verbose and not args.raw)
796
+
797
+ return 0
798
+
799
+
800
+ def console_entry_point():
801
+ """Console script entry point for pip installation"""
802
+ # Check for --dump-swml or --raw BEFORE imports happen
803
+ if "--raw" in sys.argv or "--dump-swml" in sys.argv:
804
+ os.environ['SIGNALWIRE_LOG_MODE'] = 'off'
805
+ sys.exit(main())
806
+
807
+
808
+ if __name__ == "__main__":
809
+ sys.exit(main())