Class: Crm::AssistantBrainController

Inherits:
CrmController show all
Defined in:
app/controllers/crm/assistant_brain_controller.rb

Overview

CRM controller for managing Sunny brain entries.

Brain entries are persistent rules and facts injected into Sunny's system prompt.
Two scopes exist:

  • global: apply to all users — managed by sunny_administrators only
  • user: personal preferences for one user — any user can manage their own

The index is a filterable triage table (mirroring /listing_issues): stat cards +
a filter bar + a sortable, paginated table, surfacing the pending-review queue and
each entry's scope (Global / Service-scoped / Personal) and kind (Directive / Skill).

Actions:
GET /assistant_brain — filterable triage table
POST /assistant_brain — create a new entry
PATCH /assistant_brain/:id — update title, rule, category, status, applies_to_services
DELETE /assistant_brain/:id — destroy (global: sunny_admin; personal: owner)
PATCH /assistant_brain/:id/approve — approve a pending auto_learned entry (sunny_admin)
PATCH /assistant_brain/:id/reject — reject a pending entry (sunny_admin)
PATCH /assistant_brain/bulk_approve — approve every checked pending entry (sunny_admin)
PATCH /assistant_brain/bulk_reject — reject every checked pending entry (sunny_admin)

Constant Summary

Constants included from Controllers::ReferenceFindable

Controllers::ReferenceFindable::ID_EMBEDDED_PATTERNS

Constants included from Controllers::AnalyticsEvents

Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY

Constants included from Controllers::ErrorRendering

Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES

Instance Method Summary collapse

Methods inherited from CrmController

#access_denied, #context_id, #context_object, #crm_home_path, #current_ability, #default_url_options, #download_temp, #get_tempfile_path_for_download, #init_status_job_collector, #initialize_crm_lazy_chunks, #persist_enqueued_status_jobs, #record_not_found, #redirect_to_job_or_fallback, #render_edit_action, #set_context, #set_download_path, #stash_file_for_temp_download, #sync_admin_presence_cookie, #touch_employee_last_seen

Methods inherited from ApplicationController

#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y

Methods included from Controllers::ReturnPathHandling

#check_for_return_path, #redirect_to_return_path_or_default

Methods included from Controllers::AnalyticsEvents

#consume_queued_analytics_events, #registration_lead_type, #track_event

Methods included from Controllers::DeviceDetection

#device_detector, #is_ie?

Methods included from Controllers::SubdomainDetection

#is_crm_request?, #is_www_request?, #json_request?

Methods included from Controllers::TurboSafeRedirect

#redirect_to

Methods included from Controllers::TrackingDetection

#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?

Methods included from Controllers::AcceleratedFileSending

#send_file_accelerated, #send_upload_accelerated

Methods included from Controllers::ErrorRendering

#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback

Methods included from Controllers::TurnstileVerification

#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!

Methods included from Controllers::CloudflareCaching

edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session

Methods included from Controllers::Webpackable

#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?

Methods included from Controllers::Localizable

#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale

Methods included from Controllers::Authenticable

#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Instance Method Details

#apply_compactionObject

POST /assistant_brain/apply_compaction — apply one compaction recommendation
the admin agreed to: archive rule(s), tighten a rule's text, or merge a set
into one (the first rule survives with the merged text; the rest deactivate).
Streams back the changed table rows, the refreshed weight meter, and an
"applied" confirmation in place of the recommendation.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'app/controllers/crm/assistant_brain_controller.rb', line 53

def apply_compaction
  kind    = params[:kind].to_s
  ids     = Array(params[:entry_ids]).map(&:to_i).reject(&:zero?)
  by_id   = AssistantBrainEntry.global_scope.where(id: ids).index_by(&:id)
  ordered = ids.filter_map { |id| by_id[id] }

  return redirect_to assistant_brain_index_path, alert: 'Could not apply that recommendation — it may no longer be valid.' if Assistant::BrainCompactor::KINDS.exclude?(kind) || ordered.empty?

  apply_recommendation(kind, ordered, params[:suggestion].to_s)
  @weight = always_injected_weight

  respond_to do |format|
    format.turbo_stream do
      render turbo_stream: [
        *ordered.flat_map { |entry| replace_row(entry) },
        turbo_stream.replace('brain-weight-panel', partial: 'weight_meter', locals: { weight: @weight }),
        turbo_stream.replace("brain-rec-#{params[:rec_id]}", partial: 'compaction_applied',
                                                             locals: { rec_id: params[:rec_id], kind: kind, count: ordered.size })
      ]
    end
    format.html { redirect_to assistant_brain_index_path, notice: 'Recommendation applied.' }
  end
end

#approveObject

PATCH /assistant_brain/:id/approve



150
151
152
153
154
155
156
157
158
159
# File 'app/controllers/crm/assistant_brain_controller.rb', line 150

def approve
  return redirect_to assistant_brain_index_path, alert: 'Entry is not pending approval.' unless @entry.pending?

  @entry.approve!(current_user)

  respond_to do |format|
    format.turbo_stream { render turbo_stream: replace_row(@entry) }
    format.html { redirect_to assistant_brain_index_path, notice: "\"#{@entry.title}\" approved and now active." }
  end
end

#bulk_approveObject

PATCH /assistant_brain/bulk_approve — approve every checked pending entry.
Transitions each record through approve! (not update_all) so the same
validations / callbacks / side effects run as the per-row approve.



176
177
178
179
180
181
182
# File 'app/controllers/crm/assistant_brain_controller.rb', line 176

def bulk_approve
  entries = selected_pending
  return redirect_back_or_to(assistant_brain_index_path, alert: 'No rules selected.') if entries.empty?

  AssistantBrainEntry.transaction { entries.each { |entry| entry.approve!(current_user) } }
  redirect_back_or_to(assistant_brain_index_path, notice: "#{entries.size} rule(s) approved and now active.")
end

#bulk_rejectObject

PATCH /assistant_brain/bulk_reject — reject every checked pending entry.



185
186
187
188
189
190
191
# File 'app/controllers/crm/assistant_brain_controller.rb', line 185

def bulk_reject
  entries = selected_pending
  return redirect_back_or_to(assistant_brain_index_path, alert: 'No rules selected.') if entries.empty?

  AssistantBrainEntry.transaction { entries.each(&:reject!) }
  redirect_back_or_to(assistant_brain_index_path, notice: "#{entries.size} rule(s) rejected.")
end

#compactObject

POST /assistant_brain/compact — run the LLM compaction analysis over the active
global rules and stream back consolidate / tighten / archive recommendations.



35
36
37
38
39
40
41
42
43
44
45
46
# File 'app/controllers/crm/assistant_brain_controller.rb', line 35

def compact
  @compaction = Assistant::BrainCompactor.call

  respond_to do |format|
    format.turbo_stream do
      render turbo_stream: turbo_stream.replace('brain-compaction-panel',
                                                partial: 'compaction_panel',
                                                locals:  { compaction: @compaction })
    end
    format.html { redirect_to assistant_brain_index_path }
  end
end

#createObject

POST /assistant_brain



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'app/controllers/crm/assistant_brain_controller.rb', line 78

def create
  @entry = AssistantBrainEntry.new(entry_params)
  @entry.source       = 'manual'
  @entry.suggested_by = current_user

  personal = @entry.scope == 'user'

  if personal
    @entry.user_id = current_user.id
    @entry.status  = 'active'
  else
    return redirect_to assistant_brain_index_path, alert: 'Only sunny administrators can create global rules.' unless sunny_admin?

    @entry.status = 'active'
  end

  if @entry.save
    redirect_to assistant_brain_index_path,
                notice: "#{personal ? 'Personal' : 'Global'} brain rule \"#{@entry.title}\" added."
  else
    load_index_data
    @new_entry    = @entry.scope == 'global' ? @entry : AssistantBrainEntry.new(scope: 'global')
    @new_personal = @entry.scope == 'user'   ? @entry : AssistantBrainEntry.new(scope: 'user')
    flash.now[:alert] = "Could not create brain rule: #{@entry.errors.full_messages.to_sentence}"
    render :index, status: :unprocessable_content
  end
end

#destroyObject

DELETE /assistant_brain/:id



134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'app/controllers/crm/assistant_brain_controller.rb', line 134

def destroy
  title = @entry.title
  @entry.destroy!

  respond_to do |format|
    format.turbo_stream do
      render turbo_stream: [
        turbo_stream.remove("brain-entry-#{@entry.id}"),
        turbo_stream.remove("brain-entry-edit-#{@entry.id}")
      ]
    end
    format.html { redirect_to assistant_brain_index_path, notice: "Brain rule \"#{title}\" deleted." }
  end
end

#indexObject

GET /assistant_brain



29
30
31
# File 'app/controllers/crm/assistant_brain_controller.rb', line 29

def index
  load_index_data
end

#rejectObject

PATCH /assistant_brain/:id/reject



162
163
164
165
166
167
168
169
170
171
# File 'app/controllers/crm/assistant_brain_controller.rb', line 162

def reject
  return redirect_to assistant_brain_index_path, alert: 'Entry is not pending approval.' unless @entry.pending?

  @entry.reject!

  respond_to do |format|
    format.turbo_stream { render turbo_stream: replace_row(@entry) }
    format.html { redirect_to assistant_brain_index_path, notice: 'Brain rule rejected.' }
  end
end

#updateObject

PATCH /assistant_brain/:id



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/controllers/crm/assistant_brain_controller.rb', line 107

def update
  update_attrs = entry_params

  # Coerce applies_to_services from checkboxes: may arrive as hash of "1"/"0" or array
  if update_attrs[:applies_to_services].is_a?(Hash)
    update_attrs = update_attrs.merge(
      applies_to_services: update_attrs[:applies_to_services].select { |_, v| v == '1' }.keys
    )
  end

  if @entry.update(update_attrs)
    respond_to do |format|
      format.turbo_stream { render turbo_stream: replace_row(@entry) }
      format.html { redirect_to assistant_brain_index_path, notice: 'Brain rule updated.' }
    end
  else
    respond_to do |format|
      format.turbo_stream { render turbo_stream: replace_row(@entry) }
      format.html do
        redirect_to assistant_brain_index_path,
                    alert: "Could not update: #{@entry.errors.full_messages.to_sentence}"
      end
    end
  end
end