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