From 746e7b89f7e9b99126fb69110773548dfe91b74f Mon Sep 17 00:00:00 2001 From: oleg Date: Wed, 4 Sep 2024 14:45:14 +0200 Subject: [PATCH] fix(AI Agent Node): Fix tools agent when using memory and Anthropic models (#10513) --- .../agents/Agent/agents/ToolsAgent/execute.ts | 75 +++++++++++++++++-- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts index 295b0a5d1d972..a6dc4a63f2a8d 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/execute.ts @@ -1,7 +1,7 @@ import { BINARY_ENCODING, NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; -import type { AgentAction, AgentFinish, AgentStep } from 'langchain/agents'; +import type { AgentAction, AgentFinish } from 'langchain/agents'; import { AgentExecutor, createToolCallingAgent } from 'langchain/agents'; import type { BaseChatMemory } from '@langchain/community/memory/chat_memory'; import type { BaseMessagePromptTemplateLike } from '@langchain/core/prompts'; @@ -9,12 +9,12 @@ import { ChatPromptTemplate } from '@langchain/core/prompts'; import { omit } from 'lodash'; import type { Tool } from '@langchain/core/tools'; import { DynamicStructuredTool } from '@langchain/core/tools'; -import { RunnableSequence } from '@langchain/core/runnables'; import type { ZodObject } from 'zod'; import { z } from 'zod'; import type { BaseOutputParser, StructuredOutputParser } from '@langchain/core/output_parsers'; import { OutputFixingParser } from 'langchain/output_parsers'; import { HumanMessage } from '@langchain/core/messages'; +import { RunnableSequence } from '@langchain/core/runnables'; import { isChatInstance, getPromptInputByType, @@ -93,7 +93,69 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise; const outputParser = (await getOptionalOutputParsers(this))?.[0]; let structuredOutputParserTool: DynamicStructuredTool | undefined; + /** + * Ensures consistent handling of outputs regardless of the model used, + * providing a unified output format for further processing. + * + * This method is necessary to handle different output formats from various language models. + * Specifically, it checks if the agent step is the final step (contains returnValues) and determines + * if the output is a simple string (e.g., from OpenAI models) or an array of outputs (e.g., from Anthropic models). + * + * Examples: + * 1. Anthropic model output: + * ```json + * { + * "output": [ + * { + * "index": 0, + * "type": "text", + * "text": "The result of the calculation is approximately 1001.8166..." + * } + * ] + * } + *``` + * 2. OpenAI model output: + * ```json + * { + * "output": "The result of the calculation is approximately 1001.82..." + * } + * ``` + * + * @param steps - The agent finish or agent action steps. + * @returns The modified agent finish steps or the original steps. + */ + function handleAgentFinishOutput(steps: AgentFinish | AgentAction[]) { + // Check if the steps contain multiple outputs + type AgentMultiOutputFinish = AgentFinish & { + returnValues: { output: Array<{ text: string; type: string; index: number }> }; + }; + const agentFinishSteps = steps as AgentMultiOutputFinish | AgentFinish; + + if (agentFinishSteps.returnValues) { + const isMultiOutput = Array.isArray(agentFinishSteps.returnValues?.output); + if (isMultiOutput) { + // Define the type for each item in the multi-output array + type MultiOutputItem = { index: number; type: string; text: string }; + const multiOutputSteps = agentFinishSteps.returnValues.output as MultiOutputItem[]; + + // Check if all items in the multi-output array are of type 'text' + const isTextOnly = (multiOutputSteps ?? []).every((output) => 'text' in output); + + if (isTextOnly) { + // If all items are of type 'text', merge them into a single string + agentFinishSteps.returnValues.output = multiOutputSteps + .map((output) => output.text) + .join('\n') + .trim(); + } + return agentFinishSteps; + } + } + + // If the steps do not contain multiple outputs, return them as is + return agentFinishSteps; + } async function agentStepsParser( steps: AgentFinish | AgentAction[], ): Promise { @@ -112,7 +174,6 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise([agent, agentStepsParser]); + const runnableAgent = RunnableSequence.from([agent, agentStepsParser]); const executor = AgentExecutor.fromAgentAndTools({ agent: runnableAgent, @@ -196,7 +255,7 @@ export async function toolsAgentExecute(this: IExecuteFunctions): Promise