feat: Add support for the references in FAQs (#10699)

Currently, it’s unclear whether an FAQ item is generated from a
document, derived from a conversation, or added manually.

This PR resolves the issue by providing visibility into the source of
each FAQ. Users can now see whether an FAQ was generated or manually
added and, if applicable, by whom.

- Move the document_id to a polymorphic relation (documentable).
- Updated the APIs to accommodate the change.
- Update the service to add corresponding references. 
- Updated the specs.

<img width="1007" alt="Screenshot 2025-01-15 at 11 27 56 PM"
src="https://github.com/user-attachments/assets/7d58f798-19c0-4407-b3e2-748a919d14af"
/>

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Pranav
2025-01-16 01:57:30 -08:00
committed by GitHub
parent 88f3b4de48
commit 0b4028b95d
17 changed files with 197 additions and 61 deletions

View File

@@ -42,7 +42,7 @@ const handlePageChange = event => {
<template> <template>
<section class="flex flex-col w-full h-full overflow-hidden bg-n-background"> <section class="flex flex-col w-full h-full overflow-hidden bg-n-background">
<header class="sticky top-0 z-10 px-6 lg:px-0"> <header class="sticky top-0 z-10 px-6 xl:px-0">
<div class="w-full max-w-[960px] mx-auto"> <div class="w-full max-w-[960px] mx-auto">
<div <div
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row" class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
@@ -67,7 +67,7 @@ const handlePageChange = event => {
</div> </div>
</div> </div>
</header> </header>
<main class="flex-1 px-6 overflow-y-auto lg:px-0"> <main class="flex-1 px-6 overflow-y-auto xl:px-0">
<div class="w-full max-w-[960px] mx-auto py-4"> <div class="w-full max-w-[960px] mx-auto py-4">
<slot name="default" /> <slot name="default" />
</div> </div>

View File

@@ -29,6 +29,10 @@ const props = defineProps({
type: String, type: String,
default: 'approved', default: 'approved',
}, },
documentable: {
type: Object,
default: null,
},
assistant: { assistant: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
@@ -43,7 +47,7 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['action']); const emit = defineEmits(['action', 'navigate']);
const { t } = useI18n(); const { t } = useI18n();
@@ -87,6 +91,13 @@ const handleAssistantAction = ({ action, value }) => {
toggleDropdown(false); toggleDropdown(false);
emit('action', { action, value, id: props.id }); emit('action', { action, value, id: props.id });
}; };
const handleDocumentableClick = () => {
emit('navigate', {
id: props.documentable.id,
type: props.documentable.type,
});
};
</script> </script>
<template> <template>
@@ -119,13 +130,56 @@ const handleAssistantAction = ({ action, value }) => {
<span class="text-n-slate-11 text-sm line-clamp-5"> <span class="text-n-slate-11 text-sm line-clamp-5">
{{ answer }} {{ answer }}
</span> </span>
<span v-if="!compact"> <div v-if="!compact" class="items-center justify-between hidden lg:flex">
<div class="inline-flex items-center">
<span <span
class="text-sm shrink-0 truncate text-n-slate-11 inline-flex items-center gap-1" class="text-sm shrink-0 truncate text-n-slate-11 inline-flex items-center gap-1"
> >
<i class="i-woot-captain" /> <i class="i-woot-captain" />
{{ assistant?.name || '' }} {{ assistant?.name || '' }}
</span> </span>
<div
v-if="documentable"
class="shrink-0 text-sm text-n-slate-11 inline-flex line-clamp-1 gap-1 ml-3"
>
<span
v-if="documentable.type === 'Captain::Document'"
class="inline-flex items-center gap-1 truncate over"
>
<i class="i-ph-chat-circle-dots text-base" />
<span class="max-w-96 truncate" :title="documentable.name">
{{ documentable.name }}
</span>
</span>
<span
v-if="documentable.type === 'User'"
class="inline-flex items-center gap-1"
>
<i class="i-ph-user-circle-plus text-base" />
<span
class="max-w-96 truncate"
:title="documentable.available_name"
>
{{ documentable.available_name }}
</span>
</span>
<span
v-else-if="documentable.type === 'Conversation'"
class="inline-flex items-center gap-1 group cursor-pointer"
role="button"
@click="handleDocumentableClick"
>
<i class="i-ph-chat-circle-dots text-base" />
<span class="group-hover:underline">
{{
t(`CAPTAIN.RESPONSES.DOCUMENTABLE.CONVERSATION`, {
id: documentable.display_id,
})
}}
</span>
</span>
<span v-else />
</div>
<div <div
v-if="status !== 'approved'" v-if="status !== 'approved'"
class="shrink-0 text-sm text-n-slate-11 line-clamp-1 inline-flex items-center gap-1 ml-3" class="shrink-0 text-sm text-n-slate-11 line-clamp-1 inline-flex items-center gap-1 ml-3"
@@ -136,12 +190,13 @@ const handleAssistantAction = ({ action, value }) => {
/> />
{{ t(`CAPTAIN.RESPONSES.STATUS.${status.toUpperCase()}`) }} {{ t(`CAPTAIN.RESPONSES.STATUS.${status.toUpperCase()}`) }}
</div> </div>
</div>
<div <div
class="shrink-0 text-sm text-n-slate-11 line-clamp-1 inline-flex items-center gap-1 ml-3" class="shrink-0 text-sm text-n-slate-11 line-clamp-1 inline-flex items-center gap-1 ml-3"
> >
<i class="i-ph-calendar-dot" /> <i class="i-ph-calendar-dot" />
{{ timestamp }} {{ timestamp }}
</div> </div>
</span> </div>
</CardLayout> </CardLayout>
</template> </template>

View File

@@ -409,8 +409,11 @@
} }
}, },
"RESPONSES": { "RESPONSES": {
"HEADER": "Generated FAQs", "HEADER": "FAQs",
"ADD_NEW": "Create new FAQ", "ADD_NEW": "Create new FAQ",
"DOCUMENTABLE" : {
"CONVERSATION": "Conversation #{id}"
},
"DELETE": { "DELETE": {
"TITLE": "Are you sure to delete the FAQ?", "TITLE": "Are you sure to delete the FAQ?",
"DESCRIPTION": "", "DESCRIPTION": "",

View File

@@ -4,9 +4,10 @@ import { useMapGetter, useStore } from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { OnClickOutside } from '@vueuse/components'; import { OnClickOutside } from '@vueuse/components';
import { useRouter } from 'vue-router';
import Button from 'dashboard/components-next/button/Button.vue'; import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue'; import DeleteDialog from 'dashboard/components-next/captain/pageComponents/DeleteDialog.vue';
import PageLayout from 'dashboard/components-next/captain/PageLayout.vue'; import PageLayout from 'dashboard/components-next/captain/PageLayout.vue';
import AssistantSelector from 'dashboard/components-next/captain/pageComponents/AssistantSelector.vue'; import AssistantSelector from 'dashboard/components-next/captain/pageComponents/AssistantSelector.vue';
@@ -15,6 +16,7 @@ import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue'; import CreateResponseDialog from 'dashboard/components-next/captain/pageComponents/response/CreateResponseDialog.vue';
import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue'; import ResponsePageEmptyState from 'dashboard/components-next/captain/pageComponents/emptyStates/ResponsePageEmptyState.vue';
const router = useRouter();
const store = useStore(); const store = useStore();
const uiFlags = useMapGetter('captainResponses/getUIFlags'); const uiFlags = useMapGetter('captainResponses/getUIFlags');
const assistants = useMapGetter('captainAssistants/getRecords'); const assistants = useMapGetter('captainAssistants/getRecords');
@@ -100,6 +102,15 @@ const handleAction = ({ action, id }) => {
}); });
}; };
const handleNavigationAction = ({ id, type }) => {
if (type === 'Conversation') {
router.push({
name: 'inbox_conversation',
params: { conversation_id: id },
});
}
};
const handleCreateClose = () => { const handleCreateClose = () => {
dialogType.value = ''; dialogType.value = '';
selectedResponse.value = null; selectedResponse.value = null;
@@ -184,10 +195,12 @@ onMounted(() => {
:question="response.question" :question="response.question"
:answer="response.answer" :answer="response.answer"
:assistant="response.assistant" :assistant="response.assistant"
:documentable="response.documentable"
:status="response.status" :status="response.status"
:created-at="response.created_at" :created-at="response.created_at"
:updated-at="response.updated_at" :updated-at="response.updated_at"
@action="handleAction" @action="handleAction"
@navigate="handleNavigationAction"
/> />
</div> </div>

View File

@@ -0,0 +1,33 @@
class ConvertDocumentToPolymorphicAssociation < ActiveRecord::Migration[7.0]
def up
add_column :captain_assistant_responses, :documentable_type, :string
# rubocop:disable Rails/SkipsModelValidations
Captain::AssistantResponse
.where
.not(document_id: nil)
.update_all(documentable_type: 'Captain::Document')
# rubocop:enable Rails/SkipsModelValidations
remove_index :captain_assistant_responses, :document_id if index_exists?(
:captain_assistant_responses, :document_id
)
rename_column :captain_assistant_responses, :document_id, :documentable_id
add_index :captain_assistant_responses, [:documentable_id, :documentable_type],
name: 'idx_cap_asst_resp_on_documentable'
end
def down
if index_exists?(
:captain_assistant_responses, [:documentable_id, :documentable_type], name: 'idx_cap_asst_resp_on_documentable'
)
remove_index :captain_assistant_responses, name: 'idx_cap_asst_resp_on_documentable'
end
rename_column :captain_assistant_responses, :documentable_id, :document_id
remove_column :captain_assistant_responses, :documentable_type
add_index :captain_assistant_responses, :document_id unless index_exists?(
:captain_assistant_responses, :document_id
)
end
end

View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2025_01_16_000103) do ActiveRecord::Schema[7.0].define(version: 2025_01_16_061033) do
# These extensions should be enabled to support this database # These extensions should be enabled to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
enable_extension "pg_trgm" enable_extension "pg_trgm"
@@ -250,14 +250,15 @@ ActiveRecord::Schema[7.0].define(version: 2025_01_16_000103) do
t.text "answer", null: false t.text "answer", null: false
t.vector "embedding", limit: 1536 t.vector "embedding", limit: 1536
t.bigint "assistant_id", null: false t.bigint "assistant_id", null: false
t.bigint "document_id" t.bigint "documentable_id"
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "status", default: 1, null: false t.integer "status", default: 1, null: false
t.string "documentable_type"
t.index ["account_id"], name: "index_captain_assistant_responses_on_account_id" t.index ["account_id"], name: "index_captain_assistant_responses_on_account_id"
t.index ["assistant_id"], name: "index_captain_assistant_responses_on_assistant_id" t.index ["assistant_id"], name: "index_captain_assistant_responses_on_assistant_id"
t.index ["document_id"], name: "index_captain_assistant_responses_on_document_id" t.index ["documentable_id", "documentable_type"], name: "idx_cap_asst_resp_on_documentable"
t.index ["embedding"], name: "vector_idx_knowledge_entries_embedding", using: :ivfflat t.index ["embedding"], name: "vector_idx_knowledge_entries_embedding", using: :ivfflat
t.index ["status"], name: "index_captain_assistant_responses_on_status" t.index ["status"], name: "index_captain_assistant_responses_on_status"
end end

View File

@@ -12,7 +12,14 @@ class Api::V1::Accounts::Captain::AssistantResponsesController < Api::V1::Accoun
def index def index
base_query = @responses base_query = @responses
base_query = base_query.where(assistant_id: permitted_params[:assistant_id]) if permitted_params[:assistant_id].present? base_query = base_query.where(assistant_id: permitted_params[:assistant_id]) if permitted_params[:assistant_id].present?
base_query = base_query.where(document_id: permitted_params[:document_id]) if permitted_params[:document_id].present?
if permitted_params[:document_id].present?
base_query = base_query.where(
documentable_id: permitted_params[:document_id],
documentable_type: 'Captain::Document'
)
end
base_query = base_query.where(status: permitted_params[:status]) if permitted_params[:status].present? base_query = base_query.where(status: permitted_params[:status]) if permitted_params[:status].present?
@responses_count = base_query.count @responses_count = base_query.count
@@ -24,6 +31,7 @@ class Api::V1::Accounts::Captain::AssistantResponsesController < Api::V1::Accoun
def create def create
@response = Current.account.captain_assistant_responses.new(response_params) @response = Current.account.captain_assistant_responses.new(response_params)
@response.documentable = Current.user
@response.save! @response.save!
end end
@@ -43,7 +51,7 @@ class Api::V1::Accounts::Captain::AssistantResponsesController < Api::V1::Accoun
end end
def set_responses def set_responses
@responses = Current.account.captain_assistant_responses.includes(:assistant, :document).ordered @responses = Current.account.captain_assistant_responses.includes(:assistant, :documentable).ordered
end end
def set_response def set_response
@@ -62,7 +70,6 @@ class Api::V1::Accounts::Captain::AssistantResponsesController < Api::V1::Accoun
params.require(:assistant_response).permit( params.require(:assistant_response).permit(
:question, :question,
:answer, :answer,
:document_id,
:assistant_id, :assistant_id,
:status :status
) )

View File

@@ -21,7 +21,7 @@ class Captain::Documents::ResponseBuilderJob < ApplicationJob
question: faq['question'], question: faq['question'],
answer: faq['answer'], answer: faq['answer'],
assistant: document.assistant, assistant: document.assistant,
document: document documentable: document
) )
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
Rails.logger.error "Error in creating response document: #{e.message}" Rails.logger.error "Error in creating response document: #{e.message}"

View File

@@ -4,6 +4,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# answer :text not null # answer :text not null
# documentable_type :string
# embedding :vector(1536) # embedding :vector(1536)
# question :string not null # question :string not null
# status :integer default("approved"), not null # status :integer default("approved"), not null
@@ -11,13 +12,13 @@
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint not null # account_id :bigint not null
# assistant_id :bigint not null # assistant_id :bigint not null
# document_id :bigint # documentable_id :bigint
# #
# Indexes # Indexes
# #
# idx_cap_asst_resp_on_documentable (documentable_id,documentable_type)
# index_captain_assistant_responses_on_account_id (account_id) # index_captain_assistant_responses_on_account_id (account_id)
# index_captain_assistant_responses_on_assistant_id (assistant_id) # index_captain_assistant_responses_on_assistant_id (assistant_id)
# index_captain_assistant_responses_on_document_id (document_id)
# index_captain_assistant_responses_on_status (status) # index_captain_assistant_responses_on_status (status)
# vector_idx_knowledge_entries_embedding (embedding) USING ivfflat # vector_idx_knowledge_entries_embedding (embedding) USING ivfflat
# #
@@ -26,7 +27,7 @@ class Captain::AssistantResponse < ApplicationRecord
belongs_to :assistant, class_name: 'Captain::Assistant' belongs_to :assistant, class_name: 'Captain::Assistant'
belongs_to :account belongs_to :account
belongs_to :document, optional: true, class_name: 'Captain::Document' belongs_to :documentable, polymorphic: true, optional: true
has_neighbors :embedding, normalize: true has_neighbors :embedding, normalize: true
validates :question, presence: true validates :question, presence: true

View File

@@ -23,7 +23,7 @@ class Captain::Document < ApplicationRecord
self.table_name = 'captain_documents' self.table_name = 'captain_documents'
belongs_to :assistant, class_name: 'Captain::Assistant' belongs_to :assistant, class_name: 'Captain::Assistant'
has_many :responses, class_name: 'Captain::AssistantResponse', dependent: :destroy has_many :responses, class_name: 'Captain::AssistantResponse', dependent: :destroy, as: :documentable
belongs_to :account belongs_to :account
validates :external_link, presence: true validates :external_link, presence: true

View File

@@ -5,6 +5,7 @@ module Enterprise::Concerns::Conversation
belongs_to :sla_policy, optional: true belongs_to :sla_policy, optional: true
has_one :applied_sla, dependent: :destroy_async has_one :applied_sla, dependent: :destroy_async
has_many :sla_events, dependent: :destroy_async has_many :sla_events, dependent: :destroy_async
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
before_validation :validate_sla_policy, if: -> { sla_policy_id_changed? } before_validation :validate_sla_policy, if: -> { sla_policy_id_changed? }
around_save :ensure_applied_sla_is_created, if: -> { sla_policy_id_changed? } around_save :ensure_applied_sla_is_created, if: -> { sla_policy_id_changed? }
end end

View File

@@ -3,6 +3,8 @@ module Enterprise::Concerns::User
included do included do
before_validation :ensure_installation_pricing_plan_quantity, on: :create before_validation :ensure_installation_pricing_plan_quantity, on: :create
has_many :captain_responses, class_name: 'Captain::AssistantResponse', dependent: :nullify, as: :documentable
end end
def ensure_installation_pricing_plan_quantity def ensure_installation_pricing_plan_quantity

View File

@@ -4,6 +4,7 @@ class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService
def initialize(assistant, conversation, model = DEFAULT_MODEL) def initialize(assistant, conversation, model = DEFAULT_MODEL)
super() super()
@assistant = assistant @assistant = assistant
@conversation = conversation
@content = conversation.to_llm_text @content = conversation.to_llm_text
@model = model @model = model
end end
@@ -19,7 +20,7 @@ class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService
private private
attr_reader :content attr_reader :content, :conversation, :assistant
def find_and_separate_duplicates(faqs) def find_and_separate_duplicates(faqs)
duplicate_faqs = [] duplicate_faqs = []
@@ -41,7 +42,7 @@ class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService
end end
def find_similar_faqs(embedding) def find_similar_faqs(embedding)
similar_faqs = @assistant similar_faqs = assistant
.responses .responses
.nearest_neighbors(:embedding, embedding, distance: 'cosine') .nearest_neighbors(:embedding, embedding, distance: 'cosine')
Rails.logger.debug(similar_faqs.map { |faq| [faq.question, faq.neighbor_distance] }) Rails.logger.debug(similar_faqs.map { |faq| [faq.question, faq.neighbor_distance] })
@@ -50,7 +51,12 @@ class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService
def save_new_faqs(faqs) def save_new_faqs(faqs)
faqs.map do |faq| faqs.map do |faq|
@assistant.responses.create!(question: faq['question'], answer: faq['answer'], status: 'pending') assistant.responses.create!(
question: faq['question'],
answer: faq['answer'],
status: 'pending',
documentable: conversation
)
end end
end end

View File

@@ -4,13 +4,27 @@ json.assistant do
json.partial! 'api/v1/models/captain/assistant', formats: [:json], resource: resource.assistant json.partial! 'api/v1/models/captain/assistant', formats: [:json], resource: resource.assistant
end end
json.created_at resource.created_at.to_i json.created_at resource.created_at.to_i
if resource.document
json.document do if resource.documentable
json.id resource.document.id json.documentable do
json.external_link resource.document.external_link json.type resource.documentable_type
json.name resource.document.name
case resource.documentable_type
when 'Captain::Document'
json.id resource.documentable.id
json.external_link resource.documentable.external_link
json.name resource.documentable.name
when 'Conversation'
json.id resource.documentable.display_id
json.display_id resource.documentable.display_id
when 'User'
json.id resource.documentable.id
json.email resource.documentable.email
json.available_name resource.documentable.available_name
end
end end
end end
json.id resource.id json.id resource.id
json.question resource.question json.question resource.question
json.updated_at resource.updated_at.to_i json.updated_at resource.updated_at.to_i

View File

@@ -19,7 +19,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request
create_list(:captain_assistant_response, 30, create_list(:captain_assistant_response, 30,
account: account, account: account,
assistant: assistant, assistant: assistant,
document: document) documentable: document)
end end
it 'returns first page of responses with default pagination' do it 'returns first page of responses with default pagination' do
@@ -48,11 +48,11 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request
create_list(:captain_assistant_response, 3, create_list(:captain_assistant_response, 3,
account: account, account: account,
assistant: assistant, assistant: assistant,
document: document) documentable: document)
create_list(:captain_assistant_response, 2, create_list(:captain_assistant_response, 2,
account: account, account: account,
assistant: another_assistant, assistant: another_assistant,
document: document) documentable: document)
end end
it 'returns only responses for the specified assistant' do it 'returns only responses for the specified assistant' do
@@ -72,11 +72,11 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request
create_list(:captain_assistant_response, 3, create_list(:captain_assistant_response, 3,
account: account, account: account,
assistant: assistant, assistant: assistant,
document: document) documentable: document)
create_list(:captain_assistant_response, 2, create_list(:captain_assistant_response, 2,
account: account, account: account,
assistant: assistant, assistant: assistant,
document: another_document) documentable: another_document)
end end
it 'returns only responses for the specified document' do it 'returns only responses for the specified document' do
@@ -87,7 +87,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::AssistantResponses', type: :request
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json_response[:payload].length).to eq(3) expect(json_response[:payload].length).to eq(3)
expect(json_response[:payload][0][:document][:id]).to eq(document.id) expect(json_response[:payload][0][:documentable][:id]).to eq(document.id)
end end
end end
end end

View File

@@ -21,7 +21,7 @@ RSpec.describe Captain::Documents::ResponseBuilderJob, type: :job do
describe '#perform' do describe '#perform' do
context 'when processing a document' do context 'when processing a document' do
it 'deletes previous responses' do it 'deletes previous responses' do
existing_response = create(:captain_assistant_response, document: document) existing_response = create(:captain_assistant_response, documentable: document)
described_class.new.perform(document) described_class.new.perform(document)
@@ -40,7 +40,7 @@ RSpec.describe Captain::Documents::ResponseBuilderJob, type: :job do
expect(first_response.question).to eq('What is Ruby?') expect(first_response.question).to eq('What is Ruby?')
expect(first_response.answer).to eq('A programming language') expect(first_response.answer).to eq('A programming language')
expect(first_response.assistant).to eq(assistant) expect(first_response.assistant).to eq(assistant)
expect(first_response.document).to eq(document) expect(first_response.documentable).to eq(document)
end end
end end
end end

View File

@@ -49,10 +49,10 @@ RSpec.describe Captain::Llm::ConversationFaqService do
it 'saves the correct FAQ content' do it 'saves the correct FAQ content' do
service.generate_and_deduplicate service.generate_and_deduplicate
expect( expect(
captain_assistant.responses.pluck(:question, :answer, :status) captain_assistant.responses.pluck(:question, :answer, :status, :documentable_id)
).to contain_exactly( ).to contain_exactly(
['What is the purpose?', 'To help users.', 'pending'], ['What is the purpose?', 'To help users.', 'pending', conversation.id],
['How does it work?', 'Through AI.', 'pending'] ['How does it work?', 'Through AI.', 'pending', conversation.id]
) )
end end
end end