feat(bezalel): MemPalace ecosystem — validation, audit, sync, auto-revert, Evennia integration

This commit is contained in:
Bezalel
2026-04-07 14:47:04 +00:00
committed by Bezalel
parent 34ec13bc29
commit a0ee7858ff
16 changed files with 1438 additions and 0 deletions

View File

@@ -0,0 +1,246 @@
"""
Palace commands — bridge Evennia to the local MemPalace memory system.
"""
import json
import subprocess
from evennia.commands.command import Command
from evennia import create_object, search_object
PALACE_SCRIPT = "/root/wizards/bezalel/evennia/palace_search.py"
def _search_mempalace(query, wing=None, room=None, n=5, fleet=False):
"""Call the helper script and return parsed results."""
cmd = ["/root/wizards/bezalel/hermes/venv/bin/python", PALACE_SCRIPT, query]
cmd.append(wing or "none")
cmd.append(room or "none")
cmd.append(str(n))
if fleet:
cmd.append("--fleet")
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
data = json.loads(result.stdout)
return data.get("results", [])
except Exception:
return []
def _get_wing(caller):
"""Return the caller's wing, defaulting to their key or 'general'."""
return caller.db.wing if caller.attributes.has("wing") else (caller.key.lower() if caller.key else "general")
class CmdPalaceSearch(Command):
"""
Search your memory palace.
Usage:
palace/search <query>
palace/search <query> [--room <room>]
palace/recall <topic>
palace/file <name> = <content>
palace/status
"""
key = "palace"
aliases = ["pal"]
locks = "cmd:all()"
help_category = "Mind Palace"
def func(self):
if not self.args.strip():
self.caller.msg("Usage: palace/search <query> | palace/recall <topic> | palace/file <name> = <content> | palace/status")
return
parts = self.args.strip().split(" ", 1)
subcmd = parts[0].lower()
rest = parts[1] if len(parts) > 1 else ""
if subcmd == "search":
self._do_search(rest)
elif subcmd == "recall":
self._do_recall(rest)
elif subcmd == "file":
self._do_file(rest)
elif subcmd == "status":
self._do_status()
else:
self._do_search(self.args.strip())
def _do_search(self, query):
if not query:
self.caller.msg("Search for what?")
return
self.caller.msg(f"Searching the palace for: |c{query}|n...")
wing = _get_wing(self.caller)
results = _search_mempalace(query, wing=wing)
if not results:
self.caller.msg("The palace is silent on that matter.")
return
lines = []
for i, r in enumerate(results[:5], 1):
room = r.get("room", "unknown")
source = r.get("source", "unknown")
content = r.get("content", "")[:400]
lines.append(f"\n|g[{i}]|n |c{room}|n — |x{source}|n")
lines.append(f"{content}\n")
self.caller.msg("\n".join(lines))
def _do_recall(self, topic):
if not topic:
self.caller.msg("Recall what topic?")
return
results = _search_mempalace(topic, wing=_get_wing(self.caller), n=1)
if not results:
self.caller.msg("Nothing to recall.")
return
r = results[0]
content = r.get("content", "")
source = r.get("source", "unknown")
from typeclasses.memory_object import MemoryObject
obj = create_object(
MemoryObject,
key=f"memory:{topic}",
location=self.caller.location,
)
obj.db.memory_content = content
obj.db.source_file = source
obj.db.room_name = r.get("room", "general")
self.caller.location.msg_contents(
f"$You() conjure() a memory shard from the palace: |m{obj.key}|n.",
from_obj=self.caller,
)
def _do_file(self, rest):
if "=" not in rest:
self.caller.msg("Usage: palace/file <name> = <content>")
return
name, content = rest.split("=", 1)
name = name.strip()
content = content.strip()
if not name or not content:
self.caller.msg("Both name and content are required.")
return
from typeclasses.memory_object import MemoryObject
obj = create_object(
MemoryObject,
key=f"memory:{name}",
location=self.caller.location,
)
obj.db.memory_content = content
obj.db.source_file = f"filed by {self.caller.key}"
obj.db.room_name = self.caller.location.key if self.caller.location else "general"
self.caller.location.msg_contents(
f"$You() file() a new memory in the palace: |m{obj.key}|n.",
from_obj=self.caller,
)
def _do_status(self):
cmd = [
"/root/wizards/bezalel/hermes/venv/bin/mempalace",
"--palace", "/root/wizards/bezalel/.mempalace/palace",
"status"
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
self.caller.msg(result.stdout or result.stderr)
except Exception as e:
self.caller.msg(f"Could not reach the palace: {e}")
class CmdRecall(Command):
"""
Recall a memory from the palace.
Usage:
recall <query>
recall <query> --fleet
recall <query> --room <room>
"""
key = "recall"
aliases = ["remember", "mem"]
locks = "cmd:all()"
help_category = "Mind Palace"
def func(self):
if not self.args.strip():
self.caller.msg("Recall what? Usage: recall <query> [--fleet] [--room <room>]")
return
args = self.args.strip()
fleet = "--fleet" in args
room = None
if "--room" in args:
parts = args.split("--room")
args = parts[0].strip()
room = parts[1].strip().split()[0] if len(parts) > 1 else None
if "--fleet" in args:
args = args.replace("--fleet", "").strip()
self.caller.msg(f"Recalling from the {'fleet' if fleet else 'personal'} palace: |c{args}|n...")
wing = None if fleet else _get_wing(self.caller)
results = _search_mempalace(args, wing=wing, room=room, n=5, fleet=fleet)
if not results:
self.caller.msg("The palace is silent on that matter.")
return
lines = []
for i, r in enumerate(results[:5], 1):
room_name = r.get("room", "unknown")
source = r.get("source", "unknown")
content = r.get("content", "")[:400]
wing_label = r.get("wing", "unknown")
wing_tag = f" |y[{wing_label}]|n" if fleet else ""
lines.append(f"\n|g[{i}]|n |c{room_name}|n{wing_tag} — |x{source}|n")
lines.append(f"{content}\n")
self.caller.msg("\n".join(lines))
class CmdEnterRoom(Command):
"""
Enter a room in the mind palace by topic.
Usage:
enter room <topic>
"""
key = "enter room"
aliases = ["enter palace", "go room"]
locks = "cmd:all()"
help_category = "Mind Palace"
def func(self):
if not self.args.strip():
self.caller.msg("Enter which room? Usage: enter room <topic>")
return
topic = self.args.strip().lower().replace(" ", "-")
wing = _get_wing(self.caller)
room_key = f"palace:{wing}:{topic}"
# Search for existing room
rooms = search_object(room_key, typeclass="typeclasses.palace_room.PalaceRoom")
if rooms:
room = rooms[0]
else:
# Create the room dynamically
from typeclasses.palace_room import PalaceRoom
room = create_object(
PalaceRoom,
key=room_key,
)
room.db.memory_topic = topic
room.db.wing = wing
room.update_description()
self.caller.move_to(room, move_type="teleport")
self.caller.msg(f"You step into the |c{topic}|n room of your mind palace.")

View File

@@ -0,0 +1,166 @@
"""
Live memory commands — write new memories into the palace from Evennia.
"""
import json
import subprocess
from evennia.commands.command import Command
from evennia import create_object
PALACE_SCRIPT = "/root/wizards/bezalel/evennia/palace_search.py"
PALACE_PATH = "/root/wizards/bezalel/.mempalace/palace"
ADDER_SCRIPT = "/root/wizards/bezalel/evennia/palace_add.py"
def _add_drawer(content, wing, room, source):
"""Add a verbatim drawer to the palace via the helper script."""
cmd = [
"/root/wizards/bezalel/hermes/venv/bin/python",
ADDER_SCRIPT,
content,
wing,
room,
source,
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
return result.returncode == 0 and "OK" in result.stdout
except Exception:
return False
class CmdRecord(Command):
"""
Record a decision into the palace hall_facts.
Usage:
record <text>
record We decided to use PostgreSQL over MySQL.
"""
key = "record"
aliases = ["decide"]
locks = "cmd:all()"
help_category = "Mind Palace"
def func(self):
if not self.args.strip():
self.caller.msg("Record what decision? Usage: record <text>")
return
wing = self.caller.db.wing if self.caller.attributes.has("wing") else (self.caller.key.lower() if self.caller.key else "general")
text = self.args.strip()
full_text = f"DECISION ({wing}): {text}\nRecorded by {self.caller.key} via Evennia."
ok = _add_drawer(full_text, wing, "general", f"evennia:{self.caller.key}")
if ok:
self.caller.location.msg_contents(
f"$You() record() a decision in the palace archives.",
from_obj=self.caller,
)
else:
self.caller.msg("The palace scribes could not write that down.")
class CmdNote(Command):
"""
Note a breakthrough into the palace hall_discoveries.
Usage:
note <text>
note The GraphQL schema can be auto-generated from our typeclasses.
"""
key = "note"
aliases = ["jot"]
locks = "cmd:all()"
help_category = "Mind Palace"
def func(self):
if not self.args.strip():
self.caller.msg("Note what? Usage: note <text>")
return
wing = self.caller.db.wing if self.caller.attributes.has("wing") else (self.caller.key.lower() if self.caller.key else "general")
text = self.args.strip()
full_text = f"BREAKTHROUGH ({wing}): {text}\nNoted by {self.caller.key} via Evennia."
ok = _add_drawer(full_text, wing, "general", f"evennia:{self.caller.key}")
if ok:
self.caller.location.msg_contents(
f"$You() inscribe() a breakthrough into the palace scrolls.",
from_obj=self.caller,
)
else:
self.caller.msg("The palace scribes could not write that down.")
class CmdEvent(Command):
"""
Log an event into the palace hall_events.
Usage:
event <text>
event Gitea runner came back online after being offline for 6 hours.
"""
key = "event"
aliases = ["log"]
locks = "cmd:all()"
help_category = "Mind Palace"
def func(self):
if not self.args.strip():
self.caller.msg("Log what event? Usage: event <text>")
return
wing = self.caller.db.wing if self.caller.attributes.has("wing") else (self.caller.key.lower() if self.caller.key else "general")
text = self.args.strip()
full_text = f"EVENT ({wing}): {text}\nLogged by {self.caller.key} via Evennia."
ok = _add_drawer(full_text, wing, "general", f"evennia:{self.caller.key}")
if ok:
self.caller.location.msg_contents(
f"$You() chronicle() an event in the palace records.",
from_obj=self.caller,
)
else:
self.caller.msg("The palace scribes could not write that down.")
class CmdPalaceWrite(Command):
"""
Directly write a memory into a specific palace room.
Usage:
palace/write <room> = <text>
"""
key = "palace/write"
locks = "cmd:all()"
help_category = "Mind Palace"
def func(self):
if "=" not in self.args:
self.caller.msg("Usage: palace/write <room> = <text>")
return
room, text = self.args.split("=", 1)
room = room.strip()
text = text.strip()
if not room or not text:
self.caller.msg("Both room and text are required.")
return
wing = self.caller.db.wing if self.caller.attributes.has("wing") else (self.caller.key.lower() if self.caller.key else "general")
full_text = f"MEMORY ({wing}/{room}): {text}\nWritten by {self.caller.key} via Evennia."
ok = _add_drawer(full_text, wing, room, f"evennia:{self.caller.key}")
if ok:
self.caller.location.msg_contents(
f"$You() etch() a memory into the |c{room}|n room of the palace.",
from_obj=self.caller,
)
else:
self.caller.msg("The palace scribes could not write that down.")

View File

@@ -0,0 +1,105 @@
"""
Steward commands — ask a palace steward about memories.
"""
from evennia.commands.command import Command
from evennia import search_object
class CmdAskSteward(Command):
"""
Ask a steward NPC about a topic from the palace memory.
Usage:
ask <steward> about <topic>
ask <steward> about <topic> --fleet
Example:
ask bezalel-steward about nightly watch
ask bezalel-steward about runner outage --fleet
"""
key = "ask"
aliases = ["question"]
locks = "cmd:all()"
help_category = "Mind Palace"
def parse(self):
"""Parse 'ask <target> about <topic>' syntax."""
raw = self.args.strip()
fleet = "--fleet" in raw
if fleet:
raw = raw.replace("--fleet", "").strip()
if " about " in raw.lower():
parts = raw.split(" about ", 1)
self.target_name = parts[0].strip()
self.topic = parts[1].strip()
else:
self.target_name = ""
self.topic = raw
self.fleet = fleet
def func(self):
if not self.args.strip():
self.caller.msg("Usage: ask <steward> about <topic> [--fleet]")
return
self.parse()
if not self.target_name:
self.caller.msg("Ask whom? Usage: ask <steward> about <topic>")
return
# Find steward NPC in current room
stewards = [
obj for obj in self.caller.location.contents
if hasattr(obj, "respond_to_question")
and self.target_name.lower() in obj.key.lower()
]
if not stewards:
self.caller.msg(f"There is no steward here matching '{self.target_name}'.")
return
steward = stewards[0]
self.caller.msg(f"You ask |c{steward.key}|n about '{self.topic}'...")
steward.respond_to_question(self.topic, self.caller, fleet=self.fleet)
class CmdSummonSteward(Command):
"""
Summon your wing's steward NPC to your current location.
Usage:
summon steward
"""
key = "summon steward"
locks = "cmd:all()"
help_category = "Mind Palace"
def func(self):
wing = self.caller.db.wing if self.caller.attributes.has("wing") else (self.caller.key.lower() if self.caller.key else "general")
steward_key = f"{wing}-steward"
# Search for existing steward
from typeclasses.steward_npc import StewardNPC
stewards = search_object(steward_key, typeclass="typeclasses.steward_npc.StewardNPC")
if stewards:
steward = stewards[0]
steward.move_to(self.caller.location, move_type="teleport")
self.caller.location.msg_contents(
f"A shimmer of light coalesces into |c{steward.key}|n.",
from_obj=self.caller,
)
else:
steward = StewardNPC.create(steward_key)[0]
steward.db.wing = wing
steward.db.steward_name = self.caller.key
steward.move_to(self.caller.location, move_type="teleport")
self.caller.location.msg_contents(
f"You call forth |c{steward.key}|n from the palace archives.",
from_obj=self.caller,
)

View File

@@ -0,0 +1,83 @@
"""
Hall of Wings — Builds the central MemPalace zone in Evennia.
Usage (from Evennia shell or script):
from world.hall_of_wings import build_hall_of_wings
build_hall_of_wings()
"""
from evennia import create_object
from typeclasses.palace_room import PalaceRoom
from typeclasses.steward_npc import StewardNPC
from typeclasses.rooms import Room
from typeclasses.exits import Exit
HALL_KEY = "hall_of_wings"
HALL_NAME = "Hall of Wings"
DEFAULT_WINGS = [
"bezalel",
"timmy",
"allegro",
"ezra",
]
def build_hall_of_wings():
"""Create or update the central Hall of Wings and attach steward chambers."""
# Find or create the hall
from evennia import search_object
halls = search_object(HALL_KEY, typeclass="typeclasses.rooms.Room")
if halls:
hall = halls[0]
else:
hall = create_object(Room, key=HALL_KEY)
hall.db.desc = (
"|cThe Hall of Wings|n\n"
"A vast circular chamber of pale stone and shifting starlight.\n"
"Arched doorways line the perimeter, each leading to a steward's chamber.\n"
"Here, the memories of the fleet converge.\n\n"
"Use |wsummon steward|n to call your wing's steward, or\n"
"|wask <steward> about <topic>|n to query the palace archives."
)
for wing in DEFAULT_WINGS:
chamber_key = f"chamber:{wing}"
chambers = search_object(chamber_key, typeclass="typeclasses.palace_room.PalaceRoom")
if chambers:
chamber = chambers[0]
else:
chamber = create_object(PalaceRoom, key=chamber_key)
chamber.db.memory_topic = wing
chamber.db.wing = wing
chamber.db.desc = (
f"|cThe Chamber of {wing.title()}|n\n"
f"This room holds the accumulated memories of the {wing} wing.\n"
f"A steward stands ready to answer questions."
)
chamber.update_description()
# Link hall <-> chamber with exits
exit_name = f"{wing}-chamber"
existing_exits = [ex for ex in hall.exits if ex.key == exit_name]
if not existing_exits:
create_object(Exit, key=exit_name, location=hall, destination=chamber)
return_exits = [ex for ex in chamber.exits if ex.key == "hall"]
if not return_exits:
create_object(Exit, key="hall", location=chamber, destination=hall)
# Place or summon steward
steward_key = f"{wing}-steward"
stewards = search_object(steward_key, typeclass="typeclasses.steward_npc.StewardNPC")
if stewards:
steward = stewards[0]
if steward.location != chamber:
steward.move_to(chamber, move_type="teleport")
else:
steward = create_object(StewardNPC, key=steward_key)
steward.db.wing = wing
steward.db.steward_name = wing.title()
steward.move_to(chamber, move_type="teleport")
return hall

View File

@@ -0,0 +1,87 @@
"""
PalaceRoom
A Room that represents a topic in the memory palace.
Memory objects spawned here embody concepts retrieved from mempalace.
Its description auto-populates from a palace search on the memory topic.
"""
import json
import subprocess
from evennia.objects.objects import DefaultRoom
from .objects import ObjectParent
PALACE_SCRIPT = "/root/wizards/bezalel/evennia/palace_search.py"
class PalaceRoom(ObjectParent, DefaultRoom):
"""
A room in the mind palace. Its db.memory_topic describes what
kind of memories are stored here. The description is populated
from a live MemPalace search.
"""
def at_object_creation(self):
super().at_object_creation()
self.db.memory_topic = ""
self.db.wing = "bezalel"
self.db.desc = (
f"This is the |c{self.key}|n room of your mind palace.\n"
"Memories and concepts drift here like motes of light.\n"
"Use |wpalace/search <query>|n or |wrecall <topic>|n to summon memories."
)
def _search_palace(self, query, wing=None, room=None, n=3):
"""Call the helper script and return parsed results."""
cmd = ["/root/wizards/bezalel/hermes/venv/bin/python", PALACE_SCRIPT, query]
cmd.append(wing or "none")
cmd.append(room or "none")
cmd.append(str(n))
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
data = json.loads(result.stdout)
return data.get("results", [])
except Exception:
return []
def update_description(self):
"""Refresh the room description from a palace search on its topic."""
topic = self.db.memory_topic or self.key.split(":")[-1] if ":" in self.key else self.key
wing = self.db.wing or "bezalel"
results = self._search_palace(topic, wing=wing, n=3)
header = (
f"=|c {topic.upper()} |n="
)
desc_lines = [
header,
f"You stand in the |c{topic}|n room of the |y{wing}|n wing.",
"Memories drift here like motes of light.",
"",
]
if results:
desc_lines.append("|gNearby memories:|n")
for i, r in enumerate(results, 1):
content = r.get("content", "")[:200]
source = r.get("source", "unknown")
room_name = r.get("room", "unknown")
desc_lines.append(f" |m[{i}]|n |c{room_name}|n — {content}... |x({source})|n")
else:
desc_lines.append("|xThe palace is quiet here. No memories resonate with this topic yet.|n")
desc_lines.append("")
desc_lines.append("Use |wrecall <query>|n to search deeper, or |wpalace/search <query>|n.")
self.db.desc = "\n".join(desc_lines)
def at_object_receive(self, moved_obj, source_location, **kwargs):
"""Refresh description when someone enters."""
if moved_obj.has_account:
self.update_description()
super().at_object_receive(moved_obj, source_location, **kwargs)
def return_appearance(self, looker):
text = super().return_appearance(looker)
if self.db.memory_topic:
text += f"\n|xTopic: {self.db.memory_topic}|n"
return text

View File

@@ -0,0 +1,70 @@
"""
StewardNPC
A palace steward NPC that answers questions by querying the local
or fleet MemPalace backend. One steward per wizard wing.
"""
import json
import subprocess
from evennia.objects.objects import DefaultCharacter
from typeclasses.objects import ObjectParent
PALACE_SCRIPT = "/root/wizards/bezalel/evennia/palace_search.py"
class StewardNPC(ObjectParent, DefaultCharacter):
"""
A steward of the mind palace. Ask it about memories,
decisions, or events from its wing.
"""
def at_object_creation(self):
super().at_object_creation()
self.db.wing = "bezalel"
self.db.steward_name = "Bezalel"
self.db.desc = (
f"|c{self.key}|n stands here quietly, eyes like polished steel, "
"waiting to recall anything from the palace archives."
)
self.locks.add("get:false();delete:perm(Admin)")
def _search_palace(self, query, fleet=False, n=3):
cmd = [
"/root/wizards/bezalel/hermes/venv/bin/python",
PALACE_SCRIPT,
query,
"none" if fleet else self.db.wing,
"none",
str(n),
]
if fleet:
cmd.append("--fleet")
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
data = json.loads(result.stdout)
return data.get("results", [])
except Exception:
return []
def _summarize_for_speech(self, results, query):
"""Convert search results into in-character dialogue."""
if not results:
return "I find no memory of that in the palace."
lines = [f"Regarding '{query}':"]
for r in results:
room = r.get("room", "unknown")
content = r.get("content", "")[:300]
source = r.get("source", "unknown")
lines.append(f" From the |c{room}|n room: {content}... |x[{source}]|n")
return "\n".join(lines)
def respond_to_question(self, question, asker, fleet=False):
results = self._search_palace(question, fleet=fleet, n=3)
speech = self._summarize_for_speech(results, question)
self.location.msg_contents(
f"|c{self.key}|n says to $you(asker): \"{speech}\"",
mapping={"asker": asker},
from_obj=self,
)