mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-03 16:04:30 +00:00
126 lines
3.5 KiB
Ruby
126 lines
3.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Keeps
|
|
module Helpers
|
|
class AiEditor
|
|
START_OLD_CODE = '<old_code>'
|
|
END_OLD_CODE = '</old_code>'
|
|
START_NEW_CODE = '<new_code>'
|
|
END_NEW_CODE = '</new_code>'
|
|
PATCH_SYSTEM_MESSAGE = File.read(File.expand_path('patch_system_message.md', __dir__))
|
|
AI_MODEL = 'claude-3-7-sonnet-20250219'
|
|
ANTHROPIC_API = 'https://api.anthropic.com/v1/messages'
|
|
|
|
attr_reader :model, :token
|
|
|
|
def initialize(model: AI_MODEL, token: nil)
|
|
@model = model
|
|
@token = token || ENV['ANTHROPIC_API_KEY']
|
|
|
|
raise ArgumentError, 'Anthropic API token is required' if @token.blank?
|
|
end
|
|
|
|
def ask_for_and_apply_patch(user_message, file)
|
|
puts "Asking for patch for file #{file}"
|
|
|
|
unless File.exist?(file)
|
|
puts "Error: File #{file} doesn't exist"
|
|
return false
|
|
end
|
|
|
|
begin
|
|
file_content = File.read(file)
|
|
rescue StandardError => e
|
|
puts "Error reading file #{file}: #{e.message}"
|
|
return false
|
|
end
|
|
|
|
user_message += <<~MARKDOWN
|
|
<source_code>
|
|
#{file_content}
|
|
</source_code>
|
|
MARKDOWN
|
|
|
|
begin
|
|
response = request(PATCH_SYSTEM_MESSAGE, user_message)
|
|
|
|
unless response.code.between?(200, 299)
|
|
puts "API request failed with status #{response.code}: #{response.body}"
|
|
return false
|
|
end
|
|
|
|
data = Gitlab::Json.parse(response.body)
|
|
text = data.dig('content', 0, 'text')
|
|
|
|
if text.nil?
|
|
puts "Error: AI response is missing expected text content"
|
|
puts "Response data: #{data.inspect}"
|
|
return false
|
|
end
|
|
|
|
apply_patch(file, text)
|
|
rescue ::Gitlab::Housekeeper::Shell::Error => e
|
|
puts "Error in ask_for_and_apply_patch: #{e.message}"
|
|
puts e.backtrace.join("\n") if e.backtrace
|
|
false
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def apply_patch(file, ai_response)
|
|
if ai_response.blank?
|
|
puts "Error: AI response is empty for file #{file}. Cannot proceed with patch application."
|
|
return false
|
|
end
|
|
|
|
fixed_code = File.read(file)
|
|
|
|
changes = extract_changes_from_blocks(ai_response)
|
|
|
|
if changes.empty?
|
|
puts "No valid code blocks found in AI response for #{file} assuming no fixes needed."
|
|
return true
|
|
end
|
|
|
|
changes.each do |change|
|
|
old_code, new_code = change
|
|
return false if old_code.nil? || new_code.nil?
|
|
|
|
fixed_code.gsub!(old_code.lstrip, new_code.lstrip)
|
|
end
|
|
|
|
File.write(file, fixed_code)
|
|
true
|
|
end
|
|
|
|
def extract_changes_from_blocks(llm_patch)
|
|
old_code_regex = /(?<=#{Regexp.escape(START_OLD_CODE)}).*?(?=#{Regexp.escape(END_OLD_CODE)})/mo
|
|
old_code_match_data = llm_patch.scan(old_code_regex)
|
|
new_code_regex = /(?<=#{Regexp.escape(START_NEW_CODE)}).*?(?=#{Regexp.escape(END_NEW_CODE)})/mo
|
|
new_code_match_data = llm_patch.scan(new_code_regex)
|
|
|
|
old_code_match_data.zip(new_code_match_data)
|
|
end
|
|
|
|
def request(system, message)
|
|
::HTTParty.post(
|
|
ANTHROPIC_API,
|
|
headers: {
|
|
'x-api-key' => token
|
|
},
|
|
body: {
|
|
system: system,
|
|
model: model,
|
|
max_tokens: 4096,
|
|
messages: [
|
|
{ role: 'user', content: message }
|
|
]
|
|
}.to_json,
|
|
read_timeout: 60
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|