diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 80dd9b1f0b7f90066c9fbd3fa63e6c80527c22b2..35823caa1695972dc29b258cb1b0231b08668c99 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -598,6 +598,10 @@ def all_catalog_resources Ci::Catalog::Resource.where(project: all_projects) end + def all_ai_catalog_items + ::Ai::Catalog::Item.where(project: all_projects) + end + def all_projects_except_soft_deleted all_projects.not_aimed_for_deletion end diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index f3c6b2dacc49428797e87d824ac813bfaf4f0da3..680a38562bd300eee191a511ceb420aecc771fe0 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -336,6 +336,7 @@ four standard [pagination arguments](#pagination-arguments): | ---- | ---- | ----------- | | `itemType` | [`AiCatalogItemType`](#aicatalogitemtype) | Type of items to retrieve. | | `search` | [`String`](#string) | Search items by name and description. | +| `verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Filter items by verification level. | ### `Query.aiChatAvailableModels` @@ -23505,6 +23506,7 @@ An AI catalog agent. | `public` | [`Boolean!`](#boolean) | Whether the item is publicly visible in the catalog. | | `updatedAt` | [`Time!`](#time) | Timestamp of when the item was updated. | | `userPermissions` | [`AiCatalogItemPermissions!`](#aicatalogitempermissions) | Permissions for the current user on the resource. | +| `verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Verification level of the item. | | `versions` | [`AiCatalogItemVersionConnection`](#aicatalogitemversionconnection) | Versions of the item. (see [Connections](#connections)) | #### Fields with arguments @@ -23570,6 +23572,7 @@ An AI catalog flow. | `public` | [`Boolean!`](#boolean) | Whether the item is publicly visible in the catalog. | | `updatedAt` | [`Time!`](#time) | Timestamp of when the item was updated. | | `userPermissions` | [`AiCatalogItemPermissions!`](#aicatalogitempermissions) | Permissions for the current user on the resource. | +| `verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Verification level of the item. | | `versions` | [`AiCatalogItemVersionConnection`](#aicatalogitemversionconnection) | Versions of the item. (see [Connections](#connections)) | #### Fields with arguments @@ -52708,6 +52711,7 @@ Implementations: | `public` | [`Boolean!`](#boolean) | Whether the item is publicly visible in the catalog. | | `updatedAt` | [`Time!`](#time) | Timestamp of when the item was updated. | | `userPermissions` | [`AiCatalogItemPermissions!`](#aicatalogitempermissions) | Permissions for the current user on the resource. | +| `verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Verification level of the item. | | `versions` | [`AiCatalogItemVersionConnection`](#aicatalogitemversionconnection) | Versions of the item. (see [Connections](#connections)) | ##### Fields with arguments diff --git a/ee/app/finders/ai/catalog/items_finder.rb b/ee/app/finders/ai/catalog/items_finder.rb index 1f9de99777d53c561427f36c0a01c10f9fcc284a..54b671330117d0115844c73c846c270f132457a5 100644 --- a/ee/app/finders/ai/catalog/items_finder.rb +++ b/ee/app/finders/ai/catalog/items_finder.rb @@ -15,6 +15,7 @@ def execute items = init_collection items = by_organization(items) items = by_item_type(items) + items = by_verification_level(items) by_search(items) end @@ -38,6 +39,12 @@ def by_item_type(items) items.with_item_type(params[:item_type]) end + def by_verification_level(items) + return items unless params[:verification_level] + + items.for_verification_level(params[:verification_level]) + end + def by_search(items) return items if params[:search].blank? diff --git a/ee/app/graphql/resolvers/ai/catalog/items_resolver.rb b/ee/app/graphql/resolvers/ai/catalog/items_resolver.rb index 4ca9e8ac16de1ecc475a3c2a8776d1c441f4d937..fab67cb6a2071a7be2907617b023587baa4b2904 100644 --- a/ee/app/graphql/resolvers/ai/catalog/items_resolver.rb +++ b/ee/app/graphql/resolvers/ai/catalog/items_resolver.rb @@ -18,6 +18,10 @@ class ItemsResolver < BaseResolver required: false, description: 'Search items by name and description.' + argument :verification_level, ::Types::Namespaces::VerificationLevelEnum, + required: false, + description: 'Filter items by verification level.' + def resolve_with_lookahead(**args) items = ::Ai::Catalog::ItemsFinder.new( current_user, diff --git a/ee/app/graphql/types/ai/catalog/item_interface.rb b/ee/app/graphql/types/ai/catalog/item_interface.rb index 7ffe24acbf1cfec752ae9d5d180a8ebabb68b69e..dfcd9367c45bd7c9f252c9758be31013121591a9 100644 --- a/ee/app/graphql/types/ai/catalog/item_interface.rb +++ b/ee/app/graphql/types/ai/catalog/item_interface.rb @@ -41,6 +41,9 @@ module ItemInterface argument :released, ::GraphQL::Types::Boolean, required: false, description: 'Return the latest released version.' end + field :verification_level, ::Types::Namespaces::VerificationLevelEnum, + null: true, + description: 'Verification level of the item.' orphan_types ::Types::Ai::Catalog::AgentType orphan_types ::Types::Ai::Catalog::FlowType diff --git a/ee/app/services/ai/catalog/agents/create_service.rb b/ee/app/services/ai/catalog/agents/create_service.rb index 083f96c7ba7c190feb1b1c1a458708991e6b212c..713761135828aab28e2b5b3e2404b46abbdf6094 100644 --- a/ee/app/services/ai/catalog/agents/create_service.rb +++ b/ee/app/services/ai/catalog/agents/create_service.rb @@ -11,7 +11,8 @@ def execute item_params.merge!( item_type: Ai::Catalog::Item::AGENT_TYPE, organization_id: project.organization_id, - project_id: project.id + project_id: project.id, + verification_level: verification_level_for_project ) version_params = { diff --git a/ee/app/services/ai/catalog/base_service.rb b/ee/app/services/ai/catalog/base_service.rb index f85656f597b3167ab9085571b8432796e2f76902..2b83de9d4f7787b9ea4396a2df32af1d1a782d67 100644 --- a/ee/app/services/ai/catalog/base_service.rb +++ b/ee/app/services/ai/catalog/base_service.rb @@ -33,6 +33,11 @@ def track_ai_item_events(event_type, additional_properties = {}) additional_properties: additional_properties ) end + + def verification_level_for_project + verified_namespace = project.root_namespace.catalog_verified_namespace + verified_namespace&.verification_level || :unverified + end end end end diff --git a/ee/app/services/ai/catalog/flows/create_service.rb b/ee/app/services/ai/catalog/flows/create_service.rb index 626e96d62118b9bb031fbc5cb67b9049b25ceebe..9bbff7471885d72c847694f02a95f9a40b0b28a5 100644 --- a/ee/app/services/ai/catalog/flows/create_service.rb +++ b/ee/app/services/ai/catalog/flows/create_service.rb @@ -16,7 +16,8 @@ def execute item_params.merge!( item_type: Ai::Catalog::Item::FLOW_TYPE, organization_id: project.organization_id, - project_id: project.id + project_id: project.id, + verification_level: verification_level_for_project ) version_params = { schema_version: ::Ai::Catalog::ItemVersion::FLOW_SCHEMA_VERSION, diff --git a/ee/app/services/namespaces/verify_namespace_service.rb b/ee/app/services/namespaces/verify_namespace_service.rb index 5ec80ecba4c17a4563ace700f44fb1ecc3251923..9030b3ebc8f5a36ea40aa2be4b3a13c8d6e28e2e 100644 --- a/ee/app/services/namespaces/verify_namespace_service.rb +++ b/ee/app/services/namespaces/verify_namespace_service.rb @@ -16,6 +16,7 @@ def execute create_or_update_verified_namespace update_catalog_resources + update_ai_catalog_items ServiceResponse.success end @@ -55,5 +56,9 @@ def create_or_update_verified_namespace def update_catalog_resources namespace.all_catalog_resources.update_all(verification_level: verification_level) end + + def update_ai_catalog_items + namespace.all_ai_catalog_items.update_all(verification_level: verification_level) + end end end diff --git a/ee/spec/finders/ai/catalog/items_finder_spec.rb b/ee/spec/finders/ai/catalog/items_finder_spec.rb index 83cb3618b00a223d0910041c4e7513d1e0644573..8943cedc9bd20aa98bc6da3df7d86d0cde20162b 100644 --- a/ee/spec/finders/ai/catalog/items_finder_spec.rb +++ b/ee/spec/finders/ai/catalog/items_finder_spec.rb @@ -98,6 +98,34 @@ end end + context 'when filtering by verification_level' do + ::Namespaces::VerifiedNamespace::VERIFICATION_LEVELS.each_key do |level| + context "with #{level} verification level" do + let(:params) { { verification_level: level } } + + let!(:matching_item) { create(:ai_catalog_flow, public: true, verification_level: level) } + + let!(:non_matching_item) do + other_levels = ::Namespaces::VerifiedNamespace::VERIFICATION_LEVELS.keys - [level] + + create(:ai_catalog_flow, public: true, verification_level: other_levels.first) + end + + it "returns only items with #{level} verification level" do + expect(results.map(&:verification_level).uniq).to eq([level.to_s]) + end + end + end + + context 'with invalid verification level' do + let(:params) { { verification_level: 'invalid_level' } } + + it 'returns empty result' do + is_expected.to be_empty + end + end + end + describe 'ordering' do let_it_be(:gitlab_item_1) { create(:ai_catalog_agent, public: true) } let_it_be(:regular_item_1) { create(:ai_catalog_agent, public: true) } diff --git a/ee/spec/graphql/types/ai/catalog/item_interface_spec.rb b/ee/spec/graphql/types/ai/catalog/item_interface_spec.rb index 1cb8e2d61c7ea11b5bf0899b97949d35eb9da0c0..09f7381f80c171ba7177c8dc20cbd1bfc91398d8 100644 --- a/ee/spec/graphql/types/ai/catalog/item_interface_spec.rb +++ b/ee/spec/graphql/types/ai/catalog/item_interface_spec.rb @@ -15,6 +15,7 @@ item_type name latest_version + verification_level project public updated_at diff --git a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb index 95f0ecf0bad36e2a3e349312e09cd06bae69410f..e3ee030e605b65415595de3953093951ce143df1 100644 --- a/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb +++ b/ee/spec/requests/api/graphql/ai/catalog/item_spec.rb @@ -51,6 +51,7 @@ itemType name public + verificationLevel project { id } latestVersion { ...VersionFragment @@ -82,6 +83,7 @@ 'description' => catalog_item.description, 'itemType' => 'AGENT', 'public' => catalog_item.public, + 'verificationLevel' => catalog_item.verification_level.upcase, 'latestVersion' => a_graphql_entity_for(latest_version), 'versions' => hash_including( 'count' => 1, diff --git a/ee/spec/services/ai/catalog/agents/create_service_spec.rb b/ee/spec/services/ai/catalog/agents/create_service_spec.rb index 8dfda441ac2fd629eafdb5487ea5e6569b0d5cb9..1e755f901b3150982c4aaa88454c0cf36a2404cc 100644 --- a/ee/spec/services/ai/catalog/agents/create_service_spec.rb +++ b/ee/spec/services/ai/catalog/agents/create_service_spec.rb @@ -6,7 +6,7 @@ include Ai::Catalog::TestHelpers let_it_be(:maintainer) { create(:user) } - let_it_be(:project) { create(:project, maintainers: maintainer) } + let_it_be_with_reload(:project) { create(:project, maintainers: maintainer) } let(:user) { maintainer } let(:params) do @@ -59,7 +59,8 @@ name: params[:name], description: params[:description], public: true, - item_type: Ai::Catalog::Item::AGENT_TYPE.to_s + item_type: Ai::Catalog::Item::AGENT_TYPE.to_s, + verification_level: 'unverified' ) expect(item.latest_version).to have_attributes( schema_version: 1, @@ -129,4 +130,17 @@ end end end + + context 'when project root namespace has a verified namespace' do + let_it_be(:verification_level) { :gitlab_maintained } + + it "assigns verification level to the created agent" do + create(:catalog_verified_namespace, namespace: project.root_namespace, verification_level: verification_level) + + response + + item = Ai::Catalog::Item.last + expect(item.verification_level).to eq(verification_level.to_s) + end + end end diff --git a/ee/spec/services/ai/catalog/flows/create_service_spec.rb b/ee/spec/services/ai/catalog/flows/create_service_spec.rb index 33cbfc446b91fed82a9d37459b82afffa77023e8..3a82aaa8f0f2c759c2d5adb71b8ec41cf81fb3f1 100644 --- a/ee/spec/services/ai/catalog/flows/create_service_spec.rb +++ b/ee/spec/services/ai/catalog/flows/create_service_spec.rb @@ -6,7 +6,7 @@ include Ai::Catalog::TestHelpers let_it_be(:maintainer) { create(:user) } - let_it_be(:project) { create(:project, maintainers: maintainer) } + let_it_be_with_reload(:project) { create(:project, maintainers: maintainer) } let_it_be(:agent) { create(:ai_catalog_agent, project: project) } let_it_be(:v1_0) { create(:ai_catalog_agent_version, item: agent, version: '1.0.0') } let_it_be(:v1_1) { create(:ai_catalog_agent_version, item: agent, version: '1.1.0') } @@ -62,7 +62,8 @@ name: params[:name], description: params[:description], item_type: Ai::Catalog::Item::FLOW_TYPE.to_s, - public: true + public: true, + verification_level: 'unverified' ) expect(item.latest_version).to have_attributes( schema_version: ::Ai::Catalog::ItemVersion::FLOW_SCHEMA_VERSION, @@ -212,4 +213,17 @@ end end end + + context 'when project root namespace has a verified namespace' do + let_it_be(:verification_level) { :gitlab_maintained } + + it "assigns verification level to the created flow" do + create(:catalog_verified_namespace, namespace: project.root_namespace, verification_level: verification_level) + + response + + item = Ai::Catalog::Item.last + expect(item.verification_level).to eq(verification_level.to_s) + end + end end diff --git a/ee/spec/services/namespaces/verify_namespace_service_spec.rb b/ee/spec/services/namespaces/verify_namespace_service_spec.rb index 9219c8d1144f83248feeb192d5fa8b03352c98c5..fc3529383db1421c269e31f6c614cfc9e335a19c 100644 --- a/ee/spec/services/namespaces/verify_namespace_service_spec.rb +++ b/ee/spec/services/namespaces/verify_namespace_service_spec.rb @@ -32,6 +32,10 @@ create(:ci_catalog_resource, :published, project: another_group_published_project) end + let_it_be(:group_project_ai_catalog_item) { create(:ai_catalog_item, project: group_project) } + let_it_be(:subgroup_project_ai_catalog_item) { create(:ai_catalog_item, project: subgroup_project) } + let_it_be(:another_group_ai_catalog_item) { create(:ai_catalog_item, project: another_group_private_project) } + describe '#execute' do context 'when namespace is not a root namespace' do it 'returns error' do @@ -124,6 +128,17 @@ expect(another_group_published_project_resource.reload.verification_level).to eq('unverified') end + + it 'updates the verification level for all AI catalog items under the given namespace' do + response = described_class.new(group, verification_level).execute + + expect(response).to be_success + + expect(group_project_ai_catalog_item.reload.verification_level).to eq(verification_level) + expect(subgroup_project_ai_catalog_item.reload.verification_level).to eq(verification_level) + + expect(another_group_ai_catalog_item.reload.verification_level).to eq('unverified') + end end end end @@ -159,6 +174,20 @@ expect(another_group_published_project_resource.reload.verification_level).to eq('unverified') end + + it 'cascades the verification level to the AI catalog items' do + ::Namespaces::VerifiedNamespace.find_or_create_by!(namespace: group, + verification_level: 'verified_creator_self_managed') + + response = described_class.new(group, verification_level).execute + + expect(response).to be_success + + expect(group_project_ai_catalog_item.reload.verification_level).to eq(verification_level) + expect(subgroup_project_ai_catalog_item.reload.verification_level).to eq(verification_level) + + expect(another_group_ai_catalog_item.reload.verification_level).to eq('unverified') + end end context 'when on gitlab.com' do @@ -201,6 +230,20 @@ expect(another_group_published_project_resource.reload.verification_level).to eq('unverified') end + + it 'cascades the verification level to the AI catalog items' do + ::Namespaces::VerifiedNamespace.find_or_create_by!(namespace: group, + verification_level: 'gitlab_maintained') + + response = described_class.new(group, new_verification_level).execute + + expect(response).to be_success + + expect(group_project_ai_catalog_item.reload.verification_level).to eq(new_verification_level) + expect(subgroup_project_ai_catalog_item.reload.verification_level).to eq(new_verification_level) + + expect(another_group_ai_catalog_item.reload.verification_level).to eq('unverified') + end end end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 0499f14c61d502196c0ec69be9c0b325eef7ef49..2c2404cc19875875212be6ad6db79ede709ecf4d 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -2273,6 +2273,80 @@ end end + describe '#all_ai_catalog_items' do + shared_examples 'retrieves AI catalog items' do + it 'returns AI catalog items from the namespace projects' do + expect(namespace.all_ai_catalog_items).to match_array(expected_items) + end + end + + shared_examples 'returns empty collection when no projects or items' do + it 'returns empty collection' do + expect(namespace.all_ai_catalog_items).to be_empty + end + end + + context 'when namespace is a group' do + let_it_be(:namespace) { create(:group) } + let_it_be(:project1) { create(:project, namespace: namespace) } + let_it_be(:project2) { create(:project, namespace: namespace) } + let_it_be(:ai_catalog_item1) { create(:ai_catalog_item, project: project1, organization: project1.organization) } + let_it_be(:ai_catalog_item2) { create(:ai_catalog_item, project: project2, organization: project2.organization) } + let(:expected_items) { [ai_catalog_item1, ai_catalog_item2] } + + include_examples 'retrieves AI catalog items' + + context 'when namespace has no projects' do + let_it_be(:namespace) { create(:group) } + + include_examples 'returns empty collection when no projects or items' + end + + context 'when projects have no AI catalog items' do + let_it_be(:namespace) { create(:group) } + let_it_be(:project_without_items) { create(:project, namespace: namespace) } + + include_examples 'returns empty collection when no projects or items' + end + + context 'with nested groups' do + let_it_be(:child_group) { create(:group, parent: namespace) } + let_it_be(:child_project) { create(:project, namespace: child_group) } + let_it_be(:child_ai_catalog_item) { create(:ai_catalog_item, project: child_project, organization: child_project.organization) } + let(:expected_items) { [ai_catalog_item1, ai_catalog_item2, child_ai_catalog_item] } + + include_examples 'retrieves AI catalog items' + end + end + + context 'when namespace is a user namespace' do + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { user.namespace } + let_it_be(:project1) { create(:project, namespace: namespace) } + let_it_be(:project2) { create(:project, namespace: namespace) } + let_it_be(:ai_catalog_item1) { create(:ai_catalog_item, project: project1, organization: project1.organization) } + let_it_be(:ai_catalog_item2) { create(:ai_catalog_item, project: project2, organization: project2.organization) } + let(:expected_items) { [ai_catalog_item1, ai_catalog_item2] } + + include_examples 'retrieves AI catalog items' + + context 'when namespace has no projects' do + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { user.namespace } + + include_examples 'returns empty collection when no projects or items' + end + + context 'when projects have no AI catalog items' do + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { user.namespace } + let_it_be(:project_without_items) { create(:project, namespace: namespace) } + + include_examples 'returns empty collection when no projects or items' + end + end + end + describe '#share_with_group_lock with subgroups' do context 'when creating a subgroup' do let(:subgroup) { create(:group, parent: root_group) }