use crate::cache::CacheManager;
use anyhow::Result;
use super::context::CodebaseContext;
const PROMPT_TEMPLATE: &str = include_str!("prompt_template.md");
fn read_project_config(workspace_root: &std::path::Path) -> Option<String> {
let config_path = workspace_root.join("REFLEX.md");
if !config_path.exists() {
log::debug!("No REFLEX.md found at workspace root");
return None;
}
match std::fs::read_to_string(&config_path) {
Ok(contents) => {
log::info!("Loaded project configuration from REFLEX.md ({} bytes)", contents.len());
Some(contents)
}
Err(e) => {
log::warn!("Failed to read REFLEX.md: {}", e);
None
}
}
}
pub fn build_prompt(
question: &str,
cache: &CacheManager,
additional_context: Option<&str>,
) -> Result<String> {
let context = CodebaseContext::extract(cache)
.unwrap_or_else(|e| {
log::warn!("Failed to extract full codebase context: {}. Using minimal context.", e);
CodebaseContext {
total_files: 0,
languages: vec![],
top_level_dirs: vec![],
common_paths: vec![],
is_monorepo: false,
project_count: None,
dominant_language: None,
}
});
let context_str = if context.total_files == 0 {
"No files indexed yet (empty codebase).".to_string()
} else {
context.to_prompt_string()
};
let workspace_root = cache.workspace_root();
let project_config = read_project_config(&workspace_root)
.unwrap_or_else(|| {
log::debug!("No project-specific configuration found, using defaults");
"No project-specific instructions provided.".to_string()
});
let additional_context_str = additional_context
.map(|ctx| format!("\n## Additional Context\n\n{}\n", ctx))
.unwrap_or_default();
let prompt = PROMPT_TEMPLATE
.replace("{CODEBASE_CONTEXT}", &context_str)
.replace("{PROJECT_CONFIG}", &project_config)
.replace("{ADDITIONAL_CONTEXT}", &additional_context_str);
Ok(format!(
r#"{prompt}
## Response Format
You MUST respond with valid JSON matching this exact schema:
```json
{schema}
```
## User Question
{question}
**IMPORTANT:** Return ONLY valid JSON. No markdown code blocks, no explanations outside the JSON structure. Just pure JSON.
"#,
prompt = prompt,
schema = crate::semantic::schema::RESPONSE_SCHEMA,
question = question
))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_prompt_contains_schema() {
let temp_dir = TempDir::new().unwrap();
let cache = CacheManager::new(temp_dir.path());
let prompt = build_prompt("find todos", &cache, None).unwrap();
assert!(prompt.contains("Response Format"));
assert!(prompt.contains("find todos"));
assert!(prompt.contains("JSON"));
}
#[test]
fn test_prompt_injects_codebase_context() {
let temp_dir = TempDir::new().unwrap();
let cache = CacheManager::new(temp_dir.path());
cache.init().unwrap();
let prompt = build_prompt("test", &cache, None).unwrap();
assert!(prompt.contains("No files indexed yet (empty codebase).") || prompt.contains("Languages:"));
}
#[test]
fn test_prompt_injects_additional_context() {
let temp_dir = TempDir::new().unwrap();
let cache = CacheManager::new(temp_dir.path());
let additional_context = "## Project Structure\nservices/\n backend/\n frontend/";
let prompt = build_prompt("test", &cache, Some(additional_context)).unwrap();
assert!(prompt.contains("Additional Context"));
assert!(prompt.contains("services/"));
assert!(prompt.contains("backend/"));
}
}