diff --git a/MIGRATION.md b/MIGRATION.md index db99b8817f..839489dd0a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -125,3 +125,43 @@ LangfuseConnector("Standalone Agent example") agent = Agent(chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"), tools=[...]) agent.run(messages=[ChatMessage.from_user("What's the weather in Berlin?")]) ``` + +### `PromptBuilder` and `ChatPromptBuilder` template variables are required by default + +**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. + +**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. + +**How to migrate:** + +Before (v2.x): +```python +from haystack.components.builders import PromptBuilder + +# All variables were optional by default; missing values rendered as "". +builder = PromptBuilder(template="Hello, {{ name }}! {{ greeting }}") +builder.run(name="John") # greeting silently becomes "" → "Hello, John! " +``` + +After (v3.0): +```python +from haystack.components.builders import PromptBuilder + +# Option 1: provide every variable (matches the new safe default). +builder = PromptBuilder(template="Hello, {{ name }}! {{ greeting }}") +builder.run(name="John", greeting="Welcome") + +# Option 2: declare which variables are required; everything else stays optional. +builder = PromptBuilder( + template="Hello, {{ name }}! {{ greeting }}", + required_variables=["name"], +) +builder.run(name="John") # greeting renders as "" + +# Option 3: restore the old "all optional" behavior. +builder = PromptBuilder( + template="Hello, {{ name }}! {{ greeting }}", + required_variables=None, +) +builder.run(name="John") # greeting renders as "" +``` diff --git a/haystack/components/builders/chat_prompt_builder.py b/haystack/components/builders/chat_prompt_builder.py index 0e86c65235..f5c529b8be 100644 --- a/haystack/components/builders/chat_prompt_builder.py +++ b/haystack/components/builders/chat_prompt_builder.py @@ -38,9 +38,10 @@ class ChatPromptBuilder: It constructs prompts using static or dynamic templates, which you can update for each pipeline run. - Template variables in the template are optional unless specified otherwise. - If an optional variable isn't provided, it defaults to an empty string. Use `variable` and `required_variables` - to define input types and required variables. + Template variables in the template are required by default. To make any subset of variables optional, + set `required_variables` to an explicit list of the variables that should remain required; any variable + not listed becomes optional and defaults to an empty string when missing. + Set `required_variables` to `None` to mark every variable as optional. ### Usage examples @@ -139,7 +140,7 @@ class ChatPromptBuilder: def __init__( self, template: list[ChatMessage] | str | None = None, - required_variables: list[str] | Literal["*"] | None = None, + required_variables: list[str] | Literal["*"] | None = "*", variables: list[str] | None = None, ) -> None: """ @@ -151,8 +152,10 @@ def __init__( the `init` method` or the `run` method. :param required_variables: List variables that must be provided as input to ChatPromptBuilder. - If a variable listed as required is not provided, an exception is raised. - If set to `"*"`, all variables found in the prompt are required. Optional. + Defaults to `"*"`, which marks every variable found in the prompt as required. + Pass an explicit list to only require a subset of the variables; any variable not listed becomes + optional and is replaced with an empty string in the rendered prompt when missing. + Set to `None` to mark every variable as optional. :param variables: List input variables to use in prompt templates instead of the ones inferred from the `template` parameter. For example, to use more variables during prompt engineering than the ones present @@ -192,10 +195,10 @@ def __init__( if len(self.variables) > 0 and required_variables is None: logger.warning( - "ChatPromptBuilder has {length} prompt variables, but `required_variables` is not set. " - "By default, all prompt variables are treated as optional, which may lead to unintended behavior in " - "multi-branch pipelines. To avoid unexpected execution, ensure that variables intended to be required " - "are explicitly set in `required_variables`.", + "ChatPromptBuilder has {length} prompt variables and `required_variables` is explicitly set to " + "`None`. This treats all prompt variables as optional, which may lead to unintended behavior in " + "multi-branch pipelines. Only set `required_variables` to `None` if you intentionally want all " + "variables to be optional.", length=len(self.variables), ) diff --git a/haystack/components/builders/prompt_builder.py b/haystack/components/builders/prompt_builder.py index d4e6c782a3..f137fd4a3b 100644 --- a/haystack/components/builders/prompt_builder.py +++ b/haystack/components/builders/prompt_builder.py @@ -20,8 +20,9 @@ class PromptBuilder: Renders a prompt filling in any variables so that it can send it to a Generator. The prompt uses Jinja2 template syntax. - The variables in the default template are used as PromptBuilder's input and are all optional. - If they're not provided, they're replaced with an empty string in the rendered prompt. + The variables in the default template are used as PromptBuilder's input and are all required by default. + To make any subset of variables optional, set `required_variables` to an explicit list of the variables that + should remain required. Optional variables are replaced with an empty string in the rendered prompt. To try out different prompts, you can replace the prompt template at runtime by providing a template for each pipeline run invocation. @@ -141,7 +142,7 @@ class PromptBuilder: def __init__( self, template: str, - required_variables: list[str] | Literal["*"] | None = None, + required_variables: list[str] | Literal["*"] | None = "*", variables: list[str] | None = None, ) -> None: """ @@ -151,12 +152,12 @@ def __init__( A prompt template that uses Jinja2 syntax to add variables. For example: `"Summarize this document: {{ documents[0].content }}\\nSummary:"` It's used to render the prompt. - The variables in the default template are input for PromptBuilder and are all optional, - unless explicitly specified. - If an optional variable is not provided, it's replaced with an empty string in the rendered prompt. + The variables in the default template are input for PromptBuilder and are all required by default. :param required_variables: List variables that must be provided as input to PromptBuilder. - If a variable listed as required is not provided, an exception is raised. - If set to `"*"`, all variables found in the prompt are required. Optional. + Defaults to `"*"`, which marks every variable found in the prompt as required. + Pass an explicit list to only require a subset of the variables; any variable not listed becomes + optional and is replaced with an empty string in the rendered prompt when missing. + Set to `None` to mark every variable as optional. :param variables: List input variables to use in prompt templates instead of the ones inferred from the `template` parameter. For example, to use more variables during prompt engineering than the ones present @@ -186,10 +187,10 @@ def __init__( if len(self.variables) > 0 and required_variables is None: logger.warning( - "PromptBuilder has {length} prompt variables, but `required_variables` is not set. " - "By default, all prompt variables are treated as optional, which may lead to unintended behavior in " - "multi-branch pipelines. To avoid unexpected execution, ensure that variables intended to be required " - "are explicitly set in `required_variables`.", + "PromptBuilder has {length} prompt variables and `required_variables` is explicitly set to `None`. " + "This treats all prompt variables as optional, which may lead to unintended behavior in " + "multi-branch pipelines. Only set `required_variables` to `None` if you intentionally want all " + "variables to be optional.", length=len(self.variables), ) diff --git a/releasenotes/notes/prompt-builder-required-by-default-ef29f5ae6356b134.yaml b/releasenotes/notes/prompt-builder-required-by-default-ef29f5ae6356b134.yaml new file mode 100644 index 0000000000..38a2d41e5f --- /dev/null +++ b/releasenotes/notes/prompt-builder-required-by-default-ef29f5ae6356b134.yaml @@ -0,0 +1,22 @@ +--- +upgrade: + - | + ``PromptBuilder`` and ``ChatPromptBuilder`` now treat every Jinja2 template variable as **required by default**. + Previously, variables were optional by default unless explicitly listed in ``required_variables``. + + To check if you're affected: look for ``PromptBuilder(...)`` or ``ChatPromptBuilder(...)`` calls in your code + that do not pass ``required_variables``. If those components relied on missing variables being silently + rendered as empty strings, the behavior depends on how the missing variable was reaching the builder: + + - Calling ``builder.run()`` directly without providing a required variable now raises ``ValueError``. + - In a pipeline, if a required variable is neither provided as a user input nor connected from another + component, ``Pipeline.run`` raises ``ValueError("Missing mandatory input '' for component ''.")``. + - In a pipeline, if a required variable is connected to a sender that never produces output on a given run + (for example, a loop-fed input on the first iteration or a conditional branch that isn't taken), the + pipeline cannot make progress and raises ``PipelineComponentsBlockedError``. + + Migration options: + + - To require only a subset of variables, pass ``required_variables=["var1", "var2"]``. + - To preserve the previous "all optional" behavior, explicitly pass ``required_variables=None``. + - To require everything (the new default), no change is needed. diff --git a/test/components/agents/test_agent.py b/test/components/agents/test_agent.py index c155f3610d..fe9841aed4 100644 --- a/test/components/agents/test_agent.py +++ b/test/components/agents/test_agent.py @@ -254,7 +254,7 @@ def test_to_dict(self, weather_tool, component_tool, monkeypatch): "init_parameters": { "template": "{{parrot}}", "variables": None, - "required_variables": None, + "required_variables": "*", }, }, "name": "parrot", @@ -405,7 +405,7 @@ def test_from_dict(self, monkeypatch): "init_parameters": { "template": "{{parrot}}", "variables": None, - "required_variables": None, + "required_variables": "*", }, }, "name": "parrot", @@ -552,7 +552,7 @@ def test_from_dict_state_schema_none(self, monkeypatch): "init_parameters": { "template": "{{parrot}}", "variables": None, - "required_variables": None, + "required_variables": "*", }, }, "name": "parrot", diff --git a/test/components/builders/test_chat_prompt_builder.py b/test/components/builders/test_chat_prompt_builder.py index e24a79dd21..156da9846a 100644 --- a/test/components/builders/test_chat_prompt_builder.py +++ b/test/components/builders/test_chat_prompt_builder.py @@ -28,11 +28,11 @@ def test_init(self): ChatMessage.from_system("This is a {{ variable2 }}"), ] ) - assert builder.required_variables == [] + assert builder.required_variables == "*" assert builder.template[0].text == "This is a {{ variable }}" assert builder.template[1].text == "This is a {{ variable2 }}" assert builder._variables is None - assert builder._required_variables is None + assert builder._required_variables == "*" # we have inputs that contain: template, template_variables + inferred variables inputs = builder.__haystack_input__._sockets_dict @@ -51,9 +51,9 @@ def test_init_without_template(self): variables = ["var1", "var2"] builder = ChatPromptBuilder(variables=variables) assert builder.template is None - assert builder.required_variables == [] + assert builder.required_variables == "*" assert builder._variables == variables - assert builder._required_variables is None + assert builder._required_variables == "*" # we have inputs that contain: template, template_variables + variables inputs = builder.__haystack_input__._sockets_dict @@ -93,10 +93,10 @@ def test_init_with_custom_variables(self): variables = ["var1", "var2", "var3"] template = [ChatMessage.from_user("Hello, {{ var1 }}, {{ var2 }}!")] builder = ChatPromptBuilder(template=template, variables=variables) - assert builder.required_variables == [] + assert builder.required_variables == "*" assert builder._variables == variables assert builder.template[0].text == "Hello, {{ var1 }}, {{ var2 }}!" - assert builder._required_variables is None + assert builder._required_variables == "*" # we have inputs that contain: template, template_variables + variables inputs = builder.__haystack_input__._sockets_dict @@ -132,11 +132,18 @@ def test_run_without_input(self): res = builder.run() assert res == {"prompt": [ChatMessage.from_user("This is a template without input")]} - def test_run_with_missing_input(self): - builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a {{ variable }}")]) + def test_run_with_missing_optional_input(self): + builder = ChatPromptBuilder( + template=[ChatMessage.from_user("This is a {{ variable }}")], required_variables=None + ) res = builder.run() assert res == {"prompt": [ChatMessage.from_user("This is a ")]} + def test_run_with_missing_required_input_default(self): + builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a {{ variable }}")]) + with pytest.raises(ValueError, match="variable"): + builder.run() + def test_run_with_missing_required_input(self): builder = ChatPromptBuilder( 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): variables = ["var1", "var2", "var3"] template = [ChatMessage.from_user("Hello, {{ name }}! {{ var1 }}")] - builder = ChatPromptBuilder(template=template, variables=variables) + builder = ChatPromptBuilder(template=template, variables=variables, required_variables=None) template_variables = {"name": "John"} expected_result = {"prompt": [ChatMessage.from_user("Hello, John! How are you?")]} @@ -173,7 +180,7 @@ def test_run_with_variables(self): def test_run_with_variables_and_runtime_template(self): variables = ["var1", "var2", "var3"] - builder = ChatPromptBuilder(variables=variables) + builder = ChatPromptBuilder(variables=variables, required_variables=None) template = [ChatMessage.from_user("Hello, {{ name }}! {{ var1 }}")] template_variables = {"name": "John"} @@ -209,7 +216,7 @@ def test_run_overwriting_default_template_with_variables(self): variables = ["var1", "var2", "name"] default_template = [ChatMessage.from_user("Hello, {{ name }}!")] - builder = ChatPromptBuilder(template=default_template, variables=variables) + builder = ChatPromptBuilder(template=default_template, variables=variables, required_variables=None) template = [ChatMessage.from_user("Hello, {{ var1 }} {{ name }}!")] expected_result = {"prompt": [ChatMessage.from_user("Hello, Big John!")]} @@ -346,7 +353,18 @@ def test_example_in_pipeline_simple(self): } assert result == expected_dynamic - def test_warning_no_required_variables(self, caplog): + def test_warning_when_required_variables_explicitly_none(self, caplog): + with caplog.at_level(logging.WARNING): + _ = ChatPromptBuilder( + template=[ + ChatMessage.from_system("Write your response in this language:{{language}}"), + ChatMessage.from_user("Tell me about {{location}}"), + ], + required_variables=None, + ) + assert "explicitly set to `None`" in caplog.text + + def test_no_warning_with_default_required_variables(self, caplog): with caplog.at_level(logging.WARNING): _ = ChatPromptBuilder( template=[ @@ -354,7 +372,7 @@ def test_warning_no_required_variables(self, caplog): ChatMessage.from_user("Tell me about {{location}}"), ] ) - assert "ChatPromptBuilder has 2 prompt variables, but `required_variables` is not set. " in caplog.text + assert "required_variables" not in caplog.text class TestChatPromptBuilderWithJinja2TimeExtension: @@ -697,7 +715,7 @@ def test_from_dict_template_none(self): assert comp.template is None assert comp._variables is None - assert comp._required_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 }}")] @@ -722,7 +740,7 @@ def test_init(self): assert builder.template == template assert builder._variables is None - assert builder._required_variables is None + assert builder._required_variables == "*" assert builder.variables == ["name"] def test_init_with_invalid_template(self): @@ -774,13 +792,13 @@ def test_run_without_input(self): result = builder.run() assert result["prompt"] == [ChatMessage.from_user("Hello, my name is Lukas!")] - def test_run_with_missing_input(self): + def test_run_with_missing_optional_input(self): template = """ {% message role="user" %} Hello, my name is {{name}}! {% endmessage %} """ - builder = ChatPromptBuilder(template=template) + builder = ChatPromptBuilder(template=template, required_variables=None) result = builder.run() assert result["prompt"] == [ChatMessage.from_user("Hello, my name is !")] @@ -826,7 +844,7 @@ def test_run_overwriting_default_template(self): Hello, my name is {{name}}! {% endmessage %} """ - builder = ChatPromptBuilder(template=initial_template) + builder = ChatPromptBuilder(template=initial_template, required_variables=None) runtime_template = """ {% message role="user" %} diff --git a/test/components/builders/test_prompt_builder.py b/test/components/builders/test_prompt_builder.py index e07cc7859d..4357e63c29 100644 --- a/test/components/builders/test_prompt_builder.py +++ b/test/components/builders/test_prompt_builder.py @@ -20,10 +20,10 @@ class TestPromptBuilder: def test_init(self): builder = PromptBuilder(template="This is a {{ variable }}") assert builder.template is not None - assert builder.required_variables == [] + assert builder.required_variables == "*" assert builder._template_string == "This is a {{ variable }}" assert builder._variables is None - assert builder._required_variables is None + assert builder._required_variables == "*" # we have inputs that contain: template, template_variables + inferred variables inputs = builder.__haystack_input__._sockets_dict @@ -62,10 +62,10 @@ def test_init_with_custom_variables(self): template = "Hello, {{ var1 }}, {{ var2 }}!" builder = PromptBuilder(template=template, variables=variables) assert builder.template is not None - assert builder.required_variables == [] + assert builder.required_variables == "*" assert builder._variables == variables assert builder._template_string == "Hello, {{ var1 }}, {{ var2 }}!" - assert builder._required_variables is None + assert builder._required_variables == "*" # we have inputs that contain: template, template_variables + variables inputs = builder.__haystack_input__._sockets_dict @@ -100,7 +100,7 @@ def test_to_dict_without_optional_params(self): res = builder.to_dict() assert res == { "type": "haystack.components.builders.prompt_builder.PromptBuilder", - "init_parameters": {"template": "This is a {{ variable }}", "variables": None, "required_variables": None}, + "init_parameters": {"template": "This is a {{ variable }}", "variables": None, "required_variables": "*"}, } def test_run(self): @@ -123,11 +123,16 @@ def test_run_without_input(self): res = builder.run() assert res == {"prompt": "This is a template without input"} - def test_run_with_missing_input(self): - builder = PromptBuilder(template="This is a {{ variable }}") + def test_run_with_missing_optional_input(self): + builder = PromptBuilder(template="This is a {{ variable }}", required_variables=None) res = builder.run() assert res == {"prompt": "This is a "} + def test_run_with_missing_required_input_default(self): + builder = PromptBuilder(template="This is a {{ variable }}") + with pytest.raises(ValueError, match="variable"): + builder.run() + def test_run_with_missing_required_input(self): builder = PromptBuilder(template="This is a {{ foo }}, not a {{ bar }}", required_variables=["foo", "bar"]) with pytest.raises(ValueError, match="foo"): @@ -149,45 +154,23 @@ def test_run_with_missing_required_input_using_star(self): def test_run_with_variables(self): variables = ["var1", "var2", "var3"] template = "Hello, {{ name }}! {{ var1 }}" - - builder = PromptBuilder(template=template, variables=variables) - - template_variables = {"name": "John"} - expected_result = {"prompt": "Hello, John! How are you?"} - - assert builder.run(template_variables=template_variables, var1="How are you?") == expected_result + builder = PromptBuilder(template=template, variables=variables, required_variables=["name", "var1"]) + assert builder.run(name="John", var1="How are you?") == {"prompt": "Hello, John! How are you?"} def test_run_overwriting_default_template(self): - default_template = "Hello, {{ name }}!" - - builder = PromptBuilder(template=default_template) - - template = "Hello, {{ var1 }}{{ name }}!" - expected_result = {"prompt": "Hello, John!"} - - assert builder.run(template, name="John") == expected_result + builder = PromptBuilder(template="Hello, {{ name }}!") + assert builder.run("Hello, {{ var1 }}{{ name }}!!", name="John") == {"prompt": "Hello, John!!"} def test_run_overwriting_default_template_with_template_variables(self): - default_template = "Hello, {{ name }}!" - - builder = PromptBuilder(template=default_template) - + builder = PromptBuilder(template="Hello, {{ name }}!") template = "Hello, {{ var1 }} {{ name }}!" - template_variables = {"var1": "Big"} - expected_result = {"prompt": "Hello, Big John!"} - - assert builder.run(template, template_variables, name="John") == expected_result + assert builder.run(template, var1="Big", name="John") == {"prompt": "Hello, Big John!"} def test_run_overwriting_default_template_with_variables(self): variables = ["var1", "var2", "name"] - default_template = "Hello, {{ name }}!" - - builder = PromptBuilder(template=default_template, variables=variables) - + builder = PromptBuilder(template="Hello, {{ name }}!", variables=variables, required_variables=["name"]) template = "Hello, {{ var1 }} {{ name }}!" - expected_result = {"prompt": "Hello, Big John!"} - - assert builder.run(template, name="John", var1="Big") == expected_result + assert builder.run(template, name="John", var1="Big") == {"prompt": "Hello, Big John!"} def test_run_with_invalid_template(self): builder = PromptBuilder(template="Hello, {{ name }}!") @@ -269,10 +252,15 @@ def test_example_in_pipeline_simple(self): } assert result == expected_dynamic - def test_warning_no_required_variables(self, caplog): + def test_warning_when_required_variables_explicitly_none(self, caplog): + with caplog.at_level(logging.WARNING): + _ = PromptBuilder(template="This is a {{ variable }}", required_variables=None) + assert "explicitly set to `None`" in caplog.text + + def test_no_warning_with_default_required_variables(self, caplog): with caplog.at_level(logging.WARNING): _ = PromptBuilder(template="This is a {{ variable }}") - assert "but `required_variables` is not set." in caplog.text + assert "required_variables" not in caplog.text def test_variables_correct_with_assignment(self) -> None: template = """{% if existing_documents is not none %} diff --git a/test/core/pipeline/breakpoints/test_pipeline_breakpoints_list_joiner.py b/test/core/pipeline/breakpoints/test_pipeline_breakpoints_list_joiner.py index 007b633a66..68a488cff0 100644 --- a/test/core/pipeline/breakpoints/test_pipeline_breakpoints_list_joiner.py +++ b/test/core/pipeline/breakpoints/test_pipeline_breakpoints_list_joiner.py @@ -38,9 +38,11 @@ def list_joiner_pipeline(self): feedback_message = [ChatMessage.from_system(feedback_prompt)] pipe = Pipeline() - pipe.add_component("prompt_builder", ChatPromptBuilder(template=user_message)) + pipe.add_component("prompt_builder", ChatPromptBuilder(template=user_message, required_variables=None)) pipe.add_component("llm", FakeChatGenerator("Nuclear physics is the study of atomic nuclei.")) - pipe.add_component("feedback_prompt_builder", ChatPromptBuilder(template=feedback_message)) + pipe.add_component( + "feedback_prompt_builder", ChatPromptBuilder(template=feedback_message, required_variables=None) + ) pipe.add_component("feedback_llm", FakeChatGenerator("Score: 8/10. Concise and accurate.")) pipe.add_component("list_joiner", ListJoiner(list[ChatMessage])) diff --git a/test/core/pipeline/breakpoints/test_pipeline_breakpoints_loops.py b/test/core/pipeline/breakpoints/test_pipeline_breakpoints_loops.py index 0fecb9568f..d1832284e6 100644 --- a/test/core/pipeline/breakpoints/test_pipeline_breakpoints_loops.py +++ b/test/core/pipeline/breakpoints/test_pipeline_breakpoints_loops.py @@ -91,7 +91,10 @@ def validation_loop_pipeline(self): ) pipeline = Pipeline(max_runs_per_component=5) - pipeline.add_component(instance=ChatPromptBuilder(template=prompt_template), name="prompt_builder") + pipeline.add_component( + instance=ChatPromptBuilder(template=prompt_template, required_variables=["passage", "schema"]), + name="prompt_builder", + ) pipeline.add_component(instance=FakeChatGenerator(response=response_json), name="llm") pipeline.add_component(instance=OutputValidator(pydantic_model=CitiesData), name="output_validator") diff --git a/test/core/pipeline/features/test_run.py b/test/core/pipeline/features/test_run.py index 57a0881128..10ae016b33 100644 --- a/test/core/pipeline/features/test_run.py +++ b/test/core/pipeline/features/test_run.py @@ -997,7 +997,7 @@ def fake_generator_run( pipe = pipeline_class(max_runs_per_component=2) - pipe.add_component("prompt_builder", PromptBuilder(template=template)) + pipe.add_component("prompt_builder", PromptBuilder(template=template, required_variables=["query"])) pipe.add_component("generator", FakeGenerator()) pipe.add_component("router", router) @@ -1348,7 +1348,8 @@ def pipeline_that_has_a_component_with_default_inputs_that_doesnt_receive_anythi If the question cannot be answered given the provided table and columns, return 'no_answer' The query is to be answered for the table is called 'absenteeism' with the following Columns: {{ columns }}; -Answer:""" +Answer:""", + required_variables=["question", "columns"], ) @component @@ -1385,7 +1386,8 @@ def run(self, query: str) -> dict[str, str]: fallback_prompt = PromptBuilder( template="""User entered a query that cannot be answered with the given table. The query was: {{ question }} and the table had columns: {{ columns }}. -Let the user know why the question cannot be answered""" +Let the user know why the question cannot be answered""", + required_variables=["question"], ) pipeline = pipeline_class(max_runs_per_component=1) @@ -1486,7 +1488,7 @@ def pipeline_that_has_a_loop_and_a_component_with_default_inputs_that_doesnt_rec Correct the output and try again. Just return the corrected output without any extra explanations. {%- endif -%}""" ) - prompt_builder = PromptBuilder(template=template) + prompt_builder = PromptBuilder(template=template, required_variables=None) @component class FakeOutputValidator: @@ -2582,7 +2584,10 @@ def run(self, prompt: str) -> dict[str, str]: pipeline = pipeline_class(max_runs_per_component=2) pipeline.add_component("prompt_cleaner", PromptCleaner()) - pipeline.add_component("prompt_builder", PromptBuilder(template="", variables=["question", "invalid_replies"])) + pipeline.add_component( + "prompt_builder", + PromptBuilder(template="", variables=["question", "invalid_replies"], required_variables=["question"]), + ) pipeline.add_component("llm", FakeGenerator()) pipeline.add_component( "answer_validator", @@ -3152,8 +3157,8 @@ def has_feedback_loop(pipeline_class): pipe = pipeline_class(max_runs_per_component=100) pipe.add_component("code_llm", FixedGenerator(replies=["invalid code", "valid code"])) - pipe.add_component("code_prompt", PromptBuilder(template=code_prompt_template)) - pipe.add_component("feedback_prompt", PromptBuilder(template=feedback_prompt_template)) + pipe.add_component("code_prompt", PromptBuilder(template=code_prompt_template, required_variables=["task"])) + pipe.add_component("feedback_prompt", PromptBuilder(template=feedback_prompt_template, required_variables=["code"])) pipe.add_component("feedback_llm", FixedGenerator(replies=["FAIL", "PASS"])) pipe.add_component("router", router) pipe.add_component( @@ -3297,8 +3302,8 @@ def has_non_standard_order_loop(pipeline_class): "concatenator", OutputAdapter(template="{{current_prompt[0] + '\n' + feedback[0]}}", output_type=str) ) pipe.add_component("code_llm", FixedGenerator(replies=["invalid code", "valid code"])) - pipe.add_component("code_prompt", PromptBuilder(template=code_prompt_template)) - pipe.add_component("feedback_prompt", PromptBuilder(template=feedback_prompt_template)) + pipe.add_component("code_prompt", PromptBuilder(template=code_prompt_template, required_variables=["task"])) + pipe.add_component("feedback_prompt", PromptBuilder(template=feedback_prompt_template, required_variables=["code"])) pipe.add_component("feedback_llm", FixedGenerator(replies=["FAIL", "PASS"])) pipe.add_component("router", router) pipe.add_component("answer_builder", AnswerBuilder()) @@ -3465,7 +3470,7 @@ def run(self, replies: list[str]) -> dict[str, str]: pipe = pipeline_class(max_runs_per_component=8) - pipe.add_component("code_prompt", PromptBuilder(template=code_prompt_template)) + pipe.add_component("code_prompt", PromptBuilder(template=code_prompt_template, required_variables=["task"])) pipe.add_component("joiner", BranchJoiner(type_=str)) pipe.add_component( "code_llm", FixedGenerator(replies=["Edit: file_1.py", "Edit: file_2.py", "Edit: file_3.py", "Task finished!"]) diff --git a/test/tools/test_component_tool.py b/test/tools/test_component_tool.py index 81e37d0252..90bb59a365 100644 --- a/test/tools/test_component_tool.py +++ b/test/tools/test_component_tool.py @@ -356,7 +356,7 @@ def test_from_component_with_dynamic_input_types(self): assert tool.parameters == { "description": "Renders the prompt template with the provided variables.", "properties": { - "name": {"default": "", "description": "Input 'name' for the component."}, + "name": {"description": "Input 'name' for the component."}, "template": { "anyOf": [{"type": "string"}, {"type": "null"}], "default": None, @@ -369,6 +369,7 @@ def test_from_component_with_dynamic_input_types(self): "description": "An optional dictionary of template variables to overwrite the pipeline variables.", }, }, + "required": ["name"], "type": "object", }