You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
orquestrador/admin_app/api/routes/tools.py

608 lines
22 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from admin_app.api.dependencies import (
get_settings,
get_tool_management_service,
require_admin_permission,
)
from admin_app.api.schemas import (
AdminToolContractsResponse,
AdminToolDraftIntakeRequest,
AdminToolDraftIntakeResponse,
AdminToolDraftListResponse,
AdminToolGenerationPipelineResponse,
AdminToolGovernanceDecisionRequest,
AdminToolGovernanceTransitionResponse,
AdminToolManagementActionResponse,
AdminToolOverviewResponse,
AdminToolOptionalGovernanceDecisionRequest,
AdminToolPublicationListResponse,
AdminToolReviewDecisionRequest,
AdminToolReviewDetailResponse,
AdminToolReviewQueueResponse,
)
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.services import ToolManagementService
from shared.contracts import AdminPermission, StaffRole, role_has_permission
# API de intake (processo de captação e triagem inicial de demandas, requisitos ou solicitações antes do início efetivo do projeto), pipeline, review, publish, deactivate e rollback de tools.
router = APIRouter(prefix="/tools", tags=["tools"])
@router.get(
"/overview",
response_model=AdminToolOverviewResponse,
)
def tools_overview(
settings: AdminSettings = Depends(get_settings),
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
payload = service.build_overview_payload()
return AdminToolOverviewResponse(
service="orquestrador-admin",
mode=payload["mode"],
metrics=payload["metrics"],
workflow=payload["workflow"],
actions=_build_actions(settings, current_staff.role),
next_steps=payload["next_steps"],
)
@router.get(
"/contracts",
response_model=AdminToolContractsResponse,
)
def tool_contracts(
service: ToolManagementService = Depends(get_tool_management_service),
_current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
payload = service.build_contracts_payload()
return AdminToolContractsResponse(
service="orquestrador-admin",
publication_source_service=payload["publication_source_service"],
publication_target_service=payload["publication_target_service"],
lifecycle_statuses=payload["lifecycle_statuses"],
parameter_types=payload["parameter_types"],
publication_fields=payload["publication_fields"],
published_tool_fields=payload["published_tool_fields"],
)
@router.get(
"/drafts",
response_model=AdminToolDraftListResponse,
)
def tool_drafts(
service: ToolManagementService = Depends(get_tool_management_service),
_current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
payload = service.build_drafts_payload()
return AdminToolDraftListResponse(
service="orquestrador-admin",
storage_status=payload["storage_status"],
message=payload["message"],
drafts=payload["drafts"],
supported_statuses=payload["supported_statuses"],
)
@router.post(
"/drafts/intake",
response_model=AdminToolDraftIntakeResponse,
)
def tool_draft_intake(
draft: AdminToolDraftIntakeRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
try:
payload = service.create_draft_submission(
draft.model_dump(),
owner_staff_account_id=current_staff.id,
owner_name=current_staff.display_name,
owner_role=current_staff.role,
)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(exc),
) from exc
return AdminToolDraftIntakeResponse(
service="orquestrador-admin",
storage_status=payload["storage_status"],
message=payload["message"],
submission_policy=payload["submission_policy"],
draft_preview=payload["draft_preview"],
warnings=payload["warnings"],
next_steps=payload["next_steps"],
)
@router.post(
"/pipeline/{version_id}/run",
response_model=AdminToolGenerationPipelineResponse,
)
def tool_pipeline_run(
version_id: str,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
try:
payload = service.run_generation_pipeline_in_worker(
version_id,
runner_staff_account_id=current_staff.id,
runner_name=current_staff.display_name,
runner_role=current_staff.role,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_pipeline_response(payload)
@router.post(
"/drafts/{version_id}/authorize-generation",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_draft_authorize_generation(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.authorize_generation(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.post(
"/drafts/{version_id}/close",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_draft_close(
version_id: str,
decision: AdminToolOptionalGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.close_proposal(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.get(
"/review-queue",
response_model=AdminToolReviewQueueResponse,
)
def tool_review_queue(
service: ToolManagementService = Depends(get_tool_management_service),
_current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
payload = service.build_review_queue_payload()
return AdminToolReviewQueueResponse(
service="orquestrador-admin",
queue_mode=payload["queue_mode"],
message=payload["message"],
items=payload["items"],
supported_statuses=payload["supported_statuses"],
)
@router.get(
"/review-queue/{version_id}",
response_model=AdminToolReviewDetailResponse,
)
def tool_review_queue_detail(
version_id: str,
service: ToolManagementService = Depends(get_tool_management_service),
_current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.build_review_detail_payload(version_id)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
return _build_review_detail_response(payload)
@router.post(
"/review-queue/{version_id}/review",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_review_queue_review(
version_id: str,
decision: AdminToolReviewDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.review_version(
version_id,
reviewer_staff_account_id=current_staff.id,
reviewer_name=current_staff.display_name,
reviewer_role=current_staff.role,
decision_notes=decision.decision_notes,
reviewed_generated_code=decision.reviewed_generated_code,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.post(
"/review-queue/{version_id}/request-changes",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_review_queue_request_changes(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.request_changes(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.post(
"/review-queue/{version_id}/close",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_review_queue_close(
version_id: str,
decision: AdminToolOptionalGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.close_proposal(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.post(
"/review-queue/{version_id}/approve",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_review_queue_approve(
version_id: str,
decision: AdminToolReviewDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
try:
payload = service.approve_version(
version_id,
approver_staff_account_id=current_staff.id,
approver_name=current_staff.display_name,
approver_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.get(
"/publications",
response_model=AdminToolPublicationListResponse,
)
def tool_publications(
service: ToolManagementService = Depends(get_tool_management_service),
_current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
payload = service.build_publications_payload()
return AdminToolPublicationListResponse(
service="orquestrador-admin",
source=payload["source"],
target_service=payload["target_service"],
publications=payload["publications"],
)
@router.post(
"/publications/{version_id}/publish",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_publications_publish(
version_id: str,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
try:
payload = service.publish_version(
version_id,
publisher_staff_account_id=current_staff.id,
publisher_name=current_staff.display_name,
publisher_role=current_staff.role,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.post(
"/publications/{version_id}/deactivate",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_publications_deactivate(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
try:
payload = service.deactivate_version(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
@router.post(
"/publications/{version_id}/rollback",
response_model=AdminToolGovernanceTransitionResponse,
)
def tool_publications_rollback(
version_id: str,
decision: AdminToolGovernanceDecisionRequest,
service: ToolManagementService = Depends(get_tool_management_service),
current_staff: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
try:
payload = service.rollback_version(
version_id,
actor_staff_account_id=current_staff.id,
actor_name=current_staff.display_name,
actor_role=current_staff.role,
decision_notes=decision.decision_notes,
)
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
return _build_governance_transition_response(payload)
def _build_pipeline_response(payload: dict) -> AdminToolGenerationPipelineResponse:
return AdminToolGenerationPipelineResponse(
service="orquestrador-admin",
message=payload["message"],
version_id=payload["version_id"],
tool_name=payload["tool_name"],
version_number=payload["version_number"],
status=payload["status"],
current_step=payload["current_step"],
steps=payload["steps"],
queue_entry=payload["queue_entry"],
automated_validations=payload.get("automated_validations", []),
execution=payload.get("execution"),
next_steps=payload["next_steps"],
)
def _build_governance_transition_response(payload: dict) -> AdminToolGovernanceTransitionResponse:
return AdminToolGovernanceTransitionResponse(
service="orquestrador-admin",
message=payload["message"],
version_id=payload["version_id"],
tool_name=payload["tool_name"],
version_number=payload["version_number"],
status=payload["status"],
queue_entry=payload["queue_entry"],
publication=payload["publication"],
next_steps=payload["next_steps"],
)
def _build_review_detail_response(payload: dict) -> AdminToolReviewDetailResponse:
return AdminToolReviewDetailResponse(
service="orquestrador-admin",
version_id=payload["version_id"],
tool_name=payload["tool_name"],
display_name=payload["display_name"],
domain=payload["domain"],
version_number=payload["version_number"],
status=payload["status"],
summary=payload["summary"],
description=payload["description"],
business_goal=payload["business_goal"],
owner_name=payload["owner_name"],
parameters=payload["parameters"],
queue_entry=payload["queue_entry"],
automated_validations=payload["automated_validations"],
automated_validation_summary=payload["automated_validation_summary"],
generated_module=payload["generated_module"],
generated_callable=payload["generated_callable"],
generated_source_code=payload["generated_source_code"],
execution=payload.get("execution"),
generation_context=payload["generation_context"],
human_gate=payload["human_gate"],
decision_history=payload["decision_history"],
next_steps=payload["next_steps"],
)
def _build_actions(
settings: AdminSettings,
current_role: StaffRole | str | None = None,
) -> list[AdminToolManagementActionResponse]:
actions = [
AdminToolManagementActionResponse(
key="overview",
label="Overview de tools",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/overview"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Snapshot inicial da governanca de tools no admin.",
),
AdminToolManagementActionResponse(
key="contracts",
label="Contratos compartilhados",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/contracts"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Enumera lifecycle, tipos de parametro e campos de publicacao.",
),
AdminToolManagementActionResponse(
key="drafts",
label="Fila de drafts",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/drafts"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Lista os drafts administrativos persistidos antes da geracao e revisao.",
),
AdminToolManagementActionResponse(
key="draft_intake",
label="Pre-cadastro de tool",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/drafts/intake"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Valida e persiste o draft administrativo da nova tool.",
),
AdminToolManagementActionResponse(
key="review_queue",
label="Fila de revisao",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/review-queue"),
required_permission=AdminPermission.REVIEW_TOOL_GENERATIONS,
description="Superficie para validacao, revisao tecnica e aprovacao humana.",
),
AdminToolManagementActionResponse(
key="publications",
label="Catalogo de publicacoes",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/publications"),
required_permission=AdminPermission.PUBLISH_TOOLS,
description="Catalogo bootstrap de tools ativas voltadas ao runtime de produto.",
),
]
if current_role is None:
return actions
return [action for action in actions if role_has_permission(current_role, action.required_permission)]
def _build_prefixed_path(api_prefix: str, path: str) -> str:
normalized_prefix = api_prefix.rstrip("/")
normalized_path = path if path.startswith("/") else f"/{path}"
if not normalized_prefix:
return normalized_path
if normalized_path == "/":
return f"{normalized_prefix}/"
return f"{normalized_prefix}{normalized_path}"