1use derive_builder::Builder;
19use swiftide_core::prompt::Prompt;
20
21#[derive(Clone, Debug, Builder)]
22#[builder(setter(into, strip_option))]
23pub struct SystemPrompt {
24 #[builder(default)]
26 role: Option<String>,
27
28 #[builder(default, setter(custom))]
30 guidelines: Vec<String>,
31 #[builder(default, setter(custom))]
33 constraints: Vec<String>,
34
35 #[builder(default)]
39 additional: Option<String>,
40
41 #[builder(default = default_prompt_template())]
43 template: Prompt,
44}
45
46impl SystemPrompt {
47 pub fn builder() -> SystemPromptBuilder {
48 SystemPromptBuilder::default()
49 }
50
51 pub fn to_prompt(&self) -> Prompt {
52 self.clone().into()
53 }
54
55 pub fn with_added_guideline(&mut self, guideline: impl AsRef<str>) -> &mut Self {
57 self.guidelines.push(guideline.as_ref().to_string());
58 self
59 }
60
61 pub fn with_added_constraint(&mut self, constraint: impl AsRef<str>) -> &mut Self {
63 self.constraints.push(constraint.as_ref().to_string());
64 self
65 }
66
67 pub fn with_guidelines<T: IntoIterator<Item = S>, S: AsRef<str>>(
69 &mut self,
70 guidelines: T,
71 ) -> &mut Self {
72 self.guidelines = guidelines
73 .into_iter()
74 .map(|s| s.as_ref().to_string())
75 .collect();
76 self
77 }
78
79 pub fn with_constraints<T: IntoIterator<Item = S>, S: AsRef<str>>(
81 &mut self,
82 constraints: T,
83 ) -> &mut Self {
84 self.constraints = constraints
85 .into_iter()
86 .map(|s| s.as_ref().to_string())
87 .collect();
88 self
89 }
90
91 pub fn with_role(&mut self, role: impl Into<String>) -> &mut Self {
93 self.role = Some(role.into());
94 self
95 }
96
97 pub fn with_additional(&mut self, additional: impl Into<String>) -> &mut Self {
99 self.additional = Some(additional.into());
100 self
101 }
102
103 pub fn with_template(&mut self, template: impl Into<Prompt>) -> &mut Self {
105 self.template = template.into();
106 self
107 }
108}
109
110impl From<String> for SystemPrompt {
111 fn from(text: String) -> Self {
112 SystemPrompt {
113 role: None,
114 guidelines: Vec::new(),
115 constraints: Vec::new(),
116 additional: None,
117 template: text.into(),
118 }
119 }
120}
121
122impl From<&'static str> for SystemPrompt {
123 fn from(text: &'static str) -> Self {
124 SystemPrompt {
125 role: None,
126 guidelines: Vec::new(),
127 constraints: Vec::new(),
128 additional: None,
129 template: text.into(),
130 }
131 }
132}
133
134impl From<SystemPrompt> for SystemPromptBuilder {
135 fn from(val: SystemPrompt) -> Self {
136 SystemPromptBuilder {
137 role: Some(val.role),
138 guidelines: Some(val.guidelines),
139 constraints: Some(val.constraints),
140 additional: Some(val.additional),
141 template: Some(val.template),
142 }
143 }
144}
145
146impl From<Prompt> for SystemPrompt {
147 fn from(prompt: Prompt) -> Self {
148 SystemPrompt {
149 role: None,
150 guidelines: Vec::new(),
151 constraints: Vec::new(),
152 additional: None,
153 template: prompt,
154 }
155 }
156}
157
158impl Default for SystemPrompt {
159 fn default() -> Self {
160 SystemPrompt {
161 role: None,
162 guidelines: Vec::new(),
163 constraints: Vec::new(),
164 additional: None,
165 template: default_prompt_template(),
166 }
167 }
168}
169
170impl SystemPromptBuilder {
171 pub fn add_guideline(&mut self, guideline: &str) -> &mut Self {
172 self.guidelines
173 .get_or_insert_with(Vec::new)
174 .push(guideline.to_string());
175 self
176 }
177
178 pub fn add_constraint(&mut self, constraint: &str) -> &mut Self {
179 self.constraints
180 .get_or_insert_with(Vec::new)
181 .push(constraint.to_string());
182 self
183 }
184
185 pub fn guidelines<T: IntoIterator<Item = S>, S: AsRef<str>>(
186 &mut self,
187 guidelines: T,
188 ) -> &mut Self {
189 self.guidelines = Some(
190 guidelines
191 .into_iter()
192 .map(|s| s.as_ref().to_string())
193 .collect(),
194 );
195 self
196 }
197
198 pub fn constraints<T: IntoIterator<Item = S>, S: AsRef<str>>(
199 &mut self,
200 constraints: T,
201 ) -> &mut Self {
202 self.constraints = Some(
203 constraints
204 .into_iter()
205 .map(|s| s.as_ref().to_string())
206 .collect(),
207 );
208 self
209 }
210}
211
212fn default_prompt_template() -> Prompt {
213 include_str!("system_prompt_template.md").into()
214}
215
216#[allow(clippy::from_over_into)]
217impl Into<Prompt> for SystemPrompt {
218 fn into(self) -> Prompt {
219 let SystemPrompt {
220 role,
221 guidelines,
222 constraints,
223 template,
224 additional,
225 } = self;
226
227 template
228 .with_context_value("role", role)
229 .with_context_value("guidelines", guidelines)
230 .with_context_value("constraints", constraints)
231 .with_context_value("additional", additional)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[tokio::test]
240 async fn test_customization() {
241 let prompt = SystemPrompt::builder()
242 .role("special role")
243 .guidelines(["special guideline"])
244 .constraints(vec!["special constraint".to_string()])
245 .additional("some additional info")
246 .build()
247 .unwrap();
248
249 let prompt: Prompt = prompt.into();
250
251 let rendered = prompt.render().unwrap();
252
253 assert!(rendered.contains("special role"), "error: {rendered}");
254 assert!(rendered.contains("special guideline"), "error: {rendered}");
255 assert!(rendered.contains("special constraint"), "error: {rendered}");
256 assert!(
257 rendered.contains("some additional info"),
258 "error: {rendered}"
259 );
260
261 insta::assert_snapshot!(rendered);
262 }
263
264 #[tokio::test]
265 async fn test_to_prompt() {
266 let prompt = SystemPrompt::builder()
267 .role("special role")
268 .guidelines(["special guideline"])
269 .constraints(vec!["special constraint".to_string()])
270 .additional("some additional info")
271 .build()
272 .unwrap();
273
274 let prompt: Prompt = prompt.to_prompt();
275
276 let rendered = prompt.render().unwrap();
277
278 assert!(rendered.contains("special role"), "error: {rendered}");
279 assert!(rendered.contains("special guideline"), "error: {rendered}");
280 assert!(rendered.contains("special constraint"), "error: {rendered}");
281 assert!(
282 rendered.contains("some additional info"),
283 "error: {rendered}"
284 );
285
286 insta::assert_snapshot!(rendered);
287 }
288
289 #[tokio::test]
290 async fn test_system_prompt_to_builder() {
291 let sp = SystemPrompt {
292 role: Some("Assistant".to_string()),
293 guidelines: vec!["Be concise".to_string()],
294 constraints: vec!["No personal opinions".to_string()],
295 additional: None,
296 template: "Hello, {{role}}! Guidelines: {{guidelines}}, Constraints: {{constraints}}"
297 .into(),
298 };
299
300 let builder = SystemPromptBuilder::from(sp.clone());
301
302 assert_eq!(builder.role, Some(Some("Assistant".to_string())));
303 assert_eq!(builder.guidelines, Some(vec!["Be concise".to_string()]));
304 assert_eq!(
305 builder.constraints,
306 Some(vec!["No personal opinions".to_string()])
307 );
308 assert_eq!(
310 builder.template.as_ref().unwrap().render().unwrap(),
311 sp.template.render().unwrap()
312 );
313 }
314
315 #[test]
316 fn test_with_added_guideline_and_constraint() {
317 let mut sp = SystemPrompt::default();
318 sp.with_added_guideline("Stay polite")
319 .with_added_guideline("Use Markdown")
320 .with_added_constraint("No personal info")
321 .with_added_constraint("Short responses");
322
323 assert_eq!(sp.guidelines, vec!["Stay polite", "Use Markdown"]);
324 assert_eq!(sp.constraints, vec!["No personal info", "Short responses"]);
325 }
326
327 #[test]
328 fn test_with_guidelines_and_constraints_overwrites() {
329 let mut sp = SystemPrompt::default();
330 sp.with_guidelines(["A", "B", "C"])
331 .with_constraints(vec!["X", "Y"]);
332
333 assert_eq!(sp.guidelines, vec!["A", "B", "C"]);
334 assert_eq!(sp.constraints, vec!["X", "Y"]);
335
336 sp.with_guidelines(vec!["Z"]);
338 sp.with_constraints(["P", "Q"]);
339 assert_eq!(sp.guidelines, vec!["Z"]);
340 assert_eq!(sp.constraints, vec!["P", "Q"]);
341 }
342
343 #[test]
344 fn test_with_role_and_additional_and_template() {
345 let mut sp = SystemPrompt::default();
346 sp.with_role("explainer")
347 .with_additional("AGENTS.md here")
348 .with_template("Template: {{role}}");
349
350 assert_eq!(sp.role.as_deref(), Some("explainer"));
351 assert_eq!(sp.additional.as_deref(), Some("AGENTS.md here"));
352 assert_eq!(sp.template.render().unwrap(), "Template: {{role}}");
353 }
354}