PMFlow/backend/app/services/signal/signal_planner/mixin_loaders.py

197 lines
5.6 KiB
Python

"""Data loading helpers for signal planner service."""
from __future__ import annotations
from .shared import (
Any,
AsyncSession,
Optional,
Project,
Signal,
SkillTag,
User,
UserProject,
UserSkillBinding,
UserWorkload,
and_,
select,
)
class SignalPlannerLoadersMixin:
"""Load signal/project/member contexts for planner prompt."""
@classmethod
async def _load_signal(
cls,
db: AsyncSession,
*,
tenant_id: int,
signal_id: int,
) -> Signal | None:
return (
await db.execute(
select(Signal)
.where(
and_(
Signal.tenant_id == int(tenant_id),
Signal.id == int(signal_id),
)
)
.limit(1)
)
).scalar_one_or_none()
@classmethod
async def _load_project_title(
cls,
db: AsyncSession,
*,
tenant_id: int,
project_id: Optional[int],
) -> str:
if project_id is None:
return ""
row = (
await db.execute(
select(Project.title)
.where(
and_(
Project.tenant_id == int(tenant_id),
Project.id == int(project_id),
)
)
.limit(1)
)
).scalar_one_or_none()
return str(row or "")
@classmethod
async def _load_recent_signal_summaries(
cls,
db: AsyncSession,
*,
tenant_id: int,
project_id: Optional[int],
exclude_signal_id: int,
) -> list[dict[str, Any]]:
if project_id is None:
return []
rows = (
await db.execute(
select(Signal.id, Signal.title, Signal.summary, Signal.priority)
.where(
and_(
Signal.tenant_id == int(tenant_id),
Signal.matched_project_id == int(project_id),
Signal.id != int(exclude_signal_id),
)
)
.order_by(Signal.created_at.desc())
.limit(8)
)
).all()
items: list[dict[str, Any]] = []
for row in rows:
items.append(
{
"id": int(row[0]),
"title": str(row[1] or "")[:120],
"summary": str(row[2] or "")[:180],
"priority": str(row[3] or "")[:20],
}
)
return items
@classmethod
async def _load_member_profiles(
cls,
db: AsyncSession,
*,
tenant_id: int,
project_id: Optional[int],
) -> list[dict[str, Any]]:
if project_id is None:
return []
rows = (
await db.execute(
select(
User.id,
User.name,
User.role,
UserWorkload.workload_ratio,
SkillTag.name,
UserSkillBinding.proficiency,
)
.join(
UserProject,
and_(
UserProject.tenant_id == int(tenant_id),
UserProject.user_id == User.id,
UserProject.project_id == int(project_id),
),
)
.join(
UserWorkload,
and_(
UserWorkload.tenant_id == int(tenant_id),
UserWorkload.user_id == User.id,
),
isouter=True,
)
.join(
UserSkillBinding,
and_(
UserSkillBinding.tenant_id == int(tenant_id),
UserSkillBinding.user_id == User.id,
),
isouter=True,
)
.join(
SkillTag,
and_(
SkillTag.tenant_id == int(tenant_id),
SkillTag.id == UserSkillBinding.skill_tag_id,
),
isouter=True,
)
.where(
and_(
User.tenant_id == int(tenant_id),
User.is_active == True,
)
)
.order_by(User.id.asc())
)
).all()
by_user: dict[int, dict[str, Any]] = {}
for row in rows:
user_id = cls._safe_int(row[0])
if user_id is None:
continue
profile = by_user.setdefault(
int(user_id),
{
"user_id": int(user_id),
"name": str(row[1] or "成员"),
"role": str(row[2] or ""),
"workload_ratio": float(row[3] or 0.0),
"skills": [],
},
)
skill_name = str(row[4] or "").strip()
proficiency = cls._safe_int(row[5])
if skill_name:
profile["skills"].append(
{
"name": skill_name,
"proficiency": proficiency,
}
)
items = list(by_user.values())
for item in items:
item["skills"] = item["skills"][:10]
return items