"""Shared project commit logic. Used by the COMMIT_PROJECT websocket handler ("Commit"), which durably commits to Project History as part of the student action. Snapshots are written under ~/.studybuddy/sessions/ -> that is where the history listing and resume endpoints (app.routers.library) read from. The primary, history-facing file is doc_{document_id}.json (one per document set, so re-uploading the same papers resumes the same project); {project_id}.json and latest.json are written alongside for convenience. (~/.studybuddy/projects/ is the upload root, a separate concern -> do not write snapshots there or history won't find them.) """ import json import os from typing import Any, Dict, List _SESSIONS_DIR = os.path.expanduser("~/.studybuddy/sessions") _brain = None def _get_brain(): global _brain if _brain is None: from app.agents.brain_agent import BrainAgent _brain = BrainAgent() return _brain async def commit_project_snapshot( project_id: str, topic: str, intention: str, nodes: List[Dict[str, Any]], content_files: List[str] = None, document_id: str = "", file_ids: List[str] = None, ) -> Dict[str, Any]: content_files = content_files or [] file_ids = file_ids or [] os.makedirs(_SESSIONS_DIR, exist_ok=True) # A title should describe the project once, not change on every Commit -> reuse # whatever was already generated for this document set if it exists. Keyed by # document_id (the history/dedup key) so the title is stable across resumes. title = "" doc_path = os.path.join(_SESSIONS_DIR, f"doc_{document_id}.json") if document_id else "" if doc_path and os.path.exists(doc_path): try: with open(doc_path, encoding="utf-8") as f: title = json.load(f).get("title", "") except Exception: pass if not title: try: title = _get_brain().generate_session_title(topic, content_files, intention) except Exception: title = topic or "Study Project" payload = { "project_id": project_id, "topic": topic, "intention": intention, "nodes": nodes, "content_files": content_files, "document_id": document_id, # internal cache key + history/dedup key "file_ids": file_ids, "title": title, } # doc_{document_id}.json is the file the history listing/resume endpoints read # (keyed by document set); {project_id}.json + latest.json are written alongside. paths = [os.path.join(_SESSIONS_DIR, f"{project_id}.json"), os.path.join(_SESSIONS_DIR, "latest.json")] if document_id: paths.append(os.path.join(_SESSIONS_DIR, f"doc_{document_id}.json")) for path in paths: with open(path, "w", encoding="utf-8") as f: json.dump(payload, f, indent=2) # Record the temporal Commit event without copying snapshot JSON into # semantic memory; Project History already owns the complete snapshot. from app.services.student_memory import StudentMemoryService await StudentMemoryService().stage_project_snapshot(project_id, "") return {"status": "committed", "paths": paths}