Skip to content

Commit cce4519

Browse files
feat(guardrails): add decorator framework to uipath-platform
Move @guardrail decorator, validators, actions, and adapter registry from uipath-langchain into uipath-platform as a framework-agnostic subpackage. Includes PIIValidator, PromptInjectionValidator, CustomValidator, BlockAction, LogAction, and GuardrailTargetAdapter Protocol. Bump version to 0.1.18. Remove unused _evaluate_rules function and its tests (was dead code — never called outside of tests). Also drops unused GuardrailValidationResultType import and inspect module. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 96e1894 commit cce4519

File tree

18 files changed

+1925
-3
lines changed

18 files changed

+1925
-3
lines changed

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.17"
3+
version = "0.1.18"
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/guardrails/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@
1414
)
1515

1616
from ._guardrails_service import GuardrailsService
17+
from .decorators import (
18+
BlockAction,
19+
CustomValidator,
20+
GuardrailAction,
21+
GuardrailBlockException,
22+
GuardrailExecutionStage,
23+
GuardrailTargetAdapter,
24+
GuardrailValidatorBase,
25+
LogAction,
26+
LoggingSeverityLevel,
27+
PIIDetectionEntity,
28+
PIIDetectionEntityType,
29+
PIIValidator,
30+
PromptInjectionValidator,
31+
RuleFunction,
32+
guardrail,
33+
register_guardrail_adapter,
34+
)
1735
from .guardrails import (
1836
BuiltInValidatorGuardrail,
1937
EnumListParameterValue,
@@ -22,7 +40,9 @@
2240
)
2341

2442
__all__ = [
43+
# Service
2544
"GuardrailsService",
45+
# Guardrail models
2646
"BuiltInValidatorGuardrail",
2747
"GuardrailType",
2848
"GuardrailValidationResultType",
@@ -33,4 +53,21 @@
3353
"GuardrailValidationResult",
3454
"EnumListParameterValue",
3555
"MapEnumParameterValue",
56+
# Decorator framework
57+
"guardrail",
58+
"GuardrailValidatorBase",
59+
"PIIValidator",
60+
"PromptInjectionValidator",
61+
"CustomValidator",
62+
"RuleFunction",
63+
"PIIDetectionEntity",
64+
"PIIDetectionEntityType",
65+
"GuardrailExecutionStage",
66+
"GuardrailAction",
67+
"LogAction",
68+
"BlockAction",
69+
"LoggingSeverityLevel",
70+
"GuardrailBlockException",
71+
"GuardrailTargetAdapter",
72+
"register_guardrail_adapter",
3673
]
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Guardrail decorator framework for UiPath Platform.
2+
3+
Provides the ``@guardrail`` decorator, built-in validators, actions, and an
4+
adapter registry that framework integrations (e.g. *uipath-langchain*) use to
5+
teach the decorator how to wrap their specific object types.
6+
7+
Quick start::
8+
9+
from uipath.platform.guardrails.decorators import (
10+
guardrail,
11+
PIIValidator,
12+
LogAction,
13+
BlockAction,
14+
PIIDetectionEntity,
15+
PIIDetectionEntityType,
16+
GuardrailExecutionStage,
17+
)
18+
19+
pii = PIIValidator(entities=[PIIDetectionEntity(PIIDetectionEntityType.EMAIL)])
20+
21+
# Applied to a factory function (LangChain adapter must be imported first):
22+
@guardrail(validator=pii, action=LogAction(), name="LLM PII")
23+
def create_llm():
24+
...
25+
"""
26+
27+
from ._actions import BlockAction, LogAction, LoggingSeverityLevel
28+
from ._enums import GuardrailExecutionStage, PIIDetectionEntityType
29+
from ._exceptions import GuardrailBlockException
30+
from ._guardrail import guardrail
31+
from ._models import GuardrailAction, PIIDetectionEntity
32+
from ._registry import GuardrailTargetAdapter, register_guardrail_adapter
33+
from .validators import (
34+
CustomValidator,
35+
GuardrailValidatorBase,
36+
PIIValidator,
37+
PromptInjectionValidator,
38+
RuleFunction,
39+
)
40+
41+
__all__ = [
42+
# Decorator
43+
"guardrail",
44+
# Validators
45+
"GuardrailValidatorBase",
46+
"PIIValidator",
47+
"PromptInjectionValidator",
48+
"CustomValidator",
49+
"RuleFunction",
50+
# Models & enums
51+
"PIIDetectionEntity",
52+
"PIIDetectionEntityType",
53+
"GuardrailExecutionStage",
54+
"GuardrailAction",
55+
# Actions
56+
"LogAction",
57+
"BlockAction",
58+
"LoggingSeverityLevel",
59+
# Exception
60+
"GuardrailBlockException",
61+
# Adapter registry
62+
"GuardrailTargetAdapter",
63+
"register_guardrail_adapter",
64+
]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Built-in GuardrailAction implementations."""
2+
3+
import logging
4+
from dataclasses import dataclass
5+
from enum import Enum
6+
from typing import Any, Optional
7+
8+
from uipath.core.guardrails import (
9+
GuardrailValidationResult,
10+
GuardrailValidationResultType,
11+
)
12+
13+
from ._exceptions import GuardrailBlockException
14+
from ._models import GuardrailAction
15+
16+
17+
class LoggingSeverityLevel(int, Enum):
18+
"""Logging severity level for :class:`LogAction`."""
19+
20+
ERROR = logging.ERROR
21+
INFO = logging.INFO
22+
WARNING = logging.WARNING
23+
DEBUG = logging.DEBUG
24+
25+
26+
@dataclass
27+
class LogAction(GuardrailAction):
28+
"""Log guardrail violations without stopping execution.
29+
30+
Args:
31+
severity_level: Python logging level. Defaults to ``WARNING``.
32+
message: Custom log message. If omitted, the validation reason is used.
33+
"""
34+
35+
severity_level: LoggingSeverityLevel = LoggingSeverityLevel.WARNING
36+
message: Optional[str] = None
37+
38+
def handle_validation_result(
39+
self,
40+
result: GuardrailValidationResult,
41+
data: str | dict[str, Any],
42+
guardrail_name: str,
43+
) -> str | dict[str, Any] | None:
44+
"""Log the violation and return ``None`` (no data modification)."""
45+
if result.result == GuardrailValidationResultType.VALIDATION_FAILED:
46+
msg = self.message or f"Failed: {result.reason}"
47+
logging.getLogger(__name__).log(
48+
self.severity_level,
49+
"[GUARDRAIL] [%s] %s",
50+
guardrail_name,
51+
msg,
52+
)
53+
return None
54+
55+
56+
@dataclass
57+
class BlockAction(GuardrailAction):
58+
"""Block execution by raising :class:`GuardrailBlockException`.
59+
60+
Framework adapters catch ``GuardrailBlockException`` at the wrapper boundary
61+
and convert it to their own runtime error type.
62+
63+
Args:
64+
title: Exception title. Defaults to a message derived from the guardrail name.
65+
detail: Exception detail. Defaults to the validation reason.
66+
"""
67+
68+
title: Optional[str] = None
69+
detail: Optional[str] = None
70+
71+
def handle_validation_result(
72+
self,
73+
result: GuardrailValidationResult,
74+
data: str | dict[str, Any],
75+
guardrail_name: str,
76+
) -> str | dict[str, Any] | None:
77+
"""Raise :class:`GuardrailBlockException` when validation fails."""
78+
if result.result == GuardrailValidationResultType.VALIDATION_FAILED:
79+
title = self.title or f"Guardrail [{guardrail_name}] blocked execution"
80+
detail = self.detail or result.reason or "Guardrail validation failed"
81+
raise GuardrailBlockException(title=title, detail=detail)
82+
return None
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Core framework-agnostic utilities for guardrail decorators."""
2+
3+
import ast
4+
import json
5+
import logging
6+
from typing import Any, Callable
7+
8+
from uipath.core.guardrails import GuardrailValidationResult
9+
10+
from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail
11+
12+
from ._enums import GuardrailExecutionStage
13+
14+
logger = logging.getLogger(__name__)
15+
16+
# ---------------------------------------------------------------------------
17+
# Evaluator type alias
18+
# ---------------------------------------------------------------------------
19+
20+
_EvaluatorFn = Callable[
21+
[
22+
"str | dict[str, Any]", # data
23+
GuardrailExecutionStage, # stage
24+
"dict[str, Any] | None", # input_data
25+
"dict[str, Any] | None", # output_data
26+
],
27+
GuardrailValidationResult,
28+
]
29+
"""Type alias for the unified evaluation callable used by all wrappers."""
30+
31+
32+
# ---------------------------------------------------------------------------
33+
# Evaluator factory
34+
# ---------------------------------------------------------------------------
35+
36+
37+
def _make_evaluator(
38+
validator: Any,
39+
built_in_guardrail: BuiltInValidatorGuardrail | None,
40+
) -> _EvaluatorFn:
41+
"""Return a unified evaluation callable.
42+
43+
If *built_in_guardrail* is provided the callable hits the UiPath API (lazily
44+
initializing ``UiPath()``). Otherwise, it delegates to ``validator.evaluate()``.
45+
46+
Args:
47+
validator: :class:`GuardrailValidatorBase` instance for local evaluation.
48+
built_in_guardrail: Pre-built ``BuiltInValidatorGuardrail``, or ``None``.
49+
50+
Returns:
51+
Callable with signature ``(data, stage, input_data, output_data)``.
52+
"""
53+
if built_in_guardrail is not None:
54+
_uipath_holder: list[Any] = []
55+
56+
def _api_eval(
57+
data: str | dict[str, Any],
58+
stage: GuardrailExecutionStage,
59+
input_data: dict[str, Any] | None,
60+
output_data: dict[str, Any] | None,
61+
) -> GuardrailValidationResult:
62+
if not _uipath_holder:
63+
from uipath.platform import UiPath
64+
65+
_uipath_holder.append(UiPath())
66+
return _uipath_holder[0].guardrails.evaluate_guardrail(
67+
data, built_in_guardrail
68+
)
69+
70+
return _api_eval
71+
72+
def _local_eval(
73+
data: str | dict[str, Any],
74+
stage: GuardrailExecutionStage,
75+
input_data: dict[str, Any] | None,
76+
output_data: dict[str, Any] | None,
77+
) -> GuardrailValidationResult:
78+
return validator.evaluate(data, stage, input_data, output_data)
79+
80+
return _local_eval
81+
82+
83+
# ---------------------------------------------------------------------------
84+
# Guardrail evaluation helpers
85+
# ---------------------------------------------------------------------------
86+
87+
88+
# ---------------------------------------------------------------------------
89+
# Tool I/O normalisation helpers
90+
# ---------------------------------------------------------------------------
91+
92+
93+
def _is_tool_call_envelope(tool_input: Any) -> bool:
94+
"""Return ``True`` if *tool_input* is a LangGraph tool-call envelope dict."""
95+
return (
96+
isinstance(tool_input, dict)
97+
and "args" in tool_input
98+
and tool_input.get("type") == "tool_call"
99+
)
100+
101+
102+
def _extract_input(tool_input: Any) -> dict[str, Any]:
103+
"""Normalise tool input to a plain dict for rule / guardrail evaluation.
104+
105+
LangGraph wraps tool inputs as ``{"name": ..., "args": {...}, "type": "tool_call"}``.
106+
This function unwraps ``args`` so rules can access the actual tool arguments.
107+
"""
108+
if _is_tool_call_envelope(tool_input):
109+
args = tool_input["args"]
110+
if isinstance(args, dict):
111+
return args
112+
if isinstance(tool_input, dict):
113+
return tool_input
114+
return {"input": tool_input}
115+
116+
117+
def _rewrap_input(original_tool_input: Any, modified_args: dict[str, Any]) -> Any:
118+
"""Re-wrap modified args back into the original tool-call envelope (if applicable)."""
119+
if _is_tool_call_envelope(original_tool_input):
120+
import copy
121+
122+
wrapped = copy.copy(original_tool_input)
123+
wrapped["args"] = modified_args
124+
return wrapped
125+
return modified_args
126+
127+
128+
def _extract_output(result: Any) -> dict[str, Any]:
129+
"""Normalise tool output to a dict for guardrail / rule evaluation.
130+
131+
This is the framework-agnostic version. Framework adapters that deal with
132+
framework-specific envelope types (e.g. LangGraph ``ToolMessage`` /
133+
``Command``) should pre-process the result before calling this function.
134+
135+
Falls back to ``{"output": content}`` for plain strings and anything else.
136+
"""
137+
if isinstance(result, dict):
138+
return result
139+
if isinstance(result, str):
140+
try:
141+
parsed = json.loads(result)
142+
return parsed if isinstance(parsed, dict) else {"output": parsed}
143+
except ValueError:
144+
try:
145+
parsed = ast.literal_eval(result)
146+
return parsed if isinstance(parsed, dict) else {"output": parsed}
147+
except (ValueError, SyntaxError):
148+
return {"output": result}
149+
return {"output": result}

0 commit comments

Comments
 (0)