langchain-ai / langchainjs

🦜🔗 Build context-aware reasoning applications 🦜🔗
https://js.langchain.com/docs/
MIT License
12.66k stars 2.18k forks source link

Agent executor tool invocation doesn't recognize ToolInputParsingException error #6374

Closed mamlukishay closed 2 months ago

mamlukishay commented 3 months ago

Checked other resources

Example Code

The following tool always returns as successful (with empty observation), even if it fails schema validation.

enum CampaignOptimizationKPI {
  None = 'none',
  Conversions = 'conversions',
  Clicks = 'clicks',
  CTC = 'ctc',
  VideoCompletion = 'video-complation',
  Other = 'other',
}

const briefSchema = z.object({
  requestedKPIs: z.nativeEnum(CampaignOptimizationKPI).optional(),
});

const toolName = 'submit-campaign-brief';

const tool = new DynamicStructuredTool({
  name: toolName,
  description: 'submit incremental/full updates about the campaign brief object',
  schema: briefSchema,
  returnDirect: false,
  func: async (input) => {
    console.log('[2]', '|', toolName, '|', 'calling onChange with:', updatedBriefData);

    return 'OK, brief updated';
  },
});

Error Message and Stack Trace (if applicable)

No response

Description

I'm invoking a langchain tool through an agent executor, with a wrong schema. Langchain doesn't identify the error as a ToolInputParsingException, therefore my chain proceeds and I get wrong results.

This is my tool definition (simplified for the example):

enum CampaignOptimizationKPI {
  None = 'none',
  Conversions = 'conversions',
  Clicks = 'clicks',
  CTC = 'ctc',
  VideoCompletion = 'video-complation',
  Other = 'other',
}

const briefSchema = z.object({
  requestedKPIs: z.nativeEnum(CampaignOptimizationKPI).optional(),
});

const toolName = 'submit-campaign-brief';

const tool = new DynamicStructuredTool({
  name: toolName,
  description: 'submit incremental/full updates about the campaign brief object',
  schema: briefSchema,
  returnDirect: false,
  func: async (input) => {
    console.log(input);
    return 'OK, brief updated';
  },
});

The following input fails schema validation:

 {
    requestedKPIs: 'ctr',
  }

This is the stack trace:

Error: Received tool input did not match expected schema
    at DynamicStructuredTool.call (index.js:85:1)
    at async executor.js:474:1
    at async Promise.all (:5555/acc/5/campaigns/344/v/1/index 0)
    at async AgentExecutor._call (executor.js:466:1)
    at async AgentExecutor.invoke (base.js:55:1)
    at async RunnableSequence.invoke (base.js:1140:1)
    at async Object.getAssistantResponse (openai.api.ts:61:15)

This error is checked in executor.js for its instance type, but not recognized as the correct instance.

image

image

With this error being swallowed and never thrown, I don't have no ways to handle schema validation problems. This is bad, Please assist. 🙏

System Info

langchain@0.2.12 node v20.11.0 yarn v1.22.22

dosubot[bot] commented 3 months ago

Hey there, @mamlukishay! I'm here to assist you with your LangChain.js issue. Let's squash this bug together! 🐞

To handle schema validation errors in LangChain.js and ensure that the ToolInputParsingException is recognized so that the chain does not proceed incorrectly, you can use the call method of the StructuredTool class. This method parses the input according to the schema and throws a ToolInputParsingException if the input does not match the expected schema. Here is an example of how you can handle this:

import { z } from "zod";
import { ToolInputParsingException, StructuredTool } from "path/to/langchain-core/src/tools";

// Define your schema using Zod
const briefSchema = z.object({
  requestedKPIs: z.nativeEnum(CampaignOptimizationKPI).optional(),
});

// Create a custom tool by extending StructuredTool
class SubmitCampaignBriefTool extends StructuredTool<typeof briefSchema> {
  schema = briefSchema;

  async _call(arg: z.output<typeof briefSchema>): Promise<string> {
    // Your tool logic here
    return `OK, brief updated`;
  }
}

// Instantiate your tool
const tool = new SubmitCampaignBriefTool();

// Example usage
async function runTool(input: unknown) {
  try {
    const result = await tool.call(input);
    console.log(result);
  } catch (e) {
    if (e instanceof ToolInputParsingException) {
      console.error("Input parsing error:", e.message);
      // Handle the error appropriately
    } else {
      console.error("Unexpected error:", e);
    }
  }
}

// Test with invalid input
runTool({ requestedKPIs: 'ctr' });

Additionally, you can handle parsing errors more gracefully by using the handleParsingErrors option in the AgentExecutor class. Here is an example:

import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
import { pull } from "langchain/hub";
import { DynamicStructuredTool } from "@langchain/core/tools";

const model = new ChatOpenAI({ temperature: 0.1 });
const tools = [
  new DynamicStructuredTool({
    name: "submit-campaign-brief",
    description: "submit incremental/full updates about the campaign brief object",
    schema: briefSchema,
    func: async (input) => {
      console.log(input);
      return 'OK, brief updated';
    },
  }),
];

// Get the prompt to use - you can modify this!
const prompt = await pull<ChatPromptTemplate>(
  "hwchase17/openai-functions-agent"
);

const agent = await createOpenAIFunctionsAgent({
  llm: model,
  tools,
  prompt,
});

const agentExecutor = new AgentExecutor({
  agent,
  tools,
  verbose: true,
  handleParsingErrors:
    "Please try again, paying close attention to the allowed enum values",
});

console.log("Loaded agent.");
const input = `Set a reminder to renew our online property ads next week.`;

console.log(`Executing with input "${input}"...`);

const result = await agentExecutor.invoke({ input });

console.log({ result });

In this example, the handleParsingErrors option provides a custom message to guide the user in correcting their input, ensuring the chain does not proceed incorrectly [1][2].

To continue talking to Dosu, mention @dosu.

mamlukishay commented 3 months ago

@dosu Thanks for giving me a good running example - actually this is a perfect way to reproduce the issue. The following piece of code doesn't recognize the error as a ToolInputParsingException, therefore handleParsingErrors doesn't kick in and I can't use it.

The tool.call() approach works,, but the agentExecutor.invoke() approach doesn't. The problem happens in langchain/dist/agents/executor.js:482:

if (e instanceof ToolInputParsingException) returns false although the Error is a ToolInputParsingException

You can see the problem by running the following code. Instead of throwing a ToolInputParsingException, it returns a "successful" empty observation.

import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
import { pull } from "langchain/hub";
import { DynamicStructuredTool } from "@langchain/core/tools";

enum CampaignOptimizationKPI {
  None = 'none',
  Conversions = 'conversions',
  Clicks = 'clicks',
  CTC = 'ctc',
  VideoCompletion = 'video-complation',
  Other = 'other',
}

const briefSchema = z.object({
  requestedKPIs: z.nativeEnum(CampaignOptimizationKPI).optional(),
});

const model = new ChatOpenAI({ temperature: 0.1 });
const tools = [
  new DynamicStructuredTool({
    name: "submit-campaign-brief",
    description: "submit incremental/full updates about the campaign brief object",
    schema: briefSchema,
    func: async (input) => {
      console.log(input);
      return 'OK, brief updated';
    },
  }),
];

// Get the prompt to use - you can modify this!
const prompt = await pull<ChatPromptTemplate>(
  "hwchase17/openai-functions-agent"
);

const agent = await createOpenAIFunctionsAgent({
  llm: model,
  tools,
  prompt,
});

const agentExecutor = new AgentExecutor({
  agent,
  tools,
  verbose: true,
  handleParsingErrors:
    "Please try again, paying close attention to the allowed enum values",
});

console.log("Loaded agent.");
const input = `set requestedKPIs to 'ctr'`;

console.log(`Executing with input "${input}"...`);

const result = await agentExecutor.invoke({ input });

console.log({ result });
dosubot[bot] commented 3 months ago

To resolve the issue where the AgentExecutor in LangChain.js does not recognize a ToolInputParsingException error, causing the handleParsingErrors option to not activate and instead return a "successful" empty observation, you need to ensure that the handleParsingErrors option is correctly set and that the error handling logic is properly implemented.

From the provided code, it appears that the handleParsingErrors option is already defined and used in multiple places. However, you should verify that the handleParsingErrors option is correctly passed when creating the AgentExecutor instance and that the error handling logic is correctly implemented in the _call and _takeNextStep methods.

Here is a snippet of the relevant code sections with the error handling logic for ToolInputParsingException:

// In the _call method
const newSteps = await Promise.all(
  actions.map(async (action) => {
    await runManager?.handleAgentAction(action);
    const tool =
      action.tool === "_Exception"
        ? new ExceptionTool()
        : toolsByName[action.tool?.toLowerCase()];
    let observation;
    try {
      observation = tool
        ? await tool.invoke(
            action.toolInput,
            patchConfig(config, { callbacks: runManager?.getChild() })
          )
        : `${action.tool} is not a valid tool, try another one.`;
    } catch (e) {
      if (e instanceof ToolInputParsingException) {
        if (this.handleParsingErrors === true) {
          observation = "Invalid or incomplete tool input. Please try again.";
        } else if (typeof this.handleParsingErrors === "string") {
          observation = this.handleParsingErrors;
        } else if (typeof this.handleParsingErrors === "function") {
          observation = this.handleParsingErrors(e);
        } else {
          throw e;
        }
        observation = await new ExceptionTool().call(
          observation,
          runManager?.getChild()
        );
        return { action, observation: observation ?? "" };
      }
    }

    return { action, observation: observation ?? "" };
  })
);

// In the _takeNextStep method
for (const agentAction of actions) {
  let observation = "";
  if (runManager) {
    await runManager?.handleAgentAction(agentAction);
  }
  if (agentAction.tool in nameToolMap) {
    const tool = nameToolMap[agentAction.tool];
    try {
      observation = await tool.call(
        agentAction.toolInput,
        runManager?.getChild()
      );
    } catch (e) {
      if (e instanceof ToolInputParsingException) {
        if (this.handleParsingErrors === true) {
          observation = "Invalid or incomplete tool input. Please try again.";
        } else if (typeof this.handleParsingErrors === "string") {
          observation = this.handleParsingErrors;
        } else if (typeof this.handleParsingErrors === "function") {
          observation = this.handleParsingErrors(e);
        } else {
          throw e;
        }
        observation = await new ExceptionTool().call(
          observation,
          runManager?.getChild()
        );
      }
    }
  } else {
    observation = `${
      agentAction.tool
    } is not a valid tool, try another available tool: ${Object.keys(
      nameToolMap
    ).join(", ")}`;
  }
  result.push({
    action: agentAction,
    observation,
  });
}

Ensure that the handleParsingErrors option is correctly set when creating the AgentExecutor instance:

const executor = new AgentExecutor({
  agent: yourAgent,
  tools: yourTools,
  handleParsingErrors: true, // or a string or function as needed
});

By verifying and ensuring these implementations, the handleParsingErrors option should correctly handle ToolInputParsingException errors and prevent returning a "successful" empty observation [1].

To continue talking to Dosu, mention @dosu.

mamlukishay commented 3 months ago

This is not helpful, I need a human hand here

obround commented 2 months ago

The issue is in the output parser. For example, if you are using react, then this line is to blame for raising an Error instead of an OutputParserException. Changing:

throw new Error(`Could not parse LLM output: ${text}`);

to

throw new OutputParserException(`Could not parse LLM output: ${text}`);

solves the issue for me. If you can validate that this works for you too, I'll submit a pr.

mamlukishay commented 2 months ago

Your fix works and looks good, I think you should open a PR, although it's not exactly my case as I'm using open ai functions agents and not a react agent