Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core[minor]: RunnableToolLike #6029

Merged
merged 19 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/core_docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.18.2",
"@langchain/langgraph": "latest",
"@langchain/langgraph": "0.0.26",
"@langchain/scripts": "workspace:*",
"@swc/core": "^1.3.62",
"@types/cookie": "^0",
Expand Down
1 change: 1 addition & 0 deletions langchain-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"prettier": "^2.8.3",
"release-it": "^15.10.1",
"rimraf": "^5.0.1",
"ts-jest": "^29.1.0",
bracesproul marked this conversation as resolved.
Show resolved Hide resolved
"typescript": "~5.1.6",
"web-streams-polyfill": "^3.3.3"
},
Expand Down
2 changes: 2 additions & 0 deletions langchain-core/src/language_models/chat_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
Runnable,
RunnableLambda,
RunnableSequence,
RunnableToolLike,
} from "../runnables/base.js";
import { isStreamEventsHandler } from "../tracers/event_stream.js";
import { isLogStreamHandler } from "../tracers/log_stream.js";
Expand Down Expand Up @@ -163,6 +164,7 @@ export abstract class BaseChatModel<
| StructuredToolInterface
| Record<string, unknown>
| ToolDefinition
| RunnableToolLike
)[],
kwargs?: Partial<CallOptions>
): Runnable<BaseLanguageModelInput, OutputMessageType, CallOptions>;
Expand Down
91 changes: 91 additions & 0 deletions langchain-core/src/runnables/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,26 @@ export abstract class Runnable<
],
});
}

/**
* Convert a runnable to a tool. Return a new instance of `RunnableToolLike`
* which contains the runnable, name, description and schema.
*
* @template {T extends RunInput = RunInput} RunInput - The input type of the runnable. Should be the same as the `RunInput` type of the runnable.
*
* @param fields
* @param {string | undefined} [fields.name] The name of the tool. If not provided, it will default to the name of the runnable.
* @param {string | undefined} [fields.description] The description of the tool. Falls back to the description on the Zod schema if not provided, or undefined if neither are provided.
* @param {z.ZodType<T>} [fields.schema] The Zod schema for the input of the tool. Infers the Zod type from the input type of the runnable.
* @returns {RunnableToolLike<z.ZodType<T>, RunOutput>} An instance of `RunnableToolLike` which is a runnable that can be used as a tool.
*/
asTool<T extends RunInput = RunInput>(fields: {
name?: string;
description?: string;
schema: z.ZodType<T>;
}): RunnableToolLike<z.ZodType<T>, RunOutput> {
return convertRunnableToTool<T, RunOutput>(this, fields);
}
}

export type RunnableBindingArgs<
Expand Down Expand Up @@ -2783,3 +2803,74 @@ export class RunnablePick<
return IterableReadableStream.fromAsyncGenerator(wrappedGenerator);
}
}

export interface RunnableToolLikeArgs<
RunInput extends z.ZodType = z.ZodType,
RunOutput = unknown
> extends Omit<RunnableBindingArgs<z.infer<RunInput>, RunOutput>, "config"> {
name: string;

description?: string;

schema: RunInput;

config?: RunnableConfig;
}

export class RunnableToolLike<
bracesproul marked this conversation as resolved.
Show resolved Hide resolved
RunInput extends z.ZodType = z.ZodType,
RunOutput = unknown
> extends RunnableBinding<z.infer<RunInput>, RunOutput> {
name: string;

description?: string;

schema: RunInput;

constructor(fields: RunnableToolLikeArgs<RunInput, RunOutput>) {
super({
bound: fields.bound,
config: fields.config ?? {},
});

this.name = fields.name;
this.description = fields.description;
this.schema = fields.schema;
}

static lc_name() {
return "RunnableToolLike";
}
}

/**
* Given a runnable and a Zod schema, convert the runnable to a tool.
*
* @template RunInput The input type for the runnable.
* @template RunOutput The output type for the runnable.
*
* @param {Runnable<RunInput, RunOutput>} runnable The runnable to convert to a tool.
* @param fields
* @param {string | undefined} [fields.name] The name of the tool. If not provided, it will default to the name of the runnable.
* @param {string | undefined} [fields.description] The description of the tool. Falls back to the description on the Zod schema if not provided, or undefined if neither are provided.
* @param {z.ZodType<RunInput>} [fields.schema] The Zod schema for the input of the tool. Infers the Zod type from the input type of the runnable.
* @returns {RunnableToolLike<z.ZodType<RunInput>, RunOutput>} An instance of `RunnableToolLike` which is a runnable that can be used as a tool.
*/
export function convertRunnableToTool<RunInput, RunOutput>(
runnable: Runnable<RunInput, RunOutput>,
fields: {
name?: string;
description?: string;
bracesproul marked this conversation as resolved.
Show resolved Hide resolved
schema: z.ZodType<RunInput>;
}
): RunnableToolLike<z.ZodType<RunInput>, RunOutput> {
const name = fields.name ?? runnable.getName();
const description = fields.description ?? fields.schema.description;

return new RunnableToolLike<z.ZodType<RunInput>, RunOutput>({
name,
description,
schema: fields.schema,
bound: runnable,
});
}
2 changes: 2 additions & 0 deletions langchain-core/src/runnables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export {
RunnableAssign,
RunnablePick,
_coerceToRunnable,
RunnableToolLike,
type RunnableToolLikeArgs,
} from "./base.js";
export {
type RunnableBatchOptions,
Expand Down
139 changes: 139 additions & 0 deletions langchain-core/src/runnables/tests/runnable_tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { RunnableLambda, RunnableToolLike } from "../base.js";

test("Runnable asTool works", async () => {
const schema = z.object({
foo: z.string(),
});
const runnable = RunnableLambda.from<z.infer<typeof schema>, string>(
(input, config) => {
return `${input.foo}${config?.configurable.foo}`;
}
);
const tool = runnable.asTool({
schema,
});

expect(tool).toBeInstanceOf(RunnableToolLike);
expect(tool.schema).toBe(schema);
expect(tool.name).toBe(runnable.getName());
});

test("Runnable asTool works with all populated fields", async () => {
const schema = z.object({
foo: z.string(),
});
const runnable = RunnableLambda.from<z.infer<typeof schema>, string>(
(input, config) => {
return `${input.foo}${config?.configurable.foo}`;
}
);
const tool = runnable.asTool({
schema,
name: "test",
description: "test",
});

expect(tool).toBeInstanceOf(RunnableToolLike);
expect(tool.schema).toBe(schema);
expect(tool.description).toBe("test");
expect(tool.name).toBe("test");
});

test("Runnable asTool can invoke", async () => {
const schema = z.object({
foo: z.string(),
});
const runnable = RunnableLambda.from<z.infer<typeof schema>, string>(
(input, config) => {
return `${input.foo}${config?.configurable.foo}`;
}
);
const tool = runnable.asTool({
schema,
});

const toolResponse = await tool.invoke(
{
foo: "bar",
},
{
configurable: {
foo: "bar",
},
}
);

expect(toolResponse).toBe("barbar");
});

test("asTool should type error with mismatched schema", async () => {
// asTool infers the type of the Zod schema from the existing runnable's RunInput generic.
// If the Zod schema does not match the RunInput, it should throw a type error.
const schema = z.object({
foo: z.string(),
});
const runnable = RunnableLambda.from<{ bar: string }, string>(
(input, config) => {
return `${input.bar}${config?.configurable.foo}`;
}
);
runnable.asTool({
// @ts-expect-error - Should error. If this does not give a type error, the generics/typing of `asTool` is broken.
schema,
});
});

test("Create a runnable tool directly from RunnableToolLike", async () => {
const schema = z.object({
foo: z.string(),
});
const adderFunc = (_: z.infer<typeof schema>): Promise<boolean> => {
return Promise.resolve(true);
};
const tool = new RunnableToolLike({
schema,
name: "test",
description: "test",
bound: RunnableLambda.from(adderFunc),
});

const result = await tool.invoke({ foo: "bar" });
expect(result).toBe(true);
});

test("asTool can take a single string input", async () => {
const firstRunnable = RunnableLambda.from<string, string>((input) => {
return `${input}a`;
});
const secondRunnable = RunnableLambda.from<string, string>((input) => {
return `${input}z`;
});

const runnable = firstRunnable.pipe(secondRunnable);
const asTool = runnable.asTool({
schema: z.string(),
});

const result = await asTool.invoke("b");
expect(result).toBe("baz");
});

test("Runnable asTool uses Zod schema description if not provided", async () => {
const description = "Test schema";
const schema = z
.object({
foo: z.string(),
})
.describe(description);
const runnable = RunnableLambda.from<z.infer<typeof schema>, string>(
(input, config) => {
return `${input.foo}${config?.configurable.foo}`;
}
);
const tool = runnable.asTool({
schema,
});

expect(tool.description).toBe(description);
});
4 changes: 1 addition & 3 deletions langchain-core/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import {
} from "./language_models/base.js";
import { ensureConfig, type RunnableConfig } from "./runnables/config.js";
import type { RunnableFunc, RunnableInterface } from "./runnables/base.js";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ZodAny = z.ZodObject<any, any, any, any>;
import { ZodAny } from "./types/zod.js";

/**
* Parameters for the Tool classes.
Expand Down
4 changes: 4 additions & 0 deletions langchain-core/src/types/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { z } from "zod";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ZodAny = z.ZodObject<any, any, any, any>;
52 changes: 41 additions & 11 deletions langchain-core/src/utils/function_calling.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { zodToJsonSchema } from "zod-to-json-schema";
import { StructuredToolInterface } from "../tools.js";
import { FunctionDefinition, ToolDefinition } from "../language_models/base.js";
import { Runnable, RunnableToolLike } from "../runnables/base.js";

/**
* Formats a `StructuredTool` instance into a format that is compatible
* with OpenAI function calling. It uses the `zodToJsonSchema`
* function to convert the schema of the `StructuredTool` into a JSON
* schema, which is then used as the parameters for the OpenAI function.
* Formats a `StructuredTool` or `RunnableToolLike` instance into a format
* that is compatible with OpenAI function calling. It uses the `zodToJsonSchema`
* function to convert the schema of the `StructuredTool` or `RunnableToolLike`
* into a JSON schema, which is then used as the parameters for the OpenAI function.
*
* @param {StructuredToolInterface | RunnableToolLike} tool The tool to convert to an OpenAI function.
* @returns {FunctionDefinition} The inputted tool in OpenAI function format.
*/
export function convertToOpenAIFunction(
tool: StructuredToolInterface
tool: StructuredToolInterface | RunnableToolLike
): FunctionDefinition {
return {
name: tool.name,
Expand All @@ -19,16 +23,20 @@ export function convertToOpenAIFunction(
}

/**
* Formats a `StructuredTool` instance into a format that is compatible
* with OpenAI tool calling. It uses the `zodToJsonSchema`
* function to convert the schema of the `StructuredTool` into a JSON
* schema, which is then used as the parameters for the OpenAI tool.
* Formats a `StructuredTool` or `RunnableToolLike` instance into a
* format that is compatible with OpenAI tool calling. It uses the
* `zodToJsonSchema` function to convert the schema of the `StructuredTool`
* or `RunnableToolLike` into a JSON schema, which is then used as the
* parameters for the OpenAI tool.
*
* @param {StructuredToolInterface | Record<string, any> | RunnableToolLike} tool The tool to convert to an OpenAI tool.
* @returns {ToolDefinition} The inputted tool in OpenAI tool format.
*/
export function convertToOpenAITool(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tool: StructuredToolInterface | Record<string, any>
tool: StructuredToolInterface | Record<string, any> | RunnableToolLike
): ToolDefinition {
if (isStructuredTool(tool)) {
if (isStructuredTool(tool) || isRunnableToolLike(tool)) {
return {
type: "function",
function: convertToOpenAIFunction(tool),
Expand All @@ -37,6 +45,12 @@ export function convertToOpenAITool(
return tool as ToolDefinition;
}

/**
* Confirm whether the inputted tool is an instance of `StructuredToolInterface`.
*
* @param {StructuredToolInterface | Record<string, any> | undefined} tool The tool to check if it is an instance of `StructuredToolInterface`.
* @returns {tool is StructuredToolInterface} Whether the inputted tool is an instance of `StructuredToolInterface`.
*/
export function isStructuredTool(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tool?: StructuredToolInterface | Record<string, any>
Expand All @@ -46,3 +60,19 @@ export function isStructuredTool(
Array.isArray((tool as StructuredToolInterface).lc_namespace)
);
}

/**
* Confirm whether the inputted tool is an instance of `RunnableToolLike`.
*
* @param {unknown | undefined} tool The tool to check if it is an instance of `RunnableToolLike`.
* @returns {tool is RunnableToolLike} Whether the inputted tool is an instance of `RunnableToolLike`.
*/
export function isRunnableToolLike(tool?: unknown): tool is RunnableToolLike {
return (
tool !== undefined &&
Runnable.isRunnable(tool) &&
"lc_name" in tool.constructor &&
typeof tool.constructor.lc_name === "function" &&
tool.constructor.lc_name() === "RunnableToolLike"
);
}
Loading
Loading