mirror of https://github.com/langgenius/dify.git
add Parameters of ParametersExtractor node sync
This commit is contained in:
parent
f74492eb59
commit
df9bd1b3b5
|
|
@ -4,6 +4,8 @@ import type { Node } from '@/app/components/workflow/types'
|
|||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const NODE_ID = 'node-1'
|
||||
const LLM_NODE_ID = 'llm-node'
|
||||
const PARAM_NODE_ID = 'parameter-node'
|
||||
|
||||
const createNode = (variables: string[]): Node => ({
|
||||
id: NODE_ID,
|
||||
|
|
@ -27,6 +29,58 @@ const createNode = (variables: string[]): Node => ({
|
|||
},
|
||||
})
|
||||
|
||||
const createLLMNode = (templates: Array<{ id: string; role: string; text: string }>): Node => ({
|
||||
id: LLM_NODE_ID,
|
||||
type: 'custom',
|
||||
position: { x: 200, y: 200 },
|
||||
data: {
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
selected: false,
|
||||
model: {
|
||||
mode: 'chat',
|
||||
name: 'gemini-2.5-pro',
|
||||
provider: 'langgenius/gemini/google',
|
||||
completion_params: {
|
||||
temperature: 0.7,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
enabled: false,
|
||||
variable_selector: [],
|
||||
},
|
||||
vision: {
|
||||
enabled: false,
|
||||
},
|
||||
prompt_template: templates,
|
||||
},
|
||||
})
|
||||
|
||||
const createParameterExtractorNode = (parameters: Array<{ description: string; name: string; required: boolean; type: string }>): Node => ({
|
||||
id: PARAM_NODE_ID,
|
||||
type: 'custom',
|
||||
position: { x: 400, y: 120 },
|
||||
data: {
|
||||
type: BlockEnum.ParameterExtractor,
|
||||
title: 'ParameterExtractor',
|
||||
selected: true,
|
||||
model: {
|
||||
mode: 'chat',
|
||||
name: '',
|
||||
provider: '',
|
||||
completion_params: {
|
||||
temperature: 0.7,
|
||||
},
|
||||
},
|
||||
query: [],
|
||||
reasoning_mode: 'prompt',
|
||||
parameters,
|
||||
vision: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const getManager = (doc: LoroDoc) => {
|
||||
const manager = new CollaborationManager()
|
||||
;(manager as any).doc = doc
|
||||
|
|
@ -35,6 +89,8 @@ const getManager = (doc: LoroDoc) => {
|
|||
return manager
|
||||
}
|
||||
|
||||
const deepClone = <T>(value: T): T => JSON.parse(JSON.stringify(value))
|
||||
|
||||
const exportNodes = (manager: CollaborationManager) => manager.getNodes()
|
||||
|
||||
describe('Loro merge behavior smoke test', () => {
|
||||
|
|
@ -66,4 +122,118 @@ describe('Loro merge behavior smoke test', () => {
|
|||
expect(finalA.length).toBe(1)
|
||||
expect(finalB.length).toBe(1)
|
||||
})
|
||||
|
||||
it('merges prompt template insertions and edits across replicas', () => {
|
||||
const baseTemplate = [
|
||||
{
|
||||
id: 'system-1',
|
||||
role: 'system',
|
||||
text: 'base instruction',
|
||||
},
|
||||
]
|
||||
|
||||
const docA = new LoroDoc()
|
||||
const managerA = getManager(docA)
|
||||
managerA.syncNodes([], [createLLMNode(deepClone(baseTemplate))])
|
||||
|
||||
const snapshot = docA.export({ mode: 'snapshot' })
|
||||
const docB = LoroDoc.fromSnapshot(snapshot)
|
||||
const managerB = getManager(docB)
|
||||
|
||||
const additionTemplate = [
|
||||
...baseTemplate,
|
||||
{
|
||||
id: 'user-1',
|
||||
role: 'user',
|
||||
text: 'hello from docA',
|
||||
},
|
||||
]
|
||||
managerA.syncNodes([createLLMNode(deepClone(baseTemplate))], [createLLMNode(deepClone(additionTemplate))])
|
||||
|
||||
const editedTemplate = [
|
||||
{
|
||||
id: 'system-1',
|
||||
role: 'system',
|
||||
text: 'updated by docB',
|
||||
},
|
||||
]
|
||||
managerB.syncNodes([createLLMNode(deepClone(baseTemplate))], [createLLMNode(deepClone(editedTemplate))])
|
||||
|
||||
const updateForA = docB.export({ mode: 'update', from: docA.version() })
|
||||
docA.import(updateForA)
|
||||
|
||||
const updateForB = docA.export({ mode: 'update', from: docB.version() })
|
||||
docB.import(updateForB)
|
||||
|
||||
const finalA = exportNodes(managerA).find(node => node.id === LLM_NODE_ID)
|
||||
const finalB = exportNodes(managerB).find(node => node.id === LLM_NODE_ID)
|
||||
|
||||
expect(finalA).toBeDefined()
|
||||
expect(finalB).toBeDefined()
|
||||
|
||||
const expectedTemplates = [
|
||||
{
|
||||
id: 'system-1',
|
||||
role: 'system',
|
||||
text: 'updated by docB',
|
||||
},
|
||||
{
|
||||
id: 'user-1',
|
||||
role: 'user',
|
||||
text: 'hello from docA',
|
||||
},
|
||||
]
|
||||
|
||||
expect((finalA!.data as any).prompt_template).toEqual(expectedTemplates)
|
||||
expect((finalB!.data as any).prompt_template).toEqual(expectedTemplates)
|
||||
})
|
||||
|
||||
it('converges when parameter lists are edited concurrently', () => {
|
||||
const baseParameters = [
|
||||
{ description: 'bb', name: 'aa', required: false, type: 'string' },
|
||||
{ description: 'dd', name: 'cc', required: false, type: 'string' },
|
||||
]
|
||||
|
||||
const docA = new LoroDoc()
|
||||
const managerA = getManager(docA)
|
||||
managerA.syncNodes([], [createParameterExtractorNode(deepClone(baseParameters))])
|
||||
|
||||
const snapshot = docA.export({ mode: 'snapshot' })
|
||||
const docB = LoroDoc.fromSnapshot(snapshot)
|
||||
const managerB = getManager(docB)
|
||||
|
||||
const docAUpdate = [
|
||||
{ description: 'bb updated by A', name: 'aa', required: true, type: 'string' },
|
||||
{ description: 'dd', name: 'cc', required: false, type: 'string' },
|
||||
{ description: 'new from A', name: 'ee', required: false, type: 'number' },
|
||||
]
|
||||
managerA.syncNodes([createParameterExtractorNode(deepClone(baseParameters))], [createParameterExtractorNode(deepClone(docAUpdate))])
|
||||
|
||||
const docBUpdate = [
|
||||
{ description: 'bb', name: 'aa', required: false, type: 'string' },
|
||||
{ description: 'dd updated by B', name: 'cc', required: true, type: 'string' },
|
||||
]
|
||||
managerB.syncNodes([createParameterExtractorNode(deepClone(baseParameters))], [createParameterExtractorNode(deepClone(docBUpdate))])
|
||||
|
||||
const updateForA = docB.export({ mode: 'update', from: docA.version() })
|
||||
docA.import(updateForA)
|
||||
|
||||
const updateForB = docA.export({ mode: 'update', from: docB.version() })
|
||||
docB.import(updateForB)
|
||||
|
||||
const finalA = exportNodes(managerA).find(node => node.id === PARAM_NODE_ID)
|
||||
const finalB = exportNodes(managerB).find(node => node.id === PARAM_NODE_ID)
|
||||
|
||||
expect(finalA).toBeDefined()
|
||||
expect(finalB).toBeDefined()
|
||||
|
||||
const expectedParameters = [
|
||||
{ description: 'bb updated by A', name: 'aa', required: true, type: 'string' },
|
||||
{ description: 'dd updated by B', name: 'cc', required: true, type: 'string' },
|
||||
{ description: 'new from A', name: 'ee', required: false, type: 'number' },
|
||||
]
|
||||
|
||||
expect((finalA!.data as any).parameters).toEqual(expectedParameters)
|
||||
expect((finalB!.data as any).parameters).toEqual(expectedParameters)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ type PromptTemplateItem = {
|
|||
text: string
|
||||
}
|
||||
|
||||
type ParameterItem = {
|
||||
description: string
|
||||
name: string
|
||||
required: boolean
|
||||
type: string
|
||||
}
|
||||
|
||||
const createVariable = (name: string, overrides: Partial<WorkflowVariable> = {}): WorkflowVariable => ({
|
||||
default: '',
|
||||
hint: '',
|
||||
|
|
@ -60,6 +67,7 @@ const createNodeSnapshot = (variableNames: string[]): Node<{ variables: Workflow
|
|||
})
|
||||
|
||||
const LLM_NODE_ID = 'llm-node'
|
||||
const PARAM_NODE_ID = 'param-extractor-node'
|
||||
|
||||
const createLLMNodeSnapshot = (promptTemplates: PromptTemplateItem[]): Node<any> => ({
|
||||
id: LLM_NODE_ID,
|
||||
|
|
@ -96,6 +104,39 @@ const createLLMNodeSnapshot = (promptTemplates: PromptTemplateItem[]): Node<any>
|
|||
},
|
||||
})
|
||||
|
||||
const createParameterExtractorNodeSnapshot = (parameters: ParameterItem[]): Node<any> => ({
|
||||
id: PARAM_NODE_ID,
|
||||
type: 'custom',
|
||||
position: { x: 420, y: 220 },
|
||||
positionAbsolute: { x: 420, y: 220 },
|
||||
height: 260,
|
||||
width: 420,
|
||||
selected: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left',
|
||||
data: {
|
||||
type: 'parameter-extractor',
|
||||
title: '参数提取器',
|
||||
selected: true,
|
||||
model: {
|
||||
mode: 'chat',
|
||||
name: '',
|
||||
provider: '',
|
||||
completion_params: {
|
||||
temperature: 0.7,
|
||||
},
|
||||
},
|
||||
reasoning_mode: 'prompt',
|
||||
parameters,
|
||||
query: [],
|
||||
vision: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const getVariables = (node: Node): string[] => {
|
||||
const variables = (node.data as any)?.variables ?? []
|
||||
return variables.map((item: WorkflowVariable) => item.variable)
|
||||
|
|
@ -110,6 +151,10 @@ const getPromptTemplates = (node: Node): PromptTemplateItem[] => {
|
|||
return ((node.data as any)?.prompt_template ?? []) as PromptTemplateItem[]
|
||||
}
|
||||
|
||||
const getParameters = (node: Node): ParameterItem[] => {
|
||||
return ((node.data as any)?.parameters ?? []) as ParameterItem[]
|
||||
}
|
||||
|
||||
describe('CollaborationManager syncNodes', () => {
|
||||
let manager: CollaborationManager
|
||||
|
||||
|
|
@ -279,4 +324,42 @@ describe('CollaborationManager syncNodes', () => {
|
|||
expect(finalTemplates).toHaveLength(1)
|
||||
expect(finalTemplates[0].text).toBe('updated system prompt')
|
||||
})
|
||||
|
||||
it('keeps parameter list in sync when nodes add, edit, or remove parameters', () => {
|
||||
const parameterManager = new CollaborationManager()
|
||||
const doc = new LoroDoc()
|
||||
;(parameterManager as any).doc = doc
|
||||
;(parameterManager as any).nodesMap = doc.getMap('nodes')
|
||||
;(parameterManager as any).edgesMap = doc.getMap('edges')
|
||||
|
||||
const baseParameters: ParameterItem[] = [
|
||||
{ description: 'bb', name: 'aa', required: false, type: 'string' },
|
||||
{ description: 'dd', name: 'cc', required: false, type: 'string' },
|
||||
]
|
||||
|
||||
const baseNode = createParameterExtractorNodeSnapshot(baseParameters)
|
||||
;(parameterManager as any).syncNodes([], [deepClone(baseNode)])
|
||||
|
||||
const updatedParameters: ParameterItem[] = [
|
||||
...baseParameters,
|
||||
{ description: 'ff', name: 'ee', required: true, type: 'number' },
|
||||
]
|
||||
|
||||
const updatedNode = createParameterExtractorNodeSnapshot(updatedParameters)
|
||||
;(parameterManager as any).syncNodes([deepClone(baseNode)], [deepClone(updatedNode)])
|
||||
|
||||
const stored = (parameterManager.getNodes() as Node[]).find(node => node.id === PARAM_NODE_ID)
|
||||
expect(stored).toBeDefined()
|
||||
expect(getParameters(stored!)).toEqual(updatedParameters)
|
||||
|
||||
const editedParameters: ParameterItem[] = [
|
||||
{ description: 'bb edited', name: 'aa', required: true, type: 'string' },
|
||||
]
|
||||
const editedNode = createParameterExtractorNodeSnapshot(editedParameters)
|
||||
|
||||
;(parameterManager as any).syncNodes([deepClone(updatedNode)], [deepClone(editedNode)])
|
||||
|
||||
const final = (parameterManager.getNodes() as Node[]).find(node => node.id === PARAM_NODE_ID)
|
||||
expect(getParameters(final!)).toEqual(editedParameters)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -87,6 +87,16 @@ export class CollaborationManager {
|
|||
return typeof list.getAttached === 'function' ? list.getAttached() ?? list : list
|
||||
}
|
||||
|
||||
private ensureParametersList(nodeContainer: LoroMap<any>): LoroList<any> {
|
||||
const dataContainer = this.ensureDataContainer(nodeContainer)
|
||||
let list = dataContainer.get('parameters') as any
|
||||
|
||||
if (!list || typeof list.kind !== 'function' || list.kind() !== 'List')
|
||||
list = dataContainer.setContainer('parameters', new LoroList())
|
||||
|
||||
return typeof list.getAttached === 'function' ? list.getAttached() ?? list : list
|
||||
}
|
||||
|
||||
private exportNode(nodeId: string): Node {
|
||||
const container = this.getNodeContainer(nodeId)
|
||||
const json = container.toJSON() as any
|
||||
|
|
@ -151,6 +161,8 @@ export class CollaborationManager {
|
|||
this.syncVariables(container, Array.isArray(value) ? value : [])
|
||||
else if (key === 'prompt_template')
|
||||
this.syncPromptTemplate(container, Array.isArray(value) ? value : [])
|
||||
else if (key === 'parameters')
|
||||
this.syncParameters(container, Array.isArray(value) ? value : [])
|
||||
else
|
||||
dataContainer.set(key, cloneDeep(value))
|
||||
})
|
||||
|
|
@ -164,6 +176,8 @@ export class CollaborationManager {
|
|||
dataContainer.delete('variables')
|
||||
else if (key === 'prompt_template')
|
||||
dataContainer.delete('prompt_template')
|
||||
else if (key === 'parameters')
|
||||
dataContainer.delete('parameters')
|
||||
|
||||
else
|
||||
dataContainer.delete(key)
|
||||
|
|
@ -219,6 +233,28 @@ export class CollaborationManager {
|
|||
}
|
||||
}
|
||||
|
||||
private syncParameters(nodeContainer: LoroMap<any>, desired: any[]): void {
|
||||
const list = this.ensureParametersList(nodeContainer)
|
||||
const current = list.toJSON() as any[]
|
||||
const target = Array.isArray(desired) ? desired : []
|
||||
const minLength = Math.min(current.length, target.length)
|
||||
|
||||
for (let i = 0; i < minLength; i += 1) {
|
||||
if (!isEqual(current[i], target[i])) {
|
||||
list.delete(i, 1)
|
||||
list.insert(i, cloneDeep(target[i]))
|
||||
}
|
||||
}
|
||||
|
||||
if (current.length > target.length) {
|
||||
list.delete(target.length, current.length - target.length)
|
||||
}
|
||||
else if (target.length > current.length) {
|
||||
for (let i = current.length; i < target.length; i += 1)
|
||||
list.insert(i, cloneDeep(target[i]))
|
||||
}
|
||||
}
|
||||
|
||||
private getNodePanelPresenceSnapshot(): NodePanelPresenceMap {
|
||||
const snapshot: NodePanelPresenceMap = {}
|
||||
Object.entries(this.nodePanelPresence).forEach(([nodeId, viewers]) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue