Coverage for haystack/components/builders/prompt_builder.py: 100%

50 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 13:53 +0000

1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5from typing import Any, Literal 

6 

7from haystack import component, default_to_dict, logging 

8from haystack.utils import Jinja2TimeExtension 

9from haystack.utils.jinja2_extensions import _extract_template_variables_and_assignments 

10from haystack.utils.jinja2_sandbox import HaystackSandboxedEnvironment 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15@component 

16class PromptBuilder: 

17 """ 

18 

19 Renders a prompt filling in any variables so that it can send it to a Generator. 

20 

21 The prompt uses Jinja2 template syntax. 

22 The variables in the default template are used as PromptBuilder's input and are all required by default. 

23 To make any subset of variables optional, set `required_variables` to an explicit list of the variables that 

24 should remain required. Optional variables are replaced with an empty string in the rendered prompt. 

25 To try out different prompts, you can replace the prompt template at runtime by 

26 providing a template for each pipeline run invocation. 

27 

28 ### Usage examples 

29 

30 #### On its own 

31 

32 This example uses PromptBuilder to render a prompt template and fill it with `target_language` 

33 and `snippet`. PromptBuilder returns a prompt with the string "Translate the following context to Spanish. 

34 Context: I can't speak Spanish.; Translation:". 

35 ```python 

36 from haystack.components.builders import PromptBuilder 

37 

38 template = "Translate the following context to {{ target_language }}. Context: {{ snippet }}; Translation:" 

39 builder = PromptBuilder(template=template) 

40 builder.run(target_language="spanish", snippet="I can't speak spanish.") 

41 ``` 

42 

43 #### In a Pipeline 

44 

45 This is an example of a RAG pipeline where PromptBuilder renders a custom prompt template and fills it 

46 with the contents of the retrieved documents and a query. The rendered prompt is then sent to a ChatGenerator. 

47 ```python 

48 from haystack import Pipeline, Document 

49 from haystack.utils import Secret 

50 from haystack.components.generators.chat import OpenAIChatGenerator 

51 from haystack.components.builders.prompt_builder import PromptBuilder 

52 

53 # in a real world use case documents could come from a retriever, web, or any other source 

54 documents = [Document(content="Joe lives in Berlin"), Document(content="Joe is a software engineer")] 

55 prompt_template = \"\"\" 

56 Given these documents, answer the question. 

57 Documents: 

58 {% for doc in documents %} 

59 {{ doc.content }} 

60 {% endfor %} 

61 

62 Question: {{query}} 

63 Answer: 

64 \"\"\" 

65 p = Pipeline() 

66 p.add_component(instance=PromptBuilder(template=prompt_template), name="prompt_builder") 

67 p.add_component(instance=OpenAIChatGenerator(api_key=Secret.from_env_var("OPENAI_API_KEY")), name="llm") 

68 p.connect("prompt_builder", "llm") 

69 

70 question = "Where does Joe live?" 

71 result = p.run({"prompt_builder": {"documents": documents, "query": question}}) 

72 print(result) 

73 ``` 

74 

75 #### Changing the template at runtime (prompt engineering) 

76 

77 You can change the prompt template of an existing pipeline, like in this example: 

78 ```python 

79 documents = [ 

80 Document(content="Joe lives in Berlin", meta={"name": "doc1"}), 

81 Document(content="Joe is a software engineer", meta={"name": "doc1"}), 

82 ] 

83 new_template = \"\"\" 

84 You are a helpful assistant. 

85 Given these documents, answer the question. 

86 Documents: 

87 {% for doc in documents %} 

88 Document {{ loop.index }}: 

89 Document name: {{ doc.meta['name'] }} 

90 {{ doc.content }} 

91 {% endfor %} 

92 

93 Question: {{ query }} 

94 Answer: 

95 \"\"\" 

96 p.run({ 

97 "prompt_builder": { 

98 "documents": documents, 

99 "query": question, 

100 "template": new_template, 

101 }, 

102 }) 

103 ``` 

104 To replace the variables in the default template when testing your prompt, 

105 pass the new variables in the `variables` parameter. 

106 

107 #### Overwriting variables at runtime 

108 

109 To overwrite the values of variables, use `template_variables` during runtime: 

110 ```python 

111 language_template = \"\"\" 

112 You are a helpful assistant. 

113 Given these documents, answer the question. 

114 Documents: 

115 {% for doc in documents %} 

116 Document {{ loop.index }}: 

117 Document name: {{ doc.meta['name'] }} 

118 {{ doc.content }} 

119 {% endfor %} 

120 

121 Question: {{ query }} 

122 Please provide your answer in {{ answer_language | default('English') }} 

123 Answer: 

124 \"\"\" 

125 p.run({ 

126 "prompt_builder": { 

127 "documents": documents, 

128 "query": question, 

129 "template": language_template, 

130 "template_variables": {"answer_language": "German"}, 

131 }, 

132 }) 

133 ``` 

134 Note that `language_template` introduces variable `answer_language` which is not bound to any pipeline variable. 

135 If not set otherwise, it will use its default value 'English'. 

136 This example overwrites its value to 'German'. 

137 Use `template_variables` to overwrite pipeline variables (such as documents) as well. 

138 

139 """ 

140 

141 def __init__( 

142 self, 

143 template: str, 

144 required_variables: list[str] | Literal["*"] | None = "*", 

145 variables: list[str] | None = None, 

146 ) -> None: 

147 """ 

148 Constructs a PromptBuilder component. 

149 

150 :param template: 

151 A prompt template that uses Jinja2 syntax to add variables. For example: 

152 `"Summarize this document: {{ documents[0].content }}\\nSummary:"` 

153 It's used to render the prompt. 

154 The variables in the default template are input for PromptBuilder and are all required by default. 

155 :param required_variables: List variables that must be provided as input to PromptBuilder. 

156 Defaults to `"*"`, which marks every variable found in the prompt as required. 

157 Pass an explicit list to only require a subset of the variables; any variable not listed becomes 

158 optional and is replaced with an empty string in the rendered prompt when missing. 

159 Set to `None` to mark every variable as optional. 

160 :param variables: 

161 List input variables to use in prompt templates instead of the ones inferred from the 

162 `template` parameter. For example, to use more variables during prompt engineering than the ones present 

163 in the default template, you can provide them here. 

164 """ 

165 self._template_string = template 

166 self._variables = variables 

167 self._required_variables = required_variables 

168 self.required_variables = required_variables or [] 

169 try: 

170 # The Jinja2TimeExtension needs an optional dependency to be installed. 

171 # If it's not available we can do without it and use the PromptBuilder as is. 

172 self._env = HaystackSandboxedEnvironment(extensions=[Jinja2TimeExtension]) 

173 except ImportError: 

174 self._env = HaystackSandboxedEnvironment() 

175 

176 self.template = self._env.from_string(template) 

177 

178 if not variables: 

179 assigned_variables, template_variables = _extract_template_variables_and_assignments( 

180 env=self._env, template=template 

181 ) 

182 variables = list(template_variables - assigned_variables) 

183 

184 variables = variables or [] 

185 self.variables = variables 

186 

187 if len(self.variables) > 0 and required_variables is None: 

188 logger.warning( 

189 "PromptBuilder has {length} prompt variables and `required_variables` is explicitly set to `None`. " 

190 "This treats all prompt variables as optional, which may lead to unintended behavior in " 

191 "multi-branch pipelines. Only set `required_variables` to `None` if you intentionally want all " 

192 "variables to be optional.", 

193 length=len(self.variables), 

194 ) 

195 

196 # setup inputs 

197 for var in self.variables: 

198 if self.required_variables == "*" or var in self.required_variables: 

199 component.set_input_type(self, var, Any) 

200 else: 

201 component.set_input_type(self, var, Any, "") 

202 

203 def to_dict(self) -> dict[str, Any]: 

204 """ 

205 Returns a dictionary representation of the component. 

206 

207 :returns: 

208 Serialized dictionary representation of the component. 

209 """ 

210 return default_to_dict( 

211 self, template=self._template_string, variables=self._variables, required_variables=self._required_variables 

212 ) 

213 

214 @component.output_types(prompt=str) 

215 def run( 

216 self, template: str | None = None, template_variables: dict[str, Any] | None = None, **kwargs: Any 

217 ) -> dict[str, Any]: 

218 """ 

219 Renders the prompt template with the provided variables. 

220 

221 It applies the template variables to render the final prompt. You can provide variables via pipeline kwargs. 

222 In order to overwrite the default template, you can set the `template` parameter. 

223 In order to overwrite pipeline kwargs, you can set the `template_variables` parameter. 

224 

225 :param template: 

226 An optional string template to overwrite PromptBuilder's default template. If None, the default template 

227 provided at initialization is used. 

228 :param template_variables: 

229 An optional dictionary of template variables to overwrite the pipeline variables. 

230 :param kwargs: 

231 Pipeline variables used for rendering the prompt. 

232 

233 :returns: A dictionary with the following keys: 

234 - `prompt`: The updated prompt text after rendering the prompt template. 

235 

236 :raises ValueError: 

237 If any of the required template variables is not provided. 

238 """ 

239 kwargs = kwargs or {} 

240 template_variables = template_variables or {} 

241 template_variables_combined = {**kwargs, **template_variables} 

242 self._validate_variables(set(template_variables_combined.keys())) 

243 

244 compiled_template = self.template 

245 if template is not None: 

246 compiled_template = self._env.from_string(template) 

247 

248 result = compiled_template.render(template_variables_combined) 

249 return {"prompt": result} 

250 

251 def _validate_variables(self, provided_variables: set[str]) -> None: 

252 """ 

253 Checks if all the required template variables are provided. 

254 

255 :param provided_variables: 

256 A set of provided template variables. 

257 :raises ValueError: 

258 If any of the required template variables is not provided. 

259 """ 

260 if self.required_variables == "*": 

261 required_variables = sorted(self.variables) 

262 else: 

263 required_variables = self.required_variables 

264 missing_variables = [var for var in required_variables if var not in provided_variables] 

265 if missing_variables: 

266 missing_vars_str = ", ".join(missing_variables) 

267 raise ValueError( 

268 f"Missing required input variables in PromptBuilder: {missing_vars_str}. " 

269 f"Required variables: {required_variables}. Provided variables: {provided_variables}." 

270 )