Class: Crm::AssistantBrainController
- Inherits:
-
CrmController
- Object
- ActionController::Base
- ApplicationController
- CrmController
- Crm::AssistantBrainController
- 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
-
#apply_compaction ⇒ Object
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).
-
#approve ⇒ Object
PATCH /assistant_brain/:id/approve.
-
#bulk_approve ⇒ Object
PATCH /assistant_brain/bulk_approve — approve every checked pending entry.
-
#bulk_reject ⇒ Object
PATCH /assistant_brain/bulk_reject — reject every checked pending entry.
-
#compact ⇒ Object
POST /assistant_brain/compact — run the LLM compaction analysis over the active global rules and stream back consolidate / tighten / archive recommendations.
-
#create ⇒ Object
POST /assistant_brain.
-
#destroy ⇒ Object
DELETE /assistant_brain/:id.
-
#index ⇒ Object
GET /assistant_brain.
-
#reject ⇒ Object
PATCH /assistant_brain/:id/reject.
-
#update ⇒ Object
PATCH /assistant_brain/:id.
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
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
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_compaction ⇒ Object
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 |
#approve ⇒ Object
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_approve ⇒ Object
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_reject ⇒ Object
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 |
#compact ⇒ Object
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 |
#create ⇒ Object
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..to_sentence}" render :index, status: :unprocessable_content end end |
#destroy ⇒ Object
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 |
#index ⇒ Object
GET /assistant_brain
29 30 31 |
# File 'app/controllers/crm/assistant_brain_controller.rb', line 29 def index load_index_data end |
#reject ⇒ Object
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 |
#update ⇒ Object
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..to_sentence}" end end end end |