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(|s| s.render())
142 .collect::<Vec<_>>()
143 .join("\n\n");
144 if compress {
145 compress_text(&body)
146 } else {
147 body
148 }
149 }
150}
151
152pub fn compress_text(s: &str) -> String {
156 let mut out: Vec<String> = Vec::new();
157 for line in s.lines() {
158 let collapsed = line.split_whitespace().collect::<Vec<_>>().join(" ");
159 if collapsed.is_empty() {
160 continue;
161 }
162 if out.last().map(|l| l.as_str()) == Some(collapsed.as_str()) {
163 continue;
164 }
165 out.push(collapsed);
166 }
167 out.join("\n")
168}
169
170impl PromptComposite {
171 pub fn only(self, kinds: &[PromptSectionKind]) -> Self {
173 Self {
174 sections: self
175 .sections
176 .into_iter()
177 .filter(|s| kinds.contains(&s.kind))
178 .collect(),
179 }
180 }
181
182 pub fn without(self, kinds: &[PromptSectionKind]) -> Self {
184 Self {
185 sections: self
186 .sections
187 .into_iter()
188 .filter(|s| !kinds.contains(&s.kind))
189 .collect(),
190 }
191 }
192
193 pub fn reorder(mut self, order: &[PromptSectionKind]) -> Self {
195 self.sections.sort_by_key(|s| {
196 order
197 .iter()
198 .position(|k| k == &s.kind)
199 .unwrap_or(usize::MAX)
200 });
201 self
202 }
203
204 pub fn reorder_by_name(self, order: &[&str]) -> Self {
208 let order: Vec<&str> = order.to_vec();
209 let mut ordered = Vec::with_capacity(self.sections.len());
210 let mut remaining = self.sections;
211
212 for name in &order {
213 let mut i = 0;
214 while i < remaining.len() {
215 if remaining[i].name.as_deref() == Some(name) {
216 ordered.push(remaining.remove(i));
217 } else {
218 i += 1;
219 }
220 }
221 }
222 ordered.extend(remaining);
223 Self { sections: ordered }
224 }
225
226 pub fn only_by_name(self, names: &[&str]) -> Self {
228 Self {
229 sections: self
230 .sections
231 .into_iter()
232 .filter(|s| {
233 s.name
234 .as_deref()
235 .map(|n| names.contains(&n))
236 .unwrap_or(false)
237 })
238 .collect(),
239 }
240 }
241
242 pub fn without_by_name(self, names: &[&str]) -> Self {
244 Self {
245 sections: self
246 .sections
247 .into_iter()
248 .filter(|s| {
249 s.name
250 .as_deref()
251 .map(|n| !names.contains(&n))
252 .unwrap_or(true)
253 })
254 .collect(),
255 }
256 }
257
258 pub fn apply(self, transform: PromptTransform) -> Self {
260 match transform {
261 PromptTransform::Reorder(order) => {
262 let refs: Vec<&str> = order.iter().map(|s| s.as_str()).collect();
263 self.reorder_by_name(&refs)
264 }
265 PromptTransform::Only(names) => {
266 let refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
267 self.only_by_name(&refs)
268 }
269 PromptTransform::Without(names) => {
270 let refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
271 self.without_by_name(&refs)
272 }
273 }
274 }
275}
276
277#[derive(Clone, Debug)]
281pub enum PromptTransform {
282 Reorder(Vec<String>),
284 Only(Vec<String>),
286 Without(Vec<String>),
288}
289
290impl From<PromptComposite> for String {
291 fn from(p: PromptComposite) -> String {
292 p.render()
293 }
294}
295
296impl From<PromptSection> for String {
297 fn from(s: PromptSection) -> String {
298 s.render()
299 }
300}
301
302impl std::ops::Add<PromptSection> for PromptComposite {
303 type Output = PromptComposite;
304
305 fn add(mut self, rhs: PromptSection) -> Self::Output {
306 self.sections.push(rhs);
307 self
308 }
309}
310
311pub struct P;
313
314impl P {
315 pub fn role(role: &str) -> PromptSection {
317 PromptSection {
318 kind: PromptSectionKind::Role,
319 content: role.to_string(),
320 name: Some("role".to_string()),
321 adapter: None,
322 }
323 }
324
325 pub fn task(task: &str) -> PromptSection {
327 PromptSection {
328 kind: PromptSectionKind::Task,
329 content: task.to_string(),
330 name: Some("task".to_string()),
331 adapter: None,
332 }
333 }
334
335 pub fn constraint(c: &str) -> PromptSection {
337 PromptSection {
338 kind: PromptSectionKind::Constraint,
339 content: c.to_string(),
340 name: Some("constraint".to_string()),
341 adapter: None,
342 }
343 }
344
345 pub fn format(f: &str) -> PromptSection {
347 PromptSection {
348 kind: PromptSectionKind::Format,
349 content: f.to_string(),
350 name: Some("format".to_string()),
351 adapter: None,
352 }
353 }
354
355 pub fn example(input: &str, output: &str) -> PromptSection {
357 PromptSection {
358 kind: PromptSectionKind::Example,
359 content: format!("Example:\nInput: {input}\nOutput: {output}"),
360 name: Some("example".to_string()),
361 adapter: None,
362 }
363 }
364
365 pub fn text(t: &str) -> PromptSection {
367 PromptSection {
368 kind: PromptSectionKind::Text,
369 content: t.to_string(),
370 name: None,
371 adapter: None,
372 }
373 }
374
375 pub fn context(ctx: &str) -> PromptSection {
377 PromptSection {
378 kind: PromptSectionKind::Context,
379 content: ctx.to_string(),
380 name: Some("context".to_string()),
381 adapter: None,
382 }
383 }
384
385 pub fn persona(desc: &str) -> PromptSection {
387 PromptSection {
388 kind: PromptSectionKind::Persona,
389 content: desc.to_string(),
390 name: Some("persona".to_string()),
391 adapter: None,
392 }
393 }
394
395 pub fn guidelines(items: &[&str]) -> PromptSection {
397 let content = items
398 .iter()
399 .map(|item| format!("- {item}"))
400 .collect::<Vec<_>>()
401 .join("\n");
402 PromptSection {
403 kind: PromptSectionKind::Guidelines,
404 content: format!("Guidelines:\n{content}"),
405 name: Some("guidelines".to_string()),
406 adapter: None,
407 }
408 }
409
410 pub fn section(name: &str, text: &str) -> PromptSection {
412 PromptSection {
413 kind: PromptSectionKind::Text,
414 content: format!("## {}\n{}", name, text),
415 name: Some(name.to_string()),
416 adapter: None,
417 }
418 }
419
420 pub fn template(tpl: &str) -> PromptSection {
422 PromptSection {
423 kind: PromptSectionKind::Text,
424 content: tpl.to_string(),
425 name: Some("template".to_string()),
426 adapter: None,
427 }
428 }
429
430 pub fn reorder(order: &[&str]) -> PromptTransform {
440 let order: Vec<String> = order.iter().map(|s| s.to_string()).collect();
441 PromptTransform::Reorder(order)
442 }
443
444 pub fn only(names: &[&str]) -> PromptTransform {
451 let names: Vec<String> = names.iter().map(|s| s.to_string()).collect();
452 PromptTransform::Only(names)
453 }
454
455 pub fn without(names: &[&str]) -> PromptTransform {
462 let names: Vec<String> = names.iter().map(|s| s.to_string()).collect();
463 PromptTransform::Without(names)
464 }
465
466 pub fn compress() -> PromptSection {
471 PromptSection {
472 kind: PromptSectionKind::Compressed,
473 content: String::new(),
474 name: Some("compress".to_string()),
475 adapter: None,
476 }
477 }
478
479 pub fn adapt<F>(f: F) -> PromptSection
494 where
495 F: Fn(&str) -> String + Send + Sync + 'static,
496 {
497 PromptSection {
498 kind: PromptSectionKind::Adaptive,
499 content: String::new(),
500 name: Some("adapt".to_string()),
501 adapter: None,
502 }
503 .with_adapter(f)
504 }
505
506 pub fn scaffolded(steps: &[&str]) -> PromptSection {
512 let content = steps
513 .iter()
514 .enumerate()
515 .map(|(i, step)| format!("Step {}: {step}", i + 1))
516 .collect::<Vec<_>>()
517 .join("\n");
518 PromptSection {
519 kind: PromptSectionKind::Scaffolded,
520 content: format!("Follow these steps:\n{content}"),
521 name: Some("scaffolded".to_string()),
522 adapter: None,
523 }
524 }
525
526 pub fn versioned(version: &str, text: &str) -> PromptSection {
532 PromptSection {
533 kind: PromptSectionKind::Versioned,
534 content: format!("[{version}] {text}"),
535 name: Some(format!("versioned:{version}")),
536 adapter: None,
537 }
538 }
539
540 pub fn with_state(keys: &[&str]) -> gemini_adk_rs::live::InstructionModifier {
549 gemini_adk_rs::live::InstructionModifier::StateAppend(
550 keys.iter().map(|k| k.to_string()).collect(),
551 )
552 }
553
554 pub fn when(
560 predicate: impl Fn(&gemini_adk_rs::State) -> bool + Send + Sync + 'static,
561 text: impl Into<String>,
562 ) -> gemini_adk_rs::live::InstructionModifier {
563 gemini_adk_rs::live::InstructionModifier::Conditional {
564 predicate: std::sync::Arc::new(predicate),
565 text: text.into(),
566 }
567 }
568
569 pub fn context_fn(
575 f: impl Fn(&gemini_adk_rs::State) -> String + Send + Sync + 'static,
576 ) -> gemini_adk_rs::live::InstructionModifier {
577 gemini_adk_rs::live::InstructionModifier::CustomAppend(std::sync::Arc::new(f))
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn role_renders() {
587 let s = P::role("analyst");
588 assert_eq!(s.render(), "You are analyst.");
589 }
590
591 #[test]
592 fn task_renders() {
593 let s = P::task("analyze data");
594 assert_eq!(s.render(), "Your task: analyze data");
595 }
596
597 #[test]
598 fn constraint_renders() {
599 let s = P::constraint("be concise");
600 assert_eq!(s.render(), "Constraint: be concise");
601 }
602
603 #[test]
604 fn format_renders() {
605 let s = P::format("JSON");
606 assert_eq!(s.render(), "Output format: JSON");
607 }
608
609 #[test]
610 fn example_renders() {
611 let s = P::example("hello", "world");
612 assert!(s.render().contains("Input: hello"));
613 assert!(s.render().contains("Output: world"));
614 }
615
616 #[test]
617 fn compose_with_add() {
618 let prompt = P::role("analyst") + P::task("analyze data") + P::format("JSON");
619 assert_eq!(prompt.sections.len(), 3);
620 }
621
622 #[test]
623 fn composite_renders_all() {
624 let prompt = P::role("analyst") + P::task("analyze data");
625 let rendered = prompt.render();
626 assert!(rendered.contains("You are analyst."));
627 assert!(rendered.contains("Your task: analyze data"));
628 }
629
630 #[test]
631 fn context_renders() {
632 let s = P::context("user is a developer");
633 assert_eq!(s.render(), "Context: user is a developer");
634 assert_eq!(s.kind, PromptSectionKind::Context);
635 }
636
637 #[test]
638 fn persona_renders() {
639 let s = P::persona("friendly and concise");
640 assert_eq!(s.render(), "Persona: friendly and concise");
641 assert_eq!(s.kind, PromptSectionKind::Persona);
642 }
643
644 #[test]
645 fn guidelines_renders() {
646 let s = P::guidelines(&["be concise", "use examples", "cite sources"]);
647 assert!(s.render().contains("Guidelines:"));
648 assert!(s.render().contains("- be concise"));
649 assert!(s.render().contains("- use examples"));
650 assert!(s.render().contains("- cite sources"));
651 assert_eq!(s.kind, PromptSectionKind::Guidelines);
652 }
653
654 #[test]
655 fn section_kinds() {
656 assert_eq!(P::role("x").kind, PromptSectionKind::Role);
657 assert_eq!(P::task("x").kind, PromptSectionKind::Task);
658 assert_eq!(P::text("x").kind, PromptSectionKind::Text);
659 }
660
661 #[test]
662 fn section_into_string() {
663 let s: String = P::role("analyst").into();
664 assert_eq!(s, "You are analyst.");
665 }
666
667 #[test]
668 fn composite_into_string() {
669 let s: String = (P::role("analyst") + P::task("analyze data")).into();
670 assert!(s.contains("You are analyst."));
671 assert!(s.contains("Your task: analyze data"));
672 }
673
674 #[test]
675 fn reorder_by_name() {
676 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
677 let reordered = prompt.reorder_by_name(&["format", "task", "role"]);
678 assert_eq!(reordered.sections[0].name.as_deref(), Some("format"));
679 assert_eq!(reordered.sections[1].name.as_deref(), Some("task"));
680 assert_eq!(reordered.sections[2].name.as_deref(), Some("role"));
681 }
682
683 #[test]
684 fn only_by_name() {
685 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
686 let filtered = prompt.only_by_name(&["role", "task"]);
687 assert_eq!(filtered.sections.len(), 2);
688 assert_eq!(filtered.sections[0].name.as_deref(), Some("role"));
689 assert_eq!(filtered.sections[1].name.as_deref(), Some("task"));
690 }
691
692 #[test]
693 fn without_by_name() {
694 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
695 let filtered = prompt.without_by_name(&["format"]);
696 assert_eq!(filtered.sections.len(), 2);
697 assert!(filtered
698 .sections
699 .iter()
700 .all(|s| s.name.as_deref() != Some("format")));
701 }
702
703 #[test]
704 fn reorder_transform_via_apply() {
705 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
706 let transform = P::reorder(&["format", "role"]);
707 let reordered = prompt.apply(transform);
708 assert_eq!(reordered.sections[0].name.as_deref(), Some("format"));
709 assert_eq!(reordered.sections[1].name.as_deref(), Some("role"));
710 }
711
712 #[test]
713 fn only_transform_via_apply() {
714 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
715 let transform = P::only(&["task"]);
716 let filtered = prompt.apply(transform);
717 assert_eq!(filtered.sections.len(), 1);
718 assert_eq!(filtered.sections[0].name.as_deref(), Some("task"));
719 }
720
721 #[test]
722 fn without_transform_via_apply() {
723 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
724 let transform = P::without(&["role", "format"]);
725 let filtered = prompt.apply(transform);
726 assert_eq!(filtered.sections.len(), 1);
727 assert_eq!(filtered.sections[0].name.as_deref(), Some("task"));
728 }
729
730 #[test]
731 fn compress_renders() {
732 let s = P::compress();
733 assert_eq!(s.kind, PromptSectionKind::Compressed);
734 assert_eq!(s.render(), "");
736 }
737
738 #[test]
739 fn compress_marker_shrinks_composite() {
740 let verbose = P::text("You are helpful.")
742 + P::text("You are helpful.")
743 + P::text("Be concise.")
744 + P::compress();
745 let out = verbose.render();
746 assert_eq!(out, "You are helpful.\nBe concise.");
748
749 let plain = (P::text("a b") + P::text("c")).render();
751 assert_eq!(plain, "a b\n\nc");
752 }
753
754 #[test]
755 fn adapt_renders_with_context() {
756 let s = P::adapt(|ctx| {
757 if ctx.contains("detailed") {
758 "Be thorough.".to_string()
759 } else {
760 "Be concise.".to_string()
761 }
762 });
763 assert_eq!(s.kind, PromptSectionKind::Adaptive);
764 assert_eq!(s.render_with_context("detailed"), "Be thorough.");
765 assert_eq!(s.render_with_context("brief"), "Be concise.");
766 }
767
768 #[test]
769 fn adapt_fallback_render() {
770 let s = P::adapt(|_| "adapted".to_string());
771 assert_eq!(s.render(), "");
773 }
774
775 #[test]
776 fn scaffolded_renders() {
777 let s = P::scaffolded(&["Identify", "Analyze", "Conclude"]);
778 assert_eq!(s.kind, PromptSectionKind::Scaffolded);
779 let rendered = s.render();
780 assert!(rendered.contains("Follow these steps:"));
781 assert!(rendered.contains("Step 1: Identify"));
782 assert!(rendered.contains("Step 2: Analyze"));
783 assert!(rendered.contains("Step 3: Conclude"));
784 }
785
786 #[test]
787 fn versioned_renders() {
788 let s = P::versioned("v2.1", "Use the new methodology");
789 assert_eq!(s.kind, PromptSectionKind::Versioned);
790 assert_eq!(s.render(), "[v2.1] Use the new methodology");
791 assert_eq!(s.name.as_deref(), Some("versioned:v2.1"));
792 }
793
794 #[test]
795 fn sections_have_names() {
796 assert_eq!(P::role("x").name.as_deref(), Some("role"));
797 assert_eq!(P::task("x").name.as_deref(), Some("task"));
798 assert_eq!(P::constraint("x").name.as_deref(), Some("constraint"));
799 assert_eq!(P::format("x").name.as_deref(), Some("format"));
800 assert_eq!(P::example("x", "y").name.as_deref(), Some("example"));
801 assert_eq!(P::text("x").name, None);
802 assert_eq!(P::context("x").name.as_deref(), Some("context"));
803 assert_eq!(P::persona("x").name.as_deref(), Some("persona"));
804 assert_eq!(P::guidelines(&["x"]).name.as_deref(), Some("guidelines"));
805 assert_eq!(P::section("foo", "bar").name.as_deref(), Some("foo"));
806 assert_eq!(P::scaffolded(&["x"]).name.as_deref(), Some("scaffolded"));
807 assert_eq!(P::compress().name.as_deref(), Some("compress"));
808 }
809}