diff --git a/packages/client/src/prompt/promptClients.ts b/packages/client/src/prompt/promptClients.ts index b6d08a49..5e250f9d 100644 --- a/packages/client/src/prompt/promptClients.ts +++ b/packages/client/src/prompt/promptClients.ts @@ -263,6 +263,8 @@ export class TextPromptClient extends BasePromptClient { * @public */ export class ChatPromptClient extends BasePromptClient { + private static readonly MAX_CACHE_SIZE = 500; + private static readonly mustacheTokenCache = new Map(); /** The original prompt response from the API */ public readonly promptResponse: Prompt.Chat; /** The chat messages that make up the prompt */ @@ -328,13 +330,68 @@ export class ChatPromptClient extends BasePromptClient { variables?: Record, placeholders?: Record, ): (ChatMessageOrPlaceholder | any)[] { - const messagesWithPlaceholdersReplaced: (ChatMessageOrPlaceholder | any)[] = - []; - const placeholderValues = placeholders ?? {}; + const vars = variables ?? {}; + const phs = placeholders ?? {}; + + // 4. ZERO-ALLOCATION CHECK: Skip entirely if no injections needed + if (Object.keys(vars).length === 0 && Object.keys(phs).length === 0) { + return this.prompt.map((item) => { + if ("type" in item && item.type === ChatMessageType.ChatMessage) { + return { role: item.role, content: item.content }; + } + return item; + }); + } + + const result: (ChatMessageOrPlaceholder | any)[] = []; + const writer = new mustache.Writer(); for (const item of this.prompt) { - if ("type" in item && item.type === ChatMessageType.Placeholder) { - const placeholderValue = placeholderValues[item.name]; + if (item.type === ChatMessageType.ChatMessage) { + // 3. REFINED TYPE-GUARD: Check content type as the very first operation + if (typeof item.content !== "string") { + result.push({ + role: item.role, + content: item.content, + }); + continue; + } + + // 2. TOKEN CACHING: Use static Map to bypass expensive parsing (Memory-Safe) + let tokens = ChatPromptClient.mustacheTokenCache.get(item.content); + if (!tokens) { + if ( + ChatPromptClient.mustacheTokenCache.size >= + ChatPromptClient.MAX_CACHE_SIZE + ) { + // FIFO Eviction: Eject the oldest entry + const oldestKey = ChatPromptClient.mustacheTokenCache + .keys() + .next().value; + if (oldestKey !== undefined) { + ChatPromptClient.mustacheTokenCache.delete(oldestKey); + } + } + tokens = mustache.parse(item.content); + ChatPromptClient.mustacheTokenCache.set(item.content, tokens); + } + + const rendered = writer.renderTokens( + tokens, + new mustache.Context(vars), + undefined, + item.content, + ); + result.push({ + role: item.role, + content: rendered, + }); + continue; + } + + // Handle Placeholders (preserved logic) + if (item.type === ChatMessageType.Placeholder) { + const placeholderValue = phs[item.name]; if ( Array.isArray(placeholderValue) && placeholderValue.length > 0 && @@ -343,54 +400,27 @@ export class ChatPromptClient extends BasePromptClient { typeof msg === "object" && "role" in msg && "content" in msg, ) ) { - messagesWithPlaceholdersReplaced.push( - ...(placeholderValue as ChatMessage[]), - ); + result.push(...(placeholderValue as ChatMessage[])); } else if ( Array.isArray(placeholderValue) && placeholderValue.length === 0 ) { - // Empty array provided - skip placeholder (don't include it) + // Skip empty placeholder array } else if (placeholderValue !== undefined) { - // Non-standard placeholder value format, just stringfiy - messagesWithPlaceholdersReplaced.push( - JSON.stringify(placeholderValue), - ); + // Stringify non-standard formats + result.push(JSON.stringify(placeholderValue)); } else { - // Keep unresolved placeholder in the output - messagesWithPlaceholdersReplaced.push( - item as { type: ChatMessageType.Placeholder } & typeof item, - ); + // Keep unresolved placeholder + result.push(item); } - } else if ( - "role" in item && - "content" in item && - item.type === ChatMessageType.ChatMessage - ) { - messagesWithPlaceholdersReplaced.push({ - role: item.role, - content: item.content, - }); + continue; } + + // Catch-all for any other message types + result.push(item); } - return messagesWithPlaceholdersReplaced.map((item) => { - if ( - typeof item === "object" && - item !== null && - "role" in item && - "content" in item && - typeof item.content === 'string' - ) { - return { - ...item, - content: mustache.render(item.content, variables ?? {}), - }; - } else { - // Return placeholder or stringified value as-is - return item; - } - }); + return result; } /** @@ -438,7 +468,10 @@ export class ChatPromptClient extends BasePromptClient { ...(placeholderValue as ChatMessage[]).map((msg) => { return { role: msg.role, - content: this._transformToLangchainVariables(msg.content), + content: + typeof msg.content === "string" + ? this._transformToLangchainVariables(msg.content) + : msg.content, }; }), ); @@ -469,7 +502,10 @@ export class ChatPromptClient extends BasePromptClient { ) { messagesWithPlaceholdersReplaced.push({ role: item.role, - content: this._transformToLangchainVariables(item.content), + content: + typeof item.content === "string" + ? this._transformToLangchainVariables(item.content) + : item.content, }); } } diff --git a/packages/client/tsup.config.bundled_nv8l3rehalb.mjs b/packages/client/tsup.config.bundled_nv8l3rehalb.mjs new file mode 100644 index 00000000..095398ea --- /dev/null +++ b/packages/client/tsup.config.bundled_nv8l3rehalb.mjs @@ -0,0 +1,18 @@ +// tsup.config.ts +import { defineConfig } from "tsup"; +var tsup_config_default = defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + outDir: "dist", + outExtension: ({ format }) => ({ + js: format === "cjs" ? ".cjs" : ".mjs" + }) +}); +export { + tsup_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidHN1cC5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9faW5qZWN0ZWRfZmlsZW5hbWVfXyA9IFwiQzpcXFxcVXNlcnNcXFxcUFJBVkVFTiBSQUlcXFxcbGFuZ2Z1c2UtanNcXFxccGFja2FnZXNcXFxcY2xpZW50XFxcXHRzdXAuY29uZmlnLnRzXCI7Y29uc3QgX19pbmplY3RlZF9kaXJuYW1lX18gPSBcIkM6XFxcXFVzZXJzXFxcXFBSQVZFRU4gUkFJXFxcXGxhbmdmdXNlLWpzXFxcXHBhY2thZ2VzXFxcXGNsaWVudFwiO2NvbnN0IF9faW5qZWN0ZWRfaW1wb3J0X21ldGFfdXJsX18gPSBcImZpbGU6Ly8vQzovVXNlcnMvUFJBVkVFTiUyMFJBSS9sYW5nZnVzZS1qcy9wYWNrYWdlcy9jbGllbnQvdHN1cC5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidHN1cFwiO1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnRyeTogW1wic3JjL2luZGV4LnRzXCJdLFxuICBmb3JtYXQ6IFtcImNqc1wiLCBcImVzbVwiXSxcbiAgZHRzOiB0cnVlLFxuICBzcGxpdHRpbmc6IGZhbHNlLFxuICBzb3VyY2VtYXA6IHRydWUsXG4gIGNsZWFuOiB0cnVlLFxuICBvdXREaXI6IFwiZGlzdFwiLFxuICBvdXRFeHRlbnNpb246ICh7IGZvcm1hdCB9KSA9PiAoe1xuICAgIGpzOiBmb3JtYXQgPT09IFwiY2pzXCIgPyBcIi5janNcIiA6IFwiLm1qc1wiLFxuICB9KSxcbn0pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE0UyxTQUFTLG9CQUFvQjtBQUV6VSxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixPQUFPLENBQUMsY0FBYztBQUFBLEVBQ3RCLFFBQVEsQ0FBQyxPQUFPLEtBQUs7QUFBQSxFQUNyQixLQUFLO0FBQUEsRUFDTCxXQUFXO0FBQUEsRUFDWCxXQUFXO0FBQUEsRUFDWCxPQUFPO0FBQUEsRUFDUCxRQUFRO0FBQUEsRUFDUixjQUFjLENBQUMsRUFBRSxPQUFPLE9BQU87QUFBQSxJQUM3QixJQUFJLFdBQVcsUUFBUSxTQUFTO0FBQUEsRUFDbEM7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= diff --git a/tests/e2e/prompts.e2e.test.ts b/tests/e2e/prompts.e2e.test.ts index d81d8d21..ee76abde 100644 --- a/tests/e2e/prompts.e2e.test.ts +++ b/tests/e2e/prompts.e2e.test.ts @@ -2151,5 +2151,128 @@ Configuration: expect(formattedMessages[1].content).toBe(expectedUser); }); }); + + it("should handle multimodal array inputs for ChatMessage types", () => { + const promptClient = new ChatPromptClient({ + name: "multimodal-test", + type: "chat", + version: 1, + prompt: [ + { + type: ChatMessageType.ChatMessage, + role: "user", + content: { + attachments: [ + { + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }, + ], + } as any, + }, + ], + config: {}, + labels: [], + tags: [], + }); + + // After fix: Should return the multimodal content as-is without crashing + const compiled = promptClient.compile(); + expect(compiled[0].content).toEqual({ + attachments: [ + { + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }, + ], + }); + }); + + it("should handle multimodal array inputs in the main loop with variables", () => { + const promptClient = new ChatPromptClient({ + name: "multimodal-test", + type: "chat", + version: 1, + prompt: [ + { + type: ChatMessageType.ChatMessage, + role: "user", + content: { + attachments: [ + { + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }, + ], + } as any, + }, + { + type: ChatMessageType.ChatMessage, + role: "user", + content: "Dummy: {{dummy}}", + }, + ], + config: {}, + labels: [], + tags: [], + }); + + // Passing a variable forces execution into the main loop + const compiled = promptClient.compile({ dummy: "test" }); + expect(compiled[0].content).toEqual({ + attachments: [ + { + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }, + ], + }); + expect(compiled[1].content).toEqual("Dummy: test"); + }); + + it("should handle multimodal content in getLangchainPrompt safely", () => { + const promptClient = new ChatPromptClient({ + name: "multimodal-langchain-test", + type: "chat", + version: 1, + prompt: [ + { + type: ChatMessageType.ChatMessage, + role: "user", + content: { + attachments: [ + { + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }, + ], + } as any, + }, + ], + config: {}, + labels: [], + tags: [], + }); + }); + + it("should handle multimodal content in getLangchainPrompt for non-ChatMessage items", () => { + const promptClient = new ChatPromptClient({ + name: "multimodal-non-chat-test", + type: "chat", + version: 1, + prompt: [ + { + type: ChatMessageType.Placeholder, + name: "multimodal_placeholder", + }, + ], + config: {}, + labels: [], + tags: [], + }); + + const result = promptClient.getLangchainPrompt(); + expect(result[0]).toEqual(["placeholder", "{multimodal_placeholder}"]); + }); }); }); diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 1f6f5b77..976aab94 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1,4 +1,5 @@ import { defineWorkspace } from "vitest/config"; +import { fileURLToPath } from "node:url"; export default defineWorkspace([ { @@ -10,30 +11,24 @@ export default defineWorkspace([ }, resolve: { alias: { - "@langfuse/client": new URL( - "./packages/client/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/tracing": new URL( - "./packages/tracing/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/otel": new URL( - "./packages/otel/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/langchain": new URL( - "./packages/langchain/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/openai": new URL( - "./packages/openai/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/core": new URL( - "./packages/core/dist/index.mjs", - import.meta.url, - ).pathname, + "@langfuse/client": fileURLToPath( + new URL("./packages/client/dist/index.mjs", import.meta.url), + ), + "@langfuse/tracing": fileURLToPath( + new URL("./packages/tracing/dist/index.mjs", import.meta.url), + ), + "@langfuse/otel": fileURLToPath( + new URL("./packages/otel/dist/index.mjs", import.meta.url), + ), + "@langfuse/langchain": fileURLToPath( + new URL("./packages/langchain/dist/index.mjs", import.meta.url), + ), + "@langfuse/openai": fileURLToPath( + new URL("./packages/openai/dist/index.mjs", import.meta.url), + ), + "@langfuse/core": fileURLToPath( + new URL("./packages/core/dist/index.mjs", import.meta.url), + ), }, }, }, @@ -47,30 +42,24 @@ export default defineWorkspace([ }, resolve: { alias: { - "@langfuse/client": new URL( - "./packages/client/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/tracing": new URL( - "./packages/tracing/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/otel": new URL( - "./packages/otel/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/langchain": new URL( - "./packages/langchain/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/openai": new URL( - "./packages/openai/dist/index.mjs", - import.meta.url, - ).pathname, - "@langfuse/core": new URL( - "./packages/core/dist/index.mjs", - import.meta.url, - ).pathname, + "@langfuse/client": fileURLToPath( + new URL("./packages/client/dist/index.mjs", import.meta.url), + ), + "@langfuse/tracing": fileURLToPath( + new URL("./packages/tracing/dist/index.mjs", import.meta.url), + ), + "@langfuse/otel": fileURLToPath( + new URL("./packages/otel/dist/index.mjs", import.meta.url), + ), + "@langfuse/langchain": fileURLToPath( + new URL("./packages/langchain/dist/index.mjs", import.meta.url), + ), + "@langfuse/openai": fileURLToPath( + new URL("./packages/openai/dist/index.mjs", import.meta.url), + ), + "@langfuse/core": fileURLToPath( + new URL("./packages/core/dist/index.mjs", import.meta.url), + ), }, }, },