Skip to content
Merged
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
15 changes: 14 additions & 1 deletion .github/workflows/post-merge.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Post-Merge Checks

on:
workflow_dispatch:
push:
branches: ['main']

Expand Down Expand Up @@ -52,6 +53,12 @@ jobs:
- 'api/tests/integration_tests/.env.example'
- 'api/clients/agent_backend/**'
- 'api/core/app/apps/agent_app/**'
- 'api/core/model_manager.py'
- 'api/core/provider_manager.py'
- 'api/controllers/console/workspace/models.py'
- 'api/controllers/console/workspace/model_providers.py'
- 'api/services/model_provider/**'
- 'api/tasks/install_default_plugins_task.py'
- 'api/core/workflow/nodes/agent_v2/**'
- 'api/controllers/console/agent/**'
- 'api/controllers/console/app/agent*.py'
Expand All @@ -70,6 +77,12 @@ jobs:
- 'api/services/entities/agent_tool_inner.py'
- 'api/services/errors/agent_tool_inner.py'
- 'api/services/agent/**'
- 'api/services/app/agent_*.py'
- 'api/repositories/app/agent_*.py'
- 'api/extensions/application_services/agent.py'
- 'api/models/agent.py'
- 'api/models/agent_config_entities.py'
- 'api/services/entities/agent_entities.py'
- 'api/core/plugin/**'
- 'api/services/plugin/**'
- 'api/core/tools/**'
Expand Down Expand Up @@ -112,7 +125,7 @@ jobs:
external-e2e:
name: External Runtime E2E
needs: check-changes
if: needs.check-changes.outputs.external-e2e-changed == 'true'
if: github.event_name == 'workflow_dispatch' || needs.check-changes.outputs.external-e2e-changed == 'true'
permissions:
actions: read
contents: read
Expand Down
1 change: 1 addition & 0 deletions e2e/features/agent-v2/configure-persistence.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Feature: Agent v2 configure persistence
@prepared @configure-persistence
Scenario: A new Agent shows the workspace default model in Configure
Given I am signed in as the default E2E admin
And the workspace default chat model is configured
And an Agent v2 test agent has been created via API
When I open the Agent v2 configure page
Then I should see the workspace default chat model in the Agent v2 model selector
Expand Down
131 changes: 77 additions & 54 deletions e2e/features/agent-v2/support/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import type {
AgentSoulDifyToolConfig,
} from '@dify/contracts/api/console/agent/types.gen'
import type { KnowledgeConfig } from '@dify/contracts/api/console/datasets/types.gen'
import type { ModelType } from '@dify/contracts/api/console/workspaces/types.gen'
import type { ModelType, ParserPostDefault } from '@dify/contracts/api/console/workspaces/types.gen'
import type { SeedContext, SeedResource, SeedTask } from '../../../support/seed.ts'
import { readFile } from 'node:fs/promises'
import { zModelType } from '@dify/contracts/api/console/workspaces/zod.gen'
import { createTestApp } from '../../../support/api/apps.ts'
import { bootstrapMarketplacePlugins } from '../../../support/marketplace-plugins.ts'
import { sleep } from '../../../support/process.ts'
Expand Down Expand Up @@ -305,67 +306,87 @@ const seedAgentDecisionModel = async (context: SeedContext) =>
title: agentBuilderPreseededResources.agentDecisionChatModel,
})

const seedSpeechToTextModel = async (context: SeedContext) => {
const config = speechToTextModelConfig()
const title = agentBuilderPreseededResources.speechToTextModel
const modelResult = await seedModel(context, { config, title })
if (modelResult.status === 'blocked' || modelResult.status === 'skipped') return modelResult
const seedSpeechToTextModel = async (context: SeedContext) =>
seedModel(context, {
config: speechToTextModelConfig(),
title: agentBuilderPreseededResources.speechToTextModel,
})

const model = await findModel(context.consoleClient, config)
if (!model || model.status !== activeModelStatus)
return blocked(title, `${config.provider}/${config.name} is not active after model setup.`)
const readWorkspaceDefaultModels = async (context: SeedContext) =>
new Map(
await Promise.all(
zModelType.options.map(async (modelType) => {
const response = await context.consoleClient.workspaces.current.defaultModel.get({
query: { model_type: modelType },
})
return [modelType, response.data] as const
}),
),
)

const resource = {
id: `${model.provider}/${model.name}`,
kind: 'model',
name: title,
}
const defaultModelResponse = await context.consoleClient.workspaces.current.defaultModel.get({
query: { model_type: config.type },
})
const defaultModel = defaultModelResponse.data
const isExpectedDefault =
defaultModel?.model === model.name &&
matchesProvider(defaultModel.provider.provider, model.provider)
const seedWorkspaceDefaultModels = (includeSpeechToText: boolean): SeedTask => ({
id: 'workspace-default-models',
title: 'Workspace default models',
run: async (context) => {
const title = 'Workspace default models'
const chatModel = stableModelConfig()
if (chatModel.type !== 'llm')
return blocked(
title,
'E2E_STABLE_MODEL_TYPE must be llm for the workspace default chat model.',
)

if (isExpectedDefault)
return modelResult.status === 'updated' ? modelResult : verified(title, resource)
const configs = includeSpeechToText ? [chatModel, speechToTextModelConfig()] : [chatModel]
const defaults = await readWorkspaceDefaultModels(context)
const settings = new Map<ModelType, ParserPostDefault['model_settings'][number]>()
for (const [modelType, model] of defaults) {
if (model)
settings.set(modelType, {
model: model.model,
model_type: modelType,
provider: model.provider.provider,
})
}

if (context.dryRun)
return skipped(
title,
`Would set ${model.provider}/${model.name} as the workspace default Speech-to-Text model.`,
)
let needsUpdate = false
for (const config of configs) {
const model = await findModel(context.consoleClient, config)
if (!model || model.status !== activeModelStatus) {
const reason = `${config.provider}/${config.name} is not active after model setup.`
return context.dryRun ? skipped(title, reason) : blocked(title, reason)
}

const current = settings.get(config.type)
if (current?.model !== model.name || current.provider !== model.provider) needsUpdate = true
settings.set(config.type, {
model: model.name,
model_type: config.type,
provider: model.provider,
})
}

await context.consoleClient.workspaces.current.defaultModel.post({
body: {
model_settings: [
{
model: model.name,
model_type: config.type,
provider: model.provider,
},
],
},
})
if (!needsUpdate) return verified(title)
if (context.dryRun)
return skipped(title, 'Would configure the seeded workspace default models.')

const updatedDefaultModelResponse =
await context.consoleClient.workspaces.current.defaultModel.get({
query: { model_type: config.type },
// Saving defaults replaces every type, so retain defaults outside this seed profile.
await context.consoleClient.workspaces.current.defaultModel.post({
body: { model_settings: [...settings.values()] },
})
const updatedDefaultModel = updatedDefaultModelResponse.data
if (
updatedDefaultModel?.model !== model.name ||
!matchesProvider(updatedDefaultModel.provider.provider, model.provider)
) {
return blocked(
title,
`${model.provider}/${model.name} was not selected as the workspace default Speech-to-Text model.`,
)
}

return updated(title, resource)
}
const savedDefaults = await readWorkspaceDefaultModels(context)
for (const [modelType, setting] of settings) {
const saved = savedDefaults.get(modelType)
if (saved?.model !== setting.model || saved?.provider.provider !== setting.provider)
return blocked(
title,
`Workspace default ${modelType} was not saved as ${setting.provider}/${setting.model}.`,
)
}

return updated(title)
},
})

const findBuiltinTool = async (client: SeedContext['consoleClient'], displayName: string) => {
const parsed = splitToolDisplayName(displayName)
Expand Down Expand Up @@ -849,6 +870,7 @@ const agentV2PreparedFixtureSeedTasks = (): SeedTask[] => [

const agentV2PreparedSeedTasks = (): SeedTask[] => [
...agentV2BaseSeedTasks(),
seedWorkspaceDefaultModels(false),
...agentV2PreparedFixtureSeedTasks(),
]

Expand All @@ -859,6 +881,7 @@ const agentV2ExternalRuntimeSeedTasks = (): SeedTask[] => [
title: agentBuilderPreseededResources.speechToTextModel,
run: seedSpeechToTextModel,
},
seedWorkspaceDefaultModels(true),
]

const agentV2PostMergeSeedTasks = (): SeedTask[] => [
Expand Down
9 changes: 4 additions & 5 deletions e2e/features/step-definitions/agent-v2/configure.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,11 @@ Then(
Then(
'I should see the workspace default chat model in the Agent v2 model selector',
async function (this: DifyWorld) {
const response = await this.getConsoleClient().workspaces.current.defaultModel.get({
query: { model_type: 'llm' },
})
if (!response.data) throw new Error('The workspace default chat model is not configured.')
const defaultModel = this.agentBuilder.fixtures.defaultChatModel
if (!defaultModel)
throw new Error('Resolve the workspace default chat model before creating the Agent.')

await expect(getAgentModelSelector(this.getPage())).toContainText(response.data.model, {
await expect(getAgentModelSelector(this.getPage())).toContainText(defaultModel.model, {
timeout: 30_000,
})
},
Expand Down
12 changes: 12 additions & 0 deletions e2e/features/step-definitions/agent-v2/fixtures.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ Given('the Agent Builder stable chat model is available', async function (this:
this.agentBuilder.fixtures.stableModel = stableModel
})

Given('the workspace default chat model is configured', async function (this: DifyWorld) {
const response = await this.getConsoleClient().workspaces.current.defaultModel.get({
query: { model_type: 'llm' },
})
if (!response.data)
throw new Error(
'The workspace default chat model is not configured. Run the prepared seed first.',
)

this.agentBuilder.fixtures.defaultChatModel = response.data
})

Given('the workspace default speech-to-text model is active', async function (this: DifyWorld) {
const speechToTextModel = await requireAgentBuilderSpeechToTextModel(
this,
Expand Down
2 changes: 2 additions & 0 deletions e2e/features/support/world.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IWorldOptions } from '@cucumber/cucumber'
import type { DefaultModelResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { APIRequestContext, Browser, BrowserContext, Download, Page } from '@playwright/test'
import type { AuthSessionMetadata } from '../../fixtures/auth.ts'
import type { ConsoleClient } from '../../support/api/console-client.ts'
Expand Down Expand Up @@ -51,6 +52,7 @@ export type WorkflowPreviewFixture = {
export const createAgentBuilderWorldState = () => ({
fixtures: {
agentDecisionModel: undefined as AgentBuilderChatModel | undefined,
defaultChatModel: undefined as DefaultModelResponse | undefined,
preseededResources: {} as Record<string, AgentBuilderPreseededResource>,
speechToTextModel: undefined as AgentBuilderChatModel | undefined,
stableModel: undefined as AgentBuilderChatModel | undefined,
Expand Down
7 changes: 6 additions & 1 deletion e2e/tests/agent-v2-seed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ const taskIds = (profile: string) => createAgentV2SeedTasks(profile).map((task)

describe('Agent v2 seed profiles', () => {
it('prepares every fixture required by prepared behavior', () => {
expect(taskIds('prepared')).toEqual([...baseTaskIds, ...preparedFixtureTaskIds])
expect(taskIds('prepared')).toEqual([
...baseTaskIds,
'workspace-default-models',
...preparedFixtureTaskIds,
])
})

it('prepares external runtime and prepared behavior in post-merge', () => {
expect(taskIds('post-merge')).toEqual([
...baseTaskIds,
'speech-to-text-model',
'workspace-default-models',
...preparedFixtureTaskIds,
])
})
Expand Down
Loading
Loading