diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 0b1b056fc..86bb4d18f 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.14.13" +version = "2.14.14" 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..34db5ff7d --- /dev/null +++ b/packages/uipath/src/uipath/_cli/models/agent_frameworks.py @@ -0,0 +1,51 @@ +"""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 + + +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] + + +# 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..42bbbcfc3 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): diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index adc2c346e..e4b20d6f2 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.13" +version = "2.14.14" source = { editable = "." } dependencies = [ { name = "applicationinsights" },