Skip to content

Commit

Permalink
cr
Browse files Browse the repository at this point in the history
  • Loading branch information
bracesproul committed Jul 12, 2024
1 parent 69154ae commit 471116b
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 38 deletions.
42 changes: 22 additions & 20 deletions langchain-core/src/tools/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ 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" | "contentAndRawOutput";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ContentAndRawOutput = [MessageContent, any];

/**
* Parameters for the Tool classes.
*/
Expand Down Expand Up @@ -119,7 +123,7 @@ export abstract class StructuredTool<
arg: z.output<T>,
runManager?: CallbackManagerForToolRun,
config?: RunnableConfig
): Promise<RunOutput>;
): Promise<string | ContentAndRawOutput>;

/**
* Invokes the tool with the provided input and configuration.
Expand Down Expand Up @@ -294,30 +298,27 @@ export interface BaseDynamicToolInput extends ToolParams {
/**
* Interface for the input parameters of the DynamicTool class.
*/
export interface DynamicToolInput<
RunOutput extends string | ToolMessage = string
> extends BaseDynamicToolInput {
export interface DynamicToolInput extends BaseDynamicToolInput {
func: (
input: string,
runManager?: CallbackManagerForToolRun,
config?: RunnableConfig
) => Promise<RunOutput>;
) => Promise<string | ContentAndRawOutput>;
}

/**
* Interface for the input parameters of the DynamicStructuredTool class.
*/
export interface DynamicStructuredToolInput<
T extends ZodAny = ZodAny,
RunOutput extends string | ToolMessage = string
T extends ZodAny = ZodAny
> extends BaseDynamicToolInput {
func: (
input: BaseDynamicToolInput["responseFormat"] extends "contentAndRawOutput"
? ToolCall
: z.infer<T>,
runManager?: CallbackManagerForToolRun,
config?: RunnableConfig
) => Promise<RunOutput>;
) => Promise<string | ContentAndRawOutput>;
schema: T;
}

Expand All @@ -335,9 +336,9 @@ export class DynamicTool<

description: string;

func: DynamicToolInput<RunOutput>["func"];
func: DynamicToolInput["func"];

constructor(fields: DynamicToolInput<RunOutput>) {
constructor(fields: DynamicToolInput) {
super(fields);
this.name = fields.name;
this.description = fields.description;
Expand All @@ -364,7 +365,7 @@ export class DynamicTool<
input: string,
runManager?: CallbackManagerForToolRun,
config?: RunnableConfig
): Promise<RunOutput> {
): Promise<string | ContentAndRawOutput> {
return this.func(input, runManager, config);
}
}
Expand All @@ -387,11 +388,11 @@ export class DynamicStructuredTool<

description: string;

func: DynamicStructuredToolInput<T, RunOutput>["func"];
func: DynamicStructuredToolInput<T>["func"];

schema: T;

constructor(fields: DynamicStructuredToolInput<T, RunOutput>) {
constructor(fields: DynamicStructuredToolInput<T>) {
super(fields);
this.name = fields.name;
this.description = fields.description;
Expand Down Expand Up @@ -420,7 +421,7 @@ export class DynamicStructuredTool<
arg: z.output<T> | ToolCall,
runManager?: CallbackManagerForToolRun,
config?: RunnableConfig
): Promise<RunOutput> {
): Promise<string | ContentAndRawOutput> {
return this.func(arg, runManager, config);
}
}
Expand Down Expand Up @@ -491,9 +492,9 @@ interface ToolWrapperParams<RunInput extends ZodAny = ZodAny>
export function tool<
RunInput extends ZodAny = ZodAny,
RunOutput extends ToolMessage = ToolMessage,
FuncInput extends z.infer<RunInput> | ToolCall = z.infer<RunInput>
FuncInput extends z.infer<RunInput> | ToolCall = z.infer<RunInput>,
>(
func: RunnableFunc<FuncInput, RunOutput>,
func: RunnableFunc<FuncInput, ContentAndRawOutput>,
fields: Omit<ToolWrapperParams<RunInput>, "responseFormat"> & {
responseFormat: "contentAndRawOutput";
}
Expand All @@ -502,9 +503,9 @@ export function tool<
export function tool<
RunInput extends ZodAny = ZodAny,
RunOutput extends string = string,
FuncInput extends z.infer<RunInput> | ToolCall = z.infer<RunInput>
FuncInput extends z.infer<RunInput> | ToolCall = z.infer<RunInput>,
>(
func: RunnableFunc<FuncInput, RunOutput>,
func: RunnableFunc<FuncInput, string>,
fields: Omit<ToolWrapperParams<RunInput>, "responseFormat"> & {
responseFormat?: "content" | undefined;
}
Expand All @@ -513,9 +514,10 @@ export function tool<
export function tool<
RunInput extends ZodAny = ZodAny,
RunOutput extends string | ToolMessage = string,
FuncInput extends z.infer<RunInput> | ToolCall = z.infer<RunInput>
FuncInput extends z.infer<RunInput> | ToolCall = z.infer<RunInput>,
FuncOutput extends string | ContentAndRawOutput = string,
>(
func: RunnableFunc<FuncInput, RunOutput>,
func: RunnableFunc<FuncInput, FuncOutput>,
fields: ToolWrapperParams<RunInput>
): DynamicStructuredTool<RunInput, RunOutput> {
const schema =
Expand Down
72 changes: 54 additions & 18 deletions langchain-core/src/tools/tests/tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { test, expect } from "@jest/globals";
import { z } from "zod";
import { tool } from "../base.js";
import { ContentAndRawOutput, tool } from "../base.js";
import { ToolCall, ToolMessage } from "../../messages/tool.js";

test("Tool should throw type error if responseFormat does not match func input type", () => {
test("Tool should throw type error if types are wrong", () => {
const weatherSchema = z.object({
location: z.string(),
});

// @ts-expect-error - Error because responseFormat: contentAndRawOutput makes return type be an instance of ToolMessage
// @ts-expect-error - Error because responseFormat: contentAndRawOutput makes return type ContentAndRawOutput
tool(
(_): string => {
return "no-op";
Expand All @@ -21,11 +22,8 @@ test("Tool should throw type error if responseFormat does not match func input t

// @ts-expect-error - Error because responseFormat: content makes return type be a string
tool(
(_): ToolMessage => {
return new ToolMessage({
content: "no-op",
tool_call_id: "no-op",
});
(_): ContentAndRawOutput => {
return ["no-op", true]
},
{
name: "weather",
Expand All @@ -36,11 +34,8 @@ test("Tool should throw type error if responseFormat does not match func input t

// @ts-expect-error - Error because responseFormat: undefined makes return type be a string
tool(
(_): ToolMessage => {
return new ToolMessage({
content: "no-op",
tool_call_id: "no-op",
});
(_): ContentAndRawOutput => {
return ["no-op", true]
},
{
name: "weather",
Expand All @@ -50,11 +45,8 @@ test("Tool should throw type error if responseFormat does not match func input t

// Should pass because we're expecting a `ToolMessage` return type due to `responseFormat: contentAndRawOutput`
tool(
(_): ToolMessage => {
return new ToolMessage({
content: "no-op",
tool_call_id: "no-op",
});
(_): ContentAndRawOutput => {
return ["no-op", true]
},
{
name: "weather",
Expand Down Expand Up @@ -108,3 +100,47 @@ test("Tool should throw type error if responseFormat does not match func input t
}
);
});

test("Tool should error if responseFormat is contentAndRawOutput but the function doesn't return a tuple", async () => {
const weatherSchema = z.object({
location: z.string(),
});

const weatherTool = tool(
(_): any => {

Check failure on line 110 in langchain-core/src/tools/tests/tools.test.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type
return "str"
},
{
name: "weather",
schema: weatherSchema,
responseFormat: "contentAndRawOutput",
}
);

await expect(async () => {
await weatherTool.invoke({ location: "San Francisco" });
}).rejects.toThrow();
});

test.only("Tool works if responseFormat is contentAndRawOutput and returns a tuple", async () => {

Check failure on line 125 in langchain-core/src/tools/tests/tools.test.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected focused test
const weatherSchema = z.object({
location: z.string(),
});

const weatherTool = tool(
(input): any => {

Check failure on line 131 in langchain-core/src/tools/tests/tools.test.ts

View workflow job for this annotation

GitHub Actions / Check linting

Unexpected any. Specify a different type
return ["msg_content", input]
},
{
name: "weather",
schema: weatherSchema,
responseFormat: "contentAndRawOutput",
}
);

const toolResult = await weatherTool.invoke({ location: "San Francisco" });

expect(toolResult).toBeInstanceOf(ToolMessage);
expect(toolResult.content).toBe("msg_content");
expect(toolResult.raw_output).toEqual({ location: "San Francisco" });
});

0 comments on commit 471116b

Please sign in to comment.