Files
zclaw_openfang/crates/zclaw-growth/src/injector.rs
iven 0d4fa96b82
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor: 统一项目名称从OpenFang到ZCLAW
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括:
- 配置文件中的项目名称
- 代码注释和文档引用
- 环境变量和路径
- 类型定义和接口名称
- 测试用例和模拟数据

同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
2026-03-27 07:36:03 +08:00

540 lines
17 KiB
Rust

//! Prompt Injector - Injects retrieved memories into system prompts
//!
//! This module provides the `PromptInjector` which formats and injects
//! retrieved memories into the agent's system prompt for context enhancement.
//!
//! # Formatting Options
//!
//! - `inject()` - Standard markdown format with sections
//! - `inject_compact()` - Compact format for limited token budgets
//! - `inject_json()` - JSON format for structured processing
//! - `inject_custom()` - Custom template with placeholders
use crate::types::{MemoryEntry, RetrievalConfig, RetrievalResult};
/// Output format for memory injection
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InjectionFormat {
/// Standard markdown with sections (default)
Markdown,
/// Compact inline format
Compact,
/// JSON structured format
Json,
}
/// Prompt Injector - injects memories into system prompts
pub struct PromptInjector {
/// Retrieval configuration for token budgets
config: RetrievalConfig,
/// Output format
format: InjectionFormat,
/// Custom template (uses {{preferences}}, {{knowledge}}, {{experience}} placeholders)
custom_template: Option<String>,
}
impl Default for PromptInjector {
fn default() -> Self {
Self::new()
}
}
impl PromptInjector {
/// Create a new prompt injector
pub fn new() -> Self {
Self {
config: RetrievalConfig::default(),
format: InjectionFormat::Markdown,
custom_template: None,
}
}
/// Create with custom configuration
pub fn with_config(config: RetrievalConfig) -> Self {
Self {
config,
format: InjectionFormat::Markdown,
custom_template: None,
}
}
/// Set the output format
pub fn with_format(mut self, format: InjectionFormat) -> Self {
self.format = format;
self
}
/// Set a custom template for injection
///
/// Template placeholders:
/// - `{{preferences}}` - Formatted preferences section
/// - `{{knowledge}}` - Formatted knowledge section
/// - `{{experience}}` - Formatted experience section
/// - `{{all}}` - All memories combined
pub fn with_custom_template(mut self, template: impl Into<String>) -> Self {
self.custom_template = Some(template.into());
self
}
/// Inject memories into a base system prompt
///
/// This method constructs an enhanced system prompt by:
/// 1. Starting with the base prompt
/// 2. Adding a "用户偏好" section if preferences exist
/// 3. Adding a "相关知识" section if knowledge exists
/// 4. Adding an "经验参考" section if experience exists
///
/// Each section respects the token budget configuration.
pub fn inject(&self, base_prompt: &str, memories: &RetrievalResult) -> String {
// If no memories, return base prompt unchanged
if memories.is_empty() {
return base_prompt.to_string();
}
let mut result = base_prompt.to_string();
// Inject preferences section
if !memories.preferences.is_empty() {
let section = self.format_section(
"## 用户偏好",
&memories.preferences,
self.config.preference_budget,
|entry| format!("- {}", entry.content),
);
result.push_str("\n\n");
result.push_str(&section);
}
// Inject knowledge section
if !memories.knowledge.is_empty() {
let section = self.format_section(
"## 相关知识",
&memories.knowledge,
self.config.knowledge_budget,
|entry| format!("- {}", entry.content),
);
result.push_str("\n\n");
result.push_str(&section);
}
// Inject experience section
if !memories.experience.is_empty() {
let section = self.format_section(
"## 经验参考",
&memories.experience,
self.config.experience_budget,
|entry| format!("- {}", entry.content),
);
result.push_str("\n\n");
result.push_str(&section);
}
// Add memory context footer
result.push_str("\n\n");
result.push_str("<!-- 以上内容基于历史对话自动提取的记忆 -->");
result
}
/// Format a section of memories with token budget
fn format_section<F>(
&self,
header: &str,
entries: &[MemoryEntry],
token_budget: usize,
formatter: F,
) -> String
where
F: Fn(&MemoryEntry) -> String,
{
let mut result = String::new();
result.push_str(header);
result.push('\n');
let mut used_tokens = 0;
let header_tokens = header.len() / 4;
used_tokens += header_tokens;
for entry in entries {
let line = formatter(entry);
let line_tokens = line.len() / 4;
if used_tokens + line_tokens > token_budget {
// Add truncation indicator
result.push_str("- ... (更多内容已省略)\n");
break;
}
result.push_str(&line);
result.push('\n');
used_tokens += line_tokens;
}
result
}
/// Build a minimal context string for token-limited scenarios
pub fn build_minimal_context(&self, memories: &RetrievalResult) -> String {
if memories.is_empty() {
return String::new();
}
let mut context = String::new();
// Only include top preference
if let Some(pref) = memories.preferences.first() {
context.push_str(&format!("[偏好] {}\n", pref.content));
}
// Only include top knowledge
if let Some(knowledge) = memories.knowledge.first() {
context.push_str(&format!("[知识] {}\n", knowledge.content));
}
context
}
/// Inject memories in compact format
///
/// Compact format uses inline notation: [P] for preferences, [K] for knowledge, [E] for experience
pub fn inject_compact(&self, base_prompt: &str, memories: &RetrievalResult) -> String {
if memories.is_empty() {
return base_prompt.to_string();
}
let mut result = base_prompt.to_string();
let mut context_parts = Vec::new();
// Add compact preferences
for entry in &memories.preferences {
context_parts.push(format!("[P] {}", entry.content));
}
// Add compact knowledge
for entry in &memories.knowledge {
context_parts.push(format!("[K] {}", entry.content));
}
// Add compact experience
for entry in &memories.experience {
context_parts.push(format!("[E] {}", entry.content));
}
if !context_parts.is_empty() {
result.push_str("\n\n[记忆上下文]\n");
result.push_str(&context_parts.join("\n"));
}
result
}
/// Inject memories as JSON structure
///
/// Returns a JSON object with preferences, knowledge, and experience arrays
pub fn inject_json(&self, base_prompt: &str, memories: &RetrievalResult) -> String {
if memories.is_empty() {
return base_prompt.to_string();
}
let preferences: Vec<_> = memories.preferences.iter()
.map(|e| serde_json::json!({
"content": e.content,
"importance": e.importance,
"keywords": e.keywords,
}))
.collect();
let knowledge: Vec<_> = memories.knowledge.iter()
.map(|e| serde_json::json!({
"content": e.content,
"importance": e.importance,
"keywords": e.keywords,
}))
.collect();
let experience: Vec<_> = memories.experience.iter()
.map(|e| serde_json::json!({
"content": e.content,
"importance": e.importance,
"keywords": e.keywords,
}))
.collect();
let memories_json = serde_json::json!({
"preferences": preferences,
"knowledge": knowledge,
"experience": experience,
});
format!("{}\n\n[记忆上下文]\n{}", base_prompt, serde_json::to_string_pretty(&memories_json).unwrap_or_default())
}
/// Inject using custom template
///
/// Template placeholders:
/// - `{{preferences}}` - Formatted preferences section
/// - `{{knowledge}}` - Formatted knowledge section
/// - `{{experience}}` - Formatted experience section
/// - `{{all}}` - All memories combined
pub fn inject_custom(&self, template: &str, memories: &RetrievalResult) -> String {
let mut result = template.to_string();
// Format each section
let prefs = if !memories.preferences.is_empty() {
memories.preferences.iter()
.map(|e| format!("- {}", e.content))
.collect::<Vec<_>>()
.join("\n")
} else {
String::new()
};
let knowledge = if !memories.knowledge.is_empty() {
memories.knowledge.iter()
.map(|e| format!("- {}", e.content))
.collect::<Vec<_>>()
.join("\n")
} else {
String::new()
};
let experience = if !memories.experience.is_empty() {
memories.experience.iter()
.map(|e| format!("- {}", e.content))
.collect::<Vec<_>>()
.join("\n")
} else {
String::new()
};
// Combine all
let all = format!(
"用户偏好:\n{}\n\n相关知识:\n{}\n\n经验参考:\n{}",
if prefs.is_empty() { "" } else { &prefs },
if knowledge.is_empty() { "" } else { &knowledge },
if experience.is_empty() { "" } else { &experience },
);
// Replace placeholders
result = result.replace("{{preferences}}", &prefs);
result = result.replace("{{knowledge}}", &knowledge);
result = result.replace("{{experience}}", &experience);
result = result.replace("{{all}}", &all);
result
}
/// Inject memories using the configured format
pub fn inject_with_format(&self, base_prompt: &str, memories: &RetrievalResult) -> String {
match self.format {
InjectionFormat::Markdown => self.inject(base_prompt, memories),
InjectionFormat::Compact => self.inject_compact(base_prompt, memories),
InjectionFormat::Json => self.inject_json(base_prompt, memories),
}
}
/// Estimate total tokens that will be injected
pub fn estimate_injection_tokens(&self, memories: &RetrievalResult) -> usize {
let mut total = 0;
// Count preference tokens
for entry in &memories.preferences {
total += entry.estimated_tokens();
if total > self.config.preference_budget {
total = self.config.preference_budget;
break;
}
}
// Count knowledge tokens
let mut knowledge_tokens = 0;
for entry in &memories.knowledge {
knowledge_tokens += entry.estimated_tokens();
if knowledge_tokens > self.config.knowledge_budget {
knowledge_tokens = self.config.knowledge_budget;
break;
}
}
total += knowledge_tokens;
// Count experience tokens
let mut experience_tokens = 0;
for entry in &memories.experience {
experience_tokens += entry.estimated_tokens();
if experience_tokens > self.config.experience_budget {
experience_tokens = self.config.experience_budget;
break;
}
}
total += experience_tokens;
total
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::MemoryType;
use chrono::Utc;
fn create_test_entry(content: &str) -> MemoryEntry {
MemoryEntry {
uri: "test://uri".to_string(),
memory_type: MemoryType::Preference,
content: content.to_string(),
keywords: vec![],
importance: 5,
access_count: 0,
created_at: Utc::now(),
last_accessed: Utc::now(),
overview: None,
abstract_summary: None,
}
}
#[test]
fn test_injector_empty_memories() {
let injector = PromptInjector::new();
let base = "You are a helpful assistant.";
let memories = RetrievalResult::default();
let result = injector.inject(base, &memories);
assert_eq!(result, base);
}
#[test]
fn test_injector_with_preferences() {
let injector = PromptInjector::new();
let base = "You are a helpful assistant.";
let memories = RetrievalResult {
preferences: vec![create_test_entry("User prefers concise responses")],
knowledge: vec![],
experience: vec![],
total_tokens: 0,
};
let result = injector.inject(base, &memories);
assert!(result.contains("用户偏好"));
assert!(result.contains("User prefers concise responses"));
}
#[test]
fn test_injector_with_all_types() {
let injector = PromptInjector::new();
let base = "You are a helpful assistant.";
let memories = RetrievalResult {
preferences: vec![create_test_entry("Prefers concise")],
knowledge: vec![create_test_entry("Knows Rust")],
experience: vec![create_test_entry("Browser skill works well")],
total_tokens: 0,
};
let result = injector.inject(base, &memories);
assert!(result.contains("用户偏好"));
assert!(result.contains("相关知识"));
assert!(result.contains("经验参考"));
}
#[test]
fn test_minimal_context() {
let injector = PromptInjector::new();
let memories = RetrievalResult {
preferences: vec![create_test_entry("Prefers concise")],
knowledge: vec![create_test_entry("Knows Rust")],
experience: vec![],
total_tokens: 0,
};
let context = injector.build_minimal_context(&memories);
assert!(context.contains("[偏好]"));
assert!(context.contains("[知识]"));
}
#[test]
fn test_estimate_tokens() {
let injector = PromptInjector::new();
let memories = RetrievalResult {
preferences: vec![create_test_entry("Short text")],
knowledge: vec![],
experience: vec![],
total_tokens: 0,
};
let estimate = injector.estimate_injection_tokens(&memories);
assert!(estimate > 0);
}
#[test]
fn test_inject_compact() {
let injector = PromptInjector::new();
let base = "You are a helpful assistant.";
let memories = RetrievalResult {
preferences: vec![create_test_entry("Prefers concise")],
knowledge: vec![create_test_entry("Knows Rust")],
experience: vec![],
total_tokens: 0,
};
let result = injector.inject_compact(base, &memories);
assert!(result.contains("[P]"));
assert!(result.contains("[K]"));
assert!(result.contains("[记忆上下文]"));
}
#[test]
fn test_inject_json() {
let injector = PromptInjector::new();
let base = "You are a helpful assistant.";
let memories = RetrievalResult {
preferences: vec![create_test_entry("Prefers concise")],
knowledge: vec![],
experience: vec![],
total_tokens: 0,
};
let result = injector.inject_json(base, &memories);
assert!(result.contains("\"preferences\""));
assert!(result.contains("Prefers concise"));
}
#[test]
fn test_inject_custom() {
let injector = PromptInjector::new();
let template = "Context:\n{{all}}";
let memories = RetrievalResult {
preferences: vec![create_test_entry("Prefers concise")],
knowledge: vec![create_test_entry("Knows Rust")],
experience: vec![],
total_tokens: 0,
};
let result = injector.inject_custom(template, &memories);
assert!(result.contains("用户偏好"));
assert!(result.contains("相关知识"));
}
#[test]
fn test_format_selection() {
let base = "Base";
let memories = RetrievalResult {
preferences: vec![create_test_entry("Test")],
knowledge: vec![],
experience: vec![],
total_tokens: 0,
};
// Test markdown format
let injector_md = PromptInjector::new().with_format(InjectionFormat::Markdown);
let result_md = injector_md.inject_with_format(base, &memories);
assert!(result_md.contains("## 用户偏好"));
// Test compact format
let injector_compact = PromptInjector::new().with_format(InjectionFormat::Compact);
let result_compact = injector_compact.inject_with_format(base, &memories);
assert!(result_compact.contains("[P]"));
}
}