Merge branch 'document-store' into document-entity-write

This commit is contained in:
phernandez
2024-12-21 18:50:06 -06:00
16 changed files with 1231 additions and 164 deletions
+13
View File
@@ -0,0 +1,13 @@
"""Models package for basic-memory."""
from basic_memory.models.base import Base
from basic_memory.models.documents import Document
from basic_memory.models.knowledge import Entity, Observation, Relation
__all__ = [
'Base',
'Document',
'Entity',
'Observation',
'Relation'
]
+9
View File
@@ -0,0 +1,9 @@
"""Base model class for SQLAlchemy models."""
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase
class Base(AsyncAttrs, DeclarativeBase):
"""Base class for all models"""
pass
+40
View File
@@ -0,0 +1,40 @@
"""Document model for tracking files in the knowledge base."""
from datetime import datetime
from typing import Optional, List
from sqlalchemy import String, DateTime, text, JSON
from sqlalchemy.orm import Mapped, mapped_column, relationship
from basic_memory.models.base import Base
class Document(Base):
"""
Tracks documents in the filesystem.
Documents are the source of truth for content, while this table
provides indexing and metadata storage. Like git, the filesystem
is the real source of truth.
"""
__tablename__ = "documents"
id: Mapped[int] = mapped_column(primary_key=True)
path: Mapped[str] = mapped_column(String, unique=True, nullable=False)
checksum: Mapped[str] = mapped_column(String, nullable=False)
doc_metadata: Mapped[Optional[dict]] = mapped_column(
JSON, nullable=True
) # renamed from metadata
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP"))
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=text("CURRENT_TIMESTAMP"), onupdate=text("CURRENT_TIMESTAMP")
)
# Relationships
entities: Mapped[List["Entity"]] = relationship( # pyright: ignore [reportUndefinedVariable] # noqa: F821
"Entity", back_populates="document", cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"Document(id={self.id}, path='{self.path}')"
+165
View File
@@ -0,0 +1,165 @@
"""Knowledge graph models for basic-memory."""
from datetime import datetime
from typing import List, Optional
from sqlalchemy import (
String, DateTime, ForeignKey, Text, Integer,
text, UniqueConstraint
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from basic_memory.models.base import Base
from basic_memory.utils import sanitize_name
class Entity(Base):
"""
Core entity in the knowledge graph.
Entities are the primary nodes in the knowledge graph. Each entity has:
- A unique identifier (text, based on type/name path)
- A name
- An entity type (e.g., "person", "organization", "event")
- A description (optional)
- A list of observations
"""
__tablename__ = "entity"
__table_args__ = (
UniqueConstraint("entity_type", "name", name="uix_entity_type_name"),
)
id: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String)
entity_type: Mapped[str] = mapped_column(String)
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime,
server_default=text("CURRENT_TIMESTAMP")
)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
server_default=text("CURRENT_TIMESTAMP"),
onupdate=text("CURRENT_TIMESTAMP")
)
# Link to source document
doc_id: Mapped[Optional[int]] = mapped_column(
Integer,
ForeignKey("documents.id", ondelete="SET NULL"),
nullable=True
)
# Relationships
document: Mapped["Document"] = relationship(
"Document",
back_populates="entities"
)
observations: Mapped[List["Observation"]] = relationship(
"Observation",
back_populates="entity",
cascade="all, delete-orphan"
)
outgoing_relations: Mapped[List["Relation"]] = relationship(
"Relation",
foreign_keys="[Relation.from_id]",
back_populates="from_entity",
cascade="all, delete-orphan"
)
incoming_relations: Mapped[List["Relation"]] = relationship(
"Relation",
foreign_keys="[Relation.to_id]",
back_populates="to_entity",
cascade="all, delete-orphan"
)
@property
def relations(self):
return self.outgoing_relations + self.incoming_relations
@classmethod
def generate_id(cls, entity_type: str, name: str) -> str:
"""Generate a filesystem path-based ID for this entity."""
# Use common normalization for filesystem safety
safe_name = sanitize_name(name)
return f"{entity_type}/{safe_name}"
def get_file_path(self) -> str:
"""Get the filesystem path for this entity."""
return f"{self.id}.md" # id is already in path format
def __repr__(self) -> str:
return f"Entity(id='{self.id}', name='{self.name}', type='{self.entity_type}')"
class Observation(Base):
"""
Observations are discrete pieces of information about an entity. They are:
- Stored as strings
- Attached to specific entities
- Can be added or removed independently
- Should be atomic (one fact per observation)
"""
__tablename__ = "observation"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
entity_id: Mapped[str] = mapped_column(
String,
ForeignKey("entity.id", ondelete="CASCADE"),
index=True
)
content: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(
DateTime,
server_default=text("CURRENT_TIMESTAMP")
)
context: Mapped[Optional[str]] = mapped_column(String, nullable=True)
# Relationships
entity: Mapped[Entity] = relationship("Entity", back_populates="observations")
def __repr__(self) -> str:
content = self.content[:50] + "..." if len(self.content) > 50 else self.content
return f"Observation(id={self.id}, entity='{self.entity_id}', content='{content}')"
class Relation(Base):
"""
Relations define directed connections between entities.
They are always stored in active voice and describe how entities
interact or relate to each other.
"""
__tablename__ = "relation"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
from_id: Mapped[str] = mapped_column(
String,
ForeignKey("entity.id", ondelete="CASCADE"),
index=True
)
to_id: Mapped[str] = mapped_column(
String,
ForeignKey("entity.id", ondelete="CASCADE"),
index=True
)
relation_type: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(
DateTime,
server_default=text("CURRENT_TIMESTAMP")
)
context: Mapped[Optional[str]] = mapped_column(String, nullable=True)
# Relationships
from_entity: Mapped[Entity] = relationship(
"Entity",
foreign_keys=[from_id],
back_populates="outgoing_relations"
)
to_entity: Mapped[Entity] = relationship(
"Entity",
foreign_keys=[to_id],
back_populates="incoming_relations"
)
def __repr__(self) -> str:
return f"Relation(id={self.id}, from='{self.from_id}', type='{self.relation_type}', to='{self.to_id}')"