Skip to main content

robit_agent/
prompt.rs

1//! System prompt builder — assembles the system prompt from multiple modules.
2
3use std::path::Path;
4
5use crate::datetime::current_date;
6
7/// Default agent prompt template (user-editable part).
8/// Does NOT include Skills/Environment sections - those are appended automatically.
9const DEFAULT_AGENT_PROMPT: &str = include_str!("../prompts/default.md");
10
11/// Built-in system prompt - automatically appended to all prompts.
12/// Contains Skills and Environment sections (never overridden by users).
13///
14/// Note: tools are NOT listed here. They are exposed to the LLM exclusively
15/// through the OpenAI function-calling `tools` request parameter (see
16/// `ToolRegistry::tool_schemas`), which also carries each tool's parameter
17/// JSON Schema.
18const SYSTEM_PROMPT: &str = include_str!("../prompts/system.md");
19
20pub struct PromptBuilder {
21    custom_prompt: Option<String>,
22}
23
24impl PromptBuilder {
25    pub fn new() -> Self {
26        Self::with_working_dir(None)
27    }
28
29    /// Create PromptBuilder with a specific working directory to check for
30    /// project-local custom prompt.
31    /// Priority: {cwd}/.robit/prompts/agent.md > ~/.robit/prompts/agent.md
32    pub fn with_working_dir(working_dir: Option<&Path>) -> Self {
33        // Check project-local prompt first (higher priority)
34        let local_prompt = working_dir.and_then(|cwd| {
35            let path = cwd.join(".robit/prompts/agent.md");
36            std::fs::read_to_string(&path).ok()
37        });
38
39        if local_prompt.is_some() {
40            return Self {
41                custom_prompt: local_prompt,
42            };
43        }
44
45        // Fallback to global prompt
46        let global_prompt = dirs::home_dir().and_then(|home| {
47            let path = home.join(".robit/prompts/agent.md");
48            std::fs::read_to_string(&path).ok()
49        });
50
51        Self {
52            custom_prompt: global_prompt,
53        }
54    }
55
56    /// Build the complete system prompt.
57    ///
58    /// The prompt is composed of:
59    /// 1. Agent prompt (user-provided from agent.md, or default from default.md)
60    /// 2. System prompt (Skills, Environment) - automatically appended,
61    ///    defined in system.md (built-in, never overridden by users)
62    ///
63    /// `skills` is a list of (name, description) pairs for enabled skills.
64    ///
65    /// Tools are intentionally NOT part of the system prompt: they are exposed
66    /// to the LLM via the OpenAI function-calling `tools` request parameter
67    /// (`ToolRegistry::tool_schemas`), which also carries parameter schemas.
68    pub fn build_system_prompt(
69        &self,
70        skills: &[(&str, &str)],
71        working_dir: &std::path::Path,
72    ) -> String {
73        let skills_section = Self::build_skills_section(skills);
74        let os = std::env::consts::OS;
75        let cwd = working_dir.display().to_string();
76        let date = current_date();
77
78        // Select base prompt: custom agent prompt or default agent prompt
79        let agent_prompt = self.custom_prompt.as_deref().unwrap_or(DEFAULT_AGENT_PROMPT);
80
81        // Replace variables in the system prompt (Skills, Environment sections)
82        let system_part = SYSTEM_PROMPT
83            .replace("{os}", os)
84            .replace("{cwd}", &cwd)
85            .replace("{date}", &date)
86            .replace("{skills_section}", &skills_section);
87
88        // Combine: agent prompt + system prompt
89        format!("{}\n\n{}", agent_prompt.trim(), system_part)
90    }
91
92    /// Build the skills description section.
93    fn build_skills_section(skills: &[(&str, &str)]) -> String {
94        if skills.is_empty() {
95            return "(no available skills)".to_string();
96        }
97
98        let mut section = String::new();
99        for (name, description) in skills {
100            section.push_str(&format!("- **{}**: {}\n", name, description));
101        }
102        section
103    }
104
105}
106
107impl Default for PromptBuilder {
108    fn default() -> Self {
109        Self::new()
110    }
111}