Class: N2B::Llm::VertexAi

Inherits:
Object
  • Object
show all
Defined in:
lib/n2b/llm/vertex_ai.rb

Constant Summary collapse

DEFAULT_LOCATION =

Vertex AI API endpoint format

'us-central1'
COMMON_LOCATIONS =
[
  'us-central1',    # Iowa, USA
  'us-east1',       # South Carolina, USA
  'us-west1',       # Oregon, USA
  'europe-west1',   # Belgium
  'europe-west4',   # Netherlands
  'asia-northeast1', # Tokyo, Japan
  'asia-southeast1'  # Singapore
].freeze
REQUEST_TIMEOUT =

HTTP timeout in seconds

60

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ VertexAi

Returns a new instance of VertexAi.



26
27
28
29
30
31
# File 'lib/n2b/llm/vertex_ai.rb', line 26

def initialize(config)
  @config = config # Contains 'vertex_credential_file' and 'model'
  @project_id = nil
  @location = DEFAULT_LOCATION
  load_project_info
end

Instance Method Details

#analyze_code_diff(prompt_content) ⇒ Object

Raises:



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/n2b/llm/vertex_ai.rb', line 158

def analyze_code_diff(prompt_content)
  model = get_model_name
  raise N2B::LlmApiError.new("No model configured for Vertex AI.") if model.nil? || model.empty?

  uri = URI.parse(build_api_uri(model))

  request = Net::HTTP::Post.new(uri)
  request.content_type = 'application/json'

  begin
    scope = 'https://www.googleapis.com/auth/cloud-platform'
    authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
      json_key_io: File.open(@config['vertex_credential_file']),
      scope: scope
    )
    access_token = authorizer.fetch_access_token!['access_token']
    request['Authorization'] = "Bearer #{access_token}"
  rescue StandardError => e
    raise N2B::LlmApiError.new("Vertex AI - Failed to obtain Google Cloud access token for diff analysis: #{e.message}")
  end

  request.body = JSON.dump({
    "contents" => [{
      "role" => "user",
      "parts" => [{
        "text" => prompt_content
      }]
    }],
    "generationConfig" => {
      "responseMimeType" => "application/json" # Expecting JSON response from LLM
    }
  })

  begin
    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
      http.read_timeout = REQUEST_TIMEOUT
      http.open_timeout = 30
      http.request(request)
    end
  rescue Net::TimeoutError, Net::ReadTimeout, Net::OpenTimeout => e
    error_msg = "Vertex AI diff analysis timed out (region: #{@location}): #{e.message}"
    error_msg += "\n\nThis might be a region issue. Try reconfiguring with 'n2b -c' and select a different region."
    error_msg += "\nFor EU users, try: europe-west1 (Belgium) or europe-west4 (Netherlands)"
    error_msg += "\nCommon regions: #{COMMON_LOCATIONS.join(', ')}"
    raise N2B::LlmApiError.new(error_msg)
  rescue => e
    raise N2B::LlmApiError.new("Vertex AI network error during diff analysis: #{e.message}")
  end

  if response.code != '200'
    error_msg = "Vertex AI LLM API Error for diff analysis: #{response.code} #{response.message} - #{response.body}"
    if response.code == '404'
      error_msg += "\n\nThis might be a region or model availability issue. Current region: #{@location}"
      error_msg += "\nNote: Google models via Vertex AI are not available in all regions."
      error_msg += "\nTry reconfiguring with 'n2b -c' and:"
      error_msg += "\n  1. Select a different region (Common regions: #{COMMON_LOCATIONS.join(', ')})"
      error_msg += "\n  2. Choose a different model (some models are only available in specific regions)"
    end
    raise N2B::LlmApiError.new(error_msg)
  end

  parsed_response = JSON.parse(response.body)
  # Return the raw JSON string from the 'text' field, CLI will parse it.
  parsed_response['candidates'].first['content']['parts'].first['text']
end

#get_model_nameObject



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/n2b/llm/vertex_ai.rb', line 65

def get_model_name
  # Resolve model name using the centralized configuration for 'vertexai'
  model_name = N2B::ModelConfig.resolve_model('vertexai', @config['model'])
  if model_name.nil? || model_name.empty?
    # Fallback to default if no model specified for vertexai
    model_name = N2B::ModelConfig.resolve_model('vertexai', N2B::ModelConfig.default_model('vertexai'))
  end
  # If still no model, a generic default could be used, or an error raised.
  # For now, assume ModelConfig handles returning a usable default or nil.
  # If ModelConfig.resolve_model can return nil and that's an issue, add handling here.
  # For example, if model_name is still nil, raise an error or use a hardcoded default.
  # Let's assume ModelConfig provides a valid model or a sensible default from models.yml.
  model_name
end

#make_request(content) ⇒ Object

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
# File 'lib/n2b/llm/vertex_ai.rb', line 80

def make_request(content)
  model = get_model_name
  raise N2B::LlmApiError.new("No model configured for Vertex AI.") if model.nil? || model.empty?

  uri = URI.parse(build_api_uri(model))

  request = Net::HTTP::Post.new(uri)
  request.content_type = 'application/json'

  begin
    scope = 'https://www.googleapis.com/auth/cloud-platform'
    authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
      json_key_io: File.open(@config['vertex_credential_file']),
      scope: scope
    )
    access_token = authorizer.fetch_access_token!['access_token']
    request['Authorization'] = "Bearer #{access_token}"
  rescue StandardError => e
    raise N2B::LlmApiError.new("Vertex AI - Failed to obtain Google Cloud access token: #{e.message}")
  end

  request.body = JSON.dump({
    "contents" => [{
      "role" => "user",
      "parts" => [{
        "text" => content
      }]
    }],
    "generationConfig" => {
      "responseMimeType" => "application/json" # Requesting JSON output from the LLM
    }
  })

  begin
    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
      http.read_timeout = REQUEST_TIMEOUT
      http.open_timeout = 30
      http.request(request)
    end
  rescue Net::TimeoutError, Net::ReadTimeout, Net::OpenTimeout => e
    error_msg = "Vertex AI request timed out (region: #{@location}): #{e.message}"
    error_msg += "\n\nThis might be a region issue. Try reconfiguring with 'n2b -c' and select a different region."
    error_msg += "\nFor EU users, try: europe-west1 (Belgium) or europe-west4 (Netherlands)"
    error_msg += "\nCommon regions: #{COMMON_LOCATIONS.join(', ')}"
    raise N2B::LlmApiError.new(error_msg)
  rescue => e
    raise N2B::LlmApiError.new("Vertex AI network error: #{e.message}")
  end

  if response.code != '200'
    error_msg = "Vertex AI LLM API Error: #{response.code} #{response.message} - #{response.body}"
    if response.code == '404'
      error_msg += "\n\nThis might be a region or model availability issue. Current region: #{@location}"
      error_msg += "\nNote: Google models via Vertex AI are not available in all regions."
      error_msg += "\nTry reconfiguring with 'n2b -c' and:"
      error_msg += "\n  1. Select a different region (Common regions: #{COMMON_LOCATIONS.join(', ')})"
      error_msg += "\n  2. Choose a different model (some models are only available in specific regions)"
    end
    raise N2B::LlmApiError.new(error_msg)
  end

  parsed_response = JSON.parse(response.body)
  # Vertex AI response structure is the same as Gemini API
  answer = parsed_response['candidates'].first['content']['parts'].first['text']

  begin
    if answer.strip.start_with?('{') && answer.strip.end_with?('}')
      answer = JSON.parse(answer) # LLM returned JSON as a string
    else
      # If not JSON, wrap it as per existing Gemini class (for CLI compatibility)
      answer = { 'explanation' => answer, 'code' => nil }
    end
  rescue JSON::ParserError
    answer = { 'explanation' => answer, 'code' => nil }
  end
  answer
end