sage-rails 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +202 -0
- data/app/assets/images/chevron-down-zinc-500.svg +1 -0
- data/app/assets/images/chevron-right.svg +1 -0
- data/app/assets/images/loading.svg +4 -0
- data/app/assets/images/sage/chevron-down-zinc-500.svg +1 -0
- data/app/assets/images/sage/chevron-right.svg +1 -0
- data/app/assets/images/sage/loading.svg +4 -0
- data/app/assets/javascripts/sage/application.js +18 -0
- data/app/assets/stylesheets/sage/application.css +308 -0
- data/app/controllers/sage/actions_controller.rb +5 -0
- data/app/controllers/sage/application_controller.rb +4 -0
- data/app/controllers/sage/base_controller.rb +10 -0
- data/app/controllers/sage/checks_controller.rb +65 -0
- data/app/controllers/sage/dashboards_controller.rb +130 -0
- data/app/controllers/sage/queries/messages_controller.rb +62 -0
- data/app/controllers/sage/queries_controller.rb +596 -0
- data/app/helpers/sage/application_helper.rb +30 -0
- data/app/helpers/sage/queries_helper.rb +23 -0
- data/app/javascript/controllers/element_removal_controller.js +7 -0
- data/app/javascript/sage/controllers/clipboard_controller.js +26 -0
- data/app/javascript/sage/controllers/dashboard_controller.js +132 -0
- data/app/javascript/sage/controllers/reverse_infinite_scroll_controller.js +146 -0
- data/app/javascript/sage/controllers/search_controller.js +47 -0
- data/app/javascript/sage/controllers/select_controller.js +215 -0
- data/app/javascript/sage.js +19 -0
- data/app/jobs/sage/application_job.rb +4 -0
- data/app/jobs/sage/process_report_job.rb +80 -0
- data/app/mailers/sage/application_mailer.rb +6 -0
- data/app/models/sage/application_record.rb +5 -0
- data/app/models/sage/message.rb +8 -0
- data/app/schemas/sage/report_response_schema.rb +8 -0
- data/app/views/layouts/application.html.erb +34 -0
- data/app/views/layouts/sage/application.html.erb +94 -0
- data/app/views/sage/checks/_form.html.erb +81 -0
- data/app/views/sage/checks/_search.html.erb +8 -0
- data/app/views/sage/checks/edit.html.erb +10 -0
- data/app/views/sage/checks/index.html.erb +58 -0
- data/app/views/sage/checks/new.html.erb +8 -0
- data/app/views/sage/dashboards/_form.html.erb +50 -0
- data/app/views/sage/dashboards/_search.html.erb +8 -0
- data/app/views/sage/dashboards/index.html.erb +58 -0
- data/app/views/sage/dashboards/new.html.erb +8 -0
- data/app/views/sage/dashboards/show.html.erb +58 -0
- data/app/views/sage/messages/_form.html.erb +14 -0
- data/app/views/sage/queries/_caching.html.erb +17 -0
- data/app/views/sage/queries/_form.html.erb +72 -0
- data/app/views/sage/queries/_input.html.erb +17 -0
- data/app/views/sage/queries/_message.html.erb +25 -0
- data/app/views/sage/queries/_message.turbo_stream.erb +10 -0
- data/app/views/sage/queries/_new_form.html.erb +43 -0
- data/app/views/sage/queries/_run.html.erb +232 -0
- data/app/views/sage/queries/_search.html.erb +8 -0
- data/app/views/sage/queries/_statement_box.html.erb +241 -0
- data/app/views/sage/queries/_streaming_message.html.erb +14 -0
- data/app/views/sage/queries/create.turbo_stream.erb +114 -0
- data/app/views/sage/queries/edit.html.erb +48 -0
- data/app/views/sage/queries/index.html.erb +59 -0
- data/app/views/sage/queries/messages/create.turbo_stream.erb +22 -0
- data/app/views/sage/queries/messages/index.html.erb +44 -0
- data/app/views/sage/queries/messages/index.turbo_stream.erb +15 -0
- data/app/views/sage/queries/new.html.erb +195 -0
- data/app/views/sage/queries/run.html.erb +1 -0
- data/app/views/sage/queries/run.turbo_stream.erb +3 -0
- data/app/views/sage/queries/show.html.erb +49 -0
- data/app/views/sage/queries/table_schema.html.erb +77 -0
- data/app/views/sage/shared/_navigation.html.erb +26 -0
- data/app/views/sage/shared/_overlay.html.erb +11 -0
- data/config/importmap.rb +11 -0
- data/config/initializers/pagy.rb +2 -0
- data/config/initializers/ransack.rb +152 -0
- data/config/routes.rb +31 -0
- data/lib/generators/sage/USAGE +13 -0
- data/lib/generators/sage/install/install_generator.rb +128 -0
- data/lib/generators/sage/install/templates/sage.rb +22 -0
- data/lib/sage/database_schema_context.rb +56 -0
- data/lib/sage/engine.rb +260 -0
- data/lib/sage/model_scopes_context.rb +185 -0
- data/lib/sage/report_processor.rb +263 -0
- data/lib/sage/version.rb +3 -0
- data/lib/sage.rb +25 -0
- data/lib/tasks/sage_tasks.rake +4 -0
- metadata +245 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'ransack'
|
2
|
+
|
3
|
+
# Configure Ransack for Blazer models
|
4
|
+
Rails.application.config.after_initialize do
|
5
|
+
# Ensure Blazer is loaded first
|
6
|
+
require 'blazer' if defined?(Blazer)
|
7
|
+
|
8
|
+
# Extend Blazer::Query with Ransack capabilities
|
9
|
+
if defined?(Blazer::Query)
|
10
|
+
# First, ensure Ransack is included in the model
|
11
|
+
unless Blazer::Query.respond_to?(:ransack)
|
12
|
+
Blazer::Query.send(:extend, Ransack::Adapters::ActiveRecord::Base)
|
13
|
+
end
|
14
|
+
|
15
|
+
Blazer::Query.class_eval do
|
16
|
+
# Define which attributes can be searched
|
17
|
+
def self.ransackable_attributes(auth_object = nil)
|
18
|
+
%w[name description statement creator_id created_at updated_at status]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Define which associations can be searched
|
22
|
+
def self.ransackable_associations(auth_object = nil)
|
23
|
+
associations = %w[checks audits dashboard_queries dashboards]
|
24
|
+
associations << 'creator' if Blazer.user_class
|
25
|
+
associations
|
26
|
+
end
|
27
|
+
|
28
|
+
# Optional: Define custom ransack scopes
|
29
|
+
scope :search_by_keywords, ->(keywords) {
|
30
|
+
sanitized = connection.quote_string(keywords.to_s)
|
31
|
+
where("name ILIKE '%#{sanitized}%' OR description ILIKE '%#{sanitized}%' OR statement ILIKE '%#{sanitized}%'")
|
32
|
+
} if !respond_to?(:search_by_keywords)
|
33
|
+
|
34
|
+
# Make the custom scope available to ransack
|
35
|
+
def self.ransackable_scopes(auth_object = nil)
|
36
|
+
%i[search_by_keywords]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Extend Blazer::Dashboard with Ransack capabilities
|
42
|
+
if defined?(Blazer::Dashboard)
|
43
|
+
# First, ensure Ransack is included in the model
|
44
|
+
unless Blazer::Dashboard.respond_to?(:ransack)
|
45
|
+
Blazer::Dashboard.send(:extend, Ransack::Adapters::ActiveRecord::Base)
|
46
|
+
end
|
47
|
+
|
48
|
+
Blazer::Dashboard.class_eval do
|
49
|
+
# Define which attributes can be searched
|
50
|
+
def self.ransackable_attributes(auth_object = nil)
|
51
|
+
%w[name creator_id created_at updated_at]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Define which associations can be searched
|
55
|
+
def self.ransackable_associations(auth_object = nil)
|
56
|
+
associations = %w[dashboard_queries queries]
|
57
|
+
associations << 'creator' if Blazer.user_class
|
58
|
+
associations
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Also configure in to_prepare for development reloading
|
65
|
+
Rails.application.config.to_prepare do
|
66
|
+
if defined?(Blazer::Query)
|
67
|
+
# Ensure Ransack is included
|
68
|
+
unless Blazer::Query.respond_to?(:ransack)
|
69
|
+
Blazer::Query.send(:extend, Ransack::Adapters::ActiveRecord::Base)
|
70
|
+
end
|
71
|
+
|
72
|
+
unless Blazer::Query.respond_to?(:ransackable_attributes)
|
73
|
+
Blazer::Query.class_eval do
|
74
|
+
def self.ransackable_attributes(auth_object = nil)
|
75
|
+
%w[name description statement creator_id created_at updated_at status]
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.ransackable_associations(auth_object = nil)
|
79
|
+
%w[creator checks audits dashboard_queries dashboards]
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.ransackable_scopes(auth_object = nil)
|
83
|
+
%i[search_by_keywords]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
if defined?(Blazer::Dashboard)
|
90
|
+
# Ensure Ransack is included
|
91
|
+
unless Blazer::Dashboard.respond_to?(:ransack)
|
92
|
+
Blazer::Dashboard.send(:extend, Ransack::Adapters::ActiveRecord::Base)
|
93
|
+
end
|
94
|
+
|
95
|
+
unless Blazer::Dashboard.respond_to?(:ransackable_attributes)
|
96
|
+
Blazer::Dashboard.class_eval do
|
97
|
+
def self.ransackable_attributes(auth_object = nil)
|
98
|
+
%w[name creator_id created_at updated_at]
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.ransackable_associations(auth_object = nil)
|
102
|
+
associations = %w[dashboard_queries queries]
|
103
|
+
associations << 'creator' if Blazer.user_class
|
104
|
+
associations
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
if defined?(Blazer::Check)
|
111
|
+
# Ensure Ransack is included
|
112
|
+
unless Blazer::Check.respond_to?(:ransack)
|
113
|
+
Blazer::Check.send(:extend, Ransack::Adapters::ActiveRecord::Base)
|
114
|
+
end
|
115
|
+
|
116
|
+
unless Blazer::Check.respond_to?(:ransackable_attributes)
|
117
|
+
Blazer::Check.class_eval do
|
118
|
+
def self.ransackable_attributes(auth_object = nil)
|
119
|
+
%w[emails slack_channels check_type schedule state message last_run_at invert creator_id created_at updated_at query_id]
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.ransackable_associations(auth_object = nil)
|
123
|
+
associations = %w[query]
|
124
|
+
associations << 'creator' if Blazer.user_class
|
125
|
+
associations
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Extend Blazer::Check with Ransack capabilities
|
132
|
+
if defined?(Blazer::Check)
|
133
|
+
# First, ensure Ransack is included in the model
|
134
|
+
unless Blazer::Check.respond_to?(:ransack)
|
135
|
+
Blazer::Check.send(:extend, Ransack::Adapters::ActiveRecord::Base)
|
136
|
+
end
|
137
|
+
|
138
|
+
Blazer::Check.class_eval do
|
139
|
+
# Define which attributes can be searched
|
140
|
+
def self.ransackable_attributes(auth_object = nil)
|
141
|
+
%w[emails slack_channels check_type schedule state message last_run_at invert creator_id created_at updated_at query_id]
|
142
|
+
end
|
143
|
+
|
144
|
+
# Define which associations can be searched
|
145
|
+
def self.ransackable_associations(auth_object = nil)
|
146
|
+
associations = %w[query]
|
147
|
+
associations << 'creator' if Blazer.user_class
|
148
|
+
associations
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
Sage::Engine.routes.draw do
|
2
|
+
get "/close_overlay", to: "actions#close_overlay", as: :close_overlay
|
3
|
+
|
4
|
+
resources :queries, except: [ :index ] do
|
5
|
+
post :refresh, on: :member
|
6
|
+
get :run, on: :member
|
7
|
+
|
8
|
+
resources :messages, only: [:index, :create], controller: 'queries/messages'
|
9
|
+
|
10
|
+
collection do
|
11
|
+
post :run
|
12
|
+
post :cancel
|
13
|
+
get :tables
|
14
|
+
get :schema
|
15
|
+
get :docs
|
16
|
+
get :table_schema
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
resources :checks, except: [ :show ] do
|
21
|
+
get :run, on: :member
|
22
|
+
end
|
23
|
+
|
24
|
+
resources :dashboards do
|
25
|
+
member do
|
26
|
+
post :refresh
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
root to: "queries#index"
|
31
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Description:
|
2
|
+
Installs Sage engine JavaScript files and importmap configuration.
|
3
|
+
|
4
|
+
This generator copies the necessary JavaScript files from the Sage engine
|
5
|
+
to your Rails application and configures importmap-rails to load them.
|
6
|
+
|
7
|
+
Example:
|
8
|
+
bin/rails generate sage:install
|
9
|
+
|
10
|
+
This will:
|
11
|
+
1. Copy JavaScript files to app/javascript/sage/
|
12
|
+
2. Add importmap pins to config/importmap.rb
|
13
|
+
3. Display setup instructions
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module Sage
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
desc "Install Sage engine and mount it in your application"
|
9
|
+
|
10
|
+
def install_blazer
|
11
|
+
# Check if Blazer is already installed by looking for migration
|
12
|
+
blazer_installed = Dir.glob("db/migrate/*_install_blazer.rb").any?
|
13
|
+
|
14
|
+
if blazer_installed
|
15
|
+
say "Blazer already installed, skipping...", :yellow
|
16
|
+
else
|
17
|
+
say "Installing Blazer...", :green
|
18
|
+
generate "blazer:install"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_routes
|
23
|
+
# Remove existing Blazer route if present
|
24
|
+
routes_file = "config/routes.rb"
|
25
|
+
if File.exist?(routes_file)
|
26
|
+
routes_content = File.read(routes_file)
|
27
|
+
|
28
|
+
# Pattern to match Blazer mount (with various formatting)
|
29
|
+
blazer_route_pattern = /^\s*mount\s+Blazer::Engine\s*,\s*at:\s*['"]blazer['"]\s*$/
|
30
|
+
|
31
|
+
if routes_content.match?(blazer_route_pattern)
|
32
|
+
# Remove the Blazer route
|
33
|
+
gsub_file routes_file, blazer_route_pattern, ""
|
34
|
+
say "Removed existing Blazer route", :yellow
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Mount Sage (which includes Blazer functionality)
|
39
|
+
route 'mount Sage::Engine => "/sage"'
|
40
|
+
say "Mounted Sage at /sage", :green
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_initializer
|
44
|
+
template "sage.rb", "config/initializers/sage.rb"
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_migrations
|
48
|
+
say "Creating Sage database migrations...", :green
|
49
|
+
|
50
|
+
# Generate timestamp for migration
|
51
|
+
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
52
|
+
|
53
|
+
# Create the migration file
|
54
|
+
migration_file = "db/migrate/#{timestamp}_create_sage_messages.rb"
|
55
|
+
create_file migration_file do
|
56
|
+
<<~RUBY
|
57
|
+
class CreateSageMessages < ActiveRecord::Migration[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]
|
58
|
+
def change
|
59
|
+
create_table :sage_messages do |t|
|
60
|
+
t.references :blazer_query
|
61
|
+
t.references :creator
|
62
|
+
t.string :body
|
63
|
+
t.text :statement
|
64
|
+
|
65
|
+
t.timestamps
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
RUBY
|
70
|
+
end
|
71
|
+
|
72
|
+
say "Created migration for sage_messages table", :green
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_javascript_integration
|
76
|
+
say "Configuring JavaScript integration...", :green
|
77
|
+
|
78
|
+
# Update controllers/index.js to register Sage controllers
|
79
|
+
controllers_index_path = "app/javascript/controllers/index.js"
|
80
|
+
if File.exist?(controllers_index_path)
|
81
|
+
controllers_content = File.read(controllers_index_path)
|
82
|
+
unless controllers_content.include?("sage")
|
83
|
+
append_to_file controllers_index_path do
|
84
|
+
<<~JS
|
85
|
+
|
86
|
+
// Import and register Sage controllers
|
87
|
+
import { registerControllers } from "sage"
|
88
|
+
registerControllers(application)
|
89
|
+
JS
|
90
|
+
end
|
91
|
+
say "Updated controllers/index.js to register Sage controllers", :green
|
92
|
+
else
|
93
|
+
say "Sage controllers already registered in controllers/index.js", :yellow
|
94
|
+
end
|
95
|
+
else
|
96
|
+
say "Could not find app/javascript/controllers/index.js - you'll need to manually import Sage controllers", :yellow
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def add_stylesheets
|
101
|
+
# Stylesheets are served directly from the engine via the asset pipeline
|
102
|
+
# No need to copy or require them - they're automatically available
|
103
|
+
say "Sage stylesheets will be served from the engine", :green
|
104
|
+
end
|
105
|
+
|
106
|
+
def display_instructions
|
107
|
+
say "\n" + "="*50, :green
|
108
|
+
say "Sage installation complete!", :green
|
109
|
+
say "="*50, :green
|
110
|
+
say "\nNext steps:"
|
111
|
+
say "1. Run 'bundle install' to install dependencies"
|
112
|
+
say "2. Run 'rails db:migrate' to create Blazer tables"
|
113
|
+
say "3. Configure your AI service in config/initializers/sage.rb"
|
114
|
+
say "4. Visit #{root_url}sage to start using Sage"
|
115
|
+
say "\nFor AI integration, you'll need to:"
|
116
|
+
say "- Set up an Anthropic API key (or OpenAI if preferred)"
|
117
|
+
say "- Add the API key to Rails credentials or .env file"
|
118
|
+
say "- Configure database schema context for better SQL generation"
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def root_url
|
124
|
+
"http://localhost:3000/"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Sage.configure do |config|
|
2
|
+
# Configure the AI provider (options: :anthropic, :openai)
|
3
|
+
config.provider = :anthropic
|
4
|
+
|
5
|
+
# API Key Configuration
|
6
|
+
# Priority order:
|
7
|
+
# 1. Rails credentials: rails credentials:edit
|
8
|
+
# anthropic:
|
9
|
+
# api_key: your_key_here
|
10
|
+
# openai:
|
11
|
+
# api_key: your_key_here
|
12
|
+
# 2. .env file: ANTHROPIC_API_KEY=your_key_here or OPENAI_API_KEY=your_key_here
|
13
|
+
# 3. Direct configuration (not recommended for production):
|
14
|
+
# config.anthropic_api_key = "your_key_here"
|
15
|
+
# config.open_ai_key = "your_key_here"
|
16
|
+
|
17
|
+
# Model selection (optional)
|
18
|
+
# For Anthropic (defaults to claude-3-opus-20240229):
|
19
|
+
# config.anthropic_model = "claude-3-sonnet-20240229"
|
20
|
+
# For OpenAI (defaults to gpt-4):
|
21
|
+
# config.open_ai_model = "gpt-3.5-turbo"
|
22
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Sage
|
2
|
+
class DatabaseSchemaContext
|
3
|
+
def initialize(data_source_name = "main")
|
4
|
+
@data_source_name = data_source_name
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.call(data_source_name = "main")
|
8
|
+
new(data_source_name).build_context
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_context
|
12
|
+
context_parts = []
|
13
|
+
context_parts << "\n\n## DATABASE SCHEMA\n"
|
14
|
+
context_parts << "Available tables and their columns (use these exact names in your queries):\n"
|
15
|
+
|
16
|
+
begin
|
17
|
+
data_source = Blazer.data_sources[@data_source_name]
|
18
|
+
if data_source && data_source.respond_to?(:schema)
|
19
|
+
schema_info = data_source.schema
|
20
|
+
|
21
|
+
# Format the schema array into readable text
|
22
|
+
if schema_info.is_a?(Array)
|
23
|
+
schema_info.each do |table_info|
|
24
|
+
next unless table_info.is_a?(Hash)
|
25
|
+
|
26
|
+
schema_name = table_info[:schema] || "public"
|
27
|
+
table_name = table_info[:table]
|
28
|
+
columns = table_info[:columns] || []
|
29
|
+
|
30
|
+
context_parts << "\n### Table: `#{schema_name}.#{table_name}`"
|
31
|
+
context_parts << "Columns:"
|
32
|
+
|
33
|
+
columns.each do |column|
|
34
|
+
if column.is_a?(Hash)
|
35
|
+
col_name = column[:name]
|
36
|
+
data_type = column[:data_type]
|
37
|
+
context_parts << " - `#{col_name}` (#{data_type})"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
else
|
42
|
+
# Fallback to original behavior if schema is not in expected format
|
43
|
+
context_parts << "```"
|
44
|
+
context_parts << schema_info.to_s
|
45
|
+
context_parts << "```"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
Rails.logger.warn "Could not load database schema: #{e.message}"
|
50
|
+
return nil
|
51
|
+
end
|
52
|
+
|
53
|
+
context_parts.join("\n")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/sage/engine.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
require "blazer/engine"
|
2
|
+
require "importmap-rails"
|
3
|
+
require "turbo-rails"
|
4
|
+
require "ruby_llm"
|
5
|
+
require "ransack"
|
6
|
+
|
7
|
+
module Sage
|
8
|
+
class Engine < ::Rails::Engine
|
9
|
+
isolate_namespace Sage
|
10
|
+
|
11
|
+
initializer "sage.importmap", before: "importmap" do |app|
|
12
|
+
if Rails.application.respond_to?(:importmap)
|
13
|
+
# Only add our importmap if it hasn't been added already
|
14
|
+
importmap_path = root.join("config/importmap.rb")
|
15
|
+
unless app.config.importmap.paths.include?(importmap_path)
|
16
|
+
app.config.importmap.paths << importmap_path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
initializer "sage.importmap_helpers" do
|
22
|
+
ActiveSupport.on_load(:action_controller_base) do
|
23
|
+
helper Importmap::ImportmapTagsHelper if defined?(Importmap::ImportmapTagsHelper)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
initializer "sage.ransack_helpers", after: "ransack" do
|
28
|
+
# Ensure Ransack is fully loaded with its helpers
|
29
|
+
require "ransack" unless defined?(::Ransack)
|
30
|
+
|
31
|
+
# Load Ransack's ActionView extensions which include the helpers
|
32
|
+
if defined?(::Ransack)
|
33
|
+
require "ransack/helpers/form_helper" if !defined?(Ransack::Helpers)
|
34
|
+
end
|
35
|
+
|
36
|
+
ActiveSupport.on_load(:action_controller_base) do
|
37
|
+
if defined?(Ransack::Helpers::FormHelper)
|
38
|
+
helper Ransack::Helpers::FormHelper
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
ActiveSupport.on_load(:action_view) do
|
43
|
+
if defined?(Ransack::Helpers::FormHelper)
|
44
|
+
include Ransack::Helpers::FormHelper
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Also add to our base controller immediately if it's loaded
|
49
|
+
if defined?(Sage::BaseController) && defined?(Ransack::Helpers::FormHelper)
|
50
|
+
Sage::BaseController.helper Ransack::Helpers::FormHelper
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
initializer "sage.pagy_helpers" do
|
55
|
+
ActiveSupport.on_load(:action_view) do
|
56
|
+
include Pagy::Frontend if defined?(Pagy::Frontend)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
initializer "sage.turbo_helpers" do
|
61
|
+
ActiveSupport.on_load(:action_controller_base) do
|
62
|
+
helper Turbo::FramesHelper if defined?(Turbo::FramesHelper)
|
63
|
+
helper Turbo::StreamsHelper if defined?(Turbo::StreamsHelper)
|
64
|
+
end
|
65
|
+
|
66
|
+
ActiveSupport.on_load(:action_view) do
|
67
|
+
include Turbo::FramesHelper if defined?(Turbo::FramesHelper)
|
68
|
+
include Turbo::StreamsHelper if defined?(Turbo::StreamsHelper)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
initializer "sage.ruby_llm" do
|
73
|
+
RubyLLM.configure do |config|
|
74
|
+
# Determine provider and configure accordingly
|
75
|
+
provider = (Sage.configuration&.provider || :anthropic).to_sym
|
76
|
+
|
77
|
+
case provider
|
78
|
+
when :anthropic
|
79
|
+
config.default_model = Sage.configuration&.anthropic_model || "claude-3-opus-20240229"
|
80
|
+
|
81
|
+
# Determine API key with priority:
|
82
|
+
# 1. Sage configuration (if explicitly set)
|
83
|
+
# 2. Rails credentials
|
84
|
+
# 3. Local .env file
|
85
|
+
api_key = nil
|
86
|
+
|
87
|
+
# Check if explicitly configured in Sage
|
88
|
+
if Sage.configuration && Sage.configuration.anthropic_api_key
|
89
|
+
api_key = Sage.configuration.anthropic_api_key
|
90
|
+
end
|
91
|
+
|
92
|
+
# Try Rails credentials
|
93
|
+
if api_key.nil? && defined?(Rails.application.credentials)
|
94
|
+
api_key = Rails.application.credentials.dig(:anthropic, :api_key)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Try .env file
|
98
|
+
if api_key.nil? && defined?(Rails.root)
|
99
|
+
env_path = Rails.root.join(".env")
|
100
|
+
if File.exist?(env_path)
|
101
|
+
require "dotenv"
|
102
|
+
env_vars = Dotenv.parse(env_path)
|
103
|
+
api_key = env_vars["ANTHROPIC_API_KEY"]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
config.anthropic_api_key = api_key
|
108
|
+
|
109
|
+
when :openai
|
110
|
+
config.default_model = Sage.configuration&.open_ai_model || "gpt-4"
|
111
|
+
|
112
|
+
# Determine API key with priority:
|
113
|
+
# 1. Sage configuration (if explicitly set)
|
114
|
+
# 2. Rails credentials
|
115
|
+
# 3. Local .env file
|
116
|
+
api_key = nil
|
117
|
+
|
118
|
+
# Check if explicitly configured in Sage
|
119
|
+
if Sage.configuration && Sage.configuration.open_ai_key
|
120
|
+
api_key = Sage.configuration.open_ai_key
|
121
|
+
end
|
122
|
+
|
123
|
+
# Try Rails credentials
|
124
|
+
if api_key.nil? && defined?(Rails.application.credentials)
|
125
|
+
api_key = Rails.application.credentials.dig(:openai, :api_key)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Try .env file
|
129
|
+
if api_key.nil? && defined?(Rails.root)
|
130
|
+
env_path = Rails.root.join(".env")
|
131
|
+
if File.exist?(env_path)
|
132
|
+
require "dotenv"
|
133
|
+
env_vars = Dotenv.parse(env_path)
|
134
|
+
api_key = env_vars["OPENAI_API_KEY"]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
config.openai_api_key = api_key
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
initializer "sage.assets" do |app|
|
144
|
+
# Add JavaScript path for Propshaft
|
145
|
+
if defined?(Propshaft::Railtie)
|
146
|
+
app.config.assets.paths << Engine.root.join("app/javascript")
|
147
|
+
app.config.assets.paths << Engine.root.join("app/javascript/sage")
|
148
|
+
end
|
149
|
+
|
150
|
+
if app.config.respond_to?(:assets)
|
151
|
+
# Blazer assets
|
152
|
+
blazer_css_assets = [
|
153
|
+
"blazer/selectize.css",
|
154
|
+
"blazer/daterangepicker.css"
|
155
|
+
]
|
156
|
+
|
157
|
+
blazer_js_assets = [
|
158
|
+
"blazer/jquery.js",
|
159
|
+
"blazer/rails-ujs.js",
|
160
|
+
"blazer/stupidtable.js",
|
161
|
+
"blazer/stupidtable-custom-settings.js",
|
162
|
+
"blazer/jquery.stickytableheaders.js",
|
163
|
+
"blazer/selectize.js",
|
164
|
+
"blazer/highlight.min.js",
|
165
|
+
"blazer/moment.js",
|
166
|
+
"blazer/moment-timezone-with-data.js",
|
167
|
+
"blazer/daterangepicker.js",
|
168
|
+
"blazer/chart.umd.js",
|
169
|
+
"blazer/chartjs-adapter-date-fns.bundle.js",
|
170
|
+
"blazer/chartkick.js",
|
171
|
+
"blazer/mapkick.bundle.js",
|
172
|
+
"blazer/ace/ace.js",
|
173
|
+
"blazer/ace/ext-language_tools.js",
|
174
|
+
"blazer/ace/theme-twilight.js",
|
175
|
+
"blazer/ace/mode-sql.js",
|
176
|
+
"blazer/ace/snippets/text.js",
|
177
|
+
"blazer/ace/snippets/sql.js",
|
178
|
+
"blazer/Sortable.js",
|
179
|
+
"blazer/bootstrap.js",
|
180
|
+
"blazer/vue.global.prod.js",
|
181
|
+
"blazer/routes.js",
|
182
|
+
"blazer/queries.js",
|
183
|
+
"blazer/fuzzysearch.js",
|
184
|
+
"blazer/application.js"
|
185
|
+
]
|
186
|
+
|
187
|
+
sage_assets = [
|
188
|
+
"sage.js",
|
189
|
+
"sage/application.js",
|
190
|
+
"sage/application.css",
|
191
|
+
"sage/controllers/search_controller.js",
|
192
|
+
"sage/controllers/clipboard_controller.js",
|
193
|
+
"sage/controllers/select_controller.js",
|
194
|
+
"sage/controllers/dashboard_controller.js",
|
195
|
+
"sage/controllers/reverse_infinite_scroll_controller.js"
|
196
|
+
]
|
197
|
+
|
198
|
+
if defined?(Sprockets)
|
199
|
+
if Sprockets::VERSION.to_i >= 4
|
200
|
+
app.config.assets.precompile += blazer_css_assets + blazer_js_assets + sage_assets
|
201
|
+
else
|
202
|
+
# use a proc instead of a string
|
203
|
+
app.config.assets.precompile << proc { |path| path =~ /\Asage\/.+\.css\z/ }
|
204
|
+
app.config.assets.precompile << proc { |path| path =~ /\Asage\/.+\.js\z/ }
|
205
|
+
app.config.assets.precompile << proc { |path| path =~ /\Asage\/controllers\/.+\.js\z/ }
|
206
|
+
app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/.+\.css\z/ }
|
207
|
+
app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/.+\.js\z/ }
|
208
|
+
end
|
209
|
+
else
|
210
|
+
# For Propshaft
|
211
|
+
app.config.assets.precompile += blazer_css_assets + blazer_js_assets + sage_assets
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
config.to_prepare do
|
218
|
+
# Ensure Ransack is loaded
|
219
|
+
require "ransack" if defined?(::Ransack).nil?
|
220
|
+
|
221
|
+
# Ensure we can access Blazer's models and controllers
|
222
|
+
Dir.glob(Rails.root.join("app/models/blazer/*.rb")).each { |file| require_dependency file }
|
223
|
+
|
224
|
+
# Load ransack configuration for Blazer::Query
|
225
|
+
initializer_path = Engine.root.join("config/initializers/ransack.rb")
|
226
|
+
load initializer_path if File.exist?(initializer_path)
|
227
|
+
|
228
|
+
# Extend Blazer::Query with associations
|
229
|
+
if defined?(Blazer::Query)
|
230
|
+
# Ensure Ransack is available for Blazer::Query
|
231
|
+
unless Blazer::Query.respond_to?(:ransack)
|
232
|
+
Blazer::Query.send(:extend, Ransack::Adapters::ActiveRecord::Base)
|
233
|
+
end
|
234
|
+
|
235
|
+
Blazer::Query.class_eval do
|
236
|
+
has_many :messages, class_name: "Sage::Message", foreign_key: :blazer_query_id, dependent: :destroy
|
237
|
+
|
238
|
+
# Define ransackable attributes if not already defined
|
239
|
+
unless respond_to?(:ransackable_attributes)
|
240
|
+
def self.ransackable_attributes(auth_object = nil)
|
241
|
+
%w[name description statement creator_id created_at updated_at status]
|
242
|
+
end
|
243
|
+
|
244
|
+
def self.ransackable_associations(auth_object = nil)
|
245
|
+
%w[creator checks audits dashboard_queries dashboards messages]
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Autoload schemas
|
253
|
+
config.autoload_paths += %W[#{root}/app/schemas]
|
254
|
+
|
255
|
+
# Make Blazer available at the top level
|
256
|
+
config.before_initialize do
|
257
|
+
require "blazer"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|