signalwire-agents 0.1.13__py3-none-any.whl → 1.0.17.dev4__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 (143) hide show
  1. signalwire_agents/__init__.py +99 -15
  2. signalwire_agents/agent_server.py +248 -60
  3. signalwire_agents/agents/bedrock.py +296 -0
  4. signalwire_agents/cli/__init__.py +9 -0
  5. signalwire_agents/cli/build_search.py +951 -41
  6. signalwire_agents/cli/config.py +80 -0
  7. signalwire_agents/cli/core/__init__.py +10 -0
  8. signalwire_agents/cli/core/agent_loader.py +470 -0
  9. signalwire_agents/cli/core/argparse_helpers.py +179 -0
  10. signalwire_agents/cli/core/dynamic_config.py +71 -0
  11. signalwire_agents/cli/core/service_loader.py +303 -0
  12. signalwire_agents/cli/dokku.py +2320 -0
  13. signalwire_agents/cli/execution/__init__.py +10 -0
  14. signalwire_agents/cli/execution/datamap_exec.py +446 -0
  15. signalwire_agents/cli/execution/webhook_exec.py +134 -0
  16. signalwire_agents/cli/init_project.py +2636 -0
  17. signalwire_agents/cli/output/__init__.py +10 -0
  18. signalwire_agents/cli/output/output_formatter.py +255 -0
  19. signalwire_agents/cli/output/swml_dump.py +186 -0
  20. signalwire_agents/cli/simulation/__init__.py +10 -0
  21. signalwire_agents/cli/simulation/data_generation.py +374 -0
  22. signalwire_agents/cli/simulation/data_overrides.py +200 -0
  23. signalwire_agents/cli/simulation/mock_env.py +282 -0
  24. signalwire_agents/cli/swaig_test_wrapper.py +52 -0
  25. signalwire_agents/cli/test_swaig.py +566 -2366
  26. signalwire_agents/cli/types.py +81 -0
  27. signalwire_agents/core/__init__.py +2 -2
  28. signalwire_agents/core/agent/__init__.py +12 -0
  29. signalwire_agents/core/agent/config/__init__.py +12 -0
  30. signalwire_agents/core/agent/deployment/__init__.py +9 -0
  31. signalwire_agents/core/agent/deployment/handlers/__init__.py +9 -0
  32. signalwire_agents/core/agent/prompt/__init__.py +14 -0
  33. signalwire_agents/core/agent/prompt/manager.py +306 -0
  34. signalwire_agents/core/agent/routing/__init__.py +9 -0
  35. signalwire_agents/core/agent/security/__init__.py +9 -0
  36. signalwire_agents/core/agent/swml/__init__.py +9 -0
  37. signalwire_agents/core/agent/tools/__init__.py +15 -0
  38. signalwire_agents/core/agent/tools/decorator.py +97 -0
  39. signalwire_agents/core/agent/tools/registry.py +210 -0
  40. signalwire_agents/core/agent_base.py +845 -2916
  41. signalwire_agents/core/auth_handler.py +233 -0
  42. signalwire_agents/core/config_loader.py +259 -0
  43. signalwire_agents/core/contexts.py +418 -0
  44. signalwire_agents/core/data_map.py +3 -15
  45. signalwire_agents/core/function_result.py +116 -44
  46. signalwire_agents/core/logging_config.py +162 -18
  47. signalwire_agents/core/mixins/__init__.py +28 -0
  48. signalwire_agents/core/mixins/ai_config_mixin.py +442 -0
  49. signalwire_agents/core/mixins/auth_mixin.py +280 -0
  50. signalwire_agents/core/mixins/prompt_mixin.py +358 -0
  51. signalwire_agents/core/mixins/serverless_mixin.py +460 -0
  52. signalwire_agents/core/mixins/skill_mixin.py +55 -0
  53. signalwire_agents/core/mixins/state_mixin.py +153 -0
  54. signalwire_agents/core/mixins/tool_mixin.py +230 -0
  55. signalwire_agents/core/mixins/web_mixin.py +1142 -0
  56. signalwire_agents/core/security_config.py +333 -0
  57. signalwire_agents/core/skill_base.py +84 -1
  58. signalwire_agents/core/skill_manager.py +62 -20
  59. signalwire_agents/core/swaig_function.py +18 -5
  60. signalwire_agents/core/swml_builder.py +207 -11
  61. signalwire_agents/core/swml_handler.py +27 -21
  62. signalwire_agents/core/swml_renderer.py +123 -312
  63. signalwire_agents/core/swml_service.py +171 -203
  64. signalwire_agents/mcp_gateway/__init__.py +29 -0
  65. signalwire_agents/mcp_gateway/gateway_service.py +564 -0
  66. signalwire_agents/mcp_gateway/mcp_manager.py +513 -0
  67. signalwire_agents/mcp_gateway/session_manager.py +218 -0
  68. signalwire_agents/prefabs/concierge.py +0 -3
  69. signalwire_agents/prefabs/faq_bot.py +0 -3
  70. signalwire_agents/prefabs/info_gatherer.py +0 -3
  71. signalwire_agents/prefabs/receptionist.py +0 -3
  72. signalwire_agents/prefabs/survey.py +0 -3
  73. signalwire_agents/schema.json +9218 -5489
  74. signalwire_agents/search/__init__.py +7 -1
  75. signalwire_agents/search/document_processor.py +490 -31
  76. signalwire_agents/search/index_builder.py +307 -37
  77. signalwire_agents/search/migration.py +418 -0
  78. signalwire_agents/search/models.py +30 -0
  79. signalwire_agents/search/pgvector_backend.py +748 -0
  80. signalwire_agents/search/query_processor.py +162 -31
  81. signalwire_agents/search/search_engine.py +916 -35
  82. signalwire_agents/search/search_service.py +376 -53
  83. signalwire_agents/skills/README.md +452 -0
  84. signalwire_agents/skills/__init__.py +14 -2
  85. signalwire_agents/skills/api_ninjas_trivia/README.md +215 -0
  86. signalwire_agents/skills/api_ninjas_trivia/__init__.py +12 -0
  87. signalwire_agents/skills/api_ninjas_trivia/skill.py +237 -0
  88. signalwire_agents/skills/datasphere/README.md +210 -0
  89. signalwire_agents/skills/datasphere/skill.py +84 -3
  90. signalwire_agents/skills/datasphere_serverless/README.md +258 -0
  91. signalwire_agents/skills/datasphere_serverless/__init__.py +9 -0
  92. signalwire_agents/skills/datasphere_serverless/skill.py +82 -1
  93. signalwire_agents/skills/datetime/README.md +132 -0
  94. signalwire_agents/skills/datetime/__init__.py +9 -0
  95. signalwire_agents/skills/datetime/skill.py +20 -7
  96. signalwire_agents/skills/joke/README.md +149 -0
  97. signalwire_agents/skills/joke/__init__.py +9 -0
  98. signalwire_agents/skills/joke/skill.py +21 -0
  99. signalwire_agents/skills/math/README.md +161 -0
  100. signalwire_agents/skills/math/__init__.py +9 -0
  101. signalwire_agents/skills/math/skill.py +18 -4
  102. signalwire_agents/skills/mcp_gateway/README.md +230 -0
  103. signalwire_agents/skills/mcp_gateway/__init__.py +10 -0
  104. signalwire_agents/skills/mcp_gateway/skill.py +421 -0
  105. signalwire_agents/skills/native_vector_search/README.md +210 -0
  106. signalwire_agents/skills/native_vector_search/__init__.py +9 -0
  107. signalwire_agents/skills/native_vector_search/skill.py +569 -101
  108. signalwire_agents/skills/play_background_file/README.md +218 -0
  109. signalwire_agents/skills/play_background_file/__init__.py +12 -0
  110. signalwire_agents/skills/play_background_file/skill.py +242 -0
  111. signalwire_agents/skills/registry.py +395 -40
  112. signalwire_agents/skills/spider/README.md +236 -0
  113. signalwire_agents/skills/spider/__init__.py +13 -0
  114. signalwire_agents/skills/spider/skill.py +598 -0
  115. signalwire_agents/skills/swml_transfer/README.md +395 -0
  116. signalwire_agents/skills/swml_transfer/__init__.py +10 -0
  117. signalwire_agents/skills/swml_transfer/skill.py +359 -0
  118. signalwire_agents/skills/weather_api/README.md +178 -0
  119. signalwire_agents/skills/weather_api/__init__.py +12 -0
  120. signalwire_agents/skills/weather_api/skill.py +191 -0
  121. signalwire_agents/skills/web_search/README.md +163 -0
  122. signalwire_agents/skills/web_search/__init__.py +9 -0
  123. signalwire_agents/skills/web_search/skill.py +586 -112
  124. signalwire_agents/skills/wikipedia_search/README.md +228 -0
  125. signalwire_agents/{core/state → skills/wikipedia_search}/__init__.py +5 -4
  126. signalwire_agents/skills/{wikipedia → wikipedia_search}/skill.py +33 -3
  127. signalwire_agents/web/__init__.py +17 -0
  128. signalwire_agents/web/web_service.py +559 -0
  129. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-agent-init.1 +400 -0
  130. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/sw-search.1 +483 -0
  131. signalwire_agents-1.0.17.dev4.data/data/share/man/man1/swaig-test.1 +308 -0
  132. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/METADATA +347 -215
  133. signalwire_agents-1.0.17.dev4.dist-info/RECORD +147 -0
  134. signalwire_agents-1.0.17.dev4.dist-info/entry_points.txt +6 -0
  135. signalwire_agents/core/state/file_state_manager.py +0 -219
  136. signalwire_agents/core/state/state_manager.py +0 -101
  137. signalwire_agents/skills/wikipedia/__init__.py +0 -9
  138. signalwire_agents-0.1.13.data/data/schema.json +0 -5611
  139. signalwire_agents-0.1.13.dist-info/RECORD +0 -67
  140. signalwire_agents-0.1.13.dist-info/entry_points.txt +0 -3
  141. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/WHEEL +0 -0
  142. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/licenses/LICENSE +0 -0
  143. {signalwire_agents-0.1.13.dist-info → signalwire_agents-1.0.17.dev4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,10 @@
1
+ """
2
+ Copyright (c) 2025 SignalWire
3
+
4
+ This file is part of the SignalWire AI Agents SDK.
5
+
6
+ Licensed under the MIT License.
7
+ See LICENSE file in the project root for full license information.
8
+ """
9
+
10
+ """Function execution modules"""
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Copyright (c) 2025 SignalWire
4
+
5
+ This file is part of the SignalWire AI Agents SDK.
6
+
7
+ Licensed under the MIT License.
8
+ See LICENSE file in the project root for full license information.
9
+ """
10
+
11
+ """
12
+ DataMap function execution and template expansion
13
+ """
14
+
15
+ import re
16
+ import json
17
+ import requests
18
+ from typing import Dict, Any
19
+ from ..config import HTTP_REQUEST_TIMEOUT
20
+
21
+
22
+ def simple_template_expand(template: str, data: Dict[str, Any]) -> str:
23
+ """
24
+ Simple template expansion for DataMap testing
25
+ Supports both ${key} and %{key} syntax with nested object access and array indexing
26
+
27
+ Args:
28
+ template: Template string with ${} or %{} variables
29
+ data: Data dictionary for expansion
30
+
31
+ Returns:
32
+ Expanded string
33
+ """
34
+ if not template:
35
+ return ""
36
+
37
+ result = template
38
+
39
+ # Handle both ${variable.path} and %{variable.path} syntax
40
+ patterns = [
41
+ r'\$\{([^}]+)\}', # ${variable} syntax
42
+ r'%\{([^}]+)\}' # %{variable} syntax
43
+ ]
44
+
45
+ for pattern in patterns:
46
+ for match in re.finditer(pattern, result):
47
+ var_path = match.group(1)
48
+
49
+ # Handle array indexing syntax like "array[0].joke"
50
+ if '[' in var_path and ']' in var_path:
51
+ # Split path with array indexing
52
+ parts = []
53
+ current_part = ""
54
+ i = 0
55
+ while i < len(var_path):
56
+ if var_path[i] == '[':
57
+ if current_part:
58
+ parts.append(current_part)
59
+ current_part = ""
60
+ # Find the closing bracket
61
+ j = i + 1
62
+ while j < len(var_path) and var_path[j] != ']':
63
+ j += 1
64
+ if j < len(var_path):
65
+ index = var_path[i+1:j]
66
+ parts.append(f"[{index}]")
67
+ i = j + 1
68
+ if i < len(var_path) and var_path[i] == '.':
69
+ i += 1 # Skip the dot after ]
70
+ else:
71
+ current_part += var_path[i]
72
+ i += 1
73
+ elif var_path[i] == '.':
74
+ if current_part:
75
+ parts.append(current_part)
76
+ current_part = ""
77
+ i += 1
78
+ else:
79
+ current_part += var_path[i]
80
+ i += 1
81
+
82
+ if current_part:
83
+ parts.append(current_part)
84
+
85
+ # Navigate through the data structure
86
+ value = data
87
+ try:
88
+ for part in parts:
89
+ if part.startswith('[') and part.endswith(']'):
90
+ # Array index
91
+ index = int(part[1:-1])
92
+ if isinstance(value, list) and 0 <= index < len(value):
93
+ value = value[index]
94
+ else:
95
+ value = f"<MISSING:{var_path}>"
96
+ break
97
+ else:
98
+ # Object property
99
+ if isinstance(value, dict) and part in value:
100
+ value = value[part]
101
+ else:
102
+ value = f"<MISSING:{var_path}>"
103
+ break
104
+ except (ValueError, TypeError, IndexError):
105
+ value = f"<MISSING:{var_path}>"
106
+
107
+ else:
108
+ # Regular nested object access (no array indexing)
109
+ path_parts = var_path.split('.')
110
+ value = data
111
+ for part in path_parts:
112
+ if isinstance(value, dict) and part in value:
113
+ value = value[part]
114
+ else:
115
+ value = f"<MISSING:{var_path}>"
116
+ break
117
+
118
+ # Replace the variable with its value
119
+ result = result.replace(match.group(0), str(value))
120
+
121
+ return result
122
+
123
+
124
+ def execute_datamap_function(datamap_config: Dict[str, Any], args: Dict[str, Any],
125
+ verbose: bool = False) -> Dict[str, Any]:
126
+ """
127
+ Execute a DataMap function following the actual DataMap processing pipeline:
128
+ 1. Expressions (pattern matching)
129
+ 2. Webhooks (try each sequentially until one succeeds)
130
+ 3. Foreach (within successful webhook)
131
+ 4. Output (from successful webhook)
132
+ 5. Fallback output (if all webhooks fail)
133
+
134
+ Args:
135
+ datamap_config: DataMap configuration dictionary
136
+ args: Function arguments
137
+ verbose: Enable verbose output
138
+
139
+ Returns:
140
+ Function result (should be string or dict with 'response' key)
141
+ """
142
+ if verbose:
143
+ print("=== DataMap Function Execution ===")
144
+ print(f"Config: {json.dumps(datamap_config, indent=2)}")
145
+ print(f"Args: {json.dumps(args, indent=2)}")
146
+
147
+ # Extract the actual data_map configuration
148
+ # DataMap configs have the structure: {"function": "...", "data_map": {...}}
149
+ actual_datamap = datamap_config.get("data_map", datamap_config)
150
+
151
+ if verbose:
152
+ print(f"Extracted data_map: {json.dumps(actual_datamap, indent=2)}")
153
+
154
+ # Initialize context with function arguments
155
+ context = {"args": args}
156
+ context.update(args) # Also make args available at top level for backward compatibility
157
+
158
+ if verbose:
159
+ print(f"Initial context: {json.dumps(context, indent=2)}")
160
+
161
+ # Step 1: Process expressions first (pattern matching)
162
+ if "expressions" in actual_datamap:
163
+ if verbose:
164
+ print("\n--- Processing Expressions ---")
165
+ for expr in actual_datamap["expressions"]:
166
+ # Simple expression evaluation - in real implementation this would be more sophisticated
167
+ if "pattern" in expr and "output" in expr:
168
+ # For testing, we'll just match simple strings
169
+ pattern = expr["pattern"]
170
+ if pattern in str(args):
171
+ if verbose:
172
+ print(f"Expression matched: {pattern}")
173
+ result = simple_template_expand(str(expr["output"]), context)
174
+ if verbose:
175
+ print(f"Expression result: {result}")
176
+ return result
177
+
178
+ # Step 2: Process webhooks sequentially
179
+ if "webhooks" in actual_datamap:
180
+ if verbose:
181
+ print("\n--- Processing Webhooks ---")
182
+
183
+ for i, webhook in enumerate(actual_datamap["webhooks"]):
184
+ if verbose:
185
+ print(f"\n=== Webhook {i+1}/{len(actual_datamap['webhooks'])} ===")
186
+
187
+ url = webhook.get("url", "")
188
+ method = webhook.get("method", "POST").upper()
189
+ headers = webhook.get("headers", {})
190
+
191
+ # Expand template variables in URL and headers
192
+ url = simple_template_expand(url, context)
193
+ expanded_headers = {}
194
+ for key, value in headers.items():
195
+ expanded_headers[key] = simple_template_expand(str(value), context)
196
+
197
+ if verbose:
198
+ print(f"Making {method} request to: {url}")
199
+ print(f"Headers: {json.dumps(expanded_headers, indent=2)}")
200
+
201
+ # Prepare request data
202
+ request_data = None
203
+ if method in ["POST", "PUT", "PATCH"]:
204
+ # Check for 'params' (SignalWire style) or 'data' (generic style) or 'body'
205
+ if "params" in webhook:
206
+ # Expand template variables in params
207
+ expanded_params = {}
208
+ for key, value in webhook["params"].items():
209
+ expanded_params[key] = simple_template_expand(str(value), context)
210
+ request_data = json.dumps(expanded_params)
211
+ elif "body" in webhook:
212
+ # Expand template variables in body
213
+ if isinstance(webhook["body"], str):
214
+ request_data = simple_template_expand(webhook["body"], context)
215
+ else:
216
+ expanded_body = {}
217
+ for key, value in webhook["body"].items():
218
+ expanded_body[key] = simple_template_expand(str(value), context)
219
+ request_data = json.dumps(expanded_body)
220
+ elif "data" in webhook:
221
+ # Expand template variables in data
222
+ if isinstance(webhook["data"], str):
223
+ request_data = simple_template_expand(webhook["data"], context)
224
+ else:
225
+ request_data = json.dumps(webhook["data"])
226
+
227
+ if verbose and request_data:
228
+ print(f"Request data: {request_data}")
229
+
230
+ webhook_failed = False
231
+ response_data = None
232
+
233
+ try:
234
+ # Make the HTTP request
235
+ if method == "GET":
236
+ response = requests.get(url, headers=expanded_headers, timeout=HTTP_REQUEST_TIMEOUT)
237
+ elif method == "POST":
238
+ response = requests.post(url, data=request_data, headers=expanded_headers, timeout=HTTP_REQUEST_TIMEOUT)
239
+ elif method == "PUT":
240
+ response = requests.put(url, data=request_data, headers=expanded_headers, timeout=HTTP_REQUEST_TIMEOUT)
241
+ elif method == "PATCH":
242
+ response = requests.patch(url, data=request_data, headers=expanded_headers, timeout=HTTP_REQUEST_TIMEOUT)
243
+ elif method == "DELETE":
244
+ response = requests.delete(url, headers=expanded_headers, timeout=HTTP_REQUEST_TIMEOUT)
245
+ else:
246
+ raise ValueError(f"Unsupported HTTP method: {method}")
247
+
248
+ if verbose:
249
+ print(f"Response status: {response.status_code}")
250
+ print(f"Response headers: {dict(response.headers)}")
251
+
252
+ # Parse response
253
+ try:
254
+ response_data = response.json()
255
+ except json.JSONDecodeError:
256
+ response_data = {"text": response.text, "status_code": response.status_code}
257
+ # Add parse_error like server does
258
+ response_data["parse_error"] = True
259
+ response_data["raw_response"] = response.text
260
+
261
+ if verbose:
262
+ print(f"Response data: {json.dumps(response_data, indent=2)}")
263
+
264
+ # Check for webhook failure following server logic
265
+
266
+ # 1. Check HTTP status code (fix the server bug - should be OR not AND)
267
+ if response.status_code < 200 or response.status_code > 299:
268
+ webhook_failed = True
269
+ if verbose:
270
+ print(f"Webhook failed: HTTP status {response.status_code} outside 200-299 range")
271
+
272
+ # 2. Check for explicit error keys (parse_error, protocol_error)
273
+ if not webhook_failed:
274
+ explicit_error_keys = ["parse_error", "protocol_error"]
275
+ for error_key in explicit_error_keys:
276
+ if error_key in response_data and response_data[error_key]:
277
+ webhook_failed = True
278
+ if verbose:
279
+ print(f"Webhook failed: Found explicit error key '{error_key}' = {response_data[error_key]}")
280
+ break
281
+
282
+ # 3. Check for custom error_keys from webhook config
283
+ if not webhook_failed and "error_keys" in webhook:
284
+ error_keys = webhook["error_keys"]
285
+ if isinstance(error_keys, str):
286
+ error_keys = [error_keys] # Convert single string to list
287
+ elif not isinstance(error_keys, list):
288
+ error_keys = []
289
+
290
+ for error_key in error_keys:
291
+ if error_key in response_data and response_data[error_key]:
292
+ webhook_failed = True
293
+ if verbose:
294
+ print(f"Webhook failed: Found custom error key '{error_key}' = {response_data[error_key]}")
295
+ break
296
+
297
+ except Exception as e:
298
+ webhook_failed = True
299
+ if verbose:
300
+ print(f"Webhook failed: HTTP request exception: {e}")
301
+ # Create error response like server does
302
+ response_data = {
303
+ "protocol_error": True,
304
+ "error": str(e)
305
+ }
306
+
307
+ # If webhook succeeded, process its output
308
+ if not webhook_failed:
309
+ if verbose:
310
+ print(f"Webhook {i+1} succeeded!")
311
+
312
+ # Add response data to context
313
+ webhook_context = context.copy()
314
+
315
+ # Handle different response types
316
+ if isinstance(response_data, list):
317
+ # For array responses, use ${array[0].field} syntax
318
+ webhook_context["array"] = response_data
319
+ if verbose:
320
+ print(f"Array response: {len(response_data)} items")
321
+ else:
322
+ # For object responses, use ${response.field} syntax
323
+ webhook_context["response"] = response_data
324
+ if verbose:
325
+ print("Object response")
326
+
327
+ # Step 3: Process webhook-level foreach (if present)
328
+ if "foreach" in webhook:
329
+ foreach_config = webhook["foreach"]
330
+ if verbose:
331
+ print(f"\n--- Processing Webhook Foreach ---")
332
+ print(f"Foreach config: {json.dumps(foreach_config, indent=2)}")
333
+
334
+ input_key = foreach_config.get("input_key", "data")
335
+ output_key = foreach_config.get("output_key", "result")
336
+ max_items = foreach_config.get("max", 100)
337
+ append_template = foreach_config.get("append", "${this.value}")
338
+
339
+ # Look for the input data in the response
340
+ input_data = None
341
+ if input_key in response_data and isinstance(response_data[input_key], list):
342
+ input_data = response_data[input_key]
343
+ if verbose:
344
+ print(f"Found array data in response.{input_key}: {len(input_data)} items")
345
+
346
+ if input_data:
347
+ result_parts = []
348
+ items_to_process = input_data[:max_items]
349
+
350
+ for item in items_to_process:
351
+ if isinstance(item, dict):
352
+ # For objects, make properties available as ${this.property}
353
+ item_context = {"this": item}
354
+ expanded = simple_template_expand(append_template, item_context)
355
+ else:
356
+ # For non-dict items, make them available as ${this.value}
357
+ item_context = {"this": {"value": item}}
358
+ expanded = simple_template_expand(append_template, item_context)
359
+ result_parts.append(expanded)
360
+
361
+ # Store the concatenated result
362
+ foreach_result = "".join(result_parts)
363
+ webhook_context[output_key] = foreach_result
364
+
365
+ if verbose:
366
+ print(f"Processed {len(items_to_process)} items")
367
+ print(f"Foreach result ({output_key}): {foreach_result[:200]}{'...' if len(foreach_result) > 200 else ''}")
368
+ else:
369
+ if verbose:
370
+ print(f"No array data found for foreach input_key: {input_key}")
371
+
372
+ # Step 4: Process webhook-level output (this is the final result)
373
+ if "output" in webhook:
374
+ webhook_output = webhook["output"]
375
+ if verbose:
376
+ print(f"\n--- Processing Webhook Output ---")
377
+ print(f"Output template: {json.dumps(webhook_output, indent=2)}")
378
+
379
+ if isinstance(webhook_output, dict):
380
+ # Process each key-value pair in the output
381
+ final_result = {}
382
+ for key, template in webhook_output.items():
383
+ expanded_value = simple_template_expand(str(template), webhook_context)
384
+ final_result[key] = expanded_value
385
+ if verbose:
386
+ print(f"Set {key} = {expanded_value}")
387
+ else:
388
+ # Single output value (string template)
389
+ final_result = simple_template_expand(str(webhook_output), webhook_context)
390
+ if verbose:
391
+ print(f"Final result = {final_result}")
392
+
393
+ if verbose:
394
+ print(f"\n--- Webhook {i+1} Final Result ---")
395
+ print(f"Result: {json.dumps(final_result, indent=2) if isinstance(final_result, dict) else final_result}")
396
+
397
+ return final_result
398
+
399
+ else:
400
+ # No output template defined, return the response data
401
+ if verbose:
402
+ print("No output template defined, returning response data")
403
+ return response_data
404
+
405
+ else:
406
+ # This webhook failed, try next webhook
407
+ if verbose:
408
+ print(f"Webhook {i+1} failed, trying next webhook...")
409
+ continue
410
+
411
+ # Step 5: All webhooks failed, use fallback output if available
412
+ if "output" in actual_datamap:
413
+ if verbose:
414
+ print(f"\n--- Using DataMap Fallback Output ---")
415
+ datamap_output = actual_datamap["output"]
416
+ if verbose:
417
+ print(f"Fallback output template: {json.dumps(datamap_output, indent=2)}")
418
+
419
+ if isinstance(datamap_output, dict):
420
+ # Process each key-value pair in the fallback output
421
+ final_result = {}
422
+ for key, template in datamap_output.items():
423
+ expanded_value = simple_template_expand(str(template), context)
424
+ final_result[key] = expanded_value
425
+ if verbose:
426
+ print(f"Fallback: Set {key} = {expanded_value}")
427
+ result = final_result
428
+ else:
429
+ # Single fallback output value
430
+ result = simple_template_expand(str(datamap_output), context)
431
+ if verbose:
432
+ print(f"Fallback result = {result}")
433
+
434
+ if verbose:
435
+ print(f"\n--- DataMap Fallback Final Result ---")
436
+ print(f"Result: {json.dumps(result, indent=2) if isinstance(result, dict) else result}")
437
+
438
+ return result
439
+
440
+ # No fallback defined, return generic error
441
+ error_result = {"error": "All webhooks failed and no fallback output defined", "status": "failed"}
442
+ if verbose:
443
+ print(f"\n--- DataMap Error Result ---")
444
+ print(f"Result: {json.dumps(error_result, indent=2)}")
445
+
446
+ return error_result
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Copyright (c) 2025 SignalWire
4
+
5
+ This file is part of the SignalWire AI Agents SDK.
6
+
7
+ Licensed under the MIT License.
8
+ See LICENSE file in the project root for full license information.
9
+ """
10
+
11
+ """
12
+ Webhook function execution (including external)
13
+ """
14
+
15
+ import json
16
+ import requests
17
+ from typing import Dict, Any, TYPE_CHECKING
18
+ from ..config import HTTP_REQUEST_TIMEOUT
19
+
20
+ if TYPE_CHECKING:
21
+ from signalwire_agents.core.swaig_function import SWAIGFunction
22
+
23
+
24
+ def execute_external_webhook_function(func: 'SWAIGFunction', function_name: str, function_args: Dict[str, Any],
25
+ post_data: Dict[str, Any], verbose: bool = False) -> Dict[str, Any]:
26
+ """
27
+ Execute an external webhook SWAIG function by making an HTTP request to the external service.
28
+ This simulates what SignalWire would do when calling an external webhook function.
29
+
30
+ Args:
31
+ func: The SWAIGFunction object with webhook_url
32
+ function_name: Name of the function being called
33
+ function_args: Parsed function arguments
34
+ post_data: Complete post data to send to the webhook
35
+ verbose: Whether to show verbose output
36
+
37
+ Returns:
38
+ Response from the external webhook service
39
+ """
40
+ webhook_url = func.webhook_url
41
+
42
+ if verbose:
43
+ print(f"\nCalling EXTERNAL webhook: {function_name}")
44
+ print(f"URL: {webhook_url}")
45
+ print(f"Arguments: {json.dumps(function_args, indent=2)}")
46
+ print("-" * 60)
47
+
48
+ # Prepare the SWAIG function call payload that SignalWire would send
49
+ swaig_payload = {
50
+ "function": function_name,
51
+ "argument": {
52
+ "parsed": [function_args] if function_args else [{}],
53
+ "raw": json.dumps(function_args) if function_args else "{}"
54
+ }
55
+ }
56
+
57
+ # Add call_id and other data from post_data if available
58
+ if "call_id" in post_data:
59
+ swaig_payload["call_id"] = post_data["call_id"]
60
+
61
+ # Add any other relevant fields from post_data
62
+ for key in ["call", "device", "vars"]:
63
+ if key in post_data:
64
+ swaig_payload[key] = post_data[key]
65
+
66
+ if verbose:
67
+ print(f"Sending payload: {json.dumps(swaig_payload, indent=2)}")
68
+ print(f"Making POST request to: {webhook_url}")
69
+
70
+ try:
71
+ # Make the HTTP request to the external webhook
72
+ headers = {
73
+ "Content-Type": "application/json",
74
+ "User-Agent": "SignalWire-SWAIG-Test/1.0"
75
+ }
76
+
77
+ response = requests.post(
78
+ webhook_url,
79
+ json=swaig_payload,
80
+ headers=headers,
81
+ timeout=HTTP_REQUEST_TIMEOUT
82
+ )
83
+
84
+ if verbose:
85
+ print(f"Response status: {response.status_code}")
86
+ print(f"Response headers: {dict(response.headers)}")
87
+
88
+ if response.status_code == 200:
89
+ try:
90
+ result = response.json()
91
+ if verbose:
92
+ print(f"✓ External webhook succeeded")
93
+ print(f"Response: {json.dumps(result, indent=2)}")
94
+ return result
95
+ except json.JSONDecodeError:
96
+ # If response is not JSON, wrap it in a response field
97
+ result = {"response": response.text}
98
+ if verbose:
99
+ print(f"✓ External webhook succeeded (text response)")
100
+ print(f"Response: {response.text}")
101
+ return result
102
+ else:
103
+ error_msg = f"External webhook returned HTTP {response.status_code}"
104
+ if verbose:
105
+ print(f"✗ External webhook failed: {error_msg}")
106
+ try:
107
+ error_detail = response.json()
108
+ print(f"Error details: {json.dumps(error_detail, indent=2)}")
109
+ except:
110
+ print(f"Error response: {response.text}")
111
+
112
+ return {
113
+ "error": error_msg,
114
+ "status_code": response.status_code,
115
+ "response": response.text
116
+ }
117
+
118
+ except requests.Timeout:
119
+ error_msg = f"External webhook timed out after {HTTP_REQUEST_TIMEOUT} seconds"
120
+ if verbose:
121
+ print(f"✗ {error_msg}")
122
+ return {"error": error_msg}
123
+
124
+ except requests.ConnectionError as e:
125
+ error_msg = f"Could not connect to external webhook: {e}"
126
+ if verbose:
127
+ print(f"✗ {error_msg}")
128
+ return {"error": error_msg}
129
+
130
+ except requests.RequestException as e:
131
+ error_msg = f"Request to external webhook failed: {e}"
132
+ if verbose:
133
+ print(f"✗ {error_msg}")
134
+ return {"error": error_msg}