PMFlow/backend/app/services/skill/bindings.py

211 lines
6.0 KiB
Python

from __future__ import annotations
from .shared import *
async def init_user_skill_bindings(
db: AsyncSession,
tenant_id: int,
user_id: int,
*,
include_system_only: bool = True,
commit: bool = False,
) -> int:
"""
初始化用户技能绑定(空基线)。
为用户补齐未绑定的标签,默认 `beginner + proficiency=0`。
"""
tags_query = select(SkillTag.id).where(SkillTag.tenant_id == tenant_id)
if include_system_only:
tags_query = tags_query.where(SkillTag.is_system == True)
tag_rows = await db.execute(tags_query)
tag_ids = [int(v) for v in tag_rows.scalars().all()]
if not tag_ids:
return 0
existing_rows = await db.execute(
select(UserSkillBinding.skill_tag_id).where(
and_(
UserSkillBinding.tenant_id == tenant_id,
UserSkillBinding.user_id == user_id,
UserSkillBinding.skill_tag_id.in_(tag_ids),
)
)
)
existing_ids = {int(v) for v in existing_rows.scalars().all()}
created = 0
for skill_tag_id in tag_ids:
if skill_tag_id in existing_ids:
continue
db.add(
UserSkillBinding(
tenant_id=tenant_id,
user_id=user_id,
skill_tag_id=skill_tag_id,
proficiency_level="beginner",
level="beginner",
proficiency=0,
quality_score=0.0,
efficiency_score=0.0,
)
)
created += 1
if created:
await db.flush()
if commit:
await db.commit()
return created
async def batch_update_user_skill_bindings(
db: AsyncSession,
tenant_id: int,
user_id: int,
bindings: List[dict],
) -> List[UserSkillBinding]:
"""
批量更新用户技能绑定。
若 skill_tag_id 已存在则更新,否则创建。
"""
result_bindings = []
for item in bindings:
skill_tag_id = item.get("skill_tag_id")
if skill_tag_id is None:
continue
if isinstance(skill_tag_id, str):
try:
skill_tag_id = int(skill_tag_id)
except ValueError:
continue
proficiency = item.get("proficiency", 50)
level = item.get("level", "intermediate")
years = item.get("years_experience")
existing = await db.execute(
select(UserSkillBinding).where(
and_(
UserSkillBinding.tenant_id == tenant_id,
UserSkillBinding.user_id == user_id,
UserSkillBinding.skill_tag_id == skill_tag_id,
)
)
)
binding = existing.scalar_one_or_none()
if binding:
binding.proficiency = proficiency
binding.level = level
binding.years_experience = years
binding.proficiency_level = level
else:
binding = UserSkillBinding(
tenant_id=tenant_id,
user_id=user_id,
skill_tag_id=skill_tag_id,
proficiency_level=level,
proficiency=proficiency,
level=level,
years_experience=years,
)
db.add(binding)
result_bindings.append(binding)
await db.commit()
for b in result_bindings:
await db.refresh(b)
await AIPMOKernelService.invalidate_world_model_cache_for_user(
db=db,
tenant_id=tenant_id,
user_id=user_id,
)
return result_bindings
async def get_user_skill_bindings(
db: AsyncSession,
tenant_id: int,
user_id: int,
) -> List[UserSkillBinding]:
"""按用户查询技能绑定"""
result = await db.execute(
select(UserSkillBinding)
.options(selectinload(UserSkillBinding.skill_tag))
.where(
and_(
UserSkillBinding.tenant_id == tenant_id,
UserSkillBinding.user_id == user_id,
)
)
.order_by(UserSkillBinding.proficiency.desc())
)
return list(result.scalars().all())
async def get_skill_tag_user_bindings(
db: AsyncSession,
tenant_id: int,
skill_tag_id: int,
) -> List[UserSkillBinding]:
"""按技能标签查询用户绑定"""
result = await db.execute(
select(UserSkillBinding)
.options(
selectinload(UserSkillBinding.user),
selectinload(UserSkillBinding.skill_tag),
)
.where(
and_(
UserSkillBinding.tenant_id == tenant_id,
UserSkillBinding.skill_tag_id == skill_tag_id,
)
)
.order_by(UserSkillBinding.proficiency.desc())
)
return list(result.scalars().all())
async def get_user_skill_task_history(
db: AsyncSession,
tenant_id: int,
user_id: int,
skill_tag_id: int,
limit: int = 50,
) -> List[dict]:
"""
查询用户某技能下的历史任务。
口径:
- assignee_id == user_id
- required_skill_tag_ids 包含 skill_tag_id
"""
result = await db.execute(
select(Task)
.where(
and_(
Task.tenant_id == tenant_id,
Task.assignee_id == user_id,
Task.required_skill_tag_ids.contains([skill_tag_id]),
)
)
.order_by(Task.updated_at.desc())
.limit(limit)
)
filtered = list(result.scalars().all())
return [
{
"id": str(t.id),
"title": t.title,
"description": t.description,
"status": t.status,
"priority": t.priority,
"estimated_hours": float(t.estimated_hours) if t.estimated_hours is not None else None,
"project_id": str(t.project_id) if t.project_id is not None else None,
"truth_node_id": str(t.truth_node_id) if t.truth_node_id is not None else None,
"updated_at": t.updated_at.isoformat() if t.updated_at else None,
}
for t in filtered
]