From a0de94141b7431ae7e964d84373a7ce333ba904a Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Fri, 16 Jan 2026 15:09:32 +0000 Subject: [PATCH] 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 --- src/moai/core/models.py | 155 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 2 deletions(-) diff --git a/src/moai/core/models.py b/src/moai/core/models.py index e4e6c4c..56a29a8 100644 --- a/src/moai/core/models.py +++ b/src/moai/core/models.py @@ -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")