Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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/metal-hats-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/core": patch
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think according to semver this would be minor, because it is adding something to public api? Admittedly it is a small thing we're adding; not sure how strictly we're adhering to semver

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think you're the first of us to ask that Q post v1! Will confer and let you know

---

Add `BaseMessage.toFormattedString()`
8 changes: 8 additions & 0 deletions libs/langchain-core/src/messages/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
isMessage,
Message,
} from "./message.js";
import {
convertToFormattedString,
type MessageStringFormat,
} from "./format.js";

/** @internal */
const MESSAGE_SYMBOL = Symbol.for("langchain.message");
Expand Down Expand Up @@ -378,6 +382,10 @@ export abstract class BaseMessage<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return `${(this.constructor as any).lc_name()} ${printable}`;
}

toFormattedString(format: MessageStringFormat = "pretty"): string {
return convertToFormattedString(this, format);
}
}

/**
Expand Down
56 changes: 56 additions & 0 deletions libs/langchain-core/src/messages/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { type BaseMessage } from "./base.js";
import { type AIMessage } from "./ai.js";
import { type ToolMessage } from "./tool.js";

export type MessageStringFormat = "pretty";

export function convertToFormattedString(
message: BaseMessage,
format: MessageStringFormat = "pretty"
): string {
if (format === "pretty") return convertToPrettyString(message);
return JSON.stringify(message);
}

function convertToPrettyString(message: BaseMessage): string {
const lines: string[] = [];
const title = ` ${
message.type.charAt(0).toUpperCase() + message.type.slice(1)
} Message `;
const sepLen = Math.floor((80 - title.length) / 2);
const sep = "=".repeat(sepLen);
const secondSep = title.length % 2 === 0 ? sep : `${sep}=`;
lines.push(`${sep}${title}${secondSep}`);

// Add message type specific details
if (message.type === "ai") {
const aiMessage = message as AIMessage;
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
lines.push("Tool Calls:");
for (const tc of aiMessage.tool_calls) {
lines.push(` ${tc.name} (${tc.id})`);
lines.push(` Call ID: ${tc.id}`);
lines.push(" Args:");
for (const [key, value] of Object.entries(tc.args)) {
lines.push(` ${key}: ${value}`);
}
}
}
}
if (message.type === "tool") {
const toolMessage = message as ToolMessage;
if (toolMessage.name) {
lines.push(`Name: ${toolMessage.name}`);
}
}

// Add content if it's a string and not empty
if (typeof message.content === "string" && message.content.trim()) {
if (lines.length > 1) {
lines.push(""); // blank line before content
}
lines.push(message.content);
}

return lines.join("\n");
}
164 changes: 164 additions & 0 deletions libs/langchain-core/src/messages/tests/base_message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,167 @@ describe("usage_metadata serialized", () => {
expect(jsonConcatenatedAIMessageChunk).toContain("total_tokens");
});
});

describe("toFormattedString", () => {
describe("BaseMessage (HumanMessage)", () => {
it("formats a simple string message", () => {
const message = new HumanMessage("Hello, world!");
const output = message.toFormattedString();
expect(output).toContain("Human Message");
expect(output).toContain("Hello, world!");
expect(output).toMatch(/={30,}/); // Check for separator line
});

it("formats a message with empty content", () => {
const message = new HumanMessage("");
const output = message.toFormattedString();
expect(output).toContain("Human Message");
expect(output).not.toContain("\n\n"); // No blank line before content
});

it("formats a message with whitespace-only content", () => {
const message = new HumanMessage(" ");
const output = message.toFormattedString();
expect(output).toContain("Human Message");
// Whitespace-only content should be treated as empty
expect(output.split("\n").length).toBe(1);
});
});

describe("AIMessage", () => {
it("formats an AI message without tool calls", () => {
const message = new AIMessage("I can help with that!");
const output = message.toFormattedString();
expect(output).toContain("Ai Message");
expect(output).toContain("I can help with that!");
});

it("formats an AI message with tool calls", () => {
const message = new AIMessage({
content: "Let me check the weather",
tool_calls: [
{
id: "call_123",
name: "get_weather",
args: { location: "San Francisco", unit: "celsius" },
type: "tool_call",
},
],
});
const output = message.toFormattedString();
expect(output).toContain("Ai Message");
expect(output).toContain("Tool Calls:");
expect(output).toContain("get_weather (call_123)");
expect(output).toContain("Call ID: call_123");
expect(output).toContain("Args:");
expect(output).toContain("location: San Francisco");
expect(output).toContain("unit: celsius");
});

it("formats an AI message with multiple tool calls", () => {
const message = new AIMessage({
content: "",
tool_calls: [
{
id: "call_1",
name: "search",
args: { query: "test" },
type: "tool_call",
},
{
id: "call_2",
name: "calculator",
args: { expression: "2+2" },
type: "tool_call",
},
],
});
const output = message.toFormattedString();
expect(output).toContain("search (call_1)");
expect(output).toContain("calculator (call_2)");
});

it("formats an AI message with empty tool calls array", () => {
const message = new AIMessage({
content: "Just a message",
tool_calls: [],
});
const output = message.toFormattedString();
expect(output).toContain("Ai Message");
expect(output).not.toContain("Tool Calls:");
expect(output).toContain("Just a message");
});
});

describe("ToolMessage", () => {
it("formats a tool message with name", () => {
const message = new ToolMessage({
content: '{"temperature": 72}',
tool_call_id: "call_123",
name: "get_weather",
});
const output = message.toFormattedString();
expect(output).toContain("Tool Message");
expect(output).toContain("Name: get_weather");
expect(output).toContain('{"temperature": 72}');
});

it("formats a tool message without name", () => {
const message = new ToolMessage({
content: "Success",
tool_call_id: "call_456",
});
const output = message.toFormattedString();
expect(output).toContain("Tool Message");
expect(output).not.toContain("Name:");
expect(output).toContain("Success");
});
});

describe("SystemMessage", () => {
it("formats a system message", () => {
const message = new SystemMessage("You are a helpful assistant.");
const output = message.toFormattedString();
expect(output).toContain("System Message");
expect(output).toContain("You are a helpful assistant.");
});
});

describe("Message formatting consistency", () => {
it("maintains consistent separator length for different message types", () => {
const human = new HumanMessage("Hi");
const ai = new AIMessage("Hello");
const system = new SystemMessage("System");

const humanOutput = human.toFormattedString();
const aiOutput = ai.toFormattedString();
const systemOutput = system.toFormattedString();

const humanSep = humanOutput.split("\n")[0];
const aiSep = aiOutput.split("\n")[0];
const systemSep = systemOutput.split("\n")[0];

expect(humanSep.length).toBe(80);
expect(aiSep.length).toBe(80);
expect(systemSep.length).toBe(80);
});

it("adds blank line before content when details are present", () => {
const messageWithDetails = new AIMessage({
content: "Response",
tool_calls: [
{
id: "call_1",
name: "tool",
args: {},
type: "tool_call",
},
],
});
const output = messageWithDetails.toFormattedString();
const lines = output.split("\n");
// Should have: title, Tool Calls:, tool info, blank line, content
expect(lines).toContain("");
});
});
});