6d8c462219
Fix E402 import ordering, F841 unused variable, F541 unnecessary f-strings, F401 unused import, and auto-format 6 files.
490 lines
18 KiB
Python
490 lines
18 KiB
Python
#!/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", "")
|
|
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("\n AAAK entity registry: ~/.mempalace/aaak_entities.md")
|
|
print(" Critical facts bootstrap: ~/.mempalace/critical_facts.md")
|
|
print("\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)
|