mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* [f8480831] refactor(api): extract route-layer helpers into services/schemas/utils (batch B) Moves 28 non-@router-decorated helper functions out of 15 route files (optimal, project, release, dashboard, pitch, x, docs, git, playbooks, product, provider, research, secretary, system, work_session) into their paired services module (DB/service-calling helpers), the route's schemas module as a converter (pure response/request shaping, mirroring the existing project_to_response/assignment_to_response pattern), or roboco/utils/converters.py (pure generic helpers). Adds two small shared role-check helpers to api/deps.py (require_auditor_or_ceo, require_role_in) for endpoint-specific role gates that had no existing home. Placement-only: no route paths, schemas, or observable behavior changed. Fixes the handful of tests that imported the old private helper names directly. * [f8480831] docs(map): document Batch B route-helper relocation in api-routes-schemas.md * [f8480831] docs(map): add Key Symbols rows for require_auditor_or_ceo/require_role_in --------- Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech>
116 lines
4.4 KiB
Python
116 lines
4.4 KiB
Python
"""Playbook curation API — the Auditor (or CEO) reviews drafts.
|
|
|
|
GET lists drafts (or approved); approve flips a draft to approved (+ indexes it);
|
|
reject archives it with a reason. Curation is gated to the Auditor and the CEO —
|
|
delivery roles DRAFT via the gateway verb but never curate.
|
|
"""
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, status
|
|
|
|
from roboco.api.deps import CurrentAgentContext, DbSession, require_auditor_or_ceo
|
|
from roboco.api.schemas.playbook import PlaybookRejectBody
|
|
from roboco.models.playbook import Playbook
|
|
from roboco.services.base import ConflictError, NotFoundError
|
|
from roboco.services.playbook import get_playbook_service
|
|
|
|
router = APIRouter()
|
|
|
|
_CURATOR_DETAIL = "Only the Auditor or CEO may curate playbooks"
|
|
|
|
|
|
@router.get("", response_model=list[Playbook])
|
|
async def list_playbooks(
|
|
db: DbSession,
|
|
agent: CurrentAgentContext,
|
|
status_filter: str = Query(default="draft", alias="status"),
|
|
) -> list[Playbook]:
|
|
"""List playbooks by status (default: drafts awaiting review)."""
|
|
require_auditor_or_ceo(agent.role, _CURATOR_DETAIL)
|
|
svc = get_playbook_service(db)
|
|
rows = (
|
|
await svc.list_approved()
|
|
if status_filter == "approved"
|
|
else await svc.list_drafts()
|
|
)
|
|
return [Playbook.model_validate(row) for row in rows]
|
|
|
|
|
|
@router.post("/{playbook_id}/approve", response_model=Playbook)
|
|
async def approve_playbook(
|
|
playbook_id: UUID, db: DbSession, agent: CurrentAgentContext
|
|
) -> Playbook:
|
|
"""Approve a draft playbook → approved (and indexed into the KB)."""
|
|
require_auditor_or_ceo(agent.role, _CURATOR_DETAIL)
|
|
try:
|
|
svc = get_playbook_service(db)
|
|
playbook = await svc.approve(playbook_id, approver_id=agent.agent_id)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Playbook not found"
|
|
) from exc
|
|
except ConflictError as exc:
|
|
# The playbook is not a draft (already approved/archived) — the
|
|
# curation is already finished, not a missing resource.
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
|
) from exc
|
|
# Commit the status change BEFORE indexing: the RAG index write runs through
|
|
# its own auto-committing connection, so indexing before commit would durably
|
|
# land an approved playbook in the corpus even if this commit rolled back.
|
|
await db.commit()
|
|
await svc.index_approved(playbook)
|
|
return Playbook.model_validate(playbook)
|
|
|
|
|
|
@router.post("/{playbook_id}/reject", response_model=Playbook)
|
|
async def reject_playbook(
|
|
playbook_id: UUID,
|
|
body: PlaybookRejectBody,
|
|
db: DbSession,
|
|
agent: CurrentAgentContext,
|
|
) -> Playbook:
|
|
"""Reject a draft playbook → archived, with the Auditor's reason."""
|
|
require_auditor_or_ceo(agent.role, _CURATOR_DETAIL)
|
|
try:
|
|
svc = get_playbook_service(db)
|
|
playbook = await svc.reject(
|
|
playbook_id, approver_id=agent.agent_id, reason=body.reason
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Playbook not found"
|
|
) from exc
|
|
except ConflictError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
|
) from exc
|
|
# Commit the status change BEFORE de-indexing (see approve_playbook).
|
|
await db.commit()
|
|
await svc.unindex_playbook(playbook)
|
|
return Playbook.model_validate(playbook)
|
|
|
|
|
|
@router.post("/{playbook_id}/archive", response_model=Playbook)
|
|
async def archive_playbook(
|
|
playbook_id: UUID, db: DbSession, agent: CurrentAgentContext
|
|
) -> Playbook:
|
|
"""Retire an approved playbook → archived (and de-indexed from the KB)."""
|
|
require_auditor_or_ceo(agent.role, _CURATOR_DETAIL)
|
|
try:
|
|
svc = get_playbook_service(db)
|
|
playbook = await svc.archive(playbook_id, approver_id=agent.agent_id)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Playbook not found"
|
|
) from exc
|
|
except ConflictError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
|
) from exc
|
|
# Commit the status change BEFORE de-indexing (see approve_playbook).
|
|
await db.commit()
|
|
await svc.unindex_playbook(playbook)
|
|
return Playbook.model_validate(playbook)
|