diff --git a/langchain-core/src/tools/base.ts b/langchain-core/src/tools/base.ts index c981b174387c..21258f03baf6 100644 --- a/langchain-core/src/tools/base.ts +++ b/langchain-core/src/tools/base.ts @@ -13,9 +13,13 @@ import { ensureConfig, type RunnableConfig } from "../runnables/config.js"; import type { RunnableFunc, RunnableInterface } from "../runnables/base.js"; import { ToolCall, ToolMessage } from "../messages/tool.js"; import { ZodAny } from "../types/zod.js"; +import { MessageContent } from "../messages/base.js"; export type ResponseFormat = "content" | "contentAndRawOutput"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ContentAndRawOutput = [MessageContent, any]; + /** * Parameters for the Tool classes. */ @@ -119,7 +123,7 @@ export abstract class StructuredTool< arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise; + ): Promise; /** * Invokes the tool with the provided input and configuration. @@ -294,22 +298,19 @@ export interface BaseDynamicToolInput extends ToolParams { /** * Interface for the input parameters of the DynamicTool class. */ -export interface DynamicToolInput< - RunOutput extends string | ToolMessage = string -> extends BaseDynamicToolInput { +export interface DynamicToolInput extends BaseDynamicToolInput { func: ( input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; } /** * Interface for the input parameters of the DynamicStructuredTool class. */ export interface DynamicStructuredToolInput< - T extends ZodAny = ZodAny, - RunOutput extends string | ToolMessage = string + T extends ZodAny = ZodAny > extends BaseDynamicToolInput { func: ( input: BaseDynamicToolInput["responseFormat"] extends "contentAndRawOutput" @@ -317,7 +318,7 @@ export interface DynamicStructuredToolInput< : z.infer, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; schema: T; } @@ -335,9 +336,9 @@ export class DynamicTool< description: string; - func: DynamicToolInput["func"]; + func: DynamicToolInput["func"]; - constructor(fields: DynamicToolInput) { + constructor(fields: DynamicToolInput) { super(fields); this.name = fields.name; this.description = fields.description; @@ -364,7 +365,7 @@ export class DynamicTool< input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(input, runManager, config); } } @@ -387,11 +388,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; @@ -420,7 +421,7 @@ export class DynamicStructuredTool< arg: z.output | ToolCall, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(arg, runManager, config); } } @@ -491,9 +492,9 @@ interface ToolWrapperParams export function tool< RunInput extends ZodAny = ZodAny, RunOutput extends ToolMessage = ToolMessage, - FuncInput extends z.infer | ToolCall = z.infer + FuncInput extends z.infer | ToolCall = z.infer, >( - func: RunnableFunc, + func: RunnableFunc, fields: Omit, "responseFormat"> & { responseFormat: "contentAndRawOutput"; } @@ -502,9 +503,9 @@ export function tool< export function tool< RunInput extends ZodAny = ZodAny, RunOutput extends string = string, - FuncInput extends z.infer | ToolCall = z.infer + FuncInput extends z.infer | ToolCall = z.infer, >( - func: RunnableFunc, + func: RunnableFunc, fields: Omit, "responseFormat"> & { responseFormat?: "content" | undefined; } @@ -513,9 +514,10 @@ export function tool< export function tool< RunInput extends ZodAny = ZodAny, RunOutput extends string | ToolMessage = string, - FuncInput extends z.infer | ToolCall = z.infer + FuncInput extends z.infer | ToolCall = z.infer, + FuncOutput extends string | ContentAndRawOutput = string, >( - func: RunnableFunc, + func: RunnableFunc, fields: ToolWrapperParams ): DynamicStructuredTool { const schema = diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index 6e906aa75b90..c513ffb363c8 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -1,13 +1,14 @@ +import { test, expect } from "@jest/globals"; import { z } from "zod"; -import { tool } from "../base.js"; +import { ContentAndRawOutput, tool } from "../base.js"; import { ToolCall, ToolMessage } from "../../messages/tool.js"; -test("Tool should throw type error if responseFormat does not match func input type", () => { +test("Tool should throw type error if types are wrong", () => { const weatherSchema = z.object({ location: z.string(), }); - // @ts-expect-error - Error because responseFormat: contentAndRawOutput makes return type be an instance of ToolMessage + // @ts-expect-error - Error because responseFormat: contentAndRawOutput makes return type ContentAndRawOutput tool( (_): string => { return "no-op"; @@ -21,11 +22,8 @@ test("Tool should throw type error if responseFormat does not match func input t // @ts-expect-error - Error because responseFormat: content makes return type be a string tool( - (_): ToolMessage => { - return new ToolMessage({ - content: "no-op", - tool_call_id: "no-op", - }); + (_): ContentAndRawOutput => { + return ["no-op", true] }, { name: "weather", @@ -36,11 +34,8 @@ test("Tool should throw type error if responseFormat does not match func input t // @ts-expect-error - Error because responseFormat: undefined makes return type be a string tool( - (_): ToolMessage => { - return new ToolMessage({ - content: "no-op", - tool_call_id: "no-op", - }); + (_): ContentAndRawOutput => { + return ["no-op", true] }, { name: "weather", @@ -50,11 +45,8 @@ test("Tool should throw type error if responseFormat does not match func input t // Should pass because we're expecting a `ToolMessage` return type due to `responseFormat: contentAndRawOutput` tool( - (_): ToolMessage => { - return new ToolMessage({ - content: "no-op", - tool_call_id: "no-op", - }); + (_): ContentAndRawOutput => { + return ["no-op", true] }, { name: "weather", @@ -108,3 +100,47 @@ test("Tool should throw type error if responseFormat does not match func input t } ); }); + +test("Tool should error if responseFormat is contentAndRawOutput but the function doesn't return a tuple", async () => { + const weatherSchema = z.object({ + location: z.string(), + }); + + const weatherTool = tool( + (_): any => { + return "str" + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "contentAndRawOutput", + } + ); + + await expect(async () => { + await weatherTool.invoke({ location: "San Francisco" }); + }).rejects.toThrow(); +}); + +test.only("Tool works if responseFormat is contentAndRawOutput and returns a tuple", async () => { + const weatherSchema = z.object({ + location: z.string(), + }); + + const weatherTool = tool( + (input): any => { + return ["msg_content", input] + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "contentAndRawOutput", + } + ); + + const toolResult = await weatherTool.invoke({ location: "San Francisco" }); + + expect(toolResult).toBeInstanceOf(ToolMessage); + expect(toolResult.content).toBe("msg_content"); + expect(toolResult.raw_output).toEqual({ location: "San Francisco" }); +}); \ No newline at end of file