Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-13 21:07:29 +00:00
parent e358026c1c
commit c5061bfaef
26 changed files with 249 additions and 134 deletions

View File

@ -1,6 +1,6 @@
---
start-release-environments-security-pipeline:
allow_failure: true
allow_failure: false
extends:
- .release-environments:rules:start-release-environments-security-pipeline
stage: release-environments

View File

@ -127,7 +127,7 @@
if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && ($CI_MERGE_REQUEST_LABELS =~ /pipeline::expedited/ || $CI_MERGE_REQUEST_LABELS =~ /pipeline:expedite/)'
.if-merge-request-not-labels-pipeline-run-e2e-omnibus-once: &if-merge-request-not-labels-pipeline-run-e2e-omnibus-once
if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached") && ($CI_MERGE_REQUEST_LABELS !~ /pipeline:run-e2e-omnibus-once/)'
if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && ($CI_MERGE_REQUEST_LABELS !~ /pipeline:run-e2e-omnibus-once/)'
.if-merge-request-labels-frontend-and-feature-flag: &if-merge-request-labels-frontend-and-feature-flag
if: '($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /frontend/ && $CI_MERGE_REQUEST_LABELS =~ /feature flag/'

View File

@ -75,6 +75,11 @@ export default {
required: false,
default: false,
},
showWebIdeForkSuggestion: {
type: Boolean,
required: false,
default: false,
},
projectPath: {
type: String,
required: false,
@ -161,6 +166,7 @@ export default {
:edit-url="blob.editBlobPath"
:web-ide-url="blob.ideEditPath"
:needs-to-fork="showForkSuggestion"
:needs-to-fork-with-web-ide="showWebIdeForkSuggestion"
:show-pipeline-editor-button="Boolean(blob.pipelineEditorPath)"
:pipeline-editor-url="blob.pipelineEditorPath"
:gitpod-url="blob.gitpodBlobUrl"

View File

@ -29,14 +29,14 @@ export default {
},
linkClasses: [
'gl-border',
'gl-text-decoration-none!',
'!gl-no-underline',
'gl-rounded-base',
'gl-p-7',
'gl-display-flex',
'gl-flex',
'gl-h-full',
'gl-align-items-center',
'gl-items-center',
'gl-text-purple-600',
'gl-hover-bg-gray-50',
'hover:gl-bg-gray-50',
],
inject: [
'newSubgroupPath',
@ -53,28 +53,28 @@ export default {
<template>
<div v-if="canCreateSubgroups || canCreateProjects" class="gl-mt-5">
<div class="gl-display-flex -gl-mx-3 -gl-my-3 gl-flex-wrap">
<div v-if="canCreateSubgroups" class="gl-p-3 gl-w-full gl-sm-w-half">
<div class="-gl-mx-3 -gl-my-3 gl-flex gl-flex-wrap">
<div v-if="canCreateSubgroups" class="gl-w-full gl-p-3 sm:gl-w-1/2">
<gl-link :href="newSubgroupPath" :class="$options.linkClasses">
<div class="svg-content gl-w-15 gl-flex-shrink-0 gl-mr-5">
<div class="svg-content gl-mr-5 gl-w-15 gl-shrink-0">
<img :src="newSubgroupIllustration" :alt="$options.i18n.withLinks.subgroup.title" />
</div>
<div>
<h4 class="gl-text-inherit">{{ $options.i18n.withLinks.subgroup.title }}</h4>
<p class="gl-text-body">
<p class="gl-text-primary">
{{ $options.i18n.withLinks.subgroup.description }}
</p>
</div>
</gl-link>
</div>
<div v-if="canCreateProjects" class="gl-p-3 gl-w-full gl-sm-w-half">
<div v-if="canCreateProjects" class="gl-w-full gl-p-3 sm:gl-w-1/2">
<gl-link :href="newProjectPath" :class="$options.linkClasses">
<div class="svg-content gl-w-13 gl-flex-shrink-0 gl-mr-5">
<div class="svg-content gl-mr-5 gl-w-13 gl-shrink-0">
<img :src="newProjectIllustration" :alt="$options.i18n.withLinks.project.title" />
</div>
<div>
<h4 class="gl-text-inherit">{{ $options.i18n.withLinks.project.title }}</h4>
<p class="gl-text-body">
<p class="gl-text-primary">
{{ $options.i18n.withLinks.project.description }}
</p>
</div>

View File

@ -39,7 +39,7 @@ export default {
</script>
<template>
<ul class="groups-list group-list-tree gl-display-flex gl-flex-direction-column gl-m-0">
<ul class="groups-list group-list-tree gl-m-0 gl-flex gl-flex-col">
<!-- eslint-disable-next-line vue/no-undef-components -->
<group-item
v-for="(group, index) in groups"

View File

@ -158,7 +158,7 @@ export default {
>
<div
:class="{ 'project-row-contents': !isGroup }"
class="group-row-contents gl-flex gl-items-center py-2 pr-3"
class="group-row-contents py-2 pr-3 gl-flex gl-items-center"
>
<div class="folder-toggle-wrap gl-mr-2 !gl-flex gl-items-center">
<gl-button
@ -176,11 +176,11 @@ export default {
<gl-loading-icon
v-if="group.isChildrenLoading"
size="lg"
class="gl-hidden sm:gl-inline-flex flex-shrink-0 gl-mr-3"
class="flex-shrink-0 gl-mr-3 gl-hidden sm:gl-inline-flex"
/>
<a
:class="{ 'sm:gl-flex': !group.isChildrenLoading }"
class="gl-hidden gl-text-decoration-none! gl-mr-3"
class="gl-mr-3 gl-hidden !gl-no-underline"
:href="group.relativePath"
:aria-label="group.name"
>
@ -192,17 +192,17 @@ export default {
:project-name="group.name"
/>
</a>
<div class="group-text-container !gl-flex flex-fill gl-align-items-center">
<div class="group-text-container flex-fill !gl-flex gl-items-center">
<div class="group-text flex-grow-1 flex-shrink-1">
<div
class="gl-flex gl-align-items-center gl-flex-wrap title namespace-title gl-font-bold gl-mr-3"
class="title namespace-title gl-mr-3 gl-flex gl-flex-wrap gl-items-center gl-font-bold"
>
<a
v-gl-tooltip.bottom
data-testid="group-name"
:href="group.relativePath"
:title="group.fullName"
class="no-expand gl-mr-3 gl-text-gray-900! gl-break-anywhere"
class="no-expand gl-mr-3 !gl-text-gray-900 gl-break-anywhere"
:itemprop="microdata.nameItemprop"
>
<!-- ending bracket must be by closing tag to prevent -->
@ -219,7 +219,7 @@ export default {
<template v-if="shouldShowVisibilityWarning">
<gl-button
ref="visibilityWarningButton"
class="gl-p-1! gl-bg-transparent! gl-mr-3"
class="gl-mr-3 !gl-bg-transparent !gl-p-1"
category="tertiary"
icon="warning"
:aria-label="$options.i18n.popoverTitle"
@ -233,7 +233,7 @@ export default {
{{ $options.i18n.popoverBody }}
<div class="gl-mt-3">
<gl-link
class="gl-font-sm"
class="gl-text-sm"
:href="$options.shareProjectsWithGroupsHelpPagePath"
>{{ $options.i18n.learnMore }}</gl-link
>
@ -244,7 +244,7 @@ export default {
{{ group.permission }}
</user-access-role-badge>
</div>
<div v-if="group.description" class="description gl-font-sm gl-mt-1">
<div v-if="group.description" class="description gl-mt-1 gl-text-sm">
<span
v-safe-html:[$options.safeHtmlConfig]="group.description"
:itemprop="microdata.descriptionItemprop"
@ -259,13 +259,8 @@ export default {
<div v-else-if="group.archived">
<gl-badge variant="info">{{ __('Archived') }}</gl-badge>
</div>
<div
class="metadata gl-flex gl-flex-grow-1 gl-flex-shrink-0 gl-flex-wrap justify-content-md-between"
>
<item-stats
:item="group"
class="group-stats gl-hidden md:gl-flex gl-align-items-center"
/>
<div class="metadata justify-content-md-between gl-flex gl-shrink-0 gl-grow gl-flex-wrap">
<item-stats :item="group" class="group-stats gl-hidden gl-items-center md:gl-flex" />
<item-actions
v-if="showActionsMenu"
:group="group"

View File

@ -292,7 +292,7 @@ export default {
<gl-form-input
:id="fields.name.id"
v-model="name"
class="gl-field-error-ignore gl-h-auto!"
class="gl-field-error-ignore !gl-h-auto"
required
:name="fields.name.name"
:placeholder="$options.i18n.inputs.name.placeholder"
@ -315,13 +315,13 @@ export default {
<gl-form-group v-if="newSubgroup" class="col-sm-6 gl-pr-0" :label="inputLabels.subgroupPath">
<div class="input-group gl-flex-nowrap">
<gl-button-group class="gl-w-full">
<gl-button class="js-group-namespace-button gl-text-truncate gl-flex-grow-0!" label>
<gl-button class="js-group-namespace-button !gl-grow-0 gl-truncate" label>
{{ basePath }}
</gl-button>
<gl-dropdown
class="js-group-namespace-dropdown gl-flex-grow-1"
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
class="js-group-namespace-dropdown gl-grow"
toggle-class="!gl-rounded-tr-base !gl-rounded-br-base gl-w-20"
@shown="handleDropdownShown"
>
<template #button-text>
@ -355,7 +355,7 @@ export default {
</gl-dropdown>
</gl-button-group>
<div class="gl-align-self-center gl-pl-5">
<div class="gl-self-center gl-pl-5">
<span class="gl-hidden md:gl-inline">/</span>
</div>
</div>
@ -378,7 +378,7 @@ export default {
</template>
<gl-form-input
:id="fields.path.id"
class="gl-field-error-ignore gl-h-auto!"
class="gl-field-error-ignore !gl-h-auto"
:name="fields.path.name"
:value="computedPath"
:placeholder="$options.i18n.inputs.path.placeholder"

View File

@ -153,7 +153,7 @@ export default {
</template>
<gl-form-input
:id="id"
class="gl-field-error-ignore gl-h-auto!"
class="gl-field-error-ignore !gl-h-auto"
:value="value"
:placeholder="$options.i18n.placeholder"
:state="state"

View File

@ -46,7 +46,7 @@ export default {
<pagination-links
:change="change"
:page-info="pageInfo"
class="!gl-flex justify-content-center gl-mt-3"
class="justify-content-center gl-mt-3 !gl-flex"
/>
</div>
</template>

View File

@ -56,7 +56,7 @@ export default {
text: this.$options.i18n.leaveBtnTitle,
action: this.onLeaveGroup,
extraAttrs: {
class: 'gl-text-red-500!',
class: '!gl-text-red-500',
'data-testid': `leave-group-${this.group.id}-btn`,
},
};
@ -66,7 +66,7 @@ export default {
text: this.$options.i18n.removeBtnTitle,
href: this.removeButtonHref,
extraAttrs: {
class: 'gl-text-red-500!',
class: '!gl-text-red-500',
'data-testid': `remove-group-${this.group.id}-btn`,
},
};
@ -87,7 +87,7 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-justify-content-end gl-ml-5" @click.stop>
<div class="gl-ml-5 gl-flex gl-justify-end" @click.stop>
<gl-disclosure-dropdown
v-gl-tooltip.hover.focus="$options.i18n.optionsDropdownTitle"
icon="ellipsis_v"

View File

@ -83,7 +83,7 @@ export default {
data-testid="star-count"
icon-name="star"
/>
<div v-if="isProject" class="last-updated gl-font-sm">
<div v-if="isProject" class="last-updated gl-text-sm">
<time-ago-tooltip :time="item.lastActivityAt" tooltip-placement="bottom" />
</div>
</div>

View File

@ -24,7 +24,7 @@ export default {
</script>
<template>
<span class="item-type-icon gl-display-inline-block gl-text-secondary">
<span class="item-type-icon gl-inline-block gl-text-secondary">
<gl-icon :name="iconClass" />
</span>
</template>

View File

@ -249,7 +249,7 @@ export default {
>.
</template>
</gl-form-fields>
<div class="gl-display-flex gl-gap-3">
<div class="gl-flex gl-gap-3">
<gl-button
type="submit"
variant="confirm"

View File

@ -114,7 +114,7 @@ export default {
return labelPieces.join(', ') || this.label;
},
toggleClass() {
return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
return this.toggleLabel === this.label ? '!gl-text-gray-500' : '';
},
selection() {
return [

View File

@ -174,13 +174,22 @@ export default {
return pathLock ? pathLock.user : null;
},
showForkSuggestion() {
canFork() {
const { createMergeRequestIn, forkProject } = this.userPermissions;
const { canModifyBlob } = this.blobInfo;
return (
this.isLoggedIn && !this.isUsingLfs && !canModifyBlob && createMergeRequestIn && forkProject
);
return this.isLoggedIn && !this.isUsingLfs && createMergeRequestIn && forkProject;
},
showSingleFileEditorForkSuggestion() {
const { canModifyBlob } = this.blobInfo;
return this.canFork && !canModifyBlob;
},
showWebIdeForkSuggestion() {
const { canModifyBlobWithWebIde } = this.blobInfo;
return this.canFork && !canModifyBlobWithWebIde;
},
showForkSuggestion() {
return this.showSingleFileEditorForkSuggestion || this.showWebIdeForkSuggestion;
},
forkPath() {
const forkPaths = {
@ -265,14 +274,24 @@ export default {
if (this.$route?.query?.plain === plain) return;
this.$router.push({ path: this.$route.path, query: { ...this.$route.query, plain } });
},
isIdeTarget(target) {
return target === 'ide';
},
forkSuggestionForSelectedEditor(target) {
return this.isIdeTarget(target)
? this.showWebIdeForkSuggestion
: this.showSingleFileEditorForkSuggestion;
},
editBlob(target) {
if (this.showForkSuggestion) {
this.setForkTarget(target);
return;
}
const { ideEditPath, editBlobPath } = this.blobInfo;
visitUrl(target === 'ide' ? ideEditPath : editBlobPath);
const isIdeTarget = this.isIdeTarget(target);
const showForkSuggestionForSelectedEditor = this.forkSuggestionForSelectedEditor(target);
if (showForkSuggestionForSelectedEditor) {
this.setForkTarget(target);
} else {
visitUrl(isIdeTarget ? ideEditPath : editBlobPath);
}
},
setForkTarget(target) {
this.forkTarget = target;
@ -311,7 +330,8 @@ export default {
:has-render-error="hasRenderError"
:show-path="false"
:override-copy="true"
:show-fork-suggestion="showForkSuggestion"
:show-fork-suggestion="showSingleFileEditorForkSuggestion"
:show-web-ide-fork-suggestion="showWebIdeForkSuggestion"
:show-blame-toggle="true"
:project-path="projectPath"
:project-id="projectId"
@ -334,7 +354,7 @@ export default {
:project-path="projectPath"
:is-locked="Boolean(pathLockedByUser)"
:can-lock="canLock"
:show-fork-suggestion="showForkSuggestion"
:show-fork-suggestion="showSingleFileEditorForkSuggestion"
:is-using-lfs="isUsingLfs"
@fork="setForkTarget('view')"
/>

View File

@ -44,6 +44,11 @@ export default {
required: false,
default: false,
},
needsToForkWithWebIde: {
type: Boolean,
required: false,
default: false,
},
gitpodEnabled: {
type: Boolean,
required: false,
@ -197,7 +202,7 @@ export default {
webIdeAction() {
if (!this.showWebIdeButton) return null;
const handleOptions = this.needsToFork
const handleOptions = this.needsToForkWithWebIde
? {
handle: () => {
if (this.disableForkModal) {

View File

@ -36,6 +36,7 @@ query getBlobInfo(
environmentFormattedExternalUrl
environmentExternalUrlForRouteMap
canModifyBlob
canModifyBlobWithWebIde
canCurrentUserPushToBranch
archived
storedExternally

View File

@ -4,7 +4,7 @@
- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)
- add_page_startup_graphql_call('repository/blob_info', { projectPath: @project.full_path, ref: current_ref, refType: @ref_type.to_s.upcase, filePath: @blob.path, shouldFetchRawText: @blob.rendered_as_text? && !@blob.rich_viewer })
- add_page_startup_graphql_call('repository/blob_info', { projectPath: @project.full_path, ref: current_ref, refType: @ref_type.to_s.upcase.presence, filePath: @blob.path, shouldFetchRawText: @blob.rendered_as_text? && !@blob.rich_viewer })
.js-signature-container{ data: { 'signatures-path': signatures_path } }

View File

@ -64,8 +64,8 @@ instructions sent to the LLM to perform certain tasks.
The state of the prompts is the result of weeks of iteration. If you want to
change any prompt in the current tool, you must put it behind a feature flag.
If you have any new or updated prompts, ask members of Duo Chat team or AI Framework team to
review, because they have significant experience with them.
If you have any new or updated prompts, ask members of [Duo Chat team](https://handbook.gitlab.com/handbook/engineering/development/data-science/ai-powered/duo-chat/)
to review, because they have significant experience with them.
### Troubleshooting
@ -76,11 +76,14 @@ you find a solution.
| Problem | Solution |
|-----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| There is no Chat button in the GitLab UI. | Make sure your user is a part of a group with enabled experimental and beta features. |
| There is no Chat button in the GitLab UI. | Make sure your user is a part of a group with Premium or Ultimate license and enabled Chat. |
| Chat replies with "Forbidden by auth provider" error. | Backend can't access LLMs. Make sure your [AI Gateway](index.md#required-install-ai-gateway) is set up correctly. |
| Requests take too long to appear in UI | Consider restarting Sidekiq by running `gdk restart rails-background-jobs`. If that doesn't work, try `gdk kill` and then `gdk start`. Alternatively, you can bypass Sidekiq entirely. To do that temporary alter `Llm::CompletionWorker.perform_async` statements with `Llm::CompletionWorker.perform_inline` |
| There is no chat button in GitLab UI when GDK is running on non-SaaS mode | You do not have cloud connector access token record or seat assigned. To create cloud connector access record, in rails console put following code: `CloudConnector::Access.new(data: { available_services: [{ name: "duo_chat", serviceStartTime: ":date_in_the_future" }] }).save`. |
Please, see also the section on [error codes](#interpreting-gitlab-duo-chat-error-codes) where you can read about codes
that Chat sends to assist troubleshooting.
## Contributing to GitLab Duo Chat
From the code perspective, Chat is implemented in the similar fashion as other
@ -155,28 +158,50 @@ conversation in GitLab Duo Chat towards predefined areas of interest or concern.
### Adding a new tool
To add a new tool:
To add a new tool you need to add changes both to [AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist)
and Rails Monolith. The main chat prompt is stored and assembled on AI Gateway. Rails side is responsible for assembling
required parameters of the prompt and sending them to AI Gateway. AI Gateway is responsible for assembling Chat prompt and
selecting Chat tools that are available for user based on their subscription and addon.
1. Create files for the tool in the `ee/lib/gitlab/llm/chain/tools/` folder. Use existing tools like `issue_identifier` or
`resource_reader` as a template.
When LLM selects the tool to use, this tool is executed on the Rails side. Tools use different endpoint to make
a request to AI Gateway. When you add a new tool, please take into account that AI Gateway works with different clients
and GitLab applications that have different versions. That means that old versions of GitLab won't know about a new tool,
please contact Duo Chat team if you want to add a new tool. We're working on long-term solution for this [problem](https://gitlab.com/gitlab-org/gitlab/-/issues/466247).
1. Write a class for the tool that includes:
#### Changes in AI Gateway
- Name and description of what the tool does
- Example questions that would use this tool
- Instructions for the large language model on how to use the tool to gather information - so the main prompts that
this tool is using.
1. Create a new class for a tool in `ai_gateway/chat/tools/gitlab.py`. This class should include next properties:
1. Test and iterate on the prompt using RSpec tests that make real requests to the large language model.
- Prompts require trial and error, the non-deterministic nature of working with LLM can be surprising.
- Anthropic provides good [guide](https://docs.anthropic.com/claude/docs/intro-to-prompting) on working on prompts.
- GitLab [guide](prompts.md) on working with prompts.
- `name` of the tool
- GitLab `resource` that tool works with
- `description` of what the tool does
- `example` of question and desired answer
1. Implement code in the tool to parse the response from the large language model and return it to the zero-shot agent.
1. Add tool to `__all__` list of tools in `ai_gateway/chat/tools/gitlab.py`.
1. Add the new tool name to the `tools` array in `ee/lib/gitlab/llm/completions/chat.rb` so the zero-shot agent knows about it.
1. Add tool class to the `DuoChatToolsRegistry` in `ai_gateway/chat/toolset.py` with an appropriate Unit Primitive.
1. Add tests by adding questions to the test-suite for which the new tool should respond to. Iterate on the prompts as needed.
1. Add test for your changes.
#### Changes in Rails Monolith
1. Create files for the tool in the `ee/lib/gitlab/llm/chain/tools/` folder. Use existing tools like `issue_reader` or
`epic_reader` as a template.
1. Write a class for the tool that includes instructions for the large language model on how to use the tool
to gather information - the main prompts that this tool is using.
1. Implement code in the tool to parse the response from the large language model and return it to the [chat agent](https://gitlab.com/gitlab-org/gitlab/-/blob/e0220502f1b3459b5a571d510ce5d1826877c3ce/ee/lib/gitlab/llm/chain/agents/single_action_executor.rb).
1. Add the new tool name to the `tools` array in `ee/lib/gitlab/llm/completions/chat.rb` so the agent knows about it.
#### Testing all together
Test and iterate on the prompt using RSpec tests that make real requests to the large language model.
- Prompts require trial and error, the non-deterministic nature of working with LLM can be surprising.
- Anthropic provides good [guide](https://docs.anthropic.com/claude/docs/intro-to-prompting) on working on prompts.
- GitLab [guide](prompts.md) on working with prompts.
The key things to keep in mind are properly instructing the large language model through prompts and tool descriptions,
keeping tools self-sufficient, and returning responses to the zero-shot agent. With some trial and error on prompts,
@ -590,57 +615,43 @@ flow of how we construct a Chat prompt:
from original GraphQL request and initializes a new instance of
`Gitlab::Llm::Completions::Chat` and calls `execute` on it
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/55b8eb6ff869e61500c839074f080979cc60f9de/ee/lib/gitlab/llm/completions_factory.rb#L89))
1. `Gitlab::Llm::Completions::Chat#execute` calls `Gitlab::Llm::Chain::Agents::ZeroShot::Executor`.
1. `Gitlab::Llm::Completions::Chat#execute` calls `Gitlab::Llm::Chain::Agents::SingleActionExecutor`.
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/d539f64ce6c5bed72ab65294da3bcebdc43f68c6/ee/lib/gitlab/llm/completions/chat.rb#L128-134))
1. `Gitlab::Llm::Chain::Agents::ZeroShot::Executor#execute` calls
1. `Gitlab::Llm::Chain::Agents::SingleActionExecutor#execute` calls
`execute_streamed_request`, which calls `request`, a method defined in the
`AiDependent` concern
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/d539f64ce6c5bed72ab65294da3bcebdc43f68c6/ee/lib/gitlab/llm/chain/agents/zero_shot/executor.rb#L85))
1. (`*`) `AiDependent#request` pulls the base prompt from `provider_prompt_class.prompt`.
For Chat, the provider prompt class is `ZeroShot::Prompts::Anthropic`
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/4eb4ce67ccc0fe331ddcce3fcc53e0ec0f47cd76/ee/lib/gitlab/llm/chain/concerns/ai_dependent.rb#L44-46))
1. (`*`) `ZeroShot::Prompts::Anthropic.prompt` pulls a base prompt and formats
it in the way that Anthropic expects it for the
[Text Completions API](https://docs.anthropic.com/claude/reference/complete_post)
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/4eb4ce67ccc0fe331ddcce3fcc53e0ec0f47cd76/ee/lib/gitlab/llm/chain/agents/zero_shot/prompts/anthropic.rb#L13-24))
1. (`*`) As part of constructing the prompt for Anthropic,
`ZeroShot::Prompts::Anthropic.prompt` makes a call to the `base_prompt` class
method, which is defined in `ZeroShot::Prompts::Base`
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/4eb4ce67ccc0fe331ddcce3fcc53e0ec0f47cd76/ee/lib/gitlab/llm/chain/agents/zero_shot/prompts/base.rb#L10-19))
1. (`*`) `ZeroShot::Prompts::Base.base_prompt` calls
`Utils::Prompt.no_role_text` and passes `prompt_version` to the method call.
The `prompt_version` option resolves to `PROMPT_TEMPLATE` from
`ZeroShot::Executor`
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/agents/zero_shot/executor.rb#L143))
1. (`*`) `PROMPT_TEMPLATE` is where the tools available and definitions for each
tool are interpolated into the zero shot prompt using the `format` method.
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/agents/zero_shot/executor.rb#L200-237)
1. (`*`) The `PROMPT_TEMPLATE` is interpolated into the `default_system_prompt`,
defined
[(here)](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/utils/prompt.rb#L54-73)
in the `ZeroShot::Prompts::Base.base_prompt` method call, and that whole big
prompt string is sent to `ai_request.request`
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/concerns/ai_dependent.rb#L19))
1. `ai_request` is defined in `Llm::Completions::Chat` and evaluates to either
`AiGateway` or `Anthropic` depending on the presence of a feature flag. On
production, we use `AiGateway` so this documentation follows that codepath.
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/3cd5e5bb3059d6aa9505d21a59cba57fec356473/ee/lib/gitlab/llm/completions/chat.rb#L42)
1. The `SingleActionExecutor#prompt_options` method assembles all prompt parameters for the AI Gateway request
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/971d07aa37d9f300b108ed66304505f2d7022841/ee/lib/gitlab/llm/chain/agents/single_action_executor.rb#L120-120))
1. `ai_request` is defined in `Llm::Completions::Chat` and evaluates to
`AiGateway`([code](https://gitlab.com/gitlab-org/gitlab/-/blob/971d07aa37d9f300b108ed66304505f2d7022841/ee/lib/gitlab/llm/completions/chat.rb#L51-51))
1. `ai_request.request` routes to `Llm::Chain::Requests::AiGateway#request`,
which calls `ai_client.stream`
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/requests/ai_gateway.rb#L20-27))
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/requests/ai_gateway.rb#L20-27))
1. `ai_client.stream` routes to `Gitlab::Llm::AiGateway::Client#stream`, which
makes an API request to the AI Gateway `/v1/chat/agent` endpoint
makes an API request to the AI Gateway `/v2/chat/agent` endpoint
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/ai_gateway/client.rb#L64-82))
1. AI Gateway receives the request
([code](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/e6f55d143ecb5409e8ca4fefc042e590e5a95158/ai_gateway/api/v2/chat/agent.py#L43-43))
1. AI Gateway gets the list of tools available for user
([code](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/e6f55d143ecb5409e8ca4fefc042e590e5a95158/ai_gateway/chat/toolset.py#L43-43))
1. AI GW gets definitions for each tool
([code](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/e6f55d143ecb5409e8ca4fefc042e590e5a95158/ai_gateway/chat/tools/gitlab.py#L11-11))
1. And they are inserted into prompt template alongside other prompt parameters that come from Rails
([code](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/e6f55d143ecb5409e8ca4fefc042e590e5a95158/ai_gateway/agents/definitions/chat/react/base.yml#L14-14))
1. AI Gateway makes request to LLM and return response to Rails.
([code](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/e6f55d143ecb5409e8ca4fefc042e590e5a95158/ai_gateway/api/v2/chat/agent.py#L103-103))
1. We've now made our first request to the AI Gateway. If the LLM says that the
answer to the first request is a final answer, we
[parse the answer](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/parsers/chain_of_thought_parser.rb)
and return it ([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/agents/zero_shot/executor.rb#L47))
1. (`*`) If the first answer is not final, the "thoughts" and "picked tools"
[parse the answer](https://gitlab.com/gitlab-org/gitlab/-/blob/971d07aa37d9f300b108ed66304505f2d7022841/ee/lib/gitlab/llm/chain/parsers/single_action_parser.rb#L41-42)
and stream it ([code](https://gitlab.com/gitlab-org/gitlab/-/blob/971d07aa37d9f300b108ed66304505f2d7022841/ee/lib/gitlab/llm/chain/concerns/ai_dependent.rb#L25-25))
and return it ([code](https://gitlab.com/gitlab-org/gitlab/-/blob/971d07aa37d9f300b108ed66304505f2d7022841/ee/lib/gitlab/llm/chain/agents/single_action_executor.rb#L46-46))
1. If the first answer is not final, the "thoughts" and "picked tools"
from the first LLM request are parsed and then the relevant tool class is
called.
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/e88256b1acc0d70ffc643efab99cad9190529312/ee/lib/gitlab/llm/chain/agents/zero_shot/executor.rb#L56-65))
([code](https://gitlab.com/gitlab-org/gitlab/-/blob/971d07aa37d9f300b108ed66304505f2d7022841/ee/lib/gitlab/llm/chain/agents/single_action_executor.rb#L54-54))
1. The tool executor classes also include `Concerns::AiDependent` and use the
included `request` method similar to how `ZeroShot::Executor` does
included `request` method similar to how the chat executor does
([example](https://gitlab.com/gitlab-org/gitlab/-/blob/70fca6dbec522cb2218c5dcee66caa908c84271d/ee/lib/gitlab/llm/chain/tools/identifier.rb#L8)).
The `request` method uses the same `ai_request` instance
that was injected into the `context` in `Llm::Completions::Chat`. For Chat,
@ -649,11 +660,9 @@ flow of how we construct a Chat prompt:
`prompt` / `PROMPT_TEMPLATE` than for the first request
([Example tool prompt template](https://gitlab.com/gitlab-org/gitlab/-/blob/70fca6dbec522cb2218c5dcee66caa908c84271d/ee/lib/gitlab/llm/chain/tools/issue_identifier/executor.rb#L39-104))
1. If the tool answer is not final, the response is added to `agent_scratchpad`
and the loop in `ZeroShot::Executor` starts again, adding the additional
and the loop in `SingleActionExecutor` starts again, adding the additional
context to the request. It loops to up to 10 times until a final answer is reached.
(`*`) indicates that this step is part of the actual construction of the prompt
## Interpreting GitLab Duo Chat error codes
GitLab Duo Chat has error codes with specified meanings to assist in debugging.

View File

@ -19767,9 +19767,24 @@ msgstr ""
msgid "DuoProDiscover|Accelerate your path to market"
msgstr ""
msgid "DuoProDiscover|Access Chat from the GitLab UI or your preferred IDE."
msgstr ""
msgid "DuoProDiscover|Adopt AI with guardrails"
msgstr ""
msgid "DuoProDiscover|Automates repetitive tasks and helps catch bugs early."
msgstr ""
msgid "DuoProDiscover|Chat from any location"
msgstr ""
msgid "DuoProDiscover|Code explanation"
msgstr ""
msgid "DuoProDiscover|Code refactoring"
msgstr ""
msgid "DuoProDiscover|Committed to transparent AI"
msgstr ""
@ -19782,18 +19797,33 @@ msgstr ""
msgid "DuoProDiscover|Give your developers a single platform that integrates the best AI model for each use case across the entire workflow, from understanding code to fixing security vulnerabilities."
msgstr ""
msgid "DuoProDiscover|Helps you understand code by explaining it in natural language."
msgstr ""
msgid "DuoProDiscover|Improve developer experience"
msgstr ""
msgid "DuoProDiscover|Read documentation"
msgstr ""
msgid "DuoProDiscover|Ship software faster and more securely with AI integrated into your entire DevSecOps lifecycle."
msgstr ""
msgid "DuoProDiscover|Test generation"
msgstr ""
msgid "DuoProDiscover|What's new in GitLab Duo Chat"
msgstr ""
msgid "DuoProDiscover|Why GitLab Duo?"
msgstr ""
msgid "DuoProDiscover|With GitLab Duo, you control which users, projects, and groups can use AI-powered capabilities. Also, your organization's proprietary code and data aren't used to train AI models."
msgstr ""
msgid "DuoProDiscover|Work to improve existing code quality."
msgstr ""
msgid "DuoProTrial|Activate my trial"
msgstr ""

View File

@ -75,13 +75,15 @@ describe('Blob Header Default Actions', () => {
it('renders the WebIdeLink component with the correct props', async () => {
const { ideEditPath, editBlobPath, gitpodBlobUrl, pipelineEditorPath } = Blob;
const showForkSuggestion = false;
await createComponent({ propsData: { showForkSuggestion } });
const showWebIdeForkSuggestion = false;
await createComponent({ propsData: { showForkSuggestion, showWebIdeForkSuggestion } });
expect(findWebIdeLink().props()).toMatchObject({
showEditButton: true,
editUrl: editBlobPath,
webIdeUrl: ideEditPath,
needsToFork: showForkSuggestion,
needsToForkWithWebIde: showWebIdeForkSuggestion,
showPipelineEditorButton: Boolean(pipelineEditorPath),
pipelineEditorUrl: pipelineEditorPath,
gitpodUrl: gitpodBlobUrl,

View File

@ -49,7 +49,7 @@ describe('ItemActions', () => {
it('renders component template correctly', () => {
createComponent();
expect(wrapper.classes()).toContain('gl-display-flex', 'gl-justify-content-end', 'gl-ml-5');
expect(wrapper.classes()).toContain('gl-flex', 'gl-content-end', 'gl-ml-5');
});
it('renders "Edit" group button with correct attribute values', () => {

View File

@ -544,24 +544,27 @@ describe('Blob content viewer component', () => {
});
it.each`
loggedIn | canModifyBlob | createMergeRequestIn | forkProject | showForkSuggestion
${true} | ${false} | ${true} | ${true} | ${true}
${false} | ${false} | ${true} | ${true} | ${false}
${true} | ${true} | ${false} | ${true} | ${false}
${true} | ${true} | ${true} | ${false} | ${false}
loggedIn | canModifyBlob | isUsingLfs | createMergeRequestIn | forkProject | showSingleFileEditorForkSuggestion
${true} | ${true} | ${false} | ${true} | ${true} | ${false}
${true} | ${false} | ${false} | ${true} | ${true} | ${true}
${false} | ${false} | ${false} | ${true} | ${true} | ${false}
${true} | ${false} | ${false} | ${false} | ${true} | ${false}
${true} | ${false} | ${false} | ${true} | ${false} | ${false}
${true} | ${false} | ${true} | ${true} | ${true} | ${false}
`(
'shows/hides a fork suggestion according to a set of conditions',
async ({
loggedIn,
canModifyBlob,
isUsingLfs,
createMergeRequestIn,
forkProject,
showForkSuggestion,
showSingleFileEditorForkSuggestion,
}) => {
isLoggedIn.mockReturnValueOnce(loggedIn);
await createComponent(
{
blob: { ...simpleViewerMock, canModifyBlob },
blob: { ...simpleViewerMock, canModifyBlob, storedExternally: isUsingLfs },
createMergeRequestIn,
forkProject,
},
@ -571,7 +574,42 @@ describe('Blob content viewer component', () => {
findBlobHeader().vm.$emit('edit', 'simple');
await nextTick();
expect(findForkSuggestion().exists()).toBe(showForkSuggestion);
expect(findForkSuggestion().exists()).toBe(showSingleFileEditorForkSuggestion);
},
);
it.each`
loggedIn | canModifyBlobWithWebIde | isUsingLfs | createMergeRequestIn | forkProject | showWebIdeForkSuggestion
${true} | ${true} | ${false} | ${true} | ${true} | ${false}
${true} | ${false} | ${false} | ${true} | ${true} | ${true}
${false} | ${false} | ${false} | ${true} | ${true} | ${false}
${true} | ${false} | ${false} | ${false} | ${true} | ${false}
${true} | ${false} | ${false} | ${true} | ${false} | ${false}
${true} | ${false} | ${true} | ${true} | ${true} | ${false}
`(
'shows/hides a fork suggestion for WebIDE according to a set of conditions',
async ({
loggedIn,
canModifyBlobWithWebIde,
isUsingLfs,
createMergeRequestIn,
forkProject,
showWebIdeForkSuggestion,
}) => {
isLoggedIn.mockReturnValueOnce(loggedIn);
await createComponent(
{
blob: { ...simpleViewerMock, canModifyBlobWithWebIde, storedExternally: isUsingLfs },
createMergeRequestIn,
forkProject,
},
mount,
);
findBlobHeader().vm.$emit('edit', 'ide');
await nextTick();
expect(findForkSuggestion().exists()).toBe(showWebIdeForkSuggestion);
},
);
});

View File

@ -21,6 +21,7 @@ export const simpleViewerMock = {
environmentFormattedExternalUrl: '',
environmentExternalUrlForRouteMap: '',
canModifyBlob: true,
canModifyBlobWithWebIde: true,
canCurrentUserPushToBranch: true,
archived: false,
storedExternally: false,

View File

@ -166,7 +166,7 @@ describe('vue_shared/components/web_ide_link', () => {
expectedActions: [ACTION_WEB_IDE_EDIT_FORK, ACTION_EDIT],
},
{
props: { needsToFork: true },
props: { needsToFork: true, needsToForkWithWebIde: true },
expectedActions: [ACTION_WEB_IDE_CONFIRM_FORK, ACTION_EDIT_CONFIRM_FORK],
},
{
@ -321,7 +321,12 @@ describe('vue_shared/components/web_ide_link', () => {
it.each(testActions)(
'emits the correct event when an action handler is called',
({ props, expectedEventPayload }) => {
createComponent({ ...props, needsToFork: true, disableForkModal: true });
createComponent({
...props,
needsToFork: true,
needsToForkWithWebIde: true,
disableForkModal: true,
});
findDisclosureDropdownItems().at(0).props().item.handle();
@ -330,7 +335,7 @@ describe('vue_shared/components/web_ide_link', () => {
);
it.each(testActions)('renders the fork confirmation modal', ({ props }) => {
createComponent({ ...props, needsToFork: true });
createComponent({ ...props, needsToFork: true, needsToForkWithWebIde: true });
expect(findForkConfirmModal().exists()).toBe(true);
expect(findForkConfirmModal().props()).toEqual({
@ -341,7 +346,10 @@ describe('vue_shared/components/web_ide_link', () => {
});
it.each(testActions)('opens the modal when the button is clicked', async ({ props }) => {
createComponent({ ...props, needsToFork: true }, { mountFn: mountExtended });
createComponent(
{ ...props, needsToFork: true, needsToForkWithWebIde: true },
{ mountFn: mountExtended },
);
findDisclosureDropdownItems().at(0).props().item.handle();

View File

@ -461,7 +461,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler, feature_category: :se
expect do
receiver.execute
end.to enqueue_mail_with(Notify, :service_desk_thank_you_email)
.and(not_enqueue_mail_with(Notify, :note_issue_email))
.and(not_enqueue_mail_with(Notify, :service_desk_new_note_email))
end
end
end