Skip to content

Commit

Permalink
[WIP] NLU Rate Limits
Browse files Browse the repository at this point in the history
  • Loading branch information
caiosba committed Jan 25, 2024
1 parent b0d41d4 commit 9153189
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 25 deletions.
69 changes: 53 additions & 16 deletions app/lib/smooch_nlu.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class SmoochNlu
class SmoochBotNotInstalledError < ::ArgumentError
end
class SmoochBotNotInstalledError < ::ArgumentError; end
class SmoochNluError < ::StandardError; end

# FIXME: Make it more flexible
# FIXME: Once we support paraphrase-multilingual-mpnet-base-v2 make it the only model used
Expand All @@ -10,6 +10,8 @@ class SmoochBotNotInstalledError < ::ArgumentError
Bot::Alegre::PARAPHRASE_MULTILINGUAL_MODEL => 0.6
}

NLU_GLOBAL_COUNTER_KEY = 'nlu_global_counter'

include SmoochNluMenus

def initialize(team_slug)
Expand Down Expand Up @@ -59,13 +61,25 @@ def self.disambiguation_threshold
CheckConfig.get('nlu_disambiguation_threshold', 0.11, :float).to_f
end

def self.alegre_matches_from_message(message, language, context, alegre_result_key)
def self.alegre_matches_from_message(message, language, context, alegre_result_key, uid)
# FIXME: Raise exception if not in a tipline context (so, if Bot::Smooch.config is nil)
matches = []
team_slug = Team.find(Bot::Smooch.config['team_id']).slug
params = nil
response = nil
if Bot::Smooch.config.to_h['nlu_enabled']
unless Bot::Smooch.config.to_h['nlu_enabled']
return []
end
if self.nlu_global_rate_limit_reached?
CheckSentry.notify(SmoochNluError.new('NLU global rate limit reached.'))
return []
end
if self.nlu_user_rate_limit_reached?(uid)
CheckSentry.notify(SmoochNluError.new('NLU user rate limit reached.'), user_id: uid)
return []
end
begin
self.increment_global_counter
# FIXME: In the future we could consider matches across all languages when options is nil
# FIXME: No need to call Alegre if it's an exact match to one of the keywords
# FIXME: No need to call Alegre if message has no word characters
Expand Down Expand Up @@ -94,22 +108,45 @@ def self.alegre_matches_from_message(message, language, context, alegre_result_k
ranked_options = sorted_options.map{ |o| { 'key' => o.dig('_source', 'context', alegre_result_key), 'score' => o['_score'] } }
matches = ranked_options

# FIXME: Deal with ties (i.e., where two options have an equal _score or count)
# In all cases log for analysis
log = {
version: '0.1', # Update if schema changes
datetime: DateTime.current,
team_slug: team_slug,
user_query: message,
alegre_query: params,
alegre_response: response,
matches: matches
}
Rails.logger.info("[Smooch NLU] [Matches From Message] #{log.to_json}")
rescue StandardError => e
CheckSentry.notify(SmoochNluError.new("NLU exception: #{e.message}"), exception: e)
matches = []
ensure
self.decrement_global_counter
end
# In all cases log for analysis
log = {
version: "0.1", # Update if schema changes
datetime: DateTime.current,
team_slug: team_slug,
user_query: message,
alegre_query: params,
alegre_response: response,
matches: matches
}
Rails.logger.info("[Smooch NLU] [Matches From Message] #{log.to_json}")
matches
end

def self.nlu_global_rate_limit_reached?
redis = Redis.new(REDIS_CONFIG)
redis.get(NLU_GLOBAL_COUNTER_KEY).to_i > CheckConfig.get('nlu_global_rate_limit', 100, :integer)
end

def self.nlu_user_rate_limit_reached?(uid)
TiplineMessage.where(uid: uid, created_at: Time.now.ago(1.minute)..Time.now, state: 'received').count > CheckConfig.get('nlu_user_rate_limit', 30, :integer)
end

def self.increment_global_counter
redis = Redis.new(REDIS_CONFIG)
redis.incr(NLU_GLOBAL_COUNTER_KEY)
end

def self.decrement_global_counter
redis = Redis.new(REDIS_CONFIG)
redis.decr(NLU_GLOBAL_COUNTER_KEY)
end

private

def toggle!(enabled)
Expand Down
4 changes: 2 additions & 2 deletions app/lib/smooch_nlu_menus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ def update_menu_option_keywords(language, menu, menu_option_index, keyword, oper
end

module ClassMethods
def menu_options_from_message(message, language, options)
def menu_options_from_message(message, language, options, uid)
return [{ 'smooch_menu_option_value' => 'main_state' }] if message == 'cancel_nlu'
return [] if options.blank?
context = {
context: ALEGRE_CONTEXT_KEY_MENU
}
matches = SmoochNlu.alegre_matches_from_message(message, language, context, 'menu_option_id')
matches = SmoochNlu.alegre_matches_from_message(message, language, context, 'menu_option_id', uid)
# Select the top two menu options that exists in `options`
top_options = []
matches.each do |r|
Expand Down
6 changes: 3 additions & 3 deletions app/models/bot/smooch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ def self.run(body)
'capi:verification'
when 'message:appUser'
json['messages'].each do |message|
self.parse_message(message, json['app']['_id'], json)
SmoochTiplineMessageWorker.perform_async(message, json)
self.parse_message(message, json['app']['_id'], json)
end
true
when 'message:delivery:failure'
Expand Down Expand Up @@ -570,12 +570,12 @@ def self.process_menu_option(message, state, app_id)
end
# ...if nothing is matched, try using the NLU feature
if state != 'query'
options = SmoochNlu.menu_options_from_message(typed, language, options)
options = SmoochNlu.menu_options_from_message(typed, language, options, uid)
unless options.blank?
SmoochNlu.process_menu_options(uid, options, message, language, workflow, app_id)
return true
end
resource = TiplineResource.resource_from_message(typed, language)
resource = TiplineResource.resource_from_message(typed, language, uid)
unless resource.nil?
CheckStateMachine.new(uid).reset
resource = self.send_resource_to_user(uid, workflow, resource.uuid, language)
Expand Down
4 changes: 2 additions & 2 deletions app/models/concerns/tipline_resource_nlu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ def update_resource_keywords(keyword, operation)
end

module ClassMethods
def resource_from_message(message, language)
def resource_from_message(message, language, uid)
context = {
context: ALEGRE_CONTEXT_KEY_RESOURCE
}
matches = SmoochNlu.alegre_matches_from_message(message, language, context, 'resource_id').collect{ |m| m['key'] }
matches = SmoochNlu.alegre_matches_from_message(message, language, context, 'resource_id', uid).collect{ |m| m['key'] }
# Select the top resource that exists
resource_id = matches.find { |id| TiplineResource.where(id: id).exists? }
Rails.logger.info("[Smooch NLU] [Resource From Message] Resource ID: #{resource_id} | Message: #{message}")
Expand Down
6 changes: 4 additions & 2 deletions config/config.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,14 @@ development: &default
otel_traces_sampler:
otel_custom_sampling_rate:

# Rate limit for tipline submissions, tipline users are blocked after reaching this limit
# Rate limits for tiplines
#
# OPTIONAL
# When not set, a default number will be used.
# When not set, default values are used.
#
tipline_user_max_messages_per_day: 1500
nlu_global_rate_limit: 100
nlu_user_rate_limit: 30

test:
<<: *default
Expand Down

0 comments on commit 9153189

Please sign in to comment.