feat: migrations for document auto-sync [AI-141] (#14041)

# Pull Request Template

## Description

Add migrations for document auto-sync

Fixes # (issue)

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?
locally

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Aakash Bakhle
2026-04-15 17:56:10 +05:30
committed by GitHub
parent b96bf41234
commit 5264de24b0
8 changed files with 85 additions and 11 deletions

View File

@@ -0,0 +1,5 @@
class AddEditedToCaptainAssistantResponses < ActiveRecord::Migration[7.0]
def change
add_column :captain_assistant_responses, :edited, :boolean, default: false, null: false
end
end

View File

@@ -0,0 +1,11 @@
class AddSyncColumnsToCaptainDocuments < ActiveRecord::Migration[7.0]
def change
change_table :captain_documents, bulk: true do |t|
t.integer :sync_status
t.datetime :last_synced_at
t.datetime :last_sync_attempted_at
end
add_index :captain_documents, [:account_id, :sync_status]
end
end

View File

@@ -0,0 +1,20 @@
class BackfillEditedOnCaptainAssistantResponses < ActiveRecord::Migration[7.0]
def up
return unless ChatwootApp.enterprise?
# rubocop:disable Rails/SkipsModelValidations
# NOTE: Since there is no way of knowing currently which FAQs were edited by a human
# we use a heuristic based on time passed between created_at and updated_at.
# 15 days is arbitrary but seems reasonable for a user to go back and edit an FAQ.
Captain::AssistantResponse
.where('updated_at - created_at > make_interval(days := ?)', 15)
.in_batches(of: 1000) do |batch|
batch.update_all(edited: true)
end
# rubocop:enable Rails/SkipsModelValidations
end
def down
# no-op: rolling back migration of edited column will drop the edited column entirely
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2026_04_09_091202) do
ActiveRecord::Schema[7.1].define(version: 2026_04_10_092753) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -329,6 +329,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_09_091202) do
t.datetime "updated_at", null: false
t.integer "status", default: 1, null: false
t.string "documentable_type"
t.boolean "edited", default: false, null: false
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 ["documentable_id", "documentable_type"], name: "idx_cap_asst_resp_on_documentable"
@@ -377,10 +378,14 @@ ActiveRecord::Schema[7.1].define(version: 2026_04_09_091202) do
t.datetime "updated_at", null: false
t.integer "status", default: 0, null: false
t.jsonb "metadata", default: {}
t.integer "sync_status"
t.datetime "last_synced_at"
t.datetime "last_sync_attempted_at"
t.index ["account_id"], name: "index_captain_documents_on_account_id"
t.index ["assistant_id", "external_link"], name: "index_captain_documents_on_assistant_id_and_external_link", unique: true
t.index ["assistant_id"], name: "index_captain_documents_on_assistant_id"
t.index ["status"], name: "index_captain_documents_on_status"
t.index ["account_id", "sync_status"], name: "index_captain_documents_on_account_id_and_sync_status"
end
create_table "captain_inboxes", force: :cascade do |t|

View File

@@ -5,6 +5,7 @@
# id :bigint not null, primary key
# answer :text not null
# documentable_type :string
# edited :boolean default(FALSE), not null
# embedding :vector(1536)
# question :string not null
# status :integer default("approved"), not null
@@ -35,6 +36,7 @@ class Captain::AssistantResponse < ApplicationRecord
before_validation :ensure_account
before_validation :ensure_status
before_validation :mark_as_edited, on: :update
after_commit :update_response_embedding
scope :ordered, -> { order(created_at: :desc) }
@@ -55,6 +57,10 @@ class Captain::AssistantResponse < ApplicationRecord
self.status ||= :approved
end
def mark_as_edited
self.edited = true if question_changed? || answer_changed?
end
def ensure_account
self.account = assistant&.account
end

View File

@@ -5,9 +5,12 @@
# id :bigint not null, primary key
# content :text
# external_link :string not null
# last_sync_attempted_at :datetime
# last_synced_at :datetime
# metadata :jsonb
# name :string
# status :integer default("in_progress"), not null
# sync_status :integer
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
@@ -16,6 +19,7 @@
# Indexes
#
# index_captain_documents_on_account_id (account_id)
# index_captain_documents_on_account_id_and_sync_status (account_id,sync_status)
# index_captain_documents_on_assistant_id (assistant_id)
# index_captain_documents_on_assistant_id_and_external_link (assistant_id,external_link) UNIQUE
# index_captain_documents_on_status (status)
@@ -44,6 +48,8 @@ class Captain::Document < ApplicationRecord
available: 1
}
enum :sync_status, { syncing: 0, synced: 1, failed: 2 }, prefix: :sync
before_create :ensure_within_plan_limit
after_create_commit :enqueue_crawl_job
after_create_commit :update_document_usage
@@ -68,6 +74,22 @@ class Captain::Document < ApplicationRecord
pdf_file.blob.byte_size if pdf_file.attached?
end
def content_fingerprint
metadata&.dig('content_fingerprint')
end
def content_fingerprint=(value)
self.metadata = (metadata || {}).merge('content_fingerprint' => value)
end
def last_sync_error_code
metadata&.dig('last_sync_error_code')
end
def last_sync_error_code=(value)
self.metadata = (metadata || {}).merge('last_sync_error_code' => value)
end
def openai_file_id
metadata&.dig('openai_file_id')
end

View File

@@ -29,3 +29,4 @@ json.id resource.id
json.question resource.question
json.updated_at resource.updated_at.to_i
json.status resource.status
json.edited resource.edited

View File

@@ -11,4 +11,8 @@ json.file_size resource.file_size
json.id resource.id
json.name resource.name
json.status resource.status
json.sync_status resource.sync_status
json.last_synced_at resource.last_synced_at&.to_i
json.last_sync_attempted_at resource.last_sync_attempted_at&.to_i
json.last_sync_error_code resource.last_sync_error_code
json.updated_at resource.updated_at.to_i