Files
ai-rpg/app/models/__init__.py
2026-06-21 02:41:48 +03:00

412 lines
17 KiB
Python

"""SQLAlchemy ORM models for AI-RPG.
All tables follow the schema defined in `docs/AI-RPG_TZ_TDD.md` §5.
UUIDs are used as primary keys throughout. Timestamps are TIMESTAMPTZ.
JSONB columns are used for flexible structured data (world config, entity data, etc.).
"""
from __future__ import annotations
import uuid
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import (
Boolean,
DateTime,
Float,
ForeignKey,
Index,
Integer,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
def _now() -> datetime:
return datetime.now(timezone.utc)
# --------------------------------------------------------------------------- #
# Users
# --------------------------------------------------------------------------- #
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
last_login_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
worlds: Mapped[list["World"]] = relationship(back_populates="owner")
presets: Mapped[list["WorldPreset"]] = relationship(back_populates="owner")
__table_args__ = (
Index("idx_users_email", "email", unique=True),
Index("idx_users_username", "username", unique=True),
)
# --------------------------------------------------------------------------- #
# Settings (key-value with JSONB value)
# --------------------------------------------------------------------------- #
class Setting(Base):
__tablename__ = "settings"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
value: Mapped[Any] = mapped_column(JSONB, nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now, onupdate=_now
)
__table_args__ = (Index("idx_settings_key", "key", unique=True),)
# --------------------------------------------------------------------------- #
# World presets
# --------------------------------------------------------------------------- #
class WorldPreset(Base):
__tablename__ = "world_presets"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
# owner_id is nullable so that builtin/system presets can exist without
# a real user owning them. Custom presets created via the admin UI get
# the admin's user_id.
owner_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
language: Mapped[str] = mapped_column(String(8), nullable=False, default="en")
rules: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
time_schema: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
schemas: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
environment_schema: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
environment_initial: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
is_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now, onupdate=_now
)
owner: Mapped[User | None] = relationship(back_populates="presets")
worlds: Mapped[list["World"]] = relationship(back_populates="preset")
# --------------------------------------------------------------------------- #
# Worlds
# --------------------------------------------------------------------------- #
class World(Base):
__tablename__ = "worlds"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
owner_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
preset_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("world_presets.id", ondelete="SET NULL"),
nullable=True,
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
language: Mapped[str] = mapped_column(String(8), nullable=False, default="en")
rules: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
time_schema: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
schemas: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
environment_schema: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
environment: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
plot_rails: Mapped[dict] = mapped_column(
JSONB,
nullable=False,
default=lambda: {"hooks": [], "current_goals": [], "completed_goals": []},
)
current_time: Mapped[str] = mapped_column(String(32), nullable=False, default="day_1_hour_8")
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
intro_scene: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now, onupdate=_now
)
last_played_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
owner: Mapped[User] = relationship(back_populates="worlds")
preset: Mapped[WorldPreset | None] = relationship(back_populates="worlds")
entities: Mapped[list["Entity"]] = relationship(
back_populates="world", cascade="all, delete-orphan"
)
steps: Mapped[list["Step"]] = relationship(
back_populates="world", cascade="all, delete-orphan"
)
deferred_triggers: Mapped[list["DeferredTrigger"]] = relationship(
back_populates="world", cascade="all, delete-orphan"
)
story_entries: Mapped[list["StoryEntry"]] = relationship(
back_populates="world", cascade="all, delete-orphan"
)
__table_args__ = (
Index("idx_worlds_owner_id", "owner_id"),
Index("idx_worlds_status", "status"),
Index("idx_worlds_last_played_at", "last_played_at"),
)
# --------------------------------------------------------------------------- #
# Entities
# --------------------------------------------------------------------------- #
class Entity(Base):
__tablename__ = "entities"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
world_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("worlds.id", ondelete="CASCADE"), nullable=False
)
entity_type: Mapped[str] = mapped_column(String(64), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
data: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
is_in_environment: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
qdrant_point_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
embedding_status: Mapped[str] = mapped_column(
String(16), nullable=False, default="pending"
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now, onupdate=_now
)
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
world: Mapped[World] = relationship(back_populates="entities")
__table_args__ = (
Index("idx_entities_world_id", "world_id"),
Index("idx_entities_world_type", "world_id", "entity_type"),
Index("idx_entities_embedding_status", "embedding_status"),
)
# --------------------------------------------------------------------------- #
# Steps
# --------------------------------------------------------------------------- #
class Step(Base):
__tablename__ = "steps"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
world_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("worlds.id", ondelete="CASCADE"), nullable=False
)
sequence_number: Mapped[int] = mapped_column(Integer, nullable=False)
player_action: Mapped[str] = mapped_column(Text, nullable=False)
scene_text: Mapped[str | None] = mapped_column(Text, nullable=True)
scene_delta_time: Mapped[str | None] = mapped_column(String(32), nullable=True)
suggested_actions: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
tool_calls_summary: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
metadata_: Mapped[dict] = mapped_column(
"metadata", JSONB, nullable=False, default=dict
)
phase1_log_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("llm_call_logs.id", ondelete="SET NULL"),
nullable=True,
)
phase2_log_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("llm_call_logs.id", ondelete="SET NULL"),
nullable=True,
)
phase3_summary_log_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("llm_call_logs.id", ondelete="SET NULL"),
nullable=True,
)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
world: Mapped[World] = relationship(back_populates="steps")
tool_calls: Mapped[list["StepToolCall"]] = relationship(
back_populates="step", cascade="all, delete-orphan"
)
__table_args__ = (
Index("idx_steps_world_seq", "world_id", "sequence_number"),
Index("idx_steps_created_at", "created_at"),
)
# --------------------------------------------------------------------------- #
# Step tool calls
# --------------------------------------------------------------------------- #
class StepToolCall(Base):
__tablename__ = "step_tool_calls"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
step_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("steps.id", ondelete="CASCADE"), nullable=False
)
tool_name: Mapped[str] = mapped_column(String(64), nullable=False)
arguments: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
result: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
is_success: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
executed_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
step: Mapped[Step] = relationship(back_populates="tool_calls")
# --------------------------------------------------------------------------- #
# Deferred triggers
# --------------------------------------------------------------------------- #
class DeferredTrigger(Base):
__tablename__ = "deferred_triggers"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
world_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("worlds.id", ondelete="CASCADE"), nullable=False
)
fire_at: Mapped[str] = mapped_column(String(32), nullable=False)
event_type: Mapped[str] = mapped_column(String(64), nullable=False)
payload: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
is_fired: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
fired_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
world: Mapped[World] = relationship(back_populates="deferred_triggers")
__table_args__ = (
Index("idx_triggers_world_pending", "world_id", "is_fired", "fire_at"),
)
# --------------------------------------------------------------------------- #
# Story entries (RAG facts)
# --------------------------------------------------------------------------- #
class StoryEntry(Base):
__tablename__ = "story_entries"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
world_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("worlds.id", ondelete="CASCADE"), nullable=False
)
content: Mapped[str] = mapped_column(Text, nullable=False)
entry_type: Mapped[str] = mapped_column(String(64), nullable=False)
qdrant_point_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
embedding_status: Mapped[str] = mapped_column(
String(16), nullable=False, default="pending"
)
metadata_: Mapped[dict] = mapped_column(
"metadata", JSONB, nullable=False, default=dict
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
world: Mapped[World] = relationship(back_populates="story_entries")
__table_args__ = (
Index("idx_story_world_type", "world_id", "entry_type"),
Index("idx_story_status", "embedding_status"),
)
# --------------------------------------------------------------------------- #
# LLM call logs
# --------------------------------------------------------------------------- #
class LlmCallLog(Base):
__tablename__ = "llm_call_logs"
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
user_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
)
world_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("worlds.id", ondelete="SET NULL"),
nullable=True,
)
step_id: Mapped[uuid.UUID | None] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("steps.id", ondelete="SET NULL"),
nullable=True,
)
stage: Mapped[str] = mapped_column(String(64), nullable=False)
model: Mapped[str] = mapped_column(String(128), nullable=False)
request_messages: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
request_tools: Mapped[list | None] = mapped_column(JSONB, nullable=True)
response_message: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
tool_calls: Mapped[list | None] = mapped_column(JSONB, nullable=True)
prompt_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
completion_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
temperature: Mapped[float | None] = mapped_column(Float, nullable=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="ok")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, default=_now
)
__table_args__ = (
Index("idx_logs_world_created", "world_id", "created_at"),
Index("idx_logs_stage", "stage"),
Index("idx_logs_status", "status"),
)