Files
zclaw_openfang/crates/zclaw-growth/src/viking_adapter.rs
iven b7f3d94950
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
fix(presentation): 修复 presentation 模块类型错误和语法问题
- 创建 types.ts 定义完整的类型系统
- 重写 DocumentRenderer.tsx 修复语法错误
- 重写 QuizRenderer.tsx 修复语法错误
- 重写 PresentationContainer.tsx 添加类型守卫
- 重写 TypeSwitcher.tsx 修复类型引用
- 更新 index.ts 移除不存在的 ChartRenderer 导出

审计结果:
- 类型检查: 通过
- 单元测试: 222 passed
- 构建: 成功
2026-03-26 17:19:28 +08:00

363 lines
10 KiB
Rust

//! OpenViking Adapter - Interface to the OpenViking memory system
//!
//! This module provides the `VikingAdapter` which wraps the OpenViking
//! context database for storing and retrieving agent memories.
use crate::types::MemoryEntry;
use async_trait::async_trait;
use serde::{de::DeserializeOwned, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use zclaw_types::Result;
/// Search options for find operations
#[derive(Debug, Clone, Default)]
pub struct FindOptions {
/// Scope to search within (URI prefix)
pub scope: Option<String>,
/// Maximum results to return
pub limit: Option<usize>,
/// Minimum similarity threshold
pub min_similarity: Option<f32>,
}
/// VikingStorage trait - core storage operations (dyn-compatible)
#[async_trait]
pub trait VikingStorage: Send + Sync {
/// Store a memory entry
async fn store(&self, entry: &MemoryEntry) -> Result<()>;
/// Get a memory entry by URI
async fn get(&self, uri: &str) -> Result<Option<MemoryEntry>>;
/// Find memories by query with options
async fn find(&self, query: &str, options: FindOptions) -> Result<Vec<MemoryEntry>>;
/// Find memories by URI prefix
async fn find_by_prefix(&self, prefix: &str) -> Result<Vec<MemoryEntry>>;
/// Delete a memory by URI
async fn delete(&self, uri: &str) -> Result<()>;
/// Store metadata as JSON string
async fn store_metadata_json(&self, key: &str, json: &str) -> Result<()>;
/// Get metadata as JSON string
async fn get_metadata_json(&self, key: &str) -> Result<Option<String>>;
}
/// OpenViking adapter implementation
#[derive(Clone)]
pub struct VikingAdapter {
/// Storage backend
backend: Arc<dyn VikingStorage>,
}
impl VikingAdapter {
/// Create a new Viking adapter with a storage backend
pub fn new(backend: Arc<dyn VikingStorage>) -> Self {
Self { backend }
}
/// Create with in-memory storage (for testing)
pub fn in_memory() -> Self {
Self {
backend: Arc::new(InMemoryStorage::new()),
}
}
/// Store a memory entry
pub async fn store(&self, entry: &MemoryEntry) -> Result<()> {
self.backend.store(entry).await
}
/// Get a memory entry by URI
pub async fn get(&self, uri: &str) -> Result<Option<MemoryEntry>> {
self.backend.get(uri).await
}
/// Find memories by query
pub async fn find(&self, query: &str, options: FindOptions) -> Result<Vec<MemoryEntry>> {
self.backend.find(query, options).await
}
/// Find memories by URI prefix
pub async fn find_by_prefix(&self, prefix: &str) -> Result<Vec<MemoryEntry>> {
self.backend.find_by_prefix(prefix).await
}
/// Delete a memory
pub async fn delete(&self, uri: &str) -> Result<()> {
self.backend.delete(uri).await
}
/// Store metadata (typed)
pub async fn store_metadata<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
let json = serde_json::to_string(value)?;
self.backend.store_metadata_json(key, &json).await
}
/// Get metadata (typed)
pub async fn get_metadata<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
match self.backend.get_metadata_json(key).await? {
Some(json) => {
let value: T = serde_json::from_str(&json)?;
Ok(Some(value))
}
None => Ok(None),
}
}
}
/// In-memory storage backend (for testing and development)
pub struct InMemoryStorage {
memories: std::sync::RwLock<HashMap<String, MemoryEntry>>,
metadata: std::sync::RwLock<HashMap<String, String>>,
}
impl InMemoryStorage {
/// Create a new in-memory storage
pub fn new() -> Self {
Self {
memories: std::sync::RwLock::new(HashMap::new()),
metadata: std::sync::RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryStorage {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl VikingStorage for InMemoryStorage {
async fn store(&self, entry: &MemoryEntry) -> Result<()> {
let mut memories = self.memories.write().unwrap();
memories.insert(entry.uri.clone(), entry.clone());
Ok(())
}
async fn get(&self, uri: &str) -> Result<Option<MemoryEntry>> {
let memories = self.memories.read().unwrap();
Ok(memories.get(uri).cloned())
}
async fn find(&self, query: &str, options: FindOptions) -> Result<Vec<MemoryEntry>> {
let memories = self.memories.read().unwrap();
let mut results: Vec<MemoryEntry> = memories
.values()
.filter(|entry| {
// Apply scope filter
if let Some(ref scope) = options.scope {
if !entry.uri.starts_with(scope) {
return false;
}
}
// Simple text matching (in real implementation, use semantic search)
if !query.is_empty() {
let query_lower = query.to_lowercase();
let content_lower = entry.content.to_lowercase();
let keywords_match = entry.keywords.iter().any(|k| k.to_lowercase().contains(&query_lower));
content_lower.contains(&query_lower) || keywords_match
} else {
true
}
})
.cloned()
.collect();
// Sort by importance and access count
results.sort_by(|a, b| {
b.importance
.cmp(&a.importance)
.then_with(|| b.access_count.cmp(&a.access_count))
});
// Apply limit
if let Some(limit) = options.limit {
results.truncate(limit);
}
Ok(results)
}
async fn find_by_prefix(&self, prefix: &str) -> Result<Vec<MemoryEntry>> {
let memories = self.memories.read().unwrap();
let results: Vec<MemoryEntry> = memories
.values()
.filter(|entry| entry.uri.starts_with(prefix))
.cloned()
.collect();
Ok(results)
}
async fn delete(&self, uri: &str) -> Result<()> {
let mut memories = self.memories.write().unwrap();
memories.remove(uri);
Ok(())
}
async fn store_metadata_json(&self, key: &str, json: &str) -> Result<()> {
let mut metadata = self.metadata.write().unwrap();
metadata.insert(key.to_string(), json.to_string());
Ok(())
}
async fn get_metadata_json(&self, key: &str) -> Result<Option<String>> {
let metadata = self.metadata.read().unwrap();
Ok(metadata.get(key).cloned())
}
}
/// OpenViking levels for storage
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VikingLevel {
/// L0: Raw data (original content)
L0,
/// L1: Summarized content
L1,
/// L2: Keywords and metadata
L2,
}
impl std::fmt::Display for VikingLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VikingLevel::L0 => write!(f, "L0"),
VikingLevel::L1 => write!(f, "L1"),
VikingLevel::L2 => write!(f, "L2"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::MemoryType;
#[tokio::test]
async fn test_in_memory_storage_store_and_get() {
let storage = InMemoryStorage::new();
let entry = MemoryEntry::new(
"test-agent",
MemoryType::Preference,
"style",
"test content".to_string(),
);
storage.store(&entry).await.unwrap();
let retrieved = storage.get(&entry.uri).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().content, "test content");
}
#[tokio::test]
async fn test_in_memory_storage_find() {
let storage = InMemoryStorage::new();
let entry1 = MemoryEntry::new(
"agent-1",
MemoryType::Knowledge,
"rust",
"Rust programming tips".to_string(),
);
let entry2 = MemoryEntry::new(
"agent-1",
MemoryType::Knowledge,
"python",
"Python programming tips".to_string(),
);
storage.store(&entry1).await.unwrap();
storage.store(&entry2).await.unwrap();
let results = storage
.find(
"Rust",
FindOptions {
scope: Some("agent://agent-1".to_string()),
limit: Some(10),
min_similarity: None,
},
)
.await
.unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].content.contains("Rust"));
}
#[tokio::test]
async fn test_in_memory_storage_delete() {
let storage = InMemoryStorage::new();
let entry = MemoryEntry::new(
"test-agent",
MemoryType::Preference,
"style",
"test".to_string(),
);
storage.store(&entry).await.unwrap();
storage.delete(&entry.uri).await.unwrap();
let retrieved = storage.get(&entry.uri).await.unwrap();
assert!(retrieved.is_none());
}
#[tokio::test]
async fn test_metadata_storage() {
let storage = InMemoryStorage::new();
#[derive(Serialize, serde::Deserialize)]
struct TestData {
value: String,
}
let data = TestData {
value: "test".to_string(),
};
storage.store_metadata_json("test-key", &serde_json::to_string(&data).unwrap()).await.unwrap();
let json = storage.get_metadata_json("test-key").await.unwrap();
assert!(json.is_some());
let retrieved: TestData = serde_json::from_str(&json.unwrap()).unwrap();
assert_eq!(retrieved.value, "test");
}
#[tokio::test]
async fn test_viking_adapter_typed_metadata() {
let adapter = VikingAdapter::in_memory();
#[derive(Serialize, serde::Deserialize)]
struct TestData {
value: String,
}
let data = TestData {
value: "test".to_string(),
};
adapter.store_metadata("test-key", &data).await.unwrap();
let retrieved: Option<TestData> = adapter.get_metadata("test-key").await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().value, "test");
}
#[test]
fn test_viking_level_display() {
assert_eq!(format!("{}", VikingLevel::L0), "L0");
assert_eq!(format!("{}", VikingLevel::L1), "L1");
assert_eq!(format!("{}", VikingLevel::L2), "L2");
}
}