Skip to main content

tpt_jinja_chat/
lib.rs

1//! # tpt-jinja-chat
2//!
3//! A lightweight, **dependency-free** parser for the subset of Jinja2 used by
4//! LLM `tokenizer_config.json` chat templates (e.g. Llama 3, Mistral).
5//!
6//! It supports:
7//!
8//! * `{{ expression }}` output statements
9//! * `{% set name = expr %}` variable binding, including `{% set ns.attr = .. %}`
10//! * `{% for item in iterable %} … {% endfor %}`, including tuple unpacking
11//!   (`{% for k, v in d.items() %}`)
12//! * `{% if %} / {% elif %} / {% else %} / {% endif %}`
13//! * expressions: variables, member access (`.`), indexing (`[..]`), string,
14//!   number, boolean, `none` and list literals, `+` / `-` / `*` / `/`, `~`
15//!   (string concat), the comparison operators, `in` / `not in`, and
16//!   `and` / `or` / `not`
17//! * filters via `|`: `tojson`, `trim`, `default`, `join`, `upper`, `lower`,
18//!   `length`, `first`, `last`, `selectattr`, `rejectattr`, `map`, `select`,
19//!   `reject`, `string`, `replace`, `int`, `float`, and more
20//! * `is` tests: `defined`, `undefined`, `none`, `string`, `number`, `mapping`,
21//!   `iterable`, `equalto`, `in`, …
22//! * function/method calls: `raise_exception(...)`, `namespace(...)`,
23//!   `range(...)`, `dict.items()`, `str.startswith(...)`, …
24//!
25//! ## Example
26//!
27//! ```
28//! use tpt_jinja_chat::{ChatTemplate, Context, Value};
29//!
30//! let tmpl = ChatTemplate::parse(
31//!     "{% for m in messages %}{{ m.role }}: {{ m.content }}\n{% endfor %}",
32//! ).unwrap();
33//!
34//! let mut ctx = Context::new();
35//! ctx.insert("messages", Value::Array(vec![
36//!     Value::object(vec![
37//!         ("role".into(), Value::String("user".into())),
38//!         ("content".into(), Value::String("Hello!".into())),
39//!     ]),
40//! ]));
41//!
42//! let rendered = tmpl.render(&ctx).unwrap();
43//! assert_eq!(rendered, "user: Hello!\n");
44//! ```
45#![warn(missing_docs)]
46
47pub mod error;
48pub mod value;
49
50mod ast;
51mod parser;
52mod render;
53
54pub use error::TemplateError;
55pub use value::{Context, Value};
56
57/// A parsed chat template ready to be rendered against a [`Context`].
58///
59/// Construct one with [`ChatTemplate::parse`] and render it repeatedly with
60/// [`ChatTemplate::render`].
61#[derive(Debug, Clone)]
62pub struct ChatTemplate {
63    inner: ast::Template,
64}
65
66impl ChatTemplate {
67    /// Parse a chat-template source string into a [`ChatTemplate`].
68    ///
69    /// # Errors
70    /// Returns [`TemplateError::Parse`] if the template syntax is invalid.
71    pub fn parse(src: &str) -> Result<ChatTemplate, TemplateError> {
72        let inner = parser::parse(src)?;
73        Ok(ChatTemplate { inner })
74    }
75
76    /// Render the template against `context`, returning the resulting string.
77    ///
78    /// # Errors
79    /// Returns a [`TemplateError`] if rendering fails (e.g. an undefined
80    /// variable or a type mismatch).
81    pub fn render(&self, context: &Context) -> Result<String, TemplateError> {
82        let mut out = String::new();
83        render::render_nodes(&self.inner.nodes, context, &mut out)?;
84        Ok(out)
85    }
86}
87
88impl Value {
89    /// Convenience constructor for an object [`Value`] from key/value pairs.
90    #[must_use]
91    pub fn object(pairs: Vec<(String, Value)>) -> Value {
92        use std::collections::HashMap;
93        let mut map = HashMap::new();
94        for (k, v) in pairs {
95            map.insert(k, v);
96        }
97        Value::Object(map)
98    }
99}