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