lllin000_PaperForge/paperforge/memory/permanent.py
2026-05-18 22:47:30 +08:00

217 lines
6.6 KiB
Python

from __future__ import annotations
import datetime
import json
import logging
import secrets
from pathlib import Path
from paperforge.config import paperforge_paths
logger = logging.getLogger(__name__)
def _logs_dir(vault: Path) -> Path:
paths = paperforge_paths(vault)
return paths["paperforge"] / "logs"
def _ensure_logs_dir(vault: Path) -> Path:
log_dir = _logs_dir(vault)
log_dir.mkdir(parents=True, exist_ok=True)
return log_dir
# ── Reading Log ────────────────────────────────────────────────────────────
def get_reading_log_path(vault: Path) -> Path:
return _logs_dir(vault) / "reading-log.jsonl"
def append_reading_note(
vault: Path,
paper_id: str,
section: str,
excerpt: str,
usage: str = "",
context: str = "",
note: str = "",
project: str = "",
tags: list[str] | None = None,
agent: str = "",
) -> dict:
if not paper_id:
return {"ok": False, "error": "paper_id is required"}
if not excerpt:
return {"ok": False, "error": "excerpt is required"}
date_str = datetime.date.today().strftime("%Y%m%d")
entry_id = f"rln_{date_str}_{secrets.token_hex(4)}"
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
entry: dict[str, object] = {
"id": entry_id,
"created_at": now,
"paper_id": paper_id,
"section": section,
"excerpt": excerpt,
"usage": usage,
"context": context,
"note": note,
"project": project,
"tags": tags or [],
"agent": agent,
"verified": False,
}
log_dir = _ensure_logs_dir(vault)
filepath = log_dir / "reading-log.jsonl"
try:
with filepath.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
except OSError as e:
return {"ok": False, "error": str(e)}
return {"ok": True, "id": entry_id, "path": str(filepath)}
def _read_jsonl(filepath: Path) -> list[dict]:
if not filepath.exists():
return []
entries: list[dict] = []
with filepath.open("r", encoding="utf-8") as f:
for line_no, line in enumerate(f, 1):
stripped = line.strip()
if not stripped:
continue
try:
entries.append(json.loads(stripped))
except json.JSONDecodeError:
logger.warning(
"Skipping malformed JSON line %d in %s", line_no, filepath
)
return entries
def read_all_reading_notes(vault: Path) -> list[dict]:
filepath = get_reading_log_path(vault)
return _read_jsonl(filepath)
def get_reading_notes_for_paper(vault: Path, paper_id: str) -> list[dict]:
all_notes = read_all_reading_notes(vault)
return [n for n in all_notes if n.get("paper_id") == paper_id]
# ── Project Log ────────────────────────────────────────────────────────────
def get_project_log_path(vault: Path) -> Path:
return _logs_dir(vault) / "project-log.jsonl"
def append_project_entry(vault: Path, entry: dict) -> dict:
date_str = datetime.date.today().strftime("%Y%m%d")
entry_id = f"plog_{date_str}_{secrets.token_hex(4)}"
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
record: dict[str, object] = {
"id": entry_id,
"created_at": now,
"project": entry.get("project", ""),
"date": entry.get("date", ""),
"type": entry.get("type", ""),
"title": entry.get("title", ""),
"decisions": entry.get("decisions", []),
"detours": entry.get("detours", []),
"reusable": entry.get("reusable", []),
"todos": entry.get("todos", []),
"related_papers": entry.get("related_papers", []),
"tags": entry.get("tags", []),
"agent": entry.get("agent", ""),
}
log_dir = _ensure_logs_dir(vault)
filepath = log_dir / "project-log.jsonl"
try:
with filepath.open("a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
except OSError as e:
return {"ok": False, "error": str(e)}
return {"ok": True, "id": entry_id, "path": str(filepath)}
def read_all_project_entries(vault: Path) -> list[dict]:
filepath = get_project_log_path(vault)
return _read_jsonl(filepath)
def get_project_entries(vault: Path, project: str) -> list[dict]:
all_entries = read_all_project_entries(vault)
return [e for e in all_entries if e.get("project") == project]
# ── Correction Log ──────────────────────────────────────────────────────────
def get_correction_log_path(vault: Path) -> Path:
return _logs_dir(vault) / "correction-log.jsonl"
def append_correction(
vault: Path,
paper_id: str,
original_id: str,
correction: str,
reason: str = "",
agent: str = "",
) -> dict:
"""Append a correction record to correction-log.jsonl."""
if not paper_id:
return {"ok": False, "error": "paper_id is required"}
if not original_id:
return {"ok": False, "error": "original_id is required"}
if not correction:
return {"ok": False, "error": "correction is required"}
date_str = datetime.date.today().strftime("%Y%m%d")
entry_id = f"corr_{date_str}_{secrets.token_hex(4)}"
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
entry: dict[str, object] = {
"id": entry_id,
"event_type": "correction",
"created_at": now,
"paper_id": paper_id,
"original_id": original_id,
"correction": correction,
"reason": reason,
"agent": agent,
}
log_dir = _ensure_logs_dir(vault)
filepath = log_dir / "correction-log.jsonl"
try:
with filepath.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
except OSError as e:
return {"ok": False, "error": str(e)}
return {"ok": True, "id": entry_id, "path": str(filepath)}
def read_all_corrections(vault: Path) -> list[dict]:
"""Read all correction entries from correction-log.jsonl."""
filepath = get_correction_log_path(vault)
return _read_jsonl(filepath)
def get_corrections_for_paper(vault: Path, paper_id: str) -> list[dict]:
"""Get all corrections for a specific paper."""
all_corrections = read_all_corrections(vault)
return [c for c in all_corrections if c.get("paper_id") == paper_id]