Skip to main content

shimmyjinja/
lib.rs

1pub(crate) mod ast;
2pub(crate) mod eval;
3pub(crate) mod lexer;
4pub(crate) mod parser;
5
6use crate::eval::{Evaluator, Value};
7use crate::parser::Parser;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ChatMessage {
12    pub role: String,
13    pub content: String,
14}
15
16/// Context variables available during template rendering.
17///
18/// These map to the top-level Jinja context that HF's
19/// `tokenizer.apply_chat_template()` provides, such as `eos_token`,
20/// `bos_token`, `add_generation_prompt`, etc.
21#[derive(Debug, Clone, Default)]
22pub struct RenderContext {
23    /// String variables (e.g., "eos_token" -> "</s>", "bos_token" -> "<s>")
24    pub vars: HashMap<String, String>,
25    /// Boolean variables (e.g., "add_generation_prompt" -> true)
26    pub flags: HashMap<String, bool>,
27}
28
29impl RenderContext {
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Set a string variable in the context.
35    pub fn set_var(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
36        self.vars.insert(key.into(), value.into());
37        self
38    }
39
40    /// Set a boolean flag in the context.
41    pub fn set_flag(&mut self, key: impl Into<String>, value: bool) -> &mut Self {
42        self.flags.insert(key.into(), value);
43        self
44    }
45}
46
47/// Error type returned by the fallible render functions.
48pub type RenderError = String;
49
50/// Render a HF-style chat_template with messages and default context.
51///
52/// Default context: `eos_token = "</s>"`, `add_generation_prompt = true`.
53/// For custom context use [`render_chat_template_with_context`].
54/// For a fallible version that returns `Result`, use [`try_render_chat_template`].
55///
56/// # Panics
57///
58/// Panics if the template fails to parse or render. Use [`try_render_chat_template`]
59/// if you need to handle template errors without unwinding.
60pub fn render_chat_template(template: &str, messages: &[ChatMessage]) -> String {
61    let mut ctx = RenderContext::new();
62    ctx.set_var("eos_token", "</s>");
63    ctx.set_flag("add_generation_prompt", true);
64    render_chat_template_with_context(template, messages, &ctx)
65}
66
67/// Render a HF-style chat_template with messages and explicit context.
68///
69/// The context provides string variables (`eos_token`, `bos_token`) and
70/// boolean flags (`add_generation_prompt`) that the template can reference.
71///
72/// # Panics
73///
74/// Panics if the template fails to parse or render. Use
75/// [`try_render_chat_template_with_context`] for a non-panicking variant.
76///
77/// # Examples
78///
79/// ```
80/// use shimmyjinja::{ChatMessage, RenderContext, render_chat_template_with_context};
81///
82/// let template = "{% for message in messages %}{{ message['role'] }}: {{ message['content'] }}\n{% endfor %}";
83/// let messages = vec![ChatMessage { role: "user".into(), content: "Hello!".into() }];
84/// let mut ctx = RenderContext::new();
85/// ctx.set_var("eos_token", "</s>");
86/// ctx.set_flag("add_generation_prompt", false);
87///
88/// let out = render_chat_template_with_context(template, &messages, &ctx);
89/// assert_eq!(out, "user: Hello!\n");
90/// ```
91pub fn render_chat_template_with_context(
92    template: &str,
93    messages: &[ChatMessage],
94    ctx: &RenderContext,
95) -> String {
96    try_render_chat_template_with_context(template, messages, ctx)
97        .unwrap_or_else(|e| panic!("shimmyjinja render error: {}", e))
98}
99
100/// Render a HF-style chat_template, returning `Err` instead of panicking.
101///
102/// Default context: `eos_token = "</s>"`, `add_generation_prompt = true`.
103pub fn try_render_chat_template(
104    template: &str,
105    messages: &[ChatMessage],
106) -> Result<String, RenderError> {
107    let mut ctx = RenderContext::new();
108    ctx.set_var("eos_token", "</s>");
109    ctx.set_flag("add_generation_prompt", true);
110    try_render_chat_template_with_context(template, messages, &ctx)
111}
112
113/// Render a HF-style chat_template with explicit context, returning `Err` instead of panicking.
114///
115/// Prefer this over [`render_chat_template_with_context`] when you need to handle
116/// template errors gracefully (e.g., fall back to a default format).
117///
118/// # Examples
119///
120/// ```
121/// use shimmyjinja::{ChatMessage, RenderContext, try_render_chat_template_with_context};
122///
123/// let bad_template = "{% for x in %}oops{% endfor %}";
124/// let messages = vec![ChatMessage { role: "user".into(), content: "hi".into() }];
125/// let ctx = RenderContext::new();
126///
127/// assert!(try_render_chat_template_with_context(bad_template, &messages, &ctx).is_err());
128/// ```
129pub fn try_render_chat_template_with_context(
130    template: &str,
131    messages: &[ChatMessage],
132    ctx: &RenderContext,
133) -> Result<String, RenderError> {
134
135    let mut parser = Parser::new(template);
136    let ast = parser.parse().map_err(|e| format!("parse error: {}", e))?;
137
138    let mut context = HashMap::new();
139
140    // Transform messages into Value::Array of Value::Map
141    let mut msgs_val = Vec::new();
142    for m in messages {
143        let mut map = HashMap::new();
144        map.insert("role".to_string(), Value::String(m.role.clone()));
145        map.insert("content".to_string(), Value::String(m.content.clone()));
146        msgs_val.push(Value::Map(map));
147    }
148    context.insert("messages".to_string(), Value::Array(msgs_val));
149
150    // Inject string variables from context
151    for (k, v) in &ctx.vars {
152        context.insert(k.clone(), Value::String(v.clone()));
153    }
154
155    // Inject boolean flags from context
156    for (k, v) in &ctx.flags {
157        context.insert(k.clone(), Value::Bool(*v));
158    }
159
160    let mut eval = Evaluator::new(context);
161    eval.render(&ast).map_err(|e| format!("render error: {}", e))
162}