aws-lex-conversation 5.1.1 → 6.0.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: 2c078b6b854b3df1824942655b7dd763f72dcc1a439c61e0830e1a644a0c2a65
4
+ data.tar.gz: 8f279c911fceacdc67005e02a09b04bd854e71f30d1212a92ca9d6aff5fe1f75
5
5
  SHA512:
6
- metadata.gz: dfcf7591d2c9667d17f3863dde180724c4dc5ec5b2f623173be6c11b6cbf9548d99b39ae1ad809e25adb6069fea9239dc58c3a8776bc85340ec07c9752a3c976
7
- data.tar.gz: 6cd97d779b1e482c19300db5cb63e9d36f01278be04c896dc603138b0a293d0baf8fa8cfadfd95d27896b4a467d81211607bb0cb551e960f72d3efd740a5a9cf
6
+ metadata.gz: 41b0d384f766900de162b9b7a49d9e4ffc0375168c9f33a7c7e238dfe72fdeb7d21a9ad366c3f9f7b31b2dffde1b06c02a6f6c164fddec5453915bf4403cb8d7
7
+ data.tar.gz: ab4d16c2892aa0f8bf2c25e67f133a64d6c7e583283a1a960f12602da69cedf83d7242960d789e77d03b43c3ba43411fbabfc8a147668cd4f79151eea078886e
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,86 @@
1
+ # 6.0.0 - Sept 7, 2021
2
+
3
+ * **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.
4
+ * **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.
5
+ * 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:
6
+
7
+ ```ruby
8
+ # we must explicitly require the test helpers
9
+ require 'aws/lex/conversation/spec'
10
+
11
+ # optional: include the custom matchers if you're using RSpec
12
+ RSpec.configure do |config|
13
+ config.include(Aws::Lex::Conversation::Spec)
14
+ end
15
+
16
+ # we can now simulate state in a test somewhere
17
+ it 'simulates a conversation' do
18
+ conversation # given we have an instance of Aws::Lex::Conversation
19
+ .simulate! # simulation modifies the underlying instance
20
+ .transcript('My age is 21') # optionally set an input transcript
21
+ .intent(name: 'MyCoolIntent') # route to the intent named "MyCoolIntent"
22
+ .slot(name: 'Age', value: '21') # add a slot named "Age" with a corresponding value
23
+
24
+ expect(conversation).to have_transcript('My age is 21')
25
+ expect(conversation).to route_to_intent('MyCoolIntent')
26
+ expect(conversation).to have_slot(name: 'Age', value: '21')
27
+ end
28
+
29
+ # if you'd rather create your own event from scratch
30
+ it 'creates an event' do
31
+ simulator = Aws::Lex::Conversation::Simulator.new
32
+ simulator
33
+ .transcript('I am 21 years old.')
34
+ .input_mode('Speech')
35
+ .context(name: 'WelcomeGreetingCompleted')
36
+ .invocation_source('FulfillmentCodeHook')
37
+ .session(username: 'jane.doe')
38
+ .intent(
39
+ name: 'GuessZodiacSign',
40
+ state: 'ReadyForFulfillment',
41
+ slots: {
42
+ age: {
43
+ value: '21'
44
+ }
45
+ }
46
+ )
47
+ event = simulator.event
48
+
49
+ expect(event).to have_transcript('I am 21 years old.')
50
+ expect(event).to have_input_mode('Speech')
51
+ expect(event).to have_active_context(name: 'WelcomeGreetingCompleted')
52
+ expect(event).to have_invocation_source('FulfillmentCodeHook')
53
+ expect(event).to route_to_intent('GuessZodiacSign')
54
+ expect(event).to have_slot(name: 'age', value: '21')
55
+ expect(event).to include_session_values(username: 'jane.doe')
56
+ end
57
+ ```
58
+ * Add a few convenience methods to `Aws::Lex::Conversation` instances for dealing with active contexts:
59
+ - `#active_context(name:)`:
60
+
61
+ Returns the active context instance that matches the name parameter.
62
+
63
+ - `#active_context?(name:)`:
64
+
65
+ Returns true/false depending on if an active context matching
66
+ the name parameter is found.
67
+
68
+ - `#active_context!(name:, turns:, seconds:, attributes:)`:
69
+
70
+ Creates or updates an existing active context instance for
71
+ the conversation.
72
+
73
+ # 5.1.0 - Sept 2, 2021
74
+
75
+ * Allow the intent to be specified when returning a response such as `elicit_slot`.
76
+
77
+ # 5.0.0 - August 30, 2021
78
+
79
+ * **breaking change** - `Aws::Lex::Conversation::Support::Mixins::SlotElicitation`
80
+ - Rename the `message` attribute to `messages`. This attribute must be a callable that returns and array of `Aws::Lex::Conversation::Type::Message` instances.
81
+ - rename the `follow_up_message` attribute to `follow_up_messages`. This must also be a callable that returns an array of message instances.
82
+ * 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.
83
+
1
84
  # 4.3.0 - August 25, 2021
2
85
 
3
86
  * 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
@@ -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,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)
@@ -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.0.0'
7
7
  end
8
8
  end
9
9
  end
@@ -91,6 +91,35 @@ 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
+
94
123
  def stash
95
124
  @stash ||= {}
96
125
  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.0.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-07 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