use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub mod paths {
pub const MEMORY: &str = "MEMORY.md";
pub const IDENTITY: &str = "IDENTITY.md";
pub const SOUL: &str = "SOUL.md";
pub const AGENTS: &str = "AGENTS.md";
pub const USER: &str = "USER.md";
pub const HEARTBEAT: &str = "HEARTBEAT.md";
pub const README: &str = "README.md";
pub const DAILY_DIR: &str = "daily/";
pub const CONTEXT_DIR: &str = "context/";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryDocument {
pub id: Uuid,
pub user_id: String,
pub agent_id: Option<Uuid>,
pub path: String,
pub content: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub metadata: serde_json::Value,
}
impl MemoryDocument {
pub fn new(
user_id: impl Into<String>,
agent_id: Option<Uuid>,
path: impl Into<String>,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
user_id: user_id.into(),
agent_id,
path: path.into(),
content: String::new(),
created_at: now,
updated_at: now,
metadata: serde_json::Value::Object(serde_json::Map::new()),
}
}
pub fn file_name(&self) -> &str {
self.path.rsplit('/').next().unwrap_or(&self.path)
}
pub fn parent_dir(&self) -> Option<&str> {
let idx = self.path.rfind('/')?;
Some(&self.path[..idx])
}
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
pub fn word_count(&self) -> usize {
self.content.split_whitespace().count()
}
pub fn is_identity_document(&self) -> bool {
matches!(
self.path.as_str(),
paths::IDENTITY | paths::SOUL | paths::AGENTS | paths::USER
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceEntry {
pub path: String,
pub is_directory: bool,
pub updated_at: Option<DateTime<Utc>>,
pub content_preview: Option<String>,
}
impl WorkspaceEntry {
pub fn name(&self) -> &str {
self.path.rsplit('/').next().unwrap_or(&self.path)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryChunk {
pub id: Uuid,
pub document_id: Uuid,
pub chunk_index: i32,
pub content: String,
pub embedding: Option<Vec<f32>>,
pub created_at: DateTime<Utc>,
}
impl MemoryChunk {
pub fn new(document_id: Uuid, chunk_index: i32, content: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
document_id,
chunk_index,
content: content.into(),
embedding: None,
created_at: Utc::now(),
}
}
pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
self.embedding = Some(embedding);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_document_new() {
let doc = MemoryDocument::new("user1", None, "context/vision.md");
assert_eq!(doc.user_id, "user1");
assert_eq!(doc.path, "context/vision.md");
assert!(doc.content.is_empty());
}
#[test]
fn test_memory_document_file_name() {
let doc = MemoryDocument::new("user1", None, "projects/alpha/README.md");
assert_eq!(doc.file_name(), "README.md");
}
#[test]
fn test_memory_document_parent_dir() {
let doc = MemoryDocument::new("user1", None, "projects/alpha/README.md");
assert_eq!(doc.parent_dir(), Some("projects/alpha"));
let root_doc = MemoryDocument::new("user1", None, "README.md");
assert_eq!(root_doc.parent_dir(), None);
}
#[test]
fn test_memory_document_word_count() {
let mut doc = MemoryDocument::new("user1", None, "MEMORY.md");
assert_eq!(doc.word_count(), 0);
doc.content = "Hello world, this is a test.".to_string();
assert_eq!(doc.word_count(), 6);
}
#[test]
fn test_is_identity_document() {
let identity = MemoryDocument::new("user1", None, paths::IDENTITY);
assert!(identity.is_identity_document());
let soul = MemoryDocument::new("user1", None, paths::SOUL);
assert!(soul.is_identity_document());
let memory = MemoryDocument::new("user1", None, paths::MEMORY);
assert!(!memory.is_identity_document());
let custom = MemoryDocument::new("user1", None, "projects/notes.md");
assert!(!custom.is_identity_document());
}
#[test]
fn test_workspace_entry_name() {
let entry = WorkspaceEntry {
path: "projects/alpha".to_string(),
is_directory: true,
updated_at: None,
content_preview: None,
};
assert_eq!(entry.name(), "alpha");
}
}