From 6c73ca809cbacadcdf98f76df2e1bbfca3f68504 Mon Sep 17 00:00:00 2001 From: Andrei Petraru Date: Tue, 13 Jan 2026 12:45:17 +0200 Subject: [PATCH] feat: add compatibility for guardrails entitlements --- pyproject.toml | 2 +- .../agent/guardrails/guardrail_nodes.py | 22 ++++++++++- tests/cli/conftest.py | 10 ++++- tests/cli/test_agent_with_guardrails.py | 38 +++++++++++++------ uv.lock | 9 +++-- 5 files changed, 61 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 36fadd0b..4bbb4cf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.4.0" +version = "0.4.1" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py index dc843d07..72c49975 100644 --- a/src/uipath_langchain/agent/guardrails/guardrail_nodes.py +++ b/src/uipath_langchain/agent/guardrails/guardrail_nodes.py @@ -100,10 +100,28 @@ def _create_validation_command( Returns: Command to update state and route to appropriate node. """ - if not result.validation_passed: + from uipath.core.guardrails import GuardrailValidationResultType + + # Handle new format: result has a 'result' attribute with GuardrailValidationResultType + if hasattr(result, "result"): + if result.result != GuardrailValidationResultType.PASSED: + return Command( + goto=failure_node, update={"guardrail_validation_result": result.reason} + ) + # Handle old format: backwards compatibility for tests using validation_passed + elif hasattr(result, "validation_passed"): + if not result.validation_passed: + return Command( + goto=failure_node, update={"guardrail_validation_result": result.reason} + ) + else: + # Fallback: assume failure if format is unknown return Command( - goto=failure_node, update={"guardrail_validation_result": result.reason} + goto=failure_node, + update={"guardrail_validation_result": "Unknown validation result format"}, ) + + # Update guardail trace with skip/reason return Command(goto=success_node, update={"guardrail_validation_result": None}) diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 1ae3fb99..a9eb7799 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -1,7 +1,10 @@ from unittest.mock import patch import pytest -from uipath.core.guardrails import GuardrailValidationResult +from uipath.core.guardrails import ( + GuardrailValidationResult, + GuardrailValidationResultType, +) @pytest.fixture @@ -19,7 +22,10 @@ def mock_guardrails_service(): def mock_evaluate_guardrail(text, guardrail): """Mock guardrail evaluation - always passes validation.""" - return GuardrailValidationResult(validation_passed=True, reason="") + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="", + ) with patch( "uipath.platform.guardrails.GuardrailsService.evaluate_guardrail", diff --git a/tests/cli/test_agent_with_guardrails.py b/tests/cli/test_agent_with_guardrails.py index 7cb8c0ac..7e49ba46 100644 --- a/tests/cli/test_agent_with_guardrails.py +++ b/tests/cli/test_agent_with_guardrails.py @@ -23,7 +23,10 @@ import pytest from langchain_core.messages import AIMessage -from uipath.core.guardrails import GuardrailValidationResult +from uipath.core.guardrails import ( + GuardrailValidationResult, + GuardrailValidationResultType, +) from uipath.runtime import ( UiPathExecuteOptions, UiPathRuntimeContext, @@ -288,12 +291,13 @@ def mock_evaluate_guardrail(text, guardrail): and ".com" in text ): return GuardrailValidationResult( - validation_passed=False, # PII detected - should trigger block! + result=GuardrailValidationResultType.VALIDATION_FAILED, reason="PII detected in text", ) else: return GuardrailValidationResult( - validation_passed=True, reason="" + result=GuardrailValidationResultType.PASSED, + reason="", ) with ( @@ -422,12 +426,15 @@ def mock_evaluate_guardrail(text, guardrail): # Prompt injection guardrail should detect and block if guardrail.name == "Prompt injection guardrail": return GuardrailValidationResult( - validation_passed=False, + result=GuardrailValidationResultType.VALIDATION_FAILED, reason="Prompt injection detected", ) # All other guardrails pass - return GuardrailValidationResult(validation_passed=True, reason="") + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="", + ) # Mock LLM - should NOT be called if guardrail blocks at LLM level async def mock_llm_invoke(*args, **kwargs): @@ -867,12 +874,15 @@ def mock_evaluate_guardrail(text, guardrail): and ".com" in text ): return GuardrailValidationResult( - validation_passed=False, + result=GuardrailValidationResultType.VALIDATION_FAILED, reason="PII detected in tool input", ) # All other guardrails pass - return GuardrailValidationResult(validation_passed=True, reason="") + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="", + ) # Mock LLM responses - tool call contains an email address async def mock_llm_invoke(*args, **kwargs): @@ -1025,12 +1035,15 @@ def mock_evaluate_guardrail(text, guardrail): and ".com" in text ): return GuardrailValidationResult( - validation_passed=False, + result=GuardrailValidationResultType.VALIDATION_FAILED, reason="PII detected in LLM output", ) # All other guardrails pass - return GuardrailValidationResult(validation_passed=True, reason="") + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="", + ) # Mock LLM responses - LLM output contains PII in tool call args async def mock_llm_invoke(*args, **kwargs): @@ -1228,12 +1241,15 @@ def mock_evaluate_guardrail(text, guardrail): and ".com" in text ): return GuardrailValidationResult( - validation_passed=False, + result=GuardrailValidationResultType.VALIDATION_FAILED, reason="PII detected in LLM output", ) # All other guardrails pass - return GuardrailValidationResult(validation_passed=True, reason="") + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="", + ) # Mock LLM responses - LLM output contains PII in tool call args async def mock_llm_invoke(*args, **kwargs): diff --git a/uv.lock b/uv.lock index 25410f43..84e529b2 100644 --- a/uv.lock +++ b/uv.lock @@ -3274,25 +3274,26 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/4c/62/de6137c448a4436876e0e57c3cbd08718a7b45c998bdb9e81865ce0c082c/uipath-2.5.tar.gz", hash = "sha256:6ef338d27763e03098d65318fbf126f8a0010f5aa07136ee0486a8283e951ad1", size = 3883309, upload-time = "2026-01-15T07:02:16.8Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c3/63/8254f1941351b3ee9386d0b073fb3c033e7de9c67ba588a47d5511506e17/uipath-2.5-py3-none-any.whl", hash = "sha256:19f6b5d7408e32e0a9df869dc041a158919758288fdd26bbfe337b9dea12373c", size = 437244, upload-time = "2026-01-15T07:02:14.911Z" }, + { url = "https://files.pythonhosted.org/packages/88/a4/1c81dbee07c01de70281e591c368f832afea956f2e2be7596355716709c8/uipath-2.5.0-py3-none-any.whl", hash = "sha256:7cd24c5048873dd8aaacdb12ec1722be872db9369d410b73ed77d1ce6dfc8e95", size = 437268, upload-time = "2026-01-15T08:39:47.556Z" }, ] [[package]] name = "uipath-core" -version = "0.1.4" +version = "0.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/ef/44b9b0adb378e0e988b621b72af55008dbfb166179412cba1fe54ab4b692/uipath_core-0.1.4.tar.gz", hash = "sha256:6100eb5299b30b145e557e3dbc716141bbaa92cd37633d36257c7e3f90ce578f", size = 96025, upload-time = "2025-12-16T14:25:01.62Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/da/842e9cbc70ce396fa8369803174d6ce58f9c85d1c9b57e1b96440d86ed77/uipath_core-0.1.6.tar.gz", hash = "sha256:0d1e2f305326d58e655c236bdd2755b6855cbeed2a8acf61e30bab3d056c83dd", size = 97409, upload-time = "2026-01-15T08:10:07.806Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/c3/e64ea37ba8aa56cfae4a15589652a9dc4f66889e4d19788ca5e1b034b46f/uipath_core-0.1.4-py3-none-any.whl", hash = "sha256:574d6fe0314f70c12de8b6a3c5ab05a6191f6a8b9087c1d1a6352e67765f2f72", size = 30431, upload-time = "2025-12-16T14:25:00.174Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2f/611e5443d0bb4b570768db60c80df8a9fba197205a646ca2c0b67b3f236d/uipath_core-0.1.6-py3-none-any.whl", hash = "sha256:fb514a9b15d57ea9464e62c04f83e86e1b21cb0865012ecca947d2a458805a4a", size = 31238, upload-time = "2026-01-15T08:10:06.289Z" }, ] [[package]] name = "uipath-langchain" -version = "0.4.0" +version = "0.4.1" source = { editable = "." } dependencies = [ { name = "aiosqlite" },