diff --git a/langchain-core/langchain.config.js b/langchain-core/langchain.config.js index 6c1b4e569be2..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", + tools: "tools/index", "tracers/base": "tracers/base", "tracers/console": "tracers/console", "tracers/initialize": "tracers/initialize", diff --git a/langchain-core/src/callbacks/base.ts b/langchain-core/src/callbacks/base.ts index 229119697753..389411b45845 100644 --- a/langchain-core/src/callbacks/base.ts +++ b/langchain-core/src/callbacks/base.ts @@ -197,7 +197,8 @@ abstract class BaseCallbackHandlerMethodsClass { * Called at the end of a Tool run, with the tool output and the run ID. */ handleToolEnd?( - output: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + output: any, runId: string, parentRunId?: string, tags?: string[] diff --git a/langchain-core/src/callbacks/manager.ts b/langchain-core/src/callbacks/manager.ts index 834ee61d6a38..11548a4b4212 100644 --- a/langchain-core/src/callbacks/manager.ts +++ b/langchain-core/src/callbacks/manager.ts @@ -493,7 +493,8 @@ export class CallbackManagerForToolRun ); } - async handleToolEnd(output: string): 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/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index ab84847c5757..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.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 294f3feca879..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.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/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/index.ts b/langchain-core/src/messages/index.ts index 3f0f49069f21..c95a70a2b618 100644 --- a/langchain-core/src/messages/index.ts +++ b/langchain-core/src/messages/index.ts @@ -12,4 +12,5 @@ export { type ToolMessageFieldsWithToolCallId, ToolMessage, ToolMessageChunk, + type InvalidToolCall, } from "./tool.js"; 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/messages/tool.ts b/langchain-core/src/messages/tool.ts index 4567bd91e633..482b2fa54d57 100644 --- a/langchain-core/src/messages/tool.ts +++ b/langchain-core/src/messages/tool.ts @@ -139,6 +139,8 @@ export type ToolCall = { args: Record; id?: string; + + type?: "tool_call"; }; /** @@ -199,6 +201,8 @@ export type ToolCallChunk = { id?: string; index?: number; + + type?: "tool_call_chunk"; }; export type InvalidToolCall = { @@ -206,6 +210,7 @@ export type InvalidToolCall = { args?: string; id?: string; error?: string; + type?: "invalid_tool_call"; }; export function defaultToolCallParser( diff --git a/langchain-core/src/messages/transformers.ts b/langchain-core/src/messages/transformers.ts index 19e2d09ec783..134c84222edf 100644 --- a/langchain-core/src/messages/transformers.ts +++ b/langchain-core/src/messages/transformers.ts @@ -952,6 +952,7 @@ function _switchTypeToMessage( ...aiChunkFields, tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => ({ ...tc, + type: "tool_call_chunk", index: undefined, args: JSON.stringify(tc.args), })), diff --git a/langchain-core/src/messages/utils.ts b/langchain-core/src/messages/utils.ts index ac3c6b6594d2..a17ce3853396 100644 --- a/langchain-core/src/messages/utils.ts +++ b/langchain-core/src/messages/utils.ts @@ -192,6 +192,7 @@ export function convertToChunk(message: BaseMessage) { ...aiChunkFields, tool_call_chunks: aiChunkFields.tool_calls?.map((tc) => ({ ...tc, + type: "tool_call_chunk", index: undefined, args: JSON.stringify(tc.args), })), 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..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 @@ -71,6 +71,7 @@ export function parseToolCall( const parsedToolCall: ToolCall = { name: rawToolCall.function.name, args: functionArgs, + type: "tool_call", }; if (options?.returnId) { @@ -104,6 +105,7 @@ export function makeInvalidToolCall( 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 cc52c5d85e3b..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.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 b5ae10c85cd8..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.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.ts b/langchain-core/src/tools/index.ts similarity index 60% rename from langchain-core/src/tools.ts rename to langchain-core/src/tools/index.ts index 5adc4d27b4e4..bcdca3821824 100644 --- a/langchain-core/src/tools.ts +++ b/langchain-core/src/tools/index.ts @@ -4,19 +4,40 @@ 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 { 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"; +import { MessageContent } from "../messages/base.js"; + +export type ResponseFormat = "content" | "content_and_artifact" | string; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ToolReturnType = any; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ContentAndArtifact = [MessageContent, any]; /** * 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 "content_and_artifact" then the output is expected to be a + * two-tuple corresponding to the (content, artifact) of a ToolMessage. + * + * @default "content" + */ + responseFormat?: ResponseFormat; +} /** * Custom error class used to handle exceptions related to tool input parsing. @@ -34,8 +55,8 @@ export class ToolInputParsingException extends Error { export interface StructuredToolInterface extends RunnableInterface< - (z.output extends string ? string : never) | z.input, - string + (z.output extends string ? string : never) | z.input | ToolCall, + ToolReturnType > { lc_namespace: string[]; @@ -53,11 +74,11 @@ 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[] - ): Promise; + ): Promise; name: string; @@ -72,24 +93,43 @@ export interface StructuredToolInterface export abstract class StructuredTool< T extends ZodAny = ZodAny > extends BaseLangChain< - (z.output extends string ? string : never) | z.input, - string + (z.output extends string ? string : never) | z.input | ToolCall, + ToolReturnType > { + abstract name: string; + + abstract description: string; + abstract schema: T | z.ZodEffects; + returnDirect = false; + get lc_namespace() { return ["langchain", "tools"]; } + /** + * The tool response format. + * + * If "content" then the output of the tool is interpreted as the contents of a + * 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" + */ + responseFormat?: ResponseFormat = "content"; + constructor(fields?: ToolParams) { super(fields ?? {}); + + this.responseFormat = fields?.responseFormat ?? this.responseFormat; } protected abstract _call( arg: z.output, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise; + ): Promise; /** * Invokes the tool with the provided input and configuration. @@ -98,10 +138,34 @@ 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)); + ): Promise { + 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 this.call(toolInput, { + ...ensuredConfig, + configurable: { + ...ensuredConfig.configurable, + tool_call_id, + }, + }); } /** @@ -116,11 +180,11 @@ 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 { + ): Promise { let parsed; try { parsed = await this.schema.parseAsync(arg); @@ -130,6 +194,7 @@ export abstract class StructuredTool< JSON.stringify(arg) ); } + const config = parseCallbackConfigArg(configArg); const callbackManager_ = await CallbackManager.configure( config.callbacks, @@ -157,18 +222,39 @@ export abstract class StructuredTool< await runManager?.handleToolError(e); throw e; } - await runManager?.handleToolEnd(result); - return result; - } - - abstract name: string; - - abstract description: string; + let content; + let artifact; + if (this.responseFormat === "content_and_artifact") { + if (Array.isArray(result) && result.length === 2) { + [content, artifact] = result; + } else { + throw new Error( + `Tool response format is "content_and_artifact" but the output was not a two-tuple.\nResult: ${JSON.stringify( + result + )}` + ); + } + } else { + content = result; + } - returnDirect = false; + let toolCallId: string | undefined; + if (config && "configurable" in config) { + toolCallId = (config.configurable as Record) + .tool_call_id; + } + const formattedOutput = _formatToolOutput({ + content, + artifact, + toolCallId, + }); + await runManager?.handleToolEnd(formattedOutput); + return formattedOutput; + } } -export interface ToolInterface extends StructuredToolInterface { +export interface ToolInterface + extends StructuredToolInterface { /** * @deprecated Use .invoke() instead. Will be removed in 0.3.0. * @@ -179,15 +265,15 @@ export interface ToolInterface extends StructuredToolInterface { * @returns A Promise that resolves with a string. */ call( - arg: string | undefined | z.input, + 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 extends StructuredTool { +export abstract class Tool extends StructuredTool { schema = z .object({ input: z.string().optional() }) .transform((obj) => obj.input); @@ -206,9 +292,9 @@ export abstract class Tool extends StructuredTool { * @returns A Promise that resolves with a string. */ call( - arg: string | undefined | z.input, + arg: string | undefined | z.input | ToolCall, callbacks?: Callbacks | RunnableConfig - ): Promise { + ): Promise { return super.call( typeof arg === "string" || !arg ? { input: arg } : arg, callbacks @@ -230,7 +316,7 @@ export interface DynamicToolInput extends BaseDynamicToolInput { input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; } /** @@ -239,10 +325,12 @@ export interface DynamicToolInput extends BaseDynamicToolInput { export interface DynamicStructuredToolInput extends BaseDynamicToolInput { func: ( - input: z.infer, + input: BaseDynamicToolInput["responseFormat"] extends "content_and_artifact" + ? ToolCall + : z.infer, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ) => Promise; + ) => Promise; schema: T; } @@ -272,9 +360,9 @@ export class DynamicTool extends Tool { * @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 { + ): Promise { const config = parseCallbackConfigArg(configArg); if (config.runName === undefined) { config.runName = this.name; @@ -287,7 +375,7 @@ export class DynamicTool extends Tool { input: string, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(input, runManager, config); } } @@ -326,11 +414,11 @@ 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[] - ): Promise { + ): Promise { const config = parseCallbackConfigArg(configArg); if (config.runName === undefined) { config.runName = this.name; @@ -339,10 +427,10 @@ export class DynamicStructuredTool< } protected _call( - arg: z.output, + arg: z.output | ToolCall, runManager?: CallbackManagerForToolRun, config?: RunnableConfig - ): Promise { + ): Promise { return this.func(arg, runManager, config); } } @@ -363,6 +451,7 @@ export abstract class BaseToolkit { /** * Parameters for the tool function. * @template {ZodAny} RunInput The input schema for the tool. + * @template {any} RunOutput The output type for the tool. */ interface ToolWrapperParams extends ToolParams { @@ -382,35 +471,94 @@ 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 "content_and_artifact" then the output is expected to be a + * two-tuple corresponding to the (content, artifact) 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 {RunInput extends ZodAny = ZodAny} RunInput The input schema for the tool. This corresponds to the input 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} 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( - func: RunnableFunc, string>, - fields: ToolWrapperParams -) { +export function tool( + func: RunnableFunc, ToolReturnType>, + 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, }); } + +function _isToolCall(toolCall?: unknown): toolCall is ToolCall { + return !!( + toolCall && + typeof toolCall === "object" && + "type" in toolCall && + toolCall.type === "tool_call" + ); +} + +function _formatToolOutput(params: { + content: unknown; + artifact?: unknown; + toolCallId?: string; +}): ToolReturnType { + const { content, artifact, toolCallId } = params; + if (toolCallId) { + if ( + typeof content === "string" || + (Array.isArray(content) && + content.every((item) => typeof item === "object")) + ) { + return new ToolMessage({ + content, + artifact, + tool_call_id: toolCallId, + }); + } else { + return new ToolMessage({ + content: _stringify(content), + artifact, + tool_call_id: toolCallId, + }); + } + } else { + return 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..62aca785526e --- /dev/null +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -0,0 +1,174 @@ +import { test, expect } from "@jest/globals"; +import { z } from "zod"; +import { ContentAndArtifact, tool } from "../index.js"; +import { ToolMessage } from "../../messages/tool.js"; + +test("Tool should throw type error if types are wrong", () => { + const weatherSchema = z.object({ + location: z.string(), + }); + + tool( + (_): ContentAndArtifact => { + return ["no-op", true]; + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "content", + } + ); + + tool( + (_) => { + return ["no-op", true]; + }, + { + name: "weather", + schema: weatherSchema, + } + ); + + // Should pass because we're expecting a `ToolMessage` return type due to `responseFormat: content_and_artifact` + tool( + (_): ContentAndArtifact => { + return ["no-op", true]; + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "content_and_artifact", + } + ); + + // 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, + } + ); + + // This works because not setting any generics allows it to infer the correct types + tool( + (_): string => { + return "no-op"; + }, + { + name: "weather", + schema: weatherSchema, + } + ); +}); + +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(), + }); + + const weatherTool = tool( + (_) => { + return "str"; + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "content_and_artifact", + } + ); + + await expect(async () => { + await weatherTool.invoke({ location: "San Francisco" }); + }).rejects.toThrow(); +}); + +test("Tool works if responseFormat is content_and_artifact and returns a tuple", async () => { + const weatherSchema = z.object({ + location: z.string(), + }); + + const weatherTool = tool( + (input) => { + return ["msg_content", input]; + }, + { + name: "weather", + schema: weatherSchema, + responseFormat: "content_and_artifact", + } + ); + + const toolResult = await weatherTool.invoke({ location: "San Francisco" }); + + expect(toolResult).not.toBeInstanceOf(ToolMessage); + expect(toolResult).toBe("msg_content"); +}); + +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( + (input) => { + 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(), + }); + + const weatherTool = tool( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (input) => { + 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.artifact).toEqual({ location: "San Francisco" }); +}); 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"); diff --git a/langchain-core/src/utils/function_calling.ts b/langchain-core/src/utils/function_calling.ts index cafc4c268e24..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.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 6eaf89177887..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.js"; +import { StructuredTool, ToolParams } from "../../tools/index.js"; import { BaseTracer, Run } from "../../tracers/base.js"; import { Embeddings, EmbeddingsParams } from "../../embeddings.js"; import { 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) { diff --git a/libs/langchain-openai/src/tools/dalle.ts b/libs/langchain-openai/src/tools/dalle.ts index ceff03aad2ef..40a1f9df95a9 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"; @@ -60,7 +61,12 @@ export interface DallEAPIWrapperParams extends ToolParams { * Must be one of "url" or "b64_json". * @default "url" */ - responseFormat?: "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 + responseFormat?: any; /** * A unique identifier representing your end-user, which will help * OpenAI to monitor and detect abuse. @@ -104,11 +110,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 +144,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 +156,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");