156 lines
5.2 KiB
Python
156 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from .shared import *
|
|
|
|
from ...services.sstc.entity_lookup import _ensure_charter, _ensure_project
|
|
from ...services.sstc.governance import _assert_charter_aligned, _require_manager
|
|
from ...services.sstc.signal_core import _as_dict, _parse_node_id, _parse_project_id, _sync_pmflow_rag_async
|
|
|
|
router = APIRouter(tags=["sstc"])
|
|
|
|
_TITLE_NORMALIZE_RE = re.compile(r"[\s\-_]+")
|
|
|
|
|
|
def _task_title_key(value: str) -> str:
|
|
text = str(value or "").strip().lower()
|
|
return _TITLE_NORMALIZE_RE.sub("", text)
|
|
|
|
@router.post("/projects/{project_id}/charters/{charter_id}/tasks/ai-breakdown")
|
|
async def ai_breakdown_tasks(
|
|
project_id: str,
|
|
charter_id: str,
|
|
permission_user: User = Depends(require_project_member_or_manager),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
_ = permission_user
|
|
_require_manager(current_user)
|
|
pid = _parse_project_id(project_id)
|
|
cid = _parse_node_id(charter_id, "charter_id")
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project(SSTCService(db, tenant_id=tenant_id), pid)
|
|
charter = await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=cid)
|
|
_assert_charter_aligned(charter)
|
|
|
|
prompt = f"""请基于以下需求宪章拆解 5-8 条任务,返回 JSON 数组:
|
|
[
|
|
{{"task_name":"", "type":"backend/frontend/ai/devops", "estimated_hours": 4.0, "priority":"P0/P1/P2"}}
|
|
]
|
|
|
|
宪章标题:{charter.title}
|
|
宪章内容:{_as_dict(charter.content)}
|
|
"""
|
|
try:
|
|
rows = await llm_service.generate_task_breakdown_async(
|
|
prompt,
|
|
db=db,
|
|
tenant_id=tenant_id,
|
|
project_id=pid,
|
|
)
|
|
except RuntimeError as exc:
|
|
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"AI 拆解失败: {exc}") from exc
|
|
|
|
existing_task_title_result = await db.execute(
|
|
select(Task.title).where(
|
|
and_(
|
|
Task.truth_node_id == cid,
|
|
Task.project_id == pid,
|
|
Task.tenant_id == tenant_id,
|
|
)
|
|
)
|
|
)
|
|
existing_title_keys = {
|
|
_task_title_key(str(title))
|
|
for title in existing_task_title_result.scalars().all()
|
|
if str(title or "").strip()
|
|
}
|
|
seen_title_keys = set(existing_title_keys)
|
|
|
|
created = []
|
|
skipped_duplicates = 0
|
|
prompt_tokens = max(len(prompt) // 4, 1)
|
|
completion_tokens = 0
|
|
for item in rows or []:
|
|
title = compact_title((item.get("task_name") or item.get("title") or ""), max_len=32)
|
|
if not title:
|
|
continue
|
|
title_key = _task_title_key(title)
|
|
if not title_key:
|
|
continue
|
|
if title_key in seen_title_keys:
|
|
skipped_duplicates += 1
|
|
continue
|
|
seen_title_keys.add(title_key)
|
|
|
|
priority = str(item.get("priority") or "P1").lower()
|
|
mapped_priority = "high" if priority == "p0" else "medium" if priority == "p1" else "low"
|
|
task = await persist_task(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
truth_node_id=cid,
|
|
project_id=pid,
|
|
draft=build_task_create_draft(
|
|
title=title,
|
|
description=item.get("description"),
|
|
required_skill_tag_ids=[],
|
|
priority=mapped_priority,
|
|
status="todo",
|
|
estimated_hours=float(item.get("estimated_hours") or 0) or None,
|
|
),
|
|
actor_user_id=int(current_user.id),
|
|
action="task.create",
|
|
remark="sstc.ai_breakdown",
|
|
commit=False,
|
|
refresh_workload=False,
|
|
)
|
|
created.append(task)
|
|
completion_tokens += max(len(title) // 2, 1)
|
|
|
|
db.add(
|
|
AICallLog(
|
|
tenant_id=tenant_id,
|
|
user_id=int(current_user.id),
|
|
function_name="sstc_ai_breakdown",
|
|
prompt=prompt,
|
|
response=f"created_count={len(created)},skipped_duplicates={skipped_duplicates}",
|
|
tokens_used=prompt_tokens + completion_tokens,
|
|
cost=((prompt_tokens * 0.00015 + completion_tokens * 0.0006) / 1000),
|
|
status="success",
|
|
)
|
|
)
|
|
await log_operation(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
user_id=int(current_user.id),
|
|
project_id=pid,
|
|
entity_type="task",
|
|
entity_id=str(cid),
|
|
action="ai_breakdown",
|
|
after_data={"created_count": len(created), "skipped_duplicates": skipped_duplicates},
|
|
)
|
|
await db.commit()
|
|
for task in created:
|
|
_sync_pmflow_rag_async(
|
|
tenant_id=tenant_id,
|
|
record_type="task",
|
|
record_id=int(task.id),
|
|
project_id=pid,
|
|
title=task.title,
|
|
content=task.description or "",
|
|
)
|
|
await context_cache_service.invalidate_system_context(tenant_id=tenant_id)
|
|
return success_response(
|
|
{
|
|
"created_count": len(created),
|
|
"skipped_duplicates": skipped_duplicates,
|
|
"items": [
|
|
{"id": str(t.id), "title": t.title, "priority": t.priority, "status": t.status}
|
|
for t in created
|
|
],
|
|
}
|
|
)
|