chore: initial import
This commit is contained in:
252
Memory.py
Normal file
252
Memory.py
Normal file
@@ -0,0 +1,252 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user