diff --git a/docs/core_docs/docs/how_to/custom_tools.ipynb b/docs/core_docs/docs/how_to/custom_tools.ipynb index 7cee783715b2..b6cf2f4f8a1f 100644 --- a/docs/core_docs/docs/how_to/custom_tools.ipynb +++ b/docs/core_docs/docs/how_to/custom_tools.ipynb @@ -16,7 +16,7 @@ "\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 how to use these tools.\n", "\n", "In this guide, we will walk through how to do define a tool for two functions:\n", "\n", diff --git a/langchain-core/src/callbacks/base.ts b/langchain-core/src/callbacks/base.ts index a616cc4957c2..6d5ad7c5699b 100644 --- a/langchain-core/src/callbacks/base.ts +++ b/langchain-core/src/callbacks/base.ts @@ -197,7 +197,8 @@ abstract class BaseCallbackHandlerMethodsClass { * Called at the end of a Tool run, with the tool output and the run ID. */ handleToolEnd?( - output: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + output: string | Record, runId: string, parentRunId?: string, tags?: string[] diff --git a/langchain-core/src/callbacks/manager.ts b/langchain-core/src/callbacks/manager.ts index 9234b75e9ff6..f05739bbfe3e 100644 --- a/langchain-core/src/callbacks/manager.ts +++ b/langchain-core/src/callbacks/manager.ts @@ -488,7 +488,8 @@ export class CallbackManagerForToolRun ); } - async handleToolEnd(output: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async handleToolEnd(output: string | Record): Promise { await Promise.all( this.handlers.map((handler) => consumeCallback(async () => { diff --git a/langchain-core/src/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index d78bb04b2e0e..4e0762c4b76b 100644 --- a/langchain-core/src/language_models/chat_models.ts +++ b/langchain-core/src/language_models/chat_models.ts @@ -1,3 +1,4 @@ +import { z } from "zod"; import { AIMessage, type BaseMessage, @@ -147,8 +148,13 @@ export abstract class BaseChatModel< * specific tool schema. * @param kwargs Any additional parameters to bind. */ - bindTools?( - tools: (StructuredToolInterface | Record)[], + bindTools?< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends z.ZodObject = z.ZodObject, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string + >( + tools: (StructuredToolInterface | Record)[], kwargs?: Partial ): Runnable; diff --git a/langchain-core/src/messages/base.ts b/langchain-core/src/messages/base.ts index 1135c75b84f0..abb10abdf1d7 100644 --- a/langchain-core/src/messages/base.ts +++ b/langchain-core/src/messages/base.ts @@ -53,9 +53,10 @@ export type MessageContentComplex = | MessageContentText | MessageContentImageUrl // eslint-disable-next-line @typescript-eslint/no-explicit-any - | (Record & { type?: "text" | "image_url" | string }) + | (Record & { type?: "text" | "image_url" | "tool" | string }) // eslint-disable-next-line @typescript-eslint/no-explicit-any - | (Record & { type?: never }); + | (Record & { type?: never }) + | Record; export type MessageContent = string | MessageContentComplex[]; 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..98a87a4b7b8d 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,78 @@ 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 { 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: { + 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: { 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..aa570050f6b0 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. @@ -33,10 +36,12 @@ export class ToolInputParsingException extends Error { export interface StructuredToolInterface< // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends z.ZodObject = z.ZodObject + T extends ZodAny = ZodAny, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string > extends RunnableInterface< (z.output extends string ? string : never) | z.input, - string + RunOutput > { lc_namespace: string[]; @@ -58,7 +63,7 @@ export interface StructuredToolInterface< configArg?: Callbacks | RunnableConfig, /** @deprecated */ tags?: string[] - ): Promise; + ): Promise; name: string; @@ -72,10 +77,12 @@ export interface StructuredToolInterface< */ export abstract class StructuredTool< // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends z.ZodObject = z.ZodObject + T extends ZodAny = ZodAny, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string > extends BaseLangChain< (z.output extends string ? string : never) | z.input, - string + RunOutput > { abstract schema: T | z.ZodEffects; @@ -91,7 +98,7 @@ export abstract class StructuredTool< arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise; + ): Promise; /** * Invokes the tool with the provided input and configuration. @@ -102,7 +109,7 @@ export abstract class StructuredTool< async invoke( input: (z.output extends string ? string : never) | z.input, config?: RunnableConfig - ): Promise { + ): Promise { return this.call(input, ensureConfig(config)); } @@ -122,7 +129,7 @@ export abstract class StructuredTool< configArg?: Callbacks | RunnableConfig, /** @deprecated */ tags?: string[] - ): Promise { + ): Promise { let parsed; try { parsed = await this.schema.parseAsync(arg); @@ -189,7 +196,11 @@ export interface ToolInterface extends StructuredToolInterface { /** * Base class for Tools that accept input as a string. */ -export abstract class Tool extends StructuredTool { +export abstract class Tool< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string + // eslint-disable-next-line @typescript-eslint/no-explicit-any +> extends StructuredTool { schema = z .object({ input: z.string().optional() }) .transform((obj) => obj.input); @@ -210,7 +221,7 @@ export abstract class Tool extends StructuredTool { call( arg: string | undefined | z.input, callbacks?: Callbacks | RunnableConfig - ): Promise { + ): Promise { return super.call( typeof arg === "string" || !arg ? { input: arg } : arg, callbacks @@ -227,12 +238,15 @@ export interface BaseDynamicToolInput extends ToolParams { /** * Interface for the input parameters of the DynamicTool class. */ -export interface DynamicToolInput extends BaseDynamicToolInput { +export interface DynamicToolInput< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string +> extends BaseDynamicToolInput { func: ( input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; } /** @@ -240,20 +254,25 @@ export interface DynamicToolInput extends BaseDynamicToolInput { */ export interface DynamicStructuredToolInput< // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends z.ZodObject = z.ZodObject + T extends ZodAny = ZodAny, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string > extends BaseDynamicToolInput { func: ( input: z.infer, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; schema: T; } /** * A tool that can be created dynamically from a function, name, and description. */ -export class DynamicTool extends Tool { +export class DynamicTool< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string +> extends Tool { static lc_name() { return "DynamicTool"; } @@ -262,9 +281,9 @@ export class DynamicTool extends Tool { description: string; - func: DynamicToolInput["func"]; + func: DynamicToolInput["func"]; - constructor(fields: DynamicToolInput) { + constructor(fields: DynamicToolInput) { super(fields); this.name = fields.name; this.description = fields.description; @@ -278,7 +297,7 @@ export class DynamicTool extends Tool { async call( arg: string | undefined | z.input, configArg?: RunnableConfig | Callbacks - ): Promise { + ): Promise { const config = parseCallbackConfigArg(configArg); if (config.runName === undefined) { config.runName = this.name; @@ -291,7 +310,7 @@ export class DynamicTool extends Tool { input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(input, runManager, config); } } @@ -304,8 +323,10 @@ export class DynamicTool extends Tool { */ export class DynamicStructuredTool< // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends z.ZodObject = z.ZodObject -> extends StructuredTool { + T extends ZodAny = ZodAny, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string +> extends StructuredTool { static lc_name() { return "DynamicStructuredTool"; } @@ -314,11 +335,11 @@ export class DynamicStructuredTool< description: string; - func: DynamicStructuredToolInput["func"]; + func: DynamicStructuredToolInput["func"]; schema: T; - constructor(fields: DynamicStructuredToolInput) { + constructor(fields: DynamicStructuredToolInput) { super(fields); this.name = fields.name; this.description = fields.description; @@ -335,7 +356,7 @@ export class DynamicStructuredTool< configArg?: RunnableConfig | Callbacks, /** @deprecated */ tags?: string[] - ): Promise { + ): Promise { const config = parseCallbackConfigArg(configArg); if (config.runName === undefined) { config.runName = this.name; @@ -347,7 +368,7 @@ export class DynamicStructuredTool< arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(arg, runManager, config); } } @@ -364,3 +385,63 @@ 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. + * @template {string | Record} RunOutput The output 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 `${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< + RunInput extends ZodAny = ZodAny, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput extends string | Record = string +>( + func: RunnableFunc, RunOutput>, + fields: ToolWrapperParams +) { + const schema = + fields.schema ?? + z.object({ input: z.string().optional() }).transform((obj) => obj.input); + + return new DynamicStructuredTool({ + name: fields.name, + description: fields.description ?? `${fields.name} tool`, + schema: schema as RunInput, + func: (input, _runManager, config) => { + return Promise.resolve(func(input, config)); + }, + }); +} diff --git a/langchain-core/src/tracers/base.ts b/langchain-core/src/tracers/base.ts index 44a2cb62842d..60b3ccbaabd3 100644 --- a/langchain-core/src/tracers/base.ts +++ b/langchain-core/src/tracers/base.ts @@ -392,7 +392,11 @@ export abstract class BaseTracer extends BaseCallbackHandler { return run; } - async handleToolEnd(output: string, runId: string): Promise { + async handleToolEnd( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + output: string | Record, + runId: string + ): Promise { const run = this.runMap.get(runId); if (!run || run?.run_type !== "tool") { throw new Error("No tool run to end"); diff --git a/libs/langchain-standard-tests/src/integration_tests/chat_models.ts b/libs/langchain-standard-tests/src/integration_tests/chat_models.ts index d8416b743aea..fe3cac81d986 100644 --- a/libs/langchain-standard-tests/src/integration_tests/chat_models.ts +++ b/libs/langchain-standard-tests/src/integration_tests/chat_models.ts @@ -9,7 +9,7 @@ import { UsageMetadata, } from "@langchain/core/messages"; import { z } from "zod"; -import { StructuredTool } from "@langchain/core/tools"; +import { StructuredTool, tool } from "@langchain/core/tools"; import { BaseChatModelsTests, BaseChatModelsTestsFields, @@ -340,6 +340,73 @@ export abstract class ChatModelIntegrationTests< expect(resultStringContent).toBeInstanceOf(this.invokeResponseType); } + /** + * Test that model can process few-shot examples with tool calls + * that return objects instead of strings. + * @returns {Promise} + */ + async testStructuredFewShotExamplesToolObjectReturn( + callOptions?: InstanceType["ParsedCallOptions"] + ) { + if (!this.chatModelHasToolCalling) { + console.log("Test requires tool calling. Skipping..."); + return; + } + const adderFunc = (params: { a: number; b: number }) => ({ + sum: params.a + params.b, + }); + + const model = new this.Cls(this.constructorArgs); + const adderTool = tool(adderFunc, { + name: "AdderTool", + schema: z.object({ + a: z.number().int(), + b: z.number().int(), + }), + description: "Add two numbers", + }); + if (!model.bindTools) { + throw new Error("bindTools undefined. Cannot test few-shot examples."); + } + const modelWithTools = model.bindTools([adderTool]); + const functionName = adderTool.name; + const functionArgs = { a: 1, b: 2 }; + + const { functionId } = this; + const functionResult = await adderTool.invoke(functionArgs); + + const messagesStringContent = [ + new HumanMessage("What is 1 + 2"), + new AIMessage({ + content: "", + tool_calls: [ + { + name: functionName, + args: functionArgs, + id: functionId, + }, + ], + }), + new ToolMessage( + { + content: [functionResult], + }, + functionId, + functionName + ), + new AIMessage({ + content: [functionResult], + }), + new HumanMessage("What is 3 + 4"), + ]; + + const resultStringContent = await modelWithTools.invoke( + messagesStringContent, + callOptions + ); + expect(resultStringContent).toBeInstanceOf(this.invokeResponseType); + } + async testWithStructuredOutput() { if (!this.chatModelHasStructuredOutput) { console.log("Test requires withStructuredOutput. Skipping...");