ep_cli: Allow dynamically loading prompts (#48046)

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle authored and GitHub committed 2026-01-30 14:55:24 -05:00
1 parent b029238af2
commit 3a2a43ab84
7 files changed
+106 -14

No files matched your search

+4 -1
View File
@@ -62,13 +62,16 @@ rand.workspace = true
similar = "2.7.0"
flate2 = "1.1.8"
toml.workspace = true
rust-embed.workspace = true
rust-embed = { workspace = true, features = ["debug-embed"] }
# Wasmtime is included as a dependency in order to enable the same
# features that are enabled in Zed.
#
# If we don't enable these features we get crashes when creating
# a Tree-sitter WasmStore.
[features]
dynamic_prompts = []
[package.metadata.cargo-machete]
ignored = ["wasmtime"]
@@ -18,17 +18,24 @@
use anyhow::{Context as _, Result, bail};
use clap::Args;
use collections::HashMap;
use rust_embed::RustEmbed;
use serde::Deserialize;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
#[derive(RustEmbed)]
#[folder = "../languages/src/"]
#[include = "*/config.toml"]
struct LanguageConfigs;
#[cfg(not(feature = "dynamic_prompts"))]
mod language_configs_embedded {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../languages/src/"]
#[include = "*/config.toml"]
pub struct LanguageConfigs;
}
#[cfg(not(feature = "dynamic_prompts"))]
use language_configs_embedded::LanguageConfigs;
#[derive(Debug, Deserialize)]
struct LanguageConfig {
@@ -89,6 +96,7 @@ pub struct FilterLanguagesArgs {
pub show_top_excluded: Option<usize>,
}
#[cfg(not(feature = "dynamic_prompts"))]
fn build_extension_to_language_map() -> HashMap<String, String> {
let mut map = HashMap::default();
@@ -113,6 +121,42 @@ fn build_extension_to_language_map() -> HashMap<String, String> {
map
}
#[cfg(feature = "dynamic_prompts")]
fn build_extension_to_language_map() -> HashMap<String, String> {
const LANGUAGES_SRC_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../languages/src");
let mut map = HashMap::default();
let languages_dir = Path::new(LANGUAGES_SRC_DIR);
let entries = match std::fs::read_dir(languages_dir) {
Ok(e) => e,
Err(_) => return map,
};
for entry in entries.flatten() {
let config_path = entry.path().join("config.toml");
if !config_path.exists() {
continue;
}
let content_str = match std::fs::read_to_string(&config_path) {
Ok(s) => s,
Err(_) => continue,
};
let config: LanguageConfig = match toml::from_str(&content_str) {
Ok(c) => c,
Err(_) => continue,
};
for suffix in &config.path_suffixes {
map.insert(suffix.to_lowercase(), config.name.clone());
}
}
map
}
fn get_all_languages(extension_map: &HashMap<String, String>) -> Vec<(String, Vec<String>)> {
let mut language_to_extensions: HashMap<String, Vec<String>> = HashMap::default();
@@ -164,7 +164,6 @@ pub fn zeta2_output_for_patch(
pub struct TeacherPrompt;
impl TeacherPrompt {
const PROMPT: &str = include_str!("prompts/teacher.md");
pub(crate) const EDITABLE_REGION_START: &str = "<|editable_region_start|>\n";
pub(crate) const EDITABLE_REGION_END: &str = "\n<|editable_region_end|>";
pub(crate) const USER_CURSOR_MARKER: &str = "<|user_cursor|>";
@@ -182,7 +181,8 @@ impl TeacherPrompt {
let context = Self::format_context(example);
let cursor_excerpt = Self::format_cursor_excerpt(example, editable_range, context_range);
let prompt = Self::PROMPT
let prompt_template = crate::prompt_assets::get_prompt("teacher.md");
let prompt = prompt_template
.replace("{{context}}", &context)
.replace("{{edit_history}}", &edit_history)
.replace("{{cursor_excerpt}}", &cursor_excerpt);
+1
View File
@@ -12,6 +12,7 @@ mod parse_output;
mod paths;
mod predict;
mod progress;
mod prompt_assets;
mod pull_examples;
mod qa;
mod reorder_patch;
@@ -0,0 +1,46 @@
use std::borrow::Cow;
#[cfg(feature = "dynamic_prompts")]
pub fn get_prompt(name: &'static str) -> Cow<'static, str> {
use anyhow::Context;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{LazyLock, RwLock};
const PROMPTS_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/prompts");
static PROMPT_CACHE: LazyLock<RwLock<HashMap<&'static str, &'static str>>> =
LazyLock::new(|| RwLock::new(HashMap::default()));
let filesystem_path = Path::new(PROMPTS_DIR).join(name);
if let Some(cached_contents) = PROMPT_CACHE.read().unwrap().get(name) {
return Cow::Borrowed(cached_contents);
}
let contents = std::fs::read_to_string(&filesystem_path)
.context(name)
.expect("Failed to read prompt");
let leaked = contents.leak();
PROMPT_CACHE.write().unwrap().insert(name, leaked);
return Cow::Borrowed(leaked);
}
#[cfg(not(feature = "dynamic_prompts"))]
pub fn get_prompt(name: &'static str) -> Cow<'static, str> {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "src/prompts"]
struct EmbeddedPrompts;
match EmbeddedPrompts::get(name) {
Some(file) => match file.data {
Cow::Borrowed(bytes) => {
Cow::Borrowed(std::str::from_utf8(bytes).expect("prompt file is not valid UTF-8"))
}
Cow::Owned(bytes) => {
Cow::Owned(String::from_utf8(bytes).expect("prompt file is not valid UTF-8"))
}
},
None => panic!("prompt file not found: {name}"),
}
}
+2 -3
View File
@@ -15,8 +15,6 @@ use serde::{Deserialize, Serialize};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
const PROMPT_TEMPLATE: &str = include_str!("prompts/qa.md");
/// Arguments for the QA command.
#[derive(Debug, Clone, clap::Args)]
pub struct QaArgs {
@@ -94,8 +92,9 @@ pub fn build_prompt(example: &Example) -> Option<String> {
}
}
let prompt_template = crate::prompt_assets::get_prompt("qa.md");
Some(
PROMPT_TEMPLATE
prompt_template
.replace("{edit_history}", &edit_history)
.replace("{cursor_excerpt}", &cursor_excerpt)
.replace("{actual_patch_word_diff}", &actual_patch_word_diff),
+2 -3
View File
@@ -16,8 +16,6 @@ use anyhow::Result;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
const PROMPT_TEMPLATE: &str = include_str!("prompts/repair.md");
/// Arguments for the repair command.
#[derive(Debug, Clone, clap::Args)]
pub struct RepairArgs {
@@ -91,8 +89,9 @@ pub fn build_repair_prompt(example: &Example) -> Option<String> {
.confidence
.map_or("unknown".to_string(), |v| v.to_string());
let prompt_template = crate::prompt_assets::get_prompt("repair.md");
Some(
PROMPT_TEMPLATE
prompt_template
.replace("{edit_history}", &edit_history)
.replace("{context}", &context)
.replace("{cursor_excerpt}", &cursor_excerpt)