rails_console_ai 0.19.0 → 0.21.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/CHANGELOG.md +28 -0
- data/README.md +84 -6
- data/app/views/rails_console_ai/sessions/index.html.erb +2 -0
- data/app/views/rails_console_ai/sessions/show.html.erb +6 -0
- data/lib/generators/rails_console_ai/templates/initializer.rb +12 -0
- data/lib/rails_console_ai/channel/base.rb +1 -0
- data/lib/rails_console_ai/channel/console.rb +59 -1
- data/lib/rails_console_ai/channel/slack.rb +17 -2
- data/lib/rails_console_ai/configuration.rb +12 -1
- data/lib/rails_console_ai/console_methods.rb +7 -2
- data/lib/rails_console_ai/context_builder.rb +79 -9
- data/lib/rails_console_ai/conversation_engine.rb +69 -13
- data/lib/rails_console_ai/executor.rb +45 -48
- data/lib/rails_console_ai/providers/bedrock.rb +5 -3
- data/lib/rails_console_ai/safety_guards.rb +108 -4
- data/lib/rails_console_ai/session_logger.rb +2 -1
- data/lib/rails_console_ai/skill_loader.rb +53 -0
- data/lib/rails_console_ai/slack_bot.rb +416 -40
- data/lib/rails_console_ai/tools/registry.rb +84 -1
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +11 -4
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8faec693c3d3c83dafcaa65437081d5ba8e235f04527a28182104c4ecba5b80d
|
|
4
|
+
data.tar.gz: 171445a3ddc0c50093b1688260545df1558548a29cb81a6d3262d15d8ec4cf3c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d0689ebfc82073deb58f8bd03fce39503541d63f042f531322bfc11ef0d6cf65f69fa9ae59c8b89a8a549ea9b4b7893e8505da83887ec4cbc5c1a064ffa5d99
|
|
7
|
+
data.tar.gz: e8f635c26035fcf73f2ccda5bd3d403a2ec33ef806c701c28f67ee8888a8189625a2569f45c369f7b29189fda3b162f03067be26590afbe5428be9da15487552
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.21.0]
|
|
6
|
+
|
|
7
|
+
- Add Slack @mention support and channel name tracking
|
|
8
|
+
- Stop looping after user cancels execution
|
|
9
|
+
- Remove edit feature from executor
|
|
10
|
+
- Include more info in tool call log line
|
|
11
|
+
- Support class methods in `bypass_guards_for_methods`
|
|
12
|
+
- Rename setup tasks to `ai_db_setup` and `ai_db_migrate`
|
|
13
|
+
- Fix effective model resolution in multi-threaded Slack bot
|
|
14
|
+
- Fix cost tracking with prompt caching through Bedrock
|
|
15
|
+
- Add `/unthink` command
|
|
16
|
+
- Make `!think` / `/think` thread-safe for Slack
|
|
17
|
+
- Fix truncating console output
|
|
18
|
+
- Reduce cost by deferring large output until LLM requests it
|
|
19
|
+
- Add `!name`, `!model`, and `/model` commands to Slack and console
|
|
20
|
+
|
|
21
|
+
## [0.20.0]
|
|
22
|
+
|
|
23
|
+
- Add per-user system prompt seeding
|
|
24
|
+
- Improve explicit code execution through Slack bot
|
|
25
|
+
- Show last thinking output before prompting
|
|
26
|
+
- Add Skills system
|
|
27
|
+
- Add `bypass_guards_for_methods` to allow specific methods to skip safety guards
|
|
28
|
+
- Add `execute_code` tool for simple query execution without code fences
|
|
29
|
+
- Support `<code>` tags in Slack responses
|
|
30
|
+
- Improve Slack bot server logging and keepalive visibility
|
|
31
|
+
- Improve database safety guards in Rails 5
|
|
32
|
+
|
|
5
33
|
## [0.19.0]
|
|
6
34
|
|
|
7
35
|
- Fix duplicate tool result IDs in AWS Bedrock provider
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ irb> ai "find the 5 most recent orders over $100"
|
|
|
12
12
|
|
|
13
13
|
Order.where("total > ?", 100).order(created_at: :desc).limit(5)
|
|
14
14
|
|
|
15
|
-
Execute? [y/N/
|
|
15
|
+
Execute? [y/N/danger] y
|
|
16
16
|
=> [#<Order id: 4821, ...>, ...]
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -62,7 +62,8 @@ end
|
|
|
62
62
|
| `ai!` | Enter interactive mode (multi-turn conversation) |
|
|
63
63
|
| `ai? "query"` | Explain only, no execution |
|
|
64
64
|
| `ai_init` | Generate app guide for better AI context |
|
|
65
|
-
| `
|
|
65
|
+
| `ai_db_setup` | Install session logging table + run migrations |
|
|
66
|
+
| `ai_db_migrate` | Run pending session table migrations |
|
|
66
67
|
| `ai_sessions` | List recent sessions |
|
|
67
68
|
| `ai_resume` | Resume a session by name or ID |
|
|
68
69
|
| `ai_memories` | Show stored memories |
|
|
@@ -97,9 +98,10 @@ Say "think harder" in any query to auto-upgrade to the thinking model for that s
|
|
|
97
98
|
- **Multi-step plans** — complex tasks are broken into steps, executed sequentially with `step1`/`step2` references
|
|
98
99
|
- **Two-tier models** — defaults to Sonnet for speed/cost; `/think` upgrades to Opus when you need it
|
|
99
100
|
- **Cost tracking** — `/cost` shows per-model token usage and estimated spend
|
|
101
|
+
- **Skills** — predefined procedures with guard bypasses that the AI activates on demand
|
|
100
102
|
- **Memories** — AI saves what it learns about your app across sessions
|
|
101
103
|
- **App guide** — `ai_init` generates a guide injected into every system prompt
|
|
102
|
-
- **Sessions** — name, list, and resume interactive conversations (`
|
|
104
|
+
- **Sessions** — name, list, and resume interactive conversations (`ai_db_setup` to enable)
|
|
103
105
|
- **History compaction** — `/compact` summarizes long conversations to reduce cost and latency
|
|
104
106
|
- **Output trimming** — older execution outputs are automatically replaced with references; the LLM can recall them on demand via `recall_output`, and you can `/expand <id>` to see them
|
|
105
107
|
- **Debug mode** — `/debug` shows context breakdown, token counts, and per-call cost estimates before and after each LLM call
|
|
@@ -141,10 +143,78 @@ Raise `RailsConsoleAi::SafetyError` in your app code to trigger the safe mode pr
|
|
|
141
143
|
raise RailsConsoleAi::SafetyError, "Stripe charge blocked"
|
|
142
144
|
```
|
|
143
145
|
|
|
146
|
+
### Allowing Specific Methods
|
|
147
|
+
|
|
148
|
+
Some operations (like admin approvals) need to write to the database even when guards are active. Use `bypass_guards_for_methods` to declare methods that should bypass all safety guards when called during an AI session:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
RailsConsoleAi.configure do |config|
|
|
152
|
+
# Global — applies to all channels
|
|
153
|
+
config.bypass_guards_for_methods = [
|
|
154
|
+
'ChangeApproval#approve_by!',
|
|
155
|
+
'ChangeApproval#reject_by!'
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
# Per-channel — only active in the specified channel
|
|
159
|
+
config.channels = {
|
|
160
|
+
'slack' => { 'bypass_guards_for_methods' => ['Deployment#promote!'] },
|
|
161
|
+
'console' => {}
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Global and channel-specific methods are merged for the active channel. These method shims are installed lazily on the first AI execution (not at boot) and are session-scoped — they only bypass guards inside `SafetyGuards#wrap`. Outside of an AI session (e.g. in normal web requests), the methods behave normally with zero overhead beyond a single thread-local read.
|
|
167
|
+
|
|
168
|
+
The AI is told about these trusted methods in its system prompt and will use them directly without triggering safety errors.
|
|
169
|
+
|
|
170
|
+
### Skills
|
|
171
|
+
|
|
172
|
+
Skills bundle a step-by-step recipe with guard bypass declarations into a single file. Unlike `bypass_guards_for_methods` (which is always-on), skill bypasses are only active after the AI explicitly activates the skill.
|
|
173
|
+
|
|
174
|
+
Create markdown files in `.rails_console_ai/skills/`:
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
---
|
|
178
|
+
name: Approve/Reject ChangeApprovals
|
|
179
|
+
description: Approve or reject change approval records on behalf of an admin
|
|
180
|
+
tags:
|
|
181
|
+
- change-approval
|
|
182
|
+
- admin
|
|
183
|
+
bypass_guards_for_methods:
|
|
184
|
+
- "ChangeApproval#approve_by!"
|
|
185
|
+
- "ChangeApproval#reject_by!"
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## When to use
|
|
189
|
+
Use when the user asks to approve or reject a change approval.
|
|
190
|
+
|
|
191
|
+
## Recipe
|
|
192
|
+
1. Find the ChangeApproval by ID or search
|
|
193
|
+
2. Confirm approve or reject
|
|
194
|
+
3. Get optional review notes
|
|
195
|
+
4. Determine which admin user is acting
|
|
196
|
+
5. Call approve_by! or reject_by!
|
|
197
|
+
|
|
198
|
+
## Code Examples
|
|
199
|
+
|
|
200
|
+
ca = ChangeApproval.find(id)
|
|
201
|
+
admin = User.find_by!(email: "admin@example.com")
|
|
202
|
+
ca.approve_by!(admin, "Approved per request")
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**How it works:**
|
|
206
|
+
|
|
207
|
+
1. Skill summaries (name + description) appear in the AI's system prompt
|
|
208
|
+
2. When the user's request matches a skill, the AI calls `activate_skill` to load the full recipe
|
|
209
|
+
3. The skill's `bypass_guards_for_methods` are added to the active bypass set
|
|
210
|
+
4. The AI follows the recipe, executing code with the declared methods bypassing safety guards
|
|
211
|
+
|
|
212
|
+
Skills and global `bypass_guards_for_methods` coexist — use config-level bypasses for simple trusted methods, and skills for operations that benefit from a documented procedure.
|
|
213
|
+
|
|
144
214
|
### Toggling Safe Mode
|
|
145
215
|
|
|
146
216
|
- **`/danger`** in interactive mode toggles all guards off/on for the session
|
|
147
|
-
- **`d`** at the `Execute? [y/N/
|
|
217
|
+
- **`d`** at the `Execute? [y/N/danger]` prompt disables guards for that single execution
|
|
148
218
|
- When a guard blocks an operation, the user is prompted: `Re-run with safe mode disabled? [y/N]`
|
|
149
219
|
|
|
150
220
|
## LLM Providers
|
|
@@ -227,7 +297,7 @@ Timeout is automatically raised to 300s minimum for local models to account for
|
|
|
227
297
|
RailsConsoleAi.configure do |config|
|
|
228
298
|
config.provider = :anthropic # :anthropic, :openai, :bedrock, :local
|
|
229
299
|
config.auto_execute = false # true to skip confirmations
|
|
230
|
-
config.session_logging = true # requires
|
|
300
|
+
config.session_logging = true # requires ai_db_setup
|
|
231
301
|
config.temperature = 0.2
|
|
232
302
|
config.timeout = 30 # HTTP timeout in seconds
|
|
233
303
|
config.max_tool_rounds = 200 # safety cap on tool-use loops
|
|
@@ -294,11 +364,14 @@ Run RailsConsoleAi as a Slack bot. Each Slack thread becomes an independent AI s
|
|
|
294
364
|
3. **Bot Token Scopes** — OAuth & Permissions → Bot Token Scopes, add:
|
|
295
365
|
- `chat:write`
|
|
296
366
|
- `channels:history` (public channels)
|
|
367
|
+
- `channels:read` (channel names in logs, optional)
|
|
297
368
|
- `groups:history` (private channels, optional)
|
|
369
|
+
- `groups:read` (private channel names in logs, optional)
|
|
298
370
|
- `im:history` (direct messages)
|
|
299
371
|
- `users:read`
|
|
300
372
|
|
|
301
373
|
4. **Event Subscriptions** — Event Subscriptions → toggle ON, then under "Subscribe to bot events" add:
|
|
374
|
+
- `app_mention` (respond when @mentioned in any channel)
|
|
302
375
|
- `message.channels` (public channels)
|
|
303
376
|
- `message.groups` (private channels, optional)
|
|
304
377
|
- `message.im` (direct messages)
|
|
@@ -330,7 +403,12 @@ end
|
|
|
330
403
|
bundle exec rake rails_console_ai:slack
|
|
331
404
|
```
|
|
332
405
|
|
|
333
|
-
This starts a long-running process (run it separately from your web server).
|
|
406
|
+
This starts a long-running process (run it separately from your web server). The bot auto-executes code with safety guards always enabled — there is no `/danger` equivalent in Slack.
|
|
407
|
+
|
|
408
|
+
**@mention behavior:**
|
|
409
|
+
- **DMs** — the bot responds to all messages, no @mention needed.
|
|
410
|
+
- **Channels** — the bot only responds when @mentioned. @mention it in any channel message or thread to start a session. The person who first @mentions the bot owns the session — only they can continue the conversation, and they must @mention the bot on each message. Exception: when the bot asks a question, the owner can reply without @mentioning.
|
|
411
|
+
- **Joining threads** — when @mentioned mid-thread, the bot reads the thread history for context so it understands what's already been discussed.
|
|
334
412
|
|
|
335
413
|
## Requirements
|
|
336
414
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<tr>
|
|
14
14
|
<th>Time</th>
|
|
15
15
|
<th>User</th>
|
|
16
|
+
<th>Channel</th>
|
|
16
17
|
<th>Name</th>
|
|
17
18
|
<th style="max-width: 400px;">Query</th>
|
|
18
19
|
<th>Mode</th>
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
<tr>
|
|
27
28
|
<td class="mono"><%= session.created_at.strftime('%Y-%m-%d %H:%M') %></td>
|
|
28
29
|
<td><%= session.user_name %></td>
|
|
30
|
+
<td><%= session.try(:slack_channel_name).presence || '-' %></td>
|
|
29
31
|
<td><%= session.name.present? ? session.name : '-' %></td>
|
|
30
32
|
<td class="query-cell"><a href="<%= rails_console_ai.session_path(session) %>" title="<%= h session.query.truncate(200) %>"><%= truncate(session.query.gsub(/\s+/, ' ').strip, length: 80) %></a></td>
|
|
31
33
|
<td><span class="badge badge-<%= session.mode %>"><%= session.mode %></span></td>
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
<label>User</label>
|
|
31
31
|
<span><%= @session.user_name || '-' %></span>
|
|
32
32
|
</div>
|
|
33
|
+
<% if @session.try(:slack_channel_name).present? %>
|
|
34
|
+
<div class="meta-item">
|
|
35
|
+
<label>Channel</label>
|
|
36
|
+
<span><%= @session.slack_channel_name %></span>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
33
39
|
<div class="meta-item">
|
|
34
40
|
<label>Provider / Model</label>
|
|
35
41
|
<span><%= @session.provider %> / <%= @session.model %></span>
|
|
@@ -76,4 +76,16 @@ RailsConsoleAi.configure do |config|
|
|
|
76
76
|
# config.safety_guard :jobs do |&execute|
|
|
77
77
|
# Sidekiq::Testing.fake! { execute.call }
|
|
78
78
|
# end
|
|
79
|
+
#
|
|
80
|
+
# Bypass ALL safety guards when specific methods are called (e.g. trusted admin actions):
|
|
81
|
+
# config.bypass_guards_for_methods = [
|
|
82
|
+
# 'ChangeApproval#approve_by!',
|
|
83
|
+
# 'ChangeApproval#reject_by!'
|
|
84
|
+
# ]
|
|
85
|
+
|
|
86
|
+
# Per-channel settings (channel mode keys: 'slack', 'console'):
|
|
87
|
+
# config.channels = {
|
|
88
|
+
# 'slack' => { 'pinned_memory_tags' => ['sharding'], 'bypass_guards_for_methods' => ['ChangeApproval#approve_by!'] },
|
|
89
|
+
# 'console' => { 'pinned_memory_tags' => [] }
|
|
90
|
+
# }
|
|
79
91
|
end
|
|
@@ -7,6 +7,7 @@ module RailsConsoleAi
|
|
|
7
7
|
def display_error(text); raise NotImplementedError; end
|
|
8
8
|
def display_code(code); raise NotImplementedError; end
|
|
9
9
|
def display_result(text); raise NotImplementedError; end
|
|
10
|
+
def display_tool_call(text); end # tool call: "-> name(args)"
|
|
10
11
|
def display_result_output(text); end # stdout output from code execution
|
|
11
12
|
def prompt(text); raise NotImplementedError; end
|
|
12
13
|
def confirm(text); raise NotImplementedError; end
|
|
@@ -18,6 +18,10 @@ module RailsConsoleAi
|
|
|
18
18
|
$stdout.puts "\e[2m#{text}\e[0m"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def display_tool_call(text)
|
|
22
|
+
$stdout.puts "\e[33m -> #{text}\e[0m"
|
|
23
|
+
end
|
|
24
|
+
|
|
21
25
|
def display_warning(text)
|
|
22
26
|
$stdout.puts colorize(text, :yellow)
|
|
23
27
|
end
|
|
@@ -33,6 +37,33 @@ module RailsConsoleAi
|
|
|
33
37
|
$stdout.puts
|
|
34
38
|
end
|
|
35
39
|
|
|
40
|
+
def display_result_output(output)
|
|
41
|
+
text = output.to_s
|
|
42
|
+
return if text.strip.empty?
|
|
43
|
+
|
|
44
|
+
lines = text.lines
|
|
45
|
+
total_lines = lines.length
|
|
46
|
+
total_chars = text.length
|
|
47
|
+
|
|
48
|
+
if total_lines <= MAX_DISPLAY_LINES && total_chars <= MAX_DISPLAY_CHARS
|
|
49
|
+
$stdout.print text
|
|
50
|
+
else
|
|
51
|
+
truncated = lines.first(MAX_DISPLAY_LINES).join
|
|
52
|
+
truncated = truncated[0, MAX_DISPLAY_CHARS] if truncated.length > MAX_DISPLAY_CHARS
|
|
53
|
+
$stdout.print truncated
|
|
54
|
+
|
|
55
|
+
omitted_lines = [total_lines - MAX_DISPLAY_LINES, 0].max
|
|
56
|
+
omitted_chars = [total_chars - truncated.length, 0].max
|
|
57
|
+
parts = []
|
|
58
|
+
parts << "#{omitted_lines} lines" if omitted_lines > 0
|
|
59
|
+
parts << "#{omitted_chars} chars" if omitted_chars > 0
|
|
60
|
+
|
|
61
|
+
@omitted_counter += 1
|
|
62
|
+
@omitted_outputs[@omitted_counter] = text
|
|
63
|
+
$stdout.puts colorize(" (output truncated, omitting #{parts.join(', ')}) /expand #{@omitted_counter} to see all", :yellow)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
36
67
|
def display_result(result)
|
|
37
68
|
full = "=> #{result.inspect}"
|
|
38
69
|
lines = full.lines
|
|
@@ -75,7 +106,7 @@ module RailsConsoleAi
|
|
|
75
106
|
end
|
|
76
107
|
|
|
77
108
|
def mode
|
|
78
|
-
'
|
|
109
|
+
'console'
|
|
79
110
|
end
|
|
80
111
|
|
|
81
112
|
def supports_editing?
|
|
@@ -251,8 +282,12 @@ module RailsConsoleAi
|
|
|
251
282
|
@engine.display_conversation
|
|
252
283
|
when '/cost'
|
|
253
284
|
@engine.display_cost_summary
|
|
285
|
+
when '/model'
|
|
286
|
+
display_model_info
|
|
254
287
|
when '/think'
|
|
255
288
|
@engine.upgrade_to_thinking_model
|
|
289
|
+
when '/unthink'
|
|
290
|
+
@engine.downgrade_from_thinking_model
|
|
256
291
|
when /\A\/expand/
|
|
257
292
|
expand_id = input.sub('/expand', '').strip.to_i
|
|
258
293
|
full_output = expand_output(expand_id)
|
|
@@ -316,6 +351,27 @@ module RailsConsoleAi
|
|
|
316
351
|
end
|
|
317
352
|
end
|
|
318
353
|
|
|
354
|
+
def display_model_info
|
|
355
|
+
config = RailsConsoleAi.configuration
|
|
356
|
+
model = @engine.effective_model
|
|
357
|
+
thinking = config.resolved_thinking_model
|
|
358
|
+
pricing = Configuration::PRICING[model]
|
|
359
|
+
|
|
360
|
+
@real_stdout.puts "\e[36m Model info:\e[0m"
|
|
361
|
+
@real_stdout.puts "\e[2m Provider: #{config.provider}\e[0m"
|
|
362
|
+
@real_stdout.puts "\e[2m Model: #{model}\e[0m"
|
|
363
|
+
@real_stdout.puts "\e[2m Thinking model: #{thinking}\e[0m"
|
|
364
|
+
@real_stdout.puts "\e[2m Max tokens: #{config.resolved_max_tokens}\e[0m"
|
|
365
|
+
if pricing
|
|
366
|
+
@real_stdout.puts "\e[2m Pricing: $#{pricing[:input] * 1_000_000}/M in, $#{pricing[:output] * 1_000_000}/M out\e[0m"
|
|
367
|
+
if pricing[:cache_read]
|
|
368
|
+
@real_stdout.puts "\e[2m Cache pricing: $#{pricing[:cache_read] * 1_000_000}/M read, $#{pricing[:cache_write] * 1_000_000}/M write\e[0m"
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
@real_stdout.puts "\e[2m Bedrock region: #{config.bedrock_region}\e[0m" if config.provider == :bedrock
|
|
372
|
+
@real_stdout.puts "\e[2m Local URL: #{config.local_url}\e[0m" if config.provider == :local
|
|
373
|
+
end
|
|
374
|
+
|
|
319
375
|
def handle_name_command(input)
|
|
320
376
|
name = input.sub('/name', '').strip.gsub(/\A(['"])(.*)\1\z/, '\2')
|
|
321
377
|
if name.empty?
|
|
@@ -340,7 +396,9 @@ module RailsConsoleAi
|
|
|
340
396
|
@real_stdout.puts "\e[2m /danger Toggle safe mode (currently #{safe_status})\e[0m"
|
|
341
397
|
@real_stdout.puts "\e[2m /safe Show safety guard status\e[0m"
|
|
342
398
|
end
|
|
399
|
+
@real_stdout.puts "\e[2m /model Show provider, model, and pricing info\e[0m"
|
|
343
400
|
@real_stdout.puts "\e[2m /think Switch to thinking model\e[0m"
|
|
401
|
+
@real_stdout.puts "\e[2m /unthink Switch back to default model\e[0m"
|
|
344
402
|
@real_stdout.puts "\e[2m /compact Summarize conversation to reduce context\e[0m"
|
|
345
403
|
@real_stdout.puts "\e[2m /usage Show session token totals\e[0m"
|
|
346
404
|
@real_stdout.puts "\e[2m /cost Show cost estimate by model\e[0m"
|
|
@@ -56,9 +56,16 @@ module RailsConsoleAi
|
|
|
56
56
|
post(":x: #{strip_ansi(text)}")
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
def
|
|
59
|
+
def display_tool_call(text)
|
|
60
|
+
@output_log.write("-> #{text}\n")
|
|
61
|
+
STDOUT.puts "#{@log_prefix} -> #{text}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def display_code(code)
|
|
60
65
|
# Don't post raw code/plan steps to Slack — non-technical users don't need to see Ruby
|
|
61
|
-
|
|
66
|
+
# But do log to STDOUT so server logs show what was generated/executed
|
|
67
|
+
@output_log.write("# Generated code:\n#{code}\n")
|
|
68
|
+
STDOUT.puts "#{@log_prefix} (code)\n# Generated code:\n#{code}"
|
|
62
69
|
end
|
|
63
70
|
|
|
64
71
|
def display_result_output(output)
|
|
@@ -108,6 +115,14 @@ module RailsConsoleAi
|
|
|
108
115
|
|
|
109
116
|
You are responding to non-technical users in Slack. Follow these rules:
|
|
110
117
|
|
|
118
|
+
## Code Execution
|
|
119
|
+
- ALWAYS use the `execute_code` tool to run Ruby code. Do NOT put code in markdown
|
|
120
|
+
code fences expecting it to be executed — code fences are display-only in Slack.
|
|
121
|
+
- Use `execute_code` for simple queries, and `execute_plan` for multi-step operations.
|
|
122
|
+
- If the user asks you to provide code they can run later, put it in a code fence
|
|
123
|
+
in your text response (it will be displayed but not executed).
|
|
124
|
+
|
|
125
|
+
## Formatting
|
|
111
126
|
- Slack does NOT support markdown tables. For tabular data, use `puts` to print
|
|
112
127
|
a plain-text table inside a code block. Use fixed-width columns with padding so
|
|
113
128
|
columns align. Example format:
|
|
@@ -28,7 +28,10 @@ module RailsConsoleAi
|
|
|
28
28
|
:slack_bot_token, :slack_app_token, :slack_channel_ids, :slack_allowed_usernames,
|
|
29
29
|
:local_url, :local_model, :local_api_key,
|
|
30
30
|
:bedrock_region,
|
|
31
|
-
:code_search_paths
|
|
31
|
+
:code_search_paths,
|
|
32
|
+
:channels,
|
|
33
|
+
:bypass_guards_for_methods,
|
|
34
|
+
:user_extra_info
|
|
32
35
|
|
|
33
36
|
def initialize
|
|
34
37
|
@provider = :anthropic
|
|
@@ -58,6 +61,14 @@ module RailsConsoleAi
|
|
|
58
61
|
@local_api_key = nil
|
|
59
62
|
@bedrock_region = nil
|
|
60
63
|
@code_search_paths = %w[app]
|
|
64
|
+
@channels = {}
|
|
65
|
+
@bypass_guards_for_methods = []
|
|
66
|
+
@user_extra_info = {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resolve_user_extra_info(username)
|
|
70
|
+
return nil if @user_extra_info.nil? || @user_extra_info.empty? || username.nil?
|
|
71
|
+
@user_extra_info[username.to_s.downcase]
|
|
61
72
|
end
|
|
62
73
|
|
|
63
74
|
def safety_guards
|
|
@@ -130,10 +130,14 @@ module RailsConsoleAi
|
|
|
130
130
|
nil
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
def
|
|
133
|
+
def ai_db_setup
|
|
134
134
|
RailsConsoleAi.setup!
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
def ai_db_migrate
|
|
138
|
+
RailsConsoleAi.migrate!
|
|
139
|
+
end
|
|
140
|
+
|
|
137
141
|
def ai_init
|
|
138
142
|
require 'rails_console_ai/context_builder'
|
|
139
143
|
require 'rails_console_ai/providers/base'
|
|
@@ -157,7 +161,8 @@ module RailsConsoleAi
|
|
|
157
161
|
$stderr.puts "\e[33m ai_sessions - list recent sessions\e[0m"
|
|
158
162
|
$stderr.puts "\e[33m ai_resume - resume a session by name or id\e[0m"
|
|
159
163
|
$stderr.puts "\e[33m ai_name - name a session: ai_name 42, \"my_label\"\e[0m"
|
|
160
|
-
$stderr.puts "\e[33m
|
|
164
|
+
$stderr.puts "\e[33m ai_db_setup - install session logging table + run migrations\e[0m"
|
|
165
|
+
$stderr.puts "\e[33m ai_db_migrate- run pending session table migrations\e[0m"
|
|
161
166
|
$stderr.puts "\e[33m ai_status - show current configuration\e[0m"
|
|
162
167
|
$stderr.puts "\e[33m ai_memories - show recent memories (ai_memories(n) for last n)\e[0m"
|
|
163
168
|
return nil
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module RailsConsoleAi
|
|
2
2
|
class ContextBuilder
|
|
3
|
-
def initialize(config = RailsConsoleAi.configuration)
|
|
3
|
+
def initialize(config = RailsConsoleAi.configuration, channel_mode: nil, user_name: nil)
|
|
4
4
|
@config = config
|
|
5
|
+
@channel_mode = channel_mode
|
|
6
|
+
@user_name = user_name
|
|
5
7
|
end
|
|
6
8
|
|
|
7
9
|
def build
|
|
@@ -16,6 +18,10 @@ module RailsConsoleAi
|
|
|
16
18
|
parts << smart_system_instructions
|
|
17
19
|
parts << environment_context
|
|
18
20
|
parts << guide_context
|
|
21
|
+
parts << trusted_methods_context
|
|
22
|
+
parts << skills_context
|
|
23
|
+
parts << user_extra_info_context
|
|
24
|
+
parts << pinned_memory_context
|
|
19
25
|
parts << memory_context
|
|
20
26
|
parts.compact.join("\n\n")
|
|
21
27
|
end
|
|
@@ -67,18 +73,27 @@ module RailsConsoleAi
|
|
|
67
73
|
When you use a memory, mention it briefly (e.g. "Based on what I know about sharding...").
|
|
68
74
|
When you discover important patterns about this app, save them as memories.
|
|
69
75
|
|
|
70
|
-
You have
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
You have tools for executing Ruby code:
|
|
77
|
+
- Use execute_code for simple queries and single operations.
|
|
78
|
+
- Use execute_plan for multi-step tasks that require sequential operations. Each step
|
|
79
|
+
has a description and Ruby code. The plan is shown to the user for review before
|
|
80
|
+
execution begins. After each step runs, its return value is stored as step1, step2,
|
|
81
|
+
etc. — use these variables in later steps to reference earlier results
|
|
82
|
+
(e.g. `api = SalesforceApi.new(step1)`).
|
|
83
|
+
- If the user asks you to provide code for them to run later (not execute now), put it
|
|
84
|
+
in a ```ruby code block in your text response.
|
|
85
|
+
- Use `RailsConsoleAi.configuration.safety_guards.without_guards { }` to wrap any
|
|
86
|
+
operation that should bypass safety guards (e.g. calling a known-safe admin method).
|
|
87
|
+
|
|
88
|
+
You have skills — predefined procedures for specific operations. When a user's request
|
|
89
|
+
matches a skill, call activate_skill first to load the recipe and enable its guard
|
|
90
|
+
bypasses, then follow the recipe.
|
|
76
91
|
|
|
77
92
|
RULES:
|
|
78
93
|
- Give ONE concise answer. Do not offer multiple alternatives or variations.
|
|
79
94
|
- For multi-step tasks, use execute_plan to break the work into small, clear steps.
|
|
80
|
-
- For simple queries,
|
|
81
|
-
- Include a brief one-line explanation before
|
|
95
|
+
- For simple queries, use the execute_code tool.
|
|
96
|
+
- Include a brief one-line explanation before or after executing code.
|
|
82
97
|
- Use the app's actual model names, associations, and schema.
|
|
83
98
|
- Prefer ActiveRecord query interface over raw SQL.
|
|
84
99
|
- For destructive operations, add a comment warning.
|
|
@@ -89,6 +104,19 @@ module RailsConsoleAi
|
|
|
89
104
|
PROMPT
|
|
90
105
|
end
|
|
91
106
|
|
|
107
|
+
def trusted_methods_context
|
|
108
|
+
methods = Array(@config.bypass_guards_for_methods)
|
|
109
|
+
if @channel_mode
|
|
110
|
+
channel_cfg = @config.channels[@channel_mode] || {}
|
|
111
|
+
methods = methods | Array(channel_cfg['bypass_guards_for_methods'])
|
|
112
|
+
end
|
|
113
|
+
return nil if methods.empty?
|
|
114
|
+
|
|
115
|
+
lines = ["## Trusted Methods (safety guards bypassed automatically)"]
|
|
116
|
+
methods.each { |m| lines << "- #{m}" }
|
|
117
|
+
lines.join("\n")
|
|
118
|
+
end
|
|
119
|
+
|
|
92
120
|
def guide_context
|
|
93
121
|
content = RailsConsoleAi.storage.read(RailsConsoleAi::GUIDE_KEY)
|
|
94
122
|
return nil if content.nil? || content.strip.empty?
|
|
@@ -99,6 +127,48 @@ module RailsConsoleAi
|
|
|
99
127
|
nil
|
|
100
128
|
end
|
|
101
129
|
|
|
130
|
+
def skills_context
|
|
131
|
+
require 'rails_console_ai/skill_loader'
|
|
132
|
+
summaries = RailsConsoleAi::SkillLoader.new.skill_summaries
|
|
133
|
+
return nil if summaries.nil? || summaries.empty?
|
|
134
|
+
|
|
135
|
+
lines = ["## Skills (call activate_skill to use)"]
|
|
136
|
+
lines.concat(summaries)
|
|
137
|
+
lines.join("\n")
|
|
138
|
+
rescue => e
|
|
139
|
+
RailsConsoleAi.logger.debug("RailsConsoleAi: skills context failed: #{e.message}")
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def user_extra_info_context
|
|
144
|
+
info = @config.resolve_user_extra_info(@user_name)
|
|
145
|
+
return nil if info.nil? || info.strip.empty?
|
|
146
|
+
|
|
147
|
+
"## Current User\n\nUser: #{@user_name}\n#{info}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def pinned_memory_context
|
|
151
|
+
return nil unless @channel_mode
|
|
152
|
+
|
|
153
|
+
channel_cfg = @config.channels[@channel_mode] || {}
|
|
154
|
+
pinned_tags = channel_cfg['pinned_memory_tags'] || []
|
|
155
|
+
return nil if pinned_tags.empty?
|
|
156
|
+
|
|
157
|
+
require 'rails_console_ai/tools/memory_tools'
|
|
158
|
+
sections = pinned_tags.filter_map do |tag|
|
|
159
|
+
content = Tools::MemoryTools.new.recall_memories(tag: tag)
|
|
160
|
+
next if content.nil? || content.include?("No memories")
|
|
161
|
+
content
|
|
162
|
+
end
|
|
163
|
+
return nil if sections.empty?
|
|
164
|
+
|
|
165
|
+
"## Pinned Memories (always available — no need to recall_memories for these)\n\n" \
|
|
166
|
+
+ sections.join("\n\n")
|
|
167
|
+
rescue => e
|
|
168
|
+
RailsConsoleAi.logger.debug("RailsConsoleAi: pinned memory context failed: #{e.message}")
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
102
172
|
def memory_context
|
|
103
173
|
return nil unless @config.memories_enabled
|
|
104
174
|
|