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 +4 -4
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +83 -0
- data/README.md +56 -0
- data/lib/aws/lex/conversation/simulator.rb +188 -0
- data/lib/aws/lex/conversation/spec/matchers.rb +236 -0
- data/lib/aws/lex/conversation/spec.rb +21 -0
- data/lib/aws/lex/conversation/type/base.rb +8 -3
- data/lib/aws/lex/conversation/type/bot.rb +1 -0
- data/lib/aws/lex/conversation/type/checkpoint.rb +6 -0
- data/lib/aws/lex/conversation/type/context.rb +1 -1
- data/lib/aws/lex/conversation/type/event.rb +5 -5
- data/lib/aws/lex/conversation/type/intent.rb +1 -1
- data/lib/aws/lex/conversation/type/slot.rb +15 -4
- data/lib/aws/lex/conversation/version.rb +1 -1
- data/lib/aws/lex/conversation.rb +29 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c078b6b854b3df1824942655b7dd763f72dcc1a439c61e0830e1a644a0c2a65
|
4
|
+
data.tar.gz: 8f279c911fceacdc67005e02a09b04bd854e71f30d1212a92ca9d6aff5fe1f75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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,
|
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}",
|
86
|
+
instance_variable_set("@#{attribute}", block.call(self))
|
82
87
|
end
|
83
88
|
end
|
84
89
|
|
@@ -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
|
@@ -18,21 +18,21 @@ module Aws
|
|
18
18
|
required :session_id
|
19
19
|
required :session_state
|
20
20
|
|
21
|
-
computed_property
|
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 }
|
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
|
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 }
|
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
|
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
|
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
|
data/lib/aws/lex/conversation.rb
CHANGED
@@ -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:
|
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-
|
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
|