Skip to main content

matrixcode_core/prompt/
section.rs

1//! Section-based prompt composition
2//!
3//! Each section can be:
4//! - Static: constant string, cacheable
5//! - Dynamic: computed at runtime, not cacheable
6
7use std::sync::Arc;
8
9/// Section content type
10#[derive(Clone)]
11pub enum SectionContent {
12    /// Static content that can be cached
13    Static(&'static str),
14    /// Dynamic content computed at runtime
15    Dynamic(Arc<dyn Fn() -> String + Send + Sync>),
16    /// Cached result (computed once and stored)
17    Cached(String),
18}
19
20impl SectionContent {
21    /// Create static content
22    pub fn static_content(s: &'static str) -> Self {
23        Self::Static(s)
24    }
25
26    /// Create dynamic content with a computation function
27    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    /// Compute the content (for dynamic sections)
35    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    /// Check if this content can be cached
44    pub fn is_cacheable(&self) -> bool {
45        matches!(self, Self::Static(_) | Self::Cached(_))
46    }
47
48    /// Mark content as cached (after computing)
49    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/// A prompt section with name, content, and cacheability
65#[derive(Clone, Debug)]
66pub struct PromptSection {
67    /// Unique section identifier
68    pub name: String,
69    /// Section content
70    pub content: SectionContent,
71    /// Whether this section should be cached (default: true for static)
72    pub cacheable: bool,
73    /// Section priority/order (lower = earlier in prompt)
74    pub order: usize,
75}
76
77impl PromptSection {
78    /// Create a new static section
79    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    /// Create a new dynamic section
89    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    /// Create a cached section (after computation)
102    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    /// Set section order
112    pub fn with_order(self, order: usize) -> Self {
113        Self { order, ..self }
114    }
115
116    /// Set cacheability
117    pub fn with_cacheable(self, cacheable: bool) -> Self {
118        Self { cacheable, ..self }
119    }
120
121    /// Compute and render the section content
122    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    /// Compute raw content (without section header)
132    pub fn compute_content(&self) -> String {
133        self.content.compute()
134    }
135
136    /// Get estimated token count (approximate)
137    pub fn estimated_tokens(&self) -> usize {
138        // Rough estimate: ~4 chars per token for Chinese, ~1 token per word for English
139        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
146/// Builder for creating prompt sections
147pub struct SectionBuilder {
148    sections: Vec<PromptSection>,
149}
150
151impl SectionBuilder {
152    pub fn new() -> Self {
153        Self { sections: Vec::new() }
154    }
155
156    /// Add a static section
157    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    /// Add a dynamic section
162    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    /// Add a section
170    pub fn add_section(mut self, section: PromptSection) -> Self {
171        self.sections.push(section);
172        self
173    }
174
175    /// Build sections sorted by order
176    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}