refactor: 统一项目名称从OpenFang到ZCLAW
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
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括: - 配置文件中的项目名称 - 代码注释和文档引用 - 环境变量和路径 - 类型定义和接口名称 - 测试用例和模拟数据 同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
This commit is contained in:
@@ -15,5 +15,6 @@ pub mod crypto;
|
||||
// Re-export main types for convenience
|
||||
pub use persistent::{
|
||||
PersistentMemory, PersistentMemoryStore, MemorySearchQuery, MemoryStats,
|
||||
generate_memory_id,
|
||||
generate_memory_id, configure_embedding_client, is_embedding_configured,
|
||||
EmbedFn,
|
||||
};
|
||||
|
||||
@@ -11,12 +11,69 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, OnceCell};
|
||||
use uuid::Uuid;
|
||||
use tauri::Manager;
|
||||
use sqlx::{SqliteConnection, Connection, Row, sqlite::SqliteRow};
|
||||
use chrono::Utc;
|
||||
|
||||
/// Embedding function type: text -> vector of f32
|
||||
pub type EmbedFn = Arc<dyn Fn(&str) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<f32>, String>> + Send>> + Send + Sync>;
|
||||
|
||||
/// Global embedding function for PersistentMemoryStore
|
||||
static EMBEDDING_FN: OnceCell<EmbedFn> = OnceCell::const_new();
|
||||
|
||||
/// Configure the global embedding function for memory search
|
||||
pub fn configure_embedding_client(fn_impl: EmbedFn) {
|
||||
let _ = EMBEDDING_FN.set(fn_impl);
|
||||
tracing::info!("[PersistentMemoryStore] Embedding client configured");
|
||||
}
|
||||
|
||||
/// Check if embedding is available
|
||||
pub fn is_embedding_configured() -> bool {
|
||||
EMBEDDING_FN.get().is_some()
|
||||
}
|
||||
|
||||
/// Generate embedding for text using the configured client
|
||||
async fn embed_text(text: &str) -> Result<Vec<f32>, String> {
|
||||
let client = EMBEDDING_FN.get()
|
||||
.ok_or_else(|| "Embedding client not configured".to_string())?;
|
||||
client(text).await
|
||||
}
|
||||
|
||||
/// Deserialize f32 vector from BLOB (4 bytes per f32, little-endian)
|
||||
fn deserialize_embedding(blob: &[u8]) -> Vec<f32> {
|
||||
blob.chunks_exact(4)
|
||||
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Serialize f32 vector to BLOB
|
||||
fn serialize_embedding(vec: &[f32]) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(vec.len() * 4);
|
||||
for val in vec {
|
||||
bytes.extend_from_slice(&val.to_le_bytes());
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Compute cosine similarity between two vectors
|
||||
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
|
||||
if a.is_empty() || b.is_empty() || a.len() != b.len() {
|
||||
return 0.0;
|
||||
}
|
||||
let mut dot = 0.0f32;
|
||||
let mut norm_a = 0.0f32;
|
||||
let mut norm_b = 0.0f32;
|
||||
for i in 0..a.len() {
|
||||
dot += a[i] * b[i];
|
||||
norm_a += a[i] * a[i];
|
||||
norm_b += b[i] * b[i];
|
||||
}
|
||||
let denom = (norm_a * norm_b).sqrt();
|
||||
if denom == 0.0 { 0.0 } else { (dot / denom).clamp(0.0, 1.0) }
|
||||
}
|
||||
|
||||
/// Memory entry stored in SQLite
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PersistentMemory {
|
||||
@@ -32,6 +89,7 @@ pub struct PersistentMemory {
|
||||
pub last_accessed_at: String,
|
||||
pub access_count: i32,
|
||||
pub embedding: Option<Vec<u8>>, // Vector embedding for semantic search
|
||||
pub overview: Option<String>, // L1 summary (1-2 sentences, ~200 tokens)
|
||||
}
|
||||
|
||||
// Manual implementation of FromRow since sqlx::FromRow derive has issues with Option<Vec<u8>>
|
||||
@@ -50,12 +108,13 @@ impl<'r> sqlx::FromRow<'r, SqliteRow> for PersistentMemory {
|
||||
last_accessed_at: row.try_get("last_accessed_at")?,
|
||||
access_count: row.try_get("access_count")?,
|
||||
embedding: row.try_get("embedding")?,
|
||||
overview: row.try_get("overview").ok(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory search options
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MemorySearchQuery {
|
||||
pub agent_id: Option<String>,
|
||||
pub memory_type: Option<String>,
|
||||
@@ -149,11 +208,34 @@ impl PersistentMemoryStore {
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create schema: {}", e))?;
|
||||
|
||||
// Migration: add overview column (L1 summary)
|
||||
let _ = sqlx::query("ALTER TABLE memories ADD COLUMN overview TEXT")
|
||||
.execute(&mut *conn)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store a new memory
|
||||
pub async fn store(&self, memory: &PersistentMemory) -> Result<(), String> {
|
||||
// Generate embedding if client is configured and memory doesn't have one
|
||||
let embedding = if memory.embedding.is_some() {
|
||||
memory.embedding.clone()
|
||||
} else if is_embedding_configured() {
|
||||
match embed_text(&memory.content).await {
|
||||
Ok(vec) => {
|
||||
tracing::debug!("[PersistentMemoryStore] Generated embedding for {} ({} dims)", memory.id, vec.len());
|
||||
Some(serialize_embedding(&vec))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!("[PersistentMemoryStore] Embedding generation failed: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut conn = self.conn.lock().await;
|
||||
|
||||
sqlx::query(
|
||||
@@ -161,8 +243,8 @@ impl PersistentMemoryStore {
|
||||
INSERT INTO memories (
|
||||
id, agent_id, memory_type, content, importance, source,
|
||||
tags, conversation_id, created_at, last_accessed_at,
|
||||
access_count, embedding
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
access_count, embedding, overview
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&memory.id)
|
||||
@@ -176,7 +258,8 @@ impl PersistentMemoryStore {
|
||||
.bind(&memory.created_at)
|
||||
.bind(&memory.last_accessed_at)
|
||||
.bind(memory.access_count)
|
||||
.bind(&memory.embedding)
|
||||
.bind(&embedding)
|
||||
.bind(&memory.overview)
|
||||
.execute(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to store memory: {}", e))?;
|
||||
@@ -212,7 +295,7 @@ impl PersistentMemoryStore {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Search memories with simple query
|
||||
/// Search memories with semantic ranking when embeddings are available
|
||||
pub async fn search(&self, query: MemorySearchQuery) -> Result<Vec<PersistentMemory>, String> {
|
||||
let mut conn = self.conn.lock().await;
|
||||
|
||||
@@ -239,11 +322,14 @@ impl PersistentMemoryStore {
|
||||
params.push(format!("%{}%", query_text));
|
||||
}
|
||||
|
||||
sql.push_str(" ORDER BY created_at DESC");
|
||||
// When using embedding ranking, fetch more candidates
|
||||
let effective_limit = if query.query.is_some() && is_embedding_configured() {
|
||||
query.limit.unwrap_or(50).max(20) // Fetch more for re-ranking
|
||||
} else {
|
||||
query.limit.unwrap_or(50)
|
||||
};
|
||||
|
||||
if let Some(limit) = query.limit {
|
||||
sql.push_str(&format!(" LIMIT {}", limit));
|
||||
}
|
||||
sql.push_str(&format!(" LIMIT {}", effective_limit));
|
||||
|
||||
if let Some(offset) = query.offset {
|
||||
sql.push_str(&format!(" OFFSET {}", offset));
|
||||
@@ -255,11 +341,41 @@ impl PersistentMemoryStore {
|
||||
query_builder = query_builder.bind(param);
|
||||
}
|
||||
|
||||
let results = query_builder
|
||||
let mut results = query_builder
|
||||
.fetch_all(&mut *conn)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to search memories: {}", e))?;
|
||||
|
||||
// Apply semantic ranking if query and embedding are available
|
||||
if let Some(query_text) = &query.query {
|
||||
if is_embedding_configured() {
|
||||
if let Ok(query_embedding) = embed_text(query_text).await {
|
||||
// Score each result by cosine similarity
|
||||
let mut scored: Vec<(f32, PersistentMemory)> = results
|
||||
.into_iter()
|
||||
.map(|mem| {
|
||||
let score = mem.embedding.as_ref()
|
||||
.map(|blob| {
|
||||
let vec = deserialize_embedding(blob);
|
||||
cosine_similarity(&query_embedding, &vec)
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
(score, mem)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by score descending
|
||||
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
// Apply the original limit
|
||||
results = scored.into_iter()
|
||||
.take(query.limit.unwrap_or(20))
|
||||
.map(|(_, mem)| mem)
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user