ai_refactor 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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