Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.2.8"
version = "0.2.9"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..common._job_context import header_job_key
from ..common._models import Endpoint, RequestSpec
from ..errors import EnrichedException
from .guardrails import BuiltInValidatorGuardrail
from .guardrails import BYO_VALIDATOR_TYPE, BuiltInValidatorGuardrail

# x-uipath-traceparent-id header format: {version}-{trace_id}-{span_id}[-{trace_flags}]
# Based on W3C traceparent but allows 16- or 32-hex span IDs.
Expand Down Expand Up @@ -115,12 +115,18 @@ def evaluate_guardrail(
parameters = [
param.model_dump(by_alias=True) for param in guardrail.validator_parameters
]
payload = {
payload: dict[str, Any] = {
"validator": guardrail.validator_type,
"input": input_data if isinstance(input_data, str) else str(input_data),
"parameters": parameters,
"guardrailName": guardrail.name,
}
if guardrail.validator_type == BYO_VALIDATOR_TYPE:
if not guardrail.byo_validator_name:
raise ValueError(
"BYO (Bring Your Own) guardrails require byo_validator_name."
)
payload["byoValidatorName"] = guardrail.byo_validator_name
spec = RequestSpec(
method="POST",
endpoint=Endpoint("/agentsruntime_/api/execution/guardrails/validate"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ class TextListParameterValue(BaseModel):
]


#: Sentinel ``validator_type`` for Bring Your Own Guardrail (BYOG) guardrails; the
#: connector-backed configuration is referenced by ``byo_validator_name`` instead.
BYO_VALIDATOR_TYPE = "byo"


class BuiltInValidatorGuardrail(BaseGuardrail):
"""Built-in validator guardrail model."""

Expand All @@ -86,6 +91,7 @@ class BuiltInValidatorGuardrail(BaseGuardrail):
validator_parameters: list[ValidatorParameter] = Field(
default_factory=list, alias="validatorParameters"
)
byo_validator_name: str | None = Field(default=None, alias="byoValidatorName")

model_config = ConfigDict(populate_by_name=True, extra="allow")

Expand Down
167 changes: 167 additions & 0 deletions packages/uipath-platform/tests/services/test_guardrails_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,173 @@ def capture_request(request):
assert result.result == GuardrailValidationResultType.PASSED
assert result.reason == "Validation passed"

def test_evaluate_guardrail_byog_forwards_byo_validator_name(
self,
httpx_mock: HTTPXMock,
service: GuardrailsService,
base_url: str,
org: str,
tenant: str,
) -> None:
"""A BYOG guardrail forwards byoValidatorName so the guardrails service
can resolve the connector-backed configuration."""
captured_request = None

def capture_request(request):
nonlocal captured_request
captured_request = request
return httpx.Response(
status_code=200,
json={"result": "PASSED", "details": "Validation passed"},
)

httpx_mock.add_callback(
method="POST",
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
callback=capture_request,
)

# BYOG persisted shape: "byo" sentinel + byoValidatorName reference.
byog_guardrail = BuiltInValidatorGuardrail(
id="byog-id",
name="Databricks PII (BYOG)",
description="Customer-provided PII validator",
enabled_for_evals=True,
selector=GuardrailSelector(scopes=[GuardrailScope.LLM]),
guardrail_type="builtInValidator",
validator_type="byo",
byo_validator_name="my_databricks_pii",
validator_parameters=[],
)

result = service.evaluate_guardrail("some input", byog_guardrail)

assert captured_request is not None
request_payload = json.loads(captured_request.content)
assert request_payload["validator"] == "byo"
assert request_payload["byoValidatorName"] == "my_databricks_pii"
assert result.result == GuardrailValidationResultType.PASSED

def test_evaluate_guardrail_byo_without_name_raises(
self,
service: GuardrailsService,
) -> None:
"""A "byo" guardrail missing its byoValidatorName reference fails fast
rather than sending an unresolvable request."""
guardrail = BuiltInValidatorGuardrail(
id="byog-id",
name="Broken BYOG",
enabled_for_evals=True,
selector=GuardrailSelector(scopes=[GuardrailScope.LLM]),
guardrail_type="builtInValidator",
validator_type="byo",
validator_parameters=[],
)

with pytest.raises(ValueError, match="byo_validator_name"):
service.evaluate_guardrail("some input", guardrail)

def test_evaluate_guardrail_byo_validator_name_from_alias(self) -> None:
"""byoValidatorName parses into the typed field via its camelCase alias."""
guardrail = BuiltInValidatorGuardrail.model_validate(
{
"$guardrailType": "builtInValidator",
"id": "byog-id",
"name": "BYOG",
"validatorType": "byo",
"byoValidatorName": "my_databricks_pii",
"validatorParameters": [],
}
)
assert guardrail.byo_validator_name == "my_databricks_pii"

def test_evaluate_guardrail_non_byo_type_does_not_forward_name(
self,
httpx_mock: HTTPXMock,
service: GuardrailsService,
base_url: str,
org: str,
tenant: str,
) -> None:
"""byoValidatorName is only forwarded for the "byo" sentinel, never leaked
into a non-BYOG validator payload even if the field happens to be set."""
captured_request = None

def capture_request(request):
nonlocal captured_request
captured_request = request
return httpx.Response(
status_code=200,
json={"result": "PASSED", "details": "Validation passed"},
)

httpx_mock.add_callback(
method="POST",
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
callback=capture_request,
)

guardrail = BuiltInValidatorGuardrail(
id="test-id",
name="PII detection guardrail",
enabled_for_evals=True,
selector=GuardrailSelector(scopes=[GuardrailScope.LLM]),
guardrail_type="builtInValidator",
validator_type="pii_detection",
byo_validator_name="stray_name",
validator_parameters=[],
)

service.evaluate_guardrail("some input", guardrail)

assert captured_request is not None
request_payload = json.loads(captured_request.content)
assert "byoValidatorName" not in request_payload

def test_evaluate_guardrail_ootb_omits_byo_validator_name(
self,
httpx_mock: HTTPXMock,
service: GuardrailsService,
base_url: str,
org: str,
tenant: str,
) -> None:
"""OOTB validators (no byo_validator_name) keep their payload unchanged —
byoValidatorName is not sent."""
captured_request = None

def capture_request(request):
nonlocal captured_request
captured_request = request
return httpx.Response(
status_code=200,
json={"result": "PASSED", "details": "Validation passed"},
)

httpx_mock.add_callback(
method="POST",
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
callback=capture_request,
)

pii_guardrail = BuiltInValidatorGuardrail(
id="test-id",
name="PII detection guardrail",
description="Test PII detection",
enabled_for_evals=True,
selector=GuardrailSelector(scopes=[GuardrailScope.LLM]),
guardrail_type="builtInValidator",
validator_type="pii_detection",
validator_parameters=[],
)

service.evaluate_guardrail("some input", pii_guardrail)

assert captured_request is not None
request_payload = json.loads(captured_request.content)
assert "byoValidatorName" not in request_payload
assert pii_guardrail.byo_validator_name is None

def test_evaluate_guardrail_sends_trace_context_headers(
self,
httpx_mock: HTTPXMock,
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading