fix: render every content part of ChatPromptBuilder message templates (#12907)

Co-authored-by: anakin87 <[email protected]>
This commit is contained in:
Grapette.L
2026-09-30 09:14:55 +00:00
committed by GitHub
co-authored by anakin87
parent 6391b9ddee
commit 2ae9acdf31
3 changed files with 68 additions and 16 deletions
@@ -176,12 +176,13 @@ class ChatPromptBuilder:
# infer variables from template
if message.text is None:
raise ValueError(NO_TEXT_ERROR_MESSAGE.format(role=message.role.value, message=message))
if message.text and "templatize_part" in message.text:
raise ValueError(FILTER_NOT_ALLOWED_ERROR_MESSAGE)
assigned_variables, template_variables = _extract_template_variables_and_assignments(
env=self._env, template=message.text
)
extracted_variables += list(template_variables - assigned_variables)
for text in message.texts:
if "templatize_part" in text:
raise ValueError(FILTER_NOT_ALLOWED_ERROR_MESSAGE)
assigned_variables, template_variables = _extract_template_variables_and_assignments(
env=self._env, template=text
)
extracted_variables += list(template_variables - assigned_variables)
elif isinstance(template, str):
assigned_variables, template_variables = _extract_template_variables_and_assignments(
env=self._env, template=template
@@ -263,12 +264,15 @@ class ChatPromptBuilder:
self._validate_variables(set(template_variables_combined.keys()))
if message.text is None:
raise ValueError(NO_TEXT_ERROR_MESSAGE.format(role=message.role.value, message=message))
if message.text and "templatize_part" in message.text:
raise ValueError(FILTER_NOT_ALLOWED_ERROR_MESSAGE)
compiled_template = self._env.from_string(message.text)
rendered_text = compiled_template.render(template_variables_combined)
rendered_content = list(message._content)
for index, part in enumerate(rendered_content):
if isinstance(part, TextContent):
if "templatize_part" in part.text:
raise ValueError(FILTER_NOT_ALLOWED_ERROR_MESSAGE)
rendered_text = self._env.from_string(part.text).render(template_variables_combined)
rendered_content[index] = TextContent(text=rendered_text)
# use dataclasses.replace to avoid in-place mutation of the original message
rendered_message: ChatMessage = replace(message, _content=[TextContent(text=rendered_text)])
rendered_message: ChatMessage = replace(message, _content=rendered_content)
processed_messages.append(rendered_message)
else:
processed_messages.append(message)
@@ -0,0 +1,8 @@
---
fixes:
- |
Fixed ``ChatPromptBuilder`` dropping content parts when the template is a list of ``ChatMessage`` objects.
Only the first ``TextContent`` part was rendered, and every other part - additional texts, images, files -
was removed from the rendered prompt without a warning. Template variables used in those dropped parts
were not detected either, so they were not exposed as inputs. Now all ``TextContent`` parts are
rendered and the remaining content parts are passed through unchanged.
@@ -15,7 +15,7 @@ from haystack import component
from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder
from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
from haystack.core.pipeline.pipeline import Pipeline
from haystack.dataclasses.chat_message import ChatMessage, FileContent, ImageContent, ReasoningContent
from haystack.dataclasses.chat_message import ChatMessage, FileContent, ImageContent, ReasoningContent, TextContent
from haystack.dataclasses.document import Document
@@ -119,6 +119,36 @@ class TestChatPromptBuilder:
res = builder.run(variable="test")
assert res == {"prompt": [ChatMessage.from_user("This is a test")]}
def test_run_with_multiple_text_parts(self):
template = [
ChatMessage.from_user(
content_parts=[TextContent(text="Hello, {{ name }}!"), TextContent(text="Goodbye, {{ other }}!")]
)
]
builder = ChatPromptBuilder(template=template)
assert set(builder.variables) == {"name", "other"}
assert builder.run(name="John", other="Jane")["prompt"][0].texts == ["Hello, John!", "Goodbye, Jane!"]
@pytest.mark.parametrize(
"content_part",
[
ImageContent(base64_image="cHJldGVuZC1wbmctYnl0ZXM=", mime_type="image/png"),
FileContent(base64_data="dGVzdA==", mime_type="application/pdf", filename="document.pdf"),
],
ids=["image", "file"],
)
def test_run_preserves_non_text_content_parts(self, content_part):
template = [ChatMessage.from_user(content_parts=["Describe {{ thing }}", content_part, "Also {{ other }}"])]
builder = ChatPromptBuilder(template=template)
assert builder.run(thing="this", other="that") == {
"prompt": [ChatMessage.from_user(content_parts=["Describe this", content_part, "Also that"])]
}
assert template == [
ChatMessage.from_user(content_parts=["Describe {{ thing }}", content_part, "Also {{ other }}"])
]
def test_run_template_variable(self):
builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a {{ variable }}")])
res = builder.run(template_variables={"variable": "test"})
@@ -720,14 +750,24 @@ class TestChatPromptBuilderDynamic:
assert comp._variables is None
assert comp._required_variables == "*"
def test_chat_message_list_with_templatize_part_init_raises_error(self):
template = [ChatMessage.from_user("This is a {{ variable | templatize_part }}")]
@pytest.mark.parametrize(
"content_parts",
[["This is a {{ variable | templatize_part }}"], ["First text", "This is a {{ variable | templatize_part }}"]],
ids=["first_text", "second_text"],
)
def test_chat_message_list_with_templatize_part_init_raises_error(self, content_parts):
template = [ChatMessage.from_user(content_parts=content_parts)]
with pytest.raises(ValueError, match="templatize_part filter cannot be used"):
ChatPromptBuilder(template=template)
def test_chat_message_list_with_templatize_part_run_raises_error(self):
@pytest.mark.parametrize(
"content_parts",
[["This is a {{ variable | templatize_part }}"], ["First text", "This is a {{ variable | templatize_part }}"]],
ids=["first_text", "second_text"],
)
def test_chat_message_list_with_templatize_part_run_raises_error(self, content_parts):
builder = ChatPromptBuilder()
template = [ChatMessage.from_user("This is a {{ variable | templatize_part }}")]
template = [ChatMessage.from_user(content_parts=content_parts)]
with pytest.raises(ValueError, match="templatize_part filter cannot be used"):
builder.run(template=template, variable="test")