From 8fef0a6c233a44a7d652defde387d1886740601d Mon Sep 17 00:00:00 2001 From: Andrei Petraru Date: Sat, 10 Oct 2026 00:28:12 +0300 Subject: [PATCH] feat(guardrails): send an empty attachments list when a step has no files [AL-611] evaluate_guardrail now sends "attachments": [] when the caller passes an empty list, instead of dropping the field. The Agents backend uses this to tell "files apply but there are none to inspect" apart from "files do not apply". A files-only guardrail can then pass without checking the text, while None still omits the field and keeps the text fallback for older callers. The folder-key header and the 60 second timeout are still sent only for a non-empty list, because no file is fetched when the list is empty. Bumps uipath-platform to 0.2.36 and refreshes the platform and uipath lock files. Co-Authored-By: Claude Opus 5.5 --- packages/uipath-platform/pyproject.toml | 2 +- .../guardrails/_guardrails_service.py | 6 ++- .../tests/services/test_guardrails_service.py | 47 ++++++++++++++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index be322ea1e..4699bfeeb 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.35" +version = "0.2.36" 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 cf3381e0d..f3b87b8dd 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py @@ -119,7 +119,9 @@ def evaluate_guardrail( Args: input_data: The text or structured data to validate. Dictionaries will be converted to a string before validation. guardrail: A guardrail instance used for validation. - attachments: Files attached to the run that the guardrail may inspect. + attachments: Files the guardrail may inspect. ``None`` omits the field, so + the backend checks only the text. An empty list is sent as ``[]``, + meaning files apply but there are none to inspect. termination_mode: ``FAIL_FAST`` stops scanning at the first violation; ``EVALUATE_ALL`` or ``None`` scans everything. @@ -141,7 +143,7 @@ def evaluate_guardrail( "BYO (Bring Your Own) guardrails require byo_validator_name." ) payload["byoValidatorName"] = guardrail.byo_validator_name - if attachments: + if attachments is not None: payload["attachments"] = [a.model_dump(by_alias=True) for a in attachments] if termination_mode is not None: payload["terminationMode"] = termination_mode.value diff --git a/packages/uipath-platform/tests/services/test_guardrails_service.py b/packages/uipath-platform/tests/services/test_guardrails_service.py index ad3a93c0f..d135c5c66 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_service.py +++ b/packages/uipath-platform/tests/services/test_guardrails_service.py @@ -975,7 +975,7 @@ def test_attachments_key_is_absent_when_not_supplied( assert "attachments" not in json.loads(httpx_mock.get_requests()[0].content) - def test_attachments_key_is_absent_when_empty_list( + def test_empty_attachments_list_is_sent_as_empty_list( self, httpx_mock: HTTPXMock, service: GuardrailsService, @@ -983,6 +983,7 @@ def test_attachments_key_is_absent_when_empty_list( org: str, tenant: str, ) -> None: + """An empty list tells the backend files apply but none were found.""" httpx_mock.add_response( url=f"{base_url}{org}{tenant}{_VALIDATE_PATH}", status_code=200, @@ -991,7 +992,8 @@ def test_attachments_key_is_absent_when_empty_list( service.evaluate_guardrail("x", _judge_guardrail(), attachments=[]) - assert "attachments" not in json.loads(httpx_mock.get_requests()[0].content) + body = json.loads(httpx_mock.get_requests()[0].content) + assert body["attachments"] == [] def test_attachment_round_trips_the_wire_shape(self) -> None: """The camelCase body the API emits parses back into the model unchanged.""" @@ -1126,6 +1128,27 @@ def test_folder_header_absent_without_attachments_even_if_configured( request = httpx_mock.get_requests()[0] assert "x-uipath-folderkey" not in request.headers + def test_folder_header_absent_with_empty_attachments_even_if_configured( + self, + httpx_mock: HTTPXMock, + service: GuardrailsService, + base_url: str, + org: str, + tenant: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("UIPATH_FOLDER_KEY", "folder-key-123") + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}{_VALIDATE_PATH}", + status_code=200, + json={"result": "PASSED", "details": ""}, + ) + + service.evaluate_guardrail("x", _judge_guardrail(), attachments=[]) + + request = httpx_mock.get_requests()[0] + assert "x-uipath-folderkey" not in request.headers + class TestGuardrailAttachmentTiming: """Attachments still get the longer, file-fetching-aware timeout.""" @@ -1170,6 +1193,26 @@ def test_evaluate_guardrail_keeps_default_timeout_without_attachments( timeout = httpx_mock.get_requests()[0].extensions["timeout"] assert timeout["read"] != 60.0 + def test_evaluate_guardrail_keeps_default_timeout_with_empty_attachments( + self, + httpx_mock: HTTPXMock, + service: GuardrailsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """No file is fetched for an empty list, so it needs no longer timeout.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}{_VALIDATE_PATH}", + status_code=200, + json={"result": "PASSED", "details": ""}, + ) + + service.evaluate_guardrail("x", _judge_guardrail(), attachments=[]) + + timeout = httpx_mock.get_requests()[0].extensions["timeout"] + assert timeout["read"] != 60.0 + class TestGuardrailFlaggedAttachmentIds: """evaluate_guardrail parses the flagged attachment ids from the response.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index b02fd2c40..99070bbf4 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.35" +version = "0.2.36" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 9887fc03a..09541f1cc 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.35" +version = "0.2.36" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },