feat: Add support for multi-language support for Captain (#11068)

This PR implements the following features

- FAQs from conversations will be generated in account language
- Contact notes will be generated in account language
- Copilot chat will respond in user language, unless the agent asks the
question in a different language

## Changes
### Copilot Chat

- Update the prompt to include an instruction for the language, the bot
will reply in asked language, but will default to account language
- Update the `ChatService` class to include pass the language to
`SystemPromptsService`

### FAQ and Contact note generation

- Update contact note generator and conversation generator to include
account locale
- Pass the account locale to `SystemPromptsService`


<details><summary>Screenshots</summary>

#### FAQs being generated in system langauge

![CleanShot 2025-03-12 at 13 32
30@2x](https://github.com/user-attachments/assets/84685bd8-3785-4432-aff3-419f60d96dd3)


#### Copilot responding in system language

![CleanShot 2025-03-12 at 13 47
03@2x](https://github.com/user-attachments/assets/38383293-4228-47bd-b74a-773e9a194f90)


</details>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
Shivam Mishra
2025-03-20 06:55:33 +05:30
committed by GitHub
parent 51ad80a61e
commit 3a4249da11
13 changed files with 136 additions and 13 deletions

View File

@@ -2,17 +2,71 @@ require 'rails_helper'
RSpec.describe Captain::Copilot::ChatService do
let(:account) { create(:account, custom_attributes: { plan_name: 'startups' }) }
let(:inbox) { create(:inbox, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:captain_inbox_association) { create(:captain_inbox, captain_assistant: assistant, inbox: inbox) }
let(:mock_captain_agent) { instance_double(Captain::Agent) }
let(:mock_captain_tool) { instance_double(Captain::Tool) }
let(:mock_openai_client) { instance_double(OpenAI::Client) }
let(:inbox) { create(:inbox, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
before do
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key')
end
describe '#initialize' do
it 'sets default language to english when not specified' do
service = described_class.new(assistant, { previous_messages: [], conversation_history: '' })
expect(service.instance_variable_get(:@language)).to eq('english')
end
it 'uses the specified language when provided' do
service = described_class.new(assistant, {
previous_messages: [],
conversation_history: '',
language: 'spanish'
})
expect(service.instance_variable_get(:@language)).to eq('spanish')
end
end
describe '#generate_response' do
before do
allow(OpenAI::Client).to receive(:new).and_return(mock_openai_client)
allow(mock_openai_client).to receive(:chat).and_return({ choices: [{ message: { content: '{ "result": "Hey" }' } }] }.with_indifferent_access)
allow(Captain::Agent).to receive(:new).and_return(mock_captain_agent)
allow(mock_captain_agent).to receive(:execute).and_return(true)
allow(mock_captain_agent).to receive(:register_tool).and_return(true)
allow(Captain::Tool).to receive(:new).and_return(mock_captain_tool)
allow(mock_captain_tool).to receive(:register_method).and_return(true)
allow(account).to receive(:increment_response_usage).and_return(true)
end
it 'increments usage' do
described_class.new(assistant, { previous_messages: ['Hello'], conversation_history: 'Hi' }).generate_response('Hey')
expect(account).to have_received(:increment_response_usage).once
end
it 'includes language in system message' do
service = described_class.new(assistant, {
previous_messages: [],
conversation_history: '',
language: 'spanish'
})
allow(Captain::Llm::SystemPromptsService).to receive(:copilot_response_generator)
.with(assistant.config['product_name'], 'spanish')
.and_return('Spanish system prompt')
system_message = service.send(:system_message)
expect(system_message[:content]).to eq('Spanish system prompt')
end
end
describe '#execute' do
before do
create(:installation_config) { create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-key') }
allow(OpenAI::Client).to receive(:new).and_return(mock_openai_client)
allow(mock_openai_client).to receive(:chat).and_return({ choices: [{ message: { content: '{ "result": "Hey" }' } }] }.with_indifferent_access)

View File

@@ -145,5 +145,25 @@ RSpec.describe Captain::Llm::ConversationFaqService do
{ role: 'user', content: conversation.to_llm_text }
)
end
context 'when conversation has different language' do
let(:account) { create(:account, locale: 'fr') }
let(:conversation) do
create(:conversation, account: account,
first_reply_created_at: Time.zone.now)
end
it 'includes system prompt with correct language' do
allow(Captain::Llm::SystemPromptsService).to receive(:conversation_faq_generator)
.with('french')
.and_return('system prompt in french')
params = service.send(:chat_parameters)
expect(params[:messages]).to include(
{ role: 'system', content: 'system prompt in french' }
)
end
end
end
end

View File

@@ -108,4 +108,30 @@ RSpec.describe Account do
expect(ActiveRecord::Base.connection.execute(query).count).to eq(0)
end
end
describe 'locale' do
it 'returns correct language if the value is set' do
account = create(:account, locale: 'fr')
expect(account.locale).to eq('fr')
expect(account.locale_english_name).to eq('french')
end
it 'returns english if the value is not set' do
account = create(:account, locale: nil)
expect(account.locale).to be_nil
expect(account.locale_english_name).to eq('english')
end
it 'returns english if the value is empty string' do
account = create(:account, locale: '')
expect(account.locale).to be_nil
expect(account.locale_english_name).to eq('english')
end
it 'returns correct language if the value has country code' do
account = create(:account, locale: 'pt_BR')
expect(account.locale).to eq('pt_BR')
expect(account.locale_english_name).to eq('portuguese')
end
end
end