study-buddy / app /routers /control_room.py
GitHub Actions
deploy d092bea3608b7a29952f16357fda39b7a29e399b
2e818da
Raw
History Blame Contribute Delete
6.28 kB
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from fastapi import APIRouter, HTTPException
from app.services.observability_summary import observability_summary
from app.services.project_activity import ProjectActivityService
from app.services.project_intelligence import ProjectIntelligenceService
from app.services.project_service import ProjectService
from app.services.student_memory import StudentMemoryService
router = APIRouter(prefix="/api/projects", tags=["control-room"])
_project_service = ProjectService()
_activity = ProjectActivityService()
def _is_demo_mode() -> bool:
return os.getenv("DEPLOYMENT_ENV", "desktop") == "demo"
def _artifact_roots() -> dict[str, str]:
home = Path.home() / ".studybuddy"
return {
"graphs": str(home / "graphs"),
"sessions": str(home / "sessions"),
"citation_graphs": str(home / "citation_graphs"),
"cache": str(home / "cache"),
"annotations": str(home / "annotations"),
"summaries": str(home / "summaries"),
"cognee": str(home / "cognee"),
}
def _exists(path: str) -> bool:
return bool(path) and os.path.exists(path)
def _count_files(path: str, suffix: str = "") -> int:
if not os.path.isdir(path):
return 0
return len([name for name in os.listdir(path) if not suffix or name.endswith(suffix)])
def _file_meta(path: str) -> tuple[int, float]:
if not _exists(path):
return 0, 0.0
try:
return os.path.getsize(path), os.path.getmtime(path)
except OSError:
return 0, 0.0
def _artifact(id_: str, label: str, saved: bool, detail: str = "", path: str = "") -> dict[str, Any]:
size, updated_at = _file_meta(path) if path else (0, 0.0)
return {
"id": id_,
"key": id_,
"label": label,
"status": "saved" if saved else "missing",
"exists": saved,
"detail": detail,
"path": path or detail,
"size_bytes": size,
"updated_at": updated_at,
}
def project_artifacts(project_id: str, document_id: str = "") -> list[dict[str, Any]]:
roots = _artifact_roots()
graph_path = os.path.join(roots["graphs"], f"{project_id}.json")
doc_graph_path = os.path.join(roots["graphs"], f"doc_{document_id}.json") if document_id else ""
session_path = os.path.join(roots["sessions"], f"{project_id}.json")
citation_path = os.path.join(roots["citation_graphs"], f"{project_id}.json")
annotation_count = _count_files(roots["annotations"], ".json")
summary_count = _count_files(roots["summaries"], ".md")
cache_count = _count_files(roots["cache"], ".json")
return [
_artifact("project_graph", "Project graph", _exists(graph_path), graph_path, graph_path),
_artifact("document_graph_cache", "Document graph cache", _exists(doc_graph_path), doc_graph_path, doc_graph_path),
_artifact("project_snapshot", "Project snapshot", _exists(session_path), session_path, session_path),
_artifact("citation_graph", "Citation Graph", _exists(citation_path), citation_path, citation_path),
_artifact("output_cache", "Generated output cache", cache_count > 0, f"{cache_count} cached item(s)"),
_artifact("annotations", "Annotations", annotation_count > 0, f"{annotation_count} annotation file(s)"),
_artifact("summary", "Summary artifacts", summary_count > 0, f"{summary_count} markdown summary file(s)"),
_artifact("cognee", "Cognee memory store", os.path.isdir(roots["cognee"]), roots["cognee"], roots["cognee"]),
]
@router.get("/{project_id}/activity")
def get_project_activity(project_id: str, limit: int = 50):
if _project_service.load(project_id) is None:
raise HTTPException(404, "Project not found")
return {"project_id": project_id, "items": [row.model_dump() for row in _activity.list(project_id, limit=limit)]}
@router.get("/{project_id}/artifacts")
def get_project_artifacts(project_id: str):
project = _project_service.load(project_id)
if project is None:
raise HTTPException(404, "Project not found")
document_id = _project_service.derive_document_id(project)
return {"project_id": project_id, "items": project_artifacts(project_id, document_id=document_id)}
@router.get("/{project_id}/control-room")
async def get_control_room(project_id: str, fast: bool = True):
project = _project_service.load(project_id)
if project is None:
raise HTTPException(404, "Project not found")
document_id = _project_service.derive_document_id(project)
activities = [row.model_dump() for row in _activity.list(project_id, limit=25)]
artifacts = project_artifacts(project_id, document_id=document_id)
active_jobs = [row for row in activities if row.get("status") == "running"][:5]
memory = (
{
"project_id": project_id,
"state": "disabled",
"profile_dataset": False,
"project_dataset": False,
"inventory_ok": False,
"provenance_ok": False,
"export_ok": False,
"last_error": "Cognee memory is disabled in demo mode.",
}
if _is_demo_mode()
else {
"project_id": project_id,
"state": "refreshing" if active_jobs else "cached",
"profile_dataset": None,
"project_dataset": None,
"inventory_ok": False,
"provenance_ok": False,
"export_ok": False,
"last_error": "Memory inspection is loading in the background.",
}
if fast
else await StudentMemoryService().memory_status(project_id, include_liveness=True)
)
intelligence = ProjectIntelligenceService().status(project)
return {
"project": {**project.model_dump(), "document_id": document_id},
"brief": intelligence["brief"],
"stats": intelligence["stats"],
"settings_summary": intelligence["settings_summary"],
"live_log": activities,
"memory": memory,
"memory_status": memory,
"activity": activities,
"artifacts": artifacts,
"debug_artifacts": artifacts,
"active_jobs": active_jobs,
"observability": observability_summary(),
}