From e8f33bd12405d5f572b58dbe8959b9582a231970 Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Fri, 11 Sep 2026 15:07:05 +0300 Subject: [PATCH] fix(cli): only claim uipath new scaffolds for langchain agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `new` middleware returned `should_continue=False` for every call, so installing uipath-langchain made the base `uipath new` function scaffold unreachable (UiPath/uipath-python#1543). It now claims a scaffold only when the base CLI asks for one it can serve: `--type auto` (where a single installed integration is meant to win) or `--type agent --agent-framework langchain`. Every other case — `--type function`, or an agent scaffold for another framework — passes through to the next middleware. The gate compares with `==` rather than `is`, so callers passing plain strings (an older uipath, or a direct middleware call) still work. Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 9 ++- src/uipath_langchain/_cli/cli_new.py | 10 ++- tests/cli/test_new.py | 93 ++++++++++++++++++++++++++++ uv.lock | 22 +++---- 4 files changed, 120 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 70cddefe0..2d21a973c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,13 @@ [project] name = "uipath-langchain" -version = "0.17.8" +version = "0.17.9" 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.14.13, <2.15.0", + # TODO(release): restore ">=2.14.15, <2.15.0" and drop the testpypi + # source once uipath 2.14.15 (UiPath/uipath-python#1886) is on PyPI. + "uipath==2.14.15.dev1018867616", "uipath-core>=0.5.29, <0.6.0", "uipath-platform>=0.2.28, <0.3.0", "uipath-runtime>=0.13.0, <0.14.0", @@ -182,3 +184,6 @@ name = "testpypi" url = "https://test.pypi.org/simple/" publish-url = "https://test.pypi.org/legacy/" explicit = true + +[tool.uv.sources] +uipath = { index = "testpypi" } diff --git a/src/uipath_langchain/_cli/cli_new.py b/src/uipath_langchain/_cli/cli_new.py index bd6e6c642..f9bffdf4d 100644 --- a/src/uipath_langchain/_cli/cli_new.py +++ b/src/uipath_langchain/_cli/cli_new.py @@ -4,6 +4,8 @@ import click from uipath._cli._utils._console import ConsoleLogger from uipath._cli.middlewares import MiddlewareResult +from uipath._cli.models.agent_frameworks import AgentFramework +from uipath._cli.models.project_types import ProjectType console = ConsoleLogger() @@ -47,8 +49,14 @@ def generate_pyproject(target_directory, project_name): f.write(toml_content) -def langgraph_new_middleware(name: str) -> MiddlewareResult: +def langgraph_new_middleware( + name: str, + project_type: ProjectType = ProjectType.AUTO, + agent_framework: AgentFramework | None = None, +) -> MiddlewareResult: """Middleware to create demo langchain agent""" + if not AgentFramework.LANGCHAIN.claims_scaffold(project_type, agent_framework): + return MiddlewareResult(should_continue=True) directory = os.getcwd() diff --git a/tests/cli/test_new.py b/tests/cli/test_new.py index eaf8258d8..2e33d3158 100644 --- a/tests/cli/test_new.py +++ b/tests/cli/test_new.py @@ -5,6 +5,8 @@ import pytest from packaging.specifiers import SpecifierSet from packaging.version import Version +from uipath._cli.models.agent_frameworks import AgentFramework +from uipath._cli.models.project_types import ProjectType from uipath_langchain._cli.cli_new import ( UIPATH_LANGCHAIN_SCAFFOLD_MINOR, @@ -14,6 +16,97 @@ PIN_RE = re.compile(r'"uipath-langchain\[bedrock,vertex\]([^"]*)"') +class TestProjectTypeGate: + """The middleware only claims agent scaffolds; everything else passes through.""" + + def test_function_type_passes_through( + self, tmp_path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """project_type='function' must defer to the base `uipath new` scaffold. + + Regression guard for uipath-python#1543: installing uipath-langchain + used to hijack `uipath new` unconditionally, making the base function + scaffold unreachable. + """ + monkeypatch.chdir(tmp_path) + result = langgraph_new_middleware("demo", project_type=ProjectType.FUNCTION) + assert result.should_continue is True + assert not os.path.exists("main.py") + assert not os.path.exists("langgraph.json") + assert not os.path.exists("pyproject.toml") + + def test_auto_type_scaffolds_agent( + self, tmp_path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """--type auto (the default): an installed framework claims the scaffold.""" + monkeypatch.chdir(tmp_path) + result = langgraph_new_middleware("demo", project_type=ProjectType.AUTO) + assert result.should_continue is False + assert os.path.exists("main.py") + assert os.path.exists("langgraph.json") + + def test_auto_type_plain_string_scaffolds_agent( + self, tmp_path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Raw-string 'auto' from other callers is claimed the same way.""" + monkeypatch.chdir(tmp_path) + # Deliberately off-type: the point is that a plain string still gates. + result = langgraph_new_middleware("demo", project_type="auto") # type: ignore[arg-type] + assert result.should_continue is False + assert os.path.exists("langgraph.json") + + def test_agent_type_scaffolds_agent( + self, tmp_path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + result = langgraph_new_middleware( + "demo", + project_type=ProjectType.AGENT, + agent_framework=AgentFramework.LANGCHAIN, + ) + assert result.should_continue is False + assert os.path.exists("main.py") + assert os.path.exists("langgraph.json") + + def test_other_agent_framework_passes_through( + self, tmp_path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Another framework's scaffold must be left to its own integration.""" + monkeypatch.chdir(tmp_path) + result = langgraph_new_middleware( + "demo", + project_type=ProjectType.AGENT, + agent_framework=AgentFramework.PYDANTIC_AI, + ) + assert result.should_continue is True + assert not os.path.exists("main.py") + assert not os.path.exists("langgraph.json") + assert not os.path.exists("pyproject.toml") + + def test_plain_string_arguments_still_gate_correctly( + self, tmp_path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Callers passing raw strings (older CLIs) must hit the same gate.""" + monkeypatch.chdir(tmp_path) + # Deliberately off-type: the point is that plain strings still gate. + result = langgraph_new_middleware( + "demo", + project_type="agent", # type: ignore[arg-type] + agent_framework="pydantic-ai", # type: ignore[arg-type] + ) + assert result.should_continue is True + assert not os.path.exists("langgraph.json") + + def test_default_type_stays_agent_for_old_base_cli( + self, tmp_path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Older `uipath` versions call without project_type; keep scaffolding an agent.""" + monkeypatch.chdir(tmp_path) + result = langgraph_new_middleware("demo") + assert result.should_continue is False + assert os.path.exists("langgraph.json") + + class TestUipathLangchainScaffoldPin: """The scaffolded pin must admit the uipath-langchain release it ships with.""" diff --git a/uv.lock b/uv.lock index 45df1a953..c2705d721 100644 --- a/uv.lock +++ b/uv.lock @@ -172,9 +172,9 @@ name = "aiologic" version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/7a/d51f2fde1e8ae8a83431f8e97b7a71e9358cdb1d4d2ce6be387fa44d68de/aiologic-0.17.1.tar.gz", hash = "sha256:2e1b93b9e88ced318c2a63ad7b382688f40cbfe40e3d42258d49dc9c5aea179d", size = 252354, upload-time = "2026-06-27T20:41:33.25Z" } wheels = [ @@ -991,8 +991,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic" }, - { name = "typing-extensions" }, + { name = "aiologic", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -4693,8 +4693,8 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.14" -source = { registry = "https://pypi.org/simple" } +version = "2.14.15.dev1018867616" +source = { registry = "https://test.pypi.org/simple/" } dependencies = [ { name = "applicationinsights" }, { name = "click" }, @@ -4717,9 +4717,9 @@ dependencies = [ { name = "uipath-platform" }, { name = "uipath-runtime" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/32/3a72a59e9c6691cdc7a203e2138e6715f004fe2f3c1183d6967ddcb23794/uipath-2.14.14.tar.gz", hash = "sha256:70f82bc7ef94ab6d9001d8e272336c48f2ff05f2e2abfccad85d8cd50b5b416a", size = 4552949, upload-time = "2026-09-10T12:16:07.742Z" } +sdist = { url = "https://test-files.pythonhosted.org/packages/c8/08/12aac5a8b9cd53beb8a86399adcb8b346e1ecb63e1bdb3db8b9435ece231/uipath-2.14.15.dev1018867616.tar.gz", hash = "sha256:6c2842944ddd07b7bc63215e467e0c19481f3ef77eb3c67caac532e6eaa3a111", size = 4558272, upload-time = "2026-09-11T12:25:16.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/0c/396acc5a98df7c4fcc0834dbc5d2bbc2e9f5c220fefbae4ace57c4cbbb95/uipath-2.14.14-py3-none-any.whl", hash = "sha256:1adff23d6b86f33961856d48cc3ab1aae85fab7a6d30737917619c310cfbb8df", size = 441501, upload-time = "2026-09-10T12:16:05.719Z" }, + { url = "https://test-files.pythonhosted.org/packages/e7/67/d8fef0e326feb5c4ac9b41f4160ec5116935b863f008e9d89d0235244bf3/uipath-2.14.15.dev1018867616-py3-none-any.whl", hash = "sha256:96d3e0a62950de2a1cfdf2f14f6685e9a5c7a2411bbec7f2ba9b983dd08027b3", size = 443220, upload-time = "2026-09-11T12:25:12.993Z" }, ] [[package]] @@ -4738,7 +4738,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.17.8" +version = "0.17.9" source = { editable = "." } dependencies = [ { name = "a2a-sdk" }, @@ -4827,7 +4827,7 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.6.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "rdflib", specifier = ">=7.0.0,<8.0.0" }, - { name = "uipath", specifier = ">=2.14.13,<2.15.0" }, + { name = "uipath", specifier = "==2.14.15.dev1018867616", index = "https://test.pypi.org/simple/" }, { name = "uipath-core", specifier = ">=0.5.29,<0.6.0" }, { name = "uipath-langchain-client", extras = ["all"], marker = "extra == 'all'", specifier = ">=1.20.0,<1.21.0" }, { name = "uipath-langchain-client", extras = ["anthropic"], marker = "extra == 'anthropic'", specifier = ">=1.20.0,<1.21.0" },