From 8d2318656d5b79448ef5aee6a2ab949bd7f98685 Mon Sep 17 00:00:00 2001 From: Jan Provaznik <jprovaznik@gitlab.com> Date: Fri, 31 May 2024 11:21:30 +0000 Subject: [PATCH] Automatically test connection on admin page When admin visits "GitLab Duo Pro" admin page, we automatically run a simple test completion request to check if code suggestions request work as expected and show this result as a flash message. --- .../admin/code_suggestions_controller.rb | 8 +++ .../llm/ai_gateway/code_suggestions_client.rb | 70 +++++++++++++++++++ .../code_suggestions_client_spec.rb | 59 ++++++++++++++++ .../admin/code_suggestions_controller_spec.rb | 33 +++++++-- locale/gitlab.pot | 6 ++ 5 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 ee/lib/gitlab/llm/ai_gateway/code_suggestions_client.rb create mode 100644 ee/spec/lib/gitlab/llm/ai_gateway/code_suggestions_client_spec.rb diff --git a/ee/app/controllers/admin/code_suggestions_controller.rb b/ee/app/controllers/admin/code_suggestions_controller.rb index 02e252160b0d8..af3f1b30c573d 100644 --- a/ee/app/controllers/admin/code_suggestions_controller.rb +++ b/ee/app/controllers/admin/code_suggestions_controller.rb @@ -13,6 +13,14 @@ class CodeSuggestionsController < Admin::ApplicationController before_action :ensure_feature_available! def index + error = ::Gitlab::Llm::AiGateway::CodeSuggestionsClient.new(current_user).test_completion + + if error.blank? + flash[:notice] = _('Code completion test was successful') + else + flash[:alert] = format(_('Code completion test failed: %{error}'), error: error) + end + @subscription_name = License.current.subscription_name end diff --git a/ee/lib/gitlab/llm/ai_gateway/code_suggestions_client.rb b/ee/lib/gitlab/llm/ai_gateway/code_suggestions_client.rb new file mode 100644 index 0000000000000..691181fe13f95 --- /dev/null +++ b/ee/lib/gitlab/llm/ai_gateway/code_suggestions_client.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Gitlab + module Llm + module AiGateway + class CodeSuggestionsClient + include ::Gitlab::Utils::StrongMemoize + include ::API::Helpers::CloudConnector + + COMPLETION_CHECK_TIMEOUT = 3.seconds + + def initialize(user) + @user = user + end + + def test_completion + return 'Access token is missing' unless access_token + + response = Gitlab::HTTP.post( + task.endpoint, + headers: request_headers, + body: task.body, + timeout: COMPLETION_CHECK_TIMEOUT, + allow_local_requests: true + ) + + return "AI Gateway returned code #{response.code}: #{response.body}" unless response.code == 200 + return "Response doesn't contain a completion" unless choice?(response) + + nil + rescue StandardError => err + err.message + end + + private + + attr_reader :user + + def request_headers + { + 'X-Gitlab-Authentication-Type' => 'oidc', + 'Authorization' => "Bearer #{access_token}", + 'Content-Type' => 'application/json', + 'X-Request-ID' => Labkit::Correlation::CorrelationId.current_or_new_id + }.merge(cloud_connector_headers(user)) + end + + def access_token + ::CloudConnector::AvailableServices.find_by_name(:code_suggestions).access_token(user) + end + strong_memoize_attr :access_token + + def choice?(response) + response['choices']&.first&.dig('text').present? + end + + def task + params = { + current_file: { + file_name: 'test.rb', + content_above_cursor: 'def hello_world' + } + } + CodeSuggestions::Tasks::CodeCompletion.new(unsafe_passthrough_params: params) + end + strong_memoize_attr :task + end + end + end +end diff --git a/ee/spec/lib/gitlab/llm/ai_gateway/code_suggestions_client_spec.rb b/ee/spec/lib/gitlab/llm/ai_gateway/code_suggestions_client_spec.rb new file mode 100644 index 0000000000000..f67ff8c615b7a --- /dev/null +++ b/ee/spec/lib/gitlab/llm/ai_gateway/code_suggestions_client_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Llm::AiGateway::CodeSuggestionsClient, feature_category: :code_suggestions do + let_it_be(:user) { create(:user) } + + describe "#test_completion" do + let_it_be(:token) { create(:service_access_token, :active) } + let(:body) { { choices: [{ text: "puts \"Hello World!\"\nend", index: 0, finish_reason: "length" }] } } + let(:code) { 200 } + + subject(:result) { described_class.new(user).test_completion } + + shared_examples "error response" do |message| + it "returns an error" do + expect(result).to eq(message) + end + end + + before do + stub_request(:post, /#{Gitlab::AiGateway.url}/) + .to_return(status: code, body: body.to_json, headers: { "Content-Type" => "application/json" }) + allow(CloudConnector::AvailableServices).to receive_message_chain(:find_by_name, + :access_token).and_return(token) + end + + it 'returns nil if there is no error' do + expect(result).to be_nil + end + + context 'when there is not valid token' do + let(:token) { nil } + + it_behaves_like 'error response', "Access token is missing" + end + + context 'when response does not contain a valid choice' do + let(:body) { { choices: [] } } + + it_behaves_like 'error response', "Response doesn't contain a completion" + end + + context 'when response code is not 200' do + let(:code) { 401 } + let(:body) { 'an error' } + + it_behaves_like 'error response', 'AI Gateway returned code 401: "an error"' + end + + context 'when request raises an error' do + before do + stub_request(:post, /#{Gitlab::AiGateway.url}/).to_raise(StandardError.new('an error')) + end + + it_behaves_like 'error response', 'an error' + end + end +end diff --git a/ee/spec/requests/admin/code_suggestions_controller_spec.rb b/ee/spec/requests/admin/code_suggestions_controller_spec.rb index c8a420e3429e2..5cbeebd63cc47 100644 --- a/ee/spec/requests/admin/code_suggestions_controller_spec.rb +++ b/ee/spec/requests/admin/code_suggestions_controller_spec.rb @@ -15,11 +15,36 @@ end shared_examples 'renders the activation form' do - it 'renders the activation form' do - get admin_code_suggestions_path + context 'when connection check succeeds' do + before do + allow_next_instance_of(Gitlab::Llm::AiGateway::CodeSuggestionsClient) do |client| + allow(client).to receive(:test_completion).and_return(nil) + end + end + + it 'renders the activation form' do + get admin_code_suggestions_path + + expect(response).to render_template(:index) + expect(response.body).to include('js-code-suggestions-page') + expect(flash[:notice]).to eq("Code completion test was successful") + end + end - expect(response).to render_template(:index) - expect(response.body).to include('js-code-suggestions-page') + context 'when connection check fails' do + before do + allow_next_instance_of(Gitlab::Llm::AiGateway::CodeSuggestionsClient) do |client| + allow(client).to receive(:test_completion).and_return('an error') + end + end + + it 'renders the activation form with alert message' do + get admin_code_suggestions_path + + expect(response).to render_template(:index) + expect(response.body).to include('js-code-suggestions-page') + expect(flash[:alert]).to eq("Code completion test failed: an error") + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d5409593aff63..d48f0beba2ac3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12583,6 +12583,12 @@ msgstr "" msgid "Code block" msgstr "" +msgid "Code completion test failed: %{error}" +msgstr "" + +msgid "Code completion test was successful" +msgstr "" + msgid "Code coverage statistics for %{ref} %{start_date} - %{end_date}" msgstr "" -- GitLab