Skip to content

Commit

Permalink
core[minor]: RunnableToolLike (#6029)
Browse files Browse the repository at this point in the history
* init

* RunnableToolLike

* implemented without generics

* figured out generics typing almost

* use runnable binding instead of lambda

* use RunInput generic in a way...

* cr

* more test

* more tests and start docs

* drop docs

* allow runnabletoollike in bindTools

* mark as type in re-export

* drop default description

* update isRunnableToolLike to check lc_name

* added standard test

* cr

* default to zod description

* fix some jsdoc
  • Loading branch information
bracesproul authored Jul 11, 2024
1 parent 6111718 commit 2eb699d
Show file tree
Hide file tree
Showing 22 changed files with 373 additions and 39 deletions.
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",
"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<
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;
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

0 comments on commit 2eb699d

Please sign in to comment.