markdown-memory-vec 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.
- markdown_memory_vec-0.1.0.dist-info/METADATA +219 -0
- markdown_memory_vec-0.1.0.dist-info/RECORD +13 -0
- markdown_memory_vec-0.1.0.dist-info/WHEEL +4 -0
- markdown_memory_vec-0.1.0.dist-info/entry_points.txt +2 -0
- markdown_memory_vec-0.1.0.dist-info/licenses/LICENSE +21 -0
- memory_vec/__init__.py +73 -0
- memory_vec/__main__.py +109 -0
- memory_vec/embedder.py +137 -0
- memory_vec/indexer.py +307 -0
- memory_vec/interfaces.py +118 -0
- memory_vec/search.py +234 -0
- memory_vec/service.py +326 -0
- memory_vec/store.py +470 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: markdown-memory-vec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight vector search for Markdown-based memory systems. Chunk, embed, index, and hybrid-search your .md knowledge base with sqlite-vec.
|
|
5
|
+
Project-URL: Homepage, https://github.com/aigente/markdown-memory-vec
|
|
6
|
+
Project-URL: Repository, https://github.com/aigente/markdown-memory-vec
|
|
7
|
+
Author-email: Aigente <dev@aigente.io>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: RAG,embeddings,markdown,memory,semantic-search,sqlite-vec,vector-search
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pyright>=1.1.380; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
29
|
+
Provides-Extra: vector
|
|
30
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'vector'
|
|
31
|
+
Requires-Dist: sqlite-vec>=0.1.6; extra == 'vector'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# markdown-memory-vec
|
|
35
|
+
|
|
36
|
+
Lightweight vector search for Markdown-based memory systems. Chunk, embed, index, and hybrid-search your `.md` knowledge base with sqlite-vec.
|
|
37
|
+
|
|
38
|
+
<!-- Badges -->
|
|
39
|
+
<!-- [](https://pypi.org/project/markdown-memory-vec/) -->
|
|
40
|
+
<!-- [](https://pypi.org/project/markdown-memory-vec/) -->
|
|
41
|
+
<!-- [](https://opensource.org/licenses/MIT) -->
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Markdown-native**: YAML frontmatter parsing for metadata (importance, type, tags)
|
|
46
|
+
- **Smart chunking**: Paragraph-aware splitting with configurable overlap (~400 tokens, 80 overlap)
|
|
47
|
+
- **SHA-256 dedup**: Never re-embed unchanged content — incremental indexing is fast
|
|
48
|
+
- **Hybrid search**: Combines semantic similarity (α), importance weighting (β), and temporal decay (γ)
|
|
49
|
+
- **Zero-copy storage**: sqlite-vec KNN search with cosine distance in a single `.db` file
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Core only (YAML parsing, chunking, interfaces)
|
|
57
|
+
pip install markdown-memory-vec
|
|
58
|
+
|
|
59
|
+
# With vector search support (sqlite-vec + sentence-transformers)
|
|
60
|
+
pip install 'markdown-memory-vec[vector]'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Python API
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from memory_vec import MemoryVectorService
|
|
67
|
+
|
|
68
|
+
svc = MemoryVectorService("/path/to/project")
|
|
69
|
+
svc.rebuild_index() # Full index build
|
|
70
|
+
results = svc.search("how to deploy") # Hybrid search
|
|
71
|
+
svc.close()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Low-level API
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from memory_vec import (
|
|
78
|
+
SqliteVecStore,
|
|
79
|
+
SentenceTransformerEmbedder,
|
|
80
|
+
MemoryIndexer,
|
|
81
|
+
HybridSearchService,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
store = SqliteVecStore("memory.db")
|
|
85
|
+
store.ensure_tables()
|
|
86
|
+
|
|
87
|
+
embedder = SentenceTransformerEmbedder()
|
|
88
|
+
indexer = MemoryIndexer(store, embedder, memory_root="/path/to/memory")
|
|
89
|
+
indexer.index_directory("/path/to/memory")
|
|
90
|
+
|
|
91
|
+
search = HybridSearchService(vec_store=store, embedder=embedder)
|
|
92
|
+
results = search.search("how to deploy")
|
|
93
|
+
for r in results:
|
|
94
|
+
print(f"{r.file_path} (score={r.hybrid_score:.3f}): {r.chunk_text[:80]}...")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## CLI Usage
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Full rebuild
|
|
101
|
+
memory-vec /path/to/project --rebuild
|
|
102
|
+
|
|
103
|
+
# Incremental update (only changed files)
|
|
104
|
+
memory-vec /path/to/project --incremental
|
|
105
|
+
|
|
106
|
+
# Search
|
|
107
|
+
memory-vec /path/to/project --search "how to deploy" --top-k 5
|
|
108
|
+
|
|
109
|
+
# Statistics
|
|
110
|
+
memory-vec /path/to/project --stats
|
|
111
|
+
|
|
112
|
+
# Custom memory subdirectory
|
|
113
|
+
memory-vec /path/to/project --rebuild --memory-subdir "docs/memory"
|
|
114
|
+
|
|
115
|
+
# Verbose logging
|
|
116
|
+
memory-vec /path/to/project --rebuild -v
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API Reference
|
|
120
|
+
|
|
121
|
+
### High-level
|
|
122
|
+
|
|
123
|
+
| Class | Description |
|
|
124
|
+
|-------|-------------|
|
|
125
|
+
| `MemoryVectorService` | All-in-one service: rebuild, incremental index, search, stats |
|
|
126
|
+
|
|
127
|
+
### Components
|
|
128
|
+
|
|
129
|
+
| Class | Description |
|
|
130
|
+
|-------|-------------|
|
|
131
|
+
| `SqliteVecStore` | sqlite-vec backed vector store with KNN search |
|
|
132
|
+
| `SentenceTransformerEmbedder` | Lazy-loading sentence-transformers embedder |
|
|
133
|
+
| `MemoryIndexer` | Markdown → chunks → embeddings → store pipeline |
|
|
134
|
+
| `HybridSearchService` | Hybrid scoring: `α×semantic + β×importance + γ×temporal` |
|
|
135
|
+
|
|
136
|
+
### Interfaces
|
|
137
|
+
|
|
138
|
+
| Interface | Description |
|
|
139
|
+
|-----------|-------------|
|
|
140
|
+
| `IEmbedder` | Abstract embedder (`embed`, `embed_batch`, `dimension`) |
|
|
141
|
+
| `ISqliteVecStore` | Abstract vector store (`add`, `search`, `delete`, `clear`, `count`) |
|
|
142
|
+
|
|
143
|
+
### Data Types
|
|
144
|
+
|
|
145
|
+
| Type | Description |
|
|
146
|
+
|------|-------------|
|
|
147
|
+
| `VectorRecord` | Record for insertion (id, embedding, metadata) |
|
|
148
|
+
| `VectorSearchResult` | Raw KNN result (id, distance, metadata) |
|
|
149
|
+
| `SearchResult` | Hybrid search result with all score components |
|
|
150
|
+
| `MemoryVecMeta` | Metadata dataclass for stored embeddings |
|
|
151
|
+
|
|
152
|
+
### Utilities
|
|
153
|
+
|
|
154
|
+
| Function | Description |
|
|
155
|
+
|----------|-------------|
|
|
156
|
+
| `chunk_text(text, chunk_size, overlap_size)` | Split text into overlapping chunks |
|
|
157
|
+
| `parse_frontmatter(text)` | Extract YAML frontmatter from Markdown |
|
|
158
|
+
| `content_hash(text)` | SHA-256 hex digest |
|
|
159
|
+
| `is_sqlite_vec_available()` | Check sqlite-vec availability |
|
|
160
|
+
| `is_sentence_transformers_available()` | Check sentence-transformers availability |
|
|
161
|
+
|
|
162
|
+
## Architecture
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
┌─────────────────────────────────────────────────────┐
|
|
166
|
+
│ MemoryVectorService │
|
|
167
|
+
│ (high-level orchestration layer) │
|
|
168
|
+
├──────────┬──────────┬──────────────┬────────────────┤
|
|
169
|
+
│ │ │ │ │
|
|
170
|
+
│ Indexer │ Search │ Embedder │ Store │
|
|
171
|
+
│ │ │ │ │
|
|
172
|
+
│ .md file │ hybrid │ sentence- │ sqlite-vec │
|
|
173
|
+
│ → chunks │ scoring │ transformers│ KNN + meta │
|
|
174
|
+
│ → embed │ α+β+γ │ (lazy load) │ (cosine) │
|
|
175
|
+
│ → store │ │ │ │
|
|
176
|
+
└──────────┴──────────┴──────────────┴────────────────┘
|
|
177
|
+
▲ │
|
|
178
|
+
│ YAML frontmatter │
|
|
179
|
+
│ importance/type/tags ▼
|
|
180
|
+
┌─┴─────────────────────┐ ┌─────────────────────┐
|
|
181
|
+
│ Markdown Files │ │ vector_index.db │
|
|
182
|
+
│ (.claude/memory/) │ │ (single file) │
|
|
183
|
+
└───────────────────────┘ └─────────────────────┘
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
### HuggingFace Model
|
|
189
|
+
|
|
190
|
+
By default, uses `paraphrase-multilingual-MiniLM-L12-v2` (384-dim, 50+ languages).
|
|
191
|
+
|
|
192
|
+
For users in China or regions with slow HuggingFace access:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Use a mirror
|
|
196
|
+
export HF_ENDPOINT=https://hf-mirror.com
|
|
197
|
+
|
|
198
|
+
# Or use offline mode (model must be pre-cached)
|
|
199
|
+
export HF_HUB_OFFLINE=1
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Hybrid Search Weights
|
|
203
|
+
|
|
204
|
+
Default: `α=0.6, β=0.2, γ=0.2, λ=0.05`
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
search = HybridSearchService(
|
|
208
|
+
vec_store=store,
|
|
209
|
+
embedder=embedder,
|
|
210
|
+
alpha=0.8, # Semantic weight
|
|
211
|
+
beta=0.1, # Importance weight
|
|
212
|
+
gamma=0.1, # Temporal decay weight
|
|
213
|
+
decay_lambda=0.03, # Slower decay
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
memory_vec/__init__.py,sha256=TYFh7VgOBBw2A1GjM2uTuxr_wIoVASjcjaI9eiddvLw,2356
|
|
2
|
+
memory_vec/__main__.py,sha256=_HGhIzBVmsnb1XKqV_VOGMdIVlPrOWFDNhB3PLFNvG0,3978
|
|
3
|
+
memory_vec/embedder.py,sha256=dPn0DGnNSQ8Uj_gbDDq2FlC8hbRTvOQUBHxWOtEeN3w,5260
|
|
4
|
+
memory_vec/indexer.py,sha256=ZBE7KtY-2x77x5p89SpiZ3oKKqLs2LJGE9SkAu1vTgI,10701
|
|
5
|
+
memory_vec/interfaces.py,sha256=A_vyOIlMiBO3UkW4AMCtpq58pGbSASiIHhAiSvfm16c,2810
|
|
6
|
+
memory_vec/search.py,sha256=aEkfHjJeeUtaLUksfzAW2UfJsEPOoHcnBnVfzXgoQ1w,8624
|
|
7
|
+
memory_vec/service.py,sha256=AylPef106ZWLB9NiEr2Uee2LB0IZjrYiEs8HYn8TtNU,11533
|
|
8
|
+
memory_vec/store.py,sha256=G2GJYtcqYA0Op4oOgjIRZLDzTmpI8HANoUqEMrOwO88,17319
|
|
9
|
+
markdown_memory_vec-0.1.0.dist-info/METADATA,sha256=Xob56dfeNQX5M81IALmxX8uEQioScFS3Y19m-JZCJWE,7956
|
|
10
|
+
markdown_memory_vec-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
markdown_memory_vec-0.1.0.dist-info/entry_points.txt,sha256=TGn2LA56FfN5WnjyVjbe-Q7GqW7NMU2VvbEMXeEHYN8,56
|
|
12
|
+
markdown_memory_vec-0.1.0.dist-info/licenses/LICENSE,sha256=GeX_PyX3s1Hao9OpOj1gon8E4w_75fcZMqvhdA5bd5c,1064
|
|
13
|
+
markdown_memory_vec-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aigente
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
memory_vec/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vector search infrastructure for Markdown-based memory systems.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
|
|
6
|
+
- **Interfaces** (``interfaces.py``): ``IEmbedder``, ``ISqliteVecStore``
|
|
7
|
+
- **Storage** (``store.py``): ``SqliteVecStore`` — concrete sqlite-vec
|
|
8
|
+
backed vector store implementing ``ISqliteVecStore``
|
|
9
|
+
- **Embedder** (``embedder.py``): ``SentenceTransformerEmbedder`` — concrete
|
|
10
|
+
embedder using sentence-transformers, implementing ``IEmbedder``
|
|
11
|
+
- **Indexer** (``indexer.py``): ``MemoryIndexer`` — Markdown-to-vector indexing
|
|
12
|
+
pipeline with chunking, SHA-256 dedup, and YAML frontmatter parsing
|
|
13
|
+
- **Search** (``search.py``): ``HybridSearchService`` — hybrid retrieval
|
|
14
|
+
combining semantic similarity, importance, and temporal decay
|
|
15
|
+
|
|
16
|
+
All heavy dependencies (``sqlite-vec``, ``sentence-transformers``) are optional.
|
|
17
|
+
Use the ``is_*_available()`` helpers to check at runtime.
|
|
18
|
+
|
|
19
|
+
Quick start::
|
|
20
|
+
|
|
21
|
+
from memory_vec import (
|
|
22
|
+
SqliteVecStore,
|
|
23
|
+
SentenceTransformerEmbedder,
|
|
24
|
+
MemoryIndexer,
|
|
25
|
+
HybridSearchService,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
store = SqliteVecStore("memory.db")
|
|
29
|
+
store.ensure_tables()
|
|
30
|
+
|
|
31
|
+
embedder = SentenceTransformerEmbedder()
|
|
32
|
+
indexer = MemoryIndexer(store, embedder)
|
|
33
|
+
indexer.index_directory("path/to/markdown/files")
|
|
34
|
+
|
|
35
|
+
search = HybridSearchService(vec_store=store, embedder=embedder)
|
|
36
|
+
results = search.search("how to deploy")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Interfaces
|
|
40
|
+
# Concrete implementations
|
|
41
|
+
from .embedder import SentenceTransformerEmbedder, is_sentence_transformers_available
|
|
42
|
+
from .indexer import MemoryIndexer, chunk_text, parse_frontmatter
|
|
43
|
+
from .interfaces import IEmbedder, ISqliteVecStore, VectorRecord, VectorSearchResult
|
|
44
|
+
from .search import HybridSearchService, SearchResult
|
|
45
|
+
from .service import MemoryVectorService
|
|
46
|
+
from .store import MemoryVecMeta, SqliteVecStore, content_hash, is_sqlite_vec_available
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# Interfaces
|
|
50
|
+
"IEmbedder",
|
|
51
|
+
"ISqliteVecStore",
|
|
52
|
+
"VectorRecord",
|
|
53
|
+
"VectorSearchResult",
|
|
54
|
+
# Store
|
|
55
|
+
"SqliteVecStore",
|
|
56
|
+
"MemoryVecMeta",
|
|
57
|
+
"content_hash",
|
|
58
|
+
"is_sqlite_vec_available",
|
|
59
|
+
# Embedder
|
|
60
|
+
"SentenceTransformerEmbedder",
|
|
61
|
+
"is_sentence_transformers_available",
|
|
62
|
+
# Indexer
|
|
63
|
+
"MemoryIndexer",
|
|
64
|
+
"chunk_text",
|
|
65
|
+
"parse_frontmatter",
|
|
66
|
+
# Search
|
|
67
|
+
"HybridSearchService",
|
|
68
|
+
"SearchResult",
|
|
69
|
+
# High-level service
|
|
70
|
+
"MemoryVectorService",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
__version__ = "0.1.0"
|
memory_vec/__main__.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point: ``python -m memory_vec`` or ``memory-vec``.
|
|
3
|
+
|
|
4
|
+
Provides commands for building, maintaining, and querying the vector index.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from .service import MemoryVectorService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
"""Command-line interface for memory vector maintenance."""
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="memory-vec",
|
|
20
|
+
description="Markdown memory vector index — build, maintain, and search",
|
|
21
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
22
|
+
epilog=(
|
|
23
|
+
"Examples:\n"
|
|
24
|
+
" memory-vec /path/to/project --rebuild\n"
|
|
25
|
+
" memory-vec /path/to/project --incremental\n"
|
|
26
|
+
" memory-vec /path/to/project --search 'how to deploy'\n"
|
|
27
|
+
" memory-vec /path/to/project --stats\n"
|
|
28
|
+
"\n"
|
|
29
|
+
"Environment variables:\n"
|
|
30
|
+
" HF_ENDPOINT HuggingFace mirror URL (e.g. https://hf-mirror.com)\n"
|
|
31
|
+
" HF_HUB_OFFLINE Set to '1' to disable network access\n"
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument("workspace", help="Project root directory")
|
|
35
|
+
parser.add_argument("--rebuild", action="store_true", help="Full rebuild of vector index")
|
|
36
|
+
parser.add_argument("--incremental", action="store_true", help="Incremental re-index (only changed files)")
|
|
37
|
+
parser.add_argument("--search", type=str, help="Search memories by query")
|
|
38
|
+
parser.add_argument("--stats", action="store_true", help="Show index statistics")
|
|
39
|
+
parser.add_argument("--top-k", type=int, default=5, help="Number of search results (default: 5)")
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--model",
|
|
42
|
+
type=str,
|
|
43
|
+
default="paraphrase-multilingual-MiniLM-L12-v2",
|
|
44
|
+
help="Embedding model name",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--memory-subdir",
|
|
48
|
+
type=str,
|
|
49
|
+
default=".claude/memory",
|
|
50
|
+
help="Memory subdirectory relative to workspace (default: .claude/memory)",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")
|
|
53
|
+
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
|
|
56
|
+
logging.basicConfig(
|
|
57
|
+
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
58
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
svc = MemoryVectorService(
|
|
62
|
+
args.workspace,
|
|
63
|
+
model_name=args.model,
|
|
64
|
+
memory_subdir=args.memory_subdir,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if not svc.is_available:
|
|
68
|
+
print("ERROR: Vector dependencies not installed.", file=sys.stderr) # noqa: T201
|
|
69
|
+
print("Install with: pip install 'markdown-memory-vec[vector]'", file=sys.stderr) # noqa: T201
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
if args.rebuild:
|
|
74
|
+
total = svc.rebuild_index()
|
|
75
|
+
print(f"Full rebuild complete: {total} chunks indexed") # noqa: T201
|
|
76
|
+
|
|
77
|
+
elif args.incremental:
|
|
78
|
+
total = svc.incremental_index()
|
|
79
|
+
print(f"Incremental index: {total} chunks updated") # noqa: T201
|
|
80
|
+
|
|
81
|
+
elif args.search:
|
|
82
|
+
results = svc.search(args.search, top_k=args.top_k)
|
|
83
|
+
if not results:
|
|
84
|
+
print("No results found.") # noqa: T201
|
|
85
|
+
else:
|
|
86
|
+
for i, r in enumerate(results, 1):
|
|
87
|
+
print(f"\n--- Result {i} (score: {r['hybrid_score']}) ---") # noqa: T201
|
|
88
|
+
print(f"File: {r['file_path']}") # noqa: T201
|
|
89
|
+
print(f"Type: {r['memory_type']} Importance: {r['importance']}") # noqa: T201
|
|
90
|
+
text = str(r["chunk_text"])
|
|
91
|
+
if len(text) > 200:
|
|
92
|
+
text = text[:200] + "..."
|
|
93
|
+
print(f"Text: {text}") # noqa: T201
|
|
94
|
+
|
|
95
|
+
elif args.stats:
|
|
96
|
+
s = svc.stats()
|
|
97
|
+
print("Vector Index Statistics:") # noqa: T201
|
|
98
|
+
for k, v in s.items():
|
|
99
|
+
print(f" {k}: {v}") # noqa: T201
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
parser.print_help()
|
|
103
|
+
|
|
104
|
+
finally:
|
|
105
|
+
svc.close()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
memory_vec/embedder.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Embedding model abstraction layer.
|
|
3
|
+
|
|
4
|
+
Provides a concrete implementation :class:`SentenceTransformerEmbedder` that
|
|
5
|
+
wraps the ``sentence-transformers`` library and conforms to the
|
|
6
|
+
:class:`IEmbedder` interface defined in ``interfaces.py``.
|
|
7
|
+
|
|
8
|
+
The heavy ML import is deferred until the first call so that importing this
|
|
9
|
+
module is always cheap.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, List
|
|
18
|
+
|
|
19
|
+
from .interfaces import IEmbedder
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Availability check
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
_sentence_transformers_available = False
|
|
27
|
+
try:
|
|
28
|
+
import sentence_transformers # noqa: F401 # type: ignore[import-untyped]
|
|
29
|
+
|
|
30
|
+
_sentence_transformers_available = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_sentence_transformers_available() -> bool:
|
|
36
|
+
"""Return ``True`` if the sentence-transformers package is importable."""
|
|
37
|
+
return _sentence_transformers_available
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Sentence-transformers implementation
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
_DEFAULT_MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2"
|
|
44
|
+
_DEFAULT_DIMENSION = 384
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ensure_hf_env(model_name: str) -> None:
|
|
48
|
+
"""Set HuggingFace environment variables for reliable model loading.
|
|
49
|
+
|
|
50
|
+
Strategy:
|
|
51
|
+
- If model is already cached locally → set ``HF_HUB_OFFLINE=1`` (no network).
|
|
52
|
+
- Otherwise respect user-set ``HF_ENDPOINT`` (do **not** override it).
|
|
53
|
+
"""
|
|
54
|
+
if os.environ.get("HF_HUB_OFFLINE") == "1":
|
|
55
|
+
return # User explicitly requested offline mode
|
|
56
|
+
|
|
57
|
+
# Check HuggingFace hub cache for the model
|
|
58
|
+
cache_dir = Path.home() / ".cache" / "huggingface" / "hub"
|
|
59
|
+
model_dir = cache_dir / f"models--sentence-transformers--{model_name}"
|
|
60
|
+
if model_dir.exists():
|
|
61
|
+
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
62
|
+
logger.info("Model '%s' found in local cache, using offline mode", model_name)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Model not cached — log a hint if no endpoint is set
|
|
66
|
+
if not os.environ.get("HF_ENDPOINT"):
|
|
67
|
+
logger.info(
|
|
68
|
+
"HF_ENDPOINT not set. If downloading is slow, set HF_ENDPOINT to a mirror "
|
|
69
|
+
"(e.g. https://hf-mirror.com for users in China)."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SentenceTransformerEmbedder(IEmbedder):
|
|
74
|
+
"""Embedder backed by the ``sentence-transformers`` library.
|
|
75
|
+
|
|
76
|
+
The underlying ``SentenceTransformer`` model is loaded lazily on the first
|
|
77
|
+
call to :meth:`embed` or :meth:`embed_batch`, and is then cached as a
|
|
78
|
+
class-level variable so it is shared across all instances using the same
|
|
79
|
+
model name.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
model_name:
|
|
84
|
+
HuggingFace model identifier. Defaults to
|
|
85
|
+
``"paraphrase-multilingual-MiniLM-L12-v2"`` (384-dim, 50+ languages).
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# Class-level cache: model_name -> SentenceTransformer instance
|
|
89
|
+
_model_cache: dict[str, Any] = {}
|
|
90
|
+
|
|
91
|
+
def __init__(self, model_name: str = _DEFAULT_MODEL_NAME) -> None:
|
|
92
|
+
if not _sentence_transformers_available:
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"sentence-transformers is not installed. " "Install it with: pip install 'markdown-memory-vec[vector]'"
|
|
95
|
+
)
|
|
96
|
+
self._model_name = model_name
|
|
97
|
+
self._dimension_override: int | None = None
|
|
98
|
+
|
|
99
|
+
# -- lazy model loading --------------------------------------------------
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def _model(self) -> Any:
|
|
103
|
+
"""Return the cached ``SentenceTransformer`` instance."""
|
|
104
|
+
if self._model_name not in self._model_cache:
|
|
105
|
+
from sentence_transformers import SentenceTransformer # type: ignore[import-untyped]
|
|
106
|
+
|
|
107
|
+
_ensure_hf_env(self._model_name)
|
|
108
|
+
logger.info("Loading sentence-transformer model: %s", self._model_name)
|
|
109
|
+
model = SentenceTransformer(self._model_name)
|
|
110
|
+
self._model_cache[self._model_name] = model
|
|
111
|
+
# Infer dimension from the model
|
|
112
|
+
dim: int = model.get_sentence_embedding_dimension() # type: ignore[assignment]
|
|
113
|
+
self._dimension_override = dim
|
|
114
|
+
return self._model_cache[self._model_name]
|
|
115
|
+
|
|
116
|
+
# -- IEmbedder interface implementation ----------------------------------
|
|
117
|
+
|
|
118
|
+
def embed(self, text: str) -> List[float]:
|
|
119
|
+
"""Embed a single text string into a vector."""
|
|
120
|
+
result = self._model.encode([text], show_progress_bar=False)
|
|
121
|
+
return result[0].tolist() # type: ignore[union-attr, no-any-return]
|
|
122
|
+
|
|
123
|
+
def embed_batch(self, texts: List[str]) -> List[List[float]]:
|
|
124
|
+
"""Embed multiple texts into vectors."""
|
|
125
|
+
if not texts:
|
|
126
|
+
return []
|
|
127
|
+
embeddings = self._model.encode(texts, show_progress_bar=False)
|
|
128
|
+
return [vec.tolist() for vec in embeddings] # type: ignore[union-attr]
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def dimension(self) -> int:
|
|
132
|
+
"""Return the vector dimension (triggers model load if unknown)."""
|
|
133
|
+
if self._dimension_override is not None:
|
|
134
|
+
return self._dimension_override
|
|
135
|
+
# Trigger model load to discover dimension
|
|
136
|
+
_ = self._model
|
|
137
|
+
return self._dimension_override or _DEFAULT_DIMENSION
|