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 12 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
101 changes: 101 additions & 0 deletions langchain-core/src/runnables/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type TraceableFunction,
isTraceableFunction,
} from "langsmith/singletons/traceable";
import { zodToJsonSchema } from "zod-to-json-schema";
import type { RunnableInterface, RunnableBatchOptions } from "./types.js";
import { CallbackManagerForChainRun } from "../callbacks/manager.js";
import {
Expand Down Expand Up @@ -1078,6 +1079,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. If not provided, it will default to `Takes {schema}` where `schema` is a JSON string representation of the input schema.
* @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 +2804,83 @@ 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> {
lc_runnable_tool_like: true;
bracesproul marked this conversation as resolved.
Show resolved Hide resolved

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;
}
}

/**
* Generate a placeholder description of a runnable
*/
const _getDescriptionFromRunnable = <RunInput = unknown>(
schema: RunInput
): string => {
return `Takes ${JSON.stringify(schema, null, 2)}`;
bracesproul marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* 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. If not provided, it will default to `Takes {schema}` where `schema` is a JSON string representation of the input schema.
* @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 description =
fields.description ??
_getDescriptionFromRunnable(zodToJsonSchema(fields.schema));
const name = fields.name ?? runnable.getName();

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
124 changes: 124 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,124 @@
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
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.description).toBe(
`Takes ${JSON.stringify(zodToJsonSchema(schema), null, 2)}`
);
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");
});
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>;
16 changes: 13 additions & 3 deletions langchain-core/src/utils/function_calling.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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
Expand All @@ -9,7 +10,7 @@ import { FunctionDefinition, ToolDefinition } from "../language_models/base.js";
* schema, which is then used as the parameters for the OpenAI function.
*/
export function convertToOpenAIFunction(
tool: StructuredToolInterface
tool: StructuredToolInterface | RunnableToolLike
): FunctionDefinition {
return {
name: tool.name,
Expand All @@ -26,9 +27,9 @@ export function convertToOpenAIFunction(
*/
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 @@ -46,3 +47,12 @@ export function isStructuredTool(
Array.isArray((tool as StructuredToolInterface).lc_namespace)
);
}

export function isRunnableToolLike(tool?: unknown): tool is RunnableToolLike {
return (
tool !== undefined &&
Runnable.isRunnable(tool) &&
"lc_runnable_tool_like" in tool &&
tool.lc_runnable_tool_like === true
);
}
3 changes: 3 additions & 0 deletions libs/langchain-anthropic/src/chat_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
Runnable,
RunnablePassthrough,
RunnableSequence,
RunnableToolLike,
} from "@langchain/core/runnables";
import { isZodSchema } from "@langchain/core/utils/types";
import { ToolCall } from "@langchain/core/messages/tool";
Expand Down Expand Up @@ -73,6 +74,7 @@ export interface ChatAnthropicCallOptions
| AnthropicTool
| Record<string, unknown>
| ToolDefinition
| RunnableToolLike
)[];
/**
* Whether or not to specify what tool the model should use
Expand Down Expand Up @@ -592,6 +594,7 @@ export class ChatAnthropicMessages<
| Record<string, unknown>
| StructuredToolInterface
| ToolDefinition
| RunnableToolLike
)[],
kwargs?: Partial<CallOptions>
): Runnable<BaseLanguageModelInput, AIMessageChunk, CallOptions> {
Expand Down
3 changes: 2 additions & 1 deletion libs/langchain-aws/src/chat_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from "@aws-sdk/credential-provider-node";
import type { DocumentType as __DocumentType } from "@smithy/types";
import { StructuredToolInterface } from "@langchain/core/tools";
import { Runnable } from "@langchain/core/runnables";
import { Runnable, RunnableToolLike } from "@langchain/core/runnables";
import {
BedrockToolChoice,
ConverseCommandParams,
Expand Down Expand Up @@ -289,6 +289,7 @@ export class ChatBedrockConverse
| ToolDefinition
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| Record<string, any>
| RunnableToolLike
)[],
kwargs?: Partial<this["ParsedCallOptions"]>
): Runnable<
Expand Down
Loading
Loading