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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db3b53eb0cea61e83d0d7f5632316be07ced02da11e065626cc5d72329191538
4
- data.tar.gz: 9b475208c15ccfaf9185d8c516e62efc28632dc2a6dab6d25beaba28be726278
3
+ metadata.gz: 758af953e626b18190ef1e2252e730ea636d773a53b63052aaa7aba6213b49a2
4
+ data.tar.gz: 2bf63e217e7c3647e9cc26a9112f72eb19b81700f5784730a15ec0b229889963
5
5
  SHA512:
6
- metadata.gz: 97e8c839f7c5e2dd6fc1edc12b6c46f45e99e07701da53c313c7677a159529dffd91a724cf54cb33b0c51d1b84939fb07e94c9dae516906a72030b30f3c9fcaa
7
- data.tar.gz: cd4250db8c71efad4daf61ac551b930904a103b573c595cbc3f2a8be7d6b9947576b00bce52f9c8f5858962802eb8827959b8aba5d32193a3732acfb7a108aeb
6
+ metadata.gz: 76e034568b78234e7b66c756601be73976f3367c542112b6cfc88c47ab3edb31016a8507201eee382a33ed2c7d041707295fcf140cfecc859a1d6e5b99c52d35
7
+ data.tar.gz: bb00b41760079e5203c5bcadbff10c09b7fc701ec12a1c25623dbff85862e6c3de0416d341ad41c22d03dad54fdadc97ccb33e2a02756186a65ad9e4dfb17d4d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ai_refactor (0.1.0)
4
+ ai_refactor (0.2.0)
5
5
  colorize (< 2.0)
6
6
  open3 (< 2.0)
7
7
  ruby-openai (>= 3.4.0, < 5.0)
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 only one is available:
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/company_buyer_test.rb? (y/n)
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/company_buyer_test.rb...]
35
- [Run generated test file test/models/company_buyer_test.rb (bundle exec rails test test/models/company_buyer_test.rb)...]
36
- [Done converting spec/models/my_thing_spec.rb to test/models/company_buyer_test.rb...]
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.each do |file|
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
- if refactor.run
97
- logger.success "Refactor succeeded on #{file}\n"
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.warn "Refactor failed on #{file}\n"
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(file_path, output_path, prompt_file_path:, ai_client:, logger:)
11
- @file_path = 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
- File.write(output_path, processed)
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
- attr_reader :input_file, :options, :logger
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
- def initialize(input_file, options, logger)
9
- @input_file = input_file
10
- @options = options
11
- @logger = logger
12
- end
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
- def run
15
- raise "Not implemented"
16
- end
16
+ logger.verbose "Converting #{input_file}..."
17
17
 
18
- private
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
- def ai_client
21
- @ai_client ||= OpenAI::Client.new
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
- class << self
25
- def command_line_options
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
- def refactor_name
30
- name.split("::")
31
- .last
32
- .gsub(/::/, "/")
33
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
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
- File.join(File.dirname(File.expand_path(__FILE__)), "prompts", "#{refactor_name}.md")
62
+ raise "Generic refactor requires prompt file to be user specified."
41
63
  end
42
64
  end
43
65
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AIRefactor
4
4
  module Refactors
5
- class MinitestToRspec < Generic
5
+ class MinitestToRspec < BaseRefactor
6
6
  def run
7
7
  raise "Not implemented"
8
8
  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 < Generic
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: self.class.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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AIRefactor
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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-23 00:00:00.000000000 Z
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