neomemx 0.1.2

A high-performance memory library for AI agents with semantic search
Documentation
//! Scoping identifiers for hierarchical memory management

use crate::error::{ErrorCode, NeomemxError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Hierarchical scoping identifiers for facts
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ScopeIdentifiers {
    /// User identifier
    pub user: Option<String>,
    /// Agent identifier
    pub agent: Option<String>,
    /// Session identifier
    pub session: Option<String>,
}

impl ScopeIdentifiers {
    /// Create scope for a specific user
    pub fn for_user(user_id: impl Into<String>) -> Self {
        Self {
            user: Some(user_id.into()),
            agent: None,
            session: None,
        }
    }

    /// Create scope for a specific agent
    pub fn for_agent(agent_id: impl Into<String>) -> Self {
        Self {
            user: None,
            agent: Some(agent_id.into()),
            session: None,
        }
    }

    /// Create scope for a specific session
    pub fn for_session(session_id: impl Into<String>) -> Self {
        Self {
            user: None,
            agent: None,
            session: Some(session_id.into()),
        }
    }

    /// Add user to existing scope
    pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
        self.user = Some(user_id.into());
        self
    }

    /// Add agent to existing scope
    pub fn with_agent(mut self, agent_id: impl Into<String>) -> Self {
        self.agent = Some(agent_id.into());
        self
    }

    /// Add session to existing scope
    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
        self.session = Some(session_id.into());
        self
    }

    /// Validate that at least one identifier is set
    pub fn validate(&self) -> Result<()> {
        if self.is_empty() {
            return Err(NeomemxError::validation(
                "At least one scope identifier (user, agent, or session) must be provided",
                ErrorCode::ValidationRequired,
                None,
                Some("Provide at least one identifier: user, agent, or session".to_string()),
            ));
        }
        Ok(())
    }

    /// Check if scope is empty
    pub fn is_empty(&self) -> bool {
        self.user.is_none() && self.agent.is_none() && self.session.is_none()
    }

    /// Convert to filter map for vector store queries
    pub fn to_filter_map(&self) -> HashMap<String, serde_json::Value> {
        let mut map = HashMap::new();
        if let Some(ref user) = self.user {
            map.insert("user_id".to_string(), serde_json::json!(user));
        }
        if let Some(ref agent) = self.agent {
            map.insert("agent_id".to_string(), serde_json::json!(agent));
        }
        if let Some(ref session) = self.session {
            map.insert("session_id".to_string(), serde_json::json!(session));
        }
        map
    }
}

impl Default for ScopeIdentifiers {
    fn default() -> Self {
        Self {
            user: None,
            agent: None,
            session: None,
        }
    }
}

/// Filter for scoped queries
#[derive(Debug, Clone)]
pub struct ScopeFilter {
    /// User identifier filter
    pub user: Option<String>,
    /// Agent identifier filter
    pub agent: Option<String>,
    /// Session identifier filter
    pub session: Option<String>,
    /// Additional filter criteria
    pub additional: HashMap<String, serde_json::Value>,
}

impl ScopeFilter {
    /// Create a new empty filter
    pub fn new() -> Self {
        Self {
            user: None,
            agent: None,
            session: None,
            additional: HashMap::new(),
        }
    }

    /// Create from scope identifiers
    pub fn from_scope(scope: &ScopeIdentifiers) -> Self {
        let mut filter = Self::new();
        filter.user = scope.user.clone();
        filter.agent = scope.agent.clone();
        filter.session = scope.session.clone();
        filter
    }

    /// Convert to HashMap for vector store queries
    pub fn to_map(&self) -> HashMap<String, serde_json::Value> {
        let mut map = self.additional.clone();
        if let Some(ref user) = self.user {
            map.insert("user_id".to_string(), serde_json::json!(user));
        }
        if let Some(ref agent) = self.agent {
            map.insert("agent_id".to_string(), serde_json::json!(agent));
        }
        if let Some(ref session) = self.session {
            map.insert("session_id".to_string(), serde_json::json!(session));
        }
        map
    }
}

impl Default for ScopeFilter {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn test_scope_creation() {
        let scope = ScopeIdentifiers::for_user("user_123");
        assert_eq!(scope.user, Some("user_123".to_string()));
        assert!(scope.agent.is_none());
        assert!(scope.session.is_none());
    }

    #[test]
    fn test_scope_chaining() {
        let scope = ScopeIdentifiers::for_user("user_123")
            .with_agent("agent_456")
            .with_session("session_789");

        assert_eq!(scope.user, Some("user_123".to_string()));
        assert_eq!(scope.agent, Some("agent_456".to_string()));
        assert_eq!(scope.session, Some("session_789".to_string()));
    }

    #[test]
    fn test_scope_validation() {
        let empty = ScopeIdentifiers::default();
        assert!(empty.validate().is_err());

        let valid = ScopeIdentifiers::for_user("user_123");
        assert!(valid.validate().is_ok());
    }

    #[test]
    fn test_scope_to_filter_map() {
        let scope = ScopeIdentifiers::for_user("user_123").with_agent("agent_456");

        let filter = scope.to_filter_map();
        assert_eq!(filter.get("user_id"), Some(&serde_json::json!("user_123")));
        assert_eq!(
            filter.get("agent_id"),
            Some(&serde_json::json!("agent_456"))
        );
    }
}