netget/llm/
template_engine.rs1use anyhow::{Context, Result};
10use handlebars::Handlebars;
11use include_dir::{include_dir, Dir};
12use once_cell::sync::Lazy;
13use std::sync::{Arc, RwLock};
14use tracing::{debug, error, info, warn};
15
16static EMBEDDED_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/prompts");
18
19pub static TEMPLATE_ENGINE: Lazy<Arc<TemplateEngine>> = Lazy::new(|| {
21 info!("Initializing template engine from embedded templates");
22
23 let engine = TemplateEngine::from_embedded().unwrap_or_else(|e| {
24 warn!(
25 "Failed to initialize template engine from embedded templates: {}",
26 e
27 );
28 TemplateEngine::empty()
29 });
30
31 info!(
32 "Template engine initialized with {} templates",
33 engine.get_templates().len()
34 );
35 for template_name in engine.get_templates() {
36 debug!(" Loaded template: {}", template_name);
37 }
38
39 Arc::new(engine)
40});
41
42pub struct TemplateEngine {
44 handlebars: RwLock<Handlebars<'static>>,
45}
46
47impl TemplateEngine {
48 pub fn from_embedded() -> Result<Self> {
50 let mut handlebars = Handlebars::new();
51
52 handlebars.set_strict_mode(true);
54
55 Self::load_embedded_templates(&mut handlebars, &EMBEDDED_TEMPLATES, "")?;
57
58 Ok(Self {
59 handlebars: RwLock::new(handlebars),
60 })
61 }
62
63 pub fn empty() -> Self {
65 Self {
66 handlebars: RwLock::new(Handlebars::new()),
67 }
68 }
69
70 fn load_embedded_templates(
72 handlebars: &mut Handlebars<'static>,
73 dir: &Dir,
74 prefix: &str,
75 ) -> Result<()> {
76 for file in dir.files() {
78 let file_path = file.path();
79
80 if file_path.extension().and_then(|s| s.to_str()) != Some("hbs") {
82 continue;
83 }
84
85 let file_name = file_path.file_name().unwrap().to_string_lossy().to_string();
87 let relative_path = if prefix.is_empty() {
88 file_name
89 } else {
90 format!("{}/{}", prefix, file_name)
91 };
92
93 let template_name = relative_path
95 .trim_end_matches(".hbs")
96 .replace(std::path::MAIN_SEPARATOR, "/");
97
98 let content = file
100 .contents_utf8()
101 .context("Template file is not valid UTF-8")?;
102
103 let is_partial = file_path
105 .parent()
106 .and_then(|p| p.file_name())
107 .and_then(|n| n.to_str())
108 == Some("partials");
109
110 if is_partial {
111 handlebars
113 .register_partial(&template_name, content)
114 .with_context(|| format!("Failed to register partial: {}", template_name))?;
115 debug!("Registered partial: {}", template_name);
116 } else {
117 handlebars
119 .register_template_string(&template_name, content)
120 .with_context(|| format!("Failed to register template: {}", template_name))?;
121 debug!("Registered template: {}", template_name);
122 }
123 }
124
125 for subdir in dir.dirs() {
127 let subdir_name = subdir
128 .path()
129 .file_name()
130 .unwrap()
131 .to_string_lossy()
132 .to_string();
133
134 let new_prefix = if prefix.is_empty() {
135 subdir_name
136 } else {
137 format!("{}/{}", prefix, subdir_name)
138 };
139
140 Self::load_embedded_templates(handlebars, subdir, &new_prefix)?;
141 }
142
143 Ok(())
144 }
145
146 pub fn reload(&self) -> Result<()> {
148 info!("Reload requested but templates are embedded (no-op)");
150 Ok(())
151 }
152
153 pub fn render<T>(&self, template_name: &str, data: &T) -> Result<String>
161 where
162 T: serde::Serialize,
163 {
164 let handlebars = self.handlebars.read().unwrap_or_else(|e| e.into_inner());
165
166 if !handlebars.has_template(template_name) {
168 warn!(
170 "Template not found: {}. Using empty template.",
171 template_name
172 );
173 return Ok(String::new());
174 }
175
176 match handlebars.render(template_name, data) {
177 Ok(result) => Ok(result),
178 Err(e) => {
179 let available_partials: Vec<_> = handlebars
180 .get_templates()
181 .keys()
182 .filter(|k| k.contains("partials"))
183 .map(|k| k.to_string())
184 .collect();
185 let error_msg = format!(
186 "Template render failed\nTemplate: {}\nHandlebars Error: {}\nAvailable partials: {:#?}",
187 template_name, e, available_partials
188 );
189 error!("{}", error_msg);
190 Err(anyhow::anyhow!(error_msg))
191 }
192 }
193 }
194
195 pub fn render_json(&self, template_name: &str, data: &serde_json::Value) -> Result<String> {
197 self.render(template_name, data)
198 }
199
200 pub fn has_template(&self, template_name: &str) -> bool {
202 let handlebars = self.handlebars.read().unwrap_or_else(|e| e.into_inner());
203 handlebars.has_template(template_name)
204 }
205
206 pub fn get_templates(&self) -> Vec<String> {
208 let handlebars = self.handlebars.read().unwrap_or_else(|e| e.into_inner());
209 handlebars
210 .get_templates()
211 .keys()
212 .map(|k| k.to_string())
213 .collect()
214 }
215}
216
217pub struct TemplateDataBuilder {
219 data: serde_json::Map<String, serde_json::Value>,
220}
221
222impl TemplateDataBuilder {
223 pub fn new() -> Self {
225 Self {
226 data: serde_json::Map::new(),
227 }
228 }
229
230 pub fn field<T: serde::Serialize>(mut self, key: &str, value: T) -> Self {
232 self.data.insert(
233 key.to_string(),
234 serde_json::to_value(value).unwrap_or(serde_json::Value::Null),
235 );
236 self
237 }
238
239 pub fn optional_field<T: serde::Serialize>(mut self, key: &str, value: Option<T>) -> Self {
241 if let Some(v) = value {
242 self.data.insert(
243 key.to_string(),
244 serde_json::to_value(v).unwrap_or(serde_json::Value::Null),
245 );
246 }
247 self
248 }
249
250 pub fn build(self) -> serde_json::Value {
252 serde_json::Value::Object(self.data)
253 }
254}
255
256impl Default for TemplateDataBuilder {
257 fn default() -> Self {
258 Self::new()
259 }
260}