Files
ADHDbot/Memory.py
2025-11-11 23:11:59 -06:00

253 lines
8.5 KiB
Python

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)