mnemonics 0.1.0__tar.gz

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.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: mnemonics
3
+ Version: 0.1.0
4
+ Summary: Verified AI memory — retrieval that doesn't hallucinate.
5
+ Author: atakan
6
+ License-Expression: MIT
7
+ Keywords: llm,memory,retrieval,rag,hallucination,mcp,agent
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: numpy>=1.24
14
+ Requires-Dist: hnswlib>=0.8
15
+ Requires-Dist: sentence-transformers>=3.0
16
+ Requires-Dist: requests>=2.28
17
+ Provides-Extra: verify
18
+ Requires-Dist: halluguard>=0.3; extra == "verify"
19
+ Provides-Extra: server
20
+ Requires-Dist: halluguard>=0.3; extra == "server"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == "dev"
23
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
24
+ Requires-Dist: ruff>=0.5; extra == "dev"
25
+
26
+ # Mnemonics
27
+
28
+ **Your memory doesn't hallucinate.**
29
+
30
+ Mnemonics is a local-first AI memory layer that stores, retrieves, and *verifies* what it returns. Built on sentence embeddings + HNSW vector search, with optional [halluguard](https://github.com/nakata-app/halluguard) verification to flag results that drift from the indexed corpus.
31
+
32
+ ## Why
33
+
34
+ Every RAG pipeline has the same silent failure mode: the retriever returns plausible-looking chunks, the LLM fills in the gaps, and nobody notices the fabrication until it matters. Mnemonics surfaces that problem at retrieval time, not after.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install mnemonics
40
+ # with verification support:
41
+ pip install "mnemonics[verify]"
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```bash
47
+ # Store something
48
+ mnemonics ingest "The Eiffel Tower is 330 meters tall and located in Paris."
49
+
50
+ # Retrieve with hallucination check
51
+ mnemonics retrieve "how tall is the Eiffel Tower"
52
+ # trust_score: 1.0 flagged: 0
53
+ # [0.912] The Eiffel Tower is 330 meters tall and located in Paris.
54
+ ```
55
+
56
+ ## Python API
57
+
58
+ ```python
59
+ from mnemonics.store import Store
60
+ from mnemonics.ingest import ingest
61
+ from mnemonics.retrieve import retrieve
62
+
63
+ store = Store("~/.mnemonics")
64
+
65
+ ingest(["Paris is the capital of France.", "Rome is the capital of Italy."], store)
66
+
67
+ result = retrieve("what is the capital of France", store, top_k=3, verify=True)
68
+ for r in result["results"]:
69
+ flag = " FLAGGED" if r["flagged"] else ""
70
+ print(f"[{r['score']:.3f}]{flag} {r['text']}")
71
+
72
+ print(f"trust_score: {result['trust_score']}")
73
+ ```
74
+
75
+ ## REST server
76
+
77
+ ```bash
78
+ mnemonics serve --port 7810
79
+ ```
80
+
81
+ | Method | Path | Body |
82
+ |--------|------|------|
83
+ | POST | `/ingest` | `{"texts": [...], "ns": "default"}` |
84
+ | POST | `/retrieve` | `{"query": "...", "top_k": 5, "verify": true}` |
85
+ | GET | `/health` | |
86
+ | GET | `/namespaces` | |
87
+ | GET | `/count?ns=default` | |
88
+ | DELETE | `/memory/<id>` | |
89
+
90
+ ## MCP (Claude Code / Cursor / Metis)
91
+
92
+ ```bash
93
+ mnemonics mcp
94
+ ```
95
+
96
+ Add to your MCP config:
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "mnemonics": {
102
+ "command": "mnemonics",
103
+ "args": ["mcp"]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ Tools exposed: `mnemonics_ingest`, `mnemonics_retrieve`, `mnemonics_forget`
110
+
111
+ ## Namespaces
112
+
113
+ Isolate memories by project, user, or any key:
114
+
115
+ ```bash
116
+ mnemonics ingest "project notes..." --ns work
117
+ mnemonics retrieve "deadlines" --ns work
118
+ ```
119
+
120
+ ## Architecture
121
+
122
+ ```
123
+ texts -> chunk (200w / 40w overlap) -> embed (all-MiniLM-L6-v2)
124
+ -> hnswlib cosine index (per namespace)
125
+ -> SQLite metadata store
126
+
127
+ retrieve -> embed query -> knn search -> halluguard verify -> ranked results
128
+ ```
129
+
130
+ Storage layout under `~/.mnemonics`:
131
+
132
+ ```
133
+ memories.db SQLite (text, meta, timestamps)
134
+ index_default.bin hnswlib index for "default" namespace
135
+ index_<ns>.bin one index per namespace
136
+ ```
137
+
138
+ ## Verification
139
+
140
+ When `verify=True`, retrieved chunks are sent to a local halluguard daemon (port 7801) which cross-checks each result against the full retrieved corpus. Results that diverge get flagged and the aggregate `trust_score` drops.
141
+
142
+ ```bash
143
+ pip install "mnemonics[verify]"
144
+ halluguard serve &
145
+ mnemonics retrieve "your query" # auto-verifies
146
+ ```
147
+
148
+ Verification is best-effort: if the daemon is not running, retrieval proceeds normally with `trust_score: 1.0`.
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,127 @@
1
+ # Mnemonics
2
+
3
+ **Your memory doesn't hallucinate.**
4
+
5
+ Mnemonics is a local-first AI memory layer that stores, retrieves, and *verifies* what it returns. Built on sentence embeddings + HNSW vector search, with optional [halluguard](https://github.com/nakata-app/halluguard) verification to flag results that drift from the indexed corpus.
6
+
7
+ ## Why
8
+
9
+ Every RAG pipeline has the same silent failure mode: the retriever returns plausible-looking chunks, the LLM fills in the gaps, and nobody notices the fabrication until it matters. Mnemonics surfaces that problem at retrieval time, not after.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install mnemonics
15
+ # with verification support:
16
+ pip install "mnemonics[verify]"
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```bash
22
+ # Store something
23
+ mnemonics ingest "The Eiffel Tower is 330 meters tall and located in Paris."
24
+
25
+ # Retrieve with hallucination check
26
+ mnemonics retrieve "how tall is the Eiffel Tower"
27
+ # trust_score: 1.0 flagged: 0
28
+ # [0.912] The Eiffel Tower is 330 meters tall and located in Paris.
29
+ ```
30
+
31
+ ## Python API
32
+
33
+ ```python
34
+ from mnemonics.store import Store
35
+ from mnemonics.ingest import ingest
36
+ from mnemonics.retrieve import retrieve
37
+
38
+ store = Store("~/.mnemonics")
39
+
40
+ ingest(["Paris is the capital of France.", "Rome is the capital of Italy."], store)
41
+
42
+ result = retrieve("what is the capital of France", store, top_k=3, verify=True)
43
+ for r in result["results"]:
44
+ flag = " FLAGGED" if r["flagged"] else ""
45
+ print(f"[{r['score']:.3f}]{flag} {r['text']}")
46
+
47
+ print(f"trust_score: {result['trust_score']}")
48
+ ```
49
+
50
+ ## REST server
51
+
52
+ ```bash
53
+ mnemonics serve --port 7810
54
+ ```
55
+
56
+ | Method | Path | Body |
57
+ |--------|------|------|
58
+ | POST | `/ingest` | `{"texts": [...], "ns": "default"}` |
59
+ | POST | `/retrieve` | `{"query": "...", "top_k": 5, "verify": true}` |
60
+ | GET | `/health` | |
61
+ | GET | `/namespaces` | |
62
+ | GET | `/count?ns=default` | |
63
+ | DELETE | `/memory/<id>` | |
64
+
65
+ ## MCP (Claude Code / Cursor / Metis)
66
+
67
+ ```bash
68
+ mnemonics mcp
69
+ ```
70
+
71
+ Add to your MCP config:
72
+
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "mnemonics": {
77
+ "command": "mnemonics",
78
+ "args": ["mcp"]
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ Tools exposed: `mnemonics_ingest`, `mnemonics_retrieve`, `mnemonics_forget`
85
+
86
+ ## Namespaces
87
+
88
+ Isolate memories by project, user, or any key:
89
+
90
+ ```bash
91
+ mnemonics ingest "project notes..." --ns work
92
+ mnemonics retrieve "deadlines" --ns work
93
+ ```
94
+
95
+ ## Architecture
96
+
97
+ ```
98
+ texts -> chunk (200w / 40w overlap) -> embed (all-MiniLM-L6-v2)
99
+ -> hnswlib cosine index (per namespace)
100
+ -> SQLite metadata store
101
+
102
+ retrieve -> embed query -> knn search -> halluguard verify -> ranked results
103
+ ```
104
+
105
+ Storage layout under `~/.mnemonics`:
106
+
107
+ ```
108
+ memories.db SQLite (text, meta, timestamps)
109
+ index_default.bin hnswlib index for "default" namespace
110
+ index_<ns>.bin one index per namespace
111
+ ```
112
+
113
+ ## Verification
114
+
115
+ When `verify=True`, retrieved chunks are sent to a local halluguard daemon (port 7801) which cross-checks each result against the full retrieved corpus. Results that diverge get flagged and the aggregate `trust_score` drops.
116
+
117
+ ```bash
118
+ pip install "mnemonics[verify]"
119
+ halluguard serve &
120
+ mnemonics retrieve "your query" # auto-verifies
121
+ ```
122
+
123
+ Verification is best-effort: if the daemon is not running, retrieval proceeds normally with `trust_score: 1.0`.
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,7 @@
1
+ """mnemonics — verified AI memory. Retrieval that doesn't hallucinate."""
2
+ from mnemonics.store import Store
3
+ from mnemonics.ingest import ingest
4
+ from mnemonics.retrieve import retrieve
5
+
6
+ __all__ = ["Store", "ingest", "retrieve"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,82 @@
1
+ """mnemonics CLI."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+
8
+
9
+ def main() -> None:
10
+ p = argparse.ArgumentParser(prog="mnemonics")
11
+ sub = p.add_subparsers(dest="cmd")
12
+
13
+ # serve
14
+ s = sub.add_parser("serve", help="Start REST server")
15
+ s.add_argument("--port", type=int, default=7810)
16
+ s.add_argument("--path", default="~/.mnemonics")
17
+
18
+ # mcp
19
+ sub.add_parser("mcp", help="Start MCP stdio server")
20
+
21
+ # ingest
22
+ i = sub.add_parser("ingest", help="Add text to memory")
23
+ i.add_argument("text", nargs="+")
24
+ i.add_argument("--ns", default="default")
25
+ i.add_argument("--path", default="~/.mnemonics")
26
+
27
+ # retrieve
28
+ r = sub.add_parser("retrieve", help="Search memory")
29
+ r.add_argument("query")
30
+ r.add_argument("--ns", default="default")
31
+ r.add_argument("--top-k", type=int, default=5)
32
+ r.add_argument("--no-verify", action="store_true")
33
+ r.add_argument("--path", default="~/.mnemonics")
34
+
35
+ # stats
36
+ st = sub.add_parser("stats", help="Show memory stats")
37
+ st.add_argument("--path", default="~/.mnemonics")
38
+
39
+ args = p.parse_args()
40
+
41
+ if args.cmd == "serve":
42
+ import os
43
+ os.environ["MNEMONICS_PATH"] = args.path
44
+ from mnemonics.server import serve
45
+ serve(port=args.port)
46
+
47
+ elif args.cmd == "mcp":
48
+ from mnemonics.server import serve
49
+ serve(mcp=True)
50
+
51
+ elif args.cmd == "ingest":
52
+ from mnemonics.store import Store
53
+ from mnemonics.ingest import ingest
54
+ store = Store(args.path)
55
+ n = ingest(texts=[" ".join(args.text)], store=store, ns=args.ns)
56
+ print(f"Stored {n} chunk(s).")
57
+
58
+ elif args.cmd == "retrieve":
59
+ from mnemonics.store import Store
60
+ from mnemonics.retrieve import retrieve
61
+ store = Store(args.path)
62
+ result = retrieve(
63
+ query=args.query,
64
+ store=store,
65
+ ns=args.ns,
66
+ top_k=args.top_k,
67
+ verify=not args.no_verify,
68
+ )
69
+ print(f"trust_score: {result['trust_score']} flagged: {result['flagged_count']}")
70
+ for r in result["results"]:
71
+ flag = " ⚠" if r.get("flagged") else ""
72
+ print(f" [{r['score']:.3f}]{flag} {r['text'][:120]}")
73
+
74
+ elif args.cmd == "stats":
75
+ from mnemonics.store import Store
76
+ store = Store(args.path)
77
+ for ns in store.list_namespaces():
78
+ print(f" {ns}: {store.count(ns)} chunks")
79
+
80
+ else:
81
+ p.print_help()
82
+ sys.exit(1)
@@ -0,0 +1,63 @@
1
+ """Ingest text into the store: chunk → embed → save."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+
9
+ from mnemonics.store import Store
10
+
11
+ _encoder: Any = None
12
+ _encoder_name: str = "all-MiniLM-L6-v2"
13
+
14
+
15
+ def _get_encoder(model: str = _encoder_name) -> Any:
16
+ global _encoder, _encoder_name
17
+ if _encoder is None or model != _encoder_name:
18
+ from sentence_transformers import SentenceTransformer
19
+ _encoder = SentenceTransformer(model)
20
+ _encoder_name = model
21
+ return _encoder
22
+
23
+
24
+ def _chunk(text: str, size: int = 200, overlap: int = 40) -> list[str]:
25
+ words = text.split()
26
+ if len(words) <= size:
27
+ return [text]
28
+ chunks = []
29
+ i = 0
30
+ while i < len(words):
31
+ chunk = " ".join(words[i:i + size])
32
+ chunks.append(chunk)
33
+ i += size - overlap
34
+ return chunks
35
+
36
+
37
+ def ingest(
38
+ texts: list[str],
39
+ store: Store,
40
+ ns: str = "default",
41
+ meta: list[dict] | None = None,
42
+ model: str = "all-MiniLM-L6-v2",
43
+ chunk_size: int = 200,
44
+ chunk_overlap: int = 40,
45
+ ) -> int:
46
+ """Chunk, embed and store texts. Returns total chunks stored."""
47
+ enc = _get_encoder(model)
48
+ all_chunks: list[str] = []
49
+ all_meta: list[dict] = []
50
+
51
+ for i, text in enumerate(texts):
52
+ chunks = _chunk(text, chunk_size, chunk_overlap)
53
+ m = (meta[i] if meta else {}) | {"source_idx": i}
54
+ all_chunks.extend(chunks)
55
+ all_meta.extend([m] * len(chunks))
56
+
57
+ if not all_chunks:
58
+ return 0
59
+
60
+ vecs = enc.encode(all_chunks, batch_size=64, show_progress_bar=False,
61
+ normalize_embeddings=True, convert_to_numpy=True)
62
+ store.add(all_chunks, vecs, ns=ns, meta=all_meta)
63
+ return len(all_chunks)
@@ -0,0 +1,66 @@
1
+ """Retrieve memories — embed query, search, optionally verify with halluguard."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+ from mnemonics.store import Store
9
+ from mnemonics.ingest import _get_encoder
10
+
11
+
12
+ def retrieve(
13
+ query: str,
14
+ store: Store,
15
+ ns: str = "default",
16
+ top_k: int = 5,
17
+ model: str = "all-MiniLM-L6-v2",
18
+ verify: bool = True,
19
+ verify_threshold: float = 0.45,
20
+ ) -> dict[str, Any]:
21
+ """
22
+ Search the store for query.
23
+ If verify=True, runs halluguard on results against the retrieved corpus.
24
+ Returns: {results, verified, trust_score, flagged_count}
25
+ """
26
+ enc = _get_encoder(model)
27
+ qvec = enc.encode([query], normalize_embeddings=True, convert_to_numpy=True)[0]
28
+ results = store.search(qvec, ns=ns, top_k=top_k)
29
+
30
+ if not results:
31
+ return {"results": [], "verified": True, "trust_score": 1.0, "flagged_count": 0}
32
+
33
+ trust_score = 1.0
34
+ flagged_count = 0
35
+ verified = True
36
+
37
+ if verify:
38
+ try:
39
+ corpus = [r["text"] for r in results]
40
+ combined = " ".join(corpus)
41
+ resp = requests.post(
42
+ "http://127.0.0.1:7801/check",
43
+ json={"corpus": corpus, "answer": combined, "threshold": verify_threshold},
44
+ timeout=10,
45
+ )
46
+ if resp.ok:
47
+ data = resp.json()
48
+ trust_score = data.get("trust_score", 1.0)
49
+ flagged_count = data.get("n_flagged", 0)
50
+ verified = data.get("ok", True)
51
+ # Mark flagged results
52
+ flagged_texts = {f["text"] for f in data.get("flagged", [])}
53
+ for r in results:
54
+ r["flagged"] = r["text"] in flagged_texts
55
+ except Exception:
56
+ pass # halluguard daemon not running — skip verify
57
+
58
+ for r in results:
59
+ r.setdefault("flagged", False)
60
+
61
+ return {
62
+ "results": results,
63
+ "verified": verified,
64
+ "trust_score": round(trust_score, 4),
65
+ "flagged_count": flagged_count,
66
+ }