neomemx 0.1.2

A high-performance memory library for AI agents with semantic search
Documentation
//! Core fact/memory data structures

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use crate::core::scope::ScopeIdentifiers;

/// Unique identifier for a stored fact
pub type FactId = String;

/// Content hash for deduplication
pub type ContentHash = String;

/// Embedding vector representation
pub type EmbeddingVector = Vec<f32>;

/// Metadata map for additional fact information
pub type MetadataMap = HashMap<String, serde_json::Value>;

/// A single stored fact/memory in the system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredFact {
    /// Unique identifier for this fact
    pub id: FactId,

    /// The content/text of the fact
    pub content: String,

    /// Scoping identifiers
    pub scope: ScopeIdentifiers,

    /// Embedding vector (optional, may be lazy-loaded)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub embedding: Option<EmbeddingVector>,

    /// Timestamps
    /// Timestamp when the fact was created
    pub created_at: DateTime<Utc>,
    /// Timestamp when the fact was last updated
    pub updated_at: DateTime<Utc>,

    /// Content hash for deduplication
    pub content_hash: ContentHash,

    /// Additional metadata
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub metadata: MetadataMap,

    /// Relevance score (for search results)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub relevance_score: Option<f32>,
}

impl StoredFact {
    /// Create a new stored fact
    pub fn new(id: impl Into<FactId>, content: impl Into<String>, scope: ScopeIdentifiers) -> Self {
        let content = content.into();
        let id = id.into();
        let now = Utc::now();
        let content_hash = Self::compute_hash(&content);

        Self {
            id,
            content,
            scope,
            embedding: None,
            created_at: now,
            updated_at: now,
            content_hash,
            metadata: HashMap::new(),
            relevance_score: None,
        }
    }

    /// Compute content hash
    pub fn compute_hash(content: &str) -> ContentHash {
        let digest = ::md5::compute(content);
        format!("{:x}", digest)
    }

    /// Set the embedding vector
    pub fn with_embedding(mut self, embedding: EmbeddingVector) -> Self {
        self.embedding = Some(embedding);
        self
    }

    /// Set metadata
    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
        self.metadata = metadata;
        self
    }

    /// Add a metadata key-value pair
    pub fn add_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
        self.metadata.insert(key.into(), value);
    }

    /// Set relevance score
    pub fn with_relevance_score(mut self, score: f32) -> Self {
        self.relevance_score = Some(score);
        self
    }

    /// Update the content and recompute hash
    pub fn update_content(&mut self, new_content: impl Into<String>) {
        self.content = new_content.into();
        self.content_hash = Self::compute_hash(&self.content);
        self.updated_at = Utc::now();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_stored_fact_creation() {
        let scope = ScopeIdentifiers::for_user("user_123");
        let fact = StoredFact::new("fact_1", "Test content", scope);

        assert_eq!(fact.id, "fact_1");
        assert_eq!(fact.content, "Test content");
        assert!(!fact.content_hash.is_empty());
    }

    #[test]
    fn test_content_hash() {
        let hash1 = StoredFact::compute_hash("test");
        let hash2 = StoredFact::compute_hash("test");
        let hash3 = StoredFact::compute_hash("different");

        assert_eq!(hash1, hash2);
        assert_ne!(hash1, hash3);
    }
}