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::datetime::current_date;
6use crate::tool::Tool;
7
8/// Default system prompt template.
9/// Placeholders: {os}, {cwd}, {date}, {tools_section}, {skills_section}
10const DEFAULT_PROMPT: &str = include_str!("../prompts/default.md");
11
12pub struct PromptBuilder {
13    custom_prompt: Option<String>,
14}
15
16impl PromptBuilder {
17    pub fn new() -> Self {
18        // Check for custom prompt file
19        let custom_path = Self::custom_prompt_path();
20        let custom_prompt = if let Some(path) = custom_path {
21            std::fs::read_to_string(&path).ok()
22        } else {
23            None
24        };
25
26        Self { custom_prompt }
27    }
28
29    /// Build the complete system prompt.
30    ///
31    /// `skills` is a list of (name, description) pairs for enabled skills.
32    pub fn build_system_prompt(
33        &self,
34        tools: &[&dyn Tool],
35        skills: &[(&str, &str)],
36        working_dir: &std::path::Path,
37    ) -> String {
38        let tools_section = Self::build_tools_section(tools);
39        let skills_section = Self::build_skills_section(skills);
40        let os = std::env::consts::OS;
41        let cwd = working_dir.display().to_string();
42        let date = current_date();
43
44        if let Some(custom) = &self.custom_prompt {
45            // Custom prompt: still inject dynamic variables
46            custom
47                .replace("{os}", os)
48                .replace("{cwd}", &cwd)
49                .replace("{date}", &date)
50                .replace("{tools_section}", &tools_section)
51                .replace("{skills_section}", &skills_section)
52        } else {
53            DEFAULT_PROMPT
54                .replace("{os}", os)
55                .replace("{cwd}", &cwd)
56                .replace("{date}", &date)
57                .replace("{tools_section}", &tools_section)
58                .replace("{skills_section}", &skills_section)
59        }
60    }
61
62    /// Build the tools description section.
63    fn build_tools_section(tools: &[&dyn Tool]) -> String {
64        if tools.is_empty() {
65            return "(no available tools)".to_string();
66        }
67
68        let mut section = String::new();
69        for tool in tools {
70            section.push_str(&format!(
71                "- **{}**: {}{}\n",
72                tool.name(),
73                tool.description(),
74                if tool.requires_confirmation() {
75                    " (requires user confirmation)"
76                } else {
77                    ""
78                }
79            ));
80        }
81        section
82    }
83
84    /// Build the skills description section.
85    fn build_skills_section(skills: &[(&str, &str)]) -> String {
86        if skills.is_empty() {
87            return "(no available skills)".to_string();
88        }
89
90        let mut section = String::new();
91        for (name, description) in skills {
92            section.push_str(&format!("- **{}**: {}\n", name, description));
93        }
94        section
95    }
96
97    fn custom_prompt_path() -> Option<PathBuf> {
98        dirs::home_dir().map(|home| home.join(".robit/prompts/system.txt"))
99    }
100}
101
102impl Default for PromptBuilder {
103    fn default() -> Self {
104        Self::new()
105    }
106}