Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 19 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-langchain"
version = "0.2.2"
version = "0.2.3"
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"
Expand Down Expand Up @@ -65,6 +65,7 @@ dev = [
"pre-commit>=4.1.0",
"numpy>=1.24.0",
"pytest_httpx>=0.35.0",
"rust-just>=1.39.0",
]

[tool.hatch.build.targets.wheel]
Expand Down
3 changes: 2 additions & 1 deletion src/uipath_langchain/agent/react/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion src/uipath_langchain/agent/tools/escalation_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model

from ..react.types import AgentGraphNode, AgentTerminationSource
from .utils import sanitize_tool_name


Expand Down Expand Up @@ -83,6 +82,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={
Expand Down
5 changes: 4 additions & 1 deletion src/uipath_langchain/agent/tools/integration_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
)
from uipath_langchain.agent.tools.tool_node import ToolWrapperMixin
from uipath_langchain.agent.tools.utils import sanitize_tool_name
from uipath_langchain.agent.wrappers.job_attachment_wrapper import (
get_job_attachment_wrapper,
)

ANALYZE_FILES_SYSTEM_MESSAGE = (
"Process the provided files to complete the given task. "
Expand All @@ -35,6 +32,10 @@ class AnalyzeFileTool(StructuredToolWithOutputType, ToolWrapperMixin):
def create_analyze_file_tool(
resource: AgentInternalToolResourceConfig, llm: BaseChatModel
) -> StructuredTool:
from uipath_langchain.agent.wrappers.job_attachment_wrapper import (
get_job_attachment_wrapper,
)

tool_name = sanitize_tool_name(resource.name)
input_model = create_model(resource.input_schema)
output_model = create_model(resource.output_schema)
Expand Down
2 changes: 1 addition & 1 deletion testcases/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Common testing utilities for UiPath testcases."""

from testcases.common.console import (
from .console import (
ConsoleTest,
PromptTest,
strip_ansi,
Expand Down
8 changes: 4 additions & 4 deletions tests/agent/tools/internal_tools/test_analyze_files_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def resource_config(self):
)

@patch(
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.get_job_attachment_wrapper"
"uipath_langchain.agent.wrappers.job_attachment_wrapper.get_job_attachment_wrapper"
)
@patch(
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.llm_call_with_files"
Expand Down Expand Up @@ -135,7 +135,7 @@ async def test_create_analyze_file_tool_success(
assert files[0].url == "https://example.com/file.pdf"

@patch(
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.get_job_attachment_wrapper"
"uipath_langchain.agent.wrappers.job_attachment_wrapper.get_job_attachment_wrapper"
)
async def test_create_analyze_file_tool_missing_analysis_task(
self, mock_get_wrapper, resource_config, mock_llm
Expand All @@ -157,7 +157,7 @@ async def test_create_analyze_file_tool_missing_analysis_task(
await tool.coroutine(attachments=[mock_attachment])

@patch(
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.get_job_attachment_wrapper"
"uipath_langchain.agent.wrappers.job_attachment_wrapper.get_job_attachment_wrapper"
)
async def test_create_analyze_file_tool_missing_attachments(
self, mock_get_wrapper, resource_config, mock_llm
Expand All @@ -173,7 +173,7 @@ async def test_create_analyze_file_tool_missing_attachments(
await tool.coroutine(analysisTask="Summarize the document")

@patch(
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.get_job_attachment_wrapper"
"uipath_langchain.agent.wrappers.job_attachment_wrapper.get_job_attachment_wrapper"
)
@patch(
"uipath_langchain.agent.tools.internal_tools.analyze_files_tool.llm_call_with_files"
Expand Down
81 changes: 81 additions & 0 deletions tests/test_no_circular_imports.py
Original file line number Diff line number Diff line change
@@ -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,
)
26 changes: 25 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.