feat(ui): add chat-driven dataset review flow
Move dataset review clarification into the assistant workspace and rework the review page into a chat-centric layout with execution rails. Add session-scoped assistant actions for mappings, semantic fields, and SQL preview generation. Introduce optimistic locking for dataset review mutations, propagate session versions through API responses, and mask imported filter values before assistant exposure. Refresh tests, i18n, and spec artifacts to match the new workflow. BREAKING CHANGE: dataset review mutation endpoints now require the X-Session-Version header, and clarification is no longer handled through ClarificationDialog-based flows
This commit is contained in:
@@ -28,6 +28,106 @@ from src.core.superset_client import SupersetClient
|
||||
|
||||
logger = cast(Any, logger)
|
||||
|
||||
_EMAIL_PATTERN = re.compile(
|
||||
r"(?P<local>[A-Za-z0-9._%+-]+)@(?P<domain>[A-Za-z0-9.-]+\.[A-Za-z]{2,})"
|
||||
)
|
||||
_UUID_PATTERN = re.compile(
|
||||
r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b"
|
||||
)
|
||||
_LONG_DIGIT_PATTERN = re.compile(r"\b\d{6,}\b")
|
||||
_MIXED_IDENTIFIER_PATTERN = re.compile(
|
||||
r"\b(?=[A-Za-z0-9_-]{10,}\b)(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_-]+\b"
|
||||
)
|
||||
|
||||
|
||||
# [DEF:mask_pii_value:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Redact likely sensitive identifiers while preserving structural hints for assistant-facing dataset-review context.
|
||||
def mask_pii_value(value: Any) -> tuple[Any, bool]:
|
||||
if isinstance(value, str):
|
||||
return _mask_sensitive_text(value)
|
||||
if isinstance(value, list):
|
||||
masked_any = False
|
||||
masked_list: List[Any] = []
|
||||
for item in value:
|
||||
masked_item, item_masked = mask_pii_value(item)
|
||||
masked_list.append(masked_item)
|
||||
masked_any = masked_any or item_masked
|
||||
return masked_list, masked_any
|
||||
if isinstance(value, dict):
|
||||
masked_any = False
|
||||
masked_dict: Dict[str, Any] = {}
|
||||
for key, item in value.items():
|
||||
masked_item, item_masked = mask_pii_value(item)
|
||||
masked_dict[key] = masked_item
|
||||
masked_any = masked_any or item_masked
|
||||
return masked_dict, masked_any
|
||||
return value, False
|
||||
|
||||
|
||||
# [/DEF:mask_pii_value:Function]
|
||||
|
||||
|
||||
# [DEF:sanitize_imported_filter_for_assistant:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Build assistant-safe imported-filter payload with raw-value redaction metadata when sensitive tokens are detected.
|
||||
def sanitize_imported_filter_for_assistant(
|
||||
filter_payload: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
sanitized = deepcopy(filter_payload)
|
||||
masked_raw_value, was_masked = mask_pii_value(filter_payload.get("raw_value"))
|
||||
sanitized["raw_value"] = masked_raw_value
|
||||
sanitized["raw_value_masked"] = bool(
|
||||
filter_payload.get("raw_value_masked", False) or was_masked
|
||||
)
|
||||
return sanitized
|
||||
|
||||
|
||||
# [/DEF:sanitize_imported_filter_for_assistant:Function]
|
||||
|
||||
|
||||
# [DEF:_mask_sensitive_text:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Redact likely PII substrings from one string while preserving non-sensitive delimiters and token shape.
|
||||
def _mask_sensitive_text(value: str) -> tuple[str, bool]:
|
||||
masked = value
|
||||
masked_any = False
|
||||
|
||||
def _replace_email(match: re.Match[str]) -> str:
|
||||
nonlocal masked_any
|
||||
masked_any = True
|
||||
domain = match.group("domain")
|
||||
return f"***@{domain}"
|
||||
|
||||
def _replace_uuid(match: re.Match[str]) -> str:
|
||||
nonlocal masked_any
|
||||
masked_any = True
|
||||
token = match.group(0)
|
||||
return "********-****-****-****-" + token[-12:]
|
||||
|
||||
def _replace_long_digits(match: re.Match[str]) -> str:
|
||||
nonlocal masked_any
|
||||
masked_any = True
|
||||
token = match.group(0)
|
||||
return "*" * max(len(token) - 2, 1) + token[-2:]
|
||||
|
||||
def _replace_mixed_identifier(match: re.Match[str]) -> str:
|
||||
nonlocal masked_any
|
||||
token = match.group(0)
|
||||
if token.isupper() and len(token) <= 5:
|
||||
return token
|
||||
masked_any = True
|
||||
return token[:2] + "***" + token[-2:]
|
||||
|
||||
masked = _EMAIL_PATTERN.sub(_replace_email, masked)
|
||||
masked = _UUID_PATTERN.sub(_replace_uuid, masked)
|
||||
masked = _LONG_DIGIT_PATTERN.sub(_replace_long_digits, masked)
|
||||
masked = _MIXED_IDENTIFIER_PATTERN.sub(_replace_mixed_identifier, masked)
|
||||
return masked, masked_any
|
||||
|
||||
|
||||
# [/DEF:_mask_sensitive_text:Function]
|
||||
|
||||
|
||||
# [DEF:SupersetParsedContext:Class]
|
||||
# @COMPLEXITY: 2
|
||||
@@ -44,6 +144,7 @@ class SupersetParsedContext:
|
||||
imported_filters: List[Dict[str, Any]] = field(default_factory=list)
|
||||
unresolved_references: List[str] = field(default_factory=list)
|
||||
partial_recovery: bool = False
|
||||
dataset_payload: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
# [/DEF:SupersetParsedContext:Class]
|
||||
@@ -313,11 +414,12 @@ class SupersetContextExtractor:
|
||||
)
|
||||
raise ValueError("Unsupported Superset link shape")
|
||||
|
||||
dataset_payload: Optional[Dict[str, Any]] = None
|
||||
if dataset_id is not None:
|
||||
try:
|
||||
dataset_detail = self.client.get_dataset_detail(dataset_id)
|
||||
table_name = str(dataset_detail.get("table_name") or "").strip()
|
||||
schema_name = str(dataset_detail.get("schema") or "").strip()
|
||||
dataset_payload = self.client.get_dataset_detail(dataset_id)
|
||||
table_name = str(dataset_payload.get("table_name") or "").strip()
|
||||
schema_name = str(dataset_payload.get("schema") or "").strip()
|
||||
if table_name:
|
||||
dataset_ref = (
|
||||
f"{schema_name}.{table_name}" if schema_name else table_name
|
||||
@@ -349,6 +451,7 @@ class SupersetContextExtractor:
|
||||
imported_filters=imported_filters,
|
||||
unresolved_references=unresolved_references,
|
||||
partial_recovery=partial_recovery,
|
||||
dataset_payload=dataset_payload,
|
||||
)
|
||||
logger.reflect(
|
||||
"Superset link parsing completed",
|
||||
|
||||
Reference in New Issue
Block a user