Files
galaxy/app/src/ai/prompt_builder/mod.rs
T
Ryan WardandClaude Opus 4.6 59cfd0e2f5 Bump version to 1.6.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-12 14:17:06 -05:00

191 lines
5.4 KiB
Rust

//! Prompt Builder Module
//!
//! Centralized system for constructing AI model prompts based on the current
//! operational mode (code, plan, review, etc.) and provider (Anthropic, etc.).
//!
//! # Architecture
//!
//! The prompt builder separates concerns into:
//! - **Mode**: What the agent is doing (coding, planning, reviewing, etc.)
//! - **Provider**: Which LLM is being used (Anthropic Claude, future: Gemini, GPT, etc.)
//! - **Context**: Environment info (working directory, OS, shell, git state, etc.)
//! - **Rules**: Project-specific rules from AGENTS.md / GALAXY.md files
//! - **Tools**: Which tools are available (varies by mode)
//!
//! The builder composes these layers to produce the final system prompt and tool
//! definitions for each request.
mod context;
mod mode;
mod prompts;
mod tools;
pub use context::PromptContext;
pub use mode::Mode;
pub use prompts::provider::Provider;
#[allow(unused_imports)]
pub use tools::tools_for_mode;
use crate::ai::bedrock::convert::ToolDefinition;
/// The fully-resolved prompt configuration ready to send to a model.
#[derive(Debug, Clone)]
pub struct ResolvedPrompt {
/// The system prompt text.
pub system_prompt: String,
/// The tool definitions available for this request.
pub tools: Vec<ToolDefinition>,
}
/// The main prompt builder. Composes mode, provider, context, and rules
/// into a final system prompt and tool set.
pub struct PromptBuilder {
mode: Mode,
provider: Provider,
context: PromptContext,
project_rules: Vec<ProjectRule>,
mcp_tools: Vec<ToolDefinition>,
}
/// A project rule loaded from AGENTS.md or GALAXY.md files.
#[derive(Debug, Clone)]
pub struct ProjectRule {
pub root_path: String,
pub content: String,
}
impl PromptBuilder {
/// Create a new prompt builder with the given mode and provider.
pub fn new(mode: Mode, provider: Provider) -> Self {
Self {
mode,
provider,
context: PromptContext::default(),
project_rules: Vec::new(),
mcp_tools: Vec::new(),
}
}
/// Set the environment context.
pub fn with_context(mut self, context: PromptContext) -> Self {
self.context = context;
self
}
/// Add project rules (from AGENTS.md, GALAXY.md, etc.).
pub fn with_project_rules(mut self, rules: Vec<ProjectRule>) -> Self {
self.project_rules = rules;
self
}
/// Add MCP tools from connected servers.
pub fn with_mcp_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.mcp_tools = tools;
self
}
/// Build the final resolved prompt.
pub fn build(&self) -> ResolvedPrompt {
let system_prompt = self.build_system_prompt();
let tools = self.build_tools();
ResolvedPrompt {
system_prompt,
tools,
}
}
fn build_system_prompt(&self) -> String {
let mut parts: Vec<String> = Vec::with_capacity(8);
// 1. Identity + mode-specific base prompt
parts.push(self.identity_prompt());
// 2. Environment context
if let Some(env) = self.environment_section() {
parts.push(env);
}
// 3. Project rules
if let Some(rules) = self.rules_section() {
parts.push(rules);
}
// 4. Mode-specific instructions
parts.push(self.mode_instructions());
// 5. Tool usage guidelines
parts.push(self.tool_usage_section());
parts.join("\n\n")
}
fn identity_prompt(&self) -> String {
let base = prompts::base_identity(&self.provider);
let mode_identity = prompts::mode_identity(&self.mode);
format!("{base}\n\n{mode_identity}")
}
fn environment_section(&self) -> Option<String> {
let ctx = &self.context;
if ctx.working_dir.is_empty() && ctx.os.is_empty() {
return None;
}
let mut lines = vec!["## Environment".to_string()];
if !ctx.working_dir.is_empty() {
lines.push(format!("- Working directory: {}", ctx.working_dir));
}
if !ctx.home_dir.is_empty() {
lines.push(format!("- Home directory: {}", ctx.home_dir));
}
if !ctx.os.is_empty() {
lines.push(format!("- OS: {}", ctx.os));
}
if !ctx.shell.is_empty() {
lines.push(format!("- Shell: {}", ctx.shell));
}
if !ctx.git_branch.is_empty() {
lines.push(format!("- Git branch: {}", ctx.git_branch));
}
Some(lines.join("\n"))
}
fn rules_section(&self) -> Option<String> {
if self.project_rules.is_empty() {
return None;
}
let mut section = String::from("## Project Rules\n");
for rule in &self.project_rules {
if !rule.root_path.is_empty() {
section.push_str(&format!("### Rules from {}\n", rule.root_path));
}
section.push_str(&rule.content);
section.push('\n');
}
Some(section)
}
fn mode_instructions(&self) -> String {
prompts::mode_instructions(&self.mode, &self.provider)
}
fn tool_usage_section(&self) -> String {
prompts::tool_usage_guidelines(&self.mode, &self.provider)
}
fn build_tools(&self) -> Vec<ToolDefinition> {
let mut tool_set = tools::tools_for_mode(&self.mode);
// Append MCP tools
tool_set.extend(self.mcp_tools.clone());
tool_set
}
}
#[cfg(test)]
mod tests;