Feature/seq agents (#2798)
* update build functions * sequential agents * update langchain to 0.2, added sequential agent nodes * add marketplace templates * update howto wordings * Merge branch 'main' into feature/Seq-Agents # Conflicts: # pnpm-lock.yaml * update deprecated functions and add new sequential nodes * add marketplace templates * update marketplace templates, add structured output to llm node * add multi agents template * update llm node with bindmodels * update cypress version * update templates sticky note wordings * update tool node to include human in loop action * update structured outputs error from models * update cohere package to resolve google genai pipeThrough bug * update mistral package version, added message reconstruction before invoke seq agent * add HITL to agent * update state messages restructuring * update load and split methods for s3 directory
@@ -0,0 +1,886 @@
|
||||
import { flatten, uniq } from 'lodash'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { RunnableSequence, RunnablePassthrough, RunnableConfig } from '@langchain/core/runnables'
|
||||
import { ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, BaseMessagePromptTemplateLike } from '@langchain/core/prompts'
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
||||
import { AIMessage, AIMessageChunk, BaseMessage, HumanMessage, ToolMessage } from '@langchain/core/messages'
|
||||
import { formatToOpenAIToolMessages } from 'langchain/agents/format_scratchpad/openai_tools'
|
||||
import { type ToolsAgentStep } from 'langchain/agents/openai/output_parser'
|
||||
import { StringOutputParser } from '@langchain/core/output_parsers'
|
||||
import {
|
||||
INode,
|
||||
INodeData,
|
||||
INodeParams,
|
||||
ISeqAgentsState,
|
||||
ICommonObject,
|
||||
MessageContentImageUrl,
|
||||
INodeOutputsValue,
|
||||
ISeqAgentNode,
|
||||
IDatabaseEntity,
|
||||
IUsedTool,
|
||||
IDocument
|
||||
} from '../../../src/Interface'
|
||||
import { ToolCallingAgentOutputParser, AgentExecutor, SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents'
|
||||
import { getInputVariables, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils'
|
||||
import {
|
||||
customGet,
|
||||
getVM,
|
||||
processImageMessage,
|
||||
transformObjectPropertyToFunction,
|
||||
restructureMessages,
|
||||
MessagesState,
|
||||
RunnableCallable
|
||||
} from '../commonUtils'
|
||||
import { END, StateGraph } from '@langchain/langgraph'
|
||||
import { StructuredTool } from '@langchain/core/tools'
|
||||
|
||||
const defaultApprovalPrompt = `You are about to execute tool: {tools}. Ask if user want to proceed`
|
||||
const examplePrompt = 'You are a research assistant who can search for up-to-date info using search engine.'
|
||||
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 agent'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?",
|
||||
"usedTools": [
|
||||
{
|
||||
"tool": "tool-name",
|
||||
"toolInput": "{foo: var}",
|
||||
"toolOutput": "This is the tool's output"
|
||||
}
|
||||
],
|
||||
"sourceDocuments": [
|
||||
{
|
||||
"pageContent": "This is the page content",
|
||||
"metadata": "{foo: var}",
|
||||
}
|
||||
],
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
For example, if the \`toolOutput\` is the value you want to update the state with, you can return the following:
|
||||
\`\`\`js
|
||||
return {
|
||||
"user": $flow.output.usedTools[0].toolOutput
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
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 agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure:
|
||||
\`\`\`json
|
||||
{
|
||||
"output": "Hello! How can I assist you today?",
|
||||
"usedTools": [
|
||||
{
|
||||
"tool": "tool-name",
|
||||
"toolInput": "{foo: var}",
|
||||
"toolOutput": "This is the tool's output"
|
||||
}
|
||||
],
|
||||
"sourceDocuments": [
|
||||
{
|
||||
"pageContent": "This is the page content",
|
||||
"metadata": "{foo: var}",
|
||||
}
|
||||
],
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
For example, if the \`toolOutput\` is the value you want to update the state with, you can do the following:
|
||||
| Key | Value |
|
||||
|-----------|-------------------------------------------|
|
||||
| user | \`$flow.output.usedTools[0].toolOutput\` |
|
||||
|
||||
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 TAB_IDENTIFIER = 'selectedUpdateStateMemoryTab'
|
||||
|
||||
class Agent_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
inputs?: INodeParams[]
|
||||
badge?: string
|
||||
outputs: INodeOutputsValue[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'Agent'
|
||||
this.name = 'seqAgent'
|
||||
this.version = 1.0
|
||||
this.type = 'Agent'
|
||||
this.icon = 'seqAgent.png'
|
||||
this.category = 'Sequential Agents'
|
||||
this.description = 'Agent that can execute tools'
|
||||
this.baseClasses = [this.type]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Agent Name',
|
||||
name: 'agentName',
|
||||
type: 'string',
|
||||
placeholder: 'Agent'
|
||||
},
|
||||
{
|
||||
label: 'System Prompt',
|
||||
name: 'systemMessagePrompt',
|
||||
type: 'string',
|
||||
rows: 4,
|
||||
optional: true,
|
||||
default: examplePrompt
|
||||
},
|
||||
{
|
||||
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: 'Tools',
|
||||
name: 'tools',
|
||||
type: 'Tool',
|
||||
list: true
|
||||
},
|
||||
{
|
||||
label: 'Start | Agent | LLM | Tool Node',
|
||||
name: 'sequentialNode',
|
||||
type: 'Start | Agent | LLMNode | ToolNode',
|
||||
list: true
|
||||
},
|
||||
{
|
||||
label: 'Chat Model',
|
||||
name: 'model',
|
||||
type: 'BaseChatModel',
|
||||
optional: true,
|
||||
description: `Overwrite model to be used for this agent`
|
||||
},
|
||||
{
|
||||
label: 'Require Approval',
|
||||
name: 'interrupt',
|
||||
description: 'Require approval before executing tools. Will proceed when tools are not called',
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
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
|
||||
},
|
||||
{
|
||||
label: 'Approval Prompt',
|
||||
name: 'approvalPrompt',
|
||||
description: 'Prompt for approval. Only applicable if "Require Approval" is enabled',
|
||||
type: 'string',
|
||||
default: defaultApprovalPrompt,
|
||||
rows: 4,
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
},
|
||||
{
|
||||
label: 'Approve Button Text',
|
||||
name: 'approveButtonText',
|
||||
description: 'Text for approve button. Only applicable if "Require Approval" is enabled',
|
||||
type: 'string',
|
||||
default: 'Yes',
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
},
|
||||
{
|
||||
label: 'Reject Button Text',
|
||||
name: 'rejectButtonText',
|
||||
description: 'Text for reject button. Only applicable if "Require Approval" is enabled',
|
||||
type: 'string',
|
||||
default: 'No',
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
},
|
||||
{
|
||||
label: 'Update State',
|
||||
name: 'updateStateMemory',
|
||||
type: 'tabs',
|
||||
tabIdentifier: TAB_IDENTIFIER,
|
||||
additionalParams: true,
|
||||
default: 'updateStateMemoryUI',
|
||||
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: 'Agent Output (string)',
|
||||
value: '$flow.output.content'
|
||||
},
|
||||
{
|
||||
label: `Used Tools (array)`,
|
||||
value: '$flow.output.usedTools'
|
||||
},
|
||||
{
|
||||
label: `First Tool Output (string)`,
|
||||
value: '$flow.output.usedTools[0].toolOutput'
|
||||
},
|
||||
{
|
||||
label: 'Source Documents (array)',
|
||||
value: '$flow.output.sourceDocuments'
|
||||
},
|
||||
{
|
||||
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
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Max Iterations',
|
||||
name: 'maxIterations',
|
||||
type: 'number',
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
|
||||
let tools = nodeData.inputs?.tools
|
||||
tools = flatten(tools)
|
||||
let agentSystemPrompt = nodeData.inputs?.systemMessagePrompt as string
|
||||
let agentHumanPrompt = nodeData.inputs?.humanMessagePrompt as string
|
||||
const agentLabel = nodeData.inputs?.agentName as string
|
||||
const sequentialNodes = nodeData.inputs?.sequentialNode as ISeqAgentNode[]
|
||||
const maxIterations = nodeData.inputs?.maxIterations as string
|
||||
const model = nodeData.inputs?.model as BaseChatModel
|
||||
const promptValuesStr = nodeData.inputs?.promptValues
|
||||
const output = nodeData.outputs?.output as string
|
||||
const approvalPrompt = nodeData.inputs?.approvalPrompt as string
|
||||
|
||||
if (!agentLabel) throw new Error('Agent name is required!')
|
||||
const agentName = agentLabel.toLowerCase().replace(/\s/g, '_').trim()
|
||||
|
||||
if (!sequentialNodes || !sequentialNodes.length) throw new Error('Agent must have a predecessor!')
|
||||
|
||||
let agentInputVariablesValues: ICommonObject = {}
|
||||
if (promptValuesStr) {
|
||||
try {
|
||||
agentInputVariablesValues = typeof promptValuesStr === 'object' ? promptValuesStr : JSON.parse(promptValuesStr)
|
||||
} catch (exception) {
|
||||
throw new Error("Invalid JSON in the Agent's Prompt Input Values: " + exception)
|
||||
}
|
||||
}
|
||||
agentInputVariablesValues = handleEscapeCharacters(agentInputVariablesValues, true)
|
||||
|
||||
const startLLM = sequentialNodes[0].startLLM
|
||||
const llm = model || startLLM
|
||||
if (nodeData.inputs) nodeData.inputs.model = llm
|
||||
|
||||
const multiModalMessageContent = sequentialNodes[0]?.multiModalMessageContent || (await processImageMessage(llm, nodeData, options))
|
||||
const abortControllerSignal = options.signal as AbortController
|
||||
const agentInputVariables = uniq([...getInputVariables(agentSystemPrompt), ...getInputVariables(agentHumanPrompt)])
|
||||
|
||||
if (!agentInputVariables.every((element) => Object.keys(agentInputVariablesValues).includes(element))) {
|
||||
throw new Error('Agent input variables values are not provided!')
|
||||
}
|
||||
|
||||
const interrupt = nodeData.inputs?.interrupt as boolean
|
||||
|
||||
const toolName = `tool_${nodeData.id}`
|
||||
const toolNode = new ToolNode(tools, nodeData, input, options, toolName, [], { sequentialNodeName: toolName })
|
||||
|
||||
;(toolNode as any).seekPermissionMessage = async (usedTools: IUsedTool[]) => {
|
||||
const prompt = ChatPromptTemplate.fromMessages([['human', approvalPrompt || defaultApprovalPrompt]])
|
||||
const chain = prompt.pipe(startLLM)
|
||||
const response = (await chain.invoke({
|
||||
input: 'Hello there!',
|
||||
tools: JSON.stringify(usedTools)
|
||||
})) as AIMessageChunk
|
||||
return response.content
|
||||
}
|
||||
|
||||
const workerNode = async (state: ISeqAgentsState, config: RunnableConfig) => {
|
||||
return await agentNode(
|
||||
{
|
||||
state,
|
||||
llm,
|
||||
interrupt,
|
||||
agent: await createAgent(
|
||||
agentName,
|
||||
state,
|
||||
llm,
|
||||
interrupt,
|
||||
[...tools],
|
||||
agentSystemPrompt,
|
||||
agentHumanPrompt,
|
||||
multiModalMessageContent,
|
||||
agentInputVariablesValues,
|
||||
maxIterations,
|
||||
{
|
||||
sessionId: options.sessionId,
|
||||
chatId: options.chatId,
|
||||
input
|
||||
}
|
||||
),
|
||||
name: agentName,
|
||||
abortControllerSignal,
|
||||
nodeData,
|
||||
input,
|
||||
options
|
||||
},
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
const toolInterrupt = async (
|
||||
graph: StateGraph<any>,
|
||||
nextNodeName?: string,
|
||||
runCondition?: any,
|
||||
conditionalMapping: ICommonObject = {}
|
||||
) => {
|
||||
const routeMessage = async (state: ISeqAgentsState) => {
|
||||
const messages = state.messages as unknown as BaseMessage[]
|
||||
const lastMessage = messages[messages.length - 1] as AIMessage
|
||||
|
||||
if (!lastMessage?.tool_calls?.length) {
|
||||
// if next node is condition node, run the condition
|
||||
if (runCondition) {
|
||||
const returnNodeName = await runCondition(state)
|
||||
return returnNodeName
|
||||
}
|
||||
return nextNodeName || END
|
||||
}
|
||||
return toolName
|
||||
}
|
||||
|
||||
graph.addNode(toolName, toolNode)
|
||||
|
||||
if (nextNodeName) {
|
||||
// @ts-ignore
|
||||
graph.addConditionalEdges(agentName, routeMessage, {
|
||||
[toolName]: toolName,
|
||||
[END]: END,
|
||||
[nextNodeName]: nextNodeName,
|
||||
...conditionalMapping
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore
|
||||
graph.addConditionalEdges(agentName, routeMessage, { [toolName]: toolName, [END]: END, ...conditionalMapping })
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
graph.addEdge(toolName, agentName)
|
||||
|
||||
return graph
|
||||
}
|
||||
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: workerNode,
|
||||
name: agentName,
|
||||
label: agentLabel,
|
||||
type: 'agent',
|
||||
llm,
|
||||
startLLM,
|
||||
output,
|
||||
predecessorAgents: sequentialNodes,
|
||||
multiModalMessageContent,
|
||||
moderations: sequentialNodes[0]?.moderations,
|
||||
agentInterruptToolNode: interrupt ? toolNode : undefined,
|
||||
agentInterruptToolFunc: interrupt ? toolInterrupt : undefined
|
||||
}
|
||||
|
||||
return returnOutput
|
||||
}
|
||||
}
|
||||
|
||||
async function createAgent(
|
||||
agentName: string,
|
||||
state: ISeqAgentsState,
|
||||
llm: BaseChatModel,
|
||||
interrupt: boolean,
|
||||
tools: any[],
|
||||
systemPrompt: string,
|
||||
humanPrompt: string,
|
||||
multiModalMessageContent: MessageContentImageUrl[],
|
||||
agentInputVariablesValues: ICommonObject,
|
||||
maxIterations?: string,
|
||||
flowObj?: { sessionId?: string; chatId?: string; input?: string }
|
||||
): Promise<any> {
|
||||
if (tools.length && !interrupt) {
|
||||
const promptArrays = [
|
||||
new MessagesPlaceholder('messages'),
|
||||
new MessagesPlaceholder('agent_scratchpad')
|
||||
] as BaseMessagePromptTemplateLike[]
|
||||
if (systemPrompt) promptArrays.unshift(['system', systemPrompt])
|
||||
if (humanPrompt) promptArrays.push(['human', humanPrompt])
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages(promptArrays)
|
||||
|
||||
if (multiModalMessageContent.length) {
|
||||
const msg = HumanMessagePromptTemplate.fromTemplate([...multiModalMessageContent])
|
||||
prompt.promptMessages.splice(1, 0, msg)
|
||||
}
|
||||
|
||||
if (llm.bindTools === undefined) {
|
||||
throw new Error(`This agent only compatible with function calling models.`)
|
||||
}
|
||||
const modelWithTools = llm.bindTools(tools)
|
||||
|
||||
let agent
|
||||
|
||||
if (!agentInputVariablesValues || !Object.keys(agentInputVariablesValues).length) {
|
||||
agent = RunnableSequence.from([
|
||||
RunnablePassthrough.assign({
|
||||
//@ts-ignore
|
||||
agent_scratchpad: (input: { steps: ToolsAgentStep[] }) => formatToOpenAIToolMessages(input.steps)
|
||||
}),
|
||||
prompt,
|
||||
modelWithTools,
|
||||
new ToolCallingAgentOutputParser()
|
||||
]).withConfig({
|
||||
metadata: { sequentialNodeName: agentName }
|
||||
})
|
||||
} else {
|
||||
agent = RunnableSequence.from([
|
||||
RunnablePassthrough.assign({
|
||||
//@ts-ignore
|
||||
agent_scratchpad: (input: { steps: ToolsAgentStep[] }) => formatToOpenAIToolMessages(input.steps)
|
||||
}),
|
||||
RunnablePassthrough.assign(transformObjectPropertyToFunction(agentInputVariablesValues, state)),
|
||||
prompt,
|
||||
modelWithTools,
|
||||
new ToolCallingAgentOutputParser()
|
||||
]).withConfig({
|
||||
metadata: { sequentialNodeName: agentName }
|
||||
})
|
||||
}
|
||||
|
||||
const executor = AgentExecutor.fromAgentAndTools({
|
||||
agent,
|
||||
tools,
|
||||
sessionId: flowObj?.sessionId,
|
||||
chatId: flowObj?.chatId,
|
||||
input: flowObj?.input,
|
||||
verbose: process.env.DEBUG === 'true' ? true : false,
|
||||
maxIterations: maxIterations ? parseFloat(maxIterations) : undefined
|
||||
})
|
||||
return executor
|
||||
} else if (tools.length && interrupt) {
|
||||
if (llm.bindTools === undefined) {
|
||||
throw new Error(`Agent Node only compatible with function calling models.`)
|
||||
}
|
||||
// @ts-ignore
|
||||
llm = llm.bindTools(tools)
|
||||
|
||||
const promptArrays = [new MessagesPlaceholder('messages')] as BaseMessagePromptTemplateLike[]
|
||||
if (systemPrompt) promptArrays.unshift(['system', systemPrompt])
|
||||
if (humanPrompt) promptArrays.push(['human', humanPrompt])
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages(promptArrays)
|
||||
if (multiModalMessageContent.length) {
|
||||
const msg = HumanMessagePromptTemplate.fromTemplate([...multiModalMessageContent])
|
||||
prompt.promptMessages.splice(1, 0, msg)
|
||||
}
|
||||
|
||||
let agent
|
||||
|
||||
if (!agentInputVariablesValues || !Object.keys(agentInputVariablesValues).length) {
|
||||
agent = RunnableSequence.from([prompt, llm]).withConfig({
|
||||
metadata: { sequentialNodeName: agentName }
|
||||
})
|
||||
} else {
|
||||
agent = RunnableSequence.from([
|
||||
RunnablePassthrough.assign(transformObjectPropertyToFunction(agentInputVariablesValues, state)),
|
||||
prompt,
|
||||
llm
|
||||
]).withConfig({
|
||||
metadata: { sequentialNodeName: agentName }
|
||||
})
|
||||
}
|
||||
return agent
|
||||
} else {
|
||||
const promptArrays = [new MessagesPlaceholder('messages')] as BaseMessagePromptTemplateLike[]
|
||||
if (systemPrompt) promptArrays.unshift(['system', systemPrompt])
|
||||
if (humanPrompt) promptArrays.push(['human', humanPrompt])
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages(promptArrays)
|
||||
|
||||
if (multiModalMessageContent.length) {
|
||||
const msg = HumanMessagePromptTemplate.fromTemplate([...multiModalMessageContent])
|
||||
prompt.promptMessages.splice(1, 0, msg)
|
||||
}
|
||||
|
||||
let conversationChain
|
||||
|
||||
if (!agentInputVariablesValues || !Object.keys(agentInputVariablesValues).length) {
|
||||
conversationChain = RunnableSequence.from([prompt, llm, new StringOutputParser()]).withConfig({
|
||||
metadata: { sequentialNodeName: agentName }
|
||||
})
|
||||
} else {
|
||||
conversationChain = RunnableSequence.from([
|
||||
RunnablePassthrough.assign(transformObjectPropertyToFunction(agentInputVariablesValues, state)),
|
||||
prompt,
|
||||
llm,
|
||||
new StringOutputParser()
|
||||
]).withConfig({
|
||||
metadata: { sequentialNodeName: agentName }
|
||||
})
|
||||
}
|
||||
|
||||
return conversationChain
|
||||
}
|
||||
}
|
||||
|
||||
async function agentNode(
|
||||
{
|
||||
state,
|
||||
llm,
|
||||
interrupt,
|
||||
agent,
|
||||
name,
|
||||
abortControllerSignal,
|
||||
nodeData,
|
||||
input,
|
||||
options
|
||||
}: {
|
||||
state: ISeqAgentsState
|
||||
llm: BaseChatModel
|
||||
interrupt: boolean
|
||||
agent: AgentExecutor | RunnableSequence
|
||||
name: string
|
||||
abortControllerSignal: AbortController
|
||||
nodeData: INodeData
|
||||
input: string
|
||||
options: ICommonObject
|
||||
},
|
||||
config: RunnableConfig
|
||||
) {
|
||||
try {
|
||||
if (abortControllerSignal.signal.aborted) {
|
||||
throw new Error('Aborted!')
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
state.messages = restructureMessages(llm, state)
|
||||
|
||||
let result = await agent.invoke({ ...state, signal: abortControllerSignal.signal }, config)
|
||||
|
||||
if (interrupt) {
|
||||
const messages = state.messages as unknown as BaseMessage[]
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
|
||||
// If the last message is a tool message and is an interrupted message, format output into standard agent output
|
||||
if (lastMessage._getType() === 'tool' && lastMessage.additional_kwargs?.nodeId === nodeData.id) {
|
||||
let formattedAgentResult: { output?: string; usedTools?: IUsedTool[]; sourceDocuments?: IDocument[] } = {}
|
||||
formattedAgentResult.output = result.content
|
||||
if (lastMessage.additional_kwargs?.usedTools) {
|
||||
formattedAgentResult.usedTools = lastMessage.additional_kwargs.usedTools as IUsedTool[]
|
||||
}
|
||||
if (lastMessage.additional_kwargs?.sourceDocuments) {
|
||||
formattedAgentResult.sourceDocuments = lastMessage.additional_kwargs.sourceDocuments as IDocument[]
|
||||
}
|
||||
result = formattedAgentResult
|
||||
} else {
|
||||
result.name = name
|
||||
result.additional_kwargs = { ...result.additional_kwargs, nodeId: nodeData.id, interrupt: true }
|
||||
return {
|
||||
messages: [result]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const additional_kwargs: ICommonObject = { nodeId: nodeData.id }
|
||||
|
||||
if (result.usedTools) {
|
||||
additional_kwargs.usedTools = result.usedTools
|
||||
}
|
||||
if (result.sourceDocuments) {
|
||||
additional_kwargs.sourceDocuments = result.sourceDocuments
|
||||
}
|
||||
if (result.output) {
|
||||
result.content = result.output
|
||||
delete result.output
|
||||
}
|
||||
|
||||
const outputContent = typeof result === 'string' ? result : result.content || result.output
|
||||
|
||||
if (nodeData.inputs?.updateStateMemoryUI || nodeData.inputs?.updateStateMemoryCode) {
|
||||
let formattedOutput = {
|
||||
...result,
|
||||
content: outputContent
|
||||
}
|
||||
const returnedOutput = await getReturnOutput(nodeData, input, options, formattedOutput, state)
|
||||
return {
|
||||
...returnedOutput,
|
||||
messages: convertCustomMessagesToBaseMessages([outputContent], name, additional_kwargs)
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
messages: [
|
||||
new HumanMessage({
|
||||
content: outputContent,
|
||||
name,
|
||||
additional_kwargs: Object.keys(additional_kwargs).length ? additional_kwargs : undefined
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getReturnOutput = async (nodeData: INodeData, input: string, options: ICommonObject, output: any, state: ISeqAgentsState) => {
|
||||
const appDataSource = options.appDataSource as DataSource
|
||||
const databaseEntities = options.databaseEntities as IDatabaseEntity
|
||||
const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string
|
||||
const updateStateMemoryUI = nodeData.inputs?.updateStateMemoryUI as string
|
||||
const updateStateMemoryCode = nodeData.inputs?.updateStateMemoryCode as string
|
||||
|
||||
const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'updateStateMemoryUI'
|
||||
const variables = await getVars(appDataSource, databaseEntities, nodeData)
|
||||
|
||||
const flow = {
|
||||
chatflowId: options.chatflowid,
|
||||
sessionId: options.sessionId,
|
||||
chatId: options.chatId,
|
||||
input,
|
||||
output,
|
||||
state,
|
||||
vars: prepareSandboxVars(variables)
|
||||
}
|
||||
|
||||
if (selectedTab === 'updateStateMemoryUI' && updateStateMemoryUI) {
|
||||
try {
|
||||
const parsedSchema = typeof updateStateMemoryUI === 'string' ? JSON.parse(updateStateMemoryUI) : updateStateMemoryUI
|
||||
const obj: ICommonObject = {}
|
||||
for (const sch of parsedSchema) {
|
||||
const key = sch.key
|
||||
if (!key) throw new Error(`Key is required`)
|
||||
let value = sch.value as string
|
||||
if (value.startsWith('$flow')) {
|
||||
value = customGet(flow, sch.value.replace('$flow.', ''))
|
||||
} else if (value.startsWith('$vars')) {
|
||||
value = customGet(flow, sch.value.replace('$', ''))
|
||||
}
|
||||
obj[key] = value
|
||||
}
|
||||
return obj
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
}
|
||||
} else if (selectedTab === 'updateStateMemoryCode' && updateStateMemoryCode) {
|
||||
const vm = await getVM(appDataSource, databaseEntities, nodeData, 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)
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
const convertCustomMessagesToBaseMessages = (messages: string[], name: string, additional_kwargs: ICommonObject) => {
|
||||
return messages.map((message) => {
|
||||
return new HumanMessage({
|
||||
content: message,
|
||||
name,
|
||||
additional_kwargs: Object.keys(additional_kwargs).length ? additional_kwargs : undefined
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
class ToolNode<T extends BaseMessage[] | MessagesState> extends RunnableCallable<T, T> {
|
||||
tools: StructuredTool[]
|
||||
nodeData: INodeData
|
||||
inputQuery: string
|
||||
options: ICommonObject
|
||||
|
||||
constructor(
|
||||
tools: StructuredTool[],
|
||||
nodeData: INodeData,
|
||||
inputQuery: string,
|
||||
options: ICommonObject,
|
||||
name: string = 'tools',
|
||||
tags: string[] = [],
|
||||
metadata: ICommonObject = {}
|
||||
) {
|
||||
super({ name, metadata, tags, func: (input, config) => this.run(input, config) })
|
||||
this.tools = tools
|
||||
this.nodeData = nodeData
|
||||
this.inputQuery = inputQuery
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private async run(input: BaseMessage[] | MessagesState, config: RunnableConfig): Promise<BaseMessage[] | MessagesState> {
|
||||
const message = Array.isArray(input) ? input[input.length - 1] : input.messages[input.messages.length - 1]
|
||||
|
||||
if (message._getType() !== 'ai') {
|
||||
throw new Error('ToolNode only accepts AIMessages as input.')
|
||||
}
|
||||
|
||||
const outputs = await Promise.all(
|
||||
(message as AIMessage).tool_calls?.map(async (call) => {
|
||||
const tool = this.tools.find((tool) => tool.name === call.name)
|
||||
if (tool === undefined) {
|
||||
throw new Error(`Tool ${call.name} not found.`)
|
||||
}
|
||||
let output = await tool.invoke(call.args, config)
|
||||
let sourceDocuments: Document[] = []
|
||||
if (output?.includes(SOURCE_DOCUMENTS_PREFIX)) {
|
||||
const outputArray = output.split(SOURCE_DOCUMENTS_PREFIX)
|
||||
output = outputArray[0]
|
||||
const docs = outputArray[1]
|
||||
try {
|
||||
sourceDocuments = JSON.parse(docs)
|
||||
} catch (e) {
|
||||
console.error('Error parsing source documents from tool')
|
||||
}
|
||||
}
|
||||
return new ToolMessage({
|
||||
name: tool.name,
|
||||
content: typeof output === 'string' ? output : JSON.stringify(output),
|
||||
tool_call_id: call.id!,
|
||||
additional_kwargs: {
|
||||
sourceDocuments,
|
||||
args: call.args,
|
||||
usedTools: [
|
||||
{
|
||||
tool: tool.name ?? '',
|
||||
toolInput: call.args,
|
||||
toolOutput: output
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}) ?? []
|
||||
)
|
||||
|
||||
const additional_kwargs: ICommonObject = { nodeId: this.nodeData.id }
|
||||
outputs.forEach((result) => (result.additional_kwargs = { ...result.additional_kwargs, ...additional_kwargs }))
|
||||
return Array.isArray(input) ? outputs : { messages: outputs }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: Agent_SeqAgents }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-user-circle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855" /></svg>
|
||||
|
After Width: | Height: | Size: 499 B |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,325 @@
|
||||
import { DataSource } from 'typeorm'
|
||||
import { BaseMessage } from '@langchain/core/messages'
|
||||
import {
|
||||
ICommonObject,
|
||||
IDatabaseEntity,
|
||||
INode,
|
||||
INodeData,
|
||||
INodeOutputsValue,
|
||||
INodeParams,
|
||||
ISeqAgentNode,
|
||||
ISeqAgentsState
|
||||
} from '../../../src/Interface'
|
||||
import { checkCondition, customGet, getVM } from '../commonUtils'
|
||||
import { getVars, prepareSandboxVars } from '../../../src/utils'
|
||||
|
||||
const howToUseCode = `
|
||||
1. Must return a string value at the end of function. For example:
|
||||
\`\`\`js
|
||||
if ("X" === "X") {
|
||||
return "Agent"; // connect to next agent node
|
||||
} else {
|
||||
return "End"; // connect to end node
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
2. In most cases, you would probably get the last message to do some comparison. You can get all current messages from the state: \`$flow.state.messages\`:
|
||||
\`\`\`json
|
||||
[
|
||||
{
|
||||
"content": "Hello! How can I assist you today?",
|
||||
"name": "",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {},
|
||||
"tool_calls": [],
|
||||
"invalid_tool_calls": [],
|
||||
"usage_metadata": {}
|
||||
}
|
||||
]
|
||||
\`\`\`
|
||||
|
||||
For example, to get the last message content:
|
||||
\`\`\`js
|
||||
const messages = $flow.state.messages;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
// Proceed to do something with the last message 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 state = $flow.state;
|
||||
|
||||
const messages = state.messages;
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
/* Check if the last message has content */
|
||||
if (lastMessage.content) {
|
||||
return "Agent";
|
||||
}
|
||||
|
||||
return "End";`
|
||||
|
||||
const TAB_IDENTIFIER = 'selectedConditionFunctionTab'
|
||||
|
||||
interface IConditionGridItem {
|
||||
variable: string
|
||||
operation: string
|
||||
value: string
|
||||
output: string
|
||||
}
|
||||
|
||||
class Condition_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
credential: INodeParams
|
||||
inputs: INodeParams[]
|
||||
outputs: INodeOutputsValue[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'Condition'
|
||||
this.name = 'seqCondition'
|
||||
this.version = 1.0
|
||||
this.type = 'Condition'
|
||||
this.icon = 'condition.svg'
|
||||
this.category = 'Sequential Agents'
|
||||
this.description = 'Conditional function to determine which route to take next'
|
||||
this.baseClasses = [this.type]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Condition Name',
|
||||
name: 'conditionName',
|
||||
type: 'string',
|
||||
optional: true,
|
||||
placeholder: 'If X, then Y'
|
||||
},
|
||||
{
|
||||
label: 'Start | Agent | LLM | Tool Node',
|
||||
name: 'sequentialNode',
|
||||
type: 'Start | Agent | LLMNode | ToolNode',
|
||||
list: true
|
||||
},
|
||||
{
|
||||
label: 'Condition',
|
||||
name: 'condition',
|
||||
type: 'conditionFunction', // This is a custom type to show as button on the UI and render anchor points when saved
|
||||
tabIdentifier: TAB_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
label: 'Condition (Table)',
|
||||
name: 'conditionUI',
|
||||
type: 'datagrid',
|
||||
description: 'If a condition is met, the node connected to the respective output will be executed',
|
||||
optional: true,
|
||||
datagrid: [
|
||||
{
|
||||
field: 'variable',
|
||||
headerName: 'Variable',
|
||||
type: 'freeSolo',
|
||||
editable: true,
|
||||
loadMethod: ['getPreviousMessages', 'loadStateKeys'],
|
||||
valueOptions: [
|
||||
{
|
||||
label: 'Total Messages (number)',
|
||||
value: '$flow.state.messages.length'
|
||||
},
|
||||
{
|
||||
label: 'First Message Content (string)',
|
||||
value: '$flow.state.messages[0].content'
|
||||
},
|
||||
{
|
||||
label: 'Last Message Content (string)',
|
||||
value: '$flow.state.messages[-1].content'
|
||||
},
|
||||
{
|
||||
label: `Global variable (string)`,
|
||||
value: '$vars.<variable-name>'
|
||||
}
|
||||
],
|
||||
flex: 0.5,
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
headerName: 'Operation',
|
||||
type: 'singleSelect',
|
||||
valueOptions: [
|
||||
'Contains',
|
||||
'Not Contains',
|
||||
'Start With',
|
||||
'End With',
|
||||
'Is',
|
||||
'Is Not',
|
||||
'Is Empty',
|
||||
'Is Not Empty',
|
||||
'Greater Than',
|
||||
'Less Than',
|
||||
'Equal To',
|
||||
'Not Equal To',
|
||||
'Greater Than or Equal To',
|
||||
'Less Than or Equal To'
|
||||
],
|
||||
editable: true,
|
||||
flex: 0.4,
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
headerName: 'Value',
|
||||
flex: 1,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
field: 'output',
|
||||
headerName: 'Output Name',
|
||||
editable: true,
|
||||
flex: 0.3,
|
||||
minWidth: 150
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Condition (Code)',
|
||||
name: 'conditionFunction',
|
||||
type: 'code',
|
||||
description: 'Function to evaluate the condition',
|
||||
hint: {
|
||||
label: 'How to use',
|
||||
value: howToUseCode
|
||||
},
|
||||
hideCodeExecute: true,
|
||||
codeExample: defaultFunc,
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
this.outputs = [
|
||||
{
|
||||
label: 'Next',
|
||||
name: 'next',
|
||||
baseClasses: ['Agent', 'LLMNode', 'ToolNode'],
|
||||
isAnchor: true
|
||||
},
|
||||
{
|
||||
label: 'End',
|
||||
name: 'end',
|
||||
baseClasses: ['Agent', 'LLMNode', 'ToolNode'],
|
||||
isAnchor: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
|
||||
const conditionLabel = nodeData.inputs?.conditionName as string
|
||||
const conditionName = conditionLabel.toLowerCase().replace(/\s/g, '_').trim()
|
||||
const output = nodeData.outputs?.output as string
|
||||
const sequentialNodes = nodeData.inputs?.sequentialNode as ISeqAgentNode[]
|
||||
|
||||
if (!sequentialNodes || !sequentialNodes.length) throw new Error('Condition must have a predecessor!')
|
||||
|
||||
const startLLM = sequentialNodes[0].startLLM
|
||||
|
||||
const conditionalEdge = async (state: ISeqAgentsState) => await runCondition(nodeData, input, options, state)
|
||||
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: conditionalEdge,
|
||||
name: conditionName,
|
||||
label: conditionLabel,
|
||||
type: 'condition',
|
||||
output,
|
||||
llm: startLLM,
|
||||
startLLM,
|
||||
multiModalMessageContent: sequentialNodes[0]?.multiModalMessageContent,
|
||||
predecessorAgents: sequentialNodes
|
||||
}
|
||||
|
||||
return returnOutput
|
||||
}
|
||||
}
|
||||
|
||||
const runCondition = async (nodeData: INodeData, input: string, options: ICommonObject, state: ISeqAgentsState) => {
|
||||
const appDataSource = options.appDataSource as DataSource
|
||||
const databaseEntities = options.databaseEntities as IDatabaseEntity
|
||||
const conditionUI = nodeData.inputs?.conditionUI as string
|
||||
const conditionFunction = nodeData.inputs?.conditionFunction as string
|
||||
const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string
|
||||
|
||||
const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'conditionUI'
|
||||
const variables = await getVars(appDataSource, databaseEntities, nodeData)
|
||||
|
||||
const flow = {
|
||||
chatflowId: options.chatflowid,
|
||||
sessionId: options.sessionId,
|
||||
chatId: options.chatId,
|
||||
input,
|
||||
state,
|
||||
vars: prepareSandboxVars(variables)
|
||||
}
|
||||
|
||||
if (selectedTab === 'conditionFunction' && conditionFunction) {
|
||||
const vm = await getVM(appDataSource, databaseEntities, nodeData, flow)
|
||||
try {
|
||||
const response = await vm.run(`module.exports = async function() {${conditionFunction}}()`, __dirname)
|
||||
if (typeof response !== 'string') throw new Error('Condition function must return a string')
|
||||
return response
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
}
|
||||
} else if (selectedTab === 'conditionUI' && conditionUI) {
|
||||
try {
|
||||
const conditionItems: IConditionGridItem[] = typeof conditionUI === 'string' ? JSON.parse(conditionUI) : conditionUI
|
||||
|
||||
for (const item of conditionItems) {
|
||||
if (!item.variable) throw new Error('Condition variable is required!')
|
||||
|
||||
if (item.variable.startsWith('$flow')) {
|
||||
const variableValue = customGet(flow, item.variable.replace('$flow.', ''))
|
||||
if (checkCondition(variableValue, item.operation, item.value)) {
|
||||
return item.output
|
||||
}
|
||||
} else if (item.variable.startsWith('$vars')) {
|
||||
const variableValue = customGet(flow, item.variable.replace('$', ''))
|
||||
if (checkCondition(variableValue, item.operation, item.value)) {
|
||||
return item.output
|
||||
}
|
||||
} else if (item.variable.startsWith('$')) {
|
||||
const nodeId = item.variable.replace('$', '')
|
||||
|
||||
const messageOutputs = ((state.messages as unknown as BaseMessage[]) ?? []).filter(
|
||||
(message) => message.additional_kwargs && message.additional_kwargs?.nodeId === nodeId
|
||||
)
|
||||
const messageOutput = messageOutputs[messageOutputs.length - 1]
|
||||
|
||||
if (messageOutput) {
|
||||
if (checkCondition(messageOutput.content as string, item.operation, item.value)) {
|
||||
return item.output
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'End'
|
||||
} catch (exception) {
|
||||
throw new Error('Invalid Condition: ' + exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: Condition_SeqAgents }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrows-split"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M21 17h-8l-3.5 -5h-6.5" /><path d="M21 7h-8l-3.495 5" /><path d="M18 10l3 -3l-3 -3" /><path d="M18 20l3 -3l-3 -3" /></svg>
|
||||
|
After Width: | Height: | Size: 444 B |
@@ -0,0 +1,556 @@
|
||||
import { uniq } from 'lodash'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { z } from 'zod'
|
||||
import { BaseMessagePromptTemplateLike, ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts'
|
||||
import { RunnableSequence, RunnablePassthrough, RunnableConfig } from '@langchain/core/runnables'
|
||||
import { BaseMessage } from '@langchain/core/messages'
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
||||
import {
|
||||
ICommonObject,
|
||||
IDatabaseEntity,
|
||||
INode,
|
||||
INodeData,
|
||||
INodeOutputsValue,
|
||||
INodeParams,
|
||||
ISeqAgentNode,
|
||||
ISeqAgentsState
|
||||
} from '../../../src/Interface'
|
||||
import { getInputVariables, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils'
|
||||
import {
|
||||
ExtractTool,
|
||||
checkCondition,
|
||||
convertStructuredSchemaToZod,
|
||||
customGet,
|
||||
getVM,
|
||||
transformObjectPropertyToFunction,
|
||||
restructureMessages
|
||||
} from '../commonUtils'
|
||||
import { ChatGoogleGenerativeAI } from '../../chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI'
|
||||
|
||||
interface IConditionGridItem {
|
||||
variable: string
|
||||
operation: string
|
||||
value: string
|
||||
output: string
|
||||
}
|
||||
|
||||
const examplePrompt = `You are an expert customer support routing system.
|
||||
Your job is to detect whether a customer support representative is routing a user to the technical support team, or just responding conversationally.`
|
||||
|
||||
const exampleHumanPrompt = `The previous conversation is an interaction between a customer support representative and a user.
|
||||
Extract whether the representative is routing the user to the technical support team, or just responding conversationally.
|
||||
|
||||
If representative want to route the user to the technical support team, respond only with the word "TECHNICAL".
|
||||
Otherwise, respond only with the word "CONVERSATION".
|
||||
|
||||
Remember, only respond with one of the above words.`
|
||||
|
||||
const howToUseCode = `
|
||||
1. Must return a string value at the end of function. For example:
|
||||
\`\`\`js
|
||||
if ("X" === "X") {
|
||||
return "Agent"; // connect to next agent node
|
||||
} else {
|
||||
return "End"; // connect to end node
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
2. In most cases, you would probably get the last message to do some comparison. You can get all current messages from the state: \`$flow.state.messages\`:
|
||||
\`\`\`json
|
||||
[
|
||||
{
|
||||
"content": "Hello! How can I assist you today?",
|
||||
"name": "",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {},
|
||||
"tool_calls": [],
|
||||
"invalid_tool_calls": [],
|
||||
"usage_metadata": {}
|
||||
}
|
||||
]
|
||||
\`\`\`
|
||||
|
||||
For example, to get the last message content:
|
||||
\`\`\`js
|
||||
const messages = $flow.state.messages;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
// Proceed to do something with the last message content
|
||||
\`\`\`
|
||||
|
||||
3. If you want to use the Condition Agent's output for conditional checks, 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, we can check if the agent's output contains specific keyword:
|
||||
\`\`\`js
|
||||
const result = $flow.output.content;
|
||||
|
||||
if (result.includes("some-keyword")) {
|
||||
return "Agent"; // connect to next agent node
|
||||
} else {
|
||||
return "End"; // connect to end node
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
If Structured Output is enabled, \`$flow.output\` will be in the JSON format as defined in the Structured Output configuration:
|
||||
\`\`\`json
|
||||
{
|
||||
"foo": 'var'
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
4. You can get default flow config, including the current "state":
|
||||
- \`$flow.sessionId\`
|
||||
- \`$flow.chatId\`
|
||||
- \`$flow.chatflowId\`
|
||||
- \`$flow.input\`
|
||||
- \`$flow.state\`
|
||||
|
||||
5. You can get custom variables: \`$vars.<variable-name>\`
|
||||
|
||||
`
|
||||
|
||||
const defaultFunc = `const result = $flow.output.content;
|
||||
|
||||
if (result.includes("some-keyword")) {
|
||||
return "Agent";
|
||||
}
|
||||
|
||||
return "End";
|
||||
`
|
||||
|
||||
const TAB_IDENTIFIER = 'selectedConditionFunctionTab'
|
||||
|
||||
class ConditionAgent_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
credential: INodeParams
|
||||
inputs: INodeParams[]
|
||||
outputs: INodeOutputsValue[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'Condition Agent'
|
||||
this.name = 'seqConditionAgent'
|
||||
this.version = 1.0
|
||||
this.type = 'ConditionAgent'
|
||||
this.icon = 'condition.svg'
|
||||
this.category = 'Sequential Agents'
|
||||
this.description = 'Uses an agent to determine which route to take next'
|
||||
this.baseClasses = [this.type]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Name',
|
||||
name: 'conditionAgentName',
|
||||
type: 'string',
|
||||
placeholder: 'Condition Agent'
|
||||
},
|
||||
{
|
||||
label: 'Start | Agent | LLM | Tool Node',
|
||||
name: 'sequentialNode',
|
||||
type: 'Start | Agent | LLMNode | ToolNode',
|
||||
list: true
|
||||
},
|
||||
{
|
||||
label: 'Chat Model',
|
||||
name: 'model',
|
||||
type: 'BaseChatModel',
|
||||
optional: true,
|
||||
description: `Overwrite model to be used for this agent`
|
||||
},
|
||||
{
|
||||
label: 'System Prompt',
|
||||
name: 'systemMessagePrompt',
|
||||
type: 'string',
|
||||
rows: 4,
|
||||
default: examplePrompt,
|
||||
additionalParams: true,
|
||||
optional: 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,
|
||||
default: exampleHumanPrompt,
|
||||
additionalParams: true,
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
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: 'conditionAgentStructuredOutput',
|
||||
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: 'Condition',
|
||||
name: 'condition',
|
||||
type: 'conditionFunction', // This is a custom type to show as button on the UI and render anchor points when saved
|
||||
tabIdentifier: TAB_IDENTIFIER,
|
||||
tabs: [
|
||||
{
|
||||
label: 'Condition (Table)',
|
||||
name: 'conditionUI',
|
||||
type: 'datagrid',
|
||||
description: 'If a condition is met, the node connected to the respective output will be executed',
|
||||
optional: true,
|
||||
datagrid: [
|
||||
{
|
||||
field: 'variable',
|
||||
headerName: 'Variable',
|
||||
type: 'freeSolo',
|
||||
editable: true,
|
||||
loadMethod: ['getPreviousMessages', 'loadStateKeys'],
|
||||
valueOptions: [
|
||||
{
|
||||
label: 'Agent Output (string)',
|
||||
value: '$flow.output.content'
|
||||
},
|
||||
{
|
||||
label: `Agent's JSON Key Output (string)`,
|
||||
value: '$flow.output.<replace-with-key>'
|
||||
},
|
||||
{
|
||||
label: 'Total Messages (number)',
|
||||
value: '$flow.state.messages.length'
|
||||
},
|
||||
{
|
||||
label: 'First Message Content (string)',
|
||||
value: '$flow.state.messages[0].content'
|
||||
},
|
||||
{
|
||||
label: 'Last Message Content (string)',
|
||||
value: '$flow.state.messages[-1].content'
|
||||
},
|
||||
{
|
||||
label: `Global variable (string)`,
|
||||
value: '$vars.<variable-name>'
|
||||
}
|
||||
],
|
||||
flex: 0.5,
|
||||
minWidth: 200
|
||||
},
|
||||
{
|
||||
field: 'operation',
|
||||
headerName: 'Operation',
|
||||
type: 'singleSelect',
|
||||
valueOptions: [
|
||||
'Contains',
|
||||
'Not Contains',
|
||||
'Start With',
|
||||
'End With',
|
||||
'Is',
|
||||
'Is Not',
|
||||
'Is Empty',
|
||||
'Is Not Empty',
|
||||
'Greater Than',
|
||||
'Less Than',
|
||||
'Equal To',
|
||||
'Not Equal To',
|
||||
'Greater Than or Equal To',
|
||||
'Less Than or Equal To'
|
||||
],
|
||||
editable: true,
|
||||
flex: 0.4,
|
||||
minWidth: 150
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
headerName: 'Value',
|
||||
flex: 1,
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
field: 'output',
|
||||
headerName: 'Output Name',
|
||||
editable: true,
|
||||
flex: 0.3,
|
||||
minWidth: 150
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Condition (Code)',
|
||||
name: 'conditionFunction',
|
||||
type: 'code',
|
||||
description: 'Function to evaluate the condition',
|
||||
hint: {
|
||||
label: 'How to use',
|
||||
value: howToUseCode
|
||||
},
|
||||
hideCodeExecute: true,
|
||||
codeExample: defaultFunc,
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
this.outputs = [
|
||||
{
|
||||
label: 'Next',
|
||||
name: 'next',
|
||||
baseClasses: ['Agent', 'LLMNode', 'ToolNode'],
|
||||
isAnchor: true
|
||||
},
|
||||
{
|
||||
label: 'End',
|
||||
name: 'end',
|
||||
baseClasses: ['Agent', 'LLMNode', 'ToolNode'],
|
||||
isAnchor: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
|
||||
const conditionLabel = nodeData.inputs?.conditionAgentName as string
|
||||
const conditionName = conditionLabel.toLowerCase().replace(/\s/g, '_').trim()
|
||||
const output = nodeData.outputs?.output as string
|
||||
const sequentialNodes = nodeData.inputs?.sequentialNode as ISeqAgentNode[]
|
||||
let agentPrompt = nodeData.inputs?.systemMessagePrompt as string
|
||||
let humanPrompt = nodeData.inputs?.humanMessagePrompt as string
|
||||
const promptValuesStr = nodeData.inputs?.promptValues
|
||||
const conditionAgentStructuredOutput = nodeData.inputs?.conditionAgentStructuredOutput
|
||||
const model = nodeData.inputs?.model as BaseChatModel
|
||||
|
||||
if (!sequentialNodes || !sequentialNodes.length) throw new Error('Condition Agent must have a predecessor!')
|
||||
|
||||
const startLLM = sequentialNodes[0].startLLM
|
||||
const llm = model || startLLM
|
||||
if (nodeData.inputs) nodeData.inputs.model = llm
|
||||
|
||||
let conditionAgentInputVariablesValues: ICommonObject = {}
|
||||
if (promptValuesStr) {
|
||||
try {
|
||||
conditionAgentInputVariablesValues = typeof promptValuesStr === 'object' ? promptValuesStr : JSON.parse(promptValuesStr)
|
||||
} catch (exception) {
|
||||
throw new Error("Invalid JSON in the Condition Agent's Prompt Input Values: " + exception)
|
||||
}
|
||||
}
|
||||
conditionAgentInputVariablesValues = handleEscapeCharacters(conditionAgentInputVariablesValues, true)
|
||||
|
||||
const conditionAgentInputVariables = uniq([...getInputVariables(agentPrompt), ...getInputVariables(humanPrompt)])
|
||||
|
||||
if (!conditionAgentInputVariables.every((element) => Object.keys(conditionAgentInputVariablesValues).includes(element))) {
|
||||
throw new Error('Condition Agent input variables values are not provided!')
|
||||
}
|
||||
|
||||
const abortControllerSignal = options.signal as AbortController
|
||||
|
||||
const conditionalEdge = async (state: ISeqAgentsState, config: RunnableConfig) =>
|
||||
await runCondition(
|
||||
conditionName,
|
||||
nodeData,
|
||||
input,
|
||||
options,
|
||||
state,
|
||||
config,
|
||||
llm,
|
||||
agentPrompt,
|
||||
humanPrompt,
|
||||
conditionAgentInputVariablesValues,
|
||||
conditionAgentStructuredOutput,
|
||||
abortControllerSignal
|
||||
)
|
||||
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: conditionalEdge,
|
||||
name: conditionName,
|
||||
label: conditionLabel,
|
||||
type: 'condition',
|
||||
output,
|
||||
llm,
|
||||
startLLM,
|
||||
multiModalMessageContent: sequentialNodes[0]?.multiModalMessageContent,
|
||||
predecessorAgents: sequentialNodes
|
||||
}
|
||||
|
||||
return returnOutput
|
||||
}
|
||||
}
|
||||
|
||||
const runCondition = async (
|
||||
conditionName: string,
|
||||
nodeData: INodeData,
|
||||
input: string,
|
||||
options: ICommonObject,
|
||||
state: ISeqAgentsState,
|
||||
config: RunnableConfig,
|
||||
llm: BaseChatModel,
|
||||
agentPrompt: string,
|
||||
humanPrompt: string,
|
||||
conditionAgentInputVariablesValues: ICommonObject,
|
||||
conditionAgentStructuredOutput: string,
|
||||
abortControllerSignal: AbortController
|
||||
) => {
|
||||
const appDataSource = options.appDataSource as DataSource
|
||||
const databaseEntities = options.databaseEntities as IDatabaseEntity
|
||||
const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string
|
||||
const conditionUI = nodeData.inputs?.conditionUI as string
|
||||
const conditionFunction = nodeData.inputs?.conditionFunction as string
|
||||
const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'conditionUI'
|
||||
|
||||
const promptArrays = [new MessagesPlaceholder('messages')] as BaseMessagePromptTemplateLike[]
|
||||
if (agentPrompt) promptArrays.unshift(['system', agentPrompt])
|
||||
if (humanPrompt) promptArrays.push(['human', humanPrompt])
|
||||
const prompt = ChatPromptTemplate.fromMessages(promptArrays)
|
||||
|
||||
let model
|
||||
if (conditionAgentStructuredOutput && conditionAgentStructuredOutput !== '[]') {
|
||||
try {
|
||||
const structuredOutput = z.object(convertStructuredSchemaToZod(conditionAgentStructuredOutput))
|
||||
|
||||
if (llm instanceof ChatGoogleGenerativeAI) {
|
||||
const tool = new ExtractTool({
|
||||
schema: structuredOutput
|
||||
})
|
||||
// @ts-ignore
|
||||
const modelWithTool = llm.bind({
|
||||
tools: [tool],
|
||||
signal: abortControllerSignal ? abortControllerSignal.signal : undefined
|
||||
})
|
||||
model = modelWithTool
|
||||
} else {
|
||||
// @ts-ignore
|
||||
model = llm.withStructuredOutput(structuredOutput)
|
||||
}
|
||||
} catch (exception) {
|
||||
console.error('Invalid JSON in Condition Agent Structured Output: ' + exception)
|
||||
model = llm
|
||||
}
|
||||
} else {
|
||||
model = llm
|
||||
}
|
||||
|
||||
let chain
|
||||
|
||||
if (!conditionAgentInputVariablesValues || !Object.keys(conditionAgentInputVariablesValues).length) {
|
||||
chain = RunnableSequence.from([prompt, model]).withConfig({
|
||||
metadata: { sequentialNodeName: conditionName }
|
||||
})
|
||||
} else {
|
||||
chain = RunnableSequence.from([
|
||||
RunnablePassthrough.assign(transformObjectPropertyToFunction(conditionAgentInputVariablesValues, state)),
|
||||
prompt,
|
||||
model
|
||||
]).withConfig({
|
||||
metadata: { sequentialNodeName: conditionName }
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
state.messages = restructureMessages(model, state)
|
||||
|
||||
let result = await chain.invoke({ ...state, signal: abortControllerSignal?.signal }, config)
|
||||
result.additional_kwargs = { ...result.additional_kwargs, nodeId: nodeData.id }
|
||||
|
||||
if (conditionAgentStructuredOutput && conditionAgentStructuredOutput !== '[]' && 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 } }
|
||||
}
|
||||
|
||||
const variables = await getVars(appDataSource, databaseEntities, nodeData)
|
||||
|
||||
const flow = {
|
||||
chatflowId: options.chatflowid,
|
||||
sessionId: options.sessionId,
|
||||
chatId: options.chatId,
|
||||
input,
|
||||
state,
|
||||
output: result,
|
||||
vars: prepareSandboxVars(variables)
|
||||
}
|
||||
|
||||
if (selectedTab === 'conditionFunction' && conditionFunction) {
|
||||
const vm = await getVM(appDataSource, databaseEntities, nodeData, flow)
|
||||
try {
|
||||
const response = await vm.run(`module.exports = async function() {${conditionFunction}}()`, __dirname)
|
||||
if (typeof response !== 'string') throw new Error('Condition function must return a string')
|
||||
return response
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
}
|
||||
} else if (selectedTab === 'conditionUI' && conditionUI) {
|
||||
try {
|
||||
const conditionItems: IConditionGridItem[] = typeof conditionUI === 'string' ? JSON.parse(conditionUI) : conditionUI
|
||||
|
||||
for (const item of conditionItems) {
|
||||
if (!item.variable) throw new Error('Condition variable is required!')
|
||||
|
||||
if (item.variable.startsWith('$flow')) {
|
||||
const variableValue = customGet(flow, item.variable.replace('$flow.', ''))
|
||||
if (checkCondition(variableValue, item.operation, item.value)) {
|
||||
return item.output
|
||||
}
|
||||
} else if (item.variable.startsWith('$vars')) {
|
||||
const variableValue = customGet(flow, item.variable.replace('$', ''))
|
||||
if (checkCondition(variableValue, item.operation, item.value)) {
|
||||
return item.output
|
||||
}
|
||||
} else if (item.variable.startsWith('$')) {
|
||||
const nodeId = item.variable.replace('$', '')
|
||||
|
||||
const messageOutputs = ((state.messages as unknown as BaseMessage[]) ?? []).filter(
|
||||
(message) => message.additional_kwargs && message.additional_kwargs?.nodeId === nodeId
|
||||
)
|
||||
const messageOutput = messageOutputs[messageOutputs.length - 1]
|
||||
|
||||
if (messageOutput) {
|
||||
if (checkCondition(messageOutput.content as string, item.operation, item.value)) {
|
||||
return item.output
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'End'
|
||||
} catch (exception) {
|
||||
throw new Error('Invalid Condition: ' + exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: ConditionAgent_SeqAgents }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrows-split"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M21 17h-8l-3.5 -5h-6.5" /><path d="M21 7h-8l-3.495 5" /><path d="M18 10l3 -3l-3 -3" /><path d="M18 20l3 -3l-3 -3" /></svg>
|
||||
|
After Width: | Height: | Size: 444 B |
@@ -0,0 +1,54 @@
|
||||
import { END } from '@langchain/langgraph'
|
||||
import { INode, INodeData, INodeParams, ISeqAgentNode } from '../../../src/Interface'
|
||||
|
||||
class End_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
credential: INodeParams
|
||||
inputs: INodeParams[]
|
||||
hideOutput: boolean
|
||||
|
||||
constructor() {
|
||||
this.label = 'End'
|
||||
this.name = 'seqEnd'
|
||||
this.version = 1.0
|
||||
this.type = 'End'
|
||||
this.icon = 'end.svg'
|
||||
this.category = 'Sequential Agents'
|
||||
this.description = 'End conversation'
|
||||
this.baseClasses = [this.type]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Start | Agent | LLM | Tool Node',
|
||||
name: 'sequentialNode',
|
||||
type: 'Start | Agent | LLMNode | ToolNode'
|
||||
}
|
||||
]
|
||||
this.hideOutput = true
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const sequentialNode = nodeData.inputs?.sequentialNode as ISeqAgentNode
|
||||
if (!sequentialNode) throw new Error('End must have a predecessor!')
|
||||
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: END,
|
||||
name: END,
|
||||
label: END,
|
||||
type: 'end',
|
||||
output: END,
|
||||
predecessorAgents: [sequentialNode]
|
||||
}
|
||||
|
||||
return returnOutput
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: End_SeqAgents }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-player-stop"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" /></svg>
|
||||
|
After Width: | Height: | Size: 410 B |
@@ -0,0 +1,605 @@
|
||||
import { flatten, uniq } from 'lodash'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { z } from 'zod'
|
||||
import { RunnableSequence, RunnablePassthrough, RunnableConfig } from '@langchain/core/runnables'
|
||||
import { ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, BaseMessagePromptTemplateLike } from '@langchain/core/prompts'
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
||||
import { AIMessage, AIMessageChunk } from '@langchain/core/messages'
|
||||
import {
|
||||
INode,
|
||||
INodeData,
|
||||
INodeParams,
|
||||
ISeqAgentsState,
|
||||
ICommonObject,
|
||||
MessageContentImageUrl,
|
||||
INodeOutputsValue,
|
||||
ISeqAgentNode,
|
||||
IDatabaseEntity
|
||||
} from '../../../src/Interface'
|
||||
import { AgentExecutor } from '../../../src/agents'
|
||||
import { getInputVariables, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils'
|
||||
import {
|
||||
ExtractTool,
|
||||
convertStructuredSchemaToZod,
|
||||
customGet,
|
||||
getVM,
|
||||
processImageMessage,
|
||||
transformObjectPropertyToFunction,
|
||||
restructureMessages
|
||||
} from '../commonUtils'
|
||||
import { ChatGoogleGenerativeAI } from '../../chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI'
|
||||
|
||||
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 agent'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]
|
||||
};`
|
||||
|
||||
class LLMNode_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
inputs?: INodeParams[]
|
||||
badge?: string
|
||||
outputs: INodeOutputsValue[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'LLM Node'
|
||||
this.name = 'seqLLMNode'
|
||||
this.version = 1.0
|
||||
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.inputs = [
|
||||
{
|
||||
label: 'Name',
|
||||
name: 'llmNodeName',
|
||||
type: 'string',
|
||||
placeholder: 'LLM'
|
||||
},
|
||||
{
|
||||
label: 'System Prompt',
|
||||
name: 'systemMessagePrompt',
|
||||
type: 'string',
|
||||
rows: 4,
|
||||
optional: true,
|
||||
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: 'Start | Agent | LLM | Tool Node',
|
||||
name: 'sequentialNode',
|
||||
type: 'Start | Agent | LLMNode | ToolNode',
|
||||
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: INodeData, input: string, options: ICommonObject): Promise<any> {
|
||||
// Tools can be connected through ToolNodes
|
||||
let tools = nodeData.inputs?.tools
|
||||
tools = flatten(tools)
|
||||
|
||||
let systemPrompt = nodeData.inputs?.systemMessagePrompt as string
|
||||
let humanPrompt = nodeData.inputs?.humanMessagePrompt as string
|
||||
const llmNodeLabel = nodeData.inputs?.llmNodeName as string
|
||||
const sequentialNodes = nodeData.inputs?.sequentialNode as ISeqAgentNode[]
|
||||
const model = nodeData.inputs?.model as BaseChatModel
|
||||
const promptValuesStr = nodeData.inputs?.promptValues
|
||||
const output = nodeData.outputs?.output as string
|
||||
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: ICommonObject = {}
|
||||
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 = 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 processImageMessage(llm, nodeData, options))
|
||||
const abortControllerSignal = options.signal as AbortController
|
||||
const llmNodeInputVariables = uniq([...getInputVariables(systemPrompt), ...getInputVariables(humanPrompt)])
|
||||
|
||||
if (!llmNodeInputVariables.every((element) => Object.keys(llmNodeInputVariablesValues).includes(element))) {
|
||||
throw new Error('LLM Node input variables values are not provided!')
|
||||
}
|
||||
|
||||
const workerNode = async (state: ISeqAgentsState, config: RunnableConfig) => {
|
||||
const bindModel = config.configurable?.bindModel?.[nodeData.id]
|
||||
return await agentNode(
|
||||
{
|
||||
state,
|
||||
llm,
|
||||
agent: await createAgent(
|
||||
llmNodeName,
|
||||
state,
|
||||
bindModel || llm,
|
||||
[...tools],
|
||||
systemPrompt,
|
||||
humanPrompt,
|
||||
multiModalMessageContent,
|
||||
llmNodeInputVariablesValues,
|
||||
llmStructuredOutput
|
||||
),
|
||||
name: llmNodeName,
|
||||
abortControllerSignal,
|
||||
nodeData,
|
||||
input,
|
||||
options
|
||||
},
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
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(
|
||||
llmNodeName: string,
|
||||
state: ISeqAgentsState,
|
||||
llm: BaseChatModel,
|
||||
tools: any[],
|
||||
systemPrompt: string,
|
||||
humanPrompt: string,
|
||||
multiModalMessageContent: MessageContentImageUrl[],
|
||||
llmNodeInputVariablesValues: ICommonObject,
|
||||
llmStructuredOutput: string
|
||||
): Promise<AgentExecutor | RunnableSequence> {
|
||||
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 = z.object(convertStructuredSchemaToZod(llmStructuredOutput))
|
||||
|
||||
if (llm instanceof ChatGoogleGenerativeAI) {
|
||||
const tool = new ExtractTool({
|
||||
schema: structuredOutput
|
||||
})
|
||||
// @ts-ignore
|
||||
const modelWithTool = llm.bind({
|
||||
tools: [tool]
|
||||
}) as any
|
||||
llm = modelWithTool
|
||||
} else {
|
||||
// @ts-ignore
|
||||
llm = llm.withStructuredOutput(structuredOutput)
|
||||
}
|
||||
} catch (exception) {
|
||||
console.error(exception)
|
||||
}
|
||||
}
|
||||
|
||||
const promptArrays = [new MessagesPlaceholder('messages')] as BaseMessagePromptTemplateLike[]
|
||||
if (systemPrompt) promptArrays.unshift(['system', systemPrompt])
|
||||
if (humanPrompt) promptArrays.push(['human', humanPrompt])
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages(promptArrays)
|
||||
if (multiModalMessageContent.length) {
|
||||
const msg = HumanMessagePromptTemplate.fromTemplate([...multiModalMessageContent])
|
||||
prompt.promptMessages.splice(1, 0, msg)
|
||||
}
|
||||
|
||||
let chain
|
||||
|
||||
if (!llmNodeInputVariablesValues || !Object.keys(llmNodeInputVariablesValues).length) {
|
||||
chain = RunnableSequence.from([prompt, llm]).withConfig({
|
||||
metadata: { sequentialNodeName: llmNodeName }
|
||||
})
|
||||
} else {
|
||||
chain = RunnableSequence.from([
|
||||
RunnablePassthrough.assign(transformObjectPropertyToFunction(llmNodeInputVariablesValues, state)),
|
||||
prompt,
|
||||
llm
|
||||
]).withConfig({
|
||||
metadata: { sequentialNodeName: llmNodeName }
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return chain
|
||||
}
|
||||
|
||||
async function agentNode(
|
||||
{
|
||||
state,
|
||||
llm,
|
||||
agent,
|
||||
name,
|
||||
abortControllerSignal,
|
||||
nodeData,
|
||||
input,
|
||||
options
|
||||
}: {
|
||||
state: ISeqAgentsState
|
||||
llm: BaseChatModel
|
||||
agent: AgentExecutor | RunnableSequence
|
||||
name: string
|
||||
abortControllerSignal: AbortController
|
||||
nodeData: INodeData
|
||||
input: string
|
||||
options: ICommonObject
|
||||
},
|
||||
config: RunnableConfig
|
||||
) {
|
||||
try {
|
||||
if (abortControllerSignal.signal.aborted) {
|
||||
throw new Error('Aborted!')
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
state.messages = restructureMessages(llm, state)
|
||||
|
||||
let result: AIMessageChunk | ICommonObject = 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 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 }
|
||||
return {
|
||||
...returnedOutput,
|
||||
messages: [result]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (nodeData.inputs?.llmStructuredOutput && nodeData.inputs.llmStructuredOutput !== '[]') {
|
||||
const messages = [
|
||||
new 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 }
|
||||
return {
|
||||
messages: [result]
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getReturnOutput = async (nodeData: INodeData, input: string, options: ICommonObject, output: any, state: ISeqAgentsState) => {
|
||||
const appDataSource = options.appDataSource as DataSource
|
||||
const databaseEntities = options.databaseEntities as IDatabaseEntity
|
||||
const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string
|
||||
const updateStateMemoryUI = nodeData.inputs?.updateStateMemoryUI as string
|
||||
const updateStateMemoryCode = nodeData.inputs?.updateStateMemoryCode as string
|
||||
|
||||
const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'updateStateMemoryUI'
|
||||
const variables = await getVars(appDataSource, databaseEntities, nodeData)
|
||||
|
||||
const flow = {
|
||||
chatflowId: options.chatflowid,
|
||||
sessionId: options.sessionId,
|
||||
chatId: options.chatId,
|
||||
input,
|
||||
output,
|
||||
state,
|
||||
vars: prepareSandboxVars(variables)
|
||||
}
|
||||
|
||||
if (selectedTab === 'updateStateMemoryUI' && updateStateMemoryUI) {
|
||||
try {
|
||||
const parsedSchema = typeof updateStateMemoryUI === 'string' ? JSON.parse(updateStateMemoryUI) : updateStateMemoryUI
|
||||
const obj: ICommonObject = {}
|
||||
for (const sch of parsedSchema) {
|
||||
const key = sch.key
|
||||
if (!key) throw new Error(`Key is required`)
|
||||
let value = sch.value as string
|
||||
if (value.startsWith('$flow')) {
|
||||
value = customGet(flow, sch.value.replace('$flow.', ''))
|
||||
} else if (value.startsWith('$vars')) {
|
||||
value = customGet(flow, sch.value.replace('$', ''))
|
||||
}
|
||||
obj[key] = value
|
||||
}
|
||||
return obj
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
}
|
||||
} else if (selectedTab === 'updateStateMemoryCode' && updateStateMemoryCode) {
|
||||
const vm = await getVM(appDataSource, databaseEntities, nodeData, 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 }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-language"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 5h7" /><path d="M9 3v2c0 4.418 -2.239 8 -5 8" /><path d="M5 9c0 2.144 2.952 3.908 6.7 4" /><path d="M12 20l4 -9l4 9" /><path d="M19.1 18h-6.2" /></svg>
|
||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1,66 @@
|
||||
import { INode, INodeData, INodeParams, ISeqAgentNode } from '../../../src/Interface'
|
||||
|
||||
class Loop_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
credential: INodeParams
|
||||
inputs: INodeParams[]
|
||||
hideOutput: boolean
|
||||
|
||||
constructor() {
|
||||
this.label = 'Loop'
|
||||
this.name = 'seqLoop'
|
||||
this.version = 1.0
|
||||
this.type = 'Loop'
|
||||
this.icon = 'loop.svg'
|
||||
this.category = 'Sequential Agents'
|
||||
this.description = 'Loop back to the specific sequential node'
|
||||
this.baseClasses = [this.type]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Start | Agent | LLM | Tool Node',
|
||||
name: 'sequentialNode',
|
||||
type: 'Start | Agent | LLMNode | ToolNode',
|
||||
list: true
|
||||
},
|
||||
{
|
||||
label: 'Loop To',
|
||||
name: 'loopToName',
|
||||
description: 'Name of the agent to loop back to',
|
||||
type: 'string',
|
||||
placeholder: 'agent1'
|
||||
}
|
||||
]
|
||||
this.hideOutput = true
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const sequentialNodes = nodeData.inputs?.sequentialNode as ISeqAgentNode[]
|
||||
const loopToNameLabel = nodeData.inputs?.loopToName as string
|
||||
|
||||
if (!sequentialNodes || !sequentialNodes.length) throw new Error('Loop must have a predecessor!')
|
||||
if (!loopToNameLabel) throw new Error('Loop to name is required')
|
||||
|
||||
const loopToName = loopToNameLabel.toLowerCase().replace(/\s/g, '_').trim()
|
||||
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: loopToName,
|
||||
name: loopToName,
|
||||
label: loopToNameLabel,
|
||||
type: 'agent',
|
||||
predecessorAgents: sequentialNodes,
|
||||
output: loopToName
|
||||
}
|
||||
|
||||
return returnOutput
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: Loop_SeqAgents }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-back-up-double"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M13 14l-4 -4l4 -4" /><path d="M8 14l-4 -4l4 -4" /><path d="M9 10h7a4 4 0 1 1 0 8h-1" /></svg>
|
||||
|
After Width: | Height: | Size: 423 B |
@@ -0,0 +1,81 @@
|
||||
import { START } from '@langchain/langgraph'
|
||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
||||
import { INode, INodeData, INodeParams, ISeqAgentNode } from '../../../src/Interface'
|
||||
import { Moderation } from '../../moderation/Moderation'
|
||||
|
||||
class Start_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
credential: INodeParams
|
||||
inputs: INodeParams[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'Start'
|
||||
this.name = 'seqStart'
|
||||
this.version = 1.0
|
||||
this.type = 'Start'
|
||||
this.icon = 'start.svg'
|
||||
this.category = 'Sequential Agents'
|
||||
this.description = 'Starting point of the conversation'
|
||||
this.baseClasses = [this.type]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Chat Model',
|
||||
name: 'model',
|
||||
type: 'BaseChatModel',
|
||||
description: `Only compatible with models that are capable of function calling: ChatOpenAI, ChatMistral, ChatAnthropic, ChatGoogleGenerativeAI, ChatVertexAI, GroqChat`
|
||||
},
|
||||
{
|
||||
label: 'Agent Memory',
|
||||
name: 'agentMemory',
|
||||
type: 'BaseCheckpointSaver',
|
||||
description: 'Save the state of the agent',
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'State',
|
||||
name: 'state',
|
||||
type: 'State',
|
||||
description:
|
||||
'State is an object that is updated by nodes in the graph, passing from one node to another. By default, state contains "messages" that got updated with each message sent and received.',
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Input Moderation',
|
||||
description: 'Detect text that could generate harmful output and prevent it from being sent to the language model',
|
||||
name: 'inputModeration',
|
||||
type: 'Moderation',
|
||||
optional: true,
|
||||
list: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const moderations = (nodeData.inputs?.inputModeration as Moderation[]) ?? []
|
||||
const model = nodeData.inputs?.model as BaseChatModel
|
||||
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: START,
|
||||
name: START,
|
||||
label: START,
|
||||
type: 'start',
|
||||
output: START,
|
||||
llm: model,
|
||||
startLLM: model,
|
||||
moderations,
|
||||
checkpointMemory: nodeData.inputs?.agentMemory
|
||||
}
|
||||
|
||||
return returnOutput
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: Start_SeqAgents }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-player-play"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 4v16l13 -8z" /></svg>
|
||||
|
After Width: | Height: | Size: 345 B |
@@ -0,0 +1,199 @@
|
||||
import { START } from '@langchain/langgraph'
|
||||
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams, ISeqAgentNode } from '../../../src/Interface'
|
||||
import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../../src/utils'
|
||||
import { NodeVM } from 'vm2'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
const defaultFunc = `{
|
||||
aggregate: {
|
||||
value: (x, y) => x.concat(y), // here we append the new message to the existing messages
|
||||
default: () => []
|
||||
}
|
||||
}`
|
||||
|
||||
const howToUse = `
|
||||
Specify the Key, Operation Type, and Default Value for the state object. The Operation Type can be either "Replace" or "Append".
|
||||
|
||||
**Replace**
|
||||
- Replace the existing value with the new value.
|
||||
- If the new value is null, the existing value will be retained.
|
||||
|
||||
**Append**
|
||||
- Append the new value to the existing value.
|
||||
- Default value can be empty or an array. Ex: ["a", "b"]
|
||||
- Final value is an array.
|
||||
`
|
||||
const TAB_IDENTIFIER = 'selectedStateTab'
|
||||
|
||||
class State_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
credential: INodeParams
|
||||
inputs: INodeParams[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'State'
|
||||
this.name = 'seqState'
|
||||
this.version = 1.0
|
||||
this.type = 'State'
|
||||
this.icon = 'state.svg'
|
||||
this.category = 'Sequential Agents'
|
||||
this.description = 'A centralized state object, updated by nodes in the graph, passing from one node to another'
|
||||
this.baseClasses = [this.type]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'State',
|
||||
name: 'stateMemory',
|
||||
type: 'tabs',
|
||||
tabIdentifier: TAB_IDENTIFIER,
|
||||
additionalParams: true,
|
||||
default: 'stateMemoryUI',
|
||||
tabs: [
|
||||
{
|
||||
label: 'State (Table)',
|
||||
name: 'stateMemoryUI',
|
||||
type: 'datagrid',
|
||||
description:
|
||||
'Structure for state. By default, state contains "messages" that got updated with each message sent and received.',
|
||||
hint: {
|
||||
label: 'How to use',
|
||||
value: howToUse
|
||||
},
|
||||
datagrid: [
|
||||
{ field: 'key', headerName: 'Key', editable: true },
|
||||
{
|
||||
field: 'type',
|
||||
headerName: 'Operation',
|
||||
type: 'singleSelect',
|
||||
valueOptions: ['Replace', 'Append'],
|
||||
editable: true
|
||||
},
|
||||
{ field: 'defaultValue', headerName: 'Default Value', flex: 1, editable: true }
|
||||
],
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
},
|
||||
{
|
||||
label: 'State (Code)',
|
||||
name: 'stateMemoryCode',
|
||||
type: 'code',
|
||||
description: `JSON object representing the state`,
|
||||
hideCodeExecute: true,
|
||||
codeExample: defaultFunc,
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData, input: string, options: ICommonObject): Promise<any> {
|
||||
const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string
|
||||
const stateMemoryUI = nodeData.inputs?.stateMemoryUI as string
|
||||
const stateMemoryCode = nodeData.inputs?.stateMemoryCode as string
|
||||
const appDataSource = options.appDataSource as DataSource
|
||||
const databaseEntities = options.databaseEntities as IDatabaseEntity
|
||||
const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'stateMemoryUI'
|
||||
|
||||
if (!stateMemoryUI && !stateMemoryCode) {
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: {},
|
||||
name: 'state',
|
||||
label: 'state',
|
||||
type: 'state',
|
||||
output: START
|
||||
}
|
||||
return returnOutput
|
||||
}
|
||||
|
||||
if (selectedTab === 'stateMemoryUI' && stateMemoryUI) {
|
||||
try {
|
||||
const parsedSchema = typeof stateMemoryUI === 'string' ? JSON.parse(stateMemoryUI) : stateMemoryUI
|
||||
const obj: ICommonObject = {}
|
||||
for (const sch of parsedSchema) {
|
||||
const key = sch.key
|
||||
if (!key) throw new Error(`Key is required`)
|
||||
const type = sch.type
|
||||
const defaultValue = sch.defaultValue
|
||||
|
||||
if (type === 'Append') {
|
||||
obj[key] = {
|
||||
value: (x: any, y: any) => (Array.isArray(y) ? x.concat(y) : x.concat([y])),
|
||||
default: () => (defaultValue ? JSON.parse(defaultValue) : [])
|
||||
}
|
||||
} else {
|
||||
obj[key] = {
|
||||
value: (x: any, y: any) => y ?? x,
|
||||
default: () => defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: obj,
|
||||
name: 'state',
|
||||
label: 'state',
|
||||
type: 'state',
|
||||
output: START
|
||||
}
|
||||
return returnOutput
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
}
|
||||
} else if (selectedTab === 'stateMemoryCode' && stateMemoryCode) {
|
||||
const variables = await getVars(appDataSource, databaseEntities, nodeData)
|
||||
const flow = {
|
||||
chatflowId: options.chatflowid,
|
||||
sessionId: options.sessionId,
|
||||
chatId: options.chatId,
|
||||
input
|
||||
}
|
||||
|
||||
let sandbox: any = {}
|
||||
sandbox['$vars'] = prepareSandboxVars(variables)
|
||||
sandbox['$flow'] = flow
|
||||
|
||||
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
|
||||
? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(','))
|
||||
: defaultAllowBuiltInDep
|
||||
const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : []
|
||||
const deps = availableDependencies.concat(externalDeps)
|
||||
|
||||
const nodeVMOptions = {
|
||||
console: 'inherit',
|
||||
sandbox,
|
||||
require: {
|
||||
external: { modules: deps },
|
||||
builtin: builtinDeps
|
||||
}
|
||||
} as any
|
||||
|
||||
const vm = new NodeVM(nodeVMOptions)
|
||||
try {
|
||||
const response = await vm.run(`module.exports = async function() {return ${stateMemoryCode}}()`, __dirname)
|
||||
if (typeof response !== 'object') throw new Error('State must be an object')
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: response,
|
||||
name: 'state',
|
||||
label: 'state',
|
||||
type: 'state',
|
||||
output: START
|
||||
}
|
||||
return returnOutput
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: State_SeqAgents }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-sd-card"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 21h10a2 2 0 0 0 2 -2v-14a2 2 0 0 0 -2 -2h-6.172a2 2 0 0 0 -1.414 .586l-3.828 3.828a2 2 0 0 0 -.586 1.414v10.172a2 2 0 0 0 2 2z" /><path d="M13 6v2" /><path d="M16 6v2" /><path d="M10 7v1" /></svg>
|
||||
|
After Width: | Height: | Size: 523 B |
@@ -0,0 +1,498 @@
|
||||
import { flatten } from 'lodash'
|
||||
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams, ISeqAgentNode, IUsedTool } from '../../../src/Interface'
|
||||
import { AIMessage, AIMessageChunk, BaseMessage, ToolMessage } from '@langchain/core/messages'
|
||||
import { StructuredTool } from '@langchain/core/tools'
|
||||
import { RunnableConfig } from '@langchain/core/runnables'
|
||||
import { SOURCE_DOCUMENTS_PREFIX } from '../../../src/agents'
|
||||
import { Document } from '@langchain/core/documents'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { MessagesState, RunnableCallable, customGet, getVM } from '../commonUtils'
|
||||
import { getVars, prepareSandboxVars } from '../../../src/utils'
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts'
|
||||
|
||||
const defaultApprovalPrompt = `You are about to execute tool: {tools}. Ask if user want to proceed`
|
||||
|
||||
const customOutputFuncDesc = `This is only applicable when you have a custom State at the START node. After tool 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 tool's output as the value to update state, it is available as \`$flow.output\` with the following structure (array):
|
||||
\`\`\`json
|
||||
[
|
||||
{
|
||||
"tool": "tool's name",
|
||||
"toolInput": {},
|
||||
"toolOutput": "tool's output content",
|
||||
"sourceDocuments": [
|
||||
{
|
||||
"pageContent": "This is the page content",
|
||||
"metadata": "{foo: var}",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
\`\`\`
|
||||
|
||||
For example:
|
||||
\`\`\`js
|
||||
/* Assuming you have the following state:
|
||||
{
|
||||
"sources": null
|
||||
}
|
||||
*/
|
||||
|
||||
return {
|
||||
"sources": $flow.output[0].sourceDocuments
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
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 agent's output as the value to update state, it is available as available as \`$flow.output\` with the following structure (array):
|
||||
\`\`\`json
|
||||
[
|
||||
{
|
||||
"content": "Hello! How can I assist you today?",
|
||||
"sourceDocuments": [
|
||||
{
|
||||
"pageContent": "This is the page content",
|
||||
"metadata": "{foo: var}",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
\`\`\`
|
||||
|
||||
For example:
|
||||
| Key | Value |
|
||||
|--------------|-------------------------------------------|
|
||||
| sources | \`$flow.output[0].sourceDocuments\` |
|
||||
|
||||
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 TAB_IDENTIFIER = 'selectedUpdateStateMemoryTab'
|
||||
|
||||
class ToolNode_SeqAgents implements INode {
|
||||
label: string
|
||||
name: string
|
||||
version: number
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
credential: INodeParams
|
||||
inputs: INodeParams[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'Tool Node'
|
||||
this.name = 'seqToolNode'
|
||||
this.version = 1.0
|
||||
this.type = 'ToolNode'
|
||||
this.icon = 'toolNode.svg'
|
||||
this.category = 'Sequential Agents'
|
||||
this.description = `Execute tool and return tool's output`
|
||||
this.baseClasses = [this.type]
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Tools',
|
||||
name: 'tools',
|
||||
type: 'Tool',
|
||||
list: true,
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'LLM Node',
|
||||
name: 'llmNode',
|
||||
type: 'LLMNode'
|
||||
},
|
||||
{
|
||||
label: 'Name',
|
||||
name: 'toolNodeName',
|
||||
type: 'string',
|
||||
placeholder: 'Tool'
|
||||
},
|
||||
{
|
||||
label: 'Require Approval',
|
||||
name: 'interrupt',
|
||||
description: 'Require approval before executing tools',
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Approval Prompt',
|
||||
name: 'approvalPrompt',
|
||||
description: 'Prompt for approval. Only applicable if "Require Approval" is enabled',
|
||||
type: 'string',
|
||||
default: defaultApprovalPrompt,
|
||||
rows: 4,
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
},
|
||||
{
|
||||
label: 'Approve Button Text',
|
||||
name: 'approveButtonText',
|
||||
description: 'Text for approve button. Only applicable if "Require Approval" is enabled',
|
||||
type: 'string',
|
||||
default: 'Yes',
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
},
|
||||
{
|
||||
label: 'Reject Button Text',
|
||||
name: 'rejectButtonText',
|
||||
description: 'Text for reject button. Only applicable if "Require Approval" is enabled',
|
||||
type: 'string',
|
||||
default: 'No',
|
||||
optional: true,
|
||||
additionalParams: true
|
||||
},
|
||||
{
|
||||
label: 'Update State',
|
||||
name: 'updateStateMemory',
|
||||
type: 'tabs',
|
||||
tabIdentifier: TAB_IDENTIFIER,
|
||||
additionalParams: true,
|
||||
default: 'updateStateMemoryUI',
|
||||
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: 'All Tools Output (array)',
|
||||
value: '$flow.output'
|
||||
},
|
||||
{
|
||||
label: 'First Tool Output (string)',
|
||||
value: '$flow.output[0].toolOutput'
|
||||
},
|
||||
{
|
||||
label: 'First Tool Input Arguments (string | json)',
|
||||
value: '$flow.output[0].toolInput'
|
||||
},
|
||||
{
|
||||
label: `First Tool Returned Source Documents (array)`,
|
||||
value: '$flow.output[0].sourceDocuments'
|
||||
},
|
||||
{
|
||||
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: INodeData, input: string, options: ICommonObject): Promise<any> {
|
||||
const toolNodeLabel = nodeData.inputs?.toolNodeName as string
|
||||
const llmNode = nodeData.inputs?.llmNode as ISeqAgentNode
|
||||
if (!llmNode) throw new Error('Tool node must have a predecessor!')
|
||||
|
||||
const interrupt = nodeData.inputs?.interrupt as boolean
|
||||
const approvalPrompt = nodeData.inputs?.approvalPrompt as string
|
||||
const approveButtonText = nodeData.inputs?.approveButtonText as string
|
||||
const rejectButtonText = nodeData.inputs?.rejectButtonText as string
|
||||
|
||||
let tools = nodeData.inputs?.tools
|
||||
tools = flatten(tools)
|
||||
if (!tools || !tools.length) throw new Error('Tools must not be empty')
|
||||
|
||||
const output = nodeData.outputs?.output as string
|
||||
|
||||
if (!toolNodeLabel) throw new Error('Tool node name is required!')
|
||||
const toolNodeLabelName = toolNodeLabel.toLowerCase().replace(/\s/g, '_').trim()
|
||||
|
||||
const toolNode = new ToolNode(tools, nodeData, input, options, toolNodeLabelName, [], { sequentialNodeName: toolNodeLabelName })
|
||||
;(toolNode as any).interrupt = interrupt
|
||||
|
||||
if (interrupt && approvalPrompt && approveButtonText && rejectButtonText) {
|
||||
;(toolNode as any).seekPermissionMessage = async (usedTools: IUsedTool[]) => {
|
||||
const prompt = ChatPromptTemplate.fromMessages([['human', approvalPrompt || defaultApprovalPrompt]])
|
||||
const chain = prompt.pipe(llmNode.startLLM)
|
||||
const response = (await chain.invoke({
|
||||
input: 'Hello there!',
|
||||
tools: JSON.stringify(usedTools)
|
||||
})) as AIMessageChunk
|
||||
return response.content
|
||||
}
|
||||
}
|
||||
|
||||
const returnOutput: ISeqAgentNode = {
|
||||
id: nodeData.id,
|
||||
node: toolNode,
|
||||
name: toolNodeLabelName,
|
||||
label: toolNodeLabel,
|
||||
type: 'tool',
|
||||
output,
|
||||
predecessorAgents: [llmNode],
|
||||
llm: llmNode.llm,
|
||||
startLLM: llmNode.startLLM,
|
||||
moderations: llmNode.moderations,
|
||||
multiModalMessageContent: llmNode.multiModalMessageContent
|
||||
}
|
||||
|
||||
return returnOutput
|
||||
}
|
||||
}
|
||||
|
||||
class ToolNode<T extends BaseMessage[] | MessagesState> extends RunnableCallable<T, T> {
|
||||
tools: StructuredTool[]
|
||||
nodeData: INodeData
|
||||
inputQuery: string
|
||||
options: ICommonObject
|
||||
|
||||
constructor(
|
||||
tools: StructuredTool[],
|
||||
nodeData: INodeData,
|
||||
inputQuery: string,
|
||||
options: ICommonObject,
|
||||
name: string = 'tools',
|
||||
tags: string[] = [],
|
||||
metadata: ICommonObject = {}
|
||||
) {
|
||||
super({ name, metadata, tags, func: (input, config) => this.run(input, config) })
|
||||
this.tools = tools
|
||||
this.nodeData = nodeData
|
||||
this.inputQuery = inputQuery
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private async run(input: BaseMessage[] | MessagesState, config: RunnableConfig): Promise<BaseMessage[] | MessagesState> {
|
||||
const message = Array.isArray(input) ? input[input.length - 1] : input.messages[input.messages.length - 1]
|
||||
|
||||
if (message._getType() !== 'ai') {
|
||||
throw new Error('ToolNode only accepts AIMessages as input.')
|
||||
}
|
||||
|
||||
const outputs = await Promise.all(
|
||||
(message as AIMessage).tool_calls?.map(async (call) => {
|
||||
const tool = this.tools.find((tool) => tool.name === call.name)
|
||||
if (tool === undefined) {
|
||||
throw new Error(`Tool ${call.name} not found.`)
|
||||
}
|
||||
let output = await tool.invoke(call.args, config)
|
||||
let sourceDocuments: Document[] = []
|
||||
if (output?.includes(SOURCE_DOCUMENTS_PREFIX)) {
|
||||
const outputArray = output.split(SOURCE_DOCUMENTS_PREFIX)
|
||||
output = outputArray[0]
|
||||
const docs = outputArray[1]
|
||||
try {
|
||||
sourceDocuments = JSON.parse(docs)
|
||||
} catch (e) {
|
||||
console.error('Error parsing source documents from tool')
|
||||
}
|
||||
}
|
||||
return new ToolMessage({
|
||||
name: tool.name,
|
||||
content: typeof output === 'string' ? output : JSON.stringify(output),
|
||||
tool_call_id: call.id!,
|
||||
additional_kwargs: {
|
||||
sourceDocuments,
|
||||
args: call.args,
|
||||
usedTools: [
|
||||
{
|
||||
tool: tool.name ?? '',
|
||||
toolInput: call.args,
|
||||
toolOutput: output
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}) ?? []
|
||||
)
|
||||
|
||||
const additional_kwargs: ICommonObject = { nodeId: this.nodeData.id }
|
||||
outputs.forEach((result) => (result.additional_kwargs = { ...result.additional_kwargs, ...additional_kwargs }))
|
||||
|
||||
if (this.nodeData.inputs?.updateStateMemoryUI || this.nodeData.inputs?.updateStateMemoryCode) {
|
||||
const returnedOutput = await getReturnOutput(this.nodeData, this.inputQuery, this.options, outputs, input)
|
||||
return {
|
||||
...returnedOutput,
|
||||
messages: outputs
|
||||
}
|
||||
} else {
|
||||
return Array.isArray(input) ? outputs : { messages: outputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getReturnOutput = async (
|
||||
nodeData: INodeData,
|
||||
input: string,
|
||||
options: ICommonObject,
|
||||
outputs: ToolMessage[],
|
||||
state: BaseMessage[] | MessagesState
|
||||
) => {
|
||||
const appDataSource = options.appDataSource as DataSource
|
||||
const databaseEntities = options.databaseEntities as IDatabaseEntity
|
||||
const tabIdentifier = nodeData.inputs?.[`${TAB_IDENTIFIER}_${nodeData.id}`] as string
|
||||
const updateStateMemoryUI = nodeData.inputs?.updateStateMemoryUI as string
|
||||
const updateStateMemoryCode = nodeData.inputs?.updateStateMemoryCode as string
|
||||
|
||||
const selectedTab = tabIdentifier ? tabIdentifier.split(`_${nodeData.id}`)[0] : 'updateStateMemoryUI'
|
||||
const variables = await getVars(appDataSource, databaseEntities, nodeData)
|
||||
|
||||
const reformattedOutput = outputs.map((output) => {
|
||||
return {
|
||||
tool: output.name,
|
||||
toolInput: output.additional_kwargs.args,
|
||||
toolOutput: output.content,
|
||||
sourceDocuments: output.additional_kwargs.sourceDocuments
|
||||
} as IUsedTool
|
||||
})
|
||||
|
||||
const flow = {
|
||||
chatflowId: options.chatflowid,
|
||||
sessionId: options.sessionId,
|
||||
chatId: options.chatId,
|
||||
input,
|
||||
output: reformattedOutput,
|
||||
state,
|
||||
vars: prepareSandboxVars(variables)
|
||||
}
|
||||
|
||||
if (selectedTab === 'updateStateMemoryUI' && updateStateMemoryUI) {
|
||||
try {
|
||||
const parsedSchema = typeof updateStateMemoryUI === 'string' ? JSON.parse(updateStateMemoryUI) : updateStateMemoryUI
|
||||
const obj: ICommonObject = {}
|
||||
for (const sch of parsedSchema) {
|
||||
const key = sch.key
|
||||
if (!key) throw new Error(`Key is required`)
|
||||
let value = sch.value as string
|
||||
if (value.startsWith('$flow')) {
|
||||
value = customGet(flow, sch.value.replace('$flow.', ''))
|
||||
} else if (value.startsWith('$vars')) {
|
||||
value = customGet(flow, sch.value.replace('$', ''))
|
||||
}
|
||||
obj[key] = value
|
||||
}
|
||||
return obj
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
}
|
||||
} else if (selectedTab === 'updateStateMemoryCode' && updateStateMemoryCode) {
|
||||
const vm = await getVM(appDataSource, databaseEntities, nodeData, 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: ToolNode_SeqAgents }
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-tool"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5" /></svg>
|
||||
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,345 @@
|
||||
import { get } from 'lodash'
|
||||
import { z } from 'zod'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { NodeVM } from 'vm2'
|
||||
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 } from '../../src/Interface'
|
||||
import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../src/utils'
|
||||
|
||||
export const checkCondition = (input: string | number | undefined, condition: string, value: string | number = ''): boolean => {
|
||||
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) value = messageOutput.content
|
||||
}
|
||||
} 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 getVM = async (appDataSource: DataSource, databaseEntities: IDatabaseEntity, nodeData: INodeData, flow: ICommonObject) => {
|
||||
const variables = await getVars(appDataSource, databaseEntities, nodeData)
|
||||
|
||||
let sandbox: any = {}
|
||||
sandbox['$vars'] = prepareSandboxVars(variables)
|
||||
sandbox['$flow'] = flow
|
||||
|
||||
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
|
||||
? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(','))
|
||||
: defaultAllowBuiltInDep
|
||||
const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : []
|
||||
const deps = availableDependencies.concat(externalDeps)
|
||||
|
||||
const nodeVMOptions = {
|
||||
console: 'inherit',
|
||||
sandbox,
|
||||
require: {
|
||||
external: { modules: deps },
|
||||
builtin: builtinDeps
|
||||
}
|
||||
} as any
|
||||
|
||||
return new NodeVM(nodeVMOptions)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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<any> {
|
||||
name?: string
|
||||
func: (...args: any[]) => any
|
||||
tags?: string[]
|
||||
trace?: boolean
|
||||
recurse?: boolean
|
||||
}
|
||||
|
||||
export interface MessagesState {
|
||||
messages: BaseMessage[]
|
||||
}
|
||||
|
||||
export class RunnableCallable<I = unknown, O = unknown> extends Runnable<I, O> {
|
||||
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<RunnableConfig> | undefined): Promise<any> {
|
||||
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
|
||||
}
|
||||
}
|
||||