@langchain/anthropic 0.1.2 → 0.1.3

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({
@@ -202,3 +203,66 @@ test("Test ChatAnthropic withStructuredOutput", async () => {
202
203
  console.log(JSON.stringify(res, null, 2));
203
204
  expect(res).toEqual({ name: "Alex", height: 5, hairColor: "blonde" });
204
205
  });
206
+ test("Test ChatAnthropic withStructuredOutput on a single array item", async () => {
207
+ const runnable = new ChatAnthropicTools({
208
+ modelName: "claude-3-sonnet-20240229",
209
+ maxRetries: 0,
210
+ }).withStructuredOutput({
211
+ schema: z.object({
212
+ people: z.array(z.object({
213
+ name: z.string().describe("The name of a person"),
214
+ height: z.number().describe("The person's height"),
215
+ hairColor: z.optional(z.string()).describe("The person's hair color"),
216
+ })),
217
+ }),
218
+ });
219
+ const message = new HumanMessage("Alex is 5 feet tall. Alex is blonde.");
220
+ const res = await runnable.invoke([message]);
221
+ console.log(JSON.stringify(res, null, 2));
222
+ expect(res).toEqual({
223
+ people: [{ hairColor: "blonde", height: 5, name: "Alex" }],
224
+ });
225
+ });
226
+ test("Test ChatAnthropic withStructuredOutput on a single array item", async () => {
227
+ const runnable = new ChatAnthropicTools({
228
+ modelName: "claude-3-sonnet-20240229",
229
+ maxRetries: 0,
230
+ }).withStructuredOutput({
231
+ schema: z.object({
232
+ sender: z
233
+ .optional(z.string())
234
+ .describe("The sender's name, if available"),
235
+ sender_phone_number: z
236
+ .optional(z.string())
237
+ .describe("The sender's phone number, if available"),
238
+ sender_address: z
239
+ .optional(z.string())
240
+ .describe("The sender's address, if available"),
241
+ action_items: z
242
+ .array(z.string())
243
+ .describe("A list of action items requested by the email"),
244
+ topic: z
245
+ .string()
246
+ .describe("High level description of what the email is about"),
247
+ tone: z.enum(["positive", "negative"]).describe("The tone of the email."),
248
+ }),
249
+ name: "Email",
250
+ });
251
+ const prompt = ChatPromptTemplate.fromMessages([
252
+ [
253
+ "human",
254
+ "What can you tell me about the following email? Make sure to answer in the correct format: {email}",
255
+ ],
256
+ ]);
257
+ const extractionChain = prompt.pipe(runnable);
258
+ const response = await extractionChain.invoke({
259
+ 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.",
260
+ });
261
+ console.log(JSON.stringify(response, null, 2));
262
+ expect(response).toEqual({
263
+ sender: "Erick",
264
+ action_items: [expect.any(String), expect.any(String)],
265
+ topic: expect.any(String),
266
+ tone: "positive",
267
+ });
268
+ });
@@ -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 {
@@ -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 {
@@ -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.3",
4
4
  "description": "Anthropic integrations for LangChain.js",
5
5
  "type": "module",
6
6
  "engines": {