diff --git a/db/migrate/20260410092751_add_edited_to_captain_assistant_responses.rb b/db/migrate/20260410092751_add_edited_to_captain_assistant_responses.rb new file mode 100644 index 000000000..916bae3e5 --- /dev/null +++ b/db/migrate/20260410092751_add_edited_to_captain_assistant_responses.rb @@ -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 diff --git a/db/migrate/20260410092752_add_sync_columns_to_captain_documents.rb b/db/migrate/20260410092752_add_sync_columns_to_captain_documents.rb new file mode 100644 index 000000000..f1a86b44c --- /dev/null +++ b/db/migrate/20260410092752_add_sync_columns_to_captain_documents.rb @@ -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 diff --git a/db/migrate/20260410092753_backfill_edited_on_captain_assistant_responses.rb b/db/migrate/20260410092753_backfill_edited_on_captain_assistant_responses.rb new file mode 100644 index 000000000..eb0235e70 --- /dev/null +++ b/db/migrate/20260410092753_backfill_edited_on_captain_assistant_responses.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 360bddb69..a143f593d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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| diff --git a/enterprise/app/models/captain/assistant_response.rb b/enterprise/app/models/captain/assistant_response.rb index 12dcab1cc..db2ac058a 100644 --- a/enterprise/app/models/captain/assistant_response.rb +++ b/enterprise/app/models/captain/assistant_response.rb @@ -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 diff --git a/enterprise/app/models/captain/document.rb b/enterprise/app/models/captain/document.rb index 751162b28..d8e07a2d9 100644 --- a/enterprise/app/models/captain/document.rb +++ b/enterprise/app/models/captain/document.rb @@ -2,20 +2,24 @@ # # Table name: captain_documents # -# id :bigint not null, primary key -# content :text -# external_link :string not null -# metadata :jsonb -# name :string -# status :integer default("in_progress"), not null -# created_at :datetime not null -# updated_at :datetime not null -# account_id :bigint not null -# assistant_id :bigint not null +# 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 +# assistant_id :bigint not null # # 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 diff --git a/enterprise/app/views/api/v1/models/captain/_assistant_response.json.jbuilder b/enterprise/app/views/api/v1/models/captain/_assistant_response.json.jbuilder index c118c7647..9412b8c03 100644 --- a/enterprise/app/views/api/v1/models/captain/_assistant_response.json.jbuilder +++ b/enterprise/app/views/api/v1/models/captain/_assistant_response.json.jbuilder @@ -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 diff --git a/enterprise/app/views/api/v1/models/captain/_document.json.jbuilder b/enterprise/app/views/api/v1/models/captain/_document.json.jbuilder index 8064a5181..62710bc64 100644 --- a/enterprise/app/views/api/v1/models/captain/_document.json.jbuilder +++ b/enterprise/app/views/api/v1/models/captain/_document.json.jbuilder @@ -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