signalwire-agents 0.1.23__py3-none-any.whl → 0.1.25__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.25.dist-info}/METADATA +47 -2
  58. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/RECORD +62 -22
  59. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.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.25.dist-info}/WHEEL +0 -0
  63. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.dist-info}/licenses/LICENSE +0 -0
  64. {signalwire_agents-0.1.23.dist-info → signalwire_agents-0.1.25.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"""