console_agent 0.9.0 → 0.11.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 +26 -0
- data/README.md +104 -2
- 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 +73 -5
- data/lib/console_agent/conversation_engine.rb +1122 -0
- data/lib/console_agent/executor.rb +257 -44
- data/lib/console_agent/providers/base.rb +23 -15
- data/lib/console_agent/providers/local.rb +112 -0
- data/lib/console_agent/railtie.rb +4 -0
- data/lib/console_agent/repl.rb +27 -1128
- 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 +465 -0
- data/lib/console_agent/tools/registry.rb +66 -16
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +17 -3
- data/lib/generators/console_agent/templates/initializer.rb +30 -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: 902a34c15b9fa06e60e4b8eb040d57126bbdf9f725d2e727634ed4e73d8fbaa0
|
|
4
|
+
data.tar.gz: cda463c1023ad995a8143d52702a6b8098dbcdd3cde4e2002352c327a536109e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6380481f86f277205aa2aadac23745cf7f5fe5785b8f5ce8272c64fd26206166814434f06bbcf97494f6f1d75a08e69657721fd5423e62898a7c1e5d7312347f
|
|
7
|
+
data.tar.gz: adffc1e8afa7ad60469c03df75427284c988c8b1e64f954571435441397e66cd3d597906feb2fa953e19575432b27b7583bfcea0e73bc4cbc49cdd19e32bc2d0
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.11.0]
|
|
6
|
+
|
|
7
|
+
- Add Slack channel integration with system instructions and connection pooling
|
|
8
|
+
- Extract channels abstraction and ConversationEngine from Repl
|
|
9
|
+
- Add built-in safety guards with `/danger` bypass and progressive safety allowlists
|
|
10
|
+
- Add local model support with prompt truncation warnings
|
|
11
|
+
- Add `clear!` command to clear bot messages in thread
|
|
12
|
+
- Match code blocks in LLM results
|
|
13
|
+
- Fix long query display and add cost tracking to session viewer
|
|
14
|
+
- Strip quotes from session names when saving
|
|
15
|
+
|
|
16
|
+
## [0.10.0]
|
|
17
|
+
|
|
18
|
+
- Add `/expand` command to view previous results
|
|
19
|
+
- Exclude previous output from context; add tool for LLM to retrieve it on demand
|
|
20
|
+
- Show summarized info per LLM call in `/debug`
|
|
21
|
+
|
|
22
|
+
## [0.9.0]
|
|
23
|
+
|
|
24
|
+
- Add `/system` and `/context` commands to inspect what is being sent
|
|
25
|
+
- Omit huge output from tool results
|
|
26
|
+
- Don't cancel code execution on incorrect prompt answers
|
|
27
|
+
- Preserve code blocks when compacting; require manual `/compact`
|
|
28
|
+
- Fix authentication when neither method was applied
|
|
29
|
+
- Remove prompt to upgrade model on excessive tool calls
|
|
30
|
+
|
|
5
31
|
## [0.8.0]
|
|
6
32
|
|
|
7
33
|
- Add authentication function support so host apps can avoid using basic auth
|
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,11 +75,16 @@ 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 |
|
|
81
83
|
| `/think` | Upgrade to thinking model (Opus) for the rest of the session |
|
|
82
|
-
| `/debug` | Toggle
|
|
84
|
+
| `/debug` | Toggle debug summaries (context stats, cost per call) |
|
|
85
|
+
| `/expand <id>` | Show full omitted output |
|
|
86
|
+
| `/context` | Show conversation history as sent to the LLM |
|
|
87
|
+
| `/system` | Show the system prompt |
|
|
83
88
|
| `/name <label>` | Name the session for easy resume |
|
|
84
89
|
|
|
85
90
|
Prefix input with `>` to run Ruby directly (no LLM round-trip). The result is added to conversation context.
|
|
@@ -96,6 +101,51 @@ Say "think harder" in any query to auto-upgrade to the thinking model for that s
|
|
|
96
101
|
- **App guide** — `ai_init` generates a guide injected into every system prompt
|
|
97
102
|
- **Sessions** — name, list, and resume interactive conversations (`ai_setup` to enable)
|
|
98
103
|
- **History compaction** — `/compact` summarizes long conversations to reduce cost and latency
|
|
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
|
|
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]`
|
|
99
149
|
|
|
100
150
|
## Configuration
|
|
101
151
|
|
|
@@ -141,6 +191,58 @@ end
|
|
|
141
191
|
|
|
142
192
|
When `authenticate` is set, `admin_username` / `admin_password` are ignored.
|
|
143
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
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Running
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
bundle exec rake console_agent:slack
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
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.
|
|
245
|
+
|
|
144
246
|
## Requirements
|
|
145
247
|
|
|
146
248
|
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
|