chore: Remove deprecated DynamicPromptBuilder and DynamicChatPromptBuilder components (#8085)

This commit is contained in:
Madeesh Kannan 2024-07-26 10:00:59 +02:00 committed by GitHub
parent f372ca443c
commit b2aef217da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 6 additions and 771 deletions

View File

@ -1,14 +1,7 @@
loaders:
- type: haystack_pydoc_tools.loaders.CustomPythonLoader
search_path: [../../../haystack/components/builders]
modules:
[
"answer_builder",
"prompt_builder",
"dynamic_prompt_builder",
"dynamic_chat_prompt_builder",
"chat_prompt_builder",
]
modules: ["answer_builder", "prompt_builder", "chat_prompt_builder"]
ignore_when_discovered: ["__init__"]
processors:
- type: filter

View File

@ -4,8 +4,6 @@
from haystack.components.builders.answer_builder import AnswerBuilder
from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder
from haystack.components.builders.dynamic_chat_prompt_builder import DynamicChatPromptBuilder
from haystack.components.builders.dynamic_prompt_builder import DynamicPromptBuilder
from haystack.components.builders.prompt_builder import PromptBuilder
__all__ = ["AnswerBuilder", "PromptBuilder", "DynamicPromptBuilder", "DynamicChatPromptBuilder", "ChatPromptBuilder"]
__all__ = ["AnswerBuilder", "PromptBuilder", "ChatPromptBuilder"]

View File

@ -1,190 +0,0 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import warnings
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 DynamicChatPromptBuilder:
"""
DynamicChatPromptBuilder is designed to construct dynamic prompts from a list of `ChatMessage` instances.
It integrates with Jinja2 templating for dynamic prompt generation. It considers any user or system message in the
list potentially containing a template and renders it with variables provided to the constructor. Additional
template variables can be feed into the component/pipeline `run` method and will be merged before rendering the
template.
Usage example:
```python
from haystack.components.builders import DynamicChatPromptBuilder
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 = DynamicChatPromptBuilder()
llm = OpenAIChatGenerator(api_key=Secret.from_token("<your-api-key>"), 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},
"prompt_source": 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=<ChatRole.ASSISTANT: 'assistant'>, 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"},
"prompt_source": 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=<ChatRole.ASSISTANT: 'assistant'>, 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 that the weather forecast in the example above is fictional, but it can be easily connected to a weather
API to provide real weather forecasts.
"""
def __init__(self, runtime_variables: Optional[List[str]] = None):
"""
Constructs a DynamicChatPromptBuilder component.
:param runtime_variables:
A list of template variable names you can use in chat prompt construction. For example,
if `runtime_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 ChatMessage that is provided to the `run` method.
"""
warnings.warn(
"`DynamicChatPromptBuilder` is deprecated and will be removed in Haystack 2.4.0."
"Use `ChatPromptBuilder` instead.",
DeprecationWarning,
)
runtime_variables = runtime_variables or []
# setup inputs
default_inputs = {"prompt_source": List[ChatMessage], "template_variables": Optional[Dict[str, Any]]}
additional_input_slots = {var: Optional[Any] for var in runtime_variables}
component.set_input_types(self, **default_inputs, **additional_input_slots)
# setup outputs
component.set_output_types(self, prompt=List[ChatMessage])
self.runtime_variables = runtime_variables
def run(self, prompt_source: List[ChatMessage], template_variables: Optional[Dict[str, Any]] = None, **kwargs):
"""
Executes the dynamic prompt building process by processing a list of `ChatMessage` instances.
Any user message or system message is inspected for templates and rendered with the variables provided to the
constructor. You can provide additional template variables directly to this method, which are then merged with
the variables provided to the constructor.
:param prompt_source:
A list of `ChatMessage` instances. All user and system messages are treated as potentially having templates
and are rendered with the provided template variables - if templates are found.
:param template_variables:
A dictionary of template variables. Template variables provided at initialization are required
to resolve pipeline variables, and these are additional variables users can provide directly to this method.
:param kwargs:
Additional keyword arguments, 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`.
:raises ValueError:
If the last message in `chat_messages` is not from a user.
"""
if not prompt_source:
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 prompt_source):
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."
)
kwargs = kwargs or {}
template_variables = template_variables or {}
template_variables = {**kwargs, **template_variables}
if not template_variables:
logger.warning(
"The DynamicChatPromptBuilder run method requires template variables, but none were provided. "
"Please provide an appropriate template variable to enable correct prompt generation."
)
processed_messages = []
for message in prompt_source:
if message.is_from(ChatRole.USER) or message.is_from(ChatRole.SYSTEM):
template = self._validate_template(message.content, set(template_variables.keys()))
rendered_content = template.render(template_variables)
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_template(self, template_text: str, provided_variables: Set[str]):
"""
Checks if all the required template variables are provided to the pipeline `run` method.
If all the required template variables are provided, returns a Jinja2 template object.
Otherwise, raises a ValueError.
:param template_text:
A Jinja2 template as a string.
:param provided_variables:
A set of provided template variables.
:returns:
A Jinja2 template object if all the required template variables are provided.
:raises ValueError:
If all the required template variables are not provided.
"""
template = Template(template_text)
ast = template.environment.parse(template_text)
required_template_variables = meta.find_undeclared_variables(ast)
filled_template_vars = required_template_variables.intersection(provided_variables)
if len(filled_template_vars) != len(required_template_variables):
raise ValueError(
f"The {self.__class__.__name__} requires specific template variables that are missing. "
f"Required variables: {required_template_variables}. Only the following variables were "
f"provided: {provided_variables}. Please provide all the required template variables."
)
return template

View File

@ -1,169 +0,0 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import warnings
from typing import Any, Dict, List, Optional, Set
from jinja2 import Template, meta
from haystack import component, logging
logger = logging.getLogger(__name__)
@component
class DynamicPromptBuilder:
"""
DynamicPromptBuilder is designed to construct dynamic prompts for the pipeline.
Users can change the prompt template at runtime by providing a new template for each pipeline run invocation
if needed.
Usage example:
```python
from typing import List
from haystack.components.builders import DynamicPromptBuilder
from haystack.components.generators import OpenAIGenerator
from haystack import Pipeline, component, Document
from haystack.utils import Secret
prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"])
llm = OpenAIGenerator(api_key=Secret.from_token("<your-api-key>"), model="gpt-3.5-turbo")
@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.add_component("llm", llm)
pipe.connect("doc_producer.documents", "prompt_builder.documents")
pipe.connect("prompt_builder.prompt", "llm.prompt")
template = "Here is the document: {{documents[0].content}} \\n Answer: {{query}}"
result = pipe.run(
data={
"doc_producer": {"doc_input": "Hello world, I live in Berlin"},
"prompt_builder": {
"prompt_source": template,
"template_variables": {"query": "Where does the speaker live?"},
},
}
)
print(result)
>> {'llm': {'replies': ['The speaker lives in Berlin.'],
>> 'meta': [{'model': 'gpt-3.5-turbo-0613',
>> 'index': 0,
>> 'finish_reason': 'stop',
>> 'usage': {'prompt_tokens': 28,
>> 'completion_tokens': 6,
>> 'total_tokens': 34}}]}}
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. This dynamic prompt generation is in contrast to the static prompt generation
using `PromptBuilder`, where the prompt template is fixed for the pipeline's lifetime and cannot be changed
for each pipeline run invocation.
"""
def __init__(self, runtime_variables: Optional[List[str]] = None):
"""
Constructs a DynamicPromptBuilder component.
:param runtime_variables:
A list of template variable names you can use in prompt construction. For example,
if `runtime_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.
"""
warnings.warn(
"`DynamicPromptBuilder` is deprecated and will be removed in Haystack 2.4.0."
"Use `PromptBuilder` instead.",
DeprecationWarning,
)
runtime_variables = runtime_variables or []
# setup inputs
run_input_slots = {"prompt_source": str, "template_variables": Optional[Dict[str, Any]]}
kwargs_input_slots = {var: Optional[Any] for var in runtime_variables}
component.set_input_types(self, **run_input_slots, **kwargs_input_slots)
# setup outputs
component.set_output_types(self, prompt=str)
self.runtime_variables = runtime_variables
def run(self, prompt_source: str, template_variables: Optional[Dict[str, Any]] = None, **kwargs):
"""
Executes the dynamic prompt building process.
Depending on the provided type of `prompt_source`, this method either processes a list of `ChatMessage`
instances or a string template. In the case of `ChatMessage` instances, the last user message is treated as a
template and rendered with the resolved pipeline variables and any additional template variables provided.
For a string template, it directly applies the template variables to render the final prompt. You can provide
additional template variables directly to this method, that are then merged with the variables resolved from
the pipeline runtime.
:param prompt_source:
A string template.
:param template_variables:
An optional dictionary of template variables. Template variables provided at initialization are required
to resolve pipeline variables, and these are additional variables users can provide directly to this method.
:param kwargs:
Additional keyword arguments, typically resolved from a pipeline, which are merged with the provided
template variables.
:returns: A dictionary with the following keys:
- `prompt`: The updated prompt text after rendering the string template.
"""
kwargs = kwargs or {}
template_variables = template_variables or {}
template_variables_combined = {**kwargs, **template_variables}
if not template_variables_combined:
raise ValueError(
"The DynamicPromptBuilder run method requires template variables, but none were provided. "
"Please provide an appropriate template variable to enable prompt generation."
)
template = self._validate_template(prompt_source, set(template_variables_combined.keys()))
result = template.render(template_variables_combined)
return {"prompt": result}
def _validate_template(self, template_text: str, provided_variables: Set[str]):
"""
Checks if all the required template variables are provided to the pipeline `run` method.
If all the required template variables are provided, returns a Jinja2 template object.
Otherwise, raises a ValueError.
:param template_text:
A Jinja2 template as a string.
:param provided_variables:
A set of provided template variables.
:returns:
A Jinja2 template object if all the required template variables are provided.
:raises ValueError:
If all the required template variables are not provided.
"""
template = Template(template_text)
ast = template.environment.parse(template_text)
required_template_variables = meta.find_undeclared_variables(ast)
filled_template_vars = required_template_variables.intersection(provided_variables)
if len(filled_template_vars) != len(required_template_variables):
raise ValueError(
f"The {self.__class__.__name__} requires specific template variables that are missing. "
f"Required variables: {required_template_variables}. Only the following variables were "
f"provided: {provided_variables}. Please provide all the required template variables."
)
return template

View File

@ -0,0 +1,4 @@
---
upgrade:
- |
Removed the deprecated `DynamicPromptBuilder` and `DynamicChatPromptBuilder` components. Use `PromptBuilder` and `ChatPromptBuilder` instead.

View File

@ -1,288 +0,0 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import List
import pytest
from haystack import Pipeline, component
from haystack.components.builders import DynamicChatPromptBuilder
from haystack.dataclasses import ChatMessage
class TestDynamicChatPromptBuilder:
def test_initialization(self):
runtime_variables = ["var1", "var2", "var3"]
builder = DynamicChatPromptBuilder(runtime_variables)
assert builder.runtime_variables == runtime_variables
# we have inputs that contain: prompt_source, template_variables + runtime_variables
expected_keys = set(runtime_variables + ["prompt_source", "template_variables"])
assert set(builder.__haystack_input__._sockets_dict.keys()) == expected_keys
# response is always prompt regardless of chat mode
assert set(builder.__haystack_output__._sockets_dict.keys()) == {"prompt"}
# prompt_source is a list of ChatMessage
assert builder.__haystack_input__._sockets_dict["prompt_source"].type == List[ChatMessage]
# output is always prompt, but the type is different depending on the chat mode
assert builder.__haystack_output__._sockets_dict["prompt"].type == List[ChatMessage]
def test_non_empty_chat_messages(self):
prompt_builder = DynamicChatPromptBuilder(runtime_variables=["documents"])
prompt_source = [ChatMessage.from_system(content="Hello"), ChatMessage.from_user(content="Hello, {{ who }}!")]
template_variables = {"who": "World"}
result = prompt_builder.run(prompt_source, template_variables)
assert result == {
"prompt": [ChatMessage.from_system(content="Hello"), ChatMessage.from_user(content="Hello, World!")]
}
def test_single_chat_message(self):
prompt_builder = DynamicChatPromptBuilder(runtime_variables=["documents"])
prompt_source = [ChatMessage.from_user(content="Hello, {{ who }}!")]
template_variables = {"who": "World"}
result = prompt_builder.run(prompt_source, template_variables)
assert result == {"prompt": [ChatMessage.from_user(content="Hello, World!")]}
def test_empty_chat_message_list(self):
prompt_builder = DynamicChatPromptBuilder(runtime_variables=["documents"])
with pytest.raises(ValueError):
prompt_builder.run(prompt_source=[], template_variables={})
def test_chat_message_list_with_mixed_object_list(self):
prompt_builder = DynamicChatPromptBuilder(runtime_variables=["documents"])
with pytest.raises(ValueError):
prompt_builder.run(prompt_source=[ChatMessage.from_user("Hello"), "there world"], template_variables={})
def test_chat_message_list_with_missing_variables(self):
prompt_builder = DynamicChatPromptBuilder(runtime_variables=["documents"])
prompt_source = [ChatMessage.from_user(content="Hello, {{ who }}!")]
# Call the _process_chat_messages method and expect a ValueError
with pytest.raises(ValueError):
prompt_builder.run(prompt_source, template_variables={})
def test_missing_template_variables(self):
prompt_builder = DynamicChatPromptBuilder(runtime_variables=["documents"])
# missing template variable city
with pytest.raises(ValueError):
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name"})
# missing template variable name
with pytest.raises(ValueError):
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"city"})
# completely unknown template variable
with pytest.raises(ValueError):
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"age"})
def test_provided_template_variables(self):
prompt_builder = DynamicChatPromptBuilder(runtime_variables=["documents"])
# both variables are provided
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name", "city"})
# provided variables are a superset of the required variables
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name", "city", "age"})
def test_multiple_templated_chat_messages(self):
prompt_builder = DynamicChatPromptBuilder()
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}, prompt_source=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 = DynamicChatPromptBuilder()
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}, prompt_source=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 = DynamicChatPromptBuilder()
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}, prompt_source=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 = DynamicChatPromptBuilder()
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}, "prompt_source": 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"},
"prompt_source": 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 = DynamicChatPromptBuilder()
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"},
"prompt_source": 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"},
"prompt_source": 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", DynamicChatPromptBuilder(runtime_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={"prompt_source": messages})
assert res == {
"prompt_builder": {
"prompt": [
ChatMessage.from_system("You give valuable information to tourists."),
ChatMessage.from_user("Tell me about Berlin"),
]
}
}

View File

@ -1,113 +0,0 @@
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from typing import List
import pytest
from jinja2 import TemplateSyntaxError
from haystack import Document, Pipeline, component
from haystack.components.builders import DynamicPromptBuilder
class TestDynamicPromptBuilder:
def test_initialization(self):
runtime_variables = ["var1", "var2"]
builder = DynamicPromptBuilder(runtime_variables)
assert builder.runtime_variables == runtime_variables
# regardless of the chat mode
# we have inputs that contain: prompt_source, template_variables + runtime_variables
expected_keys = set(runtime_variables + ["prompt_source", "template_variables"])
assert set(builder.__haystack_input__._sockets_dict.keys()) == expected_keys
# response is always prompt regardless of chat mode
assert set(builder.__haystack_output__._sockets_dict.keys()) == {"prompt"}
# prompt_source is a list of ChatMessage or a string
assert builder.__haystack_input__._sockets_dict["prompt_source"].type == str
# output is always prompt, but the type is different depending on the chat mode
assert builder.__haystack_output__._sockets_dict["prompt"].type == str
def test_processing_a_simple_template_with_provided_variables(self):
runtime_variables = ["var1", "var2", "var3"]
builder = DynamicPromptBuilder(runtime_variables)
template = "Hello, {{ name }}!"
template_variables = {"name": "John"}
expected_result = {"prompt": "Hello, John!"}
assert builder.run(template, template_variables) == expected_result
def test_processing_a_simple_template_with_invalid_template(self):
runtime_variables = ["var1", "var2", "var3"]
builder = DynamicPromptBuilder(runtime_variables)
template = "Hello, {{ name }!"
template_variables = {"name": "John"}
with pytest.raises(TemplateSyntaxError):
builder.run(template, template_variables)
def test_processing_a_simple_template_with_missing_variables(self):
runtime_variables = ["var1", "var2", "var3"]
builder = DynamicPromptBuilder(runtime_variables)
with pytest.raises(ValueError):
builder.run("Hello, {{ name }}!", {})
def test_missing_template_variables(self):
prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"])
# missing template variable city
with pytest.raises(ValueError):
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name"})
# missing template variable name
with pytest.raises(ValueError):
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"city"})
# completely unknown template variable
with pytest.raises(ValueError):
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"age"})
def test_provided_template_variables(self):
prompt_builder = DynamicPromptBuilder(runtime_variables=["documents"])
# both variables are provided
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name", "city"})
# provided variables are a superset of the required variables
prompt_builder._validate_template("Hello, I'm {{ name }}, and I live in {{ city }}.", {"name", "city", "age"})
def test_example_in_pipeline(self):
prompt_builder = DynamicPromptBuilder(runtime_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 = "Here is the document: {{documents[0].content}} \\n Answer: {{query}}"
result = pipe.run(
data={
"doc_producer": {"doc_input": "Hello world, I live in Berlin"},
"prompt_builder": {
"prompt_source": template,
"template_variables": {"query": "Where does the speaker live?"},
},
}
)
assert result == {
"prompt_builder": {
"prompt": "Here is the document: Hello world, I live in Berlin \\n Answer: Where does the speaker live?"
}
}