Files
openclaw-mission-control/backend/app/services/board_group_snapshot.py

234 lines
7.4 KiB
Python

"""Helpers for assembling board-group snapshot view models."""
from __future__ import annotations
from collections import defaultdict
from uuid import UUID
from sqlalchemy import case, func
from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.models.agents import Agent
from app.models.board_groups import BoardGroup
from app.models.boards import Board
from app.models.tasks import Task
from app.schemas.board_groups import BoardGroupRead
from app.schemas.boards import BoardRead
from app.schemas.view_models import (
BoardGroupBoardSnapshot,
BoardGroupSnapshot,
BoardGroupTaskSummary,
)
_STATUS_ORDER = {"in_progress": 0, "review": 1, "inbox": 2, "done": 3}
_PRIORITY_ORDER = {"high": 0, "medium": 1, "low": 2}
_RUNTIME_TYPE_REFERENCES = (UUID, AsyncSession)
def _status_weight_expr() -> object:
"""Return a SQL expression that sorts task statuses by configured order."""
whens = [(col(Task.status) == key, weight) for key, weight in _STATUS_ORDER.items()]
return case(*whens, else_=99)
def _priority_weight_expr() -> object:
"""Return a SQL expression that sorts task priorities by configured order."""
whens = [
(col(Task.priority) == key, weight)
for key, weight in _PRIORITY_ORDER.items()
]
return case(*whens, else_=99)
async def _boards_for_group(
session: AsyncSession,
*,
group_id: UUID,
exclude_board_id: UUID | None = None,
) -> list[Board]:
"""Return boards belonging to a board group with optional exclusion."""
statement = Board.objects.filter_by(board_group_id=group_id).statement
if exclude_board_id is not None:
statement = statement.where(col(Board.id) != exclude_board_id)
return list(
await session.exec(
statement.order_by(func.lower(col(Board.name)).asc()),
),
)
async def _task_counts_by_board(
session: AsyncSession,
board_ids: list[UUID],
) -> dict[UUID, dict[str, int]]:
"""Return per-board task counts keyed by task status."""
task_counts: dict[UUID, dict[str, int]] = defaultdict(lambda: defaultdict(int))
for board_id, status_value, total in list(
await session.exec(
select(col(Task.board_id), col(Task.status), func.count(col(Task.id)))
.where(col(Task.board_id).in_(board_ids))
.group_by(col(Task.board_id), col(Task.status)),
),
):
if board_id is None:
continue
task_counts[board_id][str(status_value)] = int(total or 0)
return task_counts
async def _ordered_tasks_for_boards(
session: AsyncSession,
board_ids: list[UUID],
*,
include_done: bool,
) -> list[Task]:
"""Return sorted tasks for boards, optionally excluding completed tasks."""
task_statement = select(Task).where(col(Task.board_id).in_(board_ids))
if not include_done:
task_statement = task_statement.where(col(Task.status) != "done")
task_statement = task_statement.order_by(
col(Task.board_id).asc(),
_status_weight_expr().asc(),
_priority_weight_expr().asc(),
col(Task.updated_at).desc(),
col(Task.created_at).desc(),
)
return list(await session.exec(task_statement))
async def _agent_names(
session: AsyncSession,
tasks: list[Task],
) -> dict[UUID, str]:
"""Return agent names keyed by assigned agent ids in the provided tasks."""
assigned_ids = {
task.assigned_agent_id
for task in tasks
if task.assigned_agent_id is not None
}
if not assigned_ids:
return {}
return dict(
list(
await session.exec(
select(col(Agent.id), col(Agent.name)).where(
col(Agent.id).in_(assigned_ids),
),
),
),
)
def _task_summaries_by_board(
*,
boards_by_id: dict[UUID, Board],
tasks: list[Task],
agent_name_by_id: dict[UUID, str],
per_board_task_limit: int,
) -> dict[UUID, list[BoardGroupTaskSummary]]:
"""Build limited per-board task summary lists."""
tasks_by_board: dict[UUID, list[BoardGroupTaskSummary]] = defaultdict(list)
if per_board_task_limit <= 0:
return tasks_by_board
for task in tasks:
if task.board_id is None:
continue
current = tasks_by_board[task.board_id]
if len(current) >= per_board_task_limit:
continue
board = boards_by_id.get(task.board_id)
if board is None:
continue
current.append(
BoardGroupTaskSummary(
id=task.id,
board_id=task.board_id,
board_name=board.name,
title=task.title,
status=task.status,
priority=task.priority,
assigned_agent_id=task.assigned_agent_id,
assignee=(
agent_name_by_id.get(task.assigned_agent_id)
if task.assigned_agent_id is not None
else None
),
due_at=task.due_at,
in_progress_at=task.in_progress_at,
created_at=task.created_at,
updated_at=task.updated_at,
),
)
return tasks_by_board
async def build_group_snapshot(
session: AsyncSession,
*,
group: BoardGroup,
exclude_board_id: UUID | None = None,
include_done: bool = False,
per_board_task_limit: int = 5,
) -> BoardGroupSnapshot:
"""Build a board-group snapshot with board/task summaries."""
boards = await _boards_for_group(
session,
group_id=group.id,
exclude_board_id=exclude_board_id,
)
if not boards:
return BoardGroupSnapshot(
group=BoardGroupRead.model_validate(group, from_attributes=True),
)
boards_by_id = {board.id: board for board in boards}
board_ids = list(boards_by_id.keys())
task_counts = await _task_counts_by_board(session, board_ids)
tasks = await _ordered_tasks_for_boards(
session,
board_ids,
include_done=include_done,
)
agent_name_by_id = await _agent_names(session, tasks)
tasks_by_board = _task_summaries_by_board(
boards_by_id=boards_by_id,
tasks=tasks,
agent_name_by_id=agent_name_by_id,
per_board_task_limit=per_board_task_limit,
)
snapshots = [
BoardGroupBoardSnapshot(
board=BoardRead.model_validate(board, from_attributes=True),
task_counts=dict(task_counts.get(board.id, {})),
tasks=tasks_by_board.get(board.id, []),
)
for board in boards
]
return BoardGroupSnapshot(
group=BoardGroupRead.model_validate(group, from_attributes=True),
boards=snapshots,
)
async def build_board_group_snapshot(
session: AsyncSession,
*,
board: Board,
include_self: bool = False,
include_done: bool = False,
per_board_task_limit: int = 5,
) -> BoardGroupSnapshot:
"""Build a board-group snapshot anchored to a board context."""
if not board.board_group_id:
return BoardGroupSnapshot(group=None, boards=[])
group = await BoardGroup.objects.by_id(board.board_group_id).first(session)
if group is None:
return BoardGroupSnapshot(group=None, boards=[])
return await build_group_snapshot(
session,
group=group,
exclude_board_id=None if include_self else board.id,
include_done=include_done,
per_board_task_limit=per_board_task_limit,
)