diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 4abf36ac582f61dbce75eeb6b2fd82b15019c780..e3d5de80f68f8e218c7ef94d1a3e8774ca10e674 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -334,6 +334,7 @@ Returns [`AvailableModels`](#availablemodels). | Name | Type | Description | | ---- | ---- | ----------- | +| `projectId` | [`ProjectID`](#projectid) | Global ID of the project the user is acting on. | | `rootNamespaceId` | [`GroupID!`](#groupid) | Global ID of the namespace the user is acting on. | ### `Query.aiChatContextPresets` 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 4b468abd92a5d9bcd60be505e5e41f221b51978b..8af978c34aae5c2e62f569c9ad2fa29c35a98169 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 @@ -159,6 +159,7 @@ export default { variables() { return { rootNamespaceId: this.rootNamespaceId, + projectId: this.projectId, }; }, update(data) { diff --git a/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql b/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql index def23d46b46020f4dc1815ba8213a9f3bde77b5e..289af54d2bed74d2a8ceb607bc13e3aa62daba69 100644 --- a/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql +++ b/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql @@ -1,5 +1,5 @@ -query getAiChatAvailableModels($rootNamespaceId: GroupID!) { - aiChatAvailableModels(rootNamespaceId: $rootNamespaceId) { +query getAiChatAvailableModels($rootNamespaceId: GroupID!, $projectId: ProjectID) { + aiChatAvailableModels(rootNamespaceId: $rootNamespaceId, projectId: $projectId) { defaultModel { name ref diff --git a/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb b/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb index 2650f8c4ba80eeccc0809dd512d31114dbb07c2f..681ce4e7e51fc218a985adb813becbbe634ffa58 100644 --- a/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb +++ b/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb @@ -15,8 +15,18 @@ class AvailableModelsResolver < BaseResolver required: true, description: 'Global ID of the namespace the user is acting on.' - def resolve(root_namespace_id:) - namespace = authorized_find!(id: root_namespace_id) + argument :project_id, + ::Types::GlobalIDType[::Project], + required: false, + description: 'Global ID of the project the user is acting on.' + + def resolve(root_namespace_id:, project_id: nil) + if project_id + project = authorized_find!(id: project_id) + namespace = project.root_namespace + else + namespace = authorized_find!(id: root_namespace_id) + end result = ::Ai::ModelSelection::FetchModelDefinitionsService .new(current_user, model_selection_scope: namespace) diff --git a/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb b/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb index fbe71301fe6a8c122ccae217ad685035bc438bd2..8894acdf5e069362d16d2a6cb4b7640e29aff0ca 100644 --- a/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb @@ -21,6 +21,61 @@ .and_return(true) end + shared_examples "returns model selection data" do + it "returns the correct structure with default and selectable models" do + expect(resolver).to eq(expected_result) + end + end + + shared_examples "returns a ResourceNotAvailable error" do + it "generates an error" do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + resolver + end + end + end + + describe "the resource is not authorized" do + context "when the resource is a root namespace" do + context "when the namespace does not have duo agentic chat enabled" do + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, group) + .and_return(false) + end + + include_examples "returns a ResourceNotAvailable error" + end + end + + context "when the resource is a project" do + context "when the namespace has duo agentic chat enabled" do + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, group) + .and_return(true) + end + + context "when the child project does not have duo agentic chat enabled" do + let_it_be(:project) { create(:project, group: group) } + + let(:args) do + { root_namespace_id: GitlabSchema.id_from_object(group), + project_id: GitlabSchema.id_from_object(project) } + end + + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, project) + .and_return(false) + end + + include_examples "returns a ResourceNotAvailable error" + end + end + end + end + context "when service returns successful result" do let(:service_result) do ServiceResponse.success(payload: { @@ -41,21 +96,54 @@ }) end - before do - allow_next_instance_of(::Ai::ModelSelection::FetchModelDefinitionsService) do |service| - allow(service).to receive(:execute).and_return(service_result) - end - end - - it "returns the correct structure with default and selectable models" do - expect(resolver).to eq({ + let(:expected_result) do + { default_model: { name: "Claude Sonnet 4.0 - Anthropic", ref: "claude_sonnet_4_20250514" }, selectable_models: [ { name: "Claude Sonnet 4.0 - Anthropic", ref: "claude_sonnet_4_20250514" }, { name: "Claude Sonnet 3.7 - Anthropic", ref: "claude_sonnet_3_7_20250219" } ], pinned_model: nil - }) + } + end + + before do + allow_next_instance_of(::Ai::ModelSelection::FetchModelDefinitionsService) do |service| + allow(service).to receive(:execute).and_return(service_result) + end + end + + context "when the resource is a root namespace" do + context "when the namespace has duo agentic chat enabled" do + include_examples "returns model selection data" + end + end + + context "when the resource is a project" do + context "when the namespace does not have duo agentic chat enabled" do + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, group) + .and_return(false) + end + + context "when the child project has duo agentic chat enabled" do + let_it_be(:project) { create(:project, group: group) } + + let(:args) do + { root_namespace_id: GitlabSchema.id_from_object(group), + project_id: GitlabSchema.id_from_object(project) } + end + + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :access_duo_agentic_chat, project) + .and_return(true) + end + + include_examples "returns model selection data" + end + end end context "when there is a pinned model" do