Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/honest-hats-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/core": patch
---

omit tool call chunks without tool call id
16 changes: 8 additions & 8 deletions langchain-core/src/messages/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,26 +275,26 @@ export class AIMessageChunk extends BaseMessageChunk {
(acc, chunk) => {
// Assign a fallback ID if the chunk doesn't have one
// This can happen with tools that have empty schemas
const chunkId = chunk.id || `fallback-${chunk.index || 0}`;
acc[chunkId] = acc[chunkId] ?? [];
acc[chunkId].push(chunk);
const groupId = chunk.id || `fallback-${chunk.index || 0}`;
acc[groupId] = acc[groupId] ?? [];
acc[groupId].push(chunk);
return acc;
},
{} as Record<string, ToolCallChunk[]>
);

const toolCalls: ToolCall[] = [];
const invalidToolCalls: InvalidToolCall[] = [];
for (const [id, chunks] of Object.entries(groupedToolCallChunk)) {
for (const chunks of Object.values(groupedToolCallChunk)) {
let parsedArgs = {};
const name = chunks[0]?.name ?? "";
const joinedArgs = chunks.map((c) => c.args || "").join("");
const argsStr = joinedArgs.length ? joinedArgs : "{}";
// Use the original ID from the first chunk if it exists, otherwise use the grouped ID
const originalId = chunks[0]?.id || id;
const id = chunks[0]?.id;
try {
parsedArgs = parsePartialJson(argsStr);
if (
!id ||
parsedArgs === null ||
typeof parsedArgs !== "object" ||
Array.isArray(parsedArgs)
Expand All @@ -304,14 +304,14 @@ export class AIMessageChunk extends BaseMessageChunk {
toolCalls.push({
name,
args: parsedArgs,
id: originalId,
id,
type: "tool_call",
});
} catch (e) {
invalidToolCalls.push({
name,
args: argsStr,
id: originalId,
id,
error: "Malformed args.",
type: "invalid_tool_call",
});
Expand Down
185 changes: 145 additions & 40 deletions langchain-core/src/messages/tests/base_message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,56 +467,161 @@ describe("Complex AIMessageChunk concat", () => {
});

it("concatenates tool call chunks without IDs", () => {
const chunks: ToolCallChunk[] = [
{
name: "get_current_time",
type: "tool_call_chunk",
index: 0,
// no `id` provided
},
const chunks = [
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
name: "get_weather",
args: "",
id: "call_q6ZzjkLjKNYb4DizyMOaqpfW",
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: '{"',
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: "location",
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: '":"',
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: "San",
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: " Francisco",
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: '"}',
index: 0,
type: "tool_call_chunk",
},
],
}),
];

const result = new AIMessageChunk({
content: "",
tool_call_chunks: chunks,
});

expect(result.tool_calls?.length).toBe(1);
expect(result.invalid_tool_calls?.length).toBe(0);
expect(result.tool_calls).toEqual([
let finalChunk = new AIMessageChunk("");
for (const chunk of chunks) {
finalChunk = finalChunk.concat(chunk);
}
expect(finalChunk.tool_calls).toHaveLength(1);
expect(finalChunk.tool_calls).toEqual([
{
id: "fallback-0", // Should get fallback ID
name: "get_current_time",
args: {},
type: "tool_call",
name: "get_weather",
args: {
location: "San Francisco",
},
id: "call_q6ZzjkLjKNYb4DizyMOaqpfW",
},
]);
});
});

it("concatenates tool call chunks without IDs and no index", () => {
const chunks: ToolCallChunk[] = [
{
name: "get_current_time",
type: "tool_call_chunk",
// no `id` or `index` provided
},
];
describe("AIMessageChunk", () => {
describe("constructor", () => {
it("omits tool call chunks without IDs", () => {
const chunks: ToolCallChunk[] = [
{
name: "get_current_time",
type: "tool_call_chunk",
index: 0,
// no `id` provided
},
];

const result = new AIMessageChunk({
content: "",
tool_call_chunks: chunks,
const result = new AIMessageChunk({
content: "",
tool_call_chunks: chunks,
});

expect(result.tool_calls?.length).toBe(0);
expect(result.invalid_tool_calls?.length).toBe(1);
expect(result.invalid_tool_calls).toEqual([
{
type: "invalid_tool_call",
id: undefined,
name: "get_current_time",
args: "{}",
error: "Malformed args.",
},
]);
});

expect(result.tool_calls?.length).toBe(1);
expect(result.invalid_tool_calls?.length).toBe(0);
expect(result.tool_calls).toEqual([
{
id: "fallback-0", // Should get fallback ID with index 0
name: "get_current_time",
args: {},
type: "tool_call",
},
]);
it("omits tool call chunks without IDs and no index", () => {
const chunks: ToolCallChunk[] = [
{
name: "get_current_time",
type: "tool_call_chunk",
// no `id` or `index` provided
},
];

const result = new AIMessageChunk({
content: "",
tool_call_chunks: chunks,
});

expect(result.tool_calls?.length).toBe(0);
expect(result.invalid_tool_calls?.length).toBe(1);
expect(result.invalid_tool_calls).toEqual([
{
type: "invalid_tool_call",
id: undefined,
name: "get_current_time",
args: "{}",
error: "Malformed args.",
},
]);
});
});
});

Expand Down
Loading