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