use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub const PROMPT_DYNAMIC_BOUNDARY: &str = "__PROMPT_DYNAMIC_BOUNDARY__";
pub trait DynamicSection: Fn() -> String + Send + Sync {}
impl<T> DynamicSection for T where T: Fn() -> String + Send + Sync {}
pub struct SystemPromptBuilder {
static_sections: Vec<String>,
dynamic_sections: Vec<Box<dyn Fn() -> String + Send + Sync>>,
}
impl Default for SystemPromptBuilder {
fn default() -> Self {
Self::new()
}
}
impl SystemPromptBuilder {
pub fn new() -> Self {
Self {
static_sections: Vec::new(),
dynamic_sections: Vec::new(),
}
}
pub fn add_static(&mut self, content: String) {
if !content.is_empty() {
self.static_sections.push(content);
}
}
pub fn add_dynamic<F>(&mut self, generator: F)
where
F: Fn() -> String + Send + Sync + 'static,
{
self.dynamic_sections.push(Box::new(generator));
}
pub fn add_static_optional(&mut self, content: Option<String>) {
if let Some(c) = content {
if !c.is_empty() {
self.static_sections.push(c);
}
}
}
pub fn build(&self) -> String {
let static_part = self.build_static();
let dynamic_part = self.build_dynamic();
if dynamic_part.is_empty() {
static_part
} else if static_part.is_empty() {
dynamic_part
} else {
format!(
"{}\n{}\n{}",
static_part, PROMPT_DYNAMIC_BOUNDARY, dynamic_part
)
}
}
pub fn build_static(&self) -> String {
self.static_sections
.iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn build_dynamic(&self) -> String {
self.dynamic_sections
.iter()
.map(|gen| gen())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn build_cached(&self) -> (String, String) {
let static_part = self.build_static();
let dynamic_part = self.build_dynamic();
let cache_key = compute_cache_key(&static_part);
let full_prompt = if dynamic_part.is_empty() {
static_part
} else if static_part.is_empty() {
dynamic_part
} else {
format!(
"{}\n{}\n{}",
static_part, PROMPT_DYNAMIC_BOUNDARY, dynamic_part
)
};
(cache_key, full_prompt)
}
pub fn static_cache_key(&self) -> String {
compute_cache_key(&self.build_static())
}
pub fn is_empty(&self) -> bool {
self.static_sections.is_empty() && self.dynamic_sections.is_empty()
}
pub fn static_section_count(&self) -> usize {
self.static_sections.len()
}
pub fn dynamic_section_count(&self) -> usize {
self.dynamic_sections.len()
}
pub fn clear(&mut self) {
self.static_sections.clear();
self.dynamic_sections.clear();
}
}
fn compute_cache_key(content: &str) -> String {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn split_at_boundary(prompt: &str) -> (&str, &str) {
match prompt.find(PROMPT_DYNAMIC_BOUNDARY) {
Some(pos) => {
let static_part = &prompt[..pos].trim();
let dynamic_start = pos + PROMPT_DYNAMIC_BOUNDARY.len();
let dynamic_part = &prompt[dynamic_start..].trim();
(static_part, dynamic_part)
}
None => (prompt.trim(), ""),
}
}
pub fn has_boundary(prompt: &str) -> bool {
prompt.contains(PROMPT_DYNAMIC_BOUNDARY)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PromptStats {
pub static_sections: usize,
pub dynamic_sections: usize,
pub static_chars: usize,
pub dynamic_chars: usize,
}
impl SystemPromptBuilder {
pub fn stats(&self) -> PromptStats {
let static_chars: usize = self.static_sections.iter().map(|s| s.len()).sum();
PromptStats {
static_sections: self.static_sections.len(),
dynamic_sections: self.dynamic_sections.len(),
static_chars,
dynamic_chars: 0, }
}
}
#[cfg(test)]
#[path = "../../tests/unit/agent/prompt_builder/prompt_builder_test.rs"]
mod tests;