memrec-common 0.2.0

Local-first AI memory with project isolation — for terminal, for private use
Documentation
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use uuid::Uuid;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum MemoryType {
    #[default]
    Conversation,
    Knowledge,
    Decision,
    Preference,
    Context,
}


impl std::fmt::Display for MemoryType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MemoryType::Conversation => write!(f, "conversation"),
            MemoryType::Knowledge => write!(f, "knowledge"),
            MemoryType::Decision => write!(f, "decision"),
            MemoryType::Preference => write!(f, "preference"),
            MemoryType::Context => write!(f, "context"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
    pub id: Uuid,
    pub memory_type: MemoryType,
    pub content: String,
    pub summary: Option<String>,
    pub embedding: Option<Vec<f32>>,
    pub importance: f32,
    pub created_at: DateTime<Utc>,
    pub last_accessed: DateTime<Utc>,
    pub access_count: u32,
    pub tags: Vec<String>,
    pub metadata: HashMap<String, String>,
    pub project_id: Option<Uuid>,
    pub is_deleted: bool,
    pub deleted_at: Option<DateTime<Utc>>,
    
    pub chunk_group_id: Option<Uuid>,
    pub chunk_index: Option<u32>,
    pub chunk_total: Option<u32>,
}

impl Memory {
    pub fn new(content: String, memory_type: MemoryType) -> Self {
        let now = Utc::now();
        Self {
            id: Uuid::new_v4(),
            memory_type,
            content,
            summary: None,
            embedding: None,
            importance: 0.8,
            created_at: now,
            last_accessed: now,
            access_count: 0,
            tags: Vec::new(),
            metadata: HashMap::new(),
            project_id: None,
            is_deleted: false,
            deleted_at: None,
            chunk_group_id: None,
            chunk_index: None,
            chunk_total: None,
        }
    }
    
    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
        self.tags = tags;
        self
    }
    
    pub fn with_project(mut self, project_id: Uuid) -> Self {
        self.project_id = Some(project_id);
        self
    }
    
    pub fn with_chunk_info(mut self, group_id: Uuid, index: u32, total: u32) -> Self {
        self.chunk_group_id = Some(group_id);
        self.chunk_index = Some(index);
        self.chunk_total = Some(total);
        self
    }
    
    pub fn access(&mut self) {
        self.last_accessed = Utc::now();
        self.access_count += 1;
    }
    
    pub fn is_chunked(&self) -> bool {
        self.chunk_group_id.is_some()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_memory_type_serde() {
        let types = [
            MemoryType::Conversation,
            MemoryType::Knowledge,
            MemoryType::Decision,
            MemoryType::Preference,
            MemoryType::Context,
        ];
        
        for t in types {
            let json = serde_json::to_string(&t).unwrap();
            let parsed: MemoryType = serde_json::from_str(&json).unwrap();
            assert_eq!(t, parsed);
        }
    }
    
    #[test]
    fn test_memory_type_json_values() {
        assert_eq!(serde_json::to_string(&MemoryType::Conversation).unwrap(), "\"conversation\"");
        assert_eq!(serde_json::to_string(&MemoryType::Knowledge).unwrap(), "\"knowledge\"");
    }
    
    #[test]
    fn test_memory_creation() {
        let memory = Memory::new("test content".to_string(), MemoryType::Knowledge);
        
        assert!(!memory.id.to_string().is_empty());
        assert_eq!(memory.memory_type, MemoryType::Knowledge);
        assert_eq!(memory.content, "test content");
        assert!(memory.embedding.is_none());
        assert_eq!(memory.importance, 0.8);
        assert_eq!(memory.access_count, 0);
        assert!(memory.tags.is_empty());
        assert!(!memory.is_deleted);
    }
    
    #[test]
    fn test_memory_with_tags() {
        let memory = Memory::new("test".to_string(), MemoryType::Decision)
            .with_tags(vec!["important".to_string(), "project-x".to_string()]);
        
        assert_eq!(memory.tags.len(), 2);
        assert!(memory.tags.contains(&"important".to_string()));
    }
    
    #[test]
    fn test_memory_access() {
        let mut memory = Memory::new("test".to_string(), MemoryType::Conversation);
        let initial_accessed = memory.last_accessed;
        
        memory.access();
        
        assert!(memory.last_accessed > initial_accessed);
        assert_eq!(memory.access_count, 1);
    }
    
    #[test]
    fn test_memory_serde() {
        let memory = Memory::new("test content".to_string(), MemoryType::Knowledge)
            .with_tags(vec!["tag1".to_string()]);
        
        let json = serde_json::to_string(&memory).unwrap();
        let parsed: Memory = serde_json::from_str(&json).unwrap();
        
        assert_eq!(memory.id, parsed.id);
        assert_eq!(memory.content, parsed.content);
        assert_eq!(memory.tags, parsed.tags);
    }
    
    #[test]
    fn test_memory_chunk_fields() {
        let group_id = Uuid::new_v4();
        let memory = Memory::new("test".to_string(), MemoryType::Knowledge)
            .with_chunk_info(group_id, 0, 3);
        
        assert_eq!(memory.chunk_group_id, Some(group_id));
        assert_eq!(memory.chunk_index, Some(0));
        assert_eq!(memory.chunk_total, Some(3));
        assert!(memory.is_chunked());
    }
    
    #[test]
    fn test_memory_chunk_serde() {
        let group_id = Uuid::new_v4();
        let memory = Memory::new("test".to_string(), MemoryType::Knowledge)
            .with_chunk_info(group_id, 1, 5);
        
        let json = serde_json::to_string(&memory).unwrap();
        let parsed: Memory = serde_json::from_str(&json).unwrap();
        
        assert_eq!(memory.chunk_group_id, parsed.chunk_group_id);
        assert_eq!(memory.chunk_index, parsed.chunk_index);
        assert_eq!(memory.chunk_total, parsed.chunk_total);
    }
}