use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use super::{expand_tilde, Config};
pub const SYSTEM_PROMPT_FILE_ENV: &str = "NEWT_SYSTEM_PROMPT_FILE";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct PromptConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemPromptOverride {
pub path: PathBuf,
pub text: String,
}
impl Config {
pub fn system_prompt_file(&self) -> Option<PathBuf> {
self.system_prompt_file_with(std::env::var(SYSTEM_PROMPT_FILE_ENV).ok())
}
pub(crate) fn system_prompt_file_with(&self, env: Option<String>) -> Option<PathBuf> {
let chosen = match env {
Some(twin) => twin,
None => self.prompt.as_ref().and_then(|p| p.file.clone())?,
};
(!chosen.trim().is_empty()).then(|| expand_tilde(&chosen))
}
pub fn system_prompt_override(&self) -> Result<Option<SystemPromptOverride>, String> {
self.system_prompt_file()
.map(SystemPromptOverride::read)
.transpose()
}
}
impl SystemPromptOverride {
pub fn read_typed(path: &str) -> Result<Self, String> {
Self::read(expand_tilde(path))
}
pub fn read(path: PathBuf) -> Result<Self, String> {
Self::read_with(path, |p| std::fs::read_to_string(p))
}
pub(crate) fn read_with(
path: PathBuf,
read: impl FnOnce(&std::path::Path) -> std::io::Result<String>,
) -> Result<Self, String> {
let text =
read(&path).map_err(|e| format!("system prompt file {}: {e}", path.display()))?;
if text.trim().is_empty() {
return Err(format!("system prompt file {} is empty", path.display()));
}
Ok(Self { path, text })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(toml_text: &str) -> Config {
toml::from_str(toml_text).unwrap()
}
#[test]
fn absent_section_means_no_override() {
let c = cfg("");
assert!(c.prompt.is_none());
assert_eq!(c.system_prompt_file_with(None), None);
}
#[test]
fn the_session_twin_beats_the_config_file() {
let c = cfg("[prompt]\nfile = \"/etc/p.md\"\n");
assert_eq!(c.system_prompt_file_with(None), Some("/etc/p.md".into()));
assert_eq!(
c.system_prompt_file_with(Some("/tmp/session.md".into())),
Some("/tmp/session.md".into())
);
assert_eq!(c.system_prompt_file_with(Some(" ".into())), None);
}
#[test]
fn contents_are_kept_byte_for_byte() {
let text = " leading space\n\ttab, trailing newline lost? no.\n\n";
let o = SystemPromptOverride::read_with("/p".into(), |_| Ok(text.to_string())).unwrap();
assert_eq!(o.text, text);
}
#[test]
fn an_unreadable_or_blank_file_is_an_error_never_a_fallback() {
let missing = SystemPromptOverride::read_with("/p".into(), |_| {
Err(std::io::ErrorKind::NotFound.into())
});
assert!(missing.unwrap_err().contains("/p"));
let blank = SystemPromptOverride::read_with("/p".into(), |_| Ok(" \n".into()));
assert!(blank.unwrap_err().contains("empty"));
}
}