PMFlow/backend/app/services/ai_pmo_cadence/mixin_queries_a.py

130 lines
4.4 KiB
Python

"""Query helpers (part A) for AI PMO cadence service."""
from __future__ import annotations
from .shared import (
Any,
Optional,
ProjectSignalLink,
Signal,
Task,
WorkItem,
and_,
func,
or_,
select,
utc_now_naive,
)
class CadenceQueriesAMixin:
"""Work item/task/signal query helpers."""
async def _list_recent_work_items(self, *, since, project_id: Optional[int]) -> list[dict[str, Any]]:
stmt = select(WorkItem).where(
and_(
WorkItem.tenant_id == self.tenant_id,
WorkItem.created_at >= since,
)
)
if project_id is not None:
stmt = stmt.where(WorkItem.project_id == int(project_id))
rows = (await self.db.execute(stmt.order_by(WorkItem.created_at.desc()).limit(50))).scalars().all()
return [
{
"id": int(row.id),
"state": row.state,
"intent": row.intent,
"project_id": row.project_id,
"signal_id": row.signal_id,
"created_at": row.created_at.isoformat() if row.created_at else None,
}
for row in rows
]
async def _count_work_items(self, *, state: str, project_id: Optional[int]) -> int:
stmt = select(func.count(WorkItem.id)).where(
and_(
WorkItem.tenant_id == self.tenant_id,
WorkItem.state == state,
)
)
if project_id is not None:
stmt = stmt.where(WorkItem.project_id == int(project_id))
return int((await self.db.execute(stmt)).scalar_one() or 0)
async def _list_blocked_tasks(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
stmt = select(Task).where(
and_(
Task.tenant_id == self.tenant_id,
Task.status == "blocked",
)
)
if project_id is not None:
stmt = stmt.where(Task.project_id == int(project_id))
rows = (await self.db.execute(stmt.order_by(Task.updated_at.desc()).limit(50))).scalars().all()
return [
{
"id": int(row.id),
"project_id": int(row.project_id),
"title": str(row.title or ""),
"priority": str(row.priority or ""),
}
for row in rows
]
async def _list_overdue_tasks(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
now = utc_now_naive()
stmt = select(Task).where(
and_(
Task.tenant_id == self.tenant_id,
Task.due_date.is_not(None),
Task.due_date < now,
Task.status.not_in(["done", "completed", "closed"]),
)
)
if project_id is not None:
stmt = stmt.where(Task.project_id == int(project_id))
rows = (await self.db.execute(stmt.order_by(Task.due_date.asc()).limit(50))).scalars().all()
return [
{
"id": int(row.id),
"project_id": int(row.project_id),
"title": str(row.title or ""),
"due_date": row.due_date.isoformat() if row.due_date else None,
}
for row in rows
]
async def _list_recent_signals(self, *, since, project_id: Optional[int]) -> list[dict[str, Any]]:
stmt = select(Signal).where(
and_(
Signal.tenant_id == self.tenant_id,
Signal.created_at >= since,
)
)
if project_id is not None:
stmt = stmt.where(
or_(
Signal.matched_project_id == int(project_id),
Signal.id.in_(
select(ProjectSignalLink.signal_id).where(
and_(
ProjectSignalLink.tenant_id == self.tenant_id,
ProjectSignalLink.project_id == int(project_id),
)
)
),
)
)
rows = (await self.db.execute(stmt.order_by(Signal.created_at.desc()).limit(50))).scalars().all()
return [
{
"id": int(row.id),
"title": str(row.title or ""),
"status": str(row.status or ""),
"signal_type": str(row.signal_type or ""),
}
for row in rows
]