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.
Files changed (80) hide show
  1. abstractnote/__init__.py +18 -0
  2. abstractnote/cli.py +240 -0
  3. abstractnote/flows/__init__.py +2 -0
  4. abstractnote/flows/build.py +970 -0
  5. abstractnote/flows/bundle.py +62 -0
  6. abstractnote/flows/contract.py +88 -0
  7. abstractnote/flows/graph.py +540 -0
  8. abstractnote/flows/json/note-capture.json +2280 -0
  9. abstractnote/flows/json/note-drain.json +2012 -0
  10. abstractnote/flows/json/note-mine.json +2392 -0
  11. abstractnote/flows/json/note-process.json +1853 -0
  12. abstractnote/flows/json/note-recall.json +1876 -0
  13. abstractnote/flows/json/note-reflect.json +2057 -0
  14. abstractnote/flows/prompts_fallback.py +119 -0
  15. abstractnote/flows/runner.py +320 -0
  16. abstractnote/flows/schemas_fallback.py +175 -0
  17. abstractnote/gateway/__init__.py +24 -0
  18. abstractnote/gateway/appspec.py +10 -0
  19. abstractnote/gateway/router.py +571 -0
  20. abstractnote/gateway/tools.py +74 -0
  21. abstractnote/ids.py +177 -0
  22. abstractnote/importers/__init__.py +142 -0
  23. abstractnote/importers/__main__.py +3 -0
  24. abstractnote/importers/agora.py +203 -0
  25. abstractnote/importers/claude_sessions.py +415 -0
  26. abstractnote/importers/cli.py +113 -0
  27. abstractnote/importers/files.py +72 -0
  28. abstractnote/importers/markdown.py +165 -0
  29. abstractnote/ingest/__init__.py +7 -0
  30. abstractnote/ingest/blobs.py +47 -0
  31. abstractnote/ingest/capture.py +265 -0
  32. abstractnote/ingest/extract.py +118 -0
  33. abstractnote/ingest/normalise.py +95 -0
  34. abstractnote/model.py +229 -0
  35. abstractnote/process/__init__.py +5 -0
  36. abstractnote/process/context.py +218 -0
  37. abstractnote/process/merge.py +60 -0
  38. abstractnote/process/prompts/__init__.py +39 -0
  39. abstractnote/process/prompts/extract.md +30 -0
  40. abstractnote/process/prompts/link.md +32 -0
  41. abstractnote/process/prompts/mine_dossier.md +13 -0
  42. abstractnote/process/prompts/mine_findings.md +13 -0
  43. abstractnote/process/prompts/mine_plan.md +13 -0
  44. abstractnote/process/prompts/quick_filing.md +23 -0
  45. abstractnote/process/prompts/reflect.md +10 -0
  46. abstractnote/process/prompts/synthesize.md +17 -0
  47. abstractnote/process/resolve.py +96 -0
  48. abstractnote/process/revise.py +410 -0
  49. abstractnote/process/schemas.py +377 -0
  50. abstractnote/process/vocabulary.py +68 -0
  51. abstractnote/recall/__init__.py +1 -0
  52. abstractnote/recall/changes.py +191 -0
  53. abstractnote/recall/expand.py +379 -0
  54. abstractnote/recall/insights.py +62 -0
  55. abstractnote/recall/reflect.py +202 -0
  56. abstractnote/recall/review.py +191 -0
  57. abstractnote/recall/search.py +205 -0
  58. abstractnote/recall/shapes.py +52 -0
  59. abstractnote/recall/then_now.py +100 -0
  60. abstractnote/store/__init__.py +12 -0
  61. abstractnote/store/archive.py +166 -0
  62. abstractnote/store/embeddings.py +275 -0
  63. abstractnote/store/kg.py +219 -0
  64. abstractnote/store/notebook.py +829 -0
  65. abstractnote/store/notes.py +1190 -0
  66. abstractnote/store/queue.py +159 -0
  67. abstractnote/store/schema.py +348 -0
  68. abstractnote/store/vector_index.py +88 -0
  69. abstractnote/tools/__init__.py +215 -0
  70. abstractnote/tools/capture.py +121 -0
  71. abstractnote/tools/context.py +19 -0
  72. abstractnote/tools/process.py +22 -0
  73. abstractnote/tools/queue.py +23 -0
  74. abstractnote/tools/recall.py +64 -0
  75. abstractnote-0.1.0.dist-info/METADATA +145 -0
  76. abstractnote-0.1.0.dist-info/RECORD +80 -0
  77. abstractnote-0.1.0.dist-info/WHEEL +5 -0
  78. abstractnote-0.1.0.dist-info/entry_points.txt +2 -0
  79. abstractnote-0.1.0.dist-info/licenses/LICENSE +21 -0
  80. abstractnote-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,215 @@
1
+ """Note tools: plane-bound callables for flows (``tool_calls`` nodes) and agents (``allowed_tools``).
2
+
3
+ ``bind(notebook_dir, ...)`` returns ``{tool_name: callable}``; each callable is
4
+ decorated with AbstractCore's ``@tool``, so ``MappingToolExecutor.from_tools``
5
+ and the runtime's ``tool_calls`` nodes use them directly and their
6
+ ``ToolDefinition.parameters`` describe the accepted arguments. Every tool takes
7
+ JSON-friendly keyword arguments, returns a dict, and raises ``ValueError`` with
8
+ a plain sentence on bad input. ``TOOL_NAMES`` is the auto-approve list.
9
+
10
+ The notebook is opened on the first call, so binding is cheap and creates no
11
+ files.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import threading
17
+ from pathlib import Path
18
+ from typing import Any, Callable, Dict, List, Optional
19
+
20
+ from .context import ArtifactReader, ToolContext
21
+
22
+ TOOL_NAMES: List[str] = [
23
+ "note_capture", "note_file", "note_context", "note_resolve_candidates", "note_apply_revision",
24
+ "note_queue_lease", "note_queue_ack", "note_queue_stats", "note_search", "note_expand",
25
+ "note_changes", "note_entity", "note_timeline", "note_open_loops", "note_contradictions",
26
+ "note_resurface", "note_anniversaries", "note_insight_write",
27
+ ]
28
+
29
+
30
+ class _LazyContext(ToolContext):
31
+ """A ToolContext whose notebook opens on first use."""
32
+
33
+ def __init__(self, notebook_dir: Path, embedder: Any, **kwargs: Any) -> None:
34
+ super().__init__(nb=None, **kwargs)
35
+ self._dir = Path(notebook_dir)
36
+ self._embedder = embedder
37
+ self._open_lock = threading.Lock()
38
+
39
+ def __getattribute__(self, name: str) -> Any:
40
+ if name == "nb":
41
+ nb = object.__getattribute__(self, "__dict__").get("nb")
42
+ if nb is None:
43
+ with object.__getattribute__(self, "_open_lock"):
44
+ nb = object.__getattribute__(self, "__dict__").get("nb")
45
+ if nb is None:
46
+ from ..store.notebook import Notebook
47
+
48
+ nb = Notebook.open(object.__getattribute__(self, "_dir"),
49
+ embedder=object.__getattribute__(self, "_embedder"))
50
+ nb.extractors.update(object.__getattribute__(self, "extractors"))
51
+ object.__getattribute__(self, "__dict__")["nb"] = nb
52
+ return nb
53
+ return object.__getattribute__(self, name)
54
+
55
+
56
+ def bind(notebook_dir: "str | Path", *, embedder: Any = None, artifact_reader: Optional[ArtifactReader] = None,
57
+ extractors: Optional[Dict[str, Any]] = None, allow_paths_under: "str | Path | None" = None,
58
+ account: Optional[str] = None, default_client: str = "agent") -> Dict[str, Callable[..., Any]]:
59
+ """The note tools bound to one notebook directory.
60
+
61
+ - ``embedder``: the notebook's embedding model (None keeps the notebook's own pin).
62
+ - ``artifact_reader(ref) -> (bytes, mime, filename)``: resolves ``{"$artifact": id}`` attachments.
63
+ - ``extractors``: ``{"vision": fn, "stt": fn}`` for image descriptions and transcripts.
64
+ - ``allow_paths_under``: accept ``{"path": ...}`` attachments inside this folder (headless only).
65
+ - ``account``: stamped as ``source.account`` on every captured note.
66
+ """
67
+ from abstractcore.tools.core import tool
68
+
69
+ from . import capture as cap
70
+ from . import process as proc
71
+ from . import queue as que
72
+ from . import recall as rec
73
+
74
+ ctx = _LazyContext(Path(notebook_dir).expanduser(), embedder, artifact_reader=artifact_reader,
75
+ allow_paths_under=Path(allow_paths_under).expanduser() if allow_paths_under else None,
76
+ account=account, default_client=default_client, extractors=dict(extractors or {}))
77
+
78
+ @tool(name="note_capture", description="Save a note (text, time, place, attachments) to the notebook now.",
79
+ when_to_use="Whenever something should be remembered; returns the note id, revisits and similar notes.")
80
+ def note_capture(text: str = "", idempotency_key: Optional[str] = None, title: Optional[str] = None,
81
+ occurred_at: Optional[str] = None, location: Optional[Dict[str, Any]] = None,
82
+ source: Optional[Dict[str, Any]] = None, priority: Optional[int] = None,
83
+ attachments: Optional[List[Dict[str, Any]]] = None, session_id: Optional[str] = None,
84
+ about: Optional[Dict[str, Any]] = None, answer: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
85
+ """Save a note to the notebook now (``answer`` = {question, evidence_note_ids}: a saved answer)."""
86
+ return cap.note_capture(ctx, text, idempotency_key, title, occurred_at, location, source, priority,
87
+ attachments, session_id, about, answer)
88
+
89
+ @tool(name="note_file", description="Store the quick filing of a note: title, kind, topics, urgency, language.")
90
+ def note_file(note_id: str, title: Optional[str] = None, kind: Optional[str] = None,
91
+ topics: Optional[List[str]] = None, urgency: Optional[str] = None, language: Optional[str] = None,
92
+ one_line: Optional[str] = None) -> Dict[str, Any]:
93
+ """Store the quick filing of a note."""
94
+ return cap.note_file(ctx, note_id, title, kind, topics, urgency, language, one_line)
95
+
96
+ @tool(name="note_context", description="What the notebook already knows that is relevant to one note.",
97
+ when_to_use="First step of processing a note: sections, similar notes, candidate entities, open tasks.")
98
+ def note_context(note_id: str, max_chars: Optional[int] = None) -> Dict[str, Any]:
99
+ """What the notebook already knows that is relevant to one note."""
100
+ return proc.note_context(ctx, note_id, max_chars)
101
+
102
+ @tool(name="note_resolve_candidates", description="Existing entities that each mention of a note may refer to.")
103
+ def note_resolve_candidates(note_id: str, mentions: List[Dict[str, Any]]) -> Dict[str, Any]:
104
+ """Existing entities that each mention may refer to."""
105
+ return proc.note_resolve_candidates(ctx, note_id, mentions)
106
+
107
+ @tool(name="note_apply_revision", description="Apply the extraction and linking decisions of a note to the graph.")
108
+ def note_apply_revision(note_id: str, extraction: Dict[str, Any], linking: Dict[str, Any]) -> Dict[str, Any]:
109
+ """Apply the extraction and linking decisions of a note to the graph."""
110
+ return proc.note_apply_revision(ctx, note_id, extraction, linking)
111
+
112
+ @tool(name="note_queue_lease", description="Take the next notes to process from the queue, by priority.")
113
+ def note_queue_lease(worker_id: str, max_items: int = 1) -> Dict[str, Any]:
114
+ """Take the next notes to process from the queue."""
115
+ return que.note_queue_lease(ctx, worker_id, max_items)
116
+
117
+ @tool(name="note_queue_ack", description="Report how processing a leased note went (ok or error).")
118
+ def note_queue_ack(note_id: str, outcome: str, error: Optional[str] = None,
119
+ revision_id: Optional[str] = None) -> Dict[str, Any]:
120
+ """Report how processing a leased note went."""
121
+ return que.note_queue_ack(ctx, note_id, outcome, error or None, revision_id or None)
122
+
123
+ @tool(name="note_queue_stats", description="How many notes wait, are being processed or failed.")
124
+ def note_queue_stats() -> Dict[str, Any]:
125
+ """How many notes wait, are being processed or failed."""
126
+ return que.note_queue_stats(ctx)
127
+
128
+ @tool(name="note_search", description="Quick search of the notes, entities and statements (no model).",
129
+ when_to_use="Before a task or to answer a question cheaply; filters by time, kind, entity and topic.")
130
+ def note_search(query: str = "", since: Optional[str] = None, until: Optional[str] = None,
131
+ kinds: Optional[List[str]] = None, entities: Optional[List[str]] = None,
132
+ topics: Optional[List[str]] = None, limit: int = 20, as_of: Optional[str] = None
133
+ ) -> Dict[str, Any]:
134
+ """Quick search of the notes, entities and statements."""
135
+ return rec.note_search(ctx, query, since, until, kinds, entities, topics, limit, as_of)
136
+
137
+ @tool(name="note_expand", description="Entity cards, timelines, relations and then-versus-now around notes or entities.")
138
+ def note_expand(note_ids: Optional[List[str]] = None, entity_ids: Optional[List[str]] = None,
139
+ as_of: Optional[str] = None) -> Dict[str, Any]:
140
+ """Entity cards, timelines, relations and then-versus-now around notes or entities."""
141
+ return rec.note_expand(ctx, note_ids, entity_ids, as_of)
142
+
143
+ @tool(name="note_changes", description="What the notebook learned since a cursor or a time.",
144
+ when_to_use="First call of a session: new notes and entities, superseded beliefs, closed loops, failures.")
145
+ def note_changes(since: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
146
+ """What the notebook learned since a cursor or a time."""
147
+ return rec.note_changes(ctx, since, limit)
148
+
149
+ @tool(name="note_entity", description="The card of one entity as known at a moment.")
150
+ def note_entity(entity_id: str, at: Optional[str] = None) -> Dict[str, Any]:
151
+ """The card of one entity as known at a moment."""
152
+ return rec.note_entity(ctx, entity_id, at)
153
+
154
+ @tool(name="note_timeline", description="Notebook activity per day, week or month.")
155
+ def note_timeline(since: Optional[str] = None, until: Optional[str] = None,
156
+ granularity: str = "day") -> Dict[str, Any]:
157
+ """Notebook activity per day, week or month."""
158
+ return rec.note_timeline(ctx, since, until, granularity)
159
+
160
+ @tool(name="note_open_loops", description="Open tasks and unanswered questions that may need attention.")
161
+ def note_open_loops(limit: int = 20, since: Optional[str] = None) -> Dict[str, Any]:
162
+ """Open tasks and unanswered questions."""
163
+ return rec.note_open_loops(ctx, limit, since)
164
+
165
+ @tool(name="note_contradictions", description="Disputed statements and subjects with several current values.")
166
+ def note_contradictions(limit: int = 20, since: Optional[str] = None) -> Dict[str, Any]:
167
+ """Disputed statements and subjects with several current values."""
168
+ return rec.note_contradictions(ctx, limit, since)
169
+
170
+ @tool(name="note_resurface", description="Entities and topics gone quiet that relate to recent notes.")
171
+ def note_resurface(limit: int = 20, since: Optional[str] = None) -> Dict[str, Any]:
172
+ """Entities and topics gone quiet that relate to recent notes."""
173
+ return rec.note_resurface(ctx, limit, since)
174
+
175
+ @tool(name="note_anniversaries", description="Notes from this day in earlier months and years.")
176
+ def note_anniversaries(limit: int = 20, since: Optional[str] = None) -> Dict[str, Any]:
177
+ """Notes from this day in earlier months and years."""
178
+ return rec.note_anniversaries(ctx, limit, since)
179
+
180
+ @tool(name="note_insight_write", description="Store an insight or a dossier with the notes and entities behind it.")
181
+ def note_insight_write(kind: str, title: str, body: str, evidence_note_ids: Optional[List[str]] = None,
182
+ entity_ids: Optional[List[str]] = None, run_id: Optional[str] = None) -> Dict[str, Any]:
183
+ """Store an insight or a dossier with its evidence."""
184
+ return rec.note_insight_write(ctx, kind, title, body, evidence_note_ids, entity_ids, run_id)
185
+
186
+ tools = {
187
+ "note_capture": note_capture, "note_file": note_file, "note_context": note_context,
188
+ "note_resolve_candidates": note_resolve_candidates, "note_apply_revision": note_apply_revision,
189
+ "note_queue_lease": note_queue_lease, "note_queue_ack": note_queue_ack, "note_queue_stats": note_queue_stats,
190
+ "note_search": note_search, "note_expand": note_expand, "note_changes": note_changes,
191
+ "note_entity": note_entity, "note_timeline": note_timeline, "note_open_loops": note_open_loops,
192
+ "note_contradictions": note_contradictions, "note_resurface": note_resurface,
193
+ "note_anniversaries": note_anniversaries, "note_insight_write": note_insight_write,
194
+ }
195
+ assert list(tools) == TOOL_NAMES
196
+ return tools
197
+
198
+
199
+ def bound_notebook(tools: Dict[str, Callable[..., Any]]) -> Any:
200
+ """The Notebook behind a bound tool set (opens it when needed)."""
201
+ fn = tools["note_search"]
202
+ for cell in getattr(fn, "__closure__", None) or ():
203
+ value = cell.cell_contents
204
+ if isinstance(value, ToolContext):
205
+ return value.nb
206
+ raise ValueError("These tools were not made by abstractnote.tools.bind.")
207
+
208
+
209
+ def tool_specs() -> List[Dict[str, Any]]:
210
+ """The LLM-facing specs of the note tools (for ``llm_call.tools`` or an agent's catalog)."""
211
+ tools = bind(Path("/nonexistent-abstractnote-spec-only")) # never opened: specs only
212
+ return [tools[name]._tool_definition.to_dict() for name in TOOL_NAMES]
213
+
214
+
215
+ __all__ = ["TOOL_NAMES", "ToolContext", "bind", "bound_notebook", "tool_specs"]
@@ -0,0 +1,121 @@
1
+ """``note_capture`` and ``note_file`` (docs/api.md, "Note tools").
2
+
3
+ Attachments are ``{"$artifact": id, "run_id"?: ...}`` gateway references,
4
+ resolved through the injected ``artifact_reader(ref) -> (bytes, mime,
5
+ filename)``, or ``{"path": ...}`` local files, accepted only when the tools
6
+ were bound with ``allow_paths_under=ROOT`` and the resolved path is inside it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from .context import ToolContext
15
+
16
+
17
+ def resolve_attachments(ctx: ToolContext, attachments: Optional[List[Any]]) -> List[Dict[str, Any]]:
18
+ out: List[Dict[str, Any]] = []
19
+ for i, item in enumerate(attachments or []):
20
+ if not isinstance(item, dict):
21
+ raise ValueError(f"Attachment {i + 1} must be an object with $artifact or path.")
22
+ if "$artifact" in item:
23
+ ref = {"$artifact": str(item["$artifact"])}
24
+ if item.get("run_id"):
25
+ ref["run_id"] = str(item["run_id"])
26
+ if ctx.artifact_reader is None:
27
+ raise ValueError("This notebook cannot read gateway attachments: no artifact reader was bound to "
28
+ "the note tools (abstractnote.tools.bind(..., artifact_reader=...)).")
29
+ data, mime, filename = ctx.artifact_reader(ref)
30
+ if not isinstance(data, (bytes, bytearray)):
31
+ raise ValueError(f"The attachment {ref['$artifact']} could not be read.")
32
+ out.append({"data": bytes(data), "mime": item.get("mime") or mime,
33
+ "filename": item.get("filename") or filename,
34
+ "artifact_ref": {"artifact_id": ref["$artifact"], **({"run_id": ref["run_id"]}
35
+ if "run_id" in ref else {})}})
36
+ elif "path" in item:
37
+ if ctx.allow_paths_under is None:
38
+ raise ValueError("Local file paths are not accepted here; upload the file as an attachment instead.")
39
+ root = Path(ctx.allow_paths_under).expanduser().resolve()
40
+ path = Path(str(item["path"])).expanduser()
41
+ if not path.is_absolute():
42
+ path = root / path
43
+ path = path.resolve()
44
+ if path != root and root not in path.parents:
45
+ raise ValueError("That file is outside the folder this notebook may read from.")
46
+ if not path.is_file():
47
+ raise ValueError(f"There is no file at {item['path']}.")
48
+ out.append({"data": path.read_bytes(), "mime": item.get("mime"), "filename": item.get("filename") or path.name,
49
+ "artifact_ref": None})
50
+ else:
51
+ raise ValueError(f"Attachment {i + 1} must have $artifact (or path when reading local files is allowed).")
52
+ return out
53
+
54
+
55
+ def note_capture(ctx: ToolContext, text: str = "", idempotency_key: Optional[str] = None, title: Optional[str] = None,
56
+ occurred_at: Optional[str] = None, location: Optional[Dict[str, Any]] = None,
57
+ source: Optional[Dict[str, Any]] = None, priority: Optional[int] = None,
58
+ attachments: Optional[List[Any]] = None, session_id: Optional[str] = None,
59
+ about: Optional[Dict[str, Any]] = None, answer: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
60
+ def blank(v: Any) -> Any: # flows pass "" / {} / [] for unset start pins
61
+ return None if v in ("", {}, []) else v
62
+
63
+ saved_answer = _answer(blank(answer))
64
+ resolved = resolve_attachments(ctx, blank(attachments))
65
+ priority = blank(priority)
66
+ result = ctx.nb.capture(
67
+ text, title=blank(title), occurred_at=blank(occurred_at), location=blank(location), source=blank(source),
68
+ priority=None if priority is None else int(priority), attachments=resolved,
69
+ idempotency_key=blank(idempotency_key), session_id=blank(session_id), account=ctx.account, about=blank(about),
70
+ default_client=ctx.default_client, vision=ctx.extractors.get("vision"), stt=ctx.extractors.get("stt"),
71
+ )
72
+ out = result.model_dump()
73
+ if saved_answer is not None and result.status == "new":
74
+ out["warnings"] = list(out.get("warnings") or []) + _file_answer(ctx.nb, result.note_id, saved_answer)
75
+ return out
76
+
77
+
78
+ def _answer(answer: Any) -> Optional[Dict[str, Any]]:
79
+ """``context.answer = {question, evidence_note_ids}`` (the web app's "Save this answer"), checked."""
80
+ if answer is None:
81
+ return None
82
+ if not isinstance(answer, dict) or not str(answer.get("question") or "").strip():
83
+ raise ValueError("A saved answer needs the question it answers.")
84
+ evidence = answer.get("evidence_note_ids") or []
85
+ if not isinstance(evidence, list) or not all(isinstance(x, str) for x in evidence):
86
+ raise ValueError("The evidence of a saved answer must be a list of note ids.")
87
+ return {"question": str(answer["question"]).strip(), "evidence_note_ids": list(dict.fromkeys(evidence))}
88
+
89
+
90
+ def _file_answer(nb: Any, note_id: str, answer: Dict[str, Any]) -> List[str]:
91
+ """File a saved answer as a ``synthesis`` note linked (``dcterms:references``) to each evidence
92
+ note, the same link ``context.about`` makes; evidence ids outside the notebook are left out."""
93
+ from .. import ids
94
+ from ..store.notes import _j
95
+
96
+ warnings: List[str] = []
97
+ now = ids.utc_now()
98
+ detail = _j({"kind": "answer", "question": answer["question"]})
99
+ with nb.store.tx() as c:
100
+ c.execute("UPDATE notes SET kind='synthesis' WHERE note_id=?", (note_id,))
101
+ for nid in answer["evidence_note_ids"]:
102
+ if nid == note_id or nb.store.note_row(nid) is None:
103
+ warnings.append(f"The evidence note {nid} is not in this notebook; it was left out.")
104
+ continue
105
+ c.execute("INSERT OR IGNORE INTO note_links(src_note_id, dst_note_id, predicate, score, matched_by,"
106
+ " detail_json, created_at) VALUES (?,?,?,1.0,?,?,?)",
107
+ (note_id, nid, "dcterms:references", "answer", detail, now))
108
+ return warnings
109
+
110
+
111
+ def note_file(ctx: ToolContext, note_id: str, title: Optional[str] = None, kind: Optional[str] = None,
112
+ topics: Optional[List[str]] = None, urgency: Optional[str] = None, language: Optional[str] = None,
113
+ one_line: Optional[str] = None) -> Dict[str, Any]:
114
+ def blank(v: Any) -> Any:
115
+ return None if v in ("", {}, []) else v
116
+
117
+ row = ctx.nb.store.note_row(note_id)
118
+ if row is not None and row["kind"] == "synthesis":
119
+ kind = None # a saved answer stays a synthesis whatever the quick filing proposes
120
+ return ctx.nb.file_note(note_id, title=blank(title), kind=blank(kind), topics=topics or [],
121
+ urgency=blank(urgency), language=blank(language), one_line=blank(one_line))
@@ -0,0 +1,19 @@
1
+ """What a bound tool set knows: the notebook, the injected readers and the account."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, Callable, Dict, Optional, Tuple
8
+
9
+ ArtifactReader = Callable[[Dict[str, Any]], Tuple[bytes, Optional[str], Optional[str]]]
10
+
11
+
12
+ @dataclass
13
+ class ToolContext:
14
+ nb: Any # abstractnote.store.notebook.Notebook
15
+ artifact_reader: Optional[ArtifactReader] = None
16
+ allow_paths_under: Optional[Path] = None
17
+ account: Optional[str] = None
18
+ default_client: str = "agent"
19
+ extractors: Dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,22 @@
1
+ """``note_context``, ``note_resolve_candidates``, ``note_apply_revision`` (docs/api.md)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from ..process.context import note_context as _context
8
+ from ..process.resolve import resolve_candidates
9
+ from ..process.revise import apply_note_revision
10
+ from .context import ToolContext
11
+
12
+
13
+ def note_context(ctx: ToolContext, note_id: str, max_chars: Optional[int] = None) -> Dict[str, Any]:
14
+ return _context(ctx.nb, note_id, max_chars)
15
+
16
+
17
+ def note_resolve_candidates(ctx: ToolContext, note_id: str, mentions: List[Dict[str, Any]]) -> Dict[str, Any]:
18
+ return resolve_candidates(ctx.nb, note_id, mentions)
19
+
20
+
21
+ def note_apply_revision(ctx: ToolContext, note_id: str, extraction: Any, linking: Dict[str, Any]) -> Dict[str, Any]:
22
+ return apply_note_revision(ctx.nb, note_id, extraction, linking)
@@ -0,0 +1,23 @@
1
+ """``note_queue_lease``, ``note_queue_ack``, ``note_queue_stats`` (docs/api.md)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ from .context import ToolContext
8
+
9
+
10
+ def note_queue_lease(ctx: ToolContext, worker_id: str, max_items: int = 1) -> Dict[str, Any]:
11
+ out = ctx.nb.queue.lease(worker_id, max_items)
12
+ return {"items": [{"note_id": i["note_id"], "priority": i["priority"], "attempts": i["attempts"]}
13
+ for i in out["items"]]}
14
+
15
+
16
+ def note_queue_ack(ctx: ToolContext, note_id: str, outcome: str, error: Optional[str] = None,
17
+ revision_id: Optional[str] = None) -> Dict[str, Any]:
18
+ return ctx.nb.queue.ack(note_id, outcome, error=error, revision_id=revision_id)
19
+
20
+
21
+ def note_queue_stats(ctx: ToolContext) -> Dict[str, Any]:
22
+ stats = ctx.nb.queue.stats()
23
+ return {k: stats[k] for k in ("queued", "processing", "failed", "by_priority", "oldest_enqueued_at")}
@@ -0,0 +1,64 @@
1
+ """Recall and reflect tools (docs/api.md): note_search, note_expand, note_changes, note_entity,
2
+ note_timeline, note_open_loops, note_contradictions, note_resurface, note_anniversaries,
3
+ note_insight_write."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from ..recall import expand as _expand
10
+ from ..recall import reflect as _reflect
11
+ from ..recall.insights import write_insight
12
+ from ..recall.search import search
13
+ from .context import ToolContext
14
+
15
+
16
+ def _blank(v: Any) -> Any:
17
+ return None if v in ("", {}, []) else v
18
+
19
+
20
+ def note_search(ctx: ToolContext, query: str = "", since: Optional[str] = None, until: Optional[str] = None,
21
+ kinds: Optional[List[str]] = None, entities: Optional[List[str]] = None,
22
+ topics: Optional[List[str]] = None, limit: int = 20, as_of: Optional[str] = None) -> Dict[str, Any]:
23
+ return search(ctx.nb, query or "", since=_blank(since), until=_blank(until), kinds=_blank(kinds),
24
+ entities=_blank(entities), topics=_blank(topics), limit=int(limit or 20), as_of=_blank(as_of))
25
+
26
+
27
+ def note_expand(ctx: ToolContext, note_ids: Optional[List[str]] = None, entity_ids: Optional[List[str]] = None,
28
+ as_of: Optional[str] = None) -> Dict[str, Any]:
29
+ return _expand.expand(ctx.nb, note_ids or [], entity_ids or [], _blank(as_of))
30
+
31
+
32
+ def note_changes(ctx: ToolContext, since: Optional[str] = None, limit: int = 50) -> Dict[str, Any]:
33
+ return ctx.nb.changes(_blank(since), int(limit or 50))
34
+
35
+
36
+ def note_entity(ctx: ToolContext, entity_id: str, at: Optional[str] = None) -> Dict[str, Any]:
37
+ return _expand.entity_card(ctx.nb, entity_id, _blank(at))
38
+
39
+
40
+ def note_timeline(ctx: ToolContext, since: Optional[str] = None, until: Optional[str] = None,
41
+ granularity: str = "day") -> Dict[str, Any]:
42
+ return ctx.nb.timeline(_blank(since), _blank(until), granularity or "day")
43
+
44
+
45
+ def note_open_loops(ctx: ToolContext, limit: int = 20, since: Optional[str] = None) -> Dict[str, Any]:
46
+ return _reflect.open_loops(ctx.nb, int(limit or 20), _blank(since))
47
+
48
+
49
+ def note_contradictions(ctx: ToolContext, limit: int = 20, since: Optional[str] = None) -> Dict[str, Any]:
50
+ return _reflect.contradictions(ctx.nb, int(limit or 20), _blank(since))
51
+
52
+
53
+ def note_resurface(ctx: ToolContext, limit: int = 20, since: Optional[str] = None) -> Dict[str, Any]:
54
+ return _reflect.resurface(ctx.nb, int(limit or 20), _blank(since))
55
+
56
+
57
+ def note_anniversaries(ctx: ToolContext, limit: int = 20, since: Optional[str] = None) -> Dict[str, Any]:
58
+ return _reflect.anniversaries(ctx.nb, int(limit or 20), _blank(since))
59
+
60
+
61
+ def note_insight_write(ctx: ToolContext, kind: str, title: str, body: str,
62
+ evidence_note_ids: Optional[List[str]] = None, entity_ids: Optional[List[str]] = None,
63
+ run_id: Optional[str] = None) -> Dict[str, Any]:
64
+ return write_insight(ctx.nb, kind, title, body, evidence_note_ids or [], entity_ids or [], _blank(run_id))
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: abstractnote
3
+ Version: 0.1.0
4
+ Summary: AbstractNote: one endpoint for your notes; a temporal semantic knowledge graph of your life, built on AbstractGateway, AbstractRuntime and AbstractMemory
5
+ Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/lpalbou/abstractnote
8
+ Project-URL: Repository, https://github.com/lpalbou/abstractnote
9
+ Project-URL: Issues, https://github.com/lpalbou/abstractnote/issues
10
+ Keywords: notes,knowledge-graph,memory,temporal,abstractframework
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: abstractmemory>=0.3.1
22
+ Requires-Dist: abstractsemantics>=0.0.5
23
+ Requires-Dist: abstractruntime>=0.10.2
24
+ Requires-Dist: abstractcore>=2.26.3
25
+ Requires-Dist: pydantic>=2.0
26
+ Provides-Extra: gateway
27
+ Requires-Dist: abstractgateway>=0.15.11; extra == "gateway"
28
+ Requires-Dist: fastapi>=0.110; extra == "gateway"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.1; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # AbstractNote
35
+
36
+ One endpoint for everything you want to remember. Send a sentence, a document, a photo, a
37
+ recording, a log. AbstractNote files it with its time, place and provenance, links it to what
38
+ you already know, and grows a **temporal semantic knowledge graph** of your ideas, projects,
39
+ people and decisions. You search it by effort (quick, standard, deep) and revisit it through
40
+ time: what you knew, when you learned it, and how your view changed.
41
+
42
+ AbstractNote is part of the AbstractFramework. It runs on **AbstractGateway** (sign-in, one
43
+ runtime and one notebook per user, app hosting), **AbstractRuntime** and **AbstractFlow** (the
44
+ capture and understanding workflows), **AbstractMemory** (temporal facts) and **AbstractCore**
45
+ (models, structured output, embeddings).
46
+
47
+ Status: version 0.1.0, in development. It is not published on PyPI or npm yet; you install it
48
+ from source (see [Install](#install)).
49
+
50
+ ## What you get
51
+
52
+ - **Capture anywhere**: the web app, the Apple Watch app, the `abstractnote` command line, and
53
+ agents. Attachments are de-duplicated by content; a retried send never saves a note twice.
54
+ - **Automatic filing**: a small fast model gives each note a title, a kind, topics, an urgency
55
+ and its language, next to the time and place it was made.
56
+ - **Self-organising knowledge**: a model resolves people, projects and ideas against candidates
57
+ that exact lookups, full-text and vectors produce. Every change is a revision you can replay,
58
+ and every claim keeps the note it came from.
59
+ - **Remember by cue and effort**: quick (no model, under a second), standard (one synthesis with
60
+ citations), deep (a background dossier). Then-versus-now for any person, project, idea or link.
61
+ - **Reflection**: open loops, contradictions, things that went quiet and connections you may have
62
+ missed, surfaced as insights.
63
+ - **Imports**: Claude Code sessions, an agora hub database, Markdown trees and plain files, with
64
+ their original dates and provenance.
65
+
66
+ ## Install
67
+
68
+ AbstractNote needs Python 3.10 or later and the framework packages next to it. Until the
69
+ packages are published, install everything from source checkouts:
70
+
71
+ ```sh
72
+ python3 -m venv .venv
73
+ .venv/bin/pip install -e ../abstractsemantics -e ../abstractcore -e ../abstractruntime \
74
+ -e ../abstractmemory # a checkout that has close_assertion, see below
75
+ .venv/bin/pip install -e .
76
+ .venv/bin/abstractnote --help
77
+ ```
78
+
79
+ Two framework changes that AbstractNote relies on are not in released packages yet:
80
+
81
+ - **AbstractMemory `close_assertion`** (the `close-assertion` branch): closes a superseded fact in
82
+ place. AbstractNote's first release waits for the AbstractMemory release that contains it.
83
+ - **The AbstractGateway integration** (the `abstractnote-seam` branch): binds the note tools to
84
+ each user's notebook, serves the note read API and lists the Note app. You need it only for the
85
+ gateway path; the headless notebook works without a gateway. The gateway must carry this seam
86
+ (`abstractgateway.abstractnote_seam`; the first gateway release with it is named at
87
+ AbstractNote's release): the `[gateway]` extra alone does not guarantee it, and
88
+ `abstractnote.gateway` refuses to import into a gateway without it, with one sentence.
89
+
90
+ To run models locally on Apple silicon, add `pip install "abstractcore[apple]"` (MLX).
91
+
92
+ ## Quick start
93
+
94
+ A headless notebook on this machine, no gateway and no model:
95
+
96
+ ```sh
97
+ abstractnote capture "Met Dana about the LUT budget; she wants the Pareto by Friday." --notebook ~/notes
98
+ abstractnote search "LUT budget" --notebook ~/notes
99
+ abstractnote status --notebook ~/notes
100
+ ```
101
+
102
+ Let a model understand the queued notes (entities, claims, tasks), then ask a question:
103
+
104
+ ```sh
105
+ abstractnote process --local --notebook ~/notes --provider mlx --model mlx-community/Qwen3.6-35B-A3B-4bit
106
+ abstractnote search "What does Dana want?" --effort standard --notebook ~/notes \
107
+ --provider mlx --model mlx-community/Qwen3.6-35B-A3B-4bit
108
+ ```
109
+
110
+ A notebook is pinned to the embedding model that created it. Choose it on the first command
111
+ (`--embed-base-url` and `--embed-model`, for example a local LM Studio embedding model) and give
112
+ the same flags every time, or start with the built-in offline embedder and change later with
113
+ `abstractnote rebuild --reembed`. See [docs/getting-started.md](docs/getting-started.md).
114
+
115
+ Through a gateway, clients start the `note-capture` and `note-recall` flows with the gateway's
116
+ runs API, and `POST /api/gateway/note/processor/ensure` creates the per-user processor that
117
+ understands notes in the background. The getting-started guide walks through a private scratch
118
+ gateway, the web app and the watch app.
119
+
120
+ ## Clients
121
+
122
+ | Client | Where | What it does |
123
+ |---|---|---|
124
+ | Web app | [`web/`](web/) (`@abstractframework/note`) | Capture (a chat with attachments), Graph (entities and links through time), Remember (since you last looked, cue cards, ask by effort, then and now), Settings |
125
+ | Apple Watch app | [`watch/apple/`](watch/apple/README.md) | Note, Ask, Today, Settings; watch-only, watchOS 26 or later |
126
+ | Command line | `abstractnote` | Headless capture, search, processing, imports, rebuild, JSON output for scripts and agents |
127
+
128
+ An iPhone app is planned ([`phone/iphone/`](phone/iphone/README.md)). Agents use the same note
129
+ tools and flows, with the effort level as a parameter.
130
+
131
+ ## Documentation
132
+
133
+ - [docs/README.md](docs/README.md): the documentation index
134
+ - [Getting started](docs/getting-started.md): a headless notebook in five minutes, then the gateway path
135
+ - [Architecture](docs/architecture.md): components, data plane, request lifecycles
136
+ - [API](docs/api.md): runs API usage, note read API, note tools, Python API, CLI
137
+ - [Data model](docs/data-model.md), [Cognition](docs/cognition.md), [Remembering](docs/remembering.md),
138
+ [Clients](docs/clients.md), [Importers](docs/importers.md)
139
+ - [FAQ](docs/faq.md) and [Troubleshooting](docs/troubleshooting.md)
140
+ - [Contributing](CONTRIBUTING.md), [Security](SECURITY.md), [Changelog](CHANGELOG.md),
141
+ [Acknowledgements](ACKNOWLEDGEMENTS.md)
142
+
143
+ ## License
144
+
145
+ MIT. See [LICENSE](LICENSE).