import { get } from 'lodash' import { z } from 'zod' import { DataSource } from 'typeorm' import { StructuredTool } from '@langchain/core/tools' import { ChatMistralAI } from '@langchain/mistralai' import { ChatAnthropic } from '@langchain/anthropic' import { Runnable, RunnableConfig, mergeConfigs } from '@langchain/core/runnables' import { AIMessage, BaseMessage, HumanMessage, MessageContentImageUrl, ToolMessage } from '@langchain/core/messages' import { BaseChatModel } from '@langchain/core/language_models/chat_models' import { addImagesToMessages, llmSupportsVision } from '../../src/multiModalUtils' import { ICommonObject, IDatabaseEntity, INodeData, ISeqAgentsState, IVisionChatModal, ConversationHistorySelection } from '../../src/Interface' import { getVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../src/utils' import { ChatPromptTemplate, BaseMessagePromptTemplateLike } from '@langchain/core/prompts' export const checkCondition = (input: string | number | undefined, condition: string, value: string | number = ''): boolean => { if (!input && condition === 'Is Empty') return true else if (!input) return false // Function to check if a string is a valid number const isNumericString = (str: string): boolean => /^-?\d*\.?\d+$/.test(str) // Function to convert input to number if possible const toNumber = (val: string | number): number => { if (typeof val === 'number') return val return isNumericString(val) ? parseFloat(val) : NaN } // Convert input and value to numbers const numInput = toNumber(input) const numValue = toNumber(value) // Helper function for numeric comparisons const numericCompare = (comp: (a: number, b: number) => boolean): boolean => { if (isNaN(numInput) || isNaN(numValue)) return false return comp(numInput, numValue) } // Helper function for string operations const stringCompare = (strInput: string | number, strValue: string | number, op: (a: string, b: string) => boolean): boolean => { return op(String(strInput), String(strValue)) } switch (condition) { // String conditions case 'Contains': return stringCompare(input, value, (a, b) => a.includes(b)) case 'Not Contains': return stringCompare(input, value, (a, b) => !a.includes(b)) case 'Start With': return stringCompare(input, value, (a, b) => a.startsWith(b)) case 'End With': return stringCompare(input, value, (a, b) => a.endsWith(b)) case 'Is': return String(input) === String(value) case 'Is Not': return String(input) !== String(value) case 'Is Empty': return String(input).trim().length === 0 case 'Is Not Empty': return String(input).trim().length > 0 // Numeric conditions case 'Greater Than': return numericCompare((a, b) => a > b) case 'Less Than': return numericCompare((a, b) => a < b) case 'Equal To': return numericCompare((a, b) => a === b) case 'Not Equal To': return numericCompare((a, b) => a !== b) case 'Greater Than or Equal To': return numericCompare((a, b) => a >= b) case 'Less Than or Equal To': return numericCompare((a, b) => a <= b) default: return false } } export const transformObjectPropertyToFunction = (obj: ICommonObject, state: ISeqAgentsState) => { const transformedObject: ICommonObject = {} for (const key in obj) { let value = obj[key] // get message from agent try { const parsedValue = JSON.parse(value) if (typeof parsedValue === 'object' && parsedValue.id) { const messageOutputs = ((state.messages as unknown as BaseMessage[]) ?? []).filter( (message) => message.additional_kwargs && message.additional_kwargs?.nodeId === parsedValue.id ) const messageOutput = messageOutputs[messageOutputs.length - 1] if (messageOutput) { // if messageOutput.content is a string, set value to the content if (typeof messageOutput.content === 'string') value = messageOutput.content // if messageOutput.content is an array else if (Array.isArray(messageOutput.content)) { if (messageOutput.content.length === 0) { throw new Error(`Message output content is an empty array for node ${parsedValue.id}`) } // Get the first element of the array const messageOutputContentFirstElement: any = messageOutput.content[0] if (typeof messageOutputContentFirstElement === 'string') value = messageOutputContentFirstElement // If messageOutputContentFirstElement is an object and has a text property, set value to the text property else if (typeof messageOutputContentFirstElement === 'object' && messageOutputContentFirstElement.text) value = messageOutputContentFirstElement.text // Otherwise, stringify the messageOutputContentFirstElement else value = JSON.stringify(messageOutputContentFirstElement) } } } } catch (e) { // do nothing } // get state value if (value.startsWith('$flow.state')) { value = customGet(state, value.replace('$flow.state.', '')) if (typeof value === 'object') value = JSON.stringify(value) } transformedObject[key] = () => value } return transformedObject } export const processImageMessage = async (llm: BaseChatModel, nodeData: INodeData, options: ICommonObject) => { let multiModalMessageContent: MessageContentImageUrl[] = [] if (llmSupportsVision(llm)) { const visionChatModel = llm as IVisionChatModal multiModalMessageContent = await addImagesToMessages(nodeData, options, llm.multiModalOption) if (multiModalMessageContent?.length) { visionChatModel.setVisionModel() } else { visionChatModel.revertToOriginalModel() } } return multiModalMessageContent } export const customGet = (obj: any, path: string) => { if (path.includes('[-1]')) { const parts = path.split('.') let result = obj for (let part of parts) { if (part.includes('[') && part.includes(']')) { const [name, indexPart] = part.split('[') const index = parseInt(indexPart.replace(']', '')) result = result[name] if (Array.isArray(result)) { if (index < 0) { result = result[result.length + index] } else { result = result[index] } } else { return undefined } } else { result = get(result, part) } if (result === undefined) { return undefined } } return result } else { return get(obj, path) } } export const convertStructuredSchemaToZod = (schema: string | object): ICommonObject => { try { const parsedSchema = typeof schema === 'string' ? JSON.parse(schema) : schema const zodObj: ICommonObject = {} for (const sch of parsedSchema) { if (sch.type === 'String') { zodObj[sch.key] = z.string().describe(sch.description) } else if (sch.type === 'String Array') { zodObj[sch.key] = z.array(z.string()).describe(sch.description) } else if (sch.type === 'Number') { zodObj[sch.key] = z.number().describe(sch.description) } else if (sch.type === 'Boolean') { zodObj[sch.key] = z.boolean().describe(sch.description) } else if (sch.type === 'Enum') { zodObj[sch.key] = z.enum(sch.enumValues.split(',').map((item: string) => item.trim())).describe(sch.description) } } return zodObj } catch (e) { throw new Error(e) } } /** * Filter the conversation history based on the selected option. * * @param historySelection - The selected history option. * @param input - The user input. * @param state - The current state of the sequential llm or agent node. */ export function filterConversationHistory( historySelection: ConversationHistorySelection, input: string, state: ISeqAgentsState ): BaseMessage[] { switch (historySelection) { case 'user_question': return [new HumanMessage(input)] case 'last_message': // @ts-ignore return state.messages?.length ? [state.messages[state.messages.length - 1] as BaseMessage] : [] case 'empty': return [] case 'all_messages': // @ts-ignore return (state.messages as BaseMessage[]) ?? [] default: throw new Error(`Unhandled conversationHistorySelection: ${historySelection}`) } } export const restructureMessages = (llm: BaseChatModel, state: ISeqAgentsState) => { const messages: BaseMessage[] = [] for (const message of state.messages as unknown as BaseMessage[]) { // Sometimes Anthropic can return a message with content types of array, ignore that EXECEPT when tool calls are present if ((message as any).tool_calls?.length && message.content !== '') { message.content = JSON.stringify(message.content) } if (typeof message.content === 'string') { messages.push(message) } } const isToolMessage = (message: BaseMessage) => message instanceof ToolMessage || message.constructor.name === 'ToolMessageChunk' const isAIMessage = (message: BaseMessage) => message instanceof AIMessage || message.constructor.name === 'AIMessageChunk' const isHumanMessage = (message: BaseMessage) => message instanceof HumanMessage || message.constructor.name === 'HumanMessageChunk' /* * MistralAI does not support: * 1.) Last message as AI Message or Tool Message * 2.) Tool Message followed by Human Message */ if (llm instanceof ChatMistralAI) { if (messages.length > 1) { for (let i = 0; i < messages.length; i++) { const message = messages[i] // If last message is denied Tool Message, add a new Human Message if (isToolMessage(message) && i === messages.length - 1 && message.additional_kwargs?.toolCallsDenied) { messages.push(new AIMessage({ content: `Tool calls got denied. Do you have other questions?` })) } else if (i + 1 < messages.length) { const nextMessage = messages[i + 1] const currentMessage = message // If current message is Tool Message and next message is Human Message, add AI Message between Tool and Human Message if (isToolMessage(currentMessage) && isHumanMessage(nextMessage)) { messages.splice(i + 1, 0, new AIMessage({ content: 'Tool calls executed' })) } // If last message is AI Message or Tool Message, add Human Message if (i + 1 === messages.length - 1 && (isAIMessage(nextMessage) || isToolMessage(nextMessage))) { messages.push(new HumanMessage({ content: nextMessage.content || 'Given the user question, answer user query' })) } } } } } else if (llm instanceof ChatAnthropic) { /* * Anthropic does not support first message as AI Message */ if (messages.length) { const firstMessage = messages[0] if (isAIMessage(firstMessage)) { messages.shift() messages.unshift(new HumanMessage({ ...firstMessage })) } } } return messages } export class ExtractTool extends StructuredTool { name = 'extract' description = 'Extract structured data from the output' schema constructor(fields: ICommonObject) { super() this.schema = fields.schema } async _call(input: any) { return JSON.stringify(input) } } export interface RunnableCallableArgs extends Partial { name?: string func: (...args: any[]) => any tags?: string[] trace?: boolean recurse?: boolean } export interface MessagesState { messages: BaseMessage[] } export class RunnableCallable extends Runnable { lc_namespace: string[] = ['langgraph'] func: (...args: any[]) => any tags?: string[] config?: RunnableConfig trace: boolean = true recurse: boolean = true constructor(fields: RunnableCallableArgs) { super() this.name = fields.name ?? fields.func.name this.func = fields.func this.config = fields.tags ? { tags: fields.tags } : undefined this.trace = fields.trace ?? this.trace this.recurse = fields.recurse ?? this.recurse if (fields.metadata) { this.config = { ...this.config, metadata: { ...this.config, ...fields.metadata } } } } async invoke(input: any, options?: Partial | undefined): Promise { if (this.func === undefined) { return this.invoke(input, options) } let returnValue: any if (this.trace) { returnValue = await this._callWithConfig(this.func, input, mergeConfigs(this.config, options)) } else { returnValue = await this.func(input, mergeConfigs(this.config, options)) } if (returnValue instanceof Runnable && this.recurse) { return await returnValue.invoke(input, options) } return returnValue } } export const checkMessageHistory = async ( nodeData: INodeData, options: ICommonObject, prompt: ChatPromptTemplate, promptArrays: BaseMessagePromptTemplateLike[], sysPrompt: string ) => { const messageHistory = nodeData.inputs?.messageHistory if (messageHistory) { const appDataSource = options.appDataSource as DataSource const databaseEntities = options.databaseEntities as IDatabaseEntity const variables = await getVars(appDataSource, databaseEntities, nodeData, options) const flow = { chatflowId: options.chatflowid, sessionId: options.sessionId, chatId: options.chatId } const sandbox = createCodeExecutionSandbox('', variables, flow) try { const response = await executeJavaScriptCode(messageHistory, sandbox, { timeout: 10000 }) if (!Array.isArray(response)) throw new Error('Returned message history must be an array') if (sysPrompt) { // insert at index 1 promptArrays.splice(1, 0, ...response) } else { promptArrays.unshift(...response) } prompt = ChatPromptTemplate.fromMessages(promptArrays) } catch (e) { throw new Error(e) } } return prompt }