feat(05-01): create discussion service with CRUD operations

Add discussion.py service with create_discussion, get_discussion,
get_active_discussion, list_discussions, and complete_discussion.
Uses selectinload for eager loading of rounds, messages, and consensus.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikkel Georgsen 2026-01-16 19:25:36 +00:00
parent 5afd8b6213
commit 3258c3a596
2 changed files with 141 additions and 1 deletions

View file

@ -4,6 +4,22 @@ Services encapsulate database operations and business rules,
providing a clean interface for handlers to use.
"""
from moai.core.services.discussion import (
complete_discussion,
create_discussion,
get_active_discussion,
get_discussion,
list_discussions,
)
from moai.core.services.project import create_project, get_project, list_projects
__all__ = ["create_project", "get_project", "list_projects"]
__all__ = [
"complete_discussion",
"create_discussion",
"create_project",
"get_active_discussion",
"get_discussion",
"get_project",
"list_discussions",
"list_projects",
]

View file

@ -0,0 +1,124 @@
"""Discussion service for MoAI.
Provides CRUD operations for discussions, rounds, and messages.
"""
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from moai.core.database import get_session
from moai.core.models import (
Discussion,
DiscussionStatus,
DiscussionType,
Round,
)
async def create_discussion(
project_id: str,
question: str,
discussion_type: DiscussionType,
) -> Discussion:
"""Create a new discussion within a project.
Args:
project_id: The parent project's UUID.
question: The question or topic being discussed.
discussion_type: Whether this is OPEN or DISCUSS mode.
Returns:
The created Discussion object.
"""
async with get_session() as session:
discussion = Discussion(
project_id=project_id,
question=question,
type=discussion_type,
)
session.add(discussion)
await session.flush()
await session.refresh(discussion)
return discussion
async def get_discussion(discussion_id: str) -> Discussion | None:
"""Get a discussion by ID with eager loading of rounds and messages.
Args:
discussion_id: The discussion's UUID.
Returns:
The Discussion object if found, None otherwise.
"""
async with get_session() as session:
result = await session.execute(
select(Discussion)
.where(Discussion.id == discussion_id)
.options(
selectinload(Discussion.rounds).selectinload(Round.messages),
selectinload(Discussion.consensus),
)
)
return result.scalar_one_or_none()
async def get_active_discussion(project_id: str) -> Discussion | None:
"""Get the active discussion for a project.
Args:
project_id: The project's UUID.
Returns:
The active Discussion object if found, None otherwise.
"""
async with get_session() as session:
result = await session.execute(
select(Discussion)
.where(Discussion.project_id == project_id)
.where(Discussion.status == DiscussionStatus.ACTIVE)
.options(
selectinload(Discussion.rounds).selectinload(Round.messages),
)
)
return result.scalar_one_or_none()
async def list_discussions(project_id: str) -> list[Discussion]:
"""List all discussions for a project ordered by creation date (newest first).
Args:
project_id: The project's UUID.
Returns:
List of Discussion objects.
"""
async with get_session() as session:
result = await session.execute(
select(Discussion)
.where(Discussion.project_id == project_id)
.order_by(Discussion.created_at.desc())
)
return list(result.scalars().all())
async def complete_discussion(discussion_id: str) -> Discussion | None:
"""Mark a discussion as completed.
Args:
discussion_id: The discussion's UUID.
Returns:
The updated Discussion object if found, None otherwise.
"""
async with get_session() as session:
result = await session.execute(select(Discussion).where(Discussion.id == discussion_id))
discussion = result.scalar_one_or_none()
if discussion is None:
return None
discussion.status = DiscussionStatus.COMPLETED
await session.flush()
await session.refresh(discussion)
return discussion