From fb2b1e12b84a2ea2c24dbe08ed924c0b42bd358f Mon Sep 17 00:00:00 2001 From: Akshaya Shanbhogue Date: Mon, 22 Dec 2025 11:50:25 -0800 Subject: [PATCH] fix(CircularDependencies): fix circular dependencies This will avoid accidentally introducing issues. Additionally, including justfile. --- .github/workflows/lint.yml | 2 +- justfile | 19 +++++ pyproject.toml | 5 +- src/uipath_langchain/agent/react/agent.py | 3 +- .../agent/tools/escalation_tool.py | 2 +- .../agent/tools/integration_tool.py | 5 +- testcases/common/__init__.py | 2 +- tests/test_no_circular_imports.py | 81 +++++++++++++++++++ uv.lock | 26 +++++- 9 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 justfile create mode 100644 tests/test_no_circular_imports.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 45a2a502..f6c8494f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -49,6 +49,6 @@ jobs: - name: Check formatting run: uv run ruff format --check . - + - name: Check httpx.Client() usage run: uv run python scripts/lint_httpx_client.py diff --git a/justfile b/justfile new file mode 100644 index 00000000..7794fbf3 --- /dev/null +++ b/justfile @@ -0,0 +1,19 @@ +set quiet + +default: lint format + +lint: + ruff check . + python scripts/lint_httpx_client.py + +format: + ruff format --check . + ruff check --fix + +validate: lint format + +build: + uv build + +install: + uv sync --all-extras diff --git a/pyproject.toml b/pyproject.toml index 18caa61a..f173cfa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-langchain" -version = "0.1.43" +version = "0.1.44" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" @@ -69,7 +69,8 @@ dev = [ "pytest-asyncio>=1.0.0", "pre-commit>=4.1.0", "numpy>=1.24.0", - "pytest_httpx>=0.35.0" + "pytest_httpx>=0.35.0", + "rust-just>=1.39.0", ] [tool.hatch.build.targets.wheel] diff --git a/src/uipath_langchain/agent/react/agent.py b/src/uipath_langchain/agent/react/agent.py index 10d0b60a..2a1f233b 100644 --- a/src/uipath_langchain/agent/react/agent.py +++ b/src/uipath_langchain/agent/react/agent.py @@ -10,7 +10,6 @@ from uipath.platform.guardrails import BaseGuardrail from ..guardrails.actions import GuardrailAction -from ..tools import create_tool_node from .guardrails.guardrails_subgraph import ( create_agent_init_guardrails_subgraph, create_agent_terminate_guardrails_subgraph, @@ -67,6 +66,8 @@ def create_agent( Control flow tools (end_execution, raise_error) are auto-injected alongside regular tools. """ + from ..tools import create_tool_node + if config is None: config = AgentGraphConfig() diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 90e6d18e..235df9ac 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -16,7 +16,6 @@ from uipath.eval.mocks import mockable from uipath.platform.common import CreateEscalation -from ..react.types import AgentGraphNode, AgentTerminationSource from .utils import sanitize_tool_name @@ -82,6 +81,7 @@ async def escalation_tool_fn( if outcome == EscalationAction.END: output_detail = f"Escalation output: {escalation_output}" termination_title = f"Agent run ended based on escalation outcome {outcome} with directive {escalation_action}" + from ..react.types import AgentGraphNode, AgentTerminationSource return Command( update={ diff --git a/src/uipath_langchain/agent/tools/integration_tool.py b/src/uipath_langchain/agent/tools/integration_tool.py index 468d256f..79df858e 100644 --- a/src/uipath_langchain/agent/tools/integration_tool.py +++ b/src/uipath_langchain/agent/tools/integration_tool.py @@ -11,7 +11,6 @@ from uipath.platform.connections import ActivityMetadata, ActivityParameterLocationInfo from uipath_langchain.agent.tools.tool_node import ToolWrapperMixin -from uipath_langchain.agent.wrappers.static_args_wrapper import get_static_args_wrapper from .structured_tool_with_output_type import StructuredToolWithOutputType from .utils import sanitize_dict_for_serialization, sanitize_tool_name @@ -168,6 +167,10 @@ async def integration_tool_fn(**kwargs: Any): return result + from uipath_langchain.agent.wrappers.static_args_wrapper import ( + get_static_args_wrapper, + ) + wrapper = get_static_args_wrapper(resource) tool = StructuredToolWithStaticArgs( diff --git a/testcases/common/__init__.py b/testcases/common/__init__.py index 337f3c08..a6d54161 100644 --- a/testcases/common/__init__.py +++ b/testcases/common/__init__.py @@ -1,6 +1,6 @@ """Common testing utilities for UiPath testcases.""" -from testcases.common.console import ( +from .console import ( ConsoleTest, PromptTest, strip_ansi, diff --git a/tests/test_no_circular_imports.py b/tests/test_no_circular_imports.py new file mode 100644 index 00000000..d78c211e --- /dev/null +++ b/tests/test_no_circular_imports.py @@ -0,0 +1,81 @@ +"""Test that all modules can be imported without circular dependency errors. + +This test automatically discovers all modules in uipath_langchain and tests each +one with isolated imports to catch runtime circular imports. +""" + +import importlib +import pkgutil +import sys +from typing import Iterator + +import pytest + + +def discover_all_modules(package_name: str) -> Iterator[str]: + """Discover all importable modules in a package recursively. + + Args: + package_name: The top-level package name (e.g., 'uipath_langchain') + + Yields: + Fully qualified module names (e.g., 'uipath_langchain.agent.tools') + """ + try: + package = importlib.import_module(package_name) + package_path = package.__path__ + except ImportError: + return + + # Recursively walk through all modules + for _importer, modname, _ispkg in pkgutil.walk_packages( + path=package_path, prefix=f"{package_name}.", onerror=lambda x: None + ): + yield modname + + +def get_all_module_imports() -> list[str]: + """Get all modules to test. + + Returns: + List of module names to test + """ + modules = list(discover_all_modules("uipath_langchain")) + + # Filter out optional dependency modules that won't be installed + exclude = {"uipath_langchain.chat.bedrock", "uipath_langchain.chat.vertex"} + return [m for m in modules if m not in exclude] + + +@pytest.mark.parametrize("module_name", get_all_module_imports()) +def test_module_imports_with_isolation(module_name: str) -> None: + """Test that a module can be imported in isolation. + + Clears all uipath_langchain modules from sys.modules before importing to + catch circular imports that would be masked by module caching. + + Args: + module_name: The fully qualified module name to test + + Raises: + pytest.fail: If the module cannot be imported due to circular dependency + """ + # Clear all uipath_langchain modules from sys.modules to force fresh import + to_remove = [key for key in sys.modules.keys() if "uipath_langchain" in key] + for key in to_remove: + del sys.modules[key] + + # Now try importing the module in isolation + try: + importlib.import_module(module_name) + except ImportError as e: + if "circular import" in str(e).lower(): + pytest.fail( + f"Circular import in {module_name}:\n{e}", + pytrace=False, + ) + # Other import errors (missing dependencies, syntax errors, etc) + pytest.fail( + f"Failed to import {module_name}:\n{e}", + pytrace=False, + ) diff --git a/uv.lock b/uv.lock index 9859eac7..7cdea4cc 100644 --- a/uv.lock +++ b/uv.lock @@ -2965,6 +2965,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, ] +[[package]] +name = "rust-just" +version = "1.45.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/83/8804ad2fbc12bbe05585fc13b525044c7f4c76b3f22368c9d7693e4b9e0d/rust_just-1.45.0.tar.gz", hash = "sha256:e17ed4a9d2e1d48ee024047371b71323c72194e4189cd7911184a3d4007cbe89", size = 1433575, upload-time = "2025-12-11T02:05:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/27/1357829369e9e037a66c3ab22ae35341d89cc05c66321377b1ab885cf661/rust_just-1.45.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f55a5ed6507189fb4c0c33821205f96739fab6c8c22c0264345749175bb6c59f", size = 1712097, upload-time = "2025-12-11T02:05:13.113Z" }, + { url = "https://files.pythonhosted.org/packages/44/67/7cb63895b3869282291294ea73386f517f7471b4767e05680784d0eef08a/rust_just-1.45.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a63628432f2b7e214cfb422013ddd7bf436993d8e5406e5bf1426ea8a97c794b", size = 1595130, upload-time = "2025-12-11T02:05:15.719Z" }, + { url = "https://files.pythonhosted.org/packages/69/69/f43363286b237b5ea1f43bb01792edd8bf5fbedb230f13b00a23bac34510/rust_just-1.45.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:037774833a914e6cf85771454dd623c9173dfa95f6c07033e528b4e484788f0d", size = 1677559, upload-time = "2025-12-11T02:05:18.197Z" }, + { url = "https://files.pythonhosted.org/packages/a9/af/34431539e1f072621a98e5891e436264f3028ca94267622c80ba8b11c2a3/rust_just-1.45.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84d9d0e74e3e2f182002d9ed908c4dc9dac37bfa4515991df9c96f5824070aff", size = 1644684, upload-time = "2025-12-11T02:05:23.243Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9d/b5a899977c3afa33997b1f1460f4aa198d8a4c496d98171c3468e25dd590/rust_just-1.45.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76e4bbfbfcd7e0d49cd3952f195188504285d1e04418e1e74cc3180d92babd2b", size = 1821927, upload-time = "2025-12-11T02:05:26.733Z" }, + { url = "https://files.pythonhosted.org/packages/00/d1/0c5c29c591cf4cf4345ba26e789f35c9f434fb20a1701df9ce148f7f2a5f/rust_just-1.45.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f43123b9ecc122222ac3cae69f2e698cd44afb1b3fdb03e342b56f916295cbd8", size = 1898840, upload-time = "2025-12-11T02:05:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/48e75a831926b9225a60ac602eb0dd8acd9002a6328ba02aaa91e0752e86/rust_just-1.45.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9572941d9ee8a93e78973858561e5e01ce5f8e3eb466dbfe7dad226e73862ea", size = 1885445, upload-time = "2025-12-11T02:05:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d9/4c31345e96ca7072ce07123dda7c732421d7808e8be8382c87d72600b82a/rust_just-1.45.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5e7737429353aa43685671236994fb13eeac990056f487663d2fdfb77dd369d", size = 1806701, upload-time = "2025-12-11T02:05:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/a0/25/ab55f3907fd479a1e28daaa178951d419799bae1dbab7ded3f09cae087b2/rust_just-1.45.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6fbe4634e3f4f7ba1d0b68d251da8e291377e1b75fecc1cf2dd8e89bfa577777", size = 1695387, upload-time = "2025-12-11T02:05:36.813Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/707ef48d339c2de4b6658e6c6d2c83f903fab5d9861b987833101cf2f6ac/rust_just-1.45.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:51d41861edd4872f430a3f8626ce5946581ab5f2f617767de9ff7f450b9d6498", size = 1667104, upload-time = "2025-12-11T02:05:39.3Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fe8466b95889203b93ff39b0ca92ca89af2330155bebee46165d481b0fa8/rust_just-1.45.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:00598b650295c97043175f27018c130a231cf15a62892231a42dfa8e7b4d70a2", size = 1811923, upload-time = "2025-12-11T02:05:42.279Z" }, + { url = "https://files.pythonhosted.org/packages/5b/1b/0c239cc3ff14ce6065ab6a1e879739e1e667cd6d482821679bc50bc77e3c/rust_just-1.45.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:22d5e4a963cd14c4e72c5733933a9478c4fe4b58684ac5c00a3da197b6cdbf70", size = 1871620, upload-time = "2025-12-11T02:05:45.603Z" }, + { url = "https://files.pythonhosted.org/packages/df/e3/3037f2db2cfddffd47f84c440dbf85fc83f96b3477c4c8b10364c1e4d261/rust_just-1.45.0-py3-none-win32.whl", hash = "sha256:33ba0085850fa0378ab479a4421ae79cf88e0e27589f401a63a26ce0c077ae6e", size = 1598481, upload-time = "2025-12-11T02:05:48.489Z" }, + { url = "https://files.pythonhosted.org/packages/62/3f/1ef435ecc57191be4d34a85d09790a30b3659fac320093366c62af7c56e9/rust_just-1.45.0-py3-none-win_amd64.whl", hash = "sha256:3b660701191a2bf413483b9b9d00f1372574e656ab7d0ab3a19c7b2e4321a538", size = 1768810, upload-time = "2025-12-11T02:05:51.114Z" }, +] + [[package]] name = "s3transfer" version = "0.16.0" @@ -3260,7 +3282,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.1.43" +version = "0.1.44" source = { editable = "." } dependencies = [ { name = "aiosqlite" }, @@ -3301,6 +3323,7 @@ dev = [ { name = "pytest-httpx" }, { name = "pytest-mock" }, { name = "ruff" }, + { name = "rust-just" }, ] [package.metadata] @@ -3338,6 +3361,7 @@ dev = [ { name = "pytest-httpx", specifier = ">=0.35.0" }, { name = "pytest-mock", specifier = ">=3.11.1" }, { name = "ruff", specifier = ">=0.9.4" }, + { name = "rust-just", specifier = ">=1.39.0" }, ] [[package]]