yourmemory 1.2.1__tar.gz → 1.2.2__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.2}/PKG-INFO +13 -6
- {yourmemory-1.2.1 → yourmemory-1.2.2}/README.md +10 -3
- {yourmemory-1.2.1 → yourmemory-1.2.2}/memory_mcp.py +34 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/pyproject.toml +4 -3
- yourmemory-1.2.2/src/services/extract.py +52 -0
- yourmemory-1.2.2/src/services/resolve_fallback.py +192 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2/yourmemory.egg-info}/PKG-INFO +13 -6
- {yourmemory-1.2.1 → yourmemory-1.2.2}/yourmemory.egg-info/SOURCES.txt +1 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/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.2}/LICENSE +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/setup.cfg +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/__init__.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/app.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/db/connection.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/db/duckdb_schema.sql +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/db/migrate.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/db/schema.sql +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/db/sqlite_schema.sql +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/jobs/decay_job.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/routes/__init__.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/routes/agents.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/routes/memories.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/routes/retrieve.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/services/__init__.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/services/agent_registry.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/services/api_keys.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/services/decay.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/services/embed.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/services/extract_fallback.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/services/resolve.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/src/services/retrieve.py +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/yourmemory.egg-info/dependency_links.txt +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/yourmemory.egg-info/requires.txt +0 -0
- {yourmemory-1.2.1 → yourmemory-1.2.2}/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.2
|
|
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
|
|
@@ -286,9 +288,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
|
|
|
286
288
|
|
|
287
289
|
#### Cline (VS Code)
|
|
288
290
|
|
|
289
|
-
VS Code doesn't inherit your shell PATH
|
|
291
|
+
VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
yourmemory-path
|
|
295
|
+
```
|
|
290
296
|
|
|
291
|
-
|
|
297
|
+
Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
|
|
292
298
|
|
|
293
299
|
```json
|
|
294
300
|
{
|
|
@@ -305,7 +311,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
|
|
|
305
311
|
}
|
|
306
312
|
```
|
|
307
313
|
|
|
308
|
-
|
|
314
|
+
Restart Cline after saving.
|
|
309
315
|
|
|
310
316
|
#### Cursor
|
|
311
317
|
|
|
@@ -439,6 +445,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
|
|
|
439
445
|
|
|
440
446
|
- **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
|
|
441
447
|
- **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
|
|
448
|
+
- **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
|
|
442
449
|
- **APScheduler** — automatic 24h decay job
|
|
443
450
|
- **MCP** — Claude integration via Model Context Protocol
|
|
444
451
|
- **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
|
|
@@ -91,9 +93,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
|
|
|
91
93
|
|
|
92
94
|
#### Cline (VS Code)
|
|
93
95
|
|
|
94
|
-
VS Code doesn't inherit your shell PATH
|
|
96
|
+
VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
yourmemory-path
|
|
100
|
+
```
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
|
|
97
103
|
|
|
98
104
|
```json
|
|
99
105
|
{
|
|
@@ -110,7 +116,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
|
|
|
110
116
|
}
|
|
111
117
|
```
|
|
112
118
|
|
|
113
|
-
|
|
119
|
+
Restart Cline after saving.
|
|
114
120
|
|
|
115
121
|
#### Cursor
|
|
116
122
|
|
|
@@ -244,6 +250,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
|
|
|
244
250
|
|
|
245
251
|
- **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
|
|
246
252
|
- **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
|
|
253
|
+
- **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
|
|
247
254
|
- **APScheduler** — automatic 24h decay job
|
|
248
255
|
- **MCP** — Claude integration via Model Context Protocol
|
|
249
256
|
- **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.2"
|
|
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"
|
|
@@ -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.2
|
|
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
|
|
@@ -286,9 +288,13 @@ Reload Claude Code (`Cmd+Shift+P` → `Developer: Reload Window`).
|
|
|
286
288
|
|
|
287
289
|
#### Cline (VS Code)
|
|
288
290
|
|
|
289
|
-
VS Code doesn't inherit your shell PATH
|
|
291
|
+
VS Code doesn't inherit your shell PATH. Run this in terminal to get the exact config to paste:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
yourmemory-path
|
|
295
|
+
```
|
|
290
296
|
|
|
291
|
-
|
|
297
|
+
Then in Cline → **MCP Servers** → **Edit MCP Settings**, paste the output. It looks like:
|
|
292
298
|
|
|
293
299
|
```json
|
|
294
300
|
{
|
|
@@ -305,7 +311,7 @@ In Cline → **MCP Servers** → **Edit MCP Settings**:
|
|
|
305
311
|
}
|
|
306
312
|
```
|
|
307
313
|
|
|
308
|
-
|
|
314
|
+
Restart Cline after saving.
|
|
309
315
|
|
|
310
316
|
#### Cursor
|
|
311
317
|
|
|
@@ -439,6 +445,7 @@ Runs automatically every 24 hours on startup — no cron needed. Memories below
|
|
|
439
445
|
|
|
440
446
|
- **DuckDB** — default backend, zero setup, native vector similarity (same quality as pgvector)
|
|
441
447
|
- **sentence-transformers** — local embeddings (`all-mpnet-base-v2`, 768 dims, no external service needed)
|
|
448
|
+
- **spaCy 3.8.13+** — local NLP for deduplication and categorization (Python 3.11–3.14 compatible)
|
|
442
449
|
- **APScheduler** — automatic 24h decay job
|
|
443
450
|
- **MCP** — Claude integration via Model Context Protocol
|
|
444
451
|
- **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
|
|
File without changes
|