Skip to main content

apollo/
prompt.rs

1//! System prompt builder — reads SOUL.md, USER.md, AGENTS.md, MEMORY.md, TOOLS.md, IDENTITY.md
2//! and assembles them into a system prompt for the LLM.
3
4use std::path::Path;
5
6const DEFAULT_PROMPT: &str = "You are a helpful AI assistant.";
7
8const ROUTING_GUIDANCE: &str = "## Routing guidance
9In group chats, respond to questions about the assistant, its plugins, settings, commands, upgrades, or transport even without a direct mention. Ignore unrelated ambient chatter unless the message clearly addresses the assistant or requests help.";
10
11const PROMPT_FILES: [(&str, &str, usize); 6] = [
12    ("IDENTITY.md", "## Identity", 12_000),
13    ("SOUL.md", "## Personality & Tone", 12_000),
14    ("USER.md", "## About the User", 12_000),
15    ("AGENTS.md", "## Workspace Rules", 16_000),
16    ("TOOLS.md", "## Tool Notes", 12_000),
17    ("MEMORY.md", "## Long-Term Memory", 8_000),
18];
19
20/// Build the system prompt from workspace context files
21pub async fn build_system_prompt(workspace: &Path) -> String {
22    let body = load_workspace_sections(workspace, &PROMPT_FILES).await;
23    let mut prompt = if body.is_empty() {
24        DEFAULT_PROMPT.to_string()
25    } else {
26        body
27    };
28    prompt.push_str("\n\n");
29    prompt.push_str(ROUTING_GUIDANCE);
30    prompt
31}
32
33async fn load_workspace_sections(workspace: &Path, files: &[(&str, &str, usize)]) -> String {
34    let mut parts = Vec::new();
35    for &(filename, header, limit) in files {
36        if let Some(content) = read_file(workspace, filename, limit).await {
37            parts.push(format!("{header}\n{content}"));
38        }
39    }
40    parts.join("\n\n---\n\n")
41}
42
43/// Read a file from workspace, return None if missing
44async fn read_file(workspace: &Path, filename: &str, limit: usize) -> Option<String> {
45    let path = workspace.join(filename);
46    let content = tokio::fs::read_to_string(&path).await.ok()?;
47    let trimmed = content.trim();
48    if trimmed.is_empty() {
49        return None;
50    }
51    if trimmed.len() > limit {
52        Some(format!("{}...\n(truncated)", &trimmed[..limit]))
53    } else {
54        Some(trimmed.to_string())
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use std::path::PathBuf;
62
63    #[tokio::test]
64    async fn test_build_system_prompt_empty_workspace() {
65        let prompt = build_system_prompt(&PathBuf::from("/nonexistent")).await;
66        assert!(prompt.contains(DEFAULT_PROMPT));
67        assert!(prompt.contains("Routing guidance"));
68    }
69}