Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from app.repositories.implementations.source_repository import SourceRepository
from app.repositories.implementations.search_repository import SearchRepository
from app.repositories.implementations.feedback_repository import FeedbackRepository
from app.repositories.implementations.discussion_repository import DiscussionRepository
from app.repositories.implementations.post_repository import PostRepository
from app.core.config import settings
from app.services.analysis_orchestrator import AnalysisOrchestrator
from app.services.claim_conversation_service import ClaimConversationService
Expand All @@ -37,6 +39,8 @@
from app.services.source_service import SourceService
from app.services.search_service import SearchService
from app.services.feedback_service import FeedbackService
from app.services.discussion_service import DiscussionService
from app.services.post_service import PostService
from app.db.session import AsyncSessionLocal

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -97,6 +101,14 @@ async def get_feedback_repository(session: AsyncSession = Depends(get_db)) -> Fe
return FeedbackRepository(session)


async def get_discussion_repository(session: AsyncSession = Depends(get_db)) -> DiscussionRepository:
return DiscussionRepository(session)


async def get_post_repository(session: AsyncSession = Depends(get_db)) -> PostRepository:
return PostRepository(session)


async def get_embedding_generator() -> EmbeddingGeneratorInterface:
return EmbeddingGenerator()

Expand Down Expand Up @@ -227,6 +239,19 @@ async def get_orchestrator_service(
)


async def get_discussion_service(
discussion_repository: DiscussionRepository = Depends(get_discussion_repository),
) -> DiscussionService:
return DiscussionService(discussion_repository=discussion_repository)


async def get_post_service(
post_repository: PostRepository = Depends(get_post_repository),
discussion_repository: DiscussionRepository = Depends(get_discussion_repository),
) -> PostService:
return PostService(post_repository=post_repository, discussion_repository=discussion_repository)


async def get_together_orchestrator_service(
claim_repository: ClaimRepository = Depends(get_claim_repository),
analysis_repository: AnalysisRepository = Depends(get_analysis_repository),
Expand Down
85 changes: 85 additions & 0 deletions app/api/endpoints/discussion_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from fastapi import APIRouter, Depends, Query, status, HTTPException

# from typing import Optional
from uuid import UUID
import logging

from app.models.domain.user import User
from app.services.discussion_service import DiscussionService
from app.schemas.discussion_schema import DiscussionResponse, PaginatedDiscussionsResponse, DiscussionCreate
from app.api.dependencies import get_discussion_service, get_current_user

# 'get_current_user_optional' is useful if you want to know WHO is asking,
# but allow anonymous users to read discussions.

router = APIRouter(prefix="/discussions", tags=["discussions"])
logger = logging.getLogger(__name__)


@router.get("/user", response_model=PaginatedDiscussionsResponse, status_code=status.HTTP_200_OK)
async def get_discussions_per_user(
current_user: User = Depends(get_current_user),
limit: int = Query(10, ge=1, le=100),
offset: int = Query(0, ge=0),
service: DiscussionService = Depends(get_discussion_service),
):
"""
Get a list of discussions.
- If `user_id` is provided, returns discussions for that user.
- Otherwise, returns the most recent discussions system-wide.
"""
if current_user.id:
discussions, total = await service.list_user_discussions(current_user.id, limit=limit, offset=offset)

return {"items": discussions, "total": total, "limit": limit, "offset": offset}


@router.get("/", response_model=PaginatedDiscussionsResponse, status_code=status.HTTP_200_OK)
async def get_recent_discussions(
limit: int = Query(10, ge=1, le=100),
offset: int = Query(0, ge=0),
service: DiscussionService = Depends(get_discussion_service),
):
"""
Get a list of discussions.
- If `user_id` is provided, returns discussions for that user.
- Otherwise, returns the most recent discussions system-wide.
"""

discussions, total = await service.list_recent_discussions(limit=limit, offset=offset)

return {"items": discussions, "total": total, "limit": limit, "offset": offset}


@router.get("/{discussion_id}", response_model=DiscussionResponse)
async def get_discussion_by_id(
discussion_id: UUID,
service: DiscussionService = Depends(get_discussion_service),
):
"""Get a single discussion by ID."""
try:
return await service.get_discussion(discussion_id)
except Exception as e:
# Assuming your service raises NotFoundException
raise HTTPException(status_code=404, detail=str(e))


@router.post("/", response_model=DiscussionResponse, status_code=status.HTTP_201_CREATED)
async def create_discussion(
payload: DiscussionCreate,
current_user: User = Depends(get_current_user),
service: DiscussionService = Depends(get_discussion_service),
):
"""
Create a new discussion.
- Requires authentication.
- 'analysis_id' is optional (use it if you want to attach the discussion to a specific claim analysis).
"""
discussion = await service.create_discussion(
title=payload.title,
description=payload.description,
analysis_id=payload.analysis_id,
user_id=current_user.id, # Matches your User model field
)

return discussion
110 changes: 110 additions & 0 deletions app/api/endpoints/post_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from fastapi import APIRouter, Depends, status, HTTPException, Query
from typing import List
from uuid import UUID
import logging

from app.models.domain.user import User
from app.services.post_service import PostService
from app.schemas.post_schema import PostCreate, PostUpdate, PostVote, PostResponse
from app.api.dependencies import get_post_service, get_current_user
from app.core.exceptions import NotFoundException, NotAuthorizedException

router = APIRouter(prefix="/posts", tags=["Posts"])
logger = logging.getLogger(__name__)


# --- CREATE POST ---
@router.post("/", response_model=PostResponse, status_code=status.HTTP_201_CREATED)
async def create_post(
payload: PostCreate,
current_user: User = Depends(get_current_user),
service: PostService = Depends(get_post_service),
):
"""Create a new post in a discussion."""
try:
return await service.create_post(
discussion_id=payload.discussion_id, user_id=current_user.id, text=payload.text
)
except NotFoundException as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.exception("Error creating post")
raise HTTPException(status_code=500, detail=str(e))


# --- UPDATE POST TEXT ---
@router.put("/{post_id}", response_model=PostResponse)
async def update_post_content(
post_id: UUID,
payload: PostUpdate,
current_user: User = Depends(get_current_user),
service: PostService = Depends(get_post_service),
):
"""
Update the text content of a post.
Only the creator of the post can do this.
"""
try:
return await service.update_post_text(post_id=post_id, user_id=current_user.id, new_text=payload.text)
except NotFoundException as e:
raise HTTPException(status_code=404, detail=str(e))
except NotAuthorizedException as e:
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
logger.exception("Error updating post")
raise HTTPException(status_code=500, detail=str(e))


# --- VOTE ON POST ---
@router.put("/{post_id}/vote", response_model=PostResponse)
async def vote_on_post(
post_id: UUID,
payload: PostVote,
current_user: User = Depends(get_current_user),
service: PostService = Depends(get_post_service),
):
"""
Increment upvotes or downvotes.
Payload: {"vote_type": "up"} or {"vote_type": "down"}
"""
try:
return await service.vote_post(post_id=post_id, vote_type=payload.vote_type)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except NotFoundException as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.exception("Error voting on post")
raise HTTPException(status_code=500, detail=str(e))


# --- DELETE POST ---
@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_post(
post_id: UUID,
current_user: User = Depends(get_current_user),
service: PostService = Depends(get_post_service),
):
"""Delete a post (Owner only)."""
try:
await service.delete_post(post_id=post_id, user_id=current_user.id)
except NotFoundException as e:
raise HTTPException(status_code=404, detail=str(e))
except NotAuthorizedException as e:
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
logger.exception("Error deleting post")
raise HTTPException(status_code=500, detail=str(e))


# --- GET POSTS BY DISCUSSION ---
@router.get("/discussion/{discussion_id}", response_model=List[PostResponse])
async def list_posts_for_discussion(
discussion_id: UUID,
limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0),
service: PostService = Depends(get_post_service),
):
"""List all posts belonging to a specific discussion."""
posts, _ = await service.list_discussion_posts(discussion_id=discussion_id, limit=limit, offset=offset)
return posts
4 changes: 4 additions & 0 deletions app/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
domain_endpoints,
health_endpoints,
claim_conversation_endpoints,
discussion_endpoints,
post_endpoints,
)

router = APIRouter()
Expand All @@ -24,5 +26,7 @@
router.include_router(conversation_endpoints.router, tags=["conversations"])
router.include_router(message_endpoints.router, tags=["messages"])
router.include_router(domain_endpoints.router, tags=["domains"])
router.include_router(discussion_endpoints.router, tags=["discussions"])
router.include_router(post_endpoints.router, tags=["posts"])
router.include_router(claim_conversation_endpoints.router, tags=["claim-conversations"])
router.include_router(health_endpoints.router, tags=["health"])
76 changes: 76 additions & 0 deletions app/models/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class UserModel(Base):
claims: Mapped[List["ClaimModel"]] = relationship(back_populates="user", cascade="all, delete-orphan")
conversations: Mapped[List["ConversationModel"]] = relationship(back_populates="user", cascade="all, delete-orphan")
feedbacks: Mapped[List["FeedbackModel"]] = relationship(back_populates="user", cascade="all, delete-orphan")
discussions: Mapped[List["DiscussionModel"]] = relationship(back_populates="user", passive_deletes=True)
posts: Mapped[List["PostModel"]] = relationship(back_populates="user", passive_deletes=True)


class DomainModel(Base):
Expand Down Expand Up @@ -150,6 +152,7 @@ class AnalysisModel(Base):
cascade="all, delete-orphan",
primaryjoin="FeedbackModel.analysis_id == AnalysisModel.id",
)
discussions: Mapped[List["DiscussionModel"]] = relationship(back_populates="analysis", passive_deletes=True)
messages: Mapped[List["MessageModel"]] = relationship(back_populates="analysis", doc="Related analysis, if any")

__table_args__ = (
Expand All @@ -158,6 +161,79 @@ class AnalysisModel(Base):
)


class DiscussionModel(Base):
__tablename__ = "discussions"

analysis_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("analysis.id", ondelete="SET NULL"),
nullable=True,
index=True,
)

title: Mapped[str] = mapped_column(
Text,
nullable=False,
)

description: Mapped[str] = mapped_column(
Text,
nullable=True,
)

user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="SET NULL"), # If User dies, set this to NULL
nullable=True, # Must be True for SET NULL to work
index=True,
)

# Relationships
user: Mapped["UserModel"] = relationship(back_populates="discussions")

analysis: Mapped["AnalysisModel"] = relationship(back_populates="discussions")

posts: Mapped[List["PostModel"]] = relationship(back_populates="discussion", cascade="all, delete-orphan")


class PostModel(Base):
__tablename__ = "posts"

discussion_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("discussions.id"),
nullable=False,
index=True,
)

text: Mapped[str] = mapped_column(
Text,
nullable=False,
)

up_votes: Mapped[str] = mapped_column(
Integer,
nullable=True,
)

down_votes: Mapped[str] = mapped_column(
Integer,
nullable=True,
)

user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="SET NULL"), # If User dies, set this to NULL
nullable=True, # Must be True for SET NULL to work
index=True,
)

# Relationships
user: Mapped["UserModel"] = relationship(back_populates="posts")

discussion: Mapped["DiscussionModel"] = relationship(back_populates="posts")


class SearchModel(Base):
__tablename__ = "searches"

Expand Down
Loading