PMFlow/backend/app/api/sstc_api/charters_ai_alignment.py

271 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from .shared import *
from ...services.sstc.charter_prefill_builder import _build_ai_first_charter_content
from ...services.sstc.charter_prefill_utils import _has_complete_module_data, _merge_prefill_content
from ...services.sstc.charter_seed_utils import _extract_charter_signal_ids
from ...services.sstc.entity_lookup import _ensure_charter, _ensure_project
from ...services.sstc.signal_core import _as_dict, _parse_node_id, _parse_project_id, _sync_pmflow_rag_async
router = APIRouter(tags=["sstc"])
class CharterAIPrefillRequest(BaseModel):
force: bool = False
raw_text: Optional[str] = None
@router.post("/projects/{project_id}/charters/{charter_id}/ai-prefill")
async def ai_prefill_charter(
project_id: str,
charter_id: str,
body: CharterAIPrefillRequest = Body(default_factory=CharterAIPrefillRequest),
permission_user: User = Depends(require_project_member_or_manager),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
_ = permission_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)
before_content = _as_dict(charter.content)
before_status = str(charter.status or "")
if not body.force and _has_complete_module_data(before_content):
return success_response(
{
"id": str(charter.id),
"title": charter.title,
"content": before_content,
"status": charter.status,
"version": charter.version,
"prefilled": False,
"skipped": True,
}
)
signal_ids = _extract_charter_signal_ids(before_content, charter.signal_id)
signal_rows: list[Signal] = []
if signal_ids:
signal_rows = (
await db.execute(
select(Signal).where(
and_(
Signal.tenant_id == tenant_id,
Signal.id.in_(signal_ids),
)
)
)
).scalars().all()
raw_text = str(body.raw_text or before_content.get("raw_source_text") or "").strip()
if not signal_rows and not raw_text:
raise HTTPException(status_code=400, detail="未找到可用于预填的信号源或原始文本")
generated = await _build_ai_first_charter_content(
db=db,
tenant_id=tenant_id,
project_id=pid,
signal_rows=signal_rows,
signal_ids=signal_ids,
raw_text=raw_text,
)
merged = _merge_prefill_content(
before_content, generated, overwrite_existing=bool(body.force)
)
merged["signal_ids"] = signal_ids
if raw_text:
merged["raw_source_text"] = raw_text
charter.content = merged
charter.status = "draft"
await log_operation(
db,
tenant_id=tenant_id,
user_id=int(current_user.id),
project_id=pid,
entity_type="charter",
entity_id=str(cid),
action="ai_prefill",
before_data={"content": before_content, "status": before_status},
after_data={"content": merged, "status": "draft"},
)
await db.commit()
await db.refresh(charter)
_sync_pmflow_rag_async(
tenant_id=tenant_id,
record_type="charter",
record_id=int(charter.id),
project_id=pid,
title=charter.title,
content=_as_dict(charter.content),
)
return success_response(
{
"id": str(charter.id),
"title": charter.title,
"content": _as_dict(charter.content),
"status": charter.status,
"version": charter.version,
"prefilled": True,
"skipped": False,
}
)
@router.post("/projects/{project_id}/charters/{charter_id}/alignment/start")
async def start_alignment(
project_id: str,
charter_id: str,
body: AlignmentStartRequest,
permission_user: User = Depends(require_project_member_or_manager),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
_ = permission_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)
before = {"status": charter.status}
charter.status = "aligning"
await log_operation(
db,
tenant_id=tenant_id,
user_id=int(current_user.id),
project_id=pid,
entity_type="charter",
entity_id=str(cid),
action="start_alignment",
before_data=before,
after_data={"status": charter.status, "note": body.note},
)
await db.commit()
return success_response({"id": str(cid), "status": charter.status})
@router.post("/projects/{project_id}/charters/{charter_id}/alignment/confirm")
async def confirm_alignment(
project_id: str,
charter_id: str,
body: AlignmentConfirmRequest,
permission_user: User = Depends(require_project_member_or_manager),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
_ = permission_user
pid = _parse_project_id(project_id)
cid = _parse_node_id(charter_id, "charter_id")
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
charter = await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=cid)
charter_content = _as_dict(charter.content)
readiness = charter_content.get("readiness")
if not isinstance(readiness, dict):
readiness = {}
if body.confirmed and readiness.get("ready_for_confirm") is not True:
blocking_fields = [str(item or "").strip() for item in readiness.get("blocking_fields") or [] if str(item or "").strip()]
blocking_reasons = [str(item or "").strip() for item in readiness.get("blocking_reasons") or [] if str(item or "").strip()]
detail_parts = blocking_reasons + blocking_fields
raise HTTPException(
status_code=409,
detail="宪章未通过总确认校验" if not detail_parts else ";".join(detail_parts),
)
row_result = await db.execute(
select(AlignmentRecord).where(
and_(
AlignmentRecord.truth_node_id == cid,
AlignmentRecord.user_id == int(current_user.id),
AlignmentRecord.tenant_id == tenant_id,
)
)
)
row = row_result.scalar_one_or_none()
if row is None:
row = AlignmentRecord(
truth_node_id=cid,
user_id=int(current_user.id),
alignment_score=body.score,
is_confirmed=body.confirmed,
comments=body.comments,
tenant_id=tenant_id,
)
db.add(row)
else:
row.alignment_score = body.score
row.is_confirmed = body.confirmed
row.comments = body.comments
member_ids = await svc.get_project_member_ids(pid)
if not member_ids:
# 无项目成员时至少以当前操作者作为最小确认集合,避免状态永久卡在“对齐中”。
member_ids = [int(current_user.id)]
confirmed_map: set[int] = set()
rows_result = await db.execute(
select(AlignmentRecord).where(
and_(
AlignmentRecord.truth_node_id == cid,
AlignmentRecord.tenant_id == tenant_id,
AlignmentRecord.is_confirmed.is_(True),
)
)
)
for r in rows_result.scalars().all():
confirmed_map.add(int(r.user_id))
all_confirmed = set(member_ids).issubset(confirmed_map) if member_ids else False
if all_confirmed:
charter.status = "aligned"
elif (charter.status or "").lower() not in {"aligned"}:
charter.status = "aligning"
if all_confirmed:
content = _as_dict(charter.content)
signal_ids = [sid for sid in content.get("signal_ids", []) if isinstance(sid, int)]
if charter.signal_id and int(charter.signal_id) not in signal_ids:
signal_ids.append(int(charter.signal_id))
if signal_ids:
signal_rows = (
await db.execute(
select(Signal).where(
and_(
Signal.tenant_id == tenant_id,
Signal.id.in_(signal_ids),
)
)
)
).scalars().all()
for signal in signal_rows:
signal.status = "已关闭"
signal.processed = True
await log_operation(
db,
tenant_id=tenant_id,
user_id=int(current_user.id),
project_id=pid,
entity_type="alignment",
entity_id=f"{cid}:{current_user.id}",
action="confirm",
after_data={
"score": body.score,
"confirmed": body.confirmed,
"all_confirmed": all_confirmed,
"charter_status": charter.status,
},
)
await db.commit()
return success_response(
{
"charter_id": str(cid),
"charter_status": charter.status,
"all_confirmed": all_confirmed,
"confirmed_count": len(confirmed_map),
"member_count": len(member_ids),
}
)