Add related_entities to Layer 1 for relationship tracking

Extended Memory struct and database schema to support entity
tracking, which is foundation for Layer 4 relationship system.

Changes to Memory struct (src/core/memory.rs):
- Added related_entities: Option<Vec<String>> field
- Added new_with_entities() constructor for Layer 4
- Added set_related_entities() setter method
- Added has_entity() helper method to check entity membership
- All fields are optional for backward compatibility

Changes to database (src/core/store.rs):
- Added related_entities column to memories table
- Automatic migration for existing databases
- Store as JSON array in TEXT column
- Updated all CRUD operations (create, get, update, list, search)
- Parse JSON to Vec<String> when reading from database

Design rationale:
- "Who with" is fundamental attribute of memory
- Enables efficient querying by entity
- Foundation for Layer 4 relationship inference
- Optional field maintains backward compatibility
- Simple JSON serialization for flexibility

Usage:
Memory::new_with_entities(
  content,
  ai_interpretation,
  priority_score,
  Some(vec!["alice".to_string(), "bob".to_string()])
)
This commit is contained in:
Claude
2025-11-06 07:39:59 +00:00
parent 427943800e
commit 82c8c1c2d2
2 changed files with 94 additions and 19 deletions

View File

@@ -19,6 +19,10 @@ pub struct Memory {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub priority_score: Option<f32>, pub priority_score: Option<f32>,
/// Related entities (people, places, things) involved in this memory (Layer 4)
#[serde(skip_serializing_if = "Option::is_none")]
pub related_entities: Option<Vec<String>>,
/// When this memory was created /// When this memory was created
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
@@ -37,6 +41,7 @@ impl Memory {
content, content,
ai_interpretation: None, ai_interpretation: None,
priority_score: None, priority_score: None,
related_entities: None,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
} }
@@ -56,6 +61,28 @@ impl Memory {
content, content,
ai_interpretation, ai_interpretation,
priority_score, priority_score,
related_entities: None,
created_at: now,
updated_at: now,
}
}
/// Create a new memory with related entities (Layer 4)
pub fn new_with_entities(
content: String,
ai_interpretation: Option<String>,
priority_score: Option<f32>,
related_entities: Option<Vec<String>>,
) -> Self {
let now = Utc::now();
let id = Ulid::new().to_string();
Self {
id,
content,
ai_interpretation,
priority_score,
related_entities,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
} }
@@ -78,6 +105,20 @@ impl Memory {
self.priority_score = Some(score.clamp(0.0, 1.0)); self.priority_score = Some(score.clamp(0.0, 1.0));
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
/// Set or update related entities
pub fn set_related_entities(&mut self, entities: Vec<String>) {
self.related_entities = Some(entities);
self.updated_at = Utc::now();
}
/// Check if this memory is related to a specific entity
pub fn has_entity(&self, entity_id: &str) -> bool {
self.related_entities
.as_ref()
.map(|entities| entities.iter().any(|e| e == entity_id))
.unwrap_or(false)
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -46,6 +46,16 @@ impl MemoryStore {
conn.execute("ALTER TABLE memories ADD COLUMN priority_score REAL", [])?; conn.execute("ALTER TABLE memories ADD COLUMN priority_score REAL", [])?;
} }
// Migrate for Layer 4: related_entities
let has_related_entities: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('memories') WHERE name='related_entities'")?
.query_row([], |row| row.get(0))
.map(|count: i32| count > 0)?;
if !has_related_entities {
conn.execute("ALTER TABLE memories ADD COLUMN related_entities TEXT", [])?;
}
// Create indexes for better query performance // Create indexes for better query performance
conn.execute( conn.execute(
"CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)", "CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)",
@@ -109,14 +119,20 @@ impl MemoryStore {
/// Insert a new memory /// Insert a new memory
pub fn create(&self, memory: &Memory) -> Result<()> { pub fn create(&self, memory: &Memory) -> Result<()> {
let related_entities_json = memory.related_entities
.as_ref()
.map(|entities| serde_json::to_string(entities).ok())
.flatten();
self.conn.execute( self.conn.execute(
"INSERT INTO memories (id, content, ai_interpretation, priority_score, created_at, updated_at) "INSERT INTO memories (id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![ params![
&memory.id, &memory.id,
&memory.content, &memory.content,
&memory.ai_interpretation, &memory.ai_interpretation,
&memory.priority_score, &memory.priority_score,
related_entities_json,
memory.created_at.to_rfc3339(), memory.created_at.to_rfc3339(),
memory.updated_at.to_rfc3339(), memory.updated_at.to_rfc3339(),
], ],
@@ -128,29 +144,33 @@ impl MemoryStore {
pub fn get(&self, id: &str) -> Result<Memory> { pub fn get(&self, id: &str) -> Result<Memory> {
let mut stmt = self let mut stmt = self
.conn .conn
.prepare("SELECT id, content, ai_interpretation, priority_score, created_at, updated_at .prepare("SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories WHERE id = ?1")?; FROM memories WHERE id = ?1")?;
let memory = stmt.query_row(params![id], |row| { let memory = stmt.query_row(params![id], |row| {
let created_at: String = row.get(4)?; let created_at: String = row.get(5)?;
let updated_at: String = row.get(5)?; let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory { Ok(Memory {
id: row.get(0)?, id: row.get(0)?,
content: row.get(1)?, content: row.get(1)?,
ai_interpretation: row.get(2)?, ai_interpretation: row.get(2)?,
priority_score: row.get(3)?, priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at) created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc)) .map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure( .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
4, 5,
rusqlite::types::Type::Text, rusqlite::types::Type::Text,
Box::new(e), Box::new(e),
))?, ))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at) updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc)) .map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure( .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5, 6,
rusqlite::types::Type::Text, rusqlite::types::Type::Text,
Box::new(e), Box::new(e),
))?, ))?,
@@ -162,13 +182,19 @@ impl MemoryStore {
/// Update an existing memory /// Update an existing memory
pub fn update(&self, memory: &Memory) -> Result<()> { pub fn update(&self, memory: &Memory) -> Result<()> {
let related_entities_json = memory.related_entities
.as_ref()
.map(|entities| serde_json::to_string(entities).ok())
.flatten();
let rows_affected = self.conn.execute( let rows_affected = self.conn.execute(
"UPDATE memories SET content = ?1, ai_interpretation = ?2, priority_score = ?3, updated_at = ?4 "UPDATE memories SET content = ?1, ai_interpretation = ?2, priority_score = ?3, related_entities = ?4, updated_at = ?5
WHERE id = ?5", WHERE id = ?6",
params![ params![
&memory.content, &memory.content,
&memory.ai_interpretation, &memory.ai_interpretation,
&memory.priority_score, &memory.priority_score,
related_entities_json,
memory.updated_at.to_rfc3339(), memory.updated_at.to_rfc3339(),
&memory.id, &memory.id,
], ],
@@ -197,31 +223,35 @@ impl MemoryStore {
/// List all memories, ordered by creation time (newest first) /// List all memories, ordered by creation time (newest first)
pub fn list(&self) -> Result<Vec<Memory>> { pub fn list(&self) -> Result<Vec<Memory>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT id, content, ai_interpretation, priority_score, created_at, updated_at "SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories ORDER BY created_at DESC", FROM memories ORDER BY created_at DESC",
)?; )?;
let memories = stmt let memories = stmt
.query_map([], |row| { .query_map([], |row| {
let created_at: String = row.get(4)?; let created_at: String = row.get(5)?;
let updated_at: String = row.get(5)?; let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory { Ok(Memory {
id: row.get(0)?, id: row.get(0)?,
content: row.get(1)?, content: row.get(1)?,
ai_interpretation: row.get(2)?, ai_interpretation: row.get(2)?,
priority_score: row.get(3)?, priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at) created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc)) .map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure( .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
4, 5,
rusqlite::types::Type::Text, rusqlite::types::Type::Text,
Box::new(e), Box::new(e),
))?, ))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at) updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc)) .map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure( .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5, 6,
rusqlite::types::Type::Text, rusqlite::types::Type::Text,
Box::new(e), Box::new(e),
))?, ))?,
@@ -235,7 +265,7 @@ impl MemoryStore {
/// Search memories by content or AI interpretation (case-insensitive) /// Search memories by content or AI interpretation (case-insensitive)
pub fn search(&self, query: &str) -> Result<Vec<Memory>> { pub fn search(&self, query: &str) -> Result<Vec<Memory>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT id, content, ai_interpretation, priority_score, created_at, updated_at "SELECT id, content, ai_interpretation, priority_score, related_entities, created_at, updated_at
FROM memories FROM memories
WHERE content LIKE ?1 OR ai_interpretation LIKE ?1 WHERE content LIKE ?1 OR ai_interpretation LIKE ?1
ORDER BY created_at DESC", ORDER BY created_at DESC",
@@ -244,25 +274,29 @@ impl MemoryStore {
let search_pattern = format!("%{}%", query); let search_pattern = format!("%{}%", query);
let memories = stmt let memories = stmt
.query_map(params![search_pattern], |row| { .query_map(params![search_pattern], |row| {
let created_at: String = row.get(4)?; let created_at: String = row.get(5)?;
let updated_at: String = row.get(5)?; let updated_at: String = row.get(6)?;
let related_entities_json: Option<String> = row.get(4)?;
let related_entities = related_entities_json
.and_then(|json| serde_json::from_str(&json).ok());
Ok(Memory { Ok(Memory {
id: row.get(0)?, id: row.get(0)?,
content: row.get(1)?, content: row.get(1)?,
ai_interpretation: row.get(2)?, ai_interpretation: row.get(2)?,
priority_score: row.get(3)?, priority_score: row.get(3)?,
related_entities,
created_at: DateTime::parse_from_rfc3339(&created_at) created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc)) .map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure( .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
4, 5,
rusqlite::types::Type::Text, rusqlite::types::Type::Text,
Box::new(e), Box::new(e),
))?, ))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at) updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc)) .map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure( .map_err(|e| rusqlite::Error::FromSqlConversionFailure(
5, 6,
rusqlite::types::Type::Text, rusqlite::types::Type::Text,
Box::new(e), Box::new(e),
))?, ))?,