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

290 lines
11 KiB
Python

from __future__ import annotations
from .shared import *
from ...services.ai_project_control_service import AIProjectControlService
from ...services.sstc.entity_lookup import _ensure_charter, _ensure_project
from ...services.sstc.signal_core import _parse_node_id, _parse_project_id
router = APIRouter(tags=["sstc"])
class ModelBindingRequest(BaseModel):
catalog_item_id: int
charter_id: int | None = None
estimated_daily_tokens: int = 0
selection_reason: dict[str, Any] | None = None
class PromptPackageCreateRequest(BaseModel):
task_id: int | None = None
class ReleaseValidationRunRequest(BaseModel):
charter_id: int | None = None
release_node_id: int | None = None
class FeedbackIngestRequest(BaseModel):
charter_id: int | None = None
source_type: str = "manual"
title: str | None = None
content: str
metrics: dict[str, Any] | None = None
samples: list[dict[str, Any]] | None = None
class ComplianceCheckRunRequest(BaseModel):
charter_id: int | None = None
release_node_id: int | None = None
class RevenueEventCreateRequest(BaseModel):
source_type: str = "manual"
category: str = "other"
currency: str = "USD"
amount_usd: float
external_ref: str | None = None
event_payload: dict[str, Any] | None = None
occurred_at: str | None = None
@router.get("/projects/{project_id}/model-recommendations")
async def get_model_recommendations(
project_id: str,
charter_id: int | None = Query(default=None),
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)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
if charter_id is not None:
await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=int(charter_id))
control = AIProjectControlService(db, tenant_id=tenant_id)
return success_response(await control.recommend_models(project_id=pid, charter_id=charter_id))
@router.post("/projects/{project_id}/model-binding")
async def bind_project_model(
project_id: str,
body: ModelBindingRequest,
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)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
if body.charter_id is not None:
await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=int(body.charter_id))
control = AIProjectControlService(db, tenant_id=tenant_id)
try:
payload = await control.bind_model(
project_id=pid,
catalog_item_id=int(body.catalog_item_id),
selected_by_user_id=int(current_user.id),
charter_id=int(body.charter_id) if body.charter_id is not None else None,
estimated_daily_tokens=max(int(body.estimated_daily_tokens or 0), 0),
selection_reason=dict(body.selection_reason or {}),
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return success_response(payload)
@router.get("/projects/{project_id}/model-usage")
async def get_project_model_usage(
project_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
pid = _parse_project_id(project_id)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
control = AIProjectControlService(db, tenant_id=tenant_id)
return success_response(await control.get_model_usage(project_id=pid))
@router.post("/projects/{project_id}/charters/{charter_id}/prompt-packages")
async def create_prompt_package(
project_id: str,
charter_id: str,
body: PromptPackageCreateRequest,
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)
await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=cid)
control = AIProjectControlService(db, tenant_id=tenant_id)
try:
payload = await control.create_prompt_package(
project_id=pid,
charter_id=cid,
generated_by_user_id=int(current_user.id),
task_id=int(body.task_id) if body.task_id is not None else None,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return success_response(payload)
@router.post("/projects/{project_id}/release-validations/run")
async def run_release_validation(
project_id: str,
body: ReleaseValidationRunRequest,
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)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
if body.charter_id is not None:
await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=int(body.charter_id))
control = AIProjectControlService(db, tenant_id=tenant_id)
payload = await control.run_release_validation(
project_id=pid,
actor_user_id=int(current_user.id),
charter_id=int(body.charter_id) if body.charter_id is not None else None,
release_node_id=int(body.release_node_id) if body.release_node_id is not None else None,
)
return success_response(payload)
@router.post("/projects/{project_id}/feedback-ingest")
async def ingest_project_feedback(
project_id: str,
body: FeedbackIngestRequest,
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)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
if body.charter_id is not None:
await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=int(body.charter_id))
control = AIProjectControlService(db, tenant_id=tenant_id)
try:
payload = await control.ingest_feedback(
project_id=pid,
actor_user_id=int(current_user.id),
charter_id=int(body.charter_id) if body.charter_id is not None else None,
source_type=body.source_type,
title=body.title,
content=body.content,
metrics=dict(body.metrics or {}),
samples=list(body.samples or []),
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return success_response(payload)
@router.post("/projects/{project_id}/revenue-events")
async def create_revenue_event(
project_id: str,
body: RevenueEventCreateRequest,
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)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
control = AIProjectControlService(db, tenant_id=tenant_id)
payload = await control.record_revenue_event(
project_id=pid,
amount_usd=float(body.amount_usd or 0.0),
source_type=body.source_type,
category=body.category,
currency=body.currency,
external_ref=body.external_ref,
event_payload=dict(body.event_payload or {}),
occurred_at=body.occurred_at,
)
return success_response(payload)
@router.get("/projects/{project_id}/roi-snapshots")
async def get_roi_snapshots(
project_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
pid = _parse_project_id(project_id)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
control = AIProjectControlService(db, tenant_id=tenant_id)
return success_response(await control.get_roi_snapshots(project_id=pid))
@router.post("/projects/{project_id}/compliance-checks/run")
async def run_compliance_check(
project_id: str,
body: ComplianceCheckRunRequest,
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)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
if body.charter_id is not None:
await _ensure_charter(db, tenant_id=tenant_id, project_id=pid, charter_id=int(body.charter_id))
control = AIProjectControlService(db, tenant_id=tenant_id)
payload = await control.run_compliance_check(
project_id=pid,
actor_user_id=int(current_user.id),
charter_id=int(body.charter_id) if body.charter_id is not None else None,
release_node_id=int(body.release_node_id) if body.release_node_id is not None else None,
)
return success_response(payload)
@router.get("/projects/{project_id}/compliance-checks/latest/export")
async def export_latest_compliance_evidence(
project_id: str,
format: str = Query(default="json"),
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)
tenant_id = int(current_user.tenant_id)
svc = SSTCService(db, tenant_id=tenant_id)
await _ensure_project(svc, pid)
control = AIProjectControlService(db, tenant_id=tenant_id)
try:
payload = await control.export_latest_compliance_evidence(project_id=pid, export_format=format)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return success_response(payload)