From 6b8e1a1c208d2d173af197faaf601ef747d8aaed Mon Sep 17 00:00:00 2001 From: bracesproul Date: Mon, 8 Jul 2024 15:13:07 -0700 Subject: [PATCH 01/18] core[minor]: Allow tool functions to return ToolMessage --- langchain-core/src/callbacks/base.ts | 3 +- langchain-core/src/callbacks/manager.ts | 3 +- langchain-core/src/tools.ts | 87 +++++++++++++++---------- 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/langchain-core/src/callbacks/base.ts b/langchain-core/src/callbacks/base.ts index 229119697753..802e385ad0a2 100644 --- a/langchain-core/src/callbacks/base.ts +++ b/langchain-core/src/callbacks/base.ts @@ -16,6 +16,7 @@ import { import type { SerializedFields } from "../load/map_keys.js"; import type { DocumentInterface } from "../documents/document.js"; import { getEnvironmentVariable } from "../utils/env.js"; +import { ToolMessage } from "../messages/tool.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Error = any; @@ -197,7 +198,7 @@ abstract class BaseCallbackHandlerMethodsClass { * Called at the end of a Tool run, with the tool output and the run ID. */ handleToolEnd?( - output: string, + output: string | ToolMessage, runId: string, parentRunId?: string, tags?: string[] diff --git a/langchain-core/src/callbacks/manager.ts b/langchain-core/src/callbacks/manager.ts index 834ee61d6a38..a04aef323293 100644 --- a/langchain-core/src/callbacks/manager.ts +++ b/langchain-core/src/callbacks/manager.ts @@ -20,6 +20,7 @@ import { consumeCallback } from "./promises.js"; import { Serialized } from "../load/serializable.js"; import type { DocumentInterface } from "../documents/document.js"; import { isTracingEnabled } from "../utils/callbacks.js"; +import { ToolMessage } from "../messages/tool.js"; if ( /* #__PURE__ */ getEnvironmentVariable("LANGCHAIN_TRACING_V2") === "true" && @@ -493,7 +494,7 @@ export class CallbackManagerForToolRun ); } - async handleToolEnd(output: string): Promise { + async handleToolEnd(output: string | ToolMessage): Promise { await Promise.all( this.handlers.map((handler) => consumeCallback(async () => { diff --git a/langchain-core/src/tools.ts b/langchain-core/src/tools.ts index 252571ca084a..4e05f8cd8158 100644 --- a/langchain-core/src/tools.ts +++ b/langchain-core/src/tools.ts @@ -11,6 +11,7 @@ import { } from "./language_models/base.js"; import { ensureConfig, type RunnableConfig } from "./runnables/config.js"; import type { RunnableFunc, RunnableInterface } from "./runnables/base.js"; +import type { ToolMessage } from "./messages/tool.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ZodAny = z.ZodObject; @@ -34,10 +35,12 @@ export class ToolInputParsingException extends Error { } } -export interface StructuredToolInterface - extends RunnableInterface< +export interface StructuredToolInterface< + T extends ZodAny = ZodAny, + RunOutput extends string | ToolMessage = string +> extends RunnableInterface< (z.output extends string ? string : never) | z.input, - string + RunOutput > { lc_namespace: string[]; @@ -59,7 +62,7 @@ export interface StructuredToolInterface configArg?: Callbacks | RunnableConfig, /** @deprecated */ tags?: string[] - ): Promise; + ): Promise; name: string; @@ -72,10 +75,11 @@ export interface StructuredToolInterface * Base class for Tools that accept input of any shape defined by a Zod schema. */ export abstract class StructuredTool< - T extends ZodAny = ZodAny + T extends ZodAny = ZodAny, + RunOutput extends string | ToolMessage = string > extends BaseLangChain< (z.output extends string ? string : never) | z.input, - string + RunOutput > { abstract schema: T | z.ZodEffects; @@ -91,7 +95,7 @@ export abstract class StructuredTool< arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise; + ): Promise; /** * Invokes the tool with the provided input and configuration. @@ -102,8 +106,8 @@ export abstract class StructuredTool< async invoke( input: (z.output extends string ? string : never) | z.input, config?: RunnableConfig - ): Promise { - return this.call(input, ensureConfig(config)); + ): Promise { + return this.call(input, ensureConfig(config)) as Promise; } /** @@ -122,7 +126,7 @@ export abstract class StructuredTool< configArg?: Callbacks | RunnableConfig, /** @deprecated */ tags?: string[] - ): Promise { + ): Promise { let parsed; try { parsed = await this.schema.parseAsync(arg); @@ -170,7 +174,10 @@ export abstract class StructuredTool< returnDirect = false; } -export interface ToolInterface extends StructuredToolInterface { +export interface ToolInterface< + T extends ZodAny = ZodAny, + RunOutput extends string | ToolMessage = string +> extends StructuredToolInterface { /** * @deprecated Use .invoke() instead. Will be removed in 0.3.0. * @@ -183,13 +190,15 @@ export interface ToolInterface extends StructuredToolInterface { call( arg: string | undefined | z.input, callbacks?: Callbacks | RunnableConfig - ): Promise; + ): Promise; } /** * Base class for Tools that accept input as a string. */ -export abstract class Tool extends StructuredTool { +export abstract class Tool< + RunOutput extends string | ToolMessage = string +> extends StructuredTool { schema = z .object({ input: z.string().optional() }) .transform((obj) => obj.input); @@ -210,7 +219,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,31 +236,37 @@ export interface BaseDynamicToolInput extends ToolParams { /** * Interface for the input parameters of the DynamicTool class. */ -export interface DynamicToolInput extends BaseDynamicToolInput { +export interface DynamicToolInput< + RunOutput extends string | ToolMessage = string +> extends BaseDynamicToolInput { func: ( input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; } /** * Interface for the input parameters of the DynamicStructuredTool class. */ -export interface DynamicStructuredToolInput - extends BaseDynamicToolInput { +export interface DynamicStructuredToolInput< + T extends ZodAny = ZodAny, + RunOutput extends string | ToolMessage = 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< + RunOutput extends string | ToolMessage = string +> extends Tool { static lc_name() { return "DynamicTool"; } @@ -260,9 +275,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; @@ -276,7 +291,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; @@ -289,7 +304,7 @@ export class DynamicTool extends Tool { input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(input, runManager, config); } } @@ -301,8 +316,9 @@ export class DynamicTool extends Tool { * provided function when the tool is called. */ export class DynamicStructuredTool< - T extends ZodAny = ZodAny -> extends StructuredTool { + T extends ZodAny = ZodAny, + RunOutput extends string | ToolMessage = string +> extends StructuredTool { static lc_name() { return "DynamicStructuredTool"; } @@ -311,11 +327,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; @@ -332,7 +348,7 @@ export class DynamicStructuredTool< configArg?: RunnableConfig | Callbacks, /** @deprecated */ tags?: string[] - ): Promise { + ): Promise { const config = parseCallbackConfigArg(configArg); if (config.runName === undefined) { config.runName = this.name; @@ -344,7 +360,7 @@ export class DynamicStructuredTool< arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(arg, runManager, config); } } @@ -365,6 +381,7 @@ export abstract class BaseToolkit { /** * Parameters for the tool function. * @template {ZodAny} RunInput The input schema for the tool. + * @template {string | ToolMessage} RunOutput The output type for the tool. */ interface ToolWrapperParams extends ToolParams { @@ -390,6 +407,7 @@ interface ToolWrapperParams * 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 | ToolMessage} RunOutput The output type for the tool. * * @param {RunnableFunc} func - The function to invoke when the tool is called. * @param fields - An object containing the following properties: @@ -399,8 +417,11 @@ interface ToolWrapperParams * * @returns {StructuredTool} A new StructuredTool instance. */ -export function tool( - func: RunnableFunc, string>, +export function tool< + RunInput extends ZodAny = ZodAny, + RunOutput extends string | ToolMessage = string +>( + func: RunnableFunc, RunOutput>, fields: ToolWrapperParams ) { const schema = @@ -409,7 +430,7 @@ export function tool( const description = fields.description ?? schema.description ?? `${fields.name} tool`; - return new DynamicStructuredTool({ + return new DynamicStructuredTool({ name: fields.name, description, schema: schema as RunInput, From 69154aefa4df78e62c9ede8267c773d0b3e5b528 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Thu, 11 Jul 2024 17:18:06 -0700 Subject: [PATCH 02/18] nice generics brah --- langchain-core/langchain.config.js | 2 +- .../src/language_models/chat_models.ts | 2 +- langchain-core/src/load/import_map.ts | 2 +- langchain-core/src/messages/index.ts | 4 + langchain-core/src/messages/tool.ts | 49 ++++ langchain-core/src/messages/transformers.ts | 13 +- langchain-core/src/messages/utils.ts | 18 +- .../openai_tools/json_output_tools_parsers.ts | 13 +- .../tests/runnable_stream_events.test.ts | 2 +- .../tests/runnable_stream_events_v2.test.ts | 2 +- .../src/{tools.ts => tools/base.ts} | 245 +++++++++++++++--- langchain-core/src/tools/tests/tools.test.ts | 110 ++++++++ langchain-core/src/utils/function_calling.ts | 2 +- langchain-core/src/utils/testing/index.ts | 2 +- 14 files changed, 411 insertions(+), 55 deletions(-) rename langchain-core/src/{tools.ts => tools/base.ts} (61%) create mode 100644 langchain-core/src/tools/tests/tools.test.ts diff --git a/langchain-core/langchain.config.js b/langchain-core/langchain.config.js index 6c1b4e569be2..55f373990ebf 100644 --- a/langchain-core/langchain.config.js +++ b/langchain-core/langchain.config.js @@ -45,7 +45,7 @@ export const config = { singletons: "singletons/index", stores: "stores", "structured_query": "structured_query/index", - tools: "tools", + tools: "tools/base", "tracers/base": "tracers/base", "tracers/console": "tracers/console", "tracers/initialize": "tracers/initialize", diff --git a/langchain-core/src/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index ab84847c5757..102a7dc6eb14 100644 --- a/langchain-core/src/language_models/chat_models.ts +++ b/langchain-core/src/language_models/chat_models.ts @@ -33,7 +33,7 @@ import { } from "../callbacks/manager.js"; import type { RunnableConfig } from "../runnables/config.js"; import type { BaseCache } from "../caches/base.js"; -import { StructuredToolInterface } from "../tools.js"; +import { StructuredToolInterface } from "../tools/base.js"; import { Runnable, RunnableLambda, diff --git a/langchain-core/src/load/import_map.ts b/langchain-core/src/load/import_map.ts index 294f3feca879..5b7fce9f8e0e 100644 --- a/langchain-core/src/load/import_map.ts +++ b/langchain-core/src/load/import_map.ts @@ -22,7 +22,7 @@ export * as prompt_values from "../prompt_values.js"; export * as runnables from "../runnables/index.js"; export * as retrievers from "../retrievers/index.js"; export * as stores from "../stores.js"; -export * as tools from "../tools.js"; +export * as tools from "../tools/base.js"; export * as tracers__base from "../tracers/base.js"; export * as tracers__console from "../tracers/console.js"; export * as tracers__initialize from "../tracers/initialize.js"; diff --git a/langchain-core/src/messages/index.ts b/langchain-core/src/messages/index.ts index 3f0f49069f21..ebd8094f1d65 100644 --- a/langchain-core/src/messages/index.ts +++ b/langchain-core/src/messages/index.ts @@ -12,4 +12,8 @@ export { type ToolMessageFieldsWithToolCallId, ToolMessage, ToolMessageChunk, + type InvalidToolCall, + toolCall, + toolCallChunk, + invalidToolCall, } from "./tool.js"; diff --git a/langchain-core/src/messages/tool.ts b/langchain-core/src/messages/tool.ts index 996e2cdba8cc..e321ab5bff8a 100644 --- a/langchain-core/src/messages/tool.ts +++ b/langchain-core/src/messages/tool.ts @@ -136,8 +136,24 @@ export type ToolCall = { args: Record; id?: string; + + type?: "tool_call"; }; +export function toolCall(tc: { + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: Record; + id?: string; +}): ToolCall { + return { + name: tc.name, + args: tc.args, + id: tc.id, + type: "tool_call", + }; +} + /** * A chunk of a tool call (e.g., as part of a stream). * When merging ToolCallChunks (e.g., via AIMessageChunk.__add__), @@ -196,15 +212,48 @@ export type ToolCallChunk = { id?: string; index?: number; + + type?: "tool_call_chunk"; }; +export function toolCallChunk(chunk?: { + name?: string; + args?: string; + id?: string; + index?: number; +}): ToolCallChunk { + return { + name: chunk?.name, + args: chunk?.args, + id: chunk?.id, + index: chunk?.index, + type: "tool_call_chunk", + }; +} + export type InvalidToolCall = { name?: string; args?: string; id?: string; error?: string; + type?: "invalid_tool_call"; }; +export function invalidToolCall(tc?: { + name?: string; + args?: string; + id?: string; + error?: string; +}): InvalidToolCall { + return { + name: tc?.name, + args: tc?.args, + id: tc?.id, + error: tc?.error, + type: "invalid_tool_call", + }; +} + export function defaultToolCallParser( // eslint-disable-next-line @typescript-eslint/no-explicit-any rawToolCalls: Record[] diff --git a/langchain-core/src/messages/transformers.ts b/langchain-core/src/messages/transformers.ts index 19e2d09ec783..0c9b934aefb3 100644 --- a/langchain-core/src/messages/transformers.ts +++ b/langchain-core/src/messages/transformers.ts @@ -24,6 +24,7 @@ import { ToolMessage, ToolMessageChunk, ToolMessageFieldsWithToolCallId, + toolCallChunk as createToolCallChunk, } from "./tool.js"; import { convertToChunk } from "./utils.js"; @@ -950,11 +951,13 @@ function _switchTypeToMessage( if ("tool_calls" in aiChunkFields) { aiChunkFields = { ...aiChunkFields, - tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => ({ - ...tc, - index: undefined, - args: JSON.stringify(tc.args), - })), + tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => + createToolCallChunk({ + ...tc, + index: undefined, + args: JSON.stringify(tc.args), + }) + ), }; } chunk = new AIMessageChunk(aiChunkFields); diff --git a/langchain-core/src/messages/utils.ts b/langchain-core/src/messages/utils.ts index ac3c6b6594d2..85a96290d1d9 100644 --- a/langchain-core/src/messages/utils.ts +++ b/langchain-core/src/messages/utils.ts @@ -19,7 +19,11 @@ import { } from "./function.js"; import { HumanMessage, HumanMessageChunk } from "./human.js"; import { SystemMessage, SystemMessageChunk } from "./system.js"; -import { ToolMessage, ToolMessageFieldsWithToolCallId } from "./tool.js"; +import { + ToolMessage, + ToolMessageFieldsWithToolCallId, + toolCallChunk as createToolCallChunk, +} from "./tool.js"; function _constructMessageFromParams( params: BaseMessageFields & { type: string } @@ -190,11 +194,13 @@ export function convertToChunk(message: BaseMessage) { if ("tool_calls" in aiChunkFields) { aiChunkFields = { ...aiChunkFields, - tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => ({ - ...tc, - index: undefined, - args: JSON.stringify(tc.args), - })), + tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => + createToolCallChunk({ + ...tc, + index: undefined, + args: JSON.stringify(tc.args), + }) + ), }; } // eslint-disable-next-line @typescript-eslint/no-use-before-define diff --git a/langchain-core/src/output_parsers/openai_tools/json_output_tools_parsers.ts b/langchain-core/src/output_parsers/openai_tools/json_output_tools_parsers.ts index 1ee4add51450..631997ac592e 100644 --- a/langchain-core/src/output_parsers/openai_tools/json_output_tools_parsers.ts +++ b/langchain-core/src/output_parsers/openai_tools/json_output_tools_parsers.ts @@ -2,7 +2,12 @@ import { z } from "zod"; import { ChatGeneration } from "../../outputs.js"; import { BaseLLMOutputParser, OutputParserException } from "../base.js"; import { parsePartialJson } from "../json.js"; -import { InvalidToolCall, ToolCall } from "../../messages/tool.js"; +import { + InvalidToolCall, + ToolCall, + invalidToolCall as createInvalidToolCall, + toolCall as createToolCall, +} from "../../messages/tool.js"; export type ParsedToolCall = { id?: string; @@ -77,7 +82,7 @@ export function parseToolCall( parsedToolCall.id = rawToolCall.id; } - return parsedToolCall; + return createToolCall(parsedToolCall); } export function convertLangChainToolCallToOpenAI(toolCall: ToolCall) { @@ -99,12 +104,12 @@ export function makeInvalidToolCall( rawToolCall: Record, errorMsg?: string ): InvalidToolCall { - return { + return createInvalidToolCall({ name: rawToolCall.function?.name, args: rawToolCall.function?.arguments, id: rawToolCall.id, error: errorMsg, - }; + }); } /** diff --git a/langchain-core/src/runnables/tests/runnable_stream_events.test.ts b/langchain-core/src/runnables/tests/runnable_stream_events.test.ts index cc52c5d85e3b..70f145860926 100644 --- a/langchain-core/src/runnables/tests/runnable_stream_events.test.ts +++ b/langchain-core/src/runnables/tests/runnable_stream_events.test.ts @@ -22,7 +22,7 @@ import { SystemMessage, } from "../../messages/index.js"; import { ChatGenerationChunk, GenerationChunk } from "../../outputs.js"; -import { DynamicStructuredTool, DynamicTool } from "../../tools.js"; +import { DynamicStructuredTool, DynamicTool } from "../../tools/base.js"; import { Document } from "../../documents/document.js"; function reverse(s: string) { 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 b5ae10c85cd8..4a4120538a11 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, tool } from "../../tools.js"; +import { DynamicStructuredTool, DynamicTool, tool } from "../../tools/base.js"; import { Document } from "../../documents/document.js"; import { PromptTemplate } from "../../prompts/prompt.js"; import { GenerationChunk } from "../../outputs.js"; diff --git a/langchain-core/src/tools.ts b/langchain-core/src/tools/base.ts similarity index 61% rename from langchain-core/src/tools.ts rename to langchain-core/src/tools/base.ts index f4eae993a110..c981b174387c 100644 --- a/langchain-core/src/tools.ts +++ b/langchain-core/src/tools/base.ts @@ -4,20 +4,33 @@ import { CallbackManagerForToolRun, Callbacks, parseCallbackConfigArg, -} from "./callbacks/manager.js"; +} from "../callbacks/manager.js"; import { BaseLangChain, type BaseLangChainParams, -} from "./language_models/base.js"; -import { ensureConfig, type RunnableConfig } from "./runnables/config.js"; -import type { RunnableFunc, RunnableInterface } from "./runnables/base.js"; -import type { ToolMessage } from "./messages/tool.js"; -import { ZodAny } from "./types/zod.js"; +} from "../language_models/base.js"; +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"; + +export type ResponseFormat = "content" | "contentAndRawOutput"; /** * Parameters for the Tool classes. */ -export interface ToolParams extends BaseLangChainParams {} +export interface ToolParams extends BaseLangChainParams { + /** + * The tool response format. + * + * If "content" then the output of the tool is interpreted as the contents of a + * ToolMessage. If "contentAndRawOutput" then the output is expected to be a + * two-tuple corresponding to the (content, raw_output) of a ToolMessage. + * + * @default "content" + */ + responseFormat?: ResponseFormat; +} /** * Custom error class used to handle exceptions related to tool input parsing. @@ -56,7 +69,7 @@ export interface StructuredToolInterface< * @returns A Promise that resolves with a string. */ call( - arg: (z.output extends string ? string : never) | z.input, + arg: (z.output extends string ? string : never) | z.input | ToolCall, configArg?: Callbacks | RunnableConfig, /** @deprecated */ tags?: string[] @@ -76,7 +89,7 @@ export abstract class StructuredTool< T extends ZodAny = ZodAny, RunOutput extends string | ToolMessage = string > extends BaseLangChain< - (z.output extends string ? string : never) | z.input, + (z.output extends string ? string : never) | z.input | ToolCall, RunOutput > { abstract schema: T | z.ZodEffects; @@ -85,8 +98,21 @@ export abstract class StructuredTool< return ["langchain", "tools"]; } + /** + * The tool response format. + * + * If "content" then the output of the tool is interpreted as the contents of a + * ToolMessage. If "contentAndRawOutput" then the output is expected to be a + * two-tuple corresponding to the (content, raw_output) of a ToolMessage. + * + * @default "content" + */ + responseFormat?: ResponseFormat = "content"; + constructor(fields?: ToolParams) { super(fields ?? {}); + + this.responseFormat = fields?.responseFormat ?? this.responseFormat; } protected abstract _call( @@ -102,10 +128,14 @@ export abstract class StructuredTool< * @returns A Promise that resolves with a string. */ async invoke( - input: (z.output extends string ? string : never) | z.input, + input: + | (z.output extends string ? string : never) + | z.input + | ToolCall, config?: RunnableConfig ): Promise { - return this.call(input, ensureConfig(config)) as Promise; + const [toolInput, ensuredConfig] = await _prepRunArgs(input, config); + return this.call(toolInput, ensuredConfig) as Promise; } /** @@ -120,20 +150,26 @@ export abstract class StructuredTool< * @returns A Promise that resolves with a string. */ async call( - arg: (z.output extends string ? string : never) | z.input, + arg: (z.output extends string ? string : never) | z.input | ToolCall, configArg?: Callbacks | RunnableConfig, /** @deprecated */ tags?: string[] ): Promise { let parsed; - try { - parsed = await this.schema.parseAsync(arg); - } catch (e) { - throw new ToolInputParsingException( - `Received tool input did not match expected schema`, - JSON.stringify(arg) - ); + // Only parse the input if it's not a ToolCall + if (_isToolCall(arg)) { + parsed = arg; + } else { + try { + parsed = await this.schema.parseAsync(arg); + } catch (e) { + throw new ToolInputParsingException( + `Received tool input did not match expected schema`, + JSON.stringify(arg) + ); + } } + const config = parseCallbackConfigArg(configArg); const callbackManager_ = await CallbackManager.configure( config.callbacks, @@ -161,8 +197,32 @@ export abstract class StructuredTool< await runManager?.handleToolError(e); throw e; } - await runManager?.handleToolEnd(result); - return result; + let content; + let raw_output; + if (this.responseFormat === "contentAndRawOutput") { + if (Array.isArray(result) && result.length === 2) { + [content, raw_output] = result; + } else { + throw new Error( + `Tool response format is "contentAndRawOutput" but the output was not a two-tuple.\nResult: ${JSON.stringify( + result + )}` + ); + } + } else { + content = result; + } + + let tool_call_id: string | undefined; + if (config.callbacks && "configurable" in config.callbacks) { + tool_call_id = ( + config.callbacks.configurable as Record + ).tool_call; + } + const formattedOutput = _formatOutput(content, raw_output, tool_call_id); + + await runManager?.handleToolEnd(formattedOutput); + return formattedOutput as RunOutput; } abstract name: string; @@ -186,7 +246,7 @@ export interface ToolInterface< * @returns A Promise that resolves with a string. */ call( - arg: string | undefined | z.input, + arg: string | undefined | z.input | ToolCall, callbacks?: Callbacks | RunnableConfig ): Promise; } @@ -215,7 +275,7 @@ export abstract class Tool< * @returns A Promise that resolves with a string. */ call( - arg: string | undefined | z.input, + arg: string | undefined | z.input | ToolCall, callbacks?: Callbacks | RunnableConfig ): Promise { return super.call( @@ -252,7 +312,9 @@ export interface DynamicStructuredToolInput< RunOutput extends string | ToolMessage = string > extends BaseDynamicToolInput { func: ( - input: z.infer, + input: BaseDynamicToolInput["responseFormat"] extends "contentAndRawOutput" + ? ToolCall + : z.infer, runManager?: CallbackManagerForToolRun, config?: RunnableConfig ) => Promise; @@ -342,7 +404,7 @@ export class DynamicStructuredTool< * @deprecated Use .invoke() instead. Will be removed in 0.3.0. */ async call( - arg: z.output, + arg: z.output | ToolCall, configArg?: RunnableConfig | Callbacks, /** @deprecated */ tags?: string[] @@ -355,7 +417,7 @@ export class DynamicStructuredTool< } protected _call( - arg: z.output, + arg: z.output | ToolCall, runManager?: CallbackManagerForToolRun, config?: RunnableConfig ): Promise { @@ -399,29 +461,63 @@ interface ToolWrapperParams * for. */ schema?: RunInput; + /** + * The tool response format. + * + * If "content" then the output of the tool is interpreted as the contents of a + * ToolMessage. If "contentAndRawOutput" then the output is expected to be a + * two-tuple corresponding to the (content, raw_output) of a ToolMessage. + * + * @default "content" + */ + responseFormat?: ResponseFormat; } /** * 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 | ToolMessage} RunOutput The output type for the tool. + * @template {RunInput extends ZodAny = ZodAny} RunInput The input schema for the tool. This corresponds to the input type when the tool is invoked. + * @template {RunOutput extends string | ToolMessage = string} RunOutput The output type for the tool. This corresponds to the output type when the tool is invoked. + * @template {FuncInput extends z.infer | ToolCall = z.infer} FuncInput The input type for the function. * - * @param {RunnableFunc} func - The function to invoke when the tool is called. + * @param {RunnableFunc | ToolCall, RunOutput>} 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. + * @returns {DynamicStructuredTool} A new StructuredTool instance. */ export function tool< RunInput extends ZodAny = ZodAny, - RunOutput extends string | ToolMessage = string + RunOutput extends ToolMessage = ToolMessage, + FuncInput extends z.infer | ToolCall = z.infer +>( + func: RunnableFunc, + fields: Omit, "responseFormat"> & { + responseFormat: "contentAndRawOutput"; + } +): DynamicStructuredTool; + +export function tool< + RunInput extends ZodAny = ZodAny, + RunOutput extends string = string, + FuncInput extends z.infer | ToolCall = z.infer >( - func: RunnableFunc, RunOutput>, + func: RunnableFunc, + fields: Omit, "responseFormat"> & { + responseFormat?: "content" | undefined; + } +): DynamicStructuredTool; + +export function tool< + RunInput extends ZodAny = ZodAny, + RunOutput extends string | ToolMessage = string, + FuncInput extends z.infer | ToolCall = z.infer +>( + func: RunnableFunc, fields: ToolWrapperParams -) { +): DynamicStructuredTool { const schema = fields.schema ?? z.object({ input: z.string().optional() }).transform((obj) => obj.input); @@ -433,5 +529,88 @@ export function tool< description, schema: schema as RunInput, func: async (input, _runManager, config) => func(input, config), + responseFormat: fields.responseFormat, }); } + +function _isToolCall(toolCall?: unknown): toolCall is ToolCall { + return !!( + toolCall && + typeof toolCall === "object" && + "type" in toolCall && + toolCall.type === "tool_call" + ); +} + +async function _prepRunArgs( + input: (z.output extends string ? string : never) | z.input | ToolCall, + config?: RunnableConfig +): Promise< + [ + (z.output extends string ? string : never) | z.input | ToolCall, + Record + ] +> { + let tool_call_id: string | undefined; + let toolInput: + | (z.output extends string ? string : never) + | z.input + | ToolCall + | undefined; + + if (_isToolCall(input)) { + tool_call_id = input.id; + toolInput = input.args; + } else { + toolInput = input; + } + + const ensuredConfig = ensureConfig(config); + + return [ + toolInput, + { + ...ensuredConfig, + configurable: { + ...ensuredConfig.configurable, + tool_call_id, + }, + }, + ]; +} + +function _formatOutput( + content: unknown, + raw_output?: unknown, + tool_call_id?: string +): ToolMessage | string { + if (tool_call_id) { + if ( + typeof content === "string" || + (Array.isArray(content) && + content.every((item) => typeof item === "object")) + ) { + return new ToolMessage({ + content, + raw_output, + tool_call_id, + }); + } else { + return new ToolMessage({ + content: _stringify(content), + raw_output, + tool_call_id, + }); + } + } else { + return typeof content === "string" ? content : _stringify(content); + } +} + +function _stringify(content: unknown): string { + try { + return JSON.stringify(content, null, 2); + } catch (_noOp) { + return `${content}`; + } +} diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts new file mode 100644 index 000000000000..6e906aa75b90 --- /dev/null +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -0,0 +1,110 @@ +import { z } from "zod"; +import { 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", () => { + const weatherSchema = z.object({ + location: z.string(), + }); + + // @ts-expect-error - Error because responseFormat: contentAndRawOutput makes return type be an instance of ToolMessage + tool( + (_): string => { + return "no-op"; + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "contentAndRawOutput", + } + ); + + // @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", + }); + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "content", + } + ); + + // @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", + }); + }, + { + name: "weather", + schema: weatherSchema, + } + ); + + // 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", + }); + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "contentAndRawOutput", + } + ); + + // Should pass because we're expecting a `string` return type due to `responseFormat: content` + tool( + (_): string => { + return "no-op"; + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "content", + } + ); + + // Should pass because we're expecting a `string` return type due to `responseFormat: undefined` + tool( + (_): string => { + return "no-op"; + }, + { + name: "weather", + schema: weatherSchema, + } + ); + + tool( + // @ts-expect-error - Error because setting the third generic to `ToolCall` makes the input type of the function be `ToolCall` + (_: z.infer): string => { + return "no-op"; + }, + { + name: "weather", + schema: weatherSchema, + } + ); + + // This works because not setting any generics allows it to infer the correct types + tool( + (_: ToolCall): string => { + return "no-op"; + }, + { + name: "weather", + schema: weatherSchema, + } + ); +}); diff --git a/langchain-core/src/utils/function_calling.ts b/langchain-core/src/utils/function_calling.ts index cafc4c268e24..5a38a63a35d7 100644 --- a/langchain-core/src/utils/function_calling.ts +++ b/langchain-core/src/utils/function_calling.ts @@ -1,5 +1,5 @@ import { zodToJsonSchema } from "zod-to-json-schema"; -import { StructuredToolInterface } from "../tools.js"; +import { StructuredToolInterface } from "../tools/base.js"; import { FunctionDefinition, ToolDefinition } from "../language_models/base.js"; import { Runnable, RunnableToolLike } from "../runnables/base.js"; diff --git a/langchain-core/src/utils/testing/index.ts b/langchain-core/src/utils/testing/index.ts index 6eaf89177887..8dc939052e5e 100644 --- a/langchain-core/src/utils/testing/index.ts +++ b/langchain-core/src/utils/testing/index.ts @@ -32,7 +32,7 @@ import { } from "../../outputs.js"; import { BaseRetriever } from "../../retrievers/index.js"; import { Runnable, RunnableLambda } from "../../runnables/base.js"; -import { StructuredTool, ToolParams } from "../../tools.js"; +import { StructuredTool, ToolParams } from "../../tools/base.js"; import { BaseTracer, Run } from "../../tracers/base.js"; import { Embeddings, EmbeddingsParams } from "../../embeddings.js"; import { From 471116bba659f5b04715c3299e5f2fb4013b8a05 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Thu, 11 Jul 2024 18:00:45 -0700 Subject: [PATCH 03/18] cr --- langchain-core/src/tools/base.ts | 42 ++++++------ langchain-core/src/tools/tests/tools.test.ts | 72 +++++++++++++++----- 2 files changed, 76 insertions(+), 38 deletions(-) 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 From d01e3dea4188882c41adbc1d4db4175b281b53ed Mon Sep 17 00:00:00 2001 From: bracesproul Date: Thu, 11 Jul 2024 18:03:51 -0700 Subject: [PATCH 04/18] cr --- langchain-core/src/tools/base.ts | 11 +++++------ langchain-core/src/tools/tests/tools.test.ts | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/langchain-core/src/tools/base.ts b/langchain-core/src/tools/base.ts index 21258f03baf6..d29ca9c2ab40 100644 --- a/langchain-core/src/tools/base.ts +++ b/langchain-core/src/tools/base.ts @@ -309,9 +309,8 @@ export interface DynamicToolInput extends BaseDynamicToolInput { /** * Interface for the input parameters of the DynamicStructuredTool class. */ -export interface DynamicStructuredToolInput< - T extends ZodAny = ZodAny -> extends BaseDynamicToolInput { +export interface DynamicStructuredToolInput + extends BaseDynamicToolInput { func: ( input: BaseDynamicToolInput["responseFormat"] extends "contentAndRawOutput" ? ToolCall @@ -492,7 +491,7 @@ 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, fields: Omit, "responseFormat"> & { @@ -503,7 +502,7 @@ 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, fields: Omit, "responseFormat"> & { @@ -515,7 +514,7 @@ export function tool< RunInput extends ZodAny = ZodAny, RunOutput extends string | ToolMessage = string, FuncInput extends z.infer | ToolCall = z.infer, - FuncOutput extends string | ContentAndRawOutput = string, + FuncOutput extends string | ContentAndRawOutput = string >( func: RunnableFunc, fields: ToolWrapperParams diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index c513ffb363c8..632b877c2d4e 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -23,7 +23,7 @@ test("Tool should throw type error if types are wrong", () => { // @ts-expect-error - Error because responseFormat: content makes return type be a string tool( (_): ContentAndRawOutput => { - return ["no-op", true] + return ["no-op", true]; }, { name: "weather", @@ -35,7 +35,7 @@ test("Tool should throw type error if types are wrong", () => { // @ts-expect-error - Error because responseFormat: undefined makes return type be a string tool( (_): ContentAndRawOutput => { - return ["no-op", true] + return ["no-op", true]; }, { name: "weather", @@ -46,7 +46,7 @@ test("Tool should throw type error if types are wrong", () => { // Should pass because we're expecting a `ToolMessage` return type due to `responseFormat: contentAndRawOutput` tool( (_): ContentAndRawOutput => { - return ["no-op", true] + return ["no-op", true]; }, { name: "weather", @@ -107,8 +107,9 @@ test("Tool should error if responseFormat is contentAndRawOutput but the functio }); const weatherTool = tool( + // eslint-disable-next-line @typescript-eslint/no-explicit-any (_): any => { - return "str" + return "str"; }, { name: "weather", @@ -122,14 +123,15 @@ test("Tool should error if responseFormat is contentAndRawOutput but the functio }).rejects.toThrow(); }); -test.only("Tool works if responseFormat is contentAndRawOutput and returns a tuple", async () => { +test("Tool works if responseFormat is contentAndRawOutput and returns a tuple", async () => { const weatherSchema = z.object({ location: z.string(), }); const weatherTool = tool( + // eslint-disable-next-line @typescript-eslint/no-explicit-any (input): any => { - return ["msg_content", input] + return ["msg_content", input]; }, { name: "weather", @@ -139,8 +141,8 @@ test.only("Tool works if responseFormat is contentAndRawOutput and returns a tup ); 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 +}); From b2f16cebdfc24a4f2c03ae79607069bd036af0de Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Fri, 12 Jul 2024 15:59:32 -0700 Subject: [PATCH 05/18] Remove light methods --- langchain-core/langchain.config.js | 2 +- .../src/language_models/chat_models.ts | 2 +- langchain-core/src/load/import_map.ts | 2 +- langchain-core/src/messages/index.ts | 3 -- langchain-core/src/messages/tool.ts | 44 ------------------- langchain-core/src/messages/transformers.ts | 14 +++--- langchain-core/src/messages/utils.ts | 19 +++----- .../openai_tools/json_output_tools_parsers.ts | 15 +++---- .../tests/runnable_stream_events.test.ts | 2 +- .../tests/runnable_stream_events_v2.test.ts | 2 +- .../src/tools/{base.ts => index.ts} | 0 langchain-core/src/tools/tests/tools.test.ts | 2 +- langchain-core/src/utils/function_calling.ts | 2 +- langchain-core/src/utils/testing/index.ts | 2 +- 14 files changed, 27 insertions(+), 84 deletions(-) rename langchain-core/src/tools/{base.ts => index.ts} (100%) diff --git a/langchain-core/langchain.config.js b/langchain-core/langchain.config.js index 55f373990ebf..3a81fb79a2c1 100644 --- a/langchain-core/langchain.config.js +++ b/langchain-core/langchain.config.js @@ -45,7 +45,7 @@ export const config = { singletons: "singletons/index", stores: "stores", "structured_query": "structured_query/index", - tools: "tools/base", + tools: "tools/index", "tracers/base": "tracers/base", "tracers/console": "tracers/console", "tracers/initialize": "tracers/initialize", diff --git a/langchain-core/src/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index 102a7dc6eb14..1f39802436d0 100644 --- a/langchain-core/src/language_models/chat_models.ts +++ b/langchain-core/src/language_models/chat_models.ts @@ -33,7 +33,7 @@ import { } from "../callbacks/manager.js"; import type { RunnableConfig } from "../runnables/config.js"; import type { BaseCache } from "../caches/base.js"; -import { StructuredToolInterface } from "../tools/base.js"; +import { StructuredToolInterface } from "../tools/index.js"; import { Runnable, RunnableLambda, diff --git a/langchain-core/src/load/import_map.ts b/langchain-core/src/load/import_map.ts index 5b7fce9f8e0e..2c89691a5f4d 100644 --- a/langchain-core/src/load/import_map.ts +++ b/langchain-core/src/load/import_map.ts @@ -22,7 +22,7 @@ export * as prompt_values from "../prompt_values.js"; export * as runnables from "../runnables/index.js"; export * as retrievers from "../retrievers/index.js"; export * as stores from "../stores.js"; -export * as tools from "../tools/base.js"; +export * as tools from "../tools/index.js"; export * as tracers__base from "../tracers/base.js"; export * as tracers__console from "../tracers/console.js"; export * as tracers__initialize from "../tracers/initialize.js"; diff --git a/langchain-core/src/messages/index.ts b/langchain-core/src/messages/index.ts index ebd8094f1d65..c95a70a2b618 100644 --- a/langchain-core/src/messages/index.ts +++ b/langchain-core/src/messages/index.ts @@ -13,7 +13,4 @@ export { ToolMessage, ToolMessageChunk, type InvalidToolCall, - toolCall, - toolCallChunk, - invalidToolCall, } from "./tool.js"; diff --git a/langchain-core/src/messages/tool.ts b/langchain-core/src/messages/tool.ts index e321ab5bff8a..d2c8d5a0468d 100644 --- a/langchain-core/src/messages/tool.ts +++ b/langchain-core/src/messages/tool.ts @@ -140,20 +140,6 @@ export type ToolCall = { type?: "tool_call"; }; -export function toolCall(tc: { - name: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: Record; - id?: string; -}): ToolCall { - return { - name: tc.name, - args: tc.args, - id: tc.id, - type: "tool_call", - }; -} - /** * A chunk of a tool call (e.g., as part of a stream). * When merging ToolCallChunks (e.g., via AIMessageChunk.__add__), @@ -216,21 +202,6 @@ export type ToolCallChunk = { type?: "tool_call_chunk"; }; -export function toolCallChunk(chunk?: { - name?: string; - args?: string; - id?: string; - index?: number; -}): ToolCallChunk { - return { - name: chunk?.name, - args: chunk?.args, - id: chunk?.id, - index: chunk?.index, - type: "tool_call_chunk", - }; -} - export type InvalidToolCall = { name?: string; args?: string; @@ -239,21 +210,6 @@ export type InvalidToolCall = { type?: "invalid_tool_call"; }; -export function invalidToolCall(tc?: { - name?: string; - args?: string; - id?: string; - error?: string; -}): InvalidToolCall { - return { - name: tc?.name, - args: tc?.args, - id: tc?.id, - error: tc?.error, - type: "invalid_tool_call", - }; -} - export function defaultToolCallParser( // eslint-disable-next-line @typescript-eslint/no-explicit-any rawToolCalls: Record[] diff --git a/langchain-core/src/messages/transformers.ts b/langchain-core/src/messages/transformers.ts index 0c9b934aefb3..134c84222edf 100644 --- a/langchain-core/src/messages/transformers.ts +++ b/langchain-core/src/messages/transformers.ts @@ -24,7 +24,6 @@ import { ToolMessage, ToolMessageChunk, ToolMessageFieldsWithToolCallId, - toolCallChunk as createToolCallChunk, } from "./tool.js"; import { convertToChunk } from "./utils.js"; @@ -951,13 +950,12 @@ function _switchTypeToMessage( if ("tool_calls" in aiChunkFields) { aiChunkFields = { ...aiChunkFields, - tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => - createToolCallChunk({ - ...tc, - index: undefined, - args: JSON.stringify(tc.args), - }) - ), + tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => ({ + ...tc, + type: "tool_call_chunk", + index: undefined, + args: JSON.stringify(tc.args), + })), }; } chunk = new AIMessageChunk(aiChunkFields); diff --git a/langchain-core/src/messages/utils.ts b/langchain-core/src/messages/utils.ts index 85a96290d1d9..a17ce3853396 100644 --- a/langchain-core/src/messages/utils.ts +++ b/langchain-core/src/messages/utils.ts @@ -19,11 +19,7 @@ import { } from "./function.js"; import { HumanMessage, HumanMessageChunk } from "./human.js"; import { SystemMessage, SystemMessageChunk } from "./system.js"; -import { - ToolMessage, - ToolMessageFieldsWithToolCallId, - toolCallChunk as createToolCallChunk, -} from "./tool.js"; +import { ToolMessage, ToolMessageFieldsWithToolCallId } from "./tool.js"; function _constructMessageFromParams( params: BaseMessageFields & { type: string } @@ -194,13 +190,12 @@ export function convertToChunk(message: BaseMessage) { if ("tool_calls" in aiChunkFields) { aiChunkFields = { ...aiChunkFields, - tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => - createToolCallChunk({ - ...tc, - index: undefined, - args: JSON.stringify(tc.args), - }) - ), + tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => ({ + ...tc, + type: "tool_call_chunk", + index: undefined, + args: JSON.stringify(tc.args), + })), }; } // eslint-disable-next-line @typescript-eslint/no-use-before-define diff --git a/langchain-core/src/output_parsers/openai_tools/json_output_tools_parsers.ts b/langchain-core/src/output_parsers/openai_tools/json_output_tools_parsers.ts index 631997ac592e..fad6f82206a5 100644 --- a/langchain-core/src/output_parsers/openai_tools/json_output_tools_parsers.ts +++ b/langchain-core/src/output_parsers/openai_tools/json_output_tools_parsers.ts @@ -2,12 +2,7 @@ import { z } from "zod"; import { ChatGeneration } from "../../outputs.js"; import { BaseLLMOutputParser, OutputParserException } from "../base.js"; import { parsePartialJson } from "../json.js"; -import { - InvalidToolCall, - ToolCall, - invalidToolCall as createInvalidToolCall, - toolCall as createToolCall, -} from "../../messages/tool.js"; +import { InvalidToolCall, ToolCall } from "../../messages/tool.js"; export type ParsedToolCall = { id?: string; @@ -76,13 +71,14 @@ export function parseToolCall( const parsedToolCall: ToolCall = { name: rawToolCall.function.name, args: functionArgs, + type: "tool_call", }; if (options?.returnId) { parsedToolCall.id = rawToolCall.id; } - return createToolCall(parsedToolCall); + return parsedToolCall; } export function convertLangChainToolCallToOpenAI(toolCall: ToolCall) { @@ -104,12 +100,13 @@ export function makeInvalidToolCall( rawToolCall: Record, errorMsg?: string ): InvalidToolCall { - return createInvalidToolCall({ + return { name: rawToolCall.function?.name, args: rawToolCall.function?.arguments, id: rawToolCall.id, error: errorMsg, - }); + type: "invalid_tool_call", + }; } /** diff --git a/langchain-core/src/runnables/tests/runnable_stream_events.test.ts b/langchain-core/src/runnables/tests/runnable_stream_events.test.ts index 70f145860926..7b38857eac13 100644 --- a/langchain-core/src/runnables/tests/runnable_stream_events.test.ts +++ b/langchain-core/src/runnables/tests/runnable_stream_events.test.ts @@ -22,7 +22,7 @@ import { SystemMessage, } from "../../messages/index.js"; import { ChatGenerationChunk, GenerationChunk } from "../../outputs.js"; -import { DynamicStructuredTool, DynamicTool } from "../../tools/base.js"; +import { DynamicStructuredTool, DynamicTool } from "../../tools/index.js"; import { Document } from "../../documents/document.js"; function reverse(s: string) { 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 4a4120538a11..cac224beb543 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, tool } from "../../tools/base.js"; +import { DynamicStructuredTool, DynamicTool, tool } from "../../tools/index.js"; import { Document } from "../../documents/document.js"; import { PromptTemplate } from "../../prompts/prompt.js"; import { GenerationChunk } from "../../outputs.js"; diff --git a/langchain-core/src/tools/base.ts b/langchain-core/src/tools/index.ts similarity index 100% rename from langchain-core/src/tools/base.ts rename to langchain-core/src/tools/index.ts diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index 632b877c2d4e..c41373c34919 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "@jest/globals"; import { z } from "zod"; -import { ContentAndRawOutput, tool } from "../base.js"; +import { ContentAndRawOutput, tool } from "../index.js"; import { ToolCall, ToolMessage } from "../../messages/tool.js"; test("Tool should throw type error if types are wrong", () => { diff --git a/langchain-core/src/utils/function_calling.ts b/langchain-core/src/utils/function_calling.ts index 5a38a63a35d7..3871ffc4453d 100644 --- a/langchain-core/src/utils/function_calling.ts +++ b/langchain-core/src/utils/function_calling.ts @@ -1,5 +1,5 @@ import { zodToJsonSchema } from "zod-to-json-schema"; -import { StructuredToolInterface } from "../tools/base.js"; +import { StructuredToolInterface } from "../tools/index.js"; import { FunctionDefinition, ToolDefinition } from "../language_models/base.js"; import { Runnable, RunnableToolLike } from "../runnables/base.js"; diff --git a/langchain-core/src/utils/testing/index.ts b/langchain-core/src/utils/testing/index.ts index 8dc939052e5e..6591b67f26c9 100644 --- a/langchain-core/src/utils/testing/index.ts +++ b/langchain-core/src/utils/testing/index.ts @@ -32,7 +32,7 @@ import { } from "../../outputs.js"; import { BaseRetriever } from "../../retrievers/index.js"; import { Runnable, RunnableLambda } from "../../runnables/base.js"; -import { StructuredTool, ToolParams } from "../../tools/base.js"; +import { StructuredTool, ToolParams } from "../../tools/index.js"; import { BaseTracer, Run } from "../../tracers/base.js"; import { Embeddings, EmbeddingsParams } from "../../embeddings.js"; import { From 68a520f1f4f46113d7c10f5caca90a9fb75947d7 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 13 Jul 2024 08:02:06 -0700 Subject: [PATCH 06/18] Properly return tool message --- langchain-core/src/tools/index.ts | 172 +++++++++---------- langchain-core/src/tools/tests/tools.test.ts | 54 ++++-- 2 files changed, 118 insertions(+), 108 deletions(-) diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index d29ca9c2ab40..18338068c8f2 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -15,10 +15,10 @@ import { ToolCall, ToolMessage } from "../messages/tool.js"; import { ZodAny } from "../types/zod.js"; import { MessageContent } from "../messages/base.js"; -export type ResponseFormat = "content" | "contentAndRawOutput"; +export type ResponseFormat = "content" | "content_and_artifact"; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ContentAndRawOutput = [MessageContent, any]; +export type ContentAndArtifact = [MessageContent, any]; /** * Parameters for the Tool classes. @@ -28,8 +28,8 @@ export interface ToolParams extends BaseLangChainParams { * The tool response format. * * If "content" then the output of the tool is interpreted as the contents of a - * ToolMessage. If "contentAndRawOutput" then the output is expected to be a - * two-tuple corresponding to the (content, raw_output) of a ToolMessage. + * ToolMessage. If "content_and_artifact" then the output is expected to be a + * two-tuple corresponding to the (content, artifact) of a ToolMessage. * * @default "content" */ @@ -96,8 +96,14 @@ export abstract class StructuredTool< (z.output extends string ? string : never) | z.input | ToolCall, RunOutput > { + abstract name: string; + + abstract description: string; + abstract schema: T | z.ZodEffects; + returnDirect = false; + get lc_namespace() { return ["langchain", "tools"]; } @@ -106,8 +112,8 @@ export abstract class StructuredTool< * The tool response format. * * If "content" then the output of the tool is interpreted as the contents of a - * ToolMessage. If "contentAndRawOutput" then the output is expected to be a - * two-tuple corresponding to the (content, raw_output) of a ToolMessage. + * ToolMessage. If "content_and_artifact" then the output is expected to be a + * two-tuple corresponding to the (content, artifact) of a ToolMessage. * * @default "content" */ @@ -123,7 +129,7 @@ export abstract class StructuredTool< arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise; + ): Promise; /** * Invokes the tool with the provided input and configuration. @@ -138,8 +144,29 @@ export abstract class StructuredTool< | ToolCall, config?: RunnableConfig ): Promise { - const [toolInput, ensuredConfig] = await _prepRunArgs(input, config); - return this.call(toolInput, ensuredConfig) as Promise; + let tool_call_id: string | undefined; + let toolInput: + | (z.output extends string ? string : never) + | z.input + | ToolCall + | undefined; + + if (_isToolCall(input)) { + console.log("is TC"); + tool_call_id = input.id; + toolInput = input.args; + } else { + toolInput = input; + } + + const ensuredConfig = ensureConfig(config); + return this.call(toolInput, { + ...ensuredConfig, + configurable: { + ...ensuredConfig.configurable, + tool_call_id, + }, + }) as Promise; } /** @@ -160,18 +187,13 @@ export abstract class StructuredTool< tags?: string[] ): Promise { let parsed; - // Only parse the input if it's not a ToolCall - if (_isToolCall(arg)) { - parsed = arg; - } else { - try { - parsed = await this.schema.parseAsync(arg); - } catch (e) { - throw new ToolInputParsingException( - `Received tool input did not match expected schema`, - JSON.stringify(arg) - ); - } + try { + parsed = await this.schema.parseAsync(arg); + } catch (e) { + throw new ToolInputParsingException( + `Received tool input did not match expected schema`, + JSON.stringify(arg) + ); } const config = parseCallbackConfigArg(configArg); @@ -202,13 +224,13 @@ export abstract class StructuredTool< throw e; } let content; - let raw_output; - if (this.responseFormat === "contentAndRawOutput") { + let artifact; + if (this.responseFormat === "content_and_artifact") { if (Array.isArray(result) && result.length === 2) { - [content, raw_output] = result; + [content, artifact] = result; } else { throw new Error( - `Tool response format is "contentAndRawOutput" but the output was not a two-tuple.\nResult: ${JSON.stringify( + `Tool response format is "content_and_artifact" but the output was not a two-tuple.\nResult: ${JSON.stringify( result )}` ); @@ -217,23 +239,19 @@ export abstract class StructuredTool< content = result; } - let tool_call_id: string | undefined; - if (config.callbacks && "configurable" in config.callbacks) { - tool_call_id = ( - config.callbacks.configurable as Record - ).tool_call; + let toolCallId: string | undefined; + if (config && "configurable" in config) { + toolCallId = (config.configurable as Record) + .tool_call_id; } - const formattedOutput = _formatOutput(content, raw_output, tool_call_id); - + const formattedOutput = _formatToolOutput({ + content, + artifact, + toolCallId, + }); await runManager?.handleToolEnd(formattedOutput); return formattedOutput as RunOutput; } - - abstract name: string; - - abstract description: string; - - returnDirect = false; } export interface ToolInterface< @@ -303,7 +321,7 @@ export interface DynamicToolInput extends BaseDynamicToolInput { input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; } /** @@ -312,12 +330,12 @@ export interface DynamicToolInput extends BaseDynamicToolInput { export interface DynamicStructuredToolInput extends BaseDynamicToolInput { func: ( - input: BaseDynamicToolInput["responseFormat"] extends "contentAndRawOutput" + input: BaseDynamicToolInput["responseFormat"] extends "content_and_artifact" ? ToolCall : z.infer, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; schema: T; } @@ -364,7 +382,7 @@ export class DynamicTool< input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(input, runManager, config); } } @@ -420,7 +438,7 @@ export class DynamicStructuredTool< arg: z.output | ToolCall, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(arg, runManager, config); } } @@ -465,8 +483,8 @@ interface ToolWrapperParams * The tool response format. * * If "content" then the output of the tool is interpreted as the contents of a - * ToolMessage. If "contentAndRawOutput" then the output is expected to be a - * two-tuple corresponding to the (content, raw_output) of a ToolMessage. + * ToolMessage. If "content_and_artifact" then the output is expected to be a + * two-tuple corresponding to the (content, artifact) of a ToolMessage. * * @default "content" */ @@ -493,9 +511,9 @@ export function tool< RunOutput extends ToolMessage = ToolMessage, FuncInput extends z.infer | ToolCall = z.infer >( - func: RunnableFunc, + func: RunnableFunc, fields: Omit, "responseFormat"> & { - responseFormat: "contentAndRawOutput"; + responseFormat: "content_and_artifact"; } ): DynamicStructuredTool; @@ -514,7 +532,7 @@ export function tool< RunInput extends ZodAny = ZodAny, RunOutput extends string | ToolMessage = string, FuncInput extends z.infer | ToolCall = z.infer, - FuncOutput extends string | ContentAndRawOutput = string + FuncOutput extends string | ContentAndArtifact = string >( func: RunnableFunc, fields: ToolWrapperParams @@ -543,49 +561,13 @@ function _isToolCall(toolCall?: unknown): toolCall is ToolCall { ); } -async function _prepRunArgs( - input: (z.output extends string ? string : never) | z.input | ToolCall, - config?: RunnableConfig -): Promise< - [ - (z.output extends string ? string : never) | z.input | ToolCall, - Record - ] -> { - let tool_call_id: string | undefined; - let toolInput: - | (z.output extends string ? string : never) - | z.input - | ToolCall - | undefined; - - if (_isToolCall(input)) { - tool_call_id = input.id; - toolInput = input.args; - } else { - toolInput = input; - } - - const ensuredConfig = ensureConfig(config); - - return [ - toolInput, - { - ...ensuredConfig, - configurable: { - ...ensuredConfig.configurable, - tool_call_id, - }, - }, - ]; -} - -function _formatOutput( - content: unknown, - raw_output?: unknown, - tool_call_id?: string -): ToolMessage | string { - if (tool_call_id) { +function _formatToolOutput(params: { + content: unknown; + artifact?: unknown; + toolCallId?: string; +}): ToolMessage | string { + const { content, artifact, toolCallId } = params; + if (toolCallId) { if ( typeof content === "string" || (Array.isArray(content) && @@ -593,14 +575,14 @@ function _formatOutput( ) { return new ToolMessage({ content, - raw_output, - tool_call_id, + artifact, + tool_call_id: toolCallId, }); } else { return new ToolMessage({ content: _stringify(content), - raw_output, - tool_call_id, + artifact, + tool_call_id: toolCallId, }); } } else { diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index c41373c34919..839684c5b57f 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "@jest/globals"; import { z } from "zod"; -import { ContentAndRawOutput, tool } from "../index.js"; +import { ContentAndArtifact, tool } from "../index.js"; import { ToolCall, ToolMessage } from "../../messages/tool.js"; test("Tool should throw type error if types are wrong", () => { @@ -8,7 +8,7 @@ test("Tool should throw type error if types are wrong", () => { location: z.string(), }); - // @ts-expect-error - Error because responseFormat: contentAndRawOutput makes return type ContentAndRawOutput + // @ts-expect-error - Error because responseFormat: content_and_artifact makes return type ContentAndArtifact tool( (_): string => { return "no-op"; @@ -16,13 +16,13 @@ test("Tool should throw type error if types are wrong", () => { { name: "weather", schema: weatherSchema, - responseFormat: "contentAndRawOutput", + responseFormat: "content_and_artifact", } ); // @ts-expect-error - Error because responseFormat: content makes return type be a string tool( - (_): ContentAndRawOutput => { + (_): ContentAndArtifact => { return ["no-op", true]; }, { @@ -34,7 +34,7 @@ test("Tool should throw type error if types are wrong", () => { // @ts-expect-error - Error because responseFormat: undefined makes return type be a string tool( - (_): ContentAndRawOutput => { + (_): ContentAndArtifact => { return ["no-op", true]; }, { @@ -43,15 +43,15 @@ test("Tool should throw type error if types are wrong", () => { } ); - // Should pass because we're expecting a `ToolMessage` return type due to `responseFormat: contentAndRawOutput` + // Should pass because we're expecting a `ToolMessage` return type due to `responseFormat: content_and_artifact` tool( - (_): ContentAndRawOutput => { + (_): ContentAndArtifact => { return ["no-op", true]; }, { name: "weather", schema: weatherSchema, - responseFormat: "contentAndRawOutput", + responseFormat: "content_and_artifact", } ); @@ -101,7 +101,7 @@ test("Tool should throw type error if types are wrong", () => { ); }); -test("Tool should error if responseFormat is contentAndRawOutput but the function doesn't return a tuple", async () => { +test("Tool should error if responseFormat is content_and_artifact but the function doesn't return a tuple", async () => { const weatherSchema = z.object({ location: z.string(), }); @@ -114,7 +114,7 @@ test("Tool should error if responseFormat is contentAndRawOutput but the functio { name: "weather", schema: weatherSchema, - responseFormat: "contentAndRawOutput", + responseFormat: "content_and_artifact", } ); @@ -123,7 +123,7 @@ test("Tool should error if responseFormat is contentAndRawOutput but the functio }).rejects.toThrow(); }); -test("Tool works if responseFormat is contentAndRawOutput and returns a tuple", async () => { +test("Tool works if responseFormat is content_and_artifact and returns a tuple", async () => { const weatherSchema = z.object({ location: z.string(), }); @@ -136,13 +136,41 @@ test("Tool works if responseFormat is contentAndRawOutput and returns a tuple", { name: "weather", schema: weatherSchema, - responseFormat: "contentAndRawOutput", + responseFormat: "content_and_artifact", } ); const toolResult = await weatherTool.invoke({ location: "San Francisco" }); + expect(toolResult).not.toBeInstanceOf(ToolMessage); + expect(toolResult).toBe("msg_content"); +}); + +test("Returns tool message if responseFormat is content_and_artifact and returns a tuple and a tool call is passed in", async () => { + const weatherSchema = z.object({ + location: z.string(), + }); + + const weatherTool = tool( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (input): any => { + return ["msg_content", input]; + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "content_and_artifact", + } + ); + + const toolResult = await weatherTool.invoke({ + id: "testid", + args: { location: "San Francisco" }, + name: "weather", + type: "tool_call", + }); + expect(toolResult).toBeInstanceOf(ToolMessage); expect(toolResult.content).toBe("msg_content"); - expect(toolResult.raw_output).toEqual({ location: "San Francisco" }); + expect(toolResult.artifact).toEqual({ location: "San Francisco" }); }); From 4be96c2b9b4aeb8b52ee588b927dc129445768cf Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 13 Jul 2024 08:31:05 -0700 Subject: [PATCH 07/18] Relax types to allow tools to return any --- langchain-core/src/callbacks/base.ts | 4 +- langchain-core/src/callbacks/manager.ts | 4 +- langchain-core/src/messages/ai.ts | 2 + .../src/messages/tests/message_utils.test.ts | 4 +- langchain-core/src/tools/index.ts | 52 ++++++------------- langchain-core/src/tools/tests/tools.test.ts | 47 ++++++++++------- langchain-core/src/tracers/base.ts | 3 +- 7 files changed, 55 insertions(+), 61 deletions(-) diff --git a/langchain-core/src/callbacks/base.ts b/langchain-core/src/callbacks/base.ts index 802e385ad0a2..389411b45845 100644 --- a/langchain-core/src/callbacks/base.ts +++ b/langchain-core/src/callbacks/base.ts @@ -16,7 +16,6 @@ import { import type { SerializedFields } from "../load/map_keys.js"; import type { DocumentInterface } from "../documents/document.js"; import { getEnvironmentVariable } from "../utils/env.js"; -import { ToolMessage } from "../messages/tool.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Error = any; @@ -198,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 | ToolMessage, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + output: any, runId: string, parentRunId?: string, tags?: string[] diff --git a/langchain-core/src/callbacks/manager.ts b/langchain-core/src/callbacks/manager.ts index a04aef323293..11548a4b4212 100644 --- a/langchain-core/src/callbacks/manager.ts +++ b/langchain-core/src/callbacks/manager.ts @@ -20,7 +20,6 @@ import { consumeCallback } from "./promises.js"; import { Serialized } from "../load/serializable.js"; import type { DocumentInterface } from "../documents/document.js"; import { isTracingEnabled } from "../utils/callbacks.js"; -import { ToolMessage } from "../messages/tool.js"; if ( /* #__PURE__ */ getEnvironmentVariable("LANGCHAIN_TRACING_V2") === "true" && @@ -494,7 +493,8 @@ export class CallbackManagerForToolRun ); } - async handleToolEnd(output: string | ToolMessage): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async handleToolEnd(output: any): Promise { await Promise.all( this.handlers.map((handler) => consumeCallback(async () => { diff --git a/langchain-core/src/messages/ai.ts b/langchain-core/src/messages/ai.ts index 7f98f4c3914e..a0148dd2dd3c 100644 --- a/langchain-core/src/messages/ai.ts +++ b/langchain-core/src/messages/ai.ts @@ -187,6 +187,7 @@ export class AIMessageChunk extends BaseMessageChunk { name: toolCallChunk.name ?? "", args: parsedArgs, id: toolCallChunk.id, + type: "tool_call", }); } catch (e) { invalidToolCalls.push({ @@ -194,6 +195,7 @@ export class AIMessageChunk extends BaseMessageChunk { args: toolCallChunk.args, id: toolCallChunk.id, error: "Malformed args.", + type: "invalid_tool_call", }); } } diff --git a/langchain-core/src/messages/tests/message_utils.test.ts b/langchain-core/src/messages/tests/message_utils.test.ts index 2cd179326bde..462075077d3d 100644 --- a/langchain-core/src/messages/tests/message_utils.test.ts +++ b/langchain-core/src/messages/tests/message_utils.test.ts @@ -111,8 +111,8 @@ describe("mergeMessageRuns", () => { { type: "text", text: "my favorite dish is lasagna" }, ], tool_calls: [ - { name: "blah_tool", args: { x: 2 }, id: "123" }, - { name: "blah_tool", args: { x: -10 }, id: "456" }, + { name: "blah_tool", args: { x: 2 }, id: "123", type: "tool_call" }, + { name: "blah_tool", args: { x: -10 }, id: "456", type: "tool_call" }, ], id: "baz", }), diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index 18338068c8f2..245a0935fd49 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -17,6 +17,9 @@ import { MessageContent } from "../messages/base.js"; export type ResponseFormat = "content" | "content_and_artifact"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ToolFuncReturnType = any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ContentAndArtifact = [MessageContent, any]; @@ -52,7 +55,7 @@ export class ToolInputParsingException extends Error { export interface StructuredToolInterface< T extends ZodAny = ZodAny, - RunOutput extends string | ToolMessage = string + RunOutput = ToolFuncReturnType > extends RunnableInterface< (z.output extends string ? string : never) | z.input, RunOutput @@ -91,7 +94,7 @@ export interface StructuredToolInterface< */ export abstract class StructuredTool< T extends ZodAny = ZodAny, - RunOutput extends string | ToolMessage = string + RunOutput = ToolFuncReturnType > extends BaseLangChain< (z.output extends string ? string : never) | z.input | ToolCall, RunOutput @@ -152,7 +155,6 @@ export abstract class StructuredTool< | undefined; if (_isToolCall(input)) { - console.log("is TC"); tool_call_id = input.id; toolInput = input.args; } else { @@ -166,7 +168,7 @@ export abstract class StructuredTool< ...ensuredConfig.configurable, tool_call_id, }, - }) as Promise; + }); } /** @@ -250,13 +252,13 @@ export abstract class StructuredTool< toolCallId, }); await runManager?.handleToolEnd(formattedOutput); - return formattedOutput as RunOutput; + return formattedOutput; } } export interface ToolInterface< T extends ZodAny = ZodAny, - RunOutput extends string | ToolMessage = string + RunOutput = ToolFuncReturnType > extends StructuredToolInterface { /** * @deprecated Use .invoke() instead. Will be removed in 0.3.0. @@ -277,7 +279,7 @@ export interface ToolInterface< * Base class for Tools that accept input as a string. */ export abstract class Tool< - RunOutput extends string | ToolMessage = string + RunOutput = ToolFuncReturnType > extends StructuredTool { schema = z .object({ input: z.string().optional() }) @@ -343,7 +345,7 @@ export interface DynamicStructuredToolInput * A tool that can be created dynamically from a function, name, and description. */ export class DynamicTool< - RunOutput extends string | ToolMessage = string + RunOutput = ToolFuncReturnType > extends Tool { static lc_name() { return "DynamicTool"; @@ -395,7 +397,7 @@ export class DynamicTool< */ export class DynamicStructuredTool< T extends ZodAny = ZodAny, - RunOutput extends string | ToolMessage = string + RunOutput = ToolFuncReturnType > extends StructuredTool { static lc_name() { return "DynamicStructuredTool"; @@ -459,7 +461,7 @@ export abstract class BaseToolkit { /** * Parameters for the tool function. * @template {ZodAny} RunInput The input schema for the tool. - * @template {string | ToolMessage} RunOutput The output type for the tool. + * @template {any} RunOutput The output type for the tool. */ interface ToolWrapperParams extends ToolParams { @@ -495,7 +497,7 @@ interface ToolWrapperParams * Creates a new StructuredTool instance with the provided function, name, description, and schema. * @function * @template {RunInput extends ZodAny = ZodAny} RunInput The input schema for the tool. This corresponds to the input type when the tool is invoked. - * @template {RunOutput extends string | ToolMessage = string} RunOutput The output type for the tool. This corresponds to the output type when the tool is invoked. + * @template {RunOutput = any} RunOutput The output type for the tool. This corresponds to the output type when the tool is invoked. * @template {FuncInput extends z.infer | ToolCall = z.infer} FuncInput The input type for the function. * * @param {RunnableFunc | ToolCall, RunOutput>} func - The function to invoke when the tool is called. @@ -508,29 +510,7 @@ interface ToolWrapperParams */ export function tool< RunInput extends ZodAny = ZodAny, - RunOutput extends ToolMessage = ToolMessage, - FuncInput extends z.infer | ToolCall = z.infer ->( - func: RunnableFunc, - fields: Omit, "responseFormat"> & { - responseFormat: "content_and_artifact"; - } -): DynamicStructuredTool; - -export function tool< - RunInput extends ZodAny = ZodAny, - RunOutput extends string = string, - FuncInput extends z.infer | ToolCall = z.infer ->( - func: RunnableFunc, - fields: Omit, "responseFormat"> & { - responseFormat?: "content" | undefined; - } -): DynamicStructuredTool; - -export function tool< - RunInput extends ZodAny = ZodAny, - RunOutput extends string | ToolMessage = string, + RunOutput = ToolFuncReturnType, FuncInput extends z.infer | ToolCall = z.infer, FuncOutput extends string | ContentAndArtifact = string >( @@ -565,7 +545,7 @@ function _formatToolOutput(params: { content: unknown; artifact?: unknown; toolCallId?: string; -}): ToolMessage | string { +}): ToolMessage | ToolFuncReturnType { const { content, artifact, toolCallId } = params; if (toolCallId) { if ( @@ -586,7 +566,7 @@ function _formatToolOutput(params: { }); } } else { - return typeof content === "string" ? content : _stringify(content); + return content; } } diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index 839684c5b57f..0b2553710088 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -8,19 +8,6 @@ test("Tool should throw type error if types are wrong", () => { location: z.string(), }); - // @ts-expect-error - Error because responseFormat: content_and_artifact makes return type ContentAndArtifact - tool( - (_): string => { - return "no-op"; - }, - { - name: "weather", - schema: weatherSchema, - responseFormat: "content_and_artifact", - } - ); - - // @ts-expect-error - Error because responseFormat: content makes return type be a string tool( (_): ContentAndArtifact => { return ["no-op", true]; @@ -32,9 +19,8 @@ test("Tool should throw type error if types are wrong", () => { } ); - // @ts-expect-error - Error because responseFormat: undefined makes return type be a string tool( - (_): ContentAndArtifact => { + (_) => { return ["no-op", true]; }, { @@ -107,8 +93,7 @@ test("Tool should error if responseFormat is content_and_artifact but the functi }); const weatherTool = tool( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (_): any => { + (_) => { return "str"; }, { @@ -146,7 +131,33 @@ test("Tool works if responseFormat is content_and_artifact and returns a tuple", expect(toolResult).toBe("msg_content"); }); -test("Returns tool message if responseFormat is content_and_artifact and returns a tuple and a tool call is passed in", async () => { +test("Does not return tool message if responseFormat is content_and_artifact and returns a tuple and a tool call with no id is passed in", async () => { + const weatherSchema = z.object({ + location: z.string(), + }); + + const weatherTool = tool( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (input): any => { + return ["msg_content", input]; + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "content_and_artifact", + } + ); + + const toolResult = await weatherTool.invoke({ + args: { location: "San Francisco" }, + name: "weather", + type: "tool_call", + }); + + expect(toolResult).toBe("msg_content"); +}); + +test("Returns tool message if responseFormat is content_and_artifact and returns a tuple and a tool call with id is passed in", async () => { const weatherSchema = z.object({ location: z.string(), }); diff --git a/langchain-core/src/tracers/base.ts b/langchain-core/src/tracers/base.ts index dcd3291a4a57..69cc098cdc55 100644 --- a/langchain-core/src/tracers/base.ts +++ b/langchain-core/src/tracers/base.ts @@ -392,7 +392,8 @@ export abstract class BaseTracer extends BaseCallbackHandler { return run; } - async handleToolEnd(output: string, runId: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async handleToolEnd(output: any, runId: string): Promise { const run = this.runMap.get(runId); if (!run || run?.run_type !== "tool") { throw new Error("No tool run to end"); From e854be2df517648857b112ec06f0cde6af86d6ed Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sat, 13 Jul 2024 08:36:05 -0700 Subject: [PATCH 08/18] Adds explicit output check for unsupported tools in LangChain agent executor --- langchain/src/agents/executor.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/langchain/src/agents/executor.ts b/langchain/src/agents/executor.ts index 2b6e6989e57f..70edc5a6573e 100644 --- a/langchain/src/agents/executor.ts +++ b/langchain/src/agents/executor.ts @@ -560,6 +560,11 @@ export class AgentExecutor extends BaseChain { patchConfig(config, { callbacks: runManager?.getChild() }) ) : `${action.tool} is not a valid tool, try another one.`; + if (typeof observation !== "string") { + throw new Error( + "Received unsupported non-string response from tool call." + ); + } } catch (e) { // eslint-disable-next-line no-instanceof/no-instanceof if (e instanceof ToolInputParsingException) { @@ -677,6 +682,11 @@ export class AgentExecutor extends BaseChain { agentAction.toolInput, runManager?.getChild() ); + if (typeof observation !== "string") { + throw new Error( + "Received unsupported non-string response from tool call." + ); + } } catch (e) { // eslint-disable-next-line no-instanceof/no-instanceof if (e instanceof ToolInputParsingException) { From 4132678a476002db7b44ff0c30f636d5649776d9 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sun, 14 Jul 2024 07:58:35 -0700 Subject: [PATCH 09/18] Backwards compatibility shim for Dall-E tool --- langchain-core/src/tools/index.ts | 2 +- libs/langchain-openai/src/tools/dalle.ts | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index 245a0935fd49..50235743e989 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -15,7 +15,7 @@ import { ToolCall, ToolMessage } from "../messages/tool.js"; import { ZodAny } from "../types/zod.js"; import { MessageContent } from "../messages/base.js"; -export type ResponseFormat = "content" | "content_and_artifact"; +export type ResponseFormat = "content" | "content_and_artifact" | string; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ToolFuncReturnType = any; diff --git a/libs/langchain-openai/src/tools/dalle.ts b/libs/langchain-openai/src/tools/dalle.ts index ceff03aad2ef..014b10d410d6 100644 --- a/libs/langchain-openai/src/tools/dalle.ts +++ b/libs/langchain-openai/src/tools/dalle.ts @@ -60,7 +60,7 @@ export interface DallEAPIWrapperParams extends ToolParams { * Must be one of "url" or "b64_json". * @default "url" */ - responseFormat?: "url" | "b64_json"; + dallEResponseFormat?: "url" | "b64_json"; /** * A unique identifier representing your end-user, which will help * OpenAI to monitor and detect abuse. @@ -104,11 +104,20 @@ export class DallEAPIWrapper extends Tool { | "1792x1024" | "1024x1792" = "1024x1024"; - private responseFormat: "url" | "b64_json" = "url"; + private dallEResponseFormat: "url" | "b64_json" = "url"; private user?: string; constructor(fields?: DallEAPIWrapperParams) { + // Shim for new base tool param name + if ( + fields?.responseFormat !== undefined && + ["url", "b64_json"].includes(fields.responseFormat) + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fields.dallEResponseFormat = fields.responseFormat as any; + fields.responseFormat = "content"; + } super(fields); const openAIApiKey = fields?.apiKey ?? @@ -129,7 +138,8 @@ export class DallEAPIWrapper extends Tool { this.quality = fields?.quality ?? this.quality; this.n = fields?.n ?? this.n; this.size = fields?.size ?? this.size; - this.responseFormat = fields?.responseFormat ?? this.responseFormat; + this.dallEResponseFormat = + fields?.dallEResponseFormat ?? this.dallEResponseFormat; this.user = fields?.user; } @@ -140,14 +150,14 @@ export class DallEAPIWrapper extends Tool { prompt: input, n: this.n, size: this.size, - response_format: this.responseFormat, + response_format: this.dallEResponseFormat, style: this.style, quality: this.quality, user: this.user, }); let data = ""; - if (this.responseFormat === "url") { + if (this.dallEResponseFormat === "url") { [data] = response.data .map((item) => item.url) .filter((url): url is string => url !== "undefined"); From 5fa9a7831f489fbd67b00cc7a67a4c60a81b22b7 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sun, 14 Jul 2024 08:03:36 -0700 Subject: [PATCH 10/18] Lint --- libs/langchain-openai/src/tools/dalle.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/langchain-openai/src/tools/dalle.ts b/libs/langchain-openai/src/tools/dalle.ts index 014b10d410d6..504dd99a2e3a 100644 --- a/libs/langchain-openai/src/tools/dalle.ts +++ b/libs/langchain-openai/src/tools/dalle.ts @@ -114,8 +114,9 @@ export class DallEAPIWrapper extends Tool { fields?.responseFormat !== undefined && ["url", "b64_json"].includes(fields.responseFormat) ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any no-param-reassign fields.dallEResponseFormat = fields.responseFormat as any; + // eslint-disable-next-line no-param-reassign fields.responseFormat = "content"; } super(fields); From d9c3a2e864cbabc21e05fd3b1d715b567fa4fd5f Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sun, 14 Jul 2024 08:30:36 -0700 Subject: [PATCH 11/18] Fix build --- langchain-core/src/tools/index.ts | 2 +- libs/langchain-openai/src/tools/dalle.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index 50235743e989..b6e3d35bdcbc 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -369,7 +369,7 @@ export class DynamicTool< * @deprecated Use .invoke() instead. Will be removed in 0.3.0. */ async call( - arg: string | undefined | z.input, + arg: string | undefined | z.input | ToolCall, configArg?: RunnableConfig | Callbacks ): Promise { const config = parseCallbackConfigArg(configArg); diff --git a/libs/langchain-openai/src/tools/dalle.ts b/libs/langchain-openai/src/tools/dalle.ts index 504dd99a2e3a..691669d92300 100644 --- a/libs/langchain-openai/src/tools/dalle.ts +++ b/libs/langchain-openai/src/tools/dalle.ts @@ -114,7 +114,8 @@ export class DallEAPIWrapper extends Tool { fields?.responseFormat !== undefined && ["url", "b64_json"].includes(fields.responseFormat) ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any no-param-reassign + // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line no-param-reassign fields.dallEResponseFormat = fields.responseFormat as any; // eslint-disable-next-line no-param-reassign fields.responseFormat = "content"; From 3d936ca23efc2e4eca4fbd26969ad50fb61e3ad1 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Sun, 14 Jul 2024 08:36:06 -0700 Subject: [PATCH 12/18] Fix lint --- libs/langchain-openai/src/tools/dalle.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/langchain-openai/src/tools/dalle.ts b/libs/langchain-openai/src/tools/dalle.ts index 691669d92300..fd0722353042 100644 --- a/libs/langchain-openai/src/tools/dalle.ts +++ b/libs/langchain-openai/src/tools/dalle.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import { getEnvironmentVariable } from "@langchain/core/utils/env"; import { OpenAI as OpenAIClient } from "openai"; import { Tool, ToolParams } from "@langchain/core/tools"; @@ -115,9 +116,7 @@ export class DallEAPIWrapper extends Tool { ["url", "b64_json"].includes(fields.responseFormat) ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - // eslint-disable-next-line no-param-reassign fields.dallEResponseFormat = fields.responseFormat as any; - // eslint-disable-next-line no-param-reassign fields.responseFormat = "content"; } super(fields); From 10088a98d619890a4198628bdd7b9c12d85b0135 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Mon, 15 Jul 2024 10:02:25 -0700 Subject: [PATCH 13/18] Fix generic --- langchain-core/src/tools/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index b6e3d35bdcbc..c32508cb5453 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -132,7 +132,7 @@ export abstract class StructuredTool< arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise; + ): Promise; /** * Invokes the tool with the provided input and configuration. @@ -384,7 +384,7 @@ export class DynamicTool< input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(input, runManager, config); } } From bfa0122596e6828fa58518e62df9736cceec29c3 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Mon, 15 Jul 2024 10:42:21 -0700 Subject: [PATCH 14/18] Fix types --- langchain-core/src/tools/index.ts | 32 +++++++++----------- langchain-core/src/tools/tests/tools.test.ts | 7 ++--- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index c32508cb5453..05f0987798f1 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -318,26 +318,29 @@ export interface BaseDynamicToolInput extends ToolParams { /** * Interface for the input parameters of the DynamicTool class. */ -export interface DynamicToolInput 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 - extends BaseDynamicToolInput { +export interface DynamicStructuredToolInput< + T extends ZodAny = ZodAny, + RunOutput = ToolFuncReturnType +> extends BaseDynamicToolInput { func: ( input: BaseDynamicToolInput["responseFormat"] extends "content_and_artifact" ? ToolCall : z.infer, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; schema: T; } @@ -440,7 +443,7 @@ export class DynamicStructuredTool< arg: z.output | ToolCall, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(arg, runManager, config); } } @@ -508,25 +511,20 @@ interface ToolWrapperParams * * @returns {DynamicStructuredTool} A new StructuredTool instance. */ -export function tool< - RunInput extends ZodAny = ZodAny, - RunOutput = ToolFuncReturnType, - FuncInput extends z.infer | ToolCall = z.infer, - FuncOutput extends string | ContentAndArtifact = string ->( - func: RunnableFunc, - fields: ToolWrapperParams -): DynamicStructuredTool { +export function tool( + func: RunnableFunc, RunOutput | ContentAndArtifact>, + fields: ToolWrapperParams +): DynamicStructuredTool { 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({ + return new DynamicStructuredTool({ name: fields.name, description, - schema: schema as RunInput, + schema: schema as T, func: async (input, _runManager, config) => func(input, config), responseFormat: fields.responseFormat, }); diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index 0b2553710088..84f9dea910b1 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -1,7 +1,7 @@ import { test, expect } from "@jest/globals"; import { z } from "zod"; import { ContentAndArtifact, tool } from "../index.js"; -import { ToolCall, ToolMessage } from "../../messages/tool.js"; +import { ToolMessage } from "../../messages/tool.js"; test("Tool should throw type error if types are wrong", () => { const weatherSchema = z.object({ @@ -64,8 +64,7 @@ test("Tool should throw type error if types are wrong", () => { } ); - tool( - // @ts-expect-error - Error because setting the third generic to `ToolCall` makes the input type of the function be `ToolCall` + tool( (_: z.infer): string => { return "no-op"; }, @@ -77,7 +76,7 @@ test("Tool should throw type error if types are wrong", () => { // This works because not setting any generics allows it to infer the correct types tool( - (_: ToolCall): string => { + (_): string => { return "no-op"; }, { From 9f888f3e69b93b0e246c3421c9a55419b97d09ba Mon Sep 17 00:00:00 2001 From: bracesproul Date: Mon, 15 Jul 2024 11:45:00 -0700 Subject: [PATCH 15/18] cr --- langchain-core/src/tools/index.ts | 4 ++-- langchain-core/src/tools/tests/tools.test.ts | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index 05f0987798f1..a25b228c90e0 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -18,7 +18,7 @@ import { MessageContent } from "../messages/base.js"; export type ResponseFormat = "content" | "content_and_artifact" | string; // eslint-disable-next-line @typescript-eslint/no-explicit-any -type ToolFuncReturnType = any; +type ToolFuncReturnType = any | ToolMessage; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ContentAndArtifact = [MessageContent, any]; @@ -521,7 +521,7 @@ export function tool( const description = fields.description ?? schema.description ?? `${fields.name} tool`; - return new DynamicStructuredTool({ + return new DynamicStructuredTool({ name: fields.name, description, schema: schema as T, diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index 84f9dea910b1..b6953c264a11 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -113,8 +113,7 @@ test("Tool works if responseFormat is content_and_artifact and returns a tuple", }); const weatherTool = tool( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (input): any => { + (input) => { return ["msg_content", input]; }, { @@ -136,8 +135,7 @@ test("Does not return tool message if responseFormat is content_and_artifact and }); const weatherTool = tool( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (input): any => { + (input) => { return ["msg_content", input]; }, { @@ -163,7 +161,7 @@ test("Returns tool message if responseFormat is content_and_artifact and returns const weatherTool = tool( // eslint-disable-next-line @typescript-eslint/no-explicit-any - (input): any => { + (input) => { return ["msg_content", input]; }, { From cde4c7fd86fcad657db1398279292ea9233bd92a Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Mon, 15 Jul 2024 13:46:26 -0700 Subject: [PATCH 16/18] Fix build --- langchain-core/src/tools/index.ts | 75 ++++++++------------ langchain-core/src/tools/tests/tools.test.ts | 10 --- libs/langchain-openai/src/tools/dalle.ts | 6 +- 3 files changed, 36 insertions(+), 55 deletions(-) diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index a25b228c90e0..1a87e8f4ecbe 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -18,7 +18,7 @@ import { MessageContent } from "../messages/base.js"; export type ResponseFormat = "content" | "content_and_artifact" | string; // eslint-disable-next-line @typescript-eslint/no-explicit-any -type ToolFuncReturnType = any | ToolMessage; +type ToolReturnType = any; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ContentAndArtifact = [MessageContent, any]; @@ -53,12 +53,10 @@ export class ToolInputParsingException extends Error { } } -export interface StructuredToolInterface< - T extends ZodAny = ZodAny, - RunOutput = ToolFuncReturnType -> extends RunnableInterface< +export interface StructuredToolInterface + extends RunnableInterface< (z.output extends string ? string : never) | z.input, - RunOutput + ToolReturnType > { lc_namespace: string[]; @@ -80,7 +78,7 @@ export interface StructuredToolInterface< configArg?: Callbacks | RunnableConfig, /** @deprecated */ tags?: string[] - ): Promise; + ): Promise; name: string; @@ -93,11 +91,10 @@ export interface StructuredToolInterface< * Base class for Tools that accept input of any shape defined by a Zod schema. */ export abstract class StructuredTool< - T extends ZodAny = ZodAny, - RunOutput = ToolFuncReturnType + T extends ZodAny = ZodAny > extends BaseLangChain< (z.output extends string ? string : never) | z.input | ToolCall, - RunOutput + ToolReturnType > { abstract name: string; @@ -132,7 +129,7 @@ export abstract class StructuredTool< arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise; + ): Promise; /** * Invokes the tool with the provided input and configuration. @@ -146,7 +143,7 @@ export abstract class StructuredTool< | z.input | ToolCall, config?: RunnableConfig - ): Promise { + ): Promise { let tool_call_id: string | undefined; let toolInput: | (z.output extends string ? string : never) @@ -187,7 +184,7 @@ export abstract class StructuredTool< configArg?: Callbacks | RunnableConfig, /** @deprecated */ tags?: string[] - ): Promise { + ): Promise { let parsed; try { parsed = await this.schema.parseAsync(arg); @@ -256,10 +253,8 @@ export abstract class StructuredTool< } } -export interface ToolInterface< - T extends ZodAny = ZodAny, - RunOutput = ToolFuncReturnType -> extends StructuredToolInterface { +export interface ToolInterface + extends StructuredToolInterface { /** * @deprecated Use .invoke() instead. Will be removed in 0.3.0. * @@ -272,15 +267,13 @@ export interface ToolInterface< call( arg: string | undefined | z.input | ToolCall, callbacks?: Callbacks | RunnableConfig - ): Promise; + ): Promise; } /** * Base class for Tools that accept input as a string. */ -export abstract class Tool< - RunOutput = ToolFuncReturnType -> extends StructuredTool { +export abstract class Tool extends StructuredTool { schema = z .object({ input: z.string().optional() }) .transform((obj) => obj.input); @@ -301,7 +294,7 @@ export abstract class Tool< call( arg: string | undefined | z.input | ToolCall, callbacks?: Callbacks | RunnableConfig - ): Promise { + ): Promise { return super.call( typeof arg === "string" || !arg ? { input: arg } : arg, callbacks @@ -318,38 +311,33 @@ export interface BaseDynamicToolInput extends ToolParams { /** * Interface for the input parameters of the DynamicTool class. */ -export interface DynamicToolInput - 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 = ToolFuncReturnType -> extends BaseDynamicToolInput { +export interface DynamicStructuredToolInput + extends BaseDynamicToolInput { func: ( input: BaseDynamicToolInput["responseFormat"] extends "content_and_artifact" ? ToolCall : 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< - RunOutput = ToolFuncReturnType -> extends Tool { +export class DynamicTool extends Tool { static lc_name() { return "DynamicTool"; } @@ -374,7 +362,7 @@ export class DynamicTool< async call( arg: string | undefined | z.input | ToolCall, configArg?: RunnableConfig | Callbacks - ): Promise { + ): Promise { const config = parseCallbackConfigArg(configArg); if (config.runName === undefined) { config.runName = this.name; @@ -387,7 +375,7 @@ export class DynamicTool< input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(input, runManager, config); } } @@ -399,9 +387,8 @@ export class DynamicTool< * provided function when the tool is called. */ export class DynamicStructuredTool< - T extends ZodAny = ZodAny, - RunOutput = ToolFuncReturnType -> extends StructuredTool { + T extends ZodAny = ZodAny +> extends StructuredTool { static lc_name() { return "DynamicStructuredTool"; } @@ -431,7 +418,7 @@ export class DynamicStructuredTool< configArg?: RunnableConfig | Callbacks, /** @deprecated */ tags?: string[] - ): Promise { + ): Promise { const config = parseCallbackConfigArg(configArg); if (config.runName === undefined) { config.runName = this.name; @@ -443,7 +430,7 @@ export class DynamicStructuredTool< arg: z.output | ToolCall, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(arg, runManager, config); } } @@ -511,10 +498,10 @@ interface ToolWrapperParams * * @returns {DynamicStructuredTool} A new StructuredTool instance. */ -export function tool( - func: RunnableFunc, RunOutput | ContentAndArtifact>, +export function tool( + func: RunnableFunc, ToolReturnType>, fields: ToolWrapperParams -): DynamicStructuredTool { +): DynamicStructuredTool { const schema = fields.schema ?? z.object({ input: z.string().optional() }).transform((obj) => obj.input); @@ -543,7 +530,7 @@ function _formatToolOutput(params: { content: unknown; artifact?: unknown; toolCallId?: string; -}): ToolMessage | ToolFuncReturnType { +}): ToolReturnType { const { content, artifact, toolCallId } = params; if (toolCallId) { if ( diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index b6953c264a11..62aca785526e 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -64,16 +64,6 @@ test("Tool should throw type error if types are wrong", () => { } ); - tool( - (_: z.infer): string => { - return "no-op"; - }, - { - name: "weather", - schema: weatherSchema, - } - ); - // This works because not setting any generics allows it to infer the correct types tool( (_): string => { diff --git a/libs/langchain-openai/src/tools/dalle.ts b/libs/langchain-openai/src/tools/dalle.ts index fd0722353042..0bda5fbb6577 100644 --- a/libs/langchain-openai/src/tools/dalle.ts +++ b/libs/langchain-openai/src/tools/dalle.ts @@ -61,7 +61,11 @@ export interface DallEAPIWrapperParams extends ToolParams { * Must be one of "url" or "b64_json". * @default "url" */ - dallEResponseFormat?: "url" | "b64_json"; + dallEResponseFormat?: "url" | "b64_json" + /** + * @deprecated Use dallEResponseFormat instead. + */; + responseFormat?: any; /** * A unique identifier representing your end-user, which will help * OpenAI to monitor and detect abuse. From 96d401e7e4fab7b477403d81754478f26f66c567 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Mon, 15 Jul 2024 13:50:22 -0700 Subject: [PATCH 17/18] Fix type --- langchain-core/src/tools/index.ts | 2 +- libs/langchain-openai/src/tools/dalle.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index 1a87e8f4ecbe..bcdca3821824 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -55,7 +55,7 @@ export class ToolInputParsingException extends Error { export interface StructuredToolInterface extends RunnableInterface< - (z.output extends string ? string : never) | z.input, + (z.output extends string ? string : never) | z.input | ToolCall, ToolReturnType > { lc_namespace: string[]; diff --git a/libs/langchain-openai/src/tools/dalle.ts b/libs/langchain-openai/src/tools/dalle.ts index 0bda5fbb6577..15ac6f808798 100644 --- a/libs/langchain-openai/src/tools/dalle.ts +++ b/libs/langchain-openai/src/tools/dalle.ts @@ -63,8 +63,9 @@ export interface DallEAPIWrapperParams extends ToolParams { */ dallEResponseFormat?: "url" | "b64_json" /** - * @deprecated Use dallEResponseFormat instead. + * @deprecated Use dallEResponseFormat instead for the Dall-E response type. */; + // eslint-disable-next-line @typescript-eslint/no-explicit-any responseFormat?: any; /** * A unique identifier representing your end-user, which will help From 487cef7e9388534c8f39524c6a9ed1e191776184 Mon Sep 17 00:00:00 2001 From: jacoblee93 Date: Mon, 15 Jul 2024 13:56:05 -0700 Subject: [PATCH 18/18] Format --- libs/langchain-openai/src/tools/dalle.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/langchain-openai/src/tools/dalle.ts b/libs/langchain-openai/src/tools/dalle.ts index 15ac6f808798..40a1f9df95a9 100644 --- a/libs/langchain-openai/src/tools/dalle.ts +++ b/libs/langchain-openai/src/tools/dalle.ts @@ -61,11 +61,11 @@ export interface DallEAPIWrapperParams extends ToolParams { * Must be one of "url" or "b64_json". * @default "url" */ - dallEResponseFormat?: "url" | "b64_json" + dallEResponseFormat?: "url" | "b64_json"; /** * @deprecated Use dallEResponseFormat instead for the Dall-E response type. - */; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any responseFormat?: any; /** * A unique identifier representing your end-user, which will help