From 3258c3a59662a1fdc9f8bb8d04ae05da51bcb50c Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Fri, 16 Jan 2026 19:25:36 +0000 Subject: [PATCH] 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 --- src/moai/core/services/__init__.py | 18 +++- src/moai/core/services/discussion.py | 124 +++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/moai/core/services/discussion.py diff --git a/src/moai/core/services/__init__.py b/src/moai/core/services/__init__.py index 2b8de7f..e4573f3 100644 --- a/src/moai/core/services/__init__.py +++ b/src/moai/core/services/__init__.py @@ -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", +] diff --git a/src/moai/core/services/discussion.py b/src/moai/core/services/discussion.py new file mode 100644 index 0000000..71a6ddf --- /dev/null +++ b/src/moai/core/services/discussion.py @@ -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