1158 lines
46 KiB
Python
1158 lines
46 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import date as Date
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import ValidationError
|
|
from sqlalchemy import and_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ...core.database import get_db
|
|
from ...core.response import error_response, success_response
|
|
from ...dependencies import get_current_user
|
|
from ...models import Project, User, UserProject
|
|
from ...schemas.pmflow_os import (
|
|
PMFlowAIFeedbackCreateRequest,
|
|
PMFlowArtifactReviewActionRequest,
|
|
PMFlowArtifactReworkPackageRequest,
|
|
PMFlowCapacityOverridePayload,
|
|
PMFlowChangeItemDeferRequest,
|
|
PMFlowChangeItemEditRequest,
|
|
PMFlowChangeSetCommitRequest,
|
|
PMFlowExecutionHandoffConsumeRequest,
|
|
PMFlowExecutionHandoffRequest,
|
|
PMFlowExecutionAcceptanceDecisionRequest,
|
|
PMFlowExecutionRuntimeCredentialCreateRequest,
|
|
PMFlowExecutionMemoryWritebackRequest,
|
|
PMFlowExecutionProgressEventRequest,
|
|
PMFlowRuntimeExecutionPackageRequest,
|
|
PMFlowRuntimeExecutionResultRequest,
|
|
PMFlowExtractionCreateRequest,
|
|
PMFlowIterationRecommendationChangeSetDraftRequest,
|
|
PMFlowIterationRecommendationGroupChangeSetDraftRequest,
|
|
ProjectCopilotAskRequest,
|
|
PMFlowReleaseFeedbackIngestRequest,
|
|
PMFlowReleaseValidationRunCreateRequest,
|
|
PMFlowReviewDecisionRequest,
|
|
PMFlowThreadMessageCreateRequest,
|
|
)
|
|
from ...services.auth import has_any_role
|
|
from ...services.adaptive_capacity_service import AdaptiveCapacityService
|
|
from ...services.pmflow_ai_feedback_service import PMFlowAIFeedbackService
|
|
from ...services.pmflow_change_review_service import PMFlowChangeReviewError, PMFlowChangeReviewService
|
|
from ...services.pmflow_os_projection_service import PMFlowOSProjectionService
|
|
from ...services.project_copilot_service import ProjectCopilotService
|
|
from ...services.team_execution_capability_service import TeamExecutionCapabilityService
|
|
|
|
router = APIRouter(prefix="/pmflow-os", tags=["pmflow-os"])
|
|
|
|
|
|
def _review_error(exc: PMFlowChangeReviewError) -> JSONResponse:
|
|
return JSONResponse(status_code=exc.status_code, content=error_response(exc.status_code, f"{exc.code}: {exc.message}"))
|
|
|
|
|
|
async def _ensure_project_member_or_manager(
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
project_id: int,
|
|
current_user: User,
|
|
) -> None:
|
|
project_result = await db.execute(
|
|
select(Project.id).where(and_(Project.id == int(project_id), Project.tenant_id == int(tenant_id)))
|
|
)
|
|
if project_result.scalar_one_or_none() is None:
|
|
raise HTTPException(status_code=404, detail="project_not_found")
|
|
if has_any_role(str(getattr(current_user, "role", "") or ""), ["OWNER", "REVIEWER"]):
|
|
return
|
|
member_result = await db.execute(
|
|
select(UserProject.user_id).where(
|
|
and_(
|
|
UserProject.tenant_id == int(tenant_id),
|
|
UserProject.project_id == int(project_id),
|
|
UserProject.user_id == int(current_user.id),
|
|
)
|
|
)
|
|
)
|
|
if member_result.scalar_one_or_none() is None:
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
|
|
|
|
def _ensure_capacity_override_manager(current_user: User) -> None:
|
|
if not has_any_role(str(getattr(current_user, "role", "") or ""), ["OWNER", "REVIEWER"]):
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
|
|
|
|
def _parse_capacity_date(value: str | None) -> Date | None:
|
|
if not value:
|
|
return None
|
|
try:
|
|
return Date.fromisoformat(str(value)[:10])
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=422, detail="invalid_date") from exc
|
|
|
|
|
|
@router.get("/projects/{project_id}/truth-state")
|
|
async def get_pmflow_os_truth_state(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_truth_state(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.get("/projects/{project_id}/project-memory/overview")
|
|
async def get_pmflow_os_project_memory_overview(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_project_memory_overview(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.get("/projects/{project_id}/project-memory/items")
|
|
async def get_pmflow_os_project_memory_items(
|
|
project_id: int,
|
|
category: str | None = Query(default=None),
|
|
stage: str | None = Query(default=None),
|
|
source_type: str | None = Query(default=None),
|
|
status: str | None = Query(default=None),
|
|
query: str | None = Query(default=None),
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_project_memory_items(
|
|
project_id=int(project_id),
|
|
category=category,
|
|
stage=stage,
|
|
source_type=source_type,
|
|
status=status,
|
|
query=query,
|
|
limit=int(limit),
|
|
offset=int(offset),
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.get("/projects/{project_id}/project-memory/items/{item_id}")
|
|
async def get_pmflow_os_project_memory_item(
|
|
project_id: int,
|
|
item_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_project_memory_item(project_id=int(project_id), item_id=item_id)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
if str(exc) == "project_memory_item_not_found":
|
|
raise HTTPException(status_code=404, detail="project_memory_item_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.get("/projects/{project_id}/project-memory/assets")
|
|
async def get_pmflow_os_project_memory_assets(
|
|
project_id: int,
|
|
asset_type: str | None = Query(default=None),
|
|
source_type: str | None = Query(default=None),
|
|
status: str | None = Query(default=None),
|
|
query: str | None = Query(default=None),
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_project_memory_assets(
|
|
project_id=int(project_id),
|
|
asset_type=asset_type,
|
|
source_type=source_type,
|
|
status=status,
|
|
query=query,
|
|
limit=int(limit),
|
|
offset=int(offset),
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.get("/projects/{project_id}/project-memory/context-packs")
|
|
async def get_pmflow_os_project_memory_context_packs(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_project_memory_context_packs(project_id=int(project_id))
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.get("/projects/{project_id}/project-memory/writebacks")
|
|
async def get_pmflow_os_project_memory_writebacks(
|
|
project_id: int,
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_project_memory_writebacks(project_id=int(project_id), limit=int(limit), offset=int(offset))
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/projects/{project_id}/ai-feedback")
|
|
async def create_pmflow_os_ai_feedback(
|
|
project_id: int,
|
|
body: PMFlowAIFeedbackCreateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowAIFeedbackService(db, tenant_id=tenant_id)
|
|
payload = await service.create_or_update(project_id=int(project_id), body=body, user_id=int(current_user.id))
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/project-copilot/ask")
|
|
async def ask_pmflow_os_project_copilot(
|
|
project_id: int,
|
|
body: ProjectCopilotAskRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = ProjectCopilotService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.ask(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
body=body,
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/threads/{thread_id}/messages")
|
|
async def create_pmflow_os_thread_message(
|
|
project_id: int,
|
|
thread_id: str,
|
|
body: PMFlowThreadMessageCreateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowChangeReviewService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_thread_message(
|
|
project_id=int(project_id),
|
|
thread_id=thread_id,
|
|
body=body,
|
|
user_id=int(current_user.id),
|
|
)
|
|
except PMFlowChangeReviewError as exc:
|
|
return _review_error(exc)
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/projects/{project_id}/extractions")
|
|
async def create_pmflow_os_extraction(
|
|
project_id: int,
|
|
body: PMFlowExtractionCreateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowChangeReviewService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_extraction(project_id=int(project_id), body=body, user_id=int(current_user.id))
|
|
except PMFlowChangeReviewError as exc:
|
|
return _review_error(exc)
|
|
return success_response(payload)
|
|
|
|
|
|
@router.get("/projects/{project_id}/change-sets/{change_set_id}")
|
|
async def get_pmflow_os_change_set(
|
|
project_id: int,
|
|
change_set_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=int(project_id), current_user=current_user)
|
|
service = PMFlowChangeReviewService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_change_set_detail(project_id=int(project_id), change_set_id=int(change_set_id))
|
|
except PMFlowChangeReviewError as exc:
|
|
return _review_error(exc)
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/change-items/{change_item_id}/accept")
|
|
async def accept_pmflow_os_change_item(
|
|
change_item_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
service = PMFlowChangeReviewService(db, tenant_id=tenant_id)
|
|
project_id = await service.get_item_project_id(change_item_id=int(change_item_id))
|
|
if project_id is None:
|
|
return JSONResponse(status_code=404, content=error_response(404, "change_item_not_found"))
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=project_id, current_user=current_user)
|
|
try:
|
|
payload = await service.accept_item(change_item_id=int(change_item_id), user_id=int(current_user.id))
|
|
except PMFlowChangeReviewError as exc:
|
|
return _review_error(exc)
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/change-items/{change_item_id}/edit")
|
|
async def edit_pmflow_os_change_item(
|
|
change_item_id: int,
|
|
body: PMFlowChangeItemEditRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
service = PMFlowChangeReviewService(db, tenant_id=tenant_id)
|
|
project_id = await service.get_item_project_id(change_item_id=int(change_item_id))
|
|
if project_id is None:
|
|
return JSONResponse(status_code=404, content=error_response(404, "change_item_not_found"))
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=project_id, current_user=current_user)
|
|
try:
|
|
payload = await service.edit_item(change_item_id=int(change_item_id), body=body, user_id=int(current_user.id))
|
|
except PMFlowChangeReviewError as exc:
|
|
return _review_error(exc)
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/change-items/{change_item_id}/defer")
|
|
async def defer_pmflow_os_change_item(
|
|
change_item_id: int,
|
|
body: PMFlowChangeItemDeferRequest | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
service = PMFlowChangeReviewService(db, tenant_id=tenant_id)
|
|
project_id = await service.get_item_project_id(change_item_id=int(change_item_id))
|
|
if project_id is None:
|
|
return JSONResponse(status_code=404, content=error_response(404, "change_item_not_found"))
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=project_id, current_user=current_user)
|
|
try:
|
|
payload = await service.defer_item(
|
|
change_item_id=int(change_item_id),
|
|
user_id=int(current_user.id),
|
|
reason=body.reason if body is not None else None,
|
|
)
|
|
except PMFlowChangeReviewError as exc:
|
|
return _review_error(exc)
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/change-sets/{change_set_id}/commit")
|
|
async def commit_pmflow_os_change_set(
|
|
change_set_id: int,
|
|
body: PMFlowChangeSetCommitRequest | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
service = PMFlowChangeReviewService(db, tenant_id=tenant_id)
|
|
project_id = await service.get_change_set_project_id(change_set_id=int(change_set_id))
|
|
if project_id is None:
|
|
return JSONResponse(status_code=404, content=error_response(404, "changeset_not_found"))
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=project_id, current_user=current_user)
|
|
try:
|
|
payload = await service.commit_change_set(
|
|
change_set_id=int(change_set_id),
|
|
body=body or PMFlowChangeSetCommitRequest(),
|
|
user_id=int(current_user.id),
|
|
)
|
|
except PMFlowChangeReviewError as exc:
|
|
return _review_error(exc)
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/change-sets/{change_set_id}/review-decisions")
|
|
async def create_pmflow_os_review_decision(
|
|
change_set_id: int,
|
|
body: PMFlowReviewDecisionRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
review_service = PMFlowChangeReviewService(db, tenant_id=tenant_id)
|
|
project_id = await review_service.get_change_set_project_id(change_set_id=int(change_set_id))
|
|
if project_id is None:
|
|
return JSONResponse(status_code=404, content=error_response(404, "changeset_not_found"))
|
|
await _ensure_project_member_or_manager(db, tenant_id=tenant_id, project_id=project_id, current_user=current_user)
|
|
try:
|
|
review_payload = await review_service.record_review_decision(
|
|
change_set_id=int(change_set_id),
|
|
body=body,
|
|
user_id=int(current_user.id),
|
|
)
|
|
truth_state = await PMFlowOSProjectionService(db, tenant_id=tenant_id).get_truth_state(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except PMFlowChangeReviewError as exc:
|
|
return _review_error(exc)
|
|
return success_response(
|
|
{
|
|
"changeSetDetail": review_payload["changeSetDetail"],
|
|
"truthState": truth_state.model_dump(mode="json"),
|
|
"warnings": review_payload.get("warnings", []),
|
|
}
|
|
)
|
|
|
|
|
|
@router.get("/projects/{project_id}/execution-summary")
|
|
async def get_pmflow_os_execution_summary(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_execution_summary(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.get("/projects/{project_id}/execution-capabilities")
|
|
async def get_pmflow_os_execution_capabilities(
|
|
project_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = TeamExecutionCapabilityService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_snapshot(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.get("/projects/{project_id}/capacity-plan")
|
|
async def get_pmflow_os_capacity_plan(
|
|
project_id: int,
|
|
plan_date: str | None = Query(default=None, alias="date"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = AdaptiveCapacityService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.get_plan(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
plan_date=_parse_capacity_date(plan_date),
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/capacity-overrides/simulate")
|
|
async def simulate_pmflow_os_capacity_override(
|
|
project_id: int,
|
|
body: PMFlowCapacityOverridePayload,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
_ensure_capacity_override_manager(current_user)
|
|
service = AdaptiveCapacityService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.simulate_override(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
payload=body,
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "capacity_member_not_found":
|
|
raise HTTPException(status_code=404, detail="capacity_member_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/capacity-overrides")
|
|
async def create_pmflow_os_capacity_override(
|
|
project_id: int,
|
|
body: PMFlowCapacityOverridePayload,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
_ensure_capacity_override_manager(current_user)
|
|
service = AdaptiveCapacityService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.apply_override(
|
|
project_id=int(project_id),
|
|
current_user_id=int(current_user.id),
|
|
payload=body,
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "capacity_member_not_found":
|
|
raise HTTPException(status_code=404, detail="capacity_member_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.get("/projects/{project_id}/capacity-overrides")
|
|
async def list_pmflow_os_capacity_overrides(
|
|
project_id: int,
|
|
plan_date: str | None = Query(default=None, alias="date"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = AdaptiveCapacityService(db, tenant_id=tenant_id)
|
|
payload = await service.list_overrides(project_id=int(project_id), plan_date=_parse_capacity_date(plan_date))
|
|
return success_response([item.model_dump(mode="json") for item in payload])
|
|
|
|
|
|
@router.post("/projects/{project_id}/execution-handoffs")
|
|
async def create_pmflow_os_execution_handoff(
|
|
project_id: int,
|
|
body: PMFlowExecutionHandoffRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_execution_handoff(
|
|
project_id=int(project_id),
|
|
body=body,
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "task_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "task_not_found"))
|
|
if code == "repo_binding_not_found":
|
|
return JSONResponse(status_code=422, content=error_response(422, "repo_binding_not_found"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/execution-progress-events")
|
|
async def create_pmflow_os_execution_progress_event(
|
|
project_id: int,
|
|
body: PMFlowExecutionProgressEventRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.record_execution_progress_event(
|
|
project_id=int(project_id),
|
|
body=body,
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "task_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "task_not_found"))
|
|
if code == "repo_binding_not_found":
|
|
return JSONResponse(status_code=422, content=error_response(422, "repo_binding_not_found"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/execution-artifact-review-actions")
|
|
async def create_pmflow_os_execution_artifact_review_action(
|
|
project_id: int,
|
|
body: PMFlowArtifactReviewActionRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.record_execution_artifact_review_action(
|
|
project_id=int(project_id),
|
|
body=body,
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "task_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "task_not_found"))
|
|
if code == "invalid_artifact_review_item_ids":
|
|
return JSONResponse(status_code=422, content=error_response(422, "invalid_artifact_review_item_ids"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/execution-artifact-rework-packages")
|
|
async def create_pmflow_os_execution_artifact_rework_package(
|
|
project_id: int,
|
|
body: PMFlowArtifactReworkPackageRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_execution_artifact_rework_package(
|
|
project_id=int(project_id),
|
|
body=body,
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "task_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "task_not_found"))
|
|
if code == "repo_binding_not_found":
|
|
return JSONResponse(status_code=422, content=error_response(422, "repo_binding_not_found"))
|
|
if code == "invalid_artifact_review_item_ids":
|
|
return JSONResponse(status_code=422, content=error_response(422, "invalid_artifact_review_item_ids"))
|
|
if code == "artifact_review_empty":
|
|
return JSONResponse(status_code=409, content=error_response(409, "artifact_review_empty"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/execution-handoffs/{prompt_package_id}/runtime-credential")
|
|
async def create_pmflow_os_execution_runtime_credential(
|
|
project_id: int,
|
|
prompt_package_id: int,
|
|
body: PMFlowExecutionRuntimeCredentialCreateRequest | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_execution_runtime_credential(
|
|
project_id=int(project_id),
|
|
prompt_package_id=int(prompt_package_id),
|
|
body=body or PMFlowExecutionRuntimeCredentialCreateRequest(),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code in {"prompt_package_not_found", "execution_handoff_not_found", "task_not_found"}:
|
|
return JSONResponse(status_code=404, content=error_response(404, code))
|
|
if code in {"repo_binding_not_found", "runtime_target_invalid"}:
|
|
return JSONResponse(status_code=422, content=error_response(422, code))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/runtime/execution-results")
|
|
async def create_pmflow_os_runtime_execution_result(
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
raw_body = await request.body()
|
|
key_id = request.headers.get("X-PMFlow-Key-Id") or ""
|
|
timestamp = request.headers.get("X-PMFlow-Timestamp") or ""
|
|
signature = request.headers.get("X-PMFlow-Signature") or ""
|
|
delivery_id = request.headers.get("X-PMFlow-Delivery-Id") or ""
|
|
try:
|
|
raw_payload = json.loads(raw_body.decode("utf-8"))
|
|
body = PMFlowRuntimeExecutionResultRequest.model_validate(raw_payload)
|
|
except (json.JSONDecodeError, UnicodeDecodeError, ValidationError):
|
|
return JSONResponse(status_code=422, content=error_response(422, "runtime_payload_invalid"))
|
|
service = PMFlowOSProjectionService(db, tenant_id=0)
|
|
try:
|
|
payload = await service.record_runtime_execution_result(
|
|
body=body,
|
|
key_id=key_id,
|
|
timestamp=timestamp,
|
|
signature=signature,
|
|
delivery_id=delivery_id,
|
|
raw_body=raw_body,
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code in {"runtime_credential_missing", "task_not_found", "prompt_package_not_found"}:
|
|
return JSONResponse(status_code=404, content=error_response(404, code))
|
|
if code in {
|
|
"runtime_credential_expired",
|
|
"runtime_credential_revoked",
|
|
"runtime_credential_invalid",
|
|
"runtime_signature_invalid",
|
|
"runtime_timestamp_invalid",
|
|
"runtime_source_not_allowed",
|
|
"runtime_handoff_mismatch",
|
|
}:
|
|
return JSONResponse(status_code=401, content=error_response(401, code))
|
|
if code == "repo_binding_not_found":
|
|
return JSONResponse(status_code=422, content=error_response(422, "repo_binding_not_found"))
|
|
if code == "project_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "project_not_found"))
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/runtime/execution-packages")
|
|
async def create_pmflow_os_runtime_execution_package(
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
raw_body = await request.body()
|
|
key_id = request.headers.get("X-PMFlow-Key-Id") or ""
|
|
timestamp = request.headers.get("X-PMFlow-Timestamp") or ""
|
|
signature = request.headers.get("X-PMFlow-Signature") or ""
|
|
delivery_id = request.headers.get("X-PMFlow-Delivery-Id") or ""
|
|
try:
|
|
raw_payload = json.loads(raw_body.decode("utf-8"))
|
|
body = PMFlowRuntimeExecutionPackageRequest.model_validate(raw_payload)
|
|
except (json.JSONDecodeError, UnicodeDecodeError, ValidationError):
|
|
return JSONResponse(status_code=422, content=error_response(422, "runtime_package_payload_invalid"))
|
|
service = PMFlowOSProjectionService(db, tenant_id=0)
|
|
try:
|
|
payload = await service.record_runtime_execution_package_pull(
|
|
body=body,
|
|
key_id=key_id,
|
|
timestamp=timestamp,
|
|
signature=signature,
|
|
delivery_id=delivery_id,
|
|
raw_body=raw_body,
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code in {"runtime_credential_missing", "task_not_found", "prompt_package_not_found"}:
|
|
return JSONResponse(status_code=404, content=error_response(404, code))
|
|
if code in {
|
|
"runtime_credential_expired",
|
|
"runtime_credential_revoked",
|
|
"runtime_credential_invalid",
|
|
"runtime_signature_invalid",
|
|
"runtime_timestamp_invalid",
|
|
"runtime_source_not_allowed",
|
|
"runtime_handoff_mismatch",
|
|
}:
|
|
return JSONResponse(status_code=401, content=error_response(401, code))
|
|
if code == "repo_binding_not_found":
|
|
return JSONResponse(status_code=422, content=error_response(422, "repo_binding_not_found"))
|
|
if code == "project_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "project_not_found"))
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/execution-acceptance-decisions")
|
|
async def create_pmflow_os_execution_acceptance_decision(
|
|
project_id: int,
|
|
body: PMFlowExecutionAcceptanceDecisionRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.record_execution_acceptance_decision(
|
|
project_id=int(project_id),
|
|
body=body,
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "task_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "task_not_found"))
|
|
if code == "acceptance_test_evidence_required":
|
|
return JSONResponse(status_code=409, content=error_response(409, "acceptance_test_evidence_required"))
|
|
if code == "technical_review_required":
|
|
return JSONResponse(status_code=409, content=error_response(409, "technical_review_required"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|
|
|
|
|
|
@router.post("/projects/{project_id}/execution-memory-writebacks")
|
|
async def create_pmflow_os_execution_memory_writeback(
|
|
project_id: int,
|
|
body: PMFlowExecutionMemoryWritebackRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.record_execution_memory_writeback(
|
|
project_id=int(project_id),
|
|
body=body,
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "task_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "task_not_found"))
|
|
if code in {
|
|
"execution_acceptance_required",
|
|
"execution_evidence_required",
|
|
"writeback_already_exists",
|
|
"unresolved_blocker_exists",
|
|
}:
|
|
return JSONResponse(status_code=409, content=error_response(409, code))
|
|
if code == "invalid_progress_event_ids":
|
|
return JSONResponse(status_code=422, content=error_response(422, "invalid_progress_event_ids"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/projects/{project_id}/release-validation-runs")
|
|
async def create_pmflow_os_release_validation_run(
|
|
project_id: int,
|
|
body: PMFlowReleaseValidationRunCreateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_release_validation_run(
|
|
project_id=int(project_id),
|
|
body=body,
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "release_validation_evidence_empty":
|
|
return JSONResponse(status_code=409, content=error_response(409, "release_validation_evidence_empty"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/projects/{project_id}/release-feedback-ingests")
|
|
async def create_pmflow_os_release_feedback_ingest(
|
|
project_id: int,
|
|
body: PMFlowReleaseFeedbackIngestRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_release_feedback_ingest(
|
|
project_id=int(project_id),
|
|
body=body,
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code in {"feedback_content_empty", "反馈内容不能为空"}:
|
|
return JSONResponse(status_code=400, content=error_response(400, "feedback_content_empty"))
|
|
if code == "charter_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "charter_not_found"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/projects/{project_id}/iteration-recommendations/{recommendation_id}/change-set-drafts")
|
|
async def create_pmflow_os_iteration_recommendation_change_set_draft(
|
|
project_id: int,
|
|
recommendation_id: int,
|
|
body: PMFlowIterationRecommendationChangeSetDraftRequest | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_iteration_recommendation_change_set_draft(
|
|
project_id=int(project_id),
|
|
recommendation_id=int(recommendation_id),
|
|
body=body or PMFlowIterationRecommendationChangeSetDraftRequest(),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "iteration_recommendation_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "iteration_recommendation_not_found"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/projects/{project_id}/iteration-recommendation-groups/{group_key}/change-set-drafts")
|
|
async def create_pmflow_os_iteration_recommendation_group_change_set_draft(
|
|
project_id: int,
|
|
group_key: str,
|
|
body: PMFlowIterationRecommendationGroupChangeSetDraftRequest | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.create_iteration_recommendation_group_change_set_draft(
|
|
project_id=int(project_id),
|
|
group_key=group_key,
|
|
body=body or PMFlowIterationRecommendationGroupChangeSetDraftRequest(),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code == "iteration_recommendation_group_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "iteration_recommendation_group_not_found"))
|
|
if code == "iteration_recommendation_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "iteration_recommendation_not_found"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload)
|
|
|
|
|
|
@router.post("/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume")
|
|
async def consume_pmflow_os_execution_handoff(
|
|
project_id: int,
|
|
prompt_package_id: int,
|
|
body: PMFlowExecutionHandoffConsumeRequest | None = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
tenant_id = int(current_user.tenant_id)
|
|
await _ensure_project_member_or_manager(
|
|
db,
|
|
tenant_id=tenant_id,
|
|
project_id=int(project_id),
|
|
current_user=current_user,
|
|
)
|
|
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
|
|
try:
|
|
payload = await service.consume_execution_handoff(
|
|
project_id=int(project_id),
|
|
prompt_package_id=int(prompt_package_id),
|
|
body=body or PMFlowExecutionHandoffConsumeRequest(),
|
|
current_user_id=int(current_user.id),
|
|
)
|
|
except ValueError as exc:
|
|
code = str(exc)
|
|
if code in {"prompt_package_not_found", "execution_handoff_not_found"}:
|
|
return JSONResponse(status_code=404, content=error_response(404, code))
|
|
if code == "task_not_found":
|
|
return JSONResponse(status_code=404, content=error_response(404, "task_not_found"))
|
|
if code == "repo_binding_not_found":
|
|
return JSONResponse(status_code=422, content=error_response(422, "repo_binding_not_found"))
|
|
if code == "project_not_found":
|
|
raise HTTPException(status_code=404, detail="project_not_found") from exc
|
|
raise
|
|
return success_response(payload.model_dump(mode="json"))
|