153 lines
5.1 KiB
Python
153 lines
5.1 KiB
Python
"""任务相关 API。"""
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from sqlalchemy import and_, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from ..core.database import get_db
|
||
from ..dependencies import get_current_user, require_manager_user
|
||
from ..models import Task, User, UserProject
|
||
from ..schemas import TaskBreakdownRequest, TaskBreakdownResponse
|
||
from ..services.llm_service import llm_service
|
||
from ..services.integrations.integration_core.plane_service import plane_service
|
||
from ..services.skill import recommend_personnel
|
||
from ..services.sstc.charter_data import (
|
||
_build_task_breakdown_prompt,
|
||
_load_available_skill_names,
|
||
_load_task_breakdown_context,
|
||
)
|
||
from ..services.task_breakdown import normalize_task_breakdown_payload
|
||
|
||
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
||
|
||
|
||
@router.post("/breakdown", response_model=TaskBreakdownResponse)
|
||
async def breakdown_tasks(
|
||
request: TaskBreakdownRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(require_manager_user),
|
||
):
|
||
"""AI 任务拆解"""
|
||
tenant_id = int(current_user.tenant_id)
|
||
context = await _load_task_breakdown_context(
|
||
db,
|
||
tenant_id=tenant_id,
|
||
description=request.description,
|
||
project_id=request.project_id,
|
||
charter_id=request.charter_id,
|
||
)
|
||
if request.project_id is not None and not context.get("project"):
|
||
raise HTTPException(status_code=404, detail="Project not found")
|
||
if request.charter_id is not None and not context.get("charter"):
|
||
raise HTTPException(status_code=404, detail="Charter not found")
|
||
|
||
available_skills = await _load_available_skill_names(db, tenant_id=tenant_id)
|
||
prompt = _build_task_breakdown_prompt(
|
||
project_id=request.project_id,
|
||
context=context,
|
||
available_skills=available_skills,
|
||
)
|
||
try:
|
||
structured = await llm_service.generate_structured_object(
|
||
prompt,
|
||
db=db,
|
||
tenant_id=tenant_id,
|
||
function_name="generate_task_breakdown",
|
||
)
|
||
if not structured.payload:
|
||
raise ValueError("AI 拆解返回空结果")
|
||
payload = normalize_task_breakdown_payload(structured.payload)
|
||
except RuntimeError:
|
||
raise HTTPException(status_code=403, detail="预算超限,AI 调用被阻止(hard limit)")
|
||
except Exception as exc:
|
||
raise HTTPException(status_code=500, detail=f"Failed to generate task breakdown: {exc}")
|
||
|
||
tasks = [task.model_dump(mode="json") for task in payload.tasks]
|
||
sync_result = None
|
||
if request.sync_to_plane:
|
||
sync_result = await plane_service.sync_tasks(tasks)
|
||
|
||
return {"tasks": tasks, "sync_result": sync_result}
|
||
|
||
|
||
@router.get("/recommend-assignees")
|
||
async def recommend_assignees(
|
||
task_id: int = Query(..., description="任务 ID"),
|
||
limit: int = Query(5, ge=1, le=20),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""
|
||
推荐任务负责人(按技能匹配 + 熟练度 + 负载)。
|
||
仅在任务所属项目成员中选择,且排除禁用用户。
|
||
"""
|
||
tenant_id = int(current_user.tenant_id)
|
||
task = (
|
||
await db.execute(
|
||
select(Task).where(
|
||
and_(
|
||
Task.id == task_id,
|
||
Task.tenant_id == tenant_id,
|
||
)
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="Task not found")
|
||
|
||
required_skills = list(task.required_skill_tag_ids or [])
|
||
recommendations = await recommend_personnel(
|
||
db=db,
|
||
tenant_id=tenant_id,
|
||
required_skills=required_skills,
|
||
task_description=task.description or task.title,
|
||
task_id=str(task.id),
|
||
limit=limit * 2,
|
||
)
|
||
|
||
project_user_ids_result = await db.execute(
|
||
select(UserProject.user_id).where(
|
||
and_(
|
||
UserProject.tenant_id == tenant_id,
|
||
UserProject.project_id == int(task.project_id),
|
||
)
|
||
)
|
||
)
|
||
project_user_ids = {int(uid) for uid in project_user_ids_result.scalars().all()}
|
||
|
||
active_users_result = await db.execute(
|
||
select(User.id).where(
|
||
and_(
|
||
User.tenant_id == tenant_id,
|
||
User.status == "active",
|
||
User.is_active.is_(True),
|
||
)
|
||
)
|
||
)
|
||
active_user_ids = {int(uid) for uid in active_users_result.scalars().all()}
|
||
|
||
filtered = []
|
||
for item in recommendations.get("recommendations", []):
|
||
try:
|
||
uid = int(item.get("user_id"))
|
||
except (TypeError, ValueError):
|
||
continue
|
||
if uid in project_user_ids and uid in active_user_ids:
|
||
filtered.append(item)
|
||
|
||
filtered.sort(
|
||
key=lambda x: (
|
||
float(x.get("workload_rate", 9999)) >= 70.0,
|
||
float(x.get("workload_rate", 9999)),
|
||
-float(x.get("match_score", 0)),
|
||
)
|
||
)
|
||
|
||
final_items = filtered[:limit]
|
||
return {
|
||
"task_id": str(task.id),
|
||
"required_skills": required_skills,
|
||
"recommendations": final_items,
|
||
"total_count": len(final_items),
|
||
}
|