diff --git a/agent/memory.py b/agent/memory.py index deb0c68e..56ada939 100644 --- a/agent/memory.py +++ b/agent/memory.py @@ -285,6 +285,49 @@ class AgentMemory: logger.warning(f"Failed to store memory: {e}") return None + def remember_alexander_request_response( + self, + *, + request_text: str, + response_text: str, + requester: str = "Alexander Whitestone", + source: str = "", + metadata: Optional[dict] = None, + ) -> Optional[str]: + """Store an Alexander request + wizard response artifact in the sovereign room.""" + if not self._check_available(): + logger.warning("Cannot store Alexander artifact — MemPalace unavailable") + return None + + try: + from nexus.mempalace.searcher import add_memory + from nexus.mempalace.conversation_artifacts import build_request_response_artifact + + artifact = build_request_response_artifact( + requester=requester, + responder=self.agent_name, + request_text=request_text, + response_text=response_text, + source=source, + ) + extra_metadata = dict(artifact.metadata) + if metadata: + extra_metadata.update(metadata) + + doc_id = add_memory( + text=artifact.text, + room=artifact.room, + wing=self.wing, + palace_path=self.palace_path, + source_file=source, + extra_metadata=extra_metadata, + ) + logger.debug("Stored Alexander request/response artifact in sovereign room") + return doc_id + except Exception as e: + logger.warning(f"Failed to store Alexander artifact: {e}") + return None + def write_diary( self, summary: Optional[str] = None, diff --git a/mempalace/rooms.yaml b/mempalace/rooms.yaml index d98c5b14..23bc1abd 100644 --- a/mempalace/rooms.yaml +++ b/mempalace/rooms.yaml @@ -62,6 +62,15 @@ core_rooms: - proof-of-concept code snippets - benchmark data + - key: sovereign + label: Sovereign + purpose: Artifacts of Alexander Whitestone's requests, directives, and wizard responses + examples: + - dated request/response artifacts + - conversation summaries with speaker tags + - directive ledgers + - response follow-through notes + optional_rooms: - key: evennia label: Evennia @@ -98,15 +107,6 @@ optional_rooms: purpose: Catch-all for artefacts not yet assigned to a named room wizards: ["*"] - - key: sovereign - label: Sovereign - purpose: Artifacts of Alexander Whitestone's requests, directives, and conversation history - wizards: ["*"] - conventions: - naming: "YYYY-MM-DD_HHMMSS_.md" - index: "INDEX.md" - description: "Each artifact is a dated record of a request from Alexander and the wizard's response. The running INDEX.md provides a chronological catalog." - # Tunnel routing table # Defines which room pairs are connected across wizard wings. # A tunnel lets `recall --fleet` search both wings at once. diff --git a/nexus/mempalace/__init__.py b/nexus/mempalace/__init__.py index bcb9a5d5..e74ebf04 100644 --- a/nexus/mempalace/__init__.py +++ b/nexus/mempalace/__init__.py @@ -13,6 +13,12 @@ from __future__ import annotations from nexus.mempalace.config import MEMPALACE_PATH, FLEET_WING from nexus.mempalace.searcher import search_memories, add_memory, MemPalaceResult +from nexus.mempalace.conversation_artifacts import ( + ConversationArtifact, + build_request_response_artifact, + extract_alexander_request_pairs, + normalize_speaker, +) __all__ = [ "MEMPALACE_PATH", @@ -20,4 +26,8 @@ __all__ = [ "search_memories", "add_memory", "MemPalaceResult", + "ConversationArtifact", + "build_request_response_artifact", + "extract_alexander_request_pairs", + "normalize_speaker", ] diff --git a/nexus/mempalace/config.py b/nexus/mempalace/config.py index f5155377..3500340e 100644 --- a/nexus/mempalace/config.py +++ b/nexus/mempalace/config.py @@ -40,6 +40,7 @@ CORE_ROOMS: list[str] = [ "nexus", # reports, docs, KT "issues", # tickets, backlog "experiments", # prototypes, spikes + "sovereign", # Alexander request/response artifacts ] # ── ChromaDB collection name ────────────────────────────────────────────────── diff --git a/nexus/mempalace/conversation_artifacts.py b/nexus/mempalace/conversation_artifacts.py new file mode 100644 index 00000000..b35820bd --- /dev/null +++ b/nexus/mempalace/conversation_artifacts.py @@ -0,0 +1,122 @@ +"""Helpers for preserving Alexander request/response artifacts in MemPalace. + +This module provides a small, typed bridge between raw conversation turns and +MemPalace drawers stored in the shared `sovereign` room. The goal is not to +solve all future speaker-tagging needs at once; it gives the Nexus one +canonical artifact shape that other miners and bridges can reuse. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Iterable + +_ALEXANDER_ALIASES = { + "alexander", + "alexander whitestone", + "rockachopa", + "triptimmy", +} + + +@dataclass(frozen=True) +class ConversationArtifact: + requester: str + responder: str + request_text: str + response_text: str + room: str = "sovereign" + timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")) + metadata: dict = field(default_factory=dict) + + @property + def text(self) -> str: + return ( + f"# Conversation Artifact\n\n" + f"## Alexander Request\n{self.request_text.strip()}\n\n" + f"## Wizard Response\n{self.response_text.strip()}\n" + ) + + +def normalize_speaker(name: str | None) -> str: + cleaned = " ".join((name or "").strip().lower().split()) + if cleaned in _ALEXANDER_ALIASES: + return "alexander" + return cleaned.replace(" ", "_") or "unknown" + + +def build_request_response_artifact( + *, + requester: str, + responder: str, + request_text: str, + response_text: str, + source: str = "", + timestamp: str | None = None, + request_timestamp: str | None = None, + response_timestamp: str | None = None, +) -> ConversationArtifact: + requester_slug = normalize_speaker(requester) + responder_slug = normalize_speaker(responder) + ts = timestamp or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + metadata = { + "artifact_type": "alexander_request_response", + "requester": requester_slug, + "responder": responder_slug, + "speaker_tags": [f"speaker:{requester_slug}", f"speaker:{responder_slug}"], + "source": source, + "timestamp": ts, + } + if request_timestamp: + metadata["request_timestamp"] = request_timestamp + if response_timestamp: + metadata["response_timestamp"] = response_timestamp + return ConversationArtifact( + requester=requester_slug, + responder=responder_slug, + request_text=request_text, + response_text=response_text, + timestamp=ts, + metadata=metadata, + ) + + +def extract_alexander_request_pairs( + turns: Iterable[dict], + *, + responder: str, + source: str = "", +) -> list[ConversationArtifact]: + responder_slug = normalize_speaker(responder) + pending_request: dict | None = None + artifacts: list[ConversationArtifact] = [] + + for turn in turns: + speaker = normalize_speaker( + turn.get("speaker") or turn.get("username") or turn.get("author") or turn.get("name") + ) + text = (turn.get("text") or turn.get("content") or "").strip() + if not text: + continue + + if speaker == "alexander": + pending_request = turn + continue + + if speaker == responder_slug and pending_request is not None: + artifacts.append( + build_request_response_artifact( + requester="alexander", + responder=responder_slug, + request_text=(pending_request.get("text") or pending_request.get("content") or "").strip(), + response_text=text, + source=source, + request_timestamp=pending_request.get("timestamp"), + response_timestamp=turn.get("timestamp"), + timestamp=turn.get("timestamp") or pending_request.get("timestamp"), + ) + ) + pending_request = None + + return artifacts diff --git a/tests/test_conversation_artifacts.py b/tests/test_conversation_artifacts.py index dec55bb5..63ddadb0 100644 --- a/tests/test_conversation_artifacts.py +++ b/tests/test_conversation_artifacts.py @@ -1,3 +1,7 @@ +from pathlib import Path + +import yaml + from nexus.mempalace.config import CORE_ROOMS from nexus.mempalace.conversation_artifacts import ( ConversationArtifact, @@ -9,6 +13,8 @@ from nexus.mempalace.conversation_artifacts import ( def test_sovereign_room_is_core_room() -> None: assert "sovereign" in CORE_ROOMS + rooms_yaml = yaml.safe_load(Path("mempalace/rooms.yaml").read_text()) + assert any(room["key"] == "sovereign" for room in rooms_yaml["core_rooms"]) def test_normalize_speaker_maps_alexander_variants() -> None: