rails_console_ai 0.16.0 → 0.18.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 +16 -0
- data/lib/rails_console_ai/channel/console.rb +9 -0
- data/lib/rails_console_ai/channel/slack.rb +3 -3
- data/lib/rails_console_ai/conversation_engine.rb +10 -0
- data/lib/rails_console_ai/executor.rb +15 -0
- data/lib/rails_console_ai/prefixed_io.rb +50 -0
- data/lib/rails_console_ai/providers/bedrock.rb +13 -1
- data/lib/rails_console_ai/safety_guards.rb +11 -0
- data/lib/rails_console_ai/slack_bot.rb +132 -2
- data/lib/rails_console_ai/version.rb +1 -1
- 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: f6fc753a055c35cdf2aec7ffa98dff0cf5b0dd649e9fd44048d5f43577a69a9a
|
|
4
|
+
data.tar.gz: c9ce948130dae8a58ee8eaaa6d348f00369a9c2f63cb253a9728323493df04e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c20f24123aafbef6c1e2dd218ba47f63c3773e0bae332081422aba1f52a821bf5e94baf09a5db13919e61a12cdb40124de139739a352fb9b8486f3cf3de9be77
|
|
7
|
+
data.tar.gz: 62808844bd51a0a5f3aadae221d2a9641b47dc497a548c0edbc1ccda247eaf0e24785920621850b387165d20698f9ccf77aa1f14b59f9432f0c9dcc17c034aac
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.18.0]
|
|
6
|
+
|
|
7
|
+
- Handle "smart" quotes coming from Slack
|
|
8
|
+
- Eager load when Slack bot starts up
|
|
9
|
+
- Handle `>` in Slack for direct code execution
|
|
10
|
+
- Handle stopping of sessions in Slack bot by recording the stop
|
|
11
|
+
|
|
12
|
+
## [0.17.0]
|
|
13
|
+
|
|
14
|
+
- Add `/retry` command
|
|
15
|
+
- Print provider information when `ai!` starts
|
|
16
|
+
- Keep Slack bot alive during long-running sessions
|
|
17
|
+
- Improve Slack bot log prefixes for production log search
|
|
18
|
+
- Catch safety errors even when swallowed by executed code
|
|
19
|
+
- Fix Bedrock handling of multiple tool results
|
|
20
|
+
|
|
5
21
|
## [0.16.0]
|
|
6
22
|
|
|
7
23
|
- Run migrations during setup
|
|
@@ -158,6 +158,8 @@ module RailsConsoleAi
|
|
|
158
158
|
guards = RailsConsoleAi.configuration.safety_guards
|
|
159
159
|
name_display = @engine.session_name ? " (#{@engine.session_name})" : ""
|
|
160
160
|
@real_stdout.puts "\e[36mRailsConsoleAi interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
|
|
161
|
+
config = RailsConsoleAi.configuration
|
|
162
|
+
@real_stdout.puts "\e[2m Provider: #{config.provider} | Model: #{config.resolved_model}\e[0m"
|
|
161
163
|
safe_info = guards.empty? ? '' : " | Safe mode: #{guards.enabled? ? 'ON' : 'OFF'} (/danger to toggle)"
|
|
162
164
|
@real_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle)#{safe_info} | > code | /usage | /cost | /compact | /think | /name <label>\e[0m"
|
|
163
165
|
|
|
@@ -259,6 +261,8 @@ module RailsConsoleAi
|
|
|
259
261
|
else
|
|
260
262
|
@real_stdout.puts "\e[33mNo omitted output with id #{expand_id}\e[0m"
|
|
261
263
|
end
|
|
264
|
+
when '/retry'
|
|
265
|
+
retry_last_code
|
|
262
266
|
when /\A\/name/
|
|
263
267
|
handle_name_command(input)
|
|
264
268
|
else
|
|
@@ -267,6 +271,10 @@ module RailsConsoleAi
|
|
|
267
271
|
true
|
|
268
272
|
end
|
|
269
273
|
|
|
274
|
+
def retry_last_code
|
|
275
|
+
@engine.retry_last_code
|
|
276
|
+
end
|
|
277
|
+
|
|
270
278
|
def handle_direct_execution(input)
|
|
271
279
|
raw_code = input.sub(/\A>\s?/, '')
|
|
272
280
|
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
@@ -341,6 +349,7 @@ module RailsConsoleAi
|
|
|
341
349
|
@real_stdout.puts "\e[2m /system Show the system prompt\e[0m"
|
|
342
350
|
@real_stdout.puts "\e[2m /expand <id> Show full omitted output\e[0m"
|
|
343
351
|
@real_stdout.puts "\e[2m /debug Toggle debug summaries (context stats, cost per call)\e[0m"
|
|
352
|
+
@real_stdout.puts "\e[2m /retry Re-execute the last code block\e[0m"
|
|
344
353
|
@real_stdout.puts "\e[2m > code Execute Ruby directly (skip LLM)\e[0m"
|
|
345
354
|
@real_stdout.puts "\e[2m exit/quit Leave interactive mode\e[0m"
|
|
346
355
|
end
|
|
@@ -37,14 +37,14 @@ module RailsConsoleAi
|
|
|
37
37
|
elsif stripped =~ /\ACalling LLM/
|
|
38
38
|
# Technical LLM round status — suppress in Slack
|
|
39
39
|
@output_log.write("#{stripped}\n")
|
|
40
|
-
|
|
40
|
+
STDOUT.puts "#{@log_prefix} (dim) #{stripped}"
|
|
41
41
|
elsif raw =~ /\A {2,4}\S/ && stripped.length > 10
|
|
42
42
|
# LLM thinking text (2-space indent from conversation engine) — show as status
|
|
43
43
|
post(stripped)
|
|
44
44
|
else
|
|
45
45
|
# Tool result previews (5+ space indent) and other technical noise — log only
|
|
46
46
|
@output_log.write("#{stripped}\n")
|
|
47
|
-
|
|
47
|
+
STDOUT.puts "#{@log_prefix} (dim) #{stripped}"
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -150,7 +150,7 @@ module RailsConsoleAi
|
|
|
150
150
|
def post(text)
|
|
151
151
|
return if text.nil? || text.strip.empty?
|
|
152
152
|
@output_log.write("#{text}\n")
|
|
153
|
-
|
|
153
|
+
STDOUT.puts "#{@log_prefix} >> #{text}"
|
|
154
154
|
@slack_bot.send(:post_message,
|
|
155
155
|
channel: @channel_id,
|
|
156
156
|
thread_ts: @thread_ts,
|
|
@@ -221,6 +221,16 @@ module RailsConsoleAi
|
|
|
221
221
|
end
|
|
222
222
|
end
|
|
223
223
|
|
|
224
|
+
def retry_last_code
|
|
225
|
+
code = @last_interactive_code
|
|
226
|
+
unless code
|
|
227
|
+
@channel.display_warning("No code to retry.")
|
|
228
|
+
return
|
|
229
|
+
end
|
|
230
|
+
@channel.display_dim(" Retrying last code...")
|
|
231
|
+
execute_direct(code)
|
|
232
|
+
end
|
|
233
|
+
|
|
224
234
|
def execute_direct(raw_code)
|
|
225
235
|
exec_result = @executor.execute(raw_code)
|
|
226
236
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'stringio'
|
|
2
|
+
require_relative 'safety_guards'
|
|
2
3
|
|
|
3
4
|
module RailsConsoleAi
|
|
4
5
|
# Writes to two IO streams simultaneously
|
|
@@ -97,12 +98,25 @@ module RailsConsoleAi
|
|
|
97
98
|
# Tee output: capture it and also print to the real stdout
|
|
98
99
|
$stdout = TeeIO.new(old_stdout, captured_output)
|
|
99
100
|
|
|
101
|
+
RailsConsoleAi::SafetyError.clear!
|
|
102
|
+
|
|
100
103
|
result = with_safety_guards do
|
|
101
104
|
binding_context.eval(code, "(rails_console_ai)", 1)
|
|
102
105
|
end
|
|
103
106
|
|
|
104
107
|
$stdout = old_stdout
|
|
105
108
|
|
|
109
|
+
# Check if a SafetyError was raised but swallowed by a rescue inside the eval'd code
|
|
110
|
+
if (swallowed = RailsConsoleAi::SafetyError.last_raised)
|
|
111
|
+
RailsConsoleAi::SafetyError.clear!
|
|
112
|
+
@last_error = "SafetyError: #{swallowed.message}"
|
|
113
|
+
@last_safety_error = true
|
|
114
|
+
@last_safety_exception = swallowed
|
|
115
|
+
display_error("Blocked: #{swallowed.message}")
|
|
116
|
+
@last_output = captured_output&.string
|
|
117
|
+
return nil
|
|
118
|
+
end
|
|
119
|
+
|
|
106
120
|
# Send captured puts output through channel before the return value
|
|
107
121
|
if @channel && !captured_output.string.empty?
|
|
108
122
|
@channel.display_result_output(captured_output.string)
|
|
@@ -114,6 +128,7 @@ module RailsConsoleAi
|
|
|
114
128
|
result
|
|
115
129
|
rescue RailsConsoleAi::SafetyError => e
|
|
116
130
|
$stdout = old_stdout if old_stdout
|
|
131
|
+
RailsConsoleAi::SafetyError.clear!
|
|
117
132
|
@last_error = "SafetyError: #{e.message}"
|
|
118
133
|
@last_safety_error = true
|
|
119
134
|
@last_safety_exception = e
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module RailsConsoleAi
|
|
2
|
+
class PrefixedIO
|
|
3
|
+
def initialize(io)
|
|
4
|
+
@io = io
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def write(str)
|
|
8
|
+
prefix = Thread.current[:log_prefix]
|
|
9
|
+
if prefix && str.is_a?(String) && !str.strip.empty?
|
|
10
|
+
prefixed = str.gsub(/^(?=.)/, "#{prefix} ")
|
|
11
|
+
@io.write(prefixed)
|
|
12
|
+
else
|
|
13
|
+
@io.write(str)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def puts(*args)
|
|
18
|
+
prefix = Thread.current[:log_prefix]
|
|
19
|
+
if prefix
|
|
20
|
+
args = [""] if args.empty?
|
|
21
|
+
args.each do |a|
|
|
22
|
+
line = a.to_s
|
|
23
|
+
if line.strip.empty?
|
|
24
|
+
@io.write("\n")
|
|
25
|
+
else
|
|
26
|
+
@io.write("#{prefix} #{line}\n")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
@io.puts(*args)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def print(*args)
|
|
35
|
+
@io.print(*args)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def flush
|
|
39
|
+
@io.flush
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def respond_to_missing?(method, include_private = false)
|
|
43
|
+
@io.respond_to?(method, include_private) || super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def method_missing(method, *args, &block)
|
|
47
|
+
@io.send(method, *args, &block)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -115,7 +115,7 @@ module RailsConsoleAi
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def format_messages(messages)
|
|
118
|
-
messages.map do |msg|
|
|
118
|
+
formatted = messages.map do |msg|
|
|
119
119
|
content = if msg[:content].is_a?(Array)
|
|
120
120
|
msg[:content]
|
|
121
121
|
else
|
|
@@ -123,6 +123,18 @@ module RailsConsoleAi
|
|
|
123
123
|
end
|
|
124
124
|
{ role: msg[:role].to_s, content: content }
|
|
125
125
|
end
|
|
126
|
+
|
|
127
|
+
# Bedrock requires all tool_result blocks for a single assistant turn
|
|
128
|
+
# to be in one user message. Merge consecutive same-role messages.
|
|
129
|
+
merged = []
|
|
130
|
+
formatted.each do |msg|
|
|
131
|
+
if merged.last && merged.last[:role] == msg[:role]
|
|
132
|
+
merged.last[:content].concat(msg[:content])
|
|
133
|
+
else
|
|
134
|
+
merged << msg
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
merged
|
|
126
138
|
end
|
|
127
139
|
|
|
128
140
|
def extract_text(response)
|
|
@@ -5,10 +5,21 @@ module RailsConsoleAi
|
|
|
5
5
|
class SafetyError < StandardError
|
|
6
6
|
attr_reader :guard, :blocked_key
|
|
7
7
|
|
|
8
|
+
# Thread-local tracking so the executor can detect safety errors
|
|
9
|
+
# even when swallowed by a rescue inside eval'd code.
|
|
10
|
+
def self.last_raised
|
|
11
|
+
Thread.current[:rails_console_ai_last_safety_error]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.clear!
|
|
15
|
+
Thread.current[:rails_console_ai_last_safety_error] = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
8
18
|
def initialize(message, guard: nil, blocked_key: nil)
|
|
9
19
|
super(message)
|
|
10
20
|
@guard = guard
|
|
11
21
|
@blocked_key = blocked_key
|
|
22
|
+
Thread.current[:rails_console_ai_last_safety_error] = self
|
|
12
23
|
end
|
|
13
24
|
end
|
|
14
25
|
|
|
@@ -2,6 +2,7 @@ require 'json'
|
|
|
2
2
|
require 'uri'
|
|
3
3
|
require 'net/http'
|
|
4
4
|
require 'openssl'
|
|
5
|
+
require 'rails_console_ai/prefixed_io'
|
|
5
6
|
require 'rails_console_ai/channel/slack'
|
|
6
7
|
require 'rails_console_ai/conversation_engine'
|
|
7
8
|
require 'rails_console_ai/context_builder'
|
|
@@ -10,6 +11,9 @@ require 'rails_console_ai/executor'
|
|
|
10
11
|
|
|
11
12
|
module RailsConsoleAi
|
|
12
13
|
class SlackBot
|
|
14
|
+
PING_INTERVAL = 30 # seconds — send ping if no data received
|
|
15
|
+
PONG_TIMEOUT = 60 # seconds — reconnect if no pong after ping
|
|
16
|
+
|
|
13
17
|
def initialize
|
|
14
18
|
@bot_token = RailsConsoleAi.configuration.slack_bot_token || ENV['SLACK_BOT_TOKEN']
|
|
15
19
|
@app_token = RailsConsoleAi.configuration.slack_app_token || ENV['SLACK_APP_TOKEN']
|
|
@@ -26,6 +30,18 @@ module RailsConsoleAi
|
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
def start
|
|
33
|
+
$stdout.sync = true
|
|
34
|
+
$stderr.sync = true
|
|
35
|
+
$stdout = RailsConsoleAi::PrefixedIO.new($stdout) unless $stdout.is_a?(RailsConsoleAi::PrefixedIO)
|
|
36
|
+
$stderr = RailsConsoleAi::PrefixedIO.new($stderr) unless $stderr.is_a?(RailsConsoleAi::PrefixedIO)
|
|
37
|
+
|
|
38
|
+
# Eager load the Rails app so class-level initializers (e.g. Secret.get)
|
|
39
|
+
# run before safety guards are active during user code execution.
|
|
40
|
+
if defined?(Rails) && Rails.application.respond_to?(:eager_load!)
|
|
41
|
+
puts "Eager loading application..."
|
|
42
|
+
Rails.application.eager_load!
|
|
43
|
+
end
|
|
44
|
+
|
|
29
45
|
@bot_user_id = slack_api("auth.test", token: @bot_token).dig("user_id")
|
|
30
46
|
log_startup
|
|
31
47
|
|
|
@@ -78,9 +94,27 @@ module RailsConsoleAi
|
|
|
78
94
|
|
|
79
95
|
puts "Connected to Slack Socket Mode."
|
|
80
96
|
|
|
81
|
-
# Main read loop
|
|
97
|
+
# Main read loop with keepalive
|
|
98
|
+
last_activity = Time.now
|
|
99
|
+
ping_sent = false
|
|
100
|
+
|
|
82
101
|
loop do
|
|
102
|
+
ready = IO.select([ssl.to_io], nil, nil, PING_INTERVAL)
|
|
103
|
+
|
|
104
|
+
if ready.nil?
|
|
105
|
+
# Timeout — no data received
|
|
106
|
+
if ping_sent && (Time.now - last_activity) > PONG_TIMEOUT
|
|
107
|
+
puts "Slack connection timed out (no pong received). Reconnecting..."
|
|
108
|
+
break
|
|
109
|
+
end
|
|
110
|
+
send_ws_ping(ssl)
|
|
111
|
+
ping_sent = true
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
|
|
83
115
|
data = read_ws_frame(ssl)
|
|
116
|
+
last_activity = Time.now
|
|
117
|
+
ping_sent = false
|
|
84
118
|
next unless data
|
|
85
119
|
|
|
86
120
|
begin
|
|
@@ -130,6 +164,11 @@ module RailsConsoleAi
|
|
|
130
164
|
send_ws_pong(ssl, payload)
|
|
131
165
|
return nil
|
|
132
166
|
end
|
|
167
|
+
# Handle pong (opcode 10) — response to our keepalive ping
|
|
168
|
+
if opcode == 0xA
|
|
169
|
+
read_ws_payload(ssl) # consume payload
|
|
170
|
+
return nil
|
|
171
|
+
end
|
|
133
172
|
# Close frame (opcode 8)
|
|
134
173
|
return nil if opcode == 8
|
|
135
174
|
# Only process text frames (opcode 1)
|
|
@@ -194,6 +233,14 @@ module RailsConsoleAi
|
|
|
194
233
|
ssl.write(frame)
|
|
195
234
|
end
|
|
196
235
|
|
|
236
|
+
def send_ws_ping(ssl)
|
|
237
|
+
mask_key = 4.times.map { rand(256) }
|
|
238
|
+
frame = [0x89].pack("C") # FIN + ping opcode
|
|
239
|
+
frame << [0x80].pack("C") # masked, zero-length payload
|
|
240
|
+
frame << mask_key.pack("C*")
|
|
241
|
+
ssl.write(frame)
|
|
242
|
+
end
|
|
243
|
+
|
|
197
244
|
# --- Slack Web API (minimal, uses Net::HTTP) ---
|
|
198
245
|
|
|
199
246
|
def slack_api(method, token: @bot_token, **params)
|
|
@@ -232,7 +279,7 @@ module RailsConsoleAi
|
|
|
232
279
|
return if event[:user] == @bot_user_id
|
|
233
280
|
return if event[:subtype]
|
|
234
281
|
|
|
235
|
-
text = event[:text]
|
|
282
|
+
text = unescape_slack(event[:text])
|
|
236
283
|
return unless text && !text.strip.empty?
|
|
237
284
|
|
|
238
285
|
channel_id = event[:channel]
|
|
@@ -276,6 +323,15 @@ module RailsConsoleAi
|
|
|
276
323
|
return
|
|
277
324
|
end
|
|
278
325
|
|
|
326
|
+
# Direct code execution: "> User.count" runs code without LLM
|
|
327
|
+
if text.strip.start_with?('>')
|
|
328
|
+
raw_code = text.strip.sub(/\A>\s*/, '')
|
|
329
|
+
unless raw_code.empty?
|
|
330
|
+
handle_direct_code(session, channel_id, thread_ts, raw_code, user_name)
|
|
331
|
+
return
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
279
335
|
if session
|
|
280
336
|
handle_thread_reply(session, text.strip)
|
|
281
337
|
else
|
|
@@ -309,6 +365,7 @@ module RailsConsoleAi
|
|
|
309
365
|
|
|
310
366
|
session[:thread] = Thread.new do
|
|
311
367
|
Thread.current.report_on_exception = false
|
|
368
|
+
Thread.current[:log_prefix] = "[#{channel_id}/#{thread_ts}] @#{user_name}"
|
|
312
369
|
begin
|
|
313
370
|
channel.display_dim("_session: #{channel_id}/#{thread_ts}_")
|
|
314
371
|
if restored
|
|
@@ -351,6 +408,7 @@ module RailsConsoleAi
|
|
|
351
408
|
# Otherwise treat as a new message in the conversation
|
|
352
409
|
session[:thread] = Thread.new do
|
|
353
410
|
Thread.current.report_on_exception = false
|
|
411
|
+
Thread.current[:log_prefix] = channel.instance_variable_get(:@log_prefix)
|
|
354
412
|
begin
|
|
355
413
|
engine.process_message(text)
|
|
356
414
|
rescue => e
|
|
@@ -362,11 +420,76 @@ module RailsConsoleAi
|
|
|
362
420
|
end
|
|
363
421
|
end
|
|
364
422
|
|
|
423
|
+
def handle_direct_code(session, channel_id, thread_ts, raw_code, user_name)
|
|
424
|
+
# Ensure a session exists for this thread
|
|
425
|
+
unless session
|
|
426
|
+
start_direct_session(channel_id, thread_ts, user_name)
|
|
427
|
+
session = @mutex.synchronize { @sessions[thread_ts] }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
channel = session[:channel]
|
|
431
|
+
engine = session[:engine]
|
|
432
|
+
|
|
433
|
+
session[:thread] = Thread.new do
|
|
434
|
+
Thread.current.report_on_exception = false
|
|
435
|
+
Thread.current[:log_prefix] = channel.instance_variable_get(:@log_prefix)
|
|
436
|
+
begin
|
|
437
|
+
engine.execute_direct(raw_code)
|
|
438
|
+
# Post return value to Slack (execute_direct suppresses it via display_result no-op)
|
|
439
|
+
result_value = engine.instance_variable_get(:@last_interactive_result)
|
|
440
|
+
unless result_value.nil?
|
|
441
|
+
display_text = "=> #{result_value}"
|
|
442
|
+
display_text = display_text[0, 3000] + "\n... (truncated)" if display_text.length > 3000
|
|
443
|
+
post_message(channel: channel_id, thread_ts: thread_ts, text: "```#{display_text}```")
|
|
444
|
+
end
|
|
445
|
+
engine.send(:log_interactive_turn)
|
|
446
|
+
rescue => e
|
|
447
|
+
channel.display_error("Error: #{e.class}: #{e.message}")
|
|
448
|
+
RailsConsoleAi.logger.error("SlackBot direct code error: #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
|
|
449
|
+
ensure
|
|
450
|
+
ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def start_direct_session(channel_id, thread_ts, user_name)
|
|
456
|
+
channel = Channel::Slack.new(
|
|
457
|
+
slack_bot: self,
|
|
458
|
+
channel_id: channel_id,
|
|
459
|
+
thread_ts: thread_ts,
|
|
460
|
+
user_name: user_name
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
sandbox_binding = Object.new.instance_eval { binding }
|
|
464
|
+
engine = ConversationEngine.new(
|
|
465
|
+
binding_context: sandbox_binding,
|
|
466
|
+
channel: channel,
|
|
467
|
+
slack_thread_ts: thread_ts
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
restore_from_db(engine, thread_ts)
|
|
471
|
+
engine.init_interactive unless engine.instance_variable_get(:@interactive_start)
|
|
472
|
+
|
|
473
|
+
session = { channel: channel, engine: engine, thread: nil }
|
|
474
|
+
@mutex.synchronize { @sessions[thread_ts] = session }
|
|
475
|
+
end
|
|
476
|
+
|
|
365
477
|
def cancel_session(session, channel_id, thread_ts)
|
|
366
478
|
if session
|
|
367
479
|
session[:channel].cancel!
|
|
368
480
|
session[:channel].display("Stopped.")
|
|
369
481
|
puts "[#{channel_id}/#{thread_ts}] cancel requested"
|
|
482
|
+
|
|
483
|
+
# Record stop in conversation history so restored sessions know
|
|
484
|
+
# the previous topic was abandoned by the user
|
|
485
|
+
engine = session[:engine]
|
|
486
|
+
engine.history << { role: :user, content: "stop" }
|
|
487
|
+
engine.history << { role: :assistant, content: "Stopped. Awaiting new instructions." }
|
|
488
|
+
begin
|
|
489
|
+
engine.send(:log_interactive_turn)
|
|
490
|
+
rescue => e
|
|
491
|
+
RailsConsoleAi.logger.warn("SlackBot: failed to save cancel state: #{e.message}")
|
|
492
|
+
end
|
|
370
493
|
else
|
|
371
494
|
post_message(channel: channel_id, thread_ts: thread_ts, text: "No active session to stop.")
|
|
372
495
|
puts "[#{channel_id}/#{thread_ts}] cancel: no session"
|
|
@@ -412,6 +535,13 @@ module RailsConsoleAi
|
|
|
412
535
|
{ "ok" => false, "error" => e.message }
|
|
413
536
|
end
|
|
414
537
|
|
|
538
|
+
def unescape_slack(text)
|
|
539
|
+
return text unless text
|
|
540
|
+
text.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
541
|
+
.gsub("\u2018", "'").gsub("\u2019", "'") # smart single quotes → straight
|
|
542
|
+
.gsub("\u201C", '"').gsub("\u201D", '"') # smart double quotes → straight
|
|
543
|
+
end
|
|
544
|
+
|
|
415
545
|
def waiting_for_reply?(channel)
|
|
416
546
|
channel.instance_variable_get(:@reply_queue).num_waiting > 0
|
|
417
547
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_console_ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cortfr
|
|
@@ -109,6 +109,7 @@ files:
|
|
|
109
109
|
- lib/rails_console_ai/conversation_engine.rb
|
|
110
110
|
- lib/rails_console_ai/engine.rb
|
|
111
111
|
- lib/rails_console_ai/executor.rb
|
|
112
|
+
- lib/rails_console_ai/prefixed_io.rb
|
|
112
113
|
- lib/rails_console_ai/providers/anthropic.rb
|
|
113
114
|
- lib/rails_console_ai/providers/base.rb
|
|
114
115
|
- lib/rails_console_ai/providers/bedrock.rb
|