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}