Add language-specific prompt injection for toolchain guidance
- Add language_prompts module that auto-detects programming languages in workspace - Scan for language files with depth limit (2) to inject relevant toolchain prompts - Add prompts/langs/ directory for language-specific markdown files - Include Racket/raco toolchain guidance as first language prompt - Update combine_project_content() to accept language_content parameter - Integrate language detection into main CLI flow and agent mode - Update project memory with new feature documentation
This commit is contained in:
@@ -146,6 +146,7 @@ pub async fn run_agent_mode(
|
||||
agents_content_opt,
|
||||
readme_content_opt,
|
||||
memory_content_opt,
|
||||
crate::language_prompts::get_language_prompts_for_workspace(&workspace_dir),
|
||||
&workspace_dir,
|
||||
);
|
||||
|
||||
|
||||
169
crates/g3-cli/src/language_prompts.rs
Normal file
169
crates/g3-cli/src/language_prompts.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
//! Language-specific prompt injection.
|
||||
//!
|
||||
//! Detects programming languages in the workspace and injects relevant
|
||||
//! toolchain guidance into the system prompt.
|
||||
//!
|
||||
//! Language prompts are embedded at compile time from `prompts/langs/*.md`.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/// Embedded language prompts, keyed by language name.
|
||||
/// The key should match common file extensions or language identifiers.
|
||||
static LANGUAGE_PROMPTS: &[(&str, &[&str], &str)] = &[
|
||||
// (language_name, file_extensions, prompt_content)
|
||||
(
|
||||
"racket",
|
||||
&[".rkt", ".rktl", ".rktd", ".scrbl"],
|
||||
include_str!("../../../prompts/langs/racket.md"),
|
||||
),
|
||||
];
|
||||
|
||||
/// Detect languages present in the workspace by scanning for file extensions.
|
||||
/// Returns a list of detected language names.
|
||||
pub fn detect_languages(workspace_dir: &Path) -> Vec<&'static str> {
|
||||
let mut detected = Vec::new();
|
||||
|
||||
for (lang_name, extensions, _) in LANGUAGE_PROMPTS {
|
||||
if has_files_with_extensions(workspace_dir, extensions) {
|
||||
detected.push(*lang_name);
|
||||
}
|
||||
}
|
||||
|
||||
detected
|
||||
}
|
||||
|
||||
/// Check if the workspace contains files with any of the given extensions.
|
||||
/// Scans up to a reasonable depth to avoid slow startup on large repos.
|
||||
fn has_files_with_extensions(workspace_dir: &Path, extensions: &[&str]) -> bool {
|
||||
// Quick check: scan top-level and one level deep
|
||||
// This avoids slow startup on large repos while catching most projects
|
||||
scan_directory_for_extensions(workspace_dir, extensions, 2)
|
||||
}
|
||||
|
||||
/// Recursively scan a directory for files with given extensions, up to max_depth.
|
||||
fn scan_directory_for_extensions(dir: &Path, extensions: &[&str], max_depth: usize) -> bool {
|
||||
if max_depth == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let entries = match std::fs::read_dir(dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Skip hidden directories and common non-source directories
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.starts_with('.') || name == "node_modules" || name == "target" || name == "vendor" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
for ext in extensions {
|
||||
if name.ends_with(ext) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
if scan_directory_for_extensions(&path, extensions, max_depth - 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Get the prompt content for a specific language.
|
||||
pub fn get_language_prompt(lang: &str) -> Option<&'static str> {
|
||||
LANGUAGE_PROMPTS
|
||||
.iter()
|
||||
.find(|(name, _, _)| *name == lang)
|
||||
.map(|(_, _, content)| *content)
|
||||
}
|
||||
|
||||
/// Get all language prompts for detected languages in the workspace.
|
||||
/// Returns formatted content ready for injection into the system prompt.
|
||||
pub fn get_language_prompts_for_workspace(workspace_dir: &Path) -> Option<String> {
|
||||
let detected = detect_languages(workspace_dir);
|
||||
|
||||
if detected.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut prompts = Vec::new();
|
||||
for lang in detected {
|
||||
if let Some(content) = get_language_prompt(lang) {
|
||||
prompts.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
if prompts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(format!(
|
||||
"🔧 Language-Specific Guidance:\n\n{}",
|
||||
prompts.join("\n\n---\n\n")
|
||||
))
|
||||
}
|
||||
|
||||
/// List all available language prompts.
|
||||
pub fn list_available_languages() -> Vec<&'static str> {
|
||||
LANGUAGE_PROMPTS.iter().map(|(name, _, _)| *name).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_racket_prompt_embedded() {
|
||||
let prompt = get_language_prompt("racket");
|
||||
assert!(prompt.is_some());
|
||||
assert!(prompt.unwrap().contains("raco"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_available_languages() {
|
||||
let langs = list_available_languages();
|
||||
assert!(langs.contains(&"racket"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_racket_files() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let rkt_file = temp_dir.path().join("main.rkt");
|
||||
fs::write(&rkt_file, "#lang racket\n").unwrap();
|
||||
|
||||
let detected = detect_languages(temp_dir.path());
|
||||
assert!(detected.contains(&"racket"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_detection_empty_dir() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let detected = detect_languages(temp_dir.path());
|
||||
assert!(detected.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_prompts_for_workspace() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let rkt_file = temp_dir.path().join("main.rkt");
|
||||
fs::write(&rkt_file, "#lang racket\n").unwrap();
|
||||
|
||||
let prompts = get_language_prompts_for_workspace(temp_dir.path());
|
||||
assert!(prompts.is_some());
|
||||
let content = prompts.unwrap();
|
||||
assert!(content.contains("🔧 Language-Specific Guidance"));
|
||||
assert!(content.contains("raco"));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod metrics;
|
||||
pub mod project_files;
|
||||
pub mod streaming_markdown;
|
||||
pub mod embedded_agents;
|
||||
pub mod language_prompts;
|
||||
|
||||
mod accumulative;
|
||||
mod agent_mode;
|
||||
@@ -98,6 +99,7 @@ pub async fn run() -> Result<()> {
|
||||
let agents_content = read_agents_config(&workspace_dir);
|
||||
let readme_content = read_project_readme(&workspace_dir);
|
||||
let memory_content = read_project_memory(&workspace_dir);
|
||||
let language_content = language_prompts::get_language_prompts_for_workspace(&workspace_dir);
|
||||
|
||||
// Create project model
|
||||
let project = create_project(&cli, &workspace_dir)?;
|
||||
@@ -110,7 +112,7 @@ pub async fn run() -> Result<()> {
|
||||
let config = load_config_with_cli_overrides(&cli)?;
|
||||
|
||||
// Combine AGENTS.md, README, and memory content
|
||||
let combined_content = combine_project_content(agents_content, readme_content, memory_content, &workspace_dir);
|
||||
let combined_content = combine_project_content(agents_content, readme_content, memory_content, language_content, &workspace_dir);
|
||||
|
||||
run_console_mode(cli, config, project, combined_content, workspace_dir).await
|
||||
}
|
||||
|
||||
@@ -90,12 +90,13 @@ pub fn combine_project_content(
|
||||
agents_content: Option<String>,
|
||||
readme_content: Option<String>,
|
||||
memory_content: Option<String>,
|
||||
language_content: Option<String>,
|
||||
workspace_dir: &Path,
|
||||
) -> Option<String> {
|
||||
// Always include working directory to prevent LLM from hallucinating paths
|
||||
let cwd_info = format!("📂 Working Directory: {}", workspace_dir.display());
|
||||
|
||||
let parts: Vec<String> = [Some(cwd_info), agents_content, readme_content, memory_content]
|
||||
let parts: Vec<String> = [Some(cwd_info), agents_content, readme_content, memory_content, language_content]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
@@ -222,6 +223,7 @@ mod tests {
|
||||
Some("agents".to_string()),
|
||||
Some("readme".to_string()),
|
||||
Some("memory".to_string()),
|
||||
Some("language".to_string()),
|
||||
&workspace,
|
||||
);
|
||||
assert!(result.is_some());
|
||||
@@ -230,12 +232,13 @@ mod tests {
|
||||
assert!(content.contains("agents"));
|
||||
assert!(content.contains("readme"));
|
||||
assert!(content.contains("memory"));
|
||||
assert!(content.contains("language"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_combine_project_content_partial() {
|
||||
let workspace = std::path::PathBuf::from("/test/workspace");
|
||||
let result = combine_project_content(None, Some("readme".to_string()), None, &workspace);
|
||||
let result = combine_project_content(None, Some("readme".to_string()), None, None, &workspace);
|
||||
assert!(result.is_some());
|
||||
let content = result.unwrap();
|
||||
assert!(content.contains("📂 Working Directory: /test/workspace"));
|
||||
@@ -245,7 +248,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_combine_project_content_all_none() {
|
||||
let workspace = std::path::PathBuf::from("/test/workspace");
|
||||
let result = combine_project_content(None, None, None, &workspace);
|
||||
let result = combine_project_content(None, None, None, None, &workspace);
|
||||
// Now always returns Some because we always include the working directory
|
||||
assert!(result.is_some());
|
||||
assert!(result.unwrap().contains("📂 Working Directory: /test/workspace"));
|
||||
|
||||
Reference in New Issue
Block a user