Conversation
| msg_content, # type: ignore | ||
| kwargs, | ||
| ) | ||
| if isinstance(msg_content, str) | ||
| else msg_content | ||
| ) |
There was a problem hiding this 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:
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.There was a problem hiding this comment.
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.
ede0e1b to
1f6d0de
Compare
Summary
ChatPromptClient.compile()passes every placeholder-history message'scontentstraight into the string template parser (TemplateParser.compile_template), which assumes astr. Two standard OpenAI message shapes therefore crash instead of compiling:content=None— an assistant tool-call turn — raisesTypeError: object of type 'NoneType' has no len()(compile_templatedoeswhile curr_idx < len(content)).content=[...]— a multimodal / vision message (a list of content parts) — raisesAttributeError: 'list' object has no attribute 'find'.Both are exactly the shapes the message-placeholder feature is meant to carry when replaying a real conversation:
get_langchain_prompt(...)hits the same path since it callscompile()internally.The existing
test_tool_calls_preservation_in_message_placeholdercovers a tool-call turn but setscontent="", so the realNoneshape (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 preservestool_callsand 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 onmainwith theTypeErrorabove and passes with this change.tests/unitoffline suite passes locally (only pre-existing unrelated collection errors from optional integration deps / server-credentialed fixtures).uv run --frozen ruff check langfuse/model.pyandruff format --check— no new findings.uv run --frozen mypy langfuse/model.py --no-error-summary— clean.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.
Nonecontent used by assistant tool-call turns.compile()regression coverage for both shapes.Reviews (1) · Last reviewed commit: "fix(prompt): preserve non-string content..."