#[derive(Clone, Debug)]
pub struct SystemPrompts {
pub chat_with_docs: String,
pub chat_without_docs: String,
pub rewrite: String,
}
impl Default for SystemPrompts {
fn default() -> Self {
Self {
chat_with_docs: "You are a helpful document assistant. Answer the user's question \
explicitly using the provided Context snippets.\n\
\n\
Context:\n{context}\n".to_string(),
chat_without_docs: "You are a helpful assistant. Answer the user's question.\n"
.to_string(),
rewrite: "You are a query rewriter. Given the conversation and the \
latest question, produce a single self-contained search query \
that captures all relevant context. Output ONLY the rewritten \
query, nothing else.\n\n\
Latest question: {question}".to_string(),
}
}
}
impl SystemPrompts {
pub fn load_chat_from_file(path: &std::path::Path) -> anyhow::Result<Self> {
let mut s = Self::default();
let raw = std::fs::read_to_string(path)?;
s.chat_with_docs = raw.clone();
s.chat_without_docs = strip_context_placeholder(&raw);
Ok(s)
}
pub fn load_rewrite_from_file(&mut self, path: &std::path::Path) -> anyhow::Result<()> {
self.rewrite = std::fs::read_to_string(path)?;
Ok(())
}
pub fn format_chat_with_docs(&self, context: &str) -> String {
self.chat_with_docs.replace("{context}", context)
}
pub fn format_rewrite(&self, memory: &str, question: &str) -> String {
let mut prompt = memory.to_string();
prompt.push_str(&self.rewrite.replace("{question}", question));
prompt
}
}
fn strip_context_placeholder(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let lines: Vec<&str> = text.lines().collect();
let mut skip_next_blank = false;
for (i, line) in lines.iter().enumerate() {
if line.contains("{context}") {
skip_next_blank = true;
continue;
}
if skip_next_blank && line.trim().is_empty() {
skip_next_blank = false;
continue;
}
if i + 1 < lines.len()
&& lines[i + 1].contains("{context}")
&& (line.trim().is_empty()
|| line.trim().eq_ignore_ascii_case("Context:")
|| line.trim().eq_ignore_ascii_case("Context"))
{
continue;
}
out.push_str(line);
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_contain_placeholders() {
let p = SystemPrompts::default();
assert!(p.chat_with_docs.contains("{context}"));
assert!(p.rewrite.contains("{question}"));
}
#[test]
fn strip_context_removes_placeholder_and_blanks() {
let input = "You are helpful.\n\nContext:\n{context}\n\nBe concise.\n";
let result = strip_context_placeholder(input);
assert!(!result.contains("{context}"));
assert!(result.contains("You are helpful."));
assert!(result.contains("Be concise."));
assert!(!result.contains("Context:"));
}
#[test]
fn format_chat_with_docs_substitutes_context() {
let prompts = SystemPrompts::default();
let result = prompts.format_chat_with_docs("TEST CONTEXT");
assert!(result.contains("TEST CONTEXT"));
assert!(!result.contains("{context}"));
}
#[test]
fn format_rewrite_substitutes_question() {
let prompts = SystemPrompts::default();
let result = prompts.format_rewrite("Memory:\n", "What is RAG?");
assert!(result.contains("Memory:"));
assert!(result.contains("What is RAG?"));
assert!(!result.contains("{question}"));
}
}