improvement(serializer): filter out advanced mode fields when executing in basic mode, persist the values but don't include them in serialized block for execution (#1018)

* improvement(serializer): filter out advanced mode fields when executing in basic mode, persist the values but don't include them in serialized block for execution

* fix serializer exclusion logic
This commit is contained in:
Waleed Latif
2025-08-18 16:34:53 -07:00
committed by GitHub
parent 871f4e8e18
commit 073030bfaa
4 changed files with 279 additions and 35 deletions

View File

@@ -161,6 +161,56 @@ vi.mock('@/blocks', () => ({
subreddit: { type: 'string' },
},
},
// Mock block with both basic and advanced mode fields for testing
slack: {
name: 'Slack',
description: 'Send messages to Slack',
category: 'tools',
bgColor: '#611f69',
tools: {
access: ['slack_send_message'],
config: {
tool: () => 'slack_send_message',
},
},
subBlocks: [
{ id: 'channel', type: 'dropdown', title: 'Channel', mode: 'basic' },
{ id: 'manualChannel', type: 'short-input', title: 'Channel ID', mode: 'advanced' },
{ id: 'text', type: 'long-input', title: 'Message' }, // mode: 'both' (default)
{ id: 'username', type: 'short-input', title: 'Username', mode: 'both' },
],
inputs: {
channel: { type: 'string' },
manualChannel: { type: 'string' },
text: { type: 'string' },
username: { type: 'string' },
},
},
// Mock agent block with memories for testing
agentWithMemories: {
name: 'Agent with Memories',
description: 'AI Agent with memory support',
category: 'ai',
bgColor: '#2196F3',
tools: {
access: ['anthropic_chat'],
config: {
tool: () => 'anthropic_chat',
},
},
subBlocks: [
{ id: 'systemPrompt', type: 'long-input', title: 'System Prompt' }, // mode: 'both' (default)
{ id: 'userPrompt', type: 'long-input', title: 'User Prompt' }, // mode: 'both' (default)
{ id: 'memories', type: 'short-input', title: 'Memories', mode: 'advanced' },
{ id: 'model', type: 'dropdown', title: 'Model' }, // mode: 'both' (default)
],
inputs: {
systemPrompt: { type: 'string' },
userPrompt: { type: 'string' },
memories: { type: 'array' },
model: { type: 'string' },
},
},
}
return mockConfigs[type] || null
@@ -716,4 +766,200 @@ describe('Serializer', () => {
}).toThrow('Test Reddit Block is missing required fields: Reddit Account')
})
})
/**
* Advanced mode field filtering tests
*/
describe('advanced mode field filtering', () => {
it.concurrent('should include all fields when block is in advanced mode', () => {
const serializer = new Serializer()
const advancedModeBlock: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
advancedMode: true, // Advanced mode enabled
subBlocks: {
channel: { value: 'general' }, // basic mode field
manualChannel: { value: 'C1234567890' }, // advanced mode field
text: { value: 'Hello world' }, // both mode field
username: { value: 'bot' }, // both mode field
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'slack-1': advancedModeBlock }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
// In advanced mode, should include ALL fields (basic, advanced, and both)
expect(slackBlock?.config.params.channel).toBe('general') // basic mode field included
expect(slackBlock?.config.params.manualChannel).toBe('C1234567890') // advanced mode field included
expect(slackBlock?.config.params.text).toBe('Hello world') // both mode field included
expect(slackBlock?.config.params.username).toBe('bot') // both mode field included
})
it.concurrent('should exclude advanced-only fields when block is in basic mode', () => {
const serializer = new Serializer()
const basicModeBlock: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
advancedMode: false, // Basic mode enabled
subBlocks: {
channel: { value: 'general' }, // basic mode field
manualChannel: { value: 'C1234567890' }, // advanced mode field
text: { value: 'Hello world' }, // both mode field
username: { value: 'bot' }, // both mode field
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'slack-1': basicModeBlock }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
// In basic mode, should include basic-only fields and exclude advanced-only fields
expect(slackBlock?.config.params.channel).toBe('general') // basic mode field included
expect(slackBlock?.config.params.manualChannel).toBeUndefined() // advanced mode field excluded
expect(slackBlock?.config.params.text).toBe('Hello world') // both mode field included
expect(slackBlock?.config.params.username).toBe('bot') // both mode field included
})
it.concurrent(
'should exclude advanced-only fields when advancedMode is undefined (defaults to basic mode)',
() => {
const serializer = new Serializer()
const defaultModeBlock: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
// advancedMode: undefined (defaults to false)
subBlocks: {
channel: { value: 'general' }, // basic mode field
manualChannel: { value: 'C1234567890' }, // advanced mode field
text: { value: 'Hello world' }, // both mode field
username: { value: 'bot' }, // both mode field
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'slack-1': defaultModeBlock }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
// Should default to basic mode behavior (include basic + both, exclude advanced)
expect(slackBlock?.config.params.channel).toBe('general') // basic mode field included
expect(slackBlock?.config.params.manualChannel).toBeUndefined() // advanced mode field excluded
expect(slackBlock?.config.params.text).toBe('Hello world') // both mode field included
expect(slackBlock?.config.params.username).toBe('bot') // both mode field included
}
)
it.concurrent('should filter memories field correctly in agent blocks', () => {
const serializer = new Serializer()
const agentInBasicMode: any = {
id: 'agent-1',
type: 'agentWithMemories',
name: 'Test Agent',
position: { x: 0, y: 0 },
advancedMode: false, // Basic mode
subBlocks: {
systemPrompt: { value: 'You are helpful' }, // both mode field
userPrompt: { value: 'Hello' }, // both mode field
memories: { value: [{ role: 'user', content: 'My name is John' }] }, // advanced mode field
model: { value: 'claude-3-sonnet' }, // both mode field
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'agent-1': agentInBasicMode }, [], {})
const agentBlock = serialized.blocks.find((b) => b.id === 'agent-1')
expect(agentBlock).toBeDefined()
// In basic mode, memories should be excluded
expect(agentBlock?.config.params.systemPrompt).toBe('You are helpful')
expect(agentBlock?.config.params.userPrompt).toBe('Hello')
expect(agentBlock?.config.params.memories).toBeUndefined() // Excluded in basic mode
expect(agentBlock?.config.params.model).toBe('claude-3-sonnet')
})
it.concurrent('should include memories field when agent is in advanced mode', () => {
const serializer = new Serializer()
const agentInAdvancedMode: any = {
id: 'agent-1',
type: 'agentWithMemories',
name: 'Test Agent',
position: { x: 0, y: 0 },
advancedMode: true, // Advanced mode
subBlocks: {
systemPrompt: { value: 'You are helpful' }, // both mode field
userPrompt: { value: 'Hello' }, // both mode field
memories: { value: [{ role: 'user', content: 'My name is John' }] }, // advanced mode field
model: { value: 'claude-3-sonnet' }, // both mode field
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'agent-1': agentInAdvancedMode }, [], {})
const agentBlock = serialized.blocks.find((b) => b.id === 'agent-1')
expect(agentBlock).toBeDefined()
// In advanced mode, memories should be included
expect(agentBlock?.config.params.systemPrompt).toBe('You are helpful')
expect(agentBlock?.config.params.userPrompt).toBe('Hello')
expect(agentBlock?.config.params.memories).toEqual([
{ role: 'user', content: 'My name is John' },
]) // Included in advanced mode
expect(agentBlock?.config.params.model).toBe('claude-3-sonnet')
})
it.concurrent('should handle blocks with no matching subblock config gracefully', () => {
const serializer = new Serializer()
const blockWithUnknownField: any = {
id: 'slack-1',
type: 'slack',
name: 'Test Slack Block',
position: { x: 0, y: 0 },
advancedMode: false, // Basic mode
subBlocks: {
channel: { value: 'general' }, // known field
unknownField: { value: 'someValue' }, // field not in config
text: { value: 'Hello world' }, // known field
},
outputs: {},
enabled: true,
}
const serialized = serializer.serializeWorkflow({ 'slack-1': blockWithUnknownField }, [], {})
const slackBlock = serialized.blocks.find((b) => b.id === 'slack-1')
expect(slackBlock).toBeDefined()
// Known fields should be processed according to mode rules
expect(slackBlock?.config.params.channel).toBe('general')
expect(slackBlock?.config.params.text).toBe('Hello world')
// Unknown fields are filtered out (no subblock config found, so shouldIncludeField is not called)
expect(slackBlock?.config.params.unknownField).toBeUndefined()
})
})
})

View File

@@ -1,12 +1,26 @@
import type { Edge } from 'reactflow'
import { createLogger } from '@/lib/logs/console/logger'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
import { getTool } from '@/tools/utils'
const logger = createLogger('Serializer')
/**
* Helper function to check if a subblock should be included in serialization based on current mode
*/
function shouldIncludeField(subBlockConfig: SubBlockConfig, isAdvancedMode: boolean): boolean {
const fieldMode = subBlockConfig.mode
if (fieldMode === 'advanced' && !isAdvancedMode) {
return false // Skip advanced-only fields when in basic mode
}
return true
}
export class Serializer {
serializeWorkflow(
blocks: Record<string, BlockState>,
@@ -198,16 +212,26 @@ export class Serializer {
}
const params: Record<string, any> = {}
const isAdvancedMode = block.advancedMode ?? false
// First collect all current values from subBlocks
// First collect all current values from subBlocks, filtering by mode
Object.entries(block.subBlocks).forEach(([id, subBlock]) => {
params[id] = subBlock.value
// Find the corresponding subblock config to check its mode
const subBlockConfig = blockConfig.subBlocks.find((config) => config.id === id)
if (subBlockConfig && shouldIncludeField(subBlockConfig, isAdvancedMode)) {
params[id] = subBlock.value
}
})
// Then check for any subBlocks with default values
blockConfig.subBlocks.forEach((subBlockConfig) => {
const id = subBlockConfig.id
if (params[id] === null && subBlockConfig.value) {
if (
params[id] === null &&
subBlockConfig.value &&
shouldIncludeField(subBlockConfig, isAdvancedMode)
) {
// If the value is null and there's a default value function, use it
params[id] = subBlockConfig.value(params)
}

View File

@@ -355,7 +355,7 @@ describe('workflow store', () => {
)
})
it('should clear memories when switching from advanced to basic mode', () => {
it('should preserve memories when switching from advanced to basic mode', () => {
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
const { setState: setSubBlockState } = useSubBlockStore
@@ -387,7 +387,7 @@ describe('workflow store', () => {
// Toggle back to basic mode
toggleBlockAdvancedMode('agent1')
// Check that prompts are preserved but memories are cleared
// Check that prompts and memories are all preserved
const subBlockState = useSubBlockStore.getState()
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
'You are a helpful assistant'
@@ -395,7 +395,10 @@ describe('workflow store', () => {
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
'What did we discuss?'
)
expect(subBlockState.workflowValues['test-workflow'].agent1.memories).toBeNull()
expect(subBlockState.workflowValues['test-workflow'].agent1.memories).toEqual([
{ role: 'user', content: 'My name is John' },
{ role: 'assistant', content: 'Nice to meet you, John!' },
])
})
it('should handle mode switching when no subblock values exist', () => {

View File

@@ -974,35 +974,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
set(newState)
// Clear the appropriate subblock values based on the new mode
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (activeWorkflowId) {
const subBlockStore = useSubBlockStore.getState()
const blockValues = subBlockStore.workflowValues[activeWorkflowId]?.[id] || {}
const updatedValues = { ...blockValues }
if (!block.advancedMode) {
// Switching TO advanced mode
// Preserve systemPrompt and userPrompt, memories starts empty
// No need to clear anything since advanced mode has all fields
} else {
// Switching TO basic mode
// Preserve systemPrompt and userPrompt, but clear memories
updatedValues.memories = null
}
// Update subblock store with the cleared values
useSubBlockStore.setState({
workflowValues: {
...subBlockStore.workflowValues,
[activeWorkflowId]: {
...subBlockStore.workflowValues[activeWorkflowId],
[id]: updatedValues,
},
},
})
}
get().triggerUpdate()
// Note: Socket.IO handles real-time sync automatically
},