1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
//! Handlebars template engine for prompt management
//!
//! This module provides a centralized template engine that loads and manages
//! Handlebars templates from embedded resources, supporting partials.
//!
//! Templates are embedded at compile time using include_dir, so the binary
//! is self-contained and works in release builds.
use anyhow::{Context, Result};
use handlebars::Handlebars;
use include_dir::{include_dir, Dir};
use once_cell::sync::Lazy;
use std::sync::{Arc, RwLock};
use tracing::{debug, error, info, warn};
/// Embedded template directory (compiled into binary)
static EMBEDDED_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/prompts");
/// Global template engine instance
pub static TEMPLATE_ENGINE: Lazy<Arc<TemplateEngine>> = Lazy::new(|| {
info!("Initializing template engine from embedded templates");
let engine = TemplateEngine::from_embedded().unwrap_or_else(|e| {
warn!(
"Failed to initialize template engine from embedded templates: {}",
e
);
TemplateEngine::empty()
});
info!(
"Template engine initialized with {} templates",
engine.get_templates().len()
);
for template_name in engine.get_templates() {
debug!(" Loaded template: {}", template_name);
}
Arc::new(engine)
});
/// Template engine that manages Handlebars templates
pub struct TemplateEngine {
handlebars: RwLock<Handlebars<'static>>,
}
impl TemplateEngine {
/// Create a template engine from embedded templates
pub fn from_embedded() -> Result<Self> {
let mut handlebars = Handlebars::new();
// Set strict mode to catch template errors
handlebars.set_strict_mode(true);
// Load all templates and partials from embedded directory
Self::load_embedded_templates(&mut handlebars, &EMBEDDED_TEMPLATES, "")?;
Ok(Self {
handlebars: RwLock::new(handlebars),
})
}
/// Create an empty template engine (fallback mode)
pub fn empty() -> Self {
Self {
handlebars: RwLock::new(Handlebars::new()),
}
}
/// Load all templates and partials from embedded directory
fn load_embedded_templates(
handlebars: &mut Handlebars<'static>,
dir: &Dir,
prefix: &str,
) -> Result<()> {
// Process all files in this directory
for file in dir.files() {
let file_path = file.path();
// Only process .hbs files
if file_path.extension().and_then(|s| s.to_str()) != Some("hbs") {
continue;
}
// Get the full relative path
let file_name = file_path.file_name().unwrap().to_string_lossy().to_string();
let relative_path = if prefix.is_empty() {
file_name
} else {
format!("{}/{}", prefix, file_name)
};
// Convert to template name (without .hbs extension)
let template_name = relative_path
.trim_end_matches(".hbs")
.replace(std::path::MAIN_SEPARATOR, "/");
// Get template content
let content = file
.contents_utf8()
.context("Template file is not valid UTF-8")?;
// Check if this is a partial (in a "partials" directory)
let is_partial = file_path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
== Some("partials");
if is_partial {
// Register as partial (keep the full path name)
handlebars
.register_partial(&template_name, content)
.with_context(|| format!("Failed to register partial: {}", template_name))?;
debug!("Registered partial: {}", template_name);
} else {
// Register as template
handlebars
.register_template_string(&template_name, content)
.with_context(|| format!("Failed to register template: {}", template_name))?;
debug!("Registered template: {}", template_name);
}
}
// Recursively process subdirectories
for subdir in dir.dirs() {
let subdir_name = subdir
.path()
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
let new_prefix = if prefix.is_empty() {
subdir_name
} else {
format!("{}/{}", prefix, subdir_name)
};
Self::load_embedded_templates(handlebars, subdir, &new_prefix)?;
}
Ok(())
}
/// Reload templates (no-op for embedded templates, kept for API compatibility)
pub fn reload(&self) -> Result<()> {
// Templates are embedded, so there's nothing to reload
info!("Reload requested but templates are embedded (no-op)");
Ok(())
}
/// Render a template with the given data
///
/// The lock is taken with `unwrap_or_else(|e| e.into_inner())` rather than `unwrap()`.
/// This is shared infrastructure: a single panic while any reader held it would poison
/// the `RwLock` and make **every subsequent prompt build in the process** panic, turning
/// one transient fault into a permanent outage. The `Handlebars` registry behind it is
/// immutable after construction, so recovering the guard is safe.
pub fn render<T>(&self, template_name: &str, data: &T) -> Result<String>
where
T: serde::Serialize,
{
let handlebars = self.handlebars.read().unwrap_or_else(|e| e.into_inner());
// Check if template exists
if !handlebars.has_template(template_name) {
// Fallback: return empty string with warning
warn!(
"Template not found: {}. Using empty template.",
template_name
);
return Ok(String::new());
}
match handlebars.render(template_name, data) {
Ok(result) => Ok(result),
Err(e) => {
let available_partials: Vec<_> = handlebars
.get_templates()
.keys()
.filter(|k| k.contains("partials"))
.map(|k| k.to_string())
.collect();
let error_msg = format!(
"Template render failed\nTemplate: {}\nHandlebars Error: {}\nAvailable partials: {:#?}",
template_name, e, available_partials
);
error!("{}", error_msg);
Err(anyhow::anyhow!(error_msg))
}
}
}
/// Render a template with raw JSON data
pub fn render_json(&self, template_name: &str, data: &serde_json::Value) -> Result<String> {
self.render(template_name, data)
}
/// Check if a template exists
pub fn has_template(&self, template_name: &str) -> bool {
let handlebars = self.handlebars.read().unwrap_or_else(|e| e.into_inner());
handlebars.has_template(template_name)
}
/// Get all registered template names
pub fn get_templates(&self) -> Vec<String> {
let handlebars = self.handlebars.read().unwrap_or_else(|e| e.into_inner());
handlebars
.get_templates()
.keys()
.map(|k| k.to_string())
.collect()
}
}
/// Helper to build template data
pub struct TemplateDataBuilder {
data: serde_json::Map<String, serde_json::Value>,
}
impl TemplateDataBuilder {
/// Create a new template data builder
pub fn new() -> Self {
Self {
data: serde_json::Map::new(),
}
}
/// Add a field to the template data
pub fn field<T: serde::Serialize>(mut self, key: &str, value: T) -> Self {
self.data.insert(
key.to_string(),
serde_json::to_value(value).unwrap_or(serde_json::Value::Null),
);
self
}
/// Add an optional field to the template data
pub fn optional_field<T: serde::Serialize>(mut self, key: &str, value: Option<T>) -> Self {
if let Some(v) = value {
self.data.insert(
key.to_string(),
serde_json::to_value(v).unwrap_or(serde_json::Value::Null),
);
}
self
}
/// Build the template data as a JSON value
pub fn build(self) -> serde_json::Value {
serde_json::Value::Object(self.data)
}
}
impl Default for TemplateDataBuilder {
fn default() -> Self {
Self::new()
}
}