mirror of
https://github.com/langgenius/dify.git
synced 2026-04-15 09:57:03 +08:00
fix workflow variable rename propagation
This commit is contained in:
parent
0d3ada2bc9
commit
a309d78f95
@ -0,0 +1,153 @@
|
||||
import type { Node, PromptItem } from '@/app/components/workflow/types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { BlockEnum, EditionType, PromptRole } from '@/app/components/workflow/types'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getNodeUsedVars, updateNodeVars } from '../utils'
|
||||
|
||||
const createNode = (data: Node['data']): Node => ({
|
||||
id: 'node-1',
|
||||
type: 'custom',
|
||||
position: { x: 0, y: 0 },
|
||||
data,
|
||||
})
|
||||
|
||||
const createPromptItem = (overrides: Partial<PromptItem> = {}): PromptItem => ({
|
||||
role: PromptRole.user,
|
||||
text: '',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('variable utils', () => {
|
||||
describe('getNodeUsedVars', () => {
|
||||
it('should read variables from llm jinja prompt text', () => {
|
||||
const node = createNode({
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
desc: '',
|
||||
model: {
|
||||
provider: 'provider',
|
||||
name: 'model',
|
||||
mode: AppModeEnum.CHAT,
|
||||
completion_params: {},
|
||||
},
|
||||
prompt_template: [
|
||||
createPromptItem({
|
||||
edition_type: EditionType.jinja2,
|
||||
jinja2_text: 'Hello {{#env.API_KEY#}}',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
expect(getNodeUsedVars(node)).toContainEqual(['env', 'API_KEY'])
|
||||
})
|
||||
|
||||
it('should read variables from human input email body', () => {
|
||||
const node = createNode({
|
||||
type: BlockEnum.HumanInput,
|
||||
title: 'Human Input',
|
||||
desc: '',
|
||||
form_content: '',
|
||||
inputs: [],
|
||||
user_actions: [],
|
||||
timeout: 1,
|
||||
timeout_unit: 'day',
|
||||
delivery_methods: [
|
||||
{
|
||||
id: 'email',
|
||||
type: 'email',
|
||||
enabled: true,
|
||||
config: {
|
||||
recipients: { whole_workspace: true, items: [] },
|
||||
subject: 'Subject {{#conversation.memory#}}',
|
||||
body: 'Body {{#env.API_KEY#}}',
|
||||
debug_mode: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(getNodeUsedVars(node)).toEqual(
|
||||
expect.arrayContaining([
|
||||
['env', 'API_KEY'],
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateNodeVars', () => {
|
||||
it('should replace answer prompt references', () => {
|
||||
const node = createNode({
|
||||
type: BlockEnum.Answer,
|
||||
title: 'Answer',
|
||||
desc: '',
|
||||
answer: 'Answer {{#env.API_KEY#}}',
|
||||
variables: [],
|
||||
})
|
||||
|
||||
const updatedNode = updateNodeVars(node, ['env', 'API_KEY'], ['env', 'RENAMED_KEY'])
|
||||
|
||||
expect(updatedNode.data.answer).toBe('Answer {{#env.RENAMED_KEY#}}')
|
||||
})
|
||||
|
||||
it('should replace llm jinja prompt references', () => {
|
||||
const node = createNode({
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
desc: '',
|
||||
model: {
|
||||
provider: 'provider',
|
||||
name: 'model',
|
||||
mode: AppModeEnum.CHAT,
|
||||
completion_params: {},
|
||||
},
|
||||
prompt_template: [
|
||||
createPromptItem({
|
||||
text: '{{#env.API_KEY#}}',
|
||||
edition_type: EditionType.jinja2,
|
||||
jinja2_text: 'Hello {{#env.API_KEY#}}',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const updatedNode = updateNodeVars(node, ['env', 'API_KEY'], ['env', 'RENAMED_KEY'])
|
||||
|
||||
expect((updatedNode.data.prompt_template as PromptItem[])[0]).toMatchObject({
|
||||
text: '{{#env.RENAMED_KEY#}}',
|
||||
jinja2_text: 'Hello {{#env.RENAMED_KEY#}}',
|
||||
})
|
||||
})
|
||||
|
||||
it('should replace human input email template references', () => {
|
||||
const node = createNode({
|
||||
type: BlockEnum.HumanInput,
|
||||
title: 'Human Input',
|
||||
desc: '',
|
||||
form_content: '',
|
||||
inputs: [],
|
||||
user_actions: [],
|
||||
timeout: 1,
|
||||
timeout_unit: 'day',
|
||||
delivery_methods: [
|
||||
{
|
||||
id: 'email',
|
||||
type: 'email',
|
||||
enabled: true,
|
||||
config: {
|
||||
recipients: { whole_workspace: true, items: [] },
|
||||
subject: 'Subject {{#conversation.memory#}}',
|
||||
body: 'Body {{#env.API_KEY#}}',
|
||||
debug_mode: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const updatedNode = updateNodeVars(node, ['env', 'API_KEY'], ['env', 'RENAMED_KEY'])
|
||||
|
||||
expect(updatedNode.data.delivery_methods[0]?.config).toMatchObject({
|
||||
subject: 'Subject {{#conversation.memory#}}',
|
||||
body: 'Body {{#env.RENAMED_KEY#}}',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -53,6 +53,7 @@ import {
|
||||
} from '@/app/components/workflow/constants'
|
||||
import DataSourceNodeDefault from '@/app/components/workflow/nodes/data-source/default'
|
||||
import HumanInputNodeDefault from '@/app/components/workflow/nodes/human-input/default'
|
||||
import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import ToolNodeDefault from '@/app/components/workflow/nodes/tool/default'
|
||||
import PluginTriggerNodeDefault from '@/app/components/workflow/nodes/trigger-plugin/default'
|
||||
import {
|
||||
@ -1310,6 +1311,25 @@ const replaceOldVarInText = (
|
||||
)
|
||||
}
|
||||
|
||||
const getPromptItemTexts = (prompt: PromptItem): string[] => {
|
||||
const texts = [prompt.text]
|
||||
if (prompt.jinja2_text)
|
||||
texts.push(prompt.jinja2_text)
|
||||
return texts.filter((text): text is string => !!text)
|
||||
}
|
||||
|
||||
const replaceOldVarInPromptItem = (
|
||||
prompt: PromptItem,
|
||||
oldVar: ValueSelector,
|
||||
newVar: ValueSelector,
|
||||
): PromptItem => ({
|
||||
...prompt,
|
||||
text: replaceOldVarInText(prompt.text, oldVar, newVar),
|
||||
...(prompt.jinja2_text !== undefined
|
||||
? { jinja2_text: replaceOldVarInText(prompt.jinja2_text, oldVar, newVar) }
|
||||
: {}),
|
||||
})
|
||||
|
||||
export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
const { data } = node
|
||||
const { type } = data
|
||||
@ -1331,12 +1351,12 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
let prompts: string[] = []
|
||||
if (isChatModel) {
|
||||
prompts
|
||||
= (payload.prompt_template as PromptItem[])?.map(p => p.text) || []
|
||||
= (payload.prompt_template as PromptItem[])?.flatMap(getPromptItemTexts) || []
|
||||
if (payload.memory?.query_prompt_template)
|
||||
prompts.push(payload.memory.query_prompt_template)
|
||||
}
|
||||
else {
|
||||
prompts = [(payload.prompt_template as PromptItem).text]
|
||||
prompts = getPromptItemTexts(payload.prompt_template as PromptItem)
|
||||
}
|
||||
|
||||
const inputVars: ValueSelector[] = matchNotSystemVars(prompts)
|
||||
@ -1505,7 +1525,12 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
case BlockEnum.HumanInput: {
|
||||
const payload = data as HumanInputNodeType
|
||||
const formContent = payload.form_content
|
||||
res = matchNotSystemVars([formContent])
|
||||
const mailTemplates = payload.delivery_methods.flatMap((method) => {
|
||||
if (method.type !== DeliveryMethodType.Email || !method.config)
|
||||
return []
|
||||
return [method.config.body]
|
||||
})
|
||||
res = matchNotSystemVars([formContent, ...mailTemplates])
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -1651,6 +1676,11 @@ export const updateNodeVars = (
|
||||
}
|
||||
case BlockEnum.Answer: {
|
||||
const payload = data as AnswerNodeType
|
||||
payload.answer = replaceOldVarInText(
|
||||
payload.answer,
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
)
|
||||
if (payload.variables) {
|
||||
payload.variables = payload.variables.map((v) => {
|
||||
if (v.value_selector.join('.') === oldVarSelector.join('.'))
|
||||
@ -1666,16 +1696,7 @@ export const updateNodeVars = (
|
||||
if (isChatModel) {
|
||||
payload.prompt_template = (
|
||||
payload.prompt_template as PromptItem[]
|
||||
).map((prompt) => {
|
||||
return {
|
||||
...prompt,
|
||||
text: replaceOldVarInText(
|
||||
prompt.text,
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
),
|
||||
}
|
||||
})
|
||||
).map(prompt => replaceOldVarInPromptItem(prompt, oldVarSelector, newVarSelector))
|
||||
if (payload.memory?.query_prompt_template) {
|
||||
payload.memory.query_prompt_template = replaceOldVarInText(
|
||||
payload.memory.query_prompt_template,
|
||||
@ -1685,14 +1706,11 @@ export const updateNodeVars = (
|
||||
}
|
||||
}
|
||||
else {
|
||||
payload.prompt_template = {
|
||||
...payload.prompt_template,
|
||||
text: replaceOldVarInText(
|
||||
(payload.prompt_template as PromptItem).text,
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
),
|
||||
}
|
||||
payload.prompt_template = replaceOldVarInPromptItem(
|
||||
payload.prompt_template as PromptItem,
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
)
|
||||
}
|
||||
if (
|
||||
payload.context?.variable_selector?.join('.')
|
||||
@ -1780,6 +1798,10 @@ export const updateNodeVars = (
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
)
|
||||
payload.classes = payload.classes.map(topic => ({
|
||||
...topic,
|
||||
name: replaceOldVarInText(topic.name, oldVarSelector, newVarSelector),
|
||||
}))
|
||||
break
|
||||
}
|
||||
case BlockEnum.HttpRequest: {
|
||||
@ -1889,6 +1911,46 @@ export const updateNodeVars = (
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.Agent: {
|
||||
const payload = data as AgentNodeType
|
||||
if (payload.agent_parameters) {
|
||||
Object.keys(payload.agent_parameters).forEach((key) => {
|
||||
const value = payload.agent_parameters![key]
|
||||
const { type } = value
|
||||
|
||||
if (
|
||||
type === ToolVarType.variable
|
||||
&& Array.isArray(value.value)
|
||||
&& value.value.join('.') === oldVarSelector.join('.')
|
||||
) {
|
||||
payload.agent_parameters![key] = {
|
||||
...value,
|
||||
value: newVarSelector,
|
||||
}
|
||||
}
|
||||
|
||||
if (type === ToolVarType.mixed && typeof value.value === 'string') {
|
||||
payload.agent_parameters![key] = {
|
||||
...value,
|
||||
value: replaceOldVarInText(
|
||||
value.value,
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (payload.memory?.query_prompt_template) {
|
||||
payload.memory.query_prompt_template = replaceOldVarInText(
|
||||
payload.memory.query_prompt_template,
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.VariableAssigner: {
|
||||
const payload = data as VariableAssignerNodeType
|
||||
if (payload.variables) {
|
||||
@ -1954,6 +2016,22 @@ export const updateNodeVars = (
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
)
|
||||
payload.delivery_methods = payload.delivery_methods.map((method) => {
|
||||
if (method.type !== DeliveryMethodType.Email || !method.config)
|
||||
return method
|
||||
|
||||
return {
|
||||
...method,
|
||||
config: {
|
||||
...method.config,
|
||||
body: replaceOldVarInText(
|
||||
method.config.body,
|
||||
oldVarSelector,
|
||||
newVarSelector,
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ type MockWorkflowStoreState = {
|
||||
appId: string
|
||||
conversationVariables: ConversationVariable[]
|
||||
setConversationVariables: (value: ConversationVariable[]) => void
|
||||
setControlPromptEditorRerenderKey: (value: number) => void
|
||||
}
|
||||
|
||||
type MockFlowStore = {
|
||||
@ -18,6 +19,7 @@ type MockFlowStore = {
|
||||
|
||||
const mockSetShowChatVariablePanel = vi.fn()
|
||||
const mockSetConversationVariables = vi.fn()
|
||||
const mockSetControlPromptEditorRerenderKey = vi.fn()
|
||||
const mockInvalidateConversationVarValues = vi.fn()
|
||||
const mockUpdateConversationVariables = vi.fn().mockResolvedValue(undefined)
|
||||
const mockFindUsedVarNodes = vi.fn<(selector: string[], nodes: Node[]) => Node[]>()
|
||||
@ -64,6 +66,7 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
appId: 'app-1',
|
||||
conversationVariables: mockConversationVariables,
|
||||
setConversationVariables: mockSetConversationVariables,
|
||||
setControlPromptEditorRerenderKey: mockSetControlPromptEditorRerenderKey,
|
||||
}),
|
||||
}))
|
||||
|
||||
@ -239,6 +242,7 @@ describe('ChatVariablePanel', () => {
|
||||
['conversation', 'conversation_var_next'],
|
||||
)
|
||||
expect(mockSetNodes).toHaveBeenCalledWith([updatedNode, createNode('node-2')])
|
||||
expect(mockSetControlPromptEditorRerenderKey).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should require confirmation before deleting variables referenced by workflow nodes', async () => {
|
||||
|
||||
@ -29,6 +29,7 @@ const ChatVariablePanel = () => {
|
||||
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||
const varList = useStore(s => s.conversationVariables) as ConversationVariable[]
|
||||
const updateChatVarList = useStore(s => s.setConversationVariables)
|
||||
const setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey)
|
||||
const appId = useStore(s => s.appId) as string
|
||||
const {
|
||||
invalidateConversationVarValues,
|
||||
@ -156,6 +157,7 @@ const ChatVariablePanel = () => {
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
setControlPromptEditorRerenderKey(Date.now())
|
||||
}
|
||||
|
||||
// Use new dedicated conversation variables API
|
||||
@ -179,7 +181,7 @@ const ChatVariablePanel = () => {
|
||||
// Revert local state on error
|
||||
updateChatVarList(varList)
|
||||
}
|
||||
}, [currentVar, getEffectedNodes, collaborativeWorkflow, updateChatVarList, varList, appId, invalidateConversationVarValues])
|
||||
}, [currentVar, getEffectedNodes, collaborativeWorkflow, updateChatVarList, varList, appId, invalidateConversationVarValues, setControlPromptEditorRerenderKey])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -333,6 +333,7 @@ describe('EnvPanel container', () => {
|
||||
}),
|
||||
expect.objectContaining({ id: 'node-2' }),
|
||||
])
|
||||
expect(store.getState().controlPromptEditorRerenderKey).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should convert edited plain variables into secrets and store the masked secret value', async () => {
|
||||
|
||||
@ -41,6 +41,7 @@ const useEnvPanelActions = ({
|
||||
envSecrets,
|
||||
updateEnvList,
|
||||
setEnvSecrets,
|
||||
setControlPromptEditorRerenderKey,
|
||||
doSyncWorkflowDraft,
|
||||
}: {
|
||||
collaborativeWorkflow: ReturnType<typeof useCollaborativeWorkflow>
|
||||
@ -48,6 +49,7 @@ const useEnvPanelActions = ({
|
||||
envSecrets: Record<string, string>
|
||||
updateEnvList: (envList: EnvironmentVariable[]) => void
|
||||
setEnvSecrets: (envSecrets: Record<string, string>) => void
|
||||
setControlPromptEditorRerenderKey: (controlPromptEditorRerenderKey: number) => void
|
||||
doSyncWorkflowDraft: () => Promise<void>
|
||||
}) => {
|
||||
const emitVarsAndFeaturesUpdate = useCallback(async () => {
|
||||
@ -99,7 +101,8 @@ const useEnvPanelActions = ({
|
||||
return node
|
||||
})
|
||||
setNodes(nextNodes)
|
||||
}, [collaborativeWorkflow, getAffectedNodes])
|
||||
setControlPromptEditorRerenderKey(Date.now())
|
||||
}, [collaborativeWorkflow, getAffectedNodes, setControlPromptEditorRerenderKey])
|
||||
|
||||
const syncEnvList = useCallback(async (
|
||||
nextEnvList: EnvironmentVariable[],
|
||||
@ -152,6 +155,7 @@ const EnvPanel = () => {
|
||||
const envSecrets = useStore(s => s.envSecrets)
|
||||
const updateEnvList = useStore(s => s.setEnvironmentVariables)
|
||||
const setEnvSecrets = useStore(s => s.setEnvSecrets)
|
||||
const setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey)
|
||||
const appId = useStore(s => s.appId) as string
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
@ -166,6 +170,7 @@ const EnvPanel = () => {
|
||||
envSecrets,
|
||||
updateEnvList,
|
||||
setEnvSecrets,
|
||||
setControlPromptEditorRerenderKey,
|
||||
doSyncWorkflowDraft,
|
||||
})
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user