llm_kernel/llm/
template.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PromptTemplate {
12 pub template: String,
14 #[serde(default)]
16 pub few_shot: Vec<String>,
17}
18
19impl PromptTemplate {
20 pub fn new(template: impl Into<String>) -> Self {
22 Self {
23 template: template.into(),
24 few_shot: Vec::new(),
25 }
26 }
27
28 pub fn with_few_shot(mut self, examples: Vec<String>) -> Self {
30 self.few_shot = examples;
31 self
32 }
33
34 pub fn render(&self, vars: &[(&str, &str)]) -> String {
40 let mut out = String::new();
41 for ex in &self.few_shot {
42 out.push_str(ex);
43 out.push('\n');
44 }
45 out.push_str(&crate::llm::prompt::render_prompt(&self.template, vars));
46 out
47 }
48
49 pub fn variables(&self) -> Vec<String> {
55 let mut seen = Vec::new();
56 let body = self.template.as_str();
57 let mut rest = body;
58 while let Some(start) = rest.find("{{") {
59 rest = &rest[start + "{{".len()..];
60 let Some(end) = rest.find("}}") else { break };
61 let name = &rest[..end];
62 if !seen.iter().any(|s: &String| s == name) {
63 seen.push(name.to_string());
64 }
65 rest = &rest[end + "}}".len()..];
66 }
67 seen
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn render_substitutes_variables() {
77 let t = PromptTemplate::new("Hello {{name}}, your topic is {{topic}}.");
78 let out = t.render(&[("name", "Alice"), ("topic", "Rust")]);
79 assert_eq!(out, "Hello Alice, your topic is Rust.");
80 }
81
82 #[test]
83 fn few_shot_renders_before_body() {
84 let t = PromptTemplate::new("Classify: {{text}}").with_few_shot(vec![
85 "Q: apples\nA: fruit".to_string(),
86 "Q: rover\nA: dog".to_string(),
87 ]);
88 let out = t.render(&[("text", "carrot")]);
89 let body_pos = out.find("Classify:").expect("body present");
90 let ex1_pos = out.find("Q: apples").expect("example 1 present");
91 let ex2_pos = out.find("Q: rover").expect("example 2 present");
92 assert!(ex1_pos < body_pos, "first example must precede body");
93 assert!(ex2_pos < body_pos, "second example must precede body");
94 assert!(out.ends_with("Classify: carrot"));
95 }
96
97 #[test]
98 fn serde_roundtrip_equal() {
99 let t = PromptTemplate::new("Summarize: {{input}}")
100 .with_few_shot(vec!["Example one".to_string(), "Example two".to_string()]);
101 let json = serde_json::to_string(&t).expect("serialize");
102 let back: PromptTemplate = serde_json::from_str(&json).expect("deserialize");
103 assert_eq!(back.template, t.template);
104 assert_eq!(back.few_shot, t.few_shot);
105 assert!(!back.few_shot.is_empty());
106 }
107
108 #[test]
109 fn missing_variable_left_as_is() {
110 let t = PromptTemplate::new("Hello {{name}}, age {{age}}.");
111 let out = t.render(&[("name", "Bob")]);
112 assert_eq!(out, "Hello Bob, age {{age}}.");
113 }
114
115 #[test]
116 fn variables_in_first_seen_order() {
117 let t = PromptTemplate::new("{{b}} {{a}} {{b}} {{c}}");
118 assert_eq!(t.variables(), vec!["b", "a", "c"]);
119 }
120}