Initial commit: AI Dungeon clone (FastAPI backend + React frontend)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFsGHju9szibJJa2YJcdbg
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON, Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Table, Text,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
scenario_scripts = Table(
|
||||
"scenario_scripts",
|
||||
Base.metadata,
|
||||
Column("scenario_id", ForeignKey("scenarios.id", ondelete="CASCADE"), primary_key=True),
|
||||
Column("script_id", ForeignKey("scripts.id", ondelete="CASCADE"), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
class Scenario(Base):
|
||||
__tablename__ = "scenarios"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(200), default="Untitled Scenario")
|
||||
description: Mapped[str] = mapped_column(Text, default="")
|
||||
prompt: Mapped[str] = mapped_column(Text, default="")
|
||||
# Plot components (AI Dungeon terminology; `memory` == Plot Essentials)
|
||||
memory: Mapped[str] = mapped_column(Text, default="")
|
||||
authors_note: Mapped[str] = mapped_column(Text, default="")
|
||||
ai_instructions: Mapped[str] = mapped_column(Text, default="")
|
||||
tags: Mapped[str] = mapped_column(String(500), default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, onupdate=utcnow)
|
||||
|
||||
story_cards: Mapped[list["StoryCard"]] = relationship(
|
||||
back_populates="scenario", cascade="all, delete-orphan"
|
||||
)
|
||||
adventures: Mapped[list["Adventure"]] = relationship(back_populates="scenario")
|
||||
scripts: Mapped[list["Script"]] = relationship(secondary=scenario_scripts)
|
||||
|
||||
|
||||
class Adventure(Base):
|
||||
__tablename__ = "adventures"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
scenario_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("scenarios.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
title: Mapped[str] = mapped_column(String(200), default="Untitled Adventure")
|
||||
memory: Mapped[str] = mapped_column(Text, default="")
|
||||
authors_note: Mapped[str] = mapped_column(Text, default="")
|
||||
ai_instructions: Mapped[str] = mapped_column(Text, default="")
|
||||
story_summary: Mapped[str] = mapped_column(Text, default="")
|
||||
script_state: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
# Phase 6: opt-in per adventure (extra AI calls)
|
||||
auto_summarize: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
memory_bank_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
# How many actions have already been folded into memories / the story summary.
|
||||
memory_cursor: Mapped[int] = mapped_column(Integer, default=0)
|
||||
summary_cursor: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, onupdate=utcnow)
|
||||
|
||||
scenario: Mapped[Scenario | None] = relationship(back_populates="adventures")
|
||||
story_cards: Mapped[list["StoryCard"]] = relationship(
|
||||
back_populates="adventure", cascade="all, delete-orphan"
|
||||
)
|
||||
actions: Mapped[list["Action"]] = relationship(
|
||||
back_populates="adventure",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="Action.index",
|
||||
)
|
||||
scripts: Mapped[list["AdventureScript"]] = relationship(
|
||||
back_populates="adventure",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="AdventureScript.position",
|
||||
)
|
||||
memories: Mapped[list["Memory"]] = relationship(
|
||||
back_populates="adventure",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="Memory.id",
|
||||
)
|
||||
|
||||
|
||||
class Memory(Base):
|
||||
"""Phase 6: an auto-summarized (or hand-written) fact about the adventure.
|
||||
|
||||
`embedding` is the raw vector as a JSON list (cosine ranking happens in
|
||||
Python — fine at bank sizes of a few hundred). NULL until embedded, which
|
||||
also marks it for backfill when an embedding model becomes available.
|
||||
"""
|
||||
|
||||
__tablename__ = "memories"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
adventure_id: Mapped[int] = mapped_column(ForeignKey("adventures.id", ondelete="CASCADE"))
|
||||
text: Mapped[str] = mapped_column(Text, default="")
|
||||
embedding: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||||
# Action index range this memory summarizes (null for manual memories).
|
||||
source_start: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
source_end: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
pinned: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
forgotten: Mapped[bool] = mapped_column(Boolean, default=False) # evicted, kept for UI
|
||||
use_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
last_used_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow)
|
||||
|
||||
adventure: Mapped[Adventure] = relationship(back_populates="memories")
|
||||
|
||||
@property
|
||||
def embedded(self) -> bool:
|
||||
return self.embedding is not None
|
||||
|
||||
|
||||
class StoryCard(Base):
|
||||
"""Owned by either a scenario or an adventure (exactly one set)."""
|
||||
|
||||
__tablename__ = "story_cards"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
scenario_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("scenarios.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
adventure_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("adventures.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
type: Mapped[str] = mapped_column(String(100), default="")
|
||||
name: Mapped[str] = mapped_column(String(200), default="")
|
||||
keys: Mapped[str] = mapped_column(Text, default="") # comma-separated triggers
|
||||
entry: Mapped[str] = mapped_column(Text, default="")
|
||||
notes: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
scenario: Mapped[Scenario | None] = relationship(back_populates="story_cards")
|
||||
adventure: Mapped[Adventure | None] = relationship(back_populates="story_cards")
|
||||
|
||||
|
||||
class Action(Base):
|
||||
__tablename__ = "actions"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
adventure_id: Mapped[int] = mapped_column(ForeignKey("adventures.id", ondelete="CASCADE"))
|
||||
index: Mapped[int] = mapped_column(Integer)
|
||||
type: Mapped[str] = mapped_column(String(20)) # start|do|say|story|continue|ai
|
||||
text: Mapped[str] = mapped_column(Text, default="")
|
||||
# Reasoning-model "thinking" that preceded the text (AI actions only).
|
||||
reasoning: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
context_snapshot: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow)
|
||||
|
||||
adventure: Mapped[Adventure] = relationship(back_populates="actions")
|
||||
|
||||
|
||||
class Script(Base):
|
||||
__tablename__ = "scripts"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(200), default="Untitled Script")
|
||||
description: Mapped[str] = mapped_column(Text, default="")
|
||||
library_js: Mapped[str] = mapped_column(Text, default="")
|
||||
input_js: Mapped[str] = mapped_column(Text, default="")
|
||||
context_js: Mapped[str] = mapped_column(Text, default="")
|
||||
output_js: Mapped[str] = mapped_column(Text, default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow, onupdate=utcnow)
|
||||
|
||||
|
||||
class AdventureScript(Base):
|
||||
"""A script copied into an adventure at creation, so library edits don't
|
||||
change running adventures. `state` lives on Adventure.script_state (one
|
||||
shared state per adventure, as in AI Dungeon)."""
|
||||
|
||||
__tablename__ = "adventure_scripts"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
adventure_id: Mapped[int] = mapped_column(ForeignKey("adventures.id", ondelete="CASCADE"))
|
||||
position: Mapped[int] = mapped_column(Integer, default=0)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
name: Mapped[str] = mapped_column(String(200), default="Untitled Script")
|
||||
description: Mapped[str] = mapped_column(Text, default="")
|
||||
library_js: Mapped[str] = mapped_column(Text, default="")
|
||||
input_js: Mapped[str] = mapped_column(Text, default="")
|
||||
context_js: Mapped[str] = mapped_column(Text, default="")
|
||||
output_js: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
adventure: Mapped[Adventure] = relationship(back_populates="scripts")
|
||||
|
||||
|
||||
class Settings(Base):
|
||||
__tablename__ = "settings"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True) # single row, id=1
|
||||
endpoint_url: Mapped[str] = mapped_column(String(500), default="http://localhost:11434/v1")
|
||||
api_key: Mapped[str] = mapped_column(String(500), default="")
|
||||
model: Mapped[str] = mapped_column(String(200), default="")
|
||||
api_mode: Mapped[str] = mapped_column(String(20), default="chat") # chat|completion
|
||||
temperature: Mapped[float] = mapped_column(Float, default=0.8)
|
||||
max_output_tokens: Mapped[int] = mapped_column(Integer, default=400)
|
||||
# Separate thinking budget for reasoning models (OpenRouter-style
|
||||
# `reasoning: {max_tokens}`); 0 = param not sent. Added on top of
|
||||
# max_output_tokens so story output keeps its full budget.
|
||||
reasoning_max_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
||||
context_token_budget: Mapped[int] = mapped_column(Integer, default=4096)
|
||||
narrator_prompt: Mapped[str] = mapped_column(
|
||||
Text,
|
||||
default=(
|
||||
"You are a masterful storyteller continuing an interactive adventure. "
|
||||
"Continue the story naturally in second person, staying consistent with "
|
||||
"everything established so far. Write vivid prose. Never speak for the "
|
||||
"player or break character. Do not conclude the story; always leave room "
|
||||
"for the player's next action."
|
||||
),
|
||||
)
|
||||
stream: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
# Phase 6: auto-summarization + memory bank
|
||||
summary_model: Mapped[str] = mapped_column(String(200), default="") # "" = main model
|
||||
embedding_model: Mapped[str] = mapped_column(String(200), default="") # "" = bank disabled
|
||||
memory_bank_capacity: Mapped[int] = mapped_column(Integer, default=200)
|
||||
memory_top_k: Mapped[int] = mapped_column(Integer, default=5)
|
||||
Reference in New Issue
Block a user