feat: Add article messages along with bot responses (#7993)
ref: https://linear.app/chatwoot/issue/CW-2464/bot-should-also-return-links-to-the-information
This commit is contained in:
@@ -3,8 +3,8 @@ class Enterprise::MessageTemplates::ResponseBotService
|
|||||||
|
|
||||||
def perform
|
def perform
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
response = get_response(conversation.messages.last.content)
|
@response = get_response(conversation.messages.last.content)
|
||||||
process_response(response['response'])
|
process_response
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
process_action('handoff') # something went wrong, pass to agent
|
process_action('handoff') # something went wrong, pass to agent
|
||||||
@@ -44,11 +44,11 @@ class Enterprise::MessageTemplates::ResponseBotService
|
|||||||
message.message_type == 'incoming' ? 'user' : 'system'
|
message.message_type == 'incoming' ? 'user' : 'system'
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_response(response)
|
def process_response
|
||||||
if response == 'conversation_handoff'
|
if @response['response'] == 'conversation_handoff'
|
||||||
process_action('handoff')
|
process_action('handoff')
|
||||||
else
|
else
|
||||||
create_messages(response)
|
create_messages
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -61,61 +61,47 @@ class Enterprise::MessageTemplates::ResponseBotService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_messages(response)
|
def create_messages
|
||||||
response = process_response_content(response).first
|
message_content = @response['response']
|
||||||
create_outgoing_message(response)
|
|
||||||
|
message_content = append_message_with_sources(message_content)
|
||||||
|
|
||||||
|
create_outgoing_message(message_content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_response_content(response)
|
def append_message_with_sources(message_content)
|
||||||
# Regular expression to match '{context_ids: [ids]}'
|
article_ids = @response['context_ids']
|
||||||
regex = /{context_ids: \[(\d+(?:, *\d+)*)\]}/
|
return message_content if article_ids.blank?
|
||||||
|
|
||||||
# Extract ids from string
|
message_content += "\n \n \n **Sources** \n"
|
||||||
id_string = response[regex, 1] # This will give you '42, 43'
|
articles_hash = get_article_hash(article_ids.uniq)
|
||||||
article_ids = id_string.split(',').map(&:to_i) if id_string # This will give you [42, 43]
|
|
||||||
|
|
||||||
# Remove '{context_ids: [ids]}' from string
|
articles_hash.first(3).each do |article_hash|
|
||||||
response = response.sub(regex, '')
|
message_content += " - [#{article_hash[:response].question}](#{article_hash[:response_document].document_link}) \n"
|
||||||
|
end
|
||||||
[response, article_ids]
|
message_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_outgoing_message(response)
|
def create_outgoing_message(message_content)
|
||||||
conversation.messages.create!(
|
conversation.messages.create!(
|
||||||
{
|
{
|
||||||
message_type: :outgoing,
|
message_type: :outgoing,
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
inbox_id: conversation.inbox_id,
|
inbox_id: conversation.inbox_id,
|
||||||
content: response
|
content: message_content
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_outgoing_message_with_cards(article_ids, conversation)
|
|
||||||
content_attributes = get_article_hash(article_ids.uniq)
|
|
||||||
return if content_attributes.blank?
|
|
||||||
|
|
||||||
conversation.messages.create!(
|
|
||||||
{
|
|
||||||
message_type: :outgoing,
|
|
||||||
account_id: conversation.account_id,
|
|
||||||
inbox_id: conversation.inbox_id,
|
|
||||||
content: 'suggested articles',
|
|
||||||
content_type: 'article',
|
|
||||||
content_attributes: content_attributes
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_article_hash(article_ids)
|
def get_article_hash(article_ids)
|
||||||
items = []
|
seen_documents = Set.new
|
||||||
article_ids.each do |article_id|
|
article_ids.uniq.filter_map do |article_id|
|
||||||
response = Response.find(article_id)
|
response = Response.find(article_id)
|
||||||
next if response.nil?
|
response_document = response.response_document
|
||||||
|
next if response_document.blank? || seen_documents.include?(response_document)
|
||||||
|
|
||||||
items << { title: response.question, description: response.answer[0, 120], link: response.response_document.document_link }
|
seen_documents << response_document
|
||||||
|
{ response: response, response_document: response_document }
|
||||||
end
|
end
|
||||||
|
|
||||||
items.present? ? { items: items } : {}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Openai::EmbeddingsService
|
|||||||
def fetch_embeddings(input)
|
def fetch_embeddings(input)
|
||||||
url = 'https://api.openai.com/v1/embeddings'
|
url = 'https://api.openai.com/v1/embeddings'
|
||||||
headers = {
|
headers = {
|
||||||
'Authorization' => "Bearer #{ENV.fetch('OPENAI_API_KEY')}",
|
'Authorization' => "Bearer #{ENV.fetch('OPENAI_API_KEY', '')}",
|
||||||
'Content-Type' => 'application/json'
|
'Content-Type' => 'application/json'
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
@@ -17,6 +17,6 @@ class Openai::EmbeddingsService
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = Net::HTTP.post(URI(url), data.to_json, headers)
|
response = Net::HTTP.post(URI(url), data.to_json, headers)
|
||||||
JSON.parse(response.body)['data'].pick('embedding')
|
JSON.parse(response.body)['data']&.pick('embedding')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,27 +4,32 @@ RSpec.describe Enterprise::MessageTemplates::ResponseBotService, type: :service
|
|||||||
let!(:conversation) { create(:conversation, status: :pending) }
|
let!(:conversation) { create(:conversation, status: :pending) }
|
||||||
let(:service) { described_class.new(conversation: conversation) }
|
let(:service) { described_class.new(conversation: conversation) }
|
||||||
let(:chat_gpt_double) { instance_double(ChatGpt) }
|
let(:chat_gpt_double) { instance_double(ChatGpt) }
|
||||||
|
let(:response_source) { create(:response_source, account: conversation.account) }
|
||||||
let(:response_object) { instance_double(Response, id: 1, question: 'Q1', answer: 'A1') }
|
let(:response_object) { instance_double(Response, id: 1, question: 'Q1', answer: 'A1') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
# Uncomment if you want to run the spec in your local machine
|
# Uncomment if you want to run the spec in your local machine
|
||||||
# Features::ResponseBotService.new.enable_in_installation
|
# Features::ResponseBotService.new.enable_in_installation
|
||||||
skip('Skipping since vector is not enabled in this environment') unless Features::ResponseBotService.new.vector_extension_enabled?
|
skip('Skipping since vector is not enabled in this environment') unless Features::ResponseBotService.new.vector_extension_enabled?
|
||||||
|
stub_request(:post, 'https://api.openai.com/v1/embeddings').to_return(status: 200, body: {}.to_json,
|
||||||
|
headers: { Content_Type: 'application/json' })
|
||||||
create(:message, message_type: :incoming, conversation: conversation, content: 'Hi')
|
create(:message, message_type: :incoming, conversation: conversation, content: 'Hi')
|
||||||
|
4.times { create(:response, response_source: response_source) }
|
||||||
allow(ChatGpt).to receive(:new).and_return(chat_gpt_double)
|
allow(ChatGpt).to receive(:new).and_return(chat_gpt_double)
|
||||||
allow(chat_gpt_double).to receive(:generate_response).and_return({ 'response' => 'some_response', :context_ids => [1, 2] })
|
allow(chat_gpt_double).to receive(:generate_response).and_return({ 'response' => 'some_response', 'context_ids' => Response.all.map(&:id) })
|
||||||
allow(conversation.inbox).to receive(:get_responses).and_return([response_object])
|
allow(conversation.inbox).to receive(:get_responses).and_return([response_object])
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
context 'when successful' do
|
context 'when successful' do
|
||||||
it 'creates an outgoing message' do
|
it 'creates an outgoing message along with article references' do
|
||||||
expect do
|
expect do
|
||||||
service.perform
|
service.perform
|
||||||
end.to change { conversation.messages.where(message_type: :outgoing).count }.by(1)
|
end.to change { conversation.messages.where(message_type: :outgoing).count }.by(1)
|
||||||
|
|
||||||
expect(conversation.messages.last.content).to eq('some_response')
|
last_message = conversation.messages.last
|
||||||
|
expect(last_message.content).to include('some_response')
|
||||||
|
expect(last_message.content).to include(Response.first.question)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
11
spec/factories/response.rb
Normal file
11
spec/factories/response.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :response do
|
||||||
|
response_document
|
||||||
|
response_source
|
||||||
|
question { Faker::Lorem.sentence }
|
||||||
|
answer { Faker::Lorem.paragraph }
|
||||||
|
account
|
||||||
|
end
|
||||||
|
end
|
||||||
10
spec/factories/response_document.rb
Normal file
10
spec/factories/response_document.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :response_document do
|
||||||
|
response_source
|
||||||
|
content { Faker::Lorem.paragraph }
|
||||||
|
document_link { Faker::Internet.url }
|
||||||
|
account
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user