From c4e8d70159e87757e7fc4201cab890fe24dbdd6d Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Fri, 11 Sep 2026 15:05:01 +0300 Subject: [PATCH 1/4] fix(cli): add --type to uipath new so function projects stay reachable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `uipath new` was unconditionally hijacked by any installed agent framework integration (their middleware always returned should_continue=False), making the base function scaffold unreachable once uipath-langchain was in the environment (#1543). The fix is in the dispatch rather than in the integrations: the `new` middleware chain is the only way an agent scaffold happens, and the base CLI now decides whether to consult it at all. - `--type auto` (the new default) keeps today's behaviour: an installed framework claims the scaffold, otherwise a function project is created. - `--type function` never consults the chain, so no installed framework can intercept it. - `--type agent` consults the chain and fails when nothing claims it, rather than quietly producing a function project. The framework is never named on the command line: it is whichever integration package the environment has. With several installed the command errors instead of resolving by registration order, naming the packages it found — discovered from the registered middlewares and their entry points, so the CLI never hardcodes the list of frameworks that exist. Integrations need no changes and no new uipath floor; `Middlewares.next("new", name)` is unchanged. Docs cover the new option: the previously missing `new` section in the CLI reference, a Claude Agent SDK row in the coded-agents framework table, and explicit-type notes in the agents, functions and Studio Web guides. Co-Authored-By: Claude Opus 5 (1M context) --- packages/uipath/docs/cli/index.md | 55 ++++++ packages/uipath/docs/core/agents.md | 13 ++ packages/uipath/docs/core/functions.md | 8 + packages/uipath/docs/core/studio_web.md | 10 +- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/cli_new.py | 93 ++++++++-- .../src/uipath/_cli/models/project_types.py | 21 +++ packages/uipath/tests/cli/test_new.py | 161 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 9 files changed, 348 insertions(+), 17 deletions(-) create mode 100644 packages/uipath/src/uipath/_cli/models/project_types.py diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index e74a4d83e..68a2b1d4e 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -67,6 +67,61 @@ Treat `--client-secret` as a credential. In CI, prefer reading it from a secret --- +::: mkdocs-click + :module: uipath._cli + :command: new + :depth: 1 + :style: table + +Scaffolds a project in the current directory. `--type` selects what gets created: + +- **`auto`** (default) — an installed agent framework integration (e.g. `uipath-langchain`) claims the scaffold and creates a coded agent project; with none installed, a coded function project is created. +- **`function`** — always creates a coded function project, regardless of installed integrations. +- **`agent`** — creates a coded agent project with the installed framework integration, and fails if none is installed rather than quietly creating a function project. + +| Installed framework integrations | `uipath new x` (auto) | `uipath new x --type agent` | +|----------------------------------|------------------------|------------------------------| +| none | coded function project | error: install an agent framework | +| one | that framework's coded agent project | that framework's coded agent project | +| several | error: naming the installed packages | error: naming the installed packages | + +The framework is never named on the command line — it is whichever integration package the environment has. Install exactly the one you want to build with; with several installed, `uipath new` cannot tell which you meant and says so. + +Scaffold a coded function: + + + +```shell +> uipath new my-function --type function +⠋ Creating new project my-function in current directory ... +✓ Created 'main.py' file. +✓ Created 'pyproject.toml' file. +✓ Created 'uipath.json' file. +💡 Initialize project: uipath init +💡 Run project: uipath run main '{"message": "Hello World!"}' +``` + +Scaffold a coded agent — requires the framework's integration package in the environment: + + + +```shell +> uv add uipath-langchain +Resolved 42 packages in 1.2s +Installed 42 packages in 0.8s + +> uipath new my-agent --type agent +Using the installed 'langchain' agent framework. +⠋ Creating new agent my-agent in current directory ... +✓ Created 'main.py' file. +✓ Created 'langgraph.json' file. +✓ Created 'pyproject.toml' file. +💡 Initialize project: uipath init +💡 Run agent: uipath run agent '{"topic": "UiPath"}' +``` + +--- + ::: mkdocs-click :module: uipath._cli :command: init diff --git a/packages/uipath/docs/core/agents.md b/packages/uipath/docs/core/agents.md index 5621d864e..ef834d6d8 100644 --- a/packages/uipath/docs/core/agents.md +++ b/packages/uipath/docs/core/agents.md @@ -25,6 +25,9 @@ The `uipath` package is always required. Add one framework extension on top: | PydanticAI | `uipath-pydantic-ai` | Type-safe agents with Pydantic models | | Google ADK | `uipath-google-adk` | Gemini models, Google ecosystem | | UiPath Agent Framework | `uipath-agent-framework` | UiPath-native agent primitives | +| Claude Agent SDK | `uipath-claude-sdk` | Claude models, Anthropic-native agent loop | + +Installing one of these packages is what makes `uipath new` scaffold an agent: the framework is chosen by what's in your environment, not by a CLI flag. Install exactly the one you intend to build with. --- @@ -85,6 +88,16 @@ The example below uses LangChain. Swap `uipath-langchain` for the framework of y //// +/// info | Guaranteeing an agent project +`uipath new` defaults to `--type auto`: the installed framework integration claims the scaffold, which is why the commands above create an agent project. Pass `--type agent` to make that a requirement — it fails with instructions instead of creating a function project when no framework is installed, which is the difference that matters in CI and in scripts: + +```shell +uipath new agent --type agent +``` + +With several framework integrations installed in the same environment, `uipath new` cannot tell which one you meant and asks you to keep just one. +/// + --- ## Project Structure diff --git a/packages/uipath/docs/core/functions.md b/packages/uipath/docs/core/functions.md index 6c073f68f..598f0df3b 100644 --- a/packages/uipath/docs/core/functions.md +++ b/packages/uipath/docs/core/functions.md @@ -70,6 +70,14 @@ Use coded functions for deterministic compute steps: document extraction, ERP wr //// +/// info | Guaranteeing a function project +`uipath new` defaults to `--type auto`: when an agent framework integration (e.g. `uipath-langchain`) is installed in the environment, it scaffolds a coded agent instead of a function. Pass `--type function` to always get a coded function project: + +```shell +uipath new my-function --type function +``` +/// + --- ## Project Structure diff --git a/packages/uipath/docs/core/studio_web.md b/packages/uipath/docs/core/studio_web.md index 09286dd73..2136fee30 100644 --- a/packages/uipath/docs/core/studio_web.md +++ b/packages/uipath/docs/core/studio_web.md @@ -171,6 +171,7 @@ First, install the SDK package for the framework you want to use: # uipath-pydantic-ai - PydanticAI # uipath-google-adk - Google ADK # uipath-agent-framework - UiPath Agent Framework +# uipath-claude-sdk - Claude Agent SDK > uv add uipath-langchain Resolved 42 packages in 1.2s Installed 42 packages in 0.8s @@ -190,6 +191,7 @@ Installed 42 packages in 0.8s # uipath-pydantic-ai - PydanticAI # uipath-google-adk - Google ADK # uipath-agent-framework - UiPath Agent Framework +# uipath-claude-sdk - Claude Agent SDK > pip install uipath-langchain Successfully installed uipath-langchain ``` @@ -221,9 +223,13 @@ Selected tenant: Tenant1 That's it, your agent should now be visible in Studio Web. +/// info +`uipath new` defaults to `--type auto`, which lets the installed framework integration claim the scaffold. Pass `--type agent` to require an agent project — it fails rather than creating a function project when no framework is installed. With several integrations installed, keep only the one you want to scaffold with. +/// + #### Coded Function -A coded function doesn't require an additional framework package. Authenticate, scaffold the project, and initialize it: +A coded function doesn't require an additional framework package. Authenticate, scaffold the project, and initialize it (`--type function` guarantees a function project even when a framework integration is installed): @@ -238,7 +244,7 @@ Select tenant number: 0 Selected tenant: Tenant1 ✓ Authentication successful. -> uipath new my-function +> uipath new my-function --type function ✓ Created 'main.py' file. ✓ Created 'pyproject.toml' file. ✓ Created 'uipath.json' file. diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7eea2b2c8..e778925c8 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.14.22" +version = "2.14.23" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath/src/uipath/_cli/cli_new.py b/packages/uipath/src/uipath/_cli/cli_new.py index 6d021a867..203308971 100644 --- a/packages/uipath/src/uipath/_cli/cli_new.py +++ b/packages/uipath/src/uipath/_cli/cli_new.py @@ -1,3 +1,4 @@ +import importlib.metadata import json import os import shutil @@ -11,9 +12,15 @@ from ._utils._console import ConsoleLogger from ._utils._project_files import resolve_existing_project_id from .middlewares import Middlewares +from .models.project_types import ProjectType console = ConsoleLogger() +# Agent frameworks are documented, not enumerated in code: each integration +# ships its own `new` middleware, so the CLI knows which ones are installed +# but cannot know which ones exist. +AGENT_FRAMEWORKS_DOCS_URL = "https://uipath.github.io/uipath-python/core/agents/" + # The `uipath` minor release that scaffolded projects are pinned to. # Deliberately a constant: the guard test in tests/cli/test_new.py fails on # every minor bump so the scaffold (pin, template, hints) gets reviewed @@ -57,10 +64,42 @@ def generate_uipath_json(target_directory): json.dump(uipath_config, f, indent=2) +def _installed_agent_framework_packages() -> list[str]: + """Packages of the installed agent frameworks that can scaffold a project. + + Derived from the registered `new` middlewares rather than from a list of + known frameworks, so a framework the CLI has never heard of is named + correctly and a new one needs no change here. + """ + modules = { + middleware.__module__.split(".")[0] for middleware in Middlewares.get("new") + } + packages: set[str] = set() + for entry_point in importlib.metadata.entry_points(group="uipath.middlewares"): + module = entry_point.module.split(".")[0] + if module in modules and entry_point.dist is not None: + packages.add(entry_point.dist.name) + modules.discard(module) + # A middleware registered in-process rather than through an entry point has + # no distribution to name; its module is the most accurate thing left. + return sorted(packages | modules) + + @click.command() @click.argument("name", type=str, default="") +@click.option( + "--type", + "project_type", + type=click.Choice([t.value for t in ProjectType]), + default=ProjectType.AUTO.value, + show_default=True, + help="Project type to scaffold. 'auto' scaffolds an agent when an agent " + "framework package (e.g. uipath-langchain) is installed and a function " + "otherwise; 'function' always scaffolds a function; 'agent' scaffolds an " + "agent and fails when no agent framework is installed.", +) @track_command("new") -def new(name: str): +def new(name: str, project_type: str): """Generate a quick-start project.""" directory = os.getcwd() @@ -69,18 +108,46 @@ def new(name: str): "Please specify a name for your project:\n`uipath new hello-world`" ) - result = Middlewares.next("new", name) - - if result.error_message: - console.error( - result.error_message, include_traceback=result.should_include_stacktrace - ) - - if result.info_message: - console.info(result.info_message) - - if not result.should_continue: - return + scaffold_type = ProjectType(project_type) + + # Agent frameworks scaffold through the `new` middleware chain. A function + # project never consults them, so an installed framework can no longer make + # the base scaffold unreachable (#1543). + if scaffold_type is not ProjectType.FUNCTION: + Middlewares.load_plugins() + installed = _installed_agent_framework_packages() + if len(installed) > 1: + console.error( + "Multiple agent frameworks are installed: " + + ", ".join(installed) + + ".\nKeep the one you want to scaffold with in this environment, " + f"or run `uipath new {name} --type function` to create a " + "function project." + ) + + result = Middlewares.next("new", name) + + if result.error_message: + console.error( + result.error_message, include_traceback=result.should_include_stacktrace + ) + + if result.info_message: + console.info(result.info_message) + + if not result.should_continue: + return # an agent framework scaffolded the project + + if scaffold_type is ProjectType.AGENT: + console.error( + "No agent framework is installed, so there is nothing to " + "scaffold an agent with.\n" + "Install the framework you want to use and run this command " + f"again — see {AGENT_FRAMEWORKS_DOCS_URL} for the supported " + "frameworks and their packages.\n" + f"Or run `uipath new {name} --type function` to create a " + "function project." + ) with console.spinner(f"Creating new project {name} in current directory ..."): generate_script(directory) diff --git a/packages/uipath/src/uipath/_cli/models/project_types.py b/packages/uipath/src/uipath/_cli/models/project_types.py new file mode 100644 index 000000000..1721f5999 --- /dev/null +++ b/packages/uipath/src/uipath/_cli/models/project_types.py @@ -0,0 +1,21 @@ +"""Project types scaffolded by `uipath new`. + +Framework integrations (uipath-langchain and the packages in +UiPath/uipath-integrations-python) import this to decide whether a +`uipath new` invocation is theirs to handle. +""" + +from enum import StrEnum + + +class ProjectType(StrEnum): + """What `uipath new` scaffolds. + + AUTO (the default) lets an installed agent framework claim the scaffold + and falls back to a function project; FUNCTION and AGENT request one + explicitly. + """ + + AUTO = "auto" + FUNCTION = "function" + AGENT = "agent" diff --git a/packages/uipath/tests/cli/test_new.py b/packages/uipath/tests/cli/test_new.py index d2d278a62..747e2f857 100644 --- a/packages/uipath/tests/cli/test_new.py +++ b/packages/uipath/tests/cli/test_new.py @@ -3,6 +3,7 @@ import re import uuid from importlib.metadata import version +from types import SimpleNamespace from unittest.mock import patch from click.testing import CliRunner @@ -10,6 +11,10 @@ from packaging.version import Version from uipath._cli import cli +from uipath._cli.cli_new import ( + AGENT_FRAMEWORKS_DOCS_URL, + _installed_agent_framework_packages, +) from uipath._cli.middlewares import MiddlewareResult @@ -84,6 +89,162 @@ def test_new_project_middleware_interaction( assert result.exit_code == 0 assert os.path.exists("main.py") + def test_new_default_type_consults_the_agent_frameworks( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Without --type, an installed agent framework gets first refusal. + + The chain is called with the project name only: the base CLI decides + whether frameworks are consulted at all, so they need to know nothing + about project types. + """ + with runner.isolated_filesystem(temp_dir=temp_dir): + with patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware: + mock_middleware.return_value = MiddlewareResult(should_continue=True) + + result = runner.invoke(cli, ["new", "my_project"]) + assert result.exit_code == 0 + mock_middleware.assert_called_once_with("new", "my_project") + # Nothing claimed it, so the base function scaffold runs. + assert os.path.exists("uipath.json") + + def test_new_type_auto_claimed_by_a_framework( + self, runner: CliRunner, temp_dir: str + ) -> None: + """--type auto lets an agent framework claim the scaffold.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware: + mock_middleware.return_value = MiddlewareResult(should_continue=False) + + result = runner.invoke(cli, ["new", "my_agent", "--type", "auto"]) + assert result.exit_code == 0 + mock_middleware.assert_called_once_with("new", "my_agent") + # Claimed by a framework: no base function scaffold. + assert not os.path.exists("uipath.json") + + def test_new_type_function_never_consults_agent_frameworks( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Regression guard for #1543. + + An installed agent framework used to claim `uipath new` unconditionally, + making the base function scaffold unreachable. The chain is now skipped + entirely for a function project, so no framework can intercept it. + """ + with runner.isolated_filesystem(temp_dir=temp_dir): + with patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware: + mock_middleware.return_value = MiddlewareResult(should_continue=False) + + result = runner.invoke(cli, ["new", "my_project", "--type", "function"]) + assert result.exit_code == 0 + mock_middleware.assert_not_called() + with open("uipath.json") as f: + config = json.load(f) + assert config["functions"] == {"main": "main.py:main"} + + def test_new_type_agent_scaffolds_through_the_framework( + self, runner: CliRunner, temp_dir: str + ) -> None: + """--type agent with a framework installed hands over to it.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware: + mock_middleware.return_value = MiddlewareResult(should_continue=False) + + result = runner.invoke(cli, ["new", "my_agent", "--type", "agent"]) + assert result.exit_code == 0 + mock_middleware.assert_called_once_with("new", "my_agent") + assert not os.path.exists("uipath.json") + + def test_new_type_agent_without_a_framework_errors( + self, runner: CliRunner, temp_dir: str + ) -> None: + """--type agent is a guarantee: never silently a function project.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware: + mock_middleware.return_value = MiddlewareResult(should_continue=True) + + result = runner.invoke(cli, ["new", "my_agent", "--type", "agent"]) + assert result.exit_code == 1 + assert "No agent framework is installed" in result.output + assert AGENT_FRAMEWORKS_DOCS_URL in result.output + assert "--type function" in result.output + assert not os.path.exists("main.py") + assert not os.path.exists("uipath.json") + + def test_new_multiple_frameworks_installed_error_names_them( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Several frameworks installed must not resolve by registration order.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + for extra_args in ([], ["--type", "auto"], ["--type", "agent"]): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new._installed_agent_framework_packages", + return_value=["uipath-langchain", "uipath-llamaindex"], + ), + ): + result = runner.invoke(cli, ["new", "my_agent"] + extra_args) + assert result.exit_code == 1 + assert "Multiple agent frameworks are installed" in result.output + assert "uipath-langchain, uipath-llamaindex" in result.output + assert "--type function" in result.output + assert not os.path.exists("main.py") + mock_middleware.assert_not_called() + + def test_installed_agent_framework_packages_names_the_distribution(self) -> None: + """Package names come from the entry points, not from a static list.""" + + def fake_middleware(name: str) -> MiddlewareResult: # pragma: no cover + return MiddlewareResult(should_continue=True) + + fake_middleware.__module__ = "uipath_langchain._cli.cli_new" + entry_point = SimpleNamespace( + module="uipath_langchain.middlewares", + dist=SimpleNamespace(name="uipath-langchain"), + ) + + with patch( + "uipath._cli.cli_new.Middlewares.get", return_value=[fake_middleware] + ): + with patch( + "uipath._cli.cli_new.importlib.metadata.entry_points", + return_value=[entry_point], + ): + assert _installed_agent_framework_packages() == ["uipath-langchain"] + # Registered in-process instead of through an entry point: there is + # no distribution to name it with, so the module stands in. + with patch( + "uipath._cli.cli_new.importlib.metadata.entry_points", return_value=[] + ): + assert _installed_agent_framework_packages() == ["uipath_langchain"] + + def test_new_agent_framework_option_is_gone( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Frameworks are discovered, never named on the command line.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + result = runner.invoke( + cli, + [ + "new", + "my_agent", + "--type", + "agent", + "--agent-framework", + "langchain", + ], + ) + assert result.exit_code == 2 + assert not os.path.exists("main.py") + + def test_new_invalid_type_rejected(self, runner: CliRunner, temp_dir: str) -> None: + """Unknown --type values are rejected by click.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + result = runner.invoke(cli, ["new", "my_project", "--type", "workflow"]) + assert result.exit_code == 2 + assert not os.path.exists("main.py") + def test_new_project_error_handling(self, runner: CliRunner, temp_dir: str) -> None: """Test error handling in new command.""" with runner.isolated_filesystem(temp_dir=temp_dir): diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index c9851f030..cd8ee6671 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.22" +version = "2.14.23" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From c28e863c72bd9f8684c24b6417855225a6402948 Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Tue, 15 Sep 2026 17:16:31 +0300 Subject: [PATCH 2/4] refactor(cli): address review comments on uipath new - Move AGENT_FRAMEWORKS_DOCS_URL to _cli/_utils/_constants.py. - Drop the ProjectType module docstring's note about which packages import it: the integrations no longer do. - Trim the explanation from _installed_agent_framework_packages() and from the middleware-chain comment in the command. - State what `--type function` guarantees in its test, instead of pointing at the issue it came from. - Drop "Using the installed '' agent framework." from the CLI reference transcript: the command no longer resolves a framework by name, so it no longer prints that line. Co-Authored-By: Claude Opus 5 (1M context) --- packages/uipath/docs/cli/index.md | 1 - packages/uipath/docs/core/agents.md | 2 +- .../uipath/src/uipath/_cli/_utils/_constants.py | 4 ++++ packages/uipath/src/uipath/_cli/cli_new.py | 16 +++------------- .../src/uipath/_cli/models/project_types.py | 7 +------ packages/uipath/tests/cli/test_new.py | 13 +++++-------- 6 files changed, 14 insertions(+), 29 deletions(-) diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index 68a2b1d4e..ccc996e58 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -111,7 +111,6 @@ Resolved 42 packages in 1.2s Installed 42 packages in 0.8s > uipath new my-agent --type agent -Using the installed 'langchain' agent framework. ⠋ Creating new agent my-agent in current directory ... ✓ Created 'main.py' file. ✓ Created 'langgraph.json' file. diff --git a/packages/uipath/docs/core/agents.md b/packages/uipath/docs/core/agents.md index ef834d6d8..59bdaab28 100644 --- a/packages/uipath/docs/core/agents.md +++ b/packages/uipath/docs/core/agents.md @@ -89,7 +89,7 @@ The example below uses LangChain. Swap `uipath-langchain` for the framework of y //// /// info | Guaranteeing an agent project -`uipath new` defaults to `--type auto`: the installed framework integration claims the scaffold, which is why the commands above create an agent project. Pass `--type agent` to make that a requirement — it fails with instructions instead of creating a function project when no framework is installed, which is the difference that matters in CI and in scripts: +`uipath new` defaults to `--type auto`: the installed framework integration claims the scaffold, which is why the commands above create an agent project. Pass `--type agent` to make that a requirement — it fails with instructions instead of creating a function project when no framework is installed: ```shell uipath new agent --type agent diff --git a/packages/uipath/src/uipath/_cli/_utils/_constants.py b/packages/uipath/src/uipath/_cli/_utils/_constants.py index 88dac9a96..39a286725 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_constants.py +++ b/packages/uipath/src/uipath/_cli/_utils/_constants.py @@ -61,3 +61,7 @@ def is_binary_file(file_extension: str) -> bool: """Determine if a file should be treated as binary.""" return file_extension.lower() in BINARY_EXTENSIONS + + +# Supported agent frameworks and their packages, for `uipath new --type agent` +AGENT_FRAMEWORKS_DOCS_URL = "https://uipath.github.io/uipath-python/core/agents/" diff --git a/packages/uipath/src/uipath/_cli/cli_new.py b/packages/uipath/src/uipath/_cli/cli_new.py index 203308971..d72430889 100644 --- a/packages/uipath/src/uipath/_cli/cli_new.py +++ b/packages/uipath/src/uipath/_cli/cli_new.py @@ -10,17 +10,13 @@ from ._telemetry import track_command from ._utils._console import ConsoleLogger +from ._utils._constants import AGENT_FRAMEWORKS_DOCS_URL from ._utils._project_files import resolve_existing_project_id from .middlewares import Middlewares from .models.project_types import ProjectType console = ConsoleLogger() -# Agent frameworks are documented, not enumerated in code: each integration -# ships its own `new` middleware, so the CLI knows which ones are installed -# but cannot know which ones exist. -AGENT_FRAMEWORKS_DOCS_URL = "https://uipath.github.io/uipath-python/core/agents/" - # The `uipath` minor release that scaffolded projects are pinned to. # Deliberately a constant: the guard test in tests/cli/test_new.py fails on # every minor bump so the scaffold (pin, template, hints) gets reviewed @@ -65,12 +61,7 @@ def generate_uipath_json(target_directory): def _installed_agent_framework_packages() -> list[str]: - """Packages of the installed agent frameworks that can scaffold a project. - - Derived from the registered `new` middlewares rather than from a list of - known frameworks, so a framework the CLI has never heard of is named - correctly and a new one needs no change here. - """ + """Packages of the installed agent frameworks that can scaffold a project.""" modules = { middleware.__module__.split(".")[0] for middleware in Middlewares.get("new") } @@ -111,8 +102,7 @@ def new(name: str, project_type: str): scaffold_type = ProjectType(project_type) # Agent frameworks scaffold through the `new` middleware chain. A function - # project never consults them, so an installed framework can no longer make - # the base scaffold unreachable (#1543). + # project never consults them. if scaffold_type is not ProjectType.FUNCTION: Middlewares.load_plugins() installed = _installed_agent_framework_packages() diff --git a/packages/uipath/src/uipath/_cli/models/project_types.py b/packages/uipath/src/uipath/_cli/models/project_types.py index 1721f5999..c8ab05ae1 100644 --- a/packages/uipath/src/uipath/_cli/models/project_types.py +++ b/packages/uipath/src/uipath/_cli/models/project_types.py @@ -1,9 +1,4 @@ -"""Project types scaffolded by `uipath new`. - -Framework integrations (uipath-langchain and the packages in -UiPath/uipath-integrations-python) import this to decide whether a -`uipath new` invocation is theirs to handle. -""" +"""Project types scaffolded by `uipath new`.""" from enum import StrEnum diff --git a/packages/uipath/tests/cli/test_new.py b/packages/uipath/tests/cli/test_new.py index 747e2f857..3af5ce7f2 100644 --- a/packages/uipath/tests/cli/test_new.py +++ b/packages/uipath/tests/cli/test_new.py @@ -11,10 +11,8 @@ from packaging.version import Version from uipath._cli import cli -from uipath._cli.cli_new import ( - AGENT_FRAMEWORKS_DOCS_URL, - _installed_agent_framework_packages, -) +from uipath._cli._utils._constants import AGENT_FRAMEWORKS_DOCS_URL +from uipath._cli.cli_new import _installed_agent_framework_packages from uipath._cli.middlewares import MiddlewareResult @@ -125,11 +123,10 @@ def test_new_type_auto_claimed_by_a_framework( def test_new_type_function_never_consults_agent_frameworks( self, runner: CliRunner, temp_dir: str ) -> None: - """Regression guard for #1543. + """--type function must always produce a function project. - An installed agent framework used to claim `uipath new` unconditionally, - making the base function scaffold unreachable. The chain is now skipped - entirely for a function project, so no framework can intercept it. + The middleware chain is skipped entirely, so an installed agent + framework cannot intercept the scaffold no matter what it claims. """ with runner.isolated_filesystem(temp_dir=temp_dir): with patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware: From 129c4511c469dc4fd16345e2083c7de2a35dda7f Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Wed, 16 Sep 2026 14:06:05 +0300 Subject: [PATCH 3/4] feat(cli): add --agent-framework to pick between installed frameworks With several agent framework packages in the same environment, `uipath new` could only report the ambiguity and ask for one to be uninstalled. It now takes the choice on the command line: uipath new my-agent --type agent --agent-framework uipath-langchain The value is the integration package, spelled exactly as the errors list it, and the scaffold is dispatched to that framework alone rather than through the chain, so another one cannot claim it first. Naming a framework that is not installed lists the ones that are, and the option stays limited to `--type agent`. The accepted values are still discovered, never hardcoded: frameworks come from the registered `new` middlewares, named after the distribution that registered them, so a framework this CLI has never heard of can be selected by name like any other. Co-Authored-By: Claude Opus 5 (1M context) --- packages/uipath/docs/cli/index.md | 10 +- packages/uipath/docs/core/agents.md | 8 +- packages/uipath/docs/core/studio_web.md | 2 +- packages/uipath/src/uipath/_cli/cli_new.py | 99 ++++++--- .../uipath/_cli/models/agent_frameworks.py | 16 ++ packages/uipath/tests/cli/test_new.py | 188 +++++++++++++++--- 6 files changed, 262 insertions(+), 61 deletions(-) create mode 100644 packages/uipath/src/uipath/_cli/models/agent_frameworks.py diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index ccc996e58..1e30df55b 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -83,9 +83,15 @@ Scaffolds a project in the current directory. `--type` selects what gets created |----------------------------------|------------------------|------------------------------| | none | coded function project | error: install an agent framework | | one | that framework's coded agent project | that framework's coded agent project | -| several | error: naming the installed packages | error: naming the installed packages | +| several | error: pick one with `--agent-framework` | error: pick one with `--agent-framework` | -The framework is never named on the command line — it is whichever integration package the environment has. Install exactly the one you want to build with; with several installed, `uipath new` cannot tell which you meant and says so. +With one framework installed there is nothing to choose, so `uipath new` uses it. With several, it cannot tell which you meant and asks you to name one: + +```shell +uipath new my-agent --type agent --agent-framework uipath-langchain +``` + +`--agent-framework` takes the integration package name, is only valid together with `--type agent`, and scaffolds with that framework alone. The names it accepts are the packages installed in your environment — the same ones the error above lists — so `uipath` keeps no list of its own. Scaffold a coded function: diff --git a/packages/uipath/docs/core/agents.md b/packages/uipath/docs/core/agents.md index 59bdaab28..c03b58cf4 100644 --- a/packages/uipath/docs/core/agents.md +++ b/packages/uipath/docs/core/agents.md @@ -27,7 +27,7 @@ The `uipath` package is always required. Add one framework extension on top: | UiPath Agent Framework | `uipath-agent-framework` | UiPath-native agent primitives | | Claude Agent SDK | `uipath-claude-sdk` | Claude models, Anthropic-native agent loop | -Installing one of these packages is what makes `uipath new` scaffold an agent: the framework is chosen by what's in your environment, not by a CLI flag. Install exactly the one you intend to build with. +Installing one of these packages is what makes `uipath new` scaffold an agent. With more than one installed, name the one you want with `--agent-framework`, using the package exactly as it appears above. --- @@ -95,7 +95,11 @@ The example below uses LangChain. Swap `uipath-langchain` for the framework of y uipath new agent --type agent ``` -With several framework integrations installed in the same environment, `uipath new` cannot tell which one you meant and asks you to keep just one. +With several framework integrations installed in the same environment, name the one to scaffold with: + +```shell +uipath new agent --type agent --agent-framework uipath-langchain +``` /// --- diff --git a/packages/uipath/docs/core/studio_web.md b/packages/uipath/docs/core/studio_web.md index 2136fee30..c125e538b 100644 --- a/packages/uipath/docs/core/studio_web.md +++ b/packages/uipath/docs/core/studio_web.md @@ -224,7 +224,7 @@ Selected tenant: Tenant1 That's it, your agent should now be visible in Studio Web. /// info -`uipath new` defaults to `--type auto`, which lets the installed framework integration claim the scaffold. Pass `--type agent` to require an agent project — it fails rather than creating a function project when no framework is installed. With several integrations installed, keep only the one you want to scaffold with. +`uipath new` defaults to `--type auto`, which lets the installed framework integration claim the scaffold. Pass `--type agent` to require an agent project — it fails rather than creating a function project when no framework is installed. With several integrations installed, name the one to scaffold with: `uipath new agent --type agent --agent-framework uipath-langchain`. /// #### Coded Function diff --git a/packages/uipath/src/uipath/_cli/cli_new.py b/packages/uipath/src/uipath/_cli/cli_new.py index d72430889..8a1a94ee0 100644 --- a/packages/uipath/src/uipath/_cli/cli_new.py +++ b/packages/uipath/src/uipath/_cli/cli_new.py @@ -12,7 +12,8 @@ from ._utils._console import ConsoleLogger from ._utils._constants import AGENT_FRAMEWORKS_DOCS_URL from ._utils._project_files import resolve_existing_project_id -from .middlewares import Middlewares +from .middlewares import MiddlewareResult, Middlewares +from .models.agent_frameworks import AgentFramework from .models.project_types import ProjectType console = ConsoleLogger() @@ -60,20 +61,62 @@ def generate_uipath_json(target_directory): json.dump(uipath_config, f, indent=2) -def _installed_agent_framework_packages() -> list[str]: - """Packages of the installed agent frameworks that can scaffold a project.""" - modules = { - middleware.__module__.split(".")[0] for middleware in Middlewares.get("new") - } - packages: set[str] = set() +def installed_agent_frameworks() -> list[AgentFramework]: + """Agent frameworks that can scaffold a project in this environment.""" + packages: dict[str, str] = {} for entry_point in importlib.metadata.entry_points(group="uipath.middlewares"): - module = entry_point.module.split(".")[0] - if module in modules and entry_point.dist is not None: - packages.add(entry_point.dist.name) - modules.discard(module) - # A middleware registered in-process rather than through an entry point has - # no distribution to name; its module is the most accurate thing left. - return sorted(packages | modules) + if entry_point.dist is not None: + packages.setdefault(entry_point.module.split(".")[0], entry_point.dist.name) + + frameworks = [] + for middleware in Middlewares.get("new"): + package = packages.get(middleware.__module__.split(".")[0]) + if package is not None: + frameworks.append(AgentFramework(package=package, scaffold=middleware)) + return sorted(frameworks, key=lambda framework: framework.package) + + +def _select_agent_framework( + frameworks: list[AgentFramework], requested: str +) -> AgentFramework: + """Resolve `--agent-framework` against what is installed.""" + for framework in frameworks: + if requested == framework.package: + return framework + + installed = ( + "Installed: " + ", ".join(framework.package for framework in frameworks) + "." + if frameworks + else "No agent framework is installed." + ) + console.error( + f"No installed agent framework matches '{requested}'.\n" + f"{installed}\n" + f"See {AGENT_FRAMEWORKS_DOCS_URL} for the supported frameworks and " + "their packages." + ) + + +def _scaffold_agent(name: str, agent_framework: str | None) -> MiddlewareResult: + """Offer the scaffold to the installed agent frameworks.""" + Middlewares.load_plugins() + installed = installed_agent_frameworks() + + if agent_framework: + # Dispatch to the chosen framework alone, so that another one cannot + # claim the scaffold ahead of it. + return _select_agent_framework(installed, agent_framework).scaffold(name) + + if len(installed) > 1: + console.error( + "Multiple agent frameworks are installed: " + + ", ".join(framework.package for framework in installed) + + f".\nPick one with `uipath new {name} --type agent " + "--agent-framework `, or run " + f"`uipath new {name} --type function` to create a function project." + ) + + return Middlewares.next("new", name) @click.command() @@ -89,8 +132,16 @@ def _installed_agent_framework_packages() -> list[str]: "otherwise; 'function' always scaffolds a function; 'agent' scaffolds an " "agent and fails when no agent framework is installed.", ) +@click.option( + "--agent-framework", + "agent_framework", + default=None, + help="Agent framework to scaffold with, named by its package (e.g. " + "`uipath-langchain`). Only valid together with `--type agent`; needed " + "when several frameworks are installed, optional otherwise.", +) @track_command("new") -def new(name: str, project_type: str): +def new(name: str, project_type: str, agent_framework: str | None): """Generate a quick-start project.""" directory = os.getcwd() @@ -101,21 +152,15 @@ def new(name: str, project_type: str): scaffold_type = ProjectType(project_type) + if agent_framework and scaffold_type is not ProjectType.AGENT: + console.error( + "`--agent-framework` can only be used together with `--type agent`." + ) + # Agent frameworks scaffold through the `new` middleware chain. A function # project never consults them. if scaffold_type is not ProjectType.FUNCTION: - Middlewares.load_plugins() - installed = _installed_agent_framework_packages() - if len(installed) > 1: - console.error( - "Multiple agent frameworks are installed: " - + ", ".join(installed) - + ".\nKeep the one you want to scaffold with in this environment, " - f"or run `uipath new {name} --type function` to create a " - "function project." - ) - - result = Middlewares.next("new", name) + result = _scaffold_agent(name, agent_framework) if result.error_message: console.error( diff --git a/packages/uipath/src/uipath/_cli/models/agent_frameworks.py b/packages/uipath/src/uipath/_cli/models/agent_frameworks.py new file mode 100644 index 000000000..876b5100b --- /dev/null +++ b/packages/uipath/src/uipath/_cli/models/agent_frameworks.py @@ -0,0 +1,16 @@ +"""Agent frameworks that scaffold projects for `uipath new`.""" + +from dataclasses import dataclass + +from ..middlewares import MiddlewareFunc + + +@dataclass(frozen=True) +class AgentFramework: + """An agent framework installed in this environment.""" + + package: str + """Package that provides it, and the value `--agent-framework` takes.""" + + scaffold: MiddlewareFunc + """Its `new` middleware.""" diff --git a/packages/uipath/tests/cli/test_new.py b/packages/uipath/tests/cli/test_new.py index 3af5ce7f2..e15fa1aec 100644 --- a/packages/uipath/tests/cli/test_new.py +++ b/packages/uipath/tests/cli/test_new.py @@ -4,7 +4,8 @@ import uuid from importlib.metadata import version from types import SimpleNamespace -from unittest.mock import patch +from typing import cast +from unittest.mock import MagicMock, patch from click.testing import CliRunner from packaging.specifiers import SpecifierSet @@ -12,8 +13,22 @@ from uipath._cli import cli from uipath._cli._utils._constants import AGENT_FRAMEWORKS_DOCS_URL -from uipath._cli.cli_new import _installed_agent_framework_packages +from uipath._cli.cli_new import installed_agent_frameworks from uipath._cli.middlewares import MiddlewareResult +from uipath._cli.models.agent_frameworks import AgentFramework + + +def _framework(package: str) -> AgentFramework: + """An installed agent framework whose scaffold claims the project.""" + return AgentFramework( + package=package, + scaffold=MagicMock(return_value=MiddlewareResult(should_continue=False)), + ) + + +def _scaffold_of(framework: AgentFramework) -> MagicMock: + """The mock behind a `_framework()` scaffold, to assert calls on.""" + return cast(MagicMock, framework.scaffold) class TestNew: @@ -168,7 +183,7 @@ def test_new_type_agent_without_a_framework_errors( assert not os.path.exists("main.py") assert not os.path.exists("uipath.json") - def test_new_multiple_frameworks_installed_error_names_them( + def test_new_multiple_frameworks_installed_require_a_choice( self, runner: CliRunner, temp_dir: str ) -> None: """Several frameworks installed must not resolve by registration order.""" @@ -177,20 +192,152 @@ def test_new_multiple_frameworks_installed_error_names_them( with ( patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, patch( - "uipath._cli.cli_new._installed_agent_framework_packages", - return_value=["uipath-langchain", "uipath-llamaindex"], + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[ + _framework("uipath-langchain"), + _framework("uipath-llamaindex"), + ], ), ): result = runner.invoke(cli, ["new", "my_agent"] + extra_args) assert result.exit_code == 1 assert "Multiple agent frameworks are installed" in result.output assert "uipath-langchain, uipath-llamaindex" in result.output + assert "--agent-framework" in result.output assert "--type function" in result.output assert not os.path.exists("main.py") mock_middleware.assert_not_called() - def test_installed_agent_framework_packages_names_the_distribution(self) -> None: - """Package names come from the entry points, not from a static list.""" + def test_agent_framework_scaffolds_with_the_chosen_one_only( + self, runner: CliRunner, temp_dir: str + ) -> None: + """--agent-framework dispatches to that framework and nothing else.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + chosen = _framework("uipath-llamaindex") + other = _framework("uipath-langchain") + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[other, chosen], + ), + ): + result = runner.invoke( + cli, + [ + "new", + "my_agent", + "--type", + "agent", + "--agent-framework", + "uipath-llamaindex", + ], + ) + assert result.exit_code == 0 + _scaffold_of(chosen).assert_called_once_with("my_agent") + _scaffold_of(other).assert_not_called() + # The chain would have let the first registered one claim it. + mock_middleware.assert_not_called() + assert not os.path.exists("uipath.json") + + def test_agent_framework_takes_the_package_name_only( + self, runner: CliRunner, temp_dir: str + ) -> None: + """One spelling: the package, exactly as the error messages list it.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + chosen = _framework("uipath-langchain") + for requested, expected_exit in (("uipath-langchain", 0), ("langchain", 1)): + with patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[chosen, _framework("uipath-llamaindex")], + ): + result = runner.invoke( + cli, + [ + "new", + "my_agent", + "--type", + "agent", + "--agent-framework", + requested, + ], + ) + assert result.exit_code == expected_exit + _scaffold_of(chosen).assert_called_once_with("my_agent") + + def test_agent_framework_not_installed_errors( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Asking for a framework that isn't installed names the ones that are.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + installed = _framework("uipath-langchain") + with patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[installed], + ): + result = runner.invoke( + cli, + [ + "new", + "my_agent", + "--type", + "agent", + "--agent-framework", + "uipath-crewai", + ], + ) + assert result.exit_code == 1 + assert ( + "No installed agent framework matches 'uipath-crewai'" + in result.output + ) + assert "Installed: uipath-langchain." in result.output + assert AGENT_FRAMEWORKS_DOCS_URL in result.output + _scaffold_of(installed).assert_not_called() + assert not os.path.exists("main.py") + + def test_agent_framework_with_none_installed_errors( + self, runner: CliRunner, temp_dir: str + ) -> None: + with runner.isolated_filesystem(temp_dir=temp_dir): + with patch( + "uipath._cli.cli_new.installed_agent_frameworks", return_value=[] + ): + result = runner.invoke( + cli, + [ + "new", + "my_agent", + "--type", + "agent", + "--agent-framework", + "uipath-langchain", + ], + ) + assert result.exit_code == 1 + assert "No agent framework is installed." in result.output + assert not os.path.exists("main.py") + + def test_agent_framework_requires_type_agent( + self, runner: CliRunner, temp_dir: str + ) -> None: + """The flag names an agent framework, so it only makes sense for agents.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + for extra_args in ([], ["--type", "auto"], ["--type", "function"]): + result = runner.invoke( + cli, + ["new", "my_project", "--agent-framework", "uipath-langchain"] + + extra_args, + ) + assert result.exit_code == 1 + assert ( + "`--agent-framework` can only be used together with " + "`--type agent`" in result.output + ) + assert not os.path.exists("main.py") + + def test_installed_agent_frameworks_are_named_after_their_package(self) -> None: + """Frameworks come from the registered middlewares, not a static list.""" def fake_middleware(name: str) -> MiddlewareResult: # pragma: no cover return MiddlewareResult(should_continue=True) @@ -208,32 +355,15 @@ def fake_middleware(name: str) -> MiddlewareResult: # pragma: no cover "uipath._cli.cli_new.importlib.metadata.entry_points", return_value=[entry_point], ): - assert _installed_agent_framework_packages() == ["uipath-langchain"] + (framework,) = installed_agent_frameworks() + assert framework.package == "uipath-langchain" + assert framework.scaffold is fake_middleware # Registered in-process instead of through an entry point: there is - # no distribution to name it with, so the module stands in. + # no package to name it with, so it is not one to choose between. with patch( "uipath._cli.cli_new.importlib.metadata.entry_points", return_value=[] ): - assert _installed_agent_framework_packages() == ["uipath_langchain"] - - def test_new_agent_framework_option_is_gone( - self, runner: CliRunner, temp_dir: str - ) -> None: - """Frameworks are discovered, never named on the command line.""" - with runner.isolated_filesystem(temp_dir=temp_dir): - result = runner.invoke( - cli, - [ - "new", - "my_agent", - "--type", - "agent", - "--agent-framework", - "langchain", - ], - ) - assert result.exit_code == 2 - assert not os.path.exists("main.py") + assert installed_agent_frameworks() == [] def test_new_invalid_type_rejected(self, runner: CliRunner, temp_dir: str) -> None: """Unknown --type values are rejected by click.""" From 195fac3394744e35cd91d8c95805cc07c3bc108e Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Fri, 18 Sep 2026 16:23:46 +0300 Subject: [PATCH 4/4] fix(cli): warn instead of erroring when several frameworks are installed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Environments with more than one agent framework package scaffolded fine before `--type` existed — the first discovered integration claimed the project. Turning that into an error would be a breaking change, so the old outcome stays: `uipath new` warns, names all the installed frameworks and the one it picked, and scaffolds with the first discovered. `--agent-framework` remains the way to choose deliberately. The pick is dispatched to that framework's middleware directly, so the warning always names the integration that actually scaffolds. Discovery order is the entry-point order (a filesystem accident, stable per environment); listings shown to the user stay sorted. Co-Authored-By: Claude Fable 5 --- packages/uipath/docs/cli/index.md | 4 ++-- packages/uipath/docs/core/agents.md | 2 +- packages/uipath/docs/core/studio_web.md | 2 +- packages/uipath/src/uipath/_cli/cli_new.py | 22 ++++++++++------- packages/uipath/tests/cli/test_new.py | 28 ++++++++++++++-------- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index 1e30df55b..0fdddfe13 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -83,9 +83,9 @@ Scaffolds a project in the current directory. `--type` selects what gets created |----------------------------------|------------------------|------------------------------| | none | coded function project | error: install an agent framework | | one | that framework's coded agent project | that framework's coded agent project | -| several | error: pick one with `--agent-framework` | error: pick one with `--agent-framework` | +| several | warns, scaffolds with the first discovered | warns, scaffolds with the first discovered | -With one framework installed there is nothing to choose, so `uipath new` uses it. With several, it cannot tell which you meant and asks you to name one: +With one framework installed there is nothing to choose, so `uipath new` uses it. With several, it warns and scaffolds with the first one discovered — pass `--agent-framework` to choose deliberately: ```shell uipath new my-agent --type agent --agent-framework uipath-langchain diff --git a/packages/uipath/docs/core/agents.md b/packages/uipath/docs/core/agents.md index c03b58cf4..add97b8ed 100644 --- a/packages/uipath/docs/core/agents.md +++ b/packages/uipath/docs/core/agents.md @@ -95,7 +95,7 @@ The example below uses LangChain. Swap `uipath-langchain` for the framework of y uipath new agent --type agent ``` -With several framework integrations installed in the same environment, name the one to scaffold with: +With several framework integrations installed in the same environment, `uipath new` warns and uses the first one discovered; name the one you want instead with: ```shell uipath new agent --type agent --agent-framework uipath-langchain diff --git a/packages/uipath/docs/core/studio_web.md b/packages/uipath/docs/core/studio_web.md index c125e538b..d44a565c4 100644 --- a/packages/uipath/docs/core/studio_web.md +++ b/packages/uipath/docs/core/studio_web.md @@ -224,7 +224,7 @@ Selected tenant: Tenant1 That's it, your agent should now be visible in Studio Web. /// info -`uipath new` defaults to `--type auto`, which lets the installed framework integration claim the scaffold. Pass `--type agent` to require an agent project — it fails rather than creating a function project when no framework is installed. With several integrations installed, name the one to scaffold with: `uipath new agent --type agent --agent-framework uipath-langchain`. +`uipath new` defaults to `--type auto`, which lets the installed framework integration claim the scaffold. Pass `--type agent` to require an agent project — it fails rather than creating a function project when no framework is installed. With several integrations installed, `uipath new` warns and uses the first one discovered; pick explicitly with `uipath new agent --type agent --agent-framework uipath-langchain`. /// #### Coded Function diff --git a/packages/uipath/src/uipath/_cli/cli_new.py b/packages/uipath/src/uipath/_cli/cli_new.py index 8a1a94ee0..0d6aa5847 100644 --- a/packages/uipath/src/uipath/_cli/cli_new.py +++ b/packages/uipath/src/uipath/_cli/cli_new.py @@ -62,7 +62,7 @@ def generate_uipath_json(target_directory): def installed_agent_frameworks() -> list[AgentFramework]: - """Agent frameworks that can scaffold a project in this environment.""" + """Agent frameworks that can scaffold a project, in discovery order.""" packages: dict[str, str] = {} for entry_point in importlib.metadata.entry_points(group="uipath.middlewares"): if entry_point.dist is not None: @@ -73,7 +73,7 @@ def installed_agent_frameworks() -> list[AgentFramework]: package = packages.get(middleware.__module__.split(".")[0]) if package is not None: frameworks.append(AgentFramework(package=package, scaffold=middleware)) - return sorted(frameworks, key=lambda framework: framework.package) + return frameworks def _select_agent_framework( @@ -85,7 +85,9 @@ def _select_agent_framework( return framework installed = ( - "Installed: " + ", ".join(framework.package for framework in frameworks) + "." + "Installed: " + + ", ".join(sorted(framework.package for framework in frameworks)) + + "." if frameworks else "No agent framework is installed." ) @@ -108,13 +110,15 @@ def _scaffold_agent(name: str, agent_framework: str | None) -> MiddlewareResult: return _select_agent_framework(installed, agent_framework).scaffold(name) if len(installed) > 1: - console.error( + first = installed[0] + console.warning( "Multiple agent frameworks are installed: " + ", ".join(framework.package for framework in installed) - + f".\nPick one with `uipath new {name} --type agent " - "--agent-framework `, or run " - f"`uipath new {name} --type function` to create a function project." + + f".\nScaffolding with the first one discovered: '{first.package}'. " + f"To pick a different one, run `uipath new {name} --type agent " + "--agent-framework `." ) + return first.scaffold(name) return Middlewares.next("new", name) @@ -137,8 +141,8 @@ def _scaffold_agent(name: str, agent_framework: str | None) -> MiddlewareResult: "agent_framework", default=None, help="Agent framework to scaffold with, named by its package (e.g. " - "`uipath-langchain`). Only valid together with `--type agent`; needed " - "when several frameworks are installed, optional otherwise.", + "`uipath-langchain`). Only valid together with `--type agent`; picks " + "which framework scaffolds when several are installed.", ) @track_command("new") def new(name: str, project_type: str, agent_framework: str | None): diff --git a/packages/uipath/tests/cli/test_new.py b/packages/uipath/tests/cli/test_new.py index e15fa1aec..08fe9ba37 100644 --- a/packages/uipath/tests/cli/test_new.py +++ b/packages/uipath/tests/cli/test_new.py @@ -183,30 +183,38 @@ def test_new_type_agent_without_a_framework_errors( assert not os.path.exists("main.py") assert not os.path.exists("uipath.json") - def test_new_multiple_frameworks_installed_require_a_choice( + def test_new_multiple_frameworks_warn_and_use_the_first_discovered( self, runner: CliRunner, temp_dir: str ) -> None: - """Several frameworks installed must not resolve by registration order.""" + """Several frameworks installed: warn, then scaffold with the first. + + Erroring here would break environments that scaffolded fine before + `--type` existed, so the old behaviour stays the default and the + warning points at `--agent-framework` for choosing explicitly. + """ with runner.isolated_filesystem(temp_dir=temp_dir): for extra_args in ([], ["--type", "auto"], ["--type", "agent"]): + first = _framework("uipath-llamaindex") + other = _framework("uipath-langchain") with ( patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, patch( "uipath._cli.cli_new.installed_agent_frameworks", - return_value=[ - _framework("uipath-langchain"), - _framework("uipath-llamaindex"), - ], + return_value=[first, other], ), ): result = runner.invoke(cli, ["new", "my_agent"] + extra_args) - assert result.exit_code == 1 + assert result.exit_code == 0 assert "Multiple agent frameworks are installed" in result.output - assert "uipath-langchain, uipath-llamaindex" in result.output + # listed in discovery order, so the first named is the pick + assert "uipath-llamaindex, uipath-langchain" in result.output + assert "first one discovered: 'uipath-llamaindex'" in result.output assert "--agent-framework" in result.output - assert "--type function" in result.output - assert not os.path.exists("main.py") + _scaffold_of(first).assert_called_once_with("my_agent") + _scaffold_of(other).assert_not_called() mock_middleware.assert_not_called() + # claimed by the framework: no base function scaffold + assert not os.path.exists("uipath.json") def test_agent_framework_scaffolds_with_the_chosen_one_only( self, runner: CliRunner, temp_dir: str