doc/development/ai_features/actions.md
This page includes how to implement actions and migrate them to the AI Gateway.
Implementing a new AI action will require changes across different components. We'll use the example of wanting to implement an action that allows users to rewrite issue descriptions according to a given prompt.
The Cloud Connector configuration stores the permissions needed to access your service, as well as additional metadata. If there's no entry for your feature, add the feature as a Cloud Connector unit primitive:
For more information, see Cloud Connector: Configuration.
In the AI Gateway project, create a
new prompt definition under ai_gateway/prompts/definitions with the route [ai-action]/base/[prompt-version].yml
(see Prompt versioning conventions).
Specify the model and provider you wish to use, and the prompts that
will be fed to the model. You can specify inputs to be plugged into the prompt by using {}.
# ai_gateway/prompts/definitions/rewrite_description/base/1.0.0.yml
name: Description rewriter
model:
config_file: conversation_performant
params:
model_class_provider: anthropic
prompt_template:
system: |
You are a helpful assistant that rewrites the description of resources. You'll be given the current description, and a prompt on how you should rewrite it. Reply only with your rewritten description.
<description>{description}</description>
<prompt>{prompt}</prompt>
When an AI action uses multiple prompts, the definitions can be organized in a tree structure in the form
[ai-action]/[prompt-name]/base/[version].yaml:
# ai_gateway/prompts/definitions/code_suggestions/generations/base/1.0.0.yml
name: Code generations
model:
config_file: conversation_performant
params:
model_class_provider: anthropic
...
To specify prompts for multiple models, use the name of the model in the path for the definition:
# ai_gateway/prompts/definitions/code_suggestions/generations/mistral/1.0.0.yml
name: Code generations
model:
name: mistral
params:
model_class_provider: litellm
...
ee/lib/gitlab/llm/ai_gateway/completions/ and inherit it from the Base
AI Gateway Completion.# ee/lib/gitlab/llm/ai_gateway/completions/rewrite_description.rb
module Gitlab
module Llm
module AiGateway
module Completions
class RewriteDescription < Base
extend ::Gitlab::Utils::Override
override :inputs
def inputs
{ description: resource.description, prompt: prompt_message.content }
end
end
end
end
end
end
ee/app/services/llm/ and inherit it from the BaseService.resource is the object we want to act on. It can be any object that includes the Ai::Model concern. For example it could be a Project, MergeRequest, or Issue.# ee/app/services/llm/rewrite_description_service.rb
module Llm
class RewriteDescriptionService < BaseService
extend ::Gitlab::Utils::Override
override :valid
def valid?
super &&
# You can restrict which type of resources your service applies to
resource.to_ability_name == "issue" &&
# Always check that the user is allowed to perform this action on the resource
Ability.allowed?(user, :rewrite_description, resource)
end
private
def perform
schedule_completion_worker
end
end
end
Go to Gitlab::Llm::Utils::AiFeaturesCatalogue and add a new entry for your AI action.
class AiFeaturesCatalogue
LIST = {
# ...
rewrite_description: {
service_class: ::Gitlab::Llm::AiGateway::Completions::RewriteDescription,
feature_category: :ai_abstraction_layer,
execute_method: ::Llm::RewriteDescriptionService,
maturity: :experimental,
self_managed: false,
internal: false
}
}.freeze
Go to Gitlab::Llm::PromptVersions and add an entry for your AI action with a query that includes your desired prompt
version (for new features this will usually be ^1.0.0, see Prompt version resolution):
class PromptVersions
class << self
VERSIONS = {
# ...
"rewrite_description/base": "^1.0.0"
To make changes to the template, model, or parameters of an AI feature, create a new YAML version file in the AI Gateway:
# ai_gateway/prompts/definitions/rewrite_description/base/1.0.1.yml
name: Description rewriter with Claude 3.5
model:
name: claude-3-5-sonnet-20240620
params:
model_class_provider: anthropic
prompt_template:
system: |
You are a helpful assistant that rewrites the description of resources. You'll be given the current description, and a prompt on how you should rewrite it. Reply only with your rewritten description.
<description>{description}</description>
<prompt>{prompt}</prompt>
Once a stable prompt version is added to the AI Gateway it should not be altered. You can create a mutable version of a
prompt by adding a pre-release suffix to the file name (for example, 1.0.1-dev.yml). This will also prevent it from being
automatically served to clients. Then you can use a feature flag to control the rollout this new version. For GitLab Duo Self-Hosted, forced versions are ignored, and only versions defined in PromptVersions are used. This avoids
mistakenly enabling versions for models that don't have that specified version.
If your AI action is implemented as a subclass of AiGateway::Completions::Base, you can achieve this by overriding the prompt
version in your subclass:
# ee/lib/gitlab/llm/ai_gateway/completions/rewrite_description.rb
module Gitlab
module Llm
module AiGateway
module Completions
class RewriteDescription < Base
extend ::Gitlab::Utils::Override
override :prompt_version
def prompt_version
'1.0.1-dev' if Feature.enabled?(:my_feature_flag) # You can also scope it to `user` or `resource`, as appropriate
end
# ...
Once you are ready to make this version stable and start auto-serving it to compatible clients, simply rename the YAML
definition file to remove the pre-release suffix, and remove the prompt_version override.
AI actions were initially implemented inside the GitLab monolith. As part of our AI Gateway as the Sole Access Point for Monolith to Access Models Epic we're migrating prompts, model selection and model parameters into the AI Gateway. This will increase the speed at which we can deliver improvements to users on GitLab Self-Managed, by decoupling prompt and model changes from monolith releases. To migrate an existing action:
aigw_service_class.class AiFeaturesCatalogue
LIST = {
# ...
generate_description: {
service_class: ::Gitlab::Llm::Anthropic::Completions::GenerateDescription,
aigw_service_class: ::Gitlab::Llm::AiGateway::Completions::GenerateDescription,
prompt_class: ::Gitlab::Llm::Templates::GenerateDescription,
feature_category: :ai_abstraction_layer,
execute_method: ::Llm::GenerateDescriptionService,
maturity: :experimental,
self_managed: false,
internal: false
},
# ...
}.freeze
prompt_migration_#{feature_name} feature flag (e.g prompt_migration_generate_description)When the feature flag is enabled, the aigw_service_class will be used to process the AI action.
Once you've validated the correct functioning of your action, you can remove the aigw_service_class key and replace
the service_class with the new AiGateway::Completions class to make it the permanent provider.
For a complete example of the changes needed to migrate an AI action, see the following MRs:
We recommend to use policies to deal with authorization for a feature. Currently we need to make sure to cover the following checks:
Some basic authorization is included in the Abstraction Layer classes that are base classes for more specialized classes.
What needs to be included in the code:
Gitlab::Llm::Utils::FlagChecker.flag_enabled_for_feature?(ai_action) - included in the Llm::BaseService class.Gitlab::Llm::Utils::Authorizer.resource(resource: resource, user: user).allowed? - also included in the Llm::BaseService class.::Gitlab::Llm::FeatureAuthorizer.new(container: subject_container, feature_name: action_name).allowed?[!note] For more information, see the GitLab AI Gateway documentation about authentication and authorization in AI Gateway.
If your GitLab Duo feature involves an autonomous agent, you should use composite identity authorization.
Because multiple users' requests can be processed in parallel, when receiving responses,
it can be difficult to pair a response with its original request. The requestId
field can be used for this purpose, because both the request and response are assured
to have the same requestId UUID.
AI requests and responses can be cached. Cached conversation is being used to display user interaction with AI features. In the current implementation, this cache is not used to skip consecutive calls to the AI service when a user repeats their requests.
query {
aiMessages {
nodes {
id
requestId
content
role
errors
timestamp
}
}
}
This cache is used for chat functionality. For other services, caching is
disabled. You can enable this for a service by using the cache_response: true
option.
Caching has following limitations:
There is one setting allowed on root namespace level that restrict the use of AI features:
experiment_features_enabledTo check if that feature is allowed for a given namespace, call:
Gitlab::Llm::StageCheck.available?(namespace, :name_of_the_feature)
Add the name of the feature to the Gitlab::Llm::StageCheck class. There are
arrays there that differentiate between experimental and beta features.
This way we are ready for the following different cases:
true. For example, the feature is generally available.To move the feature from the experimental phase to the beta phase, move the name of the feature from the EXPERIMENTAL_FEATURES array to the BETA_FEATURES array.
The CompletionWorker will call the Completions::Factory which will initialize the Service and execute the actual call to the API.
In our example, we will use VertexAI and implement two new classes:
# /ee/lib/gitlab/llm/vertex_ai/completions/rewrite_description.rb
module Gitlab
module Llm
module VertexAi
module Completions
class AmazingNewAiFeature < Gitlab::Llm::Completions::Base
def execute
prompt = ai_prompt_class.new(options[:user_input]).to_prompt
response = Gitlab::Llm::VertexAi::Client.new(user, unit_primitive: 'amazing_feature').text(content: prompt)
response_modifier = ::Gitlab::Llm::VertexAi::ResponseModifiers::Predictions.new(response)
::Gitlab::Llm::GraphqlSubscriptionResponseService.new(
user, nil, response_modifier, options: response_options
).execute
end
end
end
end
end
end
# /ee/lib/gitlab/llm/vertex_ai/templates/rewrite_description.rb
module Gitlab
module Llm
module VertexAi
module Templates
class AmazingNewAiFeature
def initialize(user_input)
@user_input = user_input
end
def to_prompt
<<~PROMPT
You are an assistant that writes code for the following context:
context: #{user_input}
PROMPT
end
end
end
end
end
end
Because we support multiple AI providers, you may also use those providers for the same example:
Gitlab::Llm::VertexAi::Client.new(user, unit_primitive: 'your_feature')
Gitlab::Llm::Anthropic::Client.new(user, unit_primitive: 'your_feature')
Prompt versions should adjust to Semantic Versioning standards: MAJOR.MINOR.PATCH[-PRERELEASE].
The MAJOR component guarantees that older versions of GitLab will not break once a new change is added, without blocking the evolution of our codebase. Changes in MINOR and PATCH are more subjective.
To guarantee traceability of changes, only prompts with a pre-release version (eg 1.0.1-dev.yml)
may be changed once committed. Prompts defining a stable version are immutable, and changing them will trigger a pipeline failure.
To better organize the prompts, it is possible to use partials to split a prompt into smaller parts. Partials must also be versioned. For example:
# ai_gateway/prompts/definitions/rewrite_description/base/1.0.0.yml
name: Description rewriter
model:
config_file: conversation_performant
params:
model_class_provider: anthropic
prompt_template:
system: |
{% include 'rewrite_description/system/1.0.0.jinja' %}
user: |
{% include 'rewrite_description/user/1.0.0.jinja' %}
AI Gateway will fetch the latest stable version available that matches the prompt version query passed as argument.
Queries follow Poetry's version constraint rules.
For example, if prompt foo/bar has the following versions:
1.0.1.yml1.1.0.yml1.5.0-dev.yml2.0.1.ymlThen, if /v1/prompts/foo/bar is called with
{'prompt_version': "^1.0.0"}, prompt version 1.1.0.yml will be selected.{'prompt_version': "1.5.0-dev"}, prompt version 1.5.0-dev.yml will be selected.{'prompt_version': "^2.0.0"}, prompt version 2.0.1.yml will be selected.