""" Firestore-backed implementation of the MemoryStore interface. """ from typing import List, Dict, Any, Optional from datetime import datetime, timezone from google.cloud import firestore from contracts.common import EmmaConversation, MemoryRecord, AuditEvent from .memory_store import MemoryStore class FirestoreMemoryStore(MemoryStore): """ Implements the MemoryStore interface using Google Cloud Firestore. """ def __init__(self, project_id: str, client: Optional[firestore.AsyncClient] = None): self._project_id = project_id # Client can be injected for testing, otherwise it will be created on demand. self._client = client def _get_client(self) -> firestore.AsyncClient: if not self._client: self._client = firestore.AsyncClient(project=self._project_id) return self._client async def create_conversation(self, owner_id: str, workspace_id: str, created_by: str) -> EmmaConversation: db = self._get_client() convo = EmmaConversation(owner_id=owner_id, workspace_id=workspace_id, created_by=created_by) doc_ref = db.collection("emma_conversations").document(convo.conversation_id) await doc_ref.set(convo.model_dump(mode='json')) return convo async def get_conversation(self, conversation_id: str, owner_id: str) -> Optional[EmmaConversation]: db = self._get_client() doc_ref = db.collection("emma_conversations").document(conversation_id) doc = await doc_ref.get() if not doc.exists: return None convo = EmmaConversation(**doc.to_dict()) # Security: Enforce ownership if convo.owner_id != owner_id: return None # Or raise an exception return convo async def append_message(self, conversation_id: str, message: Dict[str, Any]) -> None: db = self._get_client() # Note: message_id should be auto-generated by Firestore for ordering messages_ref = db.collection("emma_conversations").document(conversation_id).collection("messages") await messages_ref.add(message) # Update last_updated_at on parent conversation document convo_ref = db.collection("emma_conversations").document(conversation_id) await convo_ref.update({"last_updated_at": datetime.now(timezone.utc)}) async def list_messages(self, conversation_id: str, limit: int = 50) -> List[Dict[str, Any]]: db = self._get_client() messages_ref = db.collection("emma_conversations").document(conversation_id).collection("messages") docs = messages_ref.order_by("created_at", direction=firestore.Query.DESCENDING).limit(limit).stream() return [doc.to_dict() async for doc in docs][::-1] # Reverse to get chronological order async def record_audit_event(self, event: AuditEvent) -> str: db = self._get_client() doc_ref = db.collection("emma_audit_events").document(event.event_id) # Use create() to ensure the document does not already exist, guaranteeing append-only. await doc_ref.create(event.model_dump(mode='json')) return event.event_id # --- Placeholder methods not fully implemented in Phase 2B --- async def save_conversation_summary(self, conversation_id: str, summary: str) -> None: # In a real implementation, this would update a summary field pass async def store_memory(self, record: MemoryRecord) -> None: # This would write to the emma_memories collection pass async def recall_memory(self, embedding: List[float], owner_id: str, scopes: List[str]) -> List[MemoryRecord]: # This would perform a query against a vector database, which is out of scope. return [] async def get_profile(self, owner_id: str) -> Optional[Dict[str, Any]]: pass async def update_profile(self, owner_id: str, profile_data: Dict[str, Any]) -> None: pass