diff --git a/app/graph/mutations/nlu_mutations.rb b/app/graph/mutations/nlu_mutations.rb new file mode 100644 index 0000000000..0a98b8b239 --- /dev/null +++ b/app/graph/mutations/nlu_mutations.rb @@ -0,0 +1,41 @@ +module NluMutations + class ToggleKeywordInTiplineMenu < Mutations::BaseMutation + argument :language, GraphQL::Types::String, required: true + argument :keyword, GraphQL::Types::String, required: true + argument :menu, GraphQL::Types::String, required: true # "main" or "secondary" + argument :menu_option_index, GraphQL::Types::Int, required: true # zero-based... the order is the same displayed in the tipline and in the tipline settings page + + field :success, GraphQL::Types::Boolean, null: true + + def resolve(language:, menu:, menu_option_index:, keyword:) + begin + if User.current.is_admin + nlu = SmoochNlu.new(Team.current.slug) + nlu.enable! + if toggle == :add + nlu.add_keyword_to_menu_option(language, menu, menu_option_index, keyword) + elsif toggle == :remove + nlu.remove_keyword_from_menu_option(language, menu, menu_option_index, keyword) + end + { success: true } + else + { success: false } + end + rescue + { success: false } + end + end + end + + class AddKeywordToTiplineMenu < ToggleKeywordInTiplineMenu + def toggle + :add + end + end + + class RemoveKeywordFromTiplineMenu < ToggleKeywordInTiplineMenu + def toggle + :remove + end + end +end diff --git a/app/graph/types/mutation_type.rb b/app/graph/types/mutation_type.rb index ffc94151fb..353cdbf0f9 100644 --- a/app/graph/types/mutation_type.rb +++ b/app/graph/types/mutation_type.rb @@ -158,4 +158,7 @@ class MutationType < BaseObject field :destroyTiplineResource, mutation: TiplineResourceMutations::Destroy field :sendTiplineMessage, mutation: TiplineMessageMutations::Send + + field :addNluKeywordToTiplineMenu, mutation: NluMutations::AddKeywordToTiplineMenu + field :removeNluKeywordFromTiplineMenu, mutation: NluMutations::RemoveKeywordFromTiplineMenu end diff --git a/app/lib/smooch_nlu.rb b/app/lib/smooch_nlu.rb index cf7b3e1b76..7144e9cc73 100644 --- a/app/lib/smooch_nlu.rb +++ b/app/lib/smooch_nlu.rb @@ -5,7 +5,7 @@ class SmoochBotNotInstalledError < ::ArgumentError # FIXME: Make it more flexible # FIXME: Once we support paraphrase-multilingual-mpnet-base-v2 make it the only model used ALEGRE_MODELS_AND_THRESHOLDS = { - # Bot::Alegre::ELASTICSEARCH_MODEL => 0.8, Sometimes this is easier for local development + # Bot::Alegre::ELASTICSEARCH_MODEL => 0.8 # , Sometimes this is easier for local development Bot::Alegre::OPENAI_ADA_MODEL => 0.8, Bot::Alegre::MEAN_TOKENS_MODEL => 0.6 } diff --git a/app/lib/smooch_nlu_menus.rb b/app/lib/smooch_nlu_menus.rb index 3857f1b6e0..236483e6a4 100644 --- a/app/lib/smooch_nlu_menus.rb +++ b/app/lib/smooch_nlu_menus.rb @@ -13,7 +13,7 @@ def remove_keyword_from_menu_option(language, menu, menu_option_index, keyword) update_menu_option_keywords(language, menu, menu_option_index, keyword, 'remove') end - def list_menu_keywords(languages = nil, menus = nil) + def list_menu_keywords(languages = nil, menus = nil, include_empty = true) if languages.nil? languages = @smooch_bot_installation.get_smooch_workflows.map { |w| w['smooch_workflow_language'] } elsif languages.is_a? String @@ -33,12 +33,13 @@ def list_menu_keywords(languages = nil, menus = nil) output[language][menu] = [] i = 0 workflow.fetch("smooch_state_#{menu}",{}).fetch('smooch_menu_options', []).each do |option| + keywords = option.dig('smooch_menu_option_nlu_keywords').to_a output[language][menu] << { 'index' => i, 'title' => option.dig('smooch_menu_option_label'), - 'keywords' => option.dig('smooch_menu_option_nlu_keywords').to_a, + 'keywords' => keywords, 'id' => option.dig('smooch_menu_option_id'), - } + } if include_empty || !keywords.blank? i += 1 end end diff --git a/lib/relay.idl b/lib/relay.idl index a5ad34e0b5..21d3f58ea7 100644 --- a/lib/relay.idl +++ b/lib/relay.idl @@ -257,6 +257,31 @@ type AddFilesToTaskPayload { task: Task } +""" +Autogenerated input type of AddKeywordToTiplineMenu +""" +input AddKeywordToTiplineMenuInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + keyword: String! + language: String! + menu: String! + menuOptionIndex: Int! +} + +""" +Autogenerated return type of AddKeywordToTiplineMenu +""" +type AddKeywordToTiplineMenuPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + success: Boolean +} + type Annotation implements Node { annotated_id: String annotated_type: String @@ -8911,6 +8936,12 @@ type MutationType { """ input: AddFilesToTaskInput! ): AddFilesToTaskPayload + addNluKeywordToTiplineMenu( + """ + Parameters for AddKeywordToTiplineMenu + """ + input: AddKeywordToTiplineMenuInput! + ): AddKeywordToTiplineMenuPayload """ Allow multiple items to be marked as read or unread. @@ -9737,6 +9768,12 @@ type MutationType { """ input: RemoveFilesFromTaskInput! ): RemoveFilesFromTaskPayload + removeNluKeywordFromTiplineMenu( + """ + Parameters for RemoveKeywordFromTiplineMenu + """ + input: RemoveKeywordFromTiplineMenuInput! + ): RemoveKeywordFromTiplineMenuPayload replaceProjectMedia( """ Parameters for ReplaceProjectMedia @@ -11858,6 +11895,31 @@ type RemoveFilesFromTaskPayload { task: Task } +""" +Autogenerated input type of RemoveKeywordFromTiplineMenu +""" +input RemoveKeywordFromTiplineMenuInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + keyword: String! + language: String! + menu: String! + menuOptionIndex: Int! +} + +""" +Autogenerated return type of RemoveKeywordFromTiplineMenu +""" +type RemoveKeywordFromTiplineMenuPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + success: Boolean +} + """ Autogenerated input type of ReplaceProjectMedia """ diff --git a/public/relay.json b/public/relay.json index d10ba20745..4d93e551ed 100644 --- a/public/relay.json +++ b/public/relay.json @@ -1073,6 +1073,134 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "AddKeywordToTiplineMenuInput", + "description": "Autogenerated input type of AddKeywordToTiplineMenu", + "fields": null, + "inputFields": [ + { + "name": "language", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "keyword", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "menu", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "menuOptionIndex", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "AddKeywordToTiplineMenuPayload", + "description": "Autogenerated return type of AddKeywordToTiplineMenu", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "success", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Annotation", @@ -48407,6 +48535,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "addNluKeywordToTiplineMenu", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for AddKeywordToTiplineMenu", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "AddKeywordToTiplineMenuInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "AddKeywordToTiplineMenuPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "bulkProjectMediaMarkRead", "description": "Allow multiple items to be marked as read or unread.", @@ -52380,6 +52537,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "removeNluKeywordFromTiplineMenu", + "description": null, + "args": [ + { + "name": "input", + "description": "Parameters for RemoveKeywordFromTiplineMenu", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RemoveKeywordFromTiplineMenuInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RemoveKeywordFromTiplineMenuPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "replaceProjectMedia", "description": null, @@ -62364,6 +62550,134 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "RemoveKeywordFromTiplineMenuInput", + "description": "Autogenerated input type of RemoveKeywordFromTiplineMenu", + "fields": null, + "inputFields": [ + { + "name": "language", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "keyword", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "menu", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "menuOptionIndex", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RemoveKeywordFromTiplineMenuPayload", + "description": "Autogenerated return type of RemoveKeywordFromTiplineMenu", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "success", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "ReplaceProjectMediaInput", diff --git a/test/controllers/graphql_controller_10_test.rb b/test/controllers/graphql_controller_10_test.rb index 4d256a7024..35c4c331fb 100644 --- a/test/controllers/graphql_controller_10_test.rb +++ b/test/controllers/graphql_controller_10_test.rb @@ -808,4 +808,70 @@ def setup assert_response :success assert !JSON.parse(@response.body)['data']['sendTiplineMessage']['success'] end + + test "should add NLU keyword to tipline menu option" do + SmoochNlu.any_instance.stubs(:enable!).once + SmoochNlu.any_instance.stubs(:add_keyword_to_menu_option).once + SmoochNlu.any_instance.stubs(:remove_keyword_from_menu_option).never + u = create_user is_admin: true + t = create_team + b = create_team_bot name: 'Smooch', login: 'smooch', set_approved: true + b.install_to!(t) + authenticate_with_user(u) + + query = "mutation { addNluKeywordToTiplineMenu(input: { language: \"en\", menu: \"main\", menuOptionIndex: 0, keyword: \"Foo bar\" }) { success } }" + post :create, params: { query: query, team: t.slug } + + assert_response :success + assert JSON.parse(@response.body)['data']['addNluKeywordToTiplineMenu']['success'] + end + + test "should remove NLU keyword from tipline menu option" do + SmoochNlu.any_instance.stubs(:enable!).once + SmoochNlu.any_instance.stubs(:add_keyword_to_menu_option).never + SmoochNlu.any_instance.stubs(:remove_keyword_from_menu_option).once + u = create_user is_admin: true + t = create_team + b = create_team_bot name: 'Smooch', login: 'smooch', set_approved: true + b.install_to!(t) + authenticate_with_user(u) + + query = "mutation { removeNluKeywordFromTiplineMenu(input: { language: \"en\", menu: \"main\", menuOptionIndex: 0, keyword: \"Foo bar\" }) { success } }" + post :create, params: { query: query, team: t.slug } + + assert_response :success + assert JSON.parse(@response.body)['data']['removeNluKeywordFromTiplineMenu']['success'] + end + + test "should not change tipline menu option NLU keywords if it's not a super-admin" do + SmoochNlu.any_instance.stubs(:enable!).never + SmoochNlu.any_instance.stubs(:add_keyword_to_menu_option).never + SmoochNlu.any_instance.stubs(:remove_keyword_from_menu_option).never + u = create_user is_admin: false + t = create_team + b = create_team_bot name: 'Smooch', login: 'smooch', set_approved: true + b.install_to!(t) + authenticate_with_user(u) + + query = "mutation { addNluKeywordToTiplineMenu(input: { language: \"en\", menu: \"main\", menuOptionIndex: 0, keyword: \"Foo bar\" }) { success } }" + post :create, params: { query: query, team: t.slug } + + assert_response :success + assert !JSON.parse(@response.body)['data']['addNluKeywordToTiplineMenu']['success'] + end + + test "should not change tipline menu option NLU keywords if tipline is not installed" do + SmoochNlu.any_instance.stubs(:enable!).never + SmoochNlu.any_instance.stubs(:add_keyword_to_menu_option).never + SmoochNlu.any_instance.stubs(:remove_keyword_from_menu_option).never + u = create_user is_admin: true + t = create_team + authenticate_with_user(u) + + query = "mutation { addNluKeywordToTiplineMenu(input: { language: \"en\", menu: \"main\", menuOptionIndex: 0, keyword: \"Foo bar\" }) { success } }" + post :create, params: { query: query, team: t.slug } + + assert_response :success + assert !JSON.parse(@response.body)['data']['addNluKeywordToTiplineMenu']['success'] + end end