use chrono::{DateTime, Utc};
use serde::{Serialize, de::DeserializeOwned};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
pub enum MemoryType {
Episodic,
Semantic,
Procedural,
}
impl MemoryType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Episodic => "episodic",
Self::Semantic => "semantic",
Self::Procedural => "procedural",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"episodic" => Some(Self::Episodic),
"semantic" => Some(Self::Semantic),
"procedural" => Some(Self::Procedural),
_ => None,
}
}
}
impl std::fmt::Display for MemoryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub trait MemoryRecord: Send + Sync + Serialize + DeserializeOwned + 'static {
fn id(&self) -> Option<i64>;
fn searchable_text(&self) -> String;
fn memory_type(&self) -> MemoryType;
fn importance(&self) -> u8 {
5
}
fn created_at(&self) -> DateTime<Utc>;
fn category(&self) -> Option<&str> {
None
}
fn metadata(&self) -> HashMap<String, String> {
HashMap::new()
}
#[cfg(feature = "temporal")]
fn valid_from(&self) -> Option<DateTime<Utc>> {
None
}
#[cfg(feature = "temporal")]
fn valid_until(&self) -> Option<DateTime<Utc>> {
None
}
}
#[derive(Debug, Clone)]
pub struct MemoryMeta {
pub id: Option<i64>,
pub searchable_text: String,
pub memory_type: MemoryType,
pub importance: u8,
pub category: Option<String>,
pub created_at: DateTime<Utc>,
pub metadata: HashMap<String, String>,
}
impl MemoryMeta {
pub fn from_record<T: MemoryRecord>(record: &T) -> Self {
Self {
id: record.id(),
searchable_text: record.searchable_text(),
memory_type: record.memory_type(),
importance: record.importance(),
category: record.category().map(String::from),
created_at: record.created_at(),
metadata: record.metadata(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Serialize, serde::Deserialize)]
struct TestMemory {
id: Option<i64>,
text: String,
created_at: DateTime<Utc>,
}
impl MemoryRecord for TestMemory {
fn id(&self) -> Option<i64> {
self.id
}
fn searchable_text(&self) -> String {
self.text.clone()
}
fn memory_type(&self) -> MemoryType {
MemoryType::Semantic
}
fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
}
#[test]
fn memory_type_roundtrip() {
for mt in [MemoryType::Episodic, MemoryType::Semantic, MemoryType::Procedural] {
let s = mt.as_str();
let parsed = MemoryType::from_str(s);
assert_eq!(parsed, Some(mt), "roundtrip failed for {s}");
}
}
#[test]
fn memory_type_display() {
assert_eq!(MemoryType::Episodic.to_string(), "episodic");
assert_eq!(MemoryType::Semantic.to_string(), "semantic");
assert_eq!(MemoryType::Procedural.to_string(), "procedural");
}
#[test]
fn memory_meta_from_record() {
let record = TestMemory {
id: Some(42),
text: "test memory".to_string(),
created_at: Utc::now(),
};
let meta = MemoryMeta::from_record(&record);
assert_eq!(meta.id, Some(42));
assert_eq!(meta.searchable_text, "test memory");
assert_eq!(meta.memory_type, MemoryType::Semantic);
assert_eq!(meta.importance, 5); assert!(meta.category.is_none());
}
#[test]
fn memory_type_from_invalid() {
assert_eq!(MemoryType::from_str("invalid"), None);
assert_eq!(MemoryType::from_str(""), None);
}
}