n8n-nodes-roundrobin
Version:
n8n node to store and retrieve messages in a round-robin fashion, particularly for LLM conversation loops with multiple personas
1,056 lines (1,055 loc) • 104 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RoundRobin = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const ExternalStorage_1 = require("./ExternalStorage");
function calculateRoundCount(messages, spotCount) {
if (!messages.length || !spotCount)
return 0;
const messagesBySpot = {};
messages.forEach(msg => {
if (messagesBySpot[msg.spotIndex] === undefined) {
messagesBySpot[msg.spotIndex] = 0;
}
messagesBySpot[msg.spotIndex]++;
});
const spotsWithMessages = Object.keys(messagesBySpot).length;
if (spotsWithMessages < spotCount)
return 0;
const minCount = Math.min(...Object.values(messagesBySpot));
return minCount;
}
function hasReachedRoundLimit(messages, spotCount, maxRounds) {
if (maxRounds <= 0)
return false;
const currentRounds = calculateRoundCount(messages, spotCount);
return currentRounds >= maxRounds;
}
class RoundRobin {
constructor() {
this.description = {
displayName: 'Round Robin',
name: 'roundRobin',
icon: 'file:roundrobin.svg',
group: ['transform'],
version: 1,
subtitle: '={{ $parameter["mode"] }}',
description: 'Manage conversational loops between multiple participants for LLM workflows',
defaults: {
name: 'Round Robin',
color: '#ff9900',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Operation Mode',
name: 'mode',
type: 'options',
options: [
{
name: 'Setup Conversation',
value: 'setup',
description: 'Initialize a new conversation with empty state',
},
{
name: 'Run Conversation',
value: 'run',
description: 'Process conversation turns and manage round tracking',
},
{
name: 'Store Message',
value: 'store',
description: 'Add a new message to the conversation',
},
{
name: 'Retrieve Conversation',
value: 'retrieve',
description: 'Get the stored conversation history',
},
{
name: 'Recall Memory',
value: 'recall',
description: 'Get the raw memory items in their current state',
},
{
name: 'Clear Conversation',
value: 'clear',
description: 'Reset the conversation history',
},
],
default: 'store',
description: 'Select the operation you want to perform',
},
{
displayName: 'Binary Input Property',
name: 'binaryInputProperty',
type: 'string',
default: 'data',
displayOptions: {
show: {
mode: ['retrieve', 'clear', 'store', 'run'],
},
},
description: 'Name of the binary property containing the conversation data',
placeholder: 'data',
hint: 'This must match the binary output property name from previous Round Robin nodes'
},
{
displayName: 'Conversation ID',
name: 'storageId',
type: 'string',
default: '',
displayOptions: {
show: {
mode: ['store', 'retrieve', 'clear'],
},
},
description: 'Optional: Use a consistent ID to maintain multiple separate conversations in the same workflow',
placeholder: 'my-support-chat',
hint: 'Leave empty to use workflow ID as default (most common scenario)'
},
{
displayName: 'Conversation Setup',
name: 'conversationSetupHeading',
type: 'notice',
default: 'Define the participants and structure of your conversation loop',
displayOptions: {
show: {
mode: ['store'],
},
},
},
{
displayName: 'Number of Participants',
name: 'spotCount',
type: 'number',
default: 3,
required: true,
displayOptions: {
show: {
mode: ['store'],
},
},
description: 'How many different roles or participants in this conversation',
hint: 'For a typical AI chat, use 3 for User+Assistant+System'
},
{
displayName: 'Maximum Conversation Rounds',
name: 'maxRounds',
type: 'number',
default: 0,
displayOptions: {
show: {
mode: ['store'],
},
},
description: 'Optional: Limit the number of full conversation loops (0 = unlimited)',
hint: 'Each round consists of one message from each participant'
},
{
displayName: 'Participant Roles',
name: 'roles',
placeholder: 'Add Role',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
sortable: true,
},
default: {
values: [
{
name: 'User',
description: 'The human user in the conversation',
color: '#6E9BF7',
isEnabled: true
},
{
name: 'Assistant',
description: 'The AI assistant in the conversation',
color: '#9E78FF',
isEnabled: true
},
{
name: 'System',
description: 'System instructions for the AI model',
color: '#FF9900',
isEnabled: true
}
]
},
displayOptions: {
show: {
mode: ['store'],
},
},
description: 'Define the roles for each participant in the conversation',
options: [
{
name: 'values',
displayName: 'Roles',
values: [
{
displayName: 'Role Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of this participant (e.g., User, Assistant, System)',
required: true,
placeholder: 'Assistant'
},
{
displayName: 'Description',
name: 'description',
type: 'string',
typeOptions: {
rows: 2,
},
default: '',
description: 'Optional description of this role',
placeholder: 'The AI helper that responds to user queries'
},
{
displayName: 'Color',
name: 'color',
type: 'color',
default: '#ff9900',
description: 'Color for visual identification in the workflow',
},
{
displayName: 'System Prompt Template',
name: 'systemPrompt',
type: 'string',
typeOptions: {
rows: 3,
},
default: '',
description: 'Optional system prompt to guide this role (most useful for System role)',
placeholder: 'You are a helpful AI assistant that responds concisely.'
},
{
displayName: 'Enabled',
name: 'isEnabled',
type: 'boolean',
default: true,
description: 'Whether this role is active in the conversation',
},
],
},
],
},
{
displayName: 'Current Participant',
name: 'spotIndex',
type: 'number',
default: 0,
displayOptions: {
show: {
mode: ['store'],
},
},
description: 'Which participant is sending this message (0-based index)',
required: true,
hint: '0 = first role, 1 = second role, etc.'
},
{
displayName: 'Message Content Field',
name: 'inputField',
type: 'string',
default: 'output',
displayOptions: {
show: {
mode: ['store'],
},
},
description: 'Name of the input field containing the message to store',
required: true,
placeholder: 'output',
hint: 'Usually "output" from AI nodes or "message" from user inputs'
},
{
displayName: 'Usage Example',
name: 'storeExample',
type: 'notice',
default: '<b>Typical ChatGPT Conversation Setup:</b><br>• User (index 0): Human inputs<br>• Assistant (index 1): AI responses<br>• System (index 2): Instructions to guide the AI',
displayOptions: {
show: {
mode: ['store'],
},
}
},
{
displayName: 'Conversation Execution',
name: 'runHeading',
type: 'notice',
default: 'Process and manage conversation turns by participant role',
displayOptions: {
show: {
mode: ['run'],
},
},
},
{
displayName: 'Current Role Index',
name: 'currentRoleIndex',
type: 'number',
default: 0,
displayOptions: {
show: {
mode: ['run'],
},
},
description: 'Index of the current role taking a turn (0-based)',
hint: 'Typically passed from previous node or setup'
},
{
displayName: 'Message Content Field',
name: 'inputField',
type: 'string',
default: 'output',
displayOptions: {
show: {
mode: ['run', 'store'],
},
},
description: 'Name of the input field containing the message to include',
required: true,
placeholder: 'output',
hint: 'Usually "output" from AI nodes or "message" from user inputs'
},
{
displayName: 'Auto Advance Turn',
name: 'autoAdvance',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['run'],
},
},
description: 'Whether to automatically advance to the next participant after storing',
},
{
displayName: 'Track Rounds',
name: 'trackRounds',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['run'],
},
},
description: 'Whether to track and enforce round limits',
},
{
displayName: 'Continue Until Complete',
name: 'continueUntilComplete',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['run'],
},
},
description: 'Whether to continue conversation until max rounds is reached',
hint: 'Set to false to run one turn at a time'
},
{
displayName: 'Output Configuration',
name: 'retrieveHeading',
type: 'notice',
default: 'Configure how the conversation history should be formatted',
displayOptions: {
show: {
mode: ['retrieve'],
},
},
},
{
displayName: 'Output Format',
name: 'outputFormat',
type: 'options',
options: [
{
name: 'Conversation History for LLM',
value: 'conversationHistory',
description: 'Format suitable for sending directly to AI models',
},
{
name: 'Message Array',
value: 'array',
description: 'Simple array of all messages with role and content',
},
{
name: 'Grouped by Role',
value: 'object',
description: 'Messages organized by participant role',
},
],
default: 'conversationHistory',
displayOptions: {
show: {
mode: ['retrieve'],
},
},
description: 'How to structure the output data',
},
{
displayName: 'LLM Platform',
name: 'llmPlatform',
type: 'options',
options: [
{
name: 'OpenAI (ChatGPT)',
value: 'openai',
description: 'Format compatible with OpenAI models (GPT-3.5, GPT-4, etc.)',
},
{
name: 'Anthropic (Claude)',
value: 'anthropic',
description: 'Format for Anthropic Claude models',
},
{
name: 'Google (Gemini)',
value: 'google',
description: 'Format for Google Gemini models',
},
{
name: 'Generic',
value: 'generic',
description: 'Basic format compatible with most LLMs',
},
],
default: 'openai',
displayOptions: {
show: {
mode: ['retrieve'],
outputFormat: ['conversationHistory'],
},
},
description: 'Which AI model provider this conversation will be sent to',
},
{
displayName: 'Include System Instructions',
name: 'includeSystemPrompt',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['retrieve'],
outputFormat: ['conversationHistory'],
},
},
description: 'Whether to include system role messages in the conversation history',
},
{
displayName: 'System Instructions Position',
name: 'systemPromptPosition',
type: 'options',
options: [
{
name: 'Start of Conversation',
value: 'start',
description: 'Place system instructions at the beginning (recommended)',
},
{
name: 'End of Conversation',
value: 'end',
description: 'Place system instructions at the end',
},
],
default: 'start',
displayOptions: {
show: {
mode: ['retrieve'],
outputFormat: ['conversationHistory'],
includeSystemPrompt: [true],
},
},
description: 'Where to position system instructions in the conversation',
},
{
displayName: 'Simplified Output',
name: 'simplifyOutput',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['retrieve'],
},
},
description: 'Whether to provide clean output with just essential fields (recommended)'
},
{
displayName: 'Maximum Messages',
name: 'maxMessages',
type: 'number',
default: 0,
displayOptions: {
show: {
mode: ['retrieve'],
},
},
description: 'Maximum number of recent messages to include (0 = all messages)',
hint: 'Useful for limiting token usage with large conversation histories'
},
{
displayName: 'Conversation Setup',
name: 'setupOptions',
type: 'notice',
default: 'Set up a new multi-participant conversation with all participants sharing the same conversation history.',
displayOptions: {
show: {
mode: ['setup'],
},
},
},
{
displayName: 'Auto-Detect Input Format',
name: 'autoDetectInput',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['setup'],
},
},
description: 'Whether to automatically detect and use formats from the input data (recommended)',
hint: 'The node will look for conversation_id, RR_title, roles, participants, etc. in the input data',
},
{
displayName: 'Conversation ID',
name: 'conversationId',
type: 'string',
default: '={{ $json.conversation_id || $json.output?.conversation_id || "" }}',
displayOptions: {
show: {
mode: ['setup'],
},
},
description: 'Unique identifier for this conversation',
placeholder: 'lobby-maintenance-123',
},
{
displayName: 'Conversation Title',
name: 'conversationTitle',
type: 'string',
default: '={{ $json.RR_title || $json.output?.RR_title || "" }}',
displayOptions: {
show: {
mode: ['setup'],
},
},
description: 'Title for this conversation',
placeholder: 'Meeting between CEO and Janitor',
},
{
displayName: 'Number of Participants',
name: 'spotCount',
type: 'number',
displayOptions: {
show: {
mode: ['setup', 'store'],
},
},
default: '={{ $json.number_of_participants || $json.output?.number_of_participants || 2 }}',
required: true,
description: 'How many conversation spots/roles to reserve',
hint: 'This is the number of distinct speakers in the conversation'
},
{
displayName: 'Maximum Rounds',
name: 'maxRounds',
type: 'number',
default: '={{ $json.total_rounds || $json.output?.total_rounds || 5 }}',
displayOptions: {
show: {
mode: ['setup'],
},
},
description: 'Maximum number of full conversation rounds to perform',
hint: 'A round consists of one message from each participant'
},
{
displayName: 'Conversation Context',
name: 'conversationContext',
type: 'string',
typeOptions: {
rows: 4
},
default: '={{ $json.conversation_context || $json.output?.conversation_context || "" }}',
displayOptions: {
show: {
mode: ['setup'],
},
},
description: 'Initial context or instructions for this conversation',
placeholder: 'The CEO and Janitor need to discuss cleanliness issues in the lobby area...'
},
{
displayName: 'Talking Points',
name: 'talkingPoints',
type: 'string',
typeOptions: {
rows: 4
},
default: '={{ Array.isArray($json.talking_points || $json.output?.talking_points) ? ($json.talking_points || $json.output?.talking_points).join("\\n") : "" }}',
displayOptions: {
show: {
mode: ['setup'],
},
},
description: 'List of topics to be discussed (one per line)',
placeholder: 'Lobby cleanliness standards\nScheduling of maintenance\nBudget concerns'
},
{
displayName: 'Binary Output Property',
name: 'binaryOutputProperty',
type: 'string',
default: 'data',
displayOptions: {
show: {
mode: ['setup', 'store', 'retrieve', 'clear'],
},
},
description: 'Name of the binary property where conversation data will be stored',
placeholder: 'data',
hint: 'Use the same name for all Round Robin nodes in your workflow'
},
{
displayName: 'Define Roles',
name: 'setupRoles',
type: 'boolean',
displayOptions: {
show: {
mode: ['setup'],
},
},
default: true,
description: 'Whether to define custom roles during setup',
},
{
displayName: 'Use Roles from Input',
name: 'useInputRoles',
type: 'boolean',
displayOptions: {
show: {
mode: ['setup'],
setupRoles: [true],
},
},
default: true,
description: 'Whether to use role definitions from input data',
},
{
displayName: 'Input Roles Path',
name: 'inputRolesPath',
type: 'string',
default: 'roles',
displayOptions: {
show: {
mode: ['setup'],
setupRoles: [true],
useInputRoles: [true],
},
},
description: 'Property path to roles array in the input data',
placeholder: 'roles',
hint: 'For output.roles use "output.roles"'
},
{
displayName: 'Roles',
name: 'roles',
placeholder: 'Add Role',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
mode: ['setup', 'store'],
setupRoles: [true],
useInputRoles: [false],
},
},
options: [
{
name: 'values',
displayName: 'Roles',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'User',
description: 'Name of this role/persona',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
placeholder: 'User messages',
description: 'Description of this role/persona',
},
{
displayName: 'Color',
name: 'color',
type: 'color',
default: '#ff6b6b',
description: 'Color associated with this role',
},
{
displayName: 'System Prompt',
name: 'systemPrompt',
type: 'string',
default: '',
typeOptions: {
rows: 3,
},
description: 'System prompt associated with this role (optional)',
},
{
displayName: 'Is Enabled',
name: 'isEnabled',
type: 'boolean',
default: true,
description: 'Whether this role should be included in conversation outputs',
},
],
},
],
description: 'Define the different participants in the conversation',
},
{
displayName: 'Memory Recall',
name: 'recallOptions',
type: 'notice',
default: 'Recall the raw memory state of the conversation to inspect or debug its contents.',
displayOptions: {
show: {
mode: ['recall'],
},
},
},
{
displayName: 'Binary Input Property',
name: 'binaryInputProperty',
type: 'string',
default: 'data',
displayOptions: {
show: {
mode: ["retrieve", "clear", "store", "run"],
},
},
description: 'Name of the binary property containing conversation data',
placeholder: 'data',
hint: 'This should match the binary output property of the previous RoundRobin node'
},
{
displayName: 'Include Messages',
name: 'includeMessages',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['recall'],
},
},
description: 'Whether to include message history in the recall output',
},
{
displayName: 'Include Roles',
name: 'includeRoles',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['recall'],
},
},
description: 'Whether to include role definitions in the recall output',
},
{
displayName: 'Include Metadata',
name: 'includeMetadata',
type: 'boolean',
default: true,
displayOptions: {
show: {
mode: ['recall'],
},
},
description: 'Whether to include conversation metadata in the recall output',
},
{
displayName: 'Memory Selection',
name: 'recallOptions',
type: 'notice',
default: 'Select which conversation memory to retrieve.',
displayOptions: {
show: {
mode: ['recall'],
},
},
},
{
displayName: 'Conversation Memory',
name: 'memoryId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
displayOptions: {
show: {
mode: ['recall'],
},
},
description: 'Choose which conversation memory to recall',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchConversationMemories',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9_-]+',
errorMessage: 'Not a valid conversation ID',
},
},
],
},
]
},
],
};
}
async execute() {
var _a;
const items = this.getInputData();
const returnData = [];
const mode = this.getNodeParameter('mode', 0);
try {
const nodeName = this.getNode().name;
let workflowId = (_a = this.getWorkflow()) === null || _a === void 0 ? void 0 : _a.id;
console.log(`[Execution] Raw Workflow ID: ${workflowId}`);
if (!workflowId) {
console.warn(`[Execution] Warning: Workflow ID is undefined. Falling back to node name ('${nodeName}') for storage key.`);
workflowId = nodeName;
}
else {
workflowId = String(workflowId);
}
const userStorageId = this.getNodeParameter('storageId', 0, '');
if (userStorageId) {
console.log(`[Execution] Using user-provided Storage ID: "${userStorageId}" instead of workflow ID`);
workflowId = userStorageId;
}
console.log(`[Execution] Using effective ID for storage: ${workflowId}`);
await RoundRobin.handleBinaryStorageExecution(this, items, returnData, mode, workflowId);
return [returnData];
}
catch (error) {
if (error instanceof n8n_workflow_1.NodeOperationError) {
throw error;
}
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error instanceof Error ? error.message : 'An unknown error occurred');
}
}
static async handleBinaryStorageExecution(executeFunctions, items, returnData, mode, storageId) {
var _a, _b, _c, _d, _e, _f;
console.log(`[Binary Storage] Using internal binary storage with ID: ${storageId}`);
const storageManager = (0, ExternalStorage_1.createStorageManager)(executeFunctions, 'binary', storageId);
const binaryOutputProperty = executeFunctions.getNodeParameter('binaryOutputProperty', 0, 'data');
if (mode === 'setup') {
try {
console.log('[Binary Storage] Setting up new conversation');
let spotCount = executeFunctions.getNodeParameter('spotCount', 0, 3);
const maxRounds = executeFunctions.getNodeParameter('maxRounds', 0, 5);
const conversationTitle = executeFunctions.getNodeParameter('conversationTitle', 0, '');
const conversationContext = executeFunctions.getNodeParameter('conversationContext', 0, '');
const talkingPointsStr = executeFunctions.getNodeParameter('talkingPoints', 0, '');
const conversationId = executeFunctions.getNodeParameter('conversationId', 0, '');
const autoDetectInput = executeFunctions.getNodeParameter('autoDetectInput', 0, true);
const talkingPoints = talkingPointsStr
? talkingPointsStr.split('\n').filter(line => line.trim() !== '')
: [];
let uniqueConvoId = conversationId;
if (!uniqueConvoId) {
const timestamp = Date.now();
const date = new Date(timestamp).toISOString().split('T')[0];
const title = conversationTitle ?
conversationTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 20) :
'conversation';
uniqueConvoId = `${title}-${date}-${Math.random().toString(36).substring(2, 7)}`;
}
console.log(`[Binary Storage] Conversation ID: ${uniqueConvoId}`);
const setupRoles = executeFunctions.getNodeParameter('setupRoles', 0, true);
let roles = getDefaultRoles();
let participants = [];
if (setupRoles) {
const useInputRoles = executeFunctions.getNodeParameter('useInputRoles', 0, true);
if (useInputRoles && autoDetectInput) {
const inputRolesPath = executeFunctions.getNodeParameter('inputRolesPath', 0, 'roles');
let inputRoles = [];
const possibleRolePaths = [
'output.roles',
'roles',
'data.roles',
'data.output.roles',
'conversation.roles',
'result.roles',
'results.roles',
'response.roles',
'payload.roles',
'payload.output.roles'
];
let rolesFound = false;
console.log(`[Binary Storage] Searching for roles in ${possibleRolePaths.length} possible locations`);
for (const path of possibleRolePaths) {
const rolesData = getNestedProperty(items[0].json, path);
if (rolesData && Array.isArray(rolesData) && rolesData.length > 0) {
inputRoles = rolesData;
console.log(`[Binary Storage] Found ${inputRoles.length} roles at path: ${path}`);
rolesFound = true;
break;
}
}
if (!rolesFound && inputRolesPath) {
const rolesData = getNestedProperty(items[0].json, inputRolesPath);
if (rolesData && Array.isArray(rolesData) && rolesData.length > 0) {
inputRoles = rolesData;
console.log(`[Binary Storage] Found ${inputRoles.length} roles at user path: ${inputRolesPath}`);
rolesFound = true;
}
}
if (!rolesFound && Object.keys(items[0].json).length > 0) {
console.log(`[Binary Storage] No roles found in common paths, trying deep search`);
const foundRoles = findRolesDeep(items[0].json);
if (foundRoles && foundRoles.length > 0) {
inputRoles = foundRoles;
console.log(`[Binary Storage] Found ${inputRoles.length} roles via deep search`);
rolesFound = true;
}
}
if (inputRoles.length > 0) {
console.log(`[Binary Storage] Found ${inputRoles.length} roles from input data`);
const mappedRoles = inputRoles.map(role => {
console.log(`[Binary Storage] Processing role: ${JSON.stringify(role.name || 'unnamed')}`);
return {
name: role.name || role.role_name || role.roleName || role.title || '',
description: role.description || role.desc || role.roleDescription || '',
color: role.color || role.roleColor || role.hexColor || '#ff9900',
systemPrompt: role.system_prompt || role.systemPrompt || role.prompt || role.instructions || '',
isEnabled: role.is_enabled !== undefined ? role.is_enabled :
(role.isEnabled !== undefined ? role.isEnabled :
(role.enabled !== undefined ? role.enabled : true)),
expertise: []
};
});
if (mappedRoles.length > 0) {
roles = mappedRoles;
const inputSpotCount = getNestedProperty(items[0].json, 'output.number_of_participants') ||
getNestedProperty(items[0].json, 'number_of_participants');
if (inputSpotCount && typeof inputSpotCount === 'number') {
console.log(`[Binary Storage] Using number_of_participants from input: ${inputSpotCount}`);
spotCount = inputSpotCount;
}
else if (mappedRoles.length > spotCount) {
console.log(`[Binary Storage] Adjusting spotCount from ${spotCount} to ${mappedRoles.length} to match detected roles`);
spotCount = mappedRoles.length;
}
}
else {
console.log('[Binary Storage] Input roles were invalid, using defaults');
}
}
else {
console.log(`[Binary Storage] No input roles found, using defaults`);
}
if (autoDetectInput) {
try {
const possiblePaths = [
'output.participants',
'participants',
'data.participants',
'data.output.participants',
'conversation.participants',
'result.participants',
'results.participants',
'response.participants',
'payload.participants',
'payload.output.participants',
'users',
'members',
'attendees'
];
let participantsArray = [];
console.log(`[Binary Storage] Searching for participants in ${possiblePaths.length} possible locations`);
for (const path of possiblePaths) {
const pathData = getNestedProperty(items[0].json, path);
if (pathData && Array.isArray(pathData)) {
participantsArray = pathData;
console.log(`[Binary Storage] Found participants at path: ${path}`);
break;
}
}
if (participantsArray.length === 0 && Object.keys(items[0].json).length > 0) {
console.log(`[Binary Storage] No participants found in common paths, trying deep search`);
const foundParticipants = findParticipantsDeep(items[0].json);
if (foundParticipants && foundParticipants.length > 0) {
participantsArray = foundParticipants;
console.log(`[Binary Storage] Found ${participantsArray.length} participants via deep search`);
}
}
if (participantsArray.length > 0) {
console.log(`[Binary Storage] Found ${participantsArray.length} participants in input data`);
participants = participantsArray.map(p => p);
participants.forEach((p, i) => {
console.log(`[Binary Storage] Participant ${i + 1}: role=${p.role}, id=${p.participant_id || p.id}`);
});
}
}
catch (error) {
console.log(`[Binary Storage] Error processing participants: ${error.message}`);
}
}
}
else {
const rolesCollection = executeFunctions.getNodeParameter('roles', 0);
const customRoles = processRoles(rolesCollection);
if (customRoles.length > 0) {
roles = customRoles;
console.log(`[Binary Storage] Using ${roles.length} custom defined roles`);
}
else {
console.log('[Binary Storage] No custom roles defined, using defaults');
}
}
}
else {
console.log('[Binary Storage] Custom roles disabled, using defaults');
}
const storageMetadata = {
title: conversationTitle,
context: conversationContext,
roundCount: 0,
maxRounds: maxRounds,
lastUpdated: Date.now(),
isNewConversation: true,
conversationId: uniqueConvoId,
talkingPoints: talkingPoints,
created: Date.now(),
createdBy: executeFunctions.getNode().name,