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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +414 -0
  5. data/exe/rails-agent +7 -0
  6. data/lib/generators/rails_agent_console/install_generator.rb +17 -0
  7. data/lib/generators/rails_agent_console/templates/rails_agent_console.rb +21 -0
  8. data/lib/rails_agent_console/agent.rb +277 -0
  9. data/lib/rails_agent_console/cli.rb +114 -0
  10. data/lib/rails_agent_console/configuration.rb +170 -0
  11. data/lib/rails_agent_console/console_installer.rb +49 -0
  12. data/lib/rails_agent_console/console_methods.rb +54 -0
  13. data/lib/rails_agent_console/console_session.rb +41 -0
  14. data/lib/rails_agent_console/credentials.rb +66 -0
  15. data/lib/rails_agent_console/diagnosis/extra.rb +72 -0
  16. data/lib/rails_agent_console/diagnosis.rb +149 -0
  17. data/lib/rails_agent_console/errors.rb +20 -0
  18. data/lib/rails_agent_console/executor.rb +190 -0
  19. data/lib/rails_agent_console/history.rb +65 -0
  20. data/lib/rails_agent_console/model_picker.rb +166 -0
  21. data/lib/rails_agent_console/prompt/dates.rb +34 -0
  22. data/lib/rails_agent_console/prompt/follow_up.rb +38 -0
  23. data/lib/rails_agent_console/prompt.rb +122 -0
  24. data/lib/rails_agent_console/proposal.rb +208 -0
  25. data/lib/rails_agent_console/providers/anthropic.rb +48 -0
  26. data/lib/rails_agent_console/providers/base.rb +114 -0
  27. data/lib/rails_agent_console/providers/callable.rb +28 -0
  28. data/lib/rails_agent_console/providers/gemini.rb +45 -0
  29. data/lib/rails_agent_console/providers/ollama.rb +84 -0
  30. data/lib/rails_agent_console/providers/openai.rb +51 -0
  31. data/lib/rails_agent_console/providers.rb +28 -0
  32. data/lib/rails_agent_console/query_validator/model_suggestion.rb +35 -0
  33. data/lib/rails_agent_console/query_validator/parse_error.rb +23 -0
  34. data/lib/rails_agent_console/query_validator/result.rb +55 -0
  35. data/lib/rails_agent_console/query_validator/rules.rb +108 -0
  36. data/lib/rails_agent_console/query_validator/sensitive_columns.rb +22 -0
  37. data/lib/rails_agent_console/query_validator.rb +305 -0
  38. data/lib/rails_agent_console/railtie.rb +26 -0
  39. data/lib/rails_agent_console/rewriter/arguments.rb +192 -0
  40. data/lib/rails_agent_console/rewriter/chain.rb +126 -0
  41. data/lib/rails_agent_console/rewriter/date_ranges.rb +21 -0
  42. data/lib/rails_agent_console/rewriter/distinct.rb +41 -0
  43. data/lib/rails_agent_console/rewriter/joined.rb +42 -0
  44. data/lib/rails_agent_console/rewriter/qualifier.rb +116 -0
  45. data/lib/rails_agent_console/rewriter/quotes.rb +29 -0
  46. data/lib/rails_agent_console/rewriter/sql_repair.rb +114 -0
  47. data/lib/rails_agent_console/rewriter.rb +183 -0
  48. data/lib/rails_agent_console/schema.rb +275 -0
  49. data/lib/rails_agent_console/setup.rb +40 -0
  50. data/lib/rails_agent_console/ui.rb +125 -0
  51. data/lib/rails_agent_console/value_check.rb +85 -0
  52. data/lib/rails_agent_console/version.rb +5 -0
  53. data/lib/rails_agent_console/wizard.rb +235 -0
  54. data/lib/rails_agent_console.rb +58 -0
  55. 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
+ [![Gem Version](https://badge.fury.io/rb/rails_agent_console.svg)](https://rubygems.org/gems/rails_agent_console)
4
+ [![CI](https://github.com/blaz1988/rails-agent-console/actions/workflows/main.yml/badge.svg)](https://github.com/blaz1988/rails-agent-console/actions/workflows/main.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-3.1%20to%203.4-CC342D.svg)](#rails-and-ruby-support)
7
+ [![Rails](https://img.shields.io/badge/Rails-7.0%20to%208.1-D30001.svg)](#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
+ ![A question, the models it inspected, and the proposed query](docs/images/first-question.png)
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
+ ![The guided setup](docs/images/setup.png)
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
+ ![A value the column never holds, and the follow-up that fixes it](docs/images/value-check.png)
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
+ ![ask explaining the query that ran](docs/images/ask.png)
167
+
168
+ ### Writing
169
+
170
+ Writes need `ai!`. A create or an update asks for a plain yes:
171
+
172
+ ![ai! creating, renaming and deleting a record](docs/images/writes.png)
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
+ ![The destructive confirmation prompt](docs/images/destructive-guard.png)
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
+ ![The gem correcting the model's query before it runs](docs/images/corrected.png)
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,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rails_agent_console"
5
+ require "rails_agent_console/cli"
6
+
7
+ exit RailsAgentConsole::CLI.new(ARGV).run
@@ -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