systemprompt_templates/
core_provider.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use serde::Deserialize;
5use systemprompt_template_provider::{TemplateDefinition, TemplateProvider, TemplateSource};
6use tokio::fs;
7use tracing::{debug, warn};
8
9#[derive(Debug, Deserialize, Default)]
10struct TemplateManifest {
11 #[serde(default)]
12 templates: HashMap<String, TemplateConfig>,
13}
14
15#[derive(Debug, Deserialize)]
16struct TemplateConfig {
17 #[serde(default)]
18 content_types: Vec<String>,
19}
20
21#[derive(Debug)]
22pub struct CoreTemplateProvider {
23 template_dir: PathBuf,
24 templates: Vec<TemplateDefinition>,
25 priority: u32,
26}
27
28impl CoreTemplateProvider {
29 pub const DEFAULT_PRIORITY: u32 = 1000;
30 pub const EXTENSION_PRIORITY: u32 = 500;
31
32 #[must_use]
33 pub fn new(template_dir: impl Into<PathBuf>) -> Self {
34 Self {
35 template_dir: template_dir.into(),
36 templates: Vec::new(),
37 priority: Self::DEFAULT_PRIORITY,
38 }
39 }
40
41 #[must_use]
42 pub fn with_priority(template_dir: impl Into<PathBuf>, priority: u32) -> Self {
43 Self {
44 template_dir: template_dir.into(),
45 templates: Vec::new(),
46 priority,
47 }
48 }
49
50 pub async fn discover(&mut self) -> anyhow::Result<()> {
51 self.templates = discover_templates(&self.template_dir, self.priority).await?;
52 Ok(())
53 }
54
55 pub async fn discover_from(template_dir: impl Into<PathBuf>) -> anyhow::Result<Self> {
56 let mut provider = Self::new(template_dir);
57 provider.discover().await?;
58 Ok(provider)
59 }
60
61 pub async fn discover_with_priority(
62 template_dir: impl Into<PathBuf>,
63 priority: u32,
64 ) -> anyhow::Result<Self> {
65 let mut provider = Self::with_priority(template_dir, priority);
66 provider.discover().await?;
67 Ok(provider)
68 }
69}
70
71impl TemplateProvider for CoreTemplateProvider {
72 fn provider_id(&self) -> &'static str {
73 "core"
74 }
75
76 fn priority(&self) -> u32 {
77 self.priority
78 }
79
80 fn templates(&self) -> Vec<TemplateDefinition> {
81 self.templates.clone()
82 }
83}
84
85async fn load_manifest(dir: &Path) -> TemplateManifest {
86 let manifest_path = dir.join("templates.yaml");
87
88 let Ok(content) = fs::read_to_string(&manifest_path).await else {
89 return TemplateManifest::default();
90 };
91
92 match serde_yaml::from_str(&content) {
93 Ok(manifest) => {
94 debug!(path = %manifest_path.display(), "Loaded template manifest");
95 manifest
96 },
97 Err(e) => {
98 warn!(
99 path = %manifest_path.display(),
100 error = %e,
101 "Failed to parse template manifest, using defaults"
102 );
103 TemplateManifest::default()
104 },
105 }
106}
107
108async fn discover_templates(dir: &Path, priority: u32) -> anyhow::Result<Vec<TemplateDefinition>> {
109 let mut templates = Vec::new();
110
111 if !dir.exists() {
112 return Ok(templates);
113 }
114
115 let manifest = load_manifest(dir).await;
116 let mut entries = fs::read_dir(dir).await?;
117
118 while let Some(entry) = entries.next_entry().await? {
119 let path = entry.path();
120
121 if path.extension().is_some_and(|ext| ext == "html") {
122 let Some(file_stem) = path.file_stem() else {
123 continue;
124 };
125 let template_name = file_stem.to_string_lossy().to_string();
126
127 debug!(
128 template = %template_name,
129 path = %path.display(),
130 priority = priority,
131 "Discovered template"
132 );
133
134 let content_types = manifest.templates.get(&template_name).map_or_else(
135 || infer_content_types(&template_name),
136 |config| config.content_types.clone(),
137 );
138
139 let filename = path.file_name().map_or_else(|| path.clone(), PathBuf::from);
140
141 templates.push(TemplateDefinition {
142 name: template_name,
143 source: TemplateSource::File(filename),
144 priority,
145 content_types,
146 });
147 }
148 }
149
150 Ok(templates)
151}
152
153fn infer_content_types(name: &str) -> Vec<String> {
154 match name {
155 _ if name.ends_with("-post") => {
156 let content_type = name.trim_end_matches("-post");
157 vec![content_type.into()]
158 },
159 _ if name.ends_with("-list") => {
160 vec![name.into()]
161 },
162 _ => vec![],
163 }
164}