Skip to content
Snippets Groups Projects
Commit c6f8615b authored by Alexandru Croitor's avatar Alexandru Croitor :three:
Browse files

Merge branch '408120-ai-response-with-rendered-markdown' into 'master'

Ai Response with rendered Markdown

See merge request !118350



Merged-by: default avatarAlexandru Croitor <acroitor@gitlab.com>
Approved-by: Gosia Ksionek's avatarGosia Ksionek <mksionek@gitlab.com>
Approved-by: default avatarAlexandru Croitor <acroitor@gitlab.com>
Reviewed-by: Gosia Ksionek's avatarGosia Ksionek <mksionek@gitlab.com>
Reviewed-by: default avatarAlexandru Croitor <acroitor@gitlab.com>
Reviewed-by: default avatarBojan Marjanovic <bmarjanovic@gitlab.com>
Co-authored-by: default avatarbmarjanovic <bmarjanovic@gitlab.com>
parents d5f7af49 23614225
No related branches found
No related tags found
2 merge requests!122597doc/gitaly: Remove references to removed metrics,!118350Ai Response with rendered Markdown
Pipeline #849838793 passed
......@@ -973,6 +973,7 @@ Input type: `AiActionInput`
| <a id="mutationaiactionclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaiactionexplaincode"></a>`explainCode` | [`AiExplainCodeInput`](#aiexplaincodeinput) | Input for explain_code AI action. |
| <a id="mutationaiactionexplainvulnerability"></a>`explainVulnerability` | [`AiExplainVulnerabilityInput`](#aiexplainvulnerabilityinput) | Input for explain_vulnerability AI action. |
| <a id="mutationaiactionmarkupformat"></a>`markupFormat` | [`MarkupFormat`](#markupformat) | Indicates the response format. |
| <a id="mutationaiactionsummarizecomments"></a>`summarizeComments` | [`AiSummarizeCommentsInput`](#aisummarizecommentsinput) | Input for summarize_comments AI action. |
 
#### Fields
......@@ -24151,6 +24152,16 @@ List limit metric setting.
| <a id="listlimitmetricissue_count"></a>`issue_count` | Limit list by number of issues. |
| <a id="listlimitmetricissue_weights"></a>`issue_weights` | Limit list by total weight of issues. |
 
### `MarkupFormat`
List markup formats.
| Value | Description |
| ----- | ----------- |
| <a id="markupformathtml"></a>`HTML` | HTML format. |
| <a id="markupformatmarkdown"></a>`MARKDOWN` | Markdown format. |
| <a id="markupformatraw"></a>`RAW` | Raw format. |
### `MeasurementIdentifier`
 
Possible identifier types for a measurement.
......@@ -7,7 +7,7 @@ module GraphqlTriggers
prepended do
def self.ai_completion_response(user_gid, resource_gid, response)
::GitlabSchema.subscriptions.trigger(
'aiCompletionResponse', { user_id: user_gid, resource_id: resource_gid }, response
:ai_completion_response, { user_id: user_gid, resource_id: resource_gid }, response
)
end
......
# frozen_string_literal: true
module EE
module Types
class MarkupFormatEnum < ::Types::BaseEnum
graphql_name 'MarkupFormat'
description 'List markup formats'
value 'MARKDOWN', description: 'Markdown format.', value: :markdown
value 'HTML', description: 'HTML format.', value: :html
value 'RAW', description: 'Raw format.', value: :raw
end
end
end
......@@ -14,6 +14,11 @@ class Action < BaseMutation
description: "Input for #{method} AI action."
end
argument :markup_format, EE::Types::MarkupFormatEnum,
required: false,
description: 'Indicates the response format.',
default_value: :raw
def ready?(**args)
raise Gitlab::Graphql::Errors::ArgumentError, MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR if methods(args).size != 1
......@@ -65,12 +70,13 @@ def authorized_resource?(object)
end
def extract_method_params!(attributes)
options = attributes.extract!(:markup_format)
methods = methods(attributes.transform_values(&:to_h))
# At this point, we only have one method since we filtered it in `#ready?`
# so we can safely get the first.
method = methods.each_key.first
method_arguments = methods[method]
method_arguments = options.merge(methods[method])
[method_arguments.delete(:resource_id), method, method_arguments]
end
......
......@@ -18,7 +18,7 @@ def execute(response_modifier = Gitlab::Llm::OpenAi::ResponseModifiers::Completi
id: SecureRandom.uuid,
model_name: resource.class.name,
# todo: do we need to sanitize/refine this response in any ways?
response_body: response_modifier.execute(ai_response).to_s.strip,
response_body: generate_response_body(response_modifier.execute(ai_response).to_s.strip),
errors: [ai_response&.dig(:error)].compact
}
......@@ -28,6 +28,21 @@ def execute(response_modifier = Gitlab::Llm::OpenAi::ResponseModifiers::Completi
private
attr_reader :user, :resource, :ai_response, :options
def generate_response_body(response_body)
return response_body if options[:markup_format].nil? || options[:markup_format] == :raw
banzai_options = { only_path: false, pipeline: :full, current_user: user }
if resource.try(:project)
banzai_options[:project] = resource.project
elsif resource.try(:group)
banzai_options[:group] = resource.group
banzai_options[:skip_project_check] = true
end
Banzai.render_and_post_process(response_body, banzai_options)
end
end
end
end
......
......@@ -10,7 +10,7 @@
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#ready?' do
let(:arguments) { { summarize_comments: { resource_id: resource_id } } }
let(:arguments) { { summarize_comments: { resource_id: resource_id }, markup_format: :markdown } }
it { is_expected.to be_ready(**arguments) }
......
......@@ -6,6 +6,8 @@
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
let(:response_body) { 'Some response' }
let(:options) { {} }
let(:ai_response_json) do
'{
......@@ -29,7 +31,7 @@
}'
end
RSpec.shared_examples 'triggers ai completion subscription' do
shared_examples 'triggers ai completion subscription' do
it 'triggers subscription' do
uuid = 'u-u-i-d'
allow(SecureRandom).to receive(:uuid).and_return(uuid)
......@@ -37,7 +39,7 @@
data = {
id: uuid,
model_name: resource.class.name,
response_body: 'Some response',
response_body: response_body,
errors: []
}
......@@ -47,12 +49,20 @@
end
end
shared_examples 'with a markup format option' do
let(:options) { { markup_format: :html } }
it_behaves_like 'triggers ai completion subscription' do
let(:response_body) { '<p data-sourcepos="1:1-1:13" dir="auto">Some response</p>' }
end
end
describe '#execute' do
subject { described_class.new(user, resource, ai_response_json, options: {}).execute }
subject { described_class.new(user, resource, ai_response_json, options: options).execute }
context 'without user' do
let_it_be(:resource) { create(:merge_request, source_project: project) }
let_it_be(:resource) { create(:merge_request, source_project: project) }
context 'without user' do
let(:user) { nil }
it 'does not broadcast subscription' do
......@@ -63,21 +73,29 @@
end
context 'for a merge request' do
let_it_be(:resource) { create(:merge_request, source_project: project) }
it_behaves_like 'triggers ai completion subscription'
it_behaves_like 'with a markup format option'
end
context 'for a work item' do
let_it_be(:resource) { create(:work_item, project: project) }
it_behaves_like 'triggers ai completion subscription'
it_behaves_like 'with a markup format option'
end
context 'for an issue' do
let_it_be(:resource) { create(:issue, project: project) }
it_behaves_like 'triggers ai completion subscription'
it_behaves_like 'with a markup format option'
end
context 'for an epic' do
let_it_be(:resource) { create(:epic, group: group) }
it_behaves_like 'triggers ai completion subscription'
it_behaves_like 'with a markup format option'
end
end
end
......@@ -37,7 +37,7 @@
it 'successfully performs an explain code request' do
expect(Llm::CompletionWorker).to receive(:perform_async).with(
current_user.id, project.id, "Project", :explain_code, { messages: messages }
current_user.id, project.id, "Project", :explain_code, { markup_format: :raw, messages: messages }
)
post_graphql_mutation(mutation, current_user: current_user)
......
......@@ -12,7 +12,7 @@
let(:current_user) { nil }
let(:subscribe) { get_subscription(resource, current_user) }
let(:ai_completion_response) { graphql_dig_at(graphql_data(response[:result]), :aiCompletionResponse) }
let(:ai_completion_response) { graphql_dig_at(graphql_data(response[:result]), :ai_completion_response) }
before do
stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment