1#[derive(Clone)]
7pub struct PromptSection {
8 pub kind: PromptSectionKind,
10 pub content: String,
12 pub name: Option<String>,
14 pub adapter: Option<std::sync::Arc<dyn Fn(&str) -> String + Send + Sync>>,
16}
17
18impl std::fmt::Debug for PromptSection {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 f.debug_struct("PromptSection")
21 .field("kind", &self.kind)
22 .field("content", &self.content)
23 .field("name", &self.name)
24 .field(
25 "adapter",
26 &self.adapter.as_ref().map(|_| "Fn(&str) -> String"),
27 )
28 .finish()
29 }
30}
31
32#[derive(Clone, Debug, PartialEq)]
34pub enum PromptSectionKind {
35 Role,
37 Task,
39 Constraint,
41 Format,
43 Example,
45 Text,
47 Context,
49 Persona,
51 Guidelines,
53 Scaffolded,
55 Versioned,
57 Compressed,
59 Adaptive,
61}
62
63impl PromptSection {
64 pub fn render(&self) -> String {
66 match &self.kind {
67 PromptSectionKind::Role => format!("You are {}.", self.content),
68 PromptSectionKind::Task => format!("Your task: {}", self.content),
69 PromptSectionKind::Constraint => format!("Constraint: {}", self.content),
70 PromptSectionKind::Format => format!("Output format: {}", self.content),
71 PromptSectionKind::Example => self.content.clone(),
72 PromptSectionKind::Text => self.content.clone(),
73 PromptSectionKind::Context => format!("Context: {}", self.content),
74 PromptSectionKind::Persona => format!("Persona: {}", self.content),
75 PromptSectionKind::Guidelines => self.content.clone(),
76 PromptSectionKind::Scaffolded => self.content.clone(),
77 PromptSectionKind::Versioned => self.content.clone(),
78 PromptSectionKind::Compressed => format!("[compressed] {}", self.content),
79 PromptSectionKind::Adaptive => self.content.clone(),
80 }
81 }
82
83 pub fn render_with_context(&self, ctx: &str) -> String {
88 if let Some(adapter) = &self.adapter {
89 adapter(ctx)
90 } else {
91 self.render()
92 }
93 }
94
95 pub fn with_adapter<F>(mut self, f: F) -> Self
97 where
98 F: Fn(&str) -> String + Send + Sync + 'static,
99 {
100 self.adapter = Some(std::sync::Arc::new(f));
101 self
102 }
103}
104
105impl std::ops::Add for PromptSection {
107 type Output = PromptComposite;
108
109 fn add(self, rhs: PromptSection) -> Self::Output {
110 PromptComposite {
111 sections: vec![self, rhs],
112 }
113 }
114}
115
116#[derive(Clone, Debug)]
118pub struct PromptComposite {
119 pub sections: Vec<PromptSection>,
121}
122
123impl PromptComposite {
124 pub fn render(&self) -> String {
126 self.sections
127 .iter()
128 .map(|s| s.render())
129 .collect::<Vec<_>>()
130 .join("\n\n")
131 }
132}
133
134impl PromptComposite {
135 pub fn only(self, kinds: &[PromptSectionKind]) -> Self {
137 Self {
138 sections: self
139 .sections
140 .into_iter()
141 .filter(|s| kinds.contains(&s.kind))
142 .collect(),
143 }
144 }
145
146 pub fn without(self, kinds: &[PromptSectionKind]) -> Self {
148 Self {
149 sections: self
150 .sections
151 .into_iter()
152 .filter(|s| !kinds.contains(&s.kind))
153 .collect(),
154 }
155 }
156
157 pub fn reorder(mut self, order: &[PromptSectionKind]) -> Self {
159 self.sections.sort_by_key(|s| {
160 order
161 .iter()
162 .position(|k| k == &s.kind)
163 .unwrap_or(usize::MAX)
164 });
165 self
166 }
167
168 pub fn reorder_by_name(self, order: &[&str]) -> Self {
172 let order: Vec<&str> = order.to_vec();
173 let mut ordered = Vec::with_capacity(self.sections.len());
174 let mut remaining = self.sections;
175
176 for name in &order {
177 let mut i = 0;
178 while i < remaining.len() {
179 if remaining[i].name.as_deref() == Some(name) {
180 ordered.push(remaining.remove(i));
181 } else {
182 i += 1;
183 }
184 }
185 }
186 ordered.extend(remaining);
187 Self { sections: ordered }
188 }
189
190 pub fn only_by_name(self, names: &[&str]) -> Self {
192 Self {
193 sections: self
194 .sections
195 .into_iter()
196 .filter(|s| {
197 s.name
198 .as_deref()
199 .map(|n| names.contains(&n))
200 .unwrap_or(false)
201 })
202 .collect(),
203 }
204 }
205
206 pub fn without_by_name(self, names: &[&str]) -> Self {
208 Self {
209 sections: self
210 .sections
211 .into_iter()
212 .filter(|s| {
213 s.name
214 .as_deref()
215 .map(|n| !names.contains(&n))
216 .unwrap_or(true)
217 })
218 .collect(),
219 }
220 }
221
222 pub fn apply(self, transform: PromptTransform) -> Self {
224 match transform {
225 PromptTransform::Reorder(order) => {
226 let refs: Vec<&str> = order.iter().map(|s| s.as_str()).collect();
227 self.reorder_by_name(&refs)
228 }
229 PromptTransform::Only(names) => {
230 let refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
231 self.only_by_name(&refs)
232 }
233 PromptTransform::Without(names) => {
234 let refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
235 self.without_by_name(&refs)
236 }
237 }
238 }
239}
240
241#[derive(Clone, Debug)]
245pub enum PromptTransform {
246 Reorder(Vec<String>),
248 Only(Vec<String>),
250 Without(Vec<String>),
252}
253
254impl From<PromptComposite> for String {
255 fn from(p: PromptComposite) -> String {
256 p.render()
257 }
258}
259
260impl From<PromptSection> for String {
261 fn from(s: PromptSection) -> String {
262 s.render()
263 }
264}
265
266impl std::ops::Add<PromptSection> for PromptComposite {
267 type Output = PromptComposite;
268
269 fn add(mut self, rhs: PromptSection) -> Self::Output {
270 self.sections.push(rhs);
271 self
272 }
273}
274
275pub struct P;
277
278impl P {
279 pub fn role(role: &str) -> PromptSection {
281 PromptSection {
282 kind: PromptSectionKind::Role,
283 content: role.to_string(),
284 name: Some("role".to_string()),
285 adapter: None,
286 }
287 }
288
289 pub fn task(task: &str) -> PromptSection {
291 PromptSection {
292 kind: PromptSectionKind::Task,
293 content: task.to_string(),
294 name: Some("task".to_string()),
295 adapter: None,
296 }
297 }
298
299 pub fn constraint(c: &str) -> PromptSection {
301 PromptSection {
302 kind: PromptSectionKind::Constraint,
303 content: c.to_string(),
304 name: Some("constraint".to_string()),
305 adapter: None,
306 }
307 }
308
309 pub fn format(f: &str) -> PromptSection {
311 PromptSection {
312 kind: PromptSectionKind::Format,
313 content: f.to_string(),
314 name: Some("format".to_string()),
315 adapter: None,
316 }
317 }
318
319 pub fn example(input: &str, output: &str) -> PromptSection {
321 PromptSection {
322 kind: PromptSectionKind::Example,
323 content: format!("Example:\nInput: {input}\nOutput: {output}"),
324 name: Some("example".to_string()),
325 adapter: None,
326 }
327 }
328
329 pub fn text(t: &str) -> PromptSection {
331 PromptSection {
332 kind: PromptSectionKind::Text,
333 content: t.to_string(),
334 name: None,
335 adapter: None,
336 }
337 }
338
339 pub fn context(ctx: &str) -> PromptSection {
341 PromptSection {
342 kind: PromptSectionKind::Context,
343 content: ctx.to_string(),
344 name: Some("context".to_string()),
345 adapter: None,
346 }
347 }
348
349 pub fn persona(desc: &str) -> PromptSection {
351 PromptSection {
352 kind: PromptSectionKind::Persona,
353 content: desc.to_string(),
354 name: Some("persona".to_string()),
355 adapter: None,
356 }
357 }
358
359 pub fn guidelines(items: &[&str]) -> PromptSection {
361 let content = items
362 .iter()
363 .map(|item| format!("- {item}"))
364 .collect::<Vec<_>>()
365 .join("\n");
366 PromptSection {
367 kind: PromptSectionKind::Guidelines,
368 content: format!("Guidelines:\n{content}"),
369 name: Some("guidelines".to_string()),
370 adapter: None,
371 }
372 }
373
374 pub fn section(name: &str, text: &str) -> PromptSection {
376 PromptSection {
377 kind: PromptSectionKind::Text,
378 content: format!("## {}\n{}", name, text),
379 name: Some(name.to_string()),
380 adapter: None,
381 }
382 }
383
384 pub fn template(tpl: &str) -> PromptSection {
386 PromptSection {
387 kind: PromptSectionKind::Text,
388 content: tpl.to_string(),
389 name: Some("template".to_string()),
390 adapter: None,
391 }
392 }
393
394 pub fn reorder(order: &[&str]) -> PromptTransform {
404 let order: Vec<String> = order.iter().map(|s| s.to_string()).collect();
405 PromptTransform::Reorder(order)
406 }
407
408 pub fn only(names: &[&str]) -> PromptTransform {
415 let names: Vec<String> = names.iter().map(|s| s.to_string()).collect();
416 PromptTransform::Only(names)
417 }
418
419 pub fn without(names: &[&str]) -> PromptTransform {
426 let names: Vec<String> = names.iter().map(|s| s.to_string()).collect();
427 PromptTransform::Without(names)
428 }
429
430 pub fn compress() -> PromptSection {
433 PromptSection {
434 kind: PromptSectionKind::Compressed,
435 content: String::new(),
436 name: Some("compress".to_string()),
437 adapter: None,
438 }
439 }
440
441 pub fn adapt<F>(f: F) -> PromptSection
456 where
457 F: Fn(&str) -> String + Send + Sync + 'static,
458 {
459 PromptSection {
460 kind: PromptSectionKind::Adaptive,
461 content: String::new(),
462 name: Some("adapt".to_string()),
463 adapter: None,
464 }
465 .with_adapter(f)
466 }
467
468 pub fn scaffolded(steps: &[&str]) -> PromptSection {
474 let content = steps
475 .iter()
476 .enumerate()
477 .map(|(i, step)| format!("Step {}: {step}", i + 1))
478 .collect::<Vec<_>>()
479 .join("\n");
480 PromptSection {
481 kind: PromptSectionKind::Scaffolded,
482 content: format!("Follow these steps:\n{content}"),
483 name: Some("scaffolded".to_string()),
484 adapter: None,
485 }
486 }
487
488 pub fn versioned(version: &str, text: &str) -> PromptSection {
494 PromptSection {
495 kind: PromptSectionKind::Versioned,
496 content: format!("[{version}] {text}"),
497 name: Some(format!("versioned:{version}")),
498 adapter: None,
499 }
500 }
501
502 pub fn with_state(keys: &[&str]) -> gemini_adk_rs::live::InstructionModifier {
511 gemini_adk_rs::live::InstructionModifier::StateAppend(
512 keys.iter().map(|k| k.to_string()).collect(),
513 )
514 }
515
516 pub fn when(
522 predicate: impl Fn(&gemini_adk_rs::State) -> bool + Send + Sync + 'static,
523 text: impl Into<String>,
524 ) -> gemini_adk_rs::live::InstructionModifier {
525 gemini_adk_rs::live::InstructionModifier::Conditional {
526 predicate: std::sync::Arc::new(predicate),
527 text: text.into(),
528 }
529 }
530
531 pub fn context_fn(
537 f: impl Fn(&gemini_adk_rs::State) -> String + Send + Sync + 'static,
538 ) -> gemini_adk_rs::live::InstructionModifier {
539 gemini_adk_rs::live::InstructionModifier::CustomAppend(std::sync::Arc::new(f))
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn role_renders() {
549 let s = P::role("analyst");
550 assert_eq!(s.render(), "You are analyst.");
551 }
552
553 #[test]
554 fn task_renders() {
555 let s = P::task("analyze data");
556 assert_eq!(s.render(), "Your task: analyze data");
557 }
558
559 #[test]
560 fn constraint_renders() {
561 let s = P::constraint("be concise");
562 assert_eq!(s.render(), "Constraint: be concise");
563 }
564
565 #[test]
566 fn format_renders() {
567 let s = P::format("JSON");
568 assert_eq!(s.render(), "Output format: JSON");
569 }
570
571 #[test]
572 fn example_renders() {
573 let s = P::example("hello", "world");
574 assert!(s.render().contains("Input: hello"));
575 assert!(s.render().contains("Output: world"));
576 }
577
578 #[test]
579 fn compose_with_add() {
580 let prompt = P::role("analyst") + P::task("analyze data") + P::format("JSON");
581 assert_eq!(prompt.sections.len(), 3);
582 }
583
584 #[test]
585 fn composite_renders_all() {
586 let prompt = P::role("analyst") + P::task("analyze data");
587 let rendered = prompt.render();
588 assert!(rendered.contains("You are analyst."));
589 assert!(rendered.contains("Your task: analyze data"));
590 }
591
592 #[test]
593 fn context_renders() {
594 let s = P::context("user is a developer");
595 assert_eq!(s.render(), "Context: user is a developer");
596 assert_eq!(s.kind, PromptSectionKind::Context);
597 }
598
599 #[test]
600 fn persona_renders() {
601 let s = P::persona("friendly and concise");
602 assert_eq!(s.render(), "Persona: friendly and concise");
603 assert_eq!(s.kind, PromptSectionKind::Persona);
604 }
605
606 #[test]
607 fn guidelines_renders() {
608 let s = P::guidelines(&["be concise", "use examples", "cite sources"]);
609 assert!(s.render().contains("Guidelines:"));
610 assert!(s.render().contains("- be concise"));
611 assert!(s.render().contains("- use examples"));
612 assert!(s.render().contains("- cite sources"));
613 assert_eq!(s.kind, PromptSectionKind::Guidelines);
614 }
615
616 #[test]
617 fn section_kinds() {
618 assert_eq!(P::role("x").kind, PromptSectionKind::Role);
619 assert_eq!(P::task("x").kind, PromptSectionKind::Task);
620 assert_eq!(P::text("x").kind, PromptSectionKind::Text);
621 }
622
623 #[test]
624 fn section_into_string() {
625 let s: String = P::role("analyst").into();
626 assert_eq!(s, "You are analyst.");
627 }
628
629 #[test]
630 fn composite_into_string() {
631 let s: String = (P::role("analyst") + P::task("analyze data")).into();
632 assert!(s.contains("You are analyst."));
633 assert!(s.contains("Your task: analyze data"));
634 }
635
636 #[test]
637 fn reorder_by_name() {
638 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
639 let reordered = prompt.reorder_by_name(&["format", "task", "role"]);
640 assert_eq!(reordered.sections[0].name.as_deref(), Some("format"));
641 assert_eq!(reordered.sections[1].name.as_deref(), Some("task"));
642 assert_eq!(reordered.sections[2].name.as_deref(), Some("role"));
643 }
644
645 #[test]
646 fn only_by_name() {
647 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
648 let filtered = prompt.only_by_name(&["role", "task"]);
649 assert_eq!(filtered.sections.len(), 2);
650 assert_eq!(filtered.sections[0].name.as_deref(), Some("role"));
651 assert_eq!(filtered.sections[1].name.as_deref(), Some("task"));
652 }
653
654 #[test]
655 fn without_by_name() {
656 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
657 let filtered = prompt.without_by_name(&["format"]);
658 assert_eq!(filtered.sections.len(), 2);
659 assert!(filtered
660 .sections
661 .iter()
662 .all(|s| s.name.as_deref() != Some("format")));
663 }
664
665 #[test]
666 fn reorder_transform_via_apply() {
667 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
668 let transform = P::reorder(&["format", "role"]);
669 let reordered = prompt.apply(transform);
670 assert_eq!(reordered.sections[0].name.as_deref(), Some("format"));
671 assert_eq!(reordered.sections[1].name.as_deref(), Some("role"));
672 }
673
674 #[test]
675 fn only_transform_via_apply() {
676 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
677 let transform = P::only(&["task"]);
678 let filtered = prompt.apply(transform);
679 assert_eq!(filtered.sections.len(), 1);
680 assert_eq!(filtered.sections[0].name.as_deref(), Some("task"));
681 }
682
683 #[test]
684 fn without_transform_via_apply() {
685 let prompt = P::role("analyst") + P::task("analyze") + P::format("JSON");
686 let transform = P::without(&["role", "format"]);
687 let filtered = prompt.apply(transform);
688 assert_eq!(filtered.sections.len(), 1);
689 assert_eq!(filtered.sections[0].name.as_deref(), Some("task"));
690 }
691
692 #[test]
693 fn compress_renders() {
694 let s = P::compress();
695 assert_eq!(s.kind, PromptSectionKind::Compressed);
696 assert_eq!(s.render(), "[compressed] ");
697 }
698
699 #[test]
700 fn adapt_renders_with_context() {
701 let s = P::adapt(|ctx| {
702 if ctx.contains("detailed") {
703 "Be thorough.".to_string()
704 } else {
705 "Be concise.".to_string()
706 }
707 });
708 assert_eq!(s.kind, PromptSectionKind::Adaptive);
709 assert_eq!(s.render_with_context("detailed"), "Be thorough.");
710 assert_eq!(s.render_with_context("brief"), "Be concise.");
711 }
712
713 #[test]
714 fn adapt_fallback_render() {
715 let s = P::adapt(|_| "adapted".to_string());
716 assert_eq!(s.render(), "");
718 }
719
720 #[test]
721 fn scaffolded_renders() {
722 let s = P::scaffolded(&["Identify", "Analyze", "Conclude"]);
723 assert_eq!(s.kind, PromptSectionKind::Scaffolded);
724 let rendered = s.render();
725 assert!(rendered.contains("Follow these steps:"));
726 assert!(rendered.contains("Step 1: Identify"));
727 assert!(rendered.contains("Step 2: Analyze"));
728 assert!(rendered.contains("Step 3: Conclude"));
729 }
730
731 #[test]
732 fn versioned_renders() {
733 let s = P::versioned("v2.1", "Use the new methodology");
734 assert_eq!(s.kind, PromptSectionKind::Versioned);
735 assert_eq!(s.render(), "[v2.1] Use the new methodology");
736 assert_eq!(s.name.as_deref(), Some("versioned:v2.1"));
737 }
738
739 #[test]
740 fn sections_have_names() {
741 assert_eq!(P::role("x").name.as_deref(), Some("role"));
742 assert_eq!(P::task("x").name.as_deref(), Some("task"));
743 assert_eq!(P::constraint("x").name.as_deref(), Some("constraint"));
744 assert_eq!(P::format("x").name.as_deref(), Some("format"));
745 assert_eq!(P::example("x", "y").name.as_deref(), Some("example"));
746 assert_eq!(P::text("x").name, None);
747 assert_eq!(P::context("x").name.as_deref(), Some("context"));
748 assert_eq!(P::persona("x").name.as_deref(), Some("persona"));
749 assert_eq!(P::guidelines(&["x"]).name.as_deref(), Some("guidelines"));
750 assert_eq!(P::section("foo", "bar").name.as_deref(), Some("foo"));
751 assert_eq!(P::scaffolded(&["x"]).name.as_deref(), Some("scaffolded"));
752 assert_eq!(P::compress().name.as_deref(), Some("compress"));
753 }
754}