1pub type AdapterFn = std::sync::Arc<dyn Fn(&str) -> String + Send + Sync>;
7
8#[derive(Clone)]
10pub struct PromptSection {
11 pub kind: PromptSectionKind,
13 pub content: String,
15 pub name: Option<String>,
17 pub adapter: Option<AdapterFn>,
19}
20
21impl std::fmt::Debug for PromptSection {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 f.debug_struct("PromptSection")
24 .field("kind", &self.kind)
25 .field("content", &self.content)
26 .field("name", &self.name)
27 .field(
28 "adapter",
29 &self.adapter.as_ref().map(|_| "Fn(&str) -> String"),
30 )
31 .finish()
32 }
33}
34
35#[derive(Clone, Debug, PartialEq)]
37pub enum PromptSectionKind {
38 Role,
40 Task,
42 Constraint,
44 Format,
46 Example,
48 Text,
50 Context,
52 Persona,
54 Guidelines,
56 Scaffolded,
58 Versioned,
60 Compressed,
62 Adaptive,
64}
65
66impl PromptSection {
67 pub fn render(&self) -> String {
69 match &self.kind {
70 PromptSectionKind::Role => format!("You are {}.", self.content),
71 PromptSectionKind::Task => format!("Your task: {}", self.content),
72 PromptSectionKind::Constraint => format!("Constraint: {}", self.content),
73 PromptSectionKind::Format => format!("Output format: {}", self.content),
74 PromptSectionKind::Example => self.content.clone(),
75 PromptSectionKind::Text => self.content.clone(),
76 PromptSectionKind::Context => format!("Context: {}", self.content),
77 PromptSectionKind::Persona => format!("Persona: {}", self.content),
78 PromptSectionKind::Guidelines => self.content.clone(),
79 PromptSectionKind::Scaffolded => self.content.clone(),
80 PromptSectionKind::Versioned => self.content.clone(),
81 PromptSectionKind::Compressed => compress_text(&self.content),
82 PromptSectionKind::Adaptive => self.content.clone(),
83 }
84 }
85
86 pub fn render_with_context(&self, ctx: &str) -> String {
91 if let Some(adapter) = &self.adapter {
92 adapter(ctx)
93 } else {
94 self.render()
95 }
96 }
97
98 pub fn with_adapter<F>(mut self, f: F) -> Self
100 where
101 F: Fn(&str) -> String + Send + Sync + 'static,
102 {
103 self.adapter = Some(std::sync::Arc::new(f));
104 self
105 }
106}
107
108impl std::ops::Add for PromptSection {
110 type Output = PromptComposite;
111
112 fn add(self, rhs: PromptSection) -> Self::Output {
113 PromptComposite {
114 sections: vec![self, rhs],
115 }
116 }
117}
118
119#[derive(Clone, Debug)]
121pub struct PromptComposite {
122 pub sections: Vec<PromptSection>,
124}
125
126impl PromptComposite {
127 pub fn render(&self) -> String {
133 let compress = self
134 .sections
135 .iter()
136 .any(|s| s.kind == PromptSectionKind::Compressed);
137 let body = self
138 .sections
139 .iter()
140 .filter(|s| s.kind != PromptSectionKind::Compressed)
141 .map(PromptSection::render)
142 .collect::<Vec<_>>()
143 .join("\n\n");
144 if compress { compress_text(&body) } else { body }
145 }
146}
147
148pub fn compress_text(s: &str) -> String {
152 let mut out: Vec<String> = Vec::new();
153 for line in s.lines() {
154 let collapsed = line.split_whitespace().collect::<Vec<_>>().join(" ");
155 if collapsed.is_empty() {
156 continue;
157 }
158 if out.last().map(std::string::String::as_str) == Some(collapsed.as_str()) {
159 continue;
160 }
161 out.push(collapsed);
162 }
163 out.join("\n")
164}
165
166impl PromptComposite {
167 pub fn only(self, kinds: &[PromptSectionKind]) -> Self {
169 Self {
170 sections: self
171 .sections
172 .into_iter()
173 .filter(|s| kinds.contains(&s.kind))
174 .collect(),
175 }
176 }
177
178 pub fn without(self, kinds: &[PromptSectionKind]) -> Self {
180 Self {
181 sections: self
182 .sections
183 .into_iter()
184 .filter(|s| !kinds.contains(&s.kind))
185 .collect(),
186 }
187 }
188
189 pub fn reorder(mut self, order: &[PromptSectionKind]) -> Self {
191 self.sections.sort_by_key(|s| {
192 order
193 .iter()
194 .position(|k| k == &s.kind)
195 .unwrap_or(usize::MAX)
196 });
197 self
198 }
199
200 pub fn reorder_by_name(self, order: &[&str]) -> Self {
204 let order: Vec<&str> = order.to_vec();
205 let mut ordered = Vec::with_capacity(self.sections.len());
206 let mut remaining = self.sections;
207
208 for name in &order {
209 let mut i = 0;
210 while i < remaining.len() {
211 if remaining[i].name.as_deref() == Some(name) {
212 ordered.push(remaining.remove(i));
213 } else {
214 i += 1;
215 }
216 }
217 }
218 ordered.extend(remaining);
219 Self { sections: ordered }
220 }
221
222 pub fn only_by_name(self, names: &[&str]) -> Self {
224 Self {
225 sections: self
226 .sections
227 .into_iter()
228 .filter(|s| {
229 s.name
230 .as_deref()
231 .map(|n| names.contains(&n))
232 .unwrap_or(false)
233 })
234 .collect(),
235 }
236 }
237
238 pub fn without_by_name(self, names: &[&str]) -> Self {
240 Self {
241 sections: self
242 .sections
243 .into_iter()
244 .filter(|s| {
245 s.name
246 .as_deref()
247 .map(|n| !names.contains(&n))
248 .unwrap_or(true)
249 })
250 .collect(),
251 }
252 }
253
254 pub fn apply(self, transform: PromptTransform) -> Self {
256 match transform {
257 PromptTransform::Reorder(order) => {
258 let refs: Vec<&str> = order.iter().map(std::string::String::as_str).collect();
259 self.reorder_by_name(&refs)
260 }
261 PromptTransform::Only(names) => {
262 let refs: Vec<&str> = names.iter().map(std::string::String::as_str).collect();
263 self.only_by_name(&refs)
264 }
265 PromptTransform::Without(names) => {
266 let refs: Vec<&str> = names.iter().map(std::string::String::as_str).collect();
267 self.without_by_name(&refs)
268 }
269 }
270 }
271}
272
273#[derive(Clone, Debug)]
277pub enum PromptTransform {
278 Reorder(Vec<String>),
280 Only(Vec<String>),
282 Without(Vec<String>),
284}
285
286impl From<PromptComposite> for String {
287 fn from(p: PromptComposite) -> String {
288 p.render()
289 }
290}
291
292impl From<PromptSection> for String {
293 fn from(s: PromptSection) -> String {
294 s.render()
295 }
296}
297
298impl std::ops::Add<PromptSection> for PromptComposite {
299 type Output = PromptComposite;
300
301 fn add(mut self, rhs: PromptSection) -> Self::Output {
302 self.sections.push(rhs);
303 self
304 }
305}
306
307pub struct P;
309
310impl P {
311 pub fn role(role: &str) -> PromptSection {
313 PromptSection {
314 kind: PromptSectionKind::Role,
315 content: role.to_string(),
316 name: Some("role".to_string()),
317 adapter: None,
318 }
319 }
320
321 pub fn task(task: &str) -> PromptSection {
323 PromptSection {
324 kind: PromptSectionKind::Task,
325 content: task.to_string(),
326 name: Some("task".to_string()),
327 adapter: None,
328 }
329 }
330
331 pub fn constraint(c: &str) -> PromptSection {
333 PromptSection {
334 kind: PromptSectionKind::Constraint,
335 content: c.to_string(),
336 name: Some("constraint".to_string()),
337 adapter: None,
338 }
339 }
340
341 pub fn format(f: &str) -> PromptSection {
343 PromptSection {
344 kind: PromptSectionKind::Format,
345 content: f.to_string(),
346 name: Some("format".to_string()),
347 adapter: None,
348 }
349 }
350
351 pub fn example(input: &str, output: &str) -> PromptSection {
353 PromptSection {
354 kind: PromptSectionKind::Example,
355 content: format!("Example:\nInput: {input}\nOutput: {output}"),
356 name: Some("example".to_string()),
357 adapter: None,
358 }
359 }
360
361 pub fn text(t: &str) -> PromptSection {
363 PromptSection {
364 kind: PromptSectionKind::Text,
365 content: t.to_string(),
366 name: None,
367 adapter: None,
368 }
369 }
370
371 pub fn context(ctx: &str) -> PromptSection {
373 PromptSection {
374 kind: PromptSectionKind::Context,
375 content: ctx.to_string(),
376 name: Some("context".to_string()),
377 adapter: None,
378 }
379 }
380
381 pub fn persona(desc: &str) -> PromptSection {
383 PromptSection {
384 kind: PromptSectionKind::Persona,
385 content: desc.to_string(),
386 name: Some("persona".to_string()),
387 adapter: None,
388 }
389 }
390
391 pub fn guidelines(items: &[&str]) -> PromptSection {
393 let content = items
394 .iter()
395 .map(|item| format!("- {item}"))
396 .collect::<Vec<_>>()
397 .join("\n");
398 PromptSection {
399 kind: PromptSectionKind::Guidelines,
400 content: format!("Guidelines:\n{content}"),
401 name: Some("guidelines".to_string()),
402 adapter: None,
403 }
404 }
405
406 pub fn section(name: &str, text: &str) -> PromptSection {
408 PromptSection {
409 kind: PromptSectionKind::Text,
410 content: format!("## {name}\n{text}"),
411 name: Some(name.to_string()),
412 adapter: None,
413 }
414 }
415
416 pub fn template(tpl: &str) -> PromptSection {
418 PromptSection {
419 kind: PromptSectionKind::Text,
420 content: tpl.to_string(),
421 name: Some("template".to_string()),
422 adapter: None,
423 }
424 }
425
426 pub fn reorder(order: &[&str]) -> PromptTransform {
438 let order: Vec<String> = order.iter().map(std::string::ToString::to_string).collect();
439 PromptTransform::Reorder(order)
440 }
441
442 pub fn only(names: &[&str]) -> PromptTransform {
451 let names: Vec<String> = names.iter().map(std::string::ToString::to_string).collect();
452 PromptTransform::Only(names)
453 }
454
455 pub fn without(names: &[&str]) -> PromptTransform {
464 let names: Vec<String> = names.iter().map(std::string::ToString::to_string).collect();
465 PromptTransform::Without(names)
466 }
467
468 pub fn compress() -> PromptSection {
473 PromptSection {
474 kind: PromptSectionKind::Compressed,
475 content: String::new(),
476 name: Some("compress".to_string()),
477 adapter: None,
478 }
479 }
480
481 pub fn adapt<F>(f: F) -> PromptSection
497 where
498 F: Fn(&str) -> String + Send + Sync + 'static,
499 {
500 PromptSection {
501 kind: PromptSectionKind::Adaptive,
502 content: String::new(),
503 name: Some("adapt".to_string()),
504 adapter: None,
505 }
506 .with_adapter(f)
507 }
508
509 pub fn scaffolded(steps: &[&str]) -> PromptSection {
517 let content = steps
518 .iter()
519 .enumerate()
520 .map(|(i, step)| format!("Step {}: {step}", i + 1))
521 .collect::<Vec<_>>()
522 .join("\n");
523 PromptSection {
524 kind: PromptSectionKind::Scaffolded,
525 content: format!("Follow these steps:\n{content}"),
526 name: Some("scaffolded".to_string()),
527 adapter: None,
528 }
529 }
530
531 pub fn versioned(version: &str, text: &str) -> PromptSection {
539 PromptSection {
540 kind: PromptSectionKind::Versioned,
541 content: format!("[{version}] {text}"),
542 name: Some(format!("versioned:{version}")),
543 adapter: None,
544 }
545 }
546
547 pub fn show_state(keys: &[&str]) -> gemini_adk_rs::live::InstructionModifier {
558 gemini_adk_rs::live::InstructionModifier::StateAppend(
559 keys.iter().map(std::string::ToString::to_string).collect(),
560 )
561 }
562
563 pub fn when(
572 predicate: impl Fn(&gemini_adk_rs::State) -> bool + Send + Sync + 'static,
573 text: impl Into<String>,
574 ) -> gemini_adk_rs::live::InstructionModifier {
575 gemini_adk_rs::live::InstructionModifier::Conditional {
576 predicate: std::sync::Arc::new(predicate),
577 text: text.into(),
578 }
579 }
580
581 pub fn context_fn(
589 f: impl Fn(&gemini_adk_rs::State) -> String + Send + Sync + 'static,
590 ) -> gemini_adk_rs::live::InstructionModifier {
591 gemini_adk_rs::live::InstructionModifier::CustomAppend(std::sync::Arc::new(f))
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn role_renders() {
601 let s = P::role("analyst");
602 assert_eq!(s.render(), "You are analyst.");
603 }
604
605 #[test]
606 fn task_renders() {
607 let s = P::task("analyze data");
608 assert_eq!(s.render(), "Your task: analyze data");
609 }
610
611 #[test]
612 fn constraint_renders() {
613 let s = P::constraint("be concise");
614 assert_eq!(s.render(), "Constraint: be concise");
615 }
616
617 #[test]
618 fn format_renders() {
619 let s = P::format("JSON");
620 assert_eq!(s.render(), "Output format: JSON");
621 }
622
623 #[test]
624 fn example_renders() {
625 let s = P::example("hello", "world");
626 assert!(s.render().contains("Input: hello"));
627 assert!(s.render().contains("Output: world"));
628 }
629
630 #[test]
631 fn compose_with_add() {
632 let prompt = P::role("analyst") + P::task("analyze data") + P::format("JSON");
633 assert_eq!(prompt.sections.len(), 3);
634 }
635
636 #[test]
637 fn composite_renders_all() {
638 let prompt = P::role("analyst") + P::task("analyze data");
639 let rendered = prompt.render();
640 assert!(rendered.contains("You are analyst."));
641 assert!(rendered.contains("Your task: analyze data"));
642 }
643
644 #[test]
645 fn context_renders() {
646 let s = P::context("user is a developer");
647 assert_eq!(s.render(), "Context: user is a developer");
648 assert_eq!(s.kind, PromptSectionKind::Context);
649 }
650
651 #[test]
652 fn persona_renders() {
653 let s = P::persona("friendly and concise");
654 assert_eq!(s.render(), "Persona: friendly and concise");
655 assert_eq!(s.kind, PromptSectionKind::Persona);
656 }
657
658 #[test]
659 fn guidelines_renders() {
660 let s = P::guidelines(&["be concise", "use examples", "cite sources"]);
661 assert!(s.render().contains("Guidelines:"));
662 assert!(s.render().contains("- be concise"));
663 assert!(s.render().contains("- use examples"));
664 assert!(s.render().contains("- cite sources"));
665 assert_eq!(s.kind, PromptSectionKind::Guidelines);
666 }
667
668 #[test]
669 fn section_kinds() {
670 assert_eq!(P::role("x").kind, PromptSectionKind::Role);
671 assert_eq!(P::task("x").kind, PromptSectionKind::Task);
672 assert_eq!(P::text("x").kind, PromptSectionKind::Text);
673 }
674
675 #[test]
676 fn section_into_string() {
677 let s: String = P::role("analyst").into();
678 assert_eq!(s, "You are analyst.");
679 }
680
681 #[test]
682 fn composite_into_string() {
683 let s: String = (P::role("analyst") + P::task("analyze data")).into();
684 assert!(s.contains("You are analyst."));
685 assert!(s.contains("Your task: analyze data"));
686 }
687
688 #[test]
689 fn reorder_by_name() {
690 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
691 let reordered = prompt.reorder_by_name(&["format", "task", "role"]);
692 assert_eq!(reordered.sections[0].name.as_deref(), Some("format"));
693 assert_eq!(reordered.sections[1].name.as_deref(), Some("task"));
694 assert_eq!(reordered.sections[2].name.as_deref(), Some("role"));
695 }
696
697 #[test]
698 fn only_by_name() {
699 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
700 let filtered = prompt.only_by_name(&["role", "task"]);
701 assert_eq!(filtered.sections.len(), 2);
702 assert_eq!(filtered.sections[0].name.as_deref(), Some("role"));
703 assert_eq!(filtered.sections[1].name.as_deref(), Some("task"));
704 }
705
706 #[test]
707 fn without_by_name() {
708 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
709 let filtered = prompt.without_by_name(&["format"]);
710 assert_eq!(filtered.sections.len(), 2);
711 assert!(
712 filtered
713 .sections
714 .iter()
715 .all(|s| s.name.as_deref() != Some("format"))
716 );
717 }
718
719 #[test]
720 fn reorder_transform_via_apply() {
721 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
722 let transform = P::reorder(&["format", "role"]);
723 let reordered = prompt.apply(transform);
724 assert_eq!(reordered.sections[0].name.as_deref(), Some("format"));
725 assert_eq!(reordered.sections[1].name.as_deref(), Some("role"));
726 }
727
728 #[test]
729 fn only_transform_via_apply() {
730 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
731 let transform = P::only(&["task"]);
732 let filtered = prompt.apply(transform);
733 assert_eq!(filtered.sections.len(), 1);
734 assert_eq!(filtered.sections[0].name.as_deref(), Some("task"));
735 }
736
737 #[test]
738 fn without_transform_via_apply() {
739 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
740 let transform = P::without(&["role", "format"]);
741 let filtered = prompt.apply(transform);
742 assert_eq!(filtered.sections.len(), 1);
743 assert_eq!(filtered.sections[0].name.as_deref(), Some("task"));
744 }
745
746 #[test]
747 fn compress_renders() {
748 let s = P::compress();
749 assert_eq!(s.kind, PromptSectionKind::Compressed);
750 assert_eq!(s.render(), "");
752 }
753
754 #[test]
755 fn compress_marker_shrinks_composite() {
756 let verbose = P::text("You are helpful.")
758 + P::text("You are helpful.")
759 + P::text("Be concise.")
760 + P::compress();
761 let out = verbose.render();
762 assert_eq!(out, "You are helpful.\nBe concise.");
764
765 let plain = (P::text("a b") + P::text("c")).render();
767 assert_eq!(plain, "a b\n\nc");
768 }
769
770 #[test]
771 fn adapt_renders_with_context() {
772 let s = P::adapt(|ctx| {
773 if ctx.contains("detailed") {
774 "Be thorough.".to_string()
775 } else {
776 "Be concise.".to_string()
777 }
778 });
779 assert_eq!(s.kind, PromptSectionKind::Adaptive);
780 assert_eq!(s.render_with_context("detailed"), "Be thorough.");
781 assert_eq!(s.render_with_context("brief"), "Be concise.");
782 }
783
784 #[test]
785 fn adapt_fallback_render() {
786 let s = P::adapt(|_| "adapted".to_string());
787 assert_eq!(s.render(), "");
789 }
790
791 #[test]
792 fn scaffolded_renders() {
793 let s = P::scaffolded(&["Identify", "Analyze", "Conclude"]);
794 assert_eq!(s.kind, PromptSectionKind::Scaffolded);
795 let rendered = s.render();
796 assert!(rendered.contains("Follow these steps:"));
797 assert!(rendered.contains("Step 1: Identify"));
798 assert!(rendered.contains("Step 2: Analyze"));
799 assert!(rendered.contains("Step 3: Conclude"));
800 }
801
802 #[test]
803 fn versioned_renders() {
804 let s = P::versioned("v2.1", "Use the new methodology");
805 assert_eq!(s.kind, PromptSectionKind::Versioned);
806 assert_eq!(s.render(), "[v2.1] Use the new methodology");
807 assert_eq!(s.name.as_deref(), Some("versioned:v2.1"));
808 }
809
810 #[test]
811 fn sections_have_names() {
812 assert_eq!(P::role("x").name.as_deref(), Some("role"));
813 assert_eq!(P::task("x").name.as_deref(), Some("task"));
814 assert_eq!(P::constraint("x").name.as_deref(), Some("constraint"));
815 assert_eq!(P::format("x").name.as_deref(), Some("format"));
816 assert_eq!(P::example("x", "y").name.as_deref(), Some("example"));
817 assert_eq!(P::text("x").name, None);
818 assert_eq!(P::context("x").name.as_deref(), Some("context"));
819 assert_eq!(P::persona("x").name.as_deref(), Some("persona"));
820 assert_eq!(P::guidelines(&["x"]).name.as_deref(), Some("guidelines"));
821 assert_eq!(P::section("foo", "bar").name.as_deref(), Some("foo"));
822 assert_eq!(P::scaffolded(&["x"]).name.as_deref(), Some("scaffolded"));
823 assert_eq!(P::compress().name.as_deref(), Some("compress"));
824 }
825}