signalwire-agents 0.1.6__py3-none-any.whl → 0.1.8__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.
@@ -19,6 +19,19 @@ class SwaigFunctionResult:
19
19
  Wrapper around SWAIG function responses that handles proper formatting
20
20
  of response text and actions.
21
21
 
22
+ The result object has three main components:
23
+ 1. response: Text the AI should say back to the user
24
+ 2. action: List of structured actions to execute
25
+ 3. post_process: Whether to let AI take another turn before executing actions
26
+
27
+ Post-processing behavior:
28
+ - post_process=False (default): Execute actions immediately after AI response
29
+ - post_process=True: Let AI respond to user one more time, then execute actions
30
+
31
+ This is useful for confirmation workflows like:
32
+ "I'll transfer you to sales. Do you have any other questions first?"
33
+ (AI can handle follow-up, then execute the transfer)
34
+
22
35
  Example:
23
36
  return SwaigFunctionResult("Found your order")
24
37
 
@@ -42,16 +55,31 @@ class SwaigFunctionResult:
42
55
  {"play": {"url": "music.mp3"}}
43
56
  ])
44
57
  )
58
+
59
+ # With post-processing enabled
60
+ return (
61
+ SwaigFunctionResult("Let me transfer you to billing", post_process=True)
62
+ .connect("+15551234567", final=True)
63
+ )
64
+
65
+ # Using the connect helper
66
+ return (
67
+ SwaigFunctionResult("I'll transfer you to our sales team now")
68
+ .connect("sales@company.com", final=False, from_addr="+15559876543")
69
+ )
45
70
  """
46
- def __init__(self, response: Optional[str] = None):
71
+ def __init__(self, response: Optional[str] = None, post_process: bool = False):
47
72
  """
48
73
  Initialize a new SWAIG function result
49
74
 
50
75
  Args:
51
76
  response: Optional natural language response to include
77
+ post_process: Whether to let AI take another turn before executing actions.
78
+ Defaults to False (execute actions immediately after response).
52
79
  """
53
80
  self.response = response or ""
54
81
  self.action: List[Dict[str, Any]] = []
82
+ self.post_process = post_process
55
83
 
56
84
  def set_response(self, response: str) -> 'SwaigFunctionResult':
57
85
  """
@@ -66,6 +94,23 @@ class SwaigFunctionResult:
66
94
  self.response = response
67
95
  return self
68
96
 
97
+ def set_post_process(self, post_process: bool) -> 'SwaigFunctionResult':
98
+ """
99
+ Set whether to enable post-processing for this result.
100
+
101
+ Post-processing allows the AI to take one more turn with the user
102
+ before executing any actions. This is useful for confirmation workflows.
103
+
104
+ Args:
105
+ post_process: True to let AI respond once more before executing actions,
106
+ False to execute actions immediately after the response.
107
+
108
+ Returns:
109
+ Self for method chaining
110
+ """
111
+ self.post_process = post_process
112
+ return self
113
+
69
114
  def add_action(self, name: str, data: Any) -> 'SwaigFunctionResult':
70
115
  """
71
116
  Add a structured action to the response
@@ -93,6 +138,983 @@ class SwaigFunctionResult:
93
138
  self.action.extend(actions)
94
139
  return self
95
140
 
141
+ def connect(self, destination: str, final: bool = True, from_addr: Optional[str] = None) -> 'SwaigFunctionResult':
142
+ """
143
+ Add a connect action to transfer/connect the call to another destination.
144
+
145
+ This is a convenience method that abstracts the SWML connect verb, so users
146
+ don't need to manually construct SWML documents.
147
+
148
+ Transfer behavior:
149
+ - final=True: Permanent transfer - call exits the agent completely,
150
+ SWML replaces the agent and call continues there
151
+ - final=False: Temporary transfer - if far end hangs up, call returns
152
+ to the agent to continue the conversation
153
+
154
+ Args:
155
+ destination: Where to connect the call (phone number, SIP address, etc.)
156
+ final: Whether this is a permanent transfer (True) or temporary (False).
157
+ Defaults to True for permanent transfers.
158
+ from_addr: Optional caller ID override (phone number or SIP address).
159
+ If None, uses the current call's from address.
160
+
161
+ Returns:
162
+ Self for method chaining
163
+
164
+ Example:
165
+ # Permanent transfer to a phone number
166
+ result.connect("+15551234567", final=True)
167
+
168
+ # Temporary transfer to SIP address with custom caller ID
169
+ result.connect("support@company.com", final=False, from_addr="+15559876543")
170
+ """
171
+ # Build the connect verb parameters
172
+ connect_params = {"to": destination}
173
+ if from_addr is not None:
174
+ connect_params["from"] = from_addr
175
+
176
+ # Create the SWML action
177
+ swml_action = {
178
+ "SWML": {
179
+ "sections": {
180
+ "main": [{"connect": connect_params}]
181
+ },
182
+ "version": "1.0.0"
183
+ },
184
+ "transfer": str(final).lower() # Convert boolean to "true"/"false" string
185
+ }
186
+
187
+ # Add to actions list
188
+ self.action.append(swml_action)
189
+ return self
190
+
191
+ def update_global_data(self, data: Dict[str, Any]) -> 'SwaigFunctionResult':
192
+ """
193
+ Update global agent data variables.
194
+
195
+ This is a convenience method that abstracts the set_global_data action.
196
+ Global data persists across the entire agent session and is available
197
+ in prompt variables and can be accessed by all functions.
198
+
199
+ Args:
200
+ data: Dictionary of key-value pairs to set/update in global data
201
+
202
+ Returns:
203
+ self for method chaining
204
+ """
205
+ action = {"set_global_data": data}
206
+ return self.add_action("set_global_data", action)
207
+
208
+ def execute_swml(self, swml_content, transfer: bool = False) -> 'SwaigFunctionResult':
209
+ """
210
+ Execute SWML content with optional transfer behavior.
211
+
212
+ Args:
213
+ swml_content: Can be:
214
+ - String: Raw SWML JSON text
215
+ - Dict: SWML data structure
216
+ - SWML object: SignalWire SWML SDK object with .to_dict() method
217
+ transfer: Boolean - whether call should exit agent after execution
218
+
219
+ Returns:
220
+ self for method chaining
221
+ """
222
+ # Detect input type and normalize to appropriate format
223
+ if isinstance(swml_content, str):
224
+ # Raw SWML string - use as-is
225
+ swml_data = swml_content
226
+ elif hasattr(swml_content, 'to_dict'):
227
+ # SWML SDK object - convert to dict
228
+ swml_data = swml_content.to_dict()
229
+ elif isinstance(swml_content, dict):
230
+ # Dict - use directly
231
+ swml_data = swml_content
232
+ else:
233
+ raise TypeError("swml_content must be string, dict, or SWML object")
234
+
235
+ action = {"SWML": swml_data}
236
+ if transfer:
237
+ action["transfer"] = "true"
238
+
239
+ return self.add_action("SWML", action)
240
+
241
+ def hangup(self) -> 'SwaigFunctionResult':
242
+ """
243
+ Terminate the call.
244
+
245
+ Returns:
246
+ self for method chaining
247
+ """
248
+ action = {"hangup": True}
249
+ return self.add_action("hangup", action)
250
+
251
+ def hold(self, timeout: int = 300) -> 'SwaigFunctionResult':
252
+ """
253
+ Put the call on hold with optional timeout.
254
+
255
+ Args:
256
+ timeout: Timeout in seconds (max 900, default 300)
257
+
258
+ Returns:
259
+ self for method chaining
260
+ """
261
+ # Clamp timeout to valid range
262
+ timeout = max(0, min(timeout, 900))
263
+ action = {"hold": timeout}
264
+ return self.add_action("hold", action)
265
+
266
+ def wait_for_user(self, enabled: Optional[bool] = None, timeout: Optional[int] = None, answer_first: bool = False) -> 'SwaigFunctionResult':
267
+ """
268
+ Control how agent waits for user input.
269
+
270
+ Args:
271
+ enabled: Boolean to enable/disable waiting
272
+ timeout: Number of seconds to wait
273
+ answer_first: Special "answer_first" mode
274
+
275
+ Returns:
276
+ self for method chaining
277
+ """
278
+ if answer_first:
279
+ wait_value = "answer_first"
280
+ elif timeout is not None:
281
+ wait_value = timeout
282
+ elif enabled is not None:
283
+ wait_value = enabled
284
+ else:
285
+ wait_value = True
286
+
287
+ action = {"wait_for_user": wait_value}
288
+ return self.add_action("wait_for_user", action)
289
+
290
+ def stop(self) -> 'SwaigFunctionResult':
291
+ """
292
+ Stop the agent execution.
293
+
294
+ Returns:
295
+ self for method chaining
296
+ """
297
+ action = {"stop": True}
298
+ return self.add_action("stop", action)
299
+
300
+ def say(self, text: str) -> 'SwaigFunctionResult':
301
+ """
302
+ Make the agent speak specific text.
303
+
304
+ Args:
305
+ text: Text for agent to speak
306
+
307
+ Returns:
308
+ self for method chaining
309
+ """
310
+ action = {"say": text}
311
+ return self.add_action("say", action)
312
+
313
+ def play_background_audio(self, filename: str, wait: bool = False) -> 'SwaigFunctionResult':
314
+ """
315
+ Play audio file in background.
316
+
317
+ Args:
318
+ filename: Audio filename/path
319
+ wait: Whether to suppress attention-getting behavior during playback
320
+
321
+ Returns:
322
+ self for method chaining
323
+ """
324
+ if wait:
325
+ action = {"playback_bg": {"file": filename, "wait": True}}
326
+ else:
327
+ action = {"playback_bg": filename}
328
+ return self.add_action("playback_bg", action)
329
+
330
+ def stop_background_audio(self) -> 'SwaigFunctionResult':
331
+ """
332
+ Stop currently playing background audio.
333
+
334
+ Returns:
335
+ self for method chaining
336
+ """
337
+ action = {"stop_playback_bg": True}
338
+ return self.add_action("stop_playback_bg", action)
339
+
340
+ def set_end_of_speech_timeout(self, milliseconds: int) -> 'SwaigFunctionResult':
341
+ """
342
+ Adjust end of speech timeout - milliseconds of silence after speaking
343
+ has been detected to finalize speech recognition.
344
+
345
+ Args:
346
+ milliseconds: Timeout in milliseconds
347
+
348
+ Returns:
349
+ self for method chaining
350
+ """
351
+ action = {"end_of_speech_timeout": milliseconds}
352
+ return self.add_action("end_of_speech_timeout", action)
353
+
354
+ def set_speech_event_timeout(self, milliseconds: int) -> 'SwaigFunctionResult':
355
+ """
356
+ Adjust speech event timeout - milliseconds since last speech detection
357
+ event to finalize recognition. Works better in noisy environments.
358
+
359
+ Args:
360
+ milliseconds: Timeout in milliseconds
361
+
362
+ Returns:
363
+ self for method chaining
364
+ """
365
+ action = {"speech_event_timeout": milliseconds}
366
+ return self.add_action("speech_event_timeout", action)
367
+
368
+ def remove_global_data(self, keys: Union[str, List[str]]) -> 'SwaigFunctionResult':
369
+ """
370
+ Remove global agent data variables.
371
+
372
+ Args:
373
+ keys: Single key string or list of keys to remove
374
+
375
+ Returns:
376
+ self for method chaining
377
+ """
378
+ action = {"unset_global_data": keys}
379
+ return self.add_action("unset_global_data", action)
380
+
381
+ def set_metadata(self, data: Dict[str, Any]) -> 'SwaigFunctionResult':
382
+ """
383
+ Set metadata scoped to current function's meta_data_token.
384
+
385
+ Args:
386
+ data: Dictionary of key-value pairs for metadata
387
+
388
+ Returns:
389
+ self for method chaining
390
+ """
391
+ action = {"set_meta_data": data}
392
+ return self.add_action("set_meta_data", action)
393
+
394
+ def remove_metadata(self, keys: Union[str, List[str]]) -> 'SwaigFunctionResult':
395
+ """
396
+ Remove metadata from current function's meta_data_token scope.
397
+
398
+ Args:
399
+ keys: Single key string or list of keys to remove
400
+
401
+ Returns:
402
+ self for method chaining
403
+ """
404
+ action = {"unset_meta_data": keys}
405
+ return self.add_action("unset_meta_data", action)
406
+
407
+ def toggle_functions(self, function_toggles: List[Dict[str, Any]]) -> 'SwaigFunctionResult':
408
+ """
409
+ Enable/disable specific SWAIG functions.
410
+
411
+ Args:
412
+ function_toggles: List of dicts with 'function' and 'active' keys
413
+
414
+ Returns:
415
+ self for method chaining
416
+ """
417
+ action = {"toggle_functions": function_toggles}
418
+ return self.add_action("toggle_functions", action)
419
+
420
+ def enable_functions_on_timeout(self, enabled: bool = True) -> 'SwaigFunctionResult':
421
+ """
422
+ Enable function calls on speaker timeout.
423
+
424
+ Args:
425
+ enabled: Whether to enable functions on timeout
426
+
427
+ Returns:
428
+ self for method chaining
429
+ """
430
+ action = {"functions_on_speaker_timeout": enabled}
431
+ return self.add_action("functions_on_speaker_timeout", action)
432
+
433
+ def enable_extensive_data(self, enabled: bool = True) -> 'SwaigFunctionResult':
434
+ """
435
+ Send full data to LLM for this turn only, then use smaller replacement
436
+ in subsequent turns.
437
+
438
+ Args:
439
+ enabled: Whether to send extensive data this turn only
440
+
441
+ Returns:
442
+ self for method chaining
443
+ """
444
+ action = {"extensive_data": enabled}
445
+ return self.add_action("extensive_data", action)
446
+
447
+ def update_settings(self, settings: Dict[str, Any]) -> 'SwaigFunctionResult':
448
+ """
449
+ Update agent runtime settings.
450
+
451
+ Supported settings:
452
+ - frequency-penalty: Float (-2.0 to 2.0)
453
+ - presence-penalty: Float (-2.0 to 2.0)
454
+ - max-tokens: Integer (0 to 4096)
455
+ - top-p: Float (0.0 to 1.0)
456
+ - confidence: Float (0.0 to 1.0)
457
+ - barge-confidence: Float (0.0 to 1.0)
458
+ - temperature: Float (0.0 to 2.0, clamped to 1.5)
459
+
460
+ Args:
461
+ settings: Dictionary of settings to update
462
+
463
+ Returns:
464
+ self for method chaining
465
+ """
466
+ action = {"settings": settings}
467
+ return self.add_action("settings", action)
468
+
469
+ def switch_context(self, system_prompt: Optional[str] = None, user_prompt: Optional[str] = None,
470
+ consolidate: bool = False, full_reset: bool = False) -> 'SwaigFunctionResult':
471
+ """
472
+ Change agent context/prompt during conversation.
473
+
474
+ Args:
475
+ system_prompt: New system prompt
476
+ user_prompt: User message to add
477
+ consolidate: Whether to summarize existing conversation
478
+ full_reset: Whether to do complete context reset
479
+
480
+ Returns:
481
+ self for method chaining
482
+ """
483
+ if system_prompt and not user_prompt and not consolidate and not full_reset:
484
+ # Simple string context switch
485
+ action = {"context_switch": system_prompt}
486
+ else:
487
+ # Advanced object context switch
488
+ context_data = {}
489
+ if system_prompt:
490
+ context_data["system_prompt"] = system_prompt
491
+ if user_prompt:
492
+ context_data["user_prompt"] = user_prompt
493
+ if consolidate:
494
+ context_data["consolidate"] = True
495
+ if full_reset:
496
+ context_data["full_reset"] = True
497
+ action = {"context_switch": context_data}
498
+
499
+ return self.add_action("context_switch", action)
500
+
501
+ def simulate_user_input(self, text: str) -> 'SwaigFunctionResult':
502
+ """
503
+ Queue simulated user input.
504
+
505
+ Args:
506
+ text: Text to simulate as user input
507
+
508
+ Returns:
509
+ self for method chaining
510
+ """
511
+ action = {"user_input": text}
512
+ return self.add_action("user_input", action)
513
+
514
+ def send_sms(self, to_number: str, from_number: str, body: Optional[str] = None,
515
+ media: Optional[List[str]] = None, tags: Optional[List[str]] = None,
516
+ region: Optional[str] = None) -> 'SwaigFunctionResult':
517
+ """
518
+ Send a text message to a PSTN phone number using SWML.
519
+
520
+ This is a virtual helper that generates SWML to send SMS messages.
521
+ Either body or media (or both) must be provided.
522
+
523
+ Args:
524
+ to_number: Phone number in E.164 format to send to
525
+ from_number: Phone number in E.164 format to send from
526
+ body: Body text of the message (optional if media provided)
527
+ media: Array of URLs to send in the message (optional if body provided)
528
+ tags: Array of tags to associate with the message for UI searching
529
+ region: Region to originate the message from
530
+
531
+ Returns:
532
+ self for method chaining
533
+
534
+ Raises:
535
+ ValueError: If neither body nor media is provided
536
+ """
537
+ # Validate that at least body or media is provided
538
+ if not body and not media:
539
+ raise ValueError("Either body or media must be provided")
540
+
541
+ # Build the send_sms parameters
542
+ sms_params = {
543
+ "to_number": to_number,
544
+ "from_number": from_number
545
+ }
546
+
547
+ # Add optional parameters
548
+ if body:
549
+ sms_params["body"] = body
550
+ if media:
551
+ sms_params["media"] = media
552
+ if tags:
553
+ sms_params["tags"] = tags
554
+ if region:
555
+ sms_params["region"] = region
556
+
557
+ # Generate SWML document
558
+ swml_doc = {
559
+ "version": "1.0.0",
560
+ "sections": {
561
+ "main": [
562
+ {"send_sms": sms_params}
563
+ ]
564
+ }
565
+ }
566
+
567
+ # Use execute_swml to add the action
568
+ return self.execute_swml(swml_doc)
569
+
570
+ def pay(self, payment_connector_url: str, input_method: str = "dtmf",
571
+ status_url: Optional[str] = None, payment_method: str = "credit-card",
572
+ timeout: int = 5, max_attempts: int = 1, security_code: bool = True,
573
+ postal_code: Union[bool, str] = True, min_postal_code_length: int = 0,
574
+ token_type: str = "reusable", charge_amount: Optional[str] = None,
575
+ currency: str = "usd", language: str = "en-US", voice: str = "woman",
576
+ description: Optional[str] = None, valid_card_types: str = "visa mastercard amex",
577
+ parameters: Optional[List[Dict[str, str]]] = None,
578
+ prompts: Optional[List[Dict[str, Any]]] = None) -> 'SwaigFunctionResult':
579
+ """
580
+ Process payment using SWML pay action.
581
+
582
+ This is a virtual helper that generates SWML for payment processing.
583
+
584
+ Args:
585
+ payment_connector_url: URL to make payment requests to (required)
586
+ input_method: Method to collect payment details ("dtmf" or "voice")
587
+ status_url: URL for status change notifications
588
+ payment_method: Payment method ("credit-card" currently supported)
589
+ timeout: Seconds to wait for next digit (default: 5)
590
+ max_attempts: Number of retry attempts (default: 1)
591
+ security_code: Whether to prompt for security code (default: True)
592
+ postal_code: Whether to prompt for postal code, or actual postcode
593
+ min_postal_code_length: Minimum postal code digits (default: 0)
594
+ token_type: Payment type ("one-time" or "reusable", default: "reusable")
595
+ charge_amount: Amount to charge as decimal string
596
+ currency: Currency code (default: "usd")
597
+ language: Language for prompts (default: "en-US")
598
+ voice: TTS voice to use (default: "woman")
599
+ description: Custom payment description
600
+ valid_card_types: Space-separated card types (default: "visa mastercard amex")
601
+ parameters: Array of name/value pairs for payment connector
602
+ prompts: Array of custom prompt configurations
603
+
604
+ Returns:
605
+ self for method chaining
606
+ """
607
+ # Build the pay parameters
608
+ pay_params = {
609
+ "payment_connector_url": payment_connector_url,
610
+ "input": input_method,
611
+ "payment_method": payment_method,
612
+ "timeout": str(timeout),
613
+ "max_attempts": str(max_attempts),
614
+ "security_code": str(security_code).lower(),
615
+ "min_postal_code_length": str(min_postal_code_length),
616
+ "token_type": token_type,
617
+ "currency": currency,
618
+ "language": language,
619
+ "voice": voice,
620
+ "valid_card_types": valid_card_types
621
+ }
622
+
623
+ # Handle postal_code (can be boolean or string)
624
+ if isinstance(postal_code, bool):
625
+ pay_params["postal_code"] = str(postal_code).lower()
626
+ else:
627
+ pay_params["postal_code"] = postal_code
628
+
629
+ # Add optional parameters
630
+ if status_url:
631
+ pay_params["status_url"] = status_url
632
+ if charge_amount:
633
+ pay_params["charge_amount"] = charge_amount
634
+ if description:
635
+ pay_params["description"] = description
636
+ if parameters:
637
+ pay_params["parameters"] = parameters
638
+ if prompts:
639
+ pay_params["prompts"] = prompts
640
+
641
+ # Generate SWML document
642
+ swml_doc = {
643
+ "version": "1.0.0",
644
+ "sections": {
645
+ "main": [
646
+ {"pay": pay_params}
647
+ ]
648
+ }
649
+ }
650
+
651
+ # Use execute_swml to add the action
652
+ return self.execute_swml(swml_doc)
653
+
654
+ def record_call(self, control_id: Optional[str] = None, stereo: bool = False,
655
+ format: str = "wav", direction: str = "both",
656
+ terminators: Optional[str] = None, beep: bool = False,
657
+ input_sensitivity: float = 44.0, initial_timeout: float = 0.0,
658
+ end_silence_timeout: float = 0.0, max_length: Optional[float] = None,
659
+ status_url: Optional[str] = None) -> 'SwaigFunctionResult':
660
+ """
661
+ Start background call recording using SWML.
662
+
663
+ This is a virtual helper that generates SWML to start recording the call
664
+ in the background. Unlike foreground recording, the script continues
665
+ executing while recording happens in the background.
666
+
667
+ Args:
668
+ control_id: Identifier for this recording (for use with stop_record_call)
669
+ stereo: Record in stereo (default: False)
670
+ format: Recording format - "wav" or "mp3" (default: "wav")
671
+ direction: Audio direction - "speak", "listen", or "both" (default: "both")
672
+ terminators: Digits that stop recording when pressed
673
+ beep: Play beep before recording (default: False)
674
+ input_sensitivity: Input sensitivity for recording (default: 44.0)
675
+ initial_timeout: Time in seconds to wait for speech start (default: 0.0)
676
+ end_silence_timeout: Time in seconds to wait in silence before ending (default: 0.0)
677
+ max_length: Maximum recording length in seconds
678
+ status_url: URL to send recording status events to
679
+
680
+ Returns:
681
+ self for method chaining
682
+ """
683
+ # Validate format parameter
684
+ if format not in ["wav", "mp3"]:
685
+ raise ValueError("format must be 'wav' or 'mp3'")
686
+
687
+ # Validate direction parameter
688
+ if direction not in ["speak", "listen", "both"]:
689
+ raise ValueError("direction must be 'speak', 'listen', or 'both'")
690
+
691
+ # Build the record_call parameters
692
+ record_params = {
693
+ "stereo": stereo,
694
+ "format": format,
695
+ "direction": direction,
696
+ "beep": beep,
697
+ "input_sensitivity": input_sensitivity,
698
+ "initial_timeout": initial_timeout,
699
+ "end_silence_timeout": end_silence_timeout
700
+ }
701
+
702
+ # Add optional parameters
703
+ if control_id:
704
+ record_params["control_id"] = control_id
705
+ if terminators:
706
+ record_params["terminators"] = terminators
707
+ if max_length:
708
+ record_params["max_length"] = max_length
709
+ if status_url:
710
+ record_params["status_url"] = status_url
711
+
712
+ # Generate SWML document
713
+ swml_doc = {
714
+ "version": "1.0.0",
715
+ "sections": {
716
+ "main": [
717
+ {"record_call": record_params}
718
+ ]
719
+ }
720
+ }
721
+
722
+ # Use execute_swml to add the action
723
+ return self.execute_swml(swml_doc)
724
+
725
+ def stop_record_call(self, control_id: Optional[str] = None) -> 'SwaigFunctionResult':
726
+ """
727
+ Stop an active background call recording using SWML.
728
+
729
+ This is a virtual helper that generates SWML to stop a recording that
730
+ was started with record_call().
731
+
732
+ Args:
733
+ control_id: Identifier for the recording to stop. If not provided,
734
+ the most recent recording will be stopped.
735
+
736
+ Returns:
737
+ self for method chaining
738
+ """
739
+ # Build the stop_record_call parameters
740
+ stop_params = {}
741
+ if control_id:
742
+ stop_params["control_id"] = control_id
743
+
744
+ # Generate SWML document
745
+ swml_doc = {
746
+ "version": "1.0.0",
747
+ "sections": {
748
+ "main": [
749
+ {"stop_record_call": stop_params}
750
+ ]
751
+ }
752
+ }
753
+
754
+ # Use execute_swml to add the action
755
+ return self.execute_swml(swml_doc)
756
+
757
+ def join_room(self, name: str) -> 'SwaigFunctionResult':
758
+ """
759
+ Join a RELAY room using SWML.
760
+
761
+ This is a virtual helper that generates SWML to join a RELAY room,
762
+ which enables multi-party communication and collaboration.
763
+
764
+ Args:
765
+ name: The name of the room to join (required)
766
+
767
+ Returns:
768
+ self for method chaining
769
+ """
770
+ # Build the join_room parameters
771
+ join_params = {"name": name}
772
+
773
+ # Generate SWML document
774
+ swml_doc = {
775
+ "version": "1.0.0",
776
+ "sections": {
777
+ "main": [
778
+ {"join_room": join_params}
779
+ ]
780
+ }
781
+ }
782
+
783
+ # Use execute_swml to add the action
784
+ return self.execute_swml(swml_doc)
785
+
786
+ def sip_refer(self, to_uri: str) -> 'SwaigFunctionResult':
787
+ """
788
+ Send SIP REFER to a SIP call using SWML.
789
+
790
+ This is a virtual helper that generates SWML to send a SIP REFER
791
+ message, which is used for call transfer in SIP environments.
792
+
793
+ Args:
794
+ to_uri: The SIP URI to send the REFER to (required)
795
+
796
+ Returns:
797
+ self for method chaining
798
+ """
799
+ # Build the sip_refer parameters
800
+ refer_params = {"to_uri": to_uri}
801
+
802
+ # Generate SWML document
803
+ swml_doc = {
804
+ "version": "1.0.0",
805
+ "sections": {
806
+ "main": [
807
+ {"sip_refer": refer_params}
808
+ ]
809
+ }
810
+ }
811
+
812
+ # Use execute_swml to add the action
813
+ return self.execute_swml(swml_doc)
814
+
815
+ def join_conference(self, name: str, muted: bool = False, beep: str = "true",
816
+ start_on_enter: bool = True, end_on_exit: bool = False,
817
+ wait_url: Optional[str] = None, max_participants: int = 250,
818
+ record: str = "do-not-record", region: Optional[str] = None,
819
+ trim: str = "trim-silence", coach: Optional[str] = None,
820
+ status_callback_event: Optional[str] = None,
821
+ status_callback: Optional[str] = None,
822
+ status_callback_method: str = "POST",
823
+ recording_status_callback: Optional[str] = None,
824
+ recording_status_callback_method: str = "POST",
825
+ recording_status_callback_event: str = "completed",
826
+ result: Optional[Any] = None) -> 'SwaigFunctionResult':
827
+ """
828
+ Join an ad-hoc audio conference with RELAY and CXML calls using SWML.
829
+
830
+ This is a virtual helper that generates SWML to join audio conferences
831
+ with extensive configuration options for call management and recording.
832
+
833
+ Args:
834
+ name: Name of conference (required)
835
+ muted: Whether to join muted (default: False)
836
+ beep: Beep configuration - "true", "false", "onEnter", "onExit" (default: "true")
837
+ start_on_enter: Whether conference starts when this participant enters (default: True)
838
+ end_on_exit: Whether conference ends when this participant exits (default: False)
839
+ wait_url: SWML URL for hold music (default: None for default hold music)
840
+ max_participants: Maximum participants <= 250 (default: 250)
841
+ record: Recording mode - "do-not-record", "record-from-start" (default: "do-not-record")
842
+ region: Conference region (default: None)
843
+ trim: Trim silence - "trim-silence", "do-not-trim" (default: "trim-silence")
844
+ coach: SWML Call ID or CXML CallSid for coaching (default: None)
845
+ status_callback_event: Events to report - "start end join leave mute hold modify speaker announcement" (default: None)
846
+ status_callback: URL for status callbacks (default: None)
847
+ status_callback_method: HTTP method - "GET", "POST" (default: "POST")
848
+ recording_status_callback: URL for recording status callbacks (default: None)
849
+ recording_status_callback_method: HTTP method - "GET", "POST" (default: "POST")
850
+ recording_status_callback_event: Recording events - "in-progress completed absent" (default: "completed")
851
+ result: Switch on return_value when object {} or cond when array [] (default: None)
852
+
853
+ Returns:
854
+ self for method chaining
855
+
856
+ Raises:
857
+ ValueError: If beep value is invalid or max_participants exceeds 250
858
+ """
859
+ # Validate beep parameter
860
+ valid_beep_values = ["true", "false", "onEnter", "onExit"]
861
+ if beep not in valid_beep_values:
862
+ raise ValueError(f"beep must be one of {valid_beep_values}")
863
+
864
+ # Validate max_participants
865
+ if max_participants <= 0 or max_participants > 250:
866
+ raise ValueError("max_participants must be a positive integer <= 250")
867
+
868
+ # Validate record parameter
869
+ valid_record_values = ["do-not-record", "record-from-start"]
870
+ if record not in valid_record_values:
871
+ raise ValueError(f"record must be one of {valid_record_values}")
872
+
873
+ # Validate trim parameter
874
+ valid_trim_values = ["trim-silence", "do-not-trim"]
875
+ if trim not in valid_trim_values:
876
+ raise ValueError(f"trim must be one of {valid_trim_values}")
877
+
878
+ # Validate status_callback_method
879
+ valid_methods = ["GET", "POST"]
880
+ if status_callback_method not in valid_methods:
881
+ raise ValueError(f"status_callback_method must be one of {valid_methods}")
882
+ if recording_status_callback_method not in valid_methods:
883
+ raise ValueError(f"recording_status_callback_method must be one of {valid_methods}")
884
+
885
+ # Build the join_conference parameters - start with required parameter
886
+ if isinstance(name, str) and not name.strip():
887
+ raise ValueError("name cannot be empty")
888
+
889
+ # For simple case, can just be the conference name
890
+ if (not muted and beep == "true" and start_on_enter and not end_on_exit and
891
+ wait_url is None and max_participants == 250 and record == "do-not-record" and
892
+ region is None and trim == "trim-silence" and coach is None and
893
+ status_callback_event is None and status_callback is None and
894
+ status_callback_method == "POST" and recording_status_callback is None and
895
+ recording_status_callback_method == "POST" and recording_status_callback_event == "completed" and
896
+ result is None):
897
+ # Simple form - just the conference name
898
+ join_params = name
899
+ else:
900
+ # Full object form with parameters
901
+ join_params = {"name": name}
902
+
903
+ # Add non-default parameters
904
+ if muted:
905
+ join_params["muted"] = muted
906
+ if beep != "true":
907
+ join_params["beep"] = beep
908
+ if not start_on_enter:
909
+ join_params["start_on_enter"] = start_on_enter
910
+ if end_on_exit:
911
+ join_params["end_on_exit"] = end_on_exit
912
+ if wait_url:
913
+ join_params["wait_url"] = wait_url
914
+ if max_participants != 250:
915
+ join_params["max_participants"] = max_participants
916
+ if record != "do-not-record":
917
+ join_params["record"] = record
918
+ if region:
919
+ join_params["region"] = region
920
+ if trim != "trim-silence":
921
+ join_params["trim"] = trim
922
+ if coach:
923
+ join_params["coach"] = coach
924
+ if status_callback_event:
925
+ join_params["status_callback_event"] = status_callback_event
926
+ if status_callback:
927
+ join_params["status_callback"] = status_callback
928
+ if status_callback_method != "POST":
929
+ join_params["status_callback_method"] = status_callback_method
930
+ if recording_status_callback:
931
+ join_params["recording_status_callback"] = recording_status_callback
932
+ if recording_status_callback_method != "POST":
933
+ join_params["recording_status_callback_method"] = recording_status_callback_method
934
+ if recording_status_callback_event != "completed":
935
+ join_params["recording_status_callback_event"] = recording_status_callback_event
936
+ if result is not None:
937
+ join_params["result"] = result
938
+
939
+ # Generate SWML document
940
+ swml_doc = {
941
+ "version": "1.0.0",
942
+ "sections": {
943
+ "main": [
944
+ {"join_conference": join_params}
945
+ ]
946
+ }
947
+ }
948
+
949
+ # Use execute_swml to add the action
950
+ return self.execute_swml(swml_doc)
951
+
952
+ def tap(self, uri: str, control_id: Optional[str] = None, direction: str = "both",
953
+ codec: str = "PCMU", rtp_ptime: int = 20,
954
+ status_url: Optional[str] = None) -> 'SwaigFunctionResult':
955
+ """
956
+ Start background call tap using SWML.
957
+
958
+ This is a virtual helper that generates SWML to start background call tapping.
959
+ Media is streamed over Websocket or RTP to customer controlled URI.
960
+
961
+ Args:
962
+ uri: Destination of tap media stream (required)
963
+ Formats: rtp://IP:port, ws://example.com, or wss://example.com
964
+ control_id: Identifier for this tap to use with stop_tap (optional)
965
+ Default is generated and stored in tap_control_id variable
966
+ direction: Direction of audio to tap (default: "both")
967
+ "speak" = what party says
968
+ "hear" = what party hears
969
+ "both" = what party hears and says
970
+ codec: Codec for tap media stream - "PCMU" or "PCMA" (default: "PCMU")
971
+ rtp_ptime: Packetization time in milliseconds for RTP (default: 20)
972
+ status_url: URL for status change requests (optional)
973
+
974
+ Returns:
975
+ self for method chaining
976
+
977
+ Raises:
978
+ ValueError: If direction or codec values are invalid
979
+ """
980
+ # Validate direction parameter
981
+ valid_directions = ["speak", "hear", "both"]
982
+ if direction not in valid_directions:
983
+ raise ValueError(f"direction must be one of {valid_directions}")
984
+
985
+ # Validate codec parameter
986
+ valid_codecs = ["PCMU", "PCMA"]
987
+ if codec not in valid_codecs:
988
+ raise ValueError(f"codec must be one of {valid_codecs}")
989
+
990
+ # Validate rtp_ptime
991
+ if rtp_ptime <= 0:
992
+ raise ValueError("rtp_ptime must be a positive integer")
993
+
994
+ # Build the tap parameters
995
+ tap_params = {"uri": uri}
996
+
997
+ # Add optional parameters if they differ from defaults
998
+ if control_id:
999
+ tap_params["control_id"] = control_id
1000
+ if direction != "both":
1001
+ tap_params["direction"] = direction
1002
+ if codec != "PCMU":
1003
+ tap_params["codec"] = codec
1004
+ if rtp_ptime != 20:
1005
+ tap_params["rtp_ptime"] = rtp_ptime
1006
+ if status_url:
1007
+ tap_params["status_url"] = status_url
1008
+
1009
+ # Generate SWML document
1010
+ swml_doc = {
1011
+ "version": "1.0.0",
1012
+ "sections": {
1013
+ "main": [
1014
+ {"tap": tap_params}
1015
+ ]
1016
+ }
1017
+ }
1018
+
1019
+ # Use execute_swml to add the action
1020
+ return self.execute_swml(swml_doc)
1021
+
1022
+ def stop_tap(self, control_id: Optional[str] = None) -> 'SwaigFunctionResult':
1023
+ """
1024
+ Stop an active tap stream using SWML.
1025
+
1026
+ This is a virtual helper that generates SWML to stop a tap stream
1027
+ that was started with tap().
1028
+
1029
+ Args:
1030
+ control_id: ID of the tap to stop (optional)
1031
+ If not set, the last tap started will be stopped
1032
+
1033
+ Returns:
1034
+ self for method chaining
1035
+ """
1036
+ # Build the stop_tap parameters
1037
+ if control_id:
1038
+ stop_params = {"control_id": control_id}
1039
+ else:
1040
+ # For simple case with no control_id, use empty object
1041
+ stop_params = {}
1042
+
1043
+ # Generate SWML document
1044
+ swml_doc = {
1045
+ "version": "1.0.0",
1046
+ "sections": {
1047
+ "main": [
1048
+ {"stop_tap": stop_params}
1049
+ ]
1050
+ }
1051
+ }
1052
+
1053
+ # Use execute_swml to add the action
1054
+ return self.execute_swml(swml_doc)
1055
+
1056
+ @staticmethod
1057
+ def create_payment_prompt(for_situation: str, actions: List[Dict[str, str]],
1058
+ card_type: Optional[str] = None,
1059
+ error_type: Optional[str] = None) -> Dict[str, Any]:
1060
+ """
1061
+ Create a payment prompt structure for use with pay() method.
1062
+
1063
+ Args:
1064
+ for_situation: Situation to use prompt for (e.g., "payment-card-number")
1065
+ actions: List of actions with 'type' and 'phrase' keys
1066
+ card_type: Space-separated card types for this prompt
1067
+ error_type: Space-separated error types for this prompt
1068
+
1069
+ Returns:
1070
+ Dictionary representing the prompt structure
1071
+ """
1072
+ prompt = {
1073
+ "for": for_situation,
1074
+ "actions": actions
1075
+ }
1076
+
1077
+ if card_type:
1078
+ prompt["card_type"] = card_type
1079
+ if error_type:
1080
+ prompt["error_type"] = error_type
1081
+
1082
+ return prompt
1083
+
1084
+ @staticmethod
1085
+ def create_payment_action(action_type: str, phrase: str) -> Dict[str, str]:
1086
+ """
1087
+ Create a payment action for use in payment prompts.
1088
+
1089
+ Args:
1090
+ action_type: "Say" for text-to-speech or "Play" for audio file
1091
+ phrase: Sentence to say or URL to play
1092
+
1093
+ Returns:
1094
+ Dictionary representing the action
1095
+ """
1096
+ return {
1097
+ "type": action_type,
1098
+ "phrase": phrase
1099
+ }
1100
+
1101
+ @staticmethod
1102
+ def create_payment_parameter(name: str, value: str) -> Dict[str, str]:
1103
+ """
1104
+ Create a payment parameter for use with pay() method.
1105
+
1106
+ Args:
1107
+ name: Parameter name
1108
+ value: Parameter value
1109
+
1110
+ Returns:
1111
+ Dictionary representing the parameter
1112
+ """
1113
+ return {
1114
+ "name": name,
1115
+ "value": value
1116
+ }
1117
+
96
1118
  def to_dict(self) -> Dict[str, Any]:
97
1119
  """
98
1120
  Convert to the JSON structure expected by SWAIG
@@ -101,6 +1123,9 @@ class SwaigFunctionResult:
101
1123
  - 'response': Text to be spoken by the AI
102
1124
  - 'action': Array of action objects
103
1125
 
1126
+ Optional:
1127
+ - 'post_process': Boolean controlling when actions execute
1128
+
104
1129
  Returns:
105
1130
  Dictionary in SWAIG function response format
106
1131
  """
@@ -115,6 +1140,11 @@ class SwaigFunctionResult:
115
1140
  if self.action:
116
1141
  result["action"] = self.action
117
1142
 
1143
+ # Add post_process if enabled and we have actions
1144
+ # (post_process only matters when there are actions to execute)
1145
+ if self.post_process and self.action:
1146
+ result["post_process"] = True
1147
+
118
1148
  # Ensure we have at least one of response or action
119
1149
  if not result:
120
1150
  # Default response if neither is present