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.chars().filter(|c| c.is_alphabetic() && c.len_utf8() > 1).count();
141 let english_words = content.split_whitespace().count();
142 chinese_chars / 3 + english_words + (content.len() - chinese_chars) / 4
143 }
144}
145
146pub struct SectionBuilder {
148 sections: Vec<PromptSection>,
149}
150
151impl SectionBuilder {
152 pub fn new() -> Self {
153 Self { sections: Vec::new() }
154 }
155
156 pub fn add_static(self, name: impl Into<String>, content: &'static str) -> Self {
158 self.add_section(PromptSection::static_section(name, content))
159 }
160
161 pub fn add_dynamic<F>(self, name: impl Into<String>, compute: F) -> Self
163 where
164 F: Fn() -> String + Send + Sync + 'static,
165 {
166 self.add_section(PromptSection::dynamic_section(name, compute))
167 }
168
169 pub fn add_section(mut self, section: PromptSection) -> Self {
171 self.sections.push(section);
172 self
173 }
174
175 pub fn build(self) -> Vec<PromptSection> {
177 let mut sections = self.sections;
178 sections.sort_by_key(|s| s.order);
179 sections
180 }
181}
182
183impl Default for SectionBuilder {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_static_section() {
195 let section = PromptSection::static_section("identity", "You are an AI assistant.");
196 assert!(section.cacheable);
197 assert_eq!(section.compute_content(), "You are an AI assistant.");
198 }
199
200 #[test]
201 fn test_dynamic_section() {
202 let section = PromptSection::dynamic_section("date", || {
203 format!("Current date: {}", chrono::Local::now().format("%Y-%m-%d"))
204 });
205 assert!(!section.cacheable);
206 let content = section.compute_content();
207 assert!(content.starts_with("Current date:"));
208 }
209
210 #[test]
211 fn test_render_with_header() {
212 let section = PromptSection::static_section("test", "Hello");
213 let rendered = section.render();
214 assert_eq!(rendered, "[test]\nHello");
215 }
216
217 #[test]
218 fn test_section_builder() {
219 let sections = SectionBuilder::new()
220 .add_static("a", "content a")
221 .add_static("b", "content b")
222 .build();
223 assert_eq!(sections.len(), 2);
224 }
225
226 #[test]
227 fn test_order_sorting() {
228 let sections = SectionBuilder::new()
229 .add_section(PromptSection::static_section("last", "c").with_order(10))
230 .add_section(PromptSection::static_section("first", "a").with_order(1))
231 .add_section(PromptSection::static_section("middle", "b").with_order(5))
232 .build();
233
234 assert_eq!(sections[0].name, "first");
235 assert_eq!(sections[1].name, "middle");
236 assert_eq!(sections[2].name, "last");
237 }
238}