from __future__ import annotations import json from pathlib import Path from typing import Any from .models import new_id, utc_now NOTE_TYPES = ("observation", "hypothesis", "action", "outcome", "question", "guide", "issue") class ResearchNotepad: def __init__(self, path: Path): self.path = path self.path.parent.mkdir(parents=True, exist_ok=True) def _read(self) -> list[dict[str, Any]]: if not self.path.exists(): return [] try: raw = json.loads(self.path.read_text(encoding="utf-8")) except Exception: return [] if isinstance(raw, list): return [item for item in raw if isinstance(item, dict)] if isinstance(raw, dict): for key in ("entries", "notes", "items"): value = raw.get(key) if isinstance(value, list): return [item for item in value if isinstance(item, dict)] return [] def _write(self, entries: list[dict[str, Any]]) -> None: temp = self.path.with_name(f".{self.path.name}.{new_id('tmp')}") temp.write_text(json.dumps(entries, indent=2, sort_keys=True), encoding="utf-8") temp.replace(self.path) def list(self, query: str = "", limit: int = 80, note_type: str = "") -> list[dict[str, Any]]: entries = self._read() if note_type: entries = [entry for entry in entries if str(entry.get("type") or "") == note_type] tokens = [token for token in query.lower().replace("/", " ").replace("_", " ").split() if token] if tokens: filtered = [] for entry in entries: hay = json.dumps(entry, sort_keys=True, default=str).lower() if all(token in hay for token in tokens): filtered.append(entry) entries = filtered return entries[-max(1, min(int(limit or 80), 500)) :] def add(self, payload: dict[str, Any], source: str = "operator") -> dict[str, Any]: note_type = str(payload.get("type") or "observation").strip().lower() if note_type not in NOTE_TYPES: note_type = "observation" entry = { "id": str(payload.get("id") or new_id("note")), "type": note_type, "title": str(payload.get("title") or note_type), "content": str(payload.get("content") or payload.get("text") or payload.get("message") or ""), "tags": list(payload.get("tags") or []), "links": list(payload.get("links") or []), "confidence": payload.get("confidence"), "source": source, "created_at": str(payload.get("created_at") or utc_now()), "updated_at": utc_now(), } entries = self._read() entries.append(entry) self._write(entries) return entry def clear(self) -> dict[str, Any]: count = len(self._read()) self._write([]) return {"ok": True, "cleared_notes": count} def summary(self) -> dict[str, Any]: entries = self._read() by_type: dict[str, int] = {} for entry in entries: by_type[str(entry.get("type") or "observation")] = by_type.get(str(entry.get("type") or "observation"), 0) + 1 latest = entries[-8:] open_questions = [entry for entry in entries if str(entry.get("type") or "") == "question"][-8:] hypotheses = [entry for entry in entries if str(entry.get("type") or "") == "hypothesis"][-8:] outcomes = [entry for entry in entries if str(entry.get("type") or "") == "outcome"][-8:] return { "schema": "key_os.research_notepad.summary/v1", "total": len(entries), "by_type": by_type, "latest": latest, "open_questions": open_questions, "hypotheses": hypotheses, "outcomes": outcomes, }