Skip to content

fix(prompt): preserve non-string content in message placeholders - #1894

Open
winklemad wants to merge 1 commit into
langfuse:mainfrom
winklemad:fix/placeholder-non-string-content
Open

winklemad wants to merge 1 commit into
langfuse:mainfrom
winklemad:fix/placeholder-non-string-content

Conversation

@winklemad

@winklemad winklemad commented Sep 20, 2026 •

Copy link
Copy Markdown

Summary

ChatPromptClient.compile() passes every placeholder-history message's content straight into the string template parser (TemplateParser.compile_template), which assumes a str. Two standard OpenAI message shapes therefore crash instead of compiling:

  • content=None — an assistant tool-call turn — raises TypeError: object of type 'NoneType' has no len() (compile_template does while curr_idx < len(content)).
  • content=[...] — a multimodal / vision message (a list of content parts) — raises AttributeError: 'list' object has no attribute 'find'.

Both are exactly the shapes the message-placeholder feature is meant to carry when replaying a real conversation:

prompt.compile(message_history=[
    {"role": "user", "content": "What's the weather in SF?"},
    {"role": "assistant", "content": None,
     "tool_calls": [{"id": "call_1", "type": "function",
                     "function": {"name": "get_weather", "arguments": "{...}"}}]},
    {"role": "tool", "content": "72F sunny", "tool_call_id": "call_1"},
])

get_langchain_prompt(...) hits the same path since it calls compile() internally.

The existing test_tool_calls_preservation_in_message_placeholder covers a tool-call turn but sets content="", so the real None shape (and multimodal list content) were never exercised.

Fix

Only run the template parser when the content is a string; pass non-string content (None, or a multimodal list) through unchanged. One guard covers both cases and preserves tool_calls and every other field on the message.

Verification

  • uv run --frozen pytest tests/unit/test_prompt_compilation.py — all pass, including a new regression test (test_placeholder_message_with_non_string_content) that fails on main with the TypeError above and passes with this change.
  • Full tests/unit offline suite passes locally (only pre-existing unrelated collection errors from optional integration deps / server-credentialed fixtures).
  • uv run --frozen ruff check langfuse/model.py and ruff format --check — no new findings.
  • uv run --frozen mypy langfuse/model.py --no-error-summary — clean.

RetriggerConfidence Score: 4/5

The PR is not yet safe to merge because get_langchain_prompt() still crashes on the non-string placeholder values this change intends to support.

Summary

This PR updates chat-placeholder compilation to preserve non-string message content instead of sending it through the string template parser.

  • Preserves None content used by assistant tool-call turns.
  • Preserves list-based multimodal content.
  • Adds direct compile() regression coverage for both shapes.
  • The corresponding LangChain conversion path remains string-only and still fails for these values.

Reviews (1) · Last reviewed commit: "fix(prompt): preserve non-string content..."

@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.

@CLAassistant

CLAassistant commented Sep 20, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread langfuse/model.py
Comment on lines +364 to 369
msg_content, # type: ignore
kwargs,
)
if isinstance(msg_content, str)
else msg_content
)

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.

P1 LangChain conversion still crashes

Non-string placeholder content now survives compile(), but get_langchain_prompt() then passes every compiled message’s content to the string-only _get_langchain_prompt_string(). When the history contains an assistant tool-call turn with content=None or multimodal list content, this public path raises TypeError, so the fix remains incomplete for LangChain callers.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: langfuse/model.py
Line: 364-369

Comment:
**LangChain conversion still crashes**

Non-string placeholder content now survives `compile()`, but `get_langchain_prompt()` then passes every compiled message’s content to the string-only `_get_langchain_prompt_string()`. When the history contains an assistant tool-call turn with `content=None` or multimodal list content, this public path raises `TypeError`, so the fix remains incomplete for LangChain callers.

**Knowledge Base Used:**
- [Prompts and framework integrations](https://app.greptile.com/personal-org-4986/-/custom-context/knowledge-base/langfuse/langfuse-python/-/docs/prompts-and-framework-integrations.md)
- [Prompt retrieval, compilation, and caching](https://app.greptile.com/personal-org-4986/-/custom-context/knowledge-base/langfuse/langfuse-python/-/docs/prompt-retrieval-compilation-and-cache.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — get_langchain_prompt() hits the same string-only path (_escape_json_for_langchain does len(text)), so guarding compile() alone wasn't enough. Pushed a fix that applies the same isinstance-str guard there too: multimodal list content passes through (LangChain accepts it), and a tool-call turn's None is normalized to "" — LangChain rejects a (role, None) template tuple, and "" is its convention for a tool-call message. Added a get_langchain_prompt regression test that also asserts the result is consumable by ChatPromptTemplate.from_messages(...).

ChatPromptClient.compile() and get_langchain_prompt() fed every
placeholder message's content into string-only helpers, so a message
whose content was not a string crashed: content=None (an assistant
tool-call turn) raised TypeError, and a list of content parts (a
multimodal message) raised TypeError/AttributeError. Both are standard
OpenAI message shapes the placeholder feature is meant to carry -- the
existing tool-call test only exercised content="".

compile() now only templates string content and passes non-string
content through unchanged. get_langchain_prompt() does the same and
additionally normalizes None to "" (LangChain's convention for a
tool-call message, since a (role, None) tuple is not a valid LangChain
template) while passing multimodal lists through. Adds regression tests
covering both entry points.
@winklemad
winklemad force-pushed the fix/placeholder-non-string-content branch from ede0e1b to 1f6d0de Compare September 20, 2026 13:27

This branch has not been deployed

No deployments
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