diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index ee3acb790d990b2713ad406e6239751b60da36aa..b336a70cd7574bbc59b075f41d0f7fe06415e18a 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -346,6 +346,7 @@ Returns [`ContextPreset`](#contextpreset). | Name | Type | Description | | ---- | ---- | ----------- | +| `forAgenticChat` | [`Boolean`](#boolean) | Set to true if information retrieved is for agentic chat. | | `projectId` | [`ProjectID`](#projectid) | Global ID of the project the user is acting on. | | `questionCount` | [`Int`](#int) | Number of questions for the default screen. | | `resourceId` | [`AiModelID`](#aimodelid) | Global ID of the resource from the current page. | diff --git a/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue b/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue index bb194127064222548ef42002add9d6f64dd4f521..baafcba9edbe5df4d1dc282eee68b9caf71f346b 100644 --- a/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue +++ b/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue @@ -140,6 +140,7 @@ export default { projectId: this.projectId, url: typeof window !== 'undefined' && window.location ? window.location.href : '', questionCount: 4, + forAgenticChat: true, }; }, update(data) { @@ -306,13 +307,21 @@ export default { return this.contextPresets.questions || []; }, additionalContext() { - if (!this.contextPresets.aiResourceData) { - return null; + // Build page context with current URL and title for base context + const contextParts = [ + `${typeof window !== 'undefined' && window.location ? window.location.pathname : ''}`, + `${typeof document !== 'undefined' ? document.title : ''}`, + ]; + + if (this.contextPresets.aiResourceData) { + contextParts.push(this.contextPresets.aiResourceData); } + const pageContext = contextParts.join('\n'); + return [ { - content: this.contextPresets.aiResourceData, + content: pageContext, // This field depends on INCLUDE_{CATEGORY}_CONTEXT unit primitive: // https://gitlab.com/gitlab-org/cloud-connector/gitlab-cloud-connector/-/blob/main/src/python/gitlab_cloud_connector/data_model/gitlab_unit_primitives.py?ref_type=heads#L37-47 // Since there is no unit primitives for all resource types and there is no a general one, let's use the one for repository diff --git a/ee/app/assets/javascripts/ai/graphql/get_ai_chat_context_presets.query.graphql b/ee/app/assets/javascripts/ai/graphql/get_ai_chat_context_presets.query.graphql index 195288765d1900783ef8191e3e362659ef116a5d..ed0ac3a22a7877f38ff90a662f2587b3e9ebae70 100644 --- a/ee/app/assets/javascripts/ai/graphql/get_ai_chat_context_presets.query.graphql +++ b/ee/app/assets/javascripts/ai/graphql/get_ai_chat_context_presets.query.graphql @@ -3,12 +3,14 @@ query getAiChatContextPresets( $projectId: ProjectID $url: String $questionCount: Int + $forAgenticChat: Boolean ) { aiChatContextPresets( resourceId: $resourceId projectId: $projectId url: $url questionCount: $questionCount + forAgenticChat: $forAgenticChat ) { questions aiResourceData diff --git a/ee/app/graphql/resolvers/ai/chat/context_presets_resolver.rb b/ee/app/graphql/resolvers/ai/chat/context_presets_resolver.rb index 705de658ac9212b079e30c286c695375a3fa27f0..ec7d104f9f4e68e84be57087569c9a1902d4bf0c 100644 --- a/ee/app/graphql/resolvers/ai/chat/context_presets_resolver.rb +++ b/ee/app/graphql/resolvers/ai/chat/context_presets_resolver.rb @@ -18,6 +18,11 @@ class ContextPresetsResolver < BaseResolver required: false, description: 'URL of the page the user is currently on.' + argument :for_agentic_chat, + GraphQL::Types::Boolean, + required: false, + description: 'Set to true if information retrieved is for agentic chat.' + argument :resource_id, ::Types::GlobalIDType[::Ai::Model], required: false, @@ -27,14 +32,14 @@ class ContextPresetsResolver < BaseResolver required: false, description: "Global ID of the project the user is acting on." - def resolve(url: nil, resource_id: nil, project_id: nil, question_count: 4) + def resolve(url: nil, resource_id: nil, project_id: nil, question_count: 4, for_agentic_chat: false) ai_resource = find_ai_resource(resource_id, project_id) questions = ::Gitlab::Duo::Chat::DefaultQuestions.new(current_user, url: url, resource: ai_resource) .execute { questions: questions.sample(question_count), - ai_resource_data: ai_resource&.serialize_for_ai&.to_json + ai_resource_data: ai_resource&.serialize_for_ai(for_agentic_chat: for_agentic_chat)&.to_json } end diff --git a/ee/app/models/ai/ai_resource/base_ai_resource.rb b/ee/app/models/ai/ai_resource/base_ai_resource.rb index 875c6917eb646ccf247a73959b6f4bea290bff5d..51c98afa6b9eed0c10572aa1a256acee78bb3e77 100644 --- a/ee/app/models/ai/ai_resource/base_ai_resource.rb +++ b/ee/app/models/ai/ai_resource/base_ai_resource.rb @@ -15,7 +15,7 @@ def initialize(user, resource) @current_user = user end - def serialize_for_ai(_content_limit: default_content_limit) + def serialize_for_ai(_content_limit: default_content_limit, _for_agentic_chat: false) raise NotImplementedError end diff --git a/ee/app/models/ai/ai_resource/commit.rb b/ee/app/models/ai/ai_resource/commit.rb index 6fed7969a66ac2479f181a746209ed3b2c448426..cb00619ed78453d63af5966684e0dece9aab144a 100644 --- a/ee/app/models/ai/ai_resource/commit.rb +++ b/ee/app/models/ai/ai_resource/commit.rb @@ -14,7 +14,9 @@ class Commit < Ai::AiResource::BaseAiResource CHAT_UNIT_PRIMITIVE = :ask_commit - def serialize_for_ai(content_limit: default_content_limit) + def serialize_for_ai(content_limit: default_content_limit, for_agentic_chat: false) + return {} if for_agentic_chat + EE::CommitSerializer # rubocop:disable CodeReuse/Serializer -- existing serializer .new(current_user: current_user, project: resource.project) .represent(resource, { diff --git a/ee/app/models/ai/ai_resource/epic.rb b/ee/app/models/ai/ai_resource/epic.rb index 1f57afac643b44322ba892d6e9e231907e257dff..eb0f69e0a1d3815fb2bbecb42f0cc0da162e2b67 100644 --- a/ee/app/models/ai/ai_resource/epic.rb +++ b/ee/app/models/ai/ai_resource/epic.rb @@ -14,7 +14,9 @@ class Epic < Ai::AiResource::BaseAiResource CHAT_UNIT_PRIMITIVE = :ask_epic - def serialize_for_ai(content_limit: default_content_limit) + def serialize_for_ai(content_limit: default_content_limit, for_agentic_chat: false) + return {} if for_agentic_chat + ::EpicSerializer.new(current_user: current_user) # rubocop: disable CodeReuse/Serializer .represent(resource, { user: current_user, diff --git a/ee/app/models/ai/ai_resource/issue.rb b/ee/app/models/ai/ai_resource/issue.rb index a8648e4b05ef7ef933b7059a0f32225b8e6adf42..9b234b34efdf3c0fa3269241b52390b8f1e0e77c 100644 --- a/ee/app/models/ai/ai_resource/issue.rb +++ b/ee/app/models/ai/ai_resource/issue.rb @@ -14,7 +14,9 @@ class Issue < Ai::AiResource::BaseAiResource CHAT_UNIT_PRIMITIVE = :ask_issue - def serialize_for_ai(content_limit: default_content_limit) + def serialize_for_ai(content_limit: default_content_limit, for_agentic_chat: false) + return {} if for_agentic_chat + ::IssueSerializer.new(current_user: current_user, project: resource.project) # rubocop: disable CodeReuse/Serializer .represent(resource, { user: current_user, diff --git a/ee/app/models/ai/ai_resource/merge_request.rb b/ee/app/models/ai/ai_resource/merge_request.rb index db9829479a75c7f007102467b58d3943b5777f8e..b9a1587a800f7b34b4bd26efcd792da28ad1c45b 100644 --- a/ee/app/models/ai/ai_resource/merge_request.rb +++ b/ee/app/models/ai/ai_resource/merge_request.rb @@ -14,7 +14,9 @@ class MergeRequest < Ai::AiResource::BaseAiResource CHAT_UNIT_PRIMITIVE = :ask_merge_request - def serialize_for_ai(content_limit: default_content_limit, is_duo_code_review: false) + def serialize_for_ai(content_limit: default_content_limit, is_duo_code_review: false, for_agentic_chat: false) + return {} if for_agentic_chat + ::MergeRequestSerializer.new(current_user: current_user) # rubocop: disable CodeReuse/Serializer -- existing serializer .represent(resource, { user: current_user, diff --git a/ee/app/models/ai/ai_resource/work_item.rb b/ee/app/models/ai/ai_resource/work_item.rb index d8cf0fc8ca743409e00a7796266cdf403e227333..610271dc20623f526214a6b49ed831d56da4d1e5 100644 --- a/ee/app/models/ai/ai_resource/work_item.rb +++ b/ee/app/models/ai/ai_resource/work_item.rb @@ -3,7 +3,9 @@ module Ai module AiResource class WorkItem < Ai::AiResource::Issue - def serialize_for_ai(content_limit: default_content_limit) + def serialize_for_ai(content_limit: default_content_limit, for_agentic_chat: false) + return {} if for_agentic_chat + synced_epic = resource.synced_epic if synced_epic ::EpicSerializer.new(current_user: current_user) # rubocop: disable CodeReuse/Serializer -- we need to serialize resource here diff --git a/ee/spec/frontend/ai/duo_agentic_chat/components/duo_agentic_chat_spec.js b/ee/spec/frontend/ai/duo_agentic_chat/components/duo_agentic_chat_spec.js index 19977ddf1e35f7ff72ead9f836dca4fd8eb7a1ea..977c8e29ae888f4c19970471eda511659e93a801 100644 --- a/ee/spec/frontend/ai/duo_agentic_chat/components/duo_agentic_chat_spec.js +++ b/ee/spec/frontend/ai/duo_agentic_chat/components/duo_agentic_chat_spec.js @@ -209,7 +209,9 @@ const MOCK_UTILS_SETUP = () => { const expectedAdditionalContext = [ { - content: MOCK_AI_RESOURCE_DATA, + content: `/ + +${MOCK_AI_RESOURCE_DATA}`, category: DUO_WORKFLOW_ADDITIONAL_CONTEXT_REPOSITORY, metadata: JSON.stringify({}), }, @@ -402,6 +404,7 @@ describe('Duo Agentic Chat', () => { it('calls the context presets GraphQL query when component loads', () => { expect(contextPresetsQueryHandlerMock).toHaveBeenCalledWith({ + forAgenticChat: true, projectId: MOCK_PROJECT_ID, resourceId: MOCK_RESOURCE_ID, url: 'http://test.host/', diff --git a/ee/spec/models/ai/ai_resource/base_ai_resource_spec.rb b/ee/spec/models/ai/ai_resource/base_ai_resource_spec.rb index fe6c12d6c91307b95d9083332f119e4d26017270..74663dc33a59638e8d8256a6010edf28e0a7e27f 100644 --- a/ee/spec/models/ai/ai_resource/base_ai_resource_spec.rb +++ b/ee/spec/models/ai/ai_resource/base_ai_resource_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Ai::AiResource::BaseAiResource, feature_category: :duo_chat do describe '#serialize_for_ai' do it 'raises NotImplementedError' do - expect { described_class.new(nil, nil).serialize_for_ai(_content_limit: nil) } + expect { described_class.new(nil, nil).serialize_for_ai(_content_limit: nil, _for_agentic_chat: nil) } .to raise_error(NotImplementedError) end end diff --git a/ee/spec/models/ai/ai_resource/epic_spec.rb b/ee/spec/models/ai/ai_resource/epic_spec.rb index 2a77646c74f63b30be506c956d0646f17262456d..9bf22926eb9a32d9809e7bd3af551dd2db5ce866 100644 --- a/ee/spec/models/ai/ai_resource/epic_spec.rb +++ b/ee/spec/models/ai/ai_resource/epic_spec.rb @@ -9,17 +9,26 @@ subject(:wrapped_epic) { described_class.new(user, epic) } describe '#serialize_for_ai' do - it 'calls the serializations class' do - expect(EpicSerializer).to receive_message_chain(:new, :represent) - .with(current_user: user) - .with(epic, { - user: user, - notes_limit: 100, - serializer: 'ai', - resource: wrapped_epic - }) + context 'when for_agentic_chat is true' do + it 'returns empty hash' do + result = wrapped_epic.serialize_for_ai(for_agentic_chat: true) + expect(result).to eq({}) + end + end + + context 'when for_agentic_chat is false or omitted' do + it 'calls the serializations class' do + expect(EpicSerializer).to receive_message_chain(:new, :represent) + .with(current_user: user) + .with(epic, { + user: user, + notes_limit: 100, + serializer: 'ai', + resource: wrapped_epic + }) - wrapped_epic.serialize_for_ai(content_limit: 100) + wrapped_epic.serialize_for_ai(content_limit: 100) + end end end diff --git a/ee/spec/models/ai/ai_resource/issue_spec.rb b/ee/spec/models/ai/ai_resource/issue_spec.rb index 1dad522b48b3c2b0da78be0c19a16acc3557bc17..d7c99caf292c7f96a7b9dfb7fee634b07807bee8 100644 --- a/ee/spec/models/ai/ai_resource/issue_spec.rb +++ b/ee/spec/models/ai/ai_resource/issue_spec.rb @@ -9,16 +9,25 @@ subject(:wrapped_issue) { described_class.new(user, issue) } describe '#serialize_for_ai' do - it 'calls the serializations class' do - expect(::IssueSerializer).to receive_message_chain(:new, :represent) - .with(current_user: user, project: issue.project) - .with(issue, { - user: user, - notes_limit: 100, - serializer: 'ai', - resource: wrapped_issue - }) - wrapped_issue.serialize_for_ai(content_limit: 100) + context 'when for_agentic_chat is true' do + it 'returns empty hash' do + result = wrapped_issue.serialize_for_ai(for_agentic_chat: true) + expect(result).to eq({}) + end + end + + context 'when for_agentic_chat is false or omitted' do + it 'calls the serializations class' do + expect(::IssueSerializer).to receive_message_chain(:new, :represent) + .with(current_user: user, project: issue.project) + .with(issue, { + user: user, + notes_limit: 100, + serializer: 'ai', + resource: wrapped_issue + }) + wrapped_issue.serialize_for_ai(content_limit: 100) + end end end diff --git a/ee/spec/models/ai/ai_resource/merge_request_spec.rb b/ee/spec/models/ai/ai_resource/merge_request_spec.rb index 7fee06ce702fecb7313fb9cf1c5bf10e88ccd052..ef3ed8977bb88c53caeca32bf02654c4343637ba 100644 --- a/ee/spec/models/ai/ai_resource/merge_request_spec.rb +++ b/ee/spec/models/ai/ai_resource/merge_request_spec.rb @@ -9,32 +9,41 @@ subject(:wrapped_merge_request) { described_class.new(user, merge_request) } describe '#serialize_for_ai' do - it 'calls the serializations class' do - expect(MergeRequestSerializer).to receive_message_chain(:new, :represent) - .with(current_user: user) - .with(merge_request, { - user: user, - notes_limit: 100, - serializer: 'ai', - resource: wrapped_merge_request, - is_duo_code_review: false - }) - - wrapped_merge_request.serialize_for_ai(content_limit: 100) + context 'when for_agentic_chat is true' do + it 'returns empty hash' do + result = wrapped_merge_request.serialize_for_ai(for_agentic_chat: true) + expect(result).to eq({}) + end end - it 'passes is_duo_code_review parameter to serializer' do - expect(MergeRequestSerializer).to receive_message_chain(:new, :represent) - .with(current_user: user) - .with(merge_request, { - user: user, - notes_limit: 100, - serializer: 'ai', - resource: wrapped_merge_request, - is_duo_code_review: true - }) - - wrapped_merge_request.serialize_for_ai(content_limit: 100, is_duo_code_review: true) + context 'when for_agentic_chat is false or omitted' do + it 'calls the serializations class' do + expect(MergeRequestSerializer).to receive_message_chain(:new, :represent) + .with(current_user: user) + .with(merge_request, { + user: user, + notes_limit: 100, + serializer: 'ai', + resource: wrapped_merge_request, + is_duo_code_review: false + }) + + wrapped_merge_request.serialize_for_ai(content_limit: 100) + end + + it 'passes is_duo_code_review parameter to serializer' do + expect(MergeRequestSerializer).to receive_message_chain(:new, :represent) + .with(current_user: user) + .with(merge_request, { + user: user, + notes_limit: 100, + serializer: 'ai', + resource: wrapped_merge_request, + is_duo_code_review: true + }) + + wrapped_merge_request.serialize_for_ai(content_limit: 100, is_duo_code_review: true) + end end end diff --git a/ee/spec/models/ai/ai_resource/work_item_spec.rb b/ee/spec/models/ai/ai_resource/work_item_spec.rb index 0b6ac508c6f4f6f63a2b2fed29a1c9e4b8c886b2..00b6cde578523fb88dc20694a74305532d5bd44b 100644 --- a/ee/spec/models/ai/ai_resource/work_item_spec.rb +++ b/ee/spec/models/ai/ai_resource/work_item_spec.rb @@ -9,43 +9,52 @@ subject(:wrapped_work_item) { described_class.new(user, work_item) } describe '#serialize_for_ai' do - context 'when issue is synced with epic' do - let(:epic) { build(:epic) } + context 'when for_agentic_chat is true' do + it 'returns empty hash' do + result = wrapped_work_item.serialize_for_ai(for_agentic_chat: true) + expect(result).to eq({}) + end + end + + context 'when for_agentic_chat is false or omitted' do + context 'when issue is synced with epic' do + let(:epic) { build(:epic) } + + before do + work_item.synced_epic = epic + end - before do - work_item.synced_epic = epic + it 'calls the epics serializations class' do + expect(::EpicSerializer).to receive_message_chain(:new, :represent) + .with(current_user: user) + .with(epic, { + user: user, + notes_limit: 100, + serializer: 'ai', + resource: wrapped_work_item + }) + wrapped_work_item.serialize_for_ai(content_limit: 100) + end end - it 'calls the epics serializations class' do - expect(::EpicSerializer).to receive_message_chain(:new, :represent) - .with(current_user: user) - .with(epic, { - user: user, - notes_limit: 100, - serializer: 'ai', - resource: wrapped_work_item - }) + it 'calls the serializations class' do + expect(::IssueSerializer).to receive_message_chain(:new, :represent) + .with(current_user: user, project: work_item.project) + .with(work_item, { + user: user, + notes_limit: 100, + serializer: 'ai', + resource: wrapped_work_item + }) wrapped_work_item.serialize_for_ai(content_limit: 100) end - end - - it 'calls the serializations class' do - expect(::IssueSerializer).to receive_message_chain(:new, :represent) - .with(current_user: user, project: work_item.project) - .with(work_item, { - user: user, - notes_limit: 100, - serializer: 'ai', - resource: wrapped_work_item - }) - wrapped_work_item.serialize_for_ai(content_limit: 100) - end - context 'when content_limit is omitted' do - let(:work_item) { create(:work_item) } + context 'when content_limit is omitted' do + let(:work_item) { create(:work_item) } - it 'does not raise error' do - expect { wrapped_work_item.serialize_for_ai }.not_to raise_error + it 'does not raise error' do + expect { wrapped_work_item.serialize_for_ai }.not_to raise_error + end end end end