Skip to content

Commit abd5a4b

Browse files
Valentina BojanclaudeCopilot
committed
feat(guardrails): support BYOG evaluation via byoValidatorName
Bring-Your-Own-Guardrail (BYOG) guardrails are represented as a BuiltInValidatorGuardrail with the "byo" sentinel validator_type and a byoValidatorName referencing the connector-backed configuration. Add the typed field and forward byoValidatorName in the evaluation request so the guardrails service can resolve and dispatch to the connector, and fail fast when a "byo" guardrail is missing the reference. Built-in validators are unchanged. Bumps uipath-platform to 0.2.9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 03a6dec commit abd5a4b

6 files changed

Lines changed: 186 additions & 7 deletions

File tree

packages/uipath-platform/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-platform"
3-
version = "0.2.8"
3+
version = "0.2.9"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from ..common._job_context import header_job_key
1818
from ..common._models import Endpoint, RequestSpec
1919
from ..errors import EnrichedException
20-
from .guardrails import BuiltInValidatorGuardrail
20+
from .guardrails import BYO_VALIDATOR_TYPE, BuiltInValidatorGuardrail
2121

2222
# x-uipath-traceparent-id header format: {version}-{trace_id}-{span_id}[-{trace_flags}]
2323
# Based on W3C traceparent but allows 16- or 32-hex span IDs.
@@ -115,12 +115,18 @@ def evaluate_guardrail(
115115
parameters = [
116116
param.model_dump(by_alias=True) for param in guardrail.validator_parameters
117117
]
118-
payload = {
118+
payload: dict[str, Any] = {
119119
"validator": guardrail.validator_type,
120120
"input": input_data if isinstance(input_data, str) else str(input_data),
121121
"parameters": parameters,
122122
"guardrailName": guardrail.name,
123123
}
124+
if guardrail.validator_type == BYO_VALIDATOR_TYPE:
125+
if not guardrail.byo_validator_name:
126+
raise ValueError(
127+
"BYO (Bring Your Own) guardrails require byo_validator_name."
128+
)
129+
payload["byoValidatorName"] = guardrail.byo_validator_name
124130
spec = RequestSpec(
125131
method="POST",
126132
endpoint=Endpoint("/agentsruntime_/api/execution/guardrails/validate"),

packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ class TextListParameterValue(BaseModel):
7878
]
7979

8080

81+
#: Sentinel ``validator_type`` for Bring Your Own Guardrail (BYOG) guardrails; the
82+
#: connector-backed configuration is referenced by ``byo_validator_name`` instead.
83+
BYO_VALIDATOR_TYPE = "byo"
84+
85+
8186
class BuiltInValidatorGuardrail(BaseGuardrail):
8287
"""Built-in validator guardrail model."""
8388

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

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

packages/uipath-platform/tests/services/test_guardrails_service.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,173 @@ def capture_request(request):
303303
assert result.result == GuardrailValidationResultType.PASSED
304304
assert result.reason == "Validation passed"
305305

306+
def test_evaluate_guardrail_byog_forwards_byo_validator_name(
307+
self,
308+
httpx_mock: HTTPXMock,
309+
service: GuardrailsService,
310+
base_url: str,
311+
org: str,
312+
tenant: str,
313+
) -> None:
314+
"""A BYOG guardrail forwards byoValidatorName so the guardrails service
315+
can resolve the connector-backed configuration."""
316+
captured_request = None
317+
318+
def capture_request(request):
319+
nonlocal captured_request
320+
captured_request = request
321+
return httpx.Response(
322+
status_code=200,
323+
json={"result": "PASSED", "details": "Validation passed"},
324+
)
325+
326+
httpx_mock.add_callback(
327+
method="POST",
328+
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
329+
callback=capture_request,
330+
)
331+
332+
# BYOG persisted shape: "byo" sentinel + byoValidatorName reference.
333+
byog_guardrail = BuiltInValidatorGuardrail(
334+
id="byog-id",
335+
name="Databricks PII (BYOG)",
336+
description="Customer-provided PII validator",
337+
enabled_for_evals=True,
338+
selector=GuardrailSelector(scopes=[GuardrailScope.LLM]),
339+
guardrail_type="builtInValidator",
340+
validator_type="byo",
341+
byo_validator_name="my_databricks_pii",
342+
validator_parameters=[],
343+
)
344+
345+
result = service.evaluate_guardrail("some input", byog_guardrail)
346+
347+
assert captured_request is not None
348+
request_payload = json.loads(captured_request.content)
349+
assert request_payload["validator"] == "byo"
350+
assert request_payload["byoValidatorName"] == "my_databricks_pii"
351+
assert result.result == GuardrailValidationResultType.PASSED
352+
353+
def test_evaluate_guardrail_byo_without_name_raises(
354+
self,
355+
service: GuardrailsService,
356+
) -> None:
357+
"""A "byo" guardrail missing its byoValidatorName reference fails fast
358+
rather than sending an unresolvable request."""
359+
guardrail = BuiltInValidatorGuardrail(
360+
id="byog-id",
361+
name="Broken BYOG",
362+
enabled_for_evals=True,
363+
selector=GuardrailSelector(scopes=[GuardrailScope.LLM]),
364+
guardrail_type="builtInValidator",
365+
validator_type="byo",
366+
validator_parameters=[],
367+
)
368+
369+
with pytest.raises(ValueError, match="byo_validator_name"):
370+
service.evaluate_guardrail("some input", guardrail)
371+
372+
def test_evaluate_guardrail_byo_validator_name_from_alias(self) -> None:
373+
"""byoValidatorName parses into the typed field via its camelCase alias."""
374+
guardrail = BuiltInValidatorGuardrail.model_validate(
375+
{
376+
"$guardrailType": "builtInValidator",
377+
"id": "byog-id",
378+
"name": "BYOG",
379+
"validatorType": "byo",
380+
"byoValidatorName": "my_databricks_pii",
381+
"validatorParameters": [],
382+
}
383+
)
384+
assert guardrail.byo_validator_name == "my_databricks_pii"
385+
386+
def test_evaluate_guardrail_non_byo_type_does_not_forward_name(
387+
self,
388+
httpx_mock: HTTPXMock,
389+
service: GuardrailsService,
390+
base_url: str,
391+
org: str,
392+
tenant: str,
393+
) -> None:
394+
"""byoValidatorName is only forwarded for the "byo" sentinel, never leaked
395+
into a non-BYOG validator payload even if the field happens to be set."""
396+
captured_request = None
397+
398+
def capture_request(request):
399+
nonlocal captured_request
400+
captured_request = request
401+
return httpx.Response(
402+
status_code=200,
403+
json={"result": "PASSED", "details": "Validation passed"},
404+
)
405+
406+
httpx_mock.add_callback(
407+
method="POST",
408+
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
409+
callback=capture_request,
410+
)
411+
412+
guardrail = BuiltInValidatorGuardrail(
413+
id="test-id",
414+
name="PII detection guardrail",
415+
enabled_for_evals=True,
416+
selector=GuardrailSelector(scopes=[GuardrailScope.LLM]),
417+
guardrail_type="builtInValidator",
418+
validator_type="pii_detection",
419+
byo_validator_name="stray_name",
420+
validator_parameters=[],
421+
)
422+
423+
service.evaluate_guardrail("some input", guardrail)
424+
425+
assert captured_request is not None
426+
request_payload = json.loads(captured_request.content)
427+
assert "byoValidatorName" not in request_payload
428+
429+
def test_evaluate_guardrail_ootb_omits_byo_validator_name(
430+
self,
431+
httpx_mock: HTTPXMock,
432+
service: GuardrailsService,
433+
base_url: str,
434+
org: str,
435+
tenant: str,
436+
) -> None:
437+
"""OOTB validators (no byo_validator_name) keep their payload unchanged —
438+
byoValidatorName is not sent."""
439+
captured_request = None
440+
441+
def capture_request(request):
442+
nonlocal captured_request
443+
captured_request = request
444+
return httpx.Response(
445+
status_code=200,
446+
json={"result": "PASSED", "details": "Validation passed"},
447+
)
448+
449+
httpx_mock.add_callback(
450+
method="POST",
451+
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
452+
callback=capture_request,
453+
)
454+
455+
pii_guardrail = BuiltInValidatorGuardrail(
456+
id="test-id",
457+
name="PII detection guardrail",
458+
description="Test PII detection",
459+
enabled_for_evals=True,
460+
selector=GuardrailSelector(scopes=[GuardrailScope.LLM]),
461+
guardrail_type="builtInValidator",
462+
validator_type="pii_detection",
463+
validator_parameters=[],
464+
)
465+
466+
service.evaluate_guardrail("some input", pii_guardrail)
467+
468+
assert captured_request is not None
469+
request_payload = json.loads(captured_request.content)
470+
assert "byoValidatorName" not in request_payload
471+
assert pii_guardrail.byo_validator_name is None
472+
306473
def test_evaluate_guardrail_sends_trace_context_headers(
307474
self,
308475
httpx_mock: HTTPXMock,

packages/uipath-platform/uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uipath/uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)