matrixcode_core/prompt/
section.rs1use std::sync::Arc;
8
9#[derive(Clone)]
11pub enum SectionContent {
12 Static(&'static str),
14 Dynamic(Arc<dyn Fn() -> String + Send + Sync>),
16 Cached(String),
18}
19
20impl SectionContent {
21 pub fn static_content(s: &'static str) -> Self {
23 Self::Static(s)
24 }
25
26 pub fn dynamic<F>(f: F) -> Self
28 where
29 F: Fn() -> String + Send + Sync + 'static,
30 {
31 Self::Dynamic(Arc::new(f))
32 }
33
34 pub fn compute(&self) -> String {
36 match self {
37 Self::Static(s) => s.to_string(),
38 Self::Dynamic(f) => f(),
39 Self::Cached(s) => s.clone(),
40 }
41 }
42
43 pub fn is_cacheable(&self) -> bool {
45 matches!(self, Self::Static(_) | Self::Cached(_))
46 }
47
48 pub fn cache(self, content: String) -> Self {
50 Self::Cached(content)
51 }
52}
53
54impl std::fmt::Debug for SectionContent {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 Self::Static(s) => f.debug_tuple("Static").field(&s.len()).finish(),
58 Self::Dynamic(_) => f.write_str("Dynamic(<function>)"),
59 Self::Cached(s) => f.debug_tuple("Cached").field(&s.len()).finish(),
60 }
61 }
62}
63
64#[derive(Clone, Debug)]
66pub struct PromptSection {
67 pub name: String,
69 pub content: SectionContent,
71 pub cacheable: bool,
73 pub order: usize,
75}
76
77impl PromptSection {
78 pub fn static_section(name: impl Into<String>, content: &'static str) -> Self {
80 Self {
81 name: name.into(),
82 content: SectionContent::static_content(content),
83 cacheable: true,
84 order: 0,
85 }
86 }
87
88 pub fn dynamic_section<F>(name: impl Into<String>, compute: F) -> Self
90 where
91 F: Fn() -> String + Send + Sync + 'static,
92 {
93 Self {
94 name: name.into(),
95 content: SectionContent::dynamic(compute),
96 cacheable: false,
97 order: 0,
98 }
99 }
100
101 pub fn cached_section(name: impl Into<String>, content: String) -> Self {
103 Self {
104 name: name.into(),
105 content: SectionContent::Cached(content),
106 cacheable: true,
107 order: 0,
108 }
109 }
110
111 pub fn with_order(self, order: usize) -> Self {
113 Self { order, ..self }
114 }
115
116 pub fn with_cacheable(self, cacheable: bool) -> Self {
118 Self { cacheable, ..self }
119 }
120
121 pub fn render(&self) -> String {
123 let content = self.content.compute();
124 if content.is_empty() {
125 String::new()
126 } else {
127 format!("[{}]\n{}", self.name, content)
128 }
129 }
130
131 pub fn compute_content(&self) -> String {
133 self.content.compute()
134 }
135
136 pub fn estimated_tokens(&self) -> usize {
138 let content = self.compute_content();
140 let chinese_chars = content
141 .chars()
142 .filter(|c| c.is_alphabetic() && c.len_utf8() > 1)
143 .count();
144 let english_words = content.split_whitespace().count();
145 chinese_chars / 3 + english_words + (content.len() - chinese_chars) / 4
146 }
147}
148
149pub struct SectionBuilder {
151 sections: Vec<PromptSection>,
152}
153
154impl SectionBuilder {
155 pub fn new() -> Self {
156 Self {
157 sections: Vec::new(),
158 }
159 }
160
161 pub fn add_static(self, name: impl Into<String>, content: &'static str) -> Self {
163 self.add_section(PromptSection::static_section(name, content))
164 }
165
166 pub fn add_dynamic<F>(self, name: impl Into<String>, compute: F) -> Self
168 where
169 F: Fn() -> String + Send + Sync + 'static,
170 {
171 self.add_section(PromptSection::dynamic_section(name, compute))
172 }
173
174 pub fn add_section(mut self, section: PromptSection) -> Self {
176 self.sections.push(section);
177 self
178 }
179
180 pub fn build(self) -> Vec<PromptSection> {
182 let mut sections = self.sections;
183 sections.sort_by_key(|s| s.order);
184 sections
185 }
186}
187
188impl Default for SectionBuilder {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_static_section() {
200 let section = PromptSection::static_section("identity", "You are an AI assistant.");
201 assert!(section.cacheable);
202 assert_eq!(section.compute_content(), "You are an AI assistant.");
203 }
204
205 #[test]
206 fn test_dynamic_section() {
207 let section = PromptSection::dynamic_section("date", || {
208 format!("Current date: {}", chrono::Local::now().format("%Y-%m-%d"))
209 });
210 assert!(!section.cacheable);
211 let content = section.compute_content();
212 assert!(content.starts_with("Current date:"));
213 }
214
215 #[test]
216 fn test_render_with_header() {
217 let section = PromptSection::static_section("test", "Hello");
218 let rendered = section.render();
219 assert_eq!(rendered, "[test]\nHello");
220 }
221
222 #[test]
223 fn test_section_builder() {
224 let sections = SectionBuilder::new()
225 .add_static("a", "content a")
226 .add_static("b", "content b")
227 .build();
228 assert_eq!(sections.len(), 2);
229 }
230
231 #[test]
232 fn test_order_sorting() {
233 let sections = SectionBuilder::new()
234 .add_section(PromptSection::static_section("last", "c").with_order(10))
235 .add_section(PromptSection::static_section("first", "a").with_order(1))
236 .add_section(PromptSection::static_section("middle", "b").with_order(5))
237 .build();
238
239 assert_eq!(sections[0].name, "first");
240 assert_eq!(sections[1].name, "middle");
241 assert_eq!(sections[2].name, "last");
242 }
243}