diff --git a/packages/components/credentials/RecallIOMemoryApi.credential.ts b/packages/components/credentials/RecallIOMemoryApi.credential.ts
new file mode 100644
index 00000000000..de42cf0aa7b
--- /dev/null
+++ b/packages/components/credentials/RecallIOMemoryApi.credential.ts
@@ -0,0 +1,27 @@
+import { INodeParams, INodeCredential } from '../src/Interface'
+
+class RecallIOMemoryApi implements INodeCredential {
+ label: string
+ name: string
+ version: number
+ description: string
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'RecallIO Memory API'
+ this.name = 'recallIOMemoryApi'
+ this.version = 1.0
+ this.description =
+ 'Visit RecallIO Platform to get your API credentials'
+ this.inputs = [
+ {
+ label: 'API Key',
+ name: 'apiKey',
+ type: 'password',
+ description: 'API Key from RecallIO dashboard'
+ }
+ ]
+ }
+}
+
+module.exports = { credClass: RecallIOMemoryApi }
diff --git a/packages/components/nodes/memory/RecallIOMemory/RecallIO.ts b/packages/components/nodes/memory/RecallIOMemory/RecallIO.ts
new file mode 100644
index 00000000000..576beb4f86a
--- /dev/null
+++ b/packages/components/nodes/memory/RecallIOMemory/RecallIO.ts
@@ -0,0 +1,399 @@
+import { RecallioMemory as BaseRecallioMemory, RecallioMemoryInput } from 'recallio-community'
+import { BaseMessage } from '@langchain/core/messages'
+import { InputValues, MemoryVariables, OutputValues } from '@langchain/core/memory'
+import { ICommonObject, IDatabaseEntity } from '../../../src'
+import { IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface'
+import { getBaseClasses, getCredentialData, getCredentialParam, mapChatMessageToBaseMessage } from '../../../src/utils'
+import { DataSource } from 'typeorm'
+import { v4 as uuidv4 } from 'uuid'
+
+interface BufferMemoryExtendedInput {
+ sessionId: string
+ appDataSource: DataSource
+ databaseEntities: IDatabaseEntity
+ chatflowid: string
+}
+
+interface NodeFields extends RecallioMemoryInput, RecallioMemoryExtendedInput, BufferMemoryExtendedInput {
+ searchOnly: boolean
+ useFlowiseChatId: boolean
+ input: string
+}
+
+class RecallIO_Memory implements INode {
+ label: string
+ name: string
+ version: number
+ description: string
+ type: string
+ icon: string
+ category: string
+ baseClasses: string[]
+ credential: INodeParams
+ inputs: INodeParams[]
+
+ constructor() {
+ this.label = 'RecallIO'
+ this.name = 'recallio'
+ this.version = 1.0
+ this.type = 'RecallIO'
+ this.icon = 'recallio.svg'
+ this.category = 'Memory'
+ this.description = 'Stores and manages chat memory using RecallIO service'
+ this.baseClasses = [this.type, ...getBaseClasses(BaseRecallioMemory)]
+ this.credential = {
+ label: 'Connect Credential',
+ name: 'credential',
+ type: 'credential',
+ optional: false,
+ description: 'Configure API Key for RecallIO service',
+ credentialNames: ['recallIOMemoryApi']
+ }
+ this.inputs = [
+ {
+ label: 'User ID',
+ name: 'user_id',
+ type: 'string',
+ description: 'Unique identifier for the user. Required only if "Use Flowise Chat ID" is OFF.',
+ default: 'flowise-default-user',
+ optional: true
+ },
+ // Added toggle to use Flowise chat ID
+ {
+ label: 'Use Flowise Chat ID',
+ name: 'useFlowiseChatId',
+ type: 'boolean',
+ description: 'Use the Flowise internal Chat ID as the RecallIO User ID, overriding the "User ID" field above.',
+ default: false,
+ optional: true
+ },
+ {
+ label: 'Project ID',
+ name: 'projectId',
+ type: 'string',
+ description: 'RecallIO Project ID',
+ default: '',
+ optional: false
+ },
+ {
+ label: 'Search Only',
+ name: 'searchOnly',
+ type: 'boolean',
+ description: 'Search only mode',
+ default: false,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Scope',
+ name: 'scope',
+ type: 'string',
+ description: 'Recall scope (e.g., user, app)',
+ default: 'user',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Tags (comma-separated)',
+ name: 'tags',
+ type: 'string',
+ description: 'Optional tags to associate with memories, e.g. tag1, tag2',
+ default: '',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Consent Flag',
+ name: 'consentFlag',
+ type: 'boolean',
+ description: 'User consent to store memory',
+ default: true,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Separate Messages',
+ name: 'separateMessages',
+ type: 'boolean',
+ description: 'Return memory context as separate messages instead of a single system string',
+ default: false,
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'AI Prefix',
+ name: 'aiPrefix',
+ type: 'string',
+ default: 'ai',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Human Prefix',
+ name: 'humanPrefix',
+ type: 'string',
+ default: 'human',
+ optional: true,
+ additionalParams: true
+ },
+ // The following are kept for parity with similar nodes; not used directly by RecallIO
+ { label: 'App ID', name: 'app_id', type: 'string', default: '', optional: true, additionalParams: true },
+ { label: 'Organization ID', name: 'org_id', type: 'string', default: '', optional: true, additionalParams: true },
+ {
+ label: 'Memory Key',
+ name: 'memoryKey',
+ type: 'string',
+ default: 'chat_history',
+ additionalParams: true
+ },
+ {
+ label: 'Input Key',
+ name: 'inputKey',
+ type: 'string',
+ default: 'input',
+ optional: true,
+ additionalParams: true
+ },
+ {
+ label: 'Output Key',
+ name: 'outputKey',
+ type: 'string',
+ default: 'text',
+ optional: true,
+ additionalParams: true
+ }
+ ]
+ }
+
+ async init(nodeData: INodeData, input: string, options: ICommonObject): Promise {
+ return await initializeRecallIO(nodeData, input, options)
+ }
+}
+
+const parseTags = (raw?: string): string[] | undefined => {
+ if (!raw) return undefined
+ try {
+ // allow JSON array input
+ const maybeArr = JSON.parse(raw)
+ if (Array.isArray(maybeArr)) return maybeArr.map((t) => String(t))
+ } catch (_) {
+ // fall through to comma-separated parsing
+ }
+ const parts = String(raw)
+ .split(',')
+ .map((s) => s.trim())
+ .filter((s) => s.length > 0)
+ return parts.length ? parts : undefined
+}
+
+const initializeRecallIO = async (
+ nodeData: INodeData,
+ input: string,
+ options: ICommonObject
+): Promise => {
+ const initialUserId = nodeData.inputs?.user_id as string
+ const useFlowiseChatId = nodeData.inputs?.useFlowiseChatId as boolean
+ const orgId = options.orgId as string
+
+ if (!useFlowiseChatId && !initialUserId) {
+ throw new Error('User ID field cannot be empty when "Use Flowise Chat ID" is OFF.')
+ }
+
+ const credentialData = await getCredentialData(nodeData.credential ?? '', options)
+ const apiKey = getCredentialParam('apiKey', credentialData, nodeData)
+
+ const constructorSessionId = initialUserId || (useFlowiseChatId ? 'flowise-chat-id-placeholder' : '')
+
+ const obj: NodeFields = {
+ apiKey: apiKey,
+ projectId: (nodeData.inputs?.projectId as string) || '',
+ humanPrefix: (nodeData.inputs?.humanPrefix as string) || 'human',
+ aiPrefix: (nodeData.inputs?.aiPrefix as string) || 'ai',
+ inputKey: (nodeData.inputs?.inputKey as string) || 'input',
+ outputKey: (nodeData.inputs?.outputKey as string) || 'text',
+ memoryKey: (nodeData.inputs?.memoryKey as string) || 'chat_history',
+ sessionId: constructorSessionId,
+ scope: ((nodeData.inputs?.scope as string) || 'user') as string,
+ tags: parseTags(nodeData.inputs?.tags as string),
+ consentFlag: (nodeData.inputs?.consentFlag as boolean) ?? true,
+ separateMessages: (nodeData.inputs?.separateMessages as boolean) ?? false,
+ returnMessages: true,
+ appDataSource: options.appDataSource as DataSource,
+ databaseEntities: options.databaseEntities as IDatabaseEntity,
+ chatflowid: options.chatflowid as string,
+ searchOnly: (nodeData.inputs?.searchOnly as boolean) || false,
+ useFlowiseChatId: useFlowiseChatId,
+ input: input,
+ orgId: orgId
+ }
+
+ if (!obj.projectId) throw new Error('Project ID cannot be empty')
+
+ return new RecallioMemoryExtended(obj)
+}
+
+interface RecallioMemoryExtendedInput extends RecallioMemoryInput {
+ useFlowiseChatId: boolean
+ orgId: string
+}
+
+class RecallioMemoryExtended extends BaseRecallioMemory implements MemoryMethods {
+ initialSessionId: string
+ orgId: string
+ memoryKey: string
+ inputKey: string
+ appDataSource: DataSource
+ databaseEntities: IDatabaseEntity
+ chatflowid: string
+ searchOnly: boolean
+ useFlowiseChatId: boolean
+ input: string
+
+ constructor(fields: NodeFields) {
+ super(fields)
+ this.initialSessionId = fields.sessionId ?? ''
+ this.memoryKey = fields.memoryKey ?? 'chat_history'
+ this.inputKey = fields.inputKey ?? 'input'
+ this.appDataSource = fields.appDataSource
+ this.databaseEntities = fields.databaseEntities
+ this.chatflowid = fields.chatflowid
+ this.searchOnly = fields.searchOnly
+ this.useFlowiseChatId = fields.useFlowiseChatId
+ this.input = fields.input
+ this.orgId = fields.orgId
+ }
+
+ // Select sessionId based on toggle state (Flowise chat ID or input field)
+ private getEffectiveSessionId(overrideSessionId?: string): string {
+ let effectiveId: string | undefined
+
+ if (this.useFlowiseChatId) {
+ if (overrideSessionId) {
+ effectiveId = overrideSessionId
+ } else {
+ throw new Error('RecallIO: "Use Flowise Chat ID" is ON, but no runtime chat ID (overrideUserId) was provided.')
+ }
+ } else {
+ effectiveId = this.initialSessionId
+ }
+
+ if (!effectiveId) {
+ throw new Error('RecallIO: Could not determine a valid Session/User ID for the operation. Check User ID input field.')
+ }
+ return effectiveId
+ }
+
+ async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise {
+ const effectiveId = this.getEffectiveSessionId(overrideSessionId)
+ this.sessionId = effectiveId
+ const forwardedValues: InputValues = {
+ ...values,
+ input: (values as any)?.[this.inputKey] ?? (values as any)?.input ?? '*'
+ }
+ const result = await super.loadMemoryVariables(forwardedValues)
+ if (!(this.memoryKey in result)) {
+ // Guarantee presence of the expected memory key to satisfy downstream prompts
+ ;(result as any)[this.memoryKey] = this.returnMessages ? [] : ''
+ }
+ return result
+ }
+
+ async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise {
+ if (this.searchOnly) return
+ const effectiveId = this.getEffectiveSessionId(overrideSessionId)
+ this.sessionId = effectiveId
+ return super.saveContext(inputValues, outputValues)
+ }
+
+ async clear(overrideSessionId = ''): Promise {
+ const effectiveId = this.getEffectiveSessionId(overrideSessionId)
+ this.sessionId = effectiveId
+ return super.clear()
+ }
+
+ async getChatMessages(
+ overrideUserId = '',
+ returnBaseMessages = false,
+ prependMessages?: IMessage[]
+ ): Promise {
+ const flowiseSessionId = overrideUserId
+ if (!flowiseSessionId) {
+ console.warn('RecallIO: getChatMessages called without overrideUserId (Flowise Session ID). Cannot fetch DB messages.')
+ return []
+ }
+
+ let chatMessage = await this.appDataSource.getRepository(this.databaseEntities['ChatMessage']).find({
+ where: {
+ sessionId: flowiseSessionId,
+ chatflowid: this.chatflowid
+ },
+ order: {
+ createdDate: 'DESC'
+ },
+ take: 10
+ })
+ chatMessage = chatMessage.reverse()
+
+ let returnIMessages: IMessage[] = chatMessage.map((m) => ({
+ message: m.content as string,
+ type: m.role as MessageType
+ }))
+
+ if (prependMessages?.length) {
+ returnIMessages.unshift(...prependMessages)
+ // Reverted to original simpler unshift
+ chatMessage.unshift(...(prependMessages as any))
+ }
+
+ if (returnBaseMessages) {
+ const memoryVariables = await this.loadMemoryVariables({ input: this.input ?? '' }, overrideUserId)
+ const recallHistory = memoryVariables[this.memoryKey]
+
+ if (recallHistory && typeof recallHistory === 'string') {
+ const systemMessage = {
+ role: 'apiMessage' as MessageType,
+ content: recallHistory,
+ id: uuidv4()
+ }
+ // Ensure RecallIO history message also conforms structurally if mapChatMessageToBaseMessage is strict
+ chatMessage.unshift(systemMessage as any) // Cast needed if mixing structures
+ } else if (recallHistory) {
+ console.warn('RecallIO history is not a string, cannot prepend directly.')
+ }
+
+ return await mapChatMessageToBaseMessage(chatMessage, this.orgId)
+ }
+
+ return returnIMessages
+ }
+
+ async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideUserId = ''): Promise {
+ const effectiveUserId = this.getEffectiveSessionId(overrideUserId)
+ const input = msgArray.find((msg) => msg.type === 'userMessage')
+ const output = msgArray.find((msg) => msg.type === 'apiMessage')
+
+ if (input && output) {
+ const inputValues = { [this.inputKey ?? 'input']: input.text }
+ const outputValues = { [this.outputKey ?? 'text']: output.text }
+ await this.saveContext(inputValues, outputValues, effectiveUserId)
+ } else {
+ console.warn('RecallIO: Could not find both input and output messages to save context.')
+ }
+ }
+
+ async clearChatMessages(overrideUserId = ''): Promise {
+ const effectiveUserId = this.getEffectiveSessionId(overrideUserId)
+ await this.clear(effectiveUserId)
+
+ const flowiseSessionId = overrideUserId
+ if (flowiseSessionId) {
+ await this.appDataSource
+ .getRepository(this.databaseEntities['ChatMessage'])
+ .delete({ sessionId: flowiseSessionId, chatflowid: this.chatflowid })
+ } else {
+ console.warn('RecallIO: clearChatMessages called without overrideUserId (Flowise Session ID). Cannot clear DB messages.')
+ }
+ }
+}
+
+module.exports = { nodeClass: RecallIO_Memory }
diff --git a/packages/components/nodes/memory/RecallIOMemory/recallio.svg b/packages/components/nodes/memory/RecallIOMemory/recallio.svg
new file mode 100644
index 00000000000..20170f017b5
--- /dev/null
+++ b/packages/components/nodes/memory/RecallIOMemory/recallio.svg
@@ -0,0 +1,18 @@
+
diff --git a/packages/components/package.json b/packages/components/package.json
index eea9b24f277..5fdee6e1d5b 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -136,6 +136,7 @@
"playwright": "^1.35.0",
"puppeteer": "^20.7.1",
"pyodide": ">=0.21.0-alpha.2",
+ "recallio-community": "^1.0.1",
"redis": "^4.6.7",
"remove-markdown": "^0.6.2",
"replicate": "^0.31.1",