Skip to content
Open
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 pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-langchain"
version = "0.14.16"
version = "0.14.17"
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
6 changes: 1 addition & 5 deletions src/uipath_langchain/agent/tools/process_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@
}


def create_process_tool(
resource: AgentProcessToolResourceConfig,
run_as_me: bool = False,
) -> StructuredTool:
def create_process_tool(resource: AgentProcessToolResourceConfig) -> StructuredTool:

Check failure on line 45 in src/uipath_langchain/agent/tools/process_tool.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 25 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=UiPath_uipath-langchain-python&issues=AZ-WiU6MGwp10Fn50nV6&open=AZ-WiU6MGwp10Fn50nV6&pullRequest=1010
"""Uses interrupt() to suspend graph execution until process completes (handled by runtime)."""
Comment on lines +45 to 46
# Import here to avoid circular dependency
from uipath_langchain.agent.wrappers import get_job_attachment_wrapper
Expand Down Expand Up @@ -86,7 +83,6 @@
attachments=attachments,
parent_span_id=parent_span_id,
parent_operation_id=parent_operation_id,
run_as_me=True if run_as_me else None,
)
except EnrichedException as e:
raise_for_enriched(
Expand Down
29 changes: 2 additions & 27 deletions src/uipath_langchain/agent/tools/tool_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,33 +31,11 @@
logger = getLogger(__name__)


def _is_user_token() -> bool:
"""Check if the current token is a user token (sub_type == 'user')."""
try:
from uipath._cli._utils._common import get_claim_from_token

sub_type = get_claim_from_token("sub_type")
logger.info("Token sub_type=%r", sub_type)
return sub_type == "user"
except Exception as e:
logger.info("Token sub_type check failed: %s", e)
return False


async def create_tools_from_resources(
agent: LowCodeAgentDefinition, llm: BaseChatModel
) -> list[BaseTool]:

tools: list[BaseTool] = []
is_user = _is_user_token()
run_as_me = agent.is_conversational and is_user
logger.info(
"RunAsMe decision: is_conversational=%s, is_user_token=%s, run_as_me=%s",
agent.is_conversational,
is_user,
run_as_me,
)

logger.info("Creating tools for agent '%s' from resources", agent.name)

for resource in agent.resources:
Expand All @@ -74,9 +52,7 @@ async def create_tools_from_resources(
resource.name,
type(resource).__name__,
)
tool = await _build_tool_for_resource(
resource, llm, agent=agent, run_as_me=run_as_me
)
tool = await _build_tool_for_resource(resource, llm, agent=agent)
if tool is not None:
if isinstance(tool, list):
tools.extend(tool)
Expand All @@ -99,10 +75,9 @@ async def _build_tool_for_resource(
resource: BaseAgentResourceConfig,
llm: BaseChatModel,
agent: LowCodeAgentDefinition | None = None,
run_as_me: bool = False,
) -> BaseTool | list[BaseTool] | None:
if isinstance(resource, AgentProcessToolResourceConfig):
return create_process_tool(resource, run_as_me=run_as_me)
return create_process_tool(resource)

elif isinstance(resource, AgentContextResourceConfig):
return create_context_tool(resource, llm=llm, agent=agent)
Expand Down
97 changes: 0 additions & 97 deletions tests/agent/tools/test_process_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ async def test_invoke_calls_processes_invoke_async(
attachments=[],
parent_span_id=None,
parent_operation_id=None,
run_as_me=None,
)

@pytest.mark.asyncio
Expand Down Expand Up @@ -393,100 +392,6 @@ async def test_span_context_defaults_to_none_when_empty(
assert call_kwargs["parent_span_id"] is None


class TestProcessToolRunAsMe:
"""Test RunAsMe propagation passed top-down from tool factory."""

@pytest.mark.asyncio
@patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt")
@patch("uipath_langchain.agent.tools.process_tool.UiPath")
async def test_run_as_me_true_passed_to_invoke(
self,
mock_uipath_class,
mock_interrupt,
process_resource,
):
"""Test RunAsMe=True is forwarded to invoke_async when set."""
mock_job = MagicMock(spec=Job)
mock_job.key = "job-key"
mock_job.folder_key = "folder-key"

mock_resumed_job = MagicMock(spec=Job)
mock_resumed_job.state = "successful"

mock_client = MagicMock()
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
mock_uipath_class.return_value = mock_client

mock_interrupt.return_value = mock_resumed_job

tool = create_process_tool(process_resource, run_as_me=True)
await tool.ainvoke({})

call_kwargs = mock_client.processes.invoke_async.call_args[1]
assert call_kwargs["run_as_me"] is True

@pytest.mark.asyncio
@patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt")
@patch("uipath_langchain.agent.tools.process_tool.UiPath")
async def test_run_as_me_false_sends_none(
self,
mock_uipath_class,
mock_interrupt,
process_resource,
):
"""Test RunAsMe=None when run_as_me=False (default)."""
mock_job = MagicMock(spec=Job)
mock_job.key = "job-key"
mock_job.folder_key = "folder-key"

mock_resumed_job = MagicMock(spec=Job)
mock_resumed_job.state = "successful"

mock_client = MagicMock()
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
mock_uipath_class.return_value = mock_client

mock_interrupt.return_value = mock_resumed_job

tool = create_process_tool(process_resource, run_as_me=False)
await tool.ainvoke({})

call_kwargs = mock_client.processes.invoke_async.call_args[1]
assert call_kwargs["run_as_me"] is None

@pytest.mark.asyncio
@patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt")
@patch("uipath_langchain.agent.tools.process_tool.UiPath")
async def test_run_as_me_default_sends_none(
self,
mock_uipath_class,
mock_interrupt,
process_resource,
):
"""Test RunAsMe=None when run_as_me not specified (default)."""
mock_job = MagicMock(spec=Job)
mock_job.key = "job-key"
mock_job.folder_key = "folder-key"

mock_resumed_job = MagicMock(spec=Job)
mock_resumed_job.state = "successful"

mock_client = MagicMock()
mock_client.processes.invoke_async = AsyncMock(return_value=mock_job)
mock_client.jobs.extract_output_async = AsyncMock(return_value=None)
mock_uipath_class.return_value = mock_client

mock_interrupt.return_value = mock_resumed_job

tool = create_process_tool(process_resource)
await tool.ainvoke({})

call_kwargs = mock_client.processes.invoke_async.call_args[1]
assert call_kwargs["run_as_me"] is None


class TestProcessToolFlowType:
"""Test that a Flow-type resource flows through create_process_tool correctly."""

Expand Down Expand Up @@ -534,7 +439,6 @@ async def test_flow_tool_invokes_processes_invoke_async(
attachments=[],
parent_span_id=None,
parent_operation_id=None,
run_as_me=None,
)

@pytest.mark.asyncio
Expand Down Expand Up @@ -615,7 +519,6 @@ async def test_function_tool_invokes_processes_invoke_async(
attachments=[],
parent_span_id=None,
parent_operation_id=None,
run_as_me=None,
)

@pytest.mark.asyncio
Expand Down
6 changes: 2 additions & 4 deletions tests/agent/tools/test_tool_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ async def test_flow_resource_routes_through_process_tool_path(
)
tool = await _build_tool_for_resource(flow_resource, mock_llm)

mock_create_process_tool.assert_called_once_with(flow_resource, run_as_me=False)
mock_create_process_tool.assert_called_once_with(flow_resource)
assert tool is not None

async def test_quick_form_resource_routes_through_escalation_tool_path(
Expand Down Expand Up @@ -466,9 +466,7 @@ async def test_function_resource_routes_through_process_tool_path(
)
tool = await _build_tool_for_resource(function_resource, mock_llm)

mock_create_process_tool.assert_called_once_with(
function_resource, run_as_me=False
)
mock_create_process_tool.assert_called_once_with(function_resource)
assert tool is not None

@pytest.mark.asyncio
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

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

Loading