feat: 更新提示词来源类型,支持用户自定义和自动生成内容;添加 AGENT.md 模板文件
This commit is contained in:
parent
83b525e442
commit
5989b817b4
25
src/gateway/agent_md_template.md
Normal file
25
src/gateway/agent_md_template.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!--
|
||||||
|
# 自定义 Agent 配置
|
||||||
|
|
||||||
|
在此文件添加您的个性化配置,这些内容会与系统默认提示词合并。
|
||||||
|
|
||||||
|
## 可用配置项示例
|
||||||
|
|
||||||
|
### 额外身份
|
||||||
|
- 我是后端开发者
|
||||||
|
- 我熟悉 Rust / Python
|
||||||
|
|
||||||
|
### 个人偏好
|
||||||
|
- 代码注释使用英文
|
||||||
|
- 优先使用中文回复技术概念
|
||||||
|
|
||||||
|
### 工作方式
|
||||||
|
- 复杂任务请先给出整体方案
|
||||||
|
- 性能优化建议优先
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
- 此文件内容会追加在系统默认提示词之后
|
||||||
|
- 留空或删除内容则不会生效
|
||||||
|
- 使用 HTML 注释 <!-- --> 可以保留说明文字而不生效
|
||||||
|
- 删除本注释块并添加您的自定义配置即可生效
|
||||||
|
-->
|
||||||
@ -5,11 +5,20 @@ use crate::agent::AgentError;
|
|||||||
use crate::platform::atomic_rename;
|
use crate::platform::atomic_rename;
|
||||||
|
|
||||||
pub(crate) const DEFAULT_AGENT_PROMPT: &str = include_str!("default_agent_prompt.md");
|
pub(crate) const DEFAULT_AGENT_PROMPT: &str = include_str!("default_agent_prompt.md");
|
||||||
|
pub(crate) const AGENT_MD_TEMPLATE: &str = include_str!("agent_md_template.md");
|
||||||
|
|
||||||
|
/// 提示词来源类型
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct PromptSource {
|
enum PromptSource {
|
||||||
|
/// 内置内容 - 系统默认,始终注入,不可修改
|
||||||
|
Builtin(&'static str),
|
||||||
|
/// 用户自定义 - 自动创建空白文件,可选注入
|
||||||
|
UserCustom {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
default_content: Option<&'static str>,
|
template: &'static str,
|
||||||
|
},
|
||||||
|
/// 自动生成 - 由系统维护,可选注入
|
||||||
|
AutoGenerated(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load_agent_prompt() -> Result<Option<String>, AgentError> {
|
pub(crate) fn load_agent_prompt() -> Result<Option<String>, AgentError> {
|
||||||
@ -22,52 +31,73 @@ pub(crate) fn upsert_managed_agent_memory_summary(markdown_body: &str) -> Result
|
|||||||
|
|
||||||
fn prompt_sources() -> Result<Vec<PromptSource>, AgentError> {
|
fn prompt_sources() -> Result<Vec<PromptSource>, AgentError> {
|
||||||
Ok(vec![
|
Ok(vec![
|
||||||
PromptSource {
|
// 1. 系统默认 - 始终注入
|
||||||
|
PromptSource::Builtin(DEFAULT_AGENT_PROMPT),
|
||||||
|
// 2. 用户自定义 - 自动创建空白模板,可选注入
|
||||||
|
PromptSource::UserCustom {
|
||||||
path: agent_prompt_path()?,
|
path: agent_prompt_path()?,
|
||||||
default_content: Some(DEFAULT_AGENT_PROMPT),
|
template: AGENT_MD_TEMPLATE,
|
||||||
},
|
|
||||||
PromptSource {
|
|
||||||
path: memory_summary_path()?,
|
|
||||||
default_content: None,
|
|
||||||
},
|
},
|
||||||
|
// 3. 记忆摘要 - 可选
|
||||||
|
PromptSource::AutoGenerated(memory_summary_path()?),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_prompt_from_sources(sources: &[PromptSource]) -> Result<Option<String>, AgentError> {
|
fn load_prompt_from_sources(sources: &[PromptSource]) -> Result<Option<String>, AgentError> {
|
||||||
let mut fragments = Vec::with_capacity(sources.len());
|
let mut fragments = Vec::new();
|
||||||
|
|
||||||
for source in sources {
|
for source in sources {
|
||||||
if let Some(fragment) = read_prompt_fragment(source)? {
|
match source {
|
||||||
fragments.push(fragment);
|
PromptSource::Builtin(content) => {
|
||||||
|
fragments.push(content.to_string());
|
||||||
|
}
|
||||||
|
PromptSource::UserCustom { path, template } => {
|
||||||
|
// 确保父目录存在
|
||||||
|
ensure_parent_dir(path)?;
|
||||||
|
// 文件不存在时创建空白模板
|
||||||
|
if !path.exists() {
|
||||||
|
fs::write(path, template)
|
||||||
|
.map_err(|err| AgentError::Other(format!("create AGENT.md template error: {}", err)))?;
|
||||||
|
}
|
||||||
|
// 读取内容,仅当非空(去除注释后)时注入
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.map_err(|err| AgentError::Other(format!("read AGENT.md error: {}", err)))?;
|
||||||
|
let trimmed = strip_comments_and_whitespace(&content);
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
fragments.push(trimmed.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PromptSource::AutoGenerated(path) => {
|
||||||
|
if path.exists() {
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.map_err(|err| AgentError::Other(format!("read MEMORY_SUMMARY.md error: {}", err)))?;
|
||||||
|
if !content.trim().is_empty() {
|
||||||
|
fragments.push(content.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fragments.is_empty() {
|
if fragments.is_empty() {
|
||||||
return Ok(None);
|
Ok(None)
|
||||||
}
|
} else {
|
||||||
|
|
||||||
Ok(Some(fragments.join("\n\n")))
|
Ok(Some(fragments.join("\n\n")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_prompt_fragment(source: &PromptSource) -> Result<Option<String>, AgentError> {
|
/// 去除 HTML 注释和空白行,检查是否还有有效内容
|
||||||
ensure_parent_dir(&source.path)?;
|
fn strip_comments_and_whitespace(content: &str) -> String {
|
||||||
|
// 去除 HTML 注释 <!-- -->
|
||||||
|
let re = regex::Regex::new(r"<!--[\s\S]*?-->").unwrap();
|
||||||
|
let without_comments = re.replace_all(content, "");
|
||||||
|
|
||||||
if !source.path.exists() {
|
// 去除空白行后检查是否还有有效内容
|
||||||
if let Some(default_content) = source.default_content {
|
without_comments
|
||||||
write_prompt_file(&source.path, default_content)?;
|
.lines()
|
||||||
} else {
|
.filter(|line| !line.trim().is_empty())
|
||||||
return Ok(None);
|
.collect::<Vec<_>>()
|
||||||
}
|
.join("\n")
|
||||||
}
|
|
||||||
|
|
||||||
let content = fs::read_to_string(&source.path)
|
|
||||||
.map_err(|err| AgentError::Other(format!("read prompt file error: {}", err)))?;
|
|
||||||
let trimmed = content.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(trimmed.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persist_memory_summary(path: &Path, markdown_body: &str) -> Result<(), AgentError> {
|
fn persist_memory_summary(path: &Path, markdown_body: &str) -> Result<(), AgentError> {
|
||||||
@ -126,23 +156,121 @@ mod tests {
|
|||||||
let agent_path = temp.path().join("AGENT.md");
|
let agent_path = temp.path().join("AGENT.md");
|
||||||
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
|
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
|
||||||
|
|
||||||
write_prompt_file(&agent_path, "# Agent\n静态规则").unwrap();
|
// 创建用户自定义内容
|
||||||
write_prompt_file(&memory_path, "## 用户记忆摘要\n- 偏好简洁").unwrap();
|
fs::write(&agent_path, "# Agent\n静态规则\n").unwrap();
|
||||||
|
// 创建记忆摘要
|
||||||
|
fs::write(&memory_path, "## 用户记忆摘要\n- 偏好简洁\n").unwrap();
|
||||||
|
|
||||||
let prompt = load_prompt_from_sources(&[
|
let prompt = load_prompt_from_sources(&[
|
||||||
PromptSource {
|
PromptSource::Builtin(DEFAULT_AGENT_PROMPT),
|
||||||
|
PromptSource::UserCustom {
|
||||||
path: agent_path.clone(),
|
path: agent_path.clone(),
|
||||||
default_content: Some(DEFAULT_AGENT_PROMPT),
|
template: AGENT_MD_TEMPLATE,
|
||||||
},
|
|
||||||
PromptSource {
|
|
||||||
path: memory_path.clone(),
|
|
||||||
default_content: None,
|
|
||||||
},
|
},
|
||||||
|
PromptSource::AutoGenerated(memory_path.clone()),
|
||||||
])
|
])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(prompt, "# Agent\n静态规则\n\n## 用户记忆摘要\n- 偏好简洁");
|
// 验证三部分都包含:系统默认 + 用户自定义 + 记忆摘要
|
||||||
|
assert!(prompt.contains(DEFAULT_AGENT_PROMPT));
|
||||||
|
assert!(prompt.contains("# Agent\n静态规则"));
|
||||||
|
assert!(prompt.contains("## 用户记忆摘要"));
|
||||||
|
// 验证顺序
|
||||||
|
let parts: Vec<_> = prompt.split("\n\n").collect();
|
||||||
|
assert!(parts[0].contains("# PicoBot 代理配置")); // 系统默认开头
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_prompt_from_sources_skips_empty_user_custom() {
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let agent_path = temp.path().join("AGENT.md");
|
||||||
|
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
|
||||||
|
|
||||||
|
// AGENT.md 为空(只有注释和空白)
|
||||||
|
fs::write(&agent_path, "<!-- 注释 -->\n\n \n").unwrap();
|
||||||
|
// 创建记忆摘要
|
||||||
|
fs::write(&memory_path, "## 用户记忆摘要\n").unwrap();
|
||||||
|
|
||||||
|
let prompt = load_prompt_from_sources(&[
|
||||||
|
PromptSource::Builtin(DEFAULT_AGENT_PROMPT),
|
||||||
|
PromptSource::UserCustom {
|
||||||
|
path: agent_path.clone(),
|
||||||
|
template: AGENT_MD_TEMPLATE,
|
||||||
|
},
|
||||||
|
PromptSource::AutoGenerated(memory_path.clone()),
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// 只包含系统默认和记忆摘要,不包含空的 AGENT.md
|
||||||
|
assert!(prompt.contains(DEFAULT_AGENT_PROMPT));
|
||||||
|
assert!(prompt.contains("## 用户记忆摘要"));
|
||||||
|
assert!(!prompt.contains("<!-- 注释 -->"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_prompt_from_sources_creates_template_for_missing_agent_md() {
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let agent_path = temp.path().join("AGENT.md");
|
||||||
|
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
|
||||||
|
|
||||||
|
// AGENT.md 不存在
|
||||||
|
assert!(!agent_path.exists());
|
||||||
|
|
||||||
|
let prompt = load_prompt_from_sources(&[
|
||||||
|
PromptSource::Builtin(DEFAULT_AGENT_PROMPT),
|
||||||
|
PromptSource::UserCustom {
|
||||||
|
path: agent_path.clone(),
|
||||||
|
template: AGENT_MD_TEMPLATE,
|
||||||
|
},
|
||||||
|
PromptSource::AutoGenerated(memory_path.clone()),
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// AGENT.md 应该被创建
|
||||||
|
assert!(agent_path.exists());
|
||||||
|
// AGENT.md 应该包含模板内容(在注释中)
|
||||||
|
let agent_content = fs::read_to_string(&agent_path).unwrap();
|
||||||
|
assert!(agent_content.contains("# 自定义 Agent 配置"));
|
||||||
|
// 因为模板全是注释,去除后为空,所以不会注入到最终提示词
|
||||||
|
assert!(!prompt.contains("自定义 Agent 配置"));
|
||||||
|
// 但系统默认提示词始终存在
|
||||||
|
assert!(prompt.contains(DEFAULT_AGENT_PROMPT));
|
||||||
|
|
||||||
|
// 测试添加实际内容后会注入
|
||||||
|
fs::write(&agent_path, "# 我的自定义配置\n\n我是 Rust 开发者\n").unwrap();
|
||||||
|
let prompt_with_custom = load_prompt_from_sources(&[
|
||||||
|
PromptSource::Builtin(DEFAULT_AGENT_PROMPT),
|
||||||
|
PromptSource::UserCustom {
|
||||||
|
path: agent_path.clone(),
|
||||||
|
template: AGENT_MD_TEMPLATE,
|
||||||
|
},
|
||||||
|
PromptSource::AutoGenerated(memory_path.clone()),
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
// 现在应该包含自定义配置
|
||||||
|
assert!(prompt_with_custom.contains("# 我的自定义配置"));
|
||||||
|
assert!(prompt_with_custom.contains("我是 Rust 开发者"));
|
||||||
|
assert!(prompt_with_custom.contains(DEFAULT_AGENT_PROMPT));
|
||||||
|
|
||||||
|
// 测试清空 AGENT.md 后不会注入
|
||||||
|
fs::write(&agent_path, "<!-- 全部注释 -->\n\n \n").unwrap();
|
||||||
|
let prompt_empty = load_prompt_from_sources(&[
|
||||||
|
PromptSource::Builtin(DEFAULT_AGENT_PROMPT),
|
||||||
|
PromptSource::UserCustom {
|
||||||
|
path: agent_path.clone(),
|
||||||
|
template: AGENT_MD_TEMPLATE,
|
||||||
|
},
|
||||||
|
PromptSource::AutoGenerated(memory_path.clone()),
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
// 清空后(只有注释)不会包含自定义配置
|
||||||
|
assert!(!prompt_empty.contains("我的自定义配置"));
|
||||||
|
assert!(prompt_empty.contains(DEFAULT_AGENT_PROMPT));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -151,63 +279,47 @@ mod tests {
|
|||||||
let agent_path = temp.path().join("AGENT.md");
|
let agent_path = temp.path().join("AGENT.md");
|
||||||
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
|
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
|
||||||
|
|
||||||
write_prompt_file(&agent_path, "# Agent\n静态规则").unwrap();
|
// 创建用户自定义内容
|
||||||
|
fs::write(&agent_path, "# Agent\n静态规则\n").unwrap();
|
||||||
|
// MEMORY_SUMMARY.md 不存在
|
||||||
|
|
||||||
let prompt = load_prompt_from_sources(&[
|
let prompt = load_prompt_from_sources(&[
|
||||||
PromptSource {
|
PromptSource::Builtin(DEFAULT_AGENT_PROMPT),
|
||||||
|
PromptSource::UserCustom {
|
||||||
path: agent_path.clone(),
|
path: agent_path.clone(),
|
||||||
default_content: Some(DEFAULT_AGENT_PROMPT),
|
template: AGENT_MD_TEMPLATE,
|
||||||
},
|
|
||||||
PromptSource {
|
|
||||||
path: memory_path.clone(),
|
|
||||||
default_content: None,
|
|
||||||
},
|
},
|
||||||
|
PromptSource::AutoGenerated(memory_path.clone()),
|
||||||
])
|
])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(prompt, "# Agent\n静态规则");
|
// 包含系统默认和用户自定义,不包含记忆摘要
|
||||||
assert!(!memory_path.exists());
|
assert!(prompt.contains(DEFAULT_AGENT_PROMPT));
|
||||||
|
assert!(prompt.contains("# Agent\n静态规则"));
|
||||||
|
assert!(!prompt.contains("MEMORY_SUMMARY"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_prompt_from_sources_creates_default_agent_prompt() {
|
fn test_strip_comments_and_whitespace_removes_html_comments() {
|
||||||
let temp = tempdir().unwrap();
|
let content = "<!-- 注释 -->\n实际内容\n<!-- 另一个注释 -->";
|
||||||
let agent_path = temp.path().join("AGENT.md");
|
let result = strip_comments_and_whitespace(content);
|
||||||
|
assert!(!result.contains("<!--"));
|
||||||
let prompt = load_prompt_from_sources(&[PromptSource {
|
assert!(result.contains("实际内容"));
|
||||||
path: agent_path.clone(),
|
|
||||||
default_content: Some("# Default Agent\n规则"),
|
|
||||||
}])
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(prompt, "# Default Agent\n规则");
|
|
||||||
assert_eq!(fs::read_to_string(&agent_path).unwrap(), "# Default Agent\n规则\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_prompt_from_sources_returns_none_when_all_sources_empty() {
|
fn test_strip_comments_and_whitespace_removes_empty_lines() {
|
||||||
let temp = tempdir().unwrap();
|
let content = "\n\n实际内容\n\n \n\n另一行\n\n";
|
||||||
let agent_path = temp.path().join("AGENT.md");
|
let result = strip_comments_and_whitespace(content);
|
||||||
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
|
assert_eq!(result, "实际内容\n另一行");
|
||||||
|
}
|
||||||
|
|
||||||
write_prompt_file(&agent_path, " ").unwrap();
|
#[test]
|
||||||
write_prompt_file(&memory_path, "\n\n").unwrap();
|
fn test_strip_comments_and_whitespace_returns_empty_for_only_comments() {
|
||||||
|
let content = "<!-- 注释1 -->\n<!-- 注释2 -->\n\n \n";
|
||||||
let prompt = load_prompt_from_sources(&[
|
let result = strip_comments_and_whitespace(content);
|
||||||
PromptSource {
|
assert!(result.is_empty());
|
||||||
path: agent_path.clone(),
|
|
||||||
default_content: None,
|
|
||||||
},
|
|
||||||
PromptSource {
|
|
||||||
path: memory_path.clone(),
|
|
||||||
default_content: None,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(prompt.is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user