Class: Commiti::GoogleClient

Inherits:
Object
  • Object
show all
Includes:
HTTParty
Defined in:
lib/services/google_client.rb

Constant Summary collapse

DEFAULT_MODEL =
'gemma-4-31b-it'
DEFAULT_TEMPERATURE =
0.2
DEFAULT_TIMEOUT_SECONDS =
180
DEFAULT_OPEN_TIMEOUT_SECONDS =
10

Instance Method Summary collapse

Constructor Details

#initialize(config: Commiti::ConfigLoader.load) ⇒ GoogleClient

Returns a new instance of GoogleClient.



17
18
19
# File 'lib/services/google_client.rb', line 17

def initialize(config: Commiti::ConfigLoader.load)
  @config = config || {}
end

Instance Method Details

#extract_content(parsed) ⇒ Object



79
80
81
82
83
84
# File 'lib/services/google_client.rb', line 79

def extract_content(parsed)
  parts = parsed.dig('candidates', 0, 'content', 'parts')
  return '' unless parts.is_a?(Array)

  parts.map { |part| part['text'].to_s }.join.strip
end

#extract_error(body) ⇒ Object



68
69
70
71
72
73
74
75
76
77
# File 'lib/services/google_client.rb', line 68

def extract_error(body)
  parsed = JSON.parse(body.to_s)
  error = parsed['error']
  return error['message'].to_s.strip if error.is_a?(Hash)
  return error.to_s.strip unless error.nil?

  ''
rescue JSON::ParserError
  ''
end

#extract_generated_content(body) ⇒ Object



96
97
98
99
100
101
102
103
104
# File 'lib/services/google_client.rb', line 96

def extract_generated_content(body)
  parsed = JSON.parse(body.to_s)
  content = extract_content(parsed)
  raise 'Google AI error: response did not include generated text' if content.empty?

  content
rescue JSON::ParserError => e
  raise "Google AI error: invalid JSON response (#{e.message})"
end

#generate(system:, user:, api_key: nil, model: nil, temperature: nil, timeout_seconds: nil, open_timeout_seconds: nil) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/services/google_client.rb', line 21

def generate(system:, user:, api_key: nil, model: nil, temperature: nil, timeout_seconds: nil, open_timeout_seconds: nil)
  settings = request_settings(
    api_key: api_key,
    model: model,
    temperature: temperature,
    timeout_seconds: timeout_seconds,
    open_timeout_seconds: open_timeout_seconds
  )
  response = generate_content(system: system, user: user, settings: settings)
  unless response.success?
    detail = extract_error(response.body)
    message = "Google AI error: #{response.code}"
    message = "#{message} - #{detail}" unless detail.empty?
    raise message
  end
  extract_generated_content(response.body)
end

#generate_content(system:, user:, settings:) ⇒ Object



106
107
108
109
110
111
112
113
114
115
# File 'lib/services/google_client.rb', line 106

def generate_content(system:, user:, settings:)
  self.class.post(
    "/v1beta/models/#{URI.encode_www_form_component(settings[:model])}:generateContent",
    query: { key: settings[:api_key] },
    headers: { 'Content-Type' => 'application/json' },
    timeout: settings[:timeout_seconds],
    open_timeout: settings[:open_timeout_seconds],
    body: request_body(system: system, user: user, settings: settings).to_json
  )
end

#normalize_api_key(value) ⇒ Object



45
46
47
48
49
50
# File 'lib/services/google_client.rb', line 45

def normalize_api_key(value)
  key = value.to_s.strip
  return key unless key.empty?

  raise 'Google API key is missing. Set GOOGLE_API_KEY (or GEMINI_API_KEY) in your environment.'
end

#normalize_float(value, fallback) ⇒ Object



52
53
54
55
56
57
58
# File 'lib/services/google_client.rb', line 52

def normalize_float(value, fallback)
  return fallback if value.nil? || value.to_s.strip.empty?

  Float(value)
rescue ArgumentError
  fallback
end

#normalize_integer(value, fallback) ⇒ Object



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

def normalize_integer(value, fallback)
  return fallback if value.nil? || value.to_s.strip.empty?

  Integer(value)
rescue ArgumentError
  fallback
end

#normalize_model(model) ⇒ Object



39
40
41
42
43
# File 'lib/services/google_client.rb', line 39

def normalize_model(model)
  value = model.to_s.strip
  normalized = value.sub(%r{\Amodels/}, '')
  normalized.empty? ? DEFAULT_MODEL : normalized
end

#request_body(system:, user:, settings:) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/services/google_client.rb', line 117

def request_body(system:, user:, settings:)
  {
    systemInstruction: {
      parts: [{ text: system.to_s }]
    },
    generationConfig: {
      temperature: settings[:temperature]
    },
    contents: [
      {
        role: 'user',
        parts: [{ text: user.to_s }]
      }
    ]
  }
end

#request_settings(api_key:, model:, temperature:, timeout_seconds:, open_timeout_seconds:) ⇒ Object



86
87
88
89
90
91
92
93
94
# File 'lib/services/google_client.rb', line 86

def request_settings(api_key:, model:, temperature:, timeout_seconds:, open_timeout_seconds:)
  {
    api_key: normalize_api_key(api_key || @config[:google_api_key]),
    model: normalize_model(model || @config[:model]),
    temperature: normalize_float(temperature || @config[:temperature], DEFAULT_TEMPERATURE),
    timeout_seconds: normalize_integer(timeout_seconds || @config[:timeout_seconds], DEFAULT_TIMEOUT_SECONDS),
    open_timeout_seconds: normalize_integer(open_timeout_seconds || @config[:open_timeout_seconds], DEFAULT_OPEN_TIMEOUT_SECONDS)
  }
end