feat(01-02): create Project, Discussion, Round, Message, Consensus models

- Add Project model: id, name, created/updated_at, models (JSON), settings (JSON)
- Add Discussion model: id, project_id (FK), question, type, status, created_at
- Add Round model: id, discussion_id (FK), round_number, type
- Add Message model: id, round_id (FK), model, content, timestamp, is_direct
- Add Consensus model: id, discussion_id (FK unique), agreements, disagreements, generated_at/by
- Configure bidirectional relationships with cascade delete-orphan
- All FKs reference correct tables, all type hints present
This commit is contained in:
Mikkel Georgsen 2026-01-16 15:09:32 +00:00
parent 61da27c7d5
commit a0de94141b

View file

@ -8,9 +8,16 @@ All IDs use UUID stored as String(36) for SQLite compatibility.
Enums are stored as strings for database portability.
"""
from enum import Enum
from __future__ import annotations
from sqlalchemy.orm import DeclarativeBase
from datetime import datetime
from enum import Enum
from typing import Any
from uuid import uuid4
from sqlalchemy import JSON, DateTime, ForeignKey, String, Text
from sqlalchemy import Enum as SAEnum
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
@ -38,3 +45,147 @@ class RoundType(str, Enum):
PARALLEL = "parallel"
SEQUENTIAL = "sequential"
def _uuid() -> str:
"""Generate a new UUID string."""
return str(uuid4())
class Project(Base):
"""A project container for related discussions.
Attributes:
id: Unique identifier (UUID).
name: Human-readable project name.
created_at: When the project was created.
updated_at: When the project was last modified.
models: List of AI model identifiers (e.g., ["claude", "gpt", "gemini"]).
settings: Configuration dict (default_rounds, consensus_threshold, system_prompt_override).
discussions: Related discussions in this project.
"""
__tablename__ = "project"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
name: Mapped[str] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
)
models: Mapped[Any] = mapped_column(JSON, default=list)
settings: Mapped[Any] = mapped_column(JSON, default=dict)
discussions: Mapped[list[Discussion]] = relationship(
back_populates="project", cascade="all, delete-orphan"
)
class Discussion(Base):
"""A discussion within a project.
Attributes:
id: Unique identifier (UUID).
project_id: FK to parent project.
question: The question or topic being discussed.
type: Whether this is an "open" or "discuss" mode discussion.
status: Current status (active or completed).
created_at: When the discussion started.
project: Parent project relationship.
rounds: Discussion rounds.
consensus: Generated consensus (if any).
"""
__tablename__ = "discussion"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
project_id: Mapped[str] = mapped_column(ForeignKey("project.id"))
question: Mapped[str] = mapped_column(Text)
type: Mapped[DiscussionType] = mapped_column(SAEnum(DiscussionType))
status: Mapped[DiscussionStatus] = mapped_column(
SAEnum(DiscussionStatus), default=DiscussionStatus.ACTIVE
)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
project: Mapped[Project] = relationship(back_populates="discussions")
rounds: Mapped[list[Round]] = relationship(
back_populates="discussion", cascade="all, delete-orphan"
)
consensus: Mapped[Consensus | None] = relationship(
back_populates="discussion", uselist=False, cascade="all, delete-orphan"
)
class Round(Base):
"""A round within a discussion.
Attributes:
id: Unique identifier (UUID).
discussion_id: FK to parent discussion.
round_number: Sequential round number within the discussion.
type: Whether this round is parallel or sequential.
discussion: Parent discussion relationship.
messages: Messages from AI models in this round.
"""
__tablename__ = "round"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
discussion_id: Mapped[str] = mapped_column(ForeignKey("discussion.id"))
round_number: Mapped[int]
type: Mapped[RoundType] = mapped_column(SAEnum(RoundType))
discussion: Mapped[Discussion] = relationship(back_populates="rounds")
messages: Mapped[list[Message]] = relationship(
back_populates="round", cascade="all, delete-orphan"
)
class Message(Base):
"""A message from an AI model within a round.
Attributes:
id: Unique identifier (UUID).
round_id: FK to parent round.
model: AI model identifier (e.g., "claude", "gpt", "gemini").
content: The message content.
timestamp: When the message was created.
is_direct: True if this was a direct @mention to this model.
round: Parent round relationship.
"""
__tablename__ = "message"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
round_id: Mapped[str] = mapped_column(ForeignKey("round.id"))
model: Mapped[str] = mapped_column(String(50))
content: Mapped[str] = mapped_column(Text)
timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
is_direct: Mapped[bool] = mapped_column(default=False)
round: Mapped[Round] = relationship(back_populates="messages")
class Consensus(Base):
"""Generated consensus summary for a discussion.
Attributes:
id: Unique identifier (UUID).
discussion_id: FK to parent discussion (unique - one consensus per discussion).
agreements: List of bullet point strings for agreed items.
disagreements: List of {topic, positions: {model: position}} dicts.
generated_at: When the consensus was generated.
generated_by: Which model generated this summary.
discussion: Parent discussion relationship.
"""
__tablename__ = "consensus"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid)
discussion_id: Mapped[str] = mapped_column(ForeignKey("discussion.id"), unique=True)
agreements: Mapped[Any] = mapped_column(JSON, default=list)
disagreements: Mapped[Any] = mapped_column(JSON, default=list)
generated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
generated_by: Mapped[str] = mapped_column(String(50))
discussion: Mapped[Discussion] = relationship(back_populates="consensus")