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:
1
services/memory/handlers/__init__.py
Normal file
1
services/memory/handlers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Memory handlers package
|
||||
47
services/memory/handlers/facts_handler.py
Normal file
47
services/memory/handlers/facts_handler.py
Normal 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())
|
||||
49
services/memory/handlers/long_memory_handler.py
Normal file
49
services/memory/handlers/long_memory_handler.py
Normal 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())
|
||||
77
services/memory/handlers/reset_handler.py
Normal file
77
services/memory/handlers/reset_handler.py
Normal 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())
|
||||
56
services/memory/handlers/save_fact_handler.py
Normal file
56
services/memory/handlers/save_fact_handler.py
Normal 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())
|
||||
51
services/memory/handlers/search_handler.py
Normal file
51
services/memory/handlers/search_handler.py
Normal 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())
|
||||
47
services/memory/handlers/short_memory_handler.py
Normal file
47
services/memory/handlers/short_memory_handler.py
Normal 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())
|
||||
90
services/memory/handlers/store_handler.py
Normal file
90
services/memory/handlers/store_handler.py
Normal 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())
|
||||
58
services/memory/handlers/update_fact_handler.py
Normal file
58
services/memory/handlers/update_fact_handler.py
Normal 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())
|
||||
Reference in New Issue
Block a user