girb 0.1.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 +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +253 -0
- data/README_ja.md +251 -0
- data/Rakefile +8 -0
- data/exe/girb +38 -0
- data/lib/girb/ai_client.rb +157 -0
- data/lib/girb/configuration.rb +39 -0
- data/lib/girb/context_builder.rb +134 -0
- data/lib/girb/conversation_history.rb +132 -0
- data/lib/girb/exception_capture.rb +54 -0
- data/lib/girb/irb_integration.rb +87 -0
- data/lib/girb/prompt_builder.rb +173 -0
- data/lib/girb/providers/base.rb +65 -0
- data/lib/girb/session_history.rb +197 -0
- data/lib/girb/tools/base.rb +61 -0
- data/lib/girb/tools/evaluate_code.rb +43 -0
- data/lib/girb/tools/find_file.rb +93 -0
- data/lib/girb/tools/get_source.rb +145 -0
- data/lib/girb/tools/inspect_object.rb +54 -0
- data/lib/girb/tools/list_methods.rb +66 -0
- data/lib/girb/tools/rails_tools.rb +141 -0
- data/lib/girb/tools/read_file.rb +124 -0
- data/lib/girb/tools/session_history_tool.rb +183 -0
- data/lib/girb/tools.rb +38 -0
- data/lib/girb/version.rb +5 -0
- data/lib/girb.rb +53 -0
- data/lib/irb/command/qq.rb +55 -0
- metadata +131 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "conversation_history"
|
|
4
|
+
require_relative "providers/base"
|
|
5
|
+
|
|
6
|
+
module Girb
|
|
7
|
+
class AiClient
|
|
8
|
+
MAX_TOOL_ITERATIONS = 10
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@provider = Girb.configuration.provider!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ask(question, context, binding: nil, line_no: nil)
|
|
15
|
+
@current_binding = binding
|
|
16
|
+
@current_line_no = line_no
|
|
17
|
+
@reasoning_log = []
|
|
18
|
+
|
|
19
|
+
prompt_builder = PromptBuilder.new(question, context)
|
|
20
|
+
@system_prompt = prompt_builder.system_prompt
|
|
21
|
+
user_message = prompt_builder.user_message
|
|
22
|
+
|
|
23
|
+
ConversationHistory.add_user_message(user_message)
|
|
24
|
+
|
|
25
|
+
tools = build_tools
|
|
26
|
+
process_with_tools(tools)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_tools
|
|
32
|
+
Tools.available_tools.map do |tool_class|
|
|
33
|
+
{
|
|
34
|
+
name: tool_class.tool_name,
|
|
35
|
+
description: tool_class.description,
|
|
36
|
+
parameters: tool_class.parameters
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def process_with_tools(tools)
|
|
42
|
+
iterations = 0
|
|
43
|
+
accumulated_text = []
|
|
44
|
+
|
|
45
|
+
loop do
|
|
46
|
+
iterations += 1
|
|
47
|
+
if iterations > MAX_TOOL_ITERATIONS
|
|
48
|
+
puts "\n[girb] Tool iteration limit reached"
|
|
49
|
+
break
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
messages = ConversationHistory.to_normalized
|
|
53
|
+
response = @provider.chat(
|
|
54
|
+
messages: messages,
|
|
55
|
+
system_prompt: @system_prompt,
|
|
56
|
+
tools: tools
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if Girb.configuration.debug
|
|
60
|
+
puts "[girb] function_calls: #{response.function_calls.inspect}"
|
|
61
|
+
puts "[girb] text: #{response.text&.slice(0, 100).inspect}"
|
|
62
|
+
puts "[girb] error: #{response.error.inspect}" if response.error
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
unless response
|
|
66
|
+
puts "[girb] Error: No response from API"
|
|
67
|
+
break
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if response.error && !response.function_call?
|
|
71
|
+
puts "[girb] API Error: #{response.error}"
|
|
72
|
+
break
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if response.function_call?
|
|
76
|
+
# Accumulate text that comes with function calls
|
|
77
|
+
if response.text && !response.text.empty?
|
|
78
|
+
accumulated_text << response.text
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
function_call = response.function_calls.first
|
|
82
|
+
tool_name = function_call[:name]
|
|
83
|
+
tool_args = function_call[:args] || {}
|
|
84
|
+
|
|
85
|
+
if Girb.configuration.debug
|
|
86
|
+
puts "[girb] Tool: #{tool_name}(#{tool_args.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')})"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
result = execute_tool(tool_name, tool_args)
|
|
90
|
+
|
|
91
|
+
@reasoning_log << {
|
|
92
|
+
tool: tool_name,
|
|
93
|
+
args: tool_args,
|
|
94
|
+
result: result
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Record tool call and result in conversation history
|
|
98
|
+
ConversationHistory.add_tool_call(tool_name, tool_args, result)
|
|
99
|
+
|
|
100
|
+
if Girb.configuration.debug && result.is_a?(Hash) && result[:error]
|
|
101
|
+
puts "[girb] Tool error: #{result[:error]}"
|
|
102
|
+
end
|
|
103
|
+
else
|
|
104
|
+
# Text response
|
|
105
|
+
if response.text && !response.text.empty?
|
|
106
|
+
accumulated_text << response.text
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if accumulated_text.any?
|
|
110
|
+
full_text = accumulated_text.join("\n")
|
|
111
|
+
puts full_text
|
|
112
|
+
ConversationHistory.add_assistant_message(full_text)
|
|
113
|
+
record_ai_response(full_text)
|
|
114
|
+
elsif Girb.configuration.debug
|
|
115
|
+
puts "[girb] Warning: Empty or unexpected response"
|
|
116
|
+
end
|
|
117
|
+
break
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def execute_tool(tool_name, args)
|
|
123
|
+
tool_class = Tools.find_tool(tool_name)
|
|
124
|
+
|
|
125
|
+
unless tool_class
|
|
126
|
+
return { error: "Unknown tool: #{tool_name}" }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
tool = tool_class.new
|
|
130
|
+
symbolized_args = args.transform_keys(&:to_sym)
|
|
131
|
+
|
|
132
|
+
if @current_binding
|
|
133
|
+
tool.execute(@current_binding, **symbolized_args)
|
|
134
|
+
else
|
|
135
|
+
{ error: "No binding available for tool execution" }
|
|
136
|
+
end
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
{ error: "Tool execution failed: #{e.class} - #{e.message}" }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def record_ai_response(response)
|
|
142
|
+
return unless @current_line_no
|
|
143
|
+
|
|
144
|
+
reasoning = @reasoning_log.empty? ? nil : format_reasoning
|
|
145
|
+
SessionHistory.record_ai_response(@current_line_no, response, reasoning)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def format_reasoning
|
|
149
|
+
@reasoning_log.map do |log|
|
|
150
|
+
args_str = log[:args].map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
|
151
|
+
result_str = log[:result].inspect
|
|
152
|
+
result_str = result_str[0, 500] + "..." if result_str.length > 500
|
|
153
|
+
"Tool: #{log[:tool]}(#{args_str})\nResult: #{result_str}"
|
|
154
|
+
end.join("\n\n")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :provider, :debug, :custom_prompt
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@provider = nil
|
|
9
|
+
@debug = ENV["GIRB_DEBUG"] == "1"
|
|
10
|
+
@custom_prompt = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Get the configured provider, or raise an error if not configured
|
|
14
|
+
def provider!
|
|
15
|
+
return @provider if @provider
|
|
16
|
+
|
|
17
|
+
raise ConfigurationError, <<~MSG
|
|
18
|
+
No LLM provider configured.
|
|
19
|
+
|
|
20
|
+
Install a provider gem and set GIRB_PROVIDER environment variable:
|
|
21
|
+
|
|
22
|
+
gem install girb-ruby_llm
|
|
23
|
+
export GIRB_PROVIDER=girb-ruby_llm
|
|
24
|
+
|
|
25
|
+
Or implement your own provider:
|
|
26
|
+
|
|
27
|
+
class MyProvider < Girb::Providers::Base
|
|
28
|
+
def chat(messages:, system_prompt:, tools:)
|
|
29
|
+
# Your implementation
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Girb.configure do |c|
|
|
34
|
+
c.provider = MyProvider.new
|
|
35
|
+
end
|
|
36
|
+
MSG
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "session_history"
|
|
4
|
+
|
|
5
|
+
module Girb
|
|
6
|
+
class ContextBuilder
|
|
7
|
+
MAX_INSPECT_LENGTH = 500
|
|
8
|
+
|
|
9
|
+
def initialize(binding, irb_context = nil)
|
|
10
|
+
@binding = binding
|
|
11
|
+
@irb_context = irb_context
|
|
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
|
+
class_variables: capture_class_variables,
|
|
20
|
+
global_variables: capture_global_variables,
|
|
21
|
+
self_info: capture_self,
|
|
22
|
+
last_value: capture_last_value,
|
|
23
|
+
last_exception: ExceptionCapture.last_exception,
|
|
24
|
+
session_history: session_history_with_line_numbers,
|
|
25
|
+
method_definitions: session_method_definitions
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def capture_source_location
|
|
32
|
+
loc = @binding.source_location
|
|
33
|
+
return nil unless loc
|
|
34
|
+
|
|
35
|
+
file, line = loc
|
|
36
|
+
{
|
|
37
|
+
file: file,
|
|
38
|
+
line: line
|
|
39
|
+
}
|
|
40
|
+
rescue StandardError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def capture_locals
|
|
45
|
+
@binding.local_variables.to_h do |var|
|
|
46
|
+
value = @binding.local_variable_get(var)
|
|
47
|
+
[var, safe_inspect(value)]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def capture_instance_variables
|
|
52
|
+
obj = @binding.receiver
|
|
53
|
+
obj.instance_variables.to_h do |var|
|
|
54
|
+
value = obj.instance_variable_get(var)
|
|
55
|
+
[var, safe_inspect(value)]
|
|
56
|
+
end
|
|
57
|
+
rescue StandardError
|
|
58
|
+
{}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def capture_class_variables
|
|
62
|
+
obj = @binding.receiver
|
|
63
|
+
klass = obj.is_a?(Class) ? obj : obj.class
|
|
64
|
+
klass.class_variables.to_h do |var|
|
|
65
|
+
value = klass.class_variable_get(var)
|
|
66
|
+
[var, safe_inspect(value)]
|
|
67
|
+
end
|
|
68
|
+
rescue StandardError
|
|
69
|
+
{}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def capture_global_variables
|
|
73
|
+
# よく使われる重要なグローバル変数のみ収集(全部は多すぎる)
|
|
74
|
+
important_globals = %i[$! $@ $~ $& $` $' $+ $1 $2 $3 $stdin $stdout $stderr $DEBUG $VERBOSE $LOAD_PATH $LOADED_FEATURES]
|
|
75
|
+
important_globals.each_with_object({}) do |var, hash|
|
|
76
|
+
next unless global_variables.include?(var)
|
|
77
|
+
|
|
78
|
+
value = eval(var.to_s) # rubocop:disable Security/Eval
|
|
79
|
+
hash[var] = safe_inspect(value) unless value.nil?
|
|
80
|
+
rescue StandardError
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def capture_self
|
|
86
|
+
obj = @binding.receiver
|
|
87
|
+
{
|
|
88
|
+
class: obj.class.name,
|
|
89
|
+
inspect: safe_inspect(obj),
|
|
90
|
+
methods: obj.methods(false).first(20)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def capture_last_value
|
|
95
|
+
return nil unless @irb_context
|
|
96
|
+
|
|
97
|
+
safe_inspect(@irb_context.last_value)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def safe_inspect(obj, max_length: MAX_INSPECT_LENGTH)
|
|
101
|
+
if defined?(ActiveRecord::Base) && obj.is_a?(ActiveRecord::Base)
|
|
102
|
+
return inspect_active_record(obj)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
result = obj.inspect
|
|
106
|
+
result.length > max_length ? "#{result[0, max_length]}..." : result
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
"#<#{obj.class} (inspect failed: #{e.message})>"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def inspect_active_record(obj)
|
|
112
|
+
{
|
|
113
|
+
class: obj.class.name,
|
|
114
|
+
id: obj.try(:id),
|
|
115
|
+
attributes: obj.attributes,
|
|
116
|
+
new_record: obj.new_record?,
|
|
117
|
+
changed: obj.changed?,
|
|
118
|
+
errors: obj.errors.full_messages
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def session_history_with_line_numbers
|
|
123
|
+
SessionHistory.all_with_line_numbers
|
|
124
|
+
rescue StandardError
|
|
125
|
+
[]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def session_method_definitions
|
|
129
|
+
SessionHistory.method_index
|
|
130
|
+
rescue StandardError
|
|
131
|
+
[]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
# AI会話履歴をchat API形式で管理するクラス
|
|
5
|
+
class ConversationHistory
|
|
6
|
+
Message = Struct.new(:role, :content, :tool_calls, :tool_results, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def instance
|
|
10
|
+
@instance ||= new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reset!
|
|
14
|
+
@instance = new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_user_message(content)
|
|
18
|
+
instance.add_user_message(content)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_assistant_message(content)
|
|
22
|
+
instance.add_assistant_message(content)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_tool_call(tool_name, args, result)
|
|
26
|
+
instance.add_tool_call(tool_name, args, result)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_contents
|
|
30
|
+
instance.to_contents
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def messages
|
|
34
|
+
instance.messages
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def clear!
|
|
38
|
+
instance.clear!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def summary
|
|
42
|
+
instance.summary
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_normalized
|
|
46
|
+
instance.to_normalized
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attr_reader :messages
|
|
51
|
+
|
|
52
|
+
def initialize
|
|
53
|
+
@messages = []
|
|
54
|
+
@pending_tool_calls = []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_user_message(content)
|
|
58
|
+
@messages << Message.new(role: "user", content: content)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_assistant_message(content)
|
|
62
|
+
# ツール呼び出しがあった場合は、それも含める
|
|
63
|
+
if @pending_tool_calls.any?
|
|
64
|
+
@messages << Message.new(
|
|
65
|
+
role: "model",
|
|
66
|
+
content: content,
|
|
67
|
+
tool_calls: @pending_tool_calls.dup
|
|
68
|
+
)
|
|
69
|
+
@pending_tool_calls.clear
|
|
70
|
+
else
|
|
71
|
+
@messages << Message.new(role: "model", content: content)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_tool_call(tool_name, args, result)
|
|
76
|
+
@pending_tool_calls << {
|
|
77
|
+
name: tool_name,
|
|
78
|
+
args: args,
|
|
79
|
+
result: result
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear!
|
|
84
|
+
@messages.clear
|
|
85
|
+
@pending_tool_calls.clear
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Gemini API の contents 形式に変換(後方互換性のため残す)
|
|
89
|
+
def to_contents
|
|
90
|
+
@messages.map do |msg|
|
|
91
|
+
{
|
|
92
|
+
role: msg.role,
|
|
93
|
+
parts: [{ text: msg.content }]
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Provider-agnostic normalized format
|
|
99
|
+
def to_normalized
|
|
100
|
+
result = []
|
|
101
|
+
|
|
102
|
+
@messages.each do |msg|
|
|
103
|
+
role = msg.role == "model" ? :assistant : :user
|
|
104
|
+
result << { role: role, content: msg.content }
|
|
105
|
+
|
|
106
|
+
# Add tool calls and results if present
|
|
107
|
+
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] }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Add pending tool calls
|
|
114
|
+
@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] }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
result
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# 会話履歴のサマリー(デバッグ用)
|
|
123
|
+
def summary
|
|
124
|
+
@messages.map do |msg|
|
|
125
|
+
role_label = msg.role == "user" ? "USER" : "AI"
|
|
126
|
+
content_preview = msg.content.to_s[0, 50]
|
|
127
|
+
content_preview += "..." if msg.content.to_s.length > 50
|
|
128
|
+
"#{role_label}: #{content_preview}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Girb
|
|
4
|
+
module ExceptionCapture
|
|
5
|
+
@last_exception = nil
|
|
6
|
+
@last_exception_binding = nil
|
|
7
|
+
@trace_point = nil
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :last_exception, :last_exception_binding
|
|
11
|
+
|
|
12
|
+
def capture(exception, binding = nil)
|
|
13
|
+
@last_exception = {
|
|
14
|
+
class: exception.class.name,
|
|
15
|
+
message: exception.message,
|
|
16
|
+
backtrace: exception.backtrace&.first(10),
|
|
17
|
+
time: Time.now
|
|
18
|
+
}
|
|
19
|
+
@last_exception_binding = binding
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def clear
|
|
23
|
+
@last_exception = nil
|
|
24
|
+
@last_exception_binding = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def install
|
|
28
|
+
return if @trace_point
|
|
29
|
+
|
|
30
|
+
@trace_point = TracePoint.new(:raise) do |tp|
|
|
31
|
+
# IRB内部・Ruby内部の例外は除外
|
|
32
|
+
next if tp.path&.include?("irb")
|
|
33
|
+
next if tp.path&.include?("error_highlight")
|
|
34
|
+
next if tp.path&.include?("reline")
|
|
35
|
+
next if tp.path&.include?("readline")
|
|
36
|
+
next if tp.path&.include?("rdoc")
|
|
37
|
+
next if tp.path&.include?("/ri/")
|
|
38
|
+
next if tp.raised_exception.is_a?(SystemExit)
|
|
39
|
+
next if tp.raised_exception.is_a?(Interrupt)
|
|
40
|
+
# ErrorHighlight内部の例外を除外
|
|
41
|
+
next if tp.raised_exception.class.name&.start_with?("ErrorHighlight::")
|
|
42
|
+
|
|
43
|
+
capture(tp.raised_exception, tp.binding)
|
|
44
|
+
end
|
|
45
|
+
@trace_point.enable
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def uninstall
|
|
49
|
+
@trace_point&.disable
|
|
50
|
+
@trace_point = nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "irb"
|
|
4
|
+
require "irb/command"
|
|
5
|
+
require_relative "exception_capture"
|
|
6
|
+
require_relative "context_builder"
|
|
7
|
+
require_relative "session_history"
|
|
8
|
+
require_relative "ai_client"
|
|
9
|
+
|
|
10
|
+
module Girb
|
|
11
|
+
# AI送信フラグ(スレッドローカル)
|
|
12
|
+
def self.ai_send_pending?
|
|
13
|
+
Thread.current[:girb_ai_send_pending]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.ai_send_pending=(value)
|
|
17
|
+
Thread.current[:girb_ai_send_pending] = value
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module IrbIntegration
|
|
21
|
+
def self.setup
|
|
22
|
+
# コマンドを登録
|
|
23
|
+
require_relative "../irb/command/qq"
|
|
24
|
+
|
|
25
|
+
# 例外キャプチャのインストール
|
|
26
|
+
ExceptionCapture.install
|
|
27
|
+
|
|
28
|
+
# Ctrl+Space でAI送信するフックをインストール
|
|
29
|
+
install_eval_hook
|
|
30
|
+
|
|
31
|
+
# Ctrl+Space キーバインドをインストール
|
|
32
|
+
install_ai_keybinding
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.install_eval_hook
|
|
36
|
+
IRB::Context.prepend(EvalHook)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.install_ai_keybinding
|
|
40
|
+
return unless defined?(Reline)
|
|
41
|
+
|
|
42
|
+
Reline::LineEditor.prepend(GirbLineEditorExtension)
|
|
43
|
+
|
|
44
|
+
# Ctrl+Space (ASCII 0) にバインド
|
|
45
|
+
Reline.core.config.add_default_key_binding_by_keymap(:emacs, [0], :girb_send_to_ai)
|
|
46
|
+
Reline.core.config.add_default_key_binding_by_keymap(:vi_insert, [0], :girb_send_to_ai)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module GirbLineEditorExtension
|
|
51
|
+
def girb_send_to_ai(_key)
|
|
52
|
+
Girb.ai_send_pending = true
|
|
53
|
+
finish
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module EvalHook
|
|
58
|
+
def evaluate_expression(code, line_no)
|
|
59
|
+
code = code.to_s
|
|
60
|
+
|
|
61
|
+
# Ctrl+Space でAI送信された場合
|
|
62
|
+
if Girb.ai_send_pending?
|
|
63
|
+
Girb.ai_send_pending = false
|
|
64
|
+
question = code.strip
|
|
65
|
+
return if question.empty?
|
|
66
|
+
|
|
67
|
+
SessionHistory.record(line_no, question, is_ai_question: true)
|
|
68
|
+
ask_ai(question, line_no)
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 通常のRubyコード実行時はセッション履歴に記録
|
|
73
|
+
SessionHistory.record(line_no, code)
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def ask_ai(question, line_no)
|
|
80
|
+
context = ContextBuilder.new(workspace.binding, self).build
|
|
81
|
+
client = AiClient.new
|
|
82
|
+
client.ask(question, context, binding: workspace.binding, line_no: line_no)
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
puts "[girb] Error: #{e.message}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|