mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	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:
		| @@ -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> | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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": "", | ||||||
|   | |||||||
| @@ -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> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -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}" | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pranav
					Pranav