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.
Files changed (37) hide show
  1. signalwire_agents/__init__.py +39 -4
  2. signalwire_agents/agent_server.py +46 -2
  3. signalwire_agents/cli/__init__.py +9 -0
  4. signalwire_agents/cli/test_swaig.py +2545 -0
  5. signalwire_agents/core/agent_base.py +691 -82
  6. signalwire_agents/core/contexts.py +289 -0
  7. signalwire_agents/core/data_map.py +499 -0
  8. signalwire_agents/core/function_result.py +57 -10
  9. signalwire_agents/core/skill_base.py +31 -1
  10. signalwire_agents/core/skill_manager.py +89 -23
  11. signalwire_agents/core/swaig_function.py +13 -1
  12. signalwire_agents/core/swml_handler.py +37 -13
  13. signalwire_agents/core/swml_service.py +37 -28
  14. signalwire_agents/skills/datasphere/__init__.py +12 -0
  15. signalwire_agents/skills/datasphere/skill.py +229 -0
  16. signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
  17. signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
  18. signalwire_agents/skills/datetime/skill.py +7 -3
  19. signalwire_agents/skills/joke/__init__.py +1 -0
  20. signalwire_agents/skills/joke/skill.py +88 -0
  21. signalwire_agents/skills/math/skill.py +8 -5
  22. signalwire_agents/skills/registry.py +23 -4
  23. signalwire_agents/skills/web_search/skill.py +58 -33
  24. signalwire_agents/skills/wikipedia/__init__.py +9 -0
  25. signalwire_agents/skills/wikipedia/skill.py +180 -0
  26. signalwire_agents/utils/__init__.py +2 -0
  27. signalwire_agents/utils/schema_utils.py +111 -44
  28. signalwire_agents/utils/serverless.py +38 -0
  29. signalwire_agents-0.1.11.dist-info/METADATA +756 -0
  30. signalwire_agents-0.1.11.dist-info/RECORD +58 -0
  31. {signalwire_agents-0.1.9.dist-info → signalwire_agents-0.1.11.dist-info}/WHEEL +1 -1
  32. signalwire_agents-0.1.11.dist-info/entry_points.txt +2 -0
  33. signalwire_agents-0.1.9.dist-info/METADATA +0 -311
  34. signalwire_agents-0.1.9.dist-info/RECORD +0 -44
  35. {signalwire_agents-0.1.9.data → signalwire_agents-0.1.11.data}/data/schema.json +0 -0
  36. {signalwire_agents-0.1.9.dist-info → signalwire_agents-0.1.11.dist-info}/licenses/LICENSE +0 -0
  37. {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())