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:
Sojan Jose
2023-10-04 15:40:59 -07:00
committed by GitHub
parent 5c9ab21617
commit 6f19546c3c
5 changed files with 60 additions and 48 deletions

View File

@@ -3,8 +3,8 @@ class Enterprise::MessageTemplates::ResponseBotService
def perform
ActiveRecord::Base.transaction do
response = get_response(conversation.messages.last.content)
process_response(response['response'])
@response = get_response(conversation.messages.last.content)
process_response
end
rescue StandardError => e
process_action('handoff') # something went wrong, pass to agent
@@ -44,11 +44,11 @@ class Enterprise::MessageTemplates::ResponseBotService
message.message_type == 'incoming' ? 'user' : 'system'
end
def process_response(response)
if response == 'conversation_handoff'
def process_response
if @response['response'] == 'conversation_handoff'
process_action('handoff')
else
create_messages(response)
create_messages
end
end
@@ -61,61 +61,47 @@ class Enterprise::MessageTemplates::ResponseBotService
end
end
def create_messages(response)
response = process_response_content(response).first
create_outgoing_message(response)
def create_messages
message_content = @response['response']
message_content = append_message_with_sources(message_content)
create_outgoing_message(message_content)
end
def process_response_content(response)
# Regular expression to match '{context_ids: [ids]}'
regex = /{context_ids: \[(\d+(?:, *\d+)*)\]}/
def append_message_with_sources(message_content)
article_ids = @response['context_ids']
return message_content if article_ids.blank?
# Extract ids from string
id_string = response[regex, 1] # This will give you '42, 43'
article_ids = id_string.split(',').map(&:to_i) if id_string # This will give you [42, 43]
message_content += "\n \n \n **Sources** \n"
articles_hash = get_article_hash(article_ids.uniq)
# Remove '{context_ids: [ids]}' from string
response = response.sub(regex, '')
[response, article_ids]
articles_hash.first(3).each do |article_hash|
message_content += " - [#{article_hash[:response].question}](#{article_hash[:response_document].document_link}) \n"
end
message_content
end
def create_outgoing_message(response)
def create_outgoing_message(message_content)
conversation.messages.create!(
{
message_type: :outgoing,
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
content: response
}
)
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
content: message_content
}
)
end
def get_article_hash(article_ids)
items = []
article_ids.each do |article_id|
seen_documents = Set.new
article_ids.uniq.filter_map do |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
items.present? ? { items: items } : {}
end
end

View File

@@ -8,7 +8,7 @@ class Openai::EmbeddingsService
def fetch_embeddings(input)
url = 'https://api.openai.com/v1/embeddings'
headers = {
'Authorization' => "Bearer #{ENV.fetch('OPENAI_API_KEY')}",
'Authorization' => "Bearer #{ENV.fetch('OPENAI_API_KEY', '')}",
'Content-Type' => 'application/json'
}
data = {
@@ -17,6 +17,6 @@ class Openai::EmbeddingsService
}
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

View File

@@ -4,27 +4,32 @@ RSpec.describe Enterprise::MessageTemplates::ResponseBotService, type: :service
let!(:conversation) { create(:conversation, status: :pending) }
let(:service) { described_class.new(conversation: conversation) }
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') }
before do
# Uncomment if you want to run the spec in your local machine
# Features::ResponseBotService.new.enable_in_installation
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')
4.times { create(:response, response_source: response_source) }
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])
end
describe '#perform' do
context 'when successful' do
it 'creates an outgoing message' do
it 'creates an outgoing message along with article references' do
expect do
service.perform
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

View 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

View 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