diff --git a/ee/lib/gitlab/llm/ai_gateway/completions/summarize_new_merge_request.rb b/ee/lib/gitlab/llm/ai_gateway/completions/summarize_new_merge_request.rb new file mode 100644 index 0000000000000000000000000000000000000000..0a4a4242750e5f216a6317cc3be342b707713a7a --- /dev/null +++ b/ee/lib/gitlab/llm/ai_gateway/completions/summarize_new_merge_request.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Gitlab + module Llm + module AiGateway + module Completions + class SummarizeNewMergeRequest < Base + extend ::Gitlab::Utils::Override + include Gitlab::Utils::StrongMemoize + + CHARACTER_LIMIT = 2000 + + override :inputs + def inputs + { extracted_diff: extracted_diff } + end + + private + + def extracted_diff + Gitlab::Llm::Utils::MergeRequestTool.extract_diff( + source_project: source_project, + source_branch: options[:source_branch], + target_project: resource, + target_branch: options[:target_branch], + character_limit: CHARACTER_LIMIT + ) + end + strong_memoize_attr :extracted_diff + + override :valid? + def valid? + super && extracted_diff.present? + end + + def source_project + return resource unless options[:source_project_id] + + source_project = Project.find_by_id(options[:source_project_id]) + + return source_project if source_project.present? && user.can?(:create_merge_request_from, source_project) + + resource + end + end + end + end + end +end diff --git a/ee/lib/gitlab/llm/utils/ai_features_catalogue.rb b/ee/lib/gitlab/llm/utils/ai_features_catalogue.rb index 7e05976046db2b94ca723b0c01a0cc6dff336db2..99f244b2398eb5542af933e4d0d32fdd1fca9d85 100644 --- a/ee/lib/gitlab/llm/utils/ai_features_catalogue.rb +++ b/ee/lib/gitlab/llm/utils/ai_features_catalogue.rb @@ -73,6 +73,7 @@ class AiFeaturesCatalogue }, summarize_new_merge_request: { service_class: ::Gitlab::Llm::VertexAi::Completions::SummarizeNewMergeRequest, + aigw_service_class: ::Gitlab::Llm::AiGateway::Completions::SummarizeNewMergeRequest, prompt_class: ::Gitlab::Llm::Templates::SummarizeNewMergeRequest, feature_category: :code_review_workflow, execute_method: ::Llm::SummarizeNewMergeRequestService, diff --git a/ee/spec/lib/gitlab/llm/ai_gateway/completions/summarize_new_merge_request_spec.rb b/ee/spec/lib/gitlab/llm/ai_gateway/completions/summarize_new_merge_request_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..730f2ceeb65868b36ee328af6a43a1d21fc786b4 --- /dev/null +++ b/ee/spec/lib/gitlab/llm/ai_gateway/completions/summarize_new_merge_request_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Llm::AiGateway::Completions::SummarizeNewMergeRequest, feature_category: :code_review_workflow do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let(:prompt_class) { Gitlab::Llm::Templates::SummarizeNewMergeRequest } + let(:prompt_message) do + build(:ai_message, :summarize_new_merge_request, user: user, resource: project, request_id: 'uuid') + end + + let(:example_answer) { { "response" => "AI generated merge request summary" } } + let(:example_response) { instance_double(HTTParty::Response, body: example_answer.to_json, success?: true) } + + subject(:summarize_new_merge_request) { described_class.new(prompt_message, prompt_class, options).execute } + + describe '#execute' do + shared_examples 'makes AI request and publishes response' do + it 'makes AI request and publishes response' do + extracted_diff = Gitlab::Llm::Utils::MergeRequestTool.extract_diff( + source_project: options[:source_project] || project, + source_branch: options[:source_branch], + target_project: project, + target_branch: options[:target_branch], + character_limit: described_class::CHARACTER_LIMIT + ) + + expect_next_instance_of(Gitlab::Llm::AiGateway::Client) do |client| + expect(client) + .to receive(:complete) + .with( + url: "#{Gitlab::AiGateway.url}/v1/prompts/summarize_new_merge_request", + body: { 'inputs' => { extracted_diff: extracted_diff } } + ) + .and_return(example_response) + end + + expect(::Gitlab::Llm::GraphqlSubscriptionResponseService).to receive(:new).and_call_original + expect(summarize_new_merge_request[:ai_message].content).to eq(example_answer) + end + end + + context 'with valid source branch and project' do + let(:options) do + { + source_branch: 'feature', + target_branch: project.default_branch, + source_project: project + } + end + + it_behaves_like 'makes AI request and publishes response' + end + + context 'when extracted diff is blank' do + let(:options) do + { + source_branch: 'does-not-exist', + target_branch: project.default_branch, + source_project: project + } + end + + it 'does not make an AI request and returns nil' do + expect(Gitlab::Llm::AiGateway::Client).not_to receive(:new) + expect(summarize_new_merge_request).to be_nil + end + end + + context 'when source_project_id is invalid' do + let(:options) do + { + source_branch: 'feature', + target_branch: project.default_branch, + source_project_id: non_existing_record_id + } + end + + it_behaves_like 'makes AI request and publishes response' + end + end +end