mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
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:
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user