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