aws-lex-conversation 5.1.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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