253 lines
8.5 KiB
Python
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)
|