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}