"""Project-scoped APIs for operational tasks, evidence and approved actions.""" from __future__ import annotations from datetime import datetime from typing import Literal from uuid import UUID from fastapi import APIRouter, Depends, Header, HTTPException, Query, status from psycopg import AsyncConnection from app.auth.metadata_dependencies import get_metadata_repository from app.auth.permissions import ( OPERATIONAL_TASK_EXECUTE, OPERATIONAL_TASK_EXTERNAL_SYNC, OPERATIONAL_TASK_VIEW, OPERATIONAL_TASK_WRITE, SCADA_VIEW, SIMULATION_VIEW, permissions_for_context, require_permission, ) from app.auth.project_dependencies import ( ProjectContext, get_project_context, get_project_pg_connection, get_project_timescale_connection, ) from app.domain.domain_errors import DomainError from app.domain.schemas.workbench_runtime import WorkbenchDateTime from app.domain.schemas.operational_data import TaskDataQuery, TaskDataSnapshot from app.domain.schemas.task_question import TaskQuestionCreate, TaskQuestionReply, TaskQuestionRequest from app.services.task_questions import create_question, answer_question from app.domain.schemas.operational_task import ( ActionDecisionCommand, ActionExecuteCommand, ActionProposalCommand, ActionReconcileCommand, WorkOrderDraftUpdateCommand, WorkOrderConfirmDispatchCommand, TaskAccessRequest, TaskAccessRequestCreate, TaskAccessRequestDecision, AgentArtifactsCommand, AgentArtifactsResult, AgentRunCancelCommand, AgentRunCommand, AgentRunDispatchFailureCommand, AgentRunFailureCommand, AgentTaskContext, DispatchDueSchedulesCommand, DispatchDueSchedulesResult, EvidenceRecord, ExternalFeedbackCommand, ExternalFeedbackResult, ExternalSourceWithdrawalCommand, HumanApprovalAction, HumanEvidenceCommand, InvestigationReportPage, InvestigationReportRevision, NotificationAcknowledgeCommand, OperationalTaskCreate, OperationalTaskDetail, OperationalTaskNotification, OperationalTaskNotificationPage, OperationalTaskPage, OperationalTaskRetryCommand, OperationalTaskRun, OperationalTaskRunPage, OperationalTaskSchedule, OperationalTaskScheduleCreate, OperationalTaskSchedulePage, OperationalTaskScheduleUpdate, RecoverExpiredLeasesCommand, RecoverExpiredLeasesResult, TaskStatus, TaskDataQueryResult, TaskType, TaskPresentationEventCommand, TaskPresentationEventResult, TaskPresentationCursor, TaskPresentationHistoryPage, ) from app.infra.db.postgresql.operational_tasks import OperationalTaskRepository from app.infra.integrations.operational_actions import ( OperationalActionDispatcher, get_operational_action_dispatcher, ) from app.infra.integrations.operational_data import TaskDataError, read_task_data from app.services.operational_tasks import OperationalTaskService router = APIRouter() @router.post("/operational-tasks/recovery/reconcile") async def reconcile_task_recoveries( command: RecoverExpiredLeasesCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), timescale=Depends(get_project_timescale_connection), metadata=Depends(get_metadata_repository), ) -> dict[str, int]: from app.domain.schemas.project_settings import ProjectSettings from app.services.operational_recovery import reconcile_recoveries if SCADA_VIEW not in permissions_for_context(ctx): raise HTTPException(403, "恢复监测需要 SCADA 查看权限") project = await metadata.get_project_by_id(ctx.project_id) if project is None: raise HTTPException(404, "当前项目不存在") try: resumed = await reconcile_recoveries(_service(ctx, conn), timescale, ProjectSettings.model_validate(project.settings or {}), limit=command.limit, created_from=command.task_created_from) return {"resumed": resumed} except DomainError as exc: _raise_domain(exc) def _service(ctx: ProjectContext, conn: AsyncConnection) -> OperationalTaskService: return OperationalTaskService( OperationalTaskRepository(conn, ctx.project_id), ctx, _task_permissions(ctx), ) def _task_permissions(ctx: ProjectContext) -> set[str]: # Investigations use preconfigured project access, including service accounts. # Do not force an already-authorized reader through legacy per-task delegation. return set(permissions_for_context(ctx)) def _raise_domain(exc: DomainError): raise HTTPException(status_code=exc.status, detail=exc.payload()) from exc @router.get("/operational-tasks/{task_id}/reports", response_model=InvestigationReportPage) async def list_investigation_reports( task_id: UUID, limit: int = Query(default=20, ge=1, le=100), offset: int = Query(default=0, ge=0), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: items, next_offset = await _service(ctx, conn).list_reports(task_id, limit, offset) return InvestigationReportPage(items=items, next_offset=next_offset) except DomainError as exc: _raise_domain(exc) @router.get("/operational-tasks/{task_id}/reports/{revision}", response_model=InvestigationReportRevision) async def get_investigation_report( task_id: UUID, revision: int, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).report(task_id, revision) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks", response_model=OperationalTaskDetail, status_code=status.HTTP_201_CREATED, ) async def create_operational_task( command: OperationalTaskCreate, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).create(command) except DomainError as exc: _raise_domain(exc) @router.get("/operational-tasks", response_model=OperationalTaskPage) async def list_operational_tasks( task_type: TaskType | None = None, task_status: TaskStatus | None = Query(default=None, alias="status"), limit: int = Query(default=50, ge=1, le=100), offset: int = Query(default=0, ge=0), peripheral_work_order_id: UUID | None = Query( default=None, description="Only tasks sourced from or linked to this peripheral work order", ), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: items, next_offset = await _service(ctx, conn).list( task_type=task_type, status=task_status, limit=limit, offset=offset, **({"peripheral_work_order_id": peripheral_work_order_id} if peripheral_work_order_id is not None else {}), ) return OperationalTaskPage( items=items, limit=limit, offset=offset, next_offset=next_offset ) except DomainError as exc: _raise_domain(exc) @router.get("/operational-task-runs", response_model=OperationalTaskRunPage) async def list_operational_task_runs( run_status: Literal[ "queued", "claimed", "running", "succeeded", "failed", "timed_out", "cancelled" ] | None = Query(default=None, alias="status"), task_type: TaskType | None = None, task_created_from: WorkbenchDateTime | None = None, limit: int = Query(default=50, ge=1, le=100), cursor: str | None = None, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): cursor_created_at = None cursor_run_id = None if cursor: try: timestamp, run_id = cursor.rsplit("|", 1) cursor_created_at = datetime.fromisoformat(timestamp) cursor_run_id = UUID(run_id) except (ValueError, TypeError) as exc: raise HTTPException(status_code=400, detail="Invalid run cursor") from exc try: items, next_cursor = await _service(ctx, conn).list_runs( status=run_status, task_type=task_type, task_created_from=task_created_from, limit=limit, cursor_created_at=cursor_created_at, cursor_run_id=cursor_run_id, ) return OperationalTaskRunPage(items=items, next_cursor=next_cursor) except DomainError as exc: _raise_domain(exc) @router.get("/operational-tasks/{task_id}/runs", response_model=OperationalTaskRunPage) async def list_task_run_history( task_id: UUID, limit: int = Query(default=20, ge=1, le=100), offset: int = Query(default=0, ge=0), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return OperationalTaskRunPage(items=await _service(ctx, conn).task_runs(task_id, limit, offset)) except DomainError as exc: _raise_domain(exc) @router.get("/operational-tasks/{task_id}/runs/{run_id}", response_model=OperationalTaskRun) async def get_task_run( task_id: UUID, run_id: UUID, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).task_run(task_id, run_id) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/schedules", response_model=OperationalTaskSchedule, status_code=status.HTTP_201_CREATED, ) async def create_operational_schedule( command: OperationalTaskScheduleCreate, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).create_schedule(command) except DomainError as exc: _raise_domain(exc) @router.get( "/operational-tasks/schedules", response_model=OperationalTaskSchedulePage ) async def list_operational_schedules( ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return OperationalTaskSchedulePage(items=await _service(ctx, conn).list_schedules()) except DomainError as exc: _raise_domain(exc) @router.patch( "/operational-tasks/schedules/{schedule_id}", response_model=OperationalTaskSchedule, ) async def update_operational_schedule( schedule_id: UUID, command: OperationalTaskScheduleUpdate, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).update_schedule(schedule_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/schedules/dispatch-due", response_model=DispatchDueSchedulesResult, ) async def dispatch_due_operational_schedules( command: DispatchDueSchedulesCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).dispatch_due_schedules(command.limit) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/leases/recover", response_model=RecoverExpiredLeasesResult, ) async def recover_operational_task_leases( command: RecoverExpiredLeasesCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).recover_expired_leases( command.limit, task_type=command.task_type, task_created_from=command.task_created_from, ) except DomainError as exc: _raise_domain(exc) @router.get( "/operational-task-notifications", response_model=OperationalTaskNotificationPage, ) async def list_operational_task_notifications( unread_only: bool = False, limit: int = Query(default=50, ge=1, le=100), offset: int = Query(default=0, ge=0), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: items, next_offset = await _service(ctx, conn).list_notifications( unread_only=unread_only, limit=limit, offset=offset ) return OperationalTaskNotificationPage( items=items, limit=limit, offset=offset, next_offset=next_offset ) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-task-notifications/{notification_id}/acknowledge", response_model=OperationalTaskNotification, ) async def acknowledge_operational_task_notification( notification_id: UUID, command: NotificationAcknowledgeCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).acknowledge_notification(notification_id, command) except DomainError as exc: _raise_domain(exc) @router.get("/operational-tasks/{task_id}", response_model=OperationalTaskDetail) async def get_operational_task( task_id: UUID, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).detail(task_id) except DomainError as exc: _raise_domain(exc) @router.post("/operational-tasks/{task_id}/retry", response_model=OperationalTaskDetail) async def retry_operational_task( task_id: UUID, command: OperationalTaskRetryCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).retry_task(task_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/runs/{run_id}/claim", response_model=OperationalTaskRun, ) async def claim_operational_task_run( task_id: UUID, run_id: UUID, command: AgentRunCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).claim_run(task_id, run_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/runs/{run_id}/heartbeat", response_model=OperationalTaskRun, ) async def heartbeat_operational_task_run( task_id: UUID, run_id: UUID, command: AgentRunCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).heartbeat_run(task_id, run_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/runs/{run_id}/complete", response_model=OperationalTaskDetail, ) async def complete_operational_task_run( task_id: UUID, run_id: UUID, command: AgentRunCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).complete_run(task_id, run_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/runs/{run_id}/fail", response_model=OperationalTaskDetail, ) async def fail_operational_task_run( task_id: UUID, run_id: UUID, command: AgentRunFailureCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).fail_run(task_id, run_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/runs/{run_id}/cancel", response_model=OperationalTaskDetail, ) async def cancel_operational_task_run( task_id: UUID, run_id: UUID, command: AgentRunCancelCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).cancel_run(task_id, run_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/runs/{run_id}/dispatch-failure", response_model=OperationalTaskRun, ) async def record_operational_task_run_dispatch_failure( task_id: UUID, run_id: UUID, command: AgentRunDispatchFailureCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).record_dispatch_failure(task_id, run_id, command) except DomainError as exc: _raise_domain(exc) @router.get( "/operational-tasks/{task_id}/agent-context", response_model=AgentTaskContext ) async def get_operational_task_agent_context( task_id: UUID, report_revision: int | None = Query(default=None, ge=1), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).agent_context(task_id, report_revision) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/agent-artifacts", response_model=AgentArtifactsResult, ) async def publish_operational_task_artifacts( task_id: UUID, command: AgentArtifactsCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).publish_artifacts(task_id, command) except DomainError as exc: _raise_domain(exc) @router.get("/operational-tasks/{task_id}/presentation-cursor", response_model=TaskPresentationCursor) async def get_operational_task_presentation_cursor( task_id: UUID, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).presentation_cursor(task_id) except DomainError as exc: _raise_domain(exc) @router.get("/operational-tasks/{task_id}/presentation-events", response_model=TaskPresentationHistoryPage) async def get_operational_task_presentation_history( task_id: UUID, after: int = Query(default=0, ge=0), through: int | None = Query(default=None, ge=0), limit: int = Query(default=100, ge=1, le=200), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).presentation_history(task_id, after=after, through=through, limit=limit) except DomainError as exc: _raise_domain(exc) @router.post("/operational-tasks/{task_id}/presentation-events", response_model=TaskPresentationEventResult) async def publish_operational_task_presentation_event( task_id: UUID, command: TaskPresentationEventCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).publish_presentation_event(task_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/data-query", response_model=TaskDataQueryResult ) async def query_operational_task_data( task_id: UUID, query: TaskDataQuery, authorization: str | None = Header(default=None, alias="Authorization"), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): service = _service(ctx, conn) try: if not await service.can_read_source(task_id, query.source): raise DomainError("PERMISSION_DENIED", "Task has no approved access to this data source", status=403) source_permission = SCADA_VIEW if query.source.startswith("scada_") else SIMULATION_VIEW from app.services.scada_replay_queries import replay_snapshot_rows replay_context = await service.replay_context(task_id) if replay_context: if query.source != "scada_readings": raise HTTPException(403, "回放调查数据接口仅开放本轮 SCADA 快照,请通过 MCP 查询设备目录") rows = await replay_snapshot_rows(conn, ctx.project_id, replay_context, query.device_ids, query.start, query.end, 1001) from app.services.content_digest import digest from datetime import datetime, timezone snapshot = TaskDataSnapshot( source="scada_readings", source_ref=f"scada_replay:{replay_context['replay_id']}:{digest(rows)}", queried_at=datetime.now(timezone.utc), origin="synthetic", data=rows[:1000], truncated=len(rows) > 1000, ) else: snapshot = await read_task_data( context=ctx, query=query, credential=authorization, delegated=source_permission not in _task_permissions(ctx), ) evidence, stored_snapshot = await service.record_data_snapshot(task_id, snapshot) return TaskDataQueryResult(evidence=evidence, snapshot=stored_snapshot) except TaskDataError as exc: raise HTTPException(status_code=exc.status, detail=exc.detail) from exc except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/access-requests", response_model=TaskAccessRequest, status_code=status.HTTP_201_CREATED, ) async def request_operational_task_access( task_id: UUID, command: TaskAccessRequestCreate, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).request_access(task_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/access-requests/{access_request_id}/decision", response_model=TaskAccessRequest, ) async def decide_operational_task_access( task_id: UUID, access_request_id: UUID, command: TaskAccessRequestDecision, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).decide_access(task_id, access_request_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/human-evidence", response_model=OperationalTaskDetail, ) async def add_operational_task_human_evidence( task_id: UUID, command: HumanEvidenceCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).add_human_evidence(task_id, command) except DomainError as exc: _raise_domain(exc) @router.get( "/operational-tasks/{task_id}/evidence/{evidence_id}/snapshot", response_model=TaskDataSnapshot, ) async def get_operational_task_data_snapshot( task_id: UUID, evidence_id: str, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).data_snapshot(task_id, evidence_id) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/actions", response_model=HumanApprovalAction, status_code=status.HTTP_201_CREATED, ) async def propose_operational_task_action( task_id: UUID, command: ActionProposalCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).propose_action(task_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/actions/{action_id}/decision", response_model=HumanApprovalAction, ) async def decide_operational_task_action( task_id: UUID, action_id: UUID, command: ActionDecisionCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).decide_action(task_id, action_id, command) except DomainError as exc: _raise_domain(exc) @router.patch( "/operational-tasks/{task_id}/actions/{action_id}/draft", response_model=HumanApprovalAction, ) async def update_operational_work_order_draft( task_id: UUID, action_id: UUID, command: WorkOrderDraftUpdateCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).update_work_order_draft(task_id, action_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/actions/{action_id}/confirm-dispatch", response_model=HumanApprovalAction, ) async def confirm_operational_work_order_dispatch( task_id: UUID, action_id: UUID, command: WorkOrderConfirmDispatchCommand, authorization: str | None = Header(default=None, alias="Authorization"), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), dispatcher: OperationalActionDispatcher = Depends(get_operational_action_dispatcher), ): try: return await _service(ctx, conn).confirm_work_order_dispatch( task_id, action_id, command, dispatcher, authorization ) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/actions/{action_id}/execute", response_model=HumanApprovalAction, ) async def execute_operational_task_action( task_id: UUID, action_id: UUID, command: ActionExecuteCommand, authorization: str | None = Header(default=None, alias="Authorization"), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), dispatcher: OperationalActionDispatcher = Depends(get_operational_action_dispatcher), ): try: return await _service(ctx, conn).submit_action( task_id, action_id, command, dispatcher, authorization ) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/actions/{action_id}/reconcile", response_model=HumanApprovalAction, ) async def reconcile_operational_task_action( task_id: UUID, action_id: UUID, command: ActionReconcileCommand, authorization: str | None = Header(default=None, alias="Authorization"), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), conn: AsyncConnection = Depends(get_project_pg_connection), dispatcher: OperationalActionDispatcher = Depends(get_operational_action_dispatcher), ): try: return await _service(ctx, conn).reconcile_action( task_id, action_id, command, dispatcher, authorization ) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/external-feedback", response_model=ExternalFeedbackResult, ) async def synchronize_operational_task_feedback( task_id: UUID, command: ExternalFeedbackCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXTERNAL_SYNC)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).external_feedback(task_id, command) except DomainError as exc: _raise_domain(exc) @router.post( "/operational-tasks/{task_id}/withdraw-external-source", response_model=OperationalTaskDetail, ) async def withdraw_operational_task_external_source( task_id: UUID, command: ExternalSourceWithdrawalCommand, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXTERNAL_SYNC)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await _service(ctx, conn).withdraw_external_source(task_id, command) except DomainError as exc: _raise_domain(exc) @router.post("/operational-tasks/{task_id}/questions", response_model=TaskQuestionRequest) async def create_operational_task_question( task_id: UUID, command: TaskQuestionCreate, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await create_question(_service(ctx, conn), task_id, command) except DomainError as exc: _raise_domain(exc) @router.post("/operational-tasks/{task_id}/questions/{question_id}/answer", response_model=OperationalTaskDetail) async def answer_operational_task_question( task_id: UUID, question_id: UUID, command: TaskQuestionReply, ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)), conn: AsyncConnection = Depends(get_project_pg_connection), ): try: return await answer_question(_service(ctx, conn), task_id, question_id, command) except DomainError as exc: _raise_domain(exc)