mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-12 23:57:42 +00:00
469 lines
17 KiB
Markdown
469 lines
17 KiB
Markdown
---
|
|
stage: AI-powered
|
|
group: AI Framework
|
|
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review.
|
|
title: AI actions
|
|
---
|
|
|
|
This page includes how to implement actions and migrate them to the AI Gateway.
|
|
|
|
## How to implement a new action
|
|
|
|
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.
|
|
|
|
### 1. Add your action to the Cloud Connector feature list
|
|
|
|
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](../cloud_connector/_index.md#register-new-feature-for-gitlab-self-managed-dedicated-and-gitlabcom-customers):
|
|
|
|
For more information, see [Cloud Connector: Configuration](../cloud_connector/configuration.md).
|
|
|
|
### 2. Create a prompt definition in the AI gateway
|
|
|
|
In [the AI gateway project](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist), create a
|
|
new prompt definition under `ai_gateway/prompts/definitions` with the route `[ai-action]/base/[prompt-version].yml`
|
|
(see [Prompt versioning conventions](#appendix-a-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 `{}`.
|
|
|
|
```yaml
|
|
# 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`:
|
|
|
|
```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:
|
|
|
|
```yaml
|
|
# ai_gateway/prompts/definitions/code_suggestions/generations/mistral/1.0.0.yml
|
|
|
|
name: Code generations
|
|
model:
|
|
name: mistral
|
|
params:
|
|
model_class_provider: litellm
|
|
...
|
|
```
|
|
|
|
### 3. Create a Completion class
|
|
|
|
1. Create a new completion under `ee/lib/gitlab/llm/ai_gateway/completions/` and inherit it from the `Base`
|
|
AI gateway Completion.
|
|
|
|
```ruby
|
|
# 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
|
|
```
|
|
|
|
### 4. Create a Service
|
|
|
|
1. Create a new service under `ee/app/services/llm/` and inherit it from the `BaseService`.
|
|
1. The `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`.
|
|
|
|
```ruby
|
|
# 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
|
|
```
|
|
|
|
### 5. Register the feature in the catalogue
|
|
|
|
Go to `Gitlab::Llm::Utils::AiFeaturesCatalogue` and add a new entry for your AI action.
|
|
|
|
```ruby
|
|
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
|
|
```
|
|
|
|
### 6. Add a default prompt version query
|
|
|
|
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](#prompt-version-resolution)):
|
|
|
|
```ruby
|
|
class PromptVersions
|
|
class << self
|
|
VERSIONS = {
|
|
# ...
|
|
"rewrite_description/base": "^1.0.0"
|
|
```
|
|
|
|
## Updating an AI action
|
|
|
|
To make changes to the template, model, or parameters of an AI feature, create a new YAML version file in the AI Gateway:
|
|
|
|
```yaml
|
|
# 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>
|
|
```
|
|
|
|
### Incremental rollout of prompt versions
|
|
|
|
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:
|
|
|
|
```ruby
|
|
# 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.
|
|
|
|
## How to migrate an existing action to the AI gateway
|
|
|
|
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](https://gitlab.com/groups/gitlab-org/-/epics/13024)
|
|
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:
|
|
|
|
1. Follow steps 1 through 3 on [How to implement a new action](#how-to-implement-a-new-action).
|
|
1. Modify the entry for your AI action in the catalogue to list the new completion class as the `aigw_service_class`.
|
|
|
|
```ruby
|
|
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
|
|
```
|
|
|
|
1. Create `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:
|
|
|
|
- [Changes to the GitLab Rails monolith](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/152429)
|
|
- [Changes to the AI gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/merge_requests/921)
|
|
|
|
### Authorization in GitLab-Rails
|
|
|
|
We recommend to use [policies](../policies.md) 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:
|
|
|
|
1. Check for feature flag compatibility: `Gitlab::Llm::Utils::FlagChecker.flag_enabled_for_feature?(ai_action)` - included in the `Llm::BaseService` class.
|
|
1. Check if resource is authorized: `Gitlab::Llm::Utils::Authorizer.resource(resource: resource, user: user).allowed?` - also included in the `Llm::BaseService` class.
|
|
1. Both of those checks are included in the `::Gitlab::Llm::FeatureAuthorizer.new(container: subject_container, feature_name: action_name).allowed?`
|
|
1. Access to AI features depend on several factors, such as: their maturity, if they are enabled on self-managed, if they are bundled within an add-on etc.
|
|
- [Example](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/policies/ee/global_policy.rb#L222-222) of policy not connected to the particular resource.
|
|
- [Example](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/policies/ee/issue_policy.rb#L25-25) of policy connected to the particular resource.
|
|
|
|
{{< alert type="note" >}}
|
|
|
|
For more information, see [the GitLab AI gateway documentation](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitlab_ai_gateway.md#optional-enable-authentication-and-authorization-in-ai-gateway) about authentication and authorization in AI gateway.
|
|
|
|
{{< /alert >}}
|
|
|
|
If your Duo feature involves an autonomous agent, you should use
|
|
[composite identity](composite_identity.md) authorization.
|
|
|
|
### Pairing requests with responses
|
|
|
|
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.
|
|
|
|
### Caching
|
|
|
|
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.
|
|
|
|
```graphql
|
|
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:
|
|
|
|
- Messages are stored in Redis stream.
|
|
- There is a single stream of messages per user. This means that all services
|
|
currently share the same cache. If needed, this could be extended to multiple
|
|
streams per user (after checking with the infrastructure team that Redis can handle
|
|
the estimated amount of messages).
|
|
- Only the last 50 messages (requests + responses) are kept.
|
|
- Expiration time of the stream is 3 days since adding last message.
|
|
- User can access only their own messages. There is no authorization on the caching
|
|
level, and any authorization (if accessed by not current user) is expected on
|
|
the service layer.
|
|
|
|
### Check if feature is allowed for this resource based on namespace settings
|
|
|
|
There is one setting allowed on root namespace level that restrict the use of AI features:
|
|
|
|
- `experiment_features_enabled`
|
|
|
|
To check if that feature is allowed for a given namespace, call:
|
|
|
|
```ruby
|
|
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:
|
|
|
|
- If the feature is not in any array, the check will return `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.
|
|
|
|
### Implement calls to AI APIs and the prompts
|
|
|
|
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:
|
|
|
|
```ruby
|
|
# /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
|
|
```
|
|
|
|
```ruby
|
|
# /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:
|
|
|
|
```ruby
|
|
Gitlab::Llm::VertexAi::Client.new(user, unit_primitive: 'your_feature')
|
|
Gitlab::Llm::Anthropic::Client.new(user, unit_primitive: 'your_feature')
|
|
```
|
|
|
|
## Appendix A: Prompt versioning conventions
|
|
|
|
Prompt versions should adjust to [Semantic Versioning](https://semver.org/) standards: `MAJOR.MINOR.PATCH[-PRERELEASE]`.
|
|
|
|
- A change in the MAJOR component reflects changes will break with older versions of GitLab. For example, when the new
|
|
prompt must receive a new property that doesn't have a default, since if this change were applied to all GitLab versions,
|
|
requests made from older versions will throw an error since that property is not present.
|
|
|
|
- A change in the MINOR component reflects feature additions, but that are still backwards compatible. For example,
|
|
suppose we want to use a new more powerful model: requests of older versions of GitLab will still work.
|
|
|
|
- A change in the PATCH component reflects small bug fixes to prompts, like a typo.
|
|
|
|
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.
|
|
|
|
### Immutability of prompt versions
|
|
|
|
To guarantee traceability of changes, only prompts with a [pre-release version](https://semver.org/#spec-item-9) (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.
|
|
|
|
### Using partials
|
|
|
|
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:
|
|
|
|
```yaml
|
|
# 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' %}
|
|
```
|
|
|
|
### Prompt version resolution
|
|
|
|
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](https://python-poetry.org/docs/dependency-specification/#version-constraints).
|
|
For example, if prompt `foo/bar` has the following versions:
|
|
|
|
- `1.0.1.yml`
|
|
- `1.1.0.yml`
|
|
- `1.5.0-dev.yml`
|
|
- `2.0.1.yml`
|
|
|
|
Then, 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.
|