Initial scaffold: GyanDoot v2.0 in Rust
Full Cargo workspace with 11 crates: - gyandoot-core: Shared types, config, error types - gyandoot-agent: Agent loop, tool execution, context compression - gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama) - gyandoot-memory: MemPalace persistent memory system - gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI) - gyandoot-security: Credential vault, sanitizer, threat detector - gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics - gyandoot-subagents: Sub-agent spawning and orchestration - gyandoot-skills: Skills discovery, loading, preprocessing, bundles - gyandoot-profile: Multi-agent profile manager - gyandoot-gateway: Gateway sessions, routing, access control Plus: CLI entry point with clap, cross-platform installer script, KANBAN.md, .gnpl/AGENTS.md Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
227
crates/gyandoot-memory/src/palace.rs
Normal file
227
crates/gyandoot-memory/src/palace.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
//! MemPalace — The core persistent memory store.
|
||||
//!
|
||||
//! Stores conversation entries with embeddings for semantic retrieval,
|
||||
//! handles memory compaction, and provides query capabilities.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use gyandoot_core::MemoryResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::identity::IdentityStore;
|
||||
use crate::transcript::TranscriptStore;
|
||||
|
||||
/// A single memory entry stored in the palace.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MemoryEntry {
|
||||
pub id: String,
|
||||
pub content: String,
|
||||
pub agent_id: String,
|
||||
pub session_id: String,
|
||||
pub entry_type: EntryType,
|
||||
pub embedding: Option<Vec<f64>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub metadata: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// The type of a memory entry.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum EntryType {
|
||||
Conversation,
|
||||
Fact,
|
||||
Identity,
|
||||
Task,
|
||||
Skill,
|
||||
}
|
||||
|
||||
/// A memory query result with relevance score.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryQueryResult {
|
||||
pub content: String,
|
||||
pub relevance: f64,
|
||||
pub entry_type: EntryType,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Status information for the memory palace.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStatus {
|
||||
pub entry_count: usize,
|
||||
pub backend: String,
|
||||
pub storage_size: u64,
|
||||
}
|
||||
|
||||
/// The MemPalace — persistent, searchable agent memory.
|
||||
///
|
||||
/// Entries are stored as JSON files in the data directory, organized by
|
||||
/// agent and session. Future versions will include vector embeddings
|
||||
/// for semantic search.
|
||||
pub struct MemPalace {
|
||||
entries: Vec<MemoryEntry>,
|
||||
storage_path: PathBuf,
|
||||
transcript_store: TranscriptStore,
|
||||
identity_store: IdentityStore,
|
||||
}
|
||||
|
||||
impl MemPalace {
|
||||
/// Create a new MemPalace with memory directory in the user's data dir.
|
||||
pub fn new() -> Self {
|
||||
let data_dir = gyandoot_core::data_dir().join("memory");
|
||||
std::fs::create_dir_all(&data_dir).ok();
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
storage_path: data_dir,
|
||||
transcript_store: TranscriptStore::new(),
|
||||
identity_store: IdentityStore::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a MemPalace with a custom storage path.
|
||||
pub fn with_path(path: PathBuf) -> Self {
|
||||
std::fs::create_dir_all(&path).ok();
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
storage_path: path,
|
||||
transcript_store: TranscriptStore::new(),
|
||||
identity_store: IdentityStore::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a memory entry in the palace.
|
||||
pub fn store(&mut self, entry: MemoryEntry) -> MemoryResult<()> {
|
||||
debug!("Storing memory entry: {}", entry.id);
|
||||
self.entries.push(entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store a conversation turn (the last user + assistant pair) as memory.
|
||||
pub async fn store_conversation_turn(
|
||||
&mut self,
|
||||
messages: &[gyandoot_core::Message],
|
||||
) -> MemoryResult<()> {
|
||||
let recent: Vec<&gyandoot_core::Message> = messages.iter().rev().take(4).collect();
|
||||
for msg in recent.iter().rev() {
|
||||
let entry = MemoryEntry {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
content: msg.content.clone(),
|
||||
agent_id: "default".to_string(),
|
||||
session_id: String::new(),
|
||||
entry_type: EntryType::Conversation,
|
||||
embedding: None,
|
||||
created_at: Utc::now(),
|
||||
metadata: std::collections::HashMap::new(),
|
||||
};
|
||||
self.store(entry)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Query memory by simple text matching.
|
||||
///
|
||||
/// Future versions will use embedding-based semantic search.
|
||||
pub fn query(&self, query: &str, limit: usize) -> MemoryResult<Vec<MemoryQueryResult>> {
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results: Vec<MemoryQueryResult> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| e.content.to_lowercase().contains(&query_lower))
|
||||
.map(|e| MemoryQueryResult {
|
||||
content: e.content.clone(),
|
||||
relevance: 1.0, // placeholder; real relevance from embedding cosine sim
|
||||
entry_type: e.entry_type.clone(),
|
||||
created_at: e.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
results.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap_or(std::cmp::Ordering::Equal));
|
||||
results.truncate(limit);
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Export all memory to a JSON file at the given path.
|
||||
pub fn export_to(&self, path: &PathBuf) -> MemoryResult<()> {
|
||||
let json = serde_json::to_string_pretty(&self.entries)
|
||||
.map_err(|e| gyandoot_core::MemoryError::Storage(e.to_string()))?;
|
||||
std::fs::write(path, &json)
|
||||
.map_err(|e| gyandoot_core::MemoryError::Storage(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get memory status information.
|
||||
pub fn status() -> MemoryResult<MemoryStatus> {
|
||||
let data_dir = gyandoot_core::data_dir().join("memory");
|
||||
let size = std::fs::read_dir(&data_dir)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.metadata().ok())
|
||||
.map(|m| m.len())
|
||||
.sum()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let entry_count = std::fs::read_dir(&data_dir)
|
||||
.map(|entries| entries.filter_map(|e| e.ok()).count())
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(MemoryStatus {
|
||||
entry_count,
|
||||
backend: "local-json".to_string(),
|
||||
storage_size: size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a reference to the transcript store.
|
||||
pub fn transcript_store(&self) -> &TranscriptStore {
|
||||
&self.transcript_store
|
||||
}
|
||||
|
||||
/// Get a reference to the identity store.
|
||||
pub fn identity_store(&self) -> &IdentityStore {
|
||||
&self.identity_store
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemPalace {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_store_and_query() {
|
||||
let mut palace = MemPalace::new();
|
||||
|
||||
palace
|
||||
.store(MemoryEntry {
|
||||
id: "1".to_string(),
|
||||
content: "The user likes Python".to_string(),
|
||||
agent_id: "default".to_string(),
|
||||
session_id: "s1".to_string(),
|
||||
entry_type: EntryType::Fact,
|
||||
embedding: None,
|
||||
created_at: Utc::now(),
|
||||
metadata: std::collections::HashMap::new(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let results = palace.query("python", 10).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].content.contains("Python"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export() {
|
||||
let palace = MemPalace::new();
|
||||
let tmp = std::env::temp_dir().join("gyandoot_test_export.json");
|
||||
palace.export_to(&tmp).unwrap();
|
||||
assert!(tmp.exists());
|
||||
std::fs::remove_file(tmp).ok();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user