metropolis-chess / app /models /memory.py
Forkei's picture
feat(block13): agent vs character match flow (commits 3–8)
b0d952a
from __future__ import annotations
import enum
import uuid
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import JSON, DateTime, Enum, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
if TYPE_CHECKING:
from app.models.character import Character
class MemoryScope(str, enum.Enum):
CHARACTER_LORE = "character_lore"
OPPONENT_SPECIFIC = "opponent_specific"
CROSS_PLAYER = "cross_player"
MATCH_RECAP = "match_recap"
class MemoryType(str, enum.Enum):
FORMATIVE = "formative"
RIVALRY = "rivalry"
TRAVEL = "travel"
TRIUMPH = "triumph"
DEFEAT = "defeat"
HABIT = "habit"
OPINION = "opinion"
OBSERVATION = "observation"
# Phase 4.3 — learned patterns the character has picked up over
# matches (e.g. trap patterns that burned them once). Fed into the
# Subconscious's normal retrieval flow.
LEARNING = "learning"
def _uuid() -> str:
return str(uuid.uuid4())
def _now() -> datetime:
return datetime.utcnow()
class Memory(Base):
__tablename__ = "memories"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
# character_id is NULL for agent-scoped memories (agent_id is set instead).
character_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("characters.id", ondelete="CASCADE"), nullable=True, index=True
)
# Block 13: agent-scoped memories have agent_id set, character_id=NULL.
agent_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("player_agents.id", ondelete="CASCADE"), nullable=True, index=True
)
# player_id / match_id are not FK'd to real tables yet (those arrive in Phase 2).
# Nullable strings keep the schema future-proof without requiring those tables now.
player_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
match_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
scope: Mapped[MemoryScope] = mapped_column(Enum(MemoryScope, name="memory_scope"), nullable=False)
type: Mapped[MemoryType] = mapped_column(Enum(MemoryType, name="memory_type"), nullable=False)
emotional_valence: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
triggers: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
narrative_text: Mapped[str] = mapped_column(Text, nullable=False)
relevance_tags: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=_now)
last_surfaced_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
surface_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# Phase 2b: semantic retrieval vector. NULL until the memory is embedded
# (either at creation via `embed_and_persist` or by the backfill script).
embedding: Mapped[list[float] | None] = mapped_column(JSON, nullable=True)
character: Mapped["Character"] = relationship(back_populates="memories")