diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index bbf16c8ca..d973884de 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.13" +version = "0.2.14" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py index ed6c8d4dd..0f6a16209 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py @@ -17,6 +17,7 @@ from .decorators import ( BlockAction, BuiltInGuardrailValidator, + ByoValidator, CustomGuardrailValidator, CustomValidator, GuardrailAction, @@ -43,6 +44,7 @@ register_guardrail_adapter, ) from .guardrails import ( + BYO_VALIDATOR_TYPE, BuiltInValidatorGuardrail, EnumListParameterValue, GuardrailType, @@ -53,6 +55,7 @@ # Service "GuardrailsService", # Guardrail models + "BYO_VALIDATOR_TYPE", "BuiltInValidatorGuardrail", "GuardrailType", "GuardrailValidationResultType", @@ -67,6 +70,7 @@ "guardrail", "GuardrailValidatorBase", "BuiltInGuardrailValidator", + "ByoValidator", "CustomGuardrailValidator", "HarmfulContentValidator", "IntellectualPropertyValidator", diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py index 841da6660..3db6a9c4f 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py @@ -19,6 +19,7 @@ from ._registry import GuardrailTargetAdapter, register_guardrail_adapter from .validators import ( BuiltInGuardrailValidator, + ByoValidator, CustomGuardrailValidator, CustomValidator, GuardrailValidatorBase, @@ -37,6 +38,7 @@ # Validators "GuardrailValidatorBase", "BuiltInGuardrailValidator", + "ByoValidator", "CustomGuardrailValidator", "HarmfulContentValidator", "IntellectualPropertyValidator", diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py index 14cea0d4b..b2ab5e6e2 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py @@ -5,6 +5,7 @@ CustomGuardrailValidator, GuardrailValidatorBase, ) +from .byo import ByoValidator from .custom import CustomValidator, RuleFunction from .harmful_content import HarmfulContentValidator from .intellectual_property import IntellectualPropertyValidator @@ -16,6 +17,7 @@ __all__ = [ "GuardrailValidatorBase", "BuiltInGuardrailValidator", + "ByoValidator", "CustomGuardrailValidator", "HarmfulContentValidator", "IntellectualPropertyValidator", diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/byo.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/byo.py new file mode 100644 index 000000000..b89543cde --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/byo.py @@ -0,0 +1,104 @@ +"""Bring Your Own Guardrail (BYOG) validator.""" + +from typing import Sequence +from uuid import uuid4 + +from uipath.platform.guardrails.guardrails import ( + BYO_VALIDATOR_TYPE, + BuiltInValidatorGuardrail, + ValidatorParameter, +) + +from ._base import BuiltInGuardrailValidator + + +class ByoValidator(BuiltInGuardrailValidator): + """Validate data through a Bring Your Own Guardrail (BYOG) configuration. + + BYOG lets an organization plug its own safety validator (e.g. a customer + Azure Content Safety subscription, a vendor connector, or a custom + Integration Service connector) into UiPath guardrails. An admin first + creates the configuration under ``Admin -> AI Trust Layer -> Guardrails + Configurations``; this validator references it by its validator name and + (recommended) Integration Service connection id. + + Supported at all stages — BYO validator capabilities are connector-defined + and cannot be known statically, so no stage restriction is applied here. + Configuring a scope or stage the connector does not support surfaces as a + ``PROVIDER_ERROR`` at evaluation time. + + Example:: + + from uipath.platform.guardrails.decorators import ( + BlockAction, + ByoValidator, + guardrail, + ) + + byog_harmful_content = ByoValidator( + "byog-harmful-content", + connection_id="24887687-6ed1-4fe2-9b87-087ffb232682", + ) + + @guardrail(validator=byog_harmful_content, action=BlockAction()) + def summarize(text: str) -> str: + ... + + Args: + validator_name: The BYOG configuration's validator name + (``byoValidatorName``), as shown in Admin -> AI Trust Layer -> + Guardrails Configurations. + connection_id: Optional Integration Service connection id backing the + BYOG configuration. Strongly recommended: validator names are only + unique per connection, so omitting it lets the server pick the + first configuration matching the name. + parameters: Optional list of validator parameters. BYO parameter + schemas are connector-defined, so values are passed through as-is. + + Raises: + ValueError: If *validator_name* is empty or whitespace. + """ + + def __init__( + self, + validator_name: str, + *, + connection_id: str | None = None, + parameters: Sequence[ValidatorParameter] | None = None, + ) -> None: + """Initialize ByoValidator with a BYOG configuration reference.""" + if not validator_name or not validator_name.strip(): + raise ValueError("validator_name must be a non-empty string") + self.validator_name = validator_name + self.connection_id = connection_id + self.parameters = list(parameters or []) + + def get_built_in_guardrail( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + ) -> BuiltInValidatorGuardrail: + """Build a BYOG :class:`BuiltInValidatorGuardrail`. + + Args: + name: Name for the guardrail. + description: Optional description. + enabled_for_evals: Whether active in evaluation scenarios. + + Returns: + Configured :class:`BuiltInValidatorGuardrail` referencing the BYOG + configuration via ``byoValidatorName``/``byoConnectionId``. + """ + return BuiltInValidatorGuardrail( + id=str(uuid4()), + name=name, + description=description + or f"Bring Your Own Guardrail validation '{self.validator_name}'", + enabled_for_evals=enabled_for_evals, + guardrail_type="builtInValidator", + validator_type=BYO_VALIDATOR_TYPE, + validator_parameters=self.parameters, + byo_validator_name=self.validator_name, + byo_connection_id=self.connection_id, + ) diff --git a/packages/uipath-platform/tests/services/test_guardrails_decorators.py b/packages/uipath-platform/tests/services/test_guardrails_decorators.py index 29674ca09..3d75ba2c3 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_decorators.py +++ b/packages/uipath-platform/tests/services/test_guardrails_decorators.py @@ -21,6 +21,7 @@ from uipath.platform.guardrails.decorators import ( BlockAction, + ByoValidator, CustomValidator, GuardrailAction, GuardrailBlockException, @@ -1236,3 +1237,91 @@ def joke(topic: str) -> str: with pytest.raises(GuardrailBlockException): joke("cats") mock_uipath.guardrails.evaluate_guardrail.assert_called_once() + + +# --------------------------------------------------------------------------- +# ByoValidator — Bring Your Own Guardrail configuration reference +# --------------------------------------------------------------------------- + + +class TestByoValidator: + def test_empty_validator_name_raises(self): + with pytest.raises(ValueError, match="validator_name"): + ByoValidator("") + + def test_whitespace_validator_name_raises(self): + with pytest.raises(ValueError, match="validator_name"): + ByoValidator(" ") + + def test_builds_byo_guardrail_with_name_and_connection(self): + v = ByoValidator( + "byog-harmful-content", + connection_id="24887687-6ed1-4fe2-9b87-087ffb232682", + ) + g = v.get_built_in_guardrail("G", None, True) + assert g.validator_type == "byo" + assert g.byo_validator_name == "byog-harmful-content" + assert g.byo_connection_id == "24887687-6ed1-4fe2-9b87-087ffb232682" + + def test_connection_id_defaults_to_none(self): + v = ByoValidator("byog-harmful-content") + g = v.get_built_in_guardrail("G", None, True) + assert g.byo_connection_id is None + + def test_aliases_serialize_for_the_wire(self): + v = ByoValidator("byog-pii", connection_id="conn-1") + g = v.get_built_in_guardrail("G", None, True) + dumped = g.model_dump(by_alias=True) + assert dumped["validatorType"] == "byo" + assert dumped["byoValidatorName"] == "byog-pii" + assert dumped["byoConnectionId"] == "conn-1" + assert dumped["$guardrailType"] == "builtInValidator" + + def test_parameters_pass_through(self): + from uipath.platform.guardrails.guardrails import NumberParameterValue + + param = NumberParameterValue(parameter_type="number", id="threshold", value=0.7) + v = ByoValidator("byog-custom", parameters=[param]) + g = v.get_built_in_guardrail("G", None, True) + assert g.validator_parameters == [param] + + def test_parameters_default_empty(self): + v = ByoValidator("byog-custom") + g = v.get_built_in_guardrail("G", None, True) + assert g.validator_parameters == [] + + def test_default_description_includes_validator_name(self): + v = ByoValidator("byog-harmful-content") + g = v.get_built_in_guardrail("G", None, True) + assert g.description is not None + assert "byog-harmful-content" in g.description + + def test_no_stage_restriction(self): + v = ByoValidator("byog-harmful-content") + # BYO capabilities are connector-defined — all stages allowed + v.validate_stage(GuardrailExecutionStage.PRE) + v.validate_stage(GuardrailExecutionStage.POST) + + def test_selector_is_none(self): + v = ByoValidator("byog-harmful-content") + g = v.get_built_in_guardrail("G", None, True) + assert g.selector is None + + def test_run_forwards_byo_guardrail_to_service(self): + v = ByoValidator("byog-harmful-content", connection_id="conn-1") + mock_uipath = MagicMock() + mock_uipath.guardrails.evaluate_guardrail.return_value = ( + GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, reason="" + ) + ) + with patch("uipath.platform.UiPath", return_value=mock_uipath): + result = v.run( + "G", None, True, "some input", GuardrailExecutionStage.PRE, None, None + ) + assert result.result == GuardrailValidationResultType.PASSED + data, g = mock_uipath.guardrails.evaluate_guardrail.call_args[0] + assert data == "some input" + assert g.validator_type == "byo" + assert g.byo_validator_name == "byog-harmful-content" + assert g.byo_connection_id == "conn-1" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 295f77671..80451fb6f 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.13" +version = "0.2.14" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index ab4fcb07b..9c6d369c2 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2760,7 +2760,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.13" +version = "0.2.14" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },