add llamaindex

This commit is contained in:
Henry
2023-12-04 20:04:09 +00:00
parent 423c23aaf0
commit 40f8371de9
56 changed files with 4509 additions and 536 deletions
@@ -2,7 +2,7 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Inter
import { initializeAgentExecutorWithOptions, AgentExecutor, InitializeAgentExecutorOptions } from 'langchain/agents'
import { Tool } from 'langchain/tools'
import { BaseChatMemory } from 'langchain/memory'
import { getBaseClasses, mapChatHistory } from '../../../src/utils'
import { getBaseClasses } from '../../../src/utils'
import { BaseChatModel } from 'langchain/chat_models/base'
import { flatten } from 'lodash'
import { additionalCallbacks } from '../../../src/handler'
@@ -90,18 +90,17 @@ class ConversationalAgent_Agents implements INode {
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
const executor = nodeData.instance as AgentExecutor
const memory = nodeData.inputs?.memory as BaseChatMemory
const memory = nodeData.inputs?.memory
memory.returnMessages = true // Return true for BaseChatModel
if (options && options.chatHistory) {
const chatHistoryClassName = memory.chatHistory.constructor.name
// Only replace when its In-Memory
if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') {
memory.chatHistory = mapChatHistory(options)
executor.memory = memory
}
/* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory
* LongTermMemory will automatically retrieved chatHistory from sessionId
*/
if (options && options.chatHistory && memory.isShortTermMemory) {
await memory.resumeMessages(options.chatHistory)
}
;(executor.memory as any).returnMessages = true // Return true for BaseChatModel
executor.memory = memory
const callbacks = await additionalCallbacks(nodeData, options)
@@ -1,6 +1,6 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents'
import { getBaseClasses, mapChatHistory } from '../../../src/utils'
import { getBaseClasses } from '../../../src/utils'
import { flatten } from 'lodash'
import { BaseChatMemory } from 'langchain/memory'
import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
@@ -58,8 +58,8 @@ class ConversationalRetrievalAgent_Agents implements INode {
async init(nodeData: INodeData): Promise<any> {
const model = nodeData.inputs?.model
const memory = nodeData.inputs?.memory as BaseChatMemory
const systemMessage = nodeData.inputs?.systemMessage as string
const memory = nodeData.inputs?.memory as BaseChatMemory
let tools = nodeData.inputs?.tools
tools = flatten(tools)
@@ -78,19 +78,21 @@ class ConversationalRetrievalAgent_Agents implements INode {
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
const executor = nodeData.instance as AgentExecutor
const memory = nodeData.inputs?.memory
if (executor.memory) {
;(executor.memory as any).memoryKey = 'chat_history'
;(executor.memory as any).outputKey = 'output'
;(executor.memory as any).returnMessages = true
memory.memoryKey = 'chat_history'
memory.outputKey = 'output'
memory.returnMessages = true
const chatHistoryClassName = (executor.memory as any).chatHistory.constructor.name
// Only replace when its In-Memory
if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') {
;(executor.memory as any).chatHistory = mapChatHistory(options)
}
/* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory
* LongTermMemory will automatically retrieved chatHistory from sessionId
*/
if (options && options.chatHistory && memory.isShortTermMemory) {
await memory.resumeMessages(options.chatHistory)
}
executor.memory = memory
const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
@@ -81,50 +81,8 @@ class OpenAIAssistant_Agents implements INode {
}
}
async init(): Promise<any> {
return null
}
//@ts-ignore
memoryMethods = {
async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise<void> {
const selectedAssistantId = nodeData.inputs?.selectedAssistant as string
const appDataSource = options.appDataSource as DataSource
const databaseEntities = options.databaseEntities as IDatabaseEntity
let sessionId = nodeData.inputs?.sessionId as string
const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({
id: selectedAssistantId
})
if (!assistant) {
options.logger.error(`Assistant ${selectedAssistantId} not found`)
return
}
if (!sessionId && options.chatId) {
const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({
chatId: options.chatId
})
if (!chatmsg) {
options.logger.error(`Chat Message with Chat Id: ${options.chatId} not found`)
return
}
sessionId = chatmsg.sessionId
}
const credentialData = await getCredentialData(assistant.credential ?? '', options)
const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData)
if (!openAIApiKey) {
options.logger.error(`OpenAI ApiKey not found`)
return
}
const openai = new OpenAI({ apiKey: openAIApiKey })
options.logger.info(`Clearing OpenAI Thread ${sessionId}`)
if (sessionId) await openai.beta.threads.del(sessionId)
options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`)
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
return new OpenAIAssistant({ nodeData, options })
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string | object> {
@@ -459,4 +417,58 @@ const formatToOpenAIAssistantTool = (tool: any): OpenAI.Beta.AssistantCreatePara
}
}
interface OpenAIAssistantInput {
nodeData: INodeData
options: ICommonObject
}
class OpenAIAssistant {
nodeData: INodeData
options: ICommonObject = {}
constructor(fields: OpenAIAssistantInput) {
this.nodeData = fields.nodeData
this.options = fields.options
}
async clearChatMessages(): Promise<void> {
const selectedAssistantId = this.nodeData.inputs?.selectedAssistant as string
const appDataSource = this.options.appDataSource as DataSource
const databaseEntities = this.options.databaseEntities as IDatabaseEntity
let sessionId = this.nodeData.inputs?.sessionId as string
const assistant = await appDataSource.getRepository(databaseEntities['Assistant']).findOneBy({
id: selectedAssistantId
})
if (!assistant) {
this.options.logger.error(`Assistant ${selectedAssistantId} not found`)
return
}
if (!sessionId && this.options.chatId) {
const chatmsg = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({
chatId: this.options.chatId
})
if (!chatmsg) {
this.options.logger.error(`Chat Message with Chat Id: ${this.options.chatId} not found`)
return
}
sessionId = chatmsg.sessionId
}
const credentialData = await getCredentialData(assistant.credential ?? '', this.options)
const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, this.nodeData)
if (!openAIApiKey) {
this.options.logger.error(`OpenAI ApiKey not found`)
return
}
const openai = new OpenAI({ apiKey: openAIApiKey })
this.options.logger.info(`Clearing OpenAI Thread ${sessionId}`)
if (sessionId) await openai.beta.threads.del(sessionId)
this.options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`)
}
}
module.exports = { nodeClass: OpenAIAssistant_Agents }
@@ -1,6 +1,6 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { initializeAgentExecutorWithOptions, AgentExecutor } from 'langchain/agents'
import { getBaseClasses, mapChatHistory } from '../../../src/utils'
import { getBaseClasses } from '../../../src/utils'
import { BaseLanguageModel } from 'langchain/base_language'
import { flatten } from 'lodash'
import { BaseChatMemory } from 'langchain/memory'
@@ -56,8 +56,8 @@ class OpenAIFunctionAgent_Agents implements INode {
async init(nodeData: INodeData): Promise<any> {
const model = nodeData.inputs?.model as BaseLanguageModel
const memory = nodeData.inputs?.memory as BaseChatMemory
const systemMessage = nodeData.inputs?.systemMessage as string
const memory = nodeData.inputs?.memory as BaseChatMemory
let tools = nodeData.inputs?.tools
tools = flatten(tools)
@@ -69,25 +69,23 @@ class OpenAIFunctionAgent_Agents implements INode {
prefix: systemMessage ?? `You are a helpful AI assistant.`
}
})
if (memory) executor.memory = memory
executor.memory = memory
return executor
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
const executor = nodeData.instance as AgentExecutor
const memory = nodeData.inputs?.memory as BaseChatMemory
const memory = nodeData.inputs?.memory
memory.returnMessages = true // Return true for BaseChatModel
if (options && options.chatHistory) {
const chatHistoryClassName = memory.chatHistory.constructor.name
// Only replace when its In-Memory
if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') {
memory.chatHistory = mapChatHistory(options)
executor.memory = memory
}
/* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory
* LongTermMemory will automatically retrieved chatHistory from sessionId
*/
if (options && options.chatHistory && memory.isShortTermMemory) {
await memory.resumeMessages(options.chatHistory)
}
;(executor.memory as any).returnMessages = true // Return true for BaseChatModel
executor.memory = memory
const loggerHandler = new ConsoleCallbackHandler(options.logger)
const callbacks = await additionalCallbacks(nodeData, options)
@@ -1,6 +1,6 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { ConversationChain } from 'langchain/chains'
import { getBaseClasses, mapChatHistory } from '../../../src/utils'
import { getBaseClasses } from '../../../src/utils'
import { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate } from 'langchain/prompts'
import { BufferMemory } from 'langchain/memory'
import { BaseChatModel } from 'langchain/chat_models/base'
@@ -105,15 +105,14 @@ class ConversationChain_Chains implements INode {
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
const chain = nodeData.instance as ConversationChain
const memory = nodeData.inputs?.memory as BufferMemory
const memory = nodeData.inputs?.memory
memory.returnMessages = true // Return true for BaseChatModel
if (options && options.chatHistory) {
const chatHistoryClassName = memory.chatHistory.constructor.name
// Only replace when its In-Memory
if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') {
memory.chatHistory = mapChatHistory(options)
}
/* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory
* LongTermMemory will automatically retrieved chatHistory from sessionId
*/
if (options && options.chatHistory && memory.isShortTermMemory) {
await memory.resumeMessages(options.chatHistory)
}
chain.memory = memory
@@ -1,9 +1,9 @@
import { BaseLanguageModel } from 'langchain/base_language'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, mapChatHistory } from '../../../src/utils'
import { ICommonObject, IMessage, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { ConversationalRetrievalQAChain, QAChainParams } from 'langchain/chains'
import { BaseRetriever } from 'langchain/schema/retriever'
import { BufferMemory, BufferMemoryInput } from 'langchain/memory'
import { BufferMemoryInput, BufferMemory } from 'langchain/memory'
import { PromptTemplate } from 'langchain/prompts'
import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
import {
@@ -158,7 +158,7 @@ class ConversationalRetrievalQAChain_Chains implements INode {
returnMessages: true
}
if (chainOption === 'refine') fields.outputKey = 'output_text'
obj.memory = new BufferMemory(fields)
obj.memory = new BufferMemoryExtended(fields)
}
const chain = ConversationalRetrievalQAChain.fromLLM(model, vectorStoreRetriever, obj)
@@ -178,12 +178,11 @@ class ConversationalRetrievalQAChain_Chains implements INode {
const obj = { question: input }
if (options && options.chatHistory && chain.memory) {
const chatHistoryClassName = (chain.memory as any).chatHistory.constructor.name
// Only replace when its In-Memory
if (chatHistoryClassName && chatHistoryClassName === 'ChatMessageHistory') {
;(chain.memory as any).chatHistory = mapChatHistory(options)
}
/* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory
* LongTermMemory will automatically retrieved chatHistory from sessionId
*/
if (options && options.chatHistory && chain.memory && (chain.memory as any).isShortTermMemory) {
await (chain.memory as any).resumeMessages(options.chatHistory)
}
const loggerHandler = new ConsoleCallbackHandler(options.logger)
@@ -216,4 +215,27 @@ class ConversationalRetrievalQAChain_Chains implements INode {
}
}
class BufferMemoryExtended extends BufferMemory {
isShortTermMemory = true
constructor(fields: BufferMemoryInput) {
super(fields)
}
async clearChatMessages(): Promise<void> {
await this.clear()
}
async resumeMessages(messages: IMessage[]): Promise<void> {
// Clear existing chatHistory to avoid duplication
if (messages.length) await this.clear()
// Insert into chatHistory
for (const msg of messages) {
if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message)
else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message)
}
}
}
module.exports = { nodeClass: ConversationalRetrievalQAChain_Chains }
@@ -0,0 +1,135 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { OpenAI, ALL_AVAILABLE_OPENAI_MODELS } from 'llamaindex'
interface AzureOpenAIConfig {
apiKey?: string
endpoint?: string
apiVersion?: string
deploymentName?: string
}
class AzureChatOpenAI_LlamaIndex_ChatModels implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
tags: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'AzureChatOpenAI'
this.name = 'azureChatOpenAI_LlamaIndex'
this.version = 1.0
this.type = 'AzureChatOpenAI'
this.icon = 'Azure.svg'
this.category = 'Chat Models'
this.description = 'Wrapper around Azure OpenAI Chat LLM with LlamaIndex implementation'
this.baseClasses = [this.type, 'BaseChatModel_LlamaIndex', ...getBaseClasses(OpenAI)]
this.tags = ['LlamaIndex']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['azureOpenAIApi']
}
this.inputs = [
{
label: 'Model Name',
name: 'modelName',
type: 'options',
options: [
{
label: 'gpt-4',
name: 'gpt-4'
},
{
label: 'gpt-4-32k',
name: 'gpt-4-32k'
},
{
label: 'gpt-3.5-turbo',
name: 'gpt-3.5-turbo'
},
{
label: 'gpt-3.5-turbo-16k',
name: 'gpt-3.5-turbo-16k'
}
],
default: 'gpt-3.5-turbo-16k',
optional: true
},
{
label: 'Temperature',
name: 'temperature',
type: 'number',
step: 0.1,
default: 0.9,
optional: true
},
{
label: 'Max Tokens',
name: 'maxTokens',
type: 'number',
step: 1,
optional: true,
additionalParams: true
},
{
label: 'Top Probability',
name: 'topP',
type: 'number',
step: 0.1,
optional: true,
additionalParams: true
},
{
label: 'Timeout',
name: 'timeout',
type: 'number',
step: 1,
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const modelName = nodeData.inputs?.modelName as keyof typeof ALL_AVAILABLE_OPENAI_MODELS
const temperature = nodeData.inputs?.temperature as string
const maxTokens = nodeData.inputs?.maxTokens as string
const topP = nodeData.inputs?.topP as string
const timeout = nodeData.inputs?.timeout as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const azureOpenAIApiKey = getCredentialParam('azureOpenAIApiKey', credentialData, nodeData)
const azureOpenAIApiInstanceName = getCredentialParam('azureOpenAIApiInstanceName', credentialData, nodeData)
const azureOpenAIApiDeploymentName = getCredentialParam('azureOpenAIApiDeploymentName', credentialData, nodeData)
const azureOpenAIApiVersion = getCredentialParam('azureOpenAIApiVersion', credentialData, nodeData)
const obj: Partial<OpenAI> & { azure?: AzureOpenAIConfig } = {
temperature: parseFloat(temperature),
model: modelName,
azure: {
apiKey: azureOpenAIApiKey,
endpoint: `https://${azureOpenAIApiInstanceName}.openai.azure.com`,
apiVersion: azureOpenAIApiVersion,
deploymentName: azureOpenAIApiDeploymentName
}
}
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
if (topP) obj.topP = parseFloat(topP)
if (timeout) obj.timeout = parseInt(timeout, 10)
const model = new OpenAI(obj)
return model
}
}
module.exports = { nodeClass: AzureChatOpenAI_LlamaIndex_ChatModels }
@@ -3,6 +3,7 @@ import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../
import { AnthropicInput, ChatAnthropic } from 'langchain/chat_models/anthropic'
import { BaseCache } from 'langchain/schema'
import { BaseLLMParams } from 'langchain/llms/base'
import { availableModels } from './utils'
class ChatAnthropic_ChatModels implements INode {
label: string
@@ -42,67 +43,7 @@ class ChatAnthropic_ChatModels implements INode {
label: 'Model Name',
name: 'modelName',
type: 'options',
options: [
{
label: 'claude-2',
name: 'claude-2',
description: 'Claude 2 latest major version, automatically get updates to the model as they are released'
},
{
label: 'claude-2.1',
name: 'claude-2.1',
description: 'Claude 2 latest full version'
},
{
label: 'claude-instant-1',
name: 'claude-instant-1',
description: 'Claude Instant latest major version, automatically get updates to the model as they are released'
},
{
label: 'claude-v1',
name: 'claude-v1'
},
{
label: 'claude-v1-100k',
name: 'claude-v1-100k'
},
{
label: 'claude-v1.0',
name: 'claude-v1.0'
},
{
label: 'claude-v1.2',
name: 'claude-v1.2'
},
{
label: 'claude-v1.3',
name: 'claude-v1.3'
},
{
label: 'claude-v1.3-100k',
name: 'claude-v1.3-100k'
},
{
label: 'claude-instant-v1',
name: 'claude-instant-v1'
},
{
label: 'claude-instant-v1-100k',
name: 'claude-instant-v1-100k'
},
{
label: 'claude-instant-v1.0',
name: 'claude-instant-v1.0'
},
{
label: 'claude-instant-v1.1',
name: 'claude-instant-v1.1'
},
{
label: 'claude-instant-v1.1-100k',
name: 'claude-instant-v1.1-100k'
}
],
options: [...availableModels],
default: 'claude-2',
optional: true
},
@@ -0,0 +1,94 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { Anthropic } from 'llamaindex'
import { availableModels } from './utils'
class ChatAnthropic_LlamaIndex_ChatModels implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
tags: string[]
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'ChatAnthropic'
this.name = 'chatAnthropic_LlamaIndex'
this.version = 1.0
this.type = 'ChatAnthropic'
this.icon = 'chatAnthropic.png'
this.category = 'Chat Models'
this.description = 'Wrapper around ChatAnthropic LLM with LlamaIndex implementation'
this.baseClasses = [this.type, 'BaseChatModel_LlamaIndex', ...getBaseClasses(Anthropic)]
this.tags = ['LlamaIndex']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['anthropicApi']
}
this.inputs = [
{
label: 'Model Name',
name: 'modelName',
type: 'options',
options: [...availableModels],
default: 'claude-2',
optional: true
},
{
label: 'Temperature',
name: 'temperature',
type: 'number',
step: 0.1,
default: 0.9,
optional: true
},
{
label: 'Max Tokens',
name: 'maxTokensToSample',
type: 'number',
step: 1,
optional: true,
additionalParams: true
},
{
label: 'Top P',
name: 'topP',
type: 'number',
step: 0.1,
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const temperature = nodeData.inputs?.temperature as string
const modelName = nodeData.inputs?.modelName as string
const maxTokensToSample = nodeData.inputs?.maxTokensToSample as string
const topP = nodeData.inputs?.topP as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const anthropicApiKey = getCredentialParam('anthropicApiKey', credentialData, nodeData)
const obj: Partial<Anthropic> = {
temperature: parseFloat(temperature),
model: modelName,
apiKey: anthropicApiKey
}
if (maxTokensToSample) obj.maxTokens = parseInt(maxTokensToSample, 10)
if (topP) obj.topP = parseFloat(topP)
const model = new Anthropic(obj)
return model
}
}
module.exports = { nodeClass: ChatAnthropic_LlamaIndex_ChatModels }
@@ -0,0 +1,61 @@
export const availableModels = [
{
label: 'claude-2',
name: 'claude-2',
description: 'Claude 2 latest major version, automatically get updates to the model as they are released'
},
{
label: 'claude-2.1',
name: 'claude-2.1',
description: 'Claude 2 latest full version'
},
{
label: 'claude-instant-1',
name: 'claude-instant-1',
description: 'Claude Instant latest major version, automatically get updates to the model as they are released'
},
{
label: 'claude-v1',
name: 'claude-v1'
},
{
label: 'claude-v1-100k',
name: 'claude-v1-100k'
},
{
label: 'claude-v1.0',
name: 'claude-v1.0'
},
{
label: 'claude-v1.2',
name: 'claude-v1.2'
},
{
label: 'claude-v1.3',
name: 'claude-v1.3'
},
{
label: 'claude-v1.3-100k',
name: 'claude-v1.3-100k'
},
{
label: 'claude-instant-v1',
name: 'claude-instant-v1'
},
{
label: 'claude-instant-v1-100k',
name: 'claude-instant-v1-100k'
},
{
label: 'claude-instant-v1.0',
name: 'claude-instant-v1.0'
},
{
label: 'claude-instant-v1.1',
name: 'claude-instant-v1.1'
},
{
label: 'claude-instant-v1.1-100k',
name: 'claude-instant-v1.1-100k'
}
]
@@ -0,0 +1,148 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { OpenAI, ALL_AVAILABLE_OPENAI_MODELS } from 'llamaindex'
class ChatOpenAI_LlamaIndex_LLMs implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
tags: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'ChatOpenAI'
this.name = 'chatOpenAI_LlamaIndex'
this.version = 1.0
this.type = 'ChatOpenAI'
this.icon = 'openai.png'
this.category = 'Chat Models'
this.description = 'Wrapper around OpenAI Chat LLM with LlamaIndex implementation'
this.baseClasses = [this.type, 'BaseChatModel_LlamaIndex', ...getBaseClasses(OpenAI)]
this.tags = ['LlamaIndex']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}
this.inputs = [
{
label: 'Model Name',
name: 'modelName',
type: 'options',
options: [
{
label: 'gpt-4',
name: 'gpt-4'
},
{
label: 'gpt-4-1106-preview',
name: 'gpt-4-1106-preview'
},
{
label: 'gpt-4-vision-preview',
name: 'gpt-4-vision-preview'
},
{
label: 'gpt-4-0613',
name: 'gpt-4-0613'
},
{
label: 'gpt-4-32k',
name: 'gpt-4-32k'
},
{
label: 'gpt-4-32k-0613',
name: 'gpt-4-32k-0613'
},
{
label: 'gpt-3.5-turbo',
name: 'gpt-3.5-turbo'
},
{
label: 'gpt-3.5-turbo-1106',
name: 'gpt-3.5-turbo-1106'
},
{
label: 'gpt-3.5-turbo-0613',
name: 'gpt-3.5-turbo-0613'
},
{
label: 'gpt-3.5-turbo-16k',
name: 'gpt-3.5-turbo-16k'
},
{
label: 'gpt-3.5-turbo-16k-0613',
name: 'gpt-3.5-turbo-16k-0613'
}
],
default: 'gpt-3.5-turbo',
optional: true
},
{
label: 'Temperature',
name: 'temperature',
type: 'number',
step: 0.1,
default: 0.9,
optional: true
},
{
label: 'Max Tokens',
name: 'maxTokens',
type: 'number',
step: 1,
optional: true,
additionalParams: true
},
{
label: 'Top Probability',
name: 'topP',
type: 'number',
step: 0.1,
optional: true,
additionalParams: true
},
{
label: 'Timeout',
name: 'timeout',
type: 'number',
step: 1,
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const temperature = nodeData.inputs?.temperature as string
const modelName = nodeData.inputs?.modelName as keyof typeof ALL_AVAILABLE_OPENAI_MODELS
const maxTokens = nodeData.inputs?.maxTokens as string
const topP = nodeData.inputs?.topP as string
const timeout = nodeData.inputs?.timeout as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData)
const obj: Partial<OpenAI> = {
temperature: parseFloat(temperature),
model: modelName,
apiKey: openAIApiKey
}
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
if (topP) obj.topP = parseFloat(topP)
if (timeout) obj.timeout = parseInt(timeout, 10)
const model = new OpenAI(obj)
return model
}
}
module.exports = { nodeClass: ChatOpenAI_LlamaIndex_LLMs }
@@ -0,0 +1,77 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { OpenAIEmbedding } from 'llamaindex'
interface AzureOpenAIConfig {
apiKey?: string
endpoint?: string
apiVersion?: string
deploymentName?: string
}
class AzureOpenAIEmbedding_LlamaIndex_Embeddings implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
tags: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Azure OpenAI Embeddings'
this.name = 'azureOpenAIEmbeddingsLlamaIndex'
this.version = 1.0
this.type = 'AzureOpenAIEmbeddings'
this.icon = 'Azure.svg'
this.category = 'Embeddings'
this.description = 'Azure OpenAI API embeddings with LlamaIndex implementation'
this.baseClasses = [this.type, 'BaseEmbedding_LlamaIndex', ...getBaseClasses(OpenAIEmbedding)]
this.tags = ['LlamaIndex']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['azureOpenAIApi']
}
this.inputs = [
{
label: 'Timeout',
name: 'timeout',
type: 'number',
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const timeout = nodeData.inputs?.timeout as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const azureOpenAIApiKey = getCredentialParam('azureOpenAIApiKey', credentialData, nodeData)
const azureOpenAIApiInstanceName = getCredentialParam('azureOpenAIApiInstanceName', credentialData, nodeData)
const azureOpenAIApiDeploymentName = getCredentialParam('azureOpenAIApiDeploymentName', credentialData, nodeData)
const azureOpenAIApiVersion = getCredentialParam('azureOpenAIApiVersion', credentialData, nodeData)
const obj: Partial<OpenAIEmbedding> & { azure?: AzureOpenAIConfig } = {
azure: {
apiKey: azureOpenAIApiKey,
endpoint: `https://${azureOpenAIApiInstanceName}.openai.azure.com`,
apiVersion: azureOpenAIApiVersion,
deploymentName: azureOpenAIApiDeploymentName
}
}
if (timeout) obj.timeout = parseInt(timeout, 10)
const model = new OpenAIEmbedding(obj)
return model
}
}
module.exports = { nodeClass: AzureOpenAIEmbedding_LlamaIndex_Embeddings }
@@ -0,0 +1,68 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { OpenAIEmbedding } from 'llamaindex'
class OpenAIEmbedding_LlamaIndex_Embeddings implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
tags: string[]
credential: INodeParams
inputs: INodeParams[]
constructor() {
this.label = 'OpenAI Embedding'
this.name = 'openAIEmbedding_LlamaIndex'
this.version = 1.0
this.type = 'OpenAIEmbedding'
this.icon = 'openai.png'
this.category = 'Embeddings'
this.description = 'OpenAI Embedding with LlamaIndex implementation'
this.baseClasses = [this.type, 'BaseEmbedding_LlamaIndex', ...getBaseClasses(OpenAIEmbedding)]
this.tags = ['LlamaIndex']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}
this.inputs = [
{
label: 'Timeout',
name: 'timeout',
type: 'number',
optional: true,
additionalParams: true
},
{
label: 'BasePath',
name: 'basepath',
type: 'string',
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const timeout = nodeData.inputs?.timeout as string
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const openAIApiKey = getCredentialParam('openAIApiKey', credentialData, nodeData)
const obj: Partial<OpenAIEmbedding> = {
apiKey: openAIApiKey
}
if (timeout) obj.timeout = parseInt(timeout, 10)
const model = new OpenAIEmbedding(obj)
return model
}
}
module.exports = { nodeClass: OpenAIEmbedding_LlamaIndex_Embeddings }
@@ -0,0 +1,178 @@
import { ICommonObject, IMessage, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { ContextChatEngine, ChatMessage } from 'llamaindex'
class ContextChatEngine_LlamaIndex implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
tags: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Context Chat Engine'
this.name = 'contextChatEngine'
this.version = 1.0
this.type = 'ContextChatEngine'
this.icon = 'context-chat-engine.png'
this.category = 'Engine'
this.description = 'Answer question based on retrieved documents (context) with built-in memory to remember conversation'
this.baseClasses = [this.type]
this.tags = ['LlamaIndex']
this.inputs = [
{
label: 'Chat Model',
name: 'model',
type: 'BaseChatModel_LlamaIndex'
},
{
label: 'Vector Store Retriever',
name: 'vectorStoreRetriever',
type: 'VectorIndexRetriever'
},
{
label: 'Memory',
name: 'memory',
type: 'BaseChatMemory'
},
{
label: 'System Message',
name: 'systemMessagePrompt',
type: 'string',
rows: 4,
optional: true,
placeholder:
'I want you to act as a document that I am having a conversation with. Your name is "AI Assistant". You will provide me with answers from the given info. If the answer is not included, say exactly "Hmm, I am not sure." and stop after that. Refuse to answer any question not about the info. Never break character.'
}
]
}
async init(nodeData: INodeData): Promise<any> {
const model = nodeData.inputs?.model
const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever
const memory = nodeData.inputs?.memory
const chatEngine = new ContextChatEngine({ chatModel: model, retriever: vectorStoreRetriever })
;(chatEngine as any).memory = memory
return chatEngine
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string | object> {
const chatEngine = nodeData.instance as ContextChatEngine
const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string
const memory = nodeData.inputs?.memory
const chatHistory = [] as ChatMessage[]
let sessionId = ''
if (memory) {
if (memory.isSessionIdUsingChatMessageId) sessionId = options.chatId
else sessionId = nodeData.inputs?.sessionId
}
if (systemMessagePrompt) {
chatHistory.push({
content: systemMessagePrompt,
role: 'user'
})
}
/* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory
* LongTermMemory will automatically retrieved chatHistory from sessionId
*/
if (options && options.chatHistory && memory.isShortTermMemory) {
await memory.resumeMessages(options.chatHistory)
}
const msgs: IMessage[] = await memory.getChatMessages(sessionId)
for (const message of msgs) {
if (message.type === 'apiMessage') {
chatHistory.push({
content: message.message,
role: 'assistant'
})
} else if (message.type === 'userMessage') {
chatHistory.push({
content: message.message,
role: 'user'
})
}
}
if (options.socketIO && options.socketIOClientId) {
let response = ''
const stream = await chatEngine.chat(input, chatHistory, true)
let isStart = true
const onNextPromise = () => {
return new Promise((resolve, reject) => {
const onNext = async () => {
try {
const { value, done } = await stream.next()
if (!done) {
if (isStart) {
options.socketIO.to(options.socketIOClientId).emit('start')
isStart = false
}
options.socketIO.to(options.socketIOClientId).emit('token', value)
response += value
onNext()
} else {
resolve(response)
}
} catch (error) {
reject(error)
}
}
onNext()
})
}
try {
const result = await onNextPromise()
if (memory) {
await memory.addChatMessages(
[
{
text: input,
type: 'userMessage'
},
{
text: result,
type: 'apiMessage'
}
],
sessionId
)
}
return result as string
} catch (error) {
throw new Error(error)
}
} else {
const response = await chatEngine.chat(input, chatHistory)
if (memory) {
await memory.addChatMessages(
[
{
text: input,
type: 'userMessage'
},
{
text: response?.response,
type: 'apiMessage'
}
],
sessionId
)
}
return response?.response
}
}
}
module.exports = { nodeClass: ContextChatEngine_LlamaIndex }
@@ -0,0 +1,171 @@
import { ICommonObject, IMessage, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { ChatMessage, SimpleChatEngine } from 'llamaindex'
class SimpleChatEngine_LlamaIndex implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
tags: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Simple Chat Engine'
this.name = 'simpleChatEngine'
this.version = 1.0
this.type = 'SimpleChatEngine'
this.icon = 'chat-engine.png'
this.category = 'Engine'
this.description = 'Simple engine to handle back and forth conversations'
this.baseClasses = [this.type]
this.tags = ['LlamaIndex']
this.inputs = [
{
label: 'Chat Model',
name: 'model',
type: 'BaseChatModel_LlamaIndex'
},
{
label: 'Memory',
name: 'memory',
type: 'BaseChatMemory'
},
{
label: 'System Message',
name: 'systemMessagePrompt',
type: 'string',
rows: 4,
optional: true,
placeholder: 'You are a helpful assistant'
}
]
}
async init(nodeData: INodeData): Promise<any> {
const model = nodeData.inputs?.model
const memory = nodeData.inputs?.memory
const chatEngine = new SimpleChatEngine({ llm: model })
;(chatEngine as any).memory = memory
return chatEngine
}
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string> {
const chatEngine = nodeData.instance as SimpleChatEngine
const systemMessagePrompt = nodeData.inputs?.systemMessagePrompt as string
const memory = nodeData.inputs?.memory
const chatHistory = [] as ChatMessage[]
let sessionId = ''
if (memory) {
if (memory.isSessionIdUsingChatMessageId) sessionId = options.chatId
else sessionId = nodeData.inputs?.sessionId
}
if (systemMessagePrompt) {
chatHistory.push({
content: systemMessagePrompt,
role: 'user'
})
}
/* When incomingInput.history is provided, only force replace chatHistory if its ShortTermMemory
* LongTermMemory will automatically retrieved chatHistory from sessionId
*/
if (options && options.chatHistory && memory.isShortTermMemory) {
await memory.resumeMessages(options.chatHistory)
}
const msgs: IMessage[] = await memory.getChatMessages(sessionId)
for (const message of msgs) {
if (message.type === 'apiMessage') {
chatHistory.push({
content: message.message,
role: 'assistant'
})
} else if (message.type === 'userMessage') {
chatHistory.push({
content: message.message,
role: 'user'
})
}
}
if (options.socketIO && options.socketIOClientId) {
let response = ''
const stream = await chatEngine.chat(input, chatHistory, true)
let isStart = true
const onNextPromise = () => {
return new Promise((resolve, reject) => {
const onNext = async () => {
try {
const { value, done } = await stream.next()
if (!done) {
if (isStart) {
options.socketIO.to(options.socketIOClientId).emit('start')
isStart = false
}
options.socketIO.to(options.socketIOClientId).emit('token', value)
response += value
onNext()
} else {
resolve(response)
}
} catch (error) {
reject(error)
}
}
onNext()
})
}
try {
const result = await onNextPromise()
if (memory) {
await memory.addChatMessages(
[
{
text: input,
type: 'userMessage'
},
{
text: result,
type: 'apiMessage'
}
],
sessionId
)
}
return result as string
} catch (error) {
throw new Error(error)
}
} else {
const response = await chatEngine.chat(input, chatHistory)
if (memory) {
await memory.addChatMessages(
[
{
text: input,
type: 'userMessage'
},
{
text: response?.response,
type: 'apiMessage'
}
],
sessionId
)
}
return response?.response
}
}
}
module.exports = { nodeClass: SimpleChatEngine_LlamaIndex }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

@@ -0,0 +1,126 @@
import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import {
RetrieverQueryEngine,
BaseNode,
Metadata,
ResponseSynthesizer,
CompactAndRefine,
TreeSummarize,
Refine,
SimpleResponseBuilder
} from 'llamaindex'
class QueryEngine_LlamaIndex implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
tags: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Query Engine'
this.name = 'queryEngine'
this.version = 1.0
this.type = 'QueryEngine'
this.icon = 'query-engine.png'
this.category = 'Engine'
this.description = 'Simple query engine built to answer question over your data, without memory'
this.baseClasses = [this.type]
this.tags = ['LlamaIndex']
this.inputs = [
{
label: 'Vector Store Retriever',
name: 'vectorStoreRetriever',
type: 'VectorIndexRetriever'
},
{
label: 'Response Synthesizer',
name: 'responseSynthesizer',
type: 'ResponseSynthesizer',
description:
'ResponseSynthesizer is responsible for sending the query, nodes, and prompt templates to the LLM to generate a response. See <a target="_blank" href="https://ts.llamaindex.ai/modules/low_level/response_synthesizer">more</a>',
optional: true
},
{
label: 'Return Source Documents',
name: 'returnSourceDocuments',
type: 'boolean',
optional: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const vectorStoreRetriever = nodeData.inputs?.vectorStoreRetriever
const responseSynthesizerObj = nodeData.inputs?.responseSynthesizer
if (responseSynthesizerObj) {
if (responseSynthesizerObj.type === 'TreeSummarize') {
const responseSynthesizer = new ResponseSynthesizer({
responseBuilder: new TreeSummarize(vectorStoreRetriever.serviceContext, responseSynthesizerObj.textQAPromptTemplate),
serviceContext: vectorStoreRetriever.serviceContext
})
return new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer)
} else if (responseSynthesizerObj.type === 'CompactAndRefine') {
const responseSynthesizer = new ResponseSynthesizer({
responseBuilder: new CompactAndRefine(
vectorStoreRetriever.serviceContext,
responseSynthesizerObj.textQAPromptTemplate,
responseSynthesizerObj.refinePromptTemplate
),
serviceContext: vectorStoreRetriever.serviceContext
})
return new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer)
} else if (responseSynthesizerObj.type === 'Refine') {
const responseSynthesizer = new ResponseSynthesizer({
responseBuilder: new Refine(
vectorStoreRetriever.serviceContext,
responseSynthesizerObj.textQAPromptTemplate,
responseSynthesizerObj.refinePromptTemplate
),
serviceContext: vectorStoreRetriever.serviceContext
})
return new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer)
} else if (responseSynthesizerObj.type === 'SimpleResponseBuilder') {
const responseSynthesizer = new ResponseSynthesizer({
responseBuilder: new SimpleResponseBuilder(vectorStoreRetriever.serviceContext),
serviceContext: vectorStoreRetriever.serviceContext
})
return new RetrieverQueryEngine(vectorStoreRetriever, responseSynthesizer)
}
}
const queryEngine = new RetrieverQueryEngine(vectorStoreRetriever)
return queryEngine
}
async run(nodeData: INodeData, input: string): Promise<string | object> {
const queryEngine = nodeData.instance as RetrieverQueryEngine
const returnSourceDocuments = nodeData.inputs?.returnSourceDocuments as boolean
const response = await queryEngine.query(input)
if (returnSourceDocuments && response.sourceNodes?.length)
return { text: response?.response, sourceDocuments: reformatSourceDocuments(response.sourceNodes) }
return response?.response
}
}
const reformatSourceDocuments = (sourceNodes: BaseNode<Metadata>[]) => {
const sourceDocuments = []
for (const node of sourceNodes) {
sourceDocuments.push({
pageContent: (node as any).text,
metadata: node.metadata
})
}
return sourceDocuments
}
module.exports = { nodeClass: QueryEngine_LlamaIndex }
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -1,6 +1,6 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { BufferMemory } from 'langchain/memory'
import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface'
import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils'
import { BufferMemory, BufferMemoryInput } from 'langchain/memory'
class BufferMemory_Memory implements INode {
label: string
@@ -41,7 +41,7 @@ class BufferMemory_Memory implements INode {
async init(nodeData: INodeData): Promise<any> {
const memoryKey = nodeData.inputs?.memoryKey as string
const inputKey = nodeData.inputs?.inputKey as string
return new BufferMemory({
return new BufferMemoryExtended({
returnMessages: true,
memoryKey,
inputKey
@@ -49,4 +49,43 @@ class BufferMemory_Memory implements INode {
}
}
class BufferMemoryExtended extends BufferMemory {
isShortTermMemory = true
constructor(fields: BufferMemoryInput) {
super(fields)
}
async getChatMessages(): Promise<IMessage[]> {
const memoryResult = await this.loadMemoryVariables({})
const baseMessages = memoryResult[this.memoryKey ?? 'chat_history']
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise<void> {
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
const inputValues = { [this.inputKey ?? 'input']: input?.text }
const outputValues = { output: output?.text }
await this.saveContext(inputValues, outputValues)
}
async clearChatMessages(): Promise<void> {
await this.clear()
}
async resumeMessages(messages: IMessage[]): Promise<void> {
// Clear existing chatHistory to avoid duplication
if (messages.length) await this.clear()
// Insert into chatHistory
for (const msg of messages) {
if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message)
else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message)
}
}
}
module.exports = { nodeClass: BufferMemory_Memory }
@@ -1,5 +1,5 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface'
import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils'
import { BufferWindowMemory, BufferWindowMemoryInput } from 'langchain/memory'
class BufferWindowMemory_Memory implements INode {
@@ -57,7 +57,46 @@ class BufferWindowMemory_Memory implements INode {
k: parseInt(k, 10)
}
return new BufferWindowMemory(obj)
return new BufferWindowMemoryExtended(obj)
}
}
class BufferWindowMemoryExtended extends BufferWindowMemory {
isShortTermMemory = true
constructor(fields: BufferWindowMemoryInput) {
super(fields)
}
async getChatMessages(): Promise<IMessage[]> {
const memoryResult = await this.loadMemoryVariables({})
const baseMessages = memoryResult[this.memoryKey ?? 'chat_history']
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise<void> {
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
const inputValues = { [this.inputKey ?? 'input']: input?.text }
const outputValues = { output: output?.text }
await this.saveContext(inputValues, outputValues)
}
async clearChatMessages(): Promise<void> {
await this.clear()
}
async resumeMessages(messages: IMessage[]): Promise<void> {
// Clear existing chatHistory to avoid duplication
if (messages.length) await this.clear()
// Insert into chatHistory
for (const msg of messages) {
if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message)
else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message)
}
}
}
@@ -1,5 +1,5 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface'
import { convertBaseMessagetoIMessage, getBaseClasses } from '../../../src/utils'
import { ConversationSummaryMemory, ConversationSummaryMemoryInput } from 'langchain/memory'
import { BaseLanguageModel } from 'langchain/base_language'
@@ -56,7 +56,50 @@ class ConversationSummaryMemory_Memory implements INode {
inputKey
}
return new ConversationSummaryMemory(obj)
return new ConversationSummaryMemoryExtended(obj)
}
}
class ConversationSummaryMemoryExtended extends ConversationSummaryMemory {
isShortTermMemory = true
constructor(fields: ConversationSummaryMemoryInput) {
super(fields)
}
async getChatMessages(): Promise<IMessage[]> {
const memoryResult = await this.loadMemoryVariables({})
const baseMessages = memoryResult[this.memoryKey ?? 'chat_history']
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[]): Promise<void> {
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
const inputValues = { [this.inputKey ?? 'input']: input?.text }
const outputValues = { output: output?.text }
await this.saveContext(inputValues, outputValues)
}
async clearChatMessages(): Promise<void> {
await this.clear()
}
async resumeMessages(messages: IMessage[]): Promise<void> {
// Clear existing chatHistory to avoid duplication
if (messages.length) await this.clear()
// Insert into chatHistory
for (const msg of messages) {
if (msg.type === 'userMessage') await this.chatHistory.addUserMessage(msg.message)
else if (msg.type === 'apiMessage') await this.chatHistory.addAIChatMessage(msg.message)
}
// Replace buffer
const chatMessages = await this.chatHistory.getMessages()
this.buffer = await this.predictNewSummary(chatMessages.slice(-2), this.buffer)
}
}
@@ -1,15 +1,19 @@
import {
ICommonObject,
INode,
INodeData,
INodeParams,
getBaseClasses,
getCredentialData,
getCredentialParam,
serializeChatHistory
} from '../../../src'
DynamoDBClient,
DynamoDBClientConfig,
GetItemCommand,
GetItemCommandInput,
UpdateItemCommand,
UpdateItemCommandInput,
DeleteItemCommand,
DeleteItemCommandInput,
AttributeValue
} from '@aws-sdk/client-dynamodb'
import { DynamoDBChatMessageHistory } from 'langchain/stores/message/dynamodb'
import { BufferMemory, BufferMemoryInput } from 'langchain/memory'
import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage } from 'langchain/schema'
import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ICommonObject, IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface'
class DynamoDb_Memory implements INode {
label: string
@@ -60,7 +64,8 @@ class DynamoDb_Memory implements INode {
label: 'Session ID',
name: 'sessionId',
type: 'string',
description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId',
description:
'If not specified, a random id will be used. Learn <a target="_blank" href="https://docs.flowiseai.com/memory/long-term-memory#ui-and-embedded-chat">more</a>',
default: '',
additionalParams: true,
optional: true
@@ -78,73 +83,205 @@ class DynamoDb_Memory implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
return initalizeDynamoDB(nodeData, options)
}
//@ts-ignore
memoryMethods = {
async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise<void> {
const dynamodbMemory = await initalizeDynamoDB(nodeData, options)
const sessionId = nodeData.inputs?.sessionId as string
const chatId = options?.chatId as string
options.logger.info(`Clearing DynamoDb memory session ${sessionId ? sessionId : chatId}`)
await dynamodbMemory.clear()
options.logger.info(`Successfully cleared DynamoDb memory session ${sessionId ? sessionId : chatId}`)
},
async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise<string> {
const memoryKey = nodeData.inputs?.memoryKey as string
const dynamodbMemory = await initalizeDynamoDB(nodeData, options)
const key = memoryKey ?? 'chat_history'
const memoryResult = await dynamodbMemory.loadMemoryVariables({})
return serializeChatHistory(memoryResult[key])
}
}
}
const initalizeDynamoDB = async (nodeData: INodeData, options: ICommonObject): Promise<BufferMemory> => {
const tableName = nodeData.inputs?.tableName as string
const partitionKey = nodeData.inputs?.partitionKey as string
const sessionId = nodeData.inputs?.sessionId as string
const region = nodeData.inputs?.region as string
const memoryKey = nodeData.inputs?.memoryKey as string
const chatId = options.chatId
let isSessionIdUsingChatMessageId = false
if (!sessionId && chatId) isSessionIdUsingChatMessageId = true
let sessionId = ''
if (!nodeData.inputs?.sessionId && chatId) {
isSessionIdUsingChatMessageId = true
sessionId = chatId
} else {
sessionId = nodeData.inputs?.sessionId
}
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const accessKeyId = getCredentialParam('accessKey', credentialData, nodeData)
const secretAccessKey = getCredentialParam('secretAccessKey', credentialData, nodeData)
const config: DynamoDBClientConfig = {
region,
credentials: {
accessKeyId,
secretAccessKey
}
}
const client = new DynamoDBClient(config ?? {})
const dynamoDb = new DynamoDBChatMessageHistory({
tableName,
partitionKey,
sessionId: sessionId ? sessionId : chatId,
config: {
region,
credentials: {
accessKeyId,
secretAccessKey
}
}
config
})
const memory = new BufferMemoryExtended({
memoryKey: memoryKey ?? 'chat_history',
chatHistory: dynamoDb,
isSessionIdUsingChatMessageId
isSessionIdUsingChatMessageId,
sessionId,
dynamodbClient: client
})
return memory
}
interface BufferMemoryExtendedInput {
isSessionIdUsingChatMessageId: boolean
dynamodbClient: DynamoDBClient
sessionId: string
}
interface DynamoDBSerializedChatMessage {
M: {
type: {
S: string
}
text: {
S: string
}
role?: {
S: string
}
}
}
class BufferMemoryExtended extends BufferMemory {
isSessionIdUsingChatMessageId? = false
isSessionIdUsingChatMessageId = false
sessionId = ''
dynamodbClient: DynamoDBClient
constructor(fields: BufferMemoryInput & Partial<BufferMemoryExtendedInput>) {
constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) {
super(fields)
this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId
this.sessionId = fields.sessionId
this.dynamodbClient = fields.dynamodbClient
}
overrideDynamoKey(overrideSessionId = '') {
const existingDynamoKey = (this as any).dynamoKey
const partitionKey = (this as any).partitionKey
let newDynamoKey: Record<string, AttributeValue> = {}
if (Object.keys(existingDynamoKey).includes(partitionKey)) {
newDynamoKey[partitionKey] = { S: overrideSessionId }
}
return Object.keys(newDynamoKey).length ? newDynamoKey : existingDynamoKey
}
async addNewMessage(
messages: StoredMessage[],
client: DynamoDBClient,
tableName = '',
dynamoKey: Record<string, AttributeValue> = {},
messageAttributeName = 'messages'
) {
const params: UpdateItemCommandInput = {
TableName: tableName,
Key: dynamoKey,
ExpressionAttributeNames: {
'#m': messageAttributeName
},
ExpressionAttributeValues: {
':empty_list': {
L: []
},
':m': {
L: messages.map((message) => {
const dynamoSerializedMessage: DynamoDBSerializedChatMessage = {
M: {
type: {
S: message.type
},
text: {
S: message.data.content
}
}
}
if (message.data.role) {
dynamoSerializedMessage.M.role = { S: message.data.role }
}
return dynamoSerializedMessage
})
}
},
UpdateExpression: 'SET #m = list_append(if_not_exists(#m, :empty_list), :m)'
}
await client.send(new UpdateItemCommand(params))
}
async getChatMessages(overrideSessionId = ''): Promise<IMessage[]> {
if (!this.dynamodbClient) return []
const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey
const tableName = (this as any).tableName
const messageAttributeName = (this as any).messageAttributeName
const params: GetItemCommandInput = {
TableName: tableName,
Key: dynamoKey
}
const response = await this.dynamodbClient.send(new GetItemCommand(params))
const items = response.Item ? response.Item[messageAttributeName]?.L ?? [] : []
const messages = items
.map((item) => ({
type: item.M?.type.S,
data: {
role: item.M?.role?.S,
content: item.M?.text.S
}
}))
.filter((x): x is StoredMessage => x.type !== undefined && x.data.content !== undefined)
const baseMessages = messages.map(mapStoredMessageToChatMessage)
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise<void> {
if (!this.dynamodbClient) return
const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey
const tableName = (this as any).tableName
const messageAttributeName = (this as any).messageAttributeName
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
if (input) {
const newInputMessage = new HumanMessage(input.text)
const messageToAdd = [newInputMessage].map((msg) => msg.toDict())
await this.addNewMessage(messageToAdd, this.dynamodbClient, tableName, dynamoKey, messageAttributeName)
}
if (output) {
const newOutputMessage = new AIMessage(output.text)
const messageToAdd = [newOutputMessage].map((msg) => msg.toDict())
await this.addNewMessage(messageToAdd, this.dynamodbClient, tableName, dynamoKey, messageAttributeName)
}
}
async clearChatMessages(overrideSessionId = ''): Promise<void> {
if (!this.dynamodbClient) return
const dynamoKey = overrideSessionId ? this.overrideDynamoKey(overrideSessionId) : (this as any).dynamoKey
const tableName = (this as any).tableName
const params: DeleteItemCommandInput = {
TableName: tableName,
Key: dynamoKey
}
await this.dynamodbClient.send(new DeleteItemCommand(params))
await this.clear()
}
}
@@ -1,17 +1,9 @@
import {
getBaseClasses,
getCredentialData,
getCredentialParam,
ICommonObject,
INode,
INodeData,
INodeParams,
serializeChatHistory
} from '../../../src'
import { MongoClient, Collection, Document } from 'mongodb'
import { MongoDBChatMessageHistory } from 'langchain/stores/message/mongodb'
import { BufferMemory, BufferMemoryInput } from 'langchain/memory'
import { BaseMessage, mapStoredMessageToChatMessage } from 'langchain/schema'
import { MongoClient } from 'mongodb'
import { BaseMessage, mapStoredMessageToChatMessage, AIMessage, HumanMessage } from 'langchain/schema'
import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ICommonObject, IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface'
class MongoDB_Memory implements INode {
label: string
@@ -57,7 +49,8 @@ class MongoDB_Memory implements INode {
label: 'Session Id',
name: 'sessionId',
type: 'string',
description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId',
description:
'If not specified, a random id will be used. Learn <a target="_blank" href="https://docs.flowiseai.com/memory/long-term-memory#ui-and-embedded-chat">more</a>',
default: '',
additionalParams: true,
optional: true
@@ -75,44 +68,33 @@ class MongoDB_Memory implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
return initializeMongoDB(nodeData, options)
}
//@ts-ignore
memoryMethods = {
async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise<void> {
const mongodbMemory = await initializeMongoDB(nodeData, options)
const sessionId = nodeData.inputs?.sessionId as string
const chatId = options?.chatId as string
options.logger.info(`Clearing MongoDB memory session ${sessionId ? sessionId : chatId}`)
await mongodbMemory.clear()
options.logger.info(`Successfully cleared MongoDB memory session ${sessionId ? sessionId : chatId}`)
},
async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise<string> {
const memoryKey = nodeData.inputs?.memoryKey as string
const mongodbMemory = await initializeMongoDB(nodeData, options)
const key = memoryKey ?? 'chat_history'
const memoryResult = await mongodbMemory.loadMemoryVariables({})
return serializeChatHistory(memoryResult[key])
}
}
}
const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): Promise<BufferMemory> => {
const databaseName = nodeData.inputs?.databaseName as string
const collectionName = nodeData.inputs?.collectionName as string
const sessionId = nodeData.inputs?.sessionId as string
const memoryKey = nodeData.inputs?.memoryKey as string
const chatId = options?.chatId as string
let isSessionIdUsingChatMessageId = false
if (!sessionId && chatId) isSessionIdUsingChatMessageId = true
let sessionId = ''
if (!nodeData.inputs?.sessionId && chatId) {
isSessionIdUsingChatMessageId = true
sessionId = chatId
} else {
sessionId = nodeData.inputs?.sessionId
}
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
let mongoDBConnectUrl = getCredentialParam('mongoDBConnectUrl', credentialData, nodeData)
const client = new MongoClient(mongoDBConnectUrl)
await client.connect()
const collection = client.db(databaseName).collection(collectionName)
/**** Methods below are needed to override the original implementations ****/
const mongoDBChatMessageHistory = new MongoDBChatMessageHistory({
collection,
sessionId: sessionId ? sessionId : chatId
@@ -140,24 +122,83 @@ const initializeMongoDB = async (nodeData: INodeData, options: ICommonObject): P
mongoDBChatMessageHistory.clear = async (): Promise<void> => {
await collection.deleteOne({ sessionId: (mongoDBChatMessageHistory as any).sessionId })
}
/**** End of override functions ****/
return new BufferMemoryExtended({
memoryKey: memoryKey ?? 'chat_history',
chatHistory: mongoDBChatMessageHistory,
isSessionIdUsingChatMessageId
isSessionIdUsingChatMessageId,
sessionId,
collection
})
}
interface BufferMemoryExtendedInput {
isSessionIdUsingChatMessageId: boolean
collection: Collection<Document>
sessionId: string
}
class BufferMemoryExtended extends BufferMemory {
isSessionIdUsingChatMessageId? = false
isSessionIdUsingChatMessageId = false
sessionId = ''
collection: Collection<Document>
constructor(fields: BufferMemoryInput & Partial<BufferMemoryExtendedInput>) {
constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) {
super(fields)
this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId
this.sessionId = fields.sessionId
this.collection = fields.collection
}
async getChatMessages(overrideSessionId = ''): Promise<IMessage[]> {
if (!this.collection) return []
const id = overrideSessionId ?? this.sessionId
const document = await this.collection.findOne({ sessionId: id })
const messages = document?.messages || []
const baseMessages = messages.map(mapStoredMessageToChatMessage)
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise<void> {
if (!this.collection) return
const id = overrideSessionId ?? this.sessionId
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
if (input) {
const newInputMessage = new HumanMessage(input.text)
const messageToAdd = [newInputMessage].map((msg) => msg.toDict())
await this.collection.updateOne(
{ sessionId: id },
{
$push: { messages: { $each: messageToAdd } }
},
{ upsert: true }
)
}
if (output) {
const newOutputMessage = new AIMessage(output.text)
const messageToAdd = [newOutputMessage].map((msg) => msg.toDict())
await this.collection.updateOne(
{ sessionId: id },
{
$push: { messages: { $each: messageToAdd } }
},
{ upsert: true }
)
}
}
async clearChatMessages(overrideSessionId = ''): Promise<void> {
if (!this.collection) return
const id = overrideSessionId ?? this.sessionId
await this.collection.deleteOne({ sessionId: id })
await this.clear()
}
}
@@ -1,9 +1,9 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface'
import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ICommonObject } from '../../../src'
import { MotorheadMemory, MotorheadMemoryInput } from 'langchain/memory'
import fetch from 'node-fetch'
import { getBufferString } from 'langchain/memory'
import { InputValues, MemoryVariables, OutputValues } from 'langchain/memory'
class MotorMemory_Memory implements INode {
label: string
@@ -46,7 +46,8 @@ class MotorMemory_Memory implements INode {
label: 'Session Id',
name: 'sessionId',
type: 'string',
description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId',
description:
'If not specified, a random id will be used. Learn <a target="_blank" href="https://docs.flowiseai.com/memory/long-term-memory#ui-and-embedded-chat">more</a>',
default: '',
additionalParams: true,
optional: true
@@ -64,35 +65,22 @@ class MotorMemory_Memory implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
return initalizeMotorhead(nodeData, options)
}
//@ts-ignore
memoryMethods = {
async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise<void> {
const motorhead = await initalizeMotorhead(nodeData, options)
const sessionId = nodeData.inputs?.sessionId as string
const chatId = options?.chatId as string
options.logger.info(`Clearing Motorhead memory session ${sessionId ? sessionId : chatId}`)
await motorhead.clear()
options.logger.info(`Successfully cleared Motorhead memory session ${sessionId ? sessionId : chatId}`)
},
async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise<string> {
const memoryKey = nodeData.inputs?.memoryKey as string
const motorhead = await initalizeMotorhead(nodeData, options)
const key = memoryKey ?? 'chat_history'
const memoryResult = await motorhead.loadMemoryVariables({})
return getBufferString(memoryResult[key])
}
}
}
const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject): Promise<MotorheadMemory> => {
const memoryKey = nodeData.inputs?.memoryKey as string
const baseURL = nodeData.inputs?.baseURL as string
const sessionId = nodeData.inputs?.sessionId as string
const chatId = options?.chatId as string
let isSessionIdUsingChatMessageId = false
if (!sessionId && chatId) isSessionIdUsingChatMessageId = true
let sessionId = ''
if (!nodeData.inputs?.sessionId && chatId) {
isSessionIdUsingChatMessageId = true
sessionId = chatId
} else {
sessionId = nodeData.inputs?.sessionId
}
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const apiKey = getCredentialParam('apiKey', credentialData, nodeData)
@@ -100,8 +88,9 @@ const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject):
let obj: MotorheadMemoryInput & Partial<MotorheadMemoryExtendedInput> = {
returnMessages: true,
sessionId: sessionId ? sessionId : chatId,
memoryKey
sessionId,
memoryKey,
isSessionIdUsingChatMessageId
}
if (baseURL) {
@@ -117,8 +106,6 @@ const initalizeMotorhead = async (nodeData: INodeData, options: ICommonObject):
}
}
if (isSessionIdUsingChatMessageId) obj.isSessionIdUsingChatMessageId = true
const motorheadMemory = new MotorheadMemoryExtended(obj)
// Get messages from sessionId
@@ -139,7 +126,24 @@ class MotorheadMemoryExtended extends MotorheadMemory {
this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId
}
async clear(): Promise<void> {
async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise<MemoryVariables> {
if (overrideSessionId) {
super.sessionId = overrideSessionId
}
return super.loadMemoryVariables({ values })
}
async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise<void> {
if (overrideSessionId) {
super.sessionId = overrideSessionId
}
return super.saveContext(inputValues, outputValues)
}
async clear(overrideSessionId = ''): Promise<void> {
if (overrideSessionId) {
super.sessionId = overrideSessionId
}
try {
await this.caller.call(fetch, `${this.url}/sessions/${this.sessionId}/memory`, {
//@ts-ignore
@@ -155,6 +159,28 @@ class MotorheadMemoryExtended extends MotorheadMemory {
await this.chatHistory.clear()
await super.clear()
}
async getChatMessages(overrideSessionId = ''): Promise<IMessage[]> {
const id = overrideSessionId ?? this.sessionId
const memoryVariables = await this.loadMemoryVariables({}, id)
const baseMessages = memoryVariables[this.memoryKey]
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise<void> {
const id = overrideSessionId ?? this.sessionId
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
const inputValues = { [this.inputKey ?? 'input']: input?.text }
const outputValues = { output: output?.text }
await this.saveContext(inputValues, outputValues, id)
}
async clearChatMessages(overrideSessionId = ''): Promise<void> {
const id = overrideSessionId ?? this.sessionId
await this.clear(id)
}
}
module.exports = { nodeClass: MotorMemory_Memory }
@@ -1,9 +1,9 @@
import { INode, INodeData, INodeParams, ICommonObject } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, serializeChatHistory } from '../../../src/utils'
import { Redis } from 'ioredis'
import { BufferMemory, BufferMemoryInput } from 'langchain/memory'
import { RedisChatMessageHistory, RedisChatMessageHistoryInput } from 'langchain/stores/message/ioredis'
import { mapStoredMessageToChatMessage, BaseMessage } from 'langchain/schema'
import { Redis } from 'ioredis'
import { mapStoredMessageToChatMessage, BaseMessage, AIMessage, HumanMessage } from 'langchain/schema'
import { INode, INodeData, INodeParams, ICommonObject, MessageType, IMessage } from '../../../src/Interface'
import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
class RedisBackedChatMemory_Memory implements INode {
label: string
@@ -38,7 +38,8 @@ class RedisBackedChatMemory_Memory implements INode {
label: 'Session Id',
name: 'sessionId',
type: 'string',
description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId',
description:
'If not specified, a random id will be used. Learn <a target="_blank" href="https://docs.flowiseai.com/memory/long-term-memory#ui-and-embedded-chat">more</a>',
default: '',
additionalParams: true,
optional: true
@@ -64,40 +65,28 @@ class RedisBackedChatMemory_Memory implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
return await initalizeRedis(nodeData, options)
}
//@ts-ignore
memoryMethods = {
async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise<void> {
const redis = await initalizeRedis(nodeData, options)
const sessionId = nodeData.inputs?.sessionId as string
const chatId = options?.chatId as string
options.logger.info(`Clearing Redis memory session ${sessionId ? sessionId : chatId}`)
await redis.clear()
options.logger.info(`Successfully cleared Redis memory session ${sessionId ? sessionId : chatId}`)
},
async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise<string> {
const memoryKey = nodeData.inputs?.memoryKey as string
const redis = await initalizeRedis(nodeData, options)
const key = memoryKey ?? 'chat_history'
const memoryResult = await redis.loadMemoryVariables({})
return serializeChatHistory(memoryResult[key])
}
}
}
const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Promise<BufferMemory> => {
const sessionId = nodeData.inputs?.sessionId as string
const sessionTTL = nodeData.inputs?.sessionTTL as number
const memoryKey = nodeData.inputs?.memoryKey as string
const chatId = options?.chatId as string
let isSessionIdUsingChatMessageId = false
if (!sessionId && chatId) isSessionIdUsingChatMessageId = true
let sessionId = ''
if (!nodeData.inputs?.sessionId && chatId) {
isSessionIdUsingChatMessageId = true
sessionId = chatId
} else {
sessionId = nodeData.inputs?.sessionId
}
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const redisUrl = getCredentialParam('redisUrl', credentialData, nodeData)
let client: Redis
if (!redisUrl || redisUrl === '') {
const username = getCredentialParam('redisCacheUser', credentialData, nodeData)
const password = getCredentialParam('redisCachePwd', credentialData, nodeData)
@@ -115,7 +104,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom
}
let obj: RedisChatMessageHistoryInput = {
sessionId: sessionId ? sessionId : chatId,
sessionId,
client
}
@@ -128,6 +117,7 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom
const redisChatMessageHistory = new RedisChatMessageHistory(obj)
/**** Methods below are needed to override the original implementations ****/
redisChatMessageHistory.getMessages = async (): Promise<BaseMessage[]> => {
const rawStoredMessages = await client.lrange((redisChatMessageHistory as any).sessionId, 0, -1)
const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message))
@@ -145,25 +135,73 @@ const initalizeRedis = async (nodeData: INodeData, options: ICommonObject): Prom
redisChatMessageHistory.clear = async (): Promise<void> => {
await client.del((redisChatMessageHistory as any).sessionId)
}
/**** End of override functions ****/
const memory = new BufferMemoryExtended({
memoryKey: memoryKey ?? 'chat_history',
chatHistory: redisChatMessageHistory,
isSessionIdUsingChatMessageId
isSessionIdUsingChatMessageId,
sessionId,
redisClient: client
})
return memory
}
interface BufferMemoryExtendedInput {
isSessionIdUsingChatMessageId: boolean
redisClient: Redis
sessionId: string
}
class BufferMemoryExtended extends BufferMemory {
isSessionIdUsingChatMessageId? = false
isSessionIdUsingChatMessageId = false
sessionId = ''
redisClient: Redis
constructor(fields: BufferMemoryInput & Partial<BufferMemoryExtendedInput>) {
constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) {
super(fields)
this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId
this.sessionId = fields.sessionId
this.redisClient = fields.redisClient
}
async getChatMessages(overrideSessionId = ''): Promise<IMessage[]> {
if (!this.redisClient) return []
const id = overrideSessionId ?? this.sessionId
const rawStoredMessages = await this.redisClient.lrange(id, 0, -1)
const orderedMessages = rawStoredMessages.reverse().map((message) => JSON.parse(message))
const baseMessages = orderedMessages.map(mapStoredMessageToChatMessage)
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise<void> {
if (!this.redisClient) return
const id = overrideSessionId ?? this.sessionId
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
if (input) {
const newInputMessage = new HumanMessage(input.text)
const messageToAdd = [newInputMessage].map((msg) => msg.toDict())
await this.redisClient.lpush(id, JSON.stringify(messageToAdd[0]))
}
if (output) {
const newOutputMessage = new AIMessage(output.text)
const messageToAdd = [newOutputMessage].map((msg) => msg.toDict())
await this.redisClient.lpush(id, JSON.stringify(messageToAdd[0]))
}
}
async clearChatMessages(overrideSessionId = ''): Promise<void> {
if (!this.redisClient) return
const id = overrideSessionId ?? this.sessionId
await this.redisClient.del(id)
await this.clear()
}
}
@@ -1,8 +1,10 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam, serializeChatHistory } from '../../../src/utils'
import { ICommonObject } from '../../../src'
import { Redis } from '@upstash/redis'
import { BufferMemory, BufferMemoryInput } from 'langchain/memory'
import { UpstashRedisChatMessageHistory } from 'langchain/stores/message/upstash_redis'
import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage } from 'langchain/schema'
import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface'
import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ICommonObject } from '../../../src/Interface'
class UpstashRedisBackedChatMemory_Memory implements INode {
label: string
@@ -43,7 +45,8 @@ class UpstashRedisBackedChatMemory_Memory implements INode {
label: 'Session Id',
name: 'sessionId',
type: 'string',
description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId',
description:
'If not specified, a random id will be used. Learn <a target="_blank" href="https://docs.flowiseai.com/memory/long-term-memory#ui-and-embedded-chat">more</a>',
default: '',
additionalParams: true,
optional: true
@@ -62,51 +65,43 @@ class UpstashRedisBackedChatMemory_Memory implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
return initalizeUpstashRedis(nodeData, options)
}
//@ts-ignore
memoryMethods = {
async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise<void> {
const redis = await initalizeUpstashRedis(nodeData, options)
const sessionId = nodeData.inputs?.sessionId as string
const chatId = options?.chatId as string
options.logger.info(`Clearing Upstash Redis memory session ${sessionId ? sessionId : chatId}`)
await redis.clear()
options.logger.info(`Successfully cleared Upstash Redis memory session ${sessionId ? sessionId : chatId}`)
},
async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise<string> {
const redis = await initalizeUpstashRedis(nodeData, options)
const key = 'chat_history'
const memoryResult = await redis.loadMemoryVariables({})
return serializeChatHistory(memoryResult[key])
}
}
}
const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject): Promise<BufferMemory> => {
const baseURL = nodeData.inputs?.baseURL as string
const sessionId = nodeData.inputs?.sessionId as string
const sessionTTL = nodeData.inputs?.sessionTTL as string
const chatId = options?.chatId as string
let isSessionIdUsingChatMessageId = false
if (!sessionId && chatId) isSessionIdUsingChatMessageId = true
let sessionId = ''
if (!nodeData.inputs?.sessionId && chatId) {
isSessionIdUsingChatMessageId = true
sessionId = chatId
} else {
sessionId = nodeData.inputs?.sessionId
}
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const upstashRestToken = getCredentialParam('upstashRestToken', credentialData, nodeData)
const client = new Redis({
url: baseURL,
token: upstashRestToken
})
const redisChatMessageHistory = new UpstashRedisChatMessageHistory({
sessionId: sessionId ? sessionId : chatId,
sessionTTL: sessionTTL ? parseInt(sessionTTL, 10) : undefined,
config: {
url: baseURL,
token: upstashRestToken
}
client
})
const memory = new BufferMemoryExtended({
memoryKey: 'chat_history',
chatHistory: redisChatMessageHistory,
isSessionIdUsingChatMessageId
isSessionIdUsingChatMessageId,
sessionId,
redisClient: client
})
return memory
@@ -114,14 +109,59 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject
interface BufferMemoryExtendedInput {
isSessionIdUsingChatMessageId: boolean
redisClient: Redis
sessionId: string
}
class BufferMemoryExtended extends BufferMemory {
isSessionIdUsingChatMessageId? = false
isSessionIdUsingChatMessageId = false
sessionId = ''
redisClient: Redis
constructor(fields: BufferMemoryInput & Partial<BufferMemoryExtendedInput>) {
constructor(fields: BufferMemoryInput & BufferMemoryExtendedInput) {
super(fields)
this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId
this.sessionId = fields.sessionId
this.redisClient = fields.redisClient
}
async getChatMessages(overrideSessionId = ''): Promise<IMessage[]> {
if (!this.redisClient) return []
const id = overrideSessionId ?? this.sessionId
const rawStoredMessages: StoredMessage[] = await this.redisClient.lrange<StoredMessage>(id, 0, -1)
const orderedMessages = rawStoredMessages.reverse()
const previousMessages = orderedMessages.filter((x): x is StoredMessage => x.type !== undefined && x.data.content !== undefined)
const baseMessages = previousMessages.map(mapStoredMessageToChatMessage)
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise<void> {
if (!this.redisClient) return
const id = overrideSessionId ?? this.sessionId
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
if (input) {
const newInputMessage = new HumanMessage(input.text)
const messageToAdd = [newInputMessage].map((msg) => msg.toDict())
await this.redisClient.lpush(id, JSON.stringify(messageToAdd[0]))
}
if (output) {
const newOutputMessage = new AIMessage(output.text)
const messageToAdd = [newOutputMessage].map((msg) => msg.toDict())
await this.redisClient.lpush(id, JSON.stringify(messageToAdd[0]))
}
}
async clearChatMessages(overrideSessionId = ''): Promise<void> {
if (!this.redisClient) return
const id = overrideSessionId ?? this.sessionId
await this.redisClient.del(id)
await this.clear()
}
}
@@ -1,9 +1,8 @@
import { SystemMessage } from 'langchain/schema'
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { IMessage, INode, INodeData, INodeParams, MessageType } from '../../../src/Interface'
import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { ZepMemory, ZepMemoryInput } from 'langchain/memory/zep'
import { ICommonObject } from '../../../src'
import { getBufferString } from 'langchain/memory'
import { InputValues, MemoryVariables, OutputValues } from 'langchain/memory'
class ZepMemory_Memory implements INode {
label: string
@@ -20,7 +19,7 @@ class ZepMemory_Memory implements INode {
constructor() {
this.label = 'Zep Memory'
this.name = 'ZepMemory'
this.version = 1.0
this.version = 2.0
this.type = 'ZepMemory'
this.icon = 'zep.png'
this.category = 'Memory'
@@ -41,17 +40,12 @@ class ZepMemory_Memory implements INode {
type: 'string',
default: 'http://127.0.0.1:8000'
},
{
label: 'Auto Summary',
name: 'autoSummary',
type: 'boolean',
default: true
},
{
label: 'Session Id',
name: 'sessionId',
type: 'string',
description: 'If not specified, the first CHAT_MESSAGE_ID will be used as sessionId',
description:
'If not specified, a random id will be used. Learn <a target="_blank" href="https://docs.flowiseai.com/memory/long-term-memory#ui-and-embedded-chat">more</a>',
default: '',
additionalParams: true,
optional: true
@@ -61,13 +55,7 @@ class ZepMemory_Memory implements INode {
name: 'k',
type: 'number',
default: '10',
description: 'Window of size k to surface the last k back-and-forth to use as memory.'
},
{
label: 'Auto Summary Template',
name: 'autoSummaryTemplate',
type: 'string',
default: 'This is the summary of the following conversation:\n{summary}',
description: 'Window of size k to surface the last k back-and-forth to use as memory.',
additionalParams: true
},
{
@@ -109,57 +97,7 @@ class ZepMemory_Memory implements INode {
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const autoSummaryTemplate = nodeData.inputs?.autoSummaryTemplate as string
const autoSummary = nodeData.inputs?.autoSummary as boolean
const k = nodeData.inputs?.k as string
let zep = await initalizeZep(nodeData, options)
// hack to support summary
let tmpFunc = zep.loadMemoryVariables
zep.loadMemoryVariables = async (values) => {
let data = await tmpFunc.bind(zep, values)()
if (autoSummary && zep.returnMessages && data[zep.memoryKey] && data[zep.memoryKey].length) {
const zepClient = await zep.zepClientPromise
const memory = await zepClient.memory.getMemory(zep.sessionId, parseInt(k, 10) ?? 10)
if (memory?.summary) {
let summary = autoSummaryTemplate.replace(/{summary}/g, memory.summary.content)
// eslint-disable-next-line no-console
console.log('[ZepMemory] auto summary:', summary)
data[zep.memoryKey].unshift(new SystemMessage(summary))
}
}
// for langchain zep memory compatibility, or we will get "Missing value for input variable chat_history"
if (data instanceof Array) {
data = {
[zep.memoryKey]: data
}
}
return data
}
return zep
}
//@ts-ignore
memoryMethods = {
async clearSessionMemory(nodeData: INodeData, options: ICommonObject): Promise<void> {
const zep = await initalizeZep(nodeData, options)
const sessionId = nodeData.inputs?.sessionId as string
const chatId = options?.chatId as string
options.logger.info(`Clearing Zep memory session ${sessionId ? sessionId : chatId}`)
await zep.clear()
options.logger.info(`Successfully cleared Zep memory session ${sessionId ? sessionId : chatId}`)
},
async getChatMessages(nodeData: INodeData, options: ICommonObject): Promise<string> {
const memoryKey = nodeData.inputs?.memoryKey as string
const aiPrefix = nodeData.inputs?.aiPrefix as string
const humanPrefix = nodeData.inputs?.humanPrefix as string
const zep = await initalizeZep(nodeData, options)
const key = memoryKey ?? 'chat_history'
const memoryResult = await zep.loadMemoryVariables({})
return getBufferString(memoryResult[key], humanPrefix, aiPrefix)
}
return await initalizeZep(nodeData, options)
}
}
@@ -169,40 +107,94 @@ const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promis
const humanPrefix = nodeData.inputs?.humanPrefix as string
const memoryKey = nodeData.inputs?.memoryKey as string
const inputKey = nodeData.inputs?.inputKey as string
const sessionId = nodeData.inputs?.sessionId as string
const k = nodeData.inputs?.k as string
const chatId = options?.chatId as string
let isSessionIdUsingChatMessageId = false
if (!sessionId && chatId) isSessionIdUsingChatMessageId = true
let sessionId = ''
if (!nodeData.inputs?.sessionId && chatId) {
isSessionIdUsingChatMessageId = true
sessionId = chatId
} else {
sessionId = nodeData.inputs?.sessionId
}
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const apiKey = getCredentialParam('apiKey', credentialData, nodeData)
const obj: ZepMemoryInput & Partial<ZepMemoryExtendedInput> = {
const obj: ZepMemoryInput & ZepMemoryExtendedInput = {
baseURL,
sessionId: sessionId ? sessionId : chatId,
aiPrefix,
humanPrefix,
returnMessages: true,
memoryKey,
inputKey
inputKey,
sessionId,
isSessionIdUsingChatMessageId,
k: k ? parseInt(k, 10) : undefined
}
if (apiKey) obj.apiKey = apiKey
if (isSessionIdUsingChatMessageId) obj.isSessionIdUsingChatMessageId = true
return new ZepMemoryExtended(obj)
}
interface ZepMemoryExtendedInput {
isSessionIdUsingChatMessageId: boolean
k?: number
}
class ZepMemoryExtended extends ZepMemory {
isSessionIdUsingChatMessageId? = false
isSessionIdUsingChatMessageId = false
lastN?: number
constructor(fields: ZepMemoryInput & Partial<ZepMemoryExtendedInput>) {
constructor(fields: ZepMemoryInput & ZepMemoryExtendedInput) {
super(fields)
this.isSessionIdUsingChatMessageId = fields.isSessionIdUsingChatMessageId
this.lastN = fields.k
}
async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise<MemoryVariables> {
if (overrideSessionId) {
super.sessionId = overrideSessionId
}
return super.loadMemoryVariables({ ...values, lastN: this.lastN })
}
async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise<void> {
if (overrideSessionId) {
super.sessionId = overrideSessionId
}
return super.saveContext(inputValues, outputValues)
}
async clear(overrideSessionId = ''): Promise<void> {
if (overrideSessionId) {
super.sessionId = overrideSessionId
}
return super.clear()
}
async getChatMessages(overrideSessionId = ''): Promise<IMessage[]> {
const id = overrideSessionId ?? this.sessionId
const memoryVariables = await this.loadMemoryVariables({}, id)
const baseMessages = memoryVariables[this.memoryKey]
return convertBaseMessagetoIMessage(baseMessages)
}
async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise<void> {
const id = overrideSessionId ?? this.sessionId
const input = msgArray.find((msg) => msg.type === 'userMessage')
const output = msgArray.find((msg) => msg.type === 'apiMessage')
const inputValues = { [this.inputKey ?? 'input']: input?.text }
const outputValues = { output: output?.text }
await this.saveContext(inputValues, outputValues, id)
}
async clearChatMessages(overrideSessionId = ''): Promise<void> {
const id = overrideSessionId ?? this.sessionId
await this.clear(id)
}
}
@@ -0,0 +1,75 @@
import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { ResponseSynthesizerClass } from '../base'
class CompactRefine_LlamaIndex implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
tags: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Compact and Refine'
this.name = 'compactrefineLlamaIndex'
this.version = 1.0
this.type = 'CompactRefine'
this.icon = 'compactrefine.svg'
this.category = 'Response Synthesizer'
this.description =
'CompactRefine is a slight variation of Refine that first compacts the text chunks into the smallest possible number of chunks.'
this.baseClasses = [this.type, 'ResponseSynthesizer']
this.tags = ['LlamaIndex']
this.inputs = [
{
label: 'Refine Prompt',
name: 'refinePrompt',
type: 'string',
rows: 4,
default: `The original query is as follows: {query}
We have provided an existing answer: {existingAnswer}
We have the opportunity to refine the existing answer (only if needed) with some more context below.
------------
{context}
------------
Given the new context, refine the original answer to better answer the query. If the context isn't useful, return the original answer.
Refined Answer:`,
warning: `Prompt can contains no variables, or up to 3 variables. Variables must be {existingAnswer}, {context} and {query}`,
optional: true
},
{
label: 'Text QA Prompt',
name: 'textQAPrompt',
type: 'string',
rows: 4,
default: `Context information is below.
---------------------
{context}
---------------------
Given the context information and not prior knowledge, answer the query.
Query: {query}
Answer:`,
warning: `Prompt can contains no variables, or up to 2 variables. Variables must be {context} and {query}`,
optional: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const refinePrompt = nodeData.inputs?.refinePrompt as string
const textQAPrompt = nodeData.inputs?.textQAPrompt as string
const refinePromptTemplate = ({ context = '', existingAnswer = '', query = '' }) =>
refinePrompt.replace('{existingAnswer}', existingAnswer).replace('{context}', context).replace('{query}', query)
const textQAPromptTemplate = ({ context = '', query = '' }) => textQAPrompt.replace('{context}', context).replace('{query}', query)
return new ResponseSynthesizerClass({ textQAPromptTemplate, refinePromptTemplate, type: 'CompactAndRefine' })
}
}
module.exports = { nodeClass: CompactRefine_LlamaIndex }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layers-difference" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16 16v2a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2v-8a2 2 0 0 1 2 -2h2v-2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-2" /><path d="M10 8l-2 0l0 2" /><path d="M8 14l0 2l2 0" /><path d="M14 8l2 0l0 2" /><path d="M16 14l0 2l-2 0" /></svg>

After

Width:  |  Height:  |  Size: 529 B

@@ -0,0 +1,75 @@
import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { ResponseSynthesizerClass } from '../base'
class Refine_LlamaIndex implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
tags: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Refine'
this.name = 'refineLlamaIndex'
this.version = 1.0
this.type = 'Refine'
this.icon = 'refine.svg'
this.category = 'Response Synthesizer'
this.description =
'Create and refine an answer by sequentially going through each retrieved text chunk. This makes a separate LLM call per Node. Good for more detailed answers.'
this.baseClasses = [this.type, 'ResponseSynthesizer']
this.tags = ['LlamaIndex']
this.inputs = [
{
label: 'Refine Prompt',
name: 'refinePrompt',
type: 'string',
rows: 4,
default: `The original query is as follows: {query}
We have provided an existing answer: {existingAnswer}
We have the opportunity to refine the existing answer (only if needed) with some more context below.
------------
{context}
------------
Given the new context, refine the original answer to better answer the query. If the context isn't useful, return the original answer.
Refined Answer:`,
warning: `Prompt can contains no variables, or up to 3 variables. Variables must be {existingAnswer}, {context} and {query}`,
optional: true
},
{
label: 'Text QA Prompt',
name: 'textQAPrompt',
type: 'string',
rows: 4,
default: `Context information is below.
---------------------
{context}
---------------------
Given the context information and not prior knowledge, answer the query.
Query: {query}
Answer:`,
warning: `Prompt can contains no variables, or up to 2 variables. Variables must be {context} and {query}`,
optional: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const refinePrompt = nodeData.inputs?.refinePrompt as string
const textQAPrompt = nodeData.inputs?.textQAPrompt as string
const refinePromptTemplate = ({ context = '', existingAnswer = '', query = '' }) =>
refinePrompt.replace('{existingAnswer}', existingAnswer).replace('{context}', context).replace('{query}', query)
const textQAPromptTemplate = ({ context = '', query = '' }) => textQAPrompt.replace('{context}', context).replace('{query}', query)
return new ResponseSynthesizerClass({ textQAPromptTemplate, refinePromptTemplate, type: 'Refine' })
}
}
module.exports = { nodeClass: Refine_LlamaIndex }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-filter-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11.36 20.213l-2.36 .787v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414" /><path d="M18 18m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M20.2 20.2l1.8 1.8" /></svg>

After

Width:  |  Height:  |  Size: 501 B

@@ -0,0 +1,35 @@
import { INode, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { ResponseSynthesizerClass } from '../base'
class SimpleResponseBuilder_LlamaIndex implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
tags: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'Simple Response Builder'
this.name = 'simpleResponseBuilderLlamaIndex'
this.version = 1.0
this.type = 'SimpleResponseBuilder'
this.icon = 'simplerb.svg'
this.category = 'Response Synthesizer'
this.description = `Apply a query to a collection of text chunks, gathering the responses in an array, and return a combined string of all responses. Useful for individual queries on each text chunk.`
this.baseClasses = [this.type, 'ResponseSynthesizer']
this.tags = ['LlamaIndex']
this.inputs = []
}
async init(): Promise<any> {
return new ResponseSynthesizerClass({ type: 'SimpleResponseBuilder' })
}
}
module.exports = { nodeClass: SimpleResponseBuilder_LlamaIndex }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-quote" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 11h-4a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1h3a1 1 0 0 1 1 1v6c0 2.667 -1.333 4.333 -4 5" /><path d="M19 11h-4a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1h3a1 1 0 0 1 1 1v6c0 2.667 -1.333 4.333 -4 5" /></svg>

After

Width:  |  Height:  |  Size: 481 B

@@ -0,0 +1,56 @@
import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { ResponseSynthesizerClass } from '../base'
class TreeSummarize_LlamaIndex implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
tags: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'TreeSummarize'
this.name = 'treeSummarizeLlamaIndex'
this.version = 1.0
this.type = 'TreeSummarize'
this.icon = 'treesummarize.svg'
this.category = 'Response Synthesizer'
this.description =
'Given a set of text chunks and the query, recursively construct a tree and return the root node as the response. Good for summarization purposes.'
this.baseClasses = [this.type, 'ResponseSynthesizer']
this.tags = ['LlamaIndex']
this.inputs = [
{
label: 'Prompt',
name: 'prompt',
type: 'string',
rows: 4,
default: `Context information from multiple sources is below.
---------------------
{context}
---------------------
Given the information from multiple sources and not prior knowledge, answer the query.
Query: {query}
Answer:`,
warning: `Prompt can contains no variables, or up to 2 variables. Variables must be {context} and {query}`,
optional: true
}
]
}
async init(nodeData: INodeData): Promise<any> {
const prompt = nodeData.inputs?.prompt as string
const textQAPromptTemplate = ({ context = '', query = '' }) => prompt.replace('{context}', context).replace('{query}', query)
return new ResponseSynthesizerClass({ textQAPromptTemplate, type: 'TreeSummarize' })
}
}
module.exports = { nodeClass: TreeSummarize_LlamaIndex }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tree" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 13l-2 -2" /><path d="M12 12l2 -2" /><path d="M12 21v-13" /><path d="M9.824 16a3 3 0 0 1 -2.743 -3.69a3 3 0 0 1 .304 -4.833a3 3 0 0 1 4.615 -3.707a3 3 0 0 1 4.614 3.707a3 3 0 0 1 .305 4.833a3 3 0 0 1 -2.919 3.695h-4z" /></svg>

After

Width:  |  Height:  |  Size: 512 B

@@ -0,0 +1,11 @@
export class ResponseSynthesizerClass {
type: string
textQAPromptTemplate?: any
refinePromptTemplate?: any
constructor(params: { type: string; textQAPromptTemplate?: any; refinePromptTemplate?: any }) {
this.type = params.type
this.textQAPromptTemplate = params.textQAPromptTemplate
this.refinePromptTemplate = params.refinePromptTemplate
}
}
@@ -0,0 +1,366 @@
import {
BaseNode,
Document,
Metadata,
VectorStore,
VectorStoreQuery,
VectorStoreQueryResult,
serviceContextFromDefaults,
storageContextFromDefaults,
VectorStoreIndex,
BaseEmbedding
} from 'llamaindex'
import { FetchResponse, Index, Pinecone, ScoredPineconeRecord } from '@pinecone-database/pinecone'
import { flatten } from 'lodash'
import { Document as LCDocument } from 'langchain/document'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { flattenObject, getCredentialData, getCredentialParam } from '../../../src/utils'
class PineconeLlamaIndex_VectorStores implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
tags: string[]
baseClasses: string[]
inputs: INodeParams[]
credential: INodeParams
constructor() {
this.label = 'Pinecone'
this.name = 'pineconeLlamaIndex'
this.version = 1.0
this.type = 'Pinecone'
this.icon = 'pinecone.png'
this.category = 'Vector Stores'
this.description = `Upsert embedded data and perform similarity search upon query using Pinecone, a leading fully managed hosted vector database`
this.baseClasses = [this.type, 'VectorIndexRetriever']
this.tags = ['LlamaIndex']
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['pineconeApi']
}
this.inputs = [
{
label: 'Document',
name: 'document',
type: 'Document',
list: true,
optional: true
},
{
label: 'Chat Model',
name: 'model',
type: 'BaseChatModel_LlamaIndex'
},
{
label: 'Embeddings',
name: 'embeddings',
type: 'BaseEmbedding_LlamaIndex'
},
{
label: 'Pinecone Index',
name: 'pineconeIndex',
type: 'string'
},
{
label: 'Pinecone Namespace',
name: 'pineconeNamespace',
type: 'string',
placeholder: 'my-first-namespace',
additionalParams: true,
optional: true
},
{
label: 'Pinecone Metadata Filter',
name: 'pineconeMetadataFilter',
type: 'json',
optional: true,
additionalParams: true
},
{
label: 'Top K',
name: 'topK',
description: 'Number of top results to fetch. Default to 4',
placeholder: '4',
type: 'number',
additionalParams: true,
optional: true
}
]
}
//@ts-ignore
vectorStoreMethods = {
async upsert(nodeData: INodeData, options: ICommonObject): Promise<void> {
const indexName = nodeData.inputs?.pineconeIndex as string
const pineconeNamespace = nodeData.inputs?.pineconeNamespace as string
const docs = nodeData.inputs?.document as LCDocument[]
const embeddings = nodeData.inputs?.embeddings as BaseEmbedding
const model = nodeData.inputs?.model
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData)
const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData)
const pcvs = new PineconeVectorStore({
indexName,
apiKey: pineconeApiKey,
environment: pineconeEnv,
namespace: pineconeNamespace
})
const flattenDocs = docs && docs.length ? flatten(docs) : []
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
finalDocs.push(new LCDocument(flattenDocs[i]))
}
}
const llamadocs: Document[] = []
for (const doc of finalDocs) {
llamadocs.push(new Document({ text: doc.pageContent, metadata: doc.metadata }))
}
const serviceContext = serviceContextFromDefaults({ llm: model, embedModel: embeddings })
const storageContext = await storageContextFromDefaults({ vectorStore: pcvs })
try {
await VectorStoreIndex.fromDocuments(llamadocs, { serviceContext, storageContext })
} catch (e) {
throw new Error(e)
}
}
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const indexName = nodeData.inputs?.pineconeIndex as string
const pineconeNamespace = nodeData.inputs?.pineconeNamespace as string
const pineconeMetadataFilter = nodeData.inputs?.pineconeMetadataFilter
const embeddings = nodeData.inputs?.embeddings as BaseEmbedding
const model = nodeData.inputs?.model
const topK = nodeData.inputs?.topK as string
const k = topK ? parseFloat(topK) : 4
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData)
const pineconeEnv = getCredentialParam('pineconeEnv', credentialData, nodeData)
const obj: PineconeParams = {
indexName,
apiKey: pineconeApiKey,
environment: pineconeEnv
}
if (pineconeNamespace) obj.namespace = pineconeNamespace
if (pineconeMetadataFilter) {
const metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter)
obj.queryFilter = metadatafilter
}
const pcvs = new PineconeVectorStore(obj)
const serviceContext = serviceContextFromDefaults({ llm: model, embedModel: embeddings })
const storageContext = await storageContextFromDefaults({ vectorStore: pcvs })
const index = await VectorStoreIndex.init({
nodes: [],
storageContext,
serviceContext
})
const retriever = index.asRetriever()
retriever.similarityTopK = k
;(retriever as any).serviceContext = serviceContext
return retriever
}
}
type PineconeParams = {
indexName: string
apiKey: string
environment: string
namespace?: string
chunkSize?: number
queryFilter?: object
}
class PineconeVectorStore implements VectorStore {
storesText: boolean = true
db?: Pinecone
indexName: string
apiKey: string
environment: string
chunkSize: number
namespace?: string
queryFilter?: object
constructor(params: PineconeParams) {
this.indexName = params?.indexName
this.apiKey = params?.apiKey
this.environment = params?.environment
this.namespace = params?.namespace ?? ''
this.chunkSize = params?.chunkSize ?? Number.parseInt(process.env.PINECONE_CHUNK_SIZE ?? '100')
this.queryFilter = params?.queryFilter ?? {}
}
private async getDb(): Promise<Pinecone> {
if (!this.db) {
this.db = new Pinecone({
apiKey: this.apiKey,
environment: this.environment
})
}
return Promise.resolve(this.db)
}
client() {
return this.getDb()
}
async index() {
const db: Pinecone = await this.getDb()
return db.Index(this.indexName)
}
async clearIndex() {
const db: Pinecone = await this.getDb()
return await db.index(this.indexName).deleteAll()
}
async add(embeddingResults: BaseNode<Metadata>[]): Promise<string[]> {
if (embeddingResults.length == 0) {
return Promise.resolve([])
}
const idx: Index = await this.index()
const nodes = embeddingResults.map(this.nodeToRecord)
for (let i = 0; i < nodes.length; i += this.chunkSize) {
const chunk = nodes.slice(i, i + this.chunkSize)
const result = await this.saveChunk(idx, chunk)
if (!result) {
return Promise.reject()
}
}
return Promise.resolve([])
}
protected async saveChunk(idx: Index, chunk: any) {
try {
const namespace = idx.namespace(this.namespace ?? '')
await namespace.upsert(chunk)
return true
} catch (err) {
return false
}
}
async delete(refDocId: string): Promise<void> {
const idx = await this.index()
const namespace = idx.namespace(this.namespace ?? '')
return namespace.deleteOne(refDocId)
}
async query(query: VectorStoreQuery): Promise<VectorStoreQueryResult> {
const queryOptions: any = {
vector: query.queryEmbedding,
topK: query.similarityTopK,
filter: this.queryFilter
}
const idx = await this.index()
const namespace = idx.namespace(this.namespace ?? '')
const results = await namespace.query(queryOptions)
const idList = results.matches.map((row) => row.id)
const records: FetchResponse<any> = await namespace.fetch(idList)
const rows = Object.values(records.records)
const nodes = rows.map((row) => {
return new Document({
id_: row.id,
text: this.textFromResultRow(row),
metadata: this.metaWithoutText(row.metadata),
embedding: row.values
})
})
const result = {
nodes: nodes,
similarities: results.matches.map((row) => row.score || 999),
ids: results.matches.map((row) => row.id)
}
return Promise.resolve(result)
}
/**
* Required by VectorStore interface. Currently ignored.
*/
persist(): Promise<void> {
return Promise.resolve()
}
textFromResultRow(row: ScoredPineconeRecord<Metadata>): string {
return row.metadata?.text ?? ''
}
metaWithoutText(meta: Metadata): any {
return Object.keys(meta)
.filter((key) => key != 'text')
.reduce((acc: any, key: string) => {
acc[key] = meta[key]
return acc
}, {})
}
nodeToRecord(node: BaseNode<Metadata>) {
let id: any = node.id_.length ? node.id_ : null
return {
id: id,
values: node.getEmbedding(),
metadata: {
...cleanupMetadata(node.metadata),
text: (node as any).text
}
}
}
}
const cleanupMetadata = (nodeMetadata: ICommonObject) => {
// Pinecone doesn't support nested objects, so we flatten them
const documentMetadata: any = { ...nodeMetadata }
// preserve string arrays which are allowed
const stringArrays: Record<string, string[]> = {}
for (const key of Object.keys(documentMetadata)) {
if (Array.isArray(documentMetadata[key]) && documentMetadata[key].every((el: any) => typeof el === 'string')) {
stringArrays[key] = documentMetadata[key]
delete documentMetadata[key]
}
}
const metadata: {
[key: string]: string | number | boolean | string[] | null
} = {
...flattenObject(documentMetadata),
...stringArrays
}
// Pinecone doesn't support null values, so we remove them
for (const key of Object.keys(metadata)) {
if (metadata[key] == null) {
delete metadata[key]
} else if (typeof metadata[key] === 'object' && Object.keys(metadata[key] as unknown as object).length === 0) {
delete metadata[key]
}
}
return metadata
}
module.exports = { nodeClass: PineconeLlamaIndex_VectorStores }
@@ -0,0 +1,124 @@
import path from 'path'
import { flatten } from 'lodash'
import { storageContextFromDefaults, serviceContextFromDefaults, VectorStoreIndex, Document } from 'llamaindex'
import { Document as LCDocument } from 'langchain/document'
import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { getUserHome } from '../../../src'
class SimpleStoreUpsert_LlamaIndex_VectorStores implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
tags: string[]
inputs: INodeParams[]
outputs: INodeOutputsValue[]
constructor() {
this.label = 'SimpleStore'
this.name = 'simpleStoreLlamaIndex'
this.version = 1.0
this.type = 'SimpleVectorStore'
this.icon = 'simplevs.svg'
this.category = 'Vector Stores'
this.description = 'Upsert embedded data to local path and perform similarity search'
this.baseClasses = [this.type, 'VectorIndexRetriever']
this.tags = ['LlamaIndex']
this.inputs = [
{
label: 'Document',
name: 'document',
type: 'Document',
list: true,
optional: true
},
{
label: 'Chat Model',
name: 'model',
type: 'BaseChatModel_LlamaIndex'
},
{
label: 'Embeddings',
name: 'embeddings',
type: 'BaseEmbedding_LlamaIndex'
},
{
label: 'Base Path to store',
name: 'basePath',
description:
'Path to store persist embeddings indexes with persistence. If not specified, default to same path where database is stored',
type: 'string',
optional: true
},
{
label: 'Top K',
name: 'topK',
description: 'Number of top results to fetch. Default to 4',
placeholder: '4',
type: 'number',
optional: true
}
]
}
//@ts-ignore
vectorStoreMethods = {
async upsert(nodeData: INodeData): Promise<void> {
const basePath = nodeData.inputs?.basePath as string
const docs = nodeData.inputs?.document as LCDocument[]
const embeddings = nodeData.inputs?.embeddings
const model = nodeData.inputs?.model
let filePath = ''
if (!basePath) filePath = path.join(getUserHome(), '.flowise', 'llamaindex')
else filePath = basePath
const flattenDocs = docs && docs.length ? flatten(docs) : []
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
finalDocs.push(new LCDocument(flattenDocs[i]))
}
const llamadocs: Document[] = []
for (const doc of finalDocs) {
llamadocs.push(new Document({ text: doc.pageContent, metadata: doc.metadata }))
}
const serviceContext = serviceContextFromDefaults({ llm: model, embedModel: embeddings })
const storageContext = await storageContextFromDefaults({ persistDir: filePath })
try {
await VectorStoreIndex.fromDocuments(llamadocs, { serviceContext, storageContext })
} catch (e) {
throw new Error(e)
}
}
}
async init(nodeData: INodeData): Promise<any> {
const basePath = nodeData.inputs?.basePath as string
const embeddings = nodeData.inputs?.embeddings
const model = nodeData.inputs?.model
const topK = nodeData.inputs?.topK as string
const k = topK ? parseFloat(topK) : 4
let filePath = ''
if (!basePath) filePath = path.join(getUserHome(), '.flowise', 'llamaindex')
else filePath = basePath
const serviceContext = serviceContextFromDefaults({ llm: model, embedModel: embeddings })
const storageContext = await storageContextFromDefaults({ persistDir: filePath })
const index = await VectorStoreIndex.init({ storageContext, serviceContext })
const retriever = index.asRetriever()
retriever.similarityTopK = k
return retriever
}
}
module.exports = { nodeClass: SimpleStoreUpsert_LlamaIndex_VectorStores }
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path>
<path d="M4 6v6a8 3 0 0 0 16 0v-6"></path>
<path d="M4 12v6a8 3 0 0 0 16 0v-6"></path>
</svg>

After

Width:  |  Height:  |  Size: 451 B

+2 -1
View File
@@ -21,7 +21,7 @@
"@aws-sdk/client-s3": "^3.427.0",
"@dqbd/tiktoken": "^1.0.7",
"@elastic/elasticsearch": "^8.9.0",
"@getzep/zep-js": "^0.6.3",
"@getzep/zep-js": "^0.9.0",
"@gomomento/sdk": "^1.51.1",
"@gomomento/sdk-core": "^1.51.1",
"@google-ai/generativelanguage": "^0.2.1",
@@ -54,6 +54,7 @@
"langfuse-langchain": "^1.0.31",
"langsmith": "^0.0.32",
"linkifyjs": "^4.1.1",
"llamaindex": "^0.0.30",
"llmonitor": "^0.5.5",
"mammoth": "^1.5.1",
"moment": "^2.29.3",
+1 -4
View File
@@ -91,6 +91,7 @@ export interface INodeProperties {
version: number
category: string
baseClasses: string[]
tags?: string[]
description?: string
filePath?: string
badge?: string
@@ -107,10 +108,6 @@ export interface INode extends INodeProperties {
search: (nodeData: INodeData, options?: ICommonObject) => Promise<any>
delete: (nodeData: INodeData, options?: ICommonObject) => Promise<void>
}
memoryMethods?: {
clearSessionMemory: (nodeData: INodeData, options?: ICommonObject) => Promise<void>
getChatMessages: (nodeData: INodeData, options?: ICommonObject) => Promise<string>
}
init?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<any>
run?(nodeData: INodeData, input: string, options?: ICommonObject): Promise<string | ICommonObject>
}
+52 -1
View File
@@ -8,7 +8,7 @@ import { DataSource } from 'typeorm'
import { ICommonObject, IDatabaseEntity, IMessage, INodeData } from './Interface'
import { AES, enc } from 'crypto-js'
import { ChatMessageHistory } from 'langchain/memory'
import { AIMessage, HumanMessage } from 'langchain/schema'
import { AIMessage, HumanMessage, BaseMessage } from 'langchain/schema'
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
@@ -587,3 +587,54 @@ export const convertSchemaToZod = (schema: string | object): ICommonObject => {
throw new Error(e)
}
}
/**
* Flatten nested object
* @param {ICommonObject} obj
* @param {string} parentKey
* @returns {ICommonObject}
*/
export const flattenObject = (obj: ICommonObject, parentKey?: string) => {
let result: any = {}
Object.keys(obj).forEach((key) => {
const value = obj[key]
const _key = parentKey ? parentKey + '.' + key : key
if (typeof value === 'object') {
result = { ...result, ...flattenObject(value, _key) }
} else {
result[_key] = value
}
})
return result
}
/**
* Convert BaseMessage to IMessage
* @param {ICommonObject} obj
* @param {string} parentKey
* @returns {ICommonObject}
*/
export const convertBaseMessagetoIMessage = (messages: BaseMessage[]): IMessage[] => {
const formatmessages: IMessage[] = []
for (const m of messages) {
if (m._getType() === 'human') {
formatmessages.push({
message: m.content as string,
type: 'userMessage'
})
} else if (m._getType() === 'ai') {
formatmessages.push({
message: m.content as string,
type: 'apiMessage'
})
} else if (m._getType() === 'system') {
formatmessages.push({
message: m.content as string,
type: 'apiMessage'
})
}
}
return formatmessages
}