File Coverage

blib/lib/SignalWire/Agents/SWAIG/FunctionResult.pm
Criterion Covered Total %
statement 187 272 68.7
branch 46 122 37.7
condition 23 119 19.3
subroutine 46 54 85.1
pod 0 50 0.0
total 302 617 48.9


line stmt bran cond sub pod time code
1             package SignalWire::Agents::SWAIG::FunctionResult;
2 12     12   128448 use strict;
  12         33  
  12         539  
3 12     12   61 use warnings;
  12         49  
  12         763  
4 12     12   414 use Moo;
  12         5606  
  12         94  
5 12     12   6267 use JSON ();
  12         29  
  12         54517  
6              
7             has 'response' => (
8             is => 'rw',
9             default => sub { '' },
10             );
11              
12             has 'action' => (
13             is => 'rw',
14             default => sub { [] },
15             );
16              
17             has 'post_process' => (
18             is => 'rw',
19             default => sub { 0 },
20             );
21              
22             # Constructor: new(response => "text") or new("text") or new("text", post_process => 1)
23             around BUILDARGS => sub {
24             my ($orig, $class, @args) = @_;
25             if (@args == 1 && !ref $args[0]) {
26             return $class->$orig(response => $args[0]);
27             }
28             if (@args >= 1 && !ref $args[0] && $args[0] !~ /^(response|action|post_process)$/) {
29             my $resp = shift @args;
30             return $class->$orig(response => $resp, @args);
31             }
32             return $class->$orig(@args);
33             };
34              
35             # --- Core methods ---
36              
37             sub set_response {
38 1     1 0 11 my ($self, $response) = @_;
39 1         6 $self->response($response);
40 1         3 return $self;
41             }
42              
43             sub set_post_process {
44 3     3 0 2214 my ($self, $post_process) = @_;
45 3 50       18 $self->post_process($post_process ? 1 : 0);
46 3         7 return $self;
47             }
48              
49             sub add_action {
50 44     44 0 4473 my ($self, $name, $data) = @_;
51 44         88 push @{ $self->action }, { $name => $data };
  44         210  
52 44         176 return $self;
53             }
54              
55             sub add_actions {
56 1     1 0 3150 my ($self, $actions) = @_;
57 1         3 push @{ $self->action }, @$actions;
  1         5  
58 1         3 return $self;
59             }
60              
61             # --- Call Control ---
62              
63             sub connect {
64 2     2 0 31 my ($self, $destination, %opts) = @_;
65 2 50       8 my $final = exists $opts{final} ? $opts{final} : 1;
66 2         5 my $from = $opts{from};
67              
68 2         7 my $connect_params = { to => $destination };
69 2 100       8 $connect_params->{from} = $from if defined $from;
70              
71 2 100       16 my $swml_action = {
72             SWML => {
73             sections => {
74             main => [{ connect => $connect_params }],
75             },
76             version => '1.0.0',
77             },
78             transfer => $final ? 'true' : 'false',
79             };
80              
81 2         5 push @{ $self->action }, $swml_action;
  2         9  
82 2         8 return $self;
83             }
84              
85             sub swml_transfer {
86 1     1 0 15 my ($self, $dest, $ai_response, %opts) = @_;
87 1 50       5 my $final = exists $opts{final} ? $opts{final} : 1;
88              
89 1 50       14 my $swml_action = {
90             SWML => {
91             version => '1.0.0',
92             sections => {
93             main => [
94             { set => { ai_response => $ai_response } },
95             { transfer => { dest => $dest } },
96             ],
97             },
98             },
99             transfer => $final ? 'true' : 'false',
100             };
101              
102 1         3 push @{ $self->action }, $swml_action;
  1         7  
103 1         4 return $self;
104             }
105              
106             sub hangup {
107 1     1 0 12 my ($self) = @_;
108 1         8 return $self->add_action('hangup', JSON::true);
109             }
110              
111             sub hold {
112 4     4 0 42 my ($self, $timeout) = @_;
113 4   50     11 $timeout //= 300;
114 4 100       14 $timeout = 0 if $timeout < 0;
115 4 100       12 $timeout = 900 if $timeout > 900;
116 4         11 return $self->add_action('hold', $timeout);
117             }
118              
119             sub wait_for_user {
120 3     3 0 41 my ($self, %opts) = @_;
121 3         9 my $enabled = $opts{enabled};
122 3         7 my $timeout = $opts{timeout};
123 3         7 my $answer_first = $opts{answer_first};
124              
125 3         6 my $value;
126 3 100       15 if ($answer_first) {
    100          
    50          
127 1         3 $value = 'answer_first';
128             } elsif (defined $timeout) {
129 1         3 $value = $timeout;
130             } elsif (defined $enabled) {
131 0 0       0 $value = $enabled ? JSON::true : JSON::false;
132             } else {
133 1         5 $value = JSON::true;
134             }
135 3         12 return $self->add_action('wait_for_user', $value);
136             }
137              
138             sub stop {
139 1     1 0 11 my ($self) = @_;
140 1         6 return $self->add_action('stop', JSON::true);
141             }
142              
143             # --- State & Data ---
144              
145             sub update_global_data {
146 2     2 0 16 my ($self, $data) = @_;
147 2         6 return $self->add_action('set_global_data', $data);
148             }
149              
150             sub remove_global_data {
151 1     1 0 14 my ($self, $keys) = @_;
152 1         18 return $self->add_action('unset_global_data', $keys);
153             }
154              
155             sub set_metadata {
156 1     1 0 16 my ($self, $data) = @_;
157 1         5 return $self->add_action('set_meta_data', $data);
158             }
159              
160             sub remove_metadata {
161 1     1 0 16 my ($self, $keys) = @_;
162 1         5 return $self->add_action('unset_meta_data', $keys);
163             }
164              
165             sub swml_user_event {
166 1     1 0 10 my ($self, $event_data) = @_;
167 1         5 my $swml_action = {
168             sections => {
169             main => [{
170             user_event => { event => $event_data },
171             }],
172             },
173             version => '1.0.0',
174             };
175 1         3 return $self->add_action('SWML', $swml_action);
176             }
177              
178             sub swml_change_step {
179 1     1 0 13 my ($self, $step_name) = @_;
180 1         5 return $self->add_action('change_step', $step_name);
181             }
182              
183             sub swml_change_context {
184 1     1 0 15 my ($self, $context_name) = @_;
185 1         5 return $self->add_action('change_context', $context_name);
186             }
187              
188             sub switch_context {
189 2     2 0 36 my ($self, %opts) = @_;
190 2         7 my $system_prompt = $opts{system_prompt};
191 2         5 my $user_prompt = $opts{user_prompt};
192 2         5 my $consolidate = $opts{consolidate};
193 2         5 my $full_reset = $opts{full_reset};
194              
195 2 50 66     20 if ($system_prompt && !$user_prompt && !$consolidate && !$full_reset) {
      66        
      66        
196 1         5 return $self->add_action('context_switch', $system_prompt);
197             }
198              
199 1         3 my %ctx;
200 1 50       6 $ctx{system_prompt} = $system_prompt if $system_prompt;
201 1 50       4 $ctx{user_prompt} = $user_prompt if $user_prompt;
202 1 50       10 $ctx{consolidate} = JSON::true if $consolidate;
203 1 50       8 $ctx{full_reset} = JSON::true if $full_reset;
204 1         7 return $self->add_action('context_switch', \%ctx);
205             }
206              
207             sub replace_in_history {
208 1     1 0 12 my ($self, $text) = @_;
209 1   33     6 $text //= JSON::true;
210 1         5 return $self->add_action('replace_in_history', $text);
211             }
212              
213             # --- Media ---
214              
215             sub say {
216 2     2 0 19 my ($self, $text) = @_;
217 2         7 return $self->add_action('say', $text);
218             }
219              
220             sub play_background_file {
221 2     2 0 32 my ($self, $filename, %opts) = @_;
222 2         5 my $wait = $opts{wait};
223 2 100       11 if ($wait) {
224 1         7 return $self->add_action('playback_bg', { file => $filename, wait => JSON::true });
225             }
226 1         5 return $self->add_action('playback_bg', $filename);
227             }
228              
229             sub stop_background_file {
230 1     1 0 17 my ($self) = @_;
231 1         11 return $self->add_action('stop_playback_bg', JSON::true);
232             }
233              
234             sub record_call {
235 0     0 0 0 my ($self, %opts) = @_;
236 0         0 my $control_id = $opts{control_id};
237 0   0     0 my $stereo = $opts{stereo} // 0;
238 0   0     0 my $format = $opts{format} // 'wav';
239 0   0     0 my $direction = $opts{direction} // 'both';
240              
241 0 0 0     0 die "format must be 'wav' or 'mp3'" unless $format eq 'wav' || $format eq 'mp3';
242 0 0 0     0 die "direction must be 'speak', 'listen', or 'both'"
      0        
243             unless $direction eq 'speak' || $direction eq 'listen' || $direction eq 'both';
244              
245 0 0       0 my %params = (
246             stereo => $stereo ? JSON::true : JSON::false,
247             format => $format,
248             direction => $direction,
249             );
250 0 0       0 $params{control_id} = $control_id if $control_id;
251              
252 0         0 my $swml_doc = {
253             version => '1.0.0',
254             sections => { main => [{ record_call => \%params }] },
255             };
256 0         0 return $self->execute_swml($swml_doc);
257             }
258              
259             sub stop_record_call {
260 0     0 0 0 my ($self, %opts) = @_;
261 0         0 my $control_id = $opts{control_id};
262 0         0 my %params;
263 0 0       0 $params{control_id} = $control_id if $control_id;
264              
265 0         0 my $swml_doc = {
266             version => '1.0.0',
267             sections => { main => [{ stop_record_call => \%params }] },
268             };
269 0         0 return $self->execute_swml($swml_doc);
270             }
271              
272             # --- Speech & AI ---
273              
274             sub add_dynamic_hints {
275 1     1 0 35 my ($self, $hints) = @_;
276 1         5 return $self->add_action('add_dynamic_hints', $hints);
277             }
278              
279             sub clear_dynamic_hints {
280 1     1 0 15 my ($self) = @_;
281 1         3 push @{ $self->action }, { clear_dynamic_hints => {} };
  1         9  
282 1         3 return $self;
283             }
284              
285             sub set_end_of_speech_timeout {
286 1     1 0 15 my ($self, $ms) = @_;
287 1         5 return $self->add_action('end_of_speech_timeout', $ms);
288             }
289              
290             sub set_speech_event_timeout {
291 1     1 0 17 my ($self, $ms) = @_;
292 1         5 return $self->add_action('speech_event_timeout', $ms);
293             }
294              
295             sub toggle_functions {
296 1     1 0 29 my ($self, $toggles) = @_;
297 1         5 return $self->add_action('toggle_functions', $toggles);
298             }
299              
300             sub enable_functions_on_timeout {
301 1     1 0 17 my ($self, $enabled) = @_;
302 1   50     5 $enabled //= 1;
303 1 50       8 return $self->add_action('functions_on_speaker_timeout', $enabled ? JSON::true : JSON::false);
304             }
305              
306             sub enable_extensive_data {
307 1     1 0 50 my ($self, $enabled) = @_;
308 1   50     6 $enabled //= 1;
309 1 50       7 return $self->add_action('extensive_data', $enabled ? JSON::true : JSON::false);
310             }
311              
312             sub update_settings {
313 1     1 0 18 my ($self, $settings) = @_;
314 1         4 return $self->add_action('settings', $settings);
315             }
316              
317             # --- Advanced ---
318              
319             sub execute_swml {
320 9     9 0 69 my ($self, $swml_content, %opts) = @_;
321 9   100     38 my $transfer = $opts{transfer} // 0;
322              
323 9         19 my $swml_data;
324 9 100       29 if (ref $swml_content eq 'HASH') {
    50          
325             # Deep-copy to avoid mutating caller's data
326 8         106 $swml_data = JSON::decode_json(JSON::encode_json($swml_content));
327             } elsif (!ref $swml_content) {
328             # String - try parsing as JSON
329 1         3 eval {
330 1         9 $swml_data = JSON::decode_json($swml_content);
331             };
332 1 50       5 if ($@) {
333 0         0 $swml_data = { raw_swml => $swml_content };
334             }
335             } else {
336 0         0 die "swml_content must be a string or hashref";
337             }
338              
339 9 100       27 if ($transfer) {
340 1         4 $swml_data->{transfer} = 'true';
341             }
342              
343 9         25 return $self->add_action('SWML', $swml_data);
344             }
345              
346             sub join_conference {
347 0     0 0 0 my ($self, $name, %opts) = @_;
348 0 0 0     0 die "name cannot be empty" unless defined $name && length $name;
349              
350 0   0     0 my $muted = $opts{muted} // 0;
351 0   0     0 my $beep = $opts{beep} // 'true';
352              
353             # Simple form: no options set
354 0 0 0     0 if (!$muted && $beep eq 'true' && !keys %opts) {
      0        
355 0         0 my $swml_doc = {
356             version => '1.0.0',
357             sections => { main => [{ join_conference => $name }] },
358             };
359 0         0 return $self->execute_swml($swml_doc);
360             }
361              
362 0         0 my %params = (name => $name);
363 0 0       0 $params{muted} = JSON::true if $muted;
364 0 0       0 $params{beep} = $beep if $beep ne 'true';
365              
366 0         0 my $swml_doc = {
367             version => '1.0.0',
368             sections => { main => [{ join_conference => \%params }] },
369             };
370 0         0 return $self->execute_swml($swml_doc);
371             }
372              
373             sub join_room {
374 1     1 0 13 my ($self, $name) = @_;
375 1         9 my $swml_doc = {
376             version => '1.0.0',
377             sections => { main => [{ join_room => { name => $name } }] },
378             };
379 1         4 return $self->execute_swml($swml_doc);
380             }
381              
382             sub sip_refer {
383 1     1 0 12 my ($self, $to_uri) = @_;
384 1         8 my $swml_doc = {
385             version => '1.0.0',
386             sections => { main => [{ sip_refer => { to_uri => $to_uri } }] },
387             };
388 1         5 return $self->execute_swml($swml_doc);
389             }
390              
391             sub tap {
392 0     0 0 0 my ($self, $uri, %opts) = @_;
393 0         0 my $control_id = $opts{control_id};
394 0   0     0 my $direction = $opts{direction} // 'both';
395 0   0     0 my $codec = $opts{codec} // 'PCMU';
396              
397 0 0 0     0 die "direction must be 'speak', 'hear', or 'both'"
      0        
398             unless $direction eq 'speak' || $direction eq 'hear' || $direction eq 'both';
399 0 0 0     0 die "codec must be 'PCMU' or 'PCMA'"
400             unless $codec eq 'PCMU' || $codec eq 'PCMA';
401              
402 0         0 my %params = (uri => $uri);
403 0 0       0 $params{control_id} = $control_id if $control_id;
404 0 0       0 $params{direction} = $direction if $direction ne 'both';
405 0 0       0 $params{codec} = $codec if $codec ne 'PCMU';
406              
407 0         0 my $swml_doc = {
408             version => '1.0.0',
409             sections => { main => [{ tap => \%params }] },
410             };
411 0         0 return $self->execute_swml($swml_doc);
412             }
413              
414             sub stop_tap {
415 0     0 0 0 my ($self, %opts) = @_;
416 0         0 my $control_id = $opts{control_id};
417 0         0 my %params;
418 0 0       0 $params{control_id} = $control_id if $control_id;
419              
420 0         0 my $swml_doc = {
421             version => '1.0.0',
422             sections => { main => [{ stop_tap => \%params }] },
423             };
424 0         0 return $self->execute_swml($swml_doc);
425             }
426              
427             sub send_sms {
428 0     0 0 0 my ($self, %opts) = @_;
429 0   0     0 my $to_number = $opts{to_number} // die "to_number is required";
430 0   0     0 my $from_number = $opts{from_number} // die "from_number is required";
431 0         0 my $body = $opts{body};
432 0         0 my $media = $opts{media};
433 0         0 my $tags = $opts{tags};
434 0         0 my $region = $opts{region};
435              
436 0 0 0     0 die "Either body or media must be provided" unless $body || $media;
437              
438 0         0 my %sms_params = (
439             to_number => $to_number,
440             from_number => $from_number,
441             );
442 0 0       0 $sms_params{body} = $body if $body;
443 0 0       0 $sms_params{media} = $media if $media;
444 0 0       0 $sms_params{tags} = $tags if $tags;
445 0 0       0 $sms_params{region} = $region if $region;
446              
447 0         0 my $swml_doc = {
448             version => '1.0.0',
449             sections => { main => [{ send_sms => \%sms_params }] },
450             };
451 0         0 return $self->execute_swml($swml_doc);
452             }
453              
454             sub pay {
455 0     0 0 0 my ($self, %opts) = @_;
456 0   0     0 my $connector_url = $opts{payment_connector_url} // die "payment_connector_url required";
457 0   0     0 my $input_method = $opts{input_method} // 'dtmf';
458 0   0     0 my $timeout = $opts{timeout} // 5;
459 0   0     0 my $max_attempts = $opts{max_attempts} // 1;
460 0   0     0 my $ai_response = $opts{ai_response} // 'The payment status is ${pay_result}, do not mention anything else about collecting payment if successful.';
461              
462             my %pay_params = (
463             payment_connector_url => $connector_url,
464             input => $input_method,
465             payment_method => $opts{payment_method} // 'credit-card',
466             timeout => "$timeout",
467             max_attempts => "$max_attempts",
468             security_code => (($opts{security_code} // 1) ? 'true' : 'false'),
469             token_type => $opts{token_type} // 'reusable',
470             currency => $opts{currency} // 'usd',
471             language => $opts{language} // 'en-US',
472             voice => $opts{voice} // 'woman',
473 0 0 0     0 valid_card_types => $opts{valid_card_types} // 'visa mastercard amex',
      0        
      0        
      0        
      0        
      0        
      0        
474             );
475              
476 0   0     0 my $postal = $opts{postal_code} // 1;
477 0 0 0     0 if (ref $postal || $postal =~ /^[01]$/) {
478 0 0       0 $pay_params{postal_code} = $postal ? 'true' : 'false';
479             } else {
480 0         0 $pay_params{postal_code} = $postal;
481             }
482              
483 0 0       0 $pay_params{status_url} = $opts{status_url} if $opts{status_url};
484 0 0       0 $pay_params{charge_amount} = $opts{charge_amount} if $opts{charge_amount};
485 0 0       0 $pay_params{description} = $opts{description} if $opts{description};
486 0 0       0 $pay_params{parameters} = $opts{parameters} if $opts{parameters};
487 0 0       0 $pay_params{prompts} = $opts{prompts} if $opts{prompts};
488              
489 0         0 my $swml_doc = {
490             version => '1.0.0',
491             sections => {
492             main => [
493             { set => { ai_response => $ai_response } },
494             { pay => \%pay_params },
495             ],
496             },
497             };
498 0         0 return $self->execute_swml($swml_doc);
499             }
500              
501             # --- RPC ---
502              
503             sub execute_rpc {
504 4     4 0 26 my ($self, %opts) = @_;
505 4   50     12 my $method = $opts{method} // die "method is required";
506 4         6 my $params = $opts{params};
507 4         7 my $call_id = $opts{call_id};
508 4         8 my $node_id = $opts{node_id};
509              
510 4         6 my %rpc_params = (method => $method);
511 4 100       10 $rpc_params{call_id} = $call_id if $call_id;
512 4 50       9 $rpc_params{node_id} = $node_id if $node_id;
513 4 50       10 $rpc_params{params} = $params if $params;
514              
515 4         16 my $swml_doc = {
516             version => '1.0.0',
517             sections => { main => [{ execute_rpc => \%rpc_params }] },
518             };
519 4         13 return $self->execute_swml($swml_doc);
520             }
521              
522             sub rpc_dial {
523 1     1 0 14 my ($self, %opts) = @_;
524 1   50     15 my $to_number = $opts{to_number} // die "to_number is required";
525 1   50     3 my $from_number = $opts{from_number} // die "from_number is required";
526 1   50     3 my $dest_swml = $opts{dest_swml} // die "dest_swml is required";
527 1   50     6 my $device_type = $opts{device_type} // 'phone';
528              
529 1         6 return $self->execute_rpc(
530             method => 'dial',
531             params => {
532             devices => {
533             type => $device_type,
534             params => {
535             to_number => $to_number,
536             from_number => $from_number,
537             },
538             },
539             dest_swml => $dest_swml,
540             },
541             );
542             }
543              
544             sub rpc_ai_message {
545 1     1 0 13 my ($self, %opts) = @_;
546 1   50     4 my $call_id = $opts{call_id} // die "call_id is required";
547 1   50     3 my $message_text = $opts{message_text} // die "message_text is required";
548 1   50     5 my $role = $opts{role} // 'system';
549              
550 1         5 return $self->execute_rpc(
551             method => 'ai_message',
552             call_id => $call_id,
553             params => {
554             role => $role,
555             message_text => $message_text,
556             },
557             );
558             }
559              
560             sub rpc_ai_unhold {
561 1     1 0 12 my ($self, %opts) = @_;
562 1   50     4 my $call_id = $opts{call_id} // die "call_id is required";
563              
564 1         4 return $self->execute_rpc(
565             method => 'ai_unhold',
566             call_id => $call_id,
567             params => {},
568             );
569             }
570              
571             sub simulate_user_input {
572 1     1 0 13 my ($self, $text) = @_;
573 1         5 return $self->add_action('user_input', $text);
574             }
575              
576             # --- Payment helpers (class methods) ---
577              
578             sub create_payment_prompt {
579 1     1 0 1226 my ($class_or_self, %opts) = @_;
580 1   50     4 my $for_situation = $opts{for_situation} // die "for_situation is required";
581 1   50     3 my $actions = $opts{actions} // die "actions is required";
582 1         2 my $card_type = $opts{card_type};
583 1         2 my $error_type = $opts{error_type};
584              
585 1         2 my %prompt = (
586             for => $for_situation,
587             actions => $actions,
588             );
589 1 50       3 $prompt{card_type} = $card_type if $card_type;
590 1 50       7 $prompt{error_type} = $error_type if $error_type;
591              
592 1         28 return \%prompt;
593             }
594              
595             sub create_payment_action {
596 1     1 0 3853 my ($class_or_self, $action_type, $phrase) = @_;
597 1         4 return { type => $action_type, phrase => $phrase };
598             }
599              
600             sub create_payment_parameter {
601 1     1 0 1243 my ($class_or_self, $name, $value) = @_;
602 1         4 return { name => $name, value => $value };
603             }
604              
605             # --- Serialization ---
606              
607             sub to_hash {
608 57     57 0 446 my ($self) = @_;
609 57         117 my %result;
610              
611 57 100       295 $result{response} = $self->response if length $self->response;
612              
613 57 100       118 if (@{ $self->action }) {
  57         198  
614 45         104 $result{action} = $self->action;
615 45 100       137 $result{post_process} = JSON::true if $self->post_process;
616             }
617              
618             # Ensure at least one of response or action
619 57 100       174 if (!keys %result) {
620 1         12 $result{response} = 'Action completed.';
621             }
622              
623 57         868 return \%result;
624             }
625              
626             sub to_json {
627 0     0 0   my ($self) = @_;
628 0           return JSON::encode_json($self->to_hash);
629             }
630              
631             1;