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.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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand All @@ -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:
Expand All @@ -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.

Expand All @@ -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(
Expand All @@ -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()),
Expand All @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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"
57 changes: 33 additions & 24 deletions packages/uipath-platform/tests/services/test_guardrails_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,16 +350,18 @@ 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,
base_url: str,
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):
Expand All @@ -376,35 +378,37 @@ 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)

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,
base_url: str,
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):
Expand Down Expand Up @@ -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",
Expand All @@ -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,
Expand Down Expand Up @@ -529,16 +537,16 @@ 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,
base_url: str,
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):
Expand All @@ -562,14 +570,15 @@ 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=[],
)

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
assert "byoConnectionId" not in request_payload

def test_evaluate_guardrail_ootb_omits_byo_validator_name(
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

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

2 changes: 1 addition & 1 deletion packages/uipath/uv.lock

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

Loading