Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.14.12"
version = "2.14.13"
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"
Expand Down
44 changes: 42 additions & 2 deletions packages/uipath/src/uipath/_cli/cli_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 DEFAULT_AGENT_FRAMEWORK, AgentFramework
from .models.project_types import ProjectType

console = ConsoleLogger()

Expand Down Expand Up @@ -59,8 +61,26 @@ def generate_uipath_json(target_directory):

@click.command()
@click.argument("name", type=str, default="")
@click.option(
"--type",
"project_type",
type=click.Choice([t.value for t in ProjectType]),
default=ProjectType.FUNCTION.value,
show_default=True,
help="Project type to scaffold. 'agent' requires an agent framework package (e.g. uipath-langchain).",
)
@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`; "
f"defaults to '{DEFAULT_AGENT_FRAMEWORK.value}' when `--type agent` is used."
),
)
@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()

Expand All @@ -69,7 +89,20 @@ 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 scaffold_type is ProjectType.AGENT and framework is None:
framework = DEFAULT_AGENT_FRAMEWORK

result = Middlewares.next(
"new", name, project_type=scaffold_type, agent_framework=framework
)

if result.error_message:
console.error(
Expand All @@ -82,6 +115,13 @@ 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}' agent framework is not installed.\n"
f"Install `{framework.package}` to scaffold this agent, "
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.")
Expand Down
38 changes: 38 additions & 0 deletions packages/uipath/src/uipath/_cli/models/agent_frameworks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Agent frameworks supported by `uipath new --type agent`.

Each framework's integration package registers a middleware that claims
agent scaffolds for its framework.
"""

from enum import StrEnum


class AgentFramework(StrEnum):
"""Agent frameworks with a UiPath integration package."""

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.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",
}

DEFAULT_AGENT_FRAMEWORK = AgentFramework.LANGCHAIN
15 changes: 15 additions & 0 deletions packages/uipath/src/uipath/_cli/models/project_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""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."""

FUNCTION = "function"
AGENT = "agent"
156 changes: 156 additions & 0 deletions packages/uipath/tests/cli/test_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -84,6 +86,160 @@ def test_new_project_middleware_interaction(
assert result.exit_code == 0
assert os.path.exists("main.py")

def test_new_default_type_is_function(
self, runner: CliRunner, temp_dir: str
) -> None:
"""Without --type, middlewares receive project_type='function'."""
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",
project_type=ProjectType.FUNCTION,
agent_framework=None,
)
assert os.path.exists("uipath.json")

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_without_framework_errors(
self, runner: CliRunner, temp_dir: str
) -> None:
"""--type agent with no framework handling it 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:
# No agent framework middleware claimed the command.
mock_middleware.return_value = MiddlewareResult(should_continue=True)

result = runner.invoke(cli, ["new", "my_agent", "--type", "agent"])
assert result.exit_code == 1
assert "'langchain' agent framework is not installed" in result.output
assert "uipath-langchain" in result.output
assert not os.path.exists("main.py")
assert not os.path.exists("uipath.json")

def test_new_type_agent_defaults_to_langchain_framework(
self, runner: CliRunner, temp_dir: str
) -> None:
"""--type agent without --agent-framework resolves to langchain."""
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",
project_type=ProjectType.AGENT,
agent_framework=AgentFramework.LANGCHAIN,
)

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 (
"'microsoft-agent-framework' agent framework is not installed"
in result.output
)
assert "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 function
["--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):
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/uv.lock

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

Loading