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.
Files changed (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {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
- SWAIG Function CLI Testing Tool
4
-
5
- This tool loads an agent application and calls SWAIG functions with comprehensive
6
- simulation of the SignalWire environment. It supports both webhook and DataMap functions.
7
-
8
- Usage:
9
- python -m signalwire_agents.cli.test_swaig <agent_path> <tool_name> <args_json>
10
-
11
- # Or directly:
12
- python signalwire_agents/cli/test_swaig.py <agent_path> <tool_name> <args_json>
13
-
14
- # Or as installed command:
15
- swaig-test <agent_path> <tool_name> <args_json>
16
-
17
- Examples:
18
- # Test DataSphere search
19
- swaig-test examples/datasphere_webhook_env_demo.py search_knowledge '{"query":"test search"}'
20
-
21
- # Test DataMap function
22
- swaig-test examples/my_agent.py my_datamap_func '{"input":"value"}' --datamap
23
-
24
- # Test with custom post_data
25
- swaig-test examples/my_agent.py my_tool '{"param":"value"}' --fake-full-data
26
-
27
- # Test with minimal data
28
- swaig-test examples/my_agent.py my_tool '{"param":"value"}' --minimal
29
-
30
- # List available tools
31
- swaig-test examples/my_agent.py --list-tools
32
-
33
- # Dump SWML document
34
- swaig-test examples/my_agent.py --dump-swml
35
-
36
- # Dump SWML with verbose output
37
- swaig-test examples/my_agent.py --dump-swml --verbose
38
-
39
- # Dump raw SWML JSON (for piping to jq/yq)
40
- swaig-test examples/my_agent.py --dump-swml --raw
41
-
42
- # Pipe to jq for pretty formatting
43
- swaig-test examples/my_agent.py --dump-swml --raw | jq '.'
44
-
45
- # Extract specific fields with jq
46
- swaig-test examples/my_agent.py --dump-swml --raw | jq '.sections.main[1].ai.SWAIG.functions'
47
- """
48
-
49
- import sys
50
- import os
51
- import json
52
- import importlib.util
53
- import argparse
54
- import uuid
55
- import time
56
- import hashlib
57
- import re
58
- import requests
59
- import warnings
60
- from pathlib import Path
61
- from typing import Dict, Any, Optional, List, Tuple
62
- from datetime import datetime
63
- import logging
64
- import inspect
65
-
66
- try:
67
- # Try to import the AgentBase class
68
- from signalwire_agents.core.agent_base import AgentBase
69
- from signalwire_agents.core.function_result import SwaigFunctionResult
70
- except ImportError:
71
- # If import fails, we'll handle it gracefully
72
- AgentBase = None
73
- SwaigFunctionResult = None
74
-
75
- # Early setup for raw mode using the central logging system
76
- if "--raw" in sys.argv:
77
- # Use the central logging system's OFF mode for raw output
78
- # This eliminates all logging output without complex suppression hacks
79
- os.environ["SIGNALWIRE_LOG_MODE"] = "off"
80
- warnings.filterwarnings("ignore")
81
-
82
- # Store original print function before any potential suppression
83
- original_print = print
84
-
85
- # Add the parent directory to the path so we can import signalwire_agents
86
- sys.path.insert(0, str(Path(__file__).parent.parent.parent))
87
-
88
- from signalwire_agents import AgentBase
89
- from signalwire_agents.core.function_result import SwaigFunctionResult
90
-
91
-
92
- # ===== MOCK REQUEST OBJECTS FOR DYNAMIC AGENT TESTING =====
93
-
94
- class MockQueryParams:
95
- """Mock FastAPI QueryParams (case-sensitive dict-like)"""
96
- def __init__(self, params: Optional[Dict[str, str]] = None):
97
- self._params = params or {}
98
-
99
- def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
100
- return self._params.get(key, default)
101
-
102
- def __getitem__(self, key: str) -> str:
103
- return self._params[key]
104
-
105
- def __contains__(self, key: str) -> bool:
106
- return key in self._params
107
-
108
- def items(self):
109
- return self._params.items()
110
-
111
- def keys(self):
112
- return self._params.keys()
113
-
114
- def values(self):
115
- return self._params.values()
116
-
117
-
118
- class MockHeaders:
119
- """Mock FastAPI Headers (case-insensitive dict-like)"""
120
- def __init__(self, headers: Optional[Dict[str, str]] = None):
121
- # Store headers with lowercase keys for case-insensitive lookup
122
- self._headers = {}
123
- if headers:
124
- for k, v in headers.items():
125
- self._headers[k.lower()] = v
126
-
127
- def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
128
- return self._headers.get(key.lower(), default)
129
-
130
- def __getitem__(self, key: str) -> str:
131
- return self._headers[key.lower()]
132
-
133
- def __contains__(self, key: str) -> bool:
134
- return key.lower() in self._headers
135
-
136
- def items(self):
137
- return self._headers.items()
138
-
139
- def keys(self):
140
- return self._headers.keys()
141
-
142
- def values(self):
143
- return self._headers.values()
144
-
145
-
146
- class MockURL:
147
- """Mock FastAPI URL object"""
148
- def __init__(self, url: str = "http://localhost:8080/swml"):
149
- self._url = url
150
- # Parse basic components
151
- if "?" in url:
152
- self.path, query_string = url.split("?", 1)
153
- self.query = query_string
154
- else:
155
- self.path = url
156
- self.query = ""
157
-
158
- # Extract scheme and netloc
159
- if "://" in url:
160
- self.scheme, rest = url.split("://", 1)
161
- if "/" in rest:
162
- self.netloc = rest.split("/", 1)[0]
163
- else:
164
- self.netloc = rest
165
- else:
166
- self.scheme = "http"
167
- self.netloc = "localhost:8080"
168
-
169
- def __str__(self):
170
- return self._url
171
-
172
-
173
- class MockRequest:
174
- """Mock FastAPI Request object for dynamic agent testing"""
175
- def __init__(self, method: str = "POST", url: str = "http://localhost:8080/swml",
176
- headers: Optional[Dict[str, str]] = None,
177
- query_params: Optional[Dict[str, str]] = None,
178
- json_body: Optional[Dict[str, Any]] = None):
179
- self.method = method
180
- self.url = MockURL(url)
181
- self.headers = MockHeaders(headers)
182
- self.query_params = MockQueryParams(query_params)
183
- self._json_body = json_body or {}
184
- self._body = json.dumps(self._json_body).encode('utf-8')
185
-
186
- async def json(self) -> Dict[str, Any]:
187
- """Return the JSON body"""
188
- return self._json_body
189
-
190
- async def body(self) -> bytes:
191
- """Return the raw body bytes"""
192
- return self._body
193
-
194
- def client(self):
195
- """Mock client property"""
196
- return type('MockClient', (), {'host': '127.0.0.1', 'port': 0})()
197
-
198
-
199
- def create_mock_request(method: str = "POST", url: str = "http://localhost:8080/swml",
200
- headers: Optional[Dict[str, str]] = None,
201
- query_params: Optional[Dict[str, str]] = None,
202
- body: Optional[Dict[str, Any]] = None) -> MockRequest:
203
- """
204
- Factory function to create a mock FastAPI Request object
205
- """
206
- return MockRequest(method=method, url=url, headers=headers,
207
- query_params=query_params, json_body=body)
208
-
209
-
210
- # ===== SERVERLESS ENVIRONMENT SIMULATION =====
211
-
212
- class ServerlessSimulator:
213
- """Manages serverless environment simulation for different platforms"""
214
-
215
- # Default environment presets for each platform
216
- PLATFORM_PRESETS = {
217
- 'lambda': {
218
- 'AWS_LAMBDA_FUNCTION_NAME': 'test-agent-function',
219
- 'AWS_LAMBDA_FUNCTION_URL': 'https://abc123.lambda-url.us-east-1.on.aws/',
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
- # Check for --raw flag and set up suppression early
1728
- if "--raw" in sys.argv:
1729
- setup_raw_mode_suppression()
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="Test SWAIG functions from agent applications with comprehensive simulation",
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
- # Required positional arguments
265
+ # Positional arguments
1882
266
  parser.add_argument(
1883
267
  "agent_path",
1884
- help="Path to the Python file containing the agent"
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
- # Function Execution Options
1900
- func_group = parser.add_argument_group('Function Execution Options')
1901
- func_group.add_argument(
1902
- "--exec",
1903
- metavar="FUNCTION",
1904
- help="Execute a function with CLI-style arguments (replaces tool_name and --args)"
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
- func_group.add_argument(
1908
- "--custom-data",
1909
- help="Optional JSON string containing custom post_data overrides",
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="Name of the agent class to use (required only if multiple agents in file)"
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
- agent_group.add_argument(
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 available agents in the file and exit"
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 available tools in the agent and exit"
302
+ help="List all tools in agent"
1930
303
  )
1931
-
1932
- # Output and Debugging Options
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="Enable verbose output"
307
+ help="Generate and output SWML document"
1938
308
  )
1939
-
1940
- output_group.add_argument(
1941
- "--raw",
1942
- action="store_true",
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
- # SWML Generation and Testing
1947
- swml_group = parser.add_argument_group('SWML Generation and Testing')
1948
- swml_group.add_argument(
1949
- "--dump-swml",
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="Dump the SWML document from the agent and exit"
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 minimal post_data (only essential keys)"
330
+ help="Use comprehensive fake post_data"
1964
331
  )
1965
332
 
1966
- # Call Configuration Options
1967
- call_group = parser.add_argument_group('Call Configuration Options')
1968
- call_group.add_argument(
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="Type of call for SWML generation (default: webrtc)"
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="Direction of call for SWML generation (default: inbound)"
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="State of call for SWML generation (default: created)"
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 'from' address in fake SWML post_data"
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 'to' address in fake SWML post_data"
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
- # User Variables and Query Parameters
2016
- vars_group = parser.add_argument_group('User Variables and Query Parameters')
2017
- vars_group.add_argument(
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 vars.userVariables in fake SWML post_data"
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 (merged into userVariables)"
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 specific values using dot notation (e.g., --override call.state=answered)"
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 headers for mock request (e.g., --header Authorization=Bearer token)"
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 Environment Simulation
2064
- serverless_group = parser.add_argument_group('Serverless Environment Simulation')
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 environment (lambda, cgi, cloud_function, azure_function)"
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 API_KEY=secret123)"
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 variables from file"
2081
- )
2082
-
2083
- serverless_group.add_argument(
2084
- "--serverless-mode",
2085
- help="Legacy option for serverless mode (use --simulate-serverless instead)"
2086
- )
2087
-
2088
- # AWS Lambda Configuration
2089
- aws_group = parser.add_argument_group('AWS Lambda Configuration')
2090
- aws_group.add_argument(
2091
- "--aws-function-name",
2092
- help="AWS Lambda function name (overrides default)"
2093
- )
2094
-
2095
- aws_group.add_argument(
2096
- "--aws-function-url",
2097
- help="AWS Lambda function URL (overrides default)"
2098
- )
2099
-
2100
- aws_group.add_argument(
2101
- "--aws-region",
2102
- help="AWS region (overrides default)"
2103
- )
2104
-
2105
- aws_group.add_argument(
2106
- "--aws-api-gateway-id",
2107
- help="AWS API Gateway ID for API Gateway URLs"
2108
- )
2109
-
2110
- aws_group.add_argument(
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="Use HTTPS for CGI URLs"
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
- azure_group.add_argument(
2168
- "--azure-function-url",
2169
- help="Azure Function URL (overrides default)"
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
- # function_args_list is already set from --exec parsing
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 agents
2187
- args.list_agents = True
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("CGI simulation requires --cgi-host")
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
- # Parse --env arguments
2227
- for env_arg in args.env:
2228
- if '=' not in env_arg:
2229
- print(f"Error: Invalid environment variable format: {env_arg}")
2230
- print("Expected format: --env KEY=value")
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
- # Add platform-specific overrides
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 serverless simulator
520
+ # Create and activate simulator
2275
521
  serverless_simulator = ServerlessSimulator(args.simulate_serverless, env_overrides)
2276
- try:
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
- # Handle agent listing first (doesn't require loading a specific agent)
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("No agents found in the file.")
537
+ print(ERROR_NO_AGENTS.format(file_path=args.agent_path))
2293
538
  return 1
2294
539
 
2295
- print(f"Available agents in {args.agent_path}:")
2296
- print()
2297
-
540
+ print(f"\nAgents found in {args.agent_path}:")
2298
541
  for agent_info in agents:
2299
- print(f" {agent_info['class_name']}")
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 the description
548
+ # Clean up description
2310
549
  desc = agent_info['description'].strip()
2311
550
  if desc:
2312
- # Take first line or sentence
2313
- first_line = desc.split('\n')[0].strip()
2314
- print(f" Description: {first_line}")
2315
-
2316
- print()
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 for other operations
2340
- if args.verbose and not args.raw:
2341
- print(f"Loading agent from: {args.agent_path}")
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
- # Auto-select agent if only one exists and no --agent-class specified
2344
- agent_class_name = getattr(args, 'agent_class', None)
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
- discovered_agents = discover_agents_in_file(args.agent_path)
2349
- if len(discovered_agents) == 1:
2350
- agent_class_name = discovered_agents[0]['class_name']
2351
- if args.verbose and not args.raw:
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
- agent = load_agent_from_file(args.agent_path, agent_class_name)
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
- if args.verbose and not args.raw:
2365
- print(f"Loaded agent: {agent.get_name()}")
2366
- print(f"Agent route: {agent.route}")
2367
-
2368
- # Show loaded skills
2369
- skills = agent.list_skills()
2370
- if skills:
2371
- print(f"Loaded skills: {', '.join(skills)}")
2372
- else:
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
- # List tools if requested
681
+ # Handle --list-tools
2376
682
  if args.list_tools:
2377
- print("\nAvailable SWAIG functions:")
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
- # Dump SWML if requested
686
+ # Handle --dump-swml
2458
687
  if args.dump_swml:
2459
688
  return handle_dump_swml(agent, args)
2460
689
 
2461
- # Parse function arguments
2462
- if function_args_list:
2463
- # Using --exec syntax, need to get the function to parse arguments with schema
2464
- if not hasattr(agent, '_swaig_functions') or args.tool_name not in agent._swaig_functions:
2465
- print(f"Error: Function '{args.tool_name}' not found in agent")
2466
- print(f"Available functions: {list(agent._swaig_functions.keys()) if hasattr(agent, '_swaig_functions') else 'None'}")
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 = agent._swaig_functions[args.tool_name]
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
- func = agent._swaig_functions[args.tool_name]
2503
- else:
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
- try:
2519
- result = execute_datamap_function(func, function_args, args.verbose)
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"\nRaw result type: {type(result).__name__}")
2526
- print(f"Raw result: {repr(result)}")
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
- except Exception as e:
2529
- print(f"Error executing DataMap function: {e}")
2530
- if args.verbose:
2531
- import traceback
2532
- traceback.print_exc()
2533
- return 1
2534
-
2535
- else:
2536
- # Webhook function execution
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
- # For local webhook functions, call the agent's handler
2573
- result = agent.on_function_call(args.tool_name, function_args, post_data)
750
+ # Default behavior - minimal data
751
+ post_data = generate_minimal_post_data(args.tool_name, function_args)
2574
752
 
2575
- print("RESULT:")
2576
- print(format_result(result))
753
+ # Apply convenience mappings from CLI args (e.g., --call-id)
754
+ post_data = apply_convenience_mappings(post_data, args)
2577
755
 
2578
- if args.verbose:
2579
- print(f"\nRaw result type: {type(result).__name__}")
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
- import traceback
2586
- traceback.print_exc()
2587
- return 1
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())