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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +95 -0
- data/LICENSE +21 -0
- data/README.md +328 -0
- data/app/controllers/rails_console_ai/application_controller.rb +28 -0
- data/app/controllers/rails_console_ai/sessions_controller.rb +16 -0
- data/app/helpers/rails_console_ai/sessions_helper.rb +56 -0
- data/app/models/rails_console_ai/session.rb +23 -0
- data/app/views/layouts/rails_console_ai/application.html.erb +84 -0
- data/app/views/rails_console_ai/sessions/index.html.erb +57 -0
- data/app/views/rails_console_ai/sessions/show.html.erb +66 -0
- data/config/routes.rb +4 -0
- data/lib/generators/rails_console_ai/install_generator.rb +26 -0
- data/lib/generators/rails_console_ai/templates/initializer.rb +79 -0
- data/lib/rails_console_ai/channel/base.rb +23 -0
- data/lib/rails_console_ai/channel/console.rb +457 -0
- data/lib/rails_console_ai/channel/slack.rb +182 -0
- data/lib/rails_console_ai/configuration.rb +185 -0
- data/lib/rails_console_ai/console_methods.rb +277 -0
- data/lib/rails_console_ai/context_builder.rb +120 -0
- data/lib/rails_console_ai/conversation_engine.rb +1142 -0
- data/lib/rails_console_ai/engine.rb +5 -0
- data/lib/rails_console_ai/executor.rb +461 -0
- data/lib/rails_console_ai/providers/anthropic.rb +122 -0
- data/lib/rails_console_ai/providers/base.rb +118 -0
- data/lib/rails_console_ai/providers/bedrock.rb +171 -0
- data/lib/rails_console_ai/providers/local.rb +112 -0
- data/lib/rails_console_ai/providers/openai.rb +114 -0
- data/lib/rails_console_ai/railtie.rb +34 -0
- data/lib/rails_console_ai/repl.rb +65 -0
- data/lib/rails_console_ai/safety_guards.rb +207 -0
- data/lib/rails_console_ai/session_logger.rb +90 -0
- data/lib/rails_console_ai/slack_bot.rb +473 -0
- data/lib/rails_console_ai/storage/base.rb +27 -0
- data/lib/rails_console_ai/storage/file_storage.rb +63 -0
- data/lib/rails_console_ai/tools/code_tools.rb +126 -0
- data/lib/rails_console_ai/tools/memory_tools.rb +136 -0
- data/lib/rails_console_ai/tools/model_tools.rb +95 -0
- data/lib/rails_console_ai/tools/registry.rb +478 -0
- data/lib/rails_console_ai/tools/schema_tools.rb +60 -0
- data/lib/rails_console_ai/version.rb +3 -0
- data/lib/rails_console_ai.rb +214 -0
- data/lib/tasks/rails_console_ai.rake +7 -0
- 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
|