Skip to content

Commit 053de39

Browse files
authored
core[patch]: Allow dynamic tools to be initialized with JSON schema (#6306)
* Allow tools to be initialized with JSON schema * Fix lint * Add docstrings
1 parent fa376c6 commit 053de39

File tree

2 files changed

+152
-21
lines changed

2 files changed

+152
-21
lines changed

langchain-core/src/tools/index.ts

+54-20
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ZodObjectAny } from "../types/zod.js";
2020
import { MessageContent } from "../messages/base.js";
2121
import { AsyncLocalStorageProviderSingleton } from "../singletons/index.js";
2222
import { _isToolCall, ToolInputParsingException } from "./utils.js";
23+
import { isZodSchema } from "../utils/types/is_zod_schema.js";
2324

2425
export { ToolInputParsingException };
2526

@@ -319,16 +320,19 @@ export interface DynamicToolInput extends BaseDynamicToolInput {
319320
* Interface for the input parameters of the DynamicStructuredTool class.
320321
*/
321322
export interface DynamicStructuredToolInput<
322-
T extends ZodObjectAny = ZodObjectAny
323+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
324+
T extends ZodObjectAny | Record<string, any> = ZodObjectAny
323325
> extends BaseDynamicToolInput {
324326
func: (
325327
input: BaseDynamicToolInput["responseFormat"] extends "content_and_artifact"
326328
? ToolCall
327-
: z.infer<T>,
329+
: T extends ZodObjectAny
330+
? z.infer<T>
331+
: T,
328332
runManager?: CallbackManagerForToolRun,
329333
config?: RunnableConfig
330334
) => Promise<ToolReturnType>;
331-
schema: T;
335+
schema: T extends ZodObjectAny ? T : T;
332336
}
333337

334338
/**
@@ -382,10 +386,14 @@ export class DynamicTool extends Tool {
382386
* description, designed to work with structured data. It extends the
383387
* StructuredTool class and overrides the _call method to execute the
384388
* provided function when the tool is called.
389+
*
390+
* Schema can be passed as Zod or JSON schema. The tool will not validate
391+
* input if JSON schema is passed.
385392
*/
386393
export class DynamicStructuredTool<
387-
T extends ZodObjectAny = ZodObjectAny
388-
> extends StructuredTool<T> {
394+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
395+
T extends ZodObjectAny | Record<string, any> = ZodObjectAny
396+
> extends StructuredTool<T extends ZodObjectAny ? T : ZodObjectAny> {
389397
static lc_name() {
390398
return "DynamicStructuredTool";
391399
}
@@ -396,22 +404,24 @@ export class DynamicStructuredTool<
396404

397405
func: DynamicStructuredToolInput<T>["func"];
398406

399-
schema: T;
407+
schema: T extends ZodObjectAny ? T : ZodObjectAny;
400408

401409
constructor(fields: DynamicStructuredToolInput<T>) {
402410
super(fields);
403411
this.name = fields.name;
404412
this.description = fields.description;
405413
this.func = fields.func;
406414
this.returnDirect = fields.returnDirect ?? this.returnDirect;
407-
this.schema = fields.schema;
415+
this.schema = (
416+
isZodSchema(fields.schema) ? fields.schema : z.object({})
417+
) as T extends ZodObjectAny ? T : ZodObjectAny;
408418
}
409419

410420
/**
411421
* @deprecated Use .invoke() instead. Will be removed in 0.3.0.
412422
*/
413423
async call(
414-
arg: z.output<T> | ToolCall,
424+
arg: (T extends ZodObjectAny ? z.output<T> : T) | ToolCall,
415425
configArg?: RunnableConfig | Callbacks,
416426
/** @deprecated */
417427
tags?: string[]
@@ -424,11 +434,12 @@ export class DynamicStructuredTool<
424434
}
425435

426436
protected _call(
427-
arg: z.output<T> | ToolCall,
437+
arg: (T extends ZodObjectAny ? z.output<T> : T) | ToolCall,
428438
runManager?: CallbackManagerForToolRun,
429439
parentConfig?: RunnableConfig
430440
): Promise<ToolReturnType> {
431-
return this.func(arg, runManager, parentConfig);
441+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
442+
return this.func(arg as any, runManager, parentConfig);
432443
}
433444
}
434445

@@ -447,10 +458,16 @@ export abstract class BaseToolkit {
447458

448459
/**
449460
* Parameters for the tool function.
450-
* @template {ZodObjectAny | z.ZodString = ZodObjectAny} RunInput The input schema for the tool. Either any Zod object, or a Zod string.
461+
* Schema can be provided as Zod or JSON schema.
462+
* If you pass JSON schema, tool inputs will not be validated.
463+
* @template {ZodObjectAny | z.ZodString | Record<string, any> = ZodObjectAny} RunInput The input schema for the tool. Either any Zod object, a Zod string, or JSON schema.
451464
*/
452465
interface ToolWrapperParams<
453-
RunInput extends ZodObjectAny | z.ZodString = ZodObjectAny
466+
RunInput extends
467+
| ZodObjectAny
468+
| z.ZodString
469+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
470+
| Record<string, any> = ZodObjectAny
454471
> extends ToolParams {
455472
/**
456473
* The name of the tool. If using with an LLM, this
@@ -483,8 +500,11 @@ interface ToolWrapperParams<
483500
/**
484501
* Creates a new StructuredTool instance with the provided function, name, description, and schema.
485502
*
503+
* Schema can be provided as Zod or JSON schema.
504+
* If you pass JSON schema, tool inputs will not be validated.
505+
*
486506
* @function
487-
* @template {ZodObjectAny | z.ZodString = ZodObjectAny} T The input schema for the tool. Either any Zod object, or a Zod string.
507+
* @template {ZodObjectAny | z.ZodString | Record<string, any> = ZodObjectAny} T The input schema for the tool. Either any Zod object, a Zod string, or JSON schema instance.
488508
*
489509
* @param {RunnableFunc<z.output<T>, ToolReturnType>} func - The function to invoke when the tool is called.
490510
* @param {ToolWrapperParams<T>} fields - An object containing the following properties:
@@ -494,18 +514,27 @@ interface ToolWrapperParams<
494514
*
495515
* @returns {DynamicStructuredTool<T>} A new StructuredTool instance.
496516
*/
497-
export function tool<T extends z.ZodString = z.ZodString>(
517+
export function tool<T extends z.ZodString>(
498518
func: RunnableFunc<z.output<T>, ToolReturnType>,
499519
fields: ToolWrapperParams<T>
500520
): DynamicTool;
501521

502-
export function tool<T extends ZodObjectAny = ZodObjectAny>(
522+
export function tool<T extends ZodObjectAny>(
503523
func: RunnableFunc<z.output<T>, ToolReturnType>,
504524
fields: ToolWrapperParams<T>
505525
): DynamicStructuredTool<T>;
506526

507-
export function tool<T extends ZodObjectAny | z.ZodString = ZodObjectAny>(
508-
func: RunnableFunc<z.output<T>, ToolReturnType>,
527+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
528+
export function tool<T extends Record<string, any>>(
529+
func: RunnableFunc<T, ToolReturnType>,
530+
fields: ToolWrapperParams<T>
531+
): DynamicStructuredTool<T>;
532+
533+
export function tool<
534+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
535+
T extends ZodObjectAny | z.ZodString | Record<string, any> = ZodObjectAny
536+
>(
537+
func: RunnableFunc<T extends ZodObjectAny ? z.output<T> : T, ToolReturnType>,
509538
fields: ToolWrapperParams<T>
510539
):
511540
| DynamicStructuredTool<T extends ZodObjectAny ? T : ZodObjectAny>
@@ -518,7 +547,9 @@ export function tool<T extends ZodObjectAny | z.ZodString = ZodObjectAny>(
518547
fields.description ??
519548
fields.schema?.description ??
520549
`${fields.name} tool`,
521-
func,
550+
// TS doesn't restrict the type here based on the guard above
551+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
552+
func: func as any,
522553
});
523554
}
524555

@@ -528,7 +559,8 @@ export function tool<T extends ZodObjectAny | z.ZodString = ZodObjectAny>(
528559
return new DynamicStructuredTool<T extends ZodObjectAny ? T : ZodObjectAny>({
529560
...fields,
530561
description,
531-
schema: fields.schema as T extends ZodObjectAny ? T : ZodObjectAny,
562+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
563+
schema: fields.schema as any,
532564
// TODO: Consider moving into DynamicStructuredTool constructor
533565
func: async (input, runManager, config) => {
534566
return new Promise((resolve, reject) => {
@@ -539,7 +571,9 @@ export function tool<T extends ZodObjectAny | z.ZodString = ZodObjectAny>(
539571
childConfig,
540572
async () => {
541573
try {
542-
resolve(func(input, childConfig));
574+
// TS doesn't restrict the type here based on the guard above
575+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
576+
resolve(func(input as any, childConfig));
543577
} catch (e) {
544578
reject(e);
545579
}

langchain-core/src/tools/tests/tools.test.ts

+98-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test, expect } from "@jest/globals";
22
import { z } from "zod";
3-
import { tool } from "../index.js";
3+
import { DynamicStructuredTool, tool } from "../index.js";
44
import { ToolMessage } from "../../messages/tool.js";
55

66
test("Tool should error if responseFormat is content_and_artifact but the function doesn't return a tuple", async () => {
@@ -115,3 +115,100 @@ test("Tool can accept single string input", async () => {
115115
const result = await stringTool.invoke("b");
116116
expect(result).toBe("ba");
117117
});
118+
119+
test("Tool declared with JSON schema", async () => {
120+
const weatherSchema = {
121+
type: "object",
122+
properties: {
123+
location: {
124+
type: "string",
125+
description: "A place",
126+
},
127+
},
128+
required: ["location"],
129+
};
130+
const weatherTool = tool(
131+
(_) => {
132+
return "Sunny";
133+
},
134+
{
135+
name: "weather",
136+
schema: weatherSchema,
137+
}
138+
);
139+
140+
const weatherTool2 = new DynamicStructuredTool({
141+
name: "weather",
142+
description: "get the weather",
143+
func: async (_) => {
144+
return "Sunny";
145+
},
146+
schema: weatherSchema,
147+
});
148+
// No validation on JSON schema tools
149+
await weatherTool.invoke({
150+
somethingSilly: true,
151+
});
152+
await weatherTool2.invoke({
153+
somethingSilly: true,
154+
});
155+
});
156+
157+
test("Tool input typing is enforced", async () => {
158+
const weatherSchema = z.object({
159+
location: z.string(),
160+
});
161+
162+
const weatherTool = tool(
163+
(_) => {
164+
return "Sunny";
165+
},
166+
{
167+
name: "weather",
168+
schema: weatherSchema,
169+
}
170+
);
171+
172+
const weatherTool2 = new DynamicStructuredTool({
173+
name: "weather",
174+
description: "get the weather",
175+
func: async (_) => {
176+
return "Sunny";
177+
},
178+
schema: weatherSchema,
179+
});
180+
181+
const weatherTool3 = tool(
182+
async (_) => {
183+
return "Sunny";
184+
},
185+
{
186+
name: "weather",
187+
description: "get the weather",
188+
schema: z.string(),
189+
}
190+
);
191+
192+
await expect(async () => {
193+
await weatherTool.invoke({
194+
// @ts-expect-error Invalid argument
195+
badval: "someval",
196+
});
197+
}).rejects.toThrow();
198+
const res = await weatherTool.invoke({
199+
location: "somewhere",
200+
});
201+
expect(res).toEqual("Sunny");
202+
await expect(async () => {
203+
await weatherTool2.invoke({
204+
// @ts-expect-error Invalid argument
205+
badval: "someval",
206+
});
207+
}).rejects.toThrow();
208+
const res2 = await weatherTool2.invoke({
209+
location: "someval",
210+
});
211+
expect(res2).toEqual("Sunny");
212+
const res3 = await weatherTool3.invoke("blah");
213+
expect(res3).toEqual("Sunny");
214+
});

0 commit comments

Comments
 (0)