use crate::messages::{Message, Role};
use crate::tools::ToolSchema;
pub trait ChatTemplate: Send + Sync {
fn name(&self) -> &str;
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String;
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String;
fn format_message(&self, message: &Message) -> String;
fn assistant_prefix(&self) -> &str;
}
fn render_tools(tools: &[ToolSchema]) -> String {
if tools.is_empty() {
return String::new();
}
let mut s = String::from("\n\nYou have access to the following tools:\n\n");
for tool in tools {
s.push_str(&format!(
"### {}\n{}\nParameters: {}\n\n",
tool.name,
tool.description,
serde_json::to_string_pretty(&tool.parameters).unwrap_or_default()
));
}
s.push_str(
"To use a tool, respond with a JSON block:\n\
```tool_call\n\
{\"name\": \"tool_name\", \"arguments\": {...}}\n\
```\n",
);
s
}
fn render_tool_result(message: &Message) -> String {
format!("[Tool result]\n{}", message.content)
}
pub struct ChatMLTemplate;
impl ChatTemplate for ChatMLTemplate {
fn name(&self) -> &str {
"chatml"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let mut prompt = self.format_system(system_prompt, tools);
for msg in messages.iter().filter(|m| m.role != Role::System) {
prompt.push_str(&self.format_message(msg));
}
prompt.push_str(self.assistant_prefix());
prompt
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let mut s = String::from("<|im_start|>system\n");
s.push_str(system_prompt);
s.push_str(&render_tools(tools));
s.push_str("<|im_end|>\n");
s
}
fn format_message(&self, message: &Message) -> String {
let role_str = match message.role {
Role::User => "user",
Role::Assistant => "assistant",
Role::ToolResult => "tool",
Role::ToolCall => "assistant",
Role::System => return String::new(),
};
format!("<|im_start|>{role_str}\n{}<|im_end|>\n", message.content)
}
fn assistant_prefix(&self) -> &str {
"<|im_start|>assistant\n"
}
}
pub struct Llama3Template;
impl Llama3Template {
fn header(&self, role: &str, content: &str) -> String {
format!("<|start_header_id|>{role}<|end_header_id|>\n\n{content}<|eot_id|>")
}
}
impl ChatTemplate for Llama3Template {
fn name(&self) -> &str {
"llama3"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let mut prompt = self.format_system(system_prompt, tools);
for msg in messages.iter().filter(|m| m.role != Role::System) {
prompt.push_str(&self.format_message(msg));
}
prompt.push_str(self.assistant_prefix());
prompt
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let mut content = String::from(system_prompt);
content.push_str(&render_tools(tools));
format!("<|begin_of_text|>{}", self.header("system", &content))
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => self.header("user", &message.content),
Role::Assistant | Role::ToolCall => self.header("assistant", &message.content),
Role::ToolResult => self.header("ipython", &message.content),
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
"<|start_header_id|>assistant<|end_header_id|>\n\n"
}
}
pub struct MistralTemplate;
impl MistralTemplate {
fn system_text(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let mut s = String::from(system_prompt);
s.push_str(&render_tools(tools));
s
}
}
impl ChatTemplate for MistralTemplate {
fn name(&self) -> &str {
"mistral"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let system = self.system_text(system_prompt, tools);
let mut out = String::from("<s>");
let mut system_pending = !system.is_empty();
for msg in messages.iter().filter(|m| m.role != Role::System) {
match msg.role {
Role::User | Role::ToolResult => {
let body = if msg.role == Role::ToolResult {
render_tool_result(msg)
} else {
msg.content.clone()
};
let body = if system_pending {
system_pending = false;
format!("{system}\n\n{body}")
} else {
body
};
out.push_str(&format!("[INST] {body} [/INST]"));
}
Role::Assistant | Role::ToolCall => {
out.push_str(&format!(" {}</s>", msg.content));
}
Role::System => {}
}
}
out
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let system = self.system_text(system_prompt, tools);
if system.is_empty() {
String::from("<s>")
} else {
format!("<s>[INST] {system}\n\n")
}
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => format!("[INST] {} [/INST]", message.content),
Role::Assistant | Role::ToolCall => format!(" {}</s>", message.content),
Role::ToolResult => format!("[INST] {} [/INST]", render_tool_result(message)),
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
" "
}
}
pub struct AlpacaTemplate;
const ALPACA_PREAMBLE: &str =
"Below is an instruction that describes a task. Write a response that appropriately completes the request.";
impl ChatTemplate for AlpacaTemplate {
fn name(&self) -> &str {
"alpaca"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let mut prompt = self.format_system(system_prompt, tools);
for msg in messages.iter().filter(|m| m.role != Role::System) {
prompt.push_str(&self.format_message(msg));
}
prompt.push_str(self.assistant_prefix());
prompt
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let preamble = if system_prompt.is_empty() {
ALPACA_PREAMBLE
} else {
system_prompt
};
format!("{preamble}{}\n\n", render_tools(tools))
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => format!("### Instruction:\n{}\n\n", message.content),
Role::Assistant | Role::ToolCall => format!("### Response:\n{}\n\n", message.content),
Role::ToolResult => {
format!("### Instruction:\n{}\n\n", render_tool_result(message))
}
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
"### Response:\n"
}
}
pub struct VicunaTemplate;
const VICUNA_PREAMBLE: &str = "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.";
impl ChatTemplate for VicunaTemplate {
fn name(&self) -> &str {
"vicuna"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let mut prompt = self.format_system(system_prompt, tools);
for msg in messages.iter().filter(|m| m.role != Role::System) {
prompt.push_str(&self.format_message(msg));
}
prompt.push_str(self.assistant_prefix());
prompt
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let preamble = if system_prompt.is_empty() {
VICUNA_PREAMBLE
} else {
system_prompt
};
format!("{preamble}{} ", render_tools(tools))
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => format!("USER: {} ", message.content),
Role::Assistant | Role::ToolCall => format!("ASSISTANT: {}</s>", message.content),
Role::ToolResult => format!("USER: {} ", render_tool_result(message)),
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
"ASSISTANT: "
}
}
pub struct GemmaTemplate;
impl GemmaTemplate {
fn system_text(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let mut s = String::from(system_prompt);
s.push_str(&render_tools(tools));
s
}
}
impl ChatTemplate for GemmaTemplate {
fn name(&self) -> &str {
"gemma"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let system = self.system_text(system_prompt, tools);
let mut out = String::from("<bos>");
let mut system_pending = !system.is_empty();
for msg in messages.iter().filter(|m| m.role != Role::System) {
match msg.role {
Role::User | Role::ToolResult => {
let body = if msg.role == Role::ToolResult {
render_tool_result(msg)
} else {
msg.content.clone()
};
let body = if system_pending {
system_pending = false;
format!("{system}\n\n{body}")
} else {
body
};
out.push_str(&format!("<start_of_turn>user\n{body}<end_of_turn>\n"));
}
Role::Assistant | Role::ToolCall => {
out.push_str(&format!(
"<start_of_turn>model\n{}<end_of_turn>\n",
msg.content
));
}
Role::System => {}
}
}
out.push_str(self.assistant_prefix());
out
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let system = self.system_text(system_prompt, tools);
if system.is_empty() {
String::from("<bos>")
} else {
format!("<bos><start_of_turn>user\n{system}\n\n")
}
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => format!("<start_of_turn>user\n{}<end_of_turn>\n", message.content),
Role::Assistant | Role::ToolCall => {
format!("<start_of_turn>model\n{}<end_of_turn>\n", message.content)
}
Role::ToolResult => format!(
"<start_of_turn>user\n{}<end_of_turn>\n",
render_tool_result(message)
),
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
"<start_of_turn>model\n"
}
}
pub struct Phi3Template;
impl ChatTemplate for Phi3Template {
fn name(&self) -> &str {
"phi3"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let mut prompt = self.format_system(system_prompt, tools);
for msg in messages.iter().filter(|m| m.role != Role::System) {
prompt.push_str(&self.format_message(msg));
}
prompt.push_str(self.assistant_prefix());
prompt
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let tools_block = render_tools(tools);
if system_prompt.is_empty() && tools_block.is_empty() {
return String::new();
}
format!("<|system|>\n{system_prompt}{tools_block}<|end|>\n")
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => format!("<|user|>\n{}<|end|>\n", message.content),
Role::Assistant | Role::ToolCall => {
format!("<|assistant|>\n{}<|end|>\n", message.content)
}
Role::ToolResult => format!("<|user|>\n{}<|end|>\n", render_tool_result(message)),
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
"<|assistant|>\n"
}
}
const DEEPSEEK_BOS: &str = "<|begin▁of▁sentence|>";
const DEEPSEEK_EOS: &str = "<|end▁of▁sentence|>";
pub struct DeepSeekTemplate;
impl ChatTemplate for DeepSeekTemplate {
fn name(&self) -> &str {
"deepseek"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let mut prompt = self.format_system(system_prompt, tools);
for msg in messages.iter().filter(|m| m.role != Role::System) {
prompt.push_str(&self.format_message(msg));
}
prompt.push_str(self.assistant_prefix());
prompt
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let mut s = String::from(system_prompt);
s.push_str(&render_tools(tools));
if s.is_empty() {
String::from(DEEPSEEK_BOS)
} else {
format!("{DEEPSEEK_BOS}{s}\n\n")
}
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => format!("User: {}\n\n", message.content),
Role::Assistant | Role::ToolCall => {
format!("Assistant: {}{DEEPSEEK_EOS}", message.content)
}
Role::ToolResult => format!("User: {}\n\n", render_tool_result(message)),
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
"Assistant:"
}
}
pub struct CommandRTemplate;
impl ChatTemplate for CommandRTemplate {
fn name(&self) -> &str {
"command-r"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let mut prompt = self.format_system(system_prompt, tools);
for msg in messages.iter().filter(|m| m.role != Role::System) {
prompt.push_str(&self.format_message(msg));
}
prompt.push_str(self.assistant_prefix());
prompt
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let mut s = String::from(system_prompt);
s.push_str(&render_tools(tools));
if s.is_empty() {
String::from("<BOS_TOKEN>")
} else {
format!("<BOS_TOKEN><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{s}<|END_OF_TURN_TOKEN|>")
}
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => format!(
"<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{}<|END_OF_TURN_TOKEN|>",
message.content
),
Role::Assistant | Role::ToolCall => format!(
"<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{}<|END_OF_TURN_TOKEN|>",
message.content
),
Role::ToolResult => format!(
"<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{}<|END_OF_TURN_TOKEN|>",
render_tool_result(message)
),
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
"<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>"
}
}
pub struct Llama2Template;
impl Llama2Template {
fn system_text(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let mut s = String::from(system_prompt);
s.push_str(&render_tools(tools));
s
}
}
impl ChatTemplate for Llama2Template {
fn name(&self) -> &str {
"llama2"
}
fn format(&self, system_prompt: &str, messages: &[Message], tools: &[ToolSchema]) -> String {
let system = self.system_text(system_prompt, tools);
let mut out = String::new();
let mut system_pending = !system.is_empty();
for msg in messages.iter().filter(|m| m.role != Role::System) {
match msg.role {
Role::User | Role::ToolResult => {
let body = if msg.role == Role::ToolResult {
render_tool_result(msg)
} else {
msg.content.clone()
};
let inst = if system_pending {
system_pending = false;
format!("<<SYS>>\n{system}\n<</SYS>>\n\n{body}")
} else {
body
};
out.push_str(&format!("<s>[INST] {inst} [/INST]"));
}
Role::Assistant | Role::ToolCall => {
out.push_str(&format!(" {} </s>", msg.content));
}
Role::System => {}
}
}
out
}
fn format_system(&self, system_prompt: &str, tools: &[ToolSchema]) -> String {
let system = self.system_text(system_prompt, tools);
if system.is_empty() {
String::from("<s>")
} else {
format!("<s>[INST] <<SYS>>\n{system}\n<</SYS>>\n\n")
}
}
fn format_message(&self, message: &Message) -> String {
match message.role {
Role::User => format!("<s>[INST] {} [/INST]", message.content),
Role::Assistant | Role::ToolCall => format!(" {} </s>", message.content),
Role::ToolResult => format!("<s>[INST] {} [/INST]", render_tool_result(message)),
Role::System => String::new(),
}
}
fn assistant_prefix(&self) -> &str {
" "
}
}
pub fn template_from_name(name: &str) -> Option<Box<dyn ChatTemplate>> {
match name.trim().to_ascii_lowercase().as_str() {
"chatml" => Some(Box::new(ChatMLTemplate)),
"llama3" | "llama-3" | "llama3.1" | "llama-3.1" => Some(Box::new(Llama3Template)),
"llama2" | "llama-2" | "llama 2" => Some(Box::new(Llama2Template)),
"mistral" | "mixtral" => Some(Box::new(MistralTemplate)),
"alpaca" => Some(Box::new(AlpacaTemplate)),
"vicuna" => Some(Box::new(VicunaTemplate)),
"gemma" | "gemma2" => Some(Box::new(GemmaTemplate)),
"phi3" | "phi-3" | "phi" => Some(Box::new(Phi3Template)),
"deepseek" | "deepseek-llm" => Some(Box::new(DeepSeekTemplate)),
"command-r" | "commandr" | "command_r" | "cohere" => Some(Box::new(CommandRTemplate)),
_ => None,
}
}
pub fn detect_template(gguf_template: Option<&str>) -> Box<dyn ChatTemplate> {
if let Some(tmpl) = gguf_template {
if tmpl.contains("<|start_header_id|>") || tmpl.contains("<|begin_of_text|>") {
return Box::new(Llama3Template);
}
if tmpl.contains("<|START_OF_TURN_TOKEN|>") || tmpl.contains("<|CHATBOT_TOKEN|>") {
return Box::new(CommandRTemplate);
}
if tmpl.contains("<|im_start|>") {
return Box::new(ChatMLTemplate);
}
if tmpl.contains("<|assistant|>")
&& (tmpl.contains("<|user|>") || tmpl.contains("<|system|>"))
{
return Box::new(Phi3Template);
}
if tmpl.contains("<start_of_turn>") {
return Box::new(GemmaTemplate);
}
if tmpl.contains("<<SYS>>") {
return Box::new(Llama2Template);
}
if tmpl.contains("[INST]") {
return Box::new(MistralTemplate);
}
if tmpl.contains("### Instruction:") {
return Box::new(AlpacaTemplate);
}
if (tmpl.contains("User:") && tmpl.contains("Assistant:")) || tmpl.contains("▁of▁sentence")
{
return Box::new(DeepSeekTemplate);
}
if tmpl.contains("ASSISTANT:") && tmpl.contains("USER:") {
return Box::new(VicunaTemplate);
}
log::debug!(
"Unknown chat template format (len={}), falling back to ChatML",
tmpl.len()
);
}
Box::new(ChatMLTemplate)
}