@langchain/anthropic 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import { test } from "@jest/globals";
4
4
  import { z } from "zod";
5
5
  import { zodToJsonSchema } from "zod-to-json-schema";
6
6
  import { HumanMessage } from "@langchain/core/messages";
7
+ import { ChatPromptTemplate } from "@langchain/core/prompts";
7
8
  import { ChatAnthropicTools } from "../tool_calling.js";
8
9
  test("Test ChatAnthropicTools", async () => {
9
10
  const chat = new ChatAnthropicTools({
@@ -189,12 +190,11 @@ test("Test ChatAnthropic withStructuredOutput", async () => {
189
190
  const runnable = new ChatAnthropicTools({
190
191
  modelName: "claude-3-sonnet-20240229",
191
192
  maxRetries: 0,
192
- }).withStructuredOutput({
193
- schema: z.object({
194
- name: z.string().describe("The name of a person"),
195
- height: z.number().describe("The person's height"),
196
- hairColor: z.optional(z.string()).describe("The person's hair color"),
197
- }),
193
+ }).withStructuredOutput(z.object({
194
+ name: z.string().describe("The name of a person"),
195
+ height: z.number().describe("The person's height"),
196
+ hairColor: z.optional(z.string()).describe("The person's hair color"),
197
+ }), {
198
198
  name: "person",
199
199
  });
200
200
  const message = new HumanMessage("Alex is 5 feet tall. Alex is blonde.");
@@ -202,3 +202,63 @@ test("Test ChatAnthropic withStructuredOutput", async () => {
202
202
  console.log(JSON.stringify(res, null, 2));
203
203
  expect(res).toEqual({ name: "Alex", height: 5, hairColor: "blonde" });
204
204
  });
205
+ test("Test ChatAnthropic withStructuredOutput on a single array item", async () => {
206
+ const runnable = new ChatAnthropicTools({
207
+ modelName: "claude-3-sonnet-20240229",
208
+ maxRetries: 0,
209
+ }).withStructuredOutput(z.object({
210
+ people: z.array(z.object({
211
+ name: z.string().describe("The name of a person"),
212
+ height: z.number().describe("The person's height"),
213
+ hairColor: z.optional(z.string()).describe("The person's hair color"),
214
+ })),
215
+ }));
216
+ const message = new HumanMessage("Alex is 5 feet tall. Alex is blonde.");
217
+ const res = await runnable.invoke([message]);
218
+ console.log(JSON.stringify(res, null, 2));
219
+ expect(res).toEqual({
220
+ people: [{ hairColor: "blonde", height: 5, name: "Alex" }],
221
+ });
222
+ });
223
+ test("Test ChatAnthropic withStructuredOutput on a single array item", async () => {
224
+ const runnable = new ChatAnthropicTools({
225
+ modelName: "claude-3-sonnet-20240229",
226
+ maxRetries: 0,
227
+ }).withStructuredOutput(z.object({
228
+ sender: z
229
+ .optional(z.string())
230
+ .describe("The sender's name, if available"),
231
+ sender_phone_number: z
232
+ .optional(z.string())
233
+ .describe("The sender's phone number, if available"),
234
+ sender_address: z
235
+ .optional(z.string())
236
+ .describe("The sender's address, if available"),
237
+ action_items: z
238
+ .array(z.string())
239
+ .describe("A list of action items requested by the email"),
240
+ topic: z
241
+ .string()
242
+ .describe("High level description of what the email is about"),
243
+ tone: z.enum(["positive", "negative"]).describe("The tone of the email."),
244
+ }), {
245
+ name: "Email",
246
+ });
247
+ const prompt = ChatPromptTemplate.fromMessages([
248
+ [
249
+ "human",
250
+ "What can you tell me about the following email? Make sure to answer in the correct format: {email}",
251
+ ],
252
+ ]);
253
+ const extractionChain = prompt.pipe(runnable);
254
+ const response = await extractionChain.invoke({
255
+ email: "From: Erick. The email is about the new project. The tone is positive. The action items are to send the report and to schedule a meeting.",
256
+ });
257
+ console.log(JSON.stringify(response, null, 2));
258
+ expect(response).toEqual({
259
+ sender: "Erick",
260
+ action_items: [expect.any(String), expect.any(String)],
261
+ topic: expect.any(String),
262
+ tone: "positive",
263
+ });
264
+ });
@@ -63,6 +63,7 @@ class ChatAnthropicTools extends chat_models_1.BaseChatModel {
63
63
  let promptMessages = messages;
64
64
  let forced = false;
65
65
  let toolCall;
66
+ const tools = options.tools === undefined ? [] : [...options.tools];
66
67
  if (options.tools !== undefined && options.tools.length > 0) {
67
68
  const content = await systemPromptTemplate.format({
68
69
  tools: `<tools>\n${options.tools
@@ -121,14 +122,20 @@ class ChatAnthropicTools extends chat_models_1.BaseChatModel {
121
122
  const responseMessageWithFunctions = new messages_1.AIMessage({
122
123
  content: "",
123
124
  additional_kwargs: {
124
- tool_calls: invocations.map((toolInvocation, i) => ({
125
- id: i.toString(),
126
- type: "function",
127
- function: {
128
- name: toolInvocation.tool_name,
129
- arguments: JSON.stringify(toolInvocation.parameters),
130
- },
131
- })),
125
+ tool_calls: invocations.map((toolInvocation, i) => {
126
+ const calledTool = tools.find((tool) => tool.function.name === toolCall);
127
+ if (calledTool === undefined) {
128
+ throw new Error(`Called tool "${toolCall}" did not match an existing tool.`);
129
+ }
130
+ return {
131
+ id: i.toString(),
132
+ type: "function",
133
+ function: {
134
+ name: toolInvocation.tool_name,
135
+ arguments: JSON.stringify((0, tool_calling_js_1.fixArrayXMLParameters)(calledTool.function.parameters, toolInvocation.parameters)),
136
+ },
137
+ };
138
+ }),
132
139
  },
133
140
  });
134
141
  return {
@@ -144,14 +151,20 @@ class ChatAnthropicTools extends chat_models_1.BaseChatModel {
144
151
  const responseMessageWithFunctions = new messages_1.AIMessage({
145
152
  content: chatGenerationContent.split("<function_calls>")[0],
146
153
  additional_kwargs: {
147
- tool_calls: invocations.map((toolInvocation, i) => ({
148
- id: i.toString(),
149
- type: "function",
150
- function: {
151
- name: toolInvocation.tool_name,
152
- arguments: JSON.stringify(toolInvocation.parameters),
153
- },
154
- })),
154
+ tool_calls: invocations.map((toolInvocation, i) => {
155
+ const calledTool = tools.find((tool) => tool.function.name === toolInvocation.tool_name);
156
+ if (calledTool === undefined) {
157
+ throw new Error(`Called tool "${toolCall}" did not match an existing tool.`);
158
+ }
159
+ return {
160
+ id: i.toString(),
161
+ type: "function",
162
+ function: {
163
+ name: toolInvocation.tool_name,
164
+ arguments: JSON.stringify((0, tool_calling_js_1.fixArrayXMLParameters)(calledTool.function.parameters, toolInvocation.parameters)),
165
+ },
166
+ };
167
+ }),
155
168
  },
156
169
  });
157
170
  return {
@@ -171,7 +184,24 @@ class ChatAnthropicTools extends chat_models_1.BaseChatModel {
171
184
  _llmType() {
172
185
  return "anthropic_tool_calling";
173
186
  }
174
- withStructuredOutput({ schema, name, method, includeRaw, }) {
187
+ withStructuredOutput(outputSchema, config) {
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
+ let schema;
190
+ let name;
191
+ let method;
192
+ let includeRaw;
193
+ if (isStructuredOutputMethodParams(outputSchema)) {
194
+ schema = outputSchema.schema;
195
+ name = outputSchema.name;
196
+ method = outputSchema.method;
197
+ includeRaw = outputSchema.includeRaw;
198
+ }
199
+ else {
200
+ schema = outputSchema;
201
+ name = config?.name;
202
+ method = config?.method;
203
+ includeRaw = config?.includeRaw;
204
+ }
175
205
  if (method === "jsonMode") {
176
206
  throw new Error(`Anthropic only supports "functionCalling" as a method.`);
177
207
  }
@@ -245,3 +275,11 @@ function isZodSchema(input) {
245
275
  // Check for a characteristic method of Zod schemas
246
276
  return typeof input?.parse === "function";
247
277
  }
278
+ function isStructuredOutputMethodParams(x
279
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
280
+ ) {
281
+ return (x !== undefined &&
282
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
283
+ typeof x.schema ===
284
+ "object");
285
+ }
@@ -3,8 +3,9 @@ import { ChatGenerationChunk, ChatResult } from "@langchain/core/outputs";
3
3
  import { BaseChatModel, BaseChatModelParams } from "@langchain/core/language_models/chat_models";
4
4
  import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager";
5
5
  import { BasePromptTemplate } from "@langchain/core/prompts";
6
- import { BaseLanguageModelCallOptions, BaseLanguageModelInput, StructuredOutputMethodParams, ToolDefinition } from "@langchain/core/language_models/base";
6
+ import { BaseLanguageModelCallOptions, BaseLanguageModelInput, StructuredOutputMethodParams, StructuredOutputMethodOptions, ToolDefinition } from "@langchain/core/language_models/base";
7
7
  import { Runnable } from "@langchain/core/runnables";
8
+ import { z } from "zod";
8
9
  import { type AnthropicInput } from "../chat_models.js";
9
10
  export interface ChatAnthropicToolsCallOptions extends BaseLanguageModelCallOptions {
10
11
  tools?: ToolDefinition[];
@@ -43,8 +44,8 @@ export declare class ChatAnthropicTools extends BaseChatModel<ChatAnthropicTools
43
44
  }): Promise<ChatResult>;
44
45
  _generate(messages: BaseMessage[], options: this["ParsedCallOptions"], _runManager?: CallbackManagerForLLMRun | undefined): Promise<ChatResult>;
45
46
  _llmType(): string;
46
- withStructuredOutput<RunOutput extends Record<string, any> = Record<string, any>>({ schema, name, method, includeRaw, }: StructuredOutputMethodParams<RunOutput, false>): Runnable<BaseLanguageModelInput, RunOutput>;
47
- withStructuredOutput<RunOutput extends Record<string, any> = Record<string, any>>({ schema, name, method, includeRaw, }: StructuredOutputMethodParams<RunOutput, true>): Runnable<BaseLanguageModelInput, {
47
+ withStructuredOutput<RunOutput extends Record<string, any> = Record<string, any>>(outputSchema: StructuredOutputMethodParams<RunOutput, false> | z.ZodType<RunOutput> | Record<string, any>, config?: StructuredOutputMethodOptions<false>): Runnable<BaseLanguageModelInput, RunOutput>;
48
+ withStructuredOutput<RunOutput extends Record<string, any> = Record<string, any>>(outputSchema: StructuredOutputMethodParams<RunOutput, true> | z.ZodType<RunOutput> | Record<string, any>, config?: StructuredOutputMethodOptions<true>): Runnable<BaseLanguageModelInput, {
48
49
  raw: BaseMessage;
49
50
  parsed: RunOutput;
50
51
  }>;
@@ -5,7 +5,7 @@ import { RunnablePassthrough, RunnableSequence, } from "@langchain/core/runnable
5
5
  import { JsonOutputKeyToolsParser } from "@langchain/core/output_parsers/openai_tools";
6
6
  import { zodToJsonSchema } from "zod-to-json-schema";
7
7
  import { ChatAnthropic } from "../chat_models.js";
8
- import { DEFAULT_TOOL_SYSTEM_PROMPT, formatAsXMLRepresentation, } from "./utils/tool_calling.js";
8
+ import { DEFAULT_TOOL_SYSTEM_PROMPT, formatAsXMLRepresentation, fixArrayXMLParameters, } from "./utils/tool_calling.js";
9
9
  /**
10
10
  * Experimental wrapper over Anthropic chat models that adds support for
11
11
  * a function calling interface.
@@ -60,6 +60,7 @@ export class ChatAnthropicTools extends BaseChatModel {
60
60
  let promptMessages = messages;
61
61
  let forced = false;
62
62
  let toolCall;
63
+ const tools = options.tools === undefined ? [] : [...options.tools];
63
64
  if (options.tools !== undefined && options.tools.length > 0) {
64
65
  const content = await systemPromptTemplate.format({
65
66
  tools: `<tools>\n${options.tools
@@ -118,14 +119,20 @@ export class ChatAnthropicTools extends BaseChatModel {
118
119
  const responseMessageWithFunctions = new AIMessage({
119
120
  content: "",
120
121
  additional_kwargs: {
121
- tool_calls: invocations.map((toolInvocation, i) => ({
122
- id: i.toString(),
123
- type: "function",
124
- function: {
125
- name: toolInvocation.tool_name,
126
- arguments: JSON.stringify(toolInvocation.parameters),
127
- },
128
- })),
122
+ tool_calls: invocations.map((toolInvocation, i) => {
123
+ const calledTool = tools.find((tool) => tool.function.name === toolCall);
124
+ if (calledTool === undefined) {
125
+ throw new Error(`Called tool "${toolCall}" did not match an existing tool.`);
126
+ }
127
+ return {
128
+ id: i.toString(),
129
+ type: "function",
130
+ function: {
131
+ name: toolInvocation.tool_name,
132
+ arguments: JSON.stringify(fixArrayXMLParameters(calledTool.function.parameters, toolInvocation.parameters)),
133
+ },
134
+ };
135
+ }),
129
136
  },
130
137
  });
131
138
  return {
@@ -141,14 +148,20 @@ export class ChatAnthropicTools extends BaseChatModel {
141
148
  const responseMessageWithFunctions = new AIMessage({
142
149
  content: chatGenerationContent.split("<function_calls>")[0],
143
150
  additional_kwargs: {
144
- tool_calls: invocations.map((toolInvocation, i) => ({
145
- id: i.toString(),
146
- type: "function",
147
- function: {
148
- name: toolInvocation.tool_name,
149
- arguments: JSON.stringify(toolInvocation.parameters),
150
- },
151
- })),
151
+ tool_calls: invocations.map((toolInvocation, i) => {
152
+ const calledTool = tools.find((tool) => tool.function.name === toolInvocation.tool_name);
153
+ if (calledTool === undefined) {
154
+ throw new Error(`Called tool "${toolCall}" did not match an existing tool.`);
155
+ }
156
+ return {
157
+ id: i.toString(),
158
+ type: "function",
159
+ function: {
160
+ name: toolInvocation.tool_name,
161
+ arguments: JSON.stringify(fixArrayXMLParameters(calledTool.function.parameters, toolInvocation.parameters)),
162
+ },
163
+ };
164
+ }),
152
165
  },
153
166
  });
154
167
  return {
@@ -168,7 +181,24 @@ export class ChatAnthropicTools extends BaseChatModel {
168
181
  _llmType() {
169
182
  return "anthropic_tool_calling";
170
183
  }
171
- withStructuredOutput({ schema, name, method, includeRaw, }) {
184
+ withStructuredOutput(outputSchema, config) {
185
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ let schema;
187
+ let name;
188
+ let method;
189
+ let includeRaw;
190
+ if (isStructuredOutputMethodParams(outputSchema)) {
191
+ schema = outputSchema.schema;
192
+ name = outputSchema.name;
193
+ method = outputSchema.method;
194
+ includeRaw = outputSchema.includeRaw;
195
+ }
196
+ else {
197
+ schema = outputSchema;
198
+ name = config?.name;
199
+ method = config?.method;
200
+ includeRaw = config?.includeRaw;
201
+ }
172
202
  if (method === "jsonMode") {
173
203
  throw new Error(`Anthropic only supports "functionCalling" as a method.`);
174
204
  }
@@ -241,3 +271,11 @@ function isZodSchema(input) {
241
271
  // Check for a characteristic method of Zod schemas
242
272
  return typeof input?.parse === "function";
243
273
  }
274
+ function isStructuredOutputMethodParams(x
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
+ ) {
277
+ return (x !== undefined &&
278
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
+ typeof x.schema ===
280
+ "object");
281
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatAsXMLRepresentation = exports.DEFAULT_TOOL_SYSTEM_PROMPT = void 0;
3
+ exports.fixArrayXMLParameters = exports.formatAsXMLRepresentation = exports.DEFAULT_TOOL_SYSTEM_PROMPT = void 0;
4
4
  const fast_xml_parser_1 = require("fast-xml-parser");
5
5
  const prompts_1 = require("@langchain/core/prompts");
6
6
  exports.DEFAULT_TOOL_SYSTEM_PROMPT =
@@ -50,3 +50,38 @@ ${parameterXml}
50
50
  </tool_description>`;
51
51
  }
52
52
  exports.formatAsXMLRepresentation = formatAsXMLRepresentation;
53
+ function fixArrayXMLParameters(schema,
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ xmlParameters
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ ) {
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ const fixedParameters = {};
60
+ for (const key of Object.keys(xmlParameters)) {
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ const schemaType = schema.properties[key].type;
63
+ // Crawl for lists indistinguishable from single items
64
+ if (schema.properties && schema.properties[key] && schemaType === "array") {
65
+ fixedParameters[key] = Array.isArray(xmlParameters[key])
66
+ ? xmlParameters[key]
67
+ : [xmlParameters[key]];
68
+ // Crawl for objects like {"item": "my string"} that should really just be "my string"
69
+ if (schemaType !== "object" &&
70
+ typeof xmlParameters[key] === "object" &&
71
+ !Array.isArray(xmlParameters[key]) &&
72
+ Object.keys(xmlParameters[key]).length === 1) {
73
+ // eslint-disable-next-line prefer-destructuring
74
+ fixedParameters[key] = Object.values(xmlParameters[key])[0];
75
+ }
76
+ }
77
+ else if (typeof xmlParameters[key] === "object" &&
78
+ xmlParameters[key] !== null) {
79
+ fixedParameters[key] = fixArrayXMLParameters(schema, xmlParameters[key]);
80
+ }
81
+ else {
82
+ fixedParameters[key] = xmlParameters[key];
83
+ }
84
+ }
85
+ return fixedParameters;
86
+ }
87
+ exports.fixArrayXMLParameters = fixArrayXMLParameters;
@@ -1,3 +1,4 @@
1
+ import { JsonSchema7ObjectType } from "zod-to-json-schema";
1
2
  import { PromptTemplate } from "@langchain/core/prompts";
2
3
  import { ToolDefinition } from "@langchain/core/language_models/base";
3
4
  export declare const DEFAULT_TOOL_SYSTEM_PROMPT: PromptTemplate<import("@langchain/core/prompts").ParamsFromFString<"In this environment you have access to a set of tools you can use to answer the user's question.\n\nYou may call them like this:\n<function_calls>\n<invoke>\n<tool_name>$TOOL_NAME</tool_name>\n<parameters>\n<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>\n...\n</parameters>\n</invoke>\n</function_calls>\n\nHere are the tools available:\n{tools}">, any>;
@@ -6,3 +7,4 @@ export type ToolInvocation = {
6
7
  parameters: Record<string, unknown>;
7
8
  };
8
9
  export declare function formatAsXMLRepresentation(tool: ToolDefinition): string;
10
+ export declare function fixArrayXMLParameters(schema: JsonSchema7ObjectType, xmlParameters: Record<string, any>): Record<string, any>;
@@ -46,3 +46,37 @@ ${parameterXml}
46
46
  </parameters>
47
47
  </tool_description>`;
48
48
  }
49
+ export function fixArrayXMLParameters(schema,
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ xmlParameters
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ ) {
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ const fixedParameters = {};
56
+ for (const key of Object.keys(xmlParameters)) {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ const schemaType = schema.properties[key].type;
59
+ // Crawl for lists indistinguishable from single items
60
+ if (schema.properties && schema.properties[key] && schemaType === "array") {
61
+ fixedParameters[key] = Array.isArray(xmlParameters[key])
62
+ ? xmlParameters[key]
63
+ : [xmlParameters[key]];
64
+ // Crawl for objects like {"item": "my string"} that should really just be "my string"
65
+ if (schemaType !== "object" &&
66
+ typeof xmlParameters[key] === "object" &&
67
+ !Array.isArray(xmlParameters[key]) &&
68
+ Object.keys(xmlParameters[key]).length === 1) {
69
+ // eslint-disable-next-line prefer-destructuring
70
+ fixedParameters[key] = Object.values(xmlParameters[key])[0];
71
+ }
72
+ }
73
+ else if (typeof xmlParameters[key] === "object" &&
74
+ xmlParameters[key] !== null) {
75
+ fixedParameters[key] = fixArrayXMLParameters(schema, xmlParameters[key]);
76
+ }
77
+ else {
78
+ fixedParameters[key] = xmlParameters[key];
79
+ }
80
+ }
81
+ return fixedParameters;
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/anthropic",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Anthropic integrations for LangChain.js",
5
5
  "type": "module",
6
6
  "engines": {