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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +202 -0
  3. data/app/assets/images/chevron-down-zinc-500.svg +1 -0
  4. data/app/assets/images/chevron-right.svg +1 -0
  5. data/app/assets/images/loading.svg +4 -0
  6. data/app/assets/images/sage/chevron-down-zinc-500.svg +1 -0
  7. data/app/assets/images/sage/chevron-right.svg +1 -0
  8. data/app/assets/images/sage/loading.svg +4 -0
  9. data/app/assets/javascripts/sage/application.js +18 -0
  10. data/app/assets/stylesheets/sage/application.css +308 -0
  11. data/app/controllers/sage/actions_controller.rb +5 -0
  12. data/app/controllers/sage/application_controller.rb +4 -0
  13. data/app/controllers/sage/base_controller.rb +10 -0
  14. data/app/controllers/sage/checks_controller.rb +65 -0
  15. data/app/controllers/sage/dashboards_controller.rb +130 -0
  16. data/app/controllers/sage/queries/messages_controller.rb +62 -0
  17. data/app/controllers/sage/queries_controller.rb +596 -0
  18. data/app/helpers/sage/application_helper.rb +30 -0
  19. data/app/helpers/sage/queries_helper.rb +23 -0
  20. data/app/javascript/controllers/element_removal_controller.js +7 -0
  21. data/app/javascript/sage/controllers/clipboard_controller.js +26 -0
  22. data/app/javascript/sage/controllers/dashboard_controller.js +132 -0
  23. data/app/javascript/sage/controllers/reverse_infinite_scroll_controller.js +146 -0
  24. data/app/javascript/sage/controllers/search_controller.js +47 -0
  25. data/app/javascript/sage/controllers/select_controller.js +215 -0
  26. data/app/javascript/sage.js +19 -0
  27. data/app/jobs/sage/application_job.rb +4 -0
  28. data/app/jobs/sage/process_report_job.rb +80 -0
  29. data/app/mailers/sage/application_mailer.rb +6 -0
  30. data/app/models/sage/application_record.rb +5 -0
  31. data/app/models/sage/message.rb +8 -0
  32. data/app/schemas/sage/report_response_schema.rb +8 -0
  33. data/app/views/layouts/application.html.erb +34 -0
  34. data/app/views/layouts/sage/application.html.erb +94 -0
  35. data/app/views/sage/checks/_form.html.erb +81 -0
  36. data/app/views/sage/checks/_search.html.erb +8 -0
  37. data/app/views/sage/checks/edit.html.erb +10 -0
  38. data/app/views/sage/checks/index.html.erb +58 -0
  39. data/app/views/sage/checks/new.html.erb +8 -0
  40. data/app/views/sage/dashboards/_form.html.erb +50 -0
  41. data/app/views/sage/dashboards/_search.html.erb +8 -0
  42. data/app/views/sage/dashboards/index.html.erb +58 -0
  43. data/app/views/sage/dashboards/new.html.erb +8 -0
  44. data/app/views/sage/dashboards/show.html.erb +58 -0
  45. data/app/views/sage/messages/_form.html.erb +14 -0
  46. data/app/views/sage/queries/_caching.html.erb +17 -0
  47. data/app/views/sage/queries/_form.html.erb +72 -0
  48. data/app/views/sage/queries/_input.html.erb +17 -0
  49. data/app/views/sage/queries/_message.html.erb +25 -0
  50. data/app/views/sage/queries/_message.turbo_stream.erb +10 -0
  51. data/app/views/sage/queries/_new_form.html.erb +43 -0
  52. data/app/views/sage/queries/_run.html.erb +232 -0
  53. data/app/views/sage/queries/_search.html.erb +8 -0
  54. data/app/views/sage/queries/_statement_box.html.erb +241 -0
  55. data/app/views/sage/queries/_streaming_message.html.erb +14 -0
  56. data/app/views/sage/queries/create.turbo_stream.erb +114 -0
  57. data/app/views/sage/queries/edit.html.erb +48 -0
  58. data/app/views/sage/queries/index.html.erb +59 -0
  59. data/app/views/sage/queries/messages/create.turbo_stream.erb +22 -0
  60. data/app/views/sage/queries/messages/index.html.erb +44 -0
  61. data/app/views/sage/queries/messages/index.turbo_stream.erb +15 -0
  62. data/app/views/sage/queries/new.html.erb +195 -0
  63. data/app/views/sage/queries/run.html.erb +1 -0
  64. data/app/views/sage/queries/run.turbo_stream.erb +3 -0
  65. data/app/views/sage/queries/show.html.erb +49 -0
  66. data/app/views/sage/queries/table_schema.html.erb +77 -0
  67. data/app/views/sage/shared/_navigation.html.erb +26 -0
  68. data/app/views/sage/shared/_overlay.html.erb +11 -0
  69. data/config/importmap.rb +11 -0
  70. data/config/initializers/pagy.rb +2 -0
  71. data/config/initializers/ransack.rb +152 -0
  72. data/config/routes.rb +31 -0
  73. data/lib/generators/sage/USAGE +13 -0
  74. data/lib/generators/sage/install/install_generator.rb +128 -0
  75. data/lib/generators/sage/install/templates/sage.rb +22 -0
  76. data/lib/sage/database_schema_context.rb +56 -0
  77. data/lib/sage/engine.rb +260 -0
  78. data/lib/sage/model_scopes_context.rb +185 -0
  79. data/lib/sage/report_processor.rb +263 -0
  80. data/lib/sage/version.rb +3 -0
  81. data/lib/sage.rb +25 -0
  82. data/lib/tasks/sage_tasks.rake +4 -0
  83. 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
@@ -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