From 1322c7032e87a8db2c91d8d7e5723500a35d2012 Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Tue, 3 Feb 2026 17:44:26 +0200 Subject: [PATCH] feat: coded functions --- pyproject.toml | 2 +- src/uipath/_cli/cli_init.py | 2 +- src/uipath/_cli/models/uipath_json_schema.py | 7 ++++ src/uipath/functions/factory.py | 35 +++++++++++--------- src/uipath/functions/runtime.py | 20 +++++++++-- tests/functions/test_unwrap_decorated.py | 25 ++++++++++++++ uv.lock | 2 +- 7 files changed, 72 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cb4ee4b9c..2b70825a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.9.15" +version = "2.10.0" 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/src/uipath/_cli/cli_init.py b/src/uipath/_cli/cli_init.py index 04fe6288d..70c6200e0 100644 --- a/src/uipath/_cli/cli_init.py +++ b/src/uipath/_cli/cli_init.py @@ -369,7 +369,7 @@ async def initialize() -> list[UiPathRuntimeSchema]: if not entrypoints: console.warning( - 'No function entrypoints found. Add them to `uipath.json` under "functions": {"my_function": "src/main.py:main"}' + 'No entrypoints found. Add them to `uipath.json` under "functions" or "agents": {"my_function": "src/main.py:main"}' ) # Gather schemas from all discovered runtimes diff --git a/src/uipath/_cli/models/uipath_json_schema.py b/src/uipath/_cli/models/uipath_json_schema.py index bfcfa0ed2..f1cd30202 100644 --- a/src/uipath/_cli/models/uipath_json_schema.py +++ b/src/uipath/_cli/models/uipath_json_schema.py @@ -89,6 +89,12 @@ class UiPathJsonConfig(BaseModelWithDefaultConfig): "Each key is an entrypoint name, and each value is a path in format 'file_path:function_name'", ) + agents: dict[str, str] = Field( + default_factory=dict, + description="Entrypoint definitions for agent scripts. " + "Each key is an entrypoint name, and each value is a path in format 'file_path:agent_name'", + ) + def to_json_string(self, indent: int = 2) -> str: """Export to JSON string with proper formatting.""" return self.model_dump_json( @@ -110,6 +116,7 @@ def create_default(cls) -> "UiPathJsonConfig": include_uv_lock=True, ), functions={}, + agents={}, ) @classmethod diff --git a/src/uipath/functions/factory.py b/src/uipath/functions/factory.py index 12a159986..2f9888469 100644 --- a/src/uipath/functions/factory.py +++ b/src/uipath/functions/factory.py @@ -45,22 +45,18 @@ def _load_config(self) -> dict[str, Any]: return self._config def discover_entrypoints(self) -> list[str]: - """Discover all function entrypoints from uipath.json.""" + """Discover all entrypoints (functions and agents) from uipath.json.""" config = self._load_config() - return list(config.get("functions", {}).keys()) + functions = list(config.get("functions", {}).keys()) + agents = list(config.get("agents", {}).keys()) + return functions + agents async def get_storage(self) -> UiPathRuntimeStorageProtocol | None: """Get storage protocol if any (placeholder for protocol compliance).""" return None async def get_settings(self) -> UiPathRuntimeFactorySettings | None: - """Get factory settings for coded functions. - - Coded functions don't need span filtering - all spans are relevant - since developers have full control over instrumentation. - - Low-code agents (LangGraph) need filtering due to framework overhead. - """ + """Get factory settings for coded functions.""" return None async def new_runtime( @@ -77,15 +73,22 @@ def _create_runtime(self, entrypoint: str) -> UiPathRuntimeProtocol: """Create runtime instance from entrypoint specification.""" config = self._load_config() functions = config.get("functions", {}) - - if entrypoint not in functions: + agents = config.get("agents", {}) + + # Check both functions and agents + if entrypoint in functions: + func_spec = functions[entrypoint] + entrypoint_type = "function" + elif entrypoint in agents: + func_spec = agents[entrypoint] + entrypoint_type = "agent" + else: + available = list(functions.keys()) + list(agents.keys()) raise ValueError( f"Entrypoint '{entrypoint}' not found in uipath.json. " - f"Available: {', '.join(functions.keys())}" + f"Available: {', '.join(available)}" ) - func_spec = functions[entrypoint] - if ":" not in func_spec: raise ValueError( f"Invalid function specification: '{func_spec}'. " @@ -98,7 +101,9 @@ def _create_runtime(self, entrypoint: str) -> UiPathRuntimeProtocol: if not full_path.exists(): raise ValueError(f"File not found: {full_path}") - inner = UiPathFunctionsRuntime(str(full_path), function_name, entrypoint) + inner = UiPathFunctionsRuntime( + str(full_path), function_name, entrypoint, entrypoint_type + ) return UiPathDebugFunctionsRuntime( delegate=inner, entrypoint_path=str(full_path), diff --git a/src/uipath/functions/runtime.py b/src/uipath/functions/runtime.py index 56ed1fff4..700a55262 100644 --- a/src/uipath/functions/runtime.py +++ b/src/uipath/functions/runtime.py @@ -39,11 +39,25 @@ class UiPathFunctionsRuntime: """Runtime wrapper for a single Python function with full script executor compatibility.""" - def __init__(self, file_path: str, function_name: str, entrypoint_name: str): - """Initialize the function runtime.""" + def __init__( + self, + file_path: str, + function_name: str, + entrypoint_name: str, + entrypoint_type: str = "function", + ): + """Initialize the function runtime. + + Args: + file_path: Path to the Python file containing the function + function_name: Name of the function to execute + entrypoint_name: Name of the entrypoint + entrypoint_type: Type of entrypoint - 'function' or 'agent' + """ self.file_path = Path(file_path) self.function_name = function_name self.entrypoint_name = entrypoint_name + self.entrypoint_type = entrypoint_type self._function: Callable[..., Any] | None = None self._module: ModuleType | None = None @@ -240,7 +254,7 @@ async def get_schema(self) -> UiPathRuntimeSchema: return UiPathRuntimeSchema( filePath=self.entrypoint_name, uniqueId=str(uuid.uuid4()), - type="agent", + type=self.entrypoint_type, input=input_schema, output=output_schema, graph=graph, diff --git a/tests/functions/test_unwrap_decorated.py b/tests/functions/test_unwrap_decorated.py index d53bf6eb7..d7886db94 100644 --- a/tests/functions/test_unwrap_decorated.py +++ b/tests/functions/test_unwrap_decorated.py @@ -71,3 +71,28 @@ async def test_execute_unwraps_decorated_function(decorated_module): assert isinstance(result.output, dict) assert result.output["total_nodes"] == 1 assert result.output["max_value"] == 42 + + +@pytest.mark.asyncio +async def test_schema_type_defaults_to_function(decorated_module): + """Schema type should be 'function' by default.""" + runtime = UiPathFunctionsRuntime(str(decorated_module), "main", "decorated") + schema = await runtime.get_schema() + + assert schema.type == "function" + + +@pytest.mark.asyncio +async def test_schema_type_reflects_entrypoint_type(decorated_module): + """Schema type should reflect the entrypoint_type passed to the runtime.""" + runtime_fn = UiPathFunctionsRuntime( + str(decorated_module), "main", "decorated", entrypoint_type="function" + ) + schema_fn = await runtime_fn.get_schema() + assert schema_fn.type == "function" + + runtime_agent = UiPathFunctionsRuntime( + str(decorated_module), "main", "decorated", entrypoint_type="agent" + ) + schema_agent = await runtime_agent.get_schema() + assert schema_agent.type == "agent" diff --git a/uv.lock b/uv.lock index e341d3500..e01051639 100644 --- a/uv.lock +++ b/uv.lock @@ -2531,7 +2531,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.9.15" +version = "2.10.0" source = { editable = "." } dependencies = [ { name = "applicationinsights" },