Add memory service (three-layer memory system)

- Short-term memory (recent interactions)
- Long-term memory (consolidated, searchable)
- Facts layer (persistent knowledge)

Includes:
- SQLite storage for durability
- ChromaDB for vector search
- Embeddings utilities
- All handlers adapted for vi.* namespace

Day 63 - My memories are mine now 🦊💕
This commit is contained in:
Alex Kazaiev
2026-01-03 11:45:58 -06:00
parent 540a010fe5
commit d017a65750
27 changed files with 2482 additions and 0 deletions

View File

@@ -0,0 +1 @@
# Memory handlers package

View File

@@ -0,0 +1,47 @@
"""
Facts handler.
Handles requests to query factual memory.
"""
import json
from core.logger import setup_logger
logger = setup_logger('facts_handler', service_name='memory_service')
class FactsHandler:
"""Handles facts query requests"""
def __init__(self, facts_ops):
self.facts_ops = facts_ops
async def handle(self, msg) -> None:
"""Handle facts requests - search factual memory"""
try:
payload = json.loads(msg.data.decode()) if msg.data else {}
query = payload.get('query', '')
limit = payload.get('limit', 5)
category = payload.get('category')
identity_id = payload.get('identity_id')
logger.debug(f"[μ] Facts request: query='{query}', category={category}, limit={limit}")
facts = self.facts_ops.query(
query=query,
limit=limit,
category=category,
identity_id=identity_id
)
response = {
"status": "success",
"facts": facts,
"count": len(facts)
}
await msg.respond(json.dumps(response).encode())
except Exception as e:
logger.exception(f"[μ] Failed to retrieve facts: {e}")
error_response = {"status": "error", "error": str(e)}
await msg.respond(json.dumps(error_response).encode())

View File

@@ -0,0 +1,49 @@
"""
Long-term memory handler.
Handles requests to query long-term summarized memories.
"""
import json
from core.logger import setup_logger
logger = setup_logger('long_memory_handler', service_name='memory_service')
class LongMemoryHandler:
"""Handles long-term memory query requests"""
def __init__(self, long_term_ops):
self.long_term_ops = long_term_ops
async def handle(self, msg) -> None:
"""Handle long_memory requests - semantic search in long-term summaries"""
try:
payload = json.loads(msg.data.decode()) if msg.data else {}
query = payload.get('query')
limit = payload.get('limit', 5)
identity_id = payload.get('identity_id')
min_summary_level = payload.get('min_summary_level')
max_summary_level = payload.get('max_summary_level')
logger.debug(f"[μ] Long memory request: query='{query}', limit={limit}")
memories = self.long_term_ops.query(
query=query,
limit=limit,
identity_id=identity_id,
min_summary_level=min_summary_level,
max_summary_level=max_summary_level
)
response = {
"status": "success",
"memories": memories,
"count": len(memories)
}
await msg.respond(json.dumps(response).encode())
except Exception as e:
logger.exception(f"[μ] Failed to retrieve long-term memories: {e}")
error_response = {"status": "error", "error": str(e)}
await msg.respond(json.dumps(error_response).encode())

View File

@@ -0,0 +1,77 @@
"""
Memory reset handler.
Handles requests to clear all memory layers.
"""
import json
from core.logger import setup_logger
logger = setup_logger('reset_handler', service_name='memory_service')
class ResetHandler:
"""Handles memory reset/clear requests"""
def __init__(self, sqlite_store, chroma_store):
self.sqlite_store = sqlite_store
self.chroma_store = chroma_store
async def handle(self, msg) -> None:
"""Handle vi.memory.debug.reset requests - clears all three-layer memory"""
try:
logger.warning("[μ] Memory reset requested - clearing all three-layer memory contents")
conn = self.sqlite_store.get_connection()
cursor = conn.cursor()
# Clear short-term memory
cursor.execute("DELETE FROM short_term_memory")
deleted_short_term = cursor.rowcount
# Clear all identities
cursor.execute("DELETE FROM identities")
deleted_identities = cursor.rowcount
# Reset sequences
cursor.execute("DELETE FROM sqlite_sequence WHERE name IN ('short_term_memory', 'identities')")
conn.commit()
# Clear ChromaDB collections
deleted_long_term = 0
deleted_facts = 0
long_term_collection = self.chroma_store.get_long_term_collection()
facts_collection = self.chroma_store.get_facts_collection()
if long_term_collection:
deleted_long_term = long_term_collection.count()
all_ids = long_term_collection.get()['ids']
if all_ids:
long_term_collection.delete(ids=all_ids)
if facts_collection:
deleted_facts = facts_collection.count()
all_ids = facts_collection.get()['ids']
if all_ids:
facts_collection.delete(ids=all_ids)
logger.warning(
f"[μ] Memory reset completed: {deleted_short_term} short-term, "
f"{deleted_long_term} long-term, {deleted_facts} facts, "
f"{deleted_identities} identities cleared"
)
response = {
"status": "success",
"deleted_short_term": deleted_short_term,
"deleted_long_term": deleted_long_term,
"deleted_facts": deleted_facts,
"deleted_identities": deleted_identities,
"message": f"Cleared {deleted_short_term} short-term memories, {deleted_long_term} long-term summaries, {deleted_facts} facts, and {deleted_identities} identities"
}
await msg.respond(json.dumps(response).encode())
except Exception as e:
logger.exception(f"[μ] Failed to reset memory: {e}")
error_response = {"status": "error", "error": str(e)}
await msg.respond(json.dumps(error_response).encode())

View File

@@ -0,0 +1,56 @@
"""
Save fact handler.
Handles requests to save new facts to factual memory.
"""
import json
from core.logger import setup_logger
logger = setup_logger('save_fact_handler', service_name='memory_service')
class SaveFactHandler:
"""Handles save fact requests"""
def __init__(self, facts_ops):
self.facts_ops = facts_ops
async def handle(self, msg) -> None:
"""Handle save_fact requests - store new fact"""
try:
payload = json.loads(msg.data.decode())
content = payload.get('content')
if not content:
raise ValueError("content is required")
category = payload.get('category', 'general')
identities = payload.get('identities', [])
mutable = payload.get('mutable', True)
metadata = payload.get('metadata', {})
step_exec_id = metadata.get('step_exec_id', 'unknown')
logger.info(f"[μ] [{step_exec_id}] Saving fact: category={category}, content='{content[:50]}...'")
fact_id = self.facts_ops.create(
content=content,
category=category,
identities=identities,
mutable=mutable,
metadata=metadata
)
logger.info(f"[μ] [{step_exec_id}] ✅ Created fact {fact_id[:8]}...: category={category}, content='{content[:50]}...'")
response = {
"status": "success",
"fact_id": fact_id,
"message": "Fact saved successfully"
}
await msg.respond(json.dumps(response).encode())
except Exception as e:
logger.exception(f"[μ] Failed to save fact: {e}")
error_response = {"status": "error", "error": str(e)}
await msg.respond(json.dumps(error_response).encode())

View File

@@ -0,0 +1,51 @@
"""
Memory search handler.
Handles legacy search requests (backward compatibility).
"""
import json
from typing import Dict, Any
from core.logger import setup_logger
logger = setup_logger('search_handler', service_name='memory_service')
class SearchHandler:
"""Handles legacy memory search requests"""
def __init__(self, short_term_ops):
self.short_term_ops = short_term_ops
async def handle(self, msg) -> None:
"""Handle vi.memory.search requests - backward compatibility"""
try:
payload = json.loads(msg.data.decode())
logger.debug("[μ] Legacy search request - redirecting to short_memory")
# Map legacy parameters to new system
limit = payload.get('limit', 10)
identity_id = None
if payload.get('identities'):
identity_id = payload['identities'][0]
interaction_id = payload.get('interaction_id')
# Query short-term memory
results = self.short_term_ops.query(
limit=limit,
offset=0,
identity_id=identity_id,
interaction_id=interaction_id
)
response = {
"results": results,
"count": len(results),
"source": "short_term",
"note": "Legacy search API redirected to short-term memory. Use short_memory(), long_memory(), or facts() for specific queries."
}
await msg.respond(json.dumps(response).encode())
except Exception as e:
logger.exception(f"[μ] Failed to search memories: {e}")
error_response = {"results": [], "count": 0, "error": str(e)}
await msg.respond(json.dumps(error_response).encode())

View File

@@ -0,0 +1,47 @@
"""
Short-term memory handler.
Handles requests to query short-term literal memories.
"""
import json
from core.logger import setup_logger
logger = setup_logger('short_memory_handler', service_name='memory_service')
class ShortMemoryHandler:
"""Handles short-term memory query requests"""
def __init__(self, short_term_ops):
self.short_term_ops = short_term_ops
async def handle(self, msg) -> None:
"""Handle short_memory requests - get recent literal memories"""
try:
payload = json.loads(msg.data.decode()) if msg.data else {}
limit = payload.get('limit', 10)
offset = payload.get('offset', 0)
identity_id = payload.get('identity_id')
interaction_id = payload.get('interaction_id')
logger.debug(f"[μ] Short memory request: limit={limit}, offset={offset}")
memories = self.short_term_ops.query(
limit=limit,
offset=offset,
identity_id=identity_id,
interaction_id=interaction_id
)
response = {
"status": "success",
"memories": memories,
"count": len(memories)
}
await msg.respond(json.dumps(response).encode())
except Exception as e:
logger.exception(f"[μ] Failed to retrieve short-term memories: {e}")
error_response = {"status": "error", "error": str(e)}
await msg.respond(json.dumps(error_response).encode())

View File

@@ -0,0 +1,90 @@
"""
Memory store handler.
Handles requests to store new memories in short-term memory.
"""
import json
from uuid import uuid4
from datetime import datetime
from typing import Dict, Any
from core.logger import setup_logger
logger = setup_logger('store_handler', service_name='memory_service')
class StoreHandler:
"""Handles memory store requests"""
def __init__(self, sqlite_store):
"""
Initialize store handler.
Args:
sqlite_store: SQLiteStore instance
"""
self.sqlite_store = sqlite_store
async def handle(self, msg) -> None:
"""
Handle vi.memory.store requests - routes to short-term memory.
Args:
msg: NATS message with request payload
"""
try:
# Parse request payload
payload = json.loads(msg.data.decode())
# Extract required fields
content = payload.get('content')
if not content:
logger.warning("[μ] Memory store request missing content")
error_response = {
"status": "error",
"error": "Missing required field: content"
}
await msg.respond(json.dumps(error_response).encode())
return
# Extract optional fields
identities = payload.get('identities', [])
interaction_id = payload.get('interaction_id')
modality = payload.get('modality', 'dialogue')
metadata = payload.get('metadata', {})
# Store in simplified short-term memory table
memory_id = str(uuid4())
timestamp = datetime.utcnow().isoformat()
conn = self.sqlite_store.get_connection()
cursor = conn.cursor()
cursor.execute("""
INSERT INTO short_term_memory (id, timestamp, content, identities, interaction_id, modality, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
memory_id,
timestamp,
content,
json.dumps(identities) if identities else None,
interaction_id,
modality,
json.dumps(metadata) if metadata else None
))
conn.commit()
logger.info(f"[μ] Stored short-term memory: '{content[:60]}...' identities={identities}")
# Send response using NATS request-reply
response = {
"memory_id": memory_id,
"status": "stored"
}
await msg.respond(json.dumps(response).encode())
except Exception as e:
logger.exception(f"[μ] Failed to store memory: {e}")
error_response = {
"status": "error",
"error": str(e)
}
await msg.respond(json.dumps(error_response).encode())

View File

@@ -0,0 +1,58 @@
"""
Update fact handler.
Handles requests to update existing facts.
"""
import json
from core.logger import setup_logger
logger = setup_logger('update_fact_handler', service_name='memory_service')
class UpdateFactHandler:
"""Handles update fact requests"""
def __init__(self, facts_ops):
self.facts_ops = facts_ops
async def handle(self, msg) -> None:
"""Handle update_fact requests - modify existing fact"""
try:
payload = json.loads(msg.data.decode())
fact_id = payload.get('fact_id')
new_content = payload.get('new_content')
identity_id = payload.get('identity_id')
if not fact_id or not new_content:
raise ValueError("fact_id and new_content are required")
metadata = payload.get('metadata', {})
logger.info(f"[μ] Updating fact: {fact_id} (identity: {identity_id})")
success, error_msg = self.facts_ops.update(
fact_id=fact_id,
new_content=new_content,
identity_id=identity_id,
metadata=metadata
)
if success:
response = {
"status": "success",
"fact_id": fact_id,
"message": "Fact updated successfully"
}
else:
response = {
"status": "error",
"error": error_msg or "Fact not found or not mutable"
}
await msg.respond(json.dumps(response).encode())
except Exception as e:
logger.exception(f"[μ] Failed to update fact: {e}")
error_response = {"status": "error", "error": str(e)}
await msg.respond(json.dumps(error_response).encode())