llm_toolkit/prompt.rs
1//! A trait and macros for powerful, type-safe prompt generation.
2
3use minijinja::Environment;
4use serde::Serialize;
5use std::fmt::Display;
6
7/// A trait for converting any type into a string suitable for an LLM prompt.
8///
9/// This trait provides a standard interface for converting various types
10/// into strings that can be used as prompts for language models.
11///
12/// # Example
13///
14/// ```
15/// use llm_toolkit::prompt::ToPrompt;
16///
17/// // Any type implementing Display automatically gets ToPrompt
18/// let number = 42;
19/// assert_eq!(number.to_prompt(), "42");
20///
21/// let text = "Hello, LLM!";
22/// assert_eq!(text.to_prompt(), "Hello, LLM!");
23/// ```
24///
25/// # Custom Implementation
26///
27/// While a blanket implementation is provided for all types that implement
28/// `Display`, you can provide custom implementations for your own types:
29///
30/// ```
31/// use llm_toolkit::prompt::ToPrompt;
32/// use std::fmt;
33///
34/// struct CustomType {
35/// value: String,
36/// }
37///
38/// impl fmt::Display for CustomType {
39/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40/// write!(f, "{}", self.value)
41/// }
42/// }
43///
44/// // The blanket implementation provides ToPrompt automatically
45/// let custom = CustomType { value: "custom".to_string() };
46/// assert_eq!(custom.to_prompt(), "custom");
47/// ```
48pub trait ToPrompt {
49 /// Converts the object into a prompt string.
50 fn to_prompt(&self) -> String;
51}
52
53/// A blanket implementation of `ToPrompt` for any type that implements `Display`.
54///
55/// This provides automatic `ToPrompt` functionality for all standard library
56/// types and custom types that implement `Display`.
57impl<T: Display> ToPrompt for T {
58 fn to_prompt(&self) -> String {
59 self.to_string()
60 }
61}
62
63/// Renders a prompt from a template string and a serializable context.
64///
65/// This is the underlying function for the `prompt!` macro.
66pub fn render_prompt<T: Serialize>(template: &str, context: T) -> Result<String, minijinja::Error> {
67 let mut env = Environment::new();
68 env.add_template("prompt", template)?;
69 let tmpl = env.get_template("prompt")?;
70 tmpl.render(context)
71}
72
73/// Creates a prompt string from a template and key-value pairs.
74///
75/// This macro provides a `println!`-like experience for building prompts
76/// from various data sources. It leverages `minijinja` for templating.
77///
78/// # Example
79///
80/// ```
81/// use llm_toolkit::prompt;
82/// use serde::Serialize;
83///
84/// #[derive(Serialize)]
85/// struct User {
86/// name: &'static str,
87/// role: &'static str,
88/// }
89///
90/// let user = User { name: "Mai", role: "UX Engineer" };
91/// let task = "designing a new macro";
92///
93/// let p = prompt!(
94/// "User {{user.name}} ({{user.role}}) is currently {{task}}.",
95/// user = user,
96/// task = task
97/// ).unwrap();
98///
99/// assert_eq!(p, "User Mai (UX Engineer) is currently designing a new macro.");
100/// ```
101#[macro_export]
102macro_rules! prompt {
103 ($template:expr, $($key:ident = $value:expr),* $(,)?) => {
104 $crate::prompt::render_prompt($template, minijinja::context!($($key => $value),*))
105 };
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use serde::Serialize;
112
113 enum TestEnum {
114 VariantA,
115 VariantB,
116 }
117
118 impl Display for TestEnum {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 match self {
121 TestEnum::VariantA => write!(f, "Variant A"),
122 TestEnum::VariantB => write!(f, "Variant B"),
123 }
124 }
125 }
126
127 #[test]
128 fn test_to_prompt_for_enum() {
129 let variant = TestEnum::VariantA;
130 assert_eq!(variant.to_prompt(), "Variant A");
131 }
132
133 #[test]
134 fn test_to_prompt_for_enum_variant_b() {
135 let variant = TestEnum::VariantB;
136 assert_eq!(variant.to_prompt(), "Variant B");
137 }
138
139 #[test]
140 fn test_to_prompt_for_string() {
141 let s = "hello world";
142 assert_eq!(s.to_prompt(), "hello world");
143 }
144
145 #[test]
146 fn test_to_prompt_for_number() {
147 let n = 42;
148 assert_eq!(n.to_prompt(), "42");
149 }
150
151 #[derive(Serialize)]
152 struct SystemInfo {
153 version: &'static str,
154 os: &'static str,
155 }
156
157 #[test]
158 fn test_prompt_macro_simple() {
159 let user = "Yui";
160 let task = "implementation";
161 let prompt = prompt!(
162 "User {{user}} is working on the {{task}}.",
163 user = user,
164 task = task
165 )
166 .unwrap();
167 assert_eq!(prompt, "User Yui is working on the implementation.");
168 }
169
170 #[test]
171 fn test_prompt_macro_with_struct() {
172 let sys = SystemInfo {
173 version: "0.1.0",
174 os: "Rust",
175 };
176 let prompt = prompt!("System: {{sys.version}} on {{sys.os}}", sys = sys).unwrap();
177 assert_eq!(prompt, "System: 0.1.0 on Rust");
178 }
179
180 #[test]
181 fn test_prompt_macro_mixed() {
182 let user = "Mai";
183 let sys = SystemInfo {
184 version: "0.1.0",
185 os: "Rust",
186 };
187 let prompt = prompt!(
188 "User {{user}} is using {{sys.os}} v{{sys.version}}.",
189 user = user,
190 sys = sys
191 )
192 .unwrap();
193 assert_eq!(prompt, "User Mai is using Rust v0.1.0.");
194 }
195
196 #[test]
197 fn test_prompt_macro_no_args() {
198 let prompt = prompt!("This is a static prompt.",).unwrap();
199 assert_eq!(prompt, "This is a static prompt.");
200 }
201}