Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/console/public/assets/ai-tools/jira.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions apps/console/src/routeTree.gen.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
import { AgenticWorkflowConfiguration } from '@qovery/domains/services/feature'
import { useDocumentTitle } from '@qovery/shared/util-hooks'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration'
)({
component: RouteComponent,
})

function RouteComponent() {
useDocumentTitle('Agentic workflow configuration')

return <AgenticWorkflowConfiguration />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Navigate, createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/'
)({
component: RouteComponent,
})

function RouteComponent() {
const { organizationId, projectId, environmentId } = Route.useParams()

return (
<Navigate
to="/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/configuration"
params={{ organizationId, projectId, environmentId }}
replace
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Outlet, createFileRoute, useNavigate } from '@tanstack/react-router'
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useEffect } from 'react'
import { AgenticWorkflowCreationFlow } from '@qovery/domains/services/feature'
import { serviceCreateParamsSchema } from '@qovery/shared/router'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow'
)({
component: RouteComponent,
validateSearch: serviceCreateParamsSchema,
})

function RouteComponent() {
const { organizationId, projectId, environmentId } = Route.useParams()
const navigate = useNavigate()
const isAgenticWorkflowEnabled = Boolean(useFeatureFlagEnabled('argentic-workflow'))
const creationFlowUrl = `/organization/${organizationId}/project/${projectId}/environment/${environmentId}/service/create/agentic-workflow`

useEffect(() => {
if (!isAgenticWorkflowEnabled) {
navigate({
to: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/new',
params: { organizationId, projectId, environmentId },
})
}
}, [environmentId, isAgenticWorkflowEnabled, navigate, organizationId, projectId])

if (!isAgenticWorkflowEnabled) return null

return (
<AgenticWorkflowCreationFlow
creationFlowUrl={creationFlowUrl}
onExit={() =>
navigate({
to: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/new',
params: { organizationId, projectId, environmentId },
})
}
>
<Outlet />
</AgenticWorkflowCreationFlow>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
import { AgenticWorkflowSummary } from '@qovery/domains/services/feature'
import { useDocumentTitle } from '@qovery/shared/util-hooks'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow/summary'
)({
component: RouteComponent,
})

function RouteComponent() {
useDocumentTitle('Summary - Create agentic workflow')

return <AgenticWorkflowSummary />
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const hiddenProgressCardRouteIds: FileRouteTypes['id'][] = [
'/_authenticated/organization/$organizationId/cluster/create/$slug',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ const bypassLayoutRouteIds: FileRouteTypes['id'][] = [
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/blueprint/$provider/$serviceFamily',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/agentic-workflow',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/terraform',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/lifecycle-job',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createQueryKeys, type inferQueryKeys } from '@lukemorales/query-key-factory'
import {
type AgenticWorkflowRequest,
AgenticWorkflowsApi,
ApplicationActionsApi,
type ApplicationAdvancedSettings,
ApplicationConfigurationApi,
Expand Down Expand Up @@ -104,6 +106,7 @@ const helmsApi = new HelmsApi()
const terraformsApi = new TerraformsApi()
const blueprintApi = new BlueprintMainCallsApi()
const blueprintCatalogApi = new BlueprintCatalogApi()
const agenticWorkflowsApi = new AgenticWorkflowsApi()

const applicationMainCallsApi = new ApplicationMainCallsApi()
const containerMainCallsApi = new ContainerMainCallsApi()
Expand Down Expand Up @@ -780,6 +783,11 @@ type UpdateBlueprintRequest = {
payload: BlueprintUpdateRequest
}

type CreateAgenticWorkflowRequest = {
environmentId: string
payload: AgenticWorkflowRequest
}

type EditServiceRequest = {
serviceId: string
payload:
Expand Down Expand Up @@ -1061,6 +1069,10 @@ export const mutations = {
const response = await blueprintApi.updateBlueprint(blueprintId, payload)
return response.data
},
async createAgenticWorkflow({ environmentId, payload }: CreateAgenticWorkflowRequest) {
const response = await agenticWorkflowsApi.createAgenticWorkflow(environmentId, payload)
return response.data
},
async editService({ serviceId, payload }: EditServiceRequest) {
const { mutation } = match(payload)
.with({ serviceType: 'APPLICATION' }, ({ serviceType, ...payload }) => ({
Expand Down
4 changes: 4 additions & 0 deletions libs/domains/services/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export * from './lib/service-creation-flow/database/database-summary-view/databa
export * from './lib/service-creation-flow/database/step-summary/step-summary'
export * from './lib/service-creation-flow/blueprint/blueprint-creation-flow'
export * from './lib/service-update-flow/blueprint/blueprint-update-flow'
export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-context'
export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration'
export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-summary'
export * from './lib/hooks/use-create-agentic-workflow/use-create-agentic-workflow'
export * from './lib/application-container-healthchecks/application-container-healthchecks-form/application-container-healthchecks-form'
export * from './lib/application-container-healthchecks/healthchecks-utils'
export * from './lib/application-container-healthchecks/step-healthchecks/step-healthchecks'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { type AgenticWorkflowHeader, type AgenticWorkflowRequest } from 'qovery-typescript-axios'
import { mutations } from '@qovery/domains/services/data-access'
import { queries } from '@qovery/state/util-queries'
import { type AgenticWorkflowFormData } from '../../service-creation-flow/agentic-workflow/agentic-workflow-context'

function formatWhitelistHosts(value: string) {
return value
.split(',')
.map((host) => host.trim())
.filter(Boolean)
}

function parseHeaders(headersJson: string): AgenticWorkflowHeader[] {
if (!headersJson.trim()) {
return []
}

const parsedValue = JSON.parse(headersJson)

if (!parsedValue || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
return []
}

return Object.entries(parsedValue)
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
.map(([name, value]) => ({ name, value }))
}

export function formatAgenticWorkflowRequest(values: AgenticWorkflowFormData): AgenticWorkflowRequest {
return {
name: values.name,
description: values.description,
docker_fragment: values.dockerFragment,
enabled: values.workflowEnabled,
mcp: values.mcpJson.trim() || undefined,
outputs: values.outputs.map((output, index) => ({
name: `Output ${index + 1}`,
url: output.url,
headers: parseHeaders(output.headersJson),
instructions: output.prompt,
})),
model: {
type: values.aiModel,
api_key: values.modelApiKey,
settings: values.modelSettingsJson,
},
project_repositories: values.gitRepositories.map((repository) => ({
url: repository.repository,
branch: repository.branch,
git_token_id: repository.gitTokenId ?? '',
})),
agent_prompt: values.agentPrompt,
governance: {
host_allowlist: formatWhitelistHosts(values.whitelistHosts),
},
}
}

export function useCreateAgenticWorkflow({ environmentId }: { environmentId: string }) {
const queryClient = useQueryClient()

return useMutation(mutations.createAgenticWorkflow, {
onSuccess() {
queryClient.invalidateQueries({
queryKey: queries.services.list(environmentId).queryKey,
})
},
meta: {
notifyOnSuccess: {
title: 'Your agentic workflow has been created',
},
notifyOnError: true,
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getJsonError, isGitRepositoryComplete, isOutputComplete } from './agentic-workflow-configuration'

describe('AgenticWorkflowConfiguration validation', () => {
it('should require valid JSON only when a required JSON field is empty or invalid', () => {
expect(getJsonError('', false)).toBeUndefined()
expect(getJsonError('', true)).toBe('Please enter a valid JSON configuration.')
expect(getJsonError('{"mcpServers":{}}', true)).toBeUndefined()
expect(getJsonError('{invalid', true)).toBe('Invalid JSON format.')
})

it('should require token, repository, and branch for configured repositories', () => {
expect(
isGitRepositoryComplete({
provider: 'GITHUB',
repository: 'https://github.com/qovery/console',
branch: 'main',
})
).toBe(true)

expect(
isGitRepositoryComplete({
provider: 'GITHUB',
repository: 'https://github.com/qovery/console',
branch: '',
})
).toBe(false)
})

it('should require a webhook URL for configured output webhooks', () => {
expect(isOutputComplete({ url: 'https://hooks.example.com/workflow', headersJson: '{}', prompt: '' })).toBe(true)
expect(isOutputComplete({ url: '', headersJson: '{}', prompt: 'Notify the team.' })).toBe(false)
})
})
Loading
Loading