Skip to content

Commit 5fa232b

Browse files
committed
fix(google-common): pass structured objects directly in FunctionResponse
Parse JSON string content and pass the resulting object directly as FunctionResponse.response instead of wrapping in { content: ... }. Plain strings and arrays are wrapped in { result: ... }.
1 parent 49d121c commit 5fa232b

File tree

3 files changed

+108
-28
lines changed

3 files changed

+108
-28
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@langchain/google-common": patch
3+
---
4+
5+
Pass structured JSON objects directly in `FunctionResponse.response` instead of wrapping them in `{ content: ... }`, matching the Gemini API's expected format.

libs/providers/langchain-google-common/src/tests/utils.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,3 +1092,81 @@ describe("gemini empty text content handling", () => {
10921092
expect(textPart.text).toBe("I am doing well");
10931093
});
10941094
});
1095+
1096+
describe("gemini tool message formatting", () => {
1097+
test("passes JSON object content directly in FunctionResponse", async () => {
1098+
const api = getGeminiAPI();
1099+
const messages = [
1100+
new HumanMessage("What is the weather?"),
1101+
new AIMessage({
1102+
content: "",
1103+
tool_calls: [
1104+
{ id: "call_1", name: "get_weather", args: { city: "SF" } },
1105+
],
1106+
}),
1107+
new ToolMessage({
1108+
tool_call_id: "call_1",
1109+
content: JSON.stringify({ temperature: 72, unit: "F" }),
1110+
}),
1111+
];
1112+
1113+
const formatted = (await api.formatData(messages, {})) as GeminiRequest;
1114+
const functionContent = formatted.contents?.find(
1115+
(c) => c.role === "function"
1116+
);
1117+
const part = functionContent?.parts[0] as any;
1118+
expect(part.functionResponse.response.temperature).toBe(72);
1119+
expect(part.functionResponse.response.unit).toBe("F");
1120+
});
1121+
1122+
test("wraps plain string content in result field", async () => {
1123+
const api = getGeminiAPI();
1124+
const messages = [
1125+
new HumanMessage("What is the weather?"),
1126+
new AIMessage({
1127+
content: "",
1128+
tool_calls: [
1129+
{ id: "call_1", name: "get_weather", args: { city: "SF" } },
1130+
],
1131+
}),
1132+
new ToolMessage({
1133+
tool_call_id: "call_1",
1134+
content: "sunny and 72°F",
1135+
}),
1136+
];
1137+
1138+
const formatted = (await api.formatData(messages, {})) as GeminiRequest;
1139+
const functionContent = formatted.contents?.find(
1140+
(c) => c.role === "function"
1141+
);
1142+
const part = functionContent?.parts[0] as any;
1143+
expect(part.functionResponse.response).toEqual({ result: "sunny and 72°F" });
1144+
});
1145+
1146+
test("wraps JSON array content in result field", async () => {
1147+
const api = getGeminiAPI();
1148+
const messages = [
1149+
new HumanMessage("Search"),
1150+
new AIMessage({
1151+
content: "",
1152+
tool_calls: [
1153+
{ id: "call_1", name: "search", args: { q: "restaurants" } },
1154+
],
1155+
}),
1156+
new ToolMessage({
1157+
tool_call_id: "call_1",
1158+
content: JSON.stringify([{ name: "A" }, { name: "B" }]),
1159+
}),
1160+
];
1161+
1162+
const formatted = (await api.formatData(messages, {})) as GeminiRequest;
1163+
const functionContent = formatted.contents?.find(
1164+
(c) => c.role === "function"
1165+
);
1166+
const part = functionContent?.parts[0] as any;
1167+
expect(part.functionResponse.response.result).toEqual([
1168+
{ name: "A" },
1169+
{ name: "B" },
1170+
]);
1171+
});
1172+
});

libs/providers/langchain-google-common/src/utils/gemini.ts

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -893,41 +893,38 @@ export function getGeminiAPI(config?: GeminiAPIConfig): GoogleAIAPI {
893893
},
894894
""
895895
);
896-
// Hacky :(
897896
const responseName =
898897
(isAIMessage(prevMessage) && !!prevMessage.tool_calls?.length
899898
? prevMessage.tool_calls[0].name
900899
: prevMessage.name) ?? message.tool_call_id;
900+
901+
// Gemini accepts any JSON object as FunctionResponse.response.
902+
// Parse JSON strings to pass structured data directly instead of
903+
// wrapping in { content: stringified }.
904+
let response: Record<string, unknown>;
901905
try {
902-
const content = JSON.parse(contentStr);
903-
return [
904-
{
905-
role: "function",
906-
parts: [
907-
{
908-
functionResponse: {
909-
name: responseName,
910-
response: { content },
911-
},
912-
},
913-
],
914-
},
915-
];
906+
const parsed = JSON.parse(contentStr);
907+
response =
908+
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
909+
? parsed
910+
: { result: parsed };
916911
} catch (_) {
917-
return [
918-
{
919-
role: "function",
920-
parts: [
921-
{
922-
functionResponse: {
923-
name: responseName,
924-
response: { content: contentStr },
925-
},
926-
},
927-
],
928-
},
929-
];
912+
response = { result: contentStr };
930913
}
914+
915+
return [
916+
{
917+
role: "function",
918+
parts: [
919+
{
920+
functionResponse: {
921+
name: responseName,
922+
response,
923+
},
924+
},
925+
],
926+
},
927+
];
931928
}
932929

933930
async function baseMessageToContent(

0 commit comments

Comments
 (0)