Files
vi/services/memory/operations/facts_ops.py
Alex Kazaiev d017a65750 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 🦊💕
2026-01-03 11:45:58 -06:00

227 lines
8.0 KiB
Python

"""
Facts memory operations.
Provides query and CRUD operations for factual memory (ChromaDB).
"""
import json
from uuid import uuid4
from datetime import datetime
from typing import List, Dict, Any, Optional, Tuple
from core.logger import setup_logger
logger = setup_logger('facts_ops', service_name='memory_service')
class FactsOperations:
"""Handles facts queries and CRUD operations"""
def __init__(self, chroma_store):
"""
Initialize facts operations.
Args:
chroma_store: ChromaStore instance
"""
self.chroma_store = chroma_store
def query(
self,
query: str = '',
limit: int = 5,
category: Optional[str] = None,
identity_id: Optional[str] = None
) -> List[Dict[str, Any]]:
"""
Query facts from ChromaDB with optional category filtering.
Args:
query: Semantic search query (empty = get all matching filters)
limit: Maximum number of facts to return
category: Filter by fact category
identity_id: Filter by identity ID
Returns:
List of fact dictionaries with metadata
"""
try:
collection = self.chroma_store.get_facts_collection()
# Build metadata filters
where_filters = {}
if category:
where_filters["category"] = category
if identity_id:
where_filters["identity_id"] = identity_id
# Query ChromaDB
if query:
# Semantic search with query text
results = collection.query(
query_texts=[query],
n_results=limit,
where=where_filters if where_filters else None
)
else:
# Get all facts matching filters
results = collection.get(
limit=limit,
where=where_filters if where_filters else None
)
# Format results
facts = []
if query and results['ids']:
# Query results
for i, doc_id in enumerate(results['ids'][0]):
metadata = results['metadatas'][0][i] if results['metadatas'] else {}
# Parse identities from JSON string
identities_str = metadata.get('identities', '[]')
identities = json.loads(identities_str) if isinstance(identities_str, str) else identities_str
facts.append({
"id": doc_id,
"content": results['documents'][0][i],
"category": metadata.get('category', 'general'),
"mutable": metadata.get('mutable', True),
"identities": identities,
"metadata": metadata,
"distance": results['distances'][0][i] if results['distances'] else None,
"source": "facts"
})
elif not query and results['ids']:
# Get results (no query)
for i, doc_id in enumerate(results['ids']):
metadata = results['metadatas'][i] if results['metadatas'] else {}
# Parse identities from JSON string
identities_str = metadata.get('identities', '[]')
identities = json.loads(identities_str) if isinstance(identities_str, str) else identities_str
facts.append({
"id": doc_id,
"content": results['documents'][i],
"category": metadata.get('category', 'general'),
"mutable": metadata.get('mutable', True),
"identities": identities,
"metadata": metadata,
"source": "facts"
})
logger.debug(f"[μ] Retrieved {len(facts)} facts (query='{query}', category={category}, limit={limit})")
return facts
except Exception as e:
logger.error(f"[μ] Failed to query facts: {e}")
return []
def create(
self,
content: str,
category: str = 'general',
identities: List[str] = None,
mutable: bool = True,
metadata: Dict[str, Any] = None
) -> str:
"""
Create a new fact in ChromaDB facts collection.
Args:
content: Fact content
category: Fact category
identities: List of identity IDs associated with this fact
mutable: Whether the fact can be updated
metadata: Additional metadata
Returns:
Created fact ID
"""
fact_id = str(uuid4())
identities = identities or []
metadata = metadata or {}
# Prepare metadata (ChromaDB only accepts scalar types, not lists)
fact_metadata = {
"category": category,
"mutable": mutable,
"identities": json.dumps(identities), # Convert list to JSON string
"created_at": datetime.utcnow().isoformat(),
**metadata
}
# Add to ChromaDB (will automatically generate embedding)
collection = self.chroma_store.get_facts_collection()
collection.add(
ids=[fact_id],
documents=[content],
metadatas=[fact_metadata]
)
logger.info(f"[μ] Created fact {fact_id}: category={category}, content='{content[:60]}...'")
return fact_id
def update(
self,
fact_id: str,
new_content: str,
identity_id: str = None,
metadata: Dict[str, Any] = None
) -> Tuple[bool, str]:
"""
Update an existing fact in ChromaDB if it's mutable and owned by identity.
Args:
fact_id: ID of fact to update
new_content: New content for the fact
identity_id: Identity ID for ownership validation (optional)
metadata: Additional metadata to update
Returns:
(success, error_message) - error_message is empty string if successful
"""
try:
collection = self.chroma_store.get_facts_collection()
# Get the fact to check if it exists and is mutable
result = collection.get(ids=[fact_id])
if not result['ids']:
logger.warning(f"[μ] Fact {fact_id} not found")
return False, "Fact not found"
fact_metadata = result['metadatas'][0]
# Check if fact is mutable
if not fact_metadata.get('mutable', True):
logger.warning(f"[μ] Fact {fact_id} is not mutable")
return False, "Fact is not mutable"
# Validate identity ownership if identity_id provided
if identity_id:
fact_identities = fact_metadata.get('identities', [])
if isinstance(fact_identities, str):
try:
fact_identities = json.loads(fact_identities)
except:
fact_identities = [fact_identities]
if identity_id not in fact_identities:
logger.warning(f"[μ] Identity {identity_id} does not own fact {fact_id}")
return False, f"Fact does not belong to identity {identity_id}"
# Update metadata
updated_metadata = dict(fact_metadata)
updated_metadata['updated_at'] = datetime.utcnow().isoformat()
if metadata:
updated_metadata.update(metadata)
# Update in ChromaDB
collection.update(
ids=[fact_id],
documents=[new_content],
metadatas=[updated_metadata]
)
logger.info(f"[μ] Updated fact {fact_id}")
return True, ""
except Exception as e:
logger.error(f"[μ] Failed to update fact {fact_id}: {e}")
return False, str(e)