Skip to content

Commit 9c966b3

Browse files
committed
Support role name for function response in Vertex
1 parent 5887443 commit 9c966b3

File tree

3 files changed

+60
-55
lines changed

3 files changed

+60
-55
lines changed

.changeset/sweet-forks-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@langchain/google": patch
3+
---
4+
5+
support role name for function response in Vertex

libs/providers/langchain-google/src/converters/messages.ts

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ export const geminiContentBlockConverter: StandardContentBlockConverter<{
5656
typeof block.data === "string"
5757
? block.data
5858
: typeof block.data === "object" && block.data !== null
59-
? // Convert Uint8Array to base64 string
60-
btoa(
61-
Array.from(block.data as Uint8Array)
62-
.map((byte) => String.fromCharCode(byte))
63-
.join("")
64-
)
65-
: String(block.data);
59+
? // Convert Uint8Array to base64 string
60+
btoa(
61+
Array.from(block.data as Uint8Array)
62+
.map((byte) => String.fromCharCode(byte))
63+
.join("")
64+
)
65+
: String(block.data);
6666
return {
6767
inlineData: {
6868
mimeType: block.mime_type,
@@ -129,13 +129,13 @@ export const geminiContentBlockConverter: StandardContentBlockConverter<{
129129
typeof block.data === "string"
130130
? block.data
131131
: typeof block.data === "object" && block.data !== null
132-
? // Convert Uint8Array to base64 string
133-
btoa(
134-
Array.from(block.data as Uint8Array)
135-
.map((byte) => String.fromCharCode(byte))
136-
.join("")
137-
)
138-
: String(block.data);
132+
? // Convert Uint8Array to base64 string
133+
btoa(
134+
Array.from(block.data as Uint8Array)
135+
.map((byte) => String.fromCharCode(byte))
136+
.join("")
137+
)
138+
: String(block.data);
139139
return {
140140
inlineData: {
141141
mimeType: block.mime_type,
@@ -202,13 +202,13 @@ export const geminiContentBlockConverter: StandardContentBlockConverter<{
202202
typeof block.data === "string"
203203
? block.data
204204
: typeof block.data === "object" && block.data !== null
205-
? // Convert Uint8Array to base64 string
206-
btoa(
207-
Array.from(block.data as Uint8Array)
208-
.map((byte) => String.fromCharCode(byte))
209-
.join("")
210-
)
211-
: String(block.data);
205+
? // Convert Uint8Array to base64 string
206+
btoa(
207+
Array.from(block.data as Uint8Array)
208+
.map((byte) => String.fromCharCode(byte))
209+
.join("")
210+
)
211+
: String(block.data);
212212
return {
213213
inlineData: {
214214
mimeType: block.mime_type,
@@ -456,7 +456,6 @@ function convertStandardContentMessageToGeminiContent(
456456
typeof message.content === "string"
457457
? message.content
458458
: JSON.stringify(message.content);
459-
<<<<<<< HEAD
460459
// Find the matching tool call in a preceding AIMessage to get the function name
461460
const aiMsg = messages
462461
.filter(AIMessage.isInstance)
@@ -466,18 +465,11 @@ function convertStandardContentMessageToGeminiContent(
466465
const matchedToolCall = aiMsg?.tool_calls?.find(
467466
(tc) => tc.id === message.tool_call_id
468467
);
469-
=======
470-
// FIXME: ToolMessage almost never has a name, we need to refer to the message history
471-
>>>>>>> 968b70618 (Have generated IDs use a known pattern so they can be removed when being sent back to Gemini, which is not expecting them.)
472468
const isGeneratedId = message.tool_call_id.startsWith("lc-tool-call-");
473469
parts.push({
474470
functionResponse: {
475471
...(isGeneratedId ? {} : { id: message.tool_call_id }),
476-
<<<<<<< HEAD
477472
name: matchedToolCall?.name ?? message.name ?? "unknown",
478-
=======
479-
name: message.name || "unknown",
480-
>>>>>>> 968b70618 (Have generated IDs use a known pattern so they can be removed when being sent back to Gemini, which is not expecting them.)
481473
response: { result: responseContent },
482474
},
483475
});
@@ -772,23 +764,20 @@ function convertLegacyContentMessageToGeminiContent(
772764
throw new ToolCallNotFoundError(message.tool_call_id);
773765
}
774766
const isGeneratedId = message.tool_call_id.startsWith("lc-tool-call-");
775-
<<<<<<< HEAD
776767
const matchedToolCall = aiMsg.tool_calls?.find(
777768
(tc) => tc.id === message.tool_call_id
778769
);
779770
parts.push({
780771
functionResponse: {
781772
...(isGeneratedId ? {} : { id: message.tool_call_id }),
782773
name: matchedToolCall?.name ?? message.name ?? "unknown",
783-
=======
784-
parts.push({
785-
functionResponse: {
786-
...(isGeneratedId ? {} : { id: message.tool_call_id }),
787-
name: toolCall?.name || "unknown",
788-
>>>>>>> 968b70618 (Have generated IDs use a known pattern so they can be removed when being sent back to Gemini, which is not expecting them.)
789774
response: { result: responseContent },
790775
},
791776
});
777+
778+
// For tool messages, only keep functionResponse parts since the text content
779+
// is already included in the functionResponse.response.result
780+
parts = parts.filter((part) => "functionResponse" in part);
792781
}
793782

794783
// Only add content if we have parts
@@ -990,13 +979,9 @@ export const convertGeminiPartsToToolCalls: Converter<
990979
const functionCallPart = part as Gemini.Part.FunctionCall;
991980
toolCalls.push({
992981
type: "tool_call",
993-
<<<<<<< HEAD
994982
id:
995983
functionCallPart.functionCall.id ??
996984
`lc-tool-call-${uuidv4().replace(/-/g, "")}`,
997-
=======
998-
id: functionCallPart.functionCall.id ?? `lc-tool-call-${uuidv4().replace(/-/g, "")}`,
999-
>>>>>>> 968b70618 (Have generated IDs use a known pattern so they can be removed when being sent back to Gemini, which is not expecting them.)
1000985
name: functionCallPart.functionCall.name,
1001986
args: functionCallPart.functionCall.args ?? {},
1002987
thoughtSignature: functionCallPart.thoughtSignature,

libs/providers/langchain-google/src/converters/tests/messages.test.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ describe("convertMessagesToGeminiContents", () => {
133133

134134
const contents = convertMessagesToGeminiContents(messages);
135135

136-
const toolResponseContent = contents.find((c) => c.role === "function");
136+
const toolResponseContent = contents.find(
137+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
138+
);
137139
expect(toolResponseContent).toBeDefined();
138140

139141
const functionResponsePart = toolResponseContent!.parts!.find(
@@ -168,7 +170,9 @@ describe("convertMessagesToGeminiContents", () => {
168170

169171
const contents = convertMessagesToGeminiContents(messages);
170172

171-
const toolResponseContent = contents.find((c) => c.role === "function");
173+
const toolResponseContent = contents.find(
174+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
175+
);
172176
expect(toolResponseContent).toBeDefined();
173177

174178
const functionResponsePart = toolResponseContent!.parts.find(
@@ -213,7 +217,9 @@ describe("convertMessagesToGeminiContents", () => {
213217

214218
const contents = convertMessagesToGeminiContents(messages);
215219

216-
const toolResponseContents = contents.filter((c) => c.role === "function");
220+
const toolResponseContents = contents.filter(
221+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
222+
);
217223
expect(toolResponseContents).toHaveLength(1);
218224

219225
const parts = toolResponseContents[0].parts.filter(
@@ -260,13 +266,13 @@ describe("convertMessagesToGeminiContents", () => {
260266

261267
const contents = convertMessagesToGeminiContents(messages);
262268

263-
// Should produce: user, model (functionCall parts), function (single merged turn)
269+
// Should produce: user, model (functionCall parts), user (single merged turn with functionResponses)
264270
expect(contents).toHaveLength(3);
265271

266272
expect(contents[1].role).toBe("model");
267273

268274
const functionTurn = contents[2];
269-
expect(functionTurn.role).toBe("function");
275+
expect(functionTurn.role).toBe("user");
270276
expect(functionTurn.parts).toHaveLength(2);
271277

272278
const responses = functionTurn.parts.filter(
@@ -301,7 +307,9 @@ describe("convertMessagesToGeminiContents", () => {
301307

302308
const contents = convertMessagesToGeminiContents(messages);
303309

304-
const toolResponseContent = contents.find((c) => c.role === "function");
310+
const toolResponseContent = contents.find(
311+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
312+
);
305313
const functionResponsePart = toolResponseContent!.parts.find(
306314
(p) => "functionResponse" in p && p.functionResponse
307315
) as Gemini.Part.FunctionResponse;
@@ -402,7 +410,9 @@ describe("convertMessagesToGeminiContents", () => {
402410

403411
const contents = convertMessagesToGeminiContents(messages);
404412

405-
const toolResponseContent = contents.find((c) => c.role === "function");
413+
const toolResponseContent = contents.find(
414+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
415+
);
406416
expect(toolResponseContent).toBeDefined();
407417

408418
const functionResponsePart = toolResponseContent!.parts.find(
@@ -448,8 +458,10 @@ describe("convertMessagesToGeminiContents", () => {
448458

449459
const contents = convertMessagesToGeminiContents(messages);
450460

451-
// Consecutive ToolMessages with the same "function" role are merged into one content
452-
const toolResponseContents = contents.filter((c) => c.role === "function");
461+
// Consecutive ToolMessages with the same "user" role are merged into one content
462+
const toolResponseContents = contents.filter(
463+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
464+
);
453465
expect(toolResponseContents).toHaveLength(1);
454466

455467
const mergedParts = toolResponseContents[0].parts.filter(
@@ -484,7 +496,9 @@ describe("convertMessagesToGeminiContents", () => {
484496

485497
const contents = convertMessagesToGeminiContents(messages);
486498

487-
const toolResponseContent = contents.find((c) => c.role === "function");
499+
const toolResponseContent = contents.find(
500+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
501+
);
488502
expect(toolResponseContent).toBeDefined();
489503

490504
const functionResponsePart = toolResponseContent!.parts!.find(
@@ -520,7 +534,9 @@ describe("convertMessagesToGeminiContents", () => {
520534

521535
const contents = convertMessagesToGeminiContents(messages);
522536

523-
const toolResponseContent = contents.find((c) => c.role === "function");
537+
const toolResponseContent = contents.find(
538+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
539+
);
524540
expect(toolResponseContent).toBeDefined();
525541

526542
const functionResponsePart = toolResponseContent!.parts!.find(
@@ -557,7 +573,9 @@ describe("convertMessagesToGeminiContents", () => {
557573

558574
const contents = convertMessagesToGeminiContents(messages);
559575

560-
const toolResponseContent = contents.find((c) => c.role === "function");
576+
const toolResponseContent = contents.find(
577+
(c) => c.role === "user" && c.parts.some((p) => "functionResponse" in p)
578+
);
561579
expect(toolResponseContent).toBeDefined();
562580

563581
const functionResponsePart = toolResponseContent!.parts!.find(
@@ -569,7 +587,6 @@ describe("convertMessagesToGeminiContents", () => {
569587
.id
570588
).toBeUndefined();
571589
});
572-
<<<<<<< HEAD
573590

574591
test("v1 contentBlocks: text-plain block produces fileData part", () => {
575592
const messages = [
@@ -723,6 +740,4 @@ describe("convertMessagesToGeminiContents", () => {
723740
(userContent!.parts[3] as Gemini.Part.FileData).fileData!.fileUri
724741
).toBe("gs://bucket/report.pdf");
725742
});
726-
=======
727-
>>>>>>> 968b70618 (Have generated IDs use a known pattern so they can be removed when being sent back to Gemini, which is not expecting them.)
728743
});

0 commit comments

Comments
 (0)