Files
tkmind_go/crates/goose/src/prompt_template.rs
T
Douwe Osinga ddd35f6d47 feat: add local inference provider with llama.cpp backend and HuggingFace model management (#6933)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: jh-block <jhugo@block.xyz>
Co-authored-by: Spence <spencermartin@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
2026-02-19 18:30:05 +00:00

243 lines
7.1 KiB
Rust

use crate::config::paths::Paths;
use include_dir::{include_dir, Dir};
use minijinja::{Environment, Error as MiniJinjaError, Value as MJValue};
use serde::Serialize;
use std::path::PathBuf;
static CORE_PROMPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/prompts");
static TEMPLATE_REGISTRY: &[(&str, &str)] = &[
(
"system.md",
"Main system prompt that defines goose's personality and behavior",
),
(
"compaction.md",
"Prompt for summarizing conversation history when context limits are reached",
),
(
"subagent_system.md",
"System prompt for subagents spawned to handle specific tasks",
),
(
"recipe.md",
"Prompt for generating recipe files from conversations",
),
(
"apps_create.md",
"Prompt for generating new Goose apps based on the user instructions",
),
(
"apps_iterate.md",
"Prompt for updating existing Goose apps based on feedback",
),
(
"permission_judge.md",
"Prompt for analyzing tool operations for read-only detection",
),
(
"plan.md",
"Prompt used when goose creates step-by-step plans. CLI only",
),
(
"tiny_model_system.md",
"System prompt for tiny local models using shell command emulation",
),
(
"session_name.md",
"System prompt for generating short session names from conversation history",
),
];
/// Information about a template including its content and customization status
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
pub struct Template {
pub name: String,
pub description: String,
pub default_content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_content: Option<String>,
pub is_customized: bool,
}
fn user_prompts_dir() -> PathBuf {
Paths::config_dir().join("prompts")
}
fn is_registered(name: &str) -> bool {
TEMPLATE_REGISTRY.iter().any(|(n, _)| *n == name)
}
pub fn render_string<T: Serialize>(
template_str: &str,
context: &T,
) -> Result<String, MiniJinjaError> {
let mut env = Environment::new();
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
env.add_template("template", template_str)?;
let tmpl = env.get_template("template")?;
let ctx = MJValue::from_serialize(context);
let rendered = tmpl.render(ctx)?;
Ok(rendered.trim().to_string())
}
pub fn render_template<T: Serialize>(name: &str, context: &T) -> Result<String, MiniJinjaError> {
if !is_registered(name) {
return Err(MiniJinjaError::new(
minijinja::ErrorKind::TemplateNotFound,
format!("Template '{}' is not registered", name),
));
}
let user_path = user_prompts_dir().join(name);
let template_str = if user_path.exists() {
std::fs::read_to_string(&user_path).map_err(|e| {
MiniJinjaError::new(
minijinja::ErrorKind::InvalidOperation,
format!("Failed to read user template: {}", e),
)
})?
} else {
let file = CORE_PROMPTS_DIR.get_file(name).ok_or_else(|| {
MiniJinjaError::new(
minijinja::ErrorKind::TemplateNotFound,
format!("Built-in template '{}' not found", name),
)
})?;
String::from_utf8_lossy(file.contents()).to_string()
};
render_string(&template_str, context)
}
pub fn get_template(name: &str) -> Option<Template> {
let (_, description) = TEMPLATE_REGISTRY.iter().find(|(n, _)| *n == name)?;
let default_content = CORE_PROMPTS_DIR
.get_file(name)
.map(|file| String::from_utf8_lossy(file.contents()).to_string())?;
let user_path = user_prompts_dir().join(name);
let user_content = if user_path.exists() {
std::fs::read_to_string(&user_path).ok()
} else {
None
};
let is_customized = user_content.is_some();
Some(Template {
name: name.to_string(),
description: description.to_string(),
default_content,
user_content,
is_customized,
})
}
pub fn save_template(name: &str, content: &str) -> std::io::Result<()> {
if !is_registered(name) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Template '{}' is not registered", name),
));
}
let prompts_dir = user_prompts_dir();
std::fs::create_dir_all(&prompts_dir)?;
let path = prompts_dir.join(name);
std::fs::write(path, content)
}
/// Reset a template to its default by removing the user customization.
pub fn reset_template(name: &str) -> std::io::Result<()> {
if !is_registered(name) {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Template '{}' is not registered", name),
));
}
let path = user_prompts_dir().join(name);
if path.exists() {
std::fs::remove_file(path)
} else {
Ok(())
}
}
pub fn list_templates() -> Vec<Template> {
TEMPLATE_REGISTRY
.iter()
.filter_map(|(name, description)| {
let default_content = CORE_PROMPTS_DIR
.get_file(name)
.map(|file| String::from_utf8_lossy(file.contents()).to_string())?;
let user_path = user_prompts_dir().join(name);
let user_content = if user_path.exists() {
std::fs::read_to_string(&user_path).ok()
} else {
None
};
let is_customized = user_content.is_some();
Some(Template {
name: name.to_string(),
description: description.to_string(),
default_content,
user_content,
is_customized,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_get_template() {
let template = get_template("system.md");
assert!(template.is_some(), "system.md should be registered");
let template = template.unwrap();
assert_eq!(template.name, "system.md");
assert!(!template.description.is_empty());
assert!(!template.default_content.is_empty());
assert!(!template.is_customized);
}
#[test]
fn test_render_template() {
let context: HashMap<String, String> = HashMap::new();
let result = render_template("system.md", &context);
assert!(result.is_ok(), "Should be able to render system.md");
assert!(!result.unwrap().is_empty());
}
#[test]
fn test_list_templates() {
let templates = list_templates();
assert_eq!(templates.len(), TEMPLATE_REGISTRY.len());
let has_system = templates.iter().any(|t| t.name == "system.md");
assert!(has_system, "system.md should be in the template list");
for template in templates {
assert!(
!template.description.is_empty(),
"Each template should have a description"
);
assert!(
!template.default_content.is_empty(),
"Each template should have content"
);
}
}
}