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
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
149/// Builder for creating prompt sections
150pub 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    /// Add a static section
162    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    /// Add a dynamic section
167    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    /// Add a section
175    pub fn add_section(mut self, section: PromptSection) -> Self {
176        self.sections.push(section);
177        self
178    }
179
180    /// Build sections sorted by order
181    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}