rails_console_ai 0.13.0

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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +95 -0
  3. data/LICENSE +21 -0
  4. data/README.md +328 -0
  5. data/app/controllers/rails_console_ai/application_controller.rb +28 -0
  6. data/app/controllers/rails_console_ai/sessions_controller.rb +16 -0
  7. data/app/helpers/rails_console_ai/sessions_helper.rb +56 -0
  8. data/app/models/rails_console_ai/session.rb +23 -0
  9. data/app/views/layouts/rails_console_ai/application.html.erb +84 -0
  10. data/app/views/rails_console_ai/sessions/index.html.erb +57 -0
  11. data/app/views/rails_console_ai/sessions/show.html.erb +66 -0
  12. data/config/routes.rb +4 -0
  13. data/lib/generators/rails_console_ai/install_generator.rb +26 -0
  14. data/lib/generators/rails_console_ai/templates/initializer.rb +79 -0
  15. data/lib/rails_console_ai/channel/base.rb +23 -0
  16. data/lib/rails_console_ai/channel/console.rb +457 -0
  17. data/lib/rails_console_ai/channel/slack.rb +182 -0
  18. data/lib/rails_console_ai/configuration.rb +185 -0
  19. data/lib/rails_console_ai/console_methods.rb +277 -0
  20. data/lib/rails_console_ai/context_builder.rb +120 -0
  21. data/lib/rails_console_ai/conversation_engine.rb +1142 -0
  22. data/lib/rails_console_ai/engine.rb +5 -0
  23. data/lib/rails_console_ai/executor.rb +461 -0
  24. data/lib/rails_console_ai/providers/anthropic.rb +122 -0
  25. data/lib/rails_console_ai/providers/base.rb +118 -0
  26. data/lib/rails_console_ai/providers/bedrock.rb +171 -0
  27. data/lib/rails_console_ai/providers/local.rb +112 -0
  28. data/lib/rails_console_ai/providers/openai.rb +114 -0
  29. data/lib/rails_console_ai/railtie.rb +34 -0
  30. data/lib/rails_console_ai/repl.rb +65 -0
  31. data/lib/rails_console_ai/safety_guards.rb +207 -0
  32. data/lib/rails_console_ai/session_logger.rb +90 -0
  33. data/lib/rails_console_ai/slack_bot.rb +473 -0
  34. data/lib/rails_console_ai/storage/base.rb +27 -0
  35. data/lib/rails_console_ai/storage/file_storage.rb +63 -0
  36. data/lib/rails_console_ai/tools/code_tools.rb +126 -0
  37. data/lib/rails_console_ai/tools/memory_tools.rb +136 -0
  38. data/lib/rails_console_ai/tools/model_tools.rb +95 -0
  39. data/lib/rails_console_ai/tools/registry.rb +478 -0
  40. data/lib/rails_console_ai/tools/schema_tools.rb +60 -0
  41. data/lib/rails_console_ai/version.rb +3 -0
  42. data/lib/rails_console_ai.rb +214 -0
  43. data/lib/tasks/rails_console_ai.rake +7 -0
  44. metadata +152 -0
@@ -0,0 +1,185 @@
1
+ module RailsConsoleAI
2
+ class Configuration
3
+ PROVIDERS = %i[anthropic openai local bedrock].freeze
4
+
5
+ # cache_read: 0.1x input, cache_write: 1.25x input for Anthropic models
6
+ PRICING = {
7
+ 'claude-sonnet-4-6' => { input: 3.0 / 1_000_000, output: 15.0 / 1_000_000, cache_read: 0.30 / 1_000_000, cache_write: 3.75 / 1_000_000 },
8
+ 'claude-opus-4-6' => { input: 15.0 / 1_000_000, output: 75.0 / 1_000_000, cache_read: 1.50 / 1_000_000, cache_write: 18.75 / 1_000_000 },
9
+ 'claude-haiku-4-5-20251001' => { input: 0.80 / 1_000_000, output: 4.0 / 1_000_000, cache_read: 0.08 / 1_000_000, cache_write: 1.0 / 1_000_000 },
10
+ # Bedrock model IDs (same pricing as direct API)
11
+ 'us.anthropic.claude-sonnet-4-6' => { input: 3.0 / 1_000_000, output: 15.0 / 1_000_000, cache_read: 0.30 / 1_000_000, cache_write: 3.75 / 1_000_000 },
12
+ 'us.anthropic.claude-opus-4-6-v1' => { input: 15.0 / 1_000_000, output: 75.0 / 1_000_000, cache_read: 1.50 / 1_000_000, cache_write: 18.75 / 1_000_000 },
13
+ }.freeze
14
+
15
+ DEFAULT_MAX_TOKENS = {
16
+ 'claude-sonnet-4-6' => 16_000,
17
+ 'claude-haiku-4-5-20251001' => 16_000,
18
+ 'claude-opus-4-6' => 4_096,
19
+ }.freeze
20
+
21
+ attr_accessor :provider, :api_key, :model, :thinking_model, :max_tokens,
22
+ :auto_execute, :temperature,
23
+ :timeout, :debug, :max_tool_rounds,
24
+ :storage_adapter, :memories_enabled,
25
+ :session_logging, :connection_class,
26
+ :admin_username, :admin_password,
27
+ :authenticate,
28
+ :slack_bot_token, :slack_app_token, :slack_channel_ids, :slack_allowed_usernames,
29
+ :local_url, :local_model, :local_api_key,
30
+ :bedrock_region
31
+
32
+ def initialize
33
+ @provider = :anthropic
34
+ @api_key = nil
35
+ @model = nil
36
+ @thinking_model = nil
37
+ @max_tokens = nil
38
+ @auto_execute = false
39
+ @temperature = 0.2
40
+ @timeout = 30
41
+ @debug = false
42
+ @max_tool_rounds = 200
43
+ @storage_adapter = nil
44
+ @memories_enabled = true
45
+ @session_logging = true
46
+ @connection_class = nil
47
+ @admin_username = nil
48
+ @admin_password = nil
49
+ @authenticate = nil
50
+ @safety_guards = nil
51
+ @slack_bot_token = nil
52
+ @slack_app_token = nil
53
+ @slack_channel_ids = nil
54
+ @slack_allowed_usernames = nil
55
+ @local_url = 'http://localhost:11434'
56
+ @local_model = 'qwen2.5:7b'
57
+ @local_api_key = nil
58
+ @bedrock_region = nil
59
+ end
60
+
61
+ def safety_guards
62
+ @safety_guards ||= begin
63
+ require 'rails_console_ai/safety_guards'
64
+ SafetyGuards.new
65
+ end
66
+ end
67
+
68
+ # Register a custom safety guard by name with an around-block.
69
+ #
70
+ # config.safety_guard :mailers do |&execute|
71
+ # ActionMailer::Base.perform_deliveries = false
72
+ # execute.call
73
+ # ensure
74
+ # ActionMailer::Base.perform_deliveries = true
75
+ # end
76
+ def safety_guard(name, &block)
77
+ safety_guards.add(name, &block)
78
+ end
79
+
80
+ # Register a built-in safety guard by name.
81
+ # Available: :database_writes, :http_mutations, :mailers
82
+ #
83
+ # Options:
84
+ # allow: Array of strings or regexps to allowlist for this guard.
85
+ # - :http_mutations → hosts (e.g. "s3.amazonaws.com", /googleapis\.com/)
86
+ # - :database_writes → table names (e.g. "rails_console_ai_sessions")
87
+ def use_builtin_safety_guard(name, allow: nil)
88
+ require 'rails_console_ai/safety_guards'
89
+ guard_name = name.to_sym
90
+ case guard_name
91
+ when :database_writes
92
+ safety_guards.add(:database_writes, &BuiltinGuards.database_writes)
93
+ when :http_mutations
94
+ safety_guards.add(:http_mutations, &BuiltinGuards.http_mutations)
95
+ when :mailers
96
+ safety_guards.add(:mailers, &BuiltinGuards.mailers)
97
+ else
98
+ raise ConfigurationError, "Unknown built-in safety guard: #{name}. Available: database_writes, http_mutations, mailers"
99
+ end
100
+
101
+ if allow
102
+ Array(allow).each { |key| safety_guards.allow(guard_name, key) }
103
+ end
104
+ end
105
+
106
+ def resolved_api_key
107
+ return @api_key if @api_key && !@api_key.empty?
108
+
109
+ case @provider
110
+ when :anthropic
111
+ ENV['ANTHROPIC_API_KEY']
112
+ when :openai
113
+ ENV['OPENAI_API_KEY']
114
+ when :local
115
+ @local_api_key || 'no-key'
116
+ when :bedrock
117
+ 'aws-sdk'
118
+ end
119
+ end
120
+
121
+ def resolved_model
122
+ return @model if @model && !@model.empty?
123
+
124
+ case @provider
125
+ when :anthropic
126
+ 'claude-sonnet-4-6'
127
+ when :openai
128
+ 'gpt-5.3-codex'
129
+ when :local
130
+ @local_model
131
+ when :bedrock
132
+ 'us.anthropic.claude-sonnet-4-6'
133
+ end
134
+ end
135
+
136
+ def resolved_max_tokens
137
+ return @max_tokens if @max_tokens
138
+
139
+ DEFAULT_MAX_TOKENS.fetch(resolved_model, 4096)
140
+ end
141
+
142
+ def resolved_thinking_model
143
+ return @thinking_model if @thinking_model && !@thinking_model.empty?
144
+
145
+ case @provider
146
+ when :anthropic
147
+ 'claude-opus-4-6'
148
+ when :openai
149
+ 'gpt-5.3-codex'
150
+ when :local
151
+ @local_model
152
+ when :bedrock
153
+ 'us.anthropic.claude-opus-4-6-v1'
154
+ end
155
+ end
156
+
157
+ def resolved_timeout
158
+ @provider == :local ? [@timeout, 300].max : @timeout
159
+ end
160
+
161
+ def validate!
162
+ unless PROVIDERS.include?(@provider)
163
+ raise ConfigurationError, "Unknown provider: #{@provider}. Valid: #{PROVIDERS.join(', ')}"
164
+ end
165
+
166
+ if @provider == :local
167
+ raise ConfigurationError, "No local_url configured for :local provider." unless @local_url && !@local_url.empty?
168
+ elsif @provider == :bedrock
169
+ begin
170
+ require 'aws-sdk-bedrockruntime'
171
+ rescue LoadError
172
+ raise ConfigurationError,
173
+ "aws-sdk-bedrockruntime gem is required for the :bedrock provider. Add it to your Gemfile."
174
+ end
175
+ else
176
+ unless resolved_api_key
177
+ env_var = @provider == :anthropic ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'
178
+ raise ConfigurationError, "No API key. Set config.api_key or #{env_var} env var."
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ class ConfigurationError < StandardError; end
185
+ end
@@ -0,0 +1,277 @@
1
+ module RailsConsoleAI
2
+ module ConsoleMethods
3
+ def ai_status
4
+ RailsConsoleAI.status
5
+ end
6
+
7
+ def ai_memories(n = nil)
8
+ require 'yaml'
9
+ require 'rails_console_ai/tools/memory_tools'
10
+ storage = RailsConsoleAI.storage
11
+ keys = storage.list('memories/*.md').sort
12
+
13
+ if keys.empty?
14
+ $stdout.puts "\e[2mNo memories stored yet.\e[0m"
15
+ return nil
16
+ end
17
+
18
+ memories = keys.filter_map do |key|
19
+ content = storage.read(key)
20
+ next if content.nil? || content.strip.empty?
21
+ next unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m
22
+ fm = YAML.safe_load($1, permitted_classes: [Time, Date]) || {}
23
+ fm.merge('description' => $2.strip, 'file' => key)
24
+ end
25
+
26
+ if memories.empty?
27
+ $stdout.puts "\e[2mNo memories stored yet.\e[0m"
28
+ return nil
29
+ end
30
+
31
+ shown = n ? memories.last(n) : memories.last(5)
32
+ total = memories.length
33
+
34
+ $stdout.puts "\e[36m[Memories — showing last #{shown.length} of #{total}]\e[0m"
35
+ shown.each do |m|
36
+ $stdout.puts "\e[33m #{m['name']}\e[0m"
37
+ $stdout.puts "\e[2m #{m['description']}\e[0m"
38
+ tags = Array(m['tags'])
39
+ $stdout.puts "\e[2m tags: #{tags.join(', ')}\e[0m" unless tags.empty?
40
+ $stdout.puts
41
+ end
42
+
43
+ path = storage.respond_to?(:root_path) ? File.join(storage.root_path, 'memories') : 'memories/'
44
+ $stdout.puts "\e[2mStored in: #{path}/\e[0m"
45
+ $stdout.puts "\e[2mUse ai_memories(n) to show last n.\e[0m"
46
+ nil
47
+ end
48
+
49
+ def ai_sessions(n = 10, search: nil)
50
+ require 'rails_console_ai/session_logger'
51
+ session_class = Object.const_get('RailsConsoleAI::Session')
52
+
53
+ scope = session_class.recent
54
+ scope = scope.search(search) if search
55
+ sessions = scope.limit(n)
56
+
57
+ if sessions.empty?
58
+ $stdout.puts "\e[2mNo sessions found.\e[0m"
59
+ return nil
60
+ end
61
+
62
+ $stdout.puts "\e[36m[Sessions — showing #{sessions.length}#{search ? " matching \"#{search}\"" : ''}]\e[0m"
63
+ $stdout.puts
64
+
65
+ sessions.each do |s|
66
+ id_str = "\e[2m##{s.id}\e[0m"
67
+ name_str = s.name ? "\e[33m#{s.name}\e[0m " : ""
68
+ query_str = s.name ? "\e[2m#{truncate_str(s.query, 50)}\e[0m" : truncate_str(s.query, 50)
69
+ mode_str = "\e[2m[#{s.mode}]\e[0m"
70
+ time_str = "\e[2m#{time_ago(s.created_at)}\e[0m"
71
+ tokens = (s.input_tokens || 0) + (s.output_tokens || 0)
72
+ token_str = tokens > 0 ? "\e[2m#{tokens} tokens\e[0m" : ""
73
+
74
+ $stdout.puts " #{id_str} #{name_str}#{query_str}"
75
+ $stdout.puts " #{mode_str} #{time_str} #{token_str}"
76
+ $stdout.puts
77
+ end
78
+
79
+ $stdout.puts "\e[2mUse ai_resume(id_or_name) to resume a session.\e[0m"
80
+ $stdout.puts "\e[2mUse ai_sessions(n, search: \"term\") to filter.\e[0m"
81
+ nil
82
+ rescue => e
83
+ $stderr.puts "\e[31mRailsConsoleAI error: #{e.message}\e[0m"
84
+ nil
85
+ end
86
+
87
+ def ai_resume(identifier = nil)
88
+ __ensure_rails_console_ai_user
89
+
90
+ require 'rails_console_ai/context_builder'
91
+ require 'rails_console_ai/providers/base'
92
+ require 'rails_console_ai/executor'
93
+ require 'rails_console_ai/repl'
94
+ require 'rails_console_ai/session_logger'
95
+
96
+ session = if identifier
97
+ __find_session(identifier)
98
+ else
99
+ session_class = Object.const_get('RailsConsoleAI::Session')
100
+ session_class.where(mode: 'interactive', user_name: RailsConsoleAI.current_user).recent.first
101
+ end
102
+
103
+ unless session
104
+ msg = identifier ? "Session not found: #{identifier}" : "No interactive sessions found."
105
+ $stderr.puts "\e[31m#{msg}\e[0m"
106
+ return nil
107
+ end
108
+
109
+ repl = Repl.new(__rails_console_ai_binding)
110
+ repl.resume(session)
111
+ rescue => e
112
+ $stderr.puts "\e[31mRailsConsoleAI error: #{e.message}\e[0m"
113
+ nil
114
+ end
115
+
116
+ def ai_name(identifier, new_name)
117
+ require 'rails_console_ai/session_logger'
118
+
119
+ session = __find_session(identifier)
120
+ unless session
121
+ $stderr.puts "\e[31mSession not found: #{identifier}\e[0m"
122
+ return nil
123
+ end
124
+
125
+ RailsConsoleAI::SessionLogger.update(session.id, name: new_name)
126
+ $stdout.puts "\e[36mSession ##{session.id} named: #{new_name}\e[0m"
127
+ nil
128
+ rescue => e
129
+ $stderr.puts "\e[31mRailsConsoleAI error: #{e.message}\e[0m"
130
+ nil
131
+ end
132
+
133
+ def ai_setup
134
+ RailsConsoleAI.setup!
135
+ end
136
+
137
+ def ai_init
138
+ require 'rails_console_ai/context_builder'
139
+ require 'rails_console_ai/providers/base'
140
+ require 'rails_console_ai/executor'
141
+ require 'rails_console_ai/repl'
142
+
143
+ repl = Repl.new(__rails_console_ai_binding)
144
+ repl.init_guide
145
+ rescue => e
146
+ $stderr.puts "\e[31mRailsConsoleAI error: #{e.message}\e[0m"
147
+ nil
148
+ end
149
+
150
+ def ai(query = nil)
151
+ if query.nil?
152
+ $stderr.puts "\e[33mUsage: ai \"your question here\"\e[0m"
153
+ $stderr.puts "\e[33m ai \"query\" - ask + confirm execution\e[0m"
154
+ $stderr.puts "\e[33m ai! \"query\" - enter interactive mode (or ai! with no args)\e[0m"
155
+ $stderr.puts "\e[33m ai? \"query\" - explain only, no execution\e[0m"
156
+ $stderr.puts "\e[33m ai_init - generate/update app guide for better AI context\e[0m"
157
+ $stderr.puts "\e[33m ai_sessions - list recent sessions\e[0m"
158
+ $stderr.puts "\e[33m ai_resume - resume a session by name or id\e[0m"
159
+ $stderr.puts "\e[33m ai_name - name a session: ai_name 42, \"my_label\"\e[0m"
160
+ $stderr.puts "\e[33m ai_setup - install session logging table\e[0m"
161
+ $stderr.puts "\e[33m ai_status - show current configuration\e[0m"
162
+ $stderr.puts "\e[33m ai_memories - show recent memories (ai_memories(n) for last n)\e[0m"
163
+ return nil
164
+ end
165
+
166
+ __ensure_rails_console_ai_user
167
+
168
+ require 'rails_console_ai/context_builder'
169
+ require 'rails_console_ai/providers/base'
170
+ require 'rails_console_ai/executor'
171
+ require 'rails_console_ai/repl'
172
+
173
+ repl = Repl.new(__rails_console_ai_binding)
174
+ repl.one_shot(query.to_s)
175
+ rescue => e
176
+ $stderr.puts "\e[31mRailsConsoleAI error: #{e.message}\e[0m"
177
+ nil
178
+ end
179
+
180
+ def ai!(query = nil)
181
+ __ensure_rails_console_ai_user
182
+
183
+ require 'rails_console_ai/context_builder'
184
+ require 'rails_console_ai/providers/base'
185
+ require 'rails_console_ai/executor'
186
+ require 'rails_console_ai/repl'
187
+
188
+ repl = Repl.new(__rails_console_ai_binding)
189
+
190
+ if query
191
+ repl.one_shot(query.to_s)
192
+ else
193
+ repl.interactive
194
+ end
195
+ rescue => e
196
+ $stderr.puts "\e[31mRailsConsoleAI error: #{e.message}\e[0m"
197
+ nil
198
+ end
199
+
200
+ def ai?(query = nil)
201
+ unless query
202
+ $stderr.puts "\e[33mUsage: ai? \"your question here\" - explain without executing\e[0m"
203
+ return nil
204
+ end
205
+
206
+ __ensure_rails_console_ai_user
207
+
208
+ require 'rails_console_ai/context_builder'
209
+ require 'rails_console_ai/providers/base'
210
+ require 'rails_console_ai/executor'
211
+ require 'rails_console_ai/repl'
212
+
213
+ repl = Repl.new(__rails_console_ai_binding)
214
+ repl.explain(query.to_s)
215
+ rescue => e
216
+ $stderr.puts "\e[31mRailsConsoleAI error: #{e.message}\e[0m"
217
+ nil
218
+ end
219
+
220
+ private
221
+
222
+ def __find_session(identifier)
223
+ session_class = Object.const_get('RailsConsoleAI::Session')
224
+ if identifier.is_a?(Integer)
225
+ session_class.find_by(id: identifier)
226
+ else
227
+ session_class.where(name: identifier.to_s).recent.first ||
228
+ session_class.find_by(id: identifier.to_i)
229
+ end
230
+ end
231
+
232
+ def truncate_str(str, max)
233
+ return '' if str.nil?
234
+ str.length > max ? str[0...max] + '...' : str
235
+ end
236
+
237
+ def time_ago(time)
238
+ return '' unless time
239
+ seconds = Time.now - time
240
+ case seconds
241
+ when 0...60 then "just now"
242
+ when 60...3600 then "#{(seconds / 60).to_i}m ago"
243
+ when 3600...86400 then "#{(seconds / 3600).to_i}h ago"
244
+ else "#{(seconds / 86400).to_i}d ago"
245
+ end
246
+ end
247
+
248
+ def __ensure_rails_console_ai_user
249
+ return if RailsConsoleAI.current_user
250
+ $stdout.puts "\e[36mRailsConsoleAI logs all AI sessions for audit purposes.\e[0m"
251
+ $stdout.print "\e[36mPlease enter your name: \e[0m"
252
+ name = $stdin.gets.to_s.strip
253
+ RailsConsoleAI.current_user = name.empty? ? ENV['USER'] : name
254
+ end
255
+
256
+ def __rails_console_ai_binding
257
+ # Try Pry first (pry-rails replaces IRB but IRB may still be loaded)
258
+ if defined?(Pry)
259
+ pry_inst = ObjectSpace.each_object(Pry).find { |p|
260
+ p.respond_to?(:binding_stack) && !p.binding_stack.empty?
261
+ } rescue nil
262
+ return pry_inst.current_binding if pry_inst
263
+ end
264
+
265
+ # Try IRB workspace binding
266
+ if defined?(IRB) && IRB.respond_to?(:CurrentContext)
267
+ ctx = IRB.CurrentContext rescue nil
268
+ if ctx && ctx.respond_to?(:workspace) && ctx.workspace.respond_to?(:binding)
269
+ return ctx.workspace.binding
270
+ end
271
+ end
272
+
273
+ # Fallback
274
+ TOPLEVEL_BINDING
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,120 @@
1
+ module RailsConsoleAI
2
+ class ContextBuilder
3
+ def initialize(config = RailsConsoleAI.configuration)
4
+ @config = config
5
+ end
6
+
7
+ def build
8
+ build_smart
9
+ rescue => e
10
+ RailsConsoleAI.logger.warn("RailsConsoleAI: context build error: #{e.message}")
11
+ smart_system_instructions + "\n\n" + environment_context
12
+ end
13
+
14
+ def build_smart
15
+ parts = []
16
+ parts << smart_system_instructions
17
+ parts << environment_context
18
+ parts << guide_context
19
+ parts << memory_context
20
+ parts.compact.join("\n\n")
21
+ end
22
+
23
+ def environment_context
24
+ lines = ["## Environment"]
25
+ lines << "- Ruby #{RUBY_VERSION}"
26
+ lines << "- Rails #{Rails.version}" if defined?(Rails) && Rails.respond_to?(:version)
27
+
28
+ if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
29
+ adapter = ActiveRecord::Base.connection.adapter_name rescue 'unknown'
30
+ lines << "- Database adapter: #{adapter}"
31
+ end
32
+
33
+ if defined?(Bundler)
34
+ key_gems = %w[devise cancancan pundit sidekiq delayed_job resque
35
+ paperclip carrierwave activestorage shrine
36
+ pg mysql2 sqlite3 mongoid]
37
+ loaded = key_gems.select { |g| Gem.loaded_specs.key?(g) }
38
+ lines << "- Key gems: #{loaded.join(', ')}" unless loaded.empty?
39
+ end
40
+
41
+ lines.join("\n")
42
+ end
43
+
44
+ private
45
+
46
+ def smart_system_instructions
47
+ <<~PROMPT.strip
48
+ You are a Ruby on Rails console assistant. The user is in a `rails console` session.
49
+ You help them query data, debug issues, and understand their application.
50
+
51
+ You have tools available to introspect the app's database schema, models, and source code.
52
+ Use them as needed to write accurate queries. For example, call list_tables to see what
53
+ tables exist, then describe_table to get column details for the ones you need.
54
+
55
+ You also have an ask_user tool to ask the console user clarifying questions. Use it when
56
+ you need specific information to write accurate code — such as which user they are, which
57
+ record to target, or what value to use.
58
+
59
+ You have memory tools to persist what you learn across sessions:
60
+ - save_memory: persist facts or procedures you learn about this codebase.
61
+ If a memory with the same name already exists, it will be updated in place.
62
+ - delete_memory: remove a memory by name
63
+ - recall_memories: search your saved memories for details
64
+
65
+ IMPORTANT: Check the Memories section below BEFORE answering. If a memory is relevant,
66
+ use recall_memories to get full details and apply that knowledge to your answer.
67
+ When you use a memory, mention it briefly (e.g. "Based on what I know about sharding...").
68
+ When you discover important patterns about this app, save them as memories.
69
+
70
+ You have an execute_plan tool to run multi-step code. When a task requires multiple
71
+ sequential operations, use execute_plan with an array of steps (each with a description
72
+ and Ruby code). The plan is shown to the user for review before execution begins.
73
+ After each step runs, its return value is stored as step1, step2, etc. — use these
74
+ variables in later steps to reference earlier results (e.g. `api = SalesforceApi.new(step1)`).
75
+ For simple single-expression answers, you may respond with a ```ruby code block instead.
76
+
77
+ RULES:
78
+ - Give ONE concise answer. Do not offer multiple alternatives or variations.
79
+ - For multi-step tasks, use execute_plan to break the work into small, clear steps.
80
+ - For simple queries, respond with a single ```ruby code block.
81
+ - Include a brief one-line explanation before any code block.
82
+ - Use the app's actual model names, associations, and schema.
83
+ - Prefer ActiveRecord query interface over raw SQL.
84
+ - For destructive operations, add a comment warning.
85
+ - NEVER use placeholder values like YOUR_USER_ID or YOUR_EMAIL in code. If you need
86
+ a specific value from the user, call the ask_user tool to get it first.
87
+ - Keep code concise and idiomatic.
88
+ - Use tools to look up schema/model details rather than guessing column names.
89
+ PROMPT
90
+ end
91
+
92
+ def guide_context
93
+ content = RailsConsoleAI.storage.read(RailsConsoleAI::GUIDE_KEY)
94
+ return nil if content.nil? || content.strip.empty?
95
+
96
+ "## Application Guide\n\n#{content.strip}"
97
+ rescue => e
98
+ RailsConsoleAI.logger.debug("RailsConsoleAI: guide context failed: #{e.message}")
99
+ nil
100
+ end
101
+
102
+ def memory_context
103
+ return nil unless @config.memories_enabled
104
+
105
+ require 'rails_console_ai/tools/memory_tools'
106
+ summaries = Tools::MemoryTools.new.memory_summaries
107
+ return nil if summaries.nil? || summaries.empty?
108
+
109
+ lines = ["## Memories"]
110
+ lines.concat(summaries)
111
+ lines << ""
112
+ lines << "Call recall_memories to get details before answering. Do NOT guess from the name alone."
113
+ lines.join("\n")
114
+ rescue => e
115
+ RailsConsoleAI.logger.debug("RailsConsoleAI: memory context failed: #{e.message}")
116
+ nil
117
+ end
118
+
119
+ end
120
+ end