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.
- mnemonics-0.1.0/PKG-INFO +152 -0
- mnemonics-0.1.0/README.md +127 -0
- mnemonics-0.1.0/mnemonics/__init__.py +7 -0
- mnemonics-0.1.0/mnemonics/cli.py +82 -0
- mnemonics-0.1.0/mnemonics/ingest.py +63 -0
- mnemonics-0.1.0/mnemonics/retrieve.py +66 -0
- mnemonics-0.1.0/mnemonics/server.py +232 -0
- mnemonics-0.1.0/mnemonics/store.py +110 -0
- mnemonics-0.1.0/mnemonics.egg-info/PKG-INFO +152 -0
- mnemonics-0.1.0/mnemonics.egg-info/SOURCES.txt +19 -0
- mnemonics-0.1.0/mnemonics.egg-info/dependency_links.txt +1 -0
- mnemonics-0.1.0/mnemonics.egg-info/entry_points.txt +2 -0
- mnemonics-0.1.0/mnemonics.egg-info/requires.txt +15 -0
- mnemonics-0.1.0/mnemonics.egg-info/top_level.txt +1 -0
- mnemonics-0.1.0/pyproject.toml +33 -0
- mnemonics-0.1.0/setup.cfg +4 -0
- mnemonics-0.1.0/tests/test_cli.py +184 -0
- mnemonics-0.1.0/tests/test_ingest.py +136 -0
- mnemonics-0.1.0/tests/test_retrieve.py +175 -0
- mnemonics-0.1.0/tests/test_server.py +305 -0
- mnemonics-0.1.0/tests/test_store.py +170 -0
mnemonics-0.1.0/PKG-INFO
ADDED
|
@@ -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,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
|
+
}
|