UNPKG

dtamind-components

Version:

Apps integration for Dtamind. Contain Nodes and Credentials.

612 lines (597 loc) 26 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const lodash_1 = require("lodash"); const zod_1 = require("zod"); const runnables_1 = require("@langchain/core/runnables"); const prompts_1 = require("@langchain/core/prompts"); const messages_1 = require("@langchain/core/messages"); const utils_1 = require("../../../src/utils"); const commonUtils_1 = require("../commonUtils"); const TAB_IDENTIFIER = 'selectedUpdateStateMemoryTab'; const customOutputFuncDesc = `This is only applicable when you have a custom State at the START node. After agent execution, you might want to update the State values`; const howToUseCode = ` 1. Return the key value JSON object. For example: if you have the following State: \`\`\`json { "user": null } \`\`\` You can update the "user" value by returning the following: \`\`\`js return { "user": "john doe" } \`\`\` 2. If you want to use the LLM Node's output as the value to update state, it is available as \`$flow.output\` with the following structure: \`\`\`json { "content": 'Hello! How can I assist you today?', "name": "", "additional_kwargs": {}, "response_metadata": {}, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": {} } \`\`\` For example, if the output \`content\` is the value you want to update the state with, you can return the following: \`\`\`js return { "user": $flow.output.content } \`\`\` 3. You can also get default flow config, including the current "state": - \`$flow.sessionId\` - \`$flow.chatId\` - \`$flow.chatflowId\` - \`$flow.input\` - \`$flow.state\` 4. You can get custom variables: \`$vars.<variable-name>\` `; const howToUse = ` 1. Key and value pair to be updated. For example: if you have the following State: | Key | Operation | Default Value | |-----------|---------------|-------------------| | user | Replace | | You can update the "user" value with the following: | Key | Value | |-----------|-----------| | user | john doe | 2. If you want to use the LLM Node's output as the value to update state, it is available as available as \`$flow.output\` with the following structure: \`\`\`json { "content": 'Hello! How can I assist you today?', "name": "", "additional_kwargs": {}, "response_metadata": {}, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": {} } \`\`\` For example, if the output \`content\` is the value you want to update the state with, you can do the following: | Key | Value | |-----------|---------------------------| | user | \`$flow.output.content\` | 3. You can get default flow config, including the current "state": - \`$flow.sessionId\` - \`$flow.chatId\` - \`$flow.chatflowId\` - \`$flow.input\` - \`$flow.state\` 4. You can get custom variables: \`$vars.<variable-name>\` `; const defaultFunc = `const result = $flow.output; /* Suppose we have a custom State schema like this: * { aggregate: { value: (x, y) => x.concat(y), default: () => [] } } */ return { aggregate: [result.content] };`; const messageHistoryExample = `const { AIMessage, HumanMessage, ToolMessage } = require('@langchain/core/messages'); return [ new HumanMessage("What is 333382 🦜 1932?"), new AIMessage({ content: "", tool_calls: [ { id: "12345", name: "calulator", args: { number1: 333382, number2: 1932, operation: "divide", }, }, ], }), new ToolMessage({ tool_call_id: "12345", content: "The answer is 172.558.", }), new AIMessage("The answer is 172.558."), ]`; class LLMNode_SeqAgents { constructor() { this.label = 'LLM Node'; this.name = 'seqLLMNode'; this.version = 4.1; this.type = 'LLMNode'; this.icon = 'llmNode.svg'; this.category = 'Sequential Agents'; this.description = 'Run Chat Model and return the output'; this.baseClasses = [this.type]; this.documentation = 'https://docs.dtamindai.com/using-dtamind/agentflows/sequential-agents#id-5.-llm-node'; this.inputs = [ { label: 'Name', name: 'llmNodeName', type: 'string', placeholder: 'LLM' }, { label: 'System Prompt', name: 'systemMessagePrompt', type: 'string', rows: 4, optional: true, additionalParams: true }, { label: 'Prepend Messages History', name: 'messageHistory', description: 'Prepend a list of messages between System Prompt and Human Prompt. This is useful when you want to provide few shot examples', type: 'code', hideCodeExecute: true, codeExample: messageHistoryExample, optional: true, additionalParams: true }, { label: 'Conversation History', name: 'conversationHistorySelection', type: 'options', options: [ { label: 'User Question', name: 'user_question', description: 'Use the user question from the historical conversation messages as input.' }, { label: 'Last Conversation Message', name: 'last_message', description: 'Use the last conversation message from the historical conversation messages as input.' }, { label: 'All Conversation Messages', name: 'all_messages', description: 'Use all conversation messages from the historical conversation messages as input.' }, { label: 'Empty', name: 'empty', description: 'Do not use any messages from the conversation history. ' + 'Ensure to use either System Prompt, Human Prompt, or Messages History.' } ], default: 'all_messages', optional: true, description: 'Select which messages from the conversation history to include in the prompt. ' + 'The selected messages will be inserted between the System Prompt (if defined) and ' + '[Messages History, Human Prompt].', additionalParams: true }, { label: 'Human Prompt', name: 'humanMessagePrompt', type: 'string', description: 'This prompt will be added at the end of the messages as human message', rows: 4, optional: true, additionalParams: true }, { label: 'Sequential Node', name: 'sequentialNode', type: 'Start | Agent | Condition | LLMNode | ToolNode | CustomFunction | ExecuteFlow', description: 'Can be connected to one of the following nodes: Start, Agent, Condition, LLM, Tool Node, Custom Function, Execute Flow', list: true }, { label: 'Chat Model', name: 'model', type: 'BaseChatModel', optional: true, description: `Overwrite model to be used for this node` }, { label: 'Format Prompt Values', name: 'promptValues', description: 'Assign values to the prompt variables. You can also use $flow.state.<variable-name> to get the state value', type: 'json', optional: true, acceptVariable: true, list: true, additionalParams: true }, { label: 'JSON Structured Output', name: 'llmStructuredOutput', type: 'datagrid', description: 'Instruct the LLM to give output in a JSON structured schema', datagrid: [ { field: 'key', headerName: 'Key', editable: true }, { field: 'type', headerName: 'Type', type: 'singleSelect', valueOptions: ['String', 'String Array', 'Number', 'Boolean', 'Enum'], editable: true }, { field: 'enumValues', headerName: 'Enum Values', editable: true }, { field: 'description', headerName: 'Description', flex: 1, editable: true } ], optional: true, additionalParams: true }, { label: 'Update State', name: 'updateStateMemory', type: 'tabs', tabIdentifier: TAB_IDENTIFIER, default: 'updateStateMemoryUI', additionalParams: true, tabs: [ { label: 'Update State (Table)', name: 'updateStateMemoryUI', type: 'datagrid', hint: { label: 'How to use', value: howToUse }, description: customOutputFuncDesc, datagrid: [ { field: 'key', headerName: 'Key', type: 'asyncSingleSelect', loadMethod: 'loadStateKeys', flex: 0.5, editable: true }, { field: 'value', headerName: 'Value', type: 'freeSolo', valueOptions: [ { label: 'LLM Node Output (string)', value: '$flow.output.content' }, { label: `LLM JSON Output Key (string)`, value: '$flow.output.<replace-with-key>' }, { label: `Global variable (string)`, value: '$vars.<variable-name>' }, { label: 'Input Question (string)', value: '$flow.input' }, { label: 'Session Id (string)', value: '$flow.sessionId' }, { label: 'Chat Id (string)', value: '$flow.chatId' }, { label: 'Chatflow Id (string)', value: '$flow.chatflowId' } ], editable: true, flex: 1 } ], optional: true, additionalParams: true }, { label: 'Update State (Code)', name: 'updateStateMemoryCode', type: 'code', hint: { label: 'How to use', value: howToUseCode }, description: `${customOutputFuncDesc}. Must return an object representing the state`, hideCodeExecute: true, codeExample: defaultFunc, optional: true, additionalParams: true } ] } ]; } async init(nodeData, input, options) { // Tools can be connected through ToolNodes let tools = nodeData.inputs?.tools; tools = (0, lodash_1.flatten)(tools); let systemPrompt = nodeData.inputs?.systemMessagePrompt; systemPrompt = (0, utils_1.transformBracesWithColon)(systemPrompt); let humanPrompt = nodeData.inputs?.humanMessagePrompt; humanPrompt = (0, utils_1.transformBracesWithColon)(humanPrompt); const llmNodeLabel = nodeData.inputs?.llmNodeName; const sequentialNodes = nodeData.inputs?.sequentialNode; const model = nodeData.inputs?.model; const promptValuesStr = nodeData.inputs?.promptValues; const output = nodeData.outputs?.output; const llmStructuredOutput = nodeData.inputs?.llmStructuredOutput; if (!llmNodeLabel) throw new Error('LLM Node name is required!'); const llmNodeName = llmNodeLabel.toLowerCase().replace(/\s/g, '_').trim(); if (!sequentialNodes || !sequentialNodes.length) throw new Error('Agent must have a predecessor!'); let llmNodeInputVariablesValues = {}; if (promptValuesStr) { try { llmNodeInputVariablesValues = typeof promptValuesStr === 'object' ? promptValuesStr : JSON.parse(promptValuesStr); } catch (exception) { throw new Error("Invalid JSON in the LLM Node's Prompt Input Values: " + exception); } } llmNodeInputVariablesValues = (0, utils_1.handleEscapeCharacters)(llmNodeInputVariablesValues, true); const startLLM = sequentialNodes[0].startLLM; const llm = model || startLLM; if (nodeData.inputs) nodeData.inputs.model = llm; const multiModalMessageContent = sequentialNodes[0]?.multiModalMessageContent || (await (0, commonUtils_1.processImageMessage)(llm, nodeData, options)); const abortControllerSignal = options.signal; const llmNodeInputVariables = (0, lodash_1.uniq)([...(0, utils_1.getInputVariables)(systemPrompt), ...(0, utils_1.getInputVariables)(humanPrompt)]); const missingInputVars = (0, lodash_1.difference)(llmNodeInputVariables, Object.keys(llmNodeInputVariablesValues)).join(' '); const allVariablesSatisfied = missingInputVars.length === 0; if (!allVariablesSatisfied) { const nodeInputVars = llmNodeInputVariables.join(' '); const providedInputVars = Object.keys(llmNodeInputVariablesValues).join(' '); throw new Error(`LLM Node input variables values are not provided! Required: ${nodeInputVars}, Provided: ${providedInputVars}. Missing: ${missingInputVars}`); } const workerNode = async (state, config) => { const bindModel = config.configurable?.bindModel?.[nodeData.id]; return await agentNode({ state, llm, agent: await createAgent(nodeData, options, llmNodeName, state, bindModel || llm, [...tools], systemPrompt, humanPrompt, multiModalMessageContent, llmNodeInputVariablesValues, llmStructuredOutput), name: llmNodeName, abortControllerSignal, nodeData, input, options }, config); }; const returnOutput = { id: nodeData.id, node: workerNode, name: llmNodeName, label: llmNodeLabel, type: 'llm', llm, startLLM, output, predecessorAgents: sequentialNodes, multiModalMessageContent, moderations: sequentialNodes[0]?.moderations }; return returnOutput; } } async function createAgent(nodeData, options, llmNodeName, state, llm, tools, systemPrompt, humanPrompt, multiModalMessageContent, llmNodeInputVariablesValues, llmStructuredOutput) { if (tools.length) { if (llm.bindTools === undefined) { throw new Error(`LLM Node only compatible with function calling models.`); } // @ts-ignore llm = llm.bindTools(tools); } if (llmStructuredOutput && llmStructuredOutput !== '[]') { try { const structuredOutput = zod_1.z.object((0, commonUtils_1.convertStructuredSchemaToZod)(llmStructuredOutput)); // @ts-ignore llm = llm.withStructuredOutput(structuredOutput); } catch (exception) { console.error(exception); } } const promptArrays = [new prompts_1.MessagesPlaceholder('messages')]; if (systemPrompt) promptArrays.unshift(['system', systemPrompt]); if (humanPrompt) promptArrays.push(['human', humanPrompt]); let prompt = prompts_1.ChatPromptTemplate.fromMessages(promptArrays); prompt = await (0, commonUtils_1.checkMessageHistory)(nodeData, options, prompt, promptArrays, systemPrompt); if (multiModalMessageContent.length) { const msg = prompts_1.HumanMessagePromptTemplate.fromTemplate([...multiModalMessageContent]); prompt.promptMessages.splice(1, 0, msg); } let chain; if (!llmNodeInputVariablesValues || !Object.keys(llmNodeInputVariablesValues).length) { chain = runnables_1.RunnableSequence.from([prompt, llm]).withConfig({ metadata: { sequentialNodeName: llmNodeName } }); } else { chain = runnables_1.RunnableSequence.from([ runnables_1.RunnablePassthrough.assign((0, commonUtils_1.transformObjectPropertyToFunction)(llmNodeInputVariablesValues, state)), prompt, llm ]).withConfig({ metadata: { sequentialNodeName: llmNodeName } }); } // @ts-ignore return chain; } async function agentNode({ state, llm, agent, name, abortControllerSignal, nodeData, input, options }, config) { try { if (abortControllerSignal.signal.aborted) { throw new Error('Aborted!'); } const historySelection = (nodeData.inputs?.conversationHistorySelection || 'all_messages'); // @ts-ignore state.messages = (0, commonUtils_1.filterConversationHistory)(historySelection, input, state); // @ts-ignore state.messages = (0, commonUtils_1.restructureMessages)(llm, state); let result = await agent.invoke({ ...state, signal: abortControllerSignal.signal }, config); const llmStructuredOutput = nodeData.inputs?.llmStructuredOutput; if (llmStructuredOutput && llmStructuredOutput !== '[]' && result.tool_calls && result.tool_calls.length) { let jsonResult = {}; for (const toolCall of result.tool_calls) { jsonResult = { ...jsonResult, ...toolCall.args }; } result = { ...jsonResult, additional_kwargs: { nodeId: nodeData.id } }; } if (nodeData.inputs?.updateStateMemoryUI || nodeData.inputs?.updateStateMemoryCode) { const returnedOutput = await getReturnOutput(nodeData, input, options, result, state); if (nodeData.inputs?.llmStructuredOutput && nodeData.inputs.llmStructuredOutput !== '[]') { const messages = [ new messages_1.AIMessage({ content: typeof result === 'object' ? JSON.stringify(result) : result, name, additional_kwargs: { nodeId: nodeData.id } }) ]; return { ...returnedOutput, messages }; } else { result.name = name; result.additional_kwargs = { ...result.additional_kwargs, nodeId: nodeData.id }; let outputContent = typeof result === 'string' ? result : result.content; result.content = (0, utils_1.extractOutputFromArray)(outputContent); return { ...returnedOutput, messages: [result] }; } } else { if (nodeData.inputs?.llmStructuredOutput && nodeData.inputs.llmStructuredOutput !== '[]') { const messages = [ new messages_1.AIMessage({ content: typeof result === 'object' ? JSON.stringify(result) : result, name, additional_kwargs: { nodeId: nodeData.id } }) ]; return { messages }; } else { result.name = name; result.additional_kwargs = { ...result.additional_kwargs, nodeId: nodeData.id }; let outputContent = typeof result === 'string' ? result : result.content; result.content = (0, utils_1.extractOutputFromArray)(outputContent); return { messages: [result] }; } } } catch (error) { throw new Error(error); } } const getReturnOutput = async (nodeData, input, options, output, state) => { const appDataSource = options.appDataSource; const databaseEntities = options.databaseEntities; const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`]; const updateStateMemoryUI = nodeData.inputs?.updateStateMemoryUI; const updateStateMemoryCode = nodeData.inputs?.updateStateMemoryCode; const updateStateMemory = nodeData.inputs?.updateStateMemory; const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'updateStateMemoryUI'; const variables = await (0, utils_1.getVars)(appDataSource, databaseEntities, nodeData, options); const flow = { chatflowId: options.chatflowid, sessionId: options.sessionId, chatId: options.chatId, input, output, state, vars: (0, utils_1.prepareSandboxVars)(variables) }; if (updateStateMemory && updateStateMemory !== 'updateStateMemoryUI' && updateStateMemory !== 'updateStateMemoryCode') { try { const parsedSchema = typeof updateStateMemory === 'string' ? JSON.parse(updateStateMemory) : updateStateMemory; const obj = {}; for (const sch of parsedSchema) { const key = sch.Key; if (!key) throw new Error(`Key is required`); let value = sch.Value; if (value.startsWith('$flow')) { value = (0, commonUtils_1.customGet)(flow, sch.Value.replace('$flow.', '')); } else if (value.startsWith('$vars')) { value = (0, commonUtils_1.customGet)(flow, sch.Value.replace('$', '')); } obj[key] = value; } return obj; } catch (e) { throw new Error(e); } } if (selectedTab === 'updateStateMemoryUI' && updateStateMemoryUI) { try { const parsedSchema = typeof updateStateMemoryUI === 'string' ? JSON.parse(updateStateMemoryUI) : updateStateMemoryUI; const obj = {}; for (const sch of parsedSchema) { const key = sch.key; if (!key) throw new Error(`Key is required`); let value = sch.value; if (value.startsWith('$flow')) { value = (0, commonUtils_1.customGet)(flow, sch.value.replace('$flow.', '')); } else if (value.startsWith('$vars')) { value = (0, commonUtils_1.customGet)(flow, sch.value.replace('$', '')); } obj[key] = value; } return obj; } catch (e) { throw new Error(e); } } else if (selectedTab === 'updateStateMemoryCode' && updateStateMemoryCode) { const vm = await (0, commonUtils_1.getVM)(appDataSource, databaseEntities, nodeData, options, flow); try { const response = await vm.run(`module.exports = async function() {${updateStateMemoryCode}}()`, __dirname); if (typeof response !== 'object') throw new Error('Return output must be an object'); return response; } catch (e) { throw new Error(e); } } }; module.exports = { nodeClass: LLMNode_SeqAgents }; //# sourceMappingURL=LLMNode.js.map