Class: AssistantBrainEntry

Inherits:
ApplicationRecord show all
Includes:
Models::Embeddable
Defined in:
app/models/assistant_brain_entry.rb

Overview

== Schema Information

Table name: assistant_brain_entries
Database name: primary

id :bigint not null, primary key
applies_to_services :string default([]), not null, is an Array
category :string not null
priority :enum default("normal"), not null
rule :text not null
scope :string default("global"), not null
source :string default("manual"), not null
status :string default("active"), not null
title :string not null
created_at :datetime not null
updated_at :datetime not null
approved_by_id :bigint
assistant_conversation_id :bigint
suggested_by_id :bigint
user_id :bigint

Indexes

index_assistant_brain_entries_on_category (category)
index_assistant_brain_entries_on_scope (scope)
index_assistant_brain_entries_on_status_and_category (status,category)
index_assistant_brain_entries_on_status_and_priority (status,priority)
index_assistant_brain_entries_on_user_id (user_id)

Constant Summary collapse

CATEGORIES =

Categories.

%w[url_rules product_data content_rules seo_rules general].freeze
STATUSES =

Statuses.

%w[active pending inactive].freeze
SOURCES =

Sources.

%w[manual auto_learned].freeze
SCOPES =

Scopes.

%w[global user].freeze
CATEGORY_LABELS =

Category labels.

{
  'url_rules'      => 'URL Rules',
  'product_data'   => 'Product Data',
  'content_rules'  => 'Content Rules',
  'seo_rules'      => 'SEO Rules',
  'general'        => 'General'
}.freeze

Constants included from Models::Embeddable

Models::Embeddable::MAX_CONTENT_LENGTH

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Embeddable

#content_embeddings, embeddable_content_types, #embeddable_locales, #embedding_content_hash, embedding_partition_class, #embedding_stale?, #embedding_type_name, #embedding_vector, #find_content_embedding, #find_similar, #generate_all_embeddings!, #generate_chunked_embeddings!, #generate_embedding!, #has_embedding?, #locale_for_embedding, #needs_chunking?, regenerate_all_embeddings, semantic_search

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#categoryObject (readonly)



62
# File 'app/models/assistant_brain_entry.rb', line 62

validates :category, presence: true, inclusion: { in: CATEGORIES }

#ruleObject (readonly)



64
# File 'app/models/assistant_brain_entry.rb', line 64

validates :rule,     presence: true

#scopeObject (readonly)



67
# File 'app/models/assistant_brain_entry.rb', line 67

validates :scope,    presence: true, inclusion: { in: SCOPES }

#sourceObject (readonly)



66
# File 'app/models/assistant_brain_entry.rb', line 66

validates :source,   presence: true, inclusion: { in: SOURCES }

#statusObject (readonly)



65
# File 'app/models/assistant_brain_entry.rb', line 65

validates :status,   presence: true, inclusion: { in: STATUSES }

#titleObject (readonly)



63
# File 'app/models/assistant_brain_entry.rb', line 63

validates :title,    presence: true

#user_idObject (readonly)



68
# File 'app/models/assistant_brain_entry.rb', line 68

validates :user_id,  presence: true, if: -> { scope == 'user' }

Class Method Details

.activeActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are active. Active Record Scope

Returns:

See Also:



70
# File 'app/models/assistant_brain_entry.rb', line 70

scope :active,       -> { where(status: 'active') }

.auto_learnedActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are auto learned. Active Record Scope

Returns:

See Also:



78
# File 'app/models/assistant_brain_entry.rb', line 78

scope :auto_learned, -> { where(source: 'auto_learned') }

.by_categoryActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are by category. Active Record Scope

Returns:

See Also:



73
# File 'app/models/assistant_brain_entry.rb', line 73

scope :by_category,  -> { order(:category, :title) }

.for_context(user_id:, active_services: []) ⇒ ActiveRecord::Relation

Fetch all entries applicable to a given user and set of active tool services.
Returns global entries + the user's personal entries, filtered by service applicability.

Parameters:

  • user_id (Integer)

    the current user's ID

  • active_services (Array<String>) (defaults to: [])

    tool services enabled for this conversation

Returns:

  • (ActiveRecord::Relation)


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'app/models/assistant_brain_entry.rb', line 86

def self.for_context(user_id:, active_services: [])
  base = active.where('scope = ? OR (scope = ? AND user_id = ?)', 'global', 'user', user_id)

  if active_services.present?
    # Inject if: applies_to_services is empty (universal) OR overlaps with active services.
    # Cast both sides to varchar[] so PostgreSQL can use the && (overlap) operator.
    base.where(
      'cardinality(applies_to_services) = 0 OR applies_to_services && ARRAY[?]::varchar[]',
      active_services
    )
  else
    # No tools active — only inject universal entries (no service filter)
    base.where('cardinality(applies_to_services) = 0')
  end
end

.for_userActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are for user. Active Record Scope

Returns:

See Also:



76
# File 'app/models/assistant_brain_entry.rb', line 76

scope :for_user,     ->(user_id) { where(scope: 'user', user_id: user_id) }

.global_scopeActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are global scope. Active Record Scope

Returns:

See Also:



74
# File 'app/models/assistant_brain_entry.rb', line 74

scope :global_scope, -> { where(scope: 'global') }

.inactiveActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are inactive. Active Record Scope

Returns:

See Also:



72
# File 'app/models/assistant_brain_entry.rb', line 72

scope :inactive,     -> { where(status: 'inactive') }

.manualActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are manual. Active Record Scope

Returns:

See Also:



77
# File 'app/models/assistant_brain_entry.rb', line 77

scope :manual,       -> { where(source: 'manual') }

.pendingActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are pending. Active Record Scope

Returns:

See Also:



71
# File 'app/models/assistant_brain_entry.rb', line 71

scope :pending,      -> { where(status: 'pending') }

.ransackable_attributes(_auth_object = nil) ⇒ Object



158
159
160
# File 'app/models/assistant_brain_entry.rb', line 158

def self.ransackable_attributes(_auth_object = nil)
  %w[title rule category status source scope priority created_at updated_at]
end

.semantic_search_for(query, within_ids:, limit: 20) ⇒ Array<AssistantBrainEntry>

Find the most semantically relevant active brain entries for a user query.
Used by ChatService when the total entry count exceeds the injection threshold
and full verbatim injection would bloat the system prompt.

Parameters:

  • query (String)

    the current user message to match against

  • within_ids (Array<Integer>)

    pre-filtered candidate IDs (service-scoped)

  • limit (Integer) (defaults to: 20)

    max entries to return (default 20)

Returns:



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'app/models/assistant_brain_entry.rb', line 126

def self.semantic_search_for(query, within_ids:, limit: 20)
  return [] if query.blank? || within_ids.empty?

  query_vector = ContentEmbedding.generate_query_embedding(query)
  return [] unless query_vector

  ContentEmbedding
    .where(embeddable_type: 'AssistantBrainEntry', embeddable_id: within_ids)
    .by_model(ContentEmbedding::UNIFIED_MODELS)
    .with_unified_embedding
    .nearest_neighbors(:unified_embedding, query_vector, distance: :cosine)
    .limit(limit)
    .includes(:embeddable)
    .filter_map(&:embeddable)
end

.user_scopeActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are user scope. Active Record Scope

Returns:

See Also:



75
# File 'app/models/assistant_brain_entry.rb', line 75

scope :user_scope,   -> { where(scope: 'user') }

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


106
# File 'app/models/assistant_brain_entry.rb', line 106

def active?    = status == 'active'

#applies_everywhere?Boolean

Returns:

  • (Boolean)


113
114
115
# File 'app/models/assistant_brain_entry.rb', line 113

def applies_everywhere?
  applies_to_services.blank?
end

#approve!(approver) ⇒ Object



150
151
152
# File 'app/models/assistant_brain_entry.rb', line 150

def approve!(approver)
  update!(status: 'active', approved_by: approver)
end

#approved_byParty

Returns:

See Also:



59
# File 'app/models/assistant_brain_entry.rb', line 59

belongs_to :approved_by,            class_name: 'Party',                 optional: true

#assistant_conversationAssistantConversation



60
# File 'app/models/assistant_brain_entry.rb', line 60

belongs_to :assistant_conversation, class_name: 'AssistantConversation', optional: true

#auto_learned?Boolean

Returns:

  • (Boolean)


109
# File 'app/models/assistant_brain_entry.rb', line 109

def auto_learned? = source == 'auto_learned'

#category_labelObject



102
103
104
# File 'app/models/assistant_brain_entry.rb', line 102

def category_label
  CATEGORY_LABELS.fetch(category, category.humanize)
end

#content_for_embedding(_content_type = :primary) ⇒ Object

Embed the full title + rule text so retrieval matches on both what the rule
is about (title) and what it says (rule body).



146
147
148
# File 'app/models/assistant_brain_entry.rb', line 146

def content_for_embedding(_content_type = :primary)
  "#{title}\n\n#{rule}"
end

#global?Boolean

Returns:

  • (Boolean)


110
# File 'app/models/assistant_brain_entry.rb', line 110

def global?    = scope == 'global'

#inactive?Boolean

Returns:

  • (Boolean)


108
# File 'app/models/assistant_brain_entry.rb', line 108

def inactive?  = status == 'inactive'

#pending?Boolean

Returns:

  • (Boolean)


107
# File 'app/models/assistant_brain_entry.rb', line 107

def pending?   = status == 'pending'

#personal?Boolean

Returns:

  • (Boolean)


111
# File 'app/models/assistant_brain_entry.rb', line 111

def personal?  = scope == 'user'

#reject!Object



154
155
156
# File 'app/models/assistant_brain_entry.rb', line 154

def reject!
  update!(status: 'inactive')
end

#suggested_byParty

Returns:

See Also:



58
# File 'app/models/assistant_brain_entry.rb', line 58

belongs_to :suggested_by,           class_name: 'Party',                 optional: true