File Coverage

blib/lib/SignalWire/Agents/Contexts.pm
Criterion Covered Total %
statement 339 384 88.2
branch 136 194 70.1
condition 8 15 53.3
subroutine 58 65 89.2
pod 0 49 0.0
total 541 707 76.5


line stmt bran cond sub pod time code
1             package SignalWire::Agents::Contexts;
2 1     1   86090 use strict;
  1         2  
  1         27  
3 1     1   4 use warnings;
  1         1  
  1         41  
4 1     1   3 use JSON ();
  1         3  
  1         36  
5              
6             our $MAX_CONTEXTS = 50;
7             our $MAX_STEPS_PER_CONTEXT = 100;
8              
9             # ==========================================================================
10             # GatherQuestion
11             # ==========================================================================
12             package SignalWire::Agents::Contexts::GatherQuestion;
13 1     1   369 use Moo;
  1         5501  
  1         3  
14 1     1   1130 use JSON ();
  1         2  
  1         161  
15              
16             has 'key' => (is => 'ro', required => 1);
17             has 'question' => (is => 'ro', required => 1);
18             has 'type' => (is => 'ro', default => sub { 'string' });
19             has 'confirm' => (is => 'ro', default => sub { 0 });
20             has 'prompt' => (is => 'ro', default => sub { undef });
21             has 'functions' => (is => 'ro', default => sub { undef });
22              
23             sub to_hash {
24 2     2 0 4 my ($self) = @_;
25 2         11 my %d = (key => $self->key, question => $self->question);
26 2 50       9 $d{type} = $self->type if $self->type ne 'string';
27 2 100       12 $d{confirm} = JSON::true if $self->confirm;
28 2 50       11 $d{prompt} = $self->prompt if defined $self->prompt;
29 2 50       6 $d{functions} = $self->functions if defined $self->functions;
30 2         10 return \%d;
31             }
32              
33             # ==========================================================================
34             # GatherInfo
35             # ==========================================================================
36             package SignalWire::Agents::Contexts::GatherInfo;
37 1     1   4 use Moo;
  1         2  
  1         3  
38              
39             has '_questions' => (is => 'rw', default => sub { [] });
40             has '_output_key' => (is => 'rw', default => sub { undef });
41             has '_completion_action' => (is => 'rw', default => sub { undef });
42             has '_prompt' => (is => 'rw', default => sub { undef });
43              
44             sub add_question {
45 5     5 0 15 my ($self, %opts) = @_;
46             my $q = SignalWire::Agents::Contexts::GatherQuestion->new(
47             key => $opts{key},
48             question => $opts{question},
49             type => $opts{type} // 'string',
50             confirm => $opts{confirm} // 0,
51             prompt => $opts{prompt},
52             functions => $opts{functions},
53 5   100     138 );
      100        
54 5         3111 push @{ $self->_questions }, $q;
  5         18  
55 5         12 return $self;
56             }
57              
58             sub to_hash {
59 1     1 0 3 my ($self) = @_;
60 1 50       2 die "gather_info must have at least one question" unless @{ $self->_questions };
  1         8  
61 1         4 my %d = (questions => [ map { $_->to_hash } @{ $self->_questions } ]);
  2         8  
  1         3  
62 1 50       7 $d{prompt} = $self->_prompt if defined $self->_prompt;
63 1 50       7 $d{output_key} = $self->_output_key if defined $self->_output_key;
64 1 50       7 $d{completion_action} = $self->_completion_action if defined $self->_completion_action;
65 1         3 return \%d;
66             }
67              
68             # ==========================================================================
69             # Step
70             # ==========================================================================
71             package SignalWire::Agents::Contexts::Step;
72 1     1   2013 use Moo;
  1         1  
  1         3  
73 1     1   256 use JSON ();
  1         2  
  1         972  
74              
75             has 'name' => (is => 'ro', required => 1);
76              
77             has '_text' => (is => 'rw', default => sub { undef });
78             has '_step_criteria' => (is => 'rw', default => sub { undef });
79             has '_functions' => (is => 'rw', default => sub { undef });
80             has '_valid_steps' => (is => 'rw', default => sub { undef });
81             has '_valid_contexts' => (is => 'rw', default => sub { undef });
82             has '_sections' => (is => 'rw', default => sub { [] });
83             has '_gather_info' => (is => 'rw', default => sub { undef });
84             has '_end' => (is => 'rw', default => sub { 0 });
85             has '_skip_user_turn' => (is => 'rw', default => sub { 0 });
86             has '_skip_to_next_step' => (is => 'rw', default => sub { 0 });
87             has '_reset_system_prompt' => (is => 'rw', default => sub { undef });
88             has '_reset_user_prompt' => (is => 'rw', default => sub { undef });
89             has '_reset_consolidate' => (is => 'rw', default => sub { 0 });
90             has '_reset_full_reset' => (is => 'rw', default => sub { 0 });
91              
92             sub set_text {
93 35     35 0 907 my ($self, $text) = @_;
94             die "Cannot use set_text() when POM sections have been added"
95 35 100       50 if @{ $self->_sections };
  35         118  
96 34         129 $self->_text($text);
97 34         98 return $self;
98             }
99              
100             sub add_section {
101 5     5 0 39 my ($self, $title, $body) = @_;
102 5 100       25 die "Cannot add POM sections when set_text() has been used"
103             if defined $self->_text;
104 4         8 push @{ $self->_sections }, { title => $title, body => $body };
  4         22  
105 4         26 return $self;
106             }
107              
108             sub add_bullets {
109 2     2 0 9 my ($self, $title, $bullets) = @_;
110 2 50       6 die "Cannot add POM sections when set_text() has been used"
111             if defined $self->_text;
112 2         4 push @{ $self->_sections }, { title => $title, bullets => $bullets };
  2         7  
113 2         5 return $self;
114             }
115              
116             sub set_step_criteria {
117 2     2 0 5 my ($self, $criteria) = @_;
118 2         6 $self->_step_criteria($criteria);
119 2         6 return $self;
120             }
121              
122             sub set_functions {
123 3     3 0 8 my ($self, $functions) = @_;
124 3         9 $self->_functions($functions);
125 3         8 return $self;
126             }
127              
128             sub set_valid_steps {
129 4     4 0 9 my ($self, $steps) = @_;
130 4         14 $self->_valid_steps($steps);
131 4         10 return $self;
132             }
133              
134             sub set_valid_contexts {
135 1     1 0 2 my ($self, $contexts) = @_;
136 1         3 $self->_valid_contexts($contexts);
137 1         1 return $self;
138             }
139              
140             sub set_end {
141 1     1 0 3 my ($self, $end) = @_;
142 1 50       8 $self->_end($end ? 1 : 0);
143 1         5 return $self;
144             }
145              
146             sub set_skip_user_turn {
147 1     1 0 4 my ($self, $skip) = @_;
148 1 50       5 $self->_skip_user_turn($skip ? 1 : 0);
149 1         5 return $self;
150             }
151              
152             sub set_skip_to_next_step {
153 1     1 0 4 my ($self, $skip) = @_;
154 1 50       7 $self->_skip_to_next_step($skip ? 1 : 0);
155 1         2 return $self;
156             }
157              
158             sub set_gather_info {
159 4     4 0 20 my ($self, %opts) = @_;
160             $self->_gather_info(SignalWire::Agents::Contexts::GatherInfo->new(
161             _output_key => $opts{output_key},
162             _completion_action => $opts{completion_action},
163             _prompt => $opts{prompt},
164 4         80 ));
165 4         46 return $self;
166             }
167              
168             sub add_gather_question {
169 6     6 0 40 my ($self, %opts) = @_;
170 6 100       37 die "Must call set_gather_info() before add_gather_question()"
171             unless defined $self->_gather_info;
172 5         23 $self->_gather_info->add_question(%opts);
173 5         19 return $self;
174             }
175              
176             sub clear_sections {
177 1     1 0 7 my ($self) = @_;
178 1         7 $self->_sections([]);
179 1         4 $self->_text(undef);
180 1         3 return $self;
181             }
182              
183             sub set_reset_system_prompt {
184 1     1 0 3 my ($self, $sp) = @_;
185 1         3 $self->_reset_system_prompt($sp);
186 1         19 return $self;
187             }
188              
189             sub set_reset_user_prompt {
190 1     1 0 2 my ($self, $up) = @_;
191 1         3 $self->_reset_user_prompt($up);
192 1         3 return $self;
193             }
194              
195             sub set_reset_consolidate {
196 1     1 0 2 my ($self, $c) = @_;
197 1 50       4 $self->_reset_consolidate($c ? 1 : 0);
198 1         2 return $self;
199             }
200              
201             sub set_reset_full_reset {
202 1     1 0 2 my ($self, $fr) = @_;
203 1 50       4 $self->_reset_full_reset($fr ? 1 : 0);
204 1         1 return $self;
205             }
206              
207             sub _render_text {
208 26     26   48 my ($self) = @_;
209 26 100       163 return $self->_text if defined $self->_text;
210              
211             die "Step '" . $self->name . "' has no text or POM sections defined"
212 2 50       3 unless @{ $self->_sections };
  2         7  
213              
214 2         30 my @parts;
215 2         4 for my $sec (@{ $self->_sections }) {
  2         7  
216 4 100       9 if (exists $sec->{bullets}) {
217 2         5 push @parts, "## $sec->{title}";
218 2         4 push @parts, map { "- $_" } @{ $sec->{bullets} };
  4         11  
  2         6  
219             } else {
220 2         7 push @parts, "## $sec->{title}";
221 2         5 push @parts, $sec->{body};
222             }
223 4         9 push @parts, '';
224             }
225 2         9 my $text = join("\n", @parts);
226 2         19 $text =~ s/\s+$//;
227 2         10 return $text;
228             }
229              
230             sub to_hash {
231 26     26 0 63 my ($self) = @_;
232 26         103 my %d = (
233             name => $self->name,
234             text => $self->_render_text,
235             );
236              
237 26 100       88 $d{step_criteria} = $self->_step_criteria if defined $self->_step_criteria;
238 26 100       102 $d{functions} = $self->_functions if defined $self->_functions;
239 26 100       82 $d{valid_steps} = $self->_valid_steps if defined $self->_valid_steps;
240 26 100       66 $d{valid_contexts} = $self->_valid_contexts if defined $self->_valid_contexts;
241 26 100       93 $d{end} = JSON::true if $self->_end;
242 26 100       67 $d{skip_user_turn} = JSON::true if $self->_skip_user_turn;
243 26 100       88 $d{skip_to_next_step} = JSON::true if $self->_skip_to_next_step;
244              
245 26         48 my %reset;
246 26 100       64 $reset{system_prompt} = $self->_reset_system_prompt if defined $self->_reset_system_prompt;
247 26 100       69 $reset{user_prompt} = $self->_reset_user_prompt if defined $self->_reset_user_prompt;
248 26 100       66 $reset{consolidate} = JSON::true if $self->_reset_consolidate;
249 26 100       74 $reset{full_reset} = JSON::true if $self->_reset_full_reset;
250 26 100       67 $d{reset} = \%reset if keys %reset;
251              
252 26 100       76 $d{gather_info} = $self->_gather_info->to_hash if defined $self->_gather_info;
253              
254 26         128 return \%d;
255             }
256              
257             # ==========================================================================
258             # Context
259             # ==========================================================================
260             package SignalWire::Agents::Contexts::Context;
261 1     1   7 use Moo;
  1         4  
  1         4  
262 1     1   265 use JSON ();
  1         1  
  1         1418  
263              
264             has 'name' => (is => 'ro', required => 1);
265              
266             has '_steps' => (is => 'rw', default => sub { {} });
267             has '_step_order' => (is => 'rw', default => sub { [] });
268             has '_valid_contexts' => (is => 'rw', default => sub { undef });
269             has '_valid_steps' => (is => 'rw', default => sub { undef });
270             has '_post_prompt' => (is => 'rw', default => sub { undef });
271             has '_system_prompt' => (is => 'rw', default => sub { undef });
272             has '_system_prompt_sections' => (is => 'rw', default => sub { [] });
273             has '_consolidate' => (is => 'rw', default => sub { 0 });
274             has '_full_reset' => (is => 'rw', default => sub { 0 });
275             has '_user_prompt' => (is => 'rw', default => sub { undef });
276             has '_isolated' => (is => 'rw', default => sub { 0 });
277             has '_prompt_text' => (is => 'rw', default => sub { undef });
278             has '_prompt_sections' => (is => 'rw', default => sub { [] });
279             has '_enter_fillers' => (is => 'rw', default => sub { undef });
280             has '_exit_fillers' => (is => 'rw', default => sub { undef });
281              
282             sub add_step {
283 28     28 0 1132 my ($self, $name, %opts) = @_;
284             die "Step '$name' already exists in context '" . $self->name . "'"
285 28 100       109 if exists $self->_steps->{$name};
286             die "Maximum steps per context ($SignalWire::Agents::Contexts::MAX_STEPS_PER_CONTEXT) exceeded"
287 27 50       41 if keys %{ $self->_steps } >= $SignalWire::Agents::Contexts::MAX_STEPS_PER_CONTEXT;
  27         91  
288              
289 27         596 my $step = SignalWire::Agents::Contexts::Step->new(name => $name);
290 27         231 $self->_steps->{$name} = $step;
291 27         46 push @{ $self->_step_order }, $name;
  27         81  
292              
293 27 100       97 $step->add_section('Task', $opts{task}) if defined $opts{task};
294 27 100       70 $step->add_bullets('Process', $opts{bullets}) if defined $opts{bullets};
295 27 100       96 $step->set_step_criteria($opts{criteria}) if defined $opts{criteria};
296 27 100       72 $step->set_functions($opts{functions}) if defined $opts{functions};
297 27 100       66 $step->set_valid_steps($opts{valid_steps}) if defined $opts{valid_steps};
298              
299 27         140 return $step;
300             }
301              
302             sub get_step {
303 3     3 0 1614 my ($self, $name) = @_;
304 3         19 return $self->_steps->{$name};
305             }
306              
307             sub remove_step {
308 1     1 0 4 my ($self, $name) = @_;
309 1 50       8 if (exists $self->_steps->{$name}) {
310 1         4 delete $self->_steps->{$name};
311 1         4 $self->_step_order([ grep { $_ ne $name } @{ $self->_step_order } ]);
  2         10  
  1         4  
312             }
313 1         4 return $self;
314             }
315              
316             sub move_step {
317 1     1 0 5 my ($self, $name, $position) = @_;
318             die "Step '$name' not found in context '" . $self->name . "'"
319 1 50       7 unless exists $self->_steps->{$name};
320 1         3 my @order = grep { $_ ne $name } @{ $self->_step_order };
  3         9  
  1         4  
321 1         3 splice @order, $position, 0, $name;
322 1         5 $self->_step_order(\@order);
323 1         3 return $self;
324             }
325              
326             sub set_valid_contexts {
327 4     4 0 11 my ($self, $contexts) = @_;
328 4         11 $self->_valid_contexts($contexts);
329 4         8 return $self;
330             }
331              
332             sub set_valid_steps {
333 1     1 0 2 my ($self, $steps) = @_;
334 1         3 $self->_valid_steps($steps);
335 1         1 return $self;
336             }
337              
338             sub set_post_prompt {
339 1     1 0 3 my ($self, $pp) = @_;
340 1         2 $self->_post_prompt($pp);
341 1         2 return $self;
342             }
343              
344             sub set_system_prompt {
345 1     1 0 3 my ($self, $sp) = @_;
346             die "Cannot use set_system_prompt() when POM sections have been added for system prompt"
347 1 50       2 if @{ $self->_system_prompt_sections };
  1         7  
348 1         62 $self->_system_prompt($sp);
349 1         4 return $self;
350             }
351              
352             sub set_consolidate {
353 1     1 0 4 my ($self, $c) = @_;
354 1 50       8 $self->_consolidate($c ? 1 : 0);
355 1         2 return $self;
356             }
357              
358             sub set_full_reset {
359 1     1 0 3 my ($self, $fr) = @_;
360 1 50       8 $self->_full_reset($fr ? 1 : 0);
361 1         2 return $self;
362             }
363              
364             sub set_user_prompt {
365 1     1 0 3 my ($self, $up) = @_;
366 1         4 $self->_user_prompt($up);
367 1         2 return $self;
368             }
369              
370             sub set_isolated {
371 1     1 0 3 my ($self, $iso) = @_;
372 1 50       6 $self->_isolated($iso ? 1 : 0);
373 1         2 return $self;
374             }
375              
376             sub add_system_section {
377 0     0 0 0 my ($self, $title, $body) = @_;
378 0 0       0 die "Cannot add POM sections for system prompt when set_system_prompt() has been used"
379             if defined $self->_system_prompt;
380 0         0 push @{ $self->_system_prompt_sections }, { title => $title, body => $body };
  0         0  
381 0         0 return $self;
382             }
383              
384             sub add_system_bullets {
385 0     0 0 0 my ($self, $title, $bullets) = @_;
386 0 0       0 die "Cannot add POM sections for system prompt when set_system_prompt() has been used"
387             if defined $self->_system_prompt;
388 0         0 push @{ $self->_system_prompt_sections }, { title => $title, bullets => $bullets };
  0         0  
389 0         0 return $self;
390             }
391              
392             sub set_prompt {
393 1     1 0 4 my ($self, $prompt) = @_;
394             die "Cannot use set_prompt() when POM sections have been added"
395 1 50       1 if @{ $self->_prompt_sections };
  1         7  
396 1         5 $self->_prompt_text($prompt);
397 1         2 return $self;
398             }
399              
400             sub add_section {
401 1     1 0 2 my ($self, $title, $body) = @_;
402 1 50       5 die "Cannot add POM sections when set_prompt() has been used"
403             if defined $self->_prompt_text;
404 1         2 push @{ $self->_prompt_sections }, { title => $title, body => $body };
  1         4  
405 1         2 return $self;
406             }
407              
408             sub add_bullets {
409 1     1 0 2 my ($self, $title, $bullets) = @_;
410 1 50       5 die "Cannot add POM sections when set_prompt() has been used"
411             if defined $self->_prompt_text;
412 1         2 push @{ $self->_prompt_sections }, { title => $title, bullets => $bullets };
  1         3  
413 1         2 return $self;
414             }
415              
416             sub set_enter_fillers {
417 1     1 0 4 my ($self, $fillers) = @_;
418 1 50       9 $self->_enter_fillers($fillers) if ref $fillers eq 'HASH';
419 1         3 return $self;
420             }
421              
422             sub set_exit_fillers {
423 0     0 0 0 my ($self, $fillers) = @_;
424 0 0       0 $self->_exit_fillers($fillers) if ref $fillers eq 'HASH';
425 0         0 return $self;
426             }
427              
428             sub add_enter_filler {
429 0     0 0 0 my ($self, $lang, $fillers) = @_;
430 0 0 0     0 if ($lang && ref $fillers eq 'ARRAY') {
431 0 0       0 $self->_enter_fillers({}) unless defined $self->_enter_fillers;
432 0         0 $self->_enter_fillers->{$lang} = $fillers;
433             }
434 0         0 return $self;
435             }
436              
437             sub add_exit_filler {
438 1     1 0 4 my ($self, $lang, $fillers) = @_;
439 1 50 33     9 if ($lang && ref $fillers eq 'ARRAY') {
440 1 50       35 $self->_exit_fillers({}) unless defined $self->_exit_fillers;
441 1         5 $self->_exit_fillers->{$lang} = $fillers;
442             }
443 1         4 return $self;
444             }
445              
446             sub _render_prompt {
447 0     0   0 my ($self) = @_;
448 0 0       0 return $self->_prompt_text if defined $self->_prompt_text;
449 0 0       0 return undef unless @{ $self->_prompt_sections };
  0         0  
450 0         0 return _render_sections($self->_prompt_sections);
451             }
452              
453             sub _render_system_prompt {
454 12     12   25 my ($self) = @_;
455 12 100       34 return $self->_system_prompt if defined $self->_system_prompt;
456 11 50       20 return undef unless @{ $self->_system_prompt_sections };
  11         45  
457 0         0 return _render_sections($self->_system_prompt_sections);
458             }
459              
460             sub _render_sections {
461 0     0   0 my ($sections) = @_;
462 0         0 my @parts;
463 0         0 for my $sec (@$sections) {
464 0 0       0 if (exists $sec->{bullets}) {
465 0         0 push @parts, "## $sec->{title}";
466 0         0 push @parts, map { "- $_" } @{ $sec->{bullets} };
  0         0  
  0         0  
467             } else {
468 0         0 push @parts, "## $sec->{title}";
469 0         0 push @parts, $sec->{body};
470             }
471 0         0 push @parts, '';
472             }
473 0         0 my $text = join("\n", @parts);
474 0         0 $text =~ s/\s+$//;
475 0         0 return $text;
476             }
477              
478             sub to_hash {
479 12     12 0 36 my ($self) = @_;
480             die "Context '" . $self->name . "' has no steps defined"
481 12 50       21 unless keys %{ $self->_steps };
  12         45  
482              
483             my %d = (
484 12         24 steps => [ map { $self->_steps->{$_}->to_hash } @{ $self->_step_order } ],
  17         92  
  12         36  
485             );
486              
487 12 100       50 $d{valid_contexts} = $self->_valid_contexts if defined $self->_valid_contexts;
488 12 100       39 $d{valid_steps} = $self->_valid_steps if defined $self->_valid_steps;
489 12 100       38 $d{post_prompt} = $self->_post_prompt if defined $self->_post_prompt;
490              
491 12         35 my $sp = $self->_render_system_prompt;
492 12 100       29 $d{system_prompt} = $sp if defined $sp;
493              
494 12 100       38 $d{consolidate} = JSON::true if $self->_consolidate;
495 12 100       43 $d{full_reset} = JSON::true if $self->_full_reset;
496 12 100       40 $d{user_prompt} = $self->_user_prompt if defined $self->_user_prompt;
497 12 100       37 $d{isolated} = JSON::true if $self->_isolated;
498              
499 12 100       38 if (@{ $self->_prompt_sections }) {
  12 100       99  
500 1         5 $d{pom} = $self->_prompt_sections;
501             } elsif (defined $self->_prompt_text) {
502 1         7 $d{prompt} = $self->_prompt_text;
503             }
504              
505 12 100       59 $d{enter_fillers} = $self->_enter_fillers if defined $self->_enter_fillers;
506 12 100       40 $d{exit_fillers} = $self->_exit_fillers if defined $self->_exit_fillers;
507              
508 12         76 return \%d;
509             }
510              
511             # ==========================================================================
512             # ContextBuilder
513             # ==========================================================================
514             package SignalWire::Agents::Contexts::ContextBuilder;
515 1     1   7 use Moo;
  1         1  
  1         4  
516 1     1   268 use JSON ();
  1         1  
  1         835  
517              
518             has '_contexts' => (is => 'rw', default => sub { {} });
519             has '_context_order' => (is => 'rw', default => sub { [] });
520              
521             sub add_context {
522 14     14 0 77 my ($self, $name) = @_;
523 14 100       68 die "Context '$name' already exists" if exists $self->_contexts->{$name};
524             die "Maximum number of contexts ($SignalWire::Agents::Contexts::MAX_CONTEXTS) exceeded"
525 13 50       20 if keys %{ $self->_contexts } >= $SignalWire::Agents::Contexts::MAX_CONTEXTS;
  13         48  
526              
527 13         268 my $ctx = SignalWire::Agents::Contexts::Context->new(name => $name);
528 13         113 $self->_contexts->{$name} = $ctx;
529 13         27 push @{ $self->_context_order }, $name;
  13         41  
530 13         75 return $ctx;
531             }
532              
533             sub get_context {
534 2     2 0 1361 my ($self, $name) = @_;
535 2         32 return $self->_contexts->{$name};
536             }
537              
538             sub has_contexts {
539 0     0 0 0 my ($self) = @_;
540 0 0       0 return scalar(keys %{ $self->_contexts }) ? 1 : 0;
  0         0  
541             }
542              
543             sub validate {
544 11     11 0 22 my ($self) = @_;
545 11 100       19 die "At least one context must be defined" unless keys %{ $self->_contexts };
  11         52  
546              
547             # Single context must be "default"
548 10 100       18 if (keys %{ $self->_contexts } == 1) {
  10         35  
549 9         14 my ($name) = keys %{ $self->_contexts };
  9         29  
550 9 100       38 die 'When using a single context, it must be named "default"'
551             unless $name eq 'default';
552             }
553              
554             # Each context must have steps
555 9         16 for my $cname (keys %{ $self->_contexts }) {
  9         26  
556 10         21 my $ctx = $self->_contexts->{$cname};
557             die "Context '$cname' must have at least one step"
558 10 100       18 unless keys %{ $ctx->_steps };
  10         44  
559             }
560              
561             # Validate step references in valid_steps
562 8         11 for my $cname (keys %{ $self->_contexts }) {
  8         21  
563 9         23 my $ctx = $self->_contexts->{$cname};
564 9         14 for my $sname (keys %{ $ctx->_steps }) {
  9         24  
565 11         22 my $step = $ctx->_steps->{$sname};
566 11 100       36 if (defined $step->_valid_steps) {
567 2         4 for my $vs (@{ $step->_valid_steps }) {
  2         8  
568 2 100       12 next if $vs eq 'next';
569             die "Step '$sname' in context '$cname' references unknown step '$vs'"
570 1 50       17 unless exists $ctx->_steps->{$vs};
571             }
572             }
573             }
574             }
575              
576             # Validate context references (context-level and step-level)
577 7         11 for my $cname (keys %{ $self->_contexts }) {
  7         17  
578 8         19 my $ctx = $self->_contexts->{$cname};
579 8 100       22 if (defined $ctx->_valid_contexts) {
580 3         7 for my $vc (@{ $ctx->_valid_contexts }) {
  3         11  
581             die "Context '$cname' references unknown context '$vc'"
582 3 100       22 unless exists $self->_contexts->{$vc};
583             }
584             }
585 7         9 for my $sname (keys %{ $ctx->_steps }) {
  7         16  
586 9         18 my $step = $ctx->_steps->{$sname};
587 9 50       28 if (defined $step->_valid_contexts) {
588 0         0 for my $vc (@{ $step->_valid_contexts }) {
  0         0  
589             die "Step '$sname' in context '$cname' references unknown context '$vc'"
590 0 0       0 unless exists $self->_contexts->{$vc};
591             }
592             }
593             }
594             }
595              
596             # Validate gather_info
597 6         10 for my $cname (keys %{ $self->_contexts }) {
  6         13  
598 7         12 my $ctx = $self->_contexts->{$cname};
599 7         10 for my $sname (keys %{ $ctx->_steps }) {
  7         15  
600 9         17 my $step = $ctx->_steps->{$sname};
601 9 100       24 if (defined $step->_gather_info) {
602             die "Step '$sname' in context '$cname' has gather_info with no questions"
603 3 100       4 unless @{ $step->_gather_info->_questions };
  3         19  
604              
605 2         4 my %seen;
606 2         3 for my $q (@{ $step->_gather_info->_questions }) {
  2         7  
607 1         11 die "Step '$sname' in context '$cname' has duplicate gather_info question key '${\$q->key}'"
608 3 100       15 if $seen{ $q->key }++;
609             }
610              
611 1         9 my $action = $step->_gather_info->_completion_action;
612 1 50       4 if (defined $action) {
613 1 50       8 if ($action eq 'next_step') {
    0          
614 1         4 my $idx;
615 1         2 my @order = @{ $ctx->_step_order };
  1         5  
616 1         5 for my $i (0 .. $#order) {
617 1 50       5 if ($order[$i] eq $sname) { $idx = $i; last }
  1         2  
  1         2  
618             }
619 1 50 33     21 die "Step '$sname' in context '$cname' has gather_info completion_action='next_step' but it is the last step"
620             if defined $idx && $idx >= $#order;
621             } elsif (!exists $ctx->_steps->{$action}) {
622 0         0 die "Step '$sname' in context '$cname' has gather_info completion_action='$action' but step '$action' does not exist";
623             }
624             }
625             }
626             }
627             }
628             }
629              
630             sub to_hash {
631 11     11 0 42 my ($self) = @_;
632 11         38 $self->validate;
633              
634 3         5 my %result;
635 3         6 for my $cname (@{ $self->_context_order }) {
  3         8  
636 4         16 $result{$cname} = $self->_contexts->{$cname}->to_hash;
637             }
638 3         21 return \%result;
639             }
640              
641             # Back to main package
642             package SignalWire::Agents::Contexts;
643              
644             sub create_simple_context {
645 2     2 0 5830 my ($class, $name) = @_;
646 2   100     13 $name //= 'default';
647 2         80 return SignalWire::Agents::Contexts::Context->new(name => $name);
648             }
649              
650             1;