feat: Interface to validate response_source (#8894)

- This PR adds a UI to validate the response source quality quickly. It also helps to test with sample questions and update responses in the database when missing.

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose
2024-02-26 20:20:12 +05:30
committed by GitHub
parent 77e463990a
commit 773be6f8ec
23 changed files with 514 additions and 57 deletions

View File

@@ -0,0 +1,8 @@
class SuperAdmin::EnterpriseBaseController < SuperAdmin::ApplicationController
before_action :prepend_view_paths
# Prepend the view path to the enterprise/app/views won't be available by default
def prepend_view_paths
prepend_view_path 'enterprise/app/views/'
end
end

View File

@@ -1,4 +1,4 @@
class SuperAdmin::ResponseDocumentsController < SuperAdmin::ApplicationController
class SuperAdmin::ResponseDocumentsController < SuperAdmin::EnterpriseBaseController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#

View File

@@ -1,4 +1,4 @@
class SuperAdmin::ResponseSourcesController < SuperAdmin::ApplicationController
class SuperAdmin::ResponseSourcesController < SuperAdmin::EnterpriseBaseController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
@@ -41,4 +41,36 @@ class SuperAdmin::ResponseSourcesController < SuperAdmin::ApplicationController
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
# for more information
before_action :set_response_source, only: %i[chat process_chat]
def chat; end
def process_chat
previous_messages = []
get_previous_messages(previous_messages)
robin_response = ChatGpt.new(
Enterprise::MessageTemplates::ResponseBotService.response_sections(params[:message], @response_source)
).generate_response(
params[:message], previous_messages
)
message_content = robin_response['response']
if robin_response['context_ids'].present?
message_content += Enterprise::MessageTemplates::ResponseBotService.generate_sources_section(robin_response['context_ids'])
end
render json: { message: message_content }
end
private
def get_previous_messages(previous_messages)
params[:previous_messages].each do |message|
role = message['type'] == 'user' ? 'user' : 'system'
previous_messages << { content: message['message'], role: role }
end
end
def set_response_source
@response_source = requested_resource
end
end

View File

@@ -1,4 +1,4 @@
class SuperAdmin::ResponsesController < SuperAdmin::ApplicationController
class SuperAdmin::ResponsesController < SuperAdmin::EnterpriseBaseController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#

View File

@@ -17,6 +17,7 @@ class ResponseBuilderJob < ApplicationJob
def prepare_data(response_document)
{
model: 'gpt-3.5-turbo',
response_format: { type: 'json_object' },
messages: [
{
role: 'system',
@@ -32,16 +33,15 @@ class ResponseBuilderJob < ApplicationJob
def system_message_content
<<~SYSTEM_MESSAGE_CONTENT
You are a content writer looking to convert user content into short FAQs which can be added to your website's helper centre.
Format the webpage content provided in the message to FAQ format like the following example.#{' '}
Ensure that you only generate faqs from the information provider in the message.#{' '}
Ensure that output is always valid json.#{' '}
If no match is available, return an empty JSON.
```
[ { "question": "What is the pricing?",
"answer" : " There are different pricing tiers available."
}]
```
You are a content writer looking to convert user content into short FAQs which can be added to your website's helper centre.
Format the webpage content provided in the message to FAQ format mentioned below in the json
Ensure that you only generate faqs from the information provider in the message.
Ensure that output is always valid json.
If no match is available, return an empty JSON.
```json
{faqs: [{question: '', answer: ''}]
```
SYSTEM_MESSAGE_CONTENT
end
@@ -67,7 +67,7 @@ class ResponseBuilderJob < ApplicationJob
return if content.nil?
faqs = JSON.parse(content.strip)
faqs = JSON.parse(content.strip).fetch('faqs', [])
faqs.each do |faq|
response_document.responses.create!(

View File

@@ -25,4 +25,9 @@ class ResponseSource < ApplicationRecord
has_many :responses, dependent: :destroy_async
accepts_nested_attributes_for :response_documents
def get_responses(query)
embedding = Openai::EmbeddingsService.new.get_embedding(query)
responses.active.nearest_neighbors(:embedding, embedding, distance: 'cosine').first(5)
end
end

View File

@@ -1,6 +1,39 @@
class Enterprise::MessageTemplates::ResponseBotService
pattr_initialize [:conversation!]
def self.generate_sources_section(article_ids)
sources_content = ''
articles_hash = get_article_hash(article_ids.uniq)
articles_hash.first(3).each do |article_hash|
sources_content += " - [#{article_hash[:response].question}](#{article_hash[:response_document].document_link}) \n"
end
sources_content = "\n \n \n **Sources** \n#{sources_content}" if sources_content.present?
sources_content
end
def self.get_article_hash(article_ids)
seen_documents = Set.new
article_ids.uniq.filter_map do |article_id|
response = Response.find(article_id)
response_document = response.response_document
next if response_document.blank? || seen_documents.include?(response_document)
seen_documents << response_document
{ response: response, response_document: response_document }
end
end
def self.response_sections(content, response_source)
sections = ''
response_source.get_responses(content).each do |response|
sections += "{context_id: #{response.id}, context: #{response.question} ? #{response.answer}},"
end
sections
end
def perform
ActiveRecord::Base.transaction do
@response = get_response(conversation.messages.incoming.last.content)
@@ -12,15 +45,6 @@ class Enterprise::MessageTemplates::ResponseBotService
true
end
def response_sections(content)
sections = ''
inbox.get_responses(content).each do |response|
sections += "{context_id: #{response.id}, context: #{response.question} ? #{response.answer}},"
end
sections
end
private
delegate :contact, :account, :inbox, to: :conversation
@@ -28,7 +52,7 @@ class Enterprise::MessageTemplates::ResponseBotService
def get_response(content)
previous_messages = []
get_previous_messages(previous_messages)
ChatGpt.new(response_sections(content)).generate_response('', previous_messages)
ChatGpt.new(self.class.response_sections(content, inbox)).generate_response('', previous_messages)
end
def get_previous_messages(previous_messages)
@@ -63,24 +87,11 @@ class Enterprise::MessageTemplates::ResponseBotService
def create_messages
message_content = @response['response']
message_content += generate_sources_section if @response['context_ids'].present?
message_content += self.class.generate_sources_section(@response['context_ids']) if @response['context_ids'].present?
create_outgoing_message(message_content)
end
def generate_sources_section
article_ids = @response['context_ids']
sources_content = ''
articles_hash = get_article_hash(article_ids.uniq)
articles_hash.first(3).each do |article_hash|
sources_content += " - [#{article_hash[:response].question}](#{article_hash[:response_document].document_link}) \n"
end
sources_content = "\n \n \n **Sources** \n#{sources_content}" if sources_content.present?
sources_content
end
def create_outgoing_message(message_content)
conversation.messages.create!(
{
@@ -91,16 +102,4 @@ class Enterprise::MessageTemplates::ResponseBotService
}
)
end
def get_article_hash(article_ids)
seen_documents = Set.new
article_ids.uniq.filter_map do |article_id|
response = Response.find(article_id)
response_document = response.response_document
next if response_document.blank? || seen_documents.include?(response_document)
seen_documents << response_document
{ response: response, response_document: response_document }
end
end
end

View File

@@ -0,0 +1,5 @@
<% content_for :title, "Robin AI playground: #{@response_source.name}" %>
<%= render_vue_component('PlaygroundIndex', {
responseSourceName: @response_source.name,
responseSourcePath: super_admin_response_source_path(@response_source)
}) %>

View File

@@ -0,0 +1,71 @@
<%#
# Show
This view is the template for the show page.
It renders the attributes of a resource,
as well as a link to its edit page.
## Local variables:
- `page`:
An instance of [Administrate::Page::Show][1].
Contains methods for accessing the resource to be displayed on the page,
as well as helpers for describing how each attribute of the resource
should be displayed.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Show
%>
<% content_for(:title) { t("administrate.actions.show_resource", name: page.page_title) } %>
<header class="main-content__header">
<h1 class="main-content__page-title">
<%= content_for(:title) %>
</h1>
<div>
<%= link_to(
"Chat",
[:chat, namespace, page.resource],
class: "button"
) %>
<%= link_to(
"Edit",
[:edit, namespace, page.resource],
class: "button",
) if accessible_action?(page.resource, :edit) %>
<%= link_to(
t("administrate.actions.destroy"),
[namespace, page.resource],
class: "button button--danger",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if accessible_action?(page.resource, :destroy) %>
</div>
</header>
<section class="main-content__body">
<dl>
<% page.attributes.each do |title, attributes| %>
<fieldset class="<%= "field-unit--nested" if title.present? %>">
<% if title.present? %>
<legend><%= t "helpers.label.#{page.resource_name}.#{title}", default: title %></legend>
<% end %>
<% attributes.each do |attribute| %>
<dt class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
</dt>
<dd class="attribute-data attribute-data--<%=attribute.html_class%>"
><%= render_field attribute, page: page %></dd>
<% end %>
</fieldset>
<% end %>
</dl>
</section>