from typing import Any, Dict, List from fastapi import APIRouter from pydantic import BaseModel from app.agents.brain_agent import BrainAgent from app.rag.chromadb_client import ChromaDBClient from app.schemas.session import Intention, Session from app.services.graph_state import GraphStateManager from app.services.project_service import ProjectService from app.services.student_memory import StudentMemoryService from app.websockets.handlers import get_db, get_graph_manager, _journal router = APIRouter(prefix="/session", tags=["session"]) _brain = BrainAgent() _memory = StudentMemoryService() _project_service = ProjectService() # In-progress ingestion status _ingest_status: Dict[str, str] = {} # project_id -> "indexing" | "extracting" | "ready" | "error" class CreateSessionRequest(BaseModel): topic: str intention: Intention class CreateSessionResponse(BaseModel): project_id: str topic: str intention: Intention @router.post("/create", response_model=CreateSessionResponse) def create_session(req: CreateSessionRequest): project = _project_service.create(req.topic or "Study Project", str(req.intention.value)) _ingest_status[project.project_id] = "indexing" return CreateSessionResponse( project_id=project.project_id, topic=req.topic, intention=req.intention ) @router.get("/ingest-status/{project_id}") def ingest_status(project_id: str): return {"status": _ingest_status.get(project_id, "unknown")} def set_ingest_status(project_id: str, status: str) -> None: _ingest_status[project_id] = status class CommitRequest(BaseModel): project_id: str topic: str intention: str nodes: List[Dict[str, Any]] content_files: List[str] = [] document_id: str = "" file_ids: List[str] = [] @router.post("/commit") async def commit_session(req: CommitRequest): """Kept for compatibility -> the frontend no longer calls this directly. Session History is now committed automatically as part of Push (COMMIT_PROJECT, see handlers.py's commit_project_snapshot() call), which calls the same commit_project_snapshot() this route delegates to. """ from app.services.project_commit import commit_project_snapshot return await commit_project_snapshot( req.project_id, req.topic, req.intention, req.nodes, req.content_files, req.document_id, req.file_ids, ) @router.get("/trajectory/{document_id}") def get_trajectory(document_id: str): """Persistent learning trajectory for a paper (for the Evaluation window).""" from app.services.memory_service import MemoryService return {"trajectory": MemoryService().read_trajectory(document_id)} class ClearRequest(BaseModel): project_id: str document_id: str = "" file_ids: List[str] = [] @router.post("/clear") def clear_session(req: ClearRequest): """Wipe everything tied to this session/document -> persistent and in-memory. A session is exactly one input document set, so clearing means starting over with nothing: no curriculum tree, no lessons, no chunks, no uploaded files. Cognee's cross-session student-memory profile is intentionally NOT touched -> it's meant to persist and grow across documents, not reset with them. """ import shutil from pathlib import Path from app.rag.evidence_ingestion import EvidenceIngestionService from app.services.annotation_service import get_annotation_service from app.services.memory_service import MemoryService from app.services.project_files import project_upload_dir _journal.clear_session(req.project_id) mgr = get_graph_manager() mgr.clear_session(req.project_id) EvidenceIngestionService().reset_project(req.project_id) _ingest_status.pop(req.project_id, None) base = Path.home() / ".studybuddy" session_file = base / "sessions" / f"{req.project_id}.json" if session_file.exists(): session_file.unlink() document_id = req.document_id if document_id: for p in ( base / "sessions" / f"doc_{document_id}.json", base / "graphs" / f"doc_{document_id}.json", ): if p.exists(): p.unlink() get_annotation_service().delete_for_document(document_id) MemoryService().flush_cluster(document_id) # document_id is a combined hash of the whole file set, not a single PDF's cache # filename -> the per-file PDF cache is keyed by file_ids instead. for file_id in req.file_ids: p = base / "pdfs" / f"{file_id}.pdf" if p.exists(): p.unlink() # This session's own upload folder -> never a shared directory. shutil.rmtree(project_upload_dir(req.project_id), ignore_errors=True) return {"status": "cleared"}