Implement Layer 2: AI Memory with interpretation and priority scoring

Add AI interpretation and priority scoring capabilities to the memory system.
Simple, efficient implementation following the principle: "AI judges, tool records."

## Core Changes

### Memory struct (src/core/memory.rs)
- Added `ai_interpretation: Option<String>` - AI's creative interpretation
- Added `priority_score: Option<f32>` - Priority from 0.0 to 1.0
- New constructor: `Memory::new_ai()` for Layer 2
- Helper methods: `set_ai_interpretation()`, `set_priority_score()`
- Comprehensive tests for all new functionality

### SQLite storage (src/core/store.rs)
- Extended schema with `ai_interpretation` and `priority_score` columns
- Automatic migration for existing databases
- Added index on `priority_score` for future sorting
- Updated all queries to include new fields
- Search now includes `ai_interpretation` in results

### MCP Server (src/mcp/base.rs)
- New tool: `create_ai_memory` with optional interpretation and score
- Updated all existing tools to return Layer 2 fields
- Backward compatible: `create_memory` still works (Layer 1)

## Design Philosophy

**"AI judges, tool records"**
- Tool provides simple storage, no complex logic
- AI (Claude) decides interpretation and importance
- Both fields are optional for flexibility
- Natural integration: interpretation + evaluation happen together

## Usage Example

```javascript
// Layer 1: Simple storage (still works)
create_memory({ content: "Tokyo weather is sunny" })

// Layer 2: AI-enhanced storage
create_ai_memory({
  content: "Tokyo weather is sunny",
  ai_interpretation: "User planning outdoor activities in Tokyo. Weather info important for travel decisions.",
  priority_score: 0.75
})
```

## Backward Compatibility

- Layer 1 functionality unchanged
- Existing databases auto-migrate
- All Layer 2 fields are Optional<T>
- Old tools continue to work

## Testing

- All unit tests passing
- Schema migration tested
- Score clamping (0.0-1.0) tested
- Optional fields tested

Version: 0.2.0 (Layer 2)
Status: Implementation complete, ready for local testing
This commit is contained in:
Claude
2025-11-05 18:45:04 +00:00
parent f2a02abf3e
commit a558a0ba6f
3 changed files with 186 additions and 21 deletions

View File

@@ -25,12 +25,26 @@ impl MemoryStore {
"CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
ai_interpretation TEXT,
priority_score REAL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
[],
)?;
// Migrate existing tables (add columns if they don't exist)
// SQLite doesn't have "IF NOT EXISTS" for columns, so we check first
let has_ai_interpretation: bool = conn
.prepare("SELECT COUNT(*) FROM pragma_table_info('memories') WHERE name='ai_interpretation'")?
.query_row([], |row| row.get(0))
.map(|count: i32| count > 0)?;
if !has_ai_interpretation {
conn.execute("ALTER TABLE memories ADD COLUMN ai_interpretation TEXT", [])?;
conn.execute("ALTER TABLE memories ADD COLUMN priority_score REAL", [])?;
}
// Create indexes for better query performance
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)",
@@ -42,6 +56,11 @@ impl MemoryStore {
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_priority_score ON memories(priority_score)",
[],
)?;
Ok(Self { conn })
}
@@ -60,10 +79,13 @@ impl MemoryStore {
/// Insert a new memory
pub fn create(&self, memory: &Memory) -> Result<()> {
self.conn.execute(
"INSERT INTO memories (id, content, created_at, updated_at) VALUES (?1, ?2, ?3, ?4)",
"INSERT INTO memories (id, content, ai_interpretation, priority_score, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
&memory.id,
&memory.content,
&memory.ai_interpretation,
&memory.priority_score,
memory.created_at.to_rfc3339(),
memory.updated_at.to_rfc3339(),
],
@@ -75,26 +97,29 @@ impl MemoryStore {
pub fn get(&self, id: &str) -> Result<Memory> {
let mut stmt = self
.conn
.prepare("SELECT id, content, created_at, updated_at FROM memories WHERE id = ?1")?;
.prepare("SELECT id, content, ai_interpretation, priority_score, created_at, updated_at
FROM memories WHERE id = ?1")?;
let memory = stmt.query_row(params![id], |row| {
let created_at: String = row.get(2)?;
let updated_at: String = row.get(3)?;
let created_at: String = row.get(4)?;
let updated_at: String = row.get(5)?;
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
2,
4,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
3,
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,
@@ -107,9 +132,12 @@ impl MemoryStore {
/// Update an existing memory
pub fn update(&self, memory: &Memory) -> Result<()> {
let rows_affected = self.conn.execute(
"UPDATE memories SET content = ?1, updated_at = ?2 WHERE id = ?3",
"UPDATE memories SET content = ?1, ai_interpretation = ?2, priority_score = ?3, updated_at = ?4
WHERE id = ?5",
params![
&memory.content,
&memory.ai_interpretation,
&memory.priority_score,
memory.updated_at.to_rfc3339(),
&memory.id,
],
@@ -138,28 +166,31 @@ impl MemoryStore {
/// List all memories, ordered by creation time (newest first)
pub fn list(&self) -> Result<Vec<Memory>> {
let mut stmt = self.conn.prepare(
"SELECT id, content, created_at, updated_at FROM memories ORDER BY created_at DESC",
"SELECT id, content, ai_interpretation, priority_score, created_at, updated_at
FROM memories ORDER BY created_at DESC",
)?;
let memories = stmt
.query_map([], |row| {
let created_at: String = row.get(2)?;
let updated_at: String = row.get(3)?;
let created_at: String = row.get(4)?;
let updated_at: String = row.get(5)?;
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
2,
4,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
3,
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,
@@ -170,34 +201,37 @@ impl MemoryStore {
Ok(memories)
}
/// Search memories by content (case-insensitive)
/// Search memories by content or AI interpretation (case-insensitive)
pub fn search(&self, query: &str) -> Result<Vec<Memory>> {
let mut stmt = self.conn.prepare(
"SELECT id, content, created_at, updated_at FROM memories
WHERE content LIKE ?1
"SELECT id, content, ai_interpretation, priority_score, created_at, updated_at
FROM memories
WHERE content LIKE ?1 OR ai_interpretation LIKE ?1
ORDER BY created_at DESC",
)?;
let search_pattern = format!("%{}%", query);
let memories = stmt
.query_map(params![search_pattern], |row| {
let created_at: String = row.get(2)?;
let updated_at: String = row.get(3)?;
let created_at: String = row.get(4)?;
let updated_at: String = row.get(5)?;
Ok(Memory {
id: row.get(0)?,
content: row.get(1)?,
ai_interpretation: row.get(2)?,
priority_score: row.get(3)?,
created_at: DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
2,
4,
rusqlite::types::Type::Text,
Box::new(e),
))?,
updated_at: DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
3,
5,
rusqlite::types::Type::Text,
Box::new(e),
))?,