Skip to content

Commit 000d61b

Browse files
authored
Merge pull request #376 from UiPath/akshaya/circular_imports
fix(CircularDependencies): fix circular dependencies
2 parents 4dd44cf + a8a08e0 commit 000d61b

File tree

11 files changed

+144
-14
lines changed

11 files changed

+144
-14
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ jobs:
4949

5050
- name: Check formatting
5151
run: uv run ruff format --check .
52-
52+
5353
- name: Check httpx.Client() usage
5454
run: uv run python scripts/lint_httpx_client.py

justfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
set quiet
2+
3+
default: lint format
4+
5+
lint:
6+
ruff check .
7+
python scripts/lint_httpx_client.py
8+
9+
format:
10+
ruff format --check .
11+
ruff check --fix
12+
13+
validate: lint format
14+
15+
build:
16+
uv build
17+
18+
install:
19+
uv sync --all-extras

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
@@ -65,6 +65,7 @@ dev = [
6565
"pre-commit>=4.1.0",
6666
"numpy>=1.24.0",
6767
"pytest_httpx>=0.35.0",
68+
"rust-just>=1.39.0",
6869
]
6970

7071
[tool.hatch.build.targets.wheel]

src/uipath_langchain/agent/react/agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from uipath.platform.guardrails import BaseGuardrail
1111

1212
from ..guardrails.actions import GuardrailAction
13-
from ..tools import create_tool_node
1413
from .guardrails.guardrails_subgraph import (
1514
create_agent_init_guardrails_subgraph,
1615
create_agent_terminate_guardrails_subgraph,
@@ -67,6 +66,8 @@ def create_agent(
6766
6867
Control flow tools (end_execution, raise_error) are auto-injected alongside regular tools.
6968
"""
69+
from ..tools import create_tool_node
70+
7071
if config is None:
7172
config = AgentGraphConfig()
7273

src/uipath_langchain/agent/tools/escalation_tool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
1919

20-
from ..react.types import AgentGraphNode, AgentTerminationSource
2120
from .utils import sanitize_tool_name
2221

2322

@@ -83,6 +82,7 @@ async def escalation_tool_fn(
8382
if outcome == EscalationAction.END:
8483
output_detail = f"Escalation output: {escalation_output}"
8584
termination_title = f"Agent run ended based on escalation outcome {outcome} with directive {escalation_action}"
85+
from ..react.types import AgentGraphNode, AgentTerminationSource
8686

8787
return Command(
8888
update={

src/uipath_langchain/agent/tools/integration_tool.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
1313
from uipath_langchain.agent.tools.tool_node import ToolWrapperMixin
14-
from uipath_langchain.agent.wrappers.static_args_wrapper import get_static_args_wrapper
1514

1615
from .structured_tool_with_output_type import StructuredToolWithOutputType
1716
from .utils import sanitize_dict_for_serialization, sanitize_tool_name
@@ -168,6 +167,10 @@ async def integration_tool_fn(**kwargs: Any):
168167

169168
return result
170169

170+
from uipath_langchain.agent.wrappers.static_args_wrapper import (
171+
get_static_args_wrapper,
172+
)
173+
171174
wrapper = get_static_args_wrapper(resource)
172175

173176
tool = StructuredToolWithStaticArgs(

src/uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717
)
1818
from uipath_langchain.agent.tools.tool_node import ToolWrapperMixin
1919
from uipath_langchain.agent.tools.utils import sanitize_tool_name
20-
from uipath_langchain.agent.wrappers.job_attachment_wrapper import (
21-
get_job_attachment_wrapper,
22-
)
2320

2421
ANALYZE_FILES_SYSTEM_MESSAGE = (
2522
"Process the provided files to complete the given task. "
@@ -35,6 +32,10 @@ class AnalyzeFileTool(StructuredToolWithOutputType, ToolWrapperMixin):
3532
def create_analyze_file_tool(
3633
resource: AgentInternalToolResourceConfig, llm: BaseChatModel
3734
) -> StructuredTool:
35+
from uipath_langchain.agent.wrappers.job_attachment_wrapper import (
36+
get_job_attachment_wrapper,
37+
)
38+
3839
tool_name = sanitize_tool_name(resource.name)
3940
input_model = create_model(resource.input_schema)
4041
output_model = create_model(resource.output_schema)

testcases/common/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Common testing utilities for UiPath testcases."""
22

3-
from testcases.common.console import (
3+
from .console import (
44
ConsoleTest,
55
PromptTest,
66
strip_ansi,

tests/agent/tools/internal_tools/test_analyze_files_tool.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def resource_config(self):
7373
)
7474

7575
@patch(
76-
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.get_job_attachment_wrapper"
76+
"uipath_langchain.agent.wrappers.job_attachment_wrapper.get_job_attachment_wrapper"
7777
)
7878
@patch(
7979
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.llm_call_with_files"
@@ -135,7 +135,7 @@ async def test_create_analyze_file_tool_success(
135135
assert files[0].url == "https://example.com/file.pdf"
136136

137137
@patch(
138-
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.get_job_attachment_wrapper"
138+
"uipath_langchain.agent.wrappers.job_attachment_wrapper.get_job_attachment_wrapper"
139139
)
140140
async def test_create_analyze_file_tool_missing_analysis_task(
141141
self, mock_get_wrapper, resource_config, mock_llm
@@ -157,7 +157,7 @@ async def test_create_analyze_file_tool_missing_analysis_task(
157157
await tool.coroutine(attachments=[mock_attachment])
158158

159159
@patch(
160-
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.get_job_attachment_wrapper"
160+
"uipath_langchain.agent.wrappers.job_attachment_wrapper.get_job_attachment_wrapper"
161161
)
162162
async def test_create_analyze_file_tool_missing_attachments(
163163
self, mock_get_wrapper, resource_config, mock_llm
@@ -173,7 +173,7 @@ async def test_create_analyze_file_tool_missing_attachments(
173173
await tool.coroutine(analysisTask="Summarize the document")
174174

175175
@patch(
176-
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.get_job_attachment_wrapper"
176+
"uipath_langchain.agent.wrappers.job_attachment_wrapper.get_job_attachment_wrapper"
177177
)
178178
@patch(
179179
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.llm_call_with_files"

tests/test_no_circular_imports.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Test that all modules can be imported without circular dependency errors.
2+
3+
This test automatically discovers all modules in uipath_langchain and tests each
4+
one with isolated imports to catch runtime circular imports.
5+
"""
6+
7+
import importlib
8+
import pkgutil
9+
import sys
10+
from typing import Iterator
11+
12+
import pytest
13+
14+
15+
def discover_all_modules(package_name: str) -> Iterator[str]:
16+
"""Discover all importable modules in a package recursively.
17+
18+
Args:
19+
package_name: The top-level package name (e.g., 'uipath_langchain')
20+
21+
Yields:
22+
Fully qualified module names (e.g., 'uipath_langchain.agent.tools')
23+
"""
24+
try:
25+
package = importlib.import_module(package_name)
26+
package_path = package.__path__
27+
except ImportError:
28+
return
29+
30+
# Recursively walk through all modules
31+
for _importer, modname, _ispkg in pkgutil.walk_packages(
32+
path=package_path, prefix=f"{package_name}.", onerror=lambda x: None
33+
):
34+
yield modname
35+
36+
37+
def get_all_module_imports() -> list[str]:
38+
"""Get all modules to test.
39+
40+
Returns:
41+
List of module names to test
42+
"""
43+
modules = list(discover_all_modules("uipath_langchain"))
44+
45+
# Filter out optional dependency modules that won't be installed
46+
exclude = {"uipath_langchain.chat.bedrock", "uipath_langchain.chat.vertex"}
47+
return [m for m in modules if m not in exclude]
48+
49+
50+
@pytest.mark.parametrize("module_name", get_all_module_imports())
51+
def test_module_imports_with_isolation(module_name: str) -> None:
52+
"""Test that a module can be imported in isolation.
53+
54+
Clears all uipath_langchain modules from sys.modules before importing to
55+
catch circular imports that would be masked by module caching.
56+
57+
Args:
58+
module_name: The fully qualified module name to test
59+
60+
Raises:
61+
pytest.fail: If the module cannot be imported due to circular dependency
62+
"""
63+
# Clear all uipath_langchain modules from sys.modules to force fresh import
64+
to_remove = [key for key in sys.modules.keys() if "uipath_langchain" in key]
65+
for key in to_remove:
66+
del sys.modules[key]
67+
68+
# Now try importing the module in isolation
69+
try:
70+
importlib.import_module(module_name)
71+
except ImportError as e:
72+
if "circular import" in str(e).lower():
73+
pytest.fail(
74+
f"Circular import in {module_name}:\n{e}",
75+
pytrace=False,
76+
)
77+
# Other import errors (missing dependencies, syntax errors, etc)
78+
pytest.fail(
79+
f"Failed to import {module_name}:\n{e}",
80+
pytrace=False,
81+
)

0 commit comments

Comments
 (0)