study-buddy / app /services /project_intelligence.py
GitHub Actions
deploy d092bea3608b7a29952f16357fda39b7a29e399b
2e818da
Raw
History Blame Contribute Delete
6.26 kB
from __future__ import annotations
import json
import os
import time
from typing import Any
from app.schemas.project import Project
from app.services.agent_control import AgentControlService
from app.services.annotation_service import get_annotation_service
from app.services.citation_graph import CitationGraphService
from app.services.project_activity import ProjectActivityService
_DEFAULT_ROOT = os.path.expanduser("~/.studybuddy/projects/intelligence")
class ProjectIntelligenceService:
"""Student-facing project status: brief, useful counts, and settings summary."""
def __init__(
self,
root: str | None = None,
citation_service: CitationGraphService | None = None,
agent_control: AgentControlService | None = None,
) -> None:
self.root = root or _DEFAULT_ROOT
self.citation_service = citation_service or CitationGraphService()
self.agent_control = agent_control or AgentControlService()
os.makedirs(self.root, exist_ok=True)
def _path(self, project_id: str) -> str:
safe = project_id.replace("/", "_").replace("\\", "_").replace("..", "_")
return os.path.join(self.root, f"{safe}.json")
def load_brief(self, project_id: str) -> dict[str, Any]:
path = self._path(project_id)
if not os.path.exists(path):
return {"text": "", "updated_at": 0.0, "source": "empty"}
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
return {
"text": str(data.get("text") or ""),
"updated_at": float(data.get("updated_at") or 0),
"source": str(data.get("source") or "commit"),
}
except Exception:
return {"text": "", "updated_at": 0.0, "source": "error"}
def save_brief(self, project_id: str, text: str, source: str = "commit") -> dict[str, Any]:
brief = {"text": text.strip(), "updated_at": time.time(), "source": source}
with open(self._path(project_id), "w", encoding="utf-8") as f:
json.dump(brief, f, indent=2)
return brief
def update_from_commit(self, project: Project, topic: str, session_summary: str) -> dict[str, Any]:
graph = self.citation_service.load(project.project_id)
paper_titles = [node.title or node.label for node in graph.nodes if node.type == "paper"]
connector_titles = [node.label for node in graph.nodes if node.type != "paper"][:6]
summary = " ".join((session_summary or "").strip().split())[:700]
subject = topic or project.name or "this project"
if paper_titles:
text = f"This project is currently about {subject}, grounded in {', '.join(paper_titles[:3])}."
else:
text = f"This project is currently about {subject}."
if connector_titles:
text += f" The strongest shared threads are {', '.join(connector_titles)}."
if summary:
text += f" Latest Commit signal: {summary}"
else:
text += " Commit again after study activity to deepen this summary."
return self.save_brief(project.project_id, text)
def status(self, project: Project) -> dict[str, Any]:
graph = self.citation_service.load(project.project_id)
annotations = []
annotation_service = get_annotation_service()
for file in project.files:
annotations.extend(annotation_service.get_for_document(file.file_id))
notes = [a for a in annotations if (a.note_text or "").strip()]
pinned_figures = [a for a in annotations if a.image_base64]
highlighted_passages = [
a for a in annotations
if a.target_snippets and any((snippet.text or "").strip() for snippet in a.target_snippets)
]
activity = ProjectActivityService().list(project.project_id, limit=200)
stats = {
"papers": len(project.files),
"notes": len(notes),
"pinned_figures": len(pinned_figures),
"highlighted_passages": len(highlighted_passages),
"commits": sum(1 for event in activity if event.event_type in {"commit", "persona_update", "memory_flush"}),
"recommendation_adds": sum(1 for event in activity if event.event_type == "paper_added" and "recommended" in event.message.lower()),
"verified_citation_links": sum(1 for edge in graph.edges if edge.relation == "cites"),
"unresolved_verified_references": sum(
len(node.metadata.get("verified_unmatched_references", []))
for node in graph.nodes
if node.type == "paper"
),
}
brief = self.load_brief(project.project_id)
if not brief["text"]:
titles = [node.title or node.label for node in graph.nodes if node.type == "paper"]
if titles:
brief = {
"text": (
f"ResearchMate has indexed {len(titles)} paper"
f"{'' if len(titles) == 1 else 's'}: {', '.join(titles[:3])}. "
"Commit this project to write a deeper project brief from your study activity."
),
"updated_at": 0.0,
"source": "metadata",
}
else:
brief = {
"text": "Add papers to let ResearchMate build a project brief. Commit after studying to deepen it.",
"updated_at": 0.0,
"source": "empty",
}
return {
"brief": brief,
"stats": stats,
"settings_summary": self._settings_summary(project.project_id),
}
def _settings_summary(self, project_id: str) -> dict[str, int]:
profile = self.agent_control.get_profile(project_id)
skills = self.agent_control.get_skills(project_id).get("skills", {})
manual_profiles = sum(1 for key in ("student", "project") if profile.get(key, {}).get("manual_text"))
custom_skills = sum(1 for record in skills.values() if record.get("manual_text"))
return {"manual_profiles": manual_profiles, "custom_skills": custom_skills}