Files
ss-tools/backend/src/api/routes/dataset_review_pkg/_routes.py
busya 306c5ae742 semantics: complete DEF-to-region migration, fix regressions
- Convert legacy [DEF🆔Type] anchors to #region/#endregion across 329 files
- Reinstate _normalize_timestamp_value in sql_generator.py
- Fix MarkerLogger→logger migration in events.py (molecular CoT markers)
- Fix dataset_review orchestrator dependencies (_build_execution_snapshot)
- Fix config_manager stale-record deletion (moved to save path only)
- Add 77 missing [/DEF:] closers in 5 unbalanced test files
- Update assistant_chat.integration.test.js for #region format
- Apply molecular-cot-logging markers (REASON/REFLECT/EXPLORE) via logger.* methods
2026-05-12 23:54:55 +03:00

1101 lines
39 KiB
Python

# #region DatasetReviewRoutes [C:3] [TYPE Module]
# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.
from __future__ import annotations
from datetime import datetime
from typing import Any, List, Optional, Union, cast
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from src.core.logger import belief_scope, logger
from src.dependencies import get_config_manager, get_current_user, has_permission
from src.models.auth import User
from src.models.dataset_review import (
ApprovalState,
ArtifactFormat,
FieldProvenance,
MappingMethod,
PreviewStatus,
ReadinessState,
RecommendedAction,
SessionStatus,
)
from src.schemas.dataset_review import (
ClarificationAnswerDto,
CompiledPreviewDto,
ExecutionMappingDto,
SemanticFieldEntryDto,
SessionSummary,
ValidationFindingDto,
)
from src.services.dataset_review.clarification_engine import (
ClarificationAnswerCommand,
ClarificationStateResult,
)
from src.services.dataset_review.orchestrator import (
LaunchDatasetCommand,
PreparePreviewCommand,
StartSessionCommand,
)
from src.services.dataset_review.repositories.session_repository import (
DatasetReviewSessionVersionConflictError,
)
from src.api.routes.dataset_review_pkg._dependencies import (
BatchApproveMappingRequest,
BatchApproveSemanticRequest,
ClarificationAnswerRequest,
ClarificationAnswerResultResponse,
ClarificationStateResponse,
ExportArtifactResponse,
FeedbackRequest,
FeedbackResponse,
FieldSemanticUpdateRequest,
LaunchDatasetResponse,
MappingCollectionResponse,
PreviewEnqueueResultResponse,
SessionCollectionResponse,
StartSessionRequest,
UpdateExecutionMappingRequest,
UpdateSessionRequest,
_build_documentation_export,
_build_sql_lab_redirect_url,
_build_validation_export,
_commit_owned_session_mutation,
_get_clarification_engine,
_get_latest_clarification_session_or_404,
_get_owned_field_or_404,
_get_owned_mapping_or_404,
_get_owned_session_or_404,
_get_orchestrator,
_get_repository,
_prepare_owned_session_mutation,
_record_session_event,
_require_auto_review_flag,
_require_clarification_flag,
_require_execution_flag,
_require_session_version_header,
_serialize_clarification_state,
_serialize_empty_clarification_state,
_serialize_execution_mapping,
_serialize_preview,
_serialize_run_context,
_serialize_semantic_field,
_serialize_session_detail,
_serialize_session_summary,
_update_semantic_field_state,
_build_session_version_conflict_http_exception,
)
router = APIRouter(prefix="/api/dataset-orchestration", tags=["Dataset Orchestration"])
# #region list_sessions [C:3] [TYPE Function]
# @BRIEF List resumable dataset review sessions for the current user.
@router.get(
"/sessions",
response_model=SessionCollectionResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "READ")),
],
)
async def list_sessions(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.list_sessions"):
logger.reason(
"Listing dataset review sessions",
extra={"user_id": current_user.id, "page": page, "page_size": page_size},
)
sessions = repository.list_sessions_for_user(current_user.id)
start = (page - 1) * page_size
end = start + page_size
items = [_serialize_session_summary(s) for s in sessions[start:end]]
logger.reflect(
"Session page assembled",
extra={
"user_id": current_user.id,
"returned": len(items),
"total": len(sessions),
},
)
return SessionCollectionResponse(
items=items,
total=len(sessions),
page=page,
page_size=page_size,
has_next=end < len(sessions),
)
# #endregion list_sessions
# #region start_session [C:4] [TYPE Function]
# @BRIEF Start a new dataset review session from a Superset link or dataset selection.
@router.post(
"/sessions",
response_model=SessionSummary,
status_code=status.HTTP_201_CREATED,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def start_session(
request: StartSessionRequest,
orchestrator=Depends(_get_orchestrator),
current_user: User = Depends(get_current_user),
):
with belief_scope("start_session"):
logger.reason(
"Starting dataset review session",
extra={
"user_id": current_user.id,
"environment_id": request.environment_id,
},
)
try:
result = orchestrator.start_session(
StartSessionCommand(
user=current_user,
environment_id=request.environment_id,
source_kind=request.source_kind,
source_input=request.source_input,
)
)
except ValueError as exc:
logger.explore(
"Session start rejected",
extra={"user_id": current_user.id, "error": str(exc)},
)
detail = str(exc)
sc = (
status.HTTP_404_NOT_FOUND
if detail == "Environment not found"
else status.HTTP_400_BAD_REQUEST
)
raise HTTPException(status_code=sc, detail=detail) from exc
logger.reflect(
"Session started", extra={"session_id": result.session.session_id}
)
return _serialize_session_summary(result.session)
# #endregion start_session
# #region get_session_detail [C:3] [TYPE Function]
# @BRIEF Return the full accessible dataset review session aggregate.
@router.get(
"/sessions/{session_id}",
response_model=SessionSummary,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "READ")),
],
)
async def get_session_detail(
session_id: str,
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.get_session_detail"):
session = _get_owned_session_or_404(repository, session_id, current_user)
return _serialize_session_detail(session)
# #endregion get_session_detail
# #region update_session [C:3] [TYPE Function]
# @BRIEF Update resumable lifecycle status for an owned session.
@router.patch(
"/sessions/{session_id}",
response_model=SessionSummary,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def update_session(
session_id: str,
request: UpdateSessionRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("update_session"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
session_record = cast(Any, session)
session_record.status = request.status
if request.status == SessionStatus.PAUSED:
session_record.recommended_action = RecommendedAction.RESUME_SESSION
elif request.status in {
SessionStatus.ARCHIVED,
SessionStatus.CANCELLED,
SessionStatus.COMPLETED,
}:
session_record.active_task_id = None
_commit_owned_session_mutation(repository, session)
_record_session_event(
repository,
session,
current_user,
event_type="session_status_updated",
event_summary="Dataset review session lifecycle updated",
event_details={
"status": session_record.status.value,
"version": session_record.version,
},
)
return _serialize_session_summary(session)
# #endregion update_session
# #region delete_session [C:3] [TYPE Function]
# @BRIEF Archive or hard-delete a session owned by the current user.
@router.delete(
"/sessions/{session_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def delete_session(
session_id: str,
hard_delete: bool = Query(False),
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("delete_session"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
if hard_delete:
_record_session_event(
repository,
session,
current_user,
event_type="session_deleted",
event_summary="Session hard-deleted",
event_details={"hard_delete": True},
)
repository.db.delete(session)
repository.db.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)
session_record = cast(Any, session)
session_record.status = SessionStatus.ARCHIVED
session_record.active_task_id = None
_commit_owned_session_mutation(repository, session)
_record_session_event(
repository,
session,
current_user,
event_type="session_archived",
event_summary="Session archived",
event_details={"hard_delete": False, "version": session_record.version},
)
return Response(status_code=status.HTTP_204_NO_CONTENT)
# #endregion delete_session
# #region export_documentation [C:3] [TYPE Function]
# @BRIEF Export documentation output for the current session.
@router.get(
"/sessions/{session_id}/exports/documentation",
response_model=ExportArtifactResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "READ")),
],
)
async def export_documentation(
session_id: str,
format: ArtifactFormat = Query(ArtifactFormat.JSON),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("export_documentation"):
if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only json and markdown exports are supported",
)
session = _get_owned_session_or_404(repository, session_id, current_user)
payload = _build_documentation_export(session, format)
return ExportArtifactResponse(
artifact_id=f"documentation-{session.session_id}-{format.value}",
session_id=session.session_id,
artifact_type="documentation",
format=format.value,
storage_ref=payload["storage_ref"],
created_by_user_id=current_user.id,
content=payload["content"],
)
# #endregion export_documentation
# #region export_validation [C:3] [TYPE Function]
# @BRIEF Export validation findings for the current session.
@router.get(
"/sessions/{session_id}/exports/validation",
response_model=ExportArtifactResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "READ")),
],
)
async def export_validation(
session_id: str,
format: ArtifactFormat = Query(ArtifactFormat.JSON),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("export_validation"):
if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only json and markdown exports are supported",
)
session = _get_owned_session_or_404(repository, session_id, current_user)
payload = _build_validation_export(session, format)
return ExportArtifactResponse(
artifact_id=f"validation-{session.session_id}-{format.value}",
session_id=session.session_id,
artifact_type="validation_report",
format=format.value,
storage_ref=payload["storage_ref"],
created_by_user_id=current_user.id,
content=payload["content"],
)
# #endregion export_validation
# #region get_clarification_state [C:3] [TYPE Function]
# @BRIEF Return the current clarification session summary and active question payload.
@router.get(
"/sessions/{session_id}/clarification",
response_model=ClarificationStateResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_clarification_flag),
Depends(has_permission("dataset:session", "READ")),
],
)
async def get_clarification_state(
session_id: str,
repository=Depends(_get_repository),
clarification_engine=Depends(_get_clarification_engine),
current_user: User = Depends(get_current_user),
):
with belief_scope("get_clarification_state"):
session = _get_owned_session_or_404(repository, session_id, current_user)
if not session.clarification_sessions:
return _serialize_empty_clarification_state()
cs = _get_latest_clarification_session_or_404(session)
question = clarification_engine.build_question_payload(session)
return _serialize_clarification_state(
ClarificationStateResult(
clarification_session=cs,
current_question=question,
session=session,
changed_findings=[],
)
)
# #endregion get_clarification_state
# #region resume_clarification [C:3] [TYPE Function]
# @BRIEF Resume clarification mode on the highest-priority unresolved question.
@router.post(
"/sessions/{session_id}/clarification/resume",
response_model=ClarificationStateResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_clarification_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def resume_clarification(
session_id: str,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
clarification_engine=Depends(_get_clarification_engine),
current_user: User = Depends(get_current_user),
):
with belief_scope("resume_clarification"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
cs = _get_latest_clarification_session_or_404(session)
question = clarification_engine.build_question_payload(session)
return _serialize_clarification_state(
ClarificationStateResult(
clarification_session=cs,
current_question=question,
session=session,
changed_findings=[],
)
)
# #endregion resume_clarification
# #region record_clarification_answer [C:3] [TYPE Function]
# @BRIEF Persist one clarification answer before advancing the active pointer.
@router.post(
"/sessions/{session_id}/clarification/answers",
response_model=ClarificationAnswerResultResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_clarification_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def record_clarification_answer(
session_id: str,
request: ClarificationAnswerRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
clarification_engine=Depends(_get_clarification_engine),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.record_clarification_answer"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
try:
result = clarification_engine.record_answer(
ClarificationAnswerCommand(
session=session,
question_id=request.question_id,
answer_kind=request.answer_kind,
answer_value=request.answer_value,
user=current_user,
)
)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)
) from exc
return ClarificationAnswerResultResponse(
clarification_state=_serialize_clarification_state(result),
session=_serialize_session_summary(result.session),
changed_findings=[
ValidationFindingDto.model_validate(f, from_attributes=True)
for f in result.changed_findings
],
)
# #endregion record_clarification_answer
# #region update_field_semantic [C:3] [TYPE Function]
# @BRIEF Apply one field-level semantic candidate decision or manual override.
@router.patch(
"/sessions/{session_id}/fields/{field_id}/semantic",
response_model=SemanticFieldEntryDto,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def update_field_semantic(
session_id: str,
field_id: str,
request: FieldSemanticUpdateRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.update_field_semantic"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
field = _get_owned_field_or_404(session, field_id)
_update_semantic_field_state(field, request, changed_by="user")
sr = cast(Any, session)
_commit_owned_session_mutation(repository, session, refresh_targets=[field])
_record_session_event(
repository,
session,
current_user,
event_type="semantic_field_updated",
event_summary="Semantic field decision persisted",
event_details={"field_id": field.field_id, "version": sr.version},
)
return _serialize_semantic_field(field)
# #endregion update_field_semantic
# #region lock_field_semantic [C:3] [TYPE Function]
# @BRIEF Lock one semantic field against later automatic overwrite.
@router.post(
"/sessions/{session_id}/fields/{field_id}/lock",
response_model=SemanticFieldEntryDto,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def lock_field_semantic(
session_id: str,
field_id: str,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.lock_field_semantic"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
field = _get_owned_field_or_404(session, field_id)
field.is_locked = True
field.last_changed_by = "user"
sr = cast(Any, session)
_commit_owned_session_mutation(repository, session, refresh_targets=[field])
_record_session_event(
repository,
session,
current_user,
event_type="semantic_field_locked",
event_summary="Semantic field lock persisted",
event_details={"field_id": field.field_id, "version": sr.version},
)
return _serialize_semantic_field(field)
# #endregion lock_field_semantic
# #region unlock_field_semantic [C:3] [TYPE Function]
# @BRIEF Unlock one semantic field so later automated candidate application may replace it.
@router.post(
"/sessions/{session_id}/fields/{field_id}/unlock",
response_model=SemanticFieldEntryDto,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def unlock_field_semantic(
session_id: str,
field_id: str,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.unlock_field_semantic"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
field = _get_owned_field_or_404(session, field_id)
field.is_locked = False
field.last_changed_by = "user"
if field.provenance == FieldProvenance.MANUAL_OVERRIDE:
field.provenance = FieldProvenance.UNRESOLVED
field.needs_review = True
sr = cast(Any, session)
_commit_owned_session_mutation(repository, session, refresh_targets=[field])
_record_session_event(
repository,
session,
current_user,
event_type="semantic_field_unlocked",
event_summary="Semantic field unlock persisted",
event_details={"field_id": field.field_id, "version": sr.version},
)
return _serialize_semantic_field(field)
# #endregion unlock_field_semantic
# #region approve_batch_semantic_fields [C:3] [TYPE Function]
# @BRIEF Approve multiple semantic candidate decisions in one batch.
@router.post(
"/sessions/{session_id}/fields/semantic/approve-batch",
response_model=List[SemanticFieldEntryDto],
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def approve_batch_semantic_fields(
session_id: str,
request: BatchApproveSemanticRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.approve_batch_semantic_fields"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
updated = []
for item in request.items:
field = _get_owned_field_or_404(session, item.field_id)
_update_semantic_field_state(
field,
FieldSemanticUpdateRequest(
candidate_id=item.candidate_id, lock_field=item.lock_field
),
changed_by="user",
)
updated.append(field)
sr = cast(Any, session)
_commit_owned_session_mutation(
repository, session, refresh_targets=list(updated)
)
_record_session_event(
repository,
session,
current_user,
event_type="semantic_fields_batch_approved",
event_summary="Batch semantic approval persisted",
event_details={"count": len(updated), "version": sr.version},
)
return [_serialize_semantic_field(f) for f in updated]
# #endregion approve_batch_semantic_fields
# #region list_execution_mappings [C:3] [TYPE Function]
# @BRIEF Return the current mapping-review set for one accessible session.
@router.get(
"/sessions/{session_id}/mappings",
response_model=MappingCollectionResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_execution_flag),
Depends(has_permission("dataset:session", "READ")),
],
)
async def list_execution_mappings(
session_id: str,
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.list_execution_mappings"):
session = _get_owned_session_or_404(repository, session_id, current_user)
return MappingCollectionResponse(
items=[_serialize_execution_mapping(m) for m in session.execution_mappings]
)
# #endregion list_execution_mappings
# #region update_execution_mapping [C:3] [TYPE Function]
# @BRIEF Persist one owner-authorized execution-mapping effective value override.
@router.patch(
"/sessions/{session_id}/mappings/{mapping_id}",
response_model=ExecutionMappingDto,
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_execution_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def update_execution_mapping(
session_id: str,
mapping_id: str,
request: UpdateExecutionMappingRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.update_execution_mapping"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
mapping = _get_owned_mapping_or_404(session, mapping_id)
if request.effective_value is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="effective_value is required",
)
mapping.effective_value = request.effective_value
mapping.mapping_method = MappingMethod(
request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value
)
mapping.transformation_note = request.transformation_note
mapping.approval_state = ApprovalState.APPROVED
mapping.approved_by_user_id = current_user.id
mapping.approved_at = datetime.utcnow()
session.last_activity_at = datetime.utcnow()
session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW
if session.readiness_state in {
ReadinessState.MAPPING_REVIEW_NEEDED,
ReadinessState.COMPILED_PREVIEW_READY,
ReadinessState.RUN_READY,
ReadinessState.RUN_IN_PROGRESS,
}:
session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY
for preview in session.previews:
if preview.preview_status == PreviewStatus.READY:
preview.preview_status = PreviewStatus.STALE
sr = cast(Any, session)
_commit_owned_session_mutation(repository, session, refresh_targets=[mapping])
_record_session_event(
repository,
session,
current_user,
event_type="execution_mapping_updated",
event_summary="Mapping override persisted",
event_details={"mapping_id": mapping.mapping_id, "version": sr.version},
)
return _serialize_execution_mapping(mapping)
# #endregion update_execution_mapping
# #region approve_execution_mapping [C:3] [TYPE Function]
# @BRIEF Explicitly approve a warning-sensitive mapping transformation.
@router.post(
"/sessions/{session_id}/mappings/{mapping_id}/approve",
response_model=ExecutionMappingDto,
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_execution_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def approve_execution_mapping(
session_id: str,
mapping_id: str,
request: ApproveMappingRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.approve_execution_mapping"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
mapping = _get_owned_mapping_or_404(session, mapping_id)
mapping.approval_state = ApprovalState.APPROVED
mapping.approved_by_user_id = current_user.id
mapping.approved_at = datetime.utcnow()
if request.approval_note:
mapping.transformation_note = request.approval_note
session.last_activity_at = datetime.utcnow()
if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:
session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW
sr = cast(Any, session)
_commit_owned_session_mutation(repository, session, refresh_targets=[mapping])
_record_session_event(
repository,
session,
current_user,
event_type="execution_mapping_approved",
event_summary="Mapping approval persisted",
event_details={"mapping_id": mapping.mapping_id, "version": sr.version},
)
return _serialize_execution_mapping(mapping)
# #endregion approve_execution_mapping
# #region approve_batch_execution_mappings [C:3] [TYPE Function]
# @BRIEF Approve multiple warning-sensitive execution mappings in one batch.
@router.post(
"/sessions/{session_id}/mappings/approve-batch",
response_model=List[ExecutionMappingDto],
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_execution_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def approve_batch_execution_mappings(
session_id: str,
request: BatchApproveMappingRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.approve_batch_execution_mappings"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
updated = []
for mid in list(dict.fromkeys(request.mapping_ids)):
mapping = _get_owned_mapping_or_404(session, mid)
mapping.approval_state = ApprovalState.APPROVED
mapping.approved_by_user_id = current_user.id
mapping.approved_at = datetime.utcnow()
if request.approval_note:
mapping.transformation_note = request.approval_note
updated.append(mapping)
session.last_activity_at = datetime.utcnow()
if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:
session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW
sr = cast(Any, session)
_commit_owned_session_mutation(
repository, session, refresh_targets=list(updated)
)
_record_session_event(
repository,
session,
current_user,
event_type="execution_mappings_batch_approved",
event_summary="Batch mapping approval persisted",
event_details={"count": len(updated), "version": sr.version},
)
return [_serialize_execution_mapping(m) for m in updated]
# #endregion approve_batch_execution_mappings
# #region trigger_preview_generation [C:3] [TYPE Function]
# @BRIEF Trigger Superset-side preview compilation for the current owned execution context.
@router.post(
"/sessions/{session_id}/preview",
response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse],
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_execution_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def trigger_preview_generation(
session_id: str,
response: Response,
orchestrator=Depends(_get_orchestrator),
repository=Depends(_get_repository),
session_version: int = Depends(_require_session_version_header),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.trigger_preview_generation"):
_prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
try:
result = orchestrator.prepare_launch_preview(
PreparePreviewCommand(
user=current_user,
session_id=session_id,
expected_version=session_version,
)
)
except DatasetReviewSessionVersionConflictError as exc:
raise _build_session_version_conflict_http_exception(exc) from exc
except ValueError as exc:
detail = str(exc)
sc = (
status.HTTP_404_NOT_FOUND
if detail in {"Session not found", "Environment not found"}
else status.HTTP_409_CONFLICT
if detail.startswith("Preview blocked:")
else status.HTTP_400_BAD_REQUEST
)
raise HTTPException(status_code=sc, detail=detail) from exc
if result.preview.preview_status == PreviewStatus.PENDING:
response.status_code = status.HTTP_202_ACCEPTED
return PreviewEnqueueResultResponse(
session_id=result.session.session_id,
session_version=int(getattr(result.session, "version", 0) or 0),
preview_status=result.preview.preview_status.value,
task_id=None,
)
response.status_code = status.HTTP_200_OK
return _serialize_preview(
result.preview,
session_version_fallback=int(getattr(result.session, "version", 0) or 0),
)
# #endregion trigger_preview_generation
# #region launch_dataset [C:3] [TYPE Function]
# @BRIEF Execute the current owned session launch handoff and return audited SQL Lab run context.
@router.post(
"/sessions/{session_id}/launch",
response_model=LaunchDatasetResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_execution_flag),
Depends(has_permission("dataset:execution:launch", "EXECUTE")),
],
)
async def launch_dataset(
session_id: str,
orchestrator=Depends(_get_orchestrator),
repository=Depends(_get_repository),
session_version: int = Depends(_require_session_version_header),
config_manager=Depends(get_config_manager),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.launch_dataset"):
_prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
try:
result = orchestrator.launch_dataset(
LaunchDatasetCommand(
user=current_user,
session_id=session_id,
expected_version=session_version,
)
)
except DatasetReviewSessionVersionConflictError as exc:
raise _build_session_version_conflict_http_exception(exc) from exc
except ValueError as exc:
detail = str(exc)
sc = (
status.HTTP_404_NOT_FOUND
if detail in {"Session not found", "Environment not found"}
else status.HTTP_409_CONFLICT
if detail.startswith("Launch blocked:")
else status.HTTP_400_BAD_REQUEST
)
raise HTTPException(status_code=sc, detail=detail) from exc
environment = config_manager.get_environment(result.session.environment_id)
env_url = getattr(environment, "url", "") if environment is not None else ""
return LaunchDatasetResponse(
run_context=_serialize_run_context(result.run_context),
redirect_url=_build_sql_lab_redirect_url(
environment_url=env_url,
sql_lab_session_ref=result.run_context.sql_lab_session_ref,
),
)
# #endregion launch_dataset
# #region record_field_feedback [C:3] [TYPE Function]
# @BRIEF Persist thumbs up/down feedback for AI-assisted semantic field content.
@router.post(
"/sessions/{session_id}/fields/{field_id}/feedback",
response_model=FeedbackResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def record_field_feedback(
session_id: str,
field_id: str,
request: FeedbackRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.record_field_feedback"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
field = _get_owned_field_or_404(session, field_id)
field.user_feedback = request.feedback
sr = cast(Any, session)
_commit_owned_session_mutation(repository, session)
_record_session_event(
repository,
session,
current_user,
event_type="semantic_field_feedback_recorded",
event_summary="Feedback persisted",
event_details={
"field_id": field.field_id,
"feedback": request.feedback,
"version": sr.version,
},
)
return FeedbackResponse(target_id=field.field_id, feedback=request.feedback)
# #endregion record_field_feedback
# #region record_clarification_feedback [C:3] [TYPE Function]
# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.
@router.post(
"/sessions/{session_id}/clarification/questions/{question_id}/feedback",
response_model=FeedbackResponse,
dependencies=[
Depends(_require_auto_review_flag),
Depends(_require_clarification_flag),
Depends(has_permission("dataset:session", "MANAGE")),
],
)
async def record_clarification_feedback(
session_id: str,
question_id: str,
request: FeedbackRequest,
session_version: int = Depends(_require_session_version_header),
repository=Depends(_get_repository),
current_user: User = Depends(get_current_user),
):
with belief_scope("dataset_review.record_clarification_feedback"):
session = _prepare_owned_session_mutation(
repository, session_id, current_user, session_version
)
cs = _get_latest_clarification_session_or_404(session)
question = next((q for q in cs.questions if q.question_id == question_id), None)
if question is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Clarification question not found",
)
if question.answer is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Clarification answer not found",
)
question.answer.user_feedback = request.feedback
sr = cast(Any, session)
_commit_owned_session_mutation(repository, session)
_record_session_event(
repository,
session,
current_user,
event_type="clarification_feedback_recorded",
event_summary="Feedback persisted",
event_details={
"question_id": question.question_id,
"feedback": request.feedback,
"version": sr.version,
},
)
return FeedbackResponse(
target_id=question.question_id, feedback=request.feedback
)
# #endregion record_clarification_feedback
# #endregion DatasetReviewRoutes