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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
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