aws-lex-conversation 5.1.1 → 6.2.0

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: 46556f575fd08ac0ded8d8fc50692be70388fbf1e3d96ff8d236c82a5e8eb860
4
- data.tar.gz: c5456ec5961936b210d80491bb9edaebf503c71daba5acf755257b36ae211875
3
+ metadata.gz: 3a4ab092d321df0c71e5f16cf1021e87358b1b39ab025eaf5555e3e3da7894c1
4
+ data.tar.gz: a0332c4f2f73b35972c83a0740e1adfa4d724fe5d9861087d348e905e3e15633
5
5
  SHA512:
6
- metadata.gz: dfcf7591d2c9667d17f3863dde180724c4dc5ec5b2f623173be6c11b6cbf9548d99b39ae1ad809e25adb6069fea9239dc58c3a8776bc85340ec07c9752a3c976
7
- data.tar.gz: 6cd97d779b1e482c19300db5cb63e9d36f01278be04c896dc603138b0a293d0baf8fa8cfadfd95d27896b4a467d81211607bb0cb551e960f72d3efd740a5a9cf
6
+ metadata.gz: 734b1ec8921c8982a3e123520b08b0d0b9e655159c781f7df18d106b8de635815bc026862fd6a4ec7a3d2665d47393bb8d977cf530a1331e2f3c2bea543b4a94
7
+ data.tar.gz: 2c4c43bafd522791d6f5f39596c7b594d3788098876b51fc8294cf2304a23c42a06fd6635e15e791410bc824273599f9afab3935fd853981f81b732672d03d96
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,107 @@
1
+ # 6.2.0 - Sept 28, 2021
2
+
3
+ * Add a `Aws::Lex::Conversation#restore_from!` method that accepts a checkpoint parameter. This method modifies the underlying conversation state to match the data from the saved checkpoint.
4
+ * Make the `dialog_action_type` parameter on `Aws::Lex::Conversation#checkpoint!` default to `Delegate` if not specified as a developer convenience.
5
+ * Allow developers to pass an optional `intent` override parameter on `Aws::Lex::Conversation#checkpoint!` for convenience.
6
+ * Update the README with advanced examples for the conversation stash and checkpoints.
7
+
8
+ # 6.1.1 - Sept 22, 2021
9
+
10
+ * 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
11
+
12
+ # 6.1.0 - Sept 7, 2021
13
+
14
+ Added helper methods for clearing active contexts
15
+
16
+ ```ruby
17
+ conversation.clear_context!(name: 'test') # clears this specific active context
18
+ conversation.clear_all_contexts! # clears all current active contexts
19
+ ```
20
+
21
+ # 6.0.0 - Sept 7, 2021
22
+
23
+ * **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.
24
+ * **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.
25
+ * 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:
26
+
27
+ ```ruby
28
+ # we must explicitly require the test helpers
29
+ require 'aws/lex/conversation/spec'
30
+
31
+ # optional: include the custom matchers if you're using RSpec
32
+ RSpec.configure do |config|
33
+ config.include(Aws::Lex::Conversation::Spec)
34
+ end
35
+
36
+ # we can now simulate state in a test somewhere
37
+ it 'simulates a conversation' do
38
+ conversation # given we have an instance of Aws::Lex::Conversation
39
+ .simulate! # simulation modifies the underlying instance
40
+ .transcript('My age is 21') # optionally set an input transcript
41
+ .intent(name: 'MyCoolIntent') # route to the intent named "MyCoolIntent"
42
+ .slot(name: 'Age', value: '21') # add a slot named "Age" with a corresponding value
43
+
44
+ expect(conversation).to have_transcript('My age is 21')
45
+ expect(conversation).to route_to_intent('MyCoolIntent')
46
+ expect(conversation).to have_slot(name: 'Age', value: '21')
47
+ end
48
+
49
+ # if you'd rather create your own event from scratch
50
+ it 'creates an event' do
51
+ simulator = Aws::Lex::Conversation::Simulator.new
52
+ simulator
53
+ .transcript('I am 21 years old.')
54
+ .input_mode('Speech')
55
+ .context(name: 'WelcomeGreetingCompleted')
56
+ .invocation_source('FulfillmentCodeHook')
57
+ .session(username: 'jane.doe')
58
+ .intent(
59
+ name: 'GuessZodiacSign',
60
+ state: 'ReadyForFulfillment',
61
+ slots: {
62
+ age: {
63
+ value: '21'
64
+ }
65
+ }
66
+ )
67
+ event = simulator.event
68
+
69
+ expect(event).to have_transcript('I am 21 years old.')
70
+ expect(event).to have_input_mode('Speech')
71
+ expect(event).to have_active_context(name: 'WelcomeGreetingCompleted')
72
+ expect(event).to have_invocation_source('FulfillmentCodeHook')
73
+ expect(event).to route_to_intent('GuessZodiacSign')
74
+ expect(event).to have_slot(name: 'age', value: '21')
75
+ expect(event).to include_session_values(username: 'jane.doe')
76
+ end
77
+ ```
78
+
79
+ * Add a few convenience methods to `Aws::Lex::Conversation` instances for dealing with active contexts:
80
+ - `#active_context(name:)`:
81
+
82
+ Returns the active context instance that matches the name parameter.
83
+
84
+ - `#active_context?(name:)`:
85
+
86
+ Returns true/false depending on if an active context matching
87
+ the name parameter is found.
88
+
89
+ - `#active_context!(name:, turns:, seconds:, attributes:)`:
90
+
91
+ Creates or updates an existing active context instance for
92
+ the conversation.
93
+
94
+ # 5.1.0 - Sept 2, 2021
95
+
96
+ * Allow the intent to be specified when returning a response such as `elicit_slot`.
97
+
98
+ # 5.0.0 - August 30, 2021
99
+
100
+ * **breaking change** - `Aws::Lex::Conversation::Support::Mixins::SlotElicitation`
101
+ - Rename the `message` attribute to `messages`. This attribute must be a callable that returns and array of `Aws::Lex::Conversation::Type::Message` instances.
102
+ - rename the `follow_up_message` attribute to `follow_up_messages`. This must also be a callable that returns an array of message instances.
103
+ * 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.
104
+
1
105
  # 4.3.0 - August 25, 2021
2
106
 
3
107
  * 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,170 @@ conversation.handlers = [
191
191
  conversation.respond # => { dialogAction: { type: 'Delegate' } }
192
192
  ```
193
193
 
194
+ ## Advanced Concepts
195
+
196
+ This library provides a few constructs to help manage complex interactions:
197
+
198
+ ### Data Stash
199
+
200
+ `Aws::Lex::Conversation` instances implement a `stash` method that can be used to store temporary data within a single invocation.
201
+
202
+ A conversation's stashed data will not be persisted between multiple invocations of your lambda function.
203
+
204
+ The conversation stash is a great spot to store deserialized data from the session, or invocation-specific state that needs to be shared between handler classes.
205
+
206
+ This example illustrates how the stash can be used to store deserialized data from the session:
207
+
208
+ ```ruby
209
+ # given we have JSON-serialized data in as a persisted session value
210
+ conversation.session[:user_data] = '{"name":"Jane","id":1234,"email":"test@example.com"}'
211
+ # we can deserialize the data into a Hash that we store in the conversation stash
212
+ conversation.stash[:user] = JSON.parse(conversation.session[:user_data])
213
+ # later on we can reference our stashed data (within the same invocation)
214
+ conversation.stash[:user] # => {"name"=>"Jane", "id"=>1234, "email"=>"test@example.com"}
215
+ ```
216
+
217
+ ### Checkpoints
218
+
219
+ A conversation may transition between many different topics as the interaction progresses. This type of state transition can be easily handled with checkpoints.
220
+
221
+ When a checkpoint is created, all intent and slot data is encoded and stored into a `checkpoints` session value. This data persists between invocations, and is not removed until the checkpoint is restored.
222
+
223
+ You can create a checkpoint as follows:
224
+
225
+ ```ruby
226
+ # we're ready to fulfill the OrderFlowers intent, but we want to elicit another intent first
227
+ conversation.checkpoint!(
228
+ label: 'order_flowers',
229
+ dialog_action_type: 'Close' # defaults to 'Delegate' if not specified
230
+ )
231
+ conversation.elicit_intent(
232
+ messages: [
233
+ {
234
+ content: 'Thanks! Before I place your order, is there anything else I can help with?',
235
+ contentType: 'PlainText'
236
+ }
237
+ ]
238
+ )
239
+ ```
240
+
241
+ You can restore the checkpoint in one of two ways:
242
+
243
+ ```ruby
244
+ # in a future invocation, we can fetch an instance of the checkpoint and easily
245
+ # restore the conversation to the previous state
246
+ checkpoint = conversation.checkpoint(label: 'order_flowers')
247
+ checkpoint.restore!(
248
+ fulfillment_state: 'Fulfilled',
249
+ messages: [
250
+ {
251
+ content: 'Okay, your flowers have been ordered! Thanks!',
252
+ contentType: 'PlainText'
253
+ }
254
+ ]
255
+ ) # => our response object to Lex is returned
256
+ ```
257
+
258
+ It's also possible to restore state from a checkpoint and utilize the conversation's handler chain:
259
+
260
+ ```ruby
261
+ class AnotherIntent < Aws::Lex::Conversation::Handler::Base
262
+ def will_respond?(conversation)
263
+ conversation.intent_name == 'AnotherIntent' &&
264
+ conversation.checkpoint?(label: 'order_flowers')
265
+ end
266
+
267
+ def response(conversation)
268
+ checkpoint = conversation.checkpoint(label: 'order_flowers')
269
+ # replace the conversation's current resolved intent/slot data with the saved checkpoint data
270
+ conversation.restore_from!(checkpoint)
271
+ # call the next handler in the chain to produce a response
272
+ successor.handle(conversation)
273
+ end
274
+ end
275
+
276
+ class OrderFlowers < Aws::Lex::Conversation::Handler::Base
277
+ def will_respond?(conversation)
278
+ conversation.intent_name == 'OrderFlowers'
279
+ end
280
+
281
+ def response(conversation)
282
+ conversation.close(
283
+ fulfillment_state: 'Fulfilled',
284
+ messages: [
285
+ {
286
+ content: 'Okay, your flowers have been ordered! Thanks!',
287
+ contentType: 'PlainText'
288
+ }
289
+ ]
290
+ )
291
+ end
292
+ end
293
+
294
+ conversation = Aws::Lex::Conversation.new(event: event, context: context)
295
+ conversation.handlers = [
296
+ { handler: AnotherIntent },
297
+ { handler: OrderFlowers }
298
+ ]
299
+ conversation.respond # => returns a Lex response object
300
+ ```
301
+
302
+ ## Test Helpers
303
+
304
+ This library provides convenience methods to make testing easy! You can use the test helpers as follows:
305
+
306
+ ```ruby
307
+ # we must explicitly require the test helpers
308
+ require 'aws/lex/conversation/spec'
309
+
310
+ # optional: include the custom matchers if you're using RSpec
311
+ RSpec.configure do |config|
312
+ config.include(Aws::Lex::Conversation::Spec)
313
+ end
314
+
315
+ # we can now simulate state in a test somewhere
316
+ it 'simulates a conversation' do
317
+ conversation # given we have an instance of Aws::Lex::Conversation
318
+ .simulate! # simulation modifies the underlying instance
319
+ .transcript('My age is 21') # optionally set an input transcript
320
+ .intent(name: 'MyCoolIntent') # route to the intent named "MyCoolIntent"
321
+ .slot(name: 'Age', value: '21') # add a slot named "Age" with a corresponding value
322
+
323
+ expect(conversation).to have_transcript('My age is 21')
324
+ expect(conversation).to route_to_intent('MyCoolIntent')
325
+ expect(conversation).to have_slot(name: 'Age', value: '21')
326
+ end
327
+
328
+ # if you'd rather create your own event from scratch
329
+ it 'creates an event' do
330
+ simulator = Aws::Lex::Conversation::Simulator.new
331
+ simulator
332
+ .transcript('I am 21 years old.')
333
+ .input_mode('Speech')
334
+ .context(name: 'WelcomeGreetingCompleted')
335
+ .invocation_source('FulfillmentCodeHook')
336
+ .session(username: 'jane.doe')
337
+ .intent(
338
+ name: 'GuessZodiacSign',
339
+ state: 'ReadyForFulfillment',
340
+ slots: {
341
+ age: {
342
+ value: '21'
343
+ }
344
+ }
345
+ )
346
+ event = simulator.event
347
+
348
+ expect(event).to have_transcript('I am 21 years old.')
349
+ expect(event).to have_input_mode('Speech')
350
+ expect(event).to have_active_context(name: 'WelcomeGreetingCompleted')
351
+ expect(event).to have_invocation_source('FulfillmentCodeHook')
352
+ expect(event).to route_to_intent('GuessZodiacSign')
353
+ expect(event).to have_slot(name: 'age', value: '21')
354
+ expect(event).to include_session_values(username: 'jane.doe')
355
+ end
356
+ ```
357
+
194
358
  ## Development
195
359
 
196
360
  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,34 @@ module Aws
20
20
  fulfillment_state: FulfillmentState
21
21
  )
22
22
 
23
+ class << self
24
+ def build(opts = {})
25
+ new(normalize_parameters(opts))
26
+ end
27
+
28
+ private
29
+
30
+ def normalize_parameters(opts)
31
+ params = opts.dup # we don't want to mutate our arguments
32
+
33
+ if params[:dialog_action_type].is_a?(String)
34
+ params[:dialog_action_type] = DialogActionType.new(params[:dialog_action_type])
35
+ end
36
+
37
+ if params[:fulfillment_state].is_a?(String)
38
+ params[:fulfillment_state] = FulfillmentState.new(params[:fulfillment_state])
39
+ end
40
+
41
+ params
42
+ end
43
+ end
44
+
45
+ # restore the checkpoint AND remove it from session
46
+ def restore!(conversation, opts = {})
47
+ conversation.checkpoints.delete_if { |c| c.label == label }
48
+ restore(conversation, opts)
49
+ end
50
+
23
51
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
24
52
  def restore(conversation, opts = {})
25
53
  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,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.1'
6
+ VERSION = '6.2.0'
7
7
  end
8
8
  end
9
9
  end
@@ -62,9 +62,9 @@ module Aws
62
62
  label = opts.fetch(:label)
63
63
  params = {
64
64
  label: label,
65
- dialog_action_type: opts.fetch(:dialog_action_type),
65
+ dialog_action_type: opts.fetch(:dialog_action_type) { 'Delegate' },
66
66
  fulfillment_state: opts[:fulfillment_state],
67
- intent: lex.current_intent,
67
+ intent: opts.fetch(:intent) { lex.current_intent },
68
68
  slot_to_elicit: opts[:slot_to_elicit]
69
69
  }.compact
70
70
 
@@ -72,9 +72,8 @@ module Aws
72
72
  # update the existing checkpoint
73
73
  checkpoint(label: label).assign_attributes!(params)
74
74
  else
75
- # push a new checkpoint to the recent_intent_summary_view
76
75
  checkpoints.unshift(
77
- Type::Checkpoint.new(params)
76
+ Type::Checkpoint.build(params)
78
77
  )
79
78
  end
80
79
  end
@@ -91,6 +90,58 @@ module Aws
91
90
  lex.session_state.session_attributes.checkpoints
92
91
  end
93
92
 
93
+ def restore_from!(checkpoint)
94
+ # we're done with the stored checkpoint once it's been restored
95
+ checkpoints.delete_if { |c| c.label == checkpoint.label }
96
+ # remove any memoized intent data
97
+ lex.current_intent = nil
98
+ # replace the intent with data from the checkpoint
99
+ lex.session_state.intent = checkpoint.intent
100
+ dialog_action = Type::DialogAction.new(
101
+ type: checkpoint.dialog_action_type,
102
+ slot_to_elicit: checkpoint.slot_to_elicit
103
+ )
104
+ lex.session_state.dialog_action = dialog_action
105
+ self
106
+ end
107
+
108
+ def active_context?(name:)
109
+ !active_context(name: name).nil?
110
+ end
111
+
112
+ def active_context(name:)
113
+ lex.session_state.active_contexts.find { |c| c.name == name }
114
+ end
115
+
116
+ def active_context!(name:, turns: 10, seconds: 300, attributes: {})
117
+ # look for an existing active context if present
118
+ instance = active_context(name: name)
119
+
120
+ if instance
121
+ clear_context!(name: name)
122
+ else
123
+ instance = Type::Context.new
124
+ end
125
+
126
+ # update attributes as requested
127
+ instance.name = name
128
+ instance.context_attributes = attributes
129
+ instance.time_to_live = Type::TimeToLive.new(
130
+ turns_to_live: turns,
131
+ time_to_live_in_seconds: seconds
132
+ )
133
+ lex.session_state.active_contexts << instance
134
+ instance
135
+ end
136
+
137
+ def clear_context!(name:)
138
+ lex.session_state.active_contexts.delete_if { |c| c.name == name }
139
+ end
140
+
141
+ def clear_all_contexts!
142
+ lex.session_state.active_contexts = []
143
+ end
144
+
94
145
  def stash
95
146
  @stash ||= {}
96
147
  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.1
4
+ version: 6.2.0
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-03 00:00:00.000000000 Z
15
+ date: 2021-09-28 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