diff --git a/haystack/components/builders/chat_prompt_builder.py b/haystack/components/builders/chat_prompt_builder.py new file mode 100644 index 0000000000..b49509d9c9 --- /dev/null +++ b/haystack/components/builders/chat_prompt_builder.py @@ -0,0 +1,223 @@ +# SPDX-FileCopyrightText: 2022-present deepset GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any, Dict, List, Optional, Set + +from jinja2 import Template, meta + +from haystack import component, logging +from haystack.dataclasses.chat_message import ChatMessage, ChatRole + +logger = logging.getLogger(__name__) + + +@component +class ChatPromptBuilder: + """ + ChatPromptBuilder is a component that renders a chat prompt from a template string using Jinja2 templates. + + It is designed to construct prompts for the pipeline using static or dynamic templates: Users can change + the prompt template at runtime by providing a new template for each pipeline run invocation if needed. + + The template variables found in the init template string are used as input types for the component and are all optional, + unless explicitly specified. If an optional template variable is not provided as an input, it will be replaced with + an empty string in the rendered prompt. Use `variable` and `required_variables` to specify the input types and + required variables. + + Usage example with static prompt template: + ```python + template = [ChatMessage.from_user("Translate to {{ target_language }}. Context: {{ snippet }}; Translation:")] + builder = ChatPromptBuilder(template=template) + builder.run(target_language="spanish", snippet="I can't speak spanish.") + ``` + + Usage example of overriding the static template at runtime: + ```python + template = [ChatMessage.from_user("Translate to {{ target_language }}. Context: {{ snippet }}; Translation:")] + builder = ChatPromptBuilder(template=template) + builder.run(target_language="spanish", snippet="I can't speak spanish.") + + summary_template = [ChatMessage.from_user("Translate to {{ target_language }} and summarize. Context: {{ snippet }}; Summary:")] + builder.run(target_language="spanish", snippet="I can't speak spanish.", template=summary_template) + ``` + + Usage example with dynamic prompt template: + ```python + from haystack.components.builders import ChatPromptBuilder + from haystack.components.generators.chat import OpenAIChatGenerator + from haystack.dataclasses import ChatMessage + from haystack import Pipeline + from haystack.utils import Secret + + # no parameter init, we don't use any runtime template variables + prompt_builder = ChatPromptBuilder() + llm = OpenAIChatGenerator(api_key=Secret.from_token(""), model="gpt-3.5-turbo") + + pipe = Pipeline() + pipe.add_component("prompt_builder", prompt_builder) + pipe.add_component("llm", llm) + pipe.connect("prompt_builder.prompt", "llm.messages") + + location = "Berlin" + language = "English" + system_message = ChatMessage.from_system("You are an assistant giving information to tourists in {{language}}") + messages = [system_message, ChatMessage.from_user("Tell me about {{location}}")] + + res = pipe.run(data={"prompt_builder": {"template_variables": {"location": location, "language": language}, + "template": messages}}) + print(res) + + >> {'llm': {'replies': [ChatMessage(content="Berlin is the capital city of Germany and one of the most vibrant + and diverse cities in Europe. Here are some key things to know...Enjoy your time exploring the vibrant and dynamic + capital of Germany!", role=, name=None, meta={'model': 'gpt-3.5-turbo-0613', + 'index': 0, 'finish_reason': 'stop', 'usage': {'prompt_tokens': 27, 'completion_tokens': 681, 'total_tokens': + 708}})]}} + + + messages = [system_message, ChatMessage.from_user("What's the weather forecast for {{location}} in the next + {{day_count}} days?")] + + res = pipe.run(data={"prompt_builder": {"template_variables": {"location": location, "day_count": "5"}, + "template": messages}}) + + print(res) + >> {'llm': {'replies': [ChatMessage(content="Here is the weather forecast for Berlin in the next 5 + days:\n\nDay 1: Mostly cloudy with a high of 22°C (72°F) and...so it's always a good idea to check for updates + closer to your visit.", role=, name=None, meta={'model': 'gpt-3.5-turbo-0613', + 'index': 0, 'finish_reason': 'stop', 'usage': {'prompt_tokens': 37, 'completion_tokens': 201, + 'total_tokens': 238}})]}} + ``` + + Note how in the example above, we can dynamically change the prompt template by providing a new template to the + run method of the pipeline. + + """ + + def __init__( + self, + template: Optional[List[ChatMessage]] = None, + required_variables: Optional[List[str]] = None, + variables: Optional[List[str]] = None, + ): + """ + Constructs a ChatPromptBuilder component. + + :param template: + A list of `ChatMessage` instances. All user and system messages are treated as potentially having jinja2 + templates and are rendered with the provided template variables. If not provided, the template + must be provided at runtime using the `template` parameter of the `run` method. + :param required_variables: An optional list of input variables that must be provided at all times. + If not provided, an exception will be raised. + :param variables: + A list of template variable names you can use in prompt construction. For example, + if `variables` contains the string `documents`, the component will create an input called + `documents` of type `Any`. These variable names are used to resolve variables and their values during + pipeline execution. The values associated with variables from the pipeline runtime are then injected into + template placeholders of a prompt text template that is provided to the `run` method. + If not provided, variables are inferred from `template`. + """ + self._variables = variables + self._required_variables = required_variables + self.required_variables = required_variables or [] + self.template = template + variables = variables or [] + if template and not variables: + for message in template: + if message.is_from(ChatRole.USER) or message.is_from(ChatRole.SYSTEM): + # infere variables from template + msg_template = Template(message.content) + ast = msg_template.environment.parse(message.content) + template_variables = meta.find_undeclared_variables(ast) + variables += list(template_variables) + + # setup inputs + static_input_slots = {"template": Optional[str], "template_variables": Optional[Dict[str, Any]]} + component.set_input_types(self, **static_input_slots) + for var in variables: + if var in self.required_variables: + component.set_input_type(self, var, Any) + else: + component.set_input_type(self, var, Any, "") + + @component.output_types(prompt=List[ChatMessage]) + def run( + self, + template: Optional[List[ChatMessage]] = None, + template_variables: Optional[Dict[str, Any]] = None, + **kwargs, + ): + """ + Executes the prompt building process. + + It applies the template variables to render the final prompt. You can provide variables either via pipeline + (set through `variables` or inferred from `template` at initialization) or via additional template variables + set directly to this method. On collision, the variables provided directly to this method take precedence. + + :param template: + An optional list of ChatMessages to overwrite ChatPromptBuilder's default template. If None, the default template + provided at initialization is used. + :param template_variables: + An optional dictionary of template variables. These are additional variables users can provide directly + to this method in contrast to pipeline variables. + :param kwargs: + Pipeline variables (typically resolved from a pipeline) which are merged with the provided template variables. + + :returns: A dictionary with the following keys: + - `prompt`: The updated list of `ChatMessage` instances after rendering the found templates. + :raises ValueError: + If `chat_messages` is empty or contains elements that are not instances of `ChatMessage`. + """ + kwargs = kwargs or {} + template_variables = template_variables or {} + template_variables_combined = {**kwargs, **template_variables} + + if template is None: + template = self.template + + if not template: + raise ValueError( + f"The {self.__class__.__name__} requires a non-empty list of ChatMessage instances. " + f"Please provide a valid list of ChatMessage instances to render the prompt." + ) + + if not all(isinstance(message, ChatMessage) for message in template): + raise ValueError( + f"The {self.__class__.__name__} expects a list containing only ChatMessage instances. " + f"The provided list contains other types. Please ensure that all elements in the list " + f"are ChatMessage instances." + ) + + processed_messages = [] + for message in template: + if message.is_from(ChatRole.USER) or message.is_from(ChatRole.SYSTEM): + self._validate_variables(set(template_variables_combined.keys())) + compiled_template = Template(message.content) + rendered_content = compiled_template.render(template_variables_combined) + rendered_message = ( + ChatMessage.from_user(rendered_content) + if message.is_from(ChatRole.USER) + else ChatMessage.from_system(rendered_content) + ) + processed_messages.append(rendered_message) + else: + processed_messages.append(message) + + return {"prompt": processed_messages} + + def _validate_variables(self, provided_variables: Set[str]): + """ + Checks if all the required template variables are provided. + + :param provided_variables: + A set of provided template variables. + :raises ValueError: + If no template is provided or if all the required template variables are not provided. + """ + missing_variables = [var for var in self.required_variables if var not in provided_variables] + if missing_variables: + missing_vars_str = ", ".join(missing_variables) + raise ValueError( + f"Missing required input variables in ChatPromptBuilder: {missing_vars_str}. " + f"Required variables: {self.required_variables}. Provided variables: {provided_variables}." + ) diff --git a/haystack/components/builders/dynamic_chat_prompt_builder.py b/haystack/components/builders/dynamic_chat_prompt_builder.py index 719e2d8c72..a6cd2413b5 100644 --- a/haystack/components/builders/dynamic_chat_prompt_builder.py +++ b/haystack/components/builders/dynamic_chat_prompt_builder.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 +import warnings from typing import Any, Dict, List, Optional, Set from jinja2 import Template, meta @@ -84,6 +85,11 @@ def __init__(self, runtime_variables: Optional[List[str]] = None): pipeline execution. The values associated with variables from the pipeline runtime are then injected into template placeholders of a ChatMessage that is provided to the `run` method. """ + warnings.warn( + "`DynamicChatPromptBuilder` is deprecated and will be removed in Haystack 2.3.0." + "Use `ChatPromptBuilder` instead.", + DeprecationWarning, + ) runtime_variables = runtime_variables or [] # setup inputs diff --git a/releasenotes/notes/add-chatpromptbuilder-19acd18a6486909d.yaml b/releasenotes/notes/add-chatpromptbuilder-19acd18a6486909d.yaml new file mode 100644 index 0000000000..8d80cb47d1 --- /dev/null +++ b/releasenotes/notes/add-chatpromptbuilder-19acd18a6486909d.yaml @@ -0,0 +1,7 @@ +--- +enhancements: + - | + `ChatPromptBuilder` now supports changing its template at runtime. This allows you to define a default template and then change it based on your needs at runtime. +deprecations: + - | + `DynamicChatPromptBuilder` has been deprecated as `ChatPromptBuilder` fully covers its functionality. Use `ChatPromptBuilder` instead. diff --git a/test/components/builders/test_chat_prompt_builder.py b/test/components/builders/test_chat_prompt_builder.py new file mode 100644 index 0000000000..406e40b739 --- /dev/null +++ b/test/components/builders/test_chat_prompt_builder.py @@ -0,0 +1,496 @@ +from typing import Any, Dict, List, Optional +from jinja2 import TemplateSyntaxError +import pytest + +from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder +from haystack import component +from haystack.core.pipeline.pipeline import Pipeline +from haystack.dataclasses.chat_message import ChatMessage +from haystack.dataclasses.document import Document + + +class TestChatPromptBuilder: + def test_init(self): + builder = ChatPromptBuilder( + template=[ + ChatMessage.from_user(content="This is a {{ variable }}"), + ChatMessage.from_system(content="This is a {{ variable2 }}"), + ] + ) + assert builder.required_variables == [] + assert builder.template[0].content == "This is a {{ variable }}" + assert builder.template[1].content == "This is a {{ variable2 }}" + assert builder._variables is None + assert builder._required_variables is None + + # we have inputs that contain: template, template_variables + inferred variables + inputs = builder.__haystack_input__._sockets_dict + assert set(inputs.keys()) == {"template", "template_variables", "variable", "variable2"} + assert inputs["template"].type == Optional[List[ChatMessage]] + assert inputs["template_variables"].type == Optional[Dict[str, Any]] + assert inputs["variable"].type == Any + assert inputs["variable2"].type == Any + + # response is always prompt + outputs = builder.__haystack_output__._sockets_dict + assert set(outputs.keys()) == {"prompt"} + assert outputs["prompt"].type == List[ChatMessage] + + def test_init_without_template(self): + variables = ["var1", "var2"] + builder = ChatPromptBuilder(variables=variables) + assert builder.template is None + assert builder.required_variables == [] + assert builder._variables == variables + assert builder._required_variables is None + + # we have inputs that contain: template, template_variables + variables + inputs = builder.__haystack_input__._sockets_dict + assert set(inputs.keys()) == {"template", "template_variables", "var1", "var2"} + assert inputs["template"].type == Optional[List[ChatMessage]] + assert inputs["template_variables"].type == Optional[Dict[str, Any]] + assert inputs["var1"].type == Any + assert inputs["var2"].type == Any + + # response is always prompt + outputs = builder.__haystack_output__._sockets_dict + assert set(outputs.keys()) == {"prompt"} + assert outputs["prompt"].type == List[ChatMessage] + + def test_init_with_required_variables(self): + builder = ChatPromptBuilder( + template=[ChatMessage.from_user("This is a {{ variable }}")], required_variables=["variable"] + ) + assert builder.required_variables == ["variable"] + assert builder.template[0].content == "This is a {{ variable }}" + assert builder._variables is None + assert builder._required_variables == ["variable"] + + # we have inputs that contain: template, template_variables + inferred variables + inputs = builder.__haystack_input__._sockets_dict + assert set(inputs.keys()) == {"template", "template_variables", "variable"} + assert inputs["template"].type == Optional[List[ChatMessage]] + assert inputs["template_variables"].type == Optional[Dict[str, Any]] + assert inputs["variable"].type == Any + + # response is always prompt + outputs = builder.__haystack_output__._sockets_dict + assert set(outputs.keys()) == {"prompt"} + assert outputs["prompt"].type == List[ChatMessage] + + 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._variables == variables + assert builder.template[0].content == "Hello, {{ var1 }}, {{ var2 }}!" + assert builder._required_variables is None + + # we have inputs that contain: template, template_variables + variables + inputs = builder.__haystack_input__._sockets_dict + assert set(inputs.keys()) == {"template", "template_variables", "var1", "var2", "var3"} + assert inputs["template"].type == Optional[List[ChatMessage]] + assert inputs["template_variables"].type == Optional[Dict[str, Any]] + assert inputs["var1"].type == Any + assert inputs["var2"].type == Any + assert inputs["var3"].type == Any + + # response is always prompt + outputs = builder.__haystack_output__._sockets_dict + assert set(outputs.keys()) == {"prompt"} + assert outputs["prompt"].type == List[ChatMessage] + + def test_run(self): + builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a {{ variable }}")]) + res = builder.run(variable="test") + assert res == {"prompt": [ChatMessage.from_user("This is a test")]} + + def test_run_template_variable(self): + builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a {{ variable }}")]) + res = builder.run(template_variables={"variable": "test"}) + assert res == {"prompt": [ChatMessage.from_user("This is a test")]} + + def test_run_template_variable_overrides_variable(self): + builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a {{ variable }}")]) + res = builder.run(template_variables={"variable": "test_from_template_var"}, variable="test") + assert res == {"prompt": [ChatMessage.from_user("This is a test_from_template_var")]} + + def test_run_without_input(self): + builder = ChatPromptBuilder(template=[ChatMessage.from_user("This is a template without input")]) + 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 }}")]) + res = builder.run() + assert res == {"prompt": [ChatMessage.from_user("This is a ")]} + + 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"] + ) + with pytest.raises(ValueError, match="foo"): + builder.run(bar="bar") + with pytest.raises(ValueError, match="bar"): + builder.run(foo="foo") + with pytest.raises(ValueError, match="foo, bar"): + builder.run() + + def test_run_with_variables(self): + variables = ["var1", "var2", "var3"] + template = [ChatMessage.from_user("Hello, {{ name }}! {{ var1 }}")] + + builder = ChatPromptBuilder(template=template, variables=variables) + + template_variables = {"name": "John"} + expected_result = {"prompt": [ChatMessage.from_user("Hello, John! How are you?")]} + + assert builder.run(template_variables=template_variables, var1="How are you?") == expected_result + + def test_run_with_variables_and_runtime_template(self): + variables = ["var1", "var2", "var3"] + + builder = ChatPromptBuilder(variables=variables) + + template = [ChatMessage.from_user("Hello, {{ name }}! {{ var1 }}")] + template_variables = {"name": "John"} + expected_result = {"prompt": [ChatMessage.from_user("Hello, John! How are you?")]} + + assert ( + builder.run(template=template, template_variables=template_variables, var1="How are you?") + == expected_result + ) + + def test_run_overwriting_default_template(self): + default_template = [ChatMessage.from_user("Hello, {{ name }}!")] + + builder = ChatPromptBuilder(template=default_template) + + template = [ChatMessage.from_user("Hello, {{ var1 }}{{ name }}!")] + expected_result = {"prompt": [ChatMessage.from_user("Hello, John!")]} + + assert builder.run(template, name="John") == expected_result + + def test_run_overwriting_default_template_with_template_variables(self): + default_template = [ChatMessage.from_user("Hello, {{ name }}!")] + + builder = ChatPromptBuilder(template=default_template) + + template = [ChatMessage.from_user("Hello, {{ var1 }} {{ name }}!")] + template_variables = {"var1": "Big"} + expected_result = {"prompt": [ChatMessage.from_user("Hello, Big John!")]} + + assert builder.run(template, template_variables, name="John") == expected_result + + 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) + + template = [ChatMessage.from_user("Hello, {{ var1 }} {{ name }}!")] + expected_result = {"prompt": [ChatMessage.from_user("Hello, Big John!")]} + + assert builder.run(template, name="John", var1="Big") == expected_result + + def test_run_with_invalid_template(self): + builder = ChatPromptBuilder() + + template = [ChatMessage.from_user("Hello, {{ name }!")] + template_variables = {"name": "John"} + with pytest.raises(TemplateSyntaxError): + builder.run(template, template_variables) + + def test_init_with_invalid_template(self): + template = [ChatMessage.from_user("Hello, {{ name }!")] + with pytest.raises(TemplateSyntaxError): + ChatPromptBuilder(template) + + def test_run_without_template(self): + prompt_builder = ChatPromptBuilder() + with pytest.raises( + ValueError, match="The ChatPromptBuilder requires a non-empty list of ChatMessage instances" + ): + prompt_builder.run() + + def test_run_with_empty_chat_message_list(self): + prompt_builder = ChatPromptBuilder(template=[], variables=["documents"]) + with pytest.raises( + ValueError, match="The ChatPromptBuilder requires a non-empty list of ChatMessage instances" + ): + prompt_builder.run() + + def test_chat_message_list_with_mixed_object_list(self): + prompt_builder = ChatPromptBuilder( + template=[ChatMessage.from_user("Hello"), "there world"], variables=["documents"] + ) + with pytest.raises( + ValueError, match="The ChatPromptBuilder expects a list containing only ChatMessage instances" + ): + prompt_builder.run() + + def test_provided_template_variables(self): + prompt_builder = ChatPromptBuilder(variables=["documents"], required_variables=["city"]) + + # both variables are provided + prompt_builder._validate_variables({"name", "city"}) + + # provided variables are a superset of the required variables + prompt_builder._validate_variables({"name", "city", "age"}) + + with pytest.raises(ValueError): + prompt_builder._validate_variables({"name"}) + + def test_example_in_pipeline(self): + default_template = [ + ChatMessage.from_user("Here is the document: {{documents[0].content}} \\n Answer: {{query}}") + ] + prompt_builder = ChatPromptBuilder(template=default_template, variables=["documents"]) + + @component + class DocumentProducer: + @component.output_types(documents=List[Document]) + def run(self, doc_input: str): + return {"documents": [Document(content=doc_input)]} + + pipe = Pipeline() + pipe.add_component("doc_producer", DocumentProducer()) + pipe.add_component("prompt_builder", prompt_builder) + pipe.connect("doc_producer.documents", "prompt_builder.documents") + + template = [ChatMessage.from_user("Here is the document: {{documents[0].content}} \n Query: {{query}}")] + result = pipe.run( + data={ + "doc_producer": {"doc_input": "Hello world, I live in Berlin"}, + "prompt_builder": { + "template": template, + "template_variables": {"query": "Where does the speaker live?"}, + }, + } + ) + + assert result == { + "prompt_builder": { + "prompt": [ + ChatMessage.from_user( + "Here is the document: Hello world, I live in Berlin \n Query: Where does the speaker live?" + ) + ] + } + } + + def test_example_in_pipeline_simple(self): + default_template = [ChatMessage.from_user("This is the default prompt:\n Query: {{query}}")] + prompt_builder = ChatPromptBuilder(template=default_template) + + pipe = Pipeline() + pipe.add_component("prompt_builder", prompt_builder) + + # using the default prompt + result = pipe.run(data={"query": "Where does the speaker live?"}) + expected_default = { + "prompt_builder": { + "prompt": [ChatMessage.from_user("This is the default prompt:\n Query: Where does the speaker live?")] + } + } + assert result == expected_default + + # using the dynamic prompt + result = pipe.run( + data={ + "query": "Where does the speaker live?", + "template": [ChatMessage.from_user("This is the dynamic prompt:\n Query: {{query}}")], + } + ) + expected_dynamic = { + "prompt_builder": { + "prompt": [ChatMessage.from_user("This is the dynamic prompt:\n Query: Where does the speaker live?")] + } + } + assert result == expected_dynamic + + +class TestChatPromptBuilderDynamic: + def test_multiple_templated_chat_messages(self): + prompt_builder = ChatPromptBuilder() + language = "French" + location = "Berlin" + messages = [ + ChatMessage.from_system("Write your response in this language:{{language}}"), + ChatMessage.from_user("Tell me about {{location}}"), + ] + + result = prompt_builder.run(template_variables={"language": language, "location": location}, template=messages) + assert result["prompt"] == [ + ChatMessage.from_system("Write your response in this language:French"), + ChatMessage.from_user("Tell me about Berlin"), + ], "The templated messages should match the expected output." + + def test_multiple_templated_chat_messages_in_place(self): + prompt_builder = ChatPromptBuilder() + language = "French" + location = "Berlin" + messages = [ + ChatMessage.from_system("Write your response ins this language:{{language}}"), + ChatMessage.from_user("Tell me about {{location}}"), + ] + + res = prompt_builder.run(template_variables={"language": language, "location": location}, template=messages) + assert res == { + "prompt": [ + ChatMessage.from_system("Write your response ins this language:French"), + ChatMessage.from_user("Tell me about Berlin"), + ] + }, "The templated messages should match the expected output." + + def test_some_templated_chat_messages(self): + prompt_builder = ChatPromptBuilder() + language = "English" + location = "Paris" + messages = [ + ChatMessage.from_system("Please, respond in the following language: {{language}}."), + ChatMessage.from_user("I would like to learn more about {{location}}."), + ChatMessage.from_assistant("Yes, I can help you with that {{subject}}"), + ChatMessage.from_user("Ok so do so please, be elaborate."), + ] + + result = prompt_builder.run(template_variables={"language": language, "location": location}, template=messages) + + expected_messages = [ + ChatMessage.from_system("Please, respond in the following language: English."), + ChatMessage.from_user("I would like to learn more about Paris."), + ChatMessage.from_assistant( + "Yes, I can help you with that {{subject}}" + ), # assistant message should not be templated + ChatMessage.from_user("Ok so do so please, be elaborate."), + ] + + assert result["prompt"] == expected_messages, "The templated messages should match the expected output." + + def test_example_in_pipeline(self): + prompt_builder = ChatPromptBuilder() + + pipe = Pipeline() + pipe.add_component("prompt_builder", prompt_builder) + + location = "Berlin" + system_message = ChatMessage.from_system( + "You are a helpful assistant giving out valuable information to tourists." + ) + messages = [system_message, ChatMessage.from_user("Tell me about {{location}}")] + + res = pipe.run(data={"prompt_builder": {"template_variables": {"location": location}, "template": messages}}) + assert res == { + "prompt_builder": { + "prompt": [ + ChatMessage.from_system("You are a helpful assistant giving out valuable information to tourists."), + ChatMessage.from_user("Tell me about Berlin"), + ] + } + } + + messages = [ + system_message, + ChatMessage.from_user("What's the weather forecast for {{location}} in the next {{day_count}} days?"), + ] + + res = pipe.run( + data={ + "prompt_builder": {"template_variables": {"location": location, "day_count": "5"}, "template": messages} + } + ) + assert res == { + "prompt_builder": { + "prompt": [ + ChatMessage.from_system("You are a helpful assistant giving out valuable information to tourists."), + ChatMessage.from_user("What's the weather forecast for Berlin in the next 5 days?"), + ] + } + } + + def test_example_in_pipeline_with_multiple_templated_messages(self): + # no parameter init, we don't use any runtime template variables + prompt_builder = ChatPromptBuilder() + + pipe = Pipeline() + pipe.add_component("prompt_builder", prompt_builder) + + location = "Berlin" + system_message = ChatMessage.from_system( + "You are a helpful assistant giving out valuable information to tourists in {{language}}." + ) + messages = [system_message, ChatMessage.from_user("Tell me about {{location}}")] + + res = pipe.run( + data={ + "prompt_builder": { + "template_variables": {"location": location, "language": "German"}, + "template": messages, + } + } + ) + assert res == { + "prompt_builder": { + "prompt": [ + ChatMessage.from_system( + "You are a helpful assistant giving out valuable information to tourists in German." + ), + ChatMessage.from_user("Tell me about Berlin"), + ] + } + } + + messages = [ + system_message, + ChatMessage.from_user("What's the weather forecast for {{location}} in the next {{day_count}} days?"), + ] + + res = pipe.run( + data={ + "prompt_builder": { + "template_variables": {"location": location, "day_count": "5", "language": "English"}, + "template": messages, + } + } + ) + assert res == { + "prompt_builder": { + "prompt": [ + ChatMessage.from_system( + "You are a helpful assistant giving out valuable information to tourists in English." + ), + ChatMessage.from_user("What's the weather forecast for Berlin in the next 5 days?"), + ] + } + } + + def test_pipeline_complex(self): + @component + class ValueProducer: + def __init__(self, value_to_produce: str): + self.value_to_produce = value_to_produce + + @component.output_types(value_output=str) + def run(self): + return {"value_output": self.value_to_produce} + + pipe = Pipeline() + pipe.add_component("prompt_builder", ChatPromptBuilder(variables=["value_output"])) + pipe.add_component("value_producer", ValueProducer(value_to_produce="Berlin")) + pipe.connect("value_producer.value_output", "prompt_builder") + + messages = [ + ChatMessage.from_system("You give valuable information to tourists."), + ChatMessage.from_user("Tell me about {{value_output}}"), + ] + + res = pipe.run(data={"template": messages}) + assert res == { + "prompt_builder": { + "prompt": [ + ChatMessage.from_system("You give valuable information to tourists."), + ChatMessage.from_user("Tell me about Berlin"), + ] + } + }