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}