@langchain/google-genai 0.2.9 → 0.2.10

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.
package/README.md CHANGED
@@ -50,6 +50,7 @@ Then initialize
50
50
 
51
51
  ```typescript
52
52
  import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
53
+ import { HumanMessage } from "@langchain/core/messages";
53
54
 
54
55
  const model = new ChatGoogleGenerativeAI({
55
56
  modelName: "gemini-pro",
@@ -27,13 +27,12 @@ const tools_js_1 = require("./utils/tools.cjs");
27
27
  * ## [Runtime args](https://api.js.langchain.com/interfaces/langchain_google_genai.GoogleGenerativeAIChatCallOptions.html)
28
28
  *
29
29
  * Runtime args can be passed as the second argument to any of the base runnable methods `.invoke`. `.stream`, `.batch`, etc.
30
- * They can also be passed via `.bind`, or the second arg in `.bindTools`, like shown in the examples below:
30
+ * They can also be passed via `.withConfig`, or the second arg in `.bindTools`, like shown in the examples below:
31
31
  *
32
32
  * ```typescript
33
- * // When calling `.bind`, call options should be passed via the first argument
34
- * const llmWithArgsBound = llm.bind({
33
+ * // When calling `.withConfig`, call options should be passed via the first argument
34
+ * const llmWithArgsBound = llm.withConfig({
35
35
  * stop: ["\n"],
36
- * tools: [...],
37
36
  * });
38
37
  *
39
38
  * // When calling `.bindTools`, call options should be passed via the second argument
@@ -598,7 +597,10 @@ class ChatGoogleGenerativeAI extends chat_models_1.BaseChatModel {
598
597
  return "googlegenerativeai";
599
598
  }
600
599
  bindTools(tools, kwargs) {
601
- return this.bind({ tools: (0, tools_js_1.convertToolsToGenAI)(tools)?.tools, ...kwargs });
600
+ return this.withConfig({
601
+ tools: (0, tools_js_1.convertToolsToGenAI)(tools)?.tools,
602
+ ...kwargs,
603
+ });
602
604
  }
603
605
  invocationParams(options) {
604
606
  const toolsAndConfig = options?.tools?.length
@@ -668,7 +670,10 @@ class ChatGoogleGenerativeAI extends chat_models_1.BaseChatModel {
668
670
  const generationResult = (0, common_js_1.mapGenerateContentResultToChatResult)(res.response, {
669
671
  usageMetadata,
670
672
  });
671
- await runManager?.handleLLMNewToken(generationResult.generations[0].text ?? "");
673
+ // may not have generations in output if there was a refusal for safety reasons
674
+ if (generationResult.generations?.length > 0) {
675
+ await runManager?.handleLLMNewToken(generationResult.generations[0]?.text ?? "");
676
+ }
672
677
  return generationResult;
673
678
  }
674
679
  async *_streamResponseChunks(messages, options, runManager) {
@@ -800,14 +805,13 @@ class ChatGoogleGenerativeAI extends chat_models_1.BaseChatModel {
800
805
  keyName: functionName,
801
806
  });
802
807
  }
803
- llm = this.bind({
804
- tools,
805
- tool_choice: functionName,
808
+ llm = this.bindTools(tools).withConfig({
809
+ allowedFunctionNames: [functionName],
806
810
  });
807
811
  }
808
812
  else {
809
813
  const jsonSchema = (0, zod_to_genai_parameters_js_1.schemaToGenerativeAIParameters)(schema);
810
- llm = this.bind({
814
+ llm = this.withConfig({
811
815
  responseSchema: jsonSchema,
812
816
  });
813
817
  outputParser = new output_parsers_1.JsonOutputParser();
@@ -71,7 +71,7 @@ export interface GoogleGenerativeAIChatInput extends BaseChatModelParams, Pick<G
71
71
  * Top-k changes how the model selects tokens for output.
72
72
  *
73
73
  * A top-k of 1 means the selected token is the most probable among
74
- * all tokens in the models vocabulary (also called greedy decoding),
74
+ * all tokens in the model's vocabulary (also called greedy decoding),
75
75
  * while a top-k of 3 means that the next token is selected from
76
76
  * among the 3 most probable tokens (using temperature).
77
77
  *
@@ -139,13 +139,12 @@ export interface GoogleGenerativeAIChatInput extends BaseChatModelParams, Pick<G
139
139
  * ## [Runtime args](https://api.js.langchain.com/interfaces/langchain_google_genai.GoogleGenerativeAIChatCallOptions.html)
140
140
  *
141
141
  * Runtime args can be passed as the second argument to any of the base runnable methods `.invoke`. `.stream`, `.batch`, etc.
142
- * They can also be passed via `.bind`, or the second arg in `.bindTools`, like shown in the examples below:
142
+ * They can also be passed via `.withConfig`, or the second arg in `.bindTools`, like shown in the examples below:
143
143
  *
144
144
  * ```typescript
145
- * // When calling `.bind`, call options should be passed via the first argument
146
- * const llmWithArgsBound = llm.bind({
145
+ * // When calling `.withConfig`, call options should be passed via the first argument
146
+ * const llmWithArgsBound = llm.withConfig({
147
147
  * stop: ["\n"],
148
- * tools: [...],
149
148
  * });
150
149
  *
151
150
  * // When calling `.bindTools`, call options should be passed via the second argument
@@ -24,13 +24,12 @@ import { convertToolsToGenAI } from "./utils/tools.js";
24
24
  * ## [Runtime args](https://api.js.langchain.com/interfaces/langchain_google_genai.GoogleGenerativeAIChatCallOptions.html)
25
25
  *
26
26
  * Runtime args can be passed as the second argument to any of the base runnable methods `.invoke`. `.stream`, `.batch`, etc.
27
- * They can also be passed via `.bind`, or the second arg in `.bindTools`, like shown in the examples below:
27
+ * They can also be passed via `.withConfig`, or the second arg in `.bindTools`, like shown in the examples below:
28
28
  *
29
29
  * ```typescript
30
- * // When calling `.bind`, call options should be passed via the first argument
31
- * const llmWithArgsBound = llm.bind({
30
+ * // When calling `.withConfig`, call options should be passed via the first argument
31
+ * const llmWithArgsBound = llm.withConfig({
32
32
  * stop: ["\n"],
33
- * tools: [...],
34
33
  * });
35
34
  *
36
35
  * // When calling `.bindTools`, call options should be passed via the second argument
@@ -595,7 +594,10 @@ export class ChatGoogleGenerativeAI extends BaseChatModel {
595
594
  return "googlegenerativeai";
596
595
  }
597
596
  bindTools(tools, kwargs) {
598
- return this.bind({ tools: convertToolsToGenAI(tools)?.tools, ...kwargs });
597
+ return this.withConfig({
598
+ tools: convertToolsToGenAI(tools)?.tools,
599
+ ...kwargs,
600
+ });
599
601
  }
600
602
  invocationParams(options) {
601
603
  const toolsAndConfig = options?.tools?.length
@@ -665,7 +667,10 @@ export class ChatGoogleGenerativeAI extends BaseChatModel {
665
667
  const generationResult = mapGenerateContentResultToChatResult(res.response, {
666
668
  usageMetadata,
667
669
  });
668
- await runManager?.handleLLMNewToken(generationResult.generations[0].text ?? "");
670
+ // may not have generations in output if there was a refusal for safety reasons
671
+ if (generationResult.generations?.length > 0) {
672
+ await runManager?.handleLLMNewToken(generationResult.generations[0]?.text ?? "");
673
+ }
669
674
  return generationResult;
670
675
  }
671
676
  async *_streamResponseChunks(messages, options, runManager) {
@@ -797,14 +802,13 @@ export class ChatGoogleGenerativeAI extends BaseChatModel {
797
802
  keyName: functionName,
798
803
  });
799
804
  }
800
- llm = this.bind({
801
- tools,
802
- tool_choice: functionName,
805
+ llm = this.bindTools(tools).withConfig({
806
+ allowedFunctionNames: [functionName],
803
807
  });
804
808
  }
805
809
  else {
806
810
  const jsonSchema = schemaToGenerativeAIParameters(schema);
807
- llm = this.bind({
811
+ llm = this.withConfig({
808
812
  responseSchema: jsonSchema,
809
813
  });
810
814
  outputParser = new JsonOutputParser();
@@ -78,6 +78,200 @@ function inferToolNameFromPreviousMessages(message, previousMessages) {
78
78
  return toolCall.id === message.tool_call_id;
79
79
  })?.name;
80
80
  }
81
+ function _getStandardContentBlockConverter(isMultimodalModel) {
82
+ const standardContentBlockConverter = {
83
+ providerName: "Google Gemini",
84
+ fromStandardTextBlock(block) {
85
+ return {
86
+ text: block.text,
87
+ };
88
+ },
89
+ fromStandardImageBlock(block) {
90
+ if (!isMultimodalModel) {
91
+ throw new Error("This model does not support images");
92
+ }
93
+ if (block.source_type === "url") {
94
+ const data = (0, messages_1.parseBase64DataUrl)({ dataUrl: block.url });
95
+ if (data) {
96
+ return {
97
+ inlineData: {
98
+ mimeType: data.mime_type,
99
+ data: data.data,
100
+ },
101
+ };
102
+ }
103
+ else {
104
+ return {
105
+ fileData: {
106
+ mimeType: block.mime_type ?? "",
107
+ fileUri: block.url,
108
+ },
109
+ };
110
+ }
111
+ }
112
+ if (block.source_type === "base64") {
113
+ return {
114
+ inlineData: {
115
+ mimeType: block.mime_type ?? "",
116
+ data: block.data,
117
+ },
118
+ };
119
+ }
120
+ throw new Error(`Unsupported source type: ${block.source_type}`);
121
+ },
122
+ fromStandardAudioBlock(block) {
123
+ if (!isMultimodalModel) {
124
+ throw new Error("This model does not support audio");
125
+ }
126
+ if (block.source_type === "url") {
127
+ const data = (0, messages_1.parseBase64DataUrl)({ dataUrl: block.url });
128
+ if (data) {
129
+ return {
130
+ inlineData: {
131
+ mimeType: data.mime_type,
132
+ data: data.data,
133
+ },
134
+ };
135
+ }
136
+ else {
137
+ return {
138
+ fileData: {
139
+ mimeType: block.mime_type ?? "",
140
+ fileUri: block.url,
141
+ },
142
+ };
143
+ }
144
+ }
145
+ if (block.source_type === "base64") {
146
+ return {
147
+ inlineData: {
148
+ mimeType: block.mime_type ?? "",
149
+ data: block.data,
150
+ },
151
+ };
152
+ }
153
+ throw new Error(`Unsupported source type: ${block.source_type}`);
154
+ },
155
+ fromStandardFileBlock(block) {
156
+ if (!isMultimodalModel) {
157
+ throw new Error("This model does not support files");
158
+ }
159
+ if (block.source_type === "text") {
160
+ return {
161
+ text: block.text,
162
+ };
163
+ }
164
+ if (block.source_type === "url") {
165
+ const data = (0, messages_1.parseBase64DataUrl)({ dataUrl: block.url });
166
+ if (data) {
167
+ return {
168
+ inlineData: {
169
+ mimeType: data.mime_type,
170
+ data: data.data,
171
+ },
172
+ };
173
+ }
174
+ else {
175
+ return {
176
+ fileData: {
177
+ mimeType: block.mime_type ?? "",
178
+ fileUri: block.url,
179
+ },
180
+ };
181
+ }
182
+ }
183
+ if (block.source_type === "base64") {
184
+ return {
185
+ inlineData: {
186
+ mimeType: block.mime_type ?? "",
187
+ data: block.data,
188
+ },
189
+ };
190
+ }
191
+ throw new Error(`Unsupported source type: ${block.source_type}`);
192
+ },
193
+ };
194
+ return standardContentBlockConverter;
195
+ }
196
+ function _convertLangChainContentToPart(content, isMultimodalModel) {
197
+ if ((0, messages_1.isDataContentBlock)(content)) {
198
+ return (0, messages_1.convertToProviderContentBlock)(content, _getStandardContentBlockConverter(isMultimodalModel));
199
+ }
200
+ if (content.type === "text") {
201
+ return { text: content.text };
202
+ }
203
+ else if (content.type === "executableCode") {
204
+ return { executableCode: content.executableCode };
205
+ }
206
+ else if (content.type === "codeExecutionResult") {
207
+ return { codeExecutionResult: content.codeExecutionResult };
208
+ }
209
+ else if (content.type === "image_url") {
210
+ if (!isMultimodalModel) {
211
+ throw new Error(`This model does not support images`);
212
+ }
213
+ let source;
214
+ if (typeof content.image_url === "string") {
215
+ source = content.image_url;
216
+ }
217
+ else if (typeof content.image_url === "object" &&
218
+ "url" in content.image_url) {
219
+ source = content.image_url.url;
220
+ }
221
+ else {
222
+ throw new Error("Please provide image as base64 encoded data URL");
223
+ }
224
+ const [dm, data] = source.split(",");
225
+ if (!dm.startsWith("data:")) {
226
+ throw new Error("Please provide image as base64 encoded data URL");
227
+ }
228
+ const [mimeType, encoding] = dm.replace(/^data:/, "").split(";");
229
+ if (encoding !== "base64") {
230
+ throw new Error("Please provide image as base64 encoded data URL");
231
+ }
232
+ return {
233
+ inlineData: {
234
+ data,
235
+ mimeType,
236
+ },
237
+ };
238
+ }
239
+ else if (content.type === "media") {
240
+ return messageContentMedia(content);
241
+ }
242
+ else if (content.type === "tool_use") {
243
+ return {
244
+ functionCall: {
245
+ name: content.name,
246
+ args: content.input,
247
+ },
248
+ };
249
+ }
250
+ else if (content.type?.includes("/") &&
251
+ // Ensure it's a single slash.
252
+ content.type.split("/").length === 2 &&
253
+ "data" in content &&
254
+ typeof content.data === "string") {
255
+ return {
256
+ inlineData: {
257
+ mimeType: content.type,
258
+ data: content.data,
259
+ },
260
+ };
261
+ }
262
+ else if ("functionCall" in content) {
263
+ // No action needed here — function calls will be added later from message.tool_calls
264
+ return undefined;
265
+ }
266
+ else {
267
+ if ("type" in content) {
268
+ throw new Error(`Unknown content type ${content.type}`);
269
+ }
270
+ else {
271
+ throw new Error(`Unknown content ${JSON.stringify(content)}`);
272
+ }
273
+ }
274
+ }
81
275
  function convertMessageContentToParts(message, isMultimodalModel, previousMessages) {
82
276
  if ((0, messages_1.isToolMessage)(message)) {
83
277
  const messageName = message.name ??
@@ -85,13 +279,29 @@ function convertMessageContentToParts(message, isMultimodalModel, previousMessag
85
279
  if (messageName === undefined) {
86
280
  throw new Error(`Google requires a tool name for each tool call response, and we could not infer a called tool name for ToolMessage "${message.id}" from your passed messages. Please populate a "name" field on that ToolMessage explicitly.`);
87
281
  }
282
+ const result = Array.isArray(message.content)
283
+ ? message.content
284
+ .map((c) => _convertLangChainContentToPart(c, isMultimodalModel))
285
+ .filter((p) => p !== undefined)
286
+ : message.content;
287
+ if (message.status === "error") {
288
+ return [
289
+ {
290
+ functionResponse: {
291
+ name: messageName,
292
+ // The API expects an object with an `error` field if the function call fails.
293
+ // `error` must be a valid object (not a string or array), so we wrap `message.content` here
294
+ response: { error: { details: result } },
295
+ },
296
+ },
297
+ ];
298
+ }
88
299
  return [
89
300
  {
90
301
  functionResponse: {
91
302
  name: messageName,
92
- response: typeof message.content === "string"
93
- ? { result: message.content }
94
- : message.content,
303
+ // again, can't have a string or array value for `response`, so we wrap it as an object here
304
+ response: { result },
95
305
  },
96
306
  },
97
307
  ];
@@ -102,80 +312,9 @@ function convertMessageContentToParts(message, isMultimodalModel, previousMessag
102
312
  messageParts.push({ text: message.content });
103
313
  }
104
314
  if (Array.isArray(message.content)) {
105
- message.content.forEach((c) => {
106
- if (c.type === "text") {
107
- messageParts.push({ text: c.text });
108
- }
109
- else if (c.type === "executableCode") {
110
- messageParts.push({ executableCode: c.executableCode });
111
- }
112
- else if (c.type === "codeExecutionResult") {
113
- messageParts.push({ codeExecutionResult: c.codeExecutionResult });
114
- }
115
- else if (c.type === "image_url") {
116
- if (!isMultimodalModel) {
117
- throw new Error(`This model does not support images`);
118
- }
119
- let source;
120
- if (typeof c.image_url === "string") {
121
- source = c.image_url;
122
- }
123
- else if (typeof c.image_url === "object" && "url" in c.image_url) {
124
- source = c.image_url.url;
125
- }
126
- else {
127
- throw new Error("Please provide image as base64 encoded data URL");
128
- }
129
- const [dm, data] = source.split(",");
130
- if (!dm.startsWith("data:")) {
131
- throw new Error("Please provide image as base64 encoded data URL");
132
- }
133
- const [mimeType, encoding] = dm.replace(/^data:/, "").split(";");
134
- if (encoding !== "base64") {
135
- throw new Error("Please provide image as base64 encoded data URL");
136
- }
137
- messageParts.push({
138
- inlineData: {
139
- data,
140
- mimeType,
141
- },
142
- });
143
- }
144
- else if (c.type === "media") {
145
- messageParts.push(messageContentMedia(c));
146
- }
147
- else if (c.type === "tool_use") {
148
- functionCalls.push({
149
- functionCall: {
150
- name: c.name,
151
- args: c.input,
152
- },
153
- });
154
- }
155
- else if (c.type?.includes("/") &&
156
- // Ensure it's a single slash.
157
- c.type.split("/").length === 2 &&
158
- "data" in c &&
159
- typeof c.data === "string") {
160
- messageParts.push({
161
- inlineData: {
162
- mimeType: c.type,
163
- data: c.data,
164
- },
165
- });
166
- }
167
- else if ("functionCall" in c) {
168
- // No action needed here — function calls will be added later from message.tool_calls
169
- }
170
- else {
171
- if ("type" in c) {
172
- throw new Error(`Unknown content type ${c.type}`);
173
- }
174
- else {
175
- throw new Error(`Unknown content ${JSON.stringify(c)}`);
176
- }
177
- }
178
- });
315
+ messageParts.push(...message.content
316
+ .map((c) => _convertLangChainContentToPart(c, isMultimodalModel))
317
+ .filter((p) => p !== undefined));
179
318
  }
180
319
  if ((0, messages_1.isAIMessage)(message) && message.tool_calls?.length) {
181
320
  functionCalls = message.tool_calls.map((tc) => {
@@ -1,4 +1,4 @@
1
- import { AIMessage, AIMessageChunk, ChatMessage, isAIMessage, isBaseMessage, isToolMessage, } from "@langchain/core/messages";
1
+ import { AIMessage, AIMessageChunk, ChatMessage, isAIMessage, isBaseMessage, isToolMessage, parseBase64DataUrl, convertToProviderContentBlock, isDataContentBlock, } from "@langchain/core/messages";
2
2
  import { ChatGenerationChunk, } from "@langchain/core/outputs";
3
3
  import { isLangChainTool } from "@langchain/core/utils/function_calling";
4
4
  import { isOpenAITool } from "@langchain/core/language_models/base";
@@ -73,6 +73,200 @@ function inferToolNameFromPreviousMessages(message, previousMessages) {
73
73
  return toolCall.id === message.tool_call_id;
74
74
  })?.name;
75
75
  }
76
+ function _getStandardContentBlockConverter(isMultimodalModel) {
77
+ const standardContentBlockConverter = {
78
+ providerName: "Google Gemini",
79
+ fromStandardTextBlock(block) {
80
+ return {
81
+ text: block.text,
82
+ };
83
+ },
84
+ fromStandardImageBlock(block) {
85
+ if (!isMultimodalModel) {
86
+ throw new Error("This model does not support images");
87
+ }
88
+ if (block.source_type === "url") {
89
+ const data = parseBase64DataUrl({ dataUrl: block.url });
90
+ if (data) {
91
+ return {
92
+ inlineData: {
93
+ mimeType: data.mime_type,
94
+ data: data.data,
95
+ },
96
+ };
97
+ }
98
+ else {
99
+ return {
100
+ fileData: {
101
+ mimeType: block.mime_type ?? "",
102
+ fileUri: block.url,
103
+ },
104
+ };
105
+ }
106
+ }
107
+ if (block.source_type === "base64") {
108
+ return {
109
+ inlineData: {
110
+ mimeType: block.mime_type ?? "",
111
+ data: block.data,
112
+ },
113
+ };
114
+ }
115
+ throw new Error(`Unsupported source type: ${block.source_type}`);
116
+ },
117
+ fromStandardAudioBlock(block) {
118
+ if (!isMultimodalModel) {
119
+ throw new Error("This model does not support audio");
120
+ }
121
+ if (block.source_type === "url") {
122
+ const data = parseBase64DataUrl({ dataUrl: block.url });
123
+ if (data) {
124
+ return {
125
+ inlineData: {
126
+ mimeType: data.mime_type,
127
+ data: data.data,
128
+ },
129
+ };
130
+ }
131
+ else {
132
+ return {
133
+ fileData: {
134
+ mimeType: block.mime_type ?? "",
135
+ fileUri: block.url,
136
+ },
137
+ };
138
+ }
139
+ }
140
+ if (block.source_type === "base64") {
141
+ return {
142
+ inlineData: {
143
+ mimeType: block.mime_type ?? "",
144
+ data: block.data,
145
+ },
146
+ };
147
+ }
148
+ throw new Error(`Unsupported source type: ${block.source_type}`);
149
+ },
150
+ fromStandardFileBlock(block) {
151
+ if (!isMultimodalModel) {
152
+ throw new Error("This model does not support files");
153
+ }
154
+ if (block.source_type === "text") {
155
+ return {
156
+ text: block.text,
157
+ };
158
+ }
159
+ if (block.source_type === "url") {
160
+ const data = parseBase64DataUrl({ dataUrl: block.url });
161
+ if (data) {
162
+ return {
163
+ inlineData: {
164
+ mimeType: data.mime_type,
165
+ data: data.data,
166
+ },
167
+ };
168
+ }
169
+ else {
170
+ return {
171
+ fileData: {
172
+ mimeType: block.mime_type ?? "",
173
+ fileUri: block.url,
174
+ },
175
+ };
176
+ }
177
+ }
178
+ if (block.source_type === "base64") {
179
+ return {
180
+ inlineData: {
181
+ mimeType: block.mime_type ?? "",
182
+ data: block.data,
183
+ },
184
+ };
185
+ }
186
+ throw new Error(`Unsupported source type: ${block.source_type}`);
187
+ },
188
+ };
189
+ return standardContentBlockConverter;
190
+ }
191
+ function _convertLangChainContentToPart(content, isMultimodalModel) {
192
+ if (isDataContentBlock(content)) {
193
+ return convertToProviderContentBlock(content, _getStandardContentBlockConverter(isMultimodalModel));
194
+ }
195
+ if (content.type === "text") {
196
+ return { text: content.text };
197
+ }
198
+ else if (content.type === "executableCode") {
199
+ return { executableCode: content.executableCode };
200
+ }
201
+ else if (content.type === "codeExecutionResult") {
202
+ return { codeExecutionResult: content.codeExecutionResult };
203
+ }
204
+ else if (content.type === "image_url") {
205
+ if (!isMultimodalModel) {
206
+ throw new Error(`This model does not support images`);
207
+ }
208
+ let source;
209
+ if (typeof content.image_url === "string") {
210
+ source = content.image_url;
211
+ }
212
+ else if (typeof content.image_url === "object" &&
213
+ "url" in content.image_url) {
214
+ source = content.image_url.url;
215
+ }
216
+ else {
217
+ throw new Error("Please provide image as base64 encoded data URL");
218
+ }
219
+ const [dm, data] = source.split(",");
220
+ if (!dm.startsWith("data:")) {
221
+ throw new Error("Please provide image as base64 encoded data URL");
222
+ }
223
+ const [mimeType, encoding] = dm.replace(/^data:/, "").split(";");
224
+ if (encoding !== "base64") {
225
+ throw new Error("Please provide image as base64 encoded data URL");
226
+ }
227
+ return {
228
+ inlineData: {
229
+ data,
230
+ mimeType,
231
+ },
232
+ };
233
+ }
234
+ else if (content.type === "media") {
235
+ return messageContentMedia(content);
236
+ }
237
+ else if (content.type === "tool_use") {
238
+ return {
239
+ functionCall: {
240
+ name: content.name,
241
+ args: content.input,
242
+ },
243
+ };
244
+ }
245
+ else if (content.type?.includes("/") &&
246
+ // Ensure it's a single slash.
247
+ content.type.split("/").length === 2 &&
248
+ "data" in content &&
249
+ typeof content.data === "string") {
250
+ return {
251
+ inlineData: {
252
+ mimeType: content.type,
253
+ data: content.data,
254
+ },
255
+ };
256
+ }
257
+ else if ("functionCall" in content) {
258
+ // No action needed here — function calls will be added later from message.tool_calls
259
+ return undefined;
260
+ }
261
+ else {
262
+ if ("type" in content) {
263
+ throw new Error(`Unknown content type ${content.type}`);
264
+ }
265
+ else {
266
+ throw new Error(`Unknown content ${JSON.stringify(content)}`);
267
+ }
268
+ }
269
+ }
76
270
  export function convertMessageContentToParts(message, isMultimodalModel, previousMessages) {
77
271
  if (isToolMessage(message)) {
78
272
  const messageName = message.name ??
@@ -80,13 +274,29 @@ export function convertMessageContentToParts(message, isMultimodalModel, previou
80
274
  if (messageName === undefined) {
81
275
  throw new Error(`Google requires a tool name for each tool call response, and we could not infer a called tool name for ToolMessage "${message.id}" from your passed messages. Please populate a "name" field on that ToolMessage explicitly.`);
82
276
  }
277
+ const result = Array.isArray(message.content)
278
+ ? message.content
279
+ .map((c) => _convertLangChainContentToPart(c, isMultimodalModel))
280
+ .filter((p) => p !== undefined)
281
+ : message.content;
282
+ if (message.status === "error") {
283
+ return [
284
+ {
285
+ functionResponse: {
286
+ name: messageName,
287
+ // The API expects an object with an `error` field if the function call fails.
288
+ // `error` must be a valid object (not a string or array), so we wrap `message.content` here
289
+ response: { error: { details: result } },
290
+ },
291
+ },
292
+ ];
293
+ }
83
294
  return [
84
295
  {
85
296
  functionResponse: {
86
297
  name: messageName,
87
- response: typeof message.content === "string"
88
- ? { result: message.content }
89
- : message.content,
298
+ // again, can't have a string or array value for `response`, so we wrap it as an object here
299
+ response: { result },
90
300
  },
91
301
  },
92
302
  ];
@@ -97,80 +307,9 @@ export function convertMessageContentToParts(message, isMultimodalModel, previou
97
307
  messageParts.push({ text: message.content });
98
308
  }
99
309
  if (Array.isArray(message.content)) {
100
- message.content.forEach((c) => {
101
- if (c.type === "text") {
102
- messageParts.push({ text: c.text });
103
- }
104
- else if (c.type === "executableCode") {
105
- messageParts.push({ executableCode: c.executableCode });
106
- }
107
- else if (c.type === "codeExecutionResult") {
108
- messageParts.push({ codeExecutionResult: c.codeExecutionResult });
109
- }
110
- else if (c.type === "image_url") {
111
- if (!isMultimodalModel) {
112
- throw new Error(`This model does not support images`);
113
- }
114
- let source;
115
- if (typeof c.image_url === "string") {
116
- source = c.image_url;
117
- }
118
- else if (typeof c.image_url === "object" && "url" in c.image_url) {
119
- source = c.image_url.url;
120
- }
121
- else {
122
- throw new Error("Please provide image as base64 encoded data URL");
123
- }
124
- const [dm, data] = source.split(",");
125
- if (!dm.startsWith("data:")) {
126
- throw new Error("Please provide image as base64 encoded data URL");
127
- }
128
- const [mimeType, encoding] = dm.replace(/^data:/, "").split(";");
129
- if (encoding !== "base64") {
130
- throw new Error("Please provide image as base64 encoded data URL");
131
- }
132
- messageParts.push({
133
- inlineData: {
134
- data,
135
- mimeType,
136
- },
137
- });
138
- }
139
- else if (c.type === "media") {
140
- messageParts.push(messageContentMedia(c));
141
- }
142
- else if (c.type === "tool_use") {
143
- functionCalls.push({
144
- functionCall: {
145
- name: c.name,
146
- args: c.input,
147
- },
148
- });
149
- }
150
- else if (c.type?.includes("/") &&
151
- // Ensure it's a single slash.
152
- c.type.split("/").length === 2 &&
153
- "data" in c &&
154
- typeof c.data === "string") {
155
- messageParts.push({
156
- inlineData: {
157
- mimeType: c.type,
158
- data: c.data,
159
- },
160
- });
161
- }
162
- else if ("functionCall" in c) {
163
- // No action needed here — function calls will be added later from message.tool_calls
164
- }
165
- else {
166
- if ("type" in c) {
167
- throw new Error(`Unknown content type ${c.type}`);
168
- }
169
- else {
170
- throw new Error(`Unknown content ${JSON.stringify(c)}`);
171
- }
172
- }
173
- });
310
+ messageParts.push(...message.content
311
+ .map((c) => _convertLangChainContentToPart(c, isMultimodalModel))
312
+ .filter((p) => p !== undefined));
174
313
  }
175
314
  if (isAIMessage(message) && message.tool_calls?.length) {
176
315
  functionCalls = message.tool_calls.map((tc) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/google-genai",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Google Generative AI integration for LangChain.js",
5
5
  "type": "module",
6
6
  "engines": {
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@jest/globals": "^29.5.0",
47
- "@langchain/core": "0.3.55",
47
+ "@langchain/core": "0.3.57",
48
48
  "@langchain/scripts": ">=0.1.0 <0.2.0",
49
49
  "@langchain/standard-tests": "0.0.0",
50
50
  "@swc/core": "^1.3.90",