swiftide_agents/
system_prompt.rs

1//! The system prompt is the initial role and constraint defining message the LLM will receive for
2//! completion.
3//!
4//! By default, the system prompt is setup as a general-purpose chain-of-thought reasoning prompt
5//! with the role, guidelines, and constraints left empty for customization.
6//!
7//! You can override the the template entirely by providing your own `Prompt`. Optionally, you can
8//! still use the builder values by referencing them in your template.
9//!
10//! The builder provides an accessible way to build a system prompt.
11//!
12//! The agent will convert the system prompt into a prompt, adding it to the messages list the
13//! first time it is called.
14//!
15//! For customization, either the builder can be used to profit from defaults, or an override can
16//! be provided on the agent level.
17
18use derive_builder::Builder;
19use swiftide_core::prompt::Prompt;
20
21#[derive(Clone, Debug, Builder)]
22#[builder(setter(into, strip_option))]
23pub struct SystemPrompt {
24    /// The role the agent is expected to fulfil.
25    #[builder(default)]
26    role: Option<String>,
27
28    /// Additional guidelines for the agent to follow
29    #[builder(default, setter(custom))]
30    guidelines: Vec<String>,
31    /// Additional constraints
32    #[builder(default, setter(custom))]
33    constraints: Vec<String>,
34
35    /// Optional additional raw markdown to append to the prompt
36    ///
37    /// For instance, if you would like to support an AGENTS.md file, add it here.
38    #[builder(default)]
39    additional: Option<String>,
40
41    /// The template to use for the system prompt
42    #[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    /// Adds a guideline to the guidelines list.
56    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    /// Adds a constraint to the constraints list.
62    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    /// Overwrites all guidelines.
68    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    /// Overwrites all constraints.
80    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    /// Changes the role.
92    pub fn with_role(&mut self, role: impl Into<String>) -> &mut Self {
93        self.role = Some(role.into());
94        self
95    }
96
97    /// Sets the additional markdown field.
98    pub fn with_additional(&mut self, additional: impl Into<String>) -> &mut Self {
99        self.additional = Some(additional.into());
100        self
101    }
102
103    /// Sets the template.
104    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        // For template, compare the rendered string
309        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        // Overwrite with different contents
337        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}