aws-lex-conversation 1.0.0 → 2.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: 148e207d8dcce051f2fc01244390021ae5734037a3b856462cd76ccd9e4cde8a
4
- data.tar.gz: 7f019bdc51b68e037575a60784dbe76217aabccdf15e3394515d4a404dbdd85a
3
+ metadata.gz: 3d7e1994eb1948eceb9d236d65c0cc449e5120b6c91fb86585a7225e660cbc84
4
+ data.tar.gz: 071c8e344b1c6d78597986f679a7446250d2d2d75a001d4af7d393043c567529
5
5
  SHA512:
6
- metadata.gz: 856f7ab5e25ffc87f93f3ee6f2b834acdd47cbc28adf334a22af4b152b37de435f54d3715374f33acfdf4ceebca1e2945b3557b5f72c4c5873e4b8761af5758a
7
- data.tar.gz: e6dff0de003696ef87460e6dfdb54b94e470d4ff5b6470f43642dd259fa13b645b276d5d88413369dcacc7368004f4c93d92f38c1c50a5a18487c6cb2210e62d
6
+ metadata.gz: c83c69dfb06fd6faa16af4dfeb37119b84054c473e11929f201b34b419aba071918405635587ad38b21ce6d97862f60f1e360d9867059942deaea348a9eab176
7
+ data.tar.gz: f3cb8c0162884104b0ccb6f921c3cacdd08412d83b32e2463178b0a945b8e795de8da9bb99eabaaaf814de3b19b930e0a59972aea0dd128d8ca29d0da7e05982
@@ -9,6 +9,14 @@ Documentation:
9
9
  Enabled: false
10
10
  Gemspec/RequiredRubyVersion:
11
11
  Enabled: false
12
+ Layout/FirstArrayElementIndentation:
13
+ Enabled: false
14
+ Layout/MultilineMethodCallIndentation:
15
+ Enabled: false
16
+ Layout/SpaceAroundOperators:
17
+ Enabled: false
18
+ Metrics/AbcSize:
19
+ Max: 18
12
20
  Metrics/ClassLength:
13
21
  Max: 150
14
22
  Metrics/BlockLength:
@@ -27,3 +35,5 @@ Style/Lambda:
27
35
  EnforcedStyle: literal
28
36
  Style/GuardClause:
29
37
  Enabled: false
38
+ Style/SlicingWithRange:
39
+ Enabled: false
@@ -0,0 +1,24 @@
1
+ # 2.0.0 - August 19, 2020
2
+
3
+ * **breaking change:** Rename `Aws::Lex::Conversation::Type::CurrentIntent` to `Aws::Lex::Conversation::Type::Intent`.
4
+ * **breaking change:** Built-in handlers now default the `options` attribute to an empty hash.
5
+ * Add Lex NLU model improvement functionality (see: https://aws.amazon.com/about-aws/whats-new/2020/08/amazon-lex-launches-accuracy-improvements-and-confidence-scores/).
6
+ * Add the `intent_confidence` method to the conversation class that may be used as follows:
7
+
8
+ ```ruby
9
+ # NOTE: Lex model improvements must be enabled on the bot to get confidence data.
10
+ # SEE: https://aws.amazon.com/about-aws/whats-new/2020/08/amazon-lex-launches-accuracy-improvements-and-confidence-scores/
11
+ conversation.intent_confidence.ambiguous? # true/false
12
+ conversation.intent_confidence.unambiguous? # true/false
13
+ conversation.intent_confidence.candidates # [...] the array contains the current_intent and all similar intents
14
+ conversation.intent_confidence.similar_alternates # [...] the array doesn't contain the current_intent
15
+ ```
16
+
17
+ * The calculation used to determine intent ambiguity by default looks for confidence scores that are within a standard deviation of the current intent's confidence score.
18
+ * You can pass your own static `threshold` parameter if you wish to change this behaviour:
19
+
20
+ ```ruby
21
+ conversation.intent_confidence.ambiguous?(threshold: 0.4) # true/false
22
+ ```
23
+
24
+ * Implement a built-in `SlotResolution` handler that is intended to act as the initial handler in the chain. This handler will resolve all slot values to their top resolution, then call the successor handler.
data/README.md CHANGED
@@ -58,13 +58,13 @@ The first handler that returns `true` for the `will_respond?` method will provid
58
58
  ```ruby
59
59
  class SayHello < Aws::Lex::Conversation::Handler::Base
60
60
  def will_respond?(conversation)
61
- conversation.lex.incovation_source.dialog_code_hook? && # callback is for DialogCodeHook (i.e. validation)
61
+ conversation.lex.invocation_source.dialog_code_hook? && # callback is for DialogCodeHook (i.e. validation)
62
62
  conversation.lex.current_intent.name == 'SayHello' && # Lex has routed to the 'SayHello' intent
63
- conversation.slots[:name] # our expected slot value is set
63
+ conversation.slots[:name].filled? # our expected slot value is set
64
64
  end
65
65
 
66
66
  def response(conversation)
67
- name = conversation.slots[:name]
67
+ name = conversation.slots[:name].value
68
68
 
69
69
  # NOTE: you can use the Type::* classes if you wish. The final output
70
70
  # will be normalized to a value that complies with the Lex response format.
@@ -141,6 +141,34 @@ conversation.handlers = [
141
141
  conversation.respond # => { dialogAction: { type: 'Delegate' } }
142
142
  ```
143
143
 
144
+ ### `Aws::Lex::Conversation::Handler::SlotResolution`
145
+
146
+ This handler will set all slot values equal to their top resolution in the input event. The handler then calls the next handler in the chain for a response.
147
+
148
+ **NOTE:** This handler must not be the final handler in the chain. An exception of type `Aws::Lex::Conversation::Exception::MissingHandler` will be raised if there is no successor handler.
149
+
150
+ | Option | Required | Description | Default Value |
151
+ |------------------|----------|--------------------------------------------------------------|-------------------------------------|
152
+ | respond_on | No | A callable that provides the condition for `will_handle?`. | `->(c) { true }` |
153
+
154
+ i.e.
155
+
156
+ ```ruby
157
+ conversation = Aws::Lex::Conversation.new(event: event, context: context)
158
+ conversation.handlers = [
159
+ {
160
+ handler: Aws::Lex::Conversation::Handler::SlotResolution
161
+ },
162
+ {
163
+ handler: Aws::Lex::Conversation::Handler::Delegate,
164
+ options: {
165
+ respond_on: ->(c) { true }
166
+ }
167
+ }
168
+ ]
169
+ conversation.respond # => { dialogAction: { type: 'Delegate' } }
170
+ ```
171
+
144
172
  ## Development
145
173
 
146
174
  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.
@@ -42,6 +42,10 @@ module Aws
42
42
  chain.first.handle(self)
43
43
  end
44
44
 
45
+ def intent_confidence
46
+ @intent_confidence ||= Type::IntentConfidence.new(event: lex)
47
+ end
48
+
45
49
  def intent_name
46
50
  lex.current_intent.name
47
51
  end
@@ -4,6 +4,7 @@ require 'json'
4
4
  require 'shrink/wrap'
5
5
 
6
6
  require_relative 'version'
7
+ require_relative 'exception/missing_handler'
7
8
  require_relative 'response/base'
8
9
  require_relative 'response/close'
9
10
  require_relative 'response/confirm_intent'
@@ -24,11 +25,12 @@ require_relative 'type/invocation_source'
24
25
  require_relative 'type/dialog_action_type'
25
26
  require_relative 'type/confirmation_status'
26
27
  require_relative 'type/fulfillment_state'
28
+ require_relative 'type/intent_confidence'
27
29
  require_relative 'type/recent_intent_summary_view'
28
30
  require_relative 'type/slot'
29
31
  require_relative 'type/slot_resolution'
30
32
  require_relative 'type/slot_detail'
31
- require_relative 'type/current_intent'
33
+ require_relative 'type/intent'
32
34
  require_relative 'type/output_dialog_mode'
33
35
  require_relative 'type/bot'
34
36
  require_relative 'type/message/content_type'
@@ -42,3 +44,4 @@ require_relative 'type/event'
42
44
  require_relative 'handler/base'
43
45
  require_relative 'handler/echo'
44
46
  require_relative 'handler/delegate'
47
+ require_relative 'handler/slot_resolution'
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Exception
7
+ class MissingHandler < StandardError
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -9,7 +9,7 @@ module Aws
9
9
 
10
10
  def initialize(opts = {})
11
11
  self.successor = opts[:successor]
12
- self.options = opts[:options]
12
+ self.options = opts[:options] || {}
13
13
  end
14
14
 
15
15
  def will_respond?(conversation)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Handler
7
+ class SlotResolution < Base
8
+ def will_respond?(conversation)
9
+ # respond by default unless told otherwise
10
+ callable = options.fetch(:respond_on) { ->(_c) { true } }
11
+ callable.call(conversation)
12
+ end
13
+
14
+ def response(conversation)
15
+ # resolve all slots to their top resolution
16
+ conversation.slots.values.each(&:resolve!)
17
+
18
+ unless successor
19
+ msg = 'Handler `SlotResolution` must not be the final handler in the chain'
20
+ raise Exception::MissingHandler, msg
21
+ end
22
+
23
+ # call the next handler in the chain
24
+ successor.handle(conversation)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -45,7 +45,11 @@ module Aws
45
45
  end
46
46
 
47
47
  def elicitation_content
48
- first_elicitation? ? message : follow_up_message
48
+ first_elicitation? ? compose_message(message) : compose_message(follow_up_message)
49
+ end
50
+
51
+ def compose_message(msg)
52
+ msg.is_a?(Proc) ? msg.call(conversation) : msg
49
53
  end
50
54
 
51
55
  def increment_slot_elicitations!
@@ -19,7 +19,8 @@ module Aws
19
19
 
20
20
  module InstanceMethods
21
21
  def assign_attributes!(opts = {})
22
- self.class.attributes.each do |attribute|
22
+ attributes = self.class.attributes | self.class.virtual_attributes
23
+ attributes.each do |attribute|
23
24
  instance_variable_set("@#{attribute}", opts[attribute])
24
25
  end
25
26
  end
@@ -52,14 +53,29 @@ module Aws
52
53
  end
53
54
 
54
55
  module ClassMethods
55
- def integer!
56
- ->(v) { v.to_i }
56
+ def integer!(nilable: false)
57
+ nilable ? ->(v) { v&.to_i } : ->(v) { v.to_i }
58
+ end
59
+
60
+ def float!(nilable: false)
61
+ nilable ? ->(v) { v&.to_f } : ->(v) { v.to_f }
57
62
  end
58
63
 
59
64
  def symbolize_hash!
60
65
  ->(v) { v.transform_keys(&:to_sym) }
61
66
  end
62
67
 
68
+ def computed_property(attribute, callable)
69
+ mapping[attribute] = attribute
70
+ attr_writer(attribute)
71
+
72
+ # dynamically memoize the result
73
+ define_method(attribute) do
74
+ instance_variable_get("@#{attribute}") ||
75
+ instance_variable_set("@#{attribute}", callable.call(self))
76
+ end
77
+ end
78
+
63
79
  def required(attribute, opts = {})
64
80
  property(attribute, opts.merge(allow_nil: false))
65
81
  end
@@ -76,7 +92,12 @@ module Aws
76
92
 
77
93
  attr_accessor(attribute)
78
94
 
79
- mapping[attribute] = from
95
+ if opts.fetch(:virtual) { false }
96
+ virtual_attributes << attribute
97
+ else
98
+ mapping[attribute] = from
99
+ end
100
+
80
101
  translate(attribute => params)
81
102
  end
82
103
 
@@ -84,6 +105,10 @@ module Aws
84
105
  @attributes ||= mapping.keys
85
106
  end
86
107
 
108
+ def virtual_attributes
109
+ @virtual_attributes ||= []
110
+ end
111
+
87
112
  def mapping
88
113
  @mapping ||= {}
89
114
  end
@@ -7,6 +7,7 @@ module Aws
7
7
  class Event
8
8
  include Base
9
9
 
10
+ required :alternative_intents, default: -> { [] }
10
11
  required :current_intent
11
12
  required :bot
12
13
  required :user_id
@@ -20,8 +21,13 @@ module Aws
20
21
  optional :sentiment_response
21
22
  optional :kendra_response
22
23
 
24
+ computed_property :intents, ->(instance) do
25
+ [instance.current_intent] | instance.alternative_intents
26
+ end
27
+
23
28
  coerce(
24
- current_intent: CurrentIntent,
29
+ alternative_intents: Array[Intent],
30
+ current_intent: Intent,
25
31
  bot: Bot,
26
32
  invocation_source: InvocationSource,
27
33
  output_dialog_mode: OutputDialogMode,
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Type
7
+ class Intent
8
+ include Base
9
+
10
+ required :name
11
+ required :raw_slots, from: :slots, virtual: true
12
+ required :slot_details
13
+ required :confirmation_status
14
+ optional :nlu_intent_confidence_score
15
+
16
+ computed_property :slots, ->(instance) do
17
+ instance.raw_slots.each_with_object({}) do |(key, value), hash|
18
+ hash[key.to_sym] = Slot.shrink_wrap(
19
+ name: key,
20
+ value: value,
21
+ # pass a reference to the parent down to the slot so that each slot
22
+ # instance can view a broader scope such as slot_details/resolutions
23
+ current_intent: instance
24
+ )
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def slot_details!
30
+ ->(val) do
31
+ val
32
+ .reject { |_, v| v.nil? }
33
+ .each_with_object({}) do |(key, value), hash|
34
+ hash[key.to_sym] = SlotDetail.shrink_wrap(value)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ coerce(
41
+ slot_details: slot_details!,
42
+ confirmation_status: ConfirmationStatus,
43
+ nlu_intent_confidence_score: float!(nilable: true)
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Type
7
+ class IntentConfidence
8
+ include Base
9
+
10
+ required :event
11
+
12
+ def ambiguous?(threshold: standard_deviation)
13
+ candidates(threshold: threshold).size > 1
14
+ end
15
+
16
+ def unambiguous?(threshold: standard_deviation)
17
+ !ambiguous?(threshold: threshold)
18
+ end
19
+
20
+ # NOTE: by default this method looks for candidates
21
+ # with a confidence score within one standard deviation
22
+ # of the current intent. Confidence scores may not be
23
+ # normally distributed, so it's very possible that this
24
+ # method will return abnormal results for skewed sample sets.
25
+ #
26
+ # If you want a consistent threshold for the condition, pass
27
+ # a static `threshold` parameter.
28
+ def candidates(threshold: standard_deviation)
29
+ intents = event.intents.select do |intent|
30
+ diff = event.current_intent
31
+ .nlu_intent_confidence_score
32
+ .to_f
33
+ .-(intent.nlu_intent_confidence_score.to_f)
34
+ .abs
35
+
36
+ diff <= threshold
37
+ end
38
+
39
+ # sort descending
40
+ intents.sort do |a, b|
41
+ b.nlu_intent_confidence_score.to_f <=> a.nlu_intent_confidence_score.to_f
42
+ end
43
+ end
44
+
45
+ def mean
46
+ @mean ||= calculate_mean
47
+ end
48
+
49
+ def similar_alternates(threshold: standard_deviation)
50
+ # remove the first element (current intent) from consideration
51
+ candidates(threshold: threshold)[1..-1]
52
+ end
53
+
54
+ def standard_deviation
55
+ @standard_deviation ||= calculate_standard_deviation
56
+ end
57
+
58
+ private
59
+
60
+ def calculate_mean
61
+ sum = event.intents.sum { |i| i.nlu_intent_confidence_score.to_f }
62
+ sum / event.intents.size
63
+ end
64
+
65
+ def calculate_standard_deviation
66
+ normalized = event.intents.map do |intent|
67
+ (intent.nlu_intent_confidence_score.to_f - mean) ** 2
68
+ end
69
+ normalized_mean = normalized.sum / normalized.size
70
+ Math.sqrt(normalized_mean)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -7,9 +7,14 @@ module Aws
7
7
  class Slot
8
8
  include Base
9
9
 
10
+ required :current_intent, from: :current_intent, virtual: true
10
11
  required :name
11
12
  required :value
12
13
 
14
+ def as_json(_opts = {})
15
+ to_lex
16
+ end
17
+
13
18
  def to_lex
14
19
  value
15
20
  end
@@ -17,6 +22,28 @@ module Aws
17
22
  def filled?
18
23
  value.to_s != ''
19
24
  end
25
+
26
+ def resolve!(index: 0)
27
+ self.value = resolved(index: index)
28
+ end
29
+
30
+ def resolved(index: 0)
31
+ details.resolutions.fetch(index) { SlotResolution.new(value: value) }.value
32
+ end
33
+
34
+ def original_value
35
+ details.original_value
36
+ end
37
+
38
+ def resolvable?
39
+ details.resolutions.any?
40
+ end
41
+
42
+ def details
43
+ @details ||= current_intent.slot_details.fetch(name.to_sym) do
44
+ SlotDetail.new(name: name, resolutions: [], original_value: value)
45
+ end
46
+ end
20
47
  end
21
48
  end
22
49
  end
@@ -3,7 +3,7 @@
3
3
  module Aws
4
4
  module Lex
5
5
  class Conversation
6
- VERSION = '1.0.0'
6
+ VERSION = '2.0.0'
7
7
  end
8
8
  end
9
9
  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: 1.0.0
4
+ version: 2.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: 2020-06-26 00:00:00.000000000 Z
15
+ date: 2020-08-21 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: shrink_wrap
@@ -44,6 +44,7 @@ files:
44
44
  - ".rspec"
45
45
  - ".rubocop.yml"
46
46
  - ".simplecov"
47
+ - CHANGELOG.md
47
48
  - CODE_OF_CONDUCT.md
48
49
  - Gemfile
49
50
  - LICENSE.md
@@ -55,9 +56,11 @@ files:
55
56
  - lib/aws-lex-conversation.rb
56
57
  - lib/aws/lex/conversation.rb
57
58
  - lib/aws/lex/conversation/base.rb
59
+ - lib/aws/lex/conversation/exception/missing_handler.rb
58
60
  - lib/aws/lex/conversation/handler/base.rb
59
61
  - lib/aws/lex/conversation/handler/delegate.rb
60
62
  - lib/aws/lex/conversation/handler/echo.rb
63
+ - lib/aws/lex/conversation/handler/slot_resolution.rb
61
64
  - lib/aws/lex/conversation/response/base.rb
62
65
  - lib/aws/lex/conversation/response/close.rb
63
66
  - lib/aws/lex/conversation/response/confirm_intent.rb
@@ -72,11 +75,12 @@ files:
72
75
  - lib/aws/lex/conversation/type/base.rb
73
76
  - lib/aws/lex/conversation/type/bot.rb
74
77
  - lib/aws/lex/conversation/type/confirmation_status.rb
75
- - lib/aws/lex/conversation/type/current_intent.rb
76
78
  - lib/aws/lex/conversation/type/dialog_action_type.rb
77
79
  - lib/aws/lex/conversation/type/enumeration.rb
78
80
  - lib/aws/lex/conversation/type/event.rb
79
81
  - lib/aws/lex/conversation/type/fulfillment_state.rb
82
+ - lib/aws/lex/conversation/type/intent.rb
83
+ - lib/aws/lex/conversation/type/intent_confidence.rb
80
84
  - lib/aws/lex/conversation/type/invocation_source.rb
81
85
  - lib/aws/lex/conversation/type/message.rb
82
86
  - lib/aws/lex/conversation/type/message/content_type.rb
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Aws
4
- module Lex
5
- class Conversation
6
- module Type
7
- class CurrentIntent
8
- include Base
9
-
10
- required :name
11
- required :slots
12
- required :slot_details
13
- required :confirmation_status
14
-
15
- class << self
16
- def slots!
17
- ->(v) do
18
- v.each_with_object({}) do |(key, value), hash|
19
- hash[key.to_sym] = Slot.shrink_wrap(
20
- name: key,
21
- value: value
22
- )
23
- end
24
- end
25
- end
26
-
27
- def slot_details!
28
- ->(v) do
29
- v.each_with_object({}) do |(key, value), hash|
30
- hash[key.to_sym] = SlotDetail.shrink_wrap(value)
31
- end
32
- end
33
- end
34
- end
35
-
36
- coerce(
37
- slots: slots!,
38
- slot_details: slot_details!,
39
- confirmation_status: ConfirmationStatus
40
- )
41
- end
42
- end
43
- end
44
- end
45
- end