Skip to main content

bamboo_server/
prompt_defaults.rs

1//! Global prompt template defaults used by new sessions.
2//!
3//! Phase 3 semantics:
4//! - Global system prompt settings are stored under Bamboo data.
5//! - New sessions use this template when no explicit base prompt is provided.
6//! - Existing sessions stay stable via persisted `metadata.base_system_prompt`.
7
8use std::path::{Path, PathBuf};
9
10const SYSTEM_PROMPT_FILE_NAME: &str = "system-prompt.md";
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum GlobalSystemPromptSource {
14    BambooFile,
15    BuiltinDefault,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct GlobalSystemPromptTemplate {
20    pub content: String,
21    pub source: GlobalSystemPromptSource,
22    pub path: Option<PathBuf>,
23}
24
25fn bamboo_system_prompt_file_path() -> PathBuf {
26    bamboo_infrastructure::paths::bamboo_dir().join(SYSTEM_PROMPT_FILE_NAME)
27}
28
29fn read_non_empty_prompt_file(path: &Path) -> Option<String> {
30    std::fs::read_to_string(path).ok().and_then(|content| {
31        let trimmed = content.trim();
32        if trimmed.is_empty() {
33            None
34        } else {
35            Some(trimmed.to_string())
36        }
37    })
38}
39
40fn builtin_default_prompt() -> String {
41    crate::app_state::DEFAULT_BASE_PROMPT.to_string()
42}
43
44fn resolve_with_paths(bamboo_path: &Path) -> GlobalSystemPromptTemplate {
45    if let Some(content) = read_non_empty_prompt_file(bamboo_path) {
46        return GlobalSystemPromptTemplate {
47            content,
48            source: GlobalSystemPromptSource::BambooFile,
49            path: Some(bamboo_path.to_path_buf()),
50        };
51    }
52
53    GlobalSystemPromptTemplate {
54        content: builtin_default_prompt(),
55        source: GlobalSystemPromptSource::BuiltinDefault,
56        path: None,
57    }
58}
59
60/// Read the global default prompt template used for new session initialization.
61///
62/// Fallback order:
63/// 1. `${BAMBOO_DATA_DIR}/system-prompt.md` (typically `~/.bamboo/system-prompt.md`) if present and non-empty
64/// 2. Builtin [`crate::app_state::DEFAULT_BASE_PROMPT`]
65pub fn read_global_default_system_prompt_template() -> String {
66    resolve_global_default_system_prompt_template().content
67}
68
69/// Resolve the global default prompt template with source metadata.
70pub fn resolve_global_default_system_prompt_template() -> GlobalSystemPromptTemplate {
71    let bamboo_path = bamboo_system_prompt_file_path();
72    resolve_with_paths(&bamboo_path)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::{
78        read_global_default_system_prompt_template, resolve_with_paths, GlobalSystemPromptSource,
79    };
80    use std::fs;
81    use tempfile::tempdir;
82
83    #[test]
84    fn global_default_template_is_never_empty() {
85        assert!(!read_global_default_system_prompt_template()
86            .trim()
87            .is_empty());
88    }
89
90    #[test]
91    fn resolution_prefers_bamboo_file_when_present() {
92        let temp = tempdir().expect("temp dir");
93        let bamboo_prompt = temp.path().join("bamboo-system-prompt.md");
94
95        fs::write(&bamboo_prompt, "from bamboo").expect("write bamboo prompt");
96
97        let resolved = resolve_with_paths(&bamboo_prompt);
98        assert_eq!(resolved.content, "from bamboo");
99        assert_eq!(resolved.source, GlobalSystemPromptSource::BambooFile);
100        assert_eq!(resolved.path.as_deref(), Some(bamboo_prompt.as_path()));
101    }
102
103    #[test]
104    fn resolution_falls_back_to_builtin_when_bamboo_missing() {
105        let temp = tempdir().expect("temp dir");
106        let bamboo_prompt = temp.path().join("missing-bamboo-system-prompt.md");
107
108        let resolved = resolve_with_paths(&bamboo_prompt);
109        assert_eq!(resolved.source, GlobalSystemPromptSource::BuiltinDefault);
110        assert!(resolved.path.is_none());
111        assert!(!resolved.content.trim().is_empty());
112    }
113}