Skip to main content

robit_agent/
prompt.rs

1//! System prompt builder — assembles the system prompt from multiple modules.
2
3use std::path::PathBuf;
4
5use crate::tool::Tool;
6
7/// Default system prompt template.
8/// Placeholders: {os}, {cwd}, {date}, {tools_section}, {skills_section}
9const DEFAULT_PROMPT: &str = include_str!("../prompts/default.md");
10
11pub struct PromptBuilder {
12    custom_prompt: Option<String>,
13}
14
15impl PromptBuilder {
16    pub fn new() -> Self {
17        // Check for custom prompt file
18        let custom_path = Self::custom_prompt_path();
19        let custom_prompt = if let Some(path) = custom_path {
20            std::fs::read_to_string(&path).ok()
21        } else {
22            None
23        };
24
25        Self { custom_prompt }
26    }
27
28    /// Build the complete system prompt.
29    ///
30    /// `skills` is a list of (name, description) pairs for enabled skills.
31    pub fn build_system_prompt(
32        &self,
33        tools: &[&dyn Tool],
34        skills: &[(&str, &str)],
35        working_dir: &std::path::Path,
36    ) -> String {
37        let tools_section = Self::build_tools_section(tools);
38        let skills_section = Self::build_skills_section(skills);
39        let os = std::env::consts::OS;
40        let cwd = working_dir.display().to_string();
41        let date = chrono_date();
42
43        if let Some(custom) = &self.custom_prompt {
44            // Custom prompt: still inject dynamic variables
45            custom
46                .replace("{os}", os)
47                .replace("{cwd}", &cwd)
48                .replace("{date}", &date)
49                .replace("{tools_section}", &tools_section)
50                .replace("{skills_section}", &skills_section)
51        } else {
52            DEFAULT_PROMPT
53                .replace("{os}", os)
54                .replace("{cwd}", &cwd)
55                .replace("{date}", &date)
56                .replace("{tools_section}", &tools_section)
57                .replace("{skills_section}", &skills_section)
58        }
59    }
60
61    /// Build the tools description section.
62    fn build_tools_section(tools: &[&dyn Tool]) -> String {
63        if tools.is_empty() {
64            return "(no available tools)".to_string();
65        }
66
67        let mut section = String::new();
68        for tool in tools {
69            section.push_str(&format!(
70                "- **{}**: {}{}\n",
71                tool.name(),
72                tool.description(),
73                if tool.requires_confirmation() {
74                    " (requires user confirmation)"
75                } else {
76                    ""
77                }
78            ));
79        }
80        section
81    }
82
83    /// Build the skills description section.
84    fn build_skills_section(skills: &[(&str, &str)]) -> String {
85        if skills.is_empty() {
86            return "(no available skills)".to_string();
87        }
88
89        let mut section = String::new();
90        for (name, description) in skills {
91            section.push_str(&format!("- **{}**: {}\n", name, description));
92        }
93        section
94    }
95
96    fn custom_prompt_path() -> Option<PathBuf> {
97        dirs::home_dir().map(|home| home.join(".robit/prompts/system.txt"))
98    }
99}
100
101impl Default for PromptBuilder {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107/// Get today's date as a string (YYYY-MM-DD).
108fn chrono_date() -> String {
109    // Simple date formatting without chrono dependency.
110    // Uses std::time which doesn't have formatting, so we use a basic approach.
111    use std::time::SystemTime;
112    let now = SystemTime::now();
113    let duration = now
114        .duration_since(SystemTime::UNIX_EPOCH)
115        .unwrap_or_default();
116    let secs = duration.as_secs();
117
118    // Convert unix timestamp to date components
119    let days = secs / 86400;
120    let (year, month, day) = days_to_date(days);
121    format!("{:04}-{:02}-{:02}", year, month, day)
122}
123
124/// Convert days since epoch to (year, month, day).
125fn days_to_date(days: u64) -> (u64, u64, u64) {
126    // Algorithm from http://howardhinnant.github.io/date_algorithms.html
127    let z = days + 719468;
128    let era = z / 146097;
129    let doe = z - era * 146097;
130    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
131    let y = yoe + era * 400;
132    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
133    let mp = (5 * doy + 2) / 153;
134    let d = doy - (153 * mp + 2) / 5 + 1;
135    let m = if mp < 10 { mp + 3 } else { mp - 9 };
136    let y = if m <= 2 { y + 1 } else { y };
137    (y, m, d)
138}