Skip to main content

phi_agent/prompt/
mod.rs

1//! System prompt construction using composable fragments.
2//!
3//! The primary entry points are:
4//!
5//! - [`build_system_prompt`] — backward-compatible English prompt.
6//! - [`build_system_prompt_cn`] — prompt with China-specific network hints.
7//! - [`build_system_prompt_with_fragments`] — full fragment-based assembly.
8//!
9//! # Architecture
10//!
11//! phi-agent defines application-specific fragments in the [`crate::prompt::fragments`] module.
12//! Generic fragments (environment, dynamic tools) come from
13//! [`agent_works::prompt`]. Consumers inject custom fragments via
14//! [`build_system_prompt_with_fragments`].
15
16pub mod fragments;
17
18use agent_works::prompt::{FragmentContext, PromptFragment, compose_fragments};
19
20use fragments::{CoreInstructionsFragment, MemoryFragment, NetworkEnvironmentFragment};
21
22/// Build the general-purpose system prompt (English).
23///
24/// Backward-compatible wrapper around [`build_system_prompt_with_fragments`]
25/// with no extra fragments and `include_cn = false`.
26///
27/// Does not include host-specific info (consumers append that as needed).
28pub fn build_system_prompt() -> String {
29    build_system_prompt_with_fragments(&[], &[], false)
30}
31
32/// Build system prompt for users in China (GFW-aware).
33///
34/// Appends network environment hints so the agent prefers domestic services
35/// when foreign sites are inaccessible.
36pub fn build_system_prompt_cn() -> String {
37    build_system_prompt_with_fragments(&[], &[], true)
38}
39
40/// Build a system prompt from fragments.
41///
42/// # Arguments
43///
44/// * `extra_fragments` — consumer-injected custom fragments (merged into the
45///   fragment list before composition).
46/// * `tool_definitions` — tool definitions as JSON (OpenAI function-calling
47///   format). Passed to fragments that need tool info (e.g., `DynamicToolsFragment`).
48/// * `include_cn` — whether to include China-specific network environment hints.
49pub fn build_system_prompt_with_fragments(
50    extra_fragments: &[Box<dyn PromptFragment>],
51    tool_definitions: &[serde_json::Value],
52    include_cn: bool,
53) -> String {
54    let ctx = FragmentContext {
55        tool_definitions,
56        session_id: "", // not used by current fragments
57    };
58
59    let mut fragments: Vec<Box<dyn PromptFragment>> = vec![Box::new(CoreInstructionsFragment)];
60
61    if include_cn {
62        fragments.push(Box::new(NetworkEnvironmentFragment));
63    }
64
65    // Append separator before memory section
66    fragments.push(Box::new(MemoryFragment));
67
68    // Merge consumer-injected fragments
69    fragments.extend(extra_fragments.iter().map(|f| dyn_clone::clone_box(f.as_ref())));
70
71    let body = compose_fragments(&fragments, &ctx);
72
73    // Insert separator between core and memory (preserves original format)
74    // The memory fragment already renders its content; we add "---" separator
75    // before it to match the original `build_system_prompt()` format.
76    body.replacen("\n\n## Memory", "\n---\n\n## Memory", 1)
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn test_build_system_prompt() {
85        let prompt = build_system_prompt();
86        assert!(prompt.contains("versatile AI assistant"));
87        assert!(prompt.contains("[Conversation Type Detection]"));
88        assert!(prompt.contains("[Execution Guidelines]"));
89        assert!(prompt.contains("[Wrap-Up]"));
90        // Memory instructions are appended after a separator.
91        assert!(prompt.contains("\n---\n"));
92    }
93
94    #[test]
95    fn test_build_system_prompt_cn() {
96        let prompt = build_system_prompt_cn();
97        // Base prompt is preserved, then the CN network hints are appended.
98        assert!(prompt.contains("versatile AI assistant"));
99        assert!(prompt.contains("[Network Environment]"));
100        assert!(prompt.contains("mainland China"));
101        assert!(prompt.contains("Baidu"));
102        assert!(prompt.contains("gitee.com"));
103    }
104
105    #[test]
106    fn test_build_system_prompt_with_custom_fragments() {
107        #[derive(Clone)]
108        struct CustomFragment;
109        impl PromptFragment for CustomFragment {
110            fn name(&self) -> &str {
111                "custom"
112            }
113            fn priority(&self) -> i32 {
114                200
115            }
116            fn render(&self, _ctx: &FragmentContext) -> Option<String> {
117                Some("[Custom Section]".to_string())
118            }
119        }
120
121        let extra: Vec<Box<dyn PromptFragment>> = vec![Box::new(CustomFragment)];
122        let prompt = build_system_prompt_with_fragments(&extra, &[], false);
123        assert!(prompt.contains("[Custom Section]"));
124        assert!(prompt.contains("versatile AI assistant"));
125    }
126
127    #[test]
128    fn test_backward_compat_output_matches_original() {
129        // The new build_system_prompt() should produce output containing
130        // all the same key sections as the original hardcoded version.
131        let prompt = build_system_prompt();
132        let original_sections = [
133            "You are a versatile AI assistant",
134            "[Role]",
135            "[Conversation Type Detection]",
136            "[Thinking Approach]",
137            "[Execution Guidelines]",
138            "[File Operation Guidelines]",
139            "[Wrap-Up]",
140            "## Memory",
141            "MEMORY.md",
142        ];
143        for section in &original_sections {
144            assert!(prompt.contains(section), "Missing section: {}", section);
145        }
146    }
147}