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}