Skip to content

fix(client): apply mustache substitution to text items in array message content#801

Open
Ashut0sh-mishra wants to merge 2 commits into
langfuse:mainfrom
Ashut0sh-mishra:fix/chat-prompt-array-content-compile
Open

fix(client): apply mustache substitution to text items in array message content#801
Ashut0sh-mishra wants to merge 2 commits into
langfuse:mainfrom
Ashut0sh-mishra:fix/chat-prompt-array-content-compile

Conversation

@Ashut0sh-mishra

@Ashut0sh-mishra Ashut0sh-mishra commented May 12, 2026

Copy link
Copy Markdown

Problem

ChatPromptClient.compile() silently skips variable substitution when a chat message's content field is an array (multimodal / OpenAI-style format, e.g. [{ type: 'text', text: 'Hello {{name}}' }, { type: 'image_url', ... }]).

The existing guard ypeof item.content === 'string' short-circuits to the \else\ branch for array content, returning the array completely unchanged — {{variable}}\ placeholders inside text parts are never resolved.

Reported in langfuse/langfuse#12338.

Root cause

ypescript // Before — array content falls into the else branch untouched if (... && typeof item.content === 'string') { return { ...item, content: mustache.render(item.content, variables) }; } else { return item; // ← arrays pass through here with no substitution }

Fix

Split the \string\ branch into two cases:

  1. String content — existing path unchanged.
  2. Array content — iterate content parts; for each part with ype === 'text' and a string ext field, call mustache.render(part.text, variables). All other part types (image_url, input_file, etc.) are passed through untouched.

The same two-case pattern is applied inside getLangchainPrompt() wherever _transformToLangchainVariables() was previously called directly on msg.content/item.content (which would throw on arrays).

Tests

Added packages/client/src/prompt/promptClients.test.ts with cases covering:

  • String content regression
  • No crash on array content
  • Variable substitution in text parts
  • Non-text parts passed through unchanged
  • Placeholder resolution where placeholder messages have array content

Checklist

  • Bug fix (non-breaking change)
  • Tests added
  • Existing behaviour unchanged for string content

Greptile Summary

This PR fixes ChatPromptClient.compile() and getLangchainPrompt() silently skipping Mustache variable substitution when a chat message's content field is an array (multimodal / OpenAI-style format). The fix adds an Array.isArray branch that iterates content parts and applies mustache.render (or _transformToLangchainVariables) only to parts with type === "text", leaving all other part types untouched.

  • compile() fix: a new else if (Array.isArray(item.content)) branch in the final map handles multimodal messages; placeholder-injected messages with array content also flow through this same map and benefit automatically.
  • getLangchainPrompt() fix: two symmetric array-content branches are added—one for placeholder-filled messages and one for regular chat messages—both converting {{var}} syntax to {var} for LangChain.
  • Tests: five new vitest cases cover string-content regression, no-crash on array content, text-part substitution, non-text part pass-through, and placeholder resolution with array content.

Confidence Score: 4/5

Safe to merge; the fix correctly extends both compile() and getLangchainPrompt() to handle array content, and existing string-content behaviour is unchanged.

The core logic is sound across all three new code paths and the added tests cover the key scenarios. The two remaining gaps—duplicated part-mapping code and a missing test for mustache substitution inside placeholder-injected array content—are style and coverage concerns that do not affect correctness of the shipped fix.

promptClients.ts warrants a second look for the three independent copies of the part-mapping pattern, which would be worth consolidating before the codebase grows further.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant compile
    participant map as compile() map
    participant mustache

    Caller->>compile: compile(variables, placeholders)
    Note over compile: Resolve placeholders into messagesWithPlaceholdersReplaced
    compile->>map: map over resolved messages
    alt string content
        map->>mustache: mustache.render(content, variables)
        mustache-->>map: rendered string
    else array content (NEW)
        loop each part in content array
            alt "part.type === text"
                map->>mustache: mustache.render(part.text, variables)
                mustache-->>map: rendered text
            else non-text part
                map->>map: pass through unchanged
            end
        end
    else other or placeholder
        map->>map: return item as-is
    end
    map-->>Caller: compiled messages[]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/client/src/prompt/promptClients.ts:384-401
**Duplicated array-content mapping logic**

The `part => (part.type === "text" && typeof part.text === "string") ? { ...part, text: ... } : part` pattern now appears three times: once in `compile()` and twice in `getLangchainPrompt()` (once for placeholder messages, once for regular chat messages). A small private helper—e.g. `_applyToArrayContentParts(parts, fn)`—would centralise the logic and make future changes (e.g., supporting a new part type) easier to apply consistently.

### Issue 2 of 2
packages/client/src/prompt/promptClients.test.ts:84-100
**No test for mustache substitution in placeholder-injected array content**

The test verifies that array content from a placeholder passes through without crashing, but the injected messages only contain an `input_file` part (which is intentionally not substituted). There is no test that injects a placeholder whose messages contain a `{ type: "text", text: "Hello {{name}}" }` part and then asserts that `compile({ name: "Alice" }, { ... })` resolves it to `"Hello Alice"`. Given that variable substitution in placeholder array content is now supported (the injected messages go through the `map` and hit the new array branch), a test for this path would confirm the intended behaviour and guard against regressions.

Reviews (1): Last reviewed commit: "fix(client): apply mustache substitution..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

@Ashut0sh-mishra is attempting to deploy a commit to the langfuse Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant

CLAassistant commented May 12, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment on lines +84 to +100
makeChatPrompt([
{ type: ChatMessageType.ChatMessage, role: "system", content: "You are helpful." },
{ type: ChatMessageType.Placeholder, name: "attachments" },
]),
);
const attachments = [
{
role: "user",
content: [
{ type: "input_file", file_url: "path_to_pdf" },
],
},
];
const result = client.compile({ emailBody: "test" }, { attachments }) as any[];
expect(result).toHaveLength(2);
expect(result[1].role).toBe("user");
expect(result[1].content[0]).toEqual({ type: "input_file", file_url: "path_to_pdf" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 No test for mustache substitution in placeholder-injected array content

The test verifies that array content from a placeholder passes through without crashing, but the injected messages only contain an input_file part (which is intentionally not substituted). There is no test that injects a placeholder whose messages contain a { type: "text", text: "Hello {{name}}" } part and then asserts that compile({ name: "Alice" }, { ... }) resolves it to "Hello Alice". Given that variable substitution in placeholder array content is now supported (the injected messages go through the map and hit the new array branch), a test for this path would confirm the intended behaviour and guard against regressions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/client/src/prompt/promptClients.test.ts
Line: 84-100

Comment:
**No test for mustache substitution in placeholder-injected array content**

The test verifies that array content from a placeholder passes through without crashing, but the injected messages only contain an `input_file` part (which is intentionally not substituted). There is no test that injects a placeholder whose messages contain a `{ type: "text", text: "Hello {{name}}" }` part and then asserts that `compile({ name: "Alice" }, { ... })` resolves it to `"Hello Alice"`. Given that variable substitution in placeholder array content is now supported (the injected messages go through the `map` and hit the new array branch), a test for this path would confirm the intended behaviour and guard against regressions.

How can I resolve this? If you propose a fix, please make it concise.

Ashut0sh-mishra and others added 2 commits May 12, 2026 19:48
…ge content

ChatPromptClient.compile() silently skipped variable substitution when a
message's content was an array (multimodal format). Text-type content parts
containing {{variable}} placeholders were returned unchanged instead of
having variables resolved.

Fix by iterating array content parts in compile() and applying
mustache.render() to items where type === 'text' and the 'text' field is
a string, while leaving image_url, input_file, and all other content part
types untouched. Apply the same pattern in getLangchainPrompt() where
_transformToLangchainVariables() was called directly on content that could
be an array.

Add unit tests covering string content regression, crash prevention,
variable substitution in text parts, and placeholder resolution with
multimodal content.

Fixes langfuse/langfuse#12338

Co-authored-by: nik464 <nik464@users.noreply.github.com>
…d array content

Addresses greptile review on langfuse#801: verify that `{{variable}}` placeholders
in text parts of messages injected via a placeholder are resolved during
`compile()`. Also asserts non-text parts (e.g. `image_url`) within
placeholder content are passed through untouched.

Co-authored-by: nik464 <nik464@users.noreply.github.com>
@Ashut0sh-mishra Ashut0sh-mishra force-pushed the fix/chat-prompt-array-content-compile branch from b7d4cd7 to 241b574 Compare May 12, 2026 14:18
@Ashut0sh-mishra

Copy link
Copy Markdown
Author

Thanks @greptile-apps! Added the missing test in the latest commit. It injects a placeholder whose messages contain a text part with a {{name}} placeholder plus an image_url part, then asserts: (1) the text part is substituted (Hello Alice), and (2) the non-text image_url inside placeholder content is passed through untouched, matching the documented intent. Also rebased on main so the branch is no longer behind.

@greptile-apps

greptile-apps Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

The new test (lines 101–129) covers exactly the missing path — placeholder injection with a mixed text + image_url array, asserting substitution on the text part and pass-through on the non-text part. That fully addresses Issue 2.

The only remaining open item from the original review is Issue 1 — the three independent copies of the part-mapping pattern in promptClients.ts. That's a refactoring concern rather than a correctness issue, so it's fine to defer or handle separately. The bug fix itself is solid.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants