abstractnote 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- abstractnote/__init__.py +18 -0
- abstractnote/cli.py +240 -0
- abstractnote/flows/__init__.py +2 -0
- abstractnote/flows/build.py +970 -0
- abstractnote/flows/bundle.py +62 -0
- abstractnote/flows/contract.py +88 -0
- abstractnote/flows/graph.py +540 -0
- abstractnote/flows/json/note-capture.json +2280 -0
- abstractnote/flows/json/note-drain.json +2012 -0
- abstractnote/flows/json/note-mine.json +2392 -0
- abstractnote/flows/json/note-process.json +1853 -0
- abstractnote/flows/json/note-recall.json +1876 -0
- abstractnote/flows/json/note-reflect.json +2057 -0
- abstractnote/flows/prompts_fallback.py +119 -0
- abstractnote/flows/runner.py +320 -0
- abstractnote/flows/schemas_fallback.py +175 -0
- abstractnote/gateway/__init__.py +24 -0
- abstractnote/gateway/appspec.py +10 -0
- abstractnote/gateway/router.py +571 -0
- abstractnote/gateway/tools.py +74 -0
- abstractnote/ids.py +177 -0
- abstractnote/importers/__init__.py +142 -0
- abstractnote/importers/__main__.py +3 -0
- abstractnote/importers/agora.py +203 -0
- abstractnote/importers/claude_sessions.py +415 -0
- abstractnote/importers/cli.py +113 -0
- abstractnote/importers/files.py +72 -0
- abstractnote/importers/markdown.py +165 -0
- abstractnote/ingest/__init__.py +7 -0
- abstractnote/ingest/blobs.py +47 -0
- abstractnote/ingest/capture.py +265 -0
- abstractnote/ingest/extract.py +118 -0
- abstractnote/ingest/normalise.py +95 -0
- abstractnote/model.py +229 -0
- abstractnote/process/__init__.py +5 -0
- abstractnote/process/context.py +218 -0
- abstractnote/process/merge.py +60 -0
- abstractnote/process/prompts/__init__.py +39 -0
- abstractnote/process/prompts/extract.md +30 -0
- abstractnote/process/prompts/link.md +32 -0
- abstractnote/process/prompts/mine_dossier.md +13 -0
- abstractnote/process/prompts/mine_findings.md +13 -0
- abstractnote/process/prompts/mine_plan.md +13 -0
- abstractnote/process/prompts/quick_filing.md +23 -0
- abstractnote/process/prompts/reflect.md +10 -0
- abstractnote/process/prompts/synthesize.md +17 -0
- abstractnote/process/resolve.py +96 -0
- abstractnote/process/revise.py +410 -0
- abstractnote/process/schemas.py +377 -0
- abstractnote/process/vocabulary.py +68 -0
- abstractnote/recall/__init__.py +1 -0
- abstractnote/recall/changes.py +191 -0
- abstractnote/recall/expand.py +379 -0
- abstractnote/recall/insights.py +62 -0
- abstractnote/recall/reflect.py +202 -0
- abstractnote/recall/review.py +191 -0
- abstractnote/recall/search.py +205 -0
- abstractnote/recall/shapes.py +52 -0
- abstractnote/recall/then_now.py +100 -0
- abstractnote/store/__init__.py +12 -0
- abstractnote/store/archive.py +166 -0
- abstractnote/store/embeddings.py +275 -0
- abstractnote/store/kg.py +219 -0
- abstractnote/store/notebook.py +829 -0
- abstractnote/store/notes.py +1190 -0
- abstractnote/store/queue.py +159 -0
- abstractnote/store/schema.py +348 -0
- abstractnote/store/vector_index.py +88 -0
- abstractnote/tools/__init__.py +215 -0
- abstractnote/tools/capture.py +121 -0
- abstractnote/tools/context.py +19 -0
- abstractnote/tools/process.py +22 -0
- abstractnote/tools/queue.py +23 -0
- abstractnote/tools/recall.py +64 -0
- abstractnote-0.1.0.dist-info/METADATA +145 -0
- abstractnote-0.1.0.dist-info/RECORD +80 -0
- abstractnote-0.1.0.dist-info/WHEEL +5 -0
- abstractnote-0.1.0.dist-info/entry_points.txt +2 -0
- abstractnote-0.1.0.dist-info/licenses/LICENSE +21 -0
- abstractnote-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""The processing queue (docs/cognition.md, "note-drain and the queue").
|
|
2
|
+
|
|
3
|
+
Rows live in the note index ``queue`` table. Order: priority desc, occurred_at
|
|
4
|
+
asc, size asc. A lease lasts 15 minutes; an expired lease is taken over by the
|
|
5
|
+
next drain and counts as a failed attempt. A failure schedules a retry after
|
|
6
|
+
1 minute, then 10 minutes; the third failure marks the note ``failed``
|
|
7
|
+
(``RETRY_BACKOFF_S[2]``, one hour, is the delay before the attempt that follows
|
|
8
|
+
a third failure when ``MAX_FAILURES`` is raised). Leasing runs in one
|
|
9
|
+
``BEGIN IMMEDIATE`` transaction, so two drains never lease the same note.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import timedelta
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from .. import ids
|
|
18
|
+
|
|
19
|
+
LEASE_S = 15 * 60
|
|
20
|
+
RETRY_BACKOFF_S = (60, 10 * 60, 60 * 60)
|
|
21
|
+
MAX_FAILURES = 3
|
|
22
|
+
PRIORITY_BULK, PRIORITY_NORMAL, PRIORITY_HIGH, PRIORITY_URGENT = 0, 1, 2, 3
|
|
23
|
+
STOPPED_ERROR = "Processing stopped before it finished."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _plus(ts: str, seconds: int) -> str:
|
|
27
|
+
return (ids.parse_time(ts) + timedelta(seconds=seconds)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Queue:
|
|
31
|
+
def __init__(self, store: Any) -> None:
|
|
32
|
+
self.store = store
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------------------ writes
|
|
35
|
+
|
|
36
|
+
def enqueue(self, conn: Any, note_id: str, priority: int, occurred_at: str, size: int, *,
|
|
37
|
+
now: Optional[str] = None) -> None:
|
|
38
|
+
"""Add a note (inside the caller's transaction). Re-enqueueing keeps the existing row."""
|
|
39
|
+
conn.execute(
|
|
40
|
+
"INSERT OR IGNORE INTO queue(note_id, priority, occurred_at, size, enqueued_at, state)"
|
|
41
|
+
" VALUES (?,?,?,?,?,'queued')", (note_id, int(priority), occurred_at, int(size), now or ids.utc_now()))
|
|
42
|
+
conn.execute("UPDATE notes SET status='queued' WHERE note_id=? AND status='received'", (note_id,))
|
|
43
|
+
|
|
44
|
+
def set_priority(self, conn: Any, note_id: str, priority: int) -> None:
|
|
45
|
+
conn.execute("UPDATE queue SET priority=? WHERE note_id=?", (int(priority), note_id))
|
|
46
|
+
|
|
47
|
+
def lease(self, worker_id: str, max_items: int = 1, *, now: Optional[str] = None) -> Dict[str, Any]:
|
|
48
|
+
worker = str(worker_id or "").strip()
|
|
49
|
+
if not worker:
|
|
50
|
+
raise ValueError("A worker id is needed to take notes from the queue.")
|
|
51
|
+
n = max(1, min(100, int(max_items or 1)))
|
|
52
|
+
now = now or ids.utc_now()
|
|
53
|
+
items: List[Dict[str, Any]] = []
|
|
54
|
+
with self.store.tx() as c:
|
|
55
|
+
# Expired leases: the drain stopped; that counts as a failure.
|
|
56
|
+
for r in c.execute("SELECT note_id, failures FROM queue WHERE state='leased' AND lease_until<=?",
|
|
57
|
+
(now,)).fetchall():
|
|
58
|
+
self._fail(c, r["note_id"], int(r["failures"]) + 1, STOPPED_ERROR, now)
|
|
59
|
+
rows = c.execute(
|
|
60
|
+
"SELECT note_id, priority, attempts FROM queue WHERE state='queued' AND"
|
|
61
|
+
" (next_attempt_at IS NULL OR next_attempt_at<=?)"
|
|
62
|
+
" ORDER BY priority DESC, occurred_at ASC, size ASC, note_id ASC LIMIT ?", (now, n)).fetchall()
|
|
63
|
+
until = _plus(now, LEASE_S)
|
|
64
|
+
for r in rows:
|
|
65
|
+
cur = c.execute("UPDATE queue SET state='leased', leased_by=?, lease_until=?, attempts=attempts+1"
|
|
66
|
+
" WHERE note_id=? AND state='queued'", (worker, until, r["note_id"]))
|
|
67
|
+
if cur.rowcount != 1:
|
|
68
|
+
continue
|
|
69
|
+
c.execute("UPDATE notes SET status='processing' WHERE note_id=? AND archived_at IS NULL",
|
|
70
|
+
(r["note_id"],))
|
|
71
|
+
items.append({"note_id": r["note_id"], "priority": r["priority"], "attempts": r["attempts"] + 1,
|
|
72
|
+
"lease_until": until})
|
|
73
|
+
return {"items": items}
|
|
74
|
+
|
|
75
|
+
def _fail(self, c: Any, note_id: str, failures: int, error: str, now: str) -> Dict[str, Any]:
|
|
76
|
+
if failures >= MAX_FAILURES:
|
|
77
|
+
c.execute("UPDATE queue SET state='failed', failures=?, last_error=?, failed_at=?, leased_by=NULL,"
|
|
78
|
+
" lease_until=NULL, next_attempt_at=NULL WHERE note_id=?", (failures, error, now, note_id))
|
|
79
|
+
c.execute("UPDATE notes SET status='failed' WHERE note_id=? AND archived_at IS NULL", (note_id,))
|
|
80
|
+
return {"note_id": note_id, "status": "failed"}
|
|
81
|
+
nxt = _plus(now, RETRY_BACKOFF_S[min(failures - 1, len(RETRY_BACKOFF_S) - 1)])
|
|
82
|
+
c.execute("UPDATE queue SET state='queued', failures=?, last_error=?, leased_by=NULL, lease_until=NULL,"
|
|
83
|
+
" next_attempt_at=? WHERE note_id=?", (failures, error, nxt, note_id))
|
|
84
|
+
c.execute("UPDATE notes SET status='queued' WHERE note_id=? AND archived_at IS NULL", (note_id,))
|
|
85
|
+
return {"note_id": note_id, "status": "queued", "next_attempt_at": nxt}
|
|
86
|
+
|
|
87
|
+
def ack(self, note_id: str, outcome: str, error: Optional[str] = None, revision_id: Optional[str] = None, *,
|
|
88
|
+
now: Optional[str] = None) -> Dict[str, Any]:
|
|
89
|
+
if outcome not in ("ok", "error"):
|
|
90
|
+
raise ValueError("The outcome must be ok or error.")
|
|
91
|
+
now = now or ids.utc_now()
|
|
92
|
+
with self.store.tx() as c:
|
|
93
|
+
row = c.execute("SELECT * FROM queue WHERE note_id=?", (note_id,)).fetchone()
|
|
94
|
+
if row is None:
|
|
95
|
+
raise ValueError("That note is not in the queue.")
|
|
96
|
+
if outcome == "ok":
|
|
97
|
+
if revision_id is not None and c.execute(
|
|
98
|
+
"SELECT 1 FROM revisions WHERE revision_id=? AND note_id=? AND applied_index=1",
|
|
99
|
+
(revision_id, note_id)).fetchone() is None:
|
|
100
|
+
raise ValueError("That revision was not applied to this note.")
|
|
101
|
+
c.execute("UPDATE queue SET state='done', done_at=?, leased_by=NULL, lease_until=NULL,"
|
|
102
|
+
" next_attempt_at=NULL WHERE note_id=?", (now, note_id))
|
|
103
|
+
c.execute("UPDATE notes SET status='processed', processed_at=COALESCE(processed_at, ?)"
|
|
104
|
+
" WHERE note_id=? AND archived_at IS NULL", (now, note_id))
|
|
105
|
+
return {"note_id": note_id, "status": "processed"}
|
|
106
|
+
if row["state"] == "done":
|
|
107
|
+
return {"note_id": note_id, "status": "processed"}
|
|
108
|
+
return self._fail(c, note_id, int(row["failures"]) + 1, str(error or "Processing failed."), now)
|
|
109
|
+
|
|
110
|
+
def retry(self, note_id: str, *, now: Optional[str] = None) -> Dict[str, Any]:
|
|
111
|
+
"""Re-queue a failed note (user action from the queue panel)."""
|
|
112
|
+
with self.store.tx() as c:
|
|
113
|
+
row = c.execute("SELECT state FROM queue WHERE note_id=?", (note_id,)).fetchone()
|
|
114
|
+
if row is None:
|
|
115
|
+
raise ValueError("That note is not in the queue.")
|
|
116
|
+
if row["state"] != "failed":
|
|
117
|
+
raise ValueError("Only a note that failed can be retried.")
|
|
118
|
+
c.execute("UPDATE queue SET state='queued', failures=0, next_attempt_at=NULL, failed_at=NULL"
|
|
119
|
+
" WHERE note_id=?", (note_id,))
|
|
120
|
+
c.execute("UPDATE notes SET status='queued' WHERE note_id=? AND archived_at IS NULL", (note_id,))
|
|
121
|
+
return {"note_id": note_id, "status": "queued"}
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------ reads
|
|
124
|
+
|
|
125
|
+
def attempt_for(self, note_id: str) -> int:
|
|
126
|
+
"""The attempt number a revision made now would carry (the current lease, or the next one)."""
|
|
127
|
+
row = self.store.q1("SELECT state, attempts FROM queue WHERE note_id=?", (note_id,))
|
|
128
|
+
if row is None:
|
|
129
|
+
return 1
|
|
130
|
+
return int(row["attempts"]) if row["state"] == "leased" and row["attempts"] else int(row["attempts"]) + 1
|
|
131
|
+
|
|
132
|
+
def item(self, note_id: str) -> Optional[Dict[str, Any]]:
|
|
133
|
+
row = self.store.q1("SELECT * FROM queue WHERE note_id=?", (note_id,))
|
|
134
|
+
return dict(row) if row else None
|
|
135
|
+
|
|
136
|
+
def stats(self) -> Dict[str, Any]:
|
|
137
|
+
rows = self.store.q("SELECT state, priority, COUNT(*) AS n FROM queue GROUP BY state, priority")
|
|
138
|
+
out: Dict[str, Any] = {"queued": 0, "processing": 0, "failed": 0, "done": 0,
|
|
139
|
+
"by_priority": {str(p): 0 for p in (3, 2, 1, 0)}}
|
|
140
|
+
for r in rows:
|
|
141
|
+
key = {"queued": "queued", "leased": "processing", "failed": "failed", "done": "done"}[r["state"]]
|
|
142
|
+
out[key] += r["n"]
|
|
143
|
+
if r["state"] in ("queued", "leased"):
|
|
144
|
+
out["by_priority"][str(r["priority"])] = out["by_priority"].get(str(r["priority"]), 0) + r["n"]
|
|
145
|
+
oldest = self.store.q1("SELECT MIN(enqueued_at) FROM queue WHERE state IN ('queued','leased')")
|
|
146
|
+
out["oldest_enqueued_at"] = oldest[0] if oldest else None
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
def summary(self, limit: int = 50) -> Dict[str, Any]:
|
|
150
|
+
"""Queue state and failed notes with their errors (``GET /note/queue``)."""
|
|
151
|
+
failed = self.store.q(
|
|
152
|
+
"SELECT q.note_id, q.last_error, q.failed_at, q.failures, n.title, n.occurred_at FROM queue q"
|
|
153
|
+
" JOIN notes n ON n.note_id=q.note_id WHERE q.state='failed' ORDER BY q.failed_at DESC LIMIT ?",
|
|
154
|
+
(int(limit),))
|
|
155
|
+
waiting = self.store.q(
|
|
156
|
+
"SELECT q.note_id, q.priority, q.state, q.attempts, q.next_attempt_at, n.title, n.occurred_at FROM queue q"
|
|
157
|
+
" JOIN notes n ON n.note_id=q.note_id WHERE q.state IN ('queued','leased')"
|
|
158
|
+
" ORDER BY q.priority DESC, q.occurred_at ASC, q.size ASC LIMIT ?", (int(limit),))
|
|
159
|
+
return {"stats": self.stats(), "failed": [dict(r) for r in failed], "waiting": [dict(r) for r in waiting]}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""SQLite schema of the note index (``note.sqlite3``).
|
|
2
|
+
|
|
3
|
+
Truth tables (kept by a rebuild): ``meta``, ``notes``, ``attachments``,
|
|
4
|
+
``note_links``, ``queue``, ``revisions``, ``vectors`` (the embedding cache) and
|
|
5
|
+
``context_packets``. Everything else is a projection of notes plus revisions
|
|
6
|
+
and is recreated by ``Notebook.rebuild``.
|
|
7
|
+
|
|
8
|
+
Vectors are float32 blobs; cosine search runs in Python over the filtered rows.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sqlite3
|
|
14
|
+
|
|
15
|
+
SCHEMA_VERSION = 1
|
|
16
|
+
|
|
17
|
+
TRUTH_TABLES = ("meta", "notes", "attachments", "note_links", "queue", "revisions", "vectors", "context_packets")
|
|
18
|
+
|
|
19
|
+
DDL = """
|
|
20
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
23
|
+
note_id TEXT PRIMARY KEY,
|
|
24
|
+
kind TEXT,
|
|
25
|
+
title TEXT,
|
|
26
|
+
text TEXT NOT NULL DEFAULT '',
|
|
27
|
+
text_norm TEXT NOT NULL DEFAULT '',
|
|
28
|
+
text_hash TEXT,
|
|
29
|
+
one_line TEXT,
|
|
30
|
+
summary TEXT,
|
|
31
|
+
occurred_at TEXT NOT NULL,
|
|
32
|
+
received_at TEXT NOT NULL,
|
|
33
|
+
processed_at TEXT,
|
|
34
|
+
source_json TEXT NOT NULL,
|
|
35
|
+
client TEXT,
|
|
36
|
+
location_json TEXT,
|
|
37
|
+
language TEXT,
|
|
38
|
+
filing_topics_json TEXT NOT NULL DEFAULT '[]',
|
|
39
|
+
priority INTEGER NOT NULL DEFAULT 1,
|
|
40
|
+
priority_explicit INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
status TEXT NOT NULL DEFAULT 'received',
|
|
42
|
+
duplicate_of TEXT,
|
|
43
|
+
session_id TEXT,
|
|
44
|
+
run_id TEXT,
|
|
45
|
+
archived_at TEXT,
|
|
46
|
+
idempotency_key TEXT,
|
|
47
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
vector_key TEXT,
|
|
49
|
+
insight_kind TEXT,
|
|
50
|
+
evidence_note_ids_json TEXT,
|
|
51
|
+
entity_ids_json TEXT,
|
|
52
|
+
acked_at TEXT,
|
|
53
|
+
about_json TEXT,
|
|
54
|
+
seq INTEGER
|
|
55
|
+
);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_notes_occurred ON notes(occurred_at);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_notes_received ON notes(received_at);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_notes_text_hash ON notes(text_hash);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_notes_kind ON notes(kind);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
62
|
+
attachment_id TEXT PRIMARY KEY,
|
|
63
|
+
note_id TEXT NOT NULL,
|
|
64
|
+
sha256 TEXT NOT NULL,
|
|
65
|
+
size INTEGER NOT NULL,
|
|
66
|
+
mime TEXT NOT NULL,
|
|
67
|
+
filename TEXT,
|
|
68
|
+
artifact_ref_json TEXT,
|
|
69
|
+
blob_path TEXT NOT NULL,
|
|
70
|
+
extracted_by TEXT NOT NULL DEFAULT 'none',
|
|
71
|
+
extracted_chars INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
created_at TEXT NOT NULL,
|
|
73
|
+
position INTEGER NOT NULL DEFAULT 0
|
|
74
|
+
);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_attachments_note ON attachments(note_id);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_attachments_sha ON attachments(sha256);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS note_links (
|
|
79
|
+
src_note_id TEXT NOT NULL,
|
|
80
|
+
dst_note_id TEXT NOT NULL,
|
|
81
|
+
predicate TEXT NOT NULL,
|
|
82
|
+
score REAL,
|
|
83
|
+
matched_by TEXT,
|
|
84
|
+
detail_json TEXT,
|
|
85
|
+
created_at TEXT NOT NULL,
|
|
86
|
+
PRIMARY KEY (src_note_id, dst_note_id, predicate)
|
|
87
|
+
);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_note_links_dst ON note_links(dst_note_id);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS queue (
|
|
91
|
+
note_id TEXT PRIMARY KEY,
|
|
92
|
+
priority INTEGER NOT NULL,
|
|
93
|
+
occurred_at TEXT NOT NULL,
|
|
94
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
95
|
+
enqueued_at TEXT NOT NULL,
|
|
96
|
+
state TEXT NOT NULL DEFAULT 'queued',
|
|
97
|
+
leased_by TEXT,
|
|
98
|
+
lease_until TEXT,
|
|
99
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
100
|
+
failures INTEGER NOT NULL DEFAULT 0,
|
|
101
|
+
next_attempt_at TEXT,
|
|
102
|
+
last_error TEXT,
|
|
103
|
+
failed_at TEXT,
|
|
104
|
+
done_at TEXT
|
|
105
|
+
);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_queue_order ON queue(state, priority DESC, occurred_at ASC, size ASC);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE IF NOT EXISTS revisions (
|
|
109
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
110
|
+
revision_id TEXT NOT NULL UNIQUE,
|
|
111
|
+
note_id TEXT,
|
|
112
|
+
attempt INTEGER,
|
|
113
|
+
action TEXT,
|
|
114
|
+
target TEXT,
|
|
115
|
+
at TEXT NOT NULL,
|
|
116
|
+
recorded_at TEXT NOT NULL,
|
|
117
|
+
run_id TEXT,
|
|
118
|
+
ops_json TEXT NOT NULL,
|
|
119
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
120
|
+
source_json TEXT,
|
|
121
|
+
warnings_json TEXT NOT NULL DEFAULT '[]',
|
|
122
|
+
result_json TEXT,
|
|
123
|
+
applied_index INTEGER NOT NULL DEFAULT 0,
|
|
124
|
+
applied_kg INTEGER NOT NULL DEFAULT 0,
|
|
125
|
+
abandoned INTEGER NOT NULL DEFAULT 0,
|
|
126
|
+
error TEXT
|
|
127
|
+
);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_revisions_note ON revisions(note_id);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_revisions_at ON revisions(at);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_revisions_recorded ON revisions(recorded_at);
|
|
131
|
+
|
|
132
|
+
CREATE TABLE IF NOT EXISTS vectors (
|
|
133
|
+
vector_key TEXT PRIMARY KEY,
|
|
134
|
+
model_id TEXT NOT NULL,
|
|
135
|
+
dimension INTEGER NOT NULL,
|
|
136
|
+
vector BLOB NOT NULL
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
CREATE TABLE IF NOT EXISTS context_packets (
|
|
140
|
+
note_id TEXT PRIMARY KEY,
|
|
141
|
+
created_at TEXT NOT NULL,
|
|
142
|
+
packet_json TEXT NOT NULL
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
-- ---------------------------------------------------------------- projections
|
|
146
|
+
|
|
147
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
148
|
+
entity_id TEXT PRIMARY KEY,
|
|
149
|
+
kind TEXT NOT NULL,
|
|
150
|
+
name TEXT NOT NULL,
|
|
151
|
+
summary TEXT,
|
|
152
|
+
created_at TEXT NOT NULL,
|
|
153
|
+
created_revision_id TEXT NOT NULL,
|
|
154
|
+
recorded_at TEXT NOT NULL,
|
|
155
|
+
first_seen TEXT,
|
|
156
|
+
last_seen TEXT,
|
|
157
|
+
note_count INTEGER NOT NULL DEFAULT 0,
|
|
158
|
+
merged_into TEXT,
|
|
159
|
+
merged_at TEXT,
|
|
160
|
+
vector_key TEXT,
|
|
161
|
+
revision_count INTEGER NOT NULL DEFAULT 0
|
|
162
|
+
);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_entities_created ON entities(created_at);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_entities_last_seen ON entities(last_seen);
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS entity_aliases (
|
|
167
|
+
entity_id TEXT NOT NULL,
|
|
168
|
+
alias TEXT NOT NULL,
|
|
169
|
+
alias_fold TEXT NOT NULL,
|
|
170
|
+
added_at TEXT NOT NULL,
|
|
171
|
+
revision_id TEXT NOT NULL,
|
|
172
|
+
PRIMARY KEY (entity_id, alias_fold)
|
|
173
|
+
);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_alias_fold ON entity_aliases(alias_fold);
|
|
175
|
+
|
|
176
|
+
CREATE TABLE IF NOT EXISTS entity_names (
|
|
177
|
+
entity_id TEXT NOT NULL,
|
|
178
|
+
name TEXT NOT NULL,
|
|
179
|
+
at TEXT NOT NULL,
|
|
180
|
+
revision_id TEXT NOT NULL,
|
|
181
|
+
PRIMARY KEY (entity_id, revision_id)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
CREATE TABLE IF NOT EXISTS entity_summaries (
|
|
185
|
+
entity_id TEXT NOT NULL,
|
|
186
|
+
revision_id TEXT NOT NULL,
|
|
187
|
+
at TEXT NOT NULL,
|
|
188
|
+
note_id TEXT,
|
|
189
|
+
summary TEXT NOT NULL,
|
|
190
|
+
previous TEXT,
|
|
191
|
+
PRIMARY KEY (entity_id, revision_id)
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
CREATE TABLE IF NOT EXISTS entity_notes (
|
|
195
|
+
entity_id TEXT NOT NULL,
|
|
196
|
+
note_id TEXT NOT NULL,
|
|
197
|
+
at TEXT NOT NULL,
|
|
198
|
+
revision_id TEXT NOT NULL,
|
|
199
|
+
PRIMARY KEY (entity_id, note_id)
|
|
200
|
+
);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_entity_notes_note ON entity_notes(note_id);
|
|
202
|
+
|
|
203
|
+
CREATE TABLE IF NOT EXISTS merges (
|
|
204
|
+
from_id TEXT NOT NULL,
|
|
205
|
+
into_id TEXT NOT NULL,
|
|
206
|
+
at TEXT NOT NULL,
|
|
207
|
+
revision_id TEXT NOT NULL,
|
|
208
|
+
reason TEXT,
|
|
209
|
+
undone_at TEXT,
|
|
210
|
+
undone_revision_id TEXT,
|
|
211
|
+
assertion_id TEXT NOT NULL,
|
|
212
|
+
PRIMARY KEY (from_id, revision_id)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
216
|
+
claim_id TEXT PRIMARY KEY,
|
|
217
|
+
subject_id TEXT NOT NULL,
|
|
218
|
+
predicate TEXT NOT NULL,
|
|
219
|
+
object_id TEXT,
|
|
220
|
+
object_literal TEXT,
|
|
221
|
+
certainty TEXT NOT NULL,
|
|
222
|
+
stance TEXT NOT NULL,
|
|
223
|
+
observed_at TEXT NOT NULL,
|
|
224
|
+
valid_from TEXT,
|
|
225
|
+
valid_until TEXT,
|
|
226
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
227
|
+
superseded_by TEXT,
|
|
228
|
+
closed_reason TEXT,
|
|
229
|
+
closed_revision_id TEXT,
|
|
230
|
+
evidence TEXT,
|
|
231
|
+
note_id TEXT,
|
|
232
|
+
revision_id TEXT NOT NULL,
|
|
233
|
+
recorded_at TEXT NOT NULL,
|
|
234
|
+
assertion_id TEXT NOT NULL,
|
|
235
|
+
vector_key TEXT,
|
|
236
|
+
disputed_with_json TEXT NOT NULL DEFAULT '[]',
|
|
237
|
+
reinforced_count INTEGER NOT NULL DEFAULT 0,
|
|
238
|
+
seq INTEGER
|
|
239
|
+
);
|
|
240
|
+
CREATE INDEX IF NOT EXISTS idx_claims_subject ON claims(subject_id, predicate);
|
|
241
|
+
CREATE INDEX IF NOT EXISTS idx_claims_object ON claims(object_id);
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_claims_observed ON claims(observed_at);
|
|
243
|
+
CREATE INDEX IF NOT EXISTS idx_claims_note ON claims(note_id);
|
|
244
|
+
|
|
245
|
+
CREATE TABLE IF NOT EXISTS claim_reinforcements (
|
|
246
|
+
claim_id TEXT NOT NULL,
|
|
247
|
+
note_id TEXT,
|
|
248
|
+
revision_id TEXT NOT NULL,
|
|
249
|
+
at TEXT NOT NULL,
|
|
250
|
+
PRIMARY KEY (claim_id, revision_id)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
254
|
+
subject_id TEXT NOT NULL,
|
|
255
|
+
object_id TEXT NOT NULL,
|
|
256
|
+
predicate TEXT NOT NULL,
|
|
257
|
+
first_seen TEXT NOT NULL,
|
|
258
|
+
last_seen TEXT NOT NULL,
|
|
259
|
+
count INTEGER NOT NULL,
|
|
260
|
+
active_claim_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
261
|
+
PRIMARY KEY (subject_id, object_id, predicate)
|
|
262
|
+
);
|
|
263
|
+
CREATE INDEX IF NOT EXISTS idx_relations_object ON relations(object_id);
|
|
264
|
+
|
|
265
|
+
CREATE TABLE IF NOT EXISTS topics (
|
|
266
|
+
topic TEXT PRIMARY KEY,
|
|
267
|
+
first_seen TEXT,
|
|
268
|
+
last_seen TEXT,
|
|
269
|
+
note_count INTEGER NOT NULL DEFAULT 0
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
CREATE TABLE IF NOT EXISTS note_topics (
|
|
273
|
+
note_id TEXT NOT NULL,
|
|
274
|
+
topic TEXT NOT NULL,
|
|
275
|
+
assigned_by TEXT NOT NULL,
|
|
276
|
+
revision_id TEXT,
|
|
277
|
+
at TEXT NOT NULL,
|
|
278
|
+
PRIMARY KEY (note_id, topic)
|
|
279
|
+
);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_note_topics_topic ON note_topics(topic);
|
|
281
|
+
|
|
282
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
283
|
+
task_id TEXT PRIMARY KEY,
|
|
284
|
+
note_id TEXT,
|
|
285
|
+
text TEXT NOT NULL,
|
|
286
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
287
|
+
due TEXT,
|
|
288
|
+
opened_at TEXT NOT NULL,
|
|
289
|
+
opened_revision_id TEXT NOT NULL,
|
|
290
|
+
closed_at TEXT,
|
|
291
|
+
closed_by_note_id TEXT,
|
|
292
|
+
closed_revision_id TEXT,
|
|
293
|
+
recorded_closed_at TEXT
|
|
294
|
+
);
|
|
295
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
296
|
+
|
|
297
|
+
CREATE TABLE IF NOT EXISTS then_now_cache (
|
|
298
|
+
key TEXT PRIMARY KEY,
|
|
299
|
+
revision_count INTEGER NOT NULL,
|
|
300
|
+
payload_json TEXT NOT NULL
|
|
301
|
+
);
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
FTS_DDL = """
|
|
305
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(note_id UNINDEXED, title, body, summary, attachments);
|
|
306
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(entity_id UNINDEXED, name, aliases, summary);
|
|
307
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS claims_fts USING fts5(claim_id UNINDEXED, text);
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
PROJECTION_TABLES = (
|
|
311
|
+
"entities", "entity_aliases", "entity_names", "entity_summaries", "entity_notes", "merges", "claims",
|
|
312
|
+
"claim_reinforcements", "relations", "topics", "note_topics", "tasks", "then_now_cache",
|
|
313
|
+
"entities_fts", "claims_fts",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def connect(path: str) -> sqlite3.Connection:
|
|
318
|
+
conn = sqlite3.connect(path, check_same_thread=False, isolation_level=None, timeout=30.0)
|
|
319
|
+
conn.row_factory = sqlite3.Row
|
|
320
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
321
|
+
conn.execute("PRAGMA busy_timeout=30000")
|
|
322
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
323
|
+
conn.execute("PRAGMA foreign_keys=OFF")
|
|
324
|
+
return conn
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# Columns added after the first notebooks were created (in-place upgrade on open).
|
|
328
|
+
_ADDED_COLUMNS = {"notes": [("about_json", "TEXT")]}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def ensure_schema(conn: sqlite3.Connection) -> None:
|
|
332
|
+
conn.executescript(DDL)
|
|
333
|
+
for table, cols in _ADDED_COLUMNS.items():
|
|
334
|
+
have = {r[1] for r in conn.execute(f"PRAGMA table_info({table})")}
|
|
335
|
+
for name, decl in cols:
|
|
336
|
+
if name not in have:
|
|
337
|
+
conn.execute(f"ALTER TABLE {table} ADD COLUMN {name} {decl}")
|
|
338
|
+
try:
|
|
339
|
+
conn.executescript(FTS_DDL)
|
|
340
|
+
except sqlite3.OperationalError as exc: # pragma: no cover - every supported Python ships FTS5
|
|
341
|
+
raise RuntimeError("AbstractNote needs SQLite with FTS5; this Python's sqlite3 lacks it.") from exc
|
|
342
|
+
row = conn.execute("SELECT value FROM meta WHERE key='schema_version'").fetchone()
|
|
343
|
+
if row is None:
|
|
344
|
+
conn.execute("INSERT INTO meta(key, value) VALUES ('schema_version', ?)", (str(SCHEMA_VERSION),))
|
|
345
|
+
elif int(row[0]) > SCHEMA_VERSION:
|
|
346
|
+
raise RuntimeError(
|
|
347
|
+
f"This notebook was written by a newer AbstractNote (schema {row[0]}); upgrade abstractnote to open it."
|
|
348
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""In-memory cosine index over note vectors, refreshed incrementally from the note index.
|
|
2
|
+
|
|
3
|
+
Captures, quick search and the context packet all need "nearest notes"; reading
|
|
4
|
+
every vector from SQLite for each call made capture time grow with the
|
|
5
|
+
notebook. The index loads rows above the last ``notes.seq`` it saw (note
|
|
6
|
+
vectors never change except through a rebuild, which resets the index), so
|
|
7
|
+
other processes writing to the same notebook are picked up on the next call.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Any, Iterable, List, Optional, Sequence, Set, Tuple
|
|
14
|
+
|
|
15
|
+
from .embeddings import _np, cosine, unpack
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NoteVectorIndex:
|
|
19
|
+
def __init__(self, store: Any) -> None:
|
|
20
|
+
self.store = store
|
|
21
|
+
self._lock = threading.RLock()
|
|
22
|
+
self.reset()
|
|
23
|
+
|
|
24
|
+
def reset(self) -> None:
|
|
25
|
+
with self._lock:
|
|
26
|
+
self._last_seq = 0
|
|
27
|
+
self._ids: List[str] = []
|
|
28
|
+
self._insight: List[bool] = []
|
|
29
|
+
self._rows: List[bytes] = []
|
|
30
|
+
self._mat = None
|
|
31
|
+
self._dirty = False
|
|
32
|
+
|
|
33
|
+
def refresh(self) -> None:
|
|
34
|
+
rows = self.store.q(
|
|
35
|
+
"SELECT n.seq, n.note_id, n.insight_kind, v.vector FROM notes n JOIN vectors v ON v.vector_key=n.vector_key"
|
|
36
|
+
" WHERE n.seq>? ORDER BY n.seq", (self._last_seq,))
|
|
37
|
+
if not rows:
|
|
38
|
+
return
|
|
39
|
+
with self._lock:
|
|
40
|
+
for r in rows:
|
|
41
|
+
self._ids.append(r["note_id"])
|
|
42
|
+
self._insight.append(r["insight_kind"] is not None)
|
|
43
|
+
self._rows.append(r["vector"])
|
|
44
|
+
self._last_seq = max(self._last_seq, int(r["seq"]))
|
|
45
|
+
self._dirty = True
|
|
46
|
+
|
|
47
|
+
def nearest(self, query: Sequence[float], k: int, min_score: Optional[float] = None, *,
|
|
48
|
+
allowed: Optional[Set[str]] = None, exclude: Iterable[str] = (), notes_only: bool = False
|
|
49
|
+
) -> List[Tuple[str, float]]:
|
|
50
|
+
self.refresh()
|
|
51
|
+
excluded = set(exclude)
|
|
52
|
+
with self._lock:
|
|
53
|
+
if not self._ids:
|
|
54
|
+
return []
|
|
55
|
+
if _np is not None:
|
|
56
|
+
if self._dirty or self._mat is None:
|
|
57
|
+
have = 0 if self._mat is None else self._mat.shape[0]
|
|
58
|
+
new = _np.frombuffer(b"".join(self._rows[have:]), dtype="<f4").reshape(len(self._rows) - have, -1)
|
|
59
|
+
norms = _np.linalg.norm(new, axis=1)
|
|
60
|
+
norms[norms == 0] = 1.0
|
|
61
|
+
if self._mat is None:
|
|
62
|
+
self._mat, self._norms = new, norms
|
|
63
|
+
else:
|
|
64
|
+
self._mat = _np.vstack([self._mat, new])
|
|
65
|
+
self._norms = _np.concatenate([self._norms, norms])
|
|
66
|
+
self._dirty = False
|
|
67
|
+
q = _np.asarray(query, dtype=_np.float32)
|
|
68
|
+
if q.shape[0] != self._mat.shape[1]:
|
|
69
|
+
raise ValueError("A stored vector has a different dimension from the query; rebuild with --reembed.")
|
|
70
|
+
qn = float(_np.linalg.norm(q)) or 1.0
|
|
71
|
+
scores = (self._mat @ q) / (self._norms * qn)
|
|
72
|
+
order = _np.argsort(-scores)
|
|
73
|
+
pairs = ((int(i), float(scores[i])) for i in order)
|
|
74
|
+
else: # pragma: no cover - numpy ships with the framework
|
|
75
|
+
scored = [(i, cosine(query, unpack(b))) for i, b in enumerate(self._rows)]
|
|
76
|
+
scored.sort(key=lambda t: -t[1])
|
|
77
|
+
pairs = iter(scored)
|
|
78
|
+
out: List[Tuple[str, float]] = []
|
|
79
|
+
for i, s in pairs:
|
|
80
|
+
if min_score is not None and s < min_score:
|
|
81
|
+
break
|
|
82
|
+
nid = self._ids[i]
|
|
83
|
+
if nid in excluded or (notes_only and self._insight[i]) or (allowed is not None and nid not in allowed):
|
|
84
|
+
continue
|
|
85
|
+
out.append((nid, s))
|
|
86
|
+
if len(out) >= k:
|
|
87
|
+
break
|
|
88
|
+
return out
|