fleet-memory/memora/models.py
Nicolò Boschi 7d8d07d1aa memora
2025-11-03 20:10:11 +01:00

268 lines
10 KiB
Python

"""
SQLAlchemy models for the memory system.
"""
from datetime import datetime
from typing import Optional
from uuid import UUID as PyUUID, uuid4
from sqlalchemy import (
CheckConstraint,
Column,
Float,
ForeignKey,
ForeignKeyConstraint,
Index,
Integer,
PrimaryKeyConstraint,
Text,
func,
text as sql_text,
)
from sqlalchemy.dialects.postgresql import JSONB, TIMESTAMP, UUID
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from pgvector.sqlalchemy import Vector
class Base(AsyncAttrs, DeclarativeBase):
"""Base class for all models."""
pass
class Document(Base):
"""Source documents for memory units."""
__tablename__ = "documents"
id: Mapped[str] = mapped_column(Text, primary_key=True)
agent_id: Mapped[str] = mapped_column(Text, primary_key=True)
original_text: Mapped[Optional[str]] = mapped_column(Text)
content_hash: Mapped[Optional[str]] = mapped_column(Text)
doc_metadata: Mapped[dict] = mapped_column("metadata", JSONB, server_default=sql_text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)
# Relationships
memory_units = relationship("MemoryUnit", back_populates="document", cascade="all, delete-orphan")
__table_args__ = (
Index("idx_documents_agent_id", "agent_id"),
Index("idx_documents_content_hash", "content_hash"),
)
class MemoryUnit(Base):
"""Individual sentence-level memories."""
__tablename__ = "memory_units"
id: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, server_default=sql_text("uuid_generate_v4()")
)
agent_id: Mapped[str] = mapped_column(Text, nullable=False)
document_id: Mapped[Optional[str]] = mapped_column(Text)
text: Mapped[str] = mapped_column(Text, nullable=False)
embedding = mapped_column(Vector(384)) # pgvector type
context: Mapped[Optional[str]] = mapped_column(Text)
event_date: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
fact_type: Mapped[str] = mapped_column(Text, nullable=False, server_default="world")
confidence_score: Mapped[Optional[float]] = mapped_column(Float)
access_count: Mapped[int] = mapped_column(Integer, server_default="0")
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)
# Relationships
document = relationship("Document", back_populates="memory_units")
unit_entities = relationship("UnitEntity", back_populates="memory_unit", cascade="all, delete-orphan")
outgoing_links = relationship(
"MemoryLink",
foreign_keys="MemoryLink.from_unit_id",
back_populates="from_unit",
cascade="all, delete-orphan"
)
incoming_links = relationship(
"MemoryLink",
foreign_keys="MemoryLink.to_unit_id",
back_populates="to_unit",
cascade="all, delete-orphan"
)
__table_args__ = (
ForeignKeyConstraint(
["document_id", "agent_id"],
["documents.id", "documents.agent_id"],
name="memory_units_document_fkey",
ondelete="CASCADE",
),
CheckConstraint("fact_type IN ('world', 'agent', 'opinion')"),
CheckConstraint("confidence_score IS NULL OR (confidence_score >= 0.0 AND confidence_score <= 1.0)"),
CheckConstraint(
"(fact_type = 'opinion' AND confidence_score IS NOT NULL) OR "
"(fact_type != 'opinion' AND confidence_score IS NULL)",
name="confidence_score_fact_type_check"
),
Index("idx_memory_units_agent_id", "agent_id"),
Index("idx_memory_units_document_id", "document_id"),
Index("idx_memory_units_event_date", "event_date", postgresql_ops={"event_date": "DESC"}),
Index("idx_memory_units_agent_date", "agent_id", "event_date", postgresql_ops={"event_date": "DESC"}),
Index("idx_memory_units_access_count", "access_count", postgresql_ops={"access_count": "DESC"}),
Index("idx_memory_units_fact_type", "fact_type"),
Index("idx_memory_units_agent_fact_type", "agent_id", "fact_type"),
Index("idx_memory_units_agent_type_date", "agent_id", "fact_type", "event_date", postgresql_ops={"event_date": "DESC"}),
Index(
"idx_memory_units_opinion_confidence",
"agent_id",
"confidence_score",
postgresql_where=sql_text("fact_type = 'opinion'"),
postgresql_ops={"confidence_score": "DESC"}
),
Index(
"idx_memory_units_opinion_date",
"agent_id",
"event_date",
postgresql_where=sql_text("fact_type = 'opinion'"),
postgresql_ops={"event_date": "DESC"}
),
Index(
"idx_memory_units_embedding",
"embedding",
postgresql_using="hnsw",
postgresql_ops={"embedding": "vector_cosine_ops"}
),
)
class Entity(Base):
"""Resolved entities (people, organizations, locations, etc.)."""
__tablename__ = "entities"
id: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, server_default=sql_text("uuid_generate_v4()")
)
canonical_name: Mapped[str] = mapped_column(Text, nullable=False)
entity_type: Mapped[str] = mapped_column(Text, nullable=False)
agent_id: Mapped[str] = mapped_column(Text, nullable=False)
entity_metadata: Mapped[dict] = mapped_column("metadata", JSONB, server_default=sql_text("'{}'::jsonb"))
first_seen: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)
last_seen: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)
mention_count: Mapped[int] = mapped_column(Integer, server_default="1")
# Relationships
unit_entities = relationship("UnitEntity", back_populates="entity", cascade="all, delete-orphan")
memory_links = relationship("MemoryLink", back_populates="entity", cascade="all, delete-orphan")
cooccurrences_1 = relationship(
"EntityCooccurrence",
foreign_keys="EntityCooccurrence.entity_id_1",
back_populates="entity_1",
cascade="all, delete-orphan"
)
cooccurrences_2 = relationship(
"EntityCooccurrence",
foreign_keys="EntityCooccurrence.entity_id_2",
back_populates="entity_2",
cascade="all, delete-orphan"
)
__table_args__ = (
Index("idx_entities_agent_id", "agent_id"),
Index("idx_entities_canonical_name", "canonical_name"),
Index("idx_entities_type", "entity_type"),
Index("idx_entities_agent_name_type", "agent_id", "canonical_name", "entity_type"),
)
class UnitEntity(Base):
"""Association between memory units and entities."""
__tablename__ = "unit_entities"
unit_id: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("memory_units.id", ondelete="CASCADE"), primary_key=True
)
entity_id: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("entities.id", ondelete="CASCADE"), primary_key=True
)
# Relationships
memory_unit = relationship("MemoryUnit", back_populates="unit_entities")
entity = relationship("Entity", back_populates="unit_entities")
__table_args__ = (
Index("idx_unit_entities_unit", "unit_id"),
Index("idx_unit_entities_entity", "entity_id"),
)
class EntityCooccurrence(Base):
"""Materialized cache of entity co-occurrences."""
__tablename__ = "entity_cooccurrences"
entity_id_1: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("entities.id", ondelete="CASCADE"), primary_key=True
)
entity_id_2: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("entities.id", ondelete="CASCADE"), primary_key=True
)
cooccurrence_count: Mapped[int] = mapped_column(Integer, server_default="1")
last_cooccurred: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)
# Relationships
entity_1 = relationship("Entity", foreign_keys=[entity_id_1], back_populates="cooccurrences_1")
entity_2 = relationship("Entity", foreign_keys=[entity_id_2], back_populates="cooccurrences_2")
__table_args__ = (
CheckConstraint("entity_id_1 < entity_id_2", name="entity_cooccurrence_order_check"),
Index("idx_entity_cooccurrences_entity1", "entity_id_1"),
Index("idx_entity_cooccurrences_entity2", "entity_id_2"),
Index("idx_entity_cooccurrences_count", "cooccurrence_count", postgresql_ops={"cooccurrence_count": "DESC"}),
)
class MemoryLink(Base):
"""Links between memory units (temporal, semantic, entity)."""
__tablename__ = "memory_links"
from_unit_id: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("memory_units.id", ondelete="CASCADE"), primary_key=True
)
to_unit_id: Mapped[PyUUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("memory_units.id", ondelete="CASCADE"), primary_key=True
)
link_type: Mapped[str] = mapped_column(Text, primary_key=True)
entity_id: Mapped[Optional[PyUUID]] = mapped_column(
UUID(as_uuid=True), ForeignKey("entities.id", ondelete="CASCADE"), primary_key=True
)
weight: Mapped[float] = mapped_column(Float, nullable=False, server_default="1.0")
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)
# Relationships
from_unit = relationship("MemoryUnit", foreign_keys=[from_unit_id], back_populates="outgoing_links")
to_unit = relationship("MemoryUnit", foreign_keys=[to_unit_id], back_populates="incoming_links")
entity = relationship("Entity", back_populates="memory_links")
__table_args__ = (
Index("idx_memory_links_from", "from_unit_id"),
Index("idx_memory_links_to", "to_unit_id"),
Index("idx_memory_links_type", "link_type"),
Index("idx_memory_links_entity", "entity_id", postgresql_where=sql_text("entity_id IS NOT NULL")),
Index(
"idx_memory_links_from_weight",
"from_unit_id",
"weight",
postgresql_where=sql_text("weight >= 0.1"),
postgresql_ops={"weight": "DESC"}
),
)