Class: GitAuto::Services::AIService

Inherits:
Object
  • Object
show all
Defined in:
lib/git_auto/services/ai_service.rb

Defined Under Namespace

Classes: APIKeyError, DiffTooLargeError, EmptyDiffError, Error, RateLimitError

Constant Summary collapse

OPENAI_API_URL =
"https://api.openai.com/v1/chat/completions"
CLAUDE_API_URL =
"https://api.anthropic.com/v1/messages"
MAX_DIFF_SIZE =
10_000
MAX_RETRIES =
3
BACKOFF_BASE =
2
TEMPERATURE_VARIATIONS =
[
  { openai: 0.7, claude: 0.7 },
  { openai: 0.8, claude: 0.8 },
  { openai: 0.9, claude: 0.9 },
  { openai: 1.0, claude: 1.0 }
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(settings) ⇒ AIService

Returns a new instance of AIService.



34
35
36
37
38
39
40
41
42
# File 'lib/git_auto/services/ai_service.rb', line 34

def initialize(settings)
  @settings = settings
  @credential_store = Config::CredentialStore.new
  @history_service = HistoryService.new
  @diff_summarizer = Formatters::DiffSummarizer.new
  @@temperature ||= TEMPERATURE_VARIATIONS[0]
  @request_count = 0
  @previous_suggestions = []
end

Class Method Details

.reset_temperatureObject



22
23
24
# File 'lib/git_auto/services/ai_service.rb', line 22

def reset_temperature
  @@temperature = nil
end

Instance Method Details

#generate_commit_message(diff, style: :conventional, scope: nil) ⇒ Object

Raises:



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/git_auto/services/ai_service.rb', line 117

def generate_commit_message(diff, style: :conventional, scope: nil)
  raise EmptyDiffError if diff.nil? || diff.strip.empty?

  # If diff is too large, use the summarized version
  diff = @diff_summarizer.summarize(diff) if diff.length > MAX_DIFF_SIZE

  if style.to_s == "minimal"
    message = case @settings.get(:ai_provider)
              when "openai"
                generate_openai_commit_message(diff, style)
              when "claude"
                generate_claude_commit_message(diff, style)
              end

    # Extract type and description from the message
    if message =~ /^(\w+):\s*(.+)$/
      type = ::Regexp.last_match(1)
      description = ::Regexp.last_match(2)
      return "#{type}: #{description}"
    end

    return message
  elsif style.to_s == "conventional" && scope.nil?
    # Generate both scope and message in one call
    message = case @settings.get(:ai_provider)
              when "openai"
                generate_openai_commit_message(diff, style)
              when "claude"
                generate_claude_commit_message(diff, style)
              end

    # Extract type and scope from the message
    if message =~ /^(\w+)(?:\(([\w-]+)\))?:\s*(.+)$/
      type = ::Regexp.last_match(1)
      existing_scope = ::Regexp.last_match(2)
      description = ::Regexp.last_match(3)

      # If we got a scope in the message, use it, otherwise generate one
      scope ||= existing_scope || infer_scope_from_diff(diff)
      return scope ? "#{type}(#{scope}): #{description}" : "#{type}: #{description}"
    end

    # If message doesn't match expected format, just return it as is
    return message
  end

  retries = 0
  begin
    case @settings.get(:ai_provider)
    when "openai"
      generate_openai_commit_message(diff, style, scope)
    when "claude"
      generate_claude_commit_message(diff, style, scope)
    else
      raise GitAuto::Errors::InvalidProviderError, "Invalid AI provider specified"
    end
  rescue StandardError => e
    retries += 1
    if retries < MAX_RETRIES
      sleep(retries * BACKOFF_BASE)
      retry
    end
    raise e
  end
end

#generate_conventional_commit(diff) ⇒ Object



101
102
103
# File 'lib/git_auto/services/ai_service.rb', line 101

def generate_conventional_commit(diff)
  generate_commit_message(diff, style: :conventional)
end

#generate_scoped_commit(diff, scope) ⇒ Object



109
110
111
# File 'lib/git_auto/services/ai_service.rb', line 109

def generate_scoped_commit(diff, scope)
  generate_commit_message(diff, style: :conventional, scope: scope)
end

#generate_simple_commit(diff) ⇒ Object



105
106
107
# File 'lib/git_auto/services/ai_service.rb', line 105

def generate_simple_commit(diff)
  generate_commit_message(diff, style: :simple)
end

#get_system_prompt(style, retry_attempt = 0) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/git_auto/services/ai_service.rb', line 68

def get_system_prompt(style, retry_attempt = 0)
  base_prompt = case style.to_s
                when "minimal"
                  "You are an expert in writing minimal commit messages that follow the format: <type>: <description>\n" \
                  "Rules:\n" \
                  "1. ALWAYS start with a type from the list above\n" \
                  "2. NEVER include a scope\n" \
                  "3. Keep the message under 72 characters\n" \
                  "4. Use lowercase\n" \
                  "5. Use present tense\n" \
                  "6. Be descriptive but concise\n" \
                  "7. Do not include a period at the end"
                when "conventional"
                  "You are an expert in writing conventional commit messages..."
                else
                  "You are an expert in writing clear and concise git commit messages..."
                end

  # Add variation for retries
  if retry_attempt.positive?
    base_prompt += "\nPlease provide a different perspective or approach than previous attempts."
    base_prompt += "\nBe more #{["specific", "detailed", "creative", "concise"].sample} in this attempt."
  end

  base_prompt
end

#get_temperature(retry_attempt = 0) ⇒ Object



59
60
61
62
63
64
65
66
# File 'lib/git_auto/services/ai_service.rb', line 59

def get_temperature(retry_attempt = 0)
  provider = @settings.get(:ai_provider).to_sym
  return TEMPERATURE_VARIATIONS[0][provider] if retry_attempt.zero?

  # Use progressively higher temperatures for retries
  variation_index = [retry_attempt - 1, TEMPERATURE_VARIATIONS.length - 1].min
  TEMPERATURE_VARIATIONS[variation_index][provider]
end

#log_api_request(provider, payload, temperature) ⇒ Object



44
45
46
47
48
49
50
51
# File 'lib/git_auto/services/ai_service.rb', line 44

def log_api_request(provider, payload, temperature)
  puts "\n=== API Request ##{@request_count += 1} ==="
  puts "Provider: #{provider}"
  puts "Temperature: #{temperature}"
  puts "Full Payload:"
  puts JSON.pretty_generate(payload)
  puts "===================="
end

#log_api_response(response_body) ⇒ Object



53
54
55
56
57
# File 'lib/git_auto/services/ai_service.rb', line 53

def log_api_response(response_body)
  puts "\n=== API Response ==="
  puts JSON.pretty_generate(JSON.parse(response_body.to_s))
  puts "===================="
end

#next_temperature_variationObject



95
96
97
98
99
# File 'lib/git_auto/services/ai_service.rb', line 95

def next_temperature_variation
  @@temperature = (@@temperature + 1) % TEMPERATURE_VARIATIONS.length
  provider = @settings.get(:ai_provider).to_sym
  TEMPERATURE_VARIATIONS[@@temperature][provider]
end

#suggest_commit_scope(diff) ⇒ Object



113
114
115
# File 'lib/git_auto/services/ai_service.rb', line 113

def suggest_commit_scope(diff)
  generate_commit_message(diff, style: :scope)
end