signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- signalwire_agents/__init__.py +99 -15
- signalwire_agents/agent_server.py +248 -60
- signalwire_agents/agents/bedrock.py +296 -0
- signalwire_agents/cli/__init__.py +9 -0
- signalwire_agents/cli/build_search.py +951 -41
- signalwire_agents/cli/config.py +80 -0
- signalwire_agents/cli/core/__init__.py +10 -0
- signalwire_agents/cli/core/agent_loader.py +470 -0
- signalwire_agents/cli/core/argparse_helpers.py +179 -0
- signalwire_agents/cli/core/dynamic_config.py +71 -0
- signalwire_agents/cli/core/service_loader.py +303 -0
- signalwire_agents/cli/dokku.py +2320 -0
- signalwire_agents/cli/execution/__init__.py +10 -0
- signalwire_agents/cli/execution/datamap_exec.py +446 -0
- signalwire_agents/cli/execution/webhook_exec.py +134 -0
- signalwire_agents/cli/init_project.py +2636 -0
- signalwire_agents/cli/output/__init__.py +10 -0
- signalwire_agents/cli/output/output_formatter.py +255 -0
- signalwire_agents/cli/output/swml_dump.py +186 -0
- signalwire_agents/cli/simulation/__init__.py +10 -0
- signalwire_agents/cli/simulation/data_generation.py +374 -0
- signalwire_agents/cli/simulation/data_overrides.py +200 -0
- signalwire_agents/cli/simulation/mock_env.py +282 -0
- signalwire_agents/cli/swaig_test_wrapper.py +52 -0
- signalwire_agents/cli/test_swaig.py +566 -2366
- signalwire_agents/cli/types.py +81 -0
- signalwire_agents/core/__init__.py +2 -2
- signalwire_agents/core/agent/__init__.py +12 -0
- signalwire_agents/core/agent/config/__init__.py +12 -0
- signalwire_agents/core/agent/deployment/__init__.py +9 -0
- signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
- signalwire_agents/core/agent/prompt/__init__.py +14 -0
- signalwire_agents/core/agent/prompt/manager.py +306 -0
- signalwire_agents/core/agent/routing/__init__.py +9 -0
- signalwire_agents/core/agent/security/__init__.py +9 -0
- signalwire_agents/core/agent/swml/__init__.py +9 -0
- signalwire_agents/core/agent/tools/__init__.py +15 -0
- signalwire_agents/core/agent/tools/decorator.py +97 -0
- signalwire_agents/core/agent/tools/registry.py +210 -0
- signalwire_agents/core/agent_base.py +845 -2916
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +418 -0
- signalwire_agents/core/data_map.py +3 -15
- signalwire_agents/core/function_result.py +116 -44
- signalwire_agents/core/logging_config.py +162 -18
- signalwire_agents/core/mixins/__init__.py +28 -0
- signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
- signalwire_agents/core/mixins/auth_mixin.py +280 -0
- signalwire_agents/core/mixins/prompt_mixin.py +358 -0
- signalwire_agents/core/mixins/serverless_mixin.py +460 -0
- signalwire_agents/core/mixins/skill_mixin.py +55 -0
- signalwire_agents/core/mixins/state_mixin.py +153 -0
- signalwire_agents/core/mixins/tool_mixin.py +230 -0
- signalwire_agents/core/mixins/web_mixin.py +1142 -0
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/skill_base.py +84 -1
- signalwire_agents/core/skill_manager.py +62 -20
- signalwire_agents/core/swaig_function.py +18 -5
- signalwire_agents/core/swml_builder.py +207 -11
- signalwire_agents/core/swml_handler.py +27 -21
- signalwire_agents/core/swml_renderer.py +123 -312
- signalwire_agents/core/swml_service.py +171 -203
- signalwire_agents/mcp_gateway/__init__.py +29 -0
- signalwire_agents/mcp_gateway/gateway_service.py +564 -0
- signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
- signalwire_agents/mcp_gateway/session_manager.py +218 -0
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/schema.json +9218 -5489
- signalwire_agents/search/__init__.py +7 -1
- signalwire_agents/search/document_processor.py +490 -31
- signalwire_agents/search/index_builder.py +307 -37
- signalwire_agents/search/migration.py +418 -0
- signalwire_agents/search/models.py +30 -0
- signalwire_agents/search/pgvector_backend.py +748 -0
- signalwire_agents/search/query_processor.py +162 -31
- signalwire_agents/search/search_engine.py +916 -35
- signalwire_agents/search/search_service.py +376 -53
- signalwire_agents/skills/README.md +452 -0
- signalwire_agents/skills/__init__.py +14 -2
- signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
- signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
- signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
- signalwire_agents/skills/datasphere/README.md +210 -0
- signalwire_agents/skills/datasphere/skill.py +84 -3
- signalwire_agents/skills/datasphere_serverless/README.md +258 -0
- signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
- signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
- signalwire_agents/skills/datetime/README.md +132 -0
- signalwire_agents/skills/datetime/__init__.py +9 -0
- signalwire_agents/skills/datetime/skill.py +20 -7
- signalwire_agents/skills/joke/README.md +149 -0
- signalwire_agents/skills/joke/__init__.py +9 -0
- signalwire_agents/skills/joke/skill.py +21 -0
- signalwire_agents/skills/math/README.md +161 -0
- signalwire_agents/skills/math/__init__.py +9 -0
- signalwire_agents/skills/math/skill.py +18 -4
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
- signalwire_agents/skills/mcp_gateway/skill.py +421 -0
- signalwire_agents/skills/native_vector_search/README.md +210 -0
- signalwire_agents/skills/native_vector_search/__init__.py +9 -0
- signalwire_agents/skills/native_vector_search/skill.py +569 -101
- signalwire_agents/skills/play_background_file/README.md +218 -0
- signalwire_agents/skills/play_background_file/__init__.py +12 -0
- signalwire_agents/skills/play_background_file/skill.py +242 -0
- signalwire_agents/skills/registry.py +395 -40
- signalwire_agents/skills/spider/README.md +236 -0
- signalwire_agents/skills/spider/__init__.py +13 -0
- signalwire_agents/skills/spider/skill.py +598 -0
- signalwire_agents/skills/swml_transfer/README.md +395 -0
- signalwire_agents/skills/swml_transfer/__init__.py +10 -0
- signalwire_agents/skills/swml_transfer/skill.py +359 -0
- signalwire_agents/skills/weather_api/README.md +178 -0
- signalwire_agents/skills/weather_api/__init__.py +12 -0
- signalwire_agents/skills/weather_api/skill.py +191 -0
- signalwire_agents/skills/web_search/README.md +163 -0
- signalwire_agents/skills/web_search/__init__.py +9 -0
- signalwire_agents/skills/web_search/skill.py +586 -112
- signalwire_agents/skills/wikipedia_search/README.md +228 -0
- signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
- signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
- signalwire_agents/web/__init__.py +17 -0
- signalwire_agents/web/web_service.py +559 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
- signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
- signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
- signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- signalwire_agents/skills/wikipedia/__init__.py +0 -9
- signalwire_agents-0.1.13.data/data/schema.json +0 -5611
- signalwire_agents-0.1.13.dist-info/RECORD +0 -67
- signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
|
@@ -1,1732 +1,238 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
This
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
'AWS_REGION': 'us-east-1',
|
|
221
|
-
'_HANDLER': 'lambda_function.lambda_handler'
|
|
222
|
-
},
|
|
223
|
-
'cgi': {
|
|
224
|
-
'GATEWAY_INTERFACE': 'CGI/1.1',
|
|
225
|
-
'HTTP_HOST': 'example.com',
|
|
226
|
-
'SCRIPT_NAME': '/cgi-bin/agent.cgi',
|
|
227
|
-
'HTTPS': 'on',
|
|
228
|
-
'SERVER_NAME': 'example.com'
|
|
229
|
-
},
|
|
230
|
-
'cloud_function': {
|
|
231
|
-
'GOOGLE_CLOUD_PROJECT': 'test-project',
|
|
232
|
-
'FUNCTION_URL': 'https://my-function-abc123.cloudfunctions.net',
|
|
233
|
-
'GOOGLE_CLOUD_REGION': 'us-central1',
|
|
234
|
-
'K_SERVICE': 'agent'
|
|
235
|
-
},
|
|
236
|
-
'azure_function': {
|
|
237
|
-
'AZURE_FUNCTIONS_ENVIRONMENT': 'Development',
|
|
238
|
-
'FUNCTIONS_WORKER_RUNTIME': 'python',
|
|
239
|
-
'WEBSITE_SITE_NAME': 'my-function-app'
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
def __init__(self, platform: str, overrides: Optional[Dict[str, str]] = None):
|
|
244
|
-
self.platform = platform
|
|
245
|
-
self.original_env = dict(os.environ)
|
|
246
|
-
self.preset_env = self.PLATFORM_PRESETS.get(platform, {}).copy()
|
|
247
|
-
self.overrides = overrides or {}
|
|
248
|
-
self.active = False
|
|
249
|
-
self._cleared_vars = {}
|
|
250
|
-
|
|
251
|
-
def activate(self, verbose: bool = False):
|
|
252
|
-
"""Apply serverless environment simulation"""
|
|
253
|
-
if self.active:
|
|
254
|
-
return
|
|
255
|
-
|
|
256
|
-
# Clear conflicting environment variables
|
|
257
|
-
self._clear_conflicting_env()
|
|
258
|
-
|
|
259
|
-
# Apply preset environment
|
|
260
|
-
os.environ.update(self.preset_env)
|
|
261
|
-
|
|
262
|
-
# Apply user overrides
|
|
263
|
-
os.environ.update(self.overrides)
|
|
264
|
-
|
|
265
|
-
# Set appropriate logging mode for serverless simulation
|
|
266
|
-
if self.platform == 'cgi' and 'SIGNALWIRE_LOG_MODE' not in self.overrides:
|
|
267
|
-
# CGI mode should default to 'off' unless explicitly overridden
|
|
268
|
-
os.environ['SIGNALWIRE_LOG_MODE'] = 'off'
|
|
269
|
-
|
|
270
|
-
self.active = True
|
|
271
|
-
|
|
272
|
-
if verbose:
|
|
273
|
-
print(f"✓ Activated {self.platform} environment simulation")
|
|
274
|
-
|
|
275
|
-
# Debug: Show key environment variables
|
|
276
|
-
if self.platform == 'lambda':
|
|
277
|
-
print(f" AWS_LAMBDA_FUNCTION_NAME: {os.environ.get('AWS_LAMBDA_FUNCTION_NAME')}")
|
|
278
|
-
print(f" AWS_LAMBDA_FUNCTION_URL: {os.environ.get('AWS_LAMBDA_FUNCTION_URL')}")
|
|
279
|
-
print(f" AWS_REGION: {os.environ.get('AWS_REGION')}")
|
|
280
|
-
elif self.platform == 'cgi':
|
|
281
|
-
print(f" GATEWAY_INTERFACE: {os.environ.get('GATEWAY_INTERFACE')}")
|
|
282
|
-
print(f" HTTP_HOST: {os.environ.get('HTTP_HOST')}")
|
|
283
|
-
print(f" SCRIPT_NAME: {os.environ.get('SCRIPT_NAME')}")
|
|
284
|
-
print(f" SIGNALWIRE_LOG_MODE: {os.environ.get('SIGNALWIRE_LOG_MODE')}")
|
|
285
|
-
elif self.platform == 'cloud_function':
|
|
286
|
-
print(f" GOOGLE_CLOUD_PROJECT: {os.environ.get('GOOGLE_CLOUD_PROJECT')}")
|
|
287
|
-
print(f" FUNCTION_URL: {os.environ.get('FUNCTION_URL')}")
|
|
288
|
-
print(f" GOOGLE_CLOUD_REGION: {os.environ.get('GOOGLE_CLOUD_REGION')}")
|
|
289
|
-
elif self.platform == 'azure_function':
|
|
290
|
-
print(f" AZURE_FUNCTIONS_ENVIRONMENT: {os.environ.get('AZURE_FUNCTIONS_ENVIRONMENT')}")
|
|
291
|
-
print(f" WEBSITE_SITE_NAME: {os.environ.get('WEBSITE_SITE_NAME')}")
|
|
292
|
-
|
|
293
|
-
# Debug: Confirm SWML_PROXY_URL_BASE is cleared
|
|
294
|
-
proxy_url = os.environ.get('SWML_PROXY_URL_BASE')
|
|
295
|
-
if proxy_url:
|
|
296
|
-
print(f" WARNING: SWML_PROXY_URL_BASE still set: {proxy_url}")
|
|
297
|
-
else:
|
|
298
|
-
print(f" ✓ SWML_PROXY_URL_BASE cleared successfully")
|
|
299
|
-
|
|
300
|
-
def deactivate(self, verbose: bool = False):
|
|
301
|
-
"""Restore original environment"""
|
|
302
|
-
if not self.active:
|
|
303
|
-
return
|
|
304
|
-
|
|
305
|
-
os.environ.clear()
|
|
306
|
-
os.environ.update(self.original_env)
|
|
307
|
-
self.active = False
|
|
308
|
-
|
|
309
|
-
if verbose:
|
|
310
|
-
print(f"✓ Deactivated {self.platform} environment simulation")
|
|
311
|
-
|
|
312
|
-
def _clear_conflicting_env(self):
|
|
313
|
-
"""Clear environment variables that might conflict with simulation"""
|
|
314
|
-
# Remove variables from other platforms
|
|
315
|
-
conflicting_vars = []
|
|
316
|
-
for platform, preset in self.PLATFORM_PRESETS.items():
|
|
317
|
-
if platform != self.platform:
|
|
318
|
-
conflicting_vars.extend(preset.keys())
|
|
319
|
-
|
|
320
|
-
# Always clear SWML_PROXY_URL_BASE during serverless simulation
|
|
321
|
-
# so that platform-specific URL generation takes precedence
|
|
322
|
-
conflicting_vars.append('SWML_PROXY_URL_BASE')
|
|
323
|
-
|
|
324
|
-
for var in conflicting_vars:
|
|
325
|
-
if var in os.environ:
|
|
326
|
-
self._cleared_vars[var] = os.environ[var]
|
|
327
|
-
os.environ.pop(var)
|
|
328
|
-
|
|
329
|
-
def add_override(self, key: str, value: str):
|
|
330
|
-
"""Add an environment variable override"""
|
|
331
|
-
self.overrides[key] = value
|
|
332
|
-
if self.active:
|
|
333
|
-
os.environ[key] = value
|
|
334
|
-
|
|
335
|
-
def get_current_env(self) -> Dict[str, str]:
|
|
336
|
-
"""Get the current environment that would be applied"""
|
|
337
|
-
env = self.preset_env.copy()
|
|
338
|
-
env.update(self.overrides)
|
|
339
|
-
return env
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def load_env_file(env_file_path: str) -> Dict[str, str]:
|
|
343
|
-
"""Load environment variables from a file"""
|
|
344
|
-
env_vars = {}
|
|
345
|
-
if not os.path.exists(env_file_path):
|
|
346
|
-
raise FileNotFoundError(f"Environment file not found: {env_file_path}")
|
|
347
|
-
|
|
348
|
-
with open(env_file_path, 'r') as f:
|
|
349
|
-
for line in f:
|
|
350
|
-
line = line.strip()
|
|
351
|
-
if line and not line.startswith('#') and '=' in line:
|
|
352
|
-
key, value = line.split('=', 1)
|
|
353
|
-
env_vars[key.strip()] = value.strip()
|
|
354
|
-
|
|
355
|
-
return env_vars
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
# ===== FAKE SWML POST DATA GENERATION =====
|
|
359
|
-
|
|
360
|
-
def generate_fake_uuid() -> str:
|
|
361
|
-
"""Generate a fake UUID for testing"""
|
|
362
|
-
return str(uuid.uuid4())
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def generate_fake_node_id() -> str:
|
|
366
|
-
"""Generate a fake node ID for testing"""
|
|
367
|
-
return f"test-node-{uuid.uuid4().hex[:8]}"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def generate_fake_sip_from(call_type: str) -> str:
|
|
371
|
-
"""Generate a fake 'from' address based on call type"""
|
|
372
|
-
if call_type == "sip":
|
|
373
|
-
return f"+1555{uuid.uuid4().hex[:7]}" # Fake phone number
|
|
374
|
-
else: # webrtc
|
|
375
|
-
return f"user-{uuid.uuid4().hex[:8]}@test.domain"
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def generate_fake_sip_to(call_type: str) -> str:
|
|
379
|
-
"""Generate a fake 'to' address based on call type"""
|
|
380
|
-
if call_type == "sip":
|
|
381
|
-
return f"+1444{uuid.uuid4().hex[:7]}" # Fake phone number
|
|
382
|
-
else: # webrtc
|
|
383
|
-
return f"agent-{uuid.uuid4().hex[:8]}@test.domain"
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def adapt_for_call_type(call_data: Dict[str, Any], call_type: str) -> Dict[str, Any]:
|
|
387
|
-
"""
|
|
388
|
-
Adapt call data structure based on call type (sip vs webrtc)
|
|
389
|
-
|
|
390
|
-
Args:
|
|
391
|
-
call_data: Base call data structure
|
|
392
|
-
call_type: "sip" or "webrtc"
|
|
393
|
-
|
|
394
|
-
Returns:
|
|
395
|
-
Adapted call data with appropriate addresses and metadata
|
|
396
|
-
"""
|
|
397
|
-
call_data = call_data.copy()
|
|
398
|
-
|
|
399
|
-
# Update addresses based on call type
|
|
400
|
-
call_data["from"] = generate_fake_sip_from(call_type)
|
|
401
|
-
call_data["to"] = generate_fake_sip_to(call_type)
|
|
402
|
-
|
|
403
|
-
# Add call type specific metadata
|
|
404
|
-
if call_type == "sip":
|
|
405
|
-
call_data["type"] = "phone"
|
|
406
|
-
call_data["headers"] = {
|
|
407
|
-
"User-Agent": f"Test-SIP-Client/1.0.0",
|
|
408
|
-
"From": f"<sip:{call_data['from']}@test.sip.provider>",
|
|
409
|
-
"To": f"<sip:{call_data['to']}@test.sip.provider>",
|
|
410
|
-
"Call-ID": call_data["call_id"]
|
|
411
|
-
}
|
|
412
|
-
else: # webrtc
|
|
413
|
-
call_data["type"] = "webrtc"
|
|
414
|
-
call_data["headers"] = {
|
|
415
|
-
"User-Agent": "Test-WebRTC-Client/1.0.0",
|
|
416
|
-
"Origin": "https://test.webrtc.app",
|
|
417
|
-
"Sec-WebSocket-Protocol": "sip"
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return call_data
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def generate_fake_swml_post_data(call_type: str = "webrtc",
|
|
424
|
-
call_direction: str = "inbound",
|
|
425
|
-
call_state: str = "created") -> Dict[str, Any]:
|
|
426
|
-
"""
|
|
427
|
-
Generate fake SWML post_data that matches real SignalWire structure
|
|
428
|
-
|
|
429
|
-
Args:
|
|
430
|
-
call_type: "sip" or "webrtc" (default: webrtc)
|
|
431
|
-
call_direction: "inbound" or "outbound" (default: inbound)
|
|
432
|
-
call_state: Call state (default: created)
|
|
433
|
-
|
|
434
|
-
Returns:
|
|
435
|
-
Fake post_data dict with call, vars, and envs structure
|
|
436
|
-
"""
|
|
437
|
-
call_id = generate_fake_uuid()
|
|
438
|
-
project_id = generate_fake_uuid()
|
|
439
|
-
space_id = generate_fake_uuid()
|
|
440
|
-
current_time = datetime.now().isoformat()
|
|
441
|
-
|
|
442
|
-
# Base call structure
|
|
443
|
-
call_data = {
|
|
444
|
-
"call_id": call_id,
|
|
445
|
-
"node_id": generate_fake_node_id(),
|
|
446
|
-
"segment_id": generate_fake_uuid(),
|
|
447
|
-
"call_session_id": generate_fake_uuid(),
|
|
448
|
-
"tag": call_id,
|
|
449
|
-
"state": call_state,
|
|
450
|
-
"direction": call_direction,
|
|
451
|
-
"type": call_type,
|
|
452
|
-
"from": generate_fake_sip_from(call_type),
|
|
453
|
-
"to": generate_fake_sip_to(call_type),
|
|
454
|
-
"timeout": 30,
|
|
455
|
-
"max_duration": 14400,
|
|
456
|
-
"answer_on_bridge": False,
|
|
457
|
-
"hangup_after_bridge": True,
|
|
458
|
-
"ringback": [],
|
|
459
|
-
"record": {},
|
|
460
|
-
"project_id": project_id,
|
|
461
|
-
"space_id": space_id,
|
|
462
|
-
"created_at": current_time,
|
|
463
|
-
"updated_at": current_time
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
# Adapt for specific call type
|
|
467
|
-
call_data = adapt_for_call_type(call_data, call_type)
|
|
468
|
-
|
|
469
|
-
# Complete post_data structure
|
|
470
|
-
post_data = {
|
|
471
|
-
"call": call_data,
|
|
472
|
-
"vars": {
|
|
473
|
-
"userVariables": {} # Empty by default, can be filled via overrides
|
|
474
|
-
},
|
|
475
|
-
"envs": {} # Empty by default, can be filled via overrides
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return post_data
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
# ===== OVERRIDE SYSTEM =====
|
|
482
|
-
|
|
483
|
-
def set_nested_value(data: Dict[str, Any], path: str, value: Any) -> None:
|
|
484
|
-
"""
|
|
485
|
-
Set a nested value using dot notation path
|
|
486
|
-
|
|
487
|
-
Args:
|
|
488
|
-
data: Dictionary to modify
|
|
489
|
-
path: Dot-notation path (e.g., "call.call_id" or "vars.userVariables.custom")
|
|
490
|
-
value: Value to set
|
|
491
|
-
"""
|
|
492
|
-
keys = path.split('.')
|
|
493
|
-
current = data
|
|
494
|
-
|
|
495
|
-
# Navigate to the parent of the target key
|
|
496
|
-
for key in keys[:-1]:
|
|
497
|
-
if key not in current:
|
|
498
|
-
current[key] = {}
|
|
499
|
-
current = current[key]
|
|
500
|
-
|
|
501
|
-
# Set the final value
|
|
502
|
-
current[keys[-1]] = value
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
def parse_value(value_str: str) -> Any:
|
|
506
|
-
"""
|
|
507
|
-
Parse a string value into appropriate Python type
|
|
508
|
-
|
|
509
|
-
Args:
|
|
510
|
-
value_str: String representation of value
|
|
511
|
-
|
|
512
|
-
Returns:
|
|
513
|
-
Parsed value (str, int, float, bool, None, or JSON object)
|
|
514
|
-
"""
|
|
515
|
-
# Handle special values
|
|
516
|
-
if value_str.lower() == 'null':
|
|
517
|
-
return None
|
|
518
|
-
elif value_str.lower() == 'true':
|
|
519
|
-
return True
|
|
520
|
-
elif value_str.lower() == 'false':
|
|
521
|
-
return False
|
|
522
|
-
|
|
523
|
-
# Try parsing as number
|
|
524
|
-
try:
|
|
525
|
-
if '.' in value_str:
|
|
526
|
-
return float(value_str)
|
|
527
|
-
else:
|
|
528
|
-
return int(value_str)
|
|
529
|
-
except ValueError:
|
|
530
|
-
pass
|
|
531
|
-
|
|
532
|
-
# Try parsing as JSON (for objects/arrays)
|
|
533
|
-
try:
|
|
534
|
-
return json.loads(value_str)
|
|
535
|
-
except json.JSONDecodeError:
|
|
536
|
-
pass
|
|
537
|
-
|
|
538
|
-
# Return as string
|
|
539
|
-
return value_str
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def apply_overrides(data: Dict[str, Any], overrides: List[str],
|
|
543
|
-
json_overrides: List[str]) -> Dict[str, Any]:
|
|
544
|
-
"""
|
|
545
|
-
Apply override values to data using dot notation paths
|
|
546
|
-
|
|
547
|
-
Args:
|
|
548
|
-
data: Data dictionary to modify
|
|
549
|
-
overrides: List of "path=value" strings
|
|
550
|
-
json_overrides: List of "path=json_value" strings
|
|
551
|
-
|
|
552
|
-
Returns:
|
|
553
|
-
Modified data dictionary
|
|
554
|
-
"""
|
|
555
|
-
data = data.copy()
|
|
556
|
-
|
|
557
|
-
# Apply simple overrides
|
|
558
|
-
for override in overrides:
|
|
559
|
-
if '=' not in override:
|
|
560
|
-
continue
|
|
561
|
-
path, value_str = override.split('=', 1)
|
|
562
|
-
value = parse_value(value_str)
|
|
563
|
-
set_nested_value(data, path, value)
|
|
564
|
-
|
|
565
|
-
# Apply JSON overrides
|
|
566
|
-
for json_override in json_overrides:
|
|
567
|
-
if '=' not in json_override:
|
|
568
|
-
continue
|
|
569
|
-
path, json_str = json_override.split('=', 1)
|
|
570
|
-
try:
|
|
571
|
-
value = json.loads(json_str)
|
|
572
|
-
set_nested_value(data, path, value)
|
|
573
|
-
except json.JSONDecodeError as e:
|
|
574
|
-
print(f"Warning: Invalid JSON in override '{json_override}': {e}")
|
|
575
|
-
|
|
576
|
-
return data
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
def apply_convenience_mappings(data: Dict[str, Any], args: argparse.Namespace) -> Dict[str, Any]:
|
|
580
|
-
"""
|
|
581
|
-
Apply convenience CLI arguments to data structure
|
|
582
|
-
|
|
583
|
-
Args:
|
|
584
|
-
data: Data dictionary to modify
|
|
585
|
-
args: Parsed CLI arguments
|
|
586
|
-
|
|
587
|
-
Returns:
|
|
588
|
-
Modified data dictionary
|
|
589
|
-
"""
|
|
590
|
-
data = data.copy()
|
|
591
|
-
|
|
592
|
-
# Map high-level arguments to specific paths
|
|
593
|
-
if hasattr(args, 'call_id') and args.call_id:
|
|
594
|
-
set_nested_value(data, "call.call_id", args.call_id)
|
|
595
|
-
set_nested_value(data, "call.tag", args.call_id) # tag often matches call_id
|
|
596
|
-
|
|
597
|
-
if hasattr(args, 'project_id') and args.project_id:
|
|
598
|
-
set_nested_value(data, "call.project_id", args.project_id)
|
|
599
|
-
|
|
600
|
-
if hasattr(args, 'space_id') and args.space_id:
|
|
601
|
-
set_nested_value(data, "call.space_id", args.space_id)
|
|
602
|
-
|
|
603
|
-
if hasattr(args, 'call_state') and args.call_state:
|
|
604
|
-
set_nested_value(data, "call.state", args.call_state)
|
|
605
|
-
|
|
606
|
-
if hasattr(args, 'call_direction') and args.call_direction:
|
|
607
|
-
set_nested_value(data, "call.direction", args.call_direction)
|
|
608
|
-
|
|
609
|
-
# Handle from/to addresses with fake generation if needed
|
|
610
|
-
if hasattr(args, 'from_number') and args.from_number:
|
|
611
|
-
# If looks like phone number, use as-is, otherwise generate fake
|
|
612
|
-
if args.from_number.startswith('+') or args.from_number.isdigit():
|
|
613
|
-
set_nested_value(data, "call.from", args.from_number)
|
|
614
|
-
else:
|
|
615
|
-
# Generate fake phone number or SIP address
|
|
616
|
-
call_type = getattr(args, 'call_type', 'webrtc')
|
|
617
|
-
if call_type == 'sip':
|
|
618
|
-
set_nested_value(data, "call.from", f"+1555{uuid.uuid4().hex[:7]}")
|
|
619
|
-
else:
|
|
620
|
-
set_nested_value(data, "call.from", f"{args.from_number}@test.domain")
|
|
621
|
-
|
|
622
|
-
if hasattr(args, 'to_extension') and args.to_extension:
|
|
623
|
-
# Similar logic for 'to' address
|
|
624
|
-
if args.to_extension.startswith('+') or args.to_extension.isdigit():
|
|
625
|
-
set_nested_value(data, "call.to", args.to_extension)
|
|
626
|
-
else:
|
|
627
|
-
call_type = getattr(args, 'call_type', 'webrtc')
|
|
628
|
-
if call_type == 'sip':
|
|
629
|
-
set_nested_value(data, "call.to", f"+1444{uuid.uuid4().hex[:7]}")
|
|
630
|
-
else:
|
|
631
|
-
set_nested_value(data, "call.to", f"{args.to_extension}@test.domain")
|
|
632
|
-
|
|
633
|
-
# Merge user variables
|
|
634
|
-
user_vars = {}
|
|
635
|
-
|
|
636
|
-
# Add user_vars if provided
|
|
637
|
-
if hasattr(args, 'user_vars') and args.user_vars:
|
|
638
|
-
try:
|
|
639
|
-
user_vars.update(json.loads(args.user_vars))
|
|
640
|
-
except json.JSONDecodeError as e:
|
|
641
|
-
print(f"Warning: Invalid JSON in --user-vars: {e}")
|
|
642
|
-
|
|
643
|
-
# Add query_params if provided (merged into userVariables)
|
|
644
|
-
if hasattr(args, 'query_params') and args.query_params:
|
|
645
|
-
try:
|
|
646
|
-
user_vars.update(json.loads(args.query_params))
|
|
647
|
-
except json.JSONDecodeError as e:
|
|
648
|
-
print(f"Warning: Invalid JSON in --query-params: {e}")
|
|
649
|
-
|
|
650
|
-
# Set merged user variables
|
|
651
|
-
if user_vars:
|
|
652
|
-
set_nested_value(data, "vars.userVariables", user_vars)
|
|
653
|
-
|
|
654
|
-
return data
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
def handle_dump_swml(agent: 'AgentBase', args: argparse.Namespace) -> int:
|
|
658
|
-
"""
|
|
659
|
-
Handle SWML dumping with fake post_data and mock request support
|
|
660
|
-
|
|
661
|
-
Args:
|
|
662
|
-
agent: The loaded agent instance
|
|
663
|
-
args: Parsed CLI arguments
|
|
664
|
-
|
|
665
|
-
Returns:
|
|
666
|
-
Exit code (0 for success, 1 for error)
|
|
667
|
-
"""
|
|
668
|
-
if not args.raw:
|
|
669
|
-
print("\nGenerating SWML document...")
|
|
670
|
-
if args.verbose:
|
|
671
|
-
print(f"Agent: {agent.get_name()}")
|
|
672
|
-
print(f"Route: {agent.route}")
|
|
673
|
-
|
|
674
|
-
# Show loaded skills
|
|
675
|
-
skills = agent.list_skills()
|
|
676
|
-
if skills:
|
|
677
|
-
print(f"Skills: {', '.join(skills)}")
|
|
678
|
-
|
|
679
|
-
# Show available functions
|
|
680
|
-
if hasattr(agent, '_swaig_functions') and agent._swaig_functions:
|
|
681
|
-
print(f"Functions: {', '.join(agent._swaig_functions.keys())}")
|
|
682
|
-
|
|
683
|
-
print("-" * 60)
|
|
684
|
-
|
|
685
|
-
try:
|
|
686
|
-
# Generate fake SWML post_data
|
|
687
|
-
post_data = generate_fake_swml_post_data(
|
|
688
|
-
call_type=args.call_type,
|
|
689
|
-
call_direction=args.call_direction,
|
|
690
|
-
call_state=args.call_state
|
|
691
|
-
)
|
|
692
|
-
|
|
693
|
-
# Apply convenience mappings from CLI args
|
|
694
|
-
post_data = apply_convenience_mappings(post_data, args)
|
|
695
|
-
|
|
696
|
-
# Apply explicit overrides
|
|
697
|
-
post_data = apply_overrides(post_data, args.override, args.override_json)
|
|
698
|
-
|
|
699
|
-
# Parse headers for mock request
|
|
700
|
-
headers = {}
|
|
701
|
-
for header in args.header:
|
|
702
|
-
if '=' in header:
|
|
703
|
-
key, value = header.split('=', 1)
|
|
704
|
-
headers[key] = value
|
|
705
|
-
|
|
706
|
-
# Parse query params for mock request (separate from userVariables)
|
|
707
|
-
query_params = {}
|
|
708
|
-
if args.query_params:
|
|
709
|
-
try:
|
|
710
|
-
query_params = json.loads(args.query_params)
|
|
711
|
-
except json.JSONDecodeError as e:
|
|
712
|
-
if not args.raw:
|
|
713
|
-
print(f"Warning: Invalid JSON in --query-params: {e}")
|
|
714
|
-
|
|
715
|
-
# Parse request body
|
|
716
|
-
request_body = {}
|
|
717
|
-
if args.body:
|
|
718
|
-
try:
|
|
719
|
-
request_body = json.loads(args.body)
|
|
720
|
-
except json.JSONDecodeError as e:
|
|
721
|
-
if not args.raw:
|
|
722
|
-
print(f"Warning: Invalid JSON in --body: {e}")
|
|
723
|
-
|
|
724
|
-
# Create mock request object
|
|
725
|
-
mock_request = create_mock_request(
|
|
726
|
-
method=args.method,
|
|
727
|
-
headers=headers,
|
|
728
|
-
query_params=query_params,
|
|
729
|
-
body=request_body
|
|
730
|
-
)
|
|
731
|
-
|
|
732
|
-
if args.verbose and not args.raw:
|
|
733
|
-
print(f"Using fake SWML post_data:")
|
|
734
|
-
print(json.dumps(post_data, indent=2))
|
|
735
|
-
print(f"\nMock request headers: {dict(mock_request.headers.items())}")
|
|
736
|
-
print(f"Mock request query params: {dict(mock_request.query_params.items())}")
|
|
737
|
-
print(f"Mock request method: {mock_request.method}")
|
|
738
|
-
print("-" * 60)
|
|
739
|
-
|
|
740
|
-
# For dynamic agents, call on_swml_request if available
|
|
741
|
-
if hasattr(agent, 'on_swml_request'):
|
|
742
|
-
try:
|
|
743
|
-
# Dynamic agents expect (request_data, callback_path, request)
|
|
744
|
-
call_id = post_data.get('call', {}).get('call_id', 'test-call-id')
|
|
745
|
-
modifications = agent.on_swml_request(post_data, "/swml", mock_request)
|
|
746
|
-
|
|
747
|
-
if args.verbose and not args.raw:
|
|
748
|
-
print(f"Dynamic agent modifications: {modifications}")
|
|
749
|
-
|
|
750
|
-
# Generate SWML with modifications
|
|
751
|
-
swml_doc = agent._render_swml(call_id, modifications)
|
|
752
|
-
except Exception as e:
|
|
753
|
-
if args.verbose and not args.raw:
|
|
754
|
-
print(f"Dynamic agent callback failed, falling back to static SWML: {e}")
|
|
755
|
-
# Fall back to static SWML generation
|
|
756
|
-
swml_doc = agent._render_swml()
|
|
757
|
-
else:
|
|
758
|
-
# Static agent - generate SWML normally
|
|
759
|
-
swml_doc = agent._render_swml()
|
|
760
|
-
|
|
761
|
-
if args.raw:
|
|
762
|
-
# Temporarily restore print for JSON output
|
|
763
|
-
if '--raw' in sys.argv and 'original_print' in globals():
|
|
764
|
-
import builtins
|
|
765
|
-
builtins.print = original_print
|
|
766
|
-
|
|
767
|
-
# Output only the raw JSON for piping to jq/yq
|
|
768
|
-
print(swml_doc)
|
|
769
|
-
else:
|
|
770
|
-
# Normal output with headers
|
|
771
|
-
print("SWML Document:")
|
|
772
|
-
print("=" * 50)
|
|
773
|
-
print(swml_doc)
|
|
774
|
-
print("=" * 50)
|
|
775
|
-
|
|
776
|
-
if args.verbose:
|
|
777
|
-
# Parse and show formatted JSON for better readability
|
|
778
|
-
try:
|
|
779
|
-
swml_parsed = json.loads(swml_doc)
|
|
780
|
-
print("\nFormatted SWML:")
|
|
781
|
-
print(json.dumps(swml_parsed, indent=2))
|
|
782
|
-
except json.JSONDecodeError:
|
|
783
|
-
print("\nNote: SWML document is not valid JSON format")
|
|
784
|
-
|
|
785
|
-
return 0
|
|
786
|
-
|
|
787
|
-
except Exception as e:
|
|
788
|
-
if args.raw:
|
|
789
|
-
# For raw mode, output error to stderr to not interfere with JSON output
|
|
790
|
-
original_print(f"Error generating SWML: {e}", file=sys.stderr)
|
|
791
|
-
if args.verbose:
|
|
792
|
-
import traceback
|
|
793
|
-
traceback.print_exc(file=sys.stderr)
|
|
794
|
-
else:
|
|
795
|
-
print(f"Error generating SWML: {e}")
|
|
796
|
-
if args.verbose:
|
|
797
|
-
import traceback
|
|
798
|
-
traceback.print_exc()
|
|
799
|
-
return 1
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
def setup_raw_mode_suppression():
|
|
803
|
-
"""Set up output suppression for raw mode using central logging system"""
|
|
804
|
-
# The central logging system is already configured via environment variable
|
|
805
|
-
# Just suppress any remaining warnings
|
|
806
|
-
warnings.filterwarnings("ignore")
|
|
807
|
-
|
|
808
|
-
# Capture and suppress print statements in raw mode if needed
|
|
809
|
-
def suppressed_print(*args, **kwargs):
|
|
810
|
-
pass
|
|
811
|
-
|
|
812
|
-
# Replace print function globally for raw mode
|
|
813
|
-
import builtins
|
|
814
|
-
builtins.print = suppressed_print
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
def generate_comprehensive_post_data(function_name: str, args: Dict[str, Any],
|
|
818
|
-
custom_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
819
|
-
"""
|
|
820
|
-
Generate comprehensive post_data that matches what SignalWire would send
|
|
821
|
-
|
|
822
|
-
Args:
|
|
823
|
-
function_name: Name of the SWAIG function being called
|
|
824
|
-
args: Function arguments
|
|
825
|
-
custom_data: Optional custom data to override defaults
|
|
826
|
-
|
|
827
|
-
Returns:
|
|
828
|
-
Complete post_data dict with all possible keys
|
|
829
|
-
"""
|
|
830
|
-
call_id = str(uuid.uuid4())
|
|
831
|
-
session_id = str(uuid.uuid4())
|
|
832
|
-
current_time = datetime.now().isoformat()
|
|
833
|
-
|
|
834
|
-
# Generate meta_data_token (normally function name + webhook URL hash)
|
|
835
|
-
meta_data_token = hashlib.md5(f"{function_name}_test_webhook".encode()).hexdigest()[:16]
|
|
836
|
-
|
|
837
|
-
base_data = {
|
|
838
|
-
# Core identification
|
|
839
|
-
"function": function_name,
|
|
840
|
-
"argument": args,
|
|
841
|
-
"call_id": call_id,
|
|
842
|
-
"call_session_id": session_id,
|
|
843
|
-
"node_id": "test-node-001",
|
|
844
|
-
|
|
845
|
-
# Metadata and function-level data
|
|
846
|
-
"meta_data_token": meta_data_token,
|
|
847
|
-
"meta_data": {
|
|
848
|
-
"test_mode": True,
|
|
849
|
-
"function_name": function_name,
|
|
850
|
-
"last_updated": current_time
|
|
851
|
-
},
|
|
852
|
-
|
|
853
|
-
# Global application data
|
|
854
|
-
"global_data": {
|
|
855
|
-
"app_name": "test_application",
|
|
856
|
-
"environment": "test",
|
|
857
|
-
"user_preferences": {"language": "en"},
|
|
858
|
-
"session_data": {"start_time": current_time}
|
|
859
|
-
},
|
|
860
|
-
|
|
861
|
-
# Conversation context
|
|
862
|
-
"call_log": [
|
|
863
|
-
{
|
|
864
|
-
"role": "system",
|
|
865
|
-
"content": "You are a helpful AI assistant created with SignalWire AI Agents."
|
|
866
|
-
},
|
|
867
|
-
{
|
|
868
|
-
"role": "user",
|
|
869
|
-
"content": f"Please call the {function_name} function"
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
"role": "assistant",
|
|
873
|
-
"content": f"I'll call the {function_name} function for you.",
|
|
874
|
-
"tool_calls": [
|
|
875
|
-
{
|
|
876
|
-
"id": f"call_{call_id[:8]}",
|
|
877
|
-
"type": "function",
|
|
878
|
-
"function": {
|
|
879
|
-
"name": function_name,
|
|
880
|
-
"arguments": json.dumps(args)
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
]
|
|
884
|
-
}
|
|
885
|
-
],
|
|
886
|
-
"raw_call_log": [
|
|
887
|
-
{
|
|
888
|
-
"role": "system",
|
|
889
|
-
"content": "You are a helpful AI assistant created with SignalWire AI Agents."
|
|
890
|
-
},
|
|
891
|
-
{
|
|
892
|
-
"role": "user",
|
|
893
|
-
"content": "Hello"
|
|
894
|
-
},
|
|
895
|
-
{
|
|
896
|
-
"role": "assistant",
|
|
897
|
-
"content": "Hello! How can I help you today?"
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
"role": "user",
|
|
901
|
-
"content": f"Please call the {function_name} function"
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
"role": "assistant",
|
|
905
|
-
"content": f"I'll call the {function_name} function for you.",
|
|
906
|
-
"tool_calls": [
|
|
907
|
-
{
|
|
908
|
-
"id": f"call_{call_id[:8]}",
|
|
909
|
-
"type": "function",
|
|
910
|
-
"function": {
|
|
911
|
-
"name": function_name,
|
|
912
|
-
"arguments": json.dumps(args)
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
]
|
|
916
|
-
}
|
|
917
|
-
],
|
|
918
|
-
|
|
919
|
-
# SWML and prompt variables
|
|
920
|
-
"prompt_vars": {
|
|
921
|
-
# From SWML prompt variables
|
|
922
|
-
"ai_instructions": "You are a helpful assistant",
|
|
923
|
-
"temperature": 0.7,
|
|
924
|
-
"max_tokens": 1000,
|
|
925
|
-
# From global_data
|
|
926
|
-
"app_name": "test_application",
|
|
927
|
-
"environment": "test",
|
|
928
|
-
"user_preferences": {"language": "en"},
|
|
929
|
-
"session_data": {"start_time": current_time},
|
|
930
|
-
# SWML system variables
|
|
931
|
-
"current_timestamp": current_time,
|
|
932
|
-
"call_duration": "00:02:15",
|
|
933
|
-
"caller_number": "+15551234567",
|
|
934
|
-
"to_number": "+15559876543"
|
|
935
|
-
},
|
|
936
|
-
|
|
937
|
-
# Permission flags (from SWML parameters)
|
|
938
|
-
"swaig_allow_swml": True,
|
|
939
|
-
"swaig_post_conversation": True,
|
|
940
|
-
"swaig_post_swml_vars": True,
|
|
941
|
-
|
|
942
|
-
# Additional context
|
|
943
|
-
"http_method": "POST",
|
|
944
|
-
"webhook_url": f"https://test.example.com/webhook/{function_name}",
|
|
945
|
-
"user_agent": "SignalWire-AI-Agent/1.0",
|
|
946
|
-
"request_headers": {
|
|
947
|
-
"Content-Type": "application/json",
|
|
948
|
-
"User-Agent": "SignalWire-AI-Agent/1.0",
|
|
949
|
-
"X-Signalwire-Call-Id": call_id,
|
|
950
|
-
"X-Signalwire-Session-Id": session_id
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
# Merge custom data if provided
|
|
955
|
-
if custom_data:
|
|
956
|
-
def deep_merge(base: Dict, custom: Dict) -> Dict:
|
|
957
|
-
result = base.copy()
|
|
958
|
-
for key, value in custom.items():
|
|
959
|
-
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
960
|
-
result[key] = deep_merge(result[key], value)
|
|
961
|
-
else:
|
|
962
|
-
result[key] = value
|
|
963
|
-
return result
|
|
964
|
-
|
|
965
|
-
base_data = deep_merge(base_data, custom_data)
|
|
966
|
-
|
|
967
|
-
return base_data
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
def generate_minimal_post_data(function_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
971
|
-
"""Generate minimal post_data with only essential keys"""
|
|
972
|
-
return {
|
|
973
|
-
"function": function_name,
|
|
974
|
-
"argument": args,
|
|
975
|
-
"call_id": str(uuid.uuid4()),
|
|
976
|
-
"meta_data": {},
|
|
977
|
-
"global_data": {}
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
def simple_template_expand(template: str, data: Dict[str, Any]) -> str:
|
|
982
|
-
"""
|
|
983
|
-
Simple template expansion for DataMap testing
|
|
984
|
-
Supports both ${key} and %{key} syntax with nested object access and array indexing
|
|
985
|
-
|
|
986
|
-
Args:
|
|
987
|
-
template: Template string with ${} or %{} variables
|
|
988
|
-
data: Data dictionary for expansion
|
|
989
|
-
|
|
990
|
-
Returns:
|
|
991
|
-
Expanded string
|
|
992
|
-
"""
|
|
993
|
-
if not template:
|
|
994
|
-
return ""
|
|
995
|
-
|
|
996
|
-
result = template
|
|
997
|
-
|
|
998
|
-
# Handle both ${variable.path} and %{variable.path} syntax
|
|
999
|
-
patterns = [
|
|
1000
|
-
r'\$\{([^}]+)\}', # ${variable} syntax
|
|
1001
|
-
r'%\{([^}]+)\}' # %{variable} syntax
|
|
1002
|
-
]
|
|
1003
|
-
|
|
1004
|
-
for pattern in patterns:
|
|
1005
|
-
for match in re.finditer(pattern, result):
|
|
1006
|
-
var_path = match.group(1)
|
|
1007
|
-
|
|
1008
|
-
# Handle array indexing syntax like "array[0].joke"
|
|
1009
|
-
if '[' in var_path and ']' in var_path:
|
|
1010
|
-
# Split path with array indexing
|
|
1011
|
-
parts = []
|
|
1012
|
-
current_part = ""
|
|
1013
|
-
i = 0
|
|
1014
|
-
while i < len(var_path):
|
|
1015
|
-
if var_path[i] == '[':
|
|
1016
|
-
if current_part:
|
|
1017
|
-
parts.append(current_part)
|
|
1018
|
-
current_part = ""
|
|
1019
|
-
# Find the closing bracket
|
|
1020
|
-
j = i + 1
|
|
1021
|
-
while j < len(var_path) and var_path[j] != ']':
|
|
1022
|
-
j += 1
|
|
1023
|
-
if j < len(var_path):
|
|
1024
|
-
index = var_path[i+1:j]
|
|
1025
|
-
parts.append(f"[{index}]")
|
|
1026
|
-
i = j + 1
|
|
1027
|
-
if i < len(var_path) and var_path[i] == '.':
|
|
1028
|
-
i += 1 # Skip the dot after ]
|
|
1029
|
-
else:
|
|
1030
|
-
current_part += var_path[i]
|
|
1031
|
-
i += 1
|
|
1032
|
-
elif var_path[i] == '.':
|
|
1033
|
-
if current_part:
|
|
1034
|
-
parts.append(current_part)
|
|
1035
|
-
current_part = ""
|
|
1036
|
-
i += 1
|
|
1037
|
-
else:
|
|
1038
|
-
current_part += var_path[i]
|
|
1039
|
-
i += 1
|
|
1040
|
-
|
|
1041
|
-
if current_part:
|
|
1042
|
-
parts.append(current_part)
|
|
1043
|
-
|
|
1044
|
-
# Navigate through the data structure
|
|
1045
|
-
value = data
|
|
1046
|
-
try:
|
|
1047
|
-
for part in parts:
|
|
1048
|
-
if part.startswith('[') and part.endswith(']'):
|
|
1049
|
-
# Array index
|
|
1050
|
-
index = int(part[1:-1])
|
|
1051
|
-
if isinstance(value, list) and 0 <= index < len(value):
|
|
1052
|
-
value = value[index]
|
|
1053
|
-
else:
|
|
1054
|
-
value = f"<MISSING:{var_path}>"
|
|
1055
|
-
break
|
|
1056
|
-
else:
|
|
1057
|
-
# Object property
|
|
1058
|
-
if isinstance(value, dict) and part in value:
|
|
1059
|
-
value = value[part]
|
|
1060
|
-
else:
|
|
1061
|
-
value = f"<MISSING:{var_path}>"
|
|
1062
|
-
break
|
|
1063
|
-
except (ValueError, TypeError, IndexError):
|
|
1064
|
-
value = f"<MISSING:{var_path}>"
|
|
1065
|
-
|
|
1066
|
-
else:
|
|
1067
|
-
# Regular nested object access (no array indexing)
|
|
1068
|
-
path_parts = var_path.split('.')
|
|
1069
|
-
value = data
|
|
1070
|
-
for part in path_parts:
|
|
1071
|
-
if isinstance(value, dict) and part in value:
|
|
1072
|
-
value = value[part]
|
|
1073
|
-
else:
|
|
1074
|
-
value = f"<MISSING:{var_path}>"
|
|
1075
|
-
break
|
|
1076
|
-
|
|
1077
|
-
# Replace the variable with its value
|
|
1078
|
-
result = result.replace(match.group(0), str(value))
|
|
1079
|
-
|
|
1080
|
-
return result
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
def execute_datamap_function(datamap_config: Dict[str, Any], args: Dict[str, Any],
|
|
1084
|
-
verbose: bool = False) -> Dict[str, Any]:
|
|
1085
|
-
"""
|
|
1086
|
-
Execute a DataMap function by processing its configuration and making HTTP requests
|
|
1087
|
-
|
|
1088
|
-
Args:
|
|
1089
|
-
datamap_config: The complete DataMap function configuration
|
|
1090
|
-
args: Function arguments
|
|
1091
|
-
verbose: Whether to show verbose output
|
|
1092
|
-
|
|
1093
|
-
Returns:
|
|
1094
|
-
Final function result
|
|
1095
|
-
"""
|
|
1096
|
-
# Extract data_map configuration
|
|
1097
|
-
data_map = datamap_config.get('data_map', {})
|
|
1098
|
-
|
|
1099
|
-
# Check if this is a simple DataMap (webhooks directly) or complex (with expressions)
|
|
1100
|
-
if 'webhooks' in data_map and 'expressions' not in data_map:
|
|
1101
|
-
# Simple DataMap structure - webhooks directly under data_map
|
|
1102
|
-
webhooks = data_map.get('webhooks', [])
|
|
1103
|
-
fallback_output = data_map.get('output', 'Function completed')
|
|
1104
|
-
if verbose:
|
|
1105
|
-
print(f"Simple DataMap structure with {len(webhooks)} webhook(s)")
|
|
1106
|
-
else:
|
|
1107
|
-
# Complex DataMap structure - with expressions containing webhooks
|
|
1108
|
-
expressions = data_map.get('expressions', [])
|
|
1109
|
-
matched_expression = None
|
|
1110
|
-
|
|
1111
|
-
if verbose:
|
|
1112
|
-
print(f"Complex DataMap structure with {len(expressions)} expression(s)...")
|
|
1113
|
-
|
|
1114
|
-
for expr in expressions:
|
|
1115
|
-
pattern = expr.get('pattern', '.*') # Default to match everything
|
|
1116
|
-
string_value = expr.get('string', '')
|
|
1117
|
-
|
|
1118
|
-
# Create a test string from the arguments
|
|
1119
|
-
test_string = json.dumps(args)
|
|
1120
|
-
|
|
1121
|
-
if verbose:
|
|
1122
|
-
print(f" Testing pattern '{pattern}' against '{test_string}'")
|
|
1123
|
-
|
|
1124
|
-
# Use regex to match
|
|
1125
|
-
try:
|
|
1126
|
-
if re.search(pattern, test_string):
|
|
1127
|
-
matched_expression = expr
|
|
1128
|
-
if verbose:
|
|
1129
|
-
print(f" ✓ Pattern matched!")
|
|
1130
|
-
break
|
|
1131
|
-
elif re.search(pattern, string_value):
|
|
1132
|
-
matched_expression = expr
|
|
1133
|
-
if verbose:
|
|
1134
|
-
print(f" ✓ Pattern matched against string value!")
|
|
1135
|
-
break
|
|
1136
|
-
except re.error as e:
|
|
1137
|
-
if verbose:
|
|
1138
|
-
print(f" ✗ Invalid regex pattern: {e}")
|
|
1139
|
-
|
|
1140
|
-
if not matched_expression:
|
|
1141
|
-
if verbose:
|
|
1142
|
-
print(" No expressions matched, using first expression if available")
|
|
1143
|
-
matched_expression = expressions[0] if expressions else {}
|
|
1144
|
-
|
|
1145
|
-
# Get webhooks from matched expression
|
|
1146
|
-
webhooks = matched_expression.get('webhooks', [])
|
|
1147
|
-
fallback_output = matched_expression.get('output', 'Function completed')
|
|
1148
|
-
webhook_result = None
|
|
1149
|
-
|
|
1150
|
-
if verbose:
|
|
1151
|
-
print(f"Processing {len(webhooks)} webhook(s)...")
|
|
1152
|
-
|
|
1153
|
-
for i, webhook in enumerate(webhooks):
|
|
1154
|
-
url = webhook.get('url', '')
|
|
1155
|
-
method = webhook.get('method', 'POST').upper()
|
|
1156
|
-
headers = webhook.get('headers', {})
|
|
1157
|
-
|
|
1158
|
-
if verbose:
|
|
1159
|
-
print(f" Webhook {i+1}: {method} {url}")
|
|
1160
|
-
|
|
1161
|
-
# Prepare request data
|
|
1162
|
-
request_data = args.copy()
|
|
1163
|
-
|
|
1164
|
-
# Expand URL template with arguments
|
|
1165
|
-
template_context = {"args": args, "array": [], **args}
|
|
1166
|
-
expanded_url = simple_template_expand(url, template_context)
|
|
1167
|
-
|
|
1168
|
-
if verbose:
|
|
1169
|
-
print(f" Original URL: {url}")
|
|
1170
|
-
print(f" Template context: {template_context}")
|
|
1171
|
-
print(f" Expanded URL: {expanded_url}")
|
|
1172
|
-
|
|
1173
|
-
try:
|
|
1174
|
-
if method == 'GET':
|
|
1175
|
-
response = requests.get(expanded_url, params=request_data, headers=headers, timeout=10)
|
|
1176
|
-
else:
|
|
1177
|
-
response = requests.post(expanded_url, json=request_data, headers=headers, timeout=10)
|
|
1178
|
-
|
|
1179
|
-
if response.status_code == 200:
|
|
1180
|
-
try:
|
|
1181
|
-
webhook_data = response.json()
|
|
1182
|
-
if verbose:
|
|
1183
|
-
print(f" ✓ Webhook succeeded: {response.status_code}")
|
|
1184
|
-
print(f" Response: {json.dumps(webhook_data, indent=4)}")
|
|
1185
|
-
|
|
1186
|
-
# Process output template if defined in this webhook
|
|
1187
|
-
webhook_output = webhook.get('output')
|
|
1188
|
-
if webhook_output:
|
|
1189
|
-
# Create context for template expansion
|
|
1190
|
-
template_context = {
|
|
1191
|
-
"args": args,
|
|
1192
|
-
"array": webhook_data if isinstance(webhook_data, list) else [webhook_data]
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
# Add webhook_data contents to context if it's a dict
|
|
1196
|
-
if isinstance(webhook_data, dict):
|
|
1197
|
-
template_context.update(webhook_data)
|
|
1198
|
-
|
|
1199
|
-
if isinstance(webhook_output, dict):
|
|
1200
|
-
# Process dict output template (e.g., {"response": "template", "action": [...]} )
|
|
1201
|
-
webhook_result = {}
|
|
1202
|
-
for key, template in webhook_output.items():
|
|
1203
|
-
if isinstance(template, str):
|
|
1204
|
-
webhook_result[key] = simple_template_expand(template, template_context)
|
|
1205
|
-
else:
|
|
1206
|
-
webhook_result[key] = template
|
|
1207
|
-
elif isinstance(webhook_output, str):
|
|
1208
|
-
# Simple string template
|
|
1209
|
-
webhook_result = {"response": simple_template_expand(webhook_output, template_context)}
|
|
1210
|
-
else:
|
|
1211
|
-
# Other types
|
|
1212
|
-
webhook_result = {"response": str(webhook_output)}
|
|
1213
|
-
|
|
1214
|
-
if verbose:
|
|
1215
|
-
print(f" Processed output: {webhook_result}")
|
|
1216
|
-
else:
|
|
1217
|
-
webhook_result = webhook_data if isinstance(webhook_data, dict) else {"response": str(webhook_data)}
|
|
1218
|
-
|
|
1219
|
-
break
|
|
1220
|
-
except json.JSONDecodeError:
|
|
1221
|
-
webhook_result = {"response": response.text}
|
|
1222
|
-
if verbose:
|
|
1223
|
-
print(f" ✓ Webhook succeeded (text): {response.status_code}")
|
|
1224
|
-
break
|
|
1225
|
-
else:
|
|
1226
|
-
if verbose:
|
|
1227
|
-
print(f" ✗ Webhook failed: {response.status_code}")
|
|
1228
|
-
except requests.RequestException as e:
|
|
1229
|
-
if verbose:
|
|
1230
|
-
print(f" ✗ Webhook request failed: {e}")
|
|
1231
|
-
|
|
1232
|
-
# If no webhook succeeded, use fallback
|
|
1233
|
-
if webhook_result is None:
|
|
1234
|
-
if verbose:
|
|
1235
|
-
print("All webhooks failed, using fallback output...")
|
|
1236
|
-
|
|
1237
|
-
# Use the fallback output determined earlier
|
|
1238
|
-
output_template = fallback_output
|
|
1239
|
-
|
|
1240
|
-
# Handle both string and dict output templates
|
|
1241
|
-
if isinstance(output_template, dict):
|
|
1242
|
-
# Process dict output template (e.g., {"response": "template"})
|
|
1243
|
-
webhook_result = {}
|
|
1244
|
-
for key, template in output_template.items():
|
|
1245
|
-
if isinstance(template, str):
|
|
1246
|
-
webhook_result[key] = simple_template_expand(template, {"args": args})
|
|
1247
|
-
else:
|
|
1248
|
-
webhook_result[key] = template
|
|
1249
|
-
elif isinstance(output_template, str):
|
|
1250
|
-
# Simple string template
|
|
1251
|
-
webhook_result = {"response": simple_template_expand(output_template, {"args": args})}
|
|
1252
|
-
else:
|
|
1253
|
-
# Other types (shouldn't happen but be safe)
|
|
1254
|
-
webhook_result = {"response": str(output_template)}
|
|
1255
|
-
|
|
1256
|
-
if verbose:
|
|
1257
|
-
print(f"Fallback result = {webhook_result}")
|
|
1258
|
-
|
|
1259
|
-
# Process foreach (not implemented in this simple version)
|
|
1260
|
-
# This would iterate over array results and apply templates
|
|
1261
|
-
|
|
1262
|
-
return webhook_result
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
def execute_external_webhook_function(func: 'SWAIGFunction', function_name: str, function_args: Dict[str, Any],
|
|
1266
|
-
post_data: Dict[str, Any], verbose: bool = False) -> Dict[str, Any]:
|
|
1267
|
-
"""
|
|
1268
|
-
Execute an external webhook SWAIG function by making an HTTP request to the external service.
|
|
1269
|
-
This simulates what SignalWire would do when calling an external webhook function.
|
|
1270
|
-
|
|
1271
|
-
Args:
|
|
1272
|
-
func: The SWAIGFunction object with webhook_url
|
|
1273
|
-
function_name: Name of the function being called
|
|
1274
|
-
function_args: Parsed function arguments
|
|
1275
|
-
post_data: Complete post data to send to the webhook
|
|
1276
|
-
verbose: Whether to show verbose output
|
|
1277
|
-
|
|
1278
|
-
Returns:
|
|
1279
|
-
Response from the external webhook service
|
|
1280
|
-
"""
|
|
1281
|
-
webhook_url = func.webhook_url
|
|
1282
|
-
|
|
1283
|
-
if verbose:
|
|
1284
|
-
print(f"\nCalling EXTERNAL webhook: {function_name}")
|
|
1285
|
-
print(f"URL: {webhook_url}")
|
|
1286
|
-
print(f"Arguments: {json.dumps(function_args, indent=2)}")
|
|
1287
|
-
print("-" * 60)
|
|
1288
|
-
|
|
1289
|
-
# Prepare the SWAIG function call payload that SignalWire would send
|
|
1290
|
-
swaig_payload = {
|
|
1291
|
-
"function": function_name,
|
|
1292
|
-
"argument": {
|
|
1293
|
-
"parsed": [function_args] if function_args else [{}],
|
|
1294
|
-
"raw": json.dumps(function_args) if function_args else "{}"
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
# Add call_id and other data from post_data if available
|
|
1299
|
-
if "call_id" in post_data:
|
|
1300
|
-
swaig_payload["call_id"] = post_data["call_id"]
|
|
1301
|
-
|
|
1302
|
-
# Add any other relevant fields from post_data
|
|
1303
|
-
for key in ["call", "device", "vars"]:
|
|
1304
|
-
if key in post_data:
|
|
1305
|
-
swaig_payload[key] = post_data[key]
|
|
1306
|
-
|
|
1307
|
-
if verbose:
|
|
1308
|
-
print(f"Sending payload: {json.dumps(swaig_payload, indent=2)}")
|
|
1309
|
-
print(f"Making POST request to: {webhook_url}")
|
|
1310
|
-
|
|
1311
|
-
try:
|
|
1312
|
-
# Make the HTTP request to the external webhook
|
|
1313
|
-
headers = {
|
|
1314
|
-
"Content-Type": "application/json",
|
|
1315
|
-
"User-Agent": "SignalWire-SWAIG-Test/1.0"
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
response = requests.post(
|
|
1319
|
-
webhook_url,
|
|
1320
|
-
json=swaig_payload,
|
|
1321
|
-
headers=headers,
|
|
1322
|
-
timeout=30 # 30 second timeout
|
|
1323
|
-
)
|
|
1324
|
-
|
|
1325
|
-
if verbose:
|
|
1326
|
-
print(f"Response status: {response.status_code}")
|
|
1327
|
-
print(f"Response headers: {dict(response.headers)}")
|
|
1328
|
-
|
|
1329
|
-
if response.status_code == 200:
|
|
1330
|
-
try:
|
|
1331
|
-
result = response.json()
|
|
1332
|
-
if verbose:
|
|
1333
|
-
print(f"✓ External webhook succeeded")
|
|
1334
|
-
print(f"Response: {json.dumps(result, indent=2)}")
|
|
1335
|
-
return result
|
|
1336
|
-
except json.JSONDecodeError:
|
|
1337
|
-
# If response is not JSON, wrap it in a response field
|
|
1338
|
-
result = {"response": response.text}
|
|
1339
|
-
if verbose:
|
|
1340
|
-
print(f"✓ External webhook succeeded (text response)")
|
|
1341
|
-
print(f"Response: {response.text}")
|
|
1342
|
-
return result
|
|
1343
|
-
else:
|
|
1344
|
-
error_msg = f"External webhook returned HTTP {response.status_code}"
|
|
1345
|
-
if verbose:
|
|
1346
|
-
print(f"✗ External webhook failed: {error_msg}")
|
|
1347
|
-
try:
|
|
1348
|
-
error_detail = response.json()
|
|
1349
|
-
print(f"Error details: {json.dumps(error_detail, indent=2)}")
|
|
1350
|
-
except:
|
|
1351
|
-
print(f"Error response: {response.text}")
|
|
1352
|
-
|
|
1353
|
-
return {
|
|
1354
|
-
"error": error_msg,
|
|
1355
|
-
"status_code": response.status_code,
|
|
1356
|
-
"response": response.text
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
except requests.Timeout:
|
|
1360
|
-
error_msg = f"External webhook timed out after 30 seconds"
|
|
1361
|
-
if verbose:
|
|
1362
|
-
print(f"✗ {error_msg}")
|
|
1363
|
-
return {"error": error_msg}
|
|
1364
|
-
|
|
1365
|
-
except requests.ConnectionError as e:
|
|
1366
|
-
error_msg = f"Could not connect to external webhook: {e}"
|
|
1367
|
-
if verbose:
|
|
1368
|
-
print(f"✗ {error_msg}")
|
|
1369
|
-
return {"error": error_msg}
|
|
1370
|
-
|
|
1371
|
-
except requests.RequestException as e:
|
|
1372
|
-
error_msg = f"Request to external webhook failed: {e}"
|
|
1373
|
-
if verbose:
|
|
1374
|
-
print(f"✗ {error_msg}")
|
|
1375
|
-
return {"error": error_msg}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
def discover_agents_in_file(agent_path: str) -> List[Dict[str, Any]]:
|
|
1379
|
-
"""
|
|
1380
|
-
Discover all available agents in a Python file without instantiating them
|
|
1381
|
-
|
|
1382
|
-
Args:
|
|
1383
|
-
agent_path: Path to the Python file containing agents
|
|
1384
|
-
|
|
1385
|
-
Returns:
|
|
1386
|
-
List of dictionaries with agent information
|
|
1387
|
-
|
|
1388
|
-
Raises:
|
|
1389
|
-
ImportError: If the file cannot be imported
|
|
1390
|
-
FileNotFoundError: If the file doesn't exist
|
|
1391
|
-
"""
|
|
1392
|
-
agent_path = Path(agent_path).resolve()
|
|
1393
|
-
|
|
1394
|
-
if not agent_path.exists():
|
|
1395
|
-
raise FileNotFoundError(f"Agent file not found: {agent_path}")
|
|
1396
|
-
|
|
1397
|
-
if not agent_path.suffix == '.py':
|
|
1398
|
-
raise ValueError(f"Agent file must be a Python file (.py): {agent_path}")
|
|
1399
|
-
|
|
1400
|
-
# Load the module, but prevent main() execution by setting __name__ to something other than "__main__"
|
|
1401
|
-
spec = importlib.util.spec_from_file_location("agent_module", agent_path)
|
|
1402
|
-
module = importlib.util.module_from_spec(spec)
|
|
1403
|
-
|
|
1404
|
-
try:
|
|
1405
|
-
# Set __name__ to prevent if __name__ == "__main__": blocks from running
|
|
1406
|
-
module.__name__ = "agent_module"
|
|
1407
|
-
spec.loader.exec_module(module)
|
|
1408
|
-
except Exception as e:
|
|
1409
|
-
raise ImportError(f"Failed to load agent module: {e}")
|
|
1410
|
-
|
|
1411
|
-
agents_found = []
|
|
1412
|
-
|
|
1413
|
-
# Look for AgentBase instances
|
|
1414
|
-
for name, obj in vars(module).items():
|
|
1415
|
-
if isinstance(obj, AgentBase):
|
|
1416
|
-
agents_found.append({
|
|
1417
|
-
'name': name,
|
|
1418
|
-
'class_name': obj.__class__.__name__,
|
|
1419
|
-
'type': 'instance',
|
|
1420
|
-
'agent_name': getattr(obj, 'name', 'Unknown'),
|
|
1421
|
-
'route': getattr(obj, 'route', 'Unknown'),
|
|
1422
|
-
'description': obj.__class__.__doc__,
|
|
1423
|
-
'object': obj
|
|
1424
|
-
})
|
|
1425
|
-
|
|
1426
|
-
# Look for AgentBase subclasses (that could be instantiated)
|
|
1427
|
-
for name, obj in vars(module).items():
|
|
1428
|
-
if (isinstance(obj, type) and
|
|
1429
|
-
issubclass(obj, AgentBase) and
|
|
1430
|
-
obj != AgentBase):
|
|
1431
|
-
# Check if we already found an instance of this class
|
|
1432
|
-
instance_found = any(agent['class_name'] == name for agent in agents_found)
|
|
1433
|
-
if not instance_found:
|
|
1434
|
-
try:
|
|
1435
|
-
# Try to get class information without instantiating
|
|
1436
|
-
agent_info = {
|
|
1437
|
-
'name': name,
|
|
1438
|
-
'class_name': name,
|
|
1439
|
-
'type': 'class',
|
|
1440
|
-
'agent_name': 'Unknown (not instantiated)',
|
|
1441
|
-
'route': 'Unknown (not instantiated)',
|
|
1442
|
-
'description': obj.__doc__,
|
|
1443
|
-
'object': obj
|
|
1444
|
-
}
|
|
1445
|
-
agents_found.append(agent_info)
|
|
1446
|
-
except Exception:
|
|
1447
|
-
# If we can't get info, still record that the class exists
|
|
1448
|
-
agents_found.append({
|
|
1449
|
-
'name': name,
|
|
1450
|
-
'class_name': name,
|
|
1451
|
-
'type': 'class',
|
|
1452
|
-
'agent_name': 'Unknown (not instantiated)',
|
|
1453
|
-
'route': 'Unknown (not instantiated)',
|
|
1454
|
-
'description': obj.__doc__ or 'No description available',
|
|
1455
|
-
'object': obj
|
|
1456
|
-
})
|
|
1457
|
-
|
|
1458
|
-
return agents_found
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
def load_agent_from_file(agent_path: str, agent_class_name: Optional[str] = None) -> 'AgentBase':
|
|
1462
|
-
"""
|
|
1463
|
-
Load an agent from a Python file
|
|
1464
|
-
|
|
1465
|
-
Args:
|
|
1466
|
-
agent_path: Path to the Python file containing the agent
|
|
1467
|
-
agent_class_name: Optional name of the agent class to instantiate
|
|
1468
|
-
|
|
1469
|
-
Returns:
|
|
1470
|
-
AgentBase instance
|
|
1471
|
-
|
|
1472
|
-
Raises:
|
|
1473
|
-
ImportError: If the file cannot be imported
|
|
1474
|
-
ValueError: If no agent is found in the file
|
|
1475
|
-
"""
|
|
1476
|
-
agent_path = Path(agent_path).resolve()
|
|
1477
|
-
|
|
1478
|
-
if not agent_path.exists():
|
|
1479
|
-
raise FileNotFoundError(f"Agent file not found: {agent_path}")
|
|
1480
|
-
|
|
1481
|
-
if not agent_path.suffix == '.py':
|
|
1482
|
-
raise ValueError(f"Agent file must be a Python file (.py): {agent_path}")
|
|
1483
|
-
|
|
1484
|
-
# Load the module, but prevent main() execution by setting __name__ to something other than "__main__"
|
|
1485
|
-
spec = importlib.util.spec_from_file_location("agent_module", agent_path)
|
|
1486
|
-
module = importlib.util.module_from_spec(spec)
|
|
1487
|
-
|
|
1488
|
-
try:
|
|
1489
|
-
# Set __name__ to prevent if __name__ == "__main__": blocks from running
|
|
1490
|
-
module.__name__ = "agent_module"
|
|
1491
|
-
spec.loader.exec_module(module)
|
|
1492
|
-
except Exception as e:
|
|
1493
|
-
raise ImportError(f"Failed to load agent module: {e}")
|
|
1494
|
-
|
|
1495
|
-
# Find the agent instance
|
|
1496
|
-
agent = None
|
|
1497
|
-
|
|
1498
|
-
# If agent_class_name is specified, try to instantiate that specific class first
|
|
1499
|
-
if agent_class_name:
|
|
1500
|
-
if hasattr(module, agent_class_name):
|
|
1501
|
-
obj = getattr(module, agent_class_name)
|
|
1502
|
-
if isinstance(obj, type) and issubclass(obj, AgentBase) and obj != AgentBase:
|
|
1503
|
-
try:
|
|
1504
|
-
agent = obj()
|
|
1505
|
-
if agent and not agent.route.endswith('dummy'): # Avoid test agents with dummy routes
|
|
1506
|
-
pass # Successfully created specific agent
|
|
1507
|
-
else:
|
|
1508
|
-
agent = obj() # Create anyway if requested specifically
|
|
1509
|
-
except Exception as e:
|
|
1510
|
-
raise ValueError(f"Failed to instantiate agent class '{agent_class_name}': {e}")
|
|
1511
|
-
else:
|
|
1512
|
-
raise ValueError(f"'{agent_class_name}' is not a valid AgentBase subclass")
|
|
1513
|
-
else:
|
|
1514
|
-
raise ValueError(f"Agent class '{agent_class_name}' not found in {agent_path}")
|
|
1515
|
-
|
|
1516
|
-
# Strategy 1: Look for 'agent' variable (most common pattern)
|
|
1517
|
-
if agent is None and hasattr(module, 'agent') and isinstance(module.agent, AgentBase):
|
|
1518
|
-
agent = module.agent
|
|
1519
|
-
|
|
1520
|
-
# Strategy 2: Look for any AgentBase instance in module globals
|
|
1521
|
-
if agent is None:
|
|
1522
|
-
agents_found = []
|
|
1523
|
-
for name, obj in vars(module).items():
|
|
1524
|
-
if isinstance(obj, AgentBase):
|
|
1525
|
-
agents_found.append((name, obj))
|
|
1526
|
-
|
|
1527
|
-
if len(agents_found) == 1:
|
|
1528
|
-
agent = agents_found[0][1]
|
|
1529
|
-
elif len(agents_found) > 1:
|
|
1530
|
-
# Multiple agents found, prefer one named 'agent'
|
|
1531
|
-
for name, obj in agents_found:
|
|
1532
|
-
if name == 'agent':
|
|
1533
|
-
agent = obj
|
|
1534
|
-
break
|
|
1535
|
-
# If no 'agent' variable, use the first one
|
|
1536
|
-
if agent is None:
|
|
1537
|
-
agent = agents_found[0][1]
|
|
1538
|
-
print(f"Warning: Multiple agents found, using '{agents_found[0][0]}'")
|
|
1539
|
-
print(f"Hint: Use --agent-class parameter to choose specific agent")
|
|
1540
|
-
|
|
1541
|
-
# Strategy 3: Look for AgentBase subclass and try to instantiate it
|
|
1542
|
-
if agent is None:
|
|
1543
|
-
agent_classes_found = []
|
|
1544
|
-
for name, obj in vars(module).items():
|
|
1545
|
-
if (isinstance(obj, type) and
|
|
1546
|
-
issubclass(obj, AgentBase) and
|
|
1547
|
-
obj != AgentBase):
|
|
1548
|
-
agent_classes_found.append((name, obj))
|
|
1549
|
-
|
|
1550
|
-
if len(agent_classes_found) == 1:
|
|
1551
|
-
try:
|
|
1552
|
-
agent = agent_classes_found[0][1]()
|
|
1553
|
-
except Exception as e:
|
|
1554
|
-
print(f"Warning: Failed to instantiate {agent_classes_found[0][0]}: {e}")
|
|
1555
|
-
elif len(agent_classes_found) > 1:
|
|
1556
|
-
# Multiple agent classes found
|
|
1557
|
-
class_names = [name for name, _ in agent_classes_found]
|
|
1558
|
-
raise ValueError(f"Multiple agent classes found: {', '.join(class_names)}. "
|
|
1559
|
-
f"Please specify which agent class to use with --agent-class parameter. "
|
|
1560
|
-
f"Usage: swaig-test {agent_path} [tool_name] [args] --agent-class <AgentClassName>")
|
|
1561
|
-
else:
|
|
1562
|
-
# Try instantiating any AgentBase class we can find
|
|
1563
|
-
for name, obj in vars(module).items():
|
|
1564
|
-
if (isinstance(obj, type) and
|
|
1565
|
-
issubclass(obj, AgentBase) and
|
|
1566
|
-
obj != AgentBase):
|
|
1567
|
-
try:
|
|
1568
|
-
agent = obj()
|
|
1569
|
-
break
|
|
1570
|
-
except Exception as e:
|
|
1571
|
-
print(f"Warning: Failed to instantiate {name}: {e}")
|
|
1572
|
-
|
|
1573
|
-
# Strategy 4: Try calling a modified main() function that doesn't start the server
|
|
1574
|
-
if agent is None and hasattr(module, 'main'):
|
|
1575
|
-
print("Warning: No agent instance found, attempting to call main() without server startup")
|
|
1576
|
-
try:
|
|
1577
|
-
# Temporarily patch AgentBase.serve to prevent server startup
|
|
1578
|
-
original_serve = AgentBase.serve
|
|
1579
|
-
captured_agent = []
|
|
1580
|
-
|
|
1581
|
-
def mock_serve(self, *args, **kwargs):
|
|
1582
|
-
captured_agent.append(self)
|
|
1583
|
-
print(f" (Intercepted serve() call, agent captured for testing)")
|
|
1584
|
-
return self
|
|
1585
|
-
|
|
1586
|
-
AgentBase.serve = mock_serve
|
|
1587
|
-
|
|
1588
|
-
try:
|
|
1589
|
-
result = module.main()
|
|
1590
|
-
if isinstance(result, AgentBase):
|
|
1591
|
-
agent = result
|
|
1592
|
-
elif captured_agent:
|
|
1593
|
-
agent = captured_agent[0]
|
|
1594
|
-
finally:
|
|
1595
|
-
# Restore original serve method
|
|
1596
|
-
AgentBase.serve = original_serve
|
|
1597
|
-
|
|
1598
|
-
except Exception as e:
|
|
1599
|
-
print(f"Warning: Failed to call main() function: {e}")
|
|
1600
|
-
|
|
1601
|
-
if agent is None:
|
|
1602
|
-
raise ValueError(f"No AgentBase instance found in {agent_path}. "
|
|
1603
|
-
f"Make sure the file contains an agent variable or AgentBase subclass.")
|
|
1604
|
-
|
|
1605
|
-
return agent
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
def format_result(result: Any) -> str:
|
|
1609
|
-
"""
|
|
1610
|
-
Format the result of a SWAIG function call for display
|
|
1611
|
-
|
|
1612
|
-
Args:
|
|
1613
|
-
result: The result from the SWAIG function
|
|
1614
|
-
|
|
1615
|
-
Returns:
|
|
1616
|
-
Formatted string representation
|
|
1617
|
-
"""
|
|
1618
|
-
if isinstance(result, SwaigFunctionResult):
|
|
1619
|
-
return f"SwaigFunctionResult: {result.response}"
|
|
1620
|
-
elif isinstance(result, dict):
|
|
1621
|
-
if 'response' in result:
|
|
1622
|
-
return f"Response: {result['response']}"
|
|
1623
|
-
else:
|
|
1624
|
-
return f"Dict: {json.dumps(result, indent=2)}"
|
|
1625
|
-
elif isinstance(result, str):
|
|
1626
|
-
return f"String: {result}"
|
|
1627
|
-
else:
|
|
1628
|
-
return f"Other ({type(result).__name__}): {result}"
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
def parse_function_arguments(function_args_list: List[str], func_schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
1632
|
-
"""
|
|
1633
|
-
Parse function arguments from command line with type coercion based on schema
|
|
1634
|
-
|
|
1635
|
-
Args:
|
|
1636
|
-
function_args_list: List of command line arguments after --args
|
|
1637
|
-
func_schema: Function schema with parameter definitions
|
|
1638
|
-
|
|
1639
|
-
Returns:
|
|
1640
|
-
Dictionary of parsed function arguments
|
|
1641
|
-
"""
|
|
1642
|
-
parsed_args = {}
|
|
1643
|
-
i = 0
|
|
1644
|
-
|
|
1645
|
-
# Get parameter schema
|
|
1646
|
-
parameters = {}
|
|
1647
|
-
required_params = []
|
|
1648
|
-
|
|
1649
|
-
if isinstance(func_schema, dict):
|
|
1650
|
-
# DataMap function
|
|
1651
|
-
if 'parameters' in func_schema:
|
|
1652
|
-
params = func_schema['parameters']
|
|
1653
|
-
if 'properties' in params:
|
|
1654
|
-
parameters = params['properties']
|
|
1655
|
-
required_params = params.get('required', [])
|
|
1656
|
-
else:
|
|
1657
|
-
parameters = params
|
|
1658
|
-
else:
|
|
1659
|
-
parameters = func_schema
|
|
1660
|
-
else:
|
|
1661
|
-
# Regular SWAIG function
|
|
1662
|
-
if hasattr(func_schema, 'parameters') and func_schema.parameters:
|
|
1663
|
-
params = func_schema.parameters
|
|
1664
|
-
if 'properties' in params:
|
|
1665
|
-
parameters = params['properties']
|
|
1666
|
-
required_params = params.get('required', [])
|
|
1667
|
-
else:
|
|
1668
|
-
parameters = params
|
|
1669
|
-
|
|
1670
|
-
# Parse arguments
|
|
1671
|
-
while i < len(function_args_list):
|
|
1672
|
-
arg = function_args_list[i]
|
|
1673
|
-
|
|
1674
|
-
if arg.startswith('--'):
|
|
1675
|
-
param_name = arg[2:] # Remove --
|
|
1676
|
-
|
|
1677
|
-
# Convert kebab-case to snake_case for parameter lookup
|
|
1678
|
-
param_key = param_name.replace('-', '_')
|
|
1679
|
-
|
|
1680
|
-
# Check if this parameter exists in schema
|
|
1681
|
-
param_schema = parameters.get(param_key, {})
|
|
1682
|
-
param_type = param_schema.get('type', 'string')
|
|
1683
|
-
|
|
1684
|
-
if param_type == 'boolean':
|
|
1685
|
-
# Check if next arg is a boolean value or if this is a flag
|
|
1686
|
-
if i + 1 < len(function_args_list) and function_args_list[i + 1].lower() in ['true', 'false']:
|
|
1687
|
-
parsed_args[param_key] = function_args_list[i + 1].lower() == 'true'
|
|
1688
|
-
i += 2
|
|
1689
|
-
else:
|
|
1690
|
-
# Treat as flag (present = true)
|
|
1691
|
-
parsed_args[param_key] = True
|
|
1692
|
-
i += 1
|
|
1693
|
-
else:
|
|
1694
|
-
# Need a value
|
|
1695
|
-
if i + 1 >= len(function_args_list):
|
|
1696
|
-
raise ValueError(f"Parameter --{param_name} requires a value")
|
|
1697
|
-
|
|
1698
|
-
value = function_args_list[i + 1]
|
|
1699
|
-
|
|
1700
|
-
# Type coercion
|
|
1701
|
-
if param_type == 'integer':
|
|
1702
|
-
try:
|
|
1703
|
-
parsed_args[param_key] = int(value)
|
|
1704
|
-
except ValueError:
|
|
1705
|
-
raise ValueError(f"Parameter --{param_name} must be an integer, got: {value}")
|
|
1706
|
-
elif param_type == 'number':
|
|
1707
|
-
try:
|
|
1708
|
-
parsed_args[param_key] = float(value)
|
|
1709
|
-
except ValueError:
|
|
1710
|
-
raise ValueError(f"Parameter --{param_name} must be a number, got: {value}")
|
|
1711
|
-
elif param_type == 'array':
|
|
1712
|
-
# Handle comma-separated arrays
|
|
1713
|
-
parsed_args[param_key] = [item.strip() for item in value.split(',')]
|
|
1714
|
-
else:
|
|
1715
|
-
# String (default)
|
|
1716
|
-
parsed_args[param_key] = value
|
|
1717
|
-
|
|
1718
|
-
i += 2
|
|
1719
|
-
else:
|
|
1720
|
-
raise ValueError(f"Expected parameter name starting with --, got: {arg}")
|
|
1721
|
-
|
|
1722
|
-
return parsed_args
|
|
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
|
+
""")
|
|
1723
220
|
|
|
1724
221
|
|
|
1725
222
|
def main():
|
|
1726
223
|
"""Main entry point for the CLI tool"""
|
|
1727
|
-
#
|
|
1728
|
-
if "--
|
|
1729
|
-
|
|
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)
|
|
1730
236
|
|
|
1731
237
|
# Check for --exec and split arguments
|
|
1732
238
|
cli_args = sys.argv[1:]
|
|
@@ -1749,424 +255,185 @@ def main():
|
|
|
1749
255
|
original_argv = sys.argv[:]
|
|
1750
256
|
sys.argv = [sys.argv[0]] + cli_args
|
|
1751
257
|
|
|
1752
|
-
# Custom ArgumentParser class with better error handling
|
|
1753
|
-
class CustomArgumentParser(argparse.ArgumentParser):
|
|
1754
|
-
def __init__(self, *args, **kwargs):
|
|
1755
|
-
super().__init__(*args, **kwargs)
|
|
1756
|
-
self._suppress_usage = False
|
|
1757
|
-
|
|
1758
|
-
def _print_message(self, message, file=None):
|
|
1759
|
-
"""Override to suppress usage output for specific errors"""
|
|
1760
|
-
if self._suppress_usage:
|
|
1761
|
-
return
|
|
1762
|
-
super()._print_message(message, file)
|
|
1763
|
-
|
|
1764
|
-
def error(self, message):
|
|
1765
|
-
"""Override error method to provide user-friendly error messages"""
|
|
1766
|
-
if "required" in message.lower() and "agent_path" in message:
|
|
1767
|
-
self._suppress_usage = True
|
|
1768
|
-
print("Error: Missing required argument.")
|
|
1769
|
-
print()
|
|
1770
|
-
print(f"Usage: {self.prog} <agent_path> [options]")
|
|
1771
|
-
print()
|
|
1772
|
-
print("Examples:")
|
|
1773
|
-
print(f" {self.prog} examples/my_agent.py --list-tools")
|
|
1774
|
-
print(f" {self.prog} examples/my_agent.py --dump-swml")
|
|
1775
|
-
print(f" {self.prog} examples/my_agent.py --exec my_function --param value")
|
|
1776
|
-
print()
|
|
1777
|
-
print(f"For full help: {self.prog} --help")
|
|
1778
|
-
sys.exit(2)
|
|
1779
|
-
else:
|
|
1780
|
-
# For other errors, use the default behavior
|
|
1781
|
-
super().error(message)
|
|
1782
|
-
|
|
1783
|
-
def print_usage(self, file=None):
|
|
1784
|
-
"""Override print_usage to suppress output when we want custom error handling"""
|
|
1785
|
-
if self._suppress_usage:
|
|
1786
|
-
return
|
|
1787
|
-
super().print_usage(file)
|
|
1788
|
-
|
|
1789
|
-
def parse_args(self, args=None, namespace=None):
|
|
1790
|
-
"""Override parse_args to provide custom error handling for missing arguments"""
|
|
1791
|
-
# Check if no arguments provided (just the program name)
|
|
1792
|
-
import sys
|
|
1793
|
-
if args is None:
|
|
1794
|
-
args = sys.argv[1:]
|
|
1795
|
-
|
|
1796
|
-
# If no arguments provided, show custom error
|
|
1797
|
-
if not args:
|
|
1798
|
-
print("Error: Missing required argument.")
|
|
1799
|
-
print()
|
|
1800
|
-
print(f"Usage: {self.prog} <agent_path> [options]")
|
|
1801
|
-
print()
|
|
1802
|
-
print("Examples:")
|
|
1803
|
-
print(f" {self.prog} examples/my_agent.py --list-tools")
|
|
1804
|
-
print(f" {self.prog} examples/my_agent.py --dump-swml")
|
|
1805
|
-
print(f" {self.prog} examples/my_agent.py --exec my_function --param value")
|
|
1806
|
-
print()
|
|
1807
|
-
print(f"For full help: {self.prog} --help")
|
|
1808
|
-
sys.exit(2)
|
|
1809
|
-
|
|
1810
|
-
# Otherwise, use default parsing
|
|
1811
|
-
return super().parse_args(args, namespace)
|
|
1812
|
-
|
|
1813
258
|
parser = CustomArgumentParser(
|
|
1814
|
-
description=
|
|
259
|
+
description=HELP_DESCRIPTION,
|
|
1815
260
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1816
261
|
usage="%(prog)s <agent_path> [options]",
|
|
1817
|
-
epilog=
|
|
1818
|
-
Examples:
|
|
1819
|
-
# Function testing with --exec syntax
|
|
1820
|
-
%(prog)s examples/agent.py --verbose --exec search --query "AI" --limit 5
|
|
1821
|
-
%(prog)s examples/web_search_agent.py --exec web_search --query "test"
|
|
1822
|
-
|
|
1823
|
-
# Legacy JSON syntax (still supported)
|
|
1824
|
-
%(prog)s examples/web_search_agent.py web_search '{"query":"test"}'
|
|
1825
|
-
|
|
1826
|
-
# Multiple agents - auto-select when only one, or specify with --agent-class
|
|
1827
|
-
%(prog)s matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --exec transfer --name sigmond
|
|
1828
|
-
%(prog)s matti_and_sigmond/dual_agent_app.py --verbose --agent-class SigmondAgent --exec get_weather --location "New York"
|
|
1829
|
-
|
|
1830
|
-
# SWML testing (enhanced with fake post_data)
|
|
1831
|
-
%(prog)s examples/my_agent.py --dump-swml
|
|
1832
|
-
%(prog)s examples/my_agent.py --dump-swml --raw | jq '.'
|
|
1833
|
-
%(prog)s examples/my_agent.py --dump-swml --verbose
|
|
1834
|
-
|
|
1835
|
-
# SWML testing with specific agent class
|
|
1836
|
-
%(prog)s matti_and_sigmond/dual_agent_app.py --dump-swml --agent-class MattiAgent
|
|
1837
|
-
|
|
1838
|
-
# SWML testing with call customization
|
|
1839
|
-
%(prog)s examples/agent.py --dump-swml --call-type sip --call-direction outbound
|
|
1840
|
-
%(prog)s examples/agent.py --dump-swml --call-state answered --from-number +15551234567
|
|
1841
|
-
|
|
1842
|
-
# SWML testing with data overrides
|
|
1843
|
-
%(prog)s examples/agent.py --dump-swml --override call.project_id=my-project
|
|
1844
|
-
%(prog)s examples/agent.py --dump-swml --user-vars '{"customer_id":"12345","tier":"gold"}'
|
|
1845
|
-
%(prog)s examples/agent.py --dump-swml --override call.timeout=60 --override call.state=answered
|
|
1846
|
-
|
|
1847
|
-
# Dynamic agent testing with mock request
|
|
1848
|
-
%(prog)s examples/dynamic_agent.py --dump-swml --header "Authorization=Bearer token"
|
|
1849
|
-
%(prog)s examples/dynamic_agent.py --dump-swml --query-params '{"source":"api","debug":"true"}'
|
|
1850
|
-
%(prog)s examples/dynamic_agent.py --dump-swml --method GET --body '{"custom":"data"}'
|
|
1851
|
-
|
|
1852
|
-
# Serverless environment simulation
|
|
1853
|
-
%(prog)s examples/my_agent.py --simulate-serverless lambda --dump-swml
|
|
1854
|
-
%(prog)s examples/my_agent.py --simulate-serverless lambda --exec my_function --param value
|
|
1855
|
-
%(prog)s examples/my_agent.py --simulate-serverless cgi --cgi-host example.com --dump-swml
|
|
1856
|
-
%(prog)s examples/my_agent.py --simulate-serverless cloud_function --gcp-project my-project --exec my_function
|
|
1857
|
-
|
|
1858
|
-
# Serverless with environment variables
|
|
1859
|
-
%(prog)s examples/my_agent.py --simulate-serverless lambda --env API_KEY=secret --env DEBUG=1 --exec my_function
|
|
1860
|
-
%(prog)s examples/my_agent.py --simulate-serverless lambda --env-file production.env --exec my_function
|
|
1861
|
-
|
|
1862
|
-
# Platform-specific serverless configuration
|
|
1863
|
-
%(prog)s examples/my_agent.py --simulate-serverless lambda --aws-function-name prod-function --aws-region us-west-2 --dump-swml
|
|
1864
|
-
%(prog)s examples/my_agent.py --simulate-serverless cgi --cgi-host production.com --cgi-https --exec my_function
|
|
1865
|
-
|
|
1866
|
-
# Combined testing scenarios
|
|
1867
|
-
%(prog)s examples/agent.py --dump-swml --call-type sip --user-vars '{"vip":"true"}' --header "X-Source=test" --verbose
|
|
1868
|
-
%(prog)s examples/agent.py --simulate-serverless lambda --dump-swml --call-type sip --verbose
|
|
1869
|
-
|
|
1870
|
-
# Discovery commands
|
|
1871
|
-
%(prog)s examples/my_agent.py --list-agents
|
|
1872
|
-
%(prog)s examples/my_agent.py --list-tools
|
|
1873
|
-
%(prog)s matti_and_sigmond/dual_agent_app.py --list-agents
|
|
1874
|
-
%(prog)s matti_and_sigmond/dual_agent_app.py --agent-class MattiAgent --list-tools
|
|
1875
|
-
|
|
1876
|
-
# Auto-discovery (lists agents when no other args provided)
|
|
1877
|
-
%(prog)s matti_and_sigmond/dual_agent_app.py
|
|
1878
|
-
"""
|
|
262
|
+
epilog=HELP_EPILOG_SHORT
|
|
1879
263
|
)
|
|
1880
264
|
|
|
1881
|
-
#
|
|
265
|
+
# Positional arguments
|
|
1882
266
|
parser.add_argument(
|
|
1883
267
|
"agent_path",
|
|
1884
|
-
help="Path to
|
|
1885
|
-
)
|
|
1886
|
-
|
|
1887
|
-
parser.add_argument(
|
|
1888
|
-
"tool_name",
|
|
1889
|
-
nargs="?",
|
|
1890
|
-
help="Name of the SWAIG function/tool to call (optional, can use --exec instead)"
|
|
1891
|
-
)
|
|
1892
|
-
|
|
1893
|
-
parser.add_argument(
|
|
1894
|
-
"args_json",
|
|
1895
|
-
nargs="?",
|
|
1896
|
-
help="JSON string containing the arguments to pass to the function (when using positional tool_name)"
|
|
268
|
+
help="Path to Python file containing the agent"
|
|
1897
269
|
)
|
|
1898
270
|
|
|
1899
|
-
#
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
"--
|
|
1903
|
-
|
|
1904
|
-
help="
|
|
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"
|
|
1905
277
|
)
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
"
|
|
1909
|
-
help="
|
|
1910
|
-
default="{}"
|
|
278
|
+
common.add_argument(
|
|
279
|
+
"--raw",
|
|
280
|
+
action="store_true",
|
|
281
|
+
help="Output raw JSON only (for piping to jq)"
|
|
1911
282
|
)
|
|
1912
|
-
|
|
1913
|
-
# Agent Discovery and Selection
|
|
1914
|
-
agent_group = parser.add_argument_group('Agent Discovery and Selection')
|
|
1915
|
-
agent_group.add_argument(
|
|
283
|
+
common.add_argument(
|
|
1916
284
|
"--agent-class",
|
|
1917
|
-
help="
|
|
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)"
|
|
1918
290
|
)
|
|
1919
291
|
|
|
1920
|
-
|
|
292
|
+
# Actions (choose one)
|
|
293
|
+
actions = parser.add_argument_group('actions (choose one)')
|
|
294
|
+
actions.add_argument(
|
|
1921
295
|
"--list-agents",
|
|
1922
296
|
action="store_true",
|
|
1923
|
-
help="List all
|
|
297
|
+
help="List all agents in file"
|
|
1924
298
|
)
|
|
1925
|
-
|
|
1926
|
-
agent_group.add_argument(
|
|
299
|
+
actions.add_argument(
|
|
1927
300
|
"--list-tools",
|
|
1928
301
|
action="store_true",
|
|
1929
|
-
help="List all
|
|
302
|
+
help="List all tools in agent"
|
|
1930
303
|
)
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
output_group = parser.add_argument_group('Output and Debugging Options')
|
|
1934
|
-
output_group.add_argument(
|
|
1935
|
-
"--verbose", "-v",
|
|
304
|
+
actions.add_argument(
|
|
305
|
+
"--dump-swml",
|
|
1936
306
|
action="store_true",
|
|
1937
|
-
help="
|
|
307
|
+
help="Generate and output SWML document"
|
|
1938
308
|
)
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
"
|
|
1942
|
-
|
|
1943
|
-
help="Output raw SWML JSON only (no headers, useful for piping to jq/yq)"
|
|
309
|
+
actions.add_argument(
|
|
310
|
+
"--exec",
|
|
311
|
+
metavar="FUNCTION",
|
|
312
|
+
help="Execute function with CLI args (e.g., --exec search --query 'AI')"
|
|
1944
313
|
)
|
|
1945
314
|
|
|
1946
|
-
#
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
"--
|
|
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",
|
|
1950
324
|
action="store_true",
|
|
1951
|
-
help="
|
|
325
|
+
help="Use minimal post_data for function execution"
|
|
1952
326
|
)
|
|
1953
|
-
|
|
1954
|
-
swml_group.add_argument(
|
|
327
|
+
func_group.add_argument(
|
|
1955
328
|
"--fake-full-data",
|
|
1956
|
-
action="store_true",
|
|
1957
|
-
help="Generate comprehensive fake post_data with all possible keys"
|
|
1958
|
-
)
|
|
1959
|
-
|
|
1960
|
-
swml_group.add_argument(
|
|
1961
|
-
"--minimal",
|
|
1962
329
|
action="store_true",
|
|
1963
|
-
help="Use
|
|
330
|
+
help="Use comprehensive fake post_data"
|
|
1964
331
|
)
|
|
1965
332
|
|
|
1966
|
-
#
|
|
1967
|
-
|
|
1968
|
-
|
|
333
|
+
# SWML generation options
|
|
334
|
+
swml_group = parser.add_argument_group('swml generation options')
|
|
335
|
+
swml_group.add_argument(
|
|
1969
336
|
"--call-type",
|
|
1970
337
|
choices=["sip", "webrtc"],
|
|
1971
338
|
default="webrtc",
|
|
1972
|
-
help="
|
|
339
|
+
help="Call type (default: webrtc)"
|
|
1973
340
|
)
|
|
1974
|
-
|
|
1975
|
-
call_group.add_argument(
|
|
341
|
+
swml_group.add_argument(
|
|
1976
342
|
"--call-direction",
|
|
1977
343
|
choices=["inbound", "outbound"],
|
|
1978
344
|
default="inbound",
|
|
1979
|
-
help="
|
|
345
|
+
help="Call direction (default: inbound)"
|
|
1980
346
|
)
|
|
1981
|
-
|
|
1982
|
-
call_group.add_argument(
|
|
347
|
+
swml_group.add_argument(
|
|
1983
348
|
"--call-state",
|
|
1984
349
|
default="created",
|
|
1985
|
-
help="
|
|
1986
|
-
)
|
|
1987
|
-
|
|
1988
|
-
call_group.add_argument(
|
|
1989
|
-
"--call-id",
|
|
1990
|
-
help="Override call_id in fake SWML post_data"
|
|
350
|
+
help="Call state (default: created)"
|
|
1991
351
|
)
|
|
1992
|
-
|
|
1993
|
-
call_group.add_argument(
|
|
352
|
+
swml_group.add_argument(
|
|
1994
353
|
"--from-number",
|
|
1995
|
-
help="Override
|
|
354
|
+
help="Override from number"
|
|
1996
355
|
)
|
|
1997
|
-
|
|
1998
|
-
call_group.add_argument(
|
|
356
|
+
swml_group.add_argument(
|
|
1999
357
|
"--to-extension",
|
|
2000
|
-
help="Override
|
|
2001
|
-
)
|
|
2002
|
-
|
|
2003
|
-
# SignalWire Platform Configuration
|
|
2004
|
-
platform_group = parser.add_argument_group('SignalWire Platform Configuration')
|
|
2005
|
-
platform_group.add_argument(
|
|
2006
|
-
"--project-id",
|
|
2007
|
-
help="Override project_id in fake SWML post_data"
|
|
2008
|
-
)
|
|
2009
|
-
|
|
2010
|
-
platform_group.add_argument(
|
|
2011
|
-
"--space-id",
|
|
2012
|
-
help="Override space_id in fake SWML post_data"
|
|
358
|
+
help="Override to extension"
|
|
2013
359
|
)
|
|
2014
360
|
|
|
2015
|
-
#
|
|
2016
|
-
|
|
2017
|
-
|
|
361
|
+
# Data customization
|
|
362
|
+
data_group = parser.add_argument_group('data customization')
|
|
363
|
+
data_group.add_argument(
|
|
2018
364
|
"--user-vars",
|
|
2019
|
-
help="JSON string for
|
|
365
|
+
help="JSON string for userVariables"
|
|
2020
366
|
)
|
|
2021
|
-
|
|
2022
|
-
vars_group.add_argument(
|
|
367
|
+
data_group.add_argument(
|
|
2023
368
|
"--query-params",
|
|
2024
|
-
help="JSON string for query parameters
|
|
369
|
+
help="JSON string for query parameters"
|
|
2025
370
|
)
|
|
2026
|
-
|
|
2027
|
-
# Data Override Options
|
|
2028
|
-
override_group = parser.add_argument_group('Data Override Options')
|
|
2029
|
-
override_group.add_argument(
|
|
371
|
+
data_group.add_argument(
|
|
2030
372
|
"--override",
|
|
2031
373
|
action="append",
|
|
2032
374
|
default=[],
|
|
2033
|
-
help="Override
|
|
2034
|
-
)
|
|
2035
|
-
|
|
2036
|
-
override_group.add_argument(
|
|
2037
|
-
"--override-json",
|
|
2038
|
-
action="append",
|
|
2039
|
-
default=[],
|
|
2040
|
-
help="Override with JSON values using dot notation (e.g., --override-json vars.custom='{\"key\":\"value\"}')"
|
|
375
|
+
help="Override value (e.g., --override call.state=answered)"
|
|
2041
376
|
)
|
|
2042
|
-
|
|
2043
|
-
# HTTP Request Simulation
|
|
2044
|
-
http_group = parser.add_argument_group('HTTP Request Simulation')
|
|
2045
|
-
http_group.add_argument(
|
|
377
|
+
data_group.add_argument(
|
|
2046
378
|
"--header",
|
|
2047
379
|
action="append",
|
|
2048
380
|
default=[],
|
|
2049
|
-
help="Add HTTP
|
|
2050
|
-
)
|
|
2051
|
-
|
|
2052
|
-
http_group.add_argument(
|
|
2053
|
-
"--method",
|
|
2054
|
-
default="POST",
|
|
2055
|
-
help="HTTP method for mock request (default: POST)"
|
|
2056
|
-
)
|
|
2057
|
-
|
|
2058
|
-
http_group.add_argument(
|
|
2059
|
-
"--body",
|
|
2060
|
-
help="JSON string for mock request body"
|
|
381
|
+
help="Add HTTP header (e.g., --header Authorization=Bearer token)"
|
|
2061
382
|
)
|
|
2062
383
|
|
|
2063
|
-
# Serverless
|
|
2064
|
-
serverless_group = parser.add_argument_group('
|
|
384
|
+
# Serverless simulation (basic)
|
|
385
|
+
serverless_group = parser.add_argument_group('serverless simulation (use --help-platforms for platform options)')
|
|
2065
386
|
serverless_group.add_argument(
|
|
2066
387
|
"--simulate-serverless",
|
|
2067
388
|
choices=["lambda", "cgi", "cloud_function", "azure_function"],
|
|
2068
|
-
help="Simulate serverless platform
|
|
389
|
+
help="Simulate serverless platform"
|
|
2069
390
|
)
|
|
2070
|
-
|
|
2071
391
|
serverless_group.add_argument(
|
|
2072
392
|
"--env",
|
|
2073
393
|
action="append",
|
|
2074
394
|
default=[],
|
|
2075
|
-
help="Set environment variable (e.g., --env
|
|
395
|
+
help="Set environment variable (e.g., --env KEY=VALUE)"
|
|
2076
396
|
)
|
|
2077
|
-
|
|
2078
397
|
serverless_group.add_argument(
|
|
2079
398
|
"--env-file",
|
|
2080
|
-
help="Load environment
|
|
2081
|
-
)
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
)
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
)
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
)
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
)
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
"--aws-stage",
|
|
2112
|
-
help="AWS API Gateway stage (default: prod)"
|
|
2113
|
-
)
|
|
2114
|
-
|
|
2115
|
-
# CGI Configuration
|
|
2116
|
-
cgi_group = parser.add_argument_group('CGI Configuration')
|
|
2117
|
-
cgi_group.add_argument(
|
|
2118
|
-
"--cgi-host",
|
|
2119
|
-
help="CGI server hostname (required for CGI simulation)"
|
|
2120
|
-
)
|
|
2121
|
-
|
|
2122
|
-
cgi_group.add_argument(
|
|
2123
|
-
"--cgi-script-name",
|
|
2124
|
-
help="CGI script name/path (overrides default)"
|
|
2125
|
-
)
|
|
2126
|
-
|
|
2127
|
-
cgi_group.add_argument(
|
|
2128
|
-
"--cgi-https",
|
|
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",
|
|
2129
430
|
action="store_true",
|
|
2130
|
-
help="
|
|
2131
|
-
)
|
|
2132
|
-
|
|
2133
|
-
cgi_group.add_argument(
|
|
2134
|
-
"--cgi-path-info",
|
|
2135
|
-
help="CGI PATH_INFO value"
|
|
2136
|
-
)
|
|
2137
|
-
|
|
2138
|
-
# Google Cloud Platform Configuration
|
|
2139
|
-
gcp_group = parser.add_argument_group('Google Cloud Platform Configuration')
|
|
2140
|
-
gcp_group.add_argument(
|
|
2141
|
-
"--gcp-project",
|
|
2142
|
-
help="Google Cloud project ID (overrides default)"
|
|
2143
|
-
)
|
|
2144
|
-
|
|
2145
|
-
gcp_group.add_argument(
|
|
2146
|
-
"--gcp-function-url",
|
|
2147
|
-
help="Google Cloud Function URL (overrides default)"
|
|
2148
|
-
)
|
|
2149
|
-
|
|
2150
|
-
gcp_group.add_argument(
|
|
2151
|
-
"--gcp-region",
|
|
2152
|
-
help="Google Cloud region (overrides default)"
|
|
2153
|
-
)
|
|
2154
|
-
|
|
2155
|
-
gcp_group.add_argument(
|
|
2156
|
-
"--gcp-service",
|
|
2157
|
-
help="Google Cloud service name (overrides default)"
|
|
2158
|
-
)
|
|
2159
|
-
|
|
2160
|
-
# Azure Functions Configuration
|
|
2161
|
-
azure_group = parser.add_argument_group('Azure Functions Configuration')
|
|
2162
|
-
azure_group.add_argument(
|
|
2163
|
-
"--azure-env",
|
|
2164
|
-
help="Azure Functions environment (overrides default)"
|
|
431
|
+
help="Show platform-specific serverless options"
|
|
2165
432
|
)
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
"
|
|
2169
|
-
help="
|
|
433
|
+
parser.add_argument(
|
|
434
|
+
"--help-examples",
|
|
435
|
+
action="store_true",
|
|
436
|
+
help="Show comprehensive usage examples"
|
|
2170
437
|
)
|
|
2171
438
|
|
|
2172
439
|
args = parser.parse_args()
|
|
@@ -2176,38 +443,26 @@ Examples:
|
|
|
2176
443
|
|
|
2177
444
|
# Handle --exec vs positional tool_name
|
|
2178
445
|
if exec_function_name:
|
|
2179
|
-
# Using --exec syntax, override any positional tool_name
|
|
2180
446
|
args.tool_name = exec_function_name
|
|
2181
|
-
|
|
447
|
+
else:
|
|
448
|
+
args.tool_name = None
|
|
2182
449
|
|
|
2183
450
|
# Validate arguments
|
|
451
|
+
if args.route and args.agent_class:
|
|
452
|
+
parser.error("Cannot specify both --route and --agent-class. Choose one.")
|
|
453
|
+
|
|
2184
454
|
if not args.list_tools and not args.dump_swml and not args.list_agents:
|
|
2185
455
|
if not args.tool_name:
|
|
2186
|
-
# If no tool_name and no special flags, default to listing
|
|
2187
|
-
args.
|
|
2188
|
-
else:
|
|
2189
|
-
# When using positional syntax, args_json is required
|
|
2190
|
-
# When using --exec syntax, function_args_list is automatically populated
|
|
2191
|
-
if not args.args_json and not function_args_list:
|
|
2192
|
-
if exec_function_name:
|
|
2193
|
-
# --exec syntax doesn't require additional arguments (can be empty)
|
|
2194
|
-
pass
|
|
2195
|
-
else:
|
|
2196
|
-
parser.error("Positional tool_name requires args_json parameter. Use --exec for CLI-style arguments.")
|
|
456
|
+
# If no tool_name and no special flags, default to listing tools
|
|
457
|
+
args.list_tools = True
|
|
2197
458
|
|
|
2198
459
|
# ===== SERVERLESS SIMULATION SETUP =====
|
|
2199
460
|
serverless_simulator = None
|
|
2200
461
|
|
|
2201
|
-
# Handle legacy --serverless-mode option
|
|
2202
|
-
if args.serverless_mode and not args.simulate_serverless:
|
|
2203
|
-
args.simulate_serverless = args.serverless_mode
|
|
2204
|
-
if not args.raw:
|
|
2205
|
-
print("Warning: --serverless-mode is deprecated, use --simulate-serverless instead")
|
|
2206
|
-
|
|
2207
462
|
if args.simulate_serverless:
|
|
2208
463
|
# Validate CGI requirements
|
|
2209
464
|
if args.simulate_serverless == 'cgi' and not args.cgi_host:
|
|
2210
|
-
parser.error(
|
|
465
|
+
parser.error(ERROR_CGI_HOST_REQUIRED)
|
|
2211
466
|
|
|
2212
467
|
# Collect environment variable overrides
|
|
2213
468
|
env_overrides = {}
|
|
@@ -2223,16 +478,13 @@ Examples:
|
|
|
2223
478
|
print(f"Error: {e}")
|
|
2224
479
|
return 1
|
|
2225
480
|
|
|
2226
|
-
#
|
|
2227
|
-
for
|
|
2228
|
-
if '='
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
return 1
|
|
2232
|
-
key, value = env_arg.split('=', 1)
|
|
2233
|
-
env_overrides[key] = value
|
|
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
|
|
2234
486
|
|
|
2235
|
-
#
|
|
487
|
+
# Apply platform-specific overrides
|
|
2236
488
|
if args.simulate_serverless == 'lambda':
|
|
2237
489
|
if args.aws_function_name:
|
|
2238
490
|
env_overrides['AWS_LAMBDA_FUNCTION_NAME'] = args.aws_function_name
|
|
@@ -2240,21 +492,16 @@ Examples:
|
|
|
2240
492
|
env_overrides['AWS_LAMBDA_FUNCTION_URL'] = args.aws_function_url
|
|
2241
493
|
if args.aws_region:
|
|
2242
494
|
env_overrides['AWS_REGION'] = args.aws_region
|
|
2243
|
-
if args.aws_api_gateway_id:
|
|
2244
|
-
env_overrides['AWS_API_GATEWAY_ID'] = args.aws_api_gateway_id
|
|
2245
|
-
if args.aws_stage:
|
|
2246
|
-
env_overrides['AWS_API_GATEWAY_STAGE'] = args.aws_stage
|
|
2247
|
-
|
|
2248
495
|
elif args.simulate_serverless == 'cgi':
|
|
2249
496
|
if args.cgi_host:
|
|
2250
497
|
env_overrides['HTTP_HOST'] = args.cgi_host
|
|
498
|
+
env_overrides['SERVER_NAME'] = args.cgi_host
|
|
2251
499
|
if args.cgi_script_name:
|
|
2252
500
|
env_overrides['SCRIPT_NAME'] = args.cgi_script_name
|
|
2253
501
|
if args.cgi_https:
|
|
2254
502
|
env_overrides['HTTPS'] = 'on'
|
|
2255
503
|
if args.cgi_path_info:
|
|
2256
504
|
env_overrides['PATH_INFO'] = args.cgi_path_info
|
|
2257
|
-
|
|
2258
505
|
elif args.simulate_serverless == 'cloud_function':
|
|
2259
506
|
if args.gcp_project:
|
|
2260
507
|
env_overrides['GOOGLE_CLOUD_PROJECT'] = args.gcp_project
|
|
@@ -2264,71 +511,49 @@ Examples:
|
|
|
2264
511
|
env_overrides['GOOGLE_CLOUD_REGION'] = args.gcp_region
|
|
2265
512
|
if args.gcp_service:
|
|
2266
513
|
env_overrides['K_SERVICE'] = args.gcp_service
|
|
2267
|
-
|
|
2268
514
|
elif args.simulate_serverless == 'azure_function':
|
|
2269
515
|
if args.azure_env:
|
|
2270
516
|
env_overrides['AZURE_FUNCTIONS_ENVIRONMENT'] = args.azure_env
|
|
2271
517
|
if args.azure_function_url:
|
|
2272
518
|
env_overrides['AZURE_FUNCTION_URL'] = args.azure_function_url
|
|
2273
519
|
|
|
2274
|
-
# Create and activate
|
|
520
|
+
# Create and activate simulator
|
|
2275
521
|
serverless_simulator = ServerlessSimulator(args.simulate_serverless, env_overrides)
|
|
2276
|
-
|
|
2277
|
-
serverless_simulator.activate(args.verbose and not args.raw)
|
|
2278
|
-
except Exception as e:
|
|
2279
|
-
print(f"Error setting up serverless simulation: {e}")
|
|
2280
|
-
return 1
|
|
522
|
+
serverless_simulator.activate(args.verbose and not args.raw)
|
|
2281
523
|
|
|
524
|
+
# ===== MAIN EXECUTION =====
|
|
2282
525
|
try:
|
|
2283
|
-
#
|
|
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
|
|
2284
533
|
if args.list_agents:
|
|
2285
|
-
if args.verbose and not args.raw:
|
|
2286
|
-
print(f"Discovering agents in: {args.agent_path}")
|
|
2287
|
-
|
|
2288
534
|
try:
|
|
2289
535
|
agents = discover_agents_in_file(args.agent_path)
|
|
2290
|
-
|
|
2291
536
|
if not agents:
|
|
2292
|
-
print(
|
|
537
|
+
print(ERROR_NO_AGENTS.format(file_path=args.agent_path))
|
|
2293
538
|
return 1
|
|
2294
539
|
|
|
2295
|
-
print(f"
|
|
2296
|
-
print()
|
|
2297
|
-
|
|
540
|
+
print(f"\nAgents found in {args.agent_path}:")
|
|
2298
541
|
for agent_info in agents:
|
|
2299
|
-
|
|
2300
|
-
|
|
542
|
+
agent_type = "instance" if agent_info['type'] == 'instance' else "class"
|
|
543
|
+
print(f" {agent_info['class_name']} ({agent_type})")
|
|
2301
544
|
if agent_info['type'] == 'instance':
|
|
2302
|
-
print(f" Type: Ready instance")
|
|
2303
545
|
print(f" Name: {agent_info['agent_name']}")
|
|
2304
546
|
print(f" Route: {agent_info['route']}")
|
|
2305
|
-
else:
|
|
2306
|
-
print(f" Type: Available class (needs instantiation)")
|
|
2307
|
-
|
|
2308
547
|
if agent_info['description']:
|
|
2309
|
-
# Clean up
|
|
548
|
+
# Clean up description
|
|
2310
549
|
desc = agent_info['description'].strip()
|
|
2311
550
|
if desc:
|
|
2312
|
-
# Take first line
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
if len(agents) > 1:
|
|
2319
|
-
print("To use a specific agent with this tool:")
|
|
2320
|
-
print(f" swaig-test {args.agent_path} [tool_name] [args] --agent-class <AgentClassName>")
|
|
2321
|
-
print()
|
|
2322
|
-
print("Examples:")
|
|
2323
|
-
for agent_info in agents:
|
|
2324
|
-
print(f" swaig-test {args.agent_path} --list-tools --agent-class {agent_info['class_name']}")
|
|
2325
|
-
print(f" swaig-test {args.agent_path} --dump-swml --agent-class {agent_info['class_name']}")
|
|
2326
|
-
print()
|
|
2327
|
-
else:
|
|
2328
|
-
print("This file contains a single agent, no --agent-class needed.")
|
|
2329
|
-
|
|
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}")
|
|
2330
556
|
return 0
|
|
2331
|
-
|
|
2332
557
|
except Exception as e:
|
|
2333
558
|
print(f"Error discovering agents: {e}")
|
|
2334
559
|
if args.verbose:
|
|
@@ -2336,255 +561,227 @@ Examples:
|
|
|
2336
561
|
traceback.print_exc()
|
|
2337
562
|
return 1
|
|
2338
563
|
|
|
2339
|
-
# Load the agent
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
|
2342
654
|
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
if not agent_class_name:
|
|
2346
|
-
# Try to auto-discover if there's only one agent
|
|
655
|
+
query_params = {}
|
|
656
|
+
if args.query_params:
|
|
2347
657
|
try:
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
print(f"Auto-selected agent: {agent_class_name}")
|
|
2353
|
-
elif len(discovered_agents) > 1:
|
|
2354
|
-
if not args.raw:
|
|
2355
|
-
print(f"Multiple agents found: {[a['class_name'] for a in discovered_agents]}")
|
|
2356
|
-
print(f"Please specify --agent-class parameter")
|
|
2357
|
-
return 1
|
|
2358
|
-
except Exception:
|
|
2359
|
-
# If discovery fails, fall back to normal loading behavior
|
|
2360
|
-
pass
|
|
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}")
|
|
2361
662
|
|
|
2362
|
-
|
|
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}")
|
|
2363
670
|
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
print("No skills loaded")
|
|
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)
|
|
2374
680
|
|
|
2375
|
-
#
|
|
681
|
+
# Handle --list-tools
|
|
2376
682
|
if args.list_tools:
|
|
2377
|
-
|
|
2378
|
-
if hasattr(agent, '_swaig_functions') and agent._swaig_functions:
|
|
2379
|
-
for name, func in agent._swaig_functions.items():
|
|
2380
|
-
if isinstance(func, dict):
|
|
2381
|
-
# DataMap function
|
|
2382
|
-
description = func.get('description', 'DataMap function (serverless)')
|
|
2383
|
-
print(f" {name} - {description}")
|
|
2384
|
-
|
|
2385
|
-
# Show parameters for DataMap functions
|
|
2386
|
-
if 'parameters' in func and func['parameters']:
|
|
2387
|
-
params = func['parameters']
|
|
2388
|
-
# Handle both formats: direct properties dict or full schema
|
|
2389
|
-
if 'properties' in params:
|
|
2390
|
-
properties = params['properties']
|
|
2391
|
-
required_fields = params.get('required', [])
|
|
2392
|
-
else:
|
|
2393
|
-
properties = params
|
|
2394
|
-
required_fields = []
|
|
2395
|
-
|
|
2396
|
-
if properties:
|
|
2397
|
-
print(f" Parameters:")
|
|
2398
|
-
for param_name, param_def in properties.items():
|
|
2399
|
-
param_type = param_def.get('type', 'unknown')
|
|
2400
|
-
param_desc = param_def.get('description', 'No description')
|
|
2401
|
-
is_required = param_name in required_fields
|
|
2402
|
-
required_marker = " (required)" if is_required else ""
|
|
2403
|
-
print(f" {param_name} ({param_type}){required_marker}: {param_desc}")
|
|
2404
|
-
else:
|
|
2405
|
-
print(f" Parameters: None")
|
|
2406
|
-
else:
|
|
2407
|
-
print(f" Parameters: None")
|
|
2408
|
-
|
|
2409
|
-
if args.verbose:
|
|
2410
|
-
print(f" Config: {json.dumps(func, indent=6)}")
|
|
2411
|
-
else:
|
|
2412
|
-
# Regular SWAIG function
|
|
2413
|
-
func_type = ""
|
|
2414
|
-
if hasattr(func, 'webhook_url') and func.webhook_url and func.is_external:
|
|
2415
|
-
func_type = " (EXTERNAL webhook)"
|
|
2416
|
-
elif hasattr(func, 'webhook_url') and func.webhook_url:
|
|
2417
|
-
func_type = " (webhook)"
|
|
2418
|
-
else:
|
|
2419
|
-
func_type = " (LOCAL webhook)"
|
|
2420
|
-
|
|
2421
|
-
print(f" {name} - {func.description}{func_type}")
|
|
2422
|
-
|
|
2423
|
-
# Show external URL if applicable
|
|
2424
|
-
if hasattr(func, 'webhook_url') and func.webhook_url and func.is_external:
|
|
2425
|
-
print(f" External URL: {func.webhook_url}")
|
|
2426
|
-
|
|
2427
|
-
# Show parameters
|
|
2428
|
-
if hasattr(func, 'parameters') and func.parameters:
|
|
2429
|
-
params = func.parameters
|
|
2430
|
-
# Handle both formats: direct properties dict or full schema
|
|
2431
|
-
if 'properties' in params:
|
|
2432
|
-
properties = params['properties']
|
|
2433
|
-
required_fields = params.get('required', [])
|
|
2434
|
-
else:
|
|
2435
|
-
properties = params
|
|
2436
|
-
required_fields = []
|
|
2437
|
-
|
|
2438
|
-
if properties:
|
|
2439
|
-
print(f" Parameters:")
|
|
2440
|
-
for param_name, param_def in properties.items():
|
|
2441
|
-
param_type = param_def.get('type', 'unknown')
|
|
2442
|
-
param_desc = param_def.get('description', 'No description')
|
|
2443
|
-
is_required = param_name in required_fields
|
|
2444
|
-
required_marker = " (required)" if is_required else ""
|
|
2445
|
-
print(f" {param_name} ({param_type}){required_marker}: {param_desc}")
|
|
2446
|
-
else:
|
|
2447
|
-
print(f" Parameters: None")
|
|
2448
|
-
else:
|
|
2449
|
-
print(f" Parameters: None")
|
|
2450
|
-
|
|
2451
|
-
if args.verbose:
|
|
2452
|
-
print(f" Function object: {func}")
|
|
2453
|
-
else:
|
|
2454
|
-
print(" No SWAIG functions registered")
|
|
683
|
+
display_agent_tools(agent, verbose=args.verbose)
|
|
2455
684
|
return 0
|
|
2456
685
|
|
|
2457
|
-
#
|
|
686
|
+
# Handle --dump-swml
|
|
2458
687
|
if args.dump_swml:
|
|
2459
688
|
return handle_dump_swml(agent, args)
|
|
2460
689
|
|
|
2461
|
-
#
|
|
2462
|
-
if
|
|
2463
|
-
#
|
|
2464
|
-
if
|
|
2465
|
-
|
|
2466
|
-
|
|
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)
|
|
2467
698
|
return 1
|
|
2468
699
|
|
|
2469
|
-
func =
|
|
700
|
+
func = functions[args.tool_name]
|
|
2470
701
|
|
|
702
|
+
# Parse function arguments
|
|
2471
703
|
try:
|
|
2472
704
|
function_args = parse_function_arguments(function_args_list, func)
|
|
2473
|
-
if args.verbose and not args.raw:
|
|
2474
|
-
print(f"Parsed arguments: {json.dumps(function_args, indent=2)}")
|
|
2475
705
|
except ValueError as e:
|
|
2476
|
-
print(f"Error: {e}")
|
|
2477
|
-
return 1
|
|
2478
|
-
elif args.args_json:
|
|
2479
|
-
# Using legacy JSON syntax
|
|
2480
|
-
try:
|
|
2481
|
-
function_args = json.loads(args.args_json)
|
|
2482
|
-
except json.JSONDecodeError as e:
|
|
2483
|
-
print(f"Error: Invalid JSON in args: {e}")
|
|
2484
|
-
return 1
|
|
2485
|
-
else:
|
|
2486
|
-
# No arguments provided
|
|
2487
|
-
function_args = {}
|
|
2488
|
-
|
|
2489
|
-
try:
|
|
2490
|
-
custom_data = json.loads(args.custom_data)
|
|
2491
|
-
except json.JSONDecodeError as e:
|
|
2492
|
-
print(f"Error: Invalid JSON in custom-data: {e}")
|
|
2493
|
-
return 1
|
|
2494
|
-
|
|
2495
|
-
# Check if the function exists (if not already checked)
|
|
2496
|
-
if not function_args_list:
|
|
2497
|
-
if not hasattr(agent, '_swaig_functions') or args.tool_name not in agent._swaig_functions:
|
|
2498
|
-
print(f"Error: Function '{args.tool_name}' not found in agent")
|
|
2499
|
-
print(f"Available functions: {list(agent._swaig_functions.keys()) if hasattr(agent, '_swaig_functions') else 'None'}")
|
|
706
|
+
print(f"Error parsing arguments: {e}")
|
|
2500
707
|
return 1
|
|
2501
708
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
# Function already retrieved during argument parsing
|
|
2505
|
-
func = agent._swaig_functions[args.tool_name]
|
|
2506
|
-
|
|
2507
|
-
# Determine function type automatically - no --datamap flag needed
|
|
2508
|
-
# DataMap functions are stored as dicts with 'data_map' key, webhook functions as SWAIGFunction objects
|
|
2509
|
-
is_datamap = isinstance(func, dict) and 'data_map' in func
|
|
2510
|
-
|
|
2511
|
-
if is_datamap:
|
|
2512
|
-
# DataMap function execution
|
|
2513
|
-
if args.verbose:
|
|
2514
|
-
print(f"\nExecuting DataMap function: {args.tool_name}")
|
|
2515
|
-
print(f"Arguments: {json.dumps(function_args, indent=2)}")
|
|
2516
|
-
print("-" * 60)
|
|
709
|
+
# Check if this is a DataMap function
|
|
710
|
+
is_datamap = isinstance(func, dict) and 'data_map' in func
|
|
2517
711
|
|
|
2518
|
-
|
|
2519
|
-
|
|
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)
|
|
2520
724
|
|
|
725
|
+
# Execute DataMap function
|
|
726
|
+
result = execute_datamap_function(func, function_args, args.verbose)
|
|
2521
727
|
print("RESULT:")
|
|
2522
728
|
print(format_result(result))
|
|
2523
|
-
|
|
729
|
+
else:
|
|
730
|
+
# Regular SWAIG function
|
|
2524
731
|
if args.verbose:
|
|
2525
|
-
print(f"\
|
|
2526
|
-
print(f"
|
|
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")
|
|
2527
739
|
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
if args.verbose:
|
|
2538
|
-
print(f"\nCalling webhook function: {args.tool_name}")
|
|
2539
|
-
print(f"Arguments: {json.dumps(function_args, indent=2)}")
|
|
2540
|
-
print(f"Function description: {func.description}")
|
|
2541
|
-
|
|
2542
|
-
# Check if this is an external webhook function
|
|
2543
|
-
is_external_webhook = hasattr(func, 'webhook_url') and func.webhook_url and func.is_external
|
|
2544
|
-
|
|
2545
|
-
if args.verbose and is_external_webhook:
|
|
2546
|
-
print(f"Function type: EXTERNAL webhook")
|
|
2547
|
-
print(f"External URL: {func.webhook_url}")
|
|
2548
|
-
elif args.verbose:
|
|
2549
|
-
print(f"Function type: LOCAL webhook")
|
|
2550
|
-
|
|
2551
|
-
# Generate post_data based on options
|
|
2552
|
-
if args.minimal:
|
|
2553
|
-
post_data = generate_minimal_post_data(args.tool_name, function_args)
|
|
2554
|
-
if custom_data:
|
|
2555
|
-
post_data.update(custom_data)
|
|
2556
|
-
elif args.fake_full_data or custom_data:
|
|
2557
|
-
post_data = generate_comprehensive_post_data(args.tool_name, function_args, custom_data)
|
|
2558
|
-
else:
|
|
2559
|
-
# Default behavior - minimal data
|
|
2560
|
-
post_data = generate_minimal_post_data(args.tool_name, function_args)
|
|
2561
|
-
|
|
2562
|
-
if args.verbose:
|
|
2563
|
-
print(f"Post data: {json.dumps(post_data, indent=2)}")
|
|
2564
|
-
print("-" * 60)
|
|
2565
|
-
|
|
2566
|
-
# Call the function
|
|
2567
|
-
try:
|
|
2568
|
-
if is_external_webhook:
|
|
2569
|
-
# For external webhook functions, make HTTP request to external service
|
|
2570
|
-
result = execute_external_webhook_function(func, args.tool_name, function_args, post_data, args.verbose)
|
|
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)
|
|
2571
749
|
else:
|
|
2572
|
-
#
|
|
2573
|
-
|
|
750
|
+
# Default behavior - minimal data
|
|
751
|
+
post_data = generate_minimal_post_data(args.tool_name, function_args)
|
|
2574
752
|
|
|
2575
|
-
|
|
2576
|
-
|
|
753
|
+
# Apply convenience mappings from CLI args (e.g., --call-id)
|
|
754
|
+
post_data = apply_convenience_mappings(post_data, args)
|
|
2577
755
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
print(f"Raw result: {repr(result)}")
|
|
756
|
+
# Apply explicit overrides
|
|
757
|
+
post_data = apply_overrides(post_data, args.override, args.override_json)
|
|
2581
758
|
|
|
2582
|
-
except Exception as e:
|
|
2583
|
-
print(f"Error calling function: {e}")
|
|
2584
759
|
if args.verbose:
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
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
|
|
2588
785
|
|
|
2589
786
|
except Exception as e:
|
|
2590
787
|
print(f"Error: {e}")
|
|
@@ -2602,8 +799,11 @@ Examples:
|
|
|
2602
799
|
|
|
2603
800
|
def console_entry_point():
|
|
2604
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'
|
|
2605
805
|
sys.exit(main())
|
|
2606
806
|
|
|
2607
807
|
|
|
2608
808
|
if __name__ == "__main__":
|
|
2609
|
-
sys.exit(main())
|
|
809
|
+
sys.exit(main())
|