girb 0.1.2 → 0.3.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 +80 -0
- data/README.md +283 -129
- data/README_ja.md +280 -126
- data/lib/girb/ai_client.rb +209 -29
- data/lib/girb/auto_continue.rb +33 -0
- data/lib/girb/conversation_history.rb +8 -7
- data/lib/girb/debug_context_builder.rb +113 -0
- data/lib/girb/debug_integration.rb +426 -0
- data/lib/girb/debug_prompt_builder.rb +241 -0
- data/lib/girb/debug_session_history.rb +121 -0
- data/lib/girb/exception_capture.rb +4 -0
- data/lib/girb/girbrc_loader.rb +15 -9
- data/lib/girb/irb_integration.rb +233 -1
- data/lib/girb/prompt_builder.rb +156 -46
- data/lib/girb/session_persistence.rb +170 -0
- data/lib/girb/tools/continue_analysis.rb +45 -0
- data/lib/girb/tools/debug_session_history_tool.rb +132 -0
- data/lib/girb/tools/evaluate_code.rb +24 -3
- data/lib/girb/tools/run_debug_command.rb +49 -0
- data/lib/girb/tools/run_irb_debug_command.rb +58 -0
- data/lib/girb/tools/session_history_tool.rb +46 -8
- data/lib/girb/tools.rb +23 -2
- data/lib/girb/version.rb +1 -1
- data/lib/girb.rb +24 -7
- data/lib/irb/command/qq.rb +48 -6
- metadata +11 -1
data/lib/girb/ai_client.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "auto_continue"
|
|
3
4
|
require_relative "conversation_history"
|
|
4
5
|
require_relative "providers/base"
|
|
6
|
+
require_relative "debug_context_builder"
|
|
7
|
+
require_relative "debug_prompt_builder"
|
|
5
8
|
|
|
6
9
|
module Girb
|
|
7
10
|
class AiClient
|
|
@@ -11,23 +14,100 @@ module Girb
|
|
|
11
14
|
@provider = Girb.configuration.provider!
|
|
12
15
|
end
|
|
13
16
|
|
|
14
|
-
def ask(question, context, binding: nil, line_no: nil)
|
|
17
|
+
def ask(question, context, binding: nil, line_no: nil, irb_context: nil, debug_mode: false)
|
|
15
18
|
@current_binding = binding
|
|
16
19
|
@current_line_no = line_no
|
|
20
|
+
@irb_context = irb_context
|
|
21
|
+
@debug_mode = debug_mode
|
|
17
22
|
@reasoning_log = []
|
|
18
23
|
|
|
19
|
-
prompt_builder =
|
|
24
|
+
prompt_builder = create_prompt_builder(question, context)
|
|
20
25
|
@system_prompt = prompt_builder.system_prompt
|
|
21
26
|
user_message = prompt_builder.user_message
|
|
22
27
|
|
|
23
28
|
ConversationHistory.add_user_message(user_message)
|
|
24
29
|
|
|
25
30
|
tools = build_tools
|
|
26
|
-
|
|
31
|
+
|
|
32
|
+
# In debug mode, auto-continue is handled by DebugIntegration, not here
|
|
33
|
+
if @debug_mode
|
|
34
|
+
process_with_tools(tools)
|
|
35
|
+
else
|
|
36
|
+
auto_continue_count = 0
|
|
37
|
+
original_int_handler = setup_interrupt_handler
|
|
38
|
+
@debug_command_queued = false
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
loop do
|
|
42
|
+
# Check for interrupt at start of loop
|
|
43
|
+
if Girb::AutoContinue.interrupted?
|
|
44
|
+
Girb::AutoContinue.clear_interrupt!
|
|
45
|
+
handle_irb_interrupted
|
|
46
|
+
break
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
process_with_tools(tools)
|
|
50
|
+
|
|
51
|
+
# If a debug command was queued, exit immediately
|
|
52
|
+
# The command needs to be executed by IRB first, then DebugIntegration handles auto-continue
|
|
53
|
+
if @debug_command_queued
|
|
54
|
+
break
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check for interrupt after API call (Ctrl+C during request)
|
|
58
|
+
if Girb::AutoContinue.interrupted?
|
|
59
|
+
Girb::AutoContinue.clear_interrupt!
|
|
60
|
+
handle_irb_interrupted
|
|
61
|
+
break
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
break unless Girb::AutoContinue.active?
|
|
65
|
+
|
|
66
|
+
auto_continue_count += 1
|
|
67
|
+
if auto_continue_count >= Girb::AutoContinue::MAX_ITERATIONS
|
|
68
|
+
handle_irb_limit_reached
|
|
69
|
+
break
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
Girb::AutoContinue.reset!
|
|
73
|
+
|
|
74
|
+
# Rebuild context with current binding state
|
|
75
|
+
new_context = create_context_builder(@current_binding, @irb_context).build
|
|
76
|
+
continuation = "(auto-continue: Your previous action has been completed. " \
|
|
77
|
+
"Here is the updated context. Continue your investigation.)"
|
|
78
|
+
continuation_builder = create_prompt_builder(continuation, new_context)
|
|
79
|
+
ConversationHistory.add_user_message(continuation_builder.user_message)
|
|
80
|
+
end
|
|
81
|
+
ensure
|
|
82
|
+
restore_interrupt_handler(original_int_handler)
|
|
83
|
+
# Only reset AutoContinue if no debug command was queued
|
|
84
|
+
# (it will be transferred to DebugIntegration in IrbDebugHook)
|
|
85
|
+
unless @debug_command_queued
|
|
86
|
+
Girb::AutoContinue.reset!
|
|
87
|
+
end
|
|
88
|
+
Girb::AutoContinue.clear_interrupt!
|
|
89
|
+
end
|
|
90
|
+
end
|
|
27
91
|
end
|
|
28
92
|
|
|
29
93
|
private
|
|
30
94
|
|
|
95
|
+
def create_prompt_builder(question, context)
|
|
96
|
+
if @debug_mode
|
|
97
|
+
DebugPromptBuilder.new(question, context)
|
|
98
|
+
else
|
|
99
|
+
PromptBuilder.new(question, context)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def create_context_builder(binding, irb_context)
|
|
104
|
+
if @debug_mode
|
|
105
|
+
DebugContextBuilder.new(binding)
|
|
106
|
+
else
|
|
107
|
+
ContextBuilder.new(binding, irb_context)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
31
111
|
def build_tools
|
|
32
112
|
Tools.available_tools.map do |tool_class|
|
|
33
113
|
{
|
|
@@ -43,6 +123,12 @@ module Girb
|
|
|
43
123
|
accumulated_text = []
|
|
44
124
|
|
|
45
125
|
loop do
|
|
126
|
+
# Check for interrupt at start of each iteration
|
|
127
|
+
if check_interrupted?
|
|
128
|
+
puts "\n[girb] Interrupted by user (Ctrl+C)"
|
|
129
|
+
break
|
|
130
|
+
end
|
|
131
|
+
|
|
46
132
|
iterations += 1
|
|
47
133
|
if iterations > MAX_TOOL_ITERATIONS
|
|
48
134
|
puts "\n[girb] Tool iteration limit reached"
|
|
@@ -50,12 +136,29 @@ module Girb
|
|
|
50
136
|
end
|
|
51
137
|
|
|
52
138
|
messages = ConversationHistory.to_normalized
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
139
|
+
begin
|
|
140
|
+
response = @provider.chat(
|
|
141
|
+
messages: messages,
|
|
142
|
+
system_prompt: @system_prompt,
|
|
143
|
+
tools: tools,
|
|
144
|
+
binding: @current_binding
|
|
145
|
+
)
|
|
146
|
+
rescue Interrupt => e
|
|
147
|
+
puts "\n[girb] Interrupted by user (Ctrl+C)"
|
|
148
|
+
Girb::AutoContinue.interrupt! unless @debug_mode
|
|
149
|
+
Girb::DebugIntegration.interrupt! if @debug_mode && defined?(Girb::DebugIntegration)
|
|
150
|
+
break
|
|
151
|
+
rescue Exception => e
|
|
152
|
+
# IRB::Abort and similar exceptions
|
|
153
|
+
if e.class.name.include?("Abort") || e.class.name.include?("Interrupt")
|
|
154
|
+
puts "\n[girb] Interrupted by user (Ctrl+C)"
|
|
155
|
+
Girb::AutoContinue.interrupt! unless @debug_mode
|
|
156
|
+
Girb::DebugIntegration.interrupt! if @debug_mode && defined?(Girb::DebugIntegration)
|
|
157
|
+
break
|
|
158
|
+
else
|
|
159
|
+
raise
|
|
160
|
+
end
|
|
161
|
+
end
|
|
59
162
|
|
|
60
163
|
if Girb.configuration.debug
|
|
61
164
|
puts "[girb] function_calls: #{response.function_calls.inspect}"
|
|
@@ -79,27 +182,49 @@ module Girb
|
|
|
79
182
|
accumulated_text << response.text
|
|
80
183
|
end
|
|
81
184
|
|
|
82
|
-
|
|
83
|
-
tool_name = function_call[:name]
|
|
84
|
-
tool_args = function_call[:args] || {}
|
|
185
|
+
debug_command_called = false
|
|
85
186
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
187
|
+
response.function_calls.each do |function_call|
|
|
188
|
+
tool_name = function_call[:name]
|
|
189
|
+
tool_args = function_call[:args] || {}
|
|
190
|
+
tool_id = function_call[:id]
|
|
191
|
+
|
|
192
|
+
if Girb.configuration.debug
|
|
193
|
+
puts "[girb] Tool: #{tool_name}(#{tool_args.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')})"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
result = execute_tool(tool_name, tool_args)
|
|
89
197
|
|
|
90
|
-
|
|
198
|
+
@reasoning_log << {
|
|
199
|
+
tool: tool_name,
|
|
200
|
+
args: tool_args,
|
|
201
|
+
result: result
|
|
202
|
+
}
|
|
91
203
|
|
|
92
|
-
|
|
93
|
-
tool: tool_name,
|
|
94
|
-
args: tool_args,
|
|
95
|
-
result: result
|
|
96
|
-
}
|
|
204
|
+
ConversationHistory.add_tool_call(tool_name, tool_args, result, id: tool_id)
|
|
97
205
|
|
|
98
|
-
|
|
99
|
-
|
|
206
|
+
if Girb.configuration.debug && result.is_a?(Hash) && result[:error]
|
|
207
|
+
puts "[girb] Tool error: #{result[:error]}"
|
|
208
|
+
end
|
|
100
209
|
|
|
101
|
-
|
|
102
|
-
|
|
210
|
+
# If run_debug_command was called, we need to exit the tool loop
|
|
211
|
+
# so the debugger/IRB can execute the pending commands
|
|
212
|
+
if tool_name == "run_debug_command"
|
|
213
|
+
debug_command_called = true
|
|
214
|
+
# In IRB mode, mark that we've queued a debug command
|
|
215
|
+
# This will prevent the auto-continue loop from continuing
|
|
216
|
+
@debug_command_queued = true unless @debug_mode
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Exit tool loop if debug command was called - let debugger take over
|
|
221
|
+
if debug_command_called
|
|
222
|
+
# Save accumulated text and pending tool calls as assistant message
|
|
223
|
+
text = accumulated_text.any? ? accumulated_text.join("\n") : ""
|
|
224
|
+
ConversationHistory.add_assistant_message(text)
|
|
225
|
+
record_ai_response(text) unless text.empty?
|
|
226
|
+
puts text unless text.empty?
|
|
227
|
+
break
|
|
103
228
|
end
|
|
104
229
|
else
|
|
105
230
|
# Text response
|
|
@@ -140,10 +265,13 @@ module Girb
|
|
|
140
265
|
end
|
|
141
266
|
|
|
142
267
|
def record_ai_response(response)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
268
|
+
if @debug_mode
|
|
269
|
+
require_relative "debug_session_history"
|
|
270
|
+
DebugSessionHistory.record_ai_response(response)
|
|
271
|
+
elsif @current_line_no
|
|
272
|
+
reasoning = @reasoning_log.empty? ? nil : format_reasoning
|
|
273
|
+
SessionHistory.record_ai_response(@current_line_no, response, reasoning)
|
|
274
|
+
end
|
|
147
275
|
end
|
|
148
276
|
|
|
149
277
|
def format_reasoning
|
|
@@ -154,5 +282,57 @@ module Girb
|
|
|
154
282
|
"Tool: #{log[:tool]}(#{args_str})\nResult: #{result_str}"
|
|
155
283
|
end.join("\n\n")
|
|
156
284
|
end
|
|
285
|
+
|
|
286
|
+
def setup_interrupt_handler
|
|
287
|
+
trap("INT") do
|
|
288
|
+
Girb::AutoContinue.interrupt!
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def check_interrupted?
|
|
293
|
+
if @debug_mode
|
|
294
|
+
defined?(Girb::DebugIntegration) && Girb::DebugIntegration.interrupted?
|
|
295
|
+
else
|
|
296
|
+
Girb::AutoContinue.interrupted?
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def restore_interrupt_handler(original_handler)
|
|
301
|
+
if original_handler
|
|
302
|
+
trap("INT", original_handler)
|
|
303
|
+
else
|
|
304
|
+
trap("INT", "DEFAULT")
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def handle_irb_interrupted
|
|
309
|
+
return unless @current_binding
|
|
310
|
+
|
|
311
|
+
new_context = create_context_builder(@current_binding, @irb_context).build
|
|
312
|
+
interrupt_message = "(System: User interrupted with Ctrl+C. " \
|
|
313
|
+
"Briefly summarize your progress so far. " \
|
|
314
|
+
"Tell the user where you stopped and how to continue if needed.)"
|
|
315
|
+
continuation_builder = create_prompt_builder(interrupt_message, new_context)
|
|
316
|
+
ConversationHistory.add_user_message(continuation_builder.user_message)
|
|
317
|
+
process_with_tools(build_tools)
|
|
318
|
+
rescue StandardError => e
|
|
319
|
+
puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def handle_irb_limit_reached
|
|
323
|
+
puts "\n[girb] Auto-continue limit reached (#{Girb::AutoContinue::MAX_ITERATIONS})"
|
|
324
|
+
return unless @current_binding
|
|
325
|
+
|
|
326
|
+
new_context = create_context_builder(@current_binding, @irb_context).build
|
|
327
|
+
limit_message = "(System: Auto-continue limit (#{Girb::AutoContinue::MAX_ITERATIONS}) reached. " \
|
|
328
|
+
"Summarize your progress so far and tell the user what was accomplished. " \
|
|
329
|
+
"If the task is not complete, explain what remains and instruct the user " \
|
|
330
|
+
"to continue with a follow-up request.)"
|
|
331
|
+
continuation_builder = create_prompt_builder(limit_message, new_context)
|
|
332
|
+
ConversationHistory.add_user_message(continuation_builder.user_message)
|
|
333
|
+
process_with_tools(build_tools)
|
|
334
|
+
rescue StandardError => e
|
|
335
|
+
puts "[girb] Error summarizing: #{e.message}" if Girb.configuration.debug
|
|
336
|
+
end
|
|
157
337
|
end
|
|
158
338
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
module AutoContinue
|
|
5
|
+
MAX_ITERATIONS = 20
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def active?
|
|
9
|
+
@active || false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def request!
|
|
13
|
+
@active = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reset!
|
|
17
|
+
@active = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def interrupted?
|
|
21
|
+
@interrupted || false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def interrupt!
|
|
25
|
+
@interrupted = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clear_interrupt!
|
|
29
|
+
@interrupted = false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -22,8 +22,8 @@ module Girb
|
|
|
22
22
|
instance.add_assistant_message(content)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def add_tool_call(tool_name, args, result)
|
|
26
|
-
instance.add_tool_call(tool_name, args, result)
|
|
25
|
+
def add_tool_call(tool_name, args, result, id: nil)
|
|
26
|
+
instance.add_tool_call(tool_name, args, result, id: id)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def to_contents
|
|
@@ -72,8 +72,9 @@ module Girb
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
def add_tool_call(tool_name, args, result)
|
|
75
|
+
def add_tool_call(tool_name, args, result, id: nil)
|
|
76
76
|
@pending_tool_calls << {
|
|
77
|
+
id: id || "call_#{SecureRandom.hex(12)}",
|
|
77
78
|
name: tool_name,
|
|
78
79
|
args: args,
|
|
79
80
|
result: result
|
|
@@ -105,15 +106,15 @@ module Girb
|
|
|
105
106
|
|
|
106
107
|
# Add tool calls and results if present
|
|
107
108
|
msg.tool_calls&.each do |tc|
|
|
108
|
-
result << { role: :tool_call, name: tc[:name], args: tc[:args] }
|
|
109
|
-
result << { role: :tool_result, name: tc[:name], result: tc[:result] }
|
|
109
|
+
result << { role: :tool_call, id: tc[:id], name: tc[:name], args: tc[:args] }
|
|
110
|
+
result << { role: :tool_result, id: tc[:id], name: tc[:name], result: tc[:result] }
|
|
110
111
|
end
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
# Add pending tool calls
|
|
114
115
|
@pending_tool_calls.each do |tc|
|
|
115
|
-
result << { role: :tool_call, name: tc[:name], args: tc[:args] }
|
|
116
|
-
result << { role: :tool_result, name: tc[:name], result: tc[:result] }
|
|
116
|
+
result << { role: :tool_call, id: tc[:id], name: tc[:name], args: tc[:args] }
|
|
117
|
+
result << { role: :tool_result, id: tc[:id], name: tc[:name], result: tc[:result] }
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
result
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "debug_session_history"
|
|
4
|
+
|
|
5
|
+
module Girb
|
|
6
|
+
class DebugContextBuilder
|
|
7
|
+
MAX_INSPECT_LENGTH = 500
|
|
8
|
+
|
|
9
|
+
def initialize(binding, thread_client: nil)
|
|
10
|
+
@binding = binding
|
|
11
|
+
@thread_client = thread_client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build
|
|
15
|
+
{
|
|
16
|
+
source_location: capture_source_location,
|
|
17
|
+
local_variables: capture_locals,
|
|
18
|
+
instance_variables: capture_instance_variables,
|
|
19
|
+
self_info: capture_self,
|
|
20
|
+
backtrace: capture_backtrace,
|
|
21
|
+
breakpoint_info: capture_breakpoint_info,
|
|
22
|
+
session_history: capture_session_history
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def capture_source_location
|
|
29
|
+
loc = @binding.source_location
|
|
30
|
+
return nil unless loc
|
|
31
|
+
|
|
32
|
+
file, line = loc
|
|
33
|
+
{
|
|
34
|
+
file: file,
|
|
35
|
+
line: line
|
|
36
|
+
}
|
|
37
|
+
rescue StandardError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def capture_locals
|
|
42
|
+
@binding.local_variables.to_h do |var|
|
|
43
|
+
value = @binding.local_variable_get(var)
|
|
44
|
+
[var, safe_inspect(value)]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def capture_instance_variables
|
|
49
|
+
obj = @binding.receiver
|
|
50
|
+
obj.instance_variables.to_h do |var|
|
|
51
|
+
value = obj.instance_variable_get(var)
|
|
52
|
+
[var, safe_inspect(value)]
|
|
53
|
+
end
|
|
54
|
+
rescue StandardError
|
|
55
|
+
{}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def capture_self
|
|
59
|
+
obj = @binding.receiver
|
|
60
|
+
{
|
|
61
|
+
class: obj.class.name,
|
|
62
|
+
inspect: safe_inspect(obj),
|
|
63
|
+
methods: obj.methods(false).first(20)
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def capture_backtrace
|
|
68
|
+
return nil unless @thread_client
|
|
69
|
+
|
|
70
|
+
@thread_client.current_frame&.location&.to_s
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def capture_breakpoint_info
|
|
76
|
+
return nil unless defined?(DEBUGGER__) && DEBUGGER__.respond_to?(:breakpoints)
|
|
77
|
+
|
|
78
|
+
DEBUGGER__.breakpoints.map do |bp|
|
|
79
|
+
{ type: bp.class.name, location: bp.to_s }
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def capture_session_history
|
|
86
|
+
DebugSessionHistory.format_history(20)
|
|
87
|
+
rescue StandardError
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def safe_inspect(obj, max_length: MAX_INSPECT_LENGTH)
|
|
92
|
+
if defined?(ActiveRecord::Base) && obj.is_a?(ActiveRecord::Base)
|
|
93
|
+
return inspect_active_record(obj)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
result = obj.inspect
|
|
97
|
+
result.length > max_length ? "#{result[0, max_length]}..." : result
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
"#<#{obj.class} (inspect failed: #{e.message})>"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def inspect_active_record(obj)
|
|
103
|
+
{
|
|
104
|
+
class: obj.class.name,
|
|
105
|
+
id: obj.try(:id),
|
|
106
|
+
attributes: obj.attributes,
|
|
107
|
+
new_record: obj.new_record?,
|
|
108
|
+
changed: obj.changed?,
|
|
109
|
+
errors: obj.errors.full_messages
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|