yourmemory 1.2.1__tar.gz → 1.2.3__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.
- {yourmemory-1.2.1/yourmemory.egg-info → yourmemory-1.2.3}/PKG-INFO +24 -11
- {yourmemory-1.2.1 → yourmemory-1.2.3}/README.md +21 -8
- {yourmemory-1.2.1 → yourmemory-1.2.3}/memory_mcp.py +34 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/pyproject.toml +4 -3
- yourmemory-1.2.3/src/services/extract.py +52 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/resolve.py +44 -23
- yourmemory-1.2.3/src/services/resolve_fallback.py +192 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3/yourmemory.egg-info}/PKG-INFO +24 -11
- {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/SOURCES.txt +1 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/entry_points.txt +1 -0
- yourmemory-1.2.1/src/services/extract.py +0 -36
- {yourmemory-1.2.1 → yourmemory-1.2.3}/LICENSE +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/setup.cfg +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/__init__.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/app.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/connection.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/duckdb_schema.sql +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/migrate.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/schema.sql +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/db/sqlite_schema.sql +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/jobs/decay_job.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/routes/__init__.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/routes/agents.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/routes/memories.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/routes/retrieve.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/__init__.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/agent_registry.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/api_keys.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/decay.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/embed.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/extract_fallback.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/src/services/retrieve.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/dependency_links.txt +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/requires.txt +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.3}/yourmemory.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yourmemory
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native
|
|
5
5
|
Author-email: Sachit Misra <mishrasachit1@gmail.com>
|
|
6
6
|
License: Apache License
|
|
@@ -163,8 +163,8 @@ License: Apache License
|
|
|
163
163
|
See the License for the specific language governing permissions and
|
|
164
164
|
limitations under the License.
|
|
165
165
|
|
|
166
|
-
Project-URL: Homepage, https://github.com/sachitrafa/
|
|
167
|
-
Project-URL: Repository, https://github.com/sachitrafa/
|
|
166
|
+
Project-URL: Homepage, https://github.com/sachitrafa/YourMemory
|
|
167
|
+
Project-URL: Repository, https://github.com/sachitrafa/YourMemory
|
|
168
168
|
Keywords: mcp,claude,memory,ebbinghaus,ai,sqlite,postgresql
|
|
169
169
|
Classifier: Programming Language :: Python :: 3
|
|
170
170
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -246,6 +246,8 @@ Importance additionally modulates the decay rate within each category. Memories
|
|
|
246
246
|
|
|
247
247
|
**Zero infrastructure required** — uses DuckDB out of the box. Two commands and you're done.
|
|
248
248
|
|
|
249
|
+
Supports **Python 3.11, 3.12, 3.13, and 3.14**.
|
|
250
|
+
|
|
249
251
|
### 1. Install
|
|
250
252
|
|
|
251
253
|
```bash
|
|
@@ -254,17 +256,23 @@ pip install yourmemory
|
|
|
254
256
|
|
|
255
257
|
All dependencies installed automatically. No clone, no Docker, no database setup.
|
|
256
258
|
|
|
257
|
-
### 2.
|
|
259
|
+
### 2. Run setup (once)
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
yourmemory-setup
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Downloads the spaCy language model and initialises the database. Run this once after install.
|
|
258
266
|
|
|
259
|
-
|
|
267
|
+
### 3. Get your config
|
|
260
268
|
|
|
261
269
|
```bash
|
|
262
270
|
yourmemory-path
|
|
263
271
|
```
|
|
264
272
|
|
|
265
|
-
|
|
273
|
+
Prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
|
|
266
274
|
|
|
267
|
-
###
|
|
275
|
+
### 4. Wire into your AI client
|
|
268
276
|
|
|
269
277
|
The database is created automatically at `~/.yourmemory/memories.duckdb` on first use.
|
|
270
278
|
|
|
@@ -286,9 +294,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
|
|
|
286
294
|
|
|
287
295
|
#### Cline (VS Code)
|
|
288
296
|
|
|
289
|
-
VS Code doesn't inherit your shell PATH
|
|
297
|
+
VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
yourmemory-path
|
|
301
|
+
```
|
|
290
302
|
|
|
291
|
-
|
|
303
|
+
Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
|
|
292
304
|
|
|
293
305
|
```json
|
|
294
306
|
{
|
|
@@ -305,7 +317,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
|
|
|
305
317
|
}
|
|
306
318
|
```
|
|
307
319
|
|
|
308
|
-
|
|
320
|
+
Restart Cline after saving.
|
|
309
321
|
|
|
310
322
|
#### Cursor
|
|
311
323
|
|
|
@@ -346,7 +358,7 @@ Restart Claude Desktop.
|
|
|
346
358
|
|
|
347
359
|
YourMemory is a standard stdio MCP server. Works with Claude Code, Claude Desktop, Cline, Cursor, Windsurf, Continue, and Zed. Use the full path from `yourmemory-path` if the client doesn't inherit shell PATH.
|
|
348
360
|
|
|
349
|
-
###
|
|
361
|
+
### 5. Add memory instructions to your project
|
|
350
362
|
|
|
351
363
|
Copy `sample_CLAUDE.md` into your project root as `CLAUDE.md` and replace:
|
|
352
364
|
- `YOUR_NAME` — your name (e.g. `Alice`)
|
|
@@ -439,6 +451,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
|
|
|
439
451
|
|
|
440
452
|
- **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
|
|
441
453
|
- **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
|
|
454
|
+
- **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
|
|
442
455
|
- **APScheduler** — automatic 24h decay job
|
|
443
456
|
- **MCP** — Claude integration via Model Context Protocol
|
|
444
457
|
- **PostgreSQL + pgvector** — optional, for teams / large datasets
|
|
@@ -51,6 +51,8 @@ Importance additionally modulates the decay rate within each category. Memories
|
|
|
51
51
|
|
|
52
52
|
**Zero infrastructure required** — uses DuckDB out of the box. Two commands and you're done.
|
|
53
53
|
|
|
54
|
+
Supports **Python 3.11, 3.12, 3.13, and 3.14**.
|
|
55
|
+
|
|
54
56
|
### 1. Install
|
|
55
57
|
|
|
56
58
|
```bash
|
|
@@ -59,17 +61,23 @@ pip install yourmemory
|
|
|
59
61
|
|
|
60
62
|
All dependencies installed automatically. No clone, no Docker, no database setup.
|
|
61
63
|
|
|
62
|
-
### 2.
|
|
64
|
+
### 2. Run setup (once)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
yourmemory-setup
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Downloads the spaCy language model and initialises the database. Run this once after install.
|
|
63
71
|
|
|
64
|
-
|
|
72
|
+
### 3. Get your config
|
|
65
73
|
|
|
66
74
|
```bash
|
|
67
75
|
yourmemory-path
|
|
68
76
|
```
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
Prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
|
|
71
79
|
|
|
72
|
-
###
|
|
80
|
+
### 4. Wire into your AI client
|
|
73
81
|
|
|
74
82
|
The database is created automatically at `~/.yourmemory/memories.duckdb` on first use.
|
|
75
83
|
|
|
@@ -91,9 +99,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
|
|
|
91
99
|
|
|
92
100
|
#### Cline (VS Code)
|
|
93
101
|
|
|
94
|
-
VS Code doesn't inherit your shell PATH
|
|
102
|
+
VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
yourmemory-path
|
|
106
|
+
```
|
|
95
107
|
|
|
96
|
-
|
|
108
|
+
Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
|
|
97
109
|
|
|
98
110
|
```json
|
|
99
111
|
{
|
|
@@ -110,7 +122,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
|
|
|
110
122
|
}
|
|
111
123
|
```
|
|
112
124
|
|
|
113
|
-
|
|
125
|
+
Restart Cline after saving.
|
|
114
126
|
|
|
115
127
|
#### Cursor
|
|
116
128
|
|
|
@@ -151,7 +163,7 @@ Restart Claude Desktop.
|
|
|
151
163
|
|
|
152
164
|
YourMemory is a standard stdio MCP server. Works with Claude Code, Claude Desktop, Cline, Cursor, Windsurf, Continue, and Zed. Use the full path from `yourmemory-path` if the client doesn't inherit shell PATH.
|
|
153
165
|
|
|
154
|
-
###
|
|
166
|
+
### 5. Add memory instructions to your project
|
|
155
167
|
|
|
156
168
|
Copy `sample_CLAUDE.md` into your project root as `CLAUDE.md` and replace:
|
|
157
169
|
- `YOUR_NAME` — your name (e.g. `Alice`)
|
|
@@ -244,6 +256,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
|
|
|
244
256
|
|
|
245
257
|
- **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
|
|
246
258
|
- **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
|
|
259
|
+
- **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
|
|
247
260
|
- **APScheduler** — automatic 24h decay job
|
|
248
261
|
- **MCP** — Claude integration via Model Context Protocol
|
|
249
262
|
- **PostgreSQL + pgvector** — optional, for teams / large datasets
|
|
@@ -563,6 +563,40 @@ def print_path():
|
|
|
563
563
|
print("Paste this into your Cline MCP settings:\n")
|
|
564
564
|
print(_json.dumps(config, indent=2))
|
|
565
565
|
|
|
566
|
+
def setup():
|
|
567
|
+
"""Run once after pip install to download the spaCy model."""
|
|
568
|
+
import subprocess
|
|
569
|
+
print("YourMemory setup — installing spaCy language model...")
|
|
570
|
+
result = subprocess.run(
|
|
571
|
+
[sys.executable, "-m", "spacy", "download", "en_core_web_sm"],
|
|
572
|
+
check=False,
|
|
573
|
+
)
|
|
574
|
+
if result.returncode == 0:
|
|
575
|
+
print("✓ spaCy model installed successfully.")
|
|
576
|
+
else:
|
|
577
|
+
# Fallback: install via direct wheel URL
|
|
578
|
+
print("Direct download fallback...")
|
|
579
|
+
result2 = subprocess.run(
|
|
580
|
+
[sys.executable, "-m", "pip", "install",
|
|
581
|
+
"https://github.com/explosion/spacy-models/releases/download/"
|
|
582
|
+
"en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl",
|
|
583
|
+
"--break-system-packages"],
|
|
584
|
+
check=False,
|
|
585
|
+
)
|
|
586
|
+
if result2.returncode == 0:
|
|
587
|
+
print("✓ spaCy model installed successfully.")
|
|
588
|
+
else:
|
|
589
|
+
print("✗ Could not install spaCy model automatically.")
|
|
590
|
+
print(" Run manually: python -m spacy download en_core_web_sm")
|
|
591
|
+
print(" YourMemory will still work using the built-in regex fallback.")
|
|
592
|
+
|
|
593
|
+
# Also run DB migration
|
|
594
|
+
from src.db.migrate import migrate
|
|
595
|
+
migrate()
|
|
596
|
+
print("✓ Database initialised.")
|
|
597
|
+
print("\nSetup complete. Run yourmemory-path to get your MCP config.")
|
|
598
|
+
|
|
599
|
+
|
|
566
600
|
def run():
|
|
567
601
|
from src.db.migrate import migrate
|
|
568
602
|
migrate()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yourmemory"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.3"
|
|
8
8
|
description = "Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -39,10 +39,11 @@ all = ["yourmemory[postgres,sse]"]
|
|
|
39
39
|
[project.scripts]
|
|
40
40
|
yourmemory = "memory_mcp:run"
|
|
41
41
|
yourmemory-path = "memory_mcp:print_path"
|
|
42
|
+
yourmemory-setup = "memory_mcp:setup"
|
|
42
43
|
|
|
43
44
|
[project.urls]
|
|
44
|
-
Homepage = "https://github.com/sachitrafa/
|
|
45
|
-
Repository = "https://github.com/sachitrafa/
|
|
45
|
+
Homepage = "https://github.com/sachitrafa/YourMemory"
|
|
46
|
+
Repository = "https://github.com/sachitrafa/YourMemory"
|
|
46
47
|
|
|
47
48
|
[tool.setuptools]
|
|
48
49
|
py-modules = ["memory_mcp"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
_QUESTION_WORDS = {"what", "who", "where", "when", "why", "how", "which", "whose", "whom"}
|
|
5
|
+
|
|
6
|
+
_IMPERATIVE_PATTERNS = [
|
|
7
|
+
r'^(please|use|try|do|don\'t|make|create|add|remove|delete|update)',
|
|
8
|
+
r'^(convert|transform|change|modify|fix|help|show|tell)',
|
|
9
|
+
r'^(install|run|execute|start|stop|restart|configure)',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# Load spaCy if available — falls back to regex if model not installed yet
|
|
13
|
+
# Run `yourmemory-setup` once after pip install to download the model
|
|
14
|
+
_nlp = None
|
|
15
|
+
try:
|
|
16
|
+
import spacy
|
|
17
|
+
_nlp = spacy.load("en_core_web_sm")
|
|
18
|
+
except OSError:
|
|
19
|
+
print(
|
|
20
|
+
"YourMemory: spaCy model not found. Run `yourmemory-setup` once to install it.\n"
|
|
21
|
+
" Falling back to built-in regex categorization.",
|
|
22
|
+
file=sys.stderr,
|
|
23
|
+
)
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_question(text: str) -> bool:
|
|
29
|
+
"""Return True if the text is a question — questions are not stored as memories."""
|
|
30
|
+
stripped = text.strip()
|
|
31
|
+
if stripped.endswith("?"):
|
|
32
|
+
return True
|
|
33
|
+
first_word = re.split(r"\s+", stripped.lower())[0]
|
|
34
|
+
return first_word in _QUESTION_WORDS
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def categorize(text: str) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Classify text as fact or assumption.
|
|
40
|
+
Uses spaCy dependency parse when available, regex heuristics otherwise.
|
|
41
|
+
Run `yourmemory-setup` to enable spaCy.
|
|
42
|
+
"""
|
|
43
|
+
if _nlp is not None:
|
|
44
|
+
doc = _nlp(text)
|
|
45
|
+
has_subject = any(tok.dep_ in ("nsubj", "nsubjpass") for tok in doc)
|
|
46
|
+
return "fact" if has_subject else "assumption"
|
|
47
|
+
|
|
48
|
+
text_lower = text.lower().strip()
|
|
49
|
+
for pattern in _IMPERATIVE_PATTERNS:
|
|
50
|
+
if re.match(pattern, text_lower):
|
|
51
|
+
return "assumption"
|
|
52
|
+
return "fact"
|
|
@@ -16,17 +16,6 @@ from src.db.connection import get_backend
|
|
|
16
16
|
DEDUP_THRESHOLD = 0.65 # below → always new memory
|
|
17
17
|
REINFORCE_THRESHOLD = 0.85 # at or above → reinforce (near-identical paraphrase)
|
|
18
18
|
|
|
19
|
-
# Polarity verb antonym pairs (spaCy lemma → antonym lemma)
|
|
20
|
-
_ANTONYMS = {
|
|
21
|
-
"love": "hate", "hate": "love",
|
|
22
|
-
"like": "dislike", "dislike": "like",
|
|
23
|
-
"prefer": "avoid", "avoid": "prefer",
|
|
24
|
-
"use": "stop", "stop": "use",
|
|
25
|
-
"want": "refuse", "refuse": "want",
|
|
26
|
-
"enjoy": "dislike",
|
|
27
|
-
"start": "stop",
|
|
28
|
-
}
|
|
29
|
-
|
|
30
19
|
|
|
31
20
|
def _cosine(a: list, b: list) -> float:
|
|
32
21
|
import numpy as np
|
|
@@ -101,19 +90,50 @@ def find_near_duplicate(user_id: str, embedding: list, conn) -> dict | None:
|
|
|
101
90
|
"importance": best[3], "recall_count": best[4], "similarity": sim}
|
|
102
91
|
|
|
103
92
|
|
|
93
|
+
_POSITIVE_VERBS = {
|
|
94
|
+
"love", "like", "prefer", "enjoy", "use", "want", "start",
|
|
95
|
+
"adopt", "recommend", "favor", "support", "trust", "appreciate",
|
|
96
|
+
}
|
|
97
|
+
_NEGATIVE_VERBS = {
|
|
98
|
+
"hate", "dislike", "avoid", "stop", "refuse", "abandon",
|
|
99
|
+
"reject", "distrust", "dislike", "despise",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _polarity(doc) -> int:
|
|
104
|
+
"""
|
|
105
|
+
Return +1 (positive), -1 (negative), or 0 (neutral) for a doc.
|
|
106
|
+
Uses root verb lemma + negation detection — no sentiment model needed.
|
|
107
|
+
"""
|
|
108
|
+
for token in doc:
|
|
109
|
+
# Use both lemma and raw text to handle spaCy lemmatization bugs
|
|
110
|
+
# e.g. "hates" → lemma "hat" (wrong) but text.rstrip("s") → "hate"
|
|
111
|
+
lemma = token.lemma_.lower()
|
|
112
|
+
raw = token.text.lower().rstrip("s") # crude but catches loves/hates/likes/dislikes
|
|
113
|
+
is_negated = any(child.dep_ == "neg" for child in token.children)
|
|
114
|
+
|
|
115
|
+
if lemma in _POSITIVE_VERBS or raw in _POSITIVE_VERBS:
|
|
116
|
+
return -1 if is_negated else +1
|
|
117
|
+
if lemma in _NEGATIVE_VERBS or raw in _NEGATIVE_VERBS:
|
|
118
|
+
return +1 if is_negated else -1
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
104
122
|
def detect_contradiction(existing_text: str, incoming_text: str) -> bool:
|
|
105
123
|
"""
|
|
106
124
|
Return True if the incoming text contradicts the existing one.
|
|
107
|
-
|
|
125
|
+
Detects polarity flip using verb lemmas + negation — generalizes beyond
|
|
126
|
+
a fixed antonym list by treating any positive→negative or negative→positive
|
|
127
|
+
shift on the same topic as a contradiction.
|
|
108
128
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
129
|
+
if _nlp is None:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
existing_pol = _polarity(_nlp(existing_text))
|
|
133
|
+
incoming_pol = _polarity(_nlp(incoming_text))
|
|
111
134
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if antonym and antonym in incoming_verbs:
|
|
115
|
-
return True
|
|
116
|
-
return False
|
|
135
|
+
# Both sentences must have clear polarity and they must be opposite
|
|
136
|
+
return existing_pol != 0 and incoming_pol != 0 and existing_pol != incoming_pol
|
|
117
137
|
|
|
118
138
|
|
|
119
139
|
def merge_entities(existing_text: str, incoming_text: str) -> str:
|
|
@@ -168,13 +188,14 @@ def resolve(user_id: str, content: str, embedding: list, conn) -> dict:
|
|
|
168
188
|
|
|
169
189
|
sim = match["similarity"]
|
|
170
190
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# DEDUP_THRESHOLD ≤ sim < REINFORCE_THRESHOLD
|
|
191
|
+
# Check contradiction first — even near-identical sentences can be opposites
|
|
192
|
+
# e.g. "dislike JavaScript" vs "love JavaScript" → sim ~0.92 but must replace
|
|
175
193
|
if detect_contradiction(match["content"], content):
|
|
176
194
|
return {"action": "replace", "content": content, "existing": match}
|
|
177
195
|
|
|
196
|
+
if sim >= REINFORCE_THRESHOLD:
|
|
197
|
+
return {"action": "reinforce", "content": match["content"], "existing": match}
|
|
198
|
+
|
|
178
199
|
merged = merge_entities(match["content"], content)
|
|
179
200
|
if merged == match["content"]:
|
|
180
201
|
# No new entities found — treat as paraphrase
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Semantic deduplication for POST /memories - Fallback version without spaCy.
|
|
3
|
+
|
|
4
|
+
Detects near-duplicate memories via cosine similarity and applies one of:
|
|
5
|
+
- reinforce : sim ≥ 0.85 — paraphrase, bump recall_count only
|
|
6
|
+
- replace : 0.65–0.85 + contradiction detected — overwrite with incoming
|
|
7
|
+
- merge : 0.65–0.85 + no contradiction — entity-append to existing
|
|
8
|
+
- new : sim < 0.65 — genuinely distinct, plain INSERT
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import re
|
|
14
|
+
from src.db.connection import get_backend
|
|
15
|
+
|
|
16
|
+
DEDUP_THRESHOLD = 0.65 # below → always new memory
|
|
17
|
+
REINFORCE_THRESHOLD = 0.85 # at or above → reinforce (near-identical paraphrase)
|
|
18
|
+
|
|
19
|
+
# Simple contradiction detection patterns (fallback)
|
|
20
|
+
_CONTRADICTION_PATTERNS = [
|
|
21
|
+
(r'\b(love|like|prefer|enjoy)\b', r'\b(hate|dislike|avoid)\b'),
|
|
22
|
+
(r'\b(start|begin|use)\b', r'\b(stop|quit|avoid)\b'),
|
|
23
|
+
(r'\b(want|need)\b', r'\b(refuse|reject)\b'),
|
|
24
|
+
(r'\b(good|great|excellent)\b', r'\b(bad|terrible|awful)\b'),
|
|
25
|
+
(r'\b(yes|true|correct)\b', r'\b(no|false|wrong)\b'),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cosine(a: list, b: list) -> float:
|
|
30
|
+
import numpy as np
|
|
31
|
+
va, vb = np.array(a, dtype=float), np.array(b, dtype=float)
|
|
32
|
+
denom = np.linalg.norm(va) * np.linalg.norm(vb)
|
|
33
|
+
return float(np.dot(va, vb) / denom) if denom else 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def find_near_duplicate(user_id: str, embedding: list, conn) -> dict | None:
|
|
37
|
+
"""
|
|
38
|
+
Return the closest existing memory if cosine similarity >= DEDUP_THRESHOLD,
|
|
39
|
+
else None. Uses the caller's open connection.
|
|
40
|
+
"""
|
|
41
|
+
backend = get_backend()
|
|
42
|
+
|
|
43
|
+
if backend == "postgres":
|
|
44
|
+
embedding_str = f"[{','.join(str(x) for x in embedding)}]"
|
|
45
|
+
cur = conn.cursor()
|
|
46
|
+
cur.execute("""
|
|
47
|
+
SELECT id, content, category, importance, recall_count,
|
|
48
|
+
1 - (embedding <=> %s::vector) AS similarity
|
|
49
|
+
FROM memories
|
|
50
|
+
WHERE user_id = %s
|
|
51
|
+
ORDER BY embedding <=> %s::vector
|
|
52
|
+
LIMIT 1
|
|
53
|
+
""", (embedding_str, user_id, embedding_str))
|
|
54
|
+
row = cur.fetchone()
|
|
55
|
+
cur.close()
|
|
56
|
+
if row is None:
|
|
57
|
+
return None
|
|
58
|
+
sim = row[5]
|
|
59
|
+
if sim < DEDUP_THRESHOLD:
|
|
60
|
+
return None
|
|
61
|
+
return {"id": row[0], "content": row[1], "category": row[2],
|
|
62
|
+
"importance": row[3], "recall_count": row[4], "similarity": sim}
|
|
63
|
+
|
|
64
|
+
if backend == "duckdb":
|
|
65
|
+
from src.db.connection import duckdb_row
|
|
66
|
+
cur = conn.execute("""
|
|
67
|
+
SELECT id, content, category, importance, recall_count,
|
|
68
|
+
array_cosine_similarity(embedding, ?::FLOAT[768]) AS similarity
|
|
69
|
+
FROM memories
|
|
70
|
+
WHERE user_id = ?
|
|
71
|
+
ORDER BY similarity DESC
|
|
72
|
+
LIMIT 1
|
|
73
|
+
""", [embedding, user_id])
|
|
74
|
+
row = duckdb_row(cur)
|
|
75
|
+
if row is None or row["similarity"] < DEDUP_THRESHOLD:
|
|
76
|
+
return None
|
|
77
|
+
return row
|
|
78
|
+
|
|
79
|
+
# SQLite: numpy cosine over all user memories
|
|
80
|
+
cur = conn.cursor()
|
|
81
|
+
cur.execute("""
|
|
82
|
+
SELECT id, content, category, importance, recall_count, embedding
|
|
83
|
+
FROM memories WHERE user_id = ?
|
|
84
|
+
""", (user_id,))
|
|
85
|
+
rows = cur.fetchall()
|
|
86
|
+
cur.close()
|
|
87
|
+
|
|
88
|
+
best, sim = None, -1.0
|
|
89
|
+
for row in rows:
|
|
90
|
+
raw = row[5] if isinstance(row, tuple) else row["embedding"]
|
|
91
|
+
if raw is None:
|
|
92
|
+
continue
|
|
93
|
+
s = _cosine(embedding, json.loads(raw))
|
|
94
|
+
if s > sim:
|
|
95
|
+
sim, best = s, row
|
|
96
|
+
if best is None or sim < DEDUP_THRESHOLD:
|
|
97
|
+
return None
|
|
98
|
+
return {"id": best[0], "content": best[1], "category": best[2],
|
|
99
|
+
"importance": best[3], "recall_count": best[4], "similarity": sim}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def detect_contradiction(existing_text: str, incoming_text: str) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Fallback contradiction detection using regex patterns.
|
|
105
|
+
Return True if the incoming text contradicts the existing one.
|
|
106
|
+
"""
|
|
107
|
+
existing_lower = existing_text.lower()
|
|
108
|
+
incoming_lower = incoming_text.lower()
|
|
109
|
+
|
|
110
|
+
for positive_pattern, negative_pattern in _CONTRADICTION_PATTERNS:
|
|
111
|
+
# Check if existing has positive and incoming has negative
|
|
112
|
+
if re.search(positive_pattern, existing_lower) and re.search(negative_pattern, incoming_lower):
|
|
113
|
+
return True
|
|
114
|
+
# Check if existing has negative and incoming has positive
|
|
115
|
+
if re.search(negative_pattern, existing_lower) and re.search(positive_pattern, incoming_lower):
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def merge_entities(existing_text: str, incoming_text: str) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Fallback entity merging using simple heuristics.
|
|
124
|
+
Append capitalized words and quoted strings from incoming that are absent from existing.
|
|
125
|
+
Returns the merged string, or existing_text unchanged if nothing new found.
|
|
126
|
+
"""
|
|
127
|
+
existing_lower = existing_text.lower()
|
|
128
|
+
|
|
129
|
+
# Extract potential entities using simple patterns
|
|
130
|
+
candidates = []
|
|
131
|
+
|
|
132
|
+
# Capitalized words (potential proper nouns)
|
|
133
|
+
capitalized_words = re.findall(r'\b[A-Z][a-zA-Z]{2,}\b', incoming_text)
|
|
134
|
+
candidates.extend(capitalized_words)
|
|
135
|
+
|
|
136
|
+
# Quoted strings
|
|
137
|
+
quoted_strings = re.findall(r'"([^"]+)"', incoming_text)
|
|
138
|
+
quoted_strings.extend(re.findall(r"'([^']+)'", incoming_text))
|
|
139
|
+
candidates.extend(quoted_strings)
|
|
140
|
+
|
|
141
|
+
# Technical terms (words with numbers, dots, underscores)
|
|
142
|
+
tech_terms = re.findall(r'\b[a-zA-Z][a-zA-Z0-9._-]*[a-zA-Z0-9]\b', incoming_text)
|
|
143
|
+
candidates.extend([t for t in tech_terms if '.' in t or '_' in t or any(c.isdigit() for c in t)])
|
|
144
|
+
|
|
145
|
+
# Filter out terms already present in existing text
|
|
146
|
+
new_terms = [t for t in candidates if t.lower() not in existing_lower and len(t.strip()) > 2]
|
|
147
|
+
|
|
148
|
+
# Deduplicate while preserving order
|
|
149
|
+
seen, deduped = set(), []
|
|
150
|
+
for t in new_terms:
|
|
151
|
+
if t.lower() not in seen:
|
|
152
|
+
seen.add(t.lower())
|
|
153
|
+
deduped.append(t)
|
|
154
|
+
|
|
155
|
+
if not deduped:
|
|
156
|
+
return existing_text
|
|
157
|
+
if len(deduped) == 1:
|
|
158
|
+
return f"{existing_text} with {deduped[0]}"
|
|
159
|
+
return f"{existing_text} with {', '.join(deduped[:-1])} and {deduped[-1]}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def resolve(user_id: str, content: str, embedding: list, conn) -> dict:
|
|
163
|
+
"""
|
|
164
|
+
Facade: decide what to do with an incoming memory.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
{
|
|
168
|
+
"action": "new" | "reinforce" | "replace" | "merge",
|
|
169
|
+
"content": str, # final content to store/update
|
|
170
|
+
"existing": dict | None, # matched row if any
|
|
171
|
+
}
|
|
172
|
+
"""
|
|
173
|
+
match = find_near_duplicate(user_id, embedding, conn)
|
|
174
|
+
|
|
175
|
+
if match is None:
|
|
176
|
+
return {"action": "new", "content": content, "existing": None}
|
|
177
|
+
|
|
178
|
+
sim = match["similarity"]
|
|
179
|
+
|
|
180
|
+
if sim >= REINFORCE_THRESHOLD:
|
|
181
|
+
return {"action": "reinforce", "content": match["content"], "existing": match}
|
|
182
|
+
|
|
183
|
+
# DEDUP_THRESHOLD ≤ sim < REINFORCE_THRESHOLD
|
|
184
|
+
if detect_contradiction(match["content"], content):
|
|
185
|
+
return {"action": "replace", "content": content, "existing": match}
|
|
186
|
+
|
|
187
|
+
merged = merge_entities(match["content"], content)
|
|
188
|
+
if merged == match["content"]:
|
|
189
|
+
# No new entities found — treat as paraphrase
|
|
190
|
+
return {"action": "reinforce", "content": match["content"], "existing": match}
|
|
191
|
+
|
|
192
|
+
return {"action": "merge", "content": merged, "existing": match}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yourmemory
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Persistent memory for Claude — Ebbinghaus forgetting curve, semantic deduplication, MCP-native
|
|
5
5
|
Author-email: Sachit Misra <mishrasachit1@gmail.com>
|
|
6
6
|
License: Apache License
|
|
@@ -163,8 +163,8 @@ License: Apache License
|
|
|
163
163
|
See the License for the specific language governing permissions and
|
|
164
164
|
limitations under the License.
|
|
165
165
|
|
|
166
|
-
Project-URL: Homepage, https://github.com/sachitrafa/
|
|
167
|
-
Project-URL: Repository, https://github.com/sachitrafa/
|
|
166
|
+
Project-URL: Homepage, https://github.com/sachitrafa/YourMemory
|
|
167
|
+
Project-URL: Repository, https://github.com/sachitrafa/YourMemory
|
|
168
168
|
Keywords: mcp,claude,memory,ebbinghaus,ai,sqlite,postgresql
|
|
169
169
|
Classifier: Programming Language :: Python :: 3
|
|
170
170
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -246,6 +246,8 @@ Importance additionally modulates the decay rate within each category. Memories
|
|
|
246
246
|
|
|
247
247
|
**Zero infrastructure required** — uses DuckDB out of the box. Two commands and you're done.
|
|
248
248
|
|
|
249
|
+
Supports **Python 3.11, 3.12, 3.13, and 3.14**.
|
|
250
|
+
|
|
249
251
|
### 1. Install
|
|
250
252
|
|
|
251
253
|
```bash
|
|
@@ -254,17 +256,23 @@ pip install yourmemory
|
|
|
254
256
|
|
|
255
257
|
All dependencies installed automatically. No clone, no Docker, no database setup.
|
|
256
258
|
|
|
257
|
-
### 2.
|
|
259
|
+
### 2. Run setup (once)
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
yourmemory-setup
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Downloads the spaCy language model and initialises the database. Run this once after install.
|
|
258
266
|
|
|
259
|
-
|
|
267
|
+
### 3. Get your config
|
|
260
268
|
|
|
261
269
|
```bash
|
|
262
270
|
yourmemory-path
|
|
263
271
|
```
|
|
264
272
|
|
|
265
|
-
|
|
273
|
+
Prints your full executable path and a ready-to-paste config for any MCP client. Copy it.
|
|
266
274
|
|
|
267
|
-
###
|
|
275
|
+
### 4. Wire into your AI client
|
|
268
276
|
|
|
269
277
|
The database is created automatically at `~/.yourmemory/memories.duckdb` on first use.
|
|
270
278
|
|
|
@@ -286,9 +294,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
|
|
|
286
294
|
|
|
287
295
|
#### Cline (VS Code)
|
|
288
296
|
|
|
289
|
-
VS Code doesn't inherit your shell PATH
|
|
297
|
+
VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
yourmemory-path
|
|
301
|
+
```
|
|
290
302
|
|
|
291
|
-
|
|
303
|
+
Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
|
|
292
304
|
|
|
293
305
|
```json
|
|
294
306
|
{
|
|
@@ -305,7 +317,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
|
|
|
305
317
|
}
|
|
306
318
|
```
|
|
307
319
|
|
|
308
|
-
|
|
320
|
+
Restart Cline after saving.
|
|
309
321
|
|
|
310
322
|
#### Cursor
|
|
311
323
|
|
|
@@ -346,7 +358,7 @@ Restart Claude Desktop.
|
|
|
346
358
|
|
|
347
359
|
YourMemory is a standard stdio MCP server. Works with Claude Code, Claude Desktop, Cline, Cursor, Windsurf, Continue, and Zed. Use the full path from `yourmemory-path` if the client doesn't inherit shell PATH.
|
|
348
360
|
|
|
349
|
-
###
|
|
361
|
+
### 5. Add memory instructions to your project
|
|
350
362
|
|
|
351
363
|
Copy `sample_CLAUDE.md` into your project root as `CLAUDE.md` and replace:
|
|
352
364
|
- `YOUR_NAME` — your name (e.g. `Alice`)
|
|
@@ -439,6 +451,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
|
|
|
439
451
|
|
|
440
452
|
- **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
|
|
441
453
|
- **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
|
|
454
|
+
- **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
|
|
442
455
|
- **APScheduler** — automatic 24h decay job
|
|
443
456
|
- **MCP** — Claude integration via Model Context Protocol
|
|
444
457
|
- **PostgreSQL + pgvector** — optional, for teams / large datasets
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import spacy
|
|
3
|
-
|
|
4
|
-
try:
|
|
5
|
-
_nlp = spacy.load("en_core_web_sm")
|
|
6
|
-
except OSError:
|
|
7
|
-
import subprocess, sys
|
|
8
|
-
subprocess.run(
|
|
9
|
-
[sys.executable, "-m", "pip", "install",
|
|
10
|
-
"https://github.com/explosion/spacy-models/releases/download/"
|
|
11
|
-
"en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl"],
|
|
12
|
-
check=True,
|
|
13
|
-
)
|
|
14
|
-
_nlp = spacy.load("en_core_web_sm")
|
|
15
|
-
|
|
16
|
-
_QUESTION_WORDS = {"what", "who", "where", "when", "why", "how", "which", "whose", "whom"}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def is_question(text: str) -> bool:
|
|
20
|
-
"""Return True if the text is a question — questions are not stored as memories."""
|
|
21
|
-
stripped = text.strip()
|
|
22
|
-
if stripped.endswith("?"):
|
|
23
|
-
return True
|
|
24
|
-
first_word = re.split(r"\s+", stripped.lower())[0]
|
|
25
|
-
return first_word in _QUESTION_WORDS
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def categorize(text: str) -> str:
|
|
29
|
-
"""
|
|
30
|
-
Use spaCy dependency parse to classify:
|
|
31
|
-
fact — declarative sentence with an explicit subject
|
|
32
|
-
assumption — imperative sentence with no subject (command/instruction)
|
|
33
|
-
"""
|
|
34
|
-
doc = _nlp(text)
|
|
35
|
-
has_subject = any(tok.dep_ in ("nsubj", "nsubjpass") for tok in doc)
|
|
36
|
-
return "fact" if has_subject else "assumption"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|