rails_agent_console 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 +7 -0
- data/CHANGELOG.md +80 -0
- data/LICENSE.txt +21 -0
- data/README.md +414 -0
- data/exe/rails-agent +7 -0
- data/lib/generators/rails_agent_console/install_generator.rb +17 -0
- data/lib/generators/rails_agent_console/templates/rails_agent_console.rb +21 -0
- data/lib/rails_agent_console/agent.rb +277 -0
- data/lib/rails_agent_console/cli.rb +114 -0
- data/lib/rails_agent_console/configuration.rb +170 -0
- data/lib/rails_agent_console/console_installer.rb +49 -0
- data/lib/rails_agent_console/console_methods.rb +54 -0
- data/lib/rails_agent_console/console_session.rb +41 -0
- data/lib/rails_agent_console/credentials.rb +66 -0
- data/lib/rails_agent_console/diagnosis/extra.rb +72 -0
- data/lib/rails_agent_console/diagnosis.rb +149 -0
- data/lib/rails_agent_console/errors.rb +20 -0
- data/lib/rails_agent_console/executor.rb +190 -0
- data/lib/rails_agent_console/history.rb +65 -0
- data/lib/rails_agent_console/model_picker.rb +166 -0
- data/lib/rails_agent_console/prompt/dates.rb +34 -0
- data/lib/rails_agent_console/prompt/follow_up.rb +38 -0
- data/lib/rails_agent_console/prompt.rb +122 -0
- data/lib/rails_agent_console/proposal.rb +208 -0
- data/lib/rails_agent_console/providers/anthropic.rb +48 -0
- data/lib/rails_agent_console/providers/base.rb +114 -0
- data/lib/rails_agent_console/providers/callable.rb +28 -0
- data/lib/rails_agent_console/providers/gemini.rb +45 -0
- data/lib/rails_agent_console/providers/ollama.rb +84 -0
- data/lib/rails_agent_console/providers/openai.rb +51 -0
- data/lib/rails_agent_console/providers.rb +28 -0
- data/lib/rails_agent_console/query_validator/model_suggestion.rb +35 -0
- data/lib/rails_agent_console/query_validator/parse_error.rb +23 -0
- data/lib/rails_agent_console/query_validator/result.rb +55 -0
- data/lib/rails_agent_console/query_validator/rules.rb +108 -0
- data/lib/rails_agent_console/query_validator/sensitive_columns.rb +22 -0
- data/lib/rails_agent_console/query_validator.rb +305 -0
- data/lib/rails_agent_console/railtie.rb +26 -0
- data/lib/rails_agent_console/rewriter/arguments.rb +192 -0
- data/lib/rails_agent_console/rewriter/chain.rb +126 -0
- data/lib/rails_agent_console/rewriter/date_ranges.rb +21 -0
- data/lib/rails_agent_console/rewriter/distinct.rb +41 -0
- data/lib/rails_agent_console/rewriter/joined.rb +42 -0
- data/lib/rails_agent_console/rewriter/qualifier.rb +116 -0
- data/lib/rails_agent_console/rewriter/quotes.rb +29 -0
- data/lib/rails_agent_console/rewriter/sql_repair.rb +114 -0
- data/lib/rails_agent_console/rewriter.rb +183 -0
- data/lib/rails_agent_console/schema.rb +275 -0
- data/lib/rails_agent_console/setup.rb +40 -0
- data/lib/rails_agent_console/ui.rb +125 -0
- data/lib/rails_agent_console/value_check.rb +85 -0
- data/lib/rails_agent_console/version.rb +5 -0
- data/lib/rails_agent_console/wizard.rb +235 -0
- data/lib/rails_agent_console.rb +58 -0
- metadata +145 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 664c0e399fd5ad20f7e62cc3ea8f8ef38196b8e7d5b6b168af470935d803a560
|
|
4
|
+
data.tar.gz: 8c6bec024f9944af5a61c670ca14b5f979b3f85660969fa3245a93f1b3dbf52d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '09d76fcdf8d92db3d73fc0e4e019060540cf2cb69583ea0a11218c13772f6f4b8204a33f01910be2cac9632e7af0369d49e223cd18e7ad1cfffc3521e8ec3a3c'
|
|
7
|
+
data.tar.gz: f0c46f91d281e45dc001b7ad442c1aa2ad2dbdadfc64b1402698102ffa84872c6e5825f7bfa28387bfbcfa3dc15d9fb930ad247ea3f869f5029cdb1420638052
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-09-29
|
|
10
|
+
|
|
11
|
+
First public release.
|
|
12
|
+
|
|
13
|
+
### Console helpers
|
|
14
|
+
|
|
15
|
+
- `ai "..."` proposes one ActiveRecord query from a plain-English question, shows it with a
|
|
16
|
+
short explanation and its assumptions, and runs it only after confirmation.
|
|
17
|
+
- `ai! "..."` allows a single write. Destructive calls report the rows they would touch and
|
|
18
|
+
need a typed confirmation word.
|
|
19
|
+
- `ask "..."` answers a question without running anything. A question about "that query" or
|
|
20
|
+
"the last one" is sent together with the session's recent queries.
|
|
21
|
+
- `explain`, `run`, `ai_schema`, `ai_history`, `ai_reset` and `ai_model`.
|
|
22
|
+
- Follow-ups that refer back ("them", "those", "their", and the Croatian "ih" and "njih")
|
|
23
|
+
build on the previous query instead of starting over.
|
|
24
|
+
- Installed as IRB helper methods on Rails 8 and into `Rails::ConsoleMethods` on Rails 7.
|
|
25
|
+
|
|
26
|
+
### Schema context
|
|
27
|
+
|
|
28
|
+
- Built from `ActiveRecord::Base.descendants`: columns, types, nullability and every
|
|
29
|
+
association, including `through:`. Models relevant to the question are described in full
|
|
30
|
+
and the rest are listed by name, about 1,700 tokens for a whole question on an 18-model app.
|
|
31
|
+
- Only the shape of the application is sent to the model, never its data.
|
|
32
|
+
|
|
33
|
+
### Safety
|
|
34
|
+
|
|
35
|
+
- Generated code is parsed with `Ripper` and checked call by call. Read-only by default, with
|
|
36
|
+
an allowlist plus the application's own columns and associations.
|
|
37
|
+
- Always refused, even in write mode: `connection`/`execute`, `send`, `eval`, `system`,
|
|
38
|
+
backticks, `File`, `Kernel`, `ENV`, definitions, instance and global variables, mutating or
|
|
39
|
+
chained raw SQL, and credential columns such as `password_digest`.
|
|
40
|
+
- Generated code runs in a binding of its own, with a timeout.
|
|
41
|
+
|
|
42
|
+
### Corrections made in code, before a query runs
|
|
43
|
+
|
|
44
|
+
- Joins the association cannot build are written out in SQL; association names used as table
|
|
45
|
+
names inside SQL are replaced with the table.
|
|
46
|
+
- Ambiguous columns in joined queries are qualified with the table the query starts from,
|
|
47
|
+
outside of subqueries.
|
|
48
|
+
- A count divided by a count is divided as decimals, so a rate is not rounded down to 0.
|
|
49
|
+
- `joins(search_results)` with a bare association name becomes `joins(:search_results)`.
|
|
50
|
+
- A range that ends on a plain date runs to the end of that day.
|
|
51
|
+
- "in 2025", "last month" and similar phrases are turned into exact ranges before the model
|
|
52
|
+
sees the question.
|
|
53
|
+
- Aggregates placed before relation methods, `first` on a grouped relation, pointless
|
|
54
|
+
`distinct`, raw SQL passed to `order` and `pluck` without `Arel.sql`, and broken quoting.
|
|
55
|
+
|
|
56
|
+
### Results
|
|
57
|
+
|
|
58
|
+
- A condition on a text column that compares it with a value the column never holds
|
|
59
|
+
(`country: "Croatia"` where the data says `HR`) is pointed out after the query runs, with
|
|
60
|
+
the values the column does hold. Those values are printed locally and never sent.
|
|
61
|
+
- Grouped rows and rows with computed columns are shown as their values; whole numbers from
|
|
62
|
+
SQL (`EXTRACT(MONTH ...)`) are shown as integers.
|
|
63
|
+
- A failed query is retried with the error and a Rails-specific hint, up to
|
|
64
|
+
`max_repair_attempts` times. Every retry is a new proposal with the same validation and
|
|
65
|
+
confirmation.
|
|
66
|
+
|
|
67
|
+
### Providers and setup
|
|
68
|
+
|
|
69
|
+
- OpenAI, Anthropic, Gemini and Ollama over `net/http`, any OpenAI-compatible endpoint through
|
|
70
|
+
`api_base`, or any callable through `config.client`.
|
|
71
|
+
- A guided setup on first run, in `ai_model` and in `rails-agent configure`: where the model
|
|
72
|
+
runs, the provider, the base URL, the key, a model from the provider's own list, and a live
|
|
73
|
+
check before anything is kept.
|
|
74
|
+
- `ai_model "gpt-4o"` switches to the provider the name belongs to.
|
|
75
|
+
- Keys are read from the environment first, then from `~/.rails_agent_console/config`
|
|
76
|
+
(mode 0600 in a 0700 directory). A key from the environment is never copied to disk.
|
|
77
|
+
- `rails-agent configure | doctor | schema` and a `rails_agent_console:install` generator.
|
|
78
|
+
|
|
79
|
+
[Unreleased]: https://github.com/blaz1988/rails-agent-console/compare/v0.1.0...HEAD
|
|
80
|
+
[0.1.0]: https://github.com/blaz1988/rails-agent-console/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Blazevic, Rubycode (https://rubycode.co)
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# rails_agent_console
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/rails_agent_console)
|
|
4
|
+
[](https://github.com/blaz1988/rails-agent-console/actions/workflows/main.yml)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
[](#rails-and-ruby-support)
|
|
7
|
+
[](#rails-and-ruby-support)
|
|
8
|
+
|
|
9
|
+
**Plain-English questions in `rails console`, answered with ActiveRecord you read before it runs.**
|
|
10
|
+
|
|
11
|
+
`rails_agent_console` adds `ai`, `ai!`, `ask` and `explain` to the console you already use. It
|
|
12
|
+
describes your real schema to an LLM, gets back one ActiveRecord query, checks it call by call,
|
|
13
|
+
fixes the model's usual mistakes in code, and runs it only after you say yes. It's read-only by
|
|
14
|
+
default, costs about 1,700 tokens per question, and runs for free on a local model.
|
|
15
|
+
|
|
16
|
+
Built and maintained by [Rubycode](https://rubycode.co), a Ruby on Rails consultancy from
|
|
17
|
+
Zagreb. [Need Rails engineers?](#about-rubycode)
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Contents
|
|
22
|
+
|
|
23
|
+
- [Why](#why)
|
|
24
|
+
- [Installation](#installation)
|
|
25
|
+
- [Usage](#usage)
|
|
26
|
+
- [Choosing a model](#choosing-a-model)
|
|
27
|
+
- [Safety](#safety)
|
|
28
|
+
- [Corrections made in code](#corrections-made-in-code)
|
|
29
|
+
- [What the model is told](#what-the-model-is-told)
|
|
30
|
+
- [Configuration](#configuration)
|
|
31
|
+
- [Command line](#command-line)
|
|
32
|
+
- [Rails and Ruby support](#rails-and-ruby-support)
|
|
33
|
+
- [Development](#development)
|
|
34
|
+
- [Contributing](#contributing)
|
|
35
|
+
- [About Rubycode](#about-rubycode)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
38
|
+
## Why
|
|
39
|
+
|
|
40
|
+
Counting rows, checking one record, grouping by a column: these are thirty-second questions.
|
|
41
|
+
Sending them through a general coding agent means it reads `schema.rb`, your models and
|
|
42
|
+
often more before it can write one line of ActiveRecord, and you rarely see that line.
|
|
43
|
+
|
|
44
|
+
This gem is deliberately less than an agent. It never edits files and never loops, and it
|
|
45
|
+
doesn't read your repository. It sends a compact description of your models, gets one query
|
|
46
|
+
back, and shows it to you. The result is an ordinary Ruby value in your session, so you keep
|
|
47
|
+
chaining on it.
|
|
48
|
+
|
|
49
|
+
Measured on a production Rails 7 app with 18 models:
|
|
50
|
+
|
|
51
|
+
| | |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Tokens per question | about 1,700, including the schema context |
|
|
54
|
+
| Cost per question | about $0.0003 on gpt-4o-mini, $0 on Ollama |
|
|
55
|
+
| Model calls | one per question, plus a retry only when a query fails |
|
|
56
|
+
| Schema context vs. `schema.rb` + `app/models` | 1,214 tokens vs. 2,998 |
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
Add the gem to the development group of your application's Gemfile:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
gem "rails_agent_console", group: :development
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then install it and open the console:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bundle install
|
|
70
|
+
bin/rails console
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The first run walks you through setup: where the model runs, the provider, the base URL, the
|
|
74
|
+
key and the model. Every step has a suggestion, so pressing Enter all the way through works.
|
|
75
|
+
The model list comes from the provider itself (the installed models, for Ollama), and the
|
|
76
|
+
choice is checked with one short request before it's kept.
|
|
77
|
+
|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
To keep settings in the application instead, generate an initializer:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bin/rails generate rails_agent_console:install
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### API keys
|
|
87
|
+
|
|
88
|
+
Keys are looked up in this order:
|
|
89
|
+
|
|
90
|
+
1. `RailsAgentConsole.config.api_key`
|
|
91
|
+
2. The environment: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`
|
|
92
|
+
3. `~/.rails_agent_console/config`, mode 0600 in a 0700 directory
|
|
93
|
+
|
|
94
|
+
A key found in the environment is used as it is and never copied to disk. A key you type is
|
|
95
|
+
read without echo. Nothing secret is written into your application.
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
| Command | What it does |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `ai "..."` | Proposes a read-only query, shows it, runs it after confirmation |
|
|
102
|
+
| `ai! "..."` | Allows one write; destructive calls need a typed confirmation |
|
|
103
|
+
| `ask "..."` | Answers a question and never runs anything |
|
|
104
|
+
| `explain User.where(active: true)` | Explains an existing query or relation |
|
|
105
|
+
| `run "..."` | Same as `ai`; reads better for reporting questions |
|
|
106
|
+
| `ai_schema` | Prints exactly what the model is told about your app |
|
|
107
|
+
| `ai_history` | Shows the conversation so far |
|
|
108
|
+
| `ai_reset` | Forgets the conversation |
|
|
109
|
+
| `ai_model` | Shows which model answers, and switches to another |
|
|
110
|
+
|
|
111
|
+
### Asking
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
app(dev)> ai "top 3 brands by searches in 2025, with how many different customers searched each"
|
|
115
|
+
|
|
116
|
+
Proposed query:
|
|
117
|
+
|
|
118
|
+
SearchResult.joins(:brand).where(created_at: Time.zone.parse("2025-01-01")..Time.zone.parse("2025-12-31").end_of_day).group("brands.name").select("brands.name, COUNT(DISTINCT customer_id) AS customer_count").order(Arel.sql("COUNT(*) DESC")).limit(3)
|
|
119
|
+
|
|
120
|
+
This query retrieves the top 3 brands based on the number of searches in 2025, counting distinct customers for each brand. It filters the search results by the specified date range and groups by brand name to aggregate the customer counts.
|
|
121
|
+
Corrected: A range that ended on a date stopped at midnight and missed that day, so it now runs to end_of_day.
|
|
122
|
+
|
|
123
|
+
Execute? [y/N] y
|
|
124
|
+
|
|
125
|
+
✓ 3 items in 16ms
|
|
126
|
+
Grouped rows have no id, so each one is shown as its selected values.
|
|
127
|
+
|
|
128
|
+
=>
|
|
129
|
+
[{"name"=>"Nike", "customer_count"=>36},
|
|
130
|
+
{"name"=>"Adidas", "customer_count"=>26},
|
|
131
|
+
{"name"=>"New Balance", "customer_count"=>22}]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Following up
|
|
135
|
+
|
|
136
|
+
A question that refers back (*them*, *those*, *their*, or the Croatian *ih* and *njih*) is
|
|
137
|
+
sent with the previous query minus its final aggregate, so its conditions carry over:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
app(dev)> ai "How many customers signed up this month?"
|
|
141
|
+
|
|
142
|
+
Customer.where(signed_up_at: Date.current.beginning_of_month..Date.current.end_of_month).count
|
|
143
|
+
|
|
144
|
+
=> 3
|
|
145
|
+
|
|
146
|
+
app(dev)> ai "group them by country"
|
|
147
|
+
|
|
148
|
+
Customer.where(signed_up_at: Date.current.beginning_of_month..Date.current.end_of_month).group(:country).count
|
|
149
|
+
|
|
150
|
+
=> {"AT"=>1, "DE"=>1, "HR"=>1}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### When the data disagrees with the question
|
|
154
|
+
|
|
155
|
+
If a condition compares a text column with a value the column never holds, the gem says so
|
|
156
|
+
and lists the values it does hold. They're worked out from the column and printed in your
|
|
157
|
+
console only; none of them are sent to the model.
|
|
158
|
+
|
|
159
|
+

|
|
160
|
+
|
|
161
|
+
### ask
|
|
162
|
+
|
|
163
|
+
`ask` is for questions that aren't queries. When the question is about "the query you gave
|
|
164
|
+
me", the session's recent queries go with it, so it explains the query that actually ran.
|
|
165
|
+
|
|
166
|
+

|
|
167
|
+
|
|
168
|
+
### Writing
|
|
169
|
+
|
|
170
|
+
Writes need `ai!`. A create or an update asks for a plain yes:
|
|
171
|
+
|
|
172
|
+

|
|
173
|
+
|
|
174
|
+
## Choosing a model
|
|
175
|
+
|
|
176
|
+
| Provider | Setting | Notes |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| OpenAI | `provider: :openai` | default model `gpt-4o-mini` |
|
|
179
|
+
| Anthropic | `provider: :anthropic` | |
|
|
180
|
+
| Gemini | `provider: :gemini` | |
|
|
181
|
+
| Ollama | `provider: :ollama` | default model `qwen2.5-coder:7b`, runs locally, no key |
|
|
182
|
+
| Any OpenAI-compatible endpoint | `api_base: "https://..."` | OpenRouter, LM Studio, vLLM, a company gateway |
|
|
183
|
+
| Anything else | `client: ->(system:, messages:) { ... }` | for example RubyLLM |
|
|
184
|
+
|
|
185
|
+
`ai_model` switches for the rest of the session and offers to make the choice the default.
|
|
186
|
+
The provider follows from the name:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
ai_model "gpt-4o" # OpenAI
|
|
190
|
+
ai_model "claude-sonnet-4-5" # Anthropic
|
|
191
|
+
ai_model "ollama/qwen2.5-coder:7b" # a provider and its model
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Free on Ollama, better on a hosted model.** With Ollama nothing leaves your laptop and there
|
|
195
|
+
is no bill, and a 7B model handles counting, filtering and simple grouping. For anything more
|
|
196
|
+
complex we recommend at least a GPT-4-class model; gpt-4o-mini is enough and costs about
|
|
197
|
+
$0.0003 a question. On four harder questions (a rate per brand compared with a subquery,
|
|
198
|
+
monthly counts with a conditional sum, a filtered top 5, and distinct counts per group),
|
|
199
|
+
gpt-4o-mini answered all four correctly and qwen2.5-coder:7b none. The 7B model's answers ran
|
|
200
|
+
without an error and looked plausible, which is the risk.
|
|
201
|
+
|
|
202
|
+
## Safety
|
|
203
|
+
|
|
204
|
+
An LLM will occasionally suggest `User.delete_all` with total confidence, so generated code is
|
|
205
|
+
never trusted.
|
|
206
|
+
|
|
207
|
+
- **Parsed, not pattern-matched.** Code is parsed with `Ripper` and checked call by call before
|
|
208
|
+
it can run.
|
|
209
|
+
- **Read-only by default.** Every method has to be on an allowlist (`where`, `joins`, `group`,
|
|
210
|
+
`count`, `pluck`, the ActiveSupport time helpers and so on), plus your own columns and
|
|
211
|
+
associations, which come from the schema. Anything else is refused.
|
|
212
|
+
- **Some things are never allowed, even with `ai!`:** `connection` and `execute`, `send`,
|
|
213
|
+
`eval`, `system`, backticks, `File`, `Kernel`, `ENV`, method and class definitions, instance
|
|
214
|
+
and global variables, and raw SQL that modifies data or chains statements, even inside
|
|
215
|
+
`Arel.sql`.
|
|
216
|
+
- **Credential columns are never read.** Columns named like passwords, digests, tokens,
|
|
217
|
+
secrets, API keys and OTP codes are refused by name.
|
|
218
|
+
- **Destructive writes need a typed word.** `delete_all`, `update_all`, `destroy` and friends
|
|
219
|
+
make you type a word, not press a key, and bulk ones report how many rows they would touch.
|
|
220
|
+
- **Isolated.** Generated code runs in a binding of its own with a timeout, so it can't read the
|
|
221
|
+
console's locals, and a bad query reports itself instead of taking the console down.
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
V = RailsAgentConsole::QueryValidator
|
|
225
|
+
|
|
226
|
+
V.validate("Customer.where(plan: 'free').delete_all").violations
|
|
227
|
+
# => ["`delete_all` writes to the database (read-only mode)"]
|
|
228
|
+
|
|
229
|
+
V.validate(%{Customer.connection.execute("DROP TABLE customers")}).violations
|
|
230
|
+
# => ["raw SQL that modifies data: \"DROP TABLE customers\"",
|
|
231
|
+
# "`execute` is never allowed from the agent console",
|
|
232
|
+
# "`connection` is never allowed from the agent console"]
|
|
233
|
+
|
|
234
|
+
V.validate("User.pluck(:email, :password_digest)").violations
|
|
235
|
+
# => ["`password_digest` holds a credential and is never read by the agent"]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+

|
|
239
|
+
|
|
240
|
+
The gem is meant for development and for consoles you'd trust a teammate with. Treat a
|
|
241
|
+
production console with the care it deserves.
|
|
242
|
+
|
|
243
|
+
## Corrections made in code
|
|
244
|
+
|
|
245
|
+
Models make the same few mistakes again and again. Rather than growing the prompt, which is
|
|
246
|
+
sent with every question, the gem fixes them in Ruby before anything runs, and says what it
|
|
247
|
+
changed:
|
|
248
|
+
|
|
249
|
+
- joins the association can't build are written out in SQL;
|
|
250
|
+
- association names used as table names inside SQL are replaced with the table;
|
|
251
|
+
- ambiguous columns in joined queries are qualified with the starting table, outside subqueries;
|
|
252
|
+
- a count divided by a count is divided as decimals, so a rate isn't rounded down to 0;
|
|
253
|
+
- `joins(search_results)` becomes `joins(:search_results)`;
|
|
254
|
+
- a range that ends on a plain date runs to the end of that day;
|
|
255
|
+
- "in 2025" and "last month" become exact ranges before the model sees the question;
|
|
256
|
+
- misplaced aggregates, `first` on a grouped relation, a pointless `distinct`, raw SQL in
|
|
257
|
+
`order` or `pluck` without `Arel.sql`, and broken quoting.
|
|
258
|
+
|
|
259
|
+

|
|
260
|
+
|
|
261
|
+
A query that still fails is sent back with the error and a Rails-specific hint, up to
|
|
262
|
+
`max_repair_attempts` times (three by default). Every retry is a new proposal with the same
|
|
263
|
+
validation and the same confirmation.
|
|
264
|
+
|
|
265
|
+
## What the model is told
|
|
266
|
+
|
|
267
|
+
Only the shape of your application, never its data:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
Rails 8.1.4, adapter: sqlite3
|
|
271
|
+
|
|
272
|
+
Customer (customers)
|
|
273
|
+
columns: id:integer (pk), name:string (null), country:string (null), plan:string (null)
|
|
274
|
+
has_one :subscription
|
|
275
|
+
has_many :orders
|
|
276
|
+
has_many :payments through: :orders
|
|
277
|
+
|
|
278
|
+
Invoice (invoices)
|
|
279
|
+
columns: id:integer (pk), customer_id:integer, amount:decimal (null), status:string (null)
|
|
280
|
+
belongs_to :customer
|
|
281
|
+
|
|
282
|
+
Other models: Order, Payment, SupportTicket
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The models relevant to the question are described in full and the rest are listed by name,
|
|
286
|
+
which keeps the prompt small in applications with hundreds of models. `ai_schema "invoices"`
|
|
287
|
+
shows exactly what would be sent.
|
|
288
|
+
|
|
289
|
+
## Configuration
|
|
290
|
+
|
|
291
|
+
Everything has a default. To change it, use the generated initializer:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
# config/initializers/rails_agent_console.rb
|
|
295
|
+
RailsAgentConsole.configure do |config|
|
|
296
|
+
config.provider = :openai # :openai, :anthropic, :gemini, :ollama
|
|
297
|
+
config.model = "gpt-4o-mini" # any model the provider accepts
|
|
298
|
+
config.api_base = nil # e.g. "https://openrouter.ai/api/v1"
|
|
299
|
+
|
|
300
|
+
config.write_mode = false # `ai!` is usually the better choice
|
|
301
|
+
config.auto_confirm = false # true skips the Execute? prompt
|
|
302
|
+
config.extra_allowed_methods = %w[to_csv] # widen the read-only allowlist
|
|
303
|
+
config.execution_timeout = 30 # seconds, nil disables
|
|
304
|
+
|
|
305
|
+
config.excluded_models = %w[AuditLog] # keep models out of the prompt
|
|
306
|
+
config.max_focused_models = 8
|
|
307
|
+
config.extra_context = "Revenue always lives on Payment#amount_cents."
|
|
308
|
+
|
|
309
|
+
config.max_history = 6 # follow-up turns to remember
|
|
310
|
+
config.max_repair_attempts = 3 # retries after a failed query, 0 disables
|
|
311
|
+
config.console_helpers = %i[ai ai! ask explain run]
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Any callable can stand in for the built-in providers, for example to run on top of
|
|
316
|
+
[RubyLLM](https://rubyllm.com):
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
config.client = lambda do |system:, messages:|
|
|
320
|
+
RubyLLM.chat.with_instructions(system).ask(messages.last[:content]).content
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Command line
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
bundle exec rails-agent configure # the guided setup, outside the console
|
|
328
|
+
bundle exec rails-agent doctor # the resolved configuration, and a ping to the provider
|
|
329
|
+
bundle exec rails-agent schema # the schema context the model receives
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Rails and Ruby support
|
|
333
|
+
|
|
334
|
+
Ruby 3.1 or newer and Rails 7.0 or newer. CI runs the suite on every supported combination:
|
|
335
|
+
|
|
336
|
+
| | Rails 7.0 | Rails 7.1 | Rails 7.2 | Rails 8.0 | Rails 8.1 |
|
|
337
|
+
| --- | :---: | :---: | :---: | :---: | :---: |
|
|
338
|
+
| Ruby 3.1 | ✓ | ✓ | ✓ | | |
|
|
339
|
+
| Ruby 3.2 | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
340
|
+
| Ruby 3.3 | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
341
|
+
| Ruby 3.4 | | | ✓ | ✓ | ✓ |
|
|
342
|
+
|
|
343
|
+
Rails 8 needs Ruby 3.2 or newer, and Rails 7.0 and 7.1 don't load on Ruby 3.4. On Rails 8
|
|
344
|
+
the helpers are registered as IRB helper methods, the mechanism behind `app` and `reload!`; on
|
|
345
|
+
Rails 7 they're mixed into `Rails::ConsoleMethods`. Providers talk plain HTTP through
|
|
346
|
+
`net/http`, so the gem depends on nothing beyond Rails.
|
|
347
|
+
|
|
348
|
+
## Development
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
bin/setup
|
|
352
|
+
bundle exec rake # specs and RuboCop
|
|
353
|
+
bin/matrix # the specs on every supported Ruby and Rails combination
|
|
354
|
+
bin/matrix 3.3.3 # ... or on one Ruby
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
The specs run against an in-memory SQLite schema, and the provider specs against a local
|
|
358
|
+
socket, so nothing reaches the network. `bin/matrix` uses rbenv for the Rubies and
|
|
359
|
+
`gemfiles/rails_*.gemfile` for the Rails versions.
|
|
360
|
+
|
|
361
|
+
## Contributing
|
|
362
|
+
|
|
363
|
+
Contributions are very welcome, whether it's a bug report, a question the gem answered wrong, a
|
|
364
|
+
new provider or a fix in the rewriter. You don't need permission to start.
|
|
365
|
+
|
|
366
|
+
**Found a problem?** [Open an issue](https://github.com/blaz1988/rails-agent-console/issues/new)
|
|
367
|
+
with your Ruby, Rails and gem versions, the question you asked, the query the gem proposed and
|
|
368
|
+
what went wrong. A wrong answer from the model is a useful report too: most of them can be
|
|
369
|
+
fixed in code so the next person doesn't hit them.
|
|
370
|
+
|
|
371
|
+
**Want to send a fix?** Contributions go through a fork and a pull request:
|
|
372
|
+
|
|
373
|
+
1. [Fork the repository](https://github.com/blaz1988/rails-agent-console/fork) and clone your
|
|
374
|
+
fork.
|
|
375
|
+
2. Create a branch for your change: `git checkout -b fix-date-ranges`.
|
|
376
|
+
3. Run `bin/setup`, make the change, and add a spec for it.
|
|
377
|
+
4. Run `bundle exec rake` and make sure specs and RuboCop pass.
|
|
378
|
+
5. Add a line to the `Unreleased` section of [CHANGELOG.md](CHANGELOG.md).
|
|
379
|
+
6. Push the branch to your fork and
|
|
380
|
+
[open a pull request](https://github.com/blaz1988/rails-agent-console/compare) against `main`.
|
|
381
|
+
|
|
382
|
+
CI runs the specs on every supported Ruby and Rails version, and every pull request is reviewed
|
|
383
|
+
before it's merged. For a larger change, such as a new provider or a new console command, open
|
|
384
|
+
an issue first so we can agree on the approach before you write the code.
|
|
385
|
+
|
|
386
|
+
Good first contributions:
|
|
387
|
+
|
|
388
|
+
- a question that produced the wrong query, turned into a failing spec;
|
|
389
|
+
- a correction in the rewriter for a mistake models keep making;
|
|
390
|
+
- support for another LLM provider;
|
|
391
|
+
- clearer docs or error messages.
|
|
392
|
+
|
|
393
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the details. Please report security issues privately
|
|
394
|
+
as described in [SECURITY.md](SECURITY.md), not in a public issue.
|
|
395
|
+
|
|
396
|
+
## About Rubycode
|
|
397
|
+
|
|
398
|
+
`rails_agent_console` is written and maintained by [Rubycode](https://rubycode.co). We build
|
|
399
|
+
and rescue Ruby on Rails products: new applications, upgrades, performance work, and senior
|
|
400
|
+
Ruby and Rails engineers who join your team.
|
|
401
|
+
|
|
402
|
+
**Need Ruby or Ruby on Rails engineers? Get in touch.**
|
|
403
|
+
|
|
404
|
+
**Ivan Blažević**, creator of the gem
|
|
405
|
+
|
|
406
|
+
- Email: [ivan.blazevic@rubycode.co](mailto:ivan.blazevic@rubycode.co)
|
|
407
|
+
- Phone: [+385 99 351 3642](tel:+385993513642)
|
|
408
|
+
- LinkedIn: [linkedin.com/in/blazevic-ivan](https://www.linkedin.com/in/blazevic-ivan/)
|
|
409
|
+
- Web: [rubycode.co](https://rubycode.co)
|
|
410
|
+
|
|
411
|
+
## License
|
|
412
|
+
|
|
413
|
+
Released under the [MIT License](LICENSE.txt). Copyright © 2026 Ivan Blažević,
|
|
414
|
+
[Rubycode](https://rubycode.co).
|
data/exe/rails-agent
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module RailsAgentConsole
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Creates config/initializers/rails_agent_console.rb"
|
|
11
|
+
|
|
12
|
+
def create_initializer
|
|
13
|
+
template "rails_agent_console.rb", "config/initializers/rails_agent_console.rb"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rails_agent_console is only active inside `rails console`.
|
|
4
|
+
# API keys are read from the environment or ~/.rails_agent_console/config,
|
|
5
|
+
# so nothing secret belongs in this file.
|
|
6
|
+
RailsAgentConsole.configure do |config|
|
|
7
|
+
# config.provider = :openai # :openai, :anthropic, :gemini, :ollama
|
|
8
|
+
# config.model = "gpt-4o-mini"
|
|
9
|
+
|
|
10
|
+
# Safety. Leave write_mode off; use `ai!` for a one-off write instead.
|
|
11
|
+
# config.write_mode = false
|
|
12
|
+
# config.extra_allowed_methods = %w[to_csv]
|
|
13
|
+
|
|
14
|
+
# Schema context sent to the model.
|
|
15
|
+
# config.excluded_models = %w[AuditLog]
|
|
16
|
+
# config.max_focused_models = 8
|
|
17
|
+
# config.extra_context = "Revenue always lives on Payment#amount_cents."
|
|
18
|
+
|
|
19
|
+
# Console helpers to install (drop :run if it collides with your own helper).
|
|
20
|
+
# config.console_helpers = %i[ai ai! ask explain run]
|
|
21
|
+
end
|