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