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
source_message_id :bigint
suggested_by_id :bigint
user_id :bigint

Indexes

idx_brain_entries_unique_source_message (user_id,source_message_id) UNIQUE WHERE (source_message_id IS NOT NULL)
index_assistant_brain_entries_on_category (category)
index_assistant_brain_entries_on_scope (scope)
index_assistant_brain_entries_on_source_message_id (source_message_id)
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
ALWAYS_INJECTED_SOFT_BUDGET =

Soft ceiling (estimated tokens) for the always-injected set — the active,
critical, permanent (untagged) global rules rendered verbatim into every
conversation. Drives the "context weight" indicator on the triage page; the
bar turns amber past 60% and red past 100%. A guide, not a hard limit.

2_000
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 Models::Schedulable

Models::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_current?, #embedding_eligible?, 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, with_shape_lock

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#categoryObject (readonly)



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

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

#ruleObject (readonly)



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

validates :rule,     presence: true

#scopeObject (readonly)



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

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

#sourceObject (readonly)



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

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

#statusObject (readonly)



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

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

#titleObject (readonly)



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

validates :title,    presence: true

#user_idObject (readonly)



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

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:



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

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

.auto_learnedActiveRecord::Relation<AssistantBrainEntry>

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

Returns:

See Also:



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

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:



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

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)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'app/models/assistant_brain_entry.rb', line 108

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:



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

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:



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

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

.inactiveActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are inactive. Active Record Scope

Returns:

See Also:



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

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

.manualActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are manual. Active Record Scope

Returns:

See Also:



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

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

.pendingActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are pending. Active Record Scope

Returns:

See Also:



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

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

.ransackable_attributes(_auth_object = nil) ⇒ Object



193
194
195
# File 'app/models/assistant_brain_entry.rb', line 193

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

.saved_from_messageActiveRecord::Relation<AssistantBrainEntry>

A relation of AssistantBrainEntries that are saved from message. Active Record Scope

Returns:

See Also:



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

scope :saved_from_message, ->(user_id, message_id) { where(scope: 'user', user_id: user_id, source_message_id: message_id) }

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



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'app/models/assistant_brain_entry.rb', line 161

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:



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

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

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


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

def active?    = status == 'active'

#applies_everywhere?Boolean

Returns:

  • (Boolean)


141
142
143
# File 'app/models/assistant_brain_entry.rb', line 141

def applies_everywhere?
  applies_to_services.blank?
end

#approve!(approver) ⇒ Object



185
186
187
# File 'app/models/assistant_brain_entry.rb', line 185

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

#approved_byParty

Returns:

See Also:



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

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

#assistant_conversationAssistantConversation



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

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

#auto_learned?Boolean

Returns:

  • (Boolean)


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

def auto_learned? = source == 'auto_learned'

#category_labelObject



124
125
126
# File 'app/models/assistant_brain_entry.rb', line 124

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



181
182
183
# File 'app/models/assistant_brain_entry.rb', line 181

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

#directive?Boolean

Kind (the delivery axis), backed by priority: a Directive is always-on —
injected verbatim into every in-scope system prompt; a Skill is on-demand —
retrieved by search_brain only when relevant.

Returns:

  • (Boolean)


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

def directive? = priority_critical?

#estimated_tokensObject

Estimated token footprint of this rule's text (chars/4 heuristic, matching
Assistant::ChatService.estimate_tokens). For critical rules this is the weight
added to every in-scope system prompt; normal rules cost nothing until searched.



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

def estimated_tokens
  Assistant::ChatService.estimate_tokens(rule)
end

#global?Boolean

Returns:

  • (Boolean)


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

def global?    = scope == 'global'

#inactive?Boolean

Returns:

  • (Boolean)


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

def inactive?  = status == 'inactive'

#pending?Boolean

Returns:

  • (Boolean)


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

def pending?   = status == 'pending'

#personal?Boolean

Returns:

  • (Boolean)


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

def personal?  = scope == 'user'

#reject!Object



189
190
191
# File 'app/models/assistant_brain_entry.rb', line 189

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

#skill?Boolean

Returns:

  • (Boolean)


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

def skill?     = priority_normal?

#source_messageAssistantMessage

Set when a rep saves an assistant answer from a chat via the in-chat "Save to
brain" button — links the personal entry back to the message it came from.



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

belongs_to :source_message,         class_name: 'AssistantMessage',      optional: true

#suggested_byParty

Returns:

See Also:



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

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