console_agent 0.10.0 → 0.12.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 +15 -0
- data/README.md +101 -1
- data/app/helpers/console_agent/sessions_helper.rb +14 -0
- data/app/models/console_agent/session.rb +1 -1
- data/app/views/console_agent/sessions/index.html.erb +4 -4
- data/app/views/console_agent/sessions/show.html.erb +16 -6
- data/app/views/layouts/console_agent/application.html.erb +1 -0
- data/lib/console_agent/channel/base.rb +23 -0
- data/lib/console_agent/channel/console.rb +457 -0
- data/lib/console_agent/channel/slack.rb +182 -0
- data/lib/console_agent/configuration.rb +74 -5
- data/lib/console_agent/conversation_engine.rb +1122 -0
- data/lib/console_agent/executor.rb +239 -47
- data/lib/console_agent/providers/base.rb +7 -2
- data/lib/console_agent/providers/local.rb +112 -0
- data/lib/console_agent/railtie.rb +4 -0
- data/lib/console_agent/repl.rb +26 -1291
- data/lib/console_agent/safety_guards.rb +207 -0
- data/lib/console_agent/session_logger.rb +14 -3
- data/lib/console_agent/slack_bot.rb +473 -0
- data/lib/console_agent/tools/registry.rb +48 -16
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +17 -3
- data/lib/generators/console_agent/templates/initializer.rb +34 -1
- data/lib/tasks/console_agent.rake +7 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4605d79e21eecd9bdcc4d48ffee20e04f3881dbe285201887f997aefaefd13e6
|
|
4
|
+
data.tar.gz: f6b0f23b20d640d9f0e8d266f41a9c88dc6e1eb76c2d5185a5f05d30dae17890
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28c4cb1aeef7d423f014c1fab6737f7dee89cd68a2a38380b708312c16d2f7c069d645078c013fc770fae005d8eb7e4bd118120aca0aac3eb7a22bafedecb995
|
|
7
|
+
data.tar.gz: 42b4f79d3d649a985c68d694fd390cc97f34982f5c4ea6c790845336e8723ab646eff7f814c56eed4a21c1946db8671b71564ffab25d609796a4f6595a0da5ed
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.12.0]
|
|
6
|
+
|
|
7
|
+
- Add `slack_allowed_usernames` to restrict Slack channel access
|
|
8
|
+
|
|
9
|
+
## [0.11.0]
|
|
10
|
+
|
|
11
|
+
- Add Slack channel integration with system instructions and connection pooling
|
|
12
|
+
- Extract channels abstraction and ConversationEngine from Repl
|
|
13
|
+
- Add built-in safety guards with `/danger` bypass and progressive safety allowlists
|
|
14
|
+
- Add local model support with prompt truncation warnings
|
|
15
|
+
- Add `clear!` command to clear bot messages in thread
|
|
16
|
+
- Match code blocks in LLM results
|
|
17
|
+
- Fix long query display and add cost tracking to session viewer
|
|
18
|
+
- Strip quotes from session names when saving
|
|
19
|
+
|
|
5
20
|
## [0.10.0]
|
|
6
21
|
|
|
7
22
|
- Add `/expand` command to view previous results
|
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/edit] y
|
|
15
|
+
Execute? [y/N/edit/danger] y
|
|
16
16
|
=> [#<Order id: 4821, ...>, ...]
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -75,6 +75,8 @@ end
|
|
|
75
75
|
| Command | What it does |
|
|
76
76
|
|---------|-------------|
|
|
77
77
|
| `/auto` | Toggle auto-execute (skip confirmations) |
|
|
78
|
+
| `/danger` | Toggle safe mode off/on (allow side effects) |
|
|
79
|
+
| `/safe` | Show safety guard status |
|
|
78
80
|
| `/compact` | Compress history into a summary (saves tokens) |
|
|
79
81
|
| `/usage` | Show token stats |
|
|
80
82
|
| `/cost` | Show per-model cost breakdown |
|
|
@@ -101,6 +103,49 @@ Say "think harder" in any query to auto-upgrade to the thinking model for that s
|
|
|
101
103
|
- **History compaction** — `/compact` summarizes long conversations to reduce cost and latency
|
|
102
104
|
- **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
|
|
103
105
|
- **Debug mode** — `/debug` shows context breakdown, token counts, and per-call cost estimates before and after each LLM call
|
|
106
|
+
- **Safe mode** — configurable guards that block side effects (DB writes, HTTP mutations, email delivery) during AI code execution
|
|
107
|
+
|
|
108
|
+
## Safety Guards
|
|
109
|
+
|
|
110
|
+
Safety guards prevent AI-generated code from causing side effects. When a guard blocks an operation, the user is prompted to re-run with safe mode disabled.
|
|
111
|
+
|
|
112
|
+
### Built-in Guards
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
ConsoleAgent.configure do |config|
|
|
116
|
+
config.use_builtin_safety_guard :database_writes # blocks INSERT/UPDATE/DELETE/DROP/etc.
|
|
117
|
+
config.use_builtin_safety_guard :http_mutations # blocks POST/PUT/PATCH/DELETE via Net::HTTP
|
|
118
|
+
config.use_builtin_safety_guard :mailers # disables ActionMailer delivery
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **`:database_writes`** — intercepts the ActiveRecord connection adapter to block write SQL. Works on Rails 5+ with any database adapter.
|
|
123
|
+
- **`:http_mutations`** — intercepts `Net::HTTP#request` to block non-GET/HEAD/OPTIONS requests. Covers libraries built on Net::HTTP (HTTParty, RestClient, Faraday).
|
|
124
|
+
- **`:mailers`** — sets `ActionMailer::Base.perform_deliveries = false` during execution.
|
|
125
|
+
|
|
126
|
+
### Custom Guards
|
|
127
|
+
|
|
128
|
+
Write your own guards using the around-block pattern:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
ConsoleAgent.configure do |config|
|
|
132
|
+
config.safety_guard :jobs do |&execute|
|
|
133
|
+
Sidekiq::Testing.fake! { execute.call }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Raise `ConsoleAgent::SafetyError` in your app code to trigger the safe mode prompt:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
raise ConsoleAgent::SafetyError, "Stripe charge blocked"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Toggling Safe Mode
|
|
145
|
+
|
|
146
|
+
- **`/danger`** in interactive mode toggles all guards off/on for the session
|
|
147
|
+
- **`d`** at the `Execute? [y/N/edit/danger]` prompt disables guards for that single execution
|
|
148
|
+
- When a guard blocks an operation, the user is prompted: `Re-run with safe mode disabled? [y/N]`
|
|
104
149
|
|
|
105
150
|
## Configuration
|
|
106
151
|
|
|
@@ -146,6 +191,61 @@ end
|
|
|
146
191
|
|
|
147
192
|
When `authenticate` is set, `admin_username` / `admin_password` are ignored.
|
|
148
193
|
|
|
194
|
+
## Channels
|
|
195
|
+
|
|
196
|
+
ConsoleAgent can run through different channels beyond the Rails console. Each channel is a separate process that connects the same AI engine to a different interface.
|
|
197
|
+
|
|
198
|
+
### Slack
|
|
199
|
+
|
|
200
|
+
Run ConsoleAgent as a Slack bot. Each Slack thread becomes an independent AI session with full tool use, multi-step plans, and safety guards always on.
|
|
201
|
+
|
|
202
|
+
#### Slack App Setup
|
|
203
|
+
|
|
204
|
+
1. Create a new app at https://api.slack.com/apps → **Create New App** → **From scratch**
|
|
205
|
+
|
|
206
|
+
2. **Enable Socket Mode** — Settings → Socket Mode → toggle ON. Generate an App-Level Token with the `connections:write` scope. Copy the `xapp-...` token.
|
|
207
|
+
|
|
208
|
+
3. **Bot Token Scopes** — OAuth & Permissions → Bot Token Scopes, add:
|
|
209
|
+
- `chat:write`
|
|
210
|
+
- `channels:history` (public channels)
|
|
211
|
+
- `groups:history` (private channels, optional)
|
|
212
|
+
- `im:history` (direct messages)
|
|
213
|
+
- `users:read`
|
|
214
|
+
|
|
215
|
+
4. **Event Subscriptions** — Event Subscriptions → toggle ON, then under "Subscribe to bot events" add:
|
|
216
|
+
- `message.channels` (public channels)
|
|
217
|
+
- `message.groups` (private channels, optional)
|
|
218
|
+
- `message.im` (direct messages)
|
|
219
|
+
|
|
220
|
+
5. **App Home** — Show Tabs → toggle **Messages Tab** ON and check **"Allow users to send Slash commands and messages from the messages tab"**
|
|
221
|
+
|
|
222
|
+
6. **Install to workspace** — Install App → Install to Workspace. Copy the `xoxb-...` Bot User OAuth Token.
|
|
223
|
+
|
|
224
|
+
7. **Invite the bot** to a channel with `/invite @YourBotName`, or DM it directly.
|
|
225
|
+
|
|
226
|
+
#### Configuration
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
ConsoleAgent.configure do |config|
|
|
230
|
+
config.slack_bot_token = ENV['SLACK_BOT_TOKEN'] # xoxb-...
|
|
231
|
+
config.slack_app_token = ENV['SLACK_APP_TOKEN'] # xapp-...
|
|
232
|
+
|
|
233
|
+
# Optional: restrict to specific Slack channel IDs
|
|
234
|
+
# config.slack_channel_ids = 'C1234567890,C0987654321'
|
|
235
|
+
|
|
236
|
+
# Required: which users the bot responds to (by display name)
|
|
237
|
+
config.slack_allowed_usernames = ['alice', 'bob'] # or 'ALL' for everyone
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Running
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
bundle exec rake console_agent:slack
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
This starts a long-running process (run it separately from your web server). Each new message creates a session; threaded replies continue the conversation. The bot auto-executes code with safety guards always enabled — there is no `/danger` equivalent in Slack.
|
|
248
|
+
|
|
149
249
|
## Requirements
|
|
150
250
|
|
|
151
251
|
Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
module ConsoleAgent
|
|
2
2
|
module SessionsHelper
|
|
3
|
+
def estimated_cost(session)
|
|
4
|
+
pricing = Configuration::PRICING[session.model]
|
|
5
|
+
return nil unless pricing
|
|
6
|
+
|
|
7
|
+
(session.input_tokens * pricing[:input]) + (session.output_tokens * pricing[:output])
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def format_cost(session)
|
|
11
|
+
cost = estimated_cost(session)
|
|
12
|
+
return '-' unless cost
|
|
13
|
+
|
|
14
|
+
cost < 0.01 ? "<$0.01" : "$#{'%.2f' % cost}"
|
|
15
|
+
end
|
|
16
|
+
|
|
3
17
|
# Convert ANSI escape codes to HTML spans for terminal-style rendering
|
|
4
18
|
def ansi_to_html(text)
|
|
5
19
|
return '' if text.nil? || text.empty?
|
|
@@ -4,7 +4,7 @@ module ConsoleAgent
|
|
|
4
4
|
|
|
5
5
|
validates :query, presence: true
|
|
6
6
|
validates :conversation, presence: true
|
|
7
|
-
validates :mode, presence: true, inclusion: { in: %w[one_shot interactive explain] }
|
|
7
|
+
validates :mode, presence: true, inclusion: { in: %w[one_shot interactive explain slack] }
|
|
8
8
|
|
|
9
9
|
scope :recent, -> { order(created_at: :desc) }
|
|
10
10
|
scope :named, ->(name) { where(name: name) }
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
<th>Time</th>
|
|
15
15
|
<th>User</th>
|
|
16
16
|
<th>Name</th>
|
|
17
|
-
<th>Query</th>
|
|
17
|
+
<th style="max-width: 400px;">Query</th>
|
|
18
18
|
<th>Mode</th>
|
|
19
19
|
<th>Tokens</th>
|
|
20
|
-
<th>
|
|
20
|
+
<th>Cost</th>
|
|
21
21
|
<th>Duration</th>
|
|
22
22
|
</tr>
|
|
23
23
|
</thead>
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
<td class="mono"><%= session.created_at.strftime('%Y-%m-%d %H:%M') %></td>
|
|
28
28
|
<td><%= session.user_name %></td>
|
|
29
29
|
<td><%= session.name.present? ? session.name : '-' %></td>
|
|
30
|
-
<td><a href="<%= console_agent.session_path(session) %>"><%= truncate(session.query, length: 80) %></a></td>
|
|
30
|
+
<td class="query-cell"><a href="<%= console_agent.session_path(session) %>" title="<%= h session.query.truncate(200) %>"><%= truncate(session.query.gsub(/\s+/, ' ').strip, length: 80) %></a></td>
|
|
31
31
|
<td><span class="badge badge-<%= session.mode %>"><%= session.mode %></span></td>
|
|
32
32
|
<td class="mono"><%= session.input_tokens + session.output_tokens %></td>
|
|
33
|
-
<td
|
|
33
|
+
<td class="mono"><%= format_cost(session) %></td>
|
|
34
34
|
<td class="mono"><%= session.duration_ms ? "#{session.duration_ms}ms" : '-' %></td>
|
|
35
35
|
</tr>
|
|
36
36
|
<% end %>
|
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
<div class="meta-card">
|
|
4
4
|
<div class="meta-grid">
|
|
5
|
-
<div class="meta-item">
|
|
5
|
+
<div class="meta-item" style="grid-column: 1 / -1;">
|
|
6
6
|
<label>Query</label>
|
|
7
|
-
|
|
7
|
+
<% if @session.query.length > 300 %>
|
|
8
|
+
<div class="query-preview">
|
|
9
|
+
<span><%= truncate(@session.query, length: 300) %></span>
|
|
10
|
+
<details style="margin-top: 4px;">
|
|
11
|
+
<summary style="cursor: pointer; font-size: 12px; color: #4a6fa5;">Show full query</summary>
|
|
12
|
+
<pre style="white-space: pre-wrap; word-wrap: break-word; font-size: 13px; margin-top: 8px; padding: 12px; background: #f8f9fa; border-radius: 4px; max-height: 400px; overflow-y: auto;"><%= @session.query %></pre>
|
|
13
|
+
</details>
|
|
14
|
+
</div>
|
|
15
|
+
<% else %>
|
|
16
|
+
<span><%= @session.query %></span>
|
|
17
|
+
<% end %>
|
|
8
18
|
</div>
|
|
9
19
|
<% if @session.name.present? %>
|
|
10
20
|
<div class="meta-item">
|
|
@@ -29,12 +39,12 @@
|
|
|
29
39
|
<span class="mono"><%= @session.input_tokens %> / <%= @session.output_tokens %></span>
|
|
30
40
|
</div>
|
|
31
41
|
<div class="meta-item">
|
|
32
|
-
<label>
|
|
33
|
-
<span class="mono"><%= @session
|
|
42
|
+
<label>Est. Cost</label>
|
|
43
|
+
<span class="mono"><%= format_cost(@session) %></span>
|
|
34
44
|
</div>
|
|
35
45
|
<div class="meta-item">
|
|
36
|
-
<label>
|
|
37
|
-
<span><%= @session.
|
|
46
|
+
<label>Duration</label>
|
|
47
|
+
<span class="mono"><%= @session.duration_ms ? "#{@session.duration_ms}ms" : '-' %></span>
|
|
38
48
|
</div>
|
|
39
49
|
<div class="meta-item">
|
|
40
50
|
<label>Time</label>
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;
|
|
55
55
|
}
|
|
56
56
|
.mono { font-family: "SF Mono", "Monaco", "Menlo", "Consolas", monospace; font-size: 13px; }
|
|
57
|
+
.query-cell { max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
58
|
.text-muted { color: #888; }
|
|
58
59
|
.back-link { margin-bottom: 16px; display: inline-block; }
|
|
59
60
|
.check { color: #28a745; }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module ConsoleAgent
|
|
2
|
+
module Channel
|
|
3
|
+
class Base
|
|
4
|
+
def display(text); raise NotImplementedError; end
|
|
5
|
+
def display_dim(text); raise NotImplementedError; end
|
|
6
|
+
def display_warning(text); raise NotImplementedError; end
|
|
7
|
+
def display_error(text); raise NotImplementedError; end
|
|
8
|
+
def display_code(code); raise NotImplementedError; end
|
|
9
|
+
def display_result(text); raise NotImplementedError; end
|
|
10
|
+
def display_result_output(text); end # stdout output from code execution
|
|
11
|
+
def prompt(text); raise NotImplementedError; end
|
|
12
|
+
def confirm(text); raise NotImplementedError; end
|
|
13
|
+
def user_identity; raise NotImplementedError; end
|
|
14
|
+
def mode; raise NotImplementedError; end
|
|
15
|
+
def cancelled?; false; end
|
|
16
|
+
def supports_danger?; true; end
|
|
17
|
+
def supports_editing?; false; end
|
|
18
|
+
def edit_code(code); code; end
|
|
19
|
+
def wrap_llm_call(&block); yield; end
|
|
20
|
+
def system_instructions; nil; end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|