Skip to content

Commit 9d15e37

Browse files
feat: add entitlements support for guardrails (#1092)
1 parent b0e1a82 commit 9d15e37

6 files changed

Lines changed: 248 additions & 6 deletions

File tree

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"
3-
version = "2.4.12"
3+
version = "2.4.13"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/platform/guardrails/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
BuiltInValidatorGuardrail,
1818
EnumListParameterValue,
1919
GuardrailType,
20+
GuardrailValidationResultType,
2021
MapEnumParameterValue,
2122
)
2223

2324
__all__ = [
2425
"GuardrailsService",
2526
"BuiltInValidatorGuardrail",
2627
"GuardrailType",
28+
"GuardrailValidationResultType",
2729
"BaseGuardrail",
2830
"GuardrailScope",
2931
"DeterministicGuardrail",

src/uipath/platform/guardrails/_guardrails_service.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ..._utils import Endpoint, RequestSpec
66
from ...tracing import traced
77
from ..common import BaseService, UiPathApiConfig, UiPathExecutionContext
8-
from .guardrails import BuiltInValidatorGuardrail
8+
from .guardrails import BuiltInValidatorGuardrail, GuardrailValidationResultType
99

1010

1111
class GuardrailsService(BaseService):
@@ -40,7 +40,7 @@ def evaluate_guardrail(
4040
guardrail: A guardrail instance used for validation.
4141
4242
Returns:
43-
BuiltInGuardrailValidationResult: The outcome of the guardrail evaluation, containing whether validation passed and the reason.
43+
GuardrailValidationResult: The outcome of the guardrail evaluation.
4444
"""
4545
parameters = [
4646
param.model_dump(by_alias=True) for param in guardrail.validator_parameters
@@ -61,4 +61,25 @@ def evaluate_guardrail(
6161
json=spec.json,
6262
headers=spec.headers,
6363
)
64-
return GuardrailValidationResult.model_validate(response.json())
64+
response_data = response.json()
65+
66+
# Map API response to populate result enum and details field
67+
# Handle skip case for entitlements checks
68+
skip = response_data.get("skip", False)
69+
validation_passed = response_data.get("validation_passed", False)
70+
reason = response_data.get("reason", "")
71+
72+
# Determine result enum value based on skip and validation_passed
73+
if skip:
74+
result = GuardrailValidationResultType.SKIPPED
75+
elif validation_passed:
76+
result = GuardrailValidationResultType.PASSED
77+
else:
78+
result = GuardrailValidationResultType.FAILED
79+
80+
# Add result and details to response data
81+
# Convert enum to string value for JSON serialization
82+
response_data["result"] = result.value
83+
response_data["details"] = reason
84+
85+
return GuardrailValidationResult.model_validate(response_data)

src/uipath/platform/guardrails/guardrails.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,11 @@ class GuardrailType(str, Enum):
6060

6161
BUILT_IN_VALIDATOR = "builtInValidator"
6262
CUSTOM = "custom"
63+
64+
65+
class GuardrailValidationResultType(str, Enum):
66+
"""Guardrail validation result type enumeration."""
67+
68+
PASSED = "passed"
69+
FAILED = "failed"
70+
SKIPPED = "skipped"

tests/sdk/services/test_guardrails_service.py

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import json
2+
3+
import httpx
14
import pytest
25
from pytest_httpx import HTTPXMock
36
from uipath.core.guardrails import (
@@ -43,7 +46,11 @@ def test_evaluate_guardrail_validation(
4346
httpx_mock.add_response(
4447
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
4548
status_code=200,
46-
json={"validation_passed": True, "reason": "Validation passed"},
49+
json={
50+
"validation_passed": True,
51+
"reason": "Validation passed",
52+
"skip": False,
53+
},
4754
)
4855

4956
# Create a PII detection guardrail
@@ -77,6 +84,11 @@ def test_evaluate_guardrail_validation(
7784

7885
assert result.validation_passed is True
7986
assert result.reason == "Validation passed"
87+
# If skip field exists, new API is deployed - check result and details
88+
if hasattr(result, "skip"):
89+
assert result.skip is False
90+
assert result.result == "passed"
91+
assert result.details == "Validation passed"
8092

8193
def test_evaluate_guardrail_validation_failed(
8294
self,
@@ -93,6 +105,7 @@ def test_evaluate_guardrail_validation_failed(
93105
json={
94106
"validation_passed": False,
95107
"reason": "PII detected: Email found",
108+
"skip": False,
96109
},
97110
)
98111

@@ -115,3 +128,201 @@ def test_evaluate_guardrail_validation_failed(
115128

116129
assert result.validation_passed is False
117130
assert result.reason == "PII detected: Email found"
131+
# If skip field exists, new API is deployed - check result and details
132+
if hasattr(result, "skip"):
133+
assert result.skip is False
134+
assert result.result == "failed"
135+
assert result.details == "PII detected: Email found"
136+
137+
def test_evaluate_guardrail_entitlements_skip(
138+
self,
139+
httpx_mock: HTTPXMock,
140+
service: GuardrailsService,
141+
base_url: str,
142+
org: str,
143+
tenant: str,
144+
) -> None:
145+
# Mock API response for entitlements check - feature disabled
146+
httpx_mock.add_response(
147+
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
148+
status_code=200,
149+
json={
150+
"validation_passed": True,
151+
"reason": "Guardrail feature is disabled",
152+
"skip": True,
153+
},
154+
)
155+
156+
pii_guardrail = BuiltInValidatorGuardrail(
157+
id="test-id",
158+
name="PII detection guardrail",
159+
description="Test PII detection",
160+
enabled_for_evals=True,
161+
selector=GuardrailSelector(
162+
scopes=[GuardrailScope.TOOL], match_names=["StringToNumber"]
163+
),
164+
guardrail_type="builtInValidator",
165+
validator_type="pii_detection",
166+
validator_parameters=[],
167+
)
168+
169+
test_input = "Contact me at john@example.com"
170+
171+
result = service.evaluate_guardrail(test_input, pii_guardrail)
172+
173+
assert result.validation_passed is True
174+
assert result.reason == "Guardrail feature is disabled"
175+
# If skip field exists, new API is deployed - check result and details
176+
if hasattr(result, "skip"):
177+
assert result.skip is True
178+
assert result.result == "skipped"
179+
assert result.details == "Guardrail feature is disabled"
180+
181+
def test_evaluate_guardrail_entitlements_missing(
182+
self,
183+
httpx_mock: HTTPXMock,
184+
service: GuardrailsService,
185+
base_url: str,
186+
org: str,
187+
tenant: str,
188+
) -> None:
189+
# Mock API response for entitlements check - entitlement missing
190+
httpx_mock.add_response(
191+
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
192+
status_code=200,
193+
json={
194+
"validation_passed": True,
195+
"reason": "Guardrail entitlement is missing",
196+
"skip": True,
197+
},
198+
)
199+
200+
pii_guardrail = BuiltInValidatorGuardrail(
201+
id="test-id",
202+
name="PII detection guardrail",
203+
description="Test PII detection",
204+
enabled_for_evals=True,
205+
selector=GuardrailSelector(
206+
scopes=[GuardrailScope.TOOL], match_names=["StringToNumber"]
207+
),
208+
guardrail_type="builtInValidator",
209+
validator_type="pii_detection",
210+
validator_parameters=[],
211+
)
212+
213+
test_input = "Contact me at john@example.com"
214+
215+
result = service.evaluate_guardrail(test_input, pii_guardrail)
216+
217+
assert result.validation_passed is True
218+
assert result.reason == "Guardrail entitlement is missing"
219+
# If skip field exists, new API is deployed - check result and details
220+
if hasattr(result, "skip"):
221+
assert result.skip is True
222+
assert result.result == "skipped"
223+
assert result.details == "Guardrail entitlement is missing"
224+
225+
def test_evaluate_guardrail_request_payload_structure(
226+
self,
227+
httpx_mock: HTTPXMock,
228+
service: GuardrailsService,
229+
base_url: str,
230+
org: str,
231+
tenant: str,
232+
) -> None:
233+
"""Test that the request payload has the correct structure after revert."""
234+
captured_request = None
235+
236+
def capture_request(request):
237+
nonlocal captured_request
238+
captured_request = request
239+
return httpx.Response(
240+
status_code=200,
241+
json={
242+
"validation_passed": True,
243+
"reason": "Validation passed",
244+
"skip": False,
245+
},
246+
)
247+
248+
httpx_mock.add_callback(
249+
method="POST",
250+
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
251+
callback=capture_request,
252+
)
253+
254+
# Create a PII detection guardrail with parameters
255+
pii_guardrail = BuiltInValidatorGuardrail(
256+
id="test-id",
257+
name="PII detection guardrail",
258+
description="Test PII detection",
259+
enabled_for_evals=True,
260+
selector=GuardrailSelector(
261+
scopes=[GuardrailScope.TOOL], match_names=["StringToNumber"]
262+
),
263+
guardrail_type="builtInValidator",
264+
validator_type="pii_detection",
265+
validator_parameters=[
266+
EnumListParameterValue(
267+
parameter_type="enum-list",
268+
id="entities",
269+
value=["Email", "Address"],
270+
),
271+
MapEnumParameterValue(
272+
parameter_type="map-enum",
273+
id="entityThresholds",
274+
value={"Email": 1, "Address": 0.7},
275+
),
276+
],
277+
)
278+
279+
test_input = "There is no email or address here."
280+
281+
result = service.evaluate_guardrail(test_input, pii_guardrail)
282+
283+
# Verify the request was captured
284+
assert captured_request is not None
285+
286+
# Parse the request payload
287+
request_payload = json.loads(captured_request.content)
288+
289+
# Verify the payload structure matches the reverted format:
290+
# {
291+
# "validator": guardrail.validator_type,
292+
# "input": input_data,
293+
# "parameters": parameters,
294+
# }
295+
assert "validator" in request_payload
296+
assert "input" in request_payload
297+
assert "parameters" in request_payload
298+
299+
# Verify validator is a string (not an object)
300+
assert isinstance(request_payload["validator"], str)
301+
assert request_payload["validator"] == "pii_detection"
302+
303+
# Verify input is a string
304+
assert isinstance(request_payload["input"], str)
305+
assert request_payload["input"] == "There is no email or address here."
306+
307+
# Verify parameters is an array
308+
assert isinstance(request_payload["parameters"], list)
309+
assert len(request_payload["parameters"]) == 2
310+
311+
# Verify parameter structure
312+
entities_param = request_payload["parameters"][0]
313+
assert entities_param["$parameterType"] == "enum-list"
314+
assert entities_param["id"] == "entities"
315+
assert entities_param["value"] == ["Email", "Address"]
316+
317+
thresholds_param = request_payload["parameters"][1]
318+
assert thresholds_param["$parameterType"] == "map-enum"
319+
assert thresholds_param["id"] == "entityThresholds"
320+
assert thresholds_param["value"] == {"Email": 1, "Address": 0.7}
321+
322+
# Verify result fields
323+
assert result.validation_passed is True
324+
# If skip field exists, new API is deployed - check result and details
325+
if hasattr(result, "skip"):
326+
assert result.skip is False
327+
assert result.result == "passed"
328+
assert result.details == "Validation passed"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)