From fe4e5378534b9c7509d6d821eb87b9be17cb1aea Mon Sep 17 00:00:00 2001 From: Vlad Cimpeanu Date: Fri, 11 Sep 2026 15:05:01 +0300 Subject: [PATCH] fix(cli): add --type and --agent-framework to uipath new `uipath new` was unconditionally hijacked by any installed agent framework integration (the middleware always returned should_continue=False), making the base function scaffold unreachable once uipath-langchain was in the environment (#1543). Scaffold selection is now explicit, and every ambiguity is resolved in the base CLI before dispatching to the integrations: - `--type auto` (the new default) keeps the previous behaviour: the single installed integration claims the scaffold, otherwise a function project is created. - `--type function` always creates a function project. - `--type agent` creates an agent project. `--agent-framework` picks the framework; when unset it resolves to the single installed integration, and with none installed the error lists the packages to install. - Several installed integrations with no explicit framework is an error for both `auto` and `agent`, rather than an arbitrary pick. - A framework whose integration does not claim the scaffold fails with pip/uv install instructions, matching `uipath dev`. The project type and agent framework are modelled as StrEnums in `_cli/models/` and forwarded to the `new` middleware so integrations can gate on them. `AgentFramework.claims_scaffold()` is that gate: the rule is the same for every framework, so the integrations name their framework instead of each repeating the comparison. Docs cover the new options: the previously missing `new` section in the CLI reference, an --agent-framework column plus a Claude Agent SDK row in the coded-agents framework table, 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 | 27 +- 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 | 80 ++++- .../uipath/_cli/models/agent_frameworks.py | 78 +++++ .../src/uipath/_cli/models/project_types.py | 21 ++ packages/uipath/tests/cli/test_new.py | 328 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 10 files changed, 597 insertions(+), 14 deletions(-) create mode 100644 packages/uipath/src/uipath/_cli/models/agent_frameworks.py 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..7f81a74cb 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. `--agent-framework` picks the framework and defaults to the one whose integration package is installed. + +| Installed framework integrations | `uipath new x` (auto) | `uipath new x --type agent` | +|----------------------------------|------------------------|------------------------------| +| none | coded function project | error listing the integration packages | +| one | that framework's coded agent project | that framework's coded agent project | +| several | error: pick one explicitly | error: pick one explicitly | + +`--agent-framework` is only valid together with `--type agent`. + +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..d45c6c9f7 100644 --- a/packages/uipath/docs/core/agents.md +++ b/packages/uipath/docs/core/agents.md @@ -17,14 +17,17 @@ Every coded agent is built from two layers: The `uipath` package is always required. Add one framework extension on top: -| Framework | Package | Best for | -|-----------|---------|---------| -| LangChain / LangGraph | `uipath-langchain` | Graph-based agents, complex multi-step flows | -| LlamaIndex | `uipath-llamaindex` | RAG-heavy agents, document reasoning | -| OpenAI Agents SDK | `uipath-openai-agents` | OpenAI-native tool use, handoffs | -| 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 | +| Framework | Package | `--agent-framework` | Best for | +|-----------|---------|---------------------|---------| +| LangChain / LangGraph | `uipath-langchain` | `langchain` | Graph-based agents, complex multi-step flows | +| LlamaIndex | `uipath-llamaindex` | `llamaindex` | RAG-heavy agents, document reasoning | +| OpenAI Agents SDK | `uipath-openai-agents` | `openai-agents` | OpenAI-native tool use, handoffs | +| PydanticAI | `uipath-pydantic-ai` | `pydantic-ai` | Type-safe agents with Pydantic models | +| Google ADK | `uipath-google-adk` | `google-adk` | Gemini models, Google ecosystem | +| UiPath Agent Framework | `uipath-agent-framework` | `microsoft-agent-framework` | UiPath-native agent primitives | +| Claude Agent SDK | `uipath-claude-sdk` | `claude-sdk` | Claude models, Anthropic-native agent loop | + +The `--agent-framework` column is the value to pass to `uipath new --type agent` when scaffolding explicitly (see the note below the Quickstart). --- @@ -85,6 +88,14 @@ The example below uses LangChain. Swap `uipath-langchain` for the framework of y //// +/// info | Explicit project type and framework +`uipath new` defaults to `--type auto`: the installed agent framework integration claims the scaffold, which is why the commands above create an agent project. To request one explicitly — required when several framework integrations are installed in the same environment — pass the type and framework: + +```shell +uipath new agent --type agent --agent-framework langchain +``` +/// + --- ## 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..3c4c5f9fb 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. With several integrations installed, pick one explicitly: `uipath new agent --type agent --agent-framework `. +/// + #### 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 86bb4d18f..c4496ad9e 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.14.14" +version = "2.14.15" 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..1d9712a79 100644 --- a/packages/uipath/src/uipath/_cli/cli_new.py +++ b/packages/uipath/src/uipath/_cli/cli_new.py @@ -11,6 +11,8 @@ from ._utils._console import ConsoleLogger from ._utils._project_files import resolve_existing_project_id from .middlewares import Middlewares +from .models.agent_frameworks import AgentFramework, installed_agent_frameworks +from .models.project_types import ProjectType console = ConsoleLogger() @@ -57,10 +59,49 @@ def generate_uipath_json(target_directory): json.dump(uipath_config, f, indent=2) +def _detect_agent_framework(installed: list[AgentFramework]) -> AgentFramework: + """Resolve an unset --agent-framework from the installed integrations. + + Called with at most one installed integration (several error out before + this): the single installed one wins, none installed errors with the + list of frameworks and their packages. + """ + if installed: + framework = installed[0] + console.info(f"Using the installed '{framework}' agent framework.") + return framework + packages = "\n".join(f" {framework.package}" for framework in AgentFramework) + console.error( + "No agent framework integration is installed.\n" + "Please install the package for the framework you want " + "(`pip install ` or `uv add `):\n\n" + packages + ) + + @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; 'agent' requires one explicitly.", +) +@click.option( + "--agent-framework", + "agent_framework", + type=click.Choice([f.value for f in AgentFramework]), + default=None, + help=( + "Agent framework to scaffold for. Only valid together with `--type agent`; " + "defaults to the framework whose integration package is installed." + ), +) @track_command("new") -def new(name: str): +def new(name: str, project_type: str, agent_framework: str | None): """Generate a quick-start project.""" directory = os.getcwd() @@ -69,7 +110,30 @@ def new(name: str): "Please specify a name for your project:\n`uipath new hello-world`" ) - result = Middlewares.next("new", name) + scaffold_type = ProjectType(project_type) + framework = AgentFramework(agent_framework) if agent_framework else None + + if framework and scaffold_type is not ProjectType.AGENT: + console.error( + "`--agent-framework` can only be used together with `--type agent`." + ) + + if framework is None and scaffold_type is not ProjectType.FUNCTION: + installed = installed_agent_frameworks() + if len(installed) > 1: + console.error( + "Multiple agent frameworks are installed: " + + ", ".join(sorted(installed)) + + ".\nPick one with `--type agent --agent-framework `, " + f"or run `uipath new {name} --type function` to create a " + "function project." + ) + if scaffold_type is ProjectType.AGENT: + framework = _detect_agent_framework(installed) + + result = Middlewares.next( + "new", name, project_type=scaffold_type, agent_framework=framework + ) if result.error_message: console.error( @@ -82,6 +146,18 @@ def new(name: str): if not result.should_continue: return + if framework is not None: # only set for agent scaffolds + console.error( + f"The '{framework.package}' package is required to scaffold a " + f"'{framework}' agent.\n" + "Please install it:\n\n" + " # Using pip:\n" + f" pip install {framework.package}\n\n" + " # Using uv:\n" + f" uv add {framework.package}\n\n" + f"Or run `uipath new {name}` to create a function project." + ) + with console.spinner(f"Creating new project {name} in current directory ..."): generate_script(directory) console.success("Created 'main.py' file.") 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..41b38a495 --- /dev/null +++ b/packages/uipath/src/uipath/_cli/models/agent_frameworks.py @@ -0,0 +1,78 @@ +"""Agent frameworks supported by `uipath new --type agent`. + +Each framework's integration package registers a middleware that claims +agent scaffolds for its framework. +""" + +import importlib.metadata +from enum import StrEnum + +from .project_types import ProjectType + + +class AgentFramework(StrEnum): + """Agent frameworks with a UiPath integration package.""" + + CLAUDE_SDK = "claude-sdk" + GOOGLE_ADK = "google-adk" + LANGCHAIN = "langchain" + LLAMAINDEX = "llamaindex" + MICROSOFT_AGENT_FRAMEWORK = "microsoft-agent-framework" + OPENAI_AGENTS = "openai-agents" + PYDANTIC_AI = "pydantic-ai" + + @property + def package(self) -> str: + """PyPI package that provides this framework's UiPath integration.""" + return _AGENT_FRAMEWORK_PACKAGES[self] + + def claims_scaffold( + self, + project_type: ProjectType, + agent_framework: "AgentFramework | None", + ) -> bool: + """Whether this framework's `new` middleware should scaffold the project. + + Every integration answers the same question — `--type auto` (the + default) lets the installed integration win over the base function + scaffold, and `--type agent` belongs to the framework it names — so + the gate lives here rather than being repeated in each integration: + + if not AgentFramework.LANGCHAIN.claims_scaffold( + project_type, agent_framework + ): + return MiddlewareResult(should_continue=True) + + Comparisons are by value, not identity, so callers passing plain + strings (an older base CLI, or a direct middleware call) still gate + correctly. + """ + return project_type == ProjectType.AUTO or ( + project_type == ProjectType.AGENT and agent_framework == self + ) + + +# uipath-langchain lives in its own repo; the rest come from +# UiPath/uipath-integrations-python (note: microsoft-agent-framework ships +# as `uipath-agent-framework`). +_AGENT_FRAMEWORK_PACKAGES = { + AgentFramework.CLAUDE_SDK: "uipath-claude-sdk", + AgentFramework.GOOGLE_ADK: "uipath-google-adk", + AgentFramework.LANGCHAIN: "uipath-langchain", + AgentFramework.LLAMAINDEX: "uipath-llamaindex", + AgentFramework.MICROSOFT_AGENT_FRAMEWORK: "uipath-agent-framework", + AgentFramework.OPENAI_AGENTS: "uipath-openai-agents", + AgentFramework.PYDANTIC_AI: "uipath-pydantic-ai", +} + + +def installed_agent_frameworks() -> list[AgentFramework]: + """Agent frameworks whose integration package is installed in this environment.""" + installed = [] + for framework in AgentFramework: + try: + importlib.metadata.distribution(framework.package) + except importlib.metadata.PackageNotFoundError: + continue + installed.append(framework) + return installed 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..68db163cb 100644 --- a/packages/uipath/tests/cli/test_new.py +++ b/packages/uipath/tests/cli/test_new.py @@ -11,6 +11,8 @@ from uipath._cli import cli from uipath._cli.middlewares import MiddlewareResult +from uipath._cli.models.agent_frameworks import AgentFramework +from uipath._cli.models.project_types import ProjectType class TestNew: @@ -84,6 +86,297 @@ def test_new_project_middleware_interaction( assert result.exit_code == 0 assert os.path.exists("main.py") + def test_new_default_type_is_auto(self, runner: CliRunner, temp_dir: str) -> None: + """Without --type, middlewares receive project_type='auto' so an + installed agent framework can claim the scaffold; unclaimed, the + base falls back to a function project.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", return_value=[] + ), + ): + 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", + project_type=ProjectType.AUTO, + agent_framework=None, + ) + assert os.path.exists("uipath.json") + + def test_new_explicit_type_auto_claimed_by_middleware( + self, runner: CliRunner, temp_dir: str + ) -> None: + """--type auto lets a framework middleware claim the scaffold.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[AgentFramework.LLAMAINDEX], + ), + ): + 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", + project_type=ProjectType.AUTO, + agent_framework=None, + ) + # Claimed by the middleware: no base function scaffold. + assert not os.path.exists("uipath.json") + + def test_new_type_auto_multiple_installed_requires_explicit_choice( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Auto with several integrations installed must not pick one by + middleware registration order — it errors before dispatch.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[AgentFramework.LANGCHAIN, AgentFramework.LLAMAINDEX], + ), + ): + mock_middleware.return_value = MiddlewareResult(should_continue=False) + + result = runner.invoke(cli, ["new", "my_agent"]) + assert result.exit_code == 1 + assert "Multiple agent frameworks are installed" in result.output + assert "langchain, llamaindex" in result.output + assert "--type function" in result.output + assert not os.path.exists("main.py") + mock_middleware.assert_not_called() + + def test_new_explicit_type_function(self, runner: CliRunner, temp_dir: str) -> None: + """--type function scaffolds the base function project.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + result = runner.invoke(cli, ["new", "my_project", "--type", "function"]) + assert result.exit_code == 0 + with open("uipath.json") as f: + config = json.load(f) + assert config["functions"] == {"main": "main.py:main"} + + def test_new_type_agent_single_installed_but_unclaimed_errors( + self, runner: CliRunner, temp_dir: str + ) -> None: + """A resolved framework nothing claims must not fall back to a function scaffold.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[AgentFramework.LANGCHAIN], + ), + ): + # Installed, but its middleware did not claim the command + # (e.g. an outdated integration). + mock_middleware.return_value = MiddlewareResult(should_continue=True) + + result = runner.invoke(cli, ["new", "my_agent", "--type", "agent"]) + assert result.exit_code == 1 + assert ( + "'uipath-langchain' package is required to scaffold a " + "'langchain' agent" in result.output + ) + assert "pip install uipath-langchain" in result.output + assert "uv add uipath-langchain" in result.output + assert not os.path.exists("main.py") + assert not os.path.exists("uipath.json") + + def test_new_type_agent_no_integration_installed_lists_frameworks( + self, runner: CliRunner, temp_dir: str + ) -> None: + """--type agent with nothing installed lists every framework and package.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", return_value=[] + ), + ): + result = runner.invoke(cli, ["new", "my_agent", "--type", "agent"]) + assert result.exit_code == 1 + assert "No agent framework integration is installed" in result.output + for framework in AgentFramework: + assert framework.package in result.output + assert not os.path.exists("main.py") + mock_middleware.assert_not_called() + + def test_new_type_agent_uses_the_single_installed_framework( + self, runner: CliRunner, temp_dir: str + ) -> None: + """--type agent without --agent-framework picks the one installed integration.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[AgentFramework.LLAMAINDEX], + ), + ): + mock_middleware.return_value = MiddlewareResult(should_continue=False) + + result = runner.invoke(cli, ["new", "my_agent", "--type", "agent"]) + assert result.exit_code == 0 + assert "Using the installed 'llamaindex' agent framework" in ( + result.output + ) + mock_middleware.assert_called_once_with( + "new", + "my_agent", + project_type=ProjectType.AGENT, + agent_framework=AgentFramework.LLAMAINDEX, + ) + + def test_new_type_agent_multiple_installed_requires_explicit_choice( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Several integrations installed (langchain included) must be disambiguated.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[AgentFramework.LLAMAINDEX, AgentFramework.LANGCHAIN], + ), + ): + mock_middleware.return_value = MiddlewareResult(should_continue=False) + + result = runner.invoke(cli, ["new", "my_agent", "--type", "agent"]) + assert result.exit_code == 1 + assert "Multiple agent frameworks are installed" in result.output + assert "langchain, llamaindex" in result.output + mock_middleware.assert_not_called() + + def test_new_type_agent_ambiguous_installed_frameworks_error( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Several non-langchain integrations installed: ask the user to pick.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with ( + patch("uipath._cli.cli_new.Middlewares.next") as mock_middleware, + patch( + "uipath._cli.cli_new.installed_agent_frameworks", + return_value=[ + AgentFramework.LLAMAINDEX, + AgentFramework.PYDANTIC_AI, + ], + ), + ): + mock_middleware.return_value = MiddlewareResult(should_continue=True) + + result = runner.invoke(cli, ["new", "my_agent", "--type", "agent"]) + assert result.exit_code == 1 + assert "Multiple agent frameworks are installed" in result.output + assert "llamaindex, pydantic-ai" in result.output + assert not os.path.exists("main.py") + mock_middleware.assert_not_called() + + def test_new_agent_framework_forwarded_to_middlewares( + self, runner: CliRunner, temp_dir: str + ) -> None: + """An explicit --agent-framework reaches the middleware chain unchanged.""" + 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", + "--agent-framework", + "pydantic-ai", + ], + ) + assert result.exit_code == 0 + mock_middleware.assert_called_once_with( + "new", + "my_agent", + project_type=ProjectType.AGENT, + agent_framework=AgentFramework.PYDANTIC_AI, + ) + + def test_new_agent_framework_not_installed_names_package( + self, runner: CliRunner, temp_dir: str + ) -> None: + """The error for an unhandled framework names its integration package.""" + 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", + "--agent-framework", + "microsoft-agent-framework", + ], + ) + assert result.exit_code == 1 + assert ( + "'uipath-agent-framework' package is required to scaffold a " + "'microsoft-agent-framework' agent" in result.output + ) + assert "uv add uipath-agent-framework" in result.output + assert not os.path.exists("main.py") + + def test_new_agent_framework_requires_agent_type( + self, runner: CliRunner, temp_dir: str + ) -> None: + """--agent-framework without --type agent is rejected.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + for extra_args in ( + [], # implicit --type auto + ["--type", "auto"], + ["--type", "function"], + ): + result = runner.invoke( + cli, + ["new", "my_project", "--agent-framework", "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_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_invalid_agent_framework_rejected( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Unknown --agent-framework values are rejected by click.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + result = runner.invoke( + cli, + ["new", "my_agent", "--type", "agent", "--agent-framework", "crewai"], + ) + 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): @@ -139,3 +432,38 @@ def test_scaffolded_pin_contains_installed_uipath( assert pin.contains(installed, prereleases=True), ( f"scaffolded pin '{pin}' does not contain installed uipath {installed}" ) + + +class TestClaimsScaffold: + """The gate every framework integration shares.""" + + def test_auto_is_claimed_by_any_framework(self) -> None: + """`--type auto`: whichever integration is installed wins.""" + for framework in AgentFramework: + assert framework.claims_scaffold(ProjectType.AUTO, None) is True + + def test_function_is_never_claimed(self) -> None: + """Regression guard for #1543: a function scaffold stays a function.""" + for framework in AgentFramework: + assert framework.claims_scaffold(ProjectType.FUNCTION, None) is False + assert framework.claims_scaffold(ProjectType.FUNCTION, framework) is False + + def test_agent_is_claimed_only_by_the_named_framework(self) -> None: + for framework in AgentFramework: + assert framework.claims_scaffold(ProjectType.AGENT, framework) is True + others = (other for other in AgentFramework if other is not framework) + for other in others: + assert framework.claims_scaffold(ProjectType.AGENT, other) is False + + def test_agent_without_a_framework_is_not_claimed(self) -> None: + """The CLI resolves the framework first; an unset one claims nothing.""" + for framework in AgentFramework: + assert framework.claims_scaffold(ProjectType.AGENT, None) is False + + def test_plain_strings_gate_the_same_way(self) -> None: + """Older callers pass raw strings; StrEnum equality covers them.""" + langchain = AgentFramework.LANGCHAIN + assert langchain.claims_scaffold("auto", None) is True + assert langchain.claims_scaffold("agent", "langchain") is True + assert langchain.claims_scaffold("agent", "pydantic-ai") is False + assert langchain.claims_scaffold("function", None) is False diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d62b3c81c..88b7604a9 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.14" +version = "2.14.15" source = { editable = "." } dependencies = [ { name = "applicationinsights" },