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
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath-langchain"
version = "0.14.15"
version = "0.14.16"
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"
dependencies = [
"uipath>=2.13.7, <2.14.0",
"uipath-core>=0.5.29, <0.6.0",
"uipath-platform>=0.2.12, <0.3.0",
"uipath-platform==0.2.14.dev1018287328",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restore a production uipath-platform constraint before merging. This published dependency pins a TestPyPI development build; normal pip consumers resolve from PyPI and do not read [tool.uv.sources], so uipath-langchain==0.14.16 cannot install when this build is unavailable on PyPI.

"uipath-runtime>=0.12.5, <0.13.0",
"uipath-llm-client>=1.17.1, <1.18.0",
"langgraph>=1.1.8, <2.0.0",
Expand Down Expand Up @@ -172,3 +172,6 @@ name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath-platform = { index = "testpypi" }
11 changes: 6 additions & 5 deletions src/uipath_langchain/agent/tools/a2a/a2a_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ class A2aClient:
pool when done.
"""

def __init__(self, agent_card: AgentCard, slug: str) -> None:
def __init__(self, agent_card: AgentCard, resource_name: str) -> None:
self._agent_card = agent_card
self._slug = slug
self._resource_name = resource_name
self._lock = asyncio.Lock()
self._client: Client | None = None
self._http_client: httpx.AsyncClient | None = None
Expand All @@ -96,11 +96,12 @@ async def get(self) -> Client:
sdk = UiPath()
folder_path = get_execution_folder_path()
agent = await sdk.remote_a2a.retrieve_async(
slug=self._slug, folder_path=folder_path
name=self._resource_name,
folder_path=folder_path,
)
if not agent.a2a_url:
raise ValueError(
f"Remote A2A agent '{self._slug}' has no a2a_url configured"
f"Remote A2A agent '{self._resource_name}' has no a2a_url configured"
)
self._agent_card.url = agent.a2a_url

Expand Down Expand Up @@ -404,7 +405,7 @@ def create_a2a_tools_and_clients(
default_output_modes=["text/plain"],
)

a2a_client = A2aClient(agent_card, resource.slug)
a2a_client = A2aClient(agent_card, resource.name)
tool = _create_a2a_tool(resource, a2a_client, agent_card)
tools.append(tool)
clients.append(a2a_client)
Expand Down
12 changes: 7 additions & 5 deletions src/uipath_langchain/agent/tools/mcp/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ def __init__(
"""Initialize the MCP tool session.

The MCP server URL and authorization headers are retrieved lazily
from the UiPath SDK on first use, using the config's slug and folder_path.
from the UiPath SDK on first use, using the config's display name and
folder_path.

Args:
config: The MCP resource configuration containing slug and folder_path.
config: The MCP resource configuration containing name and folder_path.
timeout: Optional timeout configuration for HTTP requests.
max_retries: Maximum number of retries on session disconnect errors.
session_info_factory: Factory for creating SessionInfo instances.
Expand Down Expand Up @@ -152,7 +153,7 @@ async def _initialize_client(self) -> None:
"""
folder_path = get_execution_folder_path()
logger.debug(
f"Initializing MCP client for '{self._config.slug}' "
f"Initializing MCP client for '{self._config.name}' "
f"in folder '{folder_path}'"
)

Expand All @@ -162,11 +163,12 @@ async def _initialize_client(self) -> None:
# Retrieve MCP server URL from SDK
sdk = UiPath()
mcp_server = await sdk.mcp.retrieve_async(
slug=self._config.slug, folder_path=folder_path
name=self._config.name,
folder_path=folder_path,
)

if mcp_server.mcp_url is None:
raise ValueError(f"MCP server '{self._config.slug}' has no URL configured")
raise ValueError(f"MCP server '{self._config.name}' has no URL configured")

self._url = mcp_server.mcp_url
self._headers = {"Authorization": f"Bearer {sdk._config.secret}"}
Expand Down
22 changes: 13 additions & 9 deletions tests/agent/tools/test_a2a_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ def test_create_tools_builds_one_client_per_enabled_resource() -> None:

assert len(tools) == 1
assert len(clients) == 1
# The client carries the slug used to resolve the proxy URL at runtime.
assert clients[0]._slug == "remote-agent-slug"
assert clients[0]._resource_name == "remote-agent"
# The card is built from the cached card; the URL is not yet resolved.
assert clients[0]._agent_card.name == "Remote Agent"

Expand All @@ -96,7 +95,7 @@ def test_create_tools_builds_default_card_without_cached_card() -> None:
tools, clients = create_a2a_tools_and_clients([resource])

assert len(tools) == 1
assert clients[0]._slug == "remote-agent-slug"
assert clients[0]._resource_name == "remote-agent"
assert clients[0]._agent_card.name == "remote-agent"


Expand All @@ -114,8 +113,13 @@ def __init__(self, a2a_url: str | None) -> None:
self._a2a_url = a2a_url
self.calls: list[tuple[str, str | None]] = []

async def retrieve_async(self, *, slug: str, folder_path: str | None):
self.calls.append((slug, folder_path))
async def retrieve_async(
self,
*,
name: str,
folder_path: str | None,
):
self.calls.append((name, folder_path))
return SimpleNamespace(a2a_url=self._a2a_url)


Expand All @@ -125,7 +129,7 @@ def __init__(self, a2a_url: str | None) -> None:
self._config = SimpleNamespace(secret="token")


def _patch_runtime(monkeypatch: pytest.MonkeyPatch, sdk: _FakeSdk) -> dict[str, Any]:
def _patch_runtime(monkeypatch: pytest.MonkeyPatch, sdk: Any) -> dict[str, Any]:
"""Patch the SDK, folder-path resolver, and A2A client factory."""
import uipath.platform as uipath_platform
from a2a.client import ClientFactory
Expand Down Expand Up @@ -158,14 +162,14 @@ async def test_client_resolves_proxy_url_via_retrieve(
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
)
client = A2aClient(card, slug="remote-agent-slug")
client = A2aClient(card, resource_name="Remote Agent")

result = await client.get()

assert result is handles["connected"]
# URL resolved from the retrieved agent's a2a_url, not the cached card URL.
assert card.url == PROXY_URL
assert sdk.remote_a2a.calls == [("remote-agent-slug", "Shared")]
assert sdk.remote_a2a.calls == [("Remote Agent", "Shared")]

await client.dispose()

Expand All @@ -186,7 +190,7 @@ async def test_client_raises_when_agent_has_no_proxy_url(
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
)
client = A2aClient(card, slug="remote-agent-slug")
client = A2aClient(card, resource_name="Remote Agent")

with pytest.raises(ValueError, match="has no a2a_url"):
await client.get()
Expand Down
7 changes: 4 additions & 3 deletions tests/agent/tools/test_mcp/test_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,10 +849,10 @@ async def test_dispose_clears_tools_cache(
@pytest.mark.asyncio
@patch.dict(os.environ, {"UIPATH_FOLDER_PATH": "/Shared/TestFolder"})
@patch("httpx.AsyncClient")
async def test_retrieve_async_uses_execution_folder_path(
async def test_retrieve_async_uses_name_and_execution_folder_path(
self, mock_async_client_class, mcp_resource_config
):
"""Test that retrieve_async is called with folder_path from the execution environment."""
"""Test that name resolution receives both identities and the execution folder."""
mock_sdk = MagicMock()
mock_server = MagicMock()
mock_server.mcp_url = "https://test.uipath.com/mcp"
Expand All @@ -876,7 +876,8 @@ async def test_retrieve_async_uses_execution_folder_path(
await session.call_tool("test_tool", {"query": "test"})

mock_sdk.mcp.retrieve_async.assert_called_once_with(
slug="test-server", folder_path="/Shared/TestFolder"
name="test_server",
folder_path="/Shared/TestFolder",
)

await session.dispose()
13 changes: 7 additions & 6 deletions uv.lock

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

Loading