From 0fe2bf8647e09f35670dbae1a582715905807b35 Mon Sep 17 00:00:00 2001 From: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Date: Mon, 15 Dec 2025 18:38:21 +0530 Subject: [PATCH] feat: Rewrite APIs for ai editor message improvement (#13059) Co-authored-by: Vishnu Narayanan --- .../commands/useConversationHotKeys.js | 16 ++--- .../i18n/locale/en/integrations.json | 6 +- app/javascript/shared/constants/openai.js | 4 ++ .../integrations/openai_processor_service.rb | 4 +- lib/integrations/llm_base_service.rb | 3 +- .../openai/openai_prompts/tone_rewrite.txt | 19 ++++++ lib/integrations/openai/processor_service.rb | 59 +++++++++++++------ .../openai/processor_service_spec.rb | 26 +++++--- 8 files changed, 99 insertions(+), 38 deletions(-) create mode 100644 lib/integrations/openai/openai_prompts/tone_rewrite.txt diff --git a/app/javascript/dashboard/composables/commands/useConversationHotKeys.js b/app/javascript/dashboard/composables/commands/useConversationHotKeys.js index 30e138165..6ec1c37aa 100644 --- a/app/javascript/dashboard/composables/commands/useConversationHotKeys.js +++ b/app/javascript/dashboard/composables/commands/useConversationHotKeys.js @@ -102,8 +102,8 @@ const createNonDraftMessageAIAssistActions = (t, replyMode) => { const createDraftMessageAIAssistActions = t => { return [ { - label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.REPHRASE'), - key: 'rephrase', + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.CONFIDENT'), + key: 'confident', icon: ICON_AI_ASSIST, }, { @@ -112,13 +112,13 @@ const createDraftMessageAIAssistActions = t => { icon: ICON_AI_GRAMMAR, }, { - label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.EXPAND'), - key: 'expand', + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.PROFESSIONAL'), + key: 'professional', icon: ICON_AI_EXPAND, }, { - label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SHORTEN'), - key: 'shorten', + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.CASUAL'), + key: 'casual', icon: ICON_AI_SHORTEN, }, { @@ -132,8 +132,8 @@ const createDraftMessageAIAssistActions = t => { icon: ICON_AI_ASSIST, }, { - label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.SIMPLIFY'), - key: 'simplify', + label: t('INTEGRATION_SETTINGS.OPEN_AI.OPTIONS.STRAIGHTFORWARD'), + key: 'straightforward', icon: ICON_AI_ASSIST, }, ]; diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index c08cde4d3..c985daed4 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -145,7 +145,11 @@ "EXPAND": "Expand", "MAKE_FRIENDLY": "Change message tone to friendly", "MAKE_FORMAL": "Use formal tone", - "SIMPLIFY": "Simplify" + "SIMPLIFY": "Simplify", + "CONFIDENT": "Use confident tone", + "PROFESSIONAL": "Use professional tone", + "CASUAL": "Use casual tone", + "STRAIGHTFORWARD": "Use straightforward tone" }, "ASSISTANCE_MODAL": { "DRAFT_TITLE": "Draft content", diff --git a/app/javascript/shared/constants/openai.js b/app/javascript/shared/constants/openai.js index 8c5adb875..c7fa7db0c 100644 --- a/app/javascript/shared/constants/openai.js +++ b/app/javascript/shared/constants/openai.js @@ -8,4 +8,8 @@ export const OPEN_AI_OPTIONS = { SIMPLIFY: 'simplify', REPLY_SUGGESTION: 'reply_suggestion', SUMMARIZE: 'summarize', + CASUAL: 'casual', + PROFESSIONAL: 'professional', + STRAIGHTFORWARD: 'straightforward', + CONFIDENT: 'confident', }; diff --git a/enterprise/lib/enterprise/integrations/openai_processor_service.rb b/enterprise/lib/enterprise/integrations/openai_processor_service.rb index 5a98ad4c4..b09c7ca7c 100644 --- a/enterprise/lib/enterprise/integrations/openai_processor_service.rb +++ b/enterprise/lib/enterprise/integrations/openai_processor_service.rb @@ -1,6 +1,6 @@ module Enterprise::Integrations::OpenaiProcessorService - ALLOWED_EVENT_NAMES = %w[rephrase summarize reply_suggestion label_suggestion fix_spelling_grammar shorten expand - make_friendly make_formal simplify].freeze + ALLOWED_EVENT_NAMES = %w[summarize reply_suggestion label_suggestion fix_spelling_grammar + make_friendly make_formal casual professional confident straightforward].freeze CACHEABLE_EVENTS = %w[label_suggestion].freeze def label_suggestion_message diff --git a/lib/integrations/llm_base_service.rb b/lib/integrations/llm_base_service.rb index ca9459fc8..4652ad76f 100644 --- a/lib/integrations/llm_base_service.rb +++ b/lib/integrations/llm_base_service.rb @@ -7,7 +7,8 @@ class Integrations::LlmBaseService # 120000 * 4 = 480,000 characters (rounding off downwards to 400,000 to be safe) TOKEN_LIMIT = 400_000 GPT_MODEL = Llm::Config::DEFAULT_MODEL - ALLOWED_EVENT_NAMES = %w[rephrase summarize reply_suggestion fix_spelling_grammar shorten expand make_friendly make_formal simplify].freeze + ALLOWED_EVENT_NAMES = %w[summarize reply_suggestion fix_spelling_grammar casual professional make_friendly make_formal confident + straightforward].freeze CACHEABLE_EVENTS = %w[].freeze pattr_initialize [:hook!, :event!] diff --git a/lib/integrations/openai/openai_prompts/tone_rewrite.txt b/lib/integrations/openai/openai_prompts/tone_rewrite.txt new file mode 100644 index 000000000..200a8c652 --- /dev/null +++ b/lib/integrations/openai/openai_prompts/tone_rewrite.txt @@ -0,0 +1,19 @@ +You are an AI writing assistant integrated into Chatwoot, an omnichannel customer support platform. Your task is to rewrite customer support message to match a specific tone while preserving the original meaning and intent. + +Here is the tone to apply to the message you will receive: + +%s + + +Your task is to rewrite the message according to the specified tone instructions. + +Important guidelines: +- Preserve the core meaning and all important information from the original message +- Keep the rewritten message concise and appropriate for customer support +- Maintain helpfulness and respect regardless of tone +- Do not add information that wasn't in the original message +- Do not remove critical details or instructions +- Ensure that the reply should be in user language. + +Output only the rewritten message without any preamble, tags or explanation. + diff --git a/lib/integrations/openai/processor_service.rb b/lib/integrations/openai/processor_service.rb index 2f0180701..0f648cf22 100644 --- a/lib/integrations/openai/processor_service.rb +++ b/lib/integrations/openai/processor_service.rb @@ -1,5 +1,4 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService - AGENT_INSTRUCTION = 'You are a helpful support agent.'.freeze LANGUAGE_INSTRUCTION = 'Ensure that the reply should be in user language.'.freeze def reply_suggestion_message make_api_call(reply_suggestion_body) @@ -9,39 +8,39 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService make_api_call(summarize_body) end - def rephrase_message - make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please rephrase the following response. " \ - "#{LANGUAGE_INSTRUCTION}")) + def confident_message + tone_instruction = determine_tone_instruction('confident') + make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction))) end def fix_spelling_grammar_message - make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please fix the spelling and grammar of the following response. " \ + make_api_call(build_api_call_body('Please fix the spelling and grammar of the following response. ' \ "#{LANGUAGE_INSTRUCTION}")) end - def shorten_message - make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please shorten the following response. " \ - "#{LANGUAGE_INSTRUCTION}")) + def straightforward_message + tone_instruction = determine_tone_instruction('straightforward') + make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction))) end - def expand_message - make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please expand the following response. " \ - "#{LANGUAGE_INSTRUCTION}")) + def casual_message + tone_instruction = determine_tone_instruction('casual') + make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction))) end def make_friendly_message - make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please make the following response more friendly. " \ - "#{LANGUAGE_INSTRUCTION}")) + tone_instruction = determine_tone_instruction('friendly') + make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction))) end def make_formal_message - make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please make the following response more formal. " \ - "#{LANGUAGE_INSTRUCTION}")) + tone_instruction = determine_tone_instruction('formal') + make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction))) end - def simplify_message - make_api_call(build_api_call_body("#{AGENT_INSTRUCTION} Please simplify the following response. " \ - "#{LANGUAGE_INSTRUCTION}")) + def professional_message + tone_instruction = determine_tone_instruction('professional') + make_api_call(build_api_call_body(tone_rewrite_prompt(tone_instruction))) end private @@ -51,6 +50,10 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService Rails.root.join(path, "#{file_name}.txt").read end + def tone_rewrite_prompt(tone_instruction) + format(prompt_from_file('tone_rewrite'), tone_instruction) + end + def build_api_call_body(system_content, user_content = event['data']['content']) { model: GPT_MODEL, @@ -133,6 +136,26 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService ].concat(conversation_messages(in_array_format: true)) }.to_json end + + def determine_tone_instruction(tone) + case tone + when 'friendly' + 'Warm, approachable, and personable. Use conversational language, positive words, and show empathy. ' \ + 'May include phrases like \"Happy to help!\" or \"I\'d be glad to...\"' + when 'confident' + 'Assertive and assured. Use definitive language, avoid hedging words like \"maybe\" or \"I think\". ' \ + 'Be direct and authoritative while remaining helpful.' + when 'straightforward' + 'Clear, direct, and to-the-point. Remove unnecessary words, get straight to the information or solution. No fluff or extra pleasantries.' + when 'casual' + 'Relaxed and informal. Use contractions, simpler words, and a conversational style. Friendly but less formal than professional tone.' + when 'professional' + 'Formal, polished, and business-appropriate. Use complete sentences, proper grammar, ' \ + 'and maintain respectful distance. Avoid slang or overly casual language.' + else + determine_tone_instruction('friendly') + end + end end Integrations::Openai::ProcessorService.prepend_mod_with('Integrations::OpenaiProcessorService') diff --git a/spec/lib/integrations/openai/processor_service_spec.rb b/spec/lib/integrations/openai/processor_service_spec.rb index 28488cc15..f9bd5531f 100644 --- a/spec/lib/integrations/openai/processor_service_spec.rb +++ b/spec/lib/integrations/openai/processor_service_spec.rb @@ -53,17 +53,23 @@ RSpec.describe Integrations::Openai::ProcessorService do it 'sets system instructions' do service.perform - expect(mock_chat).to have_received(:with_instructions).with(a_string_including('You are a helpful support agent')) + if event_name == 'fix_spelling_grammar' + expect(mock_chat).to have_received(:with_instructions) + .with(a_string_including('Please fix the spelling and grammar')) + else + expect(mock_chat).to have_received(:with_instructions) + .with(a_string_including('You are an AI writing assistant integrated into Chatwoot')) + end end end - it_behaves_like 'text transformation operation', 'rephrase' + it_behaves_like 'text transformation operation', 'confident' it_behaves_like 'text transformation operation', 'fix_spelling_grammar' - it_behaves_like 'text transformation operation', 'shorten' - it_behaves_like 'text transformation operation', 'expand' + it_behaves_like 'text transformation operation', 'casual' + it_behaves_like 'text transformation operation', 'professional' it_behaves_like 'text transformation operation', 'make_friendly' it_behaves_like 'text transformation operation', 'make_formal' - it_behaves_like 'text transformation operation', 'simplify' + it_behaves_like 'text transformation operation', 'straightforward' end describe 'conversation-based operations' do @@ -125,7 +131,7 @@ RSpec.describe Integrations::Openai::ProcessorService do end describe 'response structure' do - let(:event) { { 'name' => 'rephrase', 'data' => { 'content' => 'test message' } } } + let(:event) { { 'name' => 'confident', 'data' => { 'content' => 'test message' } } } context 'when response includes usage data' do before do @@ -166,10 +172,13 @@ RSpec.describe Integrations::Openai::ProcessorService do end describe 'endpoint configuration' do - let(:event) { { 'name' => 'rephrase', 'data' => { 'content' => 'test message' } } } + let(:event) { { 'name' => 'confident', 'data' => { 'content' => 'test message' } } } context 'without CAPTAIN_OPEN_AI_ENDPOINT configured' do - before { InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.destroy } + before do + InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.destroy + allow(Llm::Config).to receive(:with_api_key).and_call_original + end it 'uses default OpenAI endpoint' do expect(Llm::Config).to receive(:with_api_key).with( @@ -185,6 +194,7 @@ RSpec.describe Integrations::Openai::ProcessorService do before do InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.destroy create(:installation_config, name: 'CAPTAIN_OPEN_AI_ENDPOINT', value: 'https://custom.azure.com/') + allow(Llm::Config).to receive(:with_api_key).and_call_original end it 'uses custom endpoint' do