Add restore version as new latest for AI Catalog items
What does this MR do and why?
This MR adds a GraphQL mutation and the service to restore an item_version as the latest item_version. So the idea is: sometimes a user updates their AI catalog item (agent/flow) and then realizes the new version isn't perfect and the previous version was actually better. So they want to restore that particular version as the latest version again, that's why this MR adds the mutation and service to do that. There are some cases where we want to block the user from restoring if those cases are met. For example: the source version is a draft version, or the source version is already the latest version.
These changes are behind a WIP feature flag, which defaults to false.
In the system, the latest version is treated as the best version of the AI catalog item, which is why a user would want to restore a previous version back to latest. And right now, when project enables an item from a different project, it's the latest version that gets used, so that's another reason a user would want to keep their best version as the latest.
References
How to set up and validate locally
Click to expand
Prerequisites
- GDK running
- Access to Rails console (
bin/rails console) - Access to GraphiQL (
http://gdk.test:3000/-/graphql-explorer)
Setup
1. Enable feature flag
Feature.enable(:ai_catalog_version_restore)2. Create test data
user = User.find_by(username: 'root')
project = Project.first
item = Ai::Catalog::Item.new(
name: 'Test Restore Agent',
description: 'Agent for testing version restore',
item_type: :agent,
public: true,
organization: project.organization,
project: project
)
v1_0 = item.versions.build(
version: '1.0.0',
schema_version: 1,
definition: { 'system_prompt' => 'Version 1.0 prompt', 'tools' => [], 'user_prompt' => '' },
release_date: Time.zone.now,
created_by: user
)
item.latest_version = v1_0
Ai::Catalog::Item.transaction do
item.save!
item.update!(latest_released_version: v1_0)
end
v1_1 = item.build_new_version(
version: '1.1.0',
schema_version: 1,
definition: { 'system_prompt' => 'Version 1.1 prompt - BAD VERSION', 'tools' => [], 'user_prompt' => '' },
release_date: Time.zone.now,
created_by: user
)
item.save!
item.update!(latest_released_version: v1_1)
puts "v1.0.0 GlobalID: #{v1_0.to_global_id}"
puts "v1.1.0 GlobalID: #{v1_1.to_global_id}"
puts "Item ID: #{item.id}"Note down the GlobalIDs printed.
Test Cases
TC1: Restore without deprecation
mutation {
aiCatalogItemVersionRestore(input: {
id: "<v1_0_global_id>"
deprecateCurrent: false
}) {
itemVersion {
versionName
released
deprecated
createdBy { username }
}
errors
}
}Expected:
errors: emptyversionName:"1.2.0"released:truedeprecated:false
Verify in console:
item.reload
item.latest_version.version # => "1.2.0"
item.latest_released_version.version # => "1.2.0"
v1_1.reload.deprecated # => false
item.latest_version.definition # => matches v1.0 definitionTC2: Restore with deprecation
Create another version first:
v1_3 = item.build_new_version(
version: '1.3.0',
schema_version: 1,
definition: { 'system_prompt' => 'Version 1.3 - ANOTHER BAD', 'tools' => [], 'user_prompt' => '' },
release_date: Time.zone.now,
created_by: user
)
item.save!
item.update!(latest_released_version: v1_3)
puts "v1.3.0 GlobalID: #{v1_3.to_global_id}"mutation {
aiCatalogItemVersionRestore(input: {
id: "<v1_0_global_id>"
deprecateCurrent: true
}) {
itemVersion {
versionName
released
deprecated
}
errors
}
}Expected:
errors: emptyversionName:"1.4.0"
Verify in console:
item.reload
item.latest_version.version # => "1.4.0"
v1_3.reload.deprecated # => trueTC3: Restore a deprecated version
Use v1.3 GlobalID from TC2 (which was deprecated).
mutation {
aiCatalogItemVersionRestore(input: {
id: "<v1_3_global_id>"
}) {
itemVersion { versionName }
errors
}
}Expected:
errors:["Cannot restore a deprecated version"]itemVersion:null
TC4: Restore the current latest version
puts "Current latest GlobalID: #{item.reload.latest_released_version.to_global_id}"mutation {
aiCatalogItemVersionRestore(input: {
id: "<current_latest_global_id>"
}) {
itemVersion { versionName }
errors
}
}Expected:
errors:["Source version is already the current latest version"]
TC5: Restore a draft version
draft = item.build_new_version(
version: '2.0.0',
schema_version: 1,
definition: { 'system_prompt' => 'Draft', 'tools' => [], 'user_prompt' => '' },
release_date: nil,
created_by: user
)
item.save!
puts "Draft GlobalID: #{draft.to_global_id}"mutation {
aiCatalogItemVersionRestore(input: {
id: "<draft_global_id>"
}) {
itemVersion { versionName }
errors
}
}Expected:
errors:["Source version must be a released version"]
TC6: Non-existent version
mutation {
aiCatalogItemVersionRestore(input: {
id: "gid://gitlab/Ai::Catalog::ItemVersion/999999999"
}) {
itemVersion { versionName }
errors
}
}Expected:
- Top-level error: resource not available
TC7: Developer user (unauthorized)
Sign in as a developer user and run TC1 mutation.
Expected:
- Top-level error: permission denied
TC8: Feature flag disabled
Feature.disable(:ai_catalog_version_restore)Run TC1 mutation as maintainer.
Expected:
errors:["This feature is not available"]
Re-enable after test:
Feature.enable(:ai_catalog_version_restore)TC9: excludeDeprecated filter
query {
aiCatalogItemVersions(excludeDeprecated: true) {
nodes {
versionName
deprecated
}
}
}Expected: No deprecated versions in results.
query {
aiCatalogItemVersions(excludeDeprecated: false) {
nodes {
versionName
deprecated
}
}
}Expected: Deprecated versions included with deprecated: true.
Database queries
next_version_number query (used by RestoreService and BaseUpdateService):
Query plan - https://postgres.ai/console/gitlab/gitlab-production-main/sessions/52135/commands/153577
SELECT "ai_catalog_item_versions".*
FROM "ai_catalog_item_versions"
WHERE "ai_catalog_item_versions"."ai_catalog_item_id" = 1009920
AND "ai_catalog_item_versions"."release_date" IS NOT NULL
ORDER BY "ai_catalog_item_versions"."id" DESC
LIMIT 1About the released and not_deprecated scopes: both come from ai_catalog_version and are always used with a given item (agent/flow), so the number of version records per item should stay quite small (around 10 or so per agent). Because of that, I didn't add an index on release_date here, and the same reasoning applies to the deprecated scope. Happy to add one if you think it would be worthwhile, though!
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Related to #601095 (closed)