diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index f74c2bb87..eab210d34 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.16" +version = "0.2.17" 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/_guardrails_service.py b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py index fbeb1f027..b73d810e7 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py @@ -127,8 +127,6 @@ def evaluate_guardrail( "BYO (Bring Your Own) guardrails require byo_validator_name." ) payload["byoValidatorName"] = guardrail.byo_validator_name - if guardrail.byo_connection_id: - payload["byoConnectionId"] = guardrail.byo_connection_id spec = RequestSpec( method="POST", endpoint=Endpoint("/agentsruntime_/api/execution/guardrails/validate"), 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 index b89543cde..425a84654 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/byo.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/byo.py @@ -19,13 +19,13 @@ class ByoValidator(BuiltInGuardrailValidator): 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. + Configurations``; this validator references it purely by its validator + name, which is unique per tenant. The Integration Service connection to + use is resolved server-side from the configuration, so an admin rebind is + always honored. 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:: @@ -35,10 +35,7 @@ class ByoValidator(BuiltInGuardrailValidator): guardrail, ) - byog_harmful_content = ByoValidator( - "byog-harmful-content", - connection_id="24887687-6ed1-4fe2-9b87-087ffb232682", - ) + byog_harmful_content = ByoValidator("my-harmful-content-guardrail") @guardrail(validator=byog_harmful_content, action=BlockAction()) def summarize(text: str) -> str: @@ -47,11 +44,7 @@ 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. + Guardrails Configurations. Unique per tenant. parameters: Optional list of validator parameters. BYO parameter schemas are connector-defined, so values are passed through as-is. @@ -63,14 +56,12 @@ 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( @@ -88,7 +79,7 @@ def get_built_in_guardrail( Returns: Configured :class:`BuiltInValidatorGuardrail` referencing the BYOG - configuration via ``byoValidatorName``/``byoConnectionId``. + configuration via ``byoValidatorName``. """ return BuiltInValidatorGuardrail( id=str(uuid4()), @@ -100,5 +91,4 @@ def get_built_in_guardrail( 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/src/uipath/platform/guardrails/guardrails.py b/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py index c7a968a61..dace18019 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py @@ -92,7 +92,6 @@ class BuiltInValidatorGuardrail(BaseGuardrail): default_factory=list, alias="validatorParameters" ) byo_validator_name: str | None = Field(default=None, alias="byoValidatorName") - byo_connection_id: str | None = Field(default=None, alias="byoConnectionId") model_config = ConfigDict(populate_by_name=True, extra="allow") diff --git a/packages/uipath-platform/tests/services/test_guardrails_decorators.py b/packages/uipath-platform/tests/services/test_guardrails_decorators.py index 3d75ba2c3..c6cfee2ad 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_decorators.py +++ b/packages/uipath-platform/tests/services/test_guardrails_decorators.py @@ -1253,29 +1253,22 @@ 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", - ) + def test_builds_byo_guardrail_from_name_alone(self): + v = ByoValidator("my-harmful-content-guardrail") 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 + assert g.byo_validator_name == "my-harmful-content-guardrail" def test_aliases_serialize_for_the_wire(self): - v = ByoValidator("byog-pii", connection_id="conn-1") + v = ByoValidator("byog-pii") 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" + # BYOG resolves by validator name alone (unique per tenant); no + # connection id exists on the wire model. + assert "byoConnectionId" not in dumped def test_parameters_pass_through(self): from uipath.platform.guardrails.guardrails import NumberParameterValue @@ -1308,7 +1301,7 @@ def test_selector_is_none(self): assert g.selector is None def test_run_forwards_byo_guardrail_to_service(self): - v = ByoValidator("byog-harmful-content", connection_id="conn-1") + v = ByoValidator("my-harmful-content-guardrail") mock_uipath = MagicMock() mock_uipath.guardrails.evaluate_guardrail.return_value = ( GuardrailValidationResult( @@ -1323,5 +1316,4 @@ def test_run_forwards_byo_guardrail_to_service(self): 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" + assert g.byo_validator_name == "my-harmful-content-guardrail" diff --git a/packages/uipath-platform/tests/services/test_guardrails_service.py b/packages/uipath-platform/tests/services/test_guardrails_service.py index c94bf8be3..d20d531a7 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_service.py +++ b/packages/uipath-platform/tests/services/test_guardrails_service.py @@ -350,7 +350,7 @@ def capture_request(request): assert request_payload["byoValidatorName"] == "my_databricks_pii" assert result.result == GuardrailValidationResultType.PASSED - def test_evaluate_guardrail_byog_forwards_byo_connection_id( + def test_evaluate_guardrail_byog_never_forwards_legacy_connection_id( self, httpx_mock: HTTPXMock, service: GuardrailsService, @@ -358,8 +358,10 @@ def test_evaluate_guardrail_byog_forwards_byo_connection_id( org: str, tenant: str, ) -> None: - """A BYOG guardrail forwards byoConnectionId so the backend can narrow - configuration resolution to the specific connection.""" + """A legacy byoConnectionId (from persisted json or an older caller) + is never forwarded: BYOG resolves by validator name alone, which is + unique per tenant; the connection comes from the configuration + server-side.""" captured_request = None def capture_request(request): @@ -376,16 +378,18 @@ def capture_request(request): callback=capture_request, ) - byog_guardrail = BuiltInValidatorGuardrail( - id="byog-id", - name="Databricks PII (BYOG)", - enabled_for_evals=True, - selector=GuardrailSelector(scopes=[GuardrailScope.LLM]), - guardrail_type="builtInValidator", - validator_type="byo", - byo_validator_name="my_databricks_pii", - byo_connection_id="byog-conn-1", - validator_parameters=[], + byog_guardrail = BuiltInValidatorGuardrail.model_validate( + { + "$guardrailType": "builtInValidator", + "id": "byog-id", + "name": "Databricks PII (BYOG)", + "enabledForEvals": True, + "selector": {"scopes": ["Llm"]}, + "validatorType": "byo", + "byoValidatorName": "my_databricks_pii", + "byoConnectionId": "byog-conn-1", + "validatorParameters": [], + } ) service.evaluate_guardrail("some input", byog_guardrail) @@ -393,9 +397,9 @@ def capture_request(request): assert captured_request is not None request_payload = json.loads(captured_request.content) assert request_payload["byoValidatorName"] == "my_databricks_pii" - assert request_payload["byoConnectionId"] == "byog-conn-1" + assert "byoConnectionId" not in request_payload - def test_evaluate_guardrail_byog_omits_connection_id_when_absent( + def test_evaluate_guardrail_byog_resolves_by_name_alone( self, httpx_mock: HTTPXMock, service: GuardrailsService, @@ -403,8 +407,8 @@ def test_evaluate_guardrail_byog_omits_connection_id_when_absent( org: str, tenant: str, ) -> None: - """byoConnectionId is only forwarded when present; a BYOG guardrail without - one resolves by validator name alone.""" + """A BYOG guardrail carries only byoValidatorName; no connection id + appears in the payload.""" captured_request = None def capture_request(request): @@ -438,8 +442,11 @@ def capture_request(request): request_payload = json.loads(captured_request.content) assert "byoConnectionId" not in request_payload - def test_evaluate_guardrail_byo_connection_id_from_alias(self) -> None: - """byoConnectionId parses into the typed field via its camelCase alias.""" + def test_evaluate_guardrail_legacy_byo_connection_id_still_parses( + self, + ) -> None: + """Legacy json carrying byoConnectionId still parses (extra="allow"), + but the value is not a typed field anymore.""" guardrail = BuiltInValidatorGuardrail.model_validate( { "$guardrailType": "builtInValidator", @@ -451,7 +458,8 @@ def test_evaluate_guardrail_byo_connection_id_from_alias(self) -> None: "validatorParameters": [], } ) - assert guardrail.byo_connection_id == "byog-conn-1" + assert guardrail.byo_validator_name == "my_databricks_pii" + assert "byo_connection_id" not in type(guardrail).model_fields def test_evaluate_guardrail_byo_without_name_raises( self, @@ -529,7 +537,7 @@ def capture_request(request): request_payload = json.loads(captured_request.content) assert "byoValidatorName" not in request_payload - def test_evaluate_guardrail_non_byo_type_does_not_forward_connection_id( + def test_evaluate_guardrail_non_byo_type_never_forwards_byo_fields( self, httpx_mock: HTTPXMock, service: GuardrailsService, @@ -537,8 +545,8 @@ def test_evaluate_guardrail_non_byo_type_does_not_forward_connection_id( org: str, tenant: str, ) -> None: - """byoConnectionId is only forwarded for the "byo" sentinel, never leaked - into a non-BYOG validator payload even if the field happens to be set.""" + """BYO fields are only forwarded for the "byo" sentinel, never leaked + into a non-BYOG validator payload even if a name happens to be set.""" captured_request = None def capture_request(request): @@ -562,7 +570,7 @@ def capture_request(request): selector=GuardrailSelector(scopes=[GuardrailScope.LLM]), guardrail_type="builtInValidator", validator_type="pii_detection", - byo_connection_id="stray-conn", + byo_validator_name="stray-name", validator_parameters=[], ) @@ -570,6 +578,7 @@ def capture_request(request): assert captured_request is not None request_payload = json.loads(captured_request.content) + assert "byoValidatorName" not in request_payload assert "byoConnectionId" not in request_payload def test_evaluate_guardrail_ootb_omits_byo_validator_name( diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index f5331a448..2e506b0af 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.16" +version = "0.2.17" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 208b7064f..3eba8c870 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2760,7 +2760,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.16" +version = "0.2.17" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },