From f9e7cd02ad1607c1a70ab020535e8f29866ef977 Mon Sep 17 00:00:00 2001 From: bracesproul Date: Wed, 12 Jun 2024 13:23:39 -0700 Subject: [PATCH] Added oai tool support to anthropic --- langchain-core/.gitignore | 4 ++ langchain-core/langchain.config.js | 1 + langchain-core/package.json | 13 ++++ langchain-core/src/utils/is_openai_tool.ts | 17 +++++ libs/langchain-anthropic/package.json | 2 +- libs/langchain-anthropic/src/chat_models.ts | 65 ++++++++++++------- .../src/tests/chat_models-tools.int.test.ts | 22 +++++++ yarn.lock | 10 +-- 8 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 langchain-core/src/utils/is_openai_tool.ts diff --git a/langchain-core/.gitignore b/langchain-core/.gitignore index 26f8fb1da8c8..247cd396048a 100644 --- a/langchain-core/.gitignore +++ b/langchain-core/.gitignore @@ -214,6 +214,10 @@ utils/types.cjs utils/types.js utils/types.d.ts utils/types.d.cts +utils/is_openai_tool.cjs +utils/is_openai_tool.js +utils/is_openai_tool.d.ts +utils/is_openai_tool.d.cts vectorstores.cjs vectorstores.js vectorstores.d.ts diff --git a/langchain-core/langchain.config.js b/langchain-core/langchain.config.js index cc1ba4060870..c1a20f6cad80 100644 --- a/langchain-core/langchain.config.js +++ b/langchain-core/langchain.config.js @@ -66,6 +66,7 @@ export const config = { "utils/testing": "utils/testing/index", "utils/tiktoken": "utils/tiktoken", "utils/types": "utils/types/index", + "utils/is_openai_tool": "utils/is_openai_tool", vectorstores: "vectorstores", }, tsConfigPath: resolve("./tsconfig.json"), diff --git a/langchain-core/package.json b/langchain-core/package.json index abc017dc35ce..0561ba93be66 100644 --- a/langchain-core/package.json +++ b/langchain-core/package.json @@ -581,6 +581,15 @@ "import": "./utils/types.js", "require": "./utils/types.cjs" }, + "./utils/is_openai_tool": { + "types": { + "import": "./utils/is_openai_tool.d.ts", + "require": "./utils/is_openai_tool.d.cts", + "default": "./utils/is_openai_tool.d.ts" + }, + "import": "./utils/is_openai_tool.js", + "require": "./utils/is_openai_tool.cjs" + }, "./vectorstores": { "types": { "import": "./vectorstores.d.ts", @@ -810,6 +819,10 @@ "utils/types.js", "utils/types.d.ts", "utils/types.d.cts", + "utils/is_openai_tool.cjs", + "utils/is_openai_tool.js", + "utils/is_openai_tool.d.ts", + "utils/is_openai_tool.d.cts", "vectorstores.cjs", "vectorstores.js", "vectorstores.d.ts", diff --git a/langchain-core/src/utils/is_openai_tool.ts b/langchain-core/src/utils/is_openai_tool.ts new file mode 100644 index 000000000000..bf80bf1a22fd --- /dev/null +++ b/langchain-core/src/utils/is_openai_tool.ts @@ -0,0 +1,17 @@ +import { ToolDefinition } from "../language_models/base.js"; + +export function isOpenAITool(tool: unknown): tool is ToolDefinition { + if (typeof tool !== "object" || !tool) return false; + if ( + "type" in tool && + tool.type === "function" && + "function" in tool && + typeof tool.function === "object" && + tool.function && + "name" in tool.function && + "parameters" in tool.function + ) { + return true; + } + return false; +} diff --git a/libs/langchain-anthropic/package.json b/libs/langchain-anthropic/package.json index b7baf1e8feed..586b79f36cab 100644 --- a/libs/langchain-anthropic/package.json +++ b/libs/langchain-anthropic/package.json @@ -35,7 +35,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.21.0", + "@anthropic-ai/sdk": "^0.22.0", "@langchain/core": ">=0.2.5 <0.3.0", "fast-xml-parser": "^4.3.5", "zod": "^3.22.4", diff --git a/libs/langchain-anthropic/src/chat_models.ts b/libs/langchain-anthropic/src/chat_models.ts index 036b21515671..c0fdad0f5fe0 100644 --- a/libs/langchain-anthropic/src/chat_models.ts +++ b/libs/langchain-anthropic/src/chat_models.ts @@ -25,9 +25,10 @@ import { type BaseChatModelParams, } from "@langchain/core/language_models/chat_models"; import { - StructuredOutputMethodOptions, + type StructuredOutputMethodOptions, type BaseLanguageModelCallOptions, - BaseLanguageModelInput, + type BaseLanguageModelInput, + type ToolDefinition, } from "@langchain/core/language_models/base"; import { StructuredToolInterface } from "@langchain/core/tools"; import { zodToJsonSchema } from "zod-to-json-schema"; @@ -45,15 +46,11 @@ import { extractToolCalls, } from "./output_parsers.js"; import { AnthropicToolResponse } from "./types.js"; - -type AnthropicTool = { - name: string; - description: string; - /** - * JSON schema. - */ - input_schema: Record; -}; +import { isOpenAITool } from "@langchain/core/utils/is_openai_tool"; +import type { + MessageCreateParams, + Tool as AnthropicTool, +} from "@anthropic-ai/sdk/resources/index.mjs"; type AnthropicMessage = Anthropic.MessageParam; type AnthropicMessageCreateParams = Anthropic.MessageCreateParamsNonStreaming; @@ -71,7 +68,12 @@ type AnthropicToolChoice = export interface ChatAnthropicCallOptions extends BaseLanguageModelCallOptions, Pick { - tools?: (StructuredToolInterface | AnthropicTool)[]; + tools?: ( + | StructuredToolInterface + | AnthropicTool + | Record + | ToolDefinition + )[]; /** * Whether or not to specify what tool the model should use * @default "auto" @@ -562,22 +564,35 @@ export class ChatAnthropicMessages< return tools as AnthropicTool[]; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((tools as any[]).every((tool) => isOpenAITool(tool))) { + // Formatted as OpenAI tool, convert to Anthropic tool + return (tools as ToolDefinition[]).map((tc) => ({ + name: tc.function.name, + description: tc.function.description, + input_schema: tc.function.parameters as AnthropicTool.InputSchema, + })); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((tools as any[]).some((tool) => isAnthropicTool(tool))) { - throw new Error( - `Can not pass in a mix of AnthropicTools and StructuredTools` - ); + throw new Error(`Can not pass in a mix of tool schemas to ChatAnthropic`); } return (tools as StructuredToolInterface[]).map((tool) => ({ name: tool.name, description: tool.description, - input_schema: zodToJsonSchema(tool.schema), + input_schema: zodToJsonSchema(tool.schema) as AnthropicTool.InputSchema, })); } override bindTools( - tools: (AnthropicTool | StructuredToolInterface)[], + tools: ( + | AnthropicTool + | Record + | StructuredToolInterface + | ToolDefinition + )[], kwargs?: Partial ): Runnable { return this.bind({ @@ -597,10 +612,9 @@ export class ChatAnthropicMessages< > & Kwargs { let tool_choice: - | { - type: string; - name?: string; - } + | MessageCreateParams.ToolChoiceAuto + | MessageCreateParams.ToolChoiceAny + | MessageCreateParams.ToolChoiceTool | undefined; if (options?.tool_choice) { if (options?.tool_choice === "any") { @@ -739,7 +753,10 @@ export class ChatAnthropicMessages< if (data?.usage !== undefined) { usageData.output_tokens += data.usage.output_tokens; } - } else if (data.type === "content_block_delta") { + } else if ( + data.type === "content_block_delta" && + data.delta.type === "text_delta" + ) { const content = data.delta?.text; if (content !== undefined) { yield new ChatGenerationChunk({ @@ -976,7 +993,7 @@ export class ChatAnthropicMessages< name: functionName, description: jsonSchema.description ?? "A function available to call.", - input_schema: jsonSchema, + input_schema: jsonSchema as AnthropicTool.InputSchema, }, ]; outputParser = new AnthropicToolsOutputParser({ @@ -998,7 +1015,7 @@ export class ChatAnthropicMessages< anthropicTools = { name: functionName, description: schema.description ?? "", - input_schema: schema, + input_schema: schema as AnthropicTool.InputSchema, }; } tools = [anthropicTools]; diff --git a/libs/langchain-anthropic/src/tests/chat_models-tools.int.test.ts b/libs/langchain-anthropic/src/tests/chat_models-tools.int.test.ts index 153a6e00ea34..04ac8900a772 100644 --- a/libs/langchain-anthropic/src/tests/chat_models-tools.int.test.ts +++ b/libs/langchain-anthropic/src/tests/chat_models-tools.int.test.ts @@ -340,3 +340,25 @@ test("Can pass tool_choice", async () => { expect(input).toBeTruthy(); expect(input.location).toBeTruthy(); }); + +test("bindTools accepts openai formatted tool", async () => { + const openaiTool = { + type: "function", + function: { + name: "get_weather", + description: + "Get the weather of a specific location and return the temperature in Celsius.", + parameters: zodToJsonSchema(zodSchema), + }, + }; + const modelWithTools = model.bindTools([openaiTool]); + const response = await modelWithTools.invoke( + "Whats the weather like in san francisco?" + ); + expect(response.tool_calls).toHaveLength(1); + const { tool_calls } = response; + if (!tool_calls) { + return; + } + expect(tool_calls[0].name).toBe("get_weather"); +}); diff --git a/yarn.lock b/yarn.lock index c3f2da202db9..f1fe833cb646 100644 --- a/yarn.lock +++ b/yarn.lock @@ -211,9 +211,9 @@ __metadata: languageName: node linkType: hard -"@anthropic-ai/sdk@npm:^0.21.0": - version: 0.21.0 - resolution: "@anthropic-ai/sdk@npm:0.21.0" +"@anthropic-ai/sdk@npm:^0.22.0": + version: 0.22.0 + resolution: "@anthropic-ai/sdk@npm:0.22.0" dependencies: "@types/node": ^18.11.18 "@types/node-fetch": ^2.6.4 @@ -223,7 +223,7 @@ __metadata: formdata-node: ^4.3.2 node-fetch: ^2.6.7 web-streams-polyfill: ^3.2.1 - checksum: fbed720938487495f1d28822fa6eb3871cf7e7be325c299b69efa78e72e1e0b66d9f564003ae5d7a1e96c7555cc69c817be4b901d1847ae002f782546a4c987d + checksum: f09fc6ea1f5f68483fd2dbdc1ab78a0914d7a3f39fd4a921f5a04b8959ae82aa210372fa2dbff2ee78143d37fb408e7b7d61e0023e7ea31d5fa1f4873f371af8 languageName: node linkType: hard @@ -8926,7 +8926,7 @@ __metadata: version: 0.0.0-use.local resolution: "@langchain/anthropic@workspace:libs/langchain-anthropic" dependencies: - "@anthropic-ai/sdk": ^0.21.0 + "@anthropic-ai/sdk": ^0.22.0 "@jest/globals": ^29.5.0 "@langchain/community": "workspace:*" "@langchain/core": ">=0.2.5 <0.3.0"