ai_refactor 0.1.0 → 0.2.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/Gemfile.lock +1 -1
- data/README.md +14 -5
- data/exe/ai_refactor +18 -5
- data/lib/ai_refactor/base_refactor.rb +59 -0
- data/lib/ai_refactor/file_processor.rb +10 -5
- data/lib/ai_refactor/refactors/generic.rb +48 -26
- data/lib/ai_refactor/refactors/minitest_to_rspec.rb +1 -1
- data/lib/ai_refactor/refactors/rspec_to_minitest_rails.rb +10 -4
- data/lib/ai_refactor/version.rb +1 -1
- data/lib/ai_refactor.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 758af953e626b18190ef1e2252e730ea636d773a53b63052aaa7aba6213b49a2
|
4
|
+
data.tar.gz: 2bf63e217e7c3647e9cc26a9112f72eb19b81700f5784730a15ec0b229889963
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76e034568b78234e7b66c756601be73976f3367c542112b6cfc88c47ab3edb31016a8507201eee382a33ed2c7d041707295fcf140cfecc859a1d6e5b99c52d35
|
7
|
+
data.tar.gz: bb00b41760079e5203c5bcadbff10c09b7fc701ec12a1c25623dbff85862e6c3de0416d341ad41c22d03dad54fdadc97ccb33e2a02756186a65ad9e4dfb17d4d
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,10 @@ This is based on the assumption that the LLM AIs are pretty good at identifying
|
|
9
9
|
|
10
10
|
## Available refactors
|
11
11
|
|
12
|
-
Currently
|
12
|
+
Currently available:
|
13
|
+
|
14
|
+
- `generic`
|
15
|
+
- `rspec_to_minitest_rails`
|
13
16
|
|
14
17
|
### `rspec_to_minitest_rails`
|
15
18
|
|
@@ -25,21 +28,27 @@ AI Refactor 1 files(s)/dir(s) '["spec/models/my_thing_spec.rb"]' with rspec_to_m
|
|
25
28
|
====================
|
26
29
|
Processing spec/models/my_thing_spec.rb...
|
27
30
|
[Run spec spec/models/my_thing_spec.rb... (bundle exec rspec spec/models/my_thing_spec.rb)]
|
28
|
-
Do you wish to overwrite test/models/
|
31
|
+
Do you wish to overwrite test/models/my_thing_test.rb? (y/n)
|
29
32
|
y
|
30
33
|
[Converting spec/models/my_thing_spec.rb...]
|
31
34
|
[Generate AI output. Generation attempts left: 3]
|
32
35
|
[OpenAI finished, with reason 'stop'...]
|
33
36
|
[Used tokens: 1869]
|
34
|
-
[Converted spec/models/my_thing_spec.rb to test/models/
|
35
|
-
[Run generated test file test/models/
|
36
|
-
[Done converting spec/models/my_thing_spec.rb to test/models/
|
37
|
+
[Converted spec/models/my_thing_spec.rb to test/models/my_thing_test.rb...]
|
38
|
+
[Run generated test file test/models/my_thing_test.rb (bundle exec rails test test/models/my_thing_test.rb)...]
|
39
|
+
[Done converting spec/models/my_thing_spec.rb to test/models/my_thing_test.rb...]
|
37
40
|
No differences found! Conversion worked!
|
38
41
|
Refactor succeeded on spec/models/my_thing_spec.rb
|
39
42
|
|
40
43
|
Done processing all files!
|
41
44
|
```
|
42
45
|
|
46
|
+
### `generic` (user supplied prompt)
|
47
|
+
|
48
|
+
Applies the refactor specified by prompting the AI with the user supplied prompt. You must supply a prompt file with the `-p` option.
|
49
|
+
|
50
|
+
The output is written to `stdout`.
|
51
|
+
|
43
52
|
## Installation
|
44
53
|
|
45
54
|
Install the gem and add to the application's Gemfile by executing:
|
data/exe/ai_refactor
CHANGED
@@ -88,15 +88,28 @@ end.flatten
|
|
88
88
|
logger.info "AI Refactor #{inputs.size} files(s)/dir(s) '#{input_file_path}' with #{refactorer.refactor_name} refactor\n"
|
89
89
|
logger.info "====================\n"
|
90
90
|
|
91
|
-
inputs.
|
91
|
+
return_values = inputs.map do |file|
|
92
92
|
logger.info "Processing #{file}..."
|
93
93
|
|
94
94
|
refactor = refactorer.new(file, options, logger)
|
95
|
-
|
96
|
-
|
97
|
-
|
95
|
+
refactor_returned = refactor.run
|
96
|
+
failed = refactor_returned == false
|
97
|
+
if failed
|
98
|
+
logger.warn "Refactor failed on #{file}\nFailed due to: #{refactor.failed_message}\n"
|
98
99
|
else
|
99
|
-
logger.
|
100
|
+
logger.success "Refactor succeeded on #{file}\n"
|
101
|
+
if refactor_returned.is_a?(String)
|
102
|
+
logger.info "Refactor #{file} output:\n\n#{refactor_returned}\n\n"
|
103
|
+
end
|
100
104
|
end
|
105
|
+
failed ? [file, refactor.failed_message] : true
|
101
106
|
end
|
107
|
+
|
108
|
+
if return_values.all?(true)
|
109
|
+
logger.success "All files processed successfully!"
|
110
|
+
else
|
111
|
+
files = return_values.select { |v| v != true }
|
112
|
+
logger.warn "Some files failed to process:\n#{files.map { |f| "#{f[0]} :\n > #{f[1]}" }.join("\n")}"
|
113
|
+
end
|
114
|
+
|
102
115
|
logger.info "Done processing all files!"
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
class BaseRefactor
|
5
|
+
attr_reader :input_file, :options, :logger
|
6
|
+
attr_writer :failed_message
|
7
|
+
|
8
|
+
def initialize(input_file, options, logger)
|
9
|
+
@input_file = input_file
|
10
|
+
@options = options
|
11
|
+
@logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
def failed_message
|
19
|
+
@failed_message || "Reason not specified"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def prompt_file_path
|
25
|
+
self.class.prompt_file_path
|
26
|
+
end
|
27
|
+
|
28
|
+
def ai_client
|
29
|
+
@ai_client ||= OpenAI::Client.new
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def command_line_options
|
34
|
+
[]
|
35
|
+
end
|
36
|
+
|
37
|
+
def refactor_name
|
38
|
+
name.split("::")
|
39
|
+
.last
|
40
|
+
.gsub(/::/, "/")
|
41
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
42
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
43
|
+
.tr("-", "_")
|
44
|
+
.downcase
|
45
|
+
end
|
46
|
+
|
47
|
+
def prompt_file_path
|
48
|
+
file = if options[:prompt_file_path]&.length&.positive?
|
49
|
+
options[:prompt_file_path]
|
50
|
+
else
|
51
|
+
File.join(File.dirname(File.expand_path(__FILE__)), "prompts", "#{refactor_name}.md")
|
52
|
+
end
|
53
|
+
file.tap do |prompt|
|
54
|
+
raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -7,15 +7,16 @@ module AIRefactor
|
|
7
7
|
class FileProcessor
|
8
8
|
attr_reader :file_path, :output_path, :logger
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
@file_path =
|
12
|
-
@output_path = output_path
|
10
|
+
def initialize(input_path:, prompt_file_path:, ai_client:, logger:, output_path: nil)
|
11
|
+
@file_path = input_path
|
13
12
|
@prompt_file_path = prompt_file_path
|
14
13
|
@ai_client = ai_client
|
15
14
|
@logger = logger
|
15
|
+
@output_path = output_path
|
16
16
|
end
|
17
17
|
|
18
18
|
def output_exists?
|
19
|
+
return false unless output_path
|
19
20
|
File.exist?(output_path)
|
20
21
|
end
|
21
22
|
|
@@ -29,9 +30,13 @@ module AIRefactor
|
|
29
30
|
]
|
30
31
|
content, finished_reason, usage = generate_next_message(messages, prompt, options, options[:ai_max_attempts] || 3)
|
31
32
|
|
32
|
-
if content && content.length > 0
|
33
|
+
content = if content && content.length > 0
|
33
34
|
processed = block_given? ? yield(content) : content
|
34
|
-
|
35
|
+
if output_path
|
36
|
+
File.write(output_path, processed)
|
37
|
+
logger.verbose "Wrote output to #{output_path}..."
|
38
|
+
end
|
39
|
+
processed
|
35
40
|
end
|
36
41
|
|
37
42
|
[content, finished_reason, usage]
|
@@ -2,42 +2,64 @@
|
|
2
2
|
|
3
3
|
module AIRefactor
|
4
4
|
module Refactors
|
5
|
-
class Generic
|
6
|
-
|
5
|
+
class Generic < BaseRefactor
|
6
|
+
def run
|
7
|
+
logger.verbose "Generic refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
processor = AIRefactor::FileProcessor.new(
|
10
|
+
input_path: input_file,
|
11
|
+
prompt_file_path: prompt_file_path,
|
12
|
+
ai_client: ai_client,
|
13
|
+
logger: logger
|
14
|
+
)
|
13
15
|
|
14
|
-
|
15
|
-
raise "Not implemented"
|
16
|
-
end
|
16
|
+
logger.verbose "Converting #{input_file}..."
|
17
17
|
|
18
|
-
|
18
|
+
begin
|
19
|
+
output_content, finished_reason, usage = processor.process!(options)
|
20
|
+
rescue => e
|
21
|
+
logger.error "Request to OpenAI failed: #{e.message}"
|
22
|
+
logger.warn "Skipping #{input_file}..."
|
23
|
+
self.failed_message = "Request to OpenAI failed"
|
24
|
+
return false
|
25
|
+
end
|
19
26
|
|
20
|
-
|
21
|
-
|
22
|
-
end
|
27
|
+
logger.verbose "OpenAI finished, with reason '#{finished_reason}'..."
|
28
|
+
logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
|
23
29
|
|
24
|
-
|
25
|
-
|
26
|
-
[]
|
30
|
+
if finished_reason == "length"
|
31
|
+
logger.warn "Translation may contain an incomplete output as the max token length was reached. You can try using the '--continue' option next time to increase the length of generated output."
|
27
32
|
end
|
28
33
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
35
|
-
.tr("-", "_")
|
36
|
-
.downcase
|
34
|
+
if !output_content || output_content.length == 0
|
35
|
+
logger.warn "Skipping #{input_file}, no translated output..."
|
36
|
+
logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
|
37
|
+
self.failed_message = "AI conversion failed, no output was generated"
|
38
|
+
return false
|
37
39
|
end
|
38
40
|
|
41
|
+
output_content
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def prompt_file_path
|
47
|
+
specified_prompt_path = options[:prompt_file_path]
|
48
|
+
if specified_prompt_path&.length&.positive?
|
49
|
+
if File.exist?(specified_prompt_path)
|
50
|
+
return specified_prompt_path
|
51
|
+
else
|
52
|
+
logger.error "No prompt file '#{specified_prompt_path}' found"
|
53
|
+
end
|
54
|
+
else
|
55
|
+
logger.error "No prompt file was specified!"
|
56
|
+
end
|
57
|
+
exit 1
|
58
|
+
end
|
59
|
+
|
60
|
+
class << self
|
39
61
|
def prompt_file_path
|
40
|
-
|
62
|
+
raise "Generic refactor requires prompt file to be user specified."
|
41
63
|
end
|
42
64
|
end
|
43
65
|
end
|
@@ -7,7 +7,7 @@ require_relative "tests/test_run_diff_report"
|
|
7
7
|
|
8
8
|
module AIRefactor
|
9
9
|
module Refactors
|
10
|
-
class RspecToMinitestRails <
|
10
|
+
class RspecToMinitestRails < BaseRefactor
|
11
11
|
def run
|
12
12
|
spec_runner = AIRefactor::Tests::RSpecRunner.new(input_file)
|
13
13
|
logger.verbose "Run spec #{input_file}... (#{spec_runner.command})"
|
@@ -17,6 +17,7 @@ module AIRefactor
|
|
17
17
|
if spec_run.failed?
|
18
18
|
logger.warn "Skipping #{input_file}..."
|
19
19
|
logger.error "Failed to run #{input_file}, exited with status #{spec_run.exitstatus}. Stdout: #{spec_run.stdout}\n\nStderr: #{spec_run.stderr}\n\n"
|
20
|
+
self.failed_message = "Failed to run RSpec file, has errors"
|
20
21
|
return false
|
21
22
|
end
|
22
23
|
|
@@ -26,9 +27,9 @@ module AIRefactor
|
|
26
27
|
output_path = input_file.gsub("_spec.rb", "_test.rb").gsub("spec/", "test/")
|
27
28
|
|
28
29
|
processor = AIRefactor::FileProcessor.new(
|
29
|
-
input_file,
|
30
|
-
output_path,
|
31
|
-
prompt_file_path:
|
30
|
+
input_path: input_file,
|
31
|
+
output_path: output_path,
|
32
|
+
prompt_file_path: prompt_file_path,
|
32
33
|
ai_client: ai_client,
|
33
34
|
logger: logger
|
34
35
|
)
|
@@ -38,6 +39,7 @@ module AIRefactor
|
|
38
39
|
answer = $stdin.gets.chomp
|
39
40
|
unless answer == "y" || answer == "Y"
|
40
41
|
logger.warn "Skipping #{input_file}..."
|
42
|
+
self.failed_message = "Skipped as output test file already exists"
|
41
43
|
return false
|
42
44
|
end
|
43
45
|
end
|
@@ -51,6 +53,7 @@ module AIRefactor
|
|
51
53
|
rescue => e
|
52
54
|
logger.error "Request to OpenAI failed: #{e.message}"
|
53
55
|
logger.warn "Skipping #{input_file}..."
|
56
|
+
self.failed_message = "Request to OpenAI failed"
|
54
57
|
return false
|
55
58
|
end
|
56
59
|
|
@@ -65,6 +68,7 @@ module AIRefactor
|
|
65
68
|
if !output_content || output_content.length == 0
|
66
69
|
logger.warn "Skipping #{input_file}, no translated output..."
|
67
70
|
logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
|
71
|
+
self.failed_message = "AI conversion failed, no output was generated"
|
68
72
|
return false
|
69
73
|
end
|
70
74
|
|
@@ -79,6 +83,7 @@ module AIRefactor
|
|
79
83
|
logger.warn "Skipping #{input_file}..."
|
80
84
|
logger.error "Failed to run translated #{output_path}, exited with status #{test_run.exitstatus}. Stdout: #{test_run.stdout}\n\nStderr: #{test_run.stderr}\n\n"
|
81
85
|
logger.error "Conversion failed!", bold: true
|
86
|
+
self.failed_message = "Generated test file failed to run correctly"
|
82
87
|
return false
|
83
88
|
end
|
84
89
|
|
@@ -95,6 +100,7 @@ module AIRefactor
|
|
95
100
|
logger.warn report.diff.colorize(:yellow)
|
96
101
|
logger.verbose "Done converting #{input_file} to #{output_path}..."
|
97
102
|
logger.error "Differences found! Conversion failed!", bold: true
|
103
|
+
self.failed_message = "Generated test file run output did not match original RSpec spec run output"
|
98
104
|
false
|
99
105
|
end
|
100
106
|
end
|
data/lib/ai_refactor/version.rb
CHANGED
data/lib/ai_refactor.rb
CHANGED
@@ -6,6 +6,7 @@ require_relative "ai_refactor/logger"
|
|
6
6
|
require_relative "ai_refactor/file_processor"
|
7
7
|
|
8
8
|
require_relative "ai_refactor/refactors"
|
9
|
+
require_relative "ai_refactor/base_refactor"
|
9
10
|
require_relative "ai_refactor/refactors/generic"
|
10
11
|
require_relative "ai_refactor/refactors/rspec_to_minitest_rails"
|
11
12
|
require_relative "ai_refactor/refactors/minitest_to_rspec"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ai_refactor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Ierodiaconou
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-05-
|
11
|
+
date: 2023-05-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- ai_refactor.gemspec
|
78
78
|
- exe/ai_refactor
|
79
79
|
- lib/ai_refactor.rb
|
80
|
+
- lib/ai_refactor/base_refactor.rb
|
80
81
|
- lib/ai_refactor/file_processor.rb
|
81
82
|
- lib/ai_refactor/logger.rb
|
82
83
|
- lib/ai_refactor/refactors.rb
|