Skip to content

Commit c8f75f1

Browse files
authored
feat!: Update PromptBuilder and ChatPromptBuilder to set required_variables="*" by default (#11344)
1 parent 8527079 commit c8f75f1

11 files changed

Lines changed: 179 additions & 96 deletions

File tree

MIGRATION.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,43 @@ LangfuseConnector("Standalone Agent example")
125125
agent = Agent(chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"), tools=[...])
126126
agent.run(messages=[ChatMessage.from_user("What's the weather in Berlin?")])
127127
```
128+
129+
### `PromptBuilder` and `ChatPromptBuilder` template variables are required by default
130+
131+
**What changed:** `PromptBuilder` and `ChatPromptBuilder` now treat every Jinja2 template variable as required by default. Previously, variables were optional by default and missing values were silently rendered as empty strings. The `required_variables` parameter's default has been changed from `None` (all optional) to `"*"` (all required). Passing `required_variables=None` explicitly still opts into the old "all optional" behavior.
132+
133+
**Why:** Avoids silent rendering bugs where a missing variable produces an unexpectedly empty section of the prompt — especially in multi-branch pipelines where the issue often surfaces far from its root cause. Aligns the default with `ConditionalRouter`'s convention that inputs are required unless declared otherwise.
134+
135+
**How to migrate:**
136+
137+
Before (v2.x):
138+
```python
139+
from haystack.components.builders import PromptBuilder
140+
141+
# All variables were optional by default; missing values rendered as "".
142+
builder = PromptBuilder(template="Hello, {{ name }}! {{ greeting }}")
143+
builder.run(name="John") # greeting silently becomes "" → "Hello, John! "
144+
```
145+
146+
After (v3.0):
147+
```python
148+
from haystack.components.builders import PromptBuilder
149+
150+
# Option 1: provide every variable (matches the new safe default).
151+
builder = PromptBuilder(template="Hello, {{ name }}! {{ greeting }}")
152+
builder.run(name="John", greeting="Welcome")
153+
154+
# Option 2: declare which variables are required; everything else stays optional.
155+
builder = PromptBuilder(
156+
template="Hello, {{ name }}! {{ greeting }}",
157+
required_variables=["name"],
158+
)
159+
builder.run(name="John") # greeting renders as ""
160+
161+
# Option 3: restore the old "all optional" behavior.
162+
builder = PromptBuilder(
163+
template="Hello, {{ name }}! {{ greeting }}",
164+
required_variables=None,
165+
)
166+
builder.run(name="John") # greeting renders as ""
167+
```

haystack/components/builders/chat_prompt_builder.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ class ChatPromptBuilder:
3838
3939
It constructs prompts using static or dynamic templates, which you can update for each pipeline run.
4040
41-
Template variables in the template are optional unless specified otherwise.
42-
If an optional variable isn't provided, it defaults to an empty string. Use `variable` and `required_variables`
43-
to define input types and required variables.
41+
Template variables in the template are required by default. To make any subset of variables optional,
42+
set `required_variables` to an explicit list of the variables that should remain required; any variable
43+
not listed becomes optional and defaults to an empty string when missing.
44+
Set `required_variables` to `None` to mark every variable as optional.
4445
4546
### Usage examples
4647
@@ -139,7 +140,7 @@ class ChatPromptBuilder:
139140
def __init__(
140141
self,
141142
template: list[ChatMessage] | str | None = None,
142-
required_variables: list[str] | Literal["*"] | None = None,
143+
required_variables: list[str] | Literal["*"] | None = "*",
143144
variables: list[str] | None = None,
144145
) -> None:
145146
"""
@@ -151,8 +152,10 @@ def __init__(
151152
the `init` method` or the `run` method.
152153
:param required_variables:
153154
List variables that must be provided as input to ChatPromptBuilder.
154-
If a variable listed as required is not provided, an exception is raised.
155-
If set to `"*"`, all variables found in the prompt are required. Optional.
155+
Defaults to `"*"`, which marks every variable found in the prompt as required.
156+
Pass an explicit list to only require a subset of the variables; any variable not listed becomes
157+
optional and is replaced with an empty string in the rendered prompt when missing.
158+
Set to `None` to mark every variable as optional.
156159
:param variables:
157160
List input variables to use in prompt templates instead of the ones inferred from the
158161
`template` parameter. For example, to use more variables during prompt engineering than the ones present
@@ -192,10 +195,10 @@ def __init__(
192195

193196
if len(self.variables) > 0 and required_variables is None:
194197
logger.warning(
195-
"ChatPromptBuilder has {length} prompt variables, but `required_variables` is not set. "
196-
"By default, all prompt variables are treated as optional, which may lead to unintended behavior in "
197-
"multi-branch pipelines. To avoid unexpected execution, ensure that variables intended to be required "
198-
"are explicitly set in `required_variables`.",
198+
"ChatPromptBuilder has {length} prompt variables and `required_variables` is explicitly set to "
199+
"`None`. This treats all prompt variables as optional, which may lead to unintended behavior in "
200+
"multi-branch pipelines. Only set `required_variables` to `None` if you intentionally want all "
201+
"variables to be optional.",
199202
length=len(self.variables),
200203
)
201204

haystack/components/builders/prompt_builder.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ class PromptBuilder:
2020
Renders a prompt filling in any variables so that it can send it to a Generator.
2121
2222
The prompt uses Jinja2 template syntax.
23-
The variables in the default template are used as PromptBuilder's input and are all optional.
24-
If they're not provided, they're replaced with an empty string in the rendered prompt.
23+
The variables in the default template are used as PromptBuilder's input and are all required by default.
24+
To make any subset of variables optional, set `required_variables` to an explicit list of the variables that
25+
should remain required. Optional variables are replaced with an empty string in the rendered prompt.
2526
To try out different prompts, you can replace the prompt template at runtime by
2627
providing a template for each pipeline run invocation.
2728
@@ -141,7 +142,7 @@ class PromptBuilder:
141142
def __init__(
142143
self,
143144
template: str,
144-
required_variables: list[str] | Literal["*"] | None = None,
145+
required_variables: list[str] | Literal["*"] | None = "*",
145146
variables: list[str] | None = None,
146147
) -> None:
147148
"""
@@ -151,12 +152,12 @@ def __init__(
151152
A prompt template that uses Jinja2 syntax to add variables. For example:
152153
`"Summarize this document: {{ documents[0].content }}\\nSummary:"`
153154
It's used to render the prompt.
154-
The variables in the default template are input for PromptBuilder and are all optional,
155-
unless explicitly specified.
156-
If an optional variable is not provided, it's replaced with an empty string in the rendered prompt.
155+
The variables in the default template are input for PromptBuilder and are all required by default.
157156
:param required_variables: List variables that must be provided as input to PromptBuilder.
158-
If a variable listed as required is not provided, an exception is raised.
159-
If set to `"*"`, all variables found in the prompt are required. Optional.
157+
Defaults to `"*"`, which marks every variable found in the prompt as required.
158+
Pass an explicit list to only require a subset of the variables; any variable not listed becomes
159+
optional and is replaced with an empty string in the rendered prompt when missing.
160+
Set to `None` to mark every variable as optional.
160161
:param variables:
161162
List input variables to use in prompt templates instead of the ones inferred from the
162163
`template` parameter. For example, to use more variables during prompt engineering than the ones present
@@ -186,10 +187,10 @@ def __init__(
186187

187188
if len(self.variables) > 0 and required_variables is None:
188189
logger.warning(
189-
"PromptBuilder has {length} prompt variables, but `required_variables` is not set. "
190-
"By default, all prompt variables are treated as optional, which may lead to unintended behavior in "
191-
"multi-branch pipelines. To avoid unexpected execution, ensure that variables intended to be required "
192-
"are explicitly set in `required_variables`.",
190+
"PromptBuilder has {length} prompt variables and `required_variables` is explicitly set to `None`. "
191+
"This treats all prompt variables as optional, which may lead to unintended behavior in "
192+
"multi-branch pipelines. Only set `required_variables` to `None` if you intentionally want all "
193+
"variables to be optional.",
193194
length=len(self.variables),
194195
)
195196

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
upgrade:
3+
- |
4+
``PromptBuilder`` and ``ChatPromptBuilder`` now treat every Jinja2 template variable as **required by default**.
5+
Previously, variables were optional by default unless explicitly listed in ``required_variables``.
6+
7+
To check if you're affected: look for ``PromptBuilder(...)`` or ``ChatPromptBuilder(...)`` calls in your code
8+
that do not pass ``required_variables``. If those components relied on missing variables being silently
9+
rendered as empty strings, the behavior depends on how the missing variable was reaching the builder:
10+
11+
- Calling ``builder.run()`` directly without providing a required variable now raises ``ValueError``.
12+
- In a pipeline, if a required variable is neither provided as a user input nor connected from another
13+
component, ``Pipeline.run`` raises ``ValueError("Missing mandatory input '<var>' for component '<name>'.")``.
14+
- In a pipeline, if a required variable is connected to a sender that never produces output on a given run
15+
(for example, a loop-fed input on the first iteration or a conditional branch that isn't taken), the
16+
pipeline cannot make progress and raises ``PipelineComponentsBlockedError``.
17+
18+
Migration options:
19+
20+
- To require only a subset of variables, pass ``required_variables=["var1", "var2"]``.
21+
- To preserve the previous "all optional" behavior, explicitly pass ``required_variables=None``.
22+
- To require everything (the new default), no change is needed.

test/components/agents/test_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def test_to_dict(self, weather_tool, component_tool, monkeypatch):
254254
"init_parameters": {
255255
"template": "{{parrot}}",
256256
"variables": None,
257-
"required_variables": None,
257+
"required_variables": "*",
258258
},
259259
},
260260
"name": "parrot",
@@ -405,7 +405,7 @@ def test_from_dict(self, monkeypatch):
405405
"init_parameters": {
406406
"template": "{{parrot}}",
407407
"variables": None,
408-
"required_variables": None,
408+
"required_variables": "*",
409409
},
410410
},
411411
"name": "parrot",
@@ -552,7 +552,7 @@ def test_from_dict_state_schema_none(self, monkeypatch):
552552
"init_parameters": {
553553
"template": "{{parrot}}",
554554
"variables": None,
555-
"required_variables": None,
555+
"required_variables": "*",
556556
},
557557
},
558558
"name": "parrot",

test/components/builders/test_chat_prompt_builder.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ def test_init(self):
2828
ChatMessage.from_system("This is a {{ variable2 }}"),
2929
]
3030
)
31-
assert builder.required_variables == []
31+
assert builder.required_variables == "*"
3232
assert builder.template[0].text == "This is a {{ variable }}"
3333
assert builder.template[1].text == "This is a {{ variable2 }}"
3434
assert builder._variables is None
35-
assert builder._required_variables is None
35+
assert builder._required_variables == "*"
3636

3737
# we have inputs that contain: template, template_variables + inferred variables
3838
inputs = builder.__haystack_input__._sockets_dict
@@ -51,9 +51,9 @@ def test_init_without_template(self):
5151
variables = ["var1", "var2"]
5252
builder = ChatPromptBuilder(variables=variables)
5353
assert builder.template is None
54-
assert builder.required_variables == []
54+
assert builder.required_variables == "*"
5555
assert builder._variables == variables
56-
assert builder._required_variables is None
56+
assert builder._required_variables == "*"
5757

5858
# we have inputs that contain: template, template_variables + variables
5959
inputs = builder.__haystack_input__._sockets_dict
@@ -93,10 +93,10 @@ def test_init_with_custom_variables(self):
9393
variables = ["var1", "var2", "var3"]
9494
template = [ChatMessage.from_user("Hello, {{ var1 }}, {{ var2 }}!")]
9595
builder = ChatPromptBuilder(template=template, variables=variables)
96-
assert builder.required_variables == []
96+
assert builder.required_variables == "*"
9797
assert builder._variables == variables
9898
assert builder.template[0].text == "Hello, {{ var1 }}, {{ var2 }}!"
99-
assert builder._required_variables is None
99+
assert builder._required_variables == "*"
100100

101101
# we have inputs that contain: template, template_variables + variables
102102
inputs = builder.__haystack_input__._sockets_dict
@@ -132,11 +132,18 @@ def test_run_without_input(self):
132132
res = builder.run()
133133
assert res == {"prompt": [ChatMessage.from_user("This is a template without input")]}
134134

135-
def test_run_with_missing_input(self):
136-
builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a {{ variable }}")])
135+
def test_run_with_missing_optional_input(self):
136+
builder = ChatPromptBuilder(
137+
template=[ChatMessage.from_user("This is a {{ variable }}")], required_variables=None
138+
)
137139
res = builder.run()
138140
assert res == {"prompt": [ChatMessage.from_user("This is a ")]}
139141

142+
def test_run_with_missing_required_input_default(self):
143+
builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a {{ variable }}")])
144+
with pytest.raises(ValueError, match="variable"):
145+
builder.run()
146+
140147
def test_run_with_missing_required_input(self):
141148
builder = ChatPromptBuilder(
142149
template=[ChatMessage.from_user("This is a {{ foo }}, not a {{ bar }}")], required_variables=["foo", "bar"]
@@ -163,7 +170,7 @@ def test_run_with_variables(self):
163170
variables = ["var1", "var2", "var3"]
164171
template = [ChatMessage.from_user("Hello, {{ name }}! {{ var1 }}")]
165172

166-
builder = ChatPromptBuilder(template=template, variables=variables)
173+
builder = ChatPromptBuilder(template=template, variables=variables, required_variables=None)
167174

168175
template_variables = {"name": "John"}
169176
expected_result = {"prompt": [ChatMessage.from_user("Hello, John! How are you?")]}
@@ -173,7 +180,7 @@ def test_run_with_variables(self):
173180
def test_run_with_variables_and_runtime_template(self):
174181
variables = ["var1", "var2", "var3"]
175182

176-
builder = ChatPromptBuilder(variables=variables)
183+
builder = ChatPromptBuilder(variables=variables, required_variables=None)
177184

178185
template = [ChatMessage.from_user("Hello, {{ name }}! {{ var1 }}")]
179186
template_variables = {"name": "John"}
@@ -209,7 +216,7 @@ def test_run_overwriting_default_template_with_variables(self):
209216
variables = ["var1", "var2", "name"]
210217
default_template = [ChatMessage.from_user("Hello, {{ name }}!")]
211218

212-
builder = ChatPromptBuilder(template=default_template, variables=variables)
219+
builder = ChatPromptBuilder(template=default_template, variables=variables, required_variables=None)
213220

214221
template = [ChatMessage.from_user("Hello, {{ var1 }} {{ name }}!")]
215222
expected_result = {"prompt": [ChatMessage.from_user("Hello, Big John!")]}
@@ -346,15 +353,26 @@ def test_example_in_pipeline_simple(self):
346353
}
347354
assert result == expected_dynamic
348355

349-
def test_warning_no_required_variables(self, caplog):
356+
def test_warning_when_required_variables_explicitly_none(self, caplog):
357+
with caplog.at_level(logging.WARNING):
358+
_ = ChatPromptBuilder(
359+
template=[
360+
ChatMessage.from_system("Write your response in this language:{{language}}"),
361+
ChatMessage.from_user("Tell me about {{location}}"),
362+
],
363+
required_variables=None,
364+
)
365+
assert "explicitly set to `None`" in caplog.text
366+
367+
def test_no_warning_with_default_required_variables(self, caplog):
350368
with caplog.at_level(logging.WARNING):
351369
_ = ChatPromptBuilder(
352370
template=[
353371
ChatMessage.from_system("Write your response in this language:{{language}}"),
354372
ChatMessage.from_user("Tell me about {{location}}"),
355373
]
356374
)
357-
assert "ChatPromptBuilder has 2 prompt variables, but `required_variables` is not set. " in caplog.text
375+
assert "required_variables" not in caplog.text
358376

359377

360378
class TestChatPromptBuilderWithJinja2TimeExtension:
@@ -697,7 +715,7 @@ def test_from_dict_template_none(self):
697715

698716
assert comp.template is None
699717
assert comp._variables is None
700-
assert comp._required_variables is None
718+
assert comp._required_variables == "*"
701719

702720
def test_chat_message_list_with_templatize_part_init_raises_error(self):
703721
template = [ChatMessage.from_user("This is a {{ variable | templatize_part }}")]
@@ -722,7 +740,7 @@ def test_init(self):
722740

723741
assert builder.template == template
724742
assert builder._variables is None
725-
assert builder._required_variables is None
743+
assert builder._required_variables == "*"
726744
assert builder.variables == ["name"]
727745

728746
def test_init_with_invalid_template(self):
@@ -774,13 +792,13 @@ def test_run_without_input(self):
774792
result = builder.run()
775793
assert result["prompt"] == [ChatMessage.from_user("Hello, my name is Lukas!")]
776794

777-
def test_run_with_missing_input(self):
795+
def test_run_with_missing_optional_input(self):
778796
template = """
779797
{% message role="user" %}
780798
Hello, my name is {{name}}!
781799
{% endmessage %}
782800
"""
783-
builder = ChatPromptBuilder(template=template)
801+
builder = ChatPromptBuilder(template=template, required_variables=None)
784802
result = builder.run()
785803
assert result["prompt"] == [ChatMessage.from_user("Hello, my name is !")]
786804

@@ -826,7 +844,7 @@ def test_run_overwriting_default_template(self):
826844
Hello, my name is {{name}}!
827845
{% endmessage %}
828846
"""
829-
builder = ChatPromptBuilder(template=initial_template)
847+
builder = ChatPromptBuilder(template=initial_template, required_variables=None)
830848

831849
runtime_template = """
832850
{% message role="user" %}

0 commit comments

Comments
 (0)