MemPalace: palace architecture, AAAK compression, knowledge graph
The memory system: - Palace structure: Wings (people/projects) → Rooms (topics) → Closets (AAAK compressed) → Drawers (verbatim transcripts) - Halls connect related rooms within a wing - Tunnels cross-reference rooms across wings - AAAK: 30x lossless compression dialect for AI agents - Knowledge graph: temporal entity-relationship triples (SQLite) - Palace graph: room-based navigation with tunnel detection - MCP server: 19 tools — search, graph traversal, agent diary, AAAK auto-teach - Onboarding: guided setup generates wing config + AAAK entity registry - Contradiction detection: catches wrong pronouns, names, ages - Auto-save hooks for Claude Code 96.6% Recall@5 on LongMemEval — highest zero-API score published. 100% with optional Haiku rerank (500/500). Local. Free. No API key required.
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
onboarding.py — MemPalace first-run setup.
|
||||
|
||||
Asks the user:
|
||||
1. How they're using MemPalace (work / personal / combo)
|
||||
2. Who the people in their life are (names, nicknames, relationships)
|
||||
3. What their projects are
|
||||
4. What they want their wings called
|
||||
|
||||
Seeds the entity_registry with confirmed data so MemPalace knows your world
|
||||
from minute one — before a single session is indexed.
|
||||
|
||||
Usage:
|
||||
python3 -m mempalace.onboarding
|
||||
or: mempalace init
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from mempalace.entity_registry import EntityRegistry
|
||||
from mempalace.entity_detector import detect_entities, scan_for_detection
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Default wing taxonomies by mode
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
DEFAULT_WINGS = {
|
||||
"work": [
|
||||
"projects",
|
||||
"clients",
|
||||
"team",
|
||||
"decisions",
|
||||
"research",
|
||||
],
|
||||
"personal": [
|
||||
"family",
|
||||
"health",
|
||||
"creative",
|
||||
"reflections",
|
||||
"relationships",
|
||||
],
|
||||
"combo": [
|
||||
"family",
|
||||
"work",
|
||||
"health",
|
||||
"creative",
|
||||
"projects",
|
||||
"reflections",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _hr():
|
||||
print(f"\n{'─' * 58}")
|
||||
|
||||
|
||||
def _header(text):
|
||||
print(f"\n{'=' * 58}")
|
||||
print(f" {text}")
|
||||
print(f"{'=' * 58}")
|
||||
|
||||
|
||||
def _ask(prompt, default=None):
|
||||
if default:
|
||||
val = input(f" {prompt} [{default}]: ").strip()
|
||||
return val if val else default
|
||||
return input(f" {prompt}: ").strip()
|
||||
|
||||
|
||||
def _yn(prompt, default="y"):
|
||||
val = input(f" {prompt} [{'Y/n' if default == 'y' else 'y/N'}]: ").strip().lower()
|
||||
if not val:
|
||||
return default == "y"
|
||||
return val.startswith("y")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Mode selection
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _ask_mode() -> str:
|
||||
_header("Welcome to MemPalace")
|
||||
print("""
|
||||
MemPalace is a personal memory system. To work well, it needs to know
|
||||
a little about your world — who the people are, what the projects
|
||||
are, and how you want your memory organized.
|
||||
|
||||
This takes about 2 minutes. You can always update it later.
|
||||
""")
|
||||
print(" How are you using MemPalace?")
|
||||
print()
|
||||
print(" [1] Work — notes, projects, clients, colleagues, decisions")
|
||||
print(" [2] Personal — diary, family, health, relationships, reflections")
|
||||
print(" [3] Both — personal and professional mixed")
|
||||
print()
|
||||
|
||||
while True:
|
||||
choice = input(" Your choice [1/2/3]: ").strip()
|
||||
if choice == "1":
|
||||
return "work"
|
||||
elif choice == "2":
|
||||
return "personal"
|
||||
elif choice == "3":
|
||||
return "combo"
|
||||
print(" Please enter 1, 2, or 3.")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: People
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _ask_people(mode: str) -> tuple[list, dict]:
|
||||
"""Returns (people_list, aliases_dict)."""
|
||||
people = []
|
||||
aliases = {} # nickname → full name
|
||||
|
||||
if mode in ("personal", "combo"):
|
||||
_hr()
|
||||
print("""
|
||||
Personal world — who are the important people in your life?
|
||||
|
||||
Format: name, relationship (e.g. "Riley, daughter" or just "Devon")
|
||||
For nicknames, you'll be asked separately.
|
||||
Type 'done' when finished.
|
||||
""")
|
||||
while True:
|
||||
entry = input(" Person: ").strip()
|
||||
if entry.lower() in ("done", ""):
|
||||
break
|
||||
parts = [p.strip() for p in entry.split(",", 1)]
|
||||
name = parts[0]
|
||||
relationship = parts[1] if len(parts) > 1 else ""
|
||||
if name:
|
||||
# Ask about nicknames
|
||||
nick = input(f" Nickname for {name}? (or enter to skip): ").strip()
|
||||
if nick:
|
||||
aliases[nick] = name
|
||||
people.append({"name": name, "relationship": relationship, "context": "personal"})
|
||||
|
||||
if mode in ("work", "combo"):
|
||||
_hr()
|
||||
print("""
|
||||
Work world — who are the colleagues, clients, or collaborators
|
||||
you'd want to find in your notes?
|
||||
|
||||
Format: name, role (e.g. "Ben, co-founder" or just "Sarah")
|
||||
Type 'done' when finished.
|
||||
""")
|
||||
while True:
|
||||
entry = input(" Person: ").strip()
|
||||
if entry.lower() in ("done", ""):
|
||||
break
|
||||
parts = [p.strip() for p in entry.split(",", 1)]
|
||||
name = parts[0]
|
||||
role = parts[1] if len(parts) > 1 else ""
|
||||
if name:
|
||||
people.append({"name": name, "relationship": role, "context": "work"})
|
||||
|
||||
return people, aliases
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Projects
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _ask_projects(mode: str) -> list:
|
||||
if mode == "personal":
|
||||
return []
|
||||
|
||||
_hr()
|
||||
print("""
|
||||
What are your main projects? (These help MemPalace distinguish project
|
||||
names from person names — e.g. "Lantern" the project vs. "Lantern" the word.)
|
||||
|
||||
Type 'done' when finished.
|
||||
""")
|
||||
projects = []
|
||||
while True:
|
||||
proj = input(" Project: ").strip()
|
||||
if proj.lower() in ("done", ""):
|
||||
break
|
||||
if proj:
|
||||
projects.append(proj)
|
||||
return projects
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 4: Wings
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _ask_wings(mode: str) -> list:
|
||||
defaults = DEFAULT_WINGS[mode]
|
||||
_hr()
|
||||
print(f"""
|
||||
Wings are the top-level categories in your memory palace.
|
||||
|
||||
Suggested wings for {mode} mode:
|
||||
{", ".join(defaults)}
|
||||
|
||||
Press enter to keep these, or type your own comma-separated list.
|
||||
""")
|
||||
custom = input(" Wings: ").strip()
|
||||
if custom:
|
||||
return [w.strip() for w in custom.split(",") if w.strip()]
|
||||
return defaults
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 5: Auto-detect from files
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _auto_detect(directory: str, known_people: list) -> list:
|
||||
"""Scan directory for additional entity candidates."""
|
||||
known_names = {p["name"].lower() for p in known_people}
|
||||
|
||||
try:
|
||||
files = scan_for_detection(directory)
|
||||
if not files:
|
||||
return []
|
||||
detected = detect_entities(files)
|
||||
new_people = [
|
||||
e
|
||||
for e in detected["people"]
|
||||
if e["name"].lower() not in known_names and e["confidence"] >= 0.7
|
||||
]
|
||||
return new_people
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Step 6: Ambiguity warnings
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _warn_ambiguous(people: list) -> list:
|
||||
"""
|
||||
Flag names that are also common English words.
|
||||
Returns list of ambiguous names for user awareness.
|
||||
"""
|
||||
from mempalace.entity_registry import COMMON_ENGLISH_WORDS
|
||||
|
||||
ambiguous = []
|
||||
for p in people:
|
||||
if p["name"].lower() in COMMON_ENGLISH_WORDS:
|
||||
ambiguous.append(p["name"])
|
||||
return ambiguous
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Main onboarding flow
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _generate_aaak_bootstrap(people: list, projects: list, wings: list, mode: str, config_dir: Path = None):
|
||||
"""
|
||||
Generate AAAK entity registry + critical facts bootstrap from onboarding data.
|
||||
These files teach the AI about the user's world from session one.
|
||||
"""
|
||||
mempalace_dir = Path(config_dir) if config_dir else Path.home() / ".mempalace"
|
||||
mempalace_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Build AAAK entity codes (first 3 letters of name, uppercase)
|
||||
entity_codes = {}
|
||||
for p in people:
|
||||
name = p["name"]
|
||||
code = name[:3].upper()
|
||||
# Handle collisions
|
||||
while code in entity_codes.values():
|
||||
code = name[:4].upper()
|
||||
entity_codes[name] = code
|
||||
|
||||
# AAAK entity registry
|
||||
registry_lines = [
|
||||
"# AAAK Entity Registry",
|
||||
"# Auto-generated by mempalace init. Update as needed.",
|
||||
"",
|
||||
"## People",
|
||||
]
|
||||
for p in people:
|
||||
name = p["name"]
|
||||
code = entity_codes[name]
|
||||
rel = p.get("relationship", "")
|
||||
ctx = p.get("context", "")
|
||||
registry_lines.append(f" {code}={name} ({rel})" if rel else f" {code}={name}")
|
||||
|
||||
if projects:
|
||||
registry_lines.extend(["", "## Projects"])
|
||||
for proj in projects:
|
||||
code = proj[:4].upper()
|
||||
registry_lines.append(f" {code}={proj}")
|
||||
|
||||
registry_lines.extend([
|
||||
"",
|
||||
"## AAAK Quick Reference",
|
||||
" Symbols: ♡=love ★=importance ⚠=warning →=relationship |=separator",
|
||||
" Structure: KEY:value | GROUP(details) | entity.attribute",
|
||||
" Read naturally — expand codes, treat *markers* as emotional context.",
|
||||
])
|
||||
|
||||
(mempalace_dir / "aaak_entities.md").write_text("\n".join(registry_lines))
|
||||
|
||||
# Critical facts bootstrap (pre-palace — before any mining)
|
||||
facts_lines = [
|
||||
"# Critical Facts (bootstrap — will be enriched after mining)",
|
||||
"",
|
||||
]
|
||||
|
||||
personal_people = [p for p in people if p.get("context") == "personal"]
|
||||
work_people = [p for p in people if p.get("context") == "work"]
|
||||
|
||||
if personal_people:
|
||||
facts_lines.append("## People (personal)")
|
||||
for p in personal_people:
|
||||
code = entity_codes[p["name"]]
|
||||
rel = p.get("relationship", "")
|
||||
facts_lines.append(f"- **{p['name']}** ({code}) — {rel}" if rel else f"- **{p['name']}** ({code})")
|
||||
facts_lines.append("")
|
||||
|
||||
if work_people:
|
||||
facts_lines.append("## People (work)")
|
||||
for p in work_people:
|
||||
code = entity_codes[p["name"]]
|
||||
rel = p.get("relationship", "")
|
||||
facts_lines.append(f"- **{p['name']}** ({code}) — {rel}" if rel else f"- **{p['name']}** ({code})")
|
||||
facts_lines.append("")
|
||||
|
||||
if projects:
|
||||
facts_lines.append("## Projects")
|
||||
for proj in projects:
|
||||
facts_lines.append(f"- **{proj}**")
|
||||
facts_lines.append("")
|
||||
|
||||
facts_lines.extend([
|
||||
"## Palace",
|
||||
f"Wings: {', '.join(wings)}",
|
||||
f"Mode: {mode}",
|
||||
"",
|
||||
"*This file will be enriched by palace_facts.py after mining.*",
|
||||
])
|
||||
|
||||
(mempalace_dir / "critical_facts.md").write_text("\n".join(facts_lines))
|
||||
|
||||
|
||||
def run_onboarding(
|
||||
directory: str = ".",
|
||||
config_dir: Path = None,
|
||||
auto_detect: bool = True,
|
||||
) -> EntityRegistry:
|
||||
"""
|
||||
Run the full onboarding flow.
|
||||
Returns the seeded EntityRegistry.
|
||||
"""
|
||||
# Step 1: Mode
|
||||
mode = _ask_mode()
|
||||
|
||||
# Step 2: People
|
||||
people, aliases = _ask_people(mode)
|
||||
|
||||
# Step 3: Projects
|
||||
projects = _ask_projects(mode)
|
||||
|
||||
# Step 4: Wings (stored in config, not registry — just show user)
|
||||
wings = _ask_wings(mode)
|
||||
|
||||
# Step 5: Auto-detect additional people from files
|
||||
if auto_detect and _yn("\nScan your files for additional names we might have missed?"):
|
||||
directory = _ask("Directory to scan", default=directory)
|
||||
detected = _auto_detect(directory, people)
|
||||
if detected:
|
||||
_hr()
|
||||
print(f"\n Found {len(detected)} additional name candidates:\n")
|
||||
for e in detected:
|
||||
print(
|
||||
f" {e['name']:20} confidence={e['confidence']:.0%} "
|
||||
f"({', '.join(e['signals'][:1])})"
|
||||
)
|
||||
print()
|
||||
if _yn(" Add any of these to your registry?"):
|
||||
for e in detected:
|
||||
ans = input(f" {e['name']} — (p)erson, (s)kip? ").strip().lower()
|
||||
if ans == "p":
|
||||
rel = input(f" Relationship/role for {e['name']}? ").strip()
|
||||
ctx = (
|
||||
"personal"
|
||||
if mode == "personal"
|
||||
else (
|
||||
"work"
|
||||
if mode == "work"
|
||||
else input(" Context — (p)ersonal or (w)ork? ")
|
||||
.strip()
|
||||
.lower()
|
||||
.replace("w", "work")
|
||||
.replace("p", "personal")
|
||||
)
|
||||
)
|
||||
people.append({"name": e["name"], "relationship": rel, "context": ctx})
|
||||
|
||||
# Step 6: Warn about ambiguous names
|
||||
ambiguous = _warn_ambiguous(people)
|
||||
if ambiguous:
|
||||
_hr()
|
||||
print(f"""
|
||||
Heads up — these names are also common English words:
|
||||
{", ".join(ambiguous)}
|
||||
|
||||
MemPalace will check the context before treating them as person names.
|
||||
For example: "I picked up Riley" → person.
|
||||
"Have you ever tried" → adverb.
|
||||
""")
|
||||
|
||||
# Build and save registry
|
||||
registry = EntityRegistry.load(config_dir)
|
||||
registry.seed(mode=mode, people=people, projects=projects, aliases=aliases)
|
||||
|
||||
# Generate AAAK entity registry + critical facts bootstrap
|
||||
_generate_aaak_bootstrap(people, projects, wings, mode, config_dir)
|
||||
|
||||
# Summary
|
||||
_header("Setup Complete")
|
||||
print()
|
||||
print(f" {registry.summary()}")
|
||||
print(f"\n Wings: {', '.join(wings)}")
|
||||
print(f"\n Registry saved to: {registry._path}")
|
||||
print(f"\n AAAK entity registry: ~/.mempalace/aaak_entities.md")
|
||||
print(f" Critical facts bootstrap: ~/.mempalace/critical_facts.md")
|
||||
print(f"\n Your AI will know your world from the first session.")
|
||||
print()
|
||||
|
||||
return registry
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Quick setup (non-interactive, for testing)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def quick_setup(
|
||||
mode: str,
|
||||
people: list,
|
||||
projects: list = None,
|
||||
aliases: dict = None,
|
||||
config_dir: Path = None,
|
||||
) -> EntityRegistry:
|
||||
"""
|
||||
Programmatic setup without interactive prompts.
|
||||
Used in tests and benchmark scripts.
|
||||
|
||||
people: list of dicts {"name": str, "relationship": str, "context": str}
|
||||
"""
|
||||
registry = EntityRegistry.load(config_dir)
|
||||
registry.seed(
|
||||
mode=mode,
|
||||
people=people,
|
||||
projects=projects or [],
|
||||
aliases=aliases or {},
|
||||
)
|
||||
return registry
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CLI
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
directory = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||
run_onboarding(directory=directory)
|
||||
Reference in New Issue
Block a user