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
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括: - 配置文件中的项目名称 - 代码注释和文档引用 - 环境变量和路径 - 类型定义和接口名称 - 测试用例和模拟数据 同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
193 lines
6.3 KiB
Rust
193 lines
6.3 KiB
Rust
//! Memory Summarizer - L0/L1 Summary Generation
|
|
//!
|
|
//! Provides trait and functions for generating layered summaries of memory entries:
|
|
//! - L1 Overview: 1-2 sentence summary (~200 tokens)
|
|
//! - L0 Abstract: 3-5 keywords (~100 tokens)
|
|
//!
|
|
//! The trait-based design allows zclaw-growth to remain decoupled from any
|
|
//! specific LLM implementation. The Tauri layer provides a concrete implementation.
|
|
|
|
use crate::types::MemoryEntry;
|
|
|
|
/// LLM driver for summary generation.
|
|
/// Implementations call an LLM API to produce concise summaries.
|
|
#[async_trait::async_trait]
|
|
pub trait SummaryLlmDriver: Send + Sync {
|
|
/// Generate a short summary (1-2 sentences, ~200 tokens) for a memory entry.
|
|
async fn generate_overview(&self, entry: &MemoryEntry) -> Result<String, String>;
|
|
|
|
/// Generate keyword extraction (3-5 keywords, ~100 tokens) for a memory entry.
|
|
async fn generate_abstract(&self, entry: &MemoryEntry) -> Result<String, String>;
|
|
}
|
|
|
|
/// Generate an L1 overview prompt for the LLM.
|
|
pub fn overview_prompt(entry: &MemoryEntry) -> String {
|
|
format!(
|
|
r#"Summarize the following memory entry in 1-2 concise sentences (in the same language as the content).
|
|
Focus on the key information. Do not add any preamble or explanation.
|
|
|
|
Memory type: {}
|
|
Category: {}
|
|
Content: {}"#,
|
|
entry.memory_type,
|
|
entry.uri.rsplit('/').next().unwrap_or("unknown"),
|
|
entry.content
|
|
)
|
|
}
|
|
|
|
/// Generate an L0 abstract prompt for the LLM.
|
|
pub fn abstract_prompt(entry: &MemoryEntry) -> String {
|
|
format!(
|
|
r#"Extract 3-5 keywords or key phrases from the following memory entry.
|
|
Output ONLY the keywords, comma-separated, in the same language as the content.
|
|
Do not add any preamble, explanation, or numbering.
|
|
|
|
Memory type: {}
|
|
Content: {}"#,
|
|
entry.memory_type, entry.content
|
|
)
|
|
}
|
|
|
|
/// Generate both L1 overview and L0 abstract for a memory entry.
|
|
/// Returns (overview, abstract_summary) tuple.
|
|
pub async fn generate_summaries(
|
|
driver: &dyn SummaryLlmDriver,
|
|
entry: &MemoryEntry,
|
|
) -> (Option<String>, Option<String>) {
|
|
// Generate L1 overview
|
|
let overview = match driver.generate_overview(entry).await {
|
|
Ok(text) => {
|
|
let cleaned = clean_summary(&text);
|
|
if !cleaned.is_empty() {
|
|
Some(cleaned)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::debug!("[Summarizer] Failed to generate overview for {}: {}", entry.uri, e);
|
|
None
|
|
}
|
|
};
|
|
|
|
// Generate L0 abstract
|
|
let abstract_summary = match driver.generate_abstract(entry).await {
|
|
Ok(text) => {
|
|
let cleaned = clean_summary(&text);
|
|
if !cleaned.is_empty() {
|
|
Some(cleaned)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::debug!("[Summarizer] Failed to generate abstract for {}: {}", entry.uri, e);
|
|
None
|
|
}
|
|
};
|
|
|
|
(overview, abstract_summary)
|
|
}
|
|
|
|
/// Clean LLM response: strip quotes, whitespace, prefixes
|
|
fn clean_summary(text: &str) -> String {
|
|
text.trim()
|
|
.trim_start_matches('"')
|
|
.trim_end_matches('"')
|
|
.trim_start_matches('\'')
|
|
.trim_end_matches('\'')
|
|
.trim_start_matches("摘要:")
|
|
.trim_start_matches("摘要:")
|
|
.trim_start_matches("关键词:")
|
|
.trim_start_matches("关键词:")
|
|
.trim_start_matches("Overview:")
|
|
.trim_start_matches("overview:")
|
|
.trim()
|
|
.to_string()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::types::MemoryType;
|
|
|
|
struct MockSummaryDriver;
|
|
|
|
#[async_trait::async_trait]
|
|
impl SummaryLlmDriver for MockSummaryDriver {
|
|
async fn generate_overview(&self, entry: &MemoryEntry) -> Result<String, String> {
|
|
Ok(format!("Summary of: {}", &entry.content[..entry.content.len().min(30)]))
|
|
}
|
|
|
|
async fn generate_abstract(&self, _entry: &MemoryEntry) -> Result<String, String> {
|
|
Ok("keyword1, keyword2, keyword3".to_string())
|
|
}
|
|
}
|
|
|
|
fn make_entry(content: &str) -> MemoryEntry {
|
|
MemoryEntry::new("test-agent", MemoryType::Knowledge, "test", content.to_string())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_generate_summaries() {
|
|
let driver = MockSummaryDriver;
|
|
let entry = make_entry("This is a test memory entry about Rust programming.");
|
|
|
|
let (overview, abstract_summary) = generate_summaries(&driver, &entry).await;
|
|
|
|
assert!(overview.is_some());
|
|
assert!(abstract_summary.is_some());
|
|
assert!(overview.unwrap().contains("Summary of"));
|
|
assert!(abstract_summary.unwrap().contains("keyword1"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_generate_summaries_handles_error() {
|
|
struct FailingDriver;
|
|
#[async_trait::async_trait]
|
|
impl SummaryLlmDriver for FailingDriver {
|
|
async fn generate_overview(&self, _entry: &MemoryEntry) -> Result<String, String> {
|
|
Err("LLM unavailable".to_string())
|
|
}
|
|
async fn generate_abstract(&self, _entry: &MemoryEntry) -> Result<String, String> {
|
|
Err("LLM unavailable".to_string())
|
|
}
|
|
}
|
|
|
|
let driver = FailingDriver;
|
|
let entry = make_entry("test content");
|
|
|
|
let (overview, abstract_summary) = generate_summaries(&driver, &entry).await;
|
|
|
|
assert!(overview.is_none());
|
|
assert!(abstract_summary.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_clean_summary() {
|
|
assert_eq!(clean_summary("\"hello world\""), "hello world");
|
|
assert_eq!(clean_summary("摘要:你好"), "你好");
|
|
assert_eq!(clean_summary(" keyword1, keyword2 "), "keyword1, keyword2");
|
|
assert_eq!(clean_summary("Overview: something"), "something");
|
|
}
|
|
|
|
#[test]
|
|
fn test_overview_prompt() {
|
|
let entry = make_entry("User prefers dark mode and compact UI");
|
|
let prompt = overview_prompt(&entry);
|
|
|
|
assert!(prompt.contains("1-2 concise sentences"));
|
|
assert!(prompt.contains("User prefers dark mode"));
|
|
assert!(prompt.contains("knowledge"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_abstract_prompt() {
|
|
let entry = make_entry("Rust is a systems programming language");
|
|
let prompt = abstract_prompt(&entry);
|
|
|
|
assert!(prompt.contains("3-5 keywords"));
|
|
assert!(prompt.contains("Rust is a systems"));
|
|
}
|
|
}
|