Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions mempalace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import argparse
from pathlib import Path

from .config import MempalaceConfig
from .config import MempalaceConfig, get_embedding_function


def cmd_init(args):
Expand Down Expand Up @@ -175,8 +175,9 @@ def cmd_repair(args):

# Try to read existing drawers
try:
ef = get_embedding_function()
client = chromadb.PersistentClient(path=palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection("mempalace_drawers", embedding_function=ef)
total = col.count()
print(f" Drawers found: {total}")
except Exception as e:
Expand Down Expand Up @@ -213,7 +214,7 @@ def cmd_repair(args):

print(" Rebuilding collection...")
client.delete_collection("mempalace_drawers")
new_col = client.create_collection("mempalace_drawers")
new_col = client.create_collection("mempalace_drawers", embedding_function=ef)

filed = 0
for i in range(0, len(all_ids), batch_size):
Expand Down Expand Up @@ -287,8 +288,9 @@ def cmd_compress(args):

# Connect to palace
try:
ef = get_embedding_function()
client = chromadb.PersistentClient(path=palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection("mempalace_drawers", embedding_function=ef)
except Exception:
print(f"\n No palace found at {palace_path}")
print(" Run: mempalace init <dir> then mempalace mine <dir>")
Expand Down Expand Up @@ -359,7 +361,9 @@ def cmd_compress(args):
# Store compressed versions (unless dry-run)
if not args.dry_run:
try:
comp_col = client.get_or_create_collection("mempalace_compressed")
comp_col = client.get_or_create_collection(
"mempalace_compressed", embedding_function=ef
)
for doc_id, compressed, meta, stats in compressed_entries:
comp_meta = dict(meta)
comp_meta["compression_ratio"] = round(stats["ratio"], 1)
Expand Down
54 changes: 54 additions & 0 deletions mempalace/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
"""

import json
import logging
import os
import re
from pathlib import Path

logger = logging.getLogger("mempalace")


# ── Input validation ──────────────────────────────────────────────────────────
# Shared sanitizers for wing/room/entity names. Prevents path traversal,
Expand Down Expand Up @@ -197,6 +200,14 @@ def init(self):
pass
return self._config_file

@property
def embedding_model(self):
"""Configured embedding model name, or None for ChromaDB default."""
env_val = os.environ.get("MEMPALACE_EMBEDDING_MODEL")
if env_val:
return env_val
return self._file_config.get("embedding_model", None)

def save_people_map(self, people_map):
"""Write people_map.json to config directory.

Expand All @@ -207,3 +218,46 @@ def save_people_map(self, people_map):
with open(self._people_map_file, "w") as f:
json.dump(people_map, f, indent=2)
return self._people_map_file


# ── Embedding function singleton ─────────────────────────────────────────────

_embedding_function = None
_embedding_function_resolved = False


def get_embedding_function(config=None):
"""Return the configured ChromaDB embedding function, or None for default.

Checks MEMPALACE_EMBEDDING_MODEL env var first, then config.json
``embedding_model`` key. When a model name is found, attempts to import
``SentenceTransformerEmbeddingFunction`` from chromadb. If
sentence-transformers is not installed the import will fail and we fall
back to None (ChromaDB's built-in default), logging a warning.

The result is cached so the function is only resolved once per process.
"""
global _embedding_function, _embedding_function_resolved
if _embedding_function_resolved:
return _embedding_function

_embedding_function_resolved = True

cfg = config or MempalaceConfig()
model_name = cfg.embedding_model
if not model_name:
return None

try:
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction

_embedding_function = SentenceTransformerEmbeddingFunction(model_name=model_name)
logger.info("Using embedding model: %s", model_name)
except Exception:
logger.warning(
"sentence-transformers not installed — falling back to ChromaDB default. "
"Install with: pip install mempalace[multilingual]"
)
_embedding_function = None

return _embedding_function
22 changes: 16 additions & 6 deletions mempalace/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import chromadb

from .config import MempalaceConfig
from .config import MempalaceConfig, get_embedding_function


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -92,7 +92,9 @@ def generate(self) -> str:
"""Pull top drawers from ChromaDB and format as compact L1 text."""
try:
client = chromadb.PersistentClient(path=self.palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection(
"mempalace_drawers", embedding_function=get_embedding_function()
)
except Exception:
return "## L1 — No palace found. Run: mempalace mine <dir>"

Expand Down Expand Up @@ -197,7 +199,9 @@ def retrieve(self, wing: str = None, room: str = None, n_results: int = 10) -> s
"""Retrieve drawers filtered by wing and/or room."""
try:
client = chromadb.PersistentClient(path=self.palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection(
"mempalace_drawers", embedding_function=get_embedding_function()
)
except Exception:
return "No palace found."

Expand Down Expand Up @@ -261,7 +265,9 @@ def search(self, query: str, wing: str = None, room: str = None, n_results: int
"""Semantic search, returns compact result text."""
try:
client = chromadb.PersistentClient(path=self.palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection(
"mempalace_drawers", embedding_function=get_embedding_function()
)
except Exception:
return "No palace found."

Expand Down Expand Up @@ -317,7 +323,9 @@ def search_raw(
"""Return raw dicts instead of formatted text."""
try:
client = chromadb.PersistentClient(path=self.palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection(
"mempalace_drawers", embedding_function=get_embedding_function()
)
except Exception:
return []

Expand Down Expand Up @@ -438,7 +446,9 @@ def status(self) -> dict:
# Count drawers
try:
client = chromadb.PersistentClient(path=self.palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection(
"mempalace_drawers", embedding_function=get_embedding_function()
)
count = col.count()
result["total_drawers"] = count
except Exception:
Expand Down
11 changes: 8 additions & 3 deletions mempalace/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from datetime import datetime
from pathlib import Path

from .config import MempalaceConfig, sanitize_name, sanitize_content
from .config import MempalaceConfig, sanitize_name, sanitize_content, get_embedding_function
from .version import __version__
from .searcher import search_memories
from .palace_graph import traverse, find_tunnels, graph_stats
Expand Down Expand Up @@ -116,11 +116,16 @@ def _get_collection(create=False):
"""Return the ChromaDB collection, caching the client between calls."""
global _collection_cache
try:
ef = get_embedding_function()
client = _get_client()
if create:
_collection_cache = client.get_or_create_collection(_config.collection_name)
_collection_cache = client.get_or_create_collection(
_config.collection_name, embedding_function=ef
)
elif _collection_cache is None:
_collection_cache = client.get_collection(_config.collection_name)
_collection_cache = client.get_collection(
_config.collection_name, embedding_function=ef
)
return _collection_cache
except Exception:
return None
Expand Down
8 changes: 5 additions & 3 deletions mempalace/miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import chromadb

from .config import get_embedding_function
from .palace import SKIP_DIRS, get_collection, file_already_mined

READABLE_EXTENSIONS = {
Expand Down Expand Up @@ -51,8 +52,8 @@
"package-lock.json",
}

CHUNK_SIZE = 800 # chars per drawer
CHUNK_OVERLAP = 100 # overlap between chunks
CHUNK_SIZE = 450 # chars per drawer
CHUNK_OVERLAP = 50 # overlap between chunks
MIN_CHUNK_SIZE = 50 # skip tiny chunks
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB — skip files larger than this

Expand Down Expand Up @@ -615,8 +616,9 @@ def mine(
def status(palace_path: str):
"""Show what's been filed in the palace."""
try:
ef = get_embedding_function()
client = chromadb.PersistentClient(path=palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection("mempalace_drawers", embedding_function=ef)
except Exception:
print(f"\n No palace found at {palace_path}")
print(" Run: mempalace init <dir> then mempalace mine <dir>")
Expand Down
7 changes: 5 additions & 2 deletions mempalace/palace.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import os
import chromadb

from .config import get_embedding_function

SKIP_DIRS = {
".git",
"node_modules",
Expand Down Expand Up @@ -41,11 +43,12 @@ def get_collection(palace_path: str, collection_name: str = "mempalace_drawers")
os.chmod(palace_path, 0o700)
except (OSError, NotImplementedError):
pass
ef = get_embedding_function()
client = chromadb.PersistentClient(path=palace_path)
try:
return client.get_collection(collection_name)
return client.get_collection(collection_name, embedding_function=ef)
except Exception:
return client.create_collection(collection_name)
return client.create_collection(collection_name, embedding_function=ef)


def file_already_mined(collection, source_file: str, check_mtime: bool = False) -> bool:
Expand Down
5 changes: 3 additions & 2 deletions mempalace/palace_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
"""

from collections import defaultdict, Counter
from .config import MempalaceConfig
from .config import MempalaceConfig, get_embedding_function

import chromadb


def _get_collection(config=None):
config = config or MempalaceConfig()
try:
ef = get_embedding_function()
client = chromadb.PersistentClient(path=config.palace_path)
return client.get_collection(config.collection_name)
return client.get_collection(config.collection_name, embedding_function=ef)
except Exception:
return None

Expand Down
8 changes: 6 additions & 2 deletions mempalace/searcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

import chromadb

from .config import get_embedding_function

logger = logging.getLogger("mempalace_mcp")


Expand All @@ -24,8 +26,9 @@ def search(query: str, palace_path: str, wing: str = None, room: str = None, n_r
Optionally filter by wing (project) or room (aspect).
"""
try:
ef = get_embedding_function()
client = chromadb.PersistentClient(path=palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection("mempalace_drawers", embedding_function=ef)
except Exception:
print(f"\n No palace found at {palace_path}")
print(" Run: mempalace init <dir> then mempalace mine <dir>")
Expand Down Expand Up @@ -98,8 +101,9 @@ def search_memories(
Used by the MCP server and other callers that need data.
"""
try:
ef = get_embedding_function()
client = chromadb.PersistentClient(path=palace_path)
col = client.get_collection("mempalace_drawers")
col = client.get_collection("mempalace_drawers", embedding_function=ef)
except Exception as e:
logger.error("No palace found at %s: %s", palace_path, e)
return {
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mempalace = "mempalace:main"

[project.optional-dependencies]
dev = ["pytest>=7.0", "pytest-cov>=4.0", "ruff>=0.4.0", "psutil>=5.9"]
multilingual = ["sentence-transformers>=3.0"]
spellcheck = ["autocorrect>=2.0"]

[dependency-groups]
Expand Down
Loading