Skip to content

Commit a5a5090

Browse files
feat(platform): add Data Fabric error classification and extractor (#1784)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f45f948 commit a5a5090

11 files changed

Lines changed: 413 additions & 3 deletions

File tree

packages/uipath-platform/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.1.89"
3+
version = "0.1.90"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-platform/src/uipath/platform/entities/_entities_service.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from ..common._bindings import _resource_overwrites
2525
from ..common._config import UiPathApiConfig
2626
from ..common._execution_context import UiPathExecutionContext
27+
from ..errors._datafabric_error import attach_datafabric_error_mapping
2728
from ..orchestrator._folder_service import FolderService
2829
from ._entity_data_service import EntityDataService, FileContent
2930
from ._entity_resolution import (
@@ -1788,6 +1789,7 @@ async def retrieve_records_async(
17881789
limit=limit,
17891790
)
17901791

1792+
@attach_datafabric_error_mapping("query_entity_records")
17911793
@traced(name="entity_query_records", run_type="uipath")
17921794
def query_entity_records(self, sql_query: str) -> List[Dict[str, Any]]:
17931795
"""Query entity records using a validated SQL query.
@@ -1812,6 +1814,7 @@ def query_entity_records(self, sql_query: str) -> List[Dict[str, Any]]:
18121814
"""
18131815
return self._data.query_entity_records(sql_query)
18141816

1817+
@attach_datafabric_error_mapping("query_entity_records_async")
18151818
@traced(name="entity_query_records", run_type="uipath")
18161819
async def query_entity_records_async(self, sql_query: str) -> List[Dict[str, Any]]:
18171820
"""Asynchronously query entity records using a validated SQL query.

packages/uipath-platform/src/uipath/platform/errors/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,20 @@
2222
from ._context_grounding_index_not_found_exception import (
2323
ContextGroundingIndexNotFoundError,
2424
)
25+
from ._datafabric_error import DataFabricError
2526
from ._enriched_exception import EnrichedException, ExtractedErrorInfo
2627
from ._folder_not_found_exception import FolderNotFoundException
2728
from ._ingestion_in_progress_exception import IngestionInProgressException
2829
from ._operation_failed_exception import OperationFailedException
2930
from ._operation_not_complete_exception import OperationNotCompleteException
3031
from ._secret_missing_error import SecretMissingError
3132
from ._unsupported_data_source_exception import UnsupportedDataSourceException
33+
from .datafabric_error_codes import DataFabricErrorCategory
3234

3335
__all__ = [
3436
"BaseUrlMissingError",
37+
"DataFabricError",
38+
"DataFabricErrorCategory",
3539
"BatchTransformFailedException",
3640
"BatchTransformNotCompleteException",
3741
"ContextGroundingIndexNotFoundError",
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Data Fabric query engine error classification.
2+
3+
Maps error codes from the DF query engine invoking the "query_execute" endpoint to actionable
4+
categories so that callers (e.g. the agent SQL sub-graph) can decide
5+
whether to retry, ask the LLM to fix the SQL, or surface an infra error.
6+
7+
The server error response JSON has the shape:
8+
{"error": "<message>", "code": "<ERROR_CODE>", "traceId": "<uuid>"}
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from dataclasses import dataclass
14+
from typing import TYPE_CHECKING, Any, Callable, TypeVar
15+
16+
from ._extractors._helpers import extract_service_prefix
17+
from .datafabric_error_codes import (
18+
_QUERY_ENTITY_RECORDS_ERROR_CODES,
19+
DataFabricErrorCategory,
20+
classify_error_code,
21+
)
22+
23+
if TYPE_CHECKING:
24+
from ._enriched_exception import EnrichedException
25+
26+
27+
TCallable = TypeVar("TCallable", bound=Callable[..., Any])
28+
29+
30+
def attach_datafabric_error_mapping(
31+
method_name: str,
32+
) -> Callable[[TCallable], TCallable]:
33+
"""Attach Data Fabric error metadata to a query method."""
34+
35+
def decorator(func: TCallable) -> TCallable:
36+
func.__uipath_datafabric_method__ = method_name # type: ignore[attr-defined]
37+
func.__uipath_datafabric_error_codes__ = ( # type: ignore[attr-defined]
38+
_QUERY_ENTITY_RECORDS_ERROR_CODES
39+
)
40+
return func
41+
42+
return decorator
43+
44+
45+
@dataclass(frozen=True)
46+
class DataFabricError:
47+
"""Structured error parsed from a DF query engine response."""
48+
49+
code: str | None
50+
message: str | None
51+
trace_id: str | None
52+
category: DataFabricErrorCategory
53+
54+
@property
55+
def is_retryable(self) -> bool:
56+
return self.category == DataFabricErrorCategory.RETRYABLE
57+
58+
@property
59+
def is_bad_sql(self) -> bool:
60+
return self.category == DataFabricErrorCategory.BAD_SQL
61+
62+
@staticmethod
63+
def from_enriched_exception(exc: EnrichedException) -> DataFabricError | None:
64+
"""Extract a DataFabricError from an EnrichedException, if applicable.
65+
66+
Returns None if the exception is not from a Data Fabric endpoint.
67+
"""
68+
if extract_service_prefix(exc.url) != "datafabric_":
69+
return None
70+
71+
info = exc.error_info
72+
code = info.error_code if info else None
73+
message = info.message if info else None
74+
trace_id = info.trace_id if info else None
75+
76+
return DataFabricError(
77+
code=code,
78+
message=message,
79+
trace_id=trace_id,
80+
category=classify_error_code(code),
81+
)
82+
83+
@staticmethod
84+
def from_response_body(body: dict[str, Any]) -> DataFabricError:
85+
"""Parse a DataFabricError directly from a response body dict."""
86+
raw_code = body.get("code")
87+
code = (
88+
str(raw_code)
89+
if raw_code is not None and not isinstance(raw_code, (dict, list))
90+
else None
91+
)
92+
message = body.get("error")
93+
if not isinstance(message, str):
94+
message = (
95+
body.get("message") if isinstance(body.get("message"), str) else None
96+
)
97+
trace_id = body.get("traceId")
98+
if not isinstance(trace_id, str):
99+
trace_id = (
100+
body.get("requestId")
101+
if isinstance(body.get("requestId"), str)
102+
else None
103+
)
104+
return DataFabricError(
105+
code=code,
106+
message=message,
107+
trace_id=trace_id,
108+
category=classify_error_code(code),
109+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Data Fabric query engine error payload extractor.
2+
3+
DF returns: {"error": "<message>", "code": "<ERROR_CODE>", "traceId": "<uuid>"}
4+
"""
5+
6+
from typing import Any
7+
8+
from .._enriched_exception import ExtractedErrorInfo
9+
from ._helpers import get_str_field, get_typed_field
10+
11+
12+
def extract_datafabric(body: dict[str, Any]) -> ExtractedErrorInfo:
13+
message = get_typed_field(body, str, "error", "message")
14+
error_code = get_str_field(body, "code", "errorCode")
15+
trace_id = get_typed_field(body, str, "traceId", "requestId")
16+
17+
return ExtractedErrorInfo(
18+
message=message,
19+
error_code=error_code,
20+
trace_id=trace_id,
21+
)

packages/uipath-platform/src/uipath/platform/errors/_extractors/_router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .._enriched_exception import ExtractedErrorInfo
1515
from ._agenthub import extract_agenthub
1616
from ._apps import extract_apps
17+
from ._datafabric import extract_datafabric
1718
from ._elements import extract_elements
1819
from ._generic import extract_generic
1920
from ._helpers import extract_service_prefix, is_llm_path
@@ -29,6 +30,7 @@
2930
"agenthub_": extract_agenthub,
3031
"agentsruntime_": extract_agenthub,
3132
"apps_": extract_apps,
33+
"datafabric_": extract_datafabric,
3234
"elements_": extract_elements,
3335
"llmopstenant_": extract_llmops,
3436
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Data Fabric query-engine error code constants."""
2+
3+
from __future__ import annotations
4+
5+
from enum import Enum
6+
7+
_RETRYABLE_CODES: frozenset[str] = frozenset(
8+
{
9+
"EXECUTION_TIMEOUT",
10+
"SQLITE_BUSY",
11+
"EXECUTION_INTERRUPTED",
12+
}
13+
)
14+
15+
_BAD_SQL_CODES: frozenset[str] = frozenset(
16+
{
17+
"SQL_PARSING",
18+
"SQL_VALIDATION",
19+
}
20+
)
21+
22+
_INFRASTRUCTURE_CODES: frozenset[str] = frozenset(
23+
{
24+
"SQLITE_MEMORY_FULL",
25+
"EPHEMERAL_STORAGE_ERROR",
26+
"INTERNAL_ERROR",
27+
"FQS_ERROR",
28+
}
29+
)
30+
31+
_DATA_ISSUE_CODES: frozenset[str] = frozenset(
32+
{
33+
"FRAGMENT_EXECUTION_FAILURE",
34+
"CONTEXT_CREATION",
35+
"UNKNOWN_ENTITY",
36+
"EXECUTION_ERROR",
37+
"RESULT_TOO_LARGE",
38+
}
39+
)
40+
41+
_QUERY_ENTITY_RECORDS_ERROR_CODES: frozenset[str] = frozenset(
42+
{*_RETRYABLE_CODES, *_BAD_SQL_CODES, *_INFRASTRUCTURE_CODES, *_DATA_ISSUE_CODES}
43+
)
44+
45+
46+
class DataFabricErrorCategory(str, Enum):
47+
"""Actionable error category for Data Fabric query failures."""
48+
49+
RETRYABLE = "retryable"
50+
BAD_SQL = "bad_sql"
51+
INFRASTRUCTURE = "infrastructure"
52+
DATA_ISSUE = "data_issue"
53+
UNKNOWN = "unknown"
54+
55+
56+
def classify_error_code(code: str | None) -> DataFabricErrorCategory:
57+
"""Classify a DF error code string into an actionable category."""
58+
if not code:
59+
return DataFabricErrorCategory.UNKNOWN
60+
upper = code.upper()
61+
if upper in _RETRYABLE_CODES:
62+
return DataFabricErrorCategory.RETRYABLE
63+
if upper in _BAD_SQL_CODES:
64+
return DataFabricErrorCategory.BAD_SQL
65+
if upper in _INFRASTRUCTURE_CODES:
66+
return DataFabricErrorCategory.INFRASTRUCTURE
67+
if upper in _DATA_ISSUE_CODES:
68+
return DataFabricErrorCategory.DATA_ISSUE
69+
return DataFabricErrorCategory.UNKNOWN

0 commit comments

Comments
 (0)