From 7b347b3b52bc7df7089502f75751d91b6818b320 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Fri, 3 Jul 2026 13:46:46 +0300 Subject: [PATCH 1/9] ci(platform): enforce uipath.platform layering with import-linter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an import-linter layers contract over uipath.platform, enforced as a CI step in the lint-uipath-platform job. The contract statically forbids import cycles and upward imports between domain packages — the class of bug behind the 0.1.89 identity/common incident (#1787). Type-checking imports are excluded; the contract is exhaustive so new subpackages must be assigned a layer explicitly. The contract is intentionally committed without ignores: lint-imports fails at this commit on the pre-existing upward imports from common/interrupt_models.py, fixed in the next commit. Co-Authored-By: Claude Fable 5 --- .github/workflows/lint-packages.yml | 5 + .gitignore | 1 + packages/uipath-platform/CLAUDE.md | 1 + packages/uipath-platform/pyproject.toml | 20 ++++ packages/uipath-platform/uv.lock | 122 ++++++++++++++++++++++++ 5 files changed, 149 insertions(+) diff --git a/.github/workflows/lint-packages.yml b/.github/workflows/lint-packages.yml index d530e25a9..94367f552 100644 --- a/.github/workflows/lint-packages.yml +++ b/.github/workflows/lint-packages.yml @@ -139,6 +139,11 @@ jobs: working-directory: packages/uipath-platform run: uv run ruff format --check . + - name: Check import contracts + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-platform + run: uv run lint-imports + lint-uipath: name: Lint uipath needs: detect-changed-packages diff --git a/.gitignore b/.gitignore index 6b24bbd06..fd06e77db 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ wheels/ .cache/* .cache +.import_linter_cache/ site diff --git a/packages/uipath-platform/CLAUDE.md b/packages/uipath-platform/CLAUDE.md index 21896676f..a13260c41 100644 --- a/packages/uipath-platform/CLAUDE.md +++ b/packages/uipath-platform/CLAUDE.md @@ -17,6 +17,7 @@ pytest tests/services/test_assets_service.py # Single test file ruff check . # Lint ruff format --check . # Format check mypy src tests # Type check +uv run lint-imports # Import-graph layering contract ``` No justfile exists for this package — run commands directly. diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 42178758a..9026a6159 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -47,6 +47,7 @@ dev = [ "pytest-cov>=4.1.0", "pytest-mock>=3.11.1", "pre-commit>=4.1.0", + "import-linter>=2.0", ] [tool.hatch.build.targets.wheel] @@ -126,6 +127,25 @@ exclude_lines = [ "@(abc\\.)?abstractmethod", ] +[tool.importlinter] +root_package = "uipath.platform" +exclude_type_checking_imports = true + +[[tool.importlinter.contracts]] +id = "platform-layers" +name = "uipath.platform layered architecture" +type = "layers" +containers = ["uipath.platform"] +exhaustive = true +layers = [ + "_uipath : guardrails : resume_triggers", + "agenthub : automation_ops : automation_tracker : connections : context_grounding : entities : external_applications : governance : memory : pii_detection : portal : resource_catalog : semantic_proxy", + "orchestrator", + "action_center : attachments : chat : documents : identity", + "common", + "constants : errors", +] + [tool.uv] exclude-newer = "2 days" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 802e19ac8..5806f630e 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -99,6 +99,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -230,6 +242,99 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, ] +[[package]] +name = "grimp" +version = "3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/46/79764cfb61a3ac80dadae5d94fb10acdb7800e31fecf4113cf3d345e4952/grimp-3.14.tar.gz", hash = "sha256:645fbd835983901042dae4e1b24fde3a89bf7ac152f9272dd17a97e55cb4f871", size = 830882, upload-time = "2025-12-10T17:55:01.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/31/d4a86207c38954b6c3d859a1fc740a80b04bbe6e3b8a39f4e66f9633dfa4/grimp-3.14-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f1c91e3fa48c2196bf62e3c71492140d227b2bfcd6d15e735cbc0b3e2d5308e0", size = 2185572, upload-time = "2025-12-10T17:53:41.287Z" }, + { url = "https://files.pythonhosted.org/packages/f5/61/ed4cba5bd75d37fe46e17a602f616619a9e4f74ad8adfcf560ce4b2a1697/grimp-3.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6291c8f1690a9fe21b70923c60b075f4a89676541999e3d33084cbc69ac06a1", size = 2118002, upload-time = "2025-12-10T17:53:18.546Z" }, + { url = "https://files.pythonhosted.org/packages/77/6a/688f6144d0b207d7845bd8ab403820a83630ce3c9420cbbc7c9e9282f9c0/grimp-3.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ec312383935c2d09e4085c8435780ada2e13ebef14e105609c2988a02a5b2ce", size = 2283939, upload-time = "2025-12-10T17:52:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/a5/98/4c540de151bf3fd58d6d7b3fe2269b6a6af6c61c915de1bc991802bfaff8/grimp-3.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f43cbf640e73ee703ad91639591046828d20103a1c363a02516e77a66a4ac07", size = 2233693, upload-time = "2025-12-10T17:52:18.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7b/84b4b52b6c6dd5bf083cb1a72945748f56ea2e61768bbebf87e8d9d0ef75/grimp-3.14-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a93c9fddccb9ff16f5c6b5fca44227f5f86cba7cffc145d2176119603d2d7c7", size = 2389745, upload-time = "2025-12-10T17:53:00.659Z" }, + { url = "https://files.pythonhosted.org/packages/a7/33/31b96907c7dd78953df5e1ce67c558bd6057220fa1203d28d52566315a2e/grimp-3.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5653a2769fdc062cb7598d12200352069c9c6559b6643af6ada3639edb98fcc3", size = 2569055, upload-time = "2025-12-10T17:52:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/b2/24/ce1a8110f3d5b178153b903aafe54b6a9216588b5bff3656e30af43e9c29/grimp-3.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:071c7ddf5e5bb7b2fdf79aefdf6e1c237cd81c095d6d0a19620e777e85bf103c", size = 2358044, upload-time = "2025-12-10T17:52:47.545Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/16d98c02287bc99884843478b9a68b04a2ef13b5cb8b9f36a9ca7daea75b/grimp-3.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e01b7a4419f535b667dfdcb556d3815b52981474f791fb40d72607228389a31", size = 2310304, upload-time = "2025-12-10T17:53:09.679Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/0fde9781b0f6b4f9227d485685f48f6bcc70b95af22e2f85ff7f416cbfc1/grimp-3.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c29682f336151d1d018d0c3aa9eeaa35734b970e4593fa396b901edca7ef5c79", size = 2463682, upload-time = "2025-12-10T17:53:49.185Z" }, + { url = "https://files.pythonhosted.org/packages/51/cb/2baff301c2c2cc2792b6e225ea0784793ca587c81b97572be0bad122cfc8/grimp-3.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a5c4fd71f363ea39e8aab0630010ced77a8de9789f27c0acdd0d7e6269d4a8ef", size = 2500573, upload-time = "2025-12-10T17:54:03.899Z" }, + { url = "https://files.pythonhosted.org/packages/96/69/797e4242f42d6665da5fe22cb250cae3f14ece4cb22ad153e9cd97158179/grimp-3.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766911e3ba0b13d833fdd03ad1f217523a8a2b2527b5507335f71dca1153183d", size = 2503005, upload-time = "2025-12-10T17:54:32.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/45/da1a27a6377807ca427cd56534231f0920e1895e16630204f382a0df14c5/grimp-3.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:154e84a2053e9f858ae48743de23a5ad4eb994007518c29371276f59b8419036", size = 2515776, upload-time = "2025-12-10T17:54:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/b918a29ce98029cd7a9e33a584be43a93288d5283fb7ccef5b6b2ba39ede/grimp-3.14-cp311-cp311-win32.whl", hash = "sha256:3189c86c3e73016a1907ee3ba9f7a6ca037e3601ad09e60ce9bf12b88877f812", size = 1873189, upload-time = "2025-12-10T17:55:11.872Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/2327c203f83a25766fbd62b0df3b24230d422b6e53518ff4d1c5e69793f1/grimp-3.14-cp311-cp311-win_amd64.whl", hash = "sha256:201f46a6a4e5ee9dfba4a2f7d043f7deab080d1d84233f4a1aee812678c25307", size = 2014277, upload-time = "2025-12-10T17:55:04.144Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/a35ff62f35aa5fd148053506eddd7a8f2f6afaed31870dc608dd0eb38e4f/grimp-3.14-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ffabc6940301214753bad89ec0bfe275892fa1f64b999e9a101f6cebfc777133", size = 2178573, upload-time = "2025-12-10T17:53:42.836Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/bd2e80273da4d46110969fc62252e5372e0249feb872bc7fe76fdc7f1818/grimp-3.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:075d9a1c78d607792d0ed8d4d3d7754a621ef04c8a95eaebf634930dc9232bb2", size = 2110452, upload-time = "2025-12-10T17:53:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/c3/7307249c657d34dca9d250d73ba027d6cfe15a98fb3119b6e5210bc388b7/grimp-3.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06ff52addeb20955a4d6aa097bee910573ffc9ef0d3c8a860844f267ad958156", size = 2283064, upload-time = "2025-12-10T17:52:07.673Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d2/cae4cf32dc8d4188837cc4ab183300d655f898969b0f169e240f3b7c25be/grimp-3.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d10e0663e961fcbe8d0f54608854af31f911f164c96a44112d5173050132701f", size = 2235893, upload-time = "2025-12-10T17:52:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/3f58bc3064fc305dac107d08003ba65713a5bc89a6d327f1c06b30cce752/grimp-3.14-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ab874d7ddddc7a1291259cf7c31a4e7b5c612e9da2e24c67c0eb1a44a624e67", size = 2393376, upload-time = "2025-12-10T17:53:02.397Z" }, + { url = "https://files.pythonhosted.org/packages/06/b8/f476f30edf114f04cb58e8ae162cb4daf52bda0ab01919f3b5b7edb98430/grimp-3.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54fec672ec83355636a852177f5a470c964bede0f6730f9ba3c7b5c8419c9eab", size = 2571342, upload-time = "2025-12-10T17:52:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/2e44d3c4f591f95f86322a8f4dbb5aac17001d49e079f3a80e07e7caaf09/grimp-3.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9e221b5e8070a916c780e88c877fee2a61c95a76a76a2a076396e459511b0bb", size = 2359022, upload-time = "2025-12-10T17:52:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/42b4d6bc0ea119ce2e91e1788feabf32c5433e9617dbb495c2a3d0dc7f12/grimp-3.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea6b495f9b4a8d82f5ce544921e76d0d12017f5d1ac3a3bd2f5ac88ab055b1c", size = 2309424, upload-time = "2025-12-10T17:53:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/6a731989625c1790f4da7602dcbf9d6525512264e853cda77b3b3602d5e0/grimp-3.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:655e8d3f79cd99bb859e09c9dd633515150e9d850879ca71417d5ac31809b745", size = 2462754, upload-time = "2025-12-10T17:53:50.886Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/3d1571c0a39a59dd68be4835f766da64fe64cbab0d69426210b716a8bdf0/grimp-3.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a14f10b1b71c6c37647a76e6a49c226509648107abc0f48c1e3ecd158ba05531", size = 2501356, upload-time = "2025-12-10T17:54:06.014Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/8950b8229095ebda5c54c8784e4d1f0a6e19423f2847289ef9751f878798/grimp-3.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:81685111ee24d3e25f8ed9e77ed00b92b58b2414e1a1c2937236026900972744", size = 2504631, upload-time = "2025-12-10T17:54:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/23bed3da9206138d36d01890b656c7fb7adfb3a37daac8842d84d8777ade/grimp-3.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce8352a8ea0e27b143136ea086582fc6653419aa8a7c15e28ed08c898c42b185", size = 2514751, upload-time = "2025-12-10T17:54:49.384Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/6f1f55c97ee982f133ec5ccb22fc99bf5335aee70c208f4fb86cd833b8d5/grimp-3.14-cp312-cp312-win32.whl", hash = "sha256:3fc0f98b3c60d88e9ffa08faff3200f36604930972f8b29155f323b76ea25a06", size = 1875041, upload-time = "2025-12-10T17:55:13.326Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/03ba01288e2a41a948bc8526f32c2eeaddd683ed34be1b895e31658d5a4c/grimp-3.14-cp312-cp312-win_amd64.whl", hash = "sha256:6bca77d1d50c8dc402c96af21f4e28e2f1e9938eeabd7417592a22bd83cde3c3", size = 2013868, upload-time = "2025-12-10T17:55:05.907Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bd/d12a9c821b79ba31fc52243e564712b64140fc6d011c2bdbb483d9092a12/grimp-3.14-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af8a625554beea84530b98cc471902155b5fc042b42dc47ec846fa3e32b0c615", size = 2178632, upload-time = "2025-12-10T17:53:44.55Z" }, + { url = "https://files.pythonhosted.org/packages/96/8c/d6620dbc245149d5a5a7a9342733556ba91a672f358259c0ab31d889b56b/grimp-3.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0dd1942ffb419ad342f76b0c3d3d2d7f312b264ddc578179d13ce8d5acec1167", size = 2110288, upload-time = "2025-12-10T17:53:21.662Z" }, + { url = "https://files.pythonhosted.org/packages/60/9d/ea51edc4eb295c99786040051c66466bfa235fd1def9f592057b36e03d0f/grimp-3.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:537f784ce9b4acf8657f0b9714ab69a6c72ffa752eccc38a5a85506103b1a194", size = 2282197, upload-time = "2025-12-10T17:52:09.304Z" }, + { url = "https://files.pythonhosted.org/packages/28/6e/7db27818ced6a797f976ca55d981a3af5c12aec6aeda12d63965847cd028/grimp-3.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78ab18c08770aa005bef67b873bc3946d33f65727e9f3e508155093db5fa57d6", size = 2235720, upload-time = "2025-12-10T17:52:21.806Z" }, + { url = "https://files.pythonhosted.org/packages/37/26/0e3bbae4826bd6eaabf404738400414071e73ddb1e65bf487dcce17858c4/grimp-3.14-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28ca58728c27e7292c99f964e6ece9295c2f9cfdefc37c18dea0679c783ffb6f", size = 2393023, upload-time = "2025-12-10T17:53:04.149Z" }, + { url = "https://files.pythonhosted.org/packages/49/f2/7da91db5703da34c7ef4c7cddcbb1a8fc30cd85fe54756eba942c6fb27d8/grimp-3.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b5577de29c6c5ae6e08d4ca0ac361b45dba323aa145796e6b320a6ea35414b7", size = 2571108, upload-time = "2025-12-10T17:52:36.523Z" }, + { url = "https://files.pythonhosted.org/packages/25/5e/4d6278f18032c7208696edf8be24a4b5f7fad80acc20ffca737344bcecb5/grimp-3.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d7d1f9f42306f455abcec34db877e4887ff15f2777a43491f7ccbd6936c449b", size = 2358531, upload-time = "2025-12-10T17:52:50.521Z" }, + { url = "https://files.pythonhosted.org/packages/24/fb/231c32493161ac82f27af6a56965daefa0ec6030fdaf5b948ddd5d68d000/grimp-3.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39bd5c9b7cef59ee30a05535e9cb4cbf45a3c503f22edce34d0aa79362a311a9", size = 2308831, upload-time = "2025-12-10T17:53:12.587Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/f6db325bf5efbbebc9c85cad0af865e821a12a0ba58ee309e938cbd5fedf/grimp-3.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fec3116b4f780a1bc54176b19e6b9f2e36e2ef3164b8fc840660566af35df88", size = 2462138, upload-time = "2025-12-10T17:53:52.403Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/cc3fe29cf07f70364018086840c228a190539ab8105147e34588db590792/grimp-3.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0233a35a5bbb23688d63e1736b54415fa9994ace8dfeb7de8514ed9dee212968", size = 2501393, upload-time = "2025-12-10T17:54:22.486Z" }, + { url = "https://files.pythonhosted.org/packages/e5/eb/54cada9a726455148da23f64577b5cd164164d23a6449e3fa14551157356/grimp-3.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e46b2fef0f1da7e7e2f8129eb93c7e79db716ff7810140a22ce5504e10ed86df", size = 2504514, upload-time = "2025-12-10T17:54:36.34Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c7/e6afe4f0652df07e8762f61899d1202b73c22c559c804d0a09e5aab2ff17/grimp-3.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e6d9b50623ee1c3d2a1927ec3f5d408995ea1f92f3e91ed996c908bb40e856f", size = 2514018, upload-time = "2025-12-10T17:54:50.76Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/2b8550acc1f010301f02c4fe9664810929fd9277cd032ab608b8534a96fb/grimp-3.14-cp313-cp313-win32.whl", hash = "sha256:fd57c56f5833c99320ec77e8ba5508d56f6fb48ec8032a942f7931cc6ebb80ce", size = 1874922, upload-time = "2025-12-10T17:55:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/46/c7/bc9db5a54ef22972cd17d15ad80a8fee274a471bd3f02300405702d29ea5/grimp-3.14-cp313-cp313-win_amd64.whl", hash = "sha256:173307cf881a126fe5120b7bbec7d54384002e3c83dcd8c4df6ce7f0fee07c53", size = 2013705, upload-time = "2025-12-10T17:55:07.488Z" }, + { url = "https://files.pythonhosted.org/packages/80/7e/02710bf5e50997168c84ac622b10dd41d35515efd0c67549945ad20996a0/grimp-3.14-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebe29f8f13fbd7c314908ed535183a36e6db71839355b04869b27f23c58fa082", size = 2281868, upload-time = "2025-12-10T17:52:10.589Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/2e440c6762cc78bd50582e1b092357d2255f0852ccc6218d8db25170ab31/grimp-3.14-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073d285b00100153fd86064c7726bb1b6d610df1356d33bb42d3fd8809cb6e72", size = 2230917, upload-time = "2025-12-10T17:52:23.212Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bb/2e7dce129b88f07fc525fe5c97f28cfb7ed7b62c59386d39226b4d08969c/grimp-3.14-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6d6efc37e1728bbfcd881b89467be5f7b046292597b3ebe5f8e44e89ea8b6cb", size = 2571371, upload-time = "2025-12-10T17:52:37.84Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2b/8f1be8294af60c953687db7dec25525d87ed9c2aa26b66dcbe5244abaca2/grimp-3.14-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5337d65d81960b712574c41e85b480d4480bbb5c6f547c94e634f6c60d730889", size = 2356980, upload-time = "2025-12-10T17:52:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/ead91e04b3ddd4774ae74601860ea0f0f21bcf6b970b6769ba9571eb2904/grimp-3.14-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:84a7fea63e352b325daa89b0b7297db411b7f0036f8d710c32f8e5090e1fc3ca", size = 2461540, upload-time = "2025-12-10T17:53:53.749Z" }, + { url = "https://files.pythonhosted.org/packages/94/aa/f8a085ff73c37d6e6a37de9f58799a3fea9e16badf267aaef6f11c9a53a3/grimp-3.14-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d0b19a3726377165fe1f7184a8af317734d80d32b371b6c5578747867ab53c0b", size = 2497925, upload-time = "2025-12-10T17:54:23.842Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a3/db3c2d6df07fe74faf5a28fcf3b44fad2831d323ba4a3c2ff66b77a6520c/grimp-3.14-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9caa4991f530750f88474a3f5ecf6ef9f0d064034889d92db00cfb4ecb78aa24", size = 2501794, upload-time = "2025-12-10T17:54:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/095f4e3765e7b60425a41e9fbd2b167f8b0acb957cc88c387f631778a09d/grimp-3.14-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1876efc119b99332a5cc2b08a6bdaada2f0ad94b596f0372a497e2aa8bda4d94", size = 2515203, upload-time = "2025-12-10T17:54:52.555Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5f/ee02a3a1237282d324f596a50923bf9d2cb1b1230ef2fef49fb4d3563c2c/grimp-3.14-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3ccf03e65864d6bc7bf1c003c319f5330a7627b3677f31143f11691a088464c2", size = 2177150, upload-time = "2025-12-10T17:53:46.145Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/2a92889e5fc78e8ef5c548e6a5c6fed78b817eeb0253aca586c28108393a/grimp-3.14-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9ecd58fa58a270e7523f8bec9e6452f4fdb9c21e4cd370640829f1e43fa87a69", size = 2109280, upload-time = "2025-12-10T17:53:23.345Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/5d0b9ab54821e7fbdeb02f3919fa2cb8b9f0c3869fa6e4b969a5766f0ffa/grimp-3.14-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d75d1f8f7944978b39b08d870315174f1ffcd5123be6ccff8ce90467ace648a", size = 2283367, upload-time = "2025-12-10T17:52:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/c2/96/a77c40c92faf7500f42ac019ab8de108b04ffe3db8ec8d6f90416d2322ce/grimp-3.14-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f70bbb1dd6055d08d29e39a78a11c4118c1778b39d17cd8271e18e213524ca7", size = 2237125, upload-time = "2025-12-10T17:52:24.606Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5e/3e1483721c83057bff921cf454dd5ff3e661ae1d2e63150a380382d116c2/grimp-3.14-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f21b7c003626c902669dc26ede83a91220cf0a81b51b27128370998c2f247b4", size = 2391735, upload-time = "2025-12-10T17:53:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/25fad4a174fe672d42f3e5616761a8120a3b03c8e9e2ae3f31159561968a/grimp-3.14-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80d9f056415c936b45561310296374c4319b5df0003da802c84d2830a103792a", size = 2571388, upload-time = "2025-12-10T17:52:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/456df7f6a765ce3f160eb32a0f64ed0c1c3cd39b518555dde02087f9b6e4/grimp-3.14-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0332963cd63a45863775d4237e59dedf95455e0a1ea50c356be23100c5fc1d7c", size = 2359637, upload-time = "2025-12-10T17:52:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/3e5005ef21a4e2243f0da489aba86aaaff0bc11d5240d67113482cba88e0/grimp-3.14-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4144350d074f2058fe7c89230a26b34296b161f085b0471a692cb2fe27036f", size = 2308335, upload-time = "2025-12-10T17:53:13.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/03/4e055f756946d6f71ab7e9d1f8536a9e476777093dd7a050f40412d1a2b1/grimp-3.14-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e148e67975e92f90a8435b1b4c02180b9a3f3d725b7a188ba63793f1b1e445a0", size = 2463680, upload-time = "2025-12-10T17:53:55.507Z" }, + { url = "https://files.pythonhosted.org/packages/26/b9/3c76b7c2e1587e4303a6eff6587c2117c3a7efe1b100cd13d8a4a5613572/grimp-3.14-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1093f7770cb5f3ca6f99fb152f9c949381cc0b078dfdfe598c8ab99abaccda3b", size = 2502808, upload-time = "2025-12-10T17:54:25.383Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/ada10b85ad3125ebedea10256d9c568b6bf28339d2f79d2d196a7b94f633/grimp-3.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a213f45ec69e9c2b28ffd3ba5ab12cc9859da17083ba4dc39317f2083b618111", size = 2504013, upload-time = "2025-12-10T17:54:39.762Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/7c369f749d50b0ceac23cd6874ca4695cc1359a96091c7010301e5c8b619/grimp-3.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f003ac3f226d2437a49af0b6036f26edba57f8a32d329275dbde1b2b2a00a56", size = 2515043, upload-time = "2025-12-10T17:54:54.437Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/85135fe83826ce11ae56a340d32a1391b91eed94d25ce7bc318019f735de/grimp-3.14-cp314-cp314-win32.whl", hash = "sha256:eec81be65a18f4b2af014b1e97296cc9ee20d1115529bf70dd7e06f457eac30b", size = 1877509, upload-time = "2025-12-10T17:55:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/db/61/e4a2234edecb3bb3cff8963bc4ec5cc482a9e3c54f8df0946d7d90003830/grimp-3.14-cp314-cp314-win_amd64.whl", hash = "sha256:cd3bab6164f1d5e313678f0ab4bf45955afe7f5bdb0f2f481014aa9cca7e81ba", size = 2014364, upload-time = "2025-12-10T17:55:08.896Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/3d304443fbf1df4d60c09668846d0c8a605c6c95646226e41d8f5c3254da/grimp-3.14-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1df33de479be4d620f69633d1876858a8e64a79c07907d47cf3aaf896af057", size = 2281385, upload-time = "2025-12-10T17:52:13.668Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/493e2648dbb83b3fc517ee675e464beb0154551d726053c7982a3138c6a8/grimp-3.14-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07096d4402e9d5a2c59c402ea3d601f4b7f99025f5e32f077468846fc8d3821b", size = 2231470, upload-time = "2025-12-10T17:52:26.104Z" }, + { url = "https://files.pythonhosted.org/packages/80/84/e772b302385a6b7ec752c88f84ffe35c33d14076245ae27a635aed9c63a2/grimp-3.14-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:712bc28f46b354316af50c469c77953ba3d6cb4166a62b8fb086436a8b05d301", size = 2571579, upload-time = "2025-12-10T17:52:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/69/92/5b23aa7b89c5f4f2cfa636cbeaf33e784378a6b0a823d77a3448670dfacc/grimp-3.14-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abe2bbef1cf8e27df636c02f60184319f138dee4f3a949405c21a4b491980397", size = 2356545, upload-time = "2025-12-10T17:52:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/bcf2116f4b1c3939ab35f9cdddd9ca59e953e57e9a0ac0c143deaf9f29cc/grimp-3.14-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2f9ae3fabb7a7a8468ddc96acc84ecabd84f168e7ca508ee94d8f32ea9bd5de2", size = 2461022, upload-time = "2025-12-10T17:53:56.923Z" }, + { url = "https://files.pythonhosted.org/packages/81/ce/1a076dce6bc22bca4b9ad5d1bbcd7e1023dcf7bf20ea9404c6462d78f049/grimp-3.14-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:efaf11ea73f7f12d847c54a5d6edcbe919e0369dce2d1aabae6c50792e16f816", size = 2498256, upload-time = "2025-12-10T17:54:27.214Z" }, + { url = "https://files.pythonhosted.org/packages/45/ea/ac735bed202c1c5c019e611b92d3861779e0cfbe2d20fdb0dec94266d248/grimp-3.14-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e089c9ab8aa755ff5af88c55891727783b4eb6b228e7bdf278e17209d954aa1e", size = 2502056, upload-time = "2025-12-10T17:54:41.537Z" }, + { url = "https://files.pythonhosted.org/packages/80/8f/774ce522de6a7e70fbeceeaeb6fbe502f5dfb8365728fb3bb4cb23463da8/grimp-3.14-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ad14d5deb56721ac24ab939747f72ab3d378d42e7d1f038317d33b052b77", size = 2515157, upload-time = "2025-12-10T17:54:55.874Z" }, + { url = "https://files.pythonhosted.org/packages/65/cc/dbc00210d0324b8fc1242d8e857757c7e0b62ff0fc0c1bc8dcc42342da85/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c8a8aab9b4310a7e69d7d845cac21cf14563aa0520ea322b948eadeae56d303", size = 2284804, upload-time = "2025-12-10T17:52:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/80/89/851d3d345342e9bcec3fe85d3997db29501fa59f958c1566bf3e24d9d7d9/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d781943b27e5875a41c8f9cfc80f8f0a349f864379192b8c3faa0e6a22593313", size = 2235176, upload-time = "2025-12-10T17:52:30.795Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/5f94702a8d5c121cafcdc9664de34c34f19d0d91a1127bf3946a2631f7a3/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9630d4633607aff94d0ac84b9c64fef1382cdb05b00d9acbde47f8745e264871", size = 2391258, upload-time = "2025-12-10T17:53:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a2/df8c79de5c9e227856d048cc1551c4742a5f97660c40304ac278bd48607f/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb00e1bcca583668554a8e9e1e4229a1d11b0620969310aae40148829ff6a32", size = 2571443, upload-time = "2025-12-10T17:52:43.853Z" }, + { url = "https://files.pythonhosted.org/packages/f0/21/747b7ed9572bbdc34a76dfec12ce510e80164b1aa06d3b21b34994e5f567/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3389da4ceaaa7f7de24a668c0afc307a9f95997bd90f81ec359a828a9bd1d270", size = 2357767, upload-time = "2025-12-10T17:52:57.84Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e6/485c5e3b64933e71f72f0cc45b0d7130418a6a5a13cedc2e8411bd76f290/grimp-3.14-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd7a32970ef97e42d4e7369397c7795287d84a736d788ccb90b6c14f0561d975", size = 2309069, upload-time = "2025-12-10T17:53:15.203Z" }, + { url = "https://files.pythonhosted.org/packages/31/bd/12024a8cba1c77facc1422a7b48cd0d04c252fc9178fd6f99dc05a8af57b/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:fd1278623fa09f62abc0fd8a6500f31b421a1fd479980f44c2926020a0becf02", size = 2466429, upload-time = "2025-12-10T17:54:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7f/0e5977887e1c8f00f84bb4125217534806ffdcef9cf52f3580aa3b151f4b/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:9cfa52c89333d3d8fe9dc782529e888270d060231c3783e036d424044671dde0", size = 2501190, upload-time = "2025-12-10T17:54:30.107Z" }, + { url = "https://files.pythonhosted.org/packages/42/6b/06acb94b6d0d8c7277bb3e33f93224aa3be5b04643f853479d3bf7b23ace/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:48a5be4a12fca6587e6885b4fc13b9e242ab8bf874519292f0f13814aecf52cc", size = 2503440, upload-time = "2025-12-10T17:54:44.444Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4d/2e531370d12e7a564f67f680234710bbc08554238a54991cd244feb61fb6/grimp-3.14-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3fcc332466783a12a42cd317fd344c30fe734ba4fa2362efff132dc3f8d36da7", size = 2516525, upload-time = "2025-12-10T17:54:58.987Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -285,6 +390,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "import-linter" +version = "2.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "grimp" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/a7/5ae6dc1caf8229f9bcd05b0dd71aba6d5b699b81e5d599fd05bb36046e2a/import_linter-2.12.tar.gz", hash = "sha256:217f0f0f0890b8625389765039d52c215f4c6d4a047f42ea78b6d00ceba1900e", size = 1173831, upload-time = "2026-06-23T08:58:35.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2b/3d9ff17662d01aa83fad076f4881d95136c72781b6b7ef8bf0464a5037a7/import_linter-2.12-py3-none-any.whl", hash = "sha256:b346eeb30cabd5b0630773d42cca4eac8dd459ac5d507ac2ecadc3f99462f231", size = 637947, upload-time = "2026-06-23T08:58:33.969Z" }, +] + [[package]] name = "importlib-metadata" version = "8.7.1" @@ -1109,6 +1229,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "bandit" }, + { name = "import-linter" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, @@ -1134,6 +1255,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.8.2" }, + { name = "import-linter", specifier = ">=2.0" }, { name = "mypy", specifier = ">=1.19.0" }, { name = "pre-commit", specifier = ">=4.1.0" }, { name = "pytest", specifier = ">=7.4.0" }, From e36cfb4961ab9b87e3d3a0038ee731694d0857e4 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Fri, 3 Jul 2026 13:52:30 +0300 Subject: [PATCH 2/9] fix(platform): move interrupt models from common to resume_triggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit common/interrupt_models.py imported action_center, attachments, context_grounding, documents, and orchestrator at runtime, forming a cycle cluster around common — the same layering inversion class that broke 0.1.89 (only masked by import ordering). The models are HITL suspend/resume payloads consumed solely by resume_triggers, so they move into that package; all former upward edges now point downward and the import-linter contract passes with zero ignores. The models are importable from uipath.platform.resume_triggers. No re-export shim in common — an eager one recreates the cycle and a lazy one still violates the layering contract. Known external usages (uipath-langchain-python docs/samples/test mocks, POC repos) import from uipath.platform.common and need a coordinated update; none are published package sources. Co-Authored-By: Claude Fable 5 --- .../src/uipath/platform/common/__init__.py | 54 ------------------- .../platform/resume_triggers/__init__.py | 54 +++++++++++++++++++ .../platform/resume_triggers/_protocol.py | 37 ++++++------- .../interrupt_models.py | 0 .../tests/services/test_hitl.py | 48 ++++++++--------- .../cli/unit/test_runtime_protocol_compat.py | 3 +- packages/uipath/uv.lock | 1 + 7 files changed, 98 insertions(+), 99 deletions(-) rename packages/uipath-platform/src/uipath/platform/{common => resume_triggers}/interrupt_models.py (100%) diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 802ec67bc..7f523e602 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -41,34 +41,6 @@ from ._user_agent import user_agent_value from .auth import TokenData from .dynamic_schema import jsonschema_to_pydantic -from .interrupt_models import ( - CreateBatchTransform, - CreateDeepRag, - CreateDeepRagRaw, - CreateEphemeralIndex, - CreateEphemeralIndexRaw, - CreateEscalation, - CreateTask, - DocumentExtraction, - DocumentExtractionValidation, - InvokeProcess, - InvokeProcessRaw, - InvokeSystemAgent, - WaitBatchTransform, - WaitDeepRag, - WaitDeepRagRaw, - WaitDocumentExtraction, - WaitDocumentExtractionValidation, - WaitEphemeralIndex, - WaitEphemeralIndexRaw, - WaitEscalation, - WaitIntegrationEvent, - WaitJob, - WaitJobRaw, - WaitSystemAgent, - WaitTask, - WaitUntil, -) from .paging import PagedResult from .timeout import ( UiPathTimeoutError, @@ -86,33 +58,7 @@ "FolderContext", "TokenData", "UiPathConfig", - "CreateTask", - "CreateEscalation", - "WaitEscalation", - "InvokeProcess", - "InvokeProcessRaw", - "WaitTask", - "WaitJob", - "WaitJobRaw", "PagedResult", - "CreateDeepRag", - "CreateDeepRagRaw", - "WaitDeepRag", - "WaitDeepRagRaw", - "CreateBatchTransform", - "WaitBatchTransform", - "DocumentExtraction", - "WaitDocumentExtraction", - "InvokeSystemAgent", - "WaitSystemAgent", - "CreateEphemeralIndex", - "CreateEphemeralIndexRaw", - "WaitEphemeralIndex", - "WaitEphemeralIndexRaw", - "DocumentExtractionValidation", - "WaitDocumentExtractionValidation", - "WaitIntegrationEvent", - "WaitUntil", "RequestSpec", "Endpoint", "UiPathUrl", diff --git a/packages/uipath-platform/src/uipath/platform/resume_triggers/__init__.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/__init__.py index 47c35f5b7..13ab154ee 100644 --- a/packages/uipath-platform/src/uipath/platform/resume_triggers/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/__init__.py @@ -6,6 +6,34 @@ UiPathResumeTriggerHandler, UiPathResumeTriggerReader, ) +from .interrupt_models import ( + CreateBatchTransform, + CreateDeepRag, + CreateDeepRagRaw, + CreateEphemeralIndex, + CreateEphemeralIndexRaw, + CreateEscalation, + CreateTask, + DocumentExtraction, + DocumentExtractionValidation, + InvokeProcess, + InvokeProcessRaw, + InvokeSystemAgent, + WaitBatchTransform, + WaitDeepRag, + WaitDeepRagRaw, + WaitDocumentExtraction, + WaitDocumentExtractionValidation, + WaitEphemeralIndex, + WaitEphemeralIndexRaw, + WaitEscalation, + WaitIntegrationEvent, + WaitJob, + WaitJobRaw, + WaitSystemAgent, + WaitTask, + WaitUntil, +) __all__ = [ "UiPathResumeTriggerReader", @@ -14,4 +42,30 @@ "PropertyName", "TriggerMarker", "is_no_content_marker", + "CreateBatchTransform", + "CreateDeepRag", + "CreateDeepRagRaw", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "CreateEscalation", + "CreateTask", + "DocumentExtraction", + "DocumentExtractionValidation", + "InvokeProcess", + "InvokeProcessRaw", + "InvokeSystemAgent", + "WaitBatchTransform", + "WaitDeepRag", + "WaitDeepRagRaw", + "WaitDocumentExtraction", + "WaitDocumentExtractionValidation", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "WaitEscalation", + "WaitIntegrationEvent", + "WaitJob", + "WaitJobRaw", + "WaitSystemAgent", + "WaitTask", + "WaitUntil", ] diff --git a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py index 784e9f2ae..5dfaff046 100644 --- a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py @@ -24,7 +24,25 @@ from uipath.platform.action_center import Task from uipath.platform.action_center.tasks import TaskStatus from uipath.platform.common._config import UiPathConfig -from uipath.platform.common.interrupt_models import ( +from uipath.platform.connections import EventArguments +from uipath.platform.context_grounding import DeepRagStatus, IndexStatus +from uipath.platform.context_grounding.context_grounding_index import ( + ContextGroundingIndex, +) +from uipath.platform.errors import ( + BatchTransformFailedException, + BatchTransformNotCompleteException, + OperationNotCompleteException, +) +from uipath.platform.orchestrator.job import JobState +from uipath.platform.resume_triggers._enums import ( + ExternalTrigger, + ExternalTriggerType, + PropertyName, + TriggerMarker, +) + +from .interrupt_models import ( CreateBatchTransform, CreateDeepRag, CreateDeepRagRaw, @@ -52,23 +70,6 @@ WaitTask, WaitUntil, ) -from uipath.platform.connections import EventArguments -from uipath.platform.context_grounding import DeepRagStatus, IndexStatus -from uipath.platform.context_grounding.context_grounding_index import ( - ContextGroundingIndex, -) -from uipath.platform.errors import ( - BatchTransformFailedException, - BatchTransformNotCompleteException, - OperationNotCompleteException, -) -from uipath.platform.orchestrator.job import JobState -from uipath.platform.resume_triggers._enums import ( - ExternalTrigger, - ExternalTriggerType, - PropertyName, - TriggerMarker, -) def _try_convert_to_json_format(value: str | None) -> Any: diff --git a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/interrupt_models.py similarity index 100% rename from packages/uipath-platform/src/uipath/platform/common/interrupt_models.py rename to packages/uipath-platform/src/uipath/platform/resume_triggers/interrupt_models.py diff --git a/packages/uipath-platform/tests/services/test_hitl.py b/packages/uipath-platform/tests/services/test_hitl.py index 134d75fc0..86c75d78c 100644 --- a/packages/uipath-platform/tests/services/test_hitl.py +++ b/packages/uipath-platform/tests/services/test_hitl.py @@ -18,31 +18,6 @@ from uipath.platform.action_center import Task from uipath.platform.action_center.tasks import TaskStatus -from uipath.platform.common import ( - CreateBatchTransform, - CreateDeepRag, - CreateDeepRagRaw, - CreateEphemeralIndex, - CreateEphemeralIndexRaw, - CreateTask, - DocumentExtraction, - DocumentExtractionValidation, - InvokeProcess, - InvokeProcessRaw, - InvokeSystemAgent, - WaitBatchTransform, - WaitDeepRag, - WaitDeepRagRaw, - WaitDocumentExtractionValidation, - WaitEphemeralIndex, - WaitEphemeralIndexRaw, - WaitIntegrationEvent, - WaitJob, - WaitJobRaw, - WaitSystemAgent, - WaitTask, - WaitUntil, -) from uipath.platform.connections import Connection from uipath.platform.context_grounding import ( BatchTransformCreationResponse, @@ -70,10 +45,33 @@ from uipath.platform.orchestrator import Job, JobErrorInfo from uipath.platform.orchestrator.job import JobState from uipath.platform.resume_triggers import ( + CreateBatchTransform, + CreateDeepRag, + CreateDeepRagRaw, + CreateEphemeralIndex, + CreateEphemeralIndexRaw, + CreateTask, + DocumentExtraction, + DocumentExtractionValidation, + InvokeProcess, + InvokeProcessRaw, + InvokeSystemAgent, PropertyName, TriggerMarker, UiPathResumeTriggerCreator, UiPathResumeTriggerReader, + WaitBatchTransform, + WaitDeepRag, + WaitDeepRagRaw, + WaitDocumentExtractionValidation, + WaitEphemeralIndex, + WaitEphemeralIndexRaw, + WaitIntegrationEvent, + WaitJob, + WaitJobRaw, + WaitSystemAgent, + WaitTask, + WaitUntil, ) diff --git a/packages/uipath/tests/cli/unit/test_runtime_protocol_compat.py b/packages/uipath/tests/cli/unit/test_runtime_protocol_compat.py index 812ddaa11..efb0aafd0 100644 --- a/packages/uipath/tests/cli/unit/test_runtime_protocol_compat.py +++ b/packages/uipath/tests/cli/unit/test_runtime_protocol_compat.py @@ -9,8 +9,7 @@ UiPathResumeTriggerName, UiPathResumeTriggerType, ) -from uipath.platform.common import WaitUntil -from uipath.platform.resume_triggers import UiPathResumeTriggerHandler +from uipath.platform.resume_triggers import UiPathResumeTriggerHandler, WaitUntil from uipath.runtime import ( UiPathExecuteOptions, UiPathResumableRuntime, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index da12487fe..35a3d7eb3 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2765,6 +2765,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.8.2" }, + { name = "import-linter", specifier = ">=2.0" }, { name = "mypy", specifier = ">=1.19.0" }, { name = "pre-commit", specifier = ">=4.1.0" }, { name = "pytest", specifier = ">=7.4.0" }, From 1ba6b70c7e3c53a9150984b962ffc62cc5e186b6 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Fri, 3 Jul 2026 16:43:15 +0300 Subject: [PATCH 3/9] refactor: decouple guardrails from _uipath to avoid cyclic dependency --- packages/uipath-platform/pyproject.toml | 4 +- .../src/uipath/platform/_uipath.py | 34 ++---- .../src/uipath/platform/common/auth.py | 26 +++- .../platform/guardrails/_default_evaluator.py | 33 ++++++ .../uipath/platform/guardrails/_evaluator.py | 19 +++ .../guardrails/decorators/validators/_base.py | 16 +-- .../uipath-platform/tests/common/test_auth.py | 44 +++++++ .../services/test_guardrails_decorators.py | 111 ++++++++++++++++-- 8 files changed, 242 insertions(+), 45 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/_default_evaluator.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/_evaluator.py create mode 100644 packages/uipath-platform/tests/common/test_auth.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 9026a6159..7af6e47b7 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -138,8 +138,8 @@ type = "layers" containers = ["uipath.platform"] exhaustive = true layers = [ - "_uipath : guardrails : resume_triggers", - "agenthub : automation_ops : automation_tracker : connections : context_grounding : entities : external_applications : governance : memory : pii_detection : portal : resource_catalog : semantic_proxy", + "_uipath : resume_triggers", + "agenthub : automation_ops : automation_tracker : connections : context_grounding : entities : external_applications : governance : guardrails : memory : pii_detection : portal : resource_catalog : semantic_proxy", "orchestrator", "action_center : attachments : chat : documents : identity", "common", diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 98af7b8b6..f2359cd6f 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -1,8 +1,6 @@ from functools import cached_property from typing import Optional -from pydantic import ValidationError - from uipath.platform.automation_tracker import AutomationTrackerService from .action_center import TasksService @@ -12,15 +10,13 @@ from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService from .common import ( ApiClient, - UiPathApiConfig, UiPathExecutionContext, ) -from .common.auth import resolve_config_from_env +from .common.auth import build_api_config, resolve_config_from_env from .connections import ConnectionsService from .context_grounding import ContextGroundingService from .documents import DocumentsService from .entities import EntitiesService -from .errors import BaseUrlMissingError, SecretMissingError from .external_applications import ExternalApplicationService from .governance import GovernanceService from .guardrails import GuardrailsService @@ -60,25 +56,15 @@ def __init__( scope: Optional[str] = None, debug: bool = False, ) -> None: - try: - if _has_valid_client_credentials(client_id, client_secret): - assert client_id and client_secret - service = ExternalApplicationService(base_url) - token_data = service.get_token_data(client_id, client_secret, scope) - base_url, secret = service._base_url, token_data.access_token - else: - base_url, secret = resolve_config_from_env(base_url, secret) - - self._config = UiPathApiConfig( - base_url=base_url, - secret=secret, - ) - except ValidationError as e: - for error in e.errors(): - if error["loc"][0] == "base_url": - raise BaseUrlMissingError() from e - elif error["loc"][0] == "secret": - raise SecretMissingError() from e + if _has_valid_client_credentials(client_id, client_secret): + assert client_id and client_secret + service = ExternalApplicationService(base_url) + token_data = service.get_token_data(client_id, client_secret, scope) + base_url, secret = service._base_url, token_data.access_token + else: + base_url, secret = resolve_config_from_env(base_url, secret) + + self._config = build_api_config(base_url, secret) self._execution_context = UiPathExecutionContext() @property diff --git a/packages/uipath-platform/src/uipath/platform/common/auth.py b/packages/uipath-platform/src/uipath/platform/common/auth.py index a8f87d3e4..612d3ca4f 100644 --- a/packages/uipath-platform/src/uipath/platform/common/auth.py +++ b/packages/uipath-platform/src/uipath/platform/common/auth.py @@ -3,13 +3,16 @@ from os import environ as env from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError from uipath.platform.constants import ( ENV_BASE_URL, ENV_UIPATH_ACCESS_TOKEN, ENV_UNATTENDED_USER_ACCESS_TOKEN, ) +from uipath.platform.errors import BaseUrlMissingError, SecretMissingError + +from ._config import UiPathApiConfig class TokenData(BaseModel): @@ -35,3 +38,24 @@ def resolve_config_from_env( or env.get(ENV_UIPATH_ACCESS_TOKEN) ) return base_url_value, secret_value + + +def build_api_config( + base_url: Optional[str], + secret: Optional[str], +) -> UiPathApiConfig: + """Build a validated API config, translating missing fields into typed errors. + + Raises: + BaseUrlMissingError: If ``base_url`` is missing. + SecretMissingError: If ``secret`` is missing. + """ + try: + return UiPathApiConfig(base_url=base_url, secret=secret) # type: ignore[arg-type] + except ValidationError as e: + for error in e.errors(): + if error["loc"][0] == "base_url": + raise BaseUrlMissingError() from e + elif error["loc"][0] == "secret": + raise SecretMissingError() from e + raise diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/_default_evaluator.py b/packages/uipath-platform/src/uipath/platform/guardrails/_default_evaluator.py new file mode 100644 index 000000000..44381ef17 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_default_evaluator.py @@ -0,0 +1,33 @@ +"""Default env-driven guardrail evaluator.""" + +from typing import Any + +from uipath.core.guardrails import GuardrailValidationResult + +from ..common import UiPathExecutionContext +from ..common.auth import build_api_config, resolve_config_from_env +from ._guardrails_service import GuardrailsService +from .guardrails import BuiltInValidatorGuardrail + + +class DefaultGuardrailEvaluator: + """Evaluates guardrails via a ``GuardrailsService`` configured from env vars. + + Raises: + BaseUrlMissingError: If the base URL is not configured. + SecretMissingError: If the access token is not configured. + """ + + def __init__(self) -> None: + base_url, secret = resolve_config_from_env(None, None) + self._service = GuardrailsService( + build_api_config(base_url, secret), UiPathExecutionContext() + ) + + def evaluate_guardrail( + self, + input_data: str | dict[str, Any], + guardrail: BuiltInValidatorGuardrail, + ) -> GuardrailValidationResult: + """Validate input data using the provided guardrail.""" + return self._service.evaluate_guardrail(input_data, guardrail) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/_evaluator.py b/packages/uipath-platform/src/uipath/platform/guardrails/_evaluator.py new file mode 100644 index 000000000..bb4dc7892 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_evaluator.py @@ -0,0 +1,19 @@ +"""Internal protocol for guardrail evaluation capabilities.""" + +from typing import Any, Protocol + +from uipath.core.guardrails import GuardrailValidationResult + +from .guardrails import BuiltInValidatorGuardrail + + +class GuardrailEvaluator(Protocol): + """Capability to evaluate a built-in guardrail against input data.""" + + def evaluate_guardrail( + self, + input_data: str | dict[str, Any], + guardrail: BuiltInValidatorGuardrail, + ) -> GuardrailValidationResult: + """Validate input data using the provided guardrail.""" + ... diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py index a9eaf5afd..02aad480d 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py @@ -5,6 +5,8 @@ from uipath.core.guardrails import GuardrailValidationResult +from uipath.platform.guardrails._default_evaluator import DefaultGuardrailEvaluator +from uipath.platform.guardrails._evaluator import GuardrailEvaluator from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail from .._enums import GuardrailExecutionStage @@ -78,6 +80,8 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): ) """ + _evaluator: GuardrailEvaluator | None = None + @abstractmethod def get_built_in_guardrail( self, @@ -109,15 +113,13 @@ def run( ) -> GuardrailValidationResult: """Evaluate via the UiPath Guardrails API. - Lazily initialises the ``UiPath`` client on the first call and reuses - it for all subsequent invocations. + Lazily initialises the guardrail evaluator on the first call and + reuses it for all subsequent invocations. """ built_in = self.get_built_in_guardrail(name, description, enabled_for_evals) - if not hasattr(self, "_uipath"): - from uipath.platform import UiPath - - self._uipath: Any = UiPath() - return self._uipath.guardrails.evaluate_guardrail(data, built_in) + if self._evaluator is None: + self._evaluator = DefaultGuardrailEvaluator() + return self._evaluator.evaluate_guardrail(data, built_in) class CustomGuardrailValidator(GuardrailValidatorBase, ABC): diff --git a/packages/uipath-platform/tests/common/test_auth.py b/packages/uipath-platform/tests/common/test_auth.py new file mode 100644 index 000000000..8b8fe67a2 --- /dev/null +++ b/packages/uipath-platform/tests/common/test_auth.py @@ -0,0 +1,44 @@ +"""Tests for config helpers in uipath.platform.common.auth.""" + +from unittest.mock import patch + +import pytest +from pydantic import BaseModel, ValidationError + +from uipath.platform.common import UiPathApiConfig +from uipath.platform.common.auth import build_api_config +from uipath.platform.errors import BaseUrlMissingError, SecretMissingError + + +class TestBuildApiConfig: + def test_returns_config_when_both_values_present(self): + config = build_api_config("https://test.uipath.com", "token") + assert isinstance(config, UiPathApiConfig) + assert config.base_url == "https://test.uipath.com" + assert config.secret == "token" + + def test_missing_base_url_raises_typed_error(self): + with pytest.raises(BaseUrlMissingError): + build_api_config(None, "token") + + def test_missing_secret_raises_typed_error(self): + with pytest.raises(SecretMissingError): + build_api_config("https://test.uipath.com", None) + + def test_both_missing_raises_base_url_error_first(self): + with pytest.raises(BaseUrlMissingError): + build_api_config(None, None) + + def test_unmatched_validation_error_is_reraised(self): + class _Other(BaseModel): + other: str + + with pytest.raises(ValidationError) as exc_info: + _Other.model_validate({}) + unmatched = exc_info.value + + with patch( + "uipath.platform.common.auth.UiPathApiConfig", side_effect=unmatched + ): + with pytest.raises(ValidationError): + build_api_config("https://test.uipath.com", "token") diff --git a/packages/uipath-platform/tests/services/test_guardrails_decorators.py b/packages/uipath-platform/tests/services/test_guardrails_decorators.py index 29674ca09..7ca4d7312 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_decorators.py +++ b/packages/uipath-platform/tests/services/test_guardrails_decorators.py @@ -989,8 +989,8 @@ def test_custom_validator_path_delegates_to_run(self): ) assert result == _PASSED - def test_built_in_validator_path_lazy_initializes_uipath(self): - """BuiltInGuardrailValidator.run() lazily creates UiPath() and calls API.""" + def test_built_in_validator_uses_injected_evaluator(self): + """BuiltInGuardrailValidator.run() delegates to an injected evaluator.""" from uipath.platform.guardrails.decorators.validators import ( BuiltInGuardrailValidator, ) @@ -1003,16 +1003,102 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): return mock_built_in validator = _TestBuiltIn() + fake_evaluator = MagicMock() + fake_evaluator.evaluate_guardrail.return_value = _PASSED + validator._evaluator = fake_evaluator evaluator = _make_evaluator(validator, "G", None, True) - mock_uipath = MagicMock() - mock_uipath.guardrails.evaluate_guardrail.return_value = _PASSED - with patch("uipath.platform.UiPath", return_value=mock_uipath): + result = evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) + + fake_evaluator.evaluate_guardrail.assert_called_once_with( + {"text": "hello"}, mock_built_in + ) + assert result == _PASSED + + def test_built_in_validator_path_lazy_initializes_default_evaluator(self): + """BuiltInGuardrailValidator.run() creates the default evaluator once.""" + from uipath.platform.guardrails.decorators.validators import ( + BuiltInGuardrailValidator, + ) + from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail + + mock_built_in = MagicMock(spec=BuiltInValidatorGuardrail) + + class _TestBuiltIn(BuiltInGuardrailValidator): + def get_built_in_guardrail(self, name, description, enabled_for_evals): + return mock_built_in + + validator = _TestBuiltIn() + evaluator = _make_evaluator(validator, "G", None, True) + + with patch( + "uipath.platform.guardrails.decorators.validators._base.DefaultGuardrailEvaluator" + ) as mock_default: + mock_default.return_value.evaluate_guardrail.return_value = _PASSED evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) - # UiPath() should be created only once despite two calls - assert mock_uipath.guardrails.evaluate_guardrail.call_count == 2 + mock_default.assert_called_once_with() + assert mock_default.return_value.evaluate_guardrail.call_count == 2 + + def test_built_in_validator_without_env_config_raises_missing_errors( + self, monkeypatch + ): + """Missing env credentials surface as typed errors from the decorator path.""" + from uipath.platform.errors import BaseUrlMissingError, SecretMissingError + from uipath.platform.guardrails.decorators.validators import ( + BuiltInGuardrailValidator, + ) + from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail + + class _TestBuiltIn(BuiltInGuardrailValidator): + def get_built_in_guardrail(self, name, description, enabled_for_evals): + return MagicMock(spec=BuiltInValidatorGuardrail) + + for var in ( + "UIPATH_URL", + "UNATTENDED_USER_ACCESS_TOKEN", + "UIPATH_ACCESS_TOKEN", + ): + monkeypatch.delenv(var, raising=False) + + evaluator = _make_evaluator(_TestBuiltIn(), "G", None, True) + with pytest.raises(BaseUrlMissingError): + evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) + + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + with pytest.raises(SecretMissingError): + evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) + + +# --------------------------------------------------------------------------- +# 15b. DefaultGuardrailEvaluator +# --------------------------------------------------------------------------- + + +class TestDefaultGuardrailEvaluator: + def test_constructs_service_from_env_and_delegates(self, monkeypatch): + """The default evaluator builds a GuardrailsService from env config.""" + from uipath.platform.guardrails._default_evaluator import ( + DefaultGuardrailEvaluator, + ) + from uipath.platform.guardrails._guardrails_service import GuardrailsService + from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail + + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "token") + + evaluator = DefaultGuardrailEvaluator() + assert isinstance(evaluator._service, GuardrailsService) + + mock_built_in = MagicMock(spec=BuiltInValidatorGuardrail) + with patch.object( + evaluator._service, "evaluate_guardrail", return_value=_PASSED + ) as mock_evaluate: + result = evaluator.evaluate_guardrail("hello", mock_built_in) + + mock_evaluate.assert_called_once_with("hello", mock_built_in) + assert result == _PASSED # --------------------------------------------------------------------------- @@ -1219,8 +1305,8 @@ def test_real_validator_block_end_to_end_mocked_backend(self): """Real LLMAsJudgeValidator.run() path with the UiPath API mocked.""" from uipath.platform.guardrails.decorators import LLMAsJudgeValidator - mock_uipath = MagicMock() - mock_uipath.guardrails.evaluate_guardrail.return_value = _FAILED + mock_evaluator = MagicMock() + mock_evaluator.evaluate_guardrail.return_value = _FAILED @guardrail( validator=LLMAsJudgeValidator( @@ -1232,7 +1318,10 @@ def test_real_validator_block_end_to_end_mocked_backend(self): def joke(topic: str) -> str: return f"joke about {topic}" - with patch("uipath.platform.UiPath", return_value=mock_uipath): + with patch( + "uipath.platform.guardrails.decorators.validators._base.DefaultGuardrailEvaluator", + return_value=mock_evaluator, + ): with pytest.raises(GuardrailBlockException): joke("cats") - mock_uipath.guardrails.evaluate_guardrail.assert_called_once() + mock_evaluator.evaluate_guardrail.assert_called_once() From fb992228400a84679b7c11065adc73805b14c4f5 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Mon, 6 Jul 2026 13:47:14 +0300 Subject: [PATCH 4/9] feat: use | instead of : --- packages/uipath-platform/pyproject.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 7af6e47b7..18311d81a 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -133,17 +133,17 @@ exclude_type_checking_imports = true [[tool.importlinter.contracts]] id = "platform-layers" -name = "uipath.platform layered architecture" +name = "uipath-platform layered architecture" type = "layers" containers = ["uipath.platform"] exhaustive = true layers = [ - "_uipath : resume_triggers", - "agenthub : automation_ops : automation_tracker : connections : context_grounding : entities : external_applications : governance : guardrails : memory : pii_detection : portal : resource_catalog : semantic_proxy", - "orchestrator", - "action_center : attachments : chat : documents : identity", - "common", - "constants : errors", + "_uipath : resume_triggers", + "agenthub | automation_ops | automation_tracker | connections | context_grounding | entities | external_applications | governance | guardrails | memory | pii_detection | portal | resource_catalog | semantic_proxy", + "orchestrator", + "action_center | attachments | chat | documents | identity", + "common", + "constants | errors", ] [tool.uv] From f56b3bf3adec8aaf885fe782ee1c75f60c551bc2 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Tue, 14 Jul 2026 15:12:22 +0300 Subject: [PATCH 5/9] fix(platform): restore common interrupt model compatibility --- packages/uipath-platform/pyproject.toml | 5 + .../src/uipath/platform/common/__init__.py | 105 ++++++++++++ .../platform/common/interrupt_models.py | 93 +++++++++++ .../test_interrupt_models_compatibility.py | 154 ++++++++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 packages/uipath-platform/src/uipath/platform/common/interrupt_models.py create mode 100644 packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 18311d81a..ea3a1b0db 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -137,6 +137,11 @@ name = "uipath-platform layered architecture" type = "layers" containers = ["uipath.platform"] exhaustive = true +# TODO: Remove this exception in the next breaking-change release. +ignore_imports = [ + "uipath.platform.common.interrupt_models -> uipath.platform.resume_triggers.interrupt_models", +] +unmatched_ignore_imports_alerting = "error" layers = [ "_uipath : resume_triggers", "agenthub | automation_ops | automation_tracker | connections | context_grounding | entities | external_applications | governance | guardrails | memory | pii_detection | portal | resource_catalog | semantic_proxy", diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 7f523e602..f2034a0e6 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -3,8 +3,41 @@ This module contains common models used across multiple services. """ +from typing import TYPE_CHECKING, Any + from uipath.core.triggers import UiPathResumeMetadata +# TODO: Remove the interrupt-model compatibility exports in the next breaking-change release. +if TYPE_CHECKING: + from .interrupt_models import ( + CreateBatchTransform, + CreateDeepRag, + CreateDeepRagRaw, + CreateEphemeralIndex, + CreateEphemeralIndexRaw, + CreateEscalation, + CreateTask, + DocumentExtraction, + DocumentExtractionValidation, + InvokeProcess, + InvokeProcessRaw, + InvokeSystemAgent, + WaitBatchTransform, + WaitDeepRag, + WaitDeepRagRaw, + WaitDocumentExtraction, + WaitDocumentExtractionValidation, + WaitEphemeralIndex, + WaitEphemeralIndexRaw, + WaitEscalation, + WaitIntegrationEvent, + WaitJob, + WaitJobRaw, + WaitSystemAgent, + WaitTask, + WaitUntil, + ) + from ._api_client import ApiClient from ._base_service import BaseService, resolve_trace_id from ._bindings import ( @@ -58,7 +91,33 @@ "FolderContext", "TokenData", "UiPathConfig", + "CreateTask", + "CreateEscalation", + "WaitEscalation", + "InvokeProcess", + "InvokeProcessRaw", + "WaitTask", + "WaitJob", + "WaitJobRaw", "PagedResult", + "CreateDeepRag", + "CreateDeepRagRaw", + "WaitDeepRag", + "WaitDeepRagRaw", + "CreateBatchTransform", + "WaitBatchTransform", + "DocumentExtraction", + "WaitDocumentExtraction", + "InvokeSystemAgent", + "WaitSystemAgent", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "DocumentExtractionValidation", + "WaitDocumentExtractionValidation", + "WaitIntegrationEvent", + "WaitUntil", "RequestSpec", "Endpoint", "UiPathUrl", @@ -97,3 +156,49 @@ ] from .validation import validate_pagination_params + +_INTERRUPT_MODEL_NAMES = { + "CreateBatchTransform", + "CreateDeepRag", + "CreateDeepRagRaw", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "CreateEscalation", + "CreateTask", + "DocumentExtraction", + "DocumentExtractionValidation", + "InvokeProcess", + "InvokeProcessRaw", + "InvokeSystemAgent", + "WaitBatchTransform", + "WaitDeepRag", + "WaitDeepRagRaw", + "WaitDocumentExtraction", + "WaitDocumentExtractionValidation", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "WaitEscalation", + "WaitIntegrationEvent", + "WaitJob", + "WaitJobRaw", + "WaitSystemAgent", + "WaitTask", + "WaitUntil", +} + + +def __getattr__(name: str) -> Any: + """Resolve moved interrupt models on demand.""" + if name not in _INTERRUPT_MODEL_NAMES: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + from . import interrupt_models + + value = getattr(interrupt_models, name) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + """List common exports, including compatibility aliases.""" + return sorted(set(globals()) | set(__all__)) diff --git a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py new file mode 100644 index 000000000..be31544c3 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py @@ -0,0 +1,93 @@ +"""Deprecated lazy aliases for resume trigger interrupt models.""" + +import warnings +from typing import TYPE_CHECKING, Any + +# TODO: Remove this compatibility module in the next breaking-change release. +if TYPE_CHECKING: + from uipath.platform.resume_triggers.interrupt_models import ( + CreateBatchTransform, + CreateDeepRag, + CreateDeepRagRaw, + CreateEphemeralIndex, + CreateEphemeralIndexRaw, + CreateEscalation, + CreateTask, + DocumentExtraction, + DocumentExtractionValidation, + InvokeProcess, + InvokeProcessRaw, + InvokeSystemAgent, + WaitBatchTransform, + WaitDeepRag, + WaitDeepRagRaw, + WaitDocumentExtraction, + WaitDocumentExtractionValidation, + WaitEphemeralIndex, + WaitEphemeralIndexRaw, + WaitEscalation, + WaitIntegrationEvent, + WaitJob, + WaitJobRaw, + WaitSystemAgent, + WaitTask, + WaitUntil, + ) + +__all__ = [ + "CreateBatchTransform", + "CreateDeepRag", + "CreateDeepRagRaw", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "CreateEscalation", + "CreateTask", + "DocumentExtraction", + "DocumentExtractionValidation", + "InvokeProcess", + "InvokeProcessRaw", + "InvokeSystemAgent", + "WaitBatchTransform", + "WaitDeepRag", + "WaitDeepRagRaw", + "WaitDocumentExtraction", + "WaitDocumentExtractionValidation", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "WaitEscalation", + "WaitIntegrationEvent", + "WaitJob", + "WaitJobRaw", + "WaitSystemAgent", + "WaitTask", + "WaitUntil", +] + +_warning_emitted = False + + +def __getattr__(name: str) -> Any: + """Resolve deprecated interrupt model aliases on demand.""" + if name not in __all__: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + import uipath.platform.resume_triggers.interrupt_models as canonical_models + + value = getattr(canonical_models, name) + global _warning_emitted + if not _warning_emitted: + warnings.warn( + "Interrupt models from uipath.platform.common are deprecated and will " + "be removed in a future release; import from " + "uipath.platform.resume_triggers instead.", + FutureWarning, + stacklevel=2, + ) + _warning_emitted = True + globals()[name] = value + return value + + +def __dir__() -> list[str]: + """List compatibility exports.""" + return sorted(set(globals()) | set(__all__)) diff --git a/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py b/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py new file mode 100644 index 000000000..5f7c02b5c --- /dev/null +++ b/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py @@ -0,0 +1,154 @@ +import importlib +import subprocess +import sys +import textwrap +import warnings + +import pytest + +# TODO: Remove these tests with the compatibility bridge in the next breaking-change release. +INTERRUPT_MODEL_NAMES = ( + "CreateBatchTransform", + "CreateDeepRag", + "CreateDeepRagRaw", + "CreateEphemeralIndex", + "CreateEphemeralIndexRaw", + "CreateEscalation", + "CreateTask", + "DocumentExtraction", + "DocumentExtractionValidation", + "InvokeProcess", + "InvokeProcessRaw", + "InvokeSystemAgent", + "WaitBatchTransform", + "WaitDeepRag", + "WaitDeepRagRaw", + "WaitDocumentExtraction", + "WaitDocumentExtractionValidation", + "WaitEphemeralIndex", + "WaitEphemeralIndexRaw", + "WaitEscalation", + "WaitIntegrationEvent", + "WaitJob", + "WaitJobRaw", + "WaitSystemAgent", + "WaitTask", + "WaitUntil", +) + + +def _run_python(source: str) -> None: + result = subprocess.run( + [sys.executable, "-c", source], + capture_output=True, + check=False, + text=True, + ) + assert result.returncode == 0, result.stderr + + +def test_compatibility_exports_are_canonical_objects(): + canonical = importlib.import_module( + "uipath.platform.resume_triggers.interrupt_models" + ) + common = importlib.import_module("uipath.platform.common") + legacy = importlib.import_module("uipath.platform.common.interrupt_models") + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + for name in INTERRUPT_MODEL_NAMES: + canonical_model = getattr(canonical, name) + assert getattr(legacy, name) is canonical_model + assert getattr(common, name) is canonical_model + + compatibility_warnings = [ + warning + for warning in caught + if issubclass(warning.category, FutureWarning) + and "uipath.platform.resume_triggers" in str(warning.message) + ] + assert len(compatibility_warnings) == 1 + assert set(INTERRUPT_MODEL_NAMES) == set(legacy.__all__) + assert set(INTERRUPT_MODEL_NAMES) <= set(common.__all__) + assert set(INTERRUPT_MODEL_NAMES) <= set(dir(legacy)) + assert set(INTERRUPT_MODEL_NAMES) <= set(dir(common)) + + +def test_importing_common_does_not_load_resume_triggers_or_warn(): + _run_python( + """ +import sys +import warnings + +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + import uipath.platform.common + +assert "uipath.platform.resume_triggers" not in sys.modules +assert "uipath.platform.resume_triggers.interrupt_models" not in sys.modules +assert not [item for item in caught if issubclass(item.category, FutureWarning)] +""" + ) + + +@pytest.mark.parametrize( + "imports", + [ + """ +from uipath.platform.resume_triggers import WaitUntil as canonical +from uipath.platform.common import WaitUntil as legacy_root +from uipath.platform.common.interrupt_models import WaitUntil as legacy_direct +""", + """ +from uipath.platform.common.interrupt_models import WaitUntil as legacy_direct +from uipath.platform.common import WaitUntil as legacy_root +from uipath.platform.resume_triggers import WaitUntil as canonical +""", + ], +) +def test_import_orders_are_cycle_safe(imports: str): + indented_imports = textwrap.indent(imports.strip(), " ") + _run_python( + f""" +import warnings + +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") +{indented_imports} + +assert canonical is legacy_root is legacy_direct +compatibility_warnings = [ + item + for item in caught + if issubclass(item.category, FutureWarning) + and "uipath.platform.resume_triggers" in str(item.message) +] +assert len(compatibility_warnings) == 1 +""" + ) + + +def test_only_first_legacy_model_resolution_warns(): + _run_python( + """ +import importlib +import warnings + +common = importlib.import_module("uipath.platform.common") +legacy = importlib.import_module("uipath.platform.common.interrupt_models") + +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + legacy.WaitUntil + legacy.WaitTask + common.WaitJob + +compatibility_warnings = [ + item + for item in caught + if issubclass(item.category, FutureWarning) + and "uipath.platform.resume_triggers" in str(item.message) +] +assert len(compatibility_warnings) == 1 +""" + ) From d809f1744898b26e4cc1659ec3d1d12bf5aab91d Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Tue, 14 Jul 2026 15:56:09 +0300 Subject: [PATCH 6/9] fix(platform): preserve interrupt model compatibility --- .../src/uipath/platform/common/__init__.py | 2 +- .../platform/common/interrupt_models.py | 35 ++++---- .../test_interrupt_models_compatibility.py | 79 +++++++++++++------ 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index f2034a0e6..d322c1aea 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -194,7 +194,7 @@ def __getattr__(name: str) -> Any: from . import interrupt_models - value = getattr(interrupt_models, name) + value = interrupt_models._resolve(name) globals()[name] = value return value diff --git a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py index be31544c3..e47406417 100644 --- a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py +++ b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py @@ -63,31 +63,34 @@ "WaitUntil", ] -_warning_emitted = False +def _resolve(name: str) -> Any: + """Resolve a deprecated alias from the canonical module and warn. -def __getattr__(name: str) -> Any: - """Resolve deprecated interrupt model aliases on demand.""" - if name not in __all__: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - + Called only from a module ``__getattr__`` (here or in ``common``), so the + frame three levels up from ``warnings.warn`` is the user's import site. + """ import uipath.platform.resume_triggers.interrupt_models as canonical_models value = getattr(canonical_models, name) - global _warning_emitted - if not _warning_emitted: - warnings.warn( - "Interrupt models from uipath.platform.common are deprecated and will " - "be removed in a future release; import from " - "uipath.platform.resume_triggers instead.", - FutureWarning, - stacklevel=2, - ) - _warning_emitted = True + warnings.warn( + "Importing interrupt models from uipath.platform.common is deprecated " + "and will be removed in a future release; import from " + "uipath.platform.resume_triggers instead.", + DeprecationWarning, + stacklevel=3, + ) globals()[name] = value return value +def __getattr__(name: str) -> Any: + """Resolve deprecated interrupt model aliases on demand.""" + if name not in __all__: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + return _resolve(name) + + def __dir__() -> list[str]: """List compatibility exports.""" return sorted(set(globals()) | set(__all__)) diff --git a/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py b/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py index 5f7c02b5c..ae3a8a22f 100644 --- a/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py +++ b/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py @@ -54,23 +54,15 @@ def test_compatibility_exports_are_canonical_objects(): common = importlib.import_module("uipath.platform.common") legacy = importlib.import_module("uipath.platform.common.interrupt_models") - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) for name in INTERRUPT_MODEL_NAMES: - canonical_model = getattr(canonical, name) - assert getattr(legacy, name) is canonical_model - assert getattr(common, name) is canonical_model - - compatibility_warnings = [ - warning - for warning in caught - if issubclass(warning.category, FutureWarning) - and "uipath.platform.resume_triggers" in str(warning.message) - ] - assert len(compatibility_warnings) == 1 + assert getattr(legacy, name) is getattr(canonical, name) + assert getattr(common, name) is getattr(canonical, name) + assert set(INTERRUPT_MODEL_NAMES) == set(legacy.__all__) assert set(INTERRUPT_MODEL_NAMES) <= set(common.__all__) - assert set(INTERRUPT_MODEL_NAMES) <= set(dir(legacy)) + assert set(legacy.__all__) <= set(dir(legacy)) assert set(INTERRUPT_MODEL_NAMES) <= set(dir(common)) @@ -86,7 +78,7 @@ def test_importing_common_does_not_load_resume_triggers_or_warn(): assert "uipath.platform.resume_triggers" not in sys.modules assert "uipath.platform.resume_triggers.interrupt_models" not in sys.modules -assert not [item for item in caught if issubclass(item.category, FutureWarning)] +assert not [item for item in caught if issubclass(item.category, DeprecationWarning)] """ ) @@ -120,35 +112,72 @@ def test_import_orders_are_cycle_safe(imports: str): compatibility_warnings = [ item for item in caught - if issubclass(item.category, FutureWarning) + if issubclass(item.category, DeprecationWarning) and "uipath.platform.resume_triggers" in str(item.message) ] -assert len(compatibility_warnings) == 1 +assert compatibility_warnings """ ) -def test_only_first_legacy_model_resolution_warns(): +def test_legacy_imports_warn_at_caller_site(): _run_python( """ -import importlib import warnings -common = importlib.import_module("uipath.platform.common") -legacy = importlib.import_module("uipath.platform.common.interrupt_models") +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + from uipath.platform.common import WaitUntil + from uipath.platform.common.interrupt_models import WaitTask + +compatibility_warnings = [ + item + for item in caught + if issubclass(item.category, DeprecationWarning) + and "uipath.platform.resume_triggers" in str(item.message) +] +assert len(compatibility_warnings) == 2 +assert all(item.filename == "" for item in compatibility_warnings), [ + item.filename for item in compatibility_warnings +] +""" + ) + + +def test_cached_resolution_warns_once_per_name(): + _run_python( + """ +import warnings + +import uipath.platform.common as common with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") - legacy.WaitUntil - legacy.WaitTask + common.WaitUntil + common.WaitUntil common.WaitJob compatibility_warnings = [ item for item in caught - if issubclass(item.category, FutureWarning) + if issubclass(item.category, DeprecationWarning) and "uipath.platform.resume_triggers" in str(item.message) ] -assert len(compatibility_warnings) == 1 +assert len(compatibility_warnings) == 2 +""" + ) + + +def test_star_import_exports_all_interrupt_models(): + _run_python( + """ +import warnings + +warnings.simplefilter("ignore", DeprecationWarning) + +from uipath.platform.common.interrupt_models import * # noqa: F403 + +for name in ("InvokeProcess", "WaitUntil", "CreateTask", "WaitIntegrationEvent"): + assert name in globals(), name """ ) From 44573c14cd92489a808021152ebfd26e8d743032 Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Tue, 14 Jul 2026 16:35:12 +0300 Subject: [PATCH 7/9] refactor: use GuardrailsService directly in guardrail validators --- .../platform/guardrails/_default_evaluator.py | 33 ----------- .../uipath/platform/guardrails/_evaluator.py | 19 ------ .../guardrails/_guardrails_service.py | 9 +++ .../guardrails/decorators/validators/_base.py | 16 ++--- .../services/test_guardrails_decorators.py | 58 ++++++++----------- 5 files changed, 43 insertions(+), 92 deletions(-) delete mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/_default_evaluator.py delete mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/_evaluator.py diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/_default_evaluator.py b/packages/uipath-platform/src/uipath/platform/guardrails/_default_evaluator.py deleted file mode 100644 index 44381ef17..000000000 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_default_evaluator.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Default env-driven guardrail evaluator.""" - -from typing import Any - -from uipath.core.guardrails import GuardrailValidationResult - -from ..common import UiPathExecutionContext -from ..common.auth import build_api_config, resolve_config_from_env -from ._guardrails_service import GuardrailsService -from .guardrails import BuiltInValidatorGuardrail - - -class DefaultGuardrailEvaluator: - """Evaluates guardrails via a ``GuardrailsService`` configured from env vars. - - Raises: - BaseUrlMissingError: If the base URL is not configured. - SecretMissingError: If the access token is not configured. - """ - - def __init__(self) -> None: - base_url, secret = resolve_config_from_env(None, None) - self._service = GuardrailsService( - build_api_config(base_url, secret), UiPathExecutionContext() - ) - - def evaluate_guardrail( - self, - input_data: str | dict[str, Any], - guardrail: BuiltInValidatorGuardrail, - ) -> GuardrailValidationResult: - """Validate input data using the provided guardrail.""" - return self._service.evaluate_guardrail(input_data, guardrail) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/_evaluator.py b/packages/uipath-platform/src/uipath/platform/guardrails/_evaluator.py deleted file mode 100644 index bb4dc7892..000000000 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_evaluator.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Internal protocol for guardrail evaluation capabilities.""" - -from typing import Any, Protocol - -from uipath.core.guardrails import GuardrailValidationResult - -from .guardrails import BuiltInValidatorGuardrail - - -class GuardrailEvaluator(Protocol): - """Capability to evaluate a built-in guardrail against input data.""" - - def evaluate_guardrail( - self, - input_data: str | dict[str, Any], - guardrail: BuiltInValidatorGuardrail, - ) -> GuardrailValidationResult: - """Validate input data using the provided guardrail.""" - ... 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 b73d810e7..070581aba 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py @@ -16,6 +16,7 @@ from ..common._execution_context import UiPathExecutionContext from ..common._job_context import header_job_key from ..common._models import Endpoint, RequestSpec +from ..common.auth import build_api_config, resolve_config_from_env from ..errors import EnrichedException from .guardrails import BYO_VALIDATOR_TYPE, BuiltInValidatorGuardrail @@ -203,3 +204,11 @@ def evaluate_guardrail( model_data["spanId"] = span_id return GuardrailValidationResult.model_validate(model_data) + + +def default_guardrails_service() -> GuardrailsService: + """Build a guardrails service configured from environment variables.""" + base_url, secret = resolve_config_from_env(None, None) + return GuardrailsService( + build_api_config(base_url, secret), UiPathExecutionContext() + ) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py index 02aad480d..f835f029b 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py @@ -5,8 +5,10 @@ from uipath.core.guardrails import GuardrailValidationResult -from uipath.platform.guardrails._default_evaluator import DefaultGuardrailEvaluator -from uipath.platform.guardrails._evaluator import GuardrailEvaluator +from ..._guardrails_service import ( + GuardrailsService, + default_guardrails_service, +) from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail from .._enums import GuardrailExecutionStage @@ -80,7 +82,7 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): ) """ - _evaluator: GuardrailEvaluator | None = None + _service: GuardrailsService | None = None @abstractmethod def get_built_in_guardrail( @@ -113,13 +115,13 @@ def run( ) -> GuardrailValidationResult: """Evaluate via the UiPath Guardrails API. - Lazily initialises the guardrail evaluator on the first call and + Lazily initialises the guardrails service on the first call and reuses it for all subsequent invocations. """ built_in = self.get_built_in_guardrail(name, description, enabled_for_evals) - if self._evaluator is None: - self._evaluator = DefaultGuardrailEvaluator() - return self._evaluator.evaluate_guardrail(data, built_in) + if self._service is None: + self._service = default_guardrails_service() + return self._service.evaluate_guardrail(data, built_in) class CustomGuardrailValidator(GuardrailValidatorBase, ABC): diff --git a/packages/uipath-platform/tests/services/test_guardrails_decorators.py b/packages/uipath-platform/tests/services/test_guardrails_decorators.py index 7ca4d7312..9539a4d2f 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_decorators.py +++ b/packages/uipath-platform/tests/services/test_guardrails_decorators.py @@ -989,8 +989,8 @@ def test_custom_validator_path_delegates_to_run(self): ) assert result == _PASSED - def test_built_in_validator_uses_injected_evaluator(self): - """BuiltInGuardrailValidator.run() delegates to an injected evaluator.""" + def test_built_in_validator_uses_injected_service(self): + """BuiltInGuardrailValidator.run() delegates to an injected service.""" from uipath.platform.guardrails.decorators.validators import ( BuiltInGuardrailValidator, ) @@ -1003,20 +1003,20 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): return mock_built_in validator = _TestBuiltIn() - fake_evaluator = MagicMock() - fake_evaluator.evaluate_guardrail.return_value = _PASSED - validator._evaluator = fake_evaluator + fake_service = MagicMock() + fake_service.evaluate_guardrail.return_value = _PASSED + validator._service = fake_service evaluator = _make_evaluator(validator, "G", None, True) result = evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) - fake_evaluator.evaluate_guardrail.assert_called_once_with( + fake_service.evaluate_guardrail.assert_called_once_with( {"text": "hello"}, mock_built_in ) assert result == _PASSED - def test_built_in_validator_path_lazy_initializes_default_evaluator(self): - """BuiltInGuardrailValidator.run() creates the default evaluator once.""" + def test_built_in_validator_path_lazy_initializes_default_service(self): + """BuiltInGuardrailValidator.run() creates the default service once.""" from uipath.platform.guardrails.decorators.validators import ( BuiltInGuardrailValidator, ) @@ -1032,7 +1032,7 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): evaluator = _make_evaluator(validator, "G", None, True) with patch( - "uipath.platform.guardrails.decorators.validators._base.DefaultGuardrailEvaluator" + "uipath.platform.guardrails.decorators.validators._base.default_guardrails_service" ) as mock_default: mock_default.return_value.evaluate_guardrail.return_value = _PASSED evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) @@ -1072,33 +1072,25 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): # --------------------------------------------------------------------------- -# 15b. DefaultGuardrailEvaluator +# 15b. default_guardrails_service # --------------------------------------------------------------------------- -class TestDefaultGuardrailEvaluator: - def test_constructs_service_from_env_and_delegates(self, monkeypatch): - """The default evaluator builds a GuardrailsService from env config.""" - from uipath.platform.guardrails._default_evaluator import ( - DefaultGuardrailEvaluator, +class TestDefaultGuardrailsService: + def test_builds_guardrails_service_from_env(self, monkeypatch): + """The default factory builds a GuardrailsService from env config.""" + from uipath.platform.guardrails._guardrails_service import ( + GuardrailsService, + default_guardrails_service, ) - from uipath.platform.guardrails._guardrails_service import GuardrailsService - from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "token") - evaluator = DefaultGuardrailEvaluator() - assert isinstance(evaluator._service, GuardrailsService) - - mock_built_in = MagicMock(spec=BuiltInValidatorGuardrail) - with patch.object( - evaluator._service, "evaluate_guardrail", return_value=_PASSED - ) as mock_evaluate: - result = evaluator.evaluate_guardrail("hello", mock_built_in) - - mock_evaluate.assert_called_once_with("hello", mock_built_in) - assert result == _PASSED + service = default_guardrails_service() + assert isinstance(service, GuardrailsService) + assert service._config.base_url == "https://test.uipath.com" + assert service._config.secret == "token" # --------------------------------------------------------------------------- @@ -1305,8 +1297,8 @@ def test_real_validator_block_end_to_end_mocked_backend(self): """Real LLMAsJudgeValidator.run() path with the UiPath API mocked.""" from uipath.platform.guardrails.decorators import LLMAsJudgeValidator - mock_evaluator = MagicMock() - mock_evaluator.evaluate_guardrail.return_value = _FAILED + mock_service = MagicMock() + mock_service.evaluate_guardrail.return_value = _FAILED @guardrail( validator=LLMAsJudgeValidator( @@ -1319,9 +1311,9 @@ def joke(topic: str) -> str: return f"joke about {topic}" with patch( - "uipath.platform.guardrails.decorators.validators._base.DefaultGuardrailEvaluator", - return_value=mock_evaluator, + "uipath.platform.guardrails.decorators.validators._base.default_guardrails_service", + return_value=mock_service, ): with pytest.raises(GuardrailBlockException): joke("cats") - mock_evaluator.evaluate_guardrail.assert_called_once() + mock_service.evaluate_guardrail.assert_called_once() From c2a3fdf41f6c45923504b698ffe3bf240a8a314d Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Tue, 14 Jul 2026 17:51:57 +0300 Subject: [PATCH 8/9] fix: manual cleanup --- .../test_interrupt_models_compatibility.py | 92 +------------------ .../services/test_guardrails_decorators.py | 48 ---------- .../tests/services/test_guardrails_service.py | 13 +++ 3 files changed, 18 insertions(+), 135 deletions(-) diff --git a/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py b/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py index ae3a8a22f..084bd6702 100644 --- a/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py +++ b/packages/uipath-platform/tests/common/test_interrupt_models_compatibility.py @@ -1,11 +1,8 @@ import importlib import subprocess import sys -import textwrap import warnings -import pytest - # TODO: Remove these tests with the compatibility bridge in the next breaking-change release. INTERRUPT_MODEL_NAMES = ( "CreateBatchTransform", @@ -62,8 +59,6 @@ def test_compatibility_exports_are_canonical_objects(): assert set(INTERRUPT_MODEL_NAMES) == set(legacy.__all__) assert set(INTERRUPT_MODEL_NAMES) <= set(common.__all__) - assert set(legacy.__all__) <= set(dir(legacy)) - assert set(INTERRUPT_MODEL_NAMES) <= set(dir(common)) def test_importing_common_does_not_load_resume_triggers_or_warn(): @@ -83,30 +78,16 @@ def test_importing_common_does_not_load_resume_triggers_or_warn(): ) -@pytest.mark.parametrize( - "imports", - [ - """ -from uipath.platform.resume_triggers import WaitUntil as canonical -from uipath.platform.common import WaitUntil as legacy_root -from uipath.platform.common.interrupt_models import WaitUntil as legacy_direct -""", - """ -from uipath.platform.common.interrupt_models import WaitUntil as legacy_direct -from uipath.platform.common import WaitUntil as legacy_root -from uipath.platform.resume_triggers import WaitUntil as canonical -""", - ], -) -def test_import_orders_are_cycle_safe(imports: str): - indented_imports = textwrap.indent(imports.strip(), " ") +def test_legacy_first_import_is_cycle_safe_and_warns(): _run_python( - f""" + """ import warnings with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") -{indented_imports} + from uipath.platform.common.interrupt_models import WaitUntil as legacy_direct + from uipath.platform.common import WaitUntil as legacy_root + from uipath.platform.resume_triggers import WaitUntil as canonical assert canonical is legacy_root is legacy_direct compatibility_warnings = [ @@ -118,66 +99,3 @@ def test_import_orders_are_cycle_safe(imports: str): assert compatibility_warnings """ ) - - -def test_legacy_imports_warn_at_caller_site(): - _run_python( - """ -import warnings - -with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - from uipath.platform.common import WaitUntil - from uipath.platform.common.interrupt_models import WaitTask - -compatibility_warnings = [ - item - for item in caught - if issubclass(item.category, DeprecationWarning) - and "uipath.platform.resume_triggers" in str(item.message) -] -assert len(compatibility_warnings) == 2 -assert all(item.filename == "" for item in compatibility_warnings), [ - item.filename for item in compatibility_warnings -] -""" - ) - - -def test_cached_resolution_warns_once_per_name(): - _run_python( - """ -import warnings - -import uipath.platform.common as common - -with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - common.WaitUntil - common.WaitUntil - common.WaitJob - -compatibility_warnings = [ - item - for item in caught - if issubclass(item.category, DeprecationWarning) - and "uipath.platform.resume_triggers" in str(item.message) -] -assert len(compatibility_warnings) == 2 -""" - ) - - -def test_star_import_exports_all_interrupt_models(): - _run_python( - """ -import warnings - -warnings.simplefilter("ignore", DeprecationWarning) - -from uipath.platform.common.interrupt_models import * # noqa: F403 - -for name in ("InvokeProcess", "WaitUntil", "CreateTask", "WaitIntegrationEvent"): - assert name in globals(), name -""" - ) diff --git a/packages/uipath-platform/tests/services/test_guardrails_decorators.py b/packages/uipath-platform/tests/services/test_guardrails_decorators.py index 9539a4d2f..161d6da5a 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_decorators.py +++ b/packages/uipath-platform/tests/services/test_guardrails_decorators.py @@ -989,32 +989,6 @@ def test_custom_validator_path_delegates_to_run(self): ) assert result == _PASSED - def test_built_in_validator_uses_injected_service(self): - """BuiltInGuardrailValidator.run() delegates to an injected service.""" - from uipath.platform.guardrails.decorators.validators import ( - BuiltInGuardrailValidator, - ) - from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail - - mock_built_in = MagicMock(spec=BuiltInValidatorGuardrail) - - class _TestBuiltIn(BuiltInGuardrailValidator): - def get_built_in_guardrail(self, name, description, enabled_for_evals): - return mock_built_in - - validator = _TestBuiltIn() - fake_service = MagicMock() - fake_service.evaluate_guardrail.return_value = _PASSED - validator._service = fake_service - evaluator = _make_evaluator(validator, "G", None, True) - - result = evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) - - fake_service.evaluate_guardrail.assert_called_once_with( - {"text": "hello"}, mock_built_in - ) - assert result == _PASSED - def test_built_in_validator_path_lazy_initializes_default_service(self): """BuiltInGuardrailValidator.run() creates the default service once.""" from uipath.platform.guardrails.decorators.validators import ( @@ -1071,28 +1045,6 @@ def get_built_in_guardrail(self, name, description, enabled_for_evals): evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) -# --------------------------------------------------------------------------- -# 15b. default_guardrails_service -# --------------------------------------------------------------------------- - - -class TestDefaultGuardrailsService: - def test_builds_guardrails_service_from_env(self, monkeypatch): - """The default factory builds a GuardrailsService from env config.""" - from uipath.platform.guardrails._guardrails_service import ( - GuardrailsService, - default_guardrails_service, - ) - - monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") - monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "token") - - service = default_guardrails_service() - assert isinstance(service, GuardrailsService) - assert service._config.base_url == "https://test.uipath.com" - assert service._config.secret == "token" - - # --------------------------------------------------------------------------- # 16. Joke-agent integration scenarios (plain functions) # --------------------------------------------------------------------------- diff --git a/packages/uipath-platform/tests/services/test_guardrails_service.py b/packages/uipath-platform/tests/services/test_guardrails_service.py index 6fd20bbc5..121bbd8ab 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_service.py +++ b/packages/uipath-platform/tests/services/test_guardrails_service.py @@ -17,6 +17,7 @@ GuardrailsService, MapEnumParameterValue, ) +from uipath.platform.guardrails._guardrails_service import default_guardrails_service @pytest.fixture @@ -29,6 +30,18 @@ def service( return GuardrailsService(config=config, execution_context=execution_context) +class TestDefaultGuardrailsService: + def test_builds_guardrails_service_from_env(self, monkeypatch): + """The default factory builds a GuardrailsService from env config.""" + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "token") + + service = default_guardrails_service() + assert isinstance(service, GuardrailsService) + assert service._config.base_url == "https://test.uipath.com" + assert service._config.secret == "token" + + class TestGuardrailsService: """Test GuardrailsService functionality.""" From 69c4bfe78d79c5c3e62e52d2dc5a2b5b32ab669d Mon Sep 17 00:00:00 2001 From: Andrei Ancuta Date: Tue, 14 Jul 2026 19:07:07 +0300 Subject: [PATCH 9/9] chore(platform): prepare 0.2.10 release --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath/platform/guardrails/decorators/validators/_base.py | 4 ++-- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index ea3a1b0db..bae8dc441 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.9" +version = "0.2.10" 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/decorators/validators/_base.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py index f835f029b..d0cc75f69 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py @@ -5,12 +5,12 @@ from uipath.core.guardrails import GuardrailValidationResult +from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail + from ..._guardrails_service import ( GuardrailsService, default_guardrails_service, ) -from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail - from .._enums import GuardrailExecutionStage diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 5806f630e..1d4b41b14 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1215,7 +1215,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.9" +version = "0.2.10" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 35a3d7eb3..657d0bd59 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2741,7 +2741,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.9" +version = "0.2.10" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" },