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,185 @@
1
+ module Sage
2
+ class ModelScopesContext
3
+ def initialize
4
+ # Nothing to initialize for now
5
+ end
6
+
7
+ def self.call
8
+ new.build_context
9
+ end
10
+
11
+ def build_context
12
+ context_parts = []
13
+ context_parts << "\n\n## AVAILABLE SCOPES → SQL MAPPINGS\n"
14
+ context_parts << "CRITICAL: Use these scopes to understand how to query the data!"
15
+ context_parts << "Each scope name shows the SQL conditions it generates."
16
+ context_parts << "When a user's request matches a scope's intent, use that scope's SQL pattern.\n"
17
+
18
+ # Get all ActiveRecord models from the host application
19
+ # Safely attempt to eager load, but continue if there are issues
20
+ begin
21
+ Rails.application.eager_load! if Rails.env.development?
22
+ rescue Zeitwerk::NameError => e
23
+ Rails.logger.warn "Could not eager load all files: #{e.message}"
24
+ end
25
+
26
+ models_with_scopes = collect_models_with_scopes
27
+
28
+ # Format the model scopes nicely
29
+ if models_with_scopes.any?
30
+ models_with_scopes.each do |model_info|
31
+ context_parts << "\n### #{model_info[:name]} (table: `#{model_info[:table]}`)"
32
+ context_parts << "Scopes and their SQL equivalents:"
33
+ model_info[:scopes].each do |scope|
34
+ context_parts << scope
35
+ end
36
+ end
37
+ end
38
+
39
+ context_parts.join("\n")
40
+ end
41
+
42
+ private
43
+
44
+ def collect_models_with_scopes
45
+ models_with_scopes = []
46
+
47
+ # Find all model files in the app/models directory
48
+ model_files = Dir.glob(Rails.root.join("app/models/**/*.rb"))
49
+
50
+ model_files.each do |file_path|
51
+ # Skip concern files and other non-model files
52
+ next if file_path.include?("/concerns/")
53
+
54
+ # Read the file content
55
+ file_content = File.read(file_path)
56
+
57
+ # Extract model name from file path
58
+ model_name = File.basename(file_path, ".rb").camelize
59
+
60
+ # Find all scope definitions using regex
61
+ # Match various scope patterns:
62
+ # scope :active, -> { where(active: true) }
63
+ # scope :recent, lambda { where("created_at > ?", 1.week.ago) }
64
+ # scope :by_role, ->(role) { where(role: role) }
65
+ scope_patterns = [
66
+ # Pattern 1: scope :name, -> { ... } or -> (...) { ... }
67
+ /scope\s+:(\w+)\s*,\s*->\s*(?:\([^)]*\))?\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/m,
68
+ # Pattern 2: scope :name, lambda { ... }
69
+ /scope\s+:(\w+)\s*,\s*lambda\s*(?:\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\})/m,
70
+ # Pattern 3: Simple one-liner scopes
71
+ /scope\s+:(\w+)\s*,\s*(.+?)$/
72
+ ]
73
+
74
+ scope_matches = []
75
+ scope_patterns.each do |pattern|
76
+ matches = file_content.scan(pattern)
77
+ matches.each do |match|
78
+ scope_name = match[0]
79
+ scope_body = match[1] || ""
80
+ # Avoid duplicate entries
81
+ unless scope_matches.any? { |s| s[0] == scope_name }
82
+ scope_matches << [scope_name, scope_body]
83
+ end
84
+ end
85
+ end
86
+
87
+ if scope_matches.any?
88
+ # Try to get the actual model class and table name
89
+ begin
90
+ model_class = model_name.constantize
91
+ table_name = model_class.table_name rescue model_name.tableize
92
+ rescue => e
93
+ table_name = model_name.tableize
94
+ model_class = nil
95
+ end
96
+
97
+ model_info = {
98
+ name: model_name,
99
+ table: table_name,
100
+ scopes: []
101
+ }
102
+
103
+ scope_matches.each do |match|
104
+ scope_name = match[0]
105
+ scope_body = match[1]
106
+
107
+ if scope_body
108
+ # Try to extract SQL-like patterns from the scope body
109
+ # Look for where conditions, joins, etc.
110
+ sql_hint = extract_sql_from_scope_body(scope_body)
111
+ model_info[:scopes] << " • `#{scope_name}` → SQL: `#{sql_hint}`"
112
+ else
113
+ # Scope might be using a lambda with parameters or complex logic
114
+ model_info[:scopes] << " • `#{scope_name}` → (check model file for implementation)"
115
+ end
116
+ end
117
+
118
+ models_with_scopes << model_info if model_info[:scopes].any?
119
+ end
120
+ end
121
+
122
+ models_with_scopes
123
+ end
124
+
125
+ def extract_sql_from_scope_body(scope_body)
126
+ # Clean up the scope body
127
+ cleaned = scope_body.strip
128
+
129
+ sql_parts = []
130
+
131
+ # Extract WHERE conditions
132
+ if cleaned =~ /where\s*\(["']([^"']+)["'](?:,\s*(.+?))?\)/
133
+ # String SQL with potential parameters
134
+ sql_parts << "WHERE #{$1}"
135
+ elsif cleaned =~ /where\s*\(([^)]+)\)/
136
+ # Hash or conditions
137
+ where_conditions = $1.strip
138
+ # Convert Ruby hash syntax to SQL-like
139
+ where_conditions = where_conditions.gsub(/(\w+):\s*(\w+)/, '\1 = \2')
140
+ where_conditions = where_conditions.gsub(/(\w+):\s*["']([^"']+)["']/, '\1 = "\2"')
141
+ where_conditions = where_conditions.gsub(/(\w+):\s*(true|false|nil)/, '\1 = \2')
142
+ sql_parts << "WHERE #{where_conditions}"
143
+ elsif cleaned =~ /where\.not\s*\(([^)]+)\)/
144
+ # WHERE NOT conditions
145
+ not_conditions = $1.strip
146
+ not_conditions = not_conditions.gsub(/(\w+):\s*/, '\1 != ')
147
+ sql_parts << "WHERE NOT (#{not_conditions})"
148
+ end
149
+
150
+ # Extract JOINs
151
+ if cleaned =~ /joins?\s*\(:?(\w+)\)/
152
+ sql_parts << "JOIN #{$1}"
153
+ elsif cleaned =~ /includes?\s*\(:?(\w+)\)/
154
+ sql_parts << "LEFT JOIN #{$1}"
155
+ end
156
+
157
+ # Extract ORDER
158
+ if cleaned =~ /order\s*\(["']([^"']+)["']\)/
159
+ sql_parts << "ORDER BY #{$1}"
160
+ elsif cleaned =~ /order\s*\(([^)]+)\)/
161
+ order_clause = $1.strip
162
+ order_clause = order_clause.gsub(/(\w+):\s*:?(asc|desc)/i, '\1 \2')
163
+ sql_parts << "ORDER BY #{order_clause}"
164
+ end
165
+
166
+ # Extract LIMIT
167
+ if cleaned =~ /limit\s*\((\d+)\)/
168
+ sql_parts << "LIMIT #{$1}"
169
+ end
170
+
171
+ # If we found SQL parts, join them
172
+ if sql_parts.any?
173
+ sql_parts.join(" ")
174
+ else
175
+ # Check if it's a simple scope referencing another scope
176
+ if cleaned =~ /^(\w+)$/
177
+ "(uses #{$1} scope)"
178
+ else
179
+ # Return a truncated version if we can't parse it
180
+ cleaned.length > 60 ? "#{cleaned[0..60]}..." : cleaned
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,263 @@
1
+ require "ruby_llm"
2
+ require_relative "database_schema_context"
3
+ require_relative "model_scopes_context"
4
+
5
+ module Sage
6
+ class ReportProcessor
7
+ include ActionView::RecordIdentifier
8
+
9
+ attr_reader :query, :prompt, :stream_target_id, :raw_response_content
10
+
11
+ def initialize(query:, prompt:, stream_target_id:)
12
+ @query = query
13
+ @prompt = prompt
14
+ @stream_target_id = stream_target_id
15
+ end
16
+
17
+ def process
18
+ response = generate_llm_response
19
+ Rails.logger.info "LLM Response: #{response.inspect}"
20
+ Rails.logger.info "LLM Response content: #{response.content.inspect}"
21
+ @raw_response_content = response.content
22
+ parsed_response = parse_response(response)
23
+ Rails.logger.info "Parsed response: #{parsed_response.inspect}"
24
+
25
+ {
26
+ summary: parsed_response[:summary],
27
+ sql: parsed_response[:sql]
28
+ }
29
+ end
30
+
31
+ def system_prompt
32
+ build_system_prompt
33
+ end
34
+
35
+ def database_schema_context
36
+ Sage::DatabaseSchemaContext.new.build_context
37
+ end
38
+
39
+ def model_scopes_context
40
+ Sage::ModelScopesContext.new.build_context
41
+ end
42
+
43
+ private
44
+
45
+ def generate_llm_response
46
+ RubyLLM.chat
47
+ .with_instructions(system_prompt)
48
+ .with_schema(Sage::ReportResponseSchema)
49
+ .ask(prompt + ". #{structured_output}")
50
+ end
51
+
52
+ def parse_response(response)
53
+ if response.content.is_a?(Hash) && response.content.key?("sql") && response.content.key?("summary")
54
+ # Direct hash with sql and summary keys
55
+ {
56
+ summary: response.content["summary"],
57
+ sql: response.content["sql"]
58
+ }
59
+ elsif response.content.is_a?(String)
60
+ parse_json_response(response.content)
61
+ else
62
+ # Fallback for unexpected response format
63
+ Rails.logger.warn "Unexpected response format: #{response.content.class}"
64
+ {
65
+ summary: "Unexpected response format. Please try again.",
66
+ sql: nil
67
+ }
68
+ end
69
+ end
70
+
71
+ def parse_json_response(content)
72
+ begin
73
+ # First attempt: direct JSON parsing
74
+ parsed_response = JSON.parse(content)
75
+ {
76
+ summary: parsed_response["summary"],
77
+ sql: parsed_response["sql"]
78
+ }
79
+ rescue JSON::ParserError
80
+ # Second attempt: fix malformed JSON by properly escaping newlines within quoted strings
81
+ begin
82
+ fixed_json = content.gsub(/"([^"]*)"/) do |match|
83
+ # Escape newlines, tabs, and other control characters within the quoted string
84
+ match.gsub(/\n/, '\\n').gsub(/\t/, '\\t').gsub(/\r/, '\\r')
85
+ end
86
+
87
+ parsed_response = JSON.parse(fixed_json)
88
+ {
89
+ summary: parsed_response["summary"],
90
+ sql: parsed_response["sql"]
91
+ }
92
+ rescue JSON::ParserError => e
93
+ # Final fallback: extract using regex patterns
94
+ Rails.logger.warn "Failed to parse JSON even after fixing newlines: #{e.message}"
95
+
96
+ # Extract SQL value (everything between "sql": " and the closing quote before comma or brace)
97
+ sql_match = content.match(/"sql"\s*:\s*"((?:[^"\\]|\\.)*)"/m)
98
+ sql = sql_match[1] if sql_match
99
+
100
+ # Extract summary value
101
+ summary_match = content.match(/"summary"\s*:\s*"((?:[^"\\]|\\.)*)"/m)
102
+ summary = summary_match[1] if summary_match
103
+
104
+ if sql.nil? || summary.nil?
105
+ Rails.logger.error "Could not extract sql and summary from response"
106
+ summary = "Failed to parse response. Please try again." if summary.nil?
107
+ end
108
+
109
+ {
110
+ summary: summary || "Failed to parse response. Please try again.",
111
+ sql: sql
112
+ }
113
+ end
114
+ end
115
+ end
116
+
117
+ def structured_output
118
+ <<~INSTRUCTION
119
+ Return as a JSON object with sql and summary keys and no additional commentary.
120
+ INSTRUCTION
121
+ end
122
+
123
+ def build_system_prompt
124
+ prompt_parts = []
125
+
126
+ # Detect database type
127
+ database_type = detect_database_type
128
+
129
+ # Base instruction optimized for LLM
130
+ prompt_parts << <<~INSTRUCTION
131
+ You are an expert SQL analyst helping users iteratively refine their database queries.
132
+
133
+ DATABASE TYPE: #{database_type}
134
+
135
+ Your task:
136
+ 1. Analyze the user's natural language request
137
+ 2. Determine if you should:
138
+ a) Modify the most recent SQL query (from Previous Context if available)
139
+ b) Modify the baseline query (from Current Query section)
140
+ c) Create an entirely new query if the request is unrelated
141
+ 3. Generate the appropriate SQL query for #{database_type}
142
+ 4. Provide a clear explanation of what changed and why
143
+
144
+ Response format (STRICT JSON):
145
+ {
146
+ "summary": "Explain what this query does and what changes were made from the previous version (if any)",
147
+ "sql": "The complete SQL query statement"
148
+ }
149
+
150
+ CLARIFICATION REQUIRED: If you're unsure how to query based on certain criteria:
151
+ - Return a summary asking for clarification
152
+ - Set sql to null
153
+ - Example: {"summary": "I need clarification on what you mean by 'activated accounts'. Do you mean users with a specific status, users who have logged in, or users with a certain field set?", "sql": null}
154
+
155
+ IMPORTANT:#{' '}
156
+ - Always return the COMPLETE query, not just the changes
157
+ - When producing SQL, exclude ALL comments and extraneous characters - the SQL will be immediately executed against a database
158
+ - Format SQL to be human readable, per SQL writing best practices, but still executable
159
+ - When modifying existing queries, preserve the original intent while incorporating the requested changes
160
+ - If the user asks for adjustments (e.g., "add a filter", "group by X", "sort differently"), modify the most recent query
161
+ - If the user asks something completely new, create a fresh query
162
+
163
+ Guidelines:
164
+ - Write efficient, readable SQL using #{database_type}-specific syntax
165
+ - Use meaningful table aliases and column names
166
+ - Do NOT include comments in SQL queries - they will be executed directly
167
+ - Prefer JOINs over subqueries when appropriate
168
+ - Consider performance implications for large datasets
169
+ - ROLE HANDLING: When dealing with "roles" (e.g., "candidates", "employers", synonyms of "users"):
170
+ * ALWAYS check the users model/table/scopes first to understand how roles are established
171
+ * Look for role-related columns, scopes, or associations in the users table
172
+ * Map role-related terms to the actual implementation in the database
173
+ - SCOPE PRIORITIZATION: ALWAYS prioritize matching user requests to available model scopes
174
+ * FIRST check if any existing scopes match the user's intent
175
+ * Use scopes as the PRIMARY source for query patterns
176
+ * Only write custom SQL when no appropriate scope exists
177
+ * Analyze the intent behind user queries and map them to corresponding scopes
178
+ * When users describe filters or conditions, identify matching scope patterns
179
+ * Example: if user asks for "recent items", look for scopes like "recent", "latest", or time-based scopes
180
+ - JSONB COLUMNS: NEVER guess at JSONB column keys or values
181
+ * Only query JSONB fields that are explicitly defined in scopes or schema documentation
182
+ * If JSONB structure is unknown, do NOT attempt to query specific keys
183
+ * Avoid assumptions about JSONB content unless explicitly documented
184
+ - PRESENCE CHECKS: For presence/existence checks:
185
+ * Use "IS NOT NULL" or "IS NULL" for presence/absence checks
186
+ * Avoid using literal values like 'true' or specific strings unless explicitly required
187
+ * For boolean presence, check for NOT NULL rather than = true
188
+ * Example: Use "activated_at IS NOT NULL" instead of "activated = 'true'"
189
+ - Ensure all table and column names match the schema exactly
190
+ - Handle NULL values appropriately (prefer IS NULL/IS NOT NULL for presence checks)
191
+ - Use proper data type casting when needed
192
+ - Follow #{database_type} best practices and syntax conventions
193
+ - NEVER make assumptions about data structure - use only what's documented in schema and scopes
194
+ INSTRUCTION
195
+
196
+ # Add current query as baseline context
197
+ if query.statement.present?
198
+ prompt_parts << "\n\n## CURRENT QUERY (BASELINE)\n"
199
+ prompt_parts << "The currently saved query that we're working with:\n"
200
+ prompt_parts << "```sql\n#{query.statement}\n```"
201
+ prompt_parts << "\nThis is the baseline query. You may modify or completely replace it based on the user's request.\n"
202
+ end
203
+
204
+ # Add latest message as context
205
+ latest_message = query.messages.order(:created_at).last
206
+ if latest_message
207
+ prompt_parts << "\n\n## PREVIOUS CONTEXT\n"
208
+ prompt_parts << "The most recent message from this conversation:\n"
209
+ prompt_parts << "\nPrevious response: #{latest_message.body}" if latest_message.body.present?
210
+ prompt_parts << "\nPrevious SQL: #{latest_message.statement}" if latest_message.statement.present?
211
+ prompt_parts << "\n\nConsider this context when generating your response.\n"
212
+ end
213
+
214
+ # Add database schema
215
+ schema_context = database_schema_context
216
+ prompt_parts << schema_context if schema_context.present?
217
+
218
+ # Add model scopes from host application
219
+ scopes_context = model_scopes_context
220
+ prompt_parts << scopes_context if scopes_context.present?
221
+
222
+ prompt_parts << "\n\n## QUERY GENERATION RULES"
223
+ prompt_parts << "1. Match table and column names EXACTLY as shown in the schema"
224
+ prompt_parts << "2. NO COMMENTS OR NEWLINES in SQL - output will be executed directly against database"
225
+ prompt_parts << "3. SCOPE FIRST: ALWAYS prioritize using available scopes over custom SQL"
226
+ prompt_parts << " - Check ALL available scopes before writing custom conditions"
227
+ prompt_parts << " - Map user language directly to scope names when possible"
228
+ prompt_parts << "4. JSONB HANDLING: NEVER guess at JSONB structure"
229
+ prompt_parts << " - Only use JSONB keys that are explicitly documented in scopes or schema"
230
+ prompt_parts << " - If unsure about JSONB structure, avoid querying it"
231
+ prompt_parts << "5. PRESENCE/ABSENCE CHECKS:"
232
+ prompt_parts << " - Use IS NOT NULL for presence (not = 'true' or = true)"
233
+ prompt_parts << " - Use IS NULL for absence"
234
+ prompt_parts << " - Example: 'activated users' → 'activated_at IS NOT NULL'"
235
+ prompt_parts << "6. ROLE HANDLING: When users mention roles like 'candidates' or 'employers':"
236
+ prompt_parts << " - Check the users table/model/scopes first to understand role implementation"
237
+ prompt_parts << " - Map role terms to actual database structure (columns, associations, etc.)"
238
+ prompt_parts << "7. Generate ONE query that best answers the user's request"
239
+ prompt_parts << "8. NEVER make assumptions - use only documented schema and scopes"
240
+
241
+ prompt_parts.join("\n")
242
+ end
243
+
244
+
245
+ def detect_database_type
246
+ adapter_name = ActiveRecord::Base.connection.adapter_name.downcase
247
+ case adapter_name
248
+ when /postgresql/, /postgis/
249
+ "PostgreSQL"
250
+ when /mysql/, /mysql2/
251
+ "MySQL"
252
+ when /sqlite/
253
+ "SQLite3"
254
+ when /sqlserver/, /mssql/
255
+ "SQL Server"
256
+ when /oracle/
257
+ "Oracle"
258
+ else
259
+ adapter_name.capitalize
260
+ end
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,3 @@
1
+ module Sage
2
+ VERSION = "0.0.3"
3
+ end
data/lib/sage.rb ADDED
@@ -0,0 +1,25 @@
1
+ require "sage/version"
2
+ require "sage/engine"
3
+ require "blazer"
4
+ require "pagy"
5
+
6
+ module Sage
7
+ class << self
8
+ attr_accessor :configuration
9
+ end
10
+
11
+ def self.configure
12
+ self.configuration ||= Configuration.new
13
+ yield(configuration)
14
+ end
15
+
16
+ class Configuration
17
+ attr_accessor :anthropic_api_key, :anthropic_model, :open_ai_key, :open_ai_model, :provider
18
+
19
+ def initialize
20
+ @provider = :anthropic
21
+ @anthropic_model = "claude-3-opus-20240229"
22
+ @open_ai_model = "gpt-4"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :sage do
3
+ # # Task goes here
4
+ # end