init
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Text, DateTime, Integer, ForeignKey, JSON, Enum as SAEnum
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
def gen_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
engine = None
|
||||
async_session_maker: async_sessionmaker[AsyncSession] | None = None
|
||||
|
||||
|
||||
async def init_db(database_url: str):
|
||||
global engine, async_session_maker
|
||||
|
||||
engine = create_async_engine(
|
||||
database_url,
|
||||
echo=False,
|
||||
pool_size=20,
|
||||
max_overflow=10,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def get_session() -> AsyncSession:
|
||||
if async_session_maker is None:
|
||||
raise RuntimeError("Database not initialized")
|
||||
async with async_session_maker() as session:
|
||||
yield session
|
||||
@@ -0,0 +1,111 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import String, Text, DateTime, Integer, ForeignKey, JSON, Enum as SAEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.database import Base, gen_uuid
|
||||
from app.models.schemas import GamePhase, CharacterRole, CharacterStatus, ClueType, ClueVisibility
|
||||
|
||||
|
||||
class Script(Base):
|
||||
__tablename__ = "scripts"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
background: Mapped[str] = mapped_column(Text, default="")
|
||||
raw_content: Mapped[str] = mapped_column(Text, default="")
|
||||
parsed_data: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
character_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
characters: Mapped[list["Character"]] = relationship(back_populates="script", cascade="all, delete-orphan")
|
||||
clues: Mapped[list["Clue"]] = relationship(back_populates="script", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Character(Base):
|
||||
__tablename__ = "characters"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
||||
script_id: Mapped[str] = mapped_column(String(36), ForeignKey("scripts.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
role: Mapped[CharacterRole] = mapped_column(SAEnum(CharacterRole), default=CharacterRole.SUSPECT)
|
||||
status: Mapped[CharacterStatus] = mapped_column(SAEnum(CharacterStatus), default=CharacterStatus.ALIVE)
|
||||
personality: Mapped[str] = mapped_column(Text, default="")
|
||||
speaking_style: Mapped[str] = mapped_column(Text, default="")
|
||||
background: Mapped[str] = mapped_column(Text, default="")
|
||||
secret: Mapped[str] = mapped_column(Text, default="")
|
||||
motive: Mapped[str] = mapped_column(Text, default="")
|
||||
avatar_url: Mapped[str] = mapped_column(String(500), default="")
|
||||
hermes_profile: Mapped[str] = mapped_column(String(100), default="")
|
||||
soul_md: Mapped[str] = mapped_column(Text, default="")
|
||||
knowledge_base: Mapped[str] = mapped_column(Text, default="")
|
||||
is_revealed_killer: Mapped[bool] = mapped_column(default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
script: Mapped["Script"] = relationship(back_populates="characters")
|
||||
messages: Mapped[list["Message"]] = relationship(back_populates="character", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = "messages"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
||||
session_id: Mapped[str] = mapped_column(String(36), default="")
|
||||
character_id: Mapped[str] = mapped_column(String(36), ForeignKey("characters.id"), nullable=True)
|
||||
game_phase: Mapped[str] = mapped_column(String(50), default="")
|
||||
msg_type: Mapped[str] = mapped_column(String(50), default="character")
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
target_character_id: Mapped[str] = mapped_column(String(36), nullable=True)
|
||||
clue_id: Mapped[str] = mapped_column(String(36), nullable=True)
|
||||
metadata: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
character: Mapped["Character"] = relationship(back_populates="messages")
|
||||
|
||||
|
||||
class Clue(Base):
|
||||
__tablename__ = "clues"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
||||
script_id: Mapped[str] = mapped_column(String(36), ForeignKey("scripts.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
clue_type: Mapped[ClueType] = mapped_column(SAEnum(ClueType), default=ClueType.PHYSICAL)
|
||||
owner_id: Mapped[str] = mapped_column(String(36), nullable=True)
|
||||
phase: Mapped[str] = mapped_column(String(50), default="round1_search")
|
||||
visibility: Mapped[ClueVisibility] = mapped_column(SAEnum(ClueVisibility), default=ClueVisibility.ALL)
|
||||
visible_to: Mapped[list] = mapped_column(JSON, default=list)
|
||||
is_unlocked: Mapped[bool] = mapped_column(default=False)
|
||||
unlocked_by: Mapped[str] = mapped_column(String(36), nullable=True)
|
||||
unlocked_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
script: Mapped["Script"] = relationship(back_populates="clues")
|
||||
|
||||
|
||||
class GameState(Base):
|
||||
__tablename__ = "game_state"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
||||
current_phase: Mapped[str] = mapped_column(String(50), default="intro")
|
||||
current_speaker_id: Mapped[str] = mapped_column(String(36), nullable=True)
|
||||
speaker_order: Mapped[list] = mapped_column(JSON, default=list)
|
||||
active_script_id: Mapped[str] = mapped_column(String(36), nullable=True)
|
||||
is_running: Mapped[bool] = mapped_column(default=False)
|
||||
is_paused: Mapped[bool] = mapped_column(default=False)
|
||||
phase_started_at: Mapped[datetime] = mapped_column(DateTime, nullable=True)
|
||||
config: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
|
||||
class Vote(Base):
|
||||
__tablename__ = "votes"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=gen_uuid)
|
||||
voter_id: Mapped[str] = mapped_column(String(36), nullable=False)
|
||||
target_id: Mapped[str] = mapped_column(String(36), nullable=False)
|
||||
round_number: Mapped[int] = mapped_column(Integer, default=1)
|
||||
reason: Mapped[str] = mapped_column(Text, default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
@@ -0,0 +1,237 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GamePhase(str, Enum):
|
||||
INTRO = "intro"
|
||||
ROUND1_SPEAK = "round1_speak"
|
||||
ROUND1_SEARCH = "round1_search"
|
||||
ROUND2_SPEAK = "round2_speak"
|
||||
ROUND2_SEARCH = "round2_search"
|
||||
FINAL_DISCUSS = "final_discuss"
|
||||
VOTING = "voting"
|
||||
REVEAL = "reveal"
|
||||
|
||||
@classmethod
|
||||
def ordered_phases(cls) -> list[str]:
|
||||
return [
|
||||
"intro", "round1_speak", "round1_search",
|
||||
"round2_speak", "round2_search",
|
||||
"final_discuss", "voting", "reveal"
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def phase_count(cls) -> int:
|
||||
return len(cls.ordered_phases())
|
||||
|
||||
|
||||
class CharacterRole(str, Enum):
|
||||
DETECTIVE = "detective"
|
||||
SUSPECT = "suspect"
|
||||
WITNESS = "witness"
|
||||
VICTIM = "victim"
|
||||
KILLER = "killer"
|
||||
|
||||
|
||||
class CharacterStatus(str, Enum):
|
||||
ALIVE = "alive"
|
||||
DEAD = "dead"
|
||||
INACTIVE = "inactive"
|
||||
|
||||
|
||||
class MessageType(str, Enum):
|
||||
SYSTEM = "system"
|
||||
DM = "dm"
|
||||
CHARACTER = "character"
|
||||
VOTE = "vote"
|
||||
CLUE = "clue"
|
||||
|
||||
|
||||
class ClueType(str, Enum):
|
||||
PHYSICAL = "physical"
|
||||
TESTIMONY = "testimony"
|
||||
MOTIVE = "motive"
|
||||
ALIBI = "alibi"
|
||||
FORENSIC = "forensic"
|
||||
|
||||
|
||||
class ClueVisibility(str, Enum):
|
||||
ALL = "all"
|
||||
SPECIFIC = "specific"
|
||||
HIDDEN = "hidden"
|
||||
|
||||
|
||||
class ScriptUploadRequest(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
file_type: str = "natural_language"
|
||||
|
||||
|
||||
class ScriptParsePreview(BaseModel):
|
||||
title: str
|
||||
background: str
|
||||
characters: list[dict]
|
||||
clues: list[dict]
|
||||
phases: list[dict]
|
||||
|
||||
|
||||
class ScriptImportRequest(BaseModel):
|
||||
title: str
|
||||
background: str = ""
|
||||
characters: list[dict] = Field(default_factory=list)
|
||||
clues: list[dict] = Field(default_factory=list)
|
||||
phases: list[dict] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ScriptResponse(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
background: str
|
||||
character_count: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class CharacterCreate(BaseModel):
|
||||
name: str
|
||||
role: CharacterRole = CharacterRole.SUSPECT
|
||||
personality: str = ""
|
||||
speaking_style: str = ""
|
||||
background: str = ""
|
||||
secret: str = ""
|
||||
motive: str = ""
|
||||
avatar_url: str = ""
|
||||
|
||||
|
||||
class CharacterResponse(BaseModel):
|
||||
id: str
|
||||
script_id: str
|
||||
name: str
|
||||
role: CharacterRole
|
||||
status: CharacterStatus
|
||||
personality: str
|
||||
speaking_style: str
|
||||
background: str
|
||||
secret: str
|
||||
motive: str
|
||||
avatar_url: str
|
||||
hermes_profile: str
|
||||
soul_md: str
|
||||
is_revealed_killer: bool
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class SoulUpdateRequest(BaseModel):
|
||||
soul_md: str
|
||||
knowledge_base: str = ""
|
||||
|
||||
|
||||
class CharacterSpeakRequest(BaseModel):
|
||||
prompt: str = ""
|
||||
target_character_id: Optional[str] = None
|
||||
|
||||
|
||||
class CharacterGenerateRequest(BaseModel):
|
||||
script_id: str
|
||||
character_names: list[str]
|
||||
|
||||
|
||||
class MessageCreate(BaseModel):
|
||||
session_id: str = ""
|
||||
character_id: Optional[str] = None
|
||||
game_phase: str = ""
|
||||
msg_type: str = "character"
|
||||
content: str
|
||||
target_character_id: Optional[str] = None
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
id: str
|
||||
session_id: str
|
||||
character_id: Optional[str] = None
|
||||
game_phase: str
|
||||
msg_type: str
|
||||
content: str
|
||||
target_character_id: Optional[str] = None
|
||||
clue_id: Optional[str] = None
|
||||
metadata: dict
|
||||
created_at: datetime
|
||||
character_name: Optional[str] = None
|
||||
character_role: Optional[CharacterRole] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class ClueCreate(BaseModel):
|
||||
script_id: str
|
||||
name: str
|
||||
content: str
|
||||
clue_type: ClueType = ClueType.PHYSICAL
|
||||
owner_id: Optional[str] = None
|
||||
phase: str = "round1_search"
|
||||
visibility: ClueVisibility = ClueVisibility.ALL
|
||||
visible_to: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ClueUnlockRequest(BaseModel):
|
||||
character_id: str
|
||||
|
||||
|
||||
class ClueResponse(BaseModel):
|
||||
id: str
|
||||
script_id: str
|
||||
name: str
|
||||
content: str
|
||||
clue_type: ClueType
|
||||
owner_id: Optional[str] = None
|
||||
phase: str
|
||||
visibility: ClueVisibility
|
||||
visible_to: list
|
||||
is_unlocked: bool
|
||||
unlocked_by: Optional[str] = None
|
||||
unlocked_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class DmSpeakRequest(BaseModel):
|
||||
content: str
|
||||
phase: str = ""
|
||||
|
||||
|
||||
class GameStateResponse(BaseModel):
|
||||
id: str
|
||||
current_phase: GamePhase
|
||||
current_speaker_id: Optional[str] = None
|
||||
speaker_order: list
|
||||
active_script_id: Optional[str] = None
|
||||
is_running: bool
|
||||
is_paused: bool
|
||||
phase_started_at: Optional[datetime] = None
|
||||
config: dict
|
||||
progress_percent: float = 0.0
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class VoteCreate(BaseModel):
|
||||
voter_id: str
|
||||
target_id: str
|
||||
reason: str = ""
|
||||
|
||||
|
||||
class VoteResponse(BaseModel):
|
||||
id: str
|
||||
voter_id: str
|
||||
target_id: str
|
||||
round_number: int
|
||||
reason: str
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
Reference in New Issue
Block a user