import json import os from datetime import datetime from uuid import uuid4 memoryFolderPath = os.path.join(os.path.dirname(__file__), "memory") defaultTriggers = { "triggerField": "action", "triggerValue": "take_note", "noteField": "note", } def ensureMemoryFolder(): if not os.path.isdir(memoryFolderPath): os.makedirs(memoryFolderPath, exist_ok=True) ensureMemoryFolder() class MemoryManager: """Stores raw notes plus summary scaffolding for long-term context.""" @staticmethod def parseAiResponse(userId, aiResponseText, triggers=None): triggerConfig = triggers or defaultTriggers payload = MemoryManager.extractJsonPayload(aiResponseText) if not payload: return None triggerField = triggerConfig.get("triggerField", "action") triggerValue = triggerConfig.get("triggerValue", "take_note") noteField = triggerConfig.get("noteField", "note") if payload.get(triggerField) != triggerValue: return None noteText = payload.get(noteField) if not noteText: return None MemoryManager.recordNote(userId, noteText, payload) print(f"[memory] Recorded note for {userId}: {noteText}") return {"noteRecorded": True, "noteText": noteText} @staticmethod def recordNote(userId, noteText, metadata=None): memory = MemoryManager.loadUserMemory(userId) notes = memory.get("notes") if notes is None: notes = [] memory["notes"] = notes entry = { "timestamp": datetime.utcnow().isoformat() + "Z", "note": noteText, "metadata": metadata or {}, } notes.append(entry) MemoryManager.saveUserMemory(userId, memory) @staticmethod def buildContextPacket(userId, mode="summary", noteLimit=5): memory = MemoryManager.loadUserMemory(userId) summaries = memory.get("summaries") or [] if mode == "raw": return summaries, memory["notes"][-noteLimit:] return MemoryManager.ensureSummaryHierarchy(userId, memory), memory["notes"][-noteLimit:] @staticmethod def ensureSummaryHierarchy(userId, memory): summaries = memory.get("summaries") or [] if not summaries: summaries.append({ "level": 0, "summary": "No summaries yet. AI should synthesize one when ready.", "children": [], }) memory["summaries"] = summaries MemoryManager.saveUserMemory(userId, memory) return summaries @staticmethod def extractJsonPayload(aiResponseText): candidates = MemoryManager.collectJsonCandidates(aiResponseText) for candidate in candidates: try: return json.loads(candidate) except json.JSONDecodeError: continue return None @staticmethod def collectJsonCandidates(aiResponseText): candidates = [] trimmed = aiResponseText.strip() if trimmed.startswith("{") and trimmed.endswith("}"): candidates.append(trimmed) for line in aiResponseText.splitlines(): stripped = line.strip() if stripped.startswith("{") and stripped.endswith("}"): candidates.append(stripped) parts = aiResponseText.split("```") for part in parts: stripped = part.strip() if stripped.lower().startswith("json"): stripped = stripped[4:].lstrip() if stripped.startswith("{") and stripped.endswith("}"): candidates.append(stripped) return candidates @staticmethod def loadUserMemory(userId): MemoryManager.ensureHandle() path = MemoryManager.userMemoryPath(userId) if not os.path.exists(path): return {"notes": [], "summaries": [], "action_items": []} try: with open(path, "r", encoding="utf-8") as handle: return json.load(handle) except (json.JSONDecodeError, OSError): return {"notes": [], "summaries": [], "action_items": []} @staticmethod def saveUserMemory(userId, memory): MemoryManager.ensureHandle() path = MemoryManager.userMemoryPath(userId) with open(path, "w", encoding="utf-8") as handle: json.dump(memory, handle, indent=2) @staticmethod def ensureHandle(): ensureMemoryFolder() @staticmethod def userMemoryPath(userId): safeUserId = userId or "global" return os.path.join(memoryFolderPath, f"{safeUserId}_memory.json") @staticmethod def timestamp(): return datetime.utcnow().isoformat() + "Z" @staticmethod def ensureActionList(memory): actions = memory.get("action_items") if actions is None: actions = [] memory["action_items"] = actions return actions @staticmethod def listActionItems(userId): memory = MemoryManager.loadUserMemory(userId) return memory.get("action_items") or [] @staticmethod def createActionItem(userId, title, cadence="daily", intervalMinutes=None, details=None): cleaned_title = (title or "").strip() if not cleaned_title: return None memory = MemoryManager.loadUserMemory(userId) actions = MemoryManager.ensureActionList(memory) now = MemoryManager.timestamp() action = { "id": str(uuid4()), "title": cleaned_title, "details": (details or "").strip(), "cadence": cadence or "daily", "interval_minutes": MemoryManager.normalizeInterval(intervalMinutes), "created_at": now, "updated_at": now, "progress": [], } actions.append(action) MemoryManager.saveUserMemory(userId, memory) return action @staticmethod def updateActionItem(userId, actionId, updates): if not actionId: return None memory = MemoryManager.loadUserMemory(userId) actions = MemoryManager.ensureActionList(memory) target = None for action in actions: if action.get("id") == actionId: target = action break if not target: return None if "title" in updates and updates["title"]: target["title"] = updates["title"].strip() if "details" in updates: target["details"] = (updates["details"] or "").strip() if "cadence" in updates and updates["cadence"]: target["cadence"] = updates["cadence"] if "interval_minutes" in updates: target["interval_minutes"] = MemoryManager.normalizeInterval(updates["interval_minutes"]) target["updated_at"] = MemoryManager.timestamp() MemoryManager.saveUserMemory(userId, memory) return target @staticmethod def deleteActionItem(userId, actionId): if not actionId: return False memory = MemoryManager.loadUserMemory(userId) actions = MemoryManager.ensureActionList(memory) original_len = len(actions) actions[:] = [action for action in actions if action.get("id") != actionId] if len(actions) == original_len: return False MemoryManager.saveUserMemory(userId, memory) return True @staticmethod def recordActionProgress(userId, actionId, status, note=None): if not actionId: return None memory = MemoryManager.loadUserMemory(userId) actions = MemoryManager.ensureActionList(memory) target = None for action in actions: if action.get("id") == actionId: target = action break if not target: return None progress_list = target.get("progress") if progress_list is None: progress_list = [] target["progress"] = progress_list entry = { "timestamp": MemoryManager.timestamp(), "status": (status or "update").strip() or "update", "note": (note or "").strip(), } progress_list.append(entry) target["updated_at"] = entry["timestamp"] MemoryManager.saveUserMemory(userId, memory) return entry @staticmethod def normalizeInterval(value): if value is None or value == "": return None try: parsed = int(value) except (ValueError, TypeError): return None return max(parsed, 0)