use std::sync::Arc;
use minijinja::{Environment, UndefinedBehavior, Value};
use serde_json::{Map, Value as Json};
use crate::clock::{Clock, SystemClock};
use crate::config::{self, ChatTemplateField, TokenizerConfig};
use crate::engine::{self, EngineConfig};
use crate::error::Error;
use crate::model::{Message, RenderInput};
pub struct ChatTemplate {
env: Environment<'static>,
special_tokens: Vec<(String, String)>,
}
impl std::fmt::Debug for ChatTemplate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChatTemplate")
.field("special_tokens", &self.special_tokens)
.finish_non_exhaustive()
}
}
impl ChatTemplate {
#[allow(clippy::should_implement_trait)]
pub fn from_str(source: &str) -> Result<Self, Error> {
ChatTemplate::builder(source).build()
}
pub fn from_tokenizer_config(config: &TokenizerConfig) -> Result<Self, Error> {
ChatTemplate::builder_from_config(config)?.build()
}
pub fn builder(source: &str) -> ChatTemplateBuilder {
ChatTemplateBuilder::new(BuilderSource::Raw(source.to_owned()), Vec::new())
}
pub fn builder_from_config(config: &TokenizerConfig) -> Result<ChatTemplateBuilder, Error> {
let field = config
.chat_template
.clone()
.ok_or_else(|| Error::Config("tokenizer config has no chat_template".into()))?;
Ok(ChatTemplateBuilder::new(
BuilderSource::Field(field),
config.special_tokens(),
))
}
pub fn render(&self, input: &RenderInput) -> Result<String, Error> {
let ctx = self.build_context(input)?;
self.render_value(ctx)
}
pub fn render_messages(
&self,
messages: &[Message],
add_generation_prompt: bool,
) -> Result<String, Error> {
let input = RenderInput {
messages: messages.to_vec(),
add_generation_prompt,
..Default::default()
};
self.render(&input)
}
pub fn render_value(&self, ctx: Value) -> Result<String, Error> {
let tmpl = self
.env
.get_template("chat")
.expect("the 'chat' template is always present after construction");
tmpl.render(ctx).map_err(Error::from_render)
}
fn build_context(&self, input: &RenderInput) -> Result<Value, Error> {
let mut ctx: Map<String, Json> = Map::new();
for (k, v) in &self.special_tokens {
ctx.insert(k.clone(), Json::String(v.clone()));
}
let serialized = serde_json::to_value(input)
.map_err(|e| Error::Config(format!("failed to serialize render input: {e}")))?;
if let Json::Object(map) = serialized {
for (k, v) in map {
ctx.insert(k, v);
}
}
Ok(Value::from_serialize(Json::Object(ctx)))
}
}
enum BuilderSource {
Raw(String),
Field(ChatTemplateField),
}
pub struct ChatTemplateBuilder {
source: BuilderSource,
special_tokens: Vec<(String, String)>,
template_name: Option<String>,
cfg: EngineConfig,
}
impl ChatTemplateBuilder {
fn new(source: BuilderSource, special_tokens: Vec<(String, String)>) -> Self {
ChatTemplateBuilder {
source,
special_tokens,
template_name: None,
cfg: EngineConfig {
trim_blocks: true,
lstrip_blocks: true,
keep_trailing_newline: true,
undefined: UndefinedBehavior::Lenient,
pycompat: true,
clock: Arc::new(SystemClock),
},
}
}
pub fn template_name(mut self, name: impl Into<String>) -> Self {
self.template_name = Some(name.into());
self
}
pub fn clock(mut self, clock: impl Clock + 'static) -> Self {
self.cfg.clock = Arc::new(clock);
self
}
pub fn pycompat(mut self, enabled: bool) -> Self {
self.cfg.pycompat = enabled;
self
}
pub fn undefined_behavior(mut self, ub: UndefinedBehavior) -> Self {
self.cfg.undefined = ub;
self
}
pub fn build(self) -> Result<ChatTemplate, Error> {
let source: String = match &self.source {
BuilderSource::Raw(s) => s.clone(),
BuilderSource::Field(field) => {
config::resolve_template(field, self.template_name.as_deref())?.to_owned()
}
};
let env = engine::build(source, &self.cfg).map_err(Error::Compile)?;
Ok(ChatTemplate {
env,
special_tokens: self.special_tokens,
})
}
}
impl std::str::FromStr for ChatTemplate {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
ChatTemplate::from_str(s)
}
}