aws-lex-conversation 5.1.0 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 128c0cd7f79f2ba2f9fa8c8a148a06100cd9b4d26f9569cd97f53fee5d33b16e
4
- data.tar.gz: 3e715ccfbec559fabc2503c49d00fac234df8668edeab39f2c561ad379909da5
3
+ metadata.gz: f15c999ea65d5d749443fbed6e0871abe69ffa65d399febdd2900d5d99b9730c
4
+ data.tar.gz: 21165febaf008f32900422e98dffa448dc6caefade98908c5dc6e0b32e97cab9
5
5
  SHA512:
6
- metadata.gz: 176b75d71df52db5799a95380d6820665511e2e3459e5cdf0d8e30ae4ca0f090086c0bb6609c6124b629fc1ab04d0e3ee1e63fd35da0463186cb7479f165c4e2
7
- data.tar.gz: 315f366c43a00923b50828360ff00d32fa8f566d2cc2c990670557de2b87d4259f0960d8abddf0baed3a039a7667ec3aa7982e82edf97db5c20443beb9611a2c
6
+ metadata.gz: a6a2fbd9f33532e9e73bd130bd42983ebc1bc8398c9425ac9f8d82f12d55e941d8979bdc498341a85a99462f74b0de5a42afea61ad15422c0b0032d55d8fd207
7
+ data.tar.gz: 929adca898e5825a5eb7b9261ad78647487a9d452e66d83c06035f3c0fc564ae01035f6df8143e201ea12db976144980c04d7b1f842deb60ccbc1b8fd7c537a1
data/.rubocop.yml CHANGED
@@ -20,12 +20,17 @@ Metrics/AbcSize:
20
20
  Max: 20
21
21
  Metrics/ClassLength:
22
22
  Max: 150
23
+ Exclude:
24
+ - lib/aws/lex/conversation/simulator.rb
23
25
  Metrics/BlockLength:
24
26
  Exclude:
25
27
  - 'spec/**/*'
26
28
  - '*.gemspec'
27
29
  Metrics/MethodLength:
28
30
  Max: 25
31
+ Metrics/ModuleLength:
32
+ Exclude:
33
+ - lib/aws/lex/conversation/spec/matchers.rb
29
34
  Naming/FileName:
30
35
  Enabled: true
31
36
  Exclude:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,100 @@
1
+ # 6.1.1 - Sept 22, 2021
2
+
3
+ * renamed `maximum_elicitations` to `max_retries` and made it backwards compatible to make the param name clear, by default this value is zero, allowing each slot to elicit only once
4
+
5
+ # 6.1.0 - Sept 7, 2021
6
+
7
+ Added helper methods for clearing active contexts
8
+
9
+ ```ruby
10
+ conversation.clear_context!(name: 'test') # clears this specific active context
11
+ conversation.clear_all_contexts! # clears all current active contexts
12
+ ```
13
+
14
+ # 6.0.0 - Sept 7, 2021
15
+
16
+ * **breaking change** - Modify `Aws::Lex::Conversation::Type::Base#computed_property` to accept a block instead of a callable argument. This is an internal class and should not require any application-level changes.
17
+ * **breaking change** - Add a required `alias_name` attribute on `Aws::Lex::Conversation::Type::Bot` instances. Please note that the Version 2 of AWS Lex correctly returns this value as input to lambda functions. Therefore no application-level changes are necessary.
18
+ * Implement a new set of test helpers to make it easier to modify state and match test expectations. You can use the test helpers as follows:
19
+
20
+ ```ruby
21
+ # we must explicitly require the test helpers
22
+ require 'aws/lex/conversation/spec'
23
+
24
+ # optional: include the custom matchers if you're using RSpec
25
+ RSpec.configure do |config|
26
+ config.include(Aws::Lex::Conversation::Spec)
27
+ end
28
+
29
+ # we can now simulate state in a test somewhere
30
+ it 'simulates a conversation' do
31
+ conversation # given we have an instance of Aws::Lex::Conversation
32
+ .simulate! # simulation modifies the underlying instance
33
+ .transcript('My age is 21') # optionally set an input transcript
34
+ .intent(name: 'MyCoolIntent') # route to the intent named "MyCoolIntent"
35
+ .slot(name: 'Age', value: '21') # add a slot named "Age" with a corresponding value
36
+
37
+ expect(conversation).to have_transcript('My age is 21')
38
+ expect(conversation).to route_to_intent('MyCoolIntent')
39
+ expect(conversation).to have_slot(name: 'Age', value: '21')
40
+ end
41
+
42
+ # if you'd rather create your own event from scratch
43
+ it 'creates an event' do
44
+ simulator = Aws::Lex::Conversation::Simulator.new
45
+ simulator
46
+ .transcript('I am 21 years old.')
47
+ .input_mode('Speech')
48
+ .context(name: 'WelcomeGreetingCompleted')
49
+ .invocation_source('FulfillmentCodeHook')
50
+ .session(username: 'jane.doe')
51
+ .intent(
52
+ name: 'GuessZodiacSign',
53
+ state: 'ReadyForFulfillment',
54
+ slots: {
55
+ age: {
56
+ value: '21'
57
+ }
58
+ }
59
+ )
60
+ event = simulator.event
61
+
62
+ expect(event).to have_transcript('I am 21 years old.')
63
+ expect(event).to have_input_mode('Speech')
64
+ expect(event).to have_active_context(name: 'WelcomeGreetingCompleted')
65
+ expect(event).to have_invocation_source('FulfillmentCodeHook')
66
+ expect(event).to route_to_intent('GuessZodiacSign')
67
+ expect(event).to have_slot(name: 'age', value: '21')
68
+ expect(event).to include_session_values(username: 'jane.doe')
69
+ end
70
+ ```
71
+
72
+ * Add a few convenience methods to `Aws::Lex::Conversation` instances for dealing with active contexts:
73
+ - `#active_context(name:)`:
74
+
75
+ Returns the active context instance that matches the name parameter.
76
+
77
+ - `#active_context?(name:)`:
78
+
79
+ Returns true/false depending on if an active context matching
80
+ the name parameter is found.
81
+
82
+ - `#active_context!(name:, turns:, seconds:, attributes:)`:
83
+
84
+ Creates or updates an existing active context instance for
85
+ the conversation.
86
+
87
+ # 5.1.0 - Sept 2, 2021
88
+
89
+ * Allow the intent to be specified when returning a response such as `elicit_slot`.
90
+
91
+ # 5.0.0 - August 30, 2021
92
+
93
+ * **breaking change** - `Aws::Lex::Conversation::Support::Mixins::SlotElicitation`
94
+ - Rename the `message` attribute to `messages`. This attribute must be a callable that returns and array of `Aws::Lex::Conversation::Type::Message` instances.
95
+ - rename the `follow_up_message` attribute to `follow_up_messages`. This must also be a callable that returns an array of message instances.
96
+ * Allow the `fallback` callable in `SlotElicitation` to be nilable. The slot value will not be elicited if the value is nil and maximum attempts have been exceeded.
97
+
1
98
  # 4.3.0 - August 25, 2021
2
99
 
3
100
  * Slot elicitor can now be passed an Aws::Lex::Conversation::Type::Message as part of the DSL/callback and properly formats the response as such
data/README.md CHANGED
@@ -191,6 +191,62 @@ conversation.handlers = [
191
191
  conversation.respond # => { dialogAction: { type: 'Delegate' } }
192
192
  ```
193
193
 
194
+ ## Test Helpers
195
+
196
+ This library provides convenience methods to make testing easy! You can use the test helpers as follows:
197
+
198
+ ```ruby
199
+ # we must explicitly require the test helpers
200
+ require 'aws/lex/conversation/spec'
201
+
202
+ # optional: include the custom matchers if you're using RSpec
203
+ RSpec.configure do |config|
204
+ config.include(Aws::Lex::Conversation::Spec)
205
+ end
206
+
207
+ # we can now simulate state in a test somewhere
208
+ it 'simulates a conversation' do
209
+ conversation # given we have an instance of Aws::Lex::Conversation
210
+ .simulate! # simulation modifies the underlying instance
211
+ .transcript('My age is 21') # optionally set an input transcript
212
+ .intent(name: 'MyCoolIntent') # route to the intent named "MyCoolIntent"
213
+ .slot(name: 'Age', value: '21') # add a slot named "Age" with a corresponding value
214
+
215
+ expect(conversation).to have_transcript('My age is 21')
216
+ expect(conversation).to route_to_intent('MyCoolIntent')
217
+ expect(conversation).to have_slot(name: 'Age', value: '21')
218
+ end
219
+
220
+ # if you'd rather create your own event from scratch
221
+ it 'creates an event' do
222
+ simulator = Aws::Lex::Conversation::Simulator.new
223
+ simulator
224
+ .transcript('I am 21 years old.')
225
+ .input_mode('Speech')
226
+ .context(name: 'WelcomeGreetingCompleted')
227
+ .invocation_source('FulfillmentCodeHook')
228
+ .session(username: 'jane.doe')
229
+ .intent(
230
+ name: 'GuessZodiacSign',
231
+ state: 'ReadyForFulfillment',
232
+ slots: {
233
+ age: {
234
+ value: '21'
235
+ }
236
+ }
237
+ )
238
+ event = simulator.event
239
+
240
+ expect(event).to have_transcript('I am 21 years old.')
241
+ expect(event).to have_input_mode('Speech')
242
+ expect(event).to have_active_context(name: 'WelcomeGreetingCompleted')
243
+ expect(event).to have_invocation_source('FulfillmentCodeHook')
244
+ expect(event).to route_to_intent('GuessZodiacSign')
245
+ expect(event).to have_slot(name: 'age', value: '21')
246
+ expect(event).to include_session_values(username: 'jane.doe')
247
+ end
248
+ ```
249
+
194
250
  ## Development
195
251
 
196
252
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ class Simulator
7
+ attr_accessor :lex
8
+
9
+ def initialize(opts = {})
10
+ self.lex = opts.fetch(:lex) do
11
+ Type::Event.shrink_wrap(
12
+ bot: default_bot,
13
+ inputMode: 'Text',
14
+ inputTranscript: '',
15
+ interpretations: [],
16
+ invocationSource: 'DialogCodeHook',
17
+ messageVersion: '1.0',
18
+ requestAttributes: {},
19
+ responseContentType: 'text/plain; charset=utf-8',
20
+ sessionId: '1234567890000',
21
+ sessionState: default_session_state
22
+ )
23
+ end
24
+ interpretation(name: 'SIMULATION')
25
+ end
26
+
27
+ def event
28
+ lex.to_lex
29
+ end
30
+
31
+ def bot(opts = {})
32
+ changes = {
33
+ aliasName: opts[:alias_name],
34
+ aliasId: opts[:alias_id],
35
+ name: opts[:name],
36
+ version: opts[:version],
37
+ localeId: opts[:locale_id],
38
+ id: opts[:id]
39
+ }.compact
40
+ lex.bot = Type::Bot.shrink_wrap(lex.bot.to_lex.merge(changes))
41
+ self
42
+ end
43
+
44
+ def transcript(message)
45
+ lex.input_transcript = message
46
+ self
47
+ end
48
+
49
+ def intent(opts = {})
50
+ data = default_intent(opts)
51
+ intent = Type::Intent.shrink_wrap(data)
52
+ lex.session_state.intent = intent
53
+ interpretation(data)
54
+ end
55
+
56
+ # rubocop:disable Metrics/AbcSize
57
+ def interpretation(opts = {})
58
+ name = opts.fetch(:name)
59
+ slots = opts.fetch(:slots) { {} }
60
+ sentiment_score = opts.dig(:sentiment_response, :sentiment_score)
61
+ sentiment = opts.dig(:sentiment_response, :sentiment)
62
+ sentiment_response = opts[:sentiment_response] && {
63
+ sentiment: sentiment,
64
+ sentimentScore: sentiment_score
65
+ }
66
+ data = {
67
+ intent: default_intent(opts),
68
+ sentimentResponse: sentiment_response,
69
+ nluConfidence: opts[:nlu_confidence]
70
+ }.compact
71
+ lex.interpretations.delete_if { |i| i.intent.name == name }
72
+ lex.interpretations << Type::Interpretation.shrink_wrap(data)
73
+ slots.each do |key, value|
74
+ slot_data = { name: key }.merge(value)
75
+ slot(slot_data)
76
+ end
77
+ reset_computed_properties!
78
+ self
79
+ end
80
+ # rubocop:enable Metrics/AbcSize
81
+
82
+ def context(opts = {})
83
+ data = {
84
+ name: opts.fetch(:name),
85
+ contextAttributes: opts.fetch(:context_attributes) { {} },
86
+ timeToLive: {
87
+ timeToLiveInSeconds: opts.fetch(:seconds) { 600 },
88
+ turnsToLive: opts.fetch(:turns) { 100 }
89
+ }
90
+ }
91
+ context = Type::Context.shrink_wrap(data)
92
+ lex.session_state.active_contexts.delete_if { |c| c.name == context.name }
93
+ lex.session_state.active_contexts << context
94
+ self
95
+ end
96
+
97
+ def slot(opts = {})
98
+ name = opts.fetch(:name).to_sym
99
+ raw_slots = {
100
+ shape: opts.fetch(:shape) { 'Scalar' },
101
+ value: {
102
+ originalValue: opts.fetch(:original_value) { opts.fetch(:value) },
103
+ resolvedValues: opts.fetch(:resolved_values) { [opts.fetch(:value)] },
104
+ interpretedValue: opts.fetch(:interpreted_value) { opts.fetch(:value) }
105
+ }
106
+ }
107
+ lex.session_state.intent.raw_slots[name] = raw_slots
108
+ current_interpretation.intent.raw_slots[name] = raw_slots
109
+ reset_computed_properties!
110
+ self
111
+ end
112
+
113
+ def invocation_source(source)
114
+ lex.invocation_source = Type::InvocationSource.new(source)
115
+ self
116
+ end
117
+
118
+ def input_mode(mode)
119
+ lex.input_mode = Type::InputMode.new(mode)
120
+ self
121
+ end
122
+
123
+ def session(data)
124
+ lex.session_state.session_attributes.merge!(Type::SessionAttributes[data])
125
+ self
126
+ end
127
+
128
+ private
129
+
130
+ def current_interpretation
131
+ lex.interpretations.find { |i| i.intent.name == lex.session_state.intent.name }
132
+ end
133
+
134
+ # computed properties are memoized using instance variables, so we must
135
+ # uncache the values when things change
136
+ def reset_computed_properties!
137
+ %w[
138
+ @alternate_intents
139
+ @current_intent
140
+ @intents
141
+ ].each do |variable|
142
+ lex.instance_variable_set(variable, nil)
143
+ end
144
+
145
+ lex.session_state.intent.instance_variable_set('@slots', nil)
146
+ current_interpretation.intent.instance_variable_set('@slots', nil)
147
+ end
148
+
149
+ def default_bot
150
+ {
151
+ aliasId: 'TSTALIASID',
152
+ aliasName: 'TestBotAlias',
153
+ id: 'BOT_ID',
154
+ localeId: 'en_US',
155
+ name: 'SIMULATOR',
156
+ version: 'DRAFT'
157
+ }
158
+ end
159
+
160
+ def default_session_state
161
+ {
162
+ activeContexts: [],
163
+ sessionAttributes: {},
164
+ intent: {
165
+ confirmationState: 'None',
166
+ name: 'SIMULATION',
167
+ slots: {},
168
+ state: 'InProgress',
169
+ originatingRequestId: SecureRandom.uuid
170
+ }
171
+ }
172
+ end
173
+
174
+ def default_intent(opts = {})
175
+ {
176
+ confirmationState: opts.fetch(:confirmation_state) { 'None' },
177
+ kendraResponse: opts[:kendra_response],
178
+ name: opts.fetch(:name),
179
+ nluConfidence: opts[:nlu_confidence],
180
+ originatingRequestId: opts.fetch(:originating_request_id) { SecureRandom.uuid },
181
+ slots: opts.fetch(:slots) { {} },
182
+ state: opts.fetch(:state) { 'InProgress' }
183
+ }.compact
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -6,7 +6,7 @@ module Aws
6
6
  module Slot
7
7
  class Elicitation
8
8
  attr_accessor :name, :elicit, :messages, :follow_up_messages,
9
- :fallback, :maximum_elicitations, :conversation
9
+ :fallback, :max_retries, :conversation
10
10
 
11
11
  def initialize(opts = {})
12
12
  self.name = opts.fetch(:name)
@@ -14,7 +14,7 @@ module Aws
14
14
  self.messages = opts.fetch(:messages)
15
15
  self.follow_up_messages = opts.fetch(:follow_up_messages) { opts.fetch(:messages) }
16
16
  self.fallback = opts[:fallback]
17
- self.maximum_elicitations = opts.fetch(:maximum_elicitations) { 0 }
17
+ self.max_retries = opts[:max_retries] || opts[:maximum_elicitations] || 0
18
18
  end
19
19
 
20
20
  def elicit!
@@ -53,9 +53,7 @@ module Aws
53
53
  end
54
54
 
55
55
  def maximum_elicitations?
56
- return false if maximum_elicitations.zero?
57
-
58
- elicitation_attempts > maximum_elicitations
56
+ elicitation_attempts > max_retries
59
57
  end
60
58
 
61
59
  def first_elicitation?
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Spec
7
+ module Matchers
8
+ extend RSpec::Matchers::DSL
9
+
10
+ # :nocov:
11
+ def build_event(input)
12
+ case input
13
+ when Aws::Lex::Conversation
14
+ input.lex.to_lex
15
+ when Hash
16
+ input
17
+ else
18
+ raise ArgumentError, \
19
+ 'expected instance of Aws::Lex::Conversation ' \
20
+ "or Hash, got: #{input.inspect}"
21
+ end
22
+ end
23
+ # :nocov:
24
+
25
+ matcher(:have_transcript) do |expected|
26
+ match do |actual|
27
+ build_event(actual).fetch(:inputTranscript) == expected
28
+ end
29
+
30
+ # :nocov:
31
+ failure_message do |actual|
32
+ response = build_event(actual)
33
+ "expected a transcript of '#{expected}', got '#{response.fetch(:inputTranscript)}'"
34
+ end
35
+
36
+ failure_message_when_negated do |actual|
37
+ response = build_event(actual)
38
+ "expected transcript to not equal '#{expected}', got '#{response.fetch(:inputTranscript)}'"
39
+ end
40
+ # :nocov:
41
+ end
42
+
43
+ matcher(:route_to_intent) do |expected|
44
+ match do |actual|
45
+ build_event(actual).dig(:sessionState, :intent, :name) == expected
46
+ end
47
+
48
+ # :nocov:
49
+ failure_message do |actual|
50
+ response = build_event(actual)
51
+ "expected intent to be '#{expected}', got '#{response.dig(:sessionState, :intent, :name)}'"
52
+ end
53
+
54
+ failure_message_when_negated do |actual|
55
+ response = build_event(actual)
56
+ "expected intent to not be '#{expected}', got '#{response.dig(:sessionState, :intent, :name)}'"
57
+ end
58
+ # :nocov:
59
+ end
60
+
61
+ matcher(:have_active_context) do |expected|
62
+ match do |actual|
63
+ build_event(actual).dig(:sessionState, :activeContexts).any? { |c| c[:name] == expected }
64
+ end
65
+
66
+ # :nocov:
67
+ failure_message do |actual|
68
+ response = build_event(actual)
69
+ names = response.dig(:sessionState, :activeContexts).map { |c| c[:name] }.inspect
70
+ "expected active context of `#{expected}` in #{names}"
71
+ end
72
+
73
+ failure_message_when_negated do |actual|
74
+ response = build_event(actual)
75
+ names = response.dig(:sessionState, :activeContexts).map { |c| c[:name] }.inspect
76
+ "expected active contexts to not include `#{expected}`, got: #{names}"
77
+ end
78
+ # :nocov:
79
+ end
80
+
81
+ matcher(:have_invocation_source) do |expected|
82
+ match do |actual|
83
+ build_event(actual).fetch(:invocationSource) == expected
84
+ end
85
+
86
+ # :nocov:
87
+ failure_message do |actual|
88
+ response = build_event(actual)
89
+ "expected invocationSource of `#{expected}`, got: #{response[:invocationSource]}"
90
+ end
91
+
92
+ failure_message_when_negated do |actual|
93
+ response = build_event(actual)
94
+ "expected invocationSource to not be `#{expected}`, got: #{response[:invocationSource]}"
95
+ end
96
+ # :nocov:
97
+ end
98
+
99
+ matcher(:have_input_mode) do |expected|
100
+ match do |actual|
101
+ build_event(actual).fetch(:inputMode) == expected
102
+ end
103
+
104
+ # :nocov:
105
+ failure_message do |actual|
106
+ response = build_event(actual)
107
+ "expected inputMode of `#{expected}`, got: #{response[:inputMode]}"
108
+ end
109
+
110
+ failure_message_when_negated do |actual|
111
+ response = build_event(actual)
112
+ "expected inputMode to not be `#{expected}`, got: #{response[:inputMode]}"
113
+ end
114
+ # :nocov:
115
+ end
116
+
117
+ matcher(:have_interpretation) do |expected|
118
+ match do |actual|
119
+ build_event(actual).fetch(:interpretations).any? { |i| i.dig(:intent, :name) == expected }
120
+ end
121
+
122
+ # :nocov:
123
+ failure_message do |actual|
124
+ response = build_event(actual)
125
+ names = response[:interpretations].map { |i| i.dig(:intent, :name) }.inspect
126
+ "expected interpretation of `#{expected}` in #{names}"
127
+ end
128
+
129
+ failure_message_when_negated do |actual|
130
+ response = build_event(actual)
131
+ names = response[:interpretations].map { |_i| c.dig(:intent, :name) }.inspect
132
+ "expected interpretations to not include `#{expected}`, got: #{names}"
133
+ end
134
+ # :nocov:
135
+ end
136
+
137
+ matcher(:have_slot) do |opts|
138
+ name = opts.fetch(:name)
139
+ value = opts[:value]
140
+ values = value && [value]
141
+ expected_slot = {
142
+ shape: opts.fetch(:shape) { 'Scalar' },
143
+ value: {
144
+ originalValue: opts.fetch(:original_value, value),
145
+ interpretedValue: opts.fetch(:interpreted_value, value),
146
+ resolvedValues: opts.fetch(:resolved_values, values)
147
+ }.compact
148
+ }
149
+
150
+ match do |actual|
151
+ slot = build_event(actual).dig(:sessionState, :intent, :slots, name.to_sym)
152
+ slot[:shape] == expected_slot[:shape] &&
153
+ slot[:value].slice(*expected_slot[:value].keys) == expected_slot[:value]
154
+ end
155
+
156
+ # :nocov:
157
+ failure_message do |actual|
158
+ slot = build_event(actual).dig(:sessionState, :intent, :slots, name.to_sym)
159
+ "expected #{expected_slot.inspect} to equal #{slot.inspect}"
160
+ end
161
+
162
+ failure_message_when_negated do |actual|
163
+ slot = build_event(actual).dig(:sessionState, :intent, :slots, name.to_sym)
164
+ "expected #{expected_slot.inspect} to not equal #{slot.inspect}"
165
+ end
166
+ # :nocov:
167
+ end
168
+
169
+ matcher(:have_action) do |expected|
170
+ match do |actual|
171
+ build_event(actual).dig(:sessionState, :dialogAction, :type) == expected
172
+ end
173
+
174
+ # :nocov:
175
+ failure_message do |actual|
176
+ "expected #{build_event(actual).dig(:sessionState, :dialogAction, :type)} to equal #{expected}"
177
+ end
178
+
179
+ failure_message_when_negated do |actual|
180
+ "expected #{build_event(actual).dig(:sessionState, :dialogAction, :type)} to not equal #{expected}"
181
+ end
182
+ # :nocov:
183
+ end
184
+
185
+ matcher(:have_intent_state) do |expected|
186
+ match do |actual|
187
+ build_event(actual).dig(:sessionState, :intent, :state) == expected
188
+ end
189
+
190
+ # :nocov:
191
+ failure_message do |actual|
192
+ "expected #{build_event(actual).dig(:sessionState, :intent, :state)} to equal #{expected}"
193
+ end
194
+
195
+ failure_message_when_negated do |actual|
196
+ "expected #{build_event(actual).dig(:sessionState, :intent, :state)} to not equal #{expected}"
197
+ end
198
+ # :nocov:
199
+ end
200
+
201
+ matcher(:elicit_slot) do |expected|
202
+ match do |actual|
203
+ response = build_event(actual)
204
+
205
+ response.dig(:sessionState, :dialogAction, :type) == 'ElicitSlot' &&
206
+ response.dig(:sessionState, :dialogAction, :slotToElicit) == expected
207
+ end
208
+ end
209
+
210
+ matcher(:include_session_values) do |expected|
211
+ match do |actual|
212
+ response = build_event(actual)
213
+ response.dig(:sessionState, :sessionAttributes).slice(*expected.keys) == expected
214
+ end
215
+ end
216
+
217
+ matcher(:have_message) do |expected|
218
+ match do |actual|
219
+ build_event(actual)[:messages].any? { |m| m.slice(*expected.keys) == expected }
220
+ end
221
+
222
+ # :nocov:
223
+ failure_message do |actual|
224
+ "expected matching message of #{expected.inspect} in #{build_event(actual)[:messages].inspect}"
225
+ end
226
+
227
+ failure_message_when_negated do |actual|
228
+ "found matching message of #{expected.inspect} in #{build_event(actual)[:messages].inspect}"
229
+ end
230
+ # :nocov:
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+ require_relative 'simulator'
5
+ require_relative 'spec/matchers'
6
+
7
+ module Aws
8
+ module Lex
9
+ class Conversation
10
+ module Spec
11
+ def self.included(base)
12
+ base.include(Matchers)
13
+ end
14
+ end
15
+
16
+ def simulate!
17
+ @simulate ||= Simulator.new(lex: lex)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -71,14 +71,19 @@ module Aws
71
71
  ->(v) { v.transform_keys(&:to_sym) }
72
72
  end
73
73
 
74
- def computed_property(attribute, callable)
75
- mapping[attribute] = attribute
74
+ def computed_property(attribute, opts = {}, &block)
76
75
  attr_writer(attribute)
77
76
 
77
+ if opts.fetch(:virtual) { false }
78
+ virtual_attributes << attribute
79
+ else
80
+ mapping[attribute] = attribute
81
+ end
82
+
78
83
  # dynamically memoize the result
79
84
  define_method(attribute) do
80
85
  instance_variable_get("@#{attribute}") ||
81
- instance_variable_set("@#{attribute}", callable.call(self))
86
+ instance_variable_set("@#{attribute}", block.call(self))
82
87
  end
83
88
  end
84
89
 
@@ -8,6 +8,7 @@ module Aws
8
8
  include Base
9
9
 
10
10
  required :alias_id
11
+ required :alias_name
11
12
  required :id
12
13
  required :locale_id
13
14
  required :name
@@ -20,6 +20,12 @@ module Aws
20
20
  fulfillment_state: FulfillmentState
21
21
  )
22
22
 
23
+ # restore the checkpoint AND remove it from session
24
+ def restore!(conversation, opts = {})
25
+ conversation.checkpoints.delete_if { |c| c.label == label }
26
+ restore(conversation, opts)
27
+ end
28
+
23
29
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
24
30
  def restore(conversation, opts = {})
25
31
  case dialog_action_type.raw
@@ -7,9 +7,9 @@ module Aws
7
7
  class Context
8
8
  include Base
9
9
 
10
- required :context_attributes
11
10
  required :name
12
11
  required :time_to_live
12
+ required :context_attributes, default: -> { {} }
13
13
 
14
14
  coerce(
15
15
  context_attributes: symbolize_hash!,
@@ -18,21 +18,21 @@ module Aws
18
18
  required :session_id
19
19
  required :session_state
20
20
 
21
- computed_property :current_intent, ->(instance) do
21
+ computed_property(:current_intent, virtual: true) do |instance|
22
22
  instance.session_state.intent.tap do |intent|
23
- intent.nlu_confidence = instance.interpretations.find { |i| i.intent.name == intent.name }.nlu_confidence
23
+ intent.nlu_confidence = instance.interpretations.find { |i| i.intent.name == intent.name }&.nlu_confidence
24
24
  end
25
25
  end
26
26
 
27
- computed_property :intents, ->(instance) do
27
+ computed_property(:intents, virutal: true) do |instance|
28
28
  instance.interpretations.map(&:intent).tap do |intents|
29
29
  intents.map do |intent|
30
- intent.nlu_confidence = instance.interpretations.find { |i| i.intent.name == intent.name }.nlu_confidence
30
+ intent.nlu_confidence = instance.interpretations.find { |i| i.intent.name == intent.name }&.nlu_confidence
31
31
  end
32
32
  end
33
33
  end
34
34
 
35
- computed_property :alternate_intents, ->(instance) do
35
+ computed_property(:alternate_intents, virtual: true) do |instance|
36
36
  instance.intents.reject { |intent| intent.name == instance.current_intent.name }
37
37
  end
38
38
 
@@ -15,7 +15,7 @@ module Aws
15
15
  optional :originating_request_id
16
16
  optional :nlu_confidence
17
17
 
18
- computed_property :slots, ->(instance) do
18
+ computed_property(:slots) do |instance|
19
19
  # any keys indexed without a value will return an empty Slot instance
20
20
  default_hash = Hash.new do |_hash, key|
21
21
  Slot.shrink_wrap(active: false, name: key.to_sym, value: nil, shape: 'Scalar')
@@ -20,7 +20,7 @@ module Aws
20
20
  def to_lex
21
21
  merge(
22
22
  checkpoints: Base64.urlsafe_encode64(checkpoints.map(&:to_lex).to_json, padding: false)
23
- )
23
+ ).to_h
24
24
  end
25
25
  end
26
26
  end
@@ -20,10 +20,7 @@ module Aws
20
20
  )
21
21
 
22
22
  def to_lex
23
- super.merge(
24
- value: transform_to_lex(lex_value),
25
- values: transform_to_lex(lex_values)
26
- )
23
+ super.merge(extra_response_attributes)
27
24
  end
28
25
 
29
26
  def value=(val)
@@ -35,7 +32,7 @@ module Aws
35
32
  def value
36
33
  raise TypeError, 'use values for List-type slots' if shape.list?
37
34
 
38
- lex_value.interpreted_value
35
+ lex_value&.interpreted_value
39
36
  end
40
37
 
41
38
  # takes an array of slot values
@@ -87,6 +84,20 @@ module Aws
87
84
  def requestable?
88
85
  active? && blank?
89
86
  end
87
+
88
+ private
89
+
90
+ def extra_response_attributes
91
+ if shape.list?
92
+ {
93
+ values: transform_to_lex(lex_values)
94
+ }
95
+ else
96
+ {
97
+ value: transform_to_lex(lex_value)
98
+ }
99
+ end
100
+ end
90
101
  end
91
102
  end
92
103
  end
@@ -3,7 +3,7 @@
3
3
  module Aws
4
4
  module Lex
5
5
  class Conversation
6
- VERSION = '5.1.0'
6
+ VERSION = '6.1.1'
7
7
  end
8
8
  end
9
9
  end
@@ -91,6 +91,43 @@ module Aws
91
91
  lex.session_state.session_attributes.checkpoints
92
92
  end
93
93
 
94
+ def active_context?(name:)
95
+ !active_context(name: name).nil?
96
+ end
97
+
98
+ def active_context(name:)
99
+ lex.session_state.active_contexts.find { |c| c.name == name }
100
+ end
101
+
102
+ def active_context!(name:, turns: 10, seconds: 300, attributes: {})
103
+ # look for an existing active context if present
104
+ instance = active_context(name: name)
105
+
106
+ if instance
107
+ lex.session_state.active_contexts.delete_if { |c| c.name == name }
108
+ else
109
+ instance = Type::Context.new
110
+ end
111
+
112
+ # update attributes as requested
113
+ instance.name = name
114
+ instance.context_attributes = attributes
115
+ instance.time_to_live = Type::TimeToLive.new(
116
+ turns_to_live: turns,
117
+ time_to_live_in_seconds: seconds
118
+ )
119
+ lex.session_state.active_contexts << instance
120
+ instance
121
+ end
122
+
123
+ def clear_context!(name:)
124
+ lex.session_state.active_contexts.delete_if { |c| c.name == name }
125
+ end
126
+
127
+ def clear_all_contexts!
128
+ lex.session_state.active_contexts = []
129
+ end
130
+
94
131
  def stash
95
132
  @stash ||= {}
96
133
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-lex-conversation
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.0
4
+ version: 6.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Doyle
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: exe
14
14
  cert_chain: []
15
- date: 2021-09-02 00:00:00.000000000 Z
15
+ date: 2021-09-22 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: shrink_wrap
@@ -67,8 +67,11 @@ files:
67
67
  - lib/aws/lex/conversation/response/delegate.rb
68
68
  - lib/aws/lex/conversation/response/elicit_intent.rb
69
69
  - lib/aws/lex/conversation/response/elicit_slot.rb
70
+ - lib/aws/lex/conversation/simulator.rb
70
71
  - lib/aws/lex/conversation/slot/elicitation.rb
71
72
  - lib/aws/lex/conversation/slot/elicitor.rb
73
+ - lib/aws/lex/conversation/spec.rb
74
+ - lib/aws/lex/conversation/spec/matchers.rb
72
75
  - lib/aws/lex/conversation/support/inflector.rb
73
76
  - lib/aws/lex/conversation/support/mixins/responses.rb
74
77
  - lib/aws/lex/conversation/support/mixins/slot_elicitation.rb