diff --git a/.changeset/nervous-clocks-invite.md b/.changeset/nervous-clocks-invite.md new file mode 100644 index 000000000000..b4cbc504a948 --- /dev/null +++ b/.changeset/nervous-clocks-invite.md @@ -0,0 +1,5 @@ +--- +"@langchain/core": patch +--- + +feat: replace btoa with toBase64Url for encoding in drawMermaidImage diff --git a/libs/langchain-core/src/runnables/graph_mermaid.ts b/libs/langchain-core/src/runnables/graph_mermaid.ts index bf909f600ce3..42bedd7336bd 100644 --- a/libs/langchain-core/src/runnables/graph_mermaid.ts +++ b/libs/langchain-core/src/runnables/graph_mermaid.ts @@ -1,4 +1,5 @@ import { Edge, Node } from "./types.js"; +import { toBase64Url } from "./utils.js"; function _escapeNodeLabel(nodeLabel: string): string { // Escapes the node label for Mermaid syntax. @@ -201,8 +202,8 @@ export async function drawMermaidImage( let backgroundColor = config?.backgroundColor ?? "white"; const imageType = config?.imageType ?? "png"; - // Use btoa for compatibility, assume ASCII - const mermaidSyntaxEncoded = btoa(mermaidSyntax); + const mermaidSyntaxEncoded = toBase64Url(mermaidSyntax); + // Check if the background color is a hexadecimal color code using regex if (backgroundColor !== undefined) { const hexColorPattern = /^#(?:[0-9a-fA-F]{3}){1,2}$/; diff --git a/libs/langchain-core/src/runnables/tests/graph_mermaid.test.ts b/libs/langchain-core/src/runnables/tests/graph_mermaid.test.ts index 43742293426e..3daec17f2ba3 100644 --- a/libs/langchain-core/src/runnables/tests/graph_mermaid.test.ts +++ b/libs/langchain-core/src/runnables/tests/graph_mermaid.test.ts @@ -8,6 +8,7 @@ import { type MockedFunction, } from "vitest"; import { drawMermaidImage } from "../graph_mermaid.js"; +import { toBase64Url } from "../utils.js"; // Mock global fetch const mockFetch = vi.fn() as MockedFunction; @@ -36,7 +37,7 @@ describe("drawMermaidImage", () => { expect(mockFetch).toHaveBeenCalledTimes(1); // Check the URL construction - const expectedEncodedSyntax = btoa(mermaidSyntax); + const expectedEncodedSyntax = toBase64Url(mermaidSyntax); const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=png`; expect(mockFetch).toHaveBeenCalledWith(expectedUrl); }); @@ -54,7 +55,7 @@ describe("drawMermaidImage", () => { }); expect(result).toBe(mockBlob); - const expectedEncodedSyntax = btoa(mermaidSyntax); + const expectedEncodedSyntax = toBase64Url(mermaidSyntax); const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=jpeg`; expect(mockFetch).toHaveBeenCalledWith(expectedUrl); }); @@ -72,7 +73,7 @@ describe("drawMermaidImage", () => { }); expect(result).toBe(mockBlob); - const expectedEncodedSyntax = btoa(mermaidSyntax); + const expectedEncodedSyntax = toBase64Url(mermaidSyntax); const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=webp`; expect(mockFetch).toHaveBeenCalledWith(expectedUrl); }); @@ -90,7 +91,7 @@ describe("drawMermaidImage", () => { }); expect(result).toBe(mockBlob); - const expectedEncodedSyntax = btoa(mermaidSyntax); + const expectedEncodedSyntax = toBase64Url(mermaidSyntax); const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=#FF5733&type=png`; expect(mockFetch).toHaveBeenCalledWith(expectedUrl); }); @@ -108,7 +109,7 @@ describe("drawMermaidImage", () => { }); expect(result).toBe(mockBlob); - const expectedEncodedSyntax = btoa(mermaidSyntax); + const expectedEncodedSyntax = toBase64Url(mermaidSyntax); const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=#FFF&type=png`; expect(mockFetch).toHaveBeenCalledWith(expectedUrl); }); @@ -126,7 +127,7 @@ describe("drawMermaidImage", () => { }); expect(result).toBe(mockBlob); - const expectedEncodedSyntax = btoa(mermaidSyntax); + const expectedEncodedSyntax = toBase64Url(mermaidSyntax); const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!transparent&type=png`; expect(mockFetch).toHaveBeenCalledWith(expectedUrl); }); @@ -176,7 +177,7 @@ describe("drawMermaidImage", () => { const result = await drawMermaidImage(complexSyntax); expect(result).toBe(mockBlob); - const expectedEncodedSyntax = btoa(complexSyntax); + const expectedEncodedSyntax = toBase64Url(complexSyntax); const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=png`; expect(mockFetch).toHaveBeenCalledWith(expectedUrl); }); @@ -194,7 +195,7 @@ describe("drawMermaidImage", () => { }); expect(result).toBe(mockBlob); - const expectedEncodedSyntax = btoa(mermaidSyntax); + const expectedEncodedSyntax = toBase64Url(mermaidSyntax); const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=png`; expect(mockFetch).toHaveBeenCalledWith(expectedUrl); }); diff --git a/libs/langchain-core/src/runnables/utils.ts b/libs/langchain-core/src/runnables/utils.ts index 2193fff6694d..a7fbc737f98b 100644 --- a/libs/langchain-core/src/runnables/utils.ts +++ b/libs/langchain-core/src/runnables/utils.ts @@ -74,3 +74,9 @@ export class _RootEventFilter { return include; } } + +export const toBase64Url = (str: string): string => { + // Use btoa for compatibility, assume ASCII + const encoded = btoa(str); + return encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +};