GitLab Duo Agent Platform
GitLab v19.5요약
이 가이드는 GitLab Duo Agent Platform으로 작업하는 방법을 설명합니다. GitLab Duo Agent Platform은 Vue.js로 만든 단일 페이지 애플리케이션(Single Page Application, SPA)으로, AI 기반 자동화 기능을 위한 통합 인터페이스를 제공합니다.
이 가이드는 GitLab Duo Agent Platform으로 작업하는 방법을 설명합니다.
개요#
GitLab Duo Agent Platform은 Vue.js로 만든 단일 페이지 애플리케이션(Single Page Application, SPA)으로, AI 기반 자동화 기능을 위한 통합 인터페이스를 제공합니다. 이 플랫폼은 여러 내비게이션 항목이 /automate 경로 아래에 함께 존재할 수 있게 하는 스코프 라우팅 시스템을 사용합니다.
이 플랫폼은 유연한 네임스페이스 시스템으로 설계되어, 동일한 프론트엔드 인프라를 서로 다른 컨텍스트(프로젝트, 그룹 등)에서 재사용하면서도 컴포넌트 매핑 시스템으로 컨텍스트별 기능을 제공합니다.
네임스페이스 아키텍처#
네임스페이스 시스템은 다음과 같은 중앙 매핑 메커니즘을 중심으로 구성됩니다.
- 네임스페이스를 확인합니다 - 플랫폼이 어느 컨텍스트에서 실행되는지 판별합니다
- Vue 컴포넌트에 매핑합니다 - 해당 네임스페이스에 맞는 Vue 컴포넌트로 라우팅합니다
- GraphQL 쿼리를 props로 전달합니다 - 의존성 주입으로 네임스페이스별 데이터를 제공합니다
진입점#
주 진입점의 위치는 다음과 같습니다.
ee/app/assets/javascripts/pages/projects/duo_agents_platform/index.js
이 파일이 플랫폼을 가져와 초기화합니다.
import { initDuoAgentsPlatformProjectPage } from 'ee/ai/duo_agents_platform/namespace/project';
initDuoAgentsPlatformProjectPage();
앱 구조#
소스 코드 보기
graph TD
A[Entry Point] --> B[initDuoAgentsPlatformPage]
B --> C[Extract Namespace Data]
C --> D[Create Router with Namespace]
D --> E[Component Mapping]
E --> F[Namespace-Specific Component]
F --> G[GraphQL Query with Props]
G --> H[Rendered UI]
I[Dataset Properties] --> C
J[Namespace Constant] --> E
K[Component Mappings] --> E</code></pre></details></div>
새 내비게이션 항목 추가#
GitLab Duo Agent Platform은 Vue Router 구성이 브레드크럼 내비게이션을 직접 구동하는 라우터 기반 내비게이션 시스템을 사용합니다. 핵심은 ee/app/assets/javascripts/ai/duo_agents_platform/router/index.js의 라우터 구조가 URL 구조와 브레드크럼 계층 구조를 모두 결정한다는 점입니다.
라우터 기반 내비게이션 동작 방식#
이 시스템은 서로 연결된 다음 구성 요소로 동작합니다.
- 라우터 구조:
meta.text 속성을 가진 중첩 라우트가 브레드크럼 레이블을 정의합니다
- 브레드크럼 컴포넌트:
duo_agents_platform_breadcrumbs.vue가 일치한 라우트에서 브레드크럼을 자동으로 생성합니다
- 라우트 주입:
index.js의 injectVueAppBreadcrumbs()가 라우터를 브레드크럼 시스템에 연결합니다
라우터 분석#
ee/app/assets/javascripts/ai/duo_agents_platform/router/index.js의 현재 라우터 구조는 다음과 같습니다.
routes: [
{
component: NestedRouteApp, // Simple <router-view /> wrapper
path: '/agent-sessions',
meta: {
text: s__('DuoAgentsPlatform|Sessions'), // This becomes a breadcrumb
},
children: [
{
name: AGENTS_PLATFORM_INDEX_ROUTE,
path: '', // Matches /agent-sessions exactly
component: AgentsPlatformIndex,
},
{
name: AGENTS_PLATFORM_NEW_ROUTE,
path: 'new', // Matches /agent-sessions/new
component: AgentsPlatformNew,
meta: {
text: s__('DuoAgentsPlatform|New'), // This becomes a breadcrumb
},
},
// ...
],
},
];
브레드크럼 생성#
브레드크럼 컴포넌트(duo_agents_platform_breadcrumbs.vue)는 다음과 같이 동작합니다.
this.$route.matched에서 일치한 모든 라우트를 가져옵니다
- 일치한 각 라우트에서
meta.text를 추출합니다
- 해당 텍스트와 라우트 경로로 브레드크럼 항목을 만듭니다
- 정적 브레드크럼("Automate" 등)과 결합합니다
// From duo_agents_platform_breadcrumbs.vue
const matchedRoutes = (this.$route?.matched || [])
.map((route) => {
return {
text: route.meta?.text, // Uses meta.text for breadcrumb label
to: { path: route.path },
};
})
.filter((r) => r.text); // Only routes with meta.text become breadcrumbs
새 내비게이션 항목 추가#
새 최상위 내비게이션 항목("Your Feature" 같은)을 추가하려면 라우터에 새 라우트 트리를 추가해야 합니다.
1단계: 라우트 상수 추가#
파일: ee/app/assets/javascripts/ai/duo_agents_platform/router/constants.js
// Add route name constants for your feature
export const AGENTS_PLATFORM_YOUR_FEATURE_INDEX = 'your_feature_index';
export const AGENTS_PLATFORM_YOUR_FEATURE_NEW = 'your_feature_new';
export const AGENTS_PLATFORM_YOUR_FEATURE_SHOW = 'your_feature_show';
2단계: 라우터에 라우트 추가#
파일: ee/app/assets/javascripts/ai/duo_agents_platform/router/index.js
import YourFeatureIndex from '../pages/your_feature/your_feature_index.vue';
import YourFeatureNew from '../pages/your_feature/your_feature_new.vue';
import YourFeatureShow from '../pages/your_feature/your_feature_show.vue';
export const createRouter = (base, namespace) => {
return new VueRouter({
base,
mode: 'history',
routes: [
// Existing agent-sessions routes
{
component: NestedRouteApp,
path: '/agent-sessions',
meta: {
text: s__('DuoAgentsPlatform|Sessions'),
},
children: [
{
name: AGENTS_PLATFORM_INDEX_ROUTE,
path: '',
component: getNamespaceIndexComponent(namespace),
},
// ... existing children
],
},
// NEW: Your feature routes
{
component: NestedRouteApp,
path: '/your-feature', // This becomes the URL path
meta: {
text: s__('DuoAgentsPlatform|Your Feature'), // This becomes the breadcrumb
},
children: [
{
name: AGENTS_PLATFORM_YOUR_FEATURE_INDEX,
path: '', // Matches /your-feature exactly
component: YourFeatureIndex,
},
{
name: AGENTS_PLATFORM_YOUR_FEATURE_NEW,
path: 'new', // Matches /your-feature/new
component: YourFeatureNew,
meta: {
text: s__('DuoAgentsPlatform|New'), // Breadcrumb: "Your Feature > New"
},
},
{
name: AGENTS_PLATFORM_YOUR_FEATURE_SHOW,
path: ':id(\\d+)', // Matches /your-feature/123
component: YourFeatureShow,
// No meta.text - will use route param as breadcrumb
},
],
},
{ path: '*', redirect: '/agent-sessions' },
],
});
};
3단계: 백엔드 라우트 추가(필요한 경우)#
ee/config/routes/project.rb의 기존 와일드카드 라우트가 새 경로를 처리합니다.
scope :automate do
get '/(*vueroute)' => 'duo_agents_platform#show', as: :automate, format: false
end
중요: 사이드바 메뉴 항목을 추가하는 경우(4단계)에는 이름이 지정된 라우트 헬퍼를 추가해야 합니다.
scope :automate do
get '/(*vueroute)' => 'duo_agents_platform#show', as: :automate, format: false
# Named routes for sidebar menu helpers
get 'agent-sessions', to: 'duo_agents_platform#show', as: :automate_agent_sessions, format: false
get 'your-feature', to: 'duo_agents_platform#show', as: :automate_your_features, format: false
end
Note
기존 패턴과 맞추고 Rails 경로 헬퍼가 올바르게 생성되도록 라우트 이름에는 복수형(예: automate_your_features)을 사용합니다.
4단계: 사이드바 메뉴 항목 추가#
파일: ee/lib/sidebars/projects/super_sidebar_menus/duo_agents_menu.rb
configure_menu_items 메서드에 메뉴 항목을 추가하고 그에 대응하는 메뉴 항목 메서드를 만듭니다.
override :configure_menu_items
def configure_menu_items
add_item(duo_agents_runs_menu_item)
add_item(duo_agents_your_feature_menu_item) # Add your new menu item
true
end
private
def duo_agents_your_feature_menu_item
::Sidebars::MenuItem.new(
title: s_('Your Feature'),
link: project_automate_your_features_path(context.project), # Note: plural 'features'
active_routes: { controller: :duo_agents_platform },
item_id: :agents_your_feature
)
end
5단계: Vue 컴포넌트 생성#
파일: ee/app/assets/javascripts/ai/duo_agents_platform/pages/your_feature/your_feature_index.vue
<script>
export default {
name: 'YourFeatureIndex',
};
</script>
<template>
<div>
<h1>Your Feature</h1>
</div>
</template>
새 네임스페이스 추가#
새 네임스페이스(예: 그룹용)를 추가하는 방법은 다음과 같습니다.
1. 네임스페이스 상수 정의#
// ee/app/assets/javascripts/ai/duo_agents_platform/constants.js
export const AGENT_PLATFORM_GROUP_PAGE = 'group';
2. 네임스페이스 디렉터리 구조 생성#
ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/
├── index.js
├── group_agents_platform_index.vue
└── graphql/
└── queries/
└── get_group_agent_flows.query.graphql
3. 네임스페이스 초기화 구현#
// ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/index.js
import { initDuoAgentsPlatformPage } from '../../index';
import { AGENT_PLATFORM_GROUP_PAGE } from '../../constants';
export const initDuoAgentsPlatformGroupPage = () => {
initDuoAgentsPlatformPage({
namespace: AGENT_PLATFORM_GROUP_PAGE,
namespaceDatasetProperties: ['groupPath', 'groupId'],
});
};
4. 네임스페이스별 컴포넌트 생성#
<!-- ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/group_agents_platform_index.vue -->
<script>
import getGroupAgentFlows from './graphql/queries/get_group_agent_flows.query.graphql';
import DuoAgentsPlatformIndex from '../../pages/index/duo_agents_platform_index.vue';
export default {
components: { DuoAgentsPlatformIndex },
inject: ['groupPath'], // Group-specific injection
apollo: {
workflows: {
query: getGroupAgentFlows, // Group-specific query
variables() {
return {
groupPath: this.groupPath,
// ...
};
},
// ...
},
},
};
</script>
<template>
<duo-agents-platform-index
:is-loading-workflows="isLoadingWorkflows"
:workflows="workflows"
:workflows-page-info="workflowsPageInfo"
:workflow-query="$apollo.queries.workflows"
/>
</template>
5. 컴포넌트 매핑 업데이트#
// ee/app/assets/javascripts/ai/duo_agents_platform/router/utils.js
import { AGENT_PLATFORM_PROJECT_PAGE, AGENT_PLATFORM_GROUP_PAGE } from '../constants';
import ProjectAgentsPlatformIndex from '../namespace/project/project_agents_platform_index.vue';
import GroupAgentsPlatformIndex from '../namespace/group/group_agents_platform_index.vue';
export const getNamespaceIndexComponent = (namespace) => {
const componentMappings = {
[AGENT_PLATFORM_PROJECT_PAGE]: ProjectAgentsPlatformIndex,
[AGENT_PLATFORM_GROUP_PAGE]: GroupAgentsPlatformIndex, // New mapping
};
return componentMappings[namespace];
};
6. 진입점 생성#
// ee/app/assets/javascripts/pages/groups/duo_agents_platform/index.js
import { initDuoAgentsPlatformGroupPage } from 'ee/ai/duo_agents_platform/namespace/group';
initDuoAgentsPlatformGroupPage();
이 아키텍처의 장점#
- 코드 재사용: 동일한 프론트엔드 인프라가 서로 다른 컨텍스트에서 동작합니다
- 관심사의 분리: 각 네임스페이스가 자체 데이터 페칭과 비즈니스 로직을 처리합니다
- 확장성: 기존 코드를 수정하지 않고 새 네임스페이스를 쉽게 추가할 수 있습니다
- 타입 안전성: 네임스페이스 상수와 필수 속성으로 계약이 명확합니다
- 유지 관리성: 네임스페이스 구현이 서로 격리되어 결합도가 낮아집니다
주요 구현 세부 사항#
라우터 기반 브레드크럼#
브레드크럼 시스템은 라우터 구조에서 내비게이션을 자동으로 생성합니다.
meta.text가 있는 상위 라우트는 브레드크럼 구간이 됩니다
meta.text가 있는 하위 라우트는 브레드크럼 체인을 확장합니다
meta.text가 없는 라우트는 라우트 파라미터(:id 등)를 브레드크럼 텍스트로 사용합니다
- 정적 브레드크럼("Automate" 등)은 모든 라우트 앞에 붙습니다
URL 구조#
최상위 내비게이션 항목마다 자체 URL 네임스페이스를 가집니다.
- Sessions:
/automate/agent-sessions, /automate/agent-sessions/new, /automate/agent-sessions/123
- Your Feature:
/automate/your-feature, /automate/your-feature/new, /automate/your-feature/456
중첩 라우트 패턴#
이 플랫폼은 일관된 중첩 라우트 패턴을 사용합니다.
- 상위 라우트: URL 경로와 최상위 브레드크럼을 정의합니다
- NestedRouteApp 컴포넌트: 하위 라우트를 위한 단순한
<router-view /> 래퍼입니다
- 하위 라우트: 개별 페이지와 추가 브레드크럼 구간을 정의합니다
예시: 현재 에이전트 세션 구현#
기존 에이전트 세션 기능이 이 패턴을 보여 줍니다.
{
component: NestedRouteApp, // Renders child routes
path: '/agent-sessions', // URL: /automate/agent-sessions
meta: {
text: s__('DuoAgentsPlatform|Sessions'), // Breadcrumb: "Sessions"
},
children: [
{
name: AGENTS_PLATFORM_INDEX_ROUTE,
path: '', // URL: /automate/agent-sessions
component: AgentsPlatformIndex, // No additional breadcrumb
},
{
name: AGENTS_PLATFORM_NEW_ROUTE,
path: 'new', // URL: /automate/agent-sessions/new
component: AgentsPlatformNew,
meta: {
text: s__('DuoAgentsPlatform|New'), // Breadcrumb: "Sessions > New"
},
},
{
name: AGENTS_PLATFORM_SHOW_ROUTE,
path: ':id(\\d+)', // URL: /automate/agent-sessions/123
component: AgentsPlatformShow, // Breadcrumb: "Sessions > 123"
// No meta.text - uses :id parameter as breadcrumb
},
],
}
이렇게 하면 다음 브레드크럼 계층 구조가 만들어집니다.
/automate/agent-sessions → "Automate > Sessions"
/automate/agent-sessions/new → "Automate > Sessions > New"
/automate/agent-sessions/123 → "Automate > Sessions > 123"
주요 파일 참조#
- 진입점:
ee/app/assets/javascripts/pages/projects/duo_agents_platform/index.js
- 주 초기화:
ee/app/assets/javascripts/ai/duo_agents_platform/index.js
- 라우터:
ee/app/assets/javascripts/ai/duo_agents_platform/router/index.js
- 컴포넌트 매핑:
ee/app/assets/javascripts/ai/duo_agents_platform/router/utils.js
- 상수:
ee/app/assets/javascripts/ai/duo_agents_platform/constants.js
- 유틸리티:
ee/app/assets/javascripts/ai/duo_agents_platform/utils.js
- 프로젝트 네임스페이스:
ee/app/assets/javascripts/ai/duo_agents_platform/namespace/project/
모범 사례#
- 라우터 구조:
meta.text 속성을 가진 중첩 라우트로 브레드크럼 계층 구조를 정의합니다
- URL 이름 지정: URL 경로에는 kebab-case를 사용합니다(
/yourFeature가 아니라 /your-feature)
- 라우트 이름: 라우트 이름에는 의미가 분명한 상수를 사용합니다(
AGENTS_PLATFORM_YOUR_FEATURE_INDEX)
- 컴포넌트 구성: 기능 컴포넌트는
pages/[feature]/ 디렉터리에 둡니다
- 브레드크럼 텍스트: 모든
meta.text 값에는 국제화 문자열(s__())을 사용합니다
- 일관된 패턴:
NestedRouteApp을 사용하는 기존 중첩 라우트 패턴을 따릅니다
- 네임스페이스 분리: 네임스페이스별 로직은
namespace/ 아래의 전용 디렉터리에 둡니다
- 컴포넌트 매핑: 컴포넌트 매핑 시스템으로 네임스페이스별 컴포넌트에 라우팅합니다
- 데이터 주입: 네임스페이스별 데이터에는 Vue의 provide/inject 패턴을 사용합니다
문제 해결#
일반적인 문제#
- 브레드크럼이 표시되지 않음: 상위 라우트에
meta.text 속성이 있는지 확인합니다
- 라우트가 동작하지 않음:
ee/config/routes/project.rb에 와일드카드 라우트가 있는지 확인합니다
- 사이드바가 강조되지 않음: 사이드바 메뉴 항목의
active_routes가 올바른지 확인합니다
- 404 오류: 라우트 경로가 기존 라우트와 충돌하지 않는지 확인합니다
디버깅 팁#
- Vue DevTools:
$route.matched를 검사해 어떤 라우트가 일치하는지 확인합니다
- 라우터 상태: Vue DevTools에서 라우터 구성을 확인합니다
- 브레드크럼 디버그: 브레드크럼 컴포넌트에
console.log(this.$route.matched)를 추가합니다
- URL 테스트: 브라우저에서 URL로 직접 이동해 라우트를 테스트합니다
가짜 플로 생성#
플랫폼을 시험해 볼 가짜 플로를 생성하려면
ee/lib/tasks/gitlab/duo_workflow/duo_workflow.rake Rake 태스크를 실행할 수 있습니다.
특정 프로젝트에서 플로 50 개를 만들고 그중 20 개는 지정한 사용자가 만들도록 하는 예
bundle exec rake "gitlab:duo_workflow:populate[50,20,user@example.com,gitlab-org/gitlab-test]