Skip to content

Commit 2e6f473

Browse files
authored
chore!: make required_variables all on by default for Agent (#12027)
1 parent 5d101e5 commit 2e6f473

6 files changed

Lines changed: 105 additions & 7 deletions

File tree

MIGRATION.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,58 @@ pipeline.run(data={"retriever": {"query": query}, "agent": {"messages": [], "que
477477
If the prompt itself must still be assembled per run, build `ChatMessage` objects before the `Agent` (e.g. with a `ChatPromptBuilder`) and pass them through the `messages` input.
478478
For a runtime system prompt, construct an `Agent` without `system_prompt` or `user_prompt` and include a system message at the start of `messages`.
479479

480+
#### Prompt template variables are required by default
481+
482+
**What changed:** `Agent` now treats every Jinja2 template variable in `user_prompt` and `system_prompt` as required by default. The `required_variables` parameter's default has been changed from `None` (all optional) to `"*"` (all required). Previously, missing variables were silently rendered as empty strings. Passing `required_variables=None` explicitly still opts into the old "all optional" behavior.
483+
484+
**Why:** Avoids silent rendering bugs where a missing variable produces an unexpectedly empty section of the prompt. Aligns `Agent` with `LLM`, `PromptBuilder`, and `ChatPromptBuilder`, which already require all variables by default in v3.0.
485+
486+
**How to migrate:**
487+
488+
Before (v2.x):
489+
```python
490+
from haystack.components.agents import Agent
491+
492+
# All variables were optional by default; missing values rendered as "".
493+
agent = Agent(
494+
chat_generator=...,
495+
tools=[...],
496+
user_prompt="Answer {{query}} in {{language}}.",
497+
)
498+
agent.run(messages=[], query="What is NLP?") # language silently becomes ""
499+
```
500+
501+
After (v3.0):
502+
```python
503+
from haystack.components.agents import Agent
504+
505+
# Option 1: provide every variable (matches the new safe default).
506+
agent = Agent(
507+
chat_generator=...,
508+
tools=[...],
509+
user_prompt="Answer {{query}} in {{language}}.",
510+
)
511+
agent.run(messages=[], query="What is NLP?", language="English")
512+
513+
# Option 2: declare which variables are required; everything else stays optional.
514+
agent = Agent(
515+
chat_generator=...,
516+
tools=[...],
517+
user_prompt="Answer {{query}} in {{language}}.",
518+
required_variables=["query"],
519+
)
520+
agent.run(messages=[], query="What is NLP?") # language renders as ""
521+
522+
# Option 3: restore the old "all optional" behavior.
523+
agent = Agent(
524+
chat_generator=...,
525+
tools=[...],
526+
user_prompt="Answer {{query}} in {{language}}.",
527+
required_variables=None,
528+
)
529+
agent.run(messages=[], query="What is NLP?") # language renders as ""
530+
```
531+
480532
#### Tools must declare `inputs_from_state` to read from `State` by name
481533

482534
**What changed:** A tool now reads a value from the Agent's `State` by name only when it declares an explicit `inputs_from_state` mapping. Previously, a tool without `inputs_from_state` had every parameter implicitly treated as a potential `State` key: any parameter whose name matched a `State` key (and that the LLM did not supply) was silently filled from `State`. This implicit name-matching has been removed. It applies to every tool type, since `ComponentTool`, `PipelineTool`, `MCPTool`, and others all derive from the base `Tool` class.

docs-website/docs/overview/migration.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ The agent-specific breakpoint API (`AgentBreakpoint`, `ToolBreakpoint`, `AgentSn
251251

252252
`Agent.run` and `Agent.run_async` no longer accept `system_prompt` or `user_prompt`; both must be set at initialization time. If a prompt must still be assembled per run, build `ChatMessage` objects before the Agent (for example, with a `ChatPromptBuilder`) and pass them through the `messages` input — a system message at the start of `messages` acts as a runtime system prompt. The same change applies to the `LLM` component.
253253

254+
### Prompt template variables are required by default
255+
256+
`Agent` now treats every Jinja2 template variable in `user_prompt` and `system_prompt` as required, in line with the [prompt builders](#prompt-builders-template-variables-are-required-by-default): the `required_variables` parameter's default changed from `None` (all optional) to `"*"` (all required). Previously, missing variables were silently rendered as empty strings. Pass `required_variables=["var1", "var2"]` to require only a subset, or `required_variables=None` to restore the old "all optional" behavior.
257+
254258
### Tools must declare `inputs_from_state` to read from `State` by name
255259

256260
A tool now reads a value from the Agent's [`State`](../pipeline-components/agents-1/state.mdx) by name only when it declares an explicit `inputs_from_state` mapping. The old implicit behavior — any tool parameter whose name matched a `State` key was silently filled from `State` — has been removed. Add `inputs_from_state={"state_key": "parameter_name"}` to any tool that should read from `State`. Tools that take the full `State` object via a `State`-annotated parameter are unaffected.

haystack/components/agents/agent.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,6 @@ def translate(
473473
user_prompt=\"\"\"{% message role="user"%}
474474
Translate the following document to {{ language }}: {{ document }}
475475
{% endmessage %}\"\"\",
476-
required_variables=["language", "document"],
477476
)
478477
479478
# The template variables 'language' and 'document' become inputs to the run method
@@ -544,7 +543,7 @@ def __init__( # noqa: PLR0913
544543
tools: ToolsType | None = None,
545544
system_prompt: str | None = None,
546545
user_prompt: str | None = None,
547-
required_variables: list[str] | Literal["*"] | None = None,
546+
required_variables: list[str] | Literal["*"] | None = "*",
548547
exit_conditions: list[str] | None = None,
549548
state_schema: dict[str, Any] | None = None,
550549
max_agent_steps: int = 100,
@@ -569,7 +568,8 @@ def __init__( # noqa: PLR0913
569568
:param required_variables:
570569
Lists the variables that must be provided as inputs to `user_prompt` or `system_prompt`.
571570
If a required variable is not provided at run time, an exception is raised.
572-
If set to `"*"`, all variables found in the prompts are required. Optional.
571+
If set to `"*"`, all variables found in the prompts are required. Defaults to `"*"`.
572+
Set to `None` to make all variables optional; missing ones render as empty strings.
573573
:param exit_conditions: List of conditions that will cause the agent to return.
574574
Can include "text" if the agent should return when it generates a message without tool calls,
575575
or tool names that will cause the agent to return once the tool was executed. Defaults to ["text"].
@@ -711,7 +711,7 @@ def _register_prompt_variables(self) -> None:
711711
prompt_builders = [
712712
builder for builder in (self._system_chat_prompt_builder, self._user_chat_prompt_builder) if builder
713713
]
714-
if required_variables is not None and not any(builder.variables for builder in prompt_builders):
714+
if isinstance(required_variables, list) and not any(builder.variables for builder in prompt_builders):
715715
logger.warning(
716716
"The parameter required_variables is provided but neither user_prompt nor system_prompt "
717717
"contains template variables. Either provide a prompt with Jinja2 template variables "
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
upgrade:
3+
- |
4+
``Agent`` now treats every Jinja2 template variable in ``user_prompt`` and ``system_prompt`` as
5+
**required by default**. The default of the ``required_variables`` init parameter has changed from
6+
``None`` (all variables optional) to ``"*"`` (all variables required). This aligns ``Agent`` with
7+
``LLM``, ``PromptBuilder``, and ``ChatPromptBuilder``.
8+
9+
To check if you're affected: look for ``Agent(...)`` calls that pass a ``user_prompt`` or
10+
``system_prompt`` containing template variables without passing ``required_variables``. If those
11+
Agents relied on missing variables being silently rendered as empty strings, the behavior depends on
12+
how the missing variable was reaching the Agent:
13+
14+
- Calling ``agent.run()`` directly without providing a required variable now raises ``ValueError``.
15+
- In a pipeline, if a required variable is neither provided as a user input nor connected from another
16+
component, ``Pipeline.run`` raises ``ValueError("Missing mandatory input '<var>' for component '<name>'.")``.
17+
- In a pipeline, if a required variable is connected to a sender that never produces output on a given run
18+
(for example, a loop-fed input on the first iteration or a conditional branch that isn't taken), the
19+
pipeline cannot make progress and raises ``PipelineComponentsBlockedError``.
20+
21+
Migration options:
22+
23+
- To require only a subset of variables, pass ``required_variables=["var1", "var2"]``.
24+
- To preserve the previous "all optional" behavior, explicitly pass ``required_variables=None``.
25+
- To require everything (the new default), no change is needed.

test/components/agents/test_agent.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def test_to_dict(self, weather_tool, component_tool, monkeypatch):
303303
],
304304
"system_prompt": None,
305305
"user_prompt": None,
306-
"required_variables": None,
306+
"required_variables": "*",
307307
"exit_conditions": ["text", "weather_tool"],
308308
"state_schema": {"foo": {"type": "str"}},
309309
"max_agent_steps": 100,
@@ -370,7 +370,7 @@ def test_to_dict_with_toolset(self, monkeypatch, weather_tool):
370370
},
371371
"system_prompt": None,
372372
"user_prompt": None,
373-
"required_variables": None,
373+
"required_variables": "*",
374374
"exit_conditions": ["text"],
375375
"state_schema": {},
376376
"max_agent_steps": 100,
@@ -1766,6 +1766,23 @@ def test_register_prompt_variables_warning_when_no_prompt_and_required_variables
17661766
make_agent(required_variables=["name"])
17671767
assert "The parameter required_variables is provided but neither" in caplog.text
17681768

1769+
def test_register_prompt_variables_no_warning_when_no_prompt_and_default(self, make_agent, caplog):
1770+
make_agent()
1771+
assert "The parameter required_variables is provided but neither" not in caplog.text
1772+
1773+
def test_register_prompt_variables_all_required_by_default(self, make_agent):
1774+
agent = make_agent(user_prompt=_user_msg("Question: {{question}}"))
1775+
assert agent._user_chat_prompt_builder.required_variables == "*"
1776+
1777+
socket = agent.__haystack_input__._sockets_dict["question"]
1778+
assert socket.is_mandatory
1779+
1780+
def test_register_prompt_variables_all_optional_with_none(self, make_agent):
1781+
agent = make_agent(user_prompt=_user_msg("Question: {{question}}"), required_variables=None)
1782+
1783+
socket = agent.__haystack_input__._sockets_dict["question"]
1784+
assert not socket.is_mandatory
1785+
17691786
def test_register_prompt_variables_set_all_variables_as_required(self, make_agent):
17701787
agent = make_agent(user_prompt=_user_msg("Question: {{question}}"), required_variables="*")
17711788
assert agent._user_chat_prompt_builder.required_variables == "*"

test/components/agents/test_agent_hitl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def test_to_dict(self, tools, confirmation_hook, monkeypatch):
119119
"raise_on_tool_invocation_failure": False,
120120
"tool_concurrency_limit": 4,
121121
"tool_streaming_callback_passthrough": False,
122-
"required_variables": None,
122+
"required_variables": "*",
123123
"user_prompt": None,
124124
"hooks": {
125125
"before_tool": [

0 commit comments

Comments
 (0)