diff --git a/fastapi_app.py b/fastapi_app.py index 86a2357..cc22b21 100644 --- a/fastapi_app.py +++ b/fastapi_app.py @@ -14,6 +14,7 @@ import sqlite3 import subprocess from datetime import datetime, timezone from pathlib import Path +from uuid import uuid4 from urllib.parse import urlparse from fastapi import FastAPI, Form, Query, Request @@ -70,6 +71,73 @@ def parse_engram(row: sqlite3.Row) -> dict: "grounding": meta.get("grounding", 0), } + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _update_correctness(engram_id: str, *, action: str, reason: str | None = None) -> dict: + """ + Update correctness_json for an engram. action: confirm|reject + """ + conn = get_db() + c = conn.cursor() + row = c.execute("SELECT correctness_json FROM engrams WHERE id = ?", (engram_id,)).fetchone() + if not row: + conn.close() + raise FileNotFoundError(f"Engram not found: {engram_id}") + + corr = json.loads(row["correctness_json"] or "{}") + corr.setdefault("confirmed", False) + corr.setdefault("confirmations", 0) + corr.setdefault("rejections", 0) + corr.setdefault("review_history", []) + corr["last_reviewed"] = _now_iso() + + entry = { + "by": "dashboard", + "action": action, + "at": corr["last_reviewed"], + "note": (reason or "").strip(), + } + try: + corr["review_history"].append(entry) + except Exception: + corr["review_history"] = [entry] + + if action == "confirm": + corr["confirmed"] = True + corr["confirmations"] = int(corr.get("confirmations", 0) or 0) + 1 + elif action == "reject": + corr["rejections"] = int(corr.get("rejections", 0) or 0) + 1 + + c.execute( + "UPDATE engrams SET correctness_json = ?, modified_at = ? WHERE id = ?", + (json.dumps(corr, ensure_ascii=False), corr["last_reviewed"], engram_id), + ) + conn.commit() + conn.close() + return {"ok": True} + + +def _bump_access(engram_id: str) -> dict: + conn = get_db() + c = conn.cursor() + row = c.execute("SELECT metadata_json FROM engrams WHERE id = ?", (engram_id,)).fetchone() + if not row: + conn.close() + raise FileNotFoundError(f"Engram not found: {engram_id}") + meta = json.loads(row["metadata_json"] or "{}") + meta["access_count"] = int(meta.get("access_count", 0) or 0) + 1 + meta["last_accessed"] = _now_iso() + c.execute( + "UPDATE engrams SET metadata_json = ?, modified_at = ? WHERE id = ?", + (json.dumps(meta, ensure_ascii=False), meta["last_accessed"], engram_id), + ) + conn.commit() + conn.close() + return {"ok": True} + def _safe_json_extract_tags(meta_json: str) -> list[str]: try: d = json.loads(meta_json or "{}") @@ -519,6 +587,109 @@ def api_engram_detail(engram_id: str): return result +@app.get("/api/pending") +def api_pending( + limit: int = Query(20, ge=1, le=200), + offset: int = Query(0, ge=0), + source: str | None = Query(None), +): + conn = get_db() + c = conn.cursor() + where = ["json_extract(correctness_json, '$.confirmed') = 0"] + params: list = [] + if source: + where.append("json_extract(metadata_json, '$.source') = ?") + params.append(source) + + rows = c.execute( + f""" + SELECT * FROM engrams + WHERE {' AND '.join(where)} + ORDER BY created_at DESC + LIMIT ? OFFSET ? + """, + params + [limit, offset], + ).fetchall() + items = [parse_engram(r) for r in rows] + conn.close() + return {"items": items, "limit": limit, "offset": offset} + + +@app.post("/api/engrams") +def api_create_engram(content: str = Form(...), tags: str = Form("")): + content = (content or "").strip() + if not content: + return JSONResponse({"error": "content required"}, status_code=400) + tag_list = [t.strip() for t in (tags or "").split(",") if t.strip()] + now = _now_iso() + engram_id = str(uuid4()) + meta = { + "source": "user", + "confidence": 0.7, + "created": now, + "modified": now, + "access_count": 0, + "last_accessed": now, + "tags": tag_list, + "session_id": None, + "agent_id": None, + "grounding": 0, + } + corr = { + "confirmed": False, + "confirmations": 0, + "rejections": 0, + "last_reviewed": None, + "review_history": [], + } + conn = get_db() + c = conn.cursor() + c.execute( + """ + INSERT INTO engrams (id, content, metadata_json, correctness_json, links_json, hierarchy_json, embedding_json, created_at, modified_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + engram_id, + content, + json.dumps(meta, ensure_ascii=False), + json.dumps(corr, ensure_ascii=False), + "[]", + "{}", + None, + now, + now, + ), + ) + conn.commit() + conn.close() + return {"id": engram_id} + + +@app.post("/api/engrams/{engram_id}/confirm") +def api_confirm_engram(engram_id: str, reason: str = Form("")): + try: + return _update_correctness(engram_id, action="confirm", reason=reason) + except FileNotFoundError as e: + return JSONResponse({"error": str(e)}, status_code=404) + + +@app.post("/api/engrams/{engram_id}/reject") +def api_reject_engram(engram_id: str, reason: str = Form("")): + try: + return _update_correctness(engram_id, action="reject", reason=reason) + except FileNotFoundError as e: + return JSONResponse({"error": str(e)}, status_code=404) + + +@app.post("/api/engrams/{engram_id}/refresh") +def api_refresh_engram(engram_id: str): + try: + return _bump_access(engram_id) + except FileNotFoundError as e: + return JSONResponse({"error": str(e)}, status_code=404) + + @app.post("/api/engrams/{engram_id}/confirm") def api_confirm(engram_id: str, reason: str = Form("")): conn = get_db() diff --git a/templates/dashboard.html b/templates/dashboard.html index 4ebea86..8b1f378 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -148,6 +148,7 @@ async function loadStatus() { api('/api/insights?limit=8'), api('/api/storage_stats'), ]); + const pend = await api('/api/pending?limit=20&offset=0'); const el = document.getElementById('status'); const jobsHtml = (jobs.items || []).map(j => ` @@ -164,6 +165,21 @@ async function loadStatus() { .map(([k,v]) => `${k}: ${v}`) .join(' '); + const pendItems = (pend.items || []); + const pendHtml = pendItems.map(p => ` +