1use crate::{CacheControl, ContentBlock, Message};
20
21#[derive(Debug, Clone)]
23struct PromptLayer {
24 text: String,
25 stable: bool,
27}
28
29#[derive(Debug, Clone)]
50pub struct Prompt {
51 layers: Vec<PromptLayer>,
52}
53
54impl Prompt {
55 pub fn new() -> Self {
57 Self { layers: vec![] }
58 }
59
60 pub fn stable(mut self, text: impl Into<String>) -> Self {
65 self.layers.push(PromptLayer {
66 text: text.into(),
67 stable: true,
68 });
69 self
70 }
71
72 pub fn dynamic(mut self, text: impl Into<String>) -> Self {
76 self.layers.push(PromptLayer {
77 text: text.into(),
78 stable: false,
79 });
80 self
81 }
82
83 pub fn build(self) -> Message {
88 let last_stable_idx = self
89 .layers
90 .iter()
91 .enumerate()
92 .rev()
93 .find(|(_, layer)| layer.stable)
94 .map(|(idx, _)| idx);
95
96 let content: Vec<ContentBlock> = self
97 .layers
98 .iter()
99 .enumerate()
100 .map(|(idx, layer)| {
101 if Some(idx) == last_stable_idx {
102 ContentBlock::text_with_cache(layer.text.clone(), CacheControl::Breakpoint)
103 } else {
104 ContentBlock::text(&layer.text)
105 }
106 })
107 .collect();
108
109 Message::System { content }
110 }
111
112 pub fn is_empty(&self) -> bool {
114 self.layers.iter().all(|l| l.text.is_empty())
115 }
116}
117
118impl Default for Prompt {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl From<String> for Prompt {
125 fn from(s: String) -> Self {
126 Self::new().dynamic(s)
127 }
128}
129
130impl From<&str> for Prompt {
131 fn from(s: &str) -> Self {
132 Self::new().dynamic(s)
133 }
134}
135
136impl From<&String> for Prompt {
137 fn from(s: &String) -> Self {
138 Self::new().dynamic(s.clone())
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_prompt_from_string() {
148 let prompt: Prompt = "hello".into();
149 let msg = prompt.build();
150 assert_eq!(msg.content().len(), 1);
151 assert_eq!(msg.content()[0].as_text(), Some("hello"));
152 if let ContentBlock::Text(t) = &msg.content()[0] {
153 assert!(t.cache_control.is_none());
154 }
155 }
156
157 #[test]
158 fn test_prompt_from_str() {
159 let s = "world";
160 let prompt: Prompt = s.into();
161 let msg = prompt.build();
162 assert_eq!(msg.content()[0].as_text(), Some("world"));
163 }
164
165 #[test]
166 fn test_prompt_new_and_build() {
167 let msg = Prompt::new()
168 .stable("layer1")
169 .stable("layer2")
170 .dynamic("dynamic")
171 .build();
172
173 let blocks = msg.content();
174 assert_eq!(blocks.len(), 3);
175
176 if let ContentBlock::Text(t) = &blocks[0] {
178 assert_eq!(t.text, "layer1");
179 assert!(
180 t.cache_control.is_none(),
181 "Intermediate stable layer should NOT have breakpoint"
182 );
183 } else {
184 panic!("expected Text block");
185 }
186
187 if let ContentBlock::Text(t) = &blocks[1] {
189 assert_eq!(t.text, "layer2");
190 assert!(
191 t.cache_control.is_some(),
192 "Last stable layer should have breakpoint"
193 );
194 } else {
195 panic!("expected Text block");
196 }
197
198 if let ContentBlock::Text(t) = &blocks[2] {
200 assert_eq!(t.text, "dynamic");
201 assert!(t.cache_control.is_none());
202 } else {
203 panic!("expected Text block");
204 }
205 }
206
207 #[test]
208 fn test_prompt_is_empty() {
209 let empty = Prompt::default();
210 assert!(empty.is_empty());
211
212 let nonempty: Prompt = "x".into();
213 assert!(!nonempty.is_empty());
214 }
215
216 #[test]
217 fn test_breakpoint_only_on_last_stable() {
218 let msg = Prompt::new()
219 .stable("L1")
220 .stable("L2")
221 .stable("L3")
222 .stable("L4")
223 .stable("L5")
224 .dynamic("D")
225 .build();
226
227 let blocks = msg.content();
228 assert_eq!(blocks.len(), 6);
229
230 let breakpoint_count = blocks
231 .iter()
232 .filter(|b| {
233 if let ContentBlock::Text(t) = b {
234 t.cache_control.is_some()
235 } else {
236 false
237 }
238 })
239 .count();
240 assert_eq!(
241 breakpoint_count, 1,
242 "Should have exactly 1 breakpoint (on last stable layer)"
243 );
244
245 if let ContentBlock::Text(t) = &blocks[4] {
247 assert!(t.cache_control.is_some());
248 }
249 }
250
251 #[test]
252 fn test_all_stable_single_breakpoint() {
253 let msg = Prompt::new().stable("A").stable("B").build();
254
255 let blocks = msg.content();
256 if let ContentBlock::Text(t) = &blocks[0] {
257 assert!(t.cache_control.is_none(), "A should not have breakpoint");
258 }
259 if let ContentBlock::Text(t) = &blocks[1] {
260 assert!(t.cache_control.is_some(), "B should have breakpoint");
261 }
262 }
263
264 #[test]
265 fn test_empty_prompt_produces_empty_message() {
266 let msg = Prompt::new().build();
267 assert!(msg.content().is_empty());
268 }
269
270 #[test]
271 fn test_flatten_text_ignores_cache_control() {
272 let blocks = vec![
273 ContentBlock::text_with_cache("cached part".into(), CacheControl::Breakpoint),
274 ContentBlock::text("dynamic part"),
275 ];
276 assert_eq!(
277 ContentBlock::flatten_text(&blocks),
278 "cached part\n\ndynamic part"
279 );
280 }
281
282 #[test]
283 fn test_flatten_text_single_block() {
284 let blocks = vec![ContentBlock::text("hello")];
285 assert_eq!(ContentBlock::flatten_text(&blocks), "hello");
286 }
287
288 #[test]
289 fn test_flatten_text_empty() {
290 let blocks: Vec<ContentBlock> = vec![];
291 assert_eq!(ContentBlock::flatten_text(&blocks), "");
292 }
293}