Skip to content
Draft
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
12 changes: 9 additions & 3 deletions packages/uipath-agent-framework/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-agent-framework"
version = "0.1.0"
version = "0.1.1"
description = "Python SDK that enables developers to build and deploy Microsoft Agent Framework agents to the UiPath Cloud Platform"
readme = "README.md"
requires-python = ">=3.11"
Expand All @@ -9,8 +9,10 @@ dependencies = [
"agent-framework-orchestrations>=1.0.0b260219",
"aiosqlite>=0.20.0",
"openinference-instrumentation-agent-framework>=0.1.0",
"uipath>=2.13.0, <2.14.0",
"uipath-runtime>=0.12.1, <0.13.0",
# TODO(release): restore ">=2.14.15, <2.15.0" and drop the testpypi
# source once uipath 2.14.15 (UiPath/uipath-python#1886) is on PyPI.
"uipath==2.14.15.dev1018867616",
"uipath-runtime>=0.13.1, <0.14.0",
]
classifiers = [
"Intended Audience :: Developers",
Expand Down Expand Up @@ -129,6 +131,10 @@ url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

# TODO(release): drop together with the uipath dev pin above.
[tool.uv.sources]
uipath = { index = "testpypi" }

[tool.coverage.run]
source = ["src/uipath_agent_framework"]
relative_files = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import click
from uipath._cli._utils._console import ConsoleLogger
from uipath._cli.middlewares import MiddlewareResult
from uipath._cli.models.agent_frameworks import AgentFramework
from uipath._cli.models.project_types import ProjectType

console = ConsoleLogger()

Expand Down Expand Up @@ -47,14 +49,21 @@ def generate_pyproject(target_directory, project_name):
f.write(toml_content)


def agent_framework_new_middleware(name: str) -> MiddlewareResult:
def agent_framework_new_middleware(
name: str,
project_type: ProjectType = ProjectType.AUTO,
agent_framework: AgentFramework | None = None,
) -> MiddlewareResult:
"""Middleware to create demo Agent Framework agent"""
if not AgentFramework.MICROSOFT_AGENT_FRAMEWORK.claims_scaffold(
project_type, agent_framework
):
return MiddlewareResult(should_continue=True)

directory = os.getcwd()

try:
with console.spinner(f"Creating new agent {name} in current directory ..."):
generate_pyproject(directory, name)
generate_script(directory)
console.success("Created 'main.py' file.")
console.success("Created 'agent_framework.json' file.")
Expand Down
76 changes: 75 additions & 1 deletion packages/uipath-agent-framework/tests/test_cli_new.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Tests for the `uipath new` Agent Framework scaffolding middleware."""

from uipath._cli.models.agent_frameworks import AgentFramework
from uipath._cli.models.project_types import ProjectType

from uipath_agent_framework._cli import cli_new
from uipath_agent_framework._cli.cli_new import (
agent_framework_new_middleware,
Expand All @@ -24,7 +27,11 @@ def test_middleware_scaffolds_project_files(tmp_path, monkeypatch):
assert (tmp_path / "pyproject.toml").exists()


def test_middleware_reports_error_with_stacktrace_on_failure(monkeypatch):
def test_middleware_reports_error_with_stacktrace_on_failure(tmp_path, monkeypatch):
# The middleware scaffolds into the current directory, so keep the files
# this writes before it fails out of the source tree.
monkeypatch.chdir(tmp_path)

def boom(*args, **kwargs):
raise OSError("disk full")

Expand All @@ -35,3 +42,70 @@ def boom(*args, **kwargs):

assert result.should_continue is False
assert result.should_include_stacktrace is True


class TestProjectTypeGate:
"""The middleware only claims scaffolds meant for microsoft-agent-framework."""

def test_function_type_passes_through(self, tmp_path, monkeypatch):
"""project_type='function' must defer to the base `uipath new` scaffold.

Regression guard for uipath-python#1543: installing an integration used
to hijack `uipath new` unconditionally, making the base function
scaffold unreachable.
"""
monkeypatch.chdir(tmp_path)
result = agent_framework_new_middleware(
"demo", project_type=ProjectType.FUNCTION
)
assert result.should_continue is True
assert not (tmp_path / "main.py").exists()
assert not (tmp_path / "agent_framework.json").exists()
assert not (tmp_path / "pyproject.toml").exists()

def test_auto_type_scaffolds_agent(self, tmp_path, monkeypatch):
"""--type auto (the default): an installed framework claims the scaffold."""
monkeypatch.chdir(tmp_path)
result = agent_framework_new_middleware("demo", project_type=ProjectType.AUTO)
assert result.should_continue is False
assert (tmp_path / "agent_framework.json").exists()

def test_agent_type_scaffolds_agent(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
result = agent_framework_new_middleware(
"demo",
project_type=ProjectType.AGENT,
agent_framework=AgentFramework.MICROSOFT_AGENT_FRAMEWORK,
)
assert result.should_continue is False
assert (tmp_path / "agent_framework.json").exists()

def test_other_agent_framework_passes_through(self, tmp_path, monkeypatch):
"""Another framework's scaffold must be left to its own integration."""
monkeypatch.chdir(tmp_path)
result = agent_framework_new_middleware(
"demo",
project_type=ProjectType.AGENT,
agent_framework=AgentFramework.PYDANTIC_AI,
)
assert result.should_continue is True
assert not (tmp_path / "agent_framework.json").exists()

def test_plain_string_arguments_still_gate_correctly(self, tmp_path, monkeypatch):
"""Callers passing raw strings (older CLIs) must hit the same gate."""
monkeypatch.chdir(tmp_path)
# Deliberately off-type: the point is that plain strings still gate.
result = agent_framework_new_middleware(
"demo",
project_type="agent", # type: ignore[arg-type]
agent_framework="pydantic-ai", # type: ignore[arg-type]
)
assert result.should_continue is True
assert not (tmp_path / "agent_framework.json").exists()

def test_default_type_stays_agent_for_old_base_cli(self, tmp_path, monkeypatch):
"""An older `uipath new` passes only the name; that still scaffolds."""
monkeypatch.chdir(tmp_path)
result = agent_framework_new_middleware("demo")
assert result.should_continue is False
assert (tmp_path / "agent_framework.json").exists()
33 changes: 17 additions & 16 deletions packages/uipath-agent-framework/uv.lock

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

12 changes: 9 additions & 3 deletions packages/uipath-claude-sdk/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-claude-sdk"
version = "0.1.0"
version = "0.1.1"
description = "Python SDK that enables developers to build and deploy Claude Agent SDK agents to the UiPath Cloud Platform"
readme = "README.md"
requires-python = ">=3.11"
Expand All @@ -11,8 +11,10 @@ dependencies = [
"httpx>=0.27.0",
"botocore>=1.34.0",
"aiosqlite>=0.19.0",
"uipath>=2.14.0, <2.15.0",
"uipath-runtime>=0.13.0, <0.14.0",
# TODO(release): restore ">=2.14.15, <2.15.0" and drop the testpypi
# source once uipath 2.14.15 (UiPath/uipath-python#1886) is on PyPI.
"uipath==2.14.15.dev1018867616",
"uipath-runtime>=0.13.1, <0.14.0",
"uipath-llm-client>=1.17.1, <1.18.0",
]
classifiers = [
Expand Down Expand Up @@ -118,6 +120,10 @@ url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

# TODO(release): drop together with the uipath dev pin above.
[tool.uv.sources]
uipath = { index = "testpypi" }

[tool.coverage.run]
source = ["src/uipath_claude_sdk"]
relative_files = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import click
from uipath._cli._utils._console import ConsoleLogger
from uipath._cli.middlewares import MiddlewareResult
from uipath._cli.models.agent_frameworks import AgentFramework
from uipath._cli.models.project_types import ProjectType

console = ConsoleLogger()

Expand Down Expand Up @@ -45,8 +47,14 @@ def generate_pyproject(target_directory, project_name):
f.write(toml_content)


def claude_new_middleware(name: str) -> MiddlewareResult:
def claude_new_middleware(
name: str,
project_type: ProjectType = ProjectType.AUTO,
agent_framework: AgentFramework | None = None,
) -> MiddlewareResult:
"""Middleware to create a demo Claude Agent SDK agent"""
if not AgentFramework.CLAUDE_SDK.claims_scaffold(project_type, agent_framework):
return MiddlewareResult(should_continue=True)

directory = os.getcwd()

Expand Down
67 changes: 67 additions & 0 deletions packages/uipath-claude-sdk/tests/test_cli_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import click
import pytest
from uipath._cli.models.agent_frameworks import AgentFramework
from uipath._cli.models.project_types import ProjectType

from uipath_claude_sdk._cli import cli_new
from uipath_claude_sdk._cli.cli_new import claude_new_middleware
Expand Down Expand Up @@ -80,3 +82,68 @@ def test_new_is_registered_as_the_new_middleware():
register_middleware()

assert claude_new_middleware in Middlewares._middlewares["new"]


class TestProjectTypeGate:
"""The middleware only claims scaffolds meant for claude-sdk."""

def test_function_type_passes_through(self, tmp_path, monkeypatch):
"""project_type='function' must defer to the base `uipath new` scaffold.

Regression guard for uipath-python#1543: installing an integration used
to hijack `uipath new` unconditionally, making the base function
scaffold unreachable.
"""
monkeypatch.chdir(tmp_path)
result = claude_new_middleware("demo", project_type=ProjectType.FUNCTION)
assert result.should_continue is True
assert not (tmp_path / "main.py").exists()
assert not (tmp_path / "claude.json").exists()
assert not (tmp_path / "pyproject.toml").exists()

def test_auto_type_scaffolds_agent(self, tmp_path, monkeypatch):
"""--type auto (the default): an installed framework claims the scaffold."""
monkeypatch.chdir(tmp_path)
result = claude_new_middleware("demo", project_type=ProjectType.AUTO)
assert result.should_continue is False
assert (tmp_path / "claude.json").exists()

def test_agent_type_scaffolds_agent(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
result = claude_new_middleware(
"demo",
project_type=ProjectType.AGENT,
agent_framework=AgentFramework.CLAUDE_SDK,
)
assert result.should_continue is False
assert (tmp_path / "claude.json").exists()

def test_other_agent_framework_passes_through(self, tmp_path, monkeypatch):
"""Another framework's scaffold must be left to its own integration."""
monkeypatch.chdir(tmp_path)
result = claude_new_middleware(
"demo",
project_type=ProjectType.AGENT,
agent_framework=AgentFramework.PYDANTIC_AI,
)
assert result.should_continue is True
assert not (tmp_path / "claude.json").exists()

def test_plain_string_arguments_still_gate_correctly(self, tmp_path, monkeypatch):
"""Callers passing raw strings (older CLIs) must hit the same gate."""
monkeypatch.chdir(tmp_path)
# Deliberately off-type: the point is that plain strings still gate.
result = claude_new_middleware(
"demo",
project_type="agent", # type: ignore[arg-type]
agent_framework="pydantic-ai", # type: ignore[arg-type]
)
assert result.should_continue is True
assert not (tmp_path / "claude.json").exists()

def test_default_type_stays_agent_for_old_base_cli(self, tmp_path, monkeypatch):
"""An older `uipath new` passes only the name; that still scaffolds."""
monkeypatch.chdir(tmp_path)
result = claude_new_middleware("demo")
assert result.should_continue is False
assert (tmp_path / "claude.json").exists()
Loading
Loading