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)