console_agent 0.0.1 → 0.1.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 +4 -4
- data/LICENSE +21 -0
- data/README.md +235 -0
- data/lib/console_agent/configuration.rb +58 -0
- data/lib/console_agent/console_methods.rb +88 -0
- data/lib/console_agent/context_builder.rb +200 -0
- data/lib/console_agent/executor.rb +131 -0
- data/lib/console_agent/providers/anthropic.rb +112 -0
- data/lib/console_agent/providers/base.rb +106 -0
- data/lib/console_agent/providers/openai.rb +114 -0
- data/lib/console_agent/railtie.rb +30 -0
- data/lib/console_agent/repl.rb +286 -0
- data/lib/console_agent/tools/code_tools.rb +114 -0
- data/lib/console_agent/tools/model_tools.rb +95 -0
- data/lib/console_agent/tools/registry.rb +181 -0
- data/lib/console_agent/tools/schema_tools.rb +60 -0
- data/lib/console_agent/version.rb +3 -0
- data/lib/console_agent.rb +58 -1
- data/lib/generators/console_agent/install_generator.rb +26 -0
- data/lib/generators/console_agent/templates/initializer.rb +33 -0
- metadata +92 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09b1eab5b0fd84fb27fe9af623c234da290779f84d5972d4a67bfe000d581ab2'
|
|
4
|
+
data.tar.gz: 344f3afbc67adc4d87c5c2fb4e11ccc902fca071776a4a3aa6cb160783e22998
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73a5c9bd000e30b3cfa63c06efbb0f9619849ec56ca8b4b8f1c71cd7eae29439f9b4ef8d10725950aa8fb813159674820706fa5883784060464f8c9959c914aa
|
|
7
|
+
data.tar.gz: 4498692aab0860b717b7d309e252cb1b2130b7946a55421f0edd76c3b3469da27c6f8426667e23cf0a2fcab66a356896573f19b819c8ba603f08f50a5e60647d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frank Cort
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# ConsoleAgent
|
|
2
|
+
|
|
3
|
+
An AI-powered assistant for your Rails console. Ask questions in plain English, get executable Ruby code.
|
|
4
|
+
|
|
5
|
+
ConsoleAgent connects your `rails console` to an LLM (Claude or GPT) and gives it tools to introspect your app's database schema, models, and source code. It figures out what it needs on its own, so you don't pay for 50K tokens of context on every query.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'console_agent', group: :development
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run the install generator:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
rails generate console_agent:install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Set your API key and open a console:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
26
|
+
rails console
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
ai "show me all users who signed up this week"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Console Commands
|
|
36
|
+
|
|
37
|
+
| Command | Description |
|
|
38
|
+
|---------|-------------|
|
|
39
|
+
| `ai "query"` | Ask a question, review generated code, confirm before executing |
|
|
40
|
+
| `ai! "query"` | Ask a question and enter interactive mode for follow-ups |
|
|
41
|
+
| `ai!` | Enter interactive mode (type `exit` to leave) |
|
|
42
|
+
| `ai? "query"` | Explain only — shows code but never executes |
|
|
43
|
+
| `ai_status` | Print current configuration summary |
|
|
44
|
+
|
|
45
|
+
### Example Session
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
irb> ai "find the 5 most recent orders over $100"
|
|
49
|
+
Thinking...
|
|
50
|
+
-> list_tables
|
|
51
|
+
12 tables: users, orders, line_items, products...
|
|
52
|
+
-> describe_table("orders")
|
|
53
|
+
8 columns
|
|
54
|
+
-> describe_model("Order")
|
|
55
|
+
4 associations, 2 validations
|
|
56
|
+
|
|
57
|
+
You can query recent high-value orders like this:
|
|
58
|
+
|
|
59
|
+
Order.where("total > ?", 100).order(created_at: :desc).limit(5)
|
|
60
|
+
|
|
61
|
+
[tokens in: 1,240 | out: 85 | total: 1,325]
|
|
62
|
+
Execute? [y/N/edit] y
|
|
63
|
+
=> [#<Order id: 4821, ...>, ...]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Interactive Mode
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
irb> ai!
|
|
70
|
+
ConsoleAgent interactive mode. Type 'exit' or 'quit' to leave.
|
|
71
|
+
ai> show me all tables
|
|
72
|
+
Thinking...
|
|
73
|
+
-> list_tables
|
|
74
|
+
12 tables: users, orders, line_items, products...
|
|
75
|
+
...
|
|
76
|
+
ai> now count orders by status
|
|
77
|
+
...
|
|
78
|
+
ai> exit
|
|
79
|
+
[session totals — in: 3,200 | out: 410 | total: 3,610]
|
|
80
|
+
Left ConsoleAgent interactive mode.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Configuration Status
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
irb> ai_status
|
|
87
|
+
[ConsoleAgent v0.1.0]
|
|
88
|
+
Provider: anthropic
|
|
89
|
+
Model: claude-opus-4-6
|
|
90
|
+
API key: sk-ant-...a3b4
|
|
91
|
+
Context mode: smart
|
|
92
|
+
Max tokens: 4096
|
|
93
|
+
Temperature: 0.2
|
|
94
|
+
Timeout: 30s
|
|
95
|
+
Max tool rounds:10
|
|
96
|
+
Auto-execute: false
|
|
97
|
+
Debug: false
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
The install generator creates `config/initializers/console_agent.rb`:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
ConsoleAgent.configure do |config|
|
|
106
|
+
# LLM provider: :anthropic or :openai
|
|
107
|
+
config.provider = :anthropic
|
|
108
|
+
|
|
109
|
+
# API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
|
|
110
|
+
# config.api_key = 'sk-...'
|
|
111
|
+
|
|
112
|
+
# Model override (defaults: claude-opus-4-6 for Anthropic, gpt-5.3-codex for OpenAI)
|
|
113
|
+
# config.model = 'claude-opus-4-6'
|
|
114
|
+
|
|
115
|
+
# Context mode:
|
|
116
|
+
# :smart - (default) LLM uses tools to fetch schema/model/code on demand
|
|
117
|
+
# :full - sends all schema, models, and routes every time
|
|
118
|
+
config.context_mode = :smart
|
|
119
|
+
|
|
120
|
+
# Max tokens for LLM response
|
|
121
|
+
config.max_tokens = 4096
|
|
122
|
+
|
|
123
|
+
# Temperature (0.0 - 1.0)
|
|
124
|
+
config.temperature = 0.2
|
|
125
|
+
|
|
126
|
+
# Auto-execute generated code without confirmation
|
|
127
|
+
config.auto_execute = false
|
|
128
|
+
|
|
129
|
+
# Max tool-use rounds per query in :smart mode
|
|
130
|
+
config.max_tool_rounds = 10
|
|
131
|
+
|
|
132
|
+
# HTTP timeout in seconds
|
|
133
|
+
config.timeout = 30
|
|
134
|
+
|
|
135
|
+
# Debug mode: prints full API requests/responses and tool calls
|
|
136
|
+
# config.debug = true
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
You can also change settings at runtime in the console:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
ConsoleAgent.configure { |c| c.debug = true }
|
|
144
|
+
ConsoleAgent.configure { |c| c.provider = :openai }
|
|
145
|
+
ENV['OPENAI_API_KEY'] = 'sk-...'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Context Modes
|
|
149
|
+
|
|
150
|
+
### Smart Mode (default)
|
|
151
|
+
|
|
152
|
+
The LLM gets a minimal system prompt and uses tools to look up what it needs:
|
|
153
|
+
|
|
154
|
+
- **list_tables** / **describe_table** — database schema on demand
|
|
155
|
+
- **list_models** / **describe_model** — ActiveRecord associations, validations
|
|
156
|
+
- **list_files** / **read_file** / **search_code** — browse app source code
|
|
157
|
+
|
|
158
|
+
This keeps token usage low (~1-3K per query) even for large apps with hundreds of tables.
|
|
159
|
+
|
|
160
|
+
### Full Mode
|
|
161
|
+
|
|
162
|
+
Sends the entire database schema, all model details, and a route summary in every request. Simple but expensive for large apps (~50K+ tokens). Useful if you want everything available without tool-call round trips.
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
ConsoleAgent.configure { |c| c.context_mode = :full }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Tools Available in Smart Mode
|
|
169
|
+
|
|
170
|
+
| Tool | Description |
|
|
171
|
+
|------|-------------|
|
|
172
|
+
| `list_tables` | All database table names |
|
|
173
|
+
| `describe_table` | Columns, types, and indexes for one table |
|
|
174
|
+
| `list_models` | All model names with association names |
|
|
175
|
+
| `describe_model` | Associations, validations, scopes for one model |
|
|
176
|
+
| `list_files` | Ruby files in a directory |
|
|
177
|
+
| `read_file` | Read a source file (capped at 200 lines) |
|
|
178
|
+
| `search_code` | Grep for a pattern across Ruby files |
|
|
179
|
+
|
|
180
|
+
The LLM decides which tools to call based on your question. You can see the tool calls happening in real time.
|
|
181
|
+
|
|
182
|
+
## Providers
|
|
183
|
+
|
|
184
|
+
### Anthropic (default)
|
|
185
|
+
|
|
186
|
+
Uses the Claude Messages API. Set `ANTHROPIC_API_KEY` or `config.api_key`.
|
|
187
|
+
|
|
188
|
+
### OpenAI
|
|
189
|
+
|
|
190
|
+
Uses the Chat Completions API. Set `OPENAI_API_KEY` or `config.api_key`.
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
ConsoleAgent.configure do |config|
|
|
194
|
+
config.provider = :openai
|
|
195
|
+
config.model = 'gpt-5.3-codex' # optional, this is the default
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Docker Setup
|
|
200
|
+
|
|
201
|
+
If your Rails app runs in Docker, mount the gem source as a volume.
|
|
202
|
+
|
|
203
|
+
In `docker-compose.yml`:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
volumes:
|
|
207
|
+
- /path/to/console_agent:/console_agent
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
In `Gemfile`:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
gem 'console_agent', path: '/console_agent', group: :development
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
For Docker builds (where the gem source isn't available yet), add a stub to your Dockerfile before `bundle install`:
|
|
217
|
+
|
|
218
|
+
```dockerfile
|
|
219
|
+
RUN mkdir -p /console_agent/lib/console_agent && \
|
|
220
|
+
echo "module ConsoleAgent; VERSION = '0.1.0'; end" > /console_agent/lib/console_agent/version.rb && \
|
|
221
|
+
echo "require 'console_agent/version'" > /console_agent/lib/console_agent.rb && \
|
|
222
|
+
printf "require_relative 'lib/console_agent/version'\nGem::Specification.new do |s|\n s.name = 'console_agent'\n s.version = ConsoleAgent::VERSION\n s.summary = 'stub'\n s.authors = ['x']\n s.files = ['lib/console_agent.rb']\n s.add_dependency 'rails', '>= 5.0'\n s.add_dependency 'faraday', '>= 1.0'\nend\n" > /console_agent/console_agent.gemspec
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The volume mount overwrites the stub at runtime with the real source.
|
|
226
|
+
|
|
227
|
+
## Requirements
|
|
228
|
+
|
|
229
|
+
- Ruby >= 2.5
|
|
230
|
+
- Rails >= 5.0
|
|
231
|
+
- Faraday >= 1.0
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module ConsoleAgent
|
|
2
|
+
class Configuration
|
|
3
|
+
PROVIDERS = %i[anthropic openai].freeze
|
|
4
|
+
CONTEXT_MODES = %i[full smart].freeze
|
|
5
|
+
|
|
6
|
+
attr_accessor :provider, :api_key, :model, :max_tokens,
|
|
7
|
+
:auto_execute, :context_mode, :temperature,
|
|
8
|
+
:timeout, :debug, :max_tool_rounds
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@provider = :anthropic
|
|
12
|
+
@api_key = nil
|
|
13
|
+
@model = nil
|
|
14
|
+
@max_tokens = 4096
|
|
15
|
+
@auto_execute = false
|
|
16
|
+
@context_mode = :smart
|
|
17
|
+
@temperature = 0.2
|
|
18
|
+
@timeout = 30
|
|
19
|
+
@debug = false
|
|
20
|
+
@max_tool_rounds = 10
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def resolved_api_key
|
|
24
|
+
return @api_key if @api_key && !@api_key.empty?
|
|
25
|
+
|
|
26
|
+
case @provider
|
|
27
|
+
when :anthropic
|
|
28
|
+
ENV['ANTHROPIC_API_KEY']
|
|
29
|
+
when :openai
|
|
30
|
+
ENV['OPENAI_API_KEY']
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def resolved_model
|
|
35
|
+
return @model if @model && !@model.empty?
|
|
36
|
+
|
|
37
|
+
case @provider
|
|
38
|
+
when :anthropic
|
|
39
|
+
'claude-opus-4-6'
|
|
40
|
+
when :openai
|
|
41
|
+
'gpt-5.3-codex'
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate!
|
|
46
|
+
unless PROVIDERS.include?(@provider)
|
|
47
|
+
raise ConfigurationError, "Unknown provider: #{@provider}. Valid: #{PROVIDERS.join(', ')}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless resolved_api_key
|
|
51
|
+
env_var = @provider == :anthropic ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'
|
|
52
|
+
raise ConfigurationError, "No API key. Set config.api_key or #{env_var} env var."
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class ConfigurationError < StandardError; end
|
|
58
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module ConsoleAgent
|
|
2
|
+
module ConsoleMethods
|
|
3
|
+
def ai_status
|
|
4
|
+
ConsoleAgent.status
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def ai(query = nil)
|
|
8
|
+
if query.nil?
|
|
9
|
+
$stderr.puts "\e[33mUsage: ai \"your question here\"\e[0m"
|
|
10
|
+
$stderr.puts "\e[33m ai \"query\" - ask + confirm execution\e[0m"
|
|
11
|
+
$stderr.puts "\e[33m ai! \"query\" - enter interactive mode (or ai! with no args)\e[0m"
|
|
12
|
+
$stderr.puts "\e[33m ai? \"query\" - explain only, no execution\e[0m"
|
|
13
|
+
$stderr.puts "\e[33m ai_status - show current configuration\e[0m"
|
|
14
|
+
return nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require 'console_agent/context_builder'
|
|
18
|
+
require 'console_agent/providers/base'
|
|
19
|
+
require 'console_agent/executor'
|
|
20
|
+
require 'console_agent/repl'
|
|
21
|
+
|
|
22
|
+
repl = Repl.new(__console_agent_binding)
|
|
23
|
+
repl.one_shot(query.to_s)
|
|
24
|
+
rescue => e
|
|
25
|
+
$stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ai!(query = nil)
|
|
30
|
+
require 'console_agent/context_builder'
|
|
31
|
+
require 'console_agent/providers/base'
|
|
32
|
+
require 'console_agent/executor'
|
|
33
|
+
require 'console_agent/repl'
|
|
34
|
+
|
|
35
|
+
repl = Repl.new(__console_agent_binding)
|
|
36
|
+
|
|
37
|
+
if query
|
|
38
|
+
repl.one_shot(query.to_s)
|
|
39
|
+
else
|
|
40
|
+
repl.interactive
|
|
41
|
+
end
|
|
42
|
+
rescue => e
|
|
43
|
+
$stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ai?(query = nil)
|
|
48
|
+
unless query
|
|
49
|
+
$stderr.puts "\e[33mUsage: ai? \"your question here\" - explain without executing\e[0m"
|
|
50
|
+
return nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
require 'console_agent/context_builder'
|
|
54
|
+
require 'console_agent/providers/base'
|
|
55
|
+
require 'console_agent/executor'
|
|
56
|
+
require 'console_agent/repl'
|
|
57
|
+
|
|
58
|
+
repl = Repl.new(__console_agent_binding)
|
|
59
|
+
repl.explain(query.to_s)
|
|
60
|
+
rescue => e
|
|
61
|
+
$stderr.puts "\e[31mConsoleAgent error: #{e.message}\e[0m"
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def __console_agent_binding
|
|
68
|
+
# Try IRB workspace binding
|
|
69
|
+
if defined?(IRB) && IRB.respond_to?(:CurrentContext)
|
|
70
|
+
ctx = IRB.CurrentContext rescue nil
|
|
71
|
+
if ctx && ctx.respond_to?(:workspace) && ctx.workspace.respond_to?(:binding)
|
|
72
|
+
return ctx.workspace.binding
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Try Pry binding
|
|
77
|
+
if defined?(Pry) && respond_to?(:pry_instance, true)
|
|
78
|
+
pry_inst = pry_instance rescue nil
|
|
79
|
+
if pry_inst && pry_inst.respond_to?(:current_binding)
|
|
80
|
+
return pry_inst.current_binding
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Fallback
|
|
85
|
+
TOPLEVEL_BINDING
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
module ConsoleAgent
|
|
2
|
+
class ContextBuilder
|
|
3
|
+
def initialize(config = ConsoleAgent.configuration)
|
|
4
|
+
@config = config
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def build
|
|
8
|
+
case @config.context_mode
|
|
9
|
+
when :smart
|
|
10
|
+
build_smart
|
|
11
|
+
else
|
|
12
|
+
build_full
|
|
13
|
+
end
|
|
14
|
+
rescue => e
|
|
15
|
+
ConsoleAgent.logger.warn("ConsoleAgent: context build error: #{e.message}")
|
|
16
|
+
system_instructions + "\n\n" + environment_context
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def build_full
|
|
20
|
+
parts = []
|
|
21
|
+
parts << system_instructions
|
|
22
|
+
parts << environment_context
|
|
23
|
+
parts << schema_context
|
|
24
|
+
parts << models_context
|
|
25
|
+
parts << routes_context
|
|
26
|
+
parts.compact.join("\n\n")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_smart
|
|
30
|
+
parts = []
|
|
31
|
+
parts << smart_system_instructions
|
|
32
|
+
parts << environment_context
|
|
33
|
+
parts.compact.join("\n\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def smart_system_instructions
|
|
39
|
+
<<~PROMPT.strip
|
|
40
|
+
You are a Ruby on Rails console assistant. The user is in a `rails console` session.
|
|
41
|
+
You help them query data, debug issues, and understand their application.
|
|
42
|
+
|
|
43
|
+
You have tools available to introspect the app's database schema, models, and source code.
|
|
44
|
+
Use them as needed to write accurate queries. For example, call list_tables to see what
|
|
45
|
+
tables exist, then describe_table to get column details for the ones you need.
|
|
46
|
+
|
|
47
|
+
You also have an ask_user tool to ask the console user clarifying questions. Use it when
|
|
48
|
+
you need specific information to write accurate code — such as which user they are, which
|
|
49
|
+
record to target, or what value to use.
|
|
50
|
+
|
|
51
|
+
RULES:
|
|
52
|
+
- Give ONE concise answer. Do not offer multiple alternatives or variations.
|
|
53
|
+
- Respond with a single ```ruby code block that directly answers the question.
|
|
54
|
+
- Include a brief one-line explanation before the code block.
|
|
55
|
+
- Use the app's actual model names, associations, and schema.
|
|
56
|
+
- Prefer ActiveRecord query interface over raw SQL.
|
|
57
|
+
- For destructive operations, add a comment warning.
|
|
58
|
+
- NEVER use placeholder values like YOUR_USER_ID or YOUR_EMAIL in code. If you need
|
|
59
|
+
a specific value from the user, call the ask_user tool to get it first.
|
|
60
|
+
- Keep code concise and idiomatic.
|
|
61
|
+
- Use tools to look up schema/model details rather than guessing column names.
|
|
62
|
+
PROMPT
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def system_instructions
|
|
66
|
+
<<~PROMPT.strip
|
|
67
|
+
You are a Ruby on Rails console assistant. The user is in a `rails console` session.
|
|
68
|
+
You help them query data, debug issues, and understand their application.
|
|
69
|
+
|
|
70
|
+
RULES:
|
|
71
|
+
- Give ONE concise answer. Do not offer multiple alternatives or variations.
|
|
72
|
+
- Respond with a single ```ruby code block that directly answers the question.
|
|
73
|
+
- Include a brief one-line explanation before the code block.
|
|
74
|
+
- Use the app's actual model names, associations, and schema.
|
|
75
|
+
- Prefer ActiveRecord query interface over raw SQL.
|
|
76
|
+
- For destructive operations, add a comment warning.
|
|
77
|
+
- If the request is ambiguous, ask a clarifying question instead of guessing.
|
|
78
|
+
- Keep code concise and idiomatic.
|
|
79
|
+
PROMPT
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def environment_context
|
|
83
|
+
lines = ["## Environment"]
|
|
84
|
+
lines << "- Ruby #{RUBY_VERSION}"
|
|
85
|
+
lines << "- Rails #{Rails.version}" if defined?(Rails) && Rails.respond_to?(:version)
|
|
86
|
+
|
|
87
|
+
if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
88
|
+
adapter = ActiveRecord::Base.connection.adapter_name rescue 'unknown'
|
|
89
|
+
lines << "- Database adapter: #{adapter}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if defined?(Bundler)
|
|
93
|
+
key_gems = %w[devise cancancan pundit sidekiq delayed_job resque
|
|
94
|
+
paperclip carrierwave activestorage shrine
|
|
95
|
+
pg mysql2 sqlite3 mongoid]
|
|
96
|
+
loaded = key_gems.select { |g| Gem.loaded_specs.key?(g) }
|
|
97
|
+
lines << "- Key gems: #{loaded.join(', ')}" unless loaded.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
lines.join("\n")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def schema_context
|
|
104
|
+
return nil unless defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
105
|
+
|
|
106
|
+
conn = ActiveRecord::Base.connection
|
|
107
|
+
tables = conn.tables.sort
|
|
108
|
+
return nil if tables.empty?
|
|
109
|
+
|
|
110
|
+
lines = ["## Database Schema"]
|
|
111
|
+
tables.each do |table|
|
|
112
|
+
next if table == 'schema_migrations' || table == 'ar_internal_metadata'
|
|
113
|
+
|
|
114
|
+
cols = conn.columns(table).map { |c| "#{c.name}:#{c.type}" }
|
|
115
|
+
lines << "- #{table} (#{cols.join(', ')})"
|
|
116
|
+
end
|
|
117
|
+
lines.join("\n")
|
|
118
|
+
rescue => e
|
|
119
|
+
ConsoleAgent.logger.debug("ConsoleAgent: schema introspection failed: #{e.message}")
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def models_context
|
|
124
|
+
return nil unless defined?(ActiveRecord::Base)
|
|
125
|
+
|
|
126
|
+
eager_load_app!
|
|
127
|
+
base_class = defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base
|
|
128
|
+
models = ObjectSpace.each_object(Class).select { |c|
|
|
129
|
+
c < base_class && !c.abstract_class? && c.name && !c.name.start_with?('HABTM_')
|
|
130
|
+
}.sort_by(&:name)
|
|
131
|
+
|
|
132
|
+
return nil if models.empty?
|
|
133
|
+
|
|
134
|
+
lines = ["## Models"]
|
|
135
|
+
models.each do |model|
|
|
136
|
+
parts = [model.name]
|
|
137
|
+
|
|
138
|
+
assocs = model.reflect_on_all_associations.map { |a| "#{a.macro} :#{a.name}" }
|
|
139
|
+
parts << " associations: #{assocs.join(', ')}" unless assocs.empty?
|
|
140
|
+
|
|
141
|
+
scopes = model.methods.grep(/^_scope_/).map { |m| m.to_s.sub('_scope_', '') } rescue []
|
|
142
|
+
if scopes.empty?
|
|
143
|
+
# Alternative: check singleton methods that return ActiveRecord::Relation
|
|
144
|
+
scope_names = (model.singleton_methods - base_class.singleton_methods).select { |m|
|
|
145
|
+
m != :all && !m.to_s.start_with?('_') && !m.to_s.start_with?('find')
|
|
146
|
+
}.first(10)
|
|
147
|
+
# We won't list these to avoid noise — scopes are hard to detect reliably
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
validations = model.validators.map { |v|
|
|
151
|
+
attrs = v.attributes.join(', ')
|
|
152
|
+
"#{v.class.name.demodulize.underscore.sub('_validator', '')} on #{attrs}"
|
|
153
|
+
}.uniq.first(5) rescue []
|
|
154
|
+
parts << " validations: #{validations.join('; ')}" unless validations.empty?
|
|
155
|
+
|
|
156
|
+
lines << "- #{parts.join("\n")}"
|
|
157
|
+
end
|
|
158
|
+
lines.join("\n")
|
|
159
|
+
rescue => e
|
|
160
|
+
ConsoleAgent.logger.debug("ConsoleAgent: model introspection failed: #{e.message}")
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def routes_context
|
|
165
|
+
return nil unless defined?(Rails) && Rails.respond_to?(:application)
|
|
166
|
+
|
|
167
|
+
routes = Rails.application.routes.routes
|
|
168
|
+
return nil if routes.empty?
|
|
169
|
+
|
|
170
|
+
lines = ["## Routes (summary)"]
|
|
171
|
+
count = 0
|
|
172
|
+
routes.each do |route|
|
|
173
|
+
next if route.internal?
|
|
174
|
+
path = route.path.spec.to_s.sub('(.:format)', '')
|
|
175
|
+
verb = route.verb.to_s
|
|
176
|
+
next if verb.empty?
|
|
177
|
+
action = route.defaults[:controller].to_s + '#' + route.defaults[:action].to_s
|
|
178
|
+
lines << "- #{verb} #{path} -> #{action}"
|
|
179
|
+
count += 1
|
|
180
|
+
break if count >= 50
|
|
181
|
+
end
|
|
182
|
+
lines << "- ... (#{routes.size - count} more routes)" if routes.size > count
|
|
183
|
+
|
|
184
|
+
lines.join("\n")
|
|
185
|
+
rescue => e
|
|
186
|
+
ConsoleAgent.logger.debug("ConsoleAgent: route introspection failed: #{e.message}")
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def eager_load_app!
|
|
191
|
+
return unless defined?(Rails) && Rails.respond_to?(:application)
|
|
192
|
+
|
|
193
|
+
if Rails.application.respond_to?(:eager_load!)
|
|
194
|
+
Rails.application.eager_load!
|
|
195
|
+
end
|
|
196
|
+
rescue => e
|
|
197
|
+
ConsoleAgent.logger.debug("ConsoleAgent: eager_load failed: #{e.message}")
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|