fix(presentation): 修复 presentation 模块类型错误和语法问题
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
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
- 创建 types.ts 定义完整的类型系统 - 重写 DocumentRenderer.tsx 修复语法错误 - 重写 QuizRenderer.tsx 修复语法错误 - 重写 PresentationContainer.tsx 添加类型守卫 - 重写 TypeSwitcher.tsx 修复类型引用 - 更新 index.ts 移除不存在的 ChartRenderer 导出 审计结果: - 类型检查: 通过 - 单元测试: 222 passed - 构建: 成功
This commit is contained in:
537
crates/zclaw-growth/src/injector.rs
Normal file
537
crates/zclaw-growth/src/injector.rs
Normal file
@@ -0,0 +1,537 @@
|
||||
//! 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(§ion);
|
||||
}
|
||||
|
||||
// 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(§ion);
|
||||
}
|
||||
|
||||
// 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(§ion);
|
||||
}
|
||||
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
#[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]"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user