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 :integer 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 =
%w[url_rules product_data content_rules seo_rules general].freeze
STATUSES =
%w[active pending inactive].freeze
SOURCES =
%w[manual auto_learned].freeze
SCOPES =
%w[global user].freeze
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::DEFAULT_MODEL, Models::Embeddable::MAX_CONTENT_LENGTH

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 Models::EventPublishable

#publish_event

Instance Attribute Details

#categoryObject (readonly)



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

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

#ruleObject (readonly)



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

validates :rule,     presence: true

#scopeObject (readonly)



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

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

#sourceObject (readonly)



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

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

#statusObject (readonly)



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

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

#titleObject (readonly)



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

validates :title,    presence: true

#user_idObject (readonly)



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

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:



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

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

.auto_learnedActiveRecord::Relation<AssistantBrainEntry>

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

Returns:

See Also:



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

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:



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

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)


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'app/models/assistant_brain_entry.rb', line 77

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:



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

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:



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

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

.inactiveActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are inactive. Active Record Scope

Returns:

See Also:



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

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

.manualActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are manual. Active Record Scope

Returns:

See Also:



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

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

.pendingActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are pending. Active Record Scope

Returns:

See Also:



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

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

.ransackable_attributes(_auth_object = nil) ⇒ Object



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

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:



117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'app/models/assistant_brain_entry.rb', line 117

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, content_type: 'primary')
    .nearest_neighbors(: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:



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

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

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


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

def active?    = status == 'active'

#applies_everywhere?Boolean

Returns:

  • (Boolean)


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

def applies_everywhere?
  applies_to_services.blank?
end

#approve!(approver) ⇒ Object



139
140
141
# File 'app/models/assistant_brain_entry.rb', line 139

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

#approved_byParty

Returns:

See Also:



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

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

#assistant_conversationAssistantConversation



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

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

#auto_learned?Boolean

Returns:

  • (Boolean)


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

def auto_learned? = source == 'auto_learned'

#category_labelObject



93
94
95
# File 'app/models/assistant_brain_entry.rb', line 93

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).



135
136
137
# File 'app/models/assistant_brain_entry.rb', line 135

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

#global?Boolean

Returns:

  • (Boolean)


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

def global?    = scope == 'global'

#inactive?Boolean

Returns:

  • (Boolean)


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

def inactive?  = status == 'inactive'

#pending?Boolean

Returns:

  • (Boolean)


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

def pending?   = status == 'pending'

#personal?Boolean

Returns:

  • (Boolean)


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

def personal?  = scope == 'user'

#reject!Object



143
144
145
# File 'app/models/assistant_brain_entry.rb', line 143

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

#suggested_byParty

Returns:

See Also:



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

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