From 4e60489f5099bf91bf6888a981eb4e9af23e8822 Mon Sep 17 00:00:00 2001 From: Milana Gurbanova Date: Mon, 14 Sep 2026 21:25:05 +0200 Subject: [PATCH] fix(langchain): serialize ToolMessage/FunctionMessage with correct role and tool_call_id --- packages/langchain/src/CallbackHandler.ts | 10 +++- .../integration/langchain.integration.test.ts | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/langchain/src/CallbackHandler.ts b/packages/langchain/src/CallbackHandler.ts index 6c0ffa1d..75186651 100644 --- a/packages/langchain/src/CallbackHandler.ts +++ b/packages/langchain/src/CallbackHandler.ts @@ -9,6 +9,7 @@ import { type UsageMetadata, type BaseMessageFields, type MessageContent, + type ToolMessage, } from "@langchain/core/messages"; import type { Generation, LLMResult } from "@langchain/core/outputs"; import type { ChainValues } from "@langchain/core/utils/types"; @@ -34,8 +35,10 @@ type LangfusePrompt = { export type LlmMessage = { role: string; + name?: string; content: BaseMessageFields["content"]; additional_kwargs?: BaseMessageFields["additional_kwargs"]; + tool_call_id?: string; }; export type AnonymousLlmMessage = { @@ -967,13 +970,16 @@ export class CallbackHandler extends BaseCallbackHandler { response = { content: message.content, additional_kwargs: message.additional_kwargs, - role: message.name, + role: "function", + name: message.name, }; } else if (message.getType() === "tool") { response = { content: message.content, additional_kwargs: message.additional_kwargs, - role: message.name, + role: "tool", + name: message.name, + tool_call_id: (message as ToolMessage).tool_call_id, }; } else if (!message.name) { response = { content: message.content }; diff --git a/tests/integration/langchain.integration.test.ts b/tests/integration/langchain.integration.test.ts index d92683db..2f427a82 100644 --- a/tests/integration/langchain.integration.test.ts +++ b/tests/integration/langchain.integration.test.ts @@ -1,3 +1,9 @@ +import { + AIMessage, + FunctionMessage, + HumanMessage, + ToolMessage, +} from "@langchain/core/messages"; import { DynamicTool } from "@langchain/core/tools"; import { CallbackHandler } from "@langfuse/langchain"; import { LangfuseOtelSpanAttributes } from "@langfuse/tracing"; @@ -138,4 +144,51 @@ describe("LangChain callback handler integration tests", () => { '[{"role":"user","content":"hi"}]', ); }); + + it("should serialize tool and function messages with role, name and tool_call_id", async () => { + const handler = new CallbackHandler(); + const runId = "generation-with-tool-results"; + + await handler.handleChatModelStart( + { id: ["ChatOpenAI"] }, + [ + [ + new HumanMessage("What is my updated debt?"), + new AIMessage({ + content: "", + tool_calls: [{ id: "call_1", name: "get_debt", args: {} }], + }), + new ToolMessage({ content: "1874.32", tool_call_id: "call_1" }), + new ToolMessage({ + content: "1874.32", + tool_call_id: "call_2", + name: "get_debt", + }), + new FunctionMessage({ content: "1874.32", name: "get_debt" }), + ], + ], + runId, + undefined, + { invocation_params: { model: "gpt-4.1-mini" } }, + ); + await handler.handleLLMEnd({ generations: [[{ text: "ok" }]] }, runId); + + await waitForSpanExport(testEnv.mockExporter, 1); + + assertions.expectSpanAttributeContains( + "ChatOpenAI", + LangfuseOtelSpanAttributes.OBSERVATION_INPUT, + '{"content":"1874.32","additional_kwargs":{},"role":"tool","tool_call_id":"call_1"}', + ); + assertions.expectSpanAttributeContains( + "ChatOpenAI", + LangfuseOtelSpanAttributes.OBSERVATION_INPUT, + '{"content":"1874.32","additional_kwargs":{},"role":"tool","name":"get_debt","tool_call_id":"call_2"}', + ); + assertions.expectSpanAttributeContains( + "ChatOpenAI", + LangfuseOtelSpanAttributes.OBSERVATION_INPUT, + '{"content":"1874.32","additional_kwargs":{},"role":"function","name":"get_debt"}', + ); + }); });