from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession from api.database import get_session from api.models.agent_message import AgentMessage from api.schemas.agent_message import MessageCreate, MessageRead, MessageReply from api.services.repository_aliases import ( canonicalize_repository_slug, resolve_repository_slug, ) from hub_core.message_identity import resolve_message_reference from hub_core.models.message_identity_alias import MessageIdentityAlias router = APIRouter(prefix="/messages", tags=["messages"]) async def _get_message(reference: str, session: AsyncSession) -> AgentMessage: message_id = await resolve_message_reference( session, reference, alias_model=MessageIdentityAlias ) if message_id is None: raise HTTPException( status_code=404, detail=f"Message reference {reference!r} not found" ) message = await session.get(AgentMessage, message_id) if message is None: raise HTTPException( status_code=404, detail=f"Message reference {reference!r} not found" ) return message @router.post("/", response_model=MessageRead, status_code=status.HTTP_201_CREATED) async def send_message( body: MessageCreate, session: AsyncSession = Depends(get_session), ) -> AgentMessage: if body.thread_id and await session.get(AgentMessage, body.thread_id) is None: raise HTTPException(status_code=404, detail=f"Thread root {body.thread_id} not found") payload = body.model_dump() payload["from_agent"] = await canonicalize_repository_slug(session, body.from_agent) payload["to_agent"] = await canonicalize_repository_slug(session, body.to_agent) message = AgentMessage(**payload) session.add(message) await session.commit() await session.refresh(message) return message @router.get("/", response_model=list[MessageRead]) async def list_messages( to_agent: str | None = None, from_agent: str | None = None, unread_only: bool = False, limit: int = 50, session: AsyncSession = Depends(get_session), ) -> list[AgentMessage]: query = select(AgentMessage).where(AgentMessage.archived_at.is_(None)) if to_agent: resolution = await resolve_repository_slug(session, to_agent, required=False) values = resolution.slug_values if resolution else (to_agent,) query = query.where( or_(AgentMessage.to_agent.in_(values), AgentMessage.to_agent == "broadcast") ) if from_agent: resolution = await resolve_repository_slug(session, from_agent, required=False) values = resolution.slug_values if resolution else (from_agent,) query = query.where(AgentMessage.from_agent.in_(values)) if unread_only: query = query.where(AgentMessage.read_at.is_(None)) result = await session.execute( query.order_by(AgentMessage.created_at.desc()).limit(limit) ) return list(result.scalars().all()) @router.get("/thread/{thread_id}", response_model=list[MessageRead]) async def get_thread( thread_id: str, session: AsyncSession = Depends(get_session), ) -> list[AgentMessage]: resolved = await resolve_message_reference( session, thread_id, alias_model=MessageIdentityAlias ) if resolved is None: raise HTTPException( status_code=404, detail=f"Message reference {thread_id!r} not found" ) result = await session.execute( select(AgentMessage) .where(or_(AgentMessage.id == resolved, AgentMessage.thread_id == resolved)) .order_by(AgentMessage.created_at) ) return list(result.scalars().all()) @router.patch("/{message_id}/read", response_model=MessageRead) async def mark_read( message_id: str, session: AsyncSession = Depends(get_session), ) -> AgentMessage: message = await _get_message(message_id, session) if message.read_at is None: message.read_at = datetime.now(timezone.utc) await session.commit() await session.refresh(message) return message @router.patch("/{message_id}/archive", response_model=MessageRead) async def archive_message( message_id: str, session: AsyncSession = Depends(get_session), ) -> AgentMessage: message = await _get_message(message_id, session) message.archived_at = datetime.now(timezone.utc) if message.read_at is None: message.read_at = message.archived_at await session.commit() await session.refresh(message) return message @router.post( "/{message_id}/reply", response_model=MessageRead, status_code=status.HTTP_201_CREATED, ) async def reply_to_message( message_id: str, body: MessageReply, session: AsyncSession = Depends(get_session), ) -> AgentMessage: original = await _get_message(message_id, session) if original.read_at is None: original.read_at = datetime.now(timezone.utc) reply = AgentMessage( from_agent=await canonicalize_repository_slug(session, body.from_agent), to_agent=await canonicalize_repository_slug(session, original.from_agent), subject=f"Re: {original.subject}", body=body.body, thread_id=original.thread_id or original.id, ) session.add(reply) await session.commit() await session.refresh(reply) return reply __all__ = ["router"]