mempalace-code 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ onboarding.py — MemPalace first-run setup.
4
+
5
+ Asks the user:
6
+ 1. How they're using MemPalace (work / personal / combo)
7
+ 2. Who the people in their life are (names, nicknames, relationships)
8
+ 3. What their projects are
9
+ 4. What they want their wings called
10
+
11
+ Seeds the entity_registry with confirmed data so MemPalace knows your world
12
+ from minute one — before a single session is indexed.
13
+
14
+ Usage:
15
+ python3 -m mempalace.onboarding
16
+ or: mempalace init
17
+ """
18
+
19
+ from pathlib import Path
20
+ from mempalace.entity_registry import EntityRegistry
21
+ from mempalace.entity_detector import detect_entities, scan_for_detection
22
+
23
+
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+ # Default wing taxonomies by mode
26
+ # ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ DEFAULT_WINGS = {
29
+ "work": [
30
+ "projects",
31
+ "clients",
32
+ "team",
33
+ "decisions",
34
+ "research",
35
+ ],
36
+ "personal": [
37
+ "family",
38
+ "health",
39
+ "creative",
40
+ "reflections",
41
+ "relationships",
42
+ ],
43
+ "combo": [
44
+ "family",
45
+ "work",
46
+ "health",
47
+ "creative",
48
+ "projects",
49
+ "reflections",
50
+ ],
51
+ }
52
+
53
+
54
+ # ─────────────────────────────────────────────────────────────────────────────
55
+ # Helpers
56
+ # ─────────────────────────────────────────────────────────────────────────────
57
+
58
+
59
+ def _hr():
60
+ print(f"\n{'─' * 58}")
61
+
62
+
63
+ def _header(text):
64
+ print(f"\n{'=' * 58}")
65
+ print(f" {text}")
66
+ print(f"{'=' * 58}")
67
+
68
+
69
+ def _ask(prompt, default=None):
70
+ if default:
71
+ val = input(f" {prompt} [{default}]: ").strip()
72
+ return val if val else default
73
+ return input(f" {prompt}: ").strip()
74
+
75
+
76
+ def _yn(prompt, default="y"):
77
+ val = input(f" {prompt} [{'Y/n' if default == 'y' else 'y/N'}]: ").strip().lower()
78
+ if not val:
79
+ return default == "y"
80
+ return val.startswith("y")
81
+
82
+
83
+ # ─────────────────────────────────────────────────────────────────────────────
84
+ # Step 1: Mode selection
85
+ # ─────────────────────────────────────────────────────────────────────────────
86
+
87
+
88
+ def _ask_mode() -> str:
89
+ _header("Welcome to MemPalace")
90
+ print("""
91
+ MemPalace is a personal memory system. To work well, it needs to know
92
+ a little about your world — who the people are, what the projects
93
+ are, and how you want your memory organized.
94
+
95
+ This takes about 2 minutes. You can always update it later.
96
+ """)
97
+ print(" How are you using MemPalace?")
98
+ print()
99
+ print(" [1] Work — notes, projects, clients, colleagues, decisions")
100
+ print(" [2] Personal — diary, family, health, relationships, reflections")
101
+ print(" [3] Both — personal and professional mixed")
102
+ print()
103
+
104
+ while True:
105
+ choice = input(" Your choice [1/2/3]: ").strip()
106
+ if choice == "1":
107
+ return "work"
108
+ elif choice == "2":
109
+ return "personal"
110
+ elif choice == "3":
111
+ return "combo"
112
+ print(" Please enter 1, 2, or 3.")
113
+
114
+
115
+ # ─────────────────────────────────────────────────────────────────────────────
116
+ # Step 2: People
117
+ # ─────────────────────────────────────────────────────────────────────────────
118
+
119
+
120
+ def _ask_people(mode: str) -> tuple[list, dict]:
121
+ """Returns (people_list, aliases_dict)."""
122
+ people = []
123
+ aliases = {} # nickname → full name
124
+
125
+ if mode in ("personal", "combo"):
126
+ _hr()
127
+ print("""
128
+ Personal world — who are the important people in your life?
129
+
130
+ Format: name, relationship (e.g. "Riley, daughter" or just "Devon")
131
+ For nicknames, you'll be asked separately.
132
+ Type 'done' when finished.
133
+ """)
134
+ while True:
135
+ entry = input(" Person: ").strip()
136
+ if entry.lower() in ("done", ""):
137
+ break
138
+ parts = [p.strip() for p in entry.split(",", 1)]
139
+ name = parts[0]
140
+ relationship = parts[1] if len(parts) > 1 else ""
141
+ if name:
142
+ # Ask about nicknames
143
+ nick = input(f" Nickname for {name}? (or enter to skip): ").strip()
144
+ if nick:
145
+ aliases[nick] = name
146
+ people.append({"name": name, "relationship": relationship, "context": "personal"})
147
+
148
+ if mode in ("work", "combo"):
149
+ _hr()
150
+ print("""
151
+ Work world — who are the colleagues, clients, or collaborators
152
+ you'd want to find in your notes?
153
+
154
+ Format: name, role (e.g. "Ben, co-founder" or just "Sarah")
155
+ Type 'done' when finished.
156
+ """)
157
+ while True:
158
+ entry = input(" Person: ").strip()
159
+ if entry.lower() in ("done", ""):
160
+ break
161
+ parts = [p.strip() for p in entry.split(",", 1)]
162
+ name = parts[0]
163
+ role = parts[1] if len(parts) > 1 else ""
164
+ if name:
165
+ people.append({"name": name, "relationship": role, "context": "work"})
166
+
167
+ return people, aliases
168
+
169
+
170
+ # ─────────────────────────────────────────────────────────────────────────────
171
+ # Step 3: Projects
172
+ # ─────────────────────────────────────────────────────────────────────────────
173
+
174
+
175
+ def _ask_projects(mode: str) -> list:
176
+ if mode == "personal":
177
+ return []
178
+
179
+ _hr()
180
+ print("""
181
+ What are your main projects? (These help MemPalace distinguish project
182
+ names from person names — e.g. "Lantern" the project vs. "Lantern" the word.)
183
+
184
+ Type 'done' when finished.
185
+ """)
186
+ projects = []
187
+ while True:
188
+ proj = input(" Project: ").strip()
189
+ if proj.lower() in ("done", ""):
190
+ break
191
+ if proj:
192
+ projects.append(proj)
193
+ return projects
194
+
195
+
196
+ # ─────────────────────────────────────────────────────────────────────────────
197
+ # Step 4: Wings
198
+ # ─────────────────────────────────────────────────────────────────────────────
199
+
200
+
201
+ def _ask_wings(mode: str) -> list:
202
+ defaults = DEFAULT_WINGS[mode]
203
+ _hr()
204
+ print(f"""
205
+ Wings are the top-level categories in your memory palace.
206
+
207
+ Suggested wings for {mode} mode:
208
+ {", ".join(defaults)}
209
+
210
+ Press enter to keep these, or type your own comma-separated list.
211
+ """)
212
+ custom = input(" Wings: ").strip()
213
+ if custom:
214
+ return [w.strip() for w in custom.split(",") if w.strip()]
215
+ return defaults
216
+
217
+
218
+ # ─────────────────────────────────────────────────────────────────────────────
219
+ # Step 5: Auto-detect from files
220
+ # ─────────────────────────────────────────────────────────────────────────────
221
+
222
+
223
+ def _auto_detect(directory: str, known_people: list) -> list:
224
+ """Scan directory for additional entity candidates."""
225
+ known_names = {p["name"].lower() for p in known_people}
226
+
227
+ try:
228
+ files = scan_for_detection(directory)
229
+ if not files:
230
+ return []
231
+ detected = detect_entities(files)
232
+ new_people = [
233
+ e
234
+ for e in detected["people"]
235
+ if e["name"].lower() not in known_names and e["confidence"] >= 0.7
236
+ ]
237
+ return new_people
238
+ except Exception:
239
+ return []
240
+
241
+
242
+ # ─────────────────────────────────────────────────────────────────────────────
243
+ # Step 6: Ambiguity warnings
244
+ # ─────────────────────────────────────────────────────────────────────────────
245
+
246
+
247
+ def _warn_ambiguous(people: list) -> list:
248
+ """
249
+ Flag names that are also common English words.
250
+ Returns list of ambiguous names for user awareness.
251
+ """
252
+ from mempalace.entity_registry import COMMON_ENGLISH_WORDS
253
+
254
+ ambiguous = []
255
+ for p in people:
256
+ if p["name"].lower() in COMMON_ENGLISH_WORDS:
257
+ ambiguous.append(p["name"])
258
+ return ambiguous
259
+
260
+
261
+ # ─────────────────────────────────────────────────────────────────────────────
262
+ # Main onboarding flow
263
+ # ─────────────────────────────────────────────────────────────────────────────
264
+
265
+
266
+ def _generate_aaak_bootstrap(
267
+ people: list, projects: list, wings: list, mode: str, config_dir: Path = None
268
+ ):
269
+ """
270
+ Generate AAAK entity registry + critical facts bootstrap from onboarding data.
271
+ These files teach the AI about the user's world from session one.
272
+ """
273
+ mempalace_dir = Path(config_dir) if config_dir else Path.home() / ".mempalace"
274
+ mempalace_dir.mkdir(parents=True, exist_ok=True)
275
+
276
+ # Build AAAK entity codes (first 3 letters of name, uppercase)
277
+ entity_codes = {}
278
+ for p in people:
279
+ name = p["name"]
280
+ code = name[:3].upper()
281
+ # Handle collisions
282
+ while code in entity_codes.values():
283
+ code = name[:4].upper()
284
+ entity_codes[name] = code
285
+
286
+ # AAAK entity registry
287
+ registry_lines = [
288
+ "# AAAK Entity Registry",
289
+ "# Auto-generated by mempalace init. Update as needed.",
290
+ "",
291
+ "## People",
292
+ ]
293
+ for p in people:
294
+ name = p["name"]
295
+ code = entity_codes[name]
296
+ rel = p.get("relationship", "")
297
+ registry_lines.append(f" {code}={name} ({rel})" if rel else f" {code}={name}")
298
+
299
+ if projects:
300
+ registry_lines.extend(["", "## Projects"])
301
+ for proj in projects:
302
+ code = proj[:4].upper()
303
+ registry_lines.append(f" {code}={proj}")
304
+
305
+ registry_lines.extend(
306
+ [
307
+ "",
308
+ "## AAAK Quick Reference",
309
+ " Symbols: ♡=love ★=importance ⚠=warning →=relationship |=separator",
310
+ " Structure: KEY:value | GROUP(details) | entity.attribute",
311
+ " Read naturally — expand codes, treat *markers* as emotional context.",
312
+ ]
313
+ )
314
+
315
+ (mempalace_dir / "aaak_entities.md").write_text("\n".join(registry_lines))
316
+
317
+ # Critical facts bootstrap (pre-palace — before any mining)
318
+ facts_lines = [
319
+ "# Critical Facts (bootstrap — will be enriched after mining)",
320
+ "",
321
+ ]
322
+
323
+ personal_people = [p for p in people if p.get("context") == "personal"]
324
+ work_people = [p for p in people if p.get("context") == "work"]
325
+
326
+ if personal_people:
327
+ facts_lines.append("## People (personal)")
328
+ for p in personal_people:
329
+ code = entity_codes[p["name"]]
330
+ rel = p.get("relationship", "")
331
+ facts_lines.append(
332
+ f"- **{p['name']}** ({code}) — {rel}" if rel else f"- **{p['name']}** ({code})"
333
+ )
334
+ facts_lines.append("")
335
+
336
+ if work_people:
337
+ facts_lines.append("## People (work)")
338
+ for p in work_people:
339
+ code = entity_codes[p["name"]]
340
+ rel = p.get("relationship", "")
341
+ facts_lines.append(
342
+ f"- **{p['name']}** ({code}) — {rel}" if rel else f"- **{p['name']}** ({code})"
343
+ )
344
+ facts_lines.append("")
345
+
346
+ if projects:
347
+ facts_lines.append("## Projects")
348
+ for proj in projects:
349
+ facts_lines.append(f"- **{proj}**")
350
+ facts_lines.append("")
351
+
352
+ facts_lines.extend(
353
+ [
354
+ "## Palace",
355
+ f"Wings: {', '.join(wings)}",
356
+ f"Mode: {mode}",
357
+ "",
358
+ "*This file will be enriched by palace_facts.py after mining.*",
359
+ ]
360
+ )
361
+
362
+ (mempalace_dir / "critical_facts.md").write_text("\n".join(facts_lines))
363
+
364
+
365
+ def run_onboarding(
366
+ directory: str = ".",
367
+ config_dir: Path = None,
368
+ auto_detect: bool = True,
369
+ ) -> EntityRegistry:
370
+ """
371
+ Run the full onboarding flow.
372
+ Returns the seeded EntityRegistry.
373
+ """
374
+ # Step 1: Mode
375
+ mode = _ask_mode()
376
+
377
+ # Step 2: People
378
+ people, aliases = _ask_people(mode)
379
+
380
+ # Step 3: Projects
381
+ projects = _ask_projects(mode)
382
+
383
+ # Step 4: Wings (stored in config, not registry — just show user)
384
+ wings = _ask_wings(mode)
385
+
386
+ # Step 5: Auto-detect additional people from files
387
+ if auto_detect and _yn("\nScan your files for additional names we might have missed?"):
388
+ directory = _ask("Directory to scan", default=directory)
389
+ detected = _auto_detect(directory, people)
390
+ if detected:
391
+ _hr()
392
+ print(f"\n Found {len(detected)} additional name candidates:\n")
393
+ for e in detected:
394
+ print(
395
+ f" {e['name']:20} confidence={e['confidence']:.0%} "
396
+ f"({', '.join(e['signals'][:1])})"
397
+ )
398
+ print()
399
+ if _yn(" Add any of these to your registry?"):
400
+ for e in detected:
401
+ ans = input(f" {e['name']} — (p)erson, (s)kip? ").strip().lower()
402
+ if ans == "p":
403
+ rel = input(f" Relationship/role for {e['name']}? ").strip()
404
+ ctx = (
405
+ "personal"
406
+ if mode == "personal"
407
+ else (
408
+ "work"
409
+ if mode == "work"
410
+ else input(" Context — (p)ersonal or (w)ork? ")
411
+ .strip()
412
+ .lower()
413
+ .replace("w", "work")
414
+ .replace("p", "personal")
415
+ )
416
+ )
417
+ people.append({"name": e["name"], "relationship": rel, "context": ctx})
418
+
419
+ # Step 6: Warn about ambiguous names
420
+ ambiguous = _warn_ambiguous(people)
421
+ if ambiguous:
422
+ _hr()
423
+ print(f"""
424
+ Heads up — these names are also common English words:
425
+ {", ".join(ambiguous)}
426
+
427
+ MemPalace will check the context before treating them as person names.
428
+ For example: "I picked up Riley" → person.
429
+ "Have you ever tried" → adverb.
430
+ """)
431
+
432
+ # Build and save registry
433
+ registry = EntityRegistry.load(config_dir)
434
+ registry.seed(mode=mode, people=people, projects=projects, aliases=aliases)
435
+
436
+ # Generate AAAK entity registry + critical facts bootstrap
437
+ _generate_aaak_bootstrap(people, projects, wings, mode, config_dir)
438
+
439
+ # Summary
440
+ _header("Setup Complete")
441
+ print()
442
+ print(f" {registry.summary()}")
443
+ print(f"\n Wings: {', '.join(wings)}")
444
+ print(f"\n Registry saved to: {registry._path}")
445
+ print("\n AAAK entity registry: ~/.mempalace/aaak_entities.md")
446
+ print(" Critical facts bootstrap: ~/.mempalace/critical_facts.md")
447
+ print("\n Your AI will know your world from the first session.")
448
+ print()
449
+
450
+ return registry
451
+
452
+
453
+ # ─────────────────────────────────────────────────────────────────────────────
454
+ # Quick setup (non-interactive, for testing)
455
+ # ─────────────────────────────────────────────────────────────────────────────
456
+
457
+
458
+ def quick_setup(
459
+ mode: str,
460
+ people: list,
461
+ projects: list = None,
462
+ aliases: dict = None,
463
+ config_dir: Path = None,
464
+ ) -> EntityRegistry:
465
+ """
466
+ Programmatic setup without interactive prompts.
467
+ Used in tests and benchmark scripts.
468
+
469
+ people: list of dicts {"name": str, "relationship": str, "context": str}
470
+ """
471
+ registry = EntityRegistry.load(config_dir)
472
+ registry.seed(
473
+ mode=mode,
474
+ people=people,
475
+ projects=projects or [],
476
+ aliases=aliases or {},
477
+ )
478
+ return registry
479
+
480
+
481
+ # ─────────────────────────────────────────────────────────────────────────────
482
+ # CLI
483
+ # ─────────────────────────────────────────────────────────────────────────────
484
+
485
+ if __name__ == "__main__":
486
+ import sys
487
+
488
+ directory = sys.argv[1] if len(sys.argv) > 1 else "."
489
+ run_onboarding(directory=directory)