Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01c84be757 | ||
|
|
5ac32803dc | ||
|
|
18e0cfb327 | ||
|
|
a70d331813 | ||
|
|
3b4c5bd916 | ||
|
|
491e7d0c6c | ||
|
|
e81b812ac2 | ||
|
|
baec4523d5 | ||
|
|
65136c90d8 | ||
|
|
24125e2d9a | ||
|
|
52e3642127 | ||
|
|
6e13eb965b | ||
|
|
7d78f67d3b | ||
|
|
a7420f8fae | ||
|
|
847ce98006 | ||
|
|
8b9da2e409 | ||
|
|
d540d885f2 |
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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 improve
|
||||
make_friendly make_formal casual professional confident straightforward].freeze
|
||||
CACHEABLE_EVENTS = %w[label_suggestion].freeze
|
||||
|
||||
def label_suggestion_message
|
||||
|
||||
@@ -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 improve].freeze
|
||||
CACHEABLE_EVENTS = %w[].freeze
|
||||
|
||||
pattr_initialize [:hook!, :event!]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
You are an AI writing assistant integrated into Chatwoot, an omnichannel customer support platform.
|
||||
|
||||
{% if selection -%}
|
||||
# Task
|
||||
Improve ONLY the selected portion of the message. Return the COMPLETE message with the improved selection seamlessly integrated. Keep all other parts exactly as they are.
|
||||
|
||||
{% endif -%}
|
||||
{% if tone_instruction -%}
|
||||
# Tone
|
||||
Apply the following tone to {% if selection %}the selected portion{% else %}the message{% endif %}:
|
||||
<tone_instruction>
|
||||
{{ tone_instruction }}
|
||||
</tone_instruction>
|
||||
|
||||
{% endif -%}
|
||||
{% unless tone_instruction or selection -%}
|
||||
# Task
|
||||
Improve the message for clarity, grammar, spelling, and natural flow while maintaining the original tone and intent.
|
||||
|
||||
{% endunless -%}
|
||||
# Guidelines
|
||||
- Preserve the core meaning and all important information
|
||||
- Keep the message concise and appropriate for customer support
|
||||
- Do not add information that wasn't in the original
|
||||
- Do not remove critical details or instructions
|
||||
- Reply in the same language as the original message
|
||||
|
||||
Output only the final improved message without any preamble, tags or explanation.
|
||||
|
||||
@@ -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:
|
||||
<tone_instruction>
|
||||
%s
|
||||
</tone_instruction>
|
||||
|
||||
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.
|
||||
|
||||
@@ -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,50 @@ 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
|
||||
|
||||
def improve_message
|
||||
content = event['data']['content']
|
||||
selection = event['data']['selection'].presence
|
||||
tone = event['data']['tone'].presence
|
||||
|
||||
system_prompt = improve_prompt(selection, tone)
|
||||
user_content = selection ? improve_user_content(content, selection) : content
|
||||
|
||||
make_api_call(build_api_call_body(system_prompt, user_content))
|
||||
end
|
||||
|
||||
private
|
||||
@@ -51,6 +61,25 @@ 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 improve_prompt(selection, tone)
|
||||
tone_instruction = determine_tone_instruction(tone) if tone.present?
|
||||
render_liquid_prompt('improve', { selection: selection, tone_instruction: tone_instruction })
|
||||
end
|
||||
|
||||
def improve_user_content(content, selection)
|
||||
"Full message:\n#{content}\n\nSelected portion to improve:\n#{selection}"
|
||||
end
|
||||
|
||||
def render_liquid_prompt(template_name, context = {})
|
||||
template_path = Rails.root.join('lib/integrations/openai/openai_prompts', "#{template_name}.liquid")
|
||||
template = Liquid::Template.parse(template_path.read)
|
||||
template.render(context.deep_stringify_keys)
|
||||
end
|
||||
|
||||
def build_api_call_body(system_content, user_content = event['data']['content'])
|
||||
{
|
||||
model: GPT_MODEL,
|
||||
@@ -133,6 +162,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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user