diff --git a/docs/core_docs/docs/how_to/custom_tools.ipynb b/docs/core_docs/docs/how_to/custom_tools.ipynb index 7cee783715b2..5027547a943b 100644 --- a/docs/core_docs/docs/how_to/custom_tools.ipynb +++ b/docs/core_docs/docs/how_to/custom_tools.ipynb @@ -1,5 +1,19 @@ { "cells": [ + { + "cell_type": "raw", + "id": "04171ad7", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "---\n", + "keywords: [custom tool, custom tools]\n", + "---" + ] + }, { "cell_type": "markdown", "id": "5436020b", @@ -16,14 +30,64 @@ "\n", ":::\n", "\n", - "When constructing your own agent, you will need to provide it with a list of Tools that it can use. While LangChain includes some prebuilt tools, it can often be more useful to use tools that use custom logic. This guide will walk you through how to use these `Dynamic` tools.\n", + "When constructing your own agent, you will need to provide it with a list of Tools that it can use. While LangChain includes some prebuilt tools, it can often be more useful to use tools that use custom logic. This guide will walk you through some ways you can create custom tools.\n", "\n", - "In this guide, we will walk through how to do define a tool for two functions:\n", + "The biggest difference here is that the first function requires an object with multiple input fields, while the second one only accepts an object with a single field. Some older agents only work with functions that require single inputs, so it's important to understand the distinction." + ] + }, + { + "cell_type": "markdown", + "id": "f6ec6ee8", + "metadata": {}, + "source": [ + "## `tool` function\n", "\n", - "1. A multiplier function that will multiply two numbers by each other\n", - "2. A made up search function that always returns the string \"LangChain\"\n", + ":::note\n", + "Only available in `@langchain/core` version 0.2.7 and above.\n", + ":::\n", "\n", - "The biggest difference here is that the first function requires an object with multiple input fields, while the second one only accepts an object with a single field. Some older agents only work with functions that require single inputs, so it's important to understand the distinction." + "\n", + "The [`tool`](https://api.js.langchain.com/classes/langchain_core_tools.tool.html) wrapper function is a convenience method for turning a JavaScript function into a tool. It requires the function itself along with some additional arguments that define your tool. The most important are:\n", + "\n", + "- The tool's `name`, which the LLM will use as context as well as to reference the tool\n", + "- An optional, but recommended `description`, which the LLM will use as context to know when to use the tool\n", + "- A `schema`, which defines the shape of the tool's input\n", + "\n", + "The `tool` function will return an instance of the [`StructuredTool`](https://api.js.langchain.com/classes/langchain_core_tools.StructuredTool.html) class, so it is compatible with all the existing tool calling infrastructure in the LangChain library." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ecc1ce9d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The sum of 1 and 2 is 3\n" + ] + } + ], + "source": [ + "import { z } from \"zod\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "\n", + "const adderSchema = z.object({\n", + " a: z.number(),\n", + " b: z.number(),\n", + "});\n", + "const adderTool = tool(async (input): Promise => {\n", + " const sum = input.a + input.b;\n", + " return `The sum of ${input.a} and ${input.b} is ${sum}`;\n", + "}, {\n", + " name: \"adder\",\n", + " description: \"Adds two numbers together\",\n", + " schema: adderSchema,\n", + "});\n", + "\n", + "await adderTool.invoke({ a: 1, b: 2 });" ] }, { @@ -33,7 +97,7 @@ "source": [ "## `DynamicStructuredTool`\n", "\n", - "Newer and more advanced agents can handle more flexible tools that take multiple inputs. You can use the [`DynamicStructuredTool`](https://v02.api.js.langchain.com/classes/langchain_core_tools.DynamicStructuredTool.html) class to declare them. Here's an example - note that tools must always return strings!" + "You can also use the [`DynamicStructuredTool`](https://api.js.langchain.com/classes/langchain_core_tools.DynamicStructuredTool.html) class to declare tools. Here's an example - note that tools must always return strings!" ] }, { @@ -112,6 +176,18 @@ "\n", "await searchTool.invoke(\"foo\");" ] + }, + { + "cell_type": "markdown", + "id": "8eceaf09", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "You've now seen a few ways to create custom tools in LangChain.\n", + "\n", + "Next, you might be interested in learning [how to use a chat model to call tools](/docs/how_to/tool_calling/)." + ] } ], "metadata": { @@ -121,12 +197,15 @@ "name": "deno" }, "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, "file_extension": ".ts", - "mimetype": "text/x.typescript", + "mimetype": "text/typescript", "name": "typescript", - "nb_converter": "script", - "pygments_lexer": "typescript", - "version": "5.3.3" + "version": "3.7.2" }, "vscode": { "interpreter": { diff --git a/docs/core_docs/docs/how_to/migrate_agent.ipynb b/docs/core_docs/docs/how_to/migrate_agent.ipynb index 33c57167b6ec..eacda605931a 100644 --- a/docs/core_docs/docs/how_to/migrate_agent.ipynb +++ b/docs/core_docs/docs/how_to/migrate_agent.ipynb @@ -49,7 +49,13 @@ "\n", "For basic creation and usage of a tool-calling ReAct-style agent, the\n", "functionality is the same. First, let's define a model and tool(s), then we'll\n", - "use those to create an agent.\n" + "use those to create an agent.\n", + "\n", + ":::note\n", + "The `tool` function is available in `@langchain/core` version 0.2.7 and above.\n", + "\n", + "If you are on an older version of core, you should use instantiate and use [`DynamicStructuredTool`](https://api.js.langchain.com/classes/langchain_core_tools.DynamicStructuredTool.html) instead.\n", + ":::" ] }, { @@ -61,7 +67,7 @@ }, "outputs": [], "source": [ - "import { DynamicStructuredTool } from \"@langchain/core/tools\";\n", + "import { tool } from \"@langchain/core/tools\";\n", "import { z } from \"zod\";\n", "import { ChatAnthropic } from \"@langchain/anthropic\";\n", "\n", @@ -70,15 +76,14 @@ " temperature: 0,\n", "});\n", "\n", - "const magicTool = new DynamicStructuredTool({\n", + "const magicTool = tool(async ({ input }: { input: number }) => {\n", + " return `${input + 2}`;\n", + "}, {\n", " name: \"magic_function\",\n", " description: \"Applies a magic function to an input.\",\n", " schema: z.object({\n", " input: z.number(),\n", " }),\n", - " func: async ({ input }: { input: number }) => {\n", - " return `${input + 2}`;\n", - " },\n", "});\n", "\n", "const tools = [magicTool];\n", @@ -1532,15 +1537,14 @@ } ], "source": [ - "const badMagicTool = new DynamicStructuredTool({\n", + "const badMagicTool = tool(async ({ input }) => {\n", + " return \"Sorry, there was an error. Please try again.\";\n", + "}, {\n", " name: \"magic_function\",\n", " description: \"Applies a magic function to an input.\",\n", " schema: z.object({\n", " input: z.string(),\n", " }),\n", - " func: async ({ input }) => {\n", - " return \"Sorry, there was an error. Please try again.\";\n", - " },\n", "});\n", "\n", "const badTools = [badMagicTool];\n", diff --git a/docs/core_docs/docs/how_to/tool_calling.ipynb b/docs/core_docs/docs/how_to/tool_calling.ipynb index ebf40e2a9eff..73c05d7cd136 100644 --- a/docs/core_docs/docs/how_to/tool_calling.ipynb +++ b/docs/core_docs/docs/how_to/tool_calling.ipynb @@ -87,16 +87,23 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A number of models implement helper methods that will take care of formatting and binding different function-like objects to the model.\n", - "Let's take a look at how we might take the following Zod function schema and get different models to invoke it:" + "We can use the `.bindTools()` method to handle the conversion from LangChain tool to our model provider's specific format and bind it to the model (i.e., passing it in each time the model is invoked). A number of models implement helper methods that will take care of formatting and binding different function-like objects to the model.\n", + "Let's create a new tool implementing a Zod schema, then bind it to the model:\n", + "\n", + ":::note\n", + "The `tool` function is available in `@langchain/core` version 0.2.7 and above.\n", + "\n", + "If you are on an older version of core, you should use instantiate and use [`DynamicStructuredTool`](https://api.js.langchain.com/classes/langchain_core_tools.DynamicStructuredTool.html) instead.\n", + ":::" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ + "import { tool } from \"@langchain/core/tools\";\n", "import { z } from \"zod\";\n", "\n", "/**\n", @@ -109,43 +116,25 @@ " .describe(\"The type of operation to execute.\"),\n", " number1: z.number().describe(\"The first number to operate on.\"),\n", " number2: z.number().describe(\"The second number to operate on.\"),\n", - "});" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can use the `.bindTools()` method to handle the conversion from LangChain tool to our model provider's specific format and bind it to the model (i.e., passing it in each time the model is invoked). Let's create a `DynamicStructuredTool` implementing a tool based on the above schema, then bind it to the model:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import { ChatOpenAI } from \"@langchain/openai\";\n", - "import { DynamicStructuredTool } from \"@langchain/core/tools\";\n", + "});\n", "\n", - "const calculatorTool = new DynamicStructuredTool({\n", + "const calculatorTool = tool(async ({ operation, number1, number2 }) => {\n", + " // Functions must return strings\n", + " if (operation === \"add\") {\n", + " return `${number1 + number2}`;\n", + " } else if (operation === \"subtract\") {\n", + " return `${number1 - number2}`;\n", + " } else if (operation === \"multiply\") {\n", + " return `${number1 * number2}`;\n", + " } else if (operation === \"divide\") {\n", + " return `${number1 / number2}`;\n", + " } else {\n", + " throw new Error(\"Invalid operation.\");\n", + " }\n", + "}, {\n", " name: \"calculator\",\n", " description: \"Can perform mathematical operations.\",\n", " schema: calculatorSchema,\n", - " func: async ({ operation, number1, number2 }) => {\n", - " // Functions must return strings\n", - " if (operation === \"add\") {\n", - " return `${number1 + number2}`;\n", - " } else if (operation === \"subtract\") {\n", - " return `${number1 - number2}`;\n", - " } else if (operation === \"multiply\") {\n", - " return `${number1 * number2}`;\n", - " } else if (operation === \"divide\") {\n", - " return `${number1 / number2}`;\n", - " } else {\n", - " throw new Error(\"Invalid operation.\");\n", - " }\n", - " }\n", "});\n", "\n", "const llmWithTools = llm.bindTools([calculatorTool]);" @@ -169,9 +158,9 @@ "text": [ "[\n", " {\n", - " name: \"calculator\",\n", - " args: { operation: \"multiply\", number1: 3, number2: 12 },\n", - " id: \"call_Ri9s27J17B224FEHrFGkLdxH\"\n", + " name: 'calculator',\n", + " args: { operation: 'multiply', number1: 3, number2: 12 },\n", + " id: 'call_5KWEQgV40XVoY1rqDhwyDmli'\n", " }\n", "]\n" ] @@ -189,7 +178,7 @@ "source": [ "```{=mdx}\n", ":::tip\n", - "See a LangSmith trace for the above [here](https://smith.langchain.com/public/14e4b50c-c6cf-4c53-b3ef-da550edb6d66/r).\n", + "See a LangSmith trace for the above [here](https://smith.langchain.com/public/b2222205-7da9-4a5a-8efe-6bc62347705d/r).\n", ":::\n", "```\n", "\n", @@ -538,12 +527,15 @@ "name": "deno" }, "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, "file_extension": ".ts", - "mimetype": "text/x.typescript", + "mimetype": "text/typescript", "name": "typescript", - "nb_converter": "script", - "pygments_lexer": "typescript", - "version": "5.3.3" + "version": "3.7.2" } }, "nbformat": 4, diff --git a/docs/core_docs/docs/how_to/tool_calls_multimodal.ipynb b/docs/core_docs/docs/how_to/tool_calls_multimodal.ipynb index ac2362f06d7c..a948c68811a1 100644 --- a/docs/core_docs/docs/how_to/tool_calls_multimodal.ipynb +++ b/docs/core_docs/docs/how_to/tool_calls_multimodal.ipynb @@ -22,7 +22,13 @@ "\n", "To call tools using such models, simply bind tools to them in the [usual way](/docs/how_to/tool_calling), and invoke the model using content blocks of the desired type (e.g., containing image data).\n", "\n", - "Below, we demonstrate examples using [OpenAI](/docs/integrations/platforms/openai) and [Anthropic](/docs/integrations/platforms/anthropic). We will use the same image and tool in all cases. Let's first select an image, and build a placeholder tool that expects as input the string \"sunny\", \"cloudy\", or \"rainy\". We will ask the models to describe the weather in the image." + "Below, we demonstrate examples using [OpenAI](/docs/integrations/platforms/openai) and [Anthropic](/docs/integrations/platforms/anthropic). We will use the same image and tool in all cases. Let's first select an image, and build a placeholder tool that expects as input the string \"sunny\", \"cloudy\", or \"rainy\". We will ask the models to describe the weather in the image.\n", + "\n", + ":::note\n", + "The `tool` function is available in `@langchain/core` version 0.2.7 and above.\n", + "\n", + "If you are on an older version of core, you should use instantiate and use [`DynamicStructuredTool`](https://api.js.langchain.com/classes/langchain_core_tools.DynamicStructuredTool.html) instead.\n", + ":::" ] }, { @@ -32,21 +38,20 @@ "metadata": {}, "outputs": [], "source": [ - "import { DynamicStructuredTool } from \"@langchain/core/tools\";\n", + "import { tool } from \"@langchain/core/tools\";\n", "import { z } from \"zod\";\n", "\n", "const imageUrl = \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg\";\n", "\n", - "const weatherTool = new DynamicStructuredTool({\n", + "const weatherTool = tool(async ({ weather }) => {\n", + " console.log(weather);\n", + " return weather;\n", + "}, {\n", " name: \"multiply\",\n", " description: \"Describe the weather\",\n", " schema: z.object({\n", " weather: z.enum([\"sunny\", \"cloudy\", \"rainy\"])\n", " }),\n", - " func: async ({ weather }) => {\n", - " console.log(weather);\n", - " return weather;\n", - " },\n", "});" ] }, diff --git a/langchain-core/src/runnables/tests/runnable_stream_events_v2.test.ts b/langchain-core/src/runnables/tests/runnable_stream_events_v2.test.ts index eb601a3bea83..b3e51d2b0347 100644 --- a/langchain-core/src/runnables/tests/runnable_stream_events_v2.test.ts +++ b/langchain-core/src/runnables/tests/runnable_stream_events_v2.test.ts @@ -24,7 +24,7 @@ import { HumanMessage, SystemMessage, } from "../../messages/index.js"; -import { DynamicStructuredTool, DynamicTool } from "../../tools.js"; +import { DynamicStructuredTool, DynamicTool, tool } from "../../tools.js"; import { Document } from "../../documents/document.js"; import { PromptTemplate } from "../../prompts/prompt.js"; import { GenerationChunk } from "../../outputs.js"; @@ -1823,6 +1823,76 @@ test("Runnable streamEvents method with simple tools", async () => { ]); }); +test("Runnable streamEvents method with tools that return objects", async () => { + const adderFunc = (_params: { x: number; y: number }) => { + return JSON.stringify({ sum: 3 }); + }; + const parameterlessTool = tool(adderFunc, { + name: "parameterless", + }); + const events = []; + const eventStream = parameterlessTool.streamEvents({}, { version: "v2" }); + for await (const event of eventStream) { + events.push(event); + } + + expect(events).toEqual([ + { + data: { input: {} }, + event: "on_tool_start", + metadata: {}, + name: "parameterless", + run_id: expect.any(String), + tags: [], + }, + { + data: { + output: JSON.stringify({ sum: 3 }), + }, + event: "on_tool_end", + metadata: {}, + name: "parameterless", + run_id: expect.any(String), + tags: [], + }, + ]); + + const adderTool = tool(adderFunc, { + name: "with_parameters", + description: "A tool that does nothing", + schema: z.object({ + x: z.number(), + y: z.number(), + }), + }); + const events2 = []; + const eventStream2 = adderTool.streamEvents( + { x: 1, y: 2 }, + { version: "v2" } + ); + for await (const event of eventStream2) { + events2.push(event); + } + expect(events2).toEqual([ + { + data: { input: { x: 1, y: 2 } }, + event: "on_tool_start", + metadata: {}, + name: "with_parameters", + run_id: expect.any(String), + tags: [], + }, + { + data: { output: JSON.stringify({ sum: 3 }) }, + event: "on_tool_end", + metadata: {}, + name: "with_parameters", + run_id: expect.any(String), + tags: [], + }, + ]); +}); + test("Runnable streamEvents method with a retriever", async () => { const retriever = new FakeRetriever({ output: [ diff --git a/langchain-core/src/tools.ts b/langchain-core/src/tools.ts index 803cc3c52409..252571ca084a 100644 --- a/langchain-core/src/tools.ts +++ b/langchain-core/src/tools.ts @@ -10,7 +10,10 @@ import { type BaseLangChainParams, } from "./language_models/base.js"; import { ensureConfig, type RunnableConfig } from "./runnables/config.js"; -import type { RunnableInterface } from "./runnables/base.js"; +import type { RunnableFunc, RunnableInterface } from "./runnables/base.js"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ZodAny = z.ZodObject; /** * Parameters for the Tool classes. @@ -31,10 +34,8 @@ export class ToolInputParsingException extends Error { } } -export interface StructuredToolInterface< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends z.ZodObject = z.ZodObject -> extends RunnableInterface< +export interface StructuredToolInterface + extends RunnableInterface< (z.output extends string ? string : never) | z.input, string > { @@ -71,8 +72,7 @@ export interface StructuredToolInterface< * Base class for Tools that accept input of any shape defined by a Zod schema. */ export abstract class StructuredTool< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends z.ZodObject = z.ZodObject + T extends ZodAny = ZodAny > extends BaseLangChain< (z.output extends string ? string : never) | z.input, string @@ -238,10 +238,8 @@ export interface DynamicToolInput extends BaseDynamicToolInput { /** * Interface for the input parameters of the DynamicStructuredTool class. */ -export interface DynamicStructuredToolInput< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends z.ZodObject = z.ZodObject -> extends BaseDynamicToolInput { +export interface DynamicStructuredToolInput + extends BaseDynamicToolInput { func: ( input: z.infer, runManager?: CallbackManagerForToolRun, @@ -303,9 +301,8 @@ export class DynamicTool extends Tool { * provided function when the tool is called. */ export class DynamicStructuredTool< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends z.ZodObject = z.ZodObject -> extends StructuredTool { + T extends ZodAny = ZodAny +> extends StructuredTool { static lc_name() { return "DynamicStructuredTool"; } @@ -314,7 +311,7 @@ export class DynamicStructuredTool< description: string; - func: DynamicStructuredToolInput["func"]; + func: DynamicStructuredToolInput["func"]; schema: T; @@ -364,3 +361,58 @@ export abstract class BaseToolkit { return this.tools; } } + +/** + * Parameters for the tool function. + * @template {ZodAny} RunInput The input schema for the tool. + */ +interface ToolWrapperParams + extends ToolParams { + /** + * The name of the tool. If using with an LLM, this + * will be passed as the tool name. + */ + name: string; + /** + * The description of the tool. + * @default `${fields.name} tool` + */ + description?: string; + /** + * The input schema for the tool. If using an LLM, this + * will be passed as the tool schema to generate arguments + * for. + */ + schema?: RunInput; +} + +/** + * Creates a new StructuredTool instance with the provided function, name, description, and schema. + * @function + * @template {ZodAny} RunInput The input schema for the tool. + * + * @param {RunnableFunc} func - The function to invoke when the tool is called. + * @param fields - An object containing the following properties: + * @param {string} fields.name The name of the tool. + * @param {string | undefined} fields.description The description of the tool. Defaults to either the description on the Zod schema, or `${fields.name} tool`. + * @param {z.ZodObject} fields.schema The Zod schema defining the input for the tool. + * + * @returns {StructuredTool} A new StructuredTool instance. + */ +export function tool( + func: RunnableFunc, string>, + fields: ToolWrapperParams +) { + const schema = + fields.schema ?? + z.object({ input: z.string().optional() }).transform((obj) => obj.input); + + const description = + fields.description ?? schema.description ?? `${fields.name} tool`; + return new DynamicStructuredTool({ + name: fields.name, + description, + schema: schema as RunInput, + func: async (input, _runManager, config) => func(input, config), + }); +}