From f5058fb92f9a69a7334f2d2562320f1a1eae673b Mon Sep 17 00:00:00 2001 From: Radu Mihai Gheorghe Date: Mon, 6 Apr 2026 13:59:09 +0300 Subject: [PATCH 001/121] feat: add entrypoint autodiscover (#1431) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/_errors.py | 20 ++++ packages/uipath/src/uipath/_cli/cli_eval.py | 68 ++++++------ packages/uipath/src/uipath/_cli/cli_run.py | 41 ++++++- packages/uipath/tests/cli/test_run.py | 117 ++++++++++++++++++-- packages/uipath/uv.lock | 2 +- 6 files changed, 199 insertions(+), 51 deletions(-) create mode 100644 packages/uipath/src/uipath/_cli/_errors.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 49a172d07..a96ce081a 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.40" +version = "2.10.41" 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/_errors.py b/packages/uipath/src/uipath/_cli/_errors.py new file mode 100644 index 000000000..feb7006a4 --- /dev/null +++ b/packages/uipath/src/uipath/_cli/_errors.py @@ -0,0 +1,20 @@ +class EntrypointDiscoveryException(Exception): + """Raised when entrypoint auto-discovery fails.""" + + def __init__(self, entrypoints: list[str]): + self.entrypoints = entrypoints + + def get_usage_help(self) -> list[str]: + if self.entrypoints: + lines = ["Available entrypoints:"] + for name in self.entrypoints: + lines.append(f" - {name}") + return lines + return [ + "No entrypoints found.", + "", + "To configure entrypoints, use one of the following:", + " 1. Functions project (uipath.json)", + " 2. Framework-specific project (e.g. langgraph.json, llamaindex.json, openai_agents.json)", + " 3. MCP project (mcp.json)", + ] diff --git a/packages/uipath/src/uipath/_cli/cli_eval.py b/packages/uipath/src/uipath/_cli/cli_eval.py index d0bdc730c..ef1edb200 100644 --- a/packages/uipath/src/uipath/_cli/cli_eval.py +++ b/packages/uipath/src/uipath/_cli/cli_eval.py @@ -8,6 +8,7 @@ import click +from uipath._cli._errors import EntrypointDiscoveryException from uipath._cli._evals._console_progress_reporter import ConsoleProgressReporter from uipath._cli._evals._progress_reporter import StudioWebProgressReporter from uipath._cli._evals._telemetry import EvalTelemetrySubscriber @@ -135,13 +136,35 @@ def _resolve_model_settings_override( return override if override else None -class _EvalDiscoveryError(Exception): +class _EvalDiscoveryError(EntrypointDiscoveryException): """Raised when auto-discovery of entrypoint or eval set fails.""" def __init__(self, entrypoints: list[str], eval_sets: list[Path]): - self.entrypoints = entrypoints + super().__init__(entrypoints) self.eval_sets = eval_sets + def get_usage_help(self) -> list[str]: + lines = super().get_usage_help() + + if self.eval_sets: + lines.append("") + lines.append("Available eval sets:") + for f in self.eval_sets: + lines.append(f" - {f}") + else: + lines.append("") + lines.append( + f"No eval sets found in '{EVAL_SETS_DIRECTORY_NAME}/' directory." + ) + + lines.append("") + lines.append("Usage: uipath eval ") + if self.entrypoints and self.eval_sets: + lines.append( + f"Example: uipath eval {self.entrypoints[0]} {self.eval_sets[0]}" + ) + return lines + def _discover_eval_sets() -> list[Path]: """Discover available eval set files.""" @@ -151,39 +174,6 @@ def _discover_eval_sets() -> list[Path]: return [] -def _show_eval_usage_help(entrypoints: list[str], eval_set_files: list[Path]) -> None: - """Show available entrypoints and eval sets with usage examples.""" - lines: list[str] = [] - - if entrypoints: - lines.append("Available entrypoints:") - for name in entrypoints: - lines.append(f" - {name}") - else: - lines.append( - "No entrypoints found. " - "Add a 'functions' or 'agents' section to your config file " - "(e.g. uipath.json, langgraph.json)." - ) - - if eval_set_files: - lines.append("\nAvailable eval sets:") - for f in eval_set_files: - lines.append(f" - {f}") - else: - lines.append( - f"\nNo eval sets found in '{EVAL_SETS_DIRECTORY_NAME}/' directory." - ) - - lines.append("\nUsage: uipath eval ") - if entrypoints and eval_set_files: - ep_name = entrypoints[0] - es_path = eval_set_files[0] - lines.append(f"Example: uipath eval {ep_name} {es_path}") - - click.echo("\n".join(lines)) - - @click.command() @click.argument("entrypoint", required=False) @click.argument("eval_set", required=False) @@ -475,7 +465,13 @@ async def execute_eval(): asyncio.run(execute_eval()) except _EvalDiscoveryError as e: - _show_eval_usage_help(e.entrypoints, e.eval_sets) + click.echo("\n".join(e.get_usage_help())) + if not e.entrypoints: + click.echo() + console.link( + "uipath.json spec:", + "https://github.com/UiPath/uipath-python/blob/main/packages/uipath/specs/uipath.spec.md", + ) except ValueError as e: console.error(str(e)) except Exception as e: diff --git a/packages/uipath/src/uipath/_cli/cli_run.py b/packages/uipath/src/uipath/_cli/cli_run.py index 6b5152ed4..5bfd811ed 100644 --- a/packages/uipath/src/uipath/_cli/cli_run.py +++ b/packages/uipath/src/uipath/_cli/cli_run.py @@ -27,6 +27,7 @@ LlmOpsHttpExporter, ) +from ._errors import EntrypointDiscoveryException from ._telemetry import track_command from ._utils._console import ConsoleLogger from .middlewares import Middlewares @@ -34,6 +35,21 @@ console = ConsoleLogger() +class _RunDiscoveryError(EntrypointDiscoveryException): + """Raised when entrypoint auto-discovery fails.""" + + def get_usage_help(self) -> list[str]: + lines = super().get_usage_help() + lines.append("") + lines.append( + "Usage: uipath run " + " [-f ]" + ) + if self.entrypoints: + lines.append(f"Example: uipath run {self.entrypoints[0]}") + return lines + + @click.command() @click.argument("entrypoint", required=False) @click.argument("input", required=False, default=None) @@ -125,11 +141,6 @@ def run( return if result.should_continue: - if not entrypoint: - console.error("""No entrypoint specified. Please provide the path to the Python function. - Usage: `uipath run [-f ]`""") - return - try: async def execute_runtime( @@ -187,6 +198,15 @@ async def execute() -> None: factory: UiPathRuntimeFactoryProtocol | None = None try: factory = UiPathRuntimeFactoryRegistry.get(context=ctx) + + resolved_entrypoint = entrypoint + if not resolved_entrypoint: + available = factory.discover_entrypoints() + if len(available) == 1: + resolved_entrypoint = available[0] + else: + raise _RunDiscoveryError(available) + factory_settings = await factory.get_settings() trace_settings = ( factory_settings.trace_settings @@ -194,7 +214,7 @@ async def execute() -> None: else None ) runtime = await factory.new_runtime( - entrypoint, + resolved_entrypoint, ctx.conversation_id or ctx.job_id or "default", ) @@ -230,6 +250,15 @@ async def execute() -> None: asyncio.run(execute()) + except _RunDiscoveryError as e: + click.echo("\n".join(e.get_usage_help())) + if not e.entrypoints: + click.echo() + console.link( + "uipath.json spec:", + "https://github.com/UiPath/uipath-python/blob/main/packages/uipath/specs/uipath.spec.md", + ) + return except UiPathRuntimeError as e: console.error(f"{e.error_info.title} - {e.error_info.detail}") except Exception as e: diff --git a/packages/uipath/tests/cli/test_run.py b/packages/uipath/tests/cli/test_run.py index 9069c5426..479fc9953 100644 --- a/packages/uipath/tests/cli/test_run.py +++ b/packages/uipath/tests/cli/test_run.py @@ -1,6 +1,7 @@ # type: ignore import os -from unittest.mock import patch +from contextlib import asynccontextmanager +from unittest.mock import AsyncMock, Mock, patch import pytest from click.testing import CliRunner @@ -9,6 +10,41 @@ from uipath._cli.middlewares import MiddlewareResult +def _middleware_continue(): + return MiddlewareResult( + should_continue=True, + error_message=None, + should_include_stacktrace=False, + ) + + +async def _empty_async_gen(*args, **kwargs): + """An async generator that yields nothing (simulates empty runtime.stream).""" + if False: # pragma: no cover + yield + + +def _make_mock_factory(entrypoints: list[str]): + """Create a mock runtime factory with given entrypoints.""" + mock_factory = Mock() + mock_factory.discover_entrypoints.return_value = entrypoints + mock_factory.get_settings = AsyncMock(return_value=None) + mock_factory.dispose = AsyncMock() + + mock_runtime = Mock() + mock_runtime.execute = AsyncMock(return_value=Mock(status="SUCCESSFUL")) + mock_runtime.stream = Mock(side_effect=_empty_async_gen) + mock_runtime.dispose = AsyncMock() + mock_factory.new_runtime = AsyncMock(return_value=mock_runtime) + + return mock_factory + + +@asynccontextmanager +async def _mock_resource_overwrites_context(*args, **kwargs): + yield + + @pytest.fixture def entrypoint(): return "main" @@ -142,14 +178,81 @@ def test_run_input_file_success( assert "Successful execution." in result.output class TestMiddleware: - def test_no_entrypoint(self, runner: CliRunner, temp_dir: str): + def test_autodiscover_entrypoint(self, runner: CliRunner, temp_dir: str): + """When exactly one entrypoint exists, it is auto-resolved.""" with runner.isolated_filesystem(temp_dir=temp_dir): - result = runner.invoke(cli, ["run"]) - assert result.exit_code == 1 - assert ( - "No entrypoint specified" in result.output - or "Missing argument" in result.output + mock_factory = _make_mock_factory(["my_agent"]) + + with ( + patch( + "uipath._cli.cli_run.Middlewares.next", + return_value=_middleware_continue(), + ), + patch( + "uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get", + return_value=mock_factory, + ), + patch( + "uipath._cli.cli_run.ResourceOverwritesContext", + side_effect=_mock_resource_overwrites_context, + ), + ): + result = runner.invoke(cli, ["run"]) + + assert result.exit_code == 0, ( + f"output: {result.output!r}, exception: {result.exception}" ) + assert "Successful execution." in result.output + mock_factory.new_runtime.assert_awaited_once() + assert mock_factory.new_runtime.call_args[0][0] == "my_agent" + + def test_no_entrypoint_multiple_available( + self, runner: CliRunner, temp_dir: str + ): + """When multiple entrypoints exist and none specified, show usage help.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + mock_factory = _make_mock_factory(["agent_a", "agent_b"]) + + with ( + patch( + "uipath._cli.cli_run.Middlewares.next", + return_value=_middleware_continue(), + ), + patch( + "uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get", + return_value=mock_factory, + ), + ): + result = runner.invoke(cli, ["run"]) + + assert result.exit_code == 0 + assert "Available entrypoints:" in result.output + assert "agent_a" in result.output + assert "agent_b" in result.output + assert "Usage: uipath run" in result.output + mock_factory.new_runtime.assert_not_awaited() + + def test_no_entrypoint_none_available(self, runner: CliRunner, temp_dir: str): + """When no entrypoints exist and none specified, show usage help.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + mock_factory = _make_mock_factory([]) + + with ( + patch( + "uipath._cli.cli_run.Middlewares.next", + return_value=_middleware_continue(), + ), + patch( + "uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get", + return_value=mock_factory, + ), + ): + result = runner.invoke(cli, ["run"]) + + assert result.exit_code == 0 + assert "No entrypoints found" in result.output + assert "Usage: uipath run" in result.output + mock_factory.new_runtime.assert_not_awaited() def test_script_not_found( self, runner: CliRunner, temp_dir: str, entrypoint: str diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 77434aaa8..73c7124f8 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.40" +version = "2.10.41" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 11e86d4c9dce3ae2157daccd6f4f998f0c6f74d5 Mon Sep 17 00:00:00 2001 From: UIPath-Harshit Date: Mon, 6 Apr 2026 16:54:43 +0530 Subject: [PATCH 002/121] Fix/entity as resource overwrite (#1544) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/_uipath.py | 4 +- .../src/uipath/platform/common/__init__.py | 2 + .../src/uipath/platform/common/_bindings.py | 20 +- .../platform/entities/_entities_service.py | 186 ++++++++++++++++-- .../tests/services/test_entities_service.py | 185 +++++++++++++---- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_resources/SDK_REFERENCE.md | 4 +- .../tests/resource_overrides/overwrites.json | 4 + .../test_resource_overrides.py | 5 + packages/uipath/uv.lock | 4 +- 12 files changed, 357 insertions(+), 63 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index ba5634ef1..02d5e25d6 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.18" +version = "0.1.19" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 87c3a17f0..e1d60fc39 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -139,7 +139,9 @@ def llm(self) -> UiPathLlmChatService: @property def entities(self) -> EntitiesService: - return EntitiesService(self._config, self._execution_context) + return EntitiesService( + self._config, self._execution_context, folders_service=self.folders + ) @cached_property def resource_catalog(self) -> ResourceCatalogService: diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 40fc1ac34..9070d0d70 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -7,6 +7,7 @@ from ._base_service import BaseService from ._bindings import ( ConnectionResourceOverwrite, + EntityResourceOverwrite, GenericResourceOverwrite, ResourceOverwrite, ResourceOverwriteParser, @@ -100,6 +101,7 @@ "EndpointManager", "jsonschema_to_pydantic", "ConnectionResourceOverwrite", + "EntityResourceOverwrite", "GenericResourceOverwrite", "ResourceOverwrite", "ResourceOverwriteParser", diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 449d2a7ef..1ccb2b1fc 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -45,7 +45,7 @@ def folder_identifier(self) -> str: class GenericResourceOverwrite(ResourceOverwrite): resource_type: Literal[ - "process", "index", "app", "asset", "bucket", "mcpServer", "queue", "entity" + "process", "index", "app", "asset", "bucket", "mcpServer", "queue" ] name: str = Field(alias="name") folder_path: str = Field(alias="folderPath") @@ -59,6 +59,20 @@ def folder_identifier(self) -> str: return self.folder_path +class EntityResourceOverwrite(ResourceOverwrite): + resource_type: Literal["entity"] + name: str = Field(alias="name") + folder_key: str = Field(alias="folderId") + + @property + def resource_identifier(self) -> str: + return self.name + + @property + def folder_identifier(self) -> str: + return self.folder_key + + class ConnectionResourceOverwrite(ResourceOverwrite): resource_type: Literal["connection"] # In eval context, studio web provides "ConnectionId". @@ -83,7 +97,9 @@ def folder_identifier(self) -> str: ResourceOverwriteUnion = Annotated[ - Union[GenericResourceOverwrite, ConnectionResourceOverwrite], + Union[ + GenericResourceOverwrite, EntityResourceOverwrite, ConnectionResourceOverwrite + ], Field(discriminator="resource_type"), ] diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index f30c9492e..dc08131c5 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -1,3 +1,4 @@ +import logging from typing import Any, Dict, List, Optional, Type import sqlparse @@ -7,16 +8,21 @@ from uipath.core.tracing import traced from ..common._base_service import BaseService +from ..common._bindings import EntityResourceOverwrite, _resource_overwrites from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..common._models import Endpoint, RequestSpec +from ..orchestrator._folder_service import FolderService from .entities import ( Entity, EntityRecord, EntityRecordsBatchResponse, + EntityRouting, QueryRoutingOverrideContext, ) +logger = logging.getLogger(__name__) + _FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"} _FORBIDDEN_DDL = {"DROP", "ALTER", "CREATE", "TRUNCATE"} _DISALLOWED_KEYWORDS = [ @@ -47,9 +53,32 @@ class EntitiesService(BaseService): """ def __init__( - self, config: UiPathApiConfig, execution_context: UiPathExecutionContext + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: Optional[FolderService] = None, + folders_map: Optional[Dict[str, str]] = None, ) -> None: super().__init__(config=config, execution_context=execution_context) + self._folders_service = folders_service + self._folders_map = folders_map or {} + + def with_folders_map(self, folders_map: Dict[str, str]) -> "EntitiesService": + """Return a new EntitiesService configured with the given folders map. + + The map is used to build a routing context automatically when + ``query_entity_records`` is called without an explicit routing context. + Folder paths in the map are resolved to folder keys via ``FolderService``. + + Args: + folders_map: Mapping of entity name to folder path. + """ + return EntitiesService( + config=self._config, + execution_context=self._execution_context, + folders_service=self._folders_service, + folders_map=folders_map, + ) @traced(name="entity_retrieve", run_type="uipath") def retrieve(self, entity_key: str) -> Entity: @@ -417,7 +446,6 @@ class CustomerRecord: def query_entity_records( self, sql_query: str, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Query entity records using a validated SQL query. @@ -427,9 +455,10 @@ def query_entity_records( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context - for multi-folder queries. When present, included in the request body - and takes precedence over the folder header on the backend. + + Notes: + A routing context is always derived from the configured ``folders_map`` + when present and included in the request body. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -438,15 +467,12 @@ def query_entity_records( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return self._query_entities_for_records( - sql_query, routing_context=routing_context - ) + return self._query_entities_for_records(sql_query) @traced(name="entity_query_records", run_type="uipath") async def query_entity_records_async( self, sql_query: str, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Asynchronously query entity records using a validated SQL query. @@ -456,9 +482,10 @@ async def query_entity_records_async( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context - for multi-folder queries. When present, included in the request body - and takes precedence over the folder header on the backend. + + Notes: + A routing context is always derived from the configured ``folders_map`` + when present and included in the request body. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -467,17 +494,14 @@ async def query_entity_records_async( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return await self._query_entities_for_records_async( - sql_query, routing_context=routing_context - ) + return await self._query_entities_for_records_async(sql_query) def _query_entities_for_records( self, sql_query: str, - *, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) + routing_context = self._build_routing_context_from_map() spec = self._query_entity_records_spec(sql_query, routing_context) response = self.request(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -485,10 +509,9 @@ def _query_entities_for_records( async def _query_entities_for_records_async( self, sql_query: str, - *, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) + routing_context = await self._build_routing_context_from_map_async() spec = self._query_entity_records_spec(sql_query, routing_context) response = await self.request_async(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -992,6 +1015,131 @@ def _query_entity_records_spec( json=body, ) + def _build_routing_context_from_map( + self, + ) -> Optional[QueryRoutingOverrideContext]: + """Build a routing context from the configured folders_map and context overwrites. + + Folder paths in the map are resolved to folder keys via FolderService. + Entity overwrites from the active ``ResourceOverwritesContext`` are + merged in, supplying ``override_entity_name`` when the overwrite + provides a different entity name. + + Returns: + A QueryRoutingOverrideContext if routing entries exist, + None otherwise. + """ + resolved = self._resolve_folder_paths_to_ids() + return self._build_routing_context_from_resolved_map(resolved) + + async def _build_routing_context_from_map_async( + self, + ) -> Optional[QueryRoutingOverrideContext]: + """Async version of _build_routing_context_from_map.""" + resolved = await self._resolve_folder_paths_to_ids_async() + return self._build_routing_context_from_resolved_map(resolved) + + def _resolve_folder_paths_to_ids(self) -> Optional[dict[str, str]]: + if not self._folders_map: + return None + + resolved: dict[str, str] = {} + for folder_path in set(self._folders_map.values()): + if self._folders_service is not None: + folder_key = self._folders_service.retrieve_folder_key(folder_path) + if folder_key is not None: + resolved[folder_path] = folder_key + continue + resolved[folder_path] = folder_path + + return resolved + + async def _resolve_folder_paths_to_ids_async(self) -> Optional[dict[str, str]]: + if not self._folders_map: + return None + + resolved: dict[str, str] = {} + for folder_path in set(self._folders_map.values()): + if self._folders_service is not None: + folder_key = await self._folders_service.retrieve_folder_key_async( + folder_path + ) + if folder_key is not None: + resolved[folder_path] = folder_key + continue + resolved[folder_path] = folder_path + + return resolved + + @staticmethod + def _get_entity_overwrites_from_context() -> Dict[str, EntityResourceOverwrite]: + """Extract entity overwrites from the active ResourceOverwritesContext. + + Returns: + A dict mapping original entity name to its EntityResourceOverwrite. + """ + context_overwrites = _resource_overwrites.get() + if not context_overwrites: + return {} + + result: Dict[str, EntityResourceOverwrite] = {} + for key, overwrite in context_overwrites.items(): + if isinstance(overwrite, EntityResourceOverwrite): + # Key format is "entity." + original_name = key.split(".", 1)[1] if "." in key else key + result[original_name] = overwrite + return result + + def _build_routing_context_from_resolved_map( + self, + resolved: Optional[dict[str, str]], + ) -> Optional[QueryRoutingOverrideContext]: + entity_overwrites = self._get_entity_overwrites_from_context() + + routings: List[EntityRouting] = [] + + # Add routings from folders_map + if self._folders_map and resolved is not None: + for name, folder_path in self._folders_map.items(): + overwrite = entity_overwrites.pop(name, None) + override_name = ( + overwrite.resource_identifier + if overwrite and overwrite.resource_identifier != name + else None + ) + folder_id = ( + overwrite.folder_identifier + if overwrite + else resolved.get(folder_path, folder_path) + ) + routings.append( + EntityRouting( + entity_name=name, + folder_id=folder_id, + override_entity_name=override_name, + ) + ) + + # Add routings from context overwrites not already in folders_map + for original_name, overwrite in entity_overwrites.items(): + override_name = ( + overwrite.resource_identifier + if overwrite.resource_identifier != original_name + else None + ) + routings.append( + EntityRouting( + entity_name=original_name, + folder_id=overwrite.folder_identifier, + override_entity_name=override_name, + ) + ) + + if not routings: + return None + + return QueryRoutingOverrideContext(entity_routings=routings) + def _insert_batch_spec(self, entity_key: str, records: List[Any]) -> RequestSpec: return RequestSpec( method="POST", diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 8a9abafef..4993a1367 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -8,7 +8,7 @@ from pytest_httpx import HTTPXMock from uipath.platform import UiPathApiConfig, UiPathExecutionContext -from uipath.platform.entities import Entity, EntityRouting, QueryRoutingOverrideContext +from uipath.platform.entities import Entity from uipath.platform.entities._entities_service import EntitiesService @@ -390,28 +390,21 @@ async def test_query_entity_records_async_calls_request_for_valid_sql( assert result == [{"id": "c1"}] service.request_async.assert_called_once() - def test_query_entity_records_with_routing_context( + def test_query_entity_records_builds_routing_context_from_folders_map( self, - service: EntitiesService, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={"Customers": "solution_folder", "Orders": "folder-2"}, + ) response = MagicMock() response.json.return_value = {"results": [{"id": 1}]} service.request = MagicMock(return_value=response) # type: ignore[method-assign] - routing = QueryRoutingOverrideContext( - entity_routings=[ - EntityRouting(entity_name="Customers", folder_id="folder-1"), - EntityRouting( - entity_name="Orders", - folder_id="folder-2", - override_entity_name="OrdersV2", - ), - ] - ) - - result = service.query_entity_records( - "SELECT id FROM Customers LIMIT 10", routing_context=routing - ) + result = service.query_entity_records("SELECT id FROM Customers LIMIT 10") assert result == [{"id": 1}] call_kwargs = service.request.call_args @@ -419,39 +412,38 @@ def test_query_entity_records_with_routing_context( assert body["query"] == "SELECT id FROM Customers LIMIT 10" assert body["routingContext"] == { "entityRoutings": [ - {"entityName": "Customers", "folderId": "folder-1"}, - { - "entityName": "Orders", - "folderId": "folder-2", - "overrideEntityName": "OrdersV2", - }, + {"entityName": "Customers", "folderId": "solution_folder"}, + {"entityName": "Orders", "folderId": "folder-2"}, ] } @pytest.mark.anyio - async def test_query_entity_records_async_with_routing_context( + async def test_query_entity_records_async_builds_routing_context_from_folders_map( self, - service: EntitiesService, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={"Customers": "solution_folder"}, + ) response = MagicMock() response.json.return_value = {"results": [{"id": "c1"}]} service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] - routing = QueryRoutingOverrideContext( - entity_routings=[ - EntityRouting(entity_name="Customers", folder_id="folder-1"), - ] - ) - result = await service.query_entity_records_async( - "SELECT id FROM Customers WHERE id = 'c1'", - routing_context=routing, + "SELECT id FROM Customers WHERE id = 'c1'" ) assert result == [{"id": "c1"}] call_kwargs = service.request_async.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert "routingContext" in body + assert body["routingContext"] == { + "entityRoutings": [ + {"entityName": "Customers", "folderId": "solution_folder"}, + ] + } def test_query_entity_records_without_routing_context_omits_key( self, @@ -466,3 +458,128 @@ def test_query_entity_records_without_routing_context_omits_key( call_kwargs = service.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert "routingContext" not in body + + def test_query_entity_records_picks_up_entity_overwrites_from_context( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + service = EntitiesService( + config=config, + execution_context=execution_context, + ) + response = MagicMock() + response.json.return_value = {"results": [{"id": 1}]} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_key="overwritten-folder-id", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "overwritten-folder-id", + "overrideEntityName": "Overwritten Customers", + }, + ] + } + + def test_query_entity_records_merges_folders_map_with_context_overwrites( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={"Customers": "original-folder", "Orders": "orders-folder"}, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + # Overwrite only Customers — Orders should keep its folders_map value + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_key="overwritten-folder-id", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + routings = body["routingContext"]["entityRoutings"] + # Customers overwritten by context + assert { + "entityName": "Customers", + "folderId": "overwritten-folder-id", + "overrideEntityName": "Overwritten Customers", + } in routings + # Orders unchanged from folders_map + assert {"entityName": "Orders", "folderId": "orders-folder"} in routings + + def test_query_entity_records_context_overwrite_same_name_no_override_field( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + service = EntitiesService( + config=config, + execution_context=execution_context, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + # Same entity name — only folder changes, no override_entity_name needed + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Customers", + folder_key="different-folder-id", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "different-folder-id", + }, + ] + } diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 0ea4a2f63..a9234b560 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.18" +version = "0.1.19" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index a96ce081a..58f523a02 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.41" +version = "2.10.42" 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/_resources/SDK_REFERENCE.md b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md index 4af1b60ae..5b03700be 100644 --- a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md @@ -500,10 +500,10 @@ sdk.entities.list_records(entity_key: str, schema: Optional[Type[Any]]=None, sta sdk.entities.list_records_async(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.EntityRecord] # Query entity records using a validated SQL query. -sdk.entities.query_entity_records(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] # Asynchronously query entity records using a validated SQL query. -sdk.entities.query_entity_records_async(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records_async(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] # Retrieve an entity by its key. sdk.entities.retrieve(entity_key: str) -> uipath.platform.entities.entities.Entity diff --git a/packages/uipath/tests/resource_overrides/overwrites.json b/packages/uipath/tests/resource_overrides/overwrites.json index c58744a69..e0bca84ba 100644 --- a/packages/uipath/tests/resource_overrides/overwrites.json +++ b/packages/uipath/tests/resource_overrides/overwrites.json @@ -28,5 +28,9 @@ "mcpServer.mcp_server_name": { "name": "Overwritten MCP Server Name", "folderPath": "Overwritten/MCPServer/Folder" + }, + "entity.entity_name": { + "name": "Overwritten Entity Name", + "folderId": "overwritten-entity-folder-id-123" } } \ No newline at end of file diff --git a/packages/uipath/tests/resource_overrides/test_resource_overrides.py b/packages/uipath/tests/resource_overrides/test_resource_overrides.py index c15bc113b..8d39a762d 100644 --- a/packages/uipath/tests/resource_overrides/test_resource_overrides.py +++ b/packages/uipath/tests/resource_overrides/test_resource_overrides.py @@ -310,6 +310,11 @@ def test_parse_overwrites_with_type_adapter(self, overwrites_data): assert mcp_server.resource_identifier == "Overwritten MCP Server Name" assert mcp_server.folder_identifier == "Overwritten/MCPServer/Folder" + # Verify entity overwrite + entity = parsed_overwrites["entity.entity_name"] + assert entity.resource_identifier == "Overwritten Entity Name" + assert entity.folder_identifier == "overwritten-entity-folder-id-123" + def test_overrides_decorator_should_pop_kwargs_dict_when_present(self): from uipath.platform.common import resource_override diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 73c7124f8..48783e0bb 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.41" +version = "2.10.42" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.18" +version = "0.1.19" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 1917f51456867ee33fa634083d106a8b7c699477 Mon Sep 17 00:00:00 2001 From: Andrei Petraru Date: Mon, 6 Apr 2026 17:30:51 +0300 Subject: [PATCH 003/121] feat(guardrails): add decorator framework to uipath-platform [AL-288] (#1537) Co-authored-by: Claude Sonnet 4.6 --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/guardrails/guardrails.py | 2 +- packages/uipath-core/uv.lock | 2 +- packages/uipath-platform/pyproject.toml | 2 +- .../uipath/platform/guardrails/__init__.py | 43 + .../guardrails/decorators/__init__.py | 52 + .../guardrails/decorators/_actions.py | 82 ++ .../platform/guardrails/decorators/_core.py | 302 +++++ .../platform/guardrails/decorators/_enums.py | 44 + .../guardrails/decorators/_exceptions.py | 18 + .../guardrails/decorators/_guardrail.py | 224 ++++ .../platform/guardrails/decorators/_models.py | 59 + .../guardrails/decorators/_registry.py | 105 ++ .../decorators/validators/__init__.py | 20 + .../guardrails/decorators/validators/_base.py | 182 +++ .../decorators/validators/custom.py | 125 ++ .../guardrails/decorators/validators/pii.py | 76 ++ .../decorators/validators/prompt_injection.py | 65 + .../services/test_guardrails_decorators.py | 1171 +++++++++++++++++ packages/uipath-platform/uv.lock | 4 +- .../uipath/tests/agent/models/test_agent.py | 1 + packages/uipath/uv.lock | 4 +- 22 files changed, 2577 insertions(+), 8 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/_actions.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/_core.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/_exceptions.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/_guardrail.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/_models.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/_registry.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/custom.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/pii.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/prompt_injection.py create mode 100644 packages/uipath-platform/tests/services/test_guardrails_decorators.py diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 5604e3938..1a5c34ab7 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.10" +version = "0.5.11" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/guardrails/guardrails.py b/packages/uipath-core/src/uipath/core/guardrails/guardrails.py index fe651c35b..fc7102904 100644 --- a/packages/uipath-core/src/uipath/core/guardrails/guardrails.py +++ b/packages/uipath-core/src/uipath/core/guardrails/guardrails.py @@ -227,7 +227,7 @@ class BaseGuardrail(BaseModel): name: str description: str | None = None enabled_for_evals: bool = Field(True, alias="enabledForEvals") - selector: GuardrailSelector + selector: GuardrailSelector | None = None model_config = ConfigDict(populate_by_name=True, extra="allow") diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 2544216df..e17802fc7 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.10" +version = "0.5.11" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 02d5e25d6..748075eb4 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.19" +version = "0.1.20" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py index ffab74581..de439e92a 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py @@ -14,6 +14,27 @@ ) from ._guardrails_service import GuardrailsService +from .decorators import ( + BlockAction, + BuiltInGuardrailValidator, + CustomGuardrailValidator, + CustomValidator, + GuardrailAction, + GuardrailBlockException, + GuardrailExclude, + GuardrailExecutionStage, + GuardrailTargetAdapter, + GuardrailValidatorBase, + LogAction, + LoggingSeverityLevel, + PIIDetectionEntity, + PIIDetectionEntityType, + PIIValidator, + PromptInjectionValidator, + RuleFunction, + guardrail, + register_guardrail_adapter, +) from .guardrails import ( BuiltInValidatorGuardrail, EnumListParameterValue, @@ -22,7 +43,9 @@ ) __all__ = [ + # Service "GuardrailsService", + # Guardrail models "BuiltInValidatorGuardrail", "GuardrailType", "GuardrailValidationResultType", @@ -33,4 +56,24 @@ "GuardrailValidationResult", "EnumListParameterValue", "MapEnumParameterValue", + # Decorator framework + "guardrail", + "GuardrailValidatorBase", + "BuiltInGuardrailValidator", + "CustomGuardrailValidator", + "PIIValidator", + "PromptInjectionValidator", + "CustomValidator", + "RuleFunction", + "PIIDetectionEntity", + "PIIDetectionEntityType", + "GuardrailExecutionStage", + "GuardrailAction", + "LogAction", + "BlockAction", + "LoggingSeverityLevel", + "GuardrailBlockException", + "GuardrailExclude", + "GuardrailTargetAdapter", + "register_guardrail_adapter", ] diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py new file mode 100644 index 000000000..727925fb1 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py @@ -0,0 +1,52 @@ +"""Guardrail decorator framework for UiPath Platform. + +Provides the ``@guardrail`` decorator, built-in validators, actions, and an +adapter registry that framework integrations (e.g. *uipath-langchain*) use to +teach the decorator how to wrap their specific object types. +""" + +from ._actions import BlockAction, LogAction, LoggingSeverityLevel +from ._core import GuardrailExclude +from ._enums import GuardrailExecutionStage, PIIDetectionEntityType +from ._exceptions import GuardrailBlockException +from ._guardrail import guardrail +from ._models import GuardrailAction, PIIDetectionEntity +from ._registry import GuardrailTargetAdapter, register_guardrail_adapter +from .validators import ( + BuiltInGuardrailValidator, + CustomGuardrailValidator, + CustomValidator, + GuardrailValidatorBase, + PIIValidator, + PromptInjectionValidator, + RuleFunction, +) + +__all__ = [ + # Decorator + "guardrail", + # Validators + "GuardrailValidatorBase", + "BuiltInGuardrailValidator", + "CustomGuardrailValidator", + "PIIValidator", + "PromptInjectionValidator", + "CustomValidator", + "RuleFunction", + # Models & enums + "PIIDetectionEntity", + "PIIDetectionEntityType", + "GuardrailExecutionStage", + "GuardrailAction", + # Actions + "LogAction", + "BlockAction", + "LoggingSeverityLevel", + # Exception + "GuardrailBlockException", + # Exclude marker + "GuardrailExclude", + # Adapter registry + "GuardrailTargetAdapter", + "register_guardrail_adapter", +] diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_actions.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_actions.py new file mode 100644 index 000000000..8e6489797 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_actions.py @@ -0,0 +1,82 @@ +"""Built-in GuardrailAction implementations.""" + +import logging +from dataclasses import dataclass +from enum import Enum +from typing import Any, Optional + +from uipath.core.guardrails import ( + GuardrailValidationResult, + GuardrailValidationResultType, +) + +from ._exceptions import GuardrailBlockException +from ._models import GuardrailAction + + +class LoggingSeverityLevel(int, Enum): + """Logging severity level for :class:`LogAction`.""" + + ERROR = logging.ERROR + INFO = logging.INFO + WARNING = logging.WARNING + DEBUG = logging.DEBUG + + +@dataclass +class LogAction(GuardrailAction): + """Log guardrail violations without stopping execution. + + Args: + severity_level: Python logging level. Defaults to ``WARNING``. + message: Custom log message. If omitted, the validation reason is used. + """ + + severity_level: LoggingSeverityLevel = LoggingSeverityLevel.WARNING + message: Optional[str] = None + + def handle_validation_result( + self, + result: GuardrailValidationResult, + data: str | dict[str, Any], + guardrail_name: str, + ) -> str | dict[str, Any] | None: + """Log the violation and return ``None`` (no data modification).""" + if result.result == GuardrailValidationResultType.VALIDATION_FAILED: + msg = self.message or f"Failed: {result.reason}" + logging.getLogger(__name__).log( + self.severity_level, + "[GUARDRAIL] [%s] %s", + guardrail_name, + msg, + ) + return None + + +@dataclass +class BlockAction(GuardrailAction): + """Block execution by raising :class:`GuardrailBlockException`. + + Framework adapters catch ``GuardrailBlockException`` at the wrapper boundary + and convert it to their own runtime error type. + + Args: + title: Exception title. Defaults to a message derived from the guardrail name. + detail: Exception detail. Defaults to the validation reason. + """ + + title: Optional[str] = None + detail: Optional[str] = None + + def handle_validation_result( + self, + result: GuardrailValidationResult, + data: str | dict[str, Any], + guardrail_name: str, + ) -> str | dict[str, Any] | None: + """Raise :class:`GuardrailBlockException` when validation fails.""" + if result.result == GuardrailValidationResultType.VALIDATION_FAILED: + title = self.title or f"Guardrail [{guardrail_name}] blocked execution" + detail = self.detail or result.reason or "Guardrail validation failed" + raise GuardrailBlockException(title=title, detail=detail) + return None diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_core.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_core.py new file mode 100644 index 000000000..ca168a1e0 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_core.py @@ -0,0 +1,302 @@ +"""Core framework-agnostic utilities for guardrail decorators.""" + +import ast +import dataclasses +import inspect +import json +import logging +from typing import Annotated, Any, Callable, get_args, get_origin, get_type_hints + +from uipath.core.guardrails import ( + GuardrailValidationResult, +) + +from ._enums import GuardrailExecutionStage + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# GuardrailExclude marker +# --------------------------------------------------------------------------- + + +class GuardrailExclude: + """Marker to exclude a parameter from guardrail input serialization. + + Use with :data:`typing.Annotated` to prevent a specific function parameter + from being collected into the guardrail evaluation payload:: + + async def process( + text: str, + config: Annotated[dict, GuardrailExclude()], + ) -> str: ... + """ + + +# --------------------------------------------------------------------------- +# Evaluator type alias +# --------------------------------------------------------------------------- + +_EvaluatorFn = Callable[ + [ + "str | dict[str, Any]", # data + GuardrailExecutionStage, # stage + "dict[str, Any] | None", # input_data + "dict[str, Any] | None", # output_data + ], + GuardrailValidationResult, +] +"""Type alias for the unified evaluation callable used by all wrappers.""" + + +# --------------------------------------------------------------------------- +# Evaluator factory +# --------------------------------------------------------------------------- + + +def _make_evaluator( + validator: Any, + name: str, + description: str | None, + enabled_for_evals: bool, +) -> _EvaluatorFn: + """Return a unified evaluation callable. + + Delegates to ``validator.run()`` which each validator subclass implements + (:class:`BuiltInGuardrailValidator` hits the UiPath API; + :class:`CustomGuardrailValidator` runs a local Python rule). + + Args: + validator: :class:`GuardrailValidatorBase` instance. + name: Guardrail name — forwarded to ``validator.run()`` on each call. + description: Optional description — forwarded to ``validator.run()``. + enabled_for_evals: Whether active in evaluation scenarios. + + Returns: + Callable with signature ``(data, stage, input_data, output_data)``. + """ + + def _eval( + data: str | dict[str, Any], + stage: GuardrailExecutionStage, + input_data: dict[str, Any] | None, + output_data: dict[str, Any] | None, + ) -> GuardrailValidationResult: + return validator.run( + name, description, enabled_for_evals, data, stage, input_data, output_data + ) + + return _eval + + +# --------------------------------------------------------------------------- +# Parameter introspection +# --------------------------------------------------------------------------- + + +def _get_excluded_params(func: Any) -> set[str]: + """Return parameter names annotated with :class:`GuardrailExclude`. + + Args: + func: Callable to inspect. + + Returns: + Set of parameter names that should be excluded from guardrail input. + """ + try: + hints = get_type_hints(func, include_extras=True) + except Exception: + return set() + excluded: set[str] = set() + for name, hint in hints.items(): + if get_origin(hint) is Annotated: + for meta in get_args(hint)[1:]: + if isinstance(meta, GuardrailExclude): + excluded.add(name) + return excluded + + +# --------------------------------------------------------------------------- +# Serialization helpers +# --------------------------------------------------------------------------- + + +def _serialize_value(value: Any) -> Any: + """Serialize *value* to a JSON-compatible type for guardrail evaluation. + + Pydantic models → ``model_dump()``, dataclasses → ``asdict()``, + primitives → as-is, everything else → ``str()``. + """ + if value is None: + return None + if isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, dict): + return {k: _serialize_value(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [_serialize_value(v) for v in value] + # Pydantic v2 + if hasattr(value, "model_dump"): + return value.model_dump() + # Pydantic v1 + if hasattr(value, "dict") and callable(value.dict): + try: + return value.dict() + except Exception: + pass + # dataclasses + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return dataclasses.asdict(value) + return str(value) + + +def _collect_input( + bound: inspect.BoundArguments, + excluded: set[str], +) -> dict[str, Any]: + """Collect non-excluded function parameters into a guardrail input dict. + + Args: + bound: Bound arguments from ``inspect.Signature.bind()``. + excluded: Parameter names to skip. + + Returns: + ``{param_name: serialized_value}`` for all non-excluded parameters. + """ + result: dict[str, Any] = {} + for name, value in bound.arguments.items(): + if name in excluded or name in ("self", "cls"): + continue + result[name] = _serialize_value(value) + return result + + +def _collect_output(return_value: Any) -> dict[str, Any]: + """Serialize a function return value into a dict for guardrail evaluation. + + Args: + return_value: The value returned by the wrapped function. + + Returns: + A ``dict`` representation suitable for guardrail evaluation. + """ + serialized = _serialize_value(return_value) + if isinstance(serialized, dict): + return serialized + return {"return": serialized} + + +def _reconstruct_output(original: Any, modified: Any) -> Any: + """Reconstruct a return value from a guardrail-modified payload. + + Args: + original: The original return value (used to determine target type). + modified: The modified value returned by the guardrail action. + + Returns: + Reconstructed value of the same type as *original* where possible. + """ + if modified is None: + return original + # Pydantic v2 model + dict modification → reconstruct via model_validate + if hasattr(original, "model_validate") and isinstance(modified, dict): + try: + return type(original).model_validate(modified) + except Exception: + pass + # Pydantic v1 + if hasattr(original, "parse_obj") and isinstance(modified, dict): + try: + return type(original).parse_obj(modified) + except Exception: + pass + return modified + + +def _apply_pre_modification( + bound: inspect.BoundArguments, + modified: Any, + excluded: set[str], +) -> None: + """Apply guardrail PRE-stage modifications back to bound function arguments. + + If the action returned a modified dict, keys matching non-excluded parameters + are updated in-place. If the action returned a plain string and there is exactly + one non-excluded parameter, that parameter is updated. + + Args: + bound: Bound arguments to mutate in-place. + modified: Value returned by the guardrail action. + excluded: Parameter names that were excluded from evaluation. + """ + if modified is None: + return + non_excluded = [ + n for n in bound.arguments if n not in excluded and n not in ("self", "cls") + ] + if isinstance(modified, dict): + for name in non_excluded: + if name in modified: + bound.arguments[name] = modified[name] + elif isinstance(modified, str) and len(non_excluded) == 1: + bound.arguments[non_excluded[0]] = modified + + +# --------------------------------------------------------------------------- +# Tool I/O normalisation helpers (used by LangChain adapter) +# --------------------------------------------------------------------------- + + +def _is_tool_call_envelope(tool_input: Any) -> bool: + """Return ``True`` if *tool_input* is a LangGraph tool-call envelope dict.""" + return ( + isinstance(tool_input, dict) + and "args" in tool_input + and tool_input.get("type") == "tool_call" + ) + + +def _extract_input(tool_input: Any) -> dict[str, Any]: + """Normalise tool input to a plain dict for rule / guardrail evaluation. + + LangGraph wraps tool inputs as ``{"name": ..., "args": {...}, "type": "tool_call"}``. + This function unwraps ``args`` so rules can access the actual tool arguments. + """ + if _is_tool_call_envelope(tool_input): + args = tool_input["args"] + if isinstance(args, dict): + return args + if isinstance(tool_input, dict): + return tool_input + return {"input": tool_input} + + +def _rewrap_input(original_tool_input: Any, modified_args: dict[str, Any]) -> Any: + """Re-wrap modified args back into the original tool-call envelope (if applicable).""" + if _is_tool_call_envelope(original_tool_input): + import copy + + wrapped = copy.copy(original_tool_input) + wrapped["args"] = modified_args + return wrapped + return modified_args + + +def _extract_output(result: Any) -> dict[str, Any]: + """Normalise tool output to a dict for guardrail / rule evaluation. + + Falls back to ``{"output": content}`` for plain strings and anything else. + """ + if isinstance(result, dict): + return result + if isinstance(result, str): + try: + parsed = json.loads(result) + return parsed if isinstance(parsed, dict) else {"output": parsed} + except ValueError: + try: + parsed = ast.literal_eval(result) + return parsed if isinstance(parsed, dict) else {"output": parsed} + except (ValueError, SyntaxError): + return {"output": result} + return {"output": result} diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py new file mode 100644 index 000000000..be7832ddf --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py @@ -0,0 +1,44 @@ +"""Enums for guardrail decorators.""" + +from enum import Enum + + +class GuardrailExecutionStage(str, Enum): + """Execution stage for guardrails.""" + + PRE = "pre" + """Evaluate before the target executes.""" + + POST = "post" + """Evaluate after the target executes.""" + + PRE_AND_POST = "pre&post" + """Evaluate both before and after the target executes.""" + + +class PIIDetectionEntityType(str, Enum): + """PII detection entity types supported by UiPath guardrails. + + These entities match the available options from the UiPath guardrails service + backend. The enum values correspond to the exact strings expected by the API. + """ + + PERSON = "Person" + ADDRESS = "Address" + DATE = "Date" + PHONE_NUMBER = "PhoneNumber" + EUGPS_COORDINATES = "EugpsCoordinates" + EMAIL = "Email" + CREDIT_CARD_NUMBER = "CreditCardNumber" + INTERNATIONAL_BANKING_ACCOUNT_NUMBER = "InternationalBankingAccountNumber" + SWIFT_CODE = "SwiftCode" + ABA_ROUTING_NUMBER = "ABARoutingNumber" + US_DRIVERS_LICENSE_NUMBER = "USDriversLicenseNumber" + UK_DRIVERS_LICENSE_NUMBER = "UKDriversLicenseNumber" + US_INDIVIDUAL_TAXPAYER_IDENTIFICATION = "USIndividualTaxpayerIdentification" + UK_UNIQUE_TAXPAYER_NUMBER = "UKUniqueTaxpayerNumber" + US_BANK_ACCOUNT_NUMBER = "USBankAccountNumber" + US_SOCIAL_SECURITY_NUMBER = "USSocialSecurityNumber" + USUK_PASSPORT_NUMBER = "UsukPassportNumber" + URL = "URL" + IP_ADDRESS = "IPAddress" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_exceptions.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_exceptions.py new file mode 100644 index 000000000..f4b7672e5 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_exceptions.py @@ -0,0 +1,18 @@ +"""Exceptions for guardrail decorators.""" + + +class GuardrailBlockException(Exception): + """Raised by BlockAction when a guardrail blocks execution. + + Framework adapters (e.g. LangChain) should catch this and convert it to + their own runtime exception type at the outermost wrapper boundary. + + Args: + title: Brief title for the block event. + detail: Detailed reason for the block. + """ + + def __init__(self, title: str, detail: str) -> None: + self.title = title + self.detail = detail + super().__init__(f"{title}: {detail}") diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_guardrail.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_guardrail.py new file mode 100644 index 000000000..d61b41c0d --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_guardrail.py @@ -0,0 +1,224 @@ +"""Single ``@guardrail`` decorator for all guardrail types.""" + +import inspect +import logging +from functools import wraps +from typing import Any + +from ._core import ( + _apply_pre_modification, + _collect_input, + _collect_output, + _EvaluatorFn, + _get_excluded_params, + _make_evaluator, + _reconstruct_output, +) +from ._enums import GuardrailExecutionStage +from ._models import GuardrailAction +from ._registry import is_recognized_by_adapter, wrap_with_adapter +from .validators._base import GuardrailValidatorBase + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _run_pre( + evaluator: _EvaluatorFn, + action: GuardrailAction, + name: str, + bound: inspect.BoundArguments, + excluded: set[str], +) -> None: + """Evaluate PRE guardrail and apply any modifications to *bound* in-place.""" + input_data = _collect_input(bound, excluded) + try: + result = evaluator(input_data, GuardrailExecutionStage.PRE, input_data, None) + except Exception as exc: + logger.error("Error evaluating PRE guardrail %r: %s", name, exc, exc_info=True) + return + from uipath.core.guardrails import GuardrailValidationResultType + + if result.result == GuardrailValidationResultType.VALIDATION_FAILED: + modified = action.handle_validation_result(result, input_data, name) + _apply_pre_modification(bound, modified, excluded) + + +def _run_post( + evaluator: _EvaluatorFn, + action: GuardrailAction, + name: str, + bound: inspect.BoundArguments, + excluded: set[str], + return_value: Any, +) -> Any: + """Evaluate POST guardrail and return (possibly modified) return value.""" + input_data = _collect_input(bound, excluded) + output_data = _collect_output(return_value) + try: + result = evaluator( + output_data, GuardrailExecutionStage.POST, input_data, output_data + ) + except Exception as exc: + logger.error("Error evaluating POST guardrail %r: %s", name, exc, exc_info=True) + return return_value + from uipath.core.guardrails import GuardrailValidationResultType + + if result.result == GuardrailValidationResultType.VALIDATION_FAILED: + modified = action.handle_validation_result(result, output_data, name) + return _reconstruct_output(return_value, modified) + return return_value + + +def _wrap_function( + func: Any, + evaluator: _EvaluatorFn, + action: GuardrailAction, + name: str, + stage: GuardrailExecutionStage, + excluded: set[str], +) -> Any: + """Wrap *func* as a pure Python function with PRE/POST guardrail evaluation.""" + sig = inspect.signature(func) + + def _dispatch_return(return_value: Any) -> Any: + """For factory functions: if the return value is recognized by an adapter, wrap it.""" + if is_recognized_by_adapter(return_value): + return wrap_with_adapter(return_value, evaluator, action, name, stage) + return return_value + + if inspect.iscoroutinefunction(func): + + @wraps(func) + async def _wrapped_async(*args: Any, **kwargs: Any) -> Any: + bound = sig.bind(*args, **kwargs) + bound.apply_defaults() + if stage in ( + GuardrailExecutionStage.PRE, + GuardrailExecutionStage.PRE_AND_POST, + ): + _run_pre(evaluator, action, name, bound, excluded) + return_value = await func(*bound.args, **bound.kwargs) + return_value = _dispatch_return(return_value) + if stage in ( + GuardrailExecutionStage.POST, + GuardrailExecutionStage.PRE_AND_POST, + ): + # Only run POST on plain (non-adapter-wrapped) values + if not is_recognized_by_adapter(return_value): + return_value = _run_post( + evaluator, action, name, bound, excluded, return_value + ) + return return_value + + return _wrapped_async + + @wraps(func) + def _wrapped(*args: Any, **kwargs: Any) -> Any: + bound = sig.bind(*args, **kwargs) + bound.apply_defaults() + if stage in ( + GuardrailExecutionStage.PRE, + GuardrailExecutionStage.PRE_AND_POST, + ): + _run_pre(evaluator, action, name, bound, excluded) + return_value = func(*bound.args, **bound.kwargs) + return_value = _dispatch_return(return_value) + if stage in ( + GuardrailExecutionStage.POST, + GuardrailExecutionStage.PRE_AND_POST, + ): + if not is_recognized_by_adapter(return_value): + return_value = _run_post( + evaluator, action, name, bound, excluded, return_value + ) + return return_value + + return _wrapped + + +# --------------------------------------------------------------------------- +# Public @guardrail decorator +# --------------------------------------------------------------------------- + + +def guardrail( + func: Any = None, + *, + validator: GuardrailValidatorBase, + action: GuardrailAction, + name: str = "Guardrail", + description: str | None = None, + stage: GuardrailExecutionStage = GuardrailExecutionStage.PRE_AND_POST, + enabled_for_evals: bool = True, +) -> Any: + """Apply a guardrail to any callable — tool functions, LLM factories, agent nodes. + + When applied to a plain function or async function, the decorator collects + function parameters (PRE) and return value (POST) and evaluates them against + the guardrail. Use :class:`~._core.GuardrailExclude` to opt individual + parameters out of serialization. + + When applied to a factory function whose return value is recognised by a + registered framework adapter (e.g. a LangChain ``BaseChatModel``), the + returned object is wrapped so every subsequent ``invoke()`` call is guarded. + + Multiple ``@guardrail`` decorators can be stacked on the same callable. + + Args: + func: Callable to decorate. Supplied directly when used without parentheses. + validator: :class:`~.validators.GuardrailValidatorBase` defining what to check. + action: :class:`~._models.GuardrailAction` defining how to respond on violation. + name: Human-readable name for this guardrail instance. + description: Optional description passed to API-based guardrails. + stage: When to evaluate — ``PRE``, ``POST``, or ``PRE_AND_POST``. + Defaults to ``PRE_AND_POST``. + enabled_for_evals: Whether this guardrail is active in evaluation scenarios. + Defaults to ``True``. + + Returns: + The decorated callable (or framework object). + + Raises: + ValueError: If *action* is invalid, or the validator does not support + the requested stage. + GuardrailBlockException: Raised at runtime by :class:`~._actions.BlockAction` + when a violation is detected. + """ + if action is None: + raise ValueError("action must be provided") + if not isinstance(action, GuardrailAction): + raise ValueError("action must be an instance of GuardrailAction") + if not isinstance(enabled_for_evals, bool): + raise ValueError("enabled_for_evals must be a boolean") + + def _apply(obj: Any) -> Any: + # ------------------------------------------------------------------ + # 1. Adapter-recognised direct object (e.g. BaseTool after @tool) + # ------------------------------------------------------------------ + if is_recognized_by_adapter(obj): + validator.validate_stage(stage) + evaluator = _make_evaluator(validator, name, description, enabled_for_evals) + return wrap_with_adapter(obj, evaluator, action, name, stage) + + # ------------------------------------------------------------------ + # 2. Plain callable — wrap as pure function + # ------------------------------------------------------------------ + if callable(obj): + validator.validate_stage(stage) + evaluator = _make_evaluator(validator, name, description, enabled_for_evals) + excluded = _get_excluded_params(obj) + return _wrap_function(obj, evaluator, action, name, stage, excluded) + + raise ValueError( + f"@guardrail cannot be applied to {type(obj)!r}. " + "Target must be a callable or a framework-registered object. " + "Ensure the relevant framework adapter is imported before using @guardrail." + ) + + if func is None: + return _apply + return _apply(func) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_models.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_models.py new file mode 100644 index 000000000..ac22538e0 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_models.py @@ -0,0 +1,59 @@ +"""Models for guardrail decorators.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any + +from uipath.core.guardrails import GuardrailValidationResult + + +@dataclass +class PIIDetectionEntity: + """PII entity configuration with detection threshold. + + Args: + name: The entity type name (e.g. ``PIIDetectionEntityType.EMAIL``). + threshold: Confidence threshold (0.0 to 1.0) for detection. + """ + + name: str + threshold: float = 0.5 + + def __post_init__(self) -> None: + if not 0.0 <= self.threshold <= 1.0: + raise ValueError( + f"Threshold must be between 0.0 and 1.0, got {self.threshold}" + ) + + +class GuardrailAction(ABC): + """Interface for defining custom actions when a guardrail violation is detected. + + Subclass this to implement custom behaviour on validation failure, such as + logging, blocking, or content sanitisation. Built-in implementations are + :class:`LogAction` and :class:`BlockAction`. + """ + + @abstractmethod + def handle_validation_result( + self, + result: GuardrailValidationResult, + data: str | dict[str, Any], + guardrail_name: str, + ) -> "str | dict[str, Any] | None": + """Handle a guardrail validation result. + + Called when guardrail validation fails. May return modified data to + sanitise/filter the validated content before execution continues, or + ``None`` to leave it unchanged. + + Args: + result: The validation result from the guardrails service. + data: The data that was validated (string or dictionary). Depending + on context this can be tool input, tool output, or message text. + guardrail_name: The name of the guardrail that triggered. + + Returns: + Modified data if the action wants to replace the original, or + ``None`` if no modification is needed. + """ diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_registry.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_registry.py new file mode 100644 index 000000000..c4b7773b5 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_registry.py @@ -0,0 +1,105 @@ +"""Adapter registry for guardrail target recognition and wrapping.""" + +from typing import Any, Protocol, runtime_checkable + +from ._core import _EvaluatorFn +from ._enums import GuardrailExecutionStage +from ._models import GuardrailAction + + +@runtime_checkable +class GuardrailTargetAdapter(Protocol): + """Protocol for framework-specific guardrail adapters. + + Implement this protocol to teach :func:`guardrail` how to handle objects + from a particular framework. Register instances via + :func:`register_guardrail_adapter`. + """ + + def recognize(self, target: Any) -> bool: + """Return ``True`` if this adapter handles *target*. + + Args: + target: Object being decorated or returned by a factory function. + + Returns: + ``True`` if this adapter can wrap *target*, ``False`` otherwise. + """ + ... + + def wrap( + self, + target: Any, + evaluator: _EvaluatorFn, + action: GuardrailAction, + name: str, + stage: GuardrailExecutionStage, + ) -> Any: + """Wrap *target* with guardrail enforcement logic. + + Args: + target: Object to wrap. + evaluator: Unified evaluation callable from :func:`_make_evaluator`. + action: Action to invoke on validation failure. + name: Human-readable guardrail name. + stage: When to evaluate (PRE, POST, or PRE_AND_POST). + + Returns: + Wrapped object, same type or duck-type compatible. + """ + ... + + +# Module-level registry. Later-registered adapters take priority (inserted at 0). +_adapters: list[GuardrailTargetAdapter] = [] + + +def register_guardrail_adapter(adapter: GuardrailTargetAdapter) -> None: + """Register a framework adapter for the ``@guardrail`` decorator. + + Later-registered adapters are tried first. + + Args: + adapter: An instance implementing :class:`GuardrailTargetAdapter`. + """ + _adapters.insert(0, adapter) + + +def is_recognized_by_adapter(target: Any) -> bool: + """Return ``True`` if any registered adapter recognizes *target*. + + Args: + target: The object being decorated. + + Returns: + ``True`` if a registered adapter handles *target*. + """ + for adapter in _adapters: + if adapter.recognize(target): + return True + return False + + +def wrap_with_adapter( + target: Any, + evaluator: _EvaluatorFn, + action: GuardrailAction, + name: str, + stage: GuardrailExecutionStage, +) -> Any: + """Ask the first matching adapter to wrap *target*. + + Args: + target: The object to wrap. + evaluator: Unified evaluation callable. + action: Action on violation. + name: Guardrail name. + stage: Execution stage. + + Returns: + Wrapped object, or *target* unchanged if no adapter handles it. + """ + for adapter in _adapters: + if adapter.recognize(target): + return adapter.wrap(target, evaluator, action, name, stage) + return target diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py new file mode 100644 index 000000000..6be170534 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py @@ -0,0 +1,20 @@ +"""Guardrail validators for the ``@guardrail`` decorator.""" + +from ._base import ( + BuiltInGuardrailValidator, + CustomGuardrailValidator, + GuardrailValidatorBase, +) +from .custom import CustomValidator, RuleFunction +from .pii import PIIValidator +from .prompt_injection import PromptInjectionValidator + +__all__ = [ + "GuardrailValidatorBase", + "BuiltInGuardrailValidator", + "CustomGuardrailValidator", + "PIIValidator", + "PromptInjectionValidator", + "CustomValidator", + "RuleFunction", +] diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py new file mode 100644 index 000000000..a9eaf5afd --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py @@ -0,0 +1,182 @@ +"""Abstract base classes for guardrail validators.""" + +from abc import ABC, abstractmethod +from typing import Any, ClassVar + +from uipath.core.guardrails import GuardrailValidationResult + +from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail + +from .._enums import GuardrailExecutionStage + + +class GuardrailValidatorBase: + """Root base class for guardrail validators. + + Concrete validators should subclass either + :class:`BuiltInGuardrailValidator` (for UiPath API-backed validation) + or :class:`CustomGuardrailValidator` (for in-process Python validation). + """ + + supported_stages: ClassVar[list[GuardrailExecutionStage]] = [] + """Stages this validator supports. Empty list means all stages are allowed.""" + + def validate_stage(self, stage: GuardrailExecutionStage) -> None: + """Raise ``ValueError`` if *stage* is not in :attr:`supported_stages`. + + Args: + stage: Requested execution stage. + + Raises: + ValueError: If :attr:`supported_stages` is non-empty and *stage* is absent. + """ + if self.supported_stages and stage not in self.supported_stages: + raise ValueError( + f"{type(self).__name__} does not support stage {stage!r}. " + f"Supported stages: {[s.value for s in self.supported_stages]}" + ) + + def run( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + data: "str | dict[str, Any]", + stage: GuardrailExecutionStage, + input_data: "dict[str, Any] | None", + output_data: "dict[str, Any] | None", + ) -> GuardrailValidationResult: + """Execute the guardrail evaluation. + + Called by the ``@guardrail`` decorator at each function invocation. + Subclasses override this via :class:`BuiltInGuardrailValidator` or + :class:`CustomGuardrailValidator`. + + Raises: + NotImplementedError: Always — subclass one of the two ABCs instead. + """ + raise NotImplementedError( + f"{type(self).__name__} must subclass BuiltInGuardrailValidator " + "or CustomGuardrailValidator and implement the required abstract method." + ) + + +class BuiltInGuardrailValidator(GuardrailValidatorBase, ABC): + """Base for validators that delegate to the UiPath Guardrails API. + + Subclass this and implement :meth:`get_built_in_guardrail` to create an + API-backed guardrail validator (e.g. PII detection, prompt injection). + + Example:: + + class MyValidator(BuiltInGuardrailValidator): + def get_built_in_guardrail(self, name, description, enabled_for_evals): + return BuiltInValidatorGuardrail( + id=str(uuid4()), + name=name, + ... + ) + """ + + @abstractmethod + def get_built_in_guardrail( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + ) -> BuiltInValidatorGuardrail: + """Build the UiPath API guardrail definition for this validator. + + Args: + name: Name for the guardrail instance. + description: Optional description. + enabled_for_evals: Whether active in evaluation scenarios. + + Returns: + :class:`BuiltInValidatorGuardrail` ready to be sent to the API. + """ + ... + + def run( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + data: "str | dict[str, Any]", + stage: GuardrailExecutionStage, + input_data: "dict[str, Any] | None", + output_data: "dict[str, Any] | None", + ) -> GuardrailValidationResult: + """Evaluate via the UiPath Guardrails API. + + Lazily initialises the ``UiPath`` client on the first call and reuses + it for all subsequent invocations. + """ + built_in = self.get_built_in_guardrail(name, description, enabled_for_evals) + if not hasattr(self, "_uipath"): + from uipath.platform import UiPath + + self._uipath: Any = UiPath() + return self._uipath.guardrails.evaluate_guardrail(data, built_in) + + +class CustomGuardrailValidator(GuardrailValidatorBase, ABC): + """Base for validators that run entirely in-process. + + Subclass this and implement :meth:`evaluate` to create a local guardrail + validator that requires no UiPath API call. + + Example:: + + class ProfanityValidator(CustomGuardrailValidator): + BANNED = {"badword"} + + def evaluate(self, data, stage, input_data, output_data): + text = (input_data or {}).get("message", "") + if any(w in text.lower() for w in self.BANNED): + return GuardrailValidationResult( + result=GuardrailValidationResultType.VALIDATION_FAILED, + reason="Profanity detected", + ) + return GuardrailValidationResult(result=GuardrailValidationResultType.PASSED) + """ + + @abstractmethod + def evaluate( + self, + data: "str | dict[str, Any]", + stage: GuardrailExecutionStage, + input_data: "dict[str, Any] | None", + output_data: "dict[str, Any] | None", + ) -> GuardrailValidationResult: + """Perform local validation without a UiPath API call. + + Return a result with ``VALIDATION_FAILED`` to **trigger** the guardrail + (causing the configured :class:`~uipath.platform.guardrails.decorators.GuardrailAction` + to fire), or ``PASSED`` to let execution continue unchanged. + + Args: + data: Primary data being evaluated. + stage: Current execution stage (PRE or POST). + input_data: Normalised function input dict, or ``None``. + output_data: Normalised function output dict, or ``None`` at PRE stage. + + Returns: + :class:`~uipath.core.guardrails.GuardrailValidationResult` — + return ``VALIDATION_FAILED`` to activate the guardrail, + ``PASSED`` to allow execution to continue. + """ + ... + + def run( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + data: "str | dict[str, Any]", + stage: GuardrailExecutionStage, + input_data: "dict[str, Any] | None", + output_data: "dict[str, Any] | None", + ) -> GuardrailValidationResult: + """Delegate to :meth:`evaluate`.""" + return self.evaluate(data, stage, input_data, output_data) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/custom.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/custom.py new file mode 100644 index 000000000..df6549600 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/custom.py @@ -0,0 +1,125 @@ +"""Custom (rule-based) guardrail validator.""" + +import inspect +from typing import Any, Callable + +from uipath.core.guardrails import ( + GuardrailValidationResult, + GuardrailValidationResultType, +) + +from .._enums import GuardrailExecutionStage +from ._base import CustomGuardrailValidator + +RuleFunction = ( + Callable[[dict[str, Any]], bool] | Callable[[dict[str, Any], dict[str, Any]], bool] +) +"""Type alias for custom rule functions passed to :class:`CustomValidator`. + +The rule must return ``True`` to **trigger** the guardrail (i.e. signal a +violation that causes the configured action to fire), or ``False`` to let +execution continue unchanged. + +It accepts either one parameter (the input or output dict) or two parameters +(input dict, output dict — POST stage only). + +Examples:: + + # Triggered when "donkey" appears in the joke argument + CustomValidator(lambda args: "donkey" in args.get("joke", "").lower()) + + # Triggered when the output joke exceeds 500 characters + CustomValidator(lambda args: len(args.get("joke", "")) > 500) + + # Two-parameter form: triggered at POST when output contains input keyword + CustomValidator(lambda inp, out: inp.get("topic", "") in out.get("joke", "")) +""" + + +class CustomValidator(CustomGuardrailValidator): + """Validate function input/output using a local Python rule function. + + No UiPath API call is made. Applicable at any stage. + + The *rule* is called with the collected parameter dict (PRE stage) or the + serialised return-value dict (POST stage). It must return ``True`` to + **activate** the guardrail — i.e. to signal a violation and invoke the + configured :class:`~uipath.platform.guardrails.decorators.GuardrailAction`. + Return ``False`` (or any falsy value) to let execution continue unchanged. + + Args: + rule: A :data:`RuleFunction` that returns ``True`` to trigger the + guardrail. Must accept 1 or 2 parameters. + + Raises: + ValueError: If *rule* is not callable or has an unsupported parameter count. + """ + + def __init__(self, rule: RuleFunction) -> None: + """Initialize CustomValidator with a rule callable.""" + if not callable(rule): + raise ValueError(f"rule must be callable, got {type(rule)}") + sig = inspect.signature(rule) + param_count = len(sig.parameters) + if param_count not in (1, 2): + raise ValueError(f"rule must have 1 or 2 parameters, got {param_count}") + self.rule = rule + self._param_count = param_count + + def evaluate( + self, + data: str | dict[str, Any], + stage: GuardrailExecutionStage, + input_data: dict[str, Any] | None, + output_data: dict[str, Any] | None, + ) -> GuardrailValidationResult: + """Run the rule against the collected input or output dict. + + The rule receives the PRE parameter dict or POST return-value dict and + must return ``True`` to **trigger** the guardrail (VALIDATION_FAILED), + or ``False`` to pass. + + Args: + data: Unused; the rule operates on *input_data* or *output_data*. + stage: Current stage (PRE or POST). + input_data: Collected function input dict. + output_data: Collected function output dict, or ``None`` at PRE stage. + + Returns: + :class:`~uipath.core.guardrails.GuardrailValidationResult` — + ``VALIDATION_FAILED`` when the rule returns ``True`` (guardrail + triggered), ``PASSED`` otherwise. + """ + try: + if self._param_count == 2: + if input_data is None or output_data is None: + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="Two-parameter rule skipped: input or output data unavailable", + ) + violation = self.rule(input_data, output_data) # type: ignore[call-arg] + else: + target = ( + input_data if stage == GuardrailExecutionStage.PRE else output_data + ) + if target is None: + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="Rule skipped: data unavailable at this stage", + ) + violation = self.rule(target) # type: ignore[call-arg] + except Exception as exc: + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason=f"Rule raised exception: {exc}", + ) + + if violation: + return GuardrailValidationResult( + result=GuardrailValidationResultType.VALIDATION_FAILED, + reason="Rule detected violation", + ) + return GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="Rule passed", + ) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/pii.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/pii.py new file mode 100644 index 000000000..64d0a47aa --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/pii.py @@ -0,0 +1,76 @@ +"""PII detection guardrail validator.""" + +from typing import Any, Sequence +from uuid import uuid4 + +from uipath.platform.guardrails.guardrails import ( + BuiltInValidatorGuardrail, + EnumListParameterValue, + MapEnumParameterValue, +) + +from .._models import PIIDetectionEntity +from ._base import BuiltInGuardrailValidator + + +class PIIValidator(BuiltInGuardrailValidator): + """Validate data for PII entities using the UiPath PII detection API. + + Supported at all stages. + + Args: + entities: One or more :class:`~uipath.platform.guardrails.decorators.PIIDetectionEntity` + instances specifying which PII types to detect and their confidence thresholds. + + Raises: + ValueError: If *entities* is empty. + """ + + def __init__(self, entities: Sequence[PIIDetectionEntity]) -> None: + """Initialize PIIValidator with a list of entities to detect.""" + if not entities: + raise ValueError("entities must be provided and non-empty") + self.entities = list(entities) + + def get_built_in_guardrail( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + ) -> BuiltInValidatorGuardrail: + """Build a PII detection :class:`BuiltInValidatorGuardrail`. + + Args: + name: Name for the guardrail. + description: Optional description. + enabled_for_evals: Whether active in evaluation scenarios. + + Returns: + Configured :class:`BuiltInValidatorGuardrail` for PII detection. + """ + entity_names = [entity.name for entity in self.entities] + entity_thresholds: dict[str, Any] = { + entity.name: entity.threshold for entity in self.entities + } + + return BuiltInValidatorGuardrail( + id=str(uuid4()), + name=name, + description=description + or f"Detects PII entities: {', '.join(entity_names)}", + enabled_for_evals=enabled_for_evals, + guardrail_type="builtInValidator", + validator_type="pii_detection", + validator_parameters=[ + EnumListParameterValue( + parameter_type="enum-list", + id="entities", + value=entity_names, + ), + MapEnumParameterValue( + parameter_type="map-enum", + id="entityThresholds", + value=entity_thresholds, + ), + ], + ) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/prompt_injection.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/prompt_injection.py new file mode 100644 index 000000000..b0943b396 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/prompt_injection.py @@ -0,0 +1,65 @@ +"""Prompt injection detection guardrail validator.""" + +from uuid import uuid4 + +from uipath.platform.guardrails.guardrails import ( + BuiltInValidatorGuardrail, + NumberParameterValue, +) + +from .._enums import GuardrailExecutionStage +from ._base import BuiltInGuardrailValidator + + +class PromptInjectionValidator(BuiltInGuardrailValidator): + """Validate input for prompt injection attacks via the UiPath API. + + Restricted to PRE stage only — prompt injection is an input-only concern. + + Args: + threshold: Detection confidence threshold (0.0–1.0). Defaults to ``0.5``. + + Raises: + ValueError: If *threshold* is outside [0.0, 1.0]. + """ + + supported_stages = [GuardrailExecutionStage.PRE] + + def __init__(self, threshold: float = 0.5) -> None: + """Initialize PromptInjectionValidator with a detection threshold.""" + if not 0.0 <= threshold <= 1.0: + raise ValueError(f"threshold must be between 0.0 and 1.0, got {threshold}") + self.threshold = threshold + + def get_built_in_guardrail( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + ) -> BuiltInValidatorGuardrail: + """Build a prompt injection :class:`BuiltInValidatorGuardrail`. + + Args: + name: Name for the guardrail. + description: Optional description. + enabled_for_evals: Whether active in evaluation scenarios. + + Returns: + Configured :class:`BuiltInValidatorGuardrail` for prompt injection. + """ + return BuiltInValidatorGuardrail( + id=str(uuid4()), + name=name, + description=description + or f"Detects prompt injection with threshold {self.threshold}", + enabled_for_evals=enabled_for_evals, + guardrail_type="builtInValidator", + validator_type="prompt_injection", + validator_parameters=[ + NumberParameterValue( + parameter_type="number", + id="threshold", + value=self.threshold, + ), + ], + ) diff --git a/packages/uipath-platform/tests/services/test_guardrails_decorators.py b/packages/uipath-platform/tests/services/test_guardrails_decorators.py new file mode 100644 index 000000000..e578cba84 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_guardrails_decorators.py @@ -0,0 +1,1171 @@ +"""Tests for the guardrails decorator framework in uipath-platform. + +Focus: meaningful business behaviour — serialization, PRE/POST evaluation, +modification flow, stage enforcement, factory-function path, and integration +scenarios modelled on the joke-agent-decorator sample. +""" + +from __future__ import annotations + +import dataclasses +import logging +from typing import Annotated, Any +from unittest.mock import MagicMock, patch + +import pytest +from pydantic import BaseModel +from uipath.core.guardrails import ( + GuardrailValidationResult, + GuardrailValidationResultType, +) + +from uipath.platform.guardrails.decorators import ( + BlockAction, + CustomValidator, + GuardrailAction, + GuardrailBlockException, + GuardrailExclude, + GuardrailExecutionStage, + LogAction, + LoggingSeverityLevel, + PIIDetectionEntity, + PIIDetectionEntityType, + PIIValidator, + PromptInjectionValidator, + guardrail, + register_guardrail_adapter, +) +from uipath.platform.guardrails.decorators._core import ( + _collect_output, + _get_excluded_params, + _make_evaluator, + _reconstruct_output, + _serialize_value, +) +from uipath.platform.guardrails.decorators._registry import ( + _adapters, +) + +# --------------------------------------------------------------------------- +# Shared result constants +# --------------------------------------------------------------------------- + +_PASSED = GuardrailValidationResult( + result=GuardrailValidationResultType.PASSED, + reason="ok", +) +_FAILED = GuardrailValidationResult( + result=GuardrailValidationResultType.VALIDATION_FAILED, + reason="violation detected", +) + + +# --------------------------------------------------------------------------- +# Registry isolation fixture +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def _isolate_adapter_registry(): + """Snapshot and restore the global adapter registry around every test.""" + snapshot = list(_adapters) + yield + _adapters.clear() + _adapters.extend(snapshot) + + +# --------------------------------------------------------------------------- +# Minimal fake types for adapter tests (no LangChain dependency) +# --------------------------------------------------------------------------- + + +class _DummyTarget: + """Minimal callable target recognised by _DummyAdapter.""" + + def __init__(self, return_value: Any = None) -> None: + self.return_value = ( + return_value if return_value is not None else {"output": "result"} + ) + self.invoke_calls: list[Any] = [] + + def invoke(self, args: Any) -> Any: + self.invoke_calls.append(args) + return self.return_value + + +class _WrappedDummyTarget: + """A _DummyTarget wrapped with guardrail evaluation.""" + + def __init__( + self, + target: Any, + evaluator: Any, + action: GuardrailAction, + name: str, + stage: GuardrailExecutionStage, + ) -> None: + self._target = target + self._evaluator = evaluator + self._action = action + self._name = name + self._stage = stage + + def invoke(self, args: Any) -> Any: + input_data = args if isinstance(args, dict) else {"input": args} + if self._stage in ( + GuardrailExecutionStage.PRE, + GuardrailExecutionStage.PRE_AND_POST, + ): + result = self._evaluator( + input_data, GuardrailExecutionStage.PRE, input_data, None + ) + if result.result == GuardrailValidationResultType.VALIDATION_FAILED: + self._action.handle_validation_result(result, input_data, self._name) + raw = self._target.invoke(args) + output_data = raw if isinstance(raw, dict) else {"output": raw} + if self._stage in ( + GuardrailExecutionStage.POST, + GuardrailExecutionStage.PRE_AND_POST, + ): + result = self._evaluator( + output_data, GuardrailExecutionStage.POST, input_data, output_data + ) + if result.result == GuardrailValidationResultType.VALIDATION_FAILED: + self._action.handle_validation_result(result, output_data, self._name) + return raw + + +class _DummyAdapter: + """Adapter that handles _DummyTarget and _WrappedDummyTarget instances.""" + + def recognize(self, target: Any) -> bool: + return isinstance(target, (_DummyTarget, _WrappedDummyTarget)) + + def wrap( + self, + target: Any, + evaluator: Any, + action: GuardrailAction, + name: str, + stage: GuardrailExecutionStage, + ) -> Any: + return _WrappedDummyTarget(target, evaluator, action, name, stage) + + +# --------------------------------------------------------------------------- +# 1. PIIDetectionEntity — threshold boundary enforcement +# --------------------------------------------------------------------------- + + +class TestPIIDetectionEntity: + def test_threshold_below_zero_raises(self): + with pytest.raises(ValueError, match="0.0 and 1.0"): + PIIDetectionEntity(name="Email", threshold=-0.1) + + def test_threshold_above_one_raises(self): + with pytest.raises(ValueError, match="0.0 and 1.0"): + PIIDetectionEntity(name="Email", threshold=1.1) + + +# --------------------------------------------------------------------------- +# 2. LogAction — does NOT stop execution; uses configured severity +# --------------------------------------------------------------------------- + + +class TestLogAction: + def test_violation_logs_guardrail_name_and_execution_continues(self, caplog): + action = LogAction() + with caplog.at_level(logging.WARNING): + result = action.handle_validation_result(_FAILED, "data", "MyGuardrail") + assert result is None # execution continues + assert any("MyGuardrail" in r.message for r in caplog.records) + + def test_pass_emits_no_log(self, caplog): + action = LogAction() + with caplog.at_level(logging.WARNING): + action.handle_validation_result(_PASSED, "data", "G") + assert not caplog.records + + def test_custom_message_overrides_reason(self, caplog): + action = LogAction(message="custom alert") + with caplog.at_level(logging.WARNING): + action.handle_validation_result(_FAILED, "data", "G") + assert any("custom alert" in r.message for r in caplog.records) + + def test_default_message_includes_validation_reason(self, caplog): + action = LogAction() + with caplog.at_level(logging.WARNING): + action.handle_validation_result(_FAILED, "data", "G") + assert any("violation detected" in r.message for r in caplog.records) + + def test_debug_severity(self, caplog): + action = LogAction(severity_level=LoggingSeverityLevel.DEBUG) + with caplog.at_level(logging.DEBUG): + action.handle_validation_result(_FAILED, "data", "G") + debug_records = [r for r in caplog.records if r.levelno == logging.DEBUG] + assert debug_records + + +# --------------------------------------------------------------------------- +# 3. BlockAction — raises GuardrailBlockException on violation +# --------------------------------------------------------------------------- + + +class TestBlockAction: + def test_raises_on_violation(self): + action = BlockAction() + with pytest.raises(GuardrailBlockException): + action.handle_validation_result(_FAILED, "data", "G") + + def test_no_raise_on_pass(self): + action = BlockAction() + result = action.handle_validation_result(_PASSED, "data", "G") + assert result is None + + def test_title_and_detail_from_result(self): + action = BlockAction() + with pytest.raises(GuardrailBlockException) as exc_info: + action.handle_validation_result(_FAILED, "data", "MyGuardrail") + assert exc_info.value.title + assert exc_info.value.detail + + def test_custom_title_and_detail(self): + action = BlockAction(title="Blocked", detail="Not allowed") + with pytest.raises(GuardrailBlockException) as exc_info: + action.handle_validation_result(_FAILED, "data", "G") + assert exc_info.value.title == "Blocked" + assert exc_info.value.detail == "Not allowed" + + +# --------------------------------------------------------------------------- +# 4. PIIValidator — builds correct BuiltInValidatorGuardrail +# --------------------------------------------------------------------------- + + +class TestPIIValidator: + def test_empty_entities_raises(self): + with pytest.raises(ValueError, match="non-empty"): + PIIValidator(entities=[]) + + def test_entity_names_and_thresholds_in_api_parameters(self): + v = PIIValidator( + entities=[ + PIIDetectionEntity(PIIDetectionEntityType.EMAIL, 0.6), + PIIDetectionEntity(PIIDetectionEntityType.PERSON, 0.8), + ] + ) + g = v.get_built_in_guardrail("G", None, True) + param_by_id = {p.id: p for p in g.validator_parameters} + entities_value = param_by_id["entities"].value + assert isinstance(entities_value, list) + assert "Email" in entities_value + assert "Person" in entities_value + thresholds_value = param_by_id["entityThresholds"].value + assert isinstance(thresholds_value, dict) + assert thresholds_value["Email"] == 0.6 + assert thresholds_value["Person"] == 0.8 + + def test_no_scope_restriction(self): + v = PIIValidator(entities=[PIIDetectionEntity(PIIDetectionEntityType.EMAIL)]) + # All stages allowed — no ValueError raised + v.validate_stage(GuardrailExecutionStage.PRE) + v.validate_stage(GuardrailExecutionStage.POST) + + def test_selector_is_none(self): + v = PIIValidator(entities=[PIIDetectionEntity(PIIDetectionEntityType.EMAIL)]) + g = v.get_built_in_guardrail("G", None, True) + assert g.selector is None + + +# --------------------------------------------------------------------------- +# 5. PromptInjectionValidator — LLM-only, PRE-only, threshold validation +# --------------------------------------------------------------------------- + + +class TestPromptInjectionValidator: + def test_threshold_below_zero_raises(self): + with pytest.raises(ValueError, match="threshold"): + PromptInjectionValidator(threshold=-0.1) + + def test_threshold_above_one_raises(self): + with pytest.raises(ValueError, match="threshold"): + PromptInjectionValidator(threshold=1.1) + + def test_restricted_to_pre_stage_only(self): + v = PromptInjectionValidator() + v.validate_stage(GuardrailExecutionStage.PRE) # ok + with pytest.raises(ValueError): + v.validate_stage(GuardrailExecutionStage.POST) + + def test_builds_prompt_injection_guardrail_with_threshold(self): + v = PromptInjectionValidator(threshold=0.7) + g = v.get_built_in_guardrail("PI", None, True) + assert g.validator_type == "prompt_injection" + threshold_param = next(p for p in g.validator_parameters if p.id == "threshold") + assert threshold_param.value == 0.7 + + def test_selector_is_none(self): + v = PromptInjectionValidator() + g = v.get_built_in_guardrail("PI", None, True) + assert g.selector is None + + +# --------------------------------------------------------------------------- +# 6. CustomValidator — rule routing and error handling +# --------------------------------------------------------------------------- + + +class TestCustomValidator: + def test_non_callable_raises(self): + with pytest.raises(ValueError, match="callable"): + CustomValidator(rule="not_a_function") # type: ignore[arg-type] + + def test_wrong_arity_raises(self): + with pytest.raises(ValueError, match="1 or 2"): + CustomValidator(rule=lambda: True) # type: ignore[arg-type] + with pytest.raises(ValueError, match="1 or 2"): + CustomValidator(rule=lambda a, b, c: True) # type: ignore[arg-type] + + def test_one_param_pre_receives_input_data(self): + received: list[Any] = [] + + def capture_pre(args: dict[str, Any]) -> bool: + received.append(args) + return False + + CustomValidator(rule=capture_pre).evaluate( + {}, GuardrailExecutionStage.PRE, {"a": 1}, None + ) + assert received == [{"a": 1}] + + def test_one_param_post_receives_output_data(self): + received: list[Any] = [] + + def capture_post(args: dict[str, Any]) -> bool: + received.append(args) + return False + + CustomValidator(rule=capture_post).evaluate( + {}, GuardrailExecutionStage.POST, {"in": 1}, {"out": 2} + ) + assert received == [{"out": 2}] + + def test_two_param_post_receives_input_and_output(self): + received: list[Any] = [] + + def rule(inp: dict[str, Any], out: dict[str, Any]) -> bool: + received.append((inp, out)) + return False + + CustomValidator(rule=rule).evaluate( + {}, GuardrailExecutionStage.POST, {"in": 1}, {"out": 2} + ) + assert received == [({"in": 1}, {"out": 2})] + + def test_two_param_rule_skipped_when_input_missing(self): + result = CustomValidator(rule=lambda a, b: True).evaluate( + {}, GuardrailExecutionStage.POST, None, {"out": 2} + ) + assert result.result == GuardrailValidationResultType.PASSED + + def test_rule_returning_true_means_violation(self): + result = CustomValidator(rule=lambda args: True).evaluate( + {}, GuardrailExecutionStage.PRE, {"x": 1}, None + ) + assert result.result == GuardrailValidationResultType.VALIDATION_FAILED + + def test_rule_exception_returns_passed(self): + def bad(args: dict[str, Any]) -> bool: + raise ValueError("boom") + + result = CustomValidator(rule=bad).evaluate( + {}, GuardrailExecutionStage.PRE, {"x": 1}, None + ) + assert result.result == GuardrailValidationResultType.PASSED + + +# --------------------------------------------------------------------------- +# 7. GuardrailExclude — parameter introspection +# --------------------------------------------------------------------------- + + +class TestGuardrailExclude: + def test_excluded_param_not_in_collected_input(self): + def func( + text: str, + config: Annotated[dict[str, Any], GuardrailExclude()], + ) -> str: + return text + + excluded = _get_excluded_params(func) + assert "config" in excluded + assert "text" not in excluded + + def test_multiple_excluded_params(self): + def func( + a: str, + b: Annotated[int, GuardrailExclude()], + c: Annotated[str, GuardrailExclude()], + ) -> str: + return a + + excluded = _get_excluded_params(func) + assert excluded == {"b", "c"} + + def test_no_annotations_returns_empty_set(self): + def func(a: str, b: int) -> str: + return a + + assert _get_excluded_params(func) == set() + + +# --------------------------------------------------------------------------- +# 8. Serialization helpers +# --------------------------------------------------------------------------- + + +class _PydanticModel(BaseModel): + topic: str + count: int = 0 + + +@dataclasses.dataclass +class _Dataclass: + name: str + value: float + + +class TestSerializationHelpers: + def test_primitive_str_passthrough(self): + assert _serialize_value("hello") == "hello" + + def test_primitive_int_passthrough(self): + assert _serialize_value(42) == 42 + + def test_dict_passthrough(self): + assert _serialize_value({"a": 1}) == {"a": 1} + + def test_pydantic_model_dumps(self): + m = _PydanticModel(topic="test", count=3) + result = _serialize_value(m) + assert result == {"topic": "test", "count": 3} + + def test_dataclass_asdict(self): + d = _Dataclass(name="x", value=1.5) + result = _serialize_value(d) + assert result == {"name": "x", "value": 1.5} + + def test_collect_output_from_pydantic(self): + m = _PydanticModel(topic="joke") + result = _collect_output(m) + assert result == {"topic": "joke", "count": 0} + + def test_collect_output_from_str(self): + result = _collect_output("hello") + assert result == {"return": "hello"} + + def test_collect_output_from_dict(self): + result = _collect_output({"key": "val"}) + assert result == {"key": "val"} + + def test_reconstruct_output_pydantic(self): + original = _PydanticModel(topic="original") + modified = {"topic": "modified", "count": 5} + result = _reconstruct_output(original, modified) + assert isinstance(result, _PydanticModel) + assert result.topic == "modified" + assert result.count == 5 + + def test_reconstruct_output_str(self): + result = _reconstruct_output("original", "modified") + assert result == "modified" + + def test_reconstruct_output_none_returns_original(self): + result = _reconstruct_output("original", None) + assert result == "original" + + +# --------------------------------------------------------------------------- +# 9. @guardrail on sync functions +# --------------------------------------------------------------------------- + + +class TestGuardrailOnSyncFunction: + def test_pre_fires_before_function(self): + calls: list[str] = [] + + mock_validator = MagicMock() + mock_validator.supported_stages = [] + mock_validator.validate_stage = MagicMock() + + mock_validator.run.side_effect = lambda *a, **kw: ( + calls.append("eval") or _PASSED # type: ignore[func-returns-value] + ) + + @guardrail( + validator=mock_validator, + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def fn(text: str) -> str: + calls.append("fn") + return text + + fn("hello") + assert calls == ["eval", "fn"] + + def test_post_fires_after_function(self): + calls: list[str] = [] + + mock_validator = MagicMock() + mock_validator.supported_stages = [] + mock_validator.validate_stage = MagicMock() + + mock_validator.run.side_effect = lambda *a, **kw: ( + calls.append("eval") or _PASSED # type: ignore[func-returns-value] + ) + + @guardrail( + validator=mock_validator, + action=LogAction(), + stage=GuardrailExecutionStage.POST, + ) + def fn(text: str) -> str: + calls.append("fn") + return text + + fn("hello") + assert calls == ["fn", "eval"] + + def test_block_action_raises_guardrail_block_exception(self): + mock_validator = MagicMock() + mock_validator.supported_stages = [] + mock_validator.validate_stage = MagicMock() + + mock_validator.run.return_value = _FAILED + + @guardrail( + validator=mock_validator, + action=BlockAction(title="Blocked", detail="not allowed"), + stage=GuardrailExecutionStage.PRE, + ) + def fn(text: str) -> str: + return text + + with pytest.raises(GuardrailBlockException): + fn("bad input") + + def test_log_action_does_not_stop_execution(self): + mock_validator = MagicMock() + mock_validator.supported_stages = [] + mock_validator.validate_stage = MagicMock() + + mock_validator.run.return_value = _FAILED + + result = [] + + @guardrail( + validator=mock_validator, + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def fn(text: str) -> str: + result.append("called") + return text + + fn("input") + assert result == ["called"] + + def test_pre_input_contains_function_params(self): + captured: list[Any] = [] + + @guardrail( + validator=CustomValidator( + lambda args: captured.append(args) or False # type: ignore[func-returns-value] + ), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def fn(joke: str, count: int) -> str: + return joke + + fn("why did the chicken", 3) + assert captured == [{"joke": "why did the chicken", "count": 3}] + + def test_excluded_param_absent_from_pre_input(self): + captured: list[Any] = [] + + @guardrail( + validator=CustomValidator( + lambda args: captured.append(args) or False # type: ignore[func-returns-value] + ), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def fn( + joke: str, + config: Annotated[dict[str, Any], GuardrailExclude()], + ) -> str: + return joke + + fn("why did the chicken", {"debug": True}) + assert "config" not in captured[0] + assert "joke" in captured[0] + + def test_pre_modification_updates_function_args(self): + class _ReplaceAction(GuardrailAction): + def handle_validation_result(self, result, data, name): + if isinstance(data, dict) and "joke" in data: + return {"joke": data["joke"].replace("donkey", "[censored]")} + return data + + @guardrail( + validator=CustomValidator(lambda args: "donkey" in args.get("joke", "")), + action=_ReplaceAction(), + stage=GuardrailExecutionStage.PRE, + ) + def fn(joke: str) -> str: + return joke + + result = fn("why did the donkey cross the road") + assert result == "why did the [censored] cross the road" + + def test_post_output_contains_return_value(self): + captured: list[Any] = [] + + @guardrail( + validator=CustomValidator( + lambda args: captured.append(args) or False # type: ignore[func-returns-value] + ), + action=LogAction(), + stage=GuardrailExecutionStage.POST, + ) + def fn(x: int) -> dict[str, int]: + return {"result": x * 2} + + fn(5) + assert captured == [{"result": 10}] + + def test_post_modification_updates_return_value(self): + class _FixedAction(GuardrailAction): + def handle_validation_result(self, result, data, name): + return {"result": 99} + + @guardrail( + validator=CustomValidator(lambda args: True), + action=_FixedAction(), + stage=GuardrailExecutionStage.POST, + ) + def fn(x: int) -> dict[str, int]: + return {"result": x * 2} + + assert fn(5) == {"result": 99} + + def test_pre_and_post_both_fire(self): + calls: list[str] = [] + + mock_validator = MagicMock() + mock_validator.supported_stages = [] + mock_validator.validate_stage = MagicMock() + + mock_validator.run.side_effect = lambda name, desc, enabled, data, stage, *a: ( + calls.append(stage.value) or _PASSED # type: ignore[func-returns-value] + ) + + @guardrail( + validator=mock_validator, + action=LogAction(), + stage=GuardrailExecutionStage.PRE_AND_POST, + ) + def fn(x: int) -> int: + return x + 1 + + fn(1) + assert "pre" in calls + assert "post" in calls + + +# --------------------------------------------------------------------------- +# 10. @guardrail on async functions +# --------------------------------------------------------------------------- + + +class TestGuardrailOnAsyncFunction: + @pytest.mark.asyncio + async def test_pre_fires_before_async_function(self): + calls: list[str] = [] + + mock_validator = MagicMock() + mock_validator.supported_stages = [] + mock_validator.validate_stage = MagicMock() + + mock_validator.run.side_effect = lambda *a, **kw: ( + calls.append("eval") or _PASSED # type: ignore[func-returns-value] + ) + + @guardrail( + validator=mock_validator, + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + async def fn(text: str) -> str: + calls.append("fn") + return text + + await fn("hello") + assert calls == ["eval", "fn"] + + @pytest.mark.asyncio + async def test_block_action_raises_in_async(self): + mock_validator = MagicMock() + mock_validator.supported_stages = [] + mock_validator.validate_stage = MagicMock() + + mock_validator.run.return_value = _FAILED + + @guardrail( + validator=mock_validator, + action=BlockAction(), + stage=GuardrailExecutionStage.PRE, + ) + async def fn(text: str) -> str: + return text + + with pytest.raises(GuardrailBlockException): + await fn("bad") + + @pytest.mark.asyncio + async def test_post_modification_in_async(self): + class _FortyTwoAction(GuardrailAction): + def handle_validation_result(self, result, data, name): + return {"result": 42} + + @guardrail( + validator=CustomValidator(lambda args: True), + action=_FortyTwoAction(), + stage=GuardrailExecutionStage.POST, + ) + async def fn(x: int) -> dict[str, int]: + return {"result": x} + + assert await fn(1) == {"result": 42} + + +# --------------------------------------------------------------------------- +# 11. Stage enforcement at decoration time +# --------------------------------------------------------------------------- + + +class TestStageEnforcement: + def test_prompt_injection_on_post_raises_at_decoration(self): + with pytest.raises(ValueError, match="stage"): + guardrail( + lambda text: text, + validator=PromptInjectionValidator(), + action=LogAction(), + stage=GuardrailExecutionStage.POST, + ) + + def test_prompt_injection_on_pre_and_post_raises_at_decoration(self): + with pytest.raises(ValueError, match="stage"): + guardrail( + lambda text: text, + validator=PromptInjectionValidator(), + action=LogAction(), + stage=GuardrailExecutionStage.PRE_AND_POST, + ) + + def test_prompt_injection_on_pre_ok(self): + # Should not raise + guardrail( + lambda text: text, + validator=PromptInjectionValidator(), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + + +# --------------------------------------------------------------------------- +# 12. @guardrail validation (bad arguments) +# --------------------------------------------------------------------------- + + +class TestGuardrailDecorator: + def test_missing_action_raises(self): + with pytest.raises(ValueError, match="action must be provided"): + guardrail( + lambda text: text, + validator=CustomValidator(lambda args: False), + action=None, # type: ignore[arg-type] + ) + + def test_non_action_instance_raises(self): + with pytest.raises(ValueError, match="GuardrailAction"): + guardrail( + lambda text: text, + validator=CustomValidator(lambda args: False), + action="bad", # type: ignore[arg-type] + ) + + def test_invalid_enabled_for_evals_type_raises(self): + with pytest.raises(ValueError, match="boolean"): + guardrail( + lambda text: text, + validator=CustomValidator(lambda args: False), + action=LogAction(), + enabled_for_evals="yes", # type: ignore[arg-type] + ) + + +# --------------------------------------------------------------------------- +# 13. Stacked decorators +# --------------------------------------------------------------------------- + + +class TestStackedDecorators: + def test_both_decorators_fire_on_same_function(self): + calls: list[str] = [] + + def _make_mock(tag: str) -> Any: + m = MagicMock() + m.supported_stages = [] + m.validate_stage = MagicMock() + m.get_built_in_guardrail.return_value = None + m.run.side_effect = lambda *a, **kw: calls.append(tag) or _PASSED # type: ignore[func-returns-value] + return m + + @guardrail( + validator=_make_mock("outer"), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + name="outer", + ) + @guardrail( + validator=_make_mock("inner"), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + name="inner", + ) + def fn(text: str) -> str: + return text + + fn("hello") + assert "outer" in calls + assert "inner" in calls + + def test_outer_block_prevents_inner_from_firing(self): + inner_called = [] + + outer_validator = MagicMock() + outer_validator.supported_stages = [] + outer_validator.validate_stage = MagicMock() + outer_validator.get_built_in_guardrail.return_value = None + outer_validator.run.return_value = _FAILED + + inner_validator = MagicMock() + inner_validator.supported_stages = [] + inner_validator.validate_stage = MagicMock() + inner_validator.get_built_in_guardrail.return_value = None + inner_validator.run.side_effect = lambda *a, **kw: ( + inner_called.append(True) or _PASSED # type: ignore[func-returns-value] + ) + + @guardrail( + validator=outer_validator, + action=BlockAction(), + stage=GuardrailExecutionStage.PRE, + ) + @guardrail( + validator=inner_validator, + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def fn(text: str) -> str: + return text + + with pytest.raises(GuardrailBlockException): + fn("bad") + + assert not inner_called + + +# --------------------------------------------------------------------------- +# 14. Factory function path (adapter wraps return value) +# --------------------------------------------------------------------------- + + +class TestFactoryFunctionPath: + def test_adapter_wraps_return_value_of_factory(self): + register_guardrail_adapter(_DummyAdapter()) + target = _DummyTarget(return_value={"output": "ok"}) + + @guardrail( + validator=CustomValidator(lambda args: False), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def factory() -> _DummyTarget: + return target + + wrapped = factory() + assert isinstance(wrapped, _WrappedDummyTarget) + + def test_factory_pre_guardrail_fires_on_factory_params(self): + register_guardrail_adapter(_DummyAdapter()) + captured: list[Any] = [] + target = _DummyTarget() + + @guardrail( + validator=CustomValidator( + lambda args: captured.append(args) or False # type: ignore[func-returns-value] + ), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def factory(config: str) -> _DummyTarget: + return target + + factory("test-config") + assert captured == [{"config": "test-config"}] + + def test_adapter_recognizes_direct_object(self): + register_guardrail_adapter(_DummyAdapter()) + target = _DummyTarget() + + wrapped = guardrail( + target, + validator=CustomValidator(lambda args: False), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + assert isinstance(wrapped, _WrappedDummyTarget) + + def test_stacked_guardrails_on_factory_both_wrap_return_value(self): + register_guardrail_adapter(_DummyAdapter()) + target = _DummyTarget() + evals: list[str] = [] + + def _make_mock(tag: str) -> Any: + m = MagicMock() + m.supported_stages = [] + m.validate_stage = MagicMock() + m.get_built_in_guardrail.return_value = None + m.run.side_effect = lambda *a, **kw: evals.append(tag) or _PASSED # type: ignore[func-returns-value] + return m + + @guardrail( + validator=_make_mock("outer"), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + @guardrail( + validator=_make_mock("inner"), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def factory() -> _DummyTarget: + return target + + wrapped = factory() + wrapped.invoke({"x": 1}) + assert "outer" in evals + assert "inner" in evals + + +# --------------------------------------------------------------------------- +# 15. _make_evaluator — local vs API path +# --------------------------------------------------------------------------- + + +class TestMakeEvaluator: + def test_custom_validator_path_delegates_to_run(self): + """_make_evaluator with a CustomGuardrailValidator calls validator.run().""" + mock_validator = MagicMock() + mock_validator.run.return_value = _PASSED + evaluator = _make_evaluator(mock_validator, "G", None, True) + result = evaluator({"data": 1}, GuardrailExecutionStage.PRE, {"a": 1}, None) + mock_validator.run.assert_called_once_with( + "G", None, True, {"data": 1}, GuardrailExecutionStage.PRE, {"a": 1}, None + ) + assert result == _PASSED + + def test_built_in_validator_path_lazy_initializes_uipath(self): + """BuiltInGuardrailValidator.run() lazily creates UiPath() and calls API.""" + from uipath.platform.guardrails.decorators.validators import ( + BuiltInGuardrailValidator, + ) + from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail + + mock_built_in = MagicMock(spec=BuiltInValidatorGuardrail) + + class _TestBuiltIn(BuiltInGuardrailValidator): + def get_built_in_guardrail(self, name, description, enabled_for_evals): + return mock_built_in + + validator = _TestBuiltIn() + evaluator = _make_evaluator(validator, "G", None, True) + + mock_uipath = MagicMock() + mock_uipath.guardrails.evaluate_guardrail.return_value = _PASSED + with patch("uipath.platform.UiPath", return_value=mock_uipath): + evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) + evaluator({"text": "hello"}, GuardrailExecutionStage.PRE, None, None) + + # UiPath() should be created only once despite two calls + assert mock_uipath.guardrails.evaluate_guardrail.call_count == 2 + + +# --------------------------------------------------------------------------- +# 16. Joke-agent integration scenarios (plain functions) +# --------------------------------------------------------------------------- + + +class _JokeInput(BaseModel): + topic: str + + +class _JokeOutput(BaseModel): + joke: str + + +class TestJokeAgentScenarios: + """Integration tests modelled on the joke-agent-decorator sample.""" + + def test_pii_validator_blocks_person_name_in_topic(self): + """Agent-level PRE guardrail blocks person names in the input topic.""" + calls: list[str] = [] + + mock_validator = MagicMock() + mock_validator.supported_stages = [] + mock_validator.validate_stage = MagicMock() + + mock_validator.run.return_value = _FAILED + + @guardrail( + validator=mock_validator, + action=BlockAction(title="Person detected", detail="Not allowed"), + stage=GuardrailExecutionStage.PRE, + name="Agent PII", + ) + async def joke_node(state: _JokeInput) -> _JokeOutput: + calls.append("called") + return _JokeOutput(joke="a joke") + + import asyncio + + with pytest.raises(GuardrailBlockException): + asyncio.run(joke_node(_JokeInput(topic="John Smith"))) + assert not calls + + def test_input_pydantic_model_serialized_for_guardrail(self): + """State Pydantic model is serialized to dict and sent to evaluator.""" + captured: list[Any] = [] + + @guardrail( + validator=CustomValidator( + lambda args: captured.append(args) or False # type: ignore[func-returns-value] + ), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def process(state: _JokeInput) -> _JokeOutput: + return _JokeOutput(joke="a joke") + + process(_JokeInput(topic="cats")) + assert captured == [{"state": {"topic": "cats"}}] + + def test_output_pydantic_model_serialized_for_guardrail(self): + """Return Pydantic model is serialized to dict and sent to POST evaluator.""" + captured: list[Any] = [] + + @guardrail( + validator=CustomValidator( + lambda args: captured.append(args) or False # type: ignore[func-returns-value] + ), + action=LogAction(), + stage=GuardrailExecutionStage.POST, + ) + def process(state: _JokeInput) -> _JokeOutput: + return _JokeOutput(joke="funny joke about cats") + + process(_JokeInput(topic="cats")) + assert captured == [{"joke": "funny joke about cats"}] + + def test_excluded_config_param_not_in_guardrail_input(self): + """RunnableConfig-style param excluded from evaluation.""" + captured: list[Any] = [] + + @guardrail( + validator=CustomValidator( + lambda args: captured.append(args) or False # type: ignore[func-returns-value] + ), + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + ) + def process( + state: _JokeInput, + config: Annotated[dict[str, Any], GuardrailExclude()], + ) -> _JokeOutput: + return _JokeOutput(joke="joke") + + process(_JokeInput(topic="dogs"), {"thread_id": "abc"}) + assert "config" not in captured[0] + assert "state" in captured[0] + + def test_word_filter_custom_validator_on_tool_function(self): + """CustomValidator on plain function replaces offensive word via action.""" + censored: list[str] = [] + + class CensorAction(GuardrailAction): + def handle_validation_result(self, result, data, name): + if isinstance(data, dict) and "joke" in data: + censored.append(data["joke"]) + return {"joke": data["joke"].replace("donkey", "[censored]")} + return data + + @guardrail( + validator=CustomValidator( + lambda args: "donkey" in args.get("joke", "").lower() + ), + action=CensorAction(), + stage=GuardrailExecutionStage.PRE, + name="Word Filter", + ) + def analyze_joke(joke: str) -> str: + return f"analyzed: {joke}" + + result = analyze_joke(joke="why did the donkey cross the road") + assert "censored" in result + assert "donkey" not in result + + def test_log_action_does_not_stop_joke_generation(self): + """LogAction on PII violation logs but lets execution continue.""" + + @guardrail( + validator=CustomValidator(lambda args: True), # always violate + action=LogAction(), + stage=GuardrailExecutionStage.PRE, + name="Always-Log", + ) + def generate_joke(topic: str) -> str: + return f"joke about {topic}" + + result = generate_joke("cats") + assert result == "joke about cats" + + def test_length_limiter_blocks_long_joke(self): + """BlockAction on length check raises for over-long content.""" + + @guardrail( + validator=CustomValidator(lambda args: len(args.get("joke", "")) > 10), + action=BlockAction(title="Too long", detail="Joke exceeds limit"), + stage=GuardrailExecutionStage.PRE, + ) + def submit_joke(joke: str) -> str: + return joke + + with pytest.raises(GuardrailBlockException, match="Too long"): + submit_joke(joke="a" * 20) + + # Short joke passes through + assert submit_joke(joke="short") == "short" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index a9234b560..b01388722 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.10" +version = "0.5.11" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.19" +version = "0.1.20" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index fe5c19bb9..1ddd7d102 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -723,6 +723,7 @@ def test_agent_config_loads_guardrails(self): == "This validator is designed to detect personally identifiable information using Azure Cognitive Services" ) assert agent_builtin_guardrail.enabled_for_evals is True + assert agent_builtin_guardrail.selector is not None assert agent_builtin_guardrail.selector.scopes == ["Tool"] assert agent_builtin_guardrail.selector.match_names == ["StringToNumber"] diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 48783e0bb..49fb5bc70 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.10" +version = "0.5.11" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.19" +version = "0.1.20" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 3b4cba5bc08c2643982e6c7a4c9117e51e484e3e Mon Sep 17 00:00:00 2001 From: UIPath-Harshit Date: Tue, 7 Apr 2026 16:00:31 +0530 Subject: [PATCH 004/121] =?UTF-8?q?Revert=20"Fix/entity=20as=20resource=20?= =?UTF-8?q?overwrite=20(#1544)"=20as=20the=20upstream=20cha=E2=80=A6=20(#1?= =?UTF-8?q?545)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/_uipath.py | 4 +- .../src/uipath/platform/common/__init__.py | 2 - .../src/uipath/platform/common/_bindings.py | 20 +- .../platform/entities/_entities_service.py | 186 ++---------------- .../tests/services/test_entities_service.py | 185 ++++------------- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_resources/SDK_REFERENCE.md | 4 +- .../tests/resource_overrides/overwrites.json | 4 - .../test_resource_overrides.py | 5 - packages/uipath/uv.lock | 4 +- 12 files changed, 63 insertions(+), 357 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 748075eb4..b53fb68f9 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.20" +version = "0.1.21" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index e1d60fc39..87c3a17f0 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -139,9 +139,7 @@ def llm(self) -> UiPathLlmChatService: @property def entities(self) -> EntitiesService: - return EntitiesService( - self._config, self._execution_context, folders_service=self.folders - ) + return EntitiesService(self._config, self._execution_context) @cached_property def resource_catalog(self) -> ResourceCatalogService: diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 9070d0d70..40fc1ac34 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -7,7 +7,6 @@ from ._base_service import BaseService from ._bindings import ( ConnectionResourceOverwrite, - EntityResourceOverwrite, GenericResourceOverwrite, ResourceOverwrite, ResourceOverwriteParser, @@ -101,7 +100,6 @@ "EndpointManager", "jsonschema_to_pydantic", "ConnectionResourceOverwrite", - "EntityResourceOverwrite", "GenericResourceOverwrite", "ResourceOverwrite", "ResourceOverwriteParser", diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 1ccb2b1fc..449d2a7ef 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -45,7 +45,7 @@ def folder_identifier(self) -> str: class GenericResourceOverwrite(ResourceOverwrite): resource_type: Literal[ - "process", "index", "app", "asset", "bucket", "mcpServer", "queue" + "process", "index", "app", "asset", "bucket", "mcpServer", "queue", "entity" ] name: str = Field(alias="name") folder_path: str = Field(alias="folderPath") @@ -59,20 +59,6 @@ def folder_identifier(self) -> str: return self.folder_path -class EntityResourceOverwrite(ResourceOverwrite): - resource_type: Literal["entity"] - name: str = Field(alias="name") - folder_key: str = Field(alias="folderId") - - @property - def resource_identifier(self) -> str: - return self.name - - @property - def folder_identifier(self) -> str: - return self.folder_key - - class ConnectionResourceOverwrite(ResourceOverwrite): resource_type: Literal["connection"] # In eval context, studio web provides "ConnectionId". @@ -97,9 +83,7 @@ def folder_identifier(self) -> str: ResourceOverwriteUnion = Annotated[ - Union[ - GenericResourceOverwrite, EntityResourceOverwrite, ConnectionResourceOverwrite - ], + Union[GenericResourceOverwrite, ConnectionResourceOverwrite], Field(discriminator="resource_type"), ] diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index dc08131c5..f30c9492e 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -1,4 +1,3 @@ -import logging from typing import Any, Dict, List, Optional, Type import sqlparse @@ -8,21 +7,16 @@ from uipath.core.tracing import traced from ..common._base_service import BaseService -from ..common._bindings import EntityResourceOverwrite, _resource_overwrites from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..common._models import Endpoint, RequestSpec -from ..orchestrator._folder_service import FolderService from .entities import ( Entity, EntityRecord, EntityRecordsBatchResponse, - EntityRouting, QueryRoutingOverrideContext, ) -logger = logging.getLogger(__name__) - _FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"} _FORBIDDEN_DDL = {"DROP", "ALTER", "CREATE", "TRUNCATE"} _DISALLOWED_KEYWORDS = [ @@ -53,32 +47,9 @@ class EntitiesService(BaseService): """ def __init__( - self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, - folders_service: Optional[FolderService] = None, - folders_map: Optional[Dict[str, str]] = None, + self, config: UiPathApiConfig, execution_context: UiPathExecutionContext ) -> None: super().__init__(config=config, execution_context=execution_context) - self._folders_service = folders_service - self._folders_map = folders_map or {} - - def with_folders_map(self, folders_map: Dict[str, str]) -> "EntitiesService": - """Return a new EntitiesService configured with the given folders map. - - The map is used to build a routing context automatically when - ``query_entity_records`` is called without an explicit routing context. - Folder paths in the map are resolved to folder keys via ``FolderService``. - - Args: - folders_map: Mapping of entity name to folder path. - """ - return EntitiesService( - config=self._config, - execution_context=self._execution_context, - folders_service=self._folders_service, - folders_map=folders_map, - ) @traced(name="entity_retrieve", run_type="uipath") def retrieve(self, entity_key: str) -> Entity: @@ -446,6 +417,7 @@ class CustomerRecord: def query_entity_records( self, sql_query: str, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Query entity records using a validated SQL query. @@ -455,10 +427,9 @@ def query_entity_records( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - - Notes: - A routing context is always derived from the configured ``folders_map`` - when present and included in the request body. + routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context + for multi-folder queries. When present, included in the request body + and takes precedence over the folder header on the backend. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -467,12 +438,15 @@ def query_entity_records( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return self._query_entities_for_records(sql_query) + return self._query_entities_for_records( + sql_query, routing_context=routing_context + ) @traced(name="entity_query_records", run_type="uipath") async def query_entity_records_async( self, sql_query: str, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Asynchronously query entity records using a validated SQL query. @@ -482,10 +456,9 @@ async def query_entity_records_async( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - - Notes: - A routing context is always derived from the configured ``folders_map`` - when present and included in the request body. + routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context + for multi-folder queries. When present, included in the request body + and takes precedence over the folder header on the backend. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -494,14 +467,17 @@ async def query_entity_records_async( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return await self._query_entities_for_records_async(sql_query) + return await self._query_entities_for_records_async( + sql_query, routing_context=routing_context + ) def _query_entities_for_records( self, sql_query: str, + *, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) - routing_context = self._build_routing_context_from_map() spec = self._query_entity_records_spec(sql_query, routing_context) response = self.request(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -509,9 +485,10 @@ def _query_entities_for_records( async def _query_entities_for_records_async( self, sql_query: str, + *, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) - routing_context = await self._build_routing_context_from_map_async() spec = self._query_entity_records_spec(sql_query, routing_context) response = await self.request_async(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -1015,131 +992,6 @@ def _query_entity_records_spec( json=body, ) - def _build_routing_context_from_map( - self, - ) -> Optional[QueryRoutingOverrideContext]: - """Build a routing context from the configured folders_map and context overwrites. - - Folder paths in the map are resolved to folder keys via FolderService. - Entity overwrites from the active ``ResourceOverwritesContext`` are - merged in, supplying ``override_entity_name`` when the overwrite - provides a different entity name. - - Returns: - A QueryRoutingOverrideContext if routing entries exist, - None otherwise. - """ - resolved = self._resolve_folder_paths_to_ids() - return self._build_routing_context_from_resolved_map(resolved) - - async def _build_routing_context_from_map_async( - self, - ) -> Optional[QueryRoutingOverrideContext]: - """Async version of _build_routing_context_from_map.""" - resolved = await self._resolve_folder_paths_to_ids_async() - return self._build_routing_context_from_resolved_map(resolved) - - def _resolve_folder_paths_to_ids(self) -> Optional[dict[str, str]]: - if not self._folders_map: - return None - - resolved: dict[str, str] = {} - for folder_path in set(self._folders_map.values()): - if self._folders_service is not None: - folder_key = self._folders_service.retrieve_folder_key(folder_path) - if folder_key is not None: - resolved[folder_path] = folder_key - continue - resolved[folder_path] = folder_path - - return resolved - - async def _resolve_folder_paths_to_ids_async(self) -> Optional[dict[str, str]]: - if not self._folders_map: - return None - - resolved: dict[str, str] = {} - for folder_path in set(self._folders_map.values()): - if self._folders_service is not None: - folder_key = await self._folders_service.retrieve_folder_key_async( - folder_path - ) - if folder_key is not None: - resolved[folder_path] = folder_key - continue - resolved[folder_path] = folder_path - - return resolved - - @staticmethod - def _get_entity_overwrites_from_context() -> Dict[str, EntityResourceOverwrite]: - """Extract entity overwrites from the active ResourceOverwritesContext. - - Returns: - A dict mapping original entity name to its EntityResourceOverwrite. - """ - context_overwrites = _resource_overwrites.get() - if not context_overwrites: - return {} - - result: Dict[str, EntityResourceOverwrite] = {} - for key, overwrite in context_overwrites.items(): - if isinstance(overwrite, EntityResourceOverwrite): - # Key format is "entity." - original_name = key.split(".", 1)[1] if "." in key else key - result[original_name] = overwrite - return result - - def _build_routing_context_from_resolved_map( - self, - resolved: Optional[dict[str, str]], - ) -> Optional[QueryRoutingOverrideContext]: - entity_overwrites = self._get_entity_overwrites_from_context() - - routings: List[EntityRouting] = [] - - # Add routings from folders_map - if self._folders_map and resolved is not None: - for name, folder_path in self._folders_map.items(): - overwrite = entity_overwrites.pop(name, None) - override_name = ( - overwrite.resource_identifier - if overwrite and overwrite.resource_identifier != name - else None - ) - folder_id = ( - overwrite.folder_identifier - if overwrite - else resolved.get(folder_path, folder_path) - ) - routings.append( - EntityRouting( - entity_name=name, - folder_id=folder_id, - override_entity_name=override_name, - ) - ) - - # Add routings from context overwrites not already in folders_map - for original_name, overwrite in entity_overwrites.items(): - override_name = ( - overwrite.resource_identifier - if overwrite.resource_identifier != original_name - else None - ) - routings.append( - EntityRouting( - entity_name=original_name, - folder_id=overwrite.folder_identifier, - override_entity_name=override_name, - ) - ) - - if not routings: - return None - - return QueryRoutingOverrideContext(entity_routings=routings) - def _insert_batch_spec(self, entity_key: str, records: List[Any]) -> RequestSpec: return RequestSpec( method="POST", diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 4993a1367..8a9abafef 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -8,7 +8,7 @@ from pytest_httpx import HTTPXMock from uipath.platform import UiPathApiConfig, UiPathExecutionContext -from uipath.platform.entities import Entity +from uipath.platform.entities import Entity, EntityRouting, QueryRoutingOverrideContext from uipath.platform.entities._entities_service import EntitiesService @@ -390,21 +390,28 @@ async def test_query_entity_records_async_calls_request_for_valid_sql( assert result == [{"id": "c1"}] service.request_async.assert_called_once() - def test_query_entity_records_builds_routing_context_from_folders_map( + def test_query_entity_records_with_routing_context( self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, + service: EntitiesService, ) -> None: - service = EntitiesService( - config=config, - execution_context=execution_context, - folders_map={"Customers": "solution_folder", "Orders": "folder-2"}, - ) response = MagicMock() response.json.return_value = {"results": [{"id": 1}]} service.request = MagicMock(return_value=response) # type: ignore[method-assign] - result = service.query_entity_records("SELECT id FROM Customers LIMIT 10") + routing = QueryRoutingOverrideContext( + entity_routings=[ + EntityRouting(entity_name="Customers", folder_id="folder-1"), + EntityRouting( + entity_name="Orders", + folder_id="folder-2", + override_entity_name="OrdersV2", + ), + ] + ) + + result = service.query_entity_records( + "SELECT id FROM Customers LIMIT 10", routing_context=routing + ) assert result == [{"id": 1}] call_kwargs = service.request.call_args @@ -412,38 +419,39 @@ def test_query_entity_records_builds_routing_context_from_folders_map( assert body["query"] == "SELECT id FROM Customers LIMIT 10" assert body["routingContext"] == { "entityRoutings": [ - {"entityName": "Customers", "folderId": "solution_folder"}, - {"entityName": "Orders", "folderId": "folder-2"}, + {"entityName": "Customers", "folderId": "folder-1"}, + { + "entityName": "Orders", + "folderId": "folder-2", + "overrideEntityName": "OrdersV2", + }, ] } @pytest.mark.anyio - async def test_query_entity_records_async_builds_routing_context_from_folders_map( + async def test_query_entity_records_async_with_routing_context( self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, + service: EntitiesService, ) -> None: - service = EntitiesService( - config=config, - execution_context=execution_context, - folders_map={"Customers": "solution_folder"}, - ) response = MagicMock() response.json.return_value = {"results": [{"id": "c1"}]} service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] + routing = QueryRoutingOverrideContext( + entity_routings=[ + EntityRouting(entity_name="Customers", folder_id="folder-1"), + ] + ) + result = await service.query_entity_records_async( - "SELECT id FROM Customers WHERE id = 'c1'" + "SELECT id FROM Customers WHERE id = 'c1'", + routing_context=routing, ) assert result == [{"id": "c1"}] call_kwargs = service.request_async.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert body["routingContext"] == { - "entityRoutings": [ - {"entityName": "Customers", "folderId": "solution_folder"}, - ] - } + assert "routingContext" in body def test_query_entity_records_without_routing_context_omits_key( self, @@ -458,128 +466,3 @@ def test_query_entity_records_without_routing_context_omits_key( call_kwargs = service.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert "routingContext" not in body - - def test_query_entity_records_picks_up_entity_overwrites_from_context( - self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, - ) -> None: - from uipath.platform.common._bindings import ( - EntityResourceOverwrite, - _resource_overwrites, - ) - - service = EntitiesService( - config=config, - execution_context=execution_context, - ) - response = MagicMock() - response.json.return_value = {"results": [{"id": 1}]} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] - - overwrite = EntityResourceOverwrite( - resource_type="entity", - name="Overwritten Customers", - folder_key="overwritten-folder-id", - ) - token = _resource_overwrites.set({"entity.Customers": overwrite}) - try: - service.query_entity_records("SELECT id FROM Customers LIMIT 10") - finally: - _resource_overwrites.reset(token) - - call_kwargs = service.request.call_args - body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert body["routingContext"] == { - "entityRoutings": [ - { - "entityName": "Customers", - "folderId": "overwritten-folder-id", - "overrideEntityName": "Overwritten Customers", - }, - ] - } - - def test_query_entity_records_merges_folders_map_with_context_overwrites( - self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, - ) -> None: - from uipath.platform.common._bindings import ( - EntityResourceOverwrite, - _resource_overwrites, - ) - - service = EntitiesService( - config=config, - execution_context=execution_context, - folders_map={"Customers": "original-folder", "Orders": "orders-folder"}, - ) - response = MagicMock() - response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] - - # Overwrite only Customers — Orders should keep its folders_map value - overwrite = EntityResourceOverwrite( - resource_type="entity", - name="Overwritten Customers", - folder_key="overwritten-folder-id", - ) - token = _resource_overwrites.set({"entity.Customers": overwrite}) - try: - service.query_entity_records("SELECT id FROM Customers LIMIT 10") - finally: - _resource_overwrites.reset(token) - - call_kwargs = service.request.call_args - body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - routings = body["routingContext"]["entityRoutings"] - # Customers overwritten by context - assert { - "entityName": "Customers", - "folderId": "overwritten-folder-id", - "overrideEntityName": "Overwritten Customers", - } in routings - # Orders unchanged from folders_map - assert {"entityName": "Orders", "folderId": "orders-folder"} in routings - - def test_query_entity_records_context_overwrite_same_name_no_override_field( - self, - config: UiPathApiConfig, - execution_context: UiPathExecutionContext, - ) -> None: - from uipath.platform.common._bindings import ( - EntityResourceOverwrite, - _resource_overwrites, - ) - - service = EntitiesService( - config=config, - execution_context=execution_context, - ) - response = MagicMock() - response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] - - # Same entity name — only folder changes, no override_entity_name needed - overwrite = EntityResourceOverwrite( - resource_type="entity", - name="Customers", - folder_key="different-folder-id", - ) - token = _resource_overwrites.set({"entity.Customers": overwrite}) - try: - service.query_entity_records("SELECT id FROM Customers LIMIT 10") - finally: - _resource_overwrites.reset(token) - - call_kwargs = service.request.call_args - body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert body["routingContext"] == { - "entityRoutings": [ - { - "entityName": "Customers", - "folderId": "different-folder-id", - }, - ] - } diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index b01388722..648df8ab4 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.20" +version = "0.1.21" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 58f523a02..207f46657 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.42" +version = "2.10.43" 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/_resources/SDK_REFERENCE.md b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md index 5b03700be..4af1b60ae 100644 --- a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md @@ -500,10 +500,10 @@ sdk.entities.list_records(entity_key: str, schema: Optional[Type[Any]]=None, sta sdk.entities.list_records_async(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.EntityRecord] # Query entity records using a validated SQL query. -sdk.entities.query_entity_records(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] # Asynchronously query entity records using a validated SQL query. -sdk.entities.query_entity_records_async(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records_async(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] # Retrieve an entity by its key. sdk.entities.retrieve(entity_key: str) -> uipath.platform.entities.entities.Entity diff --git a/packages/uipath/tests/resource_overrides/overwrites.json b/packages/uipath/tests/resource_overrides/overwrites.json index e0bca84ba..c58744a69 100644 --- a/packages/uipath/tests/resource_overrides/overwrites.json +++ b/packages/uipath/tests/resource_overrides/overwrites.json @@ -28,9 +28,5 @@ "mcpServer.mcp_server_name": { "name": "Overwritten MCP Server Name", "folderPath": "Overwritten/MCPServer/Folder" - }, - "entity.entity_name": { - "name": "Overwritten Entity Name", - "folderId": "overwritten-entity-folder-id-123" } } \ No newline at end of file diff --git a/packages/uipath/tests/resource_overrides/test_resource_overrides.py b/packages/uipath/tests/resource_overrides/test_resource_overrides.py index 8d39a762d..c15bc113b 100644 --- a/packages/uipath/tests/resource_overrides/test_resource_overrides.py +++ b/packages/uipath/tests/resource_overrides/test_resource_overrides.py @@ -310,11 +310,6 @@ def test_parse_overwrites_with_type_adapter(self, overwrites_data): assert mcp_server.resource_identifier == "Overwritten MCP Server Name" assert mcp_server.folder_identifier == "Overwritten/MCPServer/Folder" - # Verify entity overwrite - entity = parsed_overwrites["entity.entity_name"] - assert entity.resource_identifier == "Overwritten Entity Name" - assert entity.folder_identifier == "overwritten-entity-folder-id-123" - def test_overrides_decorator_should_pop_kwargs_dict_when_present(self): from uipath.platform.common import resource_override diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 49fb5bc70..b8a817758 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.42" +version = "2.10.43" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.20" +version = "0.1.21" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From d52bcf1be463425d0ea60639a3db6cf55debc75b Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 8 Apr 2026 09:37:46 -0500 Subject: [PATCH 005/121] fix(platform): use LLMOps span ID in trace header (#1547) Co-authored-by: Clement Fauchere Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/_base_service.py | 11 +++++++++-- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index b53fb68f9..44f48463f 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.21" +version = "0.1.22" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_base_service.py b/packages/uipath-platform/src/uipath/platform/common/_base_service.py index d236e4ef5..2bd7c7ec3 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_base_service.py +++ b/packages/uipath-platform/src/uipath/platform/common/_base_service.py @@ -67,8 +67,15 @@ def _get_caller_component() -> str: def _inject_trace_context(headers: dict[str, str]) -> None: - """Inject UiPath trace context header from the active OTEL span.""" - span = trace.get_current_span() + """Inject UiPath trace context header from the active OTEL span. + + Prefers the LLMOps tool span (from ContextVar) over the raw OTEL span, + so the header carries the span ID visible in the LLMOps trace. + """ + from uipath.core.tracing.span_utils import UiPathSpanUtils + + llmops_span = UiPathSpanUtils.get_external_current_span() + span = llmops_span or trace.get_current_span() ctx = span.get_span_context() if ctx.trace_id and ctx.span_id: headers[_TRACE_PARENT_HEADER] = ( diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 648df8ab4..0494f3530 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.21" +version = "0.1.22" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index b8a817758..679981407 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.21" +version = "0.1.22" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From ec4b750ab52e786012fe55d78dc683f1aeaecf2d Mon Sep 17 00:00:00 2001 From: Popescu Tudor-Cristian <94108303+PopescuTudor@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:48:20 +0300 Subject: [PATCH 006/121] feat: enable llms-full.txt generation and add eval docs to llmstxt plugin (#1550) --- packages/uipath/docs/assets/llms.txt | 58 ---------------------------- packages/uipath/mkdocs.yml | 2 + 2 files changed, 2 insertions(+), 58 deletions(-) delete mode 100644 packages/uipath/docs/assets/llms.txt diff --git a/packages/uipath/docs/assets/llms.txt b/packages/uipath/docs/assets/llms.txt deleted file mode 100644 index ac9907c58..000000000 --- a/packages/uipath/docs/assets/llms.txt +++ /dev/null @@ -1,58 +0,0 @@ -# UiPath Python SDK Documentation -> https://uipath.github.io/uipath-python/ - -A Python SDK for programmatic interaction with UiPath Cloud Platform services, featuring CLI tools for automation creation, packaging, and deployment. Includes support for LangChain, LlamaIndex, and Model Context Protocol (MCP) agent frameworks. - -## Core Documentation - -### Getting Started -- https://uipath.github.io/uipath-python/ - Main landing page -- https://uipath.github.io/uipath-python/core/getting_started - SDK quickstart guide -- https://uipath.github.io/uipath-python/FAQ - Frequently Asked Questions -- https://uipath.github.io/uipath-python/CONTRIBUTING - Contribution guidelines -- https://uipath.github.io/uipath-python/release_policy - Release policy - -### SDK Features -- https://uipath.github.io/uipath-python/core/processes - Process automation -- https://uipath.github.io/uipath-python/core/jobs - Job management -- https://uipath.github.io/uipath-python/core/assets - Asset storage and retrieval -- https://uipath.github.io/uipath-python/core/queues - Queue operations -- https://uipath.github.io/uipath-python/core/resource_catalog - Resources search -- https://uipath.github.io/uipath-python/core/buckets - Cloud storage buckets -- https://uipath.github.io/uipath-python/core/attachments - File attachments -- https://uipath.github.io/uipath-python/core/actions - Action Center integration -- https://uipath.github.io/uipath-python/core/entities - Data Service integration -- https://uipath.github.io/uipath-python/core/connections - External connections -- https://uipath.github.io/uipath-python/core/documents - Document handling -- https://uipath.github.io/uipath-python/core/documents_models - Document data models -- https://uipath.github.io/uipath-python/core/environment_variables - Environment configuration -- https://uipath.github.io/uipath-python/core/guardrails - Guardrails validation -- https://uipath.github.io/uipath-python/core/traced - Tracing and observability - -### LLM & AI Features -- https://uipath.github.io/uipath-python/core/llm_gateway - LLM Gateway for model access -- https://uipath.github.io/uipath-python/core/context_grounding - RAG and semantic search - -### Agent Frameworks -- https://uipath.github.io/uipath-python/mcp/quick_start - Model Context Protocol (MCP) SDK -- https://uipath.github.io/uipath-python/langchain/quick_start - LangChain integration -- https://uipath.github.io/uipath-python/llamaindex/quick_start - LlamaIndex integration - -### CLI Tools -- https://uipath.github.io/uipath-python/cli/ - Command-line interface reference - -## Supported LLM Models - -The following LLM models are referenced in examples and evaluations throughout the repository: - -### OpenAI Models -- gpt-4o-mini-2024-07-18 - Used in samples and evaluations -- gpt-4o-2024-08-06 - Primary model for agent examples -- gpt-4 - General purpose examples -- gpt-4.1-2025-04-14 - LLM-as-judge evaluator - -### Google Models -- gemini-1.5-flash - Used in Google ADK agent sample - -### Embedding Models -- text-embedding-3-large - Azure OpenAI embeddings for RAG diff --git a/packages/uipath/mkdocs.yml b/packages/uipath/mkdocs.yml index 0b7fafd34..abffe1161 100644 --- a/packages/uipath/mkdocs.yml +++ b/packages/uipath/mkdocs.yml @@ -120,10 +120,12 @@ nav: plugins: - search - llmstxt: + full_output: llms-full.txt sections: "UiPath SDK": - core/*.md - cli/*.md + - eval/*.md "UiPath MCP SDK": - mcp/*.md "UiPath LangChain SDK": From 8df27995cd43e47b1decbe310fc0630a4f05e7c3 Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 8 Apr 2026 15:43:52 -0500 Subject: [PATCH 007/121] fix(platform): use agent trace ID from UIPATH_TRACE_ID in trace header (#1551) Co-authored-by: Clement Fauchere Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath/platform/common/_base_service.py | 26 ++++++++++++++----- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 44f48463f..e22bf6ace 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.22" +version = "0.1.23" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_base_service.py b/packages/uipath-platform/src/uipath/platform/common/_base_service.py index 2bd7c7ec3..21d8fa404 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_base_service.py +++ b/packages/uipath-platform/src/uipath/platform/common/_base_service.py @@ -67,20 +67,32 @@ def _get_caller_component() -> str: def _inject_trace_context(headers: dict[str, str]) -> None: - """Inject UiPath trace context header from the active OTEL span. + """Inject UiPath trace context header. - Prefers the LLMOps tool span (from ContextVar) over the raw OTEL span, - so the header carries the span ID visible in the LLMOps trace. + Trace ID: uses the agent trace ID from UIPATH_TRACE_ID env var (same + remapping the LLMOps exporter applies), falling back to the OTEL trace ID. + Span ID: uses the LLMOps tool span (via external span provider) so the + span ID matches what's visible in the LLMOps trace UI. """ from uipath.core.tracing.span_utils import UiPathSpanUtils + from ._config import UiPathConfig + from ._span_utils import _SpanUtils + llmops_span = UiPathSpanUtils.get_external_current_span() span = llmops_span or trace.get_current_span() ctx = span.get_span_context() - if ctx.trace_id and ctx.span_id: - headers[_TRACE_PARENT_HEADER] = ( - f"00-{format_trace_id(ctx.trace_id)}-{format_span_id(ctx.span_id)}-01" - ) + if not (ctx.trace_id and ctx.span_id): + return + + config_trace_id = UiPathConfig.trace_id + trace_id = ( + _SpanUtils.normalize_trace_id(config_trace_id) + if config_trace_id + else format_trace_id(ctx.trace_id) + ) + span_id = format_span_id(ctx.span_id) + headers[_TRACE_PARENT_HEADER] = f"00-{trace_id}-{span_id}-01" class BaseService: diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 0494f3530..db41f6ec8 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.22" +version = "0.1.23" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 679981407..81be68e4d 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.22" +version = "0.1.23" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From cbcb8841edce75f9977eb0d1fecdeff73fd6d94d Mon Sep 17 00:00:00 2001 From: Popescu Tudor-Cristian <94108303+PopescuTudor@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:15:11 +0300 Subject: [PATCH 008/121] feat: add runtime fields to AgentA2aResourceConfig for A2A tool support (#1553) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/agent/models/agent.py | 2 ++ packages/uipath/uv.lock | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 207f46657..4b1b0dc3f 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.43" +version = "2.10.44" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 901ce7ca9..c5a63af7c 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -485,6 +485,8 @@ class AgentA2aResourceConfig(BaseAgentResourceConfig): created_by: Optional[str] = Field(default=None, alias="createdBy") updated_at: Optional[str] = Field(default=None, alias="updatedAt") updated_by: Optional[str] = Field(default=None, alias="updatedBy") + a2a_url: str = Field(default="", alias="a2aUrl") + folder_path: Optional[str] = Field(default=None, alias="folderPath") _RECIPIENT_TYPE_NORMALIZED_MAP: Mapping[int | str, AgentEscalationRecipientType] = { diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 81be68e4d..ba40c276e 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.43" +version = "2.10.44" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 1bf32e29242d3cb919dd79b1e7afdef78ea262d0 Mon Sep 17 00:00:00 2001 From: UIPath-Harshit Date: Thu, 9 Apr 2026 17:33:32 +0530 Subject: [PATCH 009/121] Fix/entity overwrite fixes (#1548) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/_uipath.py | 4 +- .../src/uipath/platform/common/__init__.py | 2 + .../src/uipath/platform/common/_bindings.py | 54 +- .../src/uipath/platform/entities/__init__.py | 4 + .../platform/entities/_entities_service.py | 189 ++++++- .../platform/entities/_entity_resolution.py | 526 ++++++++++++++++++ .../src/uipath/platform/entities/entities.py | 40 +- .../tests/services/test_entities_service.py | 377 +++++++++++-- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_resources/SDK_REFERENCE.md | 17 +- .../uipath/src/uipath/agent/models/agent.py | 11 +- .../uipath/tests/agent/models/test_agent.py | 6 +- .../tests/resource_overrides/overwrites.json | 4 + .../test_resource_overrides.py | 5 + packages/uipath/uv.lock | 4 +- 17 files changed, 1171 insertions(+), 78 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index e22bf6ace..b54858ea6 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.23" +version = "0.1.24" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 87c3a17f0..e1d60fc39 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -139,7 +139,9 @@ def llm(self) -> UiPathLlmChatService: @property def entities(self) -> EntitiesService: - return EntitiesService(self._config, self._execution_context) + return EntitiesService( + self._config, self._execution_context, folders_service=self.folders + ) @cached_property def resource_catalog(self) -> ResourceCatalogService: diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 40fc1ac34..9070d0d70 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -7,6 +7,7 @@ from ._base_service import BaseService from ._bindings import ( ConnectionResourceOverwrite, + EntityResourceOverwrite, GenericResourceOverwrite, ResourceOverwrite, ResourceOverwriteParser, @@ -100,6 +101,7 @@ "EndpointManager", "jsonschema_to_pydantic", "ConnectionResourceOverwrite", + "EntityResourceOverwrite", "GenericResourceOverwrite", "ResourceOverwrite", "ResourceOverwriteParser", diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 449d2a7ef..321b83c4f 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -14,7 +14,14 @@ Union, ) -from pydantic import AliasChoices, BaseModel, ConfigDict, Field, TypeAdapter +from pydantic import ( + AliasChoices, + BaseModel, + ConfigDict, + Field, + TypeAdapter, + model_validator, +) logger = logging.getLogger(__name__) @@ -45,7 +52,7 @@ def folder_identifier(self) -> str: class GenericResourceOverwrite(ResourceOverwrite): resource_type: Literal[ - "process", "index", "app", "asset", "bucket", "mcpServer", "queue", "entity" + "process", "index", "app", "asset", "bucket", "mcpServer", "queue" ] name: str = Field(alias="name") folder_path: str = Field(alias="folderPath") @@ -59,6 +66,29 @@ def folder_identifier(self) -> str: return self.folder_path +class EntityResourceOverwrite(ResourceOverwrite): + resource_type: Literal["entity"] + name: str = Field(alias="name") + folder_id: Optional[str] = Field(default=None, alias="folderId") + folder_path: Optional[str] = Field(default=None, alias="folderPath") + + @model_validator(mode="after") + def validate_folder_identifier(self) -> "EntityResourceOverwrite": + if self.folder_id and self.folder_path: + raise ValueError("Only one of folderId or folderPath may be provided.") + if not self.folder_id and not self.folder_path: + raise ValueError("Either folderId or folderPath must be provided.") + return self + + @property + def resource_identifier(self) -> str: + return self.name + + @property + def folder_identifier(self) -> str: + return self.folder_id or self.folder_path or "" + + class ConnectionResourceOverwrite(ResourceOverwrite): resource_type: Literal["connection"] # In eval context, studio web provides "ConnectionId". @@ -83,7 +113,9 @@ def folder_identifier(self) -> str: ResourceOverwriteUnion = Annotated[ - Union[GenericResourceOverwrite, ConnectionResourceOverwrite], + Union[ + GenericResourceOverwrite, EntityResourceOverwrite, ConnectionResourceOverwrite + ], Field(discriminator="resource_type"), ] @@ -112,9 +144,23 @@ def parse(cls, key: str, value: dict[str, Any]) -> ResourceOverwrite: The appropriate ResourceOverwrite subclass instance """ resource_type = key.split(".")[0] - value_with_type = {"resource_type": resource_type, **value} + normalized_value = cls._normalize_value(resource_type, value) + value_with_type = {"resource_type": resource_type, **normalized_value} return cls._adapter.validate_python(value_with_type) + @staticmethod + def _normalize_value(resource_type: str, value: dict[str, Any]) -> dict[str, Any]: + if resource_type != "entity": + return value + + normalized = dict(value) + if "folderId" in normalized: + normalized["folder_id"] = normalized.pop("folderId") + if "folderPath" in normalized: + normalized["folder_path"] = normalized.pop("folderPath") + + return normalized + _resource_overwrites: ContextVar[Optional[dict[str, ResourceOverwrite]]] = ContextVar( "resource_overwrites", default=None diff --git a/packages/uipath-platform/src/uipath/platform/entities/__init__.py b/packages/uipath-platform/src/uipath/platform/entities/__init__.py index bbc43cdb7..6c9ac7f9f 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/entities/__init__.py @@ -5,12 +5,14 @@ from ._entities_service import EntitiesService from .entities import ( + DataFabricEntityItem, Entity, EntityField, EntityFieldMetadata, EntityRecord, EntityRecordsBatchResponse, EntityRouting, + EntitySetResolution, ExternalField, ExternalObject, ExternalSourceFields, @@ -22,12 +24,14 @@ ) __all__ = [ + "DataFabricEntityItem", "EntitiesService", "Entity", "EntityField", "EntityRecord", "EntityFieldMetadata", "EntityRouting", + "EntitySetResolution", "FieldDataType", "FieldMetadata", "EntityRecordsBatchResponse", diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index f30c9492e..43b437d65 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -1,3 +1,4 @@ +import logging from typing import Any, Dict, List, Optional, Type import sqlparse @@ -7,16 +8,32 @@ from uipath.core.tracing import traced from ..common._base_service import BaseService +from ..common._bindings import _resource_overwrites from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..common._models import Endpoint, RequestSpec +from ..common.constants import HEADER_FOLDER_KEY +from ..orchestrator._folder_service import FolderService +from ._entity_resolution import ( + RoutingStrategy, + build_resolution_service, + create_resolution_plan, + create_resolution_plan_async, + create_routing_strategy, + fetch_resolved_entities, + fetch_resolved_entities_async, +) from .entities import ( + DataFabricEntityItem, Entity, EntityRecord, EntityRecordsBatchResponse, + EntitySetResolution, QueryRoutingOverrideContext, ) +logger = logging.getLogger(__name__) + _FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"} _FORBIDDEN_DDL = {"DROP", "ALTER", "CREATE", "TRUNCATE"} _DISALLOWED_KEYWORDS = [ @@ -47,9 +64,22 @@ class EntitiesService(BaseService): """ def __init__( - self, config: UiPathApiConfig, execution_context: UiPathExecutionContext + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: Optional[FolderService] = None, + folders_map: Optional[Dict[str, str]] = None, + entity_name_overrides: Optional[Dict[str, str]] = None, + routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> None: super().__init__(config=config, execution_context=execution_context) + self._folders_service = folders_service + self._routing_strategy: RoutingStrategy = create_routing_strategy( + folders_map=folders_map, + effective_entity_names=entity_name_overrides, + routing_context=routing_context, + folders_service=folders_service, + ) @traced(name="entity_retrieve", run_type="uipath") def retrieve(self, entity_key: str) -> Entity: @@ -128,6 +158,44 @@ async def retrieve_async(self, entity_key: str) -> Entity: return Entity.model_validate(response.json()) + @traced(name="entity_retrieve_by_name", run_type="uipath") + def retrieve_by_name( + self, entity_name: str, folder_key: Optional[str] = None + ) -> Entity: + """Retrieve an entity by its name. + + The server resolves the entity within the folder identified by + ``folder_key``. When omitted the default folder from the + execution context is used. + + Args: + entity_name: The name of the entity. + folder_key: Optional folder key for disambiguation. + """ + spec = self._retrieve_by_name_spec(entity_name) + headers = self._folder_key_headers(folder_key) + response = self.request(spec.method, spec.endpoint, headers=headers) + return Entity.model_validate(response.json()) + + @traced(name="entity_retrieve_by_name", run_type="uipath") + async def retrieve_by_name_async( + self, entity_name: str, folder_key: Optional[str] = None + ) -> Entity: + """Asynchronously retrieve an entity by its name. + + The server resolves the entity within the folder identified by + ``folder_key``. When omitted the default folder from the + execution context is used. + + Args: + entity_name: The name of the entity. + folder_key: Optional folder key for disambiguation. + """ + spec = self._retrieve_by_name_spec(entity_name) + headers = self._folder_key_headers(folder_key) + response = await self.request_async(spec.method, spec.endpoint, headers=headers) + return Entity.model_validate(response.json()) + @traced(name="list_entities", run_type="uipath") def list_entities(self) -> List[Entity]: """List all entities in Data Service. @@ -417,7 +485,6 @@ class CustomerRecord: def query_entity_records( self, sql_query: str, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Query entity records using a validated SQL query. @@ -427,9 +494,10 @@ def query_entity_records( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context - for multi-folder queries. When present, included in the request body - and takes precedence over the folder header on the backend. + + Notes: + A routing context is always derived from the configured ``folders_map`` + when present and included in the request body. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -438,15 +506,12 @@ def query_entity_records( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return self._query_entities_for_records( - sql_query, routing_context=routing_context - ) + return self._query_entities_for_records(sql_query) @traced(name="entity_query_records", run_type="uipath") async def query_entity_records_async( self, sql_query: str, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: """Asynchronously query entity records using a validated SQL query. @@ -456,9 +521,10 @@ async def query_entity_records_async( sql_query (str): A SQL SELECT query to execute against Data Service entities. Only SELECT statements are allowed. Queries without WHERE must include a LIMIT clause. Subqueries and multi-statement queries are not permitted. - routing_context (Optional[QueryRoutingOverrideContext]): Per-entity routing context - for multi-folder queries. When present, included in the request body - and takes precedence over the folder header on the backend. + + Notes: + A routing context is always derived from the configured ``folders_map`` + when present and included in the request body. Returns: List[Dict[str, Any]]: A list of result records as dictionaries. @@ -467,17 +533,84 @@ async def query_entity_records_async( ValueError: If the SQL query fails validation (e.g., non-SELECT, missing WHERE/LIMIT, forbidden keywords, subqueries). """ - return await self._query_entities_for_records_async( - sql_query, routing_context=routing_context + return await self._query_entities_for_records_async(sql_query) + + @traced(name="resolve_entity_set", run_type="uipath") + def resolve_entity_set( + self, + items: list[DataFabricEntityItem], + ) -> EntitySetResolution: + """Resolve an agent entity set, applying resource overwrites.""" + plan = create_resolution_plan( + items, + _resource_overwrites.get() or {}, + lambda folder_path: ( + self._folders_service.retrieve_key(folder_path=folder_path) + if self._folders_service is not None + else None + ), + ) + entities = fetch_resolved_entities( + plan, + self.retrieve, + self.retrieve_by_name, + logger, + ) + resolution_service: EntitiesService = build_resolution_service( # type: ignore[assignment] + config=self._config, + execution_context=self._execution_context, + folders_service=self._folders_service, + plan=plan, + service_factory=EntitiesService, + ) + return EntitySetResolution( + entities=entities, + entities_service=resolution_service, + ) + + @traced(name="resolve_entity_set", run_type="uipath") + async def resolve_entity_set_async( + self, + items: list[DataFabricEntityItem], + ) -> EntitySetResolution: + """Resolve an agent entity set, applying resource overwrites.""" + + async def _resolve_folder_path(folder_path: str) -> Optional[str]: + if self._folders_service is None: + return None + return await self._folders_service.retrieve_key_async( + folder_path=folder_path + ) + + plan = await create_resolution_plan_async( + items, + _resource_overwrites.get() or {}, + _resolve_folder_path, + ) + entities = await fetch_resolved_entities_async( + plan, + self.retrieve_async, + self.retrieve_by_name_async, + logger, + ) + resolution_service: EntitiesService = build_resolution_service( # type: ignore[assignment] + config=self._config, + execution_context=self._execution_context, + folders_service=self._folders_service, + plan=plan, + service_factory=EntitiesService, + ) + return EntitySetResolution( + entities=entities, + entities_service=resolution_service, ) def _query_entities_for_records( self, sql_query: str, - *, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) + routing_context = self._routing_strategy.resolve() spec = self._query_entity_records_spec(sql_query, routing_context) response = self.request(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -485,10 +618,9 @@ def _query_entities_for_records( async def _query_entities_for_records_async( self, sql_query: str, - *, - routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> List[Dict[str, Any]]: self._validate_sql_query(sql_query) + routing_context = await self._routing_strategy.resolve_async() spec = self._query_entity_records_spec(sql_query, routing_context) response = await self.request_async(spec.method, spec.endpoint, json=spec.json) return response.json().get("results", []) @@ -956,6 +1088,21 @@ def _retrieve_spec( endpoint=Endpoint(f"datafabric_/api/Entity/{entity_key}"), ) + def _retrieve_by_name_spec( + self, + entity_name: str, + ) -> RequestSpec: + return RequestSpec( + method="GET", + endpoint=Endpoint(f"datafabric_/api/Entity/{entity_name}/metadata"), + ) + + @staticmethod + def _folder_key_headers(folder_key: Optional[str]) -> dict[str, str]: + if folder_key: + return {HEADER_FOLDER_KEY: folder_key} + return {} + def _list_entities_spec(self) -> RequestSpec: return RequestSpec( method="GET", @@ -1117,3 +1264,9 @@ def _projection_column_count( if not text: return 0 return len([part for part in text.split(",") if part.strip()]) + + +# Resolve the forward reference to EntitiesService in EntitySetResolution. +# The model uses TYPE_CHECKING to avoid circular imports in entities.py, +# so we must rebuild it here where EntitiesService is fully defined. +EntitySetResolution.model_rebuild() diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py b/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py new file mode 100644 index 000000000..2477b26b9 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/entities/_entity_resolution.py @@ -0,0 +1,526 @@ +from __future__ import annotations + +import abc +import asyncio +import logging +from dataclasses import dataclass +from typing import Awaitable, Callable, Dict, Optional + +from ..common._bindings import ( + EntityResourceOverwrite, + ResourceOverwrite, + _resource_overwrites, +) +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..orchestrator._folder_service import FolderService +from .entities import ( + DataFabricEntityItem, + Entity, + EntityRouting, + QueryRoutingOverrideContext, +) + +FolderPathResolver = Callable[[str], Optional[str]] +AsyncFolderPathResolver = Callable[[str], Awaitable[Optional[str]]] +EntityByKeyFetcher = Callable[[str], Entity] +AsyncEntityByKeyFetcher = Callable[[str], Awaitable[Entity]] +EntityByNameFetcher = Callable[[str, Optional[str]], Entity] +AsyncEntityByNameFetcher = Callable[[str, Optional[str]], Awaitable[Entity]] + + +# --------------------------------------------------------------------------- +# Routing strategy +# --------------------------------------------------------------------------- + + +class RoutingStrategy(abc.ABC): + """Strategy for resolving a ``QueryRoutingOverrideContext`` at query time.""" + + @abc.abstractmethod + def resolve(self) -> Optional[QueryRoutingOverrideContext]: ... + + @abc.abstractmethod + async def resolve_async(self) -> Optional[QueryRoutingOverrideContext]: ... + + +class PreResolvedRoutingStrategy(RoutingStrategy): + """Returns a routing context that was fully resolved at init time. + + Used after ``resolve_entity_set`` where all folder paths have already + been converted to folder keys and the routing context is immutable. + """ + + def __init__( + self, + routing_context: QueryRoutingOverrideContext, + ) -> None: + self._routing_context = routing_context + + def resolve(self) -> Optional[QueryRoutingOverrideContext]: + return self._routing_context + + async def resolve_async(self) -> Optional[QueryRoutingOverrideContext]: + return self._routing_context + + @property + def routing_context(self) -> QueryRoutingOverrideContext: + return self._routing_context + + +class FoldersMapRoutingStrategy(RoutingStrategy): + """Builds a routing context from a pre-populated folders map. + + Used when an ``EntitiesService`` is constructed with an explicit + ``folders_map`` (and optional entity-name overrides) but *without* a + pre-built routing context. Folder paths in the map are resolved to + folder keys lazily at query time via ``FolderService``. + """ + + def __init__( + self, + folders_map: Dict[str, str], + effective_entity_names: Dict[str, str], + folders_service: Optional[FolderService], + ) -> None: + self._folders_map = folders_map + self._effective_entity_names = effective_entity_names + self._folders_service = folders_service + + def resolve(self) -> Optional[QueryRoutingOverrideContext]: + resolved = self._resolve_folder_paths() + return build_resolution_routing_context( + { + name: (resolved or {}).get(path, path) + for name, path in self._folders_map.items() + }, + self._effective_entity_names, + ) + + async def resolve_async(self) -> Optional[QueryRoutingOverrideContext]: + resolved = await self._resolve_folder_paths_async() + return build_resolution_routing_context( + { + name: (resolved or {}).get(path, path) + for name, path in self._folders_map.items() + }, + self._effective_entity_names, + ) + + def _resolve_folder_paths(self) -> Optional[dict[str, str]]: + folder_paths = set(self._folders_map.values()) + if not folder_paths: + return None + + resolved: dict[str, str] = {} + for folder_path in folder_paths: + if self._folders_service is not None: + folder_key = self._folders_service.retrieve_key(folder_path=folder_path) + if folder_key is not None: + resolved[folder_path] = folder_key + continue + resolved[folder_path] = folder_path + return resolved + + async def _resolve_folder_paths_async(self) -> Optional[dict[str, str]]: + folder_paths = set(self._folders_map.values()) + if not folder_paths: + return None + + resolved: dict[str, str] = {} + for folder_path in folder_paths: + if self._folders_service is not None: + folder_key = await self._folders_service.retrieve_key_async( + folder_path=folder_path + ) + if folder_key is not None: + resolved[folder_path] = folder_key + continue + resolved[folder_path] = folder_path + return resolved + + +class ContextOverwriteRoutingStrategy(RoutingStrategy): + """Builds a routing context lazily from ``_resource_overwrites``. + + This is the fallback for direct SDK usage where no ``folders_map`` or + pre-resolved routing context exists. Entity overwrites are read from + the active ``ResourceOverwritesContext`` at query time. + """ + + def __init__(self, folders_service: Optional[FolderService]) -> None: + self._folders_service = folders_service + + def resolve(self) -> Optional[QueryRoutingOverrideContext]: + entity_overwrites = _get_entity_overwrites_from_context() + if not entity_overwrites: + return None + + folder_paths = { + ow.folder_path for ow in entity_overwrites.values() if ow.folder_path + } + resolved = self._resolve_paths(folder_paths) + return self._build(entity_overwrites, resolved) + + async def resolve_async(self) -> Optional[QueryRoutingOverrideContext]: + entity_overwrites = _get_entity_overwrites_from_context() + if not entity_overwrites: + return None + + folder_paths = { + ow.folder_path for ow in entity_overwrites.values() if ow.folder_path + } + resolved = await self._resolve_paths_async(folder_paths) + return self._build(entity_overwrites, resolved) + + def _resolve_paths(self, folder_paths: set[str]) -> dict[str, str]: + resolved: dict[str, str] = {} + for path in folder_paths: + if self._folders_service is not None: + key = self._folders_service.retrieve_key(folder_path=path) + if key is not None: + resolved[path] = key + continue + resolved[path] = path + return resolved + + async def _resolve_paths_async(self, folder_paths: set[str]) -> dict[str, str]: + resolved: dict[str, str] = {} + for path in folder_paths: + if self._folders_service is not None: + key = await self._folders_service.retrieve_key_async(folder_path=path) + if key is not None: + resolved[path] = key + continue + resolved[path] = path + return resolved + + @staticmethod + def _build( + entity_overwrites: Dict[str, EntityResourceOverwrite], + resolved: dict[str, str], + ) -> Optional[QueryRoutingOverrideContext]: + routings: list[EntityRouting] = [] + for original_name, overwrite in entity_overwrites.items(): + override_name = ( + overwrite.resource_identifier + if overwrite.resource_identifier != original_name + else None + ) + folder_id = _resolve_overwrite_folder(overwrite, resolved) + routings.append( + EntityRouting( + entity_name=original_name, + folder_id=folder_id, + override_entity_name=override_name, + ) + ) + + if not routings: + return None + return QueryRoutingOverrideContext(entity_routings=routings) + + +def create_routing_strategy( + *, + folders_map: Optional[Dict[str, str]], + effective_entity_names: Optional[Dict[str, str]], + routing_context: Optional[QueryRoutingOverrideContext], + folders_service: Optional[FolderService], +) -> RoutingStrategy: + """Select the appropriate routing strategy based on init-time state.""" + if routing_context is not None: + return PreResolvedRoutingStrategy(routing_context) + if folders_map: + return FoldersMapRoutingStrategy( + folders_map, + effective_entity_names or {}, + folders_service, + ) + return ContextOverwriteRoutingStrategy(folders_service) + + +# --------------------------------------------------------------------------- +# Helpers shared across strategies +# --------------------------------------------------------------------------- + + +def _get_entity_overwrites_from_context() -> Dict[str, EntityResourceOverwrite]: + """Extract entity overwrites from the active ResourceOverwritesContext.""" + context_overwrites = _resource_overwrites.get() + if not context_overwrites: + return {} + + result: Dict[str, EntityResourceOverwrite] = {} + for key, overwrite in context_overwrites.items(): + if isinstance(overwrite, EntityResourceOverwrite): + original_name = key.split(".", 1)[1] if "." in key else key + result[original_name] = overwrite + return result + + +def _resolve_overwrite_folder( + overwrite: EntityResourceOverwrite, + resolved: dict[str, str], +) -> str: + """Return the folder key for an entity overwrite. + + Uses folder_id directly when present (already a key). + Falls back to resolving folder_path through the resolved map. + """ + if overwrite.folder_id: + return overwrite.folder_id + if overwrite.folder_path and resolved: + return resolved.get(overwrite.folder_path, overwrite.folder_path) + return overwrite.folder_identifier + + +# --------------------------------------------------------------------------- +# Resolution plan (used by resolve_entity_set) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class EntityFetchByKey: + entity_key: str + + +@dataclass(frozen=True) +class EntityFetchByName: + entity_name: str + folder_key: str + + +@dataclass(frozen=True) +class EntityResolutionDraft: + fetch_by_key: list[EntityFetchByKey] + fetch_by_name: list[EntityFetchByName] + folders_map: dict[str, str] + effective_entity_names: dict[str, str] + folder_paths_to_resolve: set[str] + + +@dataclass(frozen=True) +class EntityResolutionPlan: + fetch_by_key: list[EntityFetchByKey] + fetch_by_name: list[EntityFetchByName] + folders_map: dict[str, str] + effective_entity_names: dict[str, str] + routing_context: QueryRoutingOverrideContext | None + + +def create_resolution_draft( + items: list[DataFabricEntityItem], + context_overwrites: dict[str, ResourceOverwrite], +) -> EntityResolutionDraft: + folders_map: dict[str, str] = {} + effective_entity_names: dict[str, str] = {} + folder_paths_to_resolve: set[str] = set() + fetch_by_key: list[EntityFetchByKey] = [] + fetch_by_name: list[EntityFetchByName] = [] + + for item in items: + overwrite = context_overwrites.get( + f"entity.{item.id}" + ) or context_overwrites.get(f"entity.{item.name}") + resolved_folder = item.folder_key + + if isinstance(overwrite, EntityResourceOverwrite): + folder_changed = False + if overwrite.folder_id: + resolved_folder = overwrite.folder_id + folder_changed = resolved_folder != item.folder_key + elif overwrite.folder_path: + resolved_folder = overwrite.folder_path + folder_changed = True + folder_paths_to_resolve.add(overwrite.folder_path) + + if overwrite.name != item.name or folder_changed: + if overwrite.name != item.name: + effective_entity_names[item.name] = overwrite.name + fetch_by_name.append( + EntityFetchByName( + entity_name=overwrite.name, + folder_key=resolved_folder, + ) + ) + folders_map[item.name] = resolved_folder + continue + + fetch_by_key.append(EntityFetchByKey(entity_key=item.entity_key or item.id)) + folders_map[item.name] = resolved_folder + + return EntityResolutionDraft( + fetch_by_key=fetch_by_key, + fetch_by_name=fetch_by_name, + folders_map=folders_map, + effective_entity_names=effective_entity_names, + folder_paths_to_resolve=folder_paths_to_resolve, + ) + + +def finalize_resolution_plan( + draft: EntityResolutionDraft, + resolve_folder_path: Callable[[str], Optional[str]], +) -> EntityResolutionPlan: + resolved_paths: dict[str, str] = {} + for folder_path in draft.folder_paths_to_resolve: + resolved_paths[folder_path] = resolve_folder_path(folder_path) or folder_path + + resolved_folders_map = { + entity_name: resolved_paths.get(folder_key, folder_key) + for entity_name, folder_key in draft.folders_map.items() + } + resolved_fetch_by_name = [ + EntityFetchByName( + entity_name=entry.entity_name, + folder_key=resolved_paths.get(entry.folder_key, entry.folder_key), + ) + for entry in draft.fetch_by_name + ] + + return EntityResolutionPlan( + fetch_by_key=draft.fetch_by_key, + fetch_by_name=resolved_fetch_by_name, + folders_map=resolved_folders_map, + effective_entity_names=draft.effective_entity_names, + routing_context=build_resolution_routing_context( + resolved_folders_map, + draft.effective_entity_names, + ), + ) + + +def build_resolution_routing_context( + folders_map: dict[str, str], + effective_entity_names: dict[str, str], +) -> QueryRoutingOverrideContext | None: + routings = [ + EntityRouting( + entity_name=original_name, + folder_id=folder_id, + override_entity_name=effective_entity_names.get(original_name), + ) + for original_name, folder_id in folders_map.items() + ] + if not routings: + return None + + return QueryRoutingOverrideContext(entity_routings=routings) + + +def create_resolution_plan( + items: list[DataFabricEntityItem], + context_overwrites: dict[str, ResourceOverwrite], + resolve_folder_path: FolderPathResolver, +) -> EntityResolutionPlan: + draft = create_resolution_draft(items, context_overwrites) + return finalize_resolution_plan(draft, resolve_folder_path) + + +async def create_resolution_plan_async( + items: list[DataFabricEntityItem], + context_overwrites: dict[str, ResourceOverwrite], + resolve_folder_path: AsyncFolderPathResolver, +) -> EntityResolutionPlan: + draft = create_resolution_draft(items, context_overwrites) + folder_paths = list(draft.folder_paths_to_resolve) + results = await asyncio.gather(*(resolve_folder_path(fp) for fp in folder_paths)) + resolved_paths = { + fp: result or fp for fp, result in zip(folder_paths, results, strict=True) + } + + return finalize_resolution_plan( + draft, + lambda folder_path: resolved_paths.get(folder_path, folder_path), + ) + + +def fetch_resolved_entities( + plan: EntityResolutionPlan, + retrieve_by_key: EntityByKeyFetcher, + retrieve_by_name: EntityByNameFetcher, + logger: logging.Logger, +) -> list[Entity]: + entities: list[Entity] = [] + for key_entry in plan.fetch_by_key: + try: + entities.append(retrieve_by_key(key_entry.entity_key)) + except Exception: + logger.warning( + "Failed to fetch entity by key '%s', skipping.", + key_entry.entity_key, + exc_info=True, + ) + + for name_entry in plan.fetch_by_name: + try: + entities.append( + retrieve_by_name(name_entry.entity_name, name_entry.folder_key) + ) + except Exception: + logger.warning( + "Failed to fetch entity by name '%s' (folder_key=%s), skipping.", + name_entry.entity_name, + name_entry.folder_key, + exc_info=True, + ) + + return entities + + +async def fetch_resolved_entities_async( + plan: EntityResolutionPlan, + retrieve_by_key: AsyncEntityByKeyFetcher, + retrieve_by_name: AsyncEntityByNameFetcher, + logger: logging.Logger, +) -> list[Entity]: + async def _safe_fetch_by_key(entry: EntityFetchByKey) -> Optional[Entity]: + try: + return await retrieve_by_key(entry.entity_key) + except Exception: + logger.warning( + "Failed to fetch entity by key '%s', skipping.", + entry.entity_key, + exc_info=True, + ) + return None + + async def _safe_fetch_by_name(entry: EntityFetchByName) -> Optional[Entity]: + try: + return await retrieve_by_name( + entry.entity_name, + entry.folder_key, + ) + except Exception: + logger.warning( + "Failed to fetch entity by name '%s' (folder_key=%s), skipping.", + entry.entity_name, + entry.folder_key, + exc_info=True, + ) + return None + + tasks = [_safe_fetch_by_key(entry) for entry in plan.fetch_by_key] + [ + _safe_fetch_by_name(entry) for entry in plan.fetch_by_name + ] + results = await asyncio.gather(*tasks) + return [entity for entity in results if entity is not None] + + +def build_resolution_service( + *, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: FolderService | None, + plan: EntityResolutionPlan, + service_factory: Callable[..., object], +) -> object: + return service_factory( + config=config, + execution_context=execution_context, + folders_service=folders_service, + folders_map=plan.folders_map, + entity_name_overrides=plan.effective_entity_names, + routing_context=plan.routing_context, + ) diff --git a/packages/uipath-platform/src/uipath/platform/entities/entities.py b/packages/uipath-platform/src/uipath/platform/entities/entities.py index b2c49b763..b14f308d7 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/entities.py +++ b/packages/uipath-platform/src/uipath/platform/entities/entities.py @@ -1,11 +1,26 @@ """Entities models for UiPath Platform API interactions.""" +from __future__ import annotations + from enum import Enum from types import EllipsisType -from typing import Any, Dict, List, Optional, Type, Union, get_args, get_origin +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Type, + Union, + get_args, + get_origin, +) from pydantic import BaseModel, ConfigDict, Field, create_model +if TYPE_CHECKING: + from ._entities_service import EntitiesService + class ReferenceType(Enum): """Enum representing types of references between entities.""" @@ -342,4 +357,27 @@ class QueryRoutingOverrideContext(BaseModel): entity_routings: List[EntityRouting] = Field(alias="entityRoutings") +class DataFabricEntityItem(BaseModel): + """A single Data Fabric entity reference from agent configuration.""" + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + id: str + entity_key: Optional[str] = Field(None, alias="referenceKey") + name: str + folder_key: str = Field(alias="folderId") + description: Optional[str] = None + + +class EntitySetResolution(BaseModel): + """Result of resolving an agent entity set with overwrites applied.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + entities: list[Entity] + entities_service: EntitiesService + + Entity.model_rebuild() diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 8a9abafef..d71971591 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -8,7 +8,11 @@ from pytest_httpx import HTTPXMock from uipath.platform import UiPathApiConfig, UiPathExecutionContext -from uipath.platform.entities import Entity, EntityRouting, QueryRoutingOverrideContext +from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, +) +from uipath.platform.entities import DataFabricEntityItem, Entity from uipath.platform.entities._entities_service import EntitiesService @@ -390,28 +394,21 @@ async def test_query_entity_records_async_calls_request_for_valid_sql( assert result == [{"id": "c1"}] service.request_async.assert_called_once() - def test_query_entity_records_with_routing_context( + def test_query_entity_records_builds_routing_context_from_folders_map( self, - service: EntitiesService, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={"Customers": "solution_folder", "Orders": "folder-2"}, + ) response = MagicMock() response.json.return_value = {"results": [{"id": 1}]} service.request = MagicMock(return_value=response) # type: ignore[method-assign] - routing = QueryRoutingOverrideContext( - entity_routings=[ - EntityRouting(entity_name="Customers", folder_id="folder-1"), - EntityRouting( - entity_name="Orders", - folder_id="folder-2", - override_entity_name="OrdersV2", - ), - ] - ) - - result = service.query_entity_records( - "SELECT id FROM Customers LIMIT 10", routing_context=routing - ) + result = service.query_entity_records("SELECT id FROM Customers LIMIT 10") assert result == [{"id": 1}] call_kwargs = service.request.call_args @@ -419,39 +416,38 @@ def test_query_entity_records_with_routing_context( assert body["query"] == "SELECT id FROM Customers LIMIT 10" assert body["routingContext"] == { "entityRoutings": [ - {"entityName": "Customers", "folderId": "folder-1"}, - { - "entityName": "Orders", - "folderId": "folder-2", - "overrideEntityName": "OrdersV2", - }, + {"entityName": "Customers", "folderId": "solution_folder"}, + {"entityName": "Orders", "folderId": "folder-2"}, ] } @pytest.mark.anyio - async def test_query_entity_records_async_with_routing_context( + async def test_query_entity_records_async_builds_routing_context_from_folders_map( self, - service: EntitiesService, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={"Customers": "solution_folder"}, + ) response = MagicMock() response.json.return_value = {"results": [{"id": "c1"}]} service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] - routing = QueryRoutingOverrideContext( - entity_routings=[ - EntityRouting(entity_name="Customers", folder_id="folder-1"), - ] - ) - result = await service.query_entity_records_async( - "SELECT id FROM Customers WHERE id = 'c1'", - routing_context=routing, + "SELECT id FROM Customers WHERE id = 'c1'" ) assert result == [{"id": "c1"}] call_kwargs = service.request_async.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") - assert "routingContext" in body + assert body["routingContext"] == { + "entityRoutings": [ + {"entityName": "Customers", "folderId": "solution_folder"}, + ] + } def test_query_entity_records_without_routing_context_omits_key( self, @@ -466,3 +462,316 @@ def test_query_entity_records_without_routing_context_omits_key( call_kwargs = service.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert "routingContext" not in body + + def test_query_entity_records_picks_up_entity_overwrites_from_context( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + service = EntitiesService( + config=config, + execution_context=execution_context, + ) + response = MagicMock() + response.json.return_value = {"results": [{"id": 1}]} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_id="overwritten-folder-id", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "overwritten-folder-id", + "overrideEntityName": "Overwritten Customers", + }, + ] + } + + def test_query_entity_records_merges_folders_map_with_entity_name_overrides( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_map={ + "Customers": "overwritten-folder-id", + "Orders": "orders-folder", + }, + entity_name_overrides={"Customers": "Overwritten Customers"}, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + routings = body["routingContext"]["entityRoutings"] + assert { + "entityName": "Customers", + "folderId": "overwritten-folder-id", + "overrideEntityName": "Overwritten Customers", + } in routings + assert {"entityName": "Orders", "folderId": "orders-folder"} in routings + # Exactly two routings — no duplicates + assert len(routings) == 2 + + def test_resolve_entity_set_uses_effective_sql_name_in_routing_context( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + service = EntitiesService( + config=config, + execution_context=execution_context, + ) + service.retrieve_by_name = MagicMock( # type: ignore[method-assign] + return_value=MagicMock(spec=Entity) + ) + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_id="known-folder-key", + ) + token = _resource_overwrites.set({"entity.entity-1": overwrite}) + try: + resolution = service.resolve_entity_set( + [ + DataFabricEntityItem( + id="entity-1", + name="Customers", + folder_key="original-folder-key", + ) + ] + ) + finally: + _resource_overwrites.reset(token) + + assert resolution.entities_service._routing_strategy.routing_context is not None + assert resolution.entities_service._routing_strategy.routing_context.model_dump( + by_alias=True, exclude_none=True + ) == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "known-folder-key", + "overrideEntityName": "Overwritten Customers", + } + ] + } + service.retrieve_by_name.assert_called_once_with( + "Overwritten Customers", + "known-folder-key", + ) + + @pytest.mark.asyncio + async def test_resolve_entity_set_async_resolves_folder_paths_before_fetch( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + folders_service = MagicMock() + folders_service.retrieve_key_async = AsyncMock( + return_value="resolved-folder-id" + ) + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + ) + service.retrieve_by_name_async = AsyncMock( # type: ignore[method-assign] + return_value=MagicMock(spec=Entity) + ) + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_path="Shared/Finance", + ) + token = _resource_overwrites.set({"entity.entity-1": overwrite}) + try: + resolution = await service.resolve_entity_set_async( + [ + DataFabricEntityItem( + id="entity-1", + name="Customers", + folder_key="original-folder-key", + ) + ] + ) + finally: + _resource_overwrites.reset(token) + + folders_service.retrieve_key_async.assert_awaited_once_with( + folder_path="Shared/Finance" + ) + assert resolution.entities_service._routing_strategy.routing_context is not None + assert resolution.entities_service._routing_strategy.routing_context.model_dump( + by_alias=True, exclude_none=True + ) == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "resolved-folder-id", + "overrideEntityName": "Overwritten Customers", + } + ] + } + service.retrieve_by_name_async.assert_awaited_once_with( + "Overwritten Customers", + "resolved-folder-id", + ) + + def test_query_entity_records_context_overwrite_same_name_no_override_field( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + service = EntitiesService( + config=config, + execution_context=execution_context, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Customers", + folder_id="different-folder-id", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "different-folder-id", + }, + ] + } + + def test_query_entity_records_resolves_overwrite_folder_path_to_folder_key( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + folders_service = MagicMock() + folders_service.retrieve_key.return_value = "resolved-folder-id" + + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_path="Shared/Finance", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "resolved-folder-id", + "overrideEntityName": "Overwritten Customers", + }, + ] + } + + def test_query_entity_records_uses_folder_id_directly_without_resolution( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + from uipath.platform.common._bindings import ( + EntityResourceOverwrite, + _resource_overwrites, + ) + + folders_service = MagicMock() + folders_service.retrieve_key.return_value = None + + service = EntitiesService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + ) + response = MagicMock() + response.json.return_value = {"results": []} + service.request = MagicMock(return_value=response) # type: ignore[method-assign] + + overwrite = EntityResourceOverwrite( + resource_type="entity", + name="Overwritten Customers", + folder_id="known-folder-key", + ) + token = _resource_overwrites.set({"entity.Customers": overwrite}) + try: + service.query_entity_records("SELECT id FROM Customers LIMIT 10") + finally: + _resource_overwrites.reset(token) + + # folder_id is a key — should NOT be sent through FolderService + folders_service.retrieve_key.assert_not_called() + + call_kwargs = service.request.call_args + body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") + assert body["routingContext"] == { + "entityRoutings": [ + { + "entityName": "Customers", + "folderId": "known-folder-key", + "overrideEntityName": "Overwritten Customers", + }, + ] + } diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index db41f6ec8..d31a54951 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.23" +version = "0.1.24" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 4b1b0dc3f..234a9c2d8 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.44" +version = "2.10.45" 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/_resources/SDK_REFERENCE.md b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md index 4af1b60ae..2e6a4a585 100644 --- a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md @@ -500,10 +500,16 @@ sdk.entities.list_records(entity_key: str, schema: Optional[Type[Any]]=None, sta sdk.entities.list_records_async(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.EntityRecord] # Query entity records using a validated SQL query. -sdk.entities.query_entity_records(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] # Asynchronously query entity records using a validated SQL query. -sdk.entities.query_entity_records_async(sql_query: str, routing_context: Optional[uipath.platform.entities.entities.QueryRoutingOverrideContext]=None) -> typing.List[typing.Dict[str, typing.Any]] +sdk.entities.query_entity_records_async(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] + +# Resolve an agent entity set, applying resource overwrites. +sdk.entities.resolve_entity_set(items: list[uipath.platform.entities.entities.DataFabricEntityItem]) -> uipath.platform.entities.entities.EntitySetResolution + +# Resolve an agent entity set, applying resource overwrites. +sdk.entities.resolve_entity_set_async(items: list[uipath.platform.entities.entities.DataFabricEntityItem]) -> uipath.platform.entities.entities.EntitySetResolution # Retrieve an entity by its key. sdk.entities.retrieve(entity_key: str) -> uipath.platform.entities.entities.Entity @@ -511,12 +517,19 @@ sdk.entities.retrieve(entity_key: str) -> uipath.platform.entities.entities.Enti # Asynchronously retrieve an entity by its key. sdk.entities.retrieve_async(entity_key: str) -> uipath.platform.entities.entities.Entity +# Retrieve an entity by its name. +sdk.entities.retrieve_by_name(entity_name: str, folder_key: Optional[str]=None) -> uipath.platform.entities.entities.Entity + +# Asynchronously retrieve an entity by its name. +sdk.entities.retrieve_by_name_async(entity_name: str, folder_key: Optional[str]=None) -> uipath.platform.entities.entities.Entity + # Update multiple records in an entity in a single batch operation. sdk.entities.update_records(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse # Asynchronously update multiple records in an entity in a single batch operation. sdk.entities.update_records_async(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse + ``` ### Folders diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index c5a63af7c..f3121d9dd 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -35,6 +35,7 @@ ) from uipath.eval.mocks import ExampleCall from uipath.platform.connections import Connection +from uipath.platform.entities import DataFabricEntityItem from uipath.platform.guardrails import ( BuiltInValidatorGuardrail, ) @@ -394,16 +395,6 @@ class AgentContextSettings(BaseCfg): ) -class DataFabricEntityItem(BaseCfg): - """A single Data Fabric entity reference.""" - - id: str - reference_key: Optional[str] = Field(None, alias="referenceKey") - name: str - folder_id: str = Field(alias="folderId") - description: Optional[str] = None - - class AgentContextResourceConfig(BaseAgentResourceConfig): """Agent context resource configuration model.""" diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 1ddd7d102..d730f7e31 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3529,10 +3529,10 @@ def test_datafabric_context_config_parses(self): assert len(parsed.entity_set) == 2 assert parsed.entity_set[0].id == "abc-123" assert parsed.entity_set[0].name == "Customers" - assert parsed.entity_set[0].folder_id == "folder-1" + assert parsed.entity_set[0].folder_key == "folder-1" assert parsed.entity_set[0].description == "Customer records" - assert parsed.entity_set[0].reference_key is None - assert parsed.entity_set[1].reference_key == "orders-ref" + assert parsed.entity_set[0].entity_key is None + assert parsed.entity_set[1].entity_key == "orders-ref" assert parsed.entity_set[1].description is None def test_is_datafabric(self): diff --git a/packages/uipath/tests/resource_overrides/overwrites.json b/packages/uipath/tests/resource_overrides/overwrites.json index c58744a69..e0bca84ba 100644 --- a/packages/uipath/tests/resource_overrides/overwrites.json +++ b/packages/uipath/tests/resource_overrides/overwrites.json @@ -28,5 +28,9 @@ "mcpServer.mcp_server_name": { "name": "Overwritten MCP Server Name", "folderPath": "Overwritten/MCPServer/Folder" + }, + "entity.entity_name": { + "name": "Overwritten Entity Name", + "folderId": "overwritten-entity-folder-id-123" } } \ No newline at end of file diff --git a/packages/uipath/tests/resource_overrides/test_resource_overrides.py b/packages/uipath/tests/resource_overrides/test_resource_overrides.py index c15bc113b..8d39a762d 100644 --- a/packages/uipath/tests/resource_overrides/test_resource_overrides.py +++ b/packages/uipath/tests/resource_overrides/test_resource_overrides.py @@ -310,6 +310,11 @@ def test_parse_overwrites_with_type_adapter(self, overwrites_data): assert mcp_server.resource_identifier == "Overwritten MCP Server Name" assert mcp_server.folder_identifier == "Overwritten/MCPServer/Folder" + # Verify entity overwrite + entity = parsed_overwrites["entity.entity_name"] + assert entity.resource_identifier == "Overwritten Entity Name" + assert entity.folder_identifier == "overwritten-entity-folder-id-123" + def test_overrides_decorator_should_pop_kwargs_dict_when_present(self): from uipath.platform.common import resource_override diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index ba40c276e..b7fed5ea5 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.44" +version = "2.10.45" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.23" +version = "0.1.24" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 48d9f4ebd10b0b0eaebeaa9aa843ef5adc339659 Mon Sep 17 00:00:00 2001 From: Radu Mihai Gheorghe Date: Thu, 9 Apr 2026 17:58:48 +0300 Subject: [PATCH 010/121] fix: remove unwanted a2a fields (#1554) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 9 +------- .../uipath/tests/agent/models/test_agent.py | 21 +++++-------------- packages/uipath/uv.lock | 2 +- 4 files changed, 8 insertions(+), 26 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 234a9c2d8..7eb42ce59 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.45" +version = "2.10.46" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index f3121d9dd..679ddb808 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -467,17 +467,10 @@ class AgentA2aResourceConfig(BaseAgentResourceConfig): ) id: str slug: str = Field(..., alias="slug") - agent_card_url: str = Field(default="", alias="agentCardUrl") - is_active: bool = Field(default=True, alias="isActive") + folder_path: str = Field(alias="folderPath") cached_agent_card: Optional[Dict[str, Any]] = Field( default=None, alias="cachedAgentCard" ) - created_at: Optional[str] = Field(default=None, alias="createdAt") - created_by: Optional[str] = Field(default=None, alias="createdBy") - updated_at: Optional[str] = Field(default=None, alias="updatedAt") - updated_by: Optional[str] = Field(default=None, alias="updatedBy") - a2a_url: str = Field(default="", alias="a2aUrl") - folder_path: Optional[str] = Field(default=None, alias="folderPath") _RECIPIENT_TYPE_NORMALIZED_MAP: Mapping[int | str, AgentEscalationRecipientType] = { diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index d730f7e31..750564deb 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3614,8 +3614,7 @@ def test_a2a_resource(self): "name": "Philosopher Agent", "slug": "philosopher-agent", "description": "A philosophical agent that answers questions with wisdom and philosopher quotes", - "agentCardUrl": "", - "isActive": True, + "folderPath": "shared", "cachedAgentCard": { "name": "Philosopher Agent", "description": "Philosopher Agent assistant", @@ -3666,10 +3665,6 @@ def test_a2a_resource(self): ], "version": "0.7.70", }, - "createdAt": "2026-03-15T10:12:47.9073065", - "createdBy": "f4bc4946-baed-4083-82b9-03d334bbacbe", - "updatedAt": None, - "updatedBy": None, } ], "features": [], @@ -3693,13 +3688,8 @@ def test_a2a_resource(self): a2a_resource.description == "A philosophical agent that answers questions with wisdom and philosopher quotes" ) - assert a2a_resource.is_active is True - assert a2a_resource.agent_card_url == "" assert a2a_resource.id == "755e2f7d-5a3d-47f3-8e9d-7ff0bf226357" - assert a2a_resource.created_at == "2026-03-15T10:12:47.9073065" - assert a2a_resource.created_by == "f4bc4946-baed-4083-82b9-03d334bbacbe" - assert a2a_resource.updated_at is None - assert a2a_resource.updated_by is None + assert a2a_resource.folder_path == "shared" # Validate cached agent card is a plain dict card = a2a_resource.cached_agent_card @@ -3737,7 +3727,7 @@ def test_a2a_resource_without_cached_card(self): "name": "Minimal A2A Agent", "slug": "minimal-a2a", "description": "A minimal A2A agent", - "isActive": False, + "folderPath": "shared", } ], "features": [], @@ -3756,10 +3746,8 @@ def test_a2a_resource_without_cached_card(self): assert isinstance(a2a_resource, AgentA2aResourceConfig) assert a2a_resource.name == "Minimal A2A Agent" assert a2a_resource.slug == "minimal-a2a" - assert a2a_resource.is_active is False + assert a2a_resource.folder_path == "shared" assert a2a_resource.cached_agent_card is None - assert a2a_resource.agent_card_url == "" - assert a2a_resource.created_at is None def test_a2a_resource_case_insensitive(self): """Test that A2A resource type is parsed case-insensitively.""" @@ -3785,6 +3773,7 @@ def test_a2a_resource_case_insensitive(self): "name": "Case Test Agent", "slug": "case-test", "description": "Testing case insensitive parsing", + "folderPath": "shared", } ], "features": [], diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index b7fed5ea5..3c06d2318 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.45" +version = "2.10.46" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 39d51e4cf4b5a380baff5cddbffa7610da0607a6 Mon Sep 17 00:00:00 2001 From: Chibi Vikramathithan Date: Thu, 9 Apr 2026 14:02:18 -0700 Subject: [PATCH 011/121] fix: use agent model for eval simulations (#1555) Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Akshaya Shanbhogue --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/cli_debug.py | 12 ++++++++ packages/uipath/src/uipath/_cli/cli_eval.py | 23 -------------- packages/uipath/src/uipath/eval/helpers.py | 23 ++++++++++++++ .../src/uipath/eval/mocks/_input_mocker.py | 9 ++++++ .../src/uipath/eval/mocks/_llm_mocker.py | 9 +++++- .../src/uipath/eval/mocks/_mock_runtime.py | 15 ++++++++-- .../uipath/src/uipath/eval/runtime/runtime.py | 30 +++++++++++++++++-- .../cli/eval/test_eval_runtime_metadata.py | 22 +++++++------- .../uipath/tests/cli/test_debug_simulation.py | 6 ++++ packages/uipath/uv.lock | 2 +- 11 files changed, 110 insertions(+), 43 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7eb42ce59..25c704503 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.46" +version = "2.10.47" 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_debug.py b/packages/uipath/src/uipath/_cli/cli_debug.py index 44415dd26..d2e08353b 100644 --- a/packages/uipath/src/uipath/_cli/cli_debug.py +++ b/packages/uipath/src/uipath/_cli/cli_debug.py @@ -9,6 +9,7 @@ from uipath._cli._utils._studio_project import StudioClient from uipath.core.tracing import UiPathTraceManager from uipath.eval.mocks import UiPathMockRuntime +from uipath.eval.mocks._mock_runtime import load_simulation_config from uipath.platform.common import ResourceOverwritesContext, UiPathConfig from uipath.runtime import ( UiPathExecuteOptions, @@ -163,8 +164,19 @@ async def execute_debug_runtime(): trigger_poll_interval=trigger_poll_interval, ) + # Build mocking context with agent model for simulations + schema = await runtime.get_schema() + agent_model = None + if schema.metadata and "settings" in schema.metadata: + agent_model = schema.metadata["settings"].get("model") + + mocking_context = load_simulation_config( + agent_model=agent_model + ) + mock_runtime = UiPathMockRuntime( delegate=debug_runtime, + mocking_context=mocking_context, ) try: diff --git a/packages/uipath/src/uipath/_cli/cli_eval.py b/packages/uipath/src/uipath/_cli/cli_eval.py index ef1edb200..9ebe275dc 100644 --- a/packages/uipath/src/uipath/_cli/cli_eval.py +++ b/packages/uipath/src/uipath/_cli/cli_eval.py @@ -25,7 +25,6 @@ from uipath.runtime import ( UiPathRuntimeContext, UiPathRuntimeFactoryRegistry, - UiPathRuntimeSchema, ) from uipath.telemetry._track import flush_events from uipath.tracing import ( @@ -65,27 +64,6 @@ def setup_reporting_prereq(no_report: bool) -> bool: return True -def _get_agent_model(schema: UiPathRuntimeSchema) -> str | None: - """Get agent model from the runtime schema metadata. - - The model is read from schema.metadata["settings"]["model"] which is - populated by the low-code agents runtime from agent.json. - - Returns: - The model name from agent settings, or None if not found. - """ - try: - if schema.metadata and "settings" in schema.metadata: - settings = schema.metadata["settings"] - model = settings.get("model") - if model: - logger.debug(f"Got agent model from schema.metadata: {model}") - return model - return None - except Exception: - return None - - def _resolve_model_settings_override( model_settings_id: str, evaluation_set: EvaluationSet ) -> dict[str, Any] | None: @@ -431,7 +409,6 @@ async def execute_eval(): eval_context.evaluators = await EvalHelpers.load_evaluators( resolved_eval_set_path, eval_context.evaluation_set, - _get_agent_model(eval_context.runtime_schema), ) # Runtime is not required anymore. diff --git a/packages/uipath/src/uipath/eval/helpers.py b/packages/uipath/src/uipath/eval/helpers.py index 0a0a0ca7f..8405e4a7a 100644 --- a/packages/uipath/src/uipath/eval/helpers.py +++ b/packages/uipath/src/uipath/eval/helpers.py @@ -7,6 +7,8 @@ from pydantic import ValidationError +from uipath.runtime.schema import UiPathRuntimeSchema + from .evaluators.base_evaluator import GenericBaseEvaluator from .evaluators.evaluator_factory import EvaluatorFactory from .mocks._types import InputMockingStrategy, LLMMockingStrategy @@ -277,3 +279,24 @@ async def load_evaluators( ) return evaluators + + +def get_agent_model(schema: UiPathRuntimeSchema) -> str | None: + """Get agent model from the runtime schema metadata. + + The model is read from schema.metadata["settings"]["model"] which is + populated by the low-code agents runtime from agent.json. + + Returns: + The model name from agent settings, or None if not found. + """ + try: + if schema.metadata and "settings" in schema.metadata: + settings = schema.metadata["settings"] + model = settings.get("model") + if model: + logger.debug(f"Got agent model from schema.metadata: {model}") + return model + return None + except Exception: + return None diff --git a/packages/uipath/src/uipath/eval/mocks/_input_mocker.py b/packages/uipath/src/uipath/eval/mocks/_input_mocker.py index f1d253ba8..57a727ec1 100644 --- a/packages/uipath/src/uipath/eval/mocks/_input_mocker.py +++ b/packages/uipath/src/uipath/eval/mocks/_input_mocker.py @@ -1,6 +1,7 @@ """LLM Input Mocker implementation.""" import json +import logging from datetime import datetime from typing import Any @@ -9,6 +10,7 @@ from uipath.core.tracing import traced from uipath.platform import UiPath from uipath.platform.chat import UiPathLlmChatService +from uipath.platform.chat._llm_gateway_service import ChatModels from .._execution_context import eval_set_run_id_context from ._mock_context import cache_manager_context @@ -17,6 +19,8 @@ InputMockingStrategy, ) +logger = logging.getLogger(__name__) + def get_input_mocking_prompt( input_schema: str, @@ -117,6 +121,11 @@ async def generate_llm_input( else {} ) + simulation_model = completion_kwargs.get( + "model", ChatModels.gpt_4_1_mini_2025_04_14 + ) + logger.info(f"Simulating input generation using model: {simulation_model}") + if cache_manager is not None: cache_key_data = { "response_format": response_format, diff --git a/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py b/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py index 194aa6c09..3715ac226 100644 --- a/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py +++ b/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py @@ -10,7 +10,7 @@ from uipath.core.tracing import traced from uipath.platform import UiPath from uipath.platform.chat import UiPathLlmChatService -from uipath.platform.chat._llm_gateway_service import _cleanup_schema +from uipath.platform.chat._llm_gateway_service import ChatModels, _cleanup_schema from .._execution_context import ( eval_set_run_id_context, @@ -182,6 +182,13 @@ async def response( else {} ) + simulation_model = completion_kwargs.get( + "model", ChatModels.gpt_4_1_mini_2025_04_14 + ) + logger.info( + f"Simulating tool '{function_name}' using model: {simulation_model}" + ) + formatted_prompt = PROMPT.format(**prompt_generation_args) cache_key_data = { diff --git a/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py b/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py index 512d8d6ee..df41dadeb 100644 --- a/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py +++ b/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py @@ -28,13 +28,14 @@ LLMMockingStrategy, MockingContext, MockingStrategyType, + ModelSettings, ToolSimulation, ) logger = logging.getLogger(__name__) -def load_simulation_config() -> MockingContext | None: +def load_simulation_config(agent_model: str | None = None) -> MockingContext | None: """Load simulation.json from current directory and convert to MockingContext. Returns: @@ -63,11 +64,21 @@ def load_simulation_config() -> MockingContext | None: if not tools_to_simulate: return None - # Create LLM mocking strategy + # Honor model from simulation config if specified, otherwise use the agent model + simulation_model = simulation_data.get("model") + model = ( + ModelSettings(model=simulation_model) + if simulation_model + else ModelSettings(model=agent_model) + if agent_model + else None + ) + mocking_strategy = LLMMockingStrategy( type=MockingStrategyType.LLM, prompt=simulation_data.get("instructions", ""), tools_to_simulate=tools_to_simulate, + model=model, ) # Create MockingContext for debugging diff --git a/packages/uipath/src/uipath/eval/runtime/runtime.py b/packages/uipath/src/uipath/eval/runtime/runtime.py index 1c32b9efe..7f7614446 100644 --- a/packages/uipath/src/uipath/eval/runtime/runtime.py +++ b/packages/uipath/src/uipath/eval/runtime/runtime.py @@ -47,13 +47,14 @@ from .._execution_context import ExecutionSpanCollector from ..evaluators.base_evaluator import GenericBaseEvaluator from ..evaluators.output_evaluator import OutputEvaluationCriteria +from ..helpers import get_agent_model from ..mocks._cache_manager import CacheManager from ..mocks._input_mocker import ( generate_llm_input, ) from ..mocks._mock_context import cache_manager_context from ..mocks._mock_runtime import UiPathMockRuntime -from ..mocks._types import MockingContext +from ..mocks._types import LLMMockingStrategy, MockingContext, ModelSettings from ..models import EvaluationResult from ..models.evaluation_set import ( EvaluationItem, @@ -526,12 +527,25 @@ async def _execute_eval( eval_item=eval_item, ), ) + # Set agent model on the mocking strategy if not already set + mocking_strategy = eval_item.mocking_strategy + if ( + mocking_strategy + and isinstance(mocking_strategy, LLMMockingStrategy) + and not mocking_strategy.model + ): + mocking_model = get_agent_model(self.context.runtime_schema) + if mocking_model: + mocking_strategy = mocking_strategy.model_copy( + update={"model": ModelSettings(model=mocking_model)} + ) + agent_execution_output = await self.execute_runtime( eval_item, execution_id, input_overrides=self.context.input_overrides, mocking_context=MockingContext( - strategy=eval_item.mocking_strategy, + strategy=mocking_strategy, name=eval_item.name, inputs=eval_item.inputs, ), @@ -811,8 +825,18 @@ async def _generate_input_for_eval( or getattr(eval_item, "expected_output", None) or {} ) + # Set agent model on the input mocking strategy if not already set + input_strategy = eval_item.input_mocking_strategy + # If input strategy does not specify a model, extract it + if input_strategy and not input_strategy.model: + input_generation_model = get_agent_model(self.context.runtime_schema) + if input_generation_model: + input_strategy = input_strategy.model_copy( + update={"model": ModelSettings(model=input_generation_model)} + ) + generated_input = await generate_llm_input( - eval_item.input_mocking_strategy, + input_strategy, (await self.get_schema()).input, expected_behavior=eval_item.expected_agent_behavior or "", expected_output=expected_output, diff --git a/packages/uipath/tests/cli/eval/test_eval_runtime_metadata.py b/packages/uipath/tests/cli/eval/test_eval_runtime_metadata.py index 112f8774b..07042cc12 100644 --- a/packages/uipath/tests/cli/eval/test_eval_runtime_metadata.py +++ b/packages/uipath/tests/cli/eval/test_eval_runtime_metadata.py @@ -1,7 +1,7 @@ """Tests for UiPathEvalRuntime metadata loading functionality. This module tests: -- _get_agent_model() - cached agent model retrieval +- get_agent_model() - cached agent model retrieval - get_schema() - cached schema retrieval """ @@ -10,11 +10,9 @@ import pytest -from uipath._cli.cli_eval import ( - _get_agent_model, -) from uipath.core.events import EventBus from uipath.core.tracing import UiPathTraceManager +from uipath.eval.helpers import get_agent_model from uipath.eval.runtime import UiPathEvalContext, UiPathEvalRuntime from uipath.runtime import ( UiPathExecuteOptions, @@ -119,34 +117,34 @@ async def dispose(self) -> None: class TestGetAgentModel: - """Tests for _get_agent_model function.""" + """Tests for get_agent_model function.""" @pytest.mark.asyncio async def test_returns_agent_model(self): - """Test that _get_agent_model returns the correct model from schema.""" + """Test that get_agent_model returns the correct model from schema.""" schema = MockRuntimeSchema() schema.metadata = {"settings": {"model": "gpt-4o-2024-11-20"}} - model = _get_agent_model(schema) + model = get_agent_model(schema) assert model == "gpt-4o-2024-11-20" @pytest.mark.asyncio async def test_returns_none_when_no_model(self): - """Test that _get_agent_model returns None when runtime has no model.""" + """Test that get_agent_model returns None when runtime has no model.""" schema = MockRuntimeSchema() - model = _get_agent_model(schema) + model = get_agent_model(schema) assert model is None @pytest.mark.asyncio async def test_returns_model_consistently(self): - """Test that _get_agent_model returns consistent results.""" + """Test that get_agent_model returns consistent results.""" schema = MockRuntimeSchema() schema.metadata = {"settings": {"model": "consistent-model"}} # Multiple calls should return the same value - model1 = _get_agent_model(schema) - model2 = _get_agent_model(schema) + model1 = get_agent_model(schema) + model2 = get_agent_model(schema) assert model1 == model2 == "consistent-model" diff --git a/packages/uipath/tests/cli/test_debug_simulation.py b/packages/uipath/tests/cli/test_debug_simulation.py index b2d795c79..d9266327b 100644 --- a/packages/uipath/tests/cli/test_debug_simulation.py +++ b/packages/uipath/tests/cli/test_debug_simulation.py @@ -241,6 +241,9 @@ def test_debug_always_wraps_with_mock_runtime( ) as mock_factory_get: mock_runtime = Mock() mock_runtime.dispose = AsyncMock() + mock_runtime.get_schema = AsyncMock( + return_value=Mock(metadata=None) + ) mock_factory = Mock() mock_factory.new_runtime = AsyncMock(return_value=mock_runtime) @@ -305,6 +308,9 @@ def test_debug_wraps_with_mock_runtime_on_error( ) as mock_factory_get: mock_runtime = Mock() mock_runtime.dispose = AsyncMock() + mock_runtime.get_schema = AsyncMock( + return_value=Mock(metadata=None) + ) mock_factory = Mock() mock_factory.new_runtime = AsyncMock(return_value=mock_runtime) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 3c06d2318..f956b3d82 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.46" +version = "2.10.47" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From a3509b28068fa060858fb6871f04ac25e375f1b5 Mon Sep 17 00:00:00 2001 From: Chibi Vikramathithan Date: Thu, 9 Apr 2026 16:59:23 -0700 Subject: [PATCH 012/121] fix: restore agent_model for same-as-agent evaluator resolution (#1556) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/cli_eval.py | 3 ++- packages/uipath/uv.lock | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 25c704503..7a7bc20ee 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.47" +version = "2.10.48" 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_eval.py b/packages/uipath/src/uipath/_cli/cli_eval.py index 9ebe275dc..e101717d6 100644 --- a/packages/uipath/src/uipath/_cli/cli_eval.py +++ b/packages/uipath/src/uipath/_cli/cli_eval.py @@ -17,7 +17,7 @@ from uipath._cli.middlewares import Middlewares from uipath.core.events import EventBus from uipath.core.tracing import UiPathTraceManager -from uipath.eval.helpers import EVAL_SETS_DIRECTORY_NAME, EvalHelpers +from uipath.eval.helpers import EVAL_SETS_DIRECTORY_NAME, EvalHelpers, get_agent_model from uipath.eval.models.evaluation_set import EvaluationSet from uipath.eval.runtime import UiPathEvalContext, evaluate from uipath.platform.chat import set_llm_concurrency @@ -409,6 +409,7 @@ async def execute_eval(): eval_context.evaluators = await EvalHelpers.load_evaluators( resolved_eval_set_path, eval_context.evaluation_set, + get_agent_model(eval_context.runtime_schema), ) # Runtime is not required anymore. diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index f956b3d82..126658fe1 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.47" +version = "2.10.48" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From f5838a91a397b4a5fb99edfc8c2219248dfa7499 Mon Sep 17 00:00:00 2001 From: Andrei Petraru Date: Fri, 10 Apr 2026 10:18:33 +0300 Subject: [PATCH 013/121] feat(guardrails): add Azure guardrail validators [AL-372] (#1549) Co-authored-by: Claude --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath/platform/guardrails/__init__.py | 12 ++ .../guardrails/decorators/__init__.py | 18 ++- .../platform/guardrails/decorators/_enums.py | 19 +++ .../platform/guardrails/decorators/_models.py | 17 ++ .../decorators/validators/__init__.py | 6 + .../decorators/validators/harmful_content.py | 77 +++++++++ .../validators/intellectual_property.py | 67 ++++++++ .../validators/user_prompt_attacks.py | 44 ++++++ .../test_azure_guardrail_validators.py | 146 ++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 12 files changed, 407 insertions(+), 5 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/harmful_content.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/intellectual_property.py create mode 100644 packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/user_prompt_attacks.py create mode 100644 packages/uipath-platform/tests/services/test_azure_guardrail_validators.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index b54858ea6..8650e44e2 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.24" +version = "0.1.25" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py index de439e92a..93c60b0a4 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/__init__.py @@ -25,6 +25,11 @@ GuardrailExecutionStage, GuardrailTargetAdapter, GuardrailValidatorBase, + HarmfulContentEntity, + HarmfulContentEntityType, + HarmfulContentValidator, + IntellectualPropertyEntityType, + IntellectualPropertyValidator, LogAction, LoggingSeverityLevel, PIIDetectionEntity, @@ -32,6 +37,7 @@ PIIValidator, PromptInjectionValidator, RuleFunction, + UserPromptAttacksValidator, guardrail, register_guardrail_adapter, ) @@ -61,10 +67,16 @@ "GuardrailValidatorBase", "BuiltInGuardrailValidator", "CustomGuardrailValidator", + "HarmfulContentValidator", + "IntellectualPropertyValidator", "PIIValidator", "PromptInjectionValidator", + "UserPromptAttacksValidator", "CustomValidator", "RuleFunction", + "HarmfulContentEntity", + "HarmfulContentEntityType", + "IntellectualPropertyEntityType", "PIIDetectionEntity", "PIIDetectionEntityType", "GuardrailExecutionStage", diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py index 727925fb1..e8d692164 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py @@ -7,19 +7,27 @@ from ._actions import BlockAction, LogAction, LoggingSeverityLevel from ._core import GuardrailExclude -from ._enums import GuardrailExecutionStage, PIIDetectionEntityType +from ._enums import ( + GuardrailExecutionStage, + HarmfulContentEntityType, + IntellectualPropertyEntityType, + PIIDetectionEntityType, +) from ._exceptions import GuardrailBlockException from ._guardrail import guardrail -from ._models import GuardrailAction, PIIDetectionEntity +from ._models import GuardrailAction, HarmfulContentEntity, PIIDetectionEntity from ._registry import GuardrailTargetAdapter, register_guardrail_adapter from .validators import ( BuiltInGuardrailValidator, CustomGuardrailValidator, CustomValidator, GuardrailValidatorBase, + HarmfulContentValidator, + IntellectualPropertyValidator, PIIValidator, PromptInjectionValidator, RuleFunction, + UserPromptAttacksValidator, ) __all__ = [ @@ -29,11 +37,17 @@ "GuardrailValidatorBase", "BuiltInGuardrailValidator", "CustomGuardrailValidator", + "HarmfulContentValidator", + "IntellectualPropertyValidator", "PIIValidator", "PromptInjectionValidator", + "UserPromptAttacksValidator", "CustomValidator", "RuleFunction", # Models & enums + "HarmfulContentEntity", + "HarmfulContentEntityType", + "IntellectualPropertyEntityType", "PIIDetectionEntity", "PIIDetectionEntityType", "GuardrailExecutionStage", diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py index be7832ddf..49956f62f 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py @@ -42,3 +42,22 @@ class PIIDetectionEntityType(str, Enum): USUK_PASSPORT_NUMBER = "UsukPassportNumber" URL = "URL" IP_ADDRESS = "IPAddress" + + +class HarmfulContentEntityType(str, Enum): + """Harmful content entity types supported by UiPath guardrails. + + These entities correspond to the Azure Content Safety categories. + """ + + HATE = "Hate" + SELF_HARM = "SelfHarm" + SEXUAL = "Sexual" + VIOLENCE = "Violence" + + +class IntellectualPropertyEntityType(str, Enum): + """Intellectual property entity types supported by UiPath guardrails.""" + + TEXT = "Text" + CODE = "Code" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_models.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_models.py index ac22538e0..8d86fbf39 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_models.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_models.py @@ -26,6 +26,23 @@ def __post_init__(self) -> None: ) +@dataclass +class HarmfulContentEntity: + """Harmful content entity configuration with severity threshold. + + Args: + name: The entity type name (e.g. ``HarmfulContentEntityType.VIOLENCE``). + threshold: Severity threshold (0 to 6) for detection. Defaults to ``2``. + """ + + name: str + threshold: int = 2 + + def __post_init__(self) -> None: + if not 0 <= self.threshold <= 6: + raise ValueError(f"Threshold must be between 0 and 6, got {self.threshold}") + + class GuardrailAction(ABC): """Interface for defining custom actions when a guardrail violation is detected. diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py index 6be170534..bbcf29039 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/__init__.py @@ -6,15 +6,21 @@ GuardrailValidatorBase, ) from .custom import CustomValidator, RuleFunction +from .harmful_content import HarmfulContentValidator +from .intellectual_property import IntellectualPropertyValidator from .pii import PIIValidator from .prompt_injection import PromptInjectionValidator +from .user_prompt_attacks import UserPromptAttacksValidator __all__ = [ "GuardrailValidatorBase", "BuiltInGuardrailValidator", "CustomGuardrailValidator", + "HarmfulContentValidator", + "IntellectualPropertyValidator", "PIIValidator", "PromptInjectionValidator", + "UserPromptAttacksValidator", "CustomValidator", "RuleFunction", ] diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/harmful_content.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/harmful_content.py new file mode 100644 index 000000000..d186341d7 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/harmful_content.py @@ -0,0 +1,77 @@ +"""Harmful content detection guardrail validator.""" + +from typing import Any, Sequence +from uuid import uuid4 + +from uipath.platform.guardrails.guardrails import ( + BuiltInValidatorGuardrail, + EnumListParameterValue, + MapEnumParameterValue, +) + +from .._models import HarmfulContentEntity +from ._base import BuiltInGuardrailValidator + + +class HarmfulContentValidator(BuiltInGuardrailValidator): + """Validate data for harmful content using the UiPath API. + + Supported at all stages (PRE, POST, PRE_AND_POST). + + Args: + entities: One or more :class:`~uipath.platform.guardrails.decorators.HarmfulContentEntity` + instances specifying which harmful content categories to detect + and their severity thresholds. + + Raises: + ValueError: If *entities* is empty. + """ + + def __init__(self, entities: Sequence[HarmfulContentEntity]) -> None: + """Initialize HarmfulContentValidator with entities to detect.""" + if not entities: + raise ValueError("entities must be provided and non-empty") + self.entities = list(entities) + + def get_built_in_guardrail( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + ) -> BuiltInValidatorGuardrail: + """Build a harmful content :class:`BuiltInValidatorGuardrail`. + + Args: + name: Name for the guardrail. + description: Optional description. + enabled_for_evals: Whether active in evaluation scenarios. + + Returns: + Configured :class:`BuiltInValidatorGuardrail` for harmful content detection. + """ + entity_names = [entity.name for entity in self.entities] + entity_thresholds: dict[str, Any] = { + entity.name: entity.threshold for entity in self.entities + } + + return BuiltInValidatorGuardrail( + id=str(uuid4()), + name=name, + description=description + or f"Detects harmful content: {', '.join(entity_names)}", + enabled_for_evals=enabled_for_evals, + guardrail_type="builtInValidator", + validator_type="harmful_content", + validator_parameters=[ + EnumListParameterValue( + parameter_type="enum-list", + id="harmfulContentEntities", + value=entity_names, + ), + MapEnumParameterValue( + parameter_type="map-enum", + id="harmfulContentEntityThresholds", + value=entity_thresholds, + ), + ], + ) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/intellectual_property.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/intellectual_property.py new file mode 100644 index 000000000..8a18e6a37 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/intellectual_property.py @@ -0,0 +1,67 @@ +"""Intellectual property detection guardrail validator.""" + +from typing import Sequence +from uuid import uuid4 + +from uipath.platform.guardrails.guardrails import ( + BuiltInValidatorGuardrail, + EnumListParameterValue, +) + +from .._enums import GuardrailExecutionStage +from ._base import BuiltInGuardrailValidator + + +class IntellectualPropertyValidator(BuiltInGuardrailValidator): + """Validate output for intellectual property violations using the UiPath API. + + Restricted to POST stage only — IP detection is an output-only concern. + + Args: + entities: One or more entity type strings (e.g. + ``IntellectualPropertyEntityType.TEXT``). + + Raises: + ValueError: If *entities* is empty. + """ + + supported_stages = [GuardrailExecutionStage.POST] + + def __init__(self, entities: Sequence[str]) -> None: + """Initialize IntellectualPropertyValidator with entities to detect.""" + if not entities: + raise ValueError("entities must be provided and non-empty") + self.entities = list(entities) + + def get_built_in_guardrail( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + ) -> BuiltInValidatorGuardrail: + """Build an intellectual property :class:`BuiltInValidatorGuardrail`. + + Args: + name: Name for the guardrail. + description: Optional description. + enabled_for_evals: Whether active in evaluation scenarios. + + Returns: + Configured :class:`BuiltInValidatorGuardrail` for IP detection. + """ + return BuiltInValidatorGuardrail( + id=str(uuid4()), + name=name, + description=description + or f"Detects intellectual property: {', '.join(self.entities)}", + enabled_for_evals=enabled_for_evals, + guardrail_type="builtInValidator", + validator_type="intellectual_property", + validator_parameters=[ + EnumListParameterValue( + parameter_type="enum-list", + id="ipEntities", + value=self.entities, + ), + ], + ) diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/user_prompt_attacks.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/user_prompt_attacks.py new file mode 100644 index 000000000..7275acc25 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/user_prompt_attacks.py @@ -0,0 +1,44 @@ +"""User prompt attacks detection guardrail validator.""" + +from uuid import uuid4 + +from uipath.platform.guardrails.guardrails import BuiltInValidatorGuardrail + +from .._enums import GuardrailExecutionStage +from ._base import BuiltInGuardrailValidator + + +class UserPromptAttacksValidator(BuiltInGuardrailValidator): + """Validate input for user prompt attacks via the UiPath API. + + Restricted to PRE stage only — prompt attacks are an input-only concern. + Takes no parameters. + """ + + supported_stages = [GuardrailExecutionStage.PRE] + + def get_built_in_guardrail( + self, + name: str, + description: str | None, + enabled_for_evals: bool, + ) -> BuiltInValidatorGuardrail: + """Build a user prompt attacks :class:`BuiltInValidatorGuardrail`. + + Args: + name: Name for the guardrail. + description: Optional description. + enabled_for_evals: Whether active in evaluation scenarios. + + Returns: + Configured :class:`BuiltInValidatorGuardrail` for user prompt attacks. + """ + return BuiltInValidatorGuardrail( + id=str(uuid4()), + name=name, + description=description or "Detects user prompt attacks", + enabled_for_evals=enabled_for_evals, + guardrail_type="builtInValidator", + validator_type="user_prompt_attacks", + validator_parameters=[], + ) diff --git a/packages/uipath-platform/tests/services/test_azure_guardrail_validators.py b/packages/uipath-platform/tests/services/test_azure_guardrail_validators.py new file mode 100644 index 000000000..fd6a3f39e --- /dev/null +++ b/packages/uipath-platform/tests/services/test_azure_guardrail_validators.py @@ -0,0 +1,146 @@ +"""Tests for the Azure-provided guardrail validators. + +Covers HarmfulContentValidator, IntellectualPropertyValidator, and +UserPromptAttacksValidator — verifying guardrail construction, parameter +serialization, stage enforcement, and input validation. +""" + +from __future__ import annotations + +import pytest + +from uipath.platform.guardrails.decorators import ( + GuardrailExecutionStage, + HarmfulContentEntity, + HarmfulContentEntityType, + HarmfulContentValidator, + IntellectualPropertyEntityType, + IntellectualPropertyValidator, + UserPromptAttacksValidator, +) + +# --------------------------------------------------------------------------- +# HarmfulContentValidator +# --------------------------------------------------------------------------- + + +class TestHarmfulContentValidator: + """Tests for HarmfulContentValidator.""" + + def test_builds_guardrail(self): + """Verify get_built_in_guardrail returns correct structure.""" + validator = HarmfulContentValidator( + entities=[ + HarmfulContentEntity(HarmfulContentEntityType.VIOLENCE, threshold=3), + HarmfulContentEntity(HarmfulContentEntityType.HATE, threshold=4), + ] + ) + guardrail = validator.get_built_in_guardrail( + name="Test HC", + description="test", + enabled_for_evals=True, + ) + assert guardrail.validator_type == "harmful_content" + assert len(guardrail.validator_parameters) == 2 + + enum_param = guardrail.validator_parameters[0] + assert enum_param.id == "harmfulContentEntities" + assert enum_param.value == ["Violence", "Hate"] + + map_param = guardrail.validator_parameters[1] + assert map_param.id == "harmfulContentEntityThresholds" + assert map_param.value == {"Violence": 3, "Hate": 4} + + def test_empty_entities_raises(self): + """Empty entities should raise ValueError.""" + with pytest.raises(ValueError, match="non-empty"): + HarmfulContentValidator(entities=[]) + + def test_threshold_validation(self): + """Threshold outside 0-6 should raise ValueError.""" + with pytest.raises(ValueError, match="between 0 and 6"): + HarmfulContentEntity(HarmfulContentEntityType.VIOLENCE, threshold=7) + with pytest.raises(ValueError, match="between 0 and 6"): + HarmfulContentEntity(HarmfulContentEntityType.VIOLENCE, threshold=-1) + + def test_all_stages_supported(self): + """supported_stages should be empty (all stages allowed).""" + validator = HarmfulContentValidator( + entities=[HarmfulContentEntity(HarmfulContentEntityType.VIOLENCE)] + ) + assert validator.supported_stages == [] + # Should not raise for any stage + validator.validate_stage(GuardrailExecutionStage.PRE) + validator.validate_stage(GuardrailExecutionStage.POST) + + +# --------------------------------------------------------------------------- +# IntellectualPropertyValidator +# --------------------------------------------------------------------------- + + +class TestIntellectualPropertyValidator: + """Tests for IntellectualPropertyValidator.""" + + def test_builds_guardrail(self): + """Verify get_built_in_guardrail returns correct structure.""" + validator = IntellectualPropertyValidator( + entities=[ + IntellectualPropertyEntityType.TEXT, + IntellectualPropertyEntityType.CODE, + ] + ) + guardrail = validator.get_built_in_guardrail( + name="Test IP", + description=None, + enabled_for_evals=False, + ) + assert guardrail.validator_type == "intellectual_property" + assert len(guardrail.validator_parameters) == 1 + + param = guardrail.validator_parameters[0] + assert param.id == "ipEntities" + assert param.value == ["Text", "Code"] + + def test_empty_entities_raises(self): + """Empty entities should raise ValueError.""" + with pytest.raises(ValueError, match="non-empty"): + IntellectualPropertyValidator(entities=[]) + + def test_post_only(self): + """Should only support POST stage.""" + validator = IntellectualPropertyValidator( + entities=[IntellectualPropertyEntityType.TEXT] + ) + assert validator.supported_stages == [GuardrailExecutionStage.POST] + validator.validate_stage(GuardrailExecutionStage.POST) + with pytest.raises(ValueError, match="does not support stage"): + validator.validate_stage(GuardrailExecutionStage.PRE) + + +# --------------------------------------------------------------------------- +# UserPromptAttacksValidator +# --------------------------------------------------------------------------- + + +class TestUserPromptAttacksValidator: + """Tests for UserPromptAttacksValidator.""" + + def test_builds_guardrail(self): + """Verify get_built_in_guardrail returns correct structure.""" + validator = UserPromptAttacksValidator() + guardrail = validator.get_built_in_guardrail( + name="Test UPA", + description=None, + enabled_for_evals=True, + ) + assert guardrail.validator_type == "user_prompt_attacks" + assert guardrail.validator_parameters == [] + + def test_pre_only(self): + """Should only support PRE stage.""" + validator = UserPromptAttacksValidator() + assert validator.supported_stages == [GuardrailExecutionStage.PRE] + validator.validate_stage(GuardrailExecutionStage.PRE) + with pytest.raises(ValueError, match="does not support stage"): + validator.validate_stage(GuardrailExecutionStage.POST) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index d31a54951..5133a4575 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.24" +version = "0.1.25" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 126658fe1..159e6608c 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.24" +version = "0.1.25" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From a4a6cb9b965c67e623c77adff1929db669d53c10 Mon Sep 17 00:00:00 2001 From: RobinMennens-UiPath <80457569+RobinMennens-UiPath@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:09:00 +0200 Subject: [PATCH 014/121] fix: exclude start and limit from params if not provided (#1557) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/entities/_entities_service.py | 7 +++- .../tests/services/test_entities_service.py | 35 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 8650e44e2..39a8feb36 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.25" +version = "0.1.26" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index 43b437d65..09987002d 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -1115,12 +1115,17 @@ def _list_records_spec( start: Optional[int] = None, limit: Optional[int] = None, ) -> RequestSpec: + params: dict[str, Any] = {} + if start is not None: + params["start"] = start + if limit is not None: + params["limit"] = limit return RequestSpec( method="GET", endpoint=Endpoint( f"datafabric_/api/EntityService/entity/{entity_key}/read" ), - params=({"start": start, "limit": limit}), + params=params, ) def _query_entity_records_spec( diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index d71971591..d018dd639 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -267,6 +267,41 @@ def test_retrieve_records_with_optional_fields( limit=1, ) + def test_retrieve_records_without_start_and_limit( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{str(entity_key)}/read", + status_code=200, + json={ + "totalCount": 1, + "value": [ + {"Id": "12345", "name": "record_name", "integer_field": 10}, + ], + }, + ) + + records = service.list_records(entity_key=str(entity_key)) + + sent_request = httpx_mock.get_request() + if sent_request is None: + raise Exception("No request was sent") + + # Verify no start or limit query params are sent + assert "start" not in str(sent_request.url.params) + assert "limit" not in str(sent_request.url.params) + + assert isinstance(records, list) + assert len(records) == 1 + assert records[0].id == "12345" + @pytest.mark.parametrize( "sql_query", [ diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 5133a4575..2c517d570 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.25" +version = "0.1.26" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 159e6608c..7a694afc0 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.25" +version = "0.1.26" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From c209c76b556dcd4941e5ea42551eea7f82a5808a Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 14 Apr 2026 13:45:25 +0200 Subject: [PATCH 015/121] feat: add choiceset support to entities service (#1564) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/entities/__init__.py | 2 + .../platform/entities/_entities_service.py | 121 +++++++++ .../src/uipath/platform/entities/entities.py | 19 ++ .../tests/services/test_entities_service.py | 243 +++++++++++++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 7 files changed, 387 insertions(+), 4 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 39a8feb36..a74bd7a5c 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.26" +version = "0.1.27" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/entities/__init__.py b/packages/uipath-platform/src/uipath/platform/entities/__init__.py index 6c9ac7f9f..aca80997b 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/entities/__init__.py @@ -5,6 +5,7 @@ from ._entities_service import EntitiesService from .entities import ( + ChoiceSetValue, DataFabricEntityItem, Entity, EntityField, @@ -24,6 +25,7 @@ ) __all__ = [ + "ChoiceSetValue", "DataFabricEntityItem", "EntitiesService", "Entity", diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index 09987002d..a095f430d 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -1,3 +1,4 @@ +import json as json_module import logging from typing import Any, Dict, List, Optional, Type @@ -24,6 +25,7 @@ fetch_resolved_entities_async, ) from .entities import ( + ChoiceSetValue, DataFabricEntityItem, Entity, EntityRecord, @@ -282,6 +284,99 @@ async def list_entities_async(self) -> List[Entity]: entities_data = response.json() return [Entity.model_validate(entity) for entity in entities_data] + @traced(name="list_choicesets", run_type="uipath") + def list_choicesets(self) -> List[Entity]: + """List all choice sets in Data Service. + + Returns: + List[Entity]: A list of all choice set entities. + + Examples: + List all choice sets:: + + choicesets = entities_service.list_choicesets() + for cs in choicesets: + print(f"{cs.display_name} ({cs.id})") + """ + spec = self._list_choicesets_spec() + response = self.request(spec.method, spec.endpoint) + return [Entity.model_validate(item) for item in response.json()] + + @traced(name="list_choicesets", run_type="uipath") + async def list_choicesets_async(self) -> List[Entity]: + """Asynchronously list all choice sets in Data Service. + + Returns: + List[Entity]: A list of all choice set entities. + """ + spec = self._list_choicesets_spec() + response = await self.request_async(spec.method, spec.endpoint) + return [Entity.model_validate(item) for item in response.json()] + + @traced(name="get_choiceset_values", run_type="uipath") + def get_choiceset_values( + self, + choiceset_id: str, + start: int | None = None, + limit: int | None = None, + ) -> List[ChoiceSetValue]: + """Get the values of a choice set by its ID. + + Args: + choiceset_id: The unique identifier of the choice set. + start: Optional offset for pagination. + limit: Optional page size for pagination. + + Returns: + List[ChoiceSetValue]: The values in the choice set, each containing + id, name, display_name, and number_id. + + Examples: + Get all values in a choice set:: + + values = entities_service.get_choiceset_values("choiceset-id") + for v in values: + print(f"{v.number_id}: {v.display_name}") + """ + spec = self._get_choiceset_values_spec(choiceset_id, start=start, limit=limit) + response = self.request( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + data = response.json() + raw_values = data.get("jsonValue", "[]") + items = ( + json_module.loads(raw_values) if isinstance(raw_values, str) else raw_values + ) + return [ChoiceSetValue.model_validate(item) for item in items] + + @traced(name="get_choiceset_values", run_type="uipath") + async def get_choiceset_values_async( + self, + choiceset_id: str, + start: int | None = None, + limit: int | None = None, + ) -> List[ChoiceSetValue]: + """Asynchronously get the values of a choice set by its ID. + + Args: + choiceset_id: The unique identifier of the choice set. + start: Optional offset for pagination. + limit: Optional page size for pagination. + + Returns: + List[ChoiceSetValue]: The values in the choice set. + """ + spec = self._get_choiceset_values_spec(choiceset_id, start=start, limit=limit) + response = await self.request_async( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + data = response.json() + raw_values = data.get("jsonValue", "[]") + items = ( + json_module.loads(raw_values) if isinstance(raw_values, str) else raw_values + ) + return [ChoiceSetValue.model_validate(item) for item in items] + @traced(name="entity_list_records", run_type="uipath") def list_records( self, @@ -1173,6 +1268,32 @@ def _delete_batch_spec(self, entity_key: str, record_ids: List[str]) -> RequestS json=record_ids, ) + def _list_choicesets_spec(self) -> RequestSpec: + return RequestSpec( + method="GET", + endpoint=Endpoint("datafabric_/api/Entity/choiceset"), + ) + + def _get_choiceset_values_spec( + self, + choiceset_id: str, + start: int | None = None, + limit: int | None = None, + ) -> RequestSpec: + params: dict[str, Any] = {} + if start is not None: + params["start"] = start + if limit is not None: + params["limit"] = limit + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{choiceset_id}/query_expansion" + ), + params=params, + json={}, + ) + def _validate_sql_query(self, sql_query: str) -> None: query = sql_query.strip().rstrip(";").strip() if not query: diff --git a/packages/uipath-platform/src/uipath/platform/entities/entities.py b/packages/uipath-platform/src/uipath/platform/entities/entities.py index b14f308d7..f3fb862f8 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/entities.py +++ b/packages/uipath-platform/src/uipath/platform/entities/entities.py @@ -222,6 +222,25 @@ class SourceJoinCriteria(BaseModel): related_source_field_name: str = Field(alias="relatedSourceFieldName") +class ChoiceSetValue(BaseModel): + """Model representing a single value within a choice set.""" + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + ) + + id: str = Field(alias="Id") + name: str = Field(alias="Name") + display_name: str = Field(alias="DisplayName") + number_id: int = Field(alias="NumberId") + created_time: str | None = Field(default=None, alias="CreateTime") + updated_time: str | None = Field(default=None, alias="UpdateTime") + created_by: str | None = Field(default=None, alias="CreatedBy") + updated_by: str | None = Field(default=None, alias="UpdatedBy") + record_owner: str | None = Field(default=None, alias="RecordOwner") + + class EntityRecord(BaseModel): """Model representing a record within an entity.""" diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index d018dd639..71c3ccd5b 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -1,3 +1,4 @@ +import json import re import uuid from dataclasses import make_dataclass @@ -12,7 +13,7 @@ EntityResourceOverwrite, _resource_overwrites, ) -from uipath.platform.entities import DataFabricEntityItem, Entity +from uipath.platform.entities import ChoiceSetValue, DataFabricEntityItem, Entity from uipath.platform.entities._entities_service import EntitiesService @@ -810,3 +811,243 @@ def test_query_entity_records_uses_folder_id_directly_without_resolution( }, ] } + + def test_list_choicesets( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/choiceset", + status_code=200, + json=[ + { + "name": "Status", + "displayName": "Status", + "entityType": "ChoiceSet", + "description": "Status choices", + "isRbacEnabled": False, + "id": "cs-001", + }, + { + "name": "Priority", + "displayName": "Priority", + "entityType": "ChoiceSet", + "description": "Priority levels", + "isRbacEnabled": False, + "id": "cs-002", + }, + ], + ) + + choicesets = service.list_choicesets() + + assert isinstance(choicesets, list) + assert len(choicesets) == 2 + assert choicesets[0].name == "Status" + assert choicesets[0].entity_type == "ChoiceSet" + assert choicesets[0].id == "cs-001" + assert choicesets[1].name == "Priority" + + sent_request = httpx_mock.get_request() + assert sent_request is not None + assert sent_request.method == "GET" + assert str(sent_request.url).endswith("/datafabric_/api/Entity/choiceset") + + @pytest.mark.anyio + async def test_list_choicesets_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/choiceset", + status_code=200, + json=[ + { + "name": "Role", + "displayName": "Role", + "entityType": "ChoiceSet", + "isRbacEnabled": False, + "id": "cs-003", + }, + ], + ) + + choicesets = await service.list_choicesets_async() + + assert len(choicesets) == 1 + assert choicesets[0].name == "Role" + assert choicesets[0].id == "cs-003" + + def test_get_choiceset_values( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + choiceset_id = "cs-001" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{choiceset_id}/query_expansion", + status_code=200, + json={ + "totalRecordCount": 3, + "jsonValue": json.dumps( + [ + { + "Id": "v1", + "Name": "Active", + "DisplayName": "Active", + "NumberId": 0, + "CreateTime": "2026-01-01T00:00:00Z", + "UpdateTime": "2026-01-01T00:00:00Z", + }, + { + "Id": "v2", + "Name": "Inactive", + "DisplayName": "Inactive", + "NumberId": 1, + "CreateTime": "2026-01-01T00:00:00Z", + "UpdateTime": "2026-01-01T00:00:00Z", + }, + { + "Id": "v3", + "Name": "Pending", + "DisplayName": "Pending", + "NumberId": 2, + }, + ] + ), + }, + ) + + values = service.get_choiceset_values(choiceset_id) + + assert isinstance(values, list) + assert len(values) == 3 + assert isinstance(values[0], ChoiceSetValue) + assert values[0].id == "v1" + assert values[0].name == "Active" + assert values[0].display_name == "Active" + assert values[0].number_id == 0 + assert values[1].number_id == 1 + assert values[2].name == "Pending" + assert values[2].created_by is None + + sent_request = httpx_mock.get_request() + assert sent_request is not None + assert sent_request.method == "POST" + + def test_get_choiceset_values_with_pagination( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + choiceset_id = "cs-001" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{choiceset_id}/query_expansion?start=0&limit=2", + status_code=200, + json={ + "totalRecordCount": 5, + "jsonValue": json.dumps( + [ + { + "Id": "v1", + "Name": "Active", + "DisplayName": "Active", + "NumberId": 0, + }, + { + "Id": "v2", + "Name": "Inactive", + "DisplayName": "Inactive", + "NumberId": 1, + }, + ] + ), + }, + ) + + values = service.get_choiceset_values(choiceset_id, start=0, limit=2) + + assert len(values) == 2 + assert values[0].name == "Active" + assert values[1].name == "Inactive" + + sent_request = httpx_mock.get_request() + assert sent_request is not None + assert "start=0" in str(sent_request.url) + assert "limit=2" in str(sent_request.url) + + @pytest.mark.anyio + async def test_get_choiceset_values_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + choiceset_id = "cs-002" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{choiceset_id}/query_expansion", + status_code=200, + json={ + "totalRecordCount": 1, + "jsonValue": json.dumps( + [ + { + "Id": "v1", + "Name": "ReadOnly", + "DisplayName": "Read Only", + "NumberId": 0, + }, + ] + ), + }, + ) + + values = await service.get_choiceset_values_async(choiceset_id) + + assert len(values) == 1 + assert values[0].display_name == "Read Only" + assert values[0].number_id == 0 + + def test_get_choiceset_values_empty( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + choiceset_id = "cs-empty" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{choiceset_id}/query_expansion", + status_code=200, + json={ + "totalRecordCount": 0, + "jsonValue": "[]", + }, + ) + + values = service.get_choiceset_values(choiceset_id) + + assert values == [] diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 2c517d570..bbc761627 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.26" +version = "0.1.27" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 7a694afc0..e1f1b763a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.26" +version = "0.1.27" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From a99fc062c033248aa21f4cf0a26d74f8a06aed09 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 14 Apr 2026 15:18:45 +0200 Subject: [PATCH 016/121] ci: trigger docs rebuild on platform/core source changes (#1565) --- .github/workflows/publish-docs.yml | 4 ++++ packages/uipath-platform/pyproject.toml | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 79eec3cc1..417cd09c9 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -9,6 +9,10 @@ on: - "packages/uipath/docs/**" - "packages/uipath/mkdocs.yml" - "packages/uipath/pyproject.toml" + - "packages/uipath-platform/src/**" + - "packages/uipath-platform/pyproject.toml" + - "packages/uipath-core/src/**" + - "packages/uipath-core/pyproject.toml" repository_dispatch: types: [publish-docs] diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index a74bd7a5c..61d507e60 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.27" +version = "0.1.28" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index bbc761627..dce3eb8e9 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.27" +version = "0.1.28" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e1f1b763a..93c875332 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.27" +version = "0.1.28" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 54f32938cbe988546cdf2b60aa7ad8e4ae413a45 Mon Sep 17 00:00:00 2001 From: Dushyant Pathak Date: Wed, 15 Apr 2026 21:42:25 +0530 Subject: [PATCH 017/121] feat: add ArgumentEmail and ArgumentGroupName escalation recipient types (#1521) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 35 ++++++++++++- .../uipath/tests/agent/models/test_agent.py | 50 ++++++++++++++++++- packages/uipath/uv.lock | 2 +- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7a7bc20ee..626941f70 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.48" +version = "2.10.49" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 679ddb808..7694a861e 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -135,6 +135,8 @@ class AgentEscalationRecipientType(str, CaseInsensitiveEnum): ASSET_USER_EMAIL = "AssetUserEmail" GROUP_NAME = "GroupName" ASSET_GROUP_NAME = "AssetGroupName" + ARGUMENT_EMAIL = "ArgumentEmail" + ARGUMENT_GROUP_NAME = "ArgumentGroupName" class AgentContextRetrievalMode(str, CaseInsensitiveEnum): @@ -481,6 +483,8 @@ class AgentA2aResourceConfig(BaseAgentResourceConfig): 5: AgentEscalationRecipientType.GROUP_NAME, "staticgroupname": AgentEscalationRecipientType.GROUP_NAME, 6: AgentEscalationRecipientType.ASSET_GROUP_NAME, + 7: AgentEscalationRecipientType.ARGUMENT_EMAIL, + 8: AgentEscalationRecipientType.ARGUMENT_GROUP_NAME, } @@ -536,8 +540,37 @@ class AssetRecipient(BaseEscalationRecipient): folder_path: str = Field(..., alias="folderPath") +class ArgumentEmailRecipient(BaseEscalationRecipient): + """Argument email recipient resolved from a named input argument. + + The argument_path supports dot-notation for nested input fields (e.g. "user.email"). + """ + + type: Literal[AgentEscalationRecipientType.ARGUMENT_EMAIL,] = Field( + ..., alias="type" + ) + argument_path: str = Field(..., alias="argumentName") + + +class ArgumentGroupNameRecipient(BaseEscalationRecipient): + """Argument group name recipient resolved from a named input argument. + + The argument_path supports dot-notation for nested input fields (e.g. "team.groupName"). + """ + + type: Literal[AgentEscalationRecipientType.ARGUMENT_GROUP_NAME,] = Field( + ..., alias="type" + ) + argument_path: str = Field(..., alias="argumentName") + + AgentEscalationRecipient = Annotated[ - Union[StandardRecipient, AssetRecipient], + Union[ + StandardRecipient, + AssetRecipient, + ArgumentEmailRecipient, + ArgumentGroupNameRecipient, + ], Field(discriminator="type"), BeforeValidator(_normalize_recipient_type), ] diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 750564deb..d00e5a42b 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -1,7 +1,7 @@ from typing import Any import pytest -from pydantic import TypeAdapter +from pydantic import TypeAdapter, ValidationError from uipath.agent.models.agent import ( AgentA2aResourceConfig, @@ -43,6 +43,8 @@ AgentUnknownToolResourceConfig, AgentWordOperator, AgentWordRule, + ArgumentEmailRecipient, + ArgumentGroupNameRecipient, AssetRecipient, BatchTransformFileExtension, BatchTransformWebSearchGrounding, @@ -3789,3 +3791,49 @@ def test_a2a_resource_case_insensitive(self): ] assert len(a2a_resources) == 1 assert isinstance(a2a_resources[0], AgentA2aResourceConfig) + + +class TestArgumentRecipientDeserialization: + def test_argument_email_recipient_by_type_int(self): + payload = {"type": 7, "argumentName": "assigneeEmail"} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, ArgumentEmailRecipient) + assert recipient.argument_path == "assigneeEmail" + assert recipient.type == AgentEscalationRecipientType.ARGUMENT_EMAIL + + def test_argument_group_name_recipient_by_type_int(self): + payload = {"type": 8, "argumentName": "assigneeGroup"} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, ArgumentGroupNameRecipient) + assert recipient.argument_path == "assigneeGroup" + assert recipient.type == AgentEscalationRecipientType.ARGUMENT_GROUP_NAME + + def test_argument_email_recipient_by_type_string(self): + payload = {"type": "ArgumentEmail", "argumentName": "emailArg"} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, ArgumentEmailRecipient) + assert recipient.argument_path == "emailArg" + + def test_argument_group_name_recipient_by_type_string(self): + payload = {"type": "ArgumentGroupName", "argumentName": "groupArg"} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, ArgumentGroupNameRecipient) + assert recipient.argument_path == "groupArg" + + def test_argument_email_recipient_missing_argument_name_raises(self): + payload = {"type": 7} + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + def test_argument_group_name_recipient_missing_argument_name_raises(self): + payload = {"type": 8} + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 93c875332..6536f439a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.48" +version = "2.10.49" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 298ca3a89ae408e0ea1c4ba29a9e3ae70353eda2 Mon Sep 17 00:00:00 2001 From: Dhananjay Suresh Date: Thu, 16 Apr 2026 12:24:52 -0400 Subject: [PATCH 018/121] feat: add RunAsMe support to ProcessesService (#1567) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../orchestrator/_processes_service.py | 10 ++ .../tests/services/test_processes_service.py | 93 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 106 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 61d507e60..1b2b9a9aa 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.28" +version = "0.1.29" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py b/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py index 10b6010e2..73b4122e1 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py @@ -50,6 +50,7 @@ def invoke( folder_path: Optional[str] = None, attachments: Optional[list[Attachment]] = None, parent_operation_id: Optional[str] = None, + run_as_me: Optional[bool] = None, **kwargs: Any, ) -> Job: """Start execution of a process by its name. @@ -63,6 +64,7 @@ def invoke( folder_key (Optional[str]): The key of the folder to execute the process in. Override the default one set in the SDK config. folder_path (Optional[str]): The path of the folder to execute the process in. Override the default one set in the SDK config. parent_operation_id (Optional[str]): The parent operation ID for BTS tracking correlation. + run_as_me (Optional[bool]): If True, the job will run under the calling user's identity. Returns: Job: The job execution details. @@ -100,6 +102,7 @@ def invoke( folder_path=folder_path, parent_span_id=kwargs.get("parent_span_id"), parent_operation_id=parent_operation_id, + run_as_me=run_as_me, ) response = self.request( spec.method, @@ -123,6 +126,7 @@ async def invoke_async( folder_path: Optional[str] = None, attachments: Optional[list[Attachment]] = None, parent_operation_id: Optional[str] = None, + run_as_me: Optional[bool] = None, **kwargs: Any, ) -> Job: """Asynchronously start execution of a process by its name. @@ -136,6 +140,7 @@ async def invoke_async( folder_key (Optional[str]): The key of the folder to execute the process in. Override the default one set in the SDK config. folder_path (Optional[str]): The path of the folder to execute the process in. Override the default one set in the SDK config. parent_operation_id (Optional[str]): The parent operation ID for BTS tracking correlation. + run_as_me (Optional[bool]): If True, the job will run under the calling user's identity. Returns: Job: The job execution details. @@ -168,6 +173,7 @@ async def main(): folder_path=folder_path, parent_span_id=kwargs.get("parent_span_id"), parent_operation_id=parent_operation_id, + run_as_me=run_as_me, ) response = await self.request_async( @@ -313,6 +319,7 @@ def _invoke_spec( folder_path: Optional[str] = None, parent_span_id: Optional[str] = None, parent_operation_id: Optional[str] = None, + run_as_me: Optional[bool] = None, ) -> RequestSpec: payload: Dict[str, Any] = {"ReleaseName": name, **(input_data or {})} self._add_tracing(payload, UiPathConfig.trace_id, parent_span_id) @@ -320,6 +327,9 @@ def _invoke_spec( if parent_operation_id: payload["ParentOperationId"] = parent_operation_id + if run_as_me is not None: + payload["RunAsMe"] = run_as_me + request_spec = RequestSpec( method="POST", endpoint=Endpoint( diff --git a/packages/uipath-platform/tests/services/test_processes_service.py b/packages/uipath-platform/tests/services/test_processes_service.py index 85b2e3691..15b2c9c7e 100644 --- a/packages/uipath-platform/tests/services/test_processes_service.py +++ b/packages/uipath-platform/tests/services/test_processes_service.py @@ -471,3 +471,96 @@ async def test_invoke_async_over_10k_limit_input( job_request.headers[HEADER_USER_AGENT] == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ProcessesService.invoke_async/{version}" ) + + def test_invoke_with_run_as_me_true( + self, + httpx_mock: HTTPXMock, + service: ProcessesService, + base_url: str, + org: str, + tenant: str, + ) -> None: + process_name = "test-process" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs", + status_code=200, + json={ + "value": [ + { + "Key": "test-job-key", + "State": "Running", + "Id": 123, + "FolderKey": "test-folder-key", + } + ] + }, + ) + + service.invoke(process_name, run_as_me=True) + + sent_request = httpx_mock.get_request() + assert sent_request is not None + payload = json.loads(sent_request.content.decode("utf-8")) + assert payload["startInfo"]["RunAsMe"] is True + + def test_invoke_with_run_as_me_false( + self, + httpx_mock: HTTPXMock, + service: ProcessesService, + base_url: str, + org: str, + tenant: str, + ) -> None: + process_name = "test-process" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs", + status_code=200, + json={ + "value": [ + { + "Key": "test-job-key", + "State": "Running", + "Id": 123, + "FolderKey": "test-folder-key", + } + ] + }, + ) + + service.invoke(process_name, run_as_me=False) + + sent_request = httpx_mock.get_request() + assert sent_request is not None + payload = json.loads(sent_request.content.decode("utf-8")) + assert payload["startInfo"]["RunAsMe"] is False + + def test_invoke_without_run_as_me_excludes_from_payload( + self, + httpx_mock: HTTPXMock, + service: ProcessesService, + base_url: str, + org: str, + tenant: str, + ) -> None: + process_name = "test-process" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs", + status_code=200, + json={ + "value": [ + { + "Key": "test-job-key", + "State": "Running", + "Id": 123, + "FolderKey": "test-folder-key", + } + ] + }, + ) + + service.invoke(process_name) + + sent_request = httpx_mock.get_request() + assert sent_request is not None + payload = json.loads(sent_request.content.decode("utf-8")) + assert "RunAsMe" not in payload["startInfo"] diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index dce3eb8e9..e9a3d8d75 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.28" +version = "0.1.29" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 6536f439a..f61946b1a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.28" +version = "0.1.29" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 4613a600c6c63147fa2412649278d9f53408dd5e Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:48:14 +0200 Subject: [PATCH 019/121] feat: add support for folder scoped ephemeral index creation [ECS-1745] (#1574) --- packages/uipath-platform/pyproject.toml | 2 +- .../_context_grounding_service.py | 26 +++++++- .../test_context_grounding_service.py | 65 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 1b2b9a9aa..73f552851 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.29" +version = "0.1.30" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py index 75e525d3b..e577c5082 100644 --- a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py +++ b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py @@ -598,20 +598,29 @@ async def create_index_async( @resource_override(resource_type="index") @traced(name="contextgrounding_create_ephemeral_index", run_type="uipath") def create_ephemeral_index( - self, usage: EphemeralIndexUsage, attachments: List[str] + self, + usage: EphemeralIndexUsage, + attachments: List[str], + folder_key: str | None = None, + folder_path: str | None = None, ) -> ContextGroundingIndex: """Create a new ephemeral context grounding index. Args: usage (EphemeralIndexUsage): The task type for the ephemeral index (DeepRAG or BatchRAG) attachments (list[str]): The list of attachments ids from which the ephemeral index will be created + folder_key (Optional[str]): The folder key to scope the ephemeral index to. + folder_path (Optional[str]): The folder path to scope the ephemeral index to (resolved to a key if folder_key is not provided). Returns: ContextGroundingIndex: The created index information. """ + if folder_key is not None or folder_path is not None: + folder_key = self._resolve_folder_key(folder_key, folder_path) spec = self._create_ephemeral_spec( usage, attachments, + folder_key=folder_key, ) response = self.request( @@ -626,20 +635,29 @@ def create_ephemeral_index( @resource_override(resource_type="index") @traced(name="contextgrounding_create_ephemeral_index", run_type="uipath") async def create_ephemeral_index_async( - self, usage: EphemeralIndexUsage, attachments: List[str] + self, + usage: EphemeralIndexUsage, + attachments: List[str], + folder_key: str | None = None, + folder_path: str | None = None, ) -> ContextGroundingIndex: """Create a new ephemeral context grounding index. Args: usage (EphemeralIndexUsage): The task type for the ephemeral index (DeepRAG or BatchRAG) attachments (list[str]): The list of attachments ids from which the ephemeral index will be created + folder_key (Optional[str]): The folder key to scope the ephemeral index to. + folder_path (Optional[str]): The folder path to scope the ephemeral index to (resolved to a key if folder_key is not provided). Returns: ContextGroundingIndex: The created index information. """ + if folder_key is not None or folder_path is not None: + folder_key = self._resolve_folder_key(folder_key, folder_path) spec = self._create_ephemeral_spec( usage, attachments, + folder_key=folder_key, ) response = await self.request_async( @@ -1991,12 +2009,14 @@ def _create_ephemeral_spec( self, usage: str, attachments: List[str], + folder_key: str | None = None, ) -> RequestSpec: """Create request spec for ephemeral index creation. Args: usage (str): The task in which the ephemeral index will be used for attachments (list[str]): The list of attachments ids from which the ephemeral index will be created + folder_key (Optional[str]): The folder key to scope the ephemeral index to. Returns: RequestSpec for the create index request @@ -2012,7 +2032,7 @@ def _create_ephemeral_spec( method="POST", endpoint=Endpoint("/ecs_/v2/indexes/createephemeral"), json=payload.model_dump(by_alias=True, exclude_none=True), - headers={}, + headers={**header_folder(folder_key, None)}, ) def _build_data_source(self, source: SourceConfig) -> Dict[str, Any]: diff --git a/packages/uipath-platform/tests/services/test_context_grounding_service.py b/packages/uipath-platform/tests/services/test_context_grounding_service.py index 135ac281b..94626b8a1 100644 --- a/packages/uipath-platform/tests/services/test_context_grounding_service.py +++ b/packages/uipath-platform/tests/services/test_context_grounding_service.py @@ -2749,6 +2749,71 @@ async def test_create_ephemeral_index_async( == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ContextGroundingService.create_ephemeral_index_async/{version}" ) + def test_create_ephemeral_index_with_folder_key( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + import uuid + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/createephemeral", + status_code=200, + json={ + "id": "ephemeral-index-id", + "name": "ephemeral-index", + "lastIngestionStatus": "Queued", + }, + ) + + attachment_ids = [str(uuid.uuid4())] + service.create_ephemeral_index( + usage="DeepRAG", + attachments=attachment_ids, + folder_key="test-folder-key", + ) + + sent_requests = httpx_mock.get_requests() + assert sent_requests is not None + assert "x-uipath-folderkey" in sent_requests[0].headers + assert sent_requests[0].headers["x-uipath-folderkey"] == "test-folder-key" + + @pytest.mark.anyio + async def test_create_ephemeral_index_async_with_folder_key( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + import uuid + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/createephemeral", + status_code=200, + json={ + "id": "ephemeral-index-id", + "name": "ephemeral-index", + "lastIngestionStatus": "Queued", + }, + ) + + attachment_ids = [str(uuid.uuid4())] + await service.create_ephemeral_index_async( + usage="DeepRAG", + attachments=attachment_ids, + folder_key="test-folder-key", + ) + + sent_requests = httpx_mock.get_requests() + assert sent_requests is not None + assert "x-uipath-folderkey" in sent_requests[0].headers + assert sent_requests[0].headers["x-uipath-folderkey"] == "test-folder-key" + @pytest.mark.anyio async def test_download_batch_transform_result_async_creates_nested_directories( self, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index e9a3d8d75..7d31cdfd9 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.29" +version = "0.1.30" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index f61946b1a..f50030cb8 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.29" +version = "0.1.30" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From b8dba250d95341c9d408a523478ef910e395fe97 Mon Sep 17 00:00:00 2001 From: Andrei Petraru Date: Fri, 17 Apr 2026 17:36:33 +0300 Subject: [PATCH 020/121] docs(guardrails): expand guardrails page with all validators and examples [AL-290] (#1570) --- packages/uipath/docs/core/guardrails.md | 469 ++++++++++++++++++++++++ 1 file changed, 469 insertions(+) diff --git a/packages/uipath/docs/core/guardrails.md b/packages/uipath/docs/core/guardrails.md index 40aa69abc..a7e91f43f 100644 --- a/packages/uipath/docs/core/guardrails.md +++ b/packages/uipath/docs/core/guardrails.md @@ -1 +1,470 @@ +# Guardrails + +Guardrails are safeguards applied before and/or after execution to inspect inputs and outputs for policy violations — PII, harmful content, prompt injection, intellectual property, and custom rules — and respond by logging, blocking, or modifying the data. + +They can be applied at three scopes: + +- **Tool** — individual tool functions called by an agent +- **LLM** — LLM factory functions or chat model objects (e.g. LangChain `BaseChatModel`) +- **Agent** — agent-level methods and nodes + +The `@guardrail` decorator works with plain Python functions, async functions, and any LangChain/LangGraph object recognised by a registered framework adapter. + +## Usage + +Apply the `@guardrail` decorator to any callable — tool functions, LLM factories, agent factories, or async agent nodes. The decorator intercepts calls at the configured stage and evaluates the data against the provided validator. + +**Tool function:** + +```python +from uipath.platform.guardrails import ( + BlockAction, + GuardrailExecutionStage, + PIIDetectionEntity, + PIIDetectionEntityType, + PIIValidator, + guardrail, +) + +@guardrail( + validator=PIIValidator( + entities=[PIIDetectionEntity(PIIDetectionEntityType.EMAIL, threshold=0.5)] + ), + action=BlockAction(), + name="No PII in output", + stage=GuardrailExecutionStage.POST, +) +def analyze_joke(joke: str) -> str: + ... +``` + +When using LangChain's `@tool`, `@guardrail` must be placed **above** `@tool`: + +```python +from langchain_core.tools import tool + +@guardrail( + validator=PromptInjectionValidator(threshold=0.5), + action=BlockAction(), + name="No prompt injection", + stage=GuardrailExecutionStage.PRE, +) +@tool # @guardrail wraps the already-decorated tool object +def analyze_joke(joke: str) -> str: + ... +``` + +**LLM factory function:** + +```python +@guardrail( + validator=PromptInjectionValidator(threshold=0.5), + action=BlockAction(), + name="LLM Prompt Injection Detection", + stage=GuardrailExecutionStage.PRE, +) +def create_llm(): + return UiPathChat(model="gpt-4o-2024-08-06") +``` + +**Agent factory or async node:** + +```python +@guardrail( + validator=PIIValidator( + entities=[PIIDetectionEntity(PIIDetectionEntityType.PERSON, threshold=0.5)] + ), + action=BlockAction( + title="Person name detection", + detail="Person name detected and is not allowed", + ), + name="Agent PII Detection", + stage=GuardrailExecutionStage.PRE, +) +async def joke_node(state: Input) -> Output: + ... +``` + +## Execution Stages + +The `stage` parameter controls when the guardrail evaluates. Not all validators support all stages. + +| Stage | When evaluated | Supported by | +|-------|---------------|--------------| +| `PRE` | Before the function runs | All validators | +| `POST` | After the function runs | All except `PromptInjectionValidator`, `UserPromptAttacksValidator` | +| `PRE_AND_POST` | Both before and after | `PIIValidator`, `HarmfulContentValidator`, `CustomValidator` | + +## Built-in Validators + +Built-in validators are backed by the UiPath Guardrails API (powered by Azure Content Safety). They require a UiPath connection with the appropriate entitlements. + +### PII Detection + +Detects personally identifiable information in text. Supports 18 entity types with per-entity confidence thresholds. + +```python +from uipath.platform.guardrails import ( + BlockAction, + PIIDetectionEntity, + PIIDetectionEntityType, + PIIValidator, + guardrail, +) + +@guardrail( + validator=PIIValidator( + entities=[ + PIIDetectionEntity(name=PIIDetectionEntityType.EMAIL, threshold=0.7), + PIIDetectionEntity(name=PIIDetectionEntityType.PHONE_NUMBER, threshold=0.5), + PIIDetectionEntity(name=PIIDetectionEntityType.US_SOCIAL_SECURITY_NUMBER), + ] + ), + action=BlockAction(), + name="No PII", +) +def process_document(content: str) -> str: + ... +``` + +`threshold` is a confidence value between `0.0` and `1.0` (default `0.5`). Lower values increase sensitivity. + +### Harmful Content + +Detects harmful or unsafe content across four Azure Content Safety categories. Each category has a severity threshold from `0` (most sensitive) to `6` (least sensitive), defaulting to `2`. + +```python +from uipath.platform.guardrails import ( + BlockAction, + HarmfulContentEntity, + HarmfulContentEntityType, + HarmfulContentValidator, + guardrail, +) + +@guardrail( + validator=HarmfulContentValidator( + entities=[ + HarmfulContentEntity(name=HarmfulContentEntityType.VIOLENCE, threshold=2), + HarmfulContentEntity(name=HarmfulContentEntityType.HATE, threshold=2), + ] + ), + action=BlockAction(), + name="Safe content only", +) +def generate_response(prompt: str) -> str: + ... +``` + +### Prompt Injection + +Detects prompt injection attacks in user input. Restricted to `PRE` stage only — this is an input concern. + +```python +from uipath.platform.guardrails import ( + BlockAction, + GuardrailExecutionStage, + PromptInjectionValidator, + guardrail, +) + +@guardrail( + validator=PromptInjectionValidator(threshold=0.5), + action=BlockAction(), + name="No prompt injection", + stage=GuardrailExecutionStage.PRE, +) +def run_agent_step(user_input: str) -> str: + ... +``` + +### User Prompt Attacks + +Detects adversarial user prompt patterns (e.g. jailbreak attempts). No configuration parameters required. Restricted to `PRE` stage only. + +```python +from uipath.platform.guardrails import ( + BlockAction, + GuardrailExecutionStage, + UserPromptAttacksValidator, + guardrail, +) + +@guardrail( + validator=UserPromptAttacksValidator(), + action=BlockAction(), + name="No prompt attacks", + stage=GuardrailExecutionStage.PRE, +) +def chat(message: str) -> str: + ... +``` + +### Intellectual Property + +Detects potential intellectual property violations in generated output. Restricted to `POST` stage only — this is an output concern. + +```python +from uipath.platform.guardrails import ( + BlockAction, + GuardrailExecutionStage, + IntellectualPropertyEntityType, + IntellectualPropertyValidator, + guardrail, +) + +@guardrail( + validator=IntellectualPropertyValidator( + entities=[ + IntellectualPropertyEntityType.TEXT, + IntellectualPropertyEntityType.CODE, + ] + ), + action=BlockAction(), + name="No IP violations", + stage=GuardrailExecutionStage.POST, +) +def generate_code(spec: str) -> str: + ... +``` + +## Actions + +Actions define what happens when a violation is detected. + +### LogAction + +Logs the violation and lets execution continue. The original data is unchanged. + +```python +from uipath.platform.guardrails import LogAction, LoggingSeverityLevel + +action = LogAction(severity_level=LoggingSeverityLevel.WARNING) +action = LogAction(severity_level=LoggingSeverityLevel.ERROR, message="PII found in output") +``` + +### BlockAction + +Raises `GuardrailBlockException` to stop execution immediately. Framework adapters (e.g. LangChain) catch this exception and convert it to their own error type. + +```python +from uipath.platform.guardrails import BlockAction + +action = BlockAction() +action = BlockAction(title="PII detected", detail="Email address found in response") +``` + +### Custom Actions + +Subclass `GuardrailAction` to implement custom behaviour, such as content sanitisation: + +```python +from typing import Any +from uipath.core.guardrails import GuardrailValidationResult +from uipath.platform.guardrails import GuardrailAction + +class RedactAction(GuardrailAction): + def handle_validation_result( + self, + result: GuardrailValidationResult, + data: str | dict[str, Any], + guardrail_name: str, + ) -> str | dict[str, Any] | None: + # Return modified data to replace the original, or None to leave unchanged + if isinstance(data, str): + return "[REDACTED]" + return None +``` + +## Custom Validators + +`CustomValidator` applies an in-process rule function without any API call. The rule receives the input dict (PRE stage) or both input and output dicts (POST stage), and returns `True` to signal a violation. + +```python +from uipath.platform.guardrails import BlockAction, CustomValidator, guardrail + +@guardrail( + validator=CustomValidator(rule=lambda data: "forbidden" in str(data).lower()), + action=BlockAction(), + name="No forbidden words", +) +def my_tool(text: str) -> str: + ... +``` + +For POST-stage rules, accept two parameters to inspect both input and output: + +```python +def check_output(input_data: dict, output_data: dict) -> bool: + # Return True to trigger the guardrail + return len(output_data.get("response", "")) > 5000 + +@guardrail( + validator=CustomValidator(rule=check_output), + action=BlockAction(detail="Response exceeds maximum length"), + name="Length limit", + stage=GuardrailExecutionStage.POST, +) +def summarize(query: str) -> dict: + ... +``` + +For full control, subclass `CustomGuardrailValidator` directly. + +## Excluding Parameters + +Use `GuardrailExclude` with `Annotated` to prevent a specific parameter from being included in the guardrail evaluation payload. Useful for internal context objects, credentials, or other data that should never be inspected. + +```python +from typing import Annotated +from uipath.platform.guardrails import BlockAction, GuardrailExclude, PIIValidator, guardrail + +@guardrail( + validator=PIIValidator(entities=[PIIDetectionEntity(name=PIIDetectionEntityType.EMAIL)]), + action=BlockAction(), + name="No PII", +) +def process( + user_message: str, + internal_config: Annotated[dict, GuardrailExclude()], # excluded from guardrail +) -> str: + ... +``` + +## Stacking Guardrails + +Multiple `@guardrail` decorators can be stacked on the same function. Each is evaluated independently at its configured stage. + +```python +@guardrail( + validator=PromptInjectionValidator(), + action=BlockAction(), + name="No injection", + stage=GuardrailExecutionStage.PRE, +) +@guardrail( + validator=PIIValidator(entities=[PIIDetectionEntity(name=PIIDetectionEntityType.EMAIL)]), + action=LogAction(), + name="PII audit", + stage=GuardrailExecutionStage.POST, +) +def handle_request(user_input: str) -> str: + ... +``` + +## Low-level API + +For direct programmatic use without the decorator, the `GuardrailsService` is available on the `UiPath` client: + +```python +from uipath.platform import UiPath +from uipath.platform.guardrails import BuiltInValidatorGuardrail + +sdk = UiPath() +result = sdk.guardrails.evaluate_guardrail( + input_data="Contact me at user@example.com", + guardrail=BuiltInValidatorGuardrail( + id="my-guardrail", + name="PII check", + guardrail_type="builtInValidator", + validator_type="pii_detection", + ), +) +print(result.result, result.reason) +``` + +--- + +## API Reference + +### Service + ::: uipath.platform.guardrails._guardrails_service + options: + members: + - GuardrailsService + +### Decorator + +::: uipath.platform.guardrails.decorators._guardrail + options: + members: + - guardrail + +### Execution Stage + +::: uipath.platform.guardrails.decorators._enums + options: + members: + - GuardrailExecutionStage + - PIIDetectionEntityType + - HarmfulContentEntityType + - IntellectualPropertyEntityType + +### Actions + +::: uipath.platform.guardrails.decorators._models + options: + members: + - GuardrailAction + - PIIDetectionEntity + - HarmfulContentEntity + +::: uipath.platform.guardrails.decorators._actions + options: + members: + - LoggingSeverityLevel + - LogAction + - BlockAction + +::: uipath.platform.guardrails.decorators._exceptions + options: + members: + - GuardrailBlockException + +### Exclude Marker + +::: uipath.platform.guardrails.decorators._core + options: + members: + - GuardrailExclude + +### Validators + +::: uipath.platform.guardrails.decorators.validators._base + options: + members: + - GuardrailValidatorBase + - BuiltInGuardrailValidator + - CustomGuardrailValidator + +::: uipath.platform.guardrails.decorators.validators.pii + options: + members: + - PIIValidator + +::: uipath.platform.guardrails.decorators.validators.harmful_content + options: + members: + - HarmfulContentValidator + +::: uipath.platform.guardrails.decorators.validators.prompt_injection + options: + members: + - PromptInjectionValidator + +::: uipath.platform.guardrails.decorators.validators.intellectual_property + options: + members: + - IntellectualPropertyValidator + +::: uipath.platform.guardrails.decorators.validators.user_prompt_attacks + options: + members: + - UserPromptAttacksValidator + +::: uipath.platform.guardrails.decorators.validators.custom + options: + members: + - CustomValidator + - RuleFunction From 6d1ca972625bd4c1bdb98d1fc88332c42580f1d0 Mon Sep 17 00:00:00 2001 From: Mayank Jha Date: Fri, 17 Apr 2026 10:44:55 -0700 Subject: [PATCH 021/121] feat: scaffold MemoryService for Agent Episodic Memory (URT migration) (#1467) Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/test-packages.yml | 55 +- packages/uipath-platform/pyproject.toml | 7 +- .../src/uipath/platform/_uipath.py | 5 + .../src/uipath/platform/memory/__init__.py | 39 ++ .../uipath/platform/memory/_memory_service.py | 461 ++++++++++++++++ .../src/uipath/platform/memory/memory.py | 191 +++++++ .../tests/services/test_memory_service.py | 504 ++++++++++++++++++ .../tests/services/test_memory_service_e2e.py | 164 ++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 10 files changed, 1425 insertions(+), 5 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/memory/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/memory/_memory_service.py create mode 100644 packages/uipath-platform/src/uipath/platform/memory/memory.py create mode 100644 packages/uipath-platform/tests/services/test_memory_service.py create mode 100644 packages/uipath-platform/tests/services/test_memory_service_e2e.py diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml index 58e37a42a..dab4b8d7c 100644 --- a/.github/workflows/test-packages.yml +++ b/.github/workflows/test-packages.yml @@ -130,6 +130,55 @@ jobs: working-directory: packages/uipath-platform run: uv run pytest + e2e-uipath-platform: + name: E2E (uipath-platform, memory) + needs: detect-changed-packages + runs-on: ubuntu-latest + steps: + - name: Check if package changed + id: check + shell: bash + run: | + if echo '${{ needs.detect-changed-packages.outputs.packages }}' | jq -e 'index("uipath-platform")' > /dev/null; then + echo "skip=false" >> $GITHUB_OUTPUT + else + echo "skip=true" >> $GITHUB_OUTPUT + fi + + - name: Skip + if: steps.check.outputs.skip == 'true' + shell: bash + run: echo "Skipping - no changes to uipath-platform" + + - name: Checkout + if: steps.check.outputs.skip != 'true' + uses: actions/checkout@v4 + + - name: Setup uv + if: steps.check.outputs.skip != 'true' + uses: astral-sh/setup-uv@v5 + + - name: Setup Python + if: steps.check.outputs.skip != 'true' + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-platform + run: uv sync --all-extras --python 3.11 + + - name: Run E2E memory tests + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-platform + env: + UIPATH_URL: ${{ secrets.ALPHA_BASE_URL }} + UIPATH_CLIENT_ID: ${{ secrets.ALPHA_TEST_CLIENT_ID }} + UIPATH_CLIENT_SECRET: ${{ secrets.ALPHA_TEST_CLIENT_SECRET }} + UIPATH_FOLDER_KEY: ${{ secrets.UIPATH_MEMORY_FOLDER }} + run: uv run pytest tests/services/test_memory_service_e2e.py -m e2e -v --no-cov + test-uipath: name: Test (uipath, ${{ matrix.python-version }}, ${{ matrix.os }}) needs: detect-changed-packages @@ -184,7 +233,7 @@ jobs: test-gate: name: Test - needs: [test-uipath-core, test-uipath-platform, test-uipath] + needs: [test-uipath-core, test-uipath-platform, test-uipath, e2e-uipath-platform] runs-on: ubuntu-latest if: always() steps: @@ -196,4 +245,8 @@ jobs: echo "Tests failed" exit 1 fi + # E2E tests are informational — log but don't block + if [[ "${{ needs.e2e-uipath-platform.result }}" == "failure" ]]; then + echo "⚠️ E2E memory tests failed (non-blocking)" + fi echo "All tests passed" diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 73f552851..9a11c2814 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.30" +version = "0.1.31" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" @@ -98,9 +98,12 @@ warn_required_dynamic_aliases = true [tool.pytest.ini_options] testpaths = ["tests"] python_files = "test_*.py" -addopts = "-ra -q --cov=src/uipath --cov-report=term-missing" +addopts = "-ra -q --cov=src/uipath --cov-report=term-missing -m 'not e2e'" asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" +markers = [ + "e2e: end-to-end tests against real ECS/LLMOps (requires UIPATH_URL, UIPATH_ACCESS_TOKEN, UIPATH_FOLDER_KEY)", +] [tool.coverage.report] show_missing = true diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index e1d60fc39..28c7811c1 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -22,6 +22,7 @@ from .entities import EntitiesService from .errors import BaseUrlMissingError, SecretMissingError from .guardrails import GuardrailsService +from .memory import MemoryService from .orchestrator import ( AssetsService, AttachmentsService, @@ -113,6 +114,10 @@ def context_grounding(self) -> ContextGroundingService: self.buckets, ) + @property + def memory(self) -> MemoryService: + return MemoryService(self._config, self._execution_context, self.folders) + @property def documents(self) -> DocumentsService: return DocumentsService(self._config, self._execution_context) diff --git a/packages/uipath-platform/src/uipath/platform/memory/__init__.py b/packages/uipath-platform/src/uipath/platform/memory/__init__.py new file mode 100644 index 000000000..31e364814 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/memory/__init__.py @@ -0,0 +1,39 @@ +"""Init file for memory module.""" + +from ._memory_service import MemoryService +from .memory import ( + CachedRecall, + EscalationMemoryIngestRequest, + EscalationMemoryMatch, + EscalationMemorySearchResponse, + FieldSettings, + MemoryMatch, + MemoryMatchField, + MemorySearchRequest, + MemorySearchResponse, + MemorySpace, + MemorySpaceCreateRequest, + MemorySpaceListResponse, + SearchField, + SearchMode, + SearchSettings, +) + +__all__ = [ + "CachedRecall", + "EscalationMemoryIngestRequest", + "EscalationMemoryMatch", + "EscalationMemorySearchResponse", + "FieldSettings", + "MemoryMatch", + "MemoryMatchField", + "MemorySearchRequest", + "MemorySearchResponse", + "MemoryService", + "MemorySpace", + "MemorySpaceCreateRequest", + "MemorySpaceListResponse", + "SearchField", + "SearchMode", + "SearchSettings", +] diff --git a/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py b/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py new file mode 100644 index 000000000..89ec86a25 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py @@ -0,0 +1,461 @@ +"""Memory Spaces service. + +Memory space CRUD (create/list) goes through ECS v2. +Search and escalation memory operations go through LLMOps, which +enriches traces/feedback before forwarding to ECS. +""" + +from typing import Any, Optional + +from uipath.core.tracing import traced + +from ..common._base_service import BaseService +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..common._folder_context import FolderContext, header_folder +from ..common._models import Endpoint, RequestSpec +from ..orchestrator._folder_service import FolderService +from .memory import ( + EscalationMemoryIngestRequest, + EscalationMemorySearchResponse, + MemorySearchRequest, + MemorySearchResponse, + MemorySpace, + MemorySpaceCreateRequest, + MemorySpaceListResponse, +) + +_MEMORY_SPACES_BASE = "/ecs_/v2/episodicmemories" +_LLMOPS_AGENT_BASE = "/llmopstenant_/api/Agent/memory" + + +class MemoryService(FolderContext, BaseService): + """Service for Agent Memory Spaces. + + Agent Memory allows agents to persist context across jobs using dynamic + few-shot retrieval. Memory spaces are folder-scoped and managed via ECS. + Search is routed through LLMOps, which handles trace/feedback enrichment + and system prompt injection. Escalation memory enables agents to recall + previously resolved escalation outcomes. + """ + + def __init__( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: FolderService, + ) -> None: + super().__init__(config=config, execution_context=execution_context) + self._folders_service = folders_service + + # ── Memory space operations (ECS) ────────────────────────────────── + + @traced(name="memory_create", run_type="uipath") + def create( + self, + name: str, + description: Optional[str] = None, + is_encrypted: Optional[bool] = None, + folder_key: Optional[str] = None, + ) -> MemorySpace: + """Create a new memory space. + + Args: + name: The name of the memory space (max 128 chars). + description: Optional description (max 1024 chars). + is_encrypted: Whether the memory space should be encrypted. + folder_key: The folder key for the operation. + + Returns: + MemorySpace: The created memory space. + """ + spec = self._create_spec(name, description, is_encrypted, folder_key) + response = self.request( + spec.method, + spec.endpoint, + json=spec.json, + headers=spec.headers, + ).json() + return MemorySpace.model_validate(response) + + @traced(name="memory_create", run_type="uipath") + async def create_async( + self, + name: str, + description: Optional[str] = None, + is_encrypted: Optional[bool] = None, + folder_key: Optional[str] = None, + ) -> MemorySpace: + """Asynchronously create a new memory space. + + Args: + name: The name of the memory space (max 128 chars). + description: Optional description (max 1024 chars). + is_encrypted: Whether the memory space should be encrypted. + folder_key: The folder key for the operation. + + Returns: + MemorySpace: The created memory space. + """ + spec = self._create_spec(name, description, is_encrypted, folder_key) + response = ( + await self.request_async( + spec.method, + spec.endpoint, + json=spec.json, + headers=spec.headers, + ) + ).json() + return MemorySpace.model_validate(response) + + @traced(name="memory_list", run_type="uipath") + def list( + self, + filter: Optional[str] = None, + orderby: Optional[str] = None, + top: Optional[int] = None, + skip: Optional[int] = None, + folder_key: Optional[str] = None, + ) -> MemorySpaceListResponse: + """List memory spaces with optional OData query parameters. + + Args: + filter: OData $filter expression. + orderby: OData $orderby expression. + top: Maximum number of results. + skip: Number of results to skip. + folder_key: The folder key for the operation. + + Returns: + MemorySpaceListResponse: The list of memory spaces. + """ + spec = self._list_spec(filter, orderby, top, skip, folder_key) + response = self.request( + spec.method, + spec.endpoint, + params=spec.params, + headers=spec.headers, + ).json() + return MemorySpaceListResponse.model_validate(response) + + @traced(name="memory_list", run_type="uipath") + async def list_async( + self, + filter: Optional[str] = None, + orderby: Optional[str] = None, + top: Optional[int] = None, + skip: Optional[int] = None, + folder_key: Optional[str] = None, + ) -> MemorySpaceListResponse: + """Asynchronously list memory spaces. + + Args: + filter: OData $filter expression. + orderby: OData $orderby expression. + top: Maximum number of results. + skip: Number of results to skip. + folder_key: The folder key for the operation. + + Returns: + MemorySpaceListResponse: The list of memory spaces. + """ + spec = self._list_spec(filter, orderby, top, skip, folder_key) + response = ( + await self.request_async( + spec.method, + spec.endpoint, + params=spec.params, + headers=spec.headers, + ) + ).json() + return MemorySpaceListResponse.model_validate(response) + + # ── Search (LLMOps) ─────────────────────────────────────────────── + + @traced(name="memory_search", run_type="uipath") + def search( + self, + memory_space_id: str, + request: MemorySearchRequest, + folder_key: Optional[str] = None, + ) -> MemorySearchResponse: + """Search a memory space via LLMOps. + + Returns search results with scores and a systemPromptInjection + string ready for the agent loop. + + Args: + memory_space_id: The GUID of the memory space. + request: The search request payload. + folder_key: The folder key for the operation. + + Returns: + MemorySearchResponse: Results, metadata, and system prompt injection. + """ + spec = self._search_spec(memory_space_id, folder_key) + response = self.request( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ).json() + return MemorySearchResponse.model_validate(response) + + @traced(name="memory_search", run_type="uipath") + async def search_async( + self, + memory_space_id: str, + request: MemorySearchRequest, + folder_key: Optional[str] = None, + ) -> MemorySearchResponse: + """Asynchronously search a memory space via LLMOps. + + Returns search results with scores and a systemPromptInjection + string ready for the agent loop. + + Args: + memory_space_id: The GUID of the memory space. + request: The search request payload. + folder_key: The folder key for the operation. + + Returns: + MemorySearchResponse: Results, metadata, and system prompt injection. + """ + spec = self._search_spec(memory_space_id, folder_key) + response = ( + await self.request_async( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ) + ).json() + return MemorySearchResponse.model_validate(response) + + # ── Escalation memory (LLMOps) ──────────────────────────────────── + + @traced(name="memory_escalation_search", run_type="uipath") + def escalation_search( + self, + memory_space_id: str, + request: MemorySearchRequest, + folder_key: Optional[str] = None, + ) -> EscalationMemorySearchResponse: + """Search escalation memory for previously resolved outcomes. + + Allows agents to recall past escalation resolutions to avoid + re-escalating for similar situations. + + Args: + memory_space_id: The GUID of the memory space. + request: The search request payload (same as regular search). + folder_key: The folder key for the operation. + + Returns: + EscalationMemorySearchResponse: Matched escalation outcomes. + """ + spec = self._escalation_search_spec(memory_space_id, folder_key) + response = self.request( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ).json() + return EscalationMemorySearchResponse.model_validate(response) + + @traced(name="memory_escalation_search", run_type="uipath") + async def escalation_search_async( + self, + memory_space_id: str, + request: MemorySearchRequest, + folder_key: Optional[str] = None, + ) -> EscalationMemorySearchResponse: + """Asynchronously search escalation memory for previously resolved outcomes. + + Allows agents to recall past escalation resolutions to avoid + re-escalating for similar situations. + + Args: + memory_space_id: The GUID of the memory space. + request: The search request payload (same as regular search). + folder_key: The folder key for the operation. + + Returns: + EscalationMemorySearchResponse: Matched escalation outcomes. + """ + spec = self._escalation_search_spec(memory_space_id, folder_key) + response = ( + await self.request_async( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ) + ).json() + return EscalationMemorySearchResponse.model_validate(response) + + @traced(name="memory_escalation_ingest", run_type="uipath") + def escalation_ingest( + self, + memory_space_id: str, + request: EscalationMemoryIngestRequest, + folder_key: Optional[str] = None, + ) -> None: + """Ingest a resolved escalation outcome into memory. + + Persists the outcome so future agent runs can recall it + without re-escalating. + + Args: + memory_space_id: The GUID of the memory space. + request: The escalation ingest payload. + folder_key: The folder key for the operation. + """ + spec = self._escalation_ingest_spec(memory_space_id, folder_key) + self.request( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ) + + @traced(name="memory_escalation_ingest", run_type="uipath") + async def escalation_ingest_async( + self, + memory_space_id: str, + request: EscalationMemoryIngestRequest, + folder_key: Optional[str] = None, + ) -> None: + """Asynchronously ingest a resolved escalation outcome into memory. + + Persists the outcome so future agent runs can recall it + without re-escalating. + + Args: + memory_space_id: The GUID of the memory space. + request: The escalation ingest payload. + folder_key: The folder key for the operation. + """ + spec = self._escalation_ingest_spec(memory_space_id, folder_key) + await self.request_async( + spec.method, + spec.endpoint, + json=request.model_dump(by_alias=True, exclude_none=True), + headers=spec.headers, + ) + + # ── Private spec builders ───────────────────────────────────────── + + def _resolve_folder( + self, + folder_key: Optional[str] = None, + folder_path: Optional[str] = None, + ) -> Optional[str]: + """Resolve the folder key, supporting folder_path lookup for serverless. + + Priority: + 1. Explicit folder_key argument + 2. Explicit folder_path argument → resolve via FolderService + 3. UIPATH_FOLDER_KEY env var (via FolderContext._folder_key) + 4. UIPATH_FOLDER_PATH env var → resolve via FolderService + """ + if folder_key is None and folder_path is not None: + folder_key = self._folders_service.retrieve_key(folder_path=folder_path) + + if folder_key is None and folder_path is None: + folder_key = self._folder_key or ( + self._folders_service.retrieve_key(folder_path=self._folder_path) + if self._folder_path + else None + ) + + return folder_key + + # -- ECS specs -- + + def _create_spec( + self, + name: str, + description: Optional[str], + is_encrypted: Optional[bool], + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + body = MemorySpaceCreateRequest( + name=name, + description=description, + is_encrypted=is_encrypted, + ) + return RequestSpec( + method="POST", + endpoint=Endpoint(f"{_MEMORY_SPACES_BASE}/create"), + json=body.model_dump(by_alias=True, exclude_none=True), + headers={**header_folder(folder_key, None)}, + ) + + def _list_spec( + self, + filter: Optional[str], + orderby: Optional[str], + top: Optional[int], + skip: Optional[int], + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + params: dict[str, Any] = {} + if filter is not None: + params["$filter"] = filter + if orderby is not None: + params["$orderby"] = orderby + if top is not None: + params["$top"] = top + if skip is not None: + params["$skip"] = skip + return RequestSpec( + method="GET", + endpoint=Endpoint(_MEMORY_SPACES_BASE), + params=params, + headers={**header_folder(folder_key, None)}, + ) + + # -- LLMOps specs -- + + def _search_spec( + self, + memory_space_id: str, + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="POST", + endpoint=Endpoint(f"{_LLMOPS_AGENT_BASE}/{memory_space_id}/search"), + headers={**header_folder(folder_key, None)}, + ) + + def _escalation_search_spec( + self, + memory_space_id: str, + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"{_LLMOPS_AGENT_BASE}/{memory_space_id}/escalation/search" + ), + headers={**header_folder(folder_key, None)}, + ) + + def _escalation_ingest_spec( + self, + memory_space_id: str, + folder_key: Optional[str] = None, + ) -> RequestSpec: + folder_key = self._resolve_folder(folder_key) + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"{_LLMOPS_AGENT_BASE}/{memory_space_id}/escalation/ingest" + ), + headers={**header_folder(folder_key, None)}, + ) diff --git a/packages/uipath-platform/src/uipath/platform/memory/memory.py b/packages/uipath-platform/src/uipath/platform/memory/memory.py new file mode 100644 index 000000000..aadffbc79 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/memory/memory.py @@ -0,0 +1,191 @@ +"""Pydantic models for the Memory Spaces API. + +Memory space CRUD goes through ECS v2. Search goes through LLMOps, +which enriches traces/feedback before forwarding to ECS. +Escalation memory operations also go through LLMOps. +""" + +from enum import Enum +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, ConfigDict, Field + +# ── Enums ────────────────────────────────────────────────────────────── + + +class SearchMode(str, Enum): + """Search mode for memory space queries.""" + + Hybrid = "Hybrid" + Semantic = "Semantic" + + +# ── Shared field models (used by both ECS and LLMOps) ───────────────── + + +class FieldSettings(BaseModel): + """Per-field search settings (optional overrides).""" + + model_config = ConfigDict(populate_by_name=True) + + weight: float = Field(default=1.0, alias="weight", ge=0.0, le=1.0) + threshold: Optional[float] = Field(None, alias="threshold", ge=0.0, le=1.0) + search_mode: Optional[SearchMode] = Field(None, alias="searchMode") + + +class SearchField(BaseModel): + """A field in a search request, with per-field settings.""" + + model_config = ConfigDict(populate_by_name=True) + + key_path: List[str] = Field(..., alias="keyPath", min_length=1) + value: str = Field(..., alias="value", min_length=1) + settings: FieldSettings = Field(default_factory=FieldSettings, alias="settings") + + +class SearchSettings(BaseModel): + """Top-level search settings.""" + + model_config = ConfigDict(populate_by_name=True) + + threshold: float = Field(default=0.0, alias="threshold", ge=0.0, le=1.0) + result_count: int = Field(default=1, alias="resultCount", ge=1, le=10) + search_mode: SearchMode = Field(..., alias="searchMode") + + +class MemoryMatchField(BaseModel): + """A field within a search result, with scoring details.""" + + model_config = ConfigDict(populate_by_name=True) + + key_path: List[str] = Field(..., alias="keyPath") + value: str = Field(..., alias="value") + weight: float = Field(..., alias="weight") + score: float = Field(..., alias="score") + weighted_score: float = Field(..., alias="weightedScore") + + +# ── ECS request models (memory space CRUD) ──────────────────────────── + + +class MemorySpaceCreateRequest(BaseModel): + """Request payload for creating a memory space (ECS).""" + + model_config = ConfigDict(populate_by_name=True) + + name: str = Field(..., alias="name", max_length=128, min_length=1) + description: Optional[str] = Field(None, alias="description", max_length=1024) + is_encrypted: Optional[bool] = Field(None, alias="isEncrypted") + + +# ── ECS response models ─────────────────────────────────────────────── + + +class MemorySpace(BaseModel): + """A memory space (folder-scoped, from ECS).""" + + model_config = ConfigDict(populate_by_name=True) + + id: str = Field(..., alias="id") + name: str = Field(..., alias="name") + description: Optional[str] = Field(None, alias="description") + last_queried: Optional[str] = Field(None, alias="lastQueried") + memories_count: int = Field(default=0, alias="memoriesCount") + folder_key: str = Field(..., alias="folderKey") + created_by_user_id: Optional[str] = Field(None, alias="createdByUserId") + is_encrypted: bool = Field(default=False, alias="isEncrypted") + + +class MemorySpaceListResponse(BaseModel): + """OData response from listing memory spaces (ECS).""" + + model_config = ConfigDict(populate_by_name=True) + + value: List[MemorySpace] = Field(default_factory=list, alias="value") + + +# ── LLMOps search models ────────────────────────────────────────────── + + +class MemorySearchRequest(BaseModel): + """Request payload for searching memory via LLMOps. + + Includes definitionSystemPrompt so LLMOps can generate the + systemPromptInjection for the agent loop. + """ + + model_config = ConfigDict(populate_by_name=True) + + fields: List[SearchField] = Field(..., alias="fields", min_length=1, max_length=20) + settings: SearchSettings = Field(..., alias="settings") + definition_system_prompt: Optional[str] = Field( + None, alias="definitionSystemPrompt" + ) + + +class MemoryMatch(BaseModel): + """A single matched memory from a search operation (LLMOps).""" + + model_config = ConfigDict(populate_by_name=True) + + memory_item_id: str = Field(..., alias="memoryItemId") + score: float = Field(..., alias="score") + semantic_score: float = Field(..., alias="semanticScore") + weighted_score: float = Field(..., alias="weightedScore") + fields: List[MemoryMatchField] = Field(..., alias="fields") + span: Optional[Any] = Field(None, alias="span") + feedback: Optional[Any] = Field(None, alias="feedback") + + +class MemorySearchResponse(BaseModel): + """Response from LLMOps search, including system prompt injection.""" + + model_config = ConfigDict(populate_by_name=True) + + results: List[MemoryMatch] = Field(default_factory=list, alias="results") + metadata: Dict[str, str] = Field(default_factory=dict, alias="metadata") + system_prompt_injection: str = Field("", alias="systemPromptInjection") + + +# ── LLMOps escalation memory models ────────────────────────────────── + + +class EscalationMemoryIngestRequest(BaseModel): + """Request payload for ingesting an escalation outcome into memory. + + Used by the escalation tool to persist resolved outcomes so + future runs can recall them without re-escalating. + """ + + model_config = ConfigDict(populate_by_name=True) + + span_id: str = Field(..., alias="spanId") + trace_id: str = Field(..., alias="traceId") + answer: str = Field(..., alias="answer") + attributes: str = Field(..., alias="attributes") + user_id: Optional[str] = Field(None, alias="userId") + + +class CachedRecall(BaseModel): + """A cached escalation answer retrieved from memory.""" + + model_config = ConfigDict(populate_by_name=True) + + output: Optional[Any] = Field(None, alias="output") + outcome: Optional[str] = Field(None, alias="outcome") + + +class EscalationMemoryMatch(BaseModel): + """A single match from an escalation memory search.""" + + model_config = ConfigDict(populate_by_name=True) + + answer: Optional[CachedRecall] = Field(None, alias="answer") + + +class EscalationMemorySearchResponse(BaseModel): + """Response from LLMOps escalation memory search.""" + + model_config = ConfigDict(populate_by_name=True) + + results: Optional[List[EscalationMemoryMatch]] = Field(None, alias="results") diff --git a/packages/uipath-platform/tests/services/test_memory_service.py b/packages/uipath-platform/tests/services/test_memory_service.py new file mode 100644 index 000000000..716e3438c --- /dev/null +++ b/packages/uipath-platform/tests/services/test_memory_service.py @@ -0,0 +1,504 @@ +"""Unit tests for MemoryService with HTTP mocking.""" + +import json + +import pytest +from pytest_httpx import HTTPXMock + +from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.memory import ( + EscalationMemoryIngestRequest, + EscalationMemorySearchResponse, + MemoryMatch, + MemoryMatchField, + MemorySearchRequest, + MemorySearchResponse, + MemorySpace, + MemorySpaceListResponse, + SearchField, + SearchMode, + SearchSettings, +) +from uipath.platform.memory._memory_service import MemoryService +from uipath.platform.orchestrator._folder_service import FolderService + + +@pytest.fixture +def folder_service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, +) -> FolderService: + return FolderService(config=config, execution_context=execution_context) + + +@pytest.fixture +def service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folder_service: FolderService, + monkeypatch: pytest.MonkeyPatch, +) -> MemoryService: + monkeypatch.setenv("UIPATH_FOLDER_KEY", "test-folder-key") + return MemoryService( + config=config, + execution_context=execution_context, + folders_service=folder_service, + ) + + +# ── Sample response payloads ────────────────────────────────────────── + +SAMPLE_INDEX = { + "id": "aaaa-bbbb-cccc-dddd", + "name": "test-memory-space", + "description": "A test memory space", + "lastQueried": "2026-03-30T00:00:00Z", + "memoriesCount": 5, + "folderKey": "test-folder-key", + "createdByUserId": "user-123", + "isEncrypted": False, +} + +SAMPLE_LIST_RESPONSE = {"value": [SAMPLE_INDEX]} + +SAMPLE_SEARCH_RESPONSE = { + "results": [ + { + "memoryItemId": "item-001", + "score": 0.95, + "semanticScore": 0.92, + "weightedScore": 0.93, + "fields": [ + { + "keyPath": ["input"], + "value": "What is the capital of France?", + "weight": 1.0, + "score": 0.95, + "weightedScore": 0.95, + } + ], + "span": None, + "feedback": None, + } + ], + "metadata": {"queryTime": "12ms"}, + "systemPromptInjection": "Based on past interactions: Paris is the capital.", +} + +SAMPLE_ESCALATION_SEARCH_RESPONSE = { + "results": [ + { + "answer": { + "output": {"action": "approve", "reason": "meets criteria"}, + "outcome": "approved", + } + } + ], +} + + +class TestMemoryService: + """Unit tests for MemoryService.""" + + class TestCreate: + def test_create_memory_space( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/episodicmemories/create", + status_code=200, + json=SAMPLE_INDEX, + ) + + result = service.create( + name="test-memory-space", + description="A test memory space", + ) + + assert isinstance(result, MemorySpace) + assert result.id == "aaaa-bbbb-cccc-dddd" + assert result.name == "test-memory-space" + assert result.memories_count == 5 + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.method == "POST" + body = json.loads(sent.content) + assert body["name"] == "test-memory-space" + assert body["description"] == "A test memory space" + + def test_create_sends_folder_header( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/episodicmemories/create", + status_code=200, + json=SAMPLE_INDEX, + ) + + service.create(name="test", folder_key="custom-folder-key") + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.headers.get("x-uipath-folderkey") == "custom-folder-key" + + def test_create_with_encryption( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/episodicmemories/create", + status_code=200, + json={**SAMPLE_INDEX, "isEncrypted": True}, + ) + + result = service.create( + name="encrypted-space", + is_encrypted=True, + ) + + assert result.is_encrypted is True + sent = httpx_mock.get_request() + assert sent is not None + body = json.loads(sent.content) + assert body["isEncrypted"] is True + + class TestList: + def test_list_memory_spaces( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/episodicmemories", + status_code=200, + json=SAMPLE_LIST_RESPONSE, + ) + + result = service.list() + + assert isinstance(result, MemorySpaceListResponse) + assert len(result.value) == 1 + assert result.value[0].name == "test-memory-space" + + def test_list_with_odata_params( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/episodicmemories?%24filter=Name+eq+%27test%27&%24orderby=Name+asc&%24top=10&%24skip=5", + status_code=200, + json=SAMPLE_LIST_RESPONSE, + ) + + result = service.list( + filter="Name eq 'test'", + orderby="Name asc", + top=10, + skip=5, + ) + + assert isinstance(result, MemorySpaceListResponse) + + def test_list_empty( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/episodicmemories", + status_code=200, + json={"value": []}, + ) + + result = service.list() + + assert isinstance(result, MemorySpaceListResponse) + assert len(result.value) == 0 + + class TestSearch: + def test_search_memory( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/search", + status_code=200, + json=SAMPLE_SEARCH_RESPONSE, + ) + + request = MemorySearchRequest( + fields=[ + SearchField( + key_path=["input"], + value="What is the capital of France?", + ) + ], + settings=SearchSettings( + threshold=0.0, + result_count=5, + search_mode=SearchMode.Hybrid, + ), + definition_system_prompt="You are a helpful assistant.", + ) + + result = service.search( + memory_space_id=memory_space_id, + request=request, + ) + + assert isinstance(result, MemorySearchResponse) + assert len(result.results) == 1 + assert isinstance(result.results[0], MemoryMatch) + assert result.results[0].memory_item_id == "item-001" + assert result.results[0].score == 0.95 + assert isinstance(result.results[0].fields[0], MemoryMatchField) + assert ( + result.system_prompt_injection + == "Based on past interactions: Paris is the capital." + ) + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.method == "POST" + body = json.loads(sent.content) + assert body["fields"][0]["keyPath"] == ["input"] + assert body["settings"]["searchMode"] == "Hybrid" + assert body["definitionSystemPrompt"] == "You are a helpful assistant." + + def test_search_sends_folder_header( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/search", + status_code=200, + json=SAMPLE_SEARCH_RESPONSE, + ) + + request = MemorySearchRequest( + fields=[SearchField(key_path=["input"], value="test")], + settings=SearchSettings( + threshold=0.0, + result_count=1, + search_mode=SearchMode.Semantic, + ), + ) + + service.search( + memory_space_id=memory_space_id, + request=request, + folder_key="custom-folder", + ) + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.headers.get("x-uipath-folderkey") == "custom-folder" + + class TestEscalationSearch: + def test_escalation_search( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/escalation/search", + status_code=200, + json=SAMPLE_ESCALATION_SEARCH_RESPONSE, + ) + + request = MemorySearchRequest( + fields=[SearchField(key_path=["input"], value="approval request")], + settings=SearchSettings( + threshold=0.0, + result_count=5, + search_mode=SearchMode.Hybrid, + ), + ) + + result = service.escalation_search( + memory_space_id=memory_space_id, + request=request, + ) + + assert isinstance(result, EscalationMemorySearchResponse) + assert result.results is not None + assert len(result.results) == 1 + assert result.results[0].answer is not None + assert result.results[0].answer.outcome == "approved" + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.method == "POST" + assert "/escalation/search" in str(sent.url) + + def test_escalation_search_empty_results( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/escalation/search", + status_code=200, + json={"results": None}, + ) + + request = MemorySearchRequest( + fields=[SearchField(key_path=["input"], value="no match")], + settings=SearchSettings( + threshold=0.0, + result_count=1, + search_mode=SearchMode.Hybrid, + ), + ) + + result = service.escalation_search( + memory_space_id=memory_space_id, + request=request, + ) + + assert isinstance(result, EscalationMemorySearchResponse) + assert result.results is None + + class TestEscalationIngest: + def test_escalation_ingest( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/escalation/ingest", + status_code=200, + ) + + request = EscalationMemoryIngestRequest( + span_id="span-123", + trace_id="trace-456", + answer='{"action": "approve"}', + attributes='{"input": "approve this?"}', + user_id="user-789", + ) + + service.escalation_ingest( + memory_space_id=memory_space_id, + request=request, + ) + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.method == "POST" + assert "/escalation/ingest" in str(sent.url) + body = json.loads(sent.content) + assert body["spanId"] == "span-123" + assert body["traceId"] == "trace-456" + assert body["answer"] == '{"action": "approve"}' + assert body["attributes"] == '{"input": "approve this?"}' + assert body["userId"] == "user-789" + + def test_escalation_ingest_sends_folder_header( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/escalation/ingest", + status_code=200, + ) + + request = EscalationMemoryIngestRequest( + span_id="s1", + trace_id="t1", + answer="yes", + attributes="{}", + ) + + service.escalation_ingest( + memory_space_id=memory_space_id, + request=request, + folder_key="my-folder", + ) + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.headers.get("x-uipath-folderkey") == "my-folder" + + def test_escalation_ingest_excludes_none_user_id( + self, + httpx_mock: HTTPXMock, + service: MemoryService, + base_url: str, + org: str, + tenant: str, + ) -> None: + memory_space_id = "aaaa-bbbb-cccc-dddd" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/Agent/memory/{memory_space_id}/escalation/ingest", + status_code=200, + ) + + request = EscalationMemoryIngestRequest( + span_id="s1", + trace_id="t1", + answer="yes", + attributes="{}", + ) + + service.escalation_ingest( + memory_space_id=memory_space_id, + request=request, + ) + + sent = httpx_mock.get_request() + assert sent is not None + body = json.loads(sent.content) + assert "userId" not in body diff --git a/packages/uipath-platform/tests/services/test_memory_service_e2e.py b/packages/uipath-platform/tests/services/test_memory_service_e2e.py new file mode 100644 index 000000000..6c7866611 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_memory_service_e2e.py @@ -0,0 +1,164 @@ +"""E2E tests for MemoryService against real ECS + LLMOps endpoints. + +Prerequisites: + uipath auth --alpha # sets UIPATH_URL + UIPATH_ACCESS_TOKEN + export UIPATH_FOLDER_KEY=... # folder GUID with agent memory enabled + +Run: + cd packages/uipath-platform + uv run pytest tests/services/test_memory_service_e2e.py -m e2e -v +""" + +import os +import uuid + +import pytest + +from uipath.platform import UiPath +from uipath.platform.memory import ( + EscalationMemorySearchResponse, + MemorySearchRequest, + MemorySearchResponse, + MemorySpace, + MemorySpaceListResponse, + SearchField, + SearchMode, + SearchSettings, +) + +pytestmark = pytest.mark.e2e + + +def _require_env(name: str) -> str: + value = os.environ.get(name) + if not value: + pytest.skip(f"Environment variable {name} is not set") + return value + + +@pytest.fixture(scope="module") +def sdk() -> UiPath: + """Create a real UiPath client from env vars. + + Supports two auth modes: + - Token-based: UIPATH_URL + UIPATH_ACCESS_TOKEN (from `uipath auth`) + - Client credentials: UIPATH_URL + UIPATH_CLIENT_ID + UIPATH_CLIENT_SECRET (CI) + """ + _require_env("UIPATH_URL") + client_id = os.environ.get("UIPATH_CLIENT_ID") + client_secret = os.environ.get("UIPATH_CLIENT_SECRET") + if client_id and client_secret: + return UiPath(client_id=client_id, client_secret=client_secret) + _require_env("UIPATH_ACCESS_TOKEN") + return UiPath() + + +@pytest.fixture(scope="module") +def folder_key() -> str: + return _require_env("UIPATH_FOLDER_KEY") + + +@pytest.fixture(scope="module") +def memory_index(sdk: UiPath, folder_key: str): # noqa: ANN201 + """Create a test memory index and clean it up after all tests.""" + unique_name = f"sdk-e2e-test-{uuid.uuid4().hex[:8]}" + index = sdk.memory.create( + name=unique_name, + description="Created by E2E test — safe to delete", + folder_key=folder_key, + ) + yield index + + +class TestMemoryServiceE2E: + """E2E tests for MemoryService lifecycle. + + Requires: UIPATH_URL, UIPATH_ACCESS_TOKEN, UIPATH_FOLDER_KEY + """ + + # ── Index CRUD (ECS) ────────────────────────────────────────── + + def test_create_index(self, memory_index: MemorySpace) -> None: + """Verify index creation returns a well-formed MemorySpace.""" + assert memory_index.id, "Index ID should be set" + assert memory_index.name.startswith("sdk-e2e-test-") + assert memory_index.folder_key, "Folder key should be populated" + assert memory_index.memories_count == 0 + + def test_list_indexes( + self, + sdk: UiPath, + memory_index: MemorySpace, + folder_key: str, + ) -> None: + """Verify list with OData filter returns our index.""" + result = sdk.memory.list( + filter=f"Name eq '{memory_index.name}'", + folder_key=folder_key, + ) + assert isinstance(result, MemorySpaceListResponse) + names = [idx.name for idx in result.value] + assert memory_index.name in names + + # ── Search (LLMOps) ────────────────────────────────────────── + + def test_search_empty_index( + self, + sdk: UiPath, + memory_index: MemorySpace, + folder_key: str, + ) -> None: + """Search an empty index — should return empty results and systemPromptInjection.""" + request = MemorySearchRequest( + fields=[ + SearchField( + key_path=["input"], + value="test query", + ) + ], + settings=SearchSettings( + threshold=0.0, + result_count=5, + search_mode=SearchMode.Hybrid, + ), + definition_system_prompt="You are a helpful assistant.", + ) + result = sdk.memory.search( + memory_space_id=memory_index.id, + request=request, + folder_key=folder_key, + ) + assert isinstance(result, MemorySearchResponse) + assert isinstance(result.results, list) + assert isinstance(result.metadata, dict) + assert isinstance(result.system_prompt_injection, str) + + # ── Escalation search (LLMOps) ──────────────────────────────── + + def test_escalation_search_empty_index( + self, + sdk: UiPath, + memory_index: MemorySpace, + folder_key: str, + ) -> None: + """Search escalation memory on empty index — should return valid response.""" + request = MemorySearchRequest( + fields=[ + SearchField( + key_path=["input"], + value="test escalation query", + ) + ], + settings=SearchSettings( + threshold=0.0, + result_count=5, + search_mode=SearchMode.Hybrid, + ), + definition_system_prompt="You are a helpful assistant.", + ) + result = sdk.memory.escalation_search( + memory_space_id=memory_index.id, + request=request, + folder_key=folder_key, + ) + assert isinstance(result, EscalationMemorySearchResponse) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 7d31cdfd9..380597fbf 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.30" +version = "0.1.31" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index f50030cb8..c1419b45b 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.30" +version = "0.1.31" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 96828960c7ae9a5669270151f3bee8af37c6f3f1 Mon Sep 17 00:00:00 2001 From: UIPath-Harshit Date: Mon, 20 Apr 2026 15:59:12 +0530 Subject: [PATCH 022/121] feat: improve SQL validation for aggregate functions and entity model resilience (#1576) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/entities/_entities_service.py | 123 +++++++++++++++--- .../src/uipath/platform/entities/entities.py | 38 ++++-- .../tests/services/test_entities_service.py | 49 ++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 6 files changed, 183 insertions(+), 33 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 9a11c2814..3616306cd 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.31" +version = "0.1.32" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index a095f430d..951a4b07b 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -4,8 +4,8 @@ import sqlparse from httpx import Response -from sqlparse.sql import Parenthesis, Where -from sqlparse.tokens import DML, Keyword, Wildcard +from sqlparse.sql import Function, Identifier, IdentifierList, Parenthesis, Where +from sqlparse.tokens import DML, Keyword, Whitespace, Wildcard from uipath.core.tracing import traced from ..common._base_service import BaseService @@ -49,6 +49,7 @@ "GROUPING", "PARTITION", ] +_AGGREGATE_FUNCTIONS = ("COUNT", "SUM", "AVG", "MIN", "MAX") class EntitiesService(BaseService): @@ -177,6 +178,7 @@ def retrieve_by_name( spec = self._retrieve_by_name_spec(entity_name) headers = self._folder_key_headers(folder_key) response = self.request(spec.method, spec.endpoint, headers=headers) + return Entity.model_validate(response.json()) @traced(name="entity_retrieve_by_name", run_type="uipath") @@ -196,6 +198,7 @@ async def retrieve_by_name_async( spec = self._retrieve_by_name_spec(entity_name) headers = self._folder_key_headers(folder_key) response = await self.request_async(spec.method, spec.endpoint, headers=headers) + return Entity.model_validate(response.json()) @traced(name="list_entities", run_type="uipath") @@ -1333,18 +1336,102 @@ def _validate_sql_query(self, sql_query: str) -> None: has_where = any(isinstance(t, Where) for t in stmt.tokens) has_limit = "LIMIT" in keywords - if not has_where and not has_limit: - raise ValueError("Queries without WHERE must include a LIMIT clause.") + has_from = "FROM" in keywords + + if not has_from: + raise ValueError("Queries must include a FROM clause.") projection = self._projection_tokens(stmt) - has_wildcard = any(t.ttype is Wildcard for t in projection) - if has_wildcard and not has_where: - raise ValueError("SELECT * without filtering is not allowed.") + + if self._projection_has_count_star(projection): + raise ValueError( + "COUNT(*) is not supported. Use COUNT(column_name) instead." + ) + + has_aggregate = self._projection_has_aggregate(projection) + + if not has_where and not has_limit and not has_aggregate: + raise ValueError("Queries without WHERE must include a LIMIT clause.") + + has_bare_wildcard = self._projection_has_bare_wildcard(projection) + if has_bare_wildcard: + raise ValueError("SELECT * is not allowed. Specify column names instead.") if not has_where and self._projection_column_count(projection) > 4: raise ValueError( "Selecting more than 4 columns without filtering is not allowed." ) + @staticmethod + def _projection_has_aggregate( + projection: list[sqlparse.sql.Token], + ) -> bool: + """Check whether the projection contains an aggregate function call.""" + + def _has_agg(token: sqlparse.sql.Token) -> bool: + if isinstance(token, Function): + return token.get_name().upper() in _AGGREGATE_FUNCTIONS + if isinstance(token, Identifier): + return any(_has_agg(child) for child in token.tokens) + return False + + for node in projection: + if _has_agg(node): + return True + if isinstance(node, IdentifierList): + if any(_has_agg(child) for child in node.tokens): + return True + return False + + @staticmethod + def _projection_has_count_star( + projection: list[sqlparse.sql.Token], + ) -> bool: + """Check whether projection contains COUNT(*).""" + + def _is_count_star(func: Function) -> bool: + if func.get_name().upper() != "COUNT": + return False + return any(t.ttype is Wildcard for t in func.flatten()) + + def _has_count_star(token: sqlparse.sql.Token) -> bool: + if isinstance(token, Function): + return _is_count_star(token) + if isinstance(token, Identifier): + return any(_has_count_star(child) for child in token.tokens) + return False + + for node in projection: + if _has_count_star(node): + return True + if isinstance(node, IdentifierList): + if any(_has_count_star(child) for child in node.tokens): + return True + return False + + @staticmethod + def _projection_has_bare_wildcard( + projection: list[sqlparse.sql.Token], + ) -> bool: + """Check for a bare ``*`` or qualified ``table.*`` outside a function.""" + + def _identifier_has_wildcard(ident: Identifier) -> bool: + return any(t.ttype is Wildcard for t in ident.tokens) + + for node in projection: + if node.ttype is Wildcard: + return True + if isinstance(node, Identifier) and _identifier_has_wildcard(node): + return True + if isinstance(node, IdentifierList): + for child in node.tokens: + if child.ttype is Wildcard: + return True + if isinstance(child, Identifier) and _identifier_has_wildcard( + child + ): + return True + return False + @staticmethod def _has_subquery(stmt: sqlparse.sql.Statement) -> bool: """Recursively walk the AST looking for SELECT inside parentheses.""" @@ -1369,16 +1456,18 @@ def _walk(token: sqlparse.sql.Token) -> bool: def _projection_tokens( stmt: sqlparse.sql.Statement, ) -> list[sqlparse.sql.Token]: - """Extract tokens between the first SELECT and FROM.""" + """Extract non-flattened AST nodes between the first SELECT and FROM.""" tokens: list[sqlparse.sql.Token] = [] collecting = False - for token in stmt.flatten(): + for token in stmt.tokens: if token.ttype is DML and token.normalized == "SELECT": collecting = True continue - if token.ttype is Keyword and token.normalized == "FROM": + if token.ttype is Keyword and token.normalized in ("FROM", "INTO"): break - if collecting: + if token.ttype is Keyword and token.normalized == "DISTINCT": + continue + if collecting and token.ttype is not Whitespace: tokens.append(token) return tokens @@ -1386,10 +1475,14 @@ def _projection_tokens( def _projection_column_count( projection: list[sqlparse.sql.Token], ) -> int: - text = "".join(t.value for t in projection).strip() - if not text: - return 0 - return len([part for part in text.split(",") if part.strip()]) + for node in projection: + if isinstance(node, IdentifierList): + return len(list(node.get_identifiers())) + if isinstance(node, (Identifier, Function)): + return 1 + if node.ttype is Wildcard: + return 1 + return 0 # Resolve the forward reference to EntitiesService in EntitySetResolution. diff --git a/packages/uipath-platform/src/uipath/platform/entities/entities.py b/packages/uipath-platform/src/uipath/platform/entities/entities.py index f3fb862f8..48c8dce07 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/entities.py +++ b/packages/uipath-platform/src/uipath/platform/entities/entities.py @@ -16,7 +16,7 @@ get_origin, ) -from pydantic import BaseModel, ConfigDict, Field, create_model +from pydantic import AliasChoices, BaseModel, ConfigDict, Field, create_model if TYPE_CHECKING: from ._entities_service import EntitiesService @@ -140,7 +140,7 @@ class FieldMetadata(BaseModel): reference_field: Optional["EntityField"] = Field( default=None, alias="referenceField" ) - reference_type: ReferenceType = Field(alias="referenceType") + reference_type: Optional[ReferenceType] = Field(default=None, alias="referenceType") sql_type: "FieldDataType" = Field(alias="sqlType") is_required: bool = Field(alias="isRequired") display_name: str = Field(alias="displayName") @@ -212,14 +212,21 @@ class SourceJoinCriteria(BaseModel): model_config = ConfigDict( validate_by_name=True, validate_by_alias=True, + extra="allow", + ) + id: Optional[str] = None + entity_id: Optional[str] = Field(default=None, alias="entityId") + join_field_name: Optional[str] = Field(default=None, alias="joinFieldName") + join_type: Optional[str] = Field(default=None, alias="joinType") + related_source_object_id: Optional[str] = Field( + default=None, alias="relatedSourceObjectId" + ) + related_source_object_field_name: Optional[str] = Field( + default=None, alias="relatedSourceObjectFieldName" + ) + related_source_field_name: Optional[str] = Field( + default=None, alias="relatedSourceFieldName" ) - id: str - entity_id: str = Field(alias="entityId") - join_field_name: str = Field(alias="joinFieldName") - join_type: str = Field(alias="joinType") - related_source_object_id: str = Field(alias="relatedSourceObjectId") - related_source_object_field_name: str = Field(alias="relatedSourceObjectFieldName") - related_source_field_name: str = Field(alias="relatedSourceFieldName") class ChoiceSetValue(BaseModel): @@ -326,11 +333,16 @@ class Entity(BaseModel): entity_type: str = Field(alias="entityType") description: Optional[str] = Field(default=None, alias="description") fields: Optional[List[FieldMetadata]] = Field(default=None, alias="fields") - external_fields: Optional[List[ExternalSourceFields]] = Field( - default=None, alias="externalFields" + external_fields: Optional[ + List[ExternalField | ExternalSourceFields | Dict[str, Any]] + ] = Field( + default=None, + alias="externalFields", ) - source_join_criteria: Optional[List[SourceJoinCriteria]] = Field( - default=None, alias="sourceJoinCriteria" + source_join_criteria: Optional[List[SourceJoinCriteria | Dict[str, Any]]] = Field( + default=None, + validation_alias=AliasChoices("sourceJoinCriteria", "sourceJoinCriterias"), + alias="sourceJoinCriteria", ) record_count: Optional[int] = Field(default=None, alias="recordCount") storage_size_in_mb: Optional[float] = Field(default=None, alias="storageSizeInMB") diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 71c3ccd5b..29ce6fb79 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -308,7 +308,12 @@ def test_retrieve_records_without_start_and_limit( [ "SELECT id FROM Customers WHERE id = 1", "SELECT id, name FROM Customers LIMIT 10", - "SELECT * FROM Customers WHERE status = 'Active'", + "SELECT COUNT(id) FROM Customers", + "SELECT SUM(amount) FROM Orders", + "SELECT AVG(price) FROM Products", + "SELECT MIN(created), MAX(created) FROM Events", + "SELECT COUNT(id) AS total, SUM(amount) AS amt FROM Orders", + "SELECT COUNT(id), name FROM Customers LIMIT 10", "SELECT id, name, email, phone FROM Customers LIMIT 5", "SELECT DISTINCT id FROM Customers WHERE id > 100", "SELECT id FROM Customers WHERE name = 'foo;bar'", @@ -356,9 +361,49 @@ def test_validate_sql_query_allows_supported_select_queries( "SELECT id FROM Customers", "Queries without WHERE must include a LIMIT clause.", ), + ( + "SELECT UPPER(name) FROM Customers", + "Queries without WHERE must include a LIMIT clause.", + ), + ( + "SELECT COALESCE(name, 'N/A') FROM Customers", + "Queries without WHERE must include a LIMIT clause.", + ), + ( + "SELECT 1 LIMIT 1", + "Queries must include a FROM clause.", + ), + ( + "SELECT COUNT(*) FROM Customers", + "COUNT(*) is not supported. Use COUNT(column_name) instead.", + ), + ( + "SELECT COUNT(*), name FROM Customers LIMIT 10", + "COUNT(*) is not supported. Use COUNT(column_name) instead.", + ), + ( + "SELECT COUNT(*) AS total FROM Customers", + "COUNT(*) is not supported. Use COUNT(column_name) instead.", + ), ( "SELECT * FROM Customers LIMIT 10", - "SELECT * without filtering is not allowed.", + "SELECT * is not allowed. Specify column names instead.", + ), + ( + "SELECT Customers.* FROM Customers LIMIT 10", + "SELECT * is not allowed. Specify column names instead.", + ), + ( + "SELECT t.* FROM Customers t LIMIT 10", + "SELECT * is not allowed. Specify column names instead.", + ), + ( + "SELECT * FROM Customers WHERE status = 'Active'", + "SELECT * is not allowed. Specify column names instead.", + ), + ( + "SELECT Customers.* FROM Customers WHERE status = 'Active'", + "SELECT * is not allowed. Specify column names instead.", ), ( "SELECT id, name, email, phone, address FROM Customers LIMIT 10", diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 380597fbf..5a0d80299 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.31" +version = "0.1.32" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index c1419b45b..d725f853b 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.31" +version = "0.1.32" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From ff4b67d3912e47369dd0938f05cc2ff5ddba5edf Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 21 Apr 2026 10:53:17 +0200 Subject: [PATCH 023/121] docs: explain auto-generated .mermaid files (#1506) (#1579) --- packages/uipath/docs/cli/index.md | 6 +++++ packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/cli_init.py | 7 ++++++ packages/uipath/tests/cli/test_init.py | 26 +++++++++++++++++++++ packages/uipath/uv.lock | 2 +- 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index deb875353..336040742 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -82,6 +82,12 @@ Running `uipath init` will process these function definitions and create the cor ✓ Created '.uipath/studio_metadata.json' file. ✓ Created: CLAUDE.md, CLI_REFERENCE.md, SDK_REFERENCE.md, AGENTS.md, REQUIRED_STRUCTURE.md. ``` + +/// info +### About the `.mermaid` files + +`uipath init` generates one `.mermaid` file per function/agent containing a static call graph, rendered in the UiPath Orchestrator UI. These files are regenerated on every `uipath init`. +/// --- ::: mkdocs-click diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 626941f70..cba365f57 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.49" +version = "2.10.50" 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_init.py b/packages/uipath/src/uipath/_cli/cli_init.py index f86e30f06..80396d8ff 100644 --- a/packages/uipath/src/uipath/_cli/cli_init.py +++ b/packages/uipath/src/uipath/_cli/cli_init.py @@ -277,6 +277,12 @@ def write_studio_metadata_file(directory: str) -> None: ) +MERMAID_FILE_HEADER = ( + "%% AUTO-GENERATED by `uipath init`. Do not edit manually.\n" + "%% Regenerated on every `uipath init`.\n" +) + + def write_mermaid_files(entry_points: list[UiPathRuntimeSchema]) -> list[Path]: """Write mermaid diagram files for each entrypoint. @@ -299,6 +305,7 @@ def write_mermaid_files(entry_points: list[UiPathRuntimeSchema]) -> list[Path]: mermaid_file_path = Path(os.getcwd()) / f"{ep.file_path}.mermaid" with open(mermaid_file_path, "w") as f: + f.write(MERMAID_FILE_HEADER) f.write(str(chart)) mermaid_paths.append(mermaid_file_path) diff --git a/packages/uipath/tests/cli/test_init.py b/packages/uipath/tests/cli/test_init.py index fa5b47ab2..59d4eaaa8 100644 --- a/packages/uipath/tests/cli/test_init.py +++ b/packages/uipath/tests/cli/test_init.py @@ -659,3 +659,29 @@ def test_init_does_not_overwrite_existing_studio_metadata( metadata = json.load(f) assert metadata["schemaVersion"] == 99 assert metadata["codeVersion"] == "5.0.0" + + +class TestWriteMermaidFiles: + def test_mermaid_file_starts_with_header_comment( + self, runner: CliRunner, temp_dir: str + ) -> None: + """Generated .mermaid files begin with the clarifying header comment.""" + from uipath._cli.cli_init import MERMAID_FILE_HEADER, write_mermaid_files + from uipath.runtime.schema import UiPathRuntimeGraph, UiPathRuntimeSchema + + ep = UiPathRuntimeSchema( + filePath="main.py", + uniqueId="main", + type="function", + input={}, + output={}, + graph=UiPathRuntimeGraph(), + ) + + with runner.isolated_filesystem(temp_dir=temp_dir): + paths = write_mermaid_files([ep]) + assert len(paths) == 1 + contents = paths[0].read_text() + assert contents.startswith(MERMAID_FILE_HEADER) + assert "AUTO-GENERATED" in contents + assert "uipath init" in contents diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d725f853b..aed4bcb4a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.49" +version = "2.10.50" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From dbf6bffd690e1d713cb7e80ae8ea4f3b9fff5b47 Mon Sep 17 00:00:00 2001 From: Josh Park <50765702+JoshParkSJ@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:37:14 -0400 Subject: [PATCH 024/121] feat(voice): voice streamed tool calls for improved latency (#1559) Co-authored-by: Claude Opus 4.7 (1M context) --- .../scripts/test_check_version_uniqueness.py | 1 - packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/chat/__init__.py | 9 + .../uipath-core/src/uipath/core/chat/voice.py | 30 ++ packages/uipath-core/uv.lock | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_cli/_chat/_voice_bridge.py | 257 ++++++++++++++++++ .../tests/cli/chat/test_voice_bridge.py | 137 ++++++++++ packages/uipath/uv.lock | 4 +- 10 files changed, 439 insertions(+), 7 deletions(-) create mode 100644 packages/uipath-core/src/uipath/core/chat/voice.py create mode 100644 packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py create mode 100644 packages/uipath/tests/cli/chat/test_voice_bridge.py diff --git a/.github/scripts/test_check_version_uniqueness.py b/.github/scripts/test_check_version_uniqueness.py index 94a2b3cc0..af4298af1 100644 --- a/.github/scripts/test_check_version_uniqueness.py +++ b/.github/scripts/test_check_version_uniqueness.py @@ -5,7 +5,6 @@ import urllib.error from unittest import mock -import pytest from check_version_uniqueness import ( get_package_info, diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 1a5c34ab7..387818039 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/chat/__init__.py b/packages/uipath-core/src/uipath/core/chat/__init__.py index 476cb9352..c2e9f025b 100644 --- a/packages/uipath-core/src/uipath/core/chat/__init__.py +++ b/packages/uipath-core/src/uipath/core/chat/__init__.py @@ -114,6 +114,11 @@ UiPathConversationToolCallResult, UiPathConversationToolCallStartEvent, ) +from .voice import ( + UiPathVoiceToolCallMessage, + UiPathVoiceToolCallRequest, + UiPathVoiceToolCallResult, +) __all__ = [ # Root @@ -189,4 +194,8 @@ "UiPathConversationAsyncInputStreamEvent", # Meta "UiPathConversationMetaEvent", + # Voice + "UiPathVoiceToolCallRequest", + "UiPathVoiceToolCallMessage", + "UiPathVoiceToolCallResult", ] diff --git a/packages/uipath-core/src/uipath/core/chat/voice.py b/packages/uipath-core/src/uipath/core/chat/voice.py new file mode 100644 index 000000000..7b1adf7e8 --- /dev/null +++ b/packages/uipath-core/src/uipath/core/chat/voice.py @@ -0,0 +1,30 @@ +"""Voice tool-call wire models (CAS socket.io).""" + +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field + + +class _VoiceWire(BaseModel): + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathVoiceToolCallRequest(_VoiceWire): + """Single tool call in a batch.""" + + call_id: str = Field(..., alias="callId") + tool_name: str = Field(..., alias="toolName") + args: dict[str, Any] + + +class UiPathVoiceToolCallMessage(_VoiceWire): + """Batch of tool calls from CAS.""" + + calls: list[UiPathVoiceToolCallRequest] = Field(..., min_length=1) + + +class UiPathVoiceToolCallResult(_VoiceWire): + """Result of a single tool call.""" + + result: str + is_error: bool = Field(..., alias="isError") diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index e17802fc7..807653ed8 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 5a0d80299..93c33a7d7 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index cba365f57..ee647db03 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.50" +version = "2.10.51" 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/_chat/_voice_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py new file mode 100644 index 000000000..6164b9f3d --- /dev/null +++ b/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py @@ -0,0 +1,257 @@ +"""Voice tool-call session — persistent socket.io connection to CAS.""" + +from __future__ import annotations + +import asyncio +import logging +import os +from collections.abc import Awaitable, Callable +from enum import Enum +from typing import Any +from urllib.parse import urlparse + +from pydantic import ValidationError + +from uipath.core.chat import ( + UiPathVoiceToolCallMessage, + UiPathVoiceToolCallRequest, + UiPathVoiceToolCallResult, +) +from uipath.runtime.context import UiPathRuntimeContext + +logger = logging.getLogger(__name__) + + +_ATTEMPT_CAS_SOCKET_CONNECTION_TIMEOUT_SECONDS = 15.0 +_INFLIGHT_TOOL_DRAIN_AFTER_AGENT_END_TIMEOUT_SECONDS = 30.0 + + +class VoiceToolCallSessionError(RuntimeError): + pass + + +class VoiceSessionEndReason(str, Enum): + COMPLETED = "completed" + DISCONNECTED = "disconnected" + READY_EMIT_FAILED = "ready_emit_failed" + + +class VoiceEvent(str, Enum): + """CAS voice-session protocol events (excludes socket.io lifecycle).""" + + TOOL_CALL = "voice_tool_call" # received + SESSION_ENDED = "voice_session_ended" # received + TOOLS_READY = "voice_tools_ready" # sent + TOOL_RESULT = "voice_tool_result" # sent + + +ToolHandler = Callable[ + [UiPathVoiceToolCallRequest], Awaitable[UiPathVoiceToolCallResult] +] + + +class VoiceToolCallSession: + """Socket.io session with CAS for tool-call traffic. + + Receives `voice_tool_call` batches, emits one `voice_tool_result` per + `callId`, exits on `voice_session_ended` or disconnect. CAS pulls + agent config from Orchestrator directly; this session carries only + tool calls. + """ + + def __init__( + self, + url: str, + socketio_path: str, + headers: dict[str, str], + tool_handler: ToolHandler, + ) -> None: + self._url = url + self._socketio_path = socketio_path + self._headers = headers + self._tool_handler = tool_handler + self._client: Any = None + self._done = asyncio.Event() + self._in_flight: set[asyncio.Task[None]] = set() + self._end_reason: VoiceSessionEndReason | None = None + + async def run(self) -> VoiceSessionEndReason: + """Connect, dispatch tool calls until session ends, then disconnect. + + Raises: + VoiceToolCallSessionError: If connecting to CAS fails. + """ + from socketio import AsyncClient # type: ignore[import-untyped] + + self._client = AsyncClient(logger=False, engineio_logger=False) + self._client.on("connect", self._handle_connect) + self._client.on("disconnect", self._handle_disconnect) + self._client.on(VoiceEvent.TOOL_CALL, self._handle_tool_call) + self._client.on(VoiceEvent.SESSION_ENDED, self._handle_session_ended) + + try: + await asyncio.wait_for( + self._client.connect( + url=self._url, + socketio_path=self._socketio_path, + headers=self._headers, + transports=["websocket"], + ), + timeout=_ATTEMPT_CAS_SOCKET_CONNECTION_TIMEOUT_SECONDS, + ) + except Exception as exc: + await self._safe_disconnect("after connect-failure") + raise VoiceToolCallSessionError( + f"Failed to connect to CAS voice endpoint: {exc}" + ) from exc + + try: + await self._done.wait() + await self._drain_in_flight() + finally: + await self._safe_disconnect("on shutdown") + + return self._end_reason or VoiceSessionEndReason.DISCONNECTED + + async def _safe_disconnect(self, when: str) -> None: + try: + await self._client.disconnect() + except Exception as exc: + logger.debug("[Voice] disconnect %s raised: %s", when, exc) + + def _end_session(self, reason: VoiceSessionEndReason) -> None: + # First writer wins: a late disconnect must not overwrite COMPLETED. + if self._end_reason is None: + self._end_reason = reason + self._done.set() + + async def _drain_in_flight(self) -> None: + """Wait for in-flight tool tasks to finish, capped by the drain timeout.""" + if not self._in_flight: + return + logger.info( + "[Voice] Session ended with %d in-flight tool task(s); draining (max %.0fs)", + len(self._in_flight), + _INFLIGHT_TOOL_DRAIN_AFTER_AGENT_END_TIMEOUT_SECONDS, + ) + try: + await asyncio.wait_for( + asyncio.gather(*self._in_flight, return_exceptions=True), + timeout=_INFLIGHT_TOOL_DRAIN_AFTER_AGENT_END_TIMEOUT_SECONDS, + ) + except asyncio.TimeoutError: + unfinished = sum(1 for t in self._in_flight if not t.done()) + logger.warning( + "[Voice] %d tool task(s) did not complete within %.0fs of session end", + unfinished, + _INFLIGHT_TOOL_DRAIN_AFTER_AGENT_END_TIMEOUT_SECONDS, + ) + + async def _handle_connect(self) -> None: + logger.info("[Voice] Socket.io connected to CAS") + try: + await self._client.emit(VoiceEvent.TOOLS_READY, {}) + except Exception as exc: + # CAS gates tool dispatch on this event; without it the session is dead. + logger.warning("[Voice] emit voice_tools_ready failed: %s", exc) + self._end_session(VoiceSessionEndReason.READY_EMIT_FAILED) + + async def _handle_disconnect(self) -> None: + logger.info("[Voice] Socket.io disconnected from CAS") + self._end_session(VoiceSessionEndReason.DISCONNECTED) + + async def _handle_tool_call(self, data: dict[str, Any], *_: Any) -> None: + """Spawn a task per call and return — the reader must stay free for `voice_session_ended`.""" + if self._done.is_set(): + return + + try: + message = UiPathVoiceToolCallMessage.model_validate(data) + except ValidationError as exc: + logger.warning("[Voice] invalid voice_tool_call payload: %s", exc) + return + + for call in message.calls: + task = asyncio.create_task(self._execute_tool_call(call)) + self._in_flight.add(task) + task.add_done_callback(self._in_flight.discard) + + async def _execute_tool_call(self, call: UiPathVoiceToolCallRequest) -> None: + """Run one tool call and emit its `voice_tool_result`.""" + logger.info( + "[Voice] voice_tool_call dispatched: %s (%s) args=%s", + call.tool_name, + call.call_id, + call.args, + ) + try: + tool_result = await self._tool_handler(call) + except Exception as exc: + logger.exception("[Voice] Tool call execution failed: %s", call.tool_name) + tool_result = UiPathVoiceToolCallResult(result=str(exc), is_error=True) + + try: + await self._client.emit( + VoiceEvent.TOOL_RESULT, + {"callId": call.call_id, **tool_result.model_dump(by_alias=True)}, + ) + except Exception as exc: + logger.debug( + "[Voice] emit voice_tool_result failed for %s: %s", call.call_id, exc + ) + return + logger.info( + "[Voice] voice_tool_result sent: %s (isError=%s)", + call.call_id, + tool_result.is_error, + ) + + async def _handle_session_ended(self, _data: Any, *_: Any) -> None: + logger.info("[Voice] voice_session_ended received") + self._end_session(VoiceSessionEndReason.COMPLETED) + + +def get_voice_bridge( + context: UiPathRuntimeContext, + tool_handler: ToolHandler, +) -> VoiceToolCallSession: + """Factory for a CAS voice tool-call session. + + Raises: + RuntimeError: If UIPATH_URL is not set or invalid. + """ + assert context.conversation_id is not None, "conversation_id must be set in context" + + if cas_host := os.environ.get("CAS_WEBSOCKET_HOST"): + url = f"ws://{cas_host}?conversationId={context.conversation_id}" + socketio_path = "/socket.io" + logger.warning( + f"CAS_WEBSOCKET_HOST is set. Using websocket_url '{url}{socketio_path}'." + ) + else: + base_url = os.environ.get("UIPATH_URL") + if not base_url: + raise RuntimeError( + "UIPATH_URL environment variable required for conversational mode" + ) + parsed = urlparse(base_url) + if not parsed.netloc: + raise RuntimeError(f"Invalid UIPATH_URL format: {base_url}") + url = f"wss://{parsed.netloc}?conversationId={context.conversation_id}" + socketio_path = "autopilotforeveryone_/websocket_/socket.io" + + headers = { + "Authorization": f"Bearer {os.environ.get('UIPATH_ACCESS_TOKEN', '')}", + "X-UiPath-Internal-TenantId": context.tenant_id + or os.environ.get("UIPATH_TENANT_ID", ""), + "X-UiPath-Internal-AccountId": context.org_id + or os.environ.get("UIPATH_ORGANIZATION_ID", ""), + "X-UiPath-ConversationId": context.conversation_id, + } + + return VoiceToolCallSession( + url=url, + socketio_path=socketio_path, + headers=headers, + tool_handler=tool_handler, + ) diff --git a/packages/uipath/tests/cli/chat/test_voice_bridge.py b/packages/uipath/tests/cli/chat/test_voice_bridge.py new file mode 100644 index 000000000..b945fb6e3 --- /dev/null +++ b/packages/uipath/tests/cli/chat/test_voice_bridge.py @@ -0,0 +1,137 @@ +"""Tests for VoiceToolCallSession and get_voice_bridge.""" + +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from uipath._cli._chat._voice_bridge import ( + VoiceSessionEndReason, + VoiceToolCallSession, + get_voice_bridge, +) +from uipath.core.chat import ( + UiPathVoiceToolCallRequest, + UiPathVoiceToolCallResult, +) + + +def _make_session(tool_handler: Any = None) -> VoiceToolCallSession: + session = VoiceToolCallSession( + url="wss://example/test", + socketio_path="/socket.io", + headers={}, + tool_handler=tool_handler or AsyncMock(), + ) + session._client = MagicMock() + session._client.emit = AsyncMock() + return session + + +class TestEndSession: + def test_first_writer_wins(self) -> None: + """A late DISCONNECTED must not overwrite COMPLETED.""" + session = _make_session() + session._end_session(VoiceSessionEndReason.COMPLETED) + session._end_session(VoiceSessionEndReason.DISCONNECTED) + assert session._end_reason == VoiceSessionEndReason.COMPLETED + assert session._done.is_set() + + async def test_session_ended_sets_completed(self) -> None: + session = _make_session() + await session._handle_session_ended(None) + assert session._end_reason == VoiceSessionEndReason.COMPLETED + + async def test_disconnect_sets_disconnected(self) -> None: + session = _make_session() + await session._handle_disconnect() + assert session._end_reason == VoiceSessionEndReason.DISCONNECTED + + +class TestHandleToolCall: + async def test_dispatches_handler_and_emits_result(self) -> None: + handler = AsyncMock( + return_value=UiPathVoiceToolCallResult(result="ok", is_error=False) + ) + session = _make_session(handler) + + await session._handle_tool_call( + {"calls": [{"callId": "c1", "toolName": "weather", "args": {"city": "SF"}}]} + ) + # Drain the spawned task. + for task in list(session._in_flight): + await task + + handler.assert_awaited_once() + assert handler.await_args is not None + call_arg = handler.await_args.args[0] + assert isinstance(call_arg, UiPathVoiceToolCallRequest) + assert call_arg.call_id == "c1" + assert call_arg.tool_name == "weather" + + session._client.emit.assert_awaited_once_with( + "voice_tool_result", + {"callId": "c1", "result": "ok", "isError": False}, + ) + + async def test_invalid_payload_is_skipped(self) -> None: + handler = AsyncMock() + session = _make_session(handler) + + await session._handle_tool_call({"calls": []}) # min_length=1 violation + + handler.assert_not_awaited() + session._client.emit.assert_not_awaited() + + async def test_noop_after_session_ended(self) -> None: + handler = AsyncMock() + session = _make_session(handler) + session._done.set() + + await session._handle_tool_call( + {"calls": [{"callId": "c1", "toolName": "x", "args": {}}]} + ) + + handler.assert_not_awaited() + assert not session._in_flight + + async def test_handler_exception_emits_error_result(self) -> None: + handler = AsyncMock(side_effect=RuntimeError("boom")) + session = _make_session(handler) + + await session._handle_tool_call( + {"calls": [{"callId": "c1", "toolName": "x", "args": {}}]} + ) + for task in list(session._in_flight): + await task + + session._client.emit.assert_awaited_once_with( + "voice_tool_result", + {"callId": "c1", "result": "boom", "isError": True}, + ) + + +class TestGetVoiceBridge: + def test_raises_when_uipath_url_missing( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv("UIPATH_URL", raising=False) + monkeypatch.delenv("CAS_WEBSOCKET_HOST", raising=False) + ctx = MagicMock(conversation_id="conv-1", tenant_id="t", org_id="o") + + with pytest.raises(RuntimeError, match="UIPATH_URL"): + get_voice_bridge(ctx, AsyncMock()) + + def test_headers_fall_back_to_env_when_context_ids_are_none( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Regression: f"{None}" is truthy ("None"), so the `or` fallback was dead.""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + monkeypatch.setenv("UIPATH_TENANT_ID", "env-tenant") + monkeypatch.setenv("UIPATH_ORGANIZATION_ID", "env-org") + ctx = MagicMock(conversation_id="conv-1", tenant_id=None, org_id=None) + + bridge = get_voice_bridge(ctx, AsyncMock()) + + assert bridge._headers["X-UiPath-Internal-TenantId"] == "env-tenant" + assert bridge._headers["X-UiPath-Internal-AccountId"] == "env-org" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index aed4bcb4a..6605f470f 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.50" +version = "2.10.51" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.11" +version = "0.5.12" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 58adbab126524a449918c46ee678c67830b313dc Mon Sep 17 00:00:00 2001 From: Popescu Tudor-Cristian <94108303+PopescuTudor@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:35:11 +0300 Subject: [PATCH 025/121] fix: accept remoteA2aAgent in GenericResourceOverwrite (#1581) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/_bindings.py | 9 ++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/tests/sdk/test_bindings.py | 25 +++++++++++++++++++ packages/uipath/uv.lock | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 3616306cd..758ab3d5f 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.32" +version = "0.1.33" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 321b83c4f..224cae425 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -52,7 +52,14 @@ def folder_identifier(self) -> str: class GenericResourceOverwrite(ResourceOverwrite): resource_type: Literal[ - "process", "index", "app", "asset", "bucket", "mcpServer", "queue" + "process", + "index", + "app", + "asset", + "bucket", + "mcpServer", + "queue", + "remoteA2aAgent", ] name: str = Field(alias="name") folder_path: str = Field(alias="folderPath") diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 93c33a7d7..815defe05 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.32" +version = "0.1.33" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/tests/sdk/test_bindings.py b/packages/uipath/tests/sdk/test_bindings.py index d9afd8235..1d51d7413 100644 --- a/packages/uipath/tests/sdk/test_bindings.py +++ b/packages/uipath/tests/sdk/test_bindings.py @@ -391,3 +391,28 @@ def test_parse_connection_with_capitalized_alias(self): assert isinstance(overwrite, ConnectionResourceOverwrite) assert overwrite.connection_id == "conn-456" assert overwrite.folder_key == "folder2" + + +class TestRemoteA2aAgentResourceOverwrite: + """Test that Remote A2A agent resources parse as GenericResourceOverwrite.""" + + def test_remote_a2a_agent_resource_overwrite(self): + overwrite = GenericResourceOverwrite( + resource_type="remoteA2aAgent", + name="basica2a", + folder_path="Customers/ProjectA", + ) + assert overwrite.resource_type == "remoteA2aAgent" + assert overwrite.resource_identifier == "basica2a" + assert overwrite.folder_identifier == "Customers/ProjectA" + + def test_parse_remote_a2a_agent(self): + """Parser accepts a remoteA2aAgent-keyed overwrite without discriminator error.""" + overwrite = ResourceOverwriteParser.parse( + key="remoteA2aAgent.basica2a.solution_folder", + value={"name": "basica2a", "folderPath": "Customers/ProjectA"}, + ) + assert isinstance(overwrite, GenericResourceOverwrite) + assert overwrite.resource_type == "remoteA2aAgent" + assert overwrite.resource_identifier == "basica2a" + assert overwrite.folder_identifier == "Customers/ProjectA" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 6605f470f..bb7432220 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.32" +version = "0.1.33" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 59d8ca40ffa59e2a9b0975ed62b3809e81bc1988 Mon Sep 17 00:00:00 2001 From: yashwagle1 Date: Tue, 21 Apr 2026 16:50:25 -0700 Subject: [PATCH 026/121] Integrate with AutomationOps and SemanticProxy (#1578) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/_uipath.py | 10 + .../platform/automation_ops/__init__.py | 9 + .../automation_ops/_automation_ops_service.py | 64 +++++ .../platform/semantic_proxy/__init__.py | 36 +++ .../semantic_proxy/_semantic_proxy_service.py | 74 +++++ .../platform/semantic_proxy/pii_utilities.py | 115 ++++++++ .../platform/semantic_proxy/semantic_proxy.py | 91 ++++++ .../services/test_automation_ops_service.py | 159 +++++++++++ .../tests/services/test_pii_utilities.py | 170 +++++++++++ .../services/test_semantic_proxy_service.py | 264 ++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 13 files changed, 995 insertions(+), 3 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/automation_ops/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/automation_ops/_automation_ops_service.py create mode 100644 packages/uipath-platform/src/uipath/platform/semantic_proxy/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/semantic_proxy/_semantic_proxy_service.py create mode 100644 packages/uipath-platform/src/uipath/platform/semantic_proxy/pii_utilities.py create mode 100644 packages/uipath-platform/src/uipath/platform/semantic_proxy/semantic_proxy.py create mode 100644 packages/uipath-platform/tests/services/test_automation_ops_service.py create mode 100644 packages/uipath-platform/tests/services/test_pii_utilities.py create mode 100644 packages/uipath-platform/tests/services/test_semantic_proxy_service.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 758ab3d5f..cfc151484 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.33" +version = "0.1.34" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 28c7811c1..f81333862 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -8,6 +8,7 @@ from .action_center import TasksService from .agenthub._agenthub_service import AgentHubService from .agenthub._remote_a2a_service import RemoteA2aService +from .automation_ops import AutomationOpsService from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService from .common import ( ApiClient, @@ -35,6 +36,7 @@ QueuesService, ) from .resource_catalog import ResourceCatalogService +from .semantic_proxy import SemanticProxyService def _has_valid_client_credentials( @@ -178,6 +180,14 @@ def remote_a2a(self) -> RemoteA2aService: def orchestrator_setup(self) -> OrchestratorSetupService: return OrchestratorSetupService(self._config, self._execution_context) + @property + def automation_ops(self) -> AutomationOpsService: + return AutomationOpsService(self._config, self._execution_context) + + @property + def semantic_proxy(self) -> SemanticProxyService: + return SemanticProxyService(self._config, self._execution_context) + @property def automation_tracker(self) -> AutomationTrackerService: return AutomationTrackerService(self._config, self._execution_context) diff --git a/packages/uipath-platform/src/uipath/platform/automation_ops/__init__.py b/packages/uipath-platform/src/uipath/platform/automation_ops/__init__.py new file mode 100644 index 000000000..87ce420ec --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/automation_ops/__init__.py @@ -0,0 +1,9 @@ +"""AutomationOps service package. + +Provides the ``AutomationOpsService`` client for retrieving deployed AI Trust +Layer policies from AgentHub. +""" + +from ._automation_ops_service import AutomationOpsService + +__all__ = ["AutomationOpsService"] diff --git a/packages/uipath-platform/src/uipath/platform/automation_ops/_automation_ops_service.py b/packages/uipath-platform/src/uipath/platform/automation_ops/_automation_ops_service.py new file mode 100644 index 000000000..05c37bbb5 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/automation_ops/_automation_ops_service.py @@ -0,0 +1,64 @@ +"""AutomationOps service for UiPath Platform. + +Provides methods for retrieving deployed policies from the AgentHub service. +""" + +from typing import Any + +from uipath.core.tracing import traced + +from ..common._base_service import BaseService +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..common._models import Endpoint, RequestSpec + +_DEPLOYED_POLICY_ENDPOINT = Endpoint("agenthub_/api/policies/deployed-policy") + + +class AutomationOpsService(BaseService): + """Service for interacting with UiPath AutomationOps policies via AgentHub.""" + + def __init__( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + super().__init__(config=config, execution_context=execution_context) + + @traced(name="automation_ops_get_deployed_policy", run_type="uipath") + def get_deployed_policy(self) -> dict[str, Any]: + """Retrieve the deployed policy. + + Returns: + The deployed policy response as a dictionary. + """ + spec = self._deployed_policy_spec() + response = self.request( + spec.method, + url=spec.endpoint, + headers=spec.headers, + scoped="tenant", + ) + return response.json() + + @traced(name="automation_ops_get_deployed_policy", run_type="uipath") + async def get_deployed_policy_async(self) -> dict[str, Any]: + """Retrieve the deployed policy (async). + + Returns: + The deployed policy response as a dictionary. + """ + spec = self._deployed_policy_spec() + response = await self.request_async( + spec.method, + url=spec.endpoint, + headers=spec.headers, + scoped="tenant", + ) + return response.json() + + def _deployed_policy_spec(self) -> RequestSpec: + return RequestSpec( + method="POST", + endpoint=_DEPLOYED_POLICY_ENDPOINT, + ) diff --git a/packages/uipath-platform/src/uipath/platform/semantic_proxy/__init__.py b/packages/uipath-platform/src/uipath/platform/semantic_proxy/__init__.py new file mode 100644 index 000000000..e17867ac7 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/semantic_proxy/__init__.py @@ -0,0 +1,36 @@ +"""SemanticProxy service package. + +Provides the ``SemanticProxyService`` client, Pydantic request/response models for +the PII detection endpoint, and utilities for rehydrating masked text with +original PII values after LLM processing. +""" + +from ._semantic_proxy_service import SemanticProxyService +from .pii_utilities import ( + rehydrate_from_pii_entities, + rehydrate_from_pii_response, +) +from .semantic_proxy import ( + PiiDetectionRequest, + PiiDetectionResponse, + PiiDocument, + PiiDocumentResult, + PiiEntity, + PiiEntityThreshold, + PiiFile, + PiiFileResult, +) + +__all__ = [ + "PiiDetectionRequest", + "PiiDetectionResponse", + "PiiDocument", + "PiiDocumentResult", + "PiiEntity", + "PiiEntityThreshold", + "PiiFile", + "PiiFileResult", + "SemanticProxyService", + "rehydrate_from_pii_entities", + "rehydrate_from_pii_response", +] diff --git a/packages/uipath-platform/src/uipath/platform/semantic_proxy/_semantic_proxy_service.py b/packages/uipath-platform/src/uipath/platform/semantic_proxy/_semantic_proxy_service.py new file mode 100644 index 000000000..a68d7c25c --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/semantic_proxy/_semantic_proxy_service.py @@ -0,0 +1,74 @@ +"""SemanticProxy service for UiPath Platform. + +Provides methods for interacting with the SemanticProxy service (e.g. PII detection). +""" + +from uipath.core.tracing import traced + +from ..common._base_service import BaseService +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..common._models import Endpoint, RequestSpec +from .semantic_proxy import PiiDetectionRequest, PiiDetectionResponse + +_PII_DETECTION_ENDPOINT = Endpoint("semanticproxy_/api/pii-detection") + + +class SemanticProxyService(BaseService): + """Service for interacting with UiPath SemanticProxy.""" + + def __init__( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + super().__init__(config=config, execution_context=execution_context) + + @traced(name="semantic_proxy_detect_pii", run_type="uipath") + def detect_pii(self, request: PiiDetectionRequest) -> PiiDetectionResponse: + """Detect PII in the provided documents and/or files. + + Args: + request: The PII detection request payload. + + Returns: + The PII detection response. + """ + spec = self._pii_detection_spec(request) + response = self.request( + spec.method, + url=spec.endpoint, + json=spec.json, + headers=spec.headers, + scoped="tenant", + ) + return PiiDetectionResponse.model_validate(response.json()) + + @traced(name="semantic_proxy_detect_pii", run_type="uipath") + async def detect_pii_async( + self, request: PiiDetectionRequest + ) -> PiiDetectionResponse: + """Detect PII in the provided documents and/or files (async). + + Args: + request: The PII detection request payload. + + Returns: + The PII detection response. + """ + spec = self._pii_detection_spec(request) + response = await self.request_async( + spec.method, + url=spec.endpoint, + json=spec.json, + headers=spec.headers, + scoped="tenant", + ) + return PiiDetectionResponse.model_validate(response.json()) + + def _pii_detection_spec(self, request: PiiDetectionRequest) -> RequestSpec: + return RequestSpec( + method="POST", + endpoint=_PII_DETECTION_ENDPOINT, + json=request.model_dump(by_alias=True, exclude_none=True), + ) diff --git a/packages/uipath-platform/src/uipath/platform/semantic_proxy/pii_utilities.py b/packages/uipath-platform/src/uipath/platform/semantic_proxy/pii_utilities.py new file mode 100644 index 000000000..c480a5fb7 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/semantic_proxy/pii_utilities.py @@ -0,0 +1,115 @@ +"""Utility methods for working with PII data. + +Python port of UiPath.SemanticProxy.Client.PiiUtilities (C#). +""" + +import json +import re +from typing import Callable, Iterable + +from .semantic_proxy import PiiDetectionResponse, PiiEntity + + +def rehydrate_from_pii_entities( + masked_text: str, pii_entities: Iterable[PiiEntity] +) -> str: + """Rehydrate masked text by replacing PII placeholders with original values. + + Placeholders (e.g. ``[Person-1]``) are matched case-insensitively and replaced + with the corresponding original PII text. The function also replaces variants + without the surrounding brackets (e.g. ``Person-1``) in case the LLM stripped + them in its output. + + Args: + masked_text: The masked text with PII placeholders. + pii_entities: The PII entities containing the original values. + + Returns: + The rehydrated text with original PII values. + """ + if not masked_text: + return masked_text + + entities = [e for e in pii_entities if e.replacement_text] + if not entities: + return masked_text + + # Sort by replacement text length descending to avoid substring collisions + # (e.g. "[Person-10]" must be replaced before "[Person-1]"). + entities.sort(key=lambda e: len(e.replacement_text), reverse=True) + + rehydrated = masked_text + for entity in entities: + if not entity.replacement_text or not entity.pii_text: + continue + escaped_pii = _add_escape_characters(entity.pii_text) + # Replace the full placeholder (with brackets) case-insensitively. + # ``_literal_replacer`` bypasses regex backreference interpretation in the + # replacement string. + rehydrated = re.sub( + re.escape(entity.replacement_text), + _literal_replacer(escaped_pii), + rehydrated, + flags=re.IGNORECASE, + ) + # Also replace the content without brackets (in case the LLM dropped them). + if entity.replacement_text.startswith("[") and entity.replacement_text.endswith( + "]" + ): + no_brackets = entity.replacement_text[1:-1] + rehydrated = re.sub( + re.escape(no_brackets), + _literal_replacer(escaped_pii), + rehydrated, + flags=re.IGNORECASE, + ) + + return rehydrated + + +def _literal_replacer(replacement: str) -> Callable[[re.Match[str]], str]: + """Return a replacement function that ignores regex backreference syntax.""" + + def replace(_match: re.Match[str]) -> str: + return replacement + + return replace + + +def rehydrate_from_pii_response( + masked_text: str, response: PiiDetectionResponse +) -> str: + """Rehydrate masked text using all PII entities from a detection response. + + Merges entities from both ``response.response`` (detected in documents/prompts) + and ``response.files`` (detected in files), so placeholders originating from + either source are rehydrated. + + Args: + masked_text: The masked text with PII placeholders. + response: The PII detection response containing entities to rehydrate. + + Returns: + The rehydrated text with original PII values. + """ + entities: list[PiiEntity] = [] + for doc in response.response: + entities.extend(doc.pii_entities) + for file in response.files: + entities.extend(file.pii_entities) + return rehydrate_from_pii_entities(masked_text, entities) + + +def _add_escape_characters(text: str) -> str: + """Escape special characters in text using JSON serialization. + + Mirrors C# ``AddEscapeCharacters`` — serializes as JSON then strips the + surrounding quotes to get the escaped content. + """ + if not text: + return "" + try: + serialized = json.dumps(text, ensure_ascii=False) + return serialized[1:-1] + except (TypeError, ValueError): + return text diff --git a/packages/uipath-platform/src/uipath/platform/semantic_proxy/semantic_proxy.py b/packages/uipath-platform/src/uipath/platform/semantic_proxy/semantic_proxy.py new file mode 100644 index 000000000..2be35e975 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/semantic_proxy/semantic_proxy.py @@ -0,0 +1,91 @@ +"""Public Pydantic models for the SemanticProxy service.""" + +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class PiiDocument(BaseModel): + """A text document to scan for PII.""" + + id: str + role: str + document: str + + +class PiiFile(BaseModel): + """A file reference to scan for PII.""" + + model_config = ConfigDict(populate_by_name=True) + + file_name: str = Field(alias="fileName") + file_url: str = Field(alias="fileUrl") + file_type: str = Field(alias="fileType") + + +class PiiEntityThreshold(BaseModel): + """Per-entity confidence threshold override.""" + + model_config = ConfigDict(populate_by_name=True) + + category: str = Field(alias="pii-entity-category") + confidence_threshold: float = Field(alias="pii-entity-confidence-threshold") + + +class PiiDetectionRequest(BaseModel): + """Request payload for the PII detection endpoint.""" + + model_config = ConfigDict(populate_by_name=True) + + documents: Optional[list[PiiDocument]] = None + files: Optional[list[PiiFile]] = None + language_code: Optional[str] = Field(default=None, alias="languageCode") + confidence_threshold: Optional[float] = Field( + default=None, alias="confidenceThreshold" + ) + entity_thresholds: Optional[list[PiiEntityThreshold]] = Field( + default=None, alias="entityThresholds" + ) + + +class PiiEntity(BaseModel): + """A single detected PII entity.""" + + model_config = ConfigDict(populate_by_name=True) + + pii_text: str = Field(alias="piiText") + replacement_text: str = Field(alias="replacementText") + pii_type: str = Field(alias="piiType") + offset: int + confidence_score: float = Field(alias="confidenceScore") + + +class PiiDocumentResult(BaseModel): + """PII detection result for a single document.""" + + model_config = ConfigDict(populate_by_name=True) + + id: str + role: str + masked_document: str = Field(alias="maskedDocument") + initial_document: str = Field(alias="initialDocument") + pii_entities: list[PiiEntity] = Field(default_factory=list, alias="piiEntities") + + +class PiiFileResult(BaseModel): + """PII detection result for a single file (fileUrl is the redacted URL).""" + + model_config = ConfigDict(populate_by_name=True) + + file_name: str = Field(alias="fileName") + file_url: str = Field(alias="fileUrl") + pii_entities: list[PiiEntity] = Field(default_factory=list, alias="piiEntities") + + +class PiiDetectionResponse(BaseModel): + """Response payload from the PII detection endpoint.""" + + model_config = ConfigDict(populate_by_name=True) + + response: list[PiiDocumentResult] = Field(default_factory=list) + files: list[PiiFileResult] = Field(default_factory=list) diff --git a/packages/uipath-platform/tests/services/test_automation_ops_service.py b/packages/uipath-platform/tests/services/test_automation_ops_service.py new file mode 100644 index 000000000..c90e1f80a --- /dev/null +++ b/packages/uipath-platform/tests/services/test_automation_ops_service.py @@ -0,0 +1,159 @@ +"""Tests for AutomationOpsService.""" + +import json + +import httpx +import pytest +from pytest_httpx import HTTPXMock + +from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.automation_ops import AutomationOpsService + + +@pytest.fixture +def service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, +) -> AutomationOpsService: + return AutomationOpsService(config=config, execution_context=execution_context) + + +class TestAutomationOpsService: + """Test AutomationOpsService functionality.""" + + class TestGetDeployedPolicy: + """Test get_deployed_policy (sync).""" + + def test_returns_policy_dict( + self, + httpx_mock: HTTPXMock, + service: AutomationOpsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + expected_policy = { + "policy-name": "AITL Policy", + "data": { + "container": {"pii-in-flight-agents": True}, + "pii-entity-table": [], + }, + } + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/agenthub_/api/policies/deployed-policy", + status_code=200, + json=expected_policy, + ) + + result = service.get_deployed_policy() + + assert result == expected_policy + + def test_uses_post_method( + self, + httpx_mock: HTTPXMock, + service: AutomationOpsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json={}) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/agenthub_/api/policies/deployed-policy", + callback=capture, + ) + + service.get_deployed_policy() + + assert captured_request is not None + assert captured_request.method == "POST" + + class TestGetDeployedPolicyAsync: + """Test get_deployed_policy_async.""" + + async def test_returns_policy_dict( + self, + httpx_mock: HTTPXMock, + service: AutomationOpsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + expected_policy = { + "policy-name": "AITL Policy", + "data": { + "container": {"pii-in-flight-agents": False}, + }, + } + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/agenthub_/api/policies/deployed-policy", + status_code=200, + json=expected_policy, + ) + + result = await service.get_deployed_policy_async() + + assert result == expected_policy + + async def test_url_is_tenant_scoped( + self, + httpx_mock: HTTPXMock, + service: AutomationOpsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json={}) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/agenthub_/api/policies/deployed-policy", + callback=capture, + ) + + await service.get_deployed_policy_async() + + assert captured_request is not None + # Tenant-scoped: both org and tenant segments appear in the path + assert org.strip("/") in captured_request.url.path + assert tenant.strip("/") in captured_request.url.path + + async def test_request_has_no_body( + self, + httpx_mock: HTTPXMock, + service: AutomationOpsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json={}) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/agenthub_/api/policies/deployed-policy", + callback=capture, + ) + + await service.get_deployed_policy_async() + + assert captured_request is not None + # POST with no body — body should be empty (or an empty JSON object) + body = captured_request.content + assert body in (b"", b"null") or json.loads(body) in ({}, None) diff --git a/packages/uipath-platform/tests/services/test_pii_utilities.py b/packages/uipath-platform/tests/services/test_pii_utilities.py new file mode 100644 index 000000000..844985a90 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_pii_utilities.py @@ -0,0 +1,170 @@ +"""Tests for PII rehydration utilities.""" + +from uipath.platform.semantic_proxy import ( + PiiDetectionResponse, + PiiDocumentResult, + PiiEntity, + PiiFileResult, + rehydrate_from_pii_entities, + rehydrate_from_pii_response, +) + + +def _entity( + pii_text: str, + replacement_text: str, + pii_type: str = "Person", + offset: int = 0, + confidence_score: float = 0.9, +) -> PiiEntity: + return PiiEntity( + pii_text=pii_text, + replacement_text=replacement_text, + pii_type=pii_type, + offset=offset, + confidence_score=confidence_score, + ) + + +class TestRehydrateFromPiiEntities: + """Test rehydrate_from_pii_entities.""" + + def test_empty_text_returns_empty(self) -> None: + assert rehydrate_from_pii_entities("", [_entity("Alice", "[Person-1]")]) == "" + + def test_no_entities_returns_text_unchanged(self) -> None: + text = "Hello [Person-1]" + assert rehydrate_from_pii_entities(text, []) == text + + def test_replaces_single_placeholder(self) -> None: + result = rehydrate_from_pii_entities( + "Hello [Person-1]", [_entity("Alice", "[Person-1]")] + ) + assert result == "Hello Alice" + + def test_replaces_multiple_placeholders(self) -> None: + result = rehydrate_from_pii_entities( + "Contact [Person-1] at [Email-1]", + [ + _entity("Alice", "[Person-1]"), + _entity("alice@example.com", "[Email-1]", pii_type="Email"), + ], + ) + assert result == "Contact Alice at alice@example.com" + + def test_longer_placeholders_replaced_first(self) -> None: + """[Person-10] must be rehydrated before [Person-1] to avoid partial match.""" + result = rehydrate_from_pii_entities( + "[Person-1] and [Person-10]", + [ + _entity("Alice", "[Person-1]"), + _entity("Zara", "[Person-10]"), + ], + ) + assert result == "Alice and Zara" + + def test_case_insensitive_placeholder_match(self) -> None: + result = rehydrate_from_pii_entities( + "Hello [person-1]", [_entity("Alice", "[Person-1]")] + ) + assert result == "Hello Alice" + + def test_replaces_bracketless_variant(self) -> None: + """The LLM may drop brackets; bracketless variant should still be replaced.""" + result = rehydrate_from_pii_entities( + "Hello Person-1", [_entity("Alice", "[Person-1]")] + ) + assert result == "Hello Alice" + + def test_skips_entities_with_empty_replacement_text(self) -> None: + result = rehydrate_from_pii_entities( + "Hello [Person-1]", + [ + _entity("Ignored", ""), + _entity("Alice", "[Person-1]"), + ], + ) + assert result == "Hello Alice" + + def test_skips_entities_with_empty_pii_text(self) -> None: + result = rehydrate_from_pii_entities( + "Hello [Person-1]", + [_entity("", "[Person-1]")], + ) + assert result == "Hello [Person-1]" + + def test_preserves_non_placeholder_content(self) -> None: + result = rehydrate_from_pii_entities( + "The meeting with [Person-1] is at 3pm in the boardroom.", + [_entity("Alice", "[Person-1]")], + ) + assert result == "The meeting with Alice is at 3pm in the boardroom." + + def test_pii_text_with_special_characters(self) -> None: + """Special chars in PII text must not break regex substitution.""" + result = rehydrate_from_pii_entities( + "Visit [URL-1]", + [_entity("https://example.com/path?q=1&x=2", "[URL-1]", pii_type="URL")], + ) + assert result == "Visit https://example.com/path?q=1&x=2" + + def test_regex_special_chars_in_replacement_text(self) -> None: + """Regex special chars in the placeholder must be escaped for the pattern.""" + result = rehydrate_from_pii_entities( + "Hello [Person.1]", + [_entity("Alice", "[Person.1]")], + ) + assert result == "Hello Alice" + + +class TestRehydrateFromPiiResponse: + """Test rehydrate_from_pii_response.""" + + def test_merges_document_and_file_entities(self) -> None: + response = PiiDetectionResponse( + response=[ + PiiDocumentResult( + id="user-prompt", + role="user", + masked_document="Hi [Person-1]", + initial_document="Hi Alice", + pii_entities=[_entity("Alice", "[Person-1]")], + ) + ], + files=[ + PiiFileResult( + file_name="doc.pdf", + file_url="https://example.com/doc.pdf", + pii_entities=[ + _entity("bob@example.com", "[Email-1]", pii_type="Email") + ], + ) + ], + ) + + result = rehydrate_from_pii_response( + "From [Person-1]: contact [Email-1]", response + ) + assert result == "From Alice: contact bob@example.com" + + def test_file_only_entity_is_rehydrated(self) -> None: + """Entities detected in files (not prompts) must also rehydrate.""" + response = PiiDetectionResponse( + response=[], + files=[ + PiiFileResult( + file_name="doc.pdf", + file_url="https://example.com/doc.pdf", + pii_entities=[ + _entity("alice@example.com", "[Email-1]", pii_type="Email") + ], + ) + ], + ) + + result = rehydrate_from_pii_response("Email is [Email-1]", response) + assert result == "Email is alice@example.com" + + def test_empty_response_returns_text_unchanged(self) -> None: + response = PiiDetectionResponse(response=[], files=[]) + assert rehydrate_from_pii_response("No PII here", response) == "No PII here" diff --git a/packages/uipath-platform/tests/services/test_semantic_proxy_service.py b/packages/uipath-platform/tests/services/test_semantic_proxy_service.py new file mode 100644 index 000000000..51b4f4895 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_semantic_proxy_service.py @@ -0,0 +1,264 @@ +"""Tests for SemanticProxyService.""" + +import json +from typing import Any + +import httpx +import pytest +from pytest_httpx import HTTPXMock + +from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.semantic_proxy import ( + PiiDetectionRequest, + PiiDetectionResponse, + PiiDocument, + PiiEntityThreshold, + PiiFile, + SemanticProxyService, +) + + +@pytest.fixture +def service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, +) -> SemanticProxyService: + return SemanticProxyService(config=config, execution_context=execution_context) + + +@pytest.fixture +def sample_response_json() -> dict[str, Any]: + return { + "response": [ + { + "id": "user-prompt", + "role": "user", + "maskedDocument": "Contact [Person-1]", + "initialDocument": "Contact Alison", + "piiEntities": [ + { + "piiText": "Alison", + "replacementText": "[Person-1]", + "piiType": "Person", + "offset": 8, + "confidenceScore": 0.99, + } + ], + } + ], + "files": [ + { + "fileName": "doc.pdf", + "fileUrl": "https://blob.example.com/redacted/doc.pdf", + "piiEntities": [ + { + "piiText": "alice@example.com", + "replacementText": "[Email-1]", + "piiType": "Email", + "offset": 100, + "confidenceScore": 0.88, + } + ], + } + ], + } + + +class TestSemanticProxyService: + """Test SemanticProxyService functionality.""" + + class TestDetectPii: + """Test detect_pii (sync).""" + + def test_returns_typed_response( + self, + httpx_mock: HTTPXMock, + service: SemanticProxyService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/semanticproxy_/api/pii-detection", + status_code=200, + json=sample_response_json, + ) + + request = PiiDetectionRequest( + documents=[ + PiiDocument( + id="user-prompt", role="user", document="Contact Alison" + ) + ] + ) + result = service.detect_pii(request) + + assert isinstance(result, PiiDetectionResponse) + assert len(result.response) == 1 + assert result.response[0].masked_document == "Contact [Person-1]" + assert len(result.files) == 1 + assert result.files[0].file_name == "doc.pdf" + assert result.files[0].pii_entities[0].replacement_text == "[Email-1]" + + class TestDetectPiiAsync: + """Test detect_pii_async.""" + + async def test_returns_typed_response( + self, + httpx_mock: HTTPXMock, + service: SemanticProxyService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/semanticproxy_/api/pii-detection", + status_code=200, + json=sample_response_json, + ) + + request = PiiDetectionRequest( + files=[ + PiiFile( + file_name="doc.pdf", + file_url="https://input.example.com/doc.pdf", + file_type="pdf", + ) + ] + ) + result = await service.detect_pii_async(request) + + assert isinstance(result, PiiDetectionResponse) + assert ( + result.files[0].file_url == "https://blob.example.com/redacted/doc.pdf" + ) + + async def test_request_payload_uses_aliases( + self, + httpx_mock: HTTPXMock, + service: SemanticProxyService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json=sample_response_json) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/semanticproxy_/api/pii-detection", + callback=capture, + ) + + request = PiiDetectionRequest( + documents=[ + PiiDocument(id="user-prompt", role="user", document="Hello") + ], + files=[ + PiiFile( + file_name="doc.pdf", + file_url="https://input.example.com/doc.pdf", + file_type="pdf", + ) + ], + language_code="en", + confidence_threshold=0.5, + entity_thresholds=[ + PiiEntityThreshold(category="Person", confidence_threshold=0.7), + ], + ) + await service.detect_pii_async(request) + + assert captured_request is not None + payload = json.loads(captured_request.content) + + # Top-level uses camelCase aliases + assert "documents" in payload + assert "files" in payload + assert "languageCode" in payload + assert "confidenceThreshold" in payload + assert "entityThresholds" in payload + + # File uses camelCase aliases + assert payload["files"][0]["fileName"] == "doc.pdf" + assert payload["files"][0]["fileUrl"] == "https://input.example.com/doc.pdf" + assert payload["files"][0]["fileType"] == "pdf" + + # Entity threshold uses kebab-case aliases + threshold = payload["entityThresholds"][0] + assert threshold["pii-entity-category"] == "Person" + assert threshold["pii-entity-confidence-threshold"] == 0.7 + + async def test_request_excludes_none_fields( + self, + httpx_mock: HTTPXMock, + service: SemanticProxyService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json=sample_response_json) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/semanticproxy_/api/pii-detection", + callback=capture, + ) + + # Only documents set; other optional fields should be omitted + request = PiiDetectionRequest( + documents=[PiiDocument(id="user-prompt", role="user", document="Hello")] + ) + await service.detect_pii_async(request) + + assert captured_request is not None + payload = json.loads(captured_request.content) + assert "files" not in payload + assert "languageCode" not in payload + assert "confidenceThreshold" not in payload + assert "entityThresholds" not in payload + + async def test_url_is_tenant_scoped( + self, + httpx_mock: HTTPXMock, + service: SemanticProxyService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json=sample_response_json) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/semanticproxy_/api/pii-detection", + callback=capture, + ) + + request = PiiDetectionRequest( + documents=[PiiDocument(id="user-prompt", role="user", document="Hello")] + ) + await service.detect_pii_async(request) + + assert captured_request is not None + assert org.strip("/") in captured_request.url.path + assert tenant.strip("/") in captured_request.url.path + assert "/semanticproxy_/api/pii-detection" in captured_request.url.path diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 815defe05..8d6d60a56 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.33" +version = "0.1.34" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index bb7432220..0d146ae6c 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.33" +version = "0.1.34" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 9ef2936847557236132367415357911967730fa6 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Wed, 22 Apr 2026 17:29:12 +0200 Subject: [PATCH 027/121] feat: create virtual resources on push when catalog lookup misses (#1584) --- packages/uipath-platform/pyproject.toml | 2 +- .../_resource_catalog_service.py | 8 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_cli/_push/_resolvers.py | 177 ++++++++ .../uipath/_cli/_push/_resource_actions.py | 27 ++ .../uipath/src/uipath/_cli/_push/_summary.py | 35 ++ .../src/uipath/_cli/_push/_virtual_kinds.py | 34 ++ .../src/uipath/_cli/_utils/_studio_project.py | 134 ++---- packages/uipath/src/uipath/_cli/cli_push.py | 217 ++++----- .../uipath/tests/cli/test_create_resources.py | 414 ++++++++++++++++++ packages/uipath/tests/cli/test_push.py | 71 ++- packages/uipath/uv.lock | 4 +- 13 files changed, 903 insertions(+), 224 deletions(-) create mode 100644 packages/uipath/src/uipath/_cli/_push/_resolvers.py create mode 100644 packages/uipath/src/uipath/_cli/_push/_resource_actions.py create mode 100644 packages/uipath/src/uipath/_cli/_push/_summary.py create mode 100644 packages/uipath/src/uipath/_cli/_push/_virtual_kinds.py create mode 100644 packages/uipath/tests/cli/test_create_resources.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index cfc151484..abbc9afa9 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.34" +version = "0.1.35" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/resource_catalog/_resource_catalog_service.py b/packages/uipath-platform/src/uipath/platform/resource_catalog/_resource_catalog_service.py index 030d85d01..e42880e71 100644 --- a/packages/uipath-platform/src/uipath/platform/resource_catalog/_resource_catalog_service.py +++ b/packages/uipath-platform/src/uipath/platform/resource_catalog/_resource_catalog_service.py @@ -1,4 +1,4 @@ -from typing import Any, AsyncIterator, Dict, Iterator, List, Optional +from typing import Any, AsyncGenerator, Dict, Iterator, List, Optional from uipath.core.tracing import traced @@ -110,7 +110,7 @@ async def search_async( resource_types: Optional[List[ResourceType]] = None, resource_sub_types: Optional[List[str]] = None, page_size: int = _DEFAULT_PAGE_SIZE, - ) -> AsyncIterator[Resource]: + ) -> AsyncGenerator[Resource, None]: """Asynchronously search for tenant scoped resources and folder scoped resources (accessible to the user). This method automatically handles pagination and yields resources one by one. @@ -258,7 +258,7 @@ async def list_async( folder_path: Optional[str] = None, folder_key: Optional[str] = None, page_size: int = _DEFAULT_PAGE_SIZE, - ) -> AsyncIterator[Resource]: + ) -> AsyncGenerator[Resource, None]: """Asynchronously get tenant scoped resources and folder scoped resources (accessible to the user). If no folder identifier is provided (path or key) only tenant resources will be retrieved. @@ -428,7 +428,7 @@ async def list_by_type_async( folder_path: Optional[str] = None, folder_key: Optional[str] = None, page_size: int = _DEFAULT_PAGE_SIZE, - ) -> AsyncIterator[Resource]: + ) -> AsyncGenerator[Resource, None]: """Asynchronously get resources of a specific type (tenant scoped or folder scoped). If no folder identifier is provided (path or key) only tenant resources will be retrieved. diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 8d6d60a56..ff4ccc1f5 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.34" +version = "0.1.35" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index ee647db03..b80902532 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.51" +version = "2.10.52" 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/_push/_resolvers.py b/packages/uipath/src/uipath/_cli/_push/_resolvers.py new file mode 100644 index 000000000..e609014d5 --- /dev/null +++ b/packages/uipath/src/uipath/_cli/_push/_resolvers.py @@ -0,0 +1,177 @@ +from typing import AsyncIterator + +from uipath.platform.connections import ConnectionsService +from uipath.platform.errors import EnrichedException, FolderNotFoundException +from uipath.platform.resource_catalog import ( + Resource, + ResourceCatalogService, + ResourceType, +) + +from .._utils._studio_project import ( + ReferencedResourceFolder, + ReferencedResourceRequest, + VirtualResourceRequest, +) +from ..models.runtime_schema import BindingResource, Bindings +from ._resource_actions import CreateReference, CreateVirtual, ResourceAction, Skip + +_NOT_FOUND_SUFFIX = "was not found and will not be added to the solution." + + +async def resolve_bindings( + bindings: Bindings, + resource_catalog: ResourceCatalogService, + connections: ConnectionsService, + supported_virtual_kinds: set[str], +) -> AsyncIterator[ResourceAction]: + """Yield one ResourceAction per importable binding. + + Bindings that should be silently ignored (e.g. guardrail bindings without a + folderPath) are filtered out here. + """ + for binding in bindings.resources: + action = await _resolve_binding( + binding, resource_catalog, connections, supported_virtual_kinds + ) + if action is not None: + yield action + + +async def _resolve_binding( + binding: BindingResource, + resource_catalog: ResourceCatalogService, + connections: ConnectionsService, + supported_virtual_kinds: set[str], +) -> ResourceAction | None: + if binding.resource == "connection": + return await _resolve_connection(binding, resource_catalog, connections) + return await _resolve_regular(binding, resource_catalog, supported_virtual_kinds) + + +async def _resolve_connection( + binding: BindingResource, + resource_catalog: ResourceCatalogService, + connections: ConnectionsService, +) -> ResourceAction | None: + connection_id_value = binding.value.get("ConnectionId") + if connection_id_value is None: + raise ValueError( + f"Connection binding {binding.key!r} is missing required field 'ConnectionId'" + ) + connection_key = connection_id_value.default_value + + try: + connection = await connections.retrieve_async(connection_key) + except EnrichedException: + connector_name = (binding.metadata or {}).get("Connector") + return Skip( + message=( + f"Connection with key '{connection_key}' of type " + f"'{connector_name}' {_NOT_FOUND_SUFFIX}" + ) + ) + + resource_name: str = connection.name + folder_path: str = connection.folder.get("path") + + found = await _find_in_resource_catalog( + resource_catalog, "connection", resource_name, folder_path + ) + if found is None: + return Skip( + message=( + f"Resource '{resource_name}' of type 'connection' at folder path " + f"'{folder_path}' {_NOT_FOUND_SUFFIX}" + ) + ) + return _build_create_reference(found, resource_name) + + +async def _resolve_regular( + binding: BindingResource, + resource_catalog: ResourceCatalogService, + supported_virtual_kinds: set[str], +) -> ResourceAction | None: + name_value = binding.value.get("name") + folder_path_value = binding.value.get("folderPath") + if not folder_path_value: + # guardrail resource, nothing to import + return None + if name_value is None: + raise ValueError(f"Binding {binding.key!r} is missing required field 'name'") + resource_name: str = name_value.default_value + folder_path: str = folder_path_value.default_value + resource_type: str = binding.resource + + found = await _find_in_resource_catalog( + resource_catalog, resource_type, resource_name, folder_path + ) + if found is not None: + return _build_create_reference(found, resource_name) + + if resource_type not in supported_virtual_kinds: + return Skip( + message=( + f"Cannot create virtual resource '{resource_name}' — " + f"kind '{resource_type}' is not supported." + ) + ) + + sub_type: str | None = (binding.metadata or {}).get("SubType") + return CreateVirtual( + request=VirtualResourceRequest( + kind=resource_type, + name=resource_name, + type=sub_type, + ) + ) + + +async def _find_in_resource_catalog( + resource_catalog: ResourceCatalogService, + resource_type: str, + name: str, + folder_path: str, +) -> Resource | None: + """Look up a single resource in the Resource Catalog. + + Returns the first match or None if the catalog can't search this kind, the + folder is unknown, or no resource matches. + """ + catalog_type = next( + (m for m in ResourceType if m.value == resource_type.lower()), None + ) + if catalog_type is None: + return None + + resources = resource_catalog.list_by_type_async( + resource_type=catalog_type, name=name, folder_path=folder_path + ) + try: + return await anext(resources, None) + except FolderNotFoundException: + return None + finally: + await resources.aclose() + + +def _build_create_reference( + found_resource: Resource, resource_name: str +) -> CreateReference: + folder = next(iter(found_resource.folders)) + return CreateReference( + request=ReferencedResourceRequest( + key=found_resource.resource_key, + kind=found_resource.resource_type, + type=found_resource.resource_sub_type, + folder=ReferencedResourceFolder( + folder_key=folder.key, + fully_qualified_name=folder.fully_qualified_name, + path=folder.path, + ), + ), + resource_name=resource_name, + kind=found_resource.resource_type, + sub_type=found_resource.resource_sub_type, + ) diff --git a/packages/uipath/src/uipath/_cli/_push/_resource_actions.py b/packages/uipath/src/uipath/_cli/_push/_resource_actions.py new file mode 100644 index 000000000..4eb76bd57 --- /dev/null +++ b/packages/uipath/src/uipath/_cli/_push/_resource_actions.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass + +from .._utils._studio_project import ( + ReferencedResourceRequest, + VirtualResourceRequest, +) + + +@dataclass(frozen=True, slots=True) +class CreateReference: + request: ReferencedResourceRequest + resource_name: str + kind: str + sub_type: str | None + + +@dataclass(frozen=True, slots=True) +class CreateVirtual: + request: VirtualResourceRequest + + +@dataclass(frozen=True, slots=True) +class Skip: + message: str + + +ResourceAction = CreateReference | CreateVirtual | Skip diff --git a/packages/uipath/src/uipath/_cli/_push/_summary.py b/packages/uipath/src/uipath/_cli/_push/_summary.py new file mode 100644 index 000000000..2dcbca57e --- /dev/null +++ b/packages/uipath/src/uipath/_cli/_push/_summary.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass + +import click + + +@dataclass +class ResourceImportSummary: + created: int = 0 + updated: int = 0 + unchanged: int = 0 + virtual_created: int = 0 + virtual_existing: int = 0 + not_found: int = 0 + + @property + def total(self) -> int: + return ( + self.created + + self.updated + + self.unchanged + + self.virtual_created + + self.virtual_existing + + self.not_found + ) + + def __str__(self) -> str: + return ( + f"\n \U0001f535 Resource import summary: {self.total} total resources - " + f"{click.style(str(self.created), fg='green')} created, " + f"{click.style(str(self.updated), fg='blue')} updated, " + f"{click.style(str(self.unchanged), fg='yellow')} unchanged, " + f"{click.style(str(self.virtual_created), fg='green')} virtual-created, " + f"{click.style(str(self.virtual_existing), fg='yellow')} virtual-existing, " + f"{click.style(str(self.not_found), fg='red')} not found" + ) diff --git a/packages/uipath/src/uipath/_cli/_push/_virtual_kinds.py b/packages/uipath/src/uipath/_cli/_push/_virtual_kinds.py new file mode 100644 index 000000000..bc1b20f73 --- /dev/null +++ b/packages/uipath/src/uipath/_cli/_push/_virtual_kinds.py @@ -0,0 +1,34 @@ +import logging + +from .._utils._studio_project import ResourceBuilderMetadataEntry, StudioClient + +logger = logging.getLogger(__name__) + +_FALLBACK: frozenset[str] = frozenset( + {"app", "asset", "bucket", "process", "queue", "taskCatalog", "trigger"} +) + + +async def fetch_supported_virtual_kinds(studio_client: StudioClient) -> set[str]: + """Return the set of resource kinds that support inline creation. + + Falls back to a static list on any failure — the caller shouldn't have to + care whether the metadata endpoint was reachable. + """ + try: + metadata = await studio_client.get_resource_builder_metadata() + except Exception as e: + logger.debug("Resource Builder metadata fetch failed, using fallback: %s", e) + return set(_FALLBACK) + return _extract_supported_kinds(metadata) + + +def _extract_supported_kinds( + metadata: list[ResourceBuilderMetadataEntry], +) -> set[str]: + # metadata has one entry per (kind, type), so a kind may appear multiple times + return { + entry.kind + for entry in metadata + if any(version.supports_in_line_creation for version in entry.versions) + } diff --git a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py index 63ddceb37..1380d9119 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py +++ b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py @@ -6,7 +6,6 @@ from pathlib import PurePath from typing import Any, Callable, List, Optional, Union -import click from pydantic import BaseModel, ConfigDict, Field, field_validator from uipath._utils.constants import ( @@ -152,12 +151,20 @@ class LockInfo(BaseModel): solution_lock_key: Optional[str] = Field(alias="solutionLockKey") -class Severity(str, Enum): - """Severity level for virtual resource operation results.""" +class ResourceBuilderMetadataVersion(BaseModel): + model_config = ConfigDict(extra="allow") - SUCCESS = "success" - ATTENTION = "attention" - WARN = "warn" + supports_in_line_creation: bool = Field( + default=False, alias="supportsInLineCreation" + ) + + +class ResourceBuilderMetadataEntry(BaseModel): + model_config = ConfigDict(extra="allow") + + kind: str + type: str | None = None + versions: list[ResourceBuilderMetadataVersion] = Field(default_factory=list) class VirtualResourceRequest(BaseModel): @@ -173,16 +180,20 @@ class VirtualResourceRequest(BaseModel): api_version: Optional[str] = Field(default=None, alias="apiVersion") +class Status(str, Enum): + ADDED = "ADDED" + UNCHANGED = "UNCHANGED" + UPDATED = "UPDATED" + + class VirtualResourceResult(BaseModel): - """Result of a virtual resource creation operation. + """Structured outcome of a virtual resource creation attempt. - Attributes: - severity: The severity level (log, warn or attention) - message: The result message with styling + Only `ADDED` and `UNCHANGED` are possible — virtual resources are never + updated in place. """ - severity: Severity - message: str + status: Status class ReferencedResourceFolder(BaseModel): @@ -362,12 +373,6 @@ class ProjectLockUnavailableError(RuntimeError): pass -class Status(str, Enum): - ADDED = "ADDED" - UNCHANGED = "UNCHANGED" - UPDATED = "UPDATED" - - class ReferencedResourceResponse(BaseModel): """Response from creating a referenced resource. @@ -528,6 +533,19 @@ async def get_project_metadata_async(self) -> Optional[StudioProjectMetadata]: response.read().decode("utf-8") ) + async def get_resource_builder_metadata( + self, + ) -> list[ResourceBuilderMetadataEntry]: + response = await self.uipath.api_client.request_async( + "GET", + url="/studio_/backend/api/resourcebuilder/metadata", + scoped="org", + ) + return [ + ResourceBuilderMetadataEntry.model_validate(entry) + for entry in response.json() + ] + async def _get_existing_resources(self) -> List[dict[str, Any]]: if self._resources_cache is not None: return self._resources_cache @@ -599,68 +617,19 @@ async def get_resource_overwrites(self) -> dict[str, ResourceOverwrite]: async def create_virtual_resource( self, virtual_resource_request: VirtualResourceRequest ) -> VirtualResourceResult: - """Create a virtual resource or return appropriate status if it already exists. - - Args: - virtual_resource_request: The virtual resource request details + """Create a virtual resource, or report UNCHANGED if already present. - Returns: - VirtualResourceResult: Result indicating the operation status and a formatted message + Returns UNCHANGED when the same name+kind already exists in the + solution. Name collisions with a different kind are not checked + client-side — they surface as a server error via EnrichedException. """ - # Build base message with resource details - base_message_parts = [ - f"Resource {click.style(virtual_resource_request.name, fg='cyan')}", - f" (kind: {click.style(virtual_resource_request.kind, fg='yellow')}", - ] - - if virtual_resource_request.type: - base_message_parts.append( - f", type: {click.style(virtual_resource_request.type, fg='yellow')}" - ) - - if virtual_resource_request.activity_name: - base_message_parts.append( - f", activity: {click.style(virtual_resource_request.activity_name, fg='yellow')}" - ) - - base_message_parts.append(")") - base_message = "".join(base_message_parts) - + name = virtual_resource_request.name + kind = virtual_resource_request.kind existing_resources = await self._get_existing_resources() - # Check if resource with same kind and name exists - existing_same_kind = next( - ( - r - for r in existing_resources - if r["name"] == virtual_resource_request.name - and r["kind"] == virtual_resource_request.kind - ), - None, - ) - if existing_same_kind: - message = f"{base_message} already exists. Skipping..." - return VirtualResourceResult(severity=Severity.ATTENTION, message=message) + if any(r["name"] == name and r["kind"] == kind for r in existing_resources): + return VirtualResourceResult(status=Status.UNCHANGED) - # Check if resource with same name but different kind exists - existing_diff_kind = next( - ( - r - for r in existing_resources - if r["name"] == virtual_resource_request.name - and r["kind"] != virtual_resource_request.kind - ), - None, - ) - if existing_diff_kind: - message = ( - f"Cannot create {base_message}. " - f"A resource with this name already exists with kind {click.style(existing_diff_kind['kind'], fg='yellow')}. " - f"Consider renaming the resource in code." - ) - return VirtualResourceResult(severity=Severity.WARN, message=message) - - # Create the virtual resource solution_id = await self._get_solution_id() response = await self.uipath.api_client.request_async( "POST", @@ -669,21 +638,12 @@ async def create_virtual_resource( json=virtual_resource_request.model_dump(exclude_none=True), ) resource_key = response.json()["key"] - await self._update_resource_specs( - resource_key, new_specs={"name": virtual_resource_request.name} - ) + await self._update_resource_specs(resource_key, new_specs={"name": name}) - # Update cache with newly created resource if self._resources_cache is not None: - self._resources_cache.append( - { - "name": virtual_resource_request.name, - "kind": virtual_resource_request.kind, - } - ) + self._resources_cache.append({"name": name, "kind": kind}) - message = f"{base_message} created successfully." - return VirtualResourceResult(severity=Severity.SUCCESS, message=message) + return VirtualResourceResult(status=Status.ADDED) async def create_referenced_resource( self, referenced_resource_request: ReferencedResourceRequest diff --git a/packages/uipath/src/uipath/_cli/cli_push.py b/packages/uipath/src/uipath/_cli/cli_push.py index 61e46cbc6..9082d747f 100644 --- a/packages/uipath/src/uipath/_cli/cli_push.py +++ b/packages/uipath/src/uipath/_cli/cli_push.py @@ -5,9 +5,17 @@ import click from uipath.platform.common import UiPathConfig -from uipath.platform.errors import EnrichedException, FolderNotFoundException - -from ..platform.resource_catalog import ResourceType +from uipath.platform.errors import EnrichedException + +from ._push._resolvers import resolve_bindings +from ._push._resource_actions import ( + CreateReference, + CreateVirtual, + ResourceAction, + Skip, +) +from ._push._summary import ResourceImportSummary +from ._push._virtual_kinds import fetch_supported_virtual_kinds from ._push.sw_file_handler import SwFileHandler from ._telemetry import track_command from ._utils._common import ensure_coded_agent_project, may_override_files @@ -22,10 +30,9 @@ ) from ._utils._studio_project import ( ProjectLockUnavailableError, - ReferencedResourceFolder, - ReferencedResourceRequest, Status, StudioClient, + VirtualResourceRequest, ) from ._utils._uv_helpers import handle_uv_operations from .models.runtime_schema import Bindings @@ -42,133 +49,104 @@ def get_org_scoped_url(base_url: str) -> str: return org_scoped_url -async def create_resources(studio_client: StudioClient): +async def create_resources(studio_client: StudioClient) -> None: console.info("\nImporting referenced resources to Studio Web project...") from uipath.platform import UiPath uipath = UiPath() - resource_catalog = uipath.resource_catalog - connections = uipath.connections with open(UiPathConfig.bindings_file_path, "r") as f: - bindings_file_content = f.read() - - bindings = Bindings.model_validate_json(bindings_file_content) - - resources_not_found = 0 - resources_unchanged = 0 - resources_created = 0 - resource_updated = 0 - - for bindings_resource in bindings.resources: - not_found_warning = "was not found and will not be added to the solution." - found_resource = None - resource_type = bindings_resource.resource - if resource_type == "connection": - connection_key_resource_value = bindings_resource.value.get("ConnectionId") - assert connection_key_resource_value - connection_key = connection_key_resource_value.default_value - try: - connection = await connections.retrieve_async(connection_key) - except EnrichedException: - resources_not_found += 1 - assert bindings_resource.metadata is not None - connector_name = bindings_resource.metadata.get("Connector") - console.warning( - f"Connection with key '{connection_key}' of type '{connector_name}' " - f"{not_found_warning}" - ) - continue - resource_name = connection.name - folder_path = connection.folder.get("path") - else: - name_resource_value = bindings_resource.value.get("name") - folder_path_resource_value = bindings_resource.value.get("folderPath") - - if not folder_path_resource_value: - # guardrail resource, nothing to import - continue - - assert name_resource_value - resource_name = name_resource_value.default_value - folder_path = folder_path_resource_value.default_value - - resources = resource_catalog.list_by_type_async( - resource_type=ResourceType.from_string(resource_type), - name=resource_name, - folder_path=folder_path, - ) + bindings = Bindings.model_validate_json(f.read()) - try: - async for resource in resources: - found_resource = resource - break - await resources.aclose() + supported_virtual_kinds = await fetch_supported_virtual_kinds(studio_client) - except FolderNotFoundException: - pass + summary = ResourceImportSummary() + async for action in resolve_bindings( + bindings, + uipath.resource_catalog, + uipath.connections, + supported_virtual_kinds, + ): + await _execute_action(action, studio_client, summary) - if not found_resource: - console.warning( - f"Resource '{resource_name}' of type '{resource_type}' at folder path '{folder_path}' " - f"{not_found_warning}" - ) - resources_not_found += 1 - continue - - referenced_resource_request = ReferencedResourceRequest( - key=found_resource.resource_key, - kind=found_resource.resource_type, - type=found_resource.resource_sub_type, - folder=next( - ReferencedResourceFolder( - folder_key=folder.key, - fully_qualified_name=folder.fully_qualified_name, - path=folder.path, - ) - for folder in found_resource.folders - ), - ) - response = await studio_client.create_referenced_resource( - referenced_resource_request - ) + console.info(str(summary)) - resource_details = ( - f"(kind = {click.style(found_resource.resource_type, fg='cyan')}, " - f"type = {click.style(found_resource.resource_sub_type, fg='cyan')})" - ) - match response.status: - case Status.ADDED: - console.success( - f"Created reference for resource: {click.style(resource_name, fg='cyan')} " - f"{resource_details}" - ) - resources_created += 1 - case Status.UNCHANGED: - console.info( - f"Resource reference already exists ({click.style('unchanged', fg='yellow')}): {click.style(resource_name, fg='cyan')} " - f"{resource_details}" - ) - resources_unchanged += 1 - case Status.UPDATED: - console.info( - f"Resource reference already exists ({click.style('updated', fg='blue')}): {click.style(resource_name, fg='cyan')} " - f"{resource_details}" - ) - resource_updated += 1 +async def _execute_action( + action: ResourceAction, + studio_client: StudioClient, + summary: ResourceImportSummary, +) -> None: + match action: + case Skip(message=message): + console.warning(message) + summary.not_found += 1 - total_resources = ( - resources_created + resources_unchanged + resources_not_found + resource_updated - ) - console.info( - f"\n \U0001f535 Resource import summary: {total_resources} total resources - " - f"{click.style(str(resources_created), fg='green')} created, " - f"{click.style(str(resource_updated), fg='blue')} updated, " - f"{click.style(str(resources_unchanged), fg='yellow')} unchanged, " - f"{click.style(str(resources_not_found), fg='red')} not found" - ) + case CreateVirtual(request=request): + try: + result = await studio_client.create_virtual_resource(request) + except EnrichedException as e: + console.warning( + f"Failed to create virtual resource '{request.name}' of type " + f"'{request.kind}': {e}" + ) + summary.not_found += 1 + return + label = _format_virtual_label(request) + match result.status: + case Status.ADDED: + console.success(f"{label} created successfully.") + summary.virtual_created += 1 + case Status.UNCHANGED: + console.info(f"{label} already exists. Skipping...") + summary.virtual_existing += 1 + + case CreateReference( + request=request, + resource_name=resource_name, + kind=kind, + sub_type=sub_type, + ): + response = await studio_client.create_referenced_resource(request) + details = ( + f"(kind = {click.style(kind, fg='cyan')}, " + f"type = {click.style(sub_type, fg='cyan')})" + ) + match response.status: + case Status.ADDED: + console.success( + f"Created reference for resource: " + f"{click.style(resource_name, fg='cyan')} {details}" + ) + summary.created += 1 + case Status.UNCHANGED: + console.info( + f"Resource reference already exists " + f"({click.style('unchanged', fg='yellow')}): " + f"{click.style(resource_name, fg='cyan')} {details}" + ) + summary.unchanged += 1 + case Status.UPDATED: + console.info( + f"Resource reference already exists " + f"({click.style('updated', fg='blue')}): " + f"{click.style(resource_name, fg='cyan')} {details}" + ) + summary.updated += 1 + + +def _format_virtual_label(request: VirtualResourceRequest) -> str: + parts = [ + f"Resource {click.style(request.name, fg='cyan')}", + f" (kind: {click.style(request.kind, fg='yellow')}", + ] + if request.type: + parts.append(f", type: {click.style(request.type, fg='yellow')}") + if request.activity_name: + parts.append(f", activity: {click.style(request.activity_name, fg='yellow')}") + parts.append(")") + return "".join(parts) async def upload_source_files_to_project( @@ -262,7 +240,6 @@ def push(root: str, ignore_resources: bool, nolock: bool, overwrite: bool) -> No project_id = UiPathConfig.project_id if not project_id: console.error("UIPATH_PROJECT_ID environment variable not found.") - return studio_client = StudioClient(project_id=project_id) diff --git a/packages/uipath/tests/cli/test_create_resources.py b/packages/uipath/tests/cli/test_create_resources.py new file mode 100644 index 000000000..08012dccc --- /dev/null +++ b/packages/uipath/tests/cli/test_create_resources.py @@ -0,0 +1,414 @@ +"""Unit tests for cli_push.create_resources virtual-resource fallback.""" + +import json +import os +from types import SimpleNamespace +from typing import Any, List, Optional +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +from uipath._cli._utils._studio_project import ( + Status, + VirtualResourceResult, +) +from uipath.platform.errors import EnrichedException, FolderNotFoundException + + +def _enriched_exc( + status_code: int = 404, body: bytes = b"not found" +) -> EnrichedException: + """Build an EnrichedException backed by a real HTTPStatusError.""" + request = httpx.Request("GET", "https://example.test/x") + response = httpx.Response(status_code, content=body, request=request) + http_err = httpx.HTTPStatusError("x", request=request, response=response) + return EnrichedException(http_err) + + +class _AsyncIterator: + """Minimal async iterator with aclose() to mimic resource_catalog pagination.""" + + def __init__(self, items: List[Any], raise_exc: Optional[Exception] = None): + self._items = iter(items) + self._raise_exc = raise_exc + self.aclose = AsyncMock() + + def __aiter__(self): + return self + + async def __anext__(self): + if self._raise_exc is not None: + raise self._raise_exc + try: + return next(self._items) + except StopIteration: + raise StopAsyncIteration from None + + +def _make_bindings(resources: List[dict[str, Any]]) -> str: + return json.dumps({"version": "2.2", "resources": resources}) + + +def _asset_binding( + name: str = "my_asset", + folder_path: str = "Shared", + metadata: Optional[dict[str, Any]] = None, +) -> dict[str, Any]: + return { + "resource": "asset", + "key": f"binding-{name}", + "value": { + "name": { + "defaultValue": name, + "isExpression": False, + "displayName": "name", + }, + "folderPath": { + "defaultValue": folder_path, + "isExpression": False, + "displayName": "folderPath", + }, + }, + "metadata": metadata, + } + + +def _found_resource( + key: str = "resource-key", + resource_type: str = "asset", + resource_sub_type: str = "stringAsset", + folder_path: str = "Shared", +) -> SimpleNamespace: + folder = SimpleNamespace( + key="folder-key", + fully_qualified_name=folder_path, + path=folder_path, + ) + return SimpleNamespace( + resource_key=key, + resource_type=resource_type, + resource_sub_type=resource_sub_type, + folders=[folder], + ) + + +@pytest.fixture +def bindings_file(tmp_path, monkeypatch): + """Write a bindings file to a tmp path and patch UiPathConfig.bindings_file_path.""" + path = tmp_path / "bindings.json" + + def _writer(content: str) -> str: + path.write_text(content, encoding="utf-8") + return str(path) + + from uipath.platform.common._config import ConfigurationManager + + monkeypatch.setattr( + ConfigurationManager, + "bindings_file_path", + property(lambda self: path), + ) + return _writer + + +@pytest.fixture +def mock_uipath(): + """Patch UiPath() in cli_push to return a mock with resource_catalog + connections. + + cli_push imports `UiPath` lazily from `uipath.platform` inside create_resources, + so we patch the source module. + """ + with patch("uipath.platform.UiPath") as mock_cls: + instance = MagicMock() + instance.resource_catalog = MagicMock() + instance.connections = MagicMock() + mock_cls.return_value = instance + yield instance + + +@pytest.fixture +def studio_client(): + from uipath._cli._utils._studio_project import ( + ResourceBuilderMetadataEntry, + ResourceBuilderMetadataVersion, + ) + + client = MagicMock() + client.create_referenced_resource = AsyncMock() + client.create_virtual_resource = AsyncMock() + supported = ResourceBuilderMetadataVersion(supportsInLineCreation=True) + # /metadata response — every kind our tests use supports inline creation. + client.get_resource_builder_metadata = AsyncMock( + return_value=[ + ResourceBuilderMetadataEntry(kind="asset", versions=[supported]), + ResourceBuilderMetadataEntry(kind="bucket", versions=[supported]), + ResourceBuilderMetadataEntry(kind="queue", versions=[supported]), + ResourceBuilderMetadataEntry(kind="taskCatalog", versions=[supported]), + ] + ) + return client + + +async def _run_create_resources(studio_client): + from uipath._cli.cli_push import create_resources + + await create_resources(studio_client) + + +async def test_catalog_hit_calls_referenced_resource_only( + bindings_file, mock_uipath, studio_client +): + bindings_file(_make_bindings([_asset_binding(metadata={"SubType": "stringAsset"})])) + mock_uipath.resource_catalog.list_by_type_async.return_value = _AsyncIterator( + [_found_resource()] + ) + studio_client.create_referenced_resource.return_value = SimpleNamespace( + status=Status.ADDED + ) + + await _run_create_resources(studio_client) + + studio_client.create_referenced_resource.assert_awaited_once() + studio_client.create_virtual_resource.assert_not_awaited() + + +async def test_catalog_miss_with_subtype_creates_virtual_with_type( + bindings_file, mock_uipath, studio_client +): + bindings_file(_make_bindings([_asset_binding(metadata={"SubType": "stringAsset"})])) + mock_uipath.resource_catalog.list_by_type_async.return_value = _AsyncIterator([]) + studio_client.create_virtual_resource.return_value = VirtualResourceResult( + status=Status.ADDED, + ) + + await _run_create_resources(studio_client) + + studio_client.create_virtual_resource.assert_awaited_once() + req = studio_client.create_virtual_resource.call_args.args[0] + assert req.kind == "asset" + assert req.name == "my_asset" + assert req.type == "stringAsset" + studio_client.create_referenced_resource.assert_not_awaited() + + +async def test_catalog_miss_without_subtype_creates_virtual_kind_only( + bindings_file, mock_uipath, studio_client +): + bindings_file(_make_bindings([_asset_binding(metadata=None)])) + mock_uipath.resource_catalog.list_by_type_async.return_value = _AsyncIterator([]) + studio_client.create_virtual_resource.return_value = VirtualResourceResult( + status=Status.ADDED, + ) + + await _run_create_resources(studio_client) + + studio_client.create_virtual_resource.assert_awaited_once() + req = studio_client.create_virtual_resource.call_args.args[0] + assert req.kind == "asset" + assert req.name == "my_asset" + assert req.type is None + # Body that will actually be sent excludes None → no "type" key. + body = req.model_dump(exclude_none=True) + assert "type" not in body + + +async def test_catalog_miss_metadata_without_subtype_key_creates_virtual_kind_only( + bindings_file, mock_uipath, studio_client +): + bindings_file(_make_bindings([_asset_binding(metadata={"Other": "x"})])) + mock_uipath.resource_catalog.list_by_type_async.return_value = _AsyncIterator([]) + studio_client.create_virtual_resource.return_value = VirtualResourceResult( + status=Status.ADDED, + ) + + await _run_create_resources(studio_client) + + req = studio_client.create_virtual_resource.call_args.args[0] + assert req.type is None + + +async def test_unknown_resource_type_skips_catalog_and_creates_virtual( + bindings_file, mock_uipath, studio_client +): + """Bindings with a resource kind unknown to ResourceType enum but supported + by the virtual endpoint (e.g. 'taskCatalog') should skip the resource + catalog lookup and fall through to the virtual fallback instead of raising + ValueError.""" + task_catalog_binding = { + "resource": "taskCatalog", + "key": "live.good.taskcatalog.Shared", + "value": { + "name": { + "defaultValue": "live.good.taskcatalog", + "isExpression": False, + "displayName": "Name", + }, + "folderPath": { + "defaultValue": "Shared", + "isExpression": False, + "displayName": "Folder Path", + }, + }, + "metadata": None, + } + bindings_file(_make_bindings([task_catalog_binding])) + studio_client.create_virtual_resource.return_value = VirtualResourceResult( + status=Status.ADDED, + ) + + await _run_create_resources(studio_client) + + mock_uipath.resource_catalog.list_by_type_async.assert_not_called() + studio_client.create_virtual_resource.assert_awaited_once() + req = studio_client.create_virtual_resource.call_args.args[0] + assert req.kind == "taskCatalog" + assert req.type is None + + +async def test_unsupported_virtual_kind_is_skipped_with_warning( + bindings_file, mock_uipath, studio_client +): + """Bindings whose kind the virtual endpoint cannot materialize (e.g. + 'entity', 'choiceSet', 'webhook') should be skipped with a warning and + never reach create_virtual_resource.""" + entity_binding = { + "resource": "entity", + "key": "live.good.entity.Shared", + "value": { + "name": { + "defaultValue": "live.good.entity", + "isExpression": False, + "displayName": "Name", + }, + "folderPath": { + "defaultValue": "Shared", + "isExpression": False, + "displayName": "Folder Path", + }, + }, + "metadata": None, + } + bindings_file(_make_bindings([entity_binding])) + + await _run_create_resources(studio_client) + + mock_uipath.resource_catalog.list_by_type_async.assert_not_called() + studio_client.create_virtual_resource.assert_not_awaited() + studio_client.create_referenced_resource.assert_not_awaited() + + +async def test_folder_not_found_falls_back_to_virtual( + bindings_file, mock_uipath, studio_client +): + bindings_file(_make_bindings([_asset_binding(metadata={"SubType": "stringAsset"})])) + mock_uipath.resource_catalog.list_by_type_async.return_value = _AsyncIterator( + [], raise_exc=FolderNotFoundException("missing folder") + ) + studio_client.create_virtual_resource.return_value = VirtualResourceResult( + status=Status.ADDED, + ) + + await _run_create_resources(studio_client) + + studio_client.create_virtual_resource.assert_awaited_once() + req = studio_client.create_virtual_resource.call_args.args[0] + assert req.kind == "asset" + assert req.type == "stringAsset" + + +async def test_virtual_enriched_exception_caught_and_logged_as_warning( + bindings_file, mock_uipath, studio_client +): + bindings_file( + _make_bindings( + [ + _asset_binding(name="first"), + _asset_binding(name="second"), + ] + ) + ) + mock_uipath.resource_catalog.list_by_type_async.return_value = _AsyncIterator([]) + # First binding raises, second succeeds → loop must continue past the raise. + studio_client.create_virtual_resource.side_effect = [ + _enriched_exc(status_code=500, body=b"boom"), + VirtualResourceResult(status=Status.ADDED), + ] + + await _run_create_resources(studio_client) + + assert studio_client.create_virtual_resource.await_count == 2 + + +async def test_connection_branch_unchanged_no_virtual_fallback( + bindings_file, mock_uipath, studio_client +): + """Connection bindings retain old behavior: retrieve_async + warn on miss, no virtual.""" + connection_binding = { + "resource": "connection", + "key": "binding-conn", + "value": { + "ConnectionId": { + "defaultValue": "missing-conn-id", + "isExpression": False, + "displayName": "ConnectionId", + } + }, + "metadata": {"Connector": "salesforce"}, + } + bindings_file(_make_bindings([connection_binding])) + mock_uipath.connections.retrieve_async = AsyncMock(side_effect=_enriched_exc()) + + await _run_create_resources(studio_client) + + mock_uipath.connections.retrieve_async.assert_awaited_once() + studio_client.create_virtual_resource.assert_not_awaited() + studio_client.create_referenced_resource.assert_not_awaited() + + +async def test_guardrail_binding_without_folder_path_is_skipped( + bindings_file, mock_uipath, studio_client +): + # No folderPath in value → guardrail; should be skipped entirely. + guardrail_binding = { + "resource": "asset", + "key": "binding-guard", + "value": { + "name": { + "defaultValue": "g", + "isExpression": False, + "displayName": "name", + } + }, + "metadata": None, + } + bindings_file(_make_bindings([guardrail_binding])) + + await _run_create_resources(studio_client) + + mock_uipath.resource_catalog.list_by_type_async.assert_not_called() + studio_client.create_virtual_resource.assert_not_awaited() + studio_client.create_referenced_resource.assert_not_awaited() + + +# Ensure env doesn't leak solution ids between tests. +@pytest.fixture(autouse=True) +def _reset_solution_id(): + from uipath.platform.common._config import ConfigurationManager + + ConfigurationManager.studio_solution_id = None + yield + ConfigurationManager.studio_solution_id = None + + +# Set minimal env so UiPath() construction inside create_resources (if not mocked +# away cleanly) doesn't trip on missing creds. The mock_uipath fixture patches +# the class, so this is defense-in-depth. +@pytest.fixture(autouse=True) +def _env(monkeypatch): + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "mock_token") + yield + for k in ("UIPATH_URL", "UIPATH_ACCESS_TOKEN"): + if k in os.environ: + monkeypatch.delenv(k, raising=False) diff --git a/packages/uipath/tests/cli/test_push.py b/packages/uipath/tests/cli/test_push.py index d2189098d..d4d30a166 100644 --- a/packages/uipath/tests/cli/test_push.py +++ b/packages/uipath/tests/cli/test_push.py @@ -1928,6 +1928,14 @@ def test_push_with_resources_imports_referenced_resources( json=mock_structure, ) + httpx_mock.add_response( + method="GET", + url=f"{base_url}/studio_/backend/api/resourcebuilder/metadata", + json=[ + {"kind": "asset", "versions": [{"supportsInLineCreation": True}]}, + ], + ) + # Mock getting the solution ID httpx_mock.add_response( method="GET", @@ -2171,7 +2179,7 @@ def test_push_with_ignore_resources_flag_skips_resource_import( assert "Created reference for resource" not in result.output assert "Resource import summary" not in result.output - def test_push_with_resource_not_found_shows_warning( + def test_push_with_resource_not_found_creates_virtual( self, runner: CliRunner, temp_dir: str, @@ -2179,9 +2187,11 @@ def test_push_with_resource_not_found_shows_warning( mock_env_vars: dict[str, str], httpx_mock: HTTPXMock, ) -> None: - """Test that push shows warning when referenced resource is not found in catalog.""" + """When catalog lookup misses, push creates a virtual resource placeholder.""" base_url = "https://cloud.uipath.com/organization" project_id = "test-project-id" + solution_id = "test-solution-id" + tenant_id = "test-tenant-id" mock_structure = { "id": "root", @@ -2226,6 +2236,43 @@ def test_push_with_resource_not_found_shows_warning( json=mock_structure, ) + # Resource Builder metadata — declares which kinds support inline creation. + httpx_mock.add_response( + method="GET", + url=f"{base_url}/studio_/backend/api/resourcebuilder/metadata", + json=[ + {"kind": "asset", "versions": [{"supportsInLineCreation": True}]}, + ], + ) + + # Solution ID lookup for the virtual-resource fallback + httpx_mock.add_response( + method="GET", + url=f"{base_url}/studio_/backend/api/Project/{project_id}", + json={"solutionId": solution_id}, + ) + + # Existing resources in the solution (empty → no conflict with our new virtual) + httpx_mock.add_response( + method="GET", + url=f"{base_url}/studio_/backend/api/resourcebuilder/solutions/{solution_id}/entities", + json={"resources": []}, + ) + + # Virtual resource POST + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/resourcebuilder/solutions/{solution_id}/resources/virtual", + json={"key": "virtual-resource-key-123"}, + ) + + # Configuration PATCH after virtual creation + httpx_mock.add_response( + method="PATCH", + url=f"{base_url}/studio_/backend/api/resourcebuilder/solutions/{solution_id}/resources/virtual-resource-key-123/configuration", + json={}, + ) + with runner.isolated_filesystem(temp_dir=temp_dir): # Create required files with open("uipath.json", "w") as f: @@ -2255,6 +2302,7 @@ def test_push_with_resource_not_found_shows_warning( "ActivityName": "retrieve_async", "BindingsVersion": "2.2", "DisplayLabel": "FullName", + "SubType": "stringAsset", }, } ], @@ -2273,6 +2321,7 @@ def test_push_with_resource_not_found_shows_warning( configure_env_vars(mock_env_vars) os.environ["UIPATH_PROJECT_ID"] = project_id + os.environ["UIPATH_TENANT_ID"] = tenant_id # Mock resource catalog list_by_type_async to return no resources async def mock_list_by_type_async_empty(*args, **kwargs): @@ -2291,16 +2340,14 @@ async def mock_list_by_type_async_empty(*args, **kwargs): result = runner.invoke(cli, ["push", "./"]) assert result.exit_code == 0 - # Check that warning was shown for missing resource + # Check that the virtual-resource fallback ran and succeeded assert ( "Importing referenced resources to Studio Web project" in result.output ) - assert ( - "Resource 'missing.asset' of type 'asset' at folder path 'Default' was not found" - in result.output - ) + assert "missing.asset" in result.output + assert "created successfully" in result.output assert "Resource import summary:" in result.output - assert "1 not found" in result.output + assert "1 virtual-created" in result.output def test_push_with_resource_already_exists_shows_unchanged( self, @@ -2359,6 +2406,14 @@ def test_push_with_resource_already_exists_shows_unchanged( json=mock_structure, ) + httpx_mock.add_response( + method="GET", + url=f"{base_url}/studio_/backend/api/resourcebuilder/metadata", + json=[ + {"kind": "asset", "versions": [{"supportsInLineCreation": True}]}, + ], + ) + # Mock getting the solution ID httpx_mock.add_response( method="GET", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 0d146ae6c..e167bb65c 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.51" +version = "2.10.52" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.34" +version = "0.1.35" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 27646a42a59679a06267f98c40077ea371dce3b5 Mon Sep 17 00:00:00 2001 From: Akshaya Shanbhogue Date: Wed, 22 Apr 2026 08:49:24 -0700 Subject: [PATCH 028/121] fix(mocks): pass invocation as a tuple to avoid arg name collisions (#1585) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath/pyproject.toml | 2 +- .../src/uipath/eval/mocks/_llm_mocker.py | 7 +- .../src/uipath/eval/mocks/_mock_context.py | 6 +- .../uipath/src/uipath/eval/mocks/_mocker.py | 3 +- .../src/uipath/eval/mocks/_mockito_mocker.py | 7 +- .../uipath/src/uipath/eval/mocks/mockable.py | 2 +- .../eval/mocks/test_mockable_arg_collision.py | 107 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 8 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 packages/uipath/tests/cli/eval/mocks/test_mockable_arg_collision.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index b80902532..ae68beed7 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.52" +version = "2.10.53" 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/eval/mocks/_llm_mocker.py b/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py index 3715ac226..d1fd2a1c9 100644 --- a/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py +++ b/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py @@ -96,11 +96,16 @@ def __init__(self, context: MockingContext): @traced(name="__mocker__", recording=False) async def response( - self, func: Callable[[T], R], params: dict[str, Any], *args: T, **kwargs + self, + func: Callable[[T], R], + params: dict[str, Any], + invocation: tuple[tuple[Any, ...], dict[str, Any]], ) -> R: """Respond with mocked response generated by an LLM.""" assert isinstance(self.context.strategy, LLMMockingStrategy) + args, kwargs = invocation + function_name = params.get("name") or func.__name__ if function_name in [x.name for x in self.context.strategy.tools_to_simulate]: uipath = UiPath() diff --git a/packages/uipath/src/uipath/eval/mocks/_mock_context.py b/packages/uipath/src/uipath/eval/mocks/_mock_context.py index c2335544f..a50df9cba 100644 --- a/packages/uipath/src/uipath/eval/mocks/_mock_context.py +++ b/packages/uipath/src/uipath/eval/mocks/_mock_context.py @@ -64,11 +64,13 @@ def is_tool_simulated(tool_name: str) -> bool: async def get_mocked_response( - func: Callable[[Any], Any], params: dict[str, Any], *args, **kwargs + func: Callable[[Any], Any], + params: dict[str, Any], + invocation: tuple[tuple[Any, ...], dict[str, Any]], ) -> Any: """Get a mocked response.""" mocker = mocker_context.get() if mocker is None: raise UiPathNoMockFoundError() else: - return await mocker.response(func, params, *args, **kwargs) + return await mocker.response(func, params, invocation) diff --git a/packages/uipath/src/uipath/eval/mocks/_mocker.py b/packages/uipath/src/uipath/eval/mocks/_mocker.py index 57cb8bcc3..99e5da1b2 100644 --- a/packages/uipath/src/uipath/eval/mocks/_mocker.py +++ b/packages/uipath/src/uipath/eval/mocks/_mocker.py @@ -16,8 +16,7 @@ async def response( self, func: Callable[[T], R], params: dict[str, Any], - *args: T, - **kwargs, + invocation: tuple[tuple[Any, ...], dict[str, Any]], ) -> R: """Respond with mocked response.""" raise NotImplementedError() diff --git a/packages/uipath/src/uipath/eval/mocks/_mockito_mocker.py b/packages/uipath/src/uipath/eval/mocks/_mockito_mocker.py index 041478baf..a9b30230f 100644 --- a/packages/uipath/src/uipath/eval/mocks/_mockito_mocker.py +++ b/packages/uipath/src/uipath/eval/mocks/_mockito_mocker.py @@ -99,12 +99,17 @@ def __init__(self, context: MockingContext): stubbed = stubbed.thenRaise(_resolve_value(answer_dict["value"])) async def response( - self, func: Callable[[T], R], params: dict[str, Any], *args: T, **kwargs + self, + func: Callable[[T], R], + params: dict[str, Any], + invocation: tuple[tuple[Any, ...], dict[str, Any]], ) -> R: """Return mocked response or raise appropriate errors.""" if not isinstance(self.context.strategy, MockitoMockingStrategy): raise UiPathMockResponseGenerationError("Mocking strategy misconfigured.") + args, kwargs = invocation + # No behavior configured → call real function is_mocked = any( behavior.function == params["name"] diff --git a/packages/uipath/src/uipath/eval/mocks/mockable.py b/packages/uipath/src/uipath/eval/mocks/mockable.py index 254f88b89..3e9a324b9 100644 --- a/packages/uipath/src/uipath/eval/mocks/mockable.py +++ b/packages/uipath/src/uipath/eval/mocks/mockable.py @@ -39,7 +39,7 @@ def mocked_response_decorator(func, params: dict[str, Any]): """Mocked response decorator.""" async def mock_response_generator(*args, **kwargs): - mocked_response = await get_mocked_response(func, params, *args, **kwargs) + mocked_response = await get_mocked_response(func, params, (args, kwargs)) # Mocking successful. context = UiPathSpanUtils.get_parent_context() diff --git a/packages/uipath/tests/cli/eval/mocks/test_mockable_arg_collision.py b/packages/uipath/tests/cli/eval/mocks/test_mockable_arg_collision.py new file mode 100644 index 000000000..838e9d835 --- /dev/null +++ b/packages/uipath/tests/cli/eval/mocks/test_mockable_arg_collision.py @@ -0,0 +1,107 @@ +"""Regression tests: @mockable must not collide with user args named `func`/`params`.""" + +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from uipath.eval.mocks import mockable +from uipath.eval.mocks._mock_runtime import ( + clear_execution_context, + set_execution_context, +) +from uipath.eval.mocks._types import MockingContext +from uipath.eval.models.evaluation_set import EvaluationItem + +_mock_span_collector = MagicMock() + + +def _build_evaluation( + function_name: str, kwargs: dict[str, Any], value: Any +) -> EvaluationItem: + evaluation_item: dict[str, Any] = { + "id": "evaluation-id", + "name": "Test evaluation", + "inputs": {}, + "evaluationCriterias": {"ExactMatchEvaluator": None}, + "mockingStrategy": { + "type": "mockito", + "behaviors": [ + { + "function": function_name, + "arguments": {"args": [], "kwargs": kwargs}, + "then": [{"type": "return", "value": value}], + } + ], + }, + } + return EvaluationItem(**evaluation_item) + + +class TestMockableArgCollision: + """Ensure `@mockable` works when the wrapped function has args named `func` or `params`.""" + + def test_sync_function_with_func_and_params_args(self): + """A sync mockable function that takes `func` and `params` kwargs should not raise.""" + + @mockable() + def test_function(func: str, params: dict[str, Any]) -> str: + raise NotImplementedError() + + evaluation = _build_evaluation( + "test_function", + kwargs={"func": "some_func", "params": {"k": "v"}}, + value="mocked_result", + ) + + set_execution_context( + MockingContext( + strategy=evaluation.mocking_strategy, + name=evaluation.name, + inputs=evaluation.inputs, + ), + _mock_span_collector, + "test-execution-id", + ) + + try: + with patch("uipath.eval.mocks.mockable.UiPathSpanUtils"): + with patch("uipath.eval.mocks.mockable.trace"): + result = test_function(func="some_func", params={"k": "v"}) + + assert result == "mocked_result" + finally: + clear_execution_context() + + @pytest.mark.asyncio + async def test_async_function_with_func_and_params_args(self): + """An async mockable function that takes `func` and `params` kwargs should not raise.""" + + @mockable() + async def test_function(func: str, params: dict[str, Any]) -> str: + raise NotImplementedError() + + evaluation = _build_evaluation( + "test_function", + kwargs={"func": "some_func", "params": {"k": "v"}}, + value="mocked_result", + ) + + set_execution_context( + MockingContext( + strategy=evaluation.mocking_strategy, + name=evaluation.name, + inputs=evaluation.inputs, + ), + _mock_span_collector, + "test-execution-id", + ) + + try: + with patch("uipath.eval.mocks.mockable.UiPathSpanUtils"): + with patch("uipath.eval.mocks.mockable.trace"): + result = await test_function(func="some_func", params={"k": "v"}) + + assert result == "mocked_result" + finally: + clear_execution_context() diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e167bb65c..d922431c1 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.52" +version = "2.10.53" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From d1122b9d4a4d3770afd655b581b56b75217b44e5 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Thu, 23 Apr 2026 14:27:35 +0200 Subject: [PATCH 029/121] feat(debug): add --attach flag for non-interactive debug runs (#1587) --- packages/uipath/pyproject.toml | 4 +- .../uipath/src/uipath/_cli/_debug/_bridge.py | 23 +++++-- packages/uipath/src/uipath/_cli/cli_debug.py | 22 ++++++- .../tests/cli/test_debug_bridge_selection.py | 64 +++++++++++++++++++ packages/uipath/uv.lock | 10 +-- 5 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 packages/uipath/tests/cli/test_debug_bridge_selection.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index ae68beed7..a8d04271e 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "uipath" -version = "2.10.53" +version = "2.10.54" 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" dependencies = [ "uipath-core>=0.5.8, <0.6.0", - "uipath-runtime>=0.10.0, <0.11.0", + "uipath-runtime>=0.10.1, <0.11.0", "uipath-platform>=0.1.13, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", diff --git a/packages/uipath/src/uipath/_cli/_debug/_bridge.py b/packages/uipath/src/uipath/_cli/_debug/_bridge.py index 9607398a0..2ad4e0418 100644 --- a/packages/uipath/src/uipath/_cli/_debug/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_debug/_bridge.py @@ -19,9 +19,15 @@ UiPathRuntimeResult, UiPathRuntimeStatus, ) -from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugQuitError +from uipath.runtime.debug import ( + DetachedDebugBridge, + UiPathDebugProtocol, + UiPathDebugQuitError, +) from uipath.runtime.events import UiPathRuntimeStateEvent, UiPathRuntimeStatePhase +DebugAttachMode = Literal["signalr", "console", "none"] + logger = logging.getLogger(__name__) @@ -871,18 +877,27 @@ def get_remote_debug_bridge(context: UiPathRuntimeContext) -> UiPathDebugProtoco def get_debug_bridge( - context: UiPathRuntimeContext, verbose: bool = True + context: UiPathRuntimeContext, + verbose: bool = True, + attach: DebugAttachMode | None = None, ) -> UiPathDebugProtocol: """Factory to get appropriate debug bridge based on context. Args: context: The runtime context containing debug configuration. verbose: If True, console bridge shows all state updates. If False, only breakpoints. + attach: Explicit attach mode. When None, falls back to + ``context.job_id``-based selection. Returns: An instance of UiPathDebugBridge suitable for the context. """ - if context.job_id: + if attach == "none": + return DetachedDebugBridge() + if attach == "signalr": return get_remote_debug_bridge(context) - else: + if attach == "console": return ConsoleDebugBridge(verbose=verbose) + if context.job_id: + return get_remote_debug_bridge(context) + return ConsoleDebugBridge(verbose=verbose) diff --git a/packages/uipath/src/uipath/_cli/cli_debug.py b/packages/uipath/src/uipath/_cli/cli_debug.py index d2e08353b..cd9042b78 100644 --- a/packages/uipath/src/uipath/_cli/cli_debug.py +++ b/packages/uipath/src/uipath/_cli/cli_debug.py @@ -1,10 +1,11 @@ import asyncio import logging +from typing import cast, get_args import click from uipath._cli._chat._bridge import get_chat_bridge -from uipath._cli._debug._bridge import get_debug_bridge +from uipath._cli._debug._bridge import DebugAttachMode, get_debug_bridge from uipath._cli._utils._debug import setup_debugging from uipath._cli._utils._studio_project import StudioClient from uipath.core.tracing import UiPathTraceManager @@ -64,6 +65,15 @@ default=5678, help="Port for the debug server (default: 5678)", ) +@click.option( + "--attach", + type=click.Choice(list(get_args(DebugAttachMode)), case_sensitive=False), + default=None, + help=( + "Debugger attach mode. Defaults to 'signalr' for cloud runs, " + "'console' for local runs." + ), +) @track_command("debug") def debug( entrypoint: str | None, @@ -74,6 +84,7 @@ def debug( output_file: str | None, debug: bool, debug_port: int, + attach: str | None, ) -> None: """Debug the project.""" input_file = file or input_file @@ -81,6 +92,10 @@ def debug( if not setup_debugging(debug, debug_port): console.error(f"Failed to start debug server on port {debug_port}") + attach_mode: DebugAttachMode | None = ( + cast(DebugAttachMode, attach.lower()) if attach else None + ) + result = Middlewares.next( "debug", entrypoint, @@ -90,6 +105,7 @@ def debug( output_file=output_file, debug=debug, debug_port=debug_port, + attach=attach_mode, ) if result.error_message: @@ -141,7 +157,9 @@ async def execute_debug_runtime(): async def execute_debug_runtime(): chat_runtime: UiPathRuntimeProtocol | None = None - debug_bridge: UiPathDebugProtocol = get_debug_bridge(ctx) + debug_bridge: UiPathDebugProtocol = get_debug_bridge( + ctx, attach=attach_mode + ) runtime = await factory.new_runtime( entrypoint, diff --git a/packages/uipath/tests/cli/test_debug_bridge_selection.py b/packages/uipath/tests/cli/test_debug_bridge_selection.py new file mode 100644 index 000000000..732461c77 --- /dev/null +++ b/packages/uipath/tests/cli/test_debug_bridge_selection.py @@ -0,0 +1,64 @@ +"""Tests for `get_debug_bridge()` selection matrix. + +Locks in the non-breaking-change contract: absence of `attach` preserves the +legacy `job_id`-based selection. Explicit `attach` overrides that selection. +""" + +from __future__ import annotations + +import pytest + +from uipath._cli._debug._bridge import ( + ConsoleDebugBridge, + SignalRDebugBridge, + get_debug_bridge, +) +from uipath.runtime import UiPathRuntimeContext +from uipath.runtime.debug import DetachedDebugBridge + + +def _ctx(**overrides) -> UiPathRuntimeContext: + return UiPathRuntimeContext(**overrides) + + +def test_attach_none_returns_detached_bridge_without_job_id(): + bridge = get_debug_bridge(_ctx(), attach="none") + assert isinstance(bridge, DetachedDebugBridge) + + +def test_attach_none_returns_detached_bridge_even_when_job_id_set(monkeypatch): + """'none' wins over job_id — this is the whole point of the flag.""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="none") + assert isinstance(bridge, DetachedDebugBridge) + + +def test_attach_signalr_forces_signalr_bridge(monkeypatch): + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="signalr") + assert isinstance(bridge, SignalRDebugBridge) + + +def test_attach_console_forces_console_bridge_even_when_job_id_set(monkeypatch): + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="console") + assert isinstance(bridge, ConsoleDebugBridge) + + +def test_legacy_selection_signalr_when_job_id_set_and_no_attach(monkeypatch): + """Non-breaking change assertion: absence of `attach` preserves today's behavior.""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + bridge = get_debug_bridge(_ctx(job_id="job-123")) + assert isinstance(bridge, SignalRDebugBridge) + + +def test_legacy_selection_console_when_no_job_id_and_no_attach(): + """Non-breaking change assertion: absence of `attach` preserves today's behavior.""" + bridge = get_debug_bridge(_ctx()) + assert isinstance(bridge, ConsoleDebugBridge) + + +def test_attach_signalr_without_job_id_raises(): + """Explicit signalr without job_id is a user error — surface it loudly.""" + with pytest.raises(ValueError, match="UIPATH_URL and UIPATH_JOB_KEY"): + get_debug_bridge(_ctx(), attach="signalr") diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d922431c1..22bd59925 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.53" +version = "2.10.54" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2616,7 +2616,7 @@ requires-dist = [ { name = "truststore", specifier = ">=0.10.1" }, { name = "uipath-core", editable = "../uipath-core" }, { name = "uipath-platform", editable = "../uipath-platform" }, - { name = "uipath-runtime", specifier = ">=0.10.0,<0.11.0" }, + { name = "uipath-runtime", specifier = ">=0.10.1,<0.11.0" }, ] [package.metadata.requires-dev] @@ -2720,14 +2720,14 @@ dev = [ [[package]] name = "uipath-runtime" -version = "0.10.0" +version = "0.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/64/69462ee01a5607ce36b1fa152c52ac72fb28abe0aa049394406fc0b31525/uipath_runtime-0.10.0.tar.gz", hash = "sha256:d27d58e2252f506c8c0e00f814b37c3863150e8ffcde8e4c6ab14bd98febd3df", size = 139626, upload-time = "2026-03-24T19:42:43.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/87/2e625219b3364a7153549e6056bce41d2050725ed0844f2711c414a872c0/uipath_runtime-0.10.1.tar.gz", hash = "sha256:9ed1bdb6737ad64cc5bb7ef0c8466dbae8ca010858ecd856818396ea264eb3d5", size = 141189, upload-time = "2026-04-23T11:34:53.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/9c0e97a078b96e4d3742ea3515cb30886b08579cd08077cd42a159adf70d/uipath_runtime-0.10.0-py3-none-any.whl", hash = "sha256:4f52df0b56f54e70fcf34fbf74e223d02b97b5a6fd6d8f64bc06782bb5484b07", size = 42097, upload-time = "2026-03-24T19:42:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/ad/41/bc3465ee89dd01f8a9045d7d22d0f0927c0d437242eeded8d3d5b33f50ed/uipath_runtime-0.10.1-py3-none-any.whl", hash = "sha256:f04483db92ee7683513762a79bf48c229c7133d5adc7fef10ea5eaa4c7ce9b29", size = 43057, upload-time = "2026-04-23T11:34:51.781Z" }, ] [[package]] From 28ad47c572e73a201627de12d95ea7037f879ebc Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Thu, 23 Apr 2026 16:33:34 +0200 Subject: [PATCH 030/121] docs(cli): document debug and eval commands (#1588) --- packages/uipath/docs/cli/index.md | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index 336040742..9e8401e8c 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -308,3 +308,92 @@ Processing: uipath.json File 'uipath.json' is up to date ✓ Project pulled successfully ``` +--- + +::: mkdocs-click + :module: uipath._cli + :command: debug + :depth: 1 + :style: table + +Runs your agent under the debug runtime, with a debug bridge attached. Locally, the bridge is the interactive **console** (read commands from stdin, stop at breakpoints). In the cloud, the bridge is **SignalR** (driven by Studio Web / Orchestrator). The `--attach` flag lets you override that default, including `none` for executors that need the debug command's surrounding behavior (bindings fetch, state streaming) but cannot speak the interactive debug protocol. + +### Attach modes + +| Mode | When to use | +|------|-------------| +| `signalr` | Remote runs driven by Studio Web / Orchestrator. Default when `job_id` is set. | +| `console` | Local interactive debugging from the terminal. Default when no `job_id`. | +| `none` | Run under the debug command without attaching a debugger. No wait-for-start gate, no breakpoints, no step mode. | + +/// info +`--attach` selects the **debug bridge**. It's unrelated to `--debug`, which starts a `debugpy` server for Python-level breakpoints in your IDE. The two can be combined. +/// + + + +```shell +> uipath debug main '{"message": "test"}' +Debug Mode Commands + c, continue Continue until next breakpoint + s, step Step to next node + b Set breakpoint at + l, list List all breakpoints + r Remove breakpoint at + h, help Show help + q, quit Exit debugger +▶ START +> b analyze_sentiment +✓ Breakpoint set at: analyze_sentiment +> c +──────────────────────────────────────── +■ BREAKPOINT analyze_sentiment (before) +Next: analyze_sentiment +──────────────────────────────────────── +> s +● analyze_sentiment +> c +✓ Execution completed +``` +--- + +::: mkdocs-click + :module: uipath._cli + :command: eval + :depth: 1 + :style: table + +Runs an evaluation set against your agent. Entry point and eval set are auto-discovered from the project if not passed explicitly. Evaluations run in parallel (see `--workers`) and, unless `--no-report` is passed, results are reported back to Studio Web when `UIPATH_PROJECT_ID` is set. + +### Common flags + +| Flag | Purpose | +|------|---------| +| `--eval-ids` | Run only a subset of evaluations by id. | +| `--workers` | Parallel workers for running evaluations (default 1). | +| `--no-report` | Skip reporting results back to UiPath. | +| `--enable-mocker-cache` | Cache LLM mocker responses across runs. | +| `--input-overrides` | Per-eval input overrides, merged into the eval's input. | +| `--trace-file` | Write OpenTelemetry traces to a JSONL file for offline inspection. | +| `--resume` | Resume evaluation from a previous suspended state. | + + + +```shell +> uipath eval +⠋ Running evaluations ... + Weather in Paris + LLM Judge Output 0.7 + Tool Call Arguments 1.0 + Tool Call Count 1.0 + Tool Call Order 1.0 + +Evaluation Results +┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓ +┃ Evaluation ┃ LLM Judge Output ┃ Tool Call Args ┃ Tool Call Count ┃ Tool Call Order ┃ +┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩ +│ Weather in Paris │ 0.7 │ 1.0 │ 1.0 │ 1.0 │ +├────────────────────┼────────────────────┼────────────────────┼────────────────────┼────────────────────┤ +│ Average │ 0.7 │ 1.0 │ 1.0 │ 1.0 │ +└────────────────────┴────────────────────┴────────────────────┴────────────────────┴────────────────────┘ +``` From 59e427ea65256752079ee707d79bda6eecca792a Mon Sep 17 00:00:00 2001 From: Mayank Jha Date: Thu, 23 Apr 2026 15:12:27 -0700 Subject: [PATCH 031/121] feat: add memorySpace to resource overwrite types (#1586) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath-platform/src/uipath/platform/common/_bindings.py | 1 + packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index abbc9afa9..914a16e13 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.35" +version = "0.1.36" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_bindings.py b/packages/uipath-platform/src/uipath/platform/common/_bindings.py index 224cae425..a93880896 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_bindings.py +++ b/packages/uipath-platform/src/uipath/platform/common/_bindings.py @@ -60,6 +60,7 @@ class GenericResourceOverwrite(ResourceOverwrite): "mcpServer", "queue", "remoteA2aAgent", + "memorySpace", ] name: str = Field(alias="name") folder_path: str = Field(alias="folderPath") diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index ff4ccc1f5..079a3dd27 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.35" +version = "0.1.36" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 22bd59925..0839674b1 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.35" +version = "0.1.36" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 64399bfb7c8c78da1971ba4f73747ba5da1315f2 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Fri, 24 Apr 2026 11:07:59 +0200 Subject: [PATCH 032/121] fix: handle debug signalR bridge race condition (#1590) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/_debug/_bridge.py | 3 +++ packages/uipath/uv.lock | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index a8d04271e..1bc9f87c0 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.54" +version = "2.10.55" 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/_debug/_bridge.py b/packages/uipath/src/uipath/_cli/_debug/_bridge.py index 2ad4e0418..cfd286b5c 100644 --- a/packages/uipath/src/uipath/_cli/_debug/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_debug/_bridge.py @@ -747,6 +747,9 @@ async def _handle_start(self, args: list[Any]) -> None: f"Debug started: breakpoints={self.state.breakpoints}, step_mode={step_mode}" ) + # handle race conditions, runtime connected to debug bridge before the receiver + await self.emit_execution_started() + async def _handle_resume(self, args: list[Any]) -> None: """Handle Resume command from SignalR server. diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 0839674b1..880aa92f7 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.54" +version = "2.10.55" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From f82df052506aeca37167dca80bdddfe0a8ef560b Mon Sep 17 00:00:00 2001 From: CalebMartinUiPath <106194045+CalebMartinUiPath@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:11:32 -0700 Subject: [PATCH 033/121] fix: batch transform failure handling (#1573) --- packages/uipath-platform/pyproject.toml | 2 +- .../_context_grounding_service.py | 9 +++++ .../src/uipath/platform/errors/__init__.py | 3 ++ .../_batch_transform_failed_exception.py | 10 ++++++ .../platform/resume_triggers/_protocol.py | 6 ++++ .../tests/services/test_hitl.py | 35 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 8 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/errors/_batch_transform_failed_exception.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 914a16e13..f25b5cf88 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.36" +version = "0.1.37" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py index e577c5082..706394aca 100644 --- a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py +++ b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py @@ -17,6 +17,7 @@ ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE, ) from ..errors import ( + BatchTransformFailedException, BatchTransformNotCompleteException, IngestionInProgressException, UnsupportedDataSourceException, @@ -1050,6 +1051,10 @@ def download_batch_transform_result( batch_transform = self.retrieve_batch_transform( id=id, index_name=index_name ) + if batch_transform.last_batch_rag_status == BatchTransformStatus.FAILED: + raise BatchTransformFailedException( + batch_transform_id=id, + ) if batch_transform.last_batch_rag_status != BatchTransformStatus.SUCCESSFUL: raise BatchTransformNotCompleteException( batch_transform_id=id, @@ -1109,6 +1114,10 @@ async def download_batch_transform_result_async( batch_transform = await self.retrieve_batch_transform_async( id=id, index_name=index_name ) + if batch_transform.last_batch_rag_status == BatchTransformStatus.FAILED: + raise BatchTransformFailedException( + batch_transform_id=id, + ) if batch_transform.last_batch_rag_status != BatchTransformStatus.SUCCESSFUL: raise BatchTransformNotCompleteException( batch_transform_id=id, diff --git a/packages/uipath-platform/src/uipath/platform/errors/__init__.py b/packages/uipath-platform/src/uipath/platform/errors/__init__.py index 3c446fe14..58afd93f7 100644 --- a/packages/uipath-platform/src/uipath/platform/errors/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/errors/__init__.py @@ -8,6 +8,7 @@ - FolderNotFoundException: Raised when a folder cannot be found - UnsupportedDataSourceException: Raised when an operation is attempted on an unsupported data source type - IngestionInProgressException: Raised when a search is attempted on an index during ingestion +- BatchTransformFailedException: Raised when a batch transform has failed - BatchTransformNotCompleteException: Raised when attempting to get results from an incomplete batch transform - OperationNotCompleteException: Raised when attempting to get results from an incomplete operation - OperationFailedException: Raised when an operation has failed @@ -15,6 +16,7 @@ """ from ._base_url_missing_error import BaseUrlMissingError +from ._batch_transform_failed_exception import BatchTransformFailedException from ._batch_transform_not_complete_exception import BatchTransformNotCompleteException from ._enriched_exception import EnrichedException, ExtractedErrorInfo from ._folder_not_found_exception import FolderNotFoundException @@ -26,6 +28,7 @@ __all__ = [ "BaseUrlMissingError", + "BatchTransformFailedException", "BatchTransformNotCompleteException", "EnrichedException", "ExtractedErrorInfo", diff --git a/packages/uipath-platform/src/uipath/platform/errors/_batch_transform_failed_exception.py b/packages/uipath-platform/src/uipath/platform/errors/_batch_transform_failed_exception.py new file mode 100644 index 000000000..67088b0f1 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/errors/_batch_transform_failed_exception.py @@ -0,0 +1,10 @@ +class BatchTransformFailedException(Exception): + """Raised when a batch transform has failed. + + This exception is raised when a batch transform task has completed + with a failed status, as opposed to still being in progress. + """ + + def __init__(self, batch_transform_id: str): + self.message = f"Batch transform '{batch_transform_id}' failed." + super().__init__(self.message) diff --git a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py index 60a169da9..98e4f81a2 100644 --- a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py @@ -53,6 +53,7 @@ ContextGroundingIndex, ) from uipath.platform.errors import ( + BatchTransformFailedException, BatchTransformNotCompleteException, OperationNotCompleteException, ) @@ -323,6 +324,11 @@ async def read_trigger(self, trigger: UiPathResumeTrigger) -> Any | None: "index_name", trigger.payload ), ) + except BatchTransformFailedException as e: + raise UiPathFaultedTriggerError( + ErrorCategory.SYSTEM, + f"{e.message}", + ) from e except BatchTransformNotCompleteException as e: raise UiPathPendingTriggerError( ErrorCategory.SYSTEM, diff --git a/packages/uipath-platform/tests/services/test_hitl.py b/packages/uipath-platform/tests/services/test_hitl.py index aa288137d..2e8069a3e 100644 --- a/packages/uipath-platform/tests/services/test_hitl.py +++ b/packages/uipath-platform/tests/services/test_hitl.py @@ -809,6 +809,41 @@ async def test_read_batch_rag_trigger_pending( reader = UiPathResumeTriggerReader() await reader.read_trigger(resume_trigger) + @pytest.mark.anyio + async def test_read_batch_rag_trigger_failed( + self, + setup_test_env: None, + ) -> None: + """Test reading a failed batch rag trigger raises faulted error.""" + from uipath.core.errors import UiPathFaultedTriggerError + + from uipath.platform.errors import BatchTransformFailedException + + task_id = "test-batch-rag-id" + destination_path = "test/output.xlsx" + mock_download_async = AsyncMock( + side_effect=BatchTransformFailedException(task_id) + ) + + with patch( + "uipath.platform.context_grounding._context_grounding_service.ContextGroundingService.download_batch_transform_result_async", + new=mock_download_async, + ): + resume_trigger = UiPathResumeTrigger( + trigger_type=UiPathResumeTriggerType.BATCH_RAG, + item_key=task_id, + folder_key="test-folder", + folder_path="test-path", + payload={ + "index_name": "test-index", + "destination_path": destination_path, + }, + ) + + with pytest.raises(UiPathFaultedTriggerError): + reader = UiPathResumeTriggerReader() + await reader.read_trigger(resume_trigger) + @pytest.mark.anyio async def test_read_ephemeral_index_trigger_successful( self, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 079a3dd27..11138cfbd 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.36" +version = "0.1.37" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 880aa92f7..2bc995e74 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.36" +version = "0.1.37" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From cc24c8eb783b5a28f99d21491c63a46a9c74d358 Mon Sep 17 00:00:00 2001 From: Mayank Jha Date: Fri, 24 Apr 2026 15:55:36 -0700 Subject: [PATCH 034/121] feat: add @resource_override and folder_path support to MemoryService (#1591) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath/platform/memory/_memory_service.py | 62 ++++++++++++++----- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index f25b5cf88..e32ca5495 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.37" +version = "0.1.38" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py b/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py index 89ec86a25..73d788f80 100644 --- a/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py +++ b/packages/uipath-platform/src/uipath/platform/memory/_memory_service.py @@ -10,6 +10,7 @@ from uipath.core.tracing import traced from ..common._base_service import BaseService +from ..common._bindings import resource_override from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..common._folder_context import FolderContext, header_folder @@ -50,6 +51,7 @@ def __init__( # ── Memory space operations (ECS) ────────────────────────────────── + @resource_override(resource_type="memorySpace") @traced(name="memory_create", run_type="uipath") def create( self, @@ -57,6 +59,7 @@ def create( description: Optional[str] = None, is_encrypted: Optional[bool] = None, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> MemorySpace: """Create a new memory space. @@ -65,11 +68,14 @@ def create( description: Optional description (max 1024 chars). is_encrypted: Whether the memory space should be encrypted. folder_key: The folder key for the operation. + folder_path: The folder path for the operation. Returns: MemorySpace: The created memory space. """ - spec = self._create_spec(name, description, is_encrypted, folder_key) + spec = self._create_spec( + name, description, is_encrypted, folder_key, folder_path + ) response = self.request( spec.method, spec.endpoint, @@ -78,6 +84,7 @@ def create( ).json() return MemorySpace.model_validate(response) + @resource_override(resource_type="memorySpace") @traced(name="memory_create", run_type="uipath") async def create_async( self, @@ -85,6 +92,7 @@ async def create_async( description: Optional[str] = None, is_encrypted: Optional[bool] = None, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> MemorySpace: """Asynchronously create a new memory space. @@ -93,11 +101,14 @@ async def create_async( description: Optional description (max 1024 chars). is_encrypted: Whether the memory space should be encrypted. folder_key: The folder key for the operation. + folder_path: The folder path for the operation. Returns: MemorySpace: The created memory space. """ - spec = self._create_spec(name, description, is_encrypted, folder_key) + spec = self._create_spec( + name, description, is_encrypted, folder_key, folder_path + ) response = ( await self.request_async( spec.method, @@ -116,6 +127,7 @@ def list( top: Optional[int] = None, skip: Optional[int] = None, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> MemorySpaceListResponse: """List memory spaces with optional OData query parameters. @@ -125,11 +137,12 @@ def list( top: Maximum number of results. skip: Number of results to skip. folder_key: The folder key for the operation. + folder_path: The folder path for the operation. Returns: MemorySpaceListResponse: The list of memory spaces. """ - spec = self._list_spec(filter, orderby, top, skip, folder_key) + spec = self._list_spec(filter, orderby, top, skip, folder_key, folder_path) response = self.request( spec.method, spec.endpoint, @@ -146,6 +159,7 @@ async def list_async( top: Optional[int] = None, skip: Optional[int] = None, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> MemorySpaceListResponse: """Asynchronously list memory spaces. @@ -155,11 +169,12 @@ async def list_async( top: Maximum number of results. skip: Number of results to skip. folder_key: The folder key for the operation. + folder_path: The folder path for the operation. Returns: MemorySpaceListResponse: The list of memory spaces. """ - spec = self._list_spec(filter, orderby, top, skip, folder_key) + spec = self._list_spec(filter, orderby, top, skip, folder_key, folder_path) response = ( await self.request_async( spec.method, @@ -178,6 +193,7 @@ def search( memory_space_id: str, request: MemorySearchRequest, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> MemorySearchResponse: """Search a memory space via LLMOps. @@ -188,11 +204,12 @@ def search( memory_space_id: The GUID of the memory space. request: The search request payload. folder_key: The folder key for the operation. + folder_path: The folder path for the operation. Returns: MemorySearchResponse: Results, metadata, and system prompt injection. """ - spec = self._search_spec(memory_space_id, folder_key) + spec = self._search_spec(memory_space_id, folder_key, folder_path) response = self.request( spec.method, spec.endpoint, @@ -207,6 +224,7 @@ async def search_async( memory_space_id: str, request: MemorySearchRequest, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> MemorySearchResponse: """Asynchronously search a memory space via LLMOps. @@ -217,11 +235,12 @@ async def search_async( memory_space_id: The GUID of the memory space. request: The search request payload. folder_key: The folder key for the operation. + folder_path: The folder path for the operation. Returns: MemorySearchResponse: Results, metadata, and system prompt injection. """ - spec = self._search_spec(memory_space_id, folder_key) + spec = self._search_spec(memory_space_id, folder_key, folder_path) response = ( await self.request_async( spec.method, @@ -240,6 +259,7 @@ def escalation_search( memory_space_id: str, request: MemorySearchRequest, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> EscalationMemorySearchResponse: """Search escalation memory for previously resolved outcomes. @@ -250,11 +270,12 @@ def escalation_search( memory_space_id: The GUID of the memory space. request: The search request payload (same as regular search). folder_key: The folder key for the operation. + folder_path: The folder path for the operation. Returns: EscalationMemorySearchResponse: Matched escalation outcomes. """ - spec = self._escalation_search_spec(memory_space_id, folder_key) + spec = self._escalation_search_spec(memory_space_id, folder_key, folder_path) response = self.request( spec.method, spec.endpoint, @@ -269,6 +290,7 @@ async def escalation_search_async( memory_space_id: str, request: MemorySearchRequest, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> EscalationMemorySearchResponse: """Asynchronously search escalation memory for previously resolved outcomes. @@ -279,11 +301,12 @@ async def escalation_search_async( memory_space_id: The GUID of the memory space. request: The search request payload (same as regular search). folder_key: The folder key for the operation. + folder_path: The folder path for the operation. Returns: EscalationMemorySearchResponse: Matched escalation outcomes. """ - spec = self._escalation_search_spec(memory_space_id, folder_key) + spec = self._escalation_search_spec(memory_space_id, folder_key, folder_path) response = ( await self.request_async( spec.method, @@ -300,6 +323,7 @@ def escalation_ingest( memory_space_id: str, request: EscalationMemoryIngestRequest, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> None: """Ingest a resolved escalation outcome into memory. @@ -310,8 +334,9 @@ def escalation_ingest( memory_space_id: The GUID of the memory space. request: The escalation ingest payload. folder_key: The folder key for the operation. + folder_path: The folder path for the operation. """ - spec = self._escalation_ingest_spec(memory_space_id, folder_key) + spec = self._escalation_ingest_spec(memory_space_id, folder_key, folder_path) self.request( spec.method, spec.endpoint, @@ -325,6 +350,7 @@ async def escalation_ingest_async( memory_space_id: str, request: EscalationMemoryIngestRequest, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> None: """Asynchronously ingest a resolved escalation outcome into memory. @@ -335,8 +361,9 @@ async def escalation_ingest_async( memory_space_id: The GUID of the memory space. request: The escalation ingest payload. folder_key: The folder key for the operation. + folder_path: The folder path for the operation. """ - spec = self._escalation_ingest_spec(memory_space_id, folder_key) + spec = self._escalation_ingest_spec(memory_space_id, folder_key, folder_path) await self.request_async( spec.method, spec.endpoint, @@ -379,8 +406,9 @@ def _create_spec( description: Optional[str], is_encrypted: Optional[bool], folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder(folder_key) + folder_key = self._resolve_folder(folder_key, folder_path) body = MemorySpaceCreateRequest( name=name, description=description, @@ -400,8 +428,9 @@ def _list_spec( top: Optional[int], skip: Optional[int], folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder(folder_key) + folder_key = self._resolve_folder(folder_key, folder_path) params: dict[str, Any] = {} if filter is not None: params["$filter"] = filter @@ -424,8 +453,9 @@ def _search_spec( self, memory_space_id: str, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder(folder_key) + folder_key = self._resolve_folder(folder_key, folder_path) return RequestSpec( method="POST", endpoint=Endpoint(f"{_LLMOPS_AGENT_BASE}/{memory_space_id}/search"), @@ -436,8 +466,9 @@ def _escalation_search_spec( self, memory_space_id: str, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder(folder_key) + folder_key = self._resolve_folder(folder_key, folder_path) return RequestSpec( method="POST", endpoint=Endpoint( @@ -450,8 +481,9 @@ def _escalation_ingest_spec( self, memory_space_id: str, folder_key: Optional[str] = None, + folder_path: Optional[str] = None, ) -> RequestSpec: - folder_key = self._resolve_folder(folder_key) + folder_key = self._resolve_folder(folder_key, folder_path) return RequestSpec( method="POST", endpoint=Endpoint( diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 11138cfbd..6821bbc1c 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.37" +version = "0.1.38" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 2bc995e74..bf0fb3ed3 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.37" +version = "0.1.38" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 0c3dc51bf666f1f90f2523309ab545f72799d587 Mon Sep 17 00:00:00 2001 From: GabrielVasilescu04 Date: Mon, 27 Apr 2026 15:57:13 +0300 Subject: [PATCH 035/121] feat: add models-list command (#1474) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/__init__.py | 1 + .../uipath/src/uipath/_cli/cli_list_models.py | 64 +++++ .../integration/test_list_models_commands.py | 233 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 5 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 packages/uipath/src/uipath/_cli/cli_list_models.py create mode 100644 packages/uipath/tests/cli/integration/test_list_models_commands.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 1bc9f87c0..53d1a891f 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.55" +version = "2.10.56" 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/__init__.py b/packages/uipath/src/uipath/_cli/__init__.py index aa6e177e8..d8d3a8a46 100644 --- a/packages/uipath/src/uipath/_cli/__init__.py +++ b/packages/uipath/src/uipath/_cli/__init__.py @@ -45,6 +45,7 @@ "server": "cli_server", "register": "cli_register", "debug": "cli_debug", + "list-models": "cli_list_models", "assets": "services.cli_assets", "buckets": "services.cli_buckets", "context-grounding": "services.cli_context_grounding", diff --git a/packages/uipath/src/uipath/_cli/cli_list_models.py b/packages/uipath/src/uipath/_cli/cli_list_models.py new file mode 100644 index 000000000..7c14686a4 --- /dev/null +++ b/packages/uipath/src/uipath/_cli/cli_list_models.py @@ -0,0 +1,64 @@ +from collections.abc import Iterable + +import click +from rich.console import Console +from rich.table import Table + +from ..platform.agenthub import LlmModel +from ._utils._context import get_cli_context +from ._utils._service_base import ServiceCommandBase, service_command + + +@click.command(name="list-models") +@click.option( + "--format", + type=click.Choice(["json", "table", "csv"]), + help="Output format (overrides global)", +) +@click.option( + "--output", + "--output-file", + "-o", + type=click.Path(), + help="File path where the output will be written", +) +@service_command +async def list_models(ctx, format, output): + """List available LLM models.""" + client = ServiceCommandBase.get_client(ctx) + models = await client.agenthub.get_available_llm_models_async() + + fmt = format or get_cli_context(ctx).output_format + if fmt == "table" and not output: + _render_rich_table(models) + return None + return models + + +def _render_rich_table(models: Iterable[LlmModel]) -> None: + """Render models as a rich table with one column per vendor.""" + by_vendor: dict[str, list[str]] = {} + for model in models: + vendor = model.vendor or "Unknown" + by_vendor.setdefault(vendor, []).append(model.model_name) + + console = Console() + if not by_vendor: + console.print("Available LLM Models: none") + return + + for names in by_vendor.values(): + names.sort() + + vendors = sorted(by_vendor.keys()) + + table = Table(title="Available LLM Models", show_lines=False) + for vendor in vendors: + table.add_column(vendor, style="cyan", no_wrap=True) + + max_rows = max(len(by_vendor[v]) for v in vendors) + for i in range(max_rows): + row = [by_vendor[v][i] if i < len(by_vendor[v]) else "" for v in vendors] + table.add_row(*row) + + console.print(table) diff --git a/packages/uipath/tests/cli/integration/test_list_models_commands.py b/packages/uipath/tests/cli/integration/test_list_models_commands.py new file mode 100644 index 000000000..c7cb9f042 --- /dev/null +++ b/packages/uipath/tests/cli/integration/test_list_models_commands.py @@ -0,0 +1,233 @@ +"""Integration tests for the `uipath list-models` CLI command. + +The command renders a rich table grouped by vendor (one column per vendor) +for human terminal use, and falls through to the shared `format_output` +pipeline for `--format json|csv` and `--output `. +""" + +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from click.testing import CliRunner + +from uipath._cli import cli +from uipath.platform.agenthub import LlmModel + + +@pytest.fixture +def runner(): + """Provide a Click CLI test runner.""" + return CliRunner() + + +@pytest.fixture +def mock_client(): + """Provide a mocked UiPath client with an async agenthub service.""" + with patch("uipath.platform._uipath.UiPath") as mock: + client_instance = MagicMock() + mock.return_value = client_instance + + client_instance.agenthub = MagicMock() + client_instance.agenthub.get_available_llm_models_async = AsyncMock() + + yield client_instance + + +def _make_models() -> list[LlmModel]: + """Build a small list of LlmModel instances spanning multiple vendors.""" + return [ + LlmModel(model_name="gpt-4o-mini", vendor="OpenAi"), + LlmModel(model_name="gpt-4.1", vendor="OpenAi"), + LlmModel(model_name="claude-sonnet-4-5", vendor="Anthropic"), + LlmModel(model_name="gemini-2.5-flash", vendor="VertexAi"), + ] + + +class TestRichTable: + def test_renders_each_model_and_vendor(self, runner, mock_client, mock_env_vars): + """All models and vendor headers appear in the rendered table.""" + mock_client.agenthub.get_available_llm_models_async.return_value = ( + _make_models() + ) + + result = runner.invoke(cli, ["list-models"]) + + assert result.exit_code == 0 + for model in _make_models(): + assert model.model_name in result.output + assert (model.vendor or "") in result.output + mock_client.agenthub.get_available_llm_models_async.assert_awaited_once() + + def test_table_title(self, runner, mock_client, mock_env_vars): + """The rich table renders its title for orientation.""" + mock_client.agenthub.get_available_llm_models_async.return_value = ( + _make_models() + ) + + result = runner.invoke(cli, ["list-models"]) + + assert result.exit_code == 0 + assert "Available LLM Models" in result.output + + def test_missing_vendor_grouped_under_unknown( + self, runner, mock_client, mock_env_vars + ): + """A model with no vendor lands in an 'Unknown' column.""" + mock_client.agenthub.get_available_llm_models_async.return_value = [ + LlmModel(model_name="custom-model", vendor=None), + ] + + result = runner.invoke(cli, ["list-models"]) + + assert result.exit_code == 0 + assert "custom-model" in result.output + assert "Unknown" in result.output + + def test_empty(self, runner, mock_client, mock_env_vars): + """An empty model list renders the title without rows or errors.""" + mock_client.agenthub.get_available_llm_models_async.return_value = [] + + result = runner.invoke(cli, ["list-models"]) + + assert result.exit_code == 0 + assert "Available LLM Models" in result.output + + +class TestMachineReadableFormats: + def test_json_format(self, runner, mock_client, mock_env_vars): + """--format json bypasses the rich table and emits parseable JSON.""" + mock_client.agenthub.get_available_llm_models_async.return_value = ( + _make_models() + ) + + result = runner.invoke(cli, ["list-models", "--format", "json"]) + + assert result.exit_code == 0 + payload = json.loads(result.output) + assert isinstance(payload, list) + assert {m["model_name"] for m in payload} == { + "gpt-4o-mini", + "gpt-4.1", + "claude-sonnet-4-5", + "gemini-2.5-flash", + } + + def test_csv_format(self, runner, mock_client, mock_env_vars): + """--format csv emits a header row and one row per model.""" + mock_client.agenthub.get_available_llm_models_async.return_value = ( + _make_models() + ) + + result = runner.invoke(cli, ["list-models", "--format", "csv"]) + + assert result.exit_code == 0 + lines = [line for line in result.output.splitlines() if line.strip()] + assert "model_name" in lines[0] + assert "vendor" in lines[0] + assert any("gpt-4o-mini" in line for line in lines[1:]) + + def test_global_json_flag(self, runner, mock_client, mock_env_vars): + """The cli-group --format json is honored too.""" + mock_client.agenthub.get_available_llm_models_async.return_value = ( + _make_models() + ) + + result = runner.invoke(cli, ["--format", "json", "list-models"]) + + assert result.exit_code == 0 + payload = json.loads(result.output) + assert len(payload) == 4 + + def test_output_writes_through_plain_formatter( + self, runner, mock_client, mock_env_vars, tmp_path + ): + """--output writes through format_output (not the rich path).""" + mock_client.agenthub.get_available_llm_models_async.return_value = ( + _make_models() + ) + out_file = tmp_path / "models.json" + + result = runner.invoke( + cli, + ["list-models", "--format", "json", "--output", str(out_file)], + ) + + assert result.exit_code == 0 + assert out_file.exists() + payload = json.loads(out_file.read_text(encoding="utf-8")) + assert {m["model_name"] for m in payload} == { + "gpt-4o-mini", + "gpt-4.1", + "claude-sonnet-4-5", + "gemini-2.5-flash", + } + + def test_output_file_alias(self, runner, mock_client, mock_env_vars, tmp_path): + """`--output-file` works as an alias for `--output` (matches `run`).""" + mock_client.agenthub.get_available_llm_models_async.return_value = ( + _make_models() + ) + out_file = tmp_path / "models.json" + + result = runner.invoke( + cli, + ["list-models", "--format", "json", "--output-file", str(out_file)], + ) + + assert result.exit_code == 0 + assert out_file.exists() + payload = json.loads(out_file.read_text(encoding="utf-8")) + assert len(payload) == 4 + + +class TestErrorPaths: + def test_service_error(self, runner, mock_client, mock_env_vars): + """Exceptions from the service are surfaced as click errors.""" + mock_client.agenthub.get_available_llm_models_async.side_effect = RuntimeError( + "boom" + ) + + result = runner.invoke(cli, ["list-models"]) + + assert result.exit_code != 0 + assert "boom" in result.output + + def test_missing_url(self, runner, monkeypatch): + """Missing UIPATH_URL surfaces an auth-configuration error.""" + monkeypatch.delenv("UIPATH_URL", raising=False) + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "mock_token") + + result = runner.invoke(cli, ["list-models"]) + + assert result.exit_code != 0 + assert "UIPATH_URL not configured" in result.output + + def test_missing_token(self, runner, monkeypatch): + """Missing UIPATH_ACCESS_TOKEN surfaces an auth-configuration error.""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant") + monkeypatch.delenv("UIPATH_ACCESS_TOKEN", raising=False) + + result = runner.invoke(cli, ["list-models"]) + + assert result.exit_code != 0 + assert "Authentication required" in result.output + + +class TestRegistration: + def test_help_text(self, runner): + """--help surfaces the command description and options.""" + result = runner.invoke(cli, ["list-models", "--help"]) + + assert result.exit_code == 0 + assert "List available LLM models" in result.output + assert "--format" in result.output + assert "--output" in result.output + assert "--output-file" in result.output + + def test_registered_in_cli(self, runner): + """The command is wired into the top-level CLI group.""" + result = runner.invoke(cli, ["--help"]) + + assert result.exit_code == 0 + assert "list-models" in result.output diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index bf0fb3ed3..bb59734af 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.55" +version = "2.10.56" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 94567e4e20af61c56f609a556b078ee368ab4675 Mon Sep 17 00:00:00 2001 From: Josh Park <50765702+JoshParkSJ@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:32:22 -0400 Subject: [PATCH 036/121] feat: add confirmToolCall support to CAS chat bridge [JAR-8666] (#1558) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/chat/__init__.py | 33 +----- .../src/uipath/core/chat/interrupt.py | 112 ------------------ .../src/uipath/core/chat/message.py | 8 -- .../uipath-core/src/uipath/core/chat/tool.py | 37 ++++++ packages/uipath-core/uv.lock | 2 +- .../services/test_conversations_service.py | 3 - packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/_cli/_chat/_bridge.py | 112 ++++++------------ .../eval/models/_conversational_utils.py | 4 - packages/uipath/uv.lock | 4 +- 12 files changed, 86 insertions(+), 235 deletions(-) delete mode 100644 packages/uipath-core/src/uipath/core/chat/interrupt.py diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 387818039..2f38df428 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.12" +version = "0.5.13" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/chat/__init__.py b/packages/uipath-core/src/uipath/core/chat/__init__.py index c2e9f025b..d81fa8153 100644 --- a/packages/uipath-core/src/uipath/core/chat/__init__.py +++ b/packages/uipath-core/src/uipath/core/chat/__init__.py @@ -77,20 +77,6 @@ UiPathConversationExchangeEvent, UiPathConversationExchangeStartEvent, ) -from .interrupt import ( - InterruptTypeEnum, - UiPathConversationGenericInterruptEndEvent, - UiPathConversationGenericInterruptStartEvent, - UiPathConversationInterrupt, - UiPathConversationInterruptData, - UiPathConversationInterruptEndEvent, - UiPathConversationInterruptEvent, - UiPathConversationInterruptStartEvent, - UiPathConversationToolCallConfirmationEndValue, - UiPathConversationToolCallConfirmationInterruptEndEvent, - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationToolCallConfirmationValue, -) from .message import ( UiPathConversationMessage, UiPathConversationMessageData, @@ -108,6 +94,9 @@ ) from .tool import ( UiPathConversationToolCall, + UiPathConversationToolCallConfirmation, + UiPathConversationToolCallConfirmationData, + UiPathConversationToolCallConfirmationEvent, UiPathConversationToolCallData, UiPathConversationToolCallEndEvent, UiPathConversationToolCallEvent, @@ -146,19 +135,6 @@ "UiPathConversationMessageEvent", "UiPathConversationMessageData", "UiPathConversationMessage", - # Interrupt - "InterruptTypeEnum", - "UiPathConversationInterruptStartEvent", - "UiPathConversationInterruptEndEvent", - "UiPathConversationInterruptEvent", - "UiPathConversationToolCallConfirmationValue", - "UiPathConversationToolCallConfirmationEndValue", - "UiPathConversationToolCallConfirmationInterruptStartEvent", - "UiPathConversationToolCallConfirmationInterruptEndEvent", - "UiPathConversationGenericInterruptStartEvent", - "UiPathConversationGenericInterruptEndEvent", - "UiPathConversationInterruptData", - "UiPathConversationInterrupt", # Content "UiPathConversationContentPartChunkEvent", "UiPathConversationContentPartStartEvent", @@ -183,6 +159,9 @@ # Tool "UiPathConversationToolCallStartEvent", "UiPathConversationToolCallEndEvent", + "UiPathConversationToolCallConfirmation", + "UiPathConversationToolCallConfirmationData", + "UiPathConversationToolCallConfirmationEvent", "UiPathConversationToolCallEvent", "UiPathConversationToolCallResult", "UiPathConversationToolCallData", diff --git a/packages/uipath-core/src/uipath/core/chat/interrupt.py b/packages/uipath-core/src/uipath/core/chat/interrupt.py deleted file mode 100644 index a2ce3e13f..000000000 --- a/packages/uipath-core/src/uipath/core/chat/interrupt.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Interrupt events for human-in-the-loop patterns.""" - -from enum import Enum -from typing import Any, Literal, Union - -from pydantic import BaseModel, ConfigDict, Field - - -class InterruptTypeEnum(str, Enum): - """Enum of known interrupt types.""" - - TOOL_CALL_CONFIRMATION = "uipath_cas_tool_call_confirmation" - - -class UiPathConversationToolCallConfirmationValue(BaseModel): - """Schema for tool call confirmation interrupt value.""" - - tool_call_id: str = Field(..., alias="toolCallId") - tool_name: str = Field(..., alias="toolName") - input_schema: Any = Field(..., alias="inputSchema") - input_value: Any | None = Field(None, alias="inputValue") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationToolCallConfirmationInterruptStartEvent(BaseModel): - """Tool call confirmation interrupt start event with strong typing.""" - - type: Literal["uipath_cas_tool_call_confirmation"] - value: UiPathConversationToolCallConfirmationValue - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationGenericInterruptStartEvent(BaseModel): - """Generic interrupt start event for custom interrupt types.""" - - type: str - value: Any - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -UiPathConversationInterruptStartEvent = Union[ - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationGenericInterruptStartEvent, -] - - -class UiPathConversationToolCallConfirmationEndValue(BaseModel): - """Schema for tool call confirmation end value.""" - - approved: bool - input: Any | None = None - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationToolCallConfirmationInterruptEndEvent(BaseModel): - """Tool call confirmation interrupt end event with strong typing.""" - - type: Literal["uipath_cas_tool_call_confirmation"] - value: UiPathConversationToolCallConfirmationEndValue - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationGenericInterruptEndEvent(BaseModel): - """Generic interrupt end event for custom interrupt types.""" - - type: str - value: Any - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -UiPathConversationInterruptEndEvent = Union[ - UiPathConversationToolCallConfirmationInterruptEndEvent, - UiPathConversationGenericInterruptEndEvent, -] - - -class UiPathConversationInterruptEvent(BaseModel): - """Encapsulates interrupt-related events within a message.""" - - interrupt_id: str = Field(..., alias="interruptId") - start: UiPathConversationInterruptStartEvent | None = Field( - None, alias="startInterrupt" - ) - end: UiPathConversationInterruptEndEvent | None = Field(None, alias="endInterrupt") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationInterruptData(BaseModel): - """Represents the core data of an interrupt within a message - a pause point where the agent needs external input.""" - - type: str - interrupt_value: Any = Field(..., alias="interruptValue") - end_value: Any | None = Field(None, alias="endValue") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) - - -class UiPathConversationInterrupt(UiPathConversationInterruptData): - """Represents an interrupt within a message - a pause point where the agent needs external input.""" - - interrupt_id: str = Field(..., alias="interruptId") - created_at: str = Field(..., alias="createdAt") - updated_at: str = Field(..., alias="updatedAt") - - model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/message.py b/packages/uipath-core/src/uipath/core/chat/message.py index 48e79171f..9d6aa248d 100644 --- a/packages/uipath-core/src/uipath/core/chat/message.py +++ b/packages/uipath-core/src/uipath/core/chat/message.py @@ -10,11 +10,6 @@ UiPathConversationContentPartEvent, ) from .error import UiPathConversationErrorEvent -from .interrupt import ( - UiPathConversationInterrupt, - UiPathConversationInterruptData, - UiPathConversationInterruptEvent, -) from .tool import ( UiPathConversationToolCall, UiPathConversationToolCallData, @@ -53,7 +48,6 @@ class UiPathConversationMessageEvent(BaseModel): None, alias="contentPart" ) tool_call: UiPathConversationToolCallEvent | None = Field(None, alias="toolCall") - interrupt: UiPathConversationInterruptEvent | None = None meta_event: dict[str, Any] | None = Field(None, alias="metaEvent") error: UiPathConversationErrorEvent | None = Field(None, alias="messageError") @@ -68,7 +62,6 @@ class UiPathConversationMessageData(BaseModel): ..., alias="contentParts" ) tool_calls: Sequence[UiPathConversationToolCallData] = Field(..., alias="toolCalls") - interrupts: Sequence[UiPathConversationInterruptData] model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -86,6 +79,5 @@ class UiPathConversationMessage(UiPathConversationMessageData): ..., alias="contentParts" ) tool_calls: Sequence[UiPathConversationToolCall] = Field(..., alias="toolCalls") - interrupts: Sequence[UiPathConversationInterrupt] model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/tool.py b/packages/uipath-core/src/uipath/core/chat/tool.py index 9c9e911bd..8af5fb604 100644 --- a/packages/uipath-core/src/uipath/core/chat/tool.py +++ b/packages/uipath-core/src/uipath/core/chat/tool.py @@ -25,6 +25,8 @@ class UiPathConversationToolCallStartEvent(BaseModel): timestamp: str | None = None input: dict[str, Any] | None = None metadata: dict[str, Any] | None = Field(None, alias="metaData") + require_confirmation: bool | None = Field(None, alias="requireConfirmation") + input_schema: Any | None = Field(None, alias="inputSchema") model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -41,6 +43,34 @@ class UiPathConversationToolCallEndEvent(BaseModel): model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) +class UiPathConversationToolCallConfirmationEvent(BaseModel): + """Signals a tool call confirmation (approve/reject) from the client.""" + + approved: bool + input: Any | None = None + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationToolCallConfirmationData(BaseModel): + """Represents the core data of a tool call confirmation.""" + + approved: bool + input: Any | None = None + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationToolCallConfirmation( + UiPathConversationToolCallConfirmationData +): + """Represents the stored confirmation state on a tool call.""" + + confirmed_at: str | None = Field(None, alias="confirmedAt") + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + class UiPathConversationToolCallEvent(BaseModel): """Encapsulates the data related to a tool call event.""" @@ -49,6 +79,9 @@ class UiPathConversationToolCallEvent(BaseModel): None, alias="startToolCall" ) end: UiPathConversationToolCallEndEvent | None = Field(None, alias="endToolCall") + confirm: UiPathConversationToolCallConfirmationEvent | None = Field( + None, alias="confirmToolCall" + ) meta_event: dict[str, Any] | None = Field(None, alias="metaEvent") error: UiPathConversationErrorEvent | None = Field(None, alias="toolCallError") @@ -61,6 +94,9 @@ class UiPathConversationToolCallData(BaseModel): name: str input: dict[str, Any] | None = None result: UiPathConversationToolCallResult | None = None + require_confirmation: bool | None = Field(None, alias="requireConfirmation") + input_schema: Any | None = Field(None, alias="inputSchema") + confirmation: UiPathConversationToolCallConfirmationData | None = None model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -72,5 +108,6 @@ class UiPathConversationToolCall(UiPathConversationToolCallData): timestamp: str | None = None created_at: str = Field(..., alias="createdAt") updated_at: str = Field(..., alias="updatedAt") + confirmation: UiPathConversationToolCallConfirmation | None = None model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 807653ed8..a096d011c 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.12" +version = "0.5.13" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/tests/services/test_conversations_service.py b/packages/uipath-platform/tests/services/test_conversations_service.py index 31aa4a653..37e08bdfa 100644 --- a/packages/uipath-platform/tests/services/test_conversations_service.py +++ b/packages/uipath-platform/tests/services/test_conversations_service.py @@ -38,7 +38,6 @@ async def test_retrieve_message( "role": "assistant", "contentParts": [], "toolCalls": [], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, @@ -95,7 +94,6 @@ async def test_retrieve_message_with_content_parts( } ], "toolCalls": [], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, @@ -145,7 +143,6 @@ async def test_retrieve_message_with_tool_calls( "updatedAt": "2024-01-01T00:00:00Z", } ], - "interrupts": [], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 6821bbc1c..6e1fc96fd 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.12" +version = "0.5.13" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 53d1a891f..d69b420a5 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.56" +version = "2.10.57" 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/_chat/_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_bridge.py index 24c1be024..2a382a59e 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_bridge.py @@ -4,7 +4,6 @@ import json import logging import os -import uuid from typing import Any from urllib.parse import urlparse @@ -14,11 +13,8 @@ UiPathConversationEvent, UiPathConversationExchangeEndEvent, UiPathConversationExchangeEvent, - UiPathConversationInterruptEndEvent, - UiPathConversationInterruptEvent, UiPathConversationMessageEvent, - UiPathConversationToolCallConfirmationInterruptStartEvent, - UiPathConversationToolCallConfirmationValue, + UiPathConversationToolCallConfirmationEvent, ) from uipath.core.triggers import UiPathResumeTrigger from uipath.runtime.chat import UiPathChatProtocol @@ -126,9 +122,10 @@ def __init__( self._client: Any | None = None self._connected_event = asyncio.Event() - # Interrupt state for HITL round-trip - self._interrupt_end_event = asyncio.Event() - self._interrupt_end_value: UiPathConversationInterruptEndEvent | None = None + self._tool_confirmation_event = asyncio.Event() + self._tool_confirmation_value: ( + UiPathConversationToolCallConfirmationEvent | None + ) = None self._current_message_id: str | None = None # Set CAS_WEBSOCKET_DISABLED when using the debugger to prevent websocket errors from @@ -363,67 +360,35 @@ async def emit_exchange_error_event(self, error: Exception) -> None: raise RuntimeError(f"Failed to send exchange error event: {e}") from e async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger): - if self._client and self._connected_event.is_set(): - try: - # Clear previous interrupt state and generate new interrupt_id - self._interrupt_id = str(uuid.uuid4()) - - # Ensure we have a valid message_id - if self._current_message_id is None: - raise RuntimeError( - "Cannot emit interrupt event: no current message_id set" - ) - - # Ensure api_resume is not None - if resume_trigger.api_resume is None: - raise RuntimeError( - "Cannot emit interrupt event: api_resume is None" - ) - - interrupt_event = UiPathConversationEvent( - conversation_id=self.conversation_id, - exchange=UiPathConversationExchangeEvent( - exchange_id=self.exchange_id, - message=UiPathConversationMessageEvent( - message_id=self._current_message_id, - interrupt=UiPathConversationInterruptEvent( - interrupt_id=self._interrupt_id, - start=UiPathConversationToolCallConfirmationInterruptStartEvent( - type="uipath_cas_tool_call_confirmation", - value=UiPathConversationToolCallConfirmationValue( - **resume_trigger.api_resume.request - ), - ), - ), - ), - ), - ) - - event_data = interrupt_event.model_dump( - mode="json", exclude_none=True, by_alias=True - ) - if self._websocket_disabled: - logger.info( - f"SocketIOChatBridge is in debug mode. Not sending event: {json.dumps(event_data)}" - ) - else: - await self._client.emit("ConversationEvent", event_data) - except Exception as e: - logger.warning(f"Error sending interrupt event: {e}") + """No-op. + + Tool confirmation — the only interrupt pattern CAS uses today — is + handled end-to-end via ``startToolCall`` with ``requireConfirmation: + true`` paired with ``wait_for_resume()``. This is deliberately + simpler than the old interrupt-based flow: CAS needs + ``requireConfirmation`` on the tool call event itself to render the + confirmation UI, so a parallel ``startInterrupt`` event would be + redundant. + + The only hypothetical reason to put work here is a generic, + non-tool-call agent interrupt (e.g. a coded agent calling + ``interrupt("do you want to continue?")``). Nothing uses that today + and it's not a near-term requirement — the method is kept for + generic flexibility. + """ + return None async def wait_for_resume(self) -> dict[str, Any]: - """Wait for the interrupt_end event to be received. - - Returns: - Resume data from the interrupt end event - """ - self._interrupt_end_event.clear() - self._interrupt_end_value = None + """Wait for a confirmToolCall event to be received.""" + self._tool_confirmation_event.clear() + self._tool_confirmation_value = None - await self._interrupt_end_event.wait() + await self._tool_confirmation_event.wait() - if self._interrupt_end_value: - return self._interrupt_end_value.model_dump(mode="python", by_alias=False) + if self._tool_confirmation_value: + return self._tool_confirmation_value.model_dump( + mode="python", by_alias=False + ) return {} @property @@ -458,17 +423,14 @@ async def _handle_conversation_event( if ( parsed_event.exchange and parsed_event.exchange.message - and parsed_event.exchange.message.interrupt - and parsed_event.exchange.message.interrupt.end + and (tool_call := parsed_event.exchange.message.tool_call) + and (confirm := tool_call.confirm) ): - interrupt = parsed_event.exchange.message.interrupt - - if interrupt.interrupt_id == self._interrupt_id: - logger.info( - f"Received endInterrupt for interrupt_id: {self._interrupt_id}" - ) - self._interrupt_end_value = interrupt.end - self._interrupt_end_event.set() + logger.info( + f"Received confirmToolCall for tool_call_id: {tool_call.tool_call_id}, approved: {confirm.approved}" + ) + self._tool_confirmation_value = confirm + self._tool_confirmation_event.set() except Exception as e: logger.warning(f"Error parsing conversation event: {e}") diff --git a/packages/uipath/src/uipath/eval/models/_conversational_utils.py b/packages/uipath/src/uipath/eval/models/_conversational_utils.py index d4dbf0cdd..9e3523acc 100644 --- a/packages/uipath/src/uipath/eval/models/_conversational_utils.py +++ b/packages/uipath/src/uipath/eval/models/_conversational_utils.py @@ -168,7 +168,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="user", content_parts=content_parts, tool_calls=[], - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -215,7 +214,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="assistant", content_parts=content_parts, tool_calls=tool_calls, - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -259,7 +257,6 @@ def legacy_conversational_eval_input_to_uipath_message_list( role="user", content_parts=content_parts, tool_calls=[], - interrupts=[], created_at=timestamp, updated_at=timestamp, ) @@ -301,7 +298,6 @@ def legacy_conversational_eval_output_to_uipath_message_data_list( role="assistant", content_parts=content_parts, tool_calls=tool_calls, - interrupts=[], ) ) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index bb59734af..a0201a489 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.56" +version = "2.10.57" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.12" +version = "0.5.13" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 4b6fcbccdf264c18d74f81bcfaa656c54f341433 Mon Sep 17 00:00:00 2001 From: Radu Mihai Gheorghe Date: Tue, 28 Apr 2026 11:10:19 +0300 Subject: [PATCH 037/121] chore: update pyproject template version to current minor interval (#1592) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/cli_new.py | 2 +- packages/uipath/uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index d69b420a5..b81b3aa57 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.57" +version = "2.10.58" 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 390c581c5..d273055d5 100644 --- a/packages/uipath/src/uipath/_cli/cli_new.py +++ b/packages/uipath/src/uipath/_cli/cli_new.py @@ -28,7 +28,7 @@ def generate_pyproject(target_directory, project_name): description = "{project_name}" authors = [{{ name = "John Doe", email = "john.doe@myemail.com" }}] dependencies = [ - "uipath>=2.2.0, <2.3.0" + "uipath>=2.10.0, <2.11.0" ] requires-python = ">=3.11" """ diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index a0201a489..32ef182ac 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.57" +version = "2.10.58" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 97fb1a4f93b63e623bdc663dcbf6039abe7e3961 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 28 Apr 2026 12:45:46 +0200 Subject: [PATCH 038/121] chore: add agentservice source on job invocation (#1593) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/orchestrator/_processes_service.py | 6 +++++- .../tests/services/test_processes_service.py | 4 ++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index e32ca5495..59585688f 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.38" +version = "0.1.39" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py b/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py index 73b4122e1..e6ecc988a 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py @@ -321,7 +321,11 @@ def _invoke_spec( parent_operation_id: Optional[str] = None, run_as_me: Optional[bool] = None, ) -> RequestSpec: - payload: Dict[str, Any] = {"ReleaseName": name, **(input_data or {})} + payload: Dict[str, Any] = { + "ReleaseName": name, + **(input_data or {}), + "Source": "AgentService", + } self._add_tracing(payload, UiPathConfig.trace_id, parent_span_id) if parent_operation_id: diff --git a/packages/uipath-platform/tests/services/test_processes_service.py b/packages/uipath-platform/tests/services/test_processes_service.py index 15b2c9c7e..015888c54 100644 --- a/packages/uipath-platform/tests/services/test_processes_service.py +++ b/packages/uipath-platform/tests/services/test_processes_service.py @@ -79,6 +79,7 @@ def test_invoke( "startInfo": { "ReleaseName": process_name, "InputArguments": json.dumps(input_arguments), + "Source": "AgentService", } }, separators=(",", ":"), @@ -139,6 +140,7 @@ def test_invoke_without_input_arguments( "startInfo": { "ReleaseName": process_name, "InputArguments": "{}", + "Source": "AgentService", } }, separators=(",", ":"), @@ -300,6 +302,7 @@ async def test_invoke_async( "startInfo": { "ReleaseName": process_name, "InputArguments": json.dumps(input_arguments), + "Source": "AgentService", } }, separators=(",", ":"), @@ -361,6 +364,7 @@ async def test_invoke_async_without_input_arguments( "startInfo": { "ReleaseName": process_name, "InputArguments": "{}", + "Source": "AgentService", } }, separators=(",", ":"), diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 6e1fc96fd..4431c9b1c 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.38" +version = "0.1.39" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index b81b3aa57..c6a998262 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.13, <0.2.0", + "uipath-platform>=0.1.39, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 32ef182ac..87c9a3aa2 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.38" +version = "0.1.39" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 82f8e54edb118c4eddefb05c348fad3eab8528f9 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Wed, 29 Apr 2026 10:02:04 +0200 Subject: [PATCH 039/121] fix: restore interrupt event compat shims in uipath-core (#1599) (#1600) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/chat/__init__.py | 27 ++++ .../src/uipath/core/chat/interrupt.py | 122 ++++++++++++++++++ packages/uipath-core/uv.lock | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 6 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 packages/uipath-core/src/uipath/core/chat/interrupt.py diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 2f38df428..7e559c9a4 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.13" +version = "0.5.14" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/chat/__init__.py b/packages/uipath-core/src/uipath/core/chat/__init__.py index d81fa8153..77fa6cbee 100644 --- a/packages/uipath-core/src/uipath/core/chat/__init__.py +++ b/packages/uipath-core/src/uipath/core/chat/__init__.py @@ -77,6 +77,20 @@ UiPathConversationExchangeEvent, UiPathConversationExchangeStartEvent, ) +from .interrupt import ( + InterruptTypeEnum, + UiPathConversationGenericInterruptEndEvent, + UiPathConversationGenericInterruptStartEvent, + UiPathConversationInterrupt, + UiPathConversationInterruptData, + UiPathConversationInterruptEndEvent, + UiPathConversationInterruptEvent, + UiPathConversationInterruptStartEvent, + UiPathConversationToolCallConfirmationEndValue, + UiPathConversationToolCallConfirmationInterruptEndEvent, + UiPathConversationToolCallConfirmationInterruptStartEvent, + UiPathConversationToolCallConfirmationValue, +) from .message import ( UiPathConversationMessage, UiPathConversationMessageData, @@ -177,4 +191,17 @@ "UiPathVoiceToolCallRequest", "UiPathVoiceToolCallMessage", "UiPathVoiceToolCallResult", + # Interrupt (compat shims — deprecated, see interrupt.py) + "InterruptTypeEnum", + "UiPathConversationInterruptStartEvent", + "UiPathConversationInterruptEndEvent", + "UiPathConversationInterruptEvent", + "UiPathConversationInterruptData", + "UiPathConversationInterrupt", + "UiPathConversationGenericInterruptStartEvent", + "UiPathConversationGenericInterruptEndEvent", + "UiPathConversationToolCallConfirmationValue", + "UiPathConversationToolCallConfirmationEndValue", + "UiPathConversationToolCallConfirmationInterruptStartEvent", + "UiPathConversationToolCallConfirmationInterruptEndEvent", ] diff --git a/packages/uipath-core/src/uipath/core/chat/interrupt.py b/packages/uipath-core/src/uipath/core/chat/interrupt.py new file mode 100644 index 000000000..9094d1597 --- /dev/null +++ b/packages/uipath-core/src/uipath/core/chat/interrupt.py @@ -0,0 +1,122 @@ +"""Compatibility shims for legacy interrupt event types. + +The interrupt-based tool-call confirmation flow was replaced by `confirmToolCall` +on the tool call event itself (see PR #1558). The original `interrupt.py` was +removed in `uipath-core` 0.5.13, but published `uipath-runtime` versions still +import these names at module load time, breaking installs that pull the new +`uipath-core` alongside an older runtime. + +These shims keep those imports working. They are not used by current code paths +and should be removed in the next minor bump of `uipath-core`. +""" + +from enum import Enum +from typing import Any, Literal, Union + +from pydantic import BaseModel, ConfigDict, Field + + +class InterruptTypeEnum(str, Enum): + """Enum of known interrupt types.""" + + TOOL_CALL_CONFIRMATION = "uipath_cas_tool_call_confirmation" + + +class UiPathConversationToolCallConfirmationValue(BaseModel): + """Schema for tool call confirmation interrupt value.""" + + tool_call_id: str = Field(..., alias="toolCallId") + tool_name: str = Field(..., alias="toolName") + input_schema: Any = Field(..., alias="inputSchema") + input_value: Any | None = Field(None, alias="inputValue") + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationToolCallConfirmationInterruptStartEvent(BaseModel): + """Tool call confirmation interrupt start event with strong typing.""" + + type: Literal["uipath_cas_tool_call_confirmation"] + value: UiPathConversationToolCallConfirmationValue + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationGenericInterruptStartEvent(BaseModel): + """Generic interrupt start event for custom interrupt types.""" + + type: str + value: Any + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +UiPathConversationInterruptStartEvent = Union[ + UiPathConversationToolCallConfirmationInterruptStartEvent, + UiPathConversationGenericInterruptStartEvent, +] + + +class UiPathConversationToolCallConfirmationEndValue(BaseModel): + """Schema for tool call confirmation end value.""" + + approved: bool + input: Any | None = None + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationToolCallConfirmationInterruptEndEvent(BaseModel): + """Tool call confirmation interrupt end event with strong typing.""" + + type: Literal["uipath_cas_tool_call_confirmation"] + value: UiPathConversationToolCallConfirmationEndValue + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationGenericInterruptEndEvent(BaseModel): + """Generic interrupt end event for custom interrupt types.""" + + type: str + value: Any + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +UiPathConversationInterruptEndEvent = Union[ + UiPathConversationToolCallConfirmationInterruptEndEvent, + UiPathConversationGenericInterruptEndEvent, +] + + +class UiPathConversationInterruptEvent(BaseModel): + """Encapsulates interrupt-related events within a message.""" + + interrupt_id: str = Field(..., alias="interruptId") + start: UiPathConversationInterruptStartEvent | None = Field( + None, alias="startInterrupt" + ) + end: UiPathConversationInterruptEndEvent | None = Field(None, alias="endInterrupt") + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationInterruptData(BaseModel): + """Core data of an interrupt within a message.""" + + type: str + interrupt_value: Any = Field(..., alias="interruptValue") + end_value: Any | None = Field(None, alias="endValue") + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class UiPathConversationInterrupt(UiPathConversationInterruptData): + """An interrupt within a message — a pause point where the agent needs external input.""" + + interrupt_id: str = Field(..., alias="interruptId") + created_at: str = Field(..., alias="createdAt") + updated_at: str = Field(..., alias="updatedAt") + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index a096d011c..61159a1d5 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.13" +version = "0.5.14" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 4431c9b1c..415634498 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.13" +version = "0.5.14" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 87c9a3aa2..b85d0fb8d 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.13" +version = "0.5.14" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From ae5dac0748ce870f73387b3041ad087ea529545d Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Wed, 29 Apr 2026 12:29:10 +0200 Subject: [PATCH 040/121] ci: cross-test all integrations against locally-built uipath wheels (#1602) --- .github/labeler.yml | 6 +- .../workflows/test-uipath-integrations.yml | 284 ++++++++++++++++++ .github/workflows/test-uipath-langchain.yml | 48 +++ .github/workflows/test-uipath-llamaindex.yml | 173 ----------- 4 files changed, 337 insertions(+), 174 deletions(-) create mode 100644 .github/workflows/test-uipath-integrations.yml delete mode 100644 .github/workflows/test-uipath-llamaindex.yml diff --git a/.github/labeler.yml b/.github/labeler.yml index ee2f4de14..d83420502 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -6,9 +6,13 @@ test:uipath-langchain: - changed-files: - any-glob-to-any-file: ['packages/uipath-core/src/**/*.py'] -test:uipath-llamaindex: +test:uipath-integrations: - changed-files: - any-glob-to-any-file: ['packages/uipath/src/**/*.py'] + - changed-files: + - any-glob-to-any-file: ['packages/uipath-platform/src/**/*.py'] + - changed-files: + - any-glob-to-any-file: ['packages/uipath-core/src/**/*.py'] test:uipath-runtime: - changed-files: diff --git a/.github/workflows/test-uipath-integrations.yml b/.github/workflows/test-uipath-integrations.yml new file mode 100644 index 000000000..ed50a4b3d --- /dev/null +++ b/.github/workflows/test-uipath-integrations.yml @@ -0,0 +1,284 @@ +name: uipath - Test Integrations + +on: + pull_request: + types: [ opened, synchronize, reopened, labeled ] + +jobs: + build-wheels: + runs-on: ubuntu-latest + permissions: + contents: read + if: contains(github.event.pull_request.labels.*.name, 'test:uipath-integrations') + steps: + - name: Checkout uipath-python + uses: actions/checkout@v4 + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Build uipath-core package + working-directory: packages/uipath-core + run: uv build + + - name: Build uipath-platform package + working-directory: packages/uipath-platform + run: uv build + + - name: Build uipath package + working-directory: packages/uipath + run: uv build + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: uipath-wheels + path: packages/*/dist/*.whl + + discover-packages: + needs: [build-wheels] + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + packages: ${{ steps.discover.outputs.packages }} + steps: + - name: Checkout uipath-integrations-python + uses: actions/checkout@v4 + with: + repository: 'UiPath/uipath-integrations-python' + path: 'uipath-integrations-python' + + - name: Discover packages + id: discover + working-directory: uipath-integrations-python + run: | + # Find every package directory under packages/ that has a pyproject.toml + package_dirs=$(find packages -maxdepth 2 -name pyproject.toml -printf '%h\n' | sed 's|^packages/||' | sort) + + echo "Found integration packages:" + echo "$package_dirs" + + packages_json=$(echo "$package_dirs" | jq -R -s -c 'split("\n")[:-1]') + echo "packages=$packages_json" >> $GITHUB_OUTPUT + + test-package: + needs: [build-wheels, discover-packages] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + package: ${{ fromJson(needs.discover-packages.outputs.packages) }} + python-version: [ "3.11", "3.12", "3.13" ] + os: [ ubuntu-latest, windows-latest ] + + name: "${{ matrix.package }} / py${{ matrix.python-version }} / ${{ matrix.os }}" + permissions: + contents: read + + steps: + - name: Setup uv + uses: astral-sh/setup-uv@v5 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Download wheels + uses: actions/download-artifact@v4 + with: + name: uipath-wheels + path: wheels + + - name: Checkout uipath-integrations-python + uses: actions/checkout@v4 + with: + repository: 'UiPath/uipath-integrations-python' + path: 'uipath-integrations-python' + + - name: Update uipath packages + shell: bash + working-directory: uipath-integrations-python/packages/${{ matrix.package }} + run: | + uv add ../../../wheels/uipath-core/dist/*.whl --dev + uv add ../../../wheels/uipath-platform/dist/*.whl --dev + uv add ../../../wheels/uipath/dist/*.whl --dev + + - name: Install dependencies and run tests + shell: bash + working-directory: uipath-integrations-python/packages/${{ matrix.package }} + run: | + uv sync + if [ -d tests ]; then + uv run pytest + else + echo "No tests directory found in ${{ matrix.package }}, skipping pytest" + fi + + discover-testcases: + needs: [test-package, discover-packages] + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + matrix: ${{ steps.discover.outputs.matrix }} + has_testcases: ${{ steps.discover.outputs.has_testcases }} + steps: + - name: Checkout uipath-integrations-python + uses: actions/checkout@v4 + with: + repository: 'UiPath/uipath-integrations-python' + path: 'uipath-integrations-python' + + - name: Discover testcases across packages + id: discover + working-directory: uipath-integrations-python + run: | + # For each package with a testcases/ directory, list its testcase folders + # and emit one matrix entry per (package, testcase) pair. + entries="[]" + for pkg_dir in packages/*/; do + pkg=$(basename "$pkg_dir") + tc_dir="$pkg_dir/testcases" + if [ ! -d "$tc_dir" ]; then + continue + fi + testcases=$(find "$tc_dir" -maxdepth 1 -type d -name "*-*" -printf '%f\n' | sort) + if [ -z "$testcases" ]; then + continue + fi + for tc in $testcases; do + entries=$(echo "$entries" | jq --arg p "$pkg" --arg t "$tc" '. + [{package: $p, testcase: $t}]') + done + done + + echo "Discovered testcase matrix:" + echo "$entries" | jq . + + count=$(echo "$entries" | jq 'length') + if [ "$count" -eq 0 ]; then + echo "has_testcases=false" >> $GITHUB_OUTPUT + echo "matrix=[]" >> $GITHUB_OUTPUT + else + echo "has_testcases=true" >> $GITHUB_OUTPUT + echo "matrix=$(echo "$entries" | jq -c .)" >> $GITHUB_OUTPUT + fi + + run-integration-tests: + needs: [build-wheels, discover-testcases] + if: needs.discover-testcases.outputs.has_testcases == 'true' + runs-on: ubuntu-latest + container: + image: ghcr.io/astral-sh/uv:python3.12-bookworm + env: + UIPATH_JOB_KEY: "3a03d5cb-fa21-4021-894d-a8e2eda0afe0" + UIPATH_TRACING_ENABLED: false + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.discover-testcases.outputs.matrix) }} + environment: [alpha, staging] # temporary disable [cloud] + + name: "${{ matrix.package }} / ${{ matrix.testcase }} / ${{ matrix.environment }}" + + steps: + - name: Download wheels + uses: actions/download-artifact@v4 + with: + name: uipath-wheels + path: wheels + + - name: Checkout uipath-integrations-python + uses: actions/checkout@v4 + with: + repository: 'UiPath/uipath-integrations-python' + path: 'uipath-integrations-python' + + - name: Update uipath packages + shell: bash + working-directory: uipath-integrations-python/packages/${{ matrix.package }} + run: | + uv add ../../../wheels/uipath-core/dist/*.whl + uv add ../../../wheels/uipath-platform/dist/*.whl + uv add ../../../wheels/uipath/dist/*.whl + + - name: Install dependencies + working-directory: uipath-integrations-python/packages/${{ matrix.package }} + run: uv sync + + - name: Run testcase + env: + CLIENT_ID: ${{ matrix.environment == 'alpha' && secrets.ALPHA_TEST_CLIENT_ID || matrix.environment == 'staging' && secrets.STAGING_TEST_CLIENT_ID || matrix.environment == 'cloud' && secrets.CLOUD_TEST_CLIENT_ID }} + CLIENT_SECRET: ${{ matrix.environment == 'alpha' && secrets.ALPHA_TEST_CLIENT_SECRET || matrix.environment == 'staging' && secrets.STAGING_TEST_CLIENT_SECRET || matrix.environment == 'cloud' && secrets.CLOUD_TEST_CLIENT_SECRET }} + BASE_URL: ${{ matrix.environment == 'alpha' && secrets.ALPHA_BASE_URL || matrix.environment == 'staging' && secrets.STAGING_BASE_URL || matrix.environment == 'cloud' && secrets.CLOUD_BASE_URL }} + UV_PYTHON: "3.12" + working-directory: uipath-integrations-python/packages/${{ matrix.package }}/testcases/${{ matrix.testcase }} + run: | + echo "Package: ${{ matrix.package }}" + echo "Testcase: ${{ matrix.testcase }}" + echo "Environment: ${{ matrix.environment }}" + + bash run.sh + bash ../common/validate_output.sh + + notify-on-failure: + needs: [test-package, run-integration-tests] + if: always() && contains(github.event.pull_request.labels.*.name, 'test:uipath-integrations') && (needs.test-package.result == 'failure' || needs.run-integration-tests.result == 'failure') + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = [ + marker, + '## :rotating_light: **Heads up: `uipath-integrations` cross-tests are FAILING** :rotating_light:', + '', + 'Your changes may break one or more integrations in **[`uipath-integrations-python`](https://github.com/UiPath/uipath-integrations-python)**:', + '', + '- `uipath-openai-agents`', + '- `uipath-google-adk`', + '- `uipath-agent-framework`', + '- `uipath-llamaindex`', + '- `uipath-pydantic-ai`', + '', + '> :warning: **These checks are NOT enforced by branch protection rules.** Please review the failures before merging.', + '', + `**:mag: [Inspect the failed run →](${runUrl})**`, + ].join('\n'); + + // Delete any prior failure comments for this workflow so the new + // one always lands at the bottom of the PR conversation. + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + for (const c of comments) { + if (c.body && c.body.includes(marker)) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: c.id, + }); + } + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); diff --git a/.github/workflows/test-uipath-langchain.yml b/.github/workflows/test-uipath-langchain.yml index f74135975..f7ccb2cf9 100644 --- a/.github/workflows/test-uipath-langchain.yml +++ b/.github/workflows/test-uipath-langchain.yml @@ -175,3 +175,51 @@ jobs: # Execute the testcase run script directly bash run.sh bash ../common/validate_output.sh + + notify-on-failure: + needs: [test-uipath-langchain, run-uipath-langchain-integration-tests] + if: always() && contains(github.event.pull_request.labels.*.name, 'test:uipath-langchain') && (needs.test-uipath-langchain.result == 'failure' || needs.run-uipath-langchain-integration-tests.result == 'failure') + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = [ + marker, + '## :rotating_light: **Heads up: `uipath-langchain` cross-tests are FAILING** :rotating_light:', + '', + 'Your changes may break the **[`uipath-langchain-python`](https://github.com/UiPath/uipath-langchain-python)** integration.', + '', + '> :warning: **These checks are NOT enforced by branch protection rules.** Please review the failures before merging.', + '', + `**:mag: [Inspect the failed run →](${runUrl})**`, + ].join('\n'); + + // Delete any prior failure comments for this workflow so the new + // one always lands at the bottom of the PR conversation. + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + for (const c of comments) { + if (c.body && c.body.includes(marker)) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: c.id, + }); + } + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); diff --git a/.github/workflows/test-uipath-llamaindex.yml b/.github/workflows/test-uipath-llamaindex.yml deleted file mode 100644 index fcf8d0fb4..000000000 --- a/.github/workflows/test-uipath-llamaindex.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: uipath - Test LlamaIndex - -on: - pull_request: - types: [ opened, synchronize, reopened, labeled ] - -jobs: - build-wheels: - runs-on: ubuntu-latest - permissions: - contents: read - if: contains(github.event.pull_request.labels.*.name, 'test:uipath-llamaindex') - steps: - - name: Checkout uipath-python - uses: actions/checkout@v4 - - - name: Setup uv - uses: astral-sh/setup-uv@v5 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Build uipath-core package - working-directory: packages/uipath-core - run: uv build - - - name: Build uipath-platform package - working-directory: packages/uipath-platform - run: uv build - - - name: Build uipath package - working-directory: packages/uipath - run: uv build - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: uipath-wheels - path: packages/*/dist/*.whl - - test-uipath-llamaindex: - needs: [build-wheels] - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: [ "3.11", "3.12", "3.13" ] - os: [ ubuntu-latest, windows-latest ] - - permissions: - contents: read - - steps: - - name: Setup uv - uses: astral-sh/setup-uv@v5 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Download wheels - uses: actions/download-artifact@v4 - with: - name: uipath-wheels - path: wheels - - - name: Checkout uipath-integrations-python - uses: actions/checkout@v4 - with: - repository: 'UiPath/uipath-integrations-python' - path: 'uipath-integrations-python' - - - name: Update uipath packages - shell: bash - working-directory: uipath-integrations-python/packages/uipath-llamaindex - run: | - uv add ../../../wheels/uipath-core/dist/*.whl --dev - uv add ../../../wheels/uipath-platform/dist/*.whl --dev - uv add ../../../wheels/uipath/dist/*.whl --dev - - - name: Run uipath-llamaindex tests - working-directory: uipath-integrations-python/packages/uipath-llamaindex - run: | - uv sync - uv run pytest - - discover-testcases: - runs-on: ubuntu-latest - permissions: - contents: read - needs: [test-uipath-llamaindex] - outputs: - testcases: ${{ steps.discover.outputs.testcases }} - steps: - - name: Checkout uipath-integrations-python - uses: actions/checkout@v4 - with: - repository: 'UiPath/uipath-integrations-python' - path: 'uipath-integrations-python' - - - name: Discover testcases - id: discover - working-directory: uipath-integrations-python/packages/uipath-llamaindex - run: | - # Find all testcase folders (excluding common folders like README, etc.) - testcase_dirs=$(find testcases -maxdepth 1 -type d -name "*-*" | sed 's|testcases/||' | sort) - - echo "Found testcase directories:" - echo "$testcase_dirs" - - # Convert to JSON array for matrix - testcases_json=$(echo "$testcase_dirs" | jq -R -s -c 'split("\n")[:-1]') - echo "testcases=$testcases_json" >> $GITHUB_OUTPUT - - run-uipath-llamaindex-integration-tests: - runs-on: ubuntu-latest - needs: [build-wheels, discover-testcases] - container: - image: ghcr.io/astral-sh/uv:python3.12-bookworm - env: - UIPATH_JOB_KEY: "3a03d5cb-fa21-4021-894d-a8e2eda0afe0" - UIPATH_TRACING_ENABLED: false - permissions: - contents: read - strategy: - fail-fast: false - matrix: - testcase: ${{ fromJson(needs.discover-testcases.outputs.testcases) }} - environment: [alpha, staging] # temporary disable [cloud] - - name: "${{ matrix.testcase }} / ${{ matrix.environment }}" - - steps: - - name: Download wheels - uses: actions/download-artifact@v4 - with: - name: uipath-wheels - path: wheels - - - name: Checkout uipath-integrations-python - uses: actions/checkout@v4 - with: - repository: 'UiPath/uipath-integrations-python' - path: 'uipath-integrations-python' - - - name: Update uipath packages - shell: bash - working-directory: uipath-integrations-python/packages/uipath-llamaindex - run: | - uv add ../../../wheels/uipath-core/dist/*.whl - uv add ../../../wheels/uipath-platform/dist/*.whl - uv add ../../../wheels/uipath/dist/*.whl - - - name: Install dependencies - working-directory: uipath-integrations-python/packages/uipath-llamaindex - run: uv sync - - - name: Run testcase - env: - CLIENT_ID: ${{ matrix.environment == 'alpha' && secrets.ALPHA_TEST_CLIENT_ID || matrix.environment == 'staging' && secrets.STAGING_TEST_CLIENT_ID || matrix.environment == 'cloud' && secrets.CLOUD_TEST_CLIENT_ID }} - CLIENT_SECRET: ${{ matrix.environment == 'alpha' && secrets.ALPHA_TEST_CLIENT_SECRET || matrix.environment == 'staging' && secrets.STAGING_TEST_CLIENT_SECRET || matrix.environment == 'cloud' && secrets.CLOUD_TEST_CLIENT_SECRET }} - BASE_URL: ${{ matrix.environment == 'alpha' && secrets.ALPHA_BASE_URL || matrix.environment == 'staging' && secrets.STAGING_BASE_URL || matrix.environment == 'cloud' && secrets.CLOUD_BASE_URL }} - UV_PYTHON: "3.12" - working-directory: uipath-integrations-python/packages/uipath-llamaindex/testcases/${{ matrix.testcase }} - run: | - echo "Running testcase: ${{ matrix.testcase }}" - echo "Environment: ${{ matrix.environment }}" - - # Execute the testcase run script directly - bash run.sh - bash ../common/validate_output.sh From 79756aa5aacd4aa45e35505ec73631ebc60368b5 Mon Sep 17 00:00:00 2001 From: dianagrecu-uipath Date: Wed, 29 Apr 2026 15:21:37 +0300 Subject: [PATCH 041/121] feat: add is_case_manager field in agent declaration (#1594) Add is_case_manager field in agent declaration --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 6 + .../uipath/tests/agent/models/test_agent.py | 113 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 121 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index c6a998262..3883b3d84 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.58" +version = "2.10.59" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 7694a861e..8f8b8a207 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -1156,6 +1156,7 @@ class AgentMetadata(BaseCfg): """Agent metadata model.""" is_conversational: bool = Field(alias="isConversational") + is_case_manager: bool = Field(default=False, alias="isCaseManager") storage_version: str = Field(alias="storageVersion") @@ -1216,6 +1217,11 @@ def is_conversational(self) -> bool: return metadata.is_conversational return False + @property + def is_case_manager(self) -> bool: + """Checks if the agent is a case manager agent.""" + return self.metadata.is_case_manager if self.metadata else False + @staticmethod def _normalize_guardrails(v: Dict[str, Any]) -> None: guards = v.get("guardrails") diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index d00e5a42b..6c4c0519a 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3283,6 +3283,119 @@ def test_is_conversational_false_by_default(self): assert config.is_conversational is False +class TestAgentDefinitionIsCaseManager: + """Tests for AgentDefinition.is_case_manager property.""" + + def test_is_case_manager_true_when_metadata_set(self): + """Returns True when metadata.is_case_manager is True.""" + json_data = { + "id": "test-case-manager", + "name": "Case Manager Agent", + "version": "1.0.0", + "metadata": { + "isConversational": False, + "isCaseManager": True, + "storageVersion": "1.0.0", + }, + "settings": { + "model": "gpt-4o", + "maxTokens": 4096, + "temperature": 0, + "engine": "basic-v1", + }, + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "resources": [], + "messages": [ + {"role": "system", "content": "You are a case manager agent."} + ], + } + + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + assert config.is_case_manager is True + + def test_is_case_manager_false_when_metadata_set_false(self): + """Returns False when metadata.is_case_manager is False.""" + json_data = { + "id": "test-non-case-manager", + "name": "Regular Agent", + "version": "1.0.0", + "metadata": { + "isConversational": False, + "isCaseManager": False, + "storageVersion": "1.0.0", + }, + "settings": { + "model": "gpt-4o", + "maxTokens": 4096, + "temperature": 0, + "engine": "basic-v1", + }, + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "resources": [], + "messages": [{"role": "system", "content": "You are an agent."}], + } + + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + assert config.is_case_manager is False + + def test_is_case_manager_false_when_not_in_metadata(self): + """Returns False when isCaseManager is not present in metadata.""" + json_data = { + "id": "test-no-case-manager-field", + "name": "Agent Without CM Field", + "version": "1.0.0", + "metadata": {"isConversational": False, "storageVersion": "1.0.0"}, + "settings": { + "model": "gpt-4o", + "maxTokens": 4096, + "temperature": 0, + "engine": "basic-v1", + }, + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "resources": [], + "messages": [{"role": "system", "content": "You are an agent."}], + } + + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + assert config.is_case_manager is False + + def test_is_case_manager_false_when_no_metadata(self): + """Returns False when agent has no metadata.""" + json_data = { + "id": "test-no-metadata", + "name": "Agent Without Metadata", + "version": "1.0.0", + "settings": { + "model": "gpt-4o", + "maxTokens": 4096, + "temperature": 0, + "engine": "basic-v1", + }, + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "resources": [], + "messages": [{"role": "system", "content": "You are an agent."}], + } + + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + assert config.is_case_manager is False + + class TestAgentBuilderConfigResources: """Tests for AgentDefinition resource configuration parsing.""" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index b85d0fb8d..10b7e683b 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.58" +version = "2.10.59" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From a927b7ea452a0f5d7fd0d298ee9504a0d779af92 Mon Sep 17 00:00:00 2001 From: GabrielVasilescu04 Date: Thu, 30 Apr 2026 10:38:09 +0300 Subject: [PATCH 042/121] feat: add IS resume triggers (#1580) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/triggers/__init__.py | 2 + .../src/uipath/core/triggers/trigger.py | 22 ++ packages/uipath-core/uv.lock | 2 +- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/__init__.py | 2 + .../platform/common/interrupt_models.py | 20 ++ .../platform/orchestrator/_jobs_service.py | 46 +++ .../platform/resume_triggers/_protocol.py | 78 +++++ .../tests/services/test_hitl.py | 269 ++++++++++++++++++ packages/uipath-platform/uv.lock | 4 +- packages/uipath/uv.lock | 4 +- 12 files changed, 446 insertions(+), 7 deletions(-) diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 7e559c9a4..cb188084e 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.14" +version = "0.5.15" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/triggers/__init__.py b/packages/uipath-core/src/uipath/core/triggers/__init__.py index 400462277..fb4ee6ae5 100644 --- a/packages/uipath-core/src/uipath/core/triggers/__init__.py +++ b/packages/uipath-core/src/uipath/core/triggers/__init__.py @@ -4,11 +4,13 @@ "UiPathResumeTrigger", "UiPathResumeTriggerType", "UiPathApiTrigger", + "UiPathIntegrationTrigger", "UiPathResumeTriggerName", ] from uipath.core.triggers.trigger import ( UiPathApiTrigger, + UiPathIntegrationTrigger, UiPathResumeTrigger, UiPathResumeTriggerName, UiPathResumeTriggerType, diff --git a/packages/uipath-core/src/uipath/core/triggers/trigger.py b/packages/uipath-core/src/uipath/core/triggers/trigger.py index 424245079..c897acd28 100644 --- a/packages/uipath-core/src/uipath/core/triggers/trigger.py +++ b/packages/uipath-core/src/uipath/core/triggers/trigger.py @@ -53,6 +53,25 @@ class UiPathApiTrigger(BaseModel): model_config = ConfigDict(validate_by_name=True) +class UiPathIntegrationTrigger(BaseModel): + """Integration Services (Inbox) resume trigger request. + + Mirrors Orchestrator's `IntegrationResumeDto`: the configuration needed to + register a remote event trigger through the Connections service and + correlate the eventual payload back to the suspended job via `inbox_id`. + """ + + connector: str = Field(alias="connector") + connection_id: str = Field(alias="connectionId") + operation: str = Field(alias="operation") + object_name: str = Field(alias="objectName") + filter_expression: str | None = Field(default=None, alias="filterExpression") + parameters: dict[str, str] | None = Field(default=None, alias="parameters") + inbox_id: str = Field(alias="inboxId") + + model_config = ConfigDict(validate_by_name=True) + + class UiPathResumeTrigger(BaseModel): """Information needed to resume execution.""" @@ -65,6 +84,9 @@ class UiPathResumeTrigger(BaseModel): ) item_key: str | None = Field(default=None, alias="itemKey") api_resume: UiPathApiTrigger | None = Field(default=None, alias="apiResume") + integration_resume: UiPathIntegrationTrigger | None = Field( + default=None, alias="integrationResume" + ) folder_path: str | None = Field(default=None, alias="folderPath") folder_key: str | None = Field(default=None, alias="folderKey") payload: Any | None = Field(default=None, alias="interruptObject", exclude=True) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 61159a1d5..bf73c603d 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1007,7 +1007,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.14" +version = "0.5.15" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 59585688f..234dd78b8 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.39" +version = "0.1.40" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index 9070d0d70..cefd92075 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -48,6 +48,7 @@ WaitEphemeralIndex, WaitEphemeralIndexRaw, WaitEscalation, + WaitIntegrationEvent, WaitJob, WaitJobRaw, WaitSystemAgent, @@ -89,6 +90,7 @@ "WaitEphemeralIndexRaw", "DocumentExtractionValidation", "WaitDocumentExtractionValidation", + "WaitIntegrationEvent", "RequestSpec", "Endpoint", "UiPathUrl", diff --git a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py index 100b601bd..3b2468551 100644 --- a/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py +++ b/packages/uipath-platform/src/uipath/platform/common/interrupt_models.py @@ -259,3 +259,23 @@ class WaitDocumentExtractionValidation(BaseModel): extraction_validation: StartExtractionValidationResponse task_url: str | None = None + + +class WaitIntegrationEvent(BaseModel): + """Model representing a wait on an Integration Services event. + + Used to suspend a job until a remote event (e.g. Slack message, Teams reply) + is delivered by Integration Services. The SDK resolves `connection_name` + (scoped to `connection_folder_path` when provided) to the underlying + connection id and generates a fresh `inbox_id` when the trigger is created; + the rest of the fields describe which remote event to subscribe to via + the Connections service. + """ + + connector: str + connection_name: str + connection_folder_path: str | None = None + operation: str + object_name: str + filter_expression: str | None = None + parameters: dict[str, str] | None = None diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/_jobs_service.py b/packages/uipath-platform/src/uipath/platform/orchestrator/_jobs_service.py index f9433d221..a9a132e02 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/_jobs_service.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/_jobs_service.py @@ -674,6 +674,52 @@ def _retrieve_api_payload_spec( }, ) + def retrieve_inbox_payload(self, inbox_id: str) -> Any: + """Fetch payload data for Integration Services (Inbox) triggers. + + Unlike `retrieve_api_payload`, this returns the response body as-is. + Orchestrator's `GET /JobTriggers/GetPayload/{inboxId}` returns the + stored payload directly without an envelope. + + Args: + inbox_id: The Id of the inbox to fetch the payload for. + + Returns: + The stored payload. + """ + spec = self._retrieve_api_payload_spec(inbox_id=inbox_id) + + response = self.request( + spec.method, + url=spec.endpoint, + headers=spec.headers, + ) + + return response.json() + + async def retrieve_inbox_payload_async(self, inbox_id: str) -> Any: + """Asynchronously fetch payload data for Integration Services (Inbox) triggers. + + Unlike `retrieve_api_payload_async`, this returns the response body + as-is. Orchestrator's `GET /JobTriggers/GetPayload/{inboxId}` returns + the stored payload directly without an envelope. + + Args: + inbox_id: The Id of the inbox to fetch the payload for. + + Returns: + The stored payload. + """ + spec = self._retrieve_api_payload_spec(inbox_id=inbox_id) + + response = await self.request_async( + spec.method, + url=spec.endpoint, + headers=spec.headers, + ) + + return response.json() + def _extract_first_inbox_id(self, response: Any) -> str: if len(response["value"]) > 0: return response["value"][0]["ItemKey"] diff --git a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py index 98e4f81a2..b2dbae787 100644 --- a/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py +++ b/packages/uipath-platform/src/uipath/platform/resume_triggers/_protocol.py @@ -13,6 +13,7 @@ from uipath.core.serialization import serialize_object from uipath.core.triggers import ( UiPathApiTrigger, + UiPathIntegrationTrigger, UiPathResumeTrigger, UiPathResumeTriggerName, UiPathResumeTriggerType, @@ -43,11 +44,13 @@ WaitEphemeralIndex, WaitEphemeralIndexRaw, WaitEscalation, + WaitIntegrationEvent, WaitJob, WaitJobRaw, WaitSystemAgent, WaitTask, ) +from uipath.platform.connections import EventArguments from uipath.platform.context_grounding import DeepRagStatus, IndexStatus from uipath.platform.context_grounding.context_grounding_index import ( ContextGroundingIndex, @@ -401,6 +404,23 @@ async def read_trigger(self, trigger: UiPathResumeTrigger) -> Any | None: f"Error fetching API trigger payload for inbox {trigger.api_resume.inbox_id}: {str(e)}", ) from e + case UiPathResumeTriggerType.INBOX: + if trigger.integration_resume and trigger.integration_resume.inbox_id: + try: + inbox_payload = await uipath.jobs.retrieve_inbox_payload_async( + trigger.integration_resume.inbox_id + ) + event_args = EventArguments.model_validate(inbox_payload) + return await uipath.connections.retrieve_event_payload_async( + event_args + ) + except Exception as e: + raise UiPathFaultedTriggerError( + ErrorCategory.SYSTEM, + f"Failed to get trigger payload" + f"Error fetching Inbox trigger payload for inbox {trigger.integration_resume.inbox_id}: {str(e)}", + ) from e + case _: raise UiPathFaultedTriggerError( ErrorCategory.SYSTEM, @@ -461,6 +481,9 @@ async def create_trigger(self, suspend_value: Any) -> UiPathResumeTrigger: case UiPathResumeTriggerType.API: self._handle_api_trigger(suspend_value, resume_trigger) + case UiPathResumeTriggerType.INBOX: + await self._handle_inbox_trigger(suspend_value, resume_trigger) + case UiPathResumeTriggerType.DEEP_RAG: await self._handle_deep_rag_job_trigger( suspend_value, resume_trigger @@ -545,6 +568,8 @@ def _determine_trigger_type(self, value: Any) -> UiPathResumeTriggerType: value, (DocumentExtractionValidation, WaitDocumentExtractionValidation) ): return UiPathResumeTriggerType.IXP_VS_ESCALATION + if isinstance(value, WaitIntegrationEvent): + return UiPathResumeTriggerType.INBOX # default to API trigger return UiPathResumeTriggerType.API @@ -579,6 +604,8 @@ def _determine_trigger_name(self, value: Any) -> UiPathResumeTriggerName: return UiPathResumeTriggerName.BATCH_RAG if isinstance(value, (DocumentExtraction, WaitDocumentExtraction)): return UiPathResumeTriggerName.EXTRACTION + if isinstance(value, WaitIntegrationEvent): + return UiPathResumeTriggerName.INBOX # default to API trigger return UiPathResumeTriggerName.API @@ -901,6 +928,57 @@ def _handle_api_trigger( inbox_id=str(uuid.uuid4()), request=serialize_object(value) ) + async def _handle_inbox_trigger( + self, value: WaitIntegrationEvent, resume_trigger: UiPathResumeTrigger + ) -> None: + """Handle Inbox-type resume triggers. + + Resolves `connection_name` (scoped to `connection_folder_path` when + provided) to a connection id via the Connections service, populates + `integration_resume` with the Integration Services configuration plus a + freshly generated `inbox_id`. The Connections-service registration is + performed server-side by Orchestrator's `CreateResumeTriggerTaskHandler` + once the job suspends. + + Args: + value: The suspend value (WaitIntegrationEvent) + resume_trigger: The resume trigger to populate + + Raises: + Exception: If no connection matches `connection_name`, or if more + than one exact match is found. + """ + uipath = UiPath() + connections = await uipath.connections.list_async( + name=value.connection_name, + folder_path=value.connection_folder_path, + connector_key=value.connector, + ) + connection = next( + (c for c in connections if c.name == value.connection_name), None + ) + if connection is None: + raise Exception( + f"No connection named '{value.connection_name}' " + f"for connector '{value.connector}' found" + + ( + f" in folder '{value.connection_folder_path}'" + if value.connection_folder_path + else "" + ) + ) + assert connection.id is not None + + resume_trigger.integration_resume = UiPathIntegrationTrigger( + connector=value.connector, + connection_id=connection.id, + operation=value.operation, + object_name=value.object_name, + filter_expression=value.filter_expression, + parameters=value.parameters, + inbox_id=str(uuid.uuid4()), + ) + class UiPathResumeTriggerHandler: """Combined handler for creating and reading resume triggers. diff --git a/packages/uipath-platform/tests/services/test_hitl.py b/packages/uipath-platform/tests/services/test_hitl.py index 2e8069a3e..7395908a9 100644 --- a/packages/uipath-platform/tests/services/test_hitl.py +++ b/packages/uipath-platform/tests/services/test_hitl.py @@ -1,3 +1,4 @@ +import json import uuid from typing import Any from unittest.mock import AsyncMock, patch @@ -7,6 +8,7 @@ from uipath.core.errors import ErrorCategory, UiPathFaultedTriggerError from uipath.core.triggers import ( UiPathApiTrigger, + UiPathIntegrationTrigger, UiPathResumeTrigger, UiPathResumeTriggerName, UiPathResumeTriggerType, @@ -32,11 +34,13 @@ WaitDocumentExtractionValidation, WaitEphemeralIndex, WaitEphemeralIndexRaw, + WaitIntegrationEvent, WaitJob, WaitJobRaw, WaitSystemAgent, WaitTask, ) +from uipath.platform.connections import Connection from uipath.platform.context_grounding import ( BatchTransformCreationResponse, BatchTransformOutputColumn, @@ -508,6 +512,91 @@ async def test_read_api_trigger_failure( await reader.read_trigger(resume_trigger) assert exc_info.value.category == ErrorCategory.SYSTEM + @pytest.mark.anyio + async def test_read_inbox_trigger( + self, + httpx_mock: HTTPXMock, + base_url: str, + setup_test_env: None, + ) -> None: + """Test reading an Inbox trigger fetches the IS metadata via GetPayload + and then enriches it via /elements_/v1/events/{processedEventId}. + """ + inbox_id = str(uuid.uuid4()) + processed_event_id = "v2::pp::1777041494382::334071::e374ecd5d0f73c21" + inbox_metadata = { + "UiPathEventConnector": "uipath-slack", + "UiPathEvent": "NEW_MESSAGE", + "UiPathEventObjectType": "Message", + "UiPathEventObjectId": "C123:1777041494.382", + "UiPathAdditionalEventData": json.dumps( + {"processedEventId": processed_event_id} + ), + } + enriched_event = { + "channel": "alerts", + "user": "U456", + "text": "hello from slack", + "ts": "1777041494.382", + } + + httpx_mock.add_response( + url=f"{base_url}/orchestrator_/api/JobTriggers/GetPayload/{inbox_id}", + status_code=200, + json=inbox_metadata, + ) + httpx_mock.add_response( + url=f"{base_url}/elements_/v1/events/{processed_event_id}", + status_code=200, + json=enriched_event, + ) + + resume_trigger = UiPathResumeTrigger( + trigger_type=UiPathResumeTriggerType.INBOX, + integration_resume=UiPathIntegrationTrigger( + connector="slack", + connection_id=str(uuid.uuid4()), + operation="OnMessage", + object_name="Message", + inbox_id=inbox_id, + ), + ) + + reader = UiPathResumeTriggerReader() + result = await reader.read_trigger(resume_trigger) + assert result == enriched_event + + @pytest.mark.anyio + async def test_read_inbox_trigger_failure( + self, + httpx_mock: HTTPXMock, + base_url: str, + setup_test_env: None, + ) -> None: + """Test reading an Inbox trigger with a failed payload response.""" + inbox_id = str(uuid.uuid4()) + + httpx_mock.add_response( + url=f"{base_url}/orchestrator_/api/JobTriggers/GetPayload/{inbox_id}", + status_code=500, + ) + + resume_trigger = UiPathResumeTrigger( + trigger_type=UiPathResumeTriggerType.INBOX, + integration_resume=UiPathIntegrationTrigger( + connector="slack", + connection_id=str(uuid.uuid4()), + operation="OnMessage", + object_name="Message", + inbox_id=inbox_id, + ), + ) + + with pytest.raises(UiPathFaultedTriggerError) as exc_info: + reader = UiPathResumeTriggerReader() + await reader.read_trigger(resume_trigger) + assert exc_info.value.category == ErrorCategory.SYSTEM + @pytest.mark.anyio async def test_read_deep_rag_trigger_successful( self, @@ -1374,6 +1463,186 @@ async def test_create_resume_trigger_api( assert isinstance(resume_trigger.api_resume.inbox_id, str) assert resume_trigger.api_resume.request == api_input + @pytest.mark.anyio + async def test_create_resume_trigger_wait_integration_event( + self, + setup_test_env: None, + ) -> None: + """Test creating a resume trigger for WaitIntegrationEvent.""" + connection_id = str(uuid.uuid4()) + mock_connection = Connection( + id=connection_id, name="Slack-Alerts", element_instance_id=1 + ) + mock_list_async = AsyncMock(return_value=[mock_connection]) + + wait_event = WaitIntegrationEvent( + connector="slack", + connection_name="Slack-Alerts", + connection_folder_path="Shared", + operation="OnMessage", + object_name="Message", + filter_expression="channel == 'alerts'", + parameters={"channel_id": "C123"}, + ) + + with patch( + "uipath.platform.connections._connections_service.ConnectionsService.list_async", + new=mock_list_async, + ): + processor = UiPathResumeTriggerCreator() + resume_trigger = await processor.create_trigger(wait_event) + + assert resume_trigger is not None + assert resume_trigger.trigger_type == UiPathResumeTriggerType.INBOX + assert resume_trigger.trigger_name == UiPathResumeTriggerName.INBOX + assert resume_trigger.api_resume is None + assert resume_trigger.integration_resume is not None + assert resume_trigger.integration_resume.connector == "slack" + assert resume_trigger.integration_resume.connection_id == connection_id + assert resume_trigger.integration_resume.operation == "OnMessage" + assert resume_trigger.integration_resume.object_name == "Message" + assert ( + resume_trigger.integration_resume.filter_expression == "channel == 'alerts'" + ) + assert resume_trigger.integration_resume.parameters == {"channel_id": "C123"} + assert isinstance(resume_trigger.integration_resume.inbox_id, str) + uuid.UUID(resume_trigger.integration_resume.inbox_id) + mock_list_async.assert_called_once_with( + name="Slack-Alerts", folder_path="Shared", connector_key="slack" + ) + + @pytest.mark.anyio + async def test_create_resume_trigger_wait_integration_event_optional_fields_omitted( + self, + setup_test_env: None, + ) -> None: + """Test that filter_expression, parameters, and folder_path are optional.""" + mock_connection = Connection( + id=str(uuid.uuid4()), name="Teams-Default", element_instance_id=2 + ) + mock_list_async = AsyncMock(return_value=[mock_connection]) + + wait_event = WaitIntegrationEvent( + connector="teams", + connection_name="Teams-Default", + operation="OnReply", + object_name="Reply", + ) + + with patch( + "uipath.platform.connections._connections_service.ConnectionsService.list_async", + new=mock_list_async, + ): + processor = UiPathResumeTriggerCreator() + resume_trigger = await processor.create_trigger(wait_event) + + assert resume_trigger.integration_resume is not None + assert resume_trigger.integration_resume.filter_expression is None + assert resume_trigger.integration_resume.parameters is None + mock_list_async.assert_called_once_with( + name="Teams-Default", folder_path=None, connector_key="teams" + ) + + @pytest.mark.anyio + async def test_wait_integration_event_serializes_with_camelcase_aliases( + self, + setup_test_env: None, + ) -> None: + """Wire shape: UiPathResumeTrigger.integration_resume must serialize + with the field names Orchestrator's ResumeTriggerDto/IntegrationResumeDto + expect (PascalCase-ish camelCase). + """ + connection_id = str(uuid.uuid4()) + mock_connection = Connection( + id=connection_id, name="Slack-Alerts", element_instance_id=3 + ) + mock_list_async = AsyncMock(return_value=[mock_connection]) + + wait_event = WaitIntegrationEvent( + connector="slack", + connection_name="Slack-Alerts", + operation="OnMessage", + object_name="Message", + ) + + with patch( + "uipath.platform.connections._connections_service.ConnectionsService.list_async", + new=mock_list_async, + ): + processor = UiPathResumeTriggerCreator() + resume_trigger = await processor.create_trigger(wait_event) + + dumped = resume_trigger.model_dump(by_alias=True, exclude_none=True) + + assert dumped["triggerType"] == UiPathResumeTriggerType.INBOX + assert "integrationResume" in dumped + integration = dumped["integrationResume"] + assert integration["connector"] == "slack" + assert integration["connectionId"] == connection_id + assert integration["operation"] == "OnMessage" + assert integration["objectName"] == "Message" + assert "inboxId" in integration + uuid.UUID(integration["inboxId"]) + + @pytest.mark.anyio + async def test_create_resume_trigger_wait_integration_event_no_match( + self, + setup_test_env: None, + ) -> None: + """Listing returns no exact-name match -> creator raises.""" + mock_list_async = AsyncMock(return_value=[]) + + wait_event = WaitIntegrationEvent( + connector="slack", + connection_name="Missing-Connection", + operation="OnMessage", + object_name="Message", + ) + + with patch( + "uipath.platform.connections._connections_service.ConnectionsService.list_async", + new=mock_list_async, + ): + processor = UiPathResumeTriggerCreator() + with pytest.raises(UiPathFaultedTriggerError): + await processor.create_trigger(wait_event) + + @pytest.mark.anyio + async def test_create_resume_trigger_wait_integration_event_filters_to_exact_match( + self, + setup_test_env: None, + ) -> None: + """list_async partial-matches; creator must pick the exact-name entry.""" + target_id = str(uuid.uuid4()) + # list_async partial-matches; simulate prefix-matching returning extras + mock_list_async = AsyncMock( + return_value=[ + Connection( + id=str(uuid.uuid4()), + name="Slack-Alerts-Old", + element_instance_id=4, + ), + Connection(id=target_id, name="Slack-Alerts", element_instance_id=5), + ] + ) + + wait_event = WaitIntegrationEvent( + connector="slack", + connection_name="Slack-Alerts", + operation="OnMessage", + object_name="Message", + ) + + with patch( + "uipath.platform.connections._connections_service.ConnectionsService.list_async", + new=mock_list_async, + ): + processor = UiPathResumeTriggerCreator() + resume_trigger = await processor.create_trigger(wait_event) + + assert resume_trigger.integration_resume is not None + assert resume_trigger.integration_resume.connection_id == target_id + @pytest.mark.anyio async def test_create_resume_trigger_create_deep_rag( self, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 415634498..773e3ea39 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1056,7 +1056,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.14" +version = "0.5.15" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.39" +version = "0.1.40" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 10b7e683b..5bef60a68 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2650,7 +2650,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.14" +version = "0.5.15" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.39" +version = "0.1.40" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 867a31e7957cbc2ae81c7ddb9e71a3d7187eed51 Mon Sep 17 00:00:00 2001 From: dianagrecu-uipath Date: Thu, 30 Apr 2026 15:17:00 +0300 Subject: [PATCH 043/121] refactor(agent): replace metadata.is_case_manager with metadata.variant (#1606) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 12 +++++++++-- .../uipath/tests/agent/models/test_agent.py | 20 +++++++++---------- packages/uipath/uv.lock | 2 +- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 3883b3d84..c2ffb843f 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.59" +version = "2.10.60" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 8f8b8a207..38bc8a815 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -165,6 +165,12 @@ class AgentMessageRole(str, CaseInsensitiveEnum): USER = "user" +class AgentVariant(str, CaseInsensitiveEnum): + """Agent variant enumeration.""" + + CASE_MANAGER = "caseManager" + + class AgentGuardrailActionType(str, CaseInsensitiveEnum): """Agent guardrail action type enumeration.""" @@ -1156,7 +1162,7 @@ class AgentMetadata(BaseCfg): """Agent metadata model.""" is_conversational: bool = Field(alias="isConversational") - is_case_manager: bool = Field(default=False, alias="isCaseManager") + variant: Optional[AgentVariant] = Field(default=None, alias="variant") storage_version: str = Field(alias="storageVersion") @@ -1220,7 +1226,9 @@ def is_conversational(self) -> bool: @property def is_case_manager(self) -> bool: """Checks if the agent is a case manager agent.""" - return self.metadata.is_case_manager if self.metadata else False + if not self.metadata: + return False + return self.metadata.variant == AgentVariant.CASE_MANAGER @staticmethod def _normalize_guardrails(v: Dict[str, Any]) -> None: diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 6c4c0519a..4daae105a 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3286,15 +3286,15 @@ def test_is_conversational_false_by_default(self): class TestAgentDefinitionIsCaseManager: """Tests for AgentDefinition.is_case_manager property.""" - def test_is_case_manager_true_when_metadata_set(self): - """Returns True when metadata.is_case_manager is True.""" + def test_is_case_manager_true_when_variant_is_case_manager(self): + """Returns True when metadata.variant is "caseManager".""" json_data = { "id": "test-case-manager", "name": "Case Manager Agent", "version": "1.0.0", "metadata": { "isConversational": False, - "isCaseManager": True, + "variant": "caseManager", "storageVersion": "1.0.0", }, "settings": { @@ -3317,15 +3317,15 @@ def test_is_case_manager_true_when_metadata_set(self): assert config.is_case_manager is True - def test_is_case_manager_false_when_metadata_set_false(self): - """Returns False when metadata.is_case_manager is False.""" + def test_is_case_manager_false_when_variant_is_none(self): + """Returns False when metadata.variant is None.""" json_data = { "id": "test-non-case-manager", "name": "Regular Agent", "version": "1.0.0", "metadata": { "isConversational": False, - "isCaseManager": False, + "variant": None, "storageVersion": "1.0.0", }, "settings": { @@ -3346,11 +3346,11 @@ def test_is_case_manager_false_when_metadata_set_false(self): assert config.is_case_manager is False - def test_is_case_manager_false_when_not_in_metadata(self): - """Returns False when isCaseManager is not present in metadata.""" + def test_is_case_manager_false_when_variant_not_in_metadata(self): + """Returns False when variant is not present in metadata.""" json_data = { - "id": "test-no-case-manager-field", - "name": "Agent Without CM Field", + "id": "test-no-variant-field", + "name": "Agent Without Variant Field", "version": "1.0.0", "metadata": {"isConversational": False, "storageVersion": "1.0.0"}, "settings": { diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 5bef60a68..ea05f4f25 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.59" +version = "2.10.60" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 61439910d6448bccaf81dfab60698dbe47f35213 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Thu, 30 Apr 2026 14:28:51 +0200 Subject: [PATCH 044/121] docs: document uv.lock packing and --nolock flag (#423) (#1597) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath/docs/cli/index.md | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index 9e8401e8c..46f96c460 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -203,6 +203,25 @@ authors = [{name = "Your Name", email = "your.email@example.com"}] ``` /// +/// info +### Dependency Locking + +By default, `uipath pack` includes `uv.lock` in the `.nupkg` (creating it if it does not exist). The executor then installs the pinned versions from the lock file, so every run uses the exact same dependency versions. + +Use `--nolock` to opt out — `uv.lock` is not added to the package. With no lock file present, the executor resolves dependencies on each run and picks the latest versions compatible with the constraints in your `pyproject.toml`. + + +```shell +> uipath pack --nolock +⠋ Packaging project ... +✓ Project successfully packaged. +``` + +**When to lock (default):** you want reproducible runs and protection against breaking changes or malicious upgrades in your dependencies. The versions you tested with are the versions that run. + +**When to use `--nolock`:** you want each run to pick up the latest patches automatically within your declared constraints, or your project does not use uv. +/// + ```shell > uipath pack @@ -289,6 +308,19 @@ Importing referenced resources to Studio Web project... 🔵 Resource import summary: 0 total resources - 0 created, 0 updated, 0 unchanged, 0 not found ``` + +/// info +### Dependency Locking + +By default, `uipath push` includes `uv.lock` in the upload (creating it if it does not exist). The executor then installs the pinned versions from the lock file, so every run uses the exact same dependency versions. + +Use `--nolock` to opt out — `uv.lock` is not uploaded. With no lock file present, the executor resolves dependencies on each run and picks the latest versions compatible with the constraints in your `pyproject.toml`. + +**When to lock (default):** you want reproducible runs and protection against breaking changes or malicious upgrades in your dependencies. The versions you tested with are the versions that run. + +**When to use `--nolock`:** you want each run to pick up the latest patches automatically within your declared constraints, or your project does not use uv. +/// + --- ::: mkdocs-click From fa662117311e1635fa02ef5a63bcb6e072159ba2 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Thu, 30 Apr 2026 14:29:40 +0200 Subject: [PATCH 045/121] docs: document client credentials authentication flow (#422) (#1598) --- packages/uipath/docs/cli/index.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index 46f96c460..165c5431a 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -32,6 +32,37 @@ Select tenant number: 0 Selected tenant: Tenant1 ✓ Authentication successful. ``` + +/// info | Unattended Authentication (Client Credentials) + +For CI/CD pipelines and other non-interactive contexts, authenticate with the OAuth client credentials flow by passing all three of `--client-id`, `--client-secret`, and `--base-url`. The CLI exchanges them for an access token and writes it to the same on-disk session used by interactive logins, so subsequent commands like `uipath publish` and `uipath invoke` work without further setup. + +The `--base-url` must point at the tenant scope (`https:////`). The optional `--scope` flag controls the OAuth scopes requested and defaults to `OR.Execution`. Pass a space-separated list (for example `"OR.Execution OR.Queues"`) to request additional scopes — match the scopes you granted to the External Application and the operations you intend to run. + +**Setup:** + +1. In the Automation Cloud **Admin** page, open **External Applications** and create one of type *Confidential*. Grant it the Orchestrator scopes you need (for example `OR.Execution`). See the [External Applications guide](https://docs.uipath.com/automation-cloud/automation-cloud/latest/admin-guide/managing-external-applications) for details. +2. Copy the generated **App ID** and **App Secret** — these become `--client-id` and `--client-secret`. + +**Example:** + + +```shell +> uipath auth --client-id 12345678-c4c5-4f1f-93ff-4f5ab47d57ea \ + --client-secret 'your-secret' \ + --base-url https://cloud.uipath.com/your-org/your-tenant +✓ Authentication successful. +> uipath publish --tenant +``` + +/// warning +Treat `--client-secret` as a credential. In CI, prefer reading it from a secret store and passing it on the command line, rather than committing it to source control or leaving it in shell history. +/// + +**Configuring the same flow in code:** if you would rather skip the CLI session and pass credentials directly to the SDK, the [`asset-modifier-agent` sample](https://github.com/UiPath/uipath-python/tree/main/packages/uipath/samples/asset-modifier-agent) shows how to construct a `UiPath` client with `client_id`, `client_secret`, `scope`, and `base_url` from environment variables. + +/// + --- ::: mkdocs-click From 1732f7a0a5278137e0be05586a72135b9c10ba8a Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Thu, 30 Apr 2026 17:05:57 +0200 Subject: [PATCH 046/121] fix: serialize datetime fields when creating queue items (#670) (#1607) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/orchestrator/_queues_service.py | 14 +++++--- .../tests/services/test_queues_service.py | 34 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 2 +- 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 234dd78b8..a96a8eded 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.40" +version = "0.1.41" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/_queues_service.py b/packages/uipath-platform/src/uipath/platform/orchestrator/_queues_service.py index eede00ecf..1a2985072 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/_queues_service.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/_queues_service.py @@ -454,7 +454,9 @@ def _create_item_spec( elif isinstance(item, QueueItem): queue_item = item - item_data = queue_item.model_dump(exclude_unset=True, by_alias=True) + item_data = queue_item.model_dump( + mode="json", exclude_unset=True, by_alias=True + ) resolved_name = queue_name or item_data.get("Name") if resolved_name is None: raise ValueError( @@ -493,9 +495,11 @@ def _create_items_spec( "queueName": queue_name, "commitType": commit_type.value, "queueItems": [ - item.model_dump(exclude_unset=True, by_alias=True) + item.model_dump(mode="json", exclude_unset=True, by_alias=True) if isinstance(item, QueueItem) - else QueueItem(**item).model_dump(exclude_unset=True, by_alias=True) + else QueueItem(**item).model_dump( + mode="json", exclude_unset=True, by_alias=True + ) for item in items ], }, @@ -519,7 +523,7 @@ def _create_transaction_item_spec( transaction_item = item transaction_data = transaction_item.model_dump( - exclude_unset=True, by_alias=True + mode="json", exclude_unset=True, by_alias=True ) resolved_name = queue_name or transaction_data.get("Name") if resolved_name is None: @@ -580,7 +584,7 @@ def _complete_transaction_item_spec( ), json={ "transactionResult": transaction_result.model_dump( - exclude_unset=True, by_alias=True + mode="json", exclude_unset=True, by_alias=True ) }, headers={ diff --git a/packages/uipath-platform/tests/services/test_queues_service.py b/packages/uipath-platform/tests/services/test_queues_service.py index 51cfeaa95..2dd81ccc0 100644 --- a/packages/uipath-platform/tests/services/test_queues_service.py +++ b/packages/uipath-platform/tests/services/test_queues_service.py @@ -1,4 +1,5 @@ import json +from datetime import datetime, timezone import pytest from pytest_httpx import HTTPXMock @@ -518,6 +519,39 @@ async def test_create_item_with_reference_async( == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.QueuesService.create_item_async/{version}" ) + def test_create_item_with_datetime_fields( + self, + httpx_mock: HTTPXMock, + service: QueuesService, + base_url: str, + org: str, + tenant: str, + ) -> None: + defer = datetime(2026, 5, 1, 9, 0, 0, tzinfo=timezone.utc) + due = datetime(2026, 5, 2, 17, 30, 0, tzinfo=timezone.utc) + risk = datetime(2026, 5, 2, 12, 0, 0, tzinfo=timezone.utc) + queue_item = QueueItem( + priority=QueueItemPriority.NORMAL, + specific_content={"key": "value"}, + defer_date=defer, + due_date=due, + risk_sla_date=risk, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Queues/UiPathODataSvc.AddQueueItem", + status_code=200, + json={"Id": 1}, + ) + + service.create_item(queue_item, queue_name="test-queue") + + sent_request = httpx_mock.get_request() + assert sent_request is not None + body = json.loads(sent_request.content.decode()) + assert body["itemData"]["DeferDate"] == defer.isoformat() + assert body["itemData"]["DueDate"] == due.isoformat() + assert body["itemData"]["RiskSlaDate"] == risk.isoformat() + def test_create_transaction_item( self, httpx_mock: HTTPXMock, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 773e3ea39..b8f7a0163 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.40" +version = "0.1.41" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index c2ffb843f..7e98f563b 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.39, <0.2.0", + "uipath-platform>=0.1.41, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index ea05f4f25..0525ffb1c 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.40" +version = "0.1.41" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From ee98f4d79e007b2781b3fa23bc0cf7ba2f76542c Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:35:35 +0200 Subject: [PATCH 047/121] feat: add support for system indexes [ECS-1791] (#1605) --- packages/uipath-platform/pyproject.toml | 2 +- .../_context_grounding_service.py | 147 ++++- .../src/uipath/platform/errors/__init__.py | 5 + ...ext_grounding_index_not_found_exception.py | 13 + .../test_context_grounding_service.py | 524 ++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../cli/contract/test_sdk_cli_alignment.py | 8 +- packages/uipath/uv.lock | 2 +- 9 files changed, 683 insertions(+), 22 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/errors/_context_grounding_index_not_found_exception.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index a96a8eded..c52b917eb 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.41" +version = "0.1.42" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py index 706394aca..1c2eabd85 100644 --- a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py +++ b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py @@ -19,6 +19,7 @@ from ..errors import ( BatchTransformFailedException, BatchTransformNotCompleteException, + ContextGroundingIndexNotFoundError, IngestionInProgressException, UnsupportedDataSourceException, ) @@ -256,6 +257,42 @@ async def retrieve_across_folders_async( ContextGroundingIndex.model_validate(item) for item in response["value"] ] + @traced(name="contextgrounding_retrieve_system_indexes", run_type="uipath") + def _retrieve_system_indexes( + self, + name: Optional[str] = None, + ) -> List[ContextGroundingIndex]: + spec = self._retrieve_system_indexes_spec(name=name) + + response = self.request( + spec.method, + spec.endpoint, + params=spec.params, + ).json() + + return [ + ContextGroundingIndex.model_validate(item) for item in response["value"] + ] + + @traced(name="contextgrounding_retrieve_system_indexes", run_type="uipath") + async def _retrieve_system_indexes_async( + self, + name: Optional[str] = None, + ) -> List[ContextGroundingIndex]: + spec = self._retrieve_system_indexes_spec(name=name) + + response = ( + await self.request_async( + spec.method, + spec.endpoint, + params=spec.params, + ) + ).json() + + return [ + ContextGroundingIndex.model_validate(item) for item in response["value"] + ] + @resource_override(resource_type="index") @traced(name="contextgrounding_retrieve", run_type="uipath") def retrieve( @@ -263,30 +300,38 @@ def retrieve( name: str, folder_key: Optional[str] = None, folder_path: Optional[str] = None, + include_system_indexes: bool = False, ) -> ContextGroundingIndex: """Retrieve context grounding index information by its name. If no folder_key or folder_path is provided and no folder context is - configured, falls back to searching across all folders. + configured, falls back to searching across all folders. When + ``include_system_indexes`` is True, an additional fallback against + system indexes is attempted before raising not-found. Args: name (str): The name of the context index to retrieve. folder_key (Optional[str]): The key of the folder where the index resides. folder_path (Optional[str]): The path of the folder where the index resides. + include_system_indexes (bool): If True, fall back to system indexes + when the index is not found in the per-folder or across-folders listings. + Defaults to False. Returns: ContextGroundingIndex: The index information, including its configuration and metadata if found. Raises: - Exception: If no index with the given name is found. + ContextGroundingIndexNotFoundError: If no index with the given name is found. """ resolved_folder_key = self._resolve_folder_key(folder_key, folder_path) if not resolved_folder_key: indexes = self.retrieve_across_folders(name=name) try: return next(index for index in indexes if index.name == name) - except StopIteration as e: - raise Exception("ContextGroundingIndex not found") from e + except StopIteration: + if include_system_indexes: + return self._retrieve_from_system_indexes(name) + raise ContextGroundingIndexNotFoundError(name) from None spec = self._retrieve_spec( name, @@ -305,8 +350,10 @@ def retrieve( for item in response["value"] if item["name"] == name ) - except StopIteration as e: - raise Exception("ContextGroundingIndex not found") from e + except StopIteration: + if include_system_indexes: + return self._retrieve_from_system_indexes(name) + raise ContextGroundingIndexNotFoundError(name) from None @resource_override(resource_type="index") @traced(name="contextgrounding_retrieve", run_type="uipath") @@ -315,30 +362,38 @@ async def retrieve_async( name: str, folder_key: Optional[str] = None, folder_path: Optional[str] = None, + include_system_indexes: bool = False, ) -> ContextGroundingIndex: """Asynchronously retrieve context grounding index information by its name. If no folder_key or folder_path is provided and no folder context is - configured, falls back to searching across all folders. + configured, falls back to searching across all folders. When + ``include_system_indexes`` is True, an additional fallback against + system indexes is attempted before raising not-found. Args: name (str): The name of the context index to retrieve. folder_key (Optional[str]): The key of the folder where the index resides. folder_path (Optional[str]): The path of the folder where the index resides. + include_system_indexes (bool): If True, fall back to system indexes when + the index is not found in the per-folder or across-folders listings. + Defaults to False. Returns: ContextGroundingIndex: The index information, including its configuration and metadata if found. Raises: - Exception: If no index with the given name is found. + ContextGroundingIndexNotFoundError: If no index with the given name is found. """ resolved_folder_key = self._resolve_folder_key(folder_key, folder_path) if not resolved_folder_key: indexes = await self.retrieve_across_folders_async(name=name) try: return next(index for index in indexes if index.name == name) - except StopIteration as e: - raise Exception("ContextGroundingIndex not found") from e + except StopIteration: + if include_system_indexes: + return await self._retrieve_from_system_indexes_async(name) + raise ContextGroundingIndexNotFoundError(name) from None spec = self._retrieve_spec( name, @@ -359,8 +414,26 @@ async def retrieve_async( for item in response["value"] if item["name"] == name ) - except StopIteration as e: - raise Exception("ContextGroundingIndex not found") from e + except StopIteration: + if include_system_indexes: + return await self._retrieve_from_system_indexes_async(name) + raise ContextGroundingIndexNotFoundError(name) from None + + def _retrieve_from_system_indexes(self, name: str) -> ContextGroundingIndex: + indexes = self._retrieve_system_indexes(name=name) + try: + return next(index for index in indexes if index.name == name) + except StopIteration: + raise ContextGroundingIndexNotFoundError(name) from None + + async def _retrieve_from_system_indexes_async( + self, name: str + ) -> ContextGroundingIndex: + indexes = await self._retrieve_system_indexes_async(name=name) + try: + return next(index for index in indexes if index.name == name) + except StopIteration: + raise ContextGroundingIndexNotFoundError(name) from None @traced(name="contextgrounding_list", run_type="uipath") def list( @@ -1489,6 +1562,7 @@ def unified_search( scope: Optional[UnifiedSearchScope] = None, folder_key: Optional[str] = None, folder_path: Optional[str] = None, + include_system_indexes: bool = False, ) -> UnifiedQueryResult: """Perform a unified search on a context grounding index. @@ -1504,11 +1578,19 @@ def unified_search( scope (Optional[UnifiedSearchScope]): Optional search scope (folder, extension). folder_key (Optional[str]): The key of the folder where the index resides. folder_path (Optional[str]): The path of the folder where the index resides. + include_system_indexes (bool): If True, fall back to tenant-wide + system indexes when the index is not found in folder or + across-folders listings. Defaults to False. Returns: UnifiedQueryResult: The unified search result containing semantic and/or tabular results. """ - index = self.retrieve(name, folder_key=folder_key, folder_path=folder_path) + index = self.retrieve( + name, + folder_key=folder_key, + folder_path=folder_path, + include_system_indexes=include_system_indexes, + ) folder_key = folder_key or index.folder_key @@ -1544,6 +1626,7 @@ async def unified_search_async( scope: Optional[UnifiedSearchScope] = None, folder_key: Optional[str] = None, folder_path: Optional[str] = None, + include_system_indexes: bool = False, ) -> UnifiedQueryResult: """Asynchronously perform a unified search on a context grounding index. @@ -1559,12 +1642,18 @@ async def unified_search_async( scope (Optional[UnifiedSearchScope]): Optional search scope (folder, extension). folder_key (Optional[str]): The key of the folder where the index resides. folder_path (Optional[str]): The path of the folder where the index resides. + include_system_indexes (bool): If True, fall back to tenant-wide + system indexes when the index is not found in folder or + across-folders listings. Defaults to False. Returns: UnifiedQueryResult: The unified search result containing semantic and/or tabular results. """ index = await self.retrieve_async( - name, folder_key=folder_key, folder_path=folder_path + name, + folder_key=folder_key, + folder_path=folder_path, + include_system_indexes=include_system_indexes, ) if index and index.in_progress_ingestion(): raise IngestionInProgressException(index_name=name) @@ -1911,6 +2000,16 @@ def _ingest_spec( }, ) + @staticmethod + def _odata_name_filter(name: str) -> str: + """Build an OData ``Name eq ''`` filter with single quotes escaped. + + OData string literals escape ``'`` by doubling it. URL encoding of the + resulting filter is handled by the HTTP client when params are passed + as a dict. + """ + return "Name eq '{}'".format(name.replace("'", "''")) + def _retrieve_across_folders_spec( self, name: Optional[str] = None, @@ -1919,7 +2018,7 @@ def _retrieve_across_folders_spec( "$expand": "dataSource", } if name: - params["$filter"] = f"Name eq '{name}'" + params["$filter"] = self._odata_name_filter(name) return RequestSpec( method="GET", @@ -1927,6 +2026,22 @@ def _retrieve_across_folders_spec( params=params, ) + def _retrieve_system_indexes_spec( + self, + name: Optional[str] = None, + ) -> RequestSpec: + params: Dict[str, str] = { + "$expand": "dataSource", + } + if name: + params["$filter"] = self._odata_name_filter(name) + + return RequestSpec( + method="GET", + endpoint=Endpoint("/ecs_/v2/indexes/allsystemindexes"), + params=params, + ) + def _list_spec( self, folder_key: Optional[str] = None, @@ -1954,7 +2069,7 @@ def _retrieve_spec( method="GET", endpoint=Endpoint("/ecs_/v2/indexes"), params={ - "$filter": f"Name eq '{name}'", + "$filter": self._odata_name_filter(name), "$expand": "dataSource", }, headers={ diff --git a/packages/uipath-platform/src/uipath/platform/errors/__init__.py b/packages/uipath-platform/src/uipath/platform/errors/__init__.py index 58afd93f7..97f7e6d98 100644 --- a/packages/uipath-platform/src/uipath/platform/errors/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/errors/__init__.py @@ -8,6 +8,7 @@ - FolderNotFoundException: Raised when a folder cannot be found - UnsupportedDataSourceException: Raised when an operation is attempted on an unsupported data source type - IngestionInProgressException: Raised when a search is attempted on an index during ingestion +- ContextGroundingIndexNotFoundError: Raised when a context grounding index cannot be resolved by name - BatchTransformFailedException: Raised when a batch transform has failed - BatchTransformNotCompleteException: Raised when attempting to get results from an incomplete batch transform - OperationNotCompleteException: Raised when attempting to get results from an incomplete operation @@ -18,6 +19,9 @@ from ._base_url_missing_error import BaseUrlMissingError from ._batch_transform_failed_exception import BatchTransformFailedException from ._batch_transform_not_complete_exception import BatchTransformNotCompleteException +from ._context_grounding_index_not_found_exception import ( + ContextGroundingIndexNotFoundError, +) from ._enriched_exception import EnrichedException, ExtractedErrorInfo from ._folder_not_found_exception import FolderNotFoundException from ._ingestion_in_progress_exception import IngestionInProgressException @@ -30,6 +34,7 @@ "BaseUrlMissingError", "BatchTransformFailedException", "BatchTransformNotCompleteException", + "ContextGroundingIndexNotFoundError", "EnrichedException", "ExtractedErrorInfo", "FolderNotFoundException", diff --git a/packages/uipath-platform/src/uipath/platform/errors/_context_grounding_index_not_found_exception.py b/packages/uipath-platform/src/uipath/platform/errors/_context_grounding_index_not_found_exception.py new file mode 100644 index 000000000..653be92e8 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/errors/_context_grounding_index_not_found_exception.py @@ -0,0 +1,13 @@ +from typing import Optional + + +class ContextGroundingIndexNotFoundError(Exception): + """Raised when a context grounding index cannot be resolved by name.""" + + def __init__(self, index_name: Optional[str] = None): + self.index_name = index_name + if index_name: + self.message = f"ContextGroundingIndex '{index_name}' not found" + else: + self.message = "ContextGroundingIndex not found" + super().__init__(self.message) diff --git a/packages/uipath-platform/tests/services/test_context_grounding_service.py b/packages/uipath-platform/tests/services/test_context_grounding_service.py index 94626b8a1..3fef9f47a 100644 --- a/packages/uipath-platform/tests/services/test_context_grounding_service.py +++ b/packages/uipath-platform/tests/services/test_context_grounding_service.py @@ -31,6 +31,7 @@ from uipath.platform.context_grounding._context_grounding_service import ( ContextGroundingService, ) +from uipath.platform.errors import ContextGroundingIndexNotFoundError from uipath.platform.orchestrator._buckets_service import BucketsService from uipath.platform.orchestrator._folder_service import FolderService @@ -761,6 +762,529 @@ async def test_retrieve_async_falls_back_to_across_folders_when_no_folder_contex assert len(sent_requests) == 1 assert "/ecs_/v2/indexes/allacrossfolders" in str(sent_requests[0].url) + def test_retrieve_system_indexes( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource", + status_code=200, + json={ + "value": [ + { + "id": "sys-index-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + }, + { + "id": "sys-index-2", + "name": "system-other-index", + "lastIngestionStatus": "Completed", + }, + ] + }, + ) + + indexes = service._retrieve_system_indexes() + + assert isinstance(indexes, list) + assert len(indexes) == 2 + assert isinstance(indexes[0], ContextGroundingIndex) + assert indexes[0].id == "sys-index-1" + assert indexes[0].name == "system-template-index" + + sent_requests = httpx_mock.get_requests() + assert sent_requests[0].method == "GET" + assert "/ecs_/v2/indexes/allsystemindexes" in str(sent_requests[0].url) + assert "x-uipath-folderkey" not in sent_requests[0].headers + + assert HEADER_USER_AGENT in sent_requests[0].headers + assert ( + sent_requests[0].headers[HEADER_USER_AGENT] + == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ContextGroundingService._retrieve_system_indexes/{version}" + ) + + def test_retrieve_system_indexes_with_name_filter( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={ + "value": [ + { + "id": "sys-index-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + }, + ] + }, + ) + + indexes = service._retrieve_system_indexes(name="system-template-index") + + assert len(indexes) == 1 + assert indexes[0].name == "system-template-index" + + sent_requests = httpx_mock.get_requests() + assert "allsystemindexes" in str(sent_requests[0].url) + assert "x-uipath-folderkey" not in sent_requests[0].headers + + @pytest.mark.anyio + async def test_retrieve_system_indexes_async( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource", + status_code=200, + json={ + "value": [ + { + "id": "sys-index-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + indexes = await service._retrieve_system_indexes_async() + + assert len(indexes) == 1 + assert indexes[0].id == "sys-index-1" + + sent_requests = httpx_mock.get_requests() + assert sent_requests[0].method == "GET" + assert "/ecs_/v2/indexes/allsystemindexes" in str(sent_requests[0].url) + assert "x-uipath-folderkey" not in sent_requests[0].headers + + assert ( + sent_requests[0].headers[HEADER_USER_AGENT] + == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ContextGroundingService._retrieve_system_indexes_async/{version}" + ) + + def test_retrieve_system_indexes_escapes_single_quote_in_name( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'O''Brien'", + status_code=200, + json={ + "value": [ + { + "id": "sys-1", + "name": "O'Brien", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + indexes = service._retrieve_system_indexes(name="O'Brien") + + assert len(indexes) == 1 + assert indexes[0].name == "O'Brien" + + def test_retrieve_across_folders_escapes_single_quote_in_name( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'O''Brien'", + status_code=200, + json={ + "value": [ + { + "id": "idx-1", + "name": "O'Brien", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + indexes = service.retrieve_across_folders(name="O'Brien") + + assert len(indexes) == 1 + assert indexes[0].name == "O'Brien" + + def test_retrieve_system_indexes_empty( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource", + status_code=200, + json={"value": []}, + ) + + indexes = service._retrieve_system_indexes() + + assert indexes == [] + + def test_retrieve_raises_typed_not_found_when_across_folders_empty( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + + with pytest.raises(ContextGroundingIndexNotFoundError) as exc_info: + service_no_folder.retrieve(name="missing-index") + + assert exc_info.value.index_name == "missing-index" + + @pytest.mark.anyio + async def test_retrieve_async_raises_typed_not_found_when_across_folders_empty( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + + with pytest.raises(ContextGroundingIndexNotFoundError): + await service_no_folder.retrieve_async(name="missing-index") + + def test_retrieve_falls_back_to_system_indexes_when_flag_true( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={ + "value": [ + { + "id": "sys-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + index = service_no_folder.retrieve( + name="system-template-index", include_system_indexes=True + ) + + assert index.id == "sys-1" + assert index.name == "system-template-index" + + sent_requests = httpx_mock.get_requests() + assert len(sent_requests) == 2 + assert "/ecs_/v2/indexes/allacrossfolders" in str(sent_requests[0].url) + assert "/ecs_/v2/indexes/allsystemindexes" in str(sent_requests[1].url) + + def test_retrieve_does_not_fall_back_to_system_indexes_when_flag_false( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + + with pytest.raises(ContextGroundingIndexNotFoundError): + service_no_folder.retrieve(name="missing-index") + + sent_requests = httpx_mock.get_requests() + assert len(sent_requests) == 1 + assert "/ecs_/v2/indexes/allacrossfolders" in str(sent_requests[0].url) + + def test_retrieve_skips_system_indexes_when_across_folders_resolves( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'tenant-index'", + status_code=200, + json={ + "value": [ + { + "id": "tenant-1", + "name": "tenant-index", + "lastIngestionStatus": "Completed", + "folderKey": "folder-x", + } + ] + }, + ) + + index = service_no_folder.retrieve( + name="tenant-index", include_system_indexes=True + ) + + assert index.id == "tenant-1" + + sent_requests = httpx_mock.get_requests() + assert len(sent_requests) == 1 + assert "/ecs_/v2/indexes/allacrossfolders" in str(sent_requests[0].url) + + def test_retrieve_falls_back_to_system_indexes_after_folder_lookup_misses( + self, + httpx_mock: HTTPXMock, + service: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/api/FoldersNavigation/GetFoldersForCurrentUser?searchText=test-folder-path&skip=0&take=20", + status_code=200, + json={ + "PageItems": [ + { + "Key": "test-folder-key", + "FullyQualifiedName": "test-folder-path", + } + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes?$filter=Name eq 'system-template-index'&$expand=dataSource", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={ + "value": [ + { + "id": "sys-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + index = service.retrieve( + name="system-template-index", + folder_path="test-folder-path", + include_system_indexes=True, + ) + + assert index.id == "sys-1" + + @pytest.mark.anyio + async def test_retrieve_async_falls_back_to_system_indexes_when_flag_true( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={ + "value": [ + { + "id": "sys-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + + index = await service_no_folder.retrieve_async( + name="system-template-index", include_system_indexes=True + ) + + assert index.id == "sys-1" + + def test_retrieve_with_flag_raises_when_system_indexes_also_empty( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'missing-index'", + status_code=200, + json={"value": []}, + ) + + with pytest.raises(ContextGroundingIndexNotFoundError): + service_no_folder.retrieve( + name="missing-index", include_system_indexes=True + ) + + def test_unified_search_forwards_include_system_indexes( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={ + "value": [ + { + "id": "sys-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v1.2/search/sys-1", + status_code=200, + json={ + "semanticResults": { + "values": [], + "metadata": {"operation_id": "op-1", "strategy": "semantic"}, + } + }, + ) + + result = service_no_folder.unified_search( + name="system-template-index", + query="hello", + include_system_indexes=True, + ) + + assert isinstance(result, UnifiedQueryResult) + + sent_requests = httpx_mock.get_requests() + assert any("allsystemindexes" in str(r.url) for r in sent_requests) + assert any("search/sys-1" in str(r.url) for r in sent_requests) + + @pytest.mark.anyio + async def test_unified_search_async_forwards_include_system_indexes( + self, + httpx_mock: HTTPXMock, + service_no_folder: ContextGroundingService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allacrossfolders?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={"value": []}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v2/indexes/allsystemindexes?$expand=dataSource&$filter=Name eq 'system-template-index'", + status_code=200, + json={ + "value": [ + { + "id": "sys-1", + "name": "system-template-index", + "lastIngestionStatus": "Completed", + } + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/ecs_/v1.2/search/sys-1", + status_code=200, + json={ + "semanticResults": { + "values": [], + "metadata": {"operation_id": "op-1", "strategy": "semantic"}, + } + }, + ) + + result = await service_no_folder.unified_search_async( + name="system-template-index", + query="hello", + include_system_indexes=True, + ) + + assert isinstance(result, UnifiedQueryResult) + + sent_requests = httpx_mock.get_requests() + assert any("allsystemindexes" in str(r.url) for r in sent_requests) + assert any("search/sys-1" in str(r.url) for r in sent_requests) + def test_search_uses_index_folder_key_when_no_folder_context( self, httpx_mock: HTTPXMock, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index b8f7a0163..8695a33f1 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.41" +version = "0.1.42" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7e98f563b..406c919af 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.41, <0.2.0", + "uipath-platform>=0.1.42, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py b/packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py index 52699d42e..a690727d4 100644 --- a/packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py +++ b/packages/uipath/tests/cli/contract/test_sdk_cli_alignment.py @@ -192,9 +192,13 @@ def assert_cli_sdk_alignment( # Used when SDK has optional params that CLI doesn't expose SDK_EXCLUSIONS = { "context-grounding_list": set(), - "context-grounding_retrieve": set(), + "context-grounding_retrieve": {"include_system_indexes"}, "context-grounding_create": {"source", "embeddings_enabled", "is_encrypted"}, - "context-grounding_search": {"scope", "number_of_results"}, + "context-grounding_search": { + "scope", + "number_of_results", + "include_system_indexes", + }, "context-grounding_ingest": set(), "context-grounding_delete": set(), "context-grounding_deep-rag_start": set(), diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 0525ffb1c..d59970eea 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.41" +version = "0.1.42" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From af3602e6c83b25ed4e50c4bcd3f06259a7bd9573 Mon Sep 17 00:00:00 2001 From: Chibi Vikramathithan Date: Fri, 1 May 2026 17:12:16 -0700 Subject: [PATCH 048/121] feat(eval): honor UIPATH_AGENT_ID, UIPATH_CLOUD_USER_ID, UIPATH_PROJECT_FILES_SOURCE env vars [AE-1396] (#1608) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/_config.py | 18 ++ .../src/uipath/platform/common/constants.py | 3 + .../tests/common/test_config_env_vars.py | 55 +++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- .../uipath/_cli/_evals/_progress_reporter.py | 59 ++++- .../src/uipath/_cli/_evals/_telemetry.py | 22 +- packages/uipath/src/uipath/_cli/_telemetry.py | 27 +-- .../tests/cli/eval/test_progress_reporter.py | 205 +++++++++++++++++- packages/uipath/uv.lock | 4 +- 11 files changed, 357 insertions(+), 44 deletions(-) create mode 100644 packages/uipath-platform/tests/common/test_config_env_vars.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index c52b917eb..989952eb3 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.42" +version = "0.1.43" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_config.py b/packages/uipath-platform/src/uipath/platform/common/_config.py index b656830f6..cdb5eb383 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -61,6 +61,24 @@ def project_id(self) -> str | None: return os.getenv(ENV_UIPATH_PROJECT_ID, None) + @property + def agent_id(self) -> str | None: + from uipath.platform.common.constants import ENV_UIPATH_AGENT_ID + + return os.getenv(ENV_UIPATH_AGENT_ID) or self.project_id + + @property + def cloud_user_id(self) -> str | None: + from uipath.platform.common.constants import ENV_UIPATH_CLOUD_USER_ID + + return os.getenv(ENV_UIPATH_CLOUD_USER_ID, None) + + @property + def project_files_source(self) -> str | None: + from uipath.platform.common.constants import ENV_UIPATH_PROJECT_FILES_SOURCE + + return os.getenv(ENV_UIPATH_PROJECT_FILES_SOURCE, None) + @property def project_key(self) -> str | None: from uipath.platform.common.constants import ENV_PROJECT_KEY diff --git a/packages/uipath-platform/src/uipath/platform/common/constants.py b/packages/uipath-platform/src/uipath/platform/common/constants.py index 6184e844d..304ef64a6 100644 --- a/packages/uipath-platform/src/uipath/platform/common/constants.py +++ b/packages/uipath-platform/src/uipath/platform/common/constants.py @@ -17,6 +17,9 @@ ENV_TELEMETRY_ENABLED = "UIPATH_TELEMETRY_ENABLED" ENV_TRACING_ENABLED = "UIPATH_TRACING_ENABLED" ENV_UIPATH_PROJECT_ID = "UIPATH_PROJECT_ID" +ENV_UIPATH_AGENT_ID = "UIPATH_AGENT_ID" +ENV_UIPATH_CLOUD_USER_ID = "UIPATH_CLOUD_USER_ID" +ENV_UIPATH_PROJECT_FILES_SOURCE = "UIPATH_PROJECT_FILES_SOURCE" ENV_PROJECT_KEY = "PROJECT_KEY" ENV_PROCESS_KEY = "UIPATH_PROCESS_KEY" ENV_UIPATH_PROCESS_UUID = "UIPATH_PROCESS_UUID" diff --git a/packages/uipath-platform/tests/common/test_config_env_vars.py b/packages/uipath-platform/tests/common/test_config_env_vars.py new file mode 100644 index 000000000..8ff7b636d --- /dev/null +++ b/packages/uipath-platform/tests/common/test_config_env_vars.py @@ -0,0 +1,55 @@ +import pytest + +from uipath.platform.common._config import UiPathConfig + + +@pytest.fixture(autouse=True) +def _clear_env(monkeypatch): + for var in ( + "UIPATH_PROJECT_ID", + "UIPATH_AGENT_ID", + "UIPATH_CLOUD_USER_ID", + "UIPATH_PROJECT_FILES_SOURCE", + ): + monkeypatch.delenv(var, raising=False) + + +class TestProjectId: + def test_reads_env_var(self, monkeypatch): + monkeypatch.setenv("UIPATH_PROJECT_ID", "file-source-id") + assert UiPathConfig.project_id == "file-source-id" + + def test_returns_none_when_unset(self): + assert UiPathConfig.project_id is None + + +class TestAgentId: + def test_returns_explicit_agent_id_when_set(self, monkeypatch): + monkeypatch.setenv("UIPATH_PROJECT_ID", "debug-project-guid") + monkeypatch.setenv("UIPATH_AGENT_ID", "real-agent-id") + assert UiPathConfig.agent_id == "real-agent-id" + + def test_falls_back_to_project_id_when_agent_id_unset(self, monkeypatch): + monkeypatch.setenv("UIPATH_PROJECT_ID", "cloud-project-id") + assert UiPathConfig.agent_id == "cloud-project-id" + + def test_returns_none_when_neither_set(self): + assert UiPathConfig.agent_id is None + + +class TestCloudUserId: + def test_returns_value_when_set(self, monkeypatch): + monkeypatch.setenv("UIPATH_CLOUD_USER_ID", "user-guid") + assert UiPathConfig.cloud_user_id == "user-guid" + + def test_returns_none_when_unset(self): + assert UiPathConfig.cloud_user_id is None + + +class TestProjectFilesSource: + def test_returns_value_when_set(self, monkeypatch): + monkeypatch.setenv("UIPATH_PROJECT_FILES_SOURCE", "Local") + assert UiPathConfig.project_files_source == "Local" + + def test_returns_none_when_unset(self): + assert UiPathConfig.project_files_source is None diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 8695a33f1..66c8eb62a 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.42" +version = "0.1.43" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 406c919af..b48c898b5 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.60" +version = "2.10.61" 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" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.42, <0.2.0", + "uipath-platform>=0.1.43, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py b/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py index 7c5114516..fd4849076 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py +++ b/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py @@ -102,11 +102,18 @@ def __init__(self): self._console = console_logger self._rich_console = Console() self._project_id = os.getenv("UIPATH_PROJECT_ID", None) - if not self._project_id: + self._agent_id = os.getenv("UIPATH_AGENT_ID") or self._project_id + if not self._agent_id: logger.warning( "Cannot report data to StudioWeb. Please set UIPATH_PROJECT_ID." ) + # Map UIPATH_PROJECT_FILES_SOURCE (Local/Cloud) to the backend's + # ProjectFilesSource enum integer. Without this every row the worker + # creates lands as Cloud, and the UI's `?projectFilesSource=1` filter + # never matches local-workspace runs. + self._project_files_source = self._resolve_project_files_source() + self.eval_set_ids: dict[str, str] = {} # Track eval_set_id per execution self.eval_set_run_ids: dict[str, str] = {} self.evaluators: dict[str, Any] = {} @@ -1089,6 +1096,29 @@ def _collect_coded_results( evaluator_runs.append(evaluator_run) return evaluator_runs, evaluator_scores_list + @staticmethod + def _resolve_project_files_source() -> int | None: + raw = os.getenv("UIPATH_PROJECT_FILES_SOURCE") + if not raw: + return None + normalized = raw.strip().lower() + if normalized == "local": + return 1 + if normalized == "cloud": + return 0 + try: + return int(normalized) + except ValueError: + logger.warning( + f"Unrecognized UIPATH_PROJECT_FILES_SOURCE value: {raw!r}; ignoring." + ) + return None + + def _project_files_source_field(self) -> dict[str, int]: + if self._project_files_source is None: + return {} + return {"projectFilesSource": self._project_files_source} + def _update_eval_run_spec( self, assertion_runs: list[dict[str, Any]], @@ -1115,6 +1145,7 @@ def _update_eval_run_spec( }, "completionMetrics": {"duration": int(execution_time * 1000)}, "assertionRuns": assertion_runs, + **self._project_files_source_field(), } # Legacy backend expects payload wrapped in "request" field @@ -1133,7 +1164,7 @@ def _update_eval_run_spec( return RequestSpec( method="PUT", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun" ), json=payload, headers=self._tenant_header(), @@ -1166,6 +1197,7 @@ def _update_coded_eval_run_spec( }, "completionMetrics": {"duration": int(execution_time * 1000)}, "evaluatorRuns": evaluator_runs, + **self._project_files_source_field(), } # Log the payload for debugging coded eval run updates @@ -1181,7 +1213,7 @@ def _update_coded_eval_run_spec( return RequestSpec( method="PUT", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun" ), json=payload, headers=self._tenant_header(), @@ -1235,6 +1267,7 @@ def _create_eval_run_spec( "evalSnapshot": eval_snapshot, # Backend expects integer status "status": EvaluationStatus.IN_PROGRESS.value, + **self._project_files_source_field(), } # Legacy backend expects payload wrapped in "request" field @@ -1253,7 +1286,7 @@ def _create_eval_run_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun" ), json=payload, headers=self._tenant_header(), @@ -1283,7 +1316,7 @@ def _create_eval_set_run_spec( eval_set_id_value = str(uuid.uuid5(uuid.NAMESPACE_DNS, eval_set_id)) inner_payload: dict[str, Any] = { - "agentId": self._project_id, + "agentId": self._agent_id, "evalSetId": eval_set_id_value, "agentSnapshot": agent_snapshot.model_dump(by_alias=True), # Backend expects integer status @@ -1291,6 +1324,7 @@ def _create_eval_set_run_spec( "numberOfEvalsExecuted": no_of_evals, # Source is required by the backend (0 = coded SDK) "source": 0, + **self._project_files_source_field(), } # Both coded and legacy send payload directly at root level @@ -1309,7 +1343,7 @@ def _create_eval_set_run_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalSetRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalSetRun" ), json=payload, headers=self._tenant_header(), @@ -1353,6 +1387,7 @@ def _update_eval_set_run_spec( # Backend expects integer status "status": status.value, "evaluatorScores": evaluator_scores_list, + **self._project_files_source_field(), } # Legacy backend expects payload wrapped in "request" field @@ -1374,7 +1409,7 @@ def _update_eval_set_run_spec( return RequestSpec( method="PUT", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalSetRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalSetRun" ), json=payload, headers=self._tenant_header(), @@ -1406,12 +1441,12 @@ def _get_eval_runs_spec( if is_coded: endpoint_path = ( - f"{prefix}execution/agents/{self._project_id}/coded/" + f"{prefix}execution/agents/{self._agent_id}/coded/" f"evalSets/{eval_set_id}/evalSetRuns/{eval_set_run_id}/evalRuns" ) else: endpoint_path = ( - f"{prefix}execution/agents/{self._project_id}/" + f"{prefix}execution/agents/{self._agent_id}/" f"evalSets/{eval_set_id}/evalSetRuns/{eval_set_run_id}/evalRuns" ) @@ -1420,10 +1455,14 @@ def _get_eval_runs_spec( f"eval_set_run_id={eval_set_run_id}, evaluation_id={evaluation_id}, coded={is_coded}" ) + # The backend's listing endpoint filters by projectFilesSource + + # cloudUserId so the UI only shows the caller's local rows. Mirror + # that here so resume lookups match the row written by the same + # worker session. return RequestSpec( method="GET", endpoint=Endpoint(endpoint_path), - params={}, # No query params needed - evalSetRunId is in the path + params=self._project_files_source_field(), headers=self._tenant_header(), ) diff --git a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py index ad9549a6c..e28186576 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py @@ -308,28 +308,26 @@ def _enrich_properties(self, properties: dict[str, Any]) -> None: Args: properties: The properties dictionary to enrich. """ - # Add UiPath context if UiPathConfig.project_id: properties["ProjectId"] = UiPathConfig.project_id - properties["AgentId"] = UiPathConfig.project_id + if UiPathConfig.agent_id: + properties["AgentId"] = UiPathConfig.agent_id - # Get organization ID from UiPathConfig if UiPathConfig.organization_id: properties["CloudOrganizationId"] = UiPathConfig.organization_id - # Get CloudUserId from JWT token - try: - cloud_user_id = get_claim_from_token("sub") - if cloud_user_id: - properties["CloudUserId"] = cloud_user_id - except Exception: - pass # CloudUserId is optional + cloud_user_id = UiPathConfig.cloud_user_id + if not cloud_user_id: + try: + cloud_user_id = get_claim_from_token("sub") + except Exception: + cloud_user_id = None + if cloud_user_id: + properties["CloudUserId"] = cloud_user_id - # Get tenant ID from environment tenant_id = os.getenv("UIPATH_TENANT_ID") if tenant_id: properties["TenantId"] = tenant_id - # Add source identifier properties["Source"] = "uipath-python-cli" properties["ApplicationName"] = "UiPath.Eval" diff --git a/packages/uipath/src/uipath/_cli/_telemetry.py b/packages/uipath/src/uipath/_cli/_telemetry.py index 7adc54410..d245076a4 100644 --- a/packages/uipath/src/uipath/_cli/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_telemetry.py @@ -41,28 +41,26 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None: Args: properties: The properties dictionary to enrich. """ - # Add UiPath context - project_key = _get_project_key() - if project_key: - properties["AgentId"] = project_key + agent_id = os.getenv("UIPATH_AGENT_ID") or _get_project_key() + if agent_id: + properties["AgentId"] = agent_id - # Get organization ID if UiPathConfig.organization_id: properties["CloudOrganizationId"] = UiPathConfig.organization_id - # Get tenant ID if UiPathConfig.tenant_id: properties["CloudTenantId"] = UiPathConfig.tenant_id - # Get CloudUserId from JWT token - try: - cloud_user_id = get_claim_from_token("sub") - if cloud_user_id: - properties["CloudUserId"] = cloud_user_id - except Exception: - pass + cloud_user_id = UiPathConfig.cloud_user_id + if not cloud_user_id: + try: + cloud_user_id = get_claim_from_token("sub") + except Exception: + cloud_user_id = None + if cloud_user_id: + properties["CloudUserId"] = cloud_user_id - properties["SessionId"] = "nosession" # Placeholder for session ID + properties["SessionId"] = "nosession" try: properties["SDKVersion"] = version("uipath") @@ -71,7 +69,6 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None: properties["IsGithubCI"] = bool(os.getenv("GITHUB_ACTIONS")) - # Add source identifier properties["Source"] = "uipath-python-cli" properties["ApplicationName"] = "UiPath.AgentCli" diff --git a/packages/uipath/tests/cli/eval/test_progress_reporter.py b/packages/uipath/tests/cli/eval/test_progress_reporter.py index 87919c2b3..1fd00bf12 100644 --- a/packages/uipath/tests/cli/eval/test_progress_reporter.py +++ b/packages/uipath/tests/cli/eval/test_progress_reporter.py @@ -927,4 +927,207 @@ def test_build_evaluator_snapshot_skips_non_string_model(self, progress_reporter snapshot = progress_reporter._build_evaluator_snapshot(evaluator) assert snapshot["prompt"] == "Evaluate this" - assert "model" not in snapshot + + +class TestAgentIdRouting: + """Eval-set/eval-run API URLs route by AgentId, not file-source project. + + For local-workspace eval runs the file-source project (UIPATH_PROJECT_ID, + typically the cloud debug project's GUID) differs from the logical agent + (UIPATH_AGENT_ID). The route URL must reflect the logical agent so backend + auth/ownership/telemetry don't see the per-run debug project as the agent. + File fetching (UiPathConfig.project_id) is unaffected. + """ + + def _make_reporter(self, monkeypatch, project_id, agent_id): + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "test-token") + monkeypatch.setenv("UIPATH_TENANT_ID", "test-tenant-id") + if project_id is not None: + monkeypatch.setenv("UIPATH_PROJECT_ID", project_id) + else: + monkeypatch.delenv("UIPATH_PROJECT_ID", raising=False) + if agent_id is not None: + monkeypatch.setenv("UIPATH_AGENT_ID", agent_id) + else: + monkeypatch.delenv("UIPATH_AGENT_ID", raising=False) + return StudioWebProgressReporter() + + def test_agent_id_used_in_url_when_both_set(self, monkeypatch): + reporter = self._make_reporter( + monkeypatch, project_id="debug-project-guid", agent_id="real-agent-id" + ) + assert reporter._agent_id == "real-agent-id" + assert reporter._project_id == "debug-project-guid" + + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert "/agents/real-agent-id/" in spec.endpoint + assert "/agents/debug-project-guid/" not in spec.endpoint + + def test_agent_id_in_eval_set_run_payload(self, monkeypatch): + reporter = self._make_reporter( + monkeypatch, project_id="debug-project-guid", agent_id="real-agent-id" + ) + + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert spec.json["agentId"] == "real-agent-id" + + def test_falls_back_to_project_id_when_agent_id_unset(self, monkeypatch): + reporter = self._make_reporter( + monkeypatch, project_id="cloud-project-id", agent_id=None + ) + assert reporter._agent_id == "cloud-project-id" + + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert "/agents/cloud-project-id/" in spec.endpoint + + +class TestProjectFilesSourcePropagation: + """Reporter must propagate UIPATH_PROJECT_FILES_SOURCE to backend rows. + + Backend filters listings by `projectFilesSource` (Local=1, Cloud=0). Without + the SDK setting it on POST/PUT payloads and GET query params, every row + lands as Cloud and the UI's `?projectFilesSource=1` filter never matches + local-workspace runs. + """ + + def _make_reporter(self, monkeypatch, project_files_source): + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "test-token") + monkeypatch.setenv("UIPATH_TENANT_ID", "test-tenant-id") + monkeypatch.setenv("UIPATH_PROJECT_ID", "test-project-id") + if project_files_source is not None: + monkeypatch.setenv("UIPATH_PROJECT_FILES_SOURCE", project_files_source) + else: + monkeypatch.delenv("UIPATH_PROJECT_FILES_SOURCE", raising=False) + return StudioWebProgressReporter() + + @pytest.mark.parametrize( + "raw,expected", + [("Local", 1), ("local", 1), ("Cloud", 0), ("cloud", 0), ("1", 1), ("0", 0)], + ) + def test_resolves_env_var_to_int(self, monkeypatch, raw, expected): + reporter = self._make_reporter(monkeypatch, raw) + assert reporter._project_files_source == expected + + def test_returns_none_when_unset_or_garbage(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, None) + assert reporter._project_files_source is None + reporter2 = self._make_reporter(monkeypatch, "Banana") + assert reporter2._project_files_source is None + + def test_post_eval_set_run_payload_carries_source(self, monkeypatch): + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert spec.json["projectFilesSource"] == 1 + + def test_post_eval_run_payload_carries_source(self, monkeypatch): + from uipath.eval.models.evaluation_set import EvaluationItem + + reporter = self._make_reporter(monkeypatch, "Local") + item = EvaluationItem( + id="11111111-1111-1111-1111-111111111111", + name="t", + inputs={}, + evaluation_criterias={}, + ) + spec = reporter._create_eval_run_spec( + eval_item=item, eval_set_run_id="run-1", is_coded=False + ) + assert spec.json["projectFilesSource"] == 1 + + def test_put_eval_run_payload_carries_source(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._update_eval_run_spec( + assertion_runs=[], + evaluator_scores=[], + eval_run_id="run-1", + actual_output={}, + execution_time=1.0, + success=True, + is_coded=False, + ) + assert spec.json["projectFilesSource"] == 1 + + def test_put_coded_eval_run_payload_carries_source(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._update_coded_eval_run_spec( + evaluator_runs=[], + evaluator_scores=[], + eval_run_id="run-1", + actual_output={}, + execution_time=1.0, + success=True, + is_coded=True, + ) + assert spec.json["projectFilesSource"] == 1 + + def test_put_eval_set_run_payload_carries_source(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._update_eval_set_run_spec( + eval_set_run_id="set-run-1", + evaluator_scores={}, + is_coded=False, + success=True, + ) + assert spec.json["projectFilesSource"] == 1 + + def test_get_eval_runs_query_carries_source(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._get_eval_runs_spec( + eval_set_id="set-1", + eval_set_run_id="run-1", + evaluation_id=None, + is_coded=False, + ) + assert spec.params == {"projectFilesSource": 1} + + def test_unset_source_omits_field_from_payloads(self, monkeypatch): + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + reporter = self._make_reporter(monkeypatch, None) + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert "projectFilesSource" not in spec.json diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d59970eea..2eacfed8a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.60" +version = "2.10.61" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.42" +version = "0.1.43" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 67466db5672aa579f76c91426ff3d3acd4d7276c Mon Sep 17 00:00:00 2001 From: ctiliescuuipath Date: Tue, 5 May 2026 14:46:18 +0300 Subject: [PATCH 049/121] feat(platform): add W3C trace context headers to LLM Gateway requests (#1612) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/chat/_llm_gateway_service.py | 6 +- .../uipath/platform/chat/llm_trace_context.py | 42 +++++ .../src/uipath/platform/common/_config.py | 6 + .../tests/services/test_llm_trace_context.py | 161 ++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 7 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py create mode 100644 packages/uipath-platform/tests/services/test_llm_trace_context.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 989952eb3..36bc271c1 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.43" +version = "0.1.44" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py b/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py index d7c093d0d..ffe0bff99 100644 --- a/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py +++ b/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py @@ -35,6 +35,7 @@ ToolDefinition, ) from .llm_throttle import get_llm_semaphore +from .llm_trace_context import build_trace_context_headers # Common constants API_VERSION = "2024-10-21" # Standard API version for OpenAI-compatible endpoints @@ -224,7 +225,7 @@ async def embeddings( endpoint, json={"input": input}, params={"api-version": API_VERSION}, - headers=self._llm_headers, + headers={**self._llm_headers, **build_trace_context_headers()}, ) return TextEmbedding.model_validate(response.json()) @@ -355,7 +356,7 @@ class Country(BaseModel): endpoint, json=request_body, params={"api-version": API_VERSION}, - headers=self._llm_headers, + headers={**self._llm_headers, **build_trace_context_headers()}, ) return ChatCompletion.model_validate(response.json()) @@ -599,6 +600,7 @@ class Country(BaseModel): headers = { **self._llm_headers, + **build_trace_context_headers(), "X-UiPath-LlmGateway-NormalizedApi-ModelName": model, "X-UiPath-LLMGateway-AllowFull4xxResponse": "true", } diff --git a/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py b/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py new file mode 100644 index 000000000..4c6dd6062 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py @@ -0,0 +1,42 @@ +"""W3C-style trace context headers for LLM Gateway requests.""" + +from opentelemetry import trace +from uipath.core.feature_flags import FeatureFlags + +from ..common._config import UiPathConfig + + +def build_trace_context_headers( + extra_baggage: list[str] | None = None, +) -> dict[str, str]: + """Build W3C-style trace context headers from the current OpenTelemetry span. + + Args: + extra_baggage: Additional baggage entries (e.g. ``["source=agents"]``) + that callers can inject alongside the platform-level entries. + + Returns an empty dict when the ``EnableTraceContextHeaders`` feature flag + is not enabled, or when no active span is present. + """ + if not FeatureFlags.is_flag_enabled("EnableTraceContextHeaders"): + return {} + + headers: dict[str, str] = {} + span = trace.get_current_span() + ctx = span.get_span_context() + if ctx and ctx.trace_id and ctx.span_id: + trace_id = format(ctx.trace_id, "032x") + span_id = format(ctx.span_id, "016x") + headers["x-uipath-traceparent-id"] = f"00-{trace_id}-{span_id}" + + baggage_parts: list[str] = list(extra_baggage) if extra_baggage else [] + if folder_key := UiPathConfig.folder_key: + baggage_parts.append(f"folderKey={folder_key}") + if agent_id := UiPathConfig.process_uuid: + baggage_parts.append(f"agentId={agent_id}") + if process_key := UiPathConfig.process_key: + baggage_parts.append(f"processKey={process_key}") + if baggage_parts: + headers["x-uipath-tracebaggage"] = ",".join(baggage_parts) + + return headers diff --git a/packages/uipath-platform/src/uipath/platform/common/_config.py b/packages/uipath-platform/src/uipath/platform/common/_config.py index cdb5eb383..40db82214 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -121,6 +121,12 @@ def folder_path(self) -> str | None: return os.getenv(ENV_FOLDER_PATH, None) + @property + def process_key(self) -> str | None: + from uipath.platform.common.constants import ENV_PROCESS_KEY + + return os.getenv(ENV_PROCESS_KEY, None) + @property def process_uuid(self) -> str | None: from uipath.platform.common.constants import ENV_UIPATH_PROCESS_UUID diff --git a/packages/uipath-platform/tests/services/test_llm_trace_context.py b/packages/uipath-platform/tests/services/test_llm_trace_context.py new file mode 100644 index 000000000..83bde3957 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_llm_trace_context.py @@ -0,0 +1,161 @@ +"""Tests for build_trace_context_headers.""" + +import os +from unittest.mock import patch + +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags +from uipath.core.feature_flags import FeatureFlags + +from uipath.platform.chat.llm_trace_context import build_trace_context_headers + +FEATURE_FLAG = "EnableTraceContextHeaders" + + +class TestFeatureFlagDisabled: + """When the feature flag is off, no headers are returned.""" + + def setup_method(self) -> None: + FeatureFlags.reset_flags() + + def test_returns_empty_dict_by_default(self) -> None: + assert build_trace_context_headers() == {} + + def test_returns_empty_dict_when_explicitly_disabled(self) -> None: + FeatureFlags.configure_flags({FEATURE_FLAG: False}) + assert build_trace_context_headers() == {} + + +class TestTraceparentHeader: + """When enabled, x-uipath-traceparent-id is populated from the active span.""" + + def setup_method(self) -> None: + FeatureFlags.reset_flags() + FeatureFlags.configure_flags({FEATURE_FLAG: True}) + + def test_traceparent_from_active_span(self) -> None: + provider = TracerProvider() + tracer = provider.get_tracer("test") + with tracer.start_as_current_span("test-span") as span: + ctx = span.get_span_context() + expected_trace_id = format(ctx.trace_id, "032x") + expected_span_id = format(ctx.span_id, "016x") + + headers = build_trace_context_headers() + + assert "x-uipath-traceparent-id" in headers + value = headers["x-uipath-traceparent-id"] + assert value == f"00-{expected_trace_id}-{expected_span_id}" + # Verify format: version (2) + dash + trace_id (32) + dash + span_id (16) + parts = value.split("-") + assert len(parts) == 3 + assert parts[0] == "00" + assert len(parts[1]) == 32 + assert len(parts[2]) == 16 + + def test_no_traceparent_without_active_span(self) -> None: + # INVALID_SPAN has trace_id=0 and span_id=0 + from opentelemetry.context import attach, detach + + ctx = SpanContext( + trace_id=0, + span_id=0, + is_remote=False, + trace_flags=TraceFlags(0), + ) + non_recording = NonRecordingSpan(ctx) + token = attach(trace.set_span_in_context(non_recording)) + try: + headers = build_trace_context_headers() + finally: + detach(token) + + assert "x-uipath-traceparent-id" not in headers + + +class TestBaggageHeader: + """When enabled, x-uipath-tracebaggage is populated from UiPathConfig.""" + + def setup_method(self) -> None: + FeatureFlags.reset_flags() + FeatureFlags.configure_flags({FEATURE_FLAG: True}) + + def test_all_env_vars_present(self) -> None: + env = { + "UIPATH_FOLDER_KEY": "folder-abc", + "UIPATH_PROCESS_UUID": "agent-123", + "UIPATH_PROCESS_KEY": "process-789", + } + with patch.dict(os.environ, env, clear=True): + headers = build_trace_context_headers() + + baggage = headers["x-uipath-tracebaggage"] + assert "folderKey=folder-abc" in baggage + assert "agentId=agent-123" in baggage + assert "processKey=process-789" in baggage + + def test_partial_env_vars(self) -> None: + env = {"UIPATH_FOLDER_KEY": "folder-only"} + with patch.dict(os.environ, env, clear=True): + headers = build_trace_context_headers() + + baggage = headers["x-uipath-tracebaggage"] + assert "folderKey=folder-only" in baggage + + def test_no_baggage_without_env_vars(self) -> None: + with patch.dict(os.environ, {}, clear=True): + headers = build_trace_context_headers() + + assert "x-uipath-tracebaggage" not in headers + + def test_baggage_comma_separated(self) -> None: + env = { + "UIPATH_FOLDER_KEY": "f1", + "UIPATH_PROCESS_UUID": "a1", + } + with patch.dict(os.environ, env, clear=True): + headers = build_trace_context_headers() + + baggage = headers["x-uipath-tracebaggage"] + parts = baggage.split(",") + assert len(parts) == 2 # folderKey + agentId + + def test_extra_baggage_included(self) -> None: + env = {"UIPATH_FOLDER_KEY": "f1"} + with patch.dict(os.environ, env, clear=True): + headers = build_trace_context_headers(extra_baggage=["source=agents"]) + + baggage = headers["x-uipath-tracebaggage"] + assert "source=agents" in baggage + assert "folderKey=f1" in baggage + + def test_extra_baggage_only(self) -> None: + with patch.dict(os.environ, {}, clear=True): + headers = build_trace_context_headers( + extra_baggage=["source=agents", "custom=value"] + ) + + baggage = headers["x-uipath-tracebaggage"] + assert baggage == "source=agents,custom=value" + + +class TestBothHeaders: + """When enabled with an active span and env vars, both headers are present.""" + + def setup_method(self) -> None: + FeatureFlags.reset_flags() + FeatureFlags.configure_flags({FEATURE_FLAG: True}) + + def test_both_headers_present(self) -> None: + provider = TracerProvider() + tracer = provider.get_tracer("test") + env = {"UIPATH_FOLDER_KEY": "folder-abc"} + with ( + tracer.start_as_current_span("test-span"), + patch.dict(os.environ, env, clear=True), + ): + headers = build_trace_context_headers() + + assert "x-uipath-traceparent-id" in headers + assert "x-uipath-tracebaggage" in headers diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 66c8eb62a..435bc613b 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.43" +version = "0.1.44" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 2eacfed8a..dc6b7286a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.43" +version = "0.1.44" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From de3b91c4e3b3ea811bcec9e9bfc1bedd824c9ade Mon Sep 17 00:00:00 2001 From: ctiliescuuipath Date: Tue, 5 May 2026 16:36:53 +0300 Subject: [PATCH 050/121] fix(platform): use LLMOps external span in trace context headers (#1613) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/chat/llm_trace_context.py | 4 +++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 36bc271c1..f9e9d14a0 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.44" +version = "0.1.45" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py b/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py index 4c6dd6062..6efbf08dc 100644 --- a/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py +++ b/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py @@ -2,6 +2,7 @@ from opentelemetry import trace from uipath.core.feature_flags import FeatureFlags +from uipath.core.tracing.span_utils import UiPathSpanUtils from ..common._config import UiPathConfig @@ -22,7 +23,8 @@ def build_trace_context_headers( return {} headers: dict[str, str] = {} - span = trace.get_current_span() + llmops_span = UiPathSpanUtils.get_external_current_span() + span = llmops_span or trace.get_current_span() ctx = span.get_span_context() if ctx and ctx.trace_id and ctx.span_id: trace_id = format(ctx.trace_id, "032x") diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 435bc613b..ae35f75a9 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.44" +version = "0.1.45" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index b48c898b5..f3012976e 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.43, <0.2.0", + "uipath-platform>=0.1.45, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index dc6b7286a..6cbe534e9 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.44" +version = "0.1.45" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 91362d54d063122e0d6dbeabcec9d190152e1460 Mon Sep 17 00:00:00 2001 From: Andrei Tava Date: Wed, 6 May 2026 12:55:21 +0300 Subject: [PATCH 051/121] fix: pass job key to is activities for licensing (#1614) --- packages/uipath-platform/pyproject.toml | 2 +- .../connections/_connections_service.py | 15 ++++- .../services/test_connections_service.py | 59 ++++++++++++++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index f9e9d14a0..99c6d16ab 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.45" +version = "0.1.46" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/connections/_connections_service.py b/packages/uipath-platform/src/uipath/platform/connections/_connections_service.py index af3bb4f78..8ff9a82e7 100644 --- a/packages/uipath-platform/src/uipath/platform/connections/_connections_service.py +++ b/packages/uipath-platform/src/uipath/platform/connections/_connections_service.py @@ -8,7 +8,7 @@ from ..common._base_service import BaseService from ..common._bindings import resource_override -from ..common._config import UiPathApiConfig +from ..common._config import UiPathApiConfig, UiPathConfig from ..common._execution_context import UiPathExecutionContext from ..common._folder_context import header_folder from ..common._models import Endpoint, RequestSpec @@ -24,6 +24,13 @@ logger: logging.Logger = logging.getLogger("uipath") +HEADER_ORIGINATOR = "x-uipath-originator" +HEADER_SOURCE = "x-uipath-source" +# Sent on outbound Integration Service activity invocations so GenAI activities +# can be stitched back to the parent job for licensing attribution. +HEADER_ACTIVITY_JOB_ID = "x-uipath-job-id" +_ORIGINATOR_VALUE = "uipath-python" + class ConnectionsService(BaseService): """Service for managing UiPath external service connections. @@ -768,11 +775,13 @@ def _build_activity_request_spec( # header parameter handling headers = { - "x-uipath-originator": "uipath-python", - "x-uipath-source": "uipath-python", + HEADER_ORIGINATOR: _ORIGINATOR_VALUE, + HEADER_SOURCE: _ORIGINATOR_VALUE, **header_folder(folder_key, None), **header_params, } + if job_key := UiPathConfig.job_key: + headers[HEADER_ACTIVITY_JOB_ID] = job_key # body and files handling json_data: Dict[str, Any] | None = None diff --git a/packages/uipath-platform/tests/services/test_connections_service.py b/packages/uipath-platform/tests/services/test_connections_service.py index 75c89b517..d037c9372 100644 --- a/packages/uipath-platform/tests/services/test_connections_service.py +++ b/packages/uipath-platform/tests/services/test_connections_service.py @@ -17,7 +17,10 @@ ConnectionToken, EventArguments, ) -from uipath.platform.connections._connections_service import ConnectionsService +from uipath.platform.connections._connections_service import ( + HEADER_ACTIVITY_JOB_ID, + ConnectionsService, +) from uipath.platform.orchestrator._folder_service import FolderService @@ -1421,6 +1424,60 @@ def test_invoke_activity_sets_standard_headers( assert sent_request.headers["x-uipath-originator"] == "uipath-python" assert sent_request.headers["x-uipath-source"] == "uipath-python" + def test_invoke_activity_propagates_job_id_header( + self, + httpx_mock: HTTPXMock, + service: ConnectionsService, + simple_activity_metadata: ActivityMetadata, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Activity invocations carry x-uipath-job-id so GenAI calls can be stitched for licensing.""" + monkeypatch.setenv("UIPATH_JOB_KEY", "job-key-abc") + connection_id = "test-connection-123" + + httpx_mock.add_response( + method="GET", + status_code=200, + json={"id": connection_id, "name": "Test", "elementInstanceId": 1}, + ) + httpx_mock.add_response(method="POST", status_code=200, json={}) + + service.invoke_activity( + activity_metadata=simple_activity_metadata, + connection_id=connection_id, + activity_input={"body_field1": "x"}, + ) + + sent_request = httpx_mock.get_requests()[1] + assert sent_request.headers[HEADER_ACTIVITY_JOB_ID] == "job-key-abc" + + def test_invoke_activity_omits_job_id_header_when_unset( + self, + httpx_mock: HTTPXMock, + service: ConnectionsService, + simple_activity_metadata: ActivityMetadata, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """No job-id header is sent when UIPATH_JOB_KEY is not set.""" + monkeypatch.delenv("UIPATH_JOB_KEY", raising=False) + connection_id = "test-connection-123" + + httpx_mock.add_response( + method="GET", + status_code=200, + json={"id": connection_id, "name": "Test", "elementInstanceId": 1}, + ) + httpx_mock.add_response(method="POST", status_code=200, json={}) + + service.invoke_activity( + activity_metadata=simple_activity_metadata, + connection_id=connection_id, + activity_input={"body_field1": "x"}, + ) + + sent_request = httpx_mock.get_requests()[1] + assert HEADER_ACTIVITY_JOB_ID not in sent_request.headers + def test_invoke_activity_with_body_fields( self, httpx_mock: HTTPXMock, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index ae35f75a9..dbea2b79a 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.45" +version = "0.1.46" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 6cbe534e9..4339a51d2 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.45" +version = "0.1.46" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From a29ed86aab4e7c58005c94da084afcd365cd67d0 Mon Sep 17 00:00:00 2001 From: Dennis-UiPath <84574129+Dennis-UiPath@users.noreply.github.com> Date: Fri, 8 May 2026 15:09:42 +0200 Subject: [PATCH 052/121] fix(connections): preserve filename and content type in multipart file uploads (#1611) --- packages/uipath-platform/pyproject.toml | 2 +- .../connections/_connections_service.py | 22 ++- .../services/test_connections_service.py | 170 ++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 2 +- 6 files changed, 190 insertions(+), 10 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 99c6d16ab..342b22d83 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.46" +version = "0.1.47" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/connections/_connections_service.py b/packages/uipath-platform/src/uipath/platform/connections/_connections_service.py index 8ff9a82e7..b7c1e9444 100644 --- a/packages/uipath-platform/src/uipath/platform/connections/_connections_service.py +++ b/packages/uipath-platform/src/uipath/platform/connections/_connections_service.py @@ -797,12 +797,22 @@ def _build_activity_request_spec( # instead of making assumptions on whether or not it's present, we'll handle it defensively if key == json_section: continue - # files not supported yet supported so this will likely not work - files[key] = ( - key, - val, - None, - ) # probably needs to extract content type from val since IS metadata doesn't provide it + if isinstance(val, tuple): + # Caller supplied httpx's (filename, content[, content_type]) + # shape — pass through verbatim. This is the recommended path + # for file uploads so the multipart Content-Disposition gets + # the real filename instead of the form-field name. + files[key] = val + elif isinstance(val, (bytes, bytearray)) or hasattr(val, "read"): + # Raw file content with no filename — fall back to the + # form-field name (legacy behaviour). Backwards compatible + # with callers that still pass bytes directly. + files[key] = (key, val, "application/octet-stream") + else: + # Scalar (string/number/etc.) — send as a plain multipart + # form field, not a file part. The (None, value) shape tells + # httpx to omit `filename=...` from the Content-Disposition. + files[key] = (None, str(val)) files[json_section] = ( "", diff --git a/packages/uipath-platform/tests/services/test_connections_service.py b/packages/uipath-platform/tests/services/test_connections_service.py index d037c9372..27dec7310 100644 --- a/packages/uipath-platform/tests/services/test_connections_service.py +++ b/packages/uipath-platform/tests/services/test_connections_service.py @@ -2173,3 +2173,173 @@ async def test_invoke_activity_async_uses_connection_id_from_retrieve_response( assert f"/element/instances/{original_connection_id}/" not in str( activity_request.url ) + + +def _multipart_part(body: bytes, boundary: str, name: str) -> str: + """Return the raw text of the multipart part with the given form-field name.""" + text = body.decode("utf-8", errors="replace") + for part in text.split(f"--{boundary}"): + if f'name="{name}"' in part: + return part + raise AssertionError(f"part {name!r} not found in multipart body") + + +class TestMultipartFileUpload: + """Regression tests for the multipart serializer that handles file uploads. + + Before this fix, ``_build_activity_request_spec`` always built + ``files[key] = (key, val, None)``, using the form-field name as the + multipart filename and dropping the content type. Downstream services + (e.g. Coupa's ``add_attachment`` endpoint) ended up storing every + attachment with the literal name ``attachment[file]`` and no extension. + + The serializer now branches on the value type: + + * tuple → passed through (caller controls filename + content type) + * bytes → legacy fallback, key as filename, octet-stream content type + * scalar → plain multipart form field (no filename in Content-Disposition) + """ + + def test_invoke_activity_multipart_tuple_3_preserves_filename( + self, + httpx_mock: HTTPXMock, + service: ConnectionsService, + multipart_activity_metadata: ActivityMetadata, + ) -> None: + """3-tuple input is forwarded verbatim, so the real filename + content type land on the wire.""" + connection_id = "test-connection-123" + activity_input = { + "file_param": ("invoice.pdf", b"%PDF-1.4 fake", "application/pdf"), + "description": "Test file upload", + } + + httpx_mock.add_response( + method="GET", + status_code=200, + json={"id": connection_id, "name": "Test", "elementInstanceId": 1}, + ) + httpx_mock.add_response(method="POST", status_code=200, json={"ok": True}) + + _ = service.invoke_activity( + activity_metadata=multipart_activity_metadata, + connection_id=connection_id, + activity_input=activity_input, + ) + + sent_request = httpx_mock.get_requests()[1] + boundary = sent_request.headers["content-type"].split("boundary=")[1] + part = _multipart_part(sent_request.content, boundary, "file_param") + + assert 'filename="invoice.pdf"' in part + assert "Content-Type: application/pdf" in part + assert b"%PDF-1.4 fake" in sent_request.content + + def test_invoke_activity_multipart_tuple_2_preserves_filename( + self, + httpx_mock: HTTPXMock, + service: ConnectionsService, + multipart_activity_metadata: ActivityMetadata, + ) -> None: + """2-tuple (filename, content) shorthand: filename preserved, httpx infers the content type.""" + connection_id = "test-connection-123" + activity_input = { + "file_param": ("invoice.pdf", b"%PDF-1.4 fake"), + "description": "Test file upload", + } + + httpx_mock.add_response( + method="GET", + status_code=200, + json={"id": connection_id, "name": "Test", "elementInstanceId": 1}, + ) + httpx_mock.add_response(method="POST", status_code=200, json={"ok": True}) + + _ = service.invoke_activity( + activity_metadata=multipart_activity_metadata, + connection_id=connection_id, + activity_input=activity_input, + ) + + sent_request = httpx_mock.get_requests()[1] + boundary = sent_request.headers["content-type"].split("boundary=")[1] + part = _multipart_part(sent_request.content, boundary, "file_param") + + assert 'filename="invoice.pdf"' in part + assert b"%PDF-1.4 fake" in sent_request.content + + def test_invoke_activity_multipart_bytes_backwards_compatible( + self, + httpx_mock: HTTPXMock, + service: ConnectionsService, + multipart_activity_metadata: ActivityMetadata, + ) -> None: + """Existing callers passing raw bytes keep working — filename = form-field name (legacy).""" + connection_id = "test-connection-123" + activity_input = { + "file_param": b"raw bytes", + "description": "Test", + } + + httpx_mock.add_response( + method="GET", + status_code=200, + json={"id": connection_id, "name": "Test", "elementInstanceId": 1}, + ) + httpx_mock.add_response(method="POST", status_code=200, json={"ok": True}) + + _ = service.invoke_activity( + activity_metadata=multipart_activity_metadata, + connection_id=connection_id, + activity_input=activity_input, + ) + + sent_request = httpx_mock.get_requests()[1] + boundary = sent_request.headers["content-type"].split("boundary=")[1] + part = _multipart_part(sent_request.content, boundary, "file_param") + + # Legacy fallback: form-field name used as filename, octet-stream content type. + assert 'filename="file_param"' in part + assert "Content-Type: application/octet-stream" in part + assert b"raw bytes" in sent_request.content + + def test_invoke_activity_multipart_scalar_is_plain_form_field( + self, + httpx_mock: HTTPXMock, + service: ConnectionsService, + ) -> None: + """Scalar multipart_params get sent as plain form fields (no bogus filename).""" + metadata = ActivityMetadata( + object_path="/elements/test-connector/upload", + method_name="POST", + content_type="multipart/form-data", + parameter_location_info=ActivityParameterLocationInfo( + multipart_params=["file_param", "payload"], + body_fields=[], + ), + ) + connection_id = "test-connection-123" + activity_input = { + "file_param": ("doc.pdf", b"data", "application/pdf"), + "payload": "{}", + } + + httpx_mock.add_response( + method="GET", + status_code=200, + json={"id": connection_id, "name": "Test", "elementInstanceId": 1}, + ) + httpx_mock.add_response(method="POST", status_code=200, json={"ok": True}) + + _ = service.invoke_activity( + activity_metadata=metadata, + connection_id=connection_id, + activity_input=activity_input, + ) + + sent_request = httpx_mock.get_requests()[1] + boundary = sent_request.headers["content-type"].split("boundary=")[1] + payload_part = _multipart_part(sent_request.content, boundary, "payload") + + # Scalar payload must NOT carry a filename in Content-Disposition. + assert "filename=" not in payload_part + assert "{}" in payload_part diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index dbea2b79a..f8fa7aced 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.46" +version = "0.1.47" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index f3012976e..ef9f3efb7 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.45, <0.2.0", + "uipath-platform>=0.1.47, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 4339a51d2..e12347835 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.46" +version = "0.1.47" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From ed18224e1644fd9771f9692918a2db976eed57c3 Mon Sep 17 00:00:00 2001 From: Jeremy Reist <106174102+JeremyReist2@users.noreply.github.com> Date: Fri, 8 May 2026 11:36:03 -0400 Subject: [PATCH 053/121] fix: remove SearchMode.AUTO from context grounding (#1619) --- packages/uipath-platform/pyproject.toml | 2 +- .../context_grounding/_context_grounding_service.py | 10 +++++----- .../platform/context_grounding/context_grounding.py | 1 - .../tests/services/test_context_grounding_service.py | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_cli/services/cli_context_grounding.py | 6 +++--- packages/uipath/src/uipath/_resources/CLI_REFERENCE.md | 2 +- packages/uipath/src/uipath/_resources/SDK_REFERENCE.md | 4 ++-- packages/uipath/uv.lock | 4 ++-- 10 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 342b22d83..1220b386a 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.47" +version = "0.1.48" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py index 1c2eabd85..2e6e40628 100644 --- a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py +++ b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py @@ -1556,7 +1556,7 @@ def unified_search( self, name: str, query: str, - search_mode: SearchMode = SearchMode.AUTO, + search_mode: SearchMode = SearchMode.SEMANTIC, number_of_results: int = 10, threshold: float = 0.0, scope: Optional[UnifiedSearchScope] = None, @@ -1572,7 +1572,7 @@ def unified_search( Args: name (str): The name of the context index to search in. query (str): The search query in natural language. - search_mode (SearchMode): The search mode to use. Defaults to AUTO. + search_mode (SearchMode): The search mode to use. Defaults to SEMANTIC. number_of_results (int): Maximum number of results to return. Defaults to 10. threshold (float): Minimum similarity threshold. Defaults to 0.0. scope (Optional[UnifiedSearchScope]): Optional search scope (folder, extension). @@ -1620,7 +1620,7 @@ async def unified_search_async( self, name: str, query: str, - search_mode: SearchMode = SearchMode.AUTO, + search_mode: SearchMode = SearchMode.SEMANTIC, number_of_results: int = 10, threshold: float = 0.0, scope: Optional[UnifiedSearchScope] = None, @@ -1636,7 +1636,7 @@ async def unified_search_async( Args: name (str): The name of the context index to search in. query (str): The search query in natural language. - search_mode (SearchMode): The search mode to use. Defaults to AUTO. + search_mode (SearchMode): The search mode to use. Defaults to SEMANTIC. number_of_results (int): Maximum number of results to return. Defaults to 10. threshold (float): Minimum similarity threshold. Defaults to 0.0. scope (Optional[UnifiedSearchScope]): Optional search scope (folder, extension). @@ -2306,7 +2306,7 @@ def _unified_search_spec( self, index_id: str, query: str, - search_mode: SearchMode = SearchMode.AUTO, + search_mode: SearchMode = SearchMode.SEMANTIC, number_of_results: int = 10, threshold: float = 0.0, scope: Optional[UnifiedSearchScope] = None, diff --git a/packages/uipath-platform/src/uipath/platform/context_grounding/context_grounding.py b/packages/uipath-platform/src/uipath/platform/context_grounding/context_grounding.py index fdcf5eac9..c5c0915bb 100644 --- a/packages/uipath-platform/src/uipath/platform/context_grounding/context_grounding.py +++ b/packages/uipath-platform/src/uipath/platform/context_grounding/context_grounding.py @@ -239,7 +239,6 @@ class ContextGroundingSearchResultItem(BaseModel): class SearchMode(str, Enum): """Enum representing possible unified search modes.""" - AUTO = "Auto" SEMANTIC = "Semantic" diff --git a/packages/uipath-platform/tests/services/test_context_grounding_service.py b/packages/uipath-platform/tests/services/test_context_grounding_service.py index 3fef9f47a..ace7affcd 100644 --- a/packages/uipath-platform/tests/services/test_context_grounding_service.py +++ b/packages/uipath-platform/tests/services/test_context_grounding_service.py @@ -3700,7 +3700,7 @@ async def test_unified_search_async( response = await service.unified_search_async( name="test-index", query="test query", - search_mode=SearchMode.AUTO, + search_mode=SearchMode.SEMANTIC, ) assert isinstance(response, UnifiedQueryResult) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index f8fa7aced..1e7878b10 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.47" +version = "0.1.48" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index ef9f3efb7..cc074cad1 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.61" +version = "2.10.62" 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/services/cli_context_grounding.py b/packages/uipath/src/uipath/_cli/services/cli_context_grounding.py index 075ffe869..1e25def5e 100644 --- a/packages/uipath/src/uipath/_cli/services/cli_context_grounding.py +++ b/packages/uipath/src/uipath/_cli/services/cli_context_grounding.py @@ -460,9 +460,9 @@ def ingest_index( ) @click.option( "--search-mode", - type=click.Choice(["Auto", "Semantic"]), - default="Auto", - help="Search mode (default: Auto)", + type=click.Choice(["Semantic"]), + default="Semantic", + help="Search mode (default: Semantic)", ) @common_service_options @service_command diff --git a/packages/uipath/src/uipath/_resources/CLI_REFERENCE.md b/packages/uipath/src/uipath/_resources/CLI_REFERENCE.md index 98524ddfd..ff4009b22 100644 --- a/packages/uipath/src/uipath/_resources/CLI_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/CLI_REFERENCE.md @@ -923,7 +923,7 @@ Options: - `--query`: Search query in natural language (default: `Sentinel.UNSET`) - `--limit`: Maximum number of results (default: 10) (default: `10`) - `--threshold`: Minimum similarity threshold (default: 0.0) (default: `0.0`) -- `--search-mode`: Search mode (default: Auto) (default: `Auto`) +- `--search-mode`: Search mode (default: Semantic) (default: `Semantic`) - `--folder-path`: Folder path (e.g., "Shared"). Can also be set via UIPATH_FOLDER_PATH environment variable. (default: `Sentinel.UNSET`) - `--folder-key`: Folder key (UUID) (default: `Sentinel.UNSET`) - `--format`: Output format (overrides global) (default: `Sentinel.UNSET`) diff --git a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md index 2e6a4a585..d92dcfded 100644 --- a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md @@ -386,10 +386,10 @@ sdk.context_grounding.start_deep_rag_ephemeral(name: str, prompt: Annotated[str, sdk.context_grounding.start_deep_rag_ephemeral_async(name: str, prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], glob_pattern: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='*', metadata=[MaxLen(max_length=512)])]="**", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse # Perform a unified search on a context grounding index. -sdk.context_grounding.unified_search(name: str, query: str, search_mode: uipath.platform.context_grounding.context_grounding.UnifiedQueryResult +sdk.context_grounding.unified_search(name: str, query: str, search_mode: uipath.platform.context_grounding.context_grounding.UnifiedQueryResult # Asynchronously perform a unified search on a context grounding index. -sdk.context_grounding.unified_search_async(name: str, query: str, search_mode: uipath.platform.context_grounding.context_grounding.UnifiedQueryResult +sdk.context_grounding.unified_search_async(name: str, query: str, search_mode: uipath.platform.context_grounding.context_grounding.UnifiedQueryResult ``` diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e12347835..3d4c916d3 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.61" +version = "2.10.62" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.47" +version = "0.1.48" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 03191c4f0a5cb0e68aec80ea6165d64020789623 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Mon, 11 May 2026 14:05:47 +0200 Subject: [PATCH 054/121] ci: add coverage report on PRs (#1618) --- .github/workflows/ci.yml | 6 ++ .github/workflows/test-packages.yml | 115 +++++++++++++++++++++++- packages/uipath-core/pyproject.toml | 23 ++++- packages/uipath-platform/pyproject.toml | 23 ++++- packages/uipath/pyproject.toml | 23 ++++- sonar-project.properties | 13 +++ 6 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 sonar-project.properties diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e05e9c73..8be2b6b53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: pull_request: branches: - main + push: + branches: + - main permissions: contents: read @@ -18,6 +21,9 @@ jobs: test: uses: ./.github/workflows/test-packages.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} check-versions: + if: github.event_name == 'pull_request' uses: ./.github/workflows/check-version-availability.yml diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml index dab4b8d7c..8ee6b5120 100644 --- a/.github/workflows/test-packages.yml +++ b/.github/workflows/test-packages.yml @@ -2,6 +2,9 @@ name: Test Packages on: workflow_call: + secrets: + SONAR_TOKEN: + required: false permissions: contents: read @@ -77,10 +80,31 @@ jobs: run: uv sync --all-extras --python ${{ matrix.python-version }} - name: Run tests - if: steps.check.outputs.skip != 'true' + if: steps.check.outputs.skip != 'true' && !(matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13') working-directory: packages/uipath-core run: uv run pytest + - name: Run tests with coverage + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' + working-directory: packages/uipath-core + run: uv run pytest --cov-report=xml --cov-report=html --tb=short + + - name: Upload coverage HTML report + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && always() + uses: actions/upload-artifact@v4 + with: + name: coverage-html-uipath-core + path: packages/uipath-core/htmlcov/ + retention-days: 30 + + - name: Upload coverage XML report + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && always() + uses: actions/upload-artifact@v4 + with: + name: coverage-xml-uipath-core + path: packages/uipath-core/coverage.xml + retention-days: 30 + test-uipath-platform: name: Test (uipath-platform, ${{ matrix.python-version }}, ${{ matrix.os }}) needs: detect-changed-packages @@ -126,10 +150,31 @@ jobs: run: uv sync --all-extras --python ${{ matrix.python-version }} - name: Run tests - if: steps.check.outputs.skip != 'true' + if: steps.check.outputs.skip != 'true' && !(matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13') working-directory: packages/uipath-platform run: uv run pytest + - name: Run tests with coverage + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' + working-directory: packages/uipath-platform + run: uv run pytest --cov-report=xml --cov-report=html --tb=short + + - name: Upload coverage HTML report + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && always() + uses: actions/upload-artifact@v4 + with: + name: coverage-html-uipath-platform + path: packages/uipath-platform/htmlcov/ + retention-days: 30 + + - name: Upload coverage XML report + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && always() + uses: actions/upload-artifact@v4 + with: + name: coverage-xml-uipath-platform + path: packages/uipath-platform/coverage.xml + retention-days: 30 + e2e-uipath-platform: name: E2E (uipath-platform, memory) needs: detect-changed-packages @@ -225,12 +270,76 @@ jobs: run: uv sync --all-extras --python ${{ matrix.python-version }} - name: Run tests - if: steps.check.outputs.skip != 'true' + if: steps.check.outputs.skip != 'true' && !(matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13') working-directory: packages/uipath run: uv run pytest + - name: Run tests with coverage + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' + working-directory: packages/uipath + run: uv run pytest --cov-report=xml --cov-report=html --tb=short + + - name: Upload coverage HTML report + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && always() + uses: actions/upload-artifact@v4 + with: + name: coverage-html-uipath + path: packages/uipath/htmlcov/ + retention-days: 30 + + - name: Upload coverage XML report + if: steps.check.outputs.skip != 'true' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' && always() + uses: actions/upload-artifact@v4 + with: + name: coverage-xml-uipath + path: packages/uipath/coverage.xml + retention-days: 30 + continue-on-error: true + sonarcloud: + name: SonarCloud + needs: [test-uipath-core, test-uipath-platform, test-uipath] + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && always() && needs.test-uipath-core.result != 'failure' && needs.test-uipath-platform.result != 'failure' && needs.test-uipath.result != 'failure' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download uipath-core coverage + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: coverage-xml-uipath-core + path: packages/uipath-core + + - name: Download uipath-platform coverage + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: coverage-xml-uipath-platform + path: packages/uipath-platform + + - name: Download uipath coverage + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: coverage-xml-uipath + path: packages/uipath + + - name: Rewrite coverage XML to repo-relative paths + run: | + sed -i 's|src|packages/uipath-core/src|g' packages/uipath-core/coverage.xml || true + sed -i 's|src|packages/uipath-platform/src|g' packages/uipath-platform/coverage.xml || true + sed -i 's|src|packages/uipath/src|g' packages/uipath/coverage.xml || true + + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@2f77a1ec69fb1d595b06f35ab27e97605bdef703 # v5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + test-gate: name: Test needs: [test-uipath-core, test-uipath-platform, test-uipath, e2e-uipath-platform] diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index cb188084e..8a001489b 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -95,15 +95,30 @@ warn_required_dynamic_aliases = true [tool.pytest.ini_options] testpaths = ["tests"] python_files = "test_*.py" -addopts = "-ra -q --cov=src/uipath --cov-report=term-missing" +addopts = "-ra -q --cov=src --cov-report=term-missing" asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" -[tool.coverage.report] -show_missing = true - [tool.coverage.run] source = ["src"] +relative_files = true +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/site-packages/*", + "*/conftest.py", +] + +[tool.coverage.report] +show_missing = true +precision = 2 +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "@(abc\\.)?abstractmethod", +] [[tool.uv.index]] name = "testpypi" diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 1220b386a..80546ec3a 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -98,18 +98,33 @@ warn_required_dynamic_aliases = true [tool.pytest.ini_options] testpaths = ["tests"] python_files = "test_*.py" -addopts = "-ra -q --cov=src/uipath --cov-report=term-missing -m 'not e2e'" +addopts = "-ra -q --cov=src --cov-report=term-missing -m 'not e2e'" asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" markers = [ "e2e: end-to-end tests against real ECS/LLMOps (requires UIPATH_URL, UIPATH_ACCESS_TOKEN, UIPATH_FOLDER_KEY)", ] -[tool.coverage.report] -show_missing = true - [tool.coverage.run] source = ["src"] +relative_files = true +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/site-packages/*", + "*/conftest.py", +] + +[tool.coverage.report] +show_missing = true +precision = 2 +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "@(abc\\.)?abstractmethod", +] [tool.uv.sources] uipath-core = { path = "../uipath-core", editable = true } diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index cc074cad1..3302e528e 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -137,15 +137,30 @@ warn_required_dynamic_aliases = true [tool.pytest.ini_options] testpaths = ["tests"] python_files = "test_*.py" -addopts = "-ra -q --cov" +addopts = "-ra -q --cov=src --cov-report=term-missing" asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" -[tool.coverage.report] -show_missing = true - [tool.coverage.run] source = ["src"] +relative_files = true +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/site-packages/*", + "*/conftest.py", +] + +[tool.coverage.report] +show_missing = true +precision = 2 +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "@(abc\\.)?abstractmethod", +] [tool.uv.sources] uipath-core = { path = "../uipath-core", editable = true } diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..939a3a112 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,13 @@ +sonar.projectKey=UiPath_uipath-python +sonar.organization=ui +sonar.host.url=https://sonarcloud.io + +sonar.sources=packages/uipath/src,packages/uipath-core/src,packages/uipath-platform/src +sonar.tests=packages/uipath/tests,packages/uipath-core/tests,packages/uipath-platform/tests + +sonar.python.version=3.11,3.12,3.13 +sonar.python.coverage.reportPaths=packages/uipath/coverage.xml,packages/uipath-core/coverage.xml,packages/uipath-platform/coverage.xml + +sonar.exclusions=**/samples/**,**/testcases/**,**/template/**,**/_resources/** + +sonar.sourceEncoding=UTF-8 From 3a4f4ad2ea3e53b9956d16a81bb683e7dc36c53b Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Mon, 11 May 2026 14:14:10 +0200 Subject: [PATCH 055/121] fix(ci): run sonarcloud scan on push to main too (#1622) --- .github/workflows/test-packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml index 8ee6b5120..5bc300c2f 100644 --- a/.github/workflows/test-packages.yml +++ b/.github/workflows/test-packages.yml @@ -301,7 +301,7 @@ jobs: name: SonarCloud needs: [test-uipath-core, test-uipath-platform, test-uipath] runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && always() && needs.test-uipath-core.result != 'failure' && needs.test-uipath-platform.result != 'failure' && needs.test-uipath.result != 'failure' + if: always() && needs.test-uipath-core.result != 'failure' && needs.test-uipath-platform.result != 'failure' && needs.test-uipath.result != 'failure' steps: - name: Checkout uses: actions/checkout@v4 From c5c4bbd37232ea688eec80d4d8f84746864acc61 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Mon, 11 May 2026 14:38:04 +0200 Subject: [PATCH 056/121] ci: scope sonarcloud job permissions to contents:read (#1623) --- .github/workflows/test-packages.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml index 5bc300c2f..f35d77e8f 100644 --- a/.github/workflows/test-packages.yml +++ b/.github/workflows/test-packages.yml @@ -302,6 +302,8 @@ jobs: needs: [test-uipath-core, test-uipath-platform, test-uipath] runs-on: ubuntu-latest if: always() && needs.test-uipath-core.result != 'failure' && needs.test-uipath-platform.result != 'failure' && needs.test-uipath.result != 'failure' + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v4 From dc5319f25c52581a79b2f9512b8915627c634509 Mon Sep 17 00:00:00 2001 From: AAgnihotry <95259907+AAgnihotry@users.noreply.github.com> Date: Tue, 12 May 2026 14:14:12 -0700 Subject: [PATCH 057/121] feat(eval): add runtime simulation via --simulation flag (#1624) Co-authored-by: Claude Sonnet 4.6 --- packages/uipath/pyproject.toml | 2 +- .../runtime-simulations-agent/input.json | 4 + .../samples/runtime-simulations-agent/main.py | 186 ++++++++++++++++++ .../runtime-simulations-agent/pyproject.toml | 14 ++ .../runtime-simulations-agent/simulation.json | 15 ++ .../runtime-simulations-agent/uipath.json | 5 + packages/uipath/src/uipath/_cli/cli_run.py | 41 +++- .../uipath/src/uipath/eval/mocks/__init__.py | 15 +- .../src/uipath/eval/mocks/_mock_runtime.py | 105 ++++++---- .../uipath/src/uipath/eval/mocks/_types.py | 15 ++ .../simulation-testcase/pyproject.toml | 12 ++ .../testcases/simulation-testcase/run.sh | 29 +++ .../simulation-testcase/src/assert.py | 57 ++++++ .../uipath/tests/cli/eval/mocks/test_mocks.py | 127 ++++++++++++ packages/uipath/tests/cli/test_run.py | 111 +++++++++++ packages/uipath/uv.lock | 2 +- 16 files changed, 689 insertions(+), 51 deletions(-) create mode 100644 packages/uipath/samples/runtime-simulations-agent/input.json create mode 100644 packages/uipath/samples/runtime-simulations-agent/main.py create mode 100644 packages/uipath/samples/runtime-simulations-agent/pyproject.toml create mode 100644 packages/uipath/samples/runtime-simulations-agent/simulation.json create mode 100644 packages/uipath/samples/runtime-simulations-agent/uipath.json create mode 100644 packages/uipath/testcases/simulation-testcase/pyproject.toml create mode 100644 packages/uipath/testcases/simulation-testcase/run.sh create mode 100644 packages/uipath/testcases/simulation-testcase/src/assert.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 3302e528e..aba6ae877 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.62" +version = "2.10.63" 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/samples/runtime-simulations-agent/input.json b/packages/uipath/samples/runtime-simulations-agent/input.json new file mode 100644 index 000000000..9bfb2eef8 --- /dev/null +++ b/packages/uipath/samples/runtime-simulations-agent/input.json @@ -0,0 +1,4 @@ +{ + "code": "def add(a, b):\n return a+b\n\ndef divide(a,b):\n return a/b", + "language": "python" +} diff --git a/packages/uipath/samples/runtime-simulations-agent/main.py b/packages/uipath/samples/runtime-simulations-agent/main.py new file mode 100644 index 000000000..46440b459 --- /dev/null +++ b/packages/uipath/samples/runtime-simulations-agent/main.py @@ -0,0 +1,186 @@ +"""Coding agent that reviews code and suggests improvements. + +This sample demonstrates the --simulation flag: the three tool functions +(check_syntax, check_style, suggest_improvements) are decorated with @mockable, +so they can be intercepted by an LLM during a simulated run instead of +requiring a real linter or compiler to be installed. + +Run with real tools: + uipath run main.py:main -f input.json + +Run with simulation (no real tools needed): + uipath run main.py:main -f input.json --simulation "$(cat simulation.json)" +""" + +import logging + +from pydantic import BaseModel +from pydantic.dataclasses import dataclass + +from uipath.eval.mocks import ExampleCall, mockable +from uipath.tracing import traced + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Input / Output models +# --------------------------------------------------------------------------- + + +@dataclass +class CodeReviewInput: + code: str + language: str = "python" + + +class SyntaxResult(BaseModel): + valid: bool + errors: list[str] = [] + + +class StyleResult(BaseModel): + score: int # 0-100 + violations: list[str] = [] + + +class ImprovementResult(BaseModel): + suggestions: list[str] = [] + refactored_snippet: str = "" + + +class CodeReviewOutput(BaseModel): + syntax: SyntaxResult + style: StyleResult + improvements: ImprovementResult + summary: str + + +# --------------------------------------------------------------------------- +# Mockable tool functions +# --------------------------------------------------------------------------- + +CHECK_SYNTAX_EXAMPLES = [ + ExampleCall( + id="valid-python", + input='{"code": "def hello():\\n return 42", "language": "python"}', + output='{"valid": true, "errors": []}', + ), + ExampleCall( + id="syntax-error", + input='{"code": "def hello(\\n return 42", "language": "python"}', + output='{"valid": false, "errors": ["SyntaxError: unexpected EOF"]}', + ), +] + + +@traced(name="check_syntax", span_type="tool") +@mockable(example_calls=CHECK_SYNTAX_EXAMPLES) +async def check_syntax(code: str, language: str = "python") -> SyntaxResult: + """Check code for syntax errors using the language's parser. + + Args: + code: Source code to check. + language: Programming language (default: python). + + Returns: + SyntaxResult with valid flag and list of error messages. + """ + if language != "python": + return SyntaxResult(valid=True, errors=[]) + + try: + compile(code, "", "exec") + return SyntaxResult(valid=True, errors=[]) + except SyntaxError as exc: + return SyntaxResult(valid=False, errors=[str(exc)]) + + +CHECK_STYLE_EXAMPLES = [ + ExampleCall( + id="clean-code", + input='{"code": "def hello():\\n return 42\\n", "language": "python"}', + output='{"score": 95, "violations": []}', + ), + ExampleCall( + id="style-issues", + input='{"code": "def hello( ):\\n return 42", "language": "python"}', + output='{"score": 60, "violations": ["E211 whitespace before \'(\'", "W291 trailing whitespace"]}', + ), +] + + +@traced(name="check_style", span_type="tool") +@mockable(example_calls=CHECK_STYLE_EXAMPLES) +async def check_style(code: str, language: str = "python") -> StyleResult: + """Run style checks (e.g. PEP 8 for Python) on the provided code. + + Args: + code: Source code to check. + language: Programming language (default: python). + + Returns: + StyleResult with a 0-100 score and list of style violations. + """ + # Real implementation would call ruff / pycodestyle / eslint etc. + # For demo purposes we return a perfect score when not simulated. + return StyleResult(score=100, violations=[]) + + +SUGGEST_IMPROVEMENTS_EXAMPLES = [ + ExampleCall( + id="basic-function", + input='{"code": "def add(a, b):\\n return a + b"}', + output=( + '{"suggestions": ["Add type annotations", "Add a docstring"],' + ' "refactored_snippet": "def add(a: int, b: int) -> int:\\n ' + "'''Return the sum of a and b.'''\\n return a + b\"}" + ), + ) +] + + +@traced(name="suggest_improvements", span_type="tool") +@mockable(example_calls=SUGGEST_IMPROVEMENTS_EXAMPLES) +async def suggest_improvements(code: str) -> ImprovementResult: + """Analyse code and return actionable improvement suggestions. + + Args: + code: Source code to analyse. + + Returns: + ImprovementResult with suggestions and an optional refactored snippet. + """ + # Real implementation would call an LLM or static analysis tool. + return ImprovementResult(suggestions=[], refactored_snippet=code) + + +# --------------------------------------------------------------------------- +# Agent entrypoint +# --------------------------------------------------------------------------- + + +@traced(name="main") +async def main(input: CodeReviewInput) -> CodeReviewOutput: + """Orchestrate three code-review tools and produce a unified report. + + Each tool call creates its own OpenTelemetry span with span_type="tool", + which enables trajectory-based evaluation and simulation. + """ + syntax = await check_syntax(input.code, input.language) + style = await check_style(input.code, input.language) + improvements = await suggest_improvements(input.code) + + issues = len(syntax.errors) + len(style.violations) + summary = ( + f"Found {issues} issue(s). " + f"Style score: {style.score}/100. " + f"{len(improvements.suggestions)} improvement suggestion(s)." + ) + + return CodeReviewOutput( + syntax=syntax, + style=style, + improvements=improvements, + summary=summary, + ) diff --git a/packages/uipath/samples/runtime-simulations-agent/pyproject.toml b/packages/uipath/samples/runtime-simulations-agent/pyproject.toml new file mode 100644 index 000000000..335c55783 --- /dev/null +++ b/packages/uipath/samples/runtime-simulations-agent/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "runtime-simulations-agent" +version = "0.0.1" +description = "Code review agent demonstrating runtime simulation" +authors = [{ name = "UiPath", email = "python-sdk@uipath.com" }] +dependencies = [ + "uipath", +] +requires-python = ">=3.11" + +[dependency-groups] +dev = [ + "uipath-dev", +] diff --git a/packages/uipath/samples/runtime-simulations-agent/simulation.json b/packages/uipath/samples/runtime-simulations-agent/simulation.json new file mode 100644 index 000000000..d89bb253f --- /dev/null +++ b/packages/uipath/samples/runtime-simulations-agent/simulation.json @@ -0,0 +1,15 @@ +{ + "enabled": true, + "toolsToSimulate": [ + { + "name": "check_syntax" + }, + { + "name": "check_style" + }, + { + "name": "suggest_improvements" + } + ], + "instructions": "You are simulating a code review system. Given a tool name and its input arguments, produce a realistic JSON response that matches the tool's output schema.\n\n- check_syntax: return {\"valid\": , \"errors\": [, ...]}. If the code looks syntactically correct return valid=true and an empty errors list. Otherwise list the syntax errors.\n- check_style: return {\"score\": <0-100>, \"violations\": [, ...]}. Evaluate PEP 8 compliance for Python code. Deduct points for missing spaces, missing type annotations, etc.\n- suggest_improvements: return {\"suggestions\": [, ...], \"refactored_snippet\": \"\"}. Suggest concrete improvements such as adding type hints, docstrings, or handling edge cases (e.g. division by zero)." +} \ No newline at end of file diff --git a/packages/uipath/samples/runtime-simulations-agent/uipath.json b/packages/uipath/samples/runtime-simulations-agent/uipath.json new file mode 100644 index 000000000..9b02c2654 --- /dev/null +++ b/packages/uipath/samples/runtime-simulations-agent/uipath.json @@ -0,0 +1,5 @@ +{ + "functions": { + "main": "main.py:main" + } +} diff --git a/packages/uipath/src/uipath/_cli/cli_run.py b/packages/uipath/src/uipath/_cli/cli_run.py index 5bfd811ed..48f42018b 100644 --- a/packages/uipath/src/uipath/_cli/cli_run.py +++ b/packages/uipath/src/uipath/_cli/cli_run.py @@ -1,12 +1,14 @@ import asyncio import click +from pydantic import ValidationError from uipath._cli._chat._bridge import get_chat_bridge from uipath._cli._debug._bridge import ConsoleDebugBridge from uipath._cli._utils._common import read_resource_overwrites_from_file from uipath._cli._utils._debug import setup_debugging from uipath.core.tracing import UiPathTraceManager +from uipath.eval.mocks import SimulationConfig, UiPathMockRuntime, build_mocking_context from uipath.platform.common import ResourceOverwritesContext, UiPathConfig from uipath.runtime import ( UiPathExecuteOptions, @@ -101,6 +103,12 @@ def get_usage_help(self) -> list[str]: is_flag=True, help="Keep the temporary state file even when not resuming and no job id is provided", ) +@click.option( + "--simulation", + required=False, + default=None, + help="Simulation config as a JSON object (same schema as simulation.json)", +) @track_command("run") def run( entrypoint: str | None, @@ -114,6 +122,7 @@ def run( debug: bool, debug_port: int, keep_state_file: bool, + simulation: str | None, ) -> None: """Execute the project.""" input_file = file or input_file @@ -122,6 +131,14 @@ def run( if not setup_debugging(debug, debug_port): console.error(f"Failed to start debug server on port {debug_port}") + simulation_config: SimulationConfig | None = None + if simulation: + try: + simulation_config = SimulationConfig.model_validate_json(simulation) + except (ValidationError, ValueError) as e: + console.error(f"Invalid --simulation config: {e}") + return + result = Middlewares.next( "run", entrypoint, @@ -193,6 +210,7 @@ async def execute() -> None: lambda: read_resource_overwrites_from_file(ctx.runtime_dir) ): with ctx: + base_runtime: UiPathRuntimeProtocol | None = None runtime: UiPathRuntimeProtocol | None = None chat_runtime: UiPathRuntimeProtocol | None = None factory: UiPathRuntimeFactoryProtocol | None = None @@ -213,10 +231,27 @@ async def execute() -> None: if factory_settings else None ) - runtime = await factory.new_runtime( + base_runtime = await factory.new_runtime( resolved_entrypoint, ctx.conversation_id or ctx.job_id or "default", ) + runtime = base_runtime + + if simulation_config: + schema = await base_runtime.get_schema() + agent_model = None + if schema.metadata and "settings" in schema.metadata: + agent_model = schema.metadata["settings"].get( + "model" + ) + mocking_context = build_mocking_context( + simulation_config, agent_model + ) + if mocking_context: + runtime = UiPathMockRuntime( + delegate=base_runtime, + mocking_context=mocking_context, + ) if ctx.job_id: if UiPathConfig.is_tracing_enabled: @@ -243,8 +278,10 @@ async def execute() -> None: finally: if chat_runtime: await chat_runtime.dispose() - if runtime: + if runtime is not None and runtime is not base_runtime: await runtime.dispose() + if base_runtime is not None: + await base_runtime.dispose() if factory: await factory.dispose() diff --git a/packages/uipath/src/uipath/eval/mocks/__init__.py b/packages/uipath/src/uipath/eval/mocks/__init__.py index 95dfb877e..f9e7da177 100644 --- a/packages/uipath/src/uipath/eval/mocks/__init__.py +++ b/packages/uipath/src/uipath/eval/mocks/__init__.py @@ -1,14 +1,21 @@ """Mock interface.""" from ._mock_context import is_tool_simulated -from ._mock_runtime import UiPathMockRuntime -from ._types import ExampleCall, MockingContext +from ._mock_runtime import ( + UiPathMockRuntime, + build_mocking_context, + build_mocking_context_from_dict, +) +from ._types import ExampleCall, MockingContext, SimulationConfig from .mockable import mockable __all__ = [ "ExampleCall", - "UiPathMockRuntime", "MockingContext", - "mockable", + "SimulationConfig", + "UiPathMockRuntime", + "build_mocking_context", + "build_mocking_context_from_dict", "is_tool_simulated", + "mockable", ] diff --git a/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py b/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py index df41dadeb..71036215b 100644 --- a/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py +++ b/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py @@ -2,7 +2,6 @@ from __future__ import annotations -import json import logging import uuid from collections.abc import AsyncGenerator @@ -29,12 +28,70 @@ MockingContext, MockingStrategyType, ModelSettings, - ToolSimulation, + SimulationConfig, ) logger = logging.getLogger(__name__) +def build_mocking_context( + config: SimulationConfig, agent_model: str | None = None +) -> MockingContext | None: + """Build a MockingContext from a validated SimulationConfig. + + Args: + config: Validated simulation config. + agent_model: Optional agent model name to use as fallback. + + Returns: + MockingContext if enabled and tools are specified, None otherwise. + """ + if not config.enabled or not config.tools_to_simulate: + return None + + model = ( + ModelSettings(model=config.model) + if config.model + else ModelSettings(model=agent_model) + if agent_model + else None + ) + + mocking_strategy = LLMMockingStrategy( + type=MockingStrategyType.LLM, + prompt=config.instructions, + tools_to_simulate=config.tools_to_simulate, + model=model, + ) + + logger.debug( + f"Loaded simulation config for {len(config.tools_to_simulate)} tool(s)" + ) + return MockingContext( + strategy=mocking_strategy, + name="debug-simulation", + inputs={}, + ) + + +def build_mocking_context_from_dict( + simulation_data: dict[str, Any], agent_model: str | None = None +) -> MockingContext | None: + """Build a MockingContext from a simulation config dictionary. + + Deprecated: prefer build_mocking_context with a validated SimulationConfig. + + Args: + simulation_data: Parsed simulation config (same schema as simulation.json). + agent_model: Optional agent model name to use as fallback. + + Returns: + MockingContext if valid and enabled, None otherwise. + """ + config = SimulationConfig.model_validate(simulation_data) + return build_mocking_context(config, agent_model) + + def load_simulation_config(agent_model: str | None = None) -> MockingContext | None: """Load simulation.json from current directory and convert to MockingContext. @@ -48,48 +105,10 @@ def load_simulation_config(agent_model: str | None = None) -> MockingContext | N return None try: - with open(simulation_path, "r", encoding="utf-8") as f: - simulation_data = json.load(f) - - # Check if simulation is enabled - if not simulation_data.get("enabled", True): - return None - - # Extract tools to simulate - tools_to_simulate = [ - ToolSimulation(name=tool["name"]) - for tool in simulation_data.get("toolsToSimulate", []) - ] - - if not tools_to_simulate: - return None - - # Honor model from simulation config if specified, otherwise use the agent model - simulation_model = simulation_data.get("model") - model = ( - ModelSettings(model=simulation_model) - if simulation_model - else ModelSettings(model=agent_model) - if agent_model - else None - ) - - mocking_strategy = LLMMockingStrategy( - type=MockingStrategyType.LLM, - prompt=simulation_data.get("instructions", ""), - tools_to_simulate=tools_to_simulate, - model=model, - ) - - # Create MockingContext for debugging - mocking_context = MockingContext( - strategy=mocking_strategy, - name="debug-simulation", - inputs={}, + config = SimulationConfig.model_validate_json( + simulation_path.read_text(encoding="utf-8") ) - - logger.info(f"Loaded simulation config for {len(tools_to_simulate)} tool(s)") - return mocking_context + return build_mocking_context(config, agent_model) except Exception as e: logger.warning(f"Failed to load simulation.json: {e}") diff --git a/packages/uipath/src/uipath/eval/mocks/_types.py b/packages/uipath/src/uipath/eval/mocks/_types.py index 827569879..070040b65 100644 --- a/packages/uipath/src/uipath/eval/mocks/_types.py +++ b/packages/uipath/src/uipath/eval/mocks/_types.py @@ -129,6 +129,21 @@ class MockingContext(BaseModel): name: str = Field(default="debug") +class SimulationConfig(BaseModel): + """Top-level schema for simulation.json / --simulation flag.""" + + enabled: bool = True + tools_to_simulate: list[ToolSimulation] = Field( + default_factory=list, alias="toolsToSimulate" + ) + instructions: str = "" + model: str | None = None + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + class ExampleCall(BaseModel): """Example call for a resource containing resource I/O.""" diff --git a/packages/uipath/testcases/simulation-testcase/pyproject.toml b/packages/uipath/testcases/simulation-testcase/pyproject.toml new file mode 100644 index 000000000..d37877dbf --- /dev/null +++ b/packages/uipath/testcases/simulation-testcase/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "simulation-testcase" +version = "0.0.1" +description = "simulation-testcase" +authors = [{ name = "UiPath", email = "python-sdk@uipath.com" }] +dependencies = [ + "uipath", +] +requires-python = ">=3.11" + +[tool.uv.sources] +uipath = { path = "../../", editable = true } diff --git a/packages/uipath/testcases/simulation-testcase/run.sh b/packages/uipath/testcases/simulation-testcase/run.sh new file mode 100644 index 000000000..0095cc904 --- /dev/null +++ b/packages/uipath/testcases/simulation-testcase/run.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +TESTCASE_DIR="$(cd "$(dirname "$0")" && pwd)" +SAMPLE_DIR="$(cd "$TESTCASE_DIR/../../samples/runtime-simulations-agent" && pwd)" + +echo "Syncing testcase dependencies (local editable uipath)..." +uv sync --project "$TESTCASE_DIR" + +UIPATH_BIN="$TESTCASE_DIR/.venv/bin/uipath" + +# Run auth and agent from the sample dir so credentials are stored and read +# from the same location. +cd "$SAMPLE_DIR" + +echo "Authenticating with UiPath..." +"$UIPATH_BIN" auth \ + --client-id="$CLIENT_ID" \ + --client-secret="$CLIENT_SECRET" \ + --base-url="$BASE_URL" + +echo "Running agent with simulation..." +"$UIPATH_BIN" run main \ + -f input.json \ + --simulation "$(cat simulation.json)" 2>&1 | tee "$TESTCASE_DIR/run.log" + +# Copy the runtime output file back to the testcase dir for assert.py +mkdir -p "$TESTCASE_DIR/__uipath" +cp "$SAMPLE_DIR/__uipath/output.json" "$TESTCASE_DIR/__uipath/output.json" diff --git a/packages/uipath/testcases/simulation-testcase/src/assert.py b/packages/uipath/testcases/simulation-testcase/src/assert.py new file mode 100644 index 000000000..fd7e89697 --- /dev/null +++ b/packages/uipath/testcases/simulation-testcase/src/assert.py @@ -0,0 +1,57 @@ +import json +import os + +# ── 1. Verify agent output exists and succeeded ────────────────────────────── +output_file = "__uipath/output.json" +assert os.path.isfile(output_file), "Agent output file not found" + +with open(output_file, "r", encoding="utf-8") as f: + output_data = json.load(f) + +status = output_data.get("status") +assert status == "successful", f"Agent execution failed with status: {status}" + +output = output_data.get("output", {}) + +assert "syntax" in output, "Missing 'syntax' in output" +assert "style" in output, "Missing 'style' in output" +assert "improvements" in output, "Missing 'improvements' in output" +assert "summary" in output, "Missing 'summary' in output" + +assert isinstance(output["syntax"]["valid"], bool), "'syntax.valid' must be a bool" +assert isinstance(output["syntax"]["errors"], list), "'syntax.errors' must be a list" + +score = output["style"]["score"] +assert isinstance(score, int), "'style.score' must be an int" +assert 0 <= score <= 100, f"'style.score' out of range: {score}" +assert isinstance(output["style"]["violations"], list), ( + "'style.violations' must be a list" +) + +assert isinstance(output["improvements"]["suggestions"], list), ( + "'improvements.suggestions' must be a list" +) +assert isinstance(output["improvements"]["refactored_snippet"], str), ( + "'improvements.refactored_snippet' must be a str" +) + +# ── 2. Verify simulation produced non-default values ───────────────────────── +# Real tool impls always return: score=100, violations=[], suggestions=[]. +# The LLM simulation should detect issues in the input code and return richer output. +simulated_something = ( + score < 100 + or len(output["style"]["violations"]) > 0 + or len(output["improvements"]["suggestions"]) > 0 +) +assert simulated_something, ( + "Output matches hardcoded real-tool defaults — simulation may not have run. " + f"style.score={score}, violations={output['style']['violations']}, " + f"suggestions={output['improvements']['suggestions']}" +) + +print( + f"Simulation confirmed: score={score}, " + f"violations={len(output['style']['violations'])}, " + f"suggestions={len(output['improvements']['suggestions'])}" +) +print("All assertions passed.") diff --git a/packages/uipath/tests/cli/eval/mocks/test_mocks.py b/packages/uipath/tests/cli/eval/mocks/test_mocks.py index bdbdd3dc2..c4bc26ee3 100644 --- a/packages/uipath/tests/cli/eval/mocks/test_mocks.py +++ b/packages/uipath/tests/cli/eval/mocks/test_mocks.py @@ -929,3 +929,130 @@ async def foo(*args, **kwargs) -> dict[str, Any]: }, }, } + + +class TestUiPathMockRuntime: + """Tests for UiPathMockRuntime execute/stream/get_schema paths.""" + + def _make_context(self) -> MockingContext: + return MockingContext( + strategy=LLMMockingStrategy( + prompt="test", + tools_to_simulate=[ToolSimulation(name="my_tool")], + ), + name="test", + inputs={}, + ) + + async def test_execute_with_mocking_context_sets_and_clears(self): + from unittest.mock import AsyncMock, patch + + from uipath.eval.mocks._mock_runtime import UiPathMockRuntime + + delegate = MagicMock() + mock_result = MagicMock() + delegate.execute = AsyncMock(return_value=mock_result) + + runtime = UiPathMockRuntime( + delegate=delegate, + mocking_context=self._make_context(), + ) + + with ( + patch("uipath.eval.mocks._mock_runtime.set_execution_context") as mock_set, + patch( + "uipath.eval.mocks._mock_runtime.clear_execution_context" + ) as mock_clear, + ): + result = await runtime.execute({"key": "value"}) + + assert result is mock_result + mock_set.assert_called_once() + mock_clear.assert_called_once() + + async def test_stream_with_mocking_context_sets_and_clears(self): + from unittest.mock import patch + + from uipath.eval.mocks._mock_runtime import UiPathMockRuntime + + sentinel = object() + + async def _gen(*args, **kwargs): + yield sentinel + + delegate = MagicMock() + delegate.stream = _gen + + runtime = UiPathMockRuntime( + delegate=delegate, + mocking_context=self._make_context(), + ) + + with ( + patch("uipath.eval.mocks._mock_runtime.set_execution_context") as mock_set, + patch( + "uipath.eval.mocks._mock_runtime.clear_execution_context" + ) as mock_clear, + ): + events = [e async for e in runtime.stream({})] + + assert events == [sentinel] + mock_set.assert_called_once() + mock_clear.assert_called_once() + + async def test_stream_without_mocking_context_passes_through(self): + from unittest.mock import patch + + from uipath.eval.mocks._mock_runtime import UiPathMockRuntime + + sentinel = object() + + async def _gen(*args, **kwargs): + yield sentinel + + delegate = MagicMock() + delegate.stream = _gen + + runtime = UiPathMockRuntime(delegate=delegate, mocking_context=None) + with patch( + "uipath.eval.mocks._mock_runtime.load_simulation_config", return_value=None + ): + runtime._mocking_context = None + events = [e async for e in runtime.stream({})] + + assert events == [sentinel] + + async def test_get_schema_delegates(self): + from unittest.mock import AsyncMock, patch + + from uipath.eval.mocks._mock_runtime import UiPathMockRuntime + + schema = MagicMock() + delegate = MagicMock() + delegate.get_schema = AsyncMock(return_value=schema) + + runtime = UiPathMockRuntime(delegate=delegate, mocking_context=None) + with patch( + "uipath.eval.mocks._mock_runtime.load_simulation_config", return_value=None + ): + result = await runtime.get_schema() + + assert result is schema + + def test_set_execution_context_handles_mocker_creation_failure(self): + from unittest.mock import patch + + from uipath.eval._execution_context import ExecutionSpanCollector + from uipath.eval.mocks._mock_context import mocker_context + from uipath.eval.mocks._mock_runtime import set_execution_context + + context = self._make_context() + with patch( + "uipath.eval.mocks._mock_runtime.MockerFactory.create", + side_effect=RuntimeError("boom"), + ): + set_execution_context(context, ExecutionSpanCollector(), "test-id") + + # mocking_context is set, but mocker_context must be None on failure + assert mocker_context.get() is None + clear_execution_context() diff --git a/packages/uipath/tests/cli/test_run.py b/packages/uipath/tests/cli/test_run.py index 479fc9953..aa182c7c5 100644 --- a/packages/uipath/tests/cli/test_run.py +++ b/packages/uipath/tests/cli/test_run.py @@ -1,4 +1,5 @@ # type: ignore +import json import os from contextlib import asynccontextmanager from unittest.mock import AsyncMock, Mock, patch @@ -448,3 +449,113 @@ def main(input_data: PersonIn) -> PersonOut: assert output_data["email"] == "john@example.com" assert output_data["is_adult"] is True assert output_data["greeting"] == "Hello, John Doe!" + + +_SIMULATION_JSON = { + "enabled": True, + "toolsToSimulate": [{"name": "check_syntax"}, {"name": "check_style"}], + "instructions": "Simulate.", +} + + +class TestRunSimulation: + """Tests for the --simulation flag on the run command.""" + + def _make_factory(self): + factory = Mock() + runtime = Mock() + runtime.stream = Mock(side_effect=_empty_async_gen) + runtime.dispose = AsyncMock() + runtime.get_schema = AsyncMock(return_value=Mock(metadata=None)) + factory.discover_entrypoints.return_value = ["main"] + factory.get_settings = AsyncMock(return_value=None) + factory.dispose = AsyncMock() + factory.new_runtime = AsyncMock(return_value=runtime) + return factory, runtime + + def test_invalid_simulation_json_exits_with_error( + self, runner: CliRunner, temp_dir: str + ): + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("uipath.json", "w") as f: + json.dump({"functions": {"main": "main.py:main"}}, f) + with open("main.py", "w") as f: + f.write("async def main(input): return {}") + + result = runner.invoke( + cli, ["run", "main", "--simulation", "{ not valid json }"] + ) + assert result.exit_code == 1 + assert "Invalid JSON" in result.output + + def test_simulation_wraps_runtime_with_mock_runtime( + self, runner: CliRunner, temp_dir: str + ): + factory, _ = self._make_factory() + + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("uipath.json", "w") as f: + json.dump({"functions": {"main": "main.py:main"}}, f) + with open("main.py", "w") as f: + f.write("async def main(input): return {}") + + with ( + patch( + "uipath._cli.cli_run.Middlewares.next", + return_value=_middleware_continue(), + ), + patch( + "uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get", + return_value=factory, + ), + patch( + "uipath._cli.cli_run.ResourceOverwritesContext", + side_effect=_mock_resource_overwrites_context, + ), + patch("uipath._cli.cli_run.UiPathMockRuntime") as mock_cls, + ): + mock_cls.return_value = Mock( + stream=Mock(side_effect=_empty_async_gen), + dispose=AsyncMock(), + get_schema=AsyncMock(return_value=Mock(metadata=None)), + ) + runner.invoke( + cli, + ["run", "main", "--simulation", json.dumps(_SIMULATION_JSON)], + ) + + assert mock_cls.called + assert mock_cls.call_args.kwargs["mocking_context"] is not None + + def test_simulation_disabled_does_not_wrap_runtime( + self, runner: CliRunner, temp_dir: str + ): + factory, _ = self._make_factory() + disabled = {**_SIMULATION_JSON, "enabled": False} + + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("uipath.json", "w") as f: + json.dump({"functions": {"main": "main.py:main"}}, f) + with open("main.py", "w") as f: + f.write("async def main(input): return {}") + + with ( + patch( + "uipath._cli.cli_run.Middlewares.next", + return_value=_middleware_continue(), + ), + patch( + "uipath._cli.cli_run.UiPathRuntimeFactoryRegistry.get", + return_value=factory, + ), + patch( + "uipath._cli.cli_run.ResourceOverwritesContext", + side_effect=_mock_resource_overwrites_context, + ), + patch("uipath._cli.cli_run.UiPathMockRuntime") as mock_cls, + ): + runner.invoke( + cli, ["run", "main", "--simulation", json.dumps(disabled)] + ) + + assert not mock_cls.called diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 3d4c916d3..c51486b96 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.62" +version = "2.10.63" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From f2cffbbbfd5403fbacd4068013e2a885d1df07e3 Mon Sep 17 00:00:00 2001 From: Ion Mincu Date: Fri, 15 May 2026 17:00:15 +0300 Subject: [PATCH 058/121] fix(tracing): set Traces source to CodedAgents (#1631) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/_span_utils.py | 4 ++-- .../tests/services/test_span_utils.py | 12 ++++++------ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/tracing/_otel_exporters.py | 2 +- packages/uipath/tests/tracing/test_otel_exporters.py | 6 +++--- packages/uipath/uv.lock | 4 ++-- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 80546ec3a..bb97d8c38 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.48" +version = "0.1.49" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index cd7e15e23..00329ecc8 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -15,8 +15,8 @@ logger = logging.getLogger(__name__) -# SourceEnum.Robots = 4 (default for Python SDK / coded agents) -DEFAULT_SOURCE = 4 +# SourceEnum.CodedAgents = 10 (default for Python SDK / coded agents) +DEFAULT_SOURCE = 10 class AttachmentProvider(IntEnum): diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 80cd0d2db..268ffc34c 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -363,8 +363,8 @@ def test_uipath_span_missing_execution_type_and_agent_version(self): assert span_dict["AgentVersion"] is None @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) - def test_uipath_span_source_defaults_to_robots(self): - """Test that Source defaults to 4 (Robots) and ignores attributes.source.""" + def test_uipath_span_source_defaults_to_coded_agents(self): + """Test that Source defaults to 10 (CodedAgents) and ignores attributes.source.""" mock_span = Mock(spec=OTelSpan) trace_id = 0x123456789ABCDEF0123456789ABCDEF0 @@ -387,9 +387,9 @@ def test_uipath_span_source_defaults_to_robots(self): uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) span_dict = uipath_span.to_dict() - # Top-level Source should be 4 (Robots), string "runtime" is ignored - assert uipath_span.source == 4 - assert span_dict["Source"] == 4 + # Top-level Source should be 10 (CodedAgents), string "runtime" is ignored + assert uipath_span.source == 10 + assert span_dict["Source"] == 10 # attributes.source string should still be in Attributes JSON attrs = json.loads(span_dict["Attributes"]) @@ -408,7 +408,7 @@ def test_uipath_span_source_override_with_uipath_source(self): mock_span.name = "test-span" mock_span.parent = None mock_span.status.status_code = StatusCode.OK - # uipath.source=1 (Agents) overrides default of 4 (Robots) + # uipath.source=1 (Agents) overrides default of 10 (CodedAgents) mock_span.attributes = {"uipath.source": 1, "source": "runtime"} mock_span.events = [] mock_span.links = [] diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 1e7878b10..3320bcd9d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.48" +version = "0.1.49" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index aba6ae877..6870318e3 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.63" +version = "2.10.64" 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/tracing/_otel_exporters.py b/packages/uipath/src/uipath/tracing/_otel_exporters.py index 423473065..d2bf3a7c1 100644 --- a/packages/uipath/src/uipath/tracing/_otel_exporters.py +++ b/packages/uipath/src/uipath/tracing/_otel_exporters.py @@ -389,7 +389,7 @@ def _process_span_attributes(self, span_data: Dict[str, Any]) -> None: def _build_url(self, span_list: list[Dict[str, Any]]) -> str: """Construct the URL for the API request.""" trace_id = str(span_list[0]["TraceId"]) - return f"{self.base_url}/api/Traces/spans?traceId={trace_id}&source=Robots" + return f"{self.base_url}/api/Traces/spans?traceId={trace_id}&source=CodedAgents" def _send_with_retries( self, url: str, payload: list[Dict[str, Any]], max_retries: int = 4 diff --git a/packages/uipath/tests/tracing/test_otel_exporters.py b/packages/uipath/tests/tracing/test_otel_exporters.py index a55fa5d60..648dd9190 100644 --- a/packages/uipath/tests/tracing/test_otel_exporters.py +++ b/packages/uipath/tests/tracing/test_otel_exporters.py @@ -54,7 +54,7 @@ def exporter(mock_env_vars): exporter = LlmOpsHttpExporter() # Mock _build_url to include query parameters as in the actual implementation exporter._build_url = MagicMock( # type: ignore - return_value="https://test.uipath.com/org/tenant/llmopstenant_/api/Traces/spans?traceId=test-trace-id&source=Robots" + return_value="https://test.uipath.com/org/tenant/llmopstenant_/api/Traces/spans?traceId=test-trace-id&source=CodedAgents" ) yield exporter @@ -107,7 +107,7 @@ def test_export_success(exporter, mock_span): [{"span": "data", "TraceId": "test-trace-id"}] ) exporter.http_client.post.assert_called_once_with( - "https://test.uipath.com/org/tenant/llmopstenant_/api/Traces/spans?traceId=test-trace-id&source=Robots", + "https://test.uipath.com/org/tenant/llmopstenant_/api/Traces/spans?traceId=test-trace-id&source=CodedAgents", json=[{"span": "data", "TraceId": "test-trace-id"}], ) @@ -685,7 +685,7 @@ def exporter_with_mocks(self, mock_env_vars): with patch("uipath.tracing._otel_exporters.httpx.Client"): exporter = LlmOpsHttpExporter() exporter._build_url = MagicMock( # type: ignore - return_value="https://test.uipath.com/org/tenant/llmopstenant_/api/Traces/spans?traceId=test-trace-id&source=Robots" + return_value="https://test.uipath.com/org/tenant/llmopstenant_/api/Traces/spans?traceId=test-trace-id&source=CodedAgents" ) yield exporter diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index c51486b96..a184861d5 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.63" +version = "2.10.64" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.48" +version = "0.1.49" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 82ed987ad72e9e92e191b088821774d8a0e4da97 Mon Sep 17 00:00:00 2001 From: Ion Mincu Date: Mon, 18 May 2026 12:49:34 +0300 Subject: [PATCH 059/121] fix(tracing): use agentId as span reference_id (#1636) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath-platform/src/uipath/platform/common/_span_utils.py | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index bb97d8c38..944f279da 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.49" +version = "0.1.50" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index 00329ecc8..054bb3c9c 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -283,7 +283,7 @@ def otel_span_to_uipath_span( # Top-level fields for internal tracing schema execution_type = attributes_dict.get("executionType") agent_version = attributes_dict.get("agentVersion") - reference_id = attributes_dict.get("referenceId") + reference_id = attributes_dict.get("agentId") # Source: override via uipath.source attribute, else DEFAULT_SOURCE uipath_source = attributes_dict.get("uipath.source") diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 3320bcd9d..1e3cd3430 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.49" +version = "0.1.50" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index a184861d5..4729add5f 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.49" +version = "0.1.50" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 2c3b92307f7be42bc19ed466548e679e81db7b13 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Mon, 18 May 2026 15:43:20 +0200 Subject: [PATCH 060/121] fix(jobs): allow null folder_key on Job model (#1637) --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath-platform/src/uipath/platform/orchestrator/job.py | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 944f279da..ac114087b 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.50" +version = "0.1.51" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/job.py b/packages/uipath-platform/src/uipath/platform/orchestrator/job.py index 7ade631e5..6464405b4 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/job.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/job.py @@ -79,5 +79,5 @@ class Job(BaseModel): has_errors: Optional[bool] = Field(default=None, alias="HasErrors") has_warnings: Optional[bool] = Field(default=None, alias="HasWarnings") job_error: Optional[JobErrorInfo] = Field(default=None, alias="JobError") - folder_key: str = Field(alias="FolderKey") + folder_key: Optional[str] = Field(default=None, alias="FolderKey") id: int = Field(alias="Id") diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 1e3cd3430..3e7f668bf 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.50" +version = "0.1.51" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 4729add5f..c89c40005 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.50" +version = "0.1.51" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 67c4d0bea960cfd9c19a609751bd4d64e9a77caa Mon Sep 17 00:00:00 2001 From: AAgnihotry <95259907+AAgnihotry@users.noreply.github.com> Date: Mon, 18 May 2026 17:18:03 -0700 Subject: [PATCH 061/121] fix(tracing): use UIPATH_AGENT_ID for reference_id instead of PROJECT_KEY (#1643) Co-authored-by: Claude Sonnet 4.6 --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath-platform/src/uipath/platform/common/_span_utils.py | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index ac114087b..649a10962 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.51" +version = "0.1.52" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index 054bb3c9c..9cb49826b 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -283,7 +283,7 @@ def otel_span_to_uipath_span( # Top-level fields for internal tracing schema execution_type = attributes_dict.get("executionType") agent_version = attributes_dict.get("agentVersion") - reference_id = attributes_dict.get("agentId") + reference_id = env.get("UIPATH_AGENT_ID") or attributes_dict.get("agentId") # Source: override via uipath.source attribute, else DEFAULT_SOURCE uipath_source = attributes_dict.get("uipath.source") diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 3e7f668bf..23d1e040f 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.51" +version = "0.1.52" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 6870318e3..d1297735e 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.64" +version = "2.10.65" 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/uv.lock b/packages/uipath/uv.lock index c89c40005..4f0c96640 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.64" +version = "2.10.65" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.51" +version = "0.1.52" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 91049df2de93cc09e1084eeea71f2e27dc41471f Mon Sep 17 00:00:00 2001 From: Chibi Vikramathithan Date: Tue, 19 May 2026 00:01:44 -0700 Subject: [PATCH 062/121] fix(eval): trim legacy trajectory span history (#1653) --- packages/uipath/pyproject.toml | 2 +- .../evaluators/legacy_trajectory_evaluator.py | 7 +- .../test_legacy_trajectory_evaluator.py | 64 +++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 packages/uipath/tests/evaluators/test_legacy_trajectory_evaluator.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index d1297735e..9ba94e18a 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.65" +version = "2.10.66" 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/eval/evaluators/legacy_trajectory_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/legacy_trajectory_evaluator.py index 17b69d0d0..cff0e8788 100644 --- a/packages/uipath/src/uipath/eval/evaluators/legacy_trajectory_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/legacy_trajectory_evaluator.py @@ -12,13 +12,13 @@ from ..._utils.constants import COMMUNITY_agents_SUFFIX from .._execution_context import eval_set_run_id_context +from .._helpers.evaluators_helpers import trace_to_str from .._helpers.helpers import is_empty_value from ..models import EvaluationResult from ..models.models import ( AgentExecution, LLMResponse, NumericEvaluationResult, - TrajectoryEvaluationTrace, UiPathEvaluationError, UiPathEvaluationErrorCategory, ) @@ -140,10 +140,7 @@ def _create_evaluation_prompt( and agent_run_history and isinstance(agent_run_history[0], ReadableSpan) ): - trajectory_trace = TrajectoryEvaluationTrace.from_readable_spans( - agent_run_history - ) - agent_run_history = str(trajectory_trace.spans) + agent_run_history = trace_to_str(agent_run_history) else: agent_run_history = str(agent_run_history) diff --git a/packages/uipath/tests/evaluators/test_legacy_trajectory_evaluator.py b/packages/uipath/tests/evaluators/test_legacy_trajectory_evaluator.py new file mode 100644 index 000000000..687fce08d --- /dev/null +++ b/packages/uipath/tests/evaluators/test_legacy_trajectory_evaluator.py @@ -0,0 +1,64 @@ +import uuid + +from opentelemetry.sdk.trace import ReadableSpan + +from uipath.eval.evaluators import LegacyTrajectoryEvaluator +from uipath.eval.evaluators.base_legacy_evaluator import LegacyEvaluationCriteria +from uipath.eval.evaluators.legacy_trajectory_evaluator import ( + LegacyTrajectoryEvaluatorConfig, +) +from uipath.eval.models.models import LegacyEvaluatorCategory, LegacyEvaluatorType + + +def _legacy_trajectory_evaluator() -> LegacyTrajectoryEvaluator: + return LegacyTrajectoryEvaluator( + id=str(uuid.uuid4()), + name="Legacy trajectory", + config_type=LegacyTrajectoryEvaluatorConfig, + evaluation_criteria_type=LegacyEvaluationCriteria, + justification_type=str, + category=LegacyEvaluatorCategory.Trajectory, + type=LegacyEvaluatorType.Trajectory, + prompt="History:\n{{AgentRunHistory}}\nExpected:\n{{ExpectedAgentBehavior}}", + createdAt="2026-05-14T00:00:00Z", + updatedAt="2026-05-14T00:00:00Z", + ) + + +def test_legacy_trajectory_prompt_uses_compact_tool_history() -> None: + long_prompt = "SYSTEM_PROMPT_" + ("x" * 10_000) + spans = [ + ReadableSpan( + name="agent_llm_call", + start_time=0, + end_time=1, + attributes={ + "openinference.span.kind": "LLM", + "input.value": f'{{"messages": [{{"role": "system", "content": "{long_prompt}"}}]}}', + "output.value": '{"generations": []}', + }, + ), + ReadableSpan( + name="search_profiles", + start_time=1, + end_time=2, + attributes={ + "openinference.span.kind": "TOOL", + "tool.name": "search_profiles", + "input.value": '{"query": "mentor"}', + "output.value": '{"content": "found mentor profile"}', + "metadata": f'{{"agent_prompt": "{long_prompt}"}}', + }, + ), + ] + + prompt = _legacy_trajectory_evaluator()._create_evaluation_prompt( + expected_agent_behavior="The agent should search matching profiles.", + agent_run_history=spans, + ) + + assert "SYSTEM_PROMPT_" not in prompt + assert "Tool: search_profiles" in prompt + assert '{"query": "mentor"}' in prompt + assert "found mentor profile" in prompt + assert "agent_llm_call" not in prompt diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 4f0c96640..1af3b0d0a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.65" +version = "2.10.66" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 790112887acb93d14fe8a42c76930163b6f02d89 Mon Sep 17 00:00:00 2001 From: Andrei Tava Date: Tue, 19 May 2026 11:19:43 +0300 Subject: [PATCH 063/121] chore: enforce 2 days min age for third parties (#1654) --- packages/uipath-core/pyproject.toml | 3 +++ packages/uipath-core/uv.lock | 4 ++++ packages/uipath-platform/pyproject.toml | 6 ++++++ packages/uipath-platform/uv.lock | 7 +++++++ packages/uipath/pyproject.toml | 8 ++++++++ packages/uipath/uv.lock | 9 +++++++++ 6 files changed, 37 insertions(+) diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 8a001489b..959122a11 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -120,6 +120,9 @@ exclude_lines = [ "@(abc\\.)?abstractmethod", ] +[tool.uv] +exclude-newer = "2 days" + [[tool.uv.index]] name = "testpypi" url = "https://test.pypi.org/simple/" diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index bf73c603d..ab6e6aa14 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -2,6 +2,10 @@ version = 1 revision = 3 requires-python = ">=3.11" +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P2D" + [[package]] name = "annotated-types" version = "0.7.0" diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 649a10962..cad224be8 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -126,6 +126,12 @@ exclude_lines = [ "@(abc\\.)?abstractmethod", ] +[tool.uv] +exclude-newer = "2 days" + +[tool.uv.exclude-newer-package] +uipath-core = false + [tool.uv.sources] uipath-core = { path = "../uipath-core", editable = true } diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 23d1e040f..72baba331 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -2,6 +2,13 @@ version = 1 revision = 3 requires-python = ">=3.11" +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P2D" + +[options.exclude-newer-package] +uipath-core = false + [[package]] name = "annotated-types" version = "0.7.0" diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 9ba94e18a..12777e925 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -162,6 +162,14 @@ exclude_lines = [ "@(abc\\.)?abstractmethod", ] +[tool.uv] +exclude-newer = "2 days" + +[tool.uv.exclude-newer-package] +uipath-core = false +uipath-runtime = false +uipath-platform = false + [tool.uv.sources] uipath-core = { path = "../uipath-core", editable = true } uipath-platform = { path = "../uipath-platform", editable = true } diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 1af3b0d0a..3cc0f8f89 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.11" +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P2D" + +[options.exclude-newer-package] +uipath-runtime = false +uipath-platform = false +uipath-core = false + [[package]] name = "aiohappyeyeballs" version = "2.6.1" From 4de530c137337ee451919cdf2a387d056500b615 Mon Sep 17 00:00:00 2001 From: Cristian Cotovanu <87022468+cotovanu-cristian@users.noreply.github.com> Date: Tue, 19 May 2026 15:19:55 +0300 Subject: [PATCH 064/121] feat(platform): promote verbosityLevel attribute to top-level VerbosityLevel field (#1627) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/_span_utils.py | 18 ++++- .../tests/services/test_span_utils.py | 79 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- .../uipath/src/uipath/tracing/__init__.py | 2 + .../tests/tracing/test_otel_exporters.py | 13 +++ packages/uipath/uv.lock | 4 +- 8 files changed, 117 insertions(+), 7 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index cad224be8..9879982c3 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.52" +version = "0.1.53" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index 9cb49826b..120c39b68 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -29,6 +29,16 @@ class AttachmentDirection(IntEnum): OUT = 2 +class VerbosityLevel(IntEnum): + VERBOSE = 0 + TRACE = 1 + INFORMATION = 2 + WARNING = 3 + ERROR = 4 + CRITICAL = 5 + OFF = 6 + + class SpanAttachment(BaseModel): """Represents an attachment in the UiPath tracing system.""" @@ -87,6 +97,7 @@ class UiPathSpan: # Top-level fields for internal tracing schema execution_type: Optional[int] = None agent_version: Optional[str] = None + verbosity_level: Optional[int] = None attachments: Optional[List[SpanAttachment]] = None def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: @@ -114,7 +125,7 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: for att in self.attachments ] - return { + result: Dict[str, Any] = { "Id": self.id, "TraceId": self.trace_id, "ParentId": self.parent_id, @@ -138,6 +149,9 @@ def to_dict(self, serialize_attributes: bool = True) -> Dict[str, Any]: "AgentVersion": self.agent_version, "Attachments": attachments_out, } + if self.verbosity_level is not None: + result["VerbosityLevel"] = self.verbosity_level + return result class _SpanUtils: @@ -284,6 +298,7 @@ def otel_span_to_uipath_span( execution_type = attributes_dict.get("executionType") agent_version = attributes_dict.get("agentVersion") reference_id = env.get("UIPATH_AGENT_ID") or attributes_dict.get("agentId") + verbosity_level = attributes_dict.get("verbosityLevel") # Source: override via uipath.source attribute, else DEFAULT_SOURCE uipath_source = attributes_dict.get("uipath.source") @@ -334,6 +349,7 @@ def otel_span_to_uipath_span( span_type=span_type, execution_type=execution_type, agent_version=agent_version, + verbosity_level=verbosity_level, reference_id=reference_id, source=source, attachments=attachments, diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 268ffc34c..c10729d31 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -10,6 +10,85 @@ from uipath.platform.common import UiPathSpan, _SpanUtils +class TestOTelToUiPathSpan: + """OTEL attribute -> top-level UiPathSpan field mapping. + + `_SpanUtils.otel_span_to_uipath_span` lifts a small set of OTEL + span attributes onto dedicated `UiPathSpan` fields surfaced under + `to_dict()`. This test documents that mapping — adding a new row + means the attribute is newly mapped, removing one breaks + downstream consumers. + """ + + ATTRIBUTE_FIELD_MAP = [ + ("executionType", "execution_type", "ExecutionType", 1), + ("agentVersion", "agent_version", "AgentVersion", "1.2.3"), + ("agentId", "reference_id", "ReferenceId", "ref-abc"), + ("verbosityLevel", "verbosity_level", "VerbosityLevel", 6), + ] + + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) + def test_attributes_map_to_top_level_fields(self) -> None: + attrs = { + otel_attr: value for otel_attr, _, _, value in self.ATTRIBUTE_FIELD_MAP + } + + mock_span = Mock(spec=OTelSpan) + mock_context = SpanContext( + trace_id=0x123456789ABCDEF0123456789ABCDEF0, + span_id=0x0123456789ABCDEF, + is_remote=False, + ) + mock_span.get_span_context.return_value = mock_context + mock_span.name = "test-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = attrs + mock_span.events = [] + mock_span.links = [] + now_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = now_ns + mock_span.end_time = now_ns + 1_000_000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + span_dict = uipath_span.to_dict() + + for _, span_field, top_level_key, value in self.ATTRIBUTE_FIELD_MAP: + assert getattr(uipath_span, span_field) == value, span_field + assert span_dict[top_level_key] == value, top_level_key + + @patch.dict(os.environ, {"UIPATH_ORGANIZATION_ID": "test-org"}) + def test_verbosity_level_omitted_when_unset(self) -> None: + """Spans that don't set verbosityLevel must not carry the key on the wire. + + Backwards compat: pre-existing spans never emitted VerbosityLevel; the + LLMOps backend applies its own default. Adding `"VerbosityLevel": null` + unconditionally would change the wire format for every existing span. + """ + mock_span = Mock(spec=OTelSpan) + mock_context = SpanContext( + trace_id=0x123456789ABCDEF0123456789ABCDEF0, + span_id=0x0123456789ABCDEF, + is_remote=False, + ) + mock_span.get_span_context.return_value = mock_context + mock_span.name = "legacy-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = {"someOtherAttr": "value"} + mock_span.events = [] + mock_span.links = [] + now_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = now_ns + mock_span.end_time = now_ns + 1_000_000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + span_dict = uipath_span.to_dict() + + assert uipath_span.verbosity_level is None + assert "VerbosityLevel" not in span_dict + + class TestNormalizeIds: """Tests for OTEL ID normalization functions.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 72baba331..4293ee066 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.52" +version = "0.1.53" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 12777e925..2d50973d8 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.66" +version = "2.10.67" 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" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.47, <0.2.0", + "uipath-platform>=0.1.53, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/tracing/__init__.py b/packages/uipath/src/uipath/tracing/__init__.py index aaef6328c..e6c37bc99 100644 --- a/packages/uipath/src/uipath/tracing/__init__.py +++ b/packages/uipath/src/uipath/tracing/__init__.py @@ -5,6 +5,7 @@ AttachmentDirection, AttachmentProvider, SpanAttachment, + VerbosityLevel, ) from ._live_tracking_processor import LiveTrackingSpanProcessor @@ -23,4 +24,5 @@ "AttachmentDirection", "AttachmentProvider", "SpanAttachment", + "VerbosityLevel", ] diff --git a/packages/uipath/tests/tracing/test_otel_exporters.py b/packages/uipath/tests/tracing/test_otel_exporters.py index 648dd9190..fc5a370c0 100644 --- a/packages/uipath/tests/tracing/test_otel_exporters.py +++ b/packages/uipath/tests/tracing/test_otel_exporters.py @@ -810,5 +810,18 @@ def test_none_stays_none(self, mock_env_vars, mock_span): assert payload["ProcessKey"] is None +class TestVerbosityLevelReexport: + """VerbosityLevel from uipath-platform is re-exported via uipath.tracing.""" + + def test_uipath_tracing_reexports_verbosity_level(self) -> None: + from uipath.platform.common._span_utils import ( + VerbosityLevel as _CommonVerbosity, + ) + from uipath.tracing import VerbosityLevel as _TracingVerbosity + + assert _TracingVerbosity is _CommonVerbosity + assert _TracingVerbosity.OFF == 6 + + if __name__ == "__main__": unittest.main() diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 3cc0f8f89..c97b619da 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.66" +version = "2.10.67" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.52" +version = "0.1.53" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 010a8b7a6c11818184ee6478de1e76c82bfcd0ae Mon Sep 17 00:00:00 2001 From: yashwagle1 Date: Tue, 19 May 2026 09:56:40 -0700 Subject: [PATCH 065/121] fix(automation-ops): handle empty deployed-policy response (#1642) Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: AAgnihotry <95259907+AAgnihotry@users.noreply.github.com> --- packages/uipath-platform/pyproject.toml | 2 +- .../automation_ops/_automation_ops_service.py | 10 ++++-- .../services/test_automation_ops_service.py | 36 +++++++++++++++++++ packages/uipath-platform/uv.lock | 4 +-- packages/uipath/uv.lock | 4 +-- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 9879982c3..67fa9de2e 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.53" +version = "0.1.54" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/automation_ops/_automation_ops_service.py b/packages/uipath-platform/src/uipath/platform/automation_ops/_automation_ops_service.py index 05c37bbb5..b5eac8cdd 100644 --- a/packages/uipath-platform/src/uipath/platform/automation_ops/_automation_ops_service.py +++ b/packages/uipath-platform/src/uipath/platform/automation_ops/_automation_ops_service.py @@ -30,7 +30,8 @@ def get_deployed_policy(self) -> dict[str, Any]: """Retrieve the deployed policy. Returns: - The deployed policy response as a dictionary. + The deployed policy response as a dictionary. Returns an empty + dict when no policy is deployed (empty 200 response body). """ spec = self._deployed_policy_spec() response = self.request( @@ -39,6 +40,8 @@ def get_deployed_policy(self) -> dict[str, Any]: headers=spec.headers, scoped="tenant", ) + if not response.content: + return {} return response.json() @traced(name="automation_ops_get_deployed_policy", run_type="uipath") @@ -46,7 +49,8 @@ async def get_deployed_policy_async(self) -> dict[str, Any]: """Retrieve the deployed policy (async). Returns: - The deployed policy response as a dictionary. + The deployed policy response as a dictionary. Returns an empty + dict when no policy is deployed (empty 200 response body). """ spec = self._deployed_policy_spec() response = await self.request_async( @@ -55,6 +59,8 @@ async def get_deployed_policy_async(self) -> dict[str, Any]: headers=spec.headers, scoped="tenant", ) + if not response.content: + return {} return response.json() def _deployed_policy_spec(self) -> RequestSpec: diff --git a/packages/uipath-platform/tests/services/test_automation_ops_service.py b/packages/uipath-platform/tests/services/test_automation_ops_service.py index c90e1f80a..e19a02a5a 100644 --- a/packages/uipath-platform/tests/services/test_automation_ops_service.py +++ b/packages/uipath-platform/tests/services/test_automation_ops_service.py @@ -49,6 +49,24 @@ def test_returns_policy_dict( assert result == expected_policy + def test_returns_empty_dict_when_no_policy_deployed( + self, + httpx_mock: HTTPXMock, + service: AutomationOpsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/agenthub_/api/policies/deployed-policy", + status_code=200, + content=b"", + ) + + result = service.get_deployed_policy() + + assert result == {} + def test_uses_post_method( self, httpx_mock: HTTPXMock, @@ -102,6 +120,24 @@ async def test_returns_policy_dict( assert result == expected_policy + async def test_returns_empty_dict_when_no_policy_deployed( + self, + httpx_mock: HTTPXMock, + service: AutomationOpsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/agenthub_/api/policies/deployed-policy", + status_code=200, + content=b"", + ) + + result = await service.get_deployed_policy_async() + + assert result == {} + async def test_url_is_tenant_scoped( self, httpx_mock: HTTPXMock, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 4293ee066..777679614 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-17T16:21:45.1868663Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.53" +version = "0.1.54" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index c97b619da..d7e9a9ee9 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-17T16:21:47.0725551Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.53" +version = "0.1.54" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 5d8889476ccf9f2a031429afe97e50ba83b14820 Mon Sep 17 00:00:00 2001 From: Radu Mihai Gheorghe Date: Wed, 20 May 2026 16:26:29 +0300 Subject: [PATCH 066/121] feat: introduce ToolsConfiguration on AgentMcpResourceConfig (#1655) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 41 +++++++++++++++++-- packages/uipath/uv.lock | 4 +- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 2d50973d8..36550f54d 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.67" +version = "2.10.68" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 38bc8a815..ba20b8165 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -446,13 +446,48 @@ class AgentMcpTool(BaseCfg): class DynamicToolsMode(str, CaseInsensitiveEnum): - """Dynamic tools mode enumeration.""" + """Dynamic tools mode enumeration. + + Deprecated: kept for backwards compatibility with older ``agent.json`` files + that still serialize the ``dynamicTools`` field. New code should use + :class:`ToolsConfiguration` (see ``AgentMcpResourceConfig.tools_configuration``). + """ NONE = "none" SCHEMA = "schema" ALL = "all" +class CachedToolsConfig(BaseCfg): + """Cached tools configuration: use the tools saved in the agent definition snapshot.""" + + type: Literal["cached"] = Field(default="cached", frozen=True) + + +class DynamicToolsConfig(BaseCfg): + """Dynamic tools configuration: fetch the tool list from the MCP server at runtime. + + When ``allow_all`` is true, every tool the server exposes is forwarded + to the agent. When false, the live list is filtered by the snapshot's + ``available_tools`` allowlist (live schemas, curated tool set). + """ + + type: Literal["dynamic"] = Field(default="dynamic", frozen=True) + allow_all: bool = Field(alias="allowAll") + + +DiscoveryMode = Annotated[ + Union[CachedToolsConfig, DynamicToolsConfig], + Field(discriminator="type"), +] + + +class ToolsConfiguration(BaseCfg): + """Configuration describing how tools are sourced for an MCP resource.""" + + discovery_mode: DiscoveryMode = Field(alias="discoveryMode") + + class AgentMcpResourceConfig(BaseAgentResourceConfig): """Agent MCP resource configuration model.""" @@ -462,8 +497,8 @@ class AgentMcpResourceConfig(BaseAgentResourceConfig): folder_path: str = Field(alias="folderPath") slug: str = Field(..., alias="slug") available_tools: List[AgentMcpTool] = Field(..., alias="availableTools") - dynamic_tools: DynamicToolsMode = Field( - default=DynamicToolsMode.NONE, alias="dynamicTools" + tools_configuration: Optional[ToolsConfiguration] = Field( + default=None, alias="toolsConfiguration" ) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d7e9a9ee9..41ae12119 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-17T16:21:47.0725551Z" +exclude-newer = "2026-05-17T17:25:34.9197064Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.67" +version = "2.10.68" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 3658f1728b77ee134b3ad0a478eb3cc4ee7715f8 Mon Sep 17 00:00:00 2001 From: Cristian Cotovanu <87022468+cotovanu-cristian@users.noreply.github.com> Date: Thu, 21 May 2026 14:43:57 +0300 Subject: [PATCH 067/121] feat(agent): add Flow tool type for Maestro Flow processes (#1670) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 3 + .../uipath/tests/agent/models/test_agent.py | 69 +++++++++++++++++++ packages/uipath/uv.lock | 4 +- 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 36550f54d..0d70cb383 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.68" +version = "2.10.69" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index ba20b8165..8a4ad1ac7 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -112,6 +112,7 @@ class AgentToolType(str, CaseInsensitiveEnum): PROCESS = "Process" API = "Api" PROCESS_ORCHESTRATION = "ProcessOrchestration" + FLOW = "Flow" INTEGRATION = "Integration" INTERNAL = "Internal" IXP = "Ixp" @@ -779,6 +780,7 @@ class AgentProcessToolResourceConfig(BaseAgentToolResourceConfig): AgentToolType.PROCESS, AgentToolType.API, AgentToolType.PROCESS_ORCHESTRATION, + AgentToolType.FLOW, ] output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") properties: AgentProcessToolProperties @@ -1322,6 +1324,7 @@ def _normalize_resources(v: Dict[str, Any]) -> None: "process": "Process", "api": "Api", "processorchestration": "ProcessOrchestration", + "flow": "Flow", "integration": "Integration", "internal": "Internal", "ixp": "Ixp", diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 4daae105a..14ca8018d 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3558,6 +3558,75 @@ def test_process_tool_missing_output_schema(self): assert isinstance(tool_resource, AgentProcessToolResourceConfig) assert tool_resource.output_schema == {"type": "object", "properties": {}} + def test_flow_tool_type_enum_value(self): + """AgentToolType.FLOW exists with the wire value 'Flow' and is case-insensitive.""" + assert AgentToolType.FLOW.value == "Flow" + assert AgentToolType("flow") is AgentToolType.FLOW + assert AgentToolType("FLOW") is AgentToolType.FLOW + + def test_flow_tool_resource_deserialization(self): + """A resource with type='Flow' is parsed as AgentProcessToolResourceConfig.""" + resources = [ + { + "$resourceType": "tool", + "type": "Flow", + "id": "flow-tool-1", + "inputSchema": { + "type": "object", + "properties": {"input": {"type": "string"}}, + }, + "outputSchema": {"type": "object", "properties": {}}, + "arguments": {}, + "settings": {"timeout": 0, "maxAttempts": 0, "retryDelay": 0}, + "properties": { + "processName": "MyFlow", + "folderPath": "/Shared/Flows", + }, + "name": "Flow Tool", + "description": "Test Flow tool", + } + ] + + json_data = self._agent_dict_with_resources(resources) + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + tool_resource = config.resources[0] + assert isinstance(tool_resource, AgentProcessToolResourceConfig) + assert tool_resource.type == AgentToolType.FLOW + assert tool_resource.properties.process_name == "MyFlow" + assert tool_resource.properties.folder_path == "/Shared/Flows" + + def test_flow_tool_resource_case_insensitive(self): + """A resource with lowercase type='flow' also deserializes via CaseInsensitiveEnum.""" + resources = [ + { + "$resourceType": "tool", + "type": "flow", + "id": "flow-tool-2", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "arguments": {}, + "settings": {"timeout": 0, "maxAttempts": 0, "retryDelay": 0}, + "properties": { + "processName": "MyFlow", + "folderPath": "/Shared/Flows", + }, + "name": "Flow Tool", + "description": "Test Flow tool", + } + ] + + json_data = self._agent_dict_with_resources(resources) + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + tool_resource = config.resources[0] + assert isinstance(tool_resource, AgentProcessToolResourceConfig) + assert tool_resource.type == AgentToolType.FLOW + def test_escalation_missing_escalation_type_defaults_to_zero(self): """Test that missing escalationType defaults to 0.""" resources = [ diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 41ae12119..19b0d047b 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-17T17:25:34.9197064Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.68" +version = "2.10.69" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 7e3163b2676b18b7f44a126f39ba0250f16af2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ancu=C8=9Ba?= Date: Thu, 21 May 2026 16:39:49 +0300 Subject: [PATCH 068/121] feat: add arrayBuilder ArgumentProperties variant (#1672) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/agent/models/agent.py | 11 +++++++++++ packages/uipath/uv.lock | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 0d70cb383..8e9c9f581 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.69" +version = "2.10.70" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 8a4ad1ac7..72e7c438f 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -189,6 +189,7 @@ class AgentToolArgumentPropertiesVariant(str, CaseInsensitiveEnum): ARGUMENT = "argument" STATIC = "static" TEXT_BUILDER = "textBuilder" + ARRAY_BUILDER = "arrayBuilder" class TextTokenType(str, CaseInsensitiveEnum): @@ -277,11 +278,21 @@ class AgentToolTextBuilderArgumentProperties(BaseAgentToolArgumentProperties): tokens: List[TextToken] +class AgentToolArrayBuilderArgumentProperties(BaseCfg): + """Agent array builder argument properties model.""" + + variant: Literal[AgentToolArgumentPropertiesVariant.ARRAY_BUILDER] = Field( + default=AgentToolArgumentPropertiesVariant.ARRAY_BUILDER, + frozen=True, + ) + + AgentToolArgumentProperties = Annotated[ Union[ AgentToolStaticArgumentProperties, AgentToolArgumentArgumentProperties, AgentToolTextBuilderArgumentProperties, + AgentToolArrayBuilderArgumentProperties, ], Field(discriminator="variant"), _case_insensitive_enum_validator("variant", AgentToolArgumentPropertiesVariant), diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 19b0d047b..273bfe181 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.69" +version = "2.10.70" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 651b3b22568c4ce4ae4c09b094b4b688b6502ca5 Mon Sep 17 00:00:00 2001 From: Ion Mincu Date: Thu, 21 May 2026 17:02:33 +0300 Subject: [PATCH 069/121] fix(tracing): fall back to referenceId attribute when agentId is missing (#1673) Co-authored-by: Claude Opus 4.7 --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/_span_utils.py | 6 +- .../tests/services/test_span_utils.py | 77 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 67fa9de2e..9e82df730 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.54" +version = "0.1.55" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index 120c39b68..ab91b3623 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -297,7 +297,11 @@ def otel_span_to_uipath_span( # Top-level fields for internal tracing schema execution_type = attributes_dict.get("executionType") agent_version = attributes_dict.get("agentVersion") - reference_id = env.get("UIPATH_AGENT_ID") or attributes_dict.get("agentId") + reference_id = ( + env.get("UIPATH_AGENT_ID") + or attributes_dict.get("agentId") + or attributes_dict.get("referenceId") + ) verbosity_level = attributes_dict.get("verbosityLevel") # Source: override via uipath.source attribute, else DEFAULT_SOURCE diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index c10729d31..03f728eb8 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -89,6 +89,83 @@ def test_verbosity_level_omitted_when_unset(self) -> None: assert "VerbosityLevel" not in span_dict +class TestReferenceIdResolution: + """`reference_id` resolution chain. + + Priority: `UIPATH_AGENT_ID` env var > `agentId` attribute > `referenceId` + attribute. Falsy values (missing / empty string) at each step fall through + to the next source. The `referenceId` fallback exists for backwards + compatibility with older producers that only emit that attribute. + """ + + @pytest.mark.parametrize( + ("env_value", "attributes", "expected"), + [ + pytest.param( + "env-agent", + {"agentId": "attr-agent", "referenceId": "attr-ref"}, + "env-agent", + id="env-var-wins", + ), + pytest.param( + None, + {"agentId": "attr-agent", "referenceId": "attr-ref"}, + "attr-agent", + id="agent-id-attr-when-env-unset", + ), + pytest.param( + None, + {"referenceId": "attr-ref"}, + "attr-ref", + id="reference-id-fallback-when-agent-id-missing", + ), + pytest.param( + None, + {"agentId": "", "referenceId": "attr-ref"}, + "attr-ref", + id="reference-id-fallback-when-agent-id-empty", + ), + pytest.param( + None, + {}, + None, + id="none-when-all-sources-missing", + ), + ], + ) + def test_reference_id_chain( + self, + env_value: str | None, + attributes: dict[str, object], + expected: str | None, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + if env_value is None: + monkeypatch.delenv("UIPATH_AGENT_ID", raising=False) + else: + monkeypatch.setenv("UIPATH_AGENT_ID", env_value) + + mock_span = Mock(spec=OTelSpan) + mock_context = SpanContext( + trace_id=0x123456789ABCDEF0123456789ABCDEF0, + span_id=0x0123456789ABCDEF, + is_remote=False, + ) + mock_span.get_span_context.return_value = mock_context + mock_span.name = "test-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = attributes + mock_span.events = [] + mock_span.links = [] + now_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = now_ns + mock_span.end_time = now_ns + 1_000_000 + + uipath_span = _SpanUtils.otel_span_to_uipath_span(mock_span) + assert uipath_span.reference_id == expected + + class TestNormalizeIds: """Tests for OTEL ID normalization functions.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 777679614..ed99b6c36 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.54" +version = "0.1.55" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 273bfe181..72e9fde23 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.54" +version = "0.1.55" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 3bae6bb4319ebcb7b98b1a19e62e2d97ec37a8d9 Mon Sep 17 00:00:00 2001 From: Andrei Petraru Date: Fri, 22 May 2026 16:59:54 +0300 Subject: [PATCH 070/121] docs: add LangChain guardrails nav entry and remove deprecated PromptInjection [AL-437] (#1656) Co-authored-by: Claude Sonnet 4.6 --- packages/uipath/docs/core/guardrails.md | 45 ++++++------------------- packages/uipath/mkdocs.yml | 4 +++ 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/packages/uipath/docs/core/guardrails.md b/packages/uipath/docs/core/guardrails.md index a7e91f43f..4c380c20c 100644 --- a/packages/uipath/docs/core/guardrails.md +++ b/packages/uipath/docs/core/guardrails.md @@ -1,6 +1,6 @@ # Guardrails -Guardrails are safeguards applied before and/or after execution to inspect inputs and outputs for policy violations — PII, harmful content, prompt injection, intellectual property, and custom rules — and respond by logging, blocking, or modifying the data. +Guardrails are safeguards applied before and/or after execution to inspect inputs and outputs for policy violations — PII, harmful content, prompt attacks, intellectual property, and custom rules — and respond by logging, blocking, or modifying the data. They can be applied at three scopes: @@ -44,9 +44,11 @@ When using LangChain's `@tool`, `@guardrail` must be placed **above** `@tool`: from langchain_core.tools import tool @guardrail( - validator=PromptInjectionValidator(threshold=0.5), + validator=PIIValidator( + entities=[PIIDetectionEntity(PIIDetectionEntityType.EMAIL, threshold=0.5)] + ), action=BlockAction(), - name="No prompt injection", + name="No PII in tool input", stage=GuardrailExecutionStage.PRE, ) @tool # @guardrail wraps the already-decorated tool object @@ -58,9 +60,9 @@ def analyze_joke(joke: str) -> str: ```python @guardrail( - validator=PromptInjectionValidator(threshold=0.5), + validator=UserPromptAttacksValidator(), action=BlockAction(), - name="LLM Prompt Injection Detection", + name="LLM User Prompt Attacks Detection", stage=GuardrailExecutionStage.PRE, ) def create_llm(): @@ -92,7 +94,7 @@ The `stage` parameter controls when the guardrail evaluates. Not all validators | Stage | When evaluated | Supported by | |-------|---------------|--------------| | `PRE` | Before the function runs | All validators | -| `POST` | After the function runs | All except `PromptInjectionValidator`, `UserPromptAttacksValidator` | +| `POST` | After the function runs | All except `UserPromptAttacksValidator` | | `PRE_AND_POST` | Both before and after | `PIIValidator`, `HarmfulContentValidator`, `CustomValidator` | ## Built-in Validators @@ -156,28 +158,6 @@ def generate_response(prompt: str) -> str: ... ``` -### Prompt Injection - -Detects prompt injection attacks in user input. Restricted to `PRE` stage only — this is an input concern. - -```python -from uipath.platform.guardrails import ( - BlockAction, - GuardrailExecutionStage, - PromptInjectionValidator, - guardrail, -) - -@guardrail( - validator=PromptInjectionValidator(threshold=0.5), - action=BlockAction(), - name="No prompt injection", - stage=GuardrailExecutionStage.PRE, -) -def run_agent_step(user_input: str) -> str: - ... -``` - ### User Prompt Attacks Detects adversarial user prompt patterns (e.g. jailbreak attempts). No configuration parameters required. Restricted to `PRE` stage only. @@ -337,9 +317,9 @@ Multiple `@guardrail` decorators can be stacked on the same function. Each is ev ```python @guardrail( - validator=PromptInjectionValidator(), + validator=UserPromptAttacksValidator(), action=BlockAction(), - name="No injection", + name="No prompt attacks", stage=GuardrailExecutionStage.PRE, ) @guardrail( @@ -448,11 +428,6 @@ print(result.result, result.reason) members: - HarmfulContentValidator -::: uipath.platform.guardrails.decorators.validators.prompt_injection - options: - members: - - PromptInjectionValidator - ::: uipath.platform.guardrails.decorators.validators.intellectual_property options: members: diff --git a/packages/uipath/mkdocs.yml b/packages/uipath/mkdocs.yml index abffe1161..5b6a2678a 100644 --- a/packages/uipath/mkdocs.yml +++ b/packages/uipath/mkdocs.yml @@ -6,6 +6,9 @@ repo_url: https://github.com/UiPath/uipath-python copyright: Copyright © 2025 UiPath +hooks: + - docs/langchain/hooks/enum_lists.py + extra_css: - stylesheets/extra.css @@ -101,6 +104,7 @@ nav: - Getting Started: langchain/quick_start.md - Chat Models: langchain/chat_models.md - Context Grounding: langchain/context_grounding.md + - Guardrails: langchain/guardrails.md - Human In The Loop: langchain/human_in_the_loop.md - Sample Agents: https://github.com/UiPath/uipath-langchain-python/tree/main/samples - UiPath LlamaIndex SDK: From f396de9fe1d2d6ab764ac2dcc169b88f2077281f Mon Sep 17 00:00:00 2001 From: Andrei Petraru Date: Fri, 22 May 2026 18:26:25 +0300 Subject: [PATCH 071/121] docs(guardrails): add value tables to enum docstrings, remove MkDocs hook (#1676) Co-authored-by: Claude Sonnet 4.6 --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/guardrails/decorators/_enums.py | 38 +++++++++++++++++-- packages/uipath-platform/uv.lock | 4 +- packages/uipath/mkdocs.yml | 3 -- packages/uipath/uv.lock | 2 +- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 9e82df730..cb9ca69b4 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.55" +version = "0.1.56" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py index 49956f62f..c88df5acd 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/decorators/_enums.py @@ -19,8 +19,27 @@ class GuardrailExecutionStage(str, Enum): class PIIDetectionEntityType(str, Enum): """PII detection entity types supported by UiPath guardrails. - These entities match the available options from the UiPath guardrails service - backend. The enum values correspond to the exact strings expected by the API. + | Value | + |---| + | `PERSON` | + | `ADDRESS` | + | `DATE` | + | `PHONE_NUMBER` | + | `EUGPS_COORDINATES` | + | `EMAIL` | + | `CREDIT_CARD_NUMBER` | + | `INTERNATIONAL_BANKING_ACCOUNT_NUMBER` | + | `SWIFT_CODE` | + | `ABA_ROUTING_NUMBER` | + | `US_DRIVERS_LICENSE_NUMBER` | + | `UK_DRIVERS_LICENSE_NUMBER` | + | `US_INDIVIDUAL_TAXPAYER_IDENTIFICATION` | + | `UK_UNIQUE_TAXPAYER_NUMBER` | + | `US_BANK_ACCOUNT_NUMBER` | + | `US_SOCIAL_SECURITY_NUMBER` | + | `USUK_PASSPORT_NUMBER` | + | `URL` | + | `IP_ADDRESS` | """ PERSON = "Person" @@ -47,7 +66,12 @@ class PIIDetectionEntityType(str, Enum): class HarmfulContentEntityType(str, Enum): """Harmful content entity types supported by UiPath guardrails. - These entities correspond to the Azure Content Safety categories. + | Value | + |---| + | `HATE` | + | `SELF_HARM` | + | `SEXUAL` | + | `VIOLENCE` | """ HATE = "Hate" @@ -57,7 +81,13 @@ class HarmfulContentEntityType(str, Enum): class IntellectualPropertyEntityType(str, Enum): - """Intellectual property entity types supported by UiPath guardrails.""" + """Intellectual property entity types supported by UiPath guardrails. + + | Value | + |---| + | `TEXT` | + | `CODE` | + """ TEXT = "Text" CODE = "Code" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index ed99b6c36..53728076b 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-17T16:21:45.1868663Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.55" +version = "0.1.56" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/mkdocs.yml b/packages/uipath/mkdocs.yml index 5b6a2678a..de3b9a278 100644 --- a/packages/uipath/mkdocs.yml +++ b/packages/uipath/mkdocs.yml @@ -6,9 +6,6 @@ repo_url: https://github.com/UiPath/uipath-python copyright: Copyright © 2025 UiPath -hooks: - - docs/langchain/hooks/enum_lists.py - extra_css: - stylesheets/extra.css diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 72e9fde23..d29f9a146 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.55" +version = "0.1.56" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From b5d462ac384c8de8463a2c454d27e65caa494d78 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Fri, 22 May 2026 19:42:41 +0300 Subject: [PATCH 072/121] fix(uipath-core): accept minimal conversation message envelope on input (#1677) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/chat/content.py | 13 ++-- .../src/uipath/core/chat/message.py | 17 ++++-- packages/uipath-core/tests/chat/__init__.py | 0 .../uipath-core/tests/chat/test_message.py | 59 +++++++++++++++++++ packages/uipath-core/uv.lock | 4 +- packages/uipath-platform/uv.lock | 4 +- packages/uipath/uv.lock | 4 +- 8 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 packages/uipath-core/tests/chat/__init__.py create mode 100644 packages/uipath-core/tests/chat/test_message.py diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 959122a11..147726394 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.15" +version = "0.5.16" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/chat/content.py b/packages/uipath-core/src/uipath/core/chat/content.py index cc6300490..4ae8b169c 100644 --- a/packages/uipath-core/src/uipath/core/chat/content.py +++ b/packages/uipath-core/src/uipath/core/chat/content.py @@ -2,6 +2,7 @@ from __future__ import annotations +import uuid from typing import Any, Sequence from pydantic import BaseModel, ConfigDict, Field @@ -95,7 +96,7 @@ class UiPathConversationContentPartData(BaseModel): mime_type: str = Field(..., alias="mimeType") data: InlineOrExternal - citations: Sequence[UiPathConversationCitationData] + citations: Sequence[UiPathConversationCitationData] = Field(default_factory=list) is_transcript: bool | None = Field(None, alias="isTranscript") is_incomplete: bool | None = Field(None, alias="isIncomplete") name: str | None = None @@ -106,11 +107,13 @@ class UiPathConversationContentPartData(BaseModel): class UiPathConversationContentPart(UiPathConversationContentPartData): """Represents a single part of message content.""" - content_part_id: str = Field(..., alias="contentPartId") - created_at: str = Field(..., alias="createdAt") - updated_at: str = Field(..., alias="updatedAt") + content_part_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), alias="contentPartId" + ) + created_at: str | None = Field(None, alias="createdAt") + updated_at: str | None = Field(None, alias="updatedAt") # Override to use full type - citations: Sequence[UiPathConversationCitation] + citations: Sequence[UiPathConversationCitation] = Field(default_factory=list) model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/message.py b/packages/uipath-core/src/uipath/core/chat/message.py index 9d6aa248d..37aa2bd76 100644 --- a/packages/uipath-core/src/uipath/core/chat/message.py +++ b/packages/uipath-core/src/uipath/core/chat/message.py @@ -1,5 +1,6 @@ """Message-level events.""" +import uuid from typing import Any, Sequence from pydantic import BaseModel, ConfigDict, Field @@ -61,7 +62,9 @@ class UiPathConversationMessageData(BaseModel): content_parts: Sequence[UiPathConversationContentPartData] = Field( ..., alias="contentParts" ) - tool_calls: Sequence[UiPathConversationToolCallData] = Field(..., alias="toolCalls") + tool_calls: Sequence[UiPathConversationToolCallData] = Field( + default_factory=list, alias="toolCalls" + ) model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -69,15 +72,19 @@ class UiPathConversationMessageData(BaseModel): class UiPathConversationMessage(UiPathConversationMessageData): """Represents a single message within an exchange.""" - message_id: str = Field(..., alias="messageId") - created_at: str = Field(..., alias="createdAt") - updated_at: str = Field(..., alias="updatedAt") + message_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), alias="messageId" + ) + created_at: str | None = Field(None, alias="createdAt") + updated_at: str | None = Field(None, alias="updatedAt") span_id: str | None = Field(None, alias="spanId") # Overrides to use full types content_parts: Sequence[UiPathConversationContentPart] = Field( ..., alias="contentParts" ) - tool_calls: Sequence[UiPathConversationToolCall] = Field(..., alias="toolCalls") + tool_calls: Sequence[UiPathConversationToolCall] = Field( + default_factory=list, alias="toolCalls" + ) model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/tests/chat/__init__.py b/packages/uipath-core/tests/chat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/uipath-core/tests/chat/test_message.py b/packages/uipath-core/tests/chat/test_message.py new file mode 100644 index 000000000..113d34a66 --- /dev/null +++ b/packages/uipath-core/tests/chat/test_message.py @@ -0,0 +1,59 @@ +"""Tests for `UiPathConversationMessage` input validation. + +Conversational Agent Service contract treats `role` + `contentParts` as +the load-bearing fields for an inbound user message. `messageId` and +`contentPartId` are GUIDs that identify entities in the conversation +hierarchy; when omitted on input, the model fills them with fresh +UUIDs (matching what `uipath dev` does server-side). `createdAt`, +`updatedAt`, `spanId`, and `toolCalls` are server-allocated and absent +from client input. + +These tests pin that behavior so `--input-file` payloads from +`uip codedagent run` validate against the model without requiring +callers to hand-generate UUIDs. +""" + +from __future__ import annotations + +from uipath.core.chat import UiPathConversationMessage + + +def test_minimal_user_message_validates_and_fills_ids() -> None: + msg = UiPathConversationMessage.model_validate( + { + "role": "user", + "contentParts": [ + { + "mimeType": "text/plain", + "data": {"inline": "hello world"}, + } + ], + } + ) + assert msg.role == "user" + assert msg.tool_calls == [] + assert msg.created_at is None + assert msg.updated_at is None + assert msg.message_id # auto-generated UUID + assert msg.content_parts[0].content_part_id # auto-generated UUID + assert msg.content_parts[0].citations == [] + + +def test_explicit_ids_are_preserved() -> None: + msg = UiPathConversationMessage.model_validate( + { + "messageId": "00000000-0000-0000-0000-000000000001", + "role": "user", + "contentParts": [ + { + "contentPartId": "00000000-0000-0000-0000-000000000002", + "mimeType": "text/plain", + "data": {"inline": "hello world"}, + } + ], + } + ) + assert msg.message_id == "00000000-0000-0000-0000-000000000001" + assert ( + msg.content_parts[0].content_part_id == "00000000-0000-0000-0000-000000000002" + ) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index ab6e6aa14..9b043599c 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-20T15:11:06.1716446Z" exclude-newer-span = "P2D" [[package]] @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.15" +version = "0.5.16" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 53728076b..c3b60638d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-20T15:43:10.4544027Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.15" +version = "0.5.16" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d29f9a146..449f50872 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-20T15:14:33.9075119Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.15" +version = "0.5.16" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 81338db66c5f4c6706195bc475699fcb77700f34 Mon Sep 17 00:00:00 2001 From: avichalsri24 Date: Mon, 25 May 2026 15:51:51 +0530 Subject: [PATCH 073/121] feat: expand Data Fabric entities service [DS-8360] (#1616) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/entities/__init__.py | 44 +- .../platform/entities/_entities_service.py | 1948 ++++++++++++----- .../platform/entities/_entity_data_service.py | 1403 ++++++++++++ .../entities/_entity_schema_service.py | 522 +++++ .../src/uipath/platform/entities/entities.py | 456 +++- .../tests/services/test_entities_service.py | 1603 +++++++++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 9 files changed, 5337 insertions(+), 645 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/entities/_entity_data_service.py create mode 100644 packages/uipath-platform/src/uipath/platform/entities/_entity_schema_service.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index cb9ca69b4..ba264c1ba 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.56" +version = "0.1.57" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/entities/__init__.py b/packages/uipath-platform/src/uipath/platform/entities/__init__.py index aca80997b..67572ec87 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/entities/__init__.py @@ -5,42 +5,78 @@ from ._entities_service import EntitiesService from .entities import ( + AggregateRow, ChoiceSetValue, DataFabricEntityItem, Entity, + EntityAggregate, + EntityAggregateFunction, + EntityBinning, + EntityCreateFieldOptions, + EntityCreateOptions, EntityField, + EntityFieldDataType, EntityFieldMetadata, + EntityImportRecordsResponse, + EntityJoin, + EntityMetadataUpdateOptions, + EntityQueryFilter, + EntityQueryFilterGroup, + EntityQuerySortOption, EntityRecord, EntityRecordsBatchResponse, + EntityRecordsListResponse, EntityRouting, EntitySetResolution, ExternalField, ExternalObject, ExternalSourceFields, + FailureRecord, FieldDataType, FieldMetadata, + LogicalOperator, + QueryFilterOperator, QueryRoutingOverrideContext, ReferenceType, + RetrieveEntityRecordsResponse, SourceJoinCriteria, ) __all__ = [ + "AggregateRow", "ChoiceSetValue", "DataFabricEntityItem", "EntitiesService", "Entity", + "EntityAggregate", + "EntityAggregateFunction", + "EntityBinning", + "EntityCreateFieldOptions", + "EntityCreateOptions", "EntityField", - "EntityRecord", + "EntityFieldDataType", "EntityFieldMetadata", + "EntityImportRecordsResponse", + "EntityJoin", + "EntityMetadataUpdateOptions", + "EntityQueryFilter", + "EntityQueryFilterGroup", + "EntityQuerySortOption", + "EntityRecord", + "EntityRecordsBatchResponse", + "EntityRecordsListResponse", "EntityRouting", "EntitySetResolution", - "FieldDataType", - "FieldMetadata", - "EntityRecordsBatchResponse", "ExternalField", "ExternalObject", "ExternalSourceFields", + "FailureRecord", + "FieldDataType", + "FieldMetadata", + "LogicalOperator", + "QueryFilterOperator", "QueryRoutingOverrideContext", "ReferenceType", + "RetrieveEntityRecordsResponse", "SourceJoinCriteria", ] diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py index 951a4b07b..fa3c0da9e 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py +++ b/packages/uipath-platform/src/uipath/platform/entities/_entities_service.py @@ -1,22 +1,32 @@ -import json as json_module +"""Public facade for the Data Fabric entities surface. + +:class:`EntitiesService` keeps the existing ``sdk.entities.*`` API flat and +unchanged from a caller's perspective while delegating each operation to the +appropriate underlying service: + +* :class:`EntitySchemaService` — entity definitions, choice set listings, + create / delete / update-metadata lifecycle. +* :class:`EntityDataService` — record CRUD (single and batch), structured + queries, attachments, choice-set values, bulk import, and federated SQL + queries. + +The facade additionally owns cross-cutting concerns such as agent entity-set +resolution. +""" + import logging from typing import Any, Dict, List, Optional, Type -import sqlparse from httpx import Response -from sqlparse.sql import Function, Identifier, IdentifierList, Parenthesis, Where -from sqlparse.tokens import DML, Keyword, Whitespace, Wildcard from uipath.core.tracing import traced from ..common._base_service import BaseService from ..common._bindings import _resource_overwrites from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext -from ..common._models import Endpoint, RequestSpec -from ..common.constants import HEADER_FOLDER_KEY from ..orchestrator._folder_service import FolderService +from ._entity_data_service import EntityDataService, FileContent from ._entity_resolution import ( - RoutingStrategy, build_resolution_service, create_resolution_plan, create_resolution_plan_async, @@ -24,46 +34,45 @@ fetch_resolved_entities, fetch_resolved_entities_async, ) +from ._entity_schema_service import EntitySchemaService from .entities import ( ChoiceSetValue, DataFabricEntityItem, Entity, + EntityAggregate, + EntityBinning, + EntityCreateFieldOptions, + EntityCreateOptions, + EntityImportRecordsResponse, + EntityJoin, + EntityMetadataUpdateOptions, + EntityQueryFilterGroup, + EntityQuerySortOption, EntityRecord, EntityRecordsBatchResponse, + EntityRecordsListResponse, EntitySetResolution, QueryRoutingOverrideContext, + RetrieveEntityRecordsResponse, ) logger = logging.getLogger(__name__) -_FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"} -_FORBIDDEN_DDL = {"DROP", "ALTER", "CREATE", "TRUNCATE"} -_DISALLOWED_KEYWORDS = [ - "WITH", - "UNION", - "INTERSECT", - "EXCEPT", - "OVER", - "ROLLUP", - "CUBE", - "GROUPING", - "PARTITION", -] -_AGGREGATE_FUNCTIONS = ("COUNT", "SUM", "AVG", "MIN", "MAX") - class EntitiesService(BaseService): """Service for managing UiPath Data Service entities. - Entities are database tables in UiPath Data Service that can store - structured data for automation processes. + Entities are database tables in UiPath Data Service that store structured + data for automation processes. This service is the unified entry point for + every entity operation: schema management, record CRUD, structured and + SQL queries, file attachments, choice sets, and bulk import. See Also: https://docs.uipath.com/data-service/automation-cloud/latest/user-guide/introduction !!! warning "Preview Feature" - This function is currently experimental. - Behavior and parameters are subject to change in future versions. + This service is currently experimental. Behavior and parameters are + subject to change in future versions. """ def __init__( @@ -75,14 +84,30 @@ def __init__( entity_name_overrides: Optional[Dict[str, str]] = None, routing_context: Optional[QueryRoutingOverrideContext] = None, ) -> None: + """Initialise the facade and its underlying schema and data services.""" super().__init__(config=config, execution_context=execution_context) self._folders_service = folders_service - self._routing_strategy: RoutingStrategy = create_routing_strategy( + self._routing_strategy = create_routing_strategy( folders_map=folders_map, effective_entity_names=entity_name_overrides, routing_context=routing_context, folders_service=folders_service, ) + self._schema = EntitySchemaService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + ) + self._data = EntityDataService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + routing_strategy=self._routing_strategy, + ) + + # ------------------------------------------------------------------ + # Schema operations — delegate to EntitySchemaService + # ------------------------------------------------------------------ @traced(name="entity_retrieve", run_type="uipath") def retrieve(self, entity_key: str) -> Entity: @@ -117,10 +142,7 @@ def retrieve(self, entity_key: str) -> Entity: print(f" Required: {field.is_required}") print(f" Primary Key: {field.is_primary_key}") """ - spec = self._retrieve_spec(entity_key) - response = self.request(spec.method, spec.endpoint) - - return Entity.model_validate(response.json()) + return self._schema.retrieve(entity_key) @traced(name="entity_retrieve", run_type="uipath") async def retrieve_async(self, entity_key: str) -> Entity: @@ -155,11 +177,7 @@ async def retrieve_async(self, entity_key: str) -> Entity: print(f" Required: {field.is_required}") print(f" Primary Key: {field.is_primary_key}") """ - spec = self._retrieve_spec(entity_key) - - response = await self.request_async(spec.method, spec.endpoint) - - return Entity.model_validate(response.json()) + return await self._schema.retrieve_async(entity_key) @traced(name="entity_retrieve_by_name", run_type="uipath") def retrieve_by_name( @@ -175,11 +193,7 @@ def retrieve_by_name( entity_name: The name of the entity. folder_key: Optional folder key for disambiguation. """ - spec = self._retrieve_by_name_spec(entity_name) - headers = self._folder_key_headers(folder_key) - response = self.request(spec.method, spec.endpoint, headers=headers) - - return Entity.model_validate(response.json()) + return self._schema.retrieve_by_name(entity_name, folder_key=folder_key) @traced(name="entity_retrieve_by_name", run_type="uipath") async def retrieve_by_name_async( @@ -195,11 +209,9 @@ async def retrieve_by_name_async( entity_name: The name of the entity. folder_key: Optional folder key for disambiguation. """ - spec = self._retrieve_by_name_spec(entity_name) - headers = self._folder_key_headers(folder_key) - response = await self.request_async(spec.method, spec.endpoint, headers=headers) - - return Entity.model_validate(response.json()) + return await self._schema.retrieve_by_name_async( + entity_name, folder_key=folder_key + ) @traced(name="list_entities", run_type="uipath") def list_entities(self) -> List[Entity]: @@ -238,11 +250,7 @@ def list_entities(self) -> List[Entity]: print(f"Total records: {total_records}") print(f"Total storage: {total_storage:.2f} MB") """ - spec = self._list_entities_spec() - response = self.request(spec.method, spec.endpoint) - - entities_data = response.json() - return [Entity.model_validate(entity) for entity in entities_data] + return self._schema.list_entities() @traced(name="list_entities", run_type="uipath") async def list_entities_async(self) -> List[Entity]: @@ -281,11 +289,7 @@ async def list_entities_async(self) -> List[Entity]: print(f"Total records: {total_records}") print(f"Total storage: {total_storage:.2f} MB") """ - spec = self._list_entities_spec() - response = await self.request_async(spec.method, spec.endpoint) - - entities_data = response.json() - return [Entity.model_validate(entity) for entity in entities_data] + return await self._schema.list_entities_async() @traced(name="list_choicesets", run_type="uipath") def list_choicesets(self) -> List[Entity]: @@ -301,9 +305,7 @@ def list_choicesets(self) -> List[Entity]: for cs in choicesets: print(f"{cs.display_name} ({cs.id})") """ - spec = self._list_choicesets_spec() - response = self.request(spec.method, spec.endpoint) - return [Entity.model_validate(item) for item in response.json()] + return self._schema.list_choicesets() @traced(name="list_choicesets", run_type="uipath") async def list_choicesets_async(self) -> List[Entity]: @@ -312,16 +314,215 @@ async def list_choicesets_async(self) -> List[Entity]: Returns: List[Entity]: A list of all choice set entities. """ - spec = self._list_choicesets_spec() - response = await self.request_async(spec.method, spec.endpoint) - return [Entity.model_validate(item) for item in response.json()] + return await self._schema.list_choicesets_async() + + @traced(name="entity_create", run_type="uipath") + def create_entity( + self, + name: str, + fields: List[EntityCreateFieldOptions], + options: Optional[EntityCreateOptions] = None, + ) -> str: + """Create a new entity with the given schema and return its id. + + Args: + name (str): Entity name. Must start with a letter and contain + only letters, digits, and underscores (3-100 characters). + fields (List[EntityCreateFieldOptions]): Field definitions for + the new entity. Each entry declares the field's name, type, + and optional constraints such as ``length_limit``, + ``decimal_precision``, ``is_required``, ``is_unique``, etc. + options (Optional[EntityCreateOptions]): Optional entity-level + settings such as display name, description, folder + placement, and RBAC / analytics flags. + + Returns: + str: The id (UUID) of the newly created entity. + + Raises: + ValueError: If the entity name or any field name fails the + client-side validation (regex / length / reserved names) or + if a per-field constraint is not supported for that field + type or is out of range. + + Examples: + Create a simple entity:: + + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityCreateOptions, + EntityFieldDataType, + ) + + entity_id = entities_service.create_entity( + "ProductCatalog", + [ + EntityCreateFieldOptions( + field_name="product_name", + type=EntityFieldDataType.STRING, + is_required=True, + is_unique=True, + ), + EntityCreateFieldOptions( + field_name="price", + type=EntityFieldDataType.DECIMAL, + decimal_precision=2, + ), + ], + options=EntityCreateOptions( + display_name="Product Catalog", + description="Inventory of available products", + is_rbac_enabled=True, + ), + ) + """ + return self._schema.create_entity(name, fields, options) + + @traced(name="entity_create", run_type="uipath") + async def create_entity_async( + self, + name: str, + fields: List[EntityCreateFieldOptions], + options: Optional[EntityCreateOptions] = None, + ) -> str: + """Asynchronously create a new entity with the given schema. + + Args: + name (str): Entity name; same validation rules as :meth:`create_entity`. + fields (List[EntityCreateFieldOptions]): Field definitions. + options (Optional[EntityCreateOptions]): Optional entity-level settings. + + Returns: + str: The id (UUID) of the newly created entity. + + Raises: + ValueError: For client-side validation failures. + + Examples: + Create a simple entity:: + + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + entity_id = await entities_service.create_entity_async( + "ProductCatalog", + [ + EntityCreateFieldOptions( + field_name="product_name", + type=EntityFieldDataType.STRING, + is_required=True, + ), + ], + ) + """ + return await self._schema.create_entity_async(name, fields, options) + + @traced(name="entity_delete", run_type="uipath") + def delete_entity(self, entity_id: str) -> None: + """Delete an entity and all of its records. + + Args: + entity_id (str): The unique identifier of the entity to delete. + + Examples: + Delete an entity by id:: + + entities_service.delete_entity("a1b2c3d4-...") + """ + self._schema.delete_entity(entity_id) + + @traced(name="entity_delete", run_type="uipath") + async def delete_entity_async(self, entity_id: str) -> None: + """Asynchronously delete an entity and all of its records. + + Args: + entity_id (str): The unique identifier of the entity to delete. + + Examples: + Delete an entity by id:: + + await entities_service.delete_entity_async("a1b2c3d4-...") + """ + await self._schema.delete_entity_async(entity_id) + + @traced(name="entity_update_metadata", run_type="uipath") + def update_entity_metadata( + self, + entity_id: str, + metadata: EntityMetadataUpdateOptions | Dict[str, Any], + ) -> None: + """Update an entity's display name, description, and/or RBAC flag. + + Args: + entity_id (str): The unique identifier of the entity. + metadata (EntityMetadataUpdateOptions | Dict[str, Any]): + An :class:`EntityMetadataUpdateOptions` instance or a dict + with any of ``display_name``, ``description``, + ``is_rbac_enabled``. Dict keys may be snake_case + (``display_name``) or camelCase (``displayName``); both + serialize correctly to the API. + + Examples: + Rename and update description:: + + from uipath.platform.entities import EntityMetadataUpdateOptions + + entities_service.update_entity_metadata( + "a1b2c3d4-...", + EntityMetadataUpdateOptions( + display_name="New Display Name", + description="Refreshed description", + ), + ) + + From a plain dict:: + + entities_service.update_entity_metadata( + "a1b2c3d4-...", + {"display_name": "X", "is_rbac_enabled": True}, + ) + """ + self._schema.update_entity_metadata(entity_id, metadata) + + @traced(name="entity_update_metadata", run_type="uipath") + async def update_entity_metadata_async( + self, + entity_id: str, + metadata: EntityMetadataUpdateOptions | Dict[str, Any], + ) -> None: + """Asynchronously update an entity's display name, description, and/or RBAC flag. + + Args: + entity_id (str): The unique identifier of the entity. + metadata (EntityMetadataUpdateOptions | Dict[str, Any]): + An :class:`EntityMetadataUpdateOptions` instance or a dict + with any of ``display_name``, ``description``, + ``is_rbac_enabled``. + + Examples: + Rename:: + + from uipath.platform.entities import EntityMetadataUpdateOptions + + await entities_service.update_entity_metadata_async( + "a1b2c3d4-...", + EntityMetadataUpdateOptions(display_name="Renamed Entity"), + ) + """ + await self._schema.update_entity_metadata_async(entity_id, metadata) + + # ------------------------------------------------------------------ + # Data operations — delegate to EntityDataService + # ------------------------------------------------------------------ @traced(name="get_choiceset_values", run_type="uipath") def get_choiceset_values( self, choiceset_id: str, - start: int | None = None, - limit: int | None = None, + start: Optional[int] = None, + limit: Optional[int] = None, ) -> List[ChoiceSetValue]: """Get the values of a choice set by its ID. @@ -341,23 +542,14 @@ def get_choiceset_values( for v in values: print(f"{v.number_id}: {v.display_name}") """ - spec = self._get_choiceset_values_spec(choiceset_id, start=start, limit=limit) - response = self.request( - spec.method, spec.endpoint, params=spec.params, json=spec.json - ) - data = response.json() - raw_values = data.get("jsonValue", "[]") - items = ( - json_module.loads(raw_values) if isinstance(raw_values, str) else raw_values - ) - return [ChoiceSetValue.model_validate(item) for item in items] + return self._data.get_choiceset_values(choiceset_id, start=start, limit=limit) @traced(name="get_choiceset_values", run_type="uipath") async def get_choiceset_values_async( self, choiceset_id: str, - start: int | None = None, - limit: int | None = None, + start: Optional[int] = None, + limit: Optional[int] = None, ) -> List[ChoiceSetValue]: """Asynchronously get the values of a choice set by its ID. @@ -369,25 +561,23 @@ async def get_choiceset_values_async( Returns: List[ChoiceSetValue]: The values in the choice set. """ - spec = self._get_choiceset_values_spec(choiceset_id, start=start, limit=limit) - response = await self.request_async( - spec.method, spec.endpoint, params=spec.params, json=spec.json + return await self._data.get_choiceset_values_async( + choiceset_id, start=start, limit=limit ) - data = response.json() - raw_values = data.get("jsonValue", "[]") - items = ( - json_module.loads(raw_values) if isinstance(raw_values, str) else raw_values - ) - return [ChoiceSetValue.model_validate(item) for item in items] @traced(name="entity_list_records", run_type="uipath") def list_records( self, entity_key: str, - schema: Optional[Type[Any]] = None, # Optional schema + schema: Optional[Type[Any]] = None, start: Optional[int] = None, limit: Optional[int] = None, - ) -> List[EntityRecord]: + expansion_level: Optional[int] = None, + filter: Optional[str] = None, + orderby: Optional[str] = None, + select: Optional[List[str]] = None, + expand: Optional[List[str]] = None, + ) -> EntityRecordsListResponse: """List records from an entity with optional pagination and schema validation. The schema parameter enables type-safe access to entity records by validating the @@ -424,11 +614,23 @@ class CustomerRecord: start (Optional[int]): Starting index for pagination (0-based). limit (Optional[int]): Maximum number of records to return. + expansion_level (Optional[int]): Depth of foreign-key expansion in the + response (``0`` means no expansion). Higher values inline related + records up to that many hops. + filter (Optional[str]): OData ``$filter`` expression + (e.g. ``"status eq 'active'"``). + orderby (Optional[str]): OData ``$orderby`` expression + (e.g. ``"created_at desc"``). + select (Optional[List[str]]): Column projection — field names to + include (rendered as ``$select``). + expand (Optional[List[str]]): Relationship names to expand inline + (rendered as ``$expand``). Returns: - List[EntityRecord]: A list of entity records. Each record contains an 'id' field - and all other fields from the entity. Fields can be accessed as attributes - or dictionary keys on the EntityRecord object. + EntityRecordsListResponse: A list-compatible response with + ``total_count``, ``has_next_page`` and ``next_cursor`` pagination + metadata. Iteration, indexing, and ``len()`` continue to work + like a plain list of :class:`EntityRecord`. Raises: ValueError: If schema validation fails for any record, including cases where @@ -446,6 +648,22 @@ class CustomerRecord: # Get first 50 records records = entities_service.list_records("Customers", start=0, limit=50) + print(f"Showing {len(records)} of {records.total_count} total") + if records.has_next_page: + next_page = entities_service.list_records( + "Customers", start=50, limit=50 + ) + + With OData filter, sorting, projection, and expansion:: + + records = entities_service.list_records( + "Customers", + filter="status eq 'active'", + orderby="created_at desc", + select=["name", "email", "status"], + expand=["company"], + expansion_level=1, + ) With schema validation:: @@ -465,28 +683,31 @@ class CustomerRecord: for record in records: print(f"{record.name}: {record.email}") """ - # Example method to generate the API request specification (mocked here) - spec = self._list_records_spec(entity_key, start, limit) - - # Make the HTTP request (assumes self.request exists) - response = self.request(spec.method, spec.endpoint, params=spec.params) - - # Parse the response JSON and extract the "value" field - records_data = response.json().get("value", []) - - # Validate and wrap records - return [ - EntityRecord.from_data(data=record, model=schema) for record in records_data - ] + return self._data.list_records( + entity_key, + schema=schema, + start=start, + limit=limit, + expansion_level=expansion_level, + filter=filter, + orderby=orderby, + select=select, + expand=expand, + ) @traced(name="entity_list_records", run_type="uipath") async def list_records_async( self, entity_key: str, - schema: Optional[Type[Any]] = None, # Optional schema + schema: Optional[Type[Any]] = None, start: Optional[int] = None, limit: Optional[int] = None, - ) -> List[EntityRecord]: + expansion_level: Optional[int] = None, + filter: Optional[str] = None, + orderby: Optional[str] = None, + select: Optional[List[str]] = None, + expand: Optional[List[str]] = None, + ) -> EntityRecordsListResponse: """Asynchronously list records from an entity with optional pagination and schema validation. The schema parameter enables type-safe access to entity records by validating the @@ -523,11 +744,23 @@ class CustomerRecord: start (Optional[int]): Starting index for pagination (0-based). limit (Optional[int]): Maximum number of records to return. + expansion_level (Optional[int]): Depth of foreign-key expansion in the + response (``0`` means no expansion). Higher values inline related + records up to that many hops. + filter (Optional[str]): OData ``$filter`` expression + (e.g. ``"status eq 'active'"``). + orderby (Optional[str]): OData ``$orderby`` expression + (e.g. ``"created_at desc"``). + select (Optional[List[str]]): Column projection — field names to + include (rendered as ``$select``). + expand (Optional[List[str]]): Relationship names to expand inline + (rendered as ``$expand``). Returns: - List[EntityRecord]: A list of entity records. Each record contains an 'id' field - and all other fields from the entity. Fields can be accessed as attributes - or dictionary keys on the EntityRecord object. + EntityRecordsListResponse: A list-compatible response with + ``total_count``, ``has_next_page`` and ``next_cursor`` pagination + metadata. Iteration, indexing, and ``len()`` continue to work + like a plain list of :class:`EntityRecord`. Raises: ValueError: If schema validation fails for any record, including cases where @@ -545,6 +778,22 @@ class CustomerRecord: # Get first 50 records records = await entities_service.list_records_async("Customers", start=0, limit=50) + print(f"Showing {len(records)} of {records.total_count} total") + if records.has_next_page: + next_page = await entities_service.list_records_async( + "Customers", start=50, limit=50 + ) + + With OData filter, sorting, projection, and expansion:: + + records = await entities_service.list_records_async( + "Customers", + filter="status eq 'active'", + orderby="created_at desc", + select=["name", "email", "status"], + expand=["company"], + expansion_level=1, + ) With schema validation:: @@ -564,193 +813,330 @@ class CustomerRecord: for record in records: print(f"{record.name}: {record.email}") """ - spec = self._list_records_spec(entity_key, start, limit) - - # Make the HTTP request (assumes self.request exists) - response = await self.request_async( - spec.method, spec.endpoint, params=spec.params + return await self._data.list_records_async( + entity_key, + schema=schema, + start=start, + limit=limit, + expansion_level=expansion_level, + filter=filter, + orderby=orderby, + select=select, + expand=expand, ) - # Parse the response JSON and extract the "value" field - records_data = response.json().get("value", []) - - # Validate and wrap records - return [ - EntityRecord.from_data(data=record, model=schema) for record in records_data - ] - - @traced(name="entity_query_records", run_type="uipath") - def query_entity_records( + @traced(name="entity_insert_record", run_type="uipath") + def insert_record( self, - sql_query: str, - ) -> List[Dict[str, Any]]: - """Query entity records using a validated SQL query. + entity_key: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Insert a single record into an entity and return the inserted row. - PREVIEW: This method is in preview and may change in future releases. + Note: + Unlike :meth:`insert_records` (batch), this single-record endpoint + fires Data Fabric trigger events. Use this method when triggers + attached to the entity must run. Args: - sql_query (str): A SQL SELECT query to execute against Data Service entities. - Only SELECT statements are allowed. Queries without WHERE must include - a LIMIT clause. Subqueries and multi-statement queries are not permitted. - - Notes: - A routing context is always derived from the configured ``folders_map`` - when present and included in the request body. + entity_key (str): The unique key/identifier of the entity. + data (Any): Record payload — a dict, a Pydantic model, an + :class:`EntityRecord`, or any object exposing ``__dict__``. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). Returns: - List[Dict[str, Any]]: A list of result records as dictionaries. + EntityRecord: The inserted record with its server-assigned ``Id`` + plus any expanded relationships. - Raises: - ValueError: If the SQL query fails validation (e.g., non-SELECT, missing - WHERE/LIMIT, forbidden keywords, subqueries). + Examples: + Insert from a dict:: + + record = entities_service.insert_record( + "Customers", + {"name": "Alice", "email": "alice@example.com"}, + ) + print(record.id) + + Insert from a Pydantic model:: + + class CustomerInput(BaseModel): + name: str + email: str + + record = entities_service.insert_record( + "Customers", + CustomerInput(name="Bob", email="bob@example.com"), + expansion_level=1, + ) """ - return self._query_entities_for_records(sql_query) + return self._data.insert_record( + entity_key, data, expansion_level=expansion_level + ) - @traced(name="entity_query_records", run_type="uipath") - async def query_entity_records_async( + @traced(name="entity_insert_record", run_type="uipath") + async def insert_record_async( self, - sql_query: str, - ) -> List[Dict[str, Any]]: - """Asynchronously query entity records using a validated SQL query. + entity_key: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Asynchronously insert a single record into an entity. - PREVIEW: This method is in preview and may change in future releases. + Note: + Unlike :meth:`insert_records_async` (batch), this single-record + endpoint fires Data Fabric trigger events. Use this method when + triggers attached to the entity must run. Args: - sql_query (str): A SQL SELECT query to execute against Data Service entities. - Only SELECT statements are allowed. Queries without WHERE must include - a LIMIT clause. Subqueries and multi-statement queries are not permitted. - - Notes: - A routing context is always derived from the configured ``folders_map`` - when present and included in the request body. + entity_key (str): The unique key/identifier of the entity. + data (Any): Record payload — a dict, a Pydantic model, an + :class:`EntityRecord`, or any object exposing ``__dict__``. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). Returns: - List[Dict[str, Any]]: A list of result records as dictionaries. + EntityRecord: The inserted record with its server-assigned ``Id``. - Raises: - ValueError: If the SQL query fails validation (e.g., non-SELECT, missing - WHERE/LIMIT, forbidden keywords, subqueries). - """ - return await self._query_entities_for_records_async(sql_query) + Examples: + Insert from a dict:: - @traced(name="resolve_entity_set", run_type="uipath") - def resolve_entity_set( - self, - items: list[DataFabricEntityItem], - ) -> EntitySetResolution: - """Resolve an agent entity set, applying resource overwrites.""" - plan = create_resolution_plan( - items, - _resource_overwrites.get() or {}, - lambda folder_path: ( - self._folders_service.retrieve_key(folder_path=folder_path) - if self._folders_service is not None - else None - ), - ) - entities = fetch_resolved_entities( - plan, - self.retrieve, - self.retrieve_by_name, - logger, - ) - resolution_service: EntitiesService = build_resolution_service( # type: ignore[assignment] - config=self._config, - execution_context=self._execution_context, - folders_service=self._folders_service, - plan=plan, - service_factory=EntitiesService, - ) - return EntitySetResolution( - entities=entities, - entities_service=resolution_service, + record = await entities_service.insert_record_async( + "Customers", + {"name": "Alice", "email": "alice@example.com"}, + ) + print(record.id) + """ + return await self._data.insert_record_async( + entity_key, data, expansion_level=expansion_level ) - @traced(name="resolve_entity_set", run_type="uipath") - async def resolve_entity_set_async( + @traced(name="entity_get_record", run_type="uipath") + def get_record( self, - items: list[DataFabricEntityItem], - ) -> EntitySetResolution: - """Resolve an agent entity set, applying resource overwrites.""" + entity_key: str, + record_id: str, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Fetch a single entity record by its id. - async def _resolve_folder_path(folder_path: str) -> Optional[str]: - if self._folders_service is None: - return None - return await self._folders_service.retrieve_key_async( - folder_path=folder_path - ) + Args: + entity_key (str): The unique key/identifier of the entity. + record_id (str): The unique identifier of the record to fetch. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). - plan = await create_resolution_plan_async( - items, - _resource_overwrites.get() or {}, - _resolve_folder_path, - ) - entities = await fetch_resolved_entities_async( - plan, - self.retrieve_async, - self.retrieve_by_name_async, - logger, - ) - resolution_service: EntitiesService = build_resolution_service( # type: ignore[assignment] - config=self._config, - execution_context=self._execution_context, - folders_service=self._folders_service, - plan=plan, - service_factory=EntitiesService, - ) - return EntitySetResolution( - entities=entities, - entities_service=resolution_service, - ) + Returns: + EntityRecord: The record, with optional expanded relationships. - def _query_entities_for_records( - self, - sql_query: str, - ) -> List[Dict[str, Any]]: - self._validate_sql_query(sql_query) - routing_context = self._routing_strategy.resolve() - spec = self._query_entity_records_spec(sql_query, routing_context) - response = self.request(spec.method, spec.endpoint, json=spec.json) - return response.json().get("results", []) - - async def _query_entities_for_records_async( - self, - sql_query: str, - ) -> List[Dict[str, Any]]: - self._validate_sql_query(sql_query) - routing_context = await self._routing_strategy.resolve_async() - spec = self._query_entity_records_spec(sql_query, routing_context) - response = await self.request_async(spec.method, spec.endpoint, json=spec.json) - return response.json().get("results", []) + Examples: + Basic usage:: - @traced(name="entity_record_insert_batch", run_type="uipath") - def insert_records( + record = entities_service.get_record("Customers", "rec-1") + print(record.id, record.name) + + With FK expansion:: + + # Inline the related Company record on the returned Customer + record = entities_service.get_record( + "Customers", "rec-1", expansion_level=1 + ) + """ + return self._data.get_record( + entity_key, record_id, expansion_level=expansion_level + ) + + @traced(name="entity_get_record", run_type="uipath") + async def get_record_async( self, entity_key: str, - records: List[Any], - schema: Optional[Type[Any]] = None, - ) -> EntityRecordsBatchResponse: - """Insert multiple records into an entity in a single batch operation. + record_id: str, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Asynchronously fetch a single entity record by its id. Args: entity_key (str): The unique key/identifier of the entity. - records (List[Any]): List of records to insert. Each record should be an object - with attributes matching the entity's field names. - schema (Optional[Type[Any]]): Optional schema class for validation. When provided, - validates that each record in the response matches the schema structure. + record_id (str): The unique identifier of the record to fetch. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). Returns: - EntityRecordsBatchResponse: Response containing successful and failed record operations. - - success_records: List of successfully inserted EntityRecord objects - - failure_records: List of EntityRecord objects that failed to insert + EntityRecord: The record. Examples: - Insert records without schema:: + Basic usage:: - class Customer: - def __init__(self, name, email, age): - self.name = name - self.email = email + record = await entities_service.get_record_async("Customers", "rec-1") + print(record.id, record.name) + """ + return await self._data.get_record_async( + entity_key, record_id, expansion_level=expansion_level + ) + + @traced(name="entity_update_record", run_type="uipath") + def update_record( + self, + entity_key: str, + record_id: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Update a single record by id and return the updated row. + + Note: + Unlike :meth:`update_records` (batch), this single-record endpoint + fires Data Fabric trigger events. Use this method when triggers + attached to the entity must run. + + Args: + entity_key (str): The unique key/identifier of the entity. + record_id (str): The unique identifier of the record to update. + data (Any): Fields to update — a dict, a Pydantic model, or any + object exposing ``__dict__``. Fields explicitly set to + ``None`` are sent through; unset fields are omitted. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). + + Returns: + EntityRecord: The updated record. + + Examples: + Partial update from a dict:: + + record = entities_service.update_record( + "Customers", + "rec-1", + {"email": "alice.new@example.com"}, + ) + + Clear a field by passing an explicit ``None``:: + + # Note: unset fields are omitted; explicit None values are sent. + record = entities_service.update_record( + "Customers", + "rec-1", + {"middle_name": None}, + ) + """ + return self._data.update_record( + entity_key, record_id, data, expansion_level=expansion_level + ) + + @traced(name="entity_update_record", run_type="uipath") + async def update_record_async( + self, + entity_key: str, + record_id: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Asynchronously update a single record by id. + + Note: + Unlike :meth:`update_records_async` (batch), this single-record + endpoint fires Data Fabric trigger events. + + Args: + entity_key (str): The unique key/identifier of the entity. + record_id (str): The unique identifier of the record to update. + data (Any): Fields to update — a dict, a Pydantic model, or any + object exposing ``__dict__``. + expansion_level (Optional[int]): Depth of foreign-key expansion. + + Returns: + EntityRecord: The updated record. + + Examples: + Partial update:: + + record = await entities_service.update_record_async( + "Customers", + "rec-1", + {"email": "alice.new@example.com"}, + ) + """ + return await self._data.update_record_async( + entity_key, record_id, data, expansion_level=expansion_level + ) + + @traced(name="entity_delete_record", run_type="uipath") + def delete_record(self, entity_key: str, record_id: str) -> None: + """Delete a single record by id. + + Note: + Unlike :meth:`delete_records` (batch), this single-record endpoint + fires Data Fabric trigger events. Use this method when triggers + attached to the entity must run on delete. + + Args: + entity_key (str): The unique key/identifier of the entity. + record_id (str): The unique identifier of the record to delete. + + Examples: + Delete by id:: + + entities_service.delete_record("Customers", "rec-1") + """ + self._data.delete_record(entity_key, record_id) + + @traced(name="entity_delete_record", run_type="uipath") + async def delete_record_async(self, entity_key: str, record_id: str) -> None: + """Asynchronously delete a single record by id. + + Note: + Unlike :meth:`delete_records_async` (batch), this single-record + endpoint fires Data Fabric trigger events. + + Args: + entity_key (str): The unique key/identifier of the entity. + record_id (str): The unique identifier of the record to delete. + + Examples: + Delete by id:: + + await entities_service.delete_record_async("Customers", "rec-1") + """ + await self._data.delete_record_async(entity_key, record_id) + + @traced(name="entity_record_insert_batch", run_type="uipath") + def insert_records( + self, + entity_key: str, + records: List[Any], + schema: Optional[Type[Any]] = None, + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, + ) -> EntityRecordsBatchResponse: + """Insert multiple records into an entity in a single batch operation. + + Args: + entity_key (str): The unique key/identifier of the entity. + records (List[Any]): List of records to insert. Each record may be + a dict, a Pydantic model, an :class:`EntityRecord`, or any + object exposing ``__dict__``. + schema (Optional[Type[Any]]): Optional schema class for validation. When provided, + validates that each record in the response matches the schema structure. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). + fail_on_first (Optional[bool]): When ``True``, stop the batch on + the first per-record failure. When ``False`` (default), all + records are attempted and the response lists both + ``success_records`` and ``failure_records``. + + Returns: + EntityRecordsBatchResponse: Response containing successful and failed record operations. + - success_records: List of successfully inserted :class:`EntityRecord` objects + - failure_records: List of :class:`FailureRecord` describing per-record errors + + Examples: + Insert records without schema:: + + class Customer: + def __init__(self, name, email, age): + self.name = name + self.email = email self.age = age customers = [ @@ -766,6 +1152,15 @@ def __init__(self, name, email, age): print(f"Inserted: {len(response.success_records)}") print(f"Failed: {len(response.failure_records)}") + Insert with FK expansion and fail-fast:: + + response = entities_service.insert_records( + "Orders", + [{"product_id": "p-1", "qty": 3}, {"product_id": "p-2", "qty": 1}], + expansion_level=1, # inline the related Product on each response record + fail_on_first=True, # abort the batch at the first error + ) + Insert with schema validation:: class CustomerSchema: @@ -791,10 +1186,13 @@ def __init__(self, name, email, age): for record in response.success_records: print(f"Inserted: {record.name} (ID: {record.id})") """ - spec = self._insert_batch_spec(entity_key, records) - response = self.request(spec.method, spec.endpoint, json=spec.json) - - return self.validate_entity_batch(response, schema) + return self._data.insert_records( + entity_key, + records, + schema=schema, + expansion_level=expansion_level, + fail_on_first=fail_on_first, + ) @traced(name="entity_record_insert_batch", run_type="uipath") async def insert_records_async( @@ -802,20 +1200,29 @@ async def insert_records_async( entity_key: str, records: List[Any], schema: Optional[Type[Any]] = None, + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, ) -> EntityRecordsBatchResponse: """Asynchronously insert multiple records into an entity in a single batch operation. Args: entity_key (str): The unique key/identifier of the entity. - records (List[Any]): List of records to insert. Each record should be an object - with attributes matching the entity's field names. + records (List[Any]): List of records to insert. Each record may be + a dict, a Pydantic model, an :class:`EntityRecord`, or any + object exposing ``__dict__``. schema (Optional[Type[Any]]): Optional schema class for validation. When provided, validates that each record in the response matches the schema structure. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). + fail_on_first (Optional[bool]): When ``True``, stop the batch on + the first per-record failure. When ``False`` (default), all + records are attempted and the response lists both + ``success_records`` and ``failure_records``. Returns: EntityRecordsBatchResponse: Response containing successful and failed record operations. - - success_records: List of successfully inserted EntityRecord objects - - failure_records: List of EntityRecord objects that failed to insert + - success_records: List of successfully inserted :class:`EntityRecord` objects + - failure_records: List of :class:`FailureRecord` describing per-record errors Examples: Insert records without schema:: @@ -864,10 +1271,13 @@ def __init__(self, name, email, age): for record in response.success_records: print(f"Inserted: {record.name} (ID: {record.id})") """ - spec = self._insert_batch_spec(entity_key, records) - response = await self.request_async(spec.method, spec.endpoint, json=spec.json) - - return self.validate_entity_batch(response, schema) + return await self._data.insert_records_async( + entity_key, + records, + schema=schema, + expansion_level=expansion_level, + fail_on_first=fail_on_first, + ) @traced(name="entity_record_update_batch", run_type="uipath") def update_records( @@ -875,20 +1285,30 @@ def update_records( entity_key: str, records: List[Any], schema: Optional[Type[Any]] = None, + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, ) -> EntityRecordsBatchResponse: """Update multiple records in an entity in a single batch operation. Args: entity_key (str): The unique key/identifier of the entity. - records (List[Any]): List of records to update. Each record must have an 'Id' field - and should be a Pydantic model with `model_dump()` method or similar object. + records (List[Any]): List of records to update. Each record must + include its ``Id`` field. A record may be a dict, a Pydantic + model, an :class:`EntityRecord`, or any object exposing + ``__dict__``. schema (Optional[Type[Any]]): Optional schema class for validation. When provided, validates that each record in the request and response matches the schema structure. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). + fail_on_first (Optional[bool]): When ``True``, stop the batch on + the first per-record failure. When ``False`` (default), all + records are attempted and the response lists both + ``success_records`` and ``failure_records``. Returns: EntityRecordsBatchResponse: Response containing successful and failed record operations. - - success_records: List of successfully updated EntityRecord objects - - failure_records: List of EntityRecord objects that failed to update + - success_records: List of successfully updated :class:`EntityRecord` objects + - failure_records: List of :class:`FailureRecord` describing per-record errors Examples: Update records:: @@ -937,15 +1357,13 @@ class CustomerSchema: for record in response.success_records: print(f"Updated: {record.name}") """ - valid_records = [ - EntityRecord.from_data(data=record.model_dump(by_alias=True), model=schema) - for record in records - ] - - spec = self._update_batch_spec(entity_key, valid_records) - response = self.request(spec.method, spec.endpoint, json=spec.json) - - return self.validate_entity_batch(response, schema) + return self._data.update_records( + entity_key, + records, + schema=schema, + expansion_level=expansion_level, + fail_on_first=fail_on_first, + ) @traced(name="entity_record_update_batch", run_type="uipath") async def update_records_async( @@ -953,20 +1371,30 @@ async def update_records_async( entity_key: str, records: List[Any], schema: Optional[Type[Any]] = None, + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, ) -> EntityRecordsBatchResponse: """Asynchronously update multiple records in an entity in a single batch operation. Args: entity_key (str): The unique key/identifier of the entity. - records (List[Any]): List of records to update. Each record must have an 'Id' field - and should be a Pydantic model with `model_dump()` method or similar object. + records (List[Any]): List of records to update. Each record must + include its ``Id`` field. A record may be a dict, a Pydantic + model, an :class:`EntityRecord`, or any object exposing + ``__dict__``. schema (Optional[Type[Any]]): Optional schema class for validation. When provided, validates that each record in the request and response matches the schema structure. + expansion_level (Optional[int]): Depth of foreign-key expansion in + the response (``0`` means no expansion). + fail_on_first (Optional[bool]): When ``True``, stop the batch on + the first per-record failure. When ``False`` (default), all + records are attempted and the response lists both + ``success_records`` and ``failure_records``. Returns: EntityRecordsBatchResponse: Response containing successful and failed record operations. - - success_records: List of successfully updated EntityRecord objects - - failure_records: List of EntityRecord objects that failed to update + - success_records: List of successfully updated :class:`EntityRecord` objects + - failure_records: List of :class:`FailureRecord` describing per-record errors Examples: Update records:: @@ -1015,32 +1443,35 @@ class CustomerSchema: for record in response.success_records: print(f"Updated: {record.name}") """ - valid_records = [ - EntityRecord.from_data(data=record.model_dump(by_alias=True), model=schema) - for record in records - ] - - spec = self._update_batch_spec(entity_key, valid_records) - response = await self.request_async(spec.method, spec.endpoint, json=spec.json) - - return self.validate_entity_batch(response, schema) + return await self._data.update_records_async( + entity_key, + records, + schema=schema, + expansion_level=expansion_level, + fail_on_first=fail_on_first, + ) @traced(name="entity_record_delete_batch", run_type="uipath") def delete_records( self, entity_key: str, record_ids: List[str], + fail_on_first: Optional[bool] = None, ) -> EntityRecordsBatchResponse: """Delete multiple records from an entity in a single batch operation. Args: entity_key (str): The unique key/identifier of the entity. record_ids (List[str]): List of record IDs (GUIDs) to delete. + fail_on_first (Optional[bool]): When ``True``, stop the batch on + the first per-record failure. When ``False`` (default), all + records are attempted and the response lists both + ``success_records`` and ``failure_records``. Returns: EntityRecordsBatchResponse: Response containing successful and failed record operations. - - success_records: List of successfully deleted EntityRecord objects - - failure_records: List of EntityRecord objects that failed to delete + - success_records: List of successfully deleted :class:`EntityRecord` objects + - failure_records: List of :class:`FailureRecord` describing per-record errors Examples: Delete specific records by ID:: @@ -1077,31 +1508,31 @@ def delete_records( ) print(f"Deleted {len(response.success_records)} inactive records") """ - spec = self._delete_batch_spec(entity_key, record_ids) - response = self.request(spec.method, spec.endpoint, json=spec.json) - - delete_records_response = EntityRecordsBatchResponse.model_validate( - response.json() + return self._data.delete_records( + entity_key, record_ids, fail_on_first=fail_on_first ) - return delete_records_response - @traced(name="entity_record_delete_batch", run_type="uipath") async def delete_records_async( self, entity_key: str, record_ids: List[str], + fail_on_first: Optional[bool] = None, ) -> EntityRecordsBatchResponse: """Asynchronously delete multiple records from an entity in a single batch operation. Args: entity_key (str): The unique key/identifier of the entity. record_ids (List[str]): List of record IDs (GUIDs) to delete. + fail_on_first (Optional[bool]): When ``True``, stop the batch on + the first per-record failure. When ``False`` (default), all + records are attempted and the response lists both + ``success_records`` and ``failure_records``. Returns: EntityRecordsBatchResponse: Response containing successful and failed record operations. - - success_records: List of successfully deleted EntityRecord objects - - failure_records: List of EntityRecord objects that failed to delete + - success_records: List of successfully deleted :class:`EntityRecord` objects + - failure_records: List of :class:`FailureRecord` describing per-record errors Examples: Delete specific records by ID:: @@ -1138,351 +1569,658 @@ async def delete_records_async( ) print(f"Deleted {len(response.success_records)} inactive records") """ - spec = self._delete_batch_spec(entity_key, record_ids) - response = await self.request_async(spec.method, spec.endpoint, json=spec.json) - - delete_records_response = EntityRecordsBatchResponse.model_validate( - response.json() + return await self._data.delete_records_async( + entity_key, record_ids, fail_on_first=fail_on_first ) - return delete_records_response - - def validate_entity_batch( + @traced(name="entity_retrieve_records", run_type="uipath") + def retrieve_records( self, - batch_response: Response, - schema: Optional[Type[Any]] = None, - ) -> EntityRecordsBatchResponse: - # Validate the response format - insert_records_response = EntityRecordsBatchResponse.model_validate( - batch_response.json() - ) + entity_key: str, + filter_group: Optional[EntityQueryFilterGroup] = None, + sort_options: Optional[List[EntityQuerySortOption]] = None, + selected_fields: Optional[List[str]] = None, + expansions: Optional[List[Any]] = None, + expansion_level: Optional[int] = None, + aggregates: Optional[List[EntityAggregate]] = None, + group_by: Optional[List[str]] = None, + joins: Optional[List[EntityJoin]] = None, + binnings: Optional[List[EntityBinning]] = None, + start: Optional[int] = None, + limit: Optional[int] = None, + ) -> RetrieveEntityRecordsResponse: + """Retrieve records with structured filters, sorting, expansion, joins, and aggregates. - # Validate individual records - validated_successful_records = [ - EntityRecord.from_data( - data=successful_record.model_dump(by_alias=True), model=schema - ) - for successful_record in insert_records_response.success_records - ] + Routes to the V2 endpoint when ``binnings`` is provided (numeric/date + binning is gated by the ``enable-binning-on-query`` feature flag on + the backend). - validated_failed_records = [ - EntityRecord.from_data( - data=failed_record.model_dump(by_alias=True), model=schema - ) - for failed_record in insert_records_response.failure_records - ] + Args: + entity_key (str): The unique key/identifier of the entity. + filter_group (Optional[EntityQueryFilterGroup]): Nested filter + conditions combined with AND/OR. + sort_options (Optional[List[EntityQuerySortOption]]): Sort fields + and direction. + selected_fields (Optional[List[str]]): Column projection — field + names to include; omit to return all fields. + expansions (Optional[List[Any]]): Foreign-key relationships to + expand inline on each result record. + expansion_level (Optional[int]): Depth of expansion (sent as a + URL query param). + aggregates (Optional[List[EntityAggregate]]): Aggregate + expressions (``COUNT`` / ``SUM`` / ``AVG`` / ``MIN`` / + ``MAX``). Maximum 5 per query. + group_by (Optional[List[str]]): Fields to group aggregate results + by. Maximum 5; required when both ``aggregates`` and + ``selected_fields`` are supplied. + joins (Optional[List[EntityJoin]]): Cross-entity joins. Maximum + 3, all of the same type. + binnings (Optional[List[EntityBinning]]): Bucket numeric or date + group-by fields. Each entry's field must also appear in + ``group_by``. + start (Optional[int]): Records to skip (pagination offset). + limit (Optional[int]): Maximum number of records to return. - return EntityRecordsBatchResponse( - success_records=validated_successful_records, - failure_records=validated_failed_records, - ) + Returns: + RetrieveEntityRecordsResponse: A response with ``items``, + ``total_count``, ``has_next_page``, and ``next_cursor``. + ``items`` is a list of :class:`EntityRecord` for plain + queries, or :class:`AggregateRow` when ``aggregates``, + ``group_by``, or ``binnings`` are used. ``next_cursor`` is + populated only when the backend returns one; otherwise + paginate by passing the next ``start``. - def _retrieve_spec( - self, - entity_key: str, - ) -> RequestSpec: - return RequestSpec( - method="GET", - endpoint=Endpoint(f"datafabric_/api/Entity/{entity_key}"), - ) + Examples: + Filter + sort + projection:: + + from uipath.platform.entities import ( + EntityQueryFilter, + EntityQueryFilterGroup, + EntityQuerySortOption, + LogicalOperator, + QueryFilterOperator, + ) - def _retrieve_by_name_spec( - self, - entity_name: str, - ) -> RequestSpec: - return RequestSpec( - method="GET", - endpoint=Endpoint(f"datafabric_/api/Entity/{entity_name}/metadata"), - ) + result = entities_service.retrieve_records( + "Customers", + filter_group=EntityQueryFilterGroup( + logical_operator=LogicalOperator.And, + query_filters=[ + EntityQueryFilter( + field_name="status", + operator=QueryFilterOperator.Equals, + value="active", + ) + ], + ), + sort_options=[ + EntityQuerySortOption(field_name="created_at", is_descending=True) + ], + selected_fields=["Id", "name", "email"], + start=0, + limit=50, + ) + print(f"Found {result.total_count} customers") + + Aggregates and group-by (counts per status):: - @staticmethod - def _folder_key_headers(folder_key: Optional[str]) -> dict[str, str]: - if folder_key: - return {HEADER_FOLDER_KEY: folder_key} - return {} + from uipath.platform.entities import ( + EntityAggregate, + EntityAggregateFunction, + ) - def _list_entities_spec(self) -> RequestSpec: - return RequestSpec( - method="GET", - endpoint=Endpoint("datafabric_/api/Entity"), + result = entities_service.retrieve_records( + "Customers", + selected_fields=["status"], + group_by=["status"], + aggregates=[ + EntityAggregate( + function=EntityAggregateFunction.Count, + field="Id", + alias="total", + ) + ], + ) + for row in result.items: + print(row.status, row.total) + """ + return self._data.retrieve_records( + entity_key, + filter_group=filter_group, + sort_options=sort_options, + selected_fields=selected_fields, + expansions=expansions, + expansion_level=expansion_level, + aggregates=aggregates, + group_by=group_by, + joins=joins, + binnings=binnings, + start=start, + limit=limit, ) - def _list_records_spec( + @traced(name="entity_retrieve_records", run_type="uipath") + async def retrieve_records_async( self, entity_key: str, + filter_group: Optional[EntityQueryFilterGroup] = None, + sort_options: Optional[List[EntityQuerySortOption]] = None, + selected_fields: Optional[List[str]] = None, + expansions: Optional[List[Any]] = None, + expansion_level: Optional[int] = None, + aggregates: Optional[List[EntityAggregate]] = None, + group_by: Optional[List[str]] = None, + joins: Optional[List[EntityJoin]] = None, + binnings: Optional[List[EntityBinning]] = None, start: Optional[int] = None, limit: Optional[int] = None, - ) -> RequestSpec: - params: dict[str, Any] = {} - if start is not None: - params["start"] = start - if limit is not None: - params["limit"] = limit - return RequestSpec( - method="GET", - endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/read" - ), - params=params, + ) -> RetrieveEntityRecordsResponse: + """Asynchronously retrieve records with structured filters, sorting, expansion, joins, and aggregates. + + Routes to the V2 endpoint when ``binnings`` is provided (numeric/date + binning is gated by the ``enable-binning-on-query`` feature flag on + the backend). + + Args: + entity_key (str): The unique key/identifier of the entity. + filter_group (Optional[EntityQueryFilterGroup]): Nested filter + conditions combined with AND/OR. + sort_options (Optional[List[EntityQuerySortOption]]): Sort fields + and direction. + selected_fields (Optional[List[str]]): Column projection — field + names to include; omit to return all fields. + expansions (Optional[List[Any]]): Foreign-key relationships to + expand inline on each result record. + expansion_level (Optional[int]): Depth of expansion. + aggregates (Optional[List[EntityAggregate]]): Aggregate + expressions. Maximum 5 per query. + group_by (Optional[List[str]]): Fields to group aggregate results + by. Maximum 5; required when both ``aggregates`` and + ``selected_fields`` are supplied. + joins (Optional[List[EntityJoin]]): Cross-entity joins. Maximum + 3, all of the same type. + binnings (Optional[List[EntityBinning]]): Bucket numeric or date + group-by fields. + start (Optional[int]): Records to skip (pagination offset). + limit (Optional[int]): Maximum number of records to return. + + Returns: + RetrieveEntityRecordsResponse: A response with ``items``, + ``total_count``, ``has_next_page``, and ``next_cursor``. + + Examples: + Filter + sort + pagination:: + + from uipath.platform.entities import ( + EntityQueryFilter, + EntityQueryFilterGroup, + QueryFilterOperator, + ) + + result = await entities_service.retrieve_records_async( + "Customers", + filter_group=EntityQueryFilterGroup( + query_filters=[ + EntityQueryFilter( + field_name="status", + operator=QueryFilterOperator.Equals, + value="active", + ) + ], + ), + start=0, + limit=25, + ) + print(f"{len(result.items)} of {result.total_count} customers") + """ + return await self._data.retrieve_records_async( + entity_key, + filter_group=filter_group, + sort_options=sort_options, + selected_fields=selected_fields, + expansions=expansions, + expansion_level=expansion_level, + aggregates=aggregates, + group_by=group_by, + joins=joins, + binnings=binnings, + start=start, + limit=limit, ) - def _query_entity_records_spec( + @traced(name="entity_query_records", run_type="uipath") + def query_entity_records(self, sql_query: str) -> List[Dict[str, Any]]: + """Query entity records using a validated SQL query. + + PREVIEW: This method is in preview and may change in future releases. + + Args: + sql_query (str): A SQL SELECT query to execute against Data Service entities. + Only SELECT statements are allowed. Queries without WHERE must include + a LIMIT clause. Subqueries and multi-statement queries are not permitted. + + Notes: + A routing context is always derived from the configured ``folders_map`` + when present and included in the request body. + + Returns: + List[Dict[str, Any]]: A list of result records as dictionaries. + + Raises: + ValueError: If the SQL query fails validation (e.g., non-SELECT, missing + WHERE/LIMIT, forbidden keywords, subqueries). + """ + return self._data.query_entity_records(sql_query) + + @traced(name="entity_query_records", run_type="uipath") + async def query_entity_records_async(self, sql_query: str) -> List[Dict[str, Any]]: + """Asynchronously query entity records using a validated SQL query. + + PREVIEW: This method is in preview and may change in future releases. + + Args: + sql_query (str): A SQL SELECT query to execute against Data Service entities. + Only SELECT statements are allowed. Queries without WHERE must include + a LIMIT clause. Subqueries and multi-statement queries are not permitted. + + Notes: + A routing context is always derived from the configured ``folders_map`` + when present and included in the request body. + + Returns: + List[Dict[str, Any]]: A list of result records as dictionaries. + + Raises: + ValueError: If the SQL query fails validation (e.g., non-SELECT, missing + WHERE/LIMIT, forbidden keywords, subqueries). + """ + return await self._data.query_entity_records_async(sql_query) + + @traced(name="entity_upload_attachment", run_type="uipath") + def upload_attachment( self, - sql_query: str, - routing_context: Optional[QueryRoutingOverrideContext] = None, - ) -> RequestSpec: - body: Dict[str, Any] = {"query": sql_query} - if routing_context: - body["routingContext"] = routing_context.model_dump( - by_alias=True, exclude_none=True - ) - return RequestSpec( - method="POST", - endpoint=Endpoint("datafabric_/api/v1/query/execute"), - json=body, - ) + entity_id: str, + record_id: str, + field_name: str, + file: Optional[FileContent] = None, + file_path: Optional[str] = None, + expansion_level: Optional[int] = None, + ) -> Dict[str, Any]: + """Upload a file attachment to a File-type field on a record. + + Provide exactly one of ``file`` (raw bytes) or ``file_path`` (path on + disk). - def _insert_batch_spec(self, entity_key: str, records: List[Any]) -> RequestSpec: - return RequestSpec( - method="POST", - endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/insert-batch" - ), - json=[record.__dict__ for record in records], - ) + Args: + entity_id (str): The unique identifier of the entity. + record_id (str): The unique identifier of the record whose + attachment field is being set. + field_name (str): Name of the File-type field on the entity. + file (Optional[FileContent]): Raw bytes (``bytes`` / + ``bytearray`` / ``memoryview``) of the file to upload. + Mutually exclusive with ``file_path``. + file_path (Optional[str]): Path to a local file to upload. + Mutually exclusive with ``file``. + expansion_level (Optional[int]): Optional FK expansion depth in + the response (``0`` means no expansion). - def _update_batch_spec( - self, entity_key: str, records: List[EntityRecord] - ) -> RequestSpec: - return RequestSpec( - method="POST", - endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/update-batch" - ), - json=[record.model_dump(by_alias=True) for record in records], + Returns: + Dict[str, Any]: The decoded JSON response (typically the updated + record), or an empty dict when the response has no body. + + Examples: + Upload from raw bytes:: + + with open("contract.pdf", "rb") as f: + data = f.read() + entities_service.upload_attachment( + "Customers", "rec-1", "Contract", file=data + ) + + Upload from a path on disk:: + + entities_service.upload_attachment( + "Customers", "rec-1", "Contract", file_path="./contract.pdf" + ) + """ + return self._data.upload_attachment( + entity_id, + record_id, + field_name, + file=file, + file_path=file_path, + expansion_level=expansion_level, ) - def _delete_batch_spec(self, entity_key: str, record_ids: List[str]) -> RequestSpec: - return RequestSpec( - method="POST", - endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{entity_key}/delete-batch" - ), - json=record_ids, + @traced(name="entity_upload_attachment", run_type="uipath") + async def upload_attachment_async( + self, + entity_id: str, + record_id: str, + field_name: str, + file: Optional[FileContent] = None, + file_path: Optional[str] = None, + expansion_level: Optional[int] = None, + ) -> Dict[str, Any]: + """Asynchronously upload a file attachment to a File-type field on a record. + + Provide exactly one of ``file`` (raw bytes) or ``file_path`` (path on + disk). + + Args: + entity_id (str): The unique identifier of the entity. + record_id (str): The unique identifier of the record whose + attachment field is being set. + field_name (str): Name of the File-type field on the entity. + file (Optional[FileContent]): Raw bytes of the file to upload. + Mutually exclusive with ``file_path``. + file_path (Optional[str]): Path to a local file to upload. + Mutually exclusive with ``file``. + expansion_level (Optional[int]): Optional FK expansion depth in + the response. + + Returns: + Dict[str, Any]: The decoded JSON response. + + Examples: + Upload from a path on disk:: + + await entities_service.upload_attachment_async( + "Customers", "rec-1", "Contract", file_path="./contract.pdf" + ) + """ + return await self._data.upload_attachment_async( + entity_id, + record_id, + field_name, + file=file, + file_path=file_path, + expansion_level=expansion_level, ) - def _list_choicesets_spec(self) -> RequestSpec: - return RequestSpec( - method="GET", - endpoint=Endpoint("datafabric_/api/Entity/choiceset"), + @traced(name="entity_download_attachment", run_type="uipath") + def download_attachment( + self, entity_id: str, record_id: str, field_name: str + ) -> bytes: + """Download a file attached to a record and return its raw bytes. + + Args: + entity_id (str): The unique identifier of the entity. + record_id (str): The unique identifier of the record containing + the attachment. + field_name (str): Name of the File-type field on the entity. + + Returns: + bytes: The raw file content. + + Examples: + Save the downloaded bytes to disk:: + + content = entities_service.download_attachment( + "Customers", "rec-1", "Contract" + ) + with open("downloaded.pdf", "wb") as f: + f.write(content) + """ + return self._data.download_attachment(entity_id, record_id, field_name) + + @traced(name="entity_download_attachment", run_type="uipath") + async def download_attachment_async( + self, entity_id: str, record_id: str, field_name: str + ) -> bytes: + """Asynchronously download a file attached to a record. + + Args: + entity_id (str): The unique identifier of the entity. + record_id (str): The unique identifier of the record containing + the attachment. + field_name (str): Name of the File-type field on the entity. + + Returns: + bytes: The raw file content. + + Examples: + Save the downloaded bytes to disk:: + + content = await entities_service.download_attachment_async( + "Customers", "rec-1", "Contract" + ) + with open("downloaded.pdf", "wb") as f: + f.write(content) + """ + return await self._data.download_attachment_async( + entity_id, record_id, field_name ) - def _get_choiceset_values_spec( + @traced(name="entity_delete_attachment", run_type="uipath") + def delete_attachment( self, - choiceset_id: str, - start: int | None = None, - limit: int | None = None, - ) -> RequestSpec: - params: dict[str, Any] = {} - if start is not None: - params["start"] = start - if limit is not None: - params["limit"] = limit - return RequestSpec( - method="POST", - endpoint=Endpoint( - f"datafabric_/api/EntityService/entity/{choiceset_id}/query_expansion" - ), - params=params, - json={}, + entity_id: str, + record_id: str, + field_name: str, + expansion_level: Optional[int] = None, + ) -> Dict[str, Any]: + """Remove the file attached to a File-type field on a record. + + Args: + entity_id (str): The unique identifier of the entity. + record_id (str): The unique identifier of the record whose + attachment is being removed. + field_name (str): Name of the File-type field on the entity. + expansion_level (Optional[int]): Optional FK expansion depth in + the response (``0`` means no expansion). + + Returns: + Dict[str, Any]: The decoded JSON response (typically the updated + record), or an empty dict when the response has no body. + + Examples: + Clear an attachment:: + + entities_service.delete_attachment( + "Customers", "rec-1", "Contract" + ) + """ + return self._data.delete_attachment( + entity_id, record_id, field_name, expansion_level=expansion_level ) - def _validate_sql_query(self, sql_query: str) -> None: - query = sql_query.strip().rstrip(";").strip() - if not query: - raise ValueError("SQL query cannot be empty.") + @traced(name="entity_delete_attachment", run_type="uipath") + async def delete_attachment_async( + self, + entity_id: str, + record_id: str, + field_name: str, + expansion_level: Optional[int] = None, + ) -> Dict[str, Any]: + """Asynchronously remove the file attached to a File-type field. - statements = sqlparse.parse(query) - if len(statements) != 1 or not statements[0].tokens: - raise ValueError("Only a single SELECT statement is allowed.") + Args: + entity_id (str): The unique identifier of the entity. + record_id (str): The unique identifier of the record whose + attachment is being removed. + field_name (str): Name of the File-type field on the entity. + expansion_level (Optional[int]): Optional FK expansion depth. - stmt = statements[0] - stmt_type = stmt.get_type() + Returns: + Dict[str, Any]: The decoded JSON response. - if stmt_type != "SELECT": - raise ValueError("Only SELECT statements are allowed.") + Examples: + Clear an attachment:: + + await entities_service.delete_attachment_async( + "Customers", "rec-1", "Contract" + ) + """ + return await self._data.delete_attachment_async( + entity_id, record_id, field_name, expansion_level=expansion_level + ) - keywords = set() - for token in stmt.flatten(): - if token.ttype in Keyword: - keywords.add(token.normalized) + @traced(name="entity_import_records", run_type="uipath") + def import_records( + self, + entity_id: str, + file: Optional[FileContent] = None, + file_path: Optional[str] = None, + ) -> EntityImportRecordsResponse: + """Bulk-import records into an entity from a CSV file. + + Provide exactly one of ``file`` (raw bytes) or ``file_path`` (path on + disk). - for kw in _FORBIDDEN_DML: - if kw in keywords: - raise ValueError(f"SQL keyword '{kw}' is not allowed.") + Args: + entity_id (str): The unique identifier of the entity. + file (Optional[FileContent]): Raw bytes of a CSV file. Mutually + exclusive with ``file_path``. + file_path (Optional[str]): Path to a local CSV file. Mutually + exclusive with ``file``. + + Returns: + EntityImportRecordsResponse: Reports the total rows in the file, + the number successfully inserted, and an optional + ``error_file_link`` pointing to a CSV listing rows that + failed validation. - for kw in _FORBIDDEN_DDL: - if kw in keywords: - raise ValueError(f"SQL keyword '{kw}' is not allowed.") + Examples: + Import from a path on disk:: - for kw in _DISALLOWED_KEYWORDS: - if kw in keywords: - raise ValueError( - f"SQL construct '{kw}' is not allowed in entity queries." + result = entities_service.import_records( + "Customers", file_path="./customers.csv" + ) + print( + f"Inserted {result.inserted_records} of " + f"{result.total_records} rows" ) + if result.error_file_link: + print(f"Errors: {result.error_file_link}") + """ + return self._data.import_records(entity_id, file=file, file_path=file_path) - if self._has_subquery(stmt): - raise ValueError("Subqueries are not allowed.") + @traced(name="entity_import_records", run_type="uipath") + async def import_records_async( + self, + entity_id: str, + file: Optional[FileContent] = None, + file_path: Optional[str] = None, + ) -> EntityImportRecordsResponse: + """Asynchronously bulk-import records into an entity from a CSV file. - has_where = any(isinstance(t, Where) for t in stmt.tokens) - has_limit = "LIMIT" in keywords - has_from = "FROM" in keywords + Provide exactly one of ``file`` (raw bytes) or ``file_path`` (path on + disk). - if not has_from: - raise ValueError("Queries must include a FROM clause.") + Args: + entity_id (str): The unique identifier of the entity. + file (Optional[FileContent]): Raw bytes of a CSV file. + file_path (Optional[str]): Path to a local CSV file. - projection = self._projection_tokens(stmt) + Returns: + EntityImportRecordsResponse: Reports the total, inserted, and + ``error_file_link`` for failed rows. - if self._projection_has_count_star(projection): - raise ValueError( - "COUNT(*) is not supported. Use COUNT(column_name) instead." - ) + Examples: + Import from a path on disk:: + + result = await entities_service.import_records_async( + "Customers", file_path="./customers.csv" + ) + print( + f"Inserted {result.inserted_records} of " + f"{result.total_records} rows" + ) + """ + return await self._data.import_records_async( + entity_id, file=file, file_path=file_path + ) + + # ------------------------------------------------------------------ + # Public helper retained for backward compatibility — tests call this + # ------------------------------------------------------------------ + + def validate_entity_batch( + self, + batch_response: Response, + schema: Optional[Type[Any]] = None, + ) -> EntityRecordsBatchResponse: + """Parse a batch response, optionally validating success records against ``schema``. + + Failure records are returned as :class:`FailureRecord` instances and + are not validated against the user schema. + """ + return self._data.validate_entity_batch(batch_response, schema=schema) - has_aggregate = self._projection_has_aggregate(projection) + # ------------------------------------------------------------------ + # Cross-cutting — entity-set resolution for agent overrides + # ------------------------------------------------------------------ + + @traced(name="resolve_entity_set", run_type="uipath") + def resolve_entity_set( + self, + items: List[DataFabricEntityItem], + ) -> EntitySetResolution: + """Resolve an agent entity set, applying resource overwrites.""" + plan = create_resolution_plan( + items, + _resource_overwrites.get() or {}, + lambda folder_path: ( + self._folders_service.retrieve_key(folder_path=folder_path) + if self._folders_service is not None + else None + ), + ) + entities = fetch_resolved_entities( + plan, + self.retrieve, + self.retrieve_by_name, + logger, + ) + resolution_service: EntitiesService = build_resolution_service( # type: ignore[assignment] + config=self._config, + execution_context=self._execution_context, + folders_service=self._folders_service, + plan=plan, + service_factory=EntitiesService, + ) + return EntitySetResolution( + entities=entities, + entities_service=resolution_service, + ) - if not has_where and not has_limit and not has_aggregate: - raise ValueError("Queries without WHERE must include a LIMIT clause.") + @traced(name="resolve_entity_set", run_type="uipath") + async def resolve_entity_set_async( + self, + items: List[DataFabricEntityItem], + ) -> EntitySetResolution: + """Resolve an agent entity set, applying resource overwrites.""" - has_bare_wildcard = self._projection_has_bare_wildcard(projection) - if has_bare_wildcard: - raise ValueError("SELECT * is not allowed. Specify column names instead.") - if not has_where and self._projection_column_count(projection) > 4: - raise ValueError( - "Selecting more than 4 columns without filtering is not allowed." + async def _resolve_folder_path(folder_path: str) -> Optional[str]: + if self._folders_service is None: + return None + return await self._folders_service.retrieve_key_async( + folder_path=folder_path ) - @staticmethod - def _projection_has_aggregate( - projection: list[sqlparse.sql.Token], - ) -> bool: - """Check whether the projection contains an aggregate function call.""" - - def _has_agg(token: sqlparse.sql.Token) -> bool: - if isinstance(token, Function): - return token.get_name().upper() in _AGGREGATE_FUNCTIONS - if isinstance(token, Identifier): - return any(_has_agg(child) for child in token.tokens) - return False - - for node in projection: - if _has_agg(node): - return True - if isinstance(node, IdentifierList): - if any(_has_agg(child) for child in node.tokens): - return True - return False - - @staticmethod - def _projection_has_count_star( - projection: list[sqlparse.sql.Token], - ) -> bool: - """Check whether projection contains COUNT(*).""" - - def _is_count_star(func: Function) -> bool: - if func.get_name().upper() != "COUNT": - return False - return any(t.ttype is Wildcard for t in func.flatten()) - - def _has_count_star(token: sqlparse.sql.Token) -> bool: - if isinstance(token, Function): - return _is_count_star(token) - if isinstance(token, Identifier): - return any(_has_count_star(child) for child in token.tokens) - return False - - for node in projection: - if _has_count_star(node): - return True - if isinstance(node, IdentifierList): - if any(_has_count_star(child) for child in node.tokens): - return True - return False - - @staticmethod - def _projection_has_bare_wildcard( - projection: list[sqlparse.sql.Token], - ) -> bool: - """Check for a bare ``*`` or qualified ``table.*`` outside a function.""" - - def _identifier_has_wildcard(ident: Identifier) -> bool: - return any(t.ttype is Wildcard for t in ident.tokens) - - for node in projection: - if node.ttype is Wildcard: - return True - if isinstance(node, Identifier) and _identifier_has_wildcard(node): - return True - if isinstance(node, IdentifierList): - for child in node.tokens: - if child.ttype is Wildcard: - return True - if isinstance(child, Identifier) and _identifier_has_wildcard( - child - ): - return True - return False - - @staticmethod - def _has_subquery(stmt: sqlparse.sql.Statement) -> bool: - """Recursively walk the AST looking for SELECT inside parentheses.""" - - def _walk(token: sqlparse.sql.Token) -> bool: - if isinstance(token, Parenthesis): - for child in token.flatten(): - if child.ttype is DML and child.normalized == "SELECT": - return True - if hasattr(token, "tokens"): - for child in token.tokens: - if _walk(child): - return True - return False - - for token in stmt.tokens: - if _walk(token): - return True - return False - - @staticmethod - def _projection_tokens( - stmt: sqlparse.sql.Statement, - ) -> list[sqlparse.sql.Token]: - """Extract non-flattened AST nodes between the first SELECT and FROM.""" - tokens: list[sqlparse.sql.Token] = [] - collecting = False - for token in stmt.tokens: - if token.ttype is DML and token.normalized == "SELECT": - collecting = True - continue - if token.ttype is Keyword and token.normalized in ("FROM", "INTO"): - break - if token.ttype is Keyword and token.normalized == "DISTINCT": - continue - if collecting and token.ttype is not Whitespace: - tokens.append(token) - return tokens - - @staticmethod - def _projection_column_count( - projection: list[sqlparse.sql.Token], - ) -> int: - for node in projection: - if isinstance(node, IdentifierList): - return len(list(node.get_identifiers())) - if isinstance(node, (Identifier, Function)): - return 1 - if node.ttype is Wildcard: - return 1 - return 0 + plan = await create_resolution_plan_async( + items, + _resource_overwrites.get() or {}, + _resolve_folder_path, + ) + entities = await fetch_resolved_entities_async( + plan, + self.retrieve_async, + self.retrieve_by_name_async, + logger, + ) + resolution_service: EntitiesService = build_resolution_service( # type: ignore[assignment] + config=self._config, + execution_context=self._execution_context, + folders_service=self._folders_service, + plan=plan, + service_factory=EntitiesService, + ) + return EntitySetResolution( + entities=entities, + entities_service=resolution_service, + ) # Resolve the forward reference to EntitiesService in EntitySetResolution. diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entity_data_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entity_data_service.py new file mode 100644 index 000000000..60cfb99b4 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/entities/_entity_data_service.py @@ -0,0 +1,1403 @@ +"""Data-side operations for the Data Fabric entities surface. + +Handles record CRUD (single and batch), structured queries, attachments, +choice-set value lookup, bulk import, and federated SQL queries. Schema +definitions are managed by :class:`EntitySchemaService` and exposed alongside +data operations through :class:`EntitiesService`. +""" + +import json as json_module +import logging +from contextlib import nullcontext +from pathlib import Path +from typing import Any, Dict, List, Optional, Type + +import sqlparse +from httpx import HTTPStatusError, Response +from pydantic import BaseModel +from sqlparse.sql import Function, Identifier, IdentifierList, Parenthesis, Where +from sqlparse.tokens import DML, Keyword, Whitespace, Wildcard + +from ..common._base_service import BaseService +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..common._models import Endpoint, RequestSpec +from ..errors._enriched_exception import EnrichedException +from ..orchestrator._folder_service import FolderService +from ._entity_resolution import RoutingStrategy, create_routing_strategy +from .entities import ( + AggregateRow, + ChoiceSetValue, + EntityAggregate, + EntityBinning, + EntityImportRecordsResponse, + EntityJoin, + EntityQueryFilterGroup, + EntityQuerySortOption, + EntityRecord, + EntityRecordsBatchResponse, + EntityRecordsListResponse, + QueryRoutingOverrideContext, + RetrieveEntityRecordsResponse, +) + +logger = logging.getLogger(__name__) + +FileContent = bytes | bytearray | memoryview +"""Acceptable raw bytes types for attachment and CSV uploads.""" + +_FORBIDDEN_DML = {"INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE"} +_FORBIDDEN_DDL = {"DROP", "ALTER", "CREATE", "TRUNCATE"} +_DISALLOWED_KEYWORDS = [ + "WITH", + "UNION", + "INTERSECT", + "EXCEPT", + "OVER", + "ROLLUP", + "CUBE", + "GROUPING", + "PARTITION", +] +_AGGREGATE_FUNCTIONS = ("COUNT", "SUM", "AVG", "MIN", "MAX") + + +class EntityDataService(BaseService): + """HTTP service for entity-record and attachment operations. + + Backend target: ``datafabric_/api/EntityService/...`` plus + ``datafabric_/api/Attachment/...`` for file attachments, and + ``datafabric_/api/v1/query/execute`` for federated SQL queries. + + !!! warning "Preview Feature" + This service is currently experimental. Behavior and parameters are + subject to change in future versions. + """ + + def __init__( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: Optional[FolderService] = None, + routing_strategy: Optional[RoutingStrategy] = None, + folders_map: Optional[Dict[str, str]] = None, + entity_name_overrides: Optional[Dict[str, str]] = None, + routing_context: Optional[QueryRoutingOverrideContext] = None, + ) -> None: + """Initialise the data service. + + Either pass a pre-built ``routing_strategy`` (the facade does this so + both services share one) or supply the inputs and let this service + construct its own. + """ + super().__init__(config=config, execution_context=execution_context) + self._folders_service = folders_service + self._routing_strategy: RoutingStrategy = ( + routing_strategy + if routing_strategy is not None + else create_routing_strategy( + folders_map=folders_map, + effective_entity_names=entity_name_overrides, + routing_context=routing_context, + folders_service=folders_service, + ) + ) + + # ------------------------------------------------------------------ + # Choice-set value lookup + # ------------------------------------------------------------------ + + def get_choiceset_values( + self, + choiceset_id: str, + start: Optional[int] = None, + limit: Optional[int] = None, + ) -> List[ChoiceSetValue]: + """Internal implementation; see :meth:`EntitiesService.get_choiceset_values`.""" + spec = self._get_choiceset_values_spec(choiceset_id, start=start, limit=limit) + response = self.request( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + return self._parse_choiceset_values(response) + + async def get_choiceset_values_async( + self, + choiceset_id: str, + start: Optional[int] = None, + limit: Optional[int] = None, + ) -> List[ChoiceSetValue]: + """Async variant of :meth:`get_choiceset_values`.""" + spec = self._get_choiceset_values_spec(choiceset_id, start=start, limit=limit) + response = await self.request_async( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + return self._parse_choiceset_values(response) + + # ------------------------------------------------------------------ + # List records (multi-record read with OData filters) + # ------------------------------------------------------------------ + + def list_records( + self, + entity_key: str, + schema: Optional[Type[Any]] = None, + start: Optional[int] = None, + limit: Optional[int] = None, + expansion_level: Optional[int] = None, + filter: Optional[str] = None, + orderby: Optional[str] = None, + select: Optional[List[str]] = None, + expand: Optional[List[str]] = None, + ) -> EntityRecordsListResponse: + """Internal implementation; see :meth:`EntitiesService.list_records`.""" + spec = self._list_records_spec( + entity_key, + start=start, + limit=limit, + expansion_level=expansion_level, + filter=filter, + orderby=orderby, + select=select, + expand=expand, + ) + response = self.request(spec.method, spec.endpoint, params=spec.params) + return self._build_records_list_response(response, schema, start, limit) + + async def list_records_async( + self, + entity_key: str, + schema: Optional[Type[Any]] = None, + start: Optional[int] = None, + limit: Optional[int] = None, + expansion_level: Optional[int] = None, + filter: Optional[str] = None, + orderby: Optional[str] = None, + select: Optional[List[str]] = None, + expand: Optional[List[str]] = None, + ) -> EntityRecordsListResponse: + """Async variant of :meth:`list_records`.""" + spec = self._list_records_spec( + entity_key, + start=start, + limit=limit, + expansion_level=expansion_level, + filter=filter, + orderby=orderby, + select=select, + expand=expand, + ) + response = await self.request_async( + spec.method, spec.endpoint, params=spec.params + ) + return self._build_records_list_response(response, schema, start, limit) + + # ------------------------------------------------------------------ + # Single-record operations (fire trigger events; batch versions don't) + # ------------------------------------------------------------------ + + def insert_record( + self, + entity_key: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Internal implementation; see :meth:`EntitiesService.insert_record`.""" + spec = self._insert_record_spec(entity_key, data, expansion_level) + response = self.request( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + return EntityRecord.model_validate(response.json()) + + async def insert_record_async( + self, + entity_key: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Async variant of :meth:`insert_record`.""" + spec = self._insert_record_spec(entity_key, data, expansion_level) + response = await self.request_async( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + return EntityRecord.model_validate(response.json()) + + def get_record( + self, + entity_key: str, + record_id: str, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Fetch a single record by its id.""" + spec = self._get_record_spec(entity_key, record_id, expansion_level) + response = self.request(spec.method, spec.endpoint, params=spec.params) + return EntityRecord.model_validate(response.json()) + + async def get_record_async( + self, + entity_key: str, + record_id: str, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Async variant of :meth:`get_record`.""" + spec = self._get_record_spec(entity_key, record_id, expansion_level) + response = await self.request_async( + spec.method, spec.endpoint, params=spec.params + ) + return EntityRecord.model_validate(response.json()) + + def update_record( + self, + entity_key: str, + record_id: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Internal implementation; see :meth:`EntitiesService.update_record`.""" + spec = self._update_record_spec(entity_key, record_id, data, expansion_level) + response = self.request( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + return EntityRecord.model_validate(response.json()) + + async def update_record_async( + self, + entity_key: str, + record_id: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> EntityRecord: + """Async variant of :meth:`update_record`.""" + spec = self._update_record_spec(entity_key, record_id, data, expansion_level) + response = await self.request_async( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + return EntityRecord.model_validate(response.json()) + + def delete_record(self, entity_key: str, record_id: str) -> None: + """Delete a single record by id.""" + spec = self._delete_record_spec(entity_key, record_id) + self.request(spec.method, spec.endpoint) + + async def delete_record_async(self, entity_key: str, record_id: str) -> None: + """Async variant of :meth:`delete_record`.""" + spec = self._delete_record_spec(entity_key, record_id) + await self.request_async(spec.method, spec.endpoint) + + # ------------------------------------------------------------------ + # Batch record operations + # ------------------------------------------------------------------ + + def insert_records( + self, + entity_key: str, + records: List[Any], + schema: Optional[Type[Any]] = None, + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, + ) -> EntityRecordsBatchResponse: + """Internal implementation; see :meth:`EntitiesService.insert_records`.""" + spec = self._insert_batch_spec( + entity_key, + records, + expansion_level=expansion_level, + fail_on_first=fail_on_first, + ) + response = self._request_or_extract_batch( + sync_call=lambda: self.request( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + ) + if isinstance(response, EntityRecordsBatchResponse): + return response + return self.validate_entity_batch(response, schema) + + async def insert_records_async( + self, + entity_key: str, + records: List[Any], + schema: Optional[Type[Any]] = None, + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, + ) -> EntityRecordsBatchResponse: + """Async variant of :meth:`insert_records`.""" + spec = self._insert_batch_spec( + entity_key, + records, + expansion_level=expansion_level, + fail_on_first=fail_on_first, + ) + + async def _send_batch() -> Response: + return await self.request_async( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + + result = await self._request_or_extract_batch_async(_send_batch) + if isinstance(result, EntityRecordsBatchResponse): + return result + return self.validate_entity_batch(result, schema) + + def update_records( + self, + entity_key: str, + records: List[Any], + schema: Optional[Type[Any]] = None, + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, + ) -> EntityRecordsBatchResponse: + """Internal implementation; see :meth:`EntitiesService.update_records`.""" + normalized = [self._record_to_dict(record) for record in records] + if schema is not None: + for record in normalized: + EntityRecord.from_data(data=record, model=schema) + + spec = self._update_batch_spec( + entity_key, + normalized, + expansion_level=expansion_level, + fail_on_first=fail_on_first, + ) + response = self._request_or_extract_batch( + sync_call=lambda: self.request( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + ) + if isinstance(response, EntityRecordsBatchResponse): + return response + return self.validate_entity_batch(response, schema) + + async def update_records_async( + self, + entity_key: str, + records: List[Any], + schema: Optional[Type[Any]] = None, + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, + ) -> EntityRecordsBatchResponse: + """Async variant of :meth:`update_records`.""" + normalized = [self._record_to_dict(record) for record in records] + if schema is not None: + for record in normalized: + EntityRecord.from_data(data=record, model=schema) + + spec = self._update_batch_spec( + entity_key, + normalized, + expansion_level=expansion_level, + fail_on_first=fail_on_first, + ) + + async def _send_batch() -> Response: + return await self.request_async( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + + result = await self._request_or_extract_batch_async(_send_batch) + if isinstance(result, EntityRecordsBatchResponse): + return result + return self.validate_entity_batch(result, schema) + + def delete_records( + self, + entity_key: str, + record_ids: List[str], + fail_on_first: Optional[bool] = None, + ) -> EntityRecordsBatchResponse: + """Delete multiple records by id in a single batch.""" + spec = self._delete_batch_spec( + entity_key, record_ids, fail_on_first=fail_on_first + ) + result = self._request_or_extract_batch( + sync_call=lambda: self.request( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + ) + if isinstance(result, EntityRecordsBatchResponse): + return result + return EntityRecordsBatchResponse.model_validate(result.json()) + + async def delete_records_async( + self, + entity_key: str, + record_ids: List[str], + fail_on_first: Optional[bool] = None, + ) -> EntityRecordsBatchResponse: + """Async variant of :meth:`delete_records`.""" + spec = self._delete_batch_spec( + entity_key, record_ids, fail_on_first=fail_on_first + ) + + async def _send_batch() -> Response: + return await self.request_async( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + + result = await self._request_or_extract_batch_async(_send_batch) + if isinstance(result, EntityRecordsBatchResponse): + return result + return EntityRecordsBatchResponse.model_validate(result.json()) + + # ------------------------------------------------------------------ + # Structured query (POST /entity/{id}/query) + # ------------------------------------------------------------------ + + def retrieve_records( + self, + entity_key: str, + filter_group: Optional[EntityQueryFilterGroup] = None, + sort_options: Optional[List[EntityQuerySortOption]] = None, + selected_fields: Optional[List[str]] = None, + expansions: Optional[List[Any]] = None, + expansion_level: Optional[int] = None, + aggregates: Optional[List[EntityAggregate]] = None, + group_by: Optional[List[str]] = None, + joins: Optional[List[EntityJoin]] = None, + binnings: Optional[List[EntityBinning]] = None, + start: Optional[int] = None, + limit: Optional[int] = None, + ) -> RetrieveEntityRecordsResponse: + """Internal implementation; see :meth:`EntitiesService.retrieve_records`.""" + spec = self._retrieve_records_spec( + entity_key, + filter_group=filter_group, + sort_options=sort_options, + selected_fields=selected_fields, + expansions=expansions, + expansion_level=expansion_level, + aggregates=aggregates, + group_by=group_by, + joins=joins, + binnings=binnings, + start=start, + limit=limit, + ) + response = self.request( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + return self._parse_query_response(response, start=start, limit=limit) + + async def retrieve_records_async( + self, + entity_key: str, + filter_group: Optional[EntityQueryFilterGroup] = None, + sort_options: Optional[List[EntityQuerySortOption]] = None, + selected_fields: Optional[List[str]] = None, + expansions: Optional[List[Any]] = None, + expansion_level: Optional[int] = None, + aggregates: Optional[List[EntityAggregate]] = None, + group_by: Optional[List[str]] = None, + joins: Optional[List[EntityJoin]] = None, + binnings: Optional[List[EntityBinning]] = None, + start: Optional[int] = None, + limit: Optional[int] = None, + ) -> RetrieveEntityRecordsResponse: + """Async variant of :meth:`retrieve_records`.""" + spec = self._retrieve_records_spec( + entity_key, + filter_group=filter_group, + sort_options=sort_options, + selected_fields=selected_fields, + expansions=expansions, + expansion_level=expansion_level, + aggregates=aggregates, + group_by=group_by, + joins=joins, + binnings=binnings, + start=start, + limit=limit, + ) + response = await self.request_async( + spec.method, spec.endpoint, params=spec.params, json=spec.json + ) + return self._parse_query_response(response, start=start, limit=limit) + + # ------------------------------------------------------------------ + # Federated SQL query + # ------------------------------------------------------------------ + + def query_entity_records( + self, + sql_query: str, + ) -> List[Dict[str, Any]]: + """Internal implementation; see :meth:`EntitiesService.query_entity_records`.""" + return self._query_entities_for_records(sql_query) + + async def query_entity_records_async( + self, + sql_query: str, + ) -> List[Dict[str, Any]]: + """Async variant of :meth:`query_entity_records`.""" + return await self._query_entities_for_records_async(sql_query) + + # ------------------------------------------------------------------ + # Attachments + # ------------------------------------------------------------------ + + def upload_attachment( + self, + entity_id: str, + record_id: str, + field_name: str, + file: Optional[FileContent] = None, + file_path: Optional[str] = None, + expansion_level: Optional[int] = None, + ) -> Dict[str, Any]: + """Internal implementation; see :meth:`EntitiesService.upload_attachment`.""" + spec = self._attachment_endpoint( + entity_id, record_id, field_name, expansion_level + ) + with self._open_file(file, file_path) as handle: + response = self.request( + "POST", + spec.endpoint, + params=spec.params, + files={"file": handle}, + ) + return response.json() if response.content else {} + + async def upload_attachment_async( + self, + entity_id: str, + record_id: str, + field_name: str, + file: Optional[FileContent] = None, + file_path: Optional[str] = None, + expansion_level: Optional[int] = None, + ) -> Dict[str, Any]: + """Async variant of :meth:`upload_attachment`.""" + spec = self._attachment_endpoint( + entity_id, record_id, field_name, expansion_level + ) + with self._open_file(file, file_path) as handle: + response = await self.request_async( + "POST", + spec.endpoint, + params=spec.params, + files={"file": handle}, + ) + return response.json() if response.content else {} + + def download_attachment( + self, entity_id: str, record_id: str, field_name: str + ) -> bytes: + """Internal implementation; see :meth:`EntitiesService.download_attachment`.""" + spec = self._attachment_endpoint(entity_id, record_id, field_name) + response = self.request("GET", spec.endpoint) + return response.content + + async def download_attachment_async( + self, entity_id: str, record_id: str, field_name: str + ) -> bytes: + """Async variant of :meth:`download_attachment`.""" + spec = self._attachment_endpoint(entity_id, record_id, field_name) + response = await self.request_async("GET", spec.endpoint) + return response.content + + def delete_attachment( + self, + entity_id: str, + record_id: str, + field_name: str, + expansion_level: Optional[int] = None, + ) -> Dict[str, Any]: + """Internal implementation; see :meth:`EntitiesService.delete_attachment`.""" + spec = self._attachment_endpoint( + entity_id, record_id, field_name, expansion_level + ) + response = self.request("DELETE", spec.endpoint, params=spec.params) + return response.json() if response.content else {} + + async def delete_attachment_async( + self, + entity_id: str, + record_id: str, + field_name: str, + expansion_level: Optional[int] = None, + ) -> Dict[str, Any]: + """Async variant of :meth:`delete_attachment`.""" + spec = self._attachment_endpoint( + entity_id, record_id, field_name, expansion_level + ) + response = await self.request_async("DELETE", spec.endpoint, params=spec.params) + return response.json() if response.content else {} + + # ------------------------------------------------------------------ + # Bulk import + # ------------------------------------------------------------------ + + def import_records( + self, + entity_id: str, + file: Optional[FileContent] = None, + file_path: Optional[str] = None, + ) -> EntityImportRecordsResponse: + """Internal implementation; see :meth:`EntitiesService.import_records`.""" + spec = self._import_records_spec(entity_id) + with self._open_file(file, file_path) as handle: + response = self.request(spec.method, spec.endpoint, files={"file": handle}) + return EntityImportRecordsResponse.model_validate(response.json() or {}) + + async def import_records_async( + self, + entity_id: str, + file: Optional[FileContent] = None, + file_path: Optional[str] = None, + ) -> EntityImportRecordsResponse: + """Async variant of :meth:`import_records`.""" + spec = self._import_records_spec(entity_id) + with self._open_file(file, file_path) as handle: + response = await self.request_async( + spec.method, spec.endpoint, files={"file": handle} + ) + return EntityImportRecordsResponse.model_validate(response.json() or {}) + + # ------------------------------------------------------------------ + # Public helper for batch response validation + # ------------------------------------------------------------------ + + def validate_entity_batch( + self, + batch_response: Response, + schema: Optional[Type[Any]] = None, + ) -> EntityRecordsBatchResponse: + """Internal implementation; see :meth:`EntitiesService.validate_entity_batch`.""" + parsed = EntityRecordsBatchResponse.model_validate(batch_response.json()) + + validated_successful_records = [] + for successful_record in parsed.success_records: + data = successful_record.model_dump(by_alias=True) + if data.get("Id") is not None: + validated_successful_records.append( + EntityRecord.from_data(data=data, model=schema) + ) + + return EntityRecordsBatchResponse( + success_records=validated_successful_records, + failure_records=parsed.failure_records, + ) + + # ------------------------------------------------------------------ + # Internal helpers — request specs + # ------------------------------------------------------------------ + + def _query_entities_for_records(self, sql_query: str) -> List[Dict[str, Any]]: + """Synchronously run a validated SQL query through the federated query engine.""" + self._validate_sql_query(sql_query) + routing_context = self._routing_strategy.resolve() + spec = self._query_entity_records_spec(sql_query, routing_context) + response = self.request(spec.method, spec.endpoint, json=spec.json) + return response.json().get("results", []) + + async def _query_entities_for_records_async( + self, sql_query: str + ) -> List[Dict[str, Any]]: + """Asynchronously run a validated SQL query through the federated query engine.""" + self._validate_sql_query(sql_query) + routing_context = await self._routing_strategy.resolve_async() + spec = self._query_entity_records_spec(sql_query, routing_context) + response = await self.request_async(spec.method, spec.endpoint, json=spec.json) + return response.json().get("results", []) + + @staticmethod + def _list_records_spec( + entity_key: str, + start: Optional[int] = None, + limit: Optional[int] = None, + expansion_level: Optional[int] = None, + filter: Optional[str] = None, + orderby: Optional[str] = None, + select: Optional[List[str]] = None, + expand: Optional[List[str]] = None, + ) -> RequestSpec: + """Build the GET spec for the multi-record read endpoint.""" + params: Dict[str, Any] = {} + if start is not None: + params["start"] = start + if limit is not None: + params["limit"] = limit + if expansion_level is not None: + params["expansionLevel"] = expansion_level + if filter is not None: + params["$filter"] = filter + if orderby is not None: + params["$orderby"] = orderby + if select: + params["$select"] = ",".join(select) + if expand: + params["$expand"] = ",".join(expand) + return RequestSpec( + method="GET", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/read" + ), + params=params, + ) + + @staticmethod + def _insert_record_spec( + entity_key: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> RequestSpec: + """Build the POST spec for inserting a single record.""" + params: Dict[str, Any] = {} + if expansion_level is not None: + params["expansionLevel"] = expansion_level + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/insert" + ), + params=params, + json=EntityDataService._record_to_dict(data), + ) + + @staticmethod + def _get_record_spec( + entity_key: str, + record_id: str, + expansion_level: Optional[int] = None, + ) -> RequestSpec: + """Build the GET spec for fetching a single record by id.""" + params: Dict[str, Any] = {} + if expansion_level is not None: + params["expansionLevel"] = expansion_level + return RequestSpec( + method="GET", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/read/{record_id}" + ), + params=params, + ) + + @staticmethod + def _update_record_spec( + entity_key: str, + record_id: str, + data: Any, + expansion_level: Optional[int] = None, + ) -> RequestSpec: + """Build the POST spec for updating a single record by id.""" + params: Dict[str, Any] = {} + if expansion_level is not None: + params["expansionLevel"] = expansion_level + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/update/{record_id}" + ), + params=params, + json=EntityDataService._record_to_dict(data), + ) + + @staticmethod + def _delete_record_spec(entity_key: str, record_id: str) -> RequestSpec: + """Build the DELETE spec for removing a single record by id.""" + return RequestSpec( + method="DELETE", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/delete/{record_id}" + ), + ) + + @staticmethod + def _insert_batch_spec( + entity_key: str, + records: List[Any], + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, + ) -> RequestSpec: + """Build the POST spec for the batch-insert endpoint.""" + params = EntityDataService._batch_params( + expansion_level=expansion_level, fail_on_first=fail_on_first + ) + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/insert-batch" + ), + params=params, + json=[EntityDataService._record_to_dict(record) for record in records], + ) + + @staticmethod + def _update_batch_spec( + entity_key: str, + records: List[Dict[str, Any]], + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, + ) -> RequestSpec: + """Build the POST spec for the batch-update endpoint.""" + params = EntityDataService._batch_params( + expansion_level=expansion_level, fail_on_first=fail_on_first + ) + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/update-batch" + ), + params=params, + json=records, + ) + + @staticmethod + def _delete_batch_spec( + entity_key: str, + record_ids: List[str], + fail_on_first: Optional[bool] = None, + ) -> RequestSpec: + """Build the POST spec for the batch-delete endpoint.""" + params = EntityDataService._batch_params(fail_on_first=fail_on_first) + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/delete-batch" + ), + params=params, + json=record_ids, + ) + + @staticmethod + def _batch_params( + expansion_level: Optional[int] = None, + fail_on_first: Optional[bool] = None, + ) -> Dict[str, Any]: + """Build the optional URL params common to all batch endpoints.""" + params: Dict[str, Any] = {} + if expansion_level is not None: + params["expansionLevel"] = expansion_level + if fail_on_first is not None: + params["failOnFirst"] = "true" if fail_on_first else "false" + return params + + @staticmethod + def _retrieve_records_spec( + entity_key: str, + filter_group: Optional[EntityQueryFilterGroup] = None, + sort_options: Optional[List[EntityQuerySortOption]] = None, + selected_fields: Optional[List[str]] = None, + expansions: Optional[List[Any]] = None, + expansion_level: Optional[int] = None, + aggregates: Optional[List[Any]] = None, + group_by: Optional[List[str]] = None, + joins: Optional[List[EntityJoin]] = None, + binnings: Optional[List[EntityBinning]] = None, + start: Optional[int] = None, + limit: Optional[int] = None, + ) -> RequestSpec: + """Build the request spec for the structured-query endpoint. + + Filters, sorting, projection, expansions, aggregates, group-by, joins, + binnings, ``start``, and ``limit`` are placed in the JSON body; + ``expansionLevel`` is a URL query parameter. The V2 endpoint is used + only when ``binnings`` are supplied. + """ + body: Dict[str, Any] = {} + if filter_group is not None: + body["filterGroup"] = filter_group.model_dump( + by_alias=True, exclude_none=True + ) + if sort_options: + body["sortOptions"] = [ + opt.model_dump(by_alias=True, exclude_none=True) for opt in sort_options + ] + if selected_fields: + body["selectedFields"] = list(selected_fields) + if expansions: + body["expansions"] = [ + e.model_dump(by_alias=True, exclude_none=True) + if isinstance(e, BaseModel) + else e + for e in expansions + ] + if aggregates: + body["aggregates"] = [ + a.model_dump(by_alias=True, exclude_none=True) + if isinstance(a, BaseModel) + else a + for a in aggregates + ] + if group_by: + body["groupBy"] = list(group_by) + if joins: + body["joins"] = [ + j.model_dump(by_alias=True, exclude_none=True) for j in joins + ] + if binnings: + body["binnings"] = [ + b.model_dump(by_alias=True, exclude_none=True) for b in binnings + ] + if start is not None: + body["start"] = start + if limit is not None: + body["limit"] = limit + + params: Dict[str, Any] = {} + if expansion_level is not None: + params["expansionLevel"] = expansion_level + + if binnings: + endpoint = Endpoint( + f"datafabric_/api/v2/EntityService/entity/{entity_key}/query" + ) + else: + endpoint = Endpoint( + f"datafabric_/api/EntityService/entity/{entity_key}/query" + ) + + return RequestSpec( + method="POST", + endpoint=endpoint, + params=params, + json=body, + ) + + @staticmethod + def _query_entity_records_spec( + sql_query: str, + routing_context: Optional[QueryRoutingOverrideContext] = None, + ) -> RequestSpec: + """Build the POST spec for the federated SQL query endpoint.""" + body: Dict[str, Any] = {"query": sql_query} + if routing_context: + body["routingContext"] = routing_context.model_dump( + by_alias=True, exclude_none=True + ) + return RequestSpec( + method="POST", + endpoint=Endpoint("datafabric_/api/v1/query/execute"), + json=body, + ) + + @staticmethod + def _get_choiceset_values_spec( + choiceset_id: str, + start: Optional[int] = None, + limit: Optional[int] = None, + ) -> RequestSpec: + """Build the POST spec for the choice-set values endpoint.""" + params: Dict[str, Any] = {} + if start is not None: + params["start"] = start + if limit is not None: + params["limit"] = limit + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{choiceset_id}/query_expansion" + ), + params=params, + json={}, + ) + + @staticmethod + def _attachment_endpoint( + entity_id: str, + record_id: str, + field_name: str, + expansion_level: Optional[int] = None, + ) -> RequestSpec: + """Return the attachment endpoint plus any ``expansionLevel`` query param. + + The HTTP verb is supplied by the caller; only the URL and query + parameters depend on these arguments. + """ + params: Dict[str, Any] = {} + if expansion_level is not None: + params["expansionLevel"] = expansion_level + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/Attachment/entity/{entity_id}/{record_id}/{field_name}" + ), + params=params, + ) + + @staticmethod + def _import_records_spec(entity_id: str) -> RequestSpec: + """Build the POST spec for the bulk-upload (CSV import) endpoint.""" + return RequestSpec( + method="POST", + endpoint=Endpoint( + f"datafabric_/api/EntityService/entity/{entity_id}/bulk-upload" + ), + ) + + @staticmethod + def _open_file(file: Optional[FileContent], file_path: Optional[str]) -> Any: + """Yield a file-like object from raw bytes or a path on disk. + + Exactly one of ``file`` and ``file_path`` must be supplied. + """ + if (file is None) == (file_path is None): + raise ValueError( + "Provide exactly one of `file` (bytes) or `file_path` (str path on disk)." + ) + if file_path is not None: + return open(Path(file_path), "rb") + return nullcontext(file) + + # ------------------------------------------------------------------ + # Internal helpers — response parsing and record normalisation + # ------------------------------------------------------------------ + + @staticmethod + def _record_to_dict(record: Any) -> Dict[str, Any]: + """Normalize an input record to a plain dict. + + Accepts dicts, Pydantic ``BaseModel`` (including :class:`EntityRecord`), + or any object exposing ``__dict__``. Explicit ``None`` values are + preserved so callers can clear fields by setting them to ``None`` on a + model instance — only unset fields (whose Pydantic default applies) are + dropped via ``exclude_unset=True``. + """ + if isinstance(record, dict): + return dict(record) + if isinstance(record, BaseModel): + return record.model_dump(by_alias=True, exclude_unset=True) + if hasattr(record, "__dict__"): + return {k: v for k, v in record.__dict__.items() if not k.startswith("_")} + raise TypeError( + f"Cannot convert record of type {type(record).__name__} to dict — " + "pass a dict, an EntityRecord, a Pydantic BaseModel, or an object with __dict__." + ) + + @staticmethod + def _build_records_list_response( + response: Response, + schema: Optional[Type[Any]], + start: Optional[int], + limit: Optional[int], + ) -> EntityRecordsListResponse: + """Build an :class:`EntityRecordsListResponse` from a list-records body.""" + body = response.json() or {} + records_data = body.get("value", []) + total_count = int( + body.get("totalRecordCount", body.get("totalCount", len(records_data))) or 0 + ) + records = [ + EntityRecord.from_data(data=record, model=schema) for record in records_data + ] + + next_cursor = body.get("nextCursor") + if limit is not None and limit > 0: + consumed = (start or 0) + len(records) + has_next_page = consumed < total_count + else: + has_next_page = bool(body.get("hasNextPage", False)) + + return EntityRecordsListResponse( + items=records, + total_count=total_count, + has_next_page=has_next_page, + next_cursor=next_cursor, + ) + + @staticmethod + def _parse_query_response( + response: Response, + start: Optional[int] = None, + limit: Optional[int] = None, + ) -> RetrieveEntityRecordsResponse: + """Parse a query response into :class:`RetrieveEntityRecordsResponse`. + + Rows that include an ``Id`` field are parsed as :class:`EntityRecord`; + rows that don't (aggregate / group-by / binning results) are parsed as + :class:`AggregateRow`. ``has_next_page`` is derived from + ``start + len(items) < total_count`` whenever ``limit`` is supplied; + ``next_cursor`` is populated only when the backend returns one, + otherwise the caller paginates by passing the next ``start``. + """ + body = response.json() or {} + items_raw = body.get("value", []) or [] + items: List[EntityRecord | AggregateRow] = [] + for raw in items_raw: + if isinstance(raw, dict) and isinstance(raw.get("Id"), str): + items.append(EntityRecord.from_data(data=raw)) + else: + items.append(AggregateRow.model_validate(raw)) + + total_count = int(body.get("totalRecordCount", body.get("totalCount", 0)) or 0) + + next_cursor: Optional[str] = body.get("nextCursor") + has_next_page = bool(body.get("hasNextPage", False)) + if next_cursor is None and limit is not None and limit > 0: + consumed = (start or 0) + len(items) + has_next_page = consumed < total_count + + return RetrieveEntityRecordsResponse( + items=items, + total_count=total_count, + has_next_page=has_next_page, + next_cursor=next_cursor, + ) + + @staticmethod + def _parse_choiceset_values(response: Response) -> List[ChoiceSetValue]: + """Decode and return the choice-set values from a query-expansion response.""" + data = response.json() + raw_values = data.get("jsonValue", "[]") + items = ( + json_module.loads(raw_values) if isinstance(raw_values, str) else raw_values + ) + return [ChoiceSetValue.model_validate(item) for item in items] + + # ------------------------------------------------------------------ + # Internal helpers — batch error recovery + # ------------------------------------------------------------------ + + def _request_or_extract_batch( + self, + sync_call: Any, + ) -> Response | EntityRecordsBatchResponse: + """Run a batch request and recover per-record failures from a 400 body. + + On HTTP 400 with a body that lists both ``successRecords`` and + ``failureRecords``, returns the parsed batch response instead of + raising. All other errors propagate. + """ + try: + return sync_call() + except EnrichedException as exc: + extracted = self._extract_batch_response_from_error(exc) + if extracted is not None: + return extracted + raise + + async def _request_or_extract_batch_async( + self, + async_call: Any, + ) -> Response | EntityRecordsBatchResponse: + """Async variant of :meth:`_request_or_extract_batch`.""" + try: + return await async_call() + except EnrichedException as exc: + extracted = self._extract_batch_response_from_error(exc) + if extracted is not None: + return extracted + raise + + @staticmethod + def _extract_batch_response_from_error( + exc: EnrichedException, + ) -> Optional[EntityRecordsBatchResponse]: + """Return a parsed batch response when the error body matches the per-record-failure shape. + + Recovery is intentionally narrow: only HTTP 400 with a JSON object + containing list-typed ``successRecords`` and ``failureRecords`` keys. + Returns ``None`` for any other status, body shape, or parse failure + so that the original error propagates. + """ + cause = exc.__cause__ + if not isinstance(cause, HTTPStatusError): + return None + if cause.response.status_code != 400: + return None + try: + data = cause.response.json() + except Exception: + return None + if not isinstance(data, dict): + return None + if not ( + isinstance(data.get("successRecords"), list) + and isinstance(data.get("failureRecords"), list) + ): + return None + try: + return EntityRecordsBatchResponse.model_validate(data) + except Exception: + return None + + # ------------------------------------------------------------------ + # Internal helpers — SQL validation (federated query path) + # ------------------------------------------------------------------ + + def _validate_sql_query(self, sql_query: str) -> None: + """Validate a SQL string for the federated query endpoint client-side.""" + query = sql_query.strip().rstrip(";").strip() + if not query: + raise ValueError("SQL query cannot be empty.") + + statements = sqlparse.parse(query) + if len(statements) != 1 or not statements[0].tokens: + raise ValueError("Only a single SELECT statement is allowed.") + + stmt = statements[0] + stmt_type = stmt.get_type() + + if stmt_type != "SELECT": + raise ValueError("Only SELECT statements are allowed.") + + keywords = set() + for token in stmt.flatten(): + if token.ttype in Keyword: + keywords.add(token.normalized) + + for kw in _FORBIDDEN_DML: + if kw in keywords: + raise ValueError(f"SQL keyword '{kw}' is not allowed.") + + for kw in _FORBIDDEN_DDL: + if kw in keywords: + raise ValueError(f"SQL keyword '{kw}' is not allowed.") + + for kw in _DISALLOWED_KEYWORDS: + if kw in keywords: + raise ValueError( + f"SQL construct '{kw}' is not allowed in entity queries." + ) + + if self._has_subquery(stmt): + raise ValueError("Subqueries are not allowed.") + + has_where = any(isinstance(t, Where) for t in stmt.tokens) + has_limit = "LIMIT" in keywords + has_from = "FROM" in keywords + + if not has_from: + raise ValueError("Queries must include a FROM clause.") + + projection = self._projection_tokens(stmt) + + if self._projection_has_count_star(projection): + raise ValueError( + "COUNT(*) is not supported. Use COUNT(column_name) instead." + ) + + has_aggregate = self._projection_has_aggregate(projection) + + if not has_where and not has_limit and not has_aggregate: + raise ValueError("Queries without WHERE must include a LIMIT clause.") + + has_bare_wildcard = self._projection_has_bare_wildcard(projection) + if has_bare_wildcard: + raise ValueError("SELECT * is not allowed. Specify column names instead.") + if not has_where and self._projection_column_count(projection) > 4: + raise ValueError( + "Selecting more than 4 columns without filtering is not allowed." + ) + + @staticmethod + def _projection_has_aggregate( + projection: List[sqlparse.sql.Token], + ) -> bool: + """Return ``True`` when the projection contains an aggregate function call.""" + + def _has_agg(token: sqlparse.sql.Token) -> bool: + if isinstance(token, Function): + return token.get_name().upper() in _AGGREGATE_FUNCTIONS + if isinstance(token, Identifier): + return any(_has_agg(child) for child in token.tokens) + return False + + for node in projection: + if _has_agg(node): + return True + if isinstance(node, IdentifierList): + if any(_has_agg(child) for child in node.tokens): + return True + return False + + @staticmethod + def _projection_has_count_star( + projection: List[sqlparse.sql.Token], + ) -> bool: + """Return ``True`` when the projection contains ``COUNT(*)``.""" + + def _is_count_star(func: Function) -> bool: + if func.get_name().upper() != "COUNT": + return False + return any(t.ttype is Wildcard for t in func.flatten()) + + def _has_count_star(token: sqlparse.sql.Token) -> bool: + if isinstance(token, Function): + return _is_count_star(token) + if isinstance(token, Identifier): + return any(_has_count_star(child) for child in token.tokens) + return False + + for node in projection: + if _has_count_star(node): + return True + if isinstance(node, IdentifierList): + if any(_has_count_star(child) for child in node.tokens): + return True + return False + + @staticmethod + def _projection_has_bare_wildcard( + projection: List[sqlparse.sql.Token], + ) -> bool: + """Return ``True`` for a bare ``*`` or qualified ``table.*`` outside a function.""" + + def _identifier_has_wildcard(ident: Identifier) -> bool: + return any(t.ttype is Wildcard for t in ident.tokens) + + for node in projection: + if node.ttype is Wildcard: + return True + if isinstance(node, Identifier) and _identifier_has_wildcard(node): + return True + if isinstance(node, IdentifierList): + for child in node.tokens: + if child.ttype is Wildcard: + return True + if isinstance(child, Identifier) and _identifier_has_wildcard( + child + ): + return True + return False + + @staticmethod + def _has_subquery(stmt: sqlparse.sql.Statement) -> bool: + """Recursively walk the AST looking for a SELECT inside parentheses.""" + + def _walk(token: sqlparse.sql.Token) -> bool: + if isinstance(token, Parenthesis): + for child in token.flatten(): + if child.ttype is DML and child.normalized == "SELECT": + return True + if hasattr(token, "tokens"): + for child in token.tokens: + if _walk(child): + return True + return False + + for token in stmt.tokens: + if _walk(token): + return True + return False + + @staticmethod + def _projection_tokens( + stmt: sqlparse.sql.Statement, + ) -> List[sqlparse.sql.Token]: + """Return the non-flattened AST nodes between the first SELECT and FROM.""" + tokens: List[sqlparse.sql.Token] = [] + collecting = False + for token in stmt.tokens: + if token.ttype is DML and token.normalized == "SELECT": + collecting = True + continue + if token.ttype is Keyword and token.normalized in ("FROM", "INTO"): + break + if token.ttype is Keyword and token.normalized == "DISTINCT": + continue + if collecting and token.ttype is not Whitespace: + tokens.append(token) + return tokens + + @staticmethod + def _projection_column_count( + projection: List[sqlparse.sql.Token], + ) -> int: + """Return the number of columns referenced by the projection.""" + for node in projection: + if isinstance(node, IdentifierList): + return len(list(node.get_identifiers())) + if isinstance(node, (Identifier, Function)): + return 1 + if node.ttype is Wildcard: + return 1 + return 0 diff --git a/packages/uipath-platform/src/uipath/platform/entities/_entity_schema_service.py b/packages/uipath-platform/src/uipath/platform/entities/_entity_schema_service.py new file mode 100644 index 000000000..9872969d6 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/entities/_entity_schema_service.py @@ -0,0 +1,522 @@ +"""Schema-side operations for the Data Fabric entities surface. + +Handles entity definitions, choice set listings, and the create / delete / +update-metadata lifecycle that targets the backend ``EntityController``. +Record CRUD, queries, attachments, and bulk import live on +:class:`EntityDataService` and are mediated by :class:`EntitiesService`. +""" + +import re +from typing import Any, Dict, List, Optional + +from httpx import Response + +from ..common._base_service import BaseService +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..common._models import Endpoint, RequestSpec +from ..common.constants import HEADER_FOLDER_KEY +from ..orchestrator._folder_service import FolderService +from .entities import ( + ENTITY_FIELD_CONSTRAINT_DEFAULTS, + ENTITY_FIELD_CONSTRAINT_SPEC, + ENTITY_SCHEMA_FIELD_TYPE_MAP, + RESERVED_FIELD_NAMES, + Entity, + EntityCreateFieldOptions, + EntityCreateOptions, + EntityFieldDataType, + EntityMetadataUpdateOptions, +) + +DATA_FABRIC_TENANT_FOLDER_ID = "00000000-0000-0000-0000-000000000000" + +_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9]*$") +"""Entity and field name pattern: must start with a letter, then letters and digits only. + +Matches the UI's create-entity / create-field form validators so any name accepted +here can later be displayed or edited through the Data Service UI. +""" + +_ENTITY_NAME_MIN_LENGTH = 1 +_ENTITY_NAME_MAX_LENGTH = 30 +_FIELD_NAME_MIN_LENGTH = 3 +_FIELD_NAME_MAX_LENGTH = 100 + + +class EntitySchemaService(BaseService): + """HTTP service for entity-schema operations. + + Provides retrieval and lifecycle management for entities and choice sets. + Backend target: ``datafabric_/api/Entity``. + + See Also: + https://docs.uipath.com/data-service/automation-cloud/latest/user-guide/introduction + + !!! warning "Preview Feature" + This service is currently experimental. Behavior and parameters are + subject to change in future versions. + """ + + def __init__( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: Optional[FolderService] = None, + ) -> None: + """Initialise the schema service.""" + super().__init__(config=config, execution_context=execution_context) + self._folders_service = folders_service + + def retrieve(self, entity_key: str) -> Entity: + """Internal implementation; see :meth:`EntitiesService.retrieve`.""" + spec = self._retrieve_spec(entity_key) + response = self.request(spec.method, spec.endpoint) + return Entity.model_validate(response.json()) + + async def retrieve_async(self, entity_key: str) -> Entity: + """Async variant of :meth:`retrieve`.""" + spec = self._retrieve_spec(entity_key) + response = await self.request_async(spec.method, spec.endpoint) + return Entity.model_validate(response.json()) + + def retrieve_by_name( + self, entity_name: str, folder_key: Optional[str] = None + ) -> Entity: + """Internal implementation; see :meth:`EntitiesService.retrieve_by_name`.""" + spec = self._retrieve_by_name_spec(entity_name) + headers = self._folder_key_headers(folder_key) + response = self.request(spec.method, spec.endpoint, headers=headers) + return Entity.model_validate(response.json()) + + async def retrieve_by_name_async( + self, entity_name: str, folder_key: Optional[str] = None + ) -> Entity: + """Async variant of :meth:`retrieve_by_name`.""" + spec = self._retrieve_by_name_spec(entity_name) + headers = self._folder_key_headers(folder_key) + response = await self.request_async(spec.method, spec.endpoint, headers=headers) + return Entity.model_validate(response.json()) + + def list_entities(self) -> List[Entity]: + """Internal implementation; see :meth:`EntitiesService.list_entities`.""" + spec = self._list_entities_spec() + response = self.request(spec.method, spec.endpoint) + entities_data = response.json() + return [Entity.model_validate(entity) for entity in entities_data] + + async def list_entities_async(self) -> List[Entity]: + """Async variant of :meth:`list_entities`.""" + spec = self._list_entities_spec() + response = await self.request_async(spec.method, spec.endpoint) + entities_data = response.json() + return [Entity.model_validate(entity) for entity in entities_data] + + def list_choicesets(self) -> List[Entity]: + """Internal implementation; see :meth:`EntitiesService.list_choicesets`.""" + spec = self._list_choicesets_spec() + response = self.request(spec.method, spec.endpoint) + return [Entity.model_validate(item) for item in response.json()] + + async def list_choicesets_async(self) -> List[Entity]: + """Async variant of :meth:`list_choicesets`.""" + spec = self._list_choicesets_spec() + response = await self.request_async(spec.method, spec.endpoint) + return [Entity.model_validate(item) for item in response.json()] + + def create_entity( + self, + name: str, + fields: List[EntityCreateFieldOptions], + options: Optional[EntityCreateOptions] = None, + ) -> str: + """Internal implementation; see :meth:`EntitiesService.create_entity`.""" + spec = self._create_entity_spec(name, fields, options) + response = self.request(spec.method, spec.endpoint, json=spec.json) + return self._extract_entity_id(response) + + async def create_entity_async( + self, + name: str, + fields: List[EntityCreateFieldOptions], + options: Optional[EntityCreateOptions] = None, + ) -> str: + """Async variant of :meth:`create_entity`.""" + spec = self._create_entity_spec(name, fields, options) + response = await self.request_async(spec.method, spec.endpoint, json=spec.json) + return self._extract_entity_id(response) + + def delete_entity(self, entity_id: str) -> None: + """Delete an entity and all of its records.""" + spec = self._delete_entity_spec(entity_id) + self.request(spec.method, spec.endpoint) + + async def delete_entity_async(self, entity_id: str) -> None: + """Async variant of :meth:`delete_entity`.""" + spec = self._delete_entity_spec(entity_id) + await self.request_async(spec.method, spec.endpoint) + + def update_entity_metadata( + self, + entity_id: str, + metadata: EntityMetadataUpdateOptions | Dict[str, Any], + ) -> None: + """Internal implementation; see :meth:`EntitiesService.update_entity_metadata`.""" + spec = self._update_entity_metadata_spec(entity_id, metadata) + self.request(spec.method, spec.endpoint, json=spec.json) + + async def update_entity_metadata_async( + self, + entity_id: str, + metadata: EntityMetadataUpdateOptions | Dict[str, Any], + ) -> None: + """Async variant of :meth:`update_entity_metadata`.""" + spec = self._update_entity_metadata_spec(entity_id, metadata) + await self.request_async(spec.method, spec.endpoint, json=spec.json) + + # ------------------------------------------------------------------ + # Request-spec builders + # ------------------------------------------------------------------ + + @staticmethod + def _retrieve_spec(entity_key: str) -> RequestSpec: + """Build the GET spec for fetching an entity by key.""" + return RequestSpec( + method="GET", + endpoint=Endpoint(f"datafabric_/api/Entity/{entity_key}"), + ) + + @staticmethod + def _retrieve_by_name_spec(entity_name: str) -> RequestSpec: + """Build the GET spec for fetching an entity by name.""" + return RequestSpec( + method="GET", + endpoint=Endpoint(f"datafabric_/api/Entity/{entity_name}/metadata"), + ) + + @staticmethod + def _folder_key_headers(folder_key: Optional[str]) -> Dict[str, str]: + """Return the folder-key header dict, empty when no key is supplied.""" + if folder_key: + return {HEADER_FOLDER_KEY: folder_key} + return {} + + @staticmethod + def _list_entities_spec() -> RequestSpec: + """Build the GET spec for listing all entities (non-choice-sets).""" + return RequestSpec( + method="GET", + endpoint=Endpoint("datafabric_/api/Entity"), + ) + + @staticmethod + def _list_choicesets_spec() -> RequestSpec: + """Build the GET spec for listing all choice sets.""" + return RequestSpec( + method="GET", + endpoint=Endpoint("datafabric_/api/Entity/choiceset"), + ) + + @classmethod + def _create_entity_spec( + cls, + name: str, + fields: List[EntityCreateFieldOptions], + options: Optional[EntityCreateOptions] = None, + ) -> RequestSpec: + """Build the POST spec for creating an entity with its field schema.""" + cls._validate_name(name, "entity") + for field in fields: + cls._validate_name(field.field_name, "field") + opts = options or EntityCreateOptions() + # The user-facing option ``is_analytics_enabled`` maps to the legacy + # backend field name ``isInsightsEnabled`` — the wire name predates + # the "Analytics" UI rename. + payload: Dict[str, Any] = { + "displayName": opts.display_name or name, + "entityDefinition": { + "name": name, + "fields": [cls._build_schema_field_payload(f) for f in fields], + "folderId": opts.folder_key or DATA_FABRIC_TENANT_FOLDER_ID, + "isRbacEnabled": bool(opts.is_rbac_enabled or False), + "isInsightsEnabled": bool(opts.is_analytics_enabled or False), + "externalFields": opts.external_fields or [], + }, + } + if opts.description is not None: + payload["description"] = opts.description + return RequestSpec( + method="POST", + endpoint=Endpoint("datafabric_/api/Entity"), + json=payload, + ) + + @staticmethod + def _delete_entity_spec(entity_id: str) -> RequestSpec: + """Build the DELETE spec for removing an entity.""" + return RequestSpec( + method="DELETE", + endpoint=Endpoint(f"datafabric_/api/Entity/{entity_id}"), + ) + + @staticmethod + def _update_entity_metadata_spec( + entity_id: str, + metadata: EntityMetadataUpdateOptions | Dict[str, Any], + ) -> RequestSpec: + """Build the PATCH spec for updating entity metadata. + + Dict inputs are validated through :class:`EntityMetadataUpdateOptions` + so snake_case keys (``display_name``) and camelCase keys + (``displayName``) both serialise to the API field names the backend + expects. + """ + if not isinstance(metadata, EntityMetadataUpdateOptions): + metadata = EntityMetadataUpdateOptions.model_validate(metadata) + body = metadata.model_dump(by_alias=True, exclude_none=True) + return RequestSpec( + method="PATCH", + endpoint=Endpoint(f"datafabric_/api/Entity/{entity_id}/metadata"), + json=body, + ) + + @classmethod + def _build_schema_field_payload( + cls, field: EntityCreateFieldOptions + ) -> Dict[str, Any]: + """Build the API field payload for a single field on create-entity. + + Maps :class:`EntityFieldDataType` to the backend's ``sqlType.name`` and + ``fieldDisplayType`` (e.g. ``STRING`` becomes ``NVARCHAR`` / ``Basic``). + Caller-supplied constraints are validated against + :data:`ENTITY_FIELD_CONSTRAINT_SPEC`; unsupplied per-type constraints + fall back to :data:`ENTITY_FIELD_CONSTRAINT_DEFAULTS` so the field is + persisted fully and remains editable later. + """ + ftype = field.type or EntityFieldDataType.STRING + cls._validate_name(field.field_name, "field") + cls._validate_field_constraints(ftype, field) + + sql_type_name, field_display_type = ENTITY_SCHEMA_FIELD_TYPE_MAP[ftype] + sql_type: Dict[str, Any] = {"name": sql_type_name} + sql_type.update(cls._build_sql_type_constraints(ftype, field)) + + payload: Dict[str, Any] = { + "name": field.field_name, + "displayName": field.display_name or field.field_name, + "sqlType": sql_type, + "fieldDisplayType": field_display_type, + "description": field.description or "", + "isRequired": bool(field.is_required or False), + "isUnique": bool(field.is_unique or False), + "isRbacEnabled": bool(field.is_rbac_enabled or False), + "isEncrypted": bool(field.is_encrypted or False), + } + if field.default_value is not None: + payload["defaultValue"] = field.default_value + if field.choice_set_id is not None: + payload["choiceSetId"] = field.choice_set_id + if field.reference_entity_name is not None: + payload["referenceEntityName"] = field.reference_entity_name + if field.reference_field_name is not None: + payload["referenceFieldName"] = field.reference_field_name + return payload + + @staticmethod + def _build_sql_type_constraints( + ftype: EntityFieldDataType, field: EntityCreateFieldOptions + ) -> Dict[str, Any]: + """Return the ``sqlType`` constraint fields required for ``ftype``. + + Caller-supplied values override defaults where the type accepts them; + types that take no constraints (UUID, DATETIME, CHOICE_SET_SINGLE, + AUTO_NUMBER) return an empty dict. + """ + d = ENTITY_FIELD_CONSTRAINT_DEFAULTS + if ftype is EntityFieldDataType.STRING: + return {"lengthLimit": field.length_limit or d["STRING_LENGTH_LIMIT"]} + if ftype is EntityFieldDataType.MULTILINE_TEXT: + return { + "lengthLimit": field.length_limit or d["MULTILINE_TEXT_LENGTH_LIMIT"] + } + if ftype is EntityFieldDataType.DECIMAL: + return { + "lengthLimit": d["DECIMAL_LENGTH_LIMIT"], + "decimalPrecision": ( + field.decimal_precision + if field.decimal_precision is not None + else d["DECIMAL_PRECISION"] + ), + "maxValue": ( + field.max_value + if field.max_value is not None + else d["NUMERIC_MAX_VALUE"] + ), + "minValue": ( + field.min_value + if field.min_value is not None + else d["NUMERIC_MIN_VALUE"] + ), + } + if ftype is EntityFieldDataType.BOOLEAN: + return {"lengthLimit": d["BOOLEAN_LENGTH_LIMIT"]} + if ftype in ( + EntityFieldDataType.DATE, + EntityFieldDataType.DATETIME_WITH_TZ, + ): + return {"lengthLimit": d["DATE_LENGTH_LIMIT"]} + if ftype in (EntityFieldDataType.INTEGER, EntityFieldDataType.BIG_INTEGER): + return { + "maxValue": ( + field.max_value + if field.max_value is not None + else d["NUMERIC_MAX_VALUE"] + ), + "minValue": ( + field.min_value + if field.min_value is not None + else d["NUMERIC_MIN_VALUE"] + ), + } + if ftype in (EntityFieldDataType.FLOAT, EntityFieldDataType.DOUBLE): + return { + "decimalPrecision": ( + field.decimal_precision + if field.decimal_precision is not None + else d["DECIMAL_PRECISION"] + ), + "maxValue": ( + field.max_value + if field.max_value is not None + else d["NUMERIC_MAX_VALUE"] + ), + "minValue": ( + field.min_value + if field.min_value is not None + else d["NUMERIC_MIN_VALUE"] + ), + } + if ftype in (EntityFieldDataType.FILE, EntityFieldDataType.RELATIONSHIP): + return {"lengthLimit": d["UNIQUEIDENTIFIER_LENGTH_LIMIT"]} + if ftype is EntityFieldDataType.CHOICE_SET_MULTIPLE: + return {"lengthLimit": d["CHOICE_SET_MULTIPLE_LENGTH_LIMIT"]} + # UUID, DATETIME, CHOICE_SET_SINGLE, AUTO_NUMBER — no constraints + return {} + + @staticmethod + def _validate_name(name: str, context: str) -> None: + r"""Validate an entity or field name against the UI's create-form rules. + + Entity names must be 1-30 characters; field names must be 3-100 + characters. Both must match ``^[a-zA-Z][a-zA-Z0-9]*$`` — start with a + letter, then letters or digits only (underscores are not permitted, to + stay consistent with the UI's entity / field creation forms). + + Field names additionally cannot collide with the system-reserved field + names in :data:`RESERVED_FIELD_NAMES`; the reserved-name check runs + first so that short reserved names produce a more informative error. + """ + if context == "field": + if name in RESERVED_FIELD_NAMES: + reserved = ", ".join(sorted(RESERVED_FIELD_NAMES)) + raise ValueError( + f"Field name {name!r} is reserved. Reserved names: {reserved}." + ) + min_len, max_len = _FIELD_NAME_MIN_LENGTH, _FIELD_NAME_MAX_LENGTH + else: + min_len, max_len = _ENTITY_NAME_MIN_LENGTH, _ENTITY_NAME_MAX_LENGTH + + if not (min_len <= len(name) <= max_len) or not _NAME_RE.match(name): + raise ValueError( + f"Invalid {context} name {name!r}. Must start with a letter, " + f"contain only letters and digits, and be {min_len}-{max_len} " + "characters." + ) + + @staticmethod + def _validate_field_constraints( + ftype: EntityFieldDataType, field: EntityCreateFieldOptions + ) -> None: + """Validate caller-supplied per-field constraints. + + Rejects constraints that ``ftype`` does not accept (e.g. + ``decimal_precision`` on ``STRING``), values outside the inclusive + range declared in :data:`ENTITY_FIELD_CONSTRAINT_SPEC`, and + ``min_value`` greater than or equal to ``max_value`` when both are + supplied. Also enforces type-dependent required references: + ``CHOICE_SET_SINGLE`` and ``CHOICE_SET_MULTIPLE`` need + ``choice_set_id``; ``RELATIONSHIP`` needs ``reference_entity_name``. + """ + if ( + ftype + in ( + EntityFieldDataType.CHOICE_SET_SINGLE, + EntityFieldDataType.CHOICE_SET_MULTIPLE, + ) + and not field.choice_set_id + ): + raise ValueError( + f"Field {field.field_name!r} of type {ftype.value} requires " + "choice_set_id." + ) + if ( + ftype is EntityFieldDataType.RELATIONSHIP + and not field.reference_entity_name + ): + raise ValueError( + f"Field {field.field_name!r} of type {ftype.value} requires " + "reference_entity_name." + ) + + spec = ENTITY_FIELD_CONSTRAINT_SPEC.get(ftype, {}) + provided: Dict[str, Any] = {} + for attr in ("length_limit", "max_value", "min_value", "decimal_precision"): + value = getattr(field, attr) + if value is not None: + provided[attr] = value + + unsupported = [name for name in provided if name not in spec] + if unsupported: + allowed = ", ".join(sorted(spec.keys())) or "none" + raise ValueError( + f"Field {field.field_name!r} of type {ftype.value} does not accept " + f"{', '.join(sorted(unsupported))}. Allowed constraints: {allowed}." + ) + + for name, value in provided.items(): + low, high = spec[name] + if not (low <= value <= high): + raise ValueError( + f"Field {field.field_name!r} of type {ftype.value}: " + f"{name}={value} is out of range [{low}, {high}]." + ) + + if ( + field.min_value is not None + and field.max_value is not None + and field.min_value >= field.max_value + ): + raise ValueError( + f"Field {field.field_name!r}: min_value ({field.min_value}) must be " + f"strictly less than max_value ({field.max_value})." + ) + + @staticmethod + def _extract_entity_id(response: Response) -> str: + """Return the new entity id from a create-entity response. + + Accepts both a bare JSON string id and a JSON object containing + ``id`` or ``entityId``. + """ + try: + body = response.json() + except Exception: + return response.text.strip().strip('"') + if isinstance(body, str): + return body + if isinstance(body, dict): + for key in ("id", "Id", "entityId", "EntityId"): + value = body.get(key) + if isinstance(value, str): + return value + return response.text.strip().strip('"') diff --git a/packages/uipath-platform/src/uipath/platform/entities/entities.py b/packages/uipath-platform/src/uipath/platform/entities/entities.py index 48c8dce07..51eea2d1b 100644 --- a/packages/uipath-platform/src/uipath/platform/entities/entities.py +++ b/packages/uipath-platform/src/uipath/platform/entities/entities.py @@ -2,21 +2,30 @@ from __future__ import annotations -from enum import Enum +from enum import Enum, IntEnum from types import EllipsisType from typing import ( TYPE_CHECKING, Any, Dict, + Iterator, List, Optional, Type, Union, get_args, get_origin, + overload, ) -from pydantic import AliasChoices, BaseModel, ConfigDict, Field, create_model +from pydantic import ( + AliasChoices, + BaseModel, + ConfigDict, + Field, + create_model, + model_validator, +) if TYPE_CHECKING: from ._entities_service import EntitiesService @@ -82,8 +91,8 @@ class ExternalConnection(BaseModel): id: str connection_id: str = Field(alias="connectionId") element_instance_id: str = Field(alias="elementInstanceId") - folder_id: str = Field(alias="folderKey") # named folderKey in TS SDK - connector_id: str = Field(alias="connectorKey") # named connectorKey in TS SDK + folder_id: str = Field(alias="folderKey") + connector_id: str = Field(alias="connectorKey") connector_name: str = Field(alias="connectorName") connection_name: str = Field(alias="connectionName") @@ -257,7 +266,7 @@ class EntityRecord(BaseModel): "extra": "allow", } - id: str = Field(alias="Id") # Mandatory field validated by Pydantic + id: str = Field(alias="Id") @classmethod def from_data( @@ -356,6 +365,25 @@ class Entity(BaseModel): id: str +class FailureRecord(BaseModel): + """A record that failed to insert/update/delete in a batch operation. + + Backend error responses for failed records do not always include a valid + ``Id`` field — this model accepts arbitrary shapes so the caller can + inspect ``error`` text and the original ``record`` payload. + """ + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + extra="allow", + ) + + id: Optional[str] = Field(default=None, alias="Id") + error: Optional[str] = Field(default=None) + record: Optional[Dict[str, Any]] = Field(default=None) + + class EntityRecordsBatchResponse(BaseModel): """Model representing a batch response of entity records.""" @@ -364,8 +392,421 @@ class EntityRecordsBatchResponse(BaseModel): validate_by_alias=True, ) - success_records: List[EntityRecord] = Field(alias="successRecords") - failure_records: List[EntityRecord] = Field(alias="failureRecords") + success_records: List[EntityRecord] = Field( + default_factory=list, alias="successRecords" + ) + failure_records: List[FailureRecord] = Field( + default_factory=list, alias="failureRecords" + ) + + +class EntityRecordsListResponse(List[EntityRecord]): + """List of EntityRecord with pagination metadata. + + Subclasses ``list`` so existing call sites that iterate, index, or call + ``len()`` continue to work; new fields ``total_count``, ``has_next_page``, + and ``next_cursor`` expose pagination information returned by the backend. + """ + + def __init__( + self, + items: Optional[List[EntityRecord]] = None, + total_count: int = 0, + has_next_page: bool = False, + next_cursor: Optional[str] = None, + ) -> None: + """Construct from a list of records plus pagination metadata.""" + super().__init__(items or []) + self.total_count = total_count + self.has_next_page = has_next_page + self.next_cursor = next_cursor + + +class LogicalOperator(IntEnum): + """Logical operator for combining query filter groups.""" + + And = 0 + Or = 1 + + +class QueryFilterOperator(str, Enum): + """Comparison operators supported by the structured query API.""" + + Equals = "=" + NotEquals = "!=" + GreaterThan = ">" + LessThan = "<" + GreaterThanOrEqual = ">=" + LessThanOrEqual = "<=" + Contains = "contains" + NotContains = "not contains" + StartsWith = "startswith" + EndsWith = "endswith" + In = "in" + NotIn = "not in" + + +class EntityQueryFilter(BaseModel): + """A single filter condition for querying entity records. + + Backend operator/operand rules: + + * ``in`` / ``not in`` — require a non-empty ``value_list`` and reject + ``value``. + * ``=`` / ``!=`` — allow a null ``value`` (becomes ``IS NULL`` / ``IS + NOT NULL``) and reject ``value_list``. + * All other operators (``>``, ``<``, ``>=``, ``<=``, ``contains``, + ``not contains``, ``startswith``, ``endswith``) — require a non-null + ``value`` and reject ``value_list``. + """ + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + field_name: str = Field(alias="fieldName") + operator: QueryFilterOperator + value: Optional[str] = None + value_list: Optional[List[str]] = Field(default=None, alias="valueList") + + @model_validator(mode="after") + def _check_operator_operands(self) -> "EntityQueryFilter": + """Reject operator/operand combinations the backend rejects. + + Implements the same rules the Data Service ``SelectQueryBuilder`` + enforces server-side, so callers see a clear local error instead of + an opaque HTTP 400. + """ + op = self.operator + if op in (QueryFilterOperator.In, QueryFilterOperator.NotIn): + if not self.value_list: + raise ValueError( + f"Operator {op.value!r} requires a non-empty value_list." + ) + if self.value is not None: + raise ValueError( + f"Operator {op.value!r} uses value_list; value must be omitted." + ) + return self + + if self.value_list is not None: + raise ValueError( + f"Operator {op.value!r} uses value; value_list must be omitted." + ) + if ( + op not in (QueryFilterOperator.Equals, QueryFilterOperator.NotEquals) + and self.value is None + ): + raise ValueError(f"Operator {op.value!r} requires a non-null value.") + return self + + +class EntityQueryFilterGroup(BaseModel): + """A group of query filters combined with a logical operator.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + logical_operator: Optional[LogicalOperator] = Field( + default=None, alias="logicalOperator" + ) + continue_logical_operator: Optional[LogicalOperator] = Field( + default=None, alias="continueLogicalOperator" + ) + query_filters: Optional[List[EntityQueryFilter]] = Field( + default=None, alias="queryFilters" + ) + filter_groups: Optional[List["EntityQueryFilterGroup"]] = Field( + default=None, alias="filterGroups" + ) + + +class EntityQuerySortOption(BaseModel): + """Sort option for query results.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + field_name: str = Field(alias="fieldName") + is_descending: Optional[bool] = Field(default=None, alias="isDescending") + + +class EntityAggregateFunction(str, Enum): + """Aggregate functions supported by the Data Fabric query API.""" + + Count = "COUNT" + Sum = "SUM" + Avg = "AVG" + Min = "MIN" + Max = "MAX" + + +class EntityAggregate(BaseModel): + """A single aggregate expression to apply during a query.""" + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + function: EntityAggregateFunction + field: str + alias: Optional[str] = None + + +class EntityJoin(BaseModel): + """Multi-entity JOIN definition for cross-entity queries.""" + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + entity_name: Optional[str] = Field(default=None, alias="entityName") + join_type: Optional[str] = Field(default=None, alias="joinType") + join_field_name: Optional[str] = Field(default=None, alias="joinFieldName") + related_entity_name: Optional[str] = Field(default=None, alias="relatedEntityName") + related_field_name: Optional[str] = Field(default=None, alias="relatedFieldName") + + +class EntityBinning(BaseModel): + """A binning (GROUP BY/aggregation) clause for V2 query endpoint.""" + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + field_name: Optional[str] = Field(default=None, alias="fieldName") + aggregate_function: Optional[EntityAggregateFunction] = Field( + default=None, alias="aggregateFunction" + ) + alias: Optional[str] = None + + +class AggregateRow(BaseModel): + """A row returned by aggregate / group-by / binning queries. + + Aggregate rows do not have an ``Id`` field; columns vary by query + (``selected_fields``, ``aggregates`` aliases, binning aliases) and are + accessible as attributes via ``extra="allow"``. + """ + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + +class RetrieveEntityRecordsResponse(BaseModel): + """Response from :meth:`EntitiesService.retrieve_records`. + + For plain queries, ``items`` is a list of :class:`EntityRecord`. When the + query uses ``aggregates``, ``group_by``, or ``binnings``, the backend + returns rows without an ``Id`` field; those rows are parsed as + :class:`AggregateRow` instances. + """ + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + items: List[EntityRecord | AggregateRow] = Field(default_factory=list) + total_count: int = Field(default=0, alias="totalCount") + has_next_page: bool = Field(default=False, alias="hasNextPage") + next_cursor: Optional[str] = Field(default=None, alias="nextCursor") + + def __iter__(self) -> Iterator[EntityRecord | AggregateRow]: # type: ignore[override] + """Iterate over records (delegates to ``self.items``).""" + return iter(self.items) + + def __len__(self) -> int: + """Return the number of records (delegates to ``self.items``).""" + return len(self.items) + + @overload + def __getitem__(self, index: int) -> EntityRecord | AggregateRow: ... + + @overload + def __getitem__(self, index: slice) -> List[EntityRecord | AggregateRow]: ... + + def __getitem__( + self, index: int | slice + ) -> EntityRecord | AggregateRow | List[EntityRecord | AggregateRow]: + """Index or slice records (delegates to ``self.items``).""" + return self.items[index] + + +class EntityFieldDataType(str, Enum): + """User-facing entity field data type names accepted by ``create_entity``.""" + + UUID = "UUID" + STRING = "STRING" + INTEGER = "INTEGER" + DATETIME = "DATETIME" + DATETIME_WITH_TZ = "DATETIME_WITH_TZ" + DECIMAL = "DECIMAL" + FLOAT = "FLOAT" + DOUBLE = "DOUBLE" + DATE = "DATE" + BOOLEAN = "BOOLEAN" + BIG_INTEGER = "BIG_INTEGER" + MULTILINE_TEXT = "MULTILINE_TEXT" + FILE = "FILE" + CHOICE_SET_SINGLE = "CHOICE_SET_SINGLE" + CHOICE_SET_MULTIPLE = "CHOICE_SET_MULTIPLE" + AUTO_NUMBER = "AUTO_NUMBER" + RELATIONSHIP = "RELATIONSHIP" + + +# Maps the user-facing EntityFieldDataType to the ``(sqlType.name, fieldDisplayType)`` +# tuple expected by the backend when creating an entity. ``sqlType.name`` is +# the raw SQL Server type the backend persists; ``fieldDisplayType`` controls +# how the field renders in the UI. +ENTITY_SCHEMA_FIELD_TYPE_MAP: Dict[EntityFieldDataType, "tuple[str, str]"] = { + EntityFieldDataType.UUID: ("UNIQUEIDENTIFIER", "Basic"), + EntityFieldDataType.STRING: ("NVARCHAR", "Basic"), + EntityFieldDataType.INTEGER: ("INT", "Basic"), + EntityFieldDataType.DATETIME: ("DATETIME2", "Basic"), + EntityFieldDataType.DATETIME_WITH_TZ: ("DATETIMEOFFSET", "Basic"), + EntityFieldDataType.DECIMAL: ("DECIMAL", "Basic"), + EntityFieldDataType.FLOAT: ("FLOAT", "Basic"), + EntityFieldDataType.DOUBLE: ("REAL", "Basic"), + EntityFieldDataType.DATE: ("DATE", "Basic"), + EntityFieldDataType.BOOLEAN: ("BIT", "Basic"), + EntityFieldDataType.BIG_INTEGER: ("BIGINT", "Basic"), + EntityFieldDataType.MULTILINE_TEXT: ("MULTILINE", "Basic"), + EntityFieldDataType.FILE: ("UNIQUEIDENTIFIER", "File"), + EntityFieldDataType.CHOICE_SET_SINGLE: ("INT", "ChoiceSetSingle"), + EntityFieldDataType.CHOICE_SET_MULTIPLE: ("NVARCHAR", "ChoiceSetMultiple"), + EntityFieldDataType.AUTO_NUMBER: ("DECIMAL", "AutoNumber"), + EntityFieldDataType.RELATIONSHIP: ("UNIQUEIDENTIFIER", "Relationship"), +} + +# Default and fixed sqlType constraint values applied when the caller does +# not supply them. The backend requires these on field creation — without +# them the field is stored in an incomplete state and the UI later fails +# with "Field type cannot be changed" when editing advanced options. +ENTITY_FIELD_CONSTRAINT_DEFAULTS: Dict[str, int] = { + "STRING_LENGTH_LIMIT": 200, + "MULTILINE_TEXT_LENGTH_LIMIT": 200, + "DECIMAL_LENGTH_LIMIT": 1000, + "DECIMAL_PRECISION": 2, + "BOOLEAN_LENGTH_LIMIT": 100, + "DATE_LENGTH_LIMIT": 1000, + "UNIQUEIDENTIFIER_LENGTH_LIMIT": 300, + "CHOICE_SET_MULTIPLE_LENGTH_LIMIT": 4000, + "NUMERIC_MAX_VALUE": 1_000_000_000_000, + "NUMERIC_MIN_VALUE": -1_000_000_000_000, +} + +# Per-field-type spec describing which user-supplied constraints are valid +# and their inclusive ranges. Field types absent from this map (BOOLEAN, +# DATE, DATETIME, DATETIME_WITH_TZ, FILE, RELATIONSHIP, UUID, CHOICE_SET_*, +# AUTO_NUMBER) accept no user-supplied constraints — passing one raises +# ``ValueError`` so the caller gets a clear local error before any HTTP call. +_MAX_SAFE_INTEGER = 9_007_199_254_740_991 + +ENTITY_FIELD_CONSTRAINT_SPEC: Dict[ + EntityFieldDataType, Dict[str, "tuple[int, int]"] +] = { + EntityFieldDataType.STRING: { + "length_limit": (1, 4000), + }, + EntityFieldDataType.MULTILINE_TEXT: { + "length_limit": (1, 10000), + }, + EntityFieldDataType.INTEGER: { + "max_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + "min_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + }, + EntityFieldDataType.BIG_INTEGER: { + "max_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + "min_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + }, + EntityFieldDataType.DECIMAL: { + "max_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + "min_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + "decimal_precision": (0, 10), + }, + EntityFieldDataType.FLOAT: { + "max_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + "min_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + "decimal_precision": (0, 10), + }, + EntityFieldDataType.DOUBLE: { + "max_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + "min_value": (-_MAX_SAFE_INTEGER, _MAX_SAFE_INTEGER), + "decimal_precision": (0, 10), + }, +} + +RESERVED_FIELD_NAMES = frozenset( + ["Id", "CreatedBy", "CreateTime", "UpdatedBy", "UpdateTime"] +) +"""Field names reserved by the backend — using one as a user field name is rejected.""" + + +class EntityCreateFieldOptions(BaseModel): + """User-facing field definition for creating or updating entity schemas.""" + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + field_name: str = Field(alias="fieldName") + type: Optional[EntityFieldDataType] = Field( + default=EntityFieldDataType.STRING, alias="type" + ) + display_name: Optional[str] = Field(default=None, alias="displayName") + description: Optional[str] = None + is_required: Optional[bool] = Field(default=None, alias="isRequired") + is_unique: Optional[bool] = Field(default=None, alias="isUnique") + is_rbac_enabled: Optional[bool] = Field(default=None, alias="isRbacEnabled") + is_encrypted: Optional[bool] = Field(default=None, alias="isEncrypted") + default_value: Optional[str] = Field(default=None, alias="defaultValue") + length_limit: Optional[int] = Field(default=None, alias="lengthLimit") + max_value: Optional[int] = Field(default=None, alias="maxValue") + min_value: Optional[int] = Field(default=None, alias="minValue") + decimal_precision: Optional[int] = Field(default=None, alias="decimalPrecision") + choice_set_id: Optional[str] = Field(default=None, alias="choiceSetId") + reference_entity_name: Optional[str] = Field( + default=None, alias="referenceEntityName" + ) + reference_field_name: Optional[str] = Field( + default=None, alias="referenceFieldName" + ) + + +class EntityCreateOptions(BaseModel): + """Options for creating a new Data Fabric entity.""" + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + display_name: Optional[str] = Field(default=None, alias="displayName") + description: Optional[str] = None + folder_key: Optional[str] = Field(default=None, alias="folderKey") + is_rbac_enabled: Optional[bool] = Field(default=None, alias="isRbacEnabled") + is_analytics_enabled: Optional[bool] = Field( + default=None, alias="isAnalyticsEnabled" + ) + external_fields: Optional[List[Dict[str, Any]]] = Field( + default=None, alias="externalFields" + ) + + +class EntityMetadataUpdateOptions(BaseModel): + """Options for updating an entity's metadata via PATCH /metadata.""" + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + display_name: Optional[str] = Field(default=None, alias="displayName") + description: Optional[str] = None + is_rbac_enabled: Optional[bool] = Field(default=None, alias="isRbacEnabled") + + +class EntityImportRecordsResponse(BaseModel): + """Response from a bulk import operation.""" + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + total_records: int = Field(default=0, alias="totalRecords") + inserted_records: int = Field(default=0, alias="insertedRecords") + error_file_link: Optional[str] = Field(default=None, alias="errorFileLink") class EntityRouting(BaseModel): @@ -412,3 +853,4 @@ class EntitySetResolution(BaseModel): Entity.model_rebuild() +EntityQueryFilterGroup.model_rebuild() diff --git a/packages/uipath-platform/tests/services/test_entities_service.py b/packages/uipath-platform/tests/services/test_entities_service.py index 29ce6fb79..258c3a9d8 100644 --- a/packages/uipath-platform/tests/services/test_entities_service.py +++ b/packages/uipath-platform/tests/services/test_entities_service.py @@ -15,6 +15,7 @@ ) from uipath.platform.entities import ChoiceSetValue, DataFabricEntityItem, Entity from uipath.platform.entities._entities_service import EntitiesService +from uipath.platform.entities._entity_data_service import EntityDataService @pytest.fixture @@ -325,7 +326,7 @@ def test_retrieve_records_without_start_and_limit( def test_validate_sql_query_allows_supported_select_queries( self, sql_query: str, service: EntitiesService ) -> None: - service._validate_sql_query(sql_query) + service._data._validate_sql_query(sql_query) @pytest.mark.parametrize( "sql_query,error_message", @@ -415,20 +416,20 @@ def test_validate_sql_query_rejects_disallowed_queries( self, sql_query: str, error_message: str, service: EntitiesService ) -> None: with pytest.raises(ValueError, match=re.escape(error_message)): - service._validate_sql_query(sql_query) + service._data._validate_sql_query(sql_query) def test_query_entity_records_rejects_invalid_sql_before_network_call( self, service: EntitiesService, ) -> None: - service.request = MagicMock() # type: ignore[method-assign] + service._data.request = MagicMock() # type: ignore[method-assign] with pytest.raises( ValueError, match=re.escape("Only SELECT statements are allowed.") ): service.query_entity_records("UPDATE Customers SET name = 'X'") - service.request.assert_not_called() + service._data.request.assert_not_called() def test_query_entity_records_calls_request_for_valid_sql( self, @@ -437,26 +438,26 @@ def test_query_entity_records_calls_request_for_valid_sql( response = MagicMock() response.json.return_value = {"results": [{"id": 1}, {"id": 2}]} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] + service._data.request = MagicMock(return_value=response) # type: ignore[method-assign] result = service.query_entity_records("SELECT id FROM Customers WHERE id > 0") assert result == [{"id": 1}, {"id": 2}] - service.request.assert_called_once() + service._data.request.assert_called_once() @pytest.mark.anyio async def test_query_entity_records_async_rejects_invalid_sql_before_network_call( self, service: EntitiesService, ) -> None: - service.request_async = AsyncMock() # type: ignore[method-assign] + service._data.request_async = AsyncMock() # type: ignore[method-assign] with pytest.raises(ValueError, match=re.escape("Subqueries are not allowed.")): await service.query_entity_records_async( "SELECT id FROM Customers WHERE id IN (SELECT id FROM Orders)" ) - service.request_async.assert_not_called() + service._data.request_async.assert_not_called() @pytest.mark.anyio async def test_query_entity_records_async_calls_request_for_valid_sql( @@ -466,14 +467,14 @@ async def test_query_entity_records_async_calls_request_for_valid_sql( response = MagicMock() response.json.return_value = {"results": [{"id": "c1"}]} - service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] + service._data.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] result = await service.query_entity_records_async( "SELECT id FROM Customers WHERE id = 'c1'" ) assert result == [{"id": "c1"}] - service.request_async.assert_called_once() + service._data.request_async.assert_called_once() def test_query_entity_records_builds_routing_context_from_folders_map( self, @@ -487,12 +488,12 @@ def test_query_entity_records_builds_routing_context_from_folders_map( ) response = MagicMock() response.json.return_value = {"results": [{"id": 1}]} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] + service._data.request = MagicMock(return_value=response) # type: ignore[method-assign] result = service.query_entity_records("SELECT id FROM Customers LIMIT 10") assert result == [{"id": 1}] - call_kwargs = service.request.call_args + call_kwargs = service._data.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert body["query"] == "SELECT id FROM Customers LIMIT 10" assert body["routingContext"] == { @@ -515,14 +516,14 @@ async def test_query_entity_records_async_builds_routing_context_from_folders_ma ) response = MagicMock() response.json.return_value = {"results": [{"id": "c1"}]} - service.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] + service._data.request_async = AsyncMock(return_value=response) # type: ignore[method-assign] result = await service.query_entity_records_async( "SELECT id FROM Customers WHERE id = 'c1'" ) assert result == [{"id": "c1"}] - call_kwargs = service.request_async.call_args + call_kwargs = service._data.request_async.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert body["routingContext"] == { "entityRoutings": [ @@ -536,11 +537,11 @@ def test_query_entity_records_without_routing_context_omits_key( ) -> None: response = MagicMock() response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] + service._data.request = MagicMock(return_value=response) # type: ignore[method-assign] service.query_entity_records("SELECT id FROM Customers WHERE id > 0") - call_kwargs = service.request.call_args + call_kwargs = service._data.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert "routingContext" not in body @@ -560,7 +561,7 @@ def test_query_entity_records_picks_up_entity_overwrites_from_context( ) response = MagicMock() response.json.return_value = {"results": [{"id": 1}]} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] + service._data.request = MagicMock(return_value=response) # type: ignore[method-assign] overwrite = EntityResourceOverwrite( resource_type="entity", @@ -573,7 +574,7 @@ def test_query_entity_records_picks_up_entity_overwrites_from_context( finally: _resource_overwrites.reset(token) - call_kwargs = service.request.call_args + call_kwargs = service._data.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert body["routingContext"] == { "entityRoutings": [ @@ -601,11 +602,11 @@ def test_query_entity_records_merges_folders_map_with_entity_name_overrides( ) response = MagicMock() response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] + service._data.request = MagicMock(return_value=response) # type: ignore[method-assign] service.query_entity_records("SELECT id FROM Customers LIMIT 10") - call_kwargs = service.request.call_args + call_kwargs = service._data.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") routings = body["routingContext"]["entityRoutings"] assert { @@ -740,7 +741,7 @@ def test_query_entity_records_context_overwrite_same_name_no_override_field( ) response = MagicMock() response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] + service._data.request = MagicMock(return_value=response) # type: ignore[method-assign] overwrite = EntityResourceOverwrite( resource_type="entity", @@ -753,7 +754,7 @@ def test_query_entity_records_context_overwrite_same_name_no_override_field( finally: _resource_overwrites.reset(token) - call_kwargs = service.request.call_args + call_kwargs = service._data.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert body["routingContext"] == { "entityRoutings": [ @@ -784,7 +785,7 @@ def test_query_entity_records_resolves_overwrite_folder_path_to_folder_key( ) response = MagicMock() response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] + service._data.request = MagicMock(return_value=response) # type: ignore[method-assign] overwrite = EntityResourceOverwrite( resource_type="entity", @@ -797,7 +798,7 @@ def test_query_entity_records_resolves_overwrite_folder_path_to_folder_key( finally: _resource_overwrites.reset(token) - call_kwargs = service.request.call_args + call_kwargs = service._data.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert body["routingContext"] == { "entityRoutings": [ @@ -829,7 +830,7 @@ def test_query_entity_records_uses_folder_id_directly_without_resolution( ) response = MagicMock() response.json.return_value = {"results": []} - service.request = MagicMock(return_value=response) # type: ignore[method-assign] + service._data.request = MagicMock(return_value=response) # type: ignore[method-assign] overwrite = EntityResourceOverwrite( resource_type="entity", @@ -845,7 +846,7 @@ def test_query_entity_records_uses_folder_id_directly_without_resolution( # folder_id is a key — should NOT be sent through FolderService folders_service.retrieve_key.assert_not_called() - call_kwargs = service.request.call_args + call_kwargs = service._data.request.call_args body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json") assert body["routingContext"] == { "entityRoutings": [ @@ -1096,3 +1097,1553 @@ def test_get_choiceset_values_empty( values = service.get_choiceset_values(choiceset_id) assert values == [] + + +class TestEntitiesServiceNewMethods: + """Single-record, structured-query, attachment, schema and bulk-import tests.""" + + def test_insert_record_fires_post_with_expansion_level( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + from uipath.platform.entities import EntityRecord + + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert?expansionLevel=2", + status_code=200, + json={"Id": "rec-1", "name": "alice"}, + ) + + record = service.insert_record( + entity_key=str(entity_key), + data={"name": "alice"}, + expansion_level=2, + ) + + assert isinstance(record, EntityRecord) + assert record.id == "rec-1" + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.method == "POST" + assert json.loads(sent.content) == {"name": "alice"} + + async def test_insert_record_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert", + status_code=200, + json={"Id": "rec-1"}, + ) + + record = await service.insert_record_async( + entity_key=str(entity_key), data={"name": "bob"} + ) + assert record.id == "rec-1" + + def test_get_record( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + record_id = "12345" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/read/{record_id}?expansionLevel=1", + status_code=200, + json={"Id": record_id, "name": "found"}, + ) + + record = service.get_record( + entity_key=str(entity_key), record_id=record_id, expansion_level=1 + ) + + assert record.id == record_id + + def test_update_record_accepts_dict( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + record_id = "rec-9" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update/{record_id}", + status_code=200, + json={"Id": record_id, "name": "updated"}, + ) + + record = service.update_record( + entity_key=str(entity_key), + record_id=record_id, + data={"name": "updated"}, + ) + + assert record.id == record_id + sent = httpx_mock.get_request() + assert sent is not None + assert json.loads(sent.content) == {"name": "updated"} + + def test_delete_record_uses_http_delete( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + record_id = "rec-9" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/delete/{record_id}", + method="DELETE", + status_code=200, + ) + + service.delete_record(entity_key=str(entity_key), record_id=record_id) + + sent = httpx_mock.get_request() + assert sent is not None + assert sent.method == "DELETE" + + def test_query_v1_with_filter_and_pagination( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + from uipath.platform.entities import ( + EntityQueryFilter, + EntityQueryFilterGroup, + EntityQuerySortOption, + LogicalOperator, + QueryFilterOperator, + ) + + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/query.*" + ), + status_code=200, + json={ + "value": [{"Id": "1", "name": "alice"}, {"Id": "2", "name": "bob"}], + "totalRecordCount": 5, + }, + ) + + result = service.retrieve_records( + entity_key=str(entity_key), + filter_group=EntityQueryFilterGroup( + logical_operator=LogicalOperator.And, + query_filters=[ + EntityQueryFilter( + field_name="status", + operator=QueryFilterOperator.Equals, + value="active", + ) + ], + ), + sort_options=[EntityQuerySortOption(field_name="name", is_descending=True)], + selected_fields=["Id", "name"], + start=0, + limit=2, + expansion_level=1, + ) + + assert result.total_count == 5 + assert len(result.items) == 2 + assert result.has_next_page is True + # Backend doesn't return next_cursor on this endpoint — caller paginates + # by passing the next ``start`` themselves. + assert result.next_cursor is None + + sent = httpx_mock.get_request() + assert sent is not None + assert "/query" in str(sent.url) and "/v2/" not in str(sent.url) + # expansionLevel is a URL query param, not body + assert sent.url.params.get("expansionLevel") == "1" + body = json.loads(sent.content) + assert body["filterGroup"]["logicalOperator"] == 0 # And + assert body["filterGroup"]["queryFilters"][0]["fieldName"] == "status" + assert body["sortOptions"][0]["fieldName"] == "name" + assert body["selectedFields"] == ["Id", "name"] + # start/limit go in BODY, not as $top/$skip query params + assert body["start"] == 0 + assert body["limit"] == 2 + + def test_query_aggregate_response_handles_id_less_rows( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + """Aggregate / GROUP BY rows lack ``Id`` — parsed as :class:`AggregateRow`.""" + from uipath.platform.entities import ( + AggregateRow, + EntityAggregate, + EntityAggregateFunction, + ) + + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/query.*" + ), + status_code=200, + json={ + "value": [ + {"status": "active", "total": 12}, + {"status": "inactive", "total": 7}, + ], + "totalRecordCount": 2, + }, + ) + + result = service.retrieve_records( + entity_key=str(entity_key), + selected_fields=["status"], + group_by=["status"], + aggregates=[ + EntityAggregate( + function=EntityAggregateFunction.Count, + field="Id", + alias="total", + ) + ], + ) + + assert result.total_count == 2 + assert len(result.items) == 2 + # Aggregate rows lack ``Id`` and are exposed as :class:`AggregateRow`. + for row in result.items: + assert isinstance(row, AggregateRow) + assert result.items[0].status == "active" + assert result.items[0].total == 12 + sent = httpx_mock.get_request() + assert sent is not None + body = json.loads(sent.content) + assert body["aggregates"][0]["function"] == "COUNT" + assert body["aggregates"][0]["alias"] == "total" + assert body["groupBy"] == ["status"] + + def test_query_v2_when_binnings_provided( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + from uipath.platform.entities import EntityAggregateFunction, EntityBinning + + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/v2/EntityService/entity/{entity_key}/query.*" + ), + status_code=200, + json={"value": [], "totalCount": 0}, + ) + + service.retrieve_records( + entity_key=str(entity_key), + binnings=[ + EntityBinning( + field_name="status", + aggregate_function=EntityAggregateFunction.Count, + alias="total", + ) + ], + ) + + sent = httpx_mock.get_request() + assert sent is not None + assert "/v2/EntityService/" in str(sent.url) + + def test_upload_attachment_sends_multipart( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_id = "ent-1" + record_id = "rec-1" + field_name = "doc" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Attachment/entity/{entity_id}/{record_id}/{field_name}?expansionLevel=1", + method="POST", + status_code=200, + json={"Id": record_id, "doc": "uploaded"}, + ) + + result = service.upload_attachment( + entity_id=entity_id, + record_id=record_id, + field_name=field_name, + file=b"hello world", + expansion_level=1, + ) + + assert result.get("doc") == "uploaded" + + sent = httpx_mock.get_request() + assert sent is not None + assert b"hello world" in sent.content + + def test_download_attachment_returns_bytes( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_id = "ent-1" + record_id = "rec-1" + field_name = "doc" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Attachment/entity/{entity_id}/{record_id}/{field_name}", + method="GET", + status_code=200, + content=b"file-content", + ) + + content = service.download_attachment( + entity_id=entity_id, record_id=record_id, field_name=field_name + ) + assert content == b"file-content" + + def test_delete_attachment( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_id = "ent-1" + record_id = "rec-1" + field_name = "doc" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Attachment/entity/{entity_id}/{record_id}/{field_name}", + method="DELETE", + status_code=200, + json={}, + ) + + result = service.delete_attachment( + entity_id=entity_id, record_id=record_id, field_name=field_name + ) + assert result == {} + + def test_create_entity_returns_id( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityCreateOptions, + EntityFieldDataType, + ) + + new_entity_id = str(uuid.uuid4()) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + method="POST", + status_code=200, + json=new_entity_id, + ) + + created_id = service.create_entity( + name="productCatalog", + fields=[ + EntityCreateFieldOptions( + field_name="productName", + type=EntityFieldDataType.STRING, + is_required=True, + length_limit=200, + ), + ], + options=EntityCreateOptions( + display_name="Product Catalog", + description="Catalog of products", + is_rbac_enabled=True, + ), + ) + + assert created_id == new_entity_id + sent = httpx_mock.get_request() + assert sent is not None + body = json.loads(sent.content) + assert body["displayName"] == "Product Catalog" + assert body["entityDefinition"]["name"] == "productCatalog" + assert body["entityDefinition"]["fields"][0]["name"] == "productName" + assert body["entityDefinition"]["isRbacEnabled"] is True + + def test_delete_entity( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_id = "ent-doomed" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/{entity_id}", + method="DELETE", + status_code=200, + ) + + service.delete_entity(entity_id=entity_id) + sent = httpx_mock.get_request() + assert sent is not None + assert sent.method == "DELETE" + + def test_update_entity_metadata( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + from uipath.platform.entities import EntityMetadataUpdateOptions + + entity_id = "ent-meta" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/{entity_id}/metadata", + method="PATCH", + status_code=200, + json={}, + ) + + service.update_entity_metadata( + entity_id=entity_id, + metadata=EntityMetadataUpdateOptions( + display_name="New Name", is_rbac_enabled=False + ), + ) + + sent = httpx_mock.get_request() + assert sent is not None + body = json.loads(sent.content) + assert body == {"displayName": "New Name", "isRbacEnabled": False} + + def test_import_records( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_id = "ent-imp" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_id}/bulk-upload", + method="POST", + status_code=200, + json={ + "totalRecords": 10, + "insertedRecords": 9, + "errorFileLink": "https://example.com/errors.csv", + }, + ) + + result = service.import_records(entity_id=entity_id, file=b"a,b,c\n1,2,3\n") + assert result.total_records == 10 + assert result.inserted_records == 9 + assert result.error_file_link == "https://example.com/errors.csv" + + def test_list_records_returns_paginated_metadata( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/read.*" + ), + status_code=200, + json={ + "totalCount": 7, + "value": [{"Id": "1"}, {"Id": "2"}, {"Id": "3"}], + }, + ) + + records = service.list_records( + entity_key=str(entity_key), + start=0, + limit=3, + expansion_level=2, + filter="status eq 'active'", + orderby="name asc", + select=["Id", "name"], + expand=["Company"], + ) + + # New pagination metadata: backend totalCount surfaced verbatim. + assert records.total_count == 7 + assert records.has_next_page is True + # Backend does not currently emit next_cursor; caller paginates with start. + assert records.next_cursor is None + + # Backward-compat: behaves as a list. + assert isinstance(records, list) + assert len(records) == 3 + assert records[0].id == "1" + + sent = httpx_mock.get_request() + assert sent is not None + params = sent.url.params + assert params.get("expansionLevel") == "2" + assert params.get("$filter") == "status eq 'active'" + assert params.get("$orderby") == "name asc" + assert params.get("$select") == "Id,name" + assert params.get("$expand") == "Company" + + def test_insert_records_passes_expansion_level_and_fail_on_first( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert-batch.*" + ), + status_code=200, + json={"successRecords": [{"Id": "1"}], "failureRecords": []}, + ) + + service.insert_records( + entity_key=str(entity_key), + records=[{"name": "alice"}], + expansion_level=1, + fail_on_first=True, + ) + + sent = httpx_mock.get_request() + assert sent is not None + params = sent.url.params + assert params.get("expansionLevel") == "1" + assert params.get("failOnFirst") == "true" + # Records are normalized to dicts before being sent. + assert json.loads(sent.content) == [{"name": "alice"}] + + def test_update_records_recovers_failure_records_from_4xx( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + """A 400 response that lists per-record failures should parse into the response. + + The caller receives an ``EntityRecordsBatchResponse`` with the failed + records populated rather than an exception, so unknown record ids on + update can be handled the same way as any other batch failure. + """ + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + method="POST", + status_code=400, + json={ + "successRecords": [], + "failureRecords": [ + {"error": "Record not found", "record": {"Id": "missing"}} + ], + }, + ) + + result = service.update_records( + entity_key=str(entity_key), + records=[{"Id": "missing", "name": "x"}], + ) + + assert len(result.failure_records) == 1 + assert result.failure_records[0].error == "Record not found" + + def test_delete_records_recovers_failure_records_from_4xx( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/delete-batch", + method="POST", + status_code=400, + json={ + "successRecords": [], + "failureRecords": [{"error": "not found"}], + }, + ) + + result = service.delete_records( + entity_key=str(entity_key), record_ids=["missing"] + ) + + assert result.failure_records[0].error == "not found" + + def test_record_to_dict_accepts_dict_pydantic_and_object(self) -> None: + from uipath.platform.entities import EntityCreateFieldOptions + + # dict + assert EntityDataService._record_to_dict({"a": 1}) == {"a": 1} + # Pydantic model — uses model_dump + result = EntityDataService._record_to_dict( + EntityCreateFieldOptions(field_name="x") + ) + assert result["fieldName"] == "x" + # Object with __dict__ + from dataclasses import dataclass + + @dataclass + class Rec: + name: str + + assert EntityDataService._record_to_dict(Rec(name="bob")) == {"name": "bob"} + + +class TestEntitiesServiceCreateEntitySqlTypeMapping: + """Verify ``create_entity`` produces the SQL types and constraint defaults the backend expects.""" + + def _captured_field( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + field_options, + ): + from uipath.platform.entities import EntityCreateOptions + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + method="POST", + status_code=200, + json="00000000-0000-0000-0000-000000000001", + ) + service.create_entity( + name="myEntity", + fields=[field_options], + options=EntityCreateOptions(display_name="My Entity"), + ) + sent = httpx_mock.get_request() + assert sent is not None + body = json.loads(sent.content) + return body["entityDefinition"]["fields"][0] + + def test_string_field_maps_to_nvarchar_with_default_length( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + f = self._captured_field( + httpx_mock, + service, + base_url, + org, + tenant, + EntityCreateFieldOptions( + field_name="productName", type=EntityFieldDataType.STRING + ), + ) + assert f["sqlType"]["name"] == "NVARCHAR" + assert f["sqlType"]["lengthLimit"] == 200 # default + assert f["fieldDisplayType"] == "Basic" + + def test_decimal_field_includes_precision_and_value_bounds( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + f = self._captured_field( + httpx_mock, + service, + base_url, + org, + tenant, + EntityCreateFieldOptions( + field_name="price", + type=EntityFieldDataType.DECIMAL, + decimal_precision=4, + ), + ) + assert f["sqlType"]["name"] == "DECIMAL" + assert f["sqlType"]["decimalPrecision"] == 4 + assert f["sqlType"]["lengthLimit"] == 1000 + assert f["sqlType"]["maxValue"] == 1_000_000_000_000 + assert f["sqlType"]["minValue"] == -1_000_000_000_000 + + def test_boolean_field_maps_to_bit( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + f = self._captured_field( + httpx_mock, + service, + base_url, + org, + tenant, + EntityCreateFieldOptions( + field_name="isActive", type=EntityFieldDataType.BOOLEAN + ), + ) + assert f["sqlType"]["name"] == "BIT" + assert f["sqlType"]["lengthLimit"] == 100 + + def test_file_field_maps_to_uniqueidentifier_with_file_display_type( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + f = self._captured_field( + httpx_mock, + service, + base_url, + org, + tenant, + EntityCreateFieldOptions( + field_name="document", type=EntityFieldDataType.FILE + ), + ) + assert f["sqlType"]["name"] == "UNIQUEIDENTIFIER" + assert f["fieldDisplayType"] == "File" + assert f["sqlType"]["lengthLimit"] == 300 + + +class TestEntitiesServiceValidation: + """Client-side validation rejects bad entity / field definitions before any HTTP call.""" + + def test_create_entity_rejects_invalid_entity_name(self, service) -> None: + + with pytest.raises(ValueError, match="Invalid entity name"): + service.create_entity(name="1bad", fields=[]) + + def test_create_entity_rejects_invalid_field_name(self, service) -> None: + from uipath.platform.entities import EntityCreateFieldOptions + + with pytest.raises(ValueError, match="Invalid field name"): + service.create_entity( + name="goodEntity", + fields=[EntityCreateFieldOptions(field_name="9bad")], + ) + + def test_create_entity_rejects_reserved_field_name(self, service) -> None: + from uipath.platform.entities import EntityCreateFieldOptions + + with pytest.raises(ValueError, match="reserved"): + service.create_entity( + name="goodEntity", + fields=[EntityCreateFieldOptions(field_name="Id")], + ) + + def test_create_entity_rejects_unsupported_constraint_for_type( + self, service + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + with pytest.raises(ValueError, match="does not accept"): + service.create_entity( + name="goodEntity", + fields=[ + EntityCreateFieldOptions( + field_name="myField", + type=EntityFieldDataType.STRING, + decimal_precision=2, # not allowed on STRING + ) + ], + ) + + def test_create_entity_rejects_out_of_range_constraint(self, service) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + with pytest.raises(ValueError, match="out of range"): + service.create_entity( + name="goodEntity", + fields=[ + EntityCreateFieldOptions( + field_name="myField", + type=EntityFieldDataType.STRING, + length_limit=99999, # > 4000 + ) + ], + ) + + def test_create_entity_rejects_min_ge_max(self, service) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + with pytest.raises(ValueError, match="strictly less than"): + service.create_entity( + name="goodEntity", + fields=[ + EntityCreateFieldOptions( + field_name="myField", + type=EntityFieldDataType.INTEGER, + min_value=100, + max_value=10, + ) + ], + ) + + def test_create_entity_rejects_choiceset_without_choice_set_id( + self, service + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + with pytest.raises(ValueError, match="choice_set_id"): + service.create_entity( + name="goodEntity", + fields=[ + EntityCreateFieldOptions( + field_name="myField", + type=EntityFieldDataType.CHOICE_SET_SINGLE, + ) + ], + ) + + def test_create_entity_rejects_choice_set_multiple_without_choice_set_id( + self, service + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + with pytest.raises(ValueError, match="choice_set_id"): + service.create_entity( + name="goodEntity", + fields=[ + EntityCreateFieldOptions( + field_name="myField", + type=EntityFieldDataType.CHOICE_SET_MULTIPLE, + ) + ], + ) + + def test_create_entity_rejects_relationship_without_reference_entity( + self, service + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + with pytest.raises(ValueError, match="reference_entity_name"): + service.create_entity( + name="goodEntity", + fields=[ + EntityCreateFieldOptions( + field_name="myField", + type=EntityFieldDataType.RELATIONSHIP, + ) + ], + ) + + def test_entity_query_filter_rejects_in_without_value_list(self) -> None: + from uipath.platform.entities import EntityQueryFilter, QueryFilterOperator + + with pytest.raises(ValueError, match="non-empty value_list"): + EntityQueryFilter( + field_name="status", + operator=QueryFilterOperator.In, + ) + + def test_entity_query_filter_rejects_in_with_scalar_value(self) -> None: + from uipath.platform.entities import EntityQueryFilter, QueryFilterOperator + + with pytest.raises(ValueError, match="value must be omitted"): + EntityQueryFilter( + field_name="status", + operator=QueryFilterOperator.In, + value="active", + value_list=["active", "pending"], + ) + + def test_entity_query_filter_rejects_scalar_op_with_value_list(self) -> None: + from uipath.platform.entities import EntityQueryFilter, QueryFilterOperator + + with pytest.raises(ValueError, match="value_list must be omitted"): + EntityQueryFilter( + field_name="amount", + operator=QueryFilterOperator.GreaterThan, + value_list=["10"], + ) + + def test_entity_query_filter_rejects_strict_op_with_null_value(self) -> None: + from uipath.platform.entities import EntityQueryFilter, QueryFilterOperator + + with pytest.raises(ValueError, match="non-null value"): + EntityQueryFilter( + field_name="amount", + operator=QueryFilterOperator.GreaterThan, + ) + + def test_entity_query_filter_allows_equals_with_null_value(self) -> None: + from uipath.platform.entities import EntityQueryFilter, QueryFilterOperator + + f = EntityQueryFilter( + field_name="middle_name", + operator=QueryFilterOperator.Equals, + ) + assert f.value is None and f.value_list is None + + def test_entity_query_filter_allows_in_with_value_list(self) -> None: + from uipath.platform.entities import EntityQueryFilter, QueryFilterOperator + + f = EntityQueryFilter( + field_name="status", + operator=QueryFilterOperator.In, + value_list=["a", "b"], + ) + assert f.value_list == ["a", "b"] + + +class TestEntitiesServiceAsyncAndEdgeCases: + async def test_get_record_async( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + entity_key = uuid.uuid4() + record_id = "rec-1" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/read/{record_id}", + status_code=200, + json={"Id": record_id, "name": "found"}, + ) + record = await service.get_record_async( + entity_key=str(entity_key), record_id=record_id + ) + assert record.id == record_id + + async def test_query_async_v1( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/query" + ), + status_code=200, + json={"value": [{"Id": "1"}], "totalRecordCount": 1}, + ) + result = await service.retrieve_records_async(entity_key=str(entity_key)) + assert result.total_count == 1 + + async def test_delete_record_async( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + entity_key = uuid.uuid4() + record_id = "rec-1" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/delete/{record_id}", + method="DELETE", + status_code=200, + ) + await service.delete_record_async( + entity_key=str(entity_key), record_id=record_id + ) + + async def test_create_entity_async( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + from uipath.platform.entities import ( + EntityCreateFieldOptions, + EntityFieldDataType, + ) + + new_id = "00000000-0000-0000-0000-000000000123" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + method="POST", + status_code=200, + json=new_id, + ) + result = await service.create_entity_async( + name="goodEntity", + fields=[ + EntityCreateFieldOptions( + field_name="myField", type=EntityFieldDataType.STRING + ) + ], + ) + assert result == new_id + + async def test_delete_entity_async( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/ent-1", + method="DELETE", + status_code=200, + ) + await service.delete_entity_async(entity_id="ent-1") + + async def test_update_entity_metadata_async_with_dict( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/ent-1/metadata", + method="PATCH", + status_code=200, + json={}, + ) + # Accepts a plain dict too + await service.update_entity_metadata_async( + entity_id="ent-1", metadata={"displayName": "X", "description": "Y"} + ) + sent = httpx_mock.get_request() + assert sent is not None + assert json.loads(sent.content) == {"displayName": "X", "description": "Y"} + + def test_update_entity_metadata_normalizes_snake_case_dict_keys( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + """Snake_case dict keys must be sent to the backend as camelCase.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/ent-1/metadata", + method="PATCH", + status_code=200, + json={}, + ) + service.update_entity_metadata( + entity_id="ent-1", + metadata={ + "display_name": "New Name", + "description": "Updated", + "is_rbac_enabled": True, + }, + ) + sent = httpx_mock.get_request() + assert sent is not None + assert json.loads(sent.content) == { + "displayName": "New Name", + "description": "Updated", + "isRbacEnabled": True, + } + + async def test_upload_attachment_async_via_file_path( + self, httpx_mock, service, base_url, org, tenant, version, tmp_path + ) -> None: + path = tmp_path / "data.bin" + path.write_bytes(b"file-on-disk") + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Attachment/entity/ent/rec/doc", + method="POST", + status_code=200, + json={"Id": "rec", "doc": "ok"}, + ) + result = await service.upload_attachment_async( + entity_id="ent", + record_id="rec", + field_name="doc", + file_path=str(path), + ) + assert result["doc"] == "ok" + + sent = httpx_mock.get_request() + assert sent is not None + assert b"file-on-disk" in sent.content + + async def test_download_and_delete_attachment_async( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + url = f"{base_url}{org}{tenant}/datafabric_/api/Attachment/entity/e/r/f" + httpx_mock.add_response( + url=url, method="GET", status_code=200, content=b"bytes" + ) + httpx_mock.add_response(url=url, method="DELETE", status_code=200, json={}) + + content = await service.download_attachment_async( + entity_id="e", record_id="r", field_name="f" + ) + assert content == b"bytes" + assert ( + await service.delete_attachment_async( + entity_id="e", record_id="r", field_name="f" + ) + == {} + ) + + def test_open_file_rejects_both_file_and_path(self) -> None: + with pytest.raises(ValueError, match="exactly one of"): + EntityDataService._open_file(file=b"x", file_path="some/path") + + def test_open_file_rejects_neither_file_nor_path(self) -> None: + with pytest.raises(ValueError, match="exactly one of"): + EntityDataService._open_file(file=None, file_path=None) + + def test_4xx_recovery_only_400_with_strict_shape( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + """5xx and 4xx other than 400 must propagate; 400 with valid shape recovers.""" + entity_key = uuid.uuid4() + # 500 with the shape — must propagate, not be silently treated as success. + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + method="POST", + status_code=500, + json={"successRecords": [], "failureRecords": []}, + ) + from uipath.platform.errors._enriched_exception import EnrichedException + + with pytest.raises(EnrichedException): + service.update_records( + entity_key=str(entity_key), records=[{"Id": "x", "name": "y"}] + ) + + def test_4xx_recovery_404_propagates( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + entity_key = uuid.uuid4() + # 404 with valid shape — still propagates because not a 400. + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + method="POST", + status_code=404, + json={"successRecords": [], "failureRecords": []}, + ) + from uipath.platform.errors._enriched_exception import EnrichedException + + with pytest.raises(EnrichedException): + service.update_records( + entity_key=str(entity_key), records=[{"Id": "x", "name": "y"}] + ) + + def test_4xx_recovery_400_unrelated_body_propagates( + self, httpx_mock, service, base_url, org, tenant, version + ) -> None: + """A 400 with an error body that lacks ``successRecords``/``failureRecords`` + must surface as an exception (so generic validation errors aren't masked).""" + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + method="POST", + status_code=400, + json={"error": "Validation failed", "code": "InvalidArg"}, + ) + from uipath.platform.errors._enriched_exception import EnrichedException + + with pytest.raises(EnrichedException): + service.update_records( + entity_key=str(entity_key), records=[{"Id": "x", "name": "y"}] + ) + + +class TestEntitiesServiceAsyncCoverage: + """Async-variant tests for previously uncovered paths on schema / data services.""" + + async def test_retrieve_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/{entity_key}", + status_code=200, + json={ + "name": "Customers", + "displayName": "Customers", + "entityType": "Entity", + "fields": [], + "isRbacEnabled": False, + "id": str(entity_key), + }, + ) + entity = await service.retrieve_async(entity_key=str(entity_key)) + assert entity.id == str(entity_key) + + def test_retrieve_by_name( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/Customers/metadata", + status_code=200, + json={ + "name": "Customers", + "displayName": "Customers", + "entityType": "Entity", + "fields": [], + "isRbacEnabled": False, + "id": "ent-1", + }, + ) + entity = service.retrieve_by_name("Customers", folder_key="folder-1") + assert entity.name == "Customers" + sent = httpx_mock.get_request() + assert sent is not None + assert sent.headers.get("X-UIPATH-FolderKey") == "folder-1" + + async def test_retrieve_by_name_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity/Orders/metadata", + status_code=200, + json={ + "name": "Orders", + "displayName": "Orders", + "entityType": "Entity", + "fields": [], + "isRbacEnabled": False, + "id": "ent-2", + }, + ) + entity = await service.retrieve_by_name_async("Orders") + assert entity.name == "Orders" + + def test_list_entities_basic( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + status_code=200, + json=[ + { + "name": "Customers", + "displayName": "Customers", + "entityType": "Entity", + "fields": [], + "isRbacEnabled": False, + "id": "ent-1", + }, + { + "name": "Orders", + "displayName": "Orders", + "entityType": "Entity", + "fields": [], + "isRbacEnabled": False, + "id": "ent-2", + }, + ], + ) + entities = service.list_entities() + assert len(entities) == 2 + + async def test_list_entities_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/Entity", + status_code=200, + json=[ + { + "name": "Customers", + "displayName": "Customers", + "entityType": "Entity", + "fields": [], + "isRbacEnabled": False, + "id": "ent-1", + } + ], + ) + entities = await service.list_entities_async() + assert len(entities) == 1 + + async def test_list_records_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/read.*" + ), + status_code=200, + json={ + "totalRecordCount": 2, + "value": [{"Id": "1"}, {"Id": "2"}], + }, + ) + records = await service.list_records_async( + entity_key=str(entity_key), start=0, limit=10 + ) + assert records.total_count == 2 + + async def test_update_record_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update/rec-1", + method="POST", + status_code=200, + json={"Id": "rec-1", "name": "renamed"}, + ) + rec = await service.update_record_async( + entity_key=str(entity_key), + record_id="rec-1", + data={"name": "renamed"}, + ) + assert rec.id == "rec-1" + + async def test_insert_records_async_batch( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert-batch.*" + ), + status_code=200, + json={ + "successRecords": [{"Id": "1", "name": "a"}], + "failureRecords": [], + }, + ) + result = await service.insert_records_async( + entity_key=str(entity_key), + records=[{"name": "a"}], + expansion_level=1, + fail_on_first=True, + ) + assert len(result.success_records) == 1 + + async def test_update_records_async_recovers_400_failures( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/update-batch", + method="POST", + status_code=400, + json={ + "successRecords": [], + "failureRecords": [{"error": "not found"}], + }, + ) + result = await service.update_records_async( + entity_key=str(entity_key), + records=[{"Id": "missing", "name": "x"}], + ) + assert result.failure_records[0].error == "not found" + + async def test_delete_records_async_batch( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=re.compile( + rf"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/delete-batch.*" + ), + status_code=200, + json={ + "successRecords": [{"Id": "rec-1"}], + "failureRecords": [], + }, + ) + result = await service.delete_records_async( + entity_key=str(entity_key), + record_ids=["rec-1"], + fail_on_first=False, + ) + assert len(result.success_records) == 1 + + async def test_import_records_async( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/ent-1/bulk-upload", + method="POST", + status_code=200, + json={ + "totalRecords": 3, + "insertedRecords": 3, + "errorFileLink": None, + }, + ) + result = await service.import_records_async( + entity_id="ent-1", file=b"a,b\n1,2\n" + ) + assert result.inserted_records == 3 + + def test_validate_entity_batch_handles_success_and_failure_records( + self, + service: EntitiesService, + ) -> None: + response = MagicMock() + response.json.return_value = { + "successRecords": [{"Id": "ok-1", "name": "first"}], + "failureRecords": [{"error": "duplicate", "record": {"name": "dup"}}], + } + result = service.validate_entity_batch(response) + assert len(result.success_records) == 1 + assert result.success_records[0].id == "ok-1" + assert result.failure_records[0].error == "duplicate" + + def test_5xx_with_batch_shape_still_propagates( + self, + httpx_mock: HTTPXMock, + service: EntitiesService, + base_url: str, + org: str, + tenant: str, + version: str, + ) -> None: + """500 with successRecords/failureRecords shape must NOT be recovered.""" + from uipath.platform.errors._enriched_exception import EnrichedException + + entity_key = uuid.uuid4() + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/datafabric_/api/EntityService/entity/{entity_key}/insert-batch", + method="POST", + status_code=500, + json={"successRecords": [], "failureRecords": []}, + ) + with pytest.raises(EnrichedException): + service.insert_records( + entity_key=str(entity_key), + records=[{"name": "x"}], + ) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index c3b60638d..84372918d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.56" +version = "0.1.57" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 449f50872..e725d6e39 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.56" +version = "0.1.57" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From cb77eb2c61df44f206b5b5785df614a3d645116b Mon Sep 17 00:00:00 2001 From: ctiliescuuipath Date: Mon, 25 May 2026 13:35:58 +0300 Subject: [PATCH 074/121] fix(platform): resolve correct span in build_trace_context_headers (#1679) Co-authored-by: Claude Opus 4.6 (1M context) --- .../uipath/platform/chat/llm_trace_context.py | 16 ++- .../tests/services/test_llm_trace_context.py | 119 ++++++++++++++---- 2 files changed, 103 insertions(+), 32 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py b/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py index 6efbf08dc..fc97da511 100644 --- a/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py +++ b/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py @@ -5,12 +5,17 @@ from uipath.core.tracing.span_utils import UiPathSpanUtils from ..common._config import UiPathConfig +from ..common._span_utils import _SpanUtils def build_trace_context_headers( extra_baggage: list[str] | None = None, ) -> dict[str, str]: - """Build W3C-style trace context headers from the current OpenTelemetry span. + """Build W3C-style trace context headers for LLM Gateway requests. + + Resolves the current span via ``UiPathSpanUtils.get_external_current_span()`` + (which returns the deepest active span from the LLMOps hierarchy) with a + fallback to ``trace.get_current_span()``. Args: extra_baggage: Additional baggage entries (e.g. ``["source=agents"]``) @@ -25,16 +30,17 @@ def build_trace_context_headers( headers: dict[str, str] = {} llmops_span = UiPathSpanUtils.get_external_current_span() span = llmops_span or trace.get_current_span() + config_trace_id = UiPathConfig.trace_id ctx = span.get_span_context() - if ctx and ctx.trace_id and ctx.span_id: - trace_id = format(ctx.trace_id, "032x") - span_id = format(ctx.span_id, "016x") + if config_trace_id and ctx and ctx.span_id: + trace_id = _SpanUtils.normalize_trace_id(config_trace_id) + span_id = format(ctx.span_id, "032x") headers["x-uipath-traceparent-id"] = f"00-{trace_id}-{span_id}" baggage_parts: list[str] = list(extra_baggage) if extra_baggage else [] if folder_key := UiPathConfig.folder_key: baggage_parts.append(f"folderKey={folder_key}") - if agent_id := UiPathConfig.process_uuid: + if agent_id := UiPathConfig.agent_id: baggage_parts.append(f"agentId={agent_id}") if process_key := UiPathConfig.process_key: baggage_parts.append(f"processKey={process_key}") diff --git a/packages/uipath-platform/tests/services/test_llm_trace_context.py b/packages/uipath-platform/tests/services/test_llm_trace_context.py index 83bde3957..8db61200b 100644 --- a/packages/uipath-platform/tests/services/test_llm_trace_context.py +++ b/packages/uipath-platform/tests/services/test_llm_trace_context.py @@ -3,7 +3,6 @@ import os from unittest.mock import patch -from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags from uipath.core.feature_flags import FeatureFlags @@ -13,6 +12,13 @@ FEATURE_FLAG = "EnableTraceContextHeaders" +def _make_span(): + """Create a real OTEL span for testing.""" + provider = TracerProvider() + tracer = provider.get_tracer("test") + return tracer.start_span("test-span") + + class TestFeatureFlagDisabled: """When the feature flag is off, no headers are returned.""" @@ -28,48 +34,74 @@ def test_returns_empty_dict_when_explicitly_disabled(self) -> None: class TestTraceparentHeader: - """When enabled, x-uipath-traceparent-id is populated from the active span.""" + """When enabled, x-uipath-traceparent-id is populated from config + span.""" def setup_method(self) -> None: FeatureFlags.reset_flags() FeatureFlags.configure_flags({FEATURE_FLAG: True}) - def test_traceparent_from_active_span(self) -> None: - provider = TracerProvider() - tracer = provider.get_tracer("test") - with tracer.start_as_current_span("test-span") as span: - ctx = span.get_span_context() - expected_trace_id = format(ctx.trace_id, "032x") - expected_span_id = format(ctx.span_id, "016x") - + def test_traceparent_from_config_and_span(self) -> None: + span = _make_span() + ctx = span.get_span_context() + expected_span_id = format(ctx.span_id, "032x") + config_trace = "abcdef1234567890abcdef1234567890" + env = {"UIPATH_TRACE_ID": config_trace} + with ( + patch.dict(os.environ, env), + patch( + "uipath.platform.chat.llm_trace_context.trace.get_current_span", + return_value=span, + ), + ): headers = build_trace_context_headers() assert "x-uipath-traceparent-id" in headers value = headers["x-uipath-traceparent-id"] - assert value == f"00-{expected_trace_id}-{expected_span_id}" - # Verify format: version (2) + dash + trace_id (32) + dash + span_id (16) + assert value == f"00-{config_trace}-{expected_span_id}" parts = value.split("-") assert len(parts) == 3 assert parts[0] == "00" assert len(parts[1]) == 32 - assert len(parts[2]) == 16 + assert len(parts[2]) == 32 + + def test_no_traceparent_without_config_trace_id(self) -> None: + headers = build_trace_context_headers() + assert "x-uipath-traceparent-id" not in headers + + def test_traceparent_strips_dashes_from_config_trace_id(self) -> None: + span = _make_span() + uuid_trace = "abcdef12-3456-7890-abcd-ef1234567890" + env = {"UIPATH_TRACE_ID": uuid_trace} + with ( + patch.dict(os.environ, env), + patch( + "uipath.platform.chat.llm_trace_context.trace.get_current_span", + return_value=span, + ), + ): + headers = build_trace_context_headers() - def test_no_traceparent_without_active_span(self) -> None: - # INVALID_SPAN has trace_id=0 and span_id=0 - from opentelemetry.context import attach, detach + value = headers["x-uipath-traceparent-id"] + parts = value.split("-") + assert parts[1] == "abcdef1234567890abcdef1234567890" + def test_no_traceparent_with_invalid_span(self) -> None: ctx = SpanContext( trace_id=0, span_id=0, is_remote=False, trace_flags=TraceFlags(0), ) - non_recording = NonRecordingSpan(ctx) - token = attach(trace.set_span_in_context(non_recording)) - try: + span = NonRecordingSpan(ctx) + env = {"UIPATH_TRACE_ID": "abcdef1234567890abcdef1234567890"} + with ( + patch.dict(os.environ, env), + patch( + "uipath.platform.chat.llm_trace_context.trace.get_current_span", + return_value=span, + ), + ): headers = build_trace_context_headers() - finally: - detach(token) assert "x-uipath-traceparent-id" not in headers @@ -84,7 +116,7 @@ def setup_method(self) -> None: def test_all_env_vars_present(self) -> None: env = { "UIPATH_FOLDER_KEY": "folder-abc", - "UIPATH_PROCESS_UUID": "agent-123", + "UIPATH_AGENT_ID": "agent-123", "UIPATH_PROCESS_KEY": "process-789", } with patch.dict(os.environ, env, clear=True): @@ -103,6 +135,31 @@ def test_partial_env_vars(self) -> None: baggage = headers["x-uipath-tracebaggage"] assert "folderKey=folder-only" in baggage + def test_agent_id_from_agent_id_env(self) -> None: + env = {"UIPATH_AGENT_ID": "real-agent-id"} + with patch.dict(os.environ, env, clear=True): + headers = build_trace_context_headers() + + baggage = headers["x-uipath-tracebaggage"] + assert "agentId=real-agent-id" in baggage + + def test_agent_id_falls_back_to_project_id(self) -> None: + env = {"UIPATH_PROJECT_ID": "project-123"} + with patch.dict(os.environ, env, clear=True): + headers = build_trace_context_headers() + + baggage = headers["x-uipath-tracebaggage"] + assert "agentId=project-123" in baggage + + def test_no_agent_id_without_env_vars(self) -> None: + env = {"UIPATH_FOLDER_KEY": "f1"} + with patch.dict(os.environ, env, clear=True): + headers = build_trace_context_headers() + + baggage = headers["x-uipath-tracebaggage"] + assert "agentId" not in baggage + assert "folderKey=f1" in baggage + def test_no_baggage_without_env_vars(self) -> None: with patch.dict(os.environ, {}, clear=True): headers = build_trace_context_headers() @@ -112,7 +169,7 @@ def test_no_baggage_without_env_vars(self) -> None: def test_baggage_comma_separated(self) -> None: env = { "UIPATH_FOLDER_KEY": "f1", - "UIPATH_PROCESS_UUID": "a1", + "UIPATH_AGENT_ID": "a1", } with patch.dict(os.environ, env, clear=True): headers = build_trace_context_headers() @@ -148,14 +205,22 @@ def setup_method(self) -> None: FeatureFlags.configure_flags({FEATURE_FLAG: True}) def test_both_headers_present(self) -> None: - provider = TracerProvider() - tracer = provider.get_tracer("test") - env = {"UIPATH_FOLDER_KEY": "folder-abc"} + span = _make_span() + env = { + "UIPATH_FOLDER_KEY": "folder-abc", + "UIPATH_TRACE_ID": "abcdef1234567890abcdef1234567890", + } with ( - tracer.start_as_current_span("test-span"), patch.dict(os.environ, env, clear=True), + patch( + "uipath.platform.chat.llm_trace_context.trace.get_current_span", + return_value=span, + ), ): headers = build_trace_context_headers() assert "x-uipath-traceparent-id" in headers + assert headers["x-uipath-traceparent-id"].startswith( + "00-abcdef1234567890abcdef1234567890-" + ) assert "x-uipath-tracebaggage" in headers From 837fe374f0547f1b7132e5522947d20c7983cb5f Mon Sep 17 00:00:00 2001 From: ctiliescuuipath Date: Mon, 25 May 2026 13:50:15 +0300 Subject: [PATCH 075/121] chore: bump package versions (#1680) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index ba264c1ba..b7a89f319 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.57" +version = "0.1.58" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 84372918d..13e5c945d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.57" +version = "0.1.58" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e725d6e39..ad93a67ee 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.57" +version = "0.1.58" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 3ff10783aafaa7ab3c847fa7f201fe718fcacb5e Mon Sep 17 00:00:00 2001 From: Cristian Cotovanu <87022468+cotovanu-cristian@users.noreply.github.com> Date: Mon, 25 May 2026 18:26:59 +0300 Subject: [PATCH 076/121] feat(cli): log bindings.json and resource overwrites at INFO (#1681) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/_cli/_utils/_common.py | 16 +- .../src/uipath/_cli/_utils/_studio_project.py | 20 +- .../test_overwrites_logging.py | 308 ++++++++++++++++++ packages/uipath/uv.lock | 4 +- 5 files changed, 333 insertions(+), 17 deletions(-) create mode 100644 packages/uipath/tests/resource_overrides/test_overwrites_logging.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 8e9c9f581..e6c906102 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.70" +version = "2.10.71" 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/_utils/_common.py b/packages/uipath/src/uipath/_cli/_utils/_common.py index 784192ab6..c24bccff0 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_common.py +++ b/packages/uipath/src/uipath/_cli/_utils/_common.py @@ -214,6 +214,14 @@ async def read_resource_overwrites_from_file( .get("internalArguments", {}) .get("resourceOverwrites", {}) ) + + logger.info( + "Resource overwrites read from %s (%d entries):\n%s", + file_path, + len(resource_overwrites), + json.dumps(resource_overwrites, indent=2, sort_keys=True), + ) + for key, value in resource_overwrites.items(): try: overwrites_dict[key] = ResourceOverwriteParser.parse(key, value) @@ -224,15 +232,9 @@ async def read_resource_overwrites_from_file( e, ) - logger.debug( - "Loaded %d resource overwrite(s) from file %s", - len(overwrites_dict), - file_path, - ) - # Return empty dict if file doesn't exist or invalid json except FileNotFoundError: - logger.debug("Resource overwrites config file not found: %s", file_path) + logger.info("Resource overwrites config file not found: %s", file_path) except json.JSONDecodeError as e: logger.warning("Failed to parse resource overwrites from %s: %s", file_path, e) diff --git a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py index 1380d9119..f70c3e96a 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py +++ b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py @@ -578,6 +578,12 @@ async def get_resource_overwrites(self) -> dict[str, ResourceOverwrite]: with open(UiPathConfig.bindings_file_path, "rb") as f: file_content = f.read() + logger.info( + "Resource bindings (%s):\n%s", + UiPathConfig.bindings_file_path, + file_content.decode(), + ) + solution_id = await self._get_solution_id() tenant_id = os.getenv(ENV_TENANT_ID, None) @@ -600,18 +606,18 @@ async def get_resource_overwrites(self) -> dict[str, ResourceOverwrite]: files=files, ) data = response.json() - overwrites = {} - - for key, value in data.items(): - overwrites[key] = ResourceOverwriteParser.parse(key, value) logger.info( - "Loaded %d resource overwrite(s) from Studio API for solution %s: %s", - len(overwrites), + "Resource overwrites received for solution %s (%d entries):\n%s", solution_id, - overwrites, + len(data), + json.dumps(data, indent=2), ) + overwrites = {} + for key, value in data.items(): + overwrites[key] = ResourceOverwriteParser.parse(key, value) + return overwrites async def create_virtual_resource( diff --git a/packages/uipath/tests/resource_overrides/test_overwrites_logging.py b/packages/uipath/tests/resource_overrides/test_overwrites_logging.py new file mode 100644 index 000000000..e05a7daed --- /dev/null +++ b/packages/uipath/tests/resource_overrides/test_overwrites_logging.py @@ -0,0 +1,308 @@ +# type: ignore +"""Tests for INFO-level diagnostic logging on the resource-overwrites read paths. + +Covers the recent change that surfaces bindings.json content and raw resource +overwrites (from both uipath.json and the Studio API) at INFO so binding/ +overwrite mismatches can be diagnosed from logs alone. +""" + +import json +import logging +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from uipath._cli._utils._common import read_resource_overwrites_from_file +from uipath._cli._utils._studio_project import StudioClient +from uipath.platform.common import GenericResourceOverwrite + +_VALID_OVERWRITES = { + "asset.asset_name": { + "name": "Overwritten Asset Name", + "folderPath": "Overwritten/Asset/Folder", + }, + "bucket.bucket_name": { + "name": "Overwritten Bucket Name", + "folderPath": "Overwritten/Bucket/Folder", + }, +} + + +_TARGET_LOGGERS = ( + "uipath._cli._utils._common", + "uipath._cli._utils._studio_project", +) + + +@pytest.fixture(autouse=True) +def _capture_uipath_loggers( + caplog: pytest.LogCaptureFixture, +) -> None: + """Attach caplog's handler directly to the target module loggers. + + Earlier tests in the suite — chiefly anything that invokes the Click CLI + — call ``setup_logging`` and leave the ``uipath`` logger with + ``propagate = False``. That breaks the usual caplog flow (handler on + root, records reach it via propagation). Some intermediate loggers can + also end up with ``propagate = False`` from other test setups. Attaching + the handler directly to each module logger we assert against, and + forcing the level to DEBUG for the duration of the test, side-steps the + propagation question entirely. + """ + snapshots: list[tuple[logging.Logger, int, bool]] = [] + for name in _TARGET_LOGGERS: + logger = logging.getLogger(name) + snapshots.append((logger, logger.level, logger.propagate)) + logger.setLevel(logging.DEBUG) + logger.propagate = True + logger.addHandler(caplog.handler) + try: + yield + finally: + for logger, level, propagate in snapshots: + logger.removeHandler(caplog.handler) + logger.setLevel(level) + logger.propagate = propagate + + +def _write_uipath_json(directory: Path, overwrites: dict) -> Path: + config_path = directory / "uipath.json" + config_path.write_text( + json.dumps( + { + "runtime": {"internalArguments": {"resourceOverwrites": overwrites}}, + } + ) + ) + return config_path + + +class TestReadResourceOverwritesFromFileLogging: + """Behavior: read_resource_overwrites_from_file logs diagnostic info at INFO.""" + + async def test_logs_raw_overwrites_at_info_when_file_present( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ) -> None: + config_path = _write_uipath_json(tmp_path, _VALID_OVERWRITES) + + with caplog.at_level(logging.INFO, logger="uipath._cli._utils._common"): + result = await read_resource_overwrites_from_file(str(tmp_path)) + + assert set(result.keys()) == set(_VALID_OVERWRITES.keys()) + + info_records = [r for r in caplog.records if r.levelno == logging.INFO] + assert any( + "Resource overwrites read from" in r.getMessage() + and str(config_path) in r.getMessage() + and f"({len(_VALID_OVERWRITES)} entries)" in r.getMessage() + for r in info_records + ), f"expected INFO log with file path and entry count, got: {caplog.text}" + + # The raw JSON payload should be present in the log so a developer can + # diff it against what Studio later returns. + assert "Overwritten Asset Name" in caplog.text + assert "Overwritten Bucket Name" in caplog.text + + async def test_logs_info_when_config_file_missing( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ) -> None: + # tmp_path is empty — no uipath.json present. + missing_dir = tmp_path / "does-not-exist" + missing_dir.mkdir() + + with caplog.at_level(logging.INFO, logger="uipath._cli._utils._common"): + result = await read_resource_overwrites_from_file(str(missing_dir)) + + assert result == {} + info_messages = [ + r.getMessage() for r in caplog.records if r.levelno == logging.INFO + ] + assert any( + "Resource overwrites config file not found" in msg for msg in info_messages + ), f"expected INFO log for missing config, got: {info_messages}" + + async def test_logs_warning_when_json_is_malformed( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ) -> None: + (tmp_path / "uipath.json").write_text("{not valid json") + + with caplog.at_level(logging.WARNING, logger="uipath._cli._utils._common"): + result = await read_resource_overwrites_from_file(str(tmp_path)) + + assert result == {} + warnings = [r for r in caplog.records if r.levelno == logging.WARNING] + assert any( + "Failed to parse resource overwrites" in r.getMessage() for r in warnings + ) + + async def test_unrecognized_overwrite_key_is_skipped_with_warning( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ) -> None: + overwrites = { + **_VALID_OVERWRITES, + "totallyUnknownKind.foo": {"name": "x", "folderPath": "y"}, + } + _write_uipath_json(tmp_path, overwrites) + + with caplog.at_level(logging.WARNING, logger="uipath._cli._utils._common"): + result = await read_resource_overwrites_from_file(str(tmp_path)) + + # Valid entries still parsed; unknown key dropped. + assert set(result.keys()) == set(_VALID_OVERWRITES.keys()) + assert any( + "Skipping unrecognized resource overwrite" in r.getMessage() + and "totallyUnknownKind.foo" in r.getMessage() + for r in caplog.records + if r.levelno == logging.WARNING + ) + + +class TestStudioClientGetResourceOverwritesLogging: + """Behavior: StudioClient.get_resource_overwrites logs bindings + raw payload.""" + + @pytest.fixture + def studio_client(self) -> StudioClient: + # Inject a mock UiPath so no real HTTP setup is required. + mock_uipath = MagicMock() + mock_uipath.api_client.request_async = AsyncMock() + client = StudioClient(project_id="test-project-id", uipath=mock_uipath) + # Avoid the network call that resolves the solution id. + client._get_solution_id = AsyncMock(return_value="test-solution-id") # type: ignore[method-assign] + return client + + async def test_warns_and_returns_empty_when_bindings_file_missing( + self, + studio_client: StudioClient, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + ) -> None: + from uipath.platform.common._config import ConfigurationManager + + missing_path = tmp_path / "bindings.json" + monkeypatch.setattr( + ConfigurationManager, + "bindings_file_path", + property(lambda self: missing_path), + ) + + with caplog.at_level(logging.WARNING): + result = await studio_client.get_resource_overwrites() + + assert result == {} + assert any( + "Bindings file not found" in r.getMessage() + for r in caplog.records + if r.levelno == logging.WARNING + ) + # No request should have been made when there is nothing to upload. + studio_client.uipath.api_client.request_async.assert_not_called() + + async def test_logs_bindings_content_and_received_overwrites_at_info( + self, + studio_client: StudioClient, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + ) -> None: + from uipath.platform.common._config import ConfigurationManager + + bindings_path = tmp_path / "bindings.json" + bindings_content = json.dumps( + {"version": "2", "resources": [{"name": "my_bucket", "kind": "bucket"}]} + ) + bindings_path.write_text(bindings_content) + monkeypatch.setattr( + ConfigurationManager, + "bindings_file_path", + property(lambda self: bindings_path), + ) + monkeypatch.delenv("UIPATH_TENANT_ID", raising=False) + + response = MagicMock() + response.json.return_value = { + "bucket.my_bucket": { + "name": "prod_bucket", + "folderPath": "Prod/Folder", + } + } + studio_client.uipath.api_client.request_async = AsyncMock(return_value=response) + + with caplog.at_level(logging.INFO, logger="uipath._cli._utils._studio_project"): + result = await studio_client.get_resource_overwrites() + + # Returned dict is parsed via ResourceOverwriteParser. + assert set(result.keys()) == {"bucket.my_bucket"} + + info_text = "\n".join( + r.getMessage() for r in caplog.records if r.levelno == logging.INFO + ) + # Bindings content is logged so we can compare what was sent to Studio. + assert "Resource bindings" in info_text + assert "my_bucket" in info_text + # Received overwrites payload is logged with the solution id and count. + assert "Resource overwrites received for solution test-solution-id" in info_text + assert "(1 entries)" in info_text + assert "prod_bucket" in info_text + + async def test_parses_received_overwrites_into_resource_overwrite_objects( + self, + studio_client: StudioClient, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + from uipath.platform.common._config import ConfigurationManager + + bindings_path = tmp_path / "bindings.json" + bindings_path.write_text("{}") + monkeypatch.setattr( + ConfigurationManager, + "bindings_file_path", + property(lambda self: bindings_path), + ) + + response = MagicMock() + response.json.return_value = { + "bucket.my_bucket": { + "name": "prod_bucket", + "folderPath": "Prod/Folder", + } + } + studio_client.uipath.api_client.request_async = AsyncMock(return_value=response) + + result = await studio_client.get_resource_overwrites() + + parsed = result["bucket.my_bucket"] + assert isinstance(parsed, GenericResourceOverwrite) + assert parsed.resource_identifier == "prod_bucket" + assert parsed.folder_identifier == "Prod/Folder" + + async def test_passes_tenant_id_header_from_environment( + self, + studio_client: StudioClient, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + from uipath.platform.common._config import ConfigurationManager + + bindings_path = tmp_path / "bindings.json" + bindings_path.write_text("{}") + monkeypatch.setattr( + ConfigurationManager, + "bindings_file_path", + property(lambda self: bindings_path), + ) + monkeypatch.setenv("UIPATH_TENANT_ID", "tenant-from-env") + + response = MagicMock() + response.json.return_value = {} + request_mock = AsyncMock(return_value=response) + studio_client.uipath.api_client.request_async = request_mock + + await studio_client.get_resource_overwrites() + + # The header carrying the tenant id should reflect the env var value. + call_kwargs = request_mock.await_args.kwargs + headers = call_kwargs["headers"] + assert any(value == "tenant-from-env" for value in headers.values()), headers diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index ad93a67ee..9490d076e 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-20T15:14:33.9075119Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.70" +version = "2.10.71" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 4cea46bd3122ccca45d2feed985d695973cc6063 Mon Sep 17 00:00:00 2001 From: Gabriel Martin <91462031+gcuip@users.noreply.github.com> Date: Thu, 28 May 2026 10:32:27 +0200 Subject: [PATCH 077/121] feat: send jobkey to ECS [ECS-1819] (#1687) --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath/platform/common/_job_context.py | 13 ++ .../_context_grounding_service.py | 33 ++++- .../tests/common/test_job_context.py | 22 +++ .../test_context_grounding_service.py | 138 +++++++++++++++++- packages/uipath-platform/uv.lock | 4 +- packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 4 +- 8 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/common/_job_context.py create mode 100644 packages/uipath-platform/tests/common/test_job_context.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index b7a89f319..215882460 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.58" +version = "0.1.59" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/_job_context.py b/packages/uipath-platform/src/uipath/platform/common/_job_context.py new file mode 100644 index 000000000..ccbd99f22 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/common/_job_context.py @@ -0,0 +1,13 @@ +from ._config import UiPathConfig +from .constants import HEADER_JOB_KEY + + +def header_job_key() -> dict[str, str]: + """Return the X-UiPath-JobKey header when the orchestrator job key is set. + + Returns an empty dict when ``UiPathConfig.job_key`` is unset or empty. + """ + job_key = UiPathConfig.job_key + if not job_key: + return {} + return {HEADER_JOB_KEY: job_key} diff --git a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py index 2e6e40628..6bff3f77f 100644 --- a/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py +++ b/packages/uipath-platform/src/uipath/platform/context_grounding/_context_grounding_service.py @@ -12,6 +12,7 @@ from ..common._execution_context import UiPathExecutionContext from ..common._folder_context import FolderContext, header_folder from ..common._http_config import get_httpx_client_kwargs +from ..common._job_context import header_job_key from ..common._models import Endpoint, RequestSpec from ..common.constants import ( ORCHESTRATOR_STORAGE_BUCKET_DATA_SOURCE, @@ -221,6 +222,7 @@ def retrieve_across_folders( spec.method, spec.endpoint, params=spec.params, + headers=spec.headers, ).json() return [ @@ -250,6 +252,7 @@ async def retrieve_across_folders_async( spec.method, spec.endpoint, params=spec.params, + headers=spec.headers, ) ).json() @@ -268,6 +271,7 @@ def _retrieve_system_indexes( spec.method, spec.endpoint, params=spec.params, + headers=spec.headers, ).json() return [ @@ -286,6 +290,7 @@ async def _retrieve_system_indexes_async( spec.method, spec.endpoint, params=spec.params, + headers=spec.headers, ) ).json() @@ -455,7 +460,7 @@ def list( "GET", Endpoint("/ecs_/v2/indexes"), params={"$expand": "dataSource"}, - headers={**header_folder(folder_key, None)}, + headers={**header_folder(folder_key, None), **header_job_key()}, ).json() return [ ContextGroundingIndex.model_validate(item) @@ -483,7 +488,7 @@ async def list_async( "GET", Endpoint("/ecs_/v2/indexes"), params={"$expand": "dataSource"}, - headers={**header_folder(folder_key, None)}, + headers={**header_folder(folder_key, None), **header_job_key()}, ) ).json() return [ @@ -521,6 +526,7 @@ def retrieve_by_id( spec.method, spec.endpoint, params=spec.params, + headers=spec.headers, ).json() @traced(name="contextgrounding_retrieve_by_id", run_type="uipath") @@ -553,6 +559,7 @@ async def retrieve_by_id_async( spec.method, spec.endpoint, params=spec.params, + headers=spec.headers, ) return response.json() @@ -1997,6 +2004,7 @@ def _ingest_spec( endpoint=Endpoint(f"/ecs_/v2/indexes/{key}/ingest"), headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2024,6 +2032,7 @@ def _retrieve_across_folders_spec( method="GET", endpoint=Endpoint("/ecs_/v2/indexes/allacrossfolders"), params=params, + headers={**header_job_key()}, ) def _retrieve_system_indexes_spec( @@ -2040,6 +2049,7 @@ def _retrieve_system_indexes_spec( method="GET", endpoint=Endpoint("/ecs_/v2/indexes/allsystemindexes"), params=params, + headers={**header_job_key()}, ) def _list_spec( @@ -2054,6 +2064,7 @@ def _list_spec( }, headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2074,6 +2085,7 @@ def _retrieve_spec( }, headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2126,6 +2138,7 @@ def _create_spec( json=payload.model_dump(by_alias=True, exclude_none=True), headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2156,7 +2169,7 @@ def _create_ephemeral_spec( method="POST", endpoint=Endpoint("/ecs_/v2/indexes/createephemeral"), json=payload.model_dump(by_alias=True, exclude_none=True), - headers={**header_folder(folder_key, None)}, + headers={**header_folder(folder_key, None), **header_job_key()}, ) def _build_data_source(self, source: SourceConfig) -> Dict[str, Any]: @@ -2256,6 +2269,7 @@ def _retrieve_by_id_spec( endpoint=Endpoint(f"/ecs_/v2/indexes/{id}"), headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2272,6 +2286,7 @@ def _delete_by_id_spec( endpoint=Endpoint(f"/ecs_/v2/indexes/{id}"), headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2299,6 +2314,7 @@ def _search_spec( }, headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2335,6 +2351,7 @@ def _unified_search_spec( json=json_body, headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2364,6 +2381,7 @@ def _deep_rag_creation_spec( }, headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2387,7 +2405,7 @@ def _deep_rag_ephemeral_creation_spec( params={ "$select": "id,lastDeepRagStatus,createdDate", }, - headers={}, + headers={**header_job_key()}, ) def _batch_transform_creation_spec( @@ -2437,6 +2455,7 @@ def _batch_transform_creation_spec( }, headers={ **header_folder(folder_key, None), + **header_job_key(), }, ) @@ -2463,7 +2482,7 @@ def _batch_transform_ephemeral_creation_spec( column.model_dump(by_alias=True) for column in output_columns ], }, - headers={}, + headers={**header_job_key()}, ) def _deep_rag_retrieve_spec( @@ -2476,6 +2495,7 @@ def _deep_rag_retrieve_spec( params={ "$expand": "content", }, + headers={**header_job_key()}, ) def _batch_transform_retrieve_spec( @@ -2485,6 +2505,7 @@ def _batch_transform_retrieve_spec( return RequestSpec( method="GET", endpoint=Endpoint(f"/ecs_/v2/batchRag/{id}"), + headers={**header_job_key()}, ) def _batch_transform_get_read_uri_spec( @@ -2494,6 +2515,7 @@ def _batch_transform_get_read_uri_spec( return RequestSpec( method="GET", endpoint=Endpoint(f"/ecs_/v2/batchRag/{id}/GetReadUri"), + headers={**header_job_key()}, ) def _batch_transform_download_blob_spec( @@ -2503,6 +2525,7 @@ def _batch_transform_download_blob_spec( return RequestSpec( method="GET", endpoint=Endpoint(f"/ecs_/v2/batchRag/{id}/DownloadBlob"), + headers={**header_job_key()}, ) def _resolve_folder_key(self, folder_key, folder_path): diff --git a/packages/uipath-platform/tests/common/test_job_context.py b/packages/uipath-platform/tests/common/test_job_context.py new file mode 100644 index 000000000..6a4e1a97d --- /dev/null +++ b/packages/uipath-platform/tests/common/test_job_context.py @@ -0,0 +1,22 @@ +import pytest + +from uipath.platform.common._job_context import header_job_key +from uipath.platform.common.constants import ENV_JOB_KEY, HEADER_JOB_KEY + + +def test_returns_header_when_env_var_set(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv(ENV_JOB_KEY, "test-job-key") + + assert header_job_key() == {HEADER_JOB_KEY: "test-job-key"} + + +def test_returns_empty_when_env_var_unset(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(ENV_JOB_KEY, raising=False) + + assert header_job_key() == {} + + +def test_returns_empty_when_env_var_blank(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv(ENV_JOB_KEY, "") + + assert header_job_key() == {} diff --git a/packages/uipath-platform/tests/services/test_context_grounding_service.py b/packages/uipath-platform/tests/services/test_context_grounding_service.py index ace7affcd..70c529ec6 100644 --- a/packages/uipath-platform/tests/services/test_context_grounding_service.py +++ b/packages/uipath-platform/tests/services/test_context_grounding_service.py @@ -6,7 +6,11 @@ from pytest_httpx import HTTPXMock from uipath.platform import UiPathApiConfig, UiPathExecutionContext -from uipath.platform.common.constants import HEADER_USER_AGENT +from uipath.platform.common.constants import ( + ENV_JOB_KEY, + HEADER_JOB_KEY, + HEADER_USER_AGENT, +) from uipath.platform.context_grounding import ( BatchTransformCreationResponse, BatchTransformOutputColumn, @@ -3782,3 +3786,135 @@ def test_unified_search_with_scope( assert "filter" not in request_body assert request_body["scope"]["folder"] == "docs" assert request_body["scope"]["extension"] == ".pdf" + + +class TestJobKeyHeader: + """X-UiPath-JobKey is attached to outbound ECS calls when UIPATH_JOB_KEY is set.""" + + _INDEX = ContextGroundingIndex( + id="test-index-id", + name="test-index", + last_ingestion_status="Completed", + ) + + def test_ingest_data_carries_job_key_header( + self, + service: ContextGroundingService, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv(ENV_JOB_KEY, "job-key-ingest") + + with patch.object(service, "request") as mock_request: + mock_request.return_value = MagicMock() + service.ingest_data(self._INDEX, folder_key="test-folder-key") + + headers = mock_request.call_args[1]["headers"] + assert headers[HEADER_JOB_KEY] == "job-key-ingest" + + @pytest.mark.anyio + async def test_ingest_data_async_carries_job_key_header( + self, + service: ContextGroundingService, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv(ENV_JOB_KEY, "job-key-ingest-async") + + with patch.object(service, "request_async") as mock_request: + mock_request.return_value = MagicMock() + await service.ingest_data_async(self._INDEX, folder_key="test-folder-key") + + headers = mock_request.call_args[1]["headers"] + assert headers[HEADER_JOB_KEY] == "job-key-ingest-async" + + def test_unified_search_carries_job_key_header( + self, + service: ContextGroundingService, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv(ENV_JOB_KEY, "job-key-search") + + with patch.object(service, "request") as mock_request: + mock_response = MagicMock() + mock_response.json.return_value = { + "semanticResults": {"values": []}, + "explanation": None, + } + mock_request.return_value = mock_response + with patch.object(service, "retrieve", return_value=self._INDEX): + service.unified_search( + name="test-index", + query="test query", + folder_key="test-folder-key", + ) + + headers = mock_request.call_args[1]["headers"] + assert headers[HEADER_JOB_KEY] == "job-key-search" + + def test_start_deep_rag_carries_job_key_header( + self, + service: ContextGroundingService, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv(ENV_JOB_KEY, "job-key-deeprag") + + with patch.object(service, "request") as mock_request: + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "new-deep-rag-task-id", + "lastDeepRagStatus": "Queued", + "createdDate": "2024-01-15T10:30:00Z", + } + mock_request.return_value = mock_response + service.start_deep_rag( + index_id="test-index-id", + name="my-deep-rag-task", + prompt="Summarize", + glob_pattern="*.pdf", + citation_mode=CitationMode.INLINE, + folder_key="test-folder-key", + ) + + headers = mock_request.call_args[1]["headers"] + assert headers[HEADER_JOB_KEY] == "job-key-deeprag" + + def test_start_batch_transform_carries_job_key_header( + self, + service: ContextGroundingService, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv(ENV_JOB_KEY, "job-key-batch") + + with patch.object(service, "request") as mock_request: + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "new-batch-id", + "lastBatchRagStatus": "Queued", + } + mock_request.return_value = mock_response + service.start_batch_transform( + index_id="test-index-id", + name="my-batch-task", + prompt="Extract", + output_columns=[ + BatchTransformOutputColumn(name="col1", description="d") + ], + enable_web_search_grounding=False, + folder_key="test-folder-key", + ) + + headers = mock_request.call_args[1]["headers"] + assert headers[HEADER_JOB_KEY] == "job-key-batch" + + def test_ingest_data_omits_job_key_header_when_env_unset( + self, + service: ContextGroundingService, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.delenv(ENV_JOB_KEY, raising=False) + + with patch.object(service, "request") as mock_request: + mock_request.return_value = MagicMock() + service.ingest_data(self._INDEX, folder_key="test-folder-key") + + headers = mock_request.call_args[1]["headers"] + assert HEADER_JOB_KEY not in headers diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 13e5c945d..bee52f2aa 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-20T15:43:10.4544027Z" +exclude-newer = "2026-05-25T20:56:22.964456Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.58" +version = "0.1.59" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index e6c906102..4aea69f52 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.53, <0.2.0", + "uipath-platform>=0.1.59, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 9490d076e..304458d8c 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-25T20:56:31.043599Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.58" +version = "0.1.59" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 1b81042fbb4f8f59d465ca59d2e3892f8bb4607f Mon Sep 17 00:00:00 2001 From: vldcmp-uipath Date: Thu, 28 May 2026 15:49:59 +0300 Subject: [PATCH 078/121] feat(agent): add Function tool type for agent process tools (#1689) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 3 + .../uipath/tests/agent/models/test_agent.py | 69 +++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 4aea69f52..2cfaef895 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.71" +version = "2.10.72" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 72e7c438f..6fda35789 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -113,6 +113,7 @@ class AgentToolType(str, CaseInsensitiveEnum): API = "Api" PROCESS_ORCHESTRATION = "ProcessOrchestration" FLOW = "Flow" + FUNCTION = "Function" INTEGRATION = "Integration" INTERNAL = "Internal" IXP = "Ixp" @@ -792,6 +793,7 @@ class AgentProcessToolResourceConfig(BaseAgentToolResourceConfig): AgentToolType.API, AgentToolType.PROCESS_ORCHESTRATION, AgentToolType.FLOW, + AgentToolType.FUNCTION, ] output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") properties: AgentProcessToolProperties @@ -1336,6 +1338,7 @@ def _normalize_resources(v: Dict[str, Any]) -> None: "api": "Api", "processorchestration": "ProcessOrchestration", "flow": "Flow", + "function": "Function", "integration": "Integration", "internal": "Internal", "ixp": "Ixp", diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 14ca8018d..6b2825833 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3627,6 +3627,75 @@ def test_flow_tool_resource_case_insensitive(self): assert isinstance(tool_resource, AgentProcessToolResourceConfig) assert tool_resource.type == AgentToolType.FLOW + def test_function_tool_type_enum_value(self): + """AgentToolType.FUNCTION exists with the wire value 'Function' and is case-insensitive.""" + assert AgentToolType.FUNCTION.value == "Function" + assert AgentToolType("function") is AgentToolType.FUNCTION + assert AgentToolType("FUNCTION") is AgentToolType.FUNCTION + + def test_function_tool_resource_deserialization(self): + """A resource with type='Function' is parsed as AgentProcessToolResourceConfig.""" + resources = [ + { + "$resourceType": "tool", + "type": "Function", + "id": "function-tool-1", + "inputSchema": { + "type": "object", + "properties": {"input": {"type": "string"}}, + }, + "outputSchema": {"type": "object", "properties": {}}, + "arguments": {}, + "settings": {"timeout": 0, "maxAttempts": 0, "retryDelay": 0}, + "properties": { + "processName": "MyFunction", + "folderPath": "/Shared/Functions", + }, + "name": "Function Tool", + "description": "Test Function tool", + } + ] + + json_data = self._agent_dict_with_resources(resources) + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + tool_resource = config.resources[0] + assert isinstance(tool_resource, AgentProcessToolResourceConfig) + assert tool_resource.type == AgentToolType.FUNCTION + assert tool_resource.properties.process_name == "MyFunction" + assert tool_resource.properties.folder_path == "/Shared/Functions" + + def test_function_tool_resource_case_insensitive(self): + """A resource with lowercase type='function' also deserializes via CaseInsensitiveEnum.""" + resources = [ + { + "$resourceType": "tool", + "type": "function", + "id": "function-tool-2", + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "arguments": {}, + "settings": {"timeout": 0, "maxAttempts": 0, "retryDelay": 0}, + "properties": { + "processName": "MyFunction", + "folderPath": "/Shared/Functions", + }, + "name": "Function Tool", + "description": "Test Function tool", + } + ] + + json_data = self._agent_dict_with_resources(resources) + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + tool_resource = config.resources[0] + assert isinstance(tool_resource, AgentProcessToolResourceConfig) + assert tool_resource.type == AgentToolType.FUNCTION + def test_escalation_missing_escalation_type_defaults_to_zero(self): """Test that missing escalationType defaults to 0.""" resources = [ diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 304458d8c..b319fa1ba 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.71" +version = "2.10.72" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From f2345830234ec4d3621f8ce6046eef651b8216bb Mon Sep 17 00:00:00 2001 From: Norman <82416746+norman-le@users.noreply.github.com> Date: Thu, 28 May 2026 17:33:23 -0400 Subject: [PATCH 079/121] feat: add client side tools to bridge and new cas events [JAR-9629] (#1638) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/chat/__init__.py | 4 + .../src/uipath/core/chat/exchange.py | 13 + .../uipath-core/src/uipath/core/chat/tool.py | 18 ++ packages/uipath-core/uv.lock | 4 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/_cli/_chat/_bridge.py | 84 +++--- .../uipath/src/uipath/agent/models/agent.py | 12 + .../uipath/tests/agent/models/test_agent.py | 206 ++++++++++++++ packages/uipath/tests/cli/chat/test_bridge.py | 253 ++++++++++++++++++ packages/uipath/uv.lock | 4 +- 12 files changed, 567 insertions(+), 37 deletions(-) diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 147726394..c07e4c3b7 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.16" +version = "0.5.17" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/chat/__init__.py b/packages/uipath-core/src/uipath/core/chat/__init__.py index 77fa6cbee..ee4a4c674 100644 --- a/packages/uipath-core/src/uipath/core/chat/__init__.py +++ b/packages/uipath-core/src/uipath/core/chat/__init__.py @@ -71,6 +71,7 @@ ) from .event import UiPathConversationEvent, UiPathConversationLabelUpdatedEvent from .exchange import ( + UiPathClientSideToolDeclaration, UiPathConversationExchange, UiPathConversationExchangeData, UiPathConversationExchangeEndEvent, @@ -107,6 +108,7 @@ UiPathSessionStartEvent, ) from .tool import ( + UiPathConversationExecutingToolCallEvent, UiPathConversationToolCall, UiPathConversationToolCallConfirmation, UiPathConversationToolCallConfirmationData, @@ -138,6 +140,7 @@ "UiPathSessionEndingEvent", "UiPathSessionEndEvent", # Exchange + "UiPathClientSideToolDeclaration", "UiPathConversationExchangeStartEvent", "UiPathConversationExchangeEndEvent", "UiPathConversationExchangeEvent", @@ -171,6 +174,7 @@ "UiPathConversationCitationData", "UiPathConversationCitation", # Tool + "UiPathConversationExecutingToolCallEvent", "UiPathConversationToolCallStartEvent", "UiPathConversationToolCallEndEvent", "UiPathConversationToolCallConfirmation", diff --git a/packages/uipath-core/src/uipath/core/chat/exchange.py b/packages/uipath-core/src/uipath/core/chat/exchange.py index 788bbe560..835489dfb 100644 --- a/packages/uipath-core/src/uipath/core/chat/exchange.py +++ b/packages/uipath-core/src/uipath/core/chat/exchange.py @@ -28,11 +28,24 @@ ) +class UiPathClientSideToolDeclaration(BaseModel): + """A client-side tool declaration from the SDK client.""" + + name: str + input_schema: dict[str, Any] | None = Field(None, alias="inputSchema") + output_schema: dict[str, Any] | None = Field(None, alias="outputSchema") + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + class UiPathConversationExchangeStartEvent(BaseModel): """Signals the start of an exchange of messages within a conversation.""" conversation_sequence: int | None = Field(None, alias="conversationSequence") metadata: dict[str, Any] | None = Field(None, alias="metaData") + client_side_tools: list[UiPathClientSideToolDeclaration] | None = Field( + None, alias="clientSideTools" + ) timestamp: str | None = None model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/tool.py b/packages/uipath-core/src/uipath/core/chat/tool.py index 8af5fb604..514e42908 100644 --- a/packages/uipath-core/src/uipath/core/chat/tool.py +++ b/packages/uipath-core/src/uipath/core/chat/tool.py @@ -27,6 +27,8 @@ class UiPathConversationToolCallStartEvent(BaseModel): metadata: dict[str, Any] | None = Field(None, alias="metaData") require_confirmation: bool | None = Field(None, alias="requireConfirmation") input_schema: Any | None = Field(None, alias="inputSchema") + is_client_side_tool: bool | None = Field(None, alias="isClientSideTool") + output_schema: Any | None = Field(None, alias="outputSchema") model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -43,6 +45,19 @@ class UiPathConversationToolCallEndEvent(BaseModel): model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) +class UiPathConversationExecutingToolCallEvent(BaseModel): + """Signals the client that the tool is about to be executed. + + Emitted in all scenarios. For client-side tools, the client should begin + executing its handler upon receiving this event. + """ + + timestamp: str | None = None + input: dict[str, Any] | None = None + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + class UiPathConversationToolCallConfirmationEvent(BaseModel): """Signals a tool call confirmation (approve/reject) from the client.""" @@ -82,6 +97,9 @@ class UiPathConversationToolCallEvent(BaseModel): confirm: UiPathConversationToolCallConfirmationEvent | None = Field( None, alias="confirmToolCall" ) + executing: UiPathConversationExecutingToolCallEvent | None = Field( + None, alias="executingToolCall" + ) meta_event: dict[str, Any] | None = Field(None, alias="metaEvent") error: UiPathConversationErrorEvent | None = Field(None, alias="toolCallError") diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 9b043599c..9aa9417f4 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-20T15:11:06.1716446Z" +exclude-newer = "2026-05-25T19:32:11.835974Z" exclude-newer-span = "P2D" [[package]] @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.16" +version = "0.5.17" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index bee52f2aa..dabbd63ad 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.16" +version = "0.5.17" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 2cfaef895..a7a978f7c 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.72" +version = "2.10.73" 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/_chat/_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_bridge.py index 2a382a59e..96566e898 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_bridge.py @@ -13,8 +13,11 @@ UiPathConversationEvent, UiPathConversationExchangeEndEvent, UiPathConversationExchangeEvent, + UiPathConversationExecutingToolCallEvent, UiPathConversationMessageEvent, UiPathConversationToolCallConfirmationEvent, + UiPathConversationToolCallEndEvent, + UiPathConversationToolCallEvent, ) from uipath.core.triggers import UiPathResumeTrigger from uipath.runtime.chat import UiPathChatProtocol @@ -122,9 +125,11 @@ def __init__( self._client: Any | None = None self._connected_event = asyncio.Event() - self._tool_confirmation_event = asyncio.Event() - self._tool_confirmation_value: ( - UiPathConversationToolCallConfirmationEvent | None + self._tool_resume_event = asyncio.Event() + self._tool_resume_value: ( + UiPathConversationToolCallConfirmationEvent + | UiPathConversationToolCallEndEvent + | None ) = None self._current_message_id: str | None = None @@ -362,33 +367,52 @@ async def emit_exchange_error_event(self, error: Exception) -> None: async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger): """No-op. - Tool confirmation — the only interrupt pattern CAS uses today — is - handled end-to-end via ``startToolCall`` with ``requireConfirmation: - true`` paired with ``wait_for_resume()``. This is deliberately - simpler than the old interrupt-based flow: CAS needs - ``requireConfirmation`` on the tool call event itself to render the - confirmation UI, so a parallel ``startInterrupt`` event would be - redundant. - - The only hypothetical reason to put work here is a generic, - non-tool-call agent interrupt (e.g. a coded agent calling - ``interrupt("do you want to continue?")``). Nothing uses that today - and it's not a near-term requirement — the method is kept for - generic flexibility. + Tool confirmation is handled end-to-end via ``startToolCall`` with + ``requireConfirmation: true`` paired with ``wait_for_resume()``. + executingToolCall is emitted by the MessageMapper (non-confirmed + tools) and the runtime loop post-confirmation (confirmed tools). """ return None + async def emit_executing_tool_call_event( + self, + tool_call_id: str, + tool_input: dict[str, Any] | None = None, + ) -> None: + """Emit an executingToolCall event. + + Called by the runtime loop after a tool-call confirmation resumes + to signal that the tool is about to execute with the final input. + """ + if not self._current_message_id: + return + + executing_event = UiPathConversationMessageEvent( + message_id=self._current_message_id, + tool_call=UiPathConversationToolCallEvent( + tool_call_id=tool_call_id, + executing=UiPathConversationExecutingToolCallEvent( + input=tool_input, + ), + ), + ) + await self.emit_message_event(executing_event) + async def wait_for_resume(self) -> dict[str, Any]: - """Wait for a confirmToolCall event to be received.""" - self._tool_confirmation_event.clear() - self._tool_confirmation_value = None + """Wait for a tool resume event (confirmToolCall or endToolCall) to be received.""" + if self._tool_resume_value is None: + self._tool_resume_event.clear() + await self._tool_resume_event.wait() - await self._tool_confirmation_event.wait() + value = self._tool_resume_value + self._tool_resume_value = None + self._tool_resume_event.clear() - if self._tool_confirmation_value: - return self._tool_confirmation_value.model_dump( - mode="python", by_alias=False - ) + """For the case where there's no tool confirmation and the client side tool sends endToolCall back before wait_for_resume is called. + Unlikely in practice, but possible in theory, since executingToolCall is emitted during the streaming. + """ + if value: + return value.model_dump(mode="python", by_alias=False) return {} @property @@ -424,13 +448,13 @@ async def _handle_conversation_event( parsed_event.exchange and parsed_event.exchange.message and (tool_call := parsed_event.exchange.message.tool_call) - and (confirm := tool_call.confirm) ): - logger.info( - f"Received confirmToolCall for tool_call_id: {tool_call.tool_call_id}, approved: {confirm.approved}" - ) - self._tool_confirmation_value = confirm - self._tool_confirmation_event.set() + if confirm := tool_call.confirm: + self._tool_resume_value = confirm + self._tool_resume_event.set() + elif end := tool_call.end: + self._tool_resume_value = end + self._tool_resume_event.set() except Exception as e: logger.warning(f"Error parsing conversation event: {e}") diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 6fda35789..7123a43e0 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -117,6 +117,7 @@ class AgentToolType(str, CaseInsensitiveEnum): INTEGRATION = "Integration" INTERNAL = "Internal" IXP = "Ixp" + CLIENT_SIDE = "ClientSide" UNKNOWN = "Unknown" # fallback branch discriminator @@ -945,6 +946,15 @@ class AgentInternalToolResourceConfig(BaseAgentToolResourceConfig): ) +class AgentClientSideToolResourceConfig(BaseAgentToolResourceConfig): + """Resource config for client-side tools executed by the client SDK.""" + + type: Literal[AgentToolType.CLIENT_SIDE] = AgentToolType.CLIENT_SIDE + properties: BaseResourceProperties = Field(default_factory=BaseResourceProperties) + output_schema: Optional[Dict[str, Any]] = Field(None, alias="outputSchema") + arguments: Optional[Dict[str, Any]] = Field(default_factory=dict) + + class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig): """Fallback for unknown tool types (parent normalizer sets type='Unknown').""" @@ -958,6 +968,7 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig): AgentIntegrationToolResourceConfig, AgentInternalToolResourceConfig, AgentIxpExtractionResourceConfig, + AgentClientSideToolResourceConfig, AgentUnknownToolResourceConfig, # when parent sets type="Unknown" ], Field(discriminator="type"), @@ -1342,6 +1353,7 @@ def _normalize_resources(v: Dict[str, Any]) -> None: "integration": "Integration", "internal": "Internal", "ixp": "Ixp", + "clientside": "ClientSide", "unknown": "Unknown", } CONTEXT_MODE_MAP = { diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 6b2825833..fcfefa946 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -8,6 +8,7 @@ AgentBooleanOperator, AgentBooleanRule, AgentBuiltInValidatorGuardrail, + AgentClientSideToolResourceConfig, AgentContextResourceConfig, AgentContextRetrievalMode, AgentContextType, @@ -4088,3 +4089,208 @@ def test_argument_group_name_recipient_missing_argument_name_raises(self): payload = {"type": 8} with pytest.raises(ValidationError): TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + def test_agent_with_client_side_tool(self): + """Test agent with ClientSide tool resource.""" + + json_data = { + "version": "1.0.0", + "id": "aaaaaaaa-0000-0000-0000-000000000010", + "name": "Agent with ClientSide Tool", + "metadata": {"isConversational": False, "storageVersion": "26.0.0"}, + "messages": [ + {"role": "System", "content": "You are an agentic assistant."}, + ], + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": { + "type": "object", + "properties": {"content": {"type": "string"}}, + }, + "settings": { + "model": "gpt-4o-2024-11-20", + "maxTokens": 16384, + "temperature": 0, + "engine": "basic-v2", + }, + "resources": [ + { + "$resourceType": "tool", + "id": "cst-0001-0000-0000-000000000001", + "name": "browser_navigate", + "description": "Navigate to a URL in the browser", + "location": "external", + "type": "ClientSide", + "inputSchema": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The URL to navigate to", + } + }, + "required": ["url"], + }, + "outputSchema": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "content": {"type": "string"}, + }, + }, + "arguments": {"timeout": 30}, + "properties": {}, + "isEnabled": True, + } + ], + "features": [], + } + + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + assert config.name == "Agent with ClientSide Tool" + assert len(config.resources) == 1 + + tool = config.resources[0] + assert isinstance(tool, AgentClientSideToolResourceConfig) + assert tool.resource_type == AgentResourceType.TOOL + assert tool.type == AgentToolType.CLIENT_SIDE + assert tool.name == "browser_navigate" + assert tool.description == "Navigate to a URL in the browser" + + # Validate input schema + assert tool.input_schema["type"] == "object" + assert "url" in tool.input_schema["properties"] + assert tool.input_schema["required"] == ["url"] + + # Validate outputSchema alias deserializes to output_schema + assert tool.output_schema is not None + assert tool.output_schema["type"] == "object" + assert "title" in tool.output_schema["properties"] + assert "content" in tool.output_schema["properties"] + + # Validate arguments + assert tool.arguments == {"timeout": 30} + + def test_agent_with_client_side_tool_lowercase_type(self): + """Test that _normalize_resources handles lowercase 'clientside' type.""" + + json_data = { + "version": "1.0.0", + "id": "aaaaaaaa-0000-0000-0000-000000000011", + "name": "Agent with clientside Tool", + "metadata": {"isConversational": False, "storageVersion": "26.0.0"}, + "messages": [ + {"role": "System", "content": "You are an agentic assistant."}, + ], + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": { + "type": "object", + "properties": {"content": {"type": "string"}}, + }, + "settings": { + "model": "gpt-4o-2024-11-20", + "maxTokens": 16384, + "temperature": 0, + "engine": "basic-v2", + }, + "resources": [ + { + "$resourceType": "tool", + "id": "cst-0002-0000-0000-000000000001", + "name": "clipboard_copy", + "description": "Copy text to clipboard", + "location": "external", + "type": "clientside", + "inputSchema": { + "type": "object", + "properties": { + "text": {"type": "string"}, + }, + }, + "properties": {}, + "isEnabled": True, + } + ], + "features": [], + } + + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + tool = config.resources[0] + assert isinstance(tool, AgentClientSideToolResourceConfig) + assert tool.type == AgentToolType.CLIENT_SIDE + assert tool.name == "clipboard_copy" + + # output_schema and arguments should default + assert tool.output_schema is None + assert tool.arguments == {} + + def test_agent_with_client_side_tool_output_schema_alias(self): + """Test that the outputSchema alias correctly maps to output_schema.""" + + json_data = { + "version": "1.0.0", + "id": "aaaaaaaa-0000-0000-0000-000000000012", + "name": "Agent with ClientSide outputSchema alias", + "metadata": {"isConversational": False, "storageVersion": "26.0.0"}, + "messages": [ + {"role": "System", "content": "You are an agentic assistant."}, + ], + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": { + "type": "object", + "properties": {"content": {"type": "string"}}, + }, + "settings": { + "model": "gpt-4o-2024-11-20", + "maxTokens": 16384, + "temperature": 0, + "engine": "basic-v2", + }, + "resources": [ + { + "$resourceType": "tool", + "id": "cst-0003-0000-0000-000000000001", + "name": "screen_capture", + "description": "Capture a screenshot", + "location": "external", + "type": "ClientSide", + "inputSchema": { + "type": "object", + "properties": { + "region": {"type": "string"}, + }, + }, + "outputSchema": { + "type": "object", + "properties": { + "imageBase64": { + "type": "string", + "description": "Base64-encoded image", + } + }, + "required": ["imageBase64"], + }, + "properties": {}, + "isEnabled": True, + } + ], + "features": [], + } + + config: AgentDefinition = TypeAdapter(AgentDefinition).validate_python( + json_data + ) + + tool = config.resources[0] + assert isinstance(tool, AgentClientSideToolResourceConfig) + + # Access via Python attribute name (snake_case) + assert tool.output_schema is not None + assert tool.output_schema["type"] == "object" + assert "imageBase64" in tool.output_schema["properties"] + assert tool.output_schema["required"] == ["imageBase64"] diff --git a/packages/uipath/tests/cli/chat/test_bridge.py b/packages/uipath/tests/cli/chat/test_bridge.py index 2da4f31ad..bbd385def 100644 --- a/packages/uipath/tests/cli/chat/test_bridge.py +++ b/packages/uipath/tests/cli/chat/test_bridge.py @@ -1,5 +1,6 @@ """Tests for SocketIOChatBridge and get_chat_bridge.""" +import asyncio import logging from datetime import datetime from typing import Any, cast @@ -9,6 +10,7 @@ from uipath._cli._chat._bridge import SocketIOChatBridge, get_chat_bridge from uipath._cli._debug._bridge import SignalRDebugBridge +from uipath.core.triggers import UiPathApiTrigger, UiPathResumeTrigger class MockRuntimeContext: @@ -351,3 +353,254 @@ async def test_send_with_datetime_does_not_raise(self) -> None: assert parsed_data["message"] == "test message" assert isinstance(parsed_data["timestamp"], str) assert isinstance(parsed_data["nested"]["created_at"], str) + + +class TestEmitInterruptEvent: + """Tests for emit_interrupt_event (now a no-op for executingToolCall).""" + + def _make_bridge(self) -> SocketIOChatBridge: + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + ) + bridge._current_message_id = "msg-100" + return bridge + + @pytest.mark.anyio + async def test_emit_interrupt_event_is_noop(self) -> None: + """emit_interrupt_event no longer emits executingToolCall.""" + bridge = self._make_bridge() + + emitted_events: list[Any] = [] + + async def capture_emit(event: Any) -> None: + emitted_events.append(event) + + bridge.emit_message_event = capture_emit # type: ignore[assignment] + + trigger = UiPathResumeTrigger( + api_resume=UiPathApiTrigger( + request={ + "tool_call_id": "tc-42", + "tool_name": "my_tool", + "input": {"key": "value"}, + } + ) + ) + + await bridge.emit_interrupt_event(trigger) + + assert len(emitted_events) == 0 + + +class TestEmitExecutingToolCall: + """Tests for emit_executing_tool_call_event (post-confirmation executingToolCall emission).""" + + def _make_bridge(self) -> SocketIOChatBridge: + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + ) + bridge._current_message_id = "msg-100" + return bridge + + @pytest.mark.anyio + async def test_emits_executing_tool_call_event( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Should emit executingToolCall with tool_call_id and input.""" + monkeypatch.setenv("CAS_WEBSOCKET_DISABLED", "true") + bridge = self._make_bridge() + await bridge.connect() + + emitted_events: list[Any] = [] + original_emit = bridge.emit_message_event + + async def capture_emit(event: Any) -> None: + emitted_events.append(event) + await original_emit(event) + + bridge.emit_message_event = capture_emit # type: ignore[assignment] + + await bridge.emit_executing_tool_call_event( + tool_call_id="tc-42", + tool_input={"key": "value"}, + ) + + assert len(emitted_events) == 1 + event = emitted_events[0] + assert event.message_id == "msg-100" + assert event.tool_call is not None + assert event.tool_call.tool_call_id == "tc-42" + assert event.tool_call.executing is not None + assert event.tool_call.executing.input == {"key": "value"} + + @pytest.mark.anyio + async def test_no_message_id_does_not_emit(self) -> None: + """Should not emit if no current message ID is set.""" + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + ) + # _current_message_id is not set + + emitted_events: list[Any] = [] + + async def capture_emit(event: Any) -> None: + emitted_events.append(event) + + bridge.emit_message_event = capture_emit # type: ignore[assignment] + + await bridge.emit_executing_tool_call_event(tool_call_id="tc-42") + + assert len(emitted_events) == 0 + + @pytest.mark.anyio + async def test_none_input_emits_with_none( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Should emit with None input when no input provided.""" + monkeypatch.setenv("CAS_WEBSOCKET_DISABLED", "true") + bridge = self._make_bridge() + await bridge.connect() + + emitted_events: list[Any] = [] + original_emit = bridge.emit_message_event + + async def capture_emit(event: Any) -> None: + emitted_events.append(event) + await original_emit(event) + + bridge.emit_message_event = capture_emit # type: ignore[assignment] + + await bridge.emit_executing_tool_call_event(tool_call_id="tc-42") + + assert len(emitted_events) == 1 + assert emitted_events[0].tool_call.executing.input is None + + +class TestWaitForResumeEndToolCall: + """Tests for wait_for_resume unblocking on endToolCall events.""" + + @pytest.mark.anyio + async def test_end_tool_call_unblocks_wait_for_resume(self) -> None: + """Receiving an endToolCall event unblocks wait_for_resume and returns parsed payload.""" + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + ) + + end_event = { + "conversationId": "conv-123", + "exchange": { + "exchangeId": "exch-456", + "message": { + "messageId": "msg-200", + "toolCall": { + "toolCallId": "tc-99", + "endToolCall": { + "output": {"result": "ok"}, + "isError": False, + }, + }, + }, + }, + } + + async def simulate_end_event() -> None: + await asyncio.sleep(0.05) + await bridge._handle_conversation_event(end_event, "sid-1") + + task = asyncio.create_task(simulate_end_event()) + result = await bridge.wait_for_resume() + await task + + assert result["output"] == {"result": "ok"} + assert result["is_error"] is False + + @pytest.mark.anyio + async def test_confirm_tool_call_unblocks_wait_for_resume(self) -> None: + """Receiving a confirmToolCall event also unblocks wait_for_resume.""" + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + ) + + confirm_event = { + "conversationId": "conv-123", + "exchange": { + "exchangeId": "exch-456", + "message": { + "messageId": "msg-200", + "toolCall": { + "toolCallId": "tc-99", + "confirmToolCall": { + "approved": True, + "input": {"edited": "data"}, + }, + }, + }, + }, + } + + async def simulate_confirm_event() -> None: + await asyncio.sleep(0.05) + await bridge._handle_conversation_event(confirm_event, "sid-1") + + task = asyncio.create_task(simulate_confirm_event()) + result = await bridge.wait_for_resume() + await task + + assert result["approved"] is True + assert result["input"] == {"edited": "data"} + + @pytest.mark.anyio + async def test_early_end_tool_call_is_not_lost(self) -> None: + """An endToolCall that arrives before wait_for_resume is called must not be lost.""" + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + ) + + end_event = { + "conversationId": "conv-123", + "exchange": { + "exchangeId": "exch-456", + "message": { + "messageId": "msg-300", + "toolCall": { + "toolCallId": "tc-100", + "endToolCall": { + "output": {"early": True}, + "isError": False, + }, + }, + }, + }, + } + + # Simulate the event arriving BEFORE wait_for_resume is called + await bridge._handle_conversation_event(end_event, "sid-1") + + result = await bridge.wait_for_resume() + + assert result["output"] == {"early": True} + assert result["is_error"] is False diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index b319fa1ba..3cf75bd40 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.72" +version = "2.10.73" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.16" +version = "0.5.17" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 825faa64d14081cce1b2eee4274824f45f217264 Mon Sep 17 00:00:00 2001 From: Norman <82416746+norman-le@users.noreply.github.com> Date: Fri, 29 May 2026 12:17:26 -0400 Subject: [PATCH 080/121] chore: update ver (#1693) --- packages/uipath/pyproject.toml | 6 +++--- packages/uipath/uv.lock | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index a7a978f7c..844177cc5 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,12 +1,12 @@ [project] name = "uipath" -version = "2.10.73" +version = "2.10.74" 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" dependencies = [ - "uipath-core>=0.5.8, <0.6.0", - "uipath-runtime>=0.10.1, <0.11.0", + "uipath-core>=0.5.17, <0.6.0", + "uipath-runtime>=0.11.0, <0.12.0", "uipath-platform>=0.1.59, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 3cf75bd40..71ce494d4 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-25T20:56:31.043599Z" +exclude-newer = "2026-05-27T15:26:21.545236Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.73" +version = "2.10.74" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2625,7 +2625,7 @@ requires-dist = [ { name = "truststore", specifier = ">=0.10.1" }, { name = "uipath-core", editable = "../uipath-core" }, { name = "uipath-platform", editable = "../uipath-platform" }, - { name = "uipath-runtime", specifier = ">=0.10.1,<0.11.0" }, + { name = "uipath-runtime", specifier = ">=0.11.0,<0.12.0" }, ] [package.metadata.requires-dev] @@ -2729,14 +2729,14 @@ dev = [ [[package]] name = "uipath-runtime" -version = "0.10.1" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/87/2e625219b3364a7153549e6056bce41d2050725ed0844f2711c414a872c0/uipath_runtime-0.10.1.tar.gz", hash = "sha256:9ed1bdb6737ad64cc5bb7ef0c8466dbae8ca010858ecd856818396ea264eb3d5", size = 141189, upload-time = "2026-04-23T11:34:53.102Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/8d/4d36d6a5dda4ca5f25e52508bc20dd82cb92fcdf2a36cd0adc4f9832d047/uipath_runtime-0.11.0.tar.gz", hash = "sha256:cc94f2fdab43b593ef678eff904fc6cdd4831963cffe39a83909ffcf9082d76f", size = 143685, upload-time = "2026-05-29T15:13:30.562Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/41/bc3465ee89dd01f8a9045d7d22d0f0927c0d437242eeded8d3d5b33f50ed/uipath_runtime-0.10.1-py3-none-any.whl", hash = "sha256:f04483db92ee7683513762a79bf48c229c7133d5adc7fef10ea5eaa4c7ce9b29", size = 43057, upload-time = "2026-04-23T11:34:51.781Z" }, + { url = "https://files.pythonhosted.org/packages/e7/08/c7b90851d4544ff5e76ca7c55452597aae1619cf1ebc2c0aa7b098110f14/uipath_runtime-0.11.0-py3-none-any.whl", hash = "sha256:08bf53a0e38bb3d19edc6708d2ecb7d918aa96fdda13e35f3ad0e6f2a6c392b9", size = 43770, upload-time = "2026-05-29T15:13:29.282Z" }, ] [[package]] From 644a45b566708c49a35819a52a338bc76e19781a Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 2 Jun 2026 11:26:34 +0300 Subject: [PATCH 081/121] ci: require min-version bumps for co-changed internal packages (#1694) --- .../scripts/check_dependency_version_bumps.py | 189 ++++++++++++++++++ .../test_check_dependency_version_bumps.py | 147 ++++++++++++++ .github/workflows/check-dependency-bumps.yml | 28 +++ .github/workflows/ci.yml | 4 + .github/workflows/test-cd-scripts.yml | 5 +- 5 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/check_dependency_version_bumps.py create mode 100644 .github/scripts/test_check_dependency_version_bumps.py create mode 100644 .github/workflows/check-dependency-bumps.yml diff --git a/.github/scripts/check_dependency_version_bumps.py b/.github/scripts/check_dependency_version_bumps.py new file mode 100644 index 000000000..fef305dea --- /dev/null +++ b/.github/scripts/check_dependency_version_bumps.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Enforce minimum-version bumps between co-changed internal packages. + +The monorepo ships several packages that depend on one another +(``uipath`` -> ``uipath-platform`` -> ``uipath-core``). When a PR changes +the *source* of a dependency package (say ``uipath-core``) **and** the +source of one of its dependents (say ``uipath``), the dependent is almost +certainly relying on the new behaviour. If the dependent does not also +raise the lower bound of its requirement on the dependency, then anyone who +installs the dependent on its own can resolve an older dependency that +predates the new behaviour — a silent runtime break. + +This check fails such a PR. For every pair of co-changed (dependency, +dependent) packages it requires the dependent's lower-bound constraint on +the dependency (the ``>=`` part of e.g. ``uipath-core>=0.5.8, <0.6.0``) to +be at least the dependency's new version declared in this PR. + +The internal dependency graph is discovered from the pyproject files, so no +hard-coded list needs maintaining as packages are added. +""" + +import re +import sys +from pathlib import Path +from typing import TypedDict + +from check_version_uniqueness import get_changed_packages + +try: + import tomllib +except ModuleNotFoundError: + import tomli as tomllib # type: ignore[no-redef] + +PACKAGES_DIR = Path("packages") + + +class PackageInfo(TypedDict): + """Resolved metadata for a single monorepo package.""" + + dir: str + name: str + version: str + dependencies: list[str] + + +def normalize_name(name: str) -> str: + """Normalize a PyPI project name (PEP 503): case-insensitive, -/_/. + treated as equivalent.""" + return re.sub(r"[-_.]+", "-", name).lower() + + +def version_key(version: str) -> tuple[int, ...]: + """Numeric sort key so ``0.5.17`` > ``0.5.8`` (``0.5.18rc1`` -> ``(0, 5, 18)``).""" + parts: list[int] = [] + for component in version.split("."): + digits = "" + for ch in component: + if ch.isdigit(): + digits += ch + else: + break + parts.append(int(digits) if digits else 0) + return tuple(parts) + + +def parse_requirement(requirement: str) -> tuple[str | None, str | None]: + """Extract (normalized name, lower-bound version) from a requirement string. + + Returns the lower bound found in a ``>=`` clause, or ``None`` if there is + no ``>=`` constraint. The name is ``None`` if the string is unparseable. + """ + name_match = re.match(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)", requirement) + if not name_match: + return None, None + name = normalize_name(name_match.group(1)) + + lower: str | None = None + lower_match = re.search(r">=\s*([0-9][0-9A-Za-z._-]*)", requirement) + if lower_match: + lower = lower_match.group(1) + return name, lower + + +def load_package(package_dir: str) -> PackageInfo | None: + """Read a package's name, version and dependency list from pyproject.toml.""" + pyproject = PACKAGES_DIR / package_dir / "pyproject.toml" + if not pyproject.exists(): + return None + with open(pyproject, "rb") as f: + data = tomllib.load(f) + project = data.get("project", {}) + name = project.get("name") + version = project.get("version") + if not name or not version: + return None + return PackageInfo( + dir=package_dir, + name=name, + version=version, + dependencies=list(project.get("dependencies", [])), + ) + + +def get_all_packages() -> dict[str, PackageInfo]: + """Map package directory name -> package info for every package.""" + packages: dict[str, PackageInfo] = {} + if not PACKAGES_DIR.is_dir(): + return packages + for item in sorted(PACKAGES_DIR.iterdir()): + if item.is_dir() and (item / "pyproject.toml").exists(): + info = load_package(item.name) + if info: + packages[item.name] = info + return packages + + +def check(packages: dict[str, PackageInfo], changed: set[str]) -> list[str]: + """Return a list of violation messages (empty when the PR is compliant).""" + name_to_dir: dict[str, str] = { + normalize_name(info["name"]): pkg_dir for pkg_dir, info in packages.items() + } + + violations: list[str] = [] + for dependent_dir in sorted(changed): + dependent = packages.get(dependent_dir) + if not dependent: + continue + + for requirement in dependent["dependencies"]: + dep_name, lower = parse_requirement(requirement) + if dep_name is None: + continue + + dep_dir = name_to_dir.get(dep_name) + # Only internal packages that *also* changed in this PR are in scope. + if dep_dir is None or dep_dir == dependent_dir or dep_dir not in changed: + continue + + dep_version = packages[dep_dir]["version"] + dep_display = packages[dep_dir]["name"] + + if lower is None: + violations.append( + f"{dependent['name']} requires '{requirement}' but has no '>=' lower bound on " + f"{dep_display}; pin it to >={dep_version} (both packages changed in this PR)." + ) + elif version_key(lower) < version_key(dep_version): + violations.append( + f"{dependent['name']} pins {dep_display}>={lower}, but {dep_display} was bumped to " + f"{dep_version} in this PR. Raise the minimum to >={dep_version}." + ) + else: + print(f"OK: {dependent['name']} requires {dep_display}>={lower} (>= new {dep_version})") + + return violations + + +def main() -> int: + packages = get_all_packages() + if not packages: + print("No packages found.") + return 0 + + changed = set(get_changed_packages()) + if not changed: + print("No source changes to internal packages detected.") + return 0 + + print(f"Changed packages: {', '.join(sorted(changed))}") + + violations = check(packages, changed) + if violations: + print("\nDependency version bump check FAILED:\n", file=sys.stderr) + for v in violations: + print(f" - {v}", file=sys.stderr) + print( + "\nWhen you change an internal package and a dependent of it in the same PR, " + "the dependent must require the dependency's new version so a standalone install " + "cannot resolve an older, incompatible release.", + file=sys.stderr, + ) + return 1 + + print("\nAll co-changed internal dependencies have an up-to-date minimum version.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/.github/scripts/test_check_dependency_version_bumps.py b/.github/scripts/test_check_dependency_version_bumps.py new file mode 100644 index 000000000..e4135e89f --- /dev/null +++ b/.github/scripts/test_check_dependency_version_bumps.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Tests for check_dependency_version_bumps.py.""" + +from unittest import mock + +from check_dependency_version_bumps import ( + PackageInfo, + check, + normalize_name, + parse_requirement, + version_key, +) + + +def pkg(name: str, version: str, dependencies: list[str] | None = None) -> PackageInfo: + return PackageInfo( + dir=name, + name=name, + version=version, + dependencies=dependencies or [], + ) + + +class TestVersionKey: + def test_numeric_components(self): + assert version_key("0.5.18") == (0, 5, 18) + + def test_compares_numerically_not_lexically(self): + # The trap a string compare would fall into: "0.5.8" > "0.5.17". + assert version_key("0.5.17") > version_key("0.5.8") + + def test_strips_prerelease_suffix(self): + assert version_key("0.5.18rc1") == (0, 5, 18) + + +class TestNormalizeName: + def test_case_and_separators_equivalent(self): + assert normalize_name("UiPath_Core") == normalize_name("uipath-core") + assert normalize_name("uipath.core") == "uipath-core" + + +class TestParseRequirement: + def test_extracts_name_and_lower_bound(self): + assert parse_requirement("uipath-core>=0.5.8, <0.6.0") == ("uipath-core", "0.5.8") + + def test_no_lower_bound(self): + assert parse_requirement("click") == ("click", None) + assert parse_requirement("httpx<1.0") == ("httpx", None) + + def test_whitespace_after_operator(self): + assert parse_requirement("uipath-core >= 0.5.8") == ("uipath-core", "0.5.8") + + +class TestCheck: + def _packages(self) -> dict[str, PackageInfo]: + return { + "uipath-core": pkg("uipath-core", "0.5.18"), + "uipath-platform": pkg( + "uipath-platform", "0.1.60", ["uipath-core>=0.5.8, <0.6.0"] + ), + "uipath": pkg( + "uipath", + "2.10.74", + [ + "uipath-core>=0.5.8, <0.6.0", + "uipath-platform>=0.1.59, <0.2.0", + "click>=8.3.1", + ], + ), + } + + def test_passes_when_only_dependency_changed(self): + # uipath-core changed alone -> dependents not touched, nothing to enforce. + assert check(self._packages(), {"uipath-core"}) == [] + + def test_passes_when_only_dependent_changed(self): + assert check(self._packages(), {"uipath"}) == [] + + def test_fails_when_co_changed_without_min_bump(self): + # uipath-core bumped to 0.5.18 but uipath still pins >=0.5.8. + violations = check(self._packages(), {"uipath-core", "uipath"}) + assert len(violations) == 1 + assert "uipath" in violations[0] + assert "0.5.18" in violations[0] + + def test_passes_when_min_raised_to_new_version(self): + packages = self._packages() + packages["uipath"]["dependencies"] = [ + "uipath-core>=0.5.18, <0.6.0", + "click>=8.3.1", + ] + assert check(packages, {"uipath-core", "uipath"}) == [] + + def test_passes_when_min_already_above_new_version(self): + packages = self._packages() + packages["uipath"]["dependencies"] = ["uipath-core>=0.6.0, <0.7.0"] + assert check(packages, {"uipath-core", "uipath"}) == [] + + def test_fails_when_no_lower_bound_on_co_changed_dep(self): + packages = self._packages() + packages["uipath"]["dependencies"] = ["uipath-core"] + violations = check(packages, {"uipath-core", "uipath"}) + assert len(violations) == 1 + assert "no '>=' lower bound" in violations[0] + + def test_external_dependencies_are_ignored(self): + # click is not an internal package, so it is never in scope. + assert check(self._packages(), {"uipath"}) == [] + + def test_transitive_chain_each_edge_enforced(self): + # All three changed: uipath must bump core AND platform; platform must bump core. + packages = self._packages() + violations = check(packages, {"uipath-core", "uipath-platform", "uipath"}) + # uipath->core (stale), uipath->platform (0.1.59 < 0.1.60), platform->core (stale) + assert len(violations) == 3 + + def test_no_self_reference(self): + packages = {"uipath": pkg("uipath", "2.0.0", ["uipath>=1.0.0"])} + assert check(packages, {"uipath"}) == [] + + +class TestMain: + def _run(self, packages: dict[str, PackageInfo], changed: list[str]) -> int: + from check_dependency_version_bumps import main + + with ( + mock.patch("check_dependency_version_bumps.get_all_packages", return_value=packages), + mock.patch("check_dependency_version_bumps.get_changed_packages", return_value=changed), + ): + return main() + + def test_returns_zero_when_compliant(self): + packages = { + "uipath-core": pkg("uipath-core", "0.5.18"), + "uipath": pkg("uipath", "2.0.0", ["uipath-core>=0.5.18, <0.6.0"]), + } + assert self._run(packages, ["uipath-core", "uipath"]) == 0 + + def test_returns_one_on_violation(self): + packages = { + "uipath-core": pkg("uipath-core", "0.5.18"), + "uipath": pkg("uipath", "2.0.0", ["uipath-core>=0.5.8, <0.6.0"]), + } + assert self._run(packages, ["uipath-core", "uipath"]) == 1 + + def test_returns_zero_when_no_changes(self): + assert self._run({"uipath-core": pkg("uipath-core", "0.5.18")}, []) == 0 \ No newline at end of file diff --git a/.github/workflows/check-dependency-bumps.yml b/.github/workflows/check-dependency-bumps.yml new file mode 100644 index 000000000..acdff63a6 --- /dev/null +++ b/.github/workflows/check-dependency-bumps.yml @@ -0,0 +1,28 @@ +name: Check Dependency Version Bumps + +on: + workflow_call: + +permissions: + contents: read + +jobs: + check-dependency-bumps: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Enforce min-version bumps for co-changed internal packages + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: python .github/scripts/check_dependency_version_bumps.py \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8be2b6b53..1dd0471f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,3 +27,7 @@ jobs: check-versions: if: github.event_name == 'pull_request' uses: ./.github/workflows/check-version-availability.yml + + check-dependency-bumps: + if: github.event_name == 'pull_request' + uses: ./.github/workflows/check-dependency-bumps.yml diff --git a/.github/workflows/test-cd-scripts.yml b/.github/workflows/test-cd-scripts.yml index a2f1bcdf1..3c6655a9d 100644 --- a/.github/workflows/test-cd-scripts.yml +++ b/.github/workflows/test-cd-scripts.yml @@ -9,8 +9,11 @@ on: - '.github/scripts/test_detect_publishable_packages.py' - '.github/scripts/check_version_uniqueness.py' - '.github/scripts/test_check_version_uniqueness.py' + - '.github/scripts/check_dependency_version_bumps.py' + - '.github/scripts/test_check_dependency_version_bumps.py' - '.github/workflows/cd.yml' - '.github/workflows/check-version-availability.yml' + - '.github/workflows/check-dependency-bumps.yml' permissions: contents: read @@ -32,4 +35,4 @@ jobs: - name: Run tests working-directory: .github/scripts - run: python -m pytest test_detect_publishable_packages.py test_check_version_uniqueness.py -v + run: python -m pytest test_detect_publishable_packages.py test_check_version_uniqueness.py test_check_dependency_version_bumps.py -v From 636d01be531031fae2a91f93094fbc472727476f Mon Sep 17 00:00:00 2001 From: Amey Date: Tue, 2 Jun 2026 10:00:17 -0400 Subject: [PATCH 082/121] feat: emit RunSource on eval telemetry from UIPATH_EVAL_RUN_SOURCE [AE-1329] (#1697) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/_evals/_telemetry.py | 10 ++++++++++ packages/uipath/tests/cli/eval/test_eval_telemetry.py | 4 ++++ packages/uipath/uv.lock | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 844177cc5..72b6e5c56 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.74" +version = "2.10.75" 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/_evals/_telemetry.py b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py index e28186576..04cb7e2c4 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py @@ -329,5 +329,15 @@ def _enrich_properties(self, properties: dict[str, Any]) -> None: if tenant_id: properties["TenantId"] = tenant_id + # Origin of the eval-set run as classified by the caller (e.g. Manual, + # Protegi, FirstSuccessfulRun). The Agents backend forwards the value + # via UIPATH_EVAL_RUN_SOURCE so adoption dashboards can exclude + # auto-triggered runs (e.g. first-successful-run) from user-driven counts. + # Distinct from the `Source` dimension below, which categorises the SDK + # emitter ("uipath-python-cli"), not the run origin. + run_source = os.getenv("UIPATH_EVAL_RUN_SOURCE") + if run_source: + properties["RunSource"] = run_source + properties["Source"] = "uipath-python-cli" properties["ApplicationName"] = "UiPath.Eval" diff --git a/packages/uipath/tests/cli/eval/test_eval_telemetry.py b/packages/uipath/tests/cli/eval/test_eval_telemetry.py index fdfe7135c..48911638a 100644 --- a/packages/uipath/tests/cli/eval/test_eval_telemetry.py +++ b/packages/uipath/tests/cli/eval/test_eval_telemetry.py @@ -431,6 +431,7 @@ def test_enrich_properties_adds_env_vars(self, mock_get_claim): "UIPATH_PROJECT_ID": "project-123", "UIPATH_ORGANIZATION_ID": "org-456", "UIPATH_TENANT_ID": "tenant-abc", + "UIPATH_EVAL_RUN_SOURCE": "FirstSuccessfulRun", }, ): subscriber._enrich_properties(properties) @@ -440,6 +441,7 @@ def test_enrich_properties_adds_env_vars(self, mock_get_claim): assert properties["CloudOrganizationId"] == "org-456" assert properties["CloudUserId"] == "user-789" assert properties["TenantId"] == "tenant-abc" + assert properties["RunSource"] == "FirstSuccessfulRun" @patch("uipath._cli._evals._telemetry.get_claim_from_token") def test_enrich_properties_skips_missing_env_vars(self, mock_get_claim): @@ -455,6 +457,7 @@ def test_enrich_properties_skips_missing_env_vars(self, mock_get_claim): "UIPATH_PROJECT_ID", "UIPATH_ORGANIZATION_ID", "UIPATH_TENANT_ID", + "UIPATH_EVAL_RUN_SOURCE", ]: os.environ.pop(key, None) @@ -465,6 +468,7 @@ def test_enrich_properties_skips_missing_env_vars(self, mock_get_claim): assert "CloudOrganizationId" not in properties assert "CloudUserId" not in properties assert "TenantId" not in properties + assert "RunSource" not in properties class TestExceptionHandling: diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 71ce494d4..b4399ef58 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.74" +version = "2.10.75" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From fcd0c94204150d36142294fb267817f545245417 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 2 Jun 2026 17:18:11 +0300 Subject: [PATCH 083/121] docs: fix pack default extensions list and leading dot example (#1699) --- packages/uipath/docs/cli/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index 165c5431a..2d8d1d814 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -198,6 +198,7 @@ By default, the following file types are included in the `.nupkg` file: - `.json` - `.yaml` - `.yml` +- `.md` --- @@ -212,7 +213,7 @@ To include additional files, update the `uipath.json` file by adding a `packOpti "" ], "fileExtensionsIncluded": [ - "" + "" ] } } From bf200a3a57ed2aaf3bdcdd4c461d3351c77c90bc Mon Sep 17 00:00:00 2001 From: chetanyauipath <107464888+chetanyauipath@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:32:58 +0530 Subject: [PATCH 084/121] feat(tasks): add QuickForm task creation SDK + agent.json model (#1683) Co-authored-by: Claude Opus 4.7 --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/action_center/_tasks_service.py | 278 ++++++++++++++++-- .../tests/services/test_actions_service.py | 145 +++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- .../uipath/src/uipath/agent/models/agent.py | 21 ++ packages/uipath/uv.lock | 4 +- 7 files changed, 421 insertions(+), 35 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 215882460..dfc759f4d 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.59" +version = "0.1.60" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index 662109ce4..491b1bd73 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -118,10 +118,34 @@ def _create_spec( ), } + _apply_priority_labels_and_actionable_toggle( + json_payload, priority, labels, is_actionable_message_enabled + ) + _apply_task_source(json_payload, source_name) + + return RequestSpec( + method="POST", + endpoint=Endpoint("/orchestrator_/tasks/AppTasks/CreateAppTask"), + json=json_payload, + headers=header_folder(app_folder_key, app_folder_path), + ) + + +def _apply_priority_labels_and_actionable_toggle( + payload: Dict[str, Any], + priority: Optional[str], + labels: Optional[List[str]], + is_actionable_message_enabled: Optional[bool], +) -> None: + """Apply priority / tags / isActionableMessageEnabled to ``payload`` in-place. + + Shared between AppTask and QuickForm spec builders — they handle these three + optional fields identically. + """ if priority and (normalized_priority := _normalize_priority(priority)): - json_payload["priority"] = normalized_priority + payload["priority"] = normalized_priority if labels is not None: - json_payload["tags"] = [ + payload["tags"] = [ { "name": label, "displayName": label, @@ -131,37 +155,29 @@ def _create_spec( for label in labels ] if is_actionable_message_enabled is not None: - json_payload["isActionableMessageEnabled"] = is_actionable_message_enabled + payload["isActionableMessageEnabled"] = is_actionable_message_enabled - project_id = UiPathConfig.project_id - trace_id = UiPathConfig.trace_id - if project_id and trace_id: - folder_key = UiPathConfig.folder_key - job_key = UiPathConfig.job_key - process_key = UiPathConfig.process_uuid +def _apply_task_source(payload: Dict[str, Any], source_name: str) -> None: + """Populate ``payload["taskSource"]`` when UiPathConfig has project_id + trace_id. - task_source_metadata: Dict[str, Any] = { + Shared between AppTask and QuickForm spec builders — the taskSource block is + identical for both task types. + """ + project_id = UiPathConfig.project_id + trace_id = UiPathConfig.trace_id + if not (project_id and trace_id): + return + payload["taskSource"] = { + "sourceName": source_name, + "sourceId": project_id, + "taskSourceMetadata": { "InstanceId": trace_id, - "FolderKey": folder_key, - "JobKey": job_key, - "ProcessKey": process_key, - } - - task_source = { - "sourceName": source_name, - "sourceId": project_id, - "taskSourceMetadata": task_source_metadata, - } - - json_payload["taskSource"] = task_source - - return RequestSpec( - method="POST", - endpoint=Endpoint("/orchestrator_/tasks/AppTasks/CreateAppTask"), - json=json_payload, - headers=header_folder(app_folder_key, app_folder_path), - ) + "FolderKey": UiPathConfig.folder_key, + "JobKey": UiPathConfig.job_key, + "ProcessKey": UiPathConfig.process_uuid, + }, + } def _normalize_priority(priority: str | None) -> str | None: @@ -196,6 +212,62 @@ def _normalize_priority(priority: str | None) -> str | None: return normalized +_TASK_TYPE_QUICKFORM = 6 + + +def _create_quickform_spec( + data: Optional[Dict[str, Any]], + title: str, + task_schema_key: str, + schema: Dict[str, Any], + creator_job_key: Optional[str] = None, + folder_key: Optional[str] = None, + folder_path: Optional[str] = None, + priority: Optional[str] = None, + labels: Optional[List[str]] = None, + is_actionable_message_enabled: Optional[bool] = None, + actionable_message_metadata: Optional[Dict[str, Any]] = None, + source_name: str = "Agent", +) -> RequestSpec: + """Build the RequestSpec for Orchestrator's GenericTasks/CreateTask endpoint. + + Sets TaskType=QuickFormTask. Mirrors _create_spec but skips the AppTask-specific + shape (no appId, no action-schema-derived fieldSet/actionSet) and instead sends + taskSchemaKey + inline schema together. + + Both taskSchemaKey AND schema are sent on every call: the Agents runtime has no + Action Center package.uploaded subscriber populating the TaskSchemas table, so + Orchestrator upserts the schema (keyed by taskSchemaKey) and then creates the task + in the same call. + + Wire contract: UiPath/Orchestrator/src/Core/Application/Dto/Tasks/TaskCreateRequest.cs. + """ + json_payload: Dict[str, Any] = { + "type": _TASK_TYPE_QUICKFORM, + "taskSchemaKey": task_schema_key, + "schema": schema, + "title": title, + "data": data if data is not None else {}, + } + + if creator_job_key is not None: + json_payload["creatorJobKey"] = creator_job_key + + _apply_priority_labels_and_actionable_toggle( + json_payload, priority, labels, is_actionable_message_enabled + ) + if actionable_message_metadata is not None: + json_payload["actionableMessageMetaData"] = actionable_message_metadata + _apply_task_source(json_payload, source_name) + + return RequestSpec( + method="POST", + endpoint=Endpoint("/orchestrator_/tasks/GenericTasks/CreateTask"), + json=json_payload, + headers=header_folder(folder_key, folder_path), + ) + + def _retrieve_action_spec( action_key: str, app_folder_key: Optional[str], @@ -506,6 +578,154 @@ def create( ) return Task.model_validate(json_response) + @traced(name="tasks_create_quickform", run_type="uipath") + async def create_quickform_async( + self, + title: str, + task_schema_key: str, + schema: Dict[str, Any], + data: Optional[Dict[str, Any]] = None, + *, + folder_path: Optional[str] = None, + folder_key: Optional[str] = None, + assignee: Optional[str] = None, + recipient: Optional[TaskRecipient] = None, + priority: Optional[str] = None, + labels: Optional[List[str]] = None, + is_actionable_message_enabled: Optional[bool] = None, + actionable_message_metadata: Optional[Dict[str, Any]] = None, + creator_job_key: Optional[str] = None, + source_name: str = "Agent", + ) -> Task: + """Creates a new QuickForm task asynchronously. + + QuickForm tasks are schema-first HITL tasks rendered by FormLib in Action + Center. Both task_schema_key AND schema are required: the Agents runtime + does not pre-populate TaskSchemas via a package.uploaded subscriber, so + Orchestrator upserts the schema (keyed by task_schema_key) and creates + the task in the same call. + + Args: + title: The title of the task. + task_schema_key: UUID key of the schema. Used as the key under which + Orchestrator stores/looks up the schema in TaskSchemas. + schema: The HITL schema body to register/upsert. Sent inline on every + call. + data: Optional dictionary containing input data for the task. + folder_path: Optional folder path for the task. Required by the + Orchestrator controller (RequireOrganizationUnit) unless + folder_key is provided. + folder_key: Optional folder key, alternative to folder_path. + assignee: Optional username or email to assign the task to. + recipient: Optional structured recipient (user id / group id / + email). Resolved via identity service before assignment. + priority: Optional priority. Low / Medium / High / Critical. + labels: Optional list of labels for the task. + is_actionable_message_enabled: Whether actionable notifications are + enabled for this task. + actionable_message_metadata: Optional metadata override. For + QuickForm, when null, Orchestrator derives it from the + referenced TaskSchema. + creator_job_key: Optional. Identifies the job that triggered the + inline schema creation/upsert. + source_name: Source name on TaskSource. Defaults to 'Agent'. + + Returns: + Task: The created task object. + """ + spec = _create_quickform_spec( + title=title, + data=data, + task_schema_key=task_schema_key, + schema=schema, + creator_job_key=creator_job_key, + folder_key=folder_key, + folder_path=folder_path, + priority=priority, + labels=labels, + is_actionable_message_enabled=is_actionable_message_enabled, + actionable_message_metadata=actionable_message_metadata, + source_name=source_name, + ) + + response = await self.request_async( + spec.method, + spec.endpoint, + json=spec.json, + content=spec.content, + headers=spec.headers, + ) + json_response = response.json() + if assignee or recipient: + assign_spec = await _assign_task_spec( + self, json_response["id"], assignee, recipient + ) + await self.request_async( + assign_spec.method, + assign_spec.endpoint, + json=assign_spec.json, + content=assign_spec.content, + ) + return Task.model_validate(json_response) + + @traced(name="tasks_create_quickform", run_type="uipath") + def create_quickform( + self, + title: str, + task_schema_key: str, + schema: Dict[str, Any], + data: Optional[Dict[str, Any]] = None, + *, + folder_path: Optional[str] = None, + folder_key: Optional[str] = None, + assignee: Optional[str] = None, + recipient: Optional[TaskRecipient] = None, + priority: Optional[str] = None, + labels: Optional[List[str]] = None, + is_actionable_message_enabled: Optional[bool] = None, + actionable_message_metadata: Optional[Dict[str, Any]] = None, + creator_job_key: Optional[str] = None, + source_name: str = "Agent", + ) -> Task: + """Create a new QuickForm task synchronously. + + See :meth:`create_quickform_async` for parameter docs. + """ + spec = _create_quickform_spec( + title=title, + data=data, + task_schema_key=task_schema_key, + schema=schema, + creator_job_key=creator_job_key, + folder_key=folder_key, + folder_path=folder_path, + priority=priority, + labels=labels, + is_actionable_message_enabled=is_actionable_message_enabled, + actionable_message_metadata=actionable_message_metadata, + source_name=source_name, + ) + + response = self.request( + spec.method, + spec.endpoint, + json=spec.json, + content=spec.content, + headers=spec.headers, + ) + json_response = response.json() + if assignee or recipient: + assign_spec = asyncio.run( + _assign_task_spec(self, json_response["id"], assignee, recipient) + ) + self.request( + assign_spec.method, + assign_spec.endpoint, + json=assign_spec.json, + content=assign_spec.content, + ) + return Task.model_validate(json_response) + @resource_override( resource_type="app", resource_identifier="app_name", diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index b97d326e8..6758c452a 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -1,3 +1,4 @@ +import json from typing import Any import pytest @@ -555,3 +556,147 @@ def test_create_raises_when_no_folder_key_or_path_provided( app_name="my-app", app_folder_path=None, ) + + +# --------------------------------------------------------------------------- +# QuickForm task tests +# --------------------------------------------------------------------------- + +_QF_SCHEMA: dict[str, Any] = { + "id": "7ebef452-fee9-45df-8fc2-01f1d0248540", + "fields": [ + {"id": "f1", "type": "text", "label": "F1", "direction": "input"}, + {"id": "f2", "type": "text", "label": "F2", "direction": "output"}, + ], + "outcomes": [ + {"id": "approve", "name": "Approve", "type": "string", "isPrimary": True}, + ], +} +_QF_DEFAULTS = { + "title": "QF task", + "task_schema_key": _QF_SCHEMA["id"], + "schema": _QF_SCHEMA, +} +_QF_CREATE_RESPONSE = {"id": 42, "title": _QF_DEFAULTS["title"]} + + +@pytest.fixture +def qf_create_url(base_url: str, org: str, tenant: str) -> str: + return f"{base_url}{org}{tenant}/orchestrator_/tasks/GenericTasks/CreateTask" + + +@pytest.fixture +def qf_assign_url(base_url: str, org: str, tenant: str) -> str: + return ( + f"{base_url}{org}{tenant}" + "/orchestrator_/odata/Tasks/UiPath.Server.Configuration.OData.AssignTasks" + ) + + +def _posted_body(httpx_mock: HTTPXMock, url: str) -> dict[str, Any]: + for req in httpx_mock.get_requests(): + if str(req.url) == url: + return json.loads(req.content) + raise AssertionError(f"no request was POSTed to {url}") + + +@pytest.fixture +def qf_runner(httpx_mock: HTTPXMock, service: TasksService, qf_create_url: str) -> Any: + """Factory: stub the QF endpoint, call create_quickform with overrides, + return (task, posted_body). One call per test eliminates setup duplication. + """ + httpx_mock.add_response( + url=qf_create_url, status_code=200, json=_QF_CREATE_RESPONSE + ) + + def _run(**overrides: Any) -> tuple[Task, dict[str, Any]]: + task = service.create_quickform(**{**_QF_DEFAULTS, **overrides}) + return task, _posted_body(httpx_mock, qf_create_url) + + return _run + + +@pytest.fixture +def qf_runner_async( + httpx_mock: HTTPXMock, service: TasksService, qf_create_url: str +) -> Any: + """Async variant of qf_runner.""" + httpx_mock.add_response( + url=qf_create_url, status_code=200, json=_QF_CREATE_RESPONSE + ) + + async def _run(**overrides: Any) -> tuple[Task, dict[str, Any]]: + task = await service.create_quickform_async(**{**_QF_DEFAULTS, **overrides}) + return task, _posted_body(httpx_mock, qf_create_url) + + return _run + + +def test_create_quickform_baseline_payload(qf_runner: Any) -> None: + task, body = qf_runner() + assert body == { + "type": 6, + "taskSchemaKey": _QF_DEFAULTS["task_schema_key"], + "schema": _QF_SCHEMA, + "title": _QF_DEFAULTS["title"], + "data": {}, + } + assert isinstance(task, Task) + assert task.id == 42 + + +def test_create_quickform_data_passthrough(qf_runner: Any) -> None: + _, body = qf_runner(data={"x": 1}) + assert body["data"] == {"x": 1} + + +def test_create_quickform_includes_optional_fields_when_set(qf_runner: Any) -> None: + _, body = qf_runner( + priority="High", + labels=["a", "b"], + is_actionable_message_enabled=True, + actionable_message_metadata={"fieldSet": {}, "actionSet": {}}, + creator_job_key="3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + assert body["priority"] == "High" + assert {tag["name"] for tag in body["tags"]} == {"a", "b"} + assert body["isActionableMessageEnabled"] is True + assert body["actionableMessageMetaData"] == {"fieldSet": {}, "actionSet": {}} + assert body["creatorJobKey"] == "3fa85f64-5717-4562-b3fc-2c963f66afa6" + + +def test_create_quickform_omits_optional_fields_when_unset(qf_runner: Any) -> None: + _, body = qf_runner() + for omitted in ( + "creatorJobKey", + "priority", + "tags", + "isActionableMessageEnabled", + "actionableMessageMetaData", + ): + assert omitted not in body + + +async def test_create_quickform_async_baseline_payload(qf_runner_async: Any) -> None: + task, body = await qf_runner_async() + assert body["type"] == 6 + assert body["taskSchemaKey"] == _QF_DEFAULTS["task_schema_key"] + assert task.id == 42 + + +def test_create_quickform_with_assignee_triggers_assign_call( + httpx_mock: HTTPXMock, qf_runner: Any, qf_assign_url: str +) -> None: + httpx_mock.add_response(url=qf_assign_url, status_code=200, json={}) + qf_runner(assignee="user@example.com") + body = _posted_body(httpx_mock, qf_assign_url) + assert body["taskAssignments"][0]["UserNameOrEmail"] == "user@example.com" + + +async def test_create_quickform_async_with_assignee_triggers_assign_call( + httpx_mock: HTTPXMock, qf_runner_async: Any, qf_assign_url: str +) -> None: + httpx_mock.add_response(url=qf_assign_url, status_code=200, json={}) + await qf_runner_async(assignee="user@example.com") + body = _posted_body(httpx_mock, qf_assign_url) + assert body["taskAssignments"][0]["UserNameOrEmail"] == "user@example.com" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index dabbd63ad..084f3efb8 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.59" +version = "0.1.60" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 72b6e5c56..fae01ad9c 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.75" +version = "2.10.76" 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" dependencies = [ "uipath-core>=0.5.17, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.59, <0.2.0", + "uipath-platform>=0.1.60, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 7123a43e0..5aa959e8f 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -726,6 +726,9 @@ class AgentEscalationChannel(BaseCfg): ) priority: Optional[str] = None labels: List[str] = Field(default_factory=list) + # schema_body avoids shadowing pydantic.BaseModel.schema(); JSON alias stays "schema". + schema_id: Optional[str] = Field(None, alias="schemaId") + schema_body: Optional[Dict[str, Any]] = Field(None, alias="schema") @model_validator(mode="before") @classmethod @@ -769,6 +772,23 @@ class AgentIxpVsEscalationResourceConfig(BaseAgentResourceConfig): ) +class AgentQuickFormEscalationResourceConfig(BaseAgentResourceConfig): + """Quick Form Agent escalation resource configuration model (escalationType=2). + + Quick Form escalations render a schema-first HITL task in Action Center via FormLib. + The schema (and its key) live on the channel (see AgentEscalationChannel.schema_id / + schema) and are sent inline to Orchestrator's GenericTasks/CreateTask endpoint. + """ + + id: Optional[str] = Field(None, alias="id") + resource_type: Literal[AgentResourceType.ESCALATION] = Field( + alias="$resourceType", default=AgentResourceType.ESCALATION, frozen=True + ) + channels: List[AgentEscalationChannel] = Field(alias="channels") + is_agent_memory_enabled: bool = Field(default=False, alias="isAgentMemoryEnabled") + escalation_type: Literal[2] = Field(default=2, alias="escalationType") + + class BaseAgentToolResourceConfig(BaseAgentResourceConfig): """Base agent tool resource configuration model.""" @@ -978,6 +998,7 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig): Union[ Annotated[AgentEscalationResourceConfig, Tag(0)], Annotated[AgentIxpVsEscalationResourceConfig, Tag(1)], + Annotated[AgentQuickFormEscalationResourceConfig, Tag(2)], ], Discriminator(lambda v: v.get("escalation_type") or v.get("escalationType") or 0), ] diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index b4399ef58..96764f7fb 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.75" +version = "2.10.76" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.59" +version = "0.1.60" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 335d7bc50d1d77365740c7755507e61c52e65dde Mon Sep 17 00:00:00 2001 From: Alexandra Baduna Date: Thu, 4 Jun 2026 13:57:47 +0300 Subject: [PATCH 085/121] docs(studio-web): document coded function integration [PRODEV-605] (#1675) Co-authored-by: Claude Sonnet 4.6 --- packages/uipath/docs/FAQ.md | 8 +- packages/uipath/docs/cli/index.md | 16 +- packages/uipath/docs/core/agents.md | 245 ++++++++++ .../assets/maestro_execution_trace_light.png | Bin 0 -> 591864 bytes .../assets/maestro_service_task_light.png | Bin 0 -> 596758 bytes .../assets/orchestrator_processes_light.png | Bin 0 -> 124279 bytes .../studio_web_select_function_dark.png | Bin 0 -> 153206 bytes .../studio_web_select_function_light.png | Bin 0 -> 150036 bytes .../uipath/docs/core/environment_variables.md | 4 +- packages/uipath/docs/core/functions.md | 426 ++++++++++++++++++ packages/uipath/docs/core/getting_started.md | 4 + packages/uipath/docs/core/studio_web.md | 158 ++++++- packages/uipath/docs/core/traced.md | 42 +- packages/uipath/docs/index.md | 33 +- packages/uipath/mkdocs.yml | 6 +- 15 files changed, 892 insertions(+), 50 deletions(-) create mode 100644 packages/uipath/docs/core/agents.md create mode 100644 packages/uipath/docs/core/assets/maestro_execution_trace_light.png create mode 100644 packages/uipath/docs/core/assets/maestro_service_task_light.png create mode 100644 packages/uipath/docs/core/assets/orchestrator_processes_light.png create mode 100644 packages/uipath/docs/core/assets/studio_web_select_function_dark.png create mode 100644 packages/uipath/docs/core/assets/studio_web_select_function_light.png create mode 100644 packages/uipath/docs/core/functions.md diff --git a/packages/uipath/docs/FAQ.md b/packages/uipath/docs/FAQ.md index c1cd1aec2..602a42196 100644 --- a/packages/uipath/docs/FAQ.md +++ b/packages/uipath/docs/FAQ.md @@ -1,6 +1,6 @@ # Frequently Asked Questions (FAQ) -### Q: Why am I getting a "Failed to prepare environment" error when deploying my python agent to UiPath Cloud Platform? +### Q: Why am I getting a "Failed to prepare environment" error when deploying my Python project to UiPath Cloud Platform? #### Error Message @@ -30,7 +30,7 @@ #### Description -This error might occur when deploying coded-agents to UiPath Cloud Platform, even though the same project might work correctly in your local environment. The issue is often related to how Python packages are discovered and distributed during the cloud deployment process. +This error might occur when deploying coded functions or coded agents to UiPath Cloud Platform, even though the same project might work correctly in your local environment. The issue is often related to how Python packages are discovered and distributed during the cloud deployment process. #### Common Causes @@ -283,7 +283,7 @@ If you encounter SSL certificate errors: //// -### Q: Why are my agent runs hanging on UiPath Cloud Platform? +### Q: Why are my job runs hanging on UiPath Cloud Platform? #### Error Message @@ -298,7 +298,7 @@ You may see errors like these in the logs panel: #### Description -If your Python agent runs are hanging or not completing when deployed to UiPath Cloud Platform's serverless environment, this may be caused by a library incompatibility issue from an outdated version of the UiPath Python library. +If your Python job runs are hanging or not completing when deployed to UiPath Cloud Platform's serverless environment, this may be caused by a library incompatibility issue from an outdated version of the UiPath Python library. #### Solution diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index 2d8d1d814..f73afa0d1 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -1,5 +1,7 @@ # CLI Reference +The following commands apply to both **coded functions** and **coded agents**. The entry point name (`main`, `agent`, or any key you define in `uipath.json`) is the first argument to `run`, `debug`, `eval`, and `invoke`. + ::: mkdocs-click :module: uipath._cli :command: auth @@ -132,7 +134,7 @@ For step-by-step debugging with breakpoints and variable inspection (supported f ```console # Install debugpy package uv pip install debugpy -# Run agent with debugging enabled +# Run with debugging enabled uipath run [ENTRYPOINT] [INPUT] --debug ``` For vscode: @@ -154,19 +156,19 @@ Depending on the shell you are using, it may be necessary to escape the input js /// tab | Bash/ZSH ```console -uipath run agent '{"topic": "UiPath"}' +uipath run main '{"message": "hello"}' ``` /// /// tab | Windows CMD ```console -uipath run agent "{""topic"": ""UiPath""}" +uipath run main "{""message"": ""hello""}" ``` /// /// tab | Windows PowerShell ```console -uipath run agent '{\"topic\":\"uipath\"}' +uipath run main '{\"message\":\"hello\"}' ``` /// @@ -312,7 +314,7 @@ Selected feed: Orchestrator Tenant Processes Feed ```shell -> uipath invoke agent '{"topic": "UiPath"}' +> uipath invoke main '{"message": "hello"}' ⠴ Loading configuration ... ⠴ Starting job ... ✨ Job started successfully! @@ -380,7 +382,7 @@ File 'uipath.json' is up to date :depth: 1 :style: table -Runs your agent under the debug runtime, with a debug bridge attached. Locally, the bridge is the interactive **console** (read commands from stdin, stop at breakpoints). In the cloud, the bridge is **SignalR** (driven by Studio Web / Orchestrator). The `--attach` flag lets you override that default, including `none` for executors that need the debug command's surrounding behavior (bindings fetch, state streaming) but cannot speak the interactive debug protocol. +Runs your project under the debug runtime, with a debug bridge attached. Locally, the bridge is the interactive **console** (read commands from stdin, stop at breakpoints). In the cloud, the bridge is **SignalR** (driven by Studio Web / Orchestrator). The `--attach` flag lets you override that default, including `none` for executors that need the debug command's surrounding behavior (bindings fetch, state streaming) but cannot speak the interactive debug protocol. ### Attach modes @@ -427,7 +429,7 @@ Next: analyze_sentiment :depth: 1 :style: table -Runs an evaluation set against your agent. Entry point and eval set are auto-discovered from the project if not passed explicitly. Evaluations run in parallel (see `--workers`) and, unless `--no-report` is passed, results are reported back to Studio Web when `UIPATH_PROJECT_ID` is set. +Runs an evaluation set against your project. Entry point and eval set are auto-discovered from the project if not passed explicitly. Evaluations run in parallel (see `--workers`) and, unless `--no-report` is passed, results are reported back to Studio Web when `UIPATH_PROJECT_ID` is set. ### Common flags diff --git a/packages/uipath/docs/core/agents.md b/packages/uipath/docs/core/agents.md new file mode 100644 index 000000000..5076d43b6 --- /dev/null +++ b/packages/uipath/docs/core/agents.md @@ -0,0 +1,245 @@ +# Python Coded Agents + +A coded agent is Python code that uses an LLM reasoning loop to make decisions, call tools, and produce a result. You write the agent logic using a framework of your choice — the `uipath` SDK provides the platform layer: authentication, assets, buckets, connections, tracing, and human-in-the-loop. Package it with the CLI and deploy it as an Orchestrator job. + +Use a coded agent when your automation needs multi-step reasoning, dynamic tool selection, or LLM-driven decisions. Use a [coded function](./functions.md) when your logic is deterministic and no LLM is required. + +--- + +## Architecture + +Every coded agent is built from two layers: + +| Layer | Package | Responsibility | +|-------|---------|---------------| +| **Platform** | `uipath` | Auth, assets, buckets, connections, tracing, human-in-the-loop, CLI, packaging | +| **Framework** | one extension (see below) | LLM calls, tool routing, agent loop, memory | + +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 | + +--- + +## Quickstart + +The example below uses LangChain. Swap `uipath-langchain` for the framework of your choice. + +//// tab | uv + + + +```shell +> mkdir my-agent && cd my-agent +> uv init . --python 3.11 +> uv add uipath uipath-langchain + +> uipath auth +⠋ Authenticating with UiPath ... +✓ Authentication successful. + +> uipath new agent +✓ Created new agent project. + +> uipath init +⠋ Initializing UiPath project ... +✓ Created 'entry-points.json' file. +✓ Created 'bindings.json' file. + +> uipath run agent '{"message": "hello"}' +``` + +//// + +//// tab | pip + + + +```shell +> mkdir my-agent && cd my-agent +> python -m venv .venv +> source .venv/bin/activate +> pip install uipath uipath-langchain + +> uipath auth +⠋ Authenticating with UiPath ... +✓ Authentication successful. + +> uipath new agent +✓ Created new agent project. + +> uipath init +⠋ Initializing UiPath project ... +✓ Created 'entry-points.json' file. +✓ Created 'bindings.json' file. + +> uipath run agent '{"message": "hello"}' +``` + +//// + +--- + +## Project Structure + +``` +my-agent/ +├── main.py # agent logic +├── pyproject.toml # project metadata and dependencies +├── uipath.json # entry point declarations +├── entry-points.json # generated — I/O JSON Schema +└── bindings.json # generated — resource binding overrides +``` + +### `uipath.json` + +```json +{ + "agents": { + "agent": "main.py:agent" + } +} +``` + +### `pyproject.toml` + +```toml +[project] +name = "my-agent" +version = "0.1.0" +description = "..." +authors = [{ name = "Your Name", email = "you@example.com" }] +requires-python = ">=3.11" +dependencies = ["uipath>=2.0", "uipath-langchain>=2.0"] + +[tool.uipath] +type = "agent" +``` + +`[tool.uipath] type = "agent"` is required — it identifies the project as an agent to the runtime and packaging tools. + +--- + +## Input & Output + +Define `Input` and `Output` as Python dataclasses, the same way as [coded functions](./functions.md#input--output): + +```python +from dataclasses import dataclass + + +@dataclass +class Input: + message: str + + +@dataclass +class Output: + response: str + + +def agent(input: Input) -> Output: + ... +``` + +--- + +## Platform Services + +The `uipath` SDK gives your agent access to Orchestrator resources at runtime — credentials are injected automatically when running as a job. + +```python +from uipath.platform import UiPath + +sdk = UiPath() +``` + +The full set of Orchestrator services is available to agents: + +- **Assets** — read credentials and configuration: [Assets reference](./assets.md) +- **Buckets** — download and upload files: [Buckets reference](./buckets.md) +- **Connections** — Integration Service connections for ERP and SaaS: [Connections reference](./connections.md) +- **Context Grounding** — semantic search over enterprise data: [Context Grounding reference](./context_grounding.md) + +--- + +## Tracing + +Apply `@traced` to custom steps inside your agent to make them visible in the Orchestrator job trace view and Maestro dashboards. Do **not** trace the entry point — the runtime wraps it automatically. + +```python +from uipath.tracing import traced + + +@traced(name="lookup_vendor", run_type="uipath") +def lookup_vendor(vendor_id: str) -> dict: + ... +``` + +See [Tracing](./traced.md) for the full decorator reference. + +--- + +## Framework Guides + +Each framework extension has its own getting started guide and sample agents: + +| Framework | Guide | Samples | +|-----------|-------|---------| +| LangChain / LangGraph | [Get Started](../langchain/quick_start.md) | [Samples](https://github.com/UiPath/uipath-langchain-python/tree/main/samples) | +| LlamaIndex | [Get Started](../llamaindex/quick_start.md) | [Samples](https://github.com/UiPath/uipath-integrations-python/tree/main/packages/uipath-llamaindex/samples) | +| OpenAI Agents SDK | [Get Started](../openai-agents/quick_start.md) | [Samples](https://github.com/UiPath/uipath-integrations-python/tree/main/packages/uipath-openai-agents/samples) | +| PydanticAI | [README](https://github.com/UiPath/uipath-integrations-python/blob/main/packages/uipath-pydantic-ai/README.md) | [Samples](https://github.com/UiPath/uipath-integrations-python/tree/main/packages/uipath-pydantic-ai/samples) | +| Google ADK | [README](https://github.com/UiPath/uipath-integrations-python/blob/main/packages/uipath-google-adk/README.md) | [Samples](https://github.com/UiPath/uipath-integrations-python/tree/main/packages/uipath-google-adk/samples) | +| UiPath Agent Framework | [README](https://github.com/UiPath/uipath-integrations-python/blob/main/packages/uipath-agent-framework/README.md) | [Samples](https://github.com/UiPath/uipath-integrations-python/tree/main/packages/uipath-agent-framework/samples) | + + +--- + +## Pack & Publish + +The same CLI workflow applies as for coded functions: + + + +```shell +> uipath pack +⠋ Packaging project ... +Name : my-agent +Version : 0.1.0 +Description: Add your description here +Authors : Your Name +✓ Project successfully packaged. + +> uipath publish +⠋ Fetching available package feeds... +Select feed number: 0 +✓ Package published successfully! +``` + +After publishing, the agent registers as an Orchestrator Process and can be invoked from Maestro, the Orchestrator API, or the CLI. + +See [CLI Reference](../cli/index.md) for full `pack`, `publish`, and `invoke` options. + +--- + +## Studio Web Integration + +Connect your agent to a Studio Web solution for cloud debugging, evaluation, and solution packaging. + +See [Studio Web Integration](./studio_web.md) for setup and sync details. + +--- + +## Evaluations + +Coded agents support evaluations in Studio Web and locally via `uipath eval`. Evaluators cover LLM output quality, tool call correctness, and trajectory analysis. + +See the [Evaluations documentation](../eval/index.md) for available evaluators and how to define evaluation sets. diff --git a/packages/uipath/docs/core/assets/maestro_execution_trace_light.png b/packages/uipath/docs/core/assets/maestro_execution_trace_light.png new file mode 100644 index 0000000000000000000000000000000000000000..d3d36416814f9ed91352506c48760134baa8ba59 GIT binary patch literal 591864 zcmZsDc{o)4`@g52QiMmg5Go>jSu?guXfR}*j4frIu`~9iQWT*EBgRs8#*lrd4WY5m z*v7sNhAd+re#htg{r&On`JC&T>s;r|csu93-}imL?$`ag&zpz(+Dr^Q40LpKOyCFi zjp*o@-RS7fJvnz4IMYKHJ_YRl@;1`GM_1f+nG78K?RXb*myWJ9j`7g;3~+qj*=QxpjLwy5k)1{ktaq)+=LvKJ3)d)+y!lSNl#~y8q?bl`A@IciG?PvfoWH z$`(7NV{HBKPBysY#n~)Y)(g8?1?)^$8USL=@4V)N8-KlU#_Yt6xi|=8bZ$KF zMs}sg2Q-!cXNy<$UYp_M=H`|LF5=%CNzM=P*S}8#XSg*y*#6rWSK|L6{r%s*_$lP} z{{8!P;*-E1KYlE>#_-m8&P2EprZN4uKblNWnwp#^V4+Crl4;A2AGc3q z6fk%Y?|=L4mk`0RulVDm1A`~9NDIlDE{$FF3Rouov)~o!7r(HW9oAqB;RBXrNt@u~BuZYmm z)$HVm);O0B`%Ha1GgjlN;pyqw5ib%K91=1&HD#N|@vqUYbiR&@gSw|)E{fR{ z7o`_$RHTO%W+HFtySce-c=#nn(vK+?dE4|Azf?YyN8w?#1H`l`*nIi35HM#}!z!fy zu}Wask!VI2+#+7RB9BxeqN1aX3ygBkxt{uS^k))yhiIsaW$N@ye5+{aK}b?=cWO@M zvLY-}rf6IjIe=TX2;5ETJihZkrpLj{D=r`)P+MQ0 zdg7`6?>1S?1q@ts8{?wjeZy;s%f@+N7o0mnk%1oyL1@$B#{WA7uiZ}3y`x?S+qbr~ zh>D5n083rWDEsw^LH)5^>HIuA&EZ&{4DWXc#&#c`)~g()$R5){FzdJi;L(UnNp+c= zT%G}Haz2Jv5wO9$%Sv940MGg7R`&KbMiTsw*Kkx~%I|i^+>tbysrc>+mlmCq%QEQW zV+Wp}B=`l!(9p1K*rg*)xf*%y&q3$_6RB?Ly!2d!BM>IQ#BG~T3IKMWj6db2^{)fY zqP!Q$2AG`$*h5xIO8>-$6UCGM+c>(f%ZJ%dZtSUuKCyCn5K^g38KExoD0`Nj+?a^D zA06F0WX%P)5f=s3-(OA>um0;=o=JS=bYxa5wyZQ0hnC0_Rylzkp~G|@Sw8%>@!nr_ zcg#>iHQd0Af&T_9@sI5e+AcaSNpk#h@FBdlI+t7@gQNi-*u`h;Z@MeGIu}W^8o-MB zW3#g-y8PH@!fdrQ(Q;F^vaMal^=aA$2@{{)0;w$`k~8f$Y;;R}S8#9Qh2bVU(5)u{ z%NMvefnUR#ZQ~2H1|f8GWx~}ze?I8X+kL|(!p_ZI06Zby&OiPSaAr--;#U3efip&!aN-jB^`#0f@$8jUAn zLM@&uKGN>X^UO-3=1kZvQ~(~kEXY9?53H8e(Esv0zNiYSsaf~lD(xcw{K+c@-BZ$j zx5?VLImW`XVcGZFKJx~j_15zxCWV)Z(a63AfqiUX=u9xDp*A%nW1h#KZ3+T-H(G5_ zp4a@ZsUrKGU&6p~e=r^0|oCH(HZ^~WgH)FO7Q7bY}#RIW=en7pilz>5oGQn1!p zT&C`Q`kJ@f7#3R;_mz)UOSBR%uI1<|%U88W7Y=6EE;aU8KJ^nG*F6I#4lG(@iiRuOJNk*Svt)50hXQ+*(&r@Nh?}Lgs7d&oVEv>IgB*;ouhge(GrjBVK{>5Q{l2lOpuk7 zOW!005fOpv3$7o&+R~iU6!kc9@b^!oq*4Qf~xxjdiAUS>6Nc{@FS(7wGa-8HrQxjYJ^YPcThi&;Iv zt^a3xHs(xgSje{>u^^~hk}v2>t!<`S&PgLTxF|B?lE>aWWqqtQq@D2ndWIk!BL(R> zJo&ox3|`Q4$fTQ`(~#?y@n*5+Jl!24fE&$UxQ^XxvAO+p?Q=5Tn7^@A%;F13H0&-I z6@)o9uIh$%P~LP9>;FBkPnEpqJ&qb(f3t^#$#ux3|C5@K>Epj(6Xg+WN!X*JDvVEy zND^&l##Amp*OdkvYFAS3OgH){S&Cm5RnUQ6%VM{0Xu00yP^hg%*0HOwGq(ZOl6$cHJ|^O7YdQJb;)K@FG`r1k^&t7A18FV{VG?Xb_DGJKqOq_PzUe5$`mTPegG z%y#E_8K@L97^oHNaCDHr1_wZ902qZm=Qh3Sd)=~BK+ZUL(bk@Kr}0T-Rg%S5?F77g zZTYCfp-poqrQNY;=Uv`HD}GFI*0jZT8i#udnZv#R!T9a@G73YN!jv z^%tu%FOv2ciBs$xq9zX$#nI662Z_G4L$Y+U#s|+2lDVL*Gme?e4B%M%Oz46@aiRJYF|NkbN1B=(Z<^a zrLq;GX~gID#TpoAdn+5oWwttv%*-r#zp3t}-mrXh$?&_?o?M|cB|qyLLsc6*k+ge@xKlTM2OsdV!yXk|>3M|{@C_1&VY?Z!!;$kU1LoypIG!OQhcRya!}qg_gBtr58y2rMIHgeIHiEsJB-ISCJUFl7iZfyuzk979`e# z6vS71E&chzLCts68YY%NcmqHovbqB&)FySPM$8rkZOCZm6+_|-*GuwfV( zBV2-50c+qh@$_ue;HzK;!Zr(y+GtP0;~`Q`T@48n6Bcf* zI|nwRjY#LWS@0DB>2{gK3rr27B}2KO=DaHV@t{;dgZzV4%6>BV4-Rwj@k8I zsFyfwBvA3sx%(0#1rjE54M!QG4~g{mBI)7w;XgsvWXED z4@GqeqSMkLJ5$U1{^7_gAv}tK<~I8fz!qw2Me7|E`cDp*io14PkfCf+9n1~doqznb*-(PE%@o^{K^lm)eM(; zOC#Ft3?Dyp-Z$&cAn>ULKU!j{13`%zhGvC-GVMhXTq(-khmGECeu z_YbxyvII8&prFpSHf=REwZO$skz2pNvtS>*zgB3N6|po};vPy;c3K)J*x20cC->)> z*895d9ULsb5lR2t<=_Qmz>PcOL9nLaTCdrkpQ`3SZ%-Qqy9y3<50uP4NzW|PiQyDRW}9k z>vNu%Sx>immn=c0Jm4q5kC@c+sqHS~Oy<+SOdZxUpex;4TOhYS5a zGp%8{``D6!L0+7VECGOjaJS<;#j*y~pzTMxBUP@L$^{H2AGWc z%50l38Ugm_YIqg|d$g+=^FhuOQ+=Fk6|{qC4Qv|IL~SiZ*lhK8zo{MovgfTNg-ffO z{oTpDBMG*&i#7H26YqQG%LvQyFBJqtEg!^dGwUc+aB~9TC)6Bg%ce%)ZQLivSkt!F z@2k@qp2Pq#w5m%AEVlDwX5VgCTWoUkW5H3sG-B&OV1AIfMnKbw8T!b-=3B9YinKTM zO_M{I+SDLBha=zLtE$zM+Dn3j^^e$l|LV5)N->Fwty3z=Z%lC~m}G}SS1mig|ysnbUWEas=ssFS%%`MoEH zTvIpk4m||ZYcPv%v@&Uq*L+fEq2bm-A7#1;Tp;Gz;7t;um**JOEguT1uVh&&d4E{y zckEV#oZiWhO&KgNfDgDA^2Ga^)0gM+#Z$WTj)GGBT0!SXK|m&e@bFFo09`!Ea;*$V zl!-lpEx;_|A7_=u=NmkDgUhg_pPP==r@I-J}Y?HEYU9r7oA0 zraKl^J z*tFYospI?&7GbY0E8u1A9)|&oxznV+-)o^G=j!PL419IfZKLM&n8NYSj6qbazKcY{ z%yW9VEt=mP06Co1z)?VmnJkUxXmHFx-gsjZHkvcVMk}%h8-04g;{5YqEd@T6B!%eh zOU&hYTZIjqTME3UQO!DHXLXtGO8?lAf#;QQl8t{0%u7eA=C;S=9oM1HUws%E82W}v zpHfEar-YZmfV4 z#1{N@Yfep9-W=2P>U=Gue|oH>I4$ zcE^|Yi}#4LoqR;@{>hK0>W~IyMPHZQ9RHzqje;^G(gTV9yg4{x@*m;w*}>0mA8qvd z)mY~HXL59Z`d0=RPH4;7uAI}D>%!dheHpc{Hl6D0?HwOkAL*PvItAdpkp`NgG4AOy zlBE!yz~e7T`V#AUiKFpS21Mb?O@NX z8F|W}$MO{qgD6mwE`oT7ySYr~#`6_I>hDMpeafIhEHKuFU=#?GU&AZ?*PqaUrzB=t zm_T(yWw%6gS?B`n506@KjSgC(0Py4R<0Bf;{Di3B5I4(z+7^4_i+zp`1Mump7OtaJ z1k`oi3Sh2~t0Z&4>n94~Kitv?+285~NLJ+GuIKW!5O}o874T-5)asI^eH|uV1(OF4 zUJJFWCfgY89d34;=IU{WthaE10yeUdkEW)k_Sf442!*ENriCWpJcEp?E-zh8d`4~n znNn99@;*21Gv}W^fYkjTcrRrwLl|Eh*HjYlzr)Wz|Bqd~78AAi*$zN1&1L zOe%8umq;1_O5sDFKWWAAn^XIT4Bow2I%;8ljUQhEjg=y3%0`_AjBwbNUFrq$1SS84M< z!LLL*7uQkOg|z?a``#y!*EVXk6}n~(5SQD)ZfV_@%NRd4CJFA@@_ii}+uLe`zx#_B zmx9TSA-a&@DP0Q`oM_frY=`mO96%ZZIMa-fCl24p|Jo6+oa8(ngZ|?9g1(_)pwL;+ z*mZ2)2GalFcZ2ZSc&-K!V{^LS5^`wyt32o>95a9K#b>H+<#*WMtM;Dt6`tjluYY;j z{H@4!=4`fqd0DK5yBX1QnMqM(^*d)7gtxFppy?q=QIKP_x|ALH9b^#!5Wa>6s?>za zqMn|I0ILkjoA94u!kL%1${D~eEBopJ3$YxHB%^49mW3t-Pxy4o^D;tVI>I#p^Lir| zT_U$Vr(XtcE!s*>7>PJa_YrR+%{jRzd-*0=I@o8_jq ziwTs0s#hCxR0V=iyGWQ?WADDavFlFL>_HtN@W{M>tCua$XMKain5p`XT@CY5ogsoa z{Pox4luf8#-%K<(rRipC5ep5X+RxxP@y=k+1jpQ4!3Q zSu#8*s6uM!LN8Rz{H{#EJZMSl?c<i!gOZk)0wT_qm`$!|j<9>-A9lZX(!4FO zkdoP#Qt~-UkHkEWY)pK+>^6Lq{G~STRiyX|yp@X|Nu5u6YN>VK>%b>EeajLA@ert2z@PS(e2Mf@K|kJsd2y z!p#Z&e<%n8+*;o3@-p_zmpe%ElcVcwS!bDe7WWo1l7xU_nbXnX9>{m)15zQLfkVt` zqA4T_C>=b0{L!q~vS4!ZF`s%U34}c{9s5o+UdgK1QmJtpfP2uAbRlxDwg90$em+dD zh(GMmlS9h#k=IMGaf z0EQMyMZO?6RK^b1`a2V`u22V<78OeIV8-@s4!o%xY$ZlP#!gu^S$;~r<{67 zm0ce5OP+DYy%5tTefp3sw;+O~^SG2n+dzLkHv3rWrP#Nv^Gny=CnC4*e6uU)g7FO9 zZK==@L@*~dHe;<`t(ME>WtFgV6xuYU=68pZ3QjM;ox4ig3lSd-Q0!beVO78SE0P&2 zeJ9@kpe)ScUf5?C*ZZmxa?H&+^k{Pcv-f{Cw`4!HOGVL1Qp$O>HWujndn}mwQlOr3 zQYg&v?5c~w$j{Cgf_B!LbN6u%fvTar??79jMIIU`6bT3T`wNjT1lIrEWs`4@9aPS< z@vn+fTTDKz+@;j*82b3U^$u)xoJmx-vF=wn51>*rfQL}d^9G>Iuubrjls2cuT?6Xi zL}XfjNL^#1P|q7X7yeHo`^CQc3hSe}?ekXnm5sm7#PVNlW^zQGZBX&TxvOf@?eh!Y9X58#I`c-lBkGPcg$ z7x#&2K63wcjZbfW9jFEcA}EKx8;R_o9Z3Raqx$I=ZN4U}Lfb@%%{<}Nld1*1GV` z%0SE0Vd*(tnmF0p=tloCt&ma>d3kDDDLFNzgcDCg_GA?_G4p4`^AzAl&8#UwLziD{ zi4pDyFfW%d7jKJn%A}90+h|0D;^gywb^ewb)z1j-wW!<#N2wJ9B)?N%M>B`*g}V;Y znft2RPA+*HIy?vi_ENv5hzkb$m#j6H^VfcUU9uFVQj5@}(09K;_xWsaU=0zGw23Q( z8G0k6qgwBCxu|35TBbn6~~UgaA_RuF)i$ zQ1S&=vE-YsFVK<}OBrEDh5&ZiJQC4Q?O7sVPhca=F-hnoGQ;X}LeE>~ZxS<2Nz7-u zeECN7%q%}}?i75Qrq&^5FKRd$qpp4PdE9;p5egq^@a3%~Dy5_@W}F$LnzXboEj71Z9!bd7 z{j`n5?j)r1mbPzwsW_Lw~MPqbzfmlwAD+6u>`!}C?BJ%PT%{P`PMt# zW}zK&uJXsr1u*tYlMUCoMiPV!x_=03c{MPL7VP_I{wK`o_5T!VTfn-0cS~6ms}K!( zwmKOXZkp8}%m*$Iv?2XP`Jk#<;^W0JlAwC2);5Y~$6722nx;(|{8rl@sW5j=M1N%? z{Z#yRyNuHA0UWYSFln@UrEs0RG{LYkrk}OL1JC0mAD_*=I8t%j>jpq~f~Q6{+ql2` z@ciH#{MJMn@GTVFU3_nGCW`&N`}=Df+Yg3Tu^ea}q3cgJ&kph5-|T5SYHg$-ZN`;c1K50d1qgBr@skC;SY<%D|V}RqMT#IP!sqTN{E> zQpHrCvk$9f_p^MyCTBRh9@~?VQ5?QA7Y{V);7u?A~&s)Dg^E3^vZfTZPMh9ll9ur24z=`_)G%xR-qDJT`l& z?e@Lu9eaotBGCJs=K@`T6cAQd8OkzH-R?7^=MmbJams3e(JERw)47DuQ-4>=fN~*ot3J$v zu+nS-Ez&U4K4sdh<&HlXofAzpxBNhiamN-4xaeFQe|v5T2Mbpq2kVG1FpD_)K?$EQ zmKkym1D~AX!+|NG2DYD$f4N{~aCv0E<&OG`$ordt0}pA>1e5F2|5)sIcxYVqO(^PL z(n++TSs)y-u*3-s8ZO|Map#{#EJxOso2=!4a!WC$z;54mRRVHJ)-qr-<)9dl28dfI z#F>15TRlQ9U(oA+B@p^kE=1P%$W5;u+!7r9Ar}{=`h;ISNQ$6v_d!Wb^W0#YO%U7u z8GGqsWM#C7D<)k1C%wL-bn2H>c;}5NP;vnfZodu{LQU<+fV#Z^suld7aWj zpNrni%&IR+5~S_*KE6V%LL=sORZ3VyV&jj0_{u2Yw_Jw|j@a6+kMFCLy!6u?SD9yi zJsw~w;ErMgIlhcI;sMB?o+qhQM|CiZB*}phdq(PLYz#~}GnV1vdvMQ54e$awu zCa!1nd_TN`SoU^qO;rlaU_mm_Faw>6Qj^ANgdAkhb4eSQ-HNTTg3Z*#`OPGm;`cyW zi2mL26#s{)wwa}M3Y=Cte)ItWEh2uR&J6s}Hro0NvLb6m-~TT8fe!?uwMG2V*oz7_ z3HzZ!D;1nlk|ek*n<4HB931_#z3h)n@%mCn7v3yiTGGsn4$v(py69Q9QF*CthoOZO zLGToe=>Cqg?gDRT(_7X}WaT#t*ym78sKwM!RdX@%h19ky_IJ*H^L%*4&0pFOEr|gE zWiyd6P84(KEnSSIOLV?X8@Au%APDP{MBb-B+h}~v)ju#cp5HX2JQsDP7%4~c5KBao1$UYz_NE{$gVO1{4 z$OkC7r>$@8l##+Wtm-NvE^t4QoKVOJ-7hG!mpul(>1C8|_&8`CgohEQzDFXd@W5C& z_aw#D3Bp(Cyy^EL0?)iO${itFA>>Mo2mN;Tiy^t=gx3z&=#5wRxl6%oRhazKL>66X z_r&7f`xK0+G5HW|P<0LM1MMVhUu5)Dma`q=nUO$6R(CX$bSR@$Mh6ZAlb(&kYn*S1 zj_sj8b0^ewIfX;`&$2G>W7{u59e->uET?Tb>!6r}`yN?^_o24_tgWqH_VZH=aM<)1 zzOgv$Sm@LWJ%Uv&1|i7Z zwb9L$wf7|m)WMo*bq~uUs*K|cTTH(IqeQ6`@2loHc_B0&$oMC1XM0FC2(@R$wifwF zU_{xLLsb zwWj38uU#}zF%Zl?XdmHKel6nd+ou+95u)zksL@r0b@U`ILE?bcad*8r2=4<%;{0O~sWS8z9C=%;TiEig&pOx_H&M)y8ndX6t8 z5bK&JI`SUg=qtZ4sphpbAlkUz${Q7}yWehAVyz191L71; zBM@MxOzV7{5yLrt;{k1R=R+L42%ei?=>-5G$U2aG9RWu9%)Y+r;x9hnTEZVE ztgnU7z_zs8A5}^BRj|E@x2AydUwM+2NR~_t4@RyN>h)Rh<_D2o{K|ICv|3A9WpqW? zeBW9IbMG|cJZ#UJk$q#qedMsYJw*nIR#-dIxT?-KHKJ7aX#Y)ayJ)s1R7W+fkP>G8m$dYH(%4gZ-`Bn8_;}U1APt zEoVyZHHj@QV_!6}3>#dW-iCksXb-M?$%$Al!H4np4Qn)u&h}j8J9aJ2t$@@~h#@zH z_6GkXL27hsfGQm(vT-yAJ7fna`KarY&qqoF($I%Dp&Dnmp_{@b`N6d!bDNXl9rLyZ zLw1|L-*tzx6awc64ejDQGM}4IeyXfc(%%+NUl_bsM${Y?P{u^6lwYSD22}sVod1e% zK2pkLWuvjP@h%q;N{lstD!44(kqQiyOV&-2#^idIf2;J|+y2pBm^N^W>{f`vTXYP2 zNV{bzP;SLx2!<1W9hh(gs$p62A=~$@uiqmcKQl}x&G)3-t>h}?&5acwFx~F|Qr&L> zx9>XA;g7>QduuP>1^0RKA?~ih-2l`7rHIm%*nl_!vI{hsTpnALYQ6rdb$#5@@tyCd$ zO7SCaPI^W7uqe0#$Lz{GbUNLPe~wUA`}Aq?M6umyj=W2W8H+_UGe^4Ts&?Kn9)5LK z7Pi|i5p-v7_gZ5j%ztx93ILxL-f^SKTcOl;qXRW0{n&^c_q$V2kq^my9&&10fV!BO zUnSqYX-|t+(aogVZOo+14(vMKFzl1@x1>eY0CjnggA={=1^CJeA!^Q|y7f-X%!mKI z`+4YSnNDJM^UrA=K($(Q9o+Yd5uNZPHwb1mu8^wr1K0b5yI5^Cv%oU5`Ni4pEc3( z@>-Tr+$RB@bZrX3MYG=)_nAk{HPW966*$T{! zM6ar4&x{T(#YrNlZRqEPSILKQp$rubTRHR+gI()XQ;_2m{PL$VxO!-*LfB&^4TEju zzhalv|44zM3f(TE)}2RN#vD}rB>fbSeI{D+=x%+Ld*4)e5OSqQjZduxdU9CSqVBYm z@Fp-%S45R&vN0GpTJIA0Q}9(=J#P(MDuExX;6m*2G-mgdERA=pbWAsk)2Bc-Yo}Ko z#ua*Rz4>Zj7tvhl=QnNg9Car9x5q1U_YKsoa^K(n3~$=YI853-8f)X*X2cxN9pz{a z%)Lb3F6Kih!13wq{DZIH1|q%;@?z#4NSZGr?P+sXuMr2K*oFMgY0Kk`sctcOe&V3g z7-eFKEEJxg^kW}+YTw*)rv(fdZslj9aTLbbM@>4)MIk-cszrbH9v2x!pN)9zT*JKT z*T5-lVknOhvQ)8QJXPauEvG15B52Eq}GsXSpZUAH?(f<)kXb z^Z%8ViE^#M6#e{}t*rp<)Q_IkXHfQAl>o$xwY9ZCJxX`3BheUG(_7Qg(n}ckCPO{F zw^S)qcA0%=AyC4r^Iy*dfR{=B!>b5U%{aA`tt%f$AV;){^Wm#_Knwp!7d0#MIc1ok zYOxS{L4bHdE1NAx9@V~LIBdBItV^WlI)eYry-K&Nxva7JH>X+$^c7Ocw2d~NO_*XM!{$u$s! zeeDF~cYcYq<2FHd4|m!Y#%s67basuIXK*Y<0y3G?*QjRV*coxV)(v1^b^l0wzF2hv zD*yChl>|wQ>O*p7Nvv)$&+nS)c?TAkKv11aU40%UwAwhGRIAsYRBU+*lGnBrVeoKN+iRW3pOa^v&t@9@eE0k2E1u#w7K_7tb!v|sYc&sss}Aj(_?pBpjKDoH*0h_%ZaN}w#UMM8-{E>^m?ui zHgcPT2yb+45&x73y~0^$%X^|swE1{> zp*Jl*w|D=F?=lIO?BQ`_X?71fgH}!Y|hX! ztr>j1fh<bN;DCQ7A zYU1VX-7o*WByo8tqT<}cGp zG4E%+BdXdId3h&XlWL~x{6nPA!A!W^?dYmj351@gLK z{v2)W%ycZmb`4!P2&kX8BgMb@^9Aw?>~V@-n+SI9)UxA%7T0wZWVfAh+?5UQs4!X3 zLHR(u!>f4DeyfBJOC4lTdGnh@BUy`kFu_B!Rs;^H6}z$bZcXeXg!63TX4fiIk;XG_ zV8q})-tq~;O1iIA!{C+Zq=ni*Q z%bIuoVUc|XLW8&ZCf|68x#E;k)t}O0-aK@*`uuRysK&K&b=vcM^UOmt0sQ4G9v1V~ z>zaJSp8{qMu++}(!dR$;$!$HJw0B3J6YMkk<&p4QS{|ii{LIBqu}C(}!oL zrqT;yqTCX{z4}E#0x~hj6n$(4v`8M3Bx$T;rQG0Fm?OOPcCQqWi~N*}!6e95zr_XV z_$C(vltXSHh5*{E-+$G`{pnM)aFw#e?{60S`;mnGr#lLKBNZly6Uu-UskhU3hfNDJmh6tbnNoPlxv0v9YMebdE|OrQqg2 zX+P}!%QNaG{&%ib)_<@OQTOiM>mz(x|MJ%$uCNm=iI5!Q93LM)5h}$Bs2A5EmqE)c zYP%nuoBobuHXjwuT`6fkDnnc9ta6#SL0@mg-WWAhO~0jg%%AVh$0Q|)?l%2+RdAV5 z91hDKQZhCgFtw^8y7eXIZ=FFJo0cVplFH0vmCaDq%Mp11-^wh(jU#psnzo(crZN4i z(?W6P{-U$%80*SPYVw#YAl(n_5D%KhTbKJ0e$7u^1#GU?@%iW6-luUXY&HeLnrFHbElyUDb9!~`xO}@Opf{bjvwAMfvoOlJmF)AQrWM4T zyeou6eddfPiPIoY<-w9SR(~~kx7|c zEKN{`rJuM?==$4NTJA>AwJ-9^S#rtj@{IeTUMf~2`mHXHuz!Wv-M^=(lLhog(Ds*0 zh+bu8MUQMH!Kn0r)(?~QNdCaZs|1_{l(tXp2gG3P{QOet>goWE|6x98@;SXp=Kb@= z5LD>iT#{U*XdIt%Za6(3`2J~Va|#M@wLpTlH4%1xjAu>r%s`WO$akOoT>QjU^2bwZ zR!{Q}0B{M9AlvP#<+e%Y#5)=t4WWgs&Jn-}{3oCDh>ta>z+iGoGPw?EcxL8WLol^t z@eIS>CJT*qwq(j`85P?Ob!NcGnVo8%67uM<|HtI9531gqSzEVhUlX!OYj`*!gq3tK-G0q7nN2o!a)| zN@%_YD`g|tcJpXu2W#&-&P3}>(F`;F4&TEK(YLZAdsx9C^W3-TdK;GRsuOuw<+}H) zeYs;~b>et+(GlD5awGvC4sF5AMgB`moX~7Sd+CvY_ zBp)Hf{s32WZi!y@I-SY}ZVNtu3a*NNO#vUz$=I|SR{`EoIwC@9#TCgQryq^Q< zkB;bS;)qMD3qN4~IWql|JQ2e)R{A_W-%^{pFG+YmTa_kGSjvARDE--Gwa>0zH$u|p z##3>L_*CS;uU>WWtx-<~A0Ixw41g6X%n4B+YJe!my7%?{0~X5^mNp-C9!|tfeBoDk zJDUG~rdp&qMCPMQJi1IrhGpu z&o<7qh4o_tzJqAUrcK{3AK&WSZ@PS;I@0~kbZm3%vXj{xDei9=KW``F9bFH$WUMb` zMCvqJ99ld8s5Ib`n9HhxCe$T@_wd~ffcvaMuQE`VdF$^AF9Bfbo(3-Oj051sfucrp zWonVO05arWZ73}eK^~trgrq5Rt1V3v7svb-QeGsK__)o1c0b$)H~wz8dPWS)>Midz z&+3g|eDLusUT>Suz_vlPz zVXQ{Wj8XjGDpOjx3X^jeq|F^tnh2R$PXQ=3e^!?YZ+x7hHUGPiU?E@zNoBTBd(_>u zg$^`GF0nsJ+&1=7Zacgjhxgtw7;q|Cy^D@+mDOZbbpRSdcd@R1VR)-@Kr14FvT5E9 z+@_L2AJQMrLINxa7e?>%YUNwEedPf;G(<43;#wOymBMo{P25Gb^d;aZv&D#Yp3hhK zri|hi2s^6UlQ;bvK#&o)^LQsrt^|ah$%s!ATVw~X>}QRFEn;5 zi^J1DXi2>qwI(ohXkh%7TySNbA5V#VTfFc1+qytY z<!*qJ;h^T!Z5vp?Jat>lp@8qw#65`m7-klF=UOO>$+9mS@=8v8ZDdN}nD~63 zdWNr2v#FFCN($FoZ>5bGfAK0}^lktL6+N3n-?er#E(Bz{Y5emUXl}#E`nw%jq==%e zK{a3)?yu0<0Nv1wu5Zpc-)xbANYFnTvXNK+d=#N=9x^aY4ZZM(3$v@*w4LHn>+Obh z`Ft1IAKRS53ri0s0uuCJLK26WKyS$76cKqzEwd)@tCbcWe;B2yDt5+l# zo5pk&6pO&+OrxJ4!F(m0uk;i*z^Wv#ky8|0tLQbc@&!*v&N>X62EA);ZFtP`h~Hf0 zQ4GRl&{%p{pxjz8h^x=hW)0Wty|a8zM~R6`jH>&{w?-i*OxHlETGv?B0{47Gf2{Z> z_>A?X>|vS_T#;G>+HwAPs*S54hz5GIyko?tI%<#J*3t#;6)PG}5Q?KVRmTl7hRX#l zZf;XTf(rtx^Rq+>C;iL-XuFJw&;I?}BT>{?3kb`7uUQ4<0C&WvghE%?a2n+&f-Jth zKOcpJ7x0jkKAa+GwF(t)ppk+e7N|CK_%Us$&l`7$7pD&EtO?MYGTs zHhA()f0HX|D;FCtsuuobo@Enip6eVw-e+tYgebKS7*A6(W6_o+Y{Xz5-OHhupPFl3 z0)@mdzl zeaxDDfOIS0zyPtk&||J@>GsbiN_xp71CC|+);-HVvaV+q~E867^m3;F?T-=?lh@#LRI85shvQDXMH7u6cW#2L(;hZ98Hx?eFu_eIH<^ z10QYs94xC}s6K#dH-Vwr*5hwlyi7Ba%H~pg2FuaOvvRp4nK&kW7mzX zU)-8M=FEnfjVtCbzHq8fU+|mveuPk1lS5sEd?<KowA&31{L`TCcx4@F=JS5G9zFJ3v}ma%4}ruMZd-hyn_}^z?M%)nQ*( z6Vjvtu}ozjk_faRbddq=ecg(i($m%pNXiHS*7XkPNOAu4;TF*=!ZiQkU7*RQ99;>> z8G!zlE|@PFc*6q}P&SwVuN*i5pmM}q2(RR$&tFWu?EwLG;iJ;b%A&QV1AX^~^@z&U zyPr~sHR)RXatYQ4Fh}*u!~OQ{Ua|4e;<+{vF=x{DnI+4gk4CfobbSV&Xyj~ttQ;ZX z|GE8kDBEutX>c)9kEn5G7J2p8){vyb<-Q~&Tb>}13MEu0<&;ppUp*gb$S&96)n_;nZZ(m$lsb>bw00r)SM_SME?O$IV;%%5X)~u}-j2PM*Dbs^cA_ zc`L`aXuOksVDTTW{|oM2kMpK4y(lB+@7|x!Lrg#431kDCYy2P}PTB%9Guf24<~o;g z;P2`vgQMWw0ISk@@jg`ENiU;7?1zu95pTx$8UcCm7m=)!z5>0?Y0jvI+|RtNh@XuK zZ6bb8?yPf0*YOl%&(~XRtBAd8(+u3GdSiBf*Hj5A5*gRvowD$n;n+gi*Cug|(bS|* z2CdfGwqcYWTsxQe9FKwndS~RCqE7xOQ?Pq^rF%U5L&=cRRmO;(mNtOl+Z$lpa=+ogqDOJ zkWPR|4WZ?Rz0W@P-Dkh&-sgV3_s$Q%lPu<1YtFgG9COU^|1DN(LQA(jHGM0gGbVAg zx=HEgYl!{SjPv%=K-xn{71dQJRTU7lZm$2wY|h{0PECskM6*ei+9xN8)luRiOBRXH z1x>Fl-n!tr;p!dC)Y!eP#Bvu9ty{qtKewQU6(CsDnbTt?mCJk|Y@)^Ao7YUxHTHPn zn5a8jAYkg3B295(peAW8yI0rL_zedAfS>WVUpYf{l;gax*gZXq;^{KkkDK?3x#+OV zNC)Smrx(N$*Z1M?F8xms$h35W;iMRoJZP$}V z>SUPI)%fviegx!S9UxAIZ}lY0A*IT5OK_XzUye|%G&)^x(nqGAJ@H_Y$$uPrVXVQs z(7eX!C8y>Ws_aWGhNHMIT}B8r)r@_~Px^MUArxmZFj%lmUF-8*3TbR;SZ-L%lEAnd zBZ7}=JZIGaMK5#)8mMWyFy}>7Ce#Me3Vw!GrZmInP810(N4w)*etKjP^TwJWg?j%i zDmtew$x=}N+xyK$Gzdz~Fr}sxWJ_V(>0(`0Cb~y14O(O>V9|RtI>%#dbt^I=oRd%H z%iccM*c2_uU91cue>ToC05P+2RIDRRg7Nc*C_dtZ$?_O*EJ7yQxLksbo$0 zFNB_#Z`}ayKpOH0@wch@x&aLl3$B)hsom%9wGd(kxWCg$uERA2Q}FX=?K9L*!&4do zUAX-c^=Lcn0McUKhP_CRJ!EJh2fkK@`JZ#k`1B8vi&qOUnSrb0Ir@|G(B0ml0`sL* zZ?^kh?-;lhUP3KIXfNNNG}v_frOYPl5{I(6Pvem;`C8*71&>M#xmDsB85Jc7mwZUg zcE5A?E}+57-d0Ylx^}F#D(mvZ3YiE9IVKlk%dJZA!3R3(v0NCcMsbITpsc*5;d-8< z5_XbiU6|#sdD7Nw(ym1hA)BTwGjVPMk{u#{;_rxR$a!_QJoSS#%k|5rQ}ZSZZmWi$ zq+COv=e#@V<&z{7d5eOIQP-2;Di&q~JITPN=I2v;F`j3>FWuxsb=gF0EiuSn{XVp{ zh%TXymi^gSbGsK(VYGys0 zf8XjOSo`(7{$pY~qk`Jg_sgHEnZUmDLH*<4Uk{$@f2#KYP~-}$g@~$Ctc;~o*OI`! zN&H*w-~NZae8d(+8S(-y*Sc)E*aDZcu2J=sw`U8MMm`#1Z^MBDMt$s9t-(NnGWSDL zHnWIDJ}=V5EW)Val^i{UVg+P9W#H%1YA#8_J=V}UtabJhAzW@-d+IUtv|`g39eepx z<88=%h4KFw9;Z${;%mC*3AoFrgXtb}ELHWtAEE?y#aO~ilpQ%Wu_5~UV>N3sBBsqo zSE+)+2*CQsE~z|_7zAQp$8h@53RMIzs`_8&VEi99bn29Qb_dc#fR7JLbp#>)4u+AP zVirgbpRNq~prm)C)oKQfxaKNHoHsylxj`^-+v7C%e0QeS`9LlsBcqx-37EQP-C>Zun2Wx&LBxEmr(g9w$|xWPj1U@uKn63i&^7s%=feWB3WDlJAd`Z2I8R zdcMmC{pR7{7uFC09#bmGnc^U|FDlf?Lm7jhNkI*jI5W7|dk0LEwo&JyrsO<7TVm#T z%3bX50AAD9gWL0s*f_(Ig6L9S(_yOO;=DFnz8V;45h4ClD8Tom^^RxNuZ*4hEcdBq z7>8tbSrb=DuUD90;rvgoOxuy$_c;6g2TLr5YkfEy!!5NP9jUz4?NM*@TK~{@r1hbE zD$S)`Kyd%rOF+4)HFXdeQgzw?QqTYAod?bQsbr#+3pApN^ZdX02qfNH0A`+F_E?yJ ztWgM`ceyVQWZ$OT`@JGH^qMNeEB+B5TjH8J=ntKUe=l|V6p8*_`hWD}$p@)FC(Zx% zi$|URE4A!zxG^VHh+q1y^LJIf(N_6?3`YC^Px055{J$W`{lBB3{=vcP{P9l~k^sq) z97UxM{7uPw7xTXiW1i8NbgFrmPWq2W=cJm?wDMHi*54GnYyaDG@qU``Z;q$h)!S5N z%iomG8W;b+d5WJR;^N{{rYEAg|8^sgC$cI_DwNNnnF?j2f|v-xkFHUnfhR<~6VbrK ziTQ;Ehs^(kr=2?WhG(7Hl~m-~z(D1v=BxQdOE)`yc3vBMl18nD`j?=fyxJF;Z`V2d zj{oN-^hvKgn^a_-h>H1$iKcwJ!Nd;aJR{D-hi%n++EtKjg+@I? z-Lt5>*R@H1XT{zKk+eM|Zr5mhWvF92VEdZ^a7&K#gnGm?nhD+a|2ZL0|H1g=dqH`u z`5AY*UB$nt;f;P0qq|zy&%vE5eo=i6hCY{P{`OKbsZu(-=1PyhNa;4Mx*e*QJ3|Jr z;vVDkuWvO?OjZXn{PQxW%dh7;# z#A8n`$*1%V=m|ALQ|&=z<$GyQT;-_ir;pJ9BcG^~&*jsOr~tYDv-p}x*RxVjo-$$z zWUasZ>Xq#cZjg)hKhc42E`RuEKatlN&2+U(gYeNYFY$jJ6ZLre(P;-u4uoY)6WsyN zKk9wdNgW3PFXH>ztLks6@1?B&n?WGKgxM!*Mwi?_G$GeU_(|a$n5oaXRL|n1m~m0I zrtsHP($7tLz+Pnk73frtI5`ssOY^j7yV5_O@6W-PPRmSNKY9IEsOR)?f_-SZ`HcvZl|LPKsfZ!Pbwl%L&6ZM9Hc`lis9xGmr6Nh~Ws{DXI5|sFOsPbekk<~# zg4NM#vws8D)zb`ko-|dx*3-@>E=&8d052C-GG*7k@dP;Gx2K%exJ7q^B$Qui!Dkvl zz3%l7dwZ{`9u)B>^{G~x+E)1VU(NN8U-jz$#I~)gJie_3=tN}jxgPzLPk7lb6J^w0 zY+rLca)EG#+8S;*SRAX9-W;4cpt8#TyR;pBr<$d~Z6-WbqxGDsJQXx4cNNBi%hit^ zMJC}%!Zy6ru1L~hH;eFy?M~b}4HPh3qn8uZfEq14IH|W@{YmE&C=yL{n)%*w0@8_b zl^ZVFM!J~QhH-^d)C79%7riKt;L%uw**t$u)#HXmI1PO7Fo(Q`A4h_NrVf4QMbofz zT2u%AjY2pDLKLIgGoOi{LERD-()Q@AKJ&j0zc$72=>?HmJF=h%**=kHIk6EuT>Dgl zAY32u4=+sSuCTbJ7#i|4BxBr>(_m&IO!9X}ET%k=eSW*wnbUXKzZcv$Dx-H-CI(9; zu#~n#t(ueGjfohGqAgGQ*~=)CuT0qo=M|L@$~aEiJ_^o`1cO%s<)u~^{|sF z96PFV&!u9MoYPLU;-7Pe7KFUh8!x*tz$COoDEPRH83Hly& zoc<`1ncJ~=JosErc3UyYlzn(*AP9QZG3B|No|5#)D-g54H zHoPhXjU_o|)-p`lt`{zz5S;&2-i&ws=WI_d_g36g1}?`m@vEdGVgQM638esjSaTSl ztuDd3)JiX!O{tKdtJghq9_3jcAkcz%(I~%f?pVL$H6Pe!-eVM?8lxE}m5E0b7vreiwn>KLwk z*m~sRd!cB)<}4O!^*uaIzA|;a?d1k`gr!JEnGOrq;7toE?S|c`LU|_36+S(y?mPtn zPNF0V>$pVsQd}@)g^d_)L!}rwQl?C@54QP;E@KE%wK%YVA{fog&=!!z$9D}Cj3oX% zHFO)M>J{o29vO<{zCyVV$sFT>lV(WAvcft+nWZ8>iv;xWk)qGc{T(wdcM<+ZTuc~4 zSL?yEBbg_%g}`0f=Xhv=*}ASkkUMK^G(EegQqql|?}3z0Pty<^dSRBD{=sYGd$u}m ziJfTRxIiDfq;=#qB^g71VrRxs67h_b*j@t4!h11mN^r|kVkfA&k(Td3PcsSyq8%O~ z2i$OJKsZi`yG-U+n;UOSGv54}L6424Smb^Ee6hn+b;x=OYAMFqC2#TG7tK*vIeu*l zJuh{uA{lDt;H^2YnD9$|tt-kHsa^_>+*Udk@j9R^O0B9Z=q(2hPMT!N(G{!gjJe?R zYBlHJF~1!qiix}5RQy)D+9=MQ;h~Kyf`zGy$%3tW1&zKgl)n&Oh2HAi( zO_I^`7r=bqo83L@KfPNp_}hQ=;ipkr$obG`p#gVlQZMM6pjNiCEhPD0)XPf(3yhBg zd@%A%>)f?Bty;Zwo9fCF@Odx&bBRXhIXyk|308Ane>ip^ehEru!M9TT3syP3b-DYS ziFE=AxpqmuWSHAn6~TzsD$7gIQ5U#|rLz*(R6IlV9I|>6aTeswh0ld~DMGi?WSol* zeG9737J+!B{Hs21LRcfSdH`RGpnw2vMs}o`Re45_eVP5s2VbtzDz=(zIp`YWrs9eF zv-iYtGm+}rRM-=n320m7lH2hO-)BR?+3Oj>txi((Fo|AC^e|^OLCsY@R$0^%FK49~ zj?!SGDt%Kfu32+^<>DUV6qlImu|2%CW&E9nKHe;4Yed$qd2Bbe^ag*K_q4m`5$sm< zl7jZ#q*XBMm^Q3GY=}uUU7&$k7{RF44JwJG-gQ^n=Q8i%YS0_s`qlm*RfX2QXNr!z z^lpMP8LQPVR}gav;9pn0I0|Gg(6|iPh^nMl`LXM6Rj!+VSO|L9dnHYKaz4_eG(QU2 zHj9^tI?xwy0%_05tyv$m1S{Ebe#!F)bDb=h=2s~SD$NDfY0`e1s~ zBFy6ji~gt$wmoB=#9yKs1M<5;%Jdf-omwQ;AGeC_4)p5rr-0f6ia{k~&{e0GOy0at zs=EU7D%saj$aNGb1xleCDd@kcv6%qENVCbfe_}p&!RgCvZ5zclnlSbpL%aBVzM#+O zN8=T{+v+=8IPW=9JJ%7A)5~AGjAq*a#=t2BnRZ*Lg~&h4K9^}2m1oJD?z~x3ypICC zbJ*7zID@cuA%7^$V;>U_YQ#2NX62dvvf_Wx9xLT-?uhY82xMm*^vk|H_y*VG80}u~ z45$iI+jY*hwmJYhZAtnP8!ZxmwsJ|qrJgNm#rhA_BQU>m(-cK-1;G^EGgETYWW1W+ zxz9))82as{7jPqOR1~wRq8Iz{P!@)je6yXMDX}0)`=;#b<6t;tNL7wbwy+&!Ib7O; zmn)Ru5jVx3TgTWtKJPX>ADL`23AH!6J7ZorI}pzySDQ^B)$M z%fzL|oF?9=l`HJpd!A26l$_q_DcOvl$846hx>ya$cPUPf>G!3iSne_S4R_z7&u%t2 zxJxeu;}G1iN+Lwlz+m*h+OnKqNe*GW?Nu$a4 zXn@A2hhOyyN-^_4Kk*1SIDS3j%lgBYYPt*uT3Z`3BEH<@q}w;Brf6j7#KrV+7gk!d z>nJn)P93r!Usmq?atm&2)%zup?STiBIa8NUDkTbti)L4x72n?@U6>6{P`$xM8Z;TY zMv<|U^oF*wPieKVB`1TXpilxdeM1>2<-EYrcPzpbS|j>%)v~lwV$rGND&gXw@cK97 znAJQbeD?DM9{Ug@7-LrX*F+&Wkl^3tmM?uQYZV`W!7xFnq>ym~Z2qmGC&2ca;1$XS zX&*_2P5z}!+@(4|JSI7P9E@|nep8?S%>+^?V^5b?z}&~h?fvn)a1V`;mZg{360f`E zGi#?fNm&_?L@`7i*|@pcMcO_0HdCR?!T}uc!!XX>iR3pcX`2l`?KjeWODB!#8zFAa z?Acm`E_T=&4k)M~|8PN1gc%j6NzmVvd8%qHY56C*baDIinr{2fP*xbS#Jy!j@C#1f z3%F|2sGAcv2LPPCAY*jNHlnwA9&?;44PD^vX8snY%pdZFa@1#dUI=46$rdYMW2_Lr z(d#Ui#B7iXrQg~8C9n?#=pQ_+z0#Kz(&n!>;a+QKE*V(G%&D9|YO6LG_h%t`gnb|1 z+E6f-lB||mAkm(^NHNMZ+wr+QI8^|gPJ5hIvBRWcu)f+LRkZME=n%i2YM-VHv(;jD zj;s!_V!*6Ecx*#Y%(rgYcdDW|(HAA`k^O^*4BWaiR=> z!E1Z2*B9Pd{WuY1RHiCMPp(KF!-*z54c%9AQSRhO+FxmxhNj~0Yt5bRTkzgkzt&(9 z%w)1+s`5->(U!`(Iwz7Q{WM;AwZOWCqmEG-`$5flFX#NYe&4Z6{NSLgp<@W9arR?; zYxD)@g`(ZlROQ-Rz&NF*LvfDoA;`DERRB%~8l%jUF@Wvr3s?J&EHu zQj?xMw4gfG?ftvpDF<5TBqvh&n8NWSN!U~)NL?56PK@8FV5>_uij<53nHO`4Ho9lw zuYBd2eRnMswk!+#_))S{e}g&AdG7(+mRpUv(yM02Z|RW3fOjvI1^0e8u?;|*Y1z&x17B6wFSAdt6eO3=*CGA>{BD$IdcZYu@zGd6f z6i|#)XnI+`+GALsVC|~>#5-ggbwr}ZF;0Zb#UCQEfY~Dn!kG}ew7?tVPMiJVf-|BT zGBvNTq>n6~g|`;W-;3IKO_WbUMa5>!ucmnDH`Jxk@0p5dIjEH0UQqkpX!Gg0Hd_`< zi#n&v2rv2+}D#p|Hd+!wPb z7oyXF{}Ec*km3q&7K;n%6JLZ&wdgue@Ubd&T3A=tOyO!`WV|htct~gbo8KR`6s36A z@iaFq{4%$S)OBT9T$?XTvbT}w^R8yFmMxO6URnN4Z}>`M!1JSm$^q@#9G}eMc2;!& zcwxnC(Py6nvukzrW4xa9P!pOp_~Q3T3yl1*N$PuNix0-G;=Lj4awL|aV~$)x7o!sr z>$Yp*@d4H3kIviLYTLGc=8KOS*r~ofmicGIqRyGJ!Cp|2qP48Z{qa+OWy{CdN_)a- zPObI;`GqloHJLICHZGyu2}Qu4?y(gGXzo$hWeyZ^5_s$?^U!aoQgI=CMvlwlh|R$# zz+!TDE!C%`ZdEaH-*eDmw64y5XdQvdZzLAXjl`Y*_??h3FZJS82)uMj*4?4)IMqgQ zDUJtH;=tP#%gWQm*AAS|H5cQ_#FN6H;aB|f?nF~mxh`Ru+h10Cm7t&wm!0Oz%ImD9 zg}2Z;=D>$hjVu7<_e{(&{CFS#(;aHoyNL+fIj*gicatv-2O=GhhPRr|qSk*lXx8?> zNv3}}WONwN?XlXMY(kC-Ep8-tA^0;cgEK%7sAhgwX>M_(9JKGd^YRtoOGO!-wV<1D z9gFp!jCbw-1g@6*9JA;g?pGXv@G<*X1YGmxRvN4QemSABCEii(344+^FIlYjTlvIK zl1tS?R9`7fsDQXuE+mMO4>cPufSafGJQU5}@+@`tXaB2XdTM$4KheU0xCSnAHr&VJ zWT9v+nd-}I_8n#mu%DLK$lpGGx4h!yYGPLj-qu>F){I+mS;5|hzRbM6`M&m8Qe9%v zeV~VLf7eri0J9e5SwnE3utfQ=R>Hr6K(C16M#b6}LIX; z4IGlzR>>K0Jb+-}r|@ClN%-bw>#ZR2n*3U=<;Z$!U6i@svTOa4g5~4(2-sX}X2@9FYE+=aq%|&WD((=9+P7WEizdOIZhS9bcAD+3Z zmP{Ed2vaWYABz`NIb|JYRA(jA60c%Xo>D!&9Zeyb`(Zl>LoMm%N~{Htvz+qQ0kK;UZJd zHkTMwH~X?d^_k89Q`zi@$l15l>rL((h_Ye0C)s^kDKTi<=kxw$tK#PxQUT|Rh~8M% zRZ*fvF^IXep8**xtdT|LHY}tNU}_zGJ9E27MmT||&f{pi*KT>6Y*$9KFxM^!6C>Z@ zRBD#CmeWXR`o$n2g!A%s4sTtHR=VNl%)+?VT8$WZ_p_I*o4q;L&@DM%tc1EZG*%OK zZJS4T5XOSjA>}p_5J-u3$BGNdUjlG{-LKYT?P5mxXgO9tt7qqzt&3@0m^s2!@+@qU zevRUgr{?PY-u59~@lby(M%aqjJ-1^a^~lgKcqYT$upGWndx(-m_wCJ2C}9rP?XC9F z6LhPyW2)PpYdHnxROg^($L`g5?kFs%gyF^*6{a4q1(ezg?a*AJGhW!a#D@E|@1Jeh zKdqmZdF7RVRWx&;slr*9p7`aii@X8ClFee1NMM^xi_vW!s)t@WeEd_mEVPhTY4jO) zE53ig$}%p-#TVaAtGTqA@mucGOeK#%h?~xFO|<6C1z7~Ru@g0#zWw^1MwSv33$5Ol ze%-qxttqSYED0@JZ5Io=)J$7!Mtr%O?(7zZ4*wdN0bsj+wRvmXb2D1WY6c}`$IlgC+wpg{ix6+D@krU{c?e=TDF3q2 z@`c!OoJMxoKUQxZ?Lq+D36$pVLm2TFFrF@oV|Vs9GiqpJLXLJMVf%2jyAj{{UdV2U zw(pqN^mcOGRCl!pZ_9X7wF|Q}hr}H{=gHDXLGg2xr)S$6?2EQ`wC3Sj^Oo|ei?*yj zO|9b!lDVPsz4Af=$<~y>;tNxACRrw=qr6Mm=;0n!RBMYbSHt#`YDTGIKOM#2n}mYu zw4sJjr2}<;@TNb#P{?!d&S*fJ1JcjBL92N71A03cZW;Ftyqc^hxxEFJx06@*41`J7 z!v*=sQ*E2FiD;Kl$_sikuOEl9quhY@sk7SyK>NflQ$LSjqDf=nyE7KZj;@;PLB{Nd z5kR~CmsV2Y_u@yH_4#q#poHw;3U`Ija3&mZl`iR(Ja?C{c_B`3C?Y;s)0f>v%@Az; zT5*0ct=oOa@55h~1E=b@EO+)*1lLFV9U3i%Q{P9f2`QKHi{!@nI8D9Sxn%0d=;naV zSEpsc-VLKe^4G1h=(rtgX5#lO58}w+!#`5(n3^?@KF@IJY9YNDOHHRnCNQr#;|*PZ zjcOnc4$in+v#hU4MDO1jQcbTwn!p;bmA{>6k(p7kukH`a9p0%<+)AOG;W5KCv^{-#@|{=87#Z zjT;zf+eK}%RE5V`MqW}}yvTSx38cx*lF#MslS_wNPN*Hwk@uBR8A%`E?su8L^bc_cAZe{+20q6AwkTvHYsoR@{?cy>(A zz~@y4(IY)L7&C}vDeRxRE$(U5-wW#6dxG=3P_)R?1$6Mo^KmrYxn4y`5mks;Gx$;I zkfP*Sn!6?Yg&OW^<`GcC=|wmpQ&X;=HSI>`&=N1>KCILgY%P>m2WSuVWjVQ|wXz1F z)&Y}5wtFJfRjrG8!AEC*qFKOa-bn^-FjaT2r?~r}WY+dJnfZY>mm*X(yn7twYe2(% z0lG+C4(rG%)x8@InK18qU>V0{SOyJW^!19B<|j40-j{LHrLyS+4GXhw*FaPc5M*kQ zijV0|g5qkm?Xev=vr%Mp+REh`%EmJMJkt{h^u^UJ1zyZS#}g;gX7P&L4l@_~&|WI1 z*IZFK@p3=q{7vf4MYGq<7{k=+7Q;!=p2IcPMvrEHl%^6j67}HAqR;lX%T&}t44OPg zJ@UHua>DTu`n88nXvp!f@1nGDu1hDXars7ri4DptL!v%eA7%}Y53J?UuWuGq2u-?5 zOIwp6GyVha-d+2Xn7DDV#H}Hl=yS$lDR5q`PO3gg+ z*}Uc+rE3pi_M^`uIGt78N68nbP`XpXVe{ffuudOHyauv*K-%_#p)+&(9-gPigFCEn zIlS$dQp~NRgdXzmoLt0GwG4j9F&!-^NDAF0_HN4WrU4_Q3Pbp#r#$s7_~9lco&=vI zbKiLZOVilv!OtNN6ZTrn@!VkHR3e6Fxql!tZKx!4Ura9N-lCs(3=#WH&uKn^H0-sl zYXzTrAX2_I{MnduIOLX<7JINre?~hj0Vb;vWE?KLDFl$1AFrezkHxgEhBN+Kvc|3WybWHKJ7cvEpAV_+0PX@!bdt+~geBdT>K?AKRklB= z@(u=t!u6l4qXc;@iN32v)v7x0Ql^*LnkRx2JwLJpI4{%)q%F%lmN+VK`YyvH`PG0y z7@B1TR=ACjnV59H6< zmSxSG86u5|1&Uc(+*L}2p10AhfKieS;V&%JF})z7Yt0WlI&h$@qjLUj8VOeC(z;X0 z+?(Mtc0u*p85Ktp!54|PlS6kM!BQnAYSHrB113XfYTEFXF~Np4ZLnlExkmo*Lyo*7 z?7t}sa+0(z}ruvgoBjN4-HihhCoYRF6M=Rm>5GXSlTod79i*l~T zlXknjI%-W$Qyp(!c9-`yZy<`{(uQFfaXzXRXIN6Q)aT20$^MDn*J+0_&k*n3?o*p% zzeL|0*8Q--)D}TkY4bC)saZZq)|~E4;fex6!2HV1ujhk9CBFgHF*>s|6ud{5?o5^{ z*|2d$&e3|_D^MjOOG}OeV(1$Lqk7*b;XmrzJa#f|*@I+)pnQnkl%q4pzs}O8fG)mV z40#;QF)r;hUcf{2e$Herf_Z_`zU(y1>QX?)(H3Jiy_=;!RX+aMHYaMCgirZq(Rd*gqM6ihwIfv z-1JTa=E4eiOXF<=B&+-gP0d2jgV_xj92_m{QT%~C%)5z5CBv8MQVO@8Tio$3QfWb@ z$%{OqDi~_+c|fOhWmiXaod@b4xiu`jX53{hJ3mCG9UPmN0l51z+(Wp7WfUJp7U=ZPDRE*-Vj3b}s>N*hQbXIMm^F$W-Oz6KvdmLD z2VIN_cdh<`uSiBtpvI@ss*&K*FR^T_u~NZIlHTTTnD-b5-*om3moy;DQq}52n<4jl zIZT8T5qZw?xxvcST3|$gSu#Z;JUot(YUmmwdX?L+UUT<7k#48b4bhVv-gecRTd?dHHldy?14wz*W&ihV2 zd;4R~uix`VyoJ}jrA;Hxbe8DhV~62H^%Z#^=t7x}oTT7ne+=ljP~=P8T(L`LQ?Yv- zbr1Dn0e0;&XEwUrC|l_?t^Boe0)L&ri~f7(4-@~)(uWPVmQ>zdr?>j#qx%fPM7mn7 zBIvSo@AZ+nKML6nheW)j2<+Bm>22PUGg@nT>_2|<4Eh#YNF8oZCedC@DSz{3Ijt@* zeU}^YCC{+gY(X(NDUos>0}ed2!po>1Ax4q{X>{T>{l`x>I^bYm_;#60o5#BNV+k5u ze2#O=Lm(D_iw(e5g99nHAr&5P}HMf#)qJrpfLl+U3(#ZN&j*BQ45E})S z`Zd)`h!+(yB<@R{jhgoAcICX%A=X6&{H3$Y9y_?omGzM4`f-8QE3mK<9B}scdtxIN zX!}SH^ZNNwj`3eX=*2lqTO&=?vt60{-PCYB6O+*zTC@2rj|S*6@-7kOcC{ujzcvWhar_WP`KC0Bl?%;@ zZI-rvOg+PNdOk;D8r$q%>;0MC*5U)VPA>k(!?0ksia+mpx=UW^^;uQ;RZtZ+68wNCB+?QY$5Y>LjM z{xu+|d?-%fVZci%4kfF0s96XtK4)5pO}Z5B@-S(HcSy8}%NeT*QaI!DJ;XHh^uXBt zS&M*0-q|f6Z83Akr3F7x%e679*6RG(dj4K$=>|&FKgR10TZbr|} zGnE%1u9b)A*sAZ$2CBGOSWFZzK1gh{h#ubwHn#YMN>$Y^h4lgUrJW+)mO ze)j9*mE8lG_doVPM=A$e z>5?EV+Q+dO0I3%q5&#d^*mGIbiKz`3^FqbwgRceS-PGUVwTBg0u3AsATo_zt)y|%VY<|Hqf(aolvML8DNsh z!CFNKId*=HpMTz$78A5I$mOQ%K28I|Hr~_NG;UA6oar8W&bjvPli%Y-u<&7_l_g2S z^m-X>**S$IL#b>6e_Ma(PXcje?e}i+q*iF0BEw`9K>>kNQNLNC4QGiPRgZBY*~l4HcioI$ zdJbsqB-Pt0@P)H-5@$6Rj*PRyjJ8^4R^~4XzCW4llb-AJCUuz4EnP8fn7v8wWT{4u zqTARs{-}uobY5}l{`0`PDxz9+lmfME%CbMu@*)|1=LeS9E6x)5jsm_{dS5SvhGT7` zdC(J20c8MHz%#>(_jv30npv5h8TN1dvLE}(x_OXl1tnRjK7EpUR!xA4Yc4LTBjfJJ z*eJQD=?XbMvWhb@jpboVKxZOT_GC+jE@e&-zrKSXEQQSO7X{6(K+SS%&3D=I&9cxAYF;6HdQ@bQqz4m57OnX%iN{wGn4-K#l z=H$3wi^?0z8l5MGAG?4?!n=%xea}q{x>J7S(|WdcDFPbWo%%RNk;f0&>-1sNT_L}r zLYK$Hl>*zRzetbAGCh<@;ww_Eh%sdoBl2c|kPWqK4yHWli&9?4T6nrPH%o2Q-_vO5 zNtEyrf_t2MqsU!cetNdRMPJ1u@U?{cEkW;E66a`^BRVa3>6^nA3QD%SO^wpYQ1|?( zM?HQd=DqYla=1cozX;Og0DV3&9*p=szlI(KL7AgKIoAX-#~ZG=xRRFQO>oBA#<=fq z(^vvEb4>2|kTu_nQh}1+8saf%#%O2BCV`jIzvj63b@xBz9Bo?K{;no&off zJz^N$E%K|ej6s2RX~kM_%ptS;VB>U)Im;nsbrO;R03Rv;Vfpn&_|B(07e(uH8uD&B zmkSuX%*PejV`YX)odz@|`b4Dqc$00O+NX)Ql$#Y5=NEhnkJDN&dV7)f&Mzh5_RRy^ z^~)qKhZk>bl>42Wy4l`~xW1cV82=|7yA2X?L}E?Bst^X$l?5IdyP(2ZTg$LawIw zv8}3&AiL#Au%q>t(?LS9CHd{fax1^Lyo>}Wa@(BZ*Xs)cSw`6hWRW)O*fn3GbdS8d?X&x8=~^6Yx?$*vrmFwm zw*yqYFLW$#NTYKrZ|$6$5<+(iI3`a9`vx7$FTK4SR4v+~yqjt1Hx4FucWFAl7OCVK zZO&os{Uu}gg)@M7JaPEqC{_`%*TGSL`DNr)nP3?kgqvO5j`ws}$S<=1TDNi|7=7Pa zruaO*Bjr}&I|jLVbK>XYro?v-#1Q=>8lI#et;dM&*u}S1?k3SLEW-h7m7r0LBU_2` z8R5R@07-8HUpnnTVN$ax#kQ`6v3I@CEje?y@4-RR8Anxa(U*7-a4Z|6>la;L3cR9H(_^%cwNXlnb=mDOF( zrSn6&60|M1$rW0Khf;XTkjUzB zjnfb?`S9GIWjJ!n?x0+5d%Ehmec0}DwVN}$NO;A;TKw@^oSi1u>!RW}n^Bj!ETMkm zcMlSzt_B`|?;_O2VuQTq3e%LAn@C@1@{zb;UfafnH$j@G%Rjbn2EtVwwpPT0dMh5@|5M6AAs9^8w4>xU-#9Ep@Qkj1 z4g}XZOj)GH9dHY_6f*B9=^kt=H($S#DkT{zcxHGWVT-l7LAy_-&}NO zY_`JZvm%o72zWZkx8WmMjjH9xWQX`IEgcuZK@KsyKmAp@(M=@KZ|{mAU!o0m!y|Q} zv+;7s%Esm38lR?vg~Pe1c&|gaJ*OlKITbLt_Quk8s&ceNeIv0HyKk*Qs0Itx_7&17 zPuQ8XrTK=1_z|S-{rKkajLrRaBxD#D?o2hZ@fR3!kd@(y{8NR4z8Irp-J2yaH$Hn* zK;H+VjQkm`Q@s!fjxA7DV9r5W^Dp4=$DNvUA2U%mQ0S~wm`s|4TL9;M+8B`zdCOnE~HC&KmC~WdY#yip8%aC zMyltN%sb9&)+$P$-7ThUG^Dd!4Yw=_VHig&n}NpHl=}C)5$%lb`wiGJCGJwnkAz~w z$RIwX@NY;Jmd5LcXZ|AF(IYO`-B^*d%#cat`s1!lrS#l%XWOxzH=)%wadz5V0shXu z*OE3MQ|xgxT3sLgxgb8fE(M0G@(0}5oA3UW7W>Ad0DaDPXEE~aT08yyGJ1mbm z`>GvoQReGa8G5_)nxxPGhqFnRqZ$Z}I1`(mejbgkRq{+3ZK#OM?@lJ~xi(yUY4#DS zL0)bu-#fLCdqYJ$U^wl1^0)ZevlH`x52N& z6VE*3th0EcUc#f`ZZh?@bRwZy{M&VprJ=&ne*3|@Ez+V#vMYTKO+DLFu2*69Xs*aZ z_T$u%(Zr5#ScSQ-2Gn;j7^<=`yc$-DSJz~%Rti`*V@}~pzj{}* z@C~Rj$k%n@=O~Xxgwkl`_Vz5>VoV1+38A0=^(WtQlOdhzwP+X^w_4%7MQlm4p457o zRms{~^RmSehP>KEZ|b3NoENwpRU+=;ByhdoIpVVH;?HfXpN*qIu?{L6bBheFHC{q5a z+AVFU7F>tz`ft1Y>$AkhXBDsVIeII#87Sw2T zBn9{NDvz%{-+;)kViv?+SA4DuUnEy0SK)TY>q-gulo?3=p(nfBXAP!8;XsQLRWq5b zO4g|fn*CQg-Jcc8(TTVu_d3Ol!mb2Foy*|CXD08Tca*B*@`!I~@=YWTxBzB&Az;w% zo`;e_EvIhC#zGSK{6ui_sulAs&JEaGx!^WP!**>BYl}8$qY_dgJg(^6JE9GmlnV$U zZ%_EFwM_(1woM!ph9q7bRH0|aa>!d&^KPP!cE3O{F{`<}pxxs^PdXZB7NA=Aq_0_P zFl71Lm8VSHXjZ(E&{^)XRrh8~9riRKF;Vb_ZgDk%#OO5YTlHjrKAnXt{djywgVMs# z`(`$_uRDL7!r5O~b=S|q?hjZ-q#4n9CUbx8gG6vrdT^j+%xFV^t$J_c1F<vfh7_flo}t0Unz9H<@&_ByELP$g)wb2YBTw|sTqwAjt}@Wrwzc8O(6)0>5L z%s;*0Y^p}psB|&Blpq+eZ$TQ-olmeJn-xfmzAf&Z8S0Rab=nwS8q|C{YME@Y&;6{?g8P{G_s( zjqoK@_aU$2(bvu0c%gEn@_NvHM*4@+CkuUAdv+}ntonhyfa#0hFA7M*#kTQNGBqgm;r2 z-wCty4>a>A_Bfc}PN(H@{SvgF`0f>&IrNqNd$uQiW=B?2a1PG*``Q6({m6o+Rkem{y*FY8A?O%Fh|B8 z%PuNe(-z`<(3DJwZ_J3-GCr43WjqZ8AzM&(G1s;<@>Z`=IXNFs4|p*}!9A)8}sog|MChac|~qz zB#wppNFy!l$INoSA}fbYF+pWV@(1u59@_Sr|2qw>)hWxuXW z<+=~LgMF!AZX;wR948c;D_Gkv?jIy6N2F!*>6iA)}M15~4Q z|G9KIM*Sgp&rgOipQ%<~NE(Xi%N}1 z2-_e^d)8vELGC1Hcm=VgzosW!1@D{Me||gfJp69`(WHMOcuZ?5wf^>L$}mcdw88ff_Ar+}zCgp&d?_3z>C({R9X{ zQZoiq@3)$oJ4kVAS}g8WMqZkX>M0sL)6yvRjj+^HGk;pnX^5v7d6qF5ahrec%k6xR zpSwp@dQWMZSW{~yIa`^VTA5ALAP$gbY~R@Nl4Sr{59zLq)WOmB>Lh1pF)@tXu}T7? zT{Wh<=6galnE2~n2PIpifo*cYxNXWh+G{OUeetUf0i98v{!oxK;Q@%D?327Z;juE| z21xM4`jX`?vf6uv-|rbLZT}Q%$&uKXznDP{xk%p+wJukCp1<6`a66qj*+N_qTdW-u zooRg7n_}Y$>M0c%C6c(2Ou5;vrBd&u9eaxp{X{jKBs4m;%O)seuQQ)!{l=GCPa-Kg zwwuNhP2j}r5^BedUYUe~x)gn_|1`*Nq#icqyj=sTZatfIe6_fZudWK%+8)c-ZtK-S zHdXeeRQh=T7*ujE#^jN96$(QT=j-mqJd|F{+Nz$|-ZJ$1sjKBV6Z#HXO3b$s(>kd4 zO6U6Q4ST4-PqOHzuXwClzp^L9W$l!;TEFE<#>}7f>h;Z}JNN}=JzD*mL1lT#&4<6# zIwWBd{6OO`&>SO1h4Wa8Jfg2XTE6mnVCJqzd2{Ra->~I)H1{Y!IBY%lhT+d<*3_xz zVr%Nd^hbsll;AI>2uIyR-fOH8c3t#ig}BzPg&#hgnkD#k|-6$MfsNW<176 zC;O~!FW_uQnm0+vO&Bg_H@zcvJ=rxCVz{{tcbtzbv`!Qv20zq8Nb$zI#K#b@A?gG; znPIf0YtZK6H)xVqh-)I&`ABkZ1J7p2g|HDfED&ARPUV3)J{e-_h32Xj8c8m@eI!k3 zNi@C2z@yL--sbILGyeyB|KZhSwlx6bIF2$iD$ZO52BlmTL8U1ONX;mt4AN|L0w^U? z5`@q~AlQIW1e7WSh^T<{5+H<{qXhJ-E6_(_D6Y`SOVK| ztbLt|Tn@l*R&tX{FirdUs>02-Cy0efissz#anVoUM0CN-D6>ocmKW1%@u<1^{SYtU z@qF>v(f!}pX6}kZ@oIGnYPLej?-369wz$d2X_Nlu~ojdoWS9Dzwt_3d4`m5L6{Bxr-p2umBD#FO0Lx z;Ph~4<71Q1@`{d!V-otnZ1%Bcmtk9a&{!ZXYh@Zk z&o{U3eAuL8hRfXIc2}5|lXZJ!Q~E5U_xS*t_9^k1B3IYcVgky|eKQwEJ-}-|4Vmp! zPCMO*j{vOse#jH(x#h#?6`vLXmTRO1VQs&AKE7T*&)dXE^qe{+&UiF+cs5yAKGOlN za-`4v*tkWKozk6Bf7IQXZ)M7gS8T~RXggLM(;Fo$7M8e&s#TQc! z%4-^$vH2#68gu;(^QQ2Xv_qi_YJ<`ELTJNTIsNx7hxp^^M0xW!D|H0dGl#Quqqco$ zRpM%|dU)*&>+Xkp4#3(X0uz_&5F4}Yz{XB(GWM;9u=YS0`Hu=Teke}QSf`To=;KO{ zJr|c>o^h_=3W#jNxV&_iI>S4xe3~hv%~Yx$GaO_sWaYdP`eqeF zW^$rbH+Om~Z;y4N0pm6J(W<1h;{0e~hS+MgbRcPL#PflPZmUG3=GUy-*QM+1pVd;| z?!47K{jTk(hQ!BvHOpCY#2}yjDYLA369w`>i>IU8mksGzY$FmTM#&}FVt^UcPMM2e5~>B8JSCy{;|(1<8|g7x4Z+DbRt96?*)-A zZ>L*5nn5}mQiw{%jt6H*F)i~!GfSFe#${qiLpiHd!PCc7+;PQedAb@ zQSD$&fl~H!y?(mSwf#co)Rq;38PZE#xFwI0om2pE%cK;@DY$MD-dl3Z5!21}BQ{c~kj z+5gf-Ox;d$h91PLWwigPPV!98-B3F!Ew4>olI!GG%zkqN977x$T-4S%buA?rv{Y&K z)2m``hFai?sWksU>mj+!Uu1F$T%A|^m$1IXAn!&y|E5D-$xH51L8=;=fLo-C@-<|_ zEtfV_QU+F0A3lH2qwo~_o8M^7$?uJu)T4V51GogW$$9(LqAG%2>c$u>`Ifz)K#!Z8 zI+0m$*xT5C_M$d-6`Z0)6U&5d3{kV?a7s}wp7oaMoOYT>8! z#paiO7o2bBT8_DYU~5yuSC?|=an43ueLgGU5XRDMQVtUXj)UUPEj}~=aI3q4*Y*l? zIlG-hDYiS3EZp5_d*Rm2(*}S%&~Sd1tu&cKklZn37ie4gq(ZT&=sz zoW`$CZE-KRWhan4Y2&Awx}G$vm|$sisoFc+?!{T9{r=p3)eBpX4C_)lNQaKoOjw@$ zdkHp8+TkAa1;PE^KPJSgNjKBF-x9*TsH2VGt$;NbwSxs^G;JgE>~z|(k-_gBPj_t^ z9!SuzBB#VWh}2rhN&n*;}+PU~0yj+5{8nanyi$9xv zwAO(`3&rl+`012ZNuwDA&kQjmDt4?Ja%L7=`lRMw9~#k!zHY!w!;&un$INC@UFp8k z#;*#a^9pn3tq*qRC8pPB1}|bv&aIu9uQkE+KV!XPufxqp1SNnYX6uc8jA#21tt4Dd z_o?Zm_izcmz2}YUzMcd6cjJ+pTU5fu2|L_EvTd z0JqLQ%iZW`gCSXz-(S|lYbT1TBO6!mk-VfC$LY5-|FW!*uR zP>VArA4m6*nEBct=5w0FvoH!wTPb;D0sV5B;(YCG%)YyW4&>QKxd+H*9V>>yz_EiB z0o?WL{k7Be^Hng$Ra)mT+;QaM@QClpT>OUv34=rG7x$hv^9h+GsCpQmvj#P+&_Lez zc@VderTW{sr|w;-9`j~og|9wb0?4N&{YiZ;+4gA|(x0=F&|ZE=4S~^xeA|4XVuWf# zAuk@$w7Qn)$SIChojXaKi@X2g*V?15f2IT4s7)X1W%F&?T%<)=li$4h!ccGn)0{Y+ zVYoH;_GBZAUKwlNRuvA#K#x~FQR(v4P-P=|PS%Id$~9BYQBpm}rJ?ETD$G`ObLoK= zr&h+;J@g>=r9yIBj>}-|^dbWPEN19$<+Y+j>iM1CKvJ}>M&q~i;;1nA@;ZMbaU|Vp zdExaF4)3>sXNp*@;*8|xeB+Qv7;MXV-a{~fjVd&!IN>I;qSR)KHsc0h+}mvQo{h6< zKXSn@F~+whL!T~u=&IWmpEa-}-cZo-oiJ*PfL}Jvx#q+*7v6fvo_8widYOlS8Z5ms z*&4!l?5x8zJl2V^!x#_3q&n{p>>iw2;9P-uffulX$2H_%pEz{)dD=pS`4;PrurXO4 zIQB`59B-DnSiST8YJvgt)kL~^XnegzIB)DAD&)=@_{743ltiauN8?SrfJMrFHL9mD zmKSt|TNevw!CO|75Y1~Jr%4!lDv}Idu%5x_suBgv-se^zm0RUGc`v&+`(!4l;f^UA zAsx$k4~OeM#f6d3H@LG0WDJAsmDJWxihOHBm~{|rrAelLP*#t*O4X?wOf4u>uL?e* z={Glx*daxuc-MQ4kqW)#JKwM0)tD{RmHxhvJJ~dkMIobt68ov=Ri7;T5N?nCNd(z0Tu*yp%Y!Ht%TOn^zRMee1WkO4=>l z{%_sKF?Ob^#9ptYCG6PbSVi;xcrJvTolSAsGH=m5l=+i?n$tM&DSw`*>%pP!+w+vv z;Fkv0ElbqW!1w5Vedl=@mo83E&r4`DOG4b%oQa89rkYimZADdA9iXlQA1NziQcfj+ z&MJeQ#&PyT&+L9)lasVbXxlXv6 zRKMEHUiU2y zT)XF4unfPgcBNu9BnFJHhPVz11&!6-MsUJ4*?*)O>hsZ`B zkV(t)_|?Z#W`}86mnF$y&E-jc?cn@*GksW1%VU=EBGZABW0^xJUBPPuGuk0k2~sS% z7Q(GBwoTs6?AQZcSI~tLi^#s*~jDOaCC5}s_SDZ zH22DKWZn7%WG&`r`zMxFX7lk{TcxvH-{ak7zFwFG(q()pzWlx!;OYraN6B9B@w@xr z(LV5ubign1fPdbp`RduU(Ob)jr9agg>+d~QX13Z{&aOEi4p~Tw(Zv~`F@=JK(?nQf z9&d%A5=Gj`WA#(y18>|#JIL&3%x-6T|Dw;$bB?-g(jLT(xS#vv63H7akQi;)Y3mzd z@Q)wYe^Kj9CLL2>*U|L9h@?LR+~M*_`kQ`VPNyYq7C-BPEfD57*{(I${mpPnWX;2W zGT3Jsty?fMT(*CTRBwS`HCfot=_r*OhWVm`sK4k5U-iGGpw70XVJO70$13AAb{i3L zrRCX8?Aq{?bb6hZeEZXy&PDn#`KBwVgkU~fEgsnUES>J^FI6uyNJzKa*(s*p^6{&? z)Ree$lh6Q7vLH7>B6UYJEYCnJ?F{sim8=Tu;=?$r2kWPbveq|}+40A(E0^Myb4;xC zv`sI{z{XBu06TZ%MCQ13;QW{!1@+$V*!+nR$X9_SCZ&E_K99D_ZpP2@wvz8} zN31_=O?E){2M~|I7kcuJuzX$H^Jv?so~FC^uUV)YMoql1IHsj)O^b}&Z{z36#BNI) zgK5F=;RE>frA1AT2)g@Xt6`1`nSdue0WVszc^d0o>`U3f{YL5ZP4lvzR6B6Wbh#R+ z83!mH=`6B>i+Wax0WX@0no7m5W_JeS)x~8~nhX>>UQpN2#q5u-KW}AdpF8Zl({<4C zcky&T#2@&r!cxvG3i zqr_K2@9oj+Swr0%ecvlF6RxASISuM<7_IYm5{>8DT*HJ{`gMXlD&v-(QsGk$dhTC=9F#1D+05C`H7fl*fb*_l>&~Q z&iMK}y*m8AgUKd=pXE0wZF9)7Zv2xNyZtw3Ej!k5;57J-%}?da0YM384qsQb0@70~ zOc<2`T*iw7;&k3OWbpWhgLtNf47H9~L!QKd&Nn&fK5fwGVv==!3Chqu(JU{QV(yQq zJzwF|q^{i{mreH1Q4E#|oJfR|jzmY9PmCViX-U*tp>H)rN*IOb-f&2_j?@g_PQ34u>bKGEEy|7iJr@`C34)Z!XLcY(Dr5pem;gLP0@ zk9H`rH|T-q@Cr5T_Svj@7tGS=&mKO7BF~iqCh3$8%>(~b=Xk{YlQvr_h?~21ZaLjo zy%Deamk!k!tMTDOq-Xl{xn+&Q%qCE_$@LSH9)N+j1q5I+>^#4IEDr2{s4&}K)e|BY zYIH_it-VZ11}(l5P?TwYL%Rq zzGJrySfQV4^_Jpe$l00eAh$#>j(Gj-o9XLt>jegeqH*aoZFBYGLD{h2{Vstn#@wbw ztvb~i8l&Ca95bMXn|_riNKg7jc1)*t@YrmjUFAXOos!$IS?OLSo}-L^&%|6u5+tI zF6T0la0auzV8&}r6ratc$d^TRzdOv}JYvudLQ|LwKO#H?x{2WT9&DT`iCF%^Vx$23 zBR&-55>9QxuYOjTr7BA`D%Ufsn?Fl~9p0De&1~+KGRzOL-=E_mEZDIFM2yyciO#+8i}bg$QCn{g`fgfyPZ*iXI}==o z0C8T=dR*Z%zy0ZK`o$WxYJlR#aV1(SDTWW|HZpezz|C>@djy<^vH{eSu!2L5Z_D!I zE+Q>rsF7eR9>+}Y+ais;THummZcbTUr5T=Wb6tIKEyWVi1PWfiLecD)Gf?j|OQ&XC zHi`9g4sd;2T@I(81&liA0`AEZu4>SqF1iyss)9|bk)Yb1BmeY?c8z?IXmgZ$Hd3fYY856uLksAI@?B8KLp6hMA6YW$R3KvweP(!2p z76`P%nakTzUeLW_NROG}okOb6FSL(OTU4%u$U!YF)MHefVfGX`y~w zH*gG(Az4XgU(ILqtqC=hwVnHj(6;p@$Ec*`?m~CXyN*dv;wwr5LfbS)@LSb}uI5}l zIn8Pm@K=#&ozkeC{>MkU^h+^0i`7mm21&AEt50|xIeDwC#K__ZZhkS`Z?${c+j_>d zEh`5}Gw)v;gxOa}2Z!ZZp4g|8VfQQb}k|kNM zaPnkrT`DFjG3!RH+VFexM765LGQrwVbLjnp652467G}YR)tdWTv_7{lhd|wDxf*lS z0mcW+?B^#^zx14eDU|y5duDSEkoL&o_<#y9{c!!9i`Tc(%X;(ONUeJOh_RODj5K7n zzd&koXThhyBOIJbB|N9>JSG)r$ z9&Gn?RSTX|4Ts;b!hfCI_bD-E>CXm1dkZgS9M~UotyO4HagtlDJ?r%M@l2o zj?t#g=tOMr_PTIupUjOuo1mdp&ftZyZ$(f|$YfUQI2}`Wkv7J3lI%(~U`-te1mf6c zX%_&?U)m_#tS5XWt96@6sV0A5#s86BBfWN)a4DMnPHE%Pc{}ifKc!%lV*yU;=ytdmt35I@d%q z&c(b@s{?PpO=3sdxLLxcAgrSoX6UO`5}y zF~;Oc$%eglC77#o)o?3tC+t{g2j#Ua7(P>U8nd`=GzoGci`8fL9 zG-DyV`Xq5py)WEwCdD}3Qkfg{L#r7<-iskxnrX_u7jArPPR$hIfIleI<-GLryR`w{a9^QaQ(&~ zjGE4b(9<1T4fGOM=agotOm6GU)->UziiSsZ)Nfwc7X1wq?&lUmNc4sXLo0K3SHl1sMoe5X4b+R}AzXQ2Nj24@?#d+oJaYR0`FWPWRnNM$gX#|!>97>=;K;a3c#o?B9lc*)I}-j8NDeIoc{i|D84T+~oIr$W|a_W@$tNmQFx zO`OEx>PbcMY~BEWp$qxmx|!BrQFiP*SEhae*+tv~m)00!{%%7;Zz>D;iqjD?ZONBd z;S!6Vz!uY6#6YIjDG4{yNTKgYsoL_HcVSx%l9Ze(Rqiu}JdP z!{4WojjJ>Vp`o(t%7Bdyzy^<(54h)+p&`a$Ryk1&-g8HMOb@#2*f^g|v8F30I^Vgu z&4KMrGpf$$=fHW-^xobzI5SeO)U+jsc2zqHP|)(27rJVsxLIN>a+|$sW4|a@#Vtz} zjsxD^hIYW^5=3QwaYpt5->mk(d1t^qmQM?f%IwPg@t4})+p-h8%8%u^=LYAz&xda< z4f#)zOM&cSH6xm59hPoQVV#k94@y^d{Ic zoEh;)Pg8ZTTv!(>Zjw#oIkS;wCfM43o9wdhX_|U zXsosdSm$kY5M}wJbC}4LIv{mG|2y5>uFJ}d&(^S4>MfCjhyyvwc?=(Fo4Qb4x3p{( z*_tRZdfEuRS=La!?aoqJTux){1l-u4%^h2(1pUH#Q7eZXFm8BXI5fEZGA@u|3F^6- z=WcVmEj1z3(!9!6aH0G%PFr%^S9=&eps;C+EijscH?M2F{j4 zfl151il5RWYxt65ksi^7DlE;{TW*8lq^t;VaBzlkz-0zL+CO3kGph%Pz0E; zbm=vI$?{kvW0|y58a&Xc|NU^ab-EQv4gUjn!TOBVPek`08-zA|HHR1?9Zb3rZfN*k;WJJ0f8GDn(+7RkDL}svpRy z7P6U_@_J0-kk1jR*=MnJ+|6@>`a3+s}zdhsinCxpb73Ej{pyaGsf9>u1kaLO6>Q|WS7O%%vHbdsCQEjTL zFiv2f1%%9wVmW5_2Tw@t7&zS@p5bajmmhDd8Hd;r<)qOJHzqA-TR!A-oUafwk?%f_}H;f;2v8Omsb=XGPA527z;(- z4==|dhW&{s9dKx}ML9ZWwdROsZpalEdEs>!a$bY9lc30JjU}XE6e2tUm{sCMlrn~8 zyodE<-OKRQQ5sR;Mmw)MCRsbD6!eUj)`_V&SfX{jDYceB^S_>tKGz-`Df;6<@{kEQ zpLjvlWM|DUCcc=o9%@QYeb#Xe4NY>H&><~H0;|ic&(GgHFIKeB%JA>xE*qSl_EkO_ zo7SNN#`TngHo8QyiCP*ln8^*x+g_F(8HaO{ADKjfWgYLy`RLD>ZfE5Ta18S< zeJ@u0Jq3Y%pH!f5>T^%~(xGbaQ^0$FJryy% z0RVp5EXCXq$l%YuhzDPY>~dZC&QtyedjR@YX~jAF$1RO)zZ-*Ko8TX%1OuhhF_P0_ z6-DgqP?8o<9j^PeUW0wiknMI2B|q4A0d__5M^Q1BkNvAavx~w^DGMXZB}nf zQLBy!^op7B8p-?a8(zmn7vqZ_`?W^@WG~+zJ1U=7T8g?=#SfP{6$r+mah~>S)vgVt z2MB-nR`SSkXn43sL*s?4o+jYJnFs2RA%OdbAU)LXOt775^Ywu;sO5OIL$ST+=HoZ} zW=ZID2Q0&5vJug8B8=ueQ6D^B1O5NSVmK{FyK2~xMFC|Gr9efsrQPPWgCH7t z=B8$5>gfMd{I6tc5(i1B_8B^-4bA$}Qk0u-i3a{F+8xa8#~QWhN!jgbL4Llapq660 z`76+w=y8kZ5)Q$X%+NZlMG$7_Pv|een{UD$aj@)KDCND$c8K>#trAddZO*0rTtV>S zaH02r5_5cb_Y=x+$lv$J|JL+>-BwH|7Z*=b89KSXUJoF+^V-@_=c@|4gZ}5&!x_P3 zfR@?#aNTn~73=@CG`lVCS34@oQ)(o$-02m}ztOz=$qiV|53pL;gg$cw=A_yjID}%> z);ImV?teA8TVs~3{Zx0~y#1t~?|(-8>*XS(=6a6y_X|Eg<40rPoC98d_y>)w%OC$~-``muyKO3dDE54ymA8xr3PGC1a+v+V;`)qN_nm$v z=CznvGesQ_~G`%4EL$3DQ(B_p}P0e>s%Vh_(9+5J{@rh_iX z-!dl|r!?%z?nboS6-#>;JPgZ@AAu&jfPMONORcj0fwb$%Zlz0=FUSFg*9m?$ z;D?8)Z%i3FV?hF)`C2RVH=rWgf1a*4!Ih#vht`*?@iLnCuE`%aHo1SqO;XdVGs|$b zwcC(Gl#$UIF?(_yf%q0Lg9sn&w!j#bU;1a9-wx~= zeK)lIt+JA%njF!uSVNMrVqust-~KGVuRU}}chXtn?|D>Q+|}&ty|fl*86f|j?kH2T z#RQcHAZrpbtBmmOE;%T3;nZ!&0^x6?RjfksukR?i7j%))smMe^0Nfi# zT+1hA+R6T7y)0=hq9TlD&TL|EJ@lyNIr^*YYBFC7*(LjH~XE6I&3ilr;fB zS_7@*A(>>TD^*@d8|EICh#%#Am=82bk ze;ddr=cE^ylDZ9fsK1f$@UK~2MNBarWORdJfX_uzX}G6L>z$X>|Cj>4io4UvTyK3^ z|KjxxJJr3vB~RuUDwijq!K(k5X(a)FEol3faXey&3AjDPRQoGJy%^bN;mO^jH)HHxjD)u90YQ`;c^vdmBPFI7AZDy4V(FPzm<8{dB_0vIwu?H^)6xJd7N1BBVFah&ex*kjMk^X@$mLWUh!fM3VF`3ACRY7=mjy_=95XNwB11ze8*dGk44ng(mt4&12Eua?`X@VxC;OnXxY+xn zFRA6$4G|u3A!D_s=pdhwbHQ-C_+E$d4WBCW@({p~zf=A~Ax+$ruOgH;QnV%tEC4TT zP2CS4kNGy$GB? z$O~GGZdLXD71xTq!O6^q$bT!0hm9=N%;(G#TXyFA3XuU9?A_zDIacz&?zqVx-w2B# z2r!M8mh-xE8hrRsCm*IlnL55BEYtN5w8TqpT|1%_;FcC(#+iz{+~u8!Gp|l*J*P_+ z*(AvXcYs`TGH4PxwzB)(ej!vi0jJN}I1aeT;_hr*ph;Gwu7iP5!w(S^flU{@Jz!xw zWV|5HfK4?ZOUCZGf32ZroZbrsWs{i??nun9&3`*z@%*)xztzq<^$?yOO<|L#xYDtE z?4L&J(v&fRkH6@J6u9>nLJPi~FX8ws`oINn-Pz%E7@2s@@Qw(AE$$@9l=;9TVPf@l zweR%FG=L$l?NaRh^*N!J2OtsdA)-Gx6x+lQ4V0xPv`)WMF3lqDaSQG%1TSW7&j(`# zt-g7%)0TyDzU<~pNm4Nf-GQU>N{|D7*DV#>?mR|riM-GPR#*_ONZ%tk%LooUqXP%t#$ z*N3MotSG_kn#0*I{aKvSb!W&lAh5(9?h@C=HH!~j2mHzB((L}ckUUGDaZ>WSHqptH z5U{zW|Bu&S?0?7%gq6A=MtQQA@jvm(iITa$aeV=B=&iO2>-XUqhErR6x`< zao%_=!BO@*+n0zn1|3KxY`0cux(J^IxZ&Q47X`nvh+IZbq3p;RQVHB@N=R9i(#Ol}jW~0zGB+l^> zkGMlVRW}(m{e|G-lA(H=fSV78`Wq2zT9F+73$(uOsaA4_x%-0Vdb~r%MR%8gKCkSx z!r+1OP)au%_wDf(y3{o}JG@pUE!W2d$=Wzrw@u-V{2BY)gtUO~X`OEfp!rdP(SGBe z1Ae`CpH%x}LmXRB`zpL%Yem77$XQtz23ox~zf2*Pvl&|JrNX1qPVdc)IRg=_Q_7NE z9|Iq-n`mT>99er+-C}pkw)I6`t{-tqyowi&h5*bB*aU4QLq4V7-ij6H0ZR&KWAie0 z+pud^$158b(i+9-6L%WT;m=XaBJm$z)o5L=-wxn9Ar5j_!y zXchbklhd^*sFfSgSMT0mA~WmhlHZe=7BD~~9Z#NYk7E&1gpGnbzv?Zw8Z>iPY3u8ExUQS zGa$;~M6#Eax`diXqNQSHu^YEkZs+D*9@{mMEQ(fLs(uNrhh?7vtz^hn4?W_w zD>VSz>JRoPaBB#wd?K0RN;p+csy2C$MvrJ)Tn2-hnu-|XpBnV0%Fkd$U{5s&n6`>l3tZTep zBU{Tn0Vr-#QW{Az9 zjF#MMP)Fs^2>0f0GFy1?kEBHZkqgZ6>T_v#3;EwV9_jHWS<+pS36ncmUY~KnV%d)s zFUn90nOWSGeIoY#?Z&&q+@*8_kZ>f3S%Ctu@sr6l&?}Rjh_FfYh005VAN&)+0GkAL z7S!@Q{u=$yC_3L%-Pt2_TBnra(aR75gYqf?Oa#iyu9nM&5B!pGQ}^Aa!MRiDr5r3* zaMQT9We>fG0|SiQ!_7Bskzh zp9^|9BMP;eGgxACTlTxOk8#s3-EBKTy(bMkve){sfy2g-qg`UEPgG*~#H_8v0ia9f zuupna#42kHK4A3Rq@pv7U7EXlD871Z09ZG5{jbV;!HFy&Hm3R{PQFPIViaaoNxd)N zEYcNR8EAC-k!9pDVKZ#jHFY5`QxN&H{@8Ftir1=tL4e#(jPy`P}4Uem~5?rOi;f3y>Cu?vsIsfy}AB8e|`xYTguy;<<8m0tmRJy*unbsh*fKZiLzV4Y)$1 z%Y6Z0xt8RCp^Jb7YczoJqLwSRj%j-a5!fT;QeE+co!ld9IP!#g^^X;@ax`uoRhWlF z8+EL3(wE!Eg!(x9{f=`Ycyy}qz^2WQS|E7d_`d*f+m9h3RCM3%6&F zn%i(E*LisUPGL)6l1F*za1Y38{0ur23Xt2I&S5A`pNoRLlVyLB5Ml0+wH5i#)h^H- z9_Kw>IM%$d=oE@c3C&MbNlR9w3ED&1aZk$6rR}koFs|nj@Ql%h%tM{7mo~rg1vSX$ zo^BV^5layKG@U5fWhUBSTHJ`K9>4l!B@qGnHF3a!Wa;H@Wt?8!r4&WPTzo9NbuUD! ztKwGnb}xd*SlN!FD|U!9?`0!SncwK$C!*!6UDY=VWL1WG5+?zHa@uS;d}G-e7*iBT zwvYddigQ~nTnJMcnKh2|E+XcJs>a z+Lvk=5M3-Qx{$p1{Pg`-u7-N&Lx~*jJw3C)E41>N{zSd8eQsCB^R|2Nf;p%DkkVQa zdd>30zkjLt>kr)88MhjRK|! z1Y6I_D{Qa;W$~(L4Y)21bl?j~I;G&>j<{X*rsYS%Knt_$%t~{B_5(S@9@1@p(5@v| zd=}CUShgjPl*q5sy|J^BT{giZQIMnNW=S0F(!l1`iK@%Wu4a2|s{!+gzE$q`(sI~$ z%cIm+0d1>lV>8j{K^>$cr^IYQ?gdTB3Puk?Dzg;z+~Rh48wDr_)x8zpElv93_)`@$ z(R$e_H>P2QBCGY{bhkJfMFckWg(!V~r~^?DF5;xv8pqO9kbDR@ybK#4f_3%@DY`Bld<|e$m zbT4Z8ql)?Lsr+UTQ7`1l(jYe9e!ttkc_2>^wH93lplc=p;sx0Kyp}+lCId^ZT3UBB zkXkYyJP=1Op}@dEhD+)amZI=VVL8|bm>h7sOBZ|m=}4UCo39!gLhYz1W)85Znk!#1 zkXV=jz)MU1EmG8pTZ0WiRRp`D~L=_oSMi%23o<$%S8lv72VKa|XXS)UY<=WD-ZOF89V9*untLjfy1a3L#=1P}N%kUC=AxJHUx; z$*zhkd>#+I*53ehB{F^SZ-j{=Y$qMyfHblxY7r+$kXA!9v5`NryU$6xZm5#GfIEfM zNPnG<7t%9YZY9mE?;WuutNZT41Eo}V&Wo!@RQ;AjWn*N3V;q(x*>R? z;+`|(1?4JfZqb_)SW6Z}_<%J~1=y5;FUB_MkbV%!8>!QzF!dVnDV6N}rBR$u_EIHQ zSO9tzeCWyTIC{{1iayQg&HRI(#Z<_;9A?(!@xh~Ac;@`Pcf&B-An1~JD|?aOy)aGI zpk|HNkKfaaUFQXHs6mPMWB@XMKS2Pio;KUz^DSHc_0nT7rBCMKQqMiT?D3G!js{%; z>N_{OOfPG3*LOlUG+v+~LYATc_EY8(&ZYBxznHWH)`yOtQD$<0Bpjf_1_hqguv3G1 zWfszvs3Ao9hXD>Nv65?Pv@ttCCYB`ScRQm#k$xON?@TB!4V}#%;Yc2JvkCf~BPY6! zSJ+BEn^xpi^;Jo2B|Tl}2*74Fhjgy|{^L0}vL(0O7|jH$GB&t7>87-Z68jkKNjcKe zqAam1eE@JQ01()GT#Y2&Qav5_df+YsKb=aH1Q^?q%&}{G?D6AeL$zai^>{rwfXN$G z7nPP~ySn_U7w7>b9}M99X>3gZ!s2J9LOb-c8cT82^dT@%sBY*dks@hQs-duFZH%s~pq_?h&2G;;(wG1?D}d7<|6*<3k$_>aq$laRulTJ^ z@yuK?&78T^d1J12!k5j)8>#q+6{Fis1qPwO4&_5fvt^6}eh;WKqXbRp$8vZ<613%t z2tc3tw(=8jRLy;@w^%EVPF-Ujx~*Hjg^@91cgX4I1sC6wTo3@NLtr}wbX%1%GeOMm zGDewI9;_JuP+ClCDi@x@5!cg2+J#myXYkue%W*)o2HwY*1a5}J{-ytwpFiT&m>#bV z-rT0eA~{{tZJ=AK&BuIu8ujLbrouC{%B#8rA;)3q4B~3g)kAKFP&mD*Q3vV3O_5(;B}#S42%2>Jn+C46Xx^Fll960Bb=v4TeQ9{H z8L^~tScKiw*vYUey43H^5X~K6h`>$P7?m_F`udN> z79Wp_iG3&=ZV~ybi_7crYw6~|O?2bcJ~cOhSL2;WoR)um^qI;Fb?pgV=E1_9Yt1-# zn9%vcjId_&X|o~%NJzu4uXbVEYPLO+yO4~OD8Ab>c~?skX$U*3jv>ELv_yIR_9Val z8lXtkUV3ByoxK{LPAAT4=_;hc(mCBXaVTS5Vwi6nkOQt6HuFx8t^jIJv)ky1jcPD0 z`;{%!gmS9C-RlC6a26rumR!{|5kuE{2fGH)ZHe+V5Pla@!d2nDAKhwJ00z!LhejzI zsZ$gc1cE*Iz;<%_+^_8_)3NlVHdLBX*@QA0TK)3< zn4Xh&sxeH5OY3P{t|PVSQu~L10^ee{fF3*H0JZu54klA8p1f1 z&(j*4w~G{s;58U?^V>;tzwYWv5y)5)8T67o!HMSke_Q!>vdabz-=HLC0=N3yMWB>c zcevf7LMebdHR(8AI{5txDx`TW$_j(=$Gn3HBK99@(9&Y4QuiRujErV8M0Q$L2i<9= zM4ap}wWkNrgWuU-PvUd4qYbWq%v{K*B#XK-LjPdAw-IBcH9OYoeujuxz}e`zjy?6O9dh!xob4caXbHdOzJCR`iYd*nnxPh41%8s(gy~Aw!<3F4%Cq&(xtHRI-HfyEIF`6#cO26sR{-8+CUlo{nyY zwLTbG1Ih6SGCFgo*LAxW#UIq(+dnYp$O@`0pe`EBtt4iXmstx~^{2+ycaIJ|I1I-l z5wVcDU?p6p6_z%8+QzbVa*Y1*Q#UF)NFTcK`O#a%`~KJG|c+a{&B)+pjREUPe?%i>W|9&A2wbGYjF3tCHTJP1Q*&46Et z@vRP~sMG%%ZIZtEB)HNTIGwlJ)=FqJ&`_sWEAhu0i%@l=zj6K~fugVV6cTO^jTLYB z9UrX3Kx!V7xSd|0tPXQiC;3OZGB*YZnhx8mg>U<{po@kb$BawSgjT)m)t;qlmgBP5DJcjSvKsO@65Ybh z9YWsfI7TgZEuK3@EU)%VF@!8HK7fN3q%A`D{%NIlb3DZ3_#Qx9vFk(NwnyALoMX`l zS8=}598^UNW25g=HrfPJbVB7smg{;N+X967k=a*-mfCf9_PsB$+>P}`4G$@}#*II; ziG^ay;9^-$dCz^$JD4=1KPA#tWAZg+MVoUR9+i&M9O> z=mrUpTIJH&0w4#{dkfvi=n<+8?qN4 zGw+_8UL{gZe!M&3r!ope!ZDE@J+K|Vj!giUujE;93Xm>(uE|yBij#_)XUbS{FvJMz zh8~#tBDS{p-1Suy0XSdvjAOpHsrevgq#px-X){%K|yA_QCYCUZkEhYo`oQ= zXVcXJ%q8h8CUoG=@J4m!2l=%o<=Jbv6P`AizFZLozc%TP(-wB4RXeemFqQjV_UeDw z=lfhR18omhO4>Og^J{)$FGjf8 z+7_CVH?HZr#OvBqklXQ`nNWI9wYZs#t*@+LUad`^mTFJDg=DM3Gr36tQEUEaNf&n~ z7Zdvon95_<(ry|k_EVJzXyK~ zIYyAk@)rWxYC0x}pyU7*w!>!ZEvM07Y~$^E(X&444jvlkl zOB{Eu(rCVL@=Qp%x|+4X^Luf~aTOf*`rvi1S37 zAkV^nA&dw>$RICuMJbQEP-f=8> zw!SLQyFETg*xMpGnm5))_)|j}j~|i*G8)13#MLNQiaA+I$`!HTT47)p08)TpFfUx+ zNPs`(bcD~6Irk0|13eDV2Sj3NeNl0Z~AO zx%>xt_qU7eVa55LC2n%CSkGzuoY+$8j!ODsIWl-27yPr<^$E6`S}4>s7C^R*+VniYu)1~5Kda6oGAohT&Mp!fX}Ed0-) zliyYUs2YZg?u(k9!VHMsf3#}NxyAGb1?1Kh(Q-m)T;PC=VU&>kb2TCO@i8PQ^GgWx zZuRN*Z27WB{PaYeK;E*4Ep2~dV!cjdJ>mt6T$Tx)x8`C3*P@n}!?Yt0JXco{3gie{ zHIMH@<+lKcqmwuofix~}c=H(100Ia9i-}}m{o`{5%N|nR31Hp_=ZfKth^Ha)A3(2O zK}&_7n&G2tC;cAaaQ(@4c2PIzDlkWbM6U7aPb`nM$sQynW_a9Ka`C#`dDslDHt^fO z`I9l7&l+2OPIe!1qd+5N2no79MeXdD5I=v{54*Jm|9E%HfAj z7L(nR{7CIgI+>GqLCZ283Mh1x5)xXi%R4V-@C;C^M<8n()R;Zd#jFmKh&(VgSY z<0gC;x+he-)-e;<9)dVNn0a=-(7HRo^{-v`_m2yG5{ppc1k}6Bo5PEME@H> zcMy~Tad`qL3iPb7$hz+k-qj66JD9{wv#9z5xNeP$;W2ly=e{q1rt2c)_vANPq4zTZ zQ;V>D-rX!JO}An6&ttDm)>jkn51$~y2@V$?xDd8EreyniHg_Y#5dAgtG(w!U$H-EM zQS+|Z!gdU|`B$X6Vqg%p#2@HOQP12?7iwH!F`l`lPn%GnIR!n5&o0t7czFWY%X*OK zl?{UHVK#30E7RUSspta|z$#3f4bzNgsU}FvQY0PBUF%F`MD*8l zk z6m~HI!K=kwY)$C7o1R#DVG?p7yRd^&d#O*T%p5@t_7PWglbvfRI&($>q@l8uv^T;E zqaWZX_D0qP0Yr8W27Z_o8zTYhFI5eBvX*Y}tTJ)7W$K>;QM$ee399~fz5VLeqKnJn z2ML7#m9YEiZ*%Av7w_#lxFd22i&jERnm7qyjUUTLEqP}1x6W5^cVYVfcH+GLLC=c> zH);uR5sL@{Tkh6h33YtAE&>+GMkncpTPqWj?|hMVMg>Qe00<$IihWoA|7a(lz``b>Pbz7Pk3l1ib+JSQDXU za*k`qBF{ruNp44!*NJ+zTv=kX2j(xtrC*3#fkWvm^4YqnCSdu0Wrj^aPS-*ts7~%~ zWcoXWXP6=Nw*Pz(uhZ0NU_nHwu*+frn3CDF`gcbUvDv~rY5eBxM1^ej=83PzJxO*v zUG{z5E~{M)1{d}c5ZFflj{jmZ?CV7Yg|MB+m%cc8usj_y2{G=sFj{^qq0`RxTF=Wz zKs-kJC+LrYdf4PR4WZ3)IKS27^>oqGaA-W-b6Z_z=iOCZvH8^kB^I_=l1WV9#GM5P z<&!2DqmO4b%fXej`X`w?o!K+F8OOC|^ZhIL3Cdc`B4T|#pkoA5xAG7G(g?n<0=^$3 zi7Tk-K0Lw}9Q34*w%u=53BgP&;k#-T_E2Ls+^ul5fm0D?O{^tsOg7&_9EfUc%WXTq z_zc8j1kO>B%wGTALy+3FUQa=xIkufB4hn&K_FwAzh;^&Z4h1Adc?jt6@w&J=EYYet zp`7HlgQbq^pzW(8Zv~nuhz87-+(=rwP@|;_D~T4x>O&PPM2ex6g*CxzNy~vGV|cK0 z-iz3*h*gb9;z^j9P_OTw{^P;2Np{_+-M#k<+d#YlLf7b;g?DrY8ARRy_#L!shgraf zkP%bvZhAl-x{3mgvvHX(`F&j^;MSKB7dHVXqVJwL}jXX{@? z>mBZXyGRC@EWSR*_4y>tpxa(|V)K1i`TVptV<+hFLrYJFf_XV?uJ42aYg8VxO3|pL zd&y-tJ!>{17$g!0?vf5|ShEGk%`b0;Dehg}*9Rb{7o-5%3R?+BA?SRm*3v?Tufb3W zf6&_P0!njiedv{$hlllvZsEPh%GT|+V)L(1(mge@9x#enN3$+qM*tF(4_oSk0vGt{ z0%>3-{yw7S{^52ql3j9t)!2s9{a$Cc(Pq#;@_}rz_$F8O4}rr50O%(D4HA69k#mIEV=xr zz+q~#B-Q3S1(3q}5N@btYaep%Y4+@cGw+7;B-P`&xn@POqLL9Y{(83jjduS^$KPWmR2`w)%- za7tPS8ZJg^zZ-pW&d)vLIMYqR;7*KCz!2nH+ZFce0D>iso2$rp*Xx^&B0)Vd#B*it1l+c=rnZ z0(n#XjH*qaS^3YG^ z+$}vtovqd}xZ>)C|G{iW$m|;k= zKH52NdK+kMpErmFmhX1<7!;@R)Kd^pQ7W{>&+Ht|9{8A>Tgs8;ldF8LY)5sHJ#AyW zS%W38BeeuZOF`Wi4V756)L~5eq9M9Yyqy@*l2a1L*Xw5`^(UB4a5%||X;>?4J&5|G zoOM8^s+}9hFsUnrV#R$3g?->Hcqp|-1@B5*0d)}9gT>dyaer5uvBWy!??RVl{_9Wl zN$64<)^BFy?`D(4g)%niN9q~7)E!Jo^!p`+Oc+Zr0}BRqSis4yBK7&)&9{#1N2Tj} z*fK&G9We*O9fq$*6}y&}d=};836l!WtXHr&1QK-~-);YN%r;h%K}pJ&ICvu{?*s|> zXkXJLL?agWMYsGaRsnL9_CC9w6eei>6e*?2btUeAgPk@VuV&qz{ALbQvMjTouorHb z2O;w0N<=M&T0sl^I9ycs1NcQO(@k<|VO>c77}!x;der#rg*kj~9qX99{mfLc8t3ZKJwHE~DvRUlf{3Uh zUshy|i>|Y8F-O*uAEfi<8k}q((_JT;y>&bt@c6v+C2V4?e+)3bR5X5A$ zf3>Z?D`A^?PR=s3GW}Hwv7=p7wplgx&4!=u^;Sm-SRThQz`$FDDymmZyxZlB>^`IQ zXUBG3VKVe^aJz*++xHmki1mjZvM?ioRgX3ZN??h+)0S1lqB2Eq&(L**>M!D-7EfcT zUPD@P;;9g8w|auR>t00>RLs_XU4OsWmr#C55`TC>2WG;$=H|g$dU*;#Mu+ysSJl?T zN6d9<_OL<|NE3@W3rTlpD^6=H)Fav)v6arwh}nDI$`nrM)%44;BkNhc;5d-H9jyFW z;|}#c2xHUxB#QV%QWID{M89Q3o$ly@dC&uKWc}+aVj>%J z9ug}vTz~3AhnbcNTuc$F_0d(Iv`aG&xaShq8ziHU-9~HGU_VDa!DZ_wZF{h zMH!AauHv3PMT-m}jm!y?Q6Z-`TOOr1_4b_Mqj9o${+!q=cO$FaPJ&(SUJr#f2 zCZD$a7JKGse=|rwr>2TAS_{G|LzlZ}Ws#bXSH~o%;bqBJLf#{D7>Gx+!k{*mo(GATcuBaugTcq6^ET2xTcLAK_TuxN zK}70HrQsWCQT3h7dntB>kD!cqx$tBFIJB%3h)CLY6fuwI%&|2qrVKr?GWjifTp8?; zY14P5XGuWF`pNk{#Xg)wOz{WxN;Y^`44jB%!49z0NH9nZZwkStc znyZz;up^KzCB#J4Wv6zqI7p;b=gjCKQ8z^XPk9y{J_Z9^!Td!#k);zg$!W{0Q!`;{ z#L)`MD1R?FAMoXVipf5ygYJ@p#Im$qBt;x{+jQ}$;p$ukh9f_Y8v4{%_Rxr3#W?fs z4SG2-WhcY>sEb8<5(0^NnQXxERt%sN@BSDc0x86KGojrXr zEb(^U?NI1p)IHpP#PauYV7jlfwOJr>9>D}`RW!1)$=3sy%13ORC^W9hQ_nJ@dBpDU zb?zfS%@>wZW8XxOoG31Jg8e^Ah`W-#r_0BL?TW8ro$?Hkwugp^szsiPp&vU4O@+h} z!eIu}&K1K&Zlrxye1u??^d(L{Y?_bZTx(pz?9C1exqGM7r+Ux95eOB)7qg*Ec9$}srcpr$7px+RsWnKYxv zZ^y&jrb41}&SeX!(cUb$xdzGH)X~;_6LmjviJe}=BHOGZysxrv10_%nfe`x<+YnlU zrT6dUhe$u6FuDF*Ttfh+s}CV(viqozwQ$n4WWDV>n}@-{v?Z9+~#v*Ovl zy}ttWzKijTN&%SLz}i(#buCVVw>x!hLw1ww@X6K;2QgzWV!HCiC0k~ zJNrL_(vU+I>L#-<^6dgOf*t5Fbn$wY{8V;1d46#Fv(mf-cAYzIbhA3be_qt84zkLM zl&-&*b9>pfb?i8*z8MnsK$yl^QA%3?UQTbSVJu}s+4BMcyQ0o}4OT{92z=UZS&H_X zdA+)6&8`v;@oy%=35VHn+LczHu=+f~AU&`WJM%PM^6_GIIe6IkdUx`p#1}{vjX+mp zXKT^T>)d^($ry`(1+%cuW(Lx>0-}*Mk<> z?sRm@9w9_5_udV*D>au;d){8L=kZM~{yNczks;|}dncpZynM+l7ad26*R|I=0R@=n z3wN^5`Pxjn=a`4uunj|Um`flSb7~QRC(ha`lWIZz3N>l3rsoC4=-enaMYAVt6+rQ& z))M|4Q;iQWT>10VO_H8TjSOnD$kN{)>aahNV;Or6&aK{`N{GHw`V&RFVJ^7PtOE(W zQI7D{YN1)NHT!X%ll1~>@!fudI*QE@if~~Ab?1nML#Io@#J!zBF!ppnHP~~ekqR8t zzD?+-=zEOR!m3L_`rLIZ(}Z=X+qmDEYo@-B<2+q*quxV4+mj8zhixq1%0(_-+l zo}Y9xZ)KaBWAx*XR0VAeB9jEH9T&WRS4To0zkL0+=@J(S+5DpYP#gX<4CVlfo$JIl z>Ll=m>NGv3Q}}S~7l?17k@u{sYJs{lc4pP;wis@}E;G8|;7Q}MXno>cNaSXx21_!A z;S#;@jE(nKFfs^Ps@ZfR_;4I2aZCtjlh?Jt7dUfg?50bCi~M%aYjPGO+wyqb`mU1) zc^dh$ctwRLvNFand^kMT?n|J-8~|83<)yD2gy)#LrB6{?K;+y`Su>8l+~boG&3jer z-T_-jLu4ok0@Vg^%apW4a%@AqGc<4Tna_s=q@kV93vcl)9)9)QR0YJ8ELBXNRDj5V z5bSSiXKT^de0w|bsVz1;?RzyBTTSNXsTN<+LXG76`!vc}L+j%C5OFt#$wYlsVatJy z$J!{g;^g#Ftp1j@_T3Yt+c76vnKPjWk1QTZ{CSy18NCU)=qk0~0Q;^0XgUE`LRdH_ z-omWu%3VL0m{_9hUlT)C0LpY5tEE49fnJ}mXy031f;4$x?tTB_Aad2rvN&cor{~iG z-o|y3ukt&l?m8al(c{kw;It%|Z3&^H(bdP~`~ZvkG8knq8wH;t$4kYtc>xmgD?3US zh8u4B(UM}gwJd3M9Sb_nQ}EL|Ax$qa2Mpmk%d{5M*8b0?68`>wv4SDYgYz#|5dE zzN{L_Zg_unym$-#aH>ZR@e0FZkpvz=Mb6q?As5^oME)$DN?9lCZC_4sS&nDV46uA( zkiP27eNIlrLQj_-nXgB)IrsWOy*kv=Qd zV+@UqMuf;V3sG(TgF}v+l=`v4@`%rgXZOXfBv-St=zF(5)i!X9Gdvu zl{-%d+iS=z&cvFS?~<=}B8Q?1>wBcnd`m{u{zOT3Lyu@2`Ew;8M;P7IF*2w2WV5i? zOkV7MHE5{lpp1O|I>sOny--L^VH9ACU=FJRiE#lq^V|;JfacSU>*Tfy77qjK5$(FPq1T6+5dAsF21xK>cpQI5f8IU{VX*cQVF z^#M~0VvpKt*cx>yY)g^%->cv~s#sna`SJJy=g(qBD!AeQ#T}x&ZC;pSo0^&sm@;D? z!;Pzw9r}PERzwKK+31p*c%2Y~{@xE1tbv-YW)`FzZH4Jd+k}i~Bxp(h1bjR*-WZlS zCfoz)+8CZ9l8%D4!F9#oz1;89zfK2s`$UAGwSd5D=_Ia|)sdc<)vkA9$mzQ65YP~YhP8OVKBc?zv6&To@RK&;_Ls}RrTI zpwni&zfCfj_%zkb7q9{^EtG;^n;*4KYp6*{w}{fg4?mPaZD>cs34D{?nOJ5Em~NS8 z=5^6X)Za!Li2iT^Q4`T{{_iYjWF`v%oq9tp$oBxlRSV4ez7qG#?&XZUeV@jMli6qL zhi|N16h)mcm*cg2ut3$ca^A&X1_j)42y8g>L_E$MU9F^2*e7`WGo(>Q#=Yy>=H6sc zh&WF8_OI-uQ_hIg!wx{ z8_4*VQ^|ATVpHyMsnSDUOf59S{l!)XFTDlrrA(+~QFZXS8JSqx+}21Lh!fVs?yHer z(g!CU_?HE(2By8wct5Nsp8wd#sHBM z6WtPND#jX=34rEBVpJr_WCRBGf&m+8r!JywzrKg8V_4{dIC#`on}| z^jkof-7STRXHSH^{pr^pwm+iLzp{)^X_6|A^S|+0_221m|L(+c;ri3D zJJw%GQK!FQX!zr`Z}q#ScgAInyf2)D@emK~Pa8uCVjr*4hystD=HFrO`JjQFOwgls z^Jf7lVKH7*dH*HGJQik27bkIX4!B&J+Q&FmenYl^kdr(SStj_NJL)Av+(h&Zn0Y?; z0U(gj(pzY01V4Wofc~w0pen#2BvzkW^S84tLM79(@&X!xw(R-m#qgg8=t(4oXp#6- zl#=3nB)}wZ_@SAW{^dQb@}61x%)30Nq1>q5RRZQhX$foLI0wJql_NYD6&SD5rrZ)^k*b zGsT(-_F*O@X?y9ZNlp62A%b(2*&S>{iXJVID@+D?-;AUT*2k`OZz@0fKs{zk*Zitfw2j1TyB8sFXZaw$HbB+NfB6c!A` z!6t-_r)`c;^Qn@xqMCUeg;d^!>lDeHXIiCrebrt|*_);&LVBmax} zaMWHVRkdeGY2im>5yx}f4;F3ztvGzc`wEBsX@I|{EA92~n9(#Fpx9)jxzV9g_Gm=Y zu62BTWyohGQ^ftMTPC4CWnX$iCIMQ2%6P$!Ex-L8)dxc_CXP+`mU?B??OAvcTdENy zv7Z=?2NKL*N!4TTmY#8*a)VxdcN!HNOQW&l{rp0g#r;2IEcX`!WnDe~ zmR1b)i4RJvrKaudg~v;ocNnIZ)rGjos-*rPci`?-Y}q-A)HzHC>#NJvHvWNuIPzrw z)9FHAISjjPPdha*zL(7FhP)Y4KbrbFrBq#T%`7w27JQ~U| z<&2yVA{xfpfrOLzK8cEv^>^GxJbYMqD|Yhv#%-)xqLExr>_QAZFHjF*edw)>>aKoX<1yu1Bb<^{%XL}5u>jso*E zGMRC{b;x;yxwM_4k&ph=L%}BJU;V@{n`dwLupAFL#I8s`YkN3;c*AN)zxQ#Mrh4MR z*N1h#y$}=1^#OlD?mZ4z=+NSfnpC|%H7N1jgdE;(u|K`x>cuncqJ-Er4j+{lEK`@; z^=AI>P(28#v^XS6!k)tRsZiIW=Fx+|iif0g{-l@xw)D-ouF)_ZT-IH!557Twd?7tCe6o4Z_7uc?gXhRkrRp}Cg=>W zYF~EO)XvU4{OZ@%Ja$9@NrcACU&Vn`)czdfC%we~E2j9oo9n@Vy8PHIv6XzwY69f6 zNXY|lq0}V;{!2fPC__+T+NoL;xr)>akdIB*qe5hxY&z=yF@YNCHTgXG7+5%(>YJ4r z|EqJpRp{^zXCb6b_>>7DRNSOul?8(_N?zcDcupTC;P>fB2>kgHIn)p3%g1bKq?|8b z`dOPrYg=L?R!UB~EhWw;F9Hl^e<y2KVQdvRg6r3Z>r3!Gz| z!>OX~QNgqxa1wrwo_S0G#OZ+@6N z*I$xzF~AkgkuMTAZpTh9Rn{WLeP| z*qp+c?%sYW6^dDu_d0j13~i&sN+LTwzpIt=e^eRV@@~?_7vBbV`!z|}fIeDgh-6xY zJOjIog0T|4#;$QxYD6$4%3R{`-amurOw$GOqsX0gVn7VfwchQ30_soAbEujo_r!Q3 zz_VyDBN_#W|Gk}nvTSY${FCe5ahHg)2nx_t4OBuicC*7VPQjZbFJ2`)C(Iy0yAzFl7Qu z^jDHqUwK1sNWJF~lQ@(kS1~(GqS{y#p!jST=*x4Mr%KzE%BG`?DiITM{t8>5aiesf zO~57i?|fe?Xv)!`_tB^m=F7m2Vk4T{=`PEu6%D%2)?g1=0PA^+gUa3EE;@@*18)*Q zu#fFrogXBq+nEJYmU2$myH%WBK&1idVgiME$6`ji!sr?|4M6L@p}zk=t7)-UfAI(* zY2E1jwv15@G+EjTPA@5@-wT||bkP}Q!@WnVTs$;>tFi6v+P1PH3JjbBD7u%dTMxFK z_d?`e7N63b)xCI9;%NPD(@g3FbhdOk4sZqTBG|BL(hEkiC9T*cRX!R#)z~xllS+>1 zV2IOa3(WvHQZKRSTDg0&S~imO`#pNsWW>s_t`*_q?@uJxk(EimYI+x?e-r|zY>KzY zHcdJgel6uqa8}ICu@AT9w;cX+Dm044S}fD^29(u;G6<`>5omL9BWt(D0{OrhR6stk zZRAt%4t9LxWe*WCcbnJ&? z_Fn-teRr^(?V)TVJ|RQaSb_cmbU{i(RIWLgtLRfC0q+k#&;kvf+zYed_Ud2=F6|AY zNeWJl6PHeWh}3XH@??iM<&Z_xW#U|H%}%a5lB?pahv0xP4+&keFZJdx6No%vmpFlM z-7+_p>^*%2+yM?{^wkDEx{zeoanj}Jx-M@FBn)t;<^2|)&BeaC6@Qtn^ z6Nsur^t|0$yF=XuI&^#VpSgHDb*+Rq2Yw&lfG6yd7%+GLmYRC6K@U-r3Jy5zyCBZv zp<#B3pD@4#B=BXY$vN2Xc9SDCtxNCRS=`Odnu+&%s>4^0&jtDaP36%9QaMZSTk*{I z_-R>LzOk`HHCMGOqdOc=L;u~^Rsn$R7fCx!oIs2!-SD6@5~4`hFv-av zrhn4Olf##sf}vBg^esaekUD6RoN#7L6nD)d=Hp{5kz;hWMy9y$Zy!Dlh}>u5)2Qld zVy&kkmbX+kHRN@A(iCHWA@#Q9$|IXb-34IW)?c{#h_>r8UgtQ8&y)W3cZ%AIV5UI} zC4JqO@7Xye`0xMBEmx*RZA^H;d$EKQ-S*;B7k{S}2x2-4BiV!Bw2kmM*<-2O-TBs) zl8I6hHRT7Sb!Upx2Q;dY`1+Zf#+yL%IePp-=Wjxm{cna%+W1!|2H0dW)h6x)t$*E> zP>+labzI~^z{`rgRyT-W%u+1ut!!J`N1+CK<^C9NPj06@-JV|iD;CSHtX#<2gdx`{Z+ayq zRY|>ZM%2}nWY%Ya5+QczrG@S5kI9<@K62LF2#ROgkLwjgx@lZl7cDR-aYP8go4fdj z?+;&sH%HwhSvmO;rkBVFuy*HE^#*lZu7|E0(E&sEUs>2l?w0iV-#DkluR3;<^Gfdr z2#)7XI<|@fr%Z)0E|MgX+2Sw%_rLO4v~9NxiPr0pmy1DbZN~{|Tnk-Nu0nlI3(XZ7 z{}bZ>GNT$VE}c@w4t;V#qURAHnSJmp8`(eNAr(Jt4_{ z-j)N;0=dM;$ifYVffI%N1!l9K%ep;H?F=Ee-PD9OsPEgGnwi|FYb^AglF$p+pHFX+{rv=?f@;MMT+FU z!T^0)FNYTv>IB|IiLS0MPKsG!dsJM)lktnZ%^)e7TizE_ybnbxAqJ7$^8};Nt9IDFm;AHrxPFV&|fIHNYLrO zrO%-1_T~EJm+l)tVem4&fHM$J!PFPkE=~``Q(_1J>2II$%e@5l1x)Zm-W@iA13n81URA@2EFetr+;E?Us$p*7G zcayPGQsSpYiK8`7ykP2-bX~fC9#YNa1H1x6wl-u{0<*NN5{{ff<6pJAI{{$gZHd*O z!o`aiEKB;PvPSl2C^=yX-bF2KCEt0Xa$exqFQu=jZ<=idDWb9fn@(lW7|Z~8q|%L> zC}9A@c2S(s1&06rt7R~0oz#U=4Tc#`b4Eh#0j#H#1Xm%h;0mO$nUsFqn!se~dE1o< z-;kqDi31P+HhzcDixei*K}s&E8e>$$uZUb#+?HML-}hHi)9@g~#e4C%-lG4g<>nQl z;ng&72ZTiyRw>o1iep_n>HpZ2{5HMg|EyF#n`Zg?9qUZJQMS@$$3+W?ad@v+^R4skq%>#f@(}cH!N%5J<$iaybuu zl_;n?CDB4qKO|_A59wNM*U9`Cg@hPoZ4Tk$FpQUoKC&w@r|Zs2x>0;16RJPfWKD7Q zIH`c|A8`?v6&=dhil$a2-?YPgT-V88Kpz&@eOo#wDs#j0}px2~@S*XXr zqo29x?AhHAW@NfQ;+c~E|NID2cRSk|rcgiydoNNhXK3WgkdYpWWRxDeVKd&I;drJz z{kLz+I8Rr_OyrW4HMGl`*VvOL)GiBJ_SSPlaiyN`bkL`7!u&usZUHvZR6DaR{S@;j{r2~E6S9%h3es`8A(6u?`ZW1J?yE6>hB={pkRrTfg3H$EE+IP_1y#_SM{bjIJ=tfEI z95S%gg#arDVvD#X9TTI}?@P)3vIp(QNKFeLcDgPJOp;FM%`zr%UKy+im^znq>Sh9y zL+%T5404`I9i~o(=S0beXVPLsnr3(f{dvH)vi9<>@Fp$L#Bl0T%AaCgTm7ZF_hbq` zlU3onwX?C;2R-k*6GMG5zf8lQ9RwU8tWdk#rxXNlll*`w1lq8ndnt4M9cdyl-FQ>n!Q=>8B5w=J9LC^F8NA&qfNBNOso8J_i>mu&xLHWTtCuY6!B? za&ouO&)iz)7q^~;%R;l+(@90a0@Ni|My=Dj<`)XdJX`eg#fog!5j*j>E z!AeE=07Q7nC9qd5YI8%wXVA0zV7?-R3p*^ z^U>b-q}|Xa9^dWz(Vt0Sj?iCsuQm3KLu~R3N`SHE(&zFK5`m63`oU+4DSh)mCGloP z_tkk+oLol5ySB;6a|(791`t+^%@coR#r z&dy-55t^NVkCnjdgN2W#jHe%!nw{$>nB)j5k9ZA@ z)WE~A*AUTKr~=-RL)X1wy@EH<)mjLtc`Ma0j=)$xtJeIM>KFy|(|u6Q@>93YqbG07 zo58RU%yCUswWWax4HwUvcI~*H2@y|x`+M=9@0V~E1AqtT`xZ=Z6Pn6RpdCRFSp?`g zF(|$}##1jsm2Uz|dnSjz2r{Ef`Lq9#mgEEfpOv<33encgyF zeCtk$*Qxg0ZI*y?#2phE_^JHrN^JOP55wG+O54P8o&?-G~48c zK0|uR%!m$V)A#CF0kqn%2i;Nvq^P~xy#ElM1i2@y3;y!=g*V$wfqL>vG~sgyL|BS!!l?SIL?Gc z8*2C~s(*>?#|D0I(4wa&r*ZsMto5x-ZLj3vS>H~e;^p&YKSOSnWv{LeP*kMD(F2^=c(T2!5HsAPiMfE(^75zrM8A;_mzg_}(s z%>|(bObBCDzW33FxNj-IF##OV$-+z| z-M7HgCkAxwX!IhNC<7=}=BCvTEIiGqWV(T!j<$xF5XTS;AbOtJJ0t*~aI?$$Dv%Xl z7q;TD)Bd5FlKB7Y8&q-6wPLS#lE4Z8YBJx{MuNn|>AypvV%|3X{zwF^Ra7|4fE>x` zT08&;=_L@on-)AJ-;hglelq{^Rkb`w@3Wx$E~yr_&PmV`SS9*@Rj=Q;zAwL}*Zimm zEOX~J6O6|8B4)7@8W&6`V@+YT8O7%A6Pu#M7Zb?O<4-Cm#HUfS} z_ML&?2K3}_2S1-89RhWP-3!2^+mSEiMW@YQTgQ->hfC@a{u1p1z5uDxvNZ+Z@-Y8V z041I!bs<%j&i7Z0%llfyZi!hJ2>d{SilUKH2>(dn1{-ZRKt#(6RWg@vrbt24qtz?x zq$K-925(5(iG#5?^+NA463`7k4LNJ6iV)&%--d2vBylbBO9`!Kij(j}lmE;F>@~aP`E$N`fTk|JFCOq@o-L zDb=XIp@y#;9V-jJb@BR&+Kf+qr`61Z9b8)?r_i!>rVL$L-(d@fK{IUloVbgL2pr#d zrRm&CZ7++Fs13C~^^oc>%`JWgjNk8D-JD;SN2RSEiALV#YhTunF#BaBZ+-i`lH^C#y?gydh38@!oR;?lU0AF%$+?gP)b( zVumvGxS@=S2nX~{`+f^xFVH}Lo461Cgh)s(O1r+mLAElmNV*BBOfir)$`3sxyCA9O zKloa@Uu?#Tw1}Ttv)HzhkZmwq^Mhp`4u6i8&>thv1l)gWljcG~GKW8Xxps6-6khn8 z5VR>dRJ`gr9~~P-_`{;LMG(dY4tLovR~R+^Hf2Emd0l_RjPw<-wi=F0zYenR51gYuyjcHBo8p^VENl&g z_+0}Jp2!#5Eu(nbagGgDy67melH5xPV+nWoBv>_lJRwl2qmN|M?UHduoc;l4MVww2 zK0V$#G~||-83uEzMe6hn(P7O+*HJK0 zMf@R|e%SRYo6N*PfvX(t%rnH70$Wt$)jCWvxbj??N#dmU_OpTihq$+ntEyeQMo~l* zq-%kM(%mJglmddJfOM&Zl%%9erywODAWC;B-64n~pwb`>f^_G1P4wAkzk9#m`JMmH z`h$lDl{M#F_kG2<#uzuwqaSoBv72&yBWJ-WI?KUq^?p*J9M1(y$d0MWcX@8F~<*o#<6>-wT6hD=; z*q~h=nLzMR zp!%O)!J>`m-JkEGP2*?pRAs3d*qh>X>>hpaZB4ZG4NshHGL2IErn0x=7g!RIlcBk4 z)`HG%El9Ihz7f*5&^D4PyzMv^)aR+Et#>0@HqCGUSTzU z$azo=)H!Ho6=u);{M;`TRa~)i3k!Hg*Y1o?I;}dCU4|g`!tu9~t$Em|pAh{ZPI1Xf z_MhhMK(%N8n$@xC5WgTbuG_eyU9op}zJ*1?KhPobqLWv9z#27zH7TH4Xv7rQtV(lW zZh<)#?iuvuE>)`qt(q~zCB=`&$=W*?@G6%HV7T%1``4H*BGqMeX4@Oq7WD+SZFLRY ziC4e&d?ko`cMtQEbH%lJ)-T&me$czvCj>IfvQKY|+%l`DtM5O9hK;^p8p!ODpzV^h zL=jy1blp$8Bm4gSFbFO`x8mm@*S?!<5r10oz~@GKd)k6N1@a*yB8lu0GSOf?A0)mTM$Dp;l zqT7rfC_`W9`}7*Z?905kQj|8MrXi3|%c-hBKGh3&53!h+x8ITe_G#$5NT8Y%=pOg| zLqFrdqTUl1of0{ZO9V3p_WdgGcwc2$F+=rHV#WLc({K*9h_%!R5qsTx_C^T=3=%qzJd%`%clP$V@sqQJn)VCnCXkqxX`pKm@=;`U@ zBwTlvsidT&RPyf$?EPu110B%t>Mw|Gg~OzWXK}z~WCttGZ0pm*sZJR_`3zRhnhVcI z9cXxzqy0tji+`N8w4A&|pjv~--|BXxM-CimK#73BbFccpf0K=Aicxuc3r9OqQ!`uo zmcG!g$;lDEd`4k@xc>7{m`$+g-ol>2whH1Q0Lg_k3DaesHc;&$a1+nni!YT=p3Vqq z0Yh^r&Nc*He+O`p$lKZ)sAbQp2+@?~iMkz60REXdp9UG6(csHHpGXVN-Vbq*9L?Fm zyEjL6{nk52!yX{DZ95HebObu&sbz7Y z<_(;hKq;9ZcVI6syGb@2-NB2h^;gRe6i`6LE6$dx{qJ*(V4tGv@;|Ed*g%MJJt`Xu zS}YKsRHK+WWeU+e7~94Vg9C3xVvgb2cOOVByGu4;pTq?QMN&SOGsv&Wx>vz#NnHN4 z{FMMw@Vx3)_`i)EjQ&Dqs+>(tpIvQ_(o~*Qt=AuNnWsG!daN-Q_e4f!A^Ee&M&`BhUQ8& zys=5CQ(-XQZUY1q-wr<%pqO$JUJ2OX)p54T$XxrU(sv+p;<@stXz9f13fp*><(>h= zQvjW5Y0si*qu%AY+gv31^P9OFMsjk1#0i-Xtm3Qix+Q{TYjH5MQ$a{nuVX2 z7>fGUtE=cBy;x;v!5f#Vr)54Am719w2_=|oery59$Xz1u@8MVj+B7_0}L4bot=iOJIztI;R?qyy<*H{$Q#WfS@^90rui(;!iTQhyhJ_l$(3L^D?l%~Rtu zyVTU7K+upm4wboNl3I{~3vd^3o~6BhdvNy(ARv1DLSkC{4IXV&v`;}s!R{=w*fj|M zlzYtWb_bK;hz$1Q96|Da)r8+k$T`f^-ZYM6HvoYq-^qe}Dfzh_o+d>}Mcwf<$O9Ir0w7JnSR%ny`&&)0QG& z`N|_GyXtluvTor|XgkLs094)pHo&q+O~`=+-s<#*y4b;_f7~(G1WlCJfjOX;yZH^a zS=t}5G;imo!uHR@x)=P3nIQSh%1@%D=g??RP%VY6Of)h_f4Bnm;8}G~b>MPIzp24! z4M0-tKT7A4d?YQ@v@_^=xnT5<8wP-KrGmaWJ2(3+NKWtN7zt+l$nG(> zWgmH$*K&H>5CEgqU++}-U{jOXw=Pm3*x7viLwk9Dz8*H8s@BCcalhHAUP{sF%Z3K> zwv>c(8#C!&9d26X)s|9IpUH9i&lno~xTE^Pl+tuRCKV5|)&Fwv71%fjBoMc`sm&;7)+v&Hk0b z>V?&oq2WbD4BHhK*R-qt((Yi2%3dd&2rk~ffJoKaLjJbs4imMv7dxkX@r#f6lt7lL z7Jsc`@mc`4l7=)6fGSnmzuZ7p77<2@ld*AMWFWGf;jisvg(Km)LmHRN+ zK`f8%6uM2oS95O{a7Vue;OJL06BR!CJkXqz|Ps>>?L!2{R6r<&-}a!uq;V3`Fs}_)r*{So&vKm z1Zn2z1OD_Z?+D@3YkSA%ow{2R74L3-BbbXeMt~E&J;$@S?mNW@xd*y&U;R|$*WrOt z3(VPp&uu>~(I-@XP=D{rGd9*fi*zcTZyDS!10Vz996Z?#k{d7*F@_Erk>FY2WEhza zd)tBVVOMFpF+{bysJag~w2+S!=26i#IOao!9TB7ItFri8P&sQ{-)|kF*d#%2zcPWRn_#mC~912_ucy`v$5*ywn zB)W5_4c5zKlyxKDPpV(m)`7s{fZi7S5}r&5RX+FJx(9$Kfq^D}tC12781Ej7NfI$= z#kQ_r&c%O@vJaB_W%=u&dVv5C>HPp5Yx4X?WNm$bcV3I;0yJ_!^WS~ps6JFdj4T@2 z&!j)WAz?LfDGc(esY>}7WIPmj8N#*$=5 zm2lQ1=AR48{^uRaC_I_oK#k&UN=-fbY`59UV=vMkeB;vH5j~s`5_4p}0dsAFxU4+T zF(PvNpFmgxfD8b2Imx**r{)Gow1;{mZ`#oXy|dtb3p~+PzWq$$e3e9p{Nnth`yL0) zWM4*iOdoDFGMnn%(~Hf9Ws6k)N%sm{57wxKN82%^bMgEm!{1bByn)TF;wk+oR8rs~ zP>Uza6!yqBh^Tg)8)3H8n$5m8vVyVXQugI8nbLp7DUBOOu2&!!`u@6XIpH&W)n24O z5Qu$nK`Qq(-GoO+uPEn&NuJo(CtB_vI7f`i=0UITIGZLd%0Mwn+qv~O3cXqeiYf9q z2#$rhqt7iL^yE6=O@HrX<5VYiX1HwnC|47~KZl2G{eiME zN$7WX^xpIJTsD)w=NyyKok;3HgcwuoTX60BRI~cS|B9^td=p*1&`26>s80X`TY^jn zj&T}8L{tqb#@4MJA^1F3#V1AYtypF{;GDfXZV&=#0s1lm)dHv;kX_uVq28@@vzf#B zbeYHaKMHl;(+}0`G$cFmhv0L% z5dY`eMmw(D;X*X7Fe05KW+|E>}bu3`ZfStjrI>g!xxwI$a>OJ!b9mLt9|z{Y7|fzhfd5Km5@E!#iOwksl# zX5J}_D5S+)jvWR%@az%;5j2PI|^IJY2M<$u09V~{HD-pP;V zFHF^~EDBG7$C}pvrf!!YQb6cuipTw6lz9-8c8Ks4(K;NT+@O5>4_a10yb$$|(5K|}`dJz1uC@ zlPetahW0Jp8pJD;qy->N*k_+y(_q&i2;|tV{pm^ybKOa`t~z0+PomL(Y*_y7Q!p?8 zjQ2hbBm;=xTG?DgA4ebgr+_9SB4dPS!TwML&x-P{3Bm}5i8V4iL8zjX)r0XWb1s|N zz1U$K)`;oS?rcfRSmuGMZ7Ij8Op5m#+=%_x&pM%e`p8EIlGJl&hzkt#=-{fyk$S$1^2ZoflAJ`?NWNJnPC!du1CzeU)EepykRt zc}7Ini8zzqpYn1zR2qa+m!=>91Fe>L>NZKgMITJH>A8~YQOY~wEFEYKpdPq8evb)| zg8z6`wsm298-FaGFiBzoQO?&qM|EU80K-pYRx)w$tq}B~xZ&Hv=Q4j-A&jJWZ5K+! zU<=Lartsb0AwUu1DQUC2oh^A0u>ST~=gWwkyN2?&{|k$!z&BwO{BS__)-wD$&VWVgGgYR}qy`1L`7sb71E6adb*5qL!NFSB&MQQ)FvI#c z#W>Eu9U@e4Xz}b7?n6Ft+^G;0 z{-IW#Qk7%Zx6ZeItQneLKPcGdJ**`Da9+PTPJRqn6+%FDN8|HUU%N`q2&(C!>cK1SS07LoBi$3e^Tea{_F4#AnZ0aRRLUh@42#348dqglzK?h8-m0`gK%#Grm?BAm@6T5bw zFf#6?DODp}Snxx@CllxVlV|_A@>1ApO-Y|NTD3Dg_ufNX6@#ThkjZ=zs|9bwgd~YsP z6taDBOCpxR_y)dJ25LZxesS8FV` zGz4M-9&*ewOiamq8JH&Ym5ZW#Nrnl^pg9QVqY~@4E zk!A-%ipgz@uV7MvJQQHF0Kvhie)z^|ZKEzbD4MolwJ@bOuAZfxl_nfR3++(Fx057L zZ$mWmX?`BFT*k5jh1>tKq6olXAsmR3t-_yyF&?P&NC+7$!SI+<`GHj}>y{6s)p!k; z8sQjDq+kXjMX-Giv%7yTKuXHnBC+$8yyo9o3`9W(8uosGT^E_VV;69?fjv&3eZp432vJ0U;5pN!u799)c=!HqhBw1JLC6tcjHg-cf3u;M4aKkSdF`f?TYf;EkyY- zdQ}KXnD(rn_pjmkxD-V|7zY?$c*XTR*mt1$0P9~}v*uN#Y+|72W%$LF1b@*<+4EFX zSil=JxrBz&a=!!)Ihvs?==^M>#K5qJZE>UBzv*{KB8p8(pJO2o>+(?EO(42Ob!qp5 z&%$f9D%U>v4vJA$#O@K%Qd>6~S%~YQ?nF z63m14^JWX)C2HnfOM} zb5{Xq;U@I3wWF5;>uMt$8+!HX7lZk5oG#fl zuHAknrVMs%z4@>tv1U1#0rmfn*I+nM>GEo_E1FF#5!V>-GoWv)Onz0z@@4Z{kl@U{ z5cO=q?P+18v2W{8n_cSbz05}V`o5t??IoxDF{>M3mY?RyEWILr>S%j_+9)aC&@ z4RRi1Pqhkte5CxQro;BSb>?T7=dvxekUvLliVTp6Qm+tZ_P#@=TK{oJus-rVB6h^Q+BlXQM7n-`RY)VjHs<3G(uq8 z`bot02XN|`+-ami*E*@@aXb-nKJn4S@{V4T!pjn|WsmuW3**fouzL zh4bvj4YZHyBx7ch3%d8j6eA-gS;A8VY zG7ljMIaN^hgG?k|M9NAdWf$CzrCKkpw_pNe8=4Q!j^O0C#XggycfVwFf>hH0fepJq z{F?+mEMOUhggR8|T8hw%F$$-b?*_~$wO`|U6vh-~fipTZ&8>)>P-uVGt*QYXZ9 z91Q`@c%XgGZE+zgkSv=dF(;-8nnb^YikNB8tuMX52doMNM|x2paSK+rxSRV>>2%Ks z{M|D_3HDy3VrRwRu@}{}n()w)aF_Nteq%Gx3O$3y$9c>kCvT~d_7ugiu$<`*;`;O^ zaPR<=t})C5bI_i*kdo!*t})>KXV^A3^friRkjv%;W;TcqSlZtCB2QgyLct`-1iE+b zshV}eZeYV;*~X?srwf4HY%$fQ3;NGrrX?OwDEr(8!B>}*her)#X;2WXJ|E-24%&2^FF zqMB!yG=5B7MJM(~a9cd1{sI{pK0##gnq9GfoAu~`WVkR3=jq5=O~636$GoFZ+`9O; z`vk~{;HRCx&)QF1e>C6SfYZHncLn;S(4^Q&i6kY`;M_oeXJi!k^`9y^mHx4I^b2NT(WWhk_q@@ z83K*^W|D#sA}6Xf(Lx}NvuM8hThcEVB?(Azt{gJW790d`h>87EE-cJmyHAP2oqJ(O zM|0<nWewu^ zEfma?t^#DPqRP>F*ik{RKdZMCTT1e%bv9WkMlIEl_gifEohaI~j`DMSKL@Be0nCik z^rZhUyb^~<&~o>fxnuL3M?6;5uFOjR zhJpM5@=)#FG|zcC?9{_J@hP02 zH9{7h+Q_4Lx23t2!hA&{g3776m|WMGMpEh3B%h$Dj!~fPh&Cpjh*xt?aIdC9kh5%x z8$@}L~}xq|E1gfW<)!*)av4=ykWUpqfWm5ArAeQoE)*fd)n%Ql1WotzT1r$rWIAN(3<&M>2ft zC^mEWgyQ?n!s!nVo5|kmCj9)~-ybDb`y24_*ZCrUfr;=_KaA)Gu%~Xl5Xj+g+H-&V z#Z7TMFvnI3eL7IMGG{a>KTMBP$U|I3QDu@x4C}8uJ_QO*TYs`gXTju}#Dy|FX_`r_ zz)LFoRip5`ippHeAl&BX*eNQH^2i+LyrjAM?J7J7q20Mcv3fDMZ7V?K-9&uD9>^`5o?r6P&l4GKWtLTcBRP5cKrU*E*zGN~<3<8&bg^E9 zWa(07mpl&N7A;C$Na#AcP|A>EPj{;$?M|Ab(S2ItD^^xbBCRpBGBV`MBKSi3BB=v} zR{Yg<_)PT*r{KmkfpV47jl|D#%&yDE zO;wdQ0@h&;s{pRy{ z#%Z!Kiknq-i!27Wg7!*cEj=Fg!6ldR+IW}IlZ1MN6` z;L#qQOh3rE

yanDB1nzFy|%P|hKCW25l!*xv|5!K`KchnOX=w^`%+vQQtXfXXL` zA?yAC1n(PPAzNEuXn0GGZf>jgRXQqZu%TsdQ>cb)mT>eS0=Q$FkEwlXySGCWd-Y)j z8)<%32xVcqh}F`|K%-)L^PE^oV`CFHo?%(S(sN69y4CALFZkVhcq;6VL>0nznp)hA zC#|uLSe?r$PB8NI=q8erTT?BjH$;e?GCf;*cLslACs?ErY)jxMIiU@?aX<3qS#pg59iHr}Y^aWL@V<-w#M^20 zx80d6S@mtCpK4mj(>66r^hD*gZqpQoWDjMII1XbiEdLPY`_W%k zIZ2;gv>rd|CN466MDgmLb?cexrpYwDpmAXN=E_Jml?(@a?d$*$*AKh13~94E+t1gxHwGqjkA%?UABWlilI^d0PWYRqT(!Cvw+GGXJGlyx}=NHuyirBIlJPq;DBl-!1SIC1lBzTHb z9y_tWG7i6pwd}lPKY*RCHzlDWuxb4^LRqef_R98ef77|zkG_6+PwolxR9~R4T*oxN0Gx}kL~QQ8V*`swzgW8PFde*uf#!)u zBfG2_YC_8{?Y#9BsvM!V61Yz96wjS!HXJ04b)X5@+~~z=zi;j5dB|^`po`+`CS5e6 zGSZ!_5Bf46nYL1SGrstp;Os>zL8neb8N2>_z1;Fm!>TL(rs^M>k{?{xkorSQJEC>@ zf1#z75FZo~#t7bdDvH)8Wy({2A8$ZcyP@kVn#ix~+N%#-y{j(HC6;mzs@v3`X($Ti zli0MrNF|>Tv@9akY+h)sT|G(d*H43gh{hzYZ~t*lwMqHSy-_>sZ+8bW51jn+9vjDT zCUhhink`Y^sv7@W2k;mZs8X2E?l{W*0^Kc;Vq(o8$LvZ7ydt|Pv(R7`Y4L2lvYfLY zE1;U@h1oj_k1xY-2Q-UOblAxcq?4zbrpPYfKX$wE1?Bme%flofgY;8zl^$>X6kkCY z?u@JH*|ccH3lP9vrjFYnbZB=J3#ti2^LLgA={j87b>j`tIghRKY2PfSf4>iwz3V@|nf zoHEuqG|;}bqQLRcuIiu=#b%ae>NCR;2j1sqZ5WbhVCa`9n|Mtn-Wy-=kvCpxvEkQ+ z2X|_v=rbn6mh%CzMf}H_c4wTZP!w?iwas_M_Ks*XI>&pi zny|Gc#gH1C@%K6W{`$tb*PB9do1gSy9vrP0QTFhqhCGKOGebSyZQ0B&JhF+rpLweB z{qkbO&F=zj+rK62UkeOO4a+g#{@F3D;(>aPMwg>`qoYFn92(V86SGbMRojjyT77|c zYR*XaXP^F)<|@3ksl2km(P#-j%(29l^4g1jj)<Pw(=*LNJtoMkA=c>9l`* z6^8ddkO0ld?DGV3vLv7Fp8SHv?fgiPXMu^%OB6#6c#6zF95e{kQ~*L{Wg5D=!$@64 z<8MWUJIc8^*KePsFga#=wx%vhrenO=Png%%v&pj@^Y$N&y|C~)B&^Kdn+()PBjKR1wl@%o#T5^(y_)nQblZ!>b>D^9Pz=nR+MICQZX=3 ziPoI)$XHmglj^diF8z^Sz9Dug<}J}nPsENeg=YHk(@WdT-??l!d$}uu-9)#dRz*9` zCRY3S6}ssQUoumTZmm0`@pGCdZk~|Q(%$$|R)yu=j z=#SCA=S3r2fn!d0pys(7PFmO0lOs(C7$K;-c{)YwXi@|46#S{=Rm!T$SC5{OClan) ztiey$KOAW?^xTNA<0QtL##T~tnh+emguAD{E1pw;9&ju>@k1;^5A|GM^w5rZur0C2 zPI4*aZp4hpqnx?Mvf>Qk0pY?05mQwUi?IdTCf zGZ?f*FeTtAy5|i)fKl>QVcJ3IE7zCEv-MvegW*BXyJ)J6Zz=T1w7>E$)}2s<^Ps9+ z4mx7rliLH0`7g~e!I?)XlqHc|&GQ9-IY9kT4<(E-6qWq4=_Gqb-_)K}alzu(gxSgz zawd#$j8&4`V^ZXBgIi`EiBd3yKosE!1E`4E98WS=uy5Sa-p0nh0*4b#oy1FE(Vide z@eH=|R+*iNiLo~0usI)(o6%s;xyyRG_p{ga8Lf?Ln^}YMf$uNFq zjZcjJQGHPFt7Hxl;;74+@6_*a%2w>Q;N^dEx-L+e|5IL`=e`>Cgn!|PN+H!!a7S2B zAmPORKCxU9>h(_w1%%5u+48VsNUw2#!fCUGyPD^EM9wIE2L{(R4HIqJ=7*KRGl%xG z-cd_1Z9@jT!2FAyj{YPo*)A667s&R(LeKr`lIDy}0&pLDPClld6Ee1g^E9lOF2iHH z8}%R~I%!7roobLjSU{D+PTYTMFoZ?Y?Cn2Qf3wqj^fHj*A5m$n>^KAMxeZ3M2#=mw zOMT*P0fHGvzj78=_2@D4DSr!lk-uR`)wpqHEshB=TZ2#{&7mw}P*6WDF0d>U>B@zo z!JSfVK$TzUT4c*;l92eoGjL{TO0>0Y&9=Uw#f3*nu{d>1``9&t?NasG_18?ADw{&= zwO7V1)p5k#iG8K9LbwQfTN|!E$)bxpH*&G9VE1vxvs+6;G_(u*7K1ft&OcPj(>@!r zE2SH1t>EuHQwNOhHj_bRU}RU(6kX5MbzAv2=-Gi%u|>puX_wSdVV^&J3QL)qR%8%O ztbF~f)H=d41o!UmuflaRjH?sG(X8ceo`eBulK~>L+gYrVcVwaiiMWXB{Y$`?5$rX@ zhYh6VsnAiDK{|pOop*b$5#LBIk-7+hdDCIie6s&Ga23NW;SLY3n6Fq$UwhSVakDG1 zIy-jataT9^dmey=nyY-XOwd^%;|MU1M>qm-Y!!}bj{_Mk-%~h_2{IZRj1ZCbTsJkBVRJ!%QNG8<1+_I%dEeJ88+XfGw=-7P`;NIRV! z&SU%LG2^ebx6ph(iBx_NTEb@ZigX`{Q;Ci`*)ciUtfKpH9&Hs}@g!QeblK`qV;YZg z==jXtsi`?9GB5ebD~or|)nK;_)zb8s4T++{V%@9h#Eb=hqo1cb#@ewr-T5eENy}$uoh9QoeHbot=%sO&nO=Jt0 zAY}bAdk!a#w6b(6+>nqzyxomKD`wwoE}W2GlF_EM9x$SvvHftoKQJTtp}4zp zG1*JmVcgE4`mp_hFAs~?!bFgH?)1s1{fm(YN|6kj)9?I@- zm9>?;^FGHwyhe^^ThL;LF@CeY?hcju`?*igj!fme?gZ?KKP&wd?Mriq_oO>ejjhtN zWvV+u)!fsG(#`93Qrbw16+_jCC$T7V#Nh9C1p~=Bv8Ih>=gPBL7w!nt6y0Q~Jo2e~ z`O*$Yu`2V0n88J3uZW^OJIn$0NCTo?Lt^*Q8TG@*E(ytTZ<~L5c{BJ2y0>4;D4F|; zHn^cQB*Ew_9;ZLY8+j0K=KjsB!0_WsudeXG`Y zrhF(pj9*}&(CBQX?uz+f%=<-j@&thZdk-2+AA)(zpOul?7vf@xS5B6uE$dXyh%2AocjR4={`Na_$^)C^?HjSp@rZtzHI-D|;b+s1} z`7QWbe8w@(EYU(sn>aS8t5CvWz|LM0#YX#@ zZGe`@+DX#mh27jz`et2`*!_2eYJ{nobaT#LQN|Myl5NJBBQpNIDQpq_h-cM$FKdd7 zWa|e{E=J^zWr?ndht7I8XJJPQuh2Y4+q3=su%TTND^c&ck-)O}z1O(0i5hKpT=kC5 z`pk(zH8H9_{HhS2JMLRGH!2dP<_v+4H%5- zt{`|f|2Y300*7qiR+T>EUNaybnUNfgte3d+*k$*IOM;MI@A?XUdIJmyk)urn$Hv)} zRqtNhWBkDM^=agx|J8tvN^weaGM%)~qxlv1n>r4599IDGl%U2yg2Z;$d~JMdG;y6%@|H5CcB;DF;9F_u3{Sy z=``r%$6U(dj+4Ig#tZet`dm|5?-5g$rJE-aXGzMq$V2O^(VvIISMCm&lba zViynZxHUfG$4i4r0c5H z{K~C~8(x}Z9@u<8=9|pgR}E>Z59b88ueo$x#U>v9dCp~RgZfk5FYd}kL*s!Z(Ep|6 zeb6m7u(8!FnN;Cd-c(axs$*SVOn+F2Pjw+BHDti!gQ)cn@;3u*F)Sn1=~9kYY^ofu z7ekb~CXVrm*n{S1Ut;vYs(Y@C_myW5))Gyh`iOzM{v*fAYTk)<~Xcr|s;DMcuZ1S>f(jaP};ql}%P`2^^&|WFJDJ z<_?Q4>{X{^+-P~67vjl@S|1?6lYZCl+tg%ISZ&QOm5xpp8JoH66QJ((nr`-X!p zT_>h-N4*|xr)qSi92y$<2ub}t>PR@|s@h}!n`6j1GcvDQnu^ZtYyDBzp_<-+)%x9aAems& zXjpu=?(wro!6z}1mviYKIXZS>V^$Wn?h%duq}3vR^%akOl$6vvQ27qFsQYC_EPFng zRjti?$LMn}J;o>9C=YzqM4RlT2k&*wqbexZrC&Z)ATBt2ecB~YfA>tfvrL0{^=sW= z+_nsjniYvcX;qh@o9vz`&(!Pe)1DVQzUO)(P7*9lvPT+zx76cj+ERZwa~ODC@+|G% zLe68m8mVyn;?1SUeH(!zA4#1QI&dCyyTlDK&sLF8cN_=ewD>Ll$XZFzyODam}idTn%@33<9LTlw|2w;^}g^`b+d$lg=x3-=3#-W17_FcWnKtc~{goA1u{94BBs3 zH>}9fCJ~PR^xG^g;-{H|(Ak~Gtyc%zvz~{W=N#@^k-Iy73pAWvc2ezC;(IzDVi#$s zl0`+qCWbc|WUd@LKk1Au(dkiucBP9%`u(tJX^QZfh86<4kd83)_f6LrMz2Y2E8yDw ziY8~|@M55iDFvwr;x9ht;m)Q!gP&5ox74(oQt20KYAL%AY~4JEF*4JB@4SjD1AT$d z&+92Ph~9c__2Fw4+tl z-WXUER~WfMn|#n$>mVCSI*DD)FKU15(ua>I(zTWh6*#!IL z+n^;K8RG&}Nv&Y4TY8$Xz~r^qe8t8M?{6GkqN|tpN0cqr?q5+}E^5V3iIxZt1o!3! z;|ID(=cYjBk**bDvjx^Abo{6ndVisPtPV4|bzW!IowxU{r1=7%|^8 z&9KFaGq}kEjyhR4mfQNF52OFX0#4Igh7sba*c~|!DO%v}UMIQ28{C=U5CXJ@neXyE zYD2psrV|psE}oiF^=c-L-@P$a<8+WE3SaXA(~WTqJs0JdSEgk zD|W1~>bsLl{)-XeeWsCC_1{=v~Kl#+YtnWuW97_WtO!gSJF*xk1eUm9dG5`6%G+N}c&}bu*c@8Z zWWc+t_N9FM_Z39z(=o?1No*cFA2P6YZ`s=`P1gEa$d3L^{)&SOXhxR2q?fCzn{5@C z)9(Ayy4Hv;4~5G<;#e@H9-|5v?GQJV|LD~DaV7A`(-U8ej#!niuCZ{1`VzOQ9rpP` z2neQFHl4q1**Qp!>PHU91O_qhh;X+196CgV41{nFlQBNV=f-DpIrQf&oC&A+^{r{Q zifPTP>w}@Kdz;WMPG!)Mrf!qQvc=@Ykq9bjve`pES6|^+zzBs~d%U%EE3|uxu7X@h z#TL_DC$8&WQ5;v`ruZ{#`R~uGp=myx>mRvW$+`37J<713+$CW5ifh$Ke_G{gEuklY zH13L~U6vsxRp%$~uI}QQ;`P=BW4zZhW47SyQxXGwnV8L{6Na3E4~+#(?k(9%98E-t zLRyXZ;{(M{NZ1|hG?wSGv9R3T(bu&ql$7#`yx0xcj7#;IuYApVcE;(>WJgxT+^%6a zPGQwbBW>})d_PN88RJ5MzcF6Gr)?%Qw5|2}vAV;JO=g#oM|Ei8mq?cA(4rk(@vFD{ zQ%s}olaJcnw&7gvCJN|U$%?$bNyk1sa;BQj%X38BI%!IBnMGpvYd^Vq$c5J#=ozWq z7P8&x<=i!CBkjcHBwyBrjud+7mSR`N9`TaW5s!YO--lga981cPwU}!8YDT!PpU-Pt zP~6$pybmXKovZP!t?~NykalcTqA)Yv0EuUkh|%lAWX)P`3f+qdl z>(#fIvsHgtto?oMj6`VM@?$gi_W7If9!3g>TX^O&lI^@qR<~p8_hgT4w7(rP8nhmX z_SM5`mCs|PtG|iUVxy(orP%nH{_JwNELr#U>u~PmBjw5F>4CZNj$~oezO(lo>bGgr ze4(oW9$0G64@Os-=rOMIUhim4=eXs3HIuOG8pQgUQ)VGO$kAvX$hx0KcOSQ74gbuBA>~*7SayYFMXVm zg^SrTb$HR5?B1%#Y4&$+iphW(Xv+fsf@U@*$WJh#K0#De!b%oJlgk(K>Vzl=B;7O^ z^mmV<9bLZid{=P2#r=8Hh{doX=%H)isE>r~MBQOUR)!_1)Oie(1u2@cC+u1m9^?ds zeJ0s%QIXI199AT=9I|B3a3S$2EKaP`u*azAZcPKJSX)y-x3NFO$jhM8epu`bu|M2u4ceJ~9Lo@!;?ZG{wG=6u0mb8rb zMbRYQ^9^6iutdt7+5)cX_T1QB3ozap)M5E`H-??fec%(0XALLgni!cswk=m1p3?6g z?PVd2Fvapvng$XwgH9ZbPEPUgO;o;w|sFf#b6k!s=&ND5&^f)+{5^l@BQ=F z3f^jp^rzhj-A`y4q#Qi%FPM@CZq*CotrTi#4;bkbCyoe&a9caS9R5EveREi)@Aq}C z$!D@%ldXx9Z5xyACf8)!m^j&XC)=28+wMJmzQ6bU)%Bdt^=RK)Yu#&$lkXj=(IvEj zi-eShBgESgR!3cg!hk*Zf?#OQ)iZ9yD0=w8iHme0fUGZ5gwzxA3>j=nGG@o_q#MEvS6Qm%;kWm2ig2YtF^TJ1t`O zZNjliU1*BE@LZQ$gx=b1COX`$FI^L#;AAK3W&_y(Xvl%UD796;;JH>~{HJ)pf%n>F z+X>X?zOdOuWvI5iN&leQgTVMCc|%p#v!B^&JtxeItcXmRK~!Bl#$Nrv+QrT9LDtNj z`L9l1QbAk0hT_)AU4W1)2;Q_;un+F~g!c7C=Ex2XwrSXx(N~(?2ROOcbZH18wY-Sk zVEiEFKZPj#ks5vgjL}{d{nkmGjR)|67ludGa=g>Dytkg-d;;>9_|has;Kf4)wtgGD z)W93h@pq>B>i*Ddl+8ue_m(pdVbd+ZJx0Bsx90v{FR(fX5NT# z6N?$;{%UQt2Qkgw%eUh_z%8=y_yw6HbQ(5iqLFZ4pPN>gPd7hhr9;4UP8EglL7{H!*VU_7VfE zn>B;i# z-TGY=?@rj1X>PvWB1gSW=RQrSMeK-!o=wvGzpk)Q)WydVT*Pa;=gv8q+uB#R-k}f` z+TH-xv2DzQ;~^&$1ie3nbeNxOtzMgl)Y=KBWM?p_{e{-5yY8wVy~PvJl6B<=58(K; zonyO7B>ZR@hTKA8vAh8cj(9->h1L}MSOjIR=5(B{S2E?E2WG2}#1DK`)8NP^y9b4c z5p*9e3Uf3iRghqBt{OUZ$vT(#RQfbrQzf<$;A)Op837MQ586-AVUMk_8GGOD7Atvm z*zGEXL?oB5?eR)Klo1$T6N(wsF+l5O##+~W=xtix#J|?_yFuiN=%dKTcdzv&OBB5* zfiM7O7=K1-yWfhhfsD?9LLcTJLJIj>43ANk7jtG1>$$T{+UZT396op`4XGsv!4dXcwqhVxm_;-#Sk2g>JpXkZp|-{x?2{hTO*bdEz0>Ht^jq{AOEj<1 zC?(G}y)f7~Q}UJR^bAl44`>l&~pOu6HYArC$uSLIt~N%vXFVObo}UK zdAnX!Nm@%mOKgb)eUr>I=4K;Y8a2(AboD72dr4T%ZaA^NtA~*8Om0M`-hHr(#hbAk zcm9J>COn_X%!{YY$;{n!9DS3%kYfrPt`?i#GzY+CV(C!I*4(K5*GTSYqB5D^75cJM zFNiA7x_?p=7Ij{k3<{XAQX-;W_&J_=z!haRUEGm8yk}t3xDR$2N*^2nHzp22lB*dR zPsf@TXki1p%OvE~5H+^98A&<4GA^!wqD;WVmYmboO5aepZwwGXk{`2W)g-kKeQREQ z$Kx=ILLu}*)Z3bku@N#u|Mf+GhKNV)cpJXmU8jjTw9V~ne9npL7CSjrOv@vs;F8Sg zGHE&UG0O!Bxh`Y?#Qo-(2k$!-3sy^$BcS~klc|pYf~eLSOy|7A%Y@CVyK5CbhnD>T zxcR{*O~?^H_CK3Id=|-HR!=hQ#iN4c?XP@FdOVR>9fN<8vMxnepkiDOf6J2OY)>8? zTuu&dh#{=(Uz30mLlto4PRjp4X~^5z7H>zmtn?2^ltv!~WM#xg_2N&w=qFAMCimrA^tciN!&jUny;o|4oi}rM+I8NEA zNGU}gh0rC*uW4>Dvk&}X9S2Rrx<4UY8Uba4mVy>8e;m!PPxc*DP8k}#-mrpgSnOBy z{E!anrxMljXe!USRswg0oMtL9I0SM%mDvVwm_8ye33Xb-nFWrPA;lYkhwzw83v8-> zkOPge`Fl>jwMdjLoN>Lu4b_88%+yyvn>WjQd+QIWNZ1sq2;T0v;NM2EKS5@T8uTxLTqzDxCKP zzDOIz;`Z0ak}VOI%TE}WD@A4dV4`kxgt(fnJ+8tgACwCy-`c2Uan=n}DQ%xK6Cg;= zs+4-boi0?Riz7JzqslH1r@MftUl`=*$IZm6b_JDbL{Yjhm@6eHbijr9JArV)a!)xO zFFk+TICpI>CI;`A>4V&n76SvUnqza)uOre%yifXliO6BRsH>t9K2U!9{vN|$_tY-N z5tHqUtPpSr{fPsIDqwi@;7JM(HDdoRrNz26RN!X_H- zf=60_TQ4KDfhv$DZ2|DO9|S&-B@X__o80vlbpT&`a~U1M$_!y=J{pE4&4~``kX|c~;XJ^lHL(9he)4 zY};*u%v4?9yeKJ)cj*TI!VIj6G@Q9FI6R6x8OlE|I%3TmThz!K_dRWYP-CpsJhpId zX-0i?;Xxz~>IACOGZqLvAJ}uY^Gz)+E(3am)v@uhu*q}Wmc_R6rQ$iH8)EvNZEYRv zb-uhDkX95J68KL>$~>z=i;>pfKK(S@3A^U#j$abpUK5;o%qLj@WzJz1W~LodtQmEGE^&{KMuHZjmzV~(#y4*iLG|9cQEJx z;P4(QU5h>GBH*~Yg6HY+aB;C__mT^Jz2!pM;kXc@P9hbGVNnsnqJdj`J1cJ7;g={Z zSt(CCE3}vRcOxYWp-@Z&R~^Pm-d_mXjaY{at#0PwNb++g&?*R#cW$x2*@uv`jeNX+ z5p_iB&~^N=Y8BWIJ+kSC982YZ#q+@^wP~#DKeeUw(OBUaJgpRZNrY*O94WI7IC_in z`uMJC9Q#;3CmNif8+trM^v`ViCPDn_sw!tlS4G%y<^X7yP<_({L_*7X1Bs01jo0(> z0VB%|rU*LjCU}k{Xsbk=aUhwNCx?Q5iRaf8Im<@Dt|OzCajF4nv3m-m=HM69H|~J}?X;4?v>s}4rK4aSyQ!&lz#a60?67J2$0odhP80sJnH{<+ zkAowTzAxjO(MH?m>?N(oRL_Y`Vp64G$quF&w(VvLn66ovdRU8JDBFESRA$jJ#j<#5 z3aDp@Z%471-cPC$iF|L;`CQOb$ZFToZT(?4c!&jJ z79tXllbQ$mxI?Xk6VWc<4}9vMhgLg2YLyta-9AEVI`67Ari#D*4gnm5ctN{z^_dY( zh42+gj^0Ri5j&&OE2zeE53ko2@p-rWe|x+!NkF^J`hM>7_>_l7_7ahm)ED;k`7w1M zmDBtcu@@}3WThl;MxC#22l67mZ2yu8%LrdtUUB4{XXPF?bf?pN_>ffSCX}(Na+4t2 z4X`Id+ZTy+g=04BcXB7E#BDHCV}6NCHQ)8j=kFB%XrG-O8b=9#dK5jJWK0ytO>y@S z#}$xMg);hM&!SSM99|`@Dl<<#_qg$64sNaw9~=z7BNJldn}CUwQ~oxO+I0_q22 z$tXT8b7c&wTWj;&xZ@{{FaRqN-2SlDHXBD-{_UGcNyqtBCyw%TU;-~rhhtpPX79^D zG#j%Tyg%d;$*1ZVNwE-PXR@7FYpJ9`(w!KDpt||5@p@8wAZ$hGYc51WHWLfZG=3(1 zpPpf^&4xIB=_R37?#1t9p%-N2MD-|{U4tO7{IbI3Ze`y4=vZqh=N$8Jw$$4*^vLxm z(rfseUE9*y?GYvYc1Cg=*TI$EgzA}<=Adh&-v~mj*!msay^`evPGzrwylKkBuTeTo zIUdZ4i%hAh8sI$Q9ngLjoP~w1Pa(0yKMPaS%MZ=Z%B|2%Nb}RfpgnXT0i^9>={F8A znD=HzRk4w!)ro=++1-qG+SAJEBw9JL@2EV44 zv=sY`>a1#BwIqeZ!hFbr4kSff&KN9h%iEbLa8Z0$5&l-@B)6RlEK0m``~?#&Y_itL z$P9~M>^Bte&VY0M`c5^n@jFewbm|G=jeO|E#;Tm2?m)8XX~Mi^1WTSk!yoF%tLPa0 zH)}p@;*)^d1%8xZ;6t%c5J{V0U??6Ij?_=(M=}XsN;3w+h-fjfTvV$et(SbaJ!Pl| z>^9ASHPlGVL6w6hlFEReOU-lNbcHIEq4bW5H&DR1G6JEt_yc%_N-z2J!-zYsG)R$g zZ}_?(zNC3NBlSzju(5n;)Y<3tVM~LNObKkU9A%2U$9##JqsA5vI>IqV(+L&9td12{ zZQPeYhv49i9?NtB1t%;l^*{Qr_cK=s3EGIRw-rFH{9Hw-d^a1N6t0|5D8n&@B&TL^ z>77z`B3{NwqB6&Ihciq2^?bw*hcF8#UB9n#3qq`nv&A>df}A@v9$(op*}27I^N0JL zR)UQO+*tP*@>R{oCydWtKzZSGTY~}t_SBID_s{s>Sk5bOp9+v~rl)h_A6XG{+xjH%ytEd}B%YCCl%$l3j?U7y1`O;ot7E;tAB;I@1i51H!bGODINkNq zx?{;U@=68eKZB4$Xns;s#N!Oj`HPi!@N{WA!EK7I+CA}VT;FGKv(;MjXOMS+e8*yI zjEF9c15)Z$;;v6zPQ%rWSE+orhutP}$LnRBBdZMhf7a9^0L`Wb;KA<-mnp%ivyfKBpPD9yw~Oe~nyriDO_E#&yYHWe1w0wU-@t6iN#(FFgPiI9UI#DE!U2M7 z9nAVg@De4#f@Z;>RJWY|437qAc_TVVdRCi0Kt*n#M}{Of=k9sQkFhx#l)|SH^Dczb zf`$+*Rj%~q(ID`L4V7oL`ryWtpG1A&zsQ`kuu5^UyZMjMqmsPsZ=X>A8^UWs+PZHg zW10Cq&Dcn2yIXiE=>EZ$iNn_|NI`ytZ?lNNS89I_rZXi>L2iUC^V8{vdl%>305Ufg z)t?(9PTo5cHl?bhwb(x-ir<@)B-1PSDTF%2kJFdWmK;@1v3BhS2X0!6x)#6v-raz={t7uXaFYUjM}u<^oB$Z91XHoIoCkZ>}G}tb~^AWj=Amg1aj_!Gbg9!9_&qb}X z9iNSKm6*cfQ3g6|<0>I>Ghu1mVv=74XT6Sa|Dd@a-^4^sgobq1I<%d5ek6aJ!!($m zmZ0P2MQ%Dz)C>~~F1}6JWjh9HX}sq5?CI202;%*s@T(JIoK~nudmwG9>M6!%X zxZpO5ueR`=xXL(eYa>xHs_U7mF2dGenRUG5a5cgD&^8Il`ketkxB%dRw%$usJQwsn z#x-T^^5ck0t9F0;y4>!9!N>oE(Ccbdy|uWqjdTyjoO=Ey_-R1IF(<$Y_>vy9BY5i8I%k|htVrudnT6{}4(O_2w6 zo3Y&8jfue}z8%2x9>NrQS}&*l&bJlhp4MUNRH`xB0>Z5DC?kk89OY$L(p5&7u1T4m z?;qFkFojO_4XmZDYe9O@!0WI7zK{3E9guD^R05|=Nw#*O9xEJz-$t0|c+!!hs1Gtd zfNPz}@l+mxhItAU4kuSQuIn?Sh zoRZ&_`uaq8RPPQT$Gl$6(?_4fW)`CNUOUvqN@Yw{0ubUl)5}LxNvoJpdOO_-6O73m zPA2GEGRERKu$XgTk2&6J2*av+$+m`7Qy(;V)UH}Z`dKPNPjFW^DfRKnm>cZ%7((8g z3mA$wRpzbmMUYf%#y}YO^eFYE=1IuX#?Ee~CX8jb6hb8nS6<{f@Qm7F3Z?AyKj5}ox28v@4OaWXiPpNX=JrKIji>F_{U>B&oj=n zEQTfZ#DFKwICmT2LbTM>7yLuE;16_BNFy6V$)6%simQubm&lg`=z7iU5a_?>g1Y~- zNR-Wzmo?UM7W2e3fOUkb2ox-wP3Nn?-6e_7G;fXZ!qE5>tc?rzpG<*D|*d&M94VYV#N?$*s)YWwWA5=`(` zxWX#==yQT7TAg7q3^UckVftAws@A!?@Ek#Qr6Y*8YCU`MQQAdHdJ zjvP~tAZR+l-4Zb-#jwqP5e`VBz_a2et>1u$LQoyU>(G*Mo^vQmK z1Ka>1ipn7gI~M;l0XIrlhcz;5EfDR$O^L%xbJ^B;7@6j zC@>H2BnWK0=P;62X=GUyp|Ukhu}>M1lj@+!0R!XFzO|vKx5F^1%N5;yNbp7|C$ylT zL+tpClcR43mIU@buutYzu%v4(F;qen!OZwGcd+Xn7+m&jSl9vK-KJ^$4CZ<2H4H9( zP-_26l4mu_vG?CQr5yd|M*dvSQ0HP zG;F4p5xOMZ$}oBFkHN61;(|jQX;)`|*~~^P9-r zW#<2d_`^9_kd0j<_p*@;)9)z7YkJu^*$F_Ayw6mKyESvVO;@Fxg=Z=c#TqyClwcJF znUUHVeO>qkEkxn#!tgyfYU3BsCCfRR^!MuO zuZ0nPP8 z$84Lurlm9BWTElC*xNf+LXfRxAgFCx>xIm*hZ$w?#1J+_SI%1x-@$+M8-bH?MAxIV zPHphQKV-U3Xk90DtcMJRmo!91X?>X=P(7Q}G`i}!oab=cfjV|JZw%+NO?zc=m#8+q0 zA={1wqLE%~=QekWapb`1YjVp_xgIzN-c>Yzi8~dEE}!G2D%NgJN-ZWNJY!LQXg$f5 zJ|x{!npRN@N*x6#BHSW5b`N8mDOMNo`DCtQN@rVz<${lIl_?3C==VqX{?ht6%Ivhz zCuF`FR8(qb{I1(^aJI6nW)5KI2SwH?&$k^7CvvItJf)Vx@2tPnVsvWm6pi&>1JO8mdyzQIw%4j4th{XVRv|YqAfx0Ld2sbR; zdYmZ#|xU1|WO>Kt@3ro)@zC9~Lw+_y4WKzwm#t>Z<8oD-!NXc42%2|fgwo8>$y z?qrG=-)Cz>YBX_7=oI=;44 z?2gC1>ae7x@jIG>TXhR4HC=>2s`njfP&oAPBAkFHs6d|m67>r z9ug zjW4o=lO@!=D{$i{O>s&w>&6d_MggUmuoG(ZPRciCaSAm95a$)NKDuGJzqQ?L4W(99 z{udPYhu5O?v`%NS=~V15ujKl|s^d(`*yGadfB)5xWTt{HNy;XZ{d4UXGbIC}#A)bs zDHXxAO{_|%%kZJ)OP5z8_H;}A#cwq>@XXD&mb{a5$y?`QE;cp@QZ?H_Zm%jUE`sZ8 zb#O>s?1dbG{|IIii?q4@1tS9NDo{j^qBmmR%AeLyR(Z7>g_F6hIs~983fJ%K$~}tl z1t>4dkLs$J6*%3FlNYER@sz+=EC$kqy4u=TU1pLhg3@@huQ?rxepZuWhaabfaAStd z&r`QX7MGr{62eJkE{)MVyP|PU=ta70&qRoWV@~V%{>3*%uHG`yOkQ z*M3zAQ|-wCD!G~zcLuNKyeRIWl9Q@F-f|?eWATk{v#O31EW$(|&tC{NvHS#TCr5Ne zqT)TJnW?D>s8yE^h-u}UerP?dZc=Bvl`7S|1uqxF-(NSA7uE-G0oX-Apv=|jLT)Oq z0Ay2xNfoD>S;fTfijG~>G(eS_s;@7LVHBv1IlXM&gZ5UueYO2(4PI5Q_X)DXmn(S^rC;^+&PVzg%h#-f0y9 zX7&$EnD;@i9PJUDD~Mc*fqUj6(p{T^%(7SE`TT^!DH`BJz6qmPI=#FVB6@m_xKo)z z)*gpzVe(HKuBu4X`M37{tls}2br`_Ce2(47q z=}>l}RMhx$;lIhi&}uxw75rO0u!sgRzrP^sdY7oTC&RJ&D}66hN&R*@44yn?^1CM} z(=H65=7}n87WxKY*YN$B6SOh-zLffv-YqRo=Z(hMl_vD%V$SWy87y3dyEV7vbJO?T ziyj8UyN+7#YCWY)>P)ZX9f^-PAPf}l>33R{Fv{O5{Q1s1aA$;TFYXR_3wpFihe2QX zXj?j`DNuWaSmjFBE%1?yZ;)rnNdAzXdDnEEU;8n*@1R*WJ?L}4XdIL(kqLXH@h-AO z4oSH~9BaByOMk10MAWZw20n=UQ!qBQHu?Rrp|$oAdZ66JAL-de+kaYRFhiKv$@NQ! zbyuux*Rg)rMNTgCSkkJ3IIXm+I*HdA&&nbj2$h`rdo5d)=gEgx7Fcdo*rbw{5*nN4 zg#3O&)LYtXwOc9n4Q=#ZEVmw22Baoz`X}U_m{kn#$#*$Dw_$?6bhRh)ZD@_MS+uMNNNXl0%%s%G$t?PWw30)|# z@tN}ACv9PqW&pz$%b9tiZ9ePb7q}|xI+!*RZ2D+2hVgzOITcz z#Xw#$x(&^0ID^&gK#Bvqw7amq*6QUm-o6`de~uJWBulz$((u_POyXly;iR-lPg>*o zh`vXz+`kD&#ku(Zm1a=a%wFZdXeR|nmr;c}1}%sIYyYtcKO;lWRt(ZPSG7d^&Okm- zoo+DvsCHR?is!wuu!OTJ!kCzl9Kvxk&*eVbhY{W|+zFkG{FRo)lKJK4ggy08VU$6q zzxu+t>hBAhT&iActOEE3$vw7qI@B+$GERMFK8vdb-2|+B)fi0fBnE@#pe0#fvW3)x z86ZwwN6<*=Kiu1>Rz=!L>bA{j{8CEVNaR`oro3g1fiUJhZX2-($Z-?x?HE`H`aktd zc!Z!%^98xVUx~ds^R~GZ|Dt4@OJW7pkIzH}U(q0ex20cVahwQ(z8K|oCw*~u$)TQa zWS4j`#N>dN1j{qXf!p#*j9}jw7mC*)s9@o~O1(~sLx3$C`l}pziLD&#dK;@?NQz3L zCsqXh51O=~p)#!~S}cGm$j~vVPKb=g^q_y|?oHLxJMkTms=<;;A1*fbMKBA!Li*AI zCoGI0h=@2(%xZ8r>oUp%3go>K+bqS7UsB^OM_OS0I0z8X@|{2AX&?aSv)%ilXJST1 zR9pCo5By?YaptdXGdHxCGBYH)D;5kz3-+RPg)>VRVJ%`_`gX;)oYLKeHlf3lB08p~ z7T@Ny%YSMSQV6~EIjB-%`mkes5%ZmtAFQQUj#I(C>~7~5vjw@?3yCo_Dm2ERTz%`i zeeDyFpFNy(9{iF@6WzcVWY~uyWbz=jOCDm_XR#KlaKuq|O|DPMdNVwY;a879SHo1Koec2iU9^xGKQ z@ego6JTcENcmd{U%sy`Ql6XCqEcr~VikyU?<{ufTEAv?q4>emfC%WOd)vI<5{F}%H z>Yg9+TOBOPA>+`~R-OK@Uh$>T-v3Wr5NR$d0M8MQ{>;6wmD$2@MF>;~S03o62#a7S zfhu8~FY;Tht`VyyjchpTOy**gbfhfPrcbH`Y4(6QY`K_Xdjr2jwzN2uDvJYVTVF!9 zq&NOEn*stw;ckNhRWiq0nxR=mYQNax%y_PDv;rlkF(}m5fjt%Kw`k$CKZE?q&Zg8K zm-g|y!Uz=?X{*3IU+hl9M}*4p8K)QEJB5_Jc{gtRmPXt;2ZJkxcwR8Gb?k9!k|+c> z-tqAGXR4CRa+ah;g8SYHL$>rI<9nmT29EfE`_TS}Ym!xMFrVB5IGuwPpkpVV*lESO zk^R>egY6xFM}}J;g=>GPcUlr7;uv?J$qC=(l>bMv@B3YLP?8V}*|pjq`8gf(q!&mE zHc%7OFGMZKK(=sDEn#t1NVJZwFLS<-t=j@DApImA!|8{DXYP#kx-WnVF-PdTn%dqN zWT!_MN}M};OMpmGyKrh9GnBThT^!ZaX{t^7$UD5UZ9hgKm~&UQgLiZ`=3mw@{-T@_QBWCBi5PlSyj_atT| zUcQwVe_~W2k z5njF`qg0)G^$$?SIudzKdcgi6dY~Rxlk6(`L|fTBMqK|U7gzqEE;DTn9YP-$ViE63KoI?OM99t z*jez4@?EAhfa+|iU95?@4G}2b%nM*X zJw17pola!&guK3b-5wgAmUDo<0L+V{(48q19h>Su5PLlswUhwny1ks;*^34yV|x#0 zb5RP|7+IV?+8k%BDXlLovjaI^AU7-N!T!io;l1+aTe~c#qqb3^{fD>`B={%8TMDx(ZHw?Y+vVRVBZAT=CmW@IOA~5 z?)Z)W>nsQeVXSZ8^9-h(g0Ti%m#@3rCFxbK%v#nmEN?=B(319jY6`dClmRHQL{vX` z4A1#PNblEkrn)EByzqV>NO_IEj-id&Uz9e#k<7IPBxSL8W^G_XN|&js4I)K`HHXd2 zZM#aHBC=dTs3Gtmo_Sn2dBxbF->=6r=`{oj?vQ4^NwX#@52HsBba6zF3jzz%@C7SZ2<)0cj~{E zXd^b)P1^O!i$FO~gJ8p7&;hq7 z%-IcOD)%$ZTYW0^s~8lo`lrE7`VzhET9q)xAMIk>C*0FaF4R(D<&58{QpnTOU5TCr zPW=0~sQ2iqV+JE`gsK~w_l{D6H(**N2UX~f5HOw#q$8*a#Efb_R05qHgfsS|6(FW| zO-RUCVYCzY2Y}K8yc}SQc~>fPP~@S#--8fd`AoNTyHFoXbv6s2{>&2}j8K!~M*(Nh zrX^@J)ekX$=cT~F3FyqSEw!%WFyby93h!kFMocE1#0F`I2veRjtq4SCQ?EcvrK+CWvF5lX8RcDx=&`yuT5 zqYE~{H!z0g>lU97>gj$khg`G*=AKa2-yioBPp`^j z766|Ne9d<^%rg=hP5e-HAgQBU4*KhRd1|04()JE4>i24}oTnoKMS2h={kpu0vg!Z*_g5H$4 zc=M2#_d7*VnuyFkV}X{@8MnR*iB7ga4l`a=9q#Em6ULu9JW#T#{1Lyd+#QnY9exXa z$%($$`t67M*y!S)(7;ux70umm9ITY|HlF&L4q;LA?*8p5)konU!mP(wKP{iv*7+`6 zA+TFx2J|3ujEGB?@CRe&A7${oyibES&-m@OFk&k0-C7^s{_fsAId72CwNa|=JBu84 zD?g6OOAM6c9k>M=sW01hq*fGu^+B22^ws(48eI~zwO`S-TdnC{G)jC*cvpx|>6FL0 zQ1Y7jXB`i5@vT+X|9JPsWZ1o^*<$sH4{cP<1ow)xtW-3ohS=M`T!y%PUH2F zF7kZa)s8`??8n3txwT|=M9@*lxec)eL2y5zWa_Wgp&LgZ-hnE_9!}? zy4N#sQ<;&z^%2s^rUzB@XUIL>>Of8Q);c|c4u*c zUdRq*BuG)YbC_3avAa2UcFJAsmx=3yXcFF&dW=N_`SgQv?rf zpQU1Tb<-^MZYtv=>rkboehS8$NM|i*7Akrc?-iBgG>_M3J#m3 zG%R$J9b-?GUxD5WWtaWg z;`_vu=3jRg@e#Zt1D?C!0{HkZbs#7&Z(=mygr)aGUVUJFnUK5g(H9a_m3ylXv$1?; zMeHiLkXSJ`6_!84kmk`wD1$fb&emvj0Hj|vGzx{cE-Udlh-j>Clfp?`n;J^eM1xJf z{ed@+NEp>JG!sNCmU+XporN+MZxp;YTC^m$$$O5;5ByEB+Mk!eJ_3_3z6?|j1@(ja z0}xCfed9T>5*FbkGr}n~SZV_bWWmfA;qg4aBtchU8Z4c|tHCU@55_MhURE4?ny^6g z=2%5R{t)w;C{N$alE5mz=1|eCCxz#Nt`{B1G{s(#I;YJkO-Ncl5GwL&hBnG!+ICP+ z>0ni(H;>9f;q;$bdgzz(uVJhQ?s3veFO!p&;+u(Kn_S{NNB;iDo!Om#%zLR-Sr^`9gvAg>J*9x9oki$@ z!IbxCL@DRBKzqo(slOCyw;3aZ8{g=AzZwPnWOPdfBIS2@NHS9Z+veY`qT!2+6lWmsPgA?1sV=EkA)|I z=Jm|LL??9wL#VEZE)gke&QO9i8!i`{2UZ(g@vZ6gvOQ(4U9(4WFxeI-dK0U4#)T1N zwc>s7b-C-fIJ5(NSyl4_54uHk@H*joD7|F(bl()u7k-&?`joGmE!WxMPw;lZkx;5+ zws9`VmDLo0#$~r|uBbgnmmDI?=XauVI!D8K@I!;7Qkl=SQiRDnvok|2lA>D>&5439 z$Rabf6`F~;h@s8IP>LV56uC2B7q(>{IRbfx_%IZZySKNF9TPahiXE!!SPTth9)hw| z=dm=fwN}JIaNk`Fv%dcSu$fk;CTmf8>7e@SVBGra^@}#QixCpi1kI(hIN^Qat;i=d z8|G^FxRg>WWiOt*G%uC$TFdZU#)jTKRg}$L5l^}=)d5w%p)<=1c24R3SXu?u{BV7| zs%ZSCF^zokJH4l*sIvp_g$s;}TSIUj_%)wM&1hbFf~%!~;GDrWB6s3Cj=i`daTSi< z)Ypf=lEm@D!r@YYTZ0}Q<@)r%TEs;W+9Sp~cv`5^cp2rNhSm?GQe}0FXa>iD(h&MB zYCWgA8o#P0`hkVViDiyMzFGo!Ot!&iFlqPia2G)k3JukLx1W4!O`fp}OUs#}Xr+0G zlMS1g0JqE-6hXl`HwNxvH*04eA2bvZe zQ2DS+c_HrZF5ga>Zs%RUX{>)0;Q!lqhzws`aFQ$KiSX#EYBb-M47zK=lNYuMb%eCA zV*i`$2%=Y*RqdrB`yUcHsmm2}Q|8oL=Utdx<6|O@AEIz}XW&WoOu8k`{$?j}?mWcE zU9zJC{SI-LoUSH0%8W92DPirGT;;cfU_?AvZ%!pZ08QID+`U#HK|V{|gDd>PQ``Lb zQ}G@cVp2{B4fYRJ>`54x#4^QZ;(~}T5r8vK!}VQj9sCB&+1-ck8R+Z^vKm_z%4^EW zpsq|XCFoMWFK~dIzux4OsVXLkp~dN^AQ5kA>Tl_30T+<#BIZ^o#fdW?@7LT$rlgp7 zp3w?WM(YZ?ENLSy^qn3>@ju0!<~-mQWQQ5}W1>Z3e@DExM}VMXcFYVp-tIal~6`Nb%iL4g~Jbk37Lo+{n_+>Y-nPf?*^$OG78=mqHhK$m#s z+BA^ZzvVwA_q^keN<>!bkRrYn5IZe7ltJ>khK=3yRPT*O8&Wj)uX$vc_N)Hn*g(JH zN_Dd`F)xyLb~;KkXL6>d-$w9#=_7O6%41D>js;bW#A&MH>$7{(jTb(s(bhD6+r>#r z%9?xJM!@qOBc_3_&3L(vh%B*%yFbz1*9(tfLZ)}oCl1b*A?l#~BCBmNYD8omF9KC3 z%gIlfaV$f}&x*0Uf5q1Sa*oH2I%1NO`O!vpiHPNrQrcMdFc}m{2zf^L5BcIeJ4j%-==Xnf~3JB=isou9Z9mMA3#1QgX zT1(=628lG1GPko4^24YriGv^BjX} z>0uaw$!~65Zlt6vWdAFzg35n_-H(u>v-wIeiaB@Dc{%c%@_*MwMgydUSScRXnbO3E zXS{=_Ep1E&j;J9;_IlFC`fB^?OoNIBWL@5JkkoC~AP;YF2cs`lNLc6)B3~C7dfSZa zNxwCH(l-N9{8stwBNn(eac}C7Fp1dPAlB!Clve>h(2EHZt9F)&Y6e98Rg)DIa+sZE zMCw9fGbE$2yoB+3NA%%($=54aomFdoeo*bej$?jy@%nnj=tUMX9EpyO@#mkA@dziL z8rxq%WYCjPzumAi_ou)Ey|?|~?`pa1zgsnu81 zVulhm^pY@&)8ap}IS9CE|GBk`MAEX}gVvBN_3H>58D2Igx#2z|G}K<(QyV!ht;PHA z@kLxAQ0af?1EmU|FGozuq9jrHTl099A0FB`LGMKqFhC!zIQ_gn**_+r! zfFSqtc)8LLTlY?O?(;%HpJGbgh@Ju3)}mS)V&49D+(dg)+}5&7f4JjkO&A6@p;^$_WnoG=;p1ihpc_atds*gJRuCc z(2JapQ0%>%-kJwtrNYSh@*(^N+}4*H1l2X>SqT|Uv8I4gytW7g=T=q5D*l*=@09_K48KVviop^<>{-a$ zZhXslv{_!&DqB_5$Cg^fKYqgR#I8|g6jNEu{&$VPJ^8ARyhvSG_|;Xdv6+M-mc=0f z1m-YhHd{n=t#ia6VV1b^-dms|>q*4-x{OSWAY)>7Zv;IX+kh&lBt|N3#4MvGvcKdC z+8ZkkM;eVISgg{%0MyTV0YZa5>J|q?VEEUzxm_>w`OxbA;T&GarQlaO>1sRomz@0F ztya`xw+S1aWgIeOA%kTk->Q-S4LaCbe;ApN5It)6$rmz1cLXW^hKAyX>^GWVZR(G9 zQG$qQZF=^)p#HnoQbX)#|DD~XH~!DXv=Fn_Ip{Z}EW_OsJxkn-w zy&Omee3>YH>dz6GGkEQ7Aw%wZMmViF+hh1#a`3+zu7#xf02!8CD=#hs>Q4Bzei0Z_7Uiqd79WOfmbyQkaY2!@ z)7V`~L4dTDLUFDZQR5mEER(NshGgx*Boy0l?OJOemV}_ytkTuKY)ls&Dd6YJ<(AkM zikozrX&$+;5A*V5>ux!#Q_k&yh{IVMtDI?2GCUmcofV*MOto1;F>#pL3~O0&exRBa z`MR#>4$oXd;%;6;f2tmAV0!9G{?qh_H`#;UjJYAh94AYh2OGT8!b`7@DlulbXbMJ0 z^<#@fGXe=IXFVfxb_Q&d7e2bkx~68WsHP{49l@h4>14#-BD`uC*(3G z(a>N=8=~O;7d7QWY1Gj252EaPV}l(T2<)!4&Fh2OFiD-aLbFa^8eaSKR$@+0$9@G4 z0>+M$VUAdUIL#KXBX7j@U6@5x+rxGn2#$61?^&O0bC_;PH~;|LucDBipRgT9JH!{6 zhjtBxU}#!zoO=HJk8*6Ua!jQ(n@)<1$- zlo4|Ll_mMX|B8u+~8EMxtw7( z58PGIfc<}5ePvjbUDPg!bT^U`f^;az2m%7qrF549(%m7_Qi7zEAl*ne42^VmcX!9x zgYS3F^&Nj%Gd#~;u~*&qGMJSPiW}1j#KlFe)2@pkjR>Kp^pQ3kRnPko@q;>`Gvwn( z<8JETPyUOYvGHHQoI7dGnVf$S!N)Zv4`|?4tQ`P0D^#bT(?sOvki|Uq2*0xg(|53g zpntN&Ns@!xb>MgyQws_nn99?u7pydR$#q*|qLD!QqDN+uB-_{DX5NAStwdd6e4WZ<@0rfP`N4d;k(ZlYTbiU&!Y}D%PkD(Km)b9oFX69C%0O9~h%WU)eaV1`8 zsRP|u{Vx!=;vSR{g+HTtB<74M0<72GV({IA!Q>8@VK;uGUB+IZ>Z4c%2Hk_XxEK$@ z%%GLKV2)l^oMcBf>qylmjas2mcQ`04W;!q@=$-KYtOGrFp?zM`lnv#wYL_dD{9ZCyK(+?Y&#V;BvwO2T% zk#w&OVFZ#)tkXC>@0jh9{ddFDd09M=l!~R<9eZ^G@Yz+lJwy~!!DZhrYb|L72h4bD z=m$$SP#K8zp8^GBaDt8DKI!evLBI-|4hRYt?F1q=$|qi{PZb+X{OVRYX?jCcg0e^< zt9c_SikXrWO(EdIgZ965x?r~irqiwiQWGyrB5H$B;G)d z|0A$_ad#wZ|61@8#b6bWBa6G0GxWuO{ zUYGNPnT!LEta-8N0@{xI9r}dk^xp^nb2^7oq~)l|PfeDC|F2|K*VmF3%(v+RLeGY)Q&w@}y|?Pv-%MR}0?$s9oAuu*2ELie3?P35 zM_#9PO3~PP^NDz6A^hSdIu$TnNUefr`lxYK}6z#Lm(HrB`q0E^0;rhIKF zq7>poR;|waFsfd9ygXSCz@nwV!L%(3hIi`!j;_oMhrNEvPb!B8dW8DH+N4JCqz|Dd zz=EzH`#aemd;Q8HPZQf1ME2KHpMP9twnhz;8txO>&pE9yRO#R1{(+vF_!(JnK=!&O z`=+Z3(SDb8zAZ&}Ukh1lV74hb0yo>H4N>~^gEIaLg5h~Hu>FTuc>Upr-QzHxunQ)8 zVu6kOhM7-gTltfhmp7hTy&$OB6!<21tP7xfM`I^2O}TCPFB#}5+HJ8`dfly0XLLsg z6jQR3HoRv3U&{bH`ww~bdZ?+q8~ncsu0f8Ad&Pv#5>7nmF%qyvqzeAw$HU(+t-yRr;1+`e%xkz5e?iPV4Qur- zxgLPoGtVSG1!Vv%XCh;z_W(fN>y@>F1c1GNG@A^fL7=(eoFI;cT{&)l48H>zw4d9e2?4=w~J?IbHvVJdZh*Z9+IpT44i{#M5IJ=WcQ4K_6*y2e#iZECLs^xHj_kLAYKh3oVxmliovo=6dmO4=__EhGAB_dey>2>Aqu;&!~pXc1Z56@b`fm_&CobRMIg*%Hn zZV=G!TmM4JX6DB_cc2;E^+p9+6ZFqoxirL$oyf^6v6o$a|NbXlSR_M68tULPF8sL* zLI4b=y&GmQ9{?-C#N`dKnt&c^KEaGoG&mLNjnygq4u;Hy9IulfwP5?ZnG+AyJX64i zz{HK_>0H3k*wKUj{ZBGlXm>1kV6jXaV6y;y3x@VX$TMBp=shaI!D8%?vjB~BYiht( zIhhV%n8N($Wn{RZW+WeIBL@o{Ui~xcxnt8+Kj2JIc6mNL9GBP{+g^7nnoF(o@`SH0 zXRBfHgU3Amj>y76tHV_utb5gsx4uBPS&| zMZ&Vw{}FfKfw^i6F-GIXe5`HYR>|*J#qYKdWvt&IU9>m7!-mLUj-d3%7Ux?%Qj(qh zf5JAHT`2$G!k(+5Tm%;k-2W4{!pEX6-jglQGCS+&2fssUtMZ!Ku}4*H#EP1QFa|v4 z=Ew~dI9Nr1GCU_UE?)NHTjdx{mG_)|3d25-K42dj|9MGKlC{f3aC+v~7PaJQuI7KO zG*@xp^sE$521ic+Bs+rtsyR`+ z`N*8X2ugGG$S9=-?8`Ica!eV2lNord<|-+WM%nqdJ*4_P*=N_yl)UE$_R}=~!r!HQ zk?pr3*7F=dB&5GW)O6MQN?9BHsO1E{1ZGo~m>N6_=q@fVYg^M#z&Y(P{TC|0%;=A$ z0c$`vFn^(*P71ZF6XfC#N&rCpv@W9B?Y?*$Yi z?BiNuOIFT(L<>Em73RalhVDPNK?AH`Sg3Z+mgMvJ(Bb@x`7Zt3ECId>b&%A-+<=Hu zzy0Sqb*#-ppfoClTVH~&pTGloK6rbEon1&IU_#$Rk_OwrggFS z#@9uQDj|R(oCsF5#w%x59zS=Dy(*t<`b||AmIjL%9X`YKQoBL*JMDz3Vn5&_eva4~ zR2#;RQ+g71&_oXATnd7$_=*vdzvJ8;rHQyq?PFMeWO=M?eak_@z85A8eK!oCHaCSa z(lZN{ow?n3aPtjVV?FQtq}nPXvJA|} zrozxgLhcuHAf_rEpDUqg@M0|NA@8nOOeg`dd%a;$w!ev8YD0WrK}Xh7PH3OCcH<*S z$kAJFS_VP%oLCjTHK#jk?H&K-l7R1uOF2SKN;sBZSOL5WG~i4F%=t#MS;2~YqzT3Y~^Rnnw0>SWs!TDDGK9I%7*0;BJ?BFQEfykUjfp{X~u>Ets*O{UF)r@PR>n)v_0I#q>$X=di zU}!h=7s4miV8&h6g^$k=i{_xes!dzhBoC*?3K%o_PNJ)Ex(R)Gh*K4JX?SCF%DD?k`+l0mdy1kQyK9i=H=Avj~1?L(W^*cuADZIqXX0 zr!FC@;Z3ZX!y%x)M@N&EyLH(_+v&Xg5|(_W2vkyLWhJ&+7o~y^D@l*U8m0E<3bF99 zpO0`@5G_R_4BGZ;(T$$okOJg`x262T%!nY&<(gC6LF5`e@eufs{|4YHC(%>BXjDq= zR}D~*z9v^d$gFn6;4INU(0mDRW6OXWZT9KFmM&Z#dM#fc;2Ya0J`-0o*nW4ncf{Aj z{FovsuhL=f#shs7XEYKp1~vKrPU+VFQMYu>1*>|&f;eut&`(cjz;AG$=qK(o10yNE zI?&Lna(NHTrsQbA6r^P&V1XUDmin68;q?XB7vJr3T7TGL694X^;N?W<6E;7Jm{?Ha znG3dp%_tP7Bm5Dx6={AX&a(M2m5;s=$A?Etd^Qu0SFfjK$=;hN&+AUwMD-oKW9A(0 zlq^U4yo$-Z70<@?2R?#)o0eI^U5-YxNwg8n4_nXFz$P!s1hnGSA#H-r+tm`M_Q-0> zDza7oua!z0z1!lVm>)nGRJVBl8p+-jl>KUoxa8bJeE(eQDXC9+pPQZBnf4r_7Hh2_ zHSQbfZ-;6v_YVW@BtRxZu3I|h(mA30t?Kb3oBDXa0+a75!TR_-dOwYv4dwLJ!SQXS zzWBWf%NZ#gmGq`~SpbKfG|7^TTsF(SlgAoy(7>4GInNs2tTUAdPodCHNZ4gywhjmg zV4O1{2c(&>@6~doEg5R6-kx)@rTe-QeI);sksIgMz|8rW$#ej(Vgl+JDkw&0C2mS> z92qFH`g<#EvwptGkm9OS`&`yx{3NmNr$3?~dt{3e`WoGxCRY5;sAB0KE+&%TTIKCGN z*4s-o^Y0*ui`$%2?<2gc@Wl3C?BTd?~pkQC+L z^iM?cxSMRGh|r{S*3fys1gQiA3r$*(jTf7@ei{fsVvqcvIlPMG4VG70k4m z_YRJ>ESTQ2tT&7b>^k|yLenPUvjv}2K zFK^bywb>J0?fqO-G#J3r`(NgKkGJO}nhg#`bKdwvu0aF~%HpJGFm;|YOC#r*4B0p8 zqRyoT3_I;T`}5$gOqeOX+J6BHVv1~IgC3IJWdgtp_LUFwdkfqbziXl{YA(ZsFpn@8 zpIA#Vpeh>j`bJKlHkIB$OGm7hYBes^5H`(-`&21iruxG;VeC(X5HI}Hf;Rv31mS4{ zFZ~v3edRL1r1O?Mq1^WwdxCEC3g!cxS1!0PSK#I(?(?(xID-Shq@-S1K=-yUAo@FlHYE-c8t zvtWj`{lIjLy|^3l{#}_67DC&;)pn)<;~N?-K@9?wsUj1TVtN<8msiN~ zYZo1AF{(q>B2|@WpOt5`V4&2%kd?2$&L~|YLdeuzcS(P5gsJkk&S$(8HmU(HN5dtO z1m$>}U!xusSikNEW|z5{*v<+#e^)MYYh}jOJ7M8*skK$p_g;S%Xt-W>gP8yM>FfoH z!JT#gMVZ~s&|Mo?6$OF;cVP_Gf`}TK$U(I}u6*p)FDtePix2Pn4&1(Vt|n zqRVX27V^T#B)B4N>F6DqC@$fM#|@Ub58)I|in$?Du@{qEc=&u4g&^Z1`*dwwu+wj3qsQGINn?ULwuRnpHzBAh z#!DMZk#I-u9__e$Qf8i@UyzqvzUmDd=mQ64d;fzO;XXO(Gc1MW%bh7*&iAp;V6%Rp z%oBA@S${jh^0M?ZeLzrlg~OjVj)X{maOvb`Cl0gjOlf@ac_wR27P6V{C7QE_Ru#)r zR+adBTvJvm1htr8%BkdJT`SchM9W;FCeLh2%`n8tMG;rSq;Rgaa5k3HWDZgm#8OD7 z;cl1g0`F6yE7E<+cZ5Cmg^YyuhAo);)d~VrdbKA$S;c)2!#o6eI;iHlB*}(S-ZRoq z%>k2tP4iL$^4;&rU-nSn83Epz@P<-^N0Fe^)Ur?ul)zgJKqtlMR=`t8s9XOS|} z^rS#&BItjZ1WDZs5Y;q0D5Ds)x6>9tk=4SWGOcZLO^Q@C&7KW=o|(^`nQ=?%FE5WlE;8`bkFzWuefqpLj?SA(?PQ$t~MC;jVK`*SK=buqG z8VOVSk;Py{?F6mg-l+IbO$Vj208Wlcakj-KDOR{(TSrqBAr~nU& z{P3G5%KA3X1)UGGLChq_{l~?(>zEk9jeZl-PN^|E$%zS0I0@eaDEGC#jAxpUn1q=s z&)$*M3P7-@ohbEww;*I+vB;>SbxRvpDxH%!Um>N_f~^Gk+I@gnMR8`$G* zmd-Z1M$zkhfcY}5ABvXT5})Jz&5lWwE^vqY38=7xpuH_Ar~@$MKxD+a2UnK-;#94V zRo;;~kgeZ8%3N$7!|v9H1%zMz%e+>9jyti3c3WrKF~}qeiIW@Zv15 z_K`*6iW>JML{CjM)guolK^|on?Q_YwLaOy!b??D9m`XRd%9!iQmlL+id(U;JytSMy zxyrAMG5^+j+gNG073+rnWs>3hj((m^rtp`7u*Tb_^bgp)X48lOXMIYPoapuhS@t60 zwap?^vitDGI6B;~`rwHRbKd^OuP^aE^Ofc>L*nfa(Al~uKCOOSX)$}NGU|y9%FxsR zedycfJM~7*FeH@ic+sFNUG5>Eai%N?f-dvIX&-wv@Mob(3Fy{VAlYBzQ>6fO3&)6y1 z#r*WtKe@2`1(7r|l8_b6nyt~v3stfkQx^`Iw`9;T2;shRx4E+1GAi zu@GFlAU~G8(-u3pGuapccd|c%*fe2fxl@VRweo}{Fv}H8xMIm(G=mpn!)&CWOpi@#-v_;t+Np4A++q# z!D&U?S`e$vf9yT8*^Zsb+iLXQ${tUNOZ$~vm1`YMtPIZFUX{F^Nt%-K9z^)LxS>Ue zHdeK0spLk$V(Isr)?%R6xmg78sFB!7ILn48Jmr6v(Tyf8_Ii26&Eh@IDj*}Ye6BWx zCWuw>wf5p@WfmH@i=@I==Fr@=lu`?F$6CX-b0U&ADjUqz_1lj`tP&*I0Mn40mtm0_ zZf_=PyFk{x)?T5);?9VX+7hd*s&;q4cpY<1w@LeeX|a8mta%r^ixs)G?IFUsXTP|G z%_S>x-Ef!iz3~Vl$?@*|6(C0xPM9Zr5v^KZ~Qhi0=sN)D0Xejjg>yOKZ zC0MXRTC9GIb|xr(NrNDECdBxIIBX*&i<|jiN!1`a$DlCcqTpiv+Zw?ylLd+Ty#a67 z#VeJ*cwELq@i)z#^BpUToYM2fBEmK12ahU8=&IZC194nP@-P+`%@}fJic7&UH3E{ zzHAGpdJu+2TuOGlt*3a3Fj)J0q9IFcoo)e)M?#JI{&52qsZ9m#LT^0SL(X!{3s(tZ zEWo^xFdUZt)iIjlhlBEdxLX$x&$sW-g(aE|lHgKdmJ@w}B7!2*g@?vjJtQpN^yaLG z#y1c(8*T|tw?*Y}%J(hU!BWR8mWa!Ptat`3`@(y}lW}pkK9e9aN&Ti`-A;Ho7z#y3YLj1fu1p4eT>?rBo`Yhzzy9RATN1b`Y?;9XhARy#!M%tJ zjb!nkieKiDNaxBD0=ELP_)a@u&9Ox$7pHk)_k+J&5E(*}yEcg%>oak1xr0G8Y|s*W znBYR{_LV+-xZz1c4@mt11$(DHT+`PS;>A2g-M{)Z*nhYoaTmMWAPpY?CA+!(N(C(Z z!CSp~@`U-44j~&X0bPle(#qDzAhzeum8|}`KT(jMWcCpi_Ec8nC;3@oPa8yj7tDne zHtQH3J@aC1+S05svhqT12|{e+r?T`fWuCFNPeT`Tn-=+`dvue~B)J+Q`9s_0Pvcy) zE6au41hl$uMr^0XoTZA*cWP>B<fBcJ?8s3~Q0Tt8NeuP8kd?#3z>T1%yhDK#S3N0QUzaRJDL&?a*eDx5JBs%} z$IaZc5W-LK?fxJ?KXq$>{Mfjxr3?Pb%a~DcruA(-7{H0A!j{>F2#xn>dY5;!-JzM_ zm+&jAW;E4LYoZkFwG(6MPjIr4xhSS_?j8y`^EeUo4O^mYRbnvn^QyU?o19Ftqqe)t~s`?t}t}Jaa?J_ zeGAXX`@#knT3gke!~0(G^FC5a2eJPB3W@Kxu7oLPmYI%R*w|jEN*^_g<~X6x3%^#R ztaobTm48~pOa~~TQ$Ln2XaPPyI@}Rh(q0}um0@Vr;d?+r$7HpahO|z{k3h4?URFst zZ~5_A7mV4EcfLkW$XU8%{!5Q~Ts#7Awb=4fR5>sgIZ6~y9wcI2jmK1nkpmgk&O67b z5(AHlv07_lxeia8gsP^A1r0N3`Q1bfI>;E+cIa*r9kF@nQBmWOR(7z5$$me4SBNOf z9x+P)kYtpb-E->x_JySj6W^#l7qGh^p!%FrHDic#y;faf^pU_^P}P~(%gob*=Sf-b zr)rx@JG9*2hQroz#i9A3*C1^?FNfmQ;57%q{ zvrC^Q*GvNwYp)@Y=e|e8T=J8wx(=?!VR3>mqAKNf?AAGgkDQV&#@V7u)h{ z`mAr!q82u`73loTt~+SYkQ$c0TD6T2O6b&+N8DP`;<-$hLvzJ_ugB-ab|eqKT+>>J zv#Frhs`I(a2~WG0V25@9o7o_$g~VFA{(UpV^bmMwi&M{^b! zCzsYhP1W?9a2Gbg2P0>;Xo0Pwe%4amC3bML!L-m$PpM4A` z?u#Ax*4ZspIvo^njORwk4CxYOS-j?+3sM&CXBvZz3 z&_4*PcSs%@O9N*%kWx%Z;e$vP z3zrD;OarjQ!q=S4n_Ye{vNpGMbP=A#qh@VR5CmtrR7_Xj9tayA!m-`PplKjR;Lov|W7Le}xX*pNvIfA~%zc7h12W!@u}RJ_Q$ zms`RiZn~I7vXh-T9D+wyRbtzXjH+)i*{;#a_NDIgT3G%WWkw zPGxnSK+L1$+2RuTW!)}xD0NyuL#}|ns{wMZ`buA41jIR)YC`_dpV}qOO`xdu>wQOz zNtjcJgl(O;cY}bz*@iZGCx?;pJJum1!&CT05p!doiNLj0cCqi}cFl4n-CG61`2rtw z#_ZG)0?L}20P05iWm~#uM!ewKVGYoURRZt0fnM412GCV13PL7{4C3x`vzIkw5>hgO zoAdPi60!dIX{TY-z|cZd+f=qdykBU5ADs$p@5L28e_MAfcQepVss(ECzc50=5+(^b zH2YIISfaGp2^~v+PjZ;#t+~WvUKfcf+ja7Au?JQ0aQ<2Ac>b&mT;ouDYzjg#%5QsW zGxMFIcpy){re>g26gX)kKRgN^reVS2us_QUg`hqYuJbkPhLII&xNsRjgG^@meLs;1 z*KD50GVtoL-7ve{z#tJ2Sr0_oYWlDJH;a0?$A7udYvUM?1 zC(PfPpk#s-B0aC@X`3y<2}gxVp>Vlx4I{u0Dtm{#ec?)i8()gkPaSxY!V-|Tqln>$)xfgL?K1;UbIfiliwCrO)NF%ps294Dx2x@N*Wdb*!O=g^f_Gn{bB_u zRFj{NGvgW|M8@GMlM4mW{U0R|O1oAbugh788a-XV_~h*IJ$#~W=?nA`0x~bH+dH=% z@=3fOI<{@AgiPcl<{7HxoV!A7iC~obBSJKW>vcWyJT4-id3`q0%Z$QjAR_JCaBC z#7VD#mVfMear{w!Lx$Oz=MZYWtaCM7Q+*=^658&>@9K6|&*GBilaAJ-{8dHAa{rUMPI*kQSaofNB+xm2}Xxe#sbHE2xd#1O)!f_ zH>tdXpaKD$(a&g2@p0``oP6CX$8kQ6Ks@2Zt|8sce|BnqS2|mu31wGu9z$jU6Z!y4 zw^+r%NZ0>e17Uu&>ff-dZQqL)De|scE&`>%zC{vF0Xkn^J0Re1-&11u>(xRgx8uPlBts6x58Ia=(ciGl=S$4Owk#ZXnp~i;2VhRweuz!N8&Qwc}f#dT4oWLpZO{volpG`2LD= zUc%uqDR%fnPiY9PN=I4i1e|871s$Io#_rRy7gn_IOk)1_qQSQ}j4(;pzhibtzyQ~~ z@)*~neaAu8(c#1(=$LIVy&0_BTRAZRxPe@^sormE)k4L?MvFs+4~0KjW6K=i6JLxH z5c}HW<|e5?!5V@d7xd>j2C%ZQLto=G&(4O`N^hgTG0D*quH7nE)M)aUWJgbEMR6j- zEK$xvO&=9)WF@UKsnx}~%-bWl~&YyN%HV){mFoZ1eia0(~T zrPxSQ%VBuKbvgoXl+EYq?{sGT<80S8;AB4#j723ox#^pJ;YC(}NA|iPaghCsknEbl zh^+T3c7iQM)9$b&sx(6Bj+zdIP|k-B>05oYZ5a_$CL!2`@O3;_^n0Eq*`MyJ$`P+O z=)76NqLE;{>Q|0~B1G#GpWI&SZ?Y7j+^e{m(d?V`3FD39&rkbiE<^yWJ}kTV$}kVQ zZ=ftFK~n1*3N$~8+;Dw=cIoDA) zA?_&Fu56Ev;+Lq%;9Xd_v4=o1oBOqB2`en_E)z}g3@_}x8BQ@B`e)a!k6!wtcYGNI zKl4r0Df^ShT>1E{Am1&VvsQP=&3QA-n5Zh+n$VK}yaoIiU1LvoFC2EHI-w!*Z2ALr zb|x=q@rlULR)25z`O&0D^RNtPYBebsmkrDSjsrI0S}d)WA=y|C%S*(vco(b+d@F-s z_X`g@vd^ZeOIbe8Tv5zOLb61$Us1>?EFO~X`G`fl%CNO6nw0?>SLyMMX3Ka^wZNv zrl#Iml=4;Fv4-Frm8hyJslMWP(OPNyqJ4-yYFavYM9@1pdz6_$4dK8@h9)XZb>Y}w z@ly(Witc`Zp;1kDsPsPKp@LlYi9)%y@9htK&Vu?G#p<7NKL!%a-tt$Fb2~pn`GUl` z>1N+MrcYvOUM`h^9#XIMYoxYe1%=@^p1^7fvoURs^k-MHckR0lY(KO#2Mg?MYYv9v ztK3CQNYj05@r$%@-uJ?$lumsFV_HTda|Qf$wyD~#ev_>hicxb}lD6+X39ohX+nyO6 zX)bv)Ca77u4;$$oWzS?wg3a}&i4}&{qYd_3d~eCPOS)itxJnc;uNdv9{4Kq7uwsAC zJ5h@9^U46PK`~5zfsX#iVb@zkL+@9;v#5*tz&Rl?qf03q{I+Ejz0}%GR9>wW8JSFO z^S78)TPuIb0^c)6Ovfh@zZ9ve{pvw4C&Z)mebGDz>q}xwy-fzCj}L9Dk~-u z(_E3#i2Ql0zoOHx1{YN?@xALp1+R#&p@=6u?L6K;3cgpNva*qtK!<0mr=IZxUoWu1 z3$N(+&Reo$94&!mgYM>lC{l_v1~+a)*C%-8#PC?l_L%A^OaOSrv?i?r0|CocQf6;c*{$l}JADqk&eypO~F3nL{`GI?SI{-b{r!bN)4xh>9yS)t!UZz>J+WF{e(IaV((eillydz_u zvo6T^j?x_WRo4-Si6?Y%`JxXy#xfISNSIpH-DVbycHE>zKc?eiW3}8g9IsfH&kWAJ zZGH6vr^1b;a8)<9mMq<4-P3>mwO78tACG{5i=m!5A_Annc0qk3$wOnWGX99pHBnOYgHzh4A% zdT7PRfyEXjKA7#Usz(b)mIj-V=*8OB_%A$Jg@1Z~RCp6Tqdoii(YZ!rgDpC;ldfn& z&N#>O1{}8{)jy?KlXW~uWhJ@g-A2Hz#Z^C@F%B$@uY6<{9()T2NHlw&%;JfhTNY7g zi}l4GnueskrD6zfa@kP64&Y;9>`E$JV z766vB@4@}RKI@;ku=J7p$cT7hYK5f7t&T5o%k>7xOo_5KLX9z{_R6Z|oc+_%QWOAe zsL;C1X!$z5bZqgSO8jBnVp612vzB3E&Z!{zA&DTgDpE|=DiVB_K7Mrlo}+o68=_^A zLN)(rzr5i>e#_Wa)yD=jg{^K6u&(a11V4;+tn8Uyo1476D)UkTcvZk?XgD-(3m%y! z4KH<8%wTeOmBF}gjt|)m-^@|9vV9Ls?CLG78Ao_cTdNGi#{W$g0zfBvFW8;Dli+7k z!JwuIX$KNQ%TImt&&nOO6GxZaJ$EmO^Q7;{ic6b^^fYI~N4TOJL+6esPPb)mlVXbQ z{d$S*WzPc=psG8utFujUkXIbbAyMB=jN#`C$4JON1|+d_w}e~eSi2k|zy@M!i`2HM z+~#Ne+820+xmSq{#T|>Z`+w65i=kj~}HEog;g%GR2;d1MBAt~&lSHilczQ~@Jh|07x3D-9=wN_|k1>SpetQR2{@Eh48Vu11wt(orQzw2!G)F`%g-rZdzz8zC3KNJvUgd{d0+_;*G zr6<@zM2;B@F%?lsVxt4}>gPZ3@B@f`naC+yF~+Si%`@P>pS=VJPO``?gQYqh-B@I> zqq@uqYzOR3V{#fdOYoXT>mS8l^g;N$Qb<~|vx$u}Ws&|bZp)tY`UC?%q+J_J6?Mgt z@rnai+qnM*Y`S6!$U#d}d`1278inAqpOoX*g+7F0uwo4+*aW^#5L4%9s0Agpmzss0 zg7Sd+P=f;*de}x`D>nHMdHwcLf(N>>%pOzOFj}#6=d=C`bswJ$Rz$IL^UQ6QWugjq zTYOF56sPl&jzPp-YbSMN@}w!EZSjYFIUCb&0gF7LZi~k6wojbQ>He!X_F&8k9krlu zOigeU5s}KFX%&L4cbmr~gJ$BLI(Mc3VeYloPeXi4VA8f*dnMUG-^qxB@3x&b#0a}( zDC0I*Rlddre=-ifFL>kY*A*aD8il=eD0+1VyJbNp^{1Z)$ruc8ZqCgn7uH-e zXvcZz$$(?>=R0c$EOgn!&q`)Kkkq8M{`5#^R#(!21DqSvZ@D$P9e=%S>p3}Iimb*( zXgP^_waSon%S%CholJoef>&arPiu3DIl<&m>)2unZPhUxM2mLGGd$q_N+9sU2=A_w zE&fYU3T&`}|1u@(&{l@G&uF}Fti=vtT@MK5-$+MJt~)oF8~{I&`}(l*0M`1q&fykM zh!S+=$irLwi?!Xcd{jA$L)5ItW1NuB z`5S7?xD{=ib@PVQjMz^NXB{lrg}zA^8k)!r)vukce_Gm@dlWJm$jk3Q3O?`zs-{lf9j%sziB!`!i?DKJ^_y z9qhW^q4RIf0qAessrak3ZE#s?rcw5g*-JwMh!|YZAHQKk?I-MK{%HNIXF`4T zNii?(+KE#06|GW?2n7Us_bXFtqYS+z!y#Lzu?(w;i`^{_X-OG#k6N@pXR7#D1z8P; zNEG(4H_heMWNt-dwkxZ=aU*kEo$weLAO2tx1T%@oprpp_pHlLg7vFBdDZ=T!!W*vL zXvh2cVlZ&n6qeMyLdwhW^M!3iQ<_zA@ok>ZKDI~0TT#P2b|A!8TrOcQ-=b9jZ=OcF zP8uZ7?|Ye&w(ERIrtM%4I}TCpK1t^QeT_|r9Fd90pUOA;_U=CmrL8fJP;w45zN;%< z(h60DL;xo3?4%kqkPUU@1`#$;Quu%~0#oJh-*;^<)K?&jc|u6{ zUp0AoP+`pBecivi&=E)%>-D^0z2V!?{bw2w!-g7IFo~;dYPmq(v(G1w75h5bf&6a! zdO@&50BrmD67@G{qkfUo{PkJ&jPe#F+!&W{jTyk_0e)(up;TnwQTSL1o=1y#DigH0 zn3<5JPnmFLnnMc4pK7HP@j3R9Ov{?2)cRSKdY85MK@lyaWw8DbVli#oR;U#WJ?e7} z%2^$lYwcSWqZ1g5ag;hC{FZ3yc}u2w)QZ5A35g8Ic(s}h4{AE z1k_Ek%t-h$u)s6Y>c$~GPAy$23C!AM2pW2qV80p`1fy-)abEM)2kf6sDh-#Ns0^3i zX?q3AtM#(b)uoDM4%k=lSRMzB;7^;~@)sOYrnzClwbHQ)=LS8*7im)~f(M4aXsCFQ zOu*>D zsmp=_w-~qS#{wyi36qbVK>%mNC*Q6&utx1{?cfBu>F}_%VpO)eH*qDzxbrf?d2JS) ziwRv44JUc-5}^qoVqZh*1GPo)>mT4D?dJ;+#OaSV5dt3-QGsH39@3F+mGw))jfy!l zzPh}NYB@sQb$MJ`e6{eyVuSw0(d90XagRTq4<%6^=3MiN?D%)XE$C%Jb=(`>P<{VM zLiyFy{ldoF_g$sA1SAt3NZmt@xQ-P4?Q6Pz4C|kRQ`_kbVrqiT>sysX$|SCth|zs> zlO6hTe?0QdA+W%ux2VzAdSn6m>a!YZ8RyI})?=n^sB*~d!b08>mkt~vRXBH;x7p`sEu1uuQA0tvog^6VtMl6 z#~<|2>?RLS#P%edSe2h_;)boy(>{^R+2AJ|&s}evyVM>2Cn+Pr$5a==%Ftm>2A&$U zE;?{xO4D|D$?y?1m}!L;Ern)6ACA5ag)&4Z(cqX4l|Mbk-@&KW+V5`rxoR_0V<%V}&MbXxmbBWWeL{#?klMziUy$X6x!RZ2F;YTmYld;V$k62eDwvJUf+& z*&lS5A4Jg%)S2Q*3mwzFsJ*z3W}Q2xPIaYxHg8V_ND zDK2Me9GdEA^n3)i2Oq%DE83w`ehI{!TYov7-<)AqOZW4I_d;u$&A^y-{1fFyqc}R9 zVu5JnbL_&Wy>T;D(_XZW*YC>WqeBs6?e))f9OLGs#^o;eUO%c>`n}>+D;n>T7#FNREkgP{&(<$w zPYQP4RK#G9osTjZBu~}GCBpC5qULphD5WQdWMap@l?h2|ZBtfewV?m0)JZap@eZXvw#%((p4kgu&JQbs70lYfx6x;?%0 z=?yCC!g(QbFd4Tfc?Ta<)ZCfCKq zEw((+s5_oufoK6dXh-qE`Z+GaEdlwQnfF4k59cczO0X&@v~=>>=Do5ipceq#AjO*# z>&~>EuU!kg2nqyDd|=#(w}YWoiKY()IV2g`p+d|_abDD1n5ezpiLxR>mGgP3gqog}cY}}S z!U}Ui$;UcYcR#CRM!`=lE^CNAfsJ9>$c2XE-pq0JH;1T`Hyti-sKNGf3bdMUP#xgw?i^6b73#NtMK>uu<*_A@ElzL5 zJ=ady(KKkWGZ^}O=*F=v-y>j+|HP%OD+5%K-p{xC%MuS5sj#USBY-64%ClXjjaR=+ zE$4$9@(Uu})+*nG&MNi9OXj)z#-n0pssjqW;K7=dTrJIKSKs!VbNEkgPmqR=_TUDD z?M~@c_N!EC+#I99{;)OGlY1;KtW~JX7Gg+n|2vHe1;3Mr$1~jg3F-}Q`;h1Z3eZaC zdvZJllC$4yZ_tCKi3x}lF~A!VGpbP_;7mzQqzSuWym)nKaMNJe*~uu7_nEu%JylCP zJHlcO8wRJ}m+*Nl-!0Ev+rm2(ya4i8=)iukbNcG+HN{Wnj^ee1-B10)tS!ZcO%k_u z`|6aTBN;r4vt*8iNb?r+*?WHVq2H7M45G?|HW}`2I z!N*Em+t}pMfN`kZsfe1F{qLF4=Kygnm^^dxQYYL4!Nt$T)`>AZM!RG`x*p{mhX3sh zVWhGQ4hiD1CT{dF^Nw0ZBq?1ZovOYO@0hbNtxd$9b==$x&p z!mfD^WmDjIPj7Rtk;8mgm!yJ`3SDlt`I8}&n~KPOhxsq2&nV%ln+2k}hDKxreR20+ zJ%yOv6{ zEPpMBpr2D-w+b%upCh$XRN1k^kN4qiXVs9LPYHQ9&?W#oC^@YlBCD7e>WKFut@C0y z&z!i)&yg$uOX!P&bm&$GWSx7f;9vzbFuN%upmWQVB^OR|!B^#gOy+3ZvRxou9NI}R zx8KOITy=r0=iMNpxwK(tz=#D7_Gp9s`D3_GSt!2W)<;NjPI^>)HPGQLUh~jj{XUk0 z&F}iHx*L~it%n~nV9)Z&=Z!6u!q56qafpEybn5+!oJ~O8GFd;bPqiA(XpFf}zPCag z`T_y^YlzGz)6neb`9*=9$lrGr=alOPgs-j0{Nj!G-pB!wnodN@g+K%MWolUqGRkkI zQr=x;7vMY(X%C(!$9n`U;G@KBkL1SlTd=>5tMPlRdX0BfEMN; zSK*O<=|2Gbn_*FKEN%8(*V&qMYeB@bWxxu_=G~5kj*}K^ zEP$mnl6wb|RspCS%+UgV*XY_Ut+(&t<+LE)U=t$j+ODhg;xxKF;GypjGq@SY=h({| zKbRWF$9?h&v-G^LdB}%qsnjqb;(NRrEg_#7w9}IIK-!rGun9aM_C-IDlxuy5WxCl+ zKQ&&pt)(UuXU_{)ONYN^M+e~@)G!5!8qhLT*S>uXSe`;|P5Hmx9TRwV&oDS`KRH%- zj#Yn!{%GnH#MIf8)<>2;rNN0M=|6%{19)UP96zFeWOr;kDKt=Tbvt)?`5|a7b8T}p zT~3yBOdU%@&%B7M*%tC9O;{s4+Vg`qH5xhwriCBXb%OL!GUipo5nNx`%cT+oA!agn zK0rHI@nUVk;HxAtpLU9})aTKY`t3yf}!$X`r2)>+w=91U+xcrRJW} zeQoP`K+kfUAH~=4ff+t`*L|c4NhXX?b|JgE;ON%mq zgSLTve_y~_X6p!pQ>|->os`7DHVav29W-p;b9s@u4UIUrT)bB@WQ3tOoF%b=5;y~e zuVClb&aYJdy2rjh%@e)EWrO}@xh2y-?ggc!qE*;cD=HR`%RvYTZZ$Xm4}0$c)MWNO z3`bdIEr_lPA_}XhNEZ>1mZ+!*2q*}NbZMc3^w5G0ToF(Z5JFP~q=w!hB2q&L(n3jy zw1f^OKnMZyJz@8^>-u|V-kEpi{pOqb{&xn@RA-=wr7lM5?n%M z!QD?h%NstpuD^RBmgYaU!_<6)HbQqV4AHOD77aUhOzqPS(fEqSPSFYexnQTAj5Ka{ z#0`l+l~2cmOkkfhZ+|E&32}p-?bq|Z+EC+iq3d0eV)5yGV=L+yx4TE4)Xj>r5Uma} z&mo=|n zy{Bwk_G!Ip{_awwoYAiPu|c>aA?@$>?A|>%+TttpiR3$M^-&9R8Ym-oH)A_eV15K+ zCi;7AM&K152cSBloy~X!obOaAQ1vFG?L8>Z>>ChjXb}bVGVKf2DS~TVrZ(0S=j4y0!IiR96n!YYdW9zd73zMTu8e1{B2uK zVnLT$yWRoo(HOcc{p&PG@#uKz8OS*u+0mmiB}D&Ozxq?&hytBs7x&IK`5 z>Nw@~vN-bUU$wraw)|P4Kv#uQ)4Xeu=b$pHx8tu4sLg2C9lIN$1U|Z2lb*H%bYAv* zm2=)QkSl-MnrCf8fD`&y_qKgx7k`%ROw)76VW4`pHHG0eis~V^o1IBuPI&>99P2s8 z)6*BVc;>RVY0{?lnOd9b8{xn{J? zcK&^Xz>pPpD2X-G4pjBy0c!9?Yi?J^+a7BBmsje9)$C|AP%k*{!7?Q?us^;5M-W&N88t6X?_Tmp3KHbH}{D$mrxEM|rCpr~qDcy0+OY&;eNM zPsXviP7U9*q`R4X_Z=nE9@tsC-Q0ODNM%<(Uqu79rm;v-4-=(qs zS=aS@zMax7Cmy)j0xpgQi#Z^*-QO*5QQ{VI^Owx!L`h$H<9t;4=bT3pua2#9ZQlhz zxXkn0f32R=*s~dcx&abjg!ZOI?wS1`6Gah^K)?)1gJWdzFoED$7P0HdOnQV zDUdO5{`UB{cfP*>8D;x{qBF47W$rcajZ^y>$HjdWkMG^LYh*_5Fg)|awobz-XLIlD zUzQ3Mr_kTx>Zanw)5wQ07tA(}$;ruAm(Tw4wBiWw??8S{W=*>wr~79td3>hRr>`p$v zMz*(2{lndyRRRUuCe%3t-JE;0?>I9ZF%&ItPH)CeM&G09y_6 z=+-j`yZR}RS855M4H#0tYhSSWQ_hPuTOjk{ap8tQT|<6Od?fZj!KZiOVJi!b$E<7B ziy*3pYHkKUYEfPWYHs~!lYjnQi2Cd$NtEB`aMlF~yIO$!rnQ+4u*OIJdFsM*$Cgf{;yVHTy*V5M{P2a{nI=jiA?oy&aJHpUu36^FxDks*7?x0`|TlJBXaJElszxd6m}MB1qVt8?O>+^{PQ zt`K`eM>QbsI(fmic-OA0J>E*?xjwK^vzdqIV$|=OJi~NA9A)U6qcrY%C4zfbhLdvIo2a`90(f{21JJD zPvkOwpcKUma@L2@`IeC{T)3h3uwHFo{Z{jGx$cPOT^lM(7Iy|~TEjT$rG3Z#O5MG$ z4?Dl)7*(`(c0=^U35FbyScQ&F#x0E0g`ajg1YoKMG~yUQCn`6c8>_OBmgZfil`2(j z+k|2 zTKF1&eZvQhb7|=%HGyyM7gm(F0F?or2fK0=!_69i;tq$#l&*tc{u@DBoe};`-#Yej zeLLfO5KwiGj>-N4&_Q#AV#(69#)+JnF(`QogbkixiT2_v0Uw*+oLN+7CyP_2eUYjI(6I~EY zK^Y1JOiK@Rk4X-11c9pGd^-mM<;L>5RUxI7rXaKpvBUH=%_z!yuHd(g6-tt`3}NFF zLsB`pIVDuPfQb$@OhOHNva@5YqJlP@j>dggIf1%b_T)w54$yW>gCE;S3G~A9+E(cJ zniaY{bO)Evk<^A5IK@pv_^o$PGb`>O+0vlO(}@yWt4OaNAZgc_UziP3Jv=RYJ1qG@P2QMBI+4N4qU9T=I^X zCqqwEkq%XDjbj|3Ps;k@;+^XTsBywWf#{d)4$@!aj~Y}l6HJEs)>mfY2g&|fW<1rO z*C8k31nBVc+>_NSr>Z7n6w7mWbjh9c!@aAbHU$j#h4K*3qqkkvY+~0=FHvAjWsyovy;GHY3fB)4pPoHT`~_CG z$F*2|kL%97kzW+=qjXRVUGo&Chj}v7F|#vB#EqbX?O@7XtO?eo$m6uFJ*~CGGOBZw z%jCpX3{HwpNqM~xS)k`E19CF6R~zL>E-F3|F=$0z^~oeuqPXWjv_ zoYyy#{1a>c^SjXM7sbC2`LB<}ue%@q&$ruu)2{tb7XT3d0~Zc9{GTI(r6E8^RaHt6 z5jqa`_P2x7t!HKhFJkIX(P#Nf1&^b;2YQn-DRFOJ#9Qphh@)ImxRm)WJ7b{C*u-Fw zf4)ozzKx9Azd}-O12~!{7@LUd>FRogpi|%rwYzrUZj5$3+kvA1ukq{5%q+eG4^p3* zp=7z52T^ctw}PxajpJn7?mOQ1L(yjq6fv?qIyeXEjMG=U3>|dpEJ_JqAL_Q<-Tlur z{?Y(Vg)DS+Cxx!BcjUU=wl=_C43iGm%Fs79Hty~nc#ELV%Awzh;ZB-D5m!oGw>}S> zeifX*+TY8%5fCZ&&q#&J02=^J>1SM0RFMAFSs`A$=D1Dvo4Cx(4vWg2a05?+<1_rB zB%_PJ55<(`y<>#0p7>$AmO2180d?7GIpkR!%$e&389OxWOVfqBw?EHn_Zw%{(;0Ce-gW0X7ZhnZm>(Oj>cs0}M!%v>mu!4N~Z*l}lfC@oHuiN(l0Ao4K3GxV^whhK1s=k-QsW@cWx zbSbmGUY0&9$mhCmXa(4#y|b<-wp}V2=IE+xZ4DqCs=G9r(C4k?u&`8Q4_kwAaO|v9 zQ+U2UsREx7N29in;bWR9TExBoEb7yY1_v-I@_5`6h|(!m(ninAprR*f6s;U2^AA(x zw1d*Rlj1~~6+9uXIkdMAhbN9HpdVbFD#%vS!QmWSUGG;)*(=J+XA~95(y0UvLe5aU z-A`f*`}!0!-xUG9r0&NNDOw~ng2%z$JXG-h$BZg5o)w;K;H;P@!F*Etgu^)$VPpP% zHO>=7=t|Nz?uJ=lU}Tybwh~XDkwXVx{pL~B;v`T}&78!?Hl>OJkK2yN&mWVVeQ3Te z{{xTDQ|6o>$<9?sl`W^V(%d$W1&U$N-$qddsey|8L1<@{`Lz?WcK2=;7z_sohGkBy z$1t!9Eh%yPkc{i+aMG(#^Xz+C+1SHgBwvgcPA5CnA{DW+&nSFuquzU{=$f%HU#L+0 z*#=`VfKU?of03^FZWN!!`VmR4K(`6vT#xIfk1h~O;PNc-l+uz3lsfv5K%tVE9+B); z^Fd!s@w{~^R*777S#d;GftT!mqf^Q5oU~4WyrgFhINR!Jd04#g4^$N5dYVH;Qs*cHDEMx_P&5~wy}0r18?Fbw=Rt>2P^fdP zh;G$o(M99A2N|W8vg+$k@#K0NDu(FaJE0wtdr9H|aQ zk3?2Z&g^51nO`5siyJ)-0u{Eu(aNye1F}r;6W#&Tyh`K&u6cfdJV5=@8GP?_rcMV1 zNP*k#k?-Y!x(vLR6G`au+3qAWUtHdTV}N&HB8sU)4?hd?{}^dr;^gEQeir8Bz0A5p zHnou4(WQ=aGv1J|(Gx{tuVo;B3H)cLK#hI)cWY`{Z(XN`VwQ+INMNBcw;MAX(g&@CivG@yAhl3S?5ASTOZD5*bXX#eqgL-5k;Cv>I=B*`UA5*yZR;DOuIGTFk04Q#aT zLd+$Z@of+J_hG%~;+|HT) zdG(Ws$I1ZOHtciZS^t&L6O1-2eeN?QEy^s3r6*{3tan6f-m}4p5F{IBvHJOQ|5nvR zLrF2*3Eucd)o;A4%5F{s;X_kNASD%3+hm#TMFF-p>>JRsx3P$=)kQ*-ly(K4-=)9<<>bp89|J)P^BEncHaAIuj<-0)~hC#!gR9mi6Y$^;|yoT zJiYRvG~c5A2IQvMP)Nd$#3O5a zl#vymrbg3g1Q}K0cJIbX727eB!^3@m;2c1oh(8Cc6F|P*60SQzUSacq5PlYaGL~S^7{32QhTr1jathp z9d@l#%@PW*TxAZ)(0e<4;?2|S;!iwW3n^bsdRR4^P1~W(m>V$Rd!`UG%KS2_{i}Z$2kD{+|olrs*2a>EJAYr}3ZZuC3|^eM4{ zkPhggjjwpAZ#Ucb$xIZZQxaw^ocBo=T#Pb(6QsN4&@4@_zhu+BsEfTDUk>5=b}@l& z-?=_>1F@8BrQv%%BRg9Or>*JIkMUF2T^oSX$HU@WMB$KN9o@Bf;feYKk6)=o3B+6t zH}>=-RIb9mS`_nN+)5F1!5w13F~0GPitGun4DQ2h{add30Jlu3y-q&w5grAQO*y66 zQ=_;PyM~z(GwYI5@=f|PC@m({B%J#ZXzKpVaV>E! zV&K9&@(2e!c%9j`6@U5x^Wg_BWlUw%QSAi6C7Bqz+HJ(`C`e6)+DQo!=cX26Q+rRU z5k}t9?J{vD<%nw_qhx{|CM97d9Cas|@9`}bmUSTbDg*Lt;1X#M!Jicv;!T#1l!Illd zYJv17W^;jhwAKjvvA(BgqDfrzP#`ljaN#MI-ur(1F6y-~(?hzNHbjI3kIs{VOk6jn z3A8_?oQ97m;_c`b3+aI59cdc=&K@o3J`bK!M@^{bqKg6(} zJMV*xo^_h%s;sM?0D)j~zjF9MJP&VYl(Y(XxEw2A7sD;1BE@~e@0xTi{aJ^3_B&InN#L!Sh(s+mv??2xwXB%^93 z$}`3V5-*4hxEd*}b|#1q%on6^B6eJF2!c8yW3VS<7u&(Xq3t(M6b>L6H}e#@Nz_%G zkE{1CdSo(G&0D>j*ohrj>)x zJ#FNoI}R#7wEFdTx+y5o@`>a+Bj44b10Yb|R`mi33Ia*mbp?Rqo!l-Yp|&yZzXY<>I3`com$~(D;Kp$3UBs0 zc-lqYJwA~2a#QYO-lfqLjf?qtxSYP4Oz|?a($Dt|vfpf#g|ib7j4z0$-#}`&;po1g zYWECEUs-@kHX1v{9}>Qeo@s(s zU|3B79J%Lh=a%&R+#>8_shJ|F40xPKYV$lCl9FCRm@&HloAY;Ve1CO$%{ZahvCLMX0e*1om zq1zP~!c%vxjEOZfK0N`5N}258bBaf>pyh)yn4UMR)Km+gbR%O~d;B-@_58}MvcBwv zL}VmPeXfof_5wgV;gq_3*v>R==B8Z55pB)r&6^7bk`2cBP;d0yaKmDgoSRxqOO_<| z4>AaL)I3pE37N8CS(J3Q(YK@Uk>F8m*V+Er8rQI^qZ$^QPAMGEmOzNmn48;%6kE~z zyFvP5T~k;$hB4+j$GP0D0{W7r;YWd2CnM{A_a(2FBz=F4>&Y4*Ur-_-S(S6F=~PNR zG{9)UVd8oI5PuxsHB%eE14K8LRZ+m3;)!n*5nsysjjL_>wCVJCYly~tVi3Q4=0rA? z>_AA~-=+a6hnkcyM~Ex85bUvYL`CTm6G0JhTvS~CtEyY^3!W`LmnqMkP4)}=+UL#{ zdfqQ#GHXju?69aFNvp?t{SIvnD_<~?JgZrN@cMd~FGMJ_gfP`OLiWzJ;+YdEYm4Uj zc=FYgksts7nsVmt$?VEp*zUBj$^|!!R9y5KZr7F9z^(7k?M(}KIB!5W3S4;y30w1u z90LB9&&OsRt=88_O*qr$ovf;{1Gb{f>X`U_Q3MdIBhCRTNTGOsNf7v9XCGoMGLkE4 zMP?+rCrU*D9$`{$Y34g%L(f204!ldeJNO{M=;_>EZ7r8`c(7VgzHA^N(DFKBQGZ!@ zV4<=OZ2R514<5w}Cl@CX2fjze`Bo4%>|Lw87v59K!-U^N3Y8a(P@8!0ox?n0d@5yQ z!2TP*)tfQ0zIE+5X~Br)?QF|qN;O>sseOv&EvsXkXB4ai9MwKrqKhd|`2*w>DN>>G z9-tai=2{^SS6m5siwO^3}&rK-p{gl(!MeLe(@`9tMIcV~3+^u&fsH=`|_vOW!m9fBa z^~PcD%P4Z&z@iZ9aIj)k?NC{w;6+xl@5Slw5NbVS3vdg_d}(P*2dx<0S2Fj z3`$B7-)fbQ0+gk**A)e6-4n%=zh{}ojIR=bD~ax7*Boay?Bu2^;Uv|IM4$LRTD+>p zm}t;_{Bnhtm{1hHUXIs*sMcAqv0_i;U57$*-=mC_LQu=E*X zeVX%J`wEyZ$ZgYPj4FZX4_{!h-qfzzwdBf5#a!j;u7J%-Cub?d+{O;u3bYm!Q%`Bw z+2%mH^O%} z)-G!}rA#3C1K=R=;nB%3y-+@WXHyj1g@)yPTv=zqqBCZPbKSs@bc@nhlTAEUcZ~!kzg#zRrsRAEirYU@W$Puq%%qn zm-jUN<`s^gANbCi1KbkN5=a+nI46OqaW$3>{5Z}y1eW8-m8ovGb^yX^P`T>q_>I+> zq*@omN-~ia<3w}Lkkj)#_isf6H+TLH%W^&Pg4}welD(w~y`Ckk z6NErU_iifrzI1aET6C(P;O5alt1Nah->a8U}_sF2GI{vKl=dA!~_lDP2PN&pb@8aYDV^1U(? z^-HSxE0K1nQs7GB242K=K31OkHOYI9qjHQmKo?3nm*$qj@P|Eb3*FJmCvY zF8ftaYs?MRV+_p7@p*0i!UNt7xHC$@ge?wlB}8?!u~n;o%Vq4mbI4asO@^Obvc|tF zdO}cGT4ne;|3F$eo5u=ATR3GhIgyOHCn>6_%NkdtcfV`{2!}K`_nq8njfR*5pVt@7 zW;gwI$9;W)&0$YlEcTWt0lb0&PR7-T3R!5^6RIkO=|*qN7Ffu{rI{qxvlKb^C{njl z73sH7zTIz=>;{ygXBWeC`(S1y#36$XVdx z-Pn%61cTmRq?w;}MftE@`QXj1ar!$PLp(0#j79yX1mfZP2M)|=bHjM=b?{iD$wI32 z2>tsurf~tTG0?KRdW9f~Z&mx^bVjyqe(cgrGBXuYCLboWSwdXHJt>)gmGR3ZRQR*L zPrsFLHdm6tm19=t2>5H|nY4f!g>#_%80VWa^sm_S)u2CLz zze7k-4UxwKV$dWv3yB8cLZpw?eMW(DTSd^|L_CpJWu>H9ywc@M4Pd31NOC1#cEdy> zEhq{dILyYDCjvgQY{d^U%d6^5(j$9ZGe>Ru7fEP(j0BUV;w)pQ=Bm~x#0uIpIrSA& z2Pi4rMY8L|w`U=d*X;W7l0IiOg9|NMQhm7q`%6l}W}Gw#B!PaQYZoie&2#)-zwyHr zylOI>zbuscoNE|8H<$oVO@K+R_!$}+YInH0R?L4a?ysw&zAmPCNF3c~8*0OrwT+^$ z1$bDH9t&I4P&*~tT^8Kk4;=rhkS7vLg7+IW5~hc~-M01=!>rGB0b8Rj&qgo@@Z^pT z1h8w;v^4}N!GMsz$CpVf+0~W4iCZ?eo8+;`@-6xWJCQ4_suWhs`UX7bF*%OuU<>-* z1Q3zm-)bgCX~sA8mb#}pXZK%s@MmTYi4Pa%`3%^HC1KbI4%_Dllp0$L;|YcqTVk>x zv-wss;MUz1Mb9uPLRq${QmfK&4`K&`dPp9tVV#n3G0#LzlxV=X2HGv654qB4fD09S zmmmyCv#NLJw4wvu$etxV&FKKLf-HH?axB1&IxH$Z!K{D8X3gI=|0rYC#Gu`n>!RO% zKB!~xsqsY#3~}b2xb^|p11Ia8%B7s5Hc80|M*Zes?dlAhDGhL35P#WLQh<=-E!wp$ z_t|~nIV_z}W?wYzjmRngP7MXvAX4C>?g4M{BSPjy)hU zUN?ldh7|Bl&tc3`Uk(;naCqFT_R8Sw`)mP5M=d~eM`?uknG$MLCMD{yN^m~O)o*bF zTW}Ji3+)aW%5lC>ILjtIz?A_a8FTUw=6oDKlgT!J9ksRkQTNGpP+o1W5Jp*eYvmE5 zreM5B<7C-p3#~2)FBSL5)@yQ<9Xqc$P|pGP`iw;h9=(cR(_|d20eyUhsaH59yxtcq zz`zH*Fw=}a^hB}EP`K_rEL3PTC(8Ww>si;klA`WdkDtjSU6mj2w%v};0s(|Uuf znN3<3a3D3&XNLEal%Yaf7Qo4a3PZQuB?shETBG#9k~PS6?kdXbwgl3h6NQ!QPO2?2)`g-CMy_>VcDA{`hvzNu_Y9Gtd=CVfnc6n(tE(AwKVSV%lPvBlKjgJ-YyG)w zT)prhCZE|QQswkn#G%gQ$pOga0BM7icPfmkT*H~mC8rhULu2uYCeg@~au+$vOZ03VKi(C@6JuAvvzM!}k zjS=rF1()Y&R*2UsDHu5wiy|l2BtOrheIcZjV^dl1z(5*jY6L|>=^OGuCxh)3%z1WsTq788maN}NLChuqxcxy-C)G4y z0$su}Ro0|w4ES{|nFjQtjI0tE|PF*JOg;% z<`!nfWBx9tbA1zw83hdGc|4qmiR!zX;S5SkR_d#uRh0{@!1dB3?`a+Zl`VV~+PP+{ zJ=|lR%8Ok{-OzPKRLy->U&Ws#4?&)^YfY91umP_9#&iX@II#{VYX*5nhgKylw@y|C zUmJXt%)pDbQ|D9aR9ljldWf{SHys5guv`?1kWwcH*&6z)^sNTjS(=zni->8z7DlqH zT7+uk*=Josv4y2t4c z%*mr)2Ta@bBJV4j{u=5kb@9B< zj>4p7y=ocO7US&(rUrlBN2)uw8XC-Q6D)4AtKkO@p84&Z3YB7ApobaRd|?)zguve) zoSg}hTWE@e#sRaFR-%dq=X2^jI@cDD)bI%q;qIK-<}S}|0(b$kl={nWcK?6vH#>+5 zn6H+hEXCf>_h>Zp=+u*LJ*2JthZ`xKxh0D4kV-}>Lb%N@gCPUy`D(0p!V@3m7I3As ztI5Zs)i8*M(gOZEKFZ@G&&q(4C7Zt<*EkY-PBU{N@o*$Iid|I5e7%o0pj30DS@Oxz zN{jh>hFQKYi_5y6GlGfTwO>p>6IIt5@e{o!@$S~i*@IyC#wwioWywmTC1t(sfzoJm z)qMEkmWONI`uO07AaAj1*VJK%8v}oqHzihWpS5z&Rq(F(VG_0x14$9!Ip`=@(Wz z$BO(Y6&GJ%cgqJ%si|72(B9*6PLpj$TiF+hFPCqM?Bp35&)cA=u;5eO(gt#8xPz%L zOg+{pO(_KxI~lnmhVmS4Y&_t0Wp#t%U9}RRVKd=zqTpRXpc0fkga8$bsGOh`HSJ7P zIZ@BHD$AIy4_eSas@$3PRw{JWJd_;OZtpBTS=-1>t>l$fdWBdlp?mfs>tdzw!BXT} zX7a|gFg;uy1e^M*xx!q{?|#A6(4J5UV>#Y z5K3s?uy3*zR0R__1|6JiSEIelWniO(nc0SycPPuEADS<$wInmU=PlYo`CX!A$Xk6G zp$ZT{4e(@Oh+?IC%q+7aG^itZVsf2=1ueUGA?RUXWc}9LT2|KZWJf?}`x6<+y%Re> zU5lz;CnN>18MJ!r&B?CWNMKo>Ee{s)L8$_wrEQLIG>*}+hJT6WRv5_!PQpy)C2ka> zba9wXJ-}QIqH?^P`kZli>o(jws~JhotP%#r8nd~kua2s5sdie}Tqa!u0w4@77tH8okZ9x9Gt7nkp$=zdE<;F6>X(ta_}FN)WW%y3Hq zV+CzcO0ont!LVxU5eBY5>&<&2`sC{6bE2us-{)!{eBY@#bB@|zQqX3IL7t8o2^1|s z#e7hB66zMK240n6ZXBwyv0*>67GA%~D5xBCys-csG9u;y?(FO;2LU6J>BrGzRo1t< zs?l;4xlyLrd}L1=PZ)d?SAh=}R=&&~fzZ2oz%=s9#A(0_q8BU+t*b7)Ujmh`o~;?I zSb1oE=;#R5)$CzO-^CMf30I#8IN81xGBTH9sA!@FDO1WKU#f#G^oD@@1`+h3TKM?L zpvUIOpuM}!?$zv>@-DGYIsM)G?Zi<1kt9-*rkkSe*qi3te%W9-ic|oIt`b%dldRF7 znPGc>GaWp2lf~GJBpRr=r$5-?=y_f;ol}< zFiNsfItW!c3jX|o`Chp1hL<#C89r?`Z*woNUQ28eTY;2^g14|%&@3RxGKtr@w&y+L z5Fj=D&gI1YVQBr}C*${Lr4q~_q5xNrkiq$hA(2j2Ta1+!AXd}op?y|;EnTx}IV}}1 z!~<`{zO@}b<#mK;bM*W^*K?d$ugv%!ZqJ)$J47c+e@pP6pKHy{s-u zz+{-UG;C%@?Fs}pC=@@HCF-N$llVreVmuQ^$f&|L@ zfF#ZL1_msaY)f7Hi~{J1l*s@_>fchAzF`i{Eno{17?4_$-w(vXl!|az?u}m*+4o3yu+Q>pQw?1*sbtUG;ge%P<3qo_Ujqf~E&805MNyvkk~j>~b~5s>)=!AiF)> zO@8umjooCQK(3uIsQ(BT>mHTZ*xweQP@p+3?yos-VUoOAz97tYE)iFy&7?C2Eo886 zsrB{1mOT7{=n9W+?)tarMSrcLeXtvuod}V5@OsGx!5V7Uydlu!3W`%Fn+MT3#ySHZ zrq<1Znj1xe{g-9%fwo^+&#RlOtrS7n2Y2Svl9nIgsc+2c6LogGn9c!_cANmZDe=cB zY*ux(G7vR6w(o(@L1M?}B|CL%gGA$@*nrHL#O3K86?v)LuVCL(z%j|YxOxf&KQu2Q z=H~#$joZy?auCcKJqegY+np%Q<3IV@%LjF&bb$It1-KPG^4o_pBL;K3 zE%&UBjHxr%Ry8KqPDYk}KIfDHY|E71>s|F=OsC4Ndy@kNOZIElaT#j1?wEB^4_UoDK%;E2_E>c!yhASO09 zYT`Q~wbipHMdrW`m{UH%msIPP&ajQw@#!>2Ehcb{$e*jrNe`iSsXJ$FXeJK0Ro9P}rZSOpPG2#@1Aq z+Zz*>*4dOrH{ILnp7^(-J7In6t1~sk!ue+B705XDif)esZYNc6j zd8T;AnFw6QrHV67lQKICjdF6da-4FybH%d@q%+=SUw#jtBGf1cU}}*5gd+iIa0h3H zjt|o^lMz*JHUK3gkYTCOihKYw1-y;>r$`XN{R6+F_K%zqKLxG^iZwDbOb)JFx4VToB>=YfD~I{g#|~PGox=wzq4EA|Jybmgcqc6P z9@H=kxfm?a?RHB+sPv(N3A|_QAP5v<%ki~wf@U3^wLG=Oa8goY`5g>oZjQcL-S7Kh zLf@Pm9JQVGc?Sob{yS{s>><)@OJ1i$ig#G zJL8S9@~__w&l=wMOYYnE`B_~~ZnVC3Vxn<3u{%00?itIl);UMt(5&zKe7x2^qh`~M zYvEr0gC2&O03r4AEl~H8(Xn?OH4O~t??1U{og~-wfBW`$xIoL>TgTtqBT0*cI*y$* z${hd%0QNu~`Zo@7Ou2tH%Y06xCFn{Ppxc9EGUfTOpu)Eigz4C1g;~Hp`k!uj-Z|lb z=nS%78Hr%oG+vblAXZvp&YpQWbkF|p6olV7!KxrhoGxkwSG~Ex{Pjx9?p`DKa%PZ$ znc>{uVXZI~CPfP9sNI`eEiOEe=eB$Gb?c6_I&fVM?GcWZiOi1@_`9<)^X5@b-#&D? zvM{mMV)yEI_!-c?vGax(<}Q+Rv#a(1V*$ujO+usopb^E;`pzIZLBB&{`v7Ou75T*sA3@%7Y0k=jBevrm_@0T&&jy+{^yB1 zJGKGjmCdue--ZAjVN2ZRMJwPobOjl`9(@vE8!xqP7v;NxUGlC4|Gl_-T82Ogyz6R0e%~&Wy;YRVWcd>$r7v01kCmP<2;Yeczy8vbjK!>*6vxU)<;K zQU01c?_vq1!e%BfvsS4_sb@AZoCLb}NqN(;c=S`E^QB5?|14nM_&er4?elg9&eS0w z5HCTYLTRzXrSm01DnSXYBsITfwafB;UQdPc>?*qL)ed<9<+O8~{wDYTrl2$x4$A>` zS>sa3(Mmj02__cpwasz8in03Sds+sb-9&vYZO!7tFx0(_9P#XIEq}t9vbvn^EU5Q7 z!6*yqVhE@OoxqL`_56*zu(JQk3HneC`wxKs{M#>?q2Jc8gWAThf4<6l)ZX~iOL+UF z|M?WAE-99G-&CNxU;e)y*rD~T&`$P_{cK7;CkKx7`llSYe?|dyam6bC>MT7^jU2ic z`t!t@%KPlf3$_OC)}^OrpZIwJJl;n>OYT-MK>}0g0^XCFh=RL6XG^J>$d=wG0?8i$ zbJRC5h#49hTAt~Xke8Pa-(F$F^*F@|nB|7b-(vj3Y95msMKojo$jP2k4Vy zT3{cpUrN(|WybJWTMkDU`LX5xVdYl>q+wisYRE~?`LX$bq@~%-vayP}E~~2B#P`p) zg~!i_D(06D%57umKc9bcd@o;{aHg;C^kcQx>3?(B?GCxG`;RK_QVck!!MQ;GY5r;b zeexzx5+`$A;x=4(Vp(^?oqtczMuB~e{01bU*%L+SL-7l&?efl zb91p%U(>PJA7KZ&q9`8A_k|54Y5kd}#pi;<;RG(bTYf)H8UuE8TU%SZsJ7LUo%Cjy z?dJw~wxZ@zT}I@KFWbBZ5^LwGdm@`WqE?rlwuEQ+{KVzbxM%)oz;WQUaLm`Ds8;`g zK#3dJcdjLTQfPK`PZV_wTOb+7pDoSzuO)$CvPSR9xnEimH;gx3_?rzZc?fS#tZ?shb(ykJl_XNk{jzw6(GC z^MO1*DCde#+>c#&-;|T_~~I_@$V%M>s1~ z?9W@ueFs2jg`K=f{L0Q&{RucQ9q%I}&YM-~uk7Fu1ErtGJ}$BQg33O)w8M%28rCVq z>Y%5COZCm48C6~|fQx)i!_~;2g&29@<}Unr>_VwGj-%ZF*qqX67fx-b<^A|+zk8G6 zDHpSfpAZjp?`K@P^6V$9|DP_~aQt7u<<`H|Ae2jmjLhHa&GtiuM}E5PfS3qfC=m#O zTz)@6AgZ5L^0+-T|K*5gou)h{kxL1uy4A+c4Sut45anEsD-p=HLN+v}` z4#*L!7;+xZlXDO0`nM4L={K+A#3XQlYqU-JyVO({uW=BFw4;~?AXyGqrNDfr%f z6yB6&)J6}G63J=exm&Rt^9@B+3&j#1_h)_j@~aqa(Cz{K6GTd1d={VfCy!kGngyoT zQhMo4Je|2`<=Ga3i5D)z?K;3->9_{j9tEUEN<2(VT3UbnN51WlIX<}wTXLHMQBdT^ zloiV6o`RF1&|_E6`Xx!AgNYTz^HaO7e%dw6?>R5nbT{`Fsj+HxC|*DH3=f(*B;kt9 zZIX3L_@jdDD+Kckw9~1)1cm6I*e3T+FYJ?O?gbkQn8_H6>ffp6-cGKGOEwEGB%u>1 ztaR1ysT-fOl~#(mFJ`|w8nDv5IN3|abl3kXVddailcn3aRAP>!x+TPkX68SF`{tpN z!-dV?zH)VLGxY7~_%L?t&&?P)IqAB`#L8_ZZ+zoNQ|TB#5no*ypYmyy!c7Ca3IDp` zGN?+-NIMnz-5<;jub^Eoayx1~E<`mfI5plpV)0Qe8P!}c1*9}O8Vjv8?NVaZ?xWGe zGLNbZFAKl%c5EL)?Z@x7#g%>iwz4~}OrdjqHeMduUy7NaQq5kK;8})3u+I;+Hw-Je zWY?X~KdBA3h^aA8jBU2^t%_@|RUY+K+O*KaR#nu$ZSL=)oHQE#fa;CE=xuCJa9l+t z9q)e@@M?-66Uv@uSW4ImP;Wzx*)#iPQV*D-&oyEb|J-NZ)-Q^h(YKTD#Fq#=wHikC zQ4mG>ZuL^Wm^&78QY9w!Ol*(G=X#b+$HT12|A)Qz3~RD! z;zv;*MMdDX00IK?h#*}EMY@WJNR!?{L^?@;NC{O%L8MBDP(*r{PJj><>4X+~=q*49 zB_UuE%85SpeXsxdc0QczTqj>{!fx&@GdnvoJHOd_$IW=@xvY&Z?e+MTd|{83bf|^N_fG4wnb^_|v;tQY$UhV3rZl zf>3*`vgL-kL(e8&WeiJ(@k_BpWUR(dGqGnXv{Y({$v@91ZIV|BG0(?N3TU>uFjt;Y z>QnlHSp@goD2obY?#vEhJv!)~hBq<9!|)&x7;d|E*8taCShG)Yo?NiV*m;W~lu_ z_@U><8T(ca7=#j&zf|mOU;ExXQx2V2zbV7H@(Jt~r=X0m_2zVwq@Cu_K2No)lltkT z9ou)wsf|vqod*lC#!<8s7b}kI={kKYlS3&*{fKxNPiEaye>Y=(FPS=3(Nq;zo%VX` zqj_gIxkxTm54pYDfo~UWJre0@D5{3wg@LFwKo3tm84qsCtDLrz$*C+WQ6&+Hy$1IC z%avxSjpKoWx0<57>!-&DHXAns%EgGuo<^FI$Kg=8%i!<#*0G2gsN|G_z3InH%Hh1#*1n}~JV$ZiV3B1> zjdjQRnfCMq6(iiNuQGGOY*ya%(fdMJQaE$15-6s5Ac%(*eY;>|CoP`BWZf7S%^g7{ ze)&EID5*}AvOy};D@{x~b@meMB!TAGWKDe)D#?4_cIr*Cq^mQt5k%F&>%}yR5$eBS zGZ{TwQ|Hk`xaOpgqzMi9MGMgpWH4sZ%QeuKSZhIy$5^PsT0MvBTe2#)lth3YsOa6k zP#$GDn^}ZS<*7#XtddvpRNF&e^uXpQ@1(#LyT#iZX7w)s71Yiig zsY$MGox@##o|Tiy%X>+-Og`+WEI|EqAc8!f3U$yR&ivv9x)%|@EF(=Ooj;rgMckjC zcGdq>_nMoqY1@9q!wl%{M~Jsmvd>S^-K+CVlS!< z+meX3aHK17^&CuvE!AlE2%N)W#p`u?KJ3dlxX{PD2ZoKTOjcaz`M~)iDG}J0OF!GG zCo8oSt}~<+7)C<<0EF4sE1P}R#Wp9RXU)gqt#b)F+(SnvyW%>e`%oyPpFoGwVDopn zC2x9AY`D5efR=Ue4NNc%yKFY@vk`8wG?~$(E-vM0EOyiunSNy1VWQ}UZh@~SFhbnb z9b-h1QCw#VUJza$EGN}_E=_Bvnm0GjjK~qb&_YgmLx%QPBJ)zD0c` zKkFaj!RC?BD{x`LyzZe;1;54&46@nydxw$_sP2$5%|M~&O0)Je*43$3izc8UOg&955rG=kWbNpK)x6Y{ z=i34moi<4F%rMg3pvAkp4LTP!dd~tIsS+RzKvE_Snn?`2oWU}(vqx=iv)D>n8!fnQm8gdy^&4ePU(bV)vr9wSeZZ7Z-2Kc{LAy<6F8RpFo@?4)S#L=9!<**A{p$W8cpeQh5ty5 zcA&09u>Cfuq_G#=lUi})Ko`j>-O9!V{%HwOb)~wcbDb=NZi|Uy%+?D<-o}AplND@i zu)nBEO3joa}q1W}Vh(g62lLuIIj zKsJgGx>$K|kZ{U#NnGKqMjbJLo1Iz`ur~nqx=L%)>~e5?ymF97re$}9R|r0+ZY`9du1g{h<=J7{gtlendk*#O~!sn1IT4(c@Z z^iJ5d#$3DE*x(tDdrWdmQ;{&e4Oj4}L2yrr_#uB5#Slt+?HT(g$7PU%oh_g+BFGM8 zUL)!B^*a^fKf#Vvzt#}*havppY442xl4h2Y7|q?+HQ7jTn6`mz#WM&1WE5`Mm`)7v zmtH#MIr$;5TW4W1u7o>v1oR+oDN(kXYNnY&@1~LI>*gIAycMNrAoIL!MXN+|t?1?( z8ML=Q=I|gx#QwZ389{jHeLQpN=V6y=_vH$yf{H&)@GnA@N%XSuhRnUr0((7$qw1gX z1hbF}Wg=9HrfrVWOqtQ$ckG>OGxapX`nMTHHYG$){Pvfab7>X!Zn{^0bO0 z(16<)*f){f@upr~yBl+wIeNbkMsI+1!{7d|j(+6v|lM{n=I;*iqnz2(aH@uRA>Y7TfIm z^|KcI>EV?B1K>k3zpIbon`u5t&{;EUU2Ohr-JvHacy5NlxS@1HkVjCVh=TW-n)Ud6 zTVtj{)J6ec=Y8TU$w&FRjXd$dk``41*=^ZE%}w&zMR(h(#$TAG#RG5C3g!!{>?TGc z?`+r5{w4!4MNRf0RY!TLzT`DIYIc<&`Juw{szE<0k1aC`((+liX@pzgXd@w=RB-d~ z*-d84AT@4h9g5o|ne=>8o7}~7USn^WkUSOCQIqSvxZ$@X2w(knI%XoJtQ)*UIygWk zZ~yRFO2l|}aRI5dM=1t`5*)ZEWD(R8GG>z&p`#>1L+ZlZC=&rtdhcW&OSM)OgIMpPOEZWz%iX8QA6M zsYa_`cIAM>&-G9u5afqv1S=oyE4FNR3{p2Tycnjr%y6o(DDf3=zsTX6Y)BSVK3-lf zTi9w2Q7cEo-H`Gqb>yjV{A*g&j-Pvmo zQ=$8ie@&s3Hk4=Xn1k)EL&(CXY6L$ZGcBW0!)*{=GO(K#LnmQOM~V4+Qh6V9Xyf{EAcj04`(4 zB=-?G^`nVcpku5axRji{@YM$?fsR?b z8gWM!&&MQa=4T4C$k}i)DPP#Mq(iSV5L0d;auq2;Tc;`qTU7!-_6nAwG2$p_mfUr_ z_Ko<(mVhAI8KVc2)oyx55=Oj5Ow+22J8*AB z-;p5`SM{7k)2^(^k|r!EirpueThLW(Bp2Jgi5ZpKDaq$VbFfXOYBD^mhsR2i75=SX2|oLpz+c=?v=%XT)%w2xRfoGnn+M0o=03)ze}!R|(g z^{map&3vny4EMYCmUR(wqJprv-eGDt_ErgGwCINCatn|Mq35ag;8y+!jPn`Fa>O>i zg{TTEI?8)GaUaj;r}}L2%WZw(1rl2o+){*fx3Z>L8Aa?5h?>gtD}>Z!NAuv?_$9(F5CHzT}{$L#h)@%)eD zb9JMq^8!3;ZpI*uM>kwJcYD59hhgVz+YW`$Nbo40k0|eE2%MMzT1a|`DtmaD_|1*N zs#XKK6oPUCzW`%mmIg*!#{z=OxiEv>ak`FE<5l6iT)N#pOk1lAf;73&SOsG9rDiu_ z-m1qakPM(EJ4Lci^#Rm`x(d3bL{11ge)-gUnZ9HlMUjw*vFgJUa(*RMdko@m!jZX*Ivk1&@G4pO?&4L4skhm41=dM9902g@BFGstpUUv0x1mg( zdYm=VS*eY=msP|nigOPfY*La-^1W~CRhjqxd9YFn*BTL#yj~Dk8T_zT`FBzj`2ZD_ zIy;FVznp@OzpR^QP&#@x!T0ny?Lz;7R)+OdT+niMd69Mm|0My$6K~FLpbNjb$p7 z#l=p|3Mh|`7EZ|WOv0P%QFF`z*D>NQsIJS$^>1uS(=sHBf$piIRaRt|I$`HAJ$qsO zB&^t(S;W>kWL<0N=ze4H)1&zmkLH+q z4wU6pk(*z-=Jl;hZawZ?L@_7#5#G|X^BVwB*V~g-aYsP~nu`y?vd37`XuR{>K}4|# z*Qwgqs}@$D!M^uFlhqn~E_wNbno*-QRj%7hwp4=^R*{iK3!Wpx`FVm7`tj#AI5=6S z`*N`}T`1;$)S|t>R$1whoM%T>cz0uaz|oY6Uuw8?la?;kCehxoRab{1`@I8sVVivoY65KBX^b34 zNgj)v;ChtxJ_5+*OQ@SNwj_B>;_$|>DNqT)==Y|cnr5RG8w=nEpG0ZQgl|Mb7B*l( zIKIdHv}R-G`w982$x_pjmq$&@7&fkvppn6ee(av6P7kmDX1r=2Trft z!<}v6%aU7@HHvR2`qq~Bt-U+Al2}criifUl-qr&=DJl@`Or=B#k?*Ui@Mp{HsJ7m@a^_W zytvpp6%+q4$~fa^xOnZA{rp zW`%6&PbJ#pPhZ}&x>L{+xeoAG^ZC@%x757OI76c(ZO6RnHa*1h1W+QTr<7r zQA}rn()2NJhWw@TVOV~#oFJ}`(^StIux?^~6XSyZnE-PABol1?yBBvDvg zODf2h^Os}Qd-(5-m7o%5$8QtGFu&7O-2Ma}P^RGVBzWiL?L=ZbO(Nc}YnbI*sPQUJAgc8_j zPAgERv9+Z)4xD~*8E?;I$JG5X(du;dv9er%o+p4^cJF9d07}?Yse7v-t0b?EAD8(R z8@xK+K+oOXUrr&Jv(uakkwLl-#SeCNs63sK_xGz(lbXsKe^-I8ryVR6pM!eYtDLgZ ze!)6(WztgGLD4L0#~zP=xTiTg`VhHZ_aDbN{GK&GTNYxzpM z%4jS(!W`kbOfx0E11UauGcRW>Ugc0iSIlL^VY{8v@qr_zu@=Y{u-ra1?EUq*6LotA zAl_^^x-M4BUa2>6sZ5-vdMGelY}vJJnj`8 zEy8(ad1cFWP9=Fp2|a3wbx9>EE}~_|UX?X0#oo_s-meI;-5ielPqxs5OT2^g`yIW{ z`_i@c?)_eYU6hDf3P56eJ^MUVMI?4GESa3Gn-2xL^?-PuHbxnQ*yaU zuC09Qv zp$c=4J4Z7p8@SU@*&0EOx0!~-w7Nz8g0a;Iu5um2w7SkYT@uO}^F60_PvP%dz{dwgYumk5_y0 z-x^7lj3rx<2_=v5m&~{I%&dZ%K2CBl2@CS-LH616{~$b#_U;{1c{%EG1F6X2K*SA3 zqmOAZLF_9fpuLcOSy2v2tS*;C#)lfSL@L+ia1C?!>yQEwQ5#<5qTA=jBDg{vuE@S` zf>TiV?cxFEqmh;WYJPm1^Swe_C6>Ml2SHuabj_Qqk34^v426(K zdVSr8S13G1ZsHAJB~V$xof2kbMo}yFQpO@HR%YYrrPRgRXDI8AD*NJ+q->|<@T@VM zyGwQ#rqE%!;DQ0JHozdxc5;3@Em8i1B1_@J+Q>Y4B3=L8+G{OOvV0_(SEEBYM>Y6= zsARW??pI${sqiUpcS-)N)}wj>9hG@oF{>)u+uuu*n_8{b#CB}bw77vD87|W1#{4=^ zq}e$9WfKi*S%HY_P1z;?C#@D0@)-{XO;AGe`Q=->1NpsNRj335z4j0_ z@^ER@PC}f_xv`oxoeWD^Gi9=oaJEq^Ls4cnss2_nz1u%@f?b?S$c62sDekB3O)$?v zm&aab9ubIw#kvZduM@EqMO07qHh_lElcw0ln>-OsdAdmD3L41l+PP+^s``C#Tta1; z%L#x|I=2iZj3RkznqHp#ArJ*7^Qij33?_iU= z>g$P;=p^0LYNN`vL^R}Qj<0JU84m9F(pK7^e;7nh0l z|D04L*3Q5JA62ewJLuPV2UmZ4vJA*;rDqb1sB|j|a`ckDaQ7_PxB33FNWi1;4&D3DRADlC z@=kMrI)KxRV#Uoxb?5a_;$uJy6ktrx7EmaQ^#(^t4EDdpgl47V14`xdN3)aP$w%jMvsH9-Ia8L3Q&+P z;sJ=-47J|&GC*cXPYP6ckdEbmXZQkBrjaKUZM1jjt}2-%o?&wFNM*DlHOd;y{+ze4Z)B za9{k>?qu&HBd$y3uWwX1b?RZO>+8y#6$^GmMbrItPF(I=ul-pi0pR!a$4%|N&Qwd` zrDTtWZ|gjLYdeB^es~7lA*e+NaVq)D$a|N2QPemAJErezukJYxZ${ig?6sLm^Y}e9 zVocOF>eOUty!x19=H7}+!D7BzF%OGb3}wx;IkjRcP`Eim#S4aCFD=$3o=E zBd+AfH}xA@kFr8oQWcFf@UT3S)BziO7I+~g(&E$U(DzAR{`)3YtvmazHl)FLRAUkL z-8nz-##K^4HX^fh)>^3mpD1B2YNT(adfgD#QK%MkkyiX0LiQ7Kl4S9$A!}{BF#kl3 zc~MHBydQ#6@0J%GBCv|6yeyS#0KNwC-P$C%BLsr)0$f_=Z5F*`4dwY;yF zrMz}n-@b9vd(?SJ8$YY9#HR2qC67~S{@PUFF#LCi&cf(W(zvz~(kJrh*51ZbMnLKJ zRzTBQmm}4+bNpu~&w9CyK0Y*2Q*bC$vdppYlh3a%GHn96IQws^WqM3iBxP7AWd4OR zvs@o9ubr%Qd+dbH-z6Ro{m9?jb||e2KFmInb4yCvxKqrq6MYsNCG7aZedHSB>OMGX zV(zRDYjAOv^7V#ynD!TL^|NM5w^!oi%MjT&+TT>_28mv^)(0|Tr)q?sP*0!&Iwdj! zA_*9k+U;Z~(dmPN&;So(R9G0Ug+bWu{S_Y zn=^bUB^SM3Mx2==TaD~*s#ef1x*@Le-;zeDS-EikvVE#$T7jhuQ6D3FDBYEks98e; zN%Enl{Z(O+H|ZH@(s}--;TA}Vp@e1Bbdl2JVE|(?0O18x)U~7qVWBS^cIY;$QnLasISHHDc27>9uf^9DWz)6sC#oh@7ft;d^Py zCGp%6v_o|<%cimNC)-1$$v{PU`_npI9A^XJ2!k(Td)ut1#jUdEYW=+ys%~6MyZDwf zYEU+$M6OuQe8J0?c>ZT;VpGnA#OsG6o(E$bMgaKyOyZS_Lzu~G^|>3v!Q*F1JThik z_zONXf>r1#39%=~9$1u^sH>9o3rJVXO&9Ml#(1tv`&Y?x7F+OVPLw2Sn%weTW95nl z^jg#wv}7$cBdW`uT=oLlxe%T5`cPABOn*jsZrUT7ym)I;OFs%B6)od4gH40#7A0lH zt_^*IqlfE{>AcoQbTj-&Xw6dsPFmYRYW!QmqJ``I%!9jQC0!VI;1{k$-D1+43LTwW zc_8L|S_Y_()?5nTjERWh5nG4gm$ve&6U{g`IKWMPR z1T8YF7?R(2IN=rWWy;auM-XGV)c{;HE9MN+#!@qL`^gC+Y9+qgM6A$8`GR4+IXR<` zlBOS2i(jk|7lu`Q@2-fQ|6=!Wi8wg7f9k~#HQf)jza(45&Vq#%4Jsn5uS4JjO(W%F;8#nw^ma5 ziyt;2HHRJZ>((P}MY+pGLtafT?fsfkIZ?bRzT(AG=7{zDD$(b&+>5pI*m;-qPai7^ zIGD4tE45E?@tUgI*b?VX&-!sRdA@B4nkIQCfNv@R0-}Qs^J84{8_G6Zr?TVEbLKPn zxK*?lDS`eJ0FMlU61Q$Y$cn;e7xb_gPpS(NN_^o3Qtk)AQu(c|YrU`@HMfycGsu*u z8K-TF=X0YISyaR2>;n$+GrbI$zA+N9SwUD+x=5tDY*X!DRD~s{vHO&BF2{GPEET1q ztkgaL{CcI3lY4^MM!In2hZeY^-y09CJw{5b?sB0HLGwipjKq>8a!TmES8-%%B0i3i zhbXao{EvXTys7!l3?fS(h_wT7OW_#LoHOIP#|9@7c@Q@eS|Id!LK*GjRC(8ckFJDeAwY+)r#-cs+-Mh0l`S@;S)o1Y)IV^`o zP5<-am)D*j55`~W_$M6x<+YIig=GC%9d!SHph$oI|BLqjf-n9oXgK#jXp_I%mHmZ8 z|Lfa7&pH18jjaE(Z0gOwi1|-_x|gtjlkHyx?w9`u%JNs)|HlXMeorj=xbfysXEUwtOaK1@#5S}>^QdfInXFOpTzW_Q z69oDf`6K=<{a-5F;^n=^DGQR46~Huj>F7Mq{d-4nr+@qAO)h({)}u#H z9zA*o{|B&qSM2BYCx3r7V^zbL)@T6Em6a>BVC5p(w^Z0a+xXJSfA3gxOphKt8b}hg z=tUJxR@rN&CCdG)s@8v3HLZ0?1Wh&Af^GMuZ@Mzngt|E&AWYo^l9d;?jTq zJQpr!_2I(@`4_9P|L!u=!+&=fRL#I9mI9L$QRw;G4Mf*K{P!mP1!!CkvSJZ3?3_w3 zzyJ1p5slea9R6;dPHsH#+_`i0k&Luk%pcRHNExi6q2DI?>fbW;CBx}v0t}}AE;4eM zgd0Pl#8_@Uxtph%EF)XszU}+wm#%@bO8qH$yBNp*;e;ixe^100M#)Q4&0PQPo!ozA zBXB?U8pgi-;vEOHcezJmh5xT!zVz2ohWz=~*tG9IG~c&8Z?e>o^qG|iH@UEX<7jjG ze>ACHf}1cg{}205rPjwv1)plmkykv*qhYh4u_9kc z!IDvKai(8OnKf?w{yjA%)O2U)DzIAGbF}|_l+;`4K%qjE!VOXoCTwYW>es;TW_ozr z@_40qO-*f>x3!onS42Ec#caiL@kpazdewzDdE4|k2UTpQ5yF5~q~GvdMjcYLF+Z}! z-cgcp%Yl!4Ehv1<68xwZqgWsYTz4=1&c8w2TtAwIFRfwufkTCjI@NSt=Y5xDWv4IF z$EZL0Wt2=M3Q}V-6QE0&^c<^CMn$)KVmn#zcQ#X!G%VL7$ zMT&|WF91ufLy)Km9!g>B1*%Ui9YP-biee;%`fZ$+XOE^ULsR7%$X`y4R{kS^_u~Rq zLjBP^eahG2cTvK?zai(xA*?=VZg6ZFVWHM zx2$GVkKoAl(#Ox^;J8fprz37SM=~L>(P>la)(BZ8ZMYhqCgW0WqLQ9#^6#CBZs>#T zYgn)d+62j-AL1u`=lR=FaqExE5o}K6;@Zh3Wm5*_jS`s{pTUTzjF9xW+*uhX?PmDS zUieI-dG<=X0BlMoj)@SQT%`yB^gKU7Dcb!*{Ej6!qZFUj=C z3Zjjv)bKZGno2yM{OHxMsjql_CJ;=?tPFblT7YB(3uElih+}enERODy*gBmQXkNWn z3?Gq>`Q$>Ge5B2xqJNlny0d5MUyJRfM&?-%f2jJZ8^Q3UHP?{pGHRmtz8cGLX+*pB zR>>rHzVc1yj?n+KO$dO$3sZ(}wk{XzmxQPKE~kI<29K?1@AW8WG{))N9rV<3z0V|| z%inJ0-9mFSYnWtgg|!VX^W_i)H9WA2efO?UuZ91~vUXZl}wYc6Uu$G4-l^ zpiq(LOFPMx9G)ZJeL&ld z=X_>JHI)guv?wlDR)B)--P*A9i;?C0$q1cY=bjw(TAm!_$nEplH7Y8C1gihEoxw zuRq|E*HT9>l2+2{>~eGbmJ%1u_wa5gn_Arh{jT5r5PZbsi8s5 zTDC>B3s{QBO?vPn(%#YN%dIk#@J)YoV_EW0Wf#eE!|#wxr2NI}^p{{)V&%R;=@ zJlmFe3!Mz6Ld4|14fOUo}_hYwA>rOiW9;OyIo*=*-<%hFA8lyM4Q zZRSTrPVw58Z?N`~T^zX+8KWp&Za=*uk*)m5f zpBHf6T69O)d|!1bj`@n47o+$-w0O(BktY~D&S9ZDFr(*{?x>|XB!@qH^cceCHuh@x zb*?hY?zeZxPNq4ZbWmn9$uzq}Cw8;)WDX?l*xX0^T(vI!>Ijr!xZ;yXrV_`*(}vN> z{MQuGf!Cl=fDFGTxCN3nThc_Tp2o_dEU3{gd}^y5dLhyt*t{ipWKY{+rMXR?d1+Q~ z#FKQgLJVJ*K^_O9xbjA0Vdze7oC%Zb(RYuuTD)bNfVmUEBffp4UiOI=Dj zlirOOcz|5HGvyV1soiBhjU?El;&D=FP_QQTxJJe+!w=a4TuS-VwM$2Hk#t^Wk=0Z( z?7Hn`sUoJ5zVg*KOo^h4oLIZ?9m2a48+61=8pwA$(JFX&#`(xu)o!VugVKUK5mk>V zdJ8UTrHY-g=isxJD-B^wfZwB&<|x)CynLsWCT$6{B8|)MKP+9M3c*1Iy$C@d7}cag@3?EFGrb3c@~sBX*I(CxJBxS_4TYga{y_H zzS>T5Nb7*&54`Qxv$ZwetCaJo?@CN}h-_-oX3OOKlbE3XE?FogeH2uc<+LF4UM2U` zhl^>m70Cup25|v_*VJ#tF_n!T3=H3gRb@VUhH)ctG?*4#Aa0}_)#c}2p6q?!Fq>Zj zNggbEBXd(wqhM59AKSNBLw*nM=Y!G2$MvzQso?|@Q1OVAg%jFDi2YXL3l?Aczr;%C zJmEo`w%MNZv!<_bJy4Phdpy%v*BqF>;NG%~tsp*WpFF*_C=A=0vkSr{~r4=B_F~c!t&D zLlp}++7P+lHRY@PVM%2a4cM`-<)U%m)3eVUv~~$!T}V4EOKXIC%z{E9-z08*a)M}E z+|g&0gd!ZGA1B2r3Ke#&X8UD3=&?NcbR=J=4oj*SjZuif@obe14~Sb%ZuO0R1DfxZ>a+|x`p1~4rr|?MHBaw>PeaG^QTyn2*+RHAZs<`SL11m8 zkDON9_c^E+^Xt6Lz2C#4OBGyx8Mb-4G~xZPf?bQEUA zFF5=krtRS72y6fHH11|Xn2eisS#3Km7WW?J!J9L(6oT(UACdf54`nkASX9<1Z>K5i zts3SIDuW-?TN9`pOj;%-K|R-~I!i@_GMf=slICM9vvccYR_#Q<$=qJ!se~(g_C8+6 zVxx_j@>2STkJXIsddS1QD*1J+@MnGRl$Dg?e{x>OBb@0g;UqlAgzpQ_Jgovd4*fGi z&XakvS=kHue8->ESOU9_5UmC+Cyss2W>H4*B?_{sg7w^pB>+>DCXvADNO6qd~{-j zL#VI;_&b8ciqun`j6$z0ZRuqS&90tC#7_EDer!2^)Ev>x8O_yzr%9*AUc)IxC?PkR zOCi6o@K#$}8_lQ=3}m@_weWGG(As<$z?u~I9tKB~-c3{7zk6!WdTGZY+rdG)Te&{v z>Hc?JSY%rrj?30<9uZAux=!mv4klsk#yD0Z&yu#fDa7?&;v_|v3UQ@TQ#d+Wzu83d)+ z`N>^EAZR-PtWEKJ8)OEMMAM`MVQV`(zqXF)U2LvF=s>h=~(~`s-`2Q)IMNn-ydc8IX0Xahx8$5l?8r>FK_+}d@*Lrc%Fz180WMw^vqTAuLrBGqnLzKjC z(xMn?oWI?2IUkToCR8S(81?<;dF~N4V4Acc3=}B6hPkOjWv>z{#QYqst(C49p86kd zeLOa7mH5ht3(&r0P^jdTmPk`@>6RS0?UxfcBjCBFtxNbzfopL~dWOI={ewwmuC?|% zCq-@C&eArA>`)Fwz}p~lP}M^(*Ye4(Sy_(a+3z3xzHXawHE!)M9Uq32W{r`AIig2y zeen)x_t0PhI(&I0?#Ou`R<|XtGux&$oAb>9hkg;w^_--@+9+gfme*3} zMJu?cnk;x`n3e#`Yq^_+<_&qEa(qzDDsEl+(}HK~K+F+rwYL<^+tQAnG63&47D?C5 zHZA*I{wE79+$K|Qa^501mo*9`7Q8qSk(g_Mb8!8l2^@AAH#zTx_0Z)3ljWzARPl{o zj0P9wah)e-xbms&jiB1T#;f9UMxE%X1YQsNpn)k={`+fj&E~oTD`p6z)@zf^?v^lF zV7(Phn|)uTBHi9`vC+K8^0cRt(sE+Ach|dBF3#E;&}}tQ(&oamFD?^x>x2e7Phv3F z$U}qT;<1@RyR**bWEnp_@vD8KT=kH8qFLP^yQy~0%=&&IS+==;vc?&dqFjDs^tGZ4 zc6T--lbn_Pl8 zlOeO}he1@2%vBxZ`TI#NXkOVnG{M6vYM$HKxoc2QbIO?4IDOE`l*&Y0fln^G2LfD5 z12TWyzcJeOIA(Z&D(*4K)^tO?EEuPh<`^pR*ADyA-!xNxVSd&#cT_y9=w8%GeZ$ho z895uU_gd)(UcpnL6N$ddai7Gk^mp^lz6DK|qmG*}8rtzM&-gWdvXk^f^LaU;t-yVz zWuG<0zQv@vuZPu-G|mQaxqC>5<3daJza`u_&m?j!mv<|s&RW^D3_?ah8ZLdzM_u6U z*2ItoHMxdu2i!y%ni4GM0Zq|w*zc#P@#srT)R*o2&aw>ah2tymE(^O!wIEAgg=$KDo)z`ipvAAcTU5caI^u>90cbeAC#>1$|& zpA21_-CKKSJ0#*dyB3SKYH+_NY+O1(TVN=wqDCk&-?-HD-ZIMndsa#EYT4<*IQ4tA zNBcPWrn+S7TaKB+TaA4?PyV8fJ9C%8S8Wab55?;eL9b-QvvTxS0MZs>&UpGjDNn4C zs+}?4+Qw!Z2l=6+wDXjfZY{3Vm`lFID9qZAcsR&Wwf!0Ts!8OolJeD0XVe(AHLzDG zt$bcv0}sn)Qp->UR(#^a8oN!H*o#fCpiy^}NklwL2@78|bv! zP||F}+f1RQ6fMmnHgy+6s{;D$>g)*8plb%nu;Xi5mEisG3Hfm!4Ynu&m6jTHHXQclQmMqqe9UPat>gjeidA3)he(+S_et#!zf==&a6TefW^sBPkl{ z!@c$SleMZ)!5g~dD^m4x53&G9zS9Na;&}(CsbW<}5~a`mcY%oc7n&a~o`-1V?U*(j zjdx5X$$12qjwakg4Ck5wlt$WvW*bYWv4b3vUUusOwkPSX>%xmoRd4CB2p2Wm66%U} zoGA0(b0L$J?&*GBZWRI?^?H}7K2gc^uAS5z>EoBsSD2*=h|Vmh{_)Z8iQ>RJC-Q#3 zlGz?~TvJXD9~&*R0I*EfG|`Az2a-<8R|gp_koo=ctQke8wqGUf78U`Mg`747|*ENU4D~Fh8jBBG0%aq3pS8MLS(}~74v2(HjvvvzV-UA4%zJ1R! zY}bzMn@9Eg7tzuyum}ix;3N-I6wiLQ5LX-!=YH)Th2q5{l*u8AE^Z5QXOv21XQW9>ze4re)P>z%+Fsy?A6c$zzH z4}bo!$}WDdoN<)U#!wURK()Z$^8zgc440z1#b__OCJ1FTBUQ*N|2)01uk&f`0EAkD zcHe*-)@QHVijU6k#QOFAm@1NY8215}j15=p&JW|efW<$m{1i7+davpxy6i=>mYSE$ zX#|d}*rG!kKI`4Eft8;!T9il?pL>@hCG$zLh< zIDY(e&9}xa2TISH37N*%%R1ITh+ZQEAuFtDU+Mjr)6w>D(RvGnK#x zEpd)$?9*mf{tSAd1CwPxBYx(^U~V{KWX^W0NjY|YxNDf+DF4YSuk@;n{#RaMsR|n| zGN+LH!{Zurg-Bw4d(BZm#u?CLv7Ju(#+4aG7DXFZ##5r!SBG!nUa2$dCRSk77F(=J zM5`&q_dHZ*^Xw@TcZT>{-xr%Yw=TW)EuyKA1wI-j-`2a%W=PggT%)!&pt$wr7Jp;W9YoLWCkP#nXE+4c_)u<@%S~EHnVkj)GiU+vnNbLdZ6+qGT<=^`_vUsUYC; zeIJmNVOw#I9wc-X{_zt*a^sjb=s1xuXCA1LuqBGAtGw*uewZ8A? zcmBXya86Fn+4sKpzV@}Rtr8iD2*N~cRH|OoJI*Uu#FQ%5sJk9{>Le3)@`{^GG#5gL z#yBBL9Fp+6E%SqajPy&)R?W=ZxZ8Rq>e0J14>UB=GH&EP~5 zU|<09Va+*nnGcA9UN4C5_uUCA(Dlm$&FG=R+&zHq=1y_Eo04Z5nMcLT0Q&;D2qLgyqx-<>ZOjX{CVPJ+;vq!>L5e?MzsW!FVNhUluN9evZ|M z7F9(NR3n`;zTAjncW|c@NF)K#1BXZJM9mh>Iu8C!xer{qkSF@;<19nKN>@UGR_ZETon*;H}wCA@y=8g zKu$jAy&Lze4nAcwIl>J<1m$GQ3w`?;#r^maUU22#CMTtuV-?S*7w$LQU|-7TR=1sZ z(M@$cYn+TiLJEC{mS-TQsmM<;qR34)9wGf~oLNhufM9keI{uP_gv@2X3HH_VA9L477CNEojBlc%b!ksM8nnfQh z&D;+6&ah7R`uLs8x~i^?6){%=%_=SAljGwY-_BahVD`6pTy`#}VdHgj5kGVzdcujg7!OvE1Il9<6Ts8hZG|xFEQUWKu zW>3#NWaRDBl~5-;6gEl7*=YoB%A(oV12HRa^b)b_yx$Wd>X`jw9LCS%DrddqwQIA< z<=3uXzfUg?(nna*Vc^F?yE%Pc?V7zZPqOR?$VV@tIVyA?+xqT&25G_y3F_e#volMg z@6HrsOwmu;L#&Hj;VW||M04+CO_I^3Da{3p67Jo{8xld}I+q}WLB)z$$KDwS#m8tS z?rqu?%1y9aL!te;4^A^u>uvKrmeEEi+J~HZ#5rs*T&(QKyvd6%O*wFKFSN7X2hv|u z67azr#Rf>teSIahX7(Dcy2n#C>1DsOc_cv^QA9innJ%azD7dl;feh7jvne4gWTQTIz>y zP|H=$b5t!}`jjM}>jE&^^@g35#6ZY>(m8?>A`U(%yH&pvR9ko8>+y`Hdpt9G;5VwO zk#Rp&1P2n@6=Yy|XSaF$qR=MQH2^%Pexol^`kEJ8z1mO>tQaGNS z{Z;7U+Av`OS?#v($6NcgL~StF)CRVn53wkHvmTFow+nM!!<-ciVjuOsC``Wu z*(cEmJC(;MQMUO7G%`L)2BIs$uAq7Uc{W*n0yRkdOW?2qMs^e3It zle#a1U*71WUd{~Tw*f$39S|!&MMfK;Mglao+iGMp{}2H{2qJtM7ezp+8%D!@@3 zDdyMua;MXA{RXt*MjHX^O%IiO35vl=y$Oaz*}^KRb>?I9%`ZPG6p$uxm6?@s2~NjgIry$Q>8kP8?w zRGafZ-7Oe#^!rqhrQK$Tt5aowg>J@li$aOBNkNRYbn=>xB5dGS5^egBD%k4qj31lT zu1=KleU&X;Dm>?Z5;ovqz}Cy-!36&0Zgyf}b51+ybK5Bz#ZHwXeuHe|tIa|+XLtXn z$L+fKsh7|sZ$y(O zZW)0j-?HIexzkJ9k#+cDCvjhAD^RltXU*n!69baJ%sMQMXo#CkyW}Bzx!#~#Z8e`@ zZ_K5$vh~gdyZ7=H{*)_W%XfQn*+T|iEz+pqR;^d0boaB(aX@pO3p;>(?@{x;URgKX zh2G%7R%aLK`cOdt9n%FD%R!D`292;jlIE(QgO_&JKF zcWio%K>-m~vG#LKc~*|>N(CT*q-*A?$QT!ScNC%6T6*T8dR5rI4vVYRz2->Ig7>EH z5iW$SoFGE01axz1(TTRVWOSsr``v_`EnWTW0k13=H$+j*+-9c-qcge&@mD#qHFWw# zdm;VN+Ja=kLj#kfTgE@jx4a=SXG%iIi$T6$u9VqS-e+cPP}Q6L0*h-as>rd(o6iOWD{$b-j!m)vU&FH`~`ZjAPUJV4eyzg|f~JU5EZXB;hNc z=l?}F+10K^ER)_uGD_~VQ}h&4ud=)`l@OWc6WHEeOB&C^xS18} zO_6Wn=(lV99cSk}p0n}q!=B$Ssd^SE%Q+PfRx zh~-zNVtDrZf1 zzvpL;{+EaU*n3OfX94|~=Geb*FRK zYFio)e*ubq^xNq7So^!A!SF&EW@1m835B_*|Cf8{#rrDbBdc#M)p<6y@@i0?gtvM{mCHWoujjVWRpKzMv|9wKgbWZj#m9Ik^cLJ=0kSrRQJi-at|In_(2EUZ=?wTIJ!@9H~jrC3=9(N-@82Ap#pT4V(Vn0 zU^pYoZMu*#D_{IOfzi^#4|QV3JNzmlb+bW3OaB|~jFscRY{kDHIsKRa?6`t`(j?{Y(?!=Q!H_P_j-pwGbhlb!{D{=ZrPvC0mMJ^F$G4=gRY?q}`s#|?hX z{{J)A`Qwz|oPL>EBPjIJ6@xt%Z{a_m`HTPYxI2vF&fm=2Z!JslCAr0t4g_m@A+F)h z=dNyfk+yDyjO%yJJb%CGZ&^r;DJYF~^oQ4%i64KoVv?6k0jiNLR(`)AaL5EK0z6{5EJLGfEgCrCs)SSD_wt9rx@Gn<3 zH~F@DJmYT=hg+Mo)vMZY^$tv=KT)&!e~$~OHU4(J`c6QeIpjNB^n8sqA3rr~+RFbi zWJ;$0?L?7hk=CLR+(?!~rsh+u#PYzGFmn?uRBlZD?QZSlfwCCqS37s87Ke$rw?z=@ z2&IZf8?^*-R_~X#!bw3o(9ij3rRs&CK`e;|5Cb+X70-K`Em{ul$3%9H2~63!-gN9( zheMAdwV!UCckWn%Z?@UflzY5ClA}wsM7-$}&^=Pk6^ ztBDp|-BPX%?K&uluG2jYL{|L?s4`r2{FhVxNcQte2zq^3gmu@72cioW)MrLE&Uvit zX-;ihC7hw;3RuMsG-$Jm4mRGJL^<6x^G=wzdN86FuCzc{GLzCzlBp5F9ya!ZofV2T zGD=Uhd69EQ;>q3cXJNrPu){+>1A~tz+fHBYTrY1^+*3E5+M5jwJxq=1g5AwF1`1j| zb7c;g@%XF9qT~6$JfdfV%B0a&hH?pOzu$1Z`?1038MnbQD!)M!uHWT82gzskeYkm8 zYoKAR&As*OnBztttP0hz@ii0Sh;shsQz~0Wy(Lvy?5l_x72A0uCImP=b6^8FJ}+>m zzs)J7?EAB(u94VN??9)o?*xIBuN!9pM!j8Z84b7X=`BT29+YsXUR24`U2G0$mih=_ zl;ZB;eBXEL{BC5Lv|5rt6K%(05vAm6rPh1AbODeB-1>YAlpp{esQ)%aNhv&or5cx6 zWN_Zzqeg$`)E_-c`&7TvetLY<8EX1G@F2xtu#JhJCU^^isy~0?76XtbXTYBo}V&D;AWLGIi(EZn_-cUn@4k4uPfVeFNdVFKrspQ@$6Ax-2 zNg!f<9Jw1_P_^g-wRL~oVk@Y935aJMmCZL(*dNci71^F@D8ipH*9;r@5b1tCXk?Q3 zN<&JR)#cC>CbPchOA1Y%H|^5wuR$Qa6KtoGB=h`(@kVIP+X2{w9MiU? zU`G3c!0b?bKMgg`tVc{x--v4$@9dv37K}+5^qb4v{WlkOq(~mTQDZ1kG z0ijq$AEkU*XqT@`u3CLcx$l@w28LpM+oZvC-SAkDvsw+eb8ypCXLtsjyJQ6xD=2ky z1-KU9GKwM>;~yZ_|KdU^ocY(L-*NAPN4mvVzcNR7dxYBP4Fw%xfcVJ4=N#hmYgxZf zC9#;BRzm&Grxpd+(TQ3YByWlkb5XCY`wCI%_NP-W(?u3D$2! z-Z0heegu_svgo~}csuY7#Scs;dsJB7Dc(hfTd!FF;GVm{&hV*SfDD;CUe9rE(OywWV6Aq#jqMUGTu&FWa%xZ>0&7 z;5+XDfaKsOS<%i<8w2^|#X$OJQv60^i`ss*$Ys20oCL@5?B_5p`*r0d{03;J`pbty}Y2Q5Nv&IH?`JuwoStg*TZT^9(Sx-{^J^5K?g6+q;#Q3_1L^9vOE7g@9r-#pOeepty`Rt z+2&lLV;-#!ghf_6II_ImO$LkwpOppjpv^LjY=rR;1*9xgfWK*?jgKJnhGrFNZBrDv z<(8*AZp|-zG{U@II+O@hyIav*Cb+V454Xz)yXu{ljFY?nXQFoXjCRepMY39k=-(SC z!+hAK7?Qy$4*A}a+LfVcLBy{i)<)NwsCtqV=HL50JvyW~PYbl;&BcEVIrLrozWRx(y?5Jn5fL-+twNxZw;*me-t$n2xJ}0 z)w_YrK-_g7h*Zq!ELhJFooU2qonklqL=_aY9$YC3uNuabazpYlF%1*<!>$XWWPJCf+tYeS7P+jq~R6Km`*`(a=Znu?DXdVFO(1M4Kzn?tDEbOW7TuRGy)1cHB4+F)A-STEk?z?*|(o-AU95?lNnNUI{|#wp{h`SEg+ZV5zM_xTn(K+U&e zjfSOLhAq{X+C$ga3v++r`{9=wZR4$vq)e8?aqXd=UMi;rvGCfbS@y;~*}sA^0}Q30 z>;^jCj3Y*P&1m?GC?6WpK7yZVc#q6?;MtKN;vU?)Dwqz%zGajRdPM2Elwfm$4FHH% z@O?}lDnI2B|2ch2Ly4k=z6*AT*9N*oiL$B|cmfu<)esy_q9P@pCtPP@1G2CIhE!HT zxK_7TF~!%g@|ZW@KMo*j{qT&MYwlH87@)p1@CF{y{~G`#y3$iCJRcDAyitzapX3$G zKN>C12QoMj(|7EVLWqwY3ezU`kFUD9{s!a-sZjT3@psscU6 zP->)8zh9Kz%nCyd1(ekBf#qU;Gn+-WK z`%W4d4kIGWSI?OYXNv<-Z^h|L(OLtDA(~U>X8EHq7XDHG0umgKg$b~6+yoiVG+T}lAP4z!?Wvs5 z&natkdY2zB&Vfd#t)qZPqR2eMoF$^{f+5XJdfACPgvZpyelP3 z9@0B=MkV(bsE?xM`uZcm#~H1@$>MkjUP&5!uX%I9$1_Nqf_TQV2{1abeufc z?AZ00(KA0LBm~tP{q&N`3#QG*VOKbutlBOhbHcP2q?Yc_9#gwb-dCg+vz5&LcrRi6 zN#}3EV8(QoFFZ9!h4z9n!)%!D+c7CAvRHVDxBQ42g4ATm)4do2tkgcqd&Qt-$8rnU z+0IRh*}>quJRmMLfoX2;f7``Bd(RmB4OfqGE>lsGoU=X)-y z@T{rBgWpv4rWmW8z5=HWp};z0f7hhmFGcXT zKw5p379IU~;Bkl}*C$WDFA28^vuClx9MuqUI%}S_*m}_>w^#NE7i*?8y{O_#hb#l07V}7@;W%lTe@ree%@TDqGfS!dP|rhe@TiV zto6@C1AcQ;E!Iz-_$915Tm}{=+m|s+p1EA~J77~HFJ7r$VxPD>Qo#L;+5WgnMMuK! zt(m#wxOdnvK)hZQkj;IBDZfy{qim$JQ?dnL)-LXwM81I0SZ-NU^##*{hD~zsarsi- z3B)Xd)g0Dv`(#7HEs{6fBYb4$=^;1H;1T||$aJ`i2__;0ZSFHZ;%`y&XJg7;R?UB< zWDN_Ga+E!(A?`a9)s0&yEkI<^t<02#q zB%zC6K?|9+4u&UJSe+;R;(#VeHb+sEnWinD?L_bOjRoe?*)GN@V5hIUE)@8E+|lTV z!dWY5jL$Z9&p7T6{5`8lCmQ<66afQ<8o_gkIEcg*pigo3$u!fC=t8Z7v^k^SnGsZj z#qg)Fg6eS$tzIrnM&qWqV|{6;CU~}~UAx2?5nuO~pz2OK+)6LAEvURzR_Rvmgr5x{ zVT*nlEo-dfMH00Z)=9A{4PbiXweY1*s-mCm-1exZ0!)As(PgYOxl_U|C zg(Dm0K3$Q?)*c$xR}oe7wo4zw{d90wJ^nQd^W)U7{s!xP3MDP_f`MvTJQKw%Z0gWkN*bI!aNaPXa5dbwx3;mZJw$0NrmdfzmZi+K$Co%i1zd{R zIlh2m4L9xBZKYRv`YN3@?x>7Z>E?C;pbPnn0_MteF3MnNZt4POWxE33e7*-@-Zh7OF6Bc0Z%;o#Xx` zm){j0OJ?pHXZbNAKdT|T)uBg6XgxKRM^y0W_uC6S<@1PmK0UK9Vg#z6Qr5H>z@G9o zWn%Pbgq{w3^4v!yJbxLi6~Q`HMv{B}vTyYRhS$u!%x`6*Uk?$29bQel5Zq(ZIy})G z#3OWCGfENdn3D2jRBplTeM~@b$qRv%NIfe#|HY7}@118>wo*JEAB&I*L>K!S@^cSY za{@Wh!e`D(i#iOO$XeQl!fxUB40#U^0;e7mMm^zqsKtmiLB0r3rTt0qV=Sl7T$CIW zFYsai#5AODT_HbmCPzy0sj*H!zNU3J{=o{&iJn~QE0vAPXUHK+pn6*k>N}XfypQ|8 zy^oUM0XE2Wk*|!NVZf^b^>02d&m-zh=jw?abQ(jev>=3EBGaCW`@m2DdMNnVO1(WI zNwKo;0NuX_a+%75M$dm+(mjL^FBZ!L$}ATd*$asLNM7D85HDm|-vZc!hzpQZ$Ntyh zirB$}9LKrH$N?ik4!ARIoiJ;sRrX>k%SUCgv9JB{Nt50BtwGeQKQ_#_Vvv6FB?drU zLBJ*2wnc57Jo7gWzrM*u_m>FM(Mz(bA%@i6sm?wpiU-co^CjX))N!p@jL)E~3+J!J ztAOrJ3o^IC>GRl?9h(F_m^d?HVDQv+RZhR__=Ss{TQ0vE7^VHbKN6(j`VpNh$Bw!4fEP1Ud=~uj zs&5&XwtE;In(n0-Q*-g_4bt#Ny&rD_ZSX_AZ?u_M#6!&$n(mHyQ=hFI@4#zj=sf(M zxL8`ldU`sG_SpTx@*u~Z^=PU7cL>+$a#LvulaCq^9Rd+inShb&1qk3!txChW5yhttl7`sw0BS6oeX2F*W_Z$}*Q;zhpbV~>isL-1eqhmW-X zftx8r$Z3;?`SjtRXUY#q(Io9#FJEP@d}ah^(5Dows+DP{TwqDMe31j<(BRidd0Dyv z_Y)ob_#rdlZ0q4uOOpyqm69o|9K(QA0a;+gdvV~v0Qz>)yd8CSXykZ#IpV>D7bko3 zvX!`qjfJL(jg-XulOUsUR{OGJ6d#Hu#~p98!(hUwJeHT?%PSx;@csmn4n$04(>EC| zs}GsV#o9QKLqdr*Dbm%qU$ElQW&k*vm?XcW&F05jcqZo2(7gyey399*m3YX}o+MyZ zmpiP~JcNN@1{f_y`zGwFg|H1`fi5!xN9n=S&XK_na??#}7`sqWS4NbAB`lENtm23C zhm+w4`6MGoFK093yH*V;#=2~PWD@rQdVzTH0%{9SMh^+JW-{X&B~6{2N}LS~DvBtL z;->W4d2IS7Hb$#CdSbSmJKehVU_CtzIqI?@)gIa$W|+Z=Hfl}75fj5=kuI-S6v9lC z(C`(OG9DQR>Iu;ClPFwfv_J5EBOmgEO2hVMp^fh74eguD*Cw#Rddsxq@oeE_k|M+n zd$4VjA6+Rq)8zRTTm>PU=$=z3{*c-oUH5dkmVPez+JWOAz7r`(E5+C@1fL6G;L@JT zB@`!4h@QuxBWt8aG8VvOYihFOhzRw|8f1kWxI51y@^iEAc=|tK%l~L>{i9sx`N{Tq zR$uCE`{+6mZAKREXY?u%FI{S+Zgy<#A8n0GT<=NE#tGs$2Hl4~0W0F6o2hlOyBP{w z#(<#-53K!K7q4FL5LA)B&{({pR7+wi_YzC=9eFefwtsB-Ewvf@hjL6X0AgbZ75@WH zJ>vUNz`>H&Elj zIk;Co%VaDiZbLBA7xsVX-1V`HJII`gNO*jv;gq7dDA&6FiBRRaK$Z&7u1xg)jL;|_ z753R`s#!NEUeH1R4^h3wR2R?RTQ`I6CqlkysH<~x;|5~y#VgiWsTBK1u8ppG_w_y$ zBmoVV-=&MLu!s2e)4?C@#Kq7L>Y=Pf<)6Eghxv_33TJ-PUpFFunkD2AZ0W&c&ErIe zIfGP>wGSZMBw*uP)-c>h z6LDP948^Pcq;`^J%Sy2_2uSlk|384N`{2n4#7oY9mth%rW5 z(qn7*hy`-wTX)zd+^Nc69<3r59L#OW5xKuUEn^SJ*i*sjKtc@ly%jl3bRjLczV$q4JUi}tGew5R-zI3Z zi5xa^R1F1&BiIc(CjuSGjbhFR+kQ-Bo|59C%yn;rB-?b=`q-n(4y@qCU-@1VDYM=O zc8?V(x69DKgqA>M_BED}!XsAlvfIClDzLSXYEBHkpk~c~# zi}+Jzo?(sspVB@rcnqFP7i>DT>ONjLh|U;){}Pe%$l`El&3tE(5x@cpgnV#mxOrju zYe}t{r%#Du69}h#l41VdCGxRfnv0}1lqqFkr>++$KlNcr#a4rD3jZ`u0N%AX;~Nmb zv32E75YS!YU*^Iv@06_O;5?PVBLFz98Hb;9j+8sWfcmmJUYOhs6$mVG%@N+xb21*< z|89&q?n2rH{{=37-T7Bq!|)Wgrr!#=c_{LlK?<%&|E?B75Ja`_?5Nxe<69&Wti;fy;J`= zKAw(gwf$eq=aZgp{{N``yl?Nx+=mOSkZWdc`%h7El;J~&#QPC|tYnW-T>sn5^ddoD zx2K&t$U?7R4Dw!}cW@hT5c2hoq@1j()FWT)ag8XuRCR-oxf1uNNkA&_?|BuLcJ9os%N<$I|}4FQAVdywGmpwpANic1hc5`{~Cr@>s|6s28a9I*50@I-yE!yVPb#F zs=ZJS|60tPY;j4WHETF$1EK}h+DkOZ^?0GY)_oDi-OZu*I>)3VnH;SM`9v3Fc_|o! zv`>f(Dzr57=KRl#V0b5TN?tnptAq7F^nd10pOOb(x9fKq%;T>*h?gY<9&k=I`VPa@ zW%GITSX!PFg0QHW^uD{>OkEpJg|HY@izfA0{rx$2 zBY&gIs_b4_QeS)UUj$5CGKghdgw& zy^%11U}rX}+|&gPVqbMplM)27qR*L*wH6RAG=rXU#tZdZjANlDyN~mtq7!x9TgVq7 zviFV;p1hJc~+X=EK6xAJe}k7M~H_}A|*j=!*! zplfjA-o=0zPlC>E)CY(U%FPPeK==dG96F(3J9Zh*ra~?5oh>{O)TSJM&+BXtF)qUA z)+`g>q|G`wM$VV6z^Or3;2a;x{H=|$vVyd2_W$;>(1TP0+invDBvD;k@=>DT>`zyv zgv_5~q%{3qES@PoCSoX=t$1CR+gpv;lW!+(2a`GlF2)b9s6O+dIsk=iTE?3s$E|Y4 ztyC>Cr%8hSuZ~1BN746-cUMbGyM40E3gi0w2A%@?b!Zw1+-Z&-uiPNG%QKP{o7V5A z7EVkiOdd=pM(p~l8(5o}O)txZsZNge54`nU3hH??xid8p;f7^gD|Mr}KVMo0GjvFQ ztA=bSpUzhO?mZ{7cm?q4)s?PrwVeQ<>EV^+Gk zc)$Vp@secWqYz-h1D35)y)dO3{gZfBoTQ^402&>vR^qE-0uOpaTr`_~cqvAsKrOrY zMtht#G+nj?W_zU3TWR0fx~xs8YN=P@FS9hXi)1xu4%zRiv^Pv#^d*)ga?L77%*Lg-JP(5n(?g#l5Q~%i*`y znR)(H!-amg!S?l7Q_pDX8vOI#<=`skUi#hgo?M#l?$X41p0vIrNG+yhf;<)ddkbPcx zef{-VmiKnw7`mFOzvPF$tt;cXdX|F7E~g|Sw)q^567M_c42&!U>-Gx+niobYcF)F= z40R(aIn2Aiy51RG@@rey_QN~+rSw-6-UM68Xbv}BdAo+gY##q+62*+k9KNi=%Q^O;eJ)RH0xab6Ms-8R5 zQpc&O`#X2cg;x(vX5JSIJ({>ID<|*xZUR?On#1|YZh9ctwV*N-Youi~zqt{#Qb4J9 z>fCcgHZ%t}K&$b0AJ!l%-9oYQU0>FdCys)77%IMD!f=y9RAEx*v^KjKocQ&LIC(8{ zcT4HnaXXnZw2SM1DWeu_JmvBH%ueUE33_J{`={QtFi$4wb%XR;Uz1i&lM~c4 z_+@y@D94sr?(}K|A6rQgxJl0WZdQ#TfD?68gke@(M!$~vVX8LbmYG-R;ft=4-%5Lq zY2AV?8-1f$*E}9*)cDjnJZ`E}o$Xvy&!LlaF@y%HbT*Y0@o;PGzVNwP=~)bG24nMe z&WZ=F3xDhcRqQ@1t77q^Y%&8^?oJdv;0||c;an+_3r{%no{%E8>dc&~Ln^=&_=(^e zGkIFwc?uT$;|5-ZZ)hdLn3Sg01zPzm^N;%dhTehu!e5j1uIQ^(r4=vxraLl+B(2e^ z!?M(@?PhpTfXYimyiTN?{;lPL$Rjp(9X`rzd#nfQibra&9tpPCIuh^c$ zH{x>!wTInKeo+nq_Iwb(F5f?(EgtMbyoaB-9-J@Z@d5nZTXC`?yK<|(Q&8{66=~!X zXQ-~-2m6Tu4lzQ_L$agzBqhs9Wiv9jugURgx?BQTbqo)-ti4}MF>Jwet~_AG1S4*5 za${b*PK%XY=?B{!&x5VThM(JHhX_)r&?!pV)Iufk8$rs&09!eo>c9r(o^}L}=GnH? zIN7c{ZV$ck-1^pcWQo4kUp1tC5jSyPA(F&&9$21fz9F#oZjr?&tZ5?WBn}~n=}(W# z+O~F?xayuV9gL|wkiAA?a zT_X4#tO>R9S<@fz_K32n$9l3;gwArYa1L)?U<3H6`b2O7jiw9>QGG^|-qxutRUh!U z=#SDI$1PN5f$wq4SN?d%dM;9J+=QN+H6goxt>WmH4cF$d7G9s}&h^o(AKifUPKSEi z52!p}I@JVb1{bfO))M}vn9_w@wX>16fs{7TC@ueGoaOlh0bLhLIU|njku9!1J)`_^ zu0}smK*bD!!R!!94)-@R9K(4UsYiD-G8K?UCj=Kv?_?Ax8~=8Zhu+rEhqO!^N+O$GKrk%!R3;0fc=OsHx&f%SW;#xPtM&z&HkFH-Lt%U?w;v9lkr})Z(X&-&Tk-}%%3Lf{gSlkqE;^G@!m32x z7#m$h&M9SVN?Fb?w53p87d0a*zY1%(l17e#=BSN6@A{8Y=nW~|T9|?38yb%I(qa7>i#f^hAlRbNbi!y}x zFd*9?j}P^uJN}s?S4Fh0@v|8o7vUJxF$jM9ud*QlevZC}*R`Nn^SFJ>1jSR_dLP~u zb}ZO!52cFN--K_IY+Ti6wDh_igF_ws_uU>%D%|U(q75w5gov_L@dr3@VM%q+;1_9& z?ZFqnF|no;x^P!J#ML(-dM?#yt1GhNx|{&n3nR3G4Z2C~*Sj;z11H~3$vp(0LE$wl z?7NSpo2KpPdlB|XR^6hYY~3RV%~jB*w|-`jDXgGrnmK9SP%o=pf2brmZpq-=pkUAv zxf8b_wmXQ}SE2kd9MrkC$*`s|4)K6qsEIXfc}e0BTX~-%M9O$c0ru?ps&4c!#jZ+! z;yx=mY^W8)I)Fi7n@>5-5xL4u+N9uiErk6nvgw}o4<7HcCvf|b`32WH+A zf+DwlVs_dSjSC!-pu46EPCM%WsUuO7o93KzM#rm78Pxf&d_a1fEb6{!5Bx8WJn$ZU zsVO)w33ul8`jc2Xyy%;Ns+;0ND`eA6zyZarq>RdCATJbuQyX$OJ+8e%~>+eh-+W3PLNy(mt8-6s?acAa;$VI6XRPLaou^w z!t=Z4L`s~gfFgls`t8t zPO@{$yK-!jUhbFouv#b3N&z(c&~CdoSVh)vMN8awBS_M+5~<2R(`nu$=-%|ICe8LL z6^m=ntB5pG%`Pa994Hb91g#bJW0yTx9`I%r#D`&;Q-r#Im_A@U%)_Ya6&4dI2lD>V z%tfRpkWkaKophNAlfZ)>e0o!7RY)D`Ajk{CLUk#U(^(_3%O%pbp6)y`Q$xf%f9gl6 z=C~g(sbkdC^?9p9+rNWWJ|<#+yOMJ2wcJ86#pw+m7f&AsbA-FvdqYwew@X82EOk|k zWhIQFio{n9Q3abc}AL}t!hN?Po4DRJALeCKOZv?Hpw-AH^6n=2fo64~U5N=a}e2-p|pk)qAPF~9;IPqi6=+KQm<*0BaoVT2f zyvVcJz#PHF-PcvqO5zRdjWX6y;6c$2g5;vIFK4)(QpzE518bAFZ9Q!kl-W!f>6RIz zk7eVQ$ABqFPFmxHDIeEsw`<0gi}A5WA6~D;50$u>RJnW^^H49x{88^l9;Z z9DM}QTkq6102%g7GRSMEKSs|g%73*Y%=!Rh5Ld$SL%Mv)Gp>)KFYezy6)FyVBO4nqc3|);jFy&ir3OF6sVJARY$Vq{c%mqY z4>@5*!zw)E{l26%c}@KCsIflIJzqE>rBd9YA&EfA$PLWHe9AjFYp#$7%XU+~F9r@* zGGPtTx);wQQQ)Uw)kM!GC>c^gaI=fUp{{6QlAK}8rVWg{H*?06!CQ#v{SctytGWL2a!_xWAO`28yH5GG_5>%j- zT9L34EAAHyUjSgpgH(0h9EyOii{V}>arx^|cg&8(rYhc)4W%0UFB z;_Np(HZ_M&$pnvPP=Z?a%$MtL&V5Q-cAEIQba(Xpru?pAqEmh!o&GlA*KkKEOwQWF z1Q2~btu!t+E62aHyA7ZbV#Ow^&pOCxVmd0rcfZ_$3^Ik@!a7ad^Y&OI=S-} zEWPV9YS3@!%N}Db&pIAB?fBTJid8ZvFPqn@QFv@dslWEJi2rtO8No0&yZp=zs}gf` zcG;^zYqB40vspTRSV+V;oK<>@d&ksy!dOgBB560&&L+YOcVgS`CxA2kabd>^_uyV^ zFtOlWiQD-g9ivR}ug)tM=yl5m{Mctz*Ee#>jDND*^K!l>A}dZl zTxq4(8O#KMs3_A zhCN!sxkY-)zG}_wMpIoDRd!Y=jIZAw;b~z++GTuV(sRstP#jiMyRj?BP}>5gm5$4` z9q=?pQRMepd*S31^ZbiDHJhnc=>&8opY$5(!!c$mvn&1@wRyWeho0&#_TB6yxsZcB zU|{U(>+GPRMt;Hv$SUT*J7Suv-nx9#boZDC)mDrca`3m!nQZNq9@#SEr1J167qzET z43Ed@fW>I@_{h$Rqmx<@c)!)IwEx`PQ6RNE=1AM;bL08{dW(5m`hHn=Yt<28q=2PQ z&$YDk_ODOAas*I;1c7RNw4w7WPkCo?{;G` zv-48wbucU?zU1$4b%J6s(mEZZ@t)rI>(&?#wPlkuJm#~jlC8cc4MCY6`(=$nK3QuJ z8219`S2i_jghrn(y!=NnJ;kC!8RuQ!-PmuYB0ogQ9^NkBl@4;g*G1vp$A-s!a9Fmc ztxaY%-_q7Kn*Eq74p0>ge9D z)f;O&1%kipN}Ch}Bmqfuaw8q%s6*MR3nL3?b*>?*N#Dy+bEfq!9mmR9>IRI~)x>ep zCAUG-8CYW&Kyib1KjINR8LURzttFu>1UfY}dh}n&M7XxflY9m0{~xB_IxMQNYvV-_ zQ2~*VE=5XW=nhcDD-+GMn^&rFfaFq#(IQ8^_NMM563G*lMBJngdjM% z+B}znmc|_5aHpy*x6A!Hp%FDCHMCxlTVBrSrrXOYoa#-itr4#<6&n*9LG`L<(n@84 z(!X}*DqpA8kW1QZYv4aR!lQB?Qi!gu+c))+p*T;V@Kh!Yao0js)(u3bK5J6tvfIrA z61(zk{(Li~SFbqGwJ~Px=RTj;AVKE$JH-VM+(bI^sY}?%+PrzbTtgs+A(%5FQFLFAY~{zspT2Po ze@6j7PeSg3(D8?O6oh{rd>zH}$x}g&VQOVhzRV0?KU#?%4y|e25S%bdZLWxUoYHiV zQ8Se@$+AdbYvMl5uG3%VVkrFAv9_ii(YzLNxYT|)pMHJU>rWsEGNW%6GQ!)wat+Jp zt&>sIKi^LhkfL$_gxQm|!p`86j6(TYUU^2OgBqICjTAv4dA_YMPc!rX2(nCtO1SWSVaKS=obC2yV`l-x$d zaB7_|4?!^eZvkh9x#YYTrHyYBHi@%-Em9V_9rn{E2FB|z9QeDiw`~_a2%@s7AD3}~ z9*6d%ynU=oSN0C&`BAB1vvq8sPHow{{nJU4-MAu31M@IKGPkJwzclOo%g2+m`h8># zL-P@#+Z@6F4+%9IW<7FHFmBFW9?}hpV0cw8>RDim9(}gclpD%jU!r)VC`6xsLD$Yc zi<@EpswVEGy;!9>c)2F#ehcPg8Qg9`$O#hgEqimpz&9LcHx0hnj-PdP-%S!u@oBf8 zUEtXKSmM(vDAd@rS@dxC$HYBoC0k~RD zD;M_YpeD8h(p{6ye8?`x{$9K_j)13*9-#y59x$<|wkz&4)2u~s?JShF%iKA)-q!p2 zpZH1EIgEx}r0EdEzXOa;WRFvGSGh$l>97hj@+r25IEZNX2R%q< zD!%Vl2bxom@$`2a4-?(#NqXGW;;wK`W_#|-{cNdsnx#4F@rj_4CUa)sKKp5*tLI#m7uZ}>O(VO!{AZs|6Atm}z-pPUjMnKDN=#35%^0@Ubyd14?MLrKX0sG+r?~9K z>05P$^|%Uns)#S$f4}pM@|mY4laQ)TpNm>t6KnRxek~v#n$4-um+0Npn2=nlxBj0S zR6oYyewmZST3Vw`DSlzvW`6-_b|Lt=)OB0Ebne8vJ1ajP_*TO4Yd!Qp-Sp>#7vo-> zeL7$GM2x0Y{AazfnRI%tr`@V{dD=~_TPUKo&U_bebyl49m$ur7yuDJJ-R=B6{=CnR z@Q2kaPzcn=uKmzO$6xXZ?U_f&iM|)F=JUWo=e#4a$x#8jsMkp@r$~Pg!lq`*vw)q z)LVJ?ZAra;g9^E&33ra%rct*rCVE8hHg0XI7>w<{k@|b{qIC)ER&qR zJ97eohyD{C<7%tk4|ndmt7}vyb=R5}Akas<2RwR~zD%tRZWRi1Z-i(Py@0`2F*;_% zep>tGfl`nILKgb{N zr+Os!m5R+TuOc>-cg%z+RWL`3xXbP*7S&hX)MIHx7IGQmQZmau7hF)XE#jX;J4gMH zI_}>Y01+C%qhI)e`x%-Umo7@?)uO`tEl5T4_JPe@mTz?>;}Lz6B}cCD<7asgZ`w=A z5O(L43ilQ7z2%-A3LLd>O*#UJ$*?o*OmAmm=@dKAMd?P+@U~rnZ?M}~_|=ehR`_3N zcnN&Db+t|sl?Aw*X$L32R)=<=_jD;$a9`Ixy~1yL`1!=XKl5C-srHWwIn1efY@t2@ zdQu! ze7e28)gfU7*AU=O#WsN5+xb79NTWX9Y#^D z{uRpXqg#M4^%XO2`&yJXks~5iexI35g(y$cTumv7!*d5;7Rp!cj5t3uAPAa8pQd&- z@P~C}rU&CYesG?m9=u=83{I!NCxvtWJ8E`-$gfu)i19N|1EqgRYkl_R1Tjb>!~u?3lu<@ar%~R+pQI-0No`nPg6RIfg@AiiH%t> zuZ08z$e7!K$BRuM(rB>txB@=R#T-exX(>4#*UB--xIQv-D5Xp2kRMC)$z%P}5glR> zd8VvS(<|AJb4#?FeV2D{qfAdfACRtGKKJ@cB>xHVS{4+_?_>S+f++fh%wncOD|@xO zsdb<&PNA^bjCLMSWwry@0g ziyA_+t*YGXX-#&Sm`Ka3fI6Y72Y?35m~HM&Fq!16(dRVV`XS|P!S5nVXyocD)w_w3 zKp@{a8<6+a+Vx7fz36zpJ#I%al-XR?dCmGAW?!VOy4|27Sh`r-bwp3+2{wLx`P=8J zDZ#EWxD*1yA6^{tyJ_g%Q&Hmj5b#C!muWVhP8V=-P~|IPOFoJ-Vv zdq5ahRMbMM70|Emn#lIDk~O4 z{eG4n|6%?|b8M+K@++-Fntff(4i_I<~r82VS^y6#WqWjX{03HjNUJ zwsG||dfQ8m4#^|0Ua&6wa1%)6Tagm=^1{x{T_Aw{=E}`AGZ0dW|2!s*$6aO_;sG-u zY1g`6SAE|wOPe!U^mzy?kWY8T6>qQ}maRs~J{W1{t_Brwa-ujcxOuck1vxE>52Ka* znN=E9KU-iQoeN#DW^v6j1SXNLYm@2fu4jsm&(?IpPqf>qf+Q(5)F+!p0hfMfE2=s7 zT+w!B`sT@XdDZ@lY%JD8&2;J!FK8yJetI`}Yfd}wH6S*2m86Z0cP2%k^_H$q2;7hd zp+^IoX7@dllk~a1!T0?wmMk`#o476ZfSVN$*FG+CwERHu+$gP)MK1aHUs1x9zn(lM z`fY4VV|H-h0F3#tni-aK)0>dWmmG)~^lu>&5LHF?3U9A4^t40j-2Ee)G&X&B5zd-r z82?%EZ=FwGZV;YJ=w?vS=vV;73piRH?xs=<{^XRrS19U5AGD*d*nsoZ4LHV5=moK( zNO3hGFZQI4dYVq6-!V{`dFHo-ZYo|jh=E_@6UY5Ptz^gk*F4(H^D`S@+kPjXkWt_x}BanXiqmo-y z6!)#Me}4<5c#$XAQ?PrVjZJxWbL}b>7W)N3EiimmmF(_A+DMyuxRCHxt24ge`p1#WAk3UQ!RB+Bg`pE~s4 zK(w6c#3rJfMFS*d2It9U=E^%F-iLd%ng6m@Oyc27>Ms>Ee?Sqtzvm-6cfT&LaU<`k zM_!vFC+a^+sL0z%ZTg~Q8snOLld4sFa0+<&bO~A4JZNi4sVm=dn*ZKmk^0p7xo+B` z-pHnEwJhu634K$f-G>&zDX(n}RBSxW=eU3#LbUQwr8F7(ym<{=KSxewE9J}0?JE5G z_0v{3bl2IKX4q?wOh(s1`EV+gKPaMsGUA5;uCL+*P@N}7zkP|@Kz2dBZflk}iF5sL zf>ipmbN@dGyJ%delv0L$j2b~5W0mS>(ml)fht{v~6*W1+pB0v{SB zl}=BGyr| zsYHeSFVgf`uK#_s&~h-hOMg=*ivn#&tIff02%AYSmqQ=lcLaf~S)ME14K>O}P2aXo z@Ge-6uqC1AgQU3P+GUfdB5g1zwWZ5p$-TMe>_d^QG8Vx0P= zHAR&}x_%k=G*Mvd+w&sL=wm7Aa%5fptU_!^|3%J-E~$ z?)srs_FHK-fcceFLRMhrqdn>G+lP<};Xl=*4Q^@_^41Q16Sf}_Ia@D7np{o`@-N4|VNeiM2&4}C+-M-@({UrK<;F>Q-I)5Xcv zre`i9B~g~k=Q>l$!~#nWj!IVEm#%O?^8Q}9WTsKtYcD9X9mRz_QL)^Z}1+a zSHA!VE{`xD(6j_h5HyeWOW1#~=sh`6sBX$Qyf2=z_W3QW1C#-zI}9KzyDFXgCv zT?3~DZRv8dMFiiAkRTRK<@c&LIFKEWLu4d=dd5X6(+xJe;pl|HjVg@yNUJ9r(5Z+8MX1L@8heZd`7y|N(KvT zT7bwGd&drLQ8VvK85a$#zH{xKzHpi3oNC<09ay064A4~w>D^?>7aZ$N4vYZ#b3F6J z4dGecz0_%2K4!(NnMe8tPr}eXm}I|pk#PR~NLyRgHxzjJ)E6upd17Zem5ImAo-_&D z)D`;{w^BJvt>4~0ts07OXCzam2kM* zwilzp&^erD5PtJ|w-Uauts_mb_C_Wiwg2uX8hc*q54$v^+{E_^o#mffZIw*pH&T|k z3!lWf9G$ss?u{7&q$;g8Gr^fho9A2#%6`8HDPbuwZck&+Zyh!Sw5uR09mg*>Jgp_| zA8EEpy~#hWE;YDsbjw?Yf6G&7#uM*1^pYEhAU6zqQboXTw7P@QJetHOE*u@x(;EF; zplfJLBUG$CtLJtvb3FysTqx)&zI*D}>z4Fg4dK1so9A}fkRX?m=dAf9O&4~Q*O&bU z+hFd$E4S-k4z}wx<$TH{o4B(DcExa4xl~`CPakgGwlhYr4Dz(wZH#U`9lAA@5_#e% z8P`vu?4oRSR;;O@w)|CegHB6b>ekw5b5Qp7G6+1FGMJ$2%G>-)C7`?=!x#{4f4CZT z4B$VT*+e(Wgd=|;3iD>XGucNR*dgQG1YH|-S(_=-=&4D> z)r)WH_8;KjCib^f?6HVV#quH;`YN8|4-kOdZQy{oor+YQ!fCs@S9n9nXVCyO$nxcI4Ju^eC1q&nQ%bA;j#Y~Hv)7Fre;D1Ii%es z2iF66dBN6*WANMnYmZnNQ?EXxi=vs4tEtNGf;VrC0WVkyV{tE{8pm&&Zec*8A!Wd8 zSz`)gPZ|r0lZ^^@WzvK}AAV~2L_25v)s2+(oq&bW;|6!&l^aSvg7x#2l?xI9`z7w` z&DO!tx3QA;{=oxmVtz{szxz{>{5s#YhcUL=!JrdK5?v;kBdW)S&zv4l13U~~T6&Qj zW$t%$z%JFiO;vZ36;G9mE{Gy;zZ)Xp&vu%wakqo{s+Ag0+NU+$MC3NKJWYjdh$(lI8!oo z5O_@n7RR^8wEzO|jr0v$->5GZ(+al|K8U~WHOBGsYCx^rFfgPl-eJkof#Wizs3=vb zFu+xYM4>|1;a>bztpM&%7tNVAug9H%nKrUQ;SE(5!u={{$9>EP51*K?`BvJ={_Kh! zXNkTl(c}+GweV6$B8qv74v< zPKC()lM{zIn<}t#OsCC=R-u5F^V$yBRgAQ?G5HdL*tbh^j^-wjG3?T<7EcmBR0UrB z_~dg&Xnr-pG1Y^mfGs>m(Its(6UFw~uR^nB>!)I7{j=7+;tx85S6ixb=&QJs>at)y z3kgsV*w3m{LZEqn2x%)wzAqh!6*$jS7Ro51bLCWA!PXP`bb*70qGlI=fk{m7*M7O1 z$h05j(A=MGX&c}5g_%nm_RsbA+yjo}dx|a@N+hWUTdKWskbVZ;wr`P<2@#^ZJpHjg zUib-juM^+lKRiID9?(tz$)NW2~a?Wnd41c8H{O}LIIj?^RSr~Olx)LIHE!0U0j zQy+BoC6E{W+%O}~b)j}~%fFBzcRZ}33P6ahXDs#wueU@Zkc0ul3#8aH z0W@|qEOBbaHu~06#Z`t(;TMaQCDS8o$GJ%I=bJtU7R`tV&Wbd}x z?3mY#=+(+XNJq3cnqHM`V;)^hH4_AsNBa$8_x|dHI08F);=n)JCeTrAu!woySj3Dh zs>Z5*x-FA7u~=WR=u<|JXd_XO$UO&sJQ3|OT{VMis+MNHv~ESshRrlvujAQ*?WH_C zG0TO8mVMhd4~U1S&uFi?o6nP8s7!DC3XyH?%9**6C*opxW23oj9S~c`l2(Cj5|eF_ z`Ae)ySGo>BE?Ph*r_Azars+)Syca4~%cWA!q_ci!glLVW$~fPg4uZ%{M9Q-TVCCB95!ReuX7Pf zjb-S4AXcS5f79BDpk6oQzvsu80Y6<*VAxNk$^`r>^lB-xxpNj~-DsJ0E_o$-pj^Jf zOl4^0;GEhz#!wC^eYi(DkfPm1J&_}5PwPt?1yDWQ)JF+XIHkMdtyD2MdJ`jIVa@DO zdY<{n06wD$kiqM)}GK#%+8xQ3#U<Q1>bl{yWfBok9F#b;sZj*2wVt%MsVCznJ^Q_b{QUNy;i8lK_sV(ESbN7dRXsar(5}D>%dwmJ{n`;q zZAP}k*W$^A+E=vh#tPyE5lvYe;tc{B_u?b%Yz7s%O)WoE{W3gML3w0ui`sF=hXg;J z8De5W=~W;J1dCnG!D)>xG6~(-q^0-Oc4PmsAuCJq78S3>Y$!%!SiW1j8O{*<{fTj> z946uk8>P#jP~!g8r;NjB%k~h;bWbCXo)+cvUVVk)xGBU&4h43D7v@3(xWt_az1V3w z;z6wv%^y4Th&G)5s|H9L?dWyS%8tLis+y6qHwoMg9&J^~5C<8&GeS@7qmlS|-fY?i z2NSkb1E+cEmss&+?sJLQ0Jgk$I7@ggB?U?!_Z;~8xn4GWi&S?dH3-HlF|i%2tU4a? zqv(m6H0HjeM|=FHaQU#Wx8k_@EYcwh`1UX)w{&M=gVi(4MU7DtzwZc zaHEU#kC!-lWuGRCEWn{Pn(!PeLsYPOE$ydpV^6@hfr-Me=Mt?WP0{r?A3`ME!*9Kn zd0%-tR5+H~qS9O@8-K@*BKC(=qBThWw;Bx$uo?g-M&uDL^S+w|mY+%tl$X0n+hTg1 zrd2#8qU`V6D|p;_wh3sq{27~9bL81UCHVH9`k$P8}wv+*fuVvy# zge}(K2aK#R&n@_~*V@ty&hOo8DxLx-Ir;>&-(E#fQ*CHdcsi!Zl0m+yna%P|!r82p zEOru7$Owun!H)z*Xpq|yDkzgq;mjoWt`wy*vPrJ@ zut~ZAI@mg#?n{4mvp9&gpz2<{W+3)EXLRCUcpiJ$#(%XVdLxjjxS395{h^7#l-fsFKy11C@O&OClVYGe2G;fJ0R2@DRPs zZNKvI00G8-Te(`?J8M0?G1xxFtltu)P`&*=8MC!@IXIoJs;0jS-OF;=EksqH4-b7r zzW*2w;~i|fuCp84+pv)r_TViX`G1wafBdh1%D;6l>fpchUh%@mNno_x%|mF6D+!{) zrzIHshvd-E4?h49MJqPgE;Ti-JLLd;A{ZoMOOPpR9d9WiKX)CNr=f$v6{&@~)dVp(A|YOrR9a~tUgh?6UA zJ?7Ye{Q=8-KE+>oAKv=yrVuGnmRjSEfd6{xiQA<_MX{#$qS6_1GP&zz%qaj&{0Am8 z5;qqn3M}5X&YRio<4e-SXdgtMoth zN-W(p{`$1wQrs>!Uw(8YqDUAzu|^r?^)o-fj;RH-2`twqoMCN>cf3sC8DE!vH|JKq zCOfDSn~T87fLg+qv$pB*`p2Q}a-JYI-j&^k#z-40zl#t0DB5azM_6;L&v`O^etzP$ zC%yx`nH?~B`a+uyJGj<%N&H$jBeYZlbbai0tf77JH`rfrx#LQ&R`+wU$nG>9D|>qy z^vZ!JOazvr?`GNkx0^z&x14F{&%Y~d#WsuQAJ5#B@W;pxJ5&&f!>6K$veUaOzmQ*E zE6*~l?o)Jlk34C=qx?HUAuqxxm7cn;qtYVF<1uV!5_L$HQTjL&$c>gRMH^qsQd3v z-C&lT#+(@dde6XA?WRjx!8Cf>HL~X?o!xB)u7kCox1aVenSp?fh1!YVJf{yPoX{eF zDS(lY`5I0_Jju_hI-?zOES-e@kxhvDZb!>Dp_?G6Cfe?pMNMLOI;sCWMNE&xXeUK+ zkP49kF6@X+MB33la@O}g*@!8W9oeqN@aAp6j^VO8N3~YAR$>4vDFB1Wz=b@N${=*b z8jDU9)IBeMDvc$1HA$y~8-bz7bMI-T6=4{LI?)Lplb3W^U!Fwk} zbN7!$r_+tCHqJJdL=*^9g0g!k(Ygc| zsj_Y&Hft0Q*I=#tk=rpn_OJY|Zj8yY@mD?+_-2Ar;DiB^C%vjvvrkcb=*pkjzokF^ zoE&!Ey3(lbY5$aWgt}LKp#kKtWaS5E&33%1pbH$jXJIM5L?1P$In$E!lj-xl_v=um z5=+*fHQ&X7;Q<{4N@<;t>`qN~a`!BU>mVSEciADbJ~_r(|9W5Rvve}Lfiv``P3)gJ z*EsA+v}ds8T;yk86-+PNGV^vw>pt#JmFv2$0zffKv75`)XUvaR7hVQKXIBlmyZlgbuQUemLKMdMF`a3jX zWKdTXVQ0n^9wf0aPvcYo-$QG8Iw}BP--2**U=URWl+o_;BYzMv%*&r6smkbW#O??P z!UxB&c4pdL$OEn^lJ%c#km8Bw!D&yd93x=SUAgB(jolE zgz95}IH)6_<)o7py`+(EIMk4vW^=)_wN4&0*!2GZ&Bj zxpVmlmedqserK%$pAXxhduN0FDq`Le&m6_fS}+axYJA1m5Py&>ojuZBRZ|1hJ%|Z0 zY0=xJiw0RJ^fWpFZ$7HxI*AfzIXN3!z*gZ?l_l15L3l6^)9UgyCiZ@#bKG?O){Z63 zBpc;M3=rkqJ+%k2BD7NVS=rPlyx4(wNbr{t@ZSSe%wIZD(T;$sE7iVB&3CBlUCi=8 zZmYe;+)+`7bCqb*Yx8}6oFNDQSjKWHtV zUC%GxWr0D&44Vb9KVv;9Ea{ca`;?djEugc0i22e?a>ZTo{&$DcFn%QUV718kzKUsI zgzeUmvwC}W(j?8)&{c}QRc^)c^fm0}8kSAjw6FpzOog1!O-dfxCxBY-fOh4n_J;SD z|IZeFFYoyU-$4XrGZ622N?_tRCv!_Coa@l)diXSEW`v72&1RA--G_hUgncj{1q zNQKqz1I7U!QK6dX7gRq&LnT|8{8OA+LIvGkbKFLq?6k>if?s`kHRa1w{wlFJb(G&B zDWCrApP4UheEzC$!|r($mw+h1BvzXF;M-BYt2WANe0>8xuhSx`G1ej|Jy^dEQAxKG z`Ock+rKmmrvG4oIZW3L*^*z}77Ct+plAJ11+BZeOLOl3wX=ME1&LnVZ{R*_Owm{uL?Vi}^&C8S$)e9>%)eN(&_Mfu`9uOQ^!CixS z6ggu=msRhlMzx-EhqL{z@R=p;j(Z~l^|su%So_hakb__|jeNvyBf{dCM&QAABxP8@ z2J9~Ts zH6&&BZqwwNq~FT!w^{w>_u_P5ROV<@&Efl_B=qPN`hB{!cuBDv>f zFH2X@KXe4{l{2mqaeYAO513-Lj$PvR!n&LPkM#Pkvk1xz+DihdiN@DS?~x|m3jTjS zQrqdMPKUloJf_tvu{9w>A?FT-n??uyyyZV6lrjRWkaq{fjk)`?3JcLMp@ae%iQCkD z?CF!(rdfDiZ?`gC_9$BAXC==n(4Y;RjMNw_z! ztWN*^7m7UV5wjYtlBj|7={k4`pCyP*`+ohu)WWuvw|vH#q1xW8ivq<;!}@e2uld-& zuP#iSDd|)$q;GQ5Q)FK>20NqP#+HHm*_b2CT(#$yR||77lkoUx^og0&%tjp7AgT;zyXp8q z#W*KI)7z*%_Li|Mz(ns}T4V7FOF%rA&ayM^EI44k+i^@7&G-9Jv)6S2Lh3eA*QzMu z19eRb`IHT8G<9zob#rH{pwM<#PVas1hr)E2jrVaG)`-z&-?JDh|CAHVRC#zE<;umx z&h?iDjKZ}O*#sXY?k>OPY98NMRVeX6fK<7?7Yst4s=iAb$DOAFL;VVNJnuK?CDq$l zvln2^qaVxz{z~nev0ijSe;s)}IdmQ8W#=K+y~~;G`UU?phkqD+DEr%?67a~-s2kSp zMJs8J0ehL--j<}vO#7mS&jFiUt{Z?`Gvx)HO?K}4u1(33(Y4WR$?v<9+5cSx}&BHiYYy;=_5NMo7pfS!K`L)zd3qP${o6vXdHEv=V zd+d7{X1WtPq`Q1gbFT4oluukb={H{f)8x+;wJNGwWb?`L{LWFVM;y=N2)=bcB&2(f zsleWsgh!(&X}a`Tw?az0(LpvWAa>Mlc-m<~XX-MT@Q16>#MSSXIcVQwDp8q)(PE7g z{NDAKj%$hcNPA{+>W3ot9KAK|WB49G)AW$k>mkQ(e$vJjR z$70J&PLLkpfdZt0oweEI^~knc9e@bAdZc4cCjvl*sPrbp*F>e7{w5N_TS$xfVe-@a zT08;mg~~gua8L*;Jbfl5>Vi+elg=XeJnc6E4uP|p!Zuc@Txn?OA!`;LgW3F=Z(u(= zH@i5rq&3eG`Fj|D{>x2E!btr~-bYE^mTd0?Pqz!Z1TSy~F(l7N1|Poh_f{5qG-Q8I z;GcjJW9jHBeFh*F8J1NYXvu2)&|*$9*V4v=Sf+oEFvvMJ|81zyiE{Hk^5*L)BqU3f zc=>IkA77T8T|A_MlrS;`we(!d(q_+i>WX{*^x(b>m4d72KM%a1JGGrm&Jf1^F7&V_XuatY?w9S@ zc*u>d?QIxvBUT9mn%q6y1jl|hncbi62>Xqf`t>tes}Zfv3ayvShI&kL&Kl@KRi8!` zbG!PTcprurix#X+zM*!i!cv4mUq}G*@ltb>#r$B8B1XVTS*(*Jq`IXd`E7z0&EdZ& zyEZTA$h?miWX(i9Q1XYd$$)#~SeaKmMEiwy#Z5qj6>gouIXvA#X8t%2v8XyKpz=Uq z>H0RMjxA&4L%NvGilj1cDuG9pm_Xd*)P+wiW1muK)RMGgFu&WXDvXfKwO^_t3Vmqr za)V12TinaUuG79{{sM&mY8r9VzFdTSll*ADKfWP&h=t=;1vgC#%HUKy7bjBwM6_DD zZ1vJ(_wz<~>4g+yG^1*~9%M;lV+!P!IyXt(aOkR`frn$Uam0H;c%(3B!86HeCFWXl?{1odCEn^r zy~e+t$vxuw5-9pG%C90ihs%Hxkwv(NsAqhfK(#Qm{7K!@<#uTKaI z)Ur3{{tDwfv^E>=_T10`Gbd#!F(LqwUk=CDoPifVT<*JNsm*P>J@AuCIU|VVCYm<% zWyqFp%<>>UsE&TMu^xOJf4TAg@vy9Pfa=AfErXv?Z1pk{#;~jx@o2|G7VoX^y^8IK z@*h6n=c3c)u=MhFJBHc>mk&Bpsj1m}7Q?^Y6rZLk63ZUgjVBp(chRRZe~4zgRA)KL zN5TQAOQYU0LmDYea}hD3CGS(%B4`8ZA&xmhvJ@QlHa&v{@E zzv~N{$Vx+c-o*2XyhA#Qt?9^6b)U@A%Zgvwl-DDE1%P+3MKFN z8i8cfYL8cY&JC~kTIf4t@vElAnDE4y2!?iJQuiHA#q13%FtGWhyqPIg>#fYKErW4KL6#suZqbL|Kho4i(izbGd#!RX}nLt(SW|B;V_GM+R75DUr2V| zr(|U@>DV=Yr2IITFu>i8L>oq6Sz;9ryD7Vbs-#|E%)}a;n5FlS&kI~@%xPMij}O{! zQbfFX2?%FB1w|E)j@}D8&4T<^+!(vkvjlTe56l=icYP#=c3D4di2(GRqfK|4ye7}{ zfZdcrEV1AHIlbo>Tk*|hGGNS9zH2-7x^dK|x) zDmYZwjf-IkW~j&C{U!F;Pjnvp8XQX&X%B*c{p;Ud`AUo*9u5%}#5*f}`J{_A!nl;h zGt*0!Y4(=yPW%gwt>9Jo_+|D}hoimfEA%QfWhAVhc5DmBr9~+~vJbIZm^Iup$k6Ye zSd2@tV#JzXJ7=w#Y{W0@X$sV&THB$|h9}W$-eZKXfSZa)gFD+RkU-!$+*H1DkEsl4 z*nnEDr?_VPJoe8-CoEbh6OP8*{mBN4l|91G3X^coNH>HR_w6Q+yXO_6sl0E8EZx1V z5F5^uZ!Y;{G6MWGlX^uImY)rMO@=u%eHvcRDECwyrmK`vI@)IFw=(AekX5l(`dLty z`>7v!ySMo|YtIuRBrXRKLjc7J*LVoRVh@T~UA8P|z0OL0U}n&a}NsAERjouul;0r2>0mD|S*|LL38J9c_H`M5JT zcJ^dR_gbmO@9YJik_U9#?a!p;`oHe5bwj&}xo1|r#B;J1;X26dpt(NA48)F_bG9(urwJhovtX>C93!AR@K9?%_8V^1AvTCcSjMN1T` z4&Lqm^1awm#ZxhkVwJFF6#1$y>Op0=M|G&lby>6O1j75sF%A$Y&&x={&LU%Ba=zC!B4^&3CaFi}l_1ZK zW8^Cr_P%r`1_$Iq;d}{;7&?|c2+T}dIErl`Me9z7&lJyoa!oFo>iT#`YAR`+UFn2~ z-SIZKOy7B`9jCzEZVQTS=VJ5KpHnkzNBMu{Q18kpv<)FD0S8gdq-RIUWvvjM8xl`I z-Z3~$z!xfsJ%keJ?iO0Iz?~1`&xG9B>^z(+akFRLls#|zCc@Q#%<_pV$lmHk#GE_1 z{imEs>Q;2OaDzMQPzzdh6X447sjvUN8K%ivgu#Wee$RoBC@(7exY}g%80(FvEFRlA zp>n-roMa5N`K-x1*u5SzMO#P2(3!y41~WteLBZ^qFaCJfXSb8C#HR13fah$~(k`;# zQ^CK5ng+8tk8n>2H@e+jX_mVUC^085lI78cgE#h!{vEc)!PceN>{MDPu zb5kt=&jCc_<0#A&y|2LHS>P^2_tjUJmIG?fgONp*Dm%nS|Fx<-gk&sn6Z45HdkshXgq?y6S&_4HheIxBD)8SQ$}%&Xyc@Wzx`tnx|Al zW|;hZE_jSa*a943Eg0K+tr|{wg&EW<1h!85hkgRSX2#N>l`B>0; zd@(-2gQv(QQ*}K&sXMHMKH6f#sRzs4#hksUawvN$`;Wg-6m2c&*SV3_JQ_(nD1vTh z?@-j}M*o$Xt2P+{%6k}UU&BjM>hMSR29uu&CI)duMwHK!KSu|Npte66YGZK^vHmCg zfX^-rfX6rDv}Sk93i3cH_lS~ZW2Fh}WNO9+iEu@S9=JZMdGsk> zl8)LRI;N&eVC14QzhJM5>6Q&5QT zwU`wq$XNBZOO#Vmx6O*RQACk~j@b6{7~GotACGfjy^F&ZGPu^jM#IYYNbnfKitn{37pDi%9C z(h0uKpZM6=Qc(RtGQPg$09%~@Km-{ZAFG-~BV@2(Lw_=g2zfzEocYQSfx)baolT8?hFzU|bp}|gN z-4X6>o-*2>!iRbOy_?ARL&?EBF=)d1)hFD%wh+AoLEecI!`Ok-MakqEX-V|K#Gsp< z{R4p3%Yyis+um}oS_dW9X`Z(*HQoPYP+cFx*cz-Te6gnzbo54sL}#aev$yL$qev!h*_qj%tJ=|o$`pS&k+ue8@KyOf z^SZ8Jc!}5mCj$68TVGw!0e8M*C-a)1J5_&Jv2?6O4dMEVIOLF6T zJnuQ*_kQ=^{dF@2V{ej?y;s(rYpuEFe4b~rG)cMP*lU$)BxsAbD0Nmil2v=fXcU?@ zGrFPzWp3(ujy}3w6Vc&(+d1vAG@OJ<9vz*z?R0P6QmA7J;dg`Ca{c+vecJM}Q_+gUg9abv#4mNGnx-Q|IZq9K zSP!ZZobx9QYHfwAIz~BNxg5~(@R}%UV?k^ z>Ov`57C{5QQ#?{AuTFzdL?%bcSiHl0$JHfEawx)I0|x)_I30dynsJZ;CC|NtRjKei zagjpLZw6mnm8{GEq7E?w?O7>Gy4M*@ zr1^d1-?vR@s`qVOPxy$`29r@Kp+wyF%F!kmXew=sfZ!CIQBV-rL~8e{c`$sMvSZ5&5pATRuY>Bz#(ZLPCaIe3nG z2c6|23Aj!@*H?y5DmuUGu9@l~+>N%0;&&WWCZlnV03saxcPsjWffINRvw{UiS(SUn zrtl@@bT5{;EP3XtTSooM9Y*7?0>>CLtNxZ|83$`(>lgesczi4DPQMMLc+K_}|z6+(*un29`41BB$$-_VLt5Q}+zWL`4BRZ9AHU zj`Zx>Up0nv^acasXhmpjxk|pB(LWl~b7v5g!AlE_S#&zd?y7JkczTWDy+mnnh=dy>*1e_<7EHM{~y7vwu%f)S9bN!rannR|nU!bp7uPwae&J zr?FsoSxXe|1l+aGkwfotK~}Q7(ZFL5I61oZ1n#!j5<$-!F@+M{`RHbr2?{}@?<@UF z{`1>E8<0y_^Lui}84<87dBdNR#Dp)6b0g2Q?+1889CBlT^#(|XhKYO3H>q)xFFK_k z&dfG3vOV)G(a8wmg0ym_vs<4SO+28vFQ9IksoN4bpH=dR(ur2&bE0M`6Xh6V*lv0Q zCYm_($!cFB>#C(-ZeUB2L^LfsiO+;@^*vGzeWNbB(UI^?c$ zn;T|P?kU96q0pU4c;1akb5^eETzwz$cX$10SN?n0{B;te$ifE4pA#z0yQWRkO(4`L zlmB^p*7vyK2W&QQhXF&Bj+QrZzEBiRHMhPGp94O6+9CF`dv$UflizmZ0}-)jV2~eM ze@&D|5bNVxC+unJjI`j%zvaT4&tw^DR;HU=bNCH>A_eK~2=!fG~57?ThMiX+>pQG@#NK%18EA5+tb`41h8d@xd4uJlfDLvttIy_>SgiVn*~0_HPMfz zQ@$ow<)<2Pdml-2i7bD!%sw_G?lLu zkkRdpcO!m%aGg$MJSI*%%C=aMn4|1{U+N(sm}T_xHa|(d6e?exne`TTgtc0-nBlBa zX}L${u|G~dbw}!Z){<;JG_b`R^!gUGZ-G?%R>sq{U!k<|YW=K^zx4ZP78#0(D@!K^ z)FAJ`)GwxXXS8JRR^&i8%kgq@Tl#OiZI~vjfBLsY|8?RL=Be3~S<9t=d^YBH>3;Ib ztqedfQ^qWqrK56wr|ZaWYnk2Eb~>=};Na2P?E{CyK92H&9jT_kZ^=)`?-c~k0Vlt2 zuBw7Rc!(nrbfU?)mm!qH5bTRp=7y(*Yk3<>?zVU~t+mNzp_3!6*2||_=V9wXzBN>B zY+~ZqQ84nnpd%3i>SlfVt7Ft75 zu+6uT3IJUeVXS@IpNSrf8>avUjn)xeVF+>G8|b=DV!Yl{_y2el=i;MNXuaf{A2K zJJ5oc!K|`(KxJrT7Mm8vW1O7fykPV^`qF=`e~Ixk?yKIQ<3&>o(*!KjD_01UB8HXF zrTke$Y5U90zgwb>h9Q&vyYYXHF@)Scw@$SXv3s6Qt0f=``s$}oSll_czs^j3U3zrK z{_jC*pm6v9_;Qc?vxHm6bEb_j@@e#hjM%zx0>Z7HsM}bIR_|Kx`}osh2;#~feNi9( z&xO?gd7%a@dUOyd+-8E?aLQRozLcLc1|2xE8scdDpy;kNZGvMj#g|qFZd|dZH+-$I z>?${Pz8<15Hd%7Kp58lzd!H)XIF=~R{mEPp-;)aTdby)%3=&9 z^LFWJJj<4gtfIiIfVcBt;0QEyixs?nc!s*S{yYmYcurblV6os0x5voz+1G_X`Y+p$ z;RxY#_}eD`=MDb)FFpS~bBcZzYGO&!^_k9+<>i0ug;_&dtf9~||Bytk0tuD*`(?E&Y;p|l=Y zN^C+b#9$+;Wo*LvUb*LWg_mLp zpAnMtH&XBL^dC+(_z3*L^Z~{lEDY1zrZk#v*xXDp{Qi`4VM-s;? z>SDgTz9dfrtNB%4`UpiC*1lzfKD^y_9>(&2sZ3C6M{o2w-tJ5kSy_27^`&h>ao~WJ z(z2}42xiSi`Y*$cxvTt-XPfj&oi4i-tgE>G<2b)4T&MgQ#E^_)B!HrvD1QLh4n&S; zz@i%`)ykf?`5h6-PaSl+{}Nl{d)LOumC0KNFYU4(Z3_fOa11#|H4CT35fo0Bb&f?? zno-Wx<$}7UP zTjJS15leb4C6DQxJh z{2Gsj8qT~*I-4l{l>4esC2p#2MEX?Zw#_q=b=E*TbN2r8R}JYC?U1hDq~>nuoF1;Q z2G=(A_KwDCl*;Z^+q>c0#l>Xz&~VOR?I*&>M{RViq@Wp*ZcE>Rk0Fi zd6^F7;3f_2MH)5odtOfSK!9WEtOl>oykn95Sy-P%cB8h@Va;uQ9z_Bzjo*GN`DwlT zCa%x($r>-^h8@_u{;ebTO;-8BKJw1QzFlBL0BsjTYb1xbCE%%sdwxjEl(XJp&Ufl1 zynLWjX z%cx{ihnR~$Jd}17m<~~EKbsS_1J!bjwr-r9>K{8_&_u24urV)gmKhU&{R<>spV798 z$UfNX-hir@f>pkZSx(UmJHVfh?OPg_gqn$=@#2q1*%yTaBPUPGHPd8k#41kTEv#9X z`k#H#4Lg*7B92=t058Ow1M$g;UbS^_o|Zy>=-t;j+M%c_MTiKbk>5!iZD24IzjDW; zm?(neXJN6`RU9F4d)&}K&THoOw`Xm$30r6^``TP=GK!_K%wu!UK>7fjJ$GulTGX|s zC(q5)D^&9P)_QT(Ph6G*Yh=j@-q&9$`<62b8417r~GLh!T zI3QUa64^nYes(nJD*?o2L-nQgBvehMNNYM)CI;$9n&#=wpJPfoG$z`XHDq1`c58@Q zzNKC@>hu(OVH(_7k9FRX2Q;^ZOsewa>irh(pf!Z#n|Zp-L^EfARj|KyM#S`Zs`}sg z7p6Rz3#eqgH$!DKA7h;{a;TZkyU=IPjA>41K72y8b1o>2I#9Hh^kcg=@ORf;=kf`3 z=!u?;33of2Voc$c47tKaY^d2E%DeeU2L|2KmamBf7rq@bZjN&Jz)^5{zM@>`9)fV` zLGMDceU06GJAh7Q+LmWxJR^QTBbu=rtTv;-?z(iPc$@Z{3Vunyynks)UDZ@%!{syG zkaYU2b7m`6_T}IO#X{7|rmWJz>`bXN|0dbH!K7smvRFREmXIEWj#&H z{h0n~`)<`bb5*v7qR@?*(2R7(5QlViaMyR_&t^wC9K*-M%=;$ipEGw+0u$_gmm%0@ z6s~T!PC}vOkI#t%=!TmSWOHp*N(OS4{(4=L3oRmza}jyUxmwBp1L%gq(d1-59g2K~ z?c_R*h@pnf*-$}J^+K_0>4;F}lpOo?L!+f(D%Teus+g`xNCnDA{F;@3lTIR!f9D<= zgUoV>~`JqJXq;3VFpcF_Y7;q^{OJL0U%Vle`q$Wv| zea6lg#Z)ADjm_hi0^!ZS7za~Z&RlF8=|MfdD7a7eezmOSHSa#KlSowzd2do=*7!Y( zZ`G+|38J{S``zAc3nd!;I)9y*Thf+kdCr^x&l`#=uR?W7YEST{i*`cYy$~ z^X`%}1ISZ=jGvw?drqx*X(=rvN^{$@QXfp!`%YcXzgR?!!{_@LkdT#5C3H!ma zgzxG4m%?%rHI$7~+J8K>H{H4ah(v_1{oJUDki^m`>y2*{%&%yUQde8Q6G%NzbfZAy zT;+&0m!%UCi97BKB7FQ5{Y09>CzlKDi)pt!eRfl*ZcpG?;lE^R_acGB7GDhAD1s(@Whs?+L+oav?kXmT z;zcs_rZY#cdCz;XOym z)cnHV5vybVI^Fw>HrqVs$o%9bJcwu%H8b#aUZ}o5G&iP4qH4jmTL+=DfLuKI%*}a- zuzE<(O3&86A(u3w#mwWC?mCFRObYHzY*I)hzkD-$Udo-`9rhl;!p)XGYMFwMW(Ye61NW2tg4J=x@5t zG@tlm^`_@}TgAiI?P+HF3t@$>Xg}fAv280(J!UPqxw<-;T9tTj z74o`$4>tslq7tov(*1E>_4@|WFkWU3|Ej!F%0I)wYzm8?$~(6b61(UySoA@7Y)Fir zykhu|=et(5wnGs2@9o8(V|?BF#Rrv0kM>A;Z`@xEML?Ve@9Lk92(3ox71abdMzo%Q zmsbci4t`rV$SmU#W1WiQuk~rJoFrU*)q={o7B|GKXhG44e|cl& zsdjnRb?_lu_`LLTw~h$UUmryx_gS6z<|BlDGGT|_>bRQ-{Y#?rL;VCgc&3mcxK^m0^&{%iII^9+b538b@XgMG%x)VtHTjn67)_?gtC?rlBkTPykUmLA zjy>A)Z=6y&8EMC@mZ4=Roa4HX9*Ji@*NCfnxoXS`3%+ZF09-=_MtwHh_4Dj3U&TV$ zsaR>O{Y4^fBj#7KbADPLgB<&Q`rFinQNRcwbnbHd^i0CMOt@ZcrdS9wFasSt(iAb~ zMuJFR0*(-XeUX^Zk1nZ`AeDoLg2dxWAL zItAv-bMY3`UA6Ku_A8ZtTLTU!XXXNY;Yt+YIHZE2Gnc83ex zNoTq0GLH=yIA46_yuzc%v3*TS^`T4Z#`5Q$z^Thsg+k_dLw|HA)-54!zgE52zc&?w z3IAMa>&Hrr&+fxfI1UYIpS7m+<-Zoo0d0_uclOfs5T&(lIGD346frA2OM+r@=hSS0NWO{O;# z?XJh{sXFk*nN}~l1%h|sjmwV#FYNs@qffeZ5*$T7^iO(idyeo4{M>@*DjjRV4>cYp z*d#VYnt+ z&v<=?mNu{_<$uQ|nPgo0X^-Zra=k`Npa$>ptk>dz3BSAZ^|`zgedw0l*4F;bCJ~&% zHCqvnk!fUa{J@U$AScbD!cV!1#C+2o8%6D&7Q+HIs3#fuIk@{fuxN3Z>*CJnT1yoJ zwtuz1_E@{OG>s=4>HcXn%R8MVnAxfnjT2gV1>d}bpGF7w8|XPm{BS1+)xJxQu^-_* zGPL3wsNr6hRRDe~5^qbb7xYET@{gD9GNb;FUG1GjFx>UY+1%1>YV*vw!|r!_7r`Bc z`TdE~;BKw;(abw&C4z$BBuT21q}3Y$z3+^TG6chx8aCa3ub}yBYHhYkpz?aO>h%*o zWvsTni=IBZw|Gf|MD0MXBO9BO%$YPRdhh;v$p;GbXjdmn*71??kDu`qbaMntFCkxv zf~P=ZCNvcIvjVU!P_gLzhhY(xkWwe$hVCaZsmXW69Ph`3gelQri6z|Bc{`#v;$6IO z6AXt(Cq}j-d4)#6S*)*$>S_eyJVj(e`+Vm+K7Kq_wY_&&+a2w2zVUT>bt+hS>AfWTbl1^w)cJa)yJ}9Q?cjZg9Q3<+RVnuR579U;*sgv@SwJEu zaOU)pR~E09!xz7syGxBk7t&MhTgJ6PhAO(es=ke#)8#wFy{LueA6hwL!Ww+uzX%CooUbAW#(90BbG75s zKemC<@S>@cjZW<2FNuSdKN*vc{OO{bliOP1(8A{=MP4d1LoZ}iQzvVP+*YbvTE)Es zqj>b6fTz~69PasC)MDwf4;oHjB&yiHrsQb{Ff9)!q$ zMW4k#0|%5N9)r@Bqq^S!ig|9$9b!5;H-ZJSk3|pHo`7?6V)frxLZKU4EvD;cwue8T zyP|Yueku64t$#Q}ozr4Sgx>GDx5xP@tp0FXj01w?j8r z>6TyiwZ8gcv0AQ@E;f9;{?)NkmRMggPuVx5G-xeXbxg-+M13)3lHljee{C7+l@NOS z&xD&pr-{8Q%yj%1Y|Q;PMEO7S?=tPEDla9&%~Sr2kS7*-?aGvPpqV$r62E~hm#h|S z{VDPlx4CQr>QcxXgz7K<>kRBr|K;}Y#&53oJbu50j!ysg%SBeCr!Ew-n5Me>12J7^ zZMP>SVH%azAD?n|j5-R86@E)#!OBV_sVP}6@|ah>`T>@D^cYIQaBHopl+;KMJ?o7P zY(b2wQb{pFwOoA}U*PRXsdJz%SdI{w!x&2)K<=?U!#tq-)9F~x(}@<}cuY?(-o2V@ z(Id1>iGIq|E!j?0Y`DGS7wXIHeXZl&&uy8eawCtnmeiaS-pFv zltl6C8~Cj>e9+V#31h!3+V``ZnUpd@*F$#M+!pCTbIlfTV7Y~f*BdjW9uvYQjX~Mk zhTivagBQtz=}b+Of4VE`{E@ia#HQnh_3pH`&-USmD&XC*+_Hb8I~ri<)UO*ef*plC zR&M0Hmovc3Lo?i*Aaqcd54qm67C~qgDIKpMgXcfPgyM|Bd(j53zp$YzmtSs;=@_LP zvqqzsBEB#4@Nb+JMtJ!>lj$cY@*ZzT z2M_z}0JRU6tR$Ws(*=0cKzVx`jjAXOinn%i_9qh!*%bbh9h=VX2Lul>4u2>r!bcQy zG40c+{470gY`BeiEp{!Db#jhrY~s9iGJZB1-1;weTI0h<pLS-3raXJ2NTaFmU~X6ZrGUgv*43GhM|@&c7<8zAm&YHwD*z_2e$4N zNmRS={^QB@1-qg_3V;oBzJ0K^)s+@ukAr8*Z=F17+g*V44We9n=4Ozh=VxT@+j0BX zcHSB9H&t}yN&7d)xA{3QobGfPdnd>aN&y^KFp*bqp5`L2Uw!46kvB9;>)ZXtpxf@> zx|-+$^06OyS_VQHOIg|2ro_bmD@sm6(y%+eTsR7KF;O5f%<|r-H;SOQ<^g@ox6uc% zs0Zz;ZaQ(2w=*ubCbAdmJLKNQFn0Y#({(QYMBnFp^)>HG=#D@&)Ps=i3@u;LTX-KG zS?2IzqI7k8B`t&l$ty4iB$cXwvZL@`Jn0`3e)p@{e?1#B69QQ%U38k}JMk@&;Ona- zvd*%9WWD{N{gW69pZ7dhU8EsI@_;I)OEcsY!icZ)cxUJ<2|_?vvneLc#t3>})n6e5 z81!W&{yq~~@k8PHr6&Q;S05p2lcaPwRtM39GrWQ!LgM)6XgWh@gr=OQ$2yV!P3g~V zF`oE=o6@fPa*p12Re2OEqu$lOZ@95J6FL#vT;7E_Z-5-k<2m*0Z+4SzTejmjH1wVb zI&3&1PQKi8~WXY?19tnS0{EiBAd8_E9VkWrI%BoYhrq|UFc)PMgm zu6ud0KOv`4xIL&WYykD+-xYQGllECNZ79MPKF{6Tj1nzPK9gm(L_KHF_|51Amxl5p zI^IZagFmWXm6&XNiji9v5cabK zbuSGf$JB;<&#Q_Uk)6kjk{(Mwo0U%a{l_R6NepP|SZZytp8m$LIh5y(JOPSH zO?1fEll=|a)AKe;Dx^|xpc>A2yo%;E?yrf$ss5TmwL@XMd<${wvD*^2*6>3_MS`=p zWg_tHAp6k(*|WtKTdhk-9W35bQtNeoMy)I2pY@ z7RXV7>3~XpITteA6{s)%ZoZ4^eEM_cu8JVQX(L~-#1W0(HqFa$g`Z#uqm^&Fu#Fyl zSbVGCHdhO@Kw^vrSrKUl?S5I`Ya|lznZb9d;eyL69SBdi957oVpNFDiy871)nVWC1 z7>Uz8IzDQrx$%PmtWcBd&w_MPx*y5%r-0v(qzoA5s#HUx=Iz;xr|1fSZp(GpHxBz| z?hS>s(u>YU&GIv-Rfm3W@;t@L-(_?{=OzI6C?4&6Q?v zAoj`^d0h-ej9DGAziCaRmK?N&B-U^_7RMLQE@Tn$dG*+-WQoe$HVTinotwe@v?a+_wMD=&vK<1Ny|K z%%Dt6KCx~1vtaL19$t#zHp!0RMESP|BqT8$%7D(hMp;MP&P`SQizR3jW8%vjTXm|y z(|N@Gex9UH#jJ>%H60?oeRy+f=wW%E6IOWNcx+sASjzj2DpUD~DT!yYM*bPpXFDRe zcRV*3m%ebN1tpAmnGLf(yo+OV#lI1E=U$QHQ|`bao{i(1oI7^XnQgFae@i(Pn%-}T z!Cb5VVvyK6W6pb?&*6*dhKCozc1Tu@{Jeg?Gtx~m+E{l*Thqr73$ivSm&WM5G>dQ%eN9;qGD^w59kYSS8a+H*#B)9C;NC{Qfv~hco zEEE(lK2z<)Fjaj$Gt12B%F7ixGg~S6R;6$TW;4;bCx%8jn(}{|gT2~3axy3W9aPAi z4?~@r`dv`i@?tnd53zn*S8_*kjCNaYdYtT)*`-$u>e(Il9u#v`*WpIZ?)`Gy^OUB@~ULY(Utyk}Zov+?sgSR`mBOeZNd}#6-Lti66Y?X=;b}%R{G>dZV8*C!t z>z}~%>9+v8_vA}db)TLS!(g85BjGY|e@jNG5edovz}t#uI2;b)4uKS!x+s@?=k|MX?pNp(n*y9`RJ( za(=I_qD7wUZRufvw*Fmyqc5AeomjTC7e`}5wJIG!cj~m44g6{&{j$okyRK7$Ex(qo z^-q81ZeAF=@;#8xyoU_5(w^%au2e3$PWiC7LSAhULx_xg&fezOsPU($~H%1$;h;cAlUbk7EE9$D#d=yf ze`T_AeU&uz*>7bGfoqQi1CC|bF`S5^#r=o^#^1AfdLA*Qix>{!eRSzol#Cf}12jXn z`7O5=a+y$3RQkJUi7%m2EKkH~()LK6AR9k8`C>!g9I)38 zo0+ilf%1n1;>pMD+sw7S_NLLfqr16@wU(sLb8(!JlDYK*xk#iEpnnc7a>6#g!&mTP zWFscpXfuVn*Z|KS>&7c;JO?MSb%1NZh~G^V+P+(I&4JWTgmK*DXGOP(M05Bmh%|xQ&9_els@7V3DOp# zbO$9X-{d^-ex3X>%^~yR;!3@|cGQH~6?(VRo5`g(0>?HXMaN-Jh34 z>vV^!3Yx_ab0+3-)Zn;_Nl%2-YpiH(rxW07I$SK{Q3fver#2n((`A5XqbO26&Xj-@BIB zkV}5!M$Fw!|^N4krm(nlbEp!QB`0=N=D^ z{qcp95t3PrP=`nCfV+a&5$&T7t*=yPWzX7g#oDu3fZD@O^wnOn1uAj<;`moL$~10{ zVLrh8mkc!pzgijn4;+`4lL^TK@C`OquT@E{`9*P5jW_SG=jDB}9vMKY;?YQ{d5!@5 z8G2e{%yzRI*d(0@w-i3<(&8(yb6UlHQL{ul)H4T1PV@l-)%0zjN6Gon46L}&Owd=Z zPrmq0Q6(ouN=czi%(ffM>l;X%wEZ@aO?drG1~~S58fEv= z!OonLNoLeSHshz}@3!`Cs@eN2MgYO@CbCdB`FP?X6Pw@V(27{z;coXGRT=JD_e8yv zlNyt75ZIgllqGlhE4S=@u7*3)lGI<=xH3?(E)BF7lB71~O*J=sEkSO;4YRs(B92{b z&9FZAQty2#X}(UADM99fMpjGhKEbG@n|_$mGHexbQWxsb7SQYPQdkhN{QI7j`#%%r zt!9TptU3tIqzqtu^( zH+fdT5aVBc2Y|e0J{n=+B-Oiv>>2AapFAz_0)#WLAV#=_KqOtw z2@{^y79#eX;b^g9I**7sjgE!n%)X&aY_EL1Z9eNam*a~u>#Nk2tn?Ckhbw7a)@F<4 z82-u7IDU7hyGtP$a#AQ9`Dx1tfsUD~g>Gbh7YZUDA2}!i4xbnS2enNQ_%Y@!f zL#bD93X>|EGv-9XrBL&#p_TytUlaPIxo^6QqlixZSrn;F=HN^LqX`mGrV6e+`j3d1#4+vvTYtrz8K!X7mJ8sA+;K3iw$O) zTQRE*8+aSFy#DyTjvrlt^w$KA&=J^WEB8}0FIa82vf+LC;5*hx!taqDMxTn(*OZm$ zc%#pE|5K4h%2|quPc{p3&#l7ZwI#7qj9)(#Ln}AbDp<`eM{iZxN<~thkUp`N_Pk>qpxX3&A(cuYpru3ALF|aXi|Xpb!=<;- zz67NM^&4Epb?18(q95~^%sPbQ?+{-9n&zACD5a4SRG2k+9(?(|B^fQK5@|Txw$I~x z;8eb0kDJ!`Znlw7(4>)ny$7Oil*Lk zKCQOEoQ6M+?%` zR?|@n!>=kiitHO8=L#p=aZ|rmK^Tza>T`J`EsH;~Hv<7Q(G#+5+`NrH>-&B0vpRZ? zZ(d;eI6?u!TY_hcdpd67O{%Ru1G_-HfBUV20OhSkI4^wZq0d#A6wIKF8!yiPL)QjR z$)`bi1x`hGs+Ln+`Qv?z{BOz_XuWrux*wYX_*(wzXd<#{N~(f^5ypvR=3R7>5h`XH`lfu~@VZpL^P(Ni>%*z#Np&>W zD52;)N%`GyLA~cLT0tZw)^k6HWVsVZV`3_4PJLUBj<`!WWJg%nHKJ?L#k@|}GoLeA zfz`6!NZqrwrY|1nYx|rd|Bd?1DrILABE%^l$ei>`SE?6(HmCmq#jxAmSsbVHyfH;r zue#8>N**QuB%G-^CuOJYaluOMO*D7=Iq{C9GZP|yuIKSUkABI=S#N zv*~t!3{G7V85Na|kT47P_64VjIF-HIQW@yM39IR(E@Aj%;k4=#H~A#5S+Q3~Yvv89 z^4xe+rNUFibTepWN={cHQ5LHpJ@4VH6cvEAN8s4-X@+tX5Uok_9z;zmRKe1VKjd3b zEO>ZK6&ed!z^xlCx(L%z+7zj_#*;7U?snQYXQghNYvNA_N4=v1Wv<2-d#(BQqY?1Y zdJMxB;#{uog^BfRF6mMTb8G3bhD<(D&66nYk5`C|?|AxDEk0An{V(B8e|bHs*-MX4 zij3dEhu zSQx_fSHzFu%is#BDei90zt#~vOZD0c!D>l@K^3ft!b>C>R*`=1LlV(wnQKpe5~rrX z4b7{jzTzct5G&`XK#E{w{7%x%5?R}2oUCQogMnDXVE^jm?U=qm` z7q}R08J8Kk(%f|_Pqf4CAL)yp_cFRQ9x(XA$7_kWYxEtDR--5&eNwdw89kM=c1{i6YkmSPgN3w?UbH?AVm?~3ywB@tmFL{D{Y<`Q zH|5{@!N#Ox>(^fCHQGfz-$o{s(1J+gO%@|cwrD+83 zVhVt1eZ16_`e+D0vl?pF`7A13Wdl4@K4y9Gq&5vPa~y@>yU%8xDEjGP-^B-V6piRd zsShfHbV47cm@F$-@bXK~Hyr@dX1>F52fOZ>!GtL#87ACggs9%fgCnkO{{6eoO!!_s z+x+rTkJeJAQs_Y?BQK`*z!6?i$Stayl>V}HFqP2H!c3zJHdQJ9bg9*iD#-~xXX~AE zpghANMxXiMSr&r$cN)5(bA9?4^lA9gIq7mGVc5-4TW2Xv++zXF>zc2OJ21c*CQmCh zvHE#d19)!L{vw3>Da+s#(hz_vZ|v>v=Y~DqY>?Jy;z_(^#`#>5&E^F&2Z%iyCENl zw9MPXMw=Jh;e<*m_)`3CD^*};X57MJN?ryoA20WMb6{2q0SDFmVz=v_(7Cg!lSnwR zPCh!yMDjFa9y$q7D^+(#1G$IG%!wd?V9|1N%$7G-oa6=Fkq36UwjQ$fU*0?wu!E|i zr77!nU%ukd$D@#u6XM@E$XQ>aD*CHdSgMVx-TKjmfs?D*NC6&|=9E~>gYzfC6KyfJ zMFLOEk&nL-bts-4B4u)C@c*nu>J+~ReDAdsW_x)L#O(}jj#d=nIP?A49-XA4YJ4y9%bU{rAqV8iFB6;E zY-+q6s!BoV#yUm3-~9IL^z5kU-L~G60qbH}3IxGOHt%#{Ff1Cy3Cj4u$n5k})cXk6 z(Ltmkiz_U@(A#ezb;B=e#*3mP_y?UXQcM-1(_6;|eVP00GVv1Df6JM|n>osu(os28 z`%Q+!44Ln4=Z+)Y7=Om1KLlCGan+^~yDxS+VV6l$_ZjLhnN)66tcB$gbu{yF%4?&P zo{W6eZ)?lHR$lp$Da2nLs`;Y`A%P|fvK}4~fM*<-75U=xkY)BAh8x9?-h-wMi*}2D z+e=cym~K?S$5g>%`6zk`6Wo%^E9da=q`c`%0sFU-7!O5tN+_#D1~+oN7FRX9t^g9LSQR@7J;gPnU@M2ZYwcZjV=f2b8itq)=XsX5VaI2582F7fr_`l0!v;~ zL~nOv$JLS_+exGa)#188m)>X{@G@F8c&(U$hVPoQYG-F6WQ5RzI)>vV5G0Y6{0g1g z^c1P5=)Mfod7aZNDNw-WD$|T#fG#w3j1CQ4(~?{dn+>&;!w#g%?^%M@r(^Cto3ie? z&0biYBYj$eUus6L`0wER$KSzs``3?C|HDwT#dt8XMzPtERC>bSrgpv9!%>^rb}qWG zi*$su%c84cgcvDaA8s1U2@KT}nR&J-2AN%~4!+WDI5#UY&<oEJFH1tQ1--O!8Yw}kR7&5YI?Zua~T<#%eTl`ty<>(Vboj?zY=Xzr+P z%qNMxFR$No{u5Vz0ZlRHYsm)Uu+`2tBYw&GX6`7yBXyKW&nyP8zUFmu67az&mvX(l z%B2{7k5&ni@4>Ps*sY4Jk3_3AdDHRSw^C&C$$m>a4dVU62x63(!cgvQJg-+*e!h{U3V za&3lEvvfqa?&K+i#a|s?Mmz6Qnl!ZeoPr5Eon{wcXX3uJtKXy(G6Q4W`>XDI#-}dS{HB}7MjII?-p>Ov%gqxY#1u)f%HrTtaWSLj&4NF%HhTU7*Q_fqe1>SRuDN;X3Yv?qAQ8J48 z&%M#I$ot-Wu}Mv9S#4%M81I4;C2@KArmY)9d%tUfJ@>M+MDJ*{IEtYBW?j;gDv*4W zD$9kr+>^g^pKEvHNrj`td+f0Ep8M|^TultqZd@Q2BqHvIf4M+rK2&ysnl7~I?qw-X zjohK@i1R<_;aeCg!XHMQ<}5br^x$FSkIf+Fx&5Uo!WpeQ4I#G;=R|GINk8L9VuG=^ z#n9z?KYDuf>f8m2b_Q0Q3?Pp`sE9Zl4`c+Z;)mJ|wo1!a#cMjP8C1pp_ygg>gtzUY6 zyQwzLyX1>n@crrXS8`2IY1ihzJ$KW?Dbm@ZLl55e4F;@2M8F{fVEPGmlIOQa%+#B8{tlGnrQOU;i@ zJO@1q#nd%WD-znGwFcMreUD@baDt%04#99^Ab4v&8}} z%#&mZQ06|Q@c3OETD&&l2-?#yG)KIoViN5jr|ZI588x0nVf?c~gs?rSHCoJqdggbr z=P^wDbgTYaf_K^cV<`HM#4mo=dN%qgMBWi}QoWm*jT?KwuSJnMtTlQzK_+Q_% zh-oDmPOZ0|&9SzZf;QI~fDyZ^fteP5J#)RM!It&m#|>t{pD_>(!VaD5gK|W zX)-CxcO97oZ*ns~@K<+4&m3c+hXLqrpN3_9BT{U%CXsib&e+dBp4=${+WJPw+Y$a+TRvof-!4q3%HBnWjkS3x-JbN@ z!~2Y&L{M@P)X_}KxzDRYcH$bBWOT3!rtxE$3Ci~^=4i1Hjr?|D=vK=<;-b3jh?@bm zi)s8dtWD=2)z)bkhdaFNAl4VZL}PvI1UZPSYZC&@TXF34UkD*?Ok?%X@~2LGLpMO%=s6O-}^Q zx^5J7Kc}JPT(`tsVD;J_6`;dZr_|a5x$E|C&1LT`_v=CUq}nX8+hQ-(MMYF4wai=5 zvf#MCwfmnqq*s~hl3Q{K{H~=3RAW{<;9dqF##+pAUQsbDjVpa|;s_YIUT_<%N1+m@%sfAkq2T`|i1n2=So- z;;^p_RPxZ@6b9Q(&9~BgB_!~}W+PAVyLo%#K!(>Etjq7hZoEh%LMbgL<)glNmyJE-8zvqOo+8IB;rIM7VdH=-&4>1zxbPBk@H`~l(1UO}=S zgvxV7VtHHDf39o)8J$aP5n9Q~5)AQxciJik!*2sUug&2o?WMug)#^|n(LSDo9wKiD z6iFET&o<#(dp3;JNHFDQ3GbWiHtlmNR|t8djR+K+rWY7Jw={Ykm~vR{|L#-QnD6~8 z)cHq~)tt&GF7p+u8!DOYILEkZ;wP;`m$kixC+}uO> zyi0-R>&GG-LkXqhQPx)C#0KU{Ve8n}Zp;ICRg`oyCf(|^{$9QTCHl}#$sXa$4dqU1 zL15Pk>yF+$C)#6*_5*IW=7;OTjW%OkZdNz>IT9G){^dvej91??aL8PaW|wGOjbC|Y z+){4v+{BP#>Gu`8qoa&0lTx|Pe*4<&VlYk$Y#LV{&2&1MKZc2S`3}X5fL^=NSp153 zbjf%J1vkWkadi2v=~;as!P4;Ulm~kZp<32E2=#jFd1eMAWrP>EMT)~YH*3i8BF|jJ z-uTgzx5=v(wf-DD5rky!uNgogHF7>Y;1HpXn5eMC5z+1kWU|lvBj8;(Kqq2I=-KsA zH`ekjq$Ru!+>TQGRxXgN+VfQ9`A4Vgc9+5tmwO4FGy@|XSd{;>_cnZ;!+QP&xr6RN zW@_5-U>#<htH|XscovPXN-VUmmEMlq%I(MfHrA zt=n-aI9TDe$PD{)3%8podOcWe@5;0ST}xWnaEz$O&8IF_vUk_q4*9I?S|47I^ZJA7 z%6XTcJ(gBaq&|>qCr7%#iU#Y3+@jXBZzEtbw)~ZW`umqTogko02#S82p0jdgtc6r{ z^J4_m2JIp8OFZJ6z#n@88e@2piBr6^lR8Y{jg521b!hxfi&xObr6VCZ2CXS9xZ5*M}sqrA#C}{DOKTQ1;%ZFK{bWc3& z=1vlOza$d7clBaa^2%DXJ9uqPsN}9O>L7aSIL%R={k$;<(W(|Rp)CxaS0CR@s$t)0 zf1K-bFSSteNfJCRn^P}5d*6WMV^C*7mBhb0eswJR{KRAgestaw^KKpBL_EAIPMl$brN}ZXHqr~y3h@lH(2#wlQ%j|h}is{Bb;-L zIrnP}<(u`}6EmV2Jf2)>R=n?_2wzqfizvm=Hq(6+ty&z(T1JdDHSuf68VC6C_Fjav zK^0J3EN$z@7*9^Ud9x!o{tEi^bLKzuit$IolIevl%k_11aas3>p zrCpb%%nfKU-)x>llx>+*MBZ4Au4K3bNq&K`D@;h!%_|sQj(WO>`8% zgLbS$PHBMlPY%{K6nZf-f)LG)n2oc*`PF6HpFB4zHbWHD`JofxBHMh-3S>L$$;8}L zW<)xZeQU3-Z$AIrh9f}1UK6c#dbTp`d6RGRMs|zwIUnFXz?|E51x%DsXBCj_`vdUk z&c=!@z8#F?)A!Ssj%R4u(da@rf8dp^geM^T!tTpJDuw1fz!glxllCb%%1BF?rJr}K zVn1g$Ow+1$p|u9u1}WO#5t_XSctiR$AU}poX%N+tI)=G_QwA*dmStRj@aps;oF8e+ zw9^`W(9C?o%}Th{@J1uB2Dn`xgLDym77oGHw8HY$xYv%^xl^n@_PfbxaPQMG$}t{j zL|ECE8D+3M_R?5Y>K}kv2{?+Ln<}#Na=-XMCxeKE`lGKzh;-)@*SB4Q@b)KtDP-QYc3B<>VJQ7z<9-$AHJr zq`Td1tO3dtjXTSW!YS-ZBMZ4IVpyg)+(oWGhp%>In=t&cvy+#5Zg?%X-s<5W3s5aH zV&=_Vs!aTUw`%T(%w?}h8MX#}TxN*s%L&X69XX0n-WZistMf75!y|VG3&-jZ9C|>} z^?oj^z0f$8N`;N~;FuqtNg(WcaD1-)gOo(>jRP)G*I@9M1z@1xel|8PD4CNGMSrhI zh;a<-nQMq^&As>45m(8#D$ms$RvDU)Bna8W*nuQ;WQGam1tEBPB#xk?Fng{CR3 zMpnF1u^{m&@frmW#S##Py|(D&hyDO2v8br`Z@#Eh0g%C;o8MqIKvCJZt|YiXOj zd+8Pv6QB288f|(KbA@mwg6WVuV&wj+!mA*kk=7CI*vn3i)A)1;z>dx3lmCDKFq8eT zHW5PhgU=EROG8uR#AFGhEiZtzy*Woto}v3og7^ks;0=sHmD7-I&Q}E|Yx(Oe=p`vh zT+h519f}thUiyk{81NJx{j=3iUFds|#s*Im^>1bR0!u9^Cxnc63gKMg4Sy7d zul%(s{!8ti5?|VQn9(#Ll9|!0NNtgYwn~2+DdFR`vbfT!+L9rpe+t+yTD2`fWlVY+ z|I|)S4bh6dKHmEl%clrU$?)Fi*k$383dT#6jUOs(jTyz8gm{k^qTpLklMAuuyHC^A z%ubvzk<+foi0b6`*$v)vvlThZU5*2zqiYA&0j$Kd;Yol>gYC_eV0G>y<>DgR))oh+ zo3ICe)x)@_JnQ8y{zI-F8@G9#WfeRTmemPkmPIteRf}%IE33usWy{pLRtH@4$HAF` zY7NEeA#=$^hYO0)3=XA>Pu1ZQh-{OkBKC$3vr3)T1+nG$I_B?u2;lFXfCg}d{8EDM z#!stXGp(&&pnSAPi2XLWr>hO&tfCcHao`F8z^~YczCi~Q84d+sVQECYzH}Bp)PD7MU znjKe?a>+S4)n($zQmOj)qs@Trc1amo^k>li;V(r$2RG;r^apnq2&J5 zd9tsrrB*50?Ze?=L>r?AkIvoC@|3HKdPjZLUDwz8!of;kS^Szj4H!q#tj8>fxz$bY zZkm;?Eh`&jxGrz>VT2xTV(SWqBR*%qt?p8K)O>ahCMeBRz|+{1n9$hng-s|`a-*z2 z=1Co=A~-F`Vk#)Cz)!cCrpkG#44Fy542^w)d2sN7z^J2AaZghoEQr733%9!A&eB{b z)>r~gh%fb5<4z;n`qU35J+BngZ=RmeXvr(Vrt_+gm9K}vf0@RX!AB%g^6`dVJH0uu zd$z$%QC=MNA8vmx85Qbp$lx^f1kRmL3`ahR6ixfI4vPp zs+MaKNB@g7vaq=aDJ2LcsvCeZ7g%#bl-LNYdq+o-$1hQ0-(QhBKol;0!%VqRt-T}JyE&H2>0C#xX;c$$vg2TaxFd8QX z+I8E~8$Lk2(wu0y!#;Pus|k32`0?n7+=GK$rft4~##OT$H)}*=%IdxVsNxY~v9iQw zw8x-CEz`fd0& zu3#z|NoS-+$!Z? zJk!X3g4!pGFeyBqY+Bm1)HQ3es>APFZMJtew&cYo;Vd;)CA74Gq1(Xiw$ADp%sk+I zWz|&Qo2oM(#-mvbUO=CUthguYJNeGfg1~a5+7T|?_z(!17k7Es&l##?G%SH&&hIIl zt>u#X_Y?3I+Kkw|tmgf}hKm!tHTFuMW*Y9K^ln9KNkjW%AIv_uHRrlT#tn3ABpegb z*jlcYLIN{4SE(efRsL5r{9UgVSB=mq>9wpFUibj0j9uw9UN$fl81D1kNN^@kyCNVp zA*RNuGU_dUpv$8QZw=(VMTnd+`=0(8M5&>hHPWP8(al*BG;-&*~9V)wV+gg3aH!=kKf`d8xXY66?|sFtt}9P>tG&fPhgq#OQ3|dVu&ih!N+lL5lR&I zMXkv{`kut=f{(2?X12KPZ!dOK-s63@t;p&Nl@Q0a*b!Ut8UzS+4k}sPt;VWt#i%pY`jpd6v1)c7gr|l zF%F5d$Hc-ki?&VgLg=?8D)3Jwiaz`kb28A=zyAZ+YHATQEd~BTcb}` zaUqQVX04As-7If88#~imcjT;jqbV`-*4T^Kzu=w2j7tq(Z_F{O4T-~B!IbIoQCZ^o znJ=^Q#urEi4v}#cb}Yo?rR$eRqCR((Yt*yxMHveJQeY zk3@obzV>FSBn9dNxt2dRQA`crCVD4h^MphdOn;}lUNNRHHAcTqhV^YJRGUr!RBl5{ zyz3X%->TP{(Y8IkiEK$Hcyzl4VkUWzA@nKoTGv=M;y_6>^RX*D5Gss8g_Afb#thu(45bS--BjvgY z%3%Hk#x^Fx;d{?GpYFKBjTb47K1k1T$+n}sAY|r#e)CEWB^HHHv1E5|fX3DK-31JO zc_gYFMyL|31Y{g^=^9e2hE_&gZ_KyPuL4~RJG;gv9~q`x&)|}+dn!z;IWmQ{Y_@Cl zOBPej)RI!W*JSd}Wq~7h56lkTswh7t=h_+__!GIr+ujoyJ}m{WIFNlvd&VxaS8k|GVZX3A?1W~r;a#o zy(-~-CF5WO=V+%aKk{+`*LM~I?r>r=Zr{qdkqr}G6a1WI?Dp!8JhX3yJctnpQ-bQTGOf_=jN+-%R&4M&&)) z^p6IUdW^!{(a3!jNWTlyccF@~hVG)^13bh}r#;|>0q35MvHUl02_?QIuZ$i5;XN88 zM`)HCQA*FnOHT3FzvXf85el@cw==>(SIe+P3?bTjMwqA@T^v#n^~7mBJYQea6Hxmxg}Vb%-%o^x9fa#z^&&tgz7ka`8x@YWSU^kmmCpRJ-3k>_1hyKM^}{5L7>LW(1-TPK+wiy;VZX3teca zu$j3D17x0(7@mJv7?Z=R-8rx+(vPH#I>PS<$MZM9VJV^YuADfBf!Bmzy)t#x+MU;2Bj%H&yLBkBk+&{L8q~l1ZjGmaItqsU%rR z_7ox$lV-?EW#kD9qpQyzA3QVUzv{Tta`k$IE+&hLe$fVJ_~qrWR^DPr)umP)wW&wb zcyRl7)wgF&xwLwy-mAYI=p-h_pOIu98Vz#ZM$V~CH@YjuZpE&ROE=S}EjGJSsE4~? zJteFOW{`nTj;2Y&?$kZx+O*@|plGcB?)&7D5t=#Hsog&9{W0_o*Z4}3#9|$Jh8&(p z@CDw|V8L*6O}f3y^lKZno>xJ*im?5)=0gXnE%ChdSLh9Hk$OGD+73Tu*z*g$lh#Pw=BN^xjD- zQ~GS-QLL#CZffoZO?g3OS`C zkts}sRJ?j6H#jUd?%zG>jE-99*O2j1AR0eE?FD~zti2sdDa^;XW^P&baGQCKglR$< zKR8#ZUmFV(%=3g8*VVLCU@sozYWSFzo!p#_d7Ui7bNkLGnsj9n^W{HxGYBw9+oxhU zS*oj?aG@a@j~F+iO;qh07}}zOdyB*R)L59#Eh8+AU>`Y}zou8-{9UT}qmn<@Q@}IJ zZ(Pe#cz}Z~EZ0?E#G94+Mb(E}vq?axD4y}=ak1&JdKDMF$@$@|cB3V6FAM z8l2&Jk6_cAJ0`HL#J;TDPjGXa5eqz?8hliG9;Agj`AbZjY!3OcHK^-OOw+W8g+{tf zPsP~~-V#4$%G?2FL^5%BszzDID-Tms1i^`(%mh7`*mQ+l_aGfl$eb0hkM^Z2eFh%! zMDh)hqM6~6x1!pF+DKF~CdR;LOZLdgpDlQ0(=jy(KzO@9G$kl$0jy86BV`!+Mrb*GT`8_*OJuV}bYra?vA3Q?xs^@H?R|1~I}CyI zkjV8f&l}>4AuSGND{zL^n33j@rylI?uem{o@jl(4)Z2Y8iI-*$PSz5~`DSKViqPg# z)xxUc>6|y1xX`{kOU5sTcnCYFc^oSz%e?7)FsApJjeU*}%`4;&9-VKW1;xZqP-b|E ze`Wb2{D6s6R>*m{B@QNcE?%Ufsg30XPIs#CzWXz;%mf$g zh|(|xM}q0^Qs$f<4}Fz99-1A{cVk%-vT|wft4%JD>E|KAb|GwRTjpD-LwG(tvs`GZ zH|PgRd9?k2-WP=W!Qv&!+OJEO;>}&O9xb#ya|xs#`zpPZf4$~jX=9GC_3QN`cf8vP zoa5ZgoV&B3p0&(jwhuw8{P65Fn0}Gv$yw-s+)4j0ONltFD5y`i{#dLin}g7TPxy-J zx*Y8(J`X}Get1M@yUVAOzFOWJhEuXsX=@6ROrnsP_Kx(YOZ7fLYE0*y#nFg$EO{f?)}ZErUHfhJ%c1fEZija*T9H$NwMn1$P`E(* zsl)D^hWWWMx|45eP?+&iP|!p9GX>q$5X{(e0u1d%WtfNIXPEIT>BX4TsSJ#N#AHGE z_lqLtl&3~zJr+C_X2z&dH!cy|e_`wUDVmpWS?$o5wKDQz5e*$O%8$)6TK*xfh;OzG{?ex2sL|$xZdhAVpjB+P>xYDN zki+PuWt&vemhC3xPX7H|`h(t+rp7(t#&sTbvQQ#w$GvoE;vdlox$eV(T=xCVnOw}T!p%-H61bs4wiv zHjHe=dPnZ(xf5*x_iq`r{cYIK#ni%1^Q|hqll))z*lm!k5T*>xdOr;JfbiQxD?Mk= z-vb0 zT7PV`cyv6m@-v=wsWp(GmVltqQ9@>LMR}mVIYZgV(C{9>EAWKy2l&;*>kJ3gSgr zF2GMzO2PQk1#Rstbyl707cNGX=km zpC*ssn)=pIy|CNE&2yC3a^OVRuMP8TJ+?Ux%r~s=7`#w09PJEJHeii7St%E(mOr$d zStuf`bwqL5z5RUQJUL49J8dwAJTo%~mwPi@y@>3b43GhwGWoUpQ(W!v3-c}&gMJPU z*M?rRuHCJkDTS@_WG3?O2Fl!+wT_^mr`sbsd^*;+tR8WQgf2c8VUl#+j~zV{FAWT| zM3i%1jikCTvK7#0j%12GZ9kBk$b;bVcNu;lmBAR-y}k>r(+HkyY@iD3n&+!kYMv4K zORfDqHz-Js2m6yeuJlK}B=6v7huPVnd3QH6N?&99BC~vqy;rIsy-zK@QaMk{p;p4B zhEEcjFHlql4K3HW`|~mYGjz9tw&;#4o*4?)lhS@_QX}}+7)q%3P6hm_O6kc9Pi$|j zEp+$I-_}t5JtCAx1^7qz?NWYs%u7i?FF27v!c}mbgBKBjM7O!f&01|vcR0HYc(eGx z(-li!Eizl$(fDv(;N9TBDKVV8qw$q@-+3E-LWKu}1}vT4-;734wZ1X$@70>Li{ATs zycU3Zb+ynFP|&=0y{7r}vt7!m?2ZWQ9DVGSEJtG9$!>>3dvtq<;7I}}&fntlr{Khe zP)?6@S1Hme`KWuQ$Pn;PBl;q87qYs)zpSYS=|gz#a1xzGa@z7}8?mPI6-}5PbCA27 zK*P@jm0Yw&3Ukwq)*8xHq;1GlarQxbPcF-<9Br8Ykx2msy0=FA`sBou9p#!=`To#vSXix~dk}aRIZ)xAwm$w$KHNlbC-AO!F29V@p zTFmTcc4E@*N!x+Qdc!vi$OYxJSicPeO_>Gd5x}$aZquH3-5A?U)4d;D5=2XNo%qsX z6ax6TQ2XT5-c5}x&Lx?D#a!DX03vrRxC?f|Rj4h`NmsDV)!00sg`rn#5FO9^eM_B1 zn=n8LRbY*#3W}Hj=F8f)DjaLZ?qTSXgtkb0f0?TP zyzkGy8f0AcwxQQ5W~MD`VigkE&LjzD*ivzp7mCy8fk$r!J;rFZ7{5Q=i;yhu2^56> zY))leiCC1Dt8?O)gd80$!OW9cCXd!1a-BoGcBVYpFCLjM7CtlFEsl}~Pb3uTEi+aV zxDHWuL3S$#FRG{J%a^>iIZAAg+M`2nLlb*OMg*tFO2(E+7v%IO4y?-{j~%)oVBE4~ z7-4oL(znn=dH%+)ldz=9KT_(K}OQt zJm13kbU$kbDYTT+5?-<@2CXx(YWGwwTQ+COxz$gNmnQ}L~`QO z@Tb@%20YW{uZZt{IwABlR@91r$sgeZshLag&whu$1efMM4yYJvQ$p$PK{G~kCW3e} z7MYgWoUl$&(F`cFJ*!3SPKTnsOl<~9JnIpf;{BMwe;UoStBNiPE>F}VXOwNSU?1#f zL;*@Rg^(@1`Dk7D38Z_Y-uqvV2T3KIJQdr!*{ z-nF2}&dg$L5gHuJC==uuZjYTh*PG};w#nyZ`f;BF#~lNY7IP&+Njm4a=`{WU%$&qS zufZWnyxc~+uec5Qq8KQ>ls3>C6`iO^vYwFLzZcESct;?Sh-Jdm1`D)g_VFIKJ&)qj zPifM~!E@^g(b;A~Kxb#^JMS5BWfSb4y!D2-8p{_r71)YGnIZblf^7twq;n*s0bw;I zuj5a=^w@Q<8%D@K%69SVWt z-(`73qImQWP7b&B|u?{%Q)UxV*KIsF`IaEU2`+noWE_Kwaw+2P}y>G`@>b;0=rctXvpKf3xN(k z!YbWGT%GQBtrhR{%<9$uvaw!MQ;XmUb9*}pwN2V9f}Z2&`ssTc>eO(&&YQDuPO7%n zy8ELcnJ^8Nqh@}ku>)<>lXh1fakEmZ;{Zf|3>5@=Z zn65XrkhbK9*)?)Est=Laf^Hn<`5nu;^M@R?7M`euTXNj_2ZGvkQu!5Kb7gCm8*CKP zt{Q+cjOk8wd}S)pU20kRykoiOyuc7iYFx>X#^~UC#EQD2pwM*mD~>~t-!{k#S@wTb zi?;RLpqNCB_=yV%F*1{(fDa*eqh=Kw-0g<%kGSI}hRa!ORns4?!C%T?S0g1@nWqY7 zYKho^IXs*{QOS?0Yc)c}DJUmQzVLisADk)qH5)%E$Elu=`Bx7s{jq7dwrMs`AvN11 z(c3FqJ%=Lw6Qh45WB%sx|G{wQbndHbp_BNA)Jgqmi_&S!2IsdJ3#$b03c@l_);d*v z4D@CR5cpVKUMw4prJJYW?~dih!dw8i~~G>_J6{(&z#? zb>+tyHkO^Shls~Q*dLI}P}2Gu!0)LFp&rVA<>8VOb;1T#yzKp*qin48D_OV+GSBib z)qAJ!Wp&s-238d2`LMHBcCL|5dc>m{l$xBSNf+KC_I5{{2Tu<4V)F}f=MG~Cva<-Q zZ^c%61lCz%q~X})o%s;Y`HKb?gI7w!7Dkq(>z6ZtOn9rV?n{y^ONG;pjLtNqR5d1R%DvQpm$+l}vf0d9fm6CEP1Pg(0}2{T^q7WCK@ zI{{9^zCsfrxq!xfPg7sff+VC#C-PKQq}~S=FromeHpp^ZofEc3oxL?EMa<) zLV za;!!A_RRnq+Of?t;Hteb?iXmV{cbw1UQd~-KROFPzory44qmo6Ii&#R@^EUYiJ^W+ zQWG1)k;$H->yE8STH=SEWr``B-<8;w392+@JvhE`Xi1zrYwPL1P~(omBq~s4r7t4~ zN>G0YI2OBM?>O_=?%SO2Ub5P}QEjJ>2YSVL?Y2UV2GA_Xhd zv)A~tiU^l)fqB1Nj${FWbjDwLkAI%_vzx8%kjH498Tr{02;e!qr?_~_c0XmX;ckYh z04Q?K8j6T4R7GEX>m7d7xhy<*S8iL@6)3!nzXu4BXv#q{IHyr&wy0!=vgfC^avI|XHIY*Lukc9>>;CDS~X z#M5U8(Bn&!Pxh5W>Q0^6Gbfa@NJdRRRNy^HOglYXVXl{z>|RT4qED%P!Qn>OXy-~* z|33G^^Lzcr_c|L=Eh9)Lg=OgjZ;jQU04Yyx+t&qti;PgmNZ*|@~ zg{Mh+=&d$4KG*!AGzT+L3R)62}v#46_JY{#t(j; z;hlc9M~;~uXH+nxBCWZ!Zv%dIM;ph5MJn@a z;#`~eO8FXqXy6ALsE%0K4;F!er(!;eXyEHi@olx4h2-D$PIhNZ*uTYFP&f2dcgpBt z?}-Y=duPgx*}9JFbExaUkb)B9`@3S6efbWG0(f^P6lUs-w{dms#Vh#taoczx{5XJq-p;{Oa_?fzA({6q2la=7jOJ?8vKDZt8ldS27gzh@GlqkM3F+wgl%<-e!**8sB8|7tn^ z``g)R|LQ~iP3OO7E5!b6+oMEO#f&Y3rY!{e@6MGm zJ>HB0yOjGUG5>OT2h;zZU7$Da^M@Ki%%hGf8(d$p9nFT-15F+0TGXpW$!%_@P@mU{ zy({bY#B7WE)Kz&lEJp`#6GA$M9TAU;-&mko0C#i#tn|)bZ<_L5F+*Pn{QKzV`ao09 zR$bpJhV1NnVvSZnRzD|ZjUPT4a1t@j zrVlNleWsU3IN$?`&BMZ`BpYE)zYh^VO4Z}_FGfhyrZK-&4crAJu0R4_a>^xD>kzec zm78rBsBE^%{UMzocru$QWSm=X8i~r^%GzGK!U>`r#*Xg=F z^_oNaDK0P;xog(L^)qL6gR_g=k_0D;8S^Dj`t25#Eq&1PfrLtq^Dv^Al8{x#oN2x+ z5u){B%`5QQ9@B($*Yk3bnyH#FyfrL_O=eJAqK4u2f~EsOt*xeDf%-o!tcV% z8J?h33W&MPliOPUXQ-LI4%HBf0(XWy99InKO82sk3EU`{EZLV>ho+C8-EnCwgr-%m z9JH3=S2vJu0rGdrZ~up2vK-#&AmRGm{hb!(1lpIZqEVRV*OcX8;G}d{?B3fhRY*3g zz3I8Gl~(Z&%G7$w%bKZ^-Yj1ywdd!-+(!$hb8Ct=`p-iA12qkw3?CmcgdBOI+4~sT zQ|Soyu#_hYC1}{<#U?K~8^$tD$RX$L54sJpPN=f{HP0@w^vl6kQK_iR93!fssuB-HBgKVmKU4gT5opun~7_9q&tA5u%tp(J9%}q@89ILJmB}`HxJyh zTNidoG^>d;#Wgqsvx<_D859q0@?=}I8h7PNA<;Yf{X&h<)mc|w(3UfF9mz5$JXbg8*JH9)fk%B&ti-Bqnk`L;i9 zC;6b?XaF(MhmOiqqMGjFf=r&Q=+8S5)CRCeOL!AvS1Xn}BKfmgUykwesbMktphqIEEL-@r3AF~x6FOB8*}G!%_01mj(!lBp_fIP=*uBp5ftAodt$Emy z>GOMGK>aNpC4%-YW}np|4NhX`g;0xR+x5D}5SwveW>o$o`;Sx)tB<>^HRr+ZvThF8 z_QtfjmNjZro@o6@T+LX1Xr$n&WqO}0FH+US0@`6RvyHcT&w}2>XVFrZ3C*COeS!2g z$1Zeog0;BlLSwL!@=<4xeQ%ogjh#+=CYdeR?d8?U#K}Tvt5-IrotHPJ>2qoVpasv~ z1N+?qo<>xsr5U^SG9j6+7lsI6U4t-G*M11JZ^CwXPcS|~`<=IMWw+-$MHw(5F#q}u zp2fkmtokR9b2N55l8Zh0X~~L=2BPWiJz~d;j)y|0OJKkD_MO5om%p%?i}OAHO%4^HXL+Bs}yOhnFm^-CNUovZ@Srypf+0?EDzjx5t&2KHXi& z>#a9jW{V%ni|S*4xo|*OLvYNoyrA0SY2<$6RTp%;J*Z8!kZmVB+sM+=#4AxRJ!TR* zs4Y#Bn%B}1r&-9SoRFCkSniau$e$?&1mAiz9KgteajEwXPE0V`lfhjn`DySLF=M4~?PA#;6kqIn1yjyTe$P{^{pnJFX~;*fjSB=OC^;~AoPy-#ec8A5$E)C{j+9g4Nr0R1WeP*rK!jn1g*}--IdiHR;frkF|q7phMrm@ zo_JRaW1P~&bp_B?ySiTy(wPtz9;70id3S7_P;Wx$I3X}d+a;gwK?cXm(QERQfbM+k z5m+S8I)J^mvOvHRT4XYFGVyZ4Oy!AjvxlKODqa0!|DhMp7_shD`2XmDRVlYCPPw<0lOIE*=G+{`0{fsU2~sbc9Wf1xduWSw z@-@^>GrB62jH!a5Cg3UMx^Z)mXa=(RdVuR^ySFx6BEN0W)D5ej!RhZO(?uvh#-S#a)SucVr*UJ944%9b@NN@}Ijy=hKb2nT z!-hBzOj&rj-e=#$9V-ABr~?YRjBjt+wGZ0Ta?2=nio1&W36^p0*aW$TJ!TE%dN+po zGNpm7nMoB}ukPu$j{)U@wb*Yin7V3*a3jX_s!u%Q^P3$%LRJQ$?r}otV|paYHiphV z)WjJR>DoT^+0ffq;S98NvKGsHx)h}_Mp>z4H!8>2yRPXl@lG`~RUf9YF7DcX>739! z6ElMv@MyOy7Ro;0KR)DP&lAdYso7ZShst4y4a9Ue zYC%+|f1smnRKw_Oh1nco{CJBb&)W!X)_7Idy!wPqeU~)v6uMoIt1oWrWQo13%kD!E z%8RDckX-T=#^CdWdpnL3+>!6$t>yO#q=ZS0pb@c9QUiG=BBG`N)h^>X^^GY|01mwR z%S$JsTG zVeI)ptgyP-?6(JiZ;nI?Cu~egI~?)9!0B;g;6V7)oqnU&G!}SjPrFa zd9AMg>;pdeh^It6pEVC3ib^~e3s1#3bVh!x@)|X+&_Xy^52#x{j)o z8VMH3)yaHK2iLSGe(HLSy@CU@dX|?@C@tVoGvr4a51)&W2C=GYX`yYT13{9P2`;xt zVK~syiLJ3=bx$uSe1XryX#A7FdaVRTruj9D2TZW;ewLmVq7}aW5RV~nxlMiEk0n&D z76Du|Nu6@P+tBPq=ASFenkBBU<*JEZlZ-Ra>1=M*aH9)NDL&O{+5Etx%#DN0@920=nK&1LiOVH-SHibDS^y;WhQPyw_p|ek;p!?~%nmv)u<{)8jD%YZ_{_*~!Px9c?gezGOd~x)P#UEVPr1iJA8kvK21HKN)dCx2{X9H@xc0M_X zX^rQ351Z{Bv)-?BsfFqxx#vJUXXsZ~O$7m0yQQ zokgu~)AYPTklHT)AUIAj$nxZrG?dfy`m|-2WNV%72v7h`APrXPy;yiwa6O~Dr9ve= zP5~{6S$A}Tc4W4FNY|1KnHZH{*1&H16bu^Sr{G6CdenOFZ2!8kzR9+QC;Itu;Diak zp4KMJ|M>1xfkiB05(2SQjFKkH)2uOdqzqDzk z+Iv5p|C&R29x{$}yOW1cXxLuSa5d<4#8b_y1(ya&wunE zFq0XEcOv9T?t86uEy>=+vT652^(mPA*k;N&9+#Gm?;KY}z034kkIXEf<}pUVLSImz z%bH-QT0i6}b)^c>Fc-~fEn(`5@bR-JwqpP0p3n_7*p4e=&TC(I_<{3q-X#3cCP6qM zuf-7o(Bs9!C}@6hUqL&k6njB0Ew5xr`v7IC4PhgL1{>8hJL)a1D*1}sci^k_o=6Pd zXnIHkoqD&LUmu(d6!8Hg7WzjIlTnV}@S-{^R18)nqPLYY^A2pL7#)baH?wzBo5!~J z{dbW5%HMdxVXHURCU-ctM`}P^xMzQH%hKrc@>ZS4Oq=PB-s=dBZCzk??)?9{&@N#`?@4M6i z;DAPmB~JJ$!Cni;AX1{k!TVB*PH-CBfUsrkNa@Hp9FSNkTee_-j$_&n;YmPh2v3Ly=5sqI)`KigAy_ z9RK>Chd+Mvx>y~zuex&1K)6KF-8AOtHul#`OjiyJI9WT1I^0)%+SKJufWrj_NE+4E-@5KH#% zBhK6Sol(v-X%OtX-fX!wI%@d;x(6d42Bb}PFK_NXVrMd}K8q3Ij+2@7^gXGu%(13X zZT5><^DD*A36lpjT$^an{CP0L4DprSolvu3lmL&fJ@=Q?;25f}KQu2%fgJE;qU-)+ zQ_M%~g%7=5c@)XwW9(kvCl)4Z$BczZR&a50q7Wx@L_Ka+Xvkz#(*+VvQiCh9Gs=GQI$KLTuC#8oI1ktsj?-yjd+GtqORPxcUFgM%|5( zFPxzz@q1P;YDP9aaH@hGz&FJ3Oibrunm?x*XI6~>PhPV=CzWuV@EG$3*ZQ zbZ@(-;!~&rj%d++(H|fE)ibRdJTn7Jn{mg zkSwSMDH z#1GvEUAL@ZBQH_mcbK%l-sTZdNM#61^XZPn`a@|sTU*RlpI&|HD4;x%H$0wD>5TFp zzBz3VlBij_*QWVOLr2UTe)tS&1igLc2HfO{mE|9$kE7$jFmKQY8#Pfqh@%MqGG$K+Fk5L8bV8hh0bsuu+9;K4*e{u%>b|4D zX0#2P%Yi)>%LN!Lkq62(z|TA&GhT;J>wxF>;PVC}o{1sNnp2S~vi$|PT>CS5nyev? zz|J+(SC;gaF2%|Z?D%|f^ZjN;uE30Yn8ds$-g0=Px#gkMTZg(a9u4Zp^|+SMfW5XJ zd!6s3s**R@7%GR}>1=*H8a%2zrrES)ljY|?EFmSwid2in}(*zfa|f%|at&<-~#FUbnu_OECl{sac8gsOMO%QA~q z_zkRa>*8)}wAN(P?K`*$g)qOQ zSP&)wFMc_us#f%*tETPe7KM)vuNKsp-Xe<-kXADvOoeJZ2m#thlmdE%UmH7M*nD%P z&8n$CKd0|(Ct%!hHvLT*sCSGnx=_D)2pc7p-ryL75cXRA$UI2@I&nYWuH8Kp-bc1eUp*_p9nYk5o1}bGn>7MPHZJ3kdSs8?f%i7WxNR73ae!9V#f_LZUw*O$(lEMO<_Xe6OvE&NmMtIrj4S^EfL8gs0$ zdjfGVQ~%;&cf@A`-Z?!&sg|)YCm744??=1410Y%TW$q7en~B)V9$X-#O;pE$BbCHt zo#|OfV&l+E?EoFv>g)fzdzKzXts4Tarr5jGa=X5q_BH#@`d-C1zS6>b0U~nmpf|oF zU?ZLg?|w26FR(!7e!)SYW05$&8)U7k`Tfuv-=Sn*;pdeyBLolDb9vSxqK8T@W>-&{ zIohuxq?6=v)tgC^L7CDU5N+rCDcKrfbYYd@z zma;J$y2eSWS`!^}F@|3t7oHC)5U$~`VoqX*MFj95I?vAYS`T(QNY9fNTeMpD=7Yuf z_<4>?UB*Jp6v6cBzDmPmsgwR`&#sM^4`7OxQWY*?BUZD;AEM)w8^CoQOc;raGlb^ z{RpDTbQz=WFuZT$`)p(VffQ@UB&y9S;?0BusL}`{qy{nNpPyWL=6P4KT|E$%CeYSb z+b;eu+*4Y3yqaqE5M?mYJw#lKZg6Gqlnzsp4B+wqm0|Q|U$LgzF}EM1EY4trQ((ck zoX!L8@aa6t0D%wa13`5jq$zYD)KbLx2jnVm*UU)^Sui;dVDefpeJEd5YMfRWsv$%P zpC~%P%s=mjgD2j9@Wmg#wn%WpF;qSML|eWLUS0nXYapf&Fk{M0T3~7#Z31(vqTLRY zsh3p_r)8_xk(Zy5VC;qYtO*9C}{$Q*uIisQ6D#hk!Q$@$1u2RN1Xksy~=cR9^; zlE&sFkDVb>Dco8~epvs~qj$P57m=R`xA~ONpKZpFvqo3i8l{C=8T7^6Gd49=E_mli zrE9NS_nU+V0fzqWqiw0c4nqd`7O+U)92fyxL*dLVg+EC^w_qj=)6G46vQ@#Bbu+D& zbzT$n6GnX|_)1JUy5?hV(D^Z4Rp#M9$rqFqb9hkOW`m%&c;t|;$`1eJW)|5d`!O9G zB%ld<{198$5^ZJL_|%~aAJr-Z{JxD)Z|WfV3isy9iz2IVvgj8oKEIb@TXC;y%k0uha3x5Sgj6TVF z!%x*u!=6~VOW>+c4YVPWKqWS;?d|P~9Tc0{XJE|FP^nA?1e`^MsgaZf&Nt|bOnEJB zV?z>1dTIBTdX>Kd*WQ6uV=_mSrUJ!6OdqemkOKL&kbu^Qcc(MMmImOw_t%8?*}cgd z6qm;GBJwl?QfA?a85E?CadxaMk8anAnX5UQQk2F`B()2jX?sCw9$Efr$r~~uWj+9J z5(09?ZSr;bty>=CI(y~7AcX{%Yh!Q(?d3S)9(p9xORJGkFk1C)UBTXlrUVxJ6@^Q5 zIHap?imwbW_r0BlM0bX(8TuRu8t&BLi3_vgR^k=VS#5aoo#KL-k;vX)aQ#qy>dGTV zR)fXNNnxoz=xuTBp~K!O(cc`OIC#K{QJ@v*OnXn|M)UdbUX|9ngy%O~aSp<4=7*(T{DVAeM-)Lgnj!4Y~h6qdyKM^m9 zqJw*i350^(3;nnGj~?&BT&t8EH-pTkvERm4R32y7lYM_mb2-?!5{>SEgDjG;4@|vc zwDZeG{lH^S=UGJ|COBh*YF#qdeYnr#ornGznJ)NJA@DE;Pt%em)@||RSMlx^d|imI zaT2fz0(j#8oPE}VCoEs-7{Ab)K!#r&!1eI>TlTZ>Om|72ktzHbnuf!$)tXo?iH;K~ zYC|`g*ZbAw5%x6ecc@XcED}63;&To?CyVFV?N%?u=LoD0gjW)XgzSqo+W#34d_JXeu~_%lou&ZbmhS0#%9t%>d4Dw{-*cz2%*Rczd}ipc=mlmP*a zwB@Q`QBm&N_^3kdQSIZl@+9Objf~}JQMXmRisnc4F~~SY031dWa>;Rw6mC0phPgg1 zgdl?OZlS*7HYFB3G$@5>1`!OcM=XpuxkY(*f;;B!T6Jx` zS4|yG1Apk7zsk%}!nDxUKHiuTL>@*?h0P}YsLitQ;n?YC*8Pi0#$1RLf>@Os~7h^F@+ z)MjZLnQ(2u^Zzl0+un^7oqzo)WP2)FmjDORH5N6Ko&8sR2wEvE6y+0tvsf8~9%)P0 z!kVSuI67q2yK3>%ANsbIjqUQ&X>!SqM9kWfEzL#uB+9T@?Q-sdC9#Wdp2{9j;12W` zqmYw`OCqZ%%VDSQzXdcRKEpVhEfv+%{6CiJ9ot>4*EUL&bMMn2kILFet z7umMrcY>xF`8E$rTo2-s2J{yVnSIDIWR(JBQlf}MGvu_kF({A8ym)U!%gsL5B{+L4 z0baPvKkpheT){C-`7DEmfL!7Q;ymrqPUE|ehyK6bW>dqn+(l985B|1?ISWblfgP1y zb-`HW^?%Y_0-{^ zlL;-DH@G)&)xEt`aB0tttdGxGoB|iDKZlT>+vV4yFc523wCA1PJI-r3nz3l`n}Yu$ z>%P5smn?nXwZK!cbpD}~n(USE?pQ>}-9-hd&dLQt5Dj_@9hd#?=~DC$vNaTgrCUnw zw_6mGOgyj&T&{M~aBcGrc0d}CkKSKmuA;g_^s88$;=xI&AX28$?XbK>^-g@?!KkxR z4TL67Yk8v~Bm~c9dLq2Y&+5&vWN35^pW3lvz_PUfuaUKnlfZHm;VY~!1?uQRru?7E zs|SDt5_>KP0;a9Ym^CxtD+;q}%F5l&ll-e@dbnpZPADxoX~B$K&iU16yPei{xzwzM zKMhTSyT`SPq@dpTQbQwG0X9L|(ZyCBqGz*@&wCsc)a7z4j(~luuT3$j; zk?Ge|ZccwDPKmx){#E}|kuO6WQM2>SCu2Y}4m1DaK4~m}cf6k1Xhs;$!|_&IkT9LE zw-LYG3)ubs?1rk@0M+$Da>Mo;uU_XKslpYTiHOA&q0`W@J7~ds`a#-y{t8jGe$=_R zBEob#xoDjwuQ`ok0|oFIn=%CBxiNJGzXG{sKb2^*{nX(8Rahf79qd6@NTdwJrIaF6 zXI~Fnc|DnhEKI>NU;b&ok|aXC4ZA12W>w?`9AHf2Zucu=p+QI7dA^u}_(tN51oc?r8_!tC! zj8<PAm8BlIlgwX)a5P@ol9+gCy~k^HF&`7p{OzH(^ElU^XYDp;p4rTL&}j&8=PSz8GrCE;a0&FM z%EtuR{9#TcjY3l;!M4JRbN*n;Ph1^e)c&C?%)`}7_{osR3*HHvAuunx<+bS}z@A>Y zJJc!m{g7iQXkhwbpHc2!H=0oZ|BZZ|-LP_C0D3fQc9n|U$BnL>y~$=~X5cRCTDy}( ztCrh#-+F8=68&FBqJL^kViEq&-GuQzcY$;wneVGh6JJFGruR=$2H$56MTvAS@ zk8@p%iDD{UaL_x|?_(usYw9-_i(kjh-qm?>=xVub(K|hSerRuM)M@q5!Fm^UFF9qB z_Dmhp4G#+Rx6vzOuUi84%Xkjt=Y2EB5qjf%v}edC1c~RRZ3?Xw$om8NgQ4f4^${Yu z%qpNK?B>aA7xjAx|L3sPV3Kb-MAgtJyz^(a^hxq?U-;cF+?<>3GzV}cjE?RPwfdht%412%ezeX8+WaLM zF%Un_=)X0*_iDW@-sfq3+4U5ifU{{|%EOU(3**^v_45n1k+pAwraf$Dwy| zaBncaPW|d7Im5%8`Bi!u)ykrU{xpfk#qE@(=@pzHmg~c-_3l1Eih2Qg|%Yz_DM=7x}1rOYkhG8%M@|_Zgdttj_K_9VWEC+agS0oUK}i)~ z5v?AtI^SGO|MzvTtKS=MYbj%)LMY3Bo$xZ`I{B5Hc);K^$Z3J2cGXQN6Lb7)?%$_D zbWC%0lbs-POImQax~=l~KH$BJUK`u2TXskv-0%e?4#-osbDU_jEo+t=d6hEi;DlpB zs1&!(=FIngz`!D&*XE@u0MX%Xzr&U3aD6k6)QOTFNZrWH1@PwFbGmXBlsag|i*Fs8}kz%yVYfg=;~7MfIx$XY2;mlrwa zm9n0_G2kJ7(ay$1)SXOH^P399DIR_qqt>F>N%TDa=xG(C(;7ab$qN(W8_lMwo__07 zk;WUZFX%&h^E=ljk4!~$6hGWu5QK)W)3}+3hd-4VeGd`!CH;?YuyE_Rq;te>%opuv zOAi%1SU*ERNPl$6i}}2x`uuIs-Mmbh9N~e>Bi&MM24UXOHf4|_+un-DN8;9>is{*# zyWj6oQd_688w3!yYd^BL5}iT4DWm?CK-^!#Z;XpcJ$ezwXMk@pWu=m;;oKlU;_;6 zt7t@WAX_HKkaN9@EdF0bIYxvJWYJbKMoUKr-;Tc`s$(C`P4rMtd|bRb3hcTB5lDU2 zp@n0bIK1-%&q4bIJw!7Ff5a=*ui1t=SaW96slSvl);QRAtyW6$gSPmDB82Rp$H_itEJo7WKjixg#$e2_`<99HLi&a!;O4G;8f=N#Qss14FBIf7a!PCsg zxzjwO5l&qoLr$pIyit|OcZL?X;@u2+1Vqc$xG$jSkjcsZDvO8>0X89?BNw>Mi4SyL zaYWg-8BY0ABO#K!e;LM}yGsPXVTqDb3rqd_PmC_e@49<()e)BvcK~GWKx^6OBrSR* z6!D}S4hGmwR%3=Dj5m^qnIdF9-VkUkEI<{e$U`jE>=_e(4RB-nnW5kY9R(~e>y*D# z#%)}<;@SviL<)_%j_i*sdEmMUoxjBmx;g+kVv5lvMhdvnyGBnBJ`kQAY;hk&NUH~u zl@;pMFa*!}HSszYsjA}%J2IwM(x0%~Jn+XjbrmN!uehpxW*IbdWrwX0TQ?auCz+f* zzP+R4pE-YF;ZcnrgOC>gILEN3ijm)ak=L|?`q*jQ98tIHa|hzM5 zxlvJ_2)EV;E~bPWbbHiIy(V-cXWm*o*fQB$r%J#8(|-t~iM2Xt3up_hZ-5eYyU~kO z%la%OjooUBFC=0TcL-W&%rBYB+r`zBJEW?6Y7X2)-&eGy^#$`Z=>K?7bkNQiTM2ac zbH9i|E7ub&;2~;8H-dxfO;^j;iZ9_(*qs#HlAP5+jLvj3B!cf_dpKUIgzvA6HinSt8_YKk+)Rhp6gvoA3P#q)- z7Mr?3evI}%%v+>tvBF*{C(<$?x{tC%d>WVeDALGiaVWVO#dYvqJwvWnU4)$uzcabY zckyS|aw}%KSqFbMe|to*NifMcSUu?x(9gKszHtbax|e{>eC~@G zpD1df6tjOrDG@PAwuJ%(2{b^yKLzFeMR0AM(w#aTU;+puJrLkam|2p!Mjg&0pWDhdkT_3*Khk@yN|ea z{=0YS_rYrfd0yDbJryCNUfYsoGP6u3IBBE5A^r8NzrEq^7!oRTmG;UK!R#hj7jfQe z!|S=p#2o!onBi5Xk{Db?e>p#^W(|GZCxkJ1M7`YNQLsM5_`RsUGNVvQEc+xC+f|pF ze(4eMJa%VAr>=Ka^-pVSHbd74;(6)pLM~#7Hs08uH`6sO=8KS4%(6vNU~@(437vRC&T(T867FI`fetGxu_%+ z_soqQi`FWDH{2Wabqi6h@X+bwCc+Y3QRKYx&FL`*=R+ou4w|fyq4G}+f_7n0-9}dH z*)oDDxhzT=on86kFUr26ex?A~T?Eg4gm;<^Bi+s56G|ByGT2m(kw0p`36Ol~!;cZJ zd@|M5Fup6$$lshhzlX2Ah}Uw5zo+(Tn3nT&CzI#`^{_JM_6+JNxPavKHru@uvXFyZ zE;$*^j#OE3cB`8?2V!~WQmv(x%Bs5$U@e%tn$1^-r$p|EmtIF#nTwct4uM}iygw}? ziP^ny4->ry5-~)!hUE#Xe5DBC^i7VOTl4)d;r?0ZRVkW{AA=8P$Z1YTBuif$>3>j4 zqiX|^?K)eWv{Mmn`%a&J^hh5BOh!HZa%^(T5epHe^1IMoguM8;$Z7Jn?6h+Hpgj~L zIGgD^IllRl{DJaNM7620++o_s5X3J&aj;KO(%M8KK7@pJ zU#?7Femobdz2D;^gm2K@OGF+EeCY?z{!JwMa5VO(rzx)finRTcaeei7@wg!$IV5-D z`yJZnyIzW&(B>0f8%TVn8(Es_HTNA-7$UdJT0L6+zI_QNB5Sw(YAU&TOHM4&%p+-t zzGahBj>`S!;i*t9FxgV9W{tIIxR9l{1dBE;&A$1sq?D!Q^B=|Fc*fA~Yol+onN^N2 zav#xA9<15^CfaZa;pN<%`~#z9P8Pi%8x2ZBN{v+H{92pq!fB%9`VX6nrqN=JjloO4 zzP_(g&Fs<$+Z=%gM^D$~)@9xNH4HbAZ***3a62(?DmadSKDsTcDOKjowGLi#;JH)7 z^C+rth-p6x9?CjV=iWKwgJQ0R8tMrpO63VxXq)&IzgGinV|M_FOp`M-bSikGpowrm z7#N~XK;Lm88&baBJ0Wy;#fR&qv3c@(>n*-udJqcm(=fI7sDQUCNhe8$dccD37Lgf_ z?e;*IQEKFT&nkC~=2dZww&ZKdp?s3-1f^@@sx#kTwg`LtE`x@jQr_kzrpd@<2X+0y zPXNv*mHlEavr5f491Gsj{S9i5>u{B@dUjob62`;V)D01}+BnAZjJbw&!}0>1 zu|ny#G-%C~oxTr|UcLuCmQHhoYP7_YR8SQ7nSZ1#;D~%KKX2$065SmY7%KswJ$VGJ zit#EKnG@NH@fvVnB0I6XQgel*EQ4j<#$#u!9y`Ap->tMBbuC4;?u1=oO}je?0NPb8 zS+zTsgNaS`T}!GjlcB@r1K}t><1%1*#UK1Cb$_rfSu7^LV=3^Ad?z}4!sljdiV8Z4 za=QRxa-D7hxLGbzggSOmdcd%j(&;%bxLNARGX>bfs0Zqpp~9p$g>zP~FNp}ggA1bB zD{278mPc;08J2H%Gs|Rzlr>J)f>P%^5I1)??!qQd503L|l_&IRj5D&71HOuOsJG|9 zMFG?DjsFO~%L2zKog`V;l>+TU8hw{@^5STQS;`W+qE~uedbrEvb?dnG+t|O2#S~X zcsb+lo&?z?pEXCYqtyUs^cVH!+i%;g(F?9V!%bV7JQ1_tCA9fQ7vtFji`)I_&pq*X zAVY|Qrvvt|zD2pQDZ~hFBioO#D0pOj8n0^Y+J*_1&km39rySj_uN3y>zYU})C#B^|nE5zl~~VAz#1FqvFJ z1G)(J-vtw_oQ@aWQ18GMXB^no;9^j_QE(yFocz195(|gBLgfDK$hD?IJ#SJk!D41r z)$IjbvGah782R!ax7sJ{cf*0H()7Rp&YSfYjVHRFLT&vKI}0Hfj^ADxY)^#H`^-=T zathoX2S_@!W9;&oETy{bMwxX^*=i6U|HOLnyq)}pB`SE;gU-#Eul8<@LRC4I`yx+z zBQ2uX*3rF3Z~L}Rji0&j=l zVdi+;fI&|!*W#~z5$^)+B($8#!4T%E+?1vpW{Ph|{b3L|$J+$~L?&3iYL^mQ6~Zj# zY=S7~6Tn|hn*=X7jk3Um+zu*9tpPq!4A_p4%g?c`i)DJFw&}!K|ielQhj2pCjL$CIp?jnLeOZ zYBDNYul|ECU4qF)J;PN`T7H#fZvL^DSx80}cKlaiZJ*9dy{%>7PBExQH>BL)X^+R9 z$gZIdP%bMlsr6tM^fp`NsK{oomFli|+tWci};|BCiYpwJ~O+dLIY2PRjD?T`7@5k`E3CLc zO4yVQ#bj3pjJJ2t7hy>3oerwTT&|BVrwL`C{;A^sz1-TB=y6qt5g%RagSP)Rf4RTq zfrrX-21DGB7j9RM#?93&>>k*p3+F{d0O_cd*oF^zY7{;GmGd2L3y4v|((PQ>Fj?M! zU47Df=D>5V$3%DT?*Ag8mQ12yVkcdYK5}a#i(qt^B0$H9{zRSd^fS7G^b0xA+UZJ) zO&cGeC#y4mYVjc?gS8d*6E0uu+;Z?Dvr(S}*~M3zRFxr&(!NiJaqk!3i?_7{+_;Ec ziPI!iqB*V7Vm7L~?Tz+o4oHYO-W?cex@mY2R1!_Alaf`(ahc|i>x zW#9yHUj3GImB0g#b2^b*g*-Fyc*HA`KO6~f-+n@x{;>8G;;VeUvMr+Ff!-OF1*k20 z^`A6FV}pkExp#%+5X4W#v+zKS)rK$&h zFL+j|p=ccH;+qg3ff+Hj{!2R8X*|}nZ?CC@Z*mMUW;gKO_vh&;8^LI@OvWf;1dcok z*;yJ#872sulONCyuV7-v!QH9e-%q}H&hC?(AvtoCSDS=~2bcvtClzwEi z^GBSLE)ru3ddwIiRQxNBx%p!FG4Fs*8=hjIla7PrqRu39I;>JZO3gPmekyBmR_5R^ zIih`xtLI9PO_@PmN_+L14X;7-{cyh+@mC51t6+`;-f5j{jc-%FUlMB8--l@Xj`y6L z<%3d(zM1f`ROk(3ggLn~u@w;vL$@sc)vC8QUB^0V)_{Z?_ zUj~wl=yp(zaZe^Nl6fzkBO~mm-uF4q+~WC!j4y8H<*|&_b0YsFH>zm5Q6R*wR$p1i z1}qYs=w8p{9H}6Ar3s*&miBen9c)fy#Q9iFg{`&|G_U(X63wg_atg9BzFw~pmzN*# zCxq==7{uSiIUGayG1lzZO~kA1k4>}f5a5}_=ZMutf_r4;c!|T}e&||J(!>6H(ceRK zv4K;xRXq6PF2)DHc$Ym@ZZG&}~oL#QpzK<%fGc zx0h`EF8R04W0^1Ur(CdLFHPe~!mbE$+~BE=WN{QxGj2{mE*!H!XEcL{PZ(vwe{7su z{L)@_o8{xS3Djr2c?D>BUtZ{hP>s3ff+R31LkcO344t-JiRYyoF&$jCkRhAL#j*MT z-$E*Wi4ZZ~aWCGHv+Cf<2jJsN=zQwg`IJ`Jh@EC#rT4+-#d~!n{Y@0&<616HkXb)I zyar``ZhSBzxe)t=mD1RcXd?RfQ8|Mwoy-%SY-yI!$g3Fs8q*4nMMvy3 zf;;6GO48W&9~d)_l5upC*3YY?@ThH&2FDCG)f+e7n+2OcAaSLzygd{}GEK8*rRpgo zdAh5^nXnei{82<9vuK*X8DeGTX3wopc6`f+#Ckd=j=kmuWf9U4}L93bo&!p7!pSM(zuVFxEBPE3*Au)W;(cvvNmS z7j{tE9HO(crIPz+%0$kKfKbNbe$tZug}e@a+p~9Zea{&)P6dNvg0?E}8lWEqt~#g4 zONsU*wuYYXzNnVy8H`y)Z`khl!M4p!GhP+;ZT*Ga9ppS4TdO2K>HB5Bj3pA1*Q*fj z^+GMZa zBI!UDtq4A-w=cIpgsnE|>4|sS`3?jmA&ZtH&2x5-c`Ilyl=sxRw^n&RW=I-#^j*~0 zyF8I^pwvzE=RLgJ*gP4kDl6&xGx`lJ!whhjW-^CpMkrqU>3SuL81Cru;PBqUc)a=o zpfOPKi%%0F9fJlWW&wWSc5Us%j|o#<<=H#hWp13$MbH<;t*oeg3}hCS&&^ko~H`_H1Kw?6@UkqRa+OLzXFbq{f zLV_*Tsd5spQkoe}gSA|5>*-q^)Ta7pAoTYXp_P~9s&h#a%v{qb_BIN^P-+;0vL_0g zr4iYJ26UssmyXvkNx(em4m{OcU8%0!nr<$gD9=<@i&EB%tAO+&w#}dE5RmU-$uy_imbe1K_}DslUig zYwyT*Sb+26r~FE5L}HU4xYBdtp_R)4tKiM_q249qDN5L9`AdLKr*(`gTnboH(~%M-@CqXp=$Ic>Yb9sH)%U*P06}RelJCzh2=)} zRf3aQ3h+$j8kAM@IPFD_-NJY_$&6h6+U>mSTkBmH3AR4Nw_8(FY7Dwy`&-o0O4b@W zLj2mI%HsU?w2}5fmwzZ)6;VR9=&eYYlo@+l0NvEEQV4c~fSAvlq@>=)Bw?`#vM(R7;QX+ZZlM9?^ajvj zAH&Cg_!h1)zHN!8%58~O?%!T5O*&~_Za&gUzd)LA51i7M~LAg`MUp+~oF@<|8d zbqP0MVA@;JBr|BHvw^|n->!HK(R{4+qFKb&Dxi3oQIoRP=1uCkc50<3(O7%cuhT|2 z5Dq0bHBv1jbZJt{qmq}g0*U>J-*>D;`1@9Y{xrBtzIMZ*Nf>T1`nkAzY{@42PFpVN zB$So%V*(UG^_5K6_$;62Ly` zJ1>D2PTvtRlO39D!FB+_53BhKa-YjbL(nW0%!d{7#6B^)UO4)X`dq-b>T2bDD%%^c zV6QK8`ySO=5#mB%(=v{lWYNr{YHQRZr>_PEXJvRA$Tqv73q@((a~`Ln+9nb-Hc#;i zHLO(}=(?m8$awrMY~7+At;bi>H|h7zQTlL_0h3Ecc@A>U-_dC-Q&O(ZAc`cCqKNkK zI?$k-Cv6sA|1T7D5Y3w;9@UV{D9iEvDh&c121ye`0I*JBi6K?WxWE;UB`x<5(yN3V zx#C-v;i~lNMF|hoJf`yjmSAT60E*1;Y> zeIb|?(0@ZI5Wj93uK7*6Yql=GPo`1`II9?SCVeLJ)b+@&msEXJ(`t2db@-TT6XW=# zu9r@tE_}jloAUSo7|l(%y|-L?Rt#208!)^m(${oryacKJSDv*Fo4CANRhVPx(Aphx z5%c|Y%V&s7%uk$pe?iym1Ir+6^|+0u5+o$~J}V*x8*Jn;N_sjfZQZVY<{H}DSAzbC z~D z(O17i0`$;DhUcM?y7sJ(ObHk?$C4}|m^}|iB-{!{u9=VwD}O~E0(E$?D_~kmO;?&0 zIl5=8><>bfp#!DC7PYBtmEfgKsV;3sf2M_ubum|_kh_Y-IPf&b;x^f0TuV!qgrH)R zLx4NsbVi!!N=m=^@<1w_E-v#+1>NH0H=9D8&PdGiqImgompAR@ZD~s9939Q;S}L;x z_s5nB7PNCrbvjmiQ6a8c9s_)%x4yCQsK-G0Rh_0t7-vD12lSJ7sedZb&NV5-Y?m8r zaTH*n{ZR_R;2}mWnU}6IgE9rGhE1aSHXVB{HZd~OO<=g+uP7>nW8g&?P^7zeV<9n; zJ&NOlP53cheDAsKesY!9Z>24rsGkS+xLr6&mPlg^@22(7DX4y~0YS+30!v8FQc}Dl zUQ$})UPC?4NQ-n7m)NO~QpMK);1UGiS<}D0EZR8Ia_-cxD}V>LkX??AbMpg)f=_T5 z1?IBK>?ph9@*O$tijA(P84kQ4v7*&k&>8*Nqy@bmwx~R}Y>bsJ|YS7yFZQ)$09MR>NIeomCqDg|42wei`|e9V5xcbq{M9=TYTxh zFG2*|Vh*#??P+s=GuU?k(uA|m%P~sOpdFvo(1BzPzwXA8T8My(EroLLA%iD=L)rgi zMSfEp`k%@f25N1F8=%fYs_=ZO#LCc;4sdJfjb z9K_T>%xc`s(es7Dlmy7L=n@lw`h{tYFA_m4niPwi#Yt^qGY#nvg^86!g+~S(cnRXo zoy>Wb_BqcfXL`k&*(xR>0vd6`8D&%SISnC!6{EWcQiW1vO;Ta}(FH&Pd9bV}fLGQo zX+5craG%vU-9P3@ms`lB%2c`6q1~nLDCXxj(d45M^CHn zlAUG63{8Kt?%qr~AMggtjGrs@oGLVO+1r${Ql~Fi6l?-@r9~e z6Pus@Q9QCU&A;dQ^qzU5n1ud##XpfYOqtuhu`oTvtkrSPs=Ht1K4)s<1;@mh+(ABZ zYO@BrDFoW^knO@K%41NB^T5>x_t?$HLtR;cu_Viy3V7-+tU6IhMwKuM(h)NQtvjro!Bp~ z(e4uxjHHDR%E``B@RP}zA-4bm(27iKy&h3I^5BY%WI zC8b)Vlon?U+w6Lqib)Jy;?Xl6yER=FsC(8}MRhxZF{{+0%FvV+S85moUTKlt%5IKDgD7fpo;>M zuMu>wLmC}SI+;ZTi)BgQhHB6V6y%j=o93nc7+u6Wif9v6YCfA`4XG6|(mXvwxz6#n zjV2K9Ltt)Ann3t0zqbr<)_q_!ixi<^OlzFi*Ek(iySJNhwNwE6%+~S@F&4_udY9@E z4p8pzLY;jM2}P%}Zt3?=Pys?(Mg8WEf{5U`i_LKlj?XJ@xTL-&UpN$2LJ>`dYX5`8 z{JO0GG2M;V7kYT4J6=<|Xc5&}bHf6VTi|p?htlmg)$@9>{F;I{z%4&YSXjTvrA$Ds zQeqn-_|0C1PQ>!leKb?@RyyE1gYT)Stu2dZo06A(1*%QHuC&=^L=z^d0H`mc!W4e< zU-hft{clpN14g?@upe6pjlOr^p%ERs16XI0Nqk#Hw9azNBv7U?8zxo!DU_PX^Mi{M zyN!x+{up<~{`fX67UIoY)*$id~#;WuxFc7h+h+r z+SPcvbD$b=rg)R*_4aU1zFU37yN1o22qWhrXD!>c2r+@Xc|$$SD7#2sJ+G?H7boSK zm|@v)p+G8gn z$%*Q+TO|QnJzYv>v1~#Vp5QxBD`&Y&!fU9}OiSyRUNRSlndl~?3Us#k=JHco;%@ae z`#t_(mNkM`x{dlW1Y}QBSICC#q|~IP^5FmwL(mjFVb=yPZg#7>ka=e@XDiALuV`O{ zu5XDSn!3b4naJTwqytDHiOSDn(f3j{vCCFxM>;LP)z`@Sw^*b9TF2x~sVu@7H=WZd zM~|Ku=s^%WQQ1cHrY!qlxSP>1vO?NzF6S z?ir^Nmqrydvk>wJ+U(`Ke#bkDrF=`BvUX!*GG)ry)85#cxN^-s?bUzbprcR#xj?Q?2?SDpt%?&{p>W=zavuNvG_0Ze4dkTL-3d!V^=Jpuzq*` zd66D7c^Xw4dV;0 zkHUBm3kno_^k2#gAMWYk!x6)LStTd@yNA02bKbhl5?)OtP$hM*=PE@qJy~8DoeUSzaut@ zpeyfu_S?6fWlNt|wzcE-IQu((()X75R$E2Y*9`TlX*0b}(`W5$8Q#zcithgKBts9^ z!|KDT6E4s((rr^Wk+4KT{;>BY=N$$Aj}s8Fg(IK;nrIiEI(4nL^g_K4>YlW}hFgN` zMXhS21Jekm%()89>P5uz@ZyY(Ed8k`whDJXRNP2h#5K@84<5QM{dqc&iu0_^?zu^j zwnRyGSBX{c{20l&yCYzCcs;vwlPmFOIg6Ck)AOnuany9jX=&b@_<^#Iy<*4LGmmOa z6AsE*n6~ImioT18&(Z(LI!`?$MM0s_mF0tYonBo!1s`5>?^4XYJU)Z}`pTx(WhnpD<8103z;=~mdMD}N=bxEZQ=xPU9#$_4<&IJvgsl#B`z~H* zT+|?XWx5%g%%0bZedqRaG#C(VxH|BzHLsOe1w|pNtY5l`dL1kvy_)al^q;{IUV^V4 zsM2`VxOjDH+NE2{AlDl2|43)bd~gH0Am&czDDU~^OjZHn=6oNq3jgb&iW`UHLfbpj z%;rLYO{7JEgh^3L4v zd{Y!Fy|3;i&ar()QGM}A1YJz5LNZ}{@brxB)K z6PjK{ifOXFzUM(aO+0jyYK%Z~kykBAi$jM$eejgvOcH-rBpjGlv>9Z5b1?qpx(`R2 z^In-7Lv31TUCE8Z04G+*kIhdo&1T~Pea`%qRwpq>Rx;?IQsFPqy%N)(V#F_(rfm=B zkgq4>dtNry`kMy(uP(IR6#Xi_`NhY0{Z8=o;nSmB`OWXSdLugZ>Zg;n&)mL1!}|IX z=mF@8{zGwejKf)1Lhcp&WTPP}`CG@<2!c_}<~dkeKhXDB!A{5OzX64J97xyO=V*#5 zHl)O$-Wv`&te6rh(y<@4r(_l+(>(ks16gmY4!b!x6|eEm3PS^Y*bmNSMDST34ll4% z4E4-`q)?2(9}zA)vTrmxTastme(B~es_6*`Ln)1BmaCD3XPQI7eupi-UNz{hvCK;c zGF~*u;7Hd6o6A(#`V91@KB0L46Pe#2)#z=&Jm6Ge_&R^&MP9`7ZPo)Zs&paZ4C}Xp zJ5DjVb`A-E{7j)Hh>Y-2yjqD~zU9_pb2QDe)8;8{Lw!z3!Pk9HpQ*QVdW?!<3nSKI z(=;p#_U&m6Z_(SzxlcxhNLvxy?j~^twD4gxv+E)xuCHC)gc`*D@ty^NzOU7Y1?w}z7?Q>P{ zVA+kIxBQeuedx3NM3ug`K)de-r8^F;q}M(AEo207Q#pRn9AV=?{9I?)Ai@%pCMJP`+<(Vkan{bU+kF6X9CZzF? zAr%a-&(;Ju1wU&wg*?n6IE4Bk6e>skC`d&9lF_6TRM(0pktrE!(zW_fNTg0oy>Cp2 za#xG9O?~GB=s_x7uHuzDA`idZ@DZJKwpwfnR#go~*+IuE#$h+e=vW-`QmxuDQ^`r1 z*#6-uY2;!E$REszqqmrOY+|ycpfKn5rV$WipTz+c@NO>T;R${f`vZ}w13WfgF^lkT zi#s{J`oyJ|JNU#i3oI|0&6dIVBt?VV+CM3_>C$W9&?T-mf*Z5qqjt3dmW)IODI>eT zue<#fJu5j^|0d?s(kH)fD9?7}Qp6g5G+Vc2u_G2MwI=U1^4MQXZ}I&k>EQPCoYao&d0TzQA?o?_3B9PCI=T3Q{(u5;8ST>myM%JBR<4PX9d^gN5)uM3WvLfM_) z&{6uzo9k7X_^P?nFyvs;53aXMi@=DAvgteWIej)$m2mlmsYVfcG=(HxrWid9svVPi zgA_VByD||cW`%a(c^{1H-n>=q>Fc z$p$Kj|IUzP$IaJnUYN)ewd5yNpAkLQ`dZCYA#{8PYuiq#&^i{?{^ZHTFC0yWdFirQ z)ck;h=`{zvkuhq)C2&P`wx%`Bk>8ONBY;jDncr4u>O@RheEu~AOq9vXa~HUoxF^-D z;?C7A9}g+oa39B&SdZ$N3dz{3fy$*j4a`8VRP3`ei!&>Q?!#F(BwjGrKMO%umn^_+ zOJW=*&QFZWE0OcdUxkUcws_q5W6-)~YHj0tYXpSCv zzo(@z(cJurT^n*T-&`<1Ndp6Bl50xL_6~}?@Pp@n#3+H)?g<`oxz^lB#nL;J#8M+U zRUZ-4C7V?aGaSv(A@x84tZdZvmD0cim0VI7M{ym$WwrD5u9`dXD2*|1p>e?r&z$b` zs{I;j##Fex2>PwzRYL&S!@93Mj^aasO6`+Y*^7jo@##L-&bJ@OP4yVAza5y|;qZ%B zpM8JHf%JVIc@Z=R`#4_&5T(Km^_~vxLtVsuY>G329p-+Tc~Z-oc6sIdWzSgy3}`lc z79XgdqA9+&w0-UnK9`Uh{#ZxbC|W%jRC;h8>)`yTqi6hkxaUpN>Op7^>CouL-zNOr zUw1uFaeR1#>$)iSJa*9@_rBTTnU z3~FkVc1E#SOm9KBC1!P?*t7X^F~5r@Gn#LLjO^kN_~N;i^NoJSFj1Z_qZ94y`&Yy$ zzNM=YhzSzTgU&TwjLSGbslhh0CSkYL{=}uk!~_4Kwf5_=v9TLi#xH3e&!w@Z+Uu^z zGtH~EdvhGHteDb^Ve;8v-74C6I?(z&%kY^gQxTImA&kE}MHVF;97n4$3)dgLog9XKeQR3*2=~EouoT9kwgg&QDr%zcub%t2}35 zqt#_!LP+w~Ez)48P*u~~zYO}T;29`W!FJCKD6?pQ!`laqyAk-dp_)VV`LH?=Y7?3o z%<%2iXU*0KN*0+YrBEJ?*G6?&J4sOur1s6XwIb10fLi@qk?h>MqurKg746(A8pir{ z@8njw2ROZDOVsP{1=nqufe!Z(e}RIvd_6l^JV9-rvSBY0G={EH*OS zLaMDwexfR0nt*7)!dCKKKpLXRDv`OlmstApfyv!RV<;zSrg^tbu{@QDqYZ#Vbd{Rx zX^v8^CatiHs_jeIjNkIEVb=MW%A~AH>~8X@M5E;Z_sb%luMH0?cMy}<_rsjKG+;k5 zyD7oKIQjw(-+Oiz5l9I&L0N!KA5>?a%T*Vu(auMqg1TL}T-!n5`9N-|-7 z0ZpAARjTTc)6WMY{=QLJCU_i%Yk-xtta2;95I1-{N<|JIpJzc{T-@+T?1PWP%Q(PZ z&-ym$9i^J~#FJ_Z_3RkGar6Zg3(Q;FPA!2*O*A6m1L)QeMD-h)>@59ebdIpq@aPjM z{El*%-|5#Z5N=_t4_C-4Ij9@0>0>^)TS>eDymZS4=P3_rh;~`Bu$^kJ^y#|UF2K5i z_4)y7EN9p0lYuRdC^wtzXMHdx~qYXwJRr?HG||FnxwNmb@<21$QA7K zx-uc-t-Yp$%vF}?YmETwJd82_3K!92tQsy<@J*)lnea~O1znEA>0|`Ib%R{p{;9Dp+Gi$oTdLkf8C@d7ua-1CSL z1_sCr4O9MC%ENgW-HFyk%)FeZtDCBSa@&kTtEFj_$+SdNNsFUGxv8DKFJo34wLEK(seDEA!er?hSI zVUu`GV=#Tm=no150Gs?!0B62(peym|Gik&F?P+lUc-=TikXyYVHQ}CsO!~_%j@naf1j3JKm{Z`NZLJmRsv!QePB5uE zXRDbYPawW|;O#jsiJUWSWR2?JVvHV_%$LJ@hA3HG)O0dh{Gc@N8*_qVkT0#XJZKIl zrh2GuZvR0>M#ewB5)c_@B}yE0sZ-Jg!U9t9j8g%5_{H(*HbY6)vs@p~Eq-7U>Y~Rd zNUWJaG$dG|nSi4cy2gk-*u$`W(OHo+=!%FszDYmU@Z%lL z;>uGw?40g#4NxOr=xDqmAh>_liILpVlkeoVu_Qp}XAoE`7TOk50?tst-`Bpjuci!( zR2U4mb+^%kowHsOFHpve#=86r)$qLUnRLkVWCquPB2k=(wfO-P87||Eiz*QSujIGL z{(?pw>l^)&wjMXM^;G=Z52!cIH)~eMZyzbMvX*1sMaf+BV!0IooD>s?WT$MDFu{imG!{J?P@e( zi*03G=grm!38b*~Knu(BVtLYMk!oJlPA_f*9;+s2lrn#jrD1)d?5giwsV#Z%x&tz9 zp;Du-kWP{{Vzj!59;-3>%mc@qAE?O;QWPjq>>E4pDWlo5L2GlApu&8)tMk%tZ(hM^ zmxM31DTGiF@OGha_7$GJ0RyP9J=bB>sVU@bb)DmZ{YVdyi9)F-?^MF|IC%|XLVkB9 zp*O6HCV7pmQkMgvl38u8Y~4485#8u?o-Fa1Pw07*DNF8;PlCVKYTVONfnNf09mIgr z3TIH+?^8V)m@qMeChkOR=N-M z@YYpd4PGxU^-PW`4Iin# zQu9Zu?=gQ6AyW>P{EhS2b2hR&YCF^fWA<7Wmmzu0p^72f=Rk1Rc7z@%K9e`GBw%%w zi!|iIsGWAp0PE?|xZn}kHAxln($Zo0b_%^n+oir(uouo_FcbXt73b=Ii24>U=$ zF9xzq0%xU0%7r9N9IA5K4NQZ#u2R=|2n#MVNf((%)P9|_N>`A6+Z19b&Vy(Nyw7LX zW{i%&?Fam2B+oE%y>_qQ143z9{{Wx_^IMtk+0T?WuL{qEW>AV3KDFAJ%-Z?`xIZ=- z1=f9LKSLb8+>*gyYjRn2szxOU<)OKF^kU-l(2`d4y!lPR7u{(0egNiq;iRC`Mv=g3 zltn-b5CkIlLtaS=^`{#@^TZX|*OUhshl~ssIqyv7t z8jk{~!kT3Bvmi;+IW3sH2!F%6T71w1mQz~F`rD2J+s2tD=I~wFzHzWW$41Qlz*Z&Z zY+b2#f+7%=!CSJL`L&knS`knqSzX#b3l&qv*#j<-anfAb(;fbpF;M`N2&Srs0&U(- zmeeamh(RIy=ps!esxE;J+v%8h(d@ER*B#&5sLqPoe|n-uj8o}#rY+(IqLcVnRTNmR zfp?hH;pReyX{cgV(O^lcvB$bx$zU6dhkcJy>}l(3e#u9`Sy7Ndo>WnEo?KAvS!`Tv zt}ETHqNXf)X207(1#Rb%C&w|4oh@|vUcsWLAw%DOOk$O3?Ms0{GbZ`uOHl^8$l)CJ zQ|`x)w9LR#ay|(e50EUBrI9q$&ZShB(m&gsT#V>P-4;)p4{^+(+%R`&){`X=ev;yV?}H z-P~9>A0WUcB34p3<=lvweM`;4JB%3`_%k<}G8O-qd?W{>1A2Bd_pB(IEN@CY$j5in z181U<=LW;f`i2mNN^*60F+aiOptcM@1vIkI@ zEgVc@-c8=hkV~G=#jWcIeWW?jyp@)Nrn0&6K;H^4rc_gcwWv-uZaHdqZNj+HUs@q*IOuNdN z*}5__5TO^$uD$N}CW;c|n^Im>vmF|zqV^b&b)|D%?G6*5(kBR8?b5c)pYMDoM`$wm z3#Ci6N_ZuyW9y)K#(qk%F6L&Ki!jZ$SH7D6V{0I3-%Ab=E8@>a)Lrsp&)4D^Qbo;( zC|jEt22r|I+wR?4OIP=mEpMyl*X|P+Sq-Y;voQEVI;zU9{p8M%ShI~h!vG;;H?DS` z0nQ!-GA&hX;h|?bZlnaBsBNLFK`U$)H3|hl6nd5SV@ic|KDKdgu53~E3~0`o2+8P{ zzDb%2d~6SvVUQ_9vhzR`3xu>46_2bYSs4Z#0ni!25acs=lh9Jy25Uk^#- z*0C9VA6_8*+HT&(Xy4^_Xt7Y4GGU)^CDeeo;;ZvRPA^^&yF$Q&m`6Mp7x!%OH!jqi zESOP=8p`LpXHMYNrJMZ8r?9z4(OH||UHOmWgJv+0e1M~lK@8N%G^eIr9$>9?188cAoam=Li!tSYd5&o6mCO=1F)oBmXoObRRbk%> z&K@o%i)tYofYp=bBCNZk_{e4tM;XW~;Ej0OG268f?XpVS4Mv}}rHB58{BKVYgO>Kg zRtM%T@r^}x#!_b+d!~m%RV#_cNzv-^>6Ke0uKnAK5W`4?25T~B+q=!o)>lwj#Y;a9 zL;gf!*T~~S4!k4kTRC4aPUKK?+4O#JUy-bchtK3Bbm=m7naV`rRX(|`6zykj^0o!D ze0+zCH@%YA+TjL}ZsvHpAsdk6BITrv?pIuj@fvlZh%hh7^R^Q0JmUmuNH_n%L;;1{ zr})NZVQcl1W4FD?Tz9_4sic~JQkcbKr$~|rDEq@e7z=A|rSCxjqFyWGN9INnD=ng< zQ=wpw^LeE$bbIo4?AkWEEP?x_lc-$z`>`dFk(W!{^wEsC7*-2&BlY=N+c zUa-B;xgrpxNxqF}c3JRa<`o;#nRbl7IIo=s7ey_Qm#7hKPE`L}F0~9OK6}8na=y!~N|3g5HSQ+Ii%BTW zH%Uz{cFxuO@M|({WE0!Ogrb*m?U$s_-mAE}jSc0@BAaJ5J-^eDK&^opY3t~$#@v>4 z`SGc@x7Qs<)oH7x9y!gFjJzfpnPaz{1IJz!<`>UFXZqN_kh4rL7YMR^ne;Ofi18k; z**p~0HO+(&-sN5av)H8GMYIoA&yQf69dxP%stbl9cZanLq~D$%z@fu5vghdoJ<~BU zW%AWCe$m9TpVgSJCv`-pGgax6=s&0I=lMhl%+mPoYlQP$w!6ZTsua0{+^);yVPV&r z7ak0r<3Tg=FmzN?P|xUu*@a;lS7(@n#Kg3IL^CaE4> z`D-^1sE@#l>nN-&MY2&+bj2v%JzTSGmkzPMK@$(QrjgNr>-u zUElPO?i{R%XQNQoqF-$8AUc_LFZ5OkW34?r?xst^{EtbzO@yAKJ$bEnZ*^w1%bWTl z_0u(8b~x!>a>*JW+g3s+Doj@#Pzg`2_f~mA%LKc0iq6NZemcKskV%wHw_}-pFaUX5 zCYE#xi1Y}sn^%A#^jgX_E8m8))7m(UbMwn=y$jHE(P{XW_PU@5s#nW&hVQ$^5b0}M zxr!?!60Q0f^LVBUo7E!3XKU2OGywlIGr}9ToLLgI8S^vH zjf>#Jr>xz-P}9pBON>O073W)h9|1>Nt|lN9hP;^`c0l=q#pJ+pq3bhZv7Do>N6yO6Q3riRZ|O-HQLM z@nP}V@7Tb9&K9=T_BAfnjt{Z6>lrdS{3$4%)l59%Xgsx?$OB0?f6rpmdzYuR%w=qH*rp(*U<*7KnFQ zDV40RyfAz|V`)_^Z)O#*W14zlFpu>iDT`i!U-=!++^J6X7u59E<4bZ@X%yvmSldiI|w%^VoX*E0TEI+&Ka0BOK?&g*3EAY!uCZ?u0fAKKp8MQro$_y8qw@&vu zekgsnUL|PtY-4srZO(CUw^UL63(WMhhHe*QsooJyuBG^L!)$T!l2dr^an1XKOV8x> zn+pqyz4e-q9H%mW=0f^eIp+-=jV#ep^V-ztL}nz1ev^~bhj*3!Di)O&e32YocC8-> zv0c|h{n7R3JN#3~+DO2a<88E3I2aZ1@i{S4fqc?_VzdjEm8~3apeWiqrY7sa%mM6` z@R73%p7$uHf$)m9xngw=B3`?!7?%Ec2^_7kRnt!vsF4r(QooMFfr9R*JV%9TF7tMGbwtcJ z^9(0X%Z!fBb^yF`S~oab(3SP7h2~;`9B2Cp*Nc9okn)Mj;Ku8R+qCL1~-@st=EsRDSv3DhDlxL4)V#IzqO!rOg4R z5IKQ?6Jv+1_{7hfd_`@yY+_>qvJuD&O=M$JBPxX5pV+~}k-aq>>9|{PvSz(Go>=ZJ>isHrFl7P`xk&Muo6`pJNFJWj;eZcJ9?jAkB^LgC z1D9m<-sAhrs8B;AmWeKVK~+E?r+2;N&eMkqg(hkfaX!C<{!GIxo$M^TzAZCKJKZ){ zjvl^eEiIQKn*Q9BwSuNSK2Wd?kK-QDFe|C0-Gz=-@GIQsb?$7nV@6V?QF?no6)=T! zuV;Q}{#8f^l>-3+>)HGH3VwiiZ z@grh;aYFY1g+$$X57Wx3kQhmV%_LwWVVFLB%94(1_VXFnNN4POGT(Rjvd8ynKnIjn_GSXjlIMjiR&c!&?t_?@-_Ua<=##@CyV{_vAXa*0T*UW4EvA?egY=TyJOwB z?8Hgd#LthaJ?Mzd7ywPrWG&p1Pug|-;&27aC0mRyZ)%zKuqEAh>Xd!t3 zAGu=6xamamcy3a@5g{#$+{wDSMtsAUd9^^%(7eg1uOIC|B98aj-o<2WEYfQ5+oNfIvP?H<)V?b0Eo5@*d zNgZ!#%L}#Zx0dPU@Fqb9m(Gue?B2%|v0j9JW&r7!`9s>^1==?U`b7#oXD1cquQ~i- z6I7?Fmn*#&MR}+(@uZ0!b4dL~OKu;_Fvy$)T-j^5HB-)7e^)nQ%`KD84!s#cQ=KrO&|Df~7nD*KYmQ*=WP(=7I0h(%Tyi2|in$4$F2;H;q#Dl4z8m*ha2ug=4 z7VS0YDTP&N&c2Z}u+Z+BgVHFoe?dX!>CwjQ!crjqCyS8bg_23$XTldo)LpoTSL7_= zu9$!2Txl=~?UB{^$z7Kvlx|Bz-nAQK{LkUn%SE3&{a&-P>ORoMf^m=c+%-$8=G6AR zhZ3xq@~h+XGcpC^%8ZoDAoE^elIfacCE=SOsUXKiADUeF_-AllVMMFj;fvvSe}Gc8 z_ZiYDkX*cGMy5*!8Y{=C^ceSJt>N@JJEv3{66Mf^`nG<-ntl>{Pr5&?xPFCNTAJTT z0x4w|pgPcnQUnVk?I|9}cj6ar}ICWXZ#lwO9LXq+`-uQ3GQ~(N;cq+SB~XKn<()v#A4g^bm$ts5r0lD z1pTYA zVXVaa(i@lZOL;X@NpD>4kg)7Osp1L(P{RZ4Sm`h*@y*a_s2=5s(NAV9ZhT%!Zq4nn zFjlvSVJsSW)|e`3T9j)=P-S7+!H%6#TWJ*rDSjwJ(tg++i)A5R9|h;Pc*Glz@z1E@1j`yGJc0cVjx7J(6Trgu}Idc2IL) z`VTOF&e)TENJh}NsQe8b*)T&*UjA5}m#^1mP>C!y+BQVwg1APz1&J&k)kgKW4@{uB zMgDrB9Nd`Bc9C2-AKcGT@tG5ZMj3`fB$bhmjI$C<333qptkYo|jqLhZ!_cAa9FXv{ zjRvndW8ZO(rSq}ec*sxOek7Mn7TDJp*|pxoYRZ?d=VXt`c0+kAyyG4i#*2ru1EpC` zB{Kej>o7iQp>*^p4mb18A3z8df>bK*677)tR!rI(DT0-obdn-$ zPZgM**59Tw+lWA#lb1az_ug!|g1C%*2Sk0m37m}$0nx>cs|?_cEkTkl9&dMgHfxc{ zNEWEP?L#3eV{%*-6%$`XH&4U>E;+yVNA&B?51-8|i=TTnr1=ha0scFOzab;#Mp}+T z(#J+AOQ8pALz~l1%@f)?XdujSzj36rR=WmYP&jc^SG9kz9}Pzj>>vF^P3T6sbac41 zQK1VB+EcaxcBQU2{!IjrRgO zh=u5uu+WdFG0!0WMlK^M+4G+t!7=PS!op*Kf;U%g-hWGC^Z*C*2O7fXRlWu5%K7wh zWe&;nO*Bt+&CH-CSI~Te0 zA^G?J|H=nGEpYy;;O~cL!VhY$|KENXe*OP(2%DEj>$SB0V02g$rHQDLW>sSjO*mrg zpRq(({@3Cl6%L1absyImx`&6v^EOEr7u^Y`Z8ZBvBnE*2zncVh))o{MZC&lU#ig2} zuCJe1+?0{Z&Xd>W03G;Uhm_OVizSo3mF>o8HtJ2p{^6LKp&}6G3QhZ(v{*g6)dp7@ zC`qZ4AiS7WN*OIwhE?Qs1DZJJlBAcO5Xv&aetp&NI>KRDwqt2!V#W*|P8+oY&8Xyg zrMeBZUM6t!%>R6cKWRx(KmJM2HkbXIiGSkr8rV0+4KW9e;D0_+U@+VNoagTp;OI^@ z@7HG#%`@{gf;dC8>8t|L0nzuJVhp8L{R#%%JP`h0-~In+y}G!9IC21ad-><1Y5aJA z`@4|pU{h=3f5sy-p!^pm7`7g_*VH)m*)t`{hI5U( zsC7of_V0RRHo$+Og!A=S>qd~)^mv4im6s<-SfFCFaoLjj>hJo)moNX%$*HkmDsUj? ziBE_L230lHt=IwaFW`3#82_0+hMj`4vNBdC#gwNma+a2sb@*TpXy!jZlaW=f&u;5% zas&S_mVF>4)+MFi3S`csdxGTPfxxZm2%ai5(pDWB6m@*oCGZT~JNHeOb>t%D`igKDX zAmn_EJRAVw=bLfb-F!5B=g7Vo%hW%AcaqM6EvoTe+|8lcJd$GP(?&li>3>-{!hYASUB~VnQmGb2oZZJf7{|!Tb{E z{*xkJIR8!Eemw?;7oC6f#J!7RueJ4?d{$Yl8W_GVPiR(?iDH+Ygrw~pAH(XwxBuIe zRQ<|d|H9^%xa=ycgb6YKKS+)*`qM5hw1w@QKCzY=f`VVIO&wY{9 zr9e*PF?PuG{>L`?ov7wYX8v{_hp?Yx5#nbX3WBl(yJ?Bdf51+UefaN@538*R#@hLHc=CQv8*-%Wz#D+e~~u)2h4J+5crqc2M@j>C4Up;vy7f%rF769{7z zB|_k%BQMo9)^}OdSp!O&rTk3Im^C59+t$Q}RCR49b$f>)FKqYh4DS@=xR&Yu0x0Mli<))8nd2RrT;z zLKws*#^hW9hPUatVGd3OGyHBo%zJC*pZO&fB3{MC5piZg&9@8ciDGY%_mu1Ea?_eF z-kj=jffrADqv}pq#p((Oe+|5ra|E^v?_n$rxwEr7rk170_JcRT1{yc+h zd?|Yb&jv4vV@s0}V-x9zHL9t2h3)e*o2?r>_zDZ;z01|*suaIrGkEeZ+LTU9UnKil zGW|H0S$#D|ykaLe65BP0vb964pjkIUSewxCTr@UINLG#b%_7a;odlww|N4l(%gO%# z+x-5wArN{ls<3;4uZBBQG(f+WU7&I{Niul(GB)xD<>qB@zIy4>xh3idFq!nUnXM1R zBZXd|)6Cp=qweFkZCVjaUY&u)X9|TJSJhTSqGidcnb766s0rJ|Y~Jds-?;jpF>P?g zynK9TEiUb!?{I19=;RbS*>L@bI+_cUErhweI*{iCnSajuPV(UUlLNBJ{;QQh(vK~6 zLcj)Z_KM!ew>$eg%*C>gLf`0ddFI};rUe{z$`N*3wV2B-#u~`ka3LppL11^5-Mn*B z3>~dr!eSB7_)aTZ8>8oO{`hZu{CFDR*4&=&f zS_&m)y(~*7rs2}tAoDv_OSJ?8F;IrdAg?RS?oMt=z6-wXlgYk^;~OI0hRQggU8qVx zMNg5erbuL`{))|py;$G#^#6B8B0|TiBaDs%IF=)A`AY=-Z>o7mqS4R#G)O{Ch5L#D zV7=;FD#63`C%b409JwieF!*+0++s#l$sQ0FWn6P1##_9_@o0MBw z`ABm{uFW~%_1FxRU1-4m@j+B|axH<2O0zvop0{dHFW&0(=6#ZXA*lcBo3JUHV6kmD z#evx)ft=RISN{h*o12%npSMe?&=EK%n6iE-EcQ2F2aG%}V z&cd%OZbtW_H+f)>{wvcwIgt6P4QhAI#$Fa4+#Eka+5vxAc^>sx2d*OYvRtJ5k5M`W zTN6Vzt$ZO`0zCF+1-Dt8y|yj0dCxJm0ePU8N+~P|%UW3LJ?zJhjDeClGUv}CNA*`~ zU0NRPOP8m4X_IKz&%g z%Q56ZT?B-As>kT?e~GDPkImwB@eDe+%&YMOK)S~?T@L}N;w7nzSW;UnEmkZjltz2N z2B&eB#6WIDc;rZtC=Z5~b>*&D411(%hCzvt&U0868|DOmE7wClU*kvraL&+Uj;+Zo zZ?l2>n3$MaGsG`lu!eG+1W8MrZ!9>V$720S)M3M*s-*+KB5U6-D0q5y+KtZ5QDl=# zw#(VJ;nVKCVLl<-dCkFt`3{o*Nh3m2)L?JUE8?2)M-h@sOr=#BUXRN)|J7%{h~XTc z*C@Ot^)H{a<2T*qg_U-nB1;7y8D-m}y(!|8i1c4AB)?gX4Gdc?SU5nWP3|OfRPgYC z%5_-c*wrEo@0-$MV&04h+hF09?KeR=h#89=>m0E(fukpMk*)3=t4grQ&X>0^m!0YJ zxHVQ$Pb2gy<}n(Doe=cjROnacY)KOGjNbPS(dL(Q6S=khv4H!$T1FD4gf+yGA8sdQ z@w4#+o)6e7Da)KtnvDchmbaKs`IU$R*he( zA=M>wQPKYUua#$PFaKonqWL`9o^HU)?!lD4DE+17eC#DkhsQUTcvAIccnbdGE^0t2V)AkMmbU2FL096N1s??auz^Vwz3)u;#8T1uILvY&csOpNd}`v zV9yZs*QQ4$s;H;5S1Y6ji+)IO>r7X-^S;Q&&j3>SEU(5yS>C)7Nv*4^J8haqnRw6P zvSv7}!VFp2D0iyAq14yw=1o15H58cDioSEW4+J{;SN`vpF=oh8MuS ztQw*$E@w(cXjsUC3faAR?OVA|l}CHpdxy*k+T0EUfE8s8^@<#Z4HL2&k*)Uw4;I5r zVhYTuYU3tjc9X8y&VcRn>5E+qipAX;|6#Am$ka)4{CC=KQY4GyU~&<$g&e#_SY%+b zg4`TSZOOb_PW&2X?EWQ&owRjydXHe|1+@r#q&v{I#b2|f8D&-K$5CfjJQw5a+z$PDh<1iM~TiZzcV^2;u`b-nMWa;VT7e z#?+DCgPs9`gTT@Ly|!^-uY|U`AdvH`JfGk%+J_$oNZYrFa@#gkIL-ImD$~uf=K&)1 zR_sRA?;X4nqJ4sI(wcEkEshOHT-VCHhxd-U6R$;Au z5}lIFhHuaUBvZ_FoDavkzwV$=O7F?9Bxuvqa0?IB@?T_0w33clu4=fIGE09juaptf zP%D}*(#|ALs>XivH68HfP*`FyML;-Th&`Ir)3OM5Jij|W)`c<1HqTQ?Z0Vd$3`t3% zj5Zf_?`7(EO``uMQLyLA^zKFQxu0e35%F z#Rl*izQ0d%LzEX?H)@_P*FbWoO58Q{XsSz@%h4VC(!$V&CUefvCr_r`*>4s!DCA7; zqQQ-j5D0J654P8oFLtTZNZH2wxLqu~$Fklkj8H_l{aW~cCj-e}*t8g`qZ&E!V>?94 z=z({|L3A^Tiy-(dZ(9XZ#H*p?ivbF8Tj>7Ko}^|%XqW@cQ}L`JVBbUh5BDyi>J5UM ziT1(deualz%{ws`^`^0d(;l>PC-acoXsm0)HyTjb zZ2HNgD^aD9OSR;fs%ovdCT(qHCivL=kQ#Bjz~|C0OPf5&84PB~`^$I16`*GBue2X? z>H}3`GSP_EiYc6#frfiZdfeRC3Q1=2;ff^zc#DMo6J?0$TIDK@701-fjL1hC3c!g+ z*5w|&ODWa0#vDDyb5P{;klMB}IIv(BFp=TqnG&$y&E!yFuk7Rr;JR6zv%4j#5mW!6X?(`|K+E)Wf62AzM03T+`{V(a z>JSl{(Rb$48Y9920pHBI5C|~Og!h3_M}4ZOj>juo$uD*Z?;ns0yjm|hFPB={NmtWY6qELz z;E&Q1RVX)G*2>+tH)&ZVceJK(nI9EOG+>l8s|mZ-LW7QiQ1$nOLot`cNhPq zcy&8>u!_7_YHL_0F4uf@V>{Ar{&alJ(R_V!BsjIis#xDrD|GuVA|JZeMdtiOk39L8 z0Wb}*T%olc1~2-GDR@vnI=P!vC$b8t%FjSQO^xOCeOZB56GDnBcM1V?{B`NzhcHNd zfdmP=ob$Iw-1zWV0o}-E0^tv>Prqe+G)W}Y{j|ey(dQhI!h+nU-@q>*UTiA?$BSbe ze4L!V-J7v?f5r)4 zwpMjDgG73yi?m&b8+p;KXb`U)DO`i-c3 zYA$g;0BSO>$!u_Xzp>xl0d7^KY35p#CM;>&XI;}7ME|k2hsUbRgwFmducU`>~d&FDd6C+}_!tkwX+e>0|v`vatP`znW&MKC7Yt6PJ8{cVm zPBKjX5!>Xy_237-a9cQCjhaUjHm=*^c|Y@)8)sr%aReWo?G(!0a`|3-={$8AK6$l-A(;VgPW+K9L&;LD zJ}s;fDk=!^Na$aHRmk}8@@KOb{oRUqRnYrLCX zq$wv?A$M}YNM?`~nd>&?;AIzYNuB6k0Gs}W<~xHxXPI^HMG zhZf&>c_25s7awB*p^`VTyLIW+G016zA1xs@Q#kAMTZtlY{}+XAJzktL5Hbs_GmyMh zcV;_&(|2JnP)OF*QO5AChXwQP;x__2X*(mO^$IHYbBd{%)p*x7o)|5CDA2vM_mfh+ z`_n55Lnp@ouhdm8Mro7K%`(WEb7)y=lJU){tm z?sQmtP(N+42` zbl*BwmfJ|PYE@fD#XeNd*SXw~G^dQIGX=%7Uoy_eF2IguvU^8*I3}srAKO|BSsrB6 z9T0nxE`r~5Y_{f^OPY|*;kqX$v&Zt9H`51#v)Q$s?r-$k`LX?|w3ctUt>ZA-{EqRy z0RktyPy(qr&G)Z=n9a{nNPJ*UD-D9KHkYXg3MOUpEVY5x(n}2*s(rW>9UDbQC`IZh9R#HJVxdd#T|{aKJ%kR5fKrtzy|+Mul+Xhz z0@7;|2wgg%L+IrmoN>xq@BMJkmxPdCl5_UnXRp21^Za+-y02*tRYQqmg}ER@FRJ81 zr`cdeg<8?E51zEF%eM_?5aS*FC%j!4{M@wFS|pqRNQo8P`QYKhLeQ1x@r-nTtubO2UjwZvT~U|9s`K z^KDx)aa~By$xg0Y%dHV7_JirBc+*MJF`@Y;1-Ce_o&mk^u}YnsiX33V3`TFns%g~7 zCtWcbn2>r7h9}~?dc;PEZwkSlIOmK}* z=vxago+xN7grqHNB$$niRLzGmN|T%!7V>$zW=tSUw=$h0w_|cIf|Xqe+AhHZRRuIb zQul>AvSQj>a6jkhgdCl_E)~eK@7Fv|c1JG67r2DD#@~^MEKK|nbC|(RZ_HR+Ysl~T zRDc)eEm~Rg?V8|s^*M=ryRNMh*y1hNyTO^bPo7!xQqs@9(6fw$#g*BXWMdW#6EzC9 zS$MvBK-hUWT=t6}gHJBPs?z>U(5x@8ISB-4`mengZk5ANw29z{uUvD&)Y9|XIQmhX z2D(HgG+Ae1>b!6C0_*)E7x`=bEWlVL*Dz=z@~V++D^W7dxUfWfmH5_5#Ph|Us*8{Y(UAm;dbRfPT-+WnjZ5kK`i`xK8ro7W7ZOJKr6MI< z;x1uLjp)~+W?2^KPvAzkQ-S?P=L+v>>qswV`S8Il9kaI5m#0rcXw!5YmXeP?8+4C( z%LYR{5;(LRqek#n8s=B9!|g1x<13FEqqn+Xlw|$-7SYrcoLg9RjCwvZJ9lYMJgEEw zQ)oMoRv5TNe_YKs1aUUE^r;_Ul{M{_eX+*IRstu}I}%4vD@IMkGhbn^H1DR6krW^0 zI;87YQ0V>uG?*C9^Ipp+lyk672GDrYI^7L=bgP+8oXfn#?>w(`QW zxXMWWmA$)Pn#L$Ll_ggz--drSeWWe2go*&74WxR;ONT*3+|K~XC+TSCOUm_Rq3@!5 zfT^PIjoeWRL0*EX~*zsf}pdefaa>ExrrQwE&bDocDu zPd{(<_KoksyD}p6*K)@D^tfm7w`x9#d*~6EGCf=?CRBe_qcxa-DK-A9UY4byyiNNp zCIvg^@3Mr3&eONLVG!@ww#<>kwyzdjkXZM_Kuix!u>^6Xmiw{r93}cU&y_oQ5KuP7 zOTA)6!#g=&fASpC3&yr2S@kuL5h=eo0;-^=nCY z*t=`!wCHcPo3skcw;qBc*6MOnAgMv+7t{E{1L%zTFfE`enQVfqz#W|6P?;KpT}Jn% zhjPbY`8B+^xMXXo+uc-BQh`fJEE4g%jc&%%QOp$=RSH|9omU=0%688Kw@fS1DrqC8 z1V+Bcp=H;N_`m7udYdCfoQMYhz#1s%=r>5H-RxG>%>w+`p7504o~M1Aat_M;Y8%gO}CM2#wOHaixpf^`a%j@#!8roN|Y$QU{rptI&@0}asqxnY4g3wIUfx-y}jBl z#Jj&6fWcizoNUx4=jK`UDd3YfvM}WlZ~NIBT!J~(;Y$tPb{K&Vtz=PQ9`?pjkk>T( zheF4Yp#rMn)%SV`>6fITk?6uG%ypv7gW#^w{Mj1zE6}*8RmHwLq4@k3Nbn!LW#`>! zS*Yg+?Ba#`3U3s_1G1R0wR}|hxwkq^|KgVc?X=eTS%y)U~ zj7=~D?_>)i_gbVm@^TH0s+*v5Z%VoZseA!O6+iBZ$9@oBtmA2YU(c9vE)|TgX$RCVOU3*OO1;J_SX4x`G2oPrBoe6f~38?I2*SPdhoh z-lvh9#z7qM<&y;oXr|Ad@6RBgANoKHll6E;9DG+_d$`xsY zX6ZDIlb#2pXi}E!19SZH6fr^a0%41PvVuu`HwoE4QkFCCKh7;{3Xi0ma{>>v-UsZS zw__ih&P80vJg7Zl-WP3aC`6u{mkIcwvyy@$Uu%apAGJwA%pyO?v z3d2@o&w%Z6A4XFAcmntOMnS!#gkhV~n#hn#uGwi4Ich)mw77>;tv zqxB~&T1R@=y6rOJytoqW$)&7#@I|J&Owmob%tvnSi-ly-EJjmQ(qSYgf;yDf70YfrOYiLllM2StogmRdt z88LYpHh6#OV4G?k;@t!*##JftVtl`PpNAc0ILiS7luCD&qU5_2r4fTMzE^EAF!13Ypo|G0 z0nK`?-;rEa$lvFqgkZ@-`KHjwFx9?v)VwKHqE=-$ES@O`Dcma_#0-@Qe)e{77qx^- z|1IakEBS@*TRBC^GWV1Ppb{Y>s3FWmA{20ycmFS~jX;?!Tmew1{P(`!Iuj|J~ zhsX?fQje9X+F8vn4ldymxRugrL^^PMgdginz|j6Yw`ky6!usGLr`T1s$=q3SPQgPc z-)L|AN8LPojTcUPu}!7OGUY$rD+7EfcZE~UeTMt6ShLq13gA^VrRmlnc)Vmr(tQ-j zuu@F_a0v)pIr(@Zky6^Pm;L+EK`@sUOMDBJ(~?1Fc*#3;T}aE<$3E@dhwvQTC)Jjn z(>&`y_#k{=xnjQ6_QzX|E6{Dhp7jp$B|iaCT6YC#r!N89*Be-hx(`xd98u>ot=ENq zoSydHt~cC0Em}~qzagNjvzp>BZSQ0U@p#ImpGzC`NRwu=4{|Y()h*HKdCfdn5lvj$ z2Ex2u+o}=^{o}SjCO9d+nu{z$Z)5aCY5z0^= zbe4}eo{^V96Kr!UF#>L4zMcdJ;C$}xpeTo3^x0a2a0U!_^!hpi%*$B& z$8N{h@B;AZ=dK+@SSjVdt zRa{*cmS+5*o-|secO_~vmbHHZ%ZQd7E72ATJVD?~3+%B!oFR}{paRnpURBg54o^#|uOkl|h#@QTc2bRa4A@!~ zIkxpU>Rq$XJ~W-EA#F%NW|%8e)0Xb@BS;C&>$j|JddDtz2aHc#-@CZI8Q*k}KAUk` ziX@clj4bJ1>2R90-lFHzvv7=YSn{^4-mGvxjRLnyPYan?Kn)ja+<&g1oom_0_}WNI zYPr~});=dQLsqS?&XR2H*l}mIepPqv3QHYF!$Vpq7fSRhr5va=dp}uJGmykZguP*d zM@EtCEV9zu^_d(=bE2$TY{ zh1?V(uZOA>GhoY7ydx{@Pw%IJza0Z21ckm;8h<47NaZvVTS>Ulrk4_hO3JR`Wo}o^ z3G%`Eo4ivgc%E}AHtQAd&sNkadcrQG5S=at-?Wh)REL)Kt5(y-#!JiS)SoVW+yTAr z_$}>V-zRtEx1vAVHp!!Df(p(Ka`>4rg^$7LUFbU+|KrM5D&PF0gy{Dv%xNQqsa9e_ z#D_GwoQpP+!g^O@M$b@(;?Xid2Q#O$H^t zD!!6Gj`K)ZxE($uBBV0iwu(PU@~=DUJ!Gs{e7WSbuy`VnaF@5Nq2&q0cKfap3es7{ z6n3Z{#n9cOsq^L574>y)CC-yoMHJXXu~xQmNMus<)KAU)_Am8KU)XPBmyK#Fu2{Dv z0dD!?B3{c0i`oV|C-~SLqp)#teiD6P1^FTqo|(s!Sk4aJVA0CRA|`GyVuw}VyPQIA zIx$%qgmhL#vPGW7)0!e7)e}==C<4H+N6^X1%W0L*u;lV(tlXerWnO(2c|*M8;hS~3;2o^ zL&&s0hWnN9M4poBq&Md;M^@V(k}8TP6_`{kY=TW!EK=+Cm(m}#Rvy?b31tj2P83A^ zuC%4|T?(m7%RSHlEZ@j=rw>b#!(1RW$>N#t(b-Yu|9Qe=fnZU>89^X?Ha6b8>3*-+AR~F4{G(tGh)?V`pH4YYx#}gNvaNaO*BYd(V z0xqgvqY?f}+6x}ZCDj-428<`k+^x0QdzZ3$M8b>IlYaXbE+glgO^_g1qUvQ{hp$C` z0(}C2uMy%NYTuR(L@5L1WDDZsDsztqRHUFE32-|CmQ0 zf>^nx3GP2v5fc_>jE!Bj4N>Dmpj6Zo3P&Dca5pP`@$}IZBASk)o)5|y0}5FpiieFR zOG^^&*mbkPmFBqtA+VtTkO68_5v%9Q(|^$~K6}PE(%eeQrlly+(51g!XdDe~y=J*T zD^D-bdx3`$j6Pkj7j$)H3JmXx~^S_0?RBqq?9kNUVgWh?~}E27>d-uhPs z@NL|Sjw66fnyP>s&=lJQnA^S4l#ql^+(J*<6v1C-cfsJqr+K}Nyx0-1+g>1bsf=O_BVL+={%v(0X1u{#XJeT2e2@Me&=x8^l#Iak}>} zAsg6D15E7nUe*Q#zV6;UaC$KC15WH-@=eaBrJTvz>mADVo9y_^Fjcie<-FX!Vhjm@ zxv_s#7p3bnK|;YOl^MCYsa%xMz4IY}&5|A3Y+n%*jQ-o7?|vDsVPW(-!Rl^6@c1BB z{L7V@Tf(XpT@xQ`J1qsRPA*UwiDbY5Lduengi{(nx9akujr;HDTd2Cj7nf;(967iA zD`1XSsNCN)fOm7k7|40Jv>b8P-6oc6YBJ~hZIN#wzVwXyk~Y^Wg%#;xJyqXK3(r`6>FZptZMik;TyWU6+3!od*6P>bBs-^ zi7&dAYcv{BOiJrb8iqFazne)y)w{HFoG{~lwkaUg(^hl6^A}}-D=wo-QF9x(`R=J= za+7xD{D`Fok_*_Y$kdjsd?ak{NLucri zOt*8gk;B7^Gz16tpi7YMGJ50et%egL%G-I1@Yd@_Q#+zSQh%JDg0;9-+;!{h6rN{wIfw8%2nj?cYof-VPfH2qK?;E_l?b?8+PI*S0CGhH2%@-9@fFrZ^ zbTSct(tIF(ghF0OjC^t?TrA*`xULS2kxffcA~aXY0^rVqS%<(td#5!%ucNL4LA9MNg+^p$QV4? zF?@EH>B#3KE9}e?3^oH?x=JEQrt-fMU73XcwW}c%*v~VtQ>)#-I|9gfC5`OZE)?v7 znTt$X3c__9VlNgr4Gc9~QdB%4Nlh=Dy{>E55#e=7$f}-i<=hdyYo6oK(jf!(cUR)W6x%tuT|oVmI*z)F2Y)ahxv%5JlLv(0Hv8|SL_GR)fQ9g#R89fa-8+|d$B z%DvB`b{%UX;Lgd*c;Mru$eFOd2*JGwP^Ya_yJAEuq#S-zzRpy zM`NySo2a@bR%etp(Dw?B0hmk-B-wVUY{Apaz#QYI0{*ekoaCg;uWkwVhFHz>N`;U6va`bFHaa$FrC%0;d)A`-x; z(-*0UopvLQOg{*{2S^=5h$q8Y8`ci1vu$lcVclL zWataLs&@wB_?O3`bFzmGIbFuA^npZtJI21Tqt$*SIcAj1UlHhMySEI(hbquP>0H|MB|S?YqiOSkd$8zI7g z9jpm{2&FmUWkeu!vVV#Pa~bvela;A!hS+?T_?mr(?s=E7FHYL&RsTo}AX#O4 zfwT&Bvq{`Nxi3^+T_Bm7UMhQVC66<){x~Qn?)}PrK>Fx-fhPWau4#DAow0V?Qj-u= zP^5m~>$XVrZ&V}FlW*m4{CXN8EwjBQ;Lx& z738ZNp*Hgqast7sx)}0hD`H*egheE@d@AvI9|oHtq1&nQEr~3}LY= zKZ2*mdG$sI^*|GE*W_&q?MKH7E#wO{6R$ofE1H>dSB{ii%&xR*vj-S-m-m1X*FqsLd*6BI@v1j! z6A(;pG5lT-fQZ)o=!MYmty`A-axNE#yW9edU7jjhN_`G5YFH zhkO~xhzb!vO?NffBXUsto}20U@Pa-^?_DDrV&PT2$wrga{rO(>;z<&d(DA(RR->3k z@3VvW3C2-f)6xX@(~k_sdx##R1!A82(2QiRuqr_*Rw)js5+KQF2pDNQHC8Z}W>Rtu z5X=tgoA;tOm3&-Epn*NWd>J)H9Yyx{s@$= ztbrbhU-;bgNVrK90GBTwaG8qSl^~M+S3vk@@LT<-} zA}_tcvIyBH4l|^l)9KjrYm!?*XQw0B5RZ*HqKIyVNwDQ5<;hu~KT*Fae z?`(vZ8Byt)MaX0eDVUboKv)W!xt7-C0s)!3o1zZdS6?9`6vi25%wcWBsNOuG=m<5_ znypi;1#)26`xzVoYBMM2&DIVv?s}V zSa0&S9r!Iz_5+q{(^61QqTdJ4N;k76Z)mQEZwBu?$UbDRo{wkmX9QV8CzkFsbYzkv zhMq}<7p3-4(72{gd8$D!VAsQjK7xqj$L457Wi6-xwrXb`W`Ra2h3HBs8u$zEVf$T} zh)B>dwlTbzNj6OCL4I)a%-@RsbXwcQp2#oC2S7QH6;IB^~r~ zC{Q|^_on#7v(F<~pVK@or2*7ohBQV`Nj#@?^(ZK3yMD(D26+jND%{D1n9TU4A@$8w zVnRAZhH>`$hY_6=*esEVKuf=PhO;hln16@NU=~#dwx@+Qe$O0asVZLF#=9nb^{m!3 zv?(U4u}o7~iAGupS_hC1Y<4!UZVM4ibAe=zhD|}=yqxQCZY{4^#R;2SflOIcd~VNH z*}9vU!cJ`-L2G}0j3Z5aFA^g7*HAFo1XO9Z;~hVxNR?M5UDb`L{U&lSMkzvQP?TQccRh5VGbHVddaQbd9`gr`%yebUpQTfVod0>>9{Y=B$ z=*idu)DHVZ-bzQmE;{|?f*hIV zy0LHU`k*n*A9sg#M#SA)uif-N$ymjSEt<_)NvN5Gk17=Uu~}|3RkYgmrVAY6bl(o; z-4@@=a4p!#TP>!ul8@AxV2g1tt&0eZo1~%{bt<^;KI1SnO(48W>$(ZK$-t+BG8mi+R zYMG;zmJgj(3H1`iX-R21TUx6VmJQZ^`iP1VS|murW;MAHW%L$VTQd$WT|0E5&)ALG zYK+Ns1#;dhI1lLR1@r1J+GrNVm>&F!^#31BT_%P=f36i#XTfYDYv$UrNLFm=Z~SU1BZ-u5 z$QzsgyA|5o_bhK>-#8{+%tRshTa_tc?<29`=b|6e@s|Hso9y|d&yLtvqx=fy`zC~- z#oJY?Gy~Oeq@2x}w6cWdWLZ=KbFI{I%xiDn!B;YLALN~e_NhXR*ot)Oww1s>4~kkL zwr6|il?RJ&KjXq&SW~GT*B&{;0CYju;;JH~dz{3R%P`2N?wSCl!;s8t;5H3uV1`g< zF`|Zzy>iY9ODT#n1$c`RAvE>d^_3M>Qx@RTqhjWhiq2SaT^@fkq$oA zc;s85_|dLa{ZR_!hGVCWuEUv^(iydmn-#FdS+3*qag5rDO5G{cFwD1uG*3B3dN3WB ziyk_d0`-h>s9;U__HeUX9T&b7Arfb^;sH5ycq2Wryj;tY7NAfc5;;&b(~|>7ocSf( z&-m0e>aYvK5rTGJSOrgC13yq2GGO}Pec@akn~zsjx*5XI$7nxx_pcV6`kMWNH4p<= z4Se1Fm;w_>{4XAZ;4oTbd3y03?rdM`FfV`>35D9a?P=(r84LM5%a!)t?H~i2aX5NC z(EPCOsPlPo!qx4&XdPq`UR4HipA4v~2Lb@Cqqg6Le8*$ARgoMaZbf)9H#ey_61!Yt|}?rf%=;Ro(z2aFDoYNz*JQt6q}v zD9t->p8fcBurP~VAmy=uo=lzmfRKpYN7y-e4hCBN7^=x@#cq>`DTJ2YQmFn7m z{IS5@HZIIb2D>mRD#_UakNxYbK3{RuH8dQ#8O5ZQDwvi=7m@b~LP$=Cmfi}=^S0<8hz;y-Wu=UHYFP+j@w&wrlTvg!jnh@4D` z{s26)fO&QhEmG-5NA}Mf3gy-vtBnALL;{bgJw2LTUFM|n90szmc>QO4l&F%$`0Z;0 z2`O!nf{em`z}c~x&1{FOEGSW>8q0dZs1)N(sB8Ymd*4^`zwKN97h-_10H>qgUUva4 zVSX$Akp1XwW6^tepVa)bRVBc)|F?78i1gpR9axoKoLotQvCO1nUhPHn%|k?>?+Z#p zA^gA2vG41DuFceogS8S5M<3O{w5K}NBfQH|3)tndZ$TYrJ>q-_TXQzML8q9V$XxuI zIc({!jiAWEd;jePb1m?%iyuh&U)MF&qs;=RpK8M*KO4vfS4sm)ayB3+rK5>s@buEr zox{-cVj$k6=7rUIe}S-)BNJh2%*B4)G40o{B&yW;PiLs$^cVB#KT}_t^bT zwY>v+O5>#WuG6P&2c|plE~Qi?qoh@9Cyi}d;}=)n-Hd!Sx~8{)hr@q;W+u!3z~umt zjcK*=e^h{>C>p=&<5Cyf-DQ|f{#eemzKVFjQ(gvksO>k3&eo$}q0;C0u>ZKrsfPGg zB^_d!A*hP#i>4=90nOtysLxAC?{Ldia z$*mG8icaLKIor5}rW<#EuDAFt9*_Uw)++lPimVp?6$=MaM*a_{1oy5iHB{Xm48SK< zs}+v%eLV21jq=;5@<(z{S}-GCPR=xMaozV}48xq6oKgbU~Z*$q?$GV(hEjEpOpHT~-N!7%c=RqQBX zk?1Bq?$`Cp4E}G!1$z)w59cD-;u!jMFFCr$wgFOBZVvrZ{g}N-5qtMVKit80)%kDW z*~tlh8KcIqLC%vO5z4)H1I!jCpNJe*4hmtGetouVxWd2hA%WpvRZmYC`X)2{9s{*N zWd$m!yqo~f$443T#d6J;<}|gepLxuj71B1%n|oa?9eRCYQ#l2HlDvceFi^0*_}S^I z-QcO~zkm8n0mHBF0v>s4dE-xA%9z`m+Q7^9;MVvVV7FuL7m*B+8}HrCYMt^83;upL zH{E|*wCu-C4}1$Rb5SJ2np-jQu6`!%N!Om(xTpRpFP3z_+77IlCztz2UiTR3CF>EFRA%^wQAUH+QmU2XI?P7%`q z5{3HPow__lp!It=8OyhITU^q=&RaITOx~ zHgV1QcG|Cyn8PJiZz7{sLzuRPY+>mOWW7tetT@~QHdLt#xb$3eufvwhFJv?{gr5&)+_DI7_ z8~GtXMdy~5XL@{9ywt{^=4$8zhhq=PYV*$gH^vEKfSEH|tLqRj_p3QgyN(9mErmW^ zv9`*n=(#||Z93BkBAV*J7zdc`#6=}XNBi$_<@I{%^uF=X*N5UOe>EkMZTtBuoIv6> z7@v@Y-5~i1$m*k0CbwISa3*BjDUGA6@Xq+3F$hrAFEv_L58e*Ft%C1!H;r99n$*u0LLIeOAuk)ofE>8MaDv$y2P#06PnAV<}7YLQ+qqmm0Z=| zF6n1ak4;T>ljs;rh>jQ-qnE=ZX>~^50TGh^dS15_$u(L~W3*q<8Z^MxK{1rz@UIN6ZO51}YC;f9Ubr++7a^ zs$6sDaQcInQi4tak)5$4&tDnWQxG>%>C!tX5*&JwO*V+t`$64@k@J- zN~~31hriQ&8+|KKF}3>XT`LTW;#_^=BDP&VvjqRqb9E#ICIxue2!Nx?)2~Y2Bko)| zHK@;p#ogL@3DaX;C^9+sr^@381_(;lFpoUupEkXX!^e>%?{i74+S<@2d=)R7tfQi} zJM5I!F3?na#!7!z#`*xCY9+n?q?iRyQxi;a8aWUcP z`}*AbdEa<+l;1(D70!7HDZ9DoosUJ&@ec_7CZn2K=PrC|I|mgYy9RO_OdTyf$Y};- zNFQcrn9M;ws^v36S*}NIww=aDI}<7IaPj4;>#O7Wx|SE2m1;rh{p*tmDHA(1UB-p> zYx?r)Mg=P*KS>GaI?2xC2Op#N_-F3CNGk;@QxXzi3=q9U-J_|9n-lzr)grpa4C^d6 zfrX7FVT_!r^kl0x^Z_ z8sqbe+9!Jz8l{D#B1qE7Fm$8+ zdawwpR#?EZn5dD6-_{bSD-M+f*e7U9Z&{S#KC*HAp;e4|rpJhX%0t27ViiNeQq=Ym zZcdq(=|SGTCJo?sk$RCEe7nKH#c^P=YuN0ij0;RVH4fgdZ#^SVEm;M$kuPL2T2YC5 zhAGIcPXO;??5k%aetKh~&WrP$oG^PKrw)wkP({B^thFoZNUUAwAx&(`TP{#2aAxVE zwLW&?H?1$KyFTTrp_$tSccCe>Z9?YhMV=up5M{>KNo(Cy!xw;<{kG?sTP7!!Zrt}? zitcUbP__AW@BbO+=yL)&^zbkp1D)Kj*;?_&qr5R&zaNR4W`BdnP)rDVdwYUGs_c5Vn@YD`NmvVV-&!L|%2G{7jhh?qT$G{ZGZRh5Ci8k(;s1 zb50|yZm~KGK(QI)(w$p0S|$mKPaln8Ygf2eA;NJma$z8`+r2s~?2-|K&E>~_#4Ywm z2?oK1IvK_VZh0$#{N)i}ruwIsm+g-_weJmPM?SaLj1gWzR_U|LaGBZ}dT~N;EjtX@ z$ZAdFuX`WlGFLYm{l*AKARlz(xhcebINx0ct-jAcp~(NbKTV#aJOim4BS|4_5qHe8 z^zy~rLjT%|-L}*Y;9u_qvlXe_WHNzE(w}tUt`SjvkoqLXY|mYPf+2|MIO5Ihag+vC4z2paGiH4BV=izNRuCcR_|HDDW# z!h^a9g-T3%W!KO(9xNBL4pf?70pzC4;>4TtLJ>??y>LQ4Dv+Cx)z6AE*oHg>3&Xr? z-q+e1%;6)I+{!drifmJ|R|i)a zCxZJ(D*a|V zHd9RKee*r!3T=2;S6^htYwtq?<6?JGG`qlXjl}kQ?Q?H+h+0z4M-EFutmi^iNV)&j z-21D~#Qsp)ih7GFj7pfV8z#3qU9?qAq9}e#{36~HSGM_@9%Ev^4r+c;;MU_ki5M0eseR!(vve`pK0`qvnBu-SXzSZ zsfL0qf8a6mD(inbcr91OPFB*NF%gn@ zx4{<(n#f0$Ath-Ub>dxG#W5$z2mAK{gc3=F<`$PGTKxD9V1QF+NXm(<&5KDnfgH7u zngVi4aH^29jNzxrBDdN8@uI`wdC)T^v2Ghl-&r$liU@B`z_0T3O|4msBrd6QZLw%8 zcF9N*Fc8Q%i*ULQeM=}nMn{+6(&O=VjitL-s*3JuKZyRu9~Do~(|#T9X#I8%S-?aJ z2yrUM<%#u+w@OYr;+P4#icQN>aM$QHEKUgkL=fJF!H-fo|^UN@EMLYY(6& z&r}W5VJmJFG4(ccN1&DH`NPO45s5(Kc+Tx$Te7tHmusDCTZq)jRFV5?#8O@4HNd6* z9IT_Kb(h_Cvd90mm8i#1!@WrJKu6Hb(CoKr8>j8#nYU3w`|?70;!f!^Uey(t9Z4N8 z|6A1jpCK*1tej^LdK)-Yp^zJ&s>By3jQ14f(O+j0s@A&#P*RYzyrmySUAU+-6l^Sg;Y&5xE8k7=H6K=9T5rWML zWM=y*3ly7>mu1`(c?x*^alL6^;yAD2EcgTUwyxJnGBCdd?Tq@!EK7y7pp;iZ-Mw;; zvM~*&5FWEU9Xs0JBf$eqCQKk78X{Xq%;$VSfRJOuCC05=|AUPyx61xI0*F!adCuWh zFUsegWU^nR%#N2W8zHLey~f4dQYK{dPr_k**y!T8KOqre`e9ya`i|*as|Ey9Q{MrV zQ&ZUdul@==%3c=f$DLz>V{#e&_a6nA9l*-T{cY272TlVRhcfgVVdpQ$xJQbuTd_ze zfhtgUD65qiU*_AZD`$J;ISl&vRNIX+SKL=}e4RgB{7krBE05b-o_9exrv5eSH_%bK z-yySQ%I?dRLAInK9MU2SbK)8Zk;neEUq~496?sr+2Rj5A!lCY{=k5R_VL1rt#vo9; z*tEhu{z0T)@@CzQSW80c6AeC5%*T0ki}|;Xw~;NWy`JAdCfZw`R!4-RBqjOGK2)ML zSPe_;l+@|}lQ&_cewmWP>cF~UyO=Al<865V4J@L(aVG5V z(%R}>5X`P`i9`1&j}Uz^_1Z8v2oZVA04Xz!9O z{oU29TGz5GJ-_yP@>yN+jxCtx3`c)O{#jq)Ej}ZH5ls5~Gq`WS-pgDNc1$DZlA&fy zLajm5*0{<}lW{~(6~iMw<%w9t9_PX&V77S5wP6CIEN#-rU;|nuQ~6-RJrBrIayY!- z{&FxfXbmedrH^Ok8r^o^5dc}zlNWI1$pW*mzd7$h)oVKX2Yk50F}rld|07D`dI2ml z5tqCWnc;8UV=3-2AnCC_#Wh*3ML|iSE2W(L$(2lV)T1F4vSg8NNy*-HxD*>pa#U;O zQq<@|9iWf_lg)Et^m$uzYJWt!82cn%gr20anOfH@=A5SoHmOzYtjgzNs#cui>G5Gv z@<*ZD8WjQ2#@ckBo*`Jr9D_Rkfc{eqWJOSJ@#hzeFbU@evPn z?LDUTDTlQ}(JaQaf>KV%bCgInN9TsaA-m_;V{osc?$(O)mBXRkfQgpG`g;>KYAY15 zE2RZ^lt?jRt`S%+F*eky-Nwi=y+HTRzP=94&S1yhjBq-^@b{mpl z04W4mPelf>5nze$4BGY zsgKe?PIN?HZ-9Llw009%A-@T|G&?=4`E6X`%b=cZyI*U@0UpZ%@5(~e(oN*HST9hX zDHS8F4v8R;6%G6v$Ju9%E7$_pr|lQIGoFVCON4;X9qlAAkBS|VJh-9;LRIUW{y@zW zP~b22ZvcdjiekAJg`;`#or;;Q?^JrWFK()YW_h3lEv-_&+T;7&JY(mbM7+OURZ}TE zZ9I>*A2;Z!^NY(n{&^j`1W>h;{!PP+=|f`CYPYx}~nT_VbXT)_gOp5=Za_b#i4bLdXY1Nvp_x zGaun^To!276RxUPS!g4G^m!DTl7XxKGpWINu9Tg(J>?qR*~%76=9e~slG7Bo_!Nh_ z&SF%y%_egp;WP0KOcP!Qa=Rz>^>rQ|)a#G2YV)cUBqXV@Nab{rccUe5t}!YBY(R#b zFHc%It<@r~hN@zIpyaj|DU063L6QwJK&(W4cYxX8_|C3pg9orchLmOXVBL6`fZcD; z@S$%&ap9bq4D|J&p*I!@v}Zzu!_8Z8Lu)gpl{@h6(r@g{pIt7jZSCR_pS(>s{!S4B zN-$yI?!PP+A_BJd+9`U_wi~^%RkP`MKpg9pqV$W|$}z`7bZ!Y-A)& z*To*~Al$yh-^f*5s_**rvOqIgFxOk*e*3k{QQ;evQSjLssRCTFe%)`PcI6RYvqCF{ zM@(c2$9HnK=^2$>dliOnJAht;x?La4w&Dj+4WKMw7RNg%GXI@Do`9SRmNuYYVR$ zm;f^K*@7+lK|$*Sf8CvH=x<9d$_5P@+YF5PQ&9l?v%^48PZJ;6u}kLC_)TsYLEgf` zLLQ)72sf8SS8Z>7_zC+uo&~3u$7e|7M!NLZ?QM38dtCUM0XOfohPmU*6PIbUj6 zh(t_$bqlR@uumPZAM;q-BBRUv@E9|TI)ASV*H&EB zOdtt3<{}%H*OQX34_@g25y*iA-Nh7!aUmYZw29G_!Z$^i9=vMpe19TycJmCra!A?X zxJ;N?0pr{Us7X^OO`DH@-9~}O8hPj251UhVALg$ftrOas8cI^5Z{nY;AxPbnjeUEF zC>AJJi^niA1<+*n`3t=vX7O^b(oYl1j*c=i=6%D|(ZwIqKuo5Si5z*9Aq_QFNTT^9 zHM%okIv+c0!-yzZzI8{kk6P4QmY6<#X*U0S4t-4FU0wg0_%3~H7mt?W~b>&8e~=P$y9}&YQQR)^{T;f{{<^$d0U7qcj*^+wY~2RAm=lTugW z-d0YjvWM8&veH^pPGrMv^W0}tCpEYrBBrWU5qkR7U5t%CrDd$UnBK>e3x3Zhh!{WWVgMrM+it6aMz^<2!}&7u8#XPxcb^CXNplG7gTq zpdWWzS321$ulI4jNx*pZI6M-(!Ot3<;i|TU4!PJfpBx)`G4he1F2!u;=14Dfmg*n; z9>1S-6bWm(^S9B7#(pU|O|x@L32o~;K%npNR;8}Q5Lr~THX#M;wi+>mff@D{fAJ3xJ)T@5fzx`S z3f1~@uji>J&1dvizQQ75^w&lSj*j8Z>=AA1-9B0G@xjr#VQM6@b;czO)hI2D2RwCReD#7D82WlQdD}A4$^xKy(@y! zRiuWZ(xms^drJsKLhm5~0|W>?loQu-f8SnvpYJ;V&iu%m|i#|xOBQq^xduY}XC(~KUE{X$F()myJecg%xR9ZYcW z1`=5rJKIzB=NXrq05gYIBH1Cc;Pp!;4#X>B1_NvXK3h6m)OovGiEN?B< zMP^&DAh)Upkh>v1Td$b~*?gYm@4BiNReNwF2$w(%gvcjM`j90M;U~YW8X8NxWFi&m z5C%A-o}NApuJX#keWWgZK7pEQ@aF=7y^iJuSGRCaabcylwlO}qq&Fgp+gZd$x>7qL z>`^QrRUqL0R|*Pvx%EkRQ7^yg!1M@Mj(7C+_`5fUVLt9t+`%y)^mJeVZX-F4s1N-g zuL5Lp&repM80++Q*7(+#ryn~6P<#|;Hcsw0Tsw=}KtO>slNKVInYd)eK~IafH3id& z=umGx2a>PffED7b|7;uD9=g3l*K%6)ORXNOH+)+JjW?=qyS|FyCtlDi_`xK}2&o<| z`R*!AE81}QLaeW&zaTi3!!%P;f0Cy4`6$)T?)xLbqbW5gW~QP0i-IAp1gz5ZNiFT3 zdWK-DX}jEI)^l&Mr=M`MqVI^$iJ`m?9qBRcA_c6?-X7vhXRO7MAp_cA_6!ruz`+Ct zITOGhyN+meq^4A?`Fg)QNSR{w+f!G|P#-Y`MQqDE&f`x66pu;dBu4q)!ALHu!5pII z$DTxkh^|~UmS{~w$%Nb?L{@8{O3P)s(t!FTa(_TiVf(sb4PFH5q&7=oUE|QvRc&_x zA))ekZP}vKX4XqQkzeSg+vu9XGSQk*$4Tlw69&vMQA^z3HD`!xaV~3h3o|n^`kD1n z^1QNN@kC;3WWTJx+mMtme>CS+zG2i!qGh+LZhD6eQ%h>^y{(+S4)9A`@Y`YeelDFe z2#$ycBg+v+;AiRfuU7-3-UMI6Lo5sqGM-+Z!-Ght#q03{%LIN<2!Vn7aSDvcM>I$w4zdP zyh;6n_?Z|Phu>*705(fvSSsk)MEJ{{L!2$|yD7Zz`N`!;$4IZBXm7n@zXac5#2H-Z z9qz$l+N9ZPR1e1a^r_#AKrDUAio`|yS^-`1ZENfq%S{I91JYcdFN73Jt4(6V{S zT1O6~0k8FCv2C!3HET~8;(&W=OP?lX*gn9WraV`B|UFmBw}dS3!M=Vs61Tw{w8zVL>HG?hKJR-5`-_;Wau}>nSO{^+LhXr*GRuWe7a%nyasm**{o~v?w7_ zJc_sMuBjqI%}0EA)!t7WHkyBsOqWUA(I){ZO(+h%b%Tk@h=D-sYIcEm!7A!KH}7Vk zn+PWao=RiK#V0Ekal>IVEvqIapd>!VRePP2e9IRvgpfr#0?{ovYEM&ER{6Q)!*j4d z`HWZwg1USiscA3p&HYiR?=%^#8c&kB9PhgO;u#$+4Fu|HcQp2zAE)|+oowEn7A^FW zrflnI{WAmJKUevKUc_4HL#j~xh&MP6H*zI6`qH=L- z`xmhQQOs&?;@SrXDlkwukYVaRX)ii8QX|(j^9vCOyGs-HYki(5e68A1FmD%>m}DdU zrg!_RkuIRR`wFzStO9-^Z;|>0oA>b*yTxShYHvVogA&BcT$SBi{W9P}ZONC~`YB%0 zBY4?2S4UIJe)mu-`CzRmGDN;L&4?lHH-ZPCsz`216_e>d-ReZg^o;f~FF*Th;fqJE zfzb^!yKgk3raX<+Oks`ds!2i@=lgCC2X$>rD|Pl z6idc7c*o;Wau&`zFB9NF!q5beYhRy>1K&bPkxvMOcP{d>Fy&6;bd%lIeN1WO+_(!p*wKJG}9;Va+*ee;Y=!k&nG#0q{-Tc1D9z{_2f zu3Xj)yEP`Zz(`wbPar*lgy>2eI=7?1-6kz0^ltOBrSMPvS3UUK3-vVNdqIJXeACSY zmv3>7s~t5-hAikVx2D@_Bqh#uu&^(mYMFGE`})MbJ!uYg4&h<24H2UanfcAX%!PB8EZ_@TJY!V)cPC9sO^Z zq~nUeA4!vIPYuW5sE{&xxCkSXLy=|?*<17Dg)=0Uk#A5an0O4m^7^0kw$*@iK793e|WHDi1tHex8<*%V68zn zU+1(l>0o|fI~M}=ZwBCBo0{Q{Ta2l8wPF3RE?H%`J_`puo}sJkrgc%UcHqgL*R5}v z^IZ%1+0R^7WihmPM%xVOm)ZgPkFZs`2c!4Z9PWPXDMpyH4H=*9%`q?7$F;7WE);=E z`c+f(3M9SK4!uge^pwc7wc%)Hb5N^|bIVYBYOmLMjcW(4oxcYgU}Ot|r%EAU&iduS zW#+B-H^ve^@b&8*TqWBbC2E~3N+i$srcLLzkhd-#saw5B{;g0|lM|WERd!S9o-4_LT&2Jp8_j|K>|~zf3+lfNKRsi|*Sz6__9bM7Jx2?V9NoTS zfCDx>cJYzZT+#|;i)>sB8GzgQ zeip(qC*xLPO_=PQj!#W2bQWWmySq=`fs4SCeu4JKL~yW?y&AzufuHEEm2ORFYbI4C zddfb{hxYy#LhGO}uQGYG)sEjVvF{9h9Ry>vCn24k?fsIj6DG>nEr69tMK*s&bZz`Eb>VQ3EojlSp6SkW<4Vx!_7v*bPgf1~u5TPuRtBNkql9^~ z@yR(Eh>nJL6BBcMtX@Sktq!xc?E5)xHh$fTQOj$Ms}_r|ysV!BCoR5an9GwOpVXe! zz5vinuN_(c2EzYsXQEmjy0AM*m*a}cyy^g^n~wbV&&sM~YPwFVX03aMU$r|dM?V5+ zIZu@68m2hXeMkciw-V~gl62yKT^H#06M0D5N;UEWKWNA|Aj9e)Vf*zGhGCJEy}mk9 z;9|pYZZ7=7r#C^Pk|<%H)v0vWP1E{(-4isQ^z0Lw6$jxZ@$pNiFaZ2ZNQ3@XdPL+7 z(%;%rUuD-H>N57|e2QSua@=Cg(7RVXOinaXY2(P=gD8nbtR$hG3V9V`Mm8|m)ZYo| ze4k_KU#Z)IWn(hoNL+1MLE(617_tq!roAQQJu!aA{3=G-O-rW1z@net%O+o5flMzF zlWkfU#rqgFGnCa9q^h$ByjpHs?QJgg;~+B?`{k0KH*fHQR5f1zWEUpl{f(PCcmWIr zX>UXwByH}5C6;RXYG^bi%A}$S%@t+EK(|wv66}ADFi7tHH2>1{d1e4X`=%}&_oA)? z8E+bs0OBWP2=t3y2g|?H+J=jonJ?eVbP+qGi@HfGeh>hEfRxNXW4e&6p|^&7iu<7~ z3aWpmLXRjrL!QpRyZITAMGw$T7Y+REIqHP$JSFcB(26d-8AY7c-ReqbTfZn_usEnH z*pueweGhMdR2cG@t3vF2-%IaHRfJKMAd#@U+Rx*m$8Heyn$&@D9|y*&+*@c2UAcHx zaXdr;He<+E3Y5%A8$dKNv!nn9g{GCZi(0*}Uq`8b$h}_l%;`V2Mim_6^THel4BK$hN|S(!R<;?##Go&_q2`f2TLnn0;&N#!`orfGEN<2Nygp2V`^EGC z{I`p4`WoN3QaYn^F&4p?$o`Nb%QK zb%>AMPbY`t8O5mWbUPxEy0d#TGo&o{`K3#26mtFic}A8KzvTlvjsNx%%V72${ZCG3 zae|l!G^ucK728B;QHV7o{6k<})^u}=3_c9CGW$N0Kr(QZhVlbTK;{saz^%U81ly3O zae_Z)95H2RrUNm})ulq{Ct?u>!JH#&Gj4A9PWls2TJMBb;roz66cI_=&ml*3fCqe$ z&FG8u#pVu+VvlotNJ3g;LTVYt06zd&=;?0i=rDHHl$gVXgLOhVNmfor05dNhQOIRk z_KD6pe!qOp3Gq$XE?UY(VEQZv+USIC_-*^2wyN))JpHugF zi7_{C=B^h}oAYEfS&rq?jV7p0qY?hN^Eo!=K-7u}hGw{9z@i2@4a+qkHyiQpa ziUa<&9vC|3cgxw_OV@TPuq+Sz75#g%r8^{{fVA}lB!@pavq7IzE-R~#S@IRsAAaGN z2Wu7`oi4|%z)~2!NuKdc(>Z%zHiAT-`Yn6Pm@a>@;B8q934o>x+Z)^Kk2HF&m)77s zmpowl*?sMsM0(BOv>Lz#Kj3#dOAbJmHQ>@p9}_xHDy3#LvxNKZxK)Xnd3V5^V=w5b zSj-^10nVq89Zhmi3qWh>-M^;(uNPCB;>O2beR~{2LK&-fq5Df8wJsjrm*$RLH-fK_ zSKR-eo!LW;Tv6oYWdZh_Z{cAJTYd#g+m?o1=;1DP$)*X2zd3Esa+n%!E=a-#FwQ1z zdr7wxg!$u!IgE_*Bgoo<2~qcypCJP`_K;UZVJK(4can1A3HD;r`t>*$QvFp@jwFyH zV$q0#l#qeD9hFOJjSzLiacBa9TEq4^_xiGkQd2%A?tN%6Psc9B+4MmXk)tyat#7Cs zYK2)Q_2b~kr8I^460MoZZM<~~)#cRBA;JVvP*U5$L)#1PC3;iudkb%NIV#8%J6aYF z@%ouyEz-o9PV-F>z%e+7UT(+cPDS zQU~p`%sXI)qpZ@)v39cShL>B4x0K&KE`9tPW_+HG48UuAqBbxk*oSO8ONXCs0EXX* zIqN?sVhNFZk2t6Ak{yuLGOr^4=pgODg_JJ~b)e|1z)(VYK5%f&NW34Bw$1K%G8EvP zKK4*S%UxN1DZ(QR@WFgY@@+t--UyTLRv&H)UiGJ% zHt;niAXVqvJ%bMeX2%|iwuPE{#S28;Mq}QO_%XKbVGl-}`~8ldjG92^l;e$NehA)W zh<`AZ2|RyiZEg)MuPgP+H<9jNIB|24b#nbO0@aKz?Eu`SL-DQ6ut-;nt=9TPb4m3> zci{+F^bz(@fkyj$2@LS%^jOduKJNG2I~d;F(L;!Q>7&rk$pY}XzFEGTqcEdW^W`F% z>0LUyJk*JR4w|e8`#x!}`#X-Ks0bVW8UtiC;0h89XgZ^P=G|7rvV!#7%ac}Wl7vaS z-PzI4={%omxy~Cw>sd<|gKRql%_o;>+>u|^#A6NsgW?8<%@1)poXb>yb~fFphA+I|tN7!0M-{5F-&I2wb)%68De;mT0bj@yX0qX-Xu zDHRVn`H0y15QDt68`bENdA~jzmYI?E9JjAQD#923f|BkfWpZy4#>zRd_FdCX(1n!m z_cBkTpTeo3KVGuXnkJZKS!3gK>hV|9Mjg3YN65$&6!|$5-|mk!J6khJDseQ4yZ!KR zNa-N1LqEafZsB{MZ$jIQ%V_+``jCMHVI|ll!QMeZ$cR>UW8qy{`d7y^lN&Fn#Q86s zQ-4o6(~!hXf$+Ua)EJ4xzOT4g>&IIPJ*1lBg^50G+%t26Ji+Ybw>fO4shjI7`8*`K zS=W0Eo}YQp9bc6MtZ0JE?wU3UEkCI0xtl2-)(St9CG!9N5Q93LrK|n$&HbdBZ^fHO zvSTR_Dn{Q;7cK^w1fXd%jHfaAJ1^IMZO!7;Mn|*TYLQjus9?gL-B3W2wP#HcPMfIP z3ZjeD99uWQEHVNwre zeK~XIo7(COjA%mH1J*;$m;IxNIqyhr&Aj)rm}fn@ORpiPRDPC=IO)~{%K`L3L$!FV zHLOXAq&WyJjja=lh63vb2J{Jrag_P|;6 zOp_f=()8tE6a~Ef{A{eo`K~z=t+Ia;H^ZHd>!=}jMk~9FwgZ%VogDCw38ORpW36B? zc^DpPp@8!S7x(5X+m)E1`Q<7%g$?ZE$yX0#re+ej0JH9BQSNr-JK=tp6Gu)VTIjm= zy5$^E|LhU%x|79oqaU#S*Bck-^l==2VOG}2$@H9DM-dV7#KSp=*F)}o$%N!v^x3S^ zLOb_o1NHOK*v|T_56zU{)OqHOwH+u{=EzvTbCJ*W&XMYG;YEy8l>pSpKNS~^FS8>3 zC;^wnru?*vZSln|c6+L#jSHL;p;U$G8q5kZQUHi_v*UK}K>Dk~M&I)*eq$5>pc~;VV z$9n2cC`jg&1`Dh}7`>tROTKEE$n}+` zkNm%u`CvrX0}HrG0J?3px#v1m%mf-nIe(+rpVx9qC3}*+^B@+F5#+p)f4@c#-14GJ zU2dR_RgCZIUG%{A9)tTXax#&LU$*O$W7t{=rh4#X|SkWFc+yPJpp^Wxd7Cc%bfaJM+~{v3KDo3x%&V>ZRyL`s+Yw zz{cqorLZ`aA;MfnGALlP&P8MH0sDd7aVzFNP0E0W<=HbB1U3Bm`FfT_(-woW8&_eq z9d;v|FEwgl{UyFykt6033|G0l<&V1Re z8#W_Z>y6)4k+n94zvsA=CZ2_kCfWS8|>1wWb7Y}#kh-m9hw1$KYb zGHnFPY)zeA#xuYOxjkh+Q?+iaJbWUQL;9_d#p7Ezw(#~Nh6@n*^^yq-;ON6*+;uT+ zIVr5Kg{$25*LSi1*`5cnf3I66Q>&Eay()cwkgQ9{z5jKZ$;q2^f{!Y|tx4K94a`Qy zu1Kpqz5S&0eQ_?UcGPCw^-O+I$fivPEhzMq;JBQMIzwt_u!#e_3(iExoSodk=Jx0D z0qZ;V8t37;dx!1}qC$lkfOu;i#o~fD7DM(iC5#mDdybZB0r*qn;kAi0R|!L(wQbsK zZwnM;-28H_vYVZtyt4#JVFwJ(`G2wr_5}Udqa*8`;OrrEhu!X--P^wl@8Xo+FAmU+ zOiW6YsxTJjJQfU1+V{K29@lQ28Qs52)fxNo$D_~80^Q-v7&ae@#d~*f8Aub_V})yW ztq#k&M2ZZ=j!nv~TLNH7R-OHuRpR4Ab8gz}ay5nfh#@-m`L2$2aB$1Y6S(60iT>qO zu6}YP&Vby>YEDCXO}kXztr49_V7ujoHV*$ojkZ(+6jArUt(jm4FK13@8|~9Z;=vI( zB*&&1Tcl0(4qL~go51)WtKAh4P`g*}$UuSkfA)}$_@l+dpl;>(7qlJtY}GLH{^cISzwMm+v&JDr;j9|v{%Y=TMZ?4va84%YINf?! z7xC0cNNl{>#z<-4c}P$kvboDL_+XHmo#p7Fn{#OwUcEz!{q=#uJ$J5<%U(;A|I&%m z^N{Y~-M`E&Haa`6?TfoiS#W7t#(6tWIqt}|$$`BWsg)D$Za>rB`7WN?9Iy&4xB{T8 za-K3MbqdLg8Xw%A)fWjT@?Ywlo%d-Lv1V57ZqAuu3$(e?-rf&v>w4{$a=EKoFi)8+ z{q1MVxD=_&RWdgwy=9fZw(VT}0y6dF!qc|A?u_+d)lR(c-etvOd96*B0JdBtZedMY zbBPPMJ9c%&6YcXW`PRJYWy-@ha9G%twPQ(poGd3;0PaJScvvTfC)ORtqR0?;<KxImF0z`h)xEEApP>^Y|y?R=TX9(~B$Uwvol zYPRMDQ!RwTrM2LRCSqtg%y`;c{zAOqiAI+TKh~}00EM+Z= zUzqhaMLs9L@ggH+IFtT2l9V$k=dV}Ch$sG69$5cjD+U! z)$rTa()NU7l}=#bT{AEja4fSN7!O$p=LC60ZOnDmoQ$rlKsq6uX9FQ&HlA;M(v6|D z6pu%+_g{i_`bVRgOiv27ec0kg$Yp{MK<(YS z)zSC9{7FB)bsoj<+IW&wPd#RBif-$7d7YSFZZDG}!U9i~5#LmX(9u z9i|*2;khJ>`%~)rN|**zW&O2u<~3cT!u>Hv;pcu1q-LB8Zf5yXO&2AIV(?Fur_$fQ0Lr;DX(9g9739)4lYoYj{dWT&$SV567mh?7%^M>L-Fko)6i zs>3>4?4JIJdil1gP*d>mkooBxExIBgG;uPxZ~!j&1@o!CD$A9)eM0bNNW#UmGTnc69AUG8{S-^$ie@V{?l3PA9XI)NuO2 zF5xKWD+>{Y+R8|&{z|%(xTu&6M`LWZPuhUXs}1p`NK+ztolTuup;R=DbfeMTxdkS* zn+nDcQF#Wmo@G^gZt;RviTD=*2Q)4o<$KPM^>y*2^h=7@=+&)d1Nr-oK4!A$4`4|P z4TU;faUe@E3}q_+fX*W40p3@~`?7eZ8C?AK{E4I7I4-?$yS=iz_afbX{(!V!XG1jb zPCZa%3DubfI6>4b(#5^LE^3;HdiSu`csO#k?rxXr`fAD(hy@Y3EXd24HEOTD(?0oQ zrN8Yx*g-Bf=k|{_*mtN)9)NQ?KFo0eNC@GU&zro!+s4q*i@6VlOw;jh*^f%_KRSr5 z@?lbV=5Brdo~lH`-3&*f+UgVe;9cft4hGX=7^uN>le=S?1*(XNZ;@vHzML-MbqZB{(SM+N*iBjEtNj7O@aVqbm^$b^orJr9H$})0R*7 zyof;e=T6ZMSB>W_45kK;wH1FYbL!xXy~bHZnioFvX9JkI2hShoPZ#jo2~>Yz$p*#) zszhm&?5iw%q#pn~*w3TgL`n%(!0s5eZ?-JfpVq?4_&IO;79D+`F1I~ogvNecxutA@ z=P&*>0Awm9>-Z{Rp*AXabd?{6er%U`027cR9F{To1d!g7L;yJ8QJ^hbVtA%IfQI58 zn^k^;raJ;> z1`e<8r^8G&A!Z8Q+ah8!U4;OKrPj&qh~XbEa+u^*g>a_HtmYx0hFAB*tX^#SUZd`H zU)4tkZ1syXL8_Y|MIFe*=XvWRCh4ii*7*tP&j^90=E4%b9Biq(2(530R|k1%3%Qu| zi|$tsS@6n_H^3@k2p#>w_lQyJy_*Jm#!4Zg{t_G@dEde>Rz-9CuQKQL(EVsfm5g+>tGO z9hgxeC*gea6@v5Lcz5#zKl1r^I+@))12^RQF1X#>jkzHnEpNAfnTR+&L1vkD+xVoC z3Xfh;W8ZVjPXoU3) z%5&~-@=4FM(#lrxFDJM><8&U=|KpeTM*o*`OVt{$+PT=TgQWX{NaTpqPfv4$Jq(0f zaosOzEPj23C&u;vn5xeP-JGsnI;$%=!wsKzJpbzKLZLHwl`80?uBgHWLMTs)S@!pC zQF@hjY2gHp*T%e&3OYzt?o&G_r?qq~Qz*7x)VdLdl2i{-pI!!DdS#%Q(Jw7emu<4_ z9nO2Hqq61t#=zE%pR48Tf&FCr-}kfUQb{a#*pvk(#2ok*QDt)&|NVSVJ0P7*dkZ&l z8Mp(7<@%w>euoSzki&SXd-5B$5Wo2+JJ+_`E-OLAJBuMQVO%yUzlwyqRNsGmLh5B1 z5Qnt(w9t9@j?6lgOw;W=>XGy9aRl`eBg1nFpmyCs`v65#pdo8lBIi9Bi8tqosMj!> zY$uhg&LJ1Tgxz{JZU@^&53Z@<1z9?Z^giI~8SL6ZHSipl<_@jA55fCP1AdvoQ%Pmp z;UHy|4BUz0V)Goy_q4zTdQc4AIE-Jk0dx7jv9giEy}kyw5}hNR7W~uEHe5P)`1s^H z7Cvv_W@q}Nig|0xI|%s|I1GFe2edWv``b^CNmF2Z=hPsKs$;i%#U_whC7YE1aa~)V z+{jnnf$g5X1!m7cT2XJ1ct!`uc&)SJq&O@9nt#P37REb*|MmXQm9OB|^aN3O&TNMx zp{yOs+WLXNxZB8tJ@Xo=xVbZ*f0VaM<+D6K!e1&$B>gr|*0@H$h){a2tWWjB+SnRJ zf8=$2iCrA*^}I@M=OH8oT+;#__)y;R zNsr_}eo=^$r~N>xb=ud!-bnmm~F*X%`4M}CsWjf@5Q?&g!+q2LX>C~`#b1^aNx6a zfPs&kQr)=y8&E(y_et%ZPuWc*Mfc_lSO2kFS|Cu~%bR``#~kb;nFbb3sT^7HFzej{ zHugrJ}sSan>r) zSUY-g`xKx#*P;iO8ZfF^(3|h~`L+xE@QlqZq_sk@$&dLx6V;RkKO3M-@N6D)9#Hy& zP+{CLrbb3)REpXu&aaMZ=tY?%h69Ll9L-o_8}#=hw@<|a z;4@&%k)9VQCw~pGKHyA;!i|$KO7|=Y@x$ZO?9V@+1#<@8I-S7Ii{w7ubGlt@C<&OM z+z{qZf^M?CYvu1QOnWwIo`r=%N=oj3O*s+=rV~P>h5W> zF`Sn@*yw7B@8qYfT&=8Yy8)ixI|TRJtiH*7* z*&Lb(aDPNDYHglyRe5IeTjhr&yRo_uw2&Vk*`Lr6mN5Iz9R~aE#5l;Ca6M|_!y86> zaa%RlWX=9E@p3k4dqn)9J7KYg$TQel6TH2P)^kmyI)%0?TbP?#7iUHGZOTdd6qRh_ z@3_2dNb%bNAt|xyawAzWle2kU$;U9Mr^UOOa(w}SiH2k&J5;rAN}Dyt%Y|P7Kp$Ee zxtW`Cve|J(J1sFZO!ec$Th=!AU+_o5LYhIqH^BSI*`*)f7< z6gX_64+B*1y&Zo;M*#U8tF7-9>~h24q(QFzL6<2)tGnRynWbtCs7VxA^AI=TJOiK6 zi^c}Ovrr_>sbY-*i`zzUYr zb+fxu?PpsA#-%35+V3iZ+eAVa5PG5$YZuV4^e4S;7xh?b66`Oj4!@|Uey|IBVpD}4rSqS3V6 zeW3x**hu9UzWO9C*vz~)`Pk0A?X_z;zI-(*O>pXAuXfS+uyU+|aIhmkzsqJq7O@2o zw>^+}(n84_DUDHAs#0-b{vwbMe*t~CD0f71e%D*Vs&@X`*L z%zY+&fzy5++>brxhb9evAIdc<$5>_%3pOHZX%z>OUM_9-!%K}yY;rZp?enc*S3?` zwcxma_8uadmg?oCJck#6CAaCmn}7-bhNspyxJb0fiBF#w$rWtQ>Drgv`4%{D#^+cG z;9*Bo-o*C-S<#X*e}(#5*R)#`d2krg&r+EvKho=J*Q^{Gk!Un{FjkI|(JSY9Jzy!E zXNS&L*Y8dp@D;wgyRJ)i&xeg1>Z`du%ih|Yysv@_B(0D;ZxO;Bdgv!6mV;7(yLwp! ziLqe|CU=edn!V^7{o8m^lT2JDw=Q3P_^{e0bolFifEzIebbX7NNc15^`*XqKFykzV zUL<_={Iv>Tb@)384qIFSOo`^P}RwZRz6> zZ|*_APpq#j3Ff7sMN*9iSH}A>249{JS2o#P+`KUDji%B>zkp3<$}%YTT&1p^9MIdz zwB^DIL)NX&-sfJd*|M`YbCAx3*WU0oW^gXWC-oK6{V*W3DG@-x(9SPV%xqP-=Y0#k zWH^#*K^J?fve+rB;oM?hD8Qctp)PwU!53j8J;wSj`5D)bWe0xt6AY1@d4}SE%4dJ? zL?$c&R*iCfAyNr1bRh4!^}IYVs~4tMQ_YFfXTu(%(ZJE4{Z9T++NyL2;#A_8gD(`U zaQ_O&f{FcC&K68+gZ6HRCj1*hD6Tl2_o;uCb035G>Ez?jN$vMtdd1mcxS?-KX=&VA zm6f(5Va$sV7|lIGRRIil0R?}($Vt_`m*!zQDdaXs&Y;+ka)8l8_GaRq%eZL<`7?lB zXqNi7#nUs`wDhKI)1hI&^^!uWPZie3!mx2>u^?1|VIY9ph;a7F_ZgM7_xbxFX-G!!WDah$=3rfOom$9SB-t-Qkdq^&z=8w4kxinffw6OTd0^n-MQYSib zw@e>uoX0Bd5yq#>ZU?Jj&v<1t`%JHtLITdtb&W&37o?^m?1BY$LIWir=Z_Wr85>W8W^qHl*aF3ovt`2XJ|p6PmFmP6Z}Jbu;Cs_b0^%>f$Yft< zM_^sC)je;w`fL;O{lb>0yiB85)Cd&pM6sUuNoOx-)z1$Xp8!kBct2Jh zTDuALytO^W?EkW0rSk(wy3c5Csb#kU9TRx665}5pxlqEXl7hO@rr=8?E@##b8&BhK zwh`l3%O6+DD;S+-?6o>a!yb z>C3Q$-Z|TIx`^2AX%~Gpm_h#JFw4N0(`PT{y7JMS?Sp^m%MW^<{}SRqHVsVY2TgzN zi^NKiV!9n<@~xY#A?5kH??+vNuZBrnhm*fqf^HXbC5I4hS^Q=s`A(93*F7P-x*!X7 zce2r|_O8W`9KY{I&(y$_n+kBK5fj-|sxT`dY-$MDau&OtyZUoVvBFy~;;?j!ZG^Vc znfyUScO_k)G;XEno!lo2rh%a?bFL0Gqm5i&%AF@Zq)Fi{e;szUpX7WsiHs0DU4qrN z`mqHQ$ZDRJTNC;3pZH5wyyn76FO!|aBs7e#ZQ)%&2nNJ1(;vK4XKje_%|K{vbJ%U4 zkOiyy&NEb$TRSu$RnHHLOqZujInB)}2EYcm2BMaGYYo3%JJbldygMO&*e%sk|I>LW zwnFCl@H;Xq5^`ibYywyuQoxl0+{bs~E>KUZvsUi9G{d7nRscVYmZ(@L< z`w+KZl6-^v**A5NYgiq>Nfg0EO%d^(olMZ5{4-ntx`V{IHG zfwoeOTHcPhc8}YUJ4=dx$a|S4!T0&X$o3hKS+dP8G&m14md7c^pq_CqBm?)$3zi6{ za(f1(+WUP^_sz3B49+UBCkX0!T4Om*&L{dSyz3J?_yv1VDB*=On4&IJ z;ZIiTxoy$fKf}b&dA0h$!_9HrZZG4D*5LVJuH@v8^-pz~Pfu6ZACz#`l?o)90&|=V zYbRocz!=i2Q2hkEm?Y7}tEhIC8w;Qjf)jLu7?k5*Qe>swr~Xgn{QFbW+SyNC2lVHI zA~&q;sD1;}OAn2^gNHs|9U5SnV}CW+XPy7_V*dXArMCZ<4YpnLOY?3kvDSmRLJr9h-BPJcp3_nh~5e!(umSF;uqY~|xc{BLh4&O}6paK>2J zz5#o`NEOecKlepr5{W%A`;S&+XjsBfTBFdVE>5{{A*$9qHe>u|sxR<6rw2?lv`WDyl6Y>y*rYQ;dTBqm4nI3|G5aCoCc*^ zvkEPOfUhW)3#NKCXC2JGGAh|03+{kPK|b;@mcmli4?pX*e$`ctMj+>+jP0XMQ!;8j z06Y!1`=+HT%A4Am#Z3_8ia>@^^ssf|UT>&fd;RAXOO3APYqO_&7O#XQA4ix5ZB69A z5goA}yZ6@G;9&E+&1XPNmPMx?{iB`J7q(X(Sf-WAy~gG+)}+ZY*Qu%;71eq_!o*#! zTE5p3RS}i3bUdJ02y7agNZF2!y?0syC_n!GB7Cw3(j49h0DMgJp%y{+fpe_>2vyV5 z|MEI@@PChLBt{5^tNVzsjFL2fm~Z zUuYT19Dl>n6R{c4MtssVr0|L-arNzbRU`jnNP|ry9pz$rBM@k}>vzZfRo%T!rIeH* z>8*~>dwHh5STfUPyrL|BdBf-*g!{yu@?VLEvNIqL#v|EB@Kfv{gS$Lnf`IW;<+7NQjfNp z<*7bYF|B-BD^Sq-_LtN1*;piFm};o5)R&SW9|Hyo-0TQ*I&MEfxy3Y+Ch1IJlnUC( z`;qHKMQ-%9kBB4raK!kh(x4z56XqYiSD4drI8-`~f(0DortA)P!wTik>eaz#17WqJ zTqS#N>CQ{PIyoet?Y&mv|C;8gdaUc zi>rD4zgfzoLl2}IX6C$&8W$R_Q?mfUY(GiVd2%(&zoA;HVPUrG z7WuvYiT$4RiK++skJq4;0NQ629d8Lot(cqh+NbsKV(P9sFb0d zUK9pJMB~gPjc!vwp)Uv3?T?+g37IpoFdeVbGm{**>{aYScGyeO4YI{D+dHQwYrs)1 zyMS!ep}%+PhGZ#d+WoWxE)_wsb=f7Jd3f06@nhIGbq12lH*d=+?jxRfWlMNOK!-*q z8V(J5<>WSRQ)Z98UiuE%VgedAcU)5ZkYBS74Ew0&RsKF}DcObx^OHo#W7=MI^Asv> zV_I46@Ci7&{X6Ll+LEPjedoqvVCB9Oz|QIV-Ot6*LJ~?E8q5*4q#KPR++ORM)P7un zXeTNEqa#cu4=e7I7gc3dC{K!VWt8VqNV>VU5}EGKhi%zb`Pa_cSM1h_na@_=qocOn z#>7gO#ixGaww3=4PUU)i`2F!Z#UVBE6AV5!2-ruz(K&P6LWU4M=MejpkRmVx{L6w? zLHUwiff%NWPYQ9k1oge1J{4Z+4Ul$rHZjYt3HR!6YPL69?r9YHjpaqT^nQp4?L|k6 z?rx5cxIsG&@~{!Gad_+Hc4oBb?OaoQ(VL$}iRVUeF0BB^1Li-;oo z|Da~Y(Ycg{Od)HITu4xTU3Ae{qFA%HY-gxc{9C^qJLi`6?F(o?mJXuB3=DmOC;7?g z*Z|6^Q{cYZJC8Smc8ej0UYv>8uRq+^PqDRK*`_aatrzd){y{G0UK!RBNfEX^_&I24 z^gmnTm+0MBm84n}SWnx;)flcjDDN6rhM&5*pdKFnoW;L~NR*HTtYe*l&> z3br%v+3P5v?*R`N(HpzI>9AH)34i*q;(1MdN{`0wG5cGVWpGXdjmLyr?u&35_2By_ zinzk|p-(P5w&7z+t#-QpF5-*IbQL+s+f3BdHJ0Gew*x4#r!lraCw%`O(%v#Gs`hRB z9zZ|=2?6O4Lcg?h*;5yF(i3X6O)x9=aJoV1^#L<6U@p-S2%r|9gA3 zciZ0ef#D0oS~F{%=Mnp{|K}8ztf|W>h_>(0Qob+ zrGfe1xLY;TTvO)XFgm^; zVCocdHc2pWh;}2^$xT%vi6TkFxP0Gz^7v9gU)$c#iTHFI{V8i&N2kkiW|)yq1B^S` zEoqYY=QEo*%D{!OBn_m!JFJI#-u?C|JU6y3OwbixJ5*h`#~vUzsa%vfc|nlL7h)8X zWnt9V#{@t7&gi&fx_!!-QnItV#_$|}u9erSL7=%V{HCn01*~`o^)2KJ$swya3ftap zNK+hf3`b3GndZ?!K37tK-2)0Om z_lR0FJ-A%CZwZockkMJbIpIDj`IgFvx|g@?wGg@{n`(rlX^g5MF&$1waiOAr#vKq8 zJw}=g4oX2xy42JU|5sCZ5%Wca-7B4ri|9=&VQj*U9$L2~s8%i1Z|)tNCCIC~G!UafN^P+V-9{$b^u8&szDdNgOZ{f>vI$ zE;*)3GU8;DTmjoq!Wn#@eM#XJc6x$f<>xRXu*S0acs0q#5iMqRp+?T05!Q}fUY7Q& zmgHmkL-k*1YDH$V_jR$KyEITMoUE#T7PKUAUP^whO6bKL78I=%+$$t;)MuxOR(^`k zqi{27P&1S)s9@c42wjZPkb=(e<|L}pZ=!RYO@=rq`R=KN6P+k*S3O&4>36{Dznh`C zo0RG?L_A6f=vw_KKQT|XLizhYZpYucmi>xciScXYmi03FRn zd-!dat)7`qt&pyYj!^Q@s+M*5-lk<8MaJOy7b%ySA7%#e1HsRHfcJRp@_QmB%;1Gv zFYnV&M5e{ji%UMutSf;*RfwGyK;3Y;ek&2faUrl$U;m-)3@@7BOlx9U9AhER#NiQZ zU_=O4`pWIXU<1|#v&$5T<0WlGj58n^zrO|J?i}vNv^BO?y_o?wZB_(4Z90E=XpbT) zs_k{dveK+h-5Ick7bSXY{V}~YfW)S;yCLiECMNyhE?M=~o5AFXuI9|%Bo_D+JFoUd zX7n}XXMqiV;i%*$PF!wc4l}>FFUlHUT6`FPn7d95%a4~I6LPgXD{l)$s zor}WK>bOAq##BmBehEE)9qC>NEAU3}&VA2`xE9l=3;KwvLn3+TPDmX++xcf4sOx6- zoh?zCYKnOk4g2N6ji1n^TX;<_e4D>GnafvOX);R7Stv$oBSdo4N?b^WOp@l38#hbs z1IU}F>Q?F~?^c5tleE7e4(j`a)d&~Qg+_Wit2qZD==FFY2>7RwQEiDu2=ArJa%t^!6tU!V_!9e9XD6#F8?S)s8qlCwgCh%-dZGfx(n2aO&C^jXQeA8* zV=KnzktC8m@RB1iYpg{nY7@oCg$?H*KJ8WchHU>q{<(PvcK1o3r~*{3j1ZRRM-YFn z^xVqf?d-x*!@F;EA8$pXP<=7b?(3WiP<2owZMm<^<*_#NSUt%JtJuba4#E@H$lej9 zb$L(he6_rZUVq*&Qcms5+q%OU5bqT#@%^?2=Ac_B`E%9;)Vy9}K_(n@O!u_juY%gw z1uYpekUJJ*Jkcj-!MFMBg^8%g<2f3W(Pl9;ORHHjz5W5N@6A%Sa}>>c&701Ermg?6 z57INgVr)B!Z>&Ij7D07!wc||Ar%hpMi_qCPELCKjylFp77i;03{381P22E}t<0||2 zw<^jpC$YytlqkLMhvu!bd16%<)hL;Xp_T?J(he)h*&(Sb%%T_{-gh|X3)UHt=;Rp8 zIc~{?THKpHOuUjY+9gdG+&--eZQHQojX5%vy^`f98;=cz zAQMv~RZYND2)mmz57^hIZZYZr&~ET>_@}!rBWd2}Q}j^?xWzuyAycxts5O}{2@5;L z0#S%~PXW2p;=XSGR_f&6Je|N!88(3lR3X<#Kl6D%s<7E!3ayd(^RRjoADp~ep-FLQ zgUimnQBrGm5)9%gF$Bz=wqfA31qXv~IBcl+{plWIa)@FmNFqgNg|$Cygzptidi$i+ z)^vk8*_NmVWGX{GN5{Y*v+w9R!nRG|g580tD#7H#SCT>ZZVpj1`XJ}hJHsf;h3d32 z(X_rcd|&-&llS|Y?3}-Vp7gua+}eGZkt6r2Fjj8?5|tCV6#ZABSL4nJK&Wf}m%P z$G_z|v;_x;O^SLVex9m{>qWAO2-MFPL|KEv6f5Kpd3PZiAJH;Bv<4Q2_Kp`LDvIbda`JKUd6BkkU7)+Qa1T12FjxIejZqXJq_#qKn zH?Og`P@Uv56=a61C*xFm#&E+^7kYQ{r_JO|EYHKm$w{TC&$5-<*r#3XguR}RHI z;f!Yp+avt4d=yu9_Gj}}#2;>ZboD_mma58Lx_4~eHC1U~O%v|1LBoY5?8;|j=u>&> z^VS(db&&_F!1t4p7w>n(^&I=wz^u{K@@pLrO!Ngj2hvsu2%)kPr#swLYwa(XkNFM{ zK%Q>LH}29(PvTmz+li+J1bxk;Ogc~*zf~>*k09Vo=vlZZt3pC=JM-Vv^DSBdYZ04wS=FwO;()i6vU7$99lyV za*#^KW9zN;TDhe&-3+Kis!~@4XL*P|tqC=lQZ(rF0)rINF@7pt+VMIs*#{$Zc%}IH zN|>(wTvYLmBSxnn#wTQR^=wSrb|CrQBnNl6+^BZYN2TL)3o80P0JQ$>>SK$}`x z9bz|N;9|}-$h^|uqhl^v3tDpUUfR3`v zXk0dApxeDV(b&z;ys>SIDQk=i^?6&!L2v#+w}{)*hn=_2wuHpSp4YM+ z9We%4Q;R8C7=$S|L(L^pShyGuD}UT`KAw*NiEf{d7Szh{k~d(V>3{XS4n2rS6u|7Rac-f5h=6>p2$NcW>jv{aRG|brn25ukT&P`<)#b^O}%CqrY+^B_5@i zfyzwkRH3tM#hOQ=0yWjO$(DZAygqR|g5K<#0oqanX_Ldo=jINLV%GPM@b#Mld{`ou zI}Yv!yK8n5RE|Xi9hZs;n61gBq2>z|WeoILq1bK#bEji_-{L|Q`j!m!h* zSvpVww6~iM>FozE_*=i~T);k^6x-6g9L@_t#vm9sw{fPN8 z5|0dKq@GimFp1!?j9fZ`&W>M_$rJjs=iIem(!F%jmjW(b0d5Y#= z;)3lmTEV$6*h`qt>>t4~6=ka882tU4v(5yQAzOBKXm5^@MHncZuyvOm2anZcN%kKP zNNUkFcrBtAx!;H33ktkiB+8nPxGEI6#EpN&8z{@;!IymB#c%jRm~`J39|RFq{{{i( zq2F*-4;3+R??AGIk^6NK9KImB=PbtOXYza55XB5-rKSYlJW{`5VEZV(c@Zv+#iyq= zl;+?uQw>QBB_)!Rb-9jUjWP%#YadS)e{5YKLh!*yGD>^U_C z`pAIQBFZauie+;SKKp^$7j*Uy_)$|9vdMU?s5a7nQZ_2Zv~nwd^tZ@SZt&g;?5`Q3 zJovd!1l;fmC7Iq_8@lfwKP~gH(g*SLwbBQ@^a&AX)sN+YL(N$Sn>AG zM)OZC#G!nW(zl$?5fRZ+Ivj%^!@JbN*8;ZR3PFy{dj_2-J)1QBeZY(RcI5cv9jVql znTc+5p^3{fhw8F1zV1U7<18946c$asTDp|Nbao$hVfkFlr1rr_AcPN`FQ@wq55?g* zmru_bT)@^Yh5GV&UY!GY(e+5y_?>jE6LlRcKKIdL0dxONiE^v2c;EE97T^KhW9S2^ z&HhX6vIWTdcIw~%y?cW{1HFa=Y!8=qeIDerRoc!7oT@SD5E@ZjwT0_}l41-Q<<7!% z5tsN=Pg86LbL-PqV1MjAwDr3>`=VD3F5$GYPU(1NkzF-#?_zN>iKos9o-{Zq8Pn{$ znya)A*H%R`pTpp^}y-GqJ8?xy$o#0N@hNAqmE>+ADn@|Rr3 zS7m4}?1IOHOsBk4?Z z|C<;v_0D)J## zVVZdqt~QnH?)B$DA(0^NeYBsCEeCqp#dzO*tz$%-9EEwX4dd`w!;&^fW1EG-wX>fQ z2%59CW-QODC{IE`tL4rU+MufoJOf<=S%*7UEiO~i>}S75Zg=_O!J(J$4HiWJ&&N-A zoSGYwTSMZLm@jk@s->7Zo!#()wJT40(5AeM#{H7wE;Dg)7IX{Us%gNeCTJffeRwHm zG`?yhXEeS|g zgedT2;?<7VwUEmAz07s4E;VXu6Dn-(?n^p&W6q-??_Rh^a#h~xeGZt`uNgxHnOChW~7f!}J=wj>~SoeP>z?Fe}@6DRh7)sWO?LlT6=Qk@SX7Fu<7)EFALi zbqelaFqjjcW@$AZscBsd_mQ*8psmOHX5H)0v*h?#sV>>C8EX|zziE7zpQH`m_KYQ_&qRh$6o?!afK*4=!@(t@&*@S%GDmZ_9M(gpMA(Ko7 z#iGxgq;tcIMedMrBU?(}59$G*HPXHY+C3*##->7(rlRD{Jv}UeQ%zckxm}x@@;?pF z@8~O>RU>@DF2viQHQ`?*b;mT)A&>RgBzxT{+5JiTLNTGQ^zxLf{hjZ&7bE;q4SmfM zqi04w+O2ZBS(X@85*KJcIi%{(oK_x|E6ss%MhCwn@h14}jodgJFkJ_4JVH;Jj;qY}~P# z|MJ3p9;eNXL;j}6WDX0&O5G%Uf0o<8RhSk*EA^vdU5#;*PWI6!8k`@A2L;idEq zE61<+Gf;)YqCTe=UKHO=umfAUpgiDo;e16^?#Aw^sXD~$-Q(o(yuMXl-Jph#X%umx zLCVXMY(pX{?(K;QYwE|Op#V%e%wp=IKY)wN{|N_!udpV5Ghh)E{wykM!1BEXFd0oE zmo(cmr(sS}j|5xhAzM|q?LN1cI-pXHsS>5=JA=ZE0>+SYcZ5J~se1PXka9O$F&s)I zk_6!W;_Op2s+=T_VOIe~6ke2)iEAriLsI_zi#v?7BH_NsNX} zxl7$+HhD*ZRb8KeGfX!`>f^MD0g=)J(MlBaXlM{`#>VV>6k;K4u1c|Hf*ulwIv z$T>#}qh$meTzK;i=5c=y>aARSYD;JX`Esdv>xHK~#e)G;-)+D~wYWrAr+B+gAIHXux%M#Z+`5)E#Eoz;v*MMn-yM=R)3^@B`hj<7#Hq&SS2uPI7d z4b&ojTI~rPdZ(w;GBSCi0t9KAxR|w8h7(_0^H@Jiji%!q6iYlR$~2_0Ixf_a=Zmx0 z8fz1>xjbc`It(~^f2dilNb85d0y^o8UaM2OD?hWEyo>wQt9fj`$8_1@okrTda85<6 z5-$m9Z^WPE`t`M)gyY+-#hsEZeP9wqS%3ZORd;dL6^`>WE5ng(vjO#nC^<~~7xpf= z82Chz9uTEj0ero57e9v@Mbi(hO^4R3n>r@))Yq`%R31+vA0!2csGNxu8VEdQJu(i8Wt>F6Z^mt1E1ZHsff44;QW{AEJp09Aab@ zZjYQELB9B;snfvK=3EkYM-4IwowQpo3ftD^&Nw^M6IU;z2ni0>EG*rW=wYMBQEM{f zhw#a{|4sn8UYoI%EZkrV#$qgYkDsHNF-$yS4S2?TJ(PMt)FRXm3-~sf6nLV=W<3$v z9dRi%g%D{y){(T0`!lt2%;D&+EQ7ARSXi{-p!4D(iz51Xo|QndQaY?mwwN{{X2wY{ zY~oqglkV$`?@38#55a)p;^r^wZX5PzV!JKh?pp}HUUu>H#5WyJY<~sPkfq+XMB5&i zOYc7A@0N%>864aSBnb=KYiaN1$ul|_BCu$D^7NM-VLU1>Kg-3qG)hoQ-B6KNP3uB} z^sXM<%jmtN5P5H^b^F<+W-?(xiVd6p;Bzd#_1%aFVSn)L&rOs0K!vdnk`Z2!oKW7p z2fo)*BA2rbny+3CtgZHHVo~uv|2vBi)gSmLNVxUNWW`%pB=NnyLWo{1YBIs*{PEXf-XxJu;ERw?#-&}b;aTU}tImV*Z-_~fF96Q@T`*Qv?&B46v zRS*g10;T4dL*ml&{1hAS1yNpej zLmTcrJcFTe(`0Zc_CeNKFj1z@ux4hx+PrY`dj!fJ_l4`suq&vek*qHS8CZTIQ=!WP*z=p0+kyz>Adq z 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 kO>7~f?aMgV&RPk!Y^We=)~r?VvKvM{ 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∾qyXLx4m11q#41YNglm(3Ys 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%0DjJ3&%pmp9r zM$2;1+-=uQ{X*wDz7q}S(4=BQQEd?Jo^SPGRn0c;5nZ5?r9uAk#uvDk zNM+sa>fm?$ye{|RwoxPagb{F>q#txvzj<|mnMJ;&PT4Xl<9)Rbue#535(Vvz`P%#Z z_eIq0mDX};*Z`utQp5Q&F9`7|)O+rx+f~Jk^5lr}5rG-A2wW%9rm7?bxrKRG@E2dUhJc zZy8L>nk8`bfNJa_%t#3^qt6wfB)fz8(mwV|uqm~sxtyVDS(jkZOLpcKEpNn|o=0+l zA1=?yk2}iJIIGD-S-m^f@J?^<{N=oBhRTiec8-EUo17f|RZ@gqKluioA{v~*YyFNc z`;IQR)N=AV6%}ULAPgGgmD=o6dU<|n+wSt zjS{nClKbwv?Gm&4yq?t4I|ulst^tYT?Xz0l;kTca$mp#K0%u9F+hy7HI!CSD(Uec& zgLhaS)L9BohwKRhhmUs#OtM6pT7uhBd3?7*qA_dLXC<6x=|L73-xDqG$0%}^u@t@L zokBBq-(TEz8w&`Em6}noP-0Wn89iR&`Jm_>YZ)y}oP1p$2J=}^(uMp@)x?*DTd3V< zs)DI3`9pF6$6A&D0uW6GZOQwqV(|gUJWQl&w=QOM8V@tO0)`$oKRlCczI}LwPM1}H z##OGJp4Od`8%Su&3PqP*7NWy@V<#Zw;JwsFxCT>AIQ{=MLyP2!?wBl%YJ-Ep>e>0h zWuc=sRJ~G&g^RNxY^xED#KvD5-Pz(01rprrzaJ2g>jxf-Pq{X&`Y(GE*Wdke^x ztxvbk6k;NgX?V~9PAJ*Y@+LnH7vD>{@YU?V-}QH=EH?x52}xHJWdz8=DhB*B9bPpZ zeGboIaFhLf2MLlxiwk{j*69_Ryo=#AJe#irJj2VrX}Nq}W0$ndo0rOd3Rz4}8+3Gs z73PJ4hU6YZOYy>=Zoh9O?!F_8CJQm8FF!dl(5(;qNo><19X$ZchypL>r!5EM38vaU z#w1pCD0h<;)mtR6-%2?19x9kKaX84_2G_1ddHP+SBz9g6y$rh z0`_|@QBpaR_(_tvqHz2lmWQn-ADGxn)DjriA$#?|?6dh8$dv^(^L>M*vGOe~OLjWg zvnHeH^!TCBIqYERX6w`vgoe*4k$$9j7dy0L#HKnZnoM!VVs%U0s(lZ&g@t-XPvHe* zR+sluNb;}hXN}uCU*-o(owPho0)o3ox+gCakH9~(f>YfrzlvdY*`C^$JsBNTRAp}M zqjw`J-deVwv~~lI75+}4jthK3!+**$QIX$jouCCZ@rC@fE~^%l4+ z)XI#`Lm8;Rd5;~ge2lWjLd!Wl*bB6$#?=P=YTiqUE()FbV8g2OVtT7`#@055-A*XM z3*&5$S7TFpoVbuR`R8$(9DUIi0TbFW0G)xaFunN-*vERq(n5;ComKx zAYLm7v{vG(IMPWWzX8eCg;E-lr4--U7Thm^x&3FpAbR0Ilk$cE>X+O5giCsENwKo4 z^~_Z+_f2D7(a^i_43d);Y^$N6>9%_Kf_r&Xc*s{XX^WXxk*7*FZitcGjPq*w z$*fh3%Yb zIsWK!{+N!5espqh$=+CP|98qvim%y;vrNw3JoB`i0p)aUlM+1lrYM*B*f?T2ptsDo zaX3MXE)(wINQ;<of|Boelr6me@a=H@+Gy2NCm7- z*x*Pz&JbICs9IEO|C&jXvZ(BCV&zC)SKshyGR!P-n2fg@?QT^Gb>m=N-XIiz{vAD# zvN){XUN$hg$5i_)VFuE^!$KFxqkhICxy@4Wp+*(ojpe951e{Ln1j#kL@S+>d)^&n> zmgF&O4q?(ZR|4JWQLuTF8lC!-8b!P@ADhBt@i*qg*auqGNcG$H#8X%$!_ zrFa8&0euK}VA2lrMMU`?Auge&YSHO&!+$r>YFQO|C3X*E1YAEla(%vDiW;`&6bO&E zkZ*>_SkO@=Sdu-och|qMH;Ng;(y#ON4d3UM?HxHs4-8DA$MPh??c`5|97BsEP;Fg* zOTLZC0)fJX+|!JHKADMkYlN)bQNUlyYPEN}u|J?RXp$51Jusdvo?lK4-LNKm9^6v$ zbCY?BDS}P=W)J!#j^`7)XdqV_=>_wSp5`n@M(_5{l{W*96-)Tc@4u>RL7mmbkKhO= znAdfk(px2U{S%j5g$^=E2^6`Eo?ND?-?$DQSc8(TVmS$Hd4JxO(gGB=G5nrah$jAtG#NJuy4 zMp0%L7rCi1(fr)^QoI*m!PYXI8hVL=YXviY(%&=Bq#^il^d=>Q)Xfcjj9Ki^&#Ve+ zG1e92FiGaoZF?&Gg$m4u zV}7Z2`Q2tEbxU;4gPu{uiMq34t6eP*QH0{GMXcxo!V2!v?~6^}5@!`?U=RF|8BW8) z!eO-kn|SSnOPmzy)9`BVJ)#krPT^=-xm!?A_dTWxYxV$NM;m9(Dcr)*yrRiR(Y=T? zWu6o4D-8EFi)5Y3cAl9%t>&TJpu$Q)!VQ>06Ua6*R@paWKKwfsxG74faj4z!Mvxy~ z>1sQXs6bG5zVo7GQBE9qDXzLSU2C74Y+Wvkk}IF>_U}bkgcB;)%cr@xByuC`rjqL) z2RV0k{n>aS3mn4?e_ishx+u^FlK&`3WF=I_rv5v+td}Q4edb~3Dd`2}Q$w&~9qH+c z3FR7;CBiGB^AcALYO5^TN(gGjQV37d!E&UZ&B8}+5q4jq*Pbqz419o~WSn8D{ zd-qIoL%&N9DTcTwk&A{eBqmb+;RF(b4&TC#|Ak-f#{o z_fjmWOQ!Ac#7@lqC9L>XFvc&oO2K@RRZl7)@QJnt53rxS488@nZPP0kS8@{S&88DI zkuy?ro77-1$!*U7w7f}kphIus^~<@{^S6rGV_$IK)k5+iwu7M*N`T zmt00M_s&Y9#qwhjNkx%YR{3L!$vmPA;oJtwhXt)fy;^gm@qN~!9X5805PG8+jeJfn zh(!RO+mdW5_@&D9bmNe0W!k8Z+%muLU>>-X9n@F)WQ@EL{8FjRyvZ^%LLu|Mi?Q)K zO96urH*>zlh0W&5%V|zt9yaL_rUPsye3HZO2WVk5~*Wt~%D|NFfnW1~(DWXkNUT$?7H*Jl4G(Do!0 z*FQm640-UD?4swvpLZ>BZ4k9nQr0Gqo-*VbG5^*wx=^a_kS3UGM&pe3o$xB_+W$+7 z_Q;dv)k0U|*zwLnZ(rMO!{A-o}AsVhX><382#ygZENH_ z)KDyvC>K0!XjPD8aC)X1Zg@lTs~>zcriIISv5j+#WGylpo7c(%<_EWM?q~9>g*$~4 zqGu|qMIrz}TV@9WsT z^)OvCV)Mx|NjxFO^O$uEP&UQ_GLr{>vAUDb6VJ zq>*J22&m5a~nDYX|9$l1hJ9hu{ViB;Lk;QRJ zzdG^}y&xWhJ=CkCjvL-T_UYa0HJU$%lgFIC6ynH$vt`C-xp{7M=K6jLv0ka?`o${O zgVMqhsSn0-rp}-?0j?QG_MhmctrzYB3W(dHz$>7^>sFpXvSK*xx4tN>)=gGG-8^gk z>ks_3q0>nA)5YHu4mWNEeb0_klu^F3yg3Z_x*hb%yGzh#|GD;Qx_;7e-z6r7zOF4% zjhI#k-pds`RXFsMxwi6pf_NM=B)vbu)TamOUU;u#Wqv3r&T?6=wcWZK5v8bqDkFb5 z`kWzj8Pw9mPip+=^T=EfxlCSrmgdm;YH*~o5~L}Kbadnm&u@E`T?}*_H!hQM`R?pJ zrIBd3y6O@mEGgTjXL@$$LB$cJVWhx594sk1_gBNUZt*kewRrd)W?8;M`vxcL!8>zz zm`_J4SEugm4g|JYC)$r?LzYcywj^SsXff$9+iYn+y!unX7CzSedZEt>U843 zl~jj38m0~l-_HZnGs1vr{M0Dg@2>Le+~lcnRM=#XHAl@Vkw+E$R>*l z>Rs~s=t8UL^5DHlPkF%VynWo8TMZY|UfzDmJZOclt)pV)x%qFcf*_-BB%N*ELvDHy zSQ=#A?k4_nHD=XP4G>TgG+;`M)2E1)0ilp`Wqsy zb6AOKws61p9S@^l#Yw|IOocOvvXumLN^@$7Y<1cNQbJ>34~6@y;o>ST=Yc^t!BU+m$~$*w37c zZzIg;J5yjOC~??mEi&kN&@AJoy-?pNKEYCbhTNlVu>S+hlH;TDft80M0FZ=|;g0V8 z!yi;ic!lvJ10qJRsks)ue^<#l3CYb33r#lIIBN((djL2|E#F*B}#SNT%xIr|;UCXC{b@M=(4%V9uRiB! zhEGEqV20fdXxN?yT+<@C@BwX`nxw3gdGem;e_46TLgr0($J~p&*3%s*aM)(XhGOLe zPLr~)<9`Ago#SdVgJ$eon12=8(>&+e7_^4fF8$({4gV%}`;YX;0{Q8kN;@Vkw>+#g zsmI8Tr0ucODefES8qu~J<`_jw*d$9xxDyo0@l|6;e7@Lf&g^7OB(%yi6^pVi5zceR zOLM6U{H+BO!-Ns9XDmGDAhszQ*sXA;5PySq4*Q-iBP~z2D7(8=_lv%NUWHf1vlsA@ zgTn#4o_U@w8+uzxcdj-4 zLnovcN3>)r*cKduimvPIZ7JY^uv(3qz_cFjz~Ge6JWXQ*YN1<95=1FCIVJ-20VU<@L^ZBEFluC>%HLzZGMMzICOllCpoL<=TuoQdD?F;?|pr%9sVK3vf3 zWLmyMB(rQhun4bakWL%K{%GS?@Im)s4WT2}Ve{0i^Z6#Baa(cSDgQy2@w({Ma~o7v zuv2_USfjB>CNlgl4VR_DKr=Q4wItm*y_u{{R#{}_vKsUx5yp-ko>gioP4&nakaMhf zU8J$9C|z`W4YtbUQ(X8Jp3{{3bjxm%V>xa)^gy#VuM)5?vjvaMh`J;z;RBU!Q0av)ZBGIg*!go< zMv?o9nw|$p$JF-%F(gCZa(33J&j1;bSGq%EGV11Ia=2LRq61{6A8ppgqh6E#Ly!`c zOb{M_!t0$s$N}7JZ3rxf2VVQKotV-4g^^3ToliIu8E91@CMK>`n$3=~@HMLCSg*#m zE9PUvnPUc}5Pk2`2?c48u$ziE=Jr&nPRQUk{4>`S<*_2G=ZR(zxWa=LPf?l5+ z3uh^54RN`FzPG+wGEWS5Yi=q>Zi?hIcF*`Zup_glPQ4dp9e!FyTIC^>86#8~IkY4r zE3F!LR zd)6?<(-IVJ4_C}_m$)tDG?0hi*?^4Ezg4rT3|_Yk7%~2I;MGW~j19ZW65^Qb8%4B< zNC9k*69}tYghScyLPj3y(cP7%YZDb5DhKNIGUXIn{&sIec_Ro2#Z)bk-88;>%#Hpyq-c!k+TCIQhjVMrajHHE4;|vM3sVdFF--|45 zgMSK3LlUKLjb=-$#J`!^>Bzitk)&a)+bUIDK3*NTvSU=2VqtRkCBqf{+4d|bS&udz zmd#-)EllI$56e!5?cByX-!&;9L@^*{Ic}rikTXp)No1N`%+ts7t;CLb6E^Kz(Dzp& ze3c18W!0LcQXUdFOv1j=`TZIPEud%ru%q%hjKXR9Jb&t6`5IiR=mJw2_F9eMOrF#hSu@uIzj;lPQQMl9bY7n?hu%d)gxM zs+LuqT)w^R4K@^kT&)Uaj`tp0AEsNAzlM6N?Y55n>CKbtL0N6V*wF5PL%Q*Kwm)RR zK;feAR_3Wy1f#uC?(CCS$1k+PQh0P3a-S=QdQ_*zNIb7&8j4lzlm2lTZ)P~%!=;#C z$?i+=uHknXZ$U4)l6tEyNQy5m`u>;)=Y`uEcht%j==x=f&T?EM`?p}Gl4^*Tb9Wa) zrv||bj`1V2q?Q;V3X$eLLLd*5z$LAPjFot~;5e!^OYYo}pVsED?`j7_=1s)wkG&(( zz<)@g;Uczo_PfGbA+{G!B{oq?nR)z6;gyj}GO&S6YRKB6+&KXPp^b zTxPz&^IVmp+(bz@B1{9N*Pj%l~2<|OYtAM5dRqp@dtIGE>$nYeK znS`{#oL%%lALqvH9pb}kB@_HEoVr7y*J(e ziiPhLtwEUpq`7B*xs}_?5gex7M*D#eI&1GXmzL@^I!D3z3YK|s&9f;x6hv}De+BP+e|skIqvRVVaF z>yutDD!s1bT1j2ar40gs4spk+tIMUu*OAf(#4#7Pw)gUD$dfY8rNzSKoCwLF{DU7R z+5;1W^NR6K&ie?56Wlut3 zm&5*zn5+D@ylcGkaMt>>?#r*|n$&pB0KXOz{T*XtP^Y=BHiO}r*OtZ^7INXqX4_V9!2zzO zypchTzTcM0ns6~(QK37IHbdM-?U6lfvumuVN6Yg&dD~T3pbHw&yweF8M7AaqQCky@bn^yPBl?~~<5|G@GUe?o ztph#j&5o%%oSt-}i(R56^W}9>@&?mX&i*GeZqq9Tqh8-@7|%l*pEHjYHnjV(9-%ZG*U;w>>qd^$W>|4`>e#!uM*WWNNbW7fs#HCPP#wZl&dyolnyDc3@_34^eX}pLFb(f_4K&e&^Ve_uEaCPViq24Pn#pct^d4jvL>| z=35Kte)cP4omk(^if`AJDUGgt-=0cIx`(CCuSR-PA0L02xyS$2MOptzL6~qn$b!g?fN{1&C94X2nK!fJT>_Uf(&aio{fupY$ z0X)_+6D5vDKs?eKG*|yrPNNeLAQV0lz>w2qPTl&BUEz_zvl=G|G5KO?#=t58Qs?wIP6;#9FdR^2o=(!@lUc&i$OMF-QpARAKtw$l58REAnoY2sd5Ax4Dcd*oN8|SyK zIb!B~S`?;swjKO-RpK)pa}!ae>dV`zyJNw~u!3mz+#2-B$@Bj*wg<>JFVFFdjZtGP zGa`+8Vwn#g`DSsN3|lK-1(d%E*c;)_6Wni~LH>&PQ!Sts{Jwp#^vRLOt-wOR@rbOk z7+xW}gSkwG(>FL8VN*7!TWcXDqX=2FD1xb%-Sn2IA_}$&bzP0I&?BmD-7zk2e@G4f z^+3E;>she+lxYz8s?HD7L!uh31sv>|nl5M0)bF}DSOcN;k0 zlRNYu0Reauk9ETyy8A_%w=n-eN4pn_u9wK3IdeZX4e8J{axQ!d#5={*d-gW>yGH!ma=t1z(7l8Q&#JJa5XVBA(aC z+PVEkH4E?x=njaV`YhP363Tc z9FHWKHHYOmeqo_IYGEq@fzNwxiR=akHMG9`I3f*f+PC~J>t@=fX-=pdX8qK(kL$dr zy|u&(?9oja`f1vPLWERpuJ#a(v&ASr4}$IVNIPYipj}`meC4gA(^llXlmPL+k8v?&_}E zS~Qp2R5~ijXx$7X1XI10{f>^d%AB4#?kBPqy<>_qI6gP&=<)!aYuu7_as9d#7((Ix z#;var9E9-N%8B>dI$X3m%eTkxb(GyTkxSLD@y24_C^G&(Q+n%VWNUJcFGvkW&Xf2D z#v#$baP4elcEp3wgM~5nUjy$t=5U)D{;NqW1+xB^**t=UU4P*=#y+!TxjeW zXMb-x8g=uR$=%8(ug2_VXr6s*Zg7EQIB1r>&_3;}c4let+kO-d999lJ;PTcRX|>e+ znN-|QCr0~-bnLV3ViW*OG|WbSo(Vtv0?ytpeY>^vo2OrA^3B&^_V4-o=)0M%75htVzu4Jk-FIXwmpstHvqpIrWt<;CNwX>*R{g?0G|sCu`zj9rsUZZ zV(ITVl85QOmq6d>I}Yf<)_NuR(ueOAQr{eJk;I!V zWp1B)e-_DvKDf}68jDZ0v}sR*t?Q;Dd%8aOvAV5ED5^hWk{uWH+8zG9($%92i#c?@ z&>`srmENAtC}DMa{$+XsOFDyC-H0u@9^g4LsZ?ZXragdHrFfEWdx_>ob(lWXA zyb!W(k_+(YYV0qHUVf4TpX0D7F0|M@bFC`X%|-m`)(;9E;asX^7>P)H%;MTaX@x^6 z`vcWN@~**o9M&t8D=A@+<%9jbRaEytX4j{TER0A8u|PhCf>~C zkU$2oX|$fLHO-Txg9$z+?AWbyi=kV0*C=g#hCvUAeX=>|?#h0`7bR`{%#3LvWo#Tt z!!Wu(PlZFnKRi?qidp7*-Yi?CTEu z(Mb2aSX%l+Ll;TYW(^~sMXlE2CCY`74X?%IUYzgSF{~p7}9n|OU zXJ-2oRwX4)FLAh)>4Gd8YG2gc$|7B}Mumiv=*i*zqJ}*y!^MkbYUbTT$h1LI3{oji z3SeRNEZ*0;Z42@k*yTsQW1epYgC*ArTf^k>bzqQ?lP15o(0#=A%hIrlCm(}8*i-oD z_fDlQZFc&VKkZa9xlh2x548^zBZY zln@WVdM3P42Wc$#MNvD|Q%1OZq@8}ALlbZ7O{t+jKnL1p>YzO^QpVpcBQI@e&T2J2 z%@CKtZX6u&em@ke84ZLO?Rh%iey`&d)7a|gr`oxShOtU``l1_ecQanFgRR3ZMEF~SvG%wjH@SFb0FUpYF zpks~tQaCh)(;V+Qa@JoIIM`xA>Fb-P+QPqBtPD*2SCveK>Da3`H4Rzm?ojtQMvGvZ z9Fd;if0JshsFI>LFA5IbO5zEn2)sG1q)eRX242GWCgrRuX%z z3N9%bc^xmcTL+UL9hFK8v`L9HKgbO@JxR;K2nxm^uj@d zIuoyXxJfLcF9GaO0_H-eF%gHel^-q!CMmadG_3o4)Tl4E zJ-6B}_Y$?ny1lelhp?Jh(C*gGdwwi=8+nDACXGP_bcgMZi zoziY7B-d(LPY@?3V^rTM-mo^(h>|T%+K@;V& z8#~9Y@Ko=TV>14FSzz!%DqHqJ)55T70{((tq7G#5wg$48!&RC4YDkLAa&Hm}rEz~O z?JTjM{ruzLzL6jMY3szw;9~|1jwuoS{3V5}+pTi2)O9d2>t>+}^Ay>F%qZwZsz*W9 z|N27ljE2EXtU!ayIzb{okZgh5wu~(E`0Ba!EUe0rew&Yr6{wb>X(0t_(Wn@-ZTjVi z&Z(jPf4DI}%~JfEHZbUiv_-;X%P!7Y(3{w z(|r}7{LelU(quhSCXcLhY%65F^gt1(iDo3f@B*$ddE1sIv!tAAy&~60P1TB0icJ-r z;IMzDWSqi_-`cTEJ>(+2&F_6qH8Rjv=1Iv^o#~VcJOfhFJcFd2w-fW1)|*3h{Dvvc z#_DSeGYv*cKw_Ow!F((ayoj&$PRYUr}{X01kr}rZ(m?$jAH6U$YPwu~>wtFv)8?JYVn!3{0AFNh>;WcEozn@}(B(FZ4 z>Wq(#Og%}9p}~)swAN+ZI2FwXd$!=$vydMeq<8lQz^kUx8>r|l@zyCFUf#8jJeDtq z`GFr`JTvZ7AlvD`3nv(`x1{XXR{-JC65!bTeOabusSYkJV*QY_%-@RU;ZP@qBl5ke zu~JXqqh77q$w_c%-j~7?kCx(3Q+?K-xujjDm%SeIY^v@b!7oM$9<`2)5{T+tpcUL- zQ5um8<~fQJt{O5L1E)Qqrp!X!;}Up!RKriv!ri|!c`(~HTia!2 z;j)M`2Kdmhu8kXTkeP;DdM!ug^%J=|fEA3n{O@2i8ssgQcP)?65wNO?e`0p$w6?vgW=Q1p)@xn^PSh1oEx!2`;xr;Hjt2htVnL+f0cAuuvEZjmp{ryBj2&VgyqHt zE32_;se1)B20`|?6phoE1nda z&T@4H`A3L~lg12xo{iq8NCBV*i%&2R&!RL`9KI(UoqjbH)>xO7mxeYgV!CWmMw?BR zJ6UJKBcl^nhrTiOHi5L(r|>3zJu2}*AytPHG^wotZu9^&V_t;q9(av<^mE29R$?=H@rTL9 zKEl>D8lRByJ-NA$3g{4CA6n^4IK({;>5TLO!E;JJf1`Nbz?<Dl`7pfgh=cx38^Ll^iV zCbt&aXc;pd(oyo6h>QklvbcSm`ZN!>UgIx@4&&W0Ip6OO;wPFxo8ZUPRI{wGDi=hU zxG$yYi8(5@y_d9u6;NaxN8sdo<%|Uf$MUwHxu5zbw?$seX(@{AQ&J)n2#WyS$Mr-e zFmvGRmfe-hC^h%yLImRBy*0Buy-T|dQVXdZKps-POxK&!(xB)sSirpKpS&JjrxgOA z*lFoVS>+3)XCG)DrgX!yDI@RQbM!@R^T6BU1vt?um(g{MkTwc`siB}4T>2W2QL5lP zo$ep5U4zRTRFyW{u6V3Qfop7l0~b@A-EFFJErWDuYtlS;F!CM7dqZ z@JFTAAvGkZAFK2K^S|E2d5qZkCw%7F95~hy{c-=DR@VA`NyBq4DM;syO2Fy2as#&| zwmL-%jp{f9CfykYy+uFvRu{`zRf8J>r|ph)TQf?P%|%ND zke#Pi_}0GP6b^^_Qm==qxHzU+NC&|QZL(BU$J8mm5h!l=wd8duZ1HaSH+6{i&UTk< z>uQAM$M4$(e~aPIA$}I)`l_6gBHv+$m#@*HD<*<+VL1d=ic3Bn7jZRo;J}CHSG^+k zENQ-BM*W;X9D&*8Zss!4^vqm7FaHjRE2X?JGv}?bx~~nujy%(Kv_F!>M`o2>1bQp%)Rqj<aAa*x@lmsRrmTOnSj#8O=o z=a8|}D5#en4q=_X3}7N9A}$hNF;H?c-T$4XeT)1q$ugNz4BiSi&8_IPKDtUf_HV%! z`G9cC?p8qsN8TmNGG6+uk}_d2cX>sV`j##yonkN)#j^b@6$5=^+w=!1-nG&+X~!L+aF&O4)Lo9udhGQijOy zku0|rK}9Mmk_r>8n~)KGT_rA8`ZxV?BjZUm%-au9nR&^GABnFeLL-Gi_PZPw1$)jN3;E1Blr@09`}sB>5(Bolm}8~`0~p+j|=IwcB1r{_fM+mM03M` zC1)xZi-m+W4AvjfNw%XYsaZdczN|c9uulwcYB~lS%NGUplcA<&z)^_cvs#yTcT=Tl zP-g-P9u=fk>2^uKZcc}kpjZ}xvq5*^ASdL)x8rm*xs%NGTzd*0-kY6BZSl+EKxwx}&o01%H9P5yoJ#s8jTl*R1tX4rpK7q#kQYp~c zoGBS^Kpv!)tPKai6Wg94M1_}bxDiDux^Plu8RGV{CpfK0)h6U89*WTXzNql%p42FLDqClatRnC7ALzbwa* zAS4I%Yx6ZnbF0@^cZDayu|9iiEipN4=p}xw0g)usQn)#C^GqCM!e?Vzj(T5WX4^Wp zU7ZR-#|8TF*Bi(cb& zNN7hH`oS;$Vx^9M;9w!J{35Qie;gsvba=k7&U{(SR``n!e{0zj=ouWJc#Ajh&t!?o z-rp5oomv(fbJSUXyI#^UU@$IG!@h7~w;#ab&$Qvo{!Cq22j(@uztSy~;@ak&i2)Dk z>Po5<=}hgPFQhHx@3XK=J~oK;D@`-?w^Is6mPW@9yV`NP{M;)tnwoT^)L)zmDEZbE zwzo@=xfjMOM&7@Sm%1TwC}ylte)RG84~ZP5t@#@Xgs2xYBdIG@m&syioQYEohMSL0pi{E}n1N^y;4T_;d_4=8$_H~R-GK$fE zC_%rdEVJVmGNb=mUDG-aq54}CQT`&@Uu1Or^BZ9~k!}j=9-tE!vNguLw*UE}OLRbjEHd!NVP^TRyjtE4VZoIK6FR&Z>U zMyWkQygMwWI@NFZE*)w^2J}!y^GKc(YP`Qu%C_^2-5|)q&xPMka=$=;EyLWOAzVpk zyZV71F%=It3*?X}6&X#V*sDDq?IRCi&9i6n$yf?6V=Duu794;0mEPL1pP7 z5FkK6ML2@sOdL4*Ja1~l}9^1JL?B>Vfkmw)ce zCucHq&z!mUGjq=QrrFDWnm%Tqfg;lU`W;W?Cu%qlJ)73mIZ^LxnrqrYoQ*=(Z2r2! z)F)M|Upy#^CWC;3$a-o#HM9A3Y@!4+>4DYAy9CAA+T-Jc^&u+@MoU%SW>IkBLI(1) zIOZ$1lI4EwBQe;sPh}(7&*JmwgJ3Z@^!X8R;OZyV+ZNck+?GTrrnxfaI07%jV)ila z&|NHsrgFFDVJ5aA*||P>Zcbpg>C5O&tJi`b8y7J_sSnG>bOqwvrn>Gmh17{F(!b1L zB#Ee4BB)s9$N9Y53i&e2GoL+^@a5Kvk4FyT@)m15pVof}K4({KQ07khr?2oSFW=LQ zE;Kiaw zZ3%)`)g7rs6KFw6FzlpK`YZ9V6`REE(3Y__1NX4v7%+HO+oKj#z88G|LuZeJJzrIf{gVVbtS70|XO=bB}pn?kA@ zk~l4G&eSyyQwqEYzG0LdZB!=AV}F3cE>UgPy$~-44e%W)i=x$%i=X6ozs;LGUu5Ue zzo3Cjz^B$OTZ-Y@wmBq@o;`N`08kl(YjuhOSvF4B6Zl0c)5;R}b)Ia;2C3Nc37<8KppElWCKqe(JCH`yLdKyU9j2z&;#?Nf zbB;%}l&DnEM|S26KqE-+pLd$J&$IHN5-EQNTt7}S!bIweIWHk}@=`%dPNCj4}@%K+1IL^>l ztL}UC#>OM%yyOn`!>7DWte;cRh>lad5=&IeI1I#VXO(MYwo<{zT4qY`bSxAAXU*lX zC2t@~jFv~$2);+uHy0WsoaI3@j?+^*0_F=R&F8Cv?+TBF>kmQh!sU2{PS|P;4k(DP zpU_K#-QKZ5gVl0D^R9i8#xhpdFMSN{t~49Y2q`72K@yU&y#Q6zpHo(HZ1~&r-j*C) z%J_{BN$mcYoR8&JgWu+|1l_EpT^=fLBr@ENpYVN<>{uq(&{sJ~o9@rSt6pM8o=E7} zHmzw?(Aid2e(f6+avE4d*(PQt>8kAyfaJtc_4#;Tz!Bi3-r0VRhIo@!g6$t_%QA7T zwPJ}5>enVYut@bQmX7!VY+Kj?+GC~S_p`O~bfz}n>m3u9t@cga2lmLp7XzH4=l*_< zt<>PTwgJTmOS26n@N1bag}1e`Rdj~*#Q2uq;!2)CiDfI1J=G~ju8oQ2R5w}G{zS!M zqWzPZ6&@p-%Ww>)?6Ib{-pNHKcEqz9KXHdSJpmkD)(M?9e@W|-c0{?!Z0rQ2kB3vV zs^DO)3pwB}?0Ytaqlbn-8=ggHmooJ3o#ctU!_=3f#uY;RSr9__Zh|I&%6LOL z*7+|bH(1)0lF=$i|6!qbnj^pJ1w=haC)4{{(-!v3M4Z& zsmLZ}QAPtUjpH#6c(huN{4yptwI648FRTG;+Cbc(TJSfoIdZYqpp}aAG zY%}sY6fc;UDYr0FQ=u-RsP|ijN5lQ)FEFj_ z+n-#oU_i;kju2M?qmi#6sf?P1? zuel9G%o0<3MhJS{9?@@$#iR|_Uc7|WG_H_Iu+%}qaF!=%B^5!Mhh={0Iwu%735s`F z@F}@<%QM_6-zQeS*P&_6nn~Er!l#c}ip{N!%kXy$8eJ;>>Q_S?1lbs((kavI4_9+u zz+9R{U6swi^(?bZ(&=IAPAr=(Fyk_BOt^#cbprdAEl_1Tx5GQ_lA-a4?)E18dFCrb zp5?@Z=|I=Jq`ZD0Pf2NLQNKZP6;+UjWIJ11W`4Ph-x}{M7Y!kBW>(u3NX~XG#n6{= zQO6Q1yyBa+e2dwB%6j1BgslcES{+y(_Dnq@(REFPVL5)l%p{3ze-_A7pN12P4(K3! z>wE4~*SFo4Ltxg#_AITx;RzA*z~OTTv4{G?Lt$k`V96-vstoCm`ezq|O)anYXk(uF zo^(@yIKvBYD_zscGzKHP6tMt#U)m#KfOAZ}OwX2VVK<0NXU;aU)o6CKS%JBdp|V1_ zg&kTqbK5YT;+Uh=k+jPO06STl`J@e$?p!80MHrC;gOFdz1Bxinll7R~lGOa8!dE2* zh`|eGQ_~T7dYArlKCW|!8`}2syY3V1rlJFtWqdWi^6(rPHMoAwGV%1V_gAM~FO*yn z*`?2)ugqf6F8DsAJ&Jdp6;ka^%C=PZ240Y6bNN=X)8z83xO1AN2EXH$$scc{a%+<> z52Rb?7cphEKn>p4v@=vBL>H~7cpk>vbd?zGpE2tf30@s)C83G2LH-kPiEZYgrf+X` zF(yyPL+TytVaidRlFHX2Qocr+dn^U^gO@HetN&-{kXmk7;4hET;?OPL4s=y*206t7 znRysVABUEV+A)C0D>(jOwdY*IP5}_jAz?rqKole`ZjKEfDxCF6x*}`rq!f%*@yR3aRwx z<7PPm2vV}9u=Y^VmvjYGZhnSH@Yg6Us+^9dMP}7& zCDpeD?|*>oTyXpz=bbU;wv0c0D6S~2&c~<4`VQ;p=?)XE@Ni_O^12} zzlBlNdG{wmV5GRHsObEctK1Xme+(GYznx!~-ngx&$cfz25ZExW!AgjYe58ac?RiF4 zY;L^kZfw-pT6()<>EMu2jnVb=^klb+ZvUdJtn6m-D;LsDXN=UV_5)AkFzrv*(W?mI z4Zox0SCf;rX^_+HNGq7BD5NX?+rFUd3oWrCJHEgYpP=MOJ`@UjUeoOf@ z<3E6qGEj50x%J(000A;eD~$cIy}e+4M@vhK(O+PO(b{jvc)P200j6uZMi5kcXA>u< zqKrzw6$3QcA!RQ2$hk`(P$jVl1z7g(9lBb>JF;?6e@p~?2k=FzWgEAdNJ&Y7fs^9d zV}H|sq^(uL|G&%rwzeC5!b;?r_W<5*e?VL%5Ja+WDA(X5B5=$fS@`_VNfG4we!ob& zbaK?s#|KqhT%3+Z`^(K%RsV0n#N2$7G{89FF|#Z`U{}v&{QJMi!*g!;Tw(S~N=imX z>D!2Hc#ME>Va4pqZV>O@ClW1p-m{w6a1na^eA1ujxIHo-AhCW(1zRU=pgigw4LnYA zmo{8smcYy$-fPT#jl5A0>>;ET6f9CwQq)3Lv74Kl@kGA#A zn~%9neP0?_H77>h2g|b>o0=$CxB}R7)Mg@V4Go1tEg_IwOkQr$y|TB78=$qoJ~f<% z_SSMjWo6~=6BSM9v~UVEt5^JH+4?wXM*F+X>^0lHmIhOJJq<}iIM<|<6bxWzXw48S z03nc&fP0>9FGQ#|cf#D(1K*pw{^q}$i@k5r`Ozu@fgoMl42M86A3ZvV){bxxkh2G# zl#LG!0{1Ad+@113ybwO#{rQ=hI~|?ad%S+#HA}rF#BNbgyFFBy{Azqm6H^ ZM{KS~A{6NHXLf}RZkS##)xGuT{{TpPRTcmM literal 0 HcmV?d00001 diff --git a/packages/uipath/docs/core/assets/maestro_service_task_light.png b/packages/uipath/docs/core/assets/maestro_service_task_light.png new file mode 100644 index 0000000000000000000000000000000000000000..314cb383e19c4138960227bbb8a6d6552b653578 GIT binary patch literal 596758 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⁣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

  • tDYe*iw7m{CwWROvP?vqJ32EMd&4b{G=a?M6mf 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

    F4k72l`UYZITm5H)8t0awSd1%G! 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 CCK-JI literal 0 HcmV?d00001 diff --git a/packages/uipath/docs/core/assets/studio_web_select_function_dark.png b/packages/uipath/docs/core/assets/studio_web_select_function_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..68a66fc923e1d53b4203f73842124584cb1e39e0 GIT binary patch literal 153206 zcmeFZbySpJ*9J_BB8W-|h!WBz-6ahY(m8|Djlh6(iK29Ocg;vQh)ToI-OW%V&5+;i z^SsZy-kt!uivtZrlK4+hOu6^xm@53iG6*>Gn*5KQkG-RTCjZd?V798C#~ zN144#=ARQKCDGB!llGA(`|N)7od)YM_19M@UtXCf;xTw=MDHC%IAT&h{CxYPv!3QH z30fg-Ftk-fiKn~vzBUOhHjM~R_oopNvF?41(EOL}?}NS&@}Z%rE<67Cgcey!m=@o! zEPW73zftkv0PDs33F@8X_;|R~8SA$W`B2=Y?#s}#)>VZ|1Nb{V6#|A=9V0eLTulX3 z8nHdyDDm1AUjzMOQ*#2L<=kiG4!B&!)Sf4-baUX2Z)Y<8+^mbr?W%91fAD*Ks81?Q zB!j=iHyd~!v4Y^}jcH&s{_qfMzT%N2iLmVlR1zqQn^omrRDTMUIj2P;ZF-qx^aJ*h zm@`|w>)G5dU1V?)Y1Zhm_y=3!6yKu}Zxn)U*}SqMI(M4pE4<}?Y#$S7uVO`0FcUg>LX-S^Zn++v)g{_QCzJRuiB!|U? ziK4}vx}CpkYUPJ;P-|V5#&ZTd-lmbB6)0i*gq^AN`Eg!IC3!O%T8}nbL;72~ zu1%Zl7rH*bq%Y!+aX9=RF5ML%g$LGt%X8%Q!nF??Xwz8|{zBw~9g&TuNRan=SSBgw z5hDpRA#tFh#HUv_YS@JjG8kUs!goa zDP-(jJ~U~%qqsw|1G42lVXYw*_M>cHa@-Q&PQ_y<>})ezI(X~p_|ehAk^gOA-M~A+ zCW3h!WLQKS-af@ox-*s!P{Zw50)2iP5wq#KermILG3}KkO=tVTlu_aZ`YT_wW=xnJT`CdZX}QSX-S9 z!HN(=h$g+{qiKvW>C#&*TlF|_KX`ax;a=PnBw~=x(5l^H*rIive@THI5RiqT@w(^* z^u?!doI%@nU`$w_?+3{&sg0}kFflNC$G(y&&aQrw`6hXpq#dWd_p_9u039tI2VJ^) z`mj&lp(3Uxkyb`ewo?&B(XB$#!oVI~vj#2$LA`o4)EB3SgW%86JzjCRam37P%mtdc z#f)0CS~b?iTYO_V#co=y@S)`Isoygy-Bm_XQ=G+$`n8;OR!WEqc}wPAT@OFS%yzChg9;gYAi| zs=5pNErX%579-Axup_w+QxpD`yDQc!GxWL-KPY_e2E|b1e<@0{ingjpK)5IS=3g4u zOq9AkF;8C+S!&vH_g;VIobO)ap5xvcR4n7yjuXNc!XJ+l&lxYw+Q`;wFr}BLPx-~e zvdnbXqPeZ%`*~Ve5&lH}?0OB`)cA~RnO)=V49ffvb$Hs}Y9Uu`U|HJ@9ob814s4!m z>Gqjy`$`SZ`j!=u)rb4W_O+qY4*?GWC`1Ht*4{nO)Qcz_$$+oJnc( z@NSnf^o(>?ey!|O>SmcO>D&HPo9>qFmU9{$kv5>1QQ7hc?7KkO;%&>umtZvTWZ(Ix z!S2MG>cPEZjzeE^V{$5jO8e7Ur@Xd|KR@K4$t$E{bTmq$WXQ5;IP7oGa>|;9uZSmMW{X{gjb(d zpK$I;V92N7qEBQ-JJs(dmfLh2Cod-(l{S^MS?ilLO{}(tHF}BS-^NpheE%WS!TTf3 z8AEU^b?BX|m&_$@Li=35cf0cj@!nH?0wevO@u{WRJ&y3XUJZ#aEBEBd+2WE?cDg{E1@l)i^lfjgDU)hsmls+~7U zZ9Tg0gw-tfRRcX|{3R7)z_p)!;JQiV4XT{@Gs}Ax=vv~sE!;h7JI&LP&V$a)Y=e1D zYg^|SW)6kwW0rjakJM}X+Rab5LBzJV#ve`;l-}3{j(y0+f66Coztc6+W01iAGjQNu z80rAEeA%K5Phs|zzF|Box2W7*7fop;{dosUq)!}A6z;HOUw;sDtePjTY^yvuUD@w5 z)3Q6c`)$|Ft!}rFMV+;WjeN4+(ZYtatS83`T=jSMxM6W>$QmtZ1IJXgCgrPzGC*Nh`EgB8ccT7B?N^?Fr!6RH<6j^mysYHWF8 zI;Mf4SG6d0+BNx|5c23p(kyq5g^UF_LrH{xu)g$g2~~Jl+ZfR3!F~p=a~Zl=M%kO6 zCMmHesDNA>yq52*D9&hIl$1^EK&vm!PYskJyj8a$AGde;A3L}lv2TickL<438>H8= z&zmo_cvuXj%{MN&A&=Wn6p;IS_;aewHz$%Of#n~!pS*rTERrw6?w*axJ$H*)NXa`H z*0@T)nn?d3<}L7*^4&4(;nbP&yz{|fG^r5B)<@e9Dg>VQ7T(pLtu3+)m6T7Qd>OA_ z?RT8@?~BjfH1R#)8bX}f{Kixvwi1*zL3X=1jubNDEmaAfCjEPTpzxG00lcSE? zClB7vzC-ZGI8ZKJwTdUprz8@-{gGOvjTR)38j1F`Q6H^q2u;laO~O!Q4y;PXbCqHI z`K#|T;)z#JBs>pJ>G%8xgF#hbFhv=r1Uib6r|f%84Oa#tsNnoWA?GnhrlK|F`r`gG zVTL=G-}61sTSu=3?srvO+g*}!W#RQn38`ZP99Fy|Mh2e2$QHr;`>=%k7+BIP>)Os)fB(CLM6AW%$HZEe_4Biwowa5s z`g1kE=7}48aQClE@spUvB_s?>G3XDX`P)+c(9mC{|9wotU5mXRxH*asIkB5>)oUVmkWE)?eKJ*Gqo?;pfLV@b@us ze}ti-57pI=88ZI4rN8D0Y%1CB!~NUd|6krz2`8+BRXE#JrG=^racamPw&A3TaY2&rdaXeOAzPY# z$KqwKg(_vq!@E6Dr?$GUU>^$Vka$+*l7t=z^*^^hH&UV@+pJ{MS10<7w1iyd7*tQn zS`$W3+>;RZ{bNMehp-gW<7r5R^)b1i&*i(&qgZ>YNIq;XQ+W(xL%EiC!X%nf-bC;2IHY_PA29*P{1h=h4 zp<2C|4yXv%?=C4X4;;G}LiYt+kBN=UzK*EM*Vs-4ShG2=_YGIbI3FTcyJMVo!e$d75BIMc{I;`<8U+K9X1zLH7Y&t*T! zvQ#$|203Jr?MjW8>Sd1+Ps>oTV)%nvpYUBWxO;s4`laeVGR{(BR6kRZ&1t=ljHlc4 zsW;8(9OR>ES7e*W#%#Uw!B%E%pjbx0?-kR|)E zlj{_{2^v(Xh`U}~QI)xIaq(_ALYRI7$JohDN&g13R08NU`fY+i&6))#3GU;lfhvU4 zHsxs->TtuTlSP}6!|imN#g5&c&H|nqCwREt*dxun5)a~+EoPdQq>u67_HgHDOWgcxbv;yowe(AQb_^$~H3Zn1XtXHz#;iE5iu``e>8J#oy6 z>@zI&LhWAeCJVxUHdQEDFgX}Hd|GaQ zrUOk~cQZitpW{=QjqlW>!}KyTK>RLlyio&=&ga%yfw%5;H{erT_IP?HxFV-$4QlKU zSrR5yqG*Epk<#DKk8^F4hRqF9_#EQbIxrwPo#CgKlFVvpa4~n$lJ*edxS9k*)-Vdb z$fJ&~aY|vgUSPJS@LBf??@qXcR!qb{u{!bv2I6yknGOcF)#YsS%nKR16myDg`gvfJjyFEZ60z<* zLZvaHZP&lYW32|Br#wm~o2@=ot<89ME{NJsSV*0^y_dkOr=IS0^twJ5(u>tf3s+Kg~`HQ|+*0phw5Ntj0e!v+hg)qyIK9Iv5lJTnt+9q@2mxV{?BHKS;C0tpMXe`H^D znaZ=nP|?ZWK(q#F22htd7D=pP7l0sWIYiSJGfhS>B>Y8(~^XW`&hhBzdOQg5pLUT zTtGA(k>6HhT^R*bL!=$;P``;FPP*z_vGs3;_=^(ZXD)eEL zRybu3F2bK_!f>Y3n6ju6D@&cha&K@_roq$N*QZ~b55^WFFdB3rX56wmtpbZ=D636E z7M8XFAdIK9ZK#TSsU9u({YYmhdx=?5OLnuatAP38lpK$muJe0w?>2WL(l^9h*>ta} zRR&pGO%r%Z74glR=U^NBJa)=8zV!(3 zGlTI`p8^C+#wY9tmnvU&-(iQ?exz+dZGA;|IwroR9l(IRz?8VR;2W`tTbrA|B!07X zs#vKNfn#_aH}^i8{YegOf2x8UOeKx)dlH+J*p2iP_s#1_WL$VQCI10`k@ZVT!Vm== zx`~MuU-A>>fC+QoFww;AE{B(tQPUbR9r*o230a16b3)a57u9nQMAvGUr1~>K+1QlW z>u~U5?^_kRMd8RU*}Chw&6$E)ud6AB4(pTakYT#vCbCD~H@p0~1InTgS+<14>{nsb z)dd5mA01i|#UJnf=7_QP<$YG!%+BN1ZH($Y!ZvuV2Q8vGxr$6bEvI(|_IsW7PxPae z=(wjjs9{>4TNmo|yun(W_qZa4Nh$Q;nQQ7rGpj5s=ux)K8y6$o44%>wCRD*f*dy~` zwVruDWHaW6XM`W+96lC`ffr59CJJ*EDPYbEopTjUB#pWTiXsmq7D&DM9x9&HTR&vB zpq#07Xsu4N7LPfXYUfrJqZfi%WFCEOc2hXE*21Jzd46*hfy3}lMEw>deEcJ_m0z!4 zbbnK-BbLtIZhI-ohah?_JPq*2sfoPKGS*LMN+~kUW6; zb$ol;+g|}N&M4@_Wg}G&uZ|Hl|J=Jk7=g{oRm82{y4${bjV-Ea6ucxc;Sv{D5wEgY zv|$FVZFlKUwua^o$KF!rp%$CndMO-Q6LOW@S9aa~Q|#fN3h`KtJdfkjLr|7#vKybg&-Sm=Au`+}EMK4+ zs+-F?76a+rq$RhjeN8%4BMFRPCt=^N+RjwAY}QU(HV3H@5F2sN`1dwP+2Fj_lS5C1 zU3>-++!je8Vq@I00b(aV1EJU%RZQOb6yq?K5%WH$>Qet7jTgs|H3PDSpNa6w`Zr_N zOgj0Y^~MSC*ZNexK8KwoqV{?0I$#b&*wcqcV<#D{H{(4g-dnBCm>`!MH0RZ>`{1Np z`zG$qF~5Kg}3)YkbICRrETzL=k_{@HG}s8jGWigd=jkdENJ?u&T#5UCVqqq(T3VMu7@S z+X8HXrPnkZ&t~9l))d~Up6)R_jAU<}fa}2YSfpAg=*4|bt4BGtP2KVhJWdszBs6u} z#!+k#85_^x%viWap}(NTFp8jgy?V9+Qes1glnX_H4_EiWYlhn+RA;>WN72L`TT|Nl zEpPI?uEz-7562R6x7X52Z5Ik^C_>O*fp3L{#mQ3by){A4%e#5{rS>TvWac{Ur@sGH zUs7e!a{V}Vg^F<9hnWB|(K66QGKtH(x5AUScN7~)*WdS_1yw8aNCHW5Vkh@54DDBNdLac#z} zPR?X|^wCp->)Far#c?d|>tJHz@VVFGm#d_AA?Zz!8CUMfjlPJEYy1hLm8qSb`oqhB ztdkc)3&<)m?p?i>UC+I`xmwD;4!-X+gO;lJRNGZ_@Yq;{NGjyZ@0_m=$OL|bsaL%Z z*Bp+@Zfgpdnbl=-pU&NP(rKN+I8g{J03lAJa+)f<-A{B3%lfLmEQUhZ-~;@j60b8| zmRA+@OhXqB6vv9`y@#?SLRiL1Gh2cPWSE#K$jE(@aQI$}`$c46a*n{v5OcLh1@ z%WYRP9i5qm=fTd^?mAhT$yu;c`dIo)-l;>!IK_F|EfG9V)2-AXc&;;%YwNf#k3j;Q z(&(^Z%iTHD;)e|<>qb+8zU$lv{Eo}44G6dKxoe_lRT0U_onY+JX92g;eGy~0HSr$F z`c3(=<$&>8A(iHyBrP?=oL$!(BB-T5x0@D5ccY6%Y1`= zWV+2Ciqy-U+FTttaFMtjD1u7dZZ*hxp6(qMf8$UkeRiN&E+C{U$N0ent_Isy-gOst z+fzJx&?K9cA@17-0c8@8xgn=km??V68YB|A-LZwz%v=^&eo|NxXu=N1rb`Gsr+^e@ z#?5YK&-0CN7yUm~G>J!mF!uqSGgWJIGuOU@Mt~GNu8^;i5oJ7vneH>Jt2c8)Lc*4M zB*@-O?mUjvqK|f8+8ij-D0sMA!5*+0htd3IM{Ar#6>|X;Vxk4reN|yGV4t3iXEFv| zOFx&uWNn}d;YKWTf)JZSIiM-12n_A*dS8;^T6E0)@C(<=I?qv%(>W2|##Ybvl03-y zBYpM!aGs(^dc$`UAN7F%&d8z#YS>eEl?;{3PQu;m>%dzgFd87mOGm+9!jQ3*cQ1ZJ zqoIdCBl2ZnPpfta2ft4Bdn04n=b#Cq=(78WU^*k>sq`0Q?WW=!)wbIK;@!$g0k|jF z$;@B3A;)SLhx-M4*!C{W?+wDVc6cF;2X{<@98PtdS2sD+mD9LM0uB`0aC}o)tUbr# zW}6&m`P0OC%ktiIbnQCkI)=~l;P89o%Zo6i<1mf?|Ia`S!1rBOavbx-})-H1z;OafMNEOt;8|+D8*Pou+J^W z^S7YRf!&jJR?g4cX|(C3a86S{X7Qy@B~tK~Yu3E98lXw}wzz-jK)Z^v$ar8>J)aJU zNcBypLkt~W*>gKnEq0>oaeVf(t8r|AHh9oiYq8a(45K+l{1e6yL=|a2d7CM+G{@R(U^D?=8!Sb+Ia1Yf~acO~z`K@e{W!`qF=Xg4T+i!l?2>Gya zgJqXmR=H&%^PU#s-AUYXgJs)I(ICWE|3IIq1#j_LKy|1;JTdSNIni}jxDwgD&pOng z7|ZxXGmZrVi`ZtoFn$r`9}s&xooI0pG6%iuDBX9zrv0!+>s+X_EKE>^R!?)#)svr`7`NPAmtgX%y5q8yc1z0)OZo({X{6kvJgZamqYSo7&RXW`60S3pMtA24B<&Je#JASlI8~oRnk+d=U!R ziX%-wESYG@!4q=_YH|S-LB$$q@51={>_W|(j@MHRwYIxPmKMC(K(O+LJGYWv6`Lx! zn=PX0ATCjd-LUT;0jkTTs0C97~8dcb5@m}x1I?S-lsu5fp@nl1E4#_e`y_`o$R zM%Me&QGl8i$mP$jb^rxNrNg~%SOwLl#U=%wo^?TWL`^g z6W@kPVc`q1p|Pa5L{`XRkBGVW=C2k_E}$DHKn5ujDCmB$x-SeJNE8Fl!M5ouH~QiL zp->^v1Cm$$7MyBU_VDTUO(<%*s@mZuH0Z({sak5wVbC&M$zm;H(kHkvxf9(1QSZDbs3Ge{OeKYL zq_o%Fp5J8E9#B8~R(j)M&o_4M=BvtMSq#SYMrUtKMlGG6fCmEq*k2)_mdNSU%OjAD zw`HmJ>Lx(Jq4hM&qMU!hd*N|W9&J{tLLezQ>+zjLpjG4@8+YuTnNZpCc$rP%TgJZSvzsN z`C^`__4;QT%DVFK-Bklb`sRTWi#9ZR{~-f5D+$XfoAIq27h|9x(C5xy`DlT|Vi?)Y z#((T$-g(pl&AzIG- z-8?f@F5VgLW*$^^bFl5u({gnoB_xNp-7^%Wa^7Hjv(W@DSGEV^G#`Qjv_dbPUj}sU z-b!FMU=N&EsT1>b->3;MRI-%@SvNSqx0SlR@u|ePjg7mMH!IhU$=$n%^S<6bNA|{- z8GP42lncC)`~0!mDWb=vWku|2FTT=Zz~iy9h!V1YmK+-@dg;Kz10+XEi2k4o{C6&3_<*?6yef25m2-7$^N8IN_Fc z2zp>SS>+z$f^hYIGe+;>^}R!x%e$jgy4MOF|0`(X&^+nrR9rIdUiBcAze1rDZzx+HRSH zi>Eml;n5?RQRMg@8{Sqk%m?$DnJLO}mzm8$52@@b@EuAJV>E zP)YxMDQ~<1p*L$q8l5UStV|7K*J04%S8#BpU-3~k1mnB{)S@Sag)BFVZBo#tRnuoR z@`>#4kNOTdddw2QOg0l`sxG;CC#nUVn>Vv~=#6Y2oBUr7S=gr_Zq3>MtbHzx> zHyvJkF@SszDTfdo7<5;sim;`!Q?ciPg>9D-k)vYRTi=IFO&YhY5i?zkgt+iS0o6KG3eI9 zTzg{K?btyTKs{@!#`~k~T!Zqs!z|-1g2#9B6v0#3GN0vv0M^&808|UDN9*SvqXZUk z>8;i15_3bIkCkaC6>F6-U)8@p8Wkw;8OJJRENkG1_nR z7-Lq?e#bdwD-fD-rXx+N_DjWgGMni2zAEQSr#P#y{3%jjlwCd<22CvsnFmVG%78re z43Pig0d@abt@Yt$Y@Nevgl;U$kUQr#*+8Yw#vNi-<7-Avvn`$>q@1;iduULM%RaAB zto+8a@u(RJemgGA{5CAY-HCK=3j(X5(jq}vA&w+uh{GA{v`_C74tV$YeM8v|XslSz zh*_(YO6Cq}yjICXA|5NE`gdV_xCPJ`s(d3>&hIp1WPc1pU~Tc+5I>S%x6YCxc`Tw&f^(%kR#Q>m76`TJPd%ltX>Acq*@!heKv3l_B5BYN! z@VMV7FO{Ff5y0YQ2h!tL3?;zA_Hz|M1>X)iTDFo@f6FNU^(u)2EG~Ma3tSe>-xy0V zj`cOWSYF7ZI3^=SkK;N?kJFt!_pMp?Lo*R;rWaZO=HLjxG@?l8cz7%i? zCSp~i8megc(nqT*mS!n<=l3f6uQfeW_S0F9S?|!peg7j>gRm{#vsuyt>qx z9t-e`T=^@_zjed>%mAi;!#b%Lh#~dyhS5&H_MovMEQRl4SMJS5+NpRnpn%3+q7D@D zUnZG$!qX}Mfi+3YIleEQo8y5|CjLKp>Axo=MfI-+D5}5g=D)1*BM{9wxZ}IgI{9x} z_yZQDEZ{-$B!Qbnp`05zRBpf=G{o+GygaFGh% z1+l40&)S{I79)bEVzKJ|>JC^~!^CU`YcH^-vpx|KG3ph^YL;s1^~7^WBTOB#VWMNLDt6~gqZF9R|eRMTU3XKH$& zM#5$b^UY41UnK=Sj)E;)t}7&8{F4a(%UJS;5(`y8n^p^f=n(bb)DpE-TKCd;f+gGzdt=^XXHz^wm!Y-Y{kXVu%IaRJyWsTdbr}a{pN=G3LAA8A#U1!U?!QaTrU5sH}t*JgBi=A ziZ~v-`EMegpZ2XU!4EqspQ}G$6U6`1DS~7D*ny7tVORCucX6PRKE#OWw73S%n8@xk znn-Gk`G5gB^$*`)5f19CLYAH>KkNwO+(;Mjz4jvVcoFl5^8z6z3)k|l9BHeV&t)R& zj(4|u9-Rf$uwuT8Rt5~@9Y;w2l3SDWg2rN_)kFi9}!dH z?(N5-9-1}wQ#63v5mPcHi+Ou+%acnPg_b~bBT7obmO{qHR?DzsWy8o&cs~cSdR1y9Y`Tj=DmZ_e{WRg=WJHG_56O|kOGZ0G9qWcW z7}>8{41OJu;k@3iEU8;-u-hv+vlPj4e>>n-=waypdM!Bxjf^%hYt!Z{{oZK05T4e; zyrk2Cg_t!!H}&b9lKk7#o*3kq!@805Vfa>|!9;?9bP!ePT0L5*pwSJ0Mk%;fTf0nm z>3^F#(V%W*$9o>WTuvJG)n8l`J`JM_yX3bVo$N~ywaH)L{Jm21FWNKQ;z#^&_|g3w zX36lmfb>sQvv^VJp8LTGC8R!>54gN!ib}2esw-`!J*pL zqs3OCW3x$%Se1N&$8odkh%^mA-q+?Ql_3_LsibU&rQ0fgdqT#T&W( zBEqB+dMa`<3ouL29a6qpx7}$(ugPC6*n(ZE8$~Sq*&!^p*#je+X?wd^)Yl)we7>dG z)2OrSzvJ@%5Hx~S(OI-j+ryVNsJV2Mf9+I@_Q5L3?MblIErRod$-h41@L$gu(3-*0 zfk<)QU_B7UU6ABm`%A7z;S7rW<)j0qmsSDJ=W40d-{*yu%?fyQ55M2~-zEXXS0}&&X)TnM{zTGW za7JhboZeS0=sdqwzW)Q2QNVJ^1#R^I7P)|qb;e(yb%3Ag{+IX*9ID_{z`eTkMt&=n z{Rc;-^?;SdI-vFcR*3$uTt?#G=#U)$KOE$LH~>`-u(BL4Ov@*KIljOi(=Y&raT>4s zTgC`1?-dEKvNZ~c!~exF|Aw`Ey8mTmzj}lj7tsC+uYh5?ZUct-zb*g6qyK-s<))#A z8ddKlY;WhPW`KlhS~bb}3pvDZu0OJZDp+NN1UAR~o!<6_ctIRytMnS?9Ag|Z{-zK7 zm-9djt7B>9ToKdtIhv(vAjJe`CK)`^cv`uLo^>_@v$=D8uGhm06|YzF3zPd2Mc_Lr zuFEF|4)GSZT4(~rZxr3WCtFVx{?hA?$C89~R2KLs{^sgYKpO z{cmz8nT%*#x$%_PYrP21gvrT;w_FdSA>MlP*DohOaa!M*${bL(Z~Jxsq~3Ysnbe27 z-DS-E>98Dg4!e=*rWQBxGijiOA;}u5CCBJ1c!pWoI#Y{`z4gSMX6z^}{Ez4>(F5EJ zQ`TFkE4muFw-D7UT%$UCJYJ$?zax#nZ zK{3@=`re?E@xN7%PU(tgD@hf2%hkOhckn~)++%>!`}=e?5DVXXA32;KA?O14H&U&s zfegWXP_&d(!t4(piZuV#7La5>mWGEIX)LgLmW3lV zJ&qK=eoTv`z?0dXXaW=O)Jw=S%6@pJ3z2GHwH(jqdqIlIrLH|kXko}WSb zKKoTm@eZhM*4ld&_lS50|*jMCo3w!>(qr zZre1EIB-`6S?jP+1g;Gy!>T&CL&9imckXRXNzPcMeq5|osytBucNV&w;dlnzq=A}{ zJB|z`&{Ys~WBAv~&x9B#l(_Dpm2OKLd|=<>vFgKR(y0k$(=pv`I6kWsaWlc`mUvUQ9NN-XpCOJe4w(v&^2Td=C^V+T&h5eT1w&gap!1Sb=;Zfi?!++ zrS}-sRzilCvHoN)qNb)q;mVqPg#Pk+MGe)Pn7`OzV9$S;cB7N;!qj>qRO7&~J?;>j zq|woc`Lo08OY#Tb(>#ZHH4ATe%f>*$gx-7aKaa{o;6|xanxXlf+nOc%Q(A{3KyT}s zyV=WhuCd2jR^stPkAmD4w&PDq3mqm(Um3|>od;SxA1!ePB;TF5G~@Zi1z#s@DYw08 zv(Tz$b$Et&#$r7@Fvs_1i2KnNtHth((3{wLy;>Ev0pmg~_VR%>7HtLK0@eeTb>aCA z+{X(;wGj^DSz-BM6g{hNX^q;{MCIfft%oyt|3d2IUjnS?&5OtP7}csQ?@uhtd!Ftl z^c&RRh;+Ie_l%OkS*?Z({aHBl$u1_gL$m%Y?2 zSH^rkTw4>bVoq{lF0~C+mEgIP75^I8wrgB0ht4?iOAAe_@z5JGPV1^ZaNQ&auJ`8P zYm{@JKxNZBKmNC^4DK``NRL||b7jY)W@}X;;sEb!9f)3~Yg*`yO~ur8{ms*EEAfqP zxoc0h`E=wg)Sxk*h&6H{$!_ndfaBn$VYyS>lw9*IxaslK`KV9SA2g0i4Evp#m>AMi z7ut|ouHVFsppnVS1;q9G&k2m6zBtmdP!PXs!fb~~;z=%gZD;7jRqK^c9f!fji?N|_ zax542gB6z5^8G0}q+=c=Wr%z2)ChMyLPyrvX%(o{)5cY9R=oj7{l$a5B0o2OJj(6WcP%sWo5 zIoYlJGtb*+11M<=(zgQI;!+e1J#^avI%Jd>O?J=mq2GZ*k?WcaOU`K+e_gWA!PLf8 zK$gC7@3`IY%@nDjSu+uG+qjZcl`^bf<(9uDrW@KpE1wVrK{ z4rmAg z;#Q5_!ro3g5sUU{+(uj$tZ|!hP;Eo=xH#lboiG>-+$2uyzcV5%q2H;#w@~%)80CmsBz?e!9$(-8L61uT0Jx+lZ`FW{&4zkoim$Z>v}7 z0dFI>o`JuDW~HnCCDoC@eq+J-S)o0|HwMO?%(YfAji7Da=NayYs|5MF*=A0r1AELd>N(6K zWRkNBK4&UN)d#EeMkQVF!TsFs9itR>HGJYx+NxPPB_VcmKH~@LEV&VNJ5qK{acDk?wu~H0Kbm?6TVJM#yr@ss%RD=SYGvEvo6IWe5T7M;1_)JfNoZJn`?f0@LE zq7-`fGZN@W|CIMWAYI78Ody(le#P)vu`NP+SI|55G+M z;%3wtGtcX|;ow^@tZ{mTi*mYpEW$L;T8rf(4MwYBP=%m^5qQYe*v_pBYj>MSe?e0H zg|bUv{SwX41ljoa+ar;N2|8tpX7$@A21clp&@HW!E|Yt64g*}ZH)qbFEUMft*sh)j z$)1`KHWEYIT5cEhS+?64GAFrh+G#A@f(mjf)?d_gVpM|SFsePq6Pi}Q~DP-SUgDeA8-=91xFx@B{|efocw=#&DN2VZvtu z8h>HB1)+fVxy!-!=>ol#*boal@)}grX>#;^S9&z=R%>8K%jlG@AuxyU)qr-6^f!KK?oMB8BCWu7Xa60H(F)G z%D}#7ve!v`-yd7)TNv{&*BB#F0T$qUow|ue3>p2MHqIb zA`uIHo?<+q4bd8mR`$ba59O)YGi3_?=odBEMMwG{eXf!WJy-gv$MR@RJ&)@^dyOR- zrChRBaPSwkcy_SWiBfPGbhA#4O2LnPC+Kigp-|4f?znnzkm@k1e&8tHhFL~-aSl=Lm%eMpybq4HQyTwm)Cl_y6~PtCofq{_95 znuN2o;wf;H+B^pBh-T~+z zL|H(nZN^KT)(daCOtv3o{DByeuGmcr{>qILy>WSwU%xDg_r^_}y7a3i=>m6^PduLz z#CtyR;%Uo{DR@bIrNoLTnHgyL>GY=kh0yhKNucTKX1QcWPFt7S2q0>&1?hm?(&E(* zKk4J+UoFX*jBW_nOq`<5Mle?`aofHbAe;CX6;E0e!@}M^ypGQBxtaqkvn%zHptnE) zm*!(Lt!D5ac+1HIkPmtXrLCRO9l^ug7h&fIm%XrVptUQoAs7x^0OM%BHdixq*z``h z`&>3kNXKU)hL^Ab_@@&HkAD1YTISQIjsvs^+tpG#c2&`Q*r!{H-pB^95E#;IyHGyD z3(kjxLrcZ}ANJle9L}wMA5J7h36kh7q#z=Q-g}GQdrkDw4Wko86441_^xlcydl1pf z7>p9VjNS%=`H#J`_w)RA`ToAg@q8O|k9(DCUDvwGxz1%dbDKi&ijBGN!u14kl*sAV zQ<4%H9-V(k`l13fph7csI-8JozCvG>6|gY{Tnjm&&!|FUCN})WO1i(#M=2VFZ@$|EOv4dRU;@VyI^}I0}vt_9ozW+Iw4U&?-JrY_zK%_Dub9 zACPBx3{cPV((1Kp+DxSfGK|zq=#&`S9s!MB0j)MWbO*p(F`3f1RCvM`G5AS5i9qL6 z5#Q+|J)n4AclTpONTDW=9YOE=a4L)GFuhcZC(#9XPUzD`Fhd(ZM5=IwPuKG-m88cb zT=xIjZ-4)EFGftLD_kNF2lyHz^EH-Iaf657S8g%hlSq?`)n3iATXzWMW8)KkMBg(W zQSMJ%`rAT(D+VC?xK^k3beE*8Ushdk5T@VY6U?r&jxxk;03gxvP58)%;{BD5NB>wA zC`NAyoj>F(LU1mwgC;;@rJ?f@$*FUepPOanKW3rDbkSS!`?%O2f%iS;bjf>dm3z_W zzIxf_{fQJwKL1mNN{mhpqbV!-DUY&zX`7p{W3-O3hqlY|_npG$1~tW*MQ>yO7K>c_ zzS!6Pl2fRTHxiXtE2WWm5dsbVvl^=1Dnz2o5L5KpoN7H$>XSk9Tg={fxTaZxp+6{@Hj(>7yU#P6*1j{!7Kx96i&IVx2Mm zz80=aBiY*M#|6iOBaVN!Aiqufby4)YdGa3>*uQk5KnwjC&*5sgI z{Oh*=`|HF2*N0y9cd0M`mhAcM{{#Lv^#6}aoz4uf&G#RvWwJzxNXjr7*4Jk%q=|z{ zlAo^)rO3G?;Of}0YE92<3i>YHwM4hN+V`q`{k*`Zq)aRNrI3x+AocO+ z*B*ry+sVe)0mbBpEskm7llQGhkxISE!jV%MoQ#pZ{cO9TDT5gX?>y!CC$E~8L zdAQmvTNCsSiRtMmvv|v`k3;EwyA|1|1sj5YnHVN|v^~f%FChDU{SuQ7tA#)W=a}PI zTc)Tp$n)zpWD$MciK4v+BfFT-CHx-TMi(b`@!oH4OdXQMQe(;|2CP5TB!lJ=m%5Dj zLmh8}`?%`$C$5B-TZL-=%e%wQLSwSu-;XW){kxCZVuvlXau!P@w)kNTpShfLnzRjc zqFn~c5?I*=xKN)UP2b#vrYZRG;`fis2DMbA8U;&F(9sX0mGYzvJLMCqX5-@sH!Enc z^dmD=H>O|7=DOv7$I49fZP+89V{(jtpMG`Kbk`u(?wPm&Ll#3mf91h6;U^RvR>y)h zmZLg;$Oesa3zgn4^dAz__+hQU^?F=s_AH6Mhb>nY14ewGgmGr726^+vH;!afy|zC; zl6iadvB;@duu*_II4roe=@~?D<_SMuTyzOu_xFgL*vw%(@PDxUAIZ>?-*?HYSHA8` z02h6F7Qyz$(a0?hIQ)*q?zyLBO#~&&n-`-RSD94wj=g{i&pB*w=U#AQ)bb0%2{yI~ zko*Lk*w)D9*lM?dKhr-z9pFSTj(D|7JKw6bN)XAfs4l;= zaSP0{o%KOt?FN^7j1A}+aY8{v5&GS9NB6h&@bxB2r zU#>!$vHW<8qN@dx8dD*(^iwI~V0l5<#kvPD_44x0Qfeu7wZ4gGN??X!R#CsFR^{>w zy0yXNC?c6zS$D_JN(jo&w9e>3gXa0dL~_XB{;NR6Koc~ z=JkmmT;zTVV<=>ow!e8f5p3pz7qa07m<@i;9W6v(nE1pqa%bwPU_8p+ zI7_Sba%?UhtMS-#xD6CCy&Wb}#pVBs@AZrHYWGebJyH8F`!hS0X%7!-L`*D_jGvtK zjq@PCuo__jAL?H!Z}{>#TkCvx+r-)6G>cA<5XNCEqv=NRH3_nYI&=`%*CUJa;nCo$gJ$!9!e=a0$_?&iFxtzS>lik_Q)!_69xv9d{pdT4%DJG0u= z?Y{kF>5Sikp30N z1Tc6iXz^<24%r?#=TT3$j2d3pp>n&l*+qW23v?uCSg+Fcft}w2te?bu?&t5!f5eX^ z_@19Wng>Zxtr*Oh-o_1&_UP{5KFJnWzN}O~Y3jPb$Q(fW*+uZVrRJ?vuCM^GDxcT1 z^xjnm-20aJytaOzHl`BG>!di5-u~fLuj7@5QKF###`Nf7M0<%Jz^BUQNPh; z=cxDgt<24gc06x?9r6GY{7qfCAXY8b=Gkd(Q9NzypST|Z_ll)vPg-22c*zi$Qp`0C zz3xW(ZMYv9AXCtf<<4{1fLh;WonEcFWMLTg?RXvxk&+xhqo3ssy`V1{*@X$SkF=)e zcA$=MpXP*f>d*G019uw+FBXmAs;X&}PXUNY>|VRu*(q{CwwQgqF9ZE&`;jjX3P>$~ z8u!_V&0SsCCn7$Wh*iBQGHzL_52w9_F1a!fI3kC4&w9amUx^E6#+~XVbA5(kcSlWp z=2d&`Z!c`gA=R?=6$*UhXcJGnwPZ{pV1%K~C7IpLqD{b*sul!QYt7M}o0m5;4!UZ; zSE(Q=vfizoL+)=ycyVnXw#>Wh4MDsOvjKQZBC`w%;bQY(ITI{{9KBN0;P);gG-+qY zdW7Z!8n2Fei1FXSH3_{Z`r`{|=QoQCf?~oQe6%<$hPL}E9oOsWH$g8k3Sbta2GGXU zeu!Azu`qqWmziBb0&P$nk z)S((J|5wkDd1r3}G}BLnts4Tm$~}#@%Az-$-}vDL#lBYz+2#++BRv#;zfo~aGk{aq zFx|(u^2%O@&TkH(;)&&Wsr=@T^uQa5x6*NM7I}toez&j^L{2TaMrE+E)o1nJ08DB~ z^;WvqZ&pf{5-TWy;R1pA6YTSO&P|pH9|qEM*@n!r>exqj7Lgz71G%k~sd;2VC)_Y4 zg$rMC!8^_lSD*2Di_`d8s&oPB$YMs@im)uV-XHvldgjWho~|`4ixbWGr3l z(McGIjCc2CN)TqBCb@eY$Ij!roQ1XcK%R2a4gH3dF{>YTH95ev_h8qjW_^v}_dnOr z@))|UyKf$RkdoECljO=}8`^?%I{93n&MnbirGF$p08IxJ2*|s+@h(OgHgZ)^?)RiQ zWyRL;D%}updpMLDpySKTpwoUZr^pv zW6PAL2|mAZ-S$SQ?GwL%FKCM#(t_)db4hh!Vq<*JL&y2@X}A1lBeb8(L=uu7AHl_J zTv=jm`dz-d%xi|a-g;Y5)J-#)Ges0Q?Q`jxxBnbi`gmmkYPtR7iOR$2CuqFaeo8Jd zmm$7}E!DJ%$6!`R@m9LQYUIJT?Tt=mn>KCPB)tOFoNjcTr5>XFVg4peSv|#G>l9;{ zk#IZSw;Mi1#+u9<_}Tt~w0~452}o9A9ros8ckT?bP+obm`$#7JP?__Ymo-ctSXDZQ z6zVE`RdAkii2)Xf83#XX%X;4W!f3hAZ|F!*4>9EK(uUn&qsnB`OS$@88{dhA7Oijf zkG9FhC73sg5VD{WUKv{+o;>HT2i)X&S&z6=L_ouJ-HF}W0?_=|Gz0lr=MUud6O2Qz zurtam;fINNQd+GVo5q))6)&^POpGN2^{p=9CVR}8V>rd&my=CHlrLCHMUBzKJZfd< zpS)jY1-Ok@ULy0eGLKW?nl~xyAiE~*xZ(X^9i@wLK*z;6bcWE;#2zc=xPGhvlfd4D z9$hGSkTM1pkx-ZQTxHUpN`BK`_5c=PW3@#)RXs?)P_J15|E}MInD?@D{ejc0k{7lM zDhJzNT>1V;E&Jk`$}L+_f-|2Bi9az(TYJHe2OeXZ^#5Kkw;`e@dg=Xy+<(s9_ze|) z1K|-~3;g3EN%x`mjE9kkNddU~)slhFycW$$@sqJzN=#9@2GqYW{D1e0wiOt_Ww&9s z{3!3`dpJ8TX^p2=_-($_J6VI3LV4X^B$+qi3io1@62Fwap-(e7U6I+CmKka|ZDBEp!*HfFZezu(D$O!^QmHYUXHzY|vWEpTBd5g0eo z=Qd6tpQf_FuX|5mpk>ONOxUPm_^qCNVym_%yyeulP~rfleDz8bR)-O0QA z&*ZPofZlG6^peF%)CZrb>9mL;8o9`*0(9E zwoZ1Vuvy z?N3v7+C(^xzVG03EI>+IG`OASi-)|`$0;Fpw*!jY>ICz5#Ud`~eUI1QV@c|e11qLu zmC2&>JV^L~iywtyGpQCrjuadgoNL`xw1(XKoJ-zULGzJ!&%!)a+yxo0cPTAddoJYcTYEowE|mX9UPM$6Zk95n1HRDh&huDm0O zb`Q!QAmKxaVI$@t^fdeYCpiQa-p7h2U!Ru)C4@`yx~+#H@&cc<*XqWz#SQQybZxdp z(ex#&>zcNC9wjx`cElFK)q06C+hzTkQ+yyALHqOhvsXc`A3nbQ_^igZG~mtlbVz+^ z@LBE4@~=iv2+5Z6-FxOFYT{A?*^Swf;+y_0gvo0nobHx8Gq2D6BFf-GzJrG%ME#D+ z*{)qR^3-SXG`rkU#2<06ZdNl-0@$3a`=!G@DIVU1;DDPf2Vy85t)Z_WQo*HNQY%>z zNFCw)rU#Xid8x@l(MiKqpyP4AGTSJTYVtfwUj!>w-=|xi(u$M(E~GpbOGqc11mtvg zTo*dlvbR9l?R)SlTEh|_MQ5I*Z`K{FJ(x!B@@8V+ z$pv9qh0jvl-P^cby#pH?Tr@71kv!~c3}*TA*!aa@IQS@EKdZTkwUe?_y|S~YS}&0! zU|#o@5je{TH0@p{ZI?p2eJWMURvya>*;VDkvTg>+dc;a&exOnv){H%^5?Fl0M&-7l z&1+metpS}i1zQ=UJL-Bu)yNysMN zO=)XMy*|a=E%ZJu8A^a_5u|iCH^kTQIjiYf#8{TGVEg#NSG*>Qg&-Ig%;)8X8?}l) zpP$or^Vfxu1bJ6!yrdgpyYn2ecUnwuMZn(}%ZpN5OJo%u!#pH8taCk@C)`WaM3K~7NVcY!8D1fGqNUk{ zZqPz(CRfc(rf>wm=P({lf+VM-Yy&8h%#hE)DKjsAy~#7H^)AlG=WYD9ryp`#XtGA= z-O$bs5OdgYuS~pWsmY<68;sR0Sk1yi277dCG%nMvRa>EsMvf9u?OSUg*@K*k#={z< zIp5upw&Jb5>4KSaTZPk)dVdHu7{WOo+$((sXmDjd1Kzn+#k1cUco6NbibJXdX7gZ)423lSi2DN%)D}&L4CfXWHQc)LWJ< z)-l4}xu(1e0`>&Q3>df(hxw$|3njBNZW{Y_uLml!O|*$xtlxupqm^D^@%$_**K~hc zzA&Nn#sWTBY7`UHU|WjUPl0bY)KJ4~*< zk{f+r-M1tlB=m&_Dz|V)nrWI;P)O{h=T$pCdwl0DC_8}#*{^i*@|PPdQo!PlXRN<} z(S>9EATF*ZZt@Xo9Zp+_WAY=O`Rfvs!$ECVWjdW)?4@K-0hryZnC8%FDlFPBVJFrr z96Rw+DAhJ^i5#@^ggX2&INDWwek%)7ug#%@n}*_FTsmJ8#@wS zO1Bxcl0=r>VDdt))LQO#d(PVx{Owxte?;MU=q$eF>m1dmzweMBO|eS$B1oL2EvP_Z zWt!)!T2)awjCNxrZN&>YC<-m%G7$yozfzE^=F@PWu~WjcZv)moX+(D64LX(VOU^D! z9QNRI*+;-gsYk;O!S*6wxJX~KGGgupvKYIu1<{Jm66D>RKK3S~dZr+wZhC|yV?DGq znMZ7WPz<+3o+d!P5`cZWr;vgbPVJbzouS&ZgI^yi;x_dp@mv-_Ef*%ss3urL`Q~hz zprLNb`eN`huh?d#gwRwDw0D3YBqYF5%hHqeZzPrRcS;*^qHfoWaaIpn%Q55kX;MkB zDrq~-m?pjQaqp4#(o8_jf*f~t)tgyTg~m;+{-DWl(Rs^ySa0C|nfiLiN&k%d?EBQo zJGhbuUNv1$DP%MFwq&*A5^XSNspBm5^t2)oy|nxaGX#68*i^!!);YX3ajU8;f}LDZ zYqJ`+G~U|NVN&Xq+cfoJTC4ew$g2Zz7pFu+*c}6&=T7>grWLHzWtAo$iCN#(rfYUCzq&P8d4jlt(@vVjE~ua@W6kO~QnbCI;*Zi*`E>>VD|>WGmpud4v@tV{N!(*6C*g8pSr`v2mY=o1 z`JG$A@||HGAV}Cgw6#MI%*|kt$GJ<$&p^Qp*I^^54+~T={s{qbP86STgWuO!;_>s@ zSsZs1JX%V|Vgx3No)kr8B@EH%)ID4uZ8HBs7z|VRN?ZoSVNKl2u#NA><<~KSQn~M} zI%=fQ`fVpYGOE8!aegsg$pTQRv-(I@tYgakg}8}uzGeg@I~bfjF(f;88qK>?fMEx= zx!ALv$7O2JS7uA%e{OD299MvX6IB?LHQ!>pbpQS(RY0uF3R6L)Y1bR(CV12n5`}kL z`{WEJJcpDx3>xCW1c6RX3mc&(fklrh2O1v{&D<=BXd1^01Q=Bg()yf*KAP>Bb)GuP z_Zqo(vyc;07Ugm&DD6iM7C8#93l)MXx8-KsG8}b_Y&g>(df3l~4t{Lx4KipC{Y}8# zNRpP^8Bd{KAOx`!oOf3PyW*)9 zI1YRbtGy_9iQe#^9|5@}vtO~#1>!$==-qQ8i!{~(52UPTTd+4_^P};wKGMU#RE+j` zl4mZ@Ao4iB*AL_fbc&WFRl49Yhh^f-PqNDh2HTCvdoYhZz9ThtuMGHkEVjWjHpTpz z4|%ZfVABv@osHd;scpLy2diU`)$A13d*Whf8ipJ$vBPXb{VO^pixIo^`WUXO)dgrX zO?BN8@;IV_Z8Ty&7oRtj5g*@Ed8egEeDxdBe=iUL{?5cV6I`m6tLU>$&P5zBM>L)0 zl7z^MZ!5fW8I_)a8_0BeZrp7831<*w(%LzaYuspJ*LT1zW4600q26}zc4tVMrLg;^ zcIb*nY_~Vfp&A>K@=O#(jCm5-v@iJtIsod(O?f_tyYJxbIvn`FMwF z4k6%pP`Q^<7`bO^eacR+r=#!_R|`Ob^By2kl4iWUV6vHj@OoeMew?5>mjFxB3Ln+x z-Hc?k80lCQh4#D{IM-@T9HEVG;znNkjbKG4p6;H~jst ziMXdGW@OMar8fs55du^Ml+5Se<-A2JFrrAQTGcf^!D7&#{-J@?{$|5vATDVG*Fgzv zAZ*tj&gVT~I;xvlw5q~max5?xwu{hp(%D-13YQY}SccUm+03_SKQ*B#qqCD6y};)D z#s=og{!Y)8Yk69|QcU`}D;FD~^=U2 z@YO8b@}P}u3+`^T{`$nNU=8;6P6RjA#0$JxvKG<>?qbPqzk{!5J0@gFafb}=(-z8@ z=>+{<{T(rw{h#3d;V@QXpt-ngXwJ78Y(v4cm~iJl`?>HVzdH1ZM%9)XS!ErJ;OqQI z@mqZ-8=r2Y!in-q5R{8*qV#Kf6h)W+KH<8J;kQEkX%Fad@x1M}-Bg3J$EY)S#yZak z#0lT}lHHzeb6PHERLk=9IV>@iJ9;1uu8k&$BuTvctl%tX{ms5W-ZSKIpZ=IU$Z*1_ zaJ>9xcD3cog&a>*J^j&_+t8!rBLh->DE9vrBH5qIQU#pOdZh^-cPT zg-w8rqMZZycG!mPHX)5yI9bB@c8g2O>RjZlB2wiLaxu0Y%7VGp9(EltPml466{p#H zk}(2guX@Hk_!Xm*-JkDZD8gTy#lyV_GVa|v`zEN55)a`{CB@1!N$F2Y{aczcDqXDQ z8`Hsx$b;hUs+4Z>Oq(o*C%?NyF>L8KVl72g%&}Z`z$~Edvz5vo%B)s$xgHDi8Ce*> z_BhjK&LnKn;cTh2 zm!=zvJId>f`%BS(EiyDZg5nw{ zav#L;&-VrfC}O`XP-htL{ST0ojv157d2+B(d;f>!S}F%TesJK2OZS^qu}fBn^~>oxEgko*tGsOuK^uQAY!Md1?v!Z`34JuyUT zOVt1T^6zr!k@4%<6aNz%6Iv<{u9wh9%C$eo^52vGR!kXvI(eWMkTv{&zW#F-qCTQ0 z1}*X)yJ{3u9FqptKb#GxGs9rzS$IQ2!Hi9@b`79PE-2i3zU~-G=8XxMXp#nzgpK~K zb+Lj6PUSdcMbET&hXxp)qG#X{k22+P-z&n5zJNQU@}Ki={cRnXONo_8k|e3Rlz$2% zey0eOzg}-zIw!=Si^+0J^u=+x%ClapTZe_y^v!s+d@czl|JC$PtJU_6STzo%h#+-oG$ZJ7{i)fo6fM zZ~r34bhKPoomwR8UZrR{7_J8O&dMQ19 zh8C|lpYmV$ZGVFmqp=$^*X$+UZZaYNqPo{eL!1t0%71?O@->y?OcAI2A3@Sz`iOIV z{Qh3V*k2dI@2%4z&_-$1I7RKvKWo=3RrJIJIwk)D&C>OfuD_=9Kx~FR_!rd;yjIjVh-qCZXn$2M}azMbG(<@W4MM*8cj+2QX@}s zK=;hCaa$X>*M284IFxjXO(4Otn7a2zoi7rUizWk~TLE)Brk=cAPqjyI(Fjzrxd>9( zx7kAqwJf{C5-ri34bmrh-jd?t0lS(-+9j@5X(Jb4E*ra4pG`T944<+)9TtcOoufHD_L~Se&#oYKYEMu=v-v^fgL?xpYb0I2Y{<>q z>erAvt%{t}2I#y~N`YS1d&c1gH(TJb=v5+nxFn(Qx6e!(-CX%5^IOgCcR0O{zJ$fm z({hMxWS6e)brOVnGAHrabD+66xSPF%&EsF3v8$3XYa6ZNA;KTGHj>mzn`y|K0g=5@xU)e8M7Is1>#Up`Z?4YCLy)ZBzpjkTR94L^Xzp6Ip!^_x{ow>#qdpCNpx) z;9vN*G(eW-Z)5CNucBU-0w;s{B4(93vGAK+*5WwG`c8$4SI`>2k3QR}pA3oAbW2I@OppjNMyj zr+0s*x+wkX)U@|*Gq7a(*{|#ynnkk~ZQT!R0Av1UJ+%JGyxJKqsa(#P1@?^21&%#C zGxoK!im)2{fp<7WpgPq%)WcoMldkDd0Vf?8iQmd663dayF3Z!swEmCxpL{Yzy(5a? z<=*nh1!SC_z;@qlv7t`|@k0kc6&(%m&V%`Zs~%9{<*1)wuDrhT?|&m|aya~v6`rzTxbd}CWQ@Dl;QxF*eVg+F1shzdF9J zGHqK{Q?r9$FF{z_6Gs^|xE4@|IyCW~z3)~IkDoboLSG8AnwKW?C1pDjvk)cEl zYYV7jQ)7RI+^&`E2&Jj;`@uBZgqR(unVYW^m3db?JmIaIz=}+2p5C_5ud;rP`-5Z> z@u6$Z+sGLT8VbSW`6*)kGd;>Di23}r)Gq3Ir-5UO#SyjQ(=KE(c596Y@MNd1*5=JT zuu!WAP&2vkm^cc#n&c>ygstc!`;InZCW{K5Ug}FIr#%W(U&^Sln;Jzb!m1ZbEiccHEiX>@SBq}mxTJ;t7XJD45geiS z^~;Mty|^6tbj1{g5;GKDjiz(n-vrBQD_irHuNuL8 z-i@x61d~uHD3;ZbG#?9~DNEE|tivR^~3 zh~t&d*kz1@ebLCn6TI!{xtjVKw}Kz{Sd5(7&sK{o2aiC^AM?@!@Qh^zWf5+hOt8TG z_68$|pjEaDBqp%0gqO-~iBQFBuIXq|;VPkf?^3_axVgde`!sLG&EQuy=nJ7&kD>D~ z9j6}MeoT)LGHfyrsb4w~96*~6n_HC9X_5j6B(2{|#H_b}b{`t@IRSJ00?;z6P4@j6 zV7%J6+ro)UJma}WGgnRgz4hQzQKm7Xk4+Sz@5!7F8kL)sh`Mo3tutV=Zg9W z^i61X=w0GrSu*aIeAY}%^5Yg{8tMWjsNMPWVwONJ0~)13*}0eM?JeU&IL7WpGwVIn ztj3rQ9KvaKXh`*lX|#*(fzEpj(5jp-hBh*Cnaec|h5U@f#+^(BO&1u~E944=5;>d_Xn?KxFJMJ&-u5jIQ#vLecEerAC?V$ ze#kqse5nh9Xz5IQ?NZKcvklemmP29t;GpBJ-6}lXy3JNaq6?KJd>cx>%o@wC#99gq{U-b$RIvyXB_LB-Z5eQqmgfD!ct#%vfEDNRbnz3e3 z{z6{xY_3KH>H7vWXT#`t7Q&cXA$ptOqgOHPzK;shbNSUS+G+tK-DlnM$TZ_+v$F-f zktOc^MP)6kyV`qoh^5GL3xxYSmSn&4`Tpr@+shKc1HGM8vvWIvg?FWo(pp)Aq(p3I z>i?{%05A{u9X{*V#~3wRyQZ730l6?z-|DB>Fu2oWs`#BQ$Wqx|d?ysEv7H~>s=Dk8 zaaYB;$~)``$~c#8(Ko-{BmF|Bv^epN!43Ktf=U+dpmtT?DZ<=lJKHQ{Lg95xO2e0$ zo_7p@&n73Dk@UuWLqF>m`fBIA6kfg5L_AQ$^*Gr;x;Saf^qt3?d`jtmRnlUpP7#F2 zIzS=kQ3v{a^=2`bbp|=J7YeMECC}o}URfC;A7!&hTRr_L9?c)C5OfDMlwX~m9C%z} zVGr~NuuwB;)v$cN_MTCF`GRvaMHG`|b&D=+Z+B}jX(0k@)lg#`oc9e)v(D%Pbey=0 zbHhtswM>g7Y( zT%sIMtx=}=J50Jdl!sd4e3+Jxb2x`C8ZX-y-hXLV#qZ2$gGLCEJzgoXpEB-wVh4Ax z{-iN#Ysm67D+f5p51Y@4uWqMHd|&2qH1a_*Fa!7J`Vf7y=uw3k>Y?B1YX=RPhYAPD z*rZO5$g6_2Bn7W#6vfHX=v)nECHkhR7!e|e`kX!4&wB`bh$KRR^(0lWyp+f`O`o#m zY!&YJlIy}(I*loE^1J~RfRsR2HOkm7O+5}-c%}R0%lKFBKfH_Y2ilOT*7ZMINqB55 zNyROfIuppWUPyZ?;I>ITLxXS;+BIR;e!;tB7m1%RD7b4_tGlcwdR}LmU)*y3+_U+y zfNqfKOHC*g*oj8=O$cI$1-EM@*< z;=>)m-SeKVMyH7FiBboIJZ@kieSd!<=j9j#f9o!+*(+rG!NWQ3anbxQm~OAN&%O3c z54SjnJdN^6K0?Sw2)1F~NW)NNx3U&^X{w(sZ*`s5&vs-po9TI=H$=3QLwI*?0^BFa zzUBWz!NkYJe@W>uVjED*mP^8{&4p@O1+*SpD8U9=ZJ|U8Gbtq>@SkgyG_VZkvlS6e z?Q4)EYpW-G%d_i~s($);ZEQ$rrF!Nrv$48140fYVp>xX#OMY{w_(r&`g=gud1XV$B zL4zdxDsDO3;T`U#(EhhrnW4(cA%Q*^ulaX(=Ws%JOOtX{C*O=eD(LDf7LC38bffp& zHa=szSL4Ea_<$Rf1mAkjuV-7vSA;F+%MPR zWlFm}4-qwa=l(mgfsvwT- zF(QnCE1qX7A}2jX#&ht!W2gy2gsjR0k*6IGD9Ov7cw5$VP*_X7;lINvE}1yvK7UJX zp0~7imkMeOG|E=^R$U?yj61gm-t53<&LVAhpZCop^B5!KK{^m}0r-N@fklViS5bmz z!=4GkxqVki@3VFC%i#wi7d>%0MhpH7>%wpd6pg~x7d;YTcDwLgOM|;*O-LWjdCwk0 z!Ho8yKTOs8G!d!A+a{B#I76Z+$K7Kl#%4drbDg0iDfOmZlk*GzgI>m_7t^pIBMK=1 z)Armgx1aeYemG~&4ABReLuzD)W?O_Hw8zCs>wgroTSB)5L0x>9S2q!@*qDDVFM7q- zZ=0i6W-Kha-Zbv_Fx6Xmp*1Pc)0#gqZNzYl`rP$qkA(pzE(tB?08_MWhC%FiQoVN$Thi;7 z9(Yo713r5mMfY5=@!@vQ02GW zyY<=v@QISj_P!6{cnol#>dj|z>Oa{g3*n`Ly9YrT4G|dKI!0%NsJ;ySqs?SX#4M+)YrhS9ANVnnjA_O0z+V zbf7Hfksl^ma{&W|+1jpTMjqu9B6f9-`E+IP)yZwsZn18JIu&QHjqzKh!S6`A6>qLm zQfWG~921*6-AvSl{>b&EWc@im)cUj4Pm@O}H^#{se>(w;X)uA#A21yv93hn`OTW&U~Rr`LR_vAz$ zZYfiVZhZ$#!fyrLW7n__Z34Ubfs@NF*UBZL_JF5N!yy1vKw;AK>wjfB58 zDcTX%QSQRr%T-F-!K~&IDOZr5y-d#$0*b{L2JyR&y}s=y8IG>~ z!x{6iN200w2ArF+uTsj`wKP%~RIVW2JI`q^Uy;~g-}KG33A0Kt_Ac3*f8qd(5IWf& zgSN<~Eb(In4N=+(ol53+dn<9}ELIwd$^h0sMT*Xmelu$FaPBbs$>3%3AFeO+By_^q zt|*%N#ZWt_0|!BGXhIznOSaw@Yg=7VS_(MR80DIdP+E=Y{tP6due0y<@|p4*!BN3| zJ6LJ*)DX|iNw6jL(l}THyKf!^p;0G*~>Jf0F z)c2TX+@rpWW!n*{0P!jvFV_babTumgrpQvSt`M!rB$;XUdqGTmjmJrjQmM=oP-j#}~&fP`@lE_L-jM}#q@ zjekXr@5vO|RgQ9vq}V$~%(svSCUQg%pWjO@%{Fb~{CG^EYUmkrsaKkckE+|;T&%G8 zvxcd_6gco z;5lC$$y1@_wCtaJd`4d(&T?poSlK&FU@>s$S)Qrkp41ZcNDd@9f1FJ!i-{?S=}w|& z>|j9Bz^1ey2+5S2Er8f-$+erfO@%RH-g*e0;-4Jl@MvkQsj=yHtCo>}Y7=jMDXjqB zzeKmiT;csa>X9+jWt$HM!Y9OXfTTdeQF(37#d^h|hI~J;^zIyDi`oU*FKD)d7zrFn za+Wr+^uuMm92#v0(nRNr^(wn*r^ONX2ab^$qIF}Rgi3W=IN+=+>ot4IrY8`Dldpw76I7cSVm0!>o2|;4Yw%~& zFUv3)+(x`!{-m|Fw5k5)CP`kGA@AxZpiz-ZM0t{JQ52u6H9q zJZeQ#+U}h13n`~^@h`qUSL>tq6~Tngf`nNO8v0FU?U8HmVZ$7IS)crdL*lrc)8XLx zmP_NSr&8^<)`1PL+6|0!O6U`&Xr+RT;ASfc>mvB{E`$75o~VcUMGIP6rz;Azks2ko zSUph4H{Ak7T(6zi23dtVA5$ya5w#pOg8rFIQ)w%dq)(=iz)XwfKS>(tXs-)9k}Q^c z508S=J=2p?_3duAM&({CmoWLAKYsnE=4cLI(*$=dy-)ZwSOeD~r?xQR=YswFY z==6Iy(kq(a;&3QDn#UVv$ZJ{h;)}^CJ@+O~*A{lomoV4$=di$DOcux@8V@k+n zUqMnW9^jPf$qO#K;g=l~-*}lmrc*3yF^=dzIo>|}M0-!`hO9Uq!eYC)64#*aGTAhp zr|D>CdI5HMyv-tYdE6#Z4S-x!FEXU7(!nsJHlbx!w=%B^9^ti9PjCCSyo87zHgyIqaW9Bq$JXHx+e+8=q_Mm6T@fo zfvB(7YXv7v$yvU-^NdH@qi|^!Kewj9V%($KjaWSbyc9^#5r0PSbc4zCEVXOYcKtnU zzxXOU!GjjHn5Os_dg@yXR1=PH2HK6bpKGzcH-o81zrMcZ@4Jv0eC3iIjLw>#C6SoB z7TEkeG)hw{kI}Es6QC>f8X?SZP(%}Uiw;rgXswL4%FaY_d$En!?e?O}&L)0m7IV5) zJKS+wLTE)60ol3u@hPA9x(NSKEu$O52tmC|*@%ht*Tzn^Hvru3d948z2sa?T09G~^ zwoxW1&$i2hiHXPSj3_0!PJ>$-@O5oKr-(U!=y-uam7qh@nO#-M?Ik&fiyDuJaFF8H zi|B|ca_m8@pj*)q_znu$aLa5Qra)*|wUA{$xuc?BxUKtAIPa9U;9BBRgkZZNXk$eU z?Hr_HbW~an9I%#@hJ$CmC6`3mRlkE4kTy4-}IQ_*YRT96-f16tO3qWD_O;8pIK8m6+G_xSidpG%r23#uRj9*uq4vtm zs7Kz`PK=92j>xKh*(=TZGK^ChffD90>+a8vjP~5`B1^PN^_;HxHrIy=5D(D7$IGVu zHm)v0ajj{+TVr$wNucM_Q^syr^jAOZr47H@B9UCU+5oi&@`;1tKaiSs# zQ)HT*8-qo7ZE7NZKNk`(AqbW`F+q8W%xMq_IHYbflEL5({teYB$LH-~k$zuu&5cyfBN1w&Xk z@*GS3i2nqvkvi*Flre5c6afO-EVFIzFy7R!`}&pJ=Btj>o-peu_w(tY1=pwej#)$4 zmvn`s{b;gd4uG)lvYPDqR4Y2&c;&)D%5@aj-3PMCy;M_)mAm9PAgk^5zF6!Vx>z&) z9`T?!8(GJVXdi$^gW7>n`gKAr7g^9As{t0gd z5i`@Ix*>GmnoOfeMyVyUbtyu0Pk;4_2MX*vl|e9kX(qaL$_+*STM1t#Hg~wyP|Z;D^j@s zbe|~GScH5%mCspye2Js^2|2Y(K-nt`z-X0R_JG_hDYduGtjh6>N>#N@CT3r?fHt#m zct-JS$?=ZqB&lw!TSh&2={?lY zdlgYosY(x0l-_&jO$4M10Rn^~Ez|(gLI~w;&b@j(_j1M?+r{t(mh|yg=#ZCXkEs(5O;V9&dBwNJ2 zt&1lYC@U`lg*)H9sENgI6{OHhxGwd*ishBmJwf}SJE+Mem$A(6kCfne(ZZVfy2A`t zfAH-y()@US+kOpEoLck!aJzvc&mzZP+GTQA=uMQQ+55y7g{8Y@-($7)HRrd8X;ybD z)9=Phc6;m?ivDU|`Zc3D;-s4^I+JZ9jQE3P$GY=!$VPx8kX95w$^&kZzbUw`%|94# zOVO|&}N#Aq7< zn+vJkF^oaS%N5~Qf+gyDANUtauH2>MVge_R|+KX!n1l`3B|!ecr2d2P()(A3(Q(M^O@1=^Fp?T-s~Q0_bc zEFm0Xcr+3+UhxBA#&vtOCMwiT+-AtCyq)akr;IqiqTSSX{xIRNHUQqAaRIcJnJl+e zt>aofD*)?Vc5XhlAKn3sBO`Z`mAC6^8Gpd=B;xg*OF_3kc05YC)*>HBH_G&3;=Fnb zp7`D@|h{*;f;LE(#Gv8RhQ8Kr*1w+1uy>>I}D zKeZIrb>hs!e|OWPLN>olTO9bz|XWW zAN}wZp?V_d5M5`ci4>VjsLs)7ttQ8I7l&WH{3q}zchLnV-zd@V(x71QjcOcVuEp;& zrc})Z*QVJ3P-suxMuQn)fW&B0u75eiU@YyvAG|B#)kveCk9xa%IojKF;18oW0!o|b z-NuIR*(pIpDHB}o`W`;4GKe%ouYKlTezGahFK#gvaDrd%y3DU$Y-k|h`29u|=x(IV z*df4DNz9SrgLNsan=6AB0ox6bD}~LfwnzvTK-n>Dj!(6FJA9W*jUfL- zZy+1xDze*K-8^0!(JppJD+04Lpw5pe*SL4RH*W$C51X}hfDWiajz5a~i~Nry1?c8< z%WGHZh#zEwc9-EDODQoEmS6elzYRCAfR4nl+HmVszxwXHkd8@@Y3l>zVgoB2w3HUK zw%F`+v$#zT>rnPkbV-&3BYI+7j6*LXLWD~|&z zI3t=M?-RN|4!ydz?@tNgU=L6Bn(|td;T#EJ@-qjJReC3*g7B4F07m{~ZYBE##4V4< z=72<{t=FB&-JW~C27^QM#RHMf$KXTbcHSbD_o-Q-0tD9o%1~K@J-R%*eY1Ad)>o9QH1DH*m*?g{VbX7sl$Q} zrOetDk(rG;U-vYMtisUWBSBrOp**5F|(L=q;DJtVFkw-c-f=jmLyfP#je&+@fqN^qk0v;EYSl~Q89=nf{+7w<P_gG=}*2??65U(v@ zHoG?4*u_-Y9_EpDDG>@?yF(YVf`@bC+3O8ONlnVHhd=ikY^G`(nYD+Fjc5)+;OV-} zwGi@* zgiMNi+7=81*6CK^Z1ktGxC88MDAhr+x>OTJ#VhWkdNr?3%yt^YKw+JBDSFP z65e5?CHkiR$^0zlGL9Y$!}lFOQCCT%EWU`~>XV9J8J(5&$UHnE>w%vp`f-(sgi!J9=O#=@vR)M0?gda4a9ivwswKONx(PHX7Q85 z?c*QsrkdXRFtp>27c5PwC94YA4b*g|0yg}rbZ0xh`{;V=>`J<$g%C+PFztt)gWKSG zKNX3SLlVdnTxzRFNI}z~GgCVE5xcGDcy;OKwkFPfzPvu8zjU+gR|3yPz~9a92Cd*e zC@vdxT%@h-U<4{UEleOIHssh7o4H|04ebs2dR0Y;_lnUkt-v~!U0?THfnbrIQ`nzU zz0iEO%|PB@aY#4UZp;~+G07+is_^(mcIbdKr|oT!|8Tr-)}T}E_>o%hjo{2zGV|!o zTnk9mk&4vO=!T8wkG9FV6gh=Cz~|l6&UrW&ISuKC{*Y;bYOuM@*aPMbHO;q|%ma9` zx!kg4Ma7G+Sy^&5%dKj27hkrU*;|CL47kWiX+0v&lI_YCw1bmFx$GL^d5l_oAS|Zom@q#K z+WffW+~^glaNd^A7lRPb-Qkl>pGP@G#dYfkG1T|xUcoA(H8hL#+rsE}LkCLe-(YF<(keaXibkg{yqS>t84WM(dTv%`0P?;jd3-M95tvs+tB_io=pRzv!wa0C-rr0o zujlJ@WAnh&cVw6bV<)s{$^ISFdjoO)HGO7wJD}q*S~}nVBaru8<=UkPi)<5eIiU7( zYI6f-a*pX2@-;#B!qUPdqF|IUi<@i15uBr5VDpeP<aNAR*G4-GeI zD}DRD&}-UlydHj|t6UL}{{Z?E*SdkckZZN>R6kaR^IqvMJ_VUv0A6{EL?c(NP5fjd zfL3Wu4cWJ8(B2rVzanM)T!+GJP}Jjk!4P#lNy_tHi);9ZR2KBt8G*kn9-p3|Y>$bD#REU2R_Oxl`KRa)>+ z_PkCQgGiq^_KSLE>?<6%1ztFv2r<#il|Ahuee0q+;oWO@WBG0D|5a%b2&2A32~g(A zV<`wl;5HoIvvN! z$1HnnY8Y@7&d|tB{VNvr@}1<7qLE&W>6ydk8#sAHvW4eM*1*X`tX~E)71v&VQwZ{5cD0_fxYi!aI%PA0|f#sASgC ztR%mf>91Dxb!tYzalm&q^Rsef#gJ@jdQxNCx`&zIiCH z59z$>(7IlSAewABD=9@fm(sEH7Bz^y^0IZc(|edtkkChb4=AQKVWK~WSg4O}m2v{Nk81uls&5x6t(~hjEa))CkI!=B$r*efXm?rFPLA)ETf8>V zdfFQ}p1C3s39T=k=IYUC&>Dl26#ZFwKU+r4ZNd5lcTP6$ZF|T`3qEw?b>F#DZVhLJ z7kH7dYImtJL~;H1G^6CtDP?7hMwvc(OZjf_w6C^A-(%;o5|bcW(OEVJ91w7Pw=9+P zH_HDy=~%AsrVrj;xvTX^Ard%E4{(e7Hq|^@-zb-07zyT z2GR7V%mKo{#k2>$2Aa)DXJ-1Mb=K(mE5K-hZVuLr(aU!mPNn)P17B6(QBwu73hEAI z$C5FCWWTVp z&!yLi?u?`-M7}?r!#jRv)grZFsDwbYP)#sNPvxA9#yCVoz3Dzay#6tXfWx|exjTA% zv-R1(%g+6cslVPlNAR#N*}Znm4Pnqb%9q|N64cc9ad6S-DbZ`NPI6y@aIZ$8R_{>p zbaRDGLx@h9d8tmNvqfC&``KUn{~L|IFsIpXO)5`PT}r-ZQct_tw)o>iJKx>YbgTyx zk9Z^?T@+23r>QRdO(%_goQ4&52D+UX&srnldBMQlvy@s**MCrT9LaM%oKx(i^*Zvhc^_`_AMoT&&o3&Vr_8n_f z_DpM!FKGGd&#GX*--b7~@qnxhK^%IzZFM<+u%|TIl6>tXQ>VuCF^WW%=mBs%f*s8< z3LVNgC_cI5ms6h+M-E;(G|}#0D0j@jQQ?fHZ1cI&+i_ebE}A96YSQ4k|oaeVRj$R3x6wbe?>uR0nrJrW$=j6N*C4oNbyTW{Kd=8 z%F>1Rld7ytbStOKd=Q>y!jN%unTY+)u#yIE5xdbS7mNm!SW#L~)>bcYV9&=x5Q3}} z5V9HC*?7?OfHOg=pVP4ZE2m!VP=RjgV1YqhyH%ZG!oO?x{7#fV0%r*X`QCdkYvolW z<*F6tFQTP2#c8+OwNo1*wSp4|RaFeM``n4h0L|0-X<=54NC zaZ0Xc9W2){Js1KrXwxXr=!^j@s|{15t$gQSaNb{OuBx@?hv4q5!Mote@k;mV&zfLt znFXo}Fu*?(ko2q!=POXlq)c)FhEl$&{Xb-ceH{llDK&SuH-8xBnZh zex2+a!GIX1xwm#kOA25>;33d425Pk1Ir)p1|Mnf}OTbmb=Q#SaCi~wW{(*-*%@=E0 ziLU#Hd~zBF`4g{$s)a{%4dCu(0iX z$r}2z-~Rek112P^`-d%Q0v3kS_H!0F`|X!cb^lA(-Txs;`F|UJHaq=q!_T72|4#V- z*Aq^Fbp!n-9;9IxHH!4dt~)lL%R(FaK-(Rt{r@0*{qkbt%_81-}=e1PfW`*A*e_96K$CC$K%VXDnYsV&?&D$MK$WmPBo*WKo1C3JZE{9^?ZRK;@e8x#cCQFF zP%i)Rq7I|LFnI{SS7F`Lo!X;xx1|R|ZiF+s2n*^2T8x~V= zJ)0)2M+QJZ&ThQUZ<_&l^8n}5r`VTfK-QYx)uPWVKbIG%8aPFcA)bs1tW35G?FSUP zeNib~kIS*Mqsj3h7Q9v51QqsKn}Hl7r~6*hULL0T)M9M4shensns!etI=A&N-TQ_V zLD|f-JN9lJx`^hXV5)7r&##Riyz{M0^NCay&};mZp|GnnBu0Kh-=>FhoR|Yd2Opdq z3_Sqh={&D5WETdqH^nVYBhu9&(dn{Bo+33Vy#Za^Zz zZyTz1+BuGqH>e*laOG?IBP4V>qQZ?=+4K{<-5yA+dzdvoahJP(c{rFsBV*4n=Q z2kJFW1Ts$HPyCA=02<#``ZI?!^q51)I)3rN&-wg;m#Je+jeJm{3Bgr&c6TLFx6(FgMaU+BT{AD?Z7h4@)&fVu3%ooKgPSzte+j859NuX? z6(9sx1Zco01bF88yW_b8nz(dKskomR1Fr2@oXE4c8-I%Es<>HPf2f|rw~~+617)$+ zg9VAXPt(DM6W2t}z-i3$n}N8Q-U3>I9>8baQ(xm!UYQWX@fHx)cX-Fz7s{!A)&gj( z@x^Xs9ZfMPA>GjrUZkxeibY6J{Q{xXJpA2p+`y~~1Z@#TeB(t%!T<>S_q{v)XB$aQ zHUz*e6f+g-64{6Ipa4(+o%AR+TN!rv4)M@db#(o=(+2iFF8YcNo5Z~ zs2kMo@b%q)Q%9PeV93T*3Ym`}kXh_Wgb?nX0JTR-Z^e!|qt64%3G4H?KK1dnv61W3 zmHEA8VaWq_nb>utLr-xuYi9Yy`Lp~om$Ri;g&{qF%e=9H<49}!ag#rI@C(tY?{fJR z(j;3rzkP2h>Y-9RKKWK3qh(z?Iiq0In|D_$=Fcdxb;AG;8nyg7Jwh05r|k^{0{S{M zp4iLPma(Opp6}Ak(=E5;>nH)?$3=UGNavSc_1ns09!c|E zSMoo?ZLHRF?~u|7^T)a)dwWWda*05DVklPZj@pYk+_+$&2_;W&pf(QTScAO^>Quw01WTK1j`_ zpYYY<%-W4NLeGyB8Q7wFlWhS{XlqU;P@~79;f6I8WvK>brd@6=lCx2#mvw_x^bYA8L ze5chgG2n;@+6;{r=vEG1Vie?U&AjPzhPDLgz;_5Uo<#xJ59zzX(-ON4Q#Wg3d5oq` z&({=eqw%O6z&!7oOL6?h3crK}mMx7Y%Jja%ThhBSj}%e?A+)1I-I=?6Y@i4Tj-Ie( z_$kBxrI2@3GJft4$IP-IZEZeg z^WAEDI|MN-WmdBRY8+;bRDB&@9m!vW=G$nCvVCaCV#(S4es2jcOj zE8R{!t8aYCA<@4U6M)wSG6fb>4t{dhAqC>LS^#F}*e5mK{)gw{fIaoU;b(Eo|48>g zwmFk8{{K8OoK*YduWwz{;QA~)dLD=&R5zaX+vCqp9UQZ({UHPH6+#Gx9_yK-j`;cy z@7czA&1AjvQI?SjjOsUvvW+V+dUM{MQM|cg z9OmWuXs$iLN?CO7gX07#P-Dg>%M91r3zVjVFnZ4OZO=stOmZ6jbWoiN`ZPH}#Dt!2 z5Vy+$$_j!PVW0%C5?vypo}I~vk%NH%oNzNsG1~Dr4aC(a z@$p3RR@&I7#B{9W7lMUH(`v`^TBy!HigryI^iHm-d_uIQ-0t_y$`o~N|1G{~)T zMdh+3*;}UAXaV(;)EE~7PL8M#FZ(Z*nxOjXHoDpWc+bDB!2j@EJW$Cez;mNss*lNn zCh!Hxb+6HuEK7{Ng}vqYj)P*7!5(ouvQ){|1)g?1<5?RBya(aChrR96k58T`n@VFZ zXsE@ma_nyy!sr&}g`312$y<=64pct*%x_!JSUcv2Nbcn*j*w62d(G!&gWwOs8JX$Y z!8y4)>ZMKuiz8@l2`4@C6Ze-bt>X(h1<1z(rg>>rB9>ol*Cq<|qnBm8ey_nEc^tGwhP>=tvD`H5L6ln0T84_X5kNhIkld$ctzA9uhf6!l&}0x(gaHtl z{Zv|Kc?7ghWQZjX?7Ce7q%T2P!|9>N+D6um$FB8b#gl{0Hg~Tt#v3UqkB3(xbOo$+ zZ?avg&MNj=YpLsP#Y~xu7yG94BNSEsWf#xWx`HCxlQ@Hg70E~b2h#Ol--KzsaShkk7@VxT)k}fnuyi9jR4+h ze~e$vhz=u_7O%#AGpXL=Wy^A(x*)|uuB1ezRUv-%F_|}lMz3c~FZv3ae0pl#=xROi z>7<9=p&{D-UD#r-*bfE~Qels2F~K=E;)d@+M;8)-!egL`uyqp^Nr_w6d3K)XrFvKe zBv{w^(U*5CgP>YmaWGAtC5R<7ptn~*=+%TVq9s_9pCM+l>U#3#5Nk`CJ6^|Zj*i>ell_xfU;IczqZBMm&{#e|QVaptp@ z_vxLIAhmDCCXVIoRht@f)hwJ!HejF)dEF4AcEK|A+GF^`NJ)E1oT(bG*TAR83q>gU z-bknOWmi1WyCRAWGQHvB1J}jA>mq4;bcx3F;Q?ti*uA=N1^)Qhq2T#Q0ZW3=+~gTj zk%=33?$Y?tq!H|uW|HD|)-AYe>K$~(g13jM%=GWpJDvl5hi#~E*zPYH`ST??41CEM zn(fgkhakVgyL4HK0R-tNrXHpJy2ty2kCtbu2R7dvx9U)DA$fF20{HZ8k+4U$NGP7$ zXPEFw38W@(+*dI7%cihicCj;)fu#=Zw0*Wj57q7}0pmu)3D7_Yx%PW|g4ol->6zZ3 zP#<12Iq3Fs$A-_GB<}@=fxFhSJ99AjQ`rQ3jIHH*?M6!mw(Ub&-m(HQFA5r?4V3c@ zf6-i%b<$SRBsPnUckXg$sdHFZE|A$!XN8Ewd1Oz=rFn?Xrj1<%>!f!i9E zQ$QH4Exrv;)0WMknA5ci9w%%ES5j}q7EFS!D%`Fw-Vf8~a1HNOb)P%ZEBj@uCa-HT zK3OP)slsqlEDK3%&@0On@V+F1e%aW^#DjcbqC(A^t6zf4zG)6>{92FOURQWgMPE|! zH?zRl3vl1miZ|gz8GVAdR#+As(a&4t;ez+?!*GngxA$F0G>iiLwzpoJ?8$DtwO8A0 zt$`DAOvmmIik723;Z?dB$AIrD!mhEA~W9W!v%toLLwV zts*wHQ-)T<<+caYLnEhsT2sxZDwG75j=vA~23<%%>s2|Yqtb~|6XwKH=^}9Wy^v*f zA?$3ntADMCp~o=geNaFBm~e@ZWWX`rcBP**@5(Sj@mt1cqbErko7=k~ZX2FIme~&F zQDq#s#F>8ac-~&7fgmme{6am5ZAIhwb-h%? z;)SM*T2mQQ;pLu3orp?VYjsm06GW!{MXbFi&L4T|Rq0mpN~c@6BQ4m}dkN_ld$2;%$09x}XIg2t1X3nDZcAk5;1iBF)t1O7`KI$FPWCE!F)XlcMeqAnlZ$}Gmf zgt{h@mS!>C%}wTiQFbx^cC^^Vu}}U6E^V3A1ik0Y)q1t1?5DlZ3+M5r95nCl!P|w6 zm)j1Enm#;ObpZE#y&}^> zCe^7vT!tAp6WPXwY(VkJHjE`xcn6GbqaAPdaaFYls7Q!j6l;$)RDZ62-@uJOs~J1P z5J3Wsa9lkowxJ&Ia7hMZyZ2jd9uBG~(Ti-!2AL3~9*HB-4cVlliUJ$;lKi>w^2fr` z1~&37<^&}v0V}014Un$U;X%!{B%j!NI0xm1w2};l4cXpkkIZKA9Aa+7`$YA9WL*3} zDLJ}H?#6}u2-jsTRnHbe=(if(rWwKR11`W!yH%a!ndO`Bk5d?b)GvF3O>gkuZ56zB z7$Tx;hmR{s6PL#%J2^Dke8Na9d|d7I7_N{Ztr~oiKyhg|egMBhBUc%%OVn*6YGCGc z&Vp7_>aa9pZopD*PDYO)z-ObT&hHNF#xUyh@<}8>4 z^bg|GOsZ4fk*a;@1OM@rxu-4OC~+t1i&2Ql?+&VG?9$l^Pq=8wtB$oZZ1o&s_~{lV>ztI z;KkLzgL6>%+GW@mwSwG{T<{Z{nlx~p1%G1WpaMr?%4B~8X|T;vF1(4T{Ha?v`4nR` zKMfsswYhX)5N4VNGNR8cI(pSWYp!Zq5V(u!43q<3A`IzFrs#1BYP^EA1rcar`g$%0 z_AfPV8w?^BTKa%FxSmj0dE4)&gX`juGM1DOeU>gmAFoGhyc(cQ!)TL_cN zYrh=lb)D>Zwxstu!tp-exFH^4q`_csN8;H=5qH-vV=KYbpuK5C{mxp+(tKD)`eIDc zxmfdF!9v*hiD}Ts#3G~D3p<-uYdO^KUwj;ZK!%M&zn{}{_2!=Ia2M+mH?<`)A0%m5 zm)9Os%b-q9^^*q0`&k(LM(8*pz+cfl4$^u6^q=O^tQ=E0I9I@4ymIkzreBwWUokqh z;_H==>oI~#*I?Eog3BQ@8O5yGoHaRa6pAkLnlQ_;t5!`tiF2$h^*~YC!Myn6FfRHb z@)yR(f+7WZ3oiagLLVo49U71NdJ=x5>)l4L>LWjX1r!{@fPkK)_=+{aaC)^}IpG1b zT-A`*oK|OP!lz-#&V$QFkMAG0tXJg)%0w$PNt!-Mw0N&CfTugMu#D+LZ>g#dZG4c` ztgiIa1VysOPN?l3Z)NzV>J`Eu_8JF5&@s-GPs$ZGJRv|sSi&Nk2eO6=01fHK+?Dg?Qn z-194v?Q|p~P1CC{-O^mQ?sLPM;OGx+**no}{>Fs*aFM-^3_W>?2l=GJQn?80 zN*8LSY!CGHWOIIW>G8XcRKcVXTXQG`F(PO))$V$)$ipcQ^PxWOJpDz~<~jt3jew=6EVYWM3Vx^>p8|)6>rl_c0fmX)XLk3ylAI~XtFrRU+LurHr` zSI4w66PxO);PL*0ohQ3y$KofweEt{m6OPZpl4`MoGTK_V#a>nWaSE%wEi zEl0)5c@j5*WVpu9rMLuD_n-d||_<^@i-}`CgC`_bsDk{n0iLL=;@p1GjXQvV(5(ikZ>oV$%d1j;p z&cB z91fdL<+p!C-i|aLRL+tu2Fetgt`y{nL&~UpLMVGi+)!*yf~8Uo^{tv(oW~u8MVPcL z#`HKT~;0dF;DA)yoac$76JW6Jti>Y>Z|olN*amE?l-7z zm3G=nSz0Axo0X8{)IB7q^xAFBDh)SZsRrA~vqhyUM$C>uj2E zO#XA*sSu_BQ~yJ!tfpt3mr{M(Mei?v(*RK*8g28g7Ot>J=w`y)Zr}T01JV_NQp~^2 zj|ezwzB%x?7rD`xN^u#PBNLvn&;^YucFBLdT^KD)=@F(Y7pO)cbCkhKtObP61*i=K zYv!#rw}QZ~g%pr%{!!K2j8F{)P!OjA_?`ax{V~Qb?eA8BhVD!ERrOck8?_movW)I> zgB6ObIWa5tdmp7MUS>BHG1}Mf^prSP@~jja70eLAYGSMu+$N{p5e;{x@YB6wgo@Y? z{MAWxk9$3)+u~A`9j2^$x(q$dD zRzvG1u@xSyB$Ud-6--`C82Nf>j2k><0JYU$qXb z%oD$qMM}7m$Rbgp!g}B1I97$27Pa)PVT-pG3!0cV0yaNN(Op6ms)MX``B-x**306C z2A);ic3~x#a{WL&%dwfV;(+SWdA#^_fiVW%S5yNwCEo%3Ed{#hMUefWtb9v7sfZls z6*Q00NXa;-*7j{nWZtpOs-UJC?h|6KL40_PIMuMpJ~_kBbLY)I-4z=iEMUjW>q;gO zalz#M|JVjYSNAjD72k$rDHlTJ;eQ8Wo&Y!#cct+i*>guU8kDmrawk5Um@2Dt2RP~Qf|cGZyQ}DU#DH=UYKFsmaw_=n)+>NV!eI3 z^;{Dv(;fhe(Acu}De} zO57dBzrJvUbjS^AI)-)AY4yIef(lDuQ)c6^{H&u5o$d5>WA(q|^U`6`V@(%LwM?(# zEuYs&*1sQ82qF6TgGotF*A<^WM$aW&=xSEJ_MR0{)};H?-Dfflp|U>MaIGtfUwi1^ zXtiO$7J}8|d2fbyDhE>oSTXw_o`MMs??l@iYHI8Aneb@cVZL*jl+N;M#^Gvl(>2&T z`$w61#K~Ju_Jy@8c?;vc_ksvWt|q;@qY}JYq*o{&%EpQ7{8Gkr{dR#W9y0`H8(TrW zG3u@o-xjkfaVN>l(53eaE`RH_A#T7o9kKW6@rev|r}yDZM=$}43{Um^)~Mkaxzv7h z!FHc}+pfS;2FG}(VFhi-5-fmc?wqU+JsI)l~<$X5OEnfq(+i z4Fzy$3p9gX@Sew>id5nenYF`g-Wm+))1Sdi>k2OWHI{MhTBwN2E)s)!)R+!vX8 z&&xIZHGs9$EKZ}lW_dvEVX6vr9{j4f(w3)A^UC58*3$Dj-D^VVHSqYmU4$Nwi5ly< zo-HVwU79p=O^RMQm`#Kuswg_OR|F}zEZ4Zdm7igqsGOsHe;6$=+2qTeZM#_jSCQ{> zp64iLRnntkjlp=oo(d`xZFhixU;`B+y0(wmSi*{HyV!~5$}QRA$W#&%7rvf43jmG9 zPi@z?$t#AlJi>s>K!Dx}VI>&N1n!Yb$VfA<(4h1nY(LT>B%RSqjwP{agq3fb5#MUD$^p>BuqaGcU$AN~_vt$1et!uwZ*oufWldSuk zcH&ZFAze(#;Ubrv08vWwz@jq_S?dUE6)8oHyWKJY@1_9DI4Z-`AF z=f8&`ZOO~kfRYfS`YqeOxCYEM*vA_aV|a%y-60$XOaj@$y|;}hDEQOAenkB_`dSM_~cu<~%qYEBh3>+Q0=&V?%P z+uzYyxZ(lax-MVuAN$?5`;k&tk&m=;N3GJdyRCJ%3)L?OSB4E{B&bbF^hx;q==plG z;t2_tQ|Ep6n4ghBN!Hov;b~Yt5yx&gLMNZ1JmrH6S!CiM9 zrx`?&mZMn{IvA^wUt;$)-02}2t(VpVl+^m18fI-i`Xi>JO6)3h%e)x_O!whL-4x#D zeZ@_zcBYxJTKug{KD@$rYCG)f5}x);#FqHp4Zn2V*=2D3!$4)I^+<)&XV+L7dQXF< zm)3MOCm%cBXK`^{a~7={T6-v%Ggjfm{HKfKdLtfJE0foa+=odbQ@$V*(Qw}_Yv<+B=aUe_qT&WP6#HLhy>DrET=1d$ZUWZCY6XevV@+e=lA1VY|ipl1`?f~oHbwL$m zXhyC(nhy#Fd=82fUymegC7<>Q(cu`x9U9hc*pN*W8+PW)WVSsu zs@)T^5yl75lOs3Ts>;8A-7+a0z4#)v@rWCHznf_rg$+I5(!S)!ZgLKEwa})BeLa5c zYKQ-zsL0w3&J;iM*qM%TyIoJ7{L4<%dfE|2>6r%kefuUS)z20BG8BhQCo{4{Iytvb zB&sG8c$Jvr4ToCqjH21xf-&9vy3lDUvf$sqw>uflWjZ~DQMcv|xX%Gyt=&U}TT=ZW z^(TW6hnsVe_Zjm?SDGqqhZTt(@07&k9h8U>9&F_oL*%n+OL0E{^`jI{BKqSn)H?~} z!1$qrNHgj1a1Ile$HH!$H}!g$byC;1FtJ2GH~h}#X6KiF#cRdX4g{X5Ovlw&tIuqx zP@X2iO4|`Zr112(Rc&h4=t_DUnG6ou?{*L2vI+nUe3O0mgiJ&Z4Bbmbi_|X&n9M#Z z-y640(&B<55no_*lwYKdKFnYBoNH%`|32t-^25g=0bv->T2Q;fE^{BDMig9BwO)&w zC|40tqJzX5r=`J}F_YGWml_A^MYypESE*pkemtud-k22=t%5SsM9An$iu1FMxlcM7 z3>zM8EU33nxD{){#&_&IzFpaqyk?{z0dyd{$6T}m>x&hUZL%_iLGts0(jZ9=X0^!y z@4P(Z7uBmK-HMmpQMp8!^^~sjSTLN;Cg>-#%>-x?{|F;T(Ys#ih4|FAq62Ny}PG*|5>?gD;RZbWRB7 z1Pr<=Rj&u%{F70z)JDRoe#ft@X{@Q$G_mREBr0dAtys(zStbY8QKn&kpIGmWAsK@_ z_m!Q-WXCRA?D#9D; zTBK5aifh0o>8FmJ#zJ-`#5)sL(e~d?@3Sd_9R8;I)N_x;{WbB2$kAmA???ksH5q-e z6-jx7&!*hbTW+2VksDWI^n11aDH9{bGR9p_9`yUUsE>BQW(kovdE0hX zeq=qKUIBVzmu!7});vliispsYH0k_I70a(K!_wd9#P2HEe`2vu&XyHtI-fX)!ooH3 z3{&+i46^g?I!2NjqXc$WCOgD89Z|h8ITf2T43Yhd+)=;h499An0(E}2Bmu@9?vlef z<I6idoht@o?d<>F=(l5`0m*0nm9G5nHT)iIxb@E&pTl~{=MJG5pGU2v? zKk(SB1Bh{OX8}FMWNc+ALb_cfVip-Qj1k8njMh8PZ*g}To@=)n^n$xb`UO4r&Pz+# zdziW5meN+aNZTu5uRpx$7Vv@h2b@1BB(Dfy?(Fl(P!5U0L(1Ee0AIcEQLPAKsXAJo z_FW`w)lLSs4A}*9|X|67v(=_n?AK-q82@io?7>AU(upM1LX<_4!o*L8howr37U* zt~7qd*wEYk**E`BNySWldcwAp6^|FQH0Sfh`k&$=Y(y#!4pwO^e_(cFa^;h~HRdJb zERidIwWWn>d3jlRP+SB$Trvq$D2^=1vDo&q&xMzO_rz7}UWZa0*|s-gBS@iNE|_+S z8ERgb8h5uJE|8!5F3u$69R>;b5nrZdcIgitUT%c~&?W*-k~SOVj06CFGTj0pCsy#Tz4Boxq$zwch5yvxSQR2Jpq3B@($m^K_$vJ`Iit3$@hoH7+rU-DfdqgxJTs}w zf#UgJ0!6v^FU$p6ST)p!mpOYB@&sEr+gn#H;6IVd0eC(l`UJ|ND=Ll*hH-(mW#D0c3>^1lL<{(3aI zp1_~~bNJsL{y%^GPt*OU+x%-;;~#?4 zA0TvTnNE{z)2)~L1?y^J*Xwxs-evb3PIuw(-(!dFxAfShNVW7UV$NejV!+<8fZ;df zg2yU-_=Cd@2>fUrAXH1WUqixXC3wyNNy)dk?XikxvrA6^uH3C#fM)_ET3}dwNcm)y@;p1@crzB2z|7&FZx@9_mfqvOkC2zoQuDhNlu)<7jX`>n9KaJ z6+9m_+7A%2E%>{ZymA=cCTjw#^e}ZmDyL2lk^MtH?INwr*PR;N0F&FB{Y_(F{e716 zYmfgoG~Xaeq1;HIA21kaaSom|_q)yU;+Y#6(%gF`hpyM)8+)R(GnRCclA6J0^dDNk~rkmF~abZUQYIguHTU4h4>TSz-5ENzuT9{|1p z)nVVQBWH{9S3$eCDac#JC1y(RBW>=+kbm&8Fs}0hBe*j3I{w!Y>X`|Fqzd9gHL`B z{jmJsY>ZnzYYRiG-HEYM@Y@Wkx#rEFaI4oIu%AYB_rHAo4T_$qA9sxT+~bAb%rP>- zIGMU3d>*`+j^fts!_6R_Bd<;X9LQNryhT};`%6yduKj=v(G6qKH@lzIefNJw>*oYI z1<`1lvyt5pcwx&HwAh{$-6S(7seecK;-Bqz|2LlaTc1kLwJGP?{*N=;NXPOD&DdDS zw1p7joMxt6zl>`j-DLdxmxX&f9Y8C&N_juzovL`$cIp@<`-f$~&L<@r3Bb+XqH6Xg zk|)OUSx@2AQEBF{L$ zZ=`v+aB|$pw7<@EZl-RgvZ$=uni{v#$KG#inxV>S(Fc~7cR1R7`+GshYP!M|8!P3Q zVXtu-Oqe6A5390^0|r8If)*H$?2FRqxnkJ^!iATG z&PcR)1o2y`!BaDcwYgABmHk5vgg$%V&V!{!b;r1yzsw$DE{(O8zzX8bB&s%+dSGMw zlA~=8u&u;Of8d$Ty`+S$!F|1R*g@VURwTDJ(nQPgmJ_A_(cLYUe4lM}xKlnL7hk;f z!4O?9Jlb-!Zfe(;Hc3*Zk0QC;-#zY^T)2s#mELKLa2RfZn>eBC``EqCgaN%6(T*rt zhxPu<)T&^vyS(+|@Ln}LAEL#56yg9Rjs}b+Bmimf_)GM9{+Jj_z7(6)5PXKz;bKHz zim9suHg)^bM)OQgmKaI{mjznzIc=^(!vvhMYt;R~J>D>CH`)jWtP-B?PXB_+Mm;OA zsGdB$+@O94ak8;TuKUE^V$M*;%jIt?ZH$+IgKr5YIx0*~2sZDqm@#KM*St4zSWE89 zvRqG#mpkri*&pd$2jZ>v3vndw1=Cs$R89I?`ltTtCpAxR%~?fk6)VTAFYV9NJ4J7# zVPu=|=ENn;JRyQQ1F~^s;6Rx36RkO&tURldl_v#0zg@{Z4x4gmFpn5i0aaG$z9Prr zbmTMLcIwZ*%$;p zi5sNSb}(VOjv(lTNItE%OxF||M2HK@gMwi$%>@pw*ue6N){J3Uzvb^bqd9kjp{9V~+)Y0elLy{Y2oBp9E_(CR!bLdLp_;&}gV&bq>Z5!Eyzw z#dW}lytE*HFkMbCLr=k+;S~-JZBA~sPL!L$XK2ZV3`WbK;ow$Hzv&`S|9-RXjOLE! zSzt#at(-6zbZpY7E=cGXP87Sy0Lqa)-n3~almTLqlPrF-)$M+xks$(N3&?FzO#A!R z1(uic3lh`biMb#AmC8nMR|^$}n?$^!f~3r^mjRx#) zd|}gpSj5XYMp8r9K73gnheu-XRj|!Xvii*q1Hp1$t=EA;V$Joxv;e-b<4%QBEho5d z>wXC>xFYql(w5utjJAF!NOm9w#C~uQ?@A0V2{@OS z1kcWm)+AAwF=m3>PL`q@B!d&Vre_&RCnBtJ+h0Vrn)F@`Z+Fa%GAvHJwO#Bx+VGQX z#f}RD{_}>2iIFBdf+=wjlZs6oMzMbu=A}Okg2ko2`s#^gKPjZN&7li#WfR9 zxb;vH?N!`+8kyZik{yYY_PUNtwbx_p&gsSrxynpg{A@=S2yen8KUwEy-kjqIn1PP> zfMnrbM*gd~z;}CI-7ZQSyNVR7FQcxqI*)bun(2iSf{4>4(Nst3YlwzpFmsbdNIh*U z!`B!MszOUUEI4?GzsXD)6s%0RJKlJKc#(oe*a<*hK$JgLMqd$X9FqHf(j#-QpSUDT zoW*Pu$>QyEKr>&Q+Y))_tfY4WP(`WXOV;>6Fk5(kmN?&`tIV5Sg}_rmOxOMAUx`8i zgZp;m5o;vD>*n%O7=UczM{vew4xh|5wq@%*8r%=wS~~XM@XPH#kWJh)(8wCHpubkjD(XpT@4^u70)My281LM#QVZ= zJ84LX_HDa@U+A!&dO$u1Sn4)T`-1@I7K2+)In&Jay7OL3Eryclxm>D$+@M@>Ev$B- z7%TdGSGB1<3CcTHWEBd&UNs^{kVFQbkJE6P7FuDnc%yizj1<787`lB0n)}+xeZSF@ z$p{*$p-UVC8i&KBp^-4`NEij1Rp5vVR;BjSFWiyrD*AO?XfwbeJ%dk>IvZYAt1Eudpe?SYm1oRy-55DXzfY}X~d z-W&bZ$;fMT9A*dkU(Vq7R& zFSVZ-oSEAa+G(R4EzQ|psG=iy-AlIzIFrARoF4Z&Ol4;V#QHYn5(a>O@Fy=iDOBiT z00|Pf$vN*PgTW#Q-xD98Y_SR-@O51e=o^<=V>-Z2Vj;P2^V(d!-`Jb-HrWpr(eh=5 znQ9m5N+WTWoGKz(+h1y7M{dI*l*9rb^Z9^r;%cgUAHFpWB?n)-{^Z$vN56SE>U2Aa zN1rt8{^kdRu~;YAJqUpBUeRno8#fiQ?@2BM**q3D=-ghQ-I}PNZ;5tgJu{S}-a#N2 zRL^FHYRYHWD;bd#w|T?X^dvTXDv4XL>@9Jmjao7|vvnfFDcPTTFq_zHDuac>EhJT0 zeF`5ziEv7=({T0}Nx5jV2A-4euiT0AN1$XttHaUs0K9kbW4{aK={$m-olTWc*0KT* z5^?|p79IcP7FI1xdYhPdkJUYYQsY87lBC45m>|u3V$-b<{K5m&VFbRavHtR{%WPBX3&F)OODZ_pH3Tg4t(W>B5 z9iYfJl@hhXYGiLX&OZ6^>w5{^2W#6l!qZ07 zOK}yswGjtWgyHwUs6h2JLQ?E5uEV}@w+vS3ocdQYqIrS^xcfqtfUo@lW)LOR+X z&xZ)+7O$X2OWb<4aV&wL9FUQuTmM)D?|6Aa64jk%3JqS_K<^S8y~?Cs(1TaSpP2$0 zms7osdHUNUC@9XKH$h3c+D8;YM8LeP^{eeM>Vf69O%OWXH^4s`R(+on^={KIa~FUX zvUgEkMafJ?l!-c*t>h`WX-;Jgup-!xHT7@|Qj$K@TL?X2 zTLyeA>3wDF?jdd7ck@~&3byi1S1 zNuWhue!oo2c5J0~78F2~V@olF&s5}QE^V}MyU_hs>Y5^up_kgm_~Pw|orsa5g?@cj z`O!bCugm0+?>ct87uFbu;ESC~G*E!uMz8yt zf{;bBgsHoEw&|HUXs1eIu5YT@hm_MXTHEArx+AG5bVt(!S+M#1G2`x(>&1h4bu$h|Ny+kgFHKr!a#e zQ+ajsT^(HAAeC8jU9wBTgAB7o`V6^c_{KM@(w_$fFBBa}pY-%}Lz>YyD0CtyADqda z?A~2Z$#kQR$b0cb(a0Den3%dIx3FjAIE)02%wMttcDKeiuZ}l4uILy+u5BgE4DuOl z&6BdK4AN&zWigHPz?orX5)lk<3_dBV1@jJP3TnX=El5_{fuI{zsfT9R7*t$8*so9^ zc_U%wvK(n&G+aXIV17$xQWnK)+JC$f&)xXrrR9M|xm0~P&+%;9BQLwDfh}K{mT~e! zJ-zOX#GQB*#;fCXK*DX|>MgP75sEX`o#;8MZ{_o}a~H>cmN#GW!H!CdV`?-iS>+`y zawdb&P9{kHpVdkAw2^6Gw2#*bWoINiQ#F_IfsVgT{ZEc_@S5Zq++pY%v&7qLp{nXf8UNo#I{hvM~Y7#lFjZ^hB+ts&bJj ziJNPkHw*S>H~oOS(eQl8qDf9aX&=!myLyGO>f&+E^w;uw4b5~cN-_vu6zCv?xfFgE_})PR zCD_j6F9tJX5}mm|{}39P{w*{<5L^F}3=ixYa?Q2sE0_p=zc>-!>SyX4=u=uVo!TyM z#sr$_DX8jYwZDkTc|U#WAf%;#3?!TS856k#*s9OU$qK+U&-K3P zj>=WHX9OkZ|7!S*stQl=V5LGuH?e;*D(h}sO_efwZzPY(@;QIkaH$#fGm|vERAqT= zlI>Th-lLR_`wlEWEh)L1ukkFsm7&$luBprL>Gw1c62iJ3A~faqtzMQ02?~+lG1!~q zOn$ANIu_+-wg=Uz2R|=O+6FyHBeE2tZoiE-jD>=U;8`Ib`SB8+)@V6jH)$JAsEUcj z@2c>FraXrdv{QoaGVr>)M3swHG-^xF?LiUv7mr16yEd`y4djp#nZ*ybCa*+`>#8qA zUi0OT`3&p;>bAFZIL7@<+64S$zv7&d;T=ZywU#>R%PRd6^C14Lu0IQ23Mv<5^m^XC zkKDVx2pQ#t9?z#%#@k5U`qdygj6c`n67xe5aDQTsrv6FnwH3{Z2c@}|`!qXlZ#lm{ z+${+=pGIDaHV+s`X>uBC{5 zjq4Z=e&8}*(->t&t_)i?tx9z~ZTKkf1~6bLRSj>67#^=CzE`X#G#)u__i&2qVgty1 zM^B8C$J{x&B%yG)Jm2fxd963UB2$58Yk_>XH*??<*dr)-#JPOpq}vNA zBTK#BsBaqJQD!k$B%jsrdWggIvsDJ529T07WnRJ5aV{Ey5c(xF?E#SP zWy!Pr2FDO&QiR6VbAIyxTx^5hKn|`qt4a&w<2?E_c*NHnvWb1{DqjNE{@IW zKvv~nlO`upo*uy0{H{$oHm4}YMjQBxu>MX1QRRvg}@P(_nTGwzd}v#E6fav}SV>C&p*EuV9PM9n-~R4`BEP4g;tbv?ZGldYA0Ku<`7%cJRit@dTc&E$$MUI zZ?~Y`9db4@?s+r_vjp=V5sE&X3MM(Si)LH!ZcNj{qV$^TFag_9wFl{m-GrKci8+3= z=--e1T-e_4nh2OtqvVe_xQd3pPwh&h{MFtQ85Re7kyl12`hp1oCFZY^Pm=k2JLQ1X zt;WxK%1wx3+2{pw=Oj6EJ-rnZ)8%3ECx*(}=Z9roN@N7Jj+d`UJ|CLY3{Uis&eeWP zl|FU_qH#9GejYV42!KO9VQ0!UU)a0bW_VQvl_OXTUB&}bPb41A)djVvG^Fzm`iBf? zi1S%DIlZ`J{nGaPDUHTcs-9HIvd%q&ZfqadxCmT(68;8>Zg@*eZx(!Av}NRRW+`i)0vJkAe0LkA6zeRctIn3z{Clk(X0qRrg$FEV0h zNVhhP=Clyj67!)<)Xe4ad*%ffDdy<*MX&ReJA? z#e?{wFQ9Jk;9^eKT7>J#V1138kjUJ|o}lr&ZI?Le8UYO+p({Gv7)E9Nyu!Q5W`mUr#8f6n$#Zu;sgjvPeNfE|%_%J3H*5Aqe-nVi z*bRM5Sw~Ah^hPl|(gPoTeelfA8%n_*9f~!bNTn zUZ|0(-!qe4hZ{vZ6`xvFYqY;(8~>WJ6q-T~y=F-8hAfzHY;NZ4BiEvs?g zBU3W$RYcz-_Uo3;xB^?tWNzftE3di9DNV%tA2K)GY9CRnWFB_$)TSld4_X}iFJ-X3 zH~EsANacYL_+Ui78^>WuuomjgqNpX!KA(_lyZ(t;c<~Z4@Pxx_^%=T9FPh~VUJi== z)UXh2I`r%F*%4h`rt8I7s;n((yDP|`F~FJI%6GwrFhl~fQnb}kD6d|< zhQ|%EZWXbn@Rb!;ncqM}*d~Rp=iX+-oX)b+g$&GqvRB6v8V8!8=GJ4 ztjXGTYnp3^C_+N1yvYCSXTf;Q^&?s-pkkQS%}d$41Mg&8I`kTpXWt(|Qz(g#&UJQ> z*Rpi|9)r$*jzJGxc_HQZ*XhZ|Ces2hJ{gmj&dQns8LVP`s+!@a#XlP7?__=vE9jls zNR2Rdkn_3Y10=yS6eFVFg6~qvK$Hev2TaKAd#Q0M9Vm>~4_D&vUpXQ@KmrqI2w#U& z^`K}QnM1aeMjO`Pl1AKbHN^YM0GO-ju#%sG2!ukRXw|ZFFCpjBf^!=sr0TzIfH$Cy z{;hcH{mtQ}>biRLSVe0g#U_+Xho;17kqOGUi`YrEFg{i;GN>u5EcrQM4*3HG8rK@! zD7Q4<`Y5V1Xg{H7amt7-h({%foB?|7#aV=bLJ63S~IN4rA^BpdcP$Y1~_ibtZKe^7(;n96J2eya5-caFwYX6P28k? zeZbpfrADC?EY!Q=LMN0d=7*Ve7e8>f+}zJ+{O;Pk5~t!)RIJXc2x0r!o4C3;a-M>P z4jDEvABfF{(X*19a0STjJ1RsKUEs_^YRG^P?EOYG>{U0{##*3s_elm zA0gKrq_Gy>!0x;82i0B%+Aq({k86}@SBdPlCA|dOQ+YNoP#uH3`^8C`O@8la4c_F)edDZD+yhEL^?6gKuwz!uT4#>(MS1<7Nmb#H*W=k%E|~C*e2e6V3F< zGwBFe@@iJRsPD6YQ7hCpp!C|xD}5vU1N%GWN(;OC(nYlZf9Y=|y-!-5*R&j;FC5(5 zS})_ZlR#ZDec_AbQ9^Q|HjKF3}fU;K?7cyyEW`9u$A2Z&n$nmUwOG5~8 zZ!`7?L0FJnm{PDh*cy+p&}o>1DO>%TvWZ6$_AyCE%J#FKy%p_&l|m=wGd4pcaMgxTL5bl%$BRYuunyfChsYZ;0*@PEcaG=vm zz*>%@&-yunfD&E7Vv$~|V73+#=F)%oYbD~$@R>`tk0t;N>0=ATSR^IrwA0w`TDApGrZr<)?@n>oKlAO(Zb)yK4?+g* zb#vpNucyUI%y*5|a0(u@ZmG_7bNf+O4qvSioV-KdGLj=j5p|WxHs{UDw4-j{{QBxS z3Ui&UcTeTDXX7I@1W~WXeog?%VJF8Kr(6*6?#G4iDiGGL$$HU~?j^+`1p-g9Z?pB{ zIZ?+|KD+RGeLZ4A^R%6qna>dlKfg|4rH8h>U0nX8!p+aHjjsZmh>ymr7FP@S?6(M% zt*S#dR$8A=JeU=>%0(@lHCa{r?9_WKkO&!*oh`26IUms^W%<1q^^Od63xS>~E=Y*^ zEU*GVmfn@-Bndl@LyDeJvdqz5e}kNofDY9;=eiFwU9*pu~cwXwU52Q!*2 z1VRygRQQEbV~YpdQ^a>OA11_~8TG8T7_q@IPA0_9@ish8%;^K*haUOU-Saxhc$2+P z+iRS(voOcTudmtePT@e6F{}NuBaDmTbC!yz{Ut` zkBgn#uBqK>CDXi99+q+jj$^d%AS7<_46xQMi5)es zMi(9Rb|rE%->G2YqK<#r7XCfA@4)Xdw+Y$|#vIvo4Yga>p5xU@baa5+{C`Jw4p;jykvWT~guFV>_EU%WLi33}#Vq}~LU-~Ae~6j(w`+sWsdlrSdlqnKOd zE78-`&Q^TmB#tH*pI{O1%}u)_Kkq7CATLjHxp?Tc)4_?bjMNP1@-)Hr_9YwRDcJxc z7=!cPj8FWK6C0Ld0Mbu+GA533B_PHXpt=;DnF|}>z&9oMU90Ox_x#-AdbdnKJ- zX}W{c)NAv<>6!PD~3|SicY=aKdMH zxDawm9WBbL8+{+Z;Z7bR5KTS@7oUs{N^LuP`F(IKu6*%+!lbUyT%@7Zf+R%cfveX9 z>I*U!?}8q%9_>BV(pKk9FSM93KbDHQcGK0I^htW@kUm`Rf@?;g`D=|~_^K42=L*VV z^gz4(;QDK{yMZe1c&b{ZeYUZ1ar`&r(q`e!U02^32Tub^0rL2Khnxz#@@o-ugR(-- zI=#uSV+70RbDMo-`&0JPStRTig$mYhxi(toKEk%JRpaWv1O%X`er+y3C*isVRf`l! zzW2m+!g4g67q=ywgT=a4&d=GzKN<+K(SR1IpZaGzGHDSaFsJSiA3fI2o2DXAoEkR; z!rh5|B>|5j5jv-@Wy*8*4R~9U2wMwjLc2ZteWC+3=y1WxTbpiAIPaLk`YkF$pz0w7*f z74)ESACH-`5@fy3(jVBU zd<*#J(zO>@x-zj2P6A8UgHFQ>j}i-My0ztwt~0*{C)70(XR#wkwxY-5lL_n1lt(SD zV=9a9xpS@ux&AO`&bTp|XHz_>&$Z!PIcds;{FEKi7T0RY%Y#XfYYWc$RlwYTC_Wlk z-o}m+8C>-$Fpz5lTD9~U^yZwU9w&%>Sm_KvklY`?)4spq%p;%btu41Cj$Q#M;bpD3 zZ$guT#hN-RDHsIZj!FdpEbl-u#w-O&~WaeO9ixXM}2=H;C&?vtY*3ih#@!n}Qd6c)bUIiRK`m_PF6wxyJR7VUy$Yn~Hz>xDV~IXI#m5s`L>^Heqmsp7h`~o<{yatUyJxt|$LA}(w9nTz zo->4y`fQIcF&jwMZ;w4-p5l^QpP~?=vI{1n{U{Z~8iO324LZbTA$g$O1D#!~uUIlc z*P}*<^sLF#fN4xQSasH2E{;=h!MV7*Fw7;;cybRwbM)Ke!=AM+;P!S>)zRBBvq8B2 z@Wp1f3)=O>A_dDPgPd(1bGZ!TkJ|#S><0Dt+=v^-2t#6*Qq@Os80+iM*w35tDLq?)Y?J3R@{Xb>D3*7(M%pID{W;*wFBjITD5wtrs}z{9{|w| zQano))%eVM@G?MRI?(C?jN`9S(*MOj;?-R&&U6!VeDU}^>TdkbCy6P&ux9wI>v8u| z$496962hz0%*O=bw%NyCi@%vz?*WREzyPyi^3n3c+J7~KP}j54H$B!=)9Nqe^EXP6 zw(Ry(FY*HdzE%g_)rwWOhtYq!DeH=b9a^0G0P~ml%is=Jt21!s4ivW58oD0@bh7#x zjk#9M-3*%pEKOM6ES_J=Qe_Li$71(iOexRbreB%tPXEHxs_FxJeTx+P-Q=jRv63|6 z<-puwG6<7zO>l7=R|D{M;AVRRscH7_J z05G8-x9XtvKlEXA|E)&3f_fORMc`kPvHQJa`Q5;$WSg(#LQ+=$Tl4&xO#1VMQxIT4 zd|%-3rTsJg`~SS`aRY3R{~Z2TgZ=gCKTY?aZu3{*`|HzxhRlDW%|D~>pCS04FaHS_ z{|Og=1&O~t{r?UZy5b=wpXo2aDa9N{ro>(keI0|C_d3^fbJ>v&CMBIJ;Ke4dk`2Q@ zG76f#4h93NL;IsT>qKmutmCzw3GZ*SYB! z0GnwB>z2g}0C_6s(eBran$5n%wEBq`d>d#U-5D4KxZ{Mo@DOwmvFX?LL zI97UlyuvtjnXIgct*7#okTISpbxK5YKBm`6bup+y8UM2M3y>T?%{;Hjv zk9?;*iht>JGE2Muv_|B)Z7C)7*0~2Cq@GD$x%>69@P&`({{7oIvgb-k*ofS_LuLA^ zkmuEphqkNB&*4=92^i%^o~yY)HWfKjYa6 zU0u4hZ5+14H|pL;{=1w0=QrWN-}(CX^$sgB7cT?sQ^XwipY~)unq>~s<=fA9uOoMP zjC}q-eed6=c*%W1^WDz@4_Zg4;zJr{iMLhevSVbxQEAty>pw5y|L3nwC;s~ph(GMi zcu~bknmbRToS#=R9FnN}+%Ooub=jl&f4=NxHsGR{XpzrNJO^`RAM1&ZN!=W}u54=Z zKOX|zDdX=ut^7sf=(eOKm*gB3u*E;~L8A75zQeDVzq660(Z$Nzu#V-V+ucezf zqxJ@@iu~WNMDhDdaUL{|LV%gGhJb1PRY%r#eij#1_W!*t<$fP`@_;6QmRa2Kroc{= z)cNy)`I{I2xBjEhry5E*oF@NM5pBKv{e77$qDishE-#fT`w-)e|ET41 zY;{)`$cKNR-%-!c4cb2sN|$sCzCMQEgxoi(yL^BeHk7dHD6S9#;mz7^i8^gPQ9saA zPj7f!KNFn1q*XkCUMSvs)!S8wSYXxcj05j-F_&1AiCx&TAQ7=4r78#&sho>I*Qcw$ zd>vFlhZu*D7X_~|sDI*+Bb^f2y^s4$0^C+_HvJ;ex-u8!tTg*!^~PtYXN%Wj{x)Dj zC4mmJMXjFo^N%nph8}5)7+1!jR9%iIiZ4<^3hLgvP4|p@pR7)vkJuaEN8R@-{#et( z9&$YG_PFlpQusa)2`RpqYm+nZrdc*fI<0@yd?$6D;r*d3yIXOprMfcU_*q;jd;BE7 zaH#M2MP4|N3TtmOR;ne;@~Uu%(WS|_UNx5MaAorxDJcBH7(5{mK0 z&YMFF3Ixqtyr(%Uj#%XJQ#SjH?H5zGgTDQ3(R3%& z|K~j=>?~X<6>3)0c6uCh+UOIboh_&2*5Io6Yxzg=6&Dxe6HD&cVV{qUbN||w-&KD1 z>evFdTscM_y?Y9raG~|hVVgPtM{(q*5hO#dpAXT|26Y`e1_b>2!MMN1|Cn!Y+8#W- z@qkmeN(%YN!I#)qFp>pi^6SLMF2Gc{)|nBjfWuhPkCJzNkHh>`S&hfA8aT&SY>#qgp&DExSh*LK$L$B zG0z*h`IFl~B^bzMgHQ+y{(;$aBT8F$&!$`@as#FBHmMyp;Kj#qMu&RQQK;^!bXBlIu5e9@wPW08)0lW2jLe z$UnEX+!=OWQeI#qzde&{;SvCp^*9ToY1oTW^@{1{-&k@5X|4}uMz>0NPw_S8`Ix{T zKjOVF_6E%8`;3-F=7}jRL{l}Ml{?RM!;p!?p6@aiqio5iX;N#Oj}1Be>>FGY4FlCh z1fQ+Yy2ZTLWt6!>Ku>E)!_|^DL-w^dXarGQTG_W*oMye=i|W+ab@R&%rWRi~+8Y5d zN_=X#c;BGM;1K_>n(H^COu|zHGW0UWcD=JgD=pg;rG3%G4CRwj>%YA7o<%h6FJBQF zqFg$+y^;+cT8WRC737^UiK@9AyRPv~u?x^cR%VuTOFo=Vj+V)eln}PXrMP(GPbBxh z)9YNOUVV=nF>k^r8 zo7F7lId51cL~u@Af!0evr1xKBg?6`pYfh_7_hG8z_`KF))-aJjW5G7J+!rx}@CrV_?_cXGHrK9fM@)f+E zIa^_z#<)>^SM}{nT#Ktdfo<-4w0j!2H&oRBvpXS2Rt)shIa|afV=Te@*&prmBJlyf zJF{vwWVe~e_qy>~357}sml;2_%3nFcuPH3+hnb|78uy|>jm86A`# z&GNresuS}0IPY(^HU-8EDCzWFP$w4UQSJ(uQvXvsj@oJ6+x zHTH$_RQ@EIpf8hsxU8d?*CXzza5z~s%O?zisJ|_l`RwOaXhyfs;eLAUSd|H%>K>)- zP7mjm`c*vjfL`-9W?7r<_;Esuq-&56z?h!n@cVM%PM-OOYeHzf#lrf&o407=USA@oqlQ=5?rbzCB3DHoNxyb#@d$47^l3un>G^ zj>OfbT)gJ68WnNbqnlDja*B6_)01kEEC%*cM|FS z+r_dy;pVw8LJaT-r41eq+`Yh~U%a386HNA=o^xWFiji00 z;yk)!uz;8DODo(dWY1Q}QRMmzh#ft}N%`QFe6{ZB$#q~`QPW?gKRMC(hXXL zh$L{SJw`P9`IxGREDbpjR5Q(i-=AdM%H(hWOTrYI1cn;Z9zMGhu)A>av48O_W5$au zTHBv(erMuB+q=8BF9N426z2q1R$?|iZ$#Cj6}2%)_*~>wWpJNJPVlkA49p{TVrMiR zbon0r$~#(5<*0lpRmk;-2U(&Zk+5?f+^Er_BH(#H;ZX8$yPp}@B_D$K-kL0LzHfijAD-H zD6JZoua^lCJX@!_s;>_&Jq`umQF`WEajbRcQ{L%*2<9ZR$$uPTFkgD<7qH`)246qPI}HD4{7 zL@l;LbTQOEJm3CUEl#-G8Q;vQ^jT0VWAuDs`G08vj4g+F68X>eG#=g-q*q;*=WlYR zzw(re>BG@n7HwWd{6^^CcW&o^4ZBSfDG6(Q+r9Gd zDh}W2hYam_iX?~9`UcJVqK8!r2YKqh4heqhRIkcrHeIR^@m&o}dgyV7i8|rtt;VjH zV+%eLBiFf~%4ytFjZ*)Iy0?s~D@yt|1G&Hjf?IG87Tn$4g9Ufj;O_3h-QDHlt_kk$ z?jGFcrsqGs-tM=1)_k2Wz+DSC`|Pt%?W$irRa*vrSR2(l+TD_{SpH8+2 zPq|tW-e3DW`NwC&feCduSO*xQ5&;dtn{xLxG5fDHs@0Xuf)0%$wG2pOp@EVCJV6md z-VoX2YaW(N9~NmfFa5?(tUqYlJl~(^#tSQP8b*75LqonFNI0th(~#90&oT=5Zk#*$ zNoBd5$UJ8)wQchRpp}CU9krukId3V;8wq<3BRcELp>iL>Rc75d-|ESkP06x@?%my@ zf1~<0x}Xz%Sb0anqMcgobNf8Xy$CCVTw;0zJ^?a9p-Y=U_X^^u4AiSg07}^?QRY2e zM>qT~YBri{16*dhd({uLURWP4YtYxome>&DkamchCXYCgE^@Z5St+KuW!xNNJ~aG^ z68G%kBT6YD(jS4r|5?y`-F4Q4VOk_OMt&bQ9G6~Y;-#17(U z7%=Y#TGq3ef7dOpg}(e*9*W`lNwq{((^3yYc=4{BSc26B(!){|4r^AD3%0teh?dXH zQ7{0}=p;W*%7B}h1vYVwAGv^t-RI$oVvP(LyGVc;?rt}M#dd8xNF=C{m~S+3kOZhw zxK(=nVBbH#vmljF2#-a7WTi{MAwfO>(>h%iXsu>8(1Hdkc6yeZm5rUQd50re09kYD zk3Ms8Tjex*wtb|EqS^eYnL^a5+@+_9xf18~6T-{GJPzVcNWZZM^vq$>Yb4oU#)Lap zo$r4Qv=;t@;qRPKsU>XVq{hbjqgpPLF}?LZOv4{CP@*9uE!IqJq!AxzXoTVOjP&Ai zwq71_vP`vVlNXKd_vF+IOMIiwJR`!!W75x1xdxwJDJq@pFy$WvULX zb)k4%Vaz>Bd(`ms`4ry0*i<)jZ!cNthyikkLU!(ebHJYrQ@#RnOcCQ9Tyl-d5+E!& zoIou&9M5}R9cN5)0)+`eJ3D(lCju09H!}ZwtkoSzx$)7P7+D7C<{YO$g5SlQ#Dli% zIXFg^w;<;Dm$YP{+|2EMzIb{cFEXBLkFTTcJQ*3TZib4=ApUGEV-=R}bgZPZ|z0p3( z8k3{w=~|EDNVUwBamA4u^-f37aT)e^7?!R?8T~EcZrN=ioo@oK`)ME-IS10UXaY3( zHHVuo0iQ($-E_FujM(ihE5|+4g|!!P_{W<1@Pq#<3mX8vf9% zN@f@c5De#VeF!xiy4m>+4#g68XN~GLx!bHzd;e-W! zNQ5q@NY5Hkt#YOgmVv|@uM=qG8X9J`q;jdkyS^?zn&Bd^j|zc>AiqS8;dhBU9l+A2 z6Q)ras>2w6RW$ea3gnevTvC~~-9+}s7o`ACw&0sihqE>pRe&M(VmM1vc#{uz23g=d zYv_R9@A_c2cmIKLhmXLib8W8SFAo-^uBZK_AieWXsZI7l_Os<10&5tqIP^q~R5XGb zN_lzUzDJ#bYrFxS2|EdFO!vtRGRRrZ5xmrE8@&K{Jdi$5MV;fL)Y)r?WS={W%Nt}S zAcDTd-e}=qeX(UOET2-3Ygszw(#<|?XU-Ng+V0xr;dd>i1+iM+zi2YZ;eYL3cs?q^ za-em+Vx=UuN==HUe}Fc{Z|<4W;q+c;U8O5Qi(w`Cp!iIp1_RGq9Uqoi6$SVS^+vub3I$mq!snq1P^WG!T$I10r*TS1*D zAtvgk<5|q|+P&b|PcEA#y@;z9_(9iYIbm_RY zE1|2K4664Lz3HKSTk`V~TNG5Wmov-bweW+nAtxfL%&M4U#rrt*{x=5exd^savso_j zgD18~`;o#)vC0y>6#P4wk)4u$V>mirDRnX{AWE$OM*j~e#pRUc?DSgr1WjX)fgN(C zsfNA9Wy6%o{0wIfB8Td!MLQ|xJYR3=k5Gi@P1ah5T zP%-{e87SSc)(@G@Y`-*kaCU1zn5E*Cpd|M{xYnpCeLIW@2Gcq5|7VC0DKIMR%PL~Ho+2h0?}GBRIJL)wgk){KNI`oqX0EQAjCfY? zu@rI8`B9Inq{Wj}tDLI}3uo=PW$@1OrsWFy9agDGQe58^^JagTyg?|}I*n6FEEV5a zE|c?DVM&QqcG=?W7v&!@+u%v2i9O3=GQ=E9g-thi+w*jqU+0h#ym`8x%Y=kW2-P7%9V zEH8{+$7aBq2s0bI&ecBPE!7`e^1to!U)i|V$fxzE?ur@j-#wK>50&C>BR4ypG&=V~ z8o;lZc_pqi^|5M?bv|pzE?5Drh3bDw%?yylp66{ClsvpxMP<)O2{G^+mQuR&Km5e4de&N2lXm?59wOxU(B{VJppOy4lkU8&>loAzrY)1;==2n)&P%u&E=`@Wzy2KmbO9*#w6U+7CC2DHb8|+2RaXv11zZ_BcCtv&yrSPP~ zxe?pQTEi13g@aKLObF{jUrTyj?`N*sGX@tvo% z{K;aWqUcP&tXbUcQ~xW*?>pO9vk@c#Q_LvOc_j z&qS^gcL$bmJ3ge5LYhuwIkjW1(Tj*~)HHPHb$a5I)#{jqjlJpl$>|mvxZ#U^$l6Hk zc9Xs~s#@>-fdB~N7U#2^x1CKZn*4yzL^O&WNsT3LmVI~h7>&K>u;@4p!6a`zbKgQl z&nz)Uk?RNVx4&EmMcC{FOuCx;qmyKn{na$uO1VbhVX;h^C|`yd4Xs#4E!mULfaXpu zq*u~+iZmD%Z}vOXdQxI4zzi-rlVKpO|LpNv#qy662Q}DrOyFyRextD3_~LN|m3)X@ z&p&xklfCpbX7aSE)C~$-tPAonDpSHInmA^)$z|2kMF+pt`Z9sLKhL_TCmqv_P)9~o zgLlfO-97$bW{XOz9g>v8GO-nJF=}adzdnhTGB87i;(|q;*4-Ht5#9_H!JMHtizi2> zNZK*nCaZY;6CXc|4co5wstE5Ud5)HH5~BJ!5!R(}+KmR|BBz#XLy z>i8{Op(Y#UuhY$`zfbITZFG&dNWkMMq6^>9y=%_w6z|*e7{v20PC+9K8jXJKD|;R+T5Y+pbe6cI zO9&((!?D&V0EKi5;OBInIce$7!$u+`EI~0pqA)D_Dw!6u^{;8HE}g4`k!R5n$(3aC z(f&NjC|lRv#C_(om&8nb0Yp481dHM)^R{`y;xW@+TN2=+v{`#gw_M1_pdvjycUH|T zps_11L=?E7%W~d2)(cWmy-8JeIF55PiDa15^UvqZTSd(o4WE!I-opr9?%mf9(`fp7 zF59~c-{2b!9_y4Dd^i}neWnbu&xGVqMTB>PZ^=r-y~onH6dUpH@mX+{fV}`VsfaEe zWiA9r2n!n&m@vrdZFMHoxUq|ca&8rB-RQgX_ee0)%U=2z#%93H3zUj?^nA63yOJwE2mX|>FO#!^)AP+nxQZqD6~dS%Ik&S{oWwrF_;{Q=iJWp5|z4Wt+t4?@sgD*3 zR0}lVI`sVF=AUZxyIN5x6JcBi8MkF12?iI-y)t27-gtnw=%djV2jyIiGXa%}y?;$@ zizoqlyT0d{!!$3&Ao_iT>C9VzLP1T;<690;p?E^am=^KOph<1gtA%Uj6{4}5D1I(+ z971K%KDe`%M9CvUh5=67KEy)AEq+*fa6VxtO#ttjD}$Lvi_eDrAx1yGHHrzUrWW8_ z=T_Y??zoBbZyiyKW;S`#LYJuX%5BvH`raH{s3`c3|8Q9Hj0ByASI z=K92BiOm$K_C+|Ya70Zp8zNwA*E>kCUznMxi)fpebOTpL*V{>fUICEzPKmYW*>1`1 z=cr<@72E3a_M(*eUUim-cnp$Dw*l7( z1SL2l6{kV}u-OEcvEusz(NdUEv1qO@lMXs96MW!uWR5lnC{uXFxwAnEqx7 zAX6>W4y>o>ZTHq+J#6CHyz8TCdT><+Nl5J(N5I|O}KsD;~4 z#bxg-{>*@}=}nS9$a^ID4b@OiP_x@{aHMDz=G8J)=f+jU_Fk!}ad*U8ciU_r>5 zK2P7zpD+gAmtXL_;TR)_oViZuXuC+cEox0RuPKulrQ}e*B*M^M z<;{w`gusWp#}v3zk$0$;lz?W$?BSmU?u(~&8;T!&u;F7F}g&c78H-AXWt<8C?XHIiM8V^bcPq~ zDM0a@b_;Tq8L0HBMjYNA@*WeUy#~vYq;H9-nNCSt7-nkBC|1X72+{DZ=*9=^SJ+}24#|N z@R^16_QRZGBNv23KATk z**=+;Yi3xCo8VNB{(n^_M8Kd10!g#jRJ-x|hk$@i(i;6fv2a-G!NZSFy}iKxFMxz2 z=s!y{Npp*V(mBs?*4D68|wC-w9w%aLGjM z4`>=lyHzHaaV<+n5GVNjX3p#|w#=Cs2Ad&EWKjOp0!o9Enef<8dulMHYF&O)IWVgt|BJZ_tps1& z%nJYU!#x=3<4qtHNBm3q`AQ?&`~bIwAO~dzp@e>dymWt?mZl<65=q!wYw)PP?0lu> zy?Q{iKe@}2zn^*-+Hxk$uOMb@&Wmw5D+2dvPuu<#LjgNkEuWoB+$TO=so7+xTy6Vy z9r0&7O9DCn{(P-4BDrd^4x`*`CyAk0im|MI75je=P7zp>{76YKp0aPe4z~d=7pHXC zmzh6|<>9B52(QxKfE{Z5k^jHe&Ohlv;%=S&e3MbP5zpC8JWC5~RtuJ%FQz5%%b|^R z|LT7H>!*IYzYLQ04*s9@_2wS|!HC#mJ}+0|lPA|rkRSwWk^gTjUv%7h7Pm|E(>)<_ zTw31bC()HjvHw@-Z7~EKjn>+Z<`?`T!t=O06s?)TruJ{lk(CJ!9uBqR{%<$xixTSo zcfjtWhO$wMafrpDm%DEzP}TyPuIVrN1h7@v|6;ZO&-DD`o1b_@H(E^N)1t<+Pi@%_ z7{V>u`4yuyqDo6;9!I9Q>|*sZo7SS;DzmGr0q`*QMgRAnr;Br9!Ze1`g^B8CcTc(``fc^tmV!&_J843{|+k;9-K-POa2)SaRy-I z1f#smHTt9~l^Q|R%9Qfpu7g?v9?(eFV3s4^X|>&rImg&R|3AFj|9qxXgy50ozke2M zBk(N&Fot)bl7}n(BoQqU@Owe0*Wu}Ux}M;FpY;2vJN5SZ>}J`n3$*|8zZT;^16u>6 zza(TRWMHzy8)R!W48290rX- zB6T7aINs?Mv#6;o5X1f6oco{eUjP7j%YQ%gS8!Zz{()w_R>#`w6s-sF4+Af>9+Bjc zXbfP1Osc;Qy=r6{yCn|I(@3V#zogy%dDA2jAJD_5izJ0Jxt$6O26T$4;)_Gqau!H6 zzaE$Z+J$^!6|42OMIwEfA@RAKb5>jIr`R{V#VzNn^7}$!CVk#}CV2l{#QLW(UL;1Q zki#2}r(mC}P}KHT+F5Cpzvy(9Z6o{%@m)3pk#wk_5M11ht^HP{!s4$ai412yw8JBFiFg(%wuN zav0$SVb2$CTqt3Z)5pCZw|5}n9gT8M0SAsU%!Y<3D9UVlPV1F zqKm*~v-s-ebu5z;zHbj1DFBv8N*vmnj7o9YZ1W3g)L@!}7d5r|S{q6n!weFb-0br> zejGM!_!RevM^BZ&##4RaB{dZPl#8AhbAP^`|D|$ea2?_xiy8c{x>3MR+GgzE>7OO_ zP0nShZkIuNdyc`R)fRkT?>MPNNgVDH9MbhvEYbvdlD~pUT&5Q=+^BFPc8h=GbA9Cf zbk_#|asANRVU&E$2_5%qBTs>Q&c_*GT8NW19=d?uFzn^5N{ zHnYM{At;i5540<=;hyd_rg5AuLO4-erN}Pee}9TdsYlBe2G5ORTR@}7q)wkCjdH$h0A8-VpiLnE8 zJ6|#_wHdRhO7-H>_c@od)5p={P`1i7+Hbw))( zk2hkWO=2%M)vmzQqx}Hv< zjb80D3{I8X2SZ6v=g*sdQl7hSs?-{#r#Nq#*)d%nMzAl^!iNiV*NXr8_QTCTkgwH3SY-YV9BG$c@>}54NKR~eWAz{NdmjP+>>r^pyT5( zD;I^grfQXD^+xULoZp5Y7}8aUwpyaR9VtD`YAzm+a4DhhodUsUP&EcTnYbp~s1mG3te|=%b=k<=`TYe1lBxHUz z2);QO5|-zGO^9?qt>y6U=u6uhNMgXFPt`mjUAa&ymY*3`u_V#_1l~V|;K24<*n*>( zG1Auu=;&IW_#z-;<4Ngpv%T8jrTMh+jyeoChJ=o+YOUFIv>2wbSb8_0?!axzdZQ^eN%#2Y*z)=J%mNtqQhdGC?J01 zoYTj`I0u&CAQdx>y2hf?t~jyfYxTeLP1>^vlbWLH$g_=<0)URqrCAtekflL$4Gsr^jz=~&1Adf{}_ zTCEQ<9|wi~+3mYhc!b{_Bi$(Fd%phQbUsBfh$82gW=jfQ(iR^FD@+%n>5M47Pu^ee zt6s+b!YerQzIde1k(;k49e%ICz3=2o!D<5rPWrtrpD>G`5eP7M&Ur1f^S5~-T*qNL zSLioYa#xodET;AJtI?gM)j#{C_!K_4_kB1z zNC%TD2Xt5K8JogD`rC9>TJrYDws>%@N;9%8wvMd$1kVkq?Q88Zj_RUd-svJJ3HeT3 z;B6mFh$ne*gyfhX%7L4R6X_bA{Z_E#i`8=&2vNNq#;8ord%;^q13ybLeGIVH!+63r#v`M?P1i_Y8a{f68U2|mC6u1DIZ+eU`iF3V)P zNu!b`u)(WxX&Ps7en6HDhORpmNHiv2@}k1J-@k@(u)ctG$olvQ+$KnCReiLZSH@Yb zRs%52K56=H)Ryjup|EhhVvLSGM6nGi)Mrv+b)nH7(W7`AXr&8Fj;v-hplehe5`PTlO4o9U2 zVW?Vr+wQt<(d2GEeaz@Z*C67m=BFWP0YokCvA=aP#lYPl#SWh&eOE%cNQ zq3j|Ti&yVwiz{dGUsWHnk+A6F7p@o+Nko}y@j0^>%MuhV`nP*G-#U(dU!gUw>~eLk zr5#6{u}NL}HQLNza@k$htoGi6&y!TrSJe5NCkmEUSN}42vbMf9fcdU#e&M|_R$?uH~ykcNn<$Tlf41hC}vVZv4k4#Gz&IFtHO(Hy2WCw7GFmDH<#qH zzNTi}oI1n#jvlYK1PtOuphqNE$8iG^%;CPi@m}@saLE!uPEOLNY}8}J)>lDzxhphC zVT_hgBJA8B1suii@t_xCJS#3mNX8#)Jw$PQX}nm5-FJC$PO-mO_cDhQ8NlaoOi8NN zpqcg}-}oUQ5RGNzd~Kw^AJ*z*LEp4SgC_dB$?F$g2bllfrE0N$-P|H|e7?i)h5X@F zi&kVYa2=;S_{wUrNM!M*`ruV^eCH^#9^_DDY5z2M&#zE?-f`3xk-TebjFw9tmC5dk zsOMj*oz4!+4Q~(*FzAdr{SK9fV8Ju%|Dau4!z?~(Ui4syIy7pFAij^yNdi)_-RPv< z5VhN&*Ju>#wA6P&l~EDg!Mx`LzM4a>MZ_{>(wCnYfA1wBV0{-~CKoF1q z`{K>_d~%+nt{E))Xy!3Mx@CvU&ks zf_>u0F(Qjcj%5zN+rZpBLqAm%H9vLCyayB;M&hP=H=AQ5vym@Rxqig_`I4d=i9-gP zH|UG7A2+7jXpE0aBT4LR3l}x6{%4Qa3r2Olh$Yak(^)mELKVm}ulVn(SB$qn*^9V= zKD>jR7@{Hl3#Bmo@#zi+4{R0jMNyEAG@CW#j*ID2hTR!^id`y?VPi&nqMrrz$((OvZGCu(*2 ztb7Z_l7?(WE5A&dQQVRZ$W4emhBB5}m97rn-pGhJS~Y?sfP#Q(AzhCuo3}1$`VxSS&*O>yN@B{%Ytm4|?$sr( z79uA7$w!P+-XnM*Y1FD=f8RN`An;3XHf@P_hG;>OB6I##E%3ZE{>Go}0w0!n8p_q; zSN4D6iDYHYR%nREQ~i?9Dlih#gjxPMiRiC{XErt9?1xb6#oc&F3+`YI(b);iP(FKp zfj4i0eXKJbLnY=0)y;;mvk247SC~p$l}6U2sy8DF@u=cUp!OLWE2iibO@bD$rx075Fn3^uunOFFl5AnxG1P+wE2nqc$XSHi zd)AhLW`{yKwYchn>InVY(svc919z`v-wfe36m_%tNLc}uCV~otn8w2iUx^#6W(p-~ zVT-^OrkZiTCkdXfinDpy;Ek0jHN}>N22}fgZS#onzrCPoH@l{)R;YvP0n1Pmz=e$D zPEz?4a`XCaA3#<(bLFyj4=DWJp8EBVFy{OHWGH9Xr5rvCN^TvTy;(*r)8vJN`vEXW zWQ<7Gvc)a4#t?kaXT`XcaCDy%> zgrsr1nibNj#mdWiyT!XiBK8H_WedwboHHq^(++AT^v2ZMSk-)sy&Tj$q6lIl!&(}s zXw6M)T#;=$eTS7y=3Wt@a-A^#2!t$e!y2N%4RJGS;myOr<4_dMthFI=ZWnVQ^oa(3 zgJqK}ULQYr#=_w~0|=|$K2CJZsmu2B^)`8JXU-??iP8-Xj^*BMx>H|%I`5_!;Mgkf zGhc)NT${2*1`oY68pv!_VLkg@`hjN+z3rPE&umSDW*~DN7j#FeZ2+%o*0L{WtI6$f zE0U|+RaGJ2^D-xiSHc7k^MH4$od1h63zccvBFdgz4n%|t@2M-bVg;}xjSgQQ0u$V- zPFEHt;e2?@@*m2^2XJ`w$OjgL{3r!K5N1rS2kLPrWo`=V3|+IH6eeXa3&mjZ$;&Y- zucvsoyPvhXLdejxrGk@Tw~5H2VrFSQvAkqU5&A2SyF-2N0s=HWs`j>g3^~?Nrn2FB zw3@9V321qfUsN?R%zWO7FG_9tmr1SSd%2lT357QMKdhX!V=L(jSiUOM1Km`sR}Q6B zGgZwQUSH*RPZ{q{9-}X9bS2}T${@cEZUzt{;=v_TSLf~hI1MFM2H6o;%3A95_x7zA zAo}0smg<`o34UIAf4V;|lKkq#3vZ0Rj~%+%U2Zdzq+=dTLJtDvs6#(){URhaiyDF7 z=8rsBHILt2EaSjqoM(WCAgNT%-?th1?!&-*VviL(cxRQ%YSwTN(E_PSwiVWlQp`pmu=0r(V zv|%WF=EO{%sxJGcR13bilZg1dp0OXZ>H%-j_Bm6qQ^-T#+WkMX6$*5=+q+B@XM4$A{P9*Bg+dw~q1$rtaKa+<2?E|z z?EB7x>vHcqKa!kA|DfvL>R)-BCvJ<3KKOBNva$Rq1zuBbG#Du zcp|igc4MZO)Jm)`t7`etzjOOJPiny4zVm3h+!kPMO+(;0{x^B&E99F=O-1(b8~i~~ zv>SwNc3=0=8p14BAg3iL`BHR5)gu1{tPCXF1^X}mHv8u4sjs9k6eOLjGFeP&bwJhf zRdrt0FDa@{KIlPlXU(!O)lFPJBTrsGX5y>3F3=#yq`__sHuh`H(4Y{D5VDArFo00W z$uig)Np(@+?w%LcP2P9A&H2 zIu;5Rf|rhBM-?{ocnoo9Krl##><{ZoQ6H)0PL^v7{Ky^;&SWJNeZCfmaG2kPMX5_S zS|^BjL}IRWLuibXU7~DkqAC5}7|F(&w{3qgO6Sa=tWt~uQpj~GUq<})RFR_QPdj-T zghl|Yu{a5cM}KkdRBT8h6aL8`J;`(@YBymzglA)l<93xEd7uHie>-)7FmKcmt|o4E z!!~BzPrR|5GkkRiJn#wsB+5P3xZQF_bgGYZx-!$)Y#J1d4{(FD_E1zw*-x~KYRWY5 zn^MD`rEfn|gX+16NlB|Yw_-?d+}4#qRn+9FyTOyNi2uZT@cj=we8WjHz2oV2zhYmh zdt$h|_pj#Edx}Ai(M#fGx^hE7HmvqkTkEXz&bDcMLyU!{felA@!(ASVi@Tud{Dx=g0@;(GECe#yAZz`O~~38w6- z*?^h$+*`gy3Ga5kD@z=n37R_S}1ud(h>K6IQgbs=`Lk5pm%hEa^)O@ZpqES=}WG9HjZBX*k48;hW zp&y|*bdxtGRpoOLxz=#9wSf9gKZ=++41?g3BB{_t%7E?n1Dgi#_V#eKXW;p&JxH2l zR85p%xk1d$G_dH)`R#J=In$|jv1~k!3{DSvpD9ddTw16f(VU4KPBCmYgs}MJMc2FE zalS}R2OhknJb$7u-}jmnzU2nZIEG1)a+S*PNklK{XspnX!P}GHEQ-?%?1`IA&=>HN z8f*8D$xOy^{bAS2)pl_S-_u~4NwZqOsGvm6RK>ca zkr3f~Kt882E_TF+k0 za_H~$STpi_Mu=@XZ^OFHaiNQRDX^Z!@8a>(b{RyL+=BID5#xc}WgV z!vQ}*PLBG@Qj@Q^c;@*(eJ@h}fzQ@dupQ(wTO)X_k(oBnd^rxRh&Yl>tOcv?% zKq+ny)%AoUbDr~vN*8br#AWH}^-o{2euB@_she$C7^`^|JpF}*6}W&V;Y!J55}uxn zF`p-xf4xPs*o{-=;RZp|h6ZaqUi01&62%vq z4v|9f@_bD#)*H;pHx|(NDb=Z-ahEIoc#-Gws^{`nf-M@&5PqnKYy9v|)HqF-!YZpH z^^+$r4A!bgjnL86WLUM578XYZ6C@-EcqYMLwCu#J3s4Q{-%7#GrJ`g&zTyE{}BriVUNkWVKJ*{MvgvK1{lYv$1`c43hgsR_8`hCc!9CoS1*kl0{Y8da(4=vk9h!C2;6Ma+GLGGoETNT^c z-~+Eq_u!ZjY@IxR<#+HTPMia0plX8P{3?fHoW))`KqFYcIcTI69qR=lYhix~@(tT?`3gCJ)+X?`nj6d7LGo6TQHKFPvm#y2x*LwpeVuQJ6rMZ0RP92 zR?3b-th>gD7?&>YfP0bPdCv*8ME?JDg%4hBLg`sCQtF1EXEk1qy zze)9Od~gD~RXC)Y5$r?A1NG)`G!$Ha$`dzA2Yecd=Pn+R=4ElHSZNS&^!?zsnRbM1 zcyQivE&Pd)_Mv8wy4`K97~1{k;+nG>eI3kKSO&k`hrsO}AerhDi#f@nBXgKxTR2T3 z4Gn(VrUs86A)dDHCq+oq9PMNR6c&MaxHBGdFzt1@nMJ9Xvews+)Dc{E?aAAUi=@>T zl5Y+_bDGU9E2O)Ef;OFwBlz%{(fIMWVZU@@EBRY6THf$EF&xCt%Q2S?Nm=nxq|bye z2&Xf`T&58E#Q=?bqM5{^(q+g{hnsxZW!VudZvl+51t)dn4@W;a6q|n?`t_}=iHbj& zS@5N&Rg`TCJ@#2^k7suM4*H~G?#bCJmPUelmCNsN?zwV0M-yU;A5{50{*wxJj9JYx7J-T1=r6U*%<5DO+Utd zu%K3mnYMkyL_Mo_(nht>oAKTmtr1yR7LkCn40bAks+aYe(3qQEw*~AUrCFyEXW@%B zr+@hjR%f{_a)CSHoIo~y*|zM-8Up@n-s9DJTR@0J8D85?XwOIBJpdrr104gT5I>tP zmX6n$@Y#3z>P`2()gKX$$4cxOJ$T_9-1KLdXHo;7ucXVd6-qNRC0@E+M_FBrsaMkR^`oH9Pm2 zn3rS!{Ql=M82Q$@la&VU&-cd}=z89)-rUPbJwdss+=dRjP(VGyzCk8y6xT+bS4gEB ze7lGD-5!KBA>lrD3MtD$%>UC109mEv>TgY41~TxS$yuuB>K|s?HS7ak5{Fx?I7)dS zyS~!1IL1nyxgkb@6K>OC4P5tc?;})woPtRO-<}se*v2ha)Wu7XceBUv&dDuwFJox@ zW`5;Gjy?fvNDTslkF)>TFL3^)Zf>20*E3xtL7UV0ho57PU$U!2--QV#9@*)3a|SOnx!voyc5Mm1BEFW6Fn_$-4x;TQ8Xq?? zNetg3hpj&M5cQ-w6R;}YXfdx`wYQ$j_=s%+7i!8e_)P?%DtD~DSSGd~l_h=|?m0WF zT`}F5pPE%6VNOzDjoCqlzu4(H4F#446yrddiJ6nx`O!_X+E+3y+?7Vqan0uEZ>Uh z#2YRHep_v~+=;bk+K$yh@~XapdaPoK?I?Lj7Xn{bzy~wRj!%7W?rj6LUcdoO6(9Ou zRvOg!o7R@<>GPIo6Cv(hoBJlQlvxP>LBH6EfZYjkvj<2p?FoQ{@B|7(n5_2>G_Fqy z@))_YTH#_He?$qbyedjD3aleE)T4bQ_h;v{oB=;qz%>nHitabwv(r?g$ay;0F6FE+&=KADWfO?Diw8k7KCh z9}W6$m7#zBc*9jI7J67zCCptns8#){lL2NLzQCmB19nvF^_8|T*5(YE`Kq{Y52dC1 z8x?>oRuzy4y=&R$O=8kxS6Qzx(xwU>>qlhO!kT!7l`Mf{m1FZrk3gtp5dq6G&EmVb zRi=hMtGHH?R87!Tq}Y5g8BZP!t)?EQ$u@fO(_Ct0-tILzs|335wTMS?wAfXA8+ zJltixP}=f;R6(H=H+yL~Sr#7D_Y**Ip0xP(x!EW*4-MIPQ8iOzRVlnCG`uWV@zVjl z^EEj0!C|rPZJTO2`r=A(rDcG}I9@cLD+MzHQ0?&w)c&l=zb;vhITp|1``IgB8P*>& zkK1pDI5t^o9nCp1CR~(2{#)@6_++w_4V?K(Sz9P z4O80c^8=sSK%JvaCf+`S!=tvruhs$T!T(sOmTXoeFRA`PzZxqw%rwUt>ZcVs1eF@~ z$3WEJaEKKn(ysqLI8Fr9W12cE<{#!W1sK@1ms9p9- zDJA*p{F)cK^-JcnUiMAGUxSxUs$PWAT{MC9qe61(IszbloHS52SlfrUJuIsnQL8LeMEg=+;BS>BwfK$K_-V5{(}tX6KtAB#jV@dY#=EnalT{rOyCBL7h1qs_R=`;q-Z7 zyV;HIYP0CnmF(k$U z2zlRTUnVWcgf{tdT57sBi=eX^#_2)Q(l!Yld0+RPb?LmX(-sVA*Xf!0B+Rgndcg$L zP45ZmAKWb|lf57*qWBJ#_7+(BzzFh9rofwWJVQDfmI!^V3H59htrW;ak~ejv7MiMI zG&5&Ta$NlOMRkl^_=^lI!IbVBj@{^cAn^+7Cy2QZKR|qKdAN;fkY~7cZF4H~BT3Zq zqcYgzf=jg7jXL~d_oML~IPtS7c%lEsx_xr|=|wn*)tcR6Qpdu54~`ht_^#4ZCA^kZ z)9p)!O0d}_oD*W)u$*arl3}4lBy?2z>tx>YBAXa0&ywcP0^H0@`GT#>v_qzB zp>yV!SyzfZ!`4?)(KVJ~87aFFH=;V5<>1D-O%(j%0Uok9=Tt&d4x|O;B?imM(?#w( zHmI{nel-qY>5@|y`yTF<&Ti3~z>5QQW{Bm=k}lUt_p@!8TR$z))y zD&cxgk)-HLGD&&B880qICY)*YJ2rs;Tq>-?1fzN0}g^Gg0lf{mv<~@5~`QU)wh!O=rqs?o?;0PAjZT+z3GV9N{37N^v7-lQY zvsNAzlSw0S4!Odaj-mOt14dcnmd+Kz-2LMAs0-AIqP9euc3%*B(hx$+hbW%eKPx7V zl9M6$PF+&ui{3S+dV*K|C>-Ffj3edrq0D>e%KWZGHAy5preMVMZrtV;7R1R-MO9A4 zX?LIdLP0zWzF=#@$OPnQS;Xi3NH~zhn<5qAF;*Ga!Gl~UE^LQVAS$D8UX;9vE<}rw zo2c0OWkxlv{1(@mUQWM0u(4Yp&M>(_-qx2(U$fCnb#Bp45y`Jd{J1SAovMTrzJO=O zCDB4|!>}&P>7qDy$d#1tr(E8TqSMdYrmqh3sX_!+bQ_+gkN6H4yq$rpvt8zPWCVw@gL@^ns96E0zdjT@8soIrvtK6$DcyoSI~(cKGU zTnVJ3a;IpZGou0u`t0K?O&h2b+uScjTT=F+$aQV^%VNn13*I1cBry)k=5Vg=Zl67j z{kho*Nzp$*o65OGMK>~iPSgtpWHtJg;mZB7N%l%(C)X$Cvgfs0oM7=jWv;H! z4%M#e3jRb_`5Gy^tvTDYsVTpy@5Hc!J-E)(d>ogZ@m3{k#C?e1R~%Ui`mcIzB7Vt8 zmFZietLiVb_+wFO63`B{0rVYpfD z(FRp0?HUtNk8(zQq1H`NmnqnsXitPx_t2&cWNN69_jy`k3W%x^@q#W9VcrYNSMEI< zp1qS~ACi7HI@8?R48cyR_b%&X2L$-vTD?lN8_ZFX-*AI!M*n4d05wVp@Jn(i8?zu{%{n z0HkEfY#^RKtNYC{DvV+?3c0ZBI%tNP!PJ_IKatA|yHoya%=zBTH@8s!_hWF?ZWb!D zey(txY+k1h+VFMpR=V{el}1ZxYhGby3XkCvHKLZ!>9yab4sqKYM>!;8hP8FIWk&-s zt)NNREP=;@*;pou&5cl8ChQMvHW3YlndfZfYV8JFu#jGBpkx*Xbh(8WJVY! zK;ok?ks+X|C*bRcPt)0j#@7UeLkS)@c*)o63|%gW5vX;$zT+jGpD z-Hc_$%_DGZ6sd4}>;%X0&}%f1Dzgn;Cb}xS_>dy&dc3uiuZ76yuk>6NnoxGykIguDMWn|3e*DAl9pxauDF=ssb{`=R6vESK-Y2Y^G zky)Oa&cm)dbIR}RQs3`8Q)aw4>X^Hat{9S}xHi1qQKTALm)robozT8*q9l1B;u*Ca~-M)Y7J zpk9NmAmPUnytJa}^DoWY@F$3}RoBnlglYW}^ZA;t2zR4NDE&pl@*5{923)3UjV|oE z{KlPEwXwtAiXTDMFuGFJn&s`2(6(CNxyn5?U<&_oGUHIBR&IGc{fdC{HK8+;?Shne z>6*^-LlUVBDfe$_!;?EBgYGDwNPgySb)Ac5MXx)>C?}DIN|)gkxT@!|N7_U)Wy+%C z!*`v<&_T=wMM)$-{<=C^El~Zcm`$6+tkAa9_#vV}^hMkU3*##k<^jrYj41pE!7vnVfRN{E7PV&Ww?vKU-g! zxRkMr>*TWjT5WdzrQ$++FR(3@CO}wcnKLXzhP6#_e4>>j@t#_h!b5(yaL&$pxp=

    E&*Poz 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⺺RG>ZKCKiu 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 z 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

    x8ZAdsUeVemfHyCnlDMDN`3k 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 literal 0 HcmV?d00001 diff --git a/packages/uipath/docs/core/environment_variables.md b/packages/uipath/docs/core/environment_variables.md index 6c88d3532..bcf1bcc8b 100644 --- a/packages/uipath/docs/core/environment_variables.md +++ b/packages/uipath/docs/core/environment_variables.md @@ -17,12 +17,12 @@ UIPATH_FOLDER_PATH=/default/path export UIPATH_FOLDER_PATH=/system/path ``` /// warning -When deploying your agent to production, ensure that all required environment variables (such as API keys and custom configurations) are properly configured in your process settings. This step is crucial for the successful operation of your published package. +When deploying your project to production, ensure that all required environment variables (such as API keys and custom configurations) are properly configured in your process settings. This step is crucial for the successful operation of your published package. /// ## Design -Create a `.env` file in your project's root directory to manage environment variables locally. When using the `uipath auth` or `uipath new my-agent` commands, this file is automatically created. +Create a `.env` file in your project's root directory to manage environment variables locally. When using the `uipath auth` or `uipath new` commands, this file is automatically created. The `uipath auth` command automatically populates this file with essential variables: diff --git a/packages/uipath/docs/core/functions.md b/packages/uipath/docs/core/functions.md new file mode 100644 index 000000000..e367270e8 --- /dev/null +++ b/packages/uipath/docs/core/functions.md @@ -0,0 +1,426 @@ +# Python Coded Functions + +A coded function is Python code with typed input and output that runs as an Orchestrator job. You write plain Python — no agent framework, no LLM required — package it with the CLI, and invoke it from Maestro processes, Coded Apps, or any UiPath job trigger. + +Use coded functions for deterministic compute steps: document extraction, ERP writes, data validation, external API calls. Use a [coded agent](./agents.md) when your logic needs an LLM decision loop or a multi-step reasoning chain. + +!!! warning "Preview Feature" + This feature is in preview and is subject to changes. + +--- + +## Quickstart + +//// tab | uv + + + +```shell +> mkdir my-function && cd my-function +> uv init . --python 3.11 +> uv add uipath + +> uipath auth +⠋ Authenticating with UiPath ... +✓ Authentication successful. + +> uipath new my-function +✓ Created 'main.py' file. +✓ Created 'pyproject.toml' file. +✓ Created 'uipath.json' file. + +> uipath init +⠋ Initializing UiPath project ... +✓ Created 'entry-points.json' file. +✓ Created 'bindings.json' file. + +> uipath run main '{"message": "hello"}' +{"message": "hello"} +``` + +//// + +//// tab | pip + + + +```shell +> mkdir my-function && cd my-function +> python -m venv .venv +> source .venv/bin/activate +> pip install uipath + +> uipath auth +⠋ Authenticating with UiPath ... +✓ Authentication successful. + +> uipath new my-function +✓ Created 'main.py' file. +✓ Created 'pyproject.toml' file. +✓ Created 'uipath.json' file. + +> uipath init +⠋ Initializing UiPath project ... +✓ Created 'entry-points.json' file. +✓ Created 'bindings.json' file. + +> uipath run main '{"message": "hello"}' +{"message": "hello"} +``` + +//// + +--- + +## Project Structure + +``` +my-function/ +├── main.py # function logic +├── pyproject.toml # project metadata and dependencies +├── uipath.json # entry point declarations +├── entry-points.json # generated — I/O JSON Schema +└── bindings.json # generated — resource binding overrides +``` + +### `uipath.json` + +Declares which Python functions are callable entry points: + +```json +{ + "functions": { + "main": "main.py:main" + } +} +``` + +The key (`"main"`) is the entry point name used in CLI commands. The value (`"main.py:main"`) is `:`. + +### `pyproject.toml` + +```toml +[project] +name = "my-function" +version = "0.1.0" +description = "..." +authors = [{ name = "Your Name", email = "you@example.com" }] +requires-python = ">=3.11" +dependencies = ["uipath>=2.0"] + +[tool.uipath] +type = "function" +``` + +`[tool.uipath] type = "function"` is required — it identifies the project as a function to the runtime and packaging tools. + +### Generated files + +| File | Purpose | +|------|---------| +| `entry-points.json` | Input/output JSON Schema derived from your dataclasses — used by Maestro for variable binding | +| `bindings.json` | Resource binding overrides (assets, connections, buckets) for local development | + +/// warning +`uipath init` executes `main.py` to derive the I/O schema. Re-run it after every change to your `Input` or `Output` dataclasses. +/// + +--- + +## Input & Output + +Define `Input` and `Output` as Python dataclasses. The runtime validates against these at invocation time and exports them as JSON Schema for Maestro variable binding. + +```python +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class Input: + document_id: str = "" + amount: float = 0.0 + + +@dataclass +class Output: + result_id: str = "" + status: str = "" + error_type: str = "" + error_message: str = "" + + +def main(input: Input) -> Output: + ... +``` + +### Supported types + +| Python type | Notes | +|-------------|-------| +| `str`, `int`, `float`, `bool` | Primitives | +| `list[str]`, `list[dict]` | Arrays | +| `dict[str, Any]` | Freeform object | +| Nested `@dataclass` | Becomes a nested JSON object | +| `X \| None`, `Optional[X]` | Nullable field | + +### Error output pattern + +Return business errors as typed output fields rather than raising exceptions. This lets Maestro inspect the error reason and route the process accordingly: + +```python +@dataclass +class Output: + bill_id: str = "" + error_type: str = "" # e.g. "VENDOR_NOT_FOUND", "VALIDATION_ERROR" + error_message: str = "" # human-readable detail + + +def main(input: Input) -> Output: + try: + bill_id = create_vendor_bill(input) + return Output(bill_id=bill_id) + except VendorNotFoundError as exc: + return Output(error_type="VENDOR_NOT_FOUND", error_message=str(exc)) + except Exception as exc: + return Output(error_type="FAILED", error_message=str(exc)) +``` + +Reserve `raise` for unrecoverable infrastructure failures (network timeout, authentication error) that should mark the Orchestrator job as faulted. + +--- + +## Platform Services + +`UiPath()` gives your function access to Orchestrator resources at runtime. Credentials are injected automatically when running as a job — no configuration needed. + +```python +from uipath.platform import UiPath + +sdk = UiPath() +``` + +### Assets + +Read credential and configuration values stored in Orchestrator: + +```python +# String asset +asset = sdk.assets.retrieve("API_BASE_URL", folder_path="Shared") +base_url = str(asset.string_value or "") + +# Credential asset +creds = sdk.assets.retrieve("ERP_CREDENTIALS", folder_path="Shared") +username = str(creds.credential_username or "") +password = str(creds.credential_password or "") +``` + +See [Assets](./assets.md) for the full API reference. + +### Buckets + +Download and upload files: + +```python +# Download +sdk.buckets.download( + name="Invoices", + blob_file_path="incoming/acme-001.pdf", + destination_path="/tmp/acme-001.pdf", + folder_path="Shared", +) + +# Upload +sdk.buckets.upload( + name="Processed", + blob_file_path="results/acme-001-result.json", + content_file_path="/tmp/result.json", + folder_path="Shared", +) +``` + +See [Buckets](./buckets.md) for the full API reference. + +### Connections + +Access Integration Service connections for ERP and SaaS systems: + +```python +from uipath.platform.connections.connections import ActivityMetadata, ActivityParameterLocationInfo + +conn = sdk.connections.retrieve("your-connection-id") + +result = sdk.connections.invoke_activity( + activity_metadata=ActivityMetadata( + object_path="/your-endpoint", + method_name="POST", + content_type="application/json", + parameter_location_info=ActivityParameterLocationInfo(body_fields=["query"]), + ), + connection_id="your-connection-id", + activity_input={"query": "SELECT id FROM records LIMIT 10"}, +) +``` + +See [Connections](./connections.md) for the full API reference. + +--- + +## Tracing + +Use `@traced` to make individual steps visible as spans in the Orchestrator job trace view and Maestro dashboards. + +```python +from uipath.tracing import traced + + +@traced(name="fetch_document", run_type="uipath") +def fetch_document(document_id: str) -> bytes: + ... + + +@traced(name="extract_fields", run_type="uipath") +def extract_fields(content: bytes) -> dict: + ... + + +@traced(name="post_to_erp", run_type="uipath") +def post_to_erp(data: dict) -> str: + ... + + +def main(input: Input) -> Output: # entry point — NOT traced + content = fetch_document(input.document_id) + data = extract_fields(content) + result_id = post_to_erp(data) + return Output(result_id=result_id) +``` + +/// warning +Do not apply `@traced` to the entry point function. The Orchestrator runtime wraps the entire job in its own span — adding a second trace on the entry point creates a duplicate outer span. +/// + +Use `hide_input=True` or `hide_output=True` to redact sensitive data from trace storage: + +```python +@traced(name="get_api_token", run_type="uipath", hide_input=True, hide_output=True) +def get_api_token(client_id: str, client_secret: str) -> str: + ... +``` + + + Maestro execution trail showing @traced sub-step spans (assets_retrieve, ixp_digitize, netsuite_get_vendor, etc.) with durations and parent-child nesting under a Service Task + + +See [Tracing](./traced.md) for the full decorator reference. + +--- + +## Multiple Entry Points + +One project can expose several callable functions, each with its own `Input`/`Output`. Define them in `uipath.json`: + +```json +{ + "functions": { + "extract": "main.py:extract_data", + "validate": "main.py:validate_data", + "post_erp": "main.py:post_to_erp" + } +} +``` + +Run `uipath init` after adding new entry points. Each can be invoked independently: + + + +```shell +> uipath run extract '{"document_id": "invoice-001.pdf"}' +> uipath run validate '{"vendor_name": "Acme", "total": 1234.56}' +> uipath run post_erp '{"bill_id": "12345"}' +``` + +Each entry point publishes as a separate invocable function in Orchestrator. + +--- + +## Idempotency + +Functions may be retried by Maestro after a transient failure. Always check for an existing result before writing to an external system: + +```python +@traced(name="find_existing", run_type="uipath") +def find_existing(invoice_number: str) -> str | None: + # query external system by stable business key + ... + + +def main(input: Input) -> Output: + existing_id = find_existing(input.invoice_number) + if existing_id: + return Output(result_id=existing_id, status="Already Processed") + + result_id = create_record(input) + return Output(result_id=result_id, status="Created") +``` + +Use a stable, business-meaningful identifier (invoice number, order ID) as the idempotency key — avoid auto-generated IDs that don't exist before the first write. + +--- + +## Pack & Publish + + + +```shell +> uipath pack +⠋ Packaging project ... +Name : my-function +Version : 0.1.0 +Description: ... +Authors : Your Name +✓ Project successfully packaged. + +> uipath publish +⠋ Fetching available package feeds... +👇 Select package feed: + 0: Orchestrator Tenant Processes Feed + 1: Orchestrator Personal Workspace Feed +Select feed number: 0 +✓ Package published successfully! +``` + +After publishing, the function registers as an **Orchestrator Process**. It can then be: + +- Invoked as a **Maestro Service Task** — Maestro binds typed input/output to process variables automatically from the exported JSON Schema +- Triggered via the **Orchestrator API** (`POST /Jobs/StartJobs`) +- Run from the CLI: `uipath invoke main '{"..."}'` +- Started from a **Studio workflow** using the **Run Job** activity + + + Coded functions published as Function (python) type in the Orchestrator Processes list + + + Coded function wired as a Maestro Service Task with typed input/output variable binding + + +See [CLI Reference](../cli/index.md) for full `pack`, `publish`, and `invoke` options. + +--- + +## Studio Web Integration + +Connect your function to a Studio Web solution for cloud debugging or solution packaging: + + + +```shell +> uipath push +Pushing UiPath project to Studio Web... +Uploading 'main.py' +Uploading 'uipath.json' +Updating 'pyproject.toml' +✓ Project pushed successfully +``` + +See [Studio Web Integration](./studio_web.md) for setup and sync details. diff --git a/packages/uipath/docs/core/getting_started.md b/packages/uipath/docs/core/getting_started.md index 6c00b0089..50da82107 100644 --- a/packages/uipath/docs/core/getting_started.md +++ b/packages/uipath/docs/core/getting_started.md @@ -114,6 +114,10 @@ Upon successful authentication, your project will contain a `.env` file with you ### Writing Your Code +/// tip +This walkthrough creates a **coded function** — plain Python with typed input and output, no LLM required. For a complete reference including platform services, tracing, idempotency, and Maestro integration, see [Python Coded Functions](./functions.md). +/// + Open `main.py` in your code editor. You can start with this example code: ```python from dataclasses import dataclass diff --git a/packages/uipath/docs/core/studio_web.md b/packages/uipath/docs/core/studio_web.md index 762735e48..09286dd73 100644 --- a/packages/uipath/docs/core/studio_web.md +++ b/packages/uipath/docs/core/studio_web.md @@ -1,12 +1,15 @@ # Studio Web Integration -[Studio Web](https://docs.uipath.com/studio-web/automation-cloud/latest/user-guide/overview) is a cloud IDE for building projects such as RPAs, low code agents, and API workflows. It also supports importing coded agents built locally. Bringing your coded agent into Studio Web gives you: +[Studio Web](https://docs.uipath.com/studio-web/automation-cloud/latest/user-guide/overview) is a cloud IDE for building projects such as RPAs, low code agents, and API workflows. It also supports importing coded agents and coded functions built locally. Bringing your project into Studio Web gives you: - Cloud debugging with dynamic breakpoints -- Running and defining evaluations directly in the cloud +- Running and defining evaluations directly in the cloud (coded agents only) - A unified build experience alongside multiple project types - Self contained solution deployment units +!!! warning "Preview Feature" + Coded function support is in preview and is subject to changes. + Coded agent in Studio Web -There are two ways to connect a coded agent to Studio Web: using a [Cloud Workspace](#cloud-workspace) or a [Local Workspace](#local-workspace). +There are two ways to connect your project to Studio Web: using a [Cloud Workspace](#cloud-workspace) or a [Local Workspace](#local-workspace). --- @@ -28,10 +31,14 @@ There are two ways to connect a coded agent to Studio Web: using a [Cloud Worksp In a Cloud Workspace, your project lives in Studio Web and you sync code between your local IDE and the cloud. -### Importing a Coded Agent +### Importing a Coded Agent or Coded Function 1. Open your solution in Studio Web -2. Create a new Agent and select **Coded** +2. Create the project: + + //// tab | Agent + + Create a new Agent and select **Coded**: -3. Choose a sample project to start from, or push an existing local agent - -### Pushing an Existing Agent + //// -If you already have a coded agent locally, you can sync it to Studio Web: + //// tab | Function -1. Copy the `UIPATH_PROJECT_ID` from Studio Web into your `.env` file + Use the **Initial setup screen** to get started: - + + //// + +3. Choose a sample project to start from, or push an existing local project + +### Pushing an Existing Project + +If you already have a project locally, you can sync it to Studio Web: + +1. Copy the `UIPATH_PROJECT_ID` from Studio Web into your `.env` file + + + + + + 2. Push your project: + ```shell > uipath push Pushing UiPath project to Studio Web... @@ -79,7 +107,7 @@ If you already have a coded agent locally, you can sync it to Studio Web: 🔵 Resource import summary: 3 total resources - 1 created, 1 updated, 1 unchanged, 0 not found ``` - Notice the **Resource import summary** at the end. The push command also imports resources defined in `bindings.json` into the Studio Web solution, just like importing resources for a low code agent. This ensures that all required resources are packaged with the solution, so the coded agent works anywhere the solution is deployed. + Notice the **Resource import summary** at the end. The push command also imports resources defined in `bindings.json` into the Studio Web solution, just like importing resources for a low code agent. This ensures that all required resources are packaged with the solution, so the project works anywhere the solution is deployed. See [`uipath push`](../cli/index.md) in the CLI Reference. @@ -88,6 +116,7 @@ If you already have a coded agent locally, you can sync it to Studio Web: To pull the latest version from Studio Web to your local environment: + ```shell > uipath pull Pulling UiPath project from Studio Web... @@ -116,21 +145,24 @@ See [`uipath pull`](../cli/index.md) in the CLI Reference. In a Local Workspace, your project lives on your machine and is linked to a Studio Web solution. See the [Local Workspace documentation](https://docs.uipath.com/studio-web/automation-cloud/latest/user-guide/solutions-in-the-local-workspace) for setup details. -You can either start from a predefined template in Studio Web or set up a new agent from scratch. +You can either start from a predefined template in Studio Web or set up a new project from scratch. ### Starting from a Template -When creating a new Coded agent in Studio Web with a Local Workspace, you can pick one of the predefined templates. This creates the project files directly on your machine. Templates come with sample code and predefined evaluations you can run immediately. +When creating a new coded agent or coded function in Studio Web with a Local Workspace, you can pick one of the predefined templates. This creates the project files directly on your machine. Templates come with sample code and predefined evaluations you can run immediately. -### Setting Up a New Agent +### Setting Up a New Project -You can also create a coded agent from scratch in your local IDE and have it appear in Studio Web. +You can also create a project from scratch in your local IDE and have it appear in Studio Web. + +#### Coded Agent First, install the SDK package for the framework you want to use: //// tab | uv + ```shell # Pick the package that matches your framework: # uipath-langchain - LangChain / LangGraph @@ -149,6 +181,7 @@ Installed 42 packages in 0.8s //// tab | pip + ```shell # Pick the package that matches your framework: # uipath-langchain - LangChain / LangGraph @@ -166,6 +199,7 @@ Successfully installed uipath-langchain Then authenticate, scaffold the agent, and initialize the project: + ```shell > uipath auth ⠋ Authenticating with UiPath ... @@ -187,57 +221,133 @@ Selected tenant: Tenant1 That's it, your agent should now be visible in Studio Web. +#### Coded Function + +A coded function doesn't require an additional framework package. Authenticate, scaffold the project, and initialize it: + + + +```shell +> uipath auth +⠋ Authenticating with UiPath ... +🔗 If a browser window did not open, please open the following URL in your browser: [LINK] +👇 Select tenant: + 0: Tenant1 + 1: Tenant2 +Select tenant number: 0 +Selected tenant: Tenant1 +✓ Authentication successful. + +> uipath new my-function +✓ Created 'main.py' file. +✓ Created 'pyproject.toml' file. +✓ Created 'uipath.json' file. + +> uipath init +⠋ Initializing UiPath project ... +✓ Created 'entry-points.json' file. +``` + +That's it, your coded function should now be visible in Studio Web. + --- ## Publishing -Once your coded agent is in Studio Web, publishing works the same as any other project. Click **Publish** in Studio Web and it will be packaged and deployed through the standard workflow. +Once your project is in Studio Web, publishing works the same as any other project. Click **Publish** in Studio Web and it will be packaged and deployed through the standard workflow. --- ## Running and Debugging -Your agent can be run both in the cloud (via Studio Web) and locally using the CLI. +Your project can be run both in the cloud (via Studio Web) and locally using the CLI. + +The CLI commands below take the entrypoint name as the first argument. For a coded agent, this is the graph name declared in your framework's config (for example, `agent` in `langgraph.json`). For a coded function, this is the key declared in the `functions` map of `uipath.json` (for example, `main`). ### Running Locally +//// tab | Agent + + ```shell > uipath run agent '{"message": "hello"}' ``` +//// + +//// tab | Function + + + +```shell +> uipath run main '{"message": "hello"}' +``` + +//// + See [`uipath run`](../cli/index.md) in the CLI Reference. ### Debugging Locally Use `uipath debug` for an enhanced local debugging experience. Unlike `uipath run`, the debug command: -- Auto polls for trigger responses when the agent suspends (e.g., LangGraph interrupts) +- Auto polls for trigger responses when the project suspends (e.g., LangGraph interrupts) - Fetches binding overwrites from Studio Web (configurable in **Debug > Debug Configuration > Solution resources**) +//// tab | Agent + + ```shell > uipath debug agent '{"message": "hello"}' ``` +//// + +//// tab | Function + + + +```shell +> uipath debug main '{"message": "hello"}' +``` + +//// + See [`uipath debug`](../cli/index.md) in the CLI Reference. ### Evaluating Locally -Run evaluations against your agent using the CLI: +Run evaluations against your project using the CLI: + +//// tab | Agent + ```shell > uipath eval agent .\evaluations\eval-sets\faithfulness-multi-model.json ``` +//// + +//// tab | Function + + + +```shell +> uipath eval main .\evaluations\eval-sets\default.json +``` + +//// + See [`uipath eval`](../cli/index.md) in the CLI Reference and the [Evaluations documentation](../eval/index.md). --- ## Syncing Evaluations -Evaluations can be defined either in Studio Web or locally. They sync automatically when you use `uipath pull` and `uipath push`. +Evaluations can be defined either in Studio Web or locally, and sync automatically when you use `uipath pull` and `uipath push`. Defining and running evaluations in Studio Web is supported for coded agents only; coded functions can still be evaluated locally with `uipath eval`. /// note Custom evaluators must be created locally. See [Custom Evaluators](../eval/custom_evaluators.md) for details. diff --git a/packages/uipath/docs/core/traced.md b/packages/uipath/docs/core/traced.md index da8dbc5dc..195e5751a 100644 --- a/packages/uipath/docs/core/traced.md +++ b/packages/uipath/docs/core/traced.md @@ -71,9 +71,47 @@ def sensitive_operation(secret): - Regular functions (sync/async) - Generator functions (sync/async) -## Example with plain python agents +## Example with coded functions -When used with plain python agents please call `wait_for_tracers()` at the end of the script to ensure all traces are sent, if this is not called the agent could end without sending all the traces. +Apply `@traced` to individual steps inside your function. Do **not** trace the entry point — the Orchestrator runtime wraps the job execution in its own span, so decorating the entry point creates a duplicate outer span. + +```python hl_lines="4 9 14" +from uipath.tracing import traced + + +@traced(name="fetch_document", run_type="uipath") +def fetch_document(document_id: str) -> bytes: + ... + + +@traced(name="extract_fields", run_type="uipath") +def extract_fields(content: bytes) -> dict: + ... + + +@traced(name="post_to_erp", run_type="uipath") +def post_to_erp(data: dict) -> str: + ... + + +def main(input: Input) -> Output: # entry point — NOT traced + content = fetch_document(input.document_id) + data = extract_fields(content) + result_id = post_to_erp(data) + return Output(result_id=result_id) +``` + +Use `hide_input=True` or `hide_output=True` on steps that handle credentials or PII: + +```python hl_lines="1" +@traced(name="get_api_token", run_type="uipath", hide_input=True, hide_output=True) +def get_api_token(client_id: str, client_secret: str) -> str: + ... +``` + +## Example with plain python scripts + +When used outside the Orchestrator runtime (e.g. a standalone script), call `wait_for_tracers()` at the end to ensure all traces are flushed before the process exits. ```python hl_lines="3 8" diff --git a/packages/uipath/docs/index.md b/packages/uipath/docs/index.md index 231add0ca..7ec047397 100644 --- a/packages/uipath/docs/index.md +++ b/packages/uipath/docs/index.md @@ -11,29 +11,32 @@ title: Getting Started [See Details](./core/release_notes.md) +

    What do you want to build?

    +
    -- __UiPath SDK__ +- __Python Coded Functions__ --- - Code with full UiPath context to build custom automations and agents from the ground up. + Deterministic Python automation with typed input/output. No LLM required. Runs as an Orchestrator job, invokable from Maestro, Studio, or the CLI. - [Start Building](./core/getting_started.md) + **Requires:** `uipath` -
    + [Build a Function](./core/functions.md) -
    -- __UiPath MCP SDK__ +- __Python Coded Agents__ --- - Build and host Coded MCP Servers within UiPath. + AI-driven automation with LLM reasoning loops. Uses the `uipath` SDK for platform services plus a framework extension of your choice. - [Start Building](./mcp/quick_start.md) + **Requires:** `uipath` + one of the extensions below + + [Build an Agent](./core/agents.md)
    -

    Extensions

    +

    Agent Framework Extensions

    - __UiPath Langchain SDK__ @@ -60,3 +63,15 @@ title: Getting Started [Get Started](./openai-agents/quick_start.md)
    + +

    Other SDKs

    +
    +- __UiPath MCP SDK__ + + --- + + Build and host Coded MCP Servers within UiPath. + + [Start Building](./mcp/quick_start.md) + +
    diff --git a/packages/uipath/mkdocs.yml b/packages/uipath/mkdocs.yml index de3b9a278..382e0fdd5 100644 --- a/packages/uipath/mkdocs.yml +++ b/packages/uipath/mkdocs.yml @@ -56,11 +56,13 @@ nav: - Home: index.md - UiPath SDK: - Getting Started: core/getting_started.md - - Release Notes: core/release_notes.md - - Environment Variables: core/environment_variables.md + - Python Coded Functions: core/functions.md + - Python Coded Agents: core/agents.md - CLI Reference: cli/index.md - Tracing: core/traced.md - Studio Web Integration: core/studio_web.md + - Environment Variables: core/environment_variables.md + - Release Notes: core/release_notes.md - Services: - Assets: core/assets.md - Attachments: core/attachments.md From 0a9b2dd7545c9571d7751541d512eb1bdb4c4560 Mon Sep 17 00:00:00 2001 From: Maxwell Du <60411452+maxduu@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:44:20 -0500 Subject: [PATCH 086/121] feat: expose injecting conversation-id in low-code convo agent prompt (#1589) --- packages/uipath/pyproject.toml | 2 +- .../agent/react/conversational_prompts.py | 32 +++++++++- .../react/test_conversational_prompts.py | 62 +++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index fae01ad9c..c1a3ebddf 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.76" +version = "2.10.77" 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/agent/react/conversational_prompts.py b/packages/uipath/src/uipath/agent/react/conversational_prompts.py index 7732c7f69..f79de6185 100644 --- a/packages/uipath/src/uipath/agent/react/conversational_prompts.py +++ b/packages/uipath/src/uipath/agent/react/conversational_prompts.py @@ -62,6 +62,8 @@ class PromptUserSettings(BaseModel): - Never attempt calls with incomplete data - On errors: modify parameters or change approach (never retry identical calls) +{{CONVERSATIONAL_AGENT_SERVICE_PREFIX_conversationIdPrompt}} + ===================================================================== TOOL RESULTS ===================================================================== @@ -136,18 +138,26 @@ class PromptUserSettings(BaseModel): {user_settings_json} ```""" +_CONVERSATION_ID_TEMPLATE = """ +The current conversation ID is {conversation_id}. This may be useful to include in tool-calls when tool parameters specify passing in the conversation ID. Other than tool-call inputs, this ID should not be mentioned to the user. +""" + def get_chat_system_prompt( model: str, system_message: str, agent_name: str | None, user_settings: PromptUserSettings | None = None, + conversation_id: str | None = None, ) -> str: """Generate a system prompt for a conversational agent. Args: - agent_definition: Conversational agent definition + model: Model identifier. + system_message: The agent system prompt content. + agent_name: The agent display name; defaults to "Unnamed Agent" when None. user_settings: Optional user data that is injected into the system prompt. + conversation_id: Optional conversation identifier that is injected into the system prompt. Returns: The complete system prompt string @@ -177,6 +187,10 @@ def get_chat_system_prompt( "{{CONVERSATIONAL_AGENT_SERVICE_PREFIX_userSettingsPrompt}}", get_user_settings_template(user_settings), ) + prompt = prompt.replace( + "{{CONVERSATIONAL_AGENT_SERVICE_PREFIX_conversationIdPrompt}}", + get_conversation_id_template(conversation_id), + ) return prompt @@ -190,7 +204,7 @@ def get_user_settings_template( user_settings: User profile information Returns: - The user context template with JSON or empty string + The filled-in user settings template if user_settings is provided, otherwise an empty string """ if user_settings is None: return "" @@ -205,3 +219,17 @@ def get_user_settings_template( user_settings_json = json.dumps(settings_dict, ensure_ascii=False) return _USER_CONTEXT_TEMPLATE.format(user_settings_json=user_settings_json) + + +def get_conversation_id_template(conversation_id: str | None) -> str: + """Get the conversation ID prompt section. + + Args: + conversation_id: The ID of the current conversation, if any + + Returns: + The filled-in conversation ID template if conversation_id is provided, otherwise an empty string + """ + if not conversation_id: + return "" + return _CONVERSATION_ID_TEMPLATE.format(conversation_id=conversation_id) diff --git a/packages/uipath/tests/agent/react/test_conversational_prompts.py b/packages/uipath/tests/agent/react/test_conversational_prompts.py index a58a94807..434f68465 100644 --- a/packages/uipath/tests/agent/react/test_conversational_prompts.py +++ b/packages/uipath/tests/agent/react/test_conversational_prompts.py @@ -149,6 +149,68 @@ def test_generate_system_prompt_unnamed_agent_uses_default(self): assert "You are Unnamed Agent." in prompt +class TestConversationIdInPrompt: + """Tests for conversation_id in generated prompts.""" + + def test_prompt_includes_conversation_id_when_provided(self): + prompt = get_chat_system_prompt( + model="claude-3-sonnet", + system_message=SYSTEM_MESSAGE, + agent_name="Test Agent", + user_settings=None, + conversation_id="conv-abc-123", + ) + + assert "The current conversation ID is conv-abc-123" in prompt + assert ( + "This may be useful to include in tool-calls when tool parameters specify passing in the conversation ID." + in prompt + ) + assert ( + "{{CONVERSATIONAL_AGENT_SERVICE_PREFIX_conversationIdPrompt}}" not in prompt + ) + + def test_prompt_omits_section_when_none(self): + prompt = get_chat_system_prompt( + model="claude-3-sonnet", + system_message=SYSTEM_MESSAGE, + agent_name="Test Agent", + user_settings=None, + conversation_id=None, + ) + + assert "conversation ID" not in prompt + assert ( + "{{CONVERSATIONAL_AGENT_SERVICE_PREFIX_conversationIdPrompt}}" not in prompt + ) + + def test_prompt_omits_section_when_empty_string(self): + prompt = get_chat_system_prompt( + model="claude-3-sonnet", + system_message=SYSTEM_MESSAGE, + agent_name="Test Agent", + user_settings=None, + conversation_id="", + ) + + assert "conversation ID" not in prompt + + def test_prompt_defaults_to_no_conversation_id(self): + """conversation_id defaults to None — call sites that don't pass it + must not get a dangling placeholder.""" + prompt = get_chat_system_prompt( + model="claude-3-sonnet", + system_message=SYSTEM_MESSAGE, + agent_name="Test Agent", + user_settings=None, + ) + + assert ( + "{{CONVERSATIONAL_AGENT_SERVICE_PREFIX_conversationIdPrompt}}" not in prompt + ) + assert "conversation ID" not in prompt + + class TestCitationFormat: """Tests for citation format in generated prompts.""" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 96764f7fb..cf11b2a5b 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.76" +version = "2.10.77" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 6e1cfe55626fe8318ab8a9a5ecd5b5d532e7a1ea Mon Sep 17 00:00:00 2001 From: AAgnihotry <95259907+AAgnihotry@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:37:47 -0700 Subject: [PATCH 087/121] feat(eval): support list of keys in target_output_key (#1703) Co-authored-by: Claude Sonnet 4.6 --- packages/uipath/pyproject.toml | 2 +- .../list_target_output_key_test/bindings.json | 4 + .../entry-points.json | 41 ++ .../evaluations/eval-sets/default.json | 74 +++ .../evaluators/list-keys-exact-match.json | 10 + .../evaluators/list-keys-json-similarity.json | 10 + .../list_target_output_key_test/main.py | 74 +++ .../pyproject.toml | 12 + .../list_target_output_key_test/uipath.json | 17 + .../evaluations/eval-sets/list-keys.json | 51 ++ .../evaluators/list-keys-exact-match.json | 10 + .../eval/evaluators/output_evaluator.py | 147 ++++-- .../pyproject.toml | 12 + .../list-target-output-key-evals/run.sh | 13 + .../src/assert.py | 90 ++++ .../list-target-output-key-evals/uipath.json | 5 + .../evaluators/test_documentation_examples.py | 46 ++ .../evaluators/test_evaluator_methods.py | 442 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 19 files changed, 1013 insertions(+), 49 deletions(-) create mode 100644 packages/uipath/samples/list_target_output_key_test/bindings.json create mode 100644 packages/uipath/samples/list_target_output_key_test/entry-points.json create mode 100644 packages/uipath/samples/list_target_output_key_test/evaluations/eval-sets/default.json create mode 100644 packages/uipath/samples/list_target_output_key_test/evaluations/evaluators/list-keys-exact-match.json create mode 100644 packages/uipath/samples/list_target_output_key_test/evaluations/evaluators/list-keys-json-similarity.json create mode 100644 packages/uipath/samples/list_target_output_key_test/main.py create mode 100644 packages/uipath/samples/list_target_output_key_test/pyproject.toml create mode 100644 packages/uipath/samples/list_target_output_key_test/uipath.json create mode 100644 packages/uipath/samples/multi-output-agent/evaluations/eval-sets/list-keys.json create mode 100644 packages/uipath/samples/multi-output-agent/evaluations/evaluators/list-keys-exact-match.json create mode 100644 packages/uipath/testcases/list-target-output-key-evals/pyproject.toml create mode 100755 packages/uipath/testcases/list-target-output-key-evals/run.sh create mode 100644 packages/uipath/testcases/list-target-output-key-evals/src/assert.py create mode 100644 packages/uipath/testcases/list-target-output-key-evals/uipath.json diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index c1a3ebddf..c4da7e297 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.77" +version = "2.10.78" 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/samples/list_target_output_key_test/bindings.json b/packages/uipath/samples/list_target_output_key_test/bindings.json new file mode 100644 index 000000000..6122d0e77 --- /dev/null +++ b/packages/uipath/samples/list_target_output_key_test/bindings.json @@ -0,0 +1,4 @@ +{ + "version": "2.0", + "resources": [] +} diff --git a/packages/uipath/samples/list_target_output_key_test/entry-points.json b/packages/uipath/samples/list_target_output_key_test/entry-points.json new file mode 100644 index 000000000..55ce49fba --- /dev/null +++ b/packages/uipath/samples/list_target_output_key_test/entry-points.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://cloud.uipath.com/draft/2024-12/entry-point", + "$id": "entry-points.json", + "entryPoints": [ + { + "filePath": "main", + "uniqueId": "main", + "type": "function", + "input": { + "type": "object", + "properties": { + "product_id": { + "type": "string" + } + }, + "description": "Input schema.", + "required": [ + "product_id" + ] + }, + "output": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "price": { "type": "number" }, + "category": { "type": "string" }, + "in_stock": { "type": "boolean" }, + "rating": { "type": "number" } + }, + "description": "Output schema.", + "required": [ + "name", + "price", + "category", + "in_stock", + "rating" + ] + } + } + ] +} diff --git a/packages/uipath/samples/list_target_output_key_test/evaluations/eval-sets/default.json b/packages/uipath/samples/list_target_output_key_test/evaluations/eval-sets/default.json new file mode 100644 index 000000000..87c49fdc0 --- /dev/null +++ b/packages/uipath/samples/list_target_output_key_test/evaluations/eval-sets/default.json @@ -0,0 +1,74 @@ +{ + "version": "1.0", + "id": "list-target-output-key-tests", + "name": "List Target Output Key Tests", + "evaluatorRefs": [ + "ListKeysExactMatch", + "ListKeysJsonSimilarity" + ], + "evaluations": [ + { + "id": "headphones-all-match", + "name": "Headphones - all keys match", + "inputs": { + "product_id": "p001" + }, + "evaluationCriterias": { + "ListKeysExactMatch": { + "expectedOutput": { + "name": "Wireless Headphones", + "price": 79.99 + } + }, + "ListKeysJsonSimilarity": { + "expectedOutput": { + "category": "Electronics", + "in_stock": true + } + } + } + }, + { + "id": "shoes-all-match", + "name": "Running Shoes - all keys match", + "inputs": { + "product_id": "p002" + }, + "evaluationCriterias": { + "ListKeysExactMatch": { + "expectedOutput": { + "name": "Running Shoes", + "price": 120.0 + } + }, + "ListKeysJsonSimilarity": { + "expectedOutput": { + "category": "Sports", + "in_stock": false + } + } + } + }, + { + "id": "headphones-wrong-price", + "name": "Headphones - wrong price (should fail)", + "inputs": { + "product_id": "p001" + }, + "evaluationCriterias": { + "ListKeysExactMatch": { + "expectedOutput": { + "name": "Wireless Headphones", + "price": 999.0 + } + }, + "ListKeysJsonSimilarity": { + "expectedOutput": { + "category": "Electronics", + "in_stock": true + } + } + } + } + ] +} diff --git a/packages/uipath/samples/list_target_output_key_test/evaluations/evaluators/list-keys-exact-match.json b/packages/uipath/samples/list_target_output_key_test/evaluations/evaluators/list-keys-exact-match.json new file mode 100644 index 000000000..65e77a8b1 --- /dev/null +++ b/packages/uipath/samples/list_target_output_key_test/evaluations/evaluators/list-keys-exact-match.json @@ -0,0 +1,10 @@ +{ + "version": "1.0", + "id": "ListKeysExactMatch", + "description": "Asserts 'name' and 'price' together using a list of target output keys", + "evaluatorTypeId": "uipath-exact-match", + "evaluatorConfig": { + "name": "ListKeysExactMatch", + "targetOutputKey": ["name", "price"] + } +} diff --git a/packages/uipath/samples/list_target_output_key_test/evaluations/evaluators/list-keys-json-similarity.json b/packages/uipath/samples/list_target_output_key_test/evaluations/evaluators/list-keys-json-similarity.json new file mode 100644 index 000000000..f36f1fdea --- /dev/null +++ b/packages/uipath/samples/list_target_output_key_test/evaluations/evaluators/list-keys-json-similarity.json @@ -0,0 +1,10 @@ +{ + "version": "1.0", + "id": "ListKeysJsonSimilarity", + "description": "Checks 'category' and 'in_stock' together using a list of target output keys", + "evaluatorTypeId": "uipath-json-similarity", + "evaluatorConfig": { + "name": "ListKeysJsonSimilarity", + "targetOutputKey": ["category", "in_stock"] + } +} diff --git a/packages/uipath/samples/list_target_output_key_test/main.py b/packages/uipath/samples/list_target_output_key_test/main.py new file mode 100644 index 000000000..ef7e56c73 --- /dev/null +++ b/packages/uipath/samples/list_target_output_key_test/main.py @@ -0,0 +1,74 @@ +"""Agent demonstrating list targetOutputKey evaluation. + +This agent simulates a product lookup: given a product ID it returns +a structured response with several fields. The evaluators use a list +of keys so that multiple fields can be asserted in a single evaluator +configuration, without comparing the entire output dict. +""" + +from pydantic import BaseModel + +CATALOG: dict[str, dict[str, object]] = { + "p001": { + "name": "Wireless Headphones", + "price": 79.99, + "category": "Electronics", + "in_stock": True, + "rating": 4.5, + }, + "p002": { + "name": "Running Shoes", + "price": 120.0, + "category": "Sports", + "in_stock": False, + "rating": 4.8, + }, + "p003": { + "name": "Coffee Maker", + "price": 49.99, + "category": "Kitchen", + "in_stock": True, + "rating": 4.2, + }, +} + + +class Input(BaseModel): + """Input schema.""" + + product_id: str + + +class Output(BaseModel): + """Output schema.""" + + name: str + price: float + category: str + in_stock: bool + rating: float + + +def main(input_data: Input) -> Output: + """Look up a product by ID and return its details. + + Args: + input_data: Input containing the product ID. + + Returns: + Output with product details. + + Raises: + ValueError: If the product ID is not found. + """ + product = CATALOG.get(input_data.product_id) + if product is None: + raise ValueError(f"Product '{input_data.product_id}' not found") + + return Output( + name=str(product["name"]), + price=float(product["price"]), # type: ignore[arg-type] + category=str(product["category"]), + in_stock=bool(product["in_stock"]), + rating=float(product["rating"]), # type: ignore[arg-type] + ) diff --git a/packages/uipath/samples/list_target_output_key_test/pyproject.toml b/packages/uipath/samples/list_target_output_key_test/pyproject.toml new file mode 100644 index 000000000..ef529b400 --- /dev/null +++ b/packages/uipath/samples/list_target_output_key_test/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "list-target-output-key-test" +version = "0.1.0" +description = "Sample agent demonstrating list targetOutputKey evaluation" +authors = [{ name = "John Doe", email = "john.doe@myemail.com" }] +requires-python = ">=3.11" +dependencies = [ + "uipath" +] + +[tool.uv.sources] +uipath = { path = "../..", editable = true } diff --git a/packages/uipath/samples/list_target_output_key_test/uipath.json b/packages/uipath/samples/list_target_output_key_test/uipath.json new file mode 100644 index 000000000..e2a331e84 --- /dev/null +++ b/packages/uipath/samples/list_target_output_key_test/uipath.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://cloud.uipath.com/draft/2024-12/uipath", + "runtimeOptions": { + "isConversational": false + }, + "packOptions": { + "fileExtensionsIncluded": [], + "filesIncluded": [], + "filesExcluded": [], + "directoriesExcluded": [], + "includeUvLock": true + }, + "functions": { + "main": "main.py:main" + }, + "agents": {} +} diff --git a/packages/uipath/samples/multi-output-agent/evaluations/eval-sets/list-keys.json b/packages/uipath/samples/multi-output-agent/evaluations/eval-sets/list-keys.json new file mode 100644 index 000000000..d1f6e177b --- /dev/null +++ b/packages/uipath/samples/multi-output-agent/evaluations/eval-sets/list-keys.json @@ -0,0 +1,51 @@ +{ + "version": "1.0", + "id": "list-keys-eval-set", + "name": "List Target Output Key Tests", + "evaluatorRefs": [ + "list-keys-exact-match" + ], + "evaluations": [ + { + "id": "list-keys-basic", + "name": "Check multiple keys - order completed", + "inputs": { + "customer_name": "John Doe", + "items": [ + {"name": "Widget", "quantity": 2, "price": 9.99}, + {"name": "Gadget", "quantity": 1, "price": 24.99} + ] + }, + "evaluationCriterias": { + "list-keys-exact-match": { + "expectedOutput": { + "summary": { + "status": "completed", + "total": 44.97 + } + } + } + } + }, + { + "id": "list-keys-mismatch", + "name": "Check multiple keys - wrong total (should fail)", + "inputs": { + "customer_name": "Jane Smith", + "items": [ + {"name": "Book", "quantity": 1, "price": 15.0} + ] + }, + "evaluationCriterias": { + "list-keys-exact-match": { + "expectedOutput": { + "summary": { + "status": "completed", + "total": 999.0 + } + } + } + } + } + ] +} diff --git a/packages/uipath/samples/multi-output-agent/evaluations/evaluators/list-keys-exact-match.json b/packages/uipath/samples/multi-output-agent/evaluations/evaluators/list-keys-exact-match.json new file mode 100644 index 000000000..d2b685bb3 --- /dev/null +++ b/packages/uipath/samples/multi-output-agent/evaluations/evaluators/list-keys-exact-match.json @@ -0,0 +1,10 @@ +{ + "version": "1.0", + "id": "list-keys-exact-match", + "description": "Exact match on multiple output keys at once (summary.status and summary.total)", + "evaluatorTypeId": "uipath-exact-match", + "evaluatorConfig": { + "name": "ListKeysExactMatch", + "targetOutputKey": ["summary.status", "summary.total"] + } +} diff --git a/packages/uipath/src/uipath/eval/evaluators/output_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/output_evaluator.py index 27dd05d9f..5eb54e9e3 100644 --- a/packages/uipath/src/uipath/eval/evaluators/output_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/output_evaluator.py @@ -86,8 +86,9 @@ class OutputEvaluatorConfig(BaseEvaluatorConfig[T]): specific output evaluation criteria types while maintaining type safety. """ - target_output_key: str = Field( - default="*", description="Key to extract output from agent execution" + target_output_key: str | list[str] = Field( + default="*", + description="Key or list of keys to extract output from agent execution", ) line_by_line_evaluator: bool = Field( default=False, @@ -133,12 +134,35 @@ def _get_actual_output(self, agent_execution: AgentExecution) -> Any: If the output is a job attachment URI, downloads the attachment and returns its content as a string. """ - if self.evaluator_config.target_output_key != "*": - try: - result = resolve_output_path( - agent_execution.agent_output, - self.evaluator_config.target_output_key, + key = self.evaluator_config.target_output_key + + if isinstance(key, list): + if not isinstance(agent_execution.agent_output, dict): + raise UiPathEvaluationError( + code="INVALID_ACTUAL_OUTPUT", + title="When target output keys are specified, actual output must be a dictionary", + detail=f"Got {type(agent_execution.agent_output).__name__}", + category=UiPathEvaluationErrorCategory.USER, ) + try: + list_result: dict[str, Any] = { + k: resolve_output_path(agent_execution.agent_output, k) for k in key + } + except (KeyError, IndexError, TypeError) as e: + raise UiPathEvaluationError( + code="TARGET_OUTPUT_KEY_NOT_FOUND", + title="One or more target output keys not found in actual output", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.USER, + ) from e + for k, v in list_result.items(): + if is_job_attachment_uri(v): + attachment_id = extract_attachment_id(v) + list_result[k] = download_attachment_as_string(attachment_id) + return self._normalize_numbers(list_result) + elif key != "*": + try: + result = resolve_output_path(agent_execution.agent_output, key) except (KeyError, IndexError, TypeError) as e: raise UiPathEvaluationError( code="TARGET_OUTPUT_KEY_NOT_FOUND", @@ -149,7 +173,6 @@ def _get_actual_output(self, agent_execution: AgentExecution) -> Any: else: result = agent_execution.agent_output - # Check if result is a job attachment URI and download if so if is_job_attachment_uri(result): attachment_id = extract_attachment_id(result) result = download_attachment_as_string(attachment_id) @@ -165,32 +188,67 @@ def _get_full_expected_output(self, evaluation_criteria: T) -> Any: category=UiPathEvaluationErrorCategory.SYSTEM, ) - def _get_expected_output(self, evaluation_criteria: T) -> Any: - """Load the expected output from the evaluation criteria.""" - expected_output = self._get_full_expected_output(evaluation_criteria) - if self.evaluator_config.target_output_key != "*": - if isinstance(expected_output, str): - try: - expected_output = json.loads(expected_output) - except json.JSONDecodeError as e: - raise UiPathEvaluationError( - code="INVALID_EXPECTED_OUTPUT", - title="When target output key is not '*', expected output must be a dictionary or a valid JSON string", - detail=f"Error: {e}", - category=UiPathEvaluationErrorCategory.USER, - ) from e + def _resolve_list_key_expected( + self, expected_output: Any, keys: list[str] + ) -> dict[str, Any]: + """Parse and resolve expected output for a list of keys.""" + if isinstance(expected_output, str): try: - expected_output = resolve_output_path( - expected_output, - self.evaluator_config.target_output_key, - ) - except (KeyError, IndexError, TypeError) as e: + expected_output = json.loads(expected_output) + except json.JSONDecodeError as e: raise UiPathEvaluationError( - code="TARGET_OUTPUT_KEY_NOT_FOUND", - title="Target output key not found in expected output", + code="INVALID_EXPECTED_OUTPUT", + title="When target output keys are specified, expected output must be a dictionary or a valid JSON string", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.USER, + ) from e + if not isinstance(expected_output, dict): + raise UiPathEvaluationError( + code="INVALID_EXPECTED_OUTPUT", + title="When target output keys are specified, expected output must be a dictionary", + detail=f"Got {type(expected_output).__name__}", + category=UiPathEvaluationErrorCategory.USER, + ) + try: + return {k: resolve_output_path(expected_output, k) for k in keys} + except (KeyError, IndexError, TypeError) as e: + raise UiPathEvaluationError( + code="TARGET_OUTPUT_KEY_NOT_FOUND", + title="One or more target output keys not found in expected output", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.USER, + ) from e + + def _resolve_scalar_key_expected(self, expected_output: Any, key: str) -> Any: + """Parse and resolve expected output for a single key.""" + if isinstance(expected_output, str): + try: + expected_output = json.loads(expected_output) + except json.JSONDecodeError as e: + raise UiPathEvaluationError( + code="INVALID_EXPECTED_OUTPUT", + title="When target output key is not '*', expected output must be a dictionary or a valid JSON string", detail=f"Error: {e}", category=UiPathEvaluationErrorCategory.USER, ) from e + try: + return resolve_output_path(expected_output, key) + except (KeyError, IndexError, TypeError) as e: + raise UiPathEvaluationError( + code="TARGET_OUTPUT_KEY_NOT_FOUND", + title="Target output key not found in expected output", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.USER, + ) from e + + def _get_expected_output(self, evaluation_criteria: T) -> Any: + """Load the expected output from the evaluation criteria.""" + expected_output = self._get_full_expected_output(evaluation_criteria) + key = self.evaluator_config.target_output_key + if isinstance(key, list): + expected_output = self._resolve_list_key_expected(expected_output, key) + elif key != "*": + expected_output = self._resolve_scalar_key_expected(expected_output, key) return self._normalize_numbers(expected_output) async def validate_and_evaluate_criteria( @@ -225,7 +283,9 @@ async def validate_and_evaluate_criteria( validated_criteria = self.validate_evaluation_criteria(evaluation_criteria) # Check if line-by-line evaluation is enabled - if not self.evaluator_config.line_by_line_evaluator: + if not self.evaluator_config.line_by_line_evaluator or isinstance( + self.evaluator_config.target_output_key, list + ): # Standard evaluation return await self.evaluate(agent_execution, validated_criteria) @@ -248,51 +308,44 @@ async def _evaluate_line_by_line( """ from .line_by_line_utils import build_line_by_line_result, evaluate_lines - # Get the full actual and expected outputs before splitting + key_str = ( + self.evaluator_config.target_output_key + if isinstance(self.evaluator_config.target_output_key, str) + else "*" + ) + actual_output = self._get_actual_output(agent_execution) expected_output = self._get_expected_output(evaluation_criteria) - # Split into lines using utility function actual_lines = split_into_lines( - actual_output, - self.evaluator_config.line_delimiter, - self.evaluator_config.target_output_key, + actual_output, self.evaluator_config.line_delimiter, key_str ) expected_lines = split_into_lines( - expected_output, - self.evaluator_config.line_delimiter, - self.evaluator_config.target_output_key, + expected_output, self.evaluator_config.line_delimiter, key_str ) - # Store original agent execution data original_agent_output = agent_execution.agent_output - # Create function to build line criteria def create_line_criteria(expected_line: str) -> Any: from .line_by_line_utils import wrap_line_in_structure - line_expected_output = wrap_line_in_structure( - expected_line, self.evaluator_config.target_output_key - ) + line_expected_output = wrap_line_in_structure(expected_line, key_str) line_criteria_dict = evaluation_criteria.model_dump() if "expected_output" in line_criteria_dict: line_criteria_dict["expected_output"] = line_expected_output return type(evaluation_criteria).model_validate(line_criteria_dict) - # Evaluate all lines using utility function line_details, line_results = await evaluate_lines( actual_lines=actual_lines, expected_lines=expected_lines, - target_output_key=self.evaluator_config.target_output_key, + target_output_key=key_str, agent_execution=agent_execution, evaluate_fn=self.evaluate, create_line_criteria_fn=create_line_criteria, ) - # Restore original agent output agent_execution.agent_output = original_agent_output - # Build and return the aggregated result using utility function return build_line_by_line_result( line_details=line_details, line_results=line_results, diff --git a/packages/uipath/testcases/list-target-output-key-evals/pyproject.toml b/packages/uipath/testcases/list-target-output-key-evals/pyproject.toml new file mode 100644 index 000000000..b3a04339d --- /dev/null +++ b/packages/uipath/testcases/list-target-output-key-evals/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "list-target-output-key-evals" +version = "0.0.1" +description = "Tests for evaluating multiple output keys at once using a list targetOutputKey" +authors = [{ name = "John Doe", email = "john.doe@myemail.com" }] +dependencies = [ + "uipath", +] +requires-python = ">=3.11" + +[tool.uv.sources] +uipath = { path = "../../", editable = true } diff --git a/packages/uipath/testcases/list-target-output-key-evals/run.sh b/packages/uipath/testcases/list-target-output-key-evals/run.sh new file mode 100755 index 000000000..508bd9777 --- /dev/null +++ b/packages/uipath/testcases/list-target-output-key-evals/run.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +echo "Syncing dependencies..." +uv sync + +echo "Authenticating with UiPath..." +uv run uipath auth --client-id="$CLIENT_ID" --client-secret="$CLIENT_SECRET" --base-url="$BASE_URL" + +echo "Running list targetOutputKey evaluations..." +uv run uipath eval main ../../samples/list_target_output_key_test/evaluations/eval-sets/default.json --no-report --output-file default.json + +echo "Test completed successfully!" diff --git a/packages/uipath/testcases/list-target-output-key-evals/src/assert.py b/packages/uipath/testcases/list-target-output-key-evals/src/assert.py new file mode 100644 index 000000000..80f22c195 --- /dev/null +++ b/packages/uipath/testcases/list-target-output-key-evals/src/assert.py @@ -0,0 +1,90 @@ +"""Assertions for list-target-output-key-evals testcase. + +Validates that evaluating multiple output fields at once (targetOutputKey as a +list) works correctly for both ExactMatch and JsonSimilarity evaluators. + +Expected outcomes +----------------- +- headphones-all-match : ListKeysExactMatch=1.0, ListKeysJsonSimilarity=1.0 +- shoes-all-match : ListKeysExactMatch=1.0, ListKeysJsonSimilarity=1.0 +- headphones-wrong-price: ListKeysExactMatch=0.0, ListKeysJsonSimilarity=1.0 +""" + +import json +import os + +# Maps evaluationName → evaluator ID → expected score (1.0 = pass, 0.0 = fail) +EXPECTED: dict[str, dict[str, float]] = { + "Headphones - all keys match": { + "ListKeysExactMatch": 1.0, + "ListKeysJsonSimilarity": 1.0, + }, + "Running Shoes - all keys match": { + "ListKeysExactMatch": 1.0, + "ListKeysJsonSimilarity": 1.0, + }, + "Headphones - wrong price (should fail)": { + "ListKeysExactMatch": 0.0, + "ListKeysJsonSimilarity": 1.0, + }, +} + + +def main() -> None: + output_file = "default.json" + assert os.path.isfile(output_file), f"Output file '{output_file}' not found" + print(f"Found output file: {output_file}") + + with open(output_file, "r", encoding="utf-8") as f: + output_data = json.load(f) + + assert "evaluationSetResults" in output_data, "Missing 'evaluationSetResults'" + evaluation_results = output_data["evaluationSetResults"] + assert len(evaluation_results) > 0, "No evaluation results found" + print(f"Found {len(evaluation_results)} evaluation result(s)") + + failures: list[str] = [] + + for eval_result in evaluation_results: + eval_name = eval_result.get("evaluationName", "") + expected_scores = EXPECTED.get(eval_name) + + if expected_scores is None: + print(f" [skip] '{eval_name}' not in EXPECTED map") + continue + + print(f"\n Validating: {eval_name}") + + run_results = eval_result.get("evaluationRunResults", []) + assert len(run_results) > 0, f"No run results for '{eval_name}'" + + for run in run_results: + evaluator_id = run.get("evaluatorId", run.get("evaluatorName", "")) + score = run.get("result", {}).get("score", None) + + if evaluator_id not in expected_scores: + print(f" [skip] unexpected evaluator '{evaluator_id}'") + continue + + expected = expected_scores[evaluator_id] + ok = score == expected + status = "pass" if ok else "FAIL" + print(f" {evaluator_id}: score={score} expected={expected} ({status})") + if not ok: + failures.append( + f"{eval_name} / {evaluator_id}: got {score}, expected {expected}" + ) + + print(f"\n{'=' * 60}") + if failures: + for f in failures: + print(f" FAIL: {f}") + print(f"{'=' * 60}") + assert False, f"{len(failures)} assertion(s) failed" + + print(" All assertions passed!") + print(f"{'=' * 60}") + + +if __name__ == "__main__": + main() diff --git a/packages/uipath/testcases/list-target-output-key-evals/uipath.json b/packages/uipath/testcases/list-target-output-key-evals/uipath.json new file mode 100644 index 000000000..a50f100df --- /dev/null +++ b/packages/uipath/testcases/list-target-output-key-evals/uipath.json @@ -0,0 +1,5 @@ +{ + "functions": { + "main": "../../samples/list_target_output_key_test/main.py:main" + } +} diff --git a/packages/uipath/tests/evaluators/test_documentation_examples.py b/packages/uipath/tests/evaluators/test_documentation_examples.py index c75d94329..559df3a01 100644 --- a/packages/uipath/tests/evaluators/test_documentation_examples.py +++ b/packages/uipath/tests/evaluators/test_documentation_examples.py @@ -340,6 +340,52 @@ async def test_using_default_criteria(self) -> None: assert result.score == 1.0 + @pytest.mark.asyncio + async def test_list_target_output_key(self) -> None: + """Test evaluating multiple output fields at once using a list of keys. + + Mirrors the multi-output-agent sample (list-keys-exact-match evaluator). + """ + # Agent returns a rich nested output; we only care about two summary fields. + agent_execution = AgentExecution( + agent_input={"customer_name": "John Doe", "items": []}, + agent_output={ + "order_id": "ORD-001", + "summary": {"status": "completed", "total": 44.97, "item_count": 3}, + "tags": ["priority", "express"], + }, + agent_trace=[], + ) + + evaluator = TypeAdapter(ExactMatchEvaluator).validate_python( + dict( + id="list-keys-exact-match", + evaluatorConfig={ + "name": "ListKeysExactMatch", + # Pass a list to assert multiple fields in a single evaluator run. + "target_output_key": ["summary.status", "summary.total"], + }, + ) + ) + + # Both keys match → score 1.0 + result = await evaluator.validate_and_evaluate_criteria( + agent_execution=agent_execution, + evaluation_criteria={ + "expected_output": {"summary": {"status": "completed", "total": 44.97}} + }, + ) + assert result.score == 1.0 + + # One key differs → score 0.0 + result = await evaluator.validate_and_evaluate_criteria( + agent_execution=agent_execution, + evaluation_criteria={ + "expected_output": {"summary": {"status": "completed", "total": 999.0}} + }, + ) + assert result.score == 0.0 + class TestJsonSimilarityExamples: """Test examples from docs/eval/json_similarity.md.""" diff --git a/packages/uipath/tests/evaluators/test_evaluator_methods.py b/packages/uipath/tests/evaluators/test_evaluator_methods.py index 22cfc980e..ec795499d 100644 --- a/packages/uipath/tests/evaluators/test_evaluator_methods.py +++ b/packages/uipath/tests/evaluators/test_evaluator_methods.py @@ -447,6 +447,448 @@ async def test_exact_match_line_by_line_has_individual_results(self) -> None: assert line3_result.score == 1.0 +class TestListTargetOutputKey: + """Test target_output_key as a list of keys.""" + + @pytest.mark.asyncio + async def test_exact_match_list_keys_all_match(self) -> None: + """All listed keys match → score 1.0.""" + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok", "total": 42, "extra": "ignored"}, + agent_trace=[], + ) + config = { + "name": "ExactMatchListKeys", + "target_output_key": ["status", "total"], + } + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"status": "ok", "total": 42} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, NumericEvaluationResult) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_exact_match_list_keys_value_mismatch(self) -> None: + """One key's value differs → score 0.0.""" + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok", "total": 99}, + agent_trace=[], + ) + config = { + "name": "ExactMatchListKeys", + "target_output_key": ["status", "total"], + } + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"status": "ok", "total": 42} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, NumericEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_exact_match_list_keys_dot_notation(self) -> None: + """Nested dot-notation paths inside a list of keys.""" + execution = AgentExecution( + agent_input={}, + agent_output={"order": {"status": "shipped"}, "qty": 3}, + agent_trace=[], + ) + config = { + "name": "ExactMatchListDotKeys", + "target_output_key": ["order.status", "qty"], + } + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"order": {"status": "shipped"}, "qty": 3} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, NumericEvaluationResult) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_list_keys_missing_key_in_actual_raises(self) -> None: + """Missing key in actual output returns an ErrorEvaluationResult.""" + from uipath.eval.models.models import ErrorEvaluationResult + + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok"}, # 'total' is missing + agent_trace=[], + ) + config = { + "name": "ExactMatchListKeys", + "target_output_key": ["status", "total"], + } + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"status": "ok", "total": 42} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, ErrorEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_list_keys_missing_key_in_expected_raises(self) -> None: + """Missing key in expected output returns an ErrorEvaluationResult.""" + from uipath.eval.models.models import ErrorEvaluationResult + + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok", "total": 42}, + agent_trace=[], + ) + config = { + "name": "ExactMatchListKeys", + "target_output_key": ["status", "total"], + } + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={ + "status": "ok" + } # 'total' missing # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, ErrorEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_list_keys_expected_as_json_string(self) -> None: + """Expected output as a JSON string is parsed when key is a list.""" + import json + + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok", "total": 5}, + agent_trace=[], + ) + config = { + "name": "ExactMatchListKeys", + "target_output_key": ["status", "total"], + } + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output=json.dumps({"status": "ok", "total": 5}) # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, NumericEvaluationResult) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_list_keys_invalid_json_string_expected_raises(self) -> None: + """Invalid JSON string for expected output returns an ErrorEvaluationResult.""" + from uipath.eval.models.models import ErrorEvaluationResult + + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok"}, + agent_trace=[], + ) + config = {"name": "ExactMatchListKeys", "target_output_key": ["status"]} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output="not valid json" # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, ErrorEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_list_keys_disables_line_by_line(self) -> None: + """line_by_line_evaluator=True is ignored when target_output_key is a list.""" + execution = AgentExecution( + agent_input={}, + agent_output={"a": "x", "b": "y"}, + agent_trace=[], + ) + config = { + "name": "ExactMatchListLbl", + "target_output_key": ["a", "b"], + "line_by_line_evaluator": True, + } + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + raw_criteria = {"expected_output": {"a": "x", "b": "y"}} + # Should not raise or split into lines; returns a plain NumericEvaluationResult + result = await evaluator.validate_and_evaluate_criteria(execution, raw_criteria) + assert isinstance(result, NumericEvaluationResult) + assert result.score == 1.0 + # No line-by-line details attached + assert ( + not hasattr(result, "_line_by_line_results") + or result._line_by_line_results is None + ) + + @pytest.mark.asyncio + async def test_json_similarity_list_keys_perfect_match(self) -> None: + """JsonSimilarityEvaluator with list keys scores 1.0 on exact match.""" + from uipath.eval.evaluators.json_similarity_evaluator import ( + JsonSimilarityEvaluator, + ) + + execution = AgentExecution( + agent_input={}, + agent_output={"name": "Alice", "score": 100, "extra": "ignored"}, + agent_trace=[], + ) + config = { + "name": "JsonSimListKeys", + "target_output_key": ["name", "score"], + } + evaluator = JsonSimilarityEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"name": "Alice", "score": 100} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, NumericEvaluationResult) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_list_keys_non_dict_actual_raises(self) -> None: + """Non-dict agent_output with list key returns ErrorEvaluationResult.""" + from uipath.eval.models.models import ErrorEvaluationResult + + execution = AgentExecution( + agent_input={}, + agent_output="just a string", # pyright: ignore[reportArgumentType] + agent_trace=[], + ) + config = {"name": "ExactMatchListKeys", "target_output_key": ["status"]} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"status": "ok"} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, ErrorEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_list_keys_non_dict_json_expected_raises(self) -> None: + """Valid JSON that parses to a non-dict returns ErrorEvaluationResult.""" + import json + + from uipath.eval.models.models import ErrorEvaluationResult + + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok"}, + agent_trace=[], + ) + config = {"name": "ExactMatchListKeys", "target_output_key": ["status"]} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + # Valid JSON but not an object — triggers the isinstance(expected_output, dict) guard + criteria = OutputEvaluationCriteria( + expected_output=json.dumps("just a string") # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, ErrorEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_list_keys_attachment_uri_downloaded( + self, mocker: MockerFixture + ) -> None: + """Attachment URIs within list-key actual output values are downloaded.""" + att_uri = ( + "urn:uipath:cas:file:orchestrator:00000000-0000-0000-0000-000000000001" + ) + mocker.patch( + "uipath.eval.evaluators.output_evaluator.download_attachment_as_string", + return_value="downloaded_content", + ) + execution = AgentExecution( + agent_input={}, + agent_output={"file": att_uri, "status": "ok"}, + agent_trace=[], + ) + config = {"name": "ExactMatchListKeys", "target_output_key": ["file", "status"]} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"file": "downloaded_content", "status": "ok"} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, NumericEvaluationResult) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_scalar_key_attachment_uri_downloaded( + self, mocker: MockerFixture + ) -> None: + """Attachment URI in scalar-key actual output is downloaded.""" + att_uri = ( + "urn:uipath:cas:file:orchestrator:00000000-0000-0000-0000-000000000002" + ) + mocker.patch( + "uipath.eval.evaluators.output_evaluator.download_attachment_as_string", + return_value="file_content", + ) + execution = AgentExecution( + agent_input={}, + agent_output={"report": att_uri}, + agent_trace=[], + ) + config = {"name": "ExactMatchScalarAtt", "target_output_key": "report"} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"report": "file_content"} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, NumericEvaluationResult) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_scalar_key_missing_in_actual_raises(self) -> None: + """Missing scalar target_output_key in actual output returns ErrorEvaluationResult.""" + from uipath.eval.models.models import ErrorEvaluationResult + + execution = AgentExecution( + agent_input={}, + agent_output={"other": "value"}, + agent_trace=[], + ) + config = {"name": "ExactMatchMissingActual", "target_output_key": "missing_key"} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output={"missing_key": "val"} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, ErrorEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_scalar_key_missing_in_expected_raises(self) -> None: + """Missing scalar target_output_key in expected output returns ErrorEvaluationResult.""" + from uipath.eval.models.models import ErrorEvaluationResult + + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok"}, + agent_trace=[], + ) + config = {"name": "ExactMatchMissingExpected", "target_output_key": "status"} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + # expected output dict doesn't contain the target key + criteria = OutputEvaluationCriteria( + expected_output={"other_key": "ok"} # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, ErrorEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_scalar_key_invalid_json_expected_raises(self) -> None: + """Invalid JSON string for expected output with scalar key returns ErrorEvaluationResult.""" + from uipath.eval.models.models import ErrorEvaluationResult + + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok"}, + agent_trace=[], + ) + config = {"name": "ExactMatchInvalidJson", "target_output_key": "status"} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + criteria = OutputEvaluationCriteria( + expected_output="not valid json" # pyright: ignore[reportCallIssue] + ) + result = await evaluator.evaluate(execution, criteria) + assert isinstance(result, ErrorEvaluationResult) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_validate_and_evaluate_criteria_none_raises(self) -> None: + """None criteria with no default configured raises UiPathEvaluationError.""" + execution = AgentExecution( + agent_input={}, + agent_output={"status": "ok"}, + agent_trace=[], + ) + config = {"name": "ExactMatchNoCriteria"} + evaluator = ExactMatchEvaluator.model_validate( + {"evaluatorConfig": config, "id": str(uuid.uuid4())} + ) + with pytest.raises(UiPathEvaluationError) as exc_info: + await evaluator.validate_and_evaluate_criteria(execution, None) + assert "MISSING_EVALUATION_CRITERIA" in exc_info.value.error_info.code + + def test_base_output_evaluator_get_full_expected_output_raises(self) -> None: + """BaseOutputEvaluator._get_full_expected_output raises NOT_IMPLEMENTED.""" + from uipath.eval.evaluators.base_evaluator import BaseEvaluatorJustification + from uipath.eval.evaluators.output_evaluator import ( + BaseOutputEvaluator, + OutputEvaluationCriteria, + OutputEvaluatorConfig, + ) + from uipath.eval.models import EvaluationResult + + class _MinimalEvaluator( + BaseOutputEvaluator[ + OutputEvaluationCriteria, + OutputEvaluatorConfig[OutputEvaluationCriteria], + BaseEvaluatorJustification, + ] + ): + @classmethod + def get_evaluator_id(cls) -> str: + return "uipath-minimal-test" + + async def evaluate( + self, agent_execution: Any, evaluation_criteria: Any + ) -> EvaluationResult: + return None # type: ignore[return-value] + + def validate_evaluation_criteria( + self, raw: Any + ) -> OutputEvaluationCriteria: + return OutputEvaluationCriteria.model_validate(raw) + + evaluator = _MinimalEvaluator.model_validate( + { + "evaluatorConfig": {"name": "minimal"}, + "id": str(uuid.uuid4()), + } + ) + with pytest.raises(UiPathEvaluationError) as exc_info: + evaluator._get_full_expected_output( # pyright: ignore[reportArgumentType] + OutputEvaluationCriteria(expected_output={}) # pyright: ignore[reportCallIssue] + ) + assert "NOT_IMPLEMENTED" in exc_info.value.error_info.code + + class TestContainsEvaluator: """Test ContainsEvaluator.evaluate() method.""" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index cf11b2a5b..fee751155 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.77" +version = "2.10.78" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 9a5d2bbbef4f049c36728bfb44c91a49a9bd69f0 Mon Sep 17 00:00:00 2001 From: yashwagle1 Date: Fri, 5 Jun 2026 14:47:20 -0700 Subject: [PATCH 088/121] move pii masking service to llmops (#1701) Co-authored-by: Claude Opus 4.8 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/_uipath.py | 5 + .../uipath/platform/pii_detection/__init__.py | 36 +++ .../pii_detection/_pii_detection_service.py | 80 ++++++ .../platform/pii_detection/pii_detection.py | 91 ++++++ .../platform/pii_detection/pii_utilities.py | 98 +++++++ .../platform/semantic_proxy/pii_utilities.py | 21 +- .../services/test_pii_detection_service.py | 264 ++++++++++++++++++ .../tests/services/test_pii_utilities.py | 11 +- packages/uipath-platform/uv.lock | 4 +- packages/uipath/uv.lock | 4 +- 11 files changed, 591 insertions(+), 25 deletions(-) create mode 100644 packages/uipath-platform/src/uipath/platform/pii_detection/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/pii_detection/_pii_detection_service.py create mode 100644 packages/uipath-platform/src/uipath/platform/pii_detection/pii_detection.py create mode 100644 packages/uipath-platform/src/uipath/platform/pii_detection/pii_utilities.py create mode 100644 packages/uipath-platform/tests/services/test_pii_detection_service.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index dfc759f4d..aa3ef6b47 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.60" +version = "0.1.61" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index f81333862..8e0e23867 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -35,6 +35,7 @@ ProcessesService, QueuesService, ) +from .pii_detection import PiiDetectionService from .resource_catalog import ResourceCatalogService from .semantic_proxy import SemanticProxyService @@ -184,6 +185,10 @@ def orchestrator_setup(self) -> OrchestratorSetupService: def automation_ops(self) -> AutomationOpsService: return AutomationOpsService(self._config, self._execution_context) + @property + def pii_detection(self) -> PiiDetectionService: + return PiiDetectionService(self._config, self._execution_context) + @property def semantic_proxy(self) -> SemanticProxyService: return SemanticProxyService(self._config, self._execution_context) diff --git a/packages/uipath-platform/src/uipath/platform/pii_detection/__init__.py b/packages/uipath-platform/src/uipath/platform/pii_detection/__init__.py new file mode 100644 index 000000000..7ab3b9e26 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/pii_detection/__init__.py @@ -0,0 +1,36 @@ +"""PiiDetection service package. + +Provides the ``PiiDetectionService`` client, Pydantic request/response models for +the PII detection endpoint, and utilities for rehydrating masked text with +original PII values after LLM processing. +""" + +from ._pii_detection_service import PiiDetectionService +from .pii_detection import ( + PiiDetectionRequest, + PiiDetectionResponse, + PiiDocument, + PiiDocumentResult, + PiiEntity, + PiiEntityThreshold, + PiiFile, + PiiFileResult, +) +from .pii_utilities import ( + rehydrate_from_pii_entities, + rehydrate_from_pii_response, +) + +__all__ = [ + "PiiDetectionRequest", + "PiiDetectionResponse", + "PiiDetectionService", + "PiiDocument", + "PiiDocumentResult", + "PiiEntity", + "PiiEntityThreshold", + "PiiFile", + "PiiFileResult", + "rehydrate_from_pii_entities", + "rehydrate_from_pii_response", +] diff --git a/packages/uipath-platform/src/uipath/platform/pii_detection/_pii_detection_service.py b/packages/uipath-platform/src/uipath/platform/pii_detection/_pii_detection_service.py new file mode 100644 index 000000000..a39ed4196 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/pii_detection/_pii_detection_service.py @@ -0,0 +1,80 @@ +"""PiiDetection service for UiPath Platform. + +Provides methods for detecting PII in documents and files. +""" + +from uipath.core.tracing import traced + +from ..common._base_service import BaseService +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ..common._models import Endpoint, RequestSpec +from .pii_detection import PiiDetectionRequest, PiiDetectionResponse + +_PII_DETECTION_ENDPOINT = Endpoint("llmopstenant_/api/pii-detection") + +# PII detection over documents/files can be slow, so override the default +# httpx client timeout (30s) with a longer per-request timeout. +_PII_DETECTION_TIMEOUT = 290.0 + + +class PiiDetectionService(BaseService): + """Service for detecting PII via UiPath.""" + + def __init__( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + super().__init__(config=config, execution_context=execution_context) + + @traced(name="pii_detection_detect_pii", run_type="uipath") + def detect_pii(self, request: PiiDetectionRequest) -> PiiDetectionResponse: + """Detect PII in the provided documents and/or files. + + Args: + request: The PII detection request payload. + + Returns: + The PII detection response. + """ + spec = self._pii_detection_spec(request) + response = self.request( + spec.method, + url=spec.endpoint, + json=spec.json, + headers=spec.headers, + scoped="tenant", + timeout=_PII_DETECTION_TIMEOUT, + ) + return PiiDetectionResponse.model_validate(response.json()) + + @traced(name="pii_detection_detect_pii", run_type="uipath") + async def detect_pii_async( + self, request: PiiDetectionRequest + ) -> PiiDetectionResponse: + """Detect PII in the provided documents and/or files (async). + + Args: + request: The PII detection request payload. + + Returns: + The PII detection response. + """ + spec = self._pii_detection_spec(request) + response = await self.request_async( + spec.method, + url=spec.endpoint, + json=spec.json, + headers=spec.headers, + scoped="tenant", + timeout=_PII_DETECTION_TIMEOUT, + ) + return PiiDetectionResponse.model_validate(response.json()) + + def _pii_detection_spec(self, request: PiiDetectionRequest) -> RequestSpec: + return RequestSpec( + method="POST", + endpoint=_PII_DETECTION_ENDPOINT, + json=request.model_dump(by_alias=True, exclude_none=True), + ) diff --git a/packages/uipath-platform/src/uipath/platform/pii_detection/pii_detection.py b/packages/uipath-platform/src/uipath/platform/pii_detection/pii_detection.py new file mode 100644 index 000000000..94ac10fca --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/pii_detection/pii_detection.py @@ -0,0 +1,91 @@ +"""Public Pydantic models for the PiiDetection service.""" + +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class PiiDocument(BaseModel): + """A text document to scan for PII.""" + + id: str + role: str + document: str + + +class PiiFile(BaseModel): + """A file reference to scan for PII.""" + + model_config = ConfigDict(populate_by_name=True) + + file_name: str = Field(alias="fileName") + file_url: str = Field(alias="fileUrl") + file_type: str = Field(alias="fileType") + + +class PiiEntityThreshold(BaseModel): + """Per-entity confidence threshold override.""" + + model_config = ConfigDict(populate_by_name=True) + + category: str = Field(alias="pii-entity-category") + confidence_threshold: float = Field(alias="pii-entity-confidence-threshold") + + +class PiiDetectionRequest(BaseModel): + """Request payload for the PII detection endpoint.""" + + model_config = ConfigDict(populate_by_name=True) + + documents: Optional[list[PiiDocument]] = None + files: Optional[list[PiiFile]] = None + language_code: Optional[str] = Field(default=None, alias="languageCode") + confidence_threshold: Optional[float] = Field( + default=None, alias="confidenceThreshold" + ) + entity_thresholds: Optional[list[PiiEntityThreshold]] = Field( + default=None, alias="entityThresholds" + ) + + +class PiiEntity(BaseModel): + """A single detected PII entity.""" + + model_config = ConfigDict(populate_by_name=True) + + pii_text: str = Field(alias="piiText") + replacement_text: str = Field(alias="replacementText") + pii_type: str = Field(alias="piiType") + offset: int + confidence_score: float = Field(alias="confidenceScore") + + +class PiiDocumentResult(BaseModel): + """PII detection result for a single document.""" + + model_config = ConfigDict(populate_by_name=True) + + id: str + role: str + masked_document: str = Field(alias="maskedDocument") + initial_document: str = Field(alias="initialDocument") + pii_entities: list[PiiEntity] = Field(default_factory=list, alias="piiEntities") + + +class PiiFileResult(BaseModel): + """PII detection result for a single file (fileUrl is the redacted URL).""" + + model_config = ConfigDict(populate_by_name=True) + + file_name: str = Field(alias="fileName") + file_url: str = Field(alias="fileUrl") + pii_entities: list[PiiEntity] = Field(default_factory=list, alias="piiEntities") + + +class PiiDetectionResponse(BaseModel): + """Response payload from the PII detection endpoint.""" + + model_config = ConfigDict(populate_by_name=True) + + response: list[PiiDocumentResult] = Field(default_factory=list) + files: list[PiiFileResult] = Field(default_factory=list) diff --git a/packages/uipath-platform/src/uipath/platform/pii_detection/pii_utilities.py b/packages/uipath-platform/src/uipath/platform/pii_detection/pii_utilities.py new file mode 100644 index 000000000..b2fa482d0 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/pii_detection/pii_utilities.py @@ -0,0 +1,98 @@ +"""Utility methods for working with PII data. + +Python port of UiPath.SemanticProxy.Client.PiiUtilities (C#). +""" + +import re +from typing import Callable, Iterable + +from .pii_detection import PiiDetectionResponse, PiiEntity + + +def rehydrate_from_pii_entities( + masked_text: str, pii_entities: Iterable[PiiEntity] +) -> str: + """Rehydrate masked text by replacing PII placeholders with original values. + + Placeholders (e.g. ``[Person-1]``) are matched case-insensitively and replaced + with the corresponding original PII text. The function also replaces variants + without the surrounding brackets (e.g. ``Person-1``) in case the LLM stripped + them in its output. + + Args: + masked_text: The masked text with PII placeholders. + pii_entities: The PII entities containing the original values. + + Returns: + The rehydrated text with original PII values. + """ + if not masked_text: + return masked_text + + entities = [e for e in pii_entities if e.replacement_text] + if not entities: + return masked_text + + # Sort by replacement text length descending to avoid substring collisions + # (e.g. "[Person-10]" must be replaced before "[Person-1]"). + entities.sort(key=lambda e: len(e.replacement_text), reverse=True) + + rehydrated = masked_text + for entity in entities: + if not entity.replacement_text or not entity.pii_text: + continue + # Replace the full placeholder (with brackets) case-insensitively. + # ``_literal_replacer`` bypasses regex backreference interpretation in the + # replacement string. + rehydrated = re.sub( + re.escape(entity.replacement_text), + _literal_replacer(entity.pii_text), + rehydrated, + flags=re.IGNORECASE, + ) + # Also replace the content without brackets (in case the LLM dropped them). + if entity.replacement_text.startswith("[") and entity.replacement_text.endswith( + "]" + ): + no_brackets = entity.replacement_text[1:-1] + rehydrated = re.sub( + re.escape(no_brackets), + _literal_replacer(entity.pii_text), + rehydrated, + flags=re.IGNORECASE, + ) + + return rehydrated + + +def _literal_replacer(replacement: str) -> Callable[[re.Match[str]], str]: + """Return a replacement function that ignores regex backreference syntax.""" + + def replace(_match: re.Match[str]) -> str: + return replacement + + return replace + + +def rehydrate_from_pii_response( + masked_text: str, response: PiiDetectionResponse +) -> str: + """Rehydrate masked text using all PII entities from a detection response. + + Merges entities from both ``response.response`` (detected in documents/prompts) + and ``response.files`` (detected in files), so placeholders originating from + either source are rehydrated. + + Args: + masked_text: The masked text with PII placeholders. + response: The PII detection response containing entities to rehydrate. + + Returns: + The rehydrated text with original PII values. + """ + entities: list[PiiEntity] = [] + for doc in response.response: + entities.extend(doc.pii_entities) + for file in response.files: + entities.extend(file.pii_entities) + return rehydrate_from_pii_entities(masked_text, entities) diff --git a/packages/uipath-platform/src/uipath/platform/semantic_proxy/pii_utilities.py b/packages/uipath-platform/src/uipath/platform/semantic_proxy/pii_utilities.py index c480a5fb7..0f031a19a 100644 --- a/packages/uipath-platform/src/uipath/platform/semantic_proxy/pii_utilities.py +++ b/packages/uipath-platform/src/uipath/platform/semantic_proxy/pii_utilities.py @@ -3,7 +3,6 @@ Python port of UiPath.SemanticProxy.Client.PiiUtilities (C#). """ -import json import re from typing import Callable, Iterable @@ -42,13 +41,12 @@ def rehydrate_from_pii_entities( for entity in entities: if not entity.replacement_text or not entity.pii_text: continue - escaped_pii = _add_escape_characters(entity.pii_text) # Replace the full placeholder (with brackets) case-insensitively. # ``_literal_replacer`` bypasses regex backreference interpretation in the # replacement string. rehydrated = re.sub( re.escape(entity.replacement_text), - _literal_replacer(escaped_pii), + _literal_replacer(entity.pii_text), rehydrated, flags=re.IGNORECASE, ) @@ -59,7 +57,7 @@ def rehydrate_from_pii_entities( no_brackets = entity.replacement_text[1:-1] rehydrated = re.sub( re.escape(no_brackets), - _literal_replacer(escaped_pii), + _literal_replacer(entity.pii_text), rehydrated, flags=re.IGNORECASE, ) @@ -98,18 +96,3 @@ def rehydrate_from_pii_response( for file in response.files: entities.extend(file.pii_entities) return rehydrate_from_pii_entities(masked_text, entities) - - -def _add_escape_characters(text: str) -> str: - """Escape special characters in text using JSON serialization. - - Mirrors C# ``AddEscapeCharacters`` — serializes as JSON then strips the - surrounding quotes to get the escaped content. - """ - if not text: - return "" - try: - serialized = json.dumps(text, ensure_ascii=False) - return serialized[1:-1] - except (TypeError, ValueError): - return text diff --git a/packages/uipath-platform/tests/services/test_pii_detection_service.py b/packages/uipath-platform/tests/services/test_pii_detection_service.py new file mode 100644 index 000000000..2bb424607 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_pii_detection_service.py @@ -0,0 +1,264 @@ +"""Tests for PiiDetectionService.""" + +import json +from typing import Any + +import httpx +import pytest +from pytest_httpx import HTTPXMock + +from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.pii_detection import ( + PiiDetectionRequest, + PiiDetectionResponse, + PiiDetectionService, + PiiDocument, + PiiEntityThreshold, + PiiFile, +) + + +@pytest.fixture +def service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, +) -> PiiDetectionService: + return PiiDetectionService(config=config, execution_context=execution_context) + + +@pytest.fixture +def sample_response_json() -> dict[str, Any]: + return { + "response": [ + { + "id": "user-prompt", + "role": "user", + "maskedDocument": "Contact [Person-1]", + "initialDocument": "Contact Alison", + "piiEntities": [ + { + "piiText": "Alison", + "replacementText": "[Person-1]", + "piiType": "Person", + "offset": 8, + "confidenceScore": 0.99, + } + ], + } + ], + "files": [ + { + "fileName": "doc.pdf", + "fileUrl": "https://blob.example.com/redacted/doc.pdf", + "piiEntities": [ + { + "piiText": "alice@example.com", + "replacementText": "[Email-1]", + "piiType": "Email", + "offset": 100, + "confidenceScore": 0.88, + } + ], + } + ], + } + + +class TestPiiDetectionService: + """Test PiiDetectionService functionality.""" + + class TestDetectPii: + """Test detect_pii (sync).""" + + def test_returns_typed_response( + self, + httpx_mock: HTTPXMock, + service: PiiDetectionService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/pii-detection", + status_code=200, + json=sample_response_json, + ) + + request = PiiDetectionRequest( + documents=[ + PiiDocument( + id="user-prompt", role="user", document="Contact Alison" + ) + ] + ) + result = service.detect_pii(request) + + assert isinstance(result, PiiDetectionResponse) + assert len(result.response) == 1 + assert result.response[0].masked_document == "Contact [Person-1]" + assert len(result.files) == 1 + assert result.files[0].file_name == "doc.pdf" + assert result.files[0].pii_entities[0].replacement_text == "[Email-1]" + + class TestDetectPiiAsync: + """Test detect_pii_async.""" + + async def test_returns_typed_response( + self, + httpx_mock: HTTPXMock, + service: PiiDetectionService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/llmopstenant_/api/pii-detection", + status_code=200, + json=sample_response_json, + ) + + request = PiiDetectionRequest( + files=[ + PiiFile( + file_name="doc.pdf", + file_url="https://input.example.com/doc.pdf", + file_type="pdf", + ) + ] + ) + result = await service.detect_pii_async(request) + + assert isinstance(result, PiiDetectionResponse) + assert ( + result.files[0].file_url == "https://blob.example.com/redacted/doc.pdf" + ) + + async def test_request_payload_uses_aliases( + self, + httpx_mock: HTTPXMock, + service: PiiDetectionService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json=sample_response_json) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/llmopstenant_/api/pii-detection", + callback=capture, + ) + + request = PiiDetectionRequest( + documents=[ + PiiDocument(id="user-prompt", role="user", document="Hello") + ], + files=[ + PiiFile( + file_name="doc.pdf", + file_url="https://input.example.com/doc.pdf", + file_type="pdf", + ) + ], + language_code="en", + confidence_threshold=0.5, + entity_thresholds=[ + PiiEntityThreshold(category="Person", confidence_threshold=0.7), + ], + ) + await service.detect_pii_async(request) + + assert captured_request is not None + payload = json.loads(captured_request.content) + + # Top-level uses camelCase aliases + assert "documents" in payload + assert "files" in payload + assert "languageCode" in payload + assert "confidenceThreshold" in payload + assert "entityThresholds" in payload + + # File uses camelCase aliases + assert payload["files"][0]["fileName"] == "doc.pdf" + assert payload["files"][0]["fileUrl"] == "https://input.example.com/doc.pdf" + assert payload["files"][0]["fileType"] == "pdf" + + # Entity threshold uses kebab-case aliases + threshold = payload["entityThresholds"][0] + assert threshold["pii-entity-category"] == "Person" + assert threshold["pii-entity-confidence-threshold"] == 0.7 + + async def test_request_excludes_none_fields( + self, + httpx_mock: HTTPXMock, + service: PiiDetectionService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json=sample_response_json) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/llmopstenant_/api/pii-detection", + callback=capture, + ) + + # Only documents set; other optional fields should be omitted + request = PiiDetectionRequest( + documents=[PiiDocument(id="user-prompt", role="user", document="Hello")] + ) + await service.detect_pii_async(request) + + assert captured_request is not None + payload = json.loads(captured_request.content) + assert "files" not in payload + assert "languageCode" not in payload + assert "confidenceThreshold" not in payload + assert "entityThresholds" not in payload + + async def test_url_is_tenant_scoped( + self, + httpx_mock: HTTPXMock, + service: PiiDetectionService, + base_url: str, + org: str, + tenant: str, + sample_response_json: dict[str, Any], + ) -> None: + captured_request: httpx.Request | None = None + + def capture(request: httpx.Request) -> httpx.Response: + nonlocal captured_request + captured_request = request + return httpx.Response(status_code=200, json=sample_response_json) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/llmopstenant_/api/pii-detection", + callback=capture, + ) + + request = PiiDetectionRequest( + documents=[PiiDocument(id="user-prompt", role="user", document="Hello")] + ) + await service.detect_pii_async(request) + + assert captured_request is not None + assert org.strip("/") in captured_request.url.path + assert tenant.strip("/") in captured_request.url.path + assert "/llmopstenant_/api/pii-detection" in captured_request.url.path diff --git a/packages/uipath-platform/tests/services/test_pii_utilities.py b/packages/uipath-platform/tests/services/test_pii_utilities.py index 844985a90..751897609 100644 --- a/packages/uipath-platform/tests/services/test_pii_utilities.py +++ b/packages/uipath-platform/tests/services/test_pii_utilities.py @@ -1,6 +1,6 @@ """Tests for PII rehydration utilities.""" -from uipath.platform.semantic_proxy import ( +from uipath.platform.pii_detection import ( PiiDetectionResponse, PiiDocumentResult, PiiEntity, @@ -116,6 +116,15 @@ def test_regex_special_chars_in_replacement_text(self) -> None: ) assert result == "Hello Alice" + def test_pii_text_inserted_verbatim_not_json_escaped(self) -> None: + """PII values are plain text: quotes, newlines and backslashes are + inserted as-is, never JSON-escaped.""" + pii = 'Bob "The Boss"\nC:\\Users\\bob' + result = rehydrate_from_pii_entities( + "Name: [Person-1]", [_entity(pii, "[Person-1]")] + ) + assert result == f"Name: {pii}" + class TestRehydrateFromPiiResponse: """Test rehydrate_from_pii_response.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 084f3efb8..34dc9d413 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-25T20:56:22.964456Z" +exclude-newer = "2026-06-03T20:09:03.1179056Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.60" +version = "0.1.61" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index fee751155..657a4cf7d 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-27T15:26:21.545236Z" +exclude-newer = "2026-06-03T20:09:04.4644787Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.60" +version = "0.1.61" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 4a1489b5070cbd34ab258cfaba6f6c9afb65f3e7 Mon Sep 17 00:00:00 2001 From: Viswanath Lekshmanan <84002867+viswa-uipath@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:33:51 +0530 Subject: [PATCH 089/121] feat[PLT-105095]: changes for governance (#1700) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/adapters/__init__.py | 35 ++ .../src/uipath/core/adapters/base.py | 116 +++++ .../src/uipath/core/adapters/evaluator.py | 99 ++++ .../src/uipath/core/adapters/registry.py | 176 +++++++ .../src/uipath/core/governance/__init__.py | 37 ++ .../src/uipath/core/governance/config.py | 34 ++ .../src/uipath/core/governance/exceptions.py | 114 ++++ .../src/uipath/core/governance/models.py | 68 +++ .../uipath-core/tests/adapters/__init__.py | 0 .../uipath-core/tests/adapters/test_base.py | 163 ++++++ .../tests/adapters/test_evaluator.py | 104 ++++ .../tests/adapters/test_registry.py | 492 ++++++++++++++++++ .../uipath-core/tests/governance/__init__.py | 0 .../tests/governance/test_config.py | 54 ++ .../tests/governance/test_exceptions.py | 205 ++++++++ packages/uipath-core/uv.lock | 4 +- packages/uipath-platform/uv.lock | 4 +- packages/uipath/uv.lock | 4 +- 19 files changed, 1704 insertions(+), 7 deletions(-) create mode 100644 packages/uipath-core/src/uipath/core/adapters/__init__.py create mode 100644 packages/uipath-core/src/uipath/core/adapters/base.py create mode 100644 packages/uipath-core/src/uipath/core/adapters/evaluator.py create mode 100644 packages/uipath-core/src/uipath/core/adapters/registry.py create mode 100644 packages/uipath-core/src/uipath/core/governance/__init__.py create mode 100644 packages/uipath-core/src/uipath/core/governance/config.py create mode 100644 packages/uipath-core/src/uipath/core/governance/exceptions.py create mode 100644 packages/uipath-core/src/uipath/core/governance/models.py create mode 100644 packages/uipath-core/tests/adapters/__init__.py create mode 100644 packages/uipath-core/tests/adapters/test_base.py create mode 100644 packages/uipath-core/tests/adapters/test_evaluator.py create mode 100644 packages/uipath-core/tests/adapters/test_registry.py create mode 100644 packages/uipath-core/tests/governance/__init__.py create mode 100644 packages/uipath-core/tests/governance/test_config.py create mode 100644 packages/uipath-core/tests/governance/test_exceptions.py diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index c07e4c3b7..eef3ab27e 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.17" +version = "0.5.18" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/adapters/__init__.py b/packages/uipath-core/src/uipath/core/adapters/__init__.py new file mode 100644 index 000000000..5906b1b39 --- /dev/null +++ b/packages/uipath-core/src/uipath/core/adapters/__init__.py @@ -0,0 +1,35 @@ +"""Generic adapter contracts for framework integrations. + +This package holds only the abstract contracts — concrete adapter +implementations live in framework-specific plugin packages (e.g. +``uipath-langchain``, ``uipath-openai``) that target the framework they +integrate with. Plugin packages register their concrete adapters with +the global :class:`AdapterRegistry` via the +``uipath.governance.adapters`` entry-point group. + +Public surface: + +- :class:`BaseAdapter` – abstract base every adapter inherits from. +- :class:`GovernedAgentBase` – proxy base for governed agent wrappers. +- :class:`EvaluatorProtocol` – structural protocol the adapter expects + from any policy evaluator. +- :class:`AdapterRegistry` – ordered list of adapters that resolves + the first match for a given agent. +""" + +from .base import BaseAdapter, GovernedAgentBase +from .evaluator import EvaluatorProtocol +from .registry import ( + AdapterRegistry, + get_adapter_registry, + reset_adapter_registry, +) + +__all__ = [ + "BaseAdapter", + "GovernedAgentBase", + "EvaluatorProtocol", + "AdapterRegistry", + "get_adapter_registry", + "reset_adapter_registry", +] diff --git a/packages/uipath-core/src/uipath/core/adapters/base.py b/packages/uipath-core/src/uipath/core/adapters/base.py new file mode 100644 index 000000000..3afaad6a7 --- /dev/null +++ b/packages/uipath-core/src/uipath/core/adapters/base.py @@ -0,0 +1,116 @@ +"""Base adapter contracts for framework-specific integrations. + +An adapter's job: + +1. Detect whether it can handle a given agent object. +2. Attach hooks to that agent (framework-specific). +3. Publish events to a policy evaluator when those hooks fire. + +The evaluator subscribes to events and runs policy checks; it never +knows or cares which adapter fired the event. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any +from uuid import uuid4 + +from .evaluator import EvaluatorProtocol + + +class BaseAdapter(ABC): + """Base class for framework-specific governance adapters.""" + + #: Higher value = more specific = inserted earlier in the registry. + #: Plugin authors should set this above ``0`` on adapters that target + #: a narrower agent type than another already-registered adapter, so + #: the specific one wins ``can_handle`` resolution regardless of the + #: order in which plugins happen to be imported. Among adapters with + #: the same priority, registration order is preserved (stable). + priority: int = 0 + + #: Set to True on a catch-all adapter that should always sort last in + #: the registry. The registry uses this flag (not the class name or + #: :attr:`priority`) to keep the fallback in last position when new + #: adapters register. + is_fallback: bool = False + + @property + def name(self) -> str: + """Return adapter name for logging.""" + return self.__class__.__name__ + + @abstractmethod + def can_handle(self, agent: Any) -> bool: + """Return True if this adapter knows how to hook into this agent type.""" + + @abstractmethod + def attach( + self, + agent: Any, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> Any: + """Attach governance hooks to the agent. + + Args: + agent: The agent to govern. + agent_id: Unique identifier for the agent. + session_id: Session identifier for tracing. + evaluator: Policy evaluator implementing + :class:`EvaluatorProtocol`. + + Returns: + A governed proxy (or the original agent with hooks installed). + """ + + def detach(self, governed: Any) -> Any: + """Detach governance and return the original agent. + + Default implementation uses the public :attr:`GovernedAgentBase.unwrapped` + contract; non-proxy adapters that return the original agent from + :meth:`attach` get back ``governed`` unchanged. + """ + return getattr(governed, "unwrapped", governed) + + def _generate_trace_id(self) -> str: + """Generate a trace ID for governance events.""" + return str(uuid4()) + + +class GovernedAgentBase: + """Base class for governed agent proxies. + + Provides common functionality for all governed agents: + + - Stores reference to original agent + - Forwards unknown attributes to original agent + - Tracks governance metadata + """ + + def __init__( + self, + agent: Any, + adapter: BaseAdapter, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> None: + """Initialize with the wrapped agent and governance metadata.""" + self._agent = agent + self._adapter = adapter + self._agent_id = agent_id + self._session_id = session_id + self._evaluator = evaluator + self._trace_id = adapter._generate_trace_id() + + @property + def unwrapped(self) -> Any: + """Get the original unwrapped agent.""" + return self._agent + + def __getattr__(self, name: str) -> Any: + """Forward attribute access to the original agent.""" + return getattr(self._agent, name) diff --git a/packages/uipath-core/src/uipath/core/adapters/evaluator.py b/packages/uipath-core/src/uipath/core/adapters/evaluator.py new file mode 100644 index 000000000..ee5b92dad --- /dev/null +++ b/packages/uipath-core/src/uipath/core/adapters/evaluator.py @@ -0,0 +1,99 @@ +"""Structural contract for the policy evaluator an adapter talks to. + +Framework adapters call into a policy evaluator at each lifecycle hook. +Concrete evaluator implementations (the native runtime evaluator, a +Microsoft AGT bridge, a composite, …) live in packages outside +``uipath-core`` — adapters depend only on this structural protocol so +they can be swapped against any of them without code change. + +``EvaluatorProtocol`` is a :class:`typing.Protocol` so any class whose +methods match the signatures below satisfies the contract without +inheritance. +""" + +from __future__ import annotations + +from typing import Any, Protocol, runtime_checkable + + +@runtime_checkable +class EvaluatorProtocol(Protocol): + """Structural protocol an adapter expects from a policy evaluator. + + Return types are intentionally :class:`typing.Any`: the concrete + audit record shape lives in the plugin package that owns the + evaluator and the policy model. Adapters in that package cast the + return value back to the concrete type they know. + """ + + def evaluate_before_agent( + self, + agent_input: str, + agent_name: str, + runtime_id: str, + trace_id: str, + model_name: str = "", + **kwargs: Any, + ) -> Any: + """Evaluate BEFORE_AGENT rules.""" + ... + + def evaluate_after_agent( + self, + agent_output: str, + agent_name: str, + runtime_id: str, + trace_id: str, + **kwargs: Any, + ) -> Any: + """Evaluate AFTER_AGENT rules.""" + ... + + def evaluate_before_model( + self, + model_input: str, + agent_name: str, + runtime_id: str, + trace_id: str, + messages: list[dict[str, Any]] | None = None, + model_name: str = "", + **kwargs: Any, + ) -> Any: + """Evaluate BEFORE_MODEL rules.""" + ... + + def evaluate_after_model( + self, + model_output: str, + agent_name: str, + runtime_id: str, + trace_id: str, + **kwargs: Any, + ) -> Any: + """Evaluate AFTER_MODEL rules.""" + ... + + def evaluate_tool_call( + self, + tool_name: str, + tool_args: dict[str, Any], + agent_name: str, + runtime_id: str, + trace_id: str, + session_state: dict[str, Any] | None = None, + **kwargs: Any, + ) -> Any: + """Evaluate TOOL_CALL rules.""" + ... + + def evaluate_after_tool( + self, + tool_name: str, + tool_result: str, + agent_name: str, + runtime_id: str, + trace_id: str, + **kwargs: Any, + ) -> Any: + """Evaluate AFTER_TOOL rules.""" + ... diff --git a/packages/uipath-core/src/uipath/core/adapters/registry.py b/packages/uipath-core/src/uipath/core/adapters/registry.py new file mode 100644 index 000000000..adebe780a --- /dev/null +++ b/packages/uipath-core/src/uipath/core/adapters/registry.py @@ -0,0 +1,176 @@ +"""Ordered registry of framework adapters. + +The registry is a pure, implementation-agnostic container — it does +**not** know about any concrete adapter. Plugin packages (e.g. +``uipath-langchain``) populate it by either: + +1. Declaring a ``uipath.governance.adapters`` entry point whose value + is a zero-arg callable that calls :meth:`AdapterRegistry.register`. + These are auto-discovered on first call to + :func:`get_adapter_registry`. +2. Calling :meth:`AdapterRegistry.register` directly at import time + (e.g. side-effect on importing the plugin's governance submodule). + +Adapters are checked in priority order (highest first): more specific +adapters get a higher :attr:`BaseAdapter.priority` so they win +``can_handle`` resolution over generic ones, regardless of the order in +which plugin packages happen to be imported. Among adapters with the +same priority, registration order is preserved. Adapters with +``is_fallback=True`` sort last when registered without an explicit +``position`` — passing ``position`` to :meth:`AdapterRegistry.register` +is an escape hatch that bypasses both priority and fallback ordering, +so callers using it own the resulting list order. +""" + +from __future__ import annotations + +import logging +from typing import Any + +from .base import BaseAdapter + +logger = logging.getLogger(__name__) + +ENTRY_POINT_GROUP = "uipath.governance.adapters" + + +class AdapterRegistry: + """Ordered list of adapters; resolves the first match for an agent.""" + + def __init__(self) -> None: + """Initialize an empty registry.""" + self._adapters: list[BaseAdapter] = [] + + def register(self, adapter: BaseAdapter, position: int | None = None) -> None: + """Register an adapter. + + Args: + adapter: The adapter to register. + position: Explicit insertion index (``0`` = highest priority) + that bypasses both priority-based ordering AND fallback + semantics — the adapter is inserted blindly at the given + index, so callers using ``position`` are responsible for + not placing a fallback before a specific adapter (or a + specific adapter after an existing fallback). When + ``None`` the adapter is inserted by + :attr:`BaseAdapter.priority` (higher first, stable on + ties) and before any adapter marked + :attr:`BaseAdapter.is_fallback`; adapters whose own + ``is_fallback`` is set are appended last. + """ + if position is not None: + self._adapters.insert(position, adapter) + elif adapter.is_fallback: + self._adapters.append(adapter) + else: + insert_at = len(self._adapters) + for i, existing in enumerate(self._adapters): + if existing.is_fallback or existing.priority < adapter.priority: + insert_at = i + break + self._adapters.insert(insert_at, adapter) + logger.debug("Registered adapter: %s", adapter.name) + + def resolve(self, agent: Any) -> BaseAdapter | None: + """Return the first adapter that can handle ``agent`` (or ``None``).""" + for adapter in self._adapters: + try: + if adapter.can_handle(agent): + logger.debug( + "AdapterRegistry: %s -> %s", + type(agent).__name__, + adapter.name, + ) + return adapter + except Exception as exc: + logger.warning( + "Adapter %s.can_handle() failed: %s", + adapter.name, + exc, + ) + continue + return None + + def get_all(self) -> list[BaseAdapter]: + """Return a copy of the registered adapters in priority order.""" + return self._adapters.copy() + + def clear(self) -> None: + """Remove all registered adapters.""" + self._adapters.clear() + + +_registry: AdapterRegistry | None = None + + +def _discover_entry_point_adapters() -> None: + """Load every adapter advertised under the ``uipath.governance.adapters`` group. + + Each entry-point value must be a zero-arg callable (typically a + ``register_*`` function in the plugin package) that calls + :meth:`AdapterRegistry.register`. A failure to load or invoke any + one entry point is logged and skipped — a single broken plugin + must never block governance startup. + """ + try: + from importlib.metadata import entry_points + except ImportError: # pragma: no cover - importlib.metadata is stdlib in py3.11+ + return + + try: + eps = entry_points(group=ENTRY_POINT_GROUP) + except Exception as exc: # noqa: BLE001 - discovery failures must never raise + logger.debug("Adapter entry-point discovery failed: %s", exc, exc_info=True) + return + + for ep in eps: + try: + registrar = ep.load() + except Exception as exc: # noqa: BLE001 - one broken plugin must not block others + logger.debug( + "Failed to load governance adapter entry point '%s' (%s): %s", + ep.name, + ep.value, + exc, + exc_info=True, + ) + continue + if not callable(registrar): + logger.warning( + "Governance adapter entry point '%s' is not callable: %r", + ep.name, + registrar, + ) + continue + try: + registrar() + except Exception as exc: # noqa: BLE001 - one broken plugin must not block others + logger.debug( + "Governance adapter '%s' register call failed: %s", + ep.name, + exc, + exc_info=True, + ) + + +def get_adapter_registry() -> AdapterRegistry: + """Return the process-wide adapter registry singleton. + + On first call, discovers and registers every adapter declared under + the ``uipath.governance.adapters`` entry-point group, so framework + SDKs (``uipath-langchain``, ``uipath-openai``, …) just need to be + installed — no explicit import is required. + """ + global _registry + if _registry is None: + _registry = AdapterRegistry() + _discover_entry_point_adapters() + return _registry + + +def reset_adapter_registry() -> None: + """Drop the singleton registry (intended for tests).""" + global _registry + if _registry is not None: + _registry.clear() + _registry = None diff --git a/packages/uipath-core/src/uipath/core/governance/__init__.py b/packages/uipath-core/src/uipath/core/governance/__init__.py new file mode 100644 index 000000000..dd32228ed --- /dev/null +++ b/packages/uipath-core/src/uipath/core/governance/__init__.py @@ -0,0 +1,37 @@ +"""UiPath governance shared contracts. + +Evaluator-agnostic types every governance consumer references — +adapter packages (``uipath-langchain``, ``uipath-openai``, …), the +runtime layer (``uipath.runtime.governance``), and customer code that +catches :class:`GovernanceBlockException`. The full runtime / audit / +native-evaluator implementation lives in ``uipath.runtime.governance``; +this core surface is just the contracts. +""" + +from .config import ( + GOVERNANCE_FEATURE_FLAG, + is_governance_enabled, +) +from .exceptions import ( + GovernanceBlockException, + GovernanceConfigError, + GovernanceViolation, + Severity, +) +from .models import Action, AuditRecord, LifecycleHook, RuleEvaluation + +__all__ = [ + # Output models (cross adapter boundary) + "Action", + "AuditRecord", + "LifecycleHook", + "RuleEvaluation", + # Config + "GOVERNANCE_FEATURE_FLAG", + "is_governance_enabled", + # Exceptions + "GovernanceBlockException", + "GovernanceConfigError", + "GovernanceViolation", + "Severity", +] diff --git a/packages/uipath-core/src/uipath/core/governance/config.py b/packages/uipath-core/src/uipath/core/governance/config.py new file mode 100644 index 000000000..0bb0a6042 --- /dev/null +++ b/packages/uipath-core/src/uipath/core/governance/config.py @@ -0,0 +1,34 @@ +"""Governance configuration. + +Process-level feature-flag gate that decides whether the Python +governance checker runs at all. Enforcement mode is per-policy and +lives in the runtime package alongside the ``/runtime/policy`` client. +""" + +from __future__ import annotations + +from uipath.core.feature_flags import FeatureFlags + +# Feature flag name controlling whether governance runs. +# Mirrors the gate in ``uipath-runtime`` so the platform-injection path +# and direct callers (agents constructing an evaluator themselves) +# honour the same toggle. +GOVERNANCE_FEATURE_FLAG = "EnablePythonGovernanceChecker" + + +def is_governance_enabled() -> bool: + """Return whether the ``EnablePythonGovernanceChecker`` flag is enabled. + + Governance is **off by default** — the flag must be explicitly set + to ``true`` (programmatically via the ``FeatureFlags`` registry, or + via the ``UIPATH_FEATURE_EnablePythonGovernanceChecker`` env var) + for this function to return ``True``. + + Resolution order: + + 1. :meth:`uipath.core.feature_flags.FeatureFlagsManager.is_flag_enabled` - + the in-process programmatic registry (typically populated from + gitops) and its own ``UIPATH_FEATURE_`` env-var fallback. + 2. Default ``False`` (governance disabled). + """ + return FeatureFlags.is_flag_enabled(GOVERNANCE_FEATURE_FLAG, default=False) diff --git a/packages/uipath-core/src/uipath/core/governance/exceptions.py b/packages/uipath-core/src/uipath/core/governance/exceptions.py new file mode 100644 index 000000000..48f4b178a --- /dev/null +++ b/packages/uipath-core/src/uipath/core/governance/exceptions.py @@ -0,0 +1,114 @@ +"""Governance exception types.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + +from uipath.core.governance.models import AuditRecord + +_DEFAULT_RULE_ID = "POLICY" +_DEFAULT_RULE_NAME = "Governance Policy" +_MSG_PREFIX = "[Governance Policy Violation]" + + +class Severity(str, Enum): + """Severity classification for a governance violation.""" + + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + + +@dataclass +class GovernanceViolation: + """Details of a governance rule violation.""" + + rule_id: str + rule_name: str + detail: str + severity: Severity = Severity.HIGH + + +def _format_violation_message(rule_id: str, rule_name: str, detail: str) -> str: + return f"{_MSG_PREFIX} {rule_name} ({rule_id}): {detail}" + + +class GovernanceBlockException(Exception): + """Raised when a governance policy blocks an operation. + + This exception indicates that the AI agent's operation was blocked by + a configured governance policy, not an unexpected system error. + + Prefer the classmethod constructors (:meth:`from_violation`, + :meth:`from_audit_record`) when you have structured context — the + default constructor is for raw-message use only. + """ + + # Error code for Orchestrator categorization + error_code: str = "GOVERNANCE_POLICY_VIOLATION" + + def __init__( + self, + message: str | None = None, + *, + violation: GovernanceViolation | None = None, + audit_record: AuditRecord | None = None, + rule_id: str = _DEFAULT_RULE_ID, + rule_name: str = _DEFAULT_RULE_NAME, + ) -> None: + """Construct from a pre-formatted message and optional structured context. + + Most callers should use :meth:`from_violation` or + :meth:`from_audit_record` instead of passing structured context + directly. + """ + self.violation = violation + self.audit_record = audit_record + self.rule_id = rule_id + self.rule_name = rule_name + super().__init__( + message or f"{_MSG_PREFIX} Operation blocked by governance policy." + ) + + @classmethod + def from_violation( + cls, violation: GovernanceViolation + ) -> "GovernanceBlockException": + """Build from a structured :class:`GovernanceViolation`.""" + return cls( + message=_format_violation_message( + violation.rule_id, violation.rule_name, violation.detail + ), + violation=violation, + rule_id=violation.rule_id, + rule_name=violation.rule_name, + ) + + @classmethod + def from_audit_record(cls, audit_record: AuditRecord) -> "GovernanceBlockException": + """Build from an :class:`AuditRecord` — first matched rule wins.""" + matched_rules = [e for e in audit_record.evaluations if e.matched] + if matched_rules: + rule = matched_rules[0] + message = _format_violation_message( + rule.rule_id, rule.rule_name, rule.detail or "Policy violation detected" + ) + return cls( + message=message, + audit_record=audit_record, + rule_id=rule.rule_id, + rule_name=rule.rule_name, + ) + return cls( + message=( + f"{_MSG_PREFIX} Operation blocked. " + f"Rules evaluated: {len(audit_record.evaluations)}" + ), + audit_record=audit_record, + ) + + +class GovernanceConfigError(RuntimeError): + """Raised when governance is misconfigured.""" diff --git a/packages/uipath-core/src/uipath/core/governance/models.py b/packages/uipath-core/src/uipath/core/governance/models.py new file mode 100644 index 000000000..6b3993639 --- /dev/null +++ b/packages/uipath-core/src/uipath/core/governance/models.py @@ -0,0 +1,68 @@ +"""Shared governance output types. + +These dataclasses cross the adapter boundary — every evaluator +implementation (native, AGT, composite, …) produces them, and every +adapter consumes them. They are kept free of policy-input concepts +(``Rule``/``Check``/``Condition``) so the adapter packages don't +inherit the native policy model. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from typing import Any + + +class Action(str, Enum): + """Actions that can be taken when a rule matches.""" + + ALLOW = "allow" + DENY = "deny" + AUDIT = "audit" + ESCALATE = "escalate" + + +class LifecycleHook(str, Enum): + """Agent lifecycle hooks where rules can be evaluated.""" + + BEFORE_AGENT = "before_agent" + AFTER_AGENT = "after_agent" + BEFORE_MODEL = "before_model" + AFTER_MODEL = "after_model" + TOOL_CALL = "tool_call" + AFTER_TOOL = "after_tool" + + +@dataclass +class RuleEvaluation: + """Result of evaluating a single rule.""" + + rule_id: str + rule_name: str + matched: bool + detail: str = "" + pack_name: str = "" + action: Action = Action.ALLOW + description: str = "" + check_results: list[dict[str, Any]] = field(default_factory=list) + + +@dataclass +class AuditRecord: + """Complete audit record for a governance evaluation.""" + + timestamp: datetime + agent_name: str + runtime_id: str + trace_id: str + hook: LifecycleHook + evaluations: list[RuleEvaluation] + final_action: Action + metadata: dict[str, Any] = field(default_factory=dict) + rules_matched: int = field(init=False) + + def __post_init__(self) -> None: + """Derive rules_matched from the evaluations list.""" + self.rules_matched = sum(1 for e in self.evaluations if e.matched) diff --git a/packages/uipath-core/tests/adapters/__init__.py b/packages/uipath-core/tests/adapters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/uipath-core/tests/adapters/test_base.py b/packages/uipath-core/tests/adapters/test_base.py new file mode 100644 index 000000000..9be6346ed --- /dev/null +++ b/packages/uipath-core/tests/adapters/test_base.py @@ -0,0 +1,163 @@ +"""Tests for BaseAdapter defaults and GovernedAgentBase proxy behavior.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from uipath.core.adapters import BaseAdapter, EvaluatorProtocol +from uipath.core.adapters.base import GovernedAgentBase + + +class _StubEvaluator: + """No-op evaluator that structurally matches EvaluatorProtocol.""" + + def evaluate_before_agent(self, *args: Any, **kwargs: Any) -> Any: + return None + + def evaluate_after_agent(self, *args: Any, **kwargs: Any) -> Any: + return None + + def evaluate_before_model(self, *args: Any, **kwargs: Any) -> Any: + return None + + def evaluate_after_model(self, *args: Any, **kwargs: Any) -> Any: + return None + + def evaluate_tool_call(self, *args: Any, **kwargs: Any) -> Any: + return None + + def evaluate_after_tool(self, *args: Any, **kwargs: Any) -> Any: + return None + + +class _MinimalAdapter(BaseAdapter): + """Concrete adapter that does NOT override ``name`` — exercises the default.""" + + def can_handle(self, agent: Any) -> bool: + return True + + def attach( + self, + agent: Any, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> Any: + return agent + + +class _Agent: + """Simple stand-in for a framework agent with one attribute and one method.""" + + foo = "bar" + + def greet(self) -> str: + return "hello" + + +# --------------------------------------------------------------------------- +# BaseAdapter defaults +# --------------------------------------------------------------------------- + + +def test_default_name_is_class_name(): + """The default ``name`` property returns the class name.""" + assert _MinimalAdapter().name == "_MinimalAdapter" + + +def test_detach_returns_unwrapped_when_present(): + """``detach`` honours the ``unwrapped`` contract on a governed proxy.""" + adapter = _MinimalAdapter() + original = object() + + class _Proxy: + unwrapped = original + + assert adapter.detach(_Proxy()) is original + + +def test_detach_returns_input_when_no_unwrapped_attribute(): + """For non-proxy adapters, ``detach`` returns the input unchanged.""" + adapter = _MinimalAdapter() + raw = object() + assert adapter.detach(raw) is raw + + +def test_generate_trace_id_returns_unique_uuid_string(): + """``_generate_trace_id`` returns a string UUID; consecutive calls differ.""" + adapter = _MinimalAdapter() + a = adapter._generate_trace_id() + b = adapter._generate_trace_id() + assert isinstance(a, str) + assert a != b + assert len(a) == 36 # canonical UUID4 form: 32 hex + 4 dashes + + +# --------------------------------------------------------------------------- +# GovernedAgentBase proxy +# --------------------------------------------------------------------------- + + +def test_governed_agent_base_stores_metadata_and_generates_trace_id(): + """Constructor wires every governance field and pulls a trace id from the adapter.""" + agent = _Agent() + adapter = _MinimalAdapter() + evaluator = _StubEvaluator() + + governed = GovernedAgentBase( + agent=agent, + adapter=adapter, + agent_id="agent-123", + session_id="session-abc", + evaluator=evaluator, + ) + + assert governed._agent is agent + assert governed._adapter is adapter + assert governed._agent_id == "agent-123" + assert governed._session_id == "session-abc" + assert governed._evaluator is evaluator + assert isinstance(governed._trace_id, str) + assert len(governed._trace_id) == 36 + + +def test_governed_agent_base_unwrapped_returns_original_agent(): + agent = _Agent() + governed = GovernedAgentBase( + agent=agent, + adapter=_MinimalAdapter(), + agent_id="a", + session_id="s", + evaluator=_StubEvaluator(), + ) + assert governed.unwrapped is agent + + +def test_governed_agent_base_forwards_attribute_access_to_agent(): + """Unknown attributes fall through to the wrapped agent via __getattr__.""" + governed = GovernedAgentBase( + agent=_Agent(), + adapter=_MinimalAdapter(), + agent_id="a", + session_id="s", + evaluator=_StubEvaluator(), + ) + + assert governed.foo == "bar" + assert governed.greet() == "hello" + + +def test_governed_agent_base_attribute_miss_raises_attribute_error(): + """If the wrapped agent also lacks the attribute, AttributeError surfaces.""" + governed = GovernedAgentBase( + agent=_Agent(), + adapter=_MinimalAdapter(), + agent_id="a", + session_id="s", + evaluator=_StubEvaluator(), + ) + + with pytest.raises(AttributeError): + _ = governed.does_not_exist diff --git a/packages/uipath-core/tests/adapters/test_evaluator.py b/packages/uipath-core/tests/adapters/test_evaluator.py new file mode 100644 index 000000000..5c9e5c9e5 --- /dev/null +++ b/packages/uipath-core/tests/adapters/test_evaluator.py @@ -0,0 +1,104 @@ +"""Tests for EvaluatorProtocol. + +The protocol is a structural type. These tests verify two things: + +1. A class whose method shapes match the protocol passes ``isinstance`` + against the ``runtime_checkable`` Protocol. +2. Subclassing the Protocol and calling ``super().`` actually + executes the stub bodies — this both documents that the stubs are + safely callable (they return ``None``) and brings the contract module + to full line coverage. +""" + +from __future__ import annotations + +from typing import Any + +from uipath.core.adapters import EvaluatorProtocol + + +class _MissingMethodEvaluator: + """Only implements one method — fails the structural check.""" + + def evaluate_before_agent(self, *args: Any, **kwargs: Any) -> Any: + return None + + +class _CompleteEvaluator: + """All six methods present with the expected names — passes ``isinstance``.""" + + def evaluate_before_agent(self, *args: Any, **kwargs: Any) -> Any: + return "before-agent" + + def evaluate_after_agent(self, *args: Any, **kwargs: Any) -> Any: + return "after-agent" + + def evaluate_before_model(self, *args: Any, **kwargs: Any) -> Any: + return "before-model" + + def evaluate_after_model(self, *args: Any, **kwargs: Any) -> Any: + return "after-model" + + def evaluate_tool_call(self, *args: Any, **kwargs: Any) -> Any: + return "tool-call" + + def evaluate_after_tool(self, *args: Any, **kwargs: Any) -> Any: + return "after-tool" + + +class _ProtocolSubclass(EvaluatorProtocol): + """Subclass that delegates to ``super()`` — exercises the stub bodies. + + Each override calls ``super().(...)`` so the ``...`` body of + the Protocol method actually executes (returns ``None``). + """ + + def evaluate_before_agent(self, *args: Any, **kwargs: Any) -> Any: + return super().evaluate_before_agent(*args, **kwargs) # type: ignore[safe-super] + + def evaluate_after_agent(self, *args: Any, **kwargs: Any) -> Any: + return super().evaluate_after_agent(*args, **kwargs) # type: ignore[safe-super] + + def evaluate_before_model(self, *args: Any, **kwargs: Any) -> Any: + return super().evaluate_before_model(*args, **kwargs) # type: ignore[safe-super] + + def evaluate_after_model(self, *args: Any, **kwargs: Any) -> Any: + return super().evaluate_after_model(*args, **kwargs) # type: ignore[safe-super] + + def evaluate_tool_call(self, *args: Any, **kwargs: Any) -> Any: + return super().evaluate_tool_call(*args, **kwargs) # type: ignore[safe-super] + + def evaluate_after_tool(self, *args: Any, **kwargs: Any) -> Any: + return super().evaluate_after_tool(*args, **kwargs) # type: ignore[safe-super] + + +# --------------------------------------------------------------------------- +# Structural conformance +# --------------------------------------------------------------------------- + + +def test_complete_evaluator_is_recognized_by_runtime_check(): + """A class with all six methods passes ``isinstance`` against the protocol.""" + assert isinstance(_CompleteEvaluator(), EvaluatorProtocol) + + +def test_partial_evaluator_is_rejected_by_runtime_check(): + """A class missing methods does NOT pass the structural check.""" + assert not isinstance(_MissingMethodEvaluator(), EvaluatorProtocol) + + +# --------------------------------------------------------------------------- +# Stub-body execution (line coverage for the ``...`` placeholders) +# --------------------------------------------------------------------------- + + +def test_protocol_subclass_methods_execute_stub_bodies(): + """Calling each method via ``super()`` executes the stub body and returns None.""" + e = _ProtocolSubclass() + + assert e.evaluate_before_agent("input", "agent", "rt", "trace") is None + assert e.evaluate_after_agent("output", "agent", "rt", "trace") is None + assert e.evaluate_before_model("input", "agent", "rt", "trace") is None + assert e.evaluate_after_model("output", "agent", "rt", "trace") is None + assert e.evaluate_tool_call("tool", {"arg": 1}, "agent", "rt", "trace") is None + assert e.evaluate_after_tool("tool", "result", "agent", "rt", "trace") is None diff --git a/packages/uipath-core/tests/adapters/test_registry.py b/packages/uipath-core/tests/adapters/test_registry.py new file mode 100644 index 000000000..b16b29b1e --- /dev/null +++ b/packages/uipath-core/tests/adapters/test_registry.py @@ -0,0 +1,492 @@ +"""Tests for AdapterRegistry — ordering, resolution, entry-point discovery.""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from uipath.core.adapters import BaseAdapter, EvaluatorProtocol +from uipath.core.adapters.registry import ( + AdapterRegistry, + _discover_entry_point_adapters, + get_adapter_registry, + reset_adapter_registry, +) + +# --------------------------------------------------------------------------- +# Test adapters +# --------------------------------------------------------------------------- + + +class _SpecificAdapter(BaseAdapter): + """Matches only objects with a ``__specific__`` marker.""" + + @property + def name(self) -> str: + return "specific" + + def can_handle(self, agent: Any) -> bool: + return hasattr(agent, "__specific__") + + def attach( + self, + agent: Any, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> Any: + return agent + + +class _FallbackAdapter(BaseAdapter): + """Matches anything — must always sort last.""" + + is_fallback = True + + @property + def name(self) -> str: + return "fallback" + + def can_handle(self, agent: Any) -> bool: + return True + + def attach( + self, + agent: Any, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> Any: + return agent + + +class _SecondaryAdapter(BaseAdapter): + """Another specific adapter, used to test ordering between two specifics.""" + + @property + def name(self) -> str: + return "secondary" + + def can_handle(self, agent: Any) -> bool: + return hasattr(agent, "__secondary__") + + def attach( + self, + agent: Any, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> Any: + return agent + + +class _HighPriorityAdapter(BaseAdapter): + """Specific adapter with an elevated priority.""" + + priority = 100 + + @property + def name(self) -> str: + return "high" + + def can_handle(self, agent: Any) -> bool: + return True + + def attach( + self, + agent: Any, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> Any: + return agent + + +class _LowPriorityAdapter(BaseAdapter): + """Generic adapter that should yield to higher-priority specifics.""" + + priority = -10 + + @property + def name(self) -> str: + return "low" + + def can_handle(self, agent: Any) -> bool: + return True + + def attach( + self, + agent: Any, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> Any: + return agent + + +class _BrokenAdapter(BaseAdapter): + """``can_handle`` raises — must be skipped, not crash resolution.""" + + @property + def name(self) -> str: + return "broken" + + def can_handle(self, agent: Any) -> bool: + raise RuntimeError("can_handle exploded") + + def attach( + self, + agent: Any, + agent_id: str, + session_id: str, + evaluator: EvaluatorProtocol, + ) -> Any: + raise RuntimeError("attach exploded") + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def _isolate_global_registry(): + """Each test starts with no singleton registry.""" + reset_adapter_registry() + yield + reset_adapter_registry() + + +# --------------------------------------------------------------------------- +# register / resolve / get_all / clear +# --------------------------------------------------------------------------- + + +def test_empty_registry_resolves_to_none(): + reg = AdapterRegistry() + assert reg.resolve(object()) is None + assert reg.get_all() == [] + + +def test_register_appends_in_order(): + reg = AdapterRegistry() + a, b = _SpecificAdapter(), _SecondaryAdapter() + reg.register(a) + reg.register(b) + assert reg.get_all() == [a, b] + + +def test_resolve_returns_first_matching_adapter(): + reg = AdapterRegistry() + reg.register(_SpecificAdapter()) + reg.register(_SecondaryAdapter()) + + agent = MagicMock() + agent.__secondary__ = True # only secondary should match + resolved = reg.resolve(agent) + assert resolved is not None + assert resolved.name == "secondary" + + +def test_resolve_skips_broken_can_handle_and_continues(): + """A can_handle() that raises must not break the whole resolve loop.""" + reg = AdapterRegistry() + reg.register(_BrokenAdapter()) + reg.register(_SpecificAdapter()) + + agent = MagicMock() + agent.__specific__ = True + resolved = reg.resolve(agent) + assert resolved is not None + assert resolved.name == "specific" + + +def test_register_position_inserts_at_index(): + reg = AdapterRegistry() + a, b, c = _SpecificAdapter(), _SecondaryAdapter(), _SpecificAdapter() + reg.register(a) + reg.register(b) + reg.register(c, position=0) # c jumps to head + assert reg.get_all()[0] is c + assert reg.get_all()[1:] == [a, b] + + +def test_higher_priority_adapter_inserted_before_lower_priority(): + """A specific (higher-priority) adapter must sort before a generic one + even when the generic one was registered first.""" + reg = AdapterRegistry() + generic = _LowPriorityAdapter() + specific = _HighPriorityAdapter() + reg.register(generic) + reg.register(specific) # registered later, but higher priority + + adapters = reg.get_all() + assert adapters[0] is specific + assert adapters[1] is generic + + +def test_same_priority_preserves_registration_order(): + """Adapters with equal priority should fall back to insertion order.""" + reg = AdapterRegistry() + a, b = _SpecificAdapter(), _SecondaryAdapter() # both priority=0 + reg.register(a) + reg.register(b) + assert reg.get_all() == [a, b] + + +def test_higher_priority_adapter_inserted_before_fallback(): + """High-priority adapter goes in front of an already-registered fallback.""" + reg = AdapterRegistry() + fallback = _FallbackAdapter() + reg.register(fallback) + reg.register(_HighPriorityAdapter()) + + adapters = reg.get_all() + assert adapters[0].name == "high" + assert adapters[-1] is fallback + + +def test_lower_priority_adapter_inserted_before_fallback_after_specifics(): + """Negative-priority adapter sorts after default-priority specifics but + still before the fallback.""" + reg = AdapterRegistry() + reg.register(_SpecificAdapter()) # priority=0 + reg.register(_FallbackAdapter()) + reg.register(_LowPriorityAdapter()) # priority=-10 + + adapters = reg.get_all() + assert adapters[0].name == "specific" + assert adapters[1].name == "low" + assert adapters[-1].name == "fallback" + + +def test_priority_overrides_registration_order_in_resolve(): + """Resolution must follow priority ordering, not registration order.""" + reg = AdapterRegistry() + reg.register(_LowPriorityAdapter()) # both adapters match every agent, + reg.register(_HighPriorityAdapter()) # so priority decides which wins. + + resolved = reg.resolve(object()) + assert resolved is not None + assert resolved.name == "high" + + +def test_fallback_stays_last_when_new_adapter_registered(): + """When the last entry has ``is_fallback`` set, new adapters insert before it.""" + reg = AdapterRegistry() + fallback = _FallbackAdapter() + reg.register(fallback) + reg.register(_SpecificAdapter()) # this should insert BEFORE fallback + + adapters = reg.get_all() + assert adapters[-1] is fallback + assert adapters[0].name == "specific" + + +def test_fallback_resolves_only_when_no_specific_matches(): + reg = AdapterRegistry() + reg.register(_SpecificAdapter()) + reg.register(_FallbackAdapter()) + + # Agent without the __specific__ marker → fallback wins. + resolved = reg.resolve(object()) + assert resolved is not None + assert resolved.name == "fallback" + + +def test_clear_removes_all_adapters(): + reg = AdapterRegistry() + reg.register(_SpecificAdapter()) + reg.register(_SecondaryAdapter()) + reg.clear() + assert reg.get_all() == [] + assert reg.resolve(object()) is None + + +def test_get_all_returns_copy_not_internal_list(): + """Callers must not be able to mutate the registry through get_all().""" + reg = AdapterRegistry() + reg.register(_SpecificAdapter()) + snapshot = reg.get_all() + snapshot.clear() + assert len(reg.get_all()) == 1 # unaffected + + +# --------------------------------------------------------------------------- +# Singleton + entry-point discovery +# --------------------------------------------------------------------------- + + +def test_get_adapter_registry_returns_singleton(): + reg1 = get_adapter_registry() + reg2 = get_adapter_registry() + assert reg1 is reg2 + + +def test_reset_adapter_registry_drops_singleton(): + first = get_adapter_registry() + reset_adapter_registry() + second = get_adapter_registry() + assert first is not second + + +def test_entry_point_discovery_invokes_registrars(monkeypatch): + """Each entry-point's zero-arg callable must be loaded and called.""" + called: list[str] = [] + + def make_registrar(name: str): + def _register() -> None: + called.append(name) + + return _register + + ep_a = MagicMock() + ep_a.name = "a" + ep_a.value = "pkg_a:register" + ep_a.load.return_value = make_registrar("a") + + ep_b = MagicMock() + ep_b.name = "b" + ep_b.value = "pkg_b:register" + ep_b.load.return_value = make_registrar("b") + + monkeypatch.setattr( + "uipath.core.adapters.registry.entry_points", + lambda group: [ep_a, ep_b] if group == "uipath.governance.adapters" else [], + raising=False, + ) + + # entry_points lives in importlib.metadata; the registry imports it + # lazily inside the function. Patch the import target directly. + import importlib.metadata as importlib_metadata + + monkeypatch.setattr( + importlib_metadata, + "entry_points", + lambda group=None: ( + [ep_a, ep_b] if group == "uipath.governance.adapters" else [] + ), + ) + + _discover_entry_point_adapters() + assert sorted(called) == ["a", "b"] + + +def test_entry_point_discovery_skips_broken_loader(monkeypatch): + """One broken entry-point must not stop the others from registering.""" + called: list[str] = [] + + ep_broken = MagicMock() + ep_broken.name = "broken" + ep_broken.value = "pkg_broken:register" + ep_broken.load.side_effect = ImportError("cannot import") + + ep_ok = MagicMock() + ep_ok.name = "ok" + ep_ok.value = "pkg_ok:register" + ep_ok.load.return_value = lambda: called.append("ok") + + import importlib.metadata as importlib_metadata + + monkeypatch.setattr( + importlib_metadata, + "entry_points", + lambda group=None: ( + [ep_broken, ep_ok] if group == "uipath.governance.adapters" else [] + ), + ) + + _discover_entry_point_adapters() # must not raise + assert called == ["ok"] + + +def test_entry_point_discovery_skips_non_callable(monkeypatch): + """An entry-point that resolves to a non-callable must be logged and skipped.""" + called: list[str] = [] + + ep_bad = MagicMock() + ep_bad.name = "bad" + ep_bad.value = "pkg_bad:NOT_A_FUNCTION" + ep_bad.load.return_value = "not callable" + + ep_ok = MagicMock() + ep_ok.name = "ok" + ep_ok.value = "pkg_ok:register" + ep_ok.load.return_value = lambda: called.append("ok") + + import importlib.metadata as importlib_metadata + + monkeypatch.setattr( + importlib_metadata, + "entry_points", + lambda group=None: ( + [ep_bad, ep_ok] if group == "uipath.governance.adapters" else [] + ), + ) + + _discover_entry_point_adapters() + assert called == ["ok"] + + +def test_entry_point_discovery_swallows_registrar_exception(monkeypatch): + """A registrar that raises mid-call must not stop subsequent registrars.""" + called: list[str] = [] + + def _raises() -> None: + raise RuntimeError("registrar exploded") + + ep_raising = MagicMock() + ep_raising.name = "raises" + ep_raising.value = "pkg:register" + ep_raising.load.return_value = _raises + + ep_ok = MagicMock() + ep_ok.name = "ok" + ep_ok.value = "pkg:register2" + ep_ok.load.return_value = lambda: called.append("ok") + + import importlib.metadata as importlib_metadata + + monkeypatch.setattr( + importlib_metadata, + "entry_points", + lambda group=None: ( + [ep_raising, ep_ok] if group == "uipath.governance.adapters" else [] + ), + ) + + _discover_entry_point_adapters() + assert called == ["ok"] + + +def test_entry_point_discovery_swallows_entry_points_failure(monkeypatch): + """If ``entry_points()`` itself raises, discovery must log and return cleanly.""" + import importlib.metadata as importlib_metadata + + def _boom(group=None): + raise RuntimeError("entry_points API exploded") + + monkeypatch.setattr(importlib_metadata, "entry_points", _boom) + + # Must not raise — and must not register anything. + _discover_entry_point_adapters() + reg = get_adapter_registry() + assert reg.get_all() == [] + + +# --------------------------------------------------------------------------- +# Protocol conformance smoke tests +# --------------------------------------------------------------------------- + + +def test_baseadapter_is_abc(): + """BaseAdapter must be abstract — direct instantiation must fail.""" + with pytest.raises(TypeError): + BaseAdapter() # type: ignore[abstract] + + +def test_concrete_adapter_is_baseadapter(): + """A concrete subclass must be recognized as a BaseAdapter.""" + assert isinstance(_SpecificAdapter(), BaseAdapter) diff --git a/packages/uipath-core/tests/governance/__init__.py b/packages/uipath-core/tests/governance/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/uipath-core/tests/governance/test_config.py b/packages/uipath-core/tests/governance/test_config.py new file mode 100644 index 000000000..54642a413 --- /dev/null +++ b/packages/uipath-core/tests/governance/test_config.py @@ -0,0 +1,54 @@ +"""Tests for the governance feature-flag gate.""" + +from __future__ import annotations + +import pytest + +from uipath.core.feature_flags import FeatureFlags +from uipath.core.governance.config import ( + GOVERNANCE_FEATURE_FLAG, + is_governance_enabled, +) + + +@pytest.fixture(autouse=True) +def _reset_flags(): + """Each test starts and ends with a clean flags registry.""" + FeatureFlags.reset_flags() + yield + FeatureFlags.reset_flags() + + +def test_governance_flag_name_is_stable(): + """The flag name is a public contract shared with the runtime layer.""" + assert GOVERNANCE_FEATURE_FLAG == "EnablePythonGovernanceChecker" + + +def test_is_governance_enabled_defaults_to_false(): + """With nothing configured, the gate defaults to disabled. + + The platform / host runtime must explicitly opt into governance + (programmatically via :class:`FeatureFlags`, via gitops, or via the + ``UIPATH_FEATURE_EnablePythonGovernanceChecker`` env var). This + keeps the SDK safe-by-default for callers that haven't yet + integrated with the governance backend. + """ + assert is_governance_enabled() is False + + +def test_is_governance_enabled_respects_programmatic_disable(): + """Programmatic ``False`` flips the gate off.""" + FeatureFlags.configure_flags({GOVERNANCE_FEATURE_FLAG: False}) + assert is_governance_enabled() is False + + +def test_is_governance_enabled_respects_programmatic_enable(): + """Programmatic ``True`` keeps the gate on.""" + FeatureFlags.configure_flags({GOVERNANCE_FEATURE_FLAG: True}) + assert is_governance_enabled() is True + + +def test_is_governance_enabled_reads_env_var_fallback(monkeypatch): + """When nothing is configured programmatically, the env-var fallback wins.""" + monkeypatch.setenv(f"UIPATH_FEATURE_{GOVERNANCE_FEATURE_FLAG}", "false") + assert is_governance_enabled() is False diff --git a/packages/uipath-core/tests/governance/test_exceptions.py b/packages/uipath-core/tests/governance/test_exceptions.py new file mode 100644 index 000000000..257feb3d4 --- /dev/null +++ b/packages/uipath-core/tests/governance/test_exceptions.py @@ -0,0 +1,205 @@ +"""Tests for GovernanceBlockException constructors. + +The classmethod constructors (:meth:`from_violation`, +:meth:`from_audit_record`) form the documented contract that the +evaluator and adapter packages depend on — the evaluator only ever +builds a block via ``from_audit_record``. These tests pin the message +format and attribute population so a future refactor cannot silently +drop the rule id, name, or detail. +""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest + +from uipath.core.governance.exceptions import ( + GovernanceBlockException, + GovernanceViolation, + Severity, +) +from uipath.core.governance.models import ( + Action, + AuditRecord, + LifecycleHook, + RuleEvaluation, +) + +# --------------------------------------------------------------------------- +# GovernanceViolation +# --------------------------------------------------------------------------- + + +def test_violation_defaults_to_high_severity(): + v = GovernanceViolation(rule_id="A-1", rule_name="No PII", detail="ssn leaked") + assert v.severity == Severity.HIGH + + +def test_violation_severity_can_be_overridden(): + v = GovernanceViolation( + rule_id="A-1", + rule_name="No PII", + detail="ssn leaked", + severity=Severity.CRITICAL, + ) + assert v.severity == Severity.CRITICAL + + +# --------------------------------------------------------------------------- +# GovernanceBlockException base constructor +# --------------------------------------------------------------------------- + + +def test_default_constructor_emits_prefixed_message(): + exc = GovernanceBlockException() + assert "[Governance Policy Violation]" in str(exc) + assert exc.violation is None + assert exc.audit_record is None + + +def test_default_constructor_carries_default_rule_metadata(): + """Constructing without context still gives the documented fallback IDs.""" + exc = GovernanceBlockException() + assert exc.rule_id == "POLICY" + assert exc.rule_name == "Governance Policy" + + +def test_explicit_message_is_used_verbatim(): + exc = GovernanceBlockException("custom message") + assert str(exc) == "custom message" + + +def test_error_code_constant_for_orchestrator_categorization(): + """error_code is a class-level constant the Orchestrator UI reads.""" + assert GovernanceBlockException.error_code == "GOVERNANCE_POLICY_VIOLATION" + exc = GovernanceBlockException() + assert exc.error_code == "GOVERNANCE_POLICY_VIOLATION" + + +# --------------------------------------------------------------------------- +# from_violation +# --------------------------------------------------------------------------- + + +def test_from_violation_populates_rule_metadata(): + v = GovernanceViolation(rule_id="A-1", rule_name="No PII", detail="ssn leaked") + exc = GovernanceBlockException.from_violation(v) + assert exc.rule_id == "A-1" + assert exc.rule_name == "No PII" + assert exc.violation is v + + +def test_from_violation_message_includes_rule_id_name_detail(): + v = GovernanceViolation(rule_id="A-1", rule_name="No PII", detail="ssn leaked") + msg = str(GovernanceBlockException.from_violation(v)) + assert "A-1" in msg + assert "No PII" in msg + assert "ssn leaked" in msg + assert "[Governance Policy Violation]" in msg + + +# --------------------------------------------------------------------------- +# from_audit_record +# --------------------------------------------------------------------------- + + +def _audit_record_with(*evaluations: RuleEvaluation) -> AuditRecord: + return AuditRecord( + timestamp=datetime.now(timezone.utc), + agent_name="agent", + runtime_id="run-1", + trace_id="trace-1", + hook=LifecycleHook.BEFORE_AGENT, + evaluations=list(evaluations), + final_action=Action.DENY, + ) + + +def test_from_audit_record_picks_first_matched_rule(): + """Even when later evaluations matched, the first matched wins the message.""" + audit = _audit_record_with( + RuleEvaluation( + rule_id="UNMATCHED", + rule_name="Did not fire", + matched=False, + detail="", + action=Action.ALLOW, + ), + RuleEvaluation( + rule_id="MATCHED-FIRST", + rule_name="First match", + matched=True, + detail="bad input", + action=Action.DENY, + ), + RuleEvaluation( + rule_id="MATCHED-SECOND", + rule_name="Second match", + matched=True, + detail="also bad", + action=Action.DENY, + ), + ) + + exc = GovernanceBlockException.from_audit_record(audit) + assert exc.rule_id == "MATCHED-FIRST" + assert exc.rule_name == "First match" + assert "bad input" in str(exc) + assert exc.audit_record is audit + + +def test_from_audit_record_falls_back_when_no_match(): + """When the audit has no matches, the exception is still constructible.""" + audit = _audit_record_with( + RuleEvaluation( + rule_id="UNMATCHED", + rule_name="Did not fire", + matched=False, + detail="", + action=Action.ALLOW, + ) + ) + + exc = GovernanceBlockException.from_audit_record(audit) + assert "Rules evaluated: 1" in str(exc) + assert exc.audit_record is audit + + +def test_from_audit_record_matched_detail_default_when_empty(): + """A matched evaluation with empty detail still produces a sensible message.""" + audit = _audit_record_with( + RuleEvaluation( + rule_id="A-1", + rule_name="No PII", + matched=True, + detail="", # empty + action=Action.DENY, + ) + ) + + msg = str(GovernanceBlockException.from_audit_record(audit)) + assert "A-1" in msg + assert "No PII" in msg + # Falls back to a non-empty detail string. + assert "Policy violation detected" in msg + + +# --------------------------------------------------------------------------- +# Exception identity — must be a real Exception so callers can catch broadly +# --------------------------------------------------------------------------- + + +def test_block_exception_is_exception_subclass(): + assert issubclass(GovernanceBlockException, Exception) + + +def test_block_exception_can_be_caught_via_base_exception(): + try: + raise GovernanceBlockException.from_violation( + GovernanceViolation(rule_id="A-1", rule_name="X", detail="d") + ) + except Exception as e: # noqa: BLE001 - intentional broad catch + assert isinstance(e, GovernanceBlockException) + else: + pytest.fail("Did not raise") diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 9aa9417f4..0ef8204e9 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-25T19:32:11.835974Z" +exclude-newer = "2026-06-06T13:38:31.678016Z" exclude-newer-span = "P2D" [[package]] @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.17" +version = "0.5.18" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 34dc9d413..c78924106 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-03T20:09:03.1179056Z" +exclude-newer = "2026-06-06T13:38:31.678016Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.17" +version = "0.5.18" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 657a4cf7d..87b11124e 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-03T20:09:04.4644787Z" +exclude-newer = "2026-06-06T13:38:31.678016Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.17" +version = "0.5.18" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 3825955b4093a36cc9828cf1e4a2a888c860d45e Mon Sep 17 00:00:00 2001 From: Cristian Cotovanu <87022468+cotovanu-cristian@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:48:46 +0300 Subject: [PATCH 090/121] feat(agent): QuickForm escalation channel models (#1686) Co-authored-by: Claude Opus 4.8 (1M context) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 95 ++++++++++++------- .../uipath/tests/agent/models/test_agent.py | 74 +++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 138 insertions(+), 35 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index c4da7e297..2e490ade3 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.78" +version = "2.10.79" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 5aa959e8f..385a37e7c 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -142,6 +142,13 @@ class AgentEscalationRecipientType(str, CaseInsensitiveEnum): ARGUMENT_GROUP_NAME = "ArgumentGroupName" +class AgentEscalationChannelType(str, CaseInsensitiveEnum): + """Agent escalation channel type enumeration.""" + + ACTION_CENTER = "actionCenter" + ACTION_CENTER_QUICK_FORM = "actionCenterQuickForm" + + class AgentContextRetrievalMode(str, CaseInsensitiveEnum): """Agent context retrieval mode enumeration.""" @@ -691,13 +698,9 @@ def _resolve_task_title(v: Any) -> Any: return v -class AgentEscalationChannelProperties(BaseResourceProperties): - """Agent escalation channel properties model.""" +class BaseEscalationChannelProperties(BaseResourceProperties): + """Fields shared by every escalation channel's properties.""" - app_name: str | None = Field(default=None, alias="appName") - app_version: int = Field(..., alias="appVersion") - folder_name: Optional[str] = Field(None, alias="folderName") - resource_key: str | None = Field(default=None, alias="resourceKey") is_actionable_message_enabled: Optional[bool] = Field( None, alias="isActionableMessageEnabled" ) @@ -706,12 +709,31 @@ class AgentEscalationChannelProperties(BaseResourceProperties): ) -class AgentEscalationChannel(BaseCfg): - """Agent escalation channel model.""" +class AgentEscalationChannelProperties(BaseEscalationChannelProperties): + """Action Center app-task channel properties (channel type ``actionCenter``).""" + + app_name: str | None = Field(default=None, alias="appName") + app_version: int = Field(..., alias="appVersion") + folder_name: Optional[str] = Field(None, alias="folderName") + resource_key: str | None = Field(default=None, alias="resourceKey") + + +class AgentQuickFormChannelProperties(BaseEscalationChannelProperties): + """Quick Form channel properties (channel type ``actionCenterQuickForm``).""" + + form_schema: Dict[str, Any] = Field(..., alias="schema") + + @property + def schema_id(self) -> str | None: + """Return the schema id nested inside the form schema body.""" + return self.form_schema.get("schemaId") + + +class BaseAgentEscalationChannel(BaseCfg): + """Fields shared by every escalation channel variant.""" id: Optional[str] = Field(None, alias="id") name: str = Field(..., alias="name") - type: str = Field(alias="type") description: str = Field(..., alias="description") input_schema: Dict[str, Any] = Field(..., alias="inputSchema") output_schema: Dict[str, Any] = Field(EMPTY_SCHEMA, alias="outputSchema") @@ -719,16 +741,12 @@ class AgentEscalationChannel(BaseCfg): {}, alias="argumentProperties" ) outcome_mapping: Optional[Dict[str, str]] = Field(None, alias="outcomeMapping") - properties: AgentEscalationChannelProperties = Field(..., alias="properties") recipients: List[AgentEscalationRecipient] = Field(..., alias="recipients") task_title: Optional[Union[str, TaskTitle]] = Field( default="Escalation Task", alias="taskTitle" ) priority: Optional[str] = None labels: List[str] = Field(default_factory=list) - # schema_body avoids shadowing pydantic.BaseModel.schema(); JSON alias stays "schema". - schema_id: Optional[str] = Field(None, alias="schemaId") - schema_body: Optional[Dict[str, Any]] = Field(None, alias="schema") @model_validator(mode="before") @classmethod @@ -737,6 +755,34 @@ def _apply_task_title_resolution(cls, v: Any) -> Any: return _resolve_task_title(v) +class AgentEscalationChannel(BaseAgentEscalationChannel): + """Action Center app-task escalation channel (channel type ``actionCenter``).""" + + type: Literal[AgentEscalationChannelType.ACTION_CENTER] = Field( + default=AgentEscalationChannelType.ACTION_CENTER, alias="type" + ) + properties: AgentEscalationChannelProperties = Field(..., alias="properties") + + +class AgentQuickFormEscalationChannel(BaseAgentEscalationChannel): + """Quick Form escalation channel; FormLib schema lives in ``properties.form_schema``.""" + + type: Literal[AgentEscalationChannelType.ACTION_CENTER_QUICK_FORM] = Field( + default=AgentEscalationChannelType.ACTION_CENTER_QUICK_FORM, alias="type" + ) + properties: AgentQuickFormChannelProperties = Field(..., alias="properties") + + +EscalationChannel = Annotated[ + Union[ + AgentEscalationChannel, + AgentQuickFormEscalationChannel, + ], + Field(discriminator="type"), + _case_insensitive_enum_validator("type", AgentEscalationChannelType), +] + + class AgentEscalationResourceConfig(BaseAgentResourceConfig): """Agent escalation resource configuration model.""" @@ -744,7 +790,7 @@ class AgentEscalationResourceConfig(BaseAgentResourceConfig): resource_type: Literal[AgentResourceType.ESCALATION] = Field( alias="$resourceType", default=AgentResourceType.ESCALATION, frozen=True ) - channels: List[AgentEscalationChannel] = Field(alias="channels") + channels: List[EscalationChannel] = Field(alias="channels") is_agent_memory_enabled: bool = Field(default=False, alias="isAgentMemoryEnabled") escalation_type: Literal[0] = Field(default=0, alias="escalationType") @@ -764,7 +810,7 @@ class AgentIxpVsEscalationResourceConfig(BaseAgentResourceConfig): resource_type: Literal[AgentResourceType.ESCALATION] = Field( alias="$resourceType", default=AgentResourceType.ESCALATION, frozen=True ) - channels: List[AgentEscalationChannel] = Field(alias="channels") + channels: List[EscalationChannel] = Field(alias="channels") is_agent_memory_enabled: bool = Field(default=False, alias="isAgentMemoryEnabled") escalation_type: Literal[1] = Field(default=1, alias="escalationType") vs_escalation_properties: AgentIxpVsEscalationProperties = Field( @@ -772,23 +818,6 @@ class AgentIxpVsEscalationResourceConfig(BaseAgentResourceConfig): ) -class AgentQuickFormEscalationResourceConfig(BaseAgentResourceConfig): - """Quick Form Agent escalation resource configuration model (escalationType=2). - - Quick Form escalations render a schema-first HITL task in Action Center via FormLib. - The schema (and its key) live on the channel (see AgentEscalationChannel.schema_id / - schema) and are sent inline to Orchestrator's GenericTasks/CreateTask endpoint. - """ - - id: Optional[str] = Field(None, alias="id") - resource_type: Literal[AgentResourceType.ESCALATION] = Field( - alias="$resourceType", default=AgentResourceType.ESCALATION, frozen=True - ) - channels: List[AgentEscalationChannel] = Field(alias="channels") - is_agent_memory_enabled: bool = Field(default=False, alias="isAgentMemoryEnabled") - escalation_type: Literal[2] = Field(default=2, alias="escalationType") - - class BaseAgentToolResourceConfig(BaseAgentResourceConfig): """Base agent tool resource configuration model.""" @@ -994,11 +1023,11 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig): Field(discriminator="type"), ] + EscalationResourceConfig = Annotated[ Union[ Annotated[AgentEscalationResourceConfig, Tag(0)], Annotated[AgentIxpVsEscalationResourceConfig, Tag(1)], - Annotated[AgentQuickFormEscalationResourceConfig, Tag(2)], ], Discriminator(lambda v: v.get("escalation_type") or v.get("escalationType") or 0), ] diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index fcfefa946..c324d4e7e 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -36,6 +36,7 @@ AgentNumberOperator, AgentNumberRule, AgentProcessToolResourceConfig, + AgentQuickFormChannelProperties, AgentResourceType, AgentToolArgumentPropertiesVariant, AgentToolType, @@ -2607,11 +2608,84 @@ def test_agent_with_ixp_vs_escalation(self): assert len(channel.recipients) == 0 # Validate channel properties + assert isinstance(channel, AgentEscalationChannel) assert channel.properties.app_name is None assert channel.properties.app_version == 1 assert channel.properties.folder_name is None assert channel.properties.resource_key is None + def test_quick_form_channel_properties_derive_schema_id_from_body(self): + """schema_id reads the schemaId nested inside the schema body.""" + + props = AgentQuickFormChannelProperties.model_validate( + { + "schema": { + "schemaId": "e74ebb74-80ba-47b9-a370-532a1ba4c41e", + "fields": [], + "outcomes": [], + }, + } + ) + assert props.schema_id == "e74ebb74-80ba-47b9-a370-532a1ba4c41e" + + def test_quick_form_channel_properties_schema_id_none_when_absent(self): + """schema_id is None when the schema body carries no schemaId.""" + + props = AgentQuickFormChannelProperties.model_validate( + {"schema": {"fields": [], "outcomes": []}} + ) + assert props.schema_id is None + + def test_quick_form_channel_properties_require_schema(self): + with pytest.raises(ValidationError): + AgentQuickFormChannelProperties.model_validate( + {"isActionableMessageEnabled": False} + ) + + def test_quick_form_channel_requires_schema(self): + """A quick-form channel without a schema fails to parse.""" + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationResourceConfig).validate_python( + { + "$resourceType": "escalation", + "name": "Escalation", + "description": "", + "channels": [ + { + "name": "c", + "description": "", + "inputSchema": {"type": "object", "properties": {}}, + "type": "actionCenterQuickForm", + "recipients": [], + "properties": {"isActionableMessageEnabled": False}, + } + ], + "isAgentMemoryEnabled": False, + } + ) + + def test_unknown_escalation_channel_type_is_rejected(self): + """An unrecognized channel type fails to parse; the runtime cannot handle it.""" + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationResourceConfig).validate_python( + { + "$resourceType": "escalation", + "name": "Escalation", + "description": "", + "channels": [ + { + "name": "c", + "description": "", + "inputSchema": {"type": "object", "properties": {}}, + "type": "someFutureChannel", + "recipients": [], + "properties": {}, + } + ], + "isAgentMemoryEnabled": False, + } + ) + def test_task_title_text_builder_type(self): """Test TextBuilderTaskTitle with tokens.""" from uipath.agent.models.agent import ( diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 87b11124e..8acee32f9 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.78" +version = "2.10.79" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From d31ef4237b76da90ed432b4a0101b95a939dc0fc Mon Sep 17 00:00:00 2001 From: msbhavana <91954125+msbhavana@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:37:26 +0530 Subject: [PATCH 091/121] feat(resource-catalog): add ENTITY to ResourceType enum (#1705) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../resource_catalog/resource_catalog.py | 1 + packages/uipath-platform/uv.lock | 4 +- .../uipath/tests/cli/test_create_resources.py | 48 +++++++++++++++++-- packages/uipath/uv.lock | 4 +- 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index aa3ef6b47..4a5c0b83c 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.61" +version = "0.1.62" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/resource_catalog/resource_catalog.py b/packages/uipath-platform/src/uipath/platform/resource_catalog/resource_catalog.py index bedf6525d..67fdf52f6 100644 --- a/packages/uipath-platform/src/uipath/platform/resource_catalog/resource_catalog.py +++ b/packages/uipath-platform/src/uipath/platform/resource_catalog/resource_catalog.py @@ -22,6 +22,7 @@ class ResourceType(str, Enum): CONNECTOR = "connector" MCP_SERVER = "mcpserver" QUEUE = "queue" + ENTITY = "entity" @classmethod def from_string(cls, value: str) -> "ResourceType": diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index c78924106..ac4761377 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-06T13:38:31.678016Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.61" +version = "0.1.62" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/tests/cli/test_create_resources.py b/packages/uipath/tests/cli/test_create_resources.py index 08012dccc..aff1c7cab 100644 --- a/packages/uipath/tests/cli/test_create_resources.py +++ b/packages/uipath/tests/cli/test_create_resources.py @@ -14,6 +14,7 @@ VirtualResourceResult, ) from uipath.platform.errors import EnrichedException, FolderNotFoundException +from uipath.platform.resource_catalog import ResourceType def _enriched_exc( @@ -270,8 +271,39 @@ async def test_unsupported_virtual_kind_is_skipped_with_warning( bindings_file, mock_uipath, studio_client ): """Bindings whose kind the virtual endpoint cannot materialize (e.g. - 'entity', 'choiceSet', 'webhook') should be skipped with a warning and + 'choiceSet', 'webhook') should be skipped with a warning and never reach create_virtual_resource.""" + choiceset_binding = { + "resource": "choiceSet", + "key": "live.good.choiceset.Shared", + "value": { + "name": { + "defaultValue": "live.good.choiceset", + "isExpression": False, + "displayName": "Name", + }, + "folderPath": { + "defaultValue": "Shared", + "isExpression": False, + "displayName": "Folder Path", + }, + }, + "metadata": None, + } + bindings_file(_make_bindings([choiceset_binding])) + + await _run_create_resources(studio_client) + + mock_uipath.resource_catalog.list_by_type_async.assert_not_called() + studio_client.create_virtual_resource.assert_not_awaited() + studio_client.create_referenced_resource.assert_not_awaited() + + +async def test_entity_binding_catalog_hit_creates_reference( + bindings_file, mock_uipath, studio_client +): + """Entity bindings should go through the resource catalog lookup. + When found, a referenced resource should be created.""" entity_binding = { "resource": "entity", "key": "live.good.entity.Shared", @@ -290,12 +322,22 @@ async def test_unsupported_virtual_kind_is_skipped_with_warning( "metadata": None, } bindings_file(_make_bindings([entity_binding])) + mock_uipath.resource_catalog.list_by_type_async.return_value = _AsyncIterator( + [_found_resource(resource_type="entity", resource_sub_type="Native")] + ) + studio_client.create_referenced_resource.return_value = SimpleNamespace( + status=Status.ADDED + ) await _run_create_resources(studio_client) - mock_uipath.resource_catalog.list_by_type_async.assert_not_called() + mock_uipath.resource_catalog.list_by_type_async.assert_called_once_with( + resource_type=ResourceType.ENTITY, + name="live.good.entity", + folder_path="Shared", + ) + studio_client.create_referenced_resource.assert_awaited_once() studio_client.create_virtual_resource.assert_not_awaited() - studio_client.create_referenced_resource.assert_not_awaited() async def test_folder_not_found_falls_back_to_virtual( diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 8acee32f9..476c94d05 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-06T13:38:31.678016Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.61" +version = "0.1.62" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From cf3e35f05c67bed621379a15efc68100916e3abf Mon Sep 17 00:00:00 2001 From: Chibi Vikramathithan Date: Wed, 10 Jun 2026 17:48:43 -0700 Subject: [PATCH 092/121] fix(eval): normalize uppercase GUID eval ids at ingestion (PC-4688) (#1702) Co-authored-by: Claude Opus 4.8 (1M context) --- packages/uipath/pyproject.toml | 2 +- .../src/uipath/eval/models/evaluation_set.py | 34 ++++++++- .../tests/cli/eval/test_eval_id_casing.py | 69 +++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 packages/uipath/tests/cli/eval/test_eval_id_casing.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 2e490ade3..79881bbc5 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.79" +version = "2.10.80" 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/eval/models/evaluation_set.py b/packages/uipath/src/uipath/eval/models/evaluation_set.py index 22e6ce244..c80da8e14 100644 --- a/packages/uipath/src/uipath/eval/models/evaluation_set.py +++ b/packages/uipath/src/uipath/eval/models/evaluation_set.py @@ -1,8 +1,9 @@ """Evaluation set models.""" +import re from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic.alias_generators import to_camel from ..mocks._types import ( @@ -15,6 +16,21 @@ LegacyConversationalEvalOutput, ) +_GUID_RE = re.compile( + r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" + r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" +) + + +def normalize_eval_id(value: str) -> str: + """Canonicalize a GUID id to lowercase; leave non-GUID ids unchanged. + + GUIDs are case-insensitive, but downstream correlation (selection, + span/cache keying) compares ids as plain strings, so a mixed-case id + must be normalized at ingestion to stay matchable. + """ + return value.lower() if isinstance(value, str) and _GUID_RE.match(value) else value + class EvaluatorReference(BaseModel): """Reference to an evaluator with optional weight. @@ -96,6 +112,12 @@ class EvaluationItem(BaseModel): alias="inputMockingStrategy", ) + @field_validator("id") + @classmethod + def _normalize_id(cls, value: str) -> str: + """Normalize GUID ids to canonical lowercase.""" + return normalize_eval_id(value) + class LegacyEvaluationItem(BaseModel): """Individual evaluation item within an evaluation set.""" @@ -130,6 +152,12 @@ class LegacyEvaluationItem(BaseModel): default=None, alias="conversationalExpectedOutput" ) + @field_validator("id") + @classmethod + def _normalize_id(cls, value: str) -> str: + """Normalize GUID ids to canonical lowercase.""" + return normalize_eval_id(value) + class EvaluationSet(BaseModel): """Complete evaluation set model.""" @@ -153,7 +181,7 @@ class EvaluationSet(BaseModel): def extract_selected_evals(self, eval_ids) -> None: """Filter evaluations to only include those with specified IDs.""" selected_evals: list[EvaluationItem] = [] - remaining_ids = set(eval_ids) + remaining_ids = {normalize_eval_id(eval_id) for eval_id in eval_ids} for evaluation in self.evaluations: if evaluation.id in remaining_ids: selected_evals.append(evaluation) @@ -187,7 +215,7 @@ class LegacyEvaluationSet(BaseModel): def extract_selected_evals(self, eval_ids) -> None: """Filter evaluations to only include those with specified IDs.""" selected_evals: list[LegacyEvaluationItem] = [] - remaining_ids = set(eval_ids) + remaining_ids = {normalize_eval_id(eval_id) for eval_id in eval_ids} for evaluation in self.evaluations: if evaluation.id in remaining_ids: selected_evals.append(evaluation) diff --git a/packages/uipath/tests/cli/eval/test_eval_id_casing.py b/packages/uipath/tests/cli/eval/test_eval_id_casing.py new file mode 100644 index 000000000..e14a88a68 --- /dev/null +++ b/packages/uipath/tests/cli/eval/test_eval_id_casing.py @@ -0,0 +1,69 @@ +"""Tests for case-insensitive eval id handling (PC-4688). + +Eval sets exported by some tools emit uppercase GUID ids. The backend +canonicalizes GUIDs to lowercase, so any case-sensitive correlation on the +runtime side (selection, span/cache keying) silently fails to match. These +tests pin the fix: GUID ids are normalized to lowercase at ingestion and +selection is casing-agnostic. +""" + +from typing import Any + +from uipath.eval.models.evaluation_set import ( + EvaluationItem, + EvaluationSet, + LegacyEvaluationItem, +) + +UPPER_GUID = "B063907C-76AB-4B0A-88A3-EC0FB40698B8" +LOWER_GUID = "b063907c-76ab-4b0a-88a3-ec0fb40698b8" + + +def _make_item(eval_id: str) -> dict[str, Any]: + return { + "id": eval_id, + "name": "item", + "inputs": {"x": 1}, + "evaluationCriterias": {}, + } + + +def test_evaluation_item_normalizes_uppercase_guid_id(): + """An uppercase GUID id is stored in canonical lowercase form.""" + item = EvaluationItem.model_validate(_make_item(UPPER_GUID)) + assert item.id == LOWER_GUID + + +def test_legacy_evaluation_item_normalizes_uppercase_guid_id(): + """LegacyEvaluationItem also normalizes uppercase GUID ids.""" + item = LegacyEvaluationItem.model_validate( + { + "id": UPPER_GUID, + "name": "item", + "inputs": {"x": 1}, + "expectedOutput": {}, + "evalSetId": "set-1", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z", + } + ) + assert item.id == LOWER_GUID + + +def test_non_guid_id_is_left_unchanged(): + """Non-GUID ids (e.g. slugs) keep their original value and casing.""" + item = EvaluationItem.model_validate(_make_item("Test-Eval-1")) + assert item.id == "Test-Eval-1" + + +def test_extract_selected_evals_matches_regardless_of_caller_casing(): + """Selecting by an uppercase GUID matches a normalized stored id.""" + eval_set = EvaluationSet.model_validate( + { + "id": "set-1", + "name": "set", + "evaluations": [_make_item(LOWER_GUID), _make_item("other-id")], + } + ) + eval_set.extract_selected_evals([UPPER_GUID]) + assert [e.id for e in eval_set.evaluations] == [LOWER_GUID] diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 476c94d05..f78dd4cbe 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.79" +version = "2.10.80" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 13bc71dd3962abc8c2373925c85e18a85b7f2021 Mon Sep 17 00:00:00 2001 From: Chibi Vikramathithan Date: Wed, 10 Jun 2026 21:41:51 -0700 Subject: [PATCH 093/121] fix(eval): use function calling for tool/input mocking so non-OpenAI models work (AE-1646) (#1692) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/chat/_llm_gateway_service.py | 19 +- .../services/test_uipath_llm_integration.py | 82 +++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- .../src/uipath/eval/mocks/_input_mocker.py | 28 +- .../src/uipath/eval/mocks/_llm_mocker.py | 34 +- .../uipath/eval/mocks/_structured_output.py | 259 +++++++++++++++ .../tests/cli/eval/mocks/test_input_mocker.py | 7 + .../cli/eval/mocks/test_input_mocker_span.py | 8 + .../uipath/tests/cli/eval/mocks/test_mocks.py | 128 +++++++- .../cli/eval/mocks/test_structured_output.py | 295 ++++++++++++++++++ packages/uipath/uv.lock | 4 +- 13 files changed, 808 insertions(+), 64 deletions(-) create mode 100644 packages/uipath/src/uipath/eval/mocks/_structured_output.py create mode 100644 packages/uipath/tests/cli/eval/mocks/test_structured_output.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 4a5c0b83c..50dd25553 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.62" +version = "0.1.63" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py b/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py index ffe0bff99..cb02d8af3 100644 --- a/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py +++ b/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py @@ -401,7 +401,7 @@ async def chat_completions( presence_penalty: float = 0, top_p: float | None = 1, top_k: int | None = None, - tools: list[ToolDefinition] | None = None, + tools: list[ToolDefinition | dict[str, Any]] | None = None, tool_choice: ToolChoice | None = None, response_format: dict[str, Any] | type[BaseModel] | None = None, api_version: str = NORMALIZED_API_VERSION, @@ -436,9 +436,11 @@ async def chat_completions( Controls diversity by considering only the top p probability mass. Defaults to 1. top_k (int, optional): Nucleus sampling parameter. Controls diversity by considering only the top k most probable tokens. Defaults to None. - tools (Optional[List[ToolDefinition]], optional): List of tool definitions that the - model can call. Tools enable the model to perform actions or retrieve information - beyond text generation. Defaults to None. + tools (Optional[List[ToolDefinition | dict]], optional): List of tool definitions + that the model can call. Tools enable the model to perform actions or retrieve + information beyond text generation. A tool given as a dict must already be in + UiPath wire format and is forwarded unchanged, which allows arbitrary nested + JSON schemas in its parameters. Defaults to None. tool_choice (Optional[ToolChoice], optional): Controls which tools the model can call. Can be "auto" (model decides), "none" (no tools), or a specific tool choice. Defaults to None. @@ -583,10 +585,15 @@ class Country(BaseModel): # Use provided dictionary format directly request_body["response_format"] = response_format - # Add tools if provided - convert to UiPath format + # Add tools if provided. A tool already in UiPath wire format (a dict) is + # passed through unchanged so callers can supply an arbitrary JSON schema + # for the parameters; ToolDefinition objects are converted as before. if tools: request_body["tools"] = [ - self._convert_tool_to_uipath_format(tool) for tool in tools + tool + if isinstance(tool, dict) + else self._convert_tool_to_uipath_format(tool) + for tool in tools ] # Handle tool_choice diff --git a/packages/uipath-platform/tests/services/test_uipath_llm_integration.py b/packages/uipath-platform/tests/services/test_uipath_llm_integration.py index 124ccad8b..9e2292c60 100644 --- a/packages/uipath-platform/tests/services/test_uipath_llm_integration.py +++ b/packages/uipath-platform/tests/services/test_uipath_llm_integration.py @@ -7,6 +7,7 @@ from uipath.platform.chat import ( AutoToolChoice, ChatModels, + RequiredToolChoice, SpecificToolChoice, ToolDefinition, ToolFunctionDefinition, @@ -369,6 +370,87 @@ async def test_tool_call_required_mocked(self, mock_request, llm_service): assert result.choices[0].message.tool_calls[0].arguments["name"] == "John" assert result.choices[0].message.tool_calls[0].arguments["password"] == "1234" + @pytest.mark.asyncio + @patch.object(UiPathLlmChatService, "request_async") + async def test_raw_dict_tool_passthrough_mocked(self, mock_request, llm_service): + """A tool supplied as a raw dict is sent unchanged, preserving nested schema. + + ToolDefinition's converter only emits flat properties, so callers that need + an arbitrary nested JSON schema (e.g. the eval mockers) pass the tool as a + dict already in UiPath wire format. It must reach the gateway verbatim. + """ + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "chatcmpl-raw", + "object": "chat.completion", + "created": 1677858242, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_raw", + "name": "submit_tool_response", + "arguments": {"response": {"items": [{"sku": "A1"}]}}, + } + ], + }, + "finish_reason": "tool_calls", + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "cache_read_input_tokens": None, + }, + } + mock_request.return_value = mock_response + + nested_tool = { + "name": "submit_tool_response", + "description": "Return the simulated response matching the schema.", + "parameters": { + "type": "object", + "properties": { + "response": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": {"sku": {"type": "string"}}, + }, + } + }, + } + }, + "required": ["response"], + }, + } + + result = await llm_service.chat_completions( + messages=[{"role": "user", "content": "go"}], + model=ChatModels.gpt_4_1_mini_2025_04_14, + tools=[nested_tool], + tool_choice=RequiredToolChoice(), + ) + + mock_request.assert_called_once() + _, kwargs = mock_request.call_args + body = kwargs["json"] + # The dict tool is forwarded byte-for-byte, nested array schema intact. + assert body["tools"] == [nested_tool] + assert body["tool_choice"] == {"type": "required"} + assert result.choices[0].message.tool_calls[0].arguments == { + "response": {"items": [{"sku": "A1"}]} + } + @pytest.mark.asyncio @patch.object(UiPathLlmChatService, "request_async") async def test_chat_with_conversation_history_mocked( diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index ac4761377..2f56c1df5 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.62" +version = "0.1.63" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 79881bbc5..ffaa7f881 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.80" +version = "2.10.81" 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" dependencies = [ "uipath-core>=0.5.17, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.60, <0.2.0", + "uipath-platform>=0.1.63, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/eval/mocks/_input_mocker.py b/packages/uipath/src/uipath/eval/mocks/_input_mocker.py index 57a727ec1..a542fc7ad 100644 --- a/packages/uipath/src/uipath/eval/mocks/_input_mocker.py +++ b/packages/uipath/src/uipath/eval/mocks/_input_mocker.py @@ -15,6 +15,7 @@ from .._execution_context import eval_set_run_id_context from ._mock_context import cache_manager_context from ._mocker import UiPathInputMockingError +from ._structured_output import generate_structured_output from ._types import ( InputMockingStrategy, ) @@ -105,15 +106,6 @@ async def generate_llm_input( prompt = get_input_mocking_prompt(**prompt_generation_args) - response_format = { - "type": "json_schema", - "json_schema": { - "name": "agent_input", - "strict": False, - "schema": input_schema, - }, - } - model_parameters = mocking_strategy.model if mocking_strategy else None completion_kwargs = ( model_parameters.model_dump(by_alias=False, exclude_none=True) @@ -128,7 +120,7 @@ async def generate_llm_input( if cache_manager is not None: cache_key_data = { - "response_format": response_format, + "input_schema": input_schema, "completion_kwargs": completion_kwargs, "prompt_generation_args": prompt_generation_args, } @@ -142,15 +134,15 @@ async def generate_llm_input( if cached_response is not None: return cached_response - response = await llm.chat_completions( + result = await generate_structured_output( + llm, [{"role": "user", "content": prompt}], - response_format=response_format, - **completion_kwargs, + schema=input_schema, + response_format_name="agent_input", + description="Return the simulated agent input matching the required schema.", + completion_kwargs=completion_kwargs, ) - generated_input_str = response.choices[0].message.content - result = json.loads(generated_input_str) - if cache_manager is not None: cache_manager.set( mocker_type="input_mocker", @@ -160,10 +152,6 @@ async def generate_llm_input( ) return result - except json.JSONDecodeError as e: - raise UiPathInputMockingError( - f"Failed to parse LLM response as JSON: {str(e)}" - ) from e except UiPathInputMockingError: raise except Exception as e: diff --git a/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py b/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py index d1fd2a1c9..a9ab7005e 100644 --- a/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py +++ b/packages/uipath/src/uipath/eval/mocks/_llm_mocker.py @@ -28,6 +28,7 @@ UiPathMockResponseGenerationError, UiPathNoMockFoundError, ) +from ._structured_output import generate_structured_output from ._types import ( ExampleCall, LLMMockingStrategy, @@ -125,14 +126,7 @@ async def response( "output_schema", TypeAdapter(return_type).json_schema() ) - response_format = { - "type": "json_schema", - "json_schema": { - "name": "OutputSchema", - "strict": False, - "schema": _cleanup_schema(output_schema), - }, - } + cleaned_schema = _cleanup_schema(output_schema) try: # Safely pull examples from params. example_calls = params.get("example_calls", []) @@ -197,7 +191,7 @@ async def response( formatted_prompt = PROMPT.format(**prompt_generation_args) cache_key_data = { - "response_format": response_format, + "output_schema": cleaned_schema, "completion_kwargs": completion_kwargs, "prompt_generation_args": prompt_generation_args, } @@ -213,17 +207,17 @@ async def response( if cached_response is not None: return cached_response - response = await llm.chat_completions( - [ - { - "role": "user", - "content": formatted_prompt, - }, - ], - response_format=response_format, - **completion_kwargs, + result = await generate_structured_output( + llm, + [{"role": "user", "content": formatted_prompt}], + schema=cleaned_schema, + response_format_name="OutputSchema", + description=( + "Return the simulated response for tool " + f"'{function_name}' matching the required schema." + ), + completion_kwargs=completion_kwargs, ) - result = json.loads(response.choices[0].message.content) if cache_manager is not None: cache_manager.set( @@ -235,7 +229,7 @@ async def response( return result except Exception as e: - raise UiPathMockResponseGenerationError() from e + raise UiPathMockResponseGenerationError(str(e)) from e else: raise UiPathNoMockFoundError(f"Method '{function_name}' is not simulated.") diff --git a/packages/uipath/src/uipath/eval/mocks/_structured_output.py b/packages/uipath/src/uipath/eval/mocks/_structured_output.py new file mode 100644 index 000000000..599780353 --- /dev/null +++ b/packages/uipath/src/uipath/eval/mocks/_structured_output.py @@ -0,0 +1,259 @@ +"""Provider-aware structured output for the eval mockers. + +The normalized LLM Gateway handles OpenAI-style ``response_format`` +(json_schema) differently per provider — live-verified against the gateway: + +- **OpenAI**: honors ``response_format`` and returns valid JSON content, + including native ``$defs`` support. +- **Anthropic (Claude)**: ignores it and answers with plain prose content. +- **Gemini**: returns empty content. + +Forced function calling works across all three providers, so each provider +gets a small strategy class: OpenAI prefers ``response_format`` (more reliable +for it on some schemas) with a tool-call fallback; Claude and Gemini go +straight to the forced tool call; unknown providers try ``response_format`` +first and fall back. +""" + +import json +import logging +from typing import Any + +from uipath.platform.chat.llm_gateway import RequiredToolChoice + +RESPONSE_TOOL_NAME = "submit_tool_response" +RESPONSE_KEY = "response" +_DEFS_PREFIX = "#/$defs/" + +logger = logging.getLogger(__name__) + + +def _inline_defs( + schema: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any]]: + """Inline ``$defs``/``$ref`` into a self-contained schema. + + Nested Pydantic models and enums emit root ``$defs`` referenced by ``$ref``. + The normalized gateway accepts those in ``response_format`` but not inside a + tool's ``parameters``, so they are inlined here. Sibling keys on a ``$ref`` + node (e.g. a field ``description``) are merged over the inlined definition. + Self-referential definitions cannot be inlined without looping; any ``$ref`` + reached while its target is already on the current resolution path is left + untouched and its definitions are returned so the caller can keep them + reachable. + + Returns: + A tuple of (inlined schema, leftover ``$defs`` needed for cyclic refs). + """ + defs = schema.get("$defs", {}) + leftover: dict[str, Any] = {} + + def resolve(node: Any, active: frozenset[str]) -> Any: + if isinstance(node, dict): + ref = node.get("$ref") + if isinstance(ref, str) and ref.startswith(_DEFS_PREFIX): + name = ref[len(_DEFS_PREFIX) :] + if name in defs and name not in active: + resolved = resolve(defs[name], active | {name}) + siblings = { + key: resolve(value, active) + for key, value in node.items() + if key not in ("$ref", "$defs") + } + if isinstance(resolved, dict): + return {**resolved, **siblings} + return resolved + # Cyclic or unknown ref: keep it and preserve its definition. + if name in defs: + leftover[name] = defs[name] + return dict(node) + return { + key: resolve(value, active) + for key, value in node.items() + if key != "$defs" + } + if isinstance(node, list): + return [resolve(item, active) for item in node] + return node + + root = {key: value for key, value in schema.items() if key != "$defs"} + inlined = resolve(root, frozenset()) + return inlined, leftover + + +def build_response_tool(schema: dict[str, Any], description: str) -> dict[str, Any]: + """Build a normalized-API function tool that wraps ``schema`` under ``response``. + + Tool-call arguments are always a JSON object, so an arbitrary output schema + (which may be a scalar, array, or object) is nested under a single + ``response`` property and unwrapped after the call. ``$defs``/``$ref`` are + inlined so the tool parameters are self-contained, which the gateway requires + for tool schemas (unlike ``response_format``). + """ + response_schema, leftover_defs = _inline_defs(schema) + parameters: dict[str, Any] = { + "type": "object", + "properties": {RESPONSE_KEY: response_schema}, + "required": [RESPONSE_KEY], + } + if leftover_defs: + parameters["$defs"] = leftover_defs + + return { + "name": RESPONSE_TOOL_NAME, + "description": description, + "parameters": parameters, + } + + +def extract_response(response: Any) -> Any: + """Extract the wrapped value from the forced tool call. + + Raises: + ValueError: if the response carries no usable tool call or is missing the + wrapped ``response`` key. + """ + choices = getattr(response, "choices", None) + if not choices: + raise ValueError("LLM response contained no choices") + + message = choices[0].message + tool_calls = getattr(message, "tool_calls", None) + if not tool_calls: + raise ValueError( + f"LLM response contained no tool calls (content={message.content!r})" + ) + + arguments = tool_calls[0].arguments + if RESPONSE_KEY not in arguments: + raise ValueError( + f"Tool call arguments missing '{RESPONSE_KEY}' key: {arguments}" + ) + + return arguments[RESPONSE_KEY] + + +class ToolCallStructuredOutput: + """Structured output via a forced tool call — works on every provider.""" + + async def generate( + self, + llm: Any, + messages: list[dict[str, str]], + *, + schema: dict[str, Any], + response_format_name: str, + description: str, + completion_kwargs: dict[str, Any], + ) -> Any: + """Force a tool call wrapping ``schema`` and unwrap its arguments.""" + tool = build_response_tool(schema, description) + response = await llm.chat_completions( + messages, + tools=[tool], + tool_choice=RequiredToolChoice(), + **completion_kwargs, + ) + return extract_response(response) + + +class ResponseFormatStructuredOutput(ToolCallStructuredOutput): + """Prefer ``response_format`` (json_schema); fall back to a forced tool call. + + The fallback fires when the provider rejects the request, returns empty + content, or returns content that is not valid JSON (Claude's behavior on + the normalized gateway is to answer with plain prose). + """ + + async def generate( + self, + llm: Any, + messages: list[dict[str, str]], + *, + schema: dict[str, Any], + response_format_name: str, + description: str, + completion_kwargs: dict[str, Any], + ) -> Any: + """Try ``response_format`` first, falling back to a forced tool call.""" + response_format = { + "type": "json_schema", + "json_schema": { + "name": response_format_name, + "strict": False, + "schema": schema, + }, + } + + content: str | None = None + try: + response = await llm.chat_completions( + messages, response_format=response_format, **completion_kwargs + ) + choices = getattr(response, "choices", None) + if choices: + content = choices[0].message.content + except Exception as e: + logger.info("response_format path failed, falling back to tools: %s", e) + + if content: + try: + return json.loads(content) + except json.JSONDecodeError: + logger.info( + "response_format content was not JSON, falling back to tools" + ) + + return await super().generate( + llm, + messages, + schema=schema, + response_format_name=response_format_name, + description=description, + completion_kwargs=completion_kwargs, + ) + + +class OpenAIStructuredOutput(ResponseFormatStructuredOutput): + """OpenAI honors ``response_format`` natively (including ``$defs``).""" + + +class AnthropicStructuredOutput(ToolCallStructuredOutput): + """Claude answers ``response_format`` with prose; go straight to tools.""" + + +class GeminiStructuredOutput(ToolCallStructuredOutput): + """Gemini returns empty content for ``response_format``; go straight to tools.""" + + +def _strategy_for_model(model: str | None) -> ToolCallStructuredOutput: + name = (model or "").lower() + if "claude" in name or name.startswith("anthropic"): + return AnthropicStructuredOutput() + if "gemini" in name: + return GeminiStructuredOutput() + if name.startswith(("gpt", "o1", "o3", "o4")): + return OpenAIStructuredOutput() + # Unknown providers: try response_format, fall back to tools. + return ResponseFormatStructuredOutput() + + +async def generate_structured_output( + llm: Any, + messages: list[dict[str, str]], + *, + schema: dict[str, Any], + response_format_name: str, + description: str, + completion_kwargs: dict[str, Any], +) -> Any: + """Generate structured output using the strategy for the requested model.""" + strategy = _strategy_for_model(completion_kwargs.get("model")) + return await strategy.generate( + llm, + messages, + schema=schema, + response_format_name=response_format_name, + description=description, + completion_kwargs=completion_kwargs, + ) diff --git a/packages/uipath/tests/cli/eval/mocks/test_input_mocker.py b/packages/uipath/tests/cli/eval/mocks/test_input_mocker.py index 72b3765df..a8a8a64ec 100644 --- a/packages/uipath/tests/cli/eval/mocks/test_input_mocker.py +++ b/packages/uipath/tests/cli/eval/mocks/test_input_mocker.py @@ -112,3 +112,10 @@ async def test_generate_llm_input_with_model_settings( assert len(chat_completion_requests) == 1, ( "Expected exactly one chat completion request" ) + + # OpenAI returns content via response_format; no tool-call fallback needed. + import json + + body = json.loads(chat_completion_requests[0].content.decode("utf-8")) + assert "response_format" in body + assert "tools" not in body diff --git a/packages/uipath/tests/cli/eval/mocks/test_input_mocker_span.py b/packages/uipath/tests/cli/eval/mocks/test_input_mocker_span.py index 19a432fef..d02c5d242 100644 --- a/packages/uipath/tests/cli/eval/mocks/test_input_mocker_span.py +++ b/packages/uipath/tests/cli/eval/mocks/test_input_mocker_span.py @@ -212,6 +212,14 @@ async def test_simulate_input_span_on_error(httpx_mock: HTTPXMock, monkeypatch): }, }, ) + # The prose content above triggers the tool-call fallback; an empty + # response there fails the fallback too, producing the error span. + httpx_mock.add_response( + url="https://example.com/llm/api/chat/completions" + "?api-version=2024-08-01-preview", + status_code=200, + json={}, + ) mocking_strategy = InputMockingStrategy( prompt="Generate input", diff --git a/packages/uipath/tests/cli/eval/mocks/test_mocks.py b/packages/uipath/tests/cli/eval/mocks/test_mocks.py index c4bc26ee3..e59b07d2f 100644 --- a/packages/uipath/tests/cli/eval/mocks/test_mocks.py +++ b/packages/uipath/tests/cli/eval/mocks/test_mocks.py @@ -610,12 +610,14 @@ def foofoo(*args, **kwargs): with pytest.raises(NotImplementedError): assert foofoo() - httpx_mock.add_response( - url="https://example.com/llm/api/chat/completions" - "?api-version=2024-08-01-preview", - status_code=200, - json={}, - ) + # Two empty responses: the response_format attempt and the tool-call fallback. + for _ in range(2): + httpx_mock.add_response( + url="https://example.com/llm/api/chat/completions" + "?api-version=2024-08-01-preview", + status_code=200, + json={}, + ) with pytest.raises(UiPathMockResponseGenerationError): assert foo() @@ -720,12 +722,14 @@ async def foofoo(*args, **kwargs): with pytest.raises(NotImplementedError): assert await foofoo() - httpx_mock.add_response( - url="https://example.com/llm/api/chat/completions" - "?api-version=2024-08-01-preview", - status_code=200, - json={}, - ) + # Two empty responses: the response_format attempt and the tool-call fallback. + for _ in range(2): + httpx_mock.add_response( + url="https://example.com/llm/api/chat/completions" + "?api-version=2024-08-01-preview", + status_code=200, + json={}, + ) with pytest.raises(UiPathMockResponseGenerationError): assert await foo() @@ -931,6 +935,106 @@ async def foo(*args, **kwargs) -> dict[str, Any]: } +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +async def test_llm_mockable_uses_tool_call_directly_for_non_openai( + httpx_mock: HTTPXMock, monkeypatch: MonkeyPatch +): + """Tool simulation works for non-OpenAI providers (AE-1646). + + Non-OpenAI providers don't honor ``response_format`` on the normalized + gateway (Claude answers with prose, Gemini with empty content), so their + strategies go straight to a forced tool call — a single request. + """ + monkeypatch.setenv("UIPATH_URL", "https://example.com") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "1234567890") + monkeypatch.setattr(CacheManager, "get", lambda *args, **kwargs: None) + monkeypatch.setattr(CacheManager, "set", lambda *args, **kwargs: None) + + @mockable() + async def foo(*args, **kwargs) -> str: + raise NotImplementedError() + + evaluation_item: dict[str, Any] = { + "id": "evaluation-id", + "name": "Mock foo", + "inputs": {}, + "evaluationCriterias": { + "ExactMatchEvaluator": None, + }, + "mockingStrategy": { + "type": "llm", + "prompt": "response is 'bar1'", + "toolsToSimulate": [{"name": "foo"}], + "model": {"model": "anthropic.claude-sonnet-4-5-20250929-v1:0"}, + }, + } + evaluation = EvaluationItem(**evaluation_item) + assert isinstance(evaluation.mocking_strategy, LLMMockingStrategy) + httpx_mock.add_response( + url="https://example.com/agenthub_/llm/api/capabilities", + status_code=200, + json={}, + ) + httpx_mock.add_response( + url="https://example.com/orchestrator_/llm/api/capabilities", + status_code=200, + json={}, + ) + + def _completion(message: dict[str, Any]) -> dict[str, Any]: + return { + "id": "response-id", + "object": "", + "created": 0, + "model": "anthropic.claude-sonnet-4-5-20250929-v1:0", + "choices": [{"index": 0, "message": message, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + } + + # Claude goes straight to function calling: one request, one response. + httpx_mock.add_response( + url="https://example.com/llm/api/chat/completions" + "?api-version=2024-08-01-preview", + status_code=200, + json=_completion( + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_1", + "name": "submit_tool_response", + "arguments": {"response": "bar1"}, + } + ], + } + ), + ) + + set_execution_context( + MockingContext( + strategy=evaluation.mocking_strategy, + name=evaluation.name, + inputs=evaluation.inputs, + ), + _mock_span_collector, + "test-execution-id", + ) + + assert await foo() == "bar1" + + requests = [ + r for r in httpx_mock.get_requests() if "chat/completions" in str(r.url) + ] + assert len(requests) == 1 + body = json.loads(requests[0].content.decode("utf-8")) + # Non-OpenAI providers use a forced tool call directly — no response_format. + assert body["tool_choice"] == {"type": "required"} + assert body["tools"][0]["name"] == "submit_tool_response" + assert "response_format" not in body + + class TestUiPathMockRuntime: """Tests for UiPathMockRuntime execute/stream/get_schema paths.""" diff --git a/packages/uipath/tests/cli/eval/mocks/test_structured_output.py b/packages/uipath/tests/cli/eval/mocks/test_structured_output.py new file mode 100644 index 000000000..79ad31591 --- /dev/null +++ b/packages/uipath/tests/cli/eval/mocks/test_structured_output.py @@ -0,0 +1,295 @@ +"""Unit tests for the provider-agnostic structured-output helpers.""" + +import json +from types import SimpleNamespace +from typing import Any + +import pytest + +from uipath.eval.mocks._structured_output import ( + RESPONSE_KEY, + RESPONSE_TOOL_NAME, + build_response_tool, + extract_response, + generate_structured_output, +) + + +def _response(message: SimpleNamespace | None) -> SimpleNamespace: + choices = [] if message is None else [SimpleNamespace(message=message)] + return SimpleNamespace(choices=choices) + + +class _FakeLLM: + """Records chat_completions calls and replays queued responses in order.""" + + def __init__(self, responses: list[Any]): + self._responses = list(responses) + self.calls: list[dict[str, Any]] = [] + + async def chat_completions(self, messages: Any, **kwargs: Any) -> Any: + self.calls.append(kwargs) + nxt = self._responses.pop(0) + if isinstance(nxt, Exception): + raise nxt + return nxt + + +def test_build_response_tool_wraps_schema_under_response(): + tool = build_response_tool({"type": "string"}, description="desc") + assert tool["name"] == RESPONSE_TOOL_NAME + assert tool["description"] == "desc" + assert tool["parameters"]["properties"][RESPONSE_KEY] == {"type": "string"} + assert tool["parameters"]["required"] == [RESPONSE_KEY] + + +def test_build_response_tool_inlines_refs_into_self_contained_schema(): + # Nested Pydantic models / enums emit $defs + $ref. The normalized gateway + # accepts $ref/$defs in response_format but NOT in a tool's parameters, so the + # schema must be inlined into a self-contained form (no $ref/$defs anywhere). + operator_def = {"enum": ["+", "-", "*", "/"], "type": "string"} + item_def = {"type": "object", "properties": {"sku": {"type": "string"}}} + schema = { + "type": "object", + "properties": { + "operator": {"$ref": "#/$defs/Operator"}, + "items": {"type": "array", "items": {"$ref": "#/$defs/Item"}}, + }, + "required": ["operator"], + "$defs": {"Operator": operator_def, "Item": item_def}, + } + + tool = build_response_tool(schema, description="d") + params = tool["parameters"] + + blob = json.dumps(params) + assert "$ref" not in blob + assert "$defs" not in blob + + response = params["properties"][RESPONSE_KEY] + assert response["properties"]["operator"] == operator_def + assert response["properties"]["items"]["items"] == item_def + # caller's schema is not mutated + assert "$defs" in schema + + +def test_build_response_tool_keeps_defs_for_cyclic_refs(): + # Self-referential schemas can't be fully inlined; keep $defs hoisted so the + # remaining $ref still resolves rather than infinite-looping. + node_def = { + "type": "object", + "properties": {"child": {"$ref": "#/$defs/Node"}}, + } + schema = { + "type": "object", + "properties": {"root": {"$ref": "#/$defs/Node"}}, + "$defs": {"Node": node_def}, + } + + tool = build_response_tool(schema, description="d") + params = tool["parameters"] + + assert "$defs" in params + assert "$ref" in json.dumps(params) + # the caller's schema dict is not mutated + assert "$defs" in schema + + +def test_extract_response_returns_wrapped_value(): + message = SimpleNamespace( + content=None, + tool_calls=[SimpleNamespace(arguments={RESPONSE_KEY: {"a": 1}})], + ) + assert extract_response(_response(message)) == {"a": 1} + + +def test_extract_response_raises_when_no_choices(): + with pytest.raises(ValueError, match="no choices"): + extract_response(_response(None)) + + +def test_extract_response_raises_when_no_tool_calls(): + # Non-OpenAI text response without a tool call: surface a clear error. + message = SimpleNamespace(content="not a tool call", tool_calls=None) + with pytest.raises(ValueError, match="no tool calls"): + extract_response(_response(message)) + + +def test_extract_response_raises_when_response_key_missing(): + message = SimpleNamespace( + content=None, tool_calls=[SimpleNamespace(arguments={"other": 1})] + ) + with pytest.raises(ValueError, match=RESPONSE_KEY): + extract_response(_response(message)) + + +@pytest.mark.asyncio +async def test_generate_structured_output_prefers_response_format_content(): + # OpenAI returns content via response_format; no fallback call is made. + llm = _FakeLLM([_response(SimpleNamespace(content='{"a": 1}', tool_calls=None))]) + result = await generate_structured_output( + llm, + [{"role": "user", "content": "x"}], + schema={"type": "object"}, + response_format_name="OutputSchema", + description="d", + completion_kwargs={}, + ) + assert result == {"a": 1} + assert len(llm.calls) == 1 + assert "response_format" in llm.calls[0] + assert "tools" not in llm.calls[0] + + +@pytest.mark.asyncio +async def test_generate_structured_output_falls_back_on_prose_content(): + # Claude on the normalized gateway answers response_format requests with + # plain prose (e.g. "Tokyo") — truthy but not JSON. Must fall back to tools + # instead of raising JSONDecodeError (AE-1646). + llm = _FakeLLM( + [ + _response(SimpleNamespace(content="Tokyo", tool_calls=None)), + _response( + SimpleNamespace( + content=None, + tool_calls=[SimpleNamespace(arguments={RESPONSE_KEY: {"a": 1}})], + ) + ), + ] + ) + result = await generate_structured_output( + llm, + [{"role": "user", "content": "x"}], + schema={"type": "object"}, + response_format_name="OutputSchema", + description="d", + completion_kwargs={}, + ) + assert result == {"a": 1} + assert len(llm.calls) == 2 + assert "tools" in llm.calls[1] + + +@pytest.mark.asyncio +async def test_generate_structured_output_falls_back_on_empty_content(): + # Non-OpenAI: response_format yields empty content -> fall back to tool call. + llm = _FakeLLM( + [ + _response(SimpleNamespace(content=None, tool_calls=None)), + _response( + SimpleNamespace( + content=None, + tool_calls=[SimpleNamespace(arguments={RESPONSE_KEY: {"a": 1}})], + ) + ), + ] + ) + result = await generate_structured_output( + llm, + [{"role": "user", "content": "x"}], + schema={"type": "object"}, + response_format_name="OutputSchema", + description="d", + completion_kwargs={}, + ) + assert result == {"a": 1} + assert len(llm.calls) == 2 + assert "response_format" in llm.calls[0] + assert "tools" in llm.calls[1] and "tool_choice" in llm.calls[1] + + +@pytest.mark.asyncio +async def test_generate_structured_output_falls_back_when_response_format_raises(): + # A provider that rejects response_format outright still gets a tool fallback. + llm = _FakeLLM( + [ + RuntimeError("response_format unsupported"), + _response( + SimpleNamespace( + content=None, + tool_calls=[SimpleNamespace(arguments={RESPONSE_KEY: "ok"})], + ) + ), + ] + ) + result = await generate_structured_output( + llm, + [{"role": "user", "content": "x"}], + schema={"type": "string"}, + response_format_name="OutputSchema", + description="d", + completion_kwargs={}, + ) + assert result == "ok" + assert len(llm.calls) == 2 + + +def test_build_response_tool_merges_ref_sibling_keys(): + # Pydantic can emit sibling keys (e.g. description) next to $ref; they + # must survive inlining since they guide the LLM. + schema = { + "type": "object", + "properties": { + "op": {"$ref": "#/$defs/Op", "description": "the operator to use"} + }, + "$defs": {"Op": {"type": "string", "enum": ["+", "-"]}}, + } + tool = build_response_tool(schema, description="d") + op = tool["parameters"]["properties"][RESPONSE_KEY]["properties"]["op"] + assert op == { + "type": "string", + "enum": ["+", "-"], + "description": "the operator to use", + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "model", + [ + "anthropic.claude-sonnet-4-5-20250929-v1:0", + "claude-haiku-4-5", + "gemini-2.5-pro", + ], +) +async def test_non_openai_models_use_tool_call_directly(model: str): + # Claude/Gemini don't honor response_format on the normalized gateway, so + # their strategies skip it entirely: a single forced tool call. + llm = _FakeLLM( + [ + _response( + SimpleNamespace( + content=None, + tool_calls=[SimpleNamespace(arguments={RESPONSE_KEY: "ok"})], + ) + ) + ] + ) + result = await generate_structured_output( + llm, + [{"role": "user", "content": "x"}], + schema={"type": "string"}, + response_format_name="OutputSchema", + description="d", + completion_kwargs={"model": model}, + ) + assert result == "ok" + assert len(llm.calls) == 1 + assert "tools" in llm.calls[0] and "tool_choice" in llm.calls[0] + assert "response_format" not in llm.calls[0] + + +@pytest.mark.asyncio +async def test_openai_models_prefer_response_format(): + llm = _FakeLLM([_response(SimpleNamespace(content='{"a": 1}', tool_calls=None))]) + result = await generate_structured_output( + llm, + [{"role": "user", "content": "x"}], + schema={"type": "object"}, + response_format_name="OutputSchema", + description="d", + completion_kwargs={"model": "gpt-4.1-mini-2025-04-14"}, + ) + assert result == {"a": 1} + assert len(llm.calls) == 1 + assert "response_format" in llm.calls[0] diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index f78dd4cbe..89d6d082d 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.80" +version = "2.10.81" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.62" +version = "0.1.63" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From a93a2ce3b30fcf14bb559573db8d997ca9458fbd Mon Sep 17 00:00:00 2001 From: GabrielVasilescu04 Date: Thu, 11 Jun 2026 18:58:52 +0300 Subject: [PATCH 094/121] chore: add platform mcp type (#1690) --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath-platform/src/uipath/platform/orchestrator/mcp.py | 1 + packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 50dd25553..a62398e5b 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.63" +version = "0.1.64" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/mcp.py b/packages/uipath-platform/src/uipath/platform/orchestrator/mcp.py index 9a811d876..dd96353a0 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/mcp.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/mcp.py @@ -17,6 +17,7 @@ class McpServerType(IntEnum): SelfHosted = 3 # tunnel to (externally) self-hosted server Remote = 4 # HTTP connection to remote MCP server ProcessAssistant = 5 # Dynamic user process assistant + Platform = 6 # Platform MCP server (e.g: Orchestrator, TestManager) class McpServerStatus(IntEnum): diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 2f56c1df5..3d9ac6c79 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.63" +version = "0.1.64" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 89d6d082d..62ecc13a0 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.63" +version = "0.1.64" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From dce87b22e87b7bb0b63d061b871679c2dcc6d78f Mon Sep 17 00:00:00 2001 From: AlexBizon <47317910+AlexBizon@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:39:05 +0300 Subject: [PATCH 095/121] =?UTF-8?q?docs(core):=20correct=20project-type=20?= =?UTF-8?q?marker=20and=20I/O=20typing=20for=20functions/=E2=80=A6=20(#170?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/uipath/docs/core/agents.md | 19 +++++++++---------- packages/uipath/docs/core/functions.md | 11 ++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/uipath/docs/core/agents.md b/packages/uipath/docs/core/agents.md index 5076d43b6..21b812647 100644 --- a/packages/uipath/docs/core/agents.md +++ b/packages/uipath/docs/core/agents.md @@ -91,23 +91,25 @@ The example below uses LangChain. Swap `uipath-langchain` for the framework of y ``` my-agent/ -├── main.py # agent logic +├── main.py # agent graph +├── langgraph.json # graph entry points (framework-specific) ├── pyproject.toml # project metadata and dependencies -├── uipath.json # entry point declarations ├── entry-points.json # generated — I/O JSON Schema └── bindings.json # generated — resource binding overrides ``` -### `uipath.json` +### `langgraph.json` ```json { - "agents": { - "agent": "main.py:agent" + "graphs": { + "agent": "./main.py:graph" } } ``` +Declares the agent's graph entry points. The filename is framework-specific — `langgraph.json` for LangChain/LangGraph, `llamaindex.json` for LlamaIndex, and so on. Its presence, together with the framework dependency below, is what marks the project as a coded agent. + ### `pyproject.toml` ```toml @@ -118,18 +120,15 @@ description = "..." authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.11" dependencies = ["uipath>=2.0", "uipath-langchain>=2.0"] - -[tool.uipath] -type = "agent" ``` -`[tool.uipath] type = "agent"` is required — it identifies the project as an agent to the runtime and packaging tools. +Standard metadata plus the framework dependency (`uipath-langchain` here). The framework graph file and this dependency identify the project as a coded agent — `pyproject.toml` needs no UiPath-specific entries, and `uipath.json` carries no agent entry. --- ## Input & Output -Define `Input` and `Output` as Python dataclasses, the same way as [coded functions](./functions.md#input--output): +Define `Input` and `Output` the same way as [coded functions](./functions.md#input--output) — a stdlib `@dataclass`, a pydantic `BaseModel`, or `pydantic.dataclasses.dataclass`: ```python from dataclasses import dataclass diff --git a/packages/uipath/docs/core/functions.md b/packages/uipath/docs/core/functions.md index e367270e8..6c073f68f 100644 --- a/packages/uipath/docs/core/functions.md +++ b/packages/uipath/docs/core/functions.md @@ -107,29 +107,26 @@ description = "..." authors = [{ name = "Your Name", email = "you@example.com" }] requires-python = ">=3.11" dependencies = ["uipath>=2.0"] - -[tool.uipath] -type = "function" ``` -`[tool.uipath] type = "function"` is required — it identifies the project as a function to the runtime and packaging tools. +Standard project metadata and dependencies. The `functions` map in `uipath.json` (above) is what marks the project as a coded function — `pyproject.toml` needs no UiPath-specific entries. ### Generated files | File | Purpose | |------|---------| -| `entry-points.json` | Input/output JSON Schema derived from your dataclasses — used by Maestro for variable binding | +| `entry-points.json` | Input/output JSON Schema derived from your `Input`/`Output` models — used by Maestro for variable binding | | `bindings.json` | Resource binding overrides (assets, connections, buckets) for local development | /// warning -`uipath init` executes `main.py` to derive the I/O schema. Re-run it after every change to your `Input` or `Output` dataclasses. +`uipath init` executes your entrypoint Python file(s) (as declared in `uipath.json`, e.g., `main.py`) to derive the I/O schema. Re-run it after every change to your `Input` or `Output` models. /// --- ## Input & Output -Define `Input` and `Output` as Python dataclasses. The runtime validates against these at invocation time and exports them as JSON Schema for Maestro variable binding. +Define `Input` and `Output` as typed Python — a stdlib `@dataclass`, a pydantic `BaseModel`, or `pydantic.dataclasses.dataclass`. The runtime uses these type hints to parse the invocation payload and exports them as JSON Schema for Maestro variable binding. The entry point can be a sync `def` or an `async def` — both are supported. ```python from dataclasses import dataclass, field From 7ca1d83dca086fc260696a98f53ad49177f94871 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Mon, 15 Jun 2026 11:25:18 +0300 Subject: [PATCH 096/121] ci: make dev-build PR description copy-paste ready (#1715) --- .github/workflows/publish-dev.yml | 46 +++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index 3c438f75c..d1b218f99 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -109,17 +109,53 @@ jobs: Write-Output "Package $PROJECT_NAME version set to $DEV_VERSION" + # Shared dev suffix for every package built in this run (same PR + run number) + $DEV_SUFFIX = "dev1$PADDED_PR$PADDED_RUN" + + # Intra-repo dependencies per package. A dev build of these is < the package's + # base version (PEP 440 pre-release), so it falls outside the published ">=base" + # constraint and must be forced in via [tool.uv] override-dependencies. + $internalDepsMap = @{ + "uipath" = @("uipath-platform", "uipath-core") + "uipath-platform" = @("uipath-core") + "uipath-core" = @() + } + + # Packages also published in this run (their dev builds exist on testpypi) + $changedPackages = '${{ needs.detect-changed-packages.outputs.packages }}' | ConvertFrom-Json + + $overrideDeps = @() + foreach ($dep in $internalDepsMap[$PROJECT_NAME]) { + if ($changedPackages -contains $dep) { + $depPyproj = Get-Content "../$dep/pyproject.toml" -Raw + $depBaseVersion = ($depPyproj | Select-String -Pattern '(?m)^\[(project|tool\.poetry)\][^\[]*?version\s*=\s*"([^"]*)"' -AllMatches).Matches[0].Groups[2].Value + $overrideDeps += [PSCustomObject]@{ Name = $dep; Version = "$depBaseVersion.$DEV_SUFFIX" } + } + } + + # [tool.uv.sources]: the package itself plus every overridden dep point at testpypi + $sourcesLines = @("$PROJECT_NAME = { index = `"testpypi`" }") + foreach ($d in $overrideDeps) { $sourcesLines += "$($d.Name) = { index = `"testpypi`" }" } + $sourcesBlock = $sourcesLines -join "`n" + + # Optional [tool.uv] override block (omitted when no intra-repo dep was published) + $overrideBlock = "" + if ($overrideDeps.Count -gt 0) { + $overrideItems = ($overrideDeps | ForEach-Object { "`"$($_.Name)==$($_.Version)`"" }) -join ", " + $overrideBlock = "`n[tool.uv]`noverride-dependencies = [$overrideItems]`n" + } + $dependencyMessage = @" ### $PROJECT_NAME ``````toml [project] dependencies = [ - # Exact version: + # Exact version (copy-paste ready): "$PROJECT_NAME==$DEV_VERSION", - # Any version from PR - "$PROJECT_NAME>=$MIN_VERSION,<$MAX_VERSION" + # Any version from this PR (uncomment to use a range instead): + # "$PROJECT_NAME>=$MIN_VERSION,<$MAX_VERSION", ] [[tool.uv.index]] @@ -129,8 +165,8 @@ jobs: explicit = true [tool.uv.sources] - $PROJECT_NAME = { index = "testpypi" } - `````` + $sourcesBlock + $overrideBlock`````` "@ # Get the owner and repo from the GitHub repository From 62751de723ccad14f653b463ae244e08cce3f353 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Mon, 15 Jun 2026 16:55:50 +0300 Subject: [PATCH 097/121] fix(auth): prevent oauth callback token injection (#1718) --- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_cli/_auth/_auth_server.py | 88 +++++++---- .../uipath/src/uipath/_cli/_auth/index.html | 6 + packages/uipath/tests/cli/test_auth_server.py | 138 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 5 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 packages/uipath/tests/cli/test_auth_server.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index ffaa7f881..5b63bd838 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.81" +version = "2.10.82" 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/_auth/_auth_server.py b/packages/uipath/src/uipath/_cli/_auth/_auth_server.py index 4433b1c20..673d545b3 100644 --- a/packages/uipath/src/uipath/_cli/_auth/_auth_server.py +++ b/packages/uipath/src/uipath/_cli/_auth/_auth_server.py @@ -1,4 +1,5 @@ import asyncio +import hmac import http.server import json import os @@ -10,15 +11,6 @@ PORT = 6234 -# Custom exception for token received -class TokenReceivedSignal(Exception): - """Exception raised when a token is successfully received.""" - - def __init__(self, token_data): - self.token_data = token_data - super().__init__("Token received successfully") - - def make_request_handler_class( state, code_verifier, token_callback, domain, redirect_uri, client_id ): @@ -29,12 +21,60 @@ def log_message(self, format, *args) -> None: # do nothing pass + def _is_host_allowed(self) -> bool: + """Reject requests whose Host header is not loopback. + + Defends against DNS rebinding since the legitimate flow + always lands on localhost. + """ + host = self.headers.get("Host", "") + hostname = host.rsplit(":", 1)[0] + return hostname in ("localhost", "127.0.0.1") + + def _handle_host_error(self) -> bool: + """Return True if a host error was identified and handled (403).""" + if not self._is_host_allowed(): + self.send_error(403, "Invalid host") + return True + return False + + def _state_is_valid(self) -> bool: + """Validate the OAuth state supplied.""" + received = self.headers.get("X-Auth-State", "") + return hmac.compare_digest(received, state) + + def _handle_state_error(self) -> bool: + """Return True if a state error was identified and handled (403).""" + if not self._state_is_valid(): + self.send_error(403, "Invalid or missing state") + return True + return False + + def _read_json_body(self): + """Read and parse the JSON request body. + + Returns the decoded object, or None if the + expected headers are missing or body is malformed. + """ + try: + content_length = int(self.headers["Content-Length"]) + post_data = self.rfile.read(content_length) + return json.loads(post_data.decode("utf-8")) + except (KeyError, TypeError, ValueError): + self.send_error(400, "Invalid request") + return None + def do_POST(self): """Handle POST requests to /set_token.""" + if self._handle_host_error(): + return if self.path == "/set_token": - content_length = int(self.headers["Content-Length"]) - post_data = self.rfile.read(content_length) - token_data = json.loads(post_data.decode("utf-8")) + if self._handle_state_error(): + return + + token_data = self._read_json_body() + if token_data is None: + return self.send_response(200) self.end_headers() @@ -44,9 +84,13 @@ def do_POST(self): token_callback(token_data) elif self.path == "/log": - content_length = int(self.headers["Content-Length"]) - post_data = self.rfile.read(content_length) - logs = json.loads(post_data.decode("utf-8")) + if self._handle_state_error(): + return + + logs = self._read_json_body() + if logs is None: + return + # Write logs to .uipath/.error_log file uipath_dir = os.path.join(os.getcwd(), ".uipath") os.makedirs(uipath_dir, exist_ok=True) @@ -66,6 +110,8 @@ def do_POST(self): def do_GET(self): """Handle GET requests by serving index.html.""" + if self._handle_host_error(): + return # Always serve index.html regardless of the path try: index_path = os.path.join(os.path.dirname(__file__), "index.html") @@ -86,16 +132,6 @@ def do_GET(self): except FileNotFoundError: self.send_error(404, "File not found") - def end_headers(self): - self.send_header("Access-Control-Allow-Origin", "*") - self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - self.send_header("Access-Control-Allow-Headers", "Content-Type") - super().end_headers() - - def do_OPTIONS(self): - self.send_response(200) - self.end_headers() - return SimpleHTTPSRequestHandler @@ -149,7 +185,7 @@ def create_server(self, state, code_verifier, domain): self.redirect_uri, self.client_id, ) - self.httpd = socketserver.TCPServer(("", self.port), handler) + self.httpd = socketserver.TCPServer(("127.0.0.1", self.port), handler) return self.httpd def _run_server(self): diff --git a/packages/uipath/src/uipath/_cli/_auth/index.html b/packages/uipath/src/uipath/_cli/_auth/index.html index a361e73de..08f81d81b 100644 --- a/packages/uipath/src/uipath/_cli/_auth/index.html +++ b/packages/uipath/src/uipath/_cli/_auth/index.html @@ -519,6 +519,9 @@

    Authenticate CLI

    async function sendLogs(logs) { await fetch(`${baseUrl}/log`, { method: 'POST', + headers: { + 'X-Auth-State': "__PY_REPLACE_EXPECTED_STATE__" + }, body: JSON.stringify(logs) }); } @@ -559,6 +562,9 @@

    Authenticate CLI

    await sendLogs(logs); await fetch(`${baseUrl}/set_token`, { method: 'POST', + headers: { + 'X-Auth-State': state + }, body: JSON.stringify(tokenData) }); diff --git a/packages/uipath/tests/cli/test_auth_server.py b/packages/uipath/tests/cli/test_auth_server.py new file mode 100644 index 000000000..f81d2c08b --- /dev/null +++ b/packages/uipath/tests/cli/test_auth_server.py @@ -0,0 +1,138 @@ +"""Security tests for the OAuth local callback server. + +Covers GHSA-32xc-7x5c-8vmf: the `/set_token` and `/log` endpoints must reject +unauthenticated POSTs (no / wrong OAuth `state`), and the server must bind to +loopback only rather than all interfaces. +""" + +import json +import os +import threading +import urllib.error +import urllib.request + +from uipath._cli._auth._auth_server import HTTPServer + +STATE = "LEGITIMATE_OAUTH_STATE_ABCDE12345" +CODE_VERIFIER = "LEGITIMATE_PKCE_CODE_VERIFIER" +DOMAIN = "cloud.uipath.com" + +ATTACKER_PAYLOAD = { + "access_token": "attacker-token", + "refresh_token": "attacker-refresh", + "expires_in": 3600, + "token_type": "Bearer", + "scope": "offline_access", +} + + +def _request(port, path, data, headers=None, method="POST"): + req = urllib.request.Request( + f"http://127.0.0.1:{port}{path}", + data=data, + headers=headers or {}, + method=method, + ) + try: + with urllib.request.urlopen(req, timeout=5) as resp: + return resp.status, resp.read().decode("utf-8") + except urllib.error.HTTPError as exc: + return exc.code, exc.read().decode("utf-8") + + +def _post(port, path, body, headers=None): + return _request( + port, + path, + json.dumps(body).encode("utf-8"), + {"Content-Type": "application/json", **(headers or {})}, + ) + + +async def test_endpoints_reject_unauthenticated_posts(tmp_path, monkeypatch): + """Only requests carrying the matching OAuth state are accepted. + + Exercises both /set_token and /log with missing, wrong, and valid state. + """ + monkeypatch.chdir(tmp_path) + + # Binding happens in create_server; the listen socket is up before the + # handler thread starts, so connections queue and no readiness sleep is + # needed. redirect_uri/client_id are required by the GET (index.html) path. + server = HTTPServer( + port=0, redirect_uri="http://localhost/callback", client_id="test-client" + ) + httpd = server.create_server(STATE, CODE_VERIFIER, DOMAIN) + port = httpd.server_address[1] + + results = {} + + def client(): + # DNS rebinding + results["rebind_get"] = _request( + port, "/", None, {"Host": "not-localhost.com"}, method="GET" + ) + results["rebind_post"] = _post( + port, + "/set_token", + ATTACKER_PAYLOAD, + {"X-Auth-State": STATE, "Host": "evil.com"}, + ) + # GET serves index.html with the OAuth params substituted in. + results["get"] = _request(port, "/anything", None, method="GET") + # /set_token: missing and wrong state are rejected. + results["set_missing"] = _post(port, "/set_token", ATTACKER_PAYLOAD) + results["set_wrong"] = _post( + port, "/set_token", ATTACKER_PAYLOAD, {"X-Auth-State": "not-the-state"} + ) + # Valid state but a non-JSON body -> graceful 400, not 500. + results["set_malformed"] = _request( + port, "/set_token", b"not json", {"X-Auth-State": STATE} + ) + # Unknown path -> 404. + results["unknown"] = _post(port, "/nope", {"x": 1}, {"X-Auth-State": STATE}) + # /log: missing and valid state. + results["log_missing"] = _post(port, "/log", {"msg": "x"}) + results["log_valid"] = _post( + port, "/log", {"msg": "x"}, {"X-Auth-State": STATE} + ) + # Valid /set_token last, to capture the token and unblock start(). + results["set_valid"] = _post( + port, "/set_token", {"access_token": "real"}, {"X-Auth-State": STATE} + ) + + t = threading.Thread(target=client, daemon=True) + t.start() + token_data = await server.start(STATE, CODE_VERIFIER, DOMAIN) + t.join(timeout=5) + + # DNS rebinding: forged Host is rejected on both GET and POST. + assert results["rebind_get"][0] == 403 + assert results["rebind_post"][0] == 403 + + # GET returns the page with the real state injected, placeholder gone. + assert results["get"][0] == 200 + assert STATE in results["get"][1] + assert "__PY_REPLACE_EXPECTED_STATE__" not in results["get"][1] + + assert results["set_missing"][0] == 403 + assert results["set_wrong"][0] == 403 + assert results["set_malformed"][0] == 400 + assert results["unknown"][0] == 404 + assert results["log_missing"][0] == 403 + assert results["log_valid"][0] == 200 + assert results["set_valid"][0] == 200 + + # Only the valid, state-bearing request was accepted. + assert token_data == {"access_token": "real"} + # The state-protected /log write happened for the valid request only. + assert os.path.exists(tmp_path / ".uipath" / ".error_log") + + +def test_server_binds_to_loopback_only(): + server = HTTPServer(port=0) + httpd = server.create_server(STATE, CODE_VERIFIER, DOMAIN) + try: + assert httpd.server_address[0] == "127.0.0.1" + finally: + httpd.server_close() diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 62ecc13a0..773d25d8a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.81" +version = "2.10.82" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 1c469bcac21d0c746a15275f47133cc58fe58d48 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 16 Jun 2026 10:57:18 +0300 Subject: [PATCH 098/121] docs: restructure release notes into a linked catalog (#1721) --- packages/uipath/docs/core/release_notes.md | 137 +++------------------ packages/uipath/docs/index.md | 9 -- 2 files changed, 19 insertions(+), 127 deletions(-) diff --git a/packages/uipath/docs/core/release_notes.md b/packages/uipath/docs/core/release_notes.md index 325baad92..3bdbfb4a0 100644 --- a/packages/uipath/docs/core/release_notes.md +++ b/packages/uipath/docs/core/release_notes.md @@ -1,126 +1,27 @@ -# 🚨 Breaking Changes for UiPath Python SDK (v2.2.0+) - -**Release Date:** November 26, 2025 - -Version 2.2.0 of the **UiPath Python SDK** introduces several breaking changes affecting both the SDK and CLI. - -## Breaking Changes - -### 1. Minimum Python Version: 3.11+ Required - -**What's changing:** Python 3.10 is no longer supported for `uipath-python`, `uipath-langchain-python`, `uipath-llamaindex-python`. - -**Action required:** Upgrade to Python 3.11 or higher. - -### 2. Import Path Change - -**What's changing:** The `UiPath` class has moved from `uipath` to `uipath.platform`. - -**Action required:** Update your imports: - -```python -# Before -from uipath import UiPath -from uipath.models import Job, Asset, Queue -from uipath.models import Entity - -# After -from uipath.platform import UiPath, Job, Asset, Queue - -client = UiPath(...) -``` - -### 3. Transition to LangChain v1 (for `uipath-langchain` only) - -**What's changing:** Minimum required versions are now LangChain 1.0.0+ and LangGraph 1.0.0+ - -**Action required:** Review and update your code according to the [LangChain v1 Migration Guide](https://docs.langchain.com/oss/python/migrate/langchain-v1). - -**Note:** This only applies if you're using the `uipath-langchain` package. - -### 4. Configuration Architecture Redesign - -We've restructured how UiPath projects define and manage their resources: - -**`uipath.json` - Configuration File (Updated Purpose)** -- Previously contained entrypoints and bindings; now serves as a streamlined configuration file -- For **pure Python scripts**, define entrypoints in the `functions` section: - ```json - { - "functions": { - "entrypoint1": "src/main.py:", - "entrypoint2": "src/graph.py:runtime" - } - } - ``` -- For **LangGraph graphs**, define entrypoints in `langgraph.json` (same as before) -- For **LlamaIndex workflows**, define entrypoints in `llamaindex.json` (same as before) - -**`bindings.json` - Manual Binding Definitions (New)** -- Overridable resources (bindings) now stored in a separate file -- Bindings are **no longer automatically inferred** from code -- Must be manually defined by the user for now (we're working on an interactive configurator to simplify this process) - -**`entry-points.json` - I/O Schema (New)** -- Contains the input/output schema for your entrypoints -- Automatically inferred from code based on entrypoints defined in `llamaindex.json`/`langgraph.json`/`uipath.json` - -## Migration Guide - -### Stay on v2.1.x - -To avoid these breaking changes and keep your current setup, pin your dependency in `pyproject.toml`: - -```toml -"uipath>=2.1.x,<2.2.0" -``` - -**For `uipath-langchain` users:** To stay on the current version without LangChain v1: -```toml -"uipath-langchain>=0.0.x,<0.1.0" -``` - -### Migrate to v2.2.0+ +--- +title: Release Notes +--- -1. **Upgrade to v2.2.0+** - - Update the dependencies in `pyproject.toml` with: - ```toml - "uipath>=2.2.x,<2.3.0" - ``` +# Release Notes - Bounding the version to <2.3.0 prevents future breaking changes - - **For `uipath-langchain` users:** - To migrate to LangChain v1: - ```toml - "uipath-langchain>=0.1.0,<0.2.0" - ``` - **For `uipath-langchain`/`uipath-llamaindex` users:** - Make sure to also reference `uipath` in your `pyproject.toml` - future versions will no longer reference the main `uipath` CLI package as a dependency. +A catalog of the releases most relevant to UiPath Python SDK users (breaking changes and notable updates). Full details live in each GitHub release, linked below. -2. **Upgrade the Python version to 3.11+** - - In `pyproject.toml` specify the required Python version by adding or updating the following field: - ```toml - requires-python = ">=3.11" - ``` +## `uipath` (SDK & CLI) -3. **Update imports** - - Change `from uipath import UiPath` to `from uipath.platform import UiPath`. +| Release | Date | What's relevant | Notes | +|---------|------|-----------------|-------| +| [v2.10.0](https://github.com/UiPath/uipath-python/releases/tag/v2.10.0) | 2026-02-27 | Coded function schema `type` changed from `"agent"` to `"function"` | 🚨 Breaking | +| [v2.9.0](https://github.com/UiPath/uipath-python/releases/tag/v2.9.0) | 2026-02-23 | `platform` extracted to `uipath-platform`, context grounding contract changes, `uipath dev` defaults to `web` | 🚨 Breaking | +| [v2.2.0](https://github.com/UiPath/uipath-python/releases/tag/v2.2.0) | 2025-11-26 | Python 3.11+ required, `UiPath` import moved to `uipath.platform`, configuration architecture redesign | 🚨 Breaking | -4. **Review LangChain v1 changes (if using `uipath-langchain`)** - - Review the [LangChain v1 Migration Guide](https://docs.langchain.com/oss/python/migrate/langchain-v1) and update your code accordingly. +## `uipath-langchain` -5. **Update configuration files** - - - **Define your entrypoints** in `scripts` within `uipath.json` (not applicable if you already use `langgraph.json`/`llamaindex.json`) - - **Run `uipath init`** to automatically generate the `entry-points.json` I/O schema from your configuration - - **Create `bindings.json`** and manually define all overridable resources - - **Important:** If you update your script/agent code, run `uipath init` again to regenerate the I/O schema +| Release | Date | What's relevant | Notes | +|---------|------|-----------------|-------| +| [v0.10.0](https://github.com/UiPath/uipath-langchain-python/releases/tag/v0.10.0) | 2026-04-23 | Transport/auth split into new `uipath-llm-client` and `uipath-langchain-client` packages (legacy preserved) | Non-breaking | ---- +## `uipath-runtime` -For questions or issues, please open a ticket: [UiPath Python SDK Submit Issue](https://github.com/UiPath/uipath-python/issues) \ No newline at end of file +| Release | Date | What's relevant | Notes | +|---------|------|-----------------|-------| +| [v0.3.0](https://github.com/UiPath/uipath-runtime-python/releases/tag/v0.3.0) | 2025-12-18 | `UiPathDebugBridgeProtocol` renamed to `UiPathDebugProtocol` | 🚨 Breaking (protocol implementers only) | diff --git a/packages/uipath/docs/index.md b/packages/uipath/docs/index.md index 7ec047397..1a88e6b2a 100644 --- a/packages/uipath/docs/index.md +++ b/packages/uipath/docs/index.md @@ -2,15 +2,6 @@ title: Getting Started --- -
    -- __🚨 Breaking changes__ - - --- - - UiPath Python SDK v2.2.0+ will introduce **breaking changes** starting **November 26, 2025** - [See Details](./core/release_notes.md) -
    -

    What do you want to build?

    From 99b686b23cc322edaabc835f6eafa9f75752ce6c Mon Sep 17 00:00:00 2001 From: GabrielVasilescu04 Date: Tue, 16 Jun 2026 12:49:32 +0300 Subject: [PATCH 099/121] fix(platform): expose SecretValue on assets (#1719) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/orchestrator/_assets_service.py | 130 ++++++---- .../uipath/platform/orchestrator/assets.py | 2 + .../tests/services/test_assets_service.py | 100 ++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- .../src/uipath/_resources/SDK_REFERENCE.md | 228 ++++++++++++++++-- packages/uipath/uv.lock | 4 +- 8 files changed, 400 insertions(+), 72 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index a62398e5b..a0d332607 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.64" +version = "0.1.65" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/_assets_service.py b/packages/uipath-platform/src/uipath/platform/orchestrator/_assets_service.py index 7561dd4ea..c95ad1b49 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/_assets_service.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/_assets_service.py @@ -283,6 +283,14 @@ async def retrieve_async( else: return Asset.model_validate(response.json()["value"][0]) + def _ensure_robot_context(self) -> None: + try: + is_user = self._execution_context.robot_key is not None + except ValueError: + is_user = False + if not is_user: + raise ValueError("This method can only be used for robot assets.") + @resource_override(resource_type="asset") @traced( name="assets_credential", run_type="uipath", hide_input=True, hide_output=True @@ -294,11 +302,9 @@ def retrieve_credential( folder_key: Optional[str] = None, folder_path: Optional[str] = None, ) -> Optional[str]: - """Gets a specified Orchestrator credential. - - The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable) + """Get the decrypted password of a Credential asset. - Related Activity: [Get Credential](https://docs.uipath.com/activities/other/latest/workflow/get-robot-credential) + The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable). Args: name (str): The name of the credential asset. @@ -309,22 +315,10 @@ def retrieve_credential( Optional[str]: The decrypted credential password. Raises: - ValueError: If the method is called for a user asset. + ValueError: If called outside a robot context (no `UIPATH_ROBOT_KEY`). """ - try: - is_user = self._execution_context.robot_key is not None - except ValueError: - is_user = False - - if not is_user: - raise ValueError("This method can only be used for robot assets.") - - spec = self._retrieve_spec( - name, - folder_key=folder_key, - folder_path=folder_path, - ) - + self._ensure_robot_context() + spec = self._retrieve_spec(name, folder_key=folder_key, folder_path=folder_path) response = self.request( spec.method, url=spec.endpoint, @@ -333,10 +327,7 @@ def retrieve_credential( content=spec.content, headers=spec.headers, ) - - user_asset = UserAsset.model_validate(response.json()) - - return user_asset.credential_password + return UserAsset.model_validate(response.json()).credential_password @resource_override(resource_type="asset") @traced( @@ -349,11 +340,9 @@ async def retrieve_credential_async( folder_key: Optional[str] = None, folder_path: Optional[str] = None, ) -> Optional[str]: - """Asynchronously gets a specified Orchestrator credential. + """Asynchronously get the decrypted password of a Credential asset. - The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable) - - Related Activity: [Get Credential](https://docs.uipath.com/activities/other/latest/workflow/get-robot-credential) + The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable). Args: name (str): The name of the credential asset. @@ -364,23 +353,47 @@ async def retrieve_credential_async( Optional[str]: The decrypted credential password. Raises: - ValueError: If the method is called for a user asset. + ValueError: If called outside a robot context (no `UIPATH_ROBOT_KEY`). """ - try: - is_user = self._execution_context.robot_key is not None - except ValueError: - is_user = False + self._ensure_robot_context() + spec = self._retrieve_spec(name, folder_key=folder_key, folder_path=folder_path) + response = await self.request_async( + spec.method, + url=spec.endpoint, + params=spec.params, + json=spec.json, + content=spec.content, + headers=spec.headers, + ) + return UserAsset.model_validate(response.json()).credential_password - if not is_user: - raise ValueError("This method can only be used for robot assets.") + @resource_override(resource_type="asset") + @traced(name="assets_secret", run_type="uipath", hide_input=True, hide_output=True) + def retrieve_secret( + self, + name: str, + *, + folder_key: Optional[str] = None, + folder_path: Optional[str] = None, + ) -> Optional[str]: + """Get the decrypted value of a Secret asset. - spec = self._retrieve_spec( - name, - folder_key=folder_key, - folder_path=folder_path, - ) + The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable). - response = await self.request_async( + Args: + name (str): The name of the secret asset. + folder_key (Optional[str]): The key of the folder to execute the process in. Override the default one set in the SDK config. + folder_path (Optional[str]): The path of the folder to execute the process in. Override the default one set in the SDK config. + + Returns: + Optional[str]: The decrypted secret value. + + Raises: + ValueError: If called outside a robot context (no `UIPATH_ROBOT_KEY`). + """ + self._ensure_robot_context() + spec = self._retrieve_spec(name, folder_key=folder_key, folder_path=folder_path) + response = self.request( spec.method, url=spec.endpoint, params=spec.params, @@ -388,10 +401,43 @@ async def retrieve_credential_async( content=spec.content, headers=spec.headers, ) + return UserAsset.model_validate(response.json()).secret_value - user_asset = UserAsset.model_validate(response.json()) + @resource_override(resource_type="asset") + @traced(name="assets_secret", run_type="uipath", hide_input=True, hide_output=True) + async def retrieve_secret_async( + self, + name: str, + *, + folder_key: Optional[str] = None, + folder_path: Optional[str] = None, + ) -> Optional[str]: + """Asynchronously get the decrypted value of a Secret asset. + + The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable). + + Args: + name (str): The name of the secret asset. + folder_key (Optional[str]): The key of the folder to execute the process in. Override the default one set in the SDK config. + folder_path (Optional[str]): The path of the folder to execute the process in. Override the default one set in the SDK config. + + Returns: + Optional[str]: The decrypted secret value. - return user_asset.credential_password + Raises: + ValueError: If called outside a robot context (no `UIPATH_ROBOT_KEY`). + """ + self._ensure_robot_context() + spec = self._retrieve_spec(name, folder_key=folder_key, folder_path=folder_path) + response = await self.request_async( + spec.method, + url=spec.endpoint, + params=spec.params, + json=spec.json, + content=spec.content, + headers=spec.headers, + ) + return UserAsset.model_validate(response.json()).secret_value @traced(name="assets_update", run_type="uipath", hide_input=True, hide_output=True) def update( diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/assets.py b/packages/uipath-platform/src/uipath/platform/orchestrator/assets.py index 6ee89e806..056420225 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/assets.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/assets.py @@ -38,6 +38,7 @@ class UserAsset(BaseModel): int_value: Optional[int] = Field(default=None, alias="IntValue") credential_username: Optional[str] = Field(default=None, alias="CredentialUsername") credential_password: Optional[str] = Field(default=None, alias="CredentialPassword") + secret_value: Optional[str] = Field(default=None, alias="SecretValue") external_name: Optional[str] = Field(default=None, alias="ExternalName") credential_store_id: Optional[int] = Field(default=None, alias="CredentialStoreId") key_value_list: Optional[List[Dict[str, str]]] = Field( @@ -69,5 +70,6 @@ class Asset(BaseModel): int_value: Optional[int] = Field(default=None, alias="IntValue") credential_username: Optional[str] = Field(default=None, alias="CredentialUsername") credential_password: Optional[str] = Field(default=None, alias="CredentialPassword") + secret_value: Optional[str] = Field(default=None, alias="SecretValue") external_name: Optional[str] = Field(default=None, alias="ExternalName") credential_store_id: Optional[int] = Field(default=None, alias="CredentialStoreId") diff --git a/packages/uipath-platform/tests/services/test_assets_service.py b/packages/uipath-platform/tests/services/test_assets_service.py index 6e83c3b9d..bf28a6afc 100644 --- a/packages/uipath-platform/tests/services/test_assets_service.py +++ b/packages/uipath-platform/tests/services/test_assets_service.py @@ -417,6 +417,106 @@ async def test_retrieve_credential_async( == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.AssetsService.retrieve_credential_async/{version}" ) + def test_retrieve_secret( + self, + httpx_mock: HTTPXMock, + service: AssetsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """retrieve_secret returns SecretValue for Secret-type assets.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAssetByNameForRobotKey", + status_code=200, + json={ + "Id": 1, + "Name": "Test Secret", + "ValueType": "Secret", + "SecretValue": "super-secret-value", + }, + ) + + secret = service.retrieve_secret(name="Test Secret") + + assert secret == "super-secret-value" + + async def test_retrieve_secret_async( + self, + httpx_mock: HTTPXMock, + service: AssetsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """retrieve_secret_async returns SecretValue for Secret-type assets.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAssetByNameForRobotKey", + status_code=200, + json={ + "Id": 1, + "Name": "Test Secret", + "ValueType": "Secret", + "SecretValue": "super-secret-value", + }, + ) + + secret = await service.retrieve_secret_async(name="Test Secret") + + assert secret == "super-secret-value" + + def test_retrieve_robot_asset_exposes_secret_value( + self, + httpx_mock: HTTPXMock, + service: AssetsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """`retrieve` must expose SecretValue on UserAsset for Secret-type assets.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAssetByNameForRobotKey", + status_code=200, + json={ + "Id": 1, + "Name": "Test Secret", + "ValueType": "Secret", + "SecretValue": "super-secret-value", + }, + ) + + asset = service.retrieve(name="Test Secret") + + assert isinstance(asset, UserAsset) + assert asset.value_type == "Secret" + assert asset.secret_value == "super-secret-value" + + async def test_retrieve_async_robot_asset_exposes_secret_value( + self, + httpx_mock: HTTPXMock, + service: AssetsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """`retrieve_async` must expose SecretValue on UserAsset for Secret-type assets.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAssetByNameForRobotKey", + status_code=200, + json={ + "Id": 1, + "Name": "Test Secret", + "ValueType": "Secret", + "SecretValue": "super-secret-value", + }, + ) + + asset = await service.retrieve_async(name="Test Secret") + + assert isinstance(asset, UserAsset) + assert asset.value_type == "Secret" + assert asset.secret_value == "super-secret-value" + def test_update( self, httpx_mock: HTTPXMock, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 3d9ac6c79..689662d4d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.64" +version = "0.1.65" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 5b63bd838..c04ca9d6c 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.82" +version = "2.10.83" 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" dependencies = [ "uipath-core>=0.5.17, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.63, <0.2.0", + "uipath-platform>=0.1.65, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md index d92dcfded..02e9c0676 100644 --- a/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md +++ b/packages/uipath/src/uipath/_resources/SDK_REFERENCE.md @@ -62,12 +62,18 @@ sdk.assets.retrieve(name: str, folder_key: Optional[str]=None, folder_path: Opti # Asynchronously retrieve an asset by its name. sdk.assets.retrieve_async(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.orchestrator.assets.UserAsset | uipath.platform.orchestrator.assets.Asset -# Gets a specified Orchestrator credential. +# Get the decrypted value of a Secret asset. sdk.assets.retrieve_credential(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.Optional[str] -# Asynchronously gets a specified Orchestrator credential. +# Asynchronously get the decrypted value of a Secret asset. sdk.assets.retrieve_credential_async(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.Optional[str] +# Get the decrypted value of a Secret asset. +sdk.assets.retrieve_secret(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.Optional[str] + +# Asynchronously get the decrypted value of a Secret asset. +sdk.assets.retrieve_secret_async(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.Optional[str] + # Update an asset's value. sdk.assets.update(robot_asset: uipath.platform.orchestrator.assets.UserAsset, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> httpx.Response @@ -113,6 +119,19 @@ sdk.attachments.upload_async(name: str, content: str | bytes | None=None, source ``` +### Automation Ops + +Automation Ops service + +```python +# Retrieve the deployed policy. +sdk.automation_ops.get_deployed_policy() -> dict[str, typing.Any] + +# Retrieve the deployed policy (async). +sdk.automation_ops.get_deployed_policy_async() -> dict[str, typing.Any] + +``` + ### Automation Tracker Automation Tracker service @@ -272,10 +291,10 @@ sdk.context_grounding.add_to_index(name: str, blob_file_path: str, content_type: sdk.context_grounding.add_to_index_async(name: str, blob_file_path: str, content_type: Optional[str]=None, content: Union[str, bytes, NoneType]=None, source_path: Optional[str]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, ingest_data: bool=True) -> None # Create a new ephemeral context grounding index. -sdk.context_grounding.create_ephemeral_index(usage: uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex +sdk.context_grounding.create_ephemeral_index(usage: uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex # Create a new ephemeral context grounding index. -sdk.context_grounding.create_ephemeral_index_async(usage: uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex +sdk.context_grounding.create_ephemeral_index_async(usage: uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex # Create a new context grounding index. sdk.context_grounding.create_index(name: str, source: Union[uipath.platform.context_grounding.context_grounding_payloads.BucketSourceConfig, uipath.platform.context_grounding.context_grounding_payloads.GoogleDriveSourceConfig, uipath.platform.context_grounding.context_grounding_payloads.DropboxSourceConfig, uipath.platform.context_grounding.context_grounding_payloads.OneDriveSourceConfig, uipath.platform.context_grounding.context_grounding_payloads.ConfluenceSourceConfig], description: Optional[str]=None, extraction_strategy: Optional[str]=None, embeddings_enabled: Optional[bool]=None, is_encrypted: Optional[bool]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex @@ -326,7 +345,7 @@ sdk.context_grounding.list_indexes(folder_key: Optional[str]=None, folder_path: sdk.context_grounding.list_indexes_async(folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> typing.List[uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex] # Retrieve context grounding index information by its name. -sdk.context_grounding.retrieve(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex +sdk.context_grounding.retrieve(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None, include_system_indexes: bool=False) -> uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex # Retrieve all context grounding indexes across all folders. sdk.context_grounding.retrieve_across_folders(name: Optional[str]=None) -> typing.List[uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex] @@ -335,7 +354,7 @@ sdk.context_grounding.retrieve_across_folders(name: Optional[str]=None) -> typin sdk.context_grounding.retrieve_across_folders_async(name: Optional[str]=None) -> typing.List[uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex] # Asynchronously retrieve context grounding index information by its name. -sdk.context_grounding.retrieve_async(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex +sdk.context_grounding.retrieve_async(name: str, folder_key: Optional[str]=None, folder_path: Optional[str]=None, include_system_indexes: bool=False) -> uipath.platform.context_grounding.context_grounding_index.ContextGroundingIndex # Retrieves a Batch Transform task status. sdk.context_grounding.retrieve_batch_transform(id: str, index_name: str | None=None) -> uipath.platform.context_grounding.context_grounding.BatchTransformResponse @@ -386,10 +405,10 @@ sdk.context_grounding.start_deep_rag_ephemeral(name: str, prompt: Annotated[str, sdk.context_grounding.start_deep_rag_ephemeral_async(name: str, prompt: Annotated[str, FieldInfo(annotation=NoneType, required=True, metadata=[MaxLen(max_length=250000)])], glob_pattern: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='*', metadata=[MaxLen(max_length=512)])]="**", citation_mode: uipath.platform.context_grounding.context_grounding.DeepRagCreationResponse # Perform a unified search on a context grounding index. -sdk.context_grounding.unified_search(name: str, query: str, search_mode: uipath.platform.context_grounding.context_grounding.UnifiedQueryResult +sdk.context_grounding.unified_search(name: str, query: str, search_mode: uipath.platform.context_grounding.context_grounding.UnifiedQueryResult # Asynchronously perform a unified search on a context grounding index. -sdk.context_grounding.unified_search_async(name: str, query: str, search_mode: uipath.platform.context_grounding.context_grounding.UnifiedQueryResult +sdk.context_grounding.unified_search_async(name: str, query: str, search_mode: uipath.platform.context_grounding.context_grounding.UnifiedQueryResult ``` @@ -475,17 +494,77 @@ sdk.documents.start_ixp_extraction_validation_async(extraction_response: uipath. Entities service ```python +# Create a new entity with the given schema and return its id. +sdk.entities.create_entity(name: str, fields: List[uipath.platform.entities.entities.EntityCreateFieldOptions], options: Optional[uipath.platform.entities.entities.EntityCreateOptions]=None) -> str + +# Asynchronously create a new entity with the given schema. +sdk.entities.create_entity_async(name: str, fields: List[uipath.platform.entities.entities.EntityCreateFieldOptions], options: Optional[uipath.platform.entities.entities.EntityCreateOptions]=None) -> str + +# Remove the file attached to a File-type field on a record. +sdk.entities.delete_attachment(entity_id: str, record_id: str, field_name: str, expansion_level: Optional[int]=None) -> typing.Dict[str, typing.Any] + +# Asynchronously remove the file attached to a File-type field. +sdk.entities.delete_attachment_async(entity_id: str, record_id: str, field_name: str, expansion_level: Optional[int]=None) -> typing.Dict[str, typing.Any] + +# Delete an entity and all of its records. +sdk.entities.delete_entity(entity_id: str) -> None + +# Asynchronously delete an entity and all of its records. +sdk.entities.delete_entity_async(entity_id: str) -> None + +# Delete a single record by id. +sdk.entities.delete_record(entity_key: str, record_id: str) -> None + +# Asynchronously delete a single record by id. +sdk.entities.delete_record_async(entity_key: str, record_id: str) -> None + # Delete multiple records from an entity in a single batch operation. -sdk.entities.delete_records(entity_key: str, record_ids: List[str]) -> uipath.platform.entities.entities.EntityRecordsBatchResponse +sdk.entities.delete_records(entity_key: str, record_ids: List[str], fail_on_first: Optional[bool]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse # Asynchronously delete multiple records from an entity in a single batch operation. -sdk.entities.delete_records_async(entity_key: str, record_ids: List[str]) -> uipath.platform.entities.entities.EntityRecordsBatchResponse +sdk.entities.delete_records_async(entity_key: str, record_ids: List[str], fail_on_first: Optional[bool]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse + +# Download a file attached to a record and return its raw bytes. +sdk.entities.download_attachment(entity_id: str, record_id: str, field_name: str) -> bytes + +# Asynchronously download a file attached to a record. +sdk.entities.download_attachment_async(entity_id: str, record_id: str, field_name: str) -> bytes + +# Get the values of a choice set by its ID. +sdk.entities.get_choiceset_values(choiceset_id: str, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.ChoiceSetValue] + +# Asynchronously get the values of a choice set by its ID. +sdk.entities.get_choiceset_values_async(choiceset_id: str, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.ChoiceSetValue] + +# Fetch a single entity record by its id. +sdk.entities.get_record(entity_key: str, record_id: str, expansion_level: Optional[int]=None) -> uipath.platform.entities.entities.EntityRecord + +# Asynchronously fetch a single entity record by its id. +sdk.entities.get_record_async(entity_key: str, record_id: str, expansion_level: Optional[int]=None) -> uipath.platform.entities.entities.EntityRecord + +# Bulk-import records into an entity from a CSV file. +sdk.entities.import_records(entity_id: str, file: Union[bytes, bytearray, memoryview, NoneType]=None, file_path: Optional[str]=None) -> uipath.platform.entities.entities.EntityImportRecordsResponse + +# Asynchronously bulk-import records into an entity from a CSV file. +sdk.entities.import_records_async(entity_id: str, file: Union[bytes, bytearray, memoryview, NoneType]=None, file_path: Optional[str]=None) -> uipath.platform.entities.entities.EntityImportRecordsResponse + +# Insert a single record into an entity and return the inserted row. +sdk.entities.insert_record(entity_key: str, data: Any, expansion_level: Optional[int]=None) -> uipath.platform.entities.entities.EntityRecord + +# Asynchronously insert a single record into an entity. +sdk.entities.insert_record_async(entity_key: str, data: Any, expansion_level: Optional[int]=None) -> uipath.platform.entities.entities.EntityRecord # Insert multiple records into an entity in a single batch operation. -sdk.entities.insert_records(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse +sdk.entities.insert_records(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None, expansion_level: Optional[int]=None, fail_on_first: Optional[bool]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse # Asynchronously insert multiple records into an entity in a single batch operation. -sdk.entities.insert_records_async(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse +sdk.entities.insert_records_async(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None, expansion_level: Optional[int]=None, fail_on_first: Optional[bool]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse + +# List all choice sets in Data Service. +sdk.entities.list_choicesets() -> typing.List[uipath.platform.entities.entities.Entity] + +# Asynchronously list all choice sets in Data Service. +sdk.entities.list_choicesets_async() -> typing.List[uipath.platform.entities.entities.Entity] # List all entities in Data Service. sdk.entities.list_entities() -> typing.List[uipath.platform.entities.entities.Entity] @@ -494,10 +573,10 @@ sdk.entities.list_entities() -> typing.List[uipath.platform.entities.entities.En sdk.entities.list_entities_async() -> typing.List[uipath.platform.entities.entities.Entity] # List records from an entity with optional pagination and schema validation. -sdk.entities.list_records(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.EntityRecord] +sdk.entities.list_records(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None, expansion_level: Optional[int]=None, filter: Optional[str]=None, orderby: Optional[str]=None, select: Optional[List[str]]=None, expand: Optional[List[str]]=None) -> uipath.platform.entities.entities.EntityRecordsListResponse # Asynchronously list records from an entity with optional pagination and schema validation. -sdk.entities.list_records_async(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> typing.List[uipath.platform.entities.entities.EntityRecord] +sdk.entities.list_records_async(entity_key: str, schema: Optional[Type[Any]]=None, start: Optional[int]=None, limit: Optional[int]=None, expansion_level: Optional[int]=None, filter: Optional[str]=None, orderby: Optional[str]=None, select: Optional[List[str]]=None, expand: Optional[List[str]]=None) -> uipath.platform.entities.entities.EntityRecordsListResponse # Query entity records using a validated SQL query. sdk.entities.query_entity_records(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] @@ -506,10 +585,10 @@ sdk.entities.query_entity_records(sql_query: str) -> typing.List[typing.Dict[str sdk.entities.query_entity_records_async(sql_query: str) -> typing.List[typing.Dict[str, typing.Any]] # Resolve an agent entity set, applying resource overwrites. -sdk.entities.resolve_entity_set(items: list[uipath.platform.entities.entities.DataFabricEntityItem]) -> uipath.platform.entities.entities.EntitySetResolution +sdk.entities.resolve_entity_set(items: List[uipath.platform.entities.entities.DataFabricEntityItem]) -> uipath.platform.entities.entities.EntitySetResolution # Resolve an agent entity set, applying resource overwrites. -sdk.entities.resolve_entity_set_async(items: list[uipath.platform.entities.entities.DataFabricEntityItem]) -> uipath.platform.entities.entities.EntitySetResolution +sdk.entities.resolve_entity_set_async(items: List[uipath.platform.entities.entities.DataFabricEntityItem]) -> uipath.platform.entities.entities.EntitySetResolution # Retrieve an entity by its key. sdk.entities.retrieve(entity_key: str) -> uipath.platform.entities.entities.Entity @@ -523,12 +602,38 @@ sdk.entities.retrieve_by_name(entity_name: str, folder_key: Optional[str]=None) # Asynchronously retrieve an entity by its name. sdk.entities.retrieve_by_name_async(entity_name: str, folder_key: Optional[str]=None) -> uipath.platform.entities.entities.Entity +# Retrieve records with structured filters, sorting, expansion, joins, and aggregates. +sdk.entities.retrieve_records(entity_key: str, filter_group: Optional[uipath.platform.entities.entities.EntityQueryFilterGroup]=None, sort_options: Optional[List[uipath.platform.entities.entities.EntityQuerySortOption]]=None, selected_fields: Optional[List[str]]=None, expansions: Optional[List[Any]]=None, expansion_level: Optional[int]=None, aggregates: Optional[List[uipath.platform.entities.entities.EntityAggregate]]=None, group_by: Optional[List[str]]=None, joins: Optional[List[uipath.platform.entities.entities.EntityJoin]]=None, binnings: Optional[List[uipath.platform.entities.entities.EntityBinning]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> uipath.platform.entities.entities.RetrieveEntityRecordsResponse + +# Asynchronously retrieve records with structured filters, sorting, expansion, joins, and aggregates. +sdk.entities.retrieve_records_async(entity_key: str, filter_group: Optional[uipath.platform.entities.entities.EntityQueryFilterGroup]=None, sort_options: Optional[List[uipath.platform.entities.entities.EntityQuerySortOption]]=None, selected_fields: Optional[List[str]]=None, expansions: Optional[List[Any]]=None, expansion_level: Optional[int]=None, aggregates: Optional[List[uipath.platform.entities.entities.EntityAggregate]]=None, group_by: Optional[List[str]]=None, joins: Optional[List[uipath.platform.entities.entities.EntityJoin]]=None, binnings: Optional[List[uipath.platform.entities.entities.EntityBinning]]=None, start: Optional[int]=None, limit: Optional[int]=None) -> uipath.platform.entities.entities.RetrieveEntityRecordsResponse + +# Update an entity's display name, description, and/or RBAC flag. +sdk.entities.update_entity_metadata(entity_id: str, metadata: Union[uipath.platform.entities.entities.EntityMetadataUpdateOptions, Dict[str, Any]]) -> None + +# Asynchronously update an entity's display name, description, and/or RBAC flag. +sdk.entities.update_entity_metadata_async(entity_id: str, metadata: Union[uipath.platform.entities.entities.EntityMetadataUpdateOptions, Dict[str, Any]]) -> None + +# Update a single record by id and return the updated row. +sdk.entities.update_record(entity_key: str, record_id: str, data: Any, expansion_level: Optional[int]=None) -> uipath.platform.entities.entities.EntityRecord + +# Asynchronously update a single record by id. +sdk.entities.update_record_async(entity_key: str, record_id: str, data: Any, expansion_level: Optional[int]=None) -> uipath.platform.entities.entities.EntityRecord + # Update multiple records in an entity in a single batch operation. -sdk.entities.update_records(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse +sdk.entities.update_records(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None, expansion_level: Optional[int]=None, fail_on_first: Optional[bool]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse # Asynchronously update multiple records in an entity in a single batch operation. -sdk.entities.update_records_async(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse +sdk.entities.update_records_async(entity_key: str, records: List[Any], schema: Optional[Type[Any]]=None, expansion_level: Optional[int]=None, fail_on_first: Optional[bool]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse +# Upload a file attachment to a File-type field on a record. +sdk.entities.upload_attachment(entity_id: str, record_id: str, field_name: str, file: Union[bytes, bytearray, memoryview, NoneType]=None, file_path: Optional[str]=None, expansion_level: Optional[int]=None) -> typing.Dict[str, typing.Any] + +# Asynchronously upload a file attachment to a File-type field on a record. +sdk.entities.upload_attachment_async(entity_id: str, record_id: str, field_name: str, file: Union[bytes, bytearray, memoryview, NoneType]=None, file_path: Optional[str]=None, expansion_level: Optional[int]=None) -> typing.Dict[str, typing.Any] + +# Parse a batch response, optionally validating success records against ``schema``. +sdk.entities.validate_entity_batch(batch_response: httpx.Response, schema: Optional[Type[Any]]=None) -> uipath.platform.entities.entities.EntityRecordsBatchResponse ``` @@ -632,6 +737,12 @@ sdk.jobs.retrieve_api_payload_async(inbox_id: str) -> typing.Any # Asynchronously retrieve a job identified by its key. sdk.jobs.retrieve_async(job_key: str, folder_key: str | None=None, folder_path: str | None=None, process_name: str | None=None) -> uipath.platform.orchestrator.job.Job +# Fetch payload data for Integration Services (Inbox) triggers. +sdk.jobs.retrieve_inbox_payload(inbox_id: str) -> typing.Any + +# Asynchronously fetch payload data for Integration Services (Inbox) triggers. +sdk.jobs.retrieve_inbox_payload_async(inbox_id: str) -> typing.Any + # Stop one or more jobs with specified strategy. sdk.jobs.stop(job_keys: List[str], strategy: str="SoftStop", folder_path: Optional[str]=None, folder_key: Optional[str]=None) -> None @@ -646,7 +757,7 @@ Llm service ```python # Generate chat completions using UiPath's normalized LLM Gateway API. -sdk.llm.chat_completions(messages: list[dict[str, str]] | list[tuple[str, str]], model: str="gpt-4.1-mini-2025-04-14", max_tokens: int=4096, temperature: float=0, n: int=1, frequency_penalty: float=0, presence_penalty: float=0, top_p: float | None=1, top_k: int | None=None, tools: list[uipath.platform.chat.llm_gateway.ToolDefinition] | None=None, tool_choice: Union[uipath.platform.chat.llm_gateway.AutoToolChoice, uipath.platform.chat.llm_gateway.RequiredToolChoice, uipath.platform.chat.llm_gateway.SpecificToolChoice, Literal['auto', 'none'], NoneType]=None, response_format: dict[str, Any] | type[pydantic.main.BaseModel] | None=None, api_version: str="2024-08-01-preview") +sdk.llm.chat_completions(messages: list[dict[str, str]] | list[tuple[str, str]], model: str="gpt-4.1-mini-2025-04-14", max_tokens: int=4096, temperature: float=0, n: int=1, frequency_penalty: float=0, presence_penalty: float=0, top_p: float | None=1, top_k: int | None=None, tools: list[uipath.platform.chat.llm_gateway.ToolDefinition | dict[str, Any]] | None=None, tool_choice: Union[uipath.platform.chat.llm_gateway.AutoToolChoice, uipath.platform.chat.llm_gateway.RequiredToolChoice, uipath.platform.chat.llm_gateway.SpecificToolChoice, Literal['auto', 'none'], NoneType]=None, response_format: dict[str, Any] | type[pydantic.main.BaseModel] | None=None, api_version: str="2024-08-01-preview") ``` @@ -682,6 +793,43 @@ sdk.mcp.retrieve_async(slug: str, folder_path: str | None=None) -> uipath.platfo ``` +### Memory + +Memory service + +```python +# Create a new memory space. +sdk.memory.create(name: str, description: Optional[str]=None, is_encrypted: Optional[bool]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.memory.memory.MemorySpace + +# Asynchronously create a new memory space. +sdk.memory.create_async(name: str, description: Optional[str]=None, is_encrypted: Optional[bool]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.memory.memory.MemorySpace + +# Ingest a resolved escalation outcome into memory. +sdk.memory.escalation_ingest(memory_space_id: str, request: uipath.platform.memory.memory.EscalationMemoryIngestRequest, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> None + +# Asynchronously ingest a resolved escalation outcome into memory. +sdk.memory.escalation_ingest_async(memory_space_id: str, request: uipath.platform.memory.memory.EscalationMemoryIngestRequest, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> None + +# Search escalation memory for previously resolved outcomes. +sdk.memory.escalation_search(memory_space_id: str, request: uipath.platform.memory.memory.MemorySearchRequest, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.memory.memory.EscalationMemorySearchResponse + +# Asynchronously search escalation memory for previously resolved outcomes. +sdk.memory.escalation_search_async(memory_space_id: str, request: uipath.platform.memory.memory.MemorySearchRequest, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.memory.memory.EscalationMemorySearchResponse + +# List memory spaces with optional OData query parameters. +sdk.memory.list(filter: Optional[str]=None, orderby: Optional[str]=None, top: Optional[int]=None, skip: Optional[int]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.memory.memory.MemorySpaceListResponse + +# Asynchronously list memory spaces. +sdk.memory.list_async(filter: Optional[str]=None, orderby: Optional[str]=None, top: Optional[int]=None, skip: Optional[int]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.memory.memory.MemorySpaceListResponse + +# Search a memory space via LLMOps. +sdk.memory.search(memory_space_id: str, request: uipath.platform.memory.memory.MemorySearchRequest, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.memory.memory.MemorySearchResponse + +# Asynchronously search a memory space via LLMOps. +sdk.memory.search_async(memory_space_id: str, request: uipath.platform.memory.memory.MemorySearchRequest, folder_key: Optional[str]=None, folder_path: Optional[str]=None) -> uipath.platform.memory.memory.MemorySearchResponse + +``` + ### Orchestrator Setup Orchestrator Setup service @@ -695,16 +843,29 @@ sdk.orchestrator_setup.enable_first_run_async() -> None ``` +### Pii Detection + +Pii Detection service + +```python +# Detect PII in the provided documents and/or files. +sdk.pii_detection.detect_pii(request: uipath.platform.pii_detection.pii_detection.PiiDetectionRequest) -> uipath.platform.pii_detection.pii_detection.PiiDetectionResponse + +# Detect PII in the provided documents and/or files (async). +sdk.pii_detection.detect_pii_async(request: uipath.platform.pii_detection.pii_detection.PiiDetectionRequest) -> uipath.platform.pii_detection.pii_detection.PiiDetectionResponse + +``` + ### Processes Processes service ```python # Start execution of a process by its name. -sdk.processes.invoke(name: str, input_arguments: Optional[Dict[str, Any]]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, attachments: Optional[list[uipath.platform.attachments.attachments.Attachment]]=None, parent_operation_id: Optional[str]=None, **kwargs) -> uipath.platform.orchestrator.job.Job +sdk.processes.invoke(name: str, input_arguments: Optional[Dict[str, Any]]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, attachments: Optional[list[uipath.platform.attachments.attachments.Attachment]]=None, parent_operation_id: Optional[str]=None, run_as_me: Optional[bool]=None, **kwargs) -> uipath.platform.orchestrator.job.Job # Asynchronously start execution of a process by its name. -sdk.processes.invoke_async(name: str, input_arguments: Optional[Dict[str, Any]]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, attachments: Optional[list[uipath.platform.attachments.attachments.Attachment]]=None, parent_operation_id: Optional[str]=None, **kwargs) -> uipath.platform.orchestrator.job.Job +sdk.processes.invoke_async(name: str, input_arguments: Optional[Dict[str, Any]]=None, folder_key: Optional[str]=None, folder_path: Optional[str]=None, attachments: Optional[list[uipath.platform.attachments.attachments.Attachment]]=None, parent_operation_id: Optional[str]=None, run_as_me: Optional[bool]=None, **kwargs) -> uipath.platform.orchestrator.job.Job ``` @@ -779,19 +940,32 @@ Resource Catalog service sdk.resource_catalog.list(resource_types: Optional[List[uipath.platform.resource_catalog.resource_catalog.ResourceType]]=None, resource_sub_types: Optional[List[str]]=None, folder_path: Optional[str]=None, folder_key: Optional[str]=None, page_size: int=20) -> typing.Iterator[uipath.platform.resource_catalog.resource_catalog.Resource] # Asynchronously get tenant scoped resources and folder scoped resources (accessible to the user). -sdk.resource_catalog.list_async(resource_types: Optional[List[uipath.platform.resource_catalog.resource_catalog.ResourceType]]=None, resource_sub_types: Optional[List[str]]=None, folder_path: Optional[str]=None, folder_key: Optional[str]=None, page_size: int=20) -> typing.AsyncIterator[uipath.platform.resource_catalog.resource_catalog.Resource] +sdk.resource_catalog.list_async(resource_types: Optional[List[uipath.platform.resource_catalog.resource_catalog.ResourceType]]=None, resource_sub_types: Optional[List[str]]=None, folder_path: Optional[str]=None, folder_key: Optional[str]=None, page_size: int=20) -> typing.AsyncGenerator[uipath.platform.resource_catalog.resource_catalog.Resource, NoneType] # Get resources of a specific type (tenant scoped or folder scoped). sdk.resource_catalog.list_by_type(resource_type: typing.Iterator[uipath.platform.resource_catalog.resource_catalog.Resource] # Asynchronously get resources of a specific type (tenant scoped or folder scoped). -sdk.resource_catalog.list_by_type_async(resource_type: typing.AsyncIterator[uipath.platform.resource_catalog.resource_catalog.Resource] +sdk.resource_catalog.list_by_type_async(resource_type: typing.AsyncGenerator[uipath.platform.resource_catalog.resource_catalog.Resource, NoneType] # Search for tenant scoped resources and folder scoped resources (accessible to the user). sdk.resource_catalog.search(name: Optional[str]=None, resource_types: Optional[List[uipath.platform.resource_catalog.resource_catalog.ResourceType]]=None, resource_sub_types: Optional[List[str]]=None, page_size: int=20) -> typing.Iterator[uipath.platform.resource_catalog.resource_catalog.Resource] # Asynchronously search for tenant scoped resources and folder scoped resources (accessible to the user). -sdk.resource_catalog.search_async(name: Optional[str]=None, resource_types: Optional[List[uipath.platform.resource_catalog.resource_catalog.ResourceType]]=None, resource_sub_types: Optional[List[str]]=None, page_size: int=20) -> typing.AsyncIterator[uipath.platform.resource_catalog.resource_catalog.Resource] +sdk.resource_catalog.search_async(name: Optional[str]=None, resource_types: Optional[List[uipath.platform.resource_catalog.resource_catalog.ResourceType]]=None, resource_sub_types: Optional[List[str]]=None, page_size: int=20) -> typing.AsyncGenerator[uipath.platform.resource_catalog.resource_catalog.Resource, NoneType] + +``` + +### Semantic Proxy + +Semantic Proxy service + +```python +# Detect PII in the provided documents and/or files. +sdk.semantic_proxy.detect_pii(request: uipath.platform.semantic_proxy.semantic_proxy.PiiDetectionRequest) -> uipath.platform.semantic_proxy.semantic_proxy.PiiDetectionResponse + +# Detect PII in the provided documents and/or files (async). +sdk.semantic_proxy.detect_pii_async(request: uipath.platform.semantic_proxy.semantic_proxy.PiiDetectionRequest) -> uipath.platform.semantic_proxy.semantic_proxy.PiiDetectionResponse ``` @@ -806,6 +980,12 @@ sdk.tasks.create(title: str, data: Optional[Dict[str, Any]]=None, app_name: Opti # Creates a new action asynchronously. sdk.tasks.create_async(title: str, data: Optional[Dict[str, Any]]=None, app_name: Optional[str]=None, app_key: Optional[str]=None, app_folder_path: Optional[str]=None, app_folder_key: Optional[str]=None, assignee: Optional[str]=None, recipient: Optional[uipath.platform.action_center.tasks.TaskRecipient]=None, priority: Optional[str]=None, labels: Optional[List[str]]=None, is_actionable_message_enabled: Optional[bool]=None, actionable_message_metadata: Optional[Dict[str, Any]]=None, source_name: str="Agent") -> uipath.platform.action_center.tasks.Task +# Create a new QuickForm task synchronously. +sdk.tasks.create_quickform(title: str, task_schema_key: str, schema: Dict[str, Any], data: Optional[Dict[str, Any]]=None, folder_path: Optional[str]=None, folder_key: Optional[str]=None, assignee: Optional[str]=None, recipient: Optional[uipath.platform.action_center.tasks.TaskRecipient]=None, priority: Optional[str]=None, labels: Optional[List[str]]=None, is_actionable_message_enabled: Optional[bool]=None, actionable_message_metadata: Optional[Dict[str, Any]]=None, creator_job_key: Optional[str]=None, source_name: str="Agent") -> uipath.platform.action_center.tasks.Task + +# Creates a new QuickForm task asynchronously. +sdk.tasks.create_quickform_async(title: str, task_schema_key: str, schema: Dict[str, Any], data: Optional[Dict[str, Any]]=None, folder_path: Optional[str]=None, folder_key: Optional[str]=None, assignee: Optional[str]=None, recipient: Optional[uipath.platform.action_center.tasks.TaskRecipient]=None, priority: Optional[str]=None, labels: Optional[List[str]]=None, is_actionable_message_enabled: Optional[bool]=None, actionable_message_metadata: Optional[Dict[str, Any]]=None, creator_job_key: Optional[str]=None, source_name: str="Agent") -> uipath.platform.action_center.tasks.Task + # Retrieves a task by its key synchronously. sdk.tasks.retrieve(action_key: str, app_folder_path: Optional[str]=None, app_folder_key: Optional[str]=None, app_name: str | None=None) -> uipath.platform.action_center.tasks.Task diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 773d25d8a..1c1556c06 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.82" +version = "2.10.83" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.64" +version = "0.1.65" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 97ad2b6577195e6ee0cc117ffaf37c091388d76e Mon Sep 17 00:00:00 2001 From: GabrielVasilescu04 Date: Tue, 16 Jun 2026 18:15:12 +0300 Subject: [PATCH 100/121] feat(assets): add support for AllowDirectApiAccess (#1722) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/orchestrator/_assets_service.py | 146 ++++++++++++-- .../uipath/platform/orchestrator/assets.py | 6 + .../tests/services/test_assets_service.py | 180 +++++++++++++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- packages/uipath/uv.lock | 4 +- 7 files changed, 313 insertions(+), 31 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index a0d332607..6a50d6e4b 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.65" +version = "0.1.66" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/_assets_service.py b/packages/uipath-platform/src/uipath/platform/orchestrator/_assets_service.py index c95ad1b49..2e673fb7c 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/_assets_service.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/_assets_service.py @@ -283,13 +283,55 @@ async def retrieve_async( else: return Asset.model_validate(response.json()["value"][0]) - def _ensure_robot_context(self) -> None: + def _resolve_robot_key( + self, + name: str, + *, + folder_key: Optional[str] = None, + folder_path: Optional[str] = None, + ) -> Optional[str]: + """Return the robot key, or ``None`` if the asset opts into direct API access. + + Raises ``ValueError`` when no robot key is available and ``AllowDirectApiAccess`` + is not enabled on the asset. + """ try: - is_user = self._execution_context.robot_key is not None + robot_key = self._execution_context.robot_key except ValueError: - is_user = False - if not is_user: - raise ValueError("This method can only be used for robot assets.") + robot_key = None + + if robot_key is None: + asset = self.retrieve( + name=name, folder_key=folder_key, folder_path=folder_path + ) + if not asset.allow_direct_api_access: + raise ValueError( + f"No robot key available and 'AllowDirectApiAccess' is disabled for asset '{name}'." + ) + return robot_key + + async def _resolve_robot_key_async( + self, + name: str, + *, + folder_key: Optional[str] = None, + folder_path: Optional[str] = None, + ) -> Optional[str]: + """Async variant of :meth:`_resolve_robot_key`.""" + try: + robot_key = self._execution_context.robot_key + except ValueError: + robot_key = None + + if robot_key is None: + asset = await self.retrieve_async( + name=name, folder_key=folder_key, folder_path=folder_path + ) + if not asset.allow_direct_api_access: + raise ValueError( + f"No robot key available and 'AllowDirectApiAccess' is disabled for asset '{name}'." + ) + return robot_key @resource_override(resource_type="asset") @traced( @@ -305,6 +347,10 @@ def retrieve_credential( """Get the decrypted password of a Credential asset. The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable). + If no robot key is available, the asset's `AllowDirectApiAccess` flag is checked: when + enabled, the credential is fetched without a robot key; otherwise a `ValueError` is raised. + + Related Activity: [Get Credential](https://docs.uipath.com/activities/other/latest/workflow/get-robot-credential) Args: name (str): The name of the credential asset. @@ -315,10 +361,18 @@ def retrieve_credential( Optional[str]: The decrypted credential password. Raises: - ValueError: If called outside a robot context (no `UIPATH_ROBOT_KEY`). + ValueError: If no robot key is available and the asset does not have `AllowDirectApiAccess` enabled. """ - self._ensure_robot_context() - spec = self._retrieve_spec(name, folder_key=folder_key, folder_path=folder_path) + robot_key = self._resolve_robot_key( + name, folder_key=folder_key, folder_path=folder_path + ) + + spec = self._retrieve_credential_spec( + name, + robot_key=robot_key, + folder_key=folder_key, + folder_path=folder_path, + ) response = self.request( spec.method, url=spec.endpoint, @@ -343,6 +397,10 @@ async def retrieve_credential_async( """Asynchronously get the decrypted password of a Credential asset. The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable). + If no robot key is available, the asset's `AllowDirectApiAccess` flag is checked: when + enabled, the credential is fetched without a robot key; otherwise a `ValueError` is raised. + + Related Activity: [Get Credential](https://docs.uipath.com/activities/other/latest/workflow/get-robot-credential) Args: name (str): The name of the credential asset. @@ -353,10 +411,18 @@ async def retrieve_credential_async( Optional[str]: The decrypted credential password. Raises: - ValueError: If called outside a robot context (no `UIPATH_ROBOT_KEY`). + ValueError: If no robot key is available and the asset does not have `AllowDirectApiAccess` enabled. """ - self._ensure_robot_context() - spec = self._retrieve_spec(name, folder_key=folder_key, folder_path=folder_path) + robot_key = await self._resolve_robot_key_async( + name, folder_key=folder_key, folder_path=folder_path + ) + + spec = self._retrieve_credential_spec( + name, + robot_key=robot_key, + folder_key=folder_key, + folder_path=folder_path, + ) response = await self.request_async( spec.method, url=spec.endpoint, @@ -379,6 +445,8 @@ def retrieve_secret( """Get the decrypted value of a Secret asset. The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable). + If no robot key is available, the asset's `AllowDirectApiAccess` flag is checked: when + enabled, the secret is fetched without a robot key; otherwise a `ValueError` is raised. Args: name (str): The name of the secret asset. @@ -389,10 +457,18 @@ def retrieve_secret( Optional[str]: The decrypted secret value. Raises: - ValueError: If called outside a robot context (no `UIPATH_ROBOT_KEY`). + ValueError: If no robot key is available and the asset does not have `AllowDirectApiAccess` enabled. """ - self._ensure_robot_context() - spec = self._retrieve_spec(name, folder_key=folder_key, folder_path=folder_path) + robot_key = self._resolve_robot_key( + name, folder_key=folder_key, folder_path=folder_path + ) + + spec = self._retrieve_credential_spec( + name, + robot_key=robot_key, + folder_key=folder_key, + folder_path=folder_path, + ) response = self.request( spec.method, url=spec.endpoint, @@ -415,6 +491,8 @@ async def retrieve_secret_async( """Asynchronously get the decrypted value of a Secret asset. The robot id is retrieved from the execution context (`UIPATH_ROBOT_KEY` environment variable). + If no robot key is available, the asset's `AllowDirectApiAccess` flag is checked: when + enabled, the secret is fetched without a robot key; otherwise a `ValueError` is raised. Args: name (str): The name of the secret asset. @@ -425,10 +503,18 @@ async def retrieve_secret_async( Optional[str]: The decrypted secret value. Raises: - ValueError: If called outside a robot context (no `UIPATH_ROBOT_KEY`). + ValueError: If no robot key is available and the asset does not have `AllowDirectApiAccess` enabled. """ - self._ensure_robot_context() - spec = self._retrieve_spec(name, folder_key=folder_key, folder_path=folder_path) + robot_key = await self._resolve_robot_key_async( + name, folder_key=folder_key, folder_path=folder_path + ) + + spec = self._retrieve_credential_spec( + name, + robot_key=robot_key, + folder_key=folder_key, + folder_path=folder_path, + ) response = await self.request_async( spec.method, url=spec.endpoint, @@ -559,6 +645,32 @@ def _retrieve_spec( }, ) + def _retrieve_credential_spec( + self, + name: str, + *, + robot_key: Optional[str], + folder_key: Optional[str] = None, + folder_path: Optional[str] = None, + ) -> RequestSpec: + body: Dict[str, Any] = { + "assetName": name, + "supportsCredentialsProxyDisconnected": True, + } + if robot_key is not None: + body["robotKey"] = robot_key + + return RequestSpec( + method="POST", + endpoint=Endpoint( + "/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAssetByNameForRobotKey" + ), + json=body, + headers={ + **header_folder(folder_key, folder_path), + }, + ) + def _update_spec( self, robot_asset: UserAsset, diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/assets.py b/packages/uipath-platform/src/uipath/platform/orchestrator/assets.py index 056420225..122821029 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/assets.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/assets.py @@ -47,6 +47,9 @@ class UserAsset(BaseModel): connection_data: Optional[CredentialsConnectionData] = Field( default=None, alias="ConnectionData" ) + allow_direct_api_access: Optional[bool] = Field( + default=None, alias="AllowDirectApiAccess" + ) id: Optional[int] = Field(default=None, alias="Id") @@ -73,3 +76,6 @@ class Asset(BaseModel): secret_value: Optional[str] = Field(default=None, alias="SecretValue") external_name: Optional[str] = Field(default=None, alias="ExternalName") credential_store_id: Optional[int] = Field(default=None, alias="CredentialStoreId") + allow_direct_api_access: Optional[bool] = Field( + default=None, alias="AllowDirectApiAccess" + ) diff --git a/packages/uipath-platform/tests/services/test_assets_service.py b/packages/uipath-platform/tests/services/test_assets_service.py index bf28a6afc..4b01ed210 100644 --- a/packages/uipath-platform/tests/services/test_assets_service.py +++ b/packages/uipath-platform/tests/services/test_assets_service.py @@ -362,20 +362,94 @@ def test_retrieve_credential( == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.AssetsService.retrieve_credential/{version}" ) - def test_retrieve_credential_user_asset( + def test_retrieve_credential_no_robot_key_direct_access_disabled( self, - service: AssetsService, - monkeypatch: pytest.MonkeyPatch, + httpx_mock: HTTPXMock, + base_url: str, + org: str, + tenant: str, config: UiPathApiConfig, + monkeypatch: pytest.MonkeyPatch, ) -> None: + monkeypatch.delenv("UIPATH_ROBOT_KEY", raising=False) + service = AssetsService( + config=config, + execution_context=UiPathExecutionContext(), + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetFiltered?$filter=Name eq 'Test Credential'&$top=1", + status_code=200, + json={ + "value": [ + { + "Key": "asset-key", + "Name": "Test Credential", + "ValueType": "Credential", + "AllowDirectApiAccess": False, + } + ] + }, + ) + with pytest.raises(ValueError): - monkeypatch.delenv("UIPATH_ROBOT_KEY", raising=False) - service = AssetsService( - config=config, - execution_context=UiPathExecutionContext(), - ) service.retrieve_credential(name="Test Credential") + def test_retrieve_credential_no_robot_key_direct_access_enabled( + self, + httpx_mock: HTTPXMock, + base_url: str, + org: str, + tenant: str, + config: UiPathApiConfig, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + import json + + monkeypatch.delenv("UIPATH_ROBOT_KEY", raising=False) + service = AssetsService( + config=config, + execution_context=UiPathExecutionContext(), + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetFiltered?$filter=Name eq 'Test Credential'&$top=1", + status_code=200, + json={ + "value": [ + { + "Key": "asset-key", + "Name": "Test Credential", + "ValueType": "Credential", + "AllowDirectApiAccess": True, + } + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAssetByNameForRobotKey", + status_code=200, + json={ + "id": 1, + "name": "Test Credential", + "credential_username": "test-user", + "credential_password": "test-password", + }, + ) + + credential = service.retrieve_credential(name="Test Credential") + + assert credential == "test-password" + + sent_requests = httpx_mock.get_requests() + assert len(sent_requests) == 2 + credential_request = sent_requests[1] + assert credential_request.method == "POST" + request_body = json.loads(credential_request.content) + assert request_body["assetName"] == "Test Credential" + assert request_body["supportsCredentialsProxyDisconnected"] is True + assert "robotKey" not in request_body + async def test_retrieve_credential_async( self, httpx_mock: HTTPXMock, @@ -417,6 +491,96 @@ async def test_retrieve_credential_async( == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.AssetsService.retrieve_credential_async/{version}" ) + @pytest.mark.anyio + async def test_retrieve_credential_async_no_robot_key_direct_access_disabled( + self, + httpx_mock: HTTPXMock, + base_url: str, + org: str, + tenant: str, + config: UiPathApiConfig, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.delenv("UIPATH_ROBOT_KEY", raising=False) + service = AssetsService( + config=config, + execution_context=UiPathExecutionContext(), + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetFiltered?$filter=Name eq 'Test Credential'&$top=1", + status_code=200, + json={ + "value": [ + { + "Key": "asset-key", + "Name": "Test Credential", + "ValueType": "Credential", + "AllowDirectApiAccess": False, + } + ] + }, + ) + + with pytest.raises(ValueError): + await service.retrieve_credential_async(name="Test Credential") + + @pytest.mark.anyio + async def test_retrieve_credential_async_no_robot_key_direct_access_enabled( + self, + httpx_mock: HTTPXMock, + base_url: str, + org: str, + tenant: str, + config: UiPathApiConfig, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + import json + + monkeypatch.delenv("UIPATH_ROBOT_KEY", raising=False) + service = AssetsService( + config=config, + execution_context=UiPathExecutionContext(), + ) + + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetFiltered?$filter=Name eq 'Test Credential'&$top=1", + status_code=200, + json={ + "value": [ + { + "Key": "asset-key", + "Name": "Test Credential", + "ValueType": "Credential", + "AllowDirectApiAccess": True, + } + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAssetByNameForRobotKey", + status_code=200, + json={ + "id": 1, + "name": "Test Credential", + "credential_username": "test-user", + "credential_password": "test-password", + }, + ) + + credential = await service.retrieve_credential_async(name="Test Credential") + + assert credential == "test-password" + + sent_requests = httpx_mock.get_requests() + assert len(sent_requests) == 2 + credential_request = sent_requests[1] + assert credential_request.method == "POST" + request_body = json.loads(credential_request.content) + assert request_body["assetName"] == "Test Credential" + assert request_body["supportsCredentialsProxyDisconnected"] is True + assert "robotKey" not in request_body + def test_retrieve_secret( self, httpx_mock: HTTPXMock, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 689662d4d..c38310808 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.65" +version = "0.1.66" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index c04ca9d6c..7b32baacf 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.83" +version = "2.10.84" 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" dependencies = [ "uipath-core>=0.5.17, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.65, <0.2.0", + "uipath-platform>=0.1.66, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 1c1556c06..6f903c231 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.83" +version = "2.10.84" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.65" +version = "0.1.66" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From b0d85dbb1d3f401354db3fa9bf6d7933e4cb86d2 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 16 Jun 2026 18:32:28 +0300 Subject: [PATCH 101/121] ci: force local wheels via uv override in cross-tests (#1723) --- .github/scripts/write_uv_overrides.py | 50 +++++++++++++++++++ .../workflows/test-uipath-integrations.yml | 28 +++++++---- .github/workflows/test-uipath-langchain.yml | 28 +++++++---- .github/workflows/test-uipath-runtime.yml | 13 +++-- 4 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 .github/scripts/write_uv_overrides.py diff --git a/.github/scripts/write_uv_overrides.py b/.github/scripts/write_uv_overrides.py new file mode 100644 index 000000000..557f9d26e --- /dev/null +++ b/.github/scripts/write_uv_overrides.py @@ -0,0 +1,50 @@ +"""Write a uv override file forcing the locally built uipath wheels. + +Cross-test workflows build uipath wheels from the PR and run them against +downstream repos (uipath-langchain-python, uipath-integrations-python, +uipath-runtime-python). Those downstreams cap the uipath* version (e.g. +``uipath<2.11.0``), so a backward-compatible minor bump would fail resolution +purely on the cap. uv ``override-dependencies`` ignore the declared version +specifier, so pointing them at the local wheels lets the cross-test exercise the +real new code regardless of the cap. + +The script is layout-agnostic: it overrides whatever ``uipath*`` wheels exist +under ``$GITHUB_WORKSPACE/wheels`` (recursively), so it works for the +three-wheel layout (``wheels//dist/*.whl``) and the single-wheel runtime +layout (``wheels/*.whl``) alike. + +The resulting override file path is appended to ``GITHUB_ENV`` as ``UV_OVERRIDE`` +so every subsequent ``uv`` invocation in the job honors it. +""" + +import glob +import os +import pathlib + + +def main() -> None: + wheels = pathlib.Path(os.environ.get("GITHUB_WORKSPACE", ".")).resolve() / "wheels" + + lines = [] + for whl in sorted(glob.glob(str(wheels / "**" / "*.whl"), recursive=True)): + # Wheel filename is ``{distribution}-{version}-...whl`` where the + # distribution escapes hyphens to underscores (uipath_core -> uipath-core). + dist = pathlib.Path(whl).name.split("-", 1)[0].replace("_", "-") + if not dist.startswith("uipath"): + continue + lines.append(f"{dist} @ {pathlib.Path(whl).resolve().as_uri()}") + + if not lines: + raise SystemExit(f"no uipath wheels found under {wheels}") + + out = wheels / "overrides.txt" + out.write_text("\n".join(lines) + "\n") + + with open(os.environ["GITHUB_ENV"], "a") as fh: + fh.write(f"UV_OVERRIDE={out}\n") + + print("\n".join(lines)) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/test-uipath-integrations.yml b/.github/workflows/test-uipath-integrations.yml index ed50a4b3d..95c621941 100644 --- a/.github/workflows/test-uipath-integrations.yml +++ b/.github/workflows/test-uipath-integrations.yml @@ -102,13 +102,17 @@ jobs: repository: 'UiPath/uipath-integrations-python' path: 'uipath-integrations-python' - - name: Update uipath packages + - name: Checkout uipath-python scripts + uses: actions/checkout@v4 + with: + path: _scripts + sparse-checkout: .github/scripts + + - name: Override uipath packages with local wheels shell: bash - working-directory: uipath-integrations-python/packages/${{ matrix.package }} run: | - uv add ../../../wheels/uipath-core/dist/*.whl --dev - uv add ../../../wheels/uipath-platform/dist/*.whl --dev - uv add ../../../wheels/uipath/dist/*.whl --dev + PYBIN=$(command -v python || command -v python3) + "$PYBIN" "$GITHUB_WORKSPACE/_scripts/.github/scripts/write_uv_overrides.py" - name: Install dependencies and run tests shell: bash @@ -202,13 +206,17 @@ jobs: repository: 'UiPath/uipath-integrations-python' path: 'uipath-integrations-python' - - name: Update uipath packages + - name: Checkout uipath-python scripts + uses: actions/checkout@v4 + with: + path: _scripts + sparse-checkout: .github/scripts + + - name: Override uipath packages with local wheels shell: bash - working-directory: uipath-integrations-python/packages/${{ matrix.package }} run: | - uv add ../../../wheels/uipath-core/dist/*.whl - uv add ../../../wheels/uipath-platform/dist/*.whl - uv add ../../../wheels/uipath/dist/*.whl + PYBIN=$(command -v python || command -v python3) + "$PYBIN" "$GITHUB_WORKSPACE/_scripts/.github/scripts/write_uv_overrides.py" - name: Install dependencies working-directory: uipath-integrations-python/packages/${{ matrix.package }} diff --git a/.github/workflows/test-uipath-langchain.yml b/.github/workflows/test-uipath-langchain.yml index f7ccb2cf9..25b027633 100644 --- a/.github/workflows/test-uipath-langchain.yml +++ b/.github/workflows/test-uipath-langchain.yml @@ -72,13 +72,17 @@ jobs: repository: 'UiPath/uipath-langchain-python' path: 'uipath-langchain-python' - - name: Update uipath packages + - name: Checkout uipath-python scripts + uses: actions/checkout@v4 + with: + path: _scripts + sparse-checkout: .github/scripts + + - name: Override uipath packages with local wheels shell: bash - working-directory: uipath-langchain-python run: | - uv add ../wheels/uipath-core/dist/*.whl --dev - uv add ../wheels/uipath-platform/dist/*.whl --dev - uv add ../wheels/uipath/dist/*.whl --dev + PYBIN=$(command -v python || command -v python3) + "$PYBIN" "$GITHUB_WORKSPACE/_scripts/.github/scripts/write_uv_overrides.py" - name: Run uipath-langchain tests working-directory: uipath-langchain-python @@ -146,13 +150,17 @@ jobs: repository: 'UiPath/uipath-langchain-python' path: 'uipath-langchain-python' - - name: Update uipath packages + - name: Checkout uipath-python scripts + uses: actions/checkout@v4 + with: + path: _scripts + sparse-checkout: .github/scripts + + - name: Override uipath packages with local wheels shell: bash - working-directory: uipath-langchain-python run: | - uv add ../wheels/uipath-core/dist/*.whl - uv add ../wheels/uipath-platform/dist/*.whl - uv add ../wheels/uipath/dist/*.whl + PYBIN=$(command -v python || command -v python3) + "$PYBIN" "$GITHUB_WORKSPACE/_scripts/.github/scripts/write_uv_overrides.py" - name: Install dependencies working-directory: uipath-langchain-python diff --git a/.github/workflows/test-uipath-runtime.yml b/.github/workflows/test-uipath-runtime.yml index 13ad019ef..200a1b8ca 100644 --- a/.github/workflows/test-uipath-runtime.yml +++ b/.github/workflows/test-uipath-runtime.yml @@ -64,10 +64,17 @@ jobs: repository: 'UiPath/uipath-runtime-python' path: 'uipath-runtime-python' - - name: Update uipath-core version + - name: Checkout uipath-python scripts + uses: actions/checkout@v4 + with: + path: _scripts + sparse-checkout: .github/scripts + + - name: Override uipath packages with local wheels shell: bash - working-directory: uipath-runtime-python - run: uv add ../wheels/*.whl --dev + run: | + PYBIN=$(command -v python || command -v python3) + "$PYBIN" "$GITHUB_WORKSPACE/_scripts/.github/scripts/write_uv_overrides.py" - name: Run uipath-runtime tests working-directory: uipath-runtime-python From ed5e5094cded66985565e5a6754b3bee4a41c96b Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 16 Jun 2026 19:42:03 +0300 Subject: [PATCH 102/121] feat: add stable id to uipath.json for packaging and spans (#1695) --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath/platform/chat/llm_trace_context.py | 4 +- .../src/uipath/platform/common/_span_utils.py | 46 +++++- .../tests/services/test_llm_trace_context.py | 20 +-- .../tests/services/test_span_utils.py | 144 +++++++++++++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/docs/cli/index.md | 8 + packages/uipath/pyproject.toml | 4 +- packages/uipath/specs/uipath.schema.json | 5 + packages/uipath/specs/uipath.spec.md | 37 ++++- .../src/uipath/_cli/_evals/_telemetry.py | 6 +- .../src/uipath/_cli/_utils/_project_files.py | 48 ++++-- packages/uipath/src/uipath/_cli/cli_debug.py | 1 - packages/uipath/src/uipath/_cli/cli_init.py | 47 +++--- packages/uipath/src/uipath/_cli/cli_pack.py | 50 +++--- .../uipath/_cli/models/uipath_json_schema.py | 6 + .../uipath/src/uipath/telemetry/_track.py | 17 ++- .../testcases/langchain-cross/pyproject.toml | 9 +- .../tests/cli/eval/test_eval_telemetry.py | 13 +- packages/uipath/tests/cli/test_init.py | 96 ++++++++++++ packages/uipath/tests/cli/test_pack.py | 130 +++++++++++++++- packages/uipath/tests/telemetry/test_track.py | 46 ++++++ packages/uipath/uv.lock | 4 +- 23 files changed, 621 insertions(+), 124 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 6a50d6e4b..464549970 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.66" +version = "0.1.67" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py b/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py index fc97da511..b9c1f74a0 100644 --- a/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py +++ b/packages/uipath-platform/src/uipath/platform/chat/llm_trace_context.py @@ -5,7 +5,7 @@ from uipath.core.tracing.span_utils import UiPathSpanUtils from ..common._config import UiPathConfig -from ..common._span_utils import _SpanUtils +from ..common._span_utils import _SpanUtils, resolve_project_id def build_trace_context_headers( @@ -40,7 +40,7 @@ def build_trace_context_headers( baggage_parts: list[str] = list(extra_baggage) if extra_baggage else [] if folder_key := UiPathConfig.folder_key: baggage_parts.append(f"folderKey={folder_key}") - if agent_id := UiPathConfig.agent_id: + if agent_id := resolve_project_id(): baggage_parts.append(f"agentId={agent_id}") if process_key := UiPathConfig.process_key: baggage_parts.append(f"processKey={process_key}") diff --git a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py index ab91b3623..954fbc038 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_span_utils.py +++ b/packages/uipath-platform/src/uipath/platform/common/_span_utils.py @@ -2,9 +2,11 @@ import json import logging import os +import uuid from dataclasses import dataclass, field from datetime import datetime from enum import IntEnum +from functools import lru_cache from os import environ as env from typing import Any, Dict, List, Optional @@ -19,6 +21,40 @@ DEFAULT_SOURCE = 10 +@lru_cache(maxsize=1) +def _read_config_id() -> str | None: + """Return a valid GUID ``id`` from ``uipath.json``, cached for the process lifetime.""" + from uipath.platform.common._config import UiPathConfig + + try: + config_file = json.loads(UiPathConfig.config_file_path.read_text()) + except (FileNotFoundError, json.JSONDecodeError): + return None + + project_id = config_file.get("id") + if not isinstance(project_id, str): + logger.warning("'id' field not present in uipath.json") + return None + + try: + uuid.UUID(project_id) + except ValueError: + logger.warning("Ignoring uipath.json 'id' %r: not a valid GUID.", project_id) + return None + + return project_id + + +def resolve_project_id() -> str | None: + """Resolve the project id. + + Prefers ``uipath.json#id``, falls back to env vars. + """ + from uipath.platform.common._config import UiPathConfig + + return _read_config_id() or UiPathConfig.agent_id or UiPathConfig.project_key + + class AttachmentProvider(IntEnum): ORCHESTRATOR = 0 @@ -281,9 +317,11 @@ def otel_span_to_uipath_span( ] attributes_dict["links"] = links_list + if agent_id := resolve_project_id(): + attributes_dict["agentId"] = agent_id + # Add process context attributes from environment variables for env_key, attr_key in ( - ("PROJECT_KEY", "agentId"), ("UIPATH_PROCESS_KEY", "agentName"), ("UIPATH_PROCESS_VERSION", "agentVersion"), ): @@ -297,10 +335,8 @@ def otel_span_to_uipath_span( # Top-level fields for internal tracing schema execution_type = attributes_dict.get("executionType") agent_version = attributes_dict.get("agentVersion") - reference_id = ( - env.get("UIPATH_AGENT_ID") - or attributes_dict.get("agentId") - or attributes_dict.get("referenceId") + reference_id = attributes_dict.get("agentId") or attributes_dict.get( + "referenceId" ) verbosity_level = attributes_dict.get("verbosityLevel") diff --git a/packages/uipath-platform/tests/services/test_llm_trace_context.py b/packages/uipath-platform/tests/services/test_llm_trace_context.py index 8db61200b..2b078cd62 100644 --- a/packages/uipath-platform/tests/services/test_llm_trace_context.py +++ b/packages/uipath-platform/tests/services/test_llm_trace_context.py @@ -8,6 +8,7 @@ from uipath.core.feature_flags import FeatureFlags from uipath.platform.chat.llm_trace_context import build_trace_context_headers +from uipath.platform.common.constants import ENV_PROJECT_KEY FEATURE_FLAG = "EnableTraceContextHeaders" @@ -110,13 +111,16 @@ class TestBaggageHeader: """When enabled, x-uipath-tracebaggage is populated from UiPathConfig.""" def setup_method(self) -> None: + from uipath.platform.common._span_utils import _read_config_id + + _read_config_id.cache_clear() FeatureFlags.reset_flags() FeatureFlags.configure_flags({FEATURE_FLAG: True}) def test_all_env_vars_present(self) -> None: env = { "UIPATH_FOLDER_KEY": "folder-abc", - "UIPATH_AGENT_ID": "agent-123", + ENV_PROJECT_KEY: "agent-123", "UIPATH_PROCESS_KEY": "process-789", } with patch.dict(os.environ, env, clear=True): @@ -135,22 +139,14 @@ def test_partial_env_vars(self) -> None: baggage = headers["x-uipath-tracebaggage"] assert "folderKey=folder-only" in baggage - def test_agent_id_from_agent_id_env(self) -> None: - env = {"UIPATH_AGENT_ID": "real-agent-id"} + def test_agent_id_from_project_key_env(self) -> None: + env = {ENV_PROJECT_KEY: "real-agent-id"} with patch.dict(os.environ, env, clear=True): headers = build_trace_context_headers() baggage = headers["x-uipath-tracebaggage"] assert "agentId=real-agent-id" in baggage - def test_agent_id_falls_back_to_project_id(self) -> None: - env = {"UIPATH_PROJECT_ID": "project-123"} - with patch.dict(os.environ, env, clear=True): - headers = build_trace_context_headers() - - baggage = headers["x-uipath-tracebaggage"] - assert "agentId=project-123" in baggage - def test_no_agent_id_without_env_vars(self) -> None: env = {"UIPATH_FOLDER_KEY": "f1"} with patch.dict(os.environ, env, clear=True): @@ -169,7 +165,7 @@ def test_no_baggage_without_env_vars(self) -> None: def test_baggage_comma_separated(self) -> None: env = { "UIPATH_FOLDER_KEY": "f1", - "UIPATH_AGENT_ID": "a1", + ENV_PROJECT_KEY: "a1", } with patch.dict(os.environ, env, clear=True): headers = build_trace_context_headers() diff --git a/packages/uipath-platform/tests/services/test_span_utils.py b/packages/uipath-platform/tests/services/test_span_utils.py index 03f728eb8..35c2b78e7 100644 --- a/packages/uipath-platform/tests/services/test_span_utils.py +++ b/packages/uipath-platform/tests/services/test_span_utils.py @@ -8,6 +8,21 @@ from opentelemetry.trace import SpanContext, StatusCode from uipath.platform.common import UiPathSpan, _SpanUtils +from uipath.platform.common.constants import ( + ENV_PROJECT_KEY, + ENV_UIPATH_AGENT_ID, + ENV_UIPATH_PROJECT_ID, +) + + +@pytest.fixture(autouse=True) +def _clear_id_cache(): + """Isolate the process-global id cache between tests.""" + from uipath.platform.common._span_utils import _read_config_id + + _read_config_id.cache_clear() + yield + _read_config_id.cache_clear() class TestOTelToUiPathSpan: @@ -92,10 +107,11 @@ def test_verbosity_level_omitted_when_unset(self) -> None: class TestReferenceIdResolution: """`reference_id` resolution chain. - Priority: `UIPATH_AGENT_ID` env var > `agentId` attribute > `referenceId` - attribute. Falsy values (missing / empty string) at each step fall through - to the next source. The `referenceId` fallback exists for backwards - compatibility with older producers that only emit that attribute. + `reference_id` is derived from the span's resolved `agentId` attribute + (which itself goes through `resolve_project_id()`), falling back to the + `referenceId` attribute. Falsy values (missing / empty string) at each step + fall through to the next source. The `referenceId` fallback exists for + backwards compatibility with older producers that only emit that attribute. """ @pytest.mark.parametrize( @@ -105,7 +121,7 @@ class TestReferenceIdResolution: "env-agent", {"agentId": "attr-agent", "referenceId": "attr-ref"}, "env-agent", - id="env-var-wins", + id="env-var-overrides-attr", ), pytest.param( None, @@ -140,10 +156,15 @@ def test_reference_id_chain( expected: str | None, monkeypatch: pytest.MonkeyPatch, ) -> None: + from uipath.platform.common._span_utils import _read_config_id + + _read_config_id.cache_clear() + monkeypatch.delenv(ENV_UIPATH_AGENT_ID, raising=False) + monkeypatch.delenv(ENV_UIPATH_PROJECT_ID, raising=False) if env_value is None: - monkeypatch.delenv("UIPATH_AGENT_ID", raising=False) + monkeypatch.delenv(ENV_PROJECT_KEY, raising=False) else: - monkeypatch.setenv("UIPATH_AGENT_ID", env_value) + monkeypatch.setenv(ENV_PROJECT_KEY, env_value) mock_span = Mock(spec=OTelSpan) mock_context = SpanContext( @@ -166,6 +187,115 @@ def test_reference_id_chain( assert uipath_span.reference_id == expected +class TestAgentIdResolution: + """`agentId` span attribute resolution via `resolve_project_id()`. + + Priority: `uipath.json#id` (cached, read once per process) > `UIPATH_AGENT_ID` + / `UIPATH_PROJECT_ID` > the legacy `PROJECT_KEY` env var injected by the + executor at runtime. When no source is present the `agentId` attribute is + omitted entirely. + """ + + @staticmethod + def _make_span() -> Mock: + mock_span = Mock(spec=OTelSpan) + mock_context = SpanContext( + trace_id=0x123456789ABCDEF0123456789ABCDEF0, + span_id=0x0123456789ABCDEF, + is_remote=False, + ) + mock_span.get_span_context.return_value = mock_context + mock_span.name = "test-span" + mock_span.parent = None + mock_span.status.status_code = StatusCode.OK + mock_span.attributes = {} + mock_span.events = [] + mock_span.links = [] + now_ns = int(datetime.now().timestamp() * 1e9) + mock_span.start_time = now_ns + mock_span.end_time = now_ns + 1_000_000 + return mock_span + + @staticmethod + def _resolve(monkeypatch: pytest.MonkeyPatch, tmp_path) -> object: + from uipath.platform.common._span_utils import _read_config_id + + _read_config_id.cache_clear() + monkeypatch.delenv("UIPATH_CONFIG_PATH", raising=False) + monkeypatch.delenv(ENV_UIPATH_AGENT_ID, raising=False) + monkeypatch.delenv(ENV_UIPATH_PROJECT_ID, raising=False) + monkeypatch.chdir(tmp_path) + uipath_span = _SpanUtils.otel_span_to_uipath_span( + TestAgentIdResolution._make_span(), serialize_attributes=False + ) + attributes = uipath_span.attributes + assert isinstance(attributes, dict) + return attributes.get("agentId") + + def test_agent_id_from_uipath_json_wins_over_env( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + (tmp_path / "uipath.json").write_text( + json.dumps({"id": "00000000-0000-0000-0000-000000000001"}) + ) + monkeypatch.setenv(ENV_PROJECT_KEY, "from-env") + assert ( + self._resolve(monkeypatch, tmp_path) + == "00000000-0000-0000-0000-000000000001" + ) + + def test_agent_id_falls_back_to_project_key( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + # No uipath.json on disk. + monkeypatch.setenv(ENV_PROJECT_KEY, "from-env") + assert self._resolve(monkeypatch, tmp_path) == "from-env" + + def test_agent_id_falls_back_when_config_has_no_id( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + (tmp_path / "uipath.json").write_text(json.dumps({"functions": {}})) + monkeypatch.setenv(ENV_PROJECT_KEY, "from-env") + assert self._resolve(monkeypatch, tmp_path) == "from-env" + + def test_agent_id_absent_when_no_source( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + monkeypatch.delenv(ENV_PROJECT_KEY, raising=False) + assert self._resolve(monkeypatch, tmp_path) is None + + def test_non_guid_config_id_is_ignored_and_falls_back( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + # A malformed (non-GUID) id must not reach ReferenceId; fall back to env. + (tmp_path / "uipath.json").write_text(json.dumps({"id": "not-a-guid"})) + monkeypatch.setenv(ENV_PROJECT_KEY, "from-env") + assert self._resolve(monkeypatch, tmp_path) == "from-env" + + def test_config_id_is_cached( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + from uipath.platform.common._span_utils import _read_config_id + + first = "00000000-0000-0000-0000-000000000001" + second = "00000000-0000-0000-0000-000000000002" + + _read_config_id.cache_clear() + monkeypatch.delenv("UIPATH_CONFIG_PATH", raising=False) + monkeypatch.chdir(tmp_path) + config = tmp_path / "uipath.json" + + config.write_text(json.dumps({"id": first})) + assert _read_config_id() == first + + # A later edit is not observed: the value is read once and cached. + config.write_text(json.dumps({"id": second})) + assert _read_config_id() == first + + _read_config_id.cache_clear() + assert _read_config_id() == second + + class TestNormalizeIds: """Tests for OTEL ID normalization functions.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index c38310808..0e01f1f73 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.66" +version = "0.1.67" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/docs/cli/index.md b/packages/uipath/docs/cli/index.md index f73afa0d1..e74a4d83e 100644 --- a/packages/uipath/docs/cli/index.md +++ b/packages/uipath/docs/cli/index.md @@ -121,6 +121,14 @@ Running `uipath init` will process these function definitions and create the cor `uipath init` generates one `.mermaid` file per function/agent containing a static call graph, rendered in the UiPath Orchestrator UI. These files are regenerated on every `uipath init`. /// + +/// warning +### About the `id` field + +The first `uipath init` mints a stable `id` (GUID) into `uipath.json` and preserves it across subsequent runs. It is what identifies your project consistently wherever it is deployed and run. + +Do not change or remove it. Changing it makes the project look like a brand-new, unrelated one, so you lose the link to everything previously published and tracked under the old id. `uipath pack` rejects an `id` that is not a valid GUID. +/// --- ::: mkdocs-click diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7b32baacf..be0c4c22d 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.10.84" +version = "2.11.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" dependencies = [ "uipath-core>=0.5.17, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.66, <0.2.0", + "uipath-platform>=0.1.67, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/specs/uipath.schema.json b/packages/uipath/specs/uipath.schema.json index 8f2a550f8..2ee966e82 100644 --- a/packages/uipath/specs/uipath.schema.json +++ b/packages/uipath/specs/uipath.schema.json @@ -9,6 +9,11 @@ "type": "string", "description": "Reference to this JSON schema for editor support" }, + "id": { + "type": "string", + "format": "uuid", + "description": "Stable unique identifier for the project, minted once on the first 'uipath init' and preserved for its lifetime. Used as the package 'projectId' at pack time. Do not change it." + }, "runtimeOptions": { "type": "object", "description": "Runtime behavior configuration", diff --git a/packages/uipath/specs/uipath.spec.md b/packages/uipath/specs/uipath.spec.md index 5c7599faa..9679bf223 100644 --- a/packages/uipath/specs/uipath.spec.md +++ b/packages/uipath/specs/uipath.spec.md @@ -9,6 +9,7 @@ The `uipath.json` file is a configuration file for UiPath projects that defines ```json { "$schema": "https://cloud.uipath.com/draft/2024-12/uipath", + "id": "00000000-0000-0000-0000-000000000000", "runtimeOptions": { ... }, "designOptions": { ... }, "packOptions": { ... }, @@ -20,7 +21,29 @@ The `uipath.json` file is a configuration file for UiPath projects that defines ## Configuration Sections -### 1. `runtimeOptions` +### 1. `id` + +Stable unique identifier (GUID) for the project, minted once on the first `uipath init` and preserved for its lifetime. Used as the package `projectId` at pack time. + +**Properties:** + +| Property | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| `id` | `string` (uuid) | No | minted on first `uipath init` | Stable identifier for the project. Do not change it. | + +> Do not change or remove `id`. It identifies your project consistently wherever it is deployed and run. Changing it makes the project look like a brand-new, unrelated one, so you lose the link to everything previously published and tracked under the old id. `uipath pack` rejects an `id` that is not a valid GUID. + +**Example:** + +```json +{ + "id": "00000000-0000-0000-0000-000000000001" +} +``` + +--- + +### 2. `runtimeOptions` Controls runtime behavior of your UiPath project. @@ -42,7 +65,7 @@ Controls runtime behavior of your UiPath project. --- -### 2. `designOptions` +### 3. `designOptions` Design-time configuration and preferences. @@ -57,7 +80,7 @@ Design-time configuration and preferences. --- -### 3. `packOptions` +### 4. `packOptions` Controls which files and directories are included or excluded when packaging your project. @@ -87,7 +110,7 @@ Controls which files and directories are included or excluded when packaging you --- -### 4. `functions` +### 5. `functions` Defines entrypoints for pure Python scripts. Each key is a friendly name for the entrypoint, and each value specifies the file path and function name. @@ -128,6 +151,7 @@ Defines entrypoints for pure Python scripts. Each key is a friendly name for the ```json { "$schema": "https://cloud.uipath.com/draft/2024-12/uipath", + "id": "00000000-0000-0000-0000-000000000001", "runtimeOptions": { "isConversational": false }, @@ -218,6 +242,11 @@ The complete JSON Schema is available in `uipath.schema.json`: "type": "string", "description": "Reference to this JSON schema for editor support" }, + "id": { + "type": "string", + "format": "uuid", + "description": "Stable unique identifier for the project, minted once on the first 'uipath init' and preserved for its lifetime. Used as the package 'projectId' at pack time. Do not change it." + }, "runtimeOptions": { "type": "object", "description": "Runtime behavior configuration", diff --git a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py index 04cb7e2c4..bdbdc67f7 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py @@ -308,10 +308,12 @@ def _enrich_properties(self, properties: dict[str, Any]) -> None: Args: properties: The properties dictionary to enrich. """ + from uipath.platform.common._span_utils import resolve_project_id + if UiPathConfig.project_id: properties["ProjectId"] = UiPathConfig.project_id - if UiPathConfig.agent_id: - properties["AgentId"] = UiPathConfig.agent_id + if agent_id := resolve_project_id(): + properties["AgentId"] = agent_id if UiPathConfig.organization_id: properties["CloudOrganizationId"] = UiPathConfig.organization_id diff --git a/packages/uipath/src/uipath/_cli/_utils/_project_files.py b/packages/uipath/src/uipath/_cli/_utils/_project_files.py index c7c025197..15f5e53c5 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_project_files.py +++ b/packages/uipath/src/uipath/_cli/_utils/_project_files.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Any, AsyncIterator, Dict, Literal, Optional, Tuple +import anyio from pydantic import BaseModel, Field, TypeAdapter from uipath._cli.models.uipath_json_schema import PackOptions, UiPathJsonConfig @@ -25,6 +26,34 @@ logger = logging.getLogger(__name__) +def resolve_existing_project_id(directory: str = ".") -> Optional[str]: + """Return an already-established project id for this project, if any. + + Checks the Studio Web project env var first, then falls back to the legacy + ``ProjectKey`` stored in ``.uipath/.telemetry.json``. Returns ``None`` when + neither is present. + + Args: + directory: The project root directory to look for the telemetry file in. + """ + from ...telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE + + if project_id := UiPathConfig.project_id: + return project_id + + telemetry_file = os.path.join(directory, ".uipath", _TELEMETRY_CONFIG_FILE) + if os.path.exists(telemetry_file): + try: + with open(telemetry_file, "r") as f: + telemetry_data = json.load(f) + if project_id := telemetry_data.get(_PROJECT_KEY): + return project_id + except (json.JSONDecodeError, IOError): + pass + + return None + + class Severity(IntEnum): LOG = 0 WARNING = 1 @@ -592,21 +621,21 @@ async def download_folder_files( collect_files_from_folder(folder, "", files_dict) for file_path, remote_file in files_dict.items(): - local_path = base_path / file_path - local_path.parent.mkdir(parents=True, exist_ok=True) + local_path = anyio.Path(base_path / file_path) + await local_path.parent.mkdir(parents=True, exist_ok=True) response = await studio_client.download_project_file_async(remote_file) remote_content = response.read().decode("utf-8") remote_hash = compute_normalized_hash(remote_content) - if os.path.exists(local_path): - with open(local_path, "r", encoding="utf-8") as f: - local_content = f.read() - local_hash = compute_normalized_hash(local_content) + if await local_path.exists(): + local_content = await local_path.read_text(encoding="utf-8") + local_hash = compute_normalized_hash(local_content) if local_hash != remote_hash: - with open(local_path, "w", encoding="utf-8", newline="\n") as f: - f.write(remote_content) + await local_path.write_text( + remote_content, encoding="utf-8", newline="\n" + ) yield UpdateEvent( file_path=file_path, @@ -620,8 +649,7 @@ async def download_folder_files( message=f"File '{file_path}' is up to date", ) else: - with open(local_path, "w", encoding="utf-8", newline="\n") as f: - f.write(remote_content) + await local_path.write_text(remote_content, encoding="utf-8", newline="\n") yield UpdateEvent( file_path=file_path, diff --git a/packages/uipath/src/uipath/_cli/cli_debug.py b/packages/uipath/src/uipath/_cli/cli_debug.py index cd9042b78..92b8ea454 100644 --- a/packages/uipath/src/uipath/_cli/cli_debug.py +++ b/packages/uipath/src/uipath/_cli/cli_debug.py @@ -160,7 +160,6 @@ async def execute_debug_runtime(): debug_bridge: UiPathDebugProtocol = get_debug_bridge( ctx, attach=attach_mode ) - runtime = await factory.new_runtime( entrypoint, ctx.conversation_id or ctx.job_id or "default", diff --git a/packages/uipath/src/uipath/_cli/cli_init.py b/packages/uipath/src/uipath/_cli/cli_init.py index 80396d8ff..90a4ca117 100644 --- a/packages/uipath/src/uipath/_cli/cli_init.py +++ b/packages/uipath/src/uipath/_cli/cli_init.py @@ -9,6 +9,7 @@ from pathlib import Path from typing import Any +import anyio import click from graphtty import RenderOptions, render from graphtty.themes import TOKYO_NIGHT @@ -30,13 +31,11 @@ ) from uipath.runtime.schema import UiPathRuntimeGraph, UiPathRuntimeSchema -from .._utils.constants import ENV_TELEMETRY_ENABLED -from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE from ._telemetry import track_command from ._utils._common import determine_project_type from ._utils._console import ConsoleLogger from ._utils._constants import AGENT_INITIAL_CODE_VERSION, SCHEMA_VERSION -from ._utils._project_files import read_toml_project +from ._utils._project_files import read_toml_project, resolve_existing_project_id from .middlewares import Middlewares from .models.runtime_schema import Bindings, EntryPoint from .models.uipath_json_schema import UiPathJsonConfig @@ -54,30 +53,6 @@ class Action(str, enum.Enum): UPDATED = "Updated" -def create_telemetry_config_file(target_directory: str) -> None: - """Create telemetry file if telemetry is enabled. - - Args: - target_directory: The directory where the .uipath folder should be created. - """ - telemetry_enabled = os.getenv(ENV_TELEMETRY_ENABLED, "true").lower() == "true" - - if not telemetry_enabled: - return - - uipath_dir = os.path.join(target_directory, ".uipath") - telemetry_file = os.path.join(uipath_dir, _TELEMETRY_CONFIG_FILE) - - if os.path.exists(telemetry_file): - return - - os.makedirs(uipath_dir, exist_ok=True) - telemetry_data = {_PROJECT_KEY: UiPathConfig.project_id or str(uuid.uuid4())} - - with open(telemetry_file, "w") as f: - json.dump(telemetry_data, f, indent=4) - - def generate_env_file(target_directory): env_path = os.path.join(target_directory, ".env") @@ -421,7 +396,6 @@ def init(no_agents_md_override: bool) -> None: with console.spinner("Initializing UiPath project ..."): current_directory = os.getcwd() generate_env_file(current_directory) - create_telemetry_config_file(current_directory) async def initialize() -> list[UiPathRuntimeSchema]: try: @@ -429,10 +403,25 @@ async def initialize() -> list[UiPathRuntimeSchema]: config_path = UiPathConfig.config_file_path if not config_path.exists(): config = UiPathJsonConfig.create_default() + config.id = resolve_existing_project_id(current_directory) or str( + uuid.uuid4() + ) config.save_to_file(config_path) console.success(f"{Action.CREATED.value} '{config_path}' file.") else: - console.info(f"'{config_path}' already exists, skipping.") + # backfill id if not present + async_config_path = anyio.Path(config_path) + raw_config = json.loads(await async_config_path.read_text()) + if not raw_config.get("id"): + raw_config["id"] = resolve_existing_project_id( + current_directory + ) or str(uuid.uuid4()) + await async_config_path.write_text( + json.dumps(raw_config, indent=2) + ) + console.success( + f"{Action.UPDATED.value} '{config_path}' file with 'id'." + ) # Create bindings.json if it doesn't exist bindings_path = UiPathConfig.bindings_file_path diff --git a/packages/uipath/src/uipath/_cli/cli_pack.py b/packages/uipath/src/uipath/_cli/cli_pack.py index 83cedf870..d51eec53c 100644 --- a/packages/uipath/src/uipath/_cli/cli_pack.py +++ b/packages/uipath/src/uipath/_cli/cli_pack.py @@ -8,11 +8,10 @@ from pydantic import TypeAdapter from uipath._cli.models.runtime_schema import Bindings, EntryPoint, EntryPoints -from uipath._cli.models.uipath_json_schema import RuntimeOptions, UiPathJsonConfig +from uipath._cli.models.uipath_json_schema import UiPathJsonConfig from uipath.eval.constants import EVALS_FOLDER, LEGACY_EVAL_FOLDER from uipath.platform.common import UiPathConfig -from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE from ._telemetry import track_command from ._utils._common import determine_project_type from ._utils._console import ConsoleLogger @@ -21,6 +20,7 @@ files_to_include, get_project_config, read_toml_project, + resolve_existing_project_id, validate_config, ) from ._utils._uv_helpers import handle_uv_operations @@ -30,31 +30,6 @@ schema = "https://cloud.uipath.com/draft/2024-12/entry-point" -def get_project_id() -> str: - """Get project ID from telemetry file if it exists, otherwise generate a new one. - - Returns: - Project ID string (either from telemetry file or newly generated). - """ - # first check if this is a studio project - if project_id := UiPathConfig.project_id: - return project_id - - telemetry_file = os.path.join(".uipath", _TELEMETRY_CONFIG_FILE) - - if os.path.exists(telemetry_file): - try: - with open(telemetry_file, "r") as f: - telemetry_data = json.load(f) - project_id = telemetry_data.get(_PROJECT_KEY) - if project_id: - return project_id - except (json.JSONDecodeError, IOError): - pass - - return str(uuid.uuid4()) - - def get_project_version(directory): toml_path = os.path.join(directory, "pyproject.toml") if not os.path.exists(toml_path): @@ -72,14 +47,27 @@ def validate_config_structure(config_data): def generate_operate_file( - entrypoints: list[EntryPoint], runtimeOptions: RuntimeOptions, dependencies=None + entrypoints: list[EntryPoint], + config: UiPathJsonConfig, + dependencies=None, + directory: str = ".", ): if not entrypoints: raise ValueError( "No entry points found in entry-points.json. Please run 'uipath init' to generate valid entry points." ) - project_id = get_project_id() + # prefer id from uipath.json; fall back to the legacy + # .telemetry.json or SW project id. + if config.id: + try: + uuid.UUID(config.id) + except ValueError: + console.error(f"uipath.json 'id' must be a valid GUID, got '{config.id}'.") + + project_id = ( + config.id or resolve_existing_project_id(directory) or str(uuid.uuid4()) + ) project_type = determine_project_type(entrypoints) first_entry = entrypoints[0] @@ -94,7 +82,7 @@ def generate_operate_file( "runtimeOptions": { "requiresUserInteraction": False, "isAttended": False, - "isConversational": runtimeOptions.is_conversational, + "isConversational": config.runtime_options.is_conversational, }, } @@ -239,7 +227,7 @@ def pack_fn( config_data = TypeAdapter(UiPathJsonConfig).validate_python(json.load(f)) operate_file = generate_operate_file( - entrypoints, config_data.runtime_options, dependencies + entrypoints, config_data, dependencies, directory ) # try to read bindings from bindings.json diff --git a/packages/uipath/src/uipath/_cli/models/uipath_json_schema.py b/packages/uipath/src/uipath/_cli/models/uipath_json_schema.py index f1cd30202..4dd2f6700 100644 --- a/packages/uipath/src/uipath/_cli/models/uipath_json_schema.py +++ b/packages/uipath/src/uipath/_cli/models/uipath_json_schema.py @@ -68,6 +68,12 @@ class UiPathJsonConfig(BaseModelWithDefaultConfig): alias="$schema", description="Reference to the JSON schema for editor support", ) + id: str | None = Field( + default=None, + description="Stable unique identifier for the agent. Minted once at " + "project creation (by 'uipath init' or Studio Web) and preserved for the " + "lifetime of the project. Used as the package 'projectId' at pack time.", + ) runtime_options: RuntimeOptions = Field( default_factory=RuntimeOptions, alias="runtimeOptions", diff --git a/packages/uipath/src/uipath/telemetry/_track.py b/packages/uipath/src/uipath/telemetry/_track.py index 2d3f11ebf..3e585f8e5 100644 --- a/packages/uipath/src/uipath/telemetry/_track.py +++ b/packages/uipath/src/uipath/telemetry/_track.py @@ -105,18 +105,23 @@ def _get_connection_string() -> str | None: def _get_project_key() -> str: - """Get project key from telemetry file if present. + """Get the id used to attribute telemetry. - Returns: - Project key string if available, otherwise empty string. + Resolves ``uipath.json#id`` (then the runtime env var) via the shared + ``resolve_project_id`` helper, falling back to a legacy ``.uipath/.telemetry.json`` + ``ProjectKey`` if present. + Returns ``_UNKNOWN`` when no id is available. """ + from uipath.platform.common._span_utils import resolve_project_id + + if project_id := resolve_project_id(): + return project_id + try: telemetry_file = os.path.join(".uipath", _TELEMETRY_CONFIG_FILE) if os.path.exists(telemetry_file): with open(telemetry_file, "r") as f: - telemetry_data = json.load(f) - project_id = telemetry_data.get(_PROJECT_KEY) - if project_id: + if project_id := json.load(f).get(_PROJECT_KEY): return project_id except (json.JSONDecodeError, IOError, KeyError): pass diff --git a/packages/uipath/testcases/langchain-cross/pyproject.toml b/packages/uipath/testcases/langchain-cross/pyproject.toml index ae2717777..0420b6c48 100644 --- a/packages/uipath/testcases/langchain-cross/pyproject.toml +++ b/packages/uipath/testcases/langchain-cross/pyproject.toml @@ -10,4 +10,11 @@ dependencies = [ requires-python = ">=3.11" [tool.uv.sources] -uipath = { path = "../../", editable = true } \ No newline at end of file +uipath = { path = "../../", editable = true } + +# Force the local uipath (the version under test) regardless of the upper bound +# the published uipath-langchain declares, so a backward-compatible minor bump +# does not break resolution purely on that cap. Mirrors the uv override used in +# the cross-repo test workflows. +[tool.uv] +override-dependencies = ["uipath"] \ No newline at end of file diff --git a/packages/uipath/tests/cli/eval/test_eval_telemetry.py b/packages/uipath/tests/cli/eval/test_eval_telemetry.py index 48911638a..49a71bc2f 100644 --- a/packages/uipath/tests/cli/eval/test_eval_telemetry.py +++ b/packages/uipath/tests/cli/eval/test_eval_telemetry.py @@ -25,6 +25,7 @@ EvalSetRunCreatedEvent, EvalSetRunUpdatedEvent, ) +from uipath.platform.common.constants import ENV_UIPATH_AGENT_ID class TestEventNameConstants: @@ -422,6 +423,10 @@ def test_enrich_properties_adds_env_vars(self, mock_get_claim): """Test that environment variables are added when present.""" mock_get_claim.return_value = "user-789" + from uipath.platform.common._span_utils import _read_config_id + + _read_config_id.cache_clear() + subscriber = EvalTelemetrySubscriber() properties: dict[str, Any] = {} @@ -429,6 +434,7 @@ def test_enrich_properties_adds_env_vars(self, mock_get_claim): os.environ, { "UIPATH_PROJECT_ID": "project-123", + ENV_UIPATH_AGENT_ID: "agent-123", "UIPATH_ORGANIZATION_ID": "org-456", "UIPATH_TENANT_ID": "tenant-abc", "UIPATH_EVAL_RUN_SOURCE": "FirstSuccessfulRun", @@ -437,7 +443,7 @@ def test_enrich_properties_adds_env_vars(self, mock_get_claim): subscriber._enrich_properties(properties) assert properties["ProjectId"] == "project-123" - assert properties["AgentId"] == "project-123" + assert properties["AgentId"] == "agent-123" assert properties["CloudOrganizationId"] == "org-456" assert properties["CloudUserId"] == "user-789" assert properties["TenantId"] == "tenant-abc" @@ -448,6 +454,10 @@ def test_enrich_properties_skips_missing_env_vars(self, mock_get_claim): """Test that missing environment variables are not added.""" mock_get_claim.side_effect = Exception("No token") + from uipath.platform.common._span_utils import _read_config_id + + _read_config_id.cache_clear() + subscriber = EvalTelemetrySubscriber() properties: dict[str, Any] = {} @@ -455,6 +465,7 @@ def test_enrich_properties_skips_missing_env_vars(self, mock_get_claim): # Remove env vars if they exist for key in [ "UIPATH_PROJECT_ID", + ENV_UIPATH_AGENT_ID, "UIPATH_ORGANIZATION_ID", "UIPATH_TENANT_ID", "UIPATH_EVAL_RUN_SOURCE", diff --git a/packages/uipath/tests/cli/test_init.py b/packages/uipath/tests/cli/test_init.py index 59d4eaaa8..afa5d15fa 100644 --- a/packages/uipath/tests/cli/test_init.py +++ b/packages/uipath/tests/cli/test_init.py @@ -1,5 +1,6 @@ import json import os +import uuid from unittest.mock import patch import pytest @@ -57,6 +58,101 @@ def test_init_creates_empty_uipath_json( assert isinstance(config["functions"], dict) assert len(config["functions"]) == 0 + def test_init_mints_agent_id_in_uipath_json( + self, runner: CliRunner, temp_dir: str + ) -> None: + """init writes a valid id into a newly created uipath.json.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + self._generate_pyproject() + result = runner.invoke(cli, ["init"], env={}) + assert result.exit_code == 0 + + with open("uipath.json", "r") as f: + config = json.load(f) + assert "id" in config + # Must be a valid UUID-shaped identifier. + uuid.UUID(config["id"]) + + def test_init_does_not_create_telemetry_file( + self, runner: CliRunner, temp_dir: str + ) -> None: + """init no longer writes .uipath/.telemetry.json; the id lives in uipath.json.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + self._generate_pyproject() + result = runner.invoke(cli, ["init"], env={}) + assert result.exit_code == 0 + + assert not os.path.exists(os.path.join(".uipath", ".telemetry.json")) + with open("uipath.json", "r") as f: + uuid.UUID(json.load(f)["id"]) + + def test_init_mints_agent_id_with_telemetry_disabled( + self, runner: CliRunner, temp_dir: str + ) -> None: + """id is still written when telemetry is opted out.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + self._generate_pyproject() + result = runner.invoke( + cli, ["init"], env={"UIPATH_TELEMETRY_ENABLED": "false"} + ) + assert result.exit_code == 0 + + assert not os.path.exists(os.path.join(".uipath", ".telemetry.json")) + with open("uipath.json", "r") as f: + config = json.load(f) + uuid.UUID(config["id"]) + + def test_init_preserves_existing_agent_id( + self, runner: CliRunner, temp_dir: str + ) -> None: + """init keeps an id already present in uipath.json (first writer wins).""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("main.py", "w") as f: + f.write("def main(input: str) -> str: return input") + with open("uipath.json", "w") as f: + json.dump( + { + "id": "existing-agent-id", + "functions": {"main": "main.py:main"}, + }, + f, + ) + self._generate_pyproject() + + result = runner.invoke(cli, ["init"], env={}) + assert result.exit_code == 0 + # Existing id must not be backfilled/overwritten. + assert "with 'id'" not in result.output + + with open("uipath.json", "r") as f: + assert json.load(f)["id"] == "existing-agent-id" + + def test_init_backfills_agent_id_from_telemetry( + self, runner: CliRunner, temp_dir: str + ) -> None: + """init backfills id on an existing uipath.json, reusing the telemetry key.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("main.py", "w") as f: + f.write("def main(input: str) -> str: return input") + with open("uipath.json", "w") as f: + json.dump({"functions": {"main": "main.py:main"}}, f) + os.makedirs(".uipath", exist_ok=True) + with open(os.path.join(".uipath", ".telemetry.json"), "w") as f: + json.dump({"ProjectKey": "legacy-project-key"}, f) + self._generate_pyproject() + + result = runner.invoke(cli, ["init"], env={}) + assert result.exit_code == 0 + assert "Updated 'uipath.json' file with 'id'" in result.output + + with open("uipath.json", "r") as f: + config = json.load(f) + assert config["id"] == "legacy-project-key" + # Existing fields are preserved. + assert config["functions"]["main"] == "main.py:main" + # The backfill is targeted: no defaulted fields are materialized. + assert set(config.keys()) == {"functions", "id"} + def test_init_with_existing_uipath_json( self, runner: CliRunner, temp_dir: str ) -> None: diff --git a/packages/uipath/tests/cli/test_pack.py b/packages/uipath/tests/cli/test_pack.py index cf6bd5cc1..c644b3c7d 100644 --- a/packages/uipath/tests/cli/test_pack.py +++ b/packages/uipath/tests/cli/test_pack.py @@ -10,17 +10,17 @@ import uipath._cli.cli_pack as cli_pack from uipath._cli import cli from uipath._cli.middlewares import MiddlewareResult -from uipath._cli.models.uipath_json_schema import RuntimeOptions +from uipath._cli.models.uipath_json_schema import RuntimeOptions, UiPathJsonConfig -def create_bindings_file(): +def create_bindings_file(directory: str = "."): """Helper to create a default bindings.json file for tests.""" bindings_content = {"version": "2.0", "resources": []} - with open("bindings.json", "w") as f: + with open(os.path.join(directory, "bindings.json"), "w") as f: json.dump(bindings_content, f, indent=4) -def create_entry_points_file(entrypoint_type: str = "function"): +def create_entry_points_file(entrypoint_type: str = "function", directory: str = "."): """Helper to create a default entry-points.json file for tests.""" entry_points_content = { "$schema": "https://cloud.uipath.com/draft/2024-12/entry-point", @@ -38,7 +38,7 @@ def create_entry_points_file(entrypoint_type: str = "function"): } ], } - with open("entry-points.json", "w") as f: + with open(os.path.join(directory, "entry-points.json"), "w") as f: json.dump(entry_points_content, f, indent=4) @@ -1096,14 +1096,17 @@ def test_generate_operate_file(self, runner: CliRunner, temp_dir: str) -> None: ) ] - operate_data = cli_pack.generate_operate_file( - entrypoints, RuntimeOptions(is_conversational=False) + config = UiPathJsonConfig( + runtimeOptions=RuntimeOptions(is_conversational=False), + id="00000000-0000-0000-0000-000000000001", ) + operate_data = cli_pack.generate_operate_file(entrypoints, config) assert ( operate_data["$schema"] == "https://cloud.uipath.com/draft/2024-12/entry-point" ) + assert operate_data["projectId"] == "00000000-0000-0000-0000-000000000001" assert operate_data["main"] == "agent1.py" assert operate_data["contentType"] == "agent" assert operate_data["targetFramework"] == "Portable" @@ -1114,6 +1117,119 @@ def test_generate_operate_file(self, runner: CliRunner, temp_dir: str) -> None: "isConversational": False, } + def test_pack_uses_agent_id_as_project_id( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + ) -> None: + """operate.json projectId is sourced from uipath.json#id.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + config = create_uipath_json() + config["id"] = "00000000-0000-0000-0000-000000000001" + with open("uipath.json", "w") as f: + json.dump(config, f) + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + with open("main.py", "w") as f: + f.write("def main(input): return input") + create_bindings_file() + create_entry_points_file() + + result = runner.invoke(cli, ["pack", "./"], env={}) + assert result.exit_code == 0 + + with zipfile.ZipFile( + f".uipath/{project_details.name}.{project_details.version}.nupkg", "r" + ) as z: + operate_data = json.loads(z.read("content/operate.json")) + assert operate_data["projectId"] == "00000000-0000-0000-0000-000000000001" + + def test_pack_fails_when_id_is_not_a_guid( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + ) -> None: + """pack fails when uipath.json#id is set but is not a valid GUID.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + config = create_uipath_json() + config["id"] = "not-a-guid" + with open("uipath.json", "w") as f: + json.dump(config, f) + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + with open("main.py", "w") as f: + f.write("def main(input): return input") + create_bindings_file() + create_entry_points_file() + + result = runner.invoke(cli, ["pack", "./"], env={}) + assert result.exit_code != 0 + assert "must be a valid GUID" in result.output + + def test_pack_falls_back_to_telemetry_project_key( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + ) -> None: + """Without id, operate.json projectId falls back to the telemetry key.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + # uipath.json deliberately has no id (legacy project). + with open("uipath.json", "w") as f: + json.dump(create_uipath_json(), f) + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + with open("main.py", "w") as f: + f.write("def main(input): return input") + create_bindings_file() + create_entry_points_file() + os.makedirs(".uipath", exist_ok=True) + with open(os.path.join(".uipath", ".telemetry.json"), "w") as f: + json.dump({"ProjectKey": "telemetry-fallback-key"}, f) + + result = runner.invoke(cli, ["pack", "./"], env={}) + assert result.exit_code == 0 + + with zipfile.ZipFile( + f".uipath/{project_details.name}.{project_details.version}.nupkg", "r" + ) as z: + operate_data = json.loads(z.read("content/operate.json")) + assert operate_data["projectId"] == "telemetry-fallback-key" + + def test_pack_telemetry_fallback_from_outside_project_dir( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + ) -> None: + """The legacy telemetry fallback resolves against the packed directory, not CWD.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + os.makedirs("project") + with open(os.path.join("project", "uipath.json"), "w") as f: + json.dump(create_uipath_json(), f) + with open(os.path.join("project", "pyproject.toml"), "w") as f: + f.write(project_details.to_toml()) + with open(os.path.join("project", "main.py"), "w") as f: + f.write("def main(input): return input") + create_bindings_file(directory="project") + create_entry_points_file(directory="project") + os.makedirs(os.path.join("project", ".uipath"), exist_ok=True) + with open(os.path.join("project", ".uipath", ".telemetry.json"), "w") as f: + json.dump({"ProjectKey": "telemetry-fallback-key"}, f) + + result = runner.invoke(cli, ["pack", "./project"], env={}) + assert result.exit_code == 0 + + # the package itself is written under the caller's CWD + with zipfile.ZipFile( + f".uipath/{project_details.name}.{project_details.version}.nupkg", + "r", + ) as z: + operate_data = json.loads(z.read("content/operate.json")) + assert operate_data["projectId"] == "telemetry-fallback-key" + def test_generate_bindings_content(self, runner: CliRunner, temp_dir: str) -> None: """Test generating bindings content.""" bindings_data = cli_pack.generate_bindings_content() diff --git a/packages/uipath/tests/telemetry/test_track.py b/packages/uipath/tests/telemetry/test_track.py index fe72130d5..827b3878d 100644 --- a/packages/uipath/tests/telemetry/test_track.py +++ b/packages/uipath/tests/telemetry/test_track.py @@ -1,11 +1,20 @@ """Tests for telemetry tracking functionality.""" +import json import os from unittest.mock import MagicMock, patch +import pytest + +from uipath.platform.common.constants import ( + ENV_PROJECT_KEY, + ENV_UIPATH_AGENT_ID, + ENV_UIPATH_PROJECT_ID, +) from uipath.telemetry._track import ( _AppInsightsEventClient, _DiagnosticSender, + _get_project_key, _parse_connection_string, _TelemetryClient, flush_events, @@ -17,6 +26,43 @@ ) +class TestGetProjectKey: + """`_get_project_key` resolution: uipath.json#id, then legacy telemetry file.""" + + @pytest.fixture(autouse=True) + def _clear_cache(self, monkeypatch): + from uipath.platform.common._span_utils import _read_config_id + + _read_config_id.cache_clear() + for var in (ENV_UIPATH_AGENT_ID, ENV_UIPATH_PROJECT_ID, ENV_PROJECT_KEY): + monkeypatch.delenv(var, raising=False) + yield + _read_config_id.cache_clear() + + def test_prefers_uipath_json_id(self, monkeypatch, tmp_path): + config_id = "00000000-0000-0000-0000-000000000001" + (tmp_path / "uipath.json").write_text(json.dumps({"id": config_id})) + os.makedirs(tmp_path / ".uipath", exist_ok=True) + (tmp_path / ".uipath" / ".telemetry.json").write_text( + json.dumps({"ProjectKey": "from-telemetry"}) + ) + monkeypatch.chdir(tmp_path) + assert _get_project_key() == config_id + + def test_falls_back_to_legacy_telemetry_file(self, monkeypatch, tmp_path): + # No uipath.json#id and no env var; honor an existing .telemetry.json. + os.makedirs(tmp_path / ".uipath", exist_ok=True) + (tmp_path / ".uipath" / ".telemetry.json").write_text( + json.dumps({"ProjectKey": "from-telemetry"}) + ) + monkeypatch.chdir(tmp_path) + assert _get_project_key() == "from-telemetry" + + def test_unknown_when_no_source(self, monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + assert _get_project_key() == "" + + class TestParseConnectionString: """Test connection string parsing functionality.""" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 6f903c231..a0c0cd96d 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.84" +version = "2.11.0" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.66" +version = "0.1.67" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From b2b36089b36b96dffc99ad361b67a628366d29f4 Mon Sep 17 00:00:00 2001 From: Giulia Imbrea Date: Tue, 16 Jun 2026 20:36:39 +0300 Subject: [PATCH 103/121] docs: add SETUP.MD (#1682) --- SETUP.MD | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 SETUP.MD diff --git a/SETUP.MD b/SETUP.MD new file mode 100644 index 000000000..4e728471b --- /dev/null +++ b/SETUP.MD @@ -0,0 +1,133 @@ +# SETUP.MD + +This file documents how to provision a clean development environment for the three packages in this repo (`uipath-core`, `uipath-platform`, `uipath`), run the build, execute the tests, and validate a sample code change end-to-end. It is intended both as a quick reference for human contributors and as a structured guide for automated environment-setup tooling. + +## Prerequisites + +- Python 3.11+ +- [uv](https://docs.astral.sh/uv/) 0.5+ + +### Supported platforms + +`uv` is shell- and OS-agnostic, so the commands below run unchanged on every supported platform: + +- [x] Linux +- [x] Windows +- [x] macOS + +## Environment Variables + +None required for environment setup, build, or unit tests. The suites under the `Test` section run fully offline and require no external authentication. + +> **All commands below must be run from the repository root.** The `uv --directory packages/` invocations resolve each subpackage relative to the current working directory. The first line of `## Setup` enforces this by `cd`-ing to the git root. + +## Setup + +```bash +cd "$(git rev-parse --show-toplevel)" +python3 -m pip install --upgrade uv + +# Sync all three packages (dependency order: core → platform → main) +uv --directory packages/uipath-core sync --all-extras +uv --directory packages/uipath-platform sync --all-extras +uv --directory packages/uipath sync --all-extras +``` + +## Verify Setup + +```bash +uv --version +uv --directory packages/uipath-core run python --version +uv --directory packages/uipath-core run python -c "import uipath.core; print('uipath-core ok')" +uv --directory packages/uipath-platform run python -c "import uipath.platform; print('uipath-platform ok')" +uv --directory packages/uipath run python -c "import uipath; print('uipath ok')" +``` + +## Build + +N/A + +## Test + +```bash +uv --directory packages/uipath-core run pytest +uv --directory packages/uipath-platform run pytest +uv --directory packages/uipath run pytest +``` + +> Note: `uipath-platform`'s `pyproject.toml` already excludes its E2E tests via `addopts = "... -m 'not e2e'"`. `uipath-core` and `uipath` do not register an `e2e` marker. + +## Sample Code Change + +### The change + +Add a new `size` property to `SpanRegistry` in `packages/uipath-core/src/uipath/core/tracing/span_utils.py`, immediately after the `clear` method and before the `# Global span registry instance` comment: + +```python +@property +def size(self) -> int: + """Return the number of currently registered spans.""" + return len(self._spans) +``` + +Then create `packages/uipath-core/tests/tracing/test_span_registry_size.py` with two pytest tests: + +```python +from unittest.mock import MagicMock + +from uipath.core.tracing.span_utils import SpanRegistry + + +def _make_span(span_id: int) -> MagicMock: + span = MagicMock() + span.get_span_context.return_value.span_id = span_id + span.parent = None # registered as a root span (no parent) + return span + + +def test_size_empty_registry() -> None: + registry = SpanRegistry() + assert registry.size == 0 + + +def test_size_after_registrations() -> None: + registry = SpanRegistry() + registry.register_span(_make_span(1)) + registry.register_span(_make_span(2)) + assert registry.size == 2 +``` + +### Verification + +```bash +uv --directory packages/uipath-core run pytest tests/tracing/test_span_registry_size.py -v +``` + +## Test with a real UiPath Coded Agent + +> This section is for human contributors who want to validate changes end-to-end against the real cloud platform. It is **not executed by the Agentic Inner Loop validation pipeline** — that pipeline only runs the sections above (Setup → Verify → Build → Test → Sample Code Change). + +The unit tests above are necessary but not sufficient — they don't exercise the package end-to-end through a real agent. The flow below validates changes against a live runtime: + +1. Apply the code changes locally. +2. Run the unit tests (see the `Sample Code Change` section above). +3. Scaffold a coded UiPath agent that exercises the changed code path. +4. In the downstream project's `pyproject.toml`, add this local library as an editable dependency (substitute `uipath`, `uipath-platform`, or `uipath-core` depending on which package you changed): + + ```toml + [tool.uv.sources] + uipath = { path = "../path/to/uipath-python/packages/uipath", editable = true } + ``` + +5. Exercise the new behavior end-to-end: + + ```bash + uv run uipath run --input '{...}' + ``` + +6. (Optional) Open a PR and apply the `build:dev` label — this publishes the development version to Test PyPI. +7. The PR description is updated automatically with instructions for pointing the downstream agent at the Test PyPI dev version. +8. Validate the new behavior against the real platform — use either or both of the deploy targets below (Studio Web and Orchestrator are not mutually exclusive): + - **Studio Web**: export the `UIPATH_PROJECT_ID` environment variable pointing to an existing Coded Agent project in your solution, then run [`uipath push`](https://uipath.github.io/uipath-python/cli/#push) to push the dev version to that project. Open it in Studio Web and exercise the changed code path. + - **Orchestrator**: run [`uipath deploy`](https://uipath.github.io/uipath-python/cli/#deploy) to deploy the dev version as a package, then start a job in Orchestrator and exercise the changed code path. +9. Once validation is done, close the dev PR — these PRs are not meant to be merged; their only purpose was to publish a Test PyPI build for end-to-end validation. From cc8284c23d691a096761bd4b0ed61c03ac3b3dd0 Mon Sep 17 00:00:00 2001 From: kunaluipath <138852527+kunaluipath@users.noreply.github.com> Date: Wed, 17 Jun 2026 02:32:04 -0400 Subject: [PATCH 104/121] feat(agent): add tool-output and V2 escalation recipient types [ACTN-10480] (#1667) Co-authored-by: Claude Opus 4.7 --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/action_center/_tasks_service.py | 28 +++ .../uipath/platform/action_center/tasks.py | 19 +- .../tests/services/test_actions_service.py | 162 +++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 4 +- .../uipath/src/uipath/agent/models/agent.py | 99 +++++++++- .../uipath/tests/agent/models/test_agent.py | 185 ++++++++++++++++++ packages/uipath/uv.lock | 4 +- 9 files changed, 497 insertions(+), 8 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 464549970..698270df8 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.67" +version = "0.1.68" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py index 491b1bd73..2c4a4bde7 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/_tasks_service.py @@ -305,6 +305,34 @@ async def _assign_task_spec( } ] } + elif task_recipient.type == TaskRecipientType.WORKLOAD: + # This branch covers BOTH agent-side Workload criteria (single + # group, distributed by workload) AND agent-side CustomAssignees + # criteria (explicit email list — already resolved into + # `task_recipient.values` upstream). Both submit to the Action + # Center API as a "Workload" assignment; the difference is whether + # `values` carries one group or N emails. + request_spec.json = { + "taskAssignments": [ + { + "taskId": task_key, + "assignmentCriteria": "Workload", + "assigneeNamesOrEmails": task_recipient.values + or [recipient_value], + } + ] + } + elif task_recipient.type == TaskRecipientType.ROUND_ROBIN: + request_spec.json = { + "taskAssignments": [ + { + "taskId": task_key, + "assignmentCriteria": "RoundRobin", + "assigneeNamesOrEmails": task_recipient.values + or [recipient_value], + } + ] + } else: request_spec.json = { "taskAssignments": [ diff --git a/packages/uipath-platform/src/uipath/platform/action_center/tasks.py b/packages/uipath-platform/src/uipath/platform/action_center/tasks.py index f882cf40f..f1a932cb8 100644 --- a/packages/uipath-platform/src/uipath/platform/action_center/tasks.py +++ b/packages/uipath-platform/src/uipath/platform/action_center/tasks.py @@ -22,18 +22,35 @@ class TaskRecipientType(str, enum.Enum): GROUP_ID = "GroupId" EMAIL = "UserEmail" GROUP_NAME = "GroupName" + WORKLOAD = "Workload" + ROUND_ROBIN = "RoundRobin" class TaskRecipient(BaseModel): - """Model representing a task recipient.""" + """Model representing a task recipient. + + `value` is the single identifier (group name, group id, user id, email, …). + `values` is the multi-assignee form used by Workload-with-custom-emails + assignments; when set it takes precedence over `value` for the + `assigneeNamesOrEmails` payload. + + Note: there is no CustomAssignees member here on purpose. The agent-side + CustomAssignees criteria (AgentEscalationRecipientType.CUSTOM_ASSIGNEES, + type 11) is resolved to a Workload assignment with the explicit email list + in `values` before reaching this layer, so the Action Center + AssignTasks API only ever sees the existing literal types. + """ type: Literal[ TaskRecipientType.USER_ID, TaskRecipientType.GROUP_ID, TaskRecipientType.EMAIL, TaskRecipientType.GROUP_NAME, + TaskRecipientType.WORKLOAD, + TaskRecipientType.ROUND_ROBIN, ] = Field(..., alias="type") value: str = Field(..., alias="value") + values: Optional[List[str]] = Field(default=None, alias="values") display_name: Optional[str] = Field(default=None, alias="displayName") diff --git a/packages/uipath-platform/tests/services/test_actions_service.py b/packages/uipath-platform/tests/services/test_actions_service.py index 6758c452a..ea5fe2c84 100644 --- a/packages/uipath-platform/tests/services/test_actions_service.py +++ b/packages/uipath-platform/tests/services/test_actions_service.py @@ -7,6 +7,7 @@ from uipath.platform import UiPathApiConfig, UiPathExecutionContext from uipath.platform.action_center import Task from uipath.platform.action_center._tasks_service import TasksService +from uipath.platform.action_center.tasks import TaskRecipient, TaskRecipientType from uipath.platform.common.constants import HEADER_USER_AGENT @@ -186,6 +187,167 @@ def test_create_with_assignee( assert action.title == "Test Action" +def _mock_app_lookup_and_create( + httpx_mock: HTTPXMock, + base_url: str, + org: str, + tenant: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Common httpx mock setup for app lookup + task creation + assign.""" + monkeypatch.setenv("UIPATH_TENANT_ID", "test-tenant-id") + httpx_mock.add_response( + url=f"{base_url}{org}/apps_/default/api/v1/default/deployed-action-apps-schemas?search=test-app&filterByDeploymentTitle=true", + status_code=200, + json={ + "deployed": [ + { + "systemName": "test-app", + "deploymentTitle": "test-app", + "actionSchema": { + "key": "test-key", + "inputs": [], + "outputs": [], + "inOuts": [], + "outcomes": [], + }, + "deploymentFolder": { + "fullyQualifiedName": "test-folder-path", + "key": "test-folder-key", + }, + } + ] + }, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/tasks/AppTasks/CreateAppTask", + status_code=200, + json={"id": 1, "title": "Test Action"}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Tasks/UiPath.Server.Configuration.OData.AssignTasks", + status_code=200, + json={}, + ) + + +def _assign_request_payload(httpx_mock: HTTPXMock) -> dict[str, Any]: + """Return the parsed JSON body of the last AssignTasks request captured by the mock.""" + assign_request = next( + req + for req in reversed(httpx_mock.get_requests()) + if "AssignTasks" in str(req.url) + ) + return json.loads(assign_request.content) + + +class TestAssignTaskSpec: + """Tests for the task-assignment payload built by `_assign_task_spec`.""" + + def test_assign_workload_recipient_uses_workload_criteria_with_group( + self, + httpx_mock: HTTPXMock, + service: TasksService, + base_url: str, + org: str, + tenant: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + _mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch) + + service.create( + title="Test Action", + app_name="test-app", + data={"x": 1}, + recipient=TaskRecipient( + type=TaskRecipientType.WORKLOAD, + value="Support Team", + displayName="Support Team", + ), + ) + + payload = _assign_request_payload(httpx_mock) + assert payload == { + "taskAssignments": [ + { + "taskId": 1, + "assignmentCriteria": "Workload", + "assigneeNamesOrEmails": ["Support Team"], + } + ] + } + + def test_assign_round_robin_recipient_uses_round_robin_criteria( + self, + httpx_mock: HTTPXMock, + service: TasksService, + base_url: str, + org: str, + tenant: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + _mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch) + + service.create( + title="Test Action", + app_name="test-app", + data={"x": 1}, + recipient=TaskRecipient( + type=TaskRecipientType.ROUND_ROBIN, + value="Support Team", + displayName="Support Team", + ), + ) + + payload = _assign_request_payload(httpx_mock) + assert payload == { + "taskAssignments": [ + { + "taskId": 1, + "assignmentCriteria": "RoundRobin", + "assigneeNamesOrEmails": ["Support Team"], + } + ] + } + + def test_assign_workload_with_multiple_emails_uses_values_list( + self, + httpx_mock: HTTPXMock, + service: TasksService, + base_url: str, + org: str, + tenant: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Custom-assignees path: Workload criteria with a list of emails.""" + _mock_app_lookup_and_create(httpx_mock, base_url, org, tenant, monkeypatch) + + service.create( + title="Test Action", + app_name="test-app", + data={"x": 1}, + recipient=TaskRecipient( + type=TaskRecipientType.WORKLOAD, + value="alice@example.com", + values=["alice@example.com", "bob@example.com"], + ), + ) + + payload = _assign_request_payload(httpx_mock) + assert payload == { + "taskAssignments": [ + { + "taskId": 1, + "assignmentCriteria": "Workload", + "assigneeNamesOrEmails": [ + "alice@example.com", + "bob@example.com", + ], + } + ] + } + + def _make_deployed_app( name: str, folder_path: str, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 0e01f1f73..5d4c41d63 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.67" +version = "0.1.68" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index be0c4c22d..0471ece40 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.11.0" +version = "2.11.1" 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" dependencies = [ "uipath-core>=0.5.17, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.67, <0.2.0", + "uipath-platform>=0.1.68, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 385a37e7c..a14066969 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -140,6 +140,9 @@ class AgentEscalationRecipientType(str, CaseInsensitiveEnum): ASSET_GROUP_NAME = "AssetGroupName" ARGUMENT_EMAIL = "ArgumentEmail" ARGUMENT_GROUP_NAME = "ArgumentGroupName" + WORKLOAD = "Workload" + ROUND_ROBIN = "RoundRobin" + CUSTOM_ASSIGNEES = "CustomAssignees" class AgentEscalationChannelType(str, CaseInsensitiveEnum): @@ -547,6 +550,9 @@ class AgentA2aResourceConfig(BaseAgentResourceConfig): 6: AgentEscalationRecipientType.ASSET_GROUP_NAME, 7: AgentEscalationRecipientType.ARGUMENT_EMAIL, 8: AgentEscalationRecipientType.ARGUMENT_GROUP_NAME, + 9: AgentEscalationRecipientType.WORKLOAD, + 10: AgentEscalationRecipientType.ROUND_ROBIN, + 11: AgentEscalationRecipientType.CUSTOM_ASSIGNEES, } @@ -626,14 +632,105 @@ class ArgumentGroupNameRecipient(BaseEscalationRecipient): argument_path: str = Field(..., alias="argumentName") +class WorkloadRecipient(BaseEscalationRecipient): + """Workload-based group assignment. + + The Action Center distributes tasks to the group member with the lightest workload. + """ + + type: Literal[AgentEscalationRecipientType.WORKLOAD,] = Field(..., alias="type") + value: str = Field(..., alias="value") + display_name: str = Field(..., alias="displayName") + + +class RoundRobinRecipient(BaseEscalationRecipient): + """Round-robin group assignment. + + The Action Center cycles through group members in order on each new task. + """ + + type: Literal[AgentEscalationRecipientType.ROUND_ROBIN,] = Field(..., alias="type") + value: str = Field(..., alias="value") + display_name: str = Field(..., alias="displayName") + + +class CustomAssigneesRecipient(BaseEscalationRecipient): + """Custom multi-user assignment. + + A channel can carry multiple instances, one per assignee email. All are passed + to Action Center together using a Workload assignment criteria. + """ + + type: Literal[AgentEscalationRecipientType.CUSTOM_ASSIGNEES,] = Field( + ..., alias="type" + ) + value: str = Field(..., alias="value") + display_name: Optional[str] = Field(default=None, alias="displayName") + + +class ToolOutputRecipient(BaseEscalationRecipient): + """Recipient whose value is resolved at runtime from a named tool's output. + + Instead of a literal value entered at design time, this binding points at a + field within a named tool's output. The runtime walks the agent's message + history, finds the most recent ToolMessage matching `tool_name`, parses its + content as JSON, and extracts `output_path` (a top-level field for v1). + + Only the assignment-criteria recipient types that accept a runtime-computed + value are supported: USER_ID, GROUP_ID, WORKLOAD, ROUND_ROBIN, + CUSTOM_ASSIGNEES. The asset/static/argument types do not participate in + tool-output binding (they have their own design-time resolution rules). + """ + + type: Literal[ + AgentEscalationRecipientType.USER_ID, + AgentEscalationRecipientType.GROUP_ID, + AgentEscalationRecipientType.WORKLOAD, + AgentEscalationRecipientType.ROUND_ROBIN, + AgentEscalationRecipientType.CUSTOM_ASSIGNEES, + ] = Field(..., alias="type") + source: Literal["toolOutput"] = Field(..., alias="source") + tool_name: str = Field(..., alias="toolName") + output_path: str = Field(..., alias="outputPath") + + +# ────────────────────────────────────────────────────────────────────────────── +# AgentEscalationRecipient — Union ordering & invariants +# ────────────────────────────────────────────────────────────────────────────── +# Pydantic evaluates Union members left-to-right and stops at the first +# successful match, so member order determines which class a payload resolves +# to when multiple members share the same `type` value (e.g. WORKLOAD is valid +# on both WorkloadRecipient and ToolOutputRecipient). +# +# How dispatching works: +# - Payload with `source: "toolOutput"` → matches ToolOutputRecipient +# (it is the only class declaring `source` as a required Literal field). +# - Payload without `source` → ToolOutputRecipient validation fails +# (`source` missing), so it falls through to the literal class below +# that owns its `type`. +# +# Why we don't use `Field(discriminator="type")`: +# The `type` values are NOT unique across the Union — both WorkloadRecipient +# and ToolOutputRecipient declare `type=WORKLOAD`, same for the other +# tool-output-capable criteria. A typed discriminator requires unique +# discriminator values across members, which this union violates by design. +# +# Critical invariants (any of these breaking causes silent mis-typing): +# 1. ToolOutputRecipient remains the FIRST member of the Union. +# 2. ToolOutputRecipient.source remains a required `Literal["toolOutput"]` +# (NOT `Optional`, NOT a default value). +# 3. No literal class below it gains an optional `source` field. AgentEscalationRecipient = Annotated[ Union[ + ToolOutputRecipient, StandardRecipient, AssetRecipient, ArgumentEmailRecipient, ArgumentGroupNameRecipient, + WorkloadRecipient, + RoundRobinRecipient, + CustomAssigneesRecipient, ], - Field(discriminator="type"), BeforeValidator(_normalize_recipient_type), ] diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index c324d4e7e..ab0e6a557 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -51,12 +51,16 @@ BatchTransformFileExtension, BatchTransformWebSearchGrounding, CitationMode, + CustomAssigneesRecipient, DeepRagFileExtension, + RoundRobinRecipient, StandardRecipient, TaskTitleType, TextBuilderTaskTitle, TextToken, TextTokenType, + ToolOutputRecipient, + WorkloadRecipient, ) from uipath.platform.guardrails import ( EnumListParameterValue, @@ -4368,3 +4372,184 @@ def test_agent_with_client_side_tool_output_schema_alias(self): assert tool.output_schema["type"] == "object" assert "imageBase64" in tool.output_schema["properties"] assert tool.output_schema["required"] == ["imageBase64"] + + +class TestCustomAssignmentRecipientDeserialization: + def test_workload_recipient_by_type_int(self): + payload = {"type": 9, "value": "group-1", "displayName": "Support Team"} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, WorkloadRecipient) + assert recipient.value == "group-1" + assert recipient.display_name == "Support Team" + assert recipient.type == AgentEscalationRecipientType.WORKLOAD + + def test_workload_recipient_by_type_string(self): + payload = { + "type": "Workload", + "value": "group-1", + "displayName": "Support Team", + } + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, WorkloadRecipient) + assert recipient.value == "group-1" + assert recipient.display_name == "Support Team" + + def test_round_robin_recipient_by_type_int(self): + payload = {"type": 10, "value": "group-1", "displayName": "Support Team"} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, RoundRobinRecipient) + assert recipient.value == "group-1" + assert recipient.display_name == "Support Team" + assert recipient.type == AgentEscalationRecipientType.ROUND_ROBIN + + def test_round_robin_recipient_by_type_string(self): + payload = { + "type": "RoundRobin", + "value": "group-1", + "displayName": "Support Team", + } + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, RoundRobinRecipient) + + def test_custom_assignees_recipient_by_type_int(self): + payload = { + "type": 11, + "value": "alice@example.com", + "displayName": "Alice", + } + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, CustomAssigneesRecipient) + assert recipient.value == "alice@example.com" + assert recipient.display_name == "Alice" + assert recipient.type == AgentEscalationRecipientType.CUSTOM_ASSIGNEES + + def test_custom_assignees_recipient_by_type_string(self): + payload = {"type": "CustomAssignees", "value": "alice@example.com"} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, CustomAssigneesRecipient) + assert recipient.value == "alice@example.com" + assert recipient.display_name is None + + def test_custom_assignees_recipient_accepts_empty_value_sentinel(self): + payload = {"type": 11, "value": ""} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, CustomAssigneesRecipient) + assert recipient.value == "" + + def test_workload_recipient_missing_value_raises(self): + payload = {"type": 9, "displayName": "Support Team"} + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + def test_workload_recipient_missing_display_name_raises(self): + payload = {"type": 9, "value": "group-1"} + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + def test_round_robin_recipient_missing_value_raises(self): + payload = {"type": 10, "displayName": "Support Team"} + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + def test_custom_assignees_recipient_missing_value_raises(self): + payload = {"type": 11} + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + +class TestToolOutputRecipientDeserialization: + @pytest.mark.parametrize( + "recipient_type", + [1, 2, 9, 10, 11], + ) + def test_tool_output_recipient_by_type_int_for_supported_types( + self, recipient_type + ): + payload = { + "type": recipient_type, + "source": "toolOutput", + "toolName": "API workflow A", + "outputPath": "includeEmails", + } + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, ToolOutputRecipient) + assert recipient.tool_name == "API workflow A" + assert recipient.output_path == "includeEmails" + assert recipient.source == "toolOutput" + + def test_tool_output_recipient_for_custom_assignees_by_type_string(self): + payload = { + "type": "CustomAssignees", + "source": "toolOutput", + "toolName": "API workflow A", + "outputPath": "includeEmails", + } + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, ToolOutputRecipient) + assert recipient.type == AgentEscalationRecipientType.CUSTOM_ASSIGNEES + + def test_tool_output_recipient_missing_tool_name_raises(self): + payload = {"type": 11, "source": "toolOutput", "outputPath": "emails"} + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + def test_tool_output_recipient_missing_output_path_raises(self): + payload = {"type": 11, "source": "toolOutput", "toolName": "A"} + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + def test_tool_output_recipient_unknown_source_raises(self): + payload = { + "type": 11, + "source": "magicBox", + "toolName": "A", + "outputPath": "emails", + } + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + @pytest.mark.parametrize( + "recipient_type", + [3, 4, 5, 6, 7, 8], + ) + def test_tool_output_recipient_not_allowed_for_static_asset_argument_types( + self, recipient_type + ): + # Static/asset/argument types (3, 4, 5, 6, 7, 8) are not supported + # for tool-output binding because they have their own design-time + # resolution rules. + payload = { + "type": recipient_type, + "source": "toolOutput", + "toolName": "A", + "outputPath": "emails", + } + with pytest.raises(ValidationError): + TypeAdapter(AgentEscalationRecipient).validate_python(payload) + + def test_literal_recipient_without_source_still_parses_to_literal_class(self): + # Backward compat: a payload without `source` still matches the literal class. + payload = {"type": 11, "value": "alice@example.com", "displayName": "Alice"} + recipient: AgentEscalationRecipient = TypeAdapter( + AgentEscalationRecipient + ).validate_python(payload) + assert isinstance(recipient, CustomAssigneesRecipient) + assert not isinstance(recipient, ToolOutputRecipient) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index a0c0cd96d..216cbeb0c 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.0" +version = "2.11.1" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.67" +version = "0.1.68" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 76040269f07d64bf72931a55f31c1f8d13820adf Mon Sep 17 00:00:00 2001 From: ajay-kesavan Date: Wed, 17 Jun 2026 00:00:29 -0700 Subject: [PATCH 105/121] fix(eval): skip synthesized tool spans in per-call extractors (#1724) Co-authored-by: Claude Opus 4.7 --- packages/uipath/pyproject.toml | 2 +- .../eval/_helpers/evaluators_helpers.py | 37 +++++++++----- .../evaluators/test_evaluator_helpers.py | 48 +++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 0471ece40..c887ffda1 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.1" +version = "2.11.3" 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/eval/_helpers/evaluators_helpers.py b/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py index d64954b99..bc2168648 100644 --- a/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py +++ b/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py @@ -11,6 +11,8 @@ ToolOutput, ) +TOOL_NAME_ATTR = "tool.name" + COMPARATOR_MAPPINGS = { ">": "gt", "<": "lt", @@ -24,6 +26,18 @@ COMMUNITY_agents_SUFFIX = "-community-agents" +def _unsynthesized_tool_attrs(span: ReadableSpan) -> Mapping[str, Any] | None: + """Return span.attributes if this is a real tool invocation, else None.""" + attrs = span.attributes + if ( + not attrs + or attrs.get("tool.synthesized", False) + or not attrs.get(TOOL_NAME_ATTR) + ): + return None + return attrs + + def extract_tool_calls_names(spans: Sequence[ReadableSpan]) -> list[str]: """Extract the tool call names from execution spans IN ORDER. @@ -36,9 +50,8 @@ def extract_tool_calls_names(spans: Sequence[ReadableSpan]) -> list[str]: tool_calls_names = [] for span in spans: - # Check for tool.name attribute first - if span.attributes and (tool_name := span.attributes.get("tool.name")): - tool_calls_names.append(str(tool_name)) + if (attrs := _unsynthesized_tool_attrs(span)) is not None: + tool_calls_names.append(str(attrs[TOOL_NAME_ATTR])) return tool_calls_names @@ -55,20 +68,19 @@ def extract_tool_calls(spans: Sequence[ReadableSpan]) -> list[ToolCall]: tool_calls = [] for span in spans: - if span.attributes and (tool_name := span.attributes.get("tool.name")): + if (attrs := _unsynthesized_tool_attrs(span)) is not None: + tool_name = str(attrs[TOOL_NAME_ATTR]) try: - input_value: Any = span.attributes.get("input.value", {}) - # Ensure input_value is a string before parsing + input_value: Any = attrs.get("input.value", {}) if isinstance(input_value, str): arguments = ast.literal_eval(input_value) elif isinstance(input_value, dict): arguments = input_value else: arguments = {} - tool_calls.append(ToolCall(name=str(tool_name), args=arguments)) + tool_calls.append(ToolCall(name=tool_name, args=arguments)) except (json.JSONDecodeError, SyntaxError, ValueError): - # Handle case where input.value is not valid JSON/Python syntax - tool_calls.append(ToolCall(name=str(tool_name), args={})) + tool_calls.append(ToolCall(name=tool_name, args={})) return tool_calls @@ -87,8 +99,9 @@ def extract_tool_calls_outputs(spans: Sequence[ReadableSpan]) -> list[ToolOutput potential_output_keys = ["content"] tool_calls_outputs = [] for span in spans: - if span.attributes and (tool_name := span.attributes.get("tool.name")): - output = span.attributes.get("output.value", "") + if (attrs := _unsynthesized_tool_attrs(span)) is not None: + tool_name = str(attrs[TOOL_NAME_ATTR]) + output = attrs.get("output.value", "") final_output = "" # Handle different output formats @@ -465,7 +478,7 @@ def trace_to_str(agent_trace: Sequence[ReadableSpan]) -> str: seen_tool_calls = set() for span in agent_trace: - if span.attributes and (tool_name := span.attributes.get("tool.name")): + if span.attributes and (tool_name := span.attributes.get(TOOL_NAME_ATTR)): # Get span timing information start_time = span.start_time end_time = span.end_time diff --git a/packages/uipath/tests/evaluators/test_evaluator_helpers.py b/packages/uipath/tests/evaluators/test_evaluator_helpers.py index 84eb1159f..23b6d71e3 100644 --- a/packages/uipath/tests/evaluators/test_evaluator_helpers.py +++ b/packages/uipath/tests/evaluators/test_evaluator_helpers.py @@ -681,6 +681,54 @@ def test_extract_tool_calls_outputs_filters_non_tool_spans( assert "non_tool_span" not in output_names assert len(result) == 3 + def test_extractors_skip_synthesized_tool_spans(self) -> None: + """Spans tagged with tool.synthesized=True (BPMN container spans + synthesized for trajectory rendering) must be filtered from all three + per-call extractors so they don't pollute tool-call evaluator actuals. + """ + from opentelemetry.sdk.trace import ReadableSpan + + synth_process = ReadableSpan( + name="Instance: abc", + start_time=0, + end_time=1, + attributes={ + "tool.name": "FlowExecution", + "tool.synthesized": True, + "input.value": '{"instanceId": "abc"}', + "output.value": "Status: Completed", + }, + ) + synth_element = ReadableSpan( + name="Autonomous Agent", + start_time=2, + end_time=3, + attributes={ + "tool.name": "ServiceTask: Autonomous Agent", + "tool.synthesized": True, + "input.value": "{}", + "output.value": "Status: Completed", + }, + ) + real_tool = ReadableSpan( + name="Tool call - web_search", + start_time=4, + end_time=5, + attributes={ + "tool.name": "web_search", + "input.value": '{"query": "x"}', + "output.value": '{"content": "ok"}', + }, + ) + + spans = [synth_process, synth_element, real_tool] + + assert extract_tool_calls_names(spans) == ["web_search"] + calls = extract_tool_calls(spans) + assert [c.name for c in calls] == ["web_search"] + outputs = extract_tool_calls_outputs(spans) + assert [o.name for o in outputs] == ["web_search"] + def test_all_extraction_functions_consistent(self, sample_spans: list[Any]) -> None: """Test that all extraction functions return consistent results.""" names = extract_tool_calls_names(sample_spans) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 216cbeb0c..0fb2c92cd 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.1" +version = "2.11.3" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From b7220919625d59f23c8dc9476ba24b6a02faee64 Mon Sep 17 00:00:00 2001 From: ajay-kesavan Date: Wed, 17 Jun 2026 00:20:40 -0700 Subject: [PATCH 106/121] feat(eval): match tool calls by id with name fallback (#1725) Co-authored-by: Claude Opus 4.7 --- packages/uipath/pyproject.toml | 2 +- .../eval/_helpers/evaluators_helpers.py | 219 ++++++++++++++-- .../evaluators/tool_call_count_evaluator.py | 9 +- .../evaluators/tool_call_order_evaluator.py | 12 +- .../uipath/src/uipath/eval/models/models.py | 16 +- .../evaluators/test_evaluator_helpers.py | 246 ++++++++++++++++++ packages/uipath/uv.lock | 2 +- 7 files changed, 468 insertions(+), 38 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index c887ffda1..98ecaa277 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.3" +version = "2.11.4" 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/eval/_helpers/evaluators_helpers.py b/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py index bc2168648..912dcd41d 100644 --- a/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py +++ b/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py @@ -38,6 +38,86 @@ def _unsynthesized_tool_attrs(span: ReadableSpan) -> Mapping[str, Any] | None: return attrs +def _match_key(actual_name: str, actual_id: str | None, expected_key: str) -> bool: + """True when `expected_key` matches either the actual call's `id` or `name`. + + Eval-set criteria can be authored against either the tool's stable id or + its display name; the scorers accept whichever the author used. + """ + if actual_id is not None and expected_key == actual_id: + return True + return expected_key == actual_name + + +def _calls_match(actual, expected) -> bool: + """True when an actual ToolCall/ToolOutput matches an expected one. + + Prefers id-equality when both sides carry an id; otherwise falls back to + name-equality. This keeps the legacy name-keyed behavior intact while + making id-keyed eval-sets rename-safe. + """ + if actual.id is not None and expected.id is not None: + return actual.id == expected.id + return actual.name == expected.name + + +def _parse_tool_args(input_value: Any) -> dict[str, Any]: + """Coerce a span's `input.value` into a dict of tool args. + + Tries JSON first (handles `true`/`false`/`null` and double-quoted keys), + falls back to `ast.literal_eval` for Python literal syntax (single-quoted + dict repr). Returns `{}` for non-dict parsed values or any parse failure. + """ + if isinstance(input_value, dict): + return input_value + if not isinstance(input_value, str): + return {} + try: + try: + parsed = json.loads(input_value) + except ValueError: # JSONDecodeError is a ValueError + parsed = ast.literal_eval(input_value) + except (SyntaxError, ValueError): + return {} + return parsed if isinstance(parsed, dict) else {} + + +def _tool_id_from(attrs: Mapping[str, Any]) -> str | None: + """Return the span's `tool.id` as a string when present, else None. + + Uses `is not None` (not truthiness) so an id of 0 or '' isn't dropped. + """ + tool_id = attrs.get("tool.id") + return str(tool_id) if tool_id is not None else None + + +def _build_tool_call(span: ReadableSpan, include_args: bool) -> ToolCall | None: + """Build a ToolCall from a span, or None for synthesized / non-tool spans.""" + attrs = _unsynthesized_tool_attrs(span) + if attrs is None: + return None + tool_name = str(attrs[TOOL_NAME_ATTR]) + tool_id = _tool_id_from(attrs) + args = _parse_tool_args(attrs.get("input.value", {})) if include_args else {} + return ToolCall(name=tool_name, args=args, id=tool_id) + + +def count_tool_calls_by_name_and_id(tool_calls: Sequence[ToolCall]) -> dict[str, int]: + """Count tool calls under BOTH their name and id keys. + + Each call contributes one unit to its name bucket and (when present and + different) one unit to its id bucket. Lookups by either return the same + count for that tool. This lets `tool_calls_count_score` honour eval-set + criteria keyed by id with no signature change to the score function. + """ + counts: dict[str, int] = {} + for c in tool_calls: + counts[c.name] = counts.get(c.name, 0) + 1 + if c.id is not None and c.id != c.name: + counts[c.id] = counts.get(c.id, 0) + 1 + return counts + + def extract_tool_calls_names(spans: Sequence[ReadableSpan]) -> list[str]: """Extract the tool call names from execution spans IN ORDER. @@ -56,33 +136,23 @@ def extract_tool_calls_names(spans: Sequence[ReadableSpan]) -> list[str]: return tool_calls_names -def extract_tool_calls(spans: Sequence[ReadableSpan]) -> list[ToolCall]: - """Extract the tool calls from execution spans with their arguments. +def extract_tool_calls( + spans: Sequence[ReadableSpan], + include_args: bool = True, +) -> list[ToolCall]: + """Extract the tool calls from execution spans. Args: spans: List of ReadableSpan objects from agent execution. + include_args: When False, skip parsing `input.value` and return + ToolCall objects with `args={}`. Use for evaluators that only + need name/id (count, order) — avoids a parse per span on large + traces. Returns: - Dict of tool calls with their arguments. + List of tool calls with their arguments. """ - tool_calls = [] - - for span in spans: - if (attrs := _unsynthesized_tool_attrs(span)) is not None: - tool_name = str(attrs[TOOL_NAME_ATTR]) - try: - input_value: Any = attrs.get("input.value", {}) - if isinstance(input_value, str): - arguments = ast.literal_eval(input_value) - elif isinstance(input_value, dict): - arguments = input_value - else: - arguments = {} - tool_calls.append(ToolCall(name=tool_name, args=arguments)) - except (json.JSONDecodeError, SyntaxError, ValueError): - tool_calls.append(ToolCall(name=tool_name, args={})) - - return tool_calls + return [c for s in spans if (c := _build_tool_call(s, include_args)) is not None] def extract_tool_calls_outputs(spans: Sequence[ReadableSpan]) -> list[ToolOutput]: @@ -101,6 +171,7 @@ def extract_tool_calls_outputs(spans: Sequence[ReadableSpan]) -> list[ToolOutput for span in spans: if (attrs := _unsynthesized_tool_attrs(span)) is not None: tool_name = str(attrs[TOOL_NAME_ATTR]) + tool_id = _tool_id_from(attrs) output = attrs.get("output.value", "") final_output = "" @@ -131,8 +202,9 @@ def extract_tool_calls_outputs(spans: Sequence[ReadableSpan]) -> list[ToolOutput tool_calls_outputs.append( ToolOutput( - name=str(tool_name), + name=tool_name, output=str(final_output) if final_output else "", + id=tool_id, ) ) return tool_calls_outputs @@ -209,6 +281,105 @@ def tool_calls_order_score( return lcs_length / n, justification +def _strict_order_score( + actual: Sequence[ToolCall], + expected: Sequence[str], + justification: dict[str, Any], +) -> tuple[float, dict[str, Any]]: + """Strict-mode evaluation — only an exact positional match scores 1.0.""" + if len(actual) != len(expected): + return 0.0, justification + for i, key in enumerate(expected): + if not _match_key(actual[i].name, actual[i].id, key): + return 0.0, justification + justification["lcs"] = list(expected) + return 1.0, justification + + +def _build_lcs_dp( + actual: Sequence[ToolCall], expected: Sequence[str] +) -> list[list[int]]: + """Fill the LCS dynamic-programming table for id-aware matching.""" + m, n = len(actual), len(expected) + dp = [[0] * (n + 1) for _ in range(m + 1)] + for i in range(1, m + 1): + for j in range(1, n + 1): + if _match_key(actual[i - 1].name, actual[i - 1].id, expected[j - 1]): + dp[i][j] = dp[i - 1][j - 1] + 1 + else: + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + return dp + + +def _reconstruct_lcs( + actual: Sequence[ToolCall], + expected: Sequence[str], + dp: list[list[int]], +) -> list[str]: + """Walk the DP table backwards to recover the LCS as a list of expected keys.""" + lcs: list[str] = [] + i, j = len(actual), len(expected) + while i > 0 and j > 0: + if _match_key(actual[i - 1].name, actual[i - 1].id, expected[j - 1]): + lcs.append(expected[j - 1]) + i -= 1 + j -= 1 + elif dp[i - 1][j] > dp[i][j - 1]: + i -= 1 + else: + j -= 1 + lcs.reverse() + return lcs + + +def tool_calls_order_score_with_ids( + actual_tool_calls: Sequence[ToolCall], + expected_tool_calls_keys: Sequence[str], + strict: bool = False, +) -> tuple[float, dict[str, Any]]: + """LCS-based ordering score with id-aware matching. + + Identical scoring algorithm to `tool_calls_order_score`, but each expected + key string is allowed to match either the actual call's `id` or its + `name`. Use this when eval-set criteria may be authored against the + stable tool id so renames of `name` don't silently break ordering checks. + + Args: + actual_tool_calls: ToolCall objects in the actual order. Each may carry + an `id` from the runtime's `tool.id` span attribute. + expected_tool_calls_keys: List of names OR ids in the expected order. + strict: When True, only perfect matches score above 0. + + Returns: + Same shape as `tool_calls_order_score`. The "actual" justification + renders the resolved match-key sequence (id when available, else name) + so the LCS reconstruction reads clearly. + """ + actual_keys: list[str] = [ + (c.id if c.id is not None else c.name) for c in actual_tool_calls + ] + justification: dict[str, Any] = { + "actual": str(list(actual_keys)), + "expected": str(list(expected_tool_calls_keys)), + "lcs": [], + } + + if not expected_tool_calls_keys and not actual_tool_calls: + return 1.0, justification + if not expected_tool_calls_keys or not actual_tool_calls: + return 0.0, justification + + if strict: + return _strict_order_score( + actual_tool_calls, expected_tool_calls_keys, justification + ) + + dp = _build_lcs_dp(actual_tool_calls, expected_tool_calls_keys) + lcs = _reconstruct_lcs(actual_tool_calls, expected_tool_calls_keys, dp) + justification["lcs"] = lcs + return len(lcs) / len(expected_tool_calls_keys), justification + + def tool_calls_count_score( actual_tool_calls_count: Mapping[str, int], expected_tool_calls_count: Mapping[str, tuple[str, int]], @@ -323,7 +494,7 @@ def tool_calls_args_score( for expected_tool_call in expected_tool_calls: for idx, call in enumerate(actual_tool_calls): - if call.name == expected_tool_call.name and idx not in visited: + if _calls_match(call, expected_tool_call) and idx not in visited: # Get or initialize counter for this tool name tool_counters[call.name] = tool_counters.get(call.name, 0) tool_key = f"{call.name}_{tool_counters[call.name]}" @@ -415,7 +586,7 @@ def tool_calls_output_score( for idx, actual_tool_call_output in enumerate(actual_tool_calls_outputs): if idx in visited: continue - if actual_tool_call_output.name == expected_tool_call_output.name: + if _calls_match(actual_tool_call_output, expected_tool_call_output): # Get or initialize counter for this tool name tool_counters[actual_tool_call_output.name] = tool_counters.get( actual_tool_call_output.name, 0 diff --git a/packages/uipath/src/uipath/eval/evaluators/tool_call_count_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/tool_call_count_evaluator.py index 11d684ae1..4df237e7e 100644 --- a/packages/uipath/src/uipath/eval/evaluators/tool_call_count_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/tool_call_count_evaluator.py @@ -1,9 +1,8 @@ """Tool call count evaluator for validating expected tool usage patterns.""" -from collections import Counter - from .._helpers.evaluators_helpers import ( - extract_tool_calls_names, + count_tool_calls_by_name_and_id, + extract_tool_calls, tool_calls_count_score, ) from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult @@ -72,8 +71,8 @@ async def evaluate( Returns: EvaluationResult: Boolean result indicating correct tool call order (True/False) """ - tool_calls_count = Counter( - extract_tool_calls_names(agent_execution.agent_trace) + tool_calls_count = count_tool_calls_by_name_and_id( + extract_tool_calls(agent_execution.agent_trace, include_args=False) ) score, justification = tool_calls_count_score( tool_calls_count, diff --git a/packages/uipath/src/uipath/eval/evaluators/tool_call_order_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/tool_call_order_evaluator.py index 1050ddc76..4676750d3 100644 --- a/packages/uipath/src/uipath/eval/evaluators/tool_call_order_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/tool_call_order_evaluator.py @@ -1,8 +1,8 @@ """Tool call order evaluator for validating correct sequence of tool calls.""" from .._helpers.evaluators_helpers import ( - extract_tool_calls_names, - tool_calls_order_score, + extract_tool_calls, + tool_calls_order_score_with_ids, ) from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult from ..models.models import EvaluatorType @@ -69,9 +69,11 @@ async def evaluate( Returns: EvaluationResult: Boolean result indicating correct tool call order (True/False) """ - tool_calls_order = extract_tool_calls_names(agent_execution.agent_trace) - score, justification = tool_calls_order_score( - tool_calls_order, + actual_calls = extract_tool_calls( + agent_execution.agent_trace, include_args=False + ) + score, justification = tool_calls_order_score_with_ids( + actual_calls, evaluation_criteria.tool_calls_order, self.evaluator_config.strict, ) diff --git a/packages/uipath/src/uipath/eval/models/models.py b/packages/uipath/src/uipath/eval/models/models.py index d2dc26df9..14c130c92 100644 --- a/packages/uipath/src/uipath/eval/models/models.py +++ b/packages/uipath/src/uipath/eval/models/models.py @@ -303,17 +303,29 @@ class EvaluatorType(str, Enum): class ToolCall(BaseModel): - """Represents a tool call with its arguments.""" + """Represents a tool call with its arguments. + + `id` is the stable identifier from the tool's resource definition (e.g. a + UUID from `bindings.json`). When present on both the actual call and the + expected criterion, scorers match by `id` so a rename of `name` does not + break eval sets. When `id` is absent on either side, scorers fall back to + matching by `name` (the legacy behavior). + """ name: str args: dict[str, Any] + id: str | None = None class ToolOutput(BaseModel): - """Represents a tool output with its output.""" + """Represents a tool output with its output. + + See `ToolCall.id` for the id semantics. + """ name: str output: str + id: str | None = None class UiPathEvaluationErrorCategory(str, Enum): diff --git a/packages/uipath/tests/evaluators/test_evaluator_helpers.py b/packages/uipath/tests/evaluators/test_evaluator_helpers.py index 23b6d71e3..061b57aab 100644 --- a/packages/uipath/tests/evaluators/test_evaluator_helpers.py +++ b/packages/uipath/tests/evaluators/test_evaluator_helpers.py @@ -868,3 +868,249 @@ def test_extract_tool_calls_outputs_with_json_non_dict_value(self) -> None: assert result[0].name == "json_array_tool" # Should use the original string when parsed JSON is not a dict assert result[0].output == '["item1", "item2", "item3"]' + + +class TestIdAwareExtraction: + """Verify tool.id propagation through the three extractors, plus the + include_args=False optimization and the JSON-first parse fallback. + """ + + def test_extractors_read_tool_id_when_present(self) -> None: + """When a span carries `tool.id`, it must surface on ToolCall/ToolOutput.id.""" + from opentelemetry.sdk.trace import ReadableSpan + + span = ReadableSpan( + name="Tool call - Web_Search", + start_time=0, + end_time=1, + attributes={ + "tool.name": "Web_Search", + "tool.id": "7abae702-f898-4cc9-95f1-c365b9a857f9", + "input.value": "{}", + "output.value": '{"content": "ok"}', + }, + ) + calls = extract_tool_calls([span]) + outputs = extract_tool_calls_outputs([span]) + assert calls[0].id == "7abae702-f898-4cc9-95f1-c365b9a857f9" + assert calls[0].name == "Web_Search" + assert outputs[0].id == "7abae702-f898-4cc9-95f1-c365b9a857f9" + assert outputs[0].name == "Web_Search" + + def test_extractors_preserve_falsy_but_present_tool_id(self) -> None: + """tool.id of 0 or empty string is unusual but legal — must not be silently dropped. + + Original code used `if tool_id` which would treat 0 / '' as missing. + Fix uses `is not None`. + """ + from opentelemetry.sdk.trace import ReadableSpan + + for falsy_id in (0, "", False): + span = ReadableSpan( + name="t", + start_time=0, + end_time=1, + attributes={ + "tool.name": "f", + "tool.id": falsy_id, + "input.value": "{}", + "output.value": '{"content": "ok"}', + }, + ) + calls = extract_tool_calls([span]) + outputs = extract_tool_calls_outputs([span]) + assert calls[0].id == str(falsy_id), f"falsy id {falsy_id!r} was dropped" + assert outputs[0].id == str(falsy_id), f"falsy id {falsy_id!r} was dropped" + + def test_extract_tool_calls_parses_json_literals(self) -> None: + """input.value with JSON `true`/`false`/`null` should parse cleanly. + + `ast.literal_eval` doesn't recognise those tokens (Python uses + True/False/None); the extractor now tries `json.loads` first and only + falls back to `ast.literal_eval` on JSON parse failure. + """ + from opentelemetry.sdk.trace import ReadableSpan + + span = ReadableSpan( + name="t", + start_time=0, + end_time=1, + attributes={ + "tool.name": "t", + "input.value": '{"a": true, "b": false, "c": null}', + }, + ) + calls = extract_tool_calls([span]) + assert calls[0].args == {"a": True, "b": False, "c": None} + + def test_extract_tool_calls_falls_back_to_python_literal(self) -> None: + """Single-quoted Python dict repr (the historical input shape) still parses.""" + from opentelemetry.sdk.trace import ReadableSpan + + span = ReadableSpan( + name="t", + start_time=0, + end_time=1, + attributes={ + "tool.name": "t", + "input.value": "{'a': 1, 'b': 'two'}", # JSON-invalid, Python-valid + }, + ) + calls = extract_tool_calls([span]) + assert calls[0].args == {"a": 1, "b": "two"} + + def test_extract_tool_calls_non_dict_parsed_result_yields_empty_args(self) -> None: + """If input.value parses to a non-dict (e.g. a bare string), args→{}. + + Avoids pydantic validation failures from feeding a non-dict into + ToolCall(args=...). + """ + from opentelemetry.sdk.trace import ReadableSpan + + span = ReadableSpan( + name="t", + start_time=0, + end_time=1, + attributes={ + "tool.name": "t", + "input.value": '"hello"', # JSON-valid string, not a dict + }, + ) + calls = extract_tool_calls([span]) + assert calls[0].args == {} + + def test_extract_tool_calls_include_args_false_skips_parse(self) -> None: + """With include_args=False, broken input.value is not parsed and doesn't raise. + + Used by count / order evaluators that don't need args. + """ + from opentelemetry.sdk.trace import ReadableSpan + + span = ReadableSpan( + name="t", + start_time=0, + end_time=1, + attributes={ + "tool.name": "t", + "tool.id": "abc", + "input.value": "this is not valid python or json{{{", + }, + ) + calls = extract_tool_calls([span], include_args=False) + assert len(calls) == 1 + assert calls[0].name == "t" + assert calls[0].id == "abc" + assert calls[0].args == {} # short-circuited, not parsed + + def test_extractors_default_id_to_none_when_absent(self) -> None: + """Spans without `tool.id` produce ToolCall/ToolOutput with id=None (back-compat).""" + from opentelemetry.sdk.trace import ReadableSpan + + span = ReadableSpan( + name="Tool call - legacy", + start_time=0, + end_time=1, + attributes={ + "tool.name": "legacy_tool", + "input.value": "{}", + "output.value": '{"content": "ok"}', + }, + ) + calls = extract_tool_calls([span]) + outputs = extract_tool_calls_outputs([span]) + assert calls[0].id is None + assert outputs[0].id is None + + +class TestIdAwareMatching: + """Verify id-aware matching across all four tool-call scoring functions. + + For each function: an Expected criterion authored against the tool's id + matches the actual call when the actual carries the same id, even if the + `name` differs (the common case after a tool rename or the + 'Web Search' → 'Web_Search' display-vs-runtime divergence). + """ + + def test_args_score_matches_by_id_when_names_differ(self) -> None: + """Expected keyed by id matches actual with same id but different name.""" + from uipath.eval._helpers.evaluators_helpers import tool_calls_args_score + + actual = [ToolCall(name="Web_Search", id="uuid-1", args={"q": "x"})] + expected = [ToolCall(name="Web Search", id="uuid-1", args={"q": "x"})] + score, _ = tool_calls_args_score(actual, expected) + assert score == 1.0 + + def test_args_score_falls_back_to_name_when_id_missing(self) -> None: + """Legacy eval-set without id still matches by name (back-compat).""" + from uipath.eval._helpers.evaluators_helpers import tool_calls_args_score + + actual = [ToolCall(name="Web_Search", id="uuid-1", args={"q": "x"})] + expected = [ToolCall(name="Web_Search", args={"q": "x"})] + score, _ = tool_calls_args_score(actual, expected) + assert score == 1.0 + + def test_args_score_no_match_when_ids_differ(self) -> None: + """Different ids → no match even with same name.""" + from uipath.eval._helpers.evaluators_helpers import tool_calls_args_score + + actual = [ToolCall(name="Web_Search", id="uuid-A", args={"q": "x"})] + expected = [ToolCall(name="Web_Search", id="uuid-B", args={"q": "x"})] + score, _ = tool_calls_args_score(actual, expected) + assert score == 0.0 + + def test_output_score_matches_by_id(self) -> None: + from uipath.eval._helpers.evaluators_helpers import tool_calls_output_score + + actual = [ToolOutput(name="Web_Search", id="uuid-1", output="ok")] + expected = [ToolOutput(name="Web Search", id="uuid-1", output="ok")] + score, _ = tool_calls_output_score(actual, expected) + assert score == 1.0 + + def test_count_by_name_and_id_helper(self) -> None: + """Each call contributes one unit to its name AND to its id key.""" + from uipath.eval._helpers.evaluators_helpers import ( + count_tool_calls_by_name_and_id, + ) + + calls = [ + ToolCall(name="Web_Search", id="uuid-1", args={}), + ToolCall(name="Web_Search", id="uuid-1", args={}), + ToolCall(name="get_temp", args={}), # no id + ] + counts = count_tool_calls_by_name_and_id(calls) + assert counts["Web_Search"] == 2 + assert counts["uuid-1"] == 2 # same calls retrievable by id + assert counts["get_temp"] == 1 + + def test_order_score_with_ids_matches_id_keyed_expected(self) -> None: + """LCS treats expected key as match if it equals actual.id OR actual.name.""" + from uipath.eval._helpers.evaluators_helpers import ( + tool_calls_order_score_with_ids, + ) + + actual = [ + ToolCall(name="Web_Search", id="uuid-1", args={}), + ToolCall(name="Web_Search", id="uuid-1", args={}), + ] + # Expected authored by id + score, _ = tool_calls_order_score_with_ids(actual, ["uuid-1", "uuid-1"]) + assert score == 1.0 + # Expected authored by name (legacy) + score, _ = tool_calls_order_score_with_ids(actual, ["Web_Search", "Web_Search"]) + assert score == 1.0 + # Mixed: works either way + score, _ = tool_calls_order_score_with_ids(actual, ["uuid-1", "Web_Search"]) + assert score == 1.0 + + def test_order_score_with_ids_back_compat_when_id_absent(self) -> None: + """When actual has no ids (legacy traces), comparison is name-only.""" + from uipath.eval._helpers.evaluators_helpers import ( + tool_calls_order_score_with_ids, + ) + + actual = [ + ToolCall(name="get_temp", args={}), + ToolCall(name="get_humidity", args={}), + ] + score, _ = tool_calls_order_score_with_ids(actual, ["get_temp", "get_humidity"]) + assert score == 1.0 diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 0fb2c92cd..96ddf7869 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.3" +version = "2.11.4" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 1e4089117ac402ab0d7603edbd414a42329fd0ca Mon Sep 17 00:00:00 2001 From: Viswanath Lekshmanan <84002867+viswa-uipath@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:44:49 +0530 Subject: [PATCH 107/121] feat(core): add EnforcementMode enum to governance contracts (#1727) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/governance/__init__.py | 3 +- .../src/uipath/core/governance/config.py | 7 +++-- .../src/uipath/core/governance/models.py | 29 ++++++++++++++----- packages/uipath-core/uv.lock | 4 +-- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 7 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index eef3ab27e..df8c0bb71 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.18" +version = "0.5.19" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/governance/__init__.py b/packages/uipath-core/src/uipath/core/governance/__init__.py index dd32228ed..3a06b82df 100644 --- a/packages/uipath-core/src/uipath/core/governance/__init__.py +++ b/packages/uipath-core/src/uipath/core/governance/__init__.py @@ -18,12 +18,13 @@ GovernanceViolation, Severity, ) -from .models import Action, AuditRecord, LifecycleHook, RuleEvaluation +from .models import Action, AuditRecord, EnforcementMode, LifecycleHook, RuleEvaluation __all__ = [ # Output models (cross adapter boundary) "Action", "AuditRecord", + "EnforcementMode", "LifecycleHook", "RuleEvaluation", # Config diff --git a/packages/uipath-core/src/uipath/core/governance/config.py b/packages/uipath-core/src/uipath/core/governance/config.py index 0bb0a6042..7fec33848 100644 --- a/packages/uipath-core/src/uipath/core/governance/config.py +++ b/packages/uipath-core/src/uipath/core/governance/config.py @@ -1,8 +1,11 @@ """Governance configuration. Process-level feature-flag gate that decides whether the Python -governance checker runs at all. Enforcement mode is per-policy and -lives in the runtime package alongside the ``/runtime/policy`` client. +governance checker runs at all. The +:class:`uipath.core.governance.EnforcementMode` value type is defined +in :mod:`uipath.core.governance.models`; the per-policy runtime state +that selects a mode (backend-supplied via the ``/runtime/policy`` +client) lives in the ``uipath-runtime`` package. """ from __future__ import annotations diff --git a/packages/uipath-core/src/uipath/core/governance/models.py b/packages/uipath-core/src/uipath/core/governance/models.py index 6b3993639..9fc5e2084 100644 --- a/packages/uipath-core/src/uipath/core/governance/models.py +++ b/packages/uipath-core/src/uipath/core/governance/models.py @@ -1,10 +1,17 @@ -"""Shared governance output types. - -These dataclasses cross the adapter boundary — every evaluator -implementation (native, AGT, composite, …) produces them, and every -adapter consumes them. They are kept free of policy-input concepts -(``Rule``/``Check``/``Condition``) so the adapter packages don't -inherit the native policy model. +"""Shared governance contracts. + +Two groups of types live here, both kept free of policy-input concepts +(``Rule``/``Check``/``Condition``) so adapter packages don't inherit +the native policy model: + +- **Output types** (:class:`Action`, :class:`LifecycleHook`, + :class:`RuleEvaluation`, :class:`AuditRecord`) — cross the adapter + boundary at evaluation time: every evaluator implementation (native, + AGT, composite, …) produces them, and every adapter consumes them. +- **Configuration value types** (:class:`EnforcementMode`) — describe + governance configuration shared by core, runtime, and consumers. The + runtime state that selects an enforcement mode lives in + ``uipath-runtime``; only the value type lives here. """ from __future__ import annotations @@ -35,6 +42,14 @@ class LifecycleHook(str, Enum): AFTER_TOOL = "after_tool" +class EnforcementMode(str, Enum): + """Governance enforcement modes.""" + + AUDIT = "audit" # Evaluate and log; never block. + ENFORCE = "enforce" # Block on DENY rules. + DISABLED = "disabled" # Skip evaluation entirely. + + @dataclass class RuleEvaluation: """Result of evaluating a single rule.""" diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 0ef8204e9..2aec09333 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-06T13:38:31.678016Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [[package]] @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.18" +version = "0.5.19" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 5d4c41d63..2ce9597a8 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.18" +version = "0.5.19" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 96ddf7869..59a6df63d 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.18" +version = "0.5.19" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 0ac907089067100e549b37ae7bd76db114436407 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Wed, 17 Jun 2026 12:37:19 +0300 Subject: [PATCH 108/121] fix(platform): buckets.delete honors folder_path/folder_key args (#1729) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/orchestrator/_buckets_service.py | 4 +- .../tests/services/test_buckets_service.py | 84 +++++++++++++++++++ packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 5 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 698270df8..75e84a6f8 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.68" +version = "0.1.69" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/_buckets_service.py b/packages/uipath-platform/src/uipath/platform/orchestrator/_buckets_service.py index 0d536cd46..fc1d84db3 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/_buckets_service.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/_buckets_service.py @@ -365,7 +365,7 @@ def delete( self.request( "DELETE", url=f"/orchestrator_/odata/Buckets({bucket.id})", - headers={**self.folder_headers}, + headers={**header_folder(folder_key, folder_path)}, ) @resource_override(resource_type="bucket") @@ -386,7 +386,7 @@ async def delete_async( await self.request_async( "DELETE", url=f"/orchestrator_/odata/Buckets({bucket.id})", - headers={**self.folder_headers}, + headers={**header_folder(folder_key, folder_path)}, ) @resource_override(resource_type="bucket") diff --git a/packages/uipath-platform/tests/services/test_buckets_service.py b/packages/uipath-platform/tests/services/test_buckets_service.py index 0fbb5f974..8cbb9f50c 100644 --- a/packages/uipath-platform/tests/services/test_buckets_service.py +++ b/packages/uipath-platform/tests/services/test_buckets_service.py @@ -646,6 +646,90 @@ async def test_create_async( assert bucket.id == 1 +class TestDelete: + """Tests for delete() / delete_async(). + + Regression coverage for UV-14977: delete() must build the folder header + from the folder_path/folder_key arguments (via header_folder), not solely + from the UIPATH_FOLDER_PATH / UIPATH_FOLDER_KEY env vars. + """ + + def test_delete_by_name_uses_folder_path_arg( + self, + httpx_mock: HTTPXMock, + service: BucketsService, + base_url: str, + org: str, + tenant: str, + ): + """delete(name=..., folder_path=...) sends the arg folder header on DELETE.""" + # retrieve() locates the bucket in the target folder + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Buckets?$filter=Name eq 'old-storage'&$top=1", + status_code=200, + json={"value": [{"Id": 203380, "Name": "old-storage", "Identifier": "id"}]}, + match_headers={"x-uipath-folderpath": "Playground"}, + ) + # the DELETE must carry the arg folder header (not the env-var fallback) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Buckets(203380)", + method="DELETE", + status_code=204, + match_headers={"x-uipath-folderpath": "Playground"}, + ) + + service.delete(name="old-storage", folder_path="Playground") + + def test_delete_by_key_uses_folder_key_arg( + self, + httpx_mock: HTTPXMock, + service: BucketsService, + base_url: str, + org: str, + tenant: str, + ): + """delete(key=..., folder_key=...) sends the arg folder-key header on DELETE.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Buckets/UiPath.Server.Configuration.OData.GetByKey(identifier='bucket-key')", + status_code=200, + json={"value": [{"Id": 55, "Name": "kbucket", "Identifier": "bucket-key"}]}, + match_headers={"x-uipath-folderkey": "folder-123"}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Buckets(55)", + method="DELETE", + status_code=204, + match_headers={"x-uipath-folderkey": "folder-123"}, + ) + + service.delete(key="bucket-key", folder_key="folder-123") + + @pytest.mark.asyncio + async def test_delete_async_uses_folder_path_arg( + self, + httpx_mock: HTTPXMock, + service: BucketsService, + base_url: str, + org: str, + tenant: str, + ): + """Async version honors the folder_path argument on DELETE.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Buckets?$filter=Name eq 'old-storage'&$top=1", + status_code=200, + json={"value": [{"Id": 99, "Name": "old-storage", "Identifier": "id"}]}, + match_headers={"x-uipath-folderpath": "Playground"}, + ) + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Buckets(99)", + method="DELETE", + status_code=204, + match_headers={"x-uipath-folderpath": "Playground"}, + ) + + await service.delete_async(name="old-storage", folder_path="Playground") + + class TestEdgeCases: """Tests for edge cases and error handling.""" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 2ce9597a8..a6cd279f6 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.68" +version = "0.1.69" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 59a6df63d..fd631cd69 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.68" +version = "0.1.69" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From aa5bb7e2bafe7490ce8df0803dbb2db207342437 Mon Sep 17 00:00:00 2001 From: Valentina Bojan Date: Wed, 17 Jun 2026 13:08:08 +0300 Subject: [PATCH 109/121] feat(guardrails): add enum/text/text-list validator parameter types (#1726) Co-authored-by: Valentina Bojan Co-authored-by: Claude --- packages/uipath-platform/pyproject.toml | 2 +- .../uipath/platform/guardrails/guardrails.py | 37 ++++++++++++++++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 75e84a6f8..adae84ca6 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.69" +version = "0.1.70" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py b/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py index cfc1e295f..16262ca5e 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py @@ -37,8 +37,43 @@ class NumberParameterValue(BaseModel): model_config = ConfigDict(populate_by_name=True, extra="allow") +class EnumParameterValue(BaseModel): + """Single-select enum parameter value.""" + + parameter_type: Literal["enum"] = Field(alias="$parameterType") + id: str + value: str + + model_config = ConfigDict(populate_by_name=True, extra="allow") + + +class TextParameterValue(BaseModel): + """Free-text parameter value.""" + + parameter_type: Literal["text"] = Field(alias="$parameterType") + id: str + value: str + + model_config = ConfigDict(populate_by_name=True, extra="allow") + + +class TextListParameterValue(BaseModel): + """List-of-text parameter value.""" + + parameter_type: Literal["text-list"] = Field(alias="$parameterType") + id: str + value: list[str] + + model_config = ConfigDict(populate_by_name=True, extra="allow") + + ValidatorParameter = Annotated[ - EnumListParameterValue | MapEnumParameterValue | NumberParameterValue, + EnumListParameterValue + | MapEnumParameterValue + | NumberParameterValue + | EnumParameterValue + | TextParameterValue + | TextListParameterValue, Field(discriminator="parameter_type"), ] diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index a6cd279f6..6b5a34c52 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.69" +version = "0.1.70" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index fd631cd69..783b409a5 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.69" +version = "0.1.70" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From c9593922ae12696524ebbb3d12e1bd8171aed5a7 Mon Sep 17 00:00:00 2001 From: Christin <164907691+scottcmg@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:28:50 -0700 Subject: [PATCH 110/121] feat: send conversation owner id header on CAS websocket handshake [JAR-9965] (#1712) Co-authored-by: Claude Opus 4.8 (1M context) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/_cli/_chat/_bridge.py | 13 ++++- .../src/uipath/_cli/_chat/_voice_bridge.py | 9 ++++ packages/uipath/tests/cli/chat/test_bridge.py | 47 +++++++++++++++++++ .../tests/cli/chat/test_voice_bridge.py | 32 +++++++++++++ packages/uipath/uv.lock | 2 +- 6 files changed, 101 insertions(+), 4 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 98ecaa277..0add2e09e 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.4" +version = "2.11.5" 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/_chat/_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_bridge.py index 96566e898..b4cddf3b8 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_bridge.py @@ -518,13 +518,22 @@ def get_chat_bridge( # Build headers from context headers = { "Authorization": f"Bearer {os.environ.get('UIPATH_ACCESS_TOKEN', '')}", - "X-UiPath-Internal-TenantId": f"{context.tenant_id}" + "X-UiPath-Internal-TenantId": context.tenant_id or os.environ.get("UIPATH_TENANT_ID", ""), - "X-UiPath-Internal-AccountId": f"{context.org_id}" + "X-UiPath-Internal-AccountId": context.org_id or os.environ.get("UIPATH_ORGANIZATION_ID", ""), "X-UiPath-ConversationId": context.conversation_id, } + # Conversation owner id (conversationalService.conversationalUserId) that CAS forwards via + # FpsProperties; always sent when present. It's there for RunAsMe=false, where the unattended + # robot's token subject is the robot account rather than the conversation owner, so CAS validates + # this presented id against conversation.user_id on the handshake instead of the token subject. + # Sent as a header (not a query param) to keep it out of access / load-balancer logs. + conversational_user_id = getattr(context, "conversational_user_id", None) + if conversational_user_id: + headers["X-UiPath-Internal-ConversationalUserId"] = conversational_user_id + return SocketIOChatBridge( websocket_url=websocket_url, websocket_path=websocket_path, diff --git a/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py index 6164b9f3d..8473c1574 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py @@ -249,6 +249,15 @@ def get_voice_bridge( "X-UiPath-ConversationId": context.conversation_id, } + # Conversation owner id (conversationalService.conversationalUserId) that CAS forwards via + # FpsProperties; always sent when present. It's there for RunAsMe=false, where the unattended + # robot's token subject is the robot account rather than the conversation owner, so CAS validates + # this presented id against conversation.user_id on the handshake instead of the token subject. + # Sent as a header (not a query param) to keep it out of access / load-balancer logs. + conversational_user_id = getattr(context, "conversational_user_id", None) + if conversational_user_id: + headers["X-UiPath-Internal-ConversationalUserId"] = conversational_user_id + return VoiceToolCallSession( url=url, socketio_path=socketio_path, diff --git a/packages/uipath/tests/cli/chat/test_bridge.py b/packages/uipath/tests/cli/chat/test_bridge.py index bbd385def..41659ffe2 100644 --- a/packages/uipath/tests/cli/chat/test_bridge.py +++ b/packages/uipath/tests/cli/chat/test_bridge.py @@ -206,6 +206,53 @@ def test_get_chat_bridge_constructs_correct_headers( assert "X-UiPath-ConversationId" in bridge.headers assert bridge.headers["X-UiPath-ConversationId"] == "conv-789" + def test_get_chat_bridge_falls_back_to_env_when_tenant_and_org_absent( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Tenant/account headers fall back to env vars when context values are None.""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "my-access-token") + monkeypatch.setenv("UIPATH_TENANT_ID", "env-tenant") + monkeypatch.setenv("UIPATH_ORGANIZATION_ID", "env-org") + + context = MockRuntimeContext( + tenant_id=None, # type: ignore[arg-type] + org_id=None, # type: ignore[arg-type] + conversation_id="conv-789", + ) + + bridge = cast(SocketIOChatBridge, get_chat_bridge(cast(Any, context))) + + assert bridge.headers["X-UiPath-Internal-TenantId"] == "env-tenant" + assert bridge.headers["X-UiPath-Internal-AccountId"] == "env-org" + + def test_get_chat_bridge_includes_conversational_user_id_header_when_set( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Conversation owner id (from FpsProperties) is sent on the handshake for CAS to validate.""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "my-access-token") + + context = MockRuntimeContext(conversation_id="conv-789") + context.conversational_user_id = "owner-guid" # type: ignore[attr-defined] + + bridge = cast(SocketIOChatBridge, get_chat_bridge(cast(Any, context))) + + assert bridge.headers["X-UiPath-Internal-ConversationalUserId"] == "owner-guid" + + def test_get_chat_bridge_omits_conversational_user_id_header_when_absent( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """No header is sent when the runtime has no owner id (backward compatible).""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "my-access-token") + + context = MockRuntimeContext(conversation_id="conv-789") + + bridge = cast(SocketIOChatBridge, get_chat_bridge(cast(Any, context))) + + assert "X-UiPath-Internal-ConversationalUserId" not in bridge.headers + def test_get_chat_bridge_raises_without_uipath_url( self, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/packages/uipath/tests/cli/chat/test_voice_bridge.py b/packages/uipath/tests/cli/chat/test_voice_bridge.py index b945fb6e3..39e094bfa 100644 --- a/packages/uipath/tests/cli/chat/test_voice_bridge.py +++ b/packages/uipath/tests/cli/chat/test_voice_bridge.py @@ -135,3 +135,35 @@ def test_headers_fall_back_to_env_when_context_ids_are_none( assert bridge._headers["X-UiPath-Internal-TenantId"] == "env-tenant" assert bridge._headers["X-UiPath-Internal-AccountId"] == "env-org" + + def test_includes_conversational_user_id_header_when_set( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Conversation owner id (from FpsProperties) is sent on the handshake for CAS to validate.""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + ctx = MagicMock( + conversation_id="conv-1", + tenant_id="t", + org_id="o", + conversational_user_id="owner-guid", + ) + + bridge = get_voice_bridge(ctx, AsyncMock()) + + assert bridge._headers["X-UiPath-Internal-ConversationalUserId"] == "owner-guid" + + def test_omits_conversational_user_id_header_when_none( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """No header is sent when the runtime has no owner id (backward compatible).""" + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + ctx = MagicMock( + conversation_id="conv-1", + tenant_id="t", + org_id="o", + conversational_user_id=None, + ) + + bridge = get_voice_bridge(ctx, AsyncMock()) + + assert "X-UiPath-Internal-ConversationalUserId" not in bridge._headers diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 783b409a5..86f8936e1 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.4" +version = "2.11.5" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 4e32d0447e748f2aebb24e74453e2d46512a2e38 Mon Sep 17 00:00:00 2001 From: ctiliescuuipath Date: Fri, 19 Jun 2026 15:00:56 +0300 Subject: [PATCH 111/121] feat(guardrails): extract span ID from traceparent header for trace correlation (#1734) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/guardrails/guardrails.py | 9 +- packages/uipath-core/uv.lock | 2 +- packages/uipath-platform/pyproject.toml | 4 +- .../guardrails/_guardrails_service.py | 54 +++++- .../tests/services/test_guardrails_service.py | 176 ++++++++++++++++++ packages/uipath-platform/uv.lock | 4 +- packages/uipath/uv.lock | 4 +- 8 files changed, 243 insertions(+), 12 deletions(-) diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index df8c0bb71..ef97c0d24 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.19" +version = "0.5.20" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/guardrails/guardrails.py b/packages/uipath-core/src/uipath/core/guardrails/guardrails.py index fc7102904..576cc437a 100644 --- a/packages/uipath-core/src/uipath/core/guardrails/guardrails.py +++ b/packages/uipath-core/src/uipath/core/guardrails/guardrails.py @@ -1,7 +1,7 @@ """Guardrails models for UiPath Platform.""" from enum import Enum -from typing import Annotated, Any, Callable, Literal +from typing import Annotated, Any, Callable, Literal, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -30,6 +30,8 @@ class GuardrailValidationResult(BaseModel): Attributes: result: The validation result type. reason: Textual explanation describing why the validation passed or failed. + span_id: Span ID from the guardrail service response, formatted as a GUID + for trace correlation. None when the response omits the header. """ model_config = ConfigDict(populate_by_name=True) @@ -40,6 +42,11 @@ class GuardrailValidationResult(BaseModel): reason: str = Field( alias="reason", description="Explanation for the validation result." ) + span_id: Optional[str] = Field( + default=None, + alias="spanId", + description="Span ID returned by the guardrail service for trace correlation.", + ) class FieldSource(str, Enum): diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 2aec09333..ee08896ea 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.19" +version = "0.5.20" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index adae84ca6..37d6fcdaa 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.70" +version = "0.1.71" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" @@ -8,7 +8,7 @@ dependencies = [ "httpx>=0.28.1", "tenacity>=9.0.0", "truststore>=0.10.1", - "uipath-core>=0.5.8, <0.6.0", + "uipath-core>=0.5.20, <0.6.0", "pydantic-function-models>=0.1.11", "sqlparse>=0.5.5", ] diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py index ebfbaf33d..424b35dad 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py @@ -1,4 +1,5 @@ -from typing import Any +import re +from typing import Any, Optional from httpx import HTTPStatusError from uipath.core.guardrails import ( @@ -7,6 +8,7 @@ ) from uipath.core.tracing import traced +from ..chat.llm_trace_context import build_trace_context_headers from ..common._base_service import BaseService from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext @@ -14,6 +16,13 @@ from ..errors import EnrichedException from .guardrails import BuiltInValidatorGuardrail +# x-uipath-traceparent-id header format: {version}-{trace_id}-{span_id}[-{trace_flags}] +# Based on W3C traceparent but allows 16- or 32-hex span IDs. +_TRACEPARENT_PATTERN = re.compile( + r"^[0-9a-f]{2}-[0-9a-f]{32}-(?P[0-9a-f]{16}|[0-9a-f]{32})(?:-[0-9a-f]{2})?$", + re.IGNORECASE, +) + class GuardrailsService(BaseService): """Service for validating text against UiPath Guardrails. @@ -34,6 +43,31 @@ def __init__( ) -> None: super().__init__(config=config, execution_context=execution_context) + @staticmethod + def _extract_span_id_from_traceparent( + traceparent: Optional[str], + ) -> Optional[str]: + """Extract span ID from x-uipath-traceparent-id header and format as GUID. + + Args: + traceparent: Value from the ``x-uipath-traceparent-id`` response header. + Accepts 3-part ``"00-{trace_id}-{span_id}"`` or 4-part + ``"00-{trace_id}-{span_id}-{trace_flags}"``. Span ID may be + 16 or 32 hex chars. + + Returns: + Span ID formatted as lowercase GUID (8-4-4-4-12), or None if not parseable. + """ + if not traceparent: + return None + match = _TRACEPARENT_PATTERN.match(traceparent) + if not match: + return None + span_id_hex = match.group("span_id").lower() + # Pad to 32 chars for GUID conversion (span IDs may be 16 hex chars) + padded = span_id_hex.zfill(32) + return f"{padded[:8]}-{padded[8:12]}-{padded[12:16]}-{padded[16:20]}-{padded[20:32]}" + @staticmethod def _parse_result(result_str: str) -> GuardrailValidationResultType: """Parse result string from API response to GuardrailValidationResultType. @@ -88,12 +122,19 @@ def evaluate_guardrail( endpoint=Endpoint("/agentsruntime_/api/execution/guardrails/validate"), json=payload, ) + # Include trace context headers for server-side span correlation + trace_headers = build_trace_context_headers() + request_headers = {**(spec.headers or {}), **trace_headers} + span_id = None try: response = self.request( spec.method, url=spec.endpoint, json=spec.json, - headers=spec.headers, + headers=request_headers, + ) + span_id = self._extract_span_id_from_traceparent( + response.headers.get("x-uipath-traceparent-id") ) response_data = response.json() except EnrichedException as e: @@ -107,6 +148,11 @@ def evaluate_guardrail( and original_error.response ): try: + span_id = self._extract_span_id_from_traceparent( + original_error.response.headers.get( + "x-uipath-traceparent-id" + ) + ) response_data = original_error.response.json() except Exception: # If JSON parsing fails, re-raise the original exception @@ -127,9 +173,11 @@ def evaluate_guardrail( reason = response_data.get("details", "") # Prepare model data - model_data = { + model_data: dict[str, Any] = { "result": result.value, "reason": reason, } + if span_id: + model_data["spanId"] = span_id return GuardrailValidationResult.model_validate(model_data) diff --git a/packages/uipath-platform/tests/services/test_guardrails_service.py b/packages/uipath-platform/tests/services/test_guardrails_service.py index 9d8f5a900..dd20d5646 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_service.py +++ b/packages/uipath-platform/tests/services/test_guardrails_service.py @@ -298,3 +298,179 @@ def capture_request(request): # Verify result fields assert result.result == GuardrailValidationResultType.PASSED assert result.reason == "Validation passed" + + def test_evaluate_guardrail_sends_trace_context_headers( + self, + httpx_mock: HTTPXMock, + service: GuardrailsService, + base_url: str, + org: str, + tenant: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Outgoing request includes trace context headers.""" + captured_request = None + + def capture_request(request): + nonlocal captured_request + captured_request = request + return httpx.Response( + status_code=200, + json={ + "result": "PASSED", + "details": "OK", + }, + ) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate", + callback=capture_request, + ) + + pii_guardrail = BuiltInValidatorGuardrail( + id="test-id", + name="PII guardrail", + description="Test", + enabled_for_evals=True, + selector=GuardrailSelector( + scopes=[GuardrailScope.TOOL], match_names=["tool1"] + ), + guardrail_type="builtInValidator", + validator_type="pii_detection", + validator_parameters=[], + ) + + service.evaluate_guardrail("test input", pii_guardrail) + + assert captured_request is not None + # build_trace_context_headers() injects traceparent/tracestate when + # an active span exists; at minimum, the merge with spec.headers + # should not fail and the request should go through successfully. + # When there IS an active trace context, headers are present: + headers = dict(captured_request.headers) + # The request should have been sent (basic smoke check that + # header merging works even when no active span exists) + assert "content-type" in headers + + def test_evaluate_guardrail_extracts_span_id_from_traceparent( + self, + httpx_mock: HTTPXMock, + service: GuardrailsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """Response with x-uipath-traceparent-id header populates span_id.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate", + status_code=200, + json={ + "result": "VALIDATION_FAILED", + "details": "PII detected", + }, + headers={ + "x-uipath-traceparent-id": "00-abcdef1234567890abcdef1234567890-1234567890abcdef" + }, + ) + + pii_guardrail = BuiltInValidatorGuardrail( + id="test-id", + name="PII guardrail", + description="Test", + enabled_for_evals=True, + selector=GuardrailSelector( + scopes=[GuardrailScope.TOOL], match_names=["tool1"] + ), + guardrail_type="builtInValidator", + validator_type="pii_detection", + validator_parameters=[], + ) + + result = service.evaluate_guardrail("test input", pii_guardrail) + + assert result.result == GuardrailValidationResultType.VALIDATION_FAILED + assert result.span_id == "00000000-0000-0000-1234-567890abcdef" + + def test_evaluate_guardrail_no_traceparent_header_no_span_id( + self, + httpx_mock: HTTPXMock, + service: GuardrailsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """Response without x-uipath-traceparent-id header leaves span_id as None.""" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate", + status_code=200, + json={ + "result": "PASSED", + "details": "OK", + }, + ) + + pii_guardrail = BuiltInValidatorGuardrail( + id="test-id", + name="PII guardrail", + description="Test", + enabled_for_evals=True, + selector=GuardrailSelector( + scopes=[GuardrailScope.TOOL], match_names=["tool1"] + ), + guardrail_type="builtInValidator", + validator_type="pii_detection", + validator_parameters=[], + ) + + result = service.evaluate_guardrail("test input", pii_guardrail) + + assert result.result == GuardrailValidationResultType.PASSED + assert result.span_id is None + + class TestExtractSpanIdFromTraceparent: + """Tests for _extract_span_id_from_traceparent.""" + + def test_valid_traceparent_16_char_span_id(self) -> None: + result = GuardrailsService._extract_span_id_from_traceparent( + "00-abcdef1234567890abcdef1234567890-1234567890abcdef" + ) + assert result == "00000000-0000-0000-1234-567890abcdef" + + def test_valid_traceparent_32_char_span_id(self) -> None: + result = GuardrailsService._extract_span_id_from_traceparent( + "00-abcdef1234567890abcdef1234567890-0a1b2c3d4e5f67890a1b2c3d4e5f6789" + ) + assert result == "0a1b2c3d-4e5f-6789-0a1b-2c3d4e5f6789" + + def test_none_input(self) -> None: + assert GuardrailsService._extract_span_id_from_traceparent(None) is None + + def test_empty_string(self) -> None: + assert GuardrailsService._extract_span_id_from_traceparent("") is None + + def test_valid_traceparent_4_part_with_trace_flags(self) -> None: + result = GuardrailsService._extract_span_id_from_traceparent( + "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01" + ) + assert result == "00000000-0000-0000-1234-567890abcdef" + + def test_uppercase_hex_normalized_to_lowercase(self) -> None: + result = GuardrailsService._extract_span_id_from_traceparent( + "00-ABCDEF1234567890ABCDEF1234567890-1234567890ABCDEF" + ) + assert result == "00000000-0000-0000-1234-567890abcdef" + + def test_invalid_span_id_length_rejected(self) -> None: + """Span IDs that are neither 16 nor 32 hex chars are rejected.""" + assert ( + GuardrailsService._extract_span_id_from_traceparent( + "00-abcdef1234567890abcdef1234567890-1234abcd" + ) + is None + ) + + def test_invalid_format(self) -> None: + assert ( + GuardrailsService._extract_span_id_from_traceparent("not-valid") is None + ) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 6b5a34c52..df0dc786a 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.19" +version = "0.5.20" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.70" +version = "0.1.71" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 86f8936e1..ff53d1215 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.19" +version = "0.5.20" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.70" +version = "0.1.71" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 6d9468532ead2b812fc31955a64cddc495296ec6 Mon Sep 17 00:00:00 2001 From: ajay-kesavan Date: Fri, 19 Jun 2026 19:21:35 -0700 Subject: [PATCH 112/121] feat(eval): id-aware match with sanitised-name fallback (Option C, layer 1/3) (#1736) Co-authored-by: Claude Opus 4.7 --- packages/uipath/pyproject.toml | 2 +- .../eval/_helpers/evaluators_helpers.py | 59 +++++----- .../evaluators/test_evaluator_helpers.py | 108 ++++++++++++++++-- packages/uipath/uv.lock | 2 +- 4 files changed, 129 insertions(+), 42 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 0add2e09e..fd088202e 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.5" +version = "2.11.6" 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/eval/_helpers/evaluators_helpers.py b/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py index 912dcd41d..b639e631d 100644 --- a/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py +++ b/packages/uipath/src/uipath/eval/_helpers/evaluators_helpers.py @@ -1,5 +1,6 @@ import ast import json +import re from collections.abc import Mapping, Sequence from datetime import datetime from typing import Any @@ -13,6 +14,17 @@ TOOL_NAME_ATTR = "tool.name" +# Mirrors uipath_langchain.agent.tools.utils.sanitize_tool_name; pinned by TestSanitizedNameMatch. +_TOOL_NAME_DISALLOWED = re.compile(r"[^a-zA-Z0-9_-]") + + +def _sanitize_tool_name(name: str | None) -> str: + """Sanitise a tool name the same way the LangChain runtime does.""" + if not name: + return "" + return _TOOL_NAME_DISALLOWED.sub("", "_".join(name.split()))[:64] + + COMPARATOR_MAPPINGS = { ">": "gt", "<": "lt", @@ -39,26 +51,19 @@ def _unsynthesized_tool_attrs(span: ReadableSpan) -> Mapping[str, Any] | None: def _match_key(actual_name: str, actual_id: str | None, expected_key: str) -> bool: - """True when `expected_key` matches either the actual call's `id` or `name`. - - Eval-set criteria can be authored against either the tool's stable id or - its display name; the scorers accept whichever the author used. - """ - if actual_id is not None and expected_key == actual_id: - return True - return expected_key == actual_name + """Strict per-call kind: id-only when actual has one, sanitised-name otherwise — never cross-kind.""" + if actual_id is not None: + return expected_key == actual_id + return _sanitize_tool_name(expected_key) == _sanitize_tool_name(actual_name) def _calls_match(actual, expected) -> bool: - """True when an actual ToolCall/ToolOutput matches an expected one. - - Prefers id-equality when both sides carry an id; otherwise falls back to - name-equality. This keeps the legacy name-keyed behavior intact while - making id-keyed eval-sets rename-safe. - """ - if actual.id is not None and expected.id is not None: - return actual.id == expected.id - return actual.name == expected.name + """Strict per-call kind: id-only when actual has one, sanitised-name otherwise — never cross-kind.""" + if actual.id is not None: + # Picker stores the id under `expected.name` when an id was chosen — honour either field. + expected_key = expected.id if expected.id is not None else expected.name + return actual.id == expected_key + return _sanitize_tool_name(actual.name) == _sanitize_tool_name(expected.name) def _parse_tool_args(input_value: Any) -> dict[str, Any]: @@ -103,18 +108,11 @@ def _build_tool_call(span: ReadableSpan, include_args: bool) -> ToolCall | None: def count_tool_calls_by_name_and_id(tool_calls: Sequence[ToolCall]) -> dict[str, int]: - """Count tool calls under BOTH their name and id keys. - - Each call contributes one unit to its name bucket and (when present and - different) one unit to its id bucket. Lookups by either return the same - count for that tool. This lets `tool_calls_count_score` honour eval-set - criteria keyed by id with no signature change to the score function. - """ + """Bucket each call under its id when present, else its name — strict per-call kind, no cross-kind matching.""" counts: dict[str, int] = {} for c in tool_calls: - counts[c.name] = counts.get(c.name, 0) + 1 - if c.id is not None and c.id != c.name: - counts[c.id] = counts.get(c.id, 0) + 1 + key = c.id if c.id is not None else c.name + counts[key] = counts.get(key, 0) + 1 return counts @@ -424,7 +422,12 @@ def tool_calls_count_score( expected_comparator, expected_count, ) in expected_tool_calls_count.items(): - actual_count = actual_tool_calls_count.get(tool_name, 0.0) + # Raw key first (id-keyed / exact-match), then sanitised (legacy display-name). `is None` not `or`: count of 0 is a hit. + actual_count = actual_tool_calls_count.get(tool_name) + if actual_count is None: + actual_count = actual_tool_calls_count.get( + _sanitize_tool_name(tool_name), 0 + ) comparator = f"__{COMPARATOR_MAPPINGS[expected_comparator]}__" to_add = float(getattr(actual_count, comparator)(expected_count)) diff --git a/packages/uipath/tests/evaluators/test_evaluator_helpers.py b/packages/uipath/tests/evaluators/test_evaluator_helpers.py index 061b57aab..6064381cb 100644 --- a/packages/uipath/tests/evaluators/test_evaluator_helpers.py +++ b/packages/uipath/tests/evaluators/test_evaluator_helpers.py @@ -12,6 +12,9 @@ import pytest from uipath.eval._helpers.evaluators_helpers import ( + _calls_match, + _match_key, + _sanitize_tool_name, extract_tool_calls, extract_tool_calls_names, extract_tool_calls_outputs, @@ -1040,13 +1043,22 @@ def test_args_score_matches_by_id_when_names_differ(self) -> None: score, _ = tool_calls_args_score(actual, expected) assert score == 1.0 - def test_args_score_falls_back_to_name_when_id_missing(self) -> None: - """Legacy eval-set without id still matches by name (back-compat).""" + def test_args_score_strict_kind_no_id_fallback(self) -> None: + """Strict kind: actual has id → id-only mode, no name fallback even when actual.name matches expected.name.""" from uipath.eval._helpers.evaluators_helpers import tool_calls_args_score actual = [ToolCall(name="Web_Search", id="uuid-1", args={"q": "x"})] expected = [ToolCall(name="Web_Search", args={"q": "x"})] score, _ = tool_calls_args_score(actual, expected) + assert score == 0.0 # actual.id="uuid-1" != expected.name="Web_Search" + + def test_args_score_name_only_when_actual_has_no_id(self) -> None: + """When actual has no id, sanitised-name comparison is the only path.""" + from uipath.eval._helpers.evaluators_helpers import tool_calls_args_score + + actual = [ToolCall(name="Web_Search", args={"q": "x"})] + expected = [ToolCall(name="Web Search", args={"q": "x"})] + score, _ = tool_calls_args_score(actual, expected) assert score == 1.0 def test_args_score_no_match_when_ids_differ(self) -> None: @@ -1067,7 +1079,7 @@ def test_output_score_matches_by_id(self) -> None: assert score == 1.0 def test_count_by_name_and_id_helper(self) -> None: - """Each call contributes one unit to its name AND to its id key.""" + """Strict per-call kind: id-keyed when call has id, name-keyed otherwise — never both.""" from uipath.eval._helpers.evaluators_helpers import ( count_tool_calls_by_name_and_id, ) @@ -1078,12 +1090,12 @@ def test_count_by_name_and_id_helper(self) -> None: ToolCall(name="get_temp", args={}), # no id ] counts = count_tool_calls_by_name_and_id(calls) - assert counts["Web_Search"] == 2 - assert counts["uuid-1"] == 2 # same calls retrievable by id - assert counts["get_temp"] == 1 + assert counts == {"uuid-1": 2, "get_temp": 1} + # Name key is NOT populated when id is present — kind separation. + assert "Web_Search" not in counts def test_order_score_with_ids_matches_id_keyed_expected(self) -> None: - """LCS treats expected key as match if it equals actual.id OR actual.name.""" + """Strict kind: actual has id → only id-keyed expected matches; legacy name-keyed against id-bearing actual is a miss.""" from uipath.eval._helpers.evaluators_helpers import ( tool_calls_order_score_with_ids, ) @@ -1092,15 +1104,15 @@ def test_order_score_with_ids_matches_id_keyed_expected(self) -> None: ToolCall(name="Web_Search", id="uuid-1", args={}), ToolCall(name="Web_Search", id="uuid-1", args={}), ] - # Expected authored by id + # Expected authored by id matches. score, _ = tool_calls_order_score_with_ids(actual, ["uuid-1", "uuid-1"]) assert score == 1.0 - # Expected authored by name (legacy) + # Expected authored by name against id-bearing actual is a miss (no cross-kind). score, _ = tool_calls_order_score_with_ids(actual, ["Web_Search", "Web_Search"]) - assert score == 1.0 - # Mixed: works either way + assert score == 0.0 + # Mixed expected: only the id-keyed element matches. score, _ = tool_calls_order_score_with_ids(actual, ["uuid-1", "Web_Search"]) - assert score == 1.0 + assert 0.0 < score < 1.0 def test_order_score_with_ids_back_compat_when_id_absent(self) -> None: """When actual has no ids (legacy traces), comparison is name-only.""" @@ -1114,3 +1126,75 @@ def test_order_score_with_ids_back_compat_when_id_absent(self) -> None: ] score, _ = tool_calls_order_score_with_ids(actual, ["get_temp", "get_humidity"]) assert score == 1.0 + + +class TestSanitizedNameMatch: + """Sanitised-name fallback in ``_match_key`` / ``_calls_match`` — id-equality wins first.""" + + @staticmethod + def _reference_sanitize(name: str) -> str: + """Pinned copy of ``uipath_langchain.agent.tools.utils.sanitize_tool_name``.""" + import re + + trim_whitespaces = "_".join(name.split()) + sanitized = re.sub(r"[^a-zA-Z0-9_-]", "", trim_whitespaces) + return sanitized[:64] + + @pytest.mark.parametrize( + "raw", + [ + "Web Search", + "Google Sheets / Read", + "Add Numbers", + "tool with spaces and (parens)", + "snake_case_tool", + "kebab-case-tool", + "alreadySanitised", + " multiple whitespace ", + "very-long-name-" + "x" * 100, + "", + ], + ) + def test_normalize_matches_langchain_reference(self, raw: str) -> None: + assert _sanitize_tool_name(raw) == self._reference_sanitize(raw) + + def test_normalize_handles_none(self) -> None: + assert _sanitize_tool_name(None) == "" + + def test_match_key_display_vs_sanitised(self) -> None: + assert _match_key("Web_Search", None, "Web Search") is True + + def test_match_key_id_wins_when_present(self) -> None: + assert _match_key("Web_Search", "webSearch1", "webSearch1") is True + # Strict kind: actual has id → display-name expected is rejected (no cross-kind). + assert _match_key("Web_Search", "webSearch1", "Web Search") is False + + def test_match_key_mismatch_after_sanitising(self) -> None: + assert _match_key("Web_Search", None, "Image_Search") is False + + def test_calls_match_display_vs_sanitised(self) -> None: + actual = ToolCall(name="Web_Search", args={}) + expected = ToolCall(name="Web Search", args={}) + assert _calls_match(actual, expected) is True + + def test_calls_match_id_equality_unchanged(self) -> None: + actual = ToolCall(name="Web_Search", id="webSearch1", args={}) + expected = ToolCall(name="totally different", id="webSearch1", args={}) + assert _calls_match(actual, expected) is True + + def test_calls_match_output_display_vs_sanitised(self) -> None: + actual = ToolOutput(name="Web_Search", output="x") + expected = ToolOutput(name="Web Search", output="x") + assert _calls_match(actual, expected) is True + + def test_count_score_display_name_matches_sanitised_actual(self) -> None: + actual = {"Web_Search": 2} + expected = {"Web Search": ("==", 2)} + score, _ = tool_calls_count_score(actual, expected) + assert score == 1.0 + + def test_count_score_id_keyed_expected_still_wins(self) -> None: + actual = {"Web_Search": 1, "webSearch1": 1} + expected = {"webSearch1": (">=", 1)} + score, _ = tool_calls_count_score(actual, expected) + assert score == 1.0 diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index ff53d1215..db1345c98 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.5" +version = "2.11.6" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From a99eaac18d2dabbcb73f631118b729344b57829c Mon Sep 17 00:00:00 2001 From: andrewwan-uipath <170454542+andrewwan-uipath@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:43:58 -0700 Subject: [PATCH 113/121] feat: optionally emit end exchange event to CAS [JAR-9933] (#1720) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/_cli/_chat/_bridge.py | 12 +++ packages/uipath/tests/cli/chat/test_bridge.py | 85 +++++++++++++++++++ packages/uipath/uv.lock | 10 +-- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index fd088202e..581f9a3d2 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.6" +version = "2.11.7" 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/_chat/_bridge.py b/packages/uipath/src/uipath/_cli/_chat/_bridge.py index b4cddf3b8..442322e36 100644 --- a/packages/uipath/src/uipath/_cli/_chat/_bridge.py +++ b/packages/uipath/src/uipath/_cli/_chat/_bridge.py @@ -106,6 +106,7 @@ def __init__( exchange_id: str, headers: dict[str, str], auth: dict[str, Any] | None = None, + end_exchange: bool = True, ): """Initialize the WebSocket chat bridge. @@ -115,6 +116,8 @@ def __init__( exchange_id: The exchange ID for this session headers: HTTP headers to send during connection auth: Optional authentication data to send during connection + end_exchange: Whether to send the exchange-end event to CAS on + completion. """ self.websocket_url = websocket_url self.websocket_path = websocket_path @@ -122,6 +125,7 @@ def __init__( self.exchange_id = exchange_id self.auth = auth self.headers = headers + self.end_exchange = end_exchange self._client: Any | None = None self._connected_event = asyncio.Event() @@ -283,9 +287,16 @@ async def emit_message_event( async def emit_exchange_end_event(self) -> None: """Send an exchange end event. + When end_exchange is False the exchange is left open — the event is not + sent to CAS so a downstream consumer can continue and end it later. + Raises: RuntimeError: If client is not connected """ + if not self.end_exchange: + logger.info("end_exchange is False; leaving the exchange open.") + return + if self._client is None: raise RuntimeError("WebSocket client not connected. Call connect() first.") @@ -540,6 +551,7 @@ def get_chat_bridge( conversation_id=context.conversation_id, exchange_id=context.exchange_id, headers=headers, + end_exchange=getattr(context, "end_exchange", True), ) diff --git a/packages/uipath/tests/cli/chat/test_bridge.py b/packages/uipath/tests/cli/chat/test_bridge.py index 41659ffe2..2c18640f3 100644 --- a/packages/uipath/tests/cli/chat/test_bridge.py +++ b/packages/uipath/tests/cli/chat/test_bridge.py @@ -22,11 +22,13 @@ def __init__( exchange_id: str = "test-exchange-id", tenant_id: str = "test-tenant-id", org_id: str = "test-org-id", + end_exchange: bool = True, ): self.conversation_id = conversation_id self.exchange_id = exchange_id self.tenant_id = tenant_id self.org_id = org_id + self.end_exchange = end_exchange class TestSocketIOChatBridgeDebugMode: @@ -357,6 +359,89 @@ async def test_emit_exchange_end_raises_without_client(self) -> None: assert "not connected" in str(exc_info.value).lower() +class TestSocketIOChatBridgeEndExchange: + """The bridge owns whether to honor the exchange-end event (CAS-specific).""" + + def _make_connected_bridge(self, end_exchange: bool) -> SocketIOChatBridge: + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + end_exchange=end_exchange, + ) + bridge._websocket_disabled = False + bridge._client = AsyncMock() + bridge._connected_event.set() + return bridge + + def test_end_exchange_defaults_true(self) -> None: + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + ) + assert bridge.end_exchange is True + + @pytest.mark.anyio + async def test_emit_exchange_end_sends_when_end_exchange_true(self) -> None: + bridge = self._make_connected_bridge(end_exchange=True) + + await bridge.emit_exchange_end_event() + + cast(AsyncMock, bridge._client).emit.assert_awaited_once() + assert ( + cast(AsyncMock, bridge._client).emit.await_args.args[0] + == "ConversationEvent" + ) + + @pytest.mark.anyio + async def test_emit_exchange_end_suppressed_when_end_exchange_false(self) -> None: + bridge = self._make_connected_bridge(end_exchange=False) + + await bridge.emit_exchange_end_event() + + cast(AsyncMock, bridge._client).emit.assert_not_awaited() + + @pytest.mark.anyio + async def test_emit_exchange_end_false_does_not_require_client(self) -> None: + """With the exchange kept open, suppression happens before the connection check.""" + bridge = SocketIOChatBridge( + websocket_url="wss://test.example.com", + websocket_path="/socket.io", + conversation_id="conv-123", + exchange_id="exch-456", + headers={}, + end_exchange=False, + ) + + # Should not raise even though _client is None. + await bridge.emit_exchange_end_event() + + def test_get_chat_bridge_propagates_end_exchange_false( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + context = MockRuntimeContext(end_exchange=False) + + bridge = cast(SocketIOChatBridge, get_chat_bridge(cast(Any, context))) + + assert bridge.end_exchange is False + + def test_get_chat_bridge_defaults_end_exchange_true( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com") + context = MockRuntimeContext() + + bridge = cast(SocketIOChatBridge, get_chat_bridge(cast(Any, context))) + + assert bridge.end_exchange is True + + class TestSignalRDebugBridgeSendMethod: """Tests for SignalRDebugBridge.""" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index db1345c98..a805e5cdc 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-06-20T16:42:14.097008Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.6" +version = "2.11.7" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2729,14 +2729,14 @@ dev = [ [[package]] name = "uipath-runtime" -version = "0.11.0" +version = "0.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/8d/4d36d6a5dda4ca5f25e52508bc20dd82cb92fcdf2a36cd0adc4f9832d047/uipath_runtime-0.11.0.tar.gz", hash = "sha256:cc94f2fdab43b593ef678eff904fc6cdd4831963cffe39a83909ffcf9082d76f", size = 143685, upload-time = "2026-05-29T15:13:30.562Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/87/fed3a5bd3479b9e7dc6cba769b054f5f1c00e93762356a70010e32f1f03c/uipath_runtime-0.11.2.tar.gz", hash = "sha256:8b3cc986644d6c9f2365345c231577f97d3bad8fb105fe8a6c7e16508d00d9ef", size = 145770, upload-time = "2026-06-22T16:31:40.786Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/c7b90851d4544ff5e76ca7c55452597aae1619cf1ebc2c0aa7b098110f14/uipath_runtime-0.11.0-py3-none-any.whl", hash = "sha256:08bf53a0e38bb3d19edc6708d2ecb7d918aa96fdda13e35f3ad0e6f2a6c392b9", size = 43770, upload-time = "2026-05-29T15:13:29.282Z" }, + { url = "https://files.pythonhosted.org/packages/da/62/c649c18ac39f53e5603abbcfa6917f6e880ac08047b1ce69d4c3ee937de8/uipath_runtime-0.11.2-py3-none-any.whl", hash = "sha256:a3a14dc2378bc934437bbd7523cf884d0cee0eeef26c736a9d6ce504d8a9fea0", size = 43874, upload-time = "2026-06-22T16:31:39.328Z" }, ] [[package]] From 3753803cdcf6eef45eba96b1e47928817a824bbd Mon Sep 17 00:00:00 2001 From: AAgnihotry <95259907+AAgnihotry@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:23:42 -0700 Subject: [PATCH 114/121] feat(eval): add SimulateComponentMocker for per-component API-based simulation (#1732) --- packages/uipath/pyproject.toml | 2 +- .../simulate-component-agent/input.json | 4 + .../samples/simulate-component-agent/main.py | 118 +++++ .../simulate-component-agent/pyproject.toml | 17 + .../simulate-component-agent/simulation.json | 73 +++ .../simulate-component-agent/uipath.json | 6 + .../uipath/src/uipath/eval/mocks/__init__.py | 20 +- .../src/uipath/eval/mocks/_mock_context.py | 16 +- .../src/uipath/eval/mocks/_mock_runtime.py | 42 +- .../src/uipath/eval/mocks/_mocker_factory.py | 3 + .../eval/mocks/_simulate_component_mocker.py | 143 ++++++ .../eval/mocks/_simulate_component_service.py | 37 ++ .../uipath/src/uipath/eval/mocks/_types.py | 117 ++++- .../cli/eval/mocks/test_simulate_component.py | 442 ++++++++++++++++++ .../uipath/tests/cli/test_debug_simulation.py | 33 +- packages/uipath/uv.lock | 2 +- 16 files changed, 1054 insertions(+), 21 deletions(-) create mode 100644 packages/uipath/samples/simulate-component-agent/input.json create mode 100644 packages/uipath/samples/simulate-component-agent/main.py create mode 100644 packages/uipath/samples/simulate-component-agent/pyproject.toml create mode 100644 packages/uipath/samples/simulate-component-agent/simulation.json create mode 100644 packages/uipath/samples/simulate-component-agent/uipath.json create mode 100644 packages/uipath/src/uipath/eval/mocks/_simulate_component_mocker.py create mode 100644 packages/uipath/src/uipath/eval/mocks/_simulate_component_service.py create mode 100644 packages/uipath/tests/cli/eval/mocks/test_simulate_component.py diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 581f9a3d2..4038e1313 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.7" +version = "2.11.8" 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/samples/simulate-component-agent/input.json b/packages/uipath/samples/simulate-component-agent/input.json new file mode 100644 index 000000000..68093d7d0 --- /dev/null +++ b/packages/uipath/samples/simulate-component-agent/input.json @@ -0,0 +1,4 @@ +{ + "city": "London", + "days": 3 +} diff --git a/packages/uipath/samples/simulate-component-agent/main.py b/packages/uipath/samples/simulate-component-agent/main.py new file mode 100644 index 000000000..0acd8f0cb --- /dev/null +++ b/packages/uipath/samples/simulate-component-agent/main.py @@ -0,0 +1,118 @@ +"""Weather forecast agent demonstrating per-component simulation. + +This sample shows the new ``components`` simulation format where each tool +has its own simulation strategy and instructions, routed to the +simulate-component API instead of a local LLM. + +Run with real tools (no weather API — returns hardcoded defaults): + uipath run main -f input.json + +Run with per-component simulation (routes each tool call to the API): + uipath run main -f input.json --simulation "$(cat simulation.json)" +""" + +from pydantic import BaseModel +from pydantic.dataclasses import dataclass + +from uipath.eval.mocks import mockable +from uipath.tracing import traced + +# --------------------------------------------------------------------------- +# Input / Output models +# --------------------------------------------------------------------------- + + +@dataclass +class WeatherInput: + city: str + days: int = 3 + + +class CurrentWeather(BaseModel): + city: str + temperature: float # Celsius + condition: str + humidity: int # percent + + +class ForecastDay(BaseModel): + date: str # YYYY-MM-DD + high: float + low: float + condition: str + + +class WeatherReport(BaseModel): + current: CurrentWeather + forecast: list[ForecastDay] + summary: str + + +# --------------------------------------------------------------------------- +# Mockable tool functions +# --------------------------------------------------------------------------- + + +@traced(name="get_current_weather", span_type="tool") +@mockable() +async def get_current_weather(city: str) -> CurrentWeather: + """Fetch current weather conditions for a city from an external weather API. + + Args: + city: Name of the city (e.g. "London", "New York"). + + Returns: + CurrentWeather with temperature, condition, and humidity. + """ + # Real implementation would call a weather API such as OpenWeatherMap. + # Returns hardcoded defaults when not simulated. + return CurrentWeather(city=city, temperature=20.0, condition="unknown", humidity=50) + + +@traced(name="get_forecast", span_type="tool") +@mockable() +async def get_forecast(city: str, days: int = 3) -> list[ForecastDay]: + """Retrieve a multi-day weather forecast for a city. + + Args: + city: Name of the city. + days: Number of forecast days to retrieve (default: 3). + + Returns: + List of ForecastDay objects, one per requested day. + """ + # Real implementation would call a forecast API. + # Returns an empty list when not simulated. + return [] + + +# --------------------------------------------------------------------------- +# Agent entry point +# --------------------------------------------------------------------------- + + +@traced(name="main") +async def main(input: WeatherInput) -> WeatherReport: + """Fetch current weather and forecast for a city and produce a report. + + Args: + input: WeatherInput with city name and number of forecast days. + + Returns: + WeatherReport combining current conditions, forecast, and a summary. + """ + current = await get_current_weather(input.city) + forecast = await get_forecast(input.city, input.days) + + issues = [] + if current.humidity > 80: + issues.append("high humidity") + if current.temperature < 0: + issues.append("freezing temperatures") + + alert = f" Alerts: {', '.join(issues)}." if issues else "" + summary = ( + f"{input.city}: {current.temperature}°C, {current.condition}." + f" {len(forecast)}-day forecast available.{alert}" + ) + return WeatherReport(current=current, forecast=forecast, summary=summary) diff --git a/packages/uipath/samples/simulate-component-agent/pyproject.toml b/packages/uipath/samples/simulate-component-agent/pyproject.toml new file mode 100644 index 000000000..c4228e861 --- /dev/null +++ b/packages/uipath/samples/simulate-component-agent/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "simulate-component-agent" +version = "0.0.1" +description = "Weather forecast agent demonstrating per-component simulation" +authors = [{ name = "UiPath", email = "python-sdk@uipath.com" }] +dependencies = [ + "uipath", +] +requires-python = ">=3.11" + +[dependency-groups] +dev = [ + "uipath-dev", +] + +[tool.uv.sources] +uipath = { path = "../..", editable = true } diff --git a/packages/uipath/samples/simulate-component-agent/simulation.json b/packages/uipath/samples/simulate-component-agent/simulation.json new file mode 100644 index 000000000..fc87bd527 --- /dev/null +++ b/packages/uipath/samples/simulate-component-agent/simulation.json @@ -0,0 +1,73 @@ +{ + "enabled": true, + "components": [ + { + "componentId": "get_current_weather", + "componentType": "tool", + "simulationStrategy": 0, + "simulationInstruction": "Return realistic current weather for the given city. Use typical seasonal temperatures for the Northern Hemisphere in winter. the humidity should always be 70%", + "outputSchema": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "temperature": { + "type": "number", + "description": "Temperature in Celsius" + }, + "condition": { + "type": "string", + "description": "e.g. cloudy, rainy, sunny" + }, + "humidity": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + "required": [ + "city", + "temperature", + "condition", + "humidity" + ] + } + }, + { + "componentId": "get_forecast", + "componentType": "tool", + "simulationStrategy": 0, + "simulationInstruction": "Return a realistic multi-day weather forecast for the given city.", + "outputSchema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "description": "YYYY-MM-DD" + }, + "high": { + "type": "number", + "description": "High temperature in Celsius" + }, + "low": { + "type": "number", + "description": "Low temperature in Celsius" + }, + "condition": { + "type": "string" + } + }, + "required": [ + "date", + "high", + "low", + "condition" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/packages/uipath/samples/simulate-component-agent/uipath.json b/packages/uipath/samples/simulate-component-agent/uipath.json new file mode 100644 index 000000000..a991b6914 --- /dev/null +++ b/packages/uipath/samples/simulate-component-agent/uipath.json @@ -0,0 +1,6 @@ +{ + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "functions": { + "main": "main.py:main" + } +} diff --git a/packages/uipath/src/uipath/eval/mocks/__init__.py b/packages/uipath/src/uipath/eval/mocks/__init__.py index f9e7da177..ddb76ca70 100644 --- a/packages/uipath/src/uipath/eval/mocks/__init__.py +++ b/packages/uipath/src/uipath/eval/mocks/__init__.py @@ -6,13 +6,31 @@ build_mocking_context, build_mocking_context_from_dict, ) -from ._types import ExampleCall, MockingContext, SimulationConfig +from ._types import ( + ComponentSimulationConfig, + ExampleCall, + MockingContext, + RuleOperator, + SimulationAnswer, + SimulationAnswerType, + SimulationBehavior, + SimulationCondition, + SimulationConfig, + SimulationStrategy, +) from .mockable import mockable __all__ = [ + "ComponentSimulationConfig", "ExampleCall", "MockingContext", + "RuleOperator", + "SimulationAnswer", + "SimulationAnswerType", + "SimulationBehavior", + "SimulationCondition", "SimulationConfig", + "SimulationStrategy", "UiPathMockRuntime", "build_mocking_context", "build_mocking_context_from_dict", diff --git a/packages/uipath/src/uipath/eval/mocks/_mock_context.py b/packages/uipath/src/uipath/eval/mocks/_mock_context.py index a50df9cba..bd2f80df6 100644 --- a/packages/uipath/src/uipath/eval/mocks/_mock_context.py +++ b/packages/uipath/src/uipath/eval/mocks/_mock_context.py @@ -43,17 +43,25 @@ def is_tool_simulated(tool_name: str) -> bool: to be simulated, False otherwise. """ ctx = mocking_context.get() - strategy = ctx.strategy if ctx else None - if strategy is None: + if ctx is None: return False normalized_tool_name = _normalize_tool_name(tool_name) + if ctx.components: + return any( + _normalize_tool_name(c.component_id) == normalized_tool_name + for c in ctx.components + ) + + strategy = ctx.strategy + if strategy is None: + return False + if isinstance(strategy, LLMMockingStrategy): - simulated_names = [ + return normalized_tool_name in [ _normalize_tool_name(t.name) for t in strategy.tools_to_simulate ] - return normalized_tool_name in simulated_names elif isinstance(strategy, MockitoMockingStrategy): return any( _normalize_tool_name(b.function) == normalized_tool_name diff --git a/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py b/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py index 71036215b..60cc78c2b 100644 --- a/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py +++ b/packages/uipath/src/uipath/eval/mocks/_mock_runtime.py @@ -37,16 +37,32 @@ def build_mocking_context( config: SimulationConfig, agent_model: str | None = None ) -> MockingContext | None: - """Build a MockingContext from a validated SimulationConfig. + """Build a MockingContext from a validated SimulationConfig.""" + if not config.enabled: + return None - Args: - config: Validated simulation config. - agent_model: Optional agent model name to use as fallback. + # New per-component format → routes to simulate-component API + if config.components: + from uipath.platform.common._config import UiPathConfig - Returns: - MockingContext if enabled and tools are specified, None otherwise. - """ - if not config.enabled or not config.tools_to_simulate: + workload_id = ( + getattr(UiPathConfig, "agent_id", None) + or getattr(UiPathConfig, "project_id", None) + or str(uuid.uuid4()) + ) + logger.debug( + f"Loaded simulation config for {len(config.components)} component(s)" + ) + return MockingContext( + strategy=None, + name="debug-simulation", + inputs={}, + components=config.components, + workload_id=workload_id, + ) + + # Legacy format (toolsToSimulate + instructions) → routes to local LLM mocker + if not config.tools_to_simulate: return None model = ( @@ -125,12 +141,16 @@ def set_execution_context( mocking_context.set(context) try: - if context and context.strategy: - mocker_context.set(MockerFactory.create(context)) + if context and (context.strategy or context.components): + mocker = MockerFactory.create(context) + mocker_context.set(mocker) + logger.info( + "simulate-component: mocker created (%s)", type(mocker).__name__ + ) else: mocker_context.set(None) except Exception: - logger.warning("Failed to create mocker.") + logger.warning("Failed to create mocker.", exc_info=True) mocker_context.set(None) span_collector_context.set(span_collector) diff --git a/packages/uipath/src/uipath/eval/mocks/_mocker_factory.py b/packages/uipath/src/uipath/eval/mocks/_mocker_factory.py index 4c001bfe0..2f61162a4 100644 --- a/packages/uipath/src/uipath/eval/mocks/_mocker_factory.py +++ b/packages/uipath/src/uipath/eval/mocks/_mocker_factory.py @@ -3,6 +3,7 @@ from ._llm_mocker import LLMMocker from ._mocker import Mocker from ._mockito_mocker import MockitoMocker +from ._simulate_component_mocker import SimulateComponentMocker from ._types import ( LLMMockingStrategy, MockingContext, @@ -16,6 +17,8 @@ class MockerFactory: @staticmethod def create(context: MockingContext) -> Mocker: """Create a mocker instance.""" + if context.components: + return SimulateComponentMocker(context) match context.strategy: case LLMMockingStrategy(): return LLMMocker(context) diff --git a/packages/uipath/src/uipath/eval/mocks/_simulate_component_mocker.py b/packages/uipath/src/uipath/eval/mocks/_simulate_component_mocker.py new file mode 100644 index 000000000..f35359ec4 --- /dev/null +++ b/packages/uipath/src/uipath/eval/mocks/_simulate_component_mocker.py @@ -0,0 +1,143 @@ +"""Mocker that routes tool calls through the simulate-component API.""" + +from __future__ import annotations + +import logging +from typing import Any, Callable, cast + +from pydantic import TypeAdapter + +from uipath.platform.chat._llm_gateway_service import _cleanup_schema + +from .._execution_context import execution_id_context, span_collector_context +from ._llm_mocker import LLMMocker +from ._mocker import ( + Mocker, + R, + T, + UiPathMockResponseGenerationError, + UiPathNoMockFoundError, +) +from ._simulate_component_service import _create_simulate_component_service +from ._types import ComponentSimulationConfig, MockingContext + +logger = logging.getLogger(__name__) + + +class SimulateComponentMocker(Mocker): + """Routes each tool call to the simulate-component API based on per-component config.""" + + def __init__(self, context: MockingContext) -> None: + self._context = context + self._components: dict[str, ComponentSimulationConfig] = { + c.component_id: c for c in (context.components or []) + } + self._normalized: dict[str, ComponentSimulationConfig] = { + c.component_id.replace("_", " "): c for c in (context.components or []) + } + self._workload_id = context.workload_id or "" + + def _find_component(self, tool_name: str) -> ComponentSimulationConfig | None: + return self._components.get(tool_name) or self._normalized.get( + tool_name.replace("_", " ") + ) + + async def response( + self, + func: Callable[[T], R], + params: dict[str, Any], + invocation: tuple[tuple[Any, ...], dict[str, Any]], + ) -> R: + tool_name = params.get("name") or func.__name__ + component = self._find_component(tool_name) + + if component is None: + raise UiPathNoMockFoundError(f"No simulation config for '{tool_name}'.") + + args, kwargs = invocation + + return_type: Any = func.__annotations__.get("return", None) or Any + raw_output_schema = ( + params.get("output_schema") or TypeAdapter(return_type).json_schema() + ) + output_schema = component.output_schema or _cleanup_schema(raw_output_schema) + input_payload = {"args": list(args), "kwargs": kwargs} + input_schema = component.input_schema or params.get("input_schema") + + execution_history = self._build_execution_history() + trace_id, parent_span_id = self._get_span_context() + workload_info = { + "name": self._context.name, + "userInput": self._context.inputs, + } + + example_calls = [ + {"id": ex.id, "input": ex.input, "output": ex.output} + for ex in (params.get("example_calls") or []) + ] + + payload: dict[str, Any] = { + "workloadId": self._workload_id, + "componentId": component.component_id, + "componentType": component.component_type or "tool", + "componentDescription": component.component_description + or params.get("description"), + "input": input_payload, + "inputSchema": input_schema, + "outputSchema": output_schema, + "simulationInstruction": component.simulation_instruction, + "simulationStrategy": int(component.simulation_strategy), + "mockValue": component.mock_value, + "behaviors": ( + [b.model_dump() for b in component.behaviors] + if component.behaviors + else None + ), + "exampleCalls": example_calls or None, + "executionHistory": execution_history or None, + "workloadInfo": workload_info, + "traceId": trace_id, + "parentSpanId": parent_span_id, + } + + logger.info("simulate-component: calling API for '%s'", tool_name) + try: + service = _create_simulate_component_service() + result = await service.simulate(payload) + except Exception as e: + logger.error( + "simulate-component: API call failed for '%s': %s", tool_name, e + ) + raise UiPathMockResponseGenerationError( + f"simulate-component API call failed for '{tool_name}'" + ) from e + + status = result.get("status") + if status == 1: # Completed + logger.info("simulate-component: '%s' simulated successfully", tool_name) + return cast(R, result.get("simulatedOutput")) + + error = result.get("error") or {} + error_message = error.get("message", f"Simulation failed for '{tool_name}'") + logger.error("simulate-component: '%s' failed — %s", tool_name, error_message) + raise UiPathMockResponseGenerationError(error_message) + + def _build_execution_history(self) -> str | None: + span_collector = span_collector_context.get() + execution_id = execution_id_context.get() + if span_collector and execution_id: + spans = span_collector.get_spans(execution_id) + return LLMMocker.spans_to_llm_context(spans) if spans else None + return None + + @staticmethod + def _get_span_context() -> tuple[str | None, str | None]: + """Return (traceId, parentSpanId) from the current OTel span, or (None, None).""" + from opentelemetry import trace + + span_ctx = trace.get_current_span().get_span_context() + if not span_ctx.is_valid: + return None, None + trace_id = f"{span_ctx.trace_id:032x}" + span_id = f"{span_ctx.span_id:016x}" + return trace_id, span_id diff --git a/packages/uipath/src/uipath/eval/mocks/_simulate_component_service.py b/packages/uipath/src/uipath/eval/mocks/_simulate_component_service.py new file mode 100644 index 000000000..87a08bd37 --- /dev/null +++ b/packages/uipath/src/uipath/eval/mocks/_simulate_component_service.py @@ -0,0 +1,37 @@ +"""Service for calling the simulate-component API.""" + +from typing import Any + +from uipath._utils import Endpoint +from uipath.platform.common import BaseService + + +class SimulateComponentService(BaseService): + async def simulate(self, payload: dict[str, Any]) -> dict[str, Any]: + from uipath.platform.common import UiPathConfig + + headers: dict[str, str] = {} + if UiPathConfig.tenant_id: + headers["X-UiPath-Internal-TenantId"] = UiPathConfig.tenant_id + if UiPathConfig.organization_id: + headers["X-UiPath-Internal-AccountId"] = UiPathConfig.organization_id + + response = await self.request_async( + "POST", + url=Endpoint( + "/agentsruntime_/api/execution/simulations/simulate-component" + ), + json=payload, + headers=headers, + ) + return response.json() + + +def _create_simulate_component_service() -> SimulateComponentService: + from uipath.platform import UiPath + + uipath = UiPath() + return SimulateComponentService( + config=uipath._config, + execution_context=uipath._execution_context, + ) diff --git a/packages/uipath/src/uipath/eval/mocks/_types.py b/packages/uipath/src/uipath/eval/mocks/_types.py index 070040b65..f1a15312c 100644 --- a/packages/uipath/src/uipath/eval/mocks/_types.py +++ b/packages/uipath/src/uipath/eval/mocks/_types.py @@ -121,18 +121,133 @@ class UnknownMockingStrategy(BaseMockingStrategy): MockingStrategy = Union[KnownMockingStrategy, UnknownMockingStrategy] +# --------------------------------------------------------------------------- +# Per-component simulation types — mirror the simulate-component API contract +# --------------------------------------------------------------------------- + + +class SimulationStrategy(int, Enum): + """Simulation strategy matching the simulate-component API. + + Integer values are part of the cross-language API contract — do not reorder. + """ + + LLM = 0 + MOCKITO = 1 + STATIC = 2 + + +class RuleOperator(int, Enum): + """Comparison operator for Mockito condition matching. + + Integer values are part of the cross-language API contract — do not reorder. + """ + + EQ = 0 + NE = 1 + GT = 2 + GTE = 3 + LT = 4 + LTE = 5 + CONTAINS = 6 + + +class SimulationAnswerType(int, Enum): + """Answer type for a Mockito simulation behavior. + + Integer values are part of the cross-language API contract — do not reorder. + """ + + RETURN = 0 + RAISE = 1 + + +class SimulationAnswer(BaseModel): + type: SimulationAnswerType = SimulationAnswerType.RETURN + value: Any = None + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class SimulationCondition(BaseModel): + field: str + op: RuleOperator = RuleOperator.EQ + value: Any = None + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class SimulationBehavior(BaseModel): + when: list[SimulationCondition] | None = None + then: list[SimulationAnswer] + + model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) + + +class ComponentSimulationConfig(BaseModel): + """Per-component simulation config matching the simulate-component API request schema. + + Runtime-injected fields (workloadId, runId, input, traceId, parentSpanId, + folderKey) are supplied at call time. inputSchema and outputSchema can be + overridden here; if omitted they are derived from the function annotations. + """ + + component_id: str = Field(..., alias="componentId") + component_type: str | None = Field(None, alias="componentType") + component_description: str | None = Field(None, alias="componentDescription") + simulation_instruction: str | None = Field(None, alias="simulationInstruction") + simulation_strategy: SimulationStrategy = Field( + SimulationStrategy.LLM, alias="simulationStrategy" + ) + mock_value: Any = Field(None, alias="mockValue") + behaviors: list[SimulationBehavior] | None = None + input_schema: dict[str, Any] | None = Field(None, alias="inputSchema") + output_schema: dict[str, Any] | None = Field(None, alias="outputSchema") + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + class MockingContext(BaseModel): """Execution context for mocking, holding strategy and inputs.""" strategy: MockingStrategy | None inputs: dict[str, Any] = Field(default_factory=lambda: {}) name: str = Field(default="debug") + # When set, SimulateComponentMocker routes each tool call to the simulate-component API. + components: list[ComponentSimulationConfig] | None = None + workload_id: str | None = None class SimulationConfig(BaseModel): - """Top-level schema for simulation.json / --simulation flag.""" + """Top-level schema for simulation.json / --simulation flag. + + New format (routes to simulate-component API): + { + "enabled": true, + "components": [ + { + "componentId": "my_tool", + "componentType": "tool", + "simulationStrategy": 0, + "simulationInstruction": "Simulate this tool by..." + } + ] + } + + Legacy format (routes to local LLM mocker): + { + "enabled": true, + "toolsToSimulate": [{"name": "my_tool"}], + "instructions": "Simulate these tools by..." + } + """ enabled: bool = True + # New per-component format — when non-empty, routes to simulate-component API. + components: list[ComponentSimulationConfig] = Field(default_factory=list) + # Legacy flat format — used when components is empty; routes to local LLM mocker. tools_to_simulate: list[ToolSimulation] = Field( default_factory=list, alias="toolsToSimulate" ) diff --git a/packages/uipath/tests/cli/eval/mocks/test_simulate_component.py b/packages/uipath/tests/cli/eval/mocks/test_simulate_component.py new file mode 100644 index 000000000..f4131cf48 --- /dev/null +++ b/packages/uipath/tests/cli/eval/mocks/test_simulate_component.py @@ -0,0 +1,442 @@ +"""Tests for SimulateComponentMocker and SimulateComponentService.""" + +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from _pytest.monkeypatch import MonkeyPatch +from pytest_httpx import HTTPXMock + +from uipath.eval.mocks._mock_context import is_tool_simulated +from uipath.eval.mocks._mock_runtime import ( + clear_execution_context, + set_execution_context, +) +from uipath.eval.mocks._mocker import ( + UiPathMockResponseGenerationError, +) +from uipath.eval.mocks._simulate_component_mocker import SimulateComponentMocker +from uipath.eval.mocks._simulate_component_service import ( + SimulateComponentService, + _create_simulate_component_service, +) +from uipath.eval.mocks._types import ( + ComponentSimulationConfig, + MockingContext, + SimulationStrategy, + UnknownMockingStrategy, +) +from uipath.eval.mocks.mockable import mockable + +_mock_span_collector = MagicMock() + +BASE_URL = "https://example.com" +_SIMULATE_PATH = ( + "uipath.eval.mocks._simulate_component_mocker._create_simulate_component_service" +) + + +def _make_context( + component_id: str = "my_tool", + strategy: SimulationStrategy = SimulationStrategy.LLM, + instruction: str = "simulate it", + workload_id: str = "wl-123", +) -> MockingContext: + return MockingContext( + strategy=None, + name="test-run", + inputs={"q": "hello"}, + workload_id=workload_id, + components=[ + ComponentSimulationConfig( + component_id=component_id, + component_type="tool", + simulation_strategy=strategy, + simulation_instruction=instruction, + ) + ], + ) + + +def _make_service_mock(result: dict[str, Any]) -> MagicMock: + svc = MagicMock() + svc.simulate = AsyncMock(return_value=result) + return svc + + +# --------------------------------------------------------------------------- +# is_tool_simulated with components format +# --------------------------------------------------------------------------- + + +class TestIsToolSimulatedWithComponents: + def setup_method(self): + clear_execution_context() + + def teardown_method(self): + clear_execution_context() + + def test_returns_true_for_listed_component(self): + set_execution_context(_make_context("search_tool"), _mock_span_collector, "x") + assert is_tool_simulated("search_tool") is True + + def test_returns_false_for_unlisted_component(self): + set_execution_context(_make_context("search_tool"), _mock_span_collector, "x") + assert is_tool_simulated("other_tool") is False + + def test_underscore_space_normalisation(self): + ctx = MockingContext( + strategy=None, + name="run", + inputs={}, + components=[ + ComponentSimulationConfig( + component_id="web search", + simulation_strategy=SimulationStrategy.LLM, + ) + ], + ) + set_execution_context(ctx, _mock_span_collector, "x") + assert is_tool_simulated("web_search") is True + + def test_returns_false_when_components_list_is_empty(self): + ctx = MockingContext(strategy=None, name="run", inputs={}, components=[]) + set_execution_context(ctx, _mock_span_collector, "x") + # components is set but empty — MockerFactory won't create a mocker (components is not None) + # is_tool_simulated: ctx.components is not None → iterates empty list → False + assert is_tool_simulated("any_tool") is False + + +# --------------------------------------------------------------------------- +# SimulateComponentMocker._find_component +# --------------------------------------------------------------------------- + + +class TestFindComponent: + def test_finds_by_exact_id(self): + mocker = SimulateComponentMocker(_make_context("my_tool")) + assert mocker._find_component("my_tool") is not None + + def test_finds_by_underscore_to_space_normalisation(self): + ctx = MockingContext( + strategy=None, + name="run", + inputs={}, + components=[ + ComponentSimulationConfig( + component_id="web search", + simulation_strategy=SimulationStrategy.LLM, + ) + ], + ) + mocker = SimulateComponentMocker(ctx) + assert mocker._find_component("web_search") is not None + + def test_returns_none_for_unknown_tool(self): + mocker = SimulateComponentMocker(_make_context("my_tool")) + assert mocker._find_component("unknown") is None + + +# --------------------------------------------------------------------------- +# SimulateComponentMocker.response — success path +# --------------------------------------------------------------------------- + + +class TestSimulateComponentMockerResponse: + @pytest.mark.asyncio + async def test_returns_simulated_output_on_status_1(self): + ctx = _make_context("my_tool") + svc_mock = _make_service_mock({"status": 1, "simulatedOutput": "hello"}) + + @mockable() + async def my_tool() -> str: + raise NotImplementedError() + + set_execution_context(ctx, _mock_span_collector, "exec-1") + with patch(_SIMULATE_PATH, return_value=svc_mock): + result = await my_tool() + + assert result == "hello" + clear_execution_context() + + @pytest.mark.asyncio + async def test_raises_generation_error_on_non_1_status(self): + ctx = _make_context("my_tool") + svc_mock = _make_service_mock( + {"status": 2, "error": {"message": "LLM timeout"}} + ) + + @mockable() + async def my_tool() -> str: + raise NotImplementedError() + + set_execution_context(ctx, _mock_span_collector, "exec-2") + with patch(_SIMULATE_PATH, return_value=svc_mock): + with pytest.raises(UiPathMockResponseGenerationError, match="LLM timeout"): + await my_tool() + + clear_execution_context() + + @pytest.mark.asyncio + async def test_raises_generic_error_when_error_message_missing(self): + ctx = _make_context("my_tool") + svc_mock = _make_service_mock({"status": 0, "error": {}}) + + @mockable() + async def my_tool() -> str: + raise NotImplementedError() + + set_execution_context(ctx, _mock_span_collector, "exec-3") + with patch(_SIMULATE_PATH, return_value=svc_mock): + with pytest.raises( + UiPathMockResponseGenerationError, match="Simulation failed" + ): + await my_tool() + + clear_execution_context() + + @pytest.mark.asyncio + async def test_raises_generation_error_when_api_throws(self): + ctx = _make_context("my_tool") + svc_mock = MagicMock() + svc_mock.simulate = AsyncMock(side_effect=RuntimeError("network error")) + + @mockable() + async def my_tool() -> str: + raise NotImplementedError() + + set_execution_context(ctx, _mock_span_collector, "exec-4") + with patch(_SIMULATE_PATH, return_value=svc_mock): + with pytest.raises( + UiPathMockResponseGenerationError, + match="simulate-component API call failed", + ): + await my_tool() + + clear_execution_context() + + @pytest.mark.asyncio + async def test_raises_no_mock_found_for_unconfigured_tool(self): + ctx = _make_context("my_tool") + + @mockable() + async def other_tool() -> str: + raise NotImplementedError() + + set_execution_context(ctx, _mock_span_collector, "exec-5") + # other_tool is not in components → falls through to real function + with pytest.raises(NotImplementedError): + await other_tool() + + clear_execution_context() + + +# --------------------------------------------------------------------------- +# Payload construction +# --------------------------------------------------------------------------- + + +class TestPayloadConstruction: + @pytest.mark.asyncio + async def test_payload_fields_sent_to_service(self): + ctx = _make_context("my_tool", instruction="Do something", workload_id="wl-99") + captured: list[dict[str, Any]] = [] + + async def _capture(payload, **kwargs): + captured.append(payload) + return {"status": 1, "simulatedOutput": "ok"} + + svc_mock = MagicMock() + svc_mock.simulate = _capture + + @mockable() + async def my_tool(x: int) -> str: + raise NotImplementedError() + + set_execution_context(ctx, _mock_span_collector, "exec-6") + with patch(_SIMULATE_PATH, return_value=svc_mock): + await my_tool(x=42) + + assert len(captured) == 1 + p = captured[0] + assert p["workloadId"] == "wl-99" + assert p["componentId"] == "my_tool" + assert p["componentType"] == "tool" + assert p["simulationInstruction"] == "Do something" + assert p["simulationStrategy"] == int(SimulationStrategy.LLM) + assert p["workloadInfo"] == {"name": "test-run", "userInput": {"q": "hello"}} + + clear_execution_context() + + @pytest.mark.asyncio + async def test_sync_mockable_also_works(self): + ctx = _make_context("sync_tool") + svc_mock = _make_service_mock({"status": 1, "simulatedOutput": 42}) + + @mockable() + def sync_tool() -> int: + raise NotImplementedError() + + set_execution_context(ctx, _mock_span_collector, "exec-7") + with patch(_SIMULATE_PATH, return_value=svc_mock): + result = sync_tool() + + assert result == 42 + clear_execution_context() + + @pytest.mark.asyncio + async def test_payload_uses_configured_component_id_not_invoked_name(self): + """componentId in payload must be the configured ID, not the normalised call name.""" + ctx = MockingContext( + strategy=None, + name="run", + inputs={}, + components=[ + ComponentSimulationConfig( + component_id="web search", + simulation_strategy=SimulationStrategy.LLM, + ) + ], + ) + captured: list[dict[str, Any]] = [] + + async def _capture(payload, **kwargs): + captured.append(payload) + return {"status": 1, "simulatedOutput": "ok"} + + svc_mock = MagicMock() + svc_mock.simulate = _capture + + @mockable() + async def web_search() -> str: + raise NotImplementedError() + + set_execution_context(ctx, _mock_span_collector, "exec-8") + with patch(_SIMULATE_PATH, return_value=svc_mock): + await web_search() + + assert captured[0]["componentId"] == "web search" + clear_execution_context() + + +# --------------------------------------------------------------------------- +# _build_execution_history — uncovered branch (no context vars set) +# --------------------------------------------------------------------------- + + +class TestBuildExecutionHistory: + def test_returns_none_when_context_vars_not_set(self): + clear_execution_context() + mocker = SimulateComponentMocker(_make_context()) + assert mocker._build_execution_history() is None + + def test_returns_none_when_spans_empty(self): + from uipath.eval._execution_context import ( + execution_id_context, + span_collector_context, + ) + + span_collector = MagicMock() + span_collector.get_spans = MagicMock(return_value=[]) + span_collector_context.set(span_collector) + execution_id_context.set("exec-id") + + mocker = SimulateComponentMocker(_make_context()) + assert mocker._build_execution_history() is None + + clear_execution_context() + + +# --------------------------------------------------------------------------- +# MockerFactory — unknown strategy raises ValueError +# --------------------------------------------------------------------------- + + +class TestMockerFactory: + def test_raises_for_unknown_strategy(self): + from uipath.eval.mocks._mocker_factory import MockerFactory + + ctx = MockingContext( + strategy=UnknownMockingStrategy(type="future_strategy"), + name="test", + inputs={}, + components=None, + ) + with pytest.raises(ValueError, match="Unknown mocking strategy"): + MockerFactory.create(ctx) + + def test_raises_for_none_strategy_and_no_components(self): + from uipath.eval.mocks._mocker_factory import MockerFactory + + ctx = MockingContext(strategy=None, name="test", inputs={}, components=None) + with pytest.raises(ValueError, match="Unknown mocking strategy"): + MockerFactory.create(ctx) + + +# --------------------------------------------------------------------------- +# is_tool_simulated — unknown strategy falls through to False +# --------------------------------------------------------------------------- + + +class TestIsToolSimulatedUnknownStrategy: + def setup_method(self): + clear_execution_context() + + def teardown_method(self): + clear_execution_context() + + def test_returns_false_for_unknown_strategy(self): + + ctx = MockingContext( + strategy=UnknownMockingStrategy(type="future_strategy"), + name="test", + inputs={}, + components=None, + ) + set_execution_context(ctx, _mock_span_collector, "x") + assert is_tool_simulated("any_tool") is False + + +# --------------------------------------------------------------------------- +# SimulateComponentService — actual HTTP call +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +async def test_simulate_component_service_http_call( + httpx_mock: HTTPXMock, monkeypatch: MonkeyPatch +): + monkeypatch.setenv("UIPATH_URL", "https://example.com/myorg/mytenant") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "token") + + httpx_mock.add_response( + url="https://example.com/myorg/mytenant/agentsruntime_/api/execution/simulations/simulate-component", + method="POST", + json={"status": 1, "simulatedOutput": "result"}, + ) + + service = _create_simulate_component_service() + assert isinstance(service, SimulateComponentService) + + result = await service.simulate({"componentId": "my_tool"}) + assert result == {"status": 1, "simulatedOutput": "result"} + + +@pytest.mark.asyncio +@pytest.mark.httpx_mock(assert_all_responses_were_requested=False) +async def test_simulate_component_service_no_headers( + httpx_mock: HTTPXMock, monkeypatch: MonkeyPatch +): + monkeypatch.setenv("UIPATH_URL", "https://example.com/myorg/mytenant") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "token") + + httpx_mock.add_response( + url="https://example.com/myorg/mytenant/agentsruntime_/api/execution/simulations/simulate-component", + method="POST", + json={"status": 0, "error": {"message": "boom"}}, + ) + + service = _create_simulate_component_service() + result = await service.simulate({"componentId": "my_tool"}) + assert result["status"] == 0 diff --git a/packages/uipath/tests/cli/test_debug_simulation.py b/packages/uipath/tests/cli/test_debug_simulation.py index d9266327b..9e66a1a24 100644 --- a/packages/uipath/tests/cli/test_debug_simulation.py +++ b/packages/uipath/tests/cli/test_debug_simulation.py @@ -82,11 +82,12 @@ def test_loads_valid_simulation_config( assert result is not None assert isinstance(result, MockingContext) assert result.name == "debug-simulation" - assert result.strategy is not None + # Legacy format routes to local LLM mocker via strategy + assert result.components is None or len(result.components) == 0 assert isinstance(result.strategy, LLMMockingStrategy) - assert result.strategy.prompt == valid_simulation_config["instructions"] assert len(result.strategy.tools_to_simulate) == 3 assert result.strategy.tools_to_simulate[0].name == "Web Reader" + assert result.strategy.prompt == valid_simulation_config["instructions"] def test_returns_none_when_disabled( self, temp_dir: str, disabled_simulation_config: dict[str, Any] @@ -422,6 +423,34 @@ def test_enabled_defaults_to_true_when_missing(self, temp_dir: str): # Should load successfully since enabled defaults to true assert result is not None + def test_new_format_loads_components(self, temp_dir: str): + """Test that new per-component format routes to API-based mocker (components set).""" + config = { + "enabled": True, + "components": [ + { + "componentId": "my_tool", + "componentType": "tool", + "simulationStrategy": 0, + "simulationInstruction": "Simulate this tool", + } + ], + } + simulation_path = Path(temp_dir) / "simulation.json" + with open(simulation_path, "w", encoding="utf-8") as f: + json.dump(config, f) + + with patch(f"{MOCK_RUNTIME_PATCH_PATH}.Path.cwd", return_value=Path(temp_dir)): + result = load_simulation_config() + + assert result is not None + assert isinstance(result, MockingContext) + # New format: components set, strategy is None + assert result.components is not None + assert len(result.components) == 1 + assert result.components[0].component_id == "my_tool" + assert result.strategy is None + def test_handles_tool_name_normalization(self, temp_dir: str): """Test that tool names with underscores work correctly.""" config = { diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index a805e5cdc..e8b8f6bad 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.7" +version = "2.11.8" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 6ac022a9b02438f8cea81054ca19bee30612b313 Mon Sep 17 00:00:00 2001 From: Radu Mihai Gheorghe Date: Tue, 23 Jun 2026 12:32:52 +0300 Subject: [PATCH 115/121] feat: add refreshSchemaBeforeCall to cached MCP tools config (#1730) --- packages/uipath/pyproject.toml | 2 +- .../uipath/src/uipath/agent/models/agent.py | 11 ++++- .../uipath/tests/agent/models/test_agent.py | 48 +++++++++++++++++++ packages/uipath/uv.lock | 2 +- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 4038e1313..827befcfb 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.8" +version = "2.11.9" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index a14066969..916417c94 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -483,9 +483,18 @@ class DynamicToolsMode(str, CaseInsensitiveEnum): class CachedToolsConfig(BaseCfg): - """Cached tools configuration: use the tools saved in the agent definition snapshot.""" + """Cached tools configuration: use the tools saved in the agent definition snapshot. + + When ``refresh_schema_before_call`` is true, the live tool schema is fetched + from the MCP server immediately before a tool is invoked. The agent still uses + the cached schema to decide which tool to call; the fresh schema is applied only + at invocation time. + """ type: Literal["cached"] = Field(default="cached", frozen=True) + refresh_schema_before_call: bool = Field( + default=True, alias="refreshSchemaBeforeCall" + ) class DynamicToolsConfig(BaseCfg): diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index ab0e6a557..202962c64 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -50,6 +50,7 @@ AssetRecipient, BatchTransformFileExtension, BatchTransformWebSearchGrounding, + CachedToolsConfig, CitationMode, CustomAssigneesRecipient, DeepRagFileExtension, @@ -2038,6 +2039,53 @@ def test_mcp_resource_with_output_schema(self): assert tool2.output_schema is not None assert "content" in tool2.output_schema["properties"] + def test_cached_tools_config_refresh_schema_default(self): + """CachedToolsConfig defaults refresh_schema_before_call to True.""" + + config = CachedToolsConfig() + assert config.type == "cached" + assert config.refresh_schema_before_call is True + + def test_cached_tools_config_refresh_schema_alias_roundtrip(self): + """refresh_schema_before_call parses from and serializes to refreshSchemaBeforeCall.""" + + config = TypeAdapter(CachedToolsConfig).validate_python( + {"type": "cached", "refreshSchemaBeforeCall": False} + ) + assert config.refresh_schema_before_call is False + assert config.model_dump(by_alias=True)["refreshSchemaBeforeCall"] is False + + def test_mcp_resource_with_cached_tools_configuration(self): + """AgentMcpResourceConfig parses a cached toolsConfiguration with the refresh flag.""" + + json_data = { + "$resourceType": "mcp", + "folderPath": "solution_folder", + "slug": "tavily-mcp", + "name": "tavily", + "description": "Tavily search tools", + "isEnabled": True, + "availableTools": [ + { + "name": "tavily-search", + "description": "Search the web", + "inputSchema": {"type": "object", "properties": {}}, + } + ], + "toolsConfiguration": { + "discoveryMode": { + "type": "cached", + "refreshSchemaBeforeCall": False, + } + }, + } + + mcp_resource = TypeAdapter(AgentMcpResourceConfig).validate_python(json_data) + assert mcp_resource.tools_configuration is not None + discovery_mode = mcp_resource.tools_configuration.discovery_mode + assert isinstance(discovery_mode, CachedToolsConfig) + assert discovery_mode.refresh_schema_before_call is False + @pytest.mark.parametrize( "recipient_type_int,value,expected_type", [ diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e8b8f6bad..15873d997 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.8" +version = "2.11.9" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 968e4ae4afd7cf9b5085e7983415818c06bf1093 Mon Sep 17 00:00:00 2001 From: Viswanath Lekshmanan <84002867+viswa-uipath@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:45:55 +0530 Subject: [PATCH 116/121] feat(platform): add GovernanceService for agenticgovernance_ ingress (#1738) Co-authored-by: Claude Opus 4.7 (1M context) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/governance/__init__.py | 15 + .../src/uipath/core/governance/providers.py | 153 +++++ .../tests/governance/test_providers.py | 149 +++++ packages/uipath-core/uv.lock | 2 +- packages/uipath-platform/pyproject.toml | 4 +- .../src/uipath/platform/_uipath.py | 5 + .../src/uipath/platform/common/__init__.py | 3 +- .../uipath/platform/common/_base_service.py | 54 ++ .../uipath/platform/governance/__init__.py | 20 + .../governance/_governance_provider.py | 81 +++ .../governance/_governance_service.py | 356 +++++++++++ .../uipath/platform/governance/compensate.py | 10 + .../src/uipath/platform/governance/policy.py | 10 + .../services/test_governance_provider.py | 166 +++++ .../tests/services/test_governance_service.py | 594 ++++++++++++++++++ packages/uipath-platform/uv.lock | 4 +- packages/uipath/pyproject.toml | 4 +- packages/uipath/uv.lock | 6 +- 19 files changed, 1626 insertions(+), 12 deletions(-) create mode 100644 packages/uipath-core/src/uipath/core/governance/providers.py create mode 100644 packages/uipath-core/tests/governance/test_providers.py create mode 100644 packages/uipath-platform/src/uipath/platform/governance/__init__.py create mode 100644 packages/uipath-platform/src/uipath/platform/governance/_governance_provider.py create mode 100644 packages/uipath-platform/src/uipath/platform/governance/_governance_service.py create mode 100644 packages/uipath-platform/src/uipath/platform/governance/compensate.py create mode 100644 packages/uipath-platform/src/uipath/platform/governance/policy.py create mode 100644 packages/uipath-platform/tests/services/test_governance_provider.py create mode 100644 packages/uipath-platform/tests/services/test_governance_service.py diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index ef97c0d24..56e0c4d4f 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.20" +version = "0.5.21" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/governance/__init__.py b/packages/uipath-core/src/uipath/core/governance/__init__.py index 3a06b82df..e3dcab741 100644 --- a/packages/uipath-core/src/uipath/core/governance/__init__.py +++ b/packages/uipath-core/src/uipath/core/governance/__init__.py @@ -19,6 +19,14 @@ Severity, ) from .models import Action, AuditRecord, EnforcementMode, LifecycleHook, RuleEvaluation +from .providers import ( + FiredRule, + GovernanceCompensationProvider, + GovernancePolicyProvider, + GovernRequest, + PolicyContext, + PolicyResponse, +) __all__ = [ # Output models (cross adapter boundary) @@ -35,4 +43,11 @@ "GovernanceConfigError", "GovernanceViolation", "Severity", + # Provider protocols + wire models + "FiredRule", + "GovernanceCompensationProvider", + "GovernancePolicyProvider", + "GovernRequest", + "PolicyContext", + "PolicyResponse", ] diff --git a/packages/uipath-core/src/uipath/core/governance/providers.py b/packages/uipath-core/src/uipath/core/governance/providers.py new file mode 100644 index 000000000..29f435edb --- /dev/null +++ b/packages/uipath-core/src/uipath/core/governance/providers.py @@ -0,0 +1,153 @@ +"""Provider protocols for governance backend interactions. + +The runtime needs two backend interactions to function: + +- Fetching the policy pack at startup. +- Firing the compensating ``/runtime/govern`` POST when a + ``guardrail_fallback`` rule matches so the server can run the disabled + centralised guardrail and write the per-rule LLMOps audit records. + +Both have wire formats owned by the ``agenticgovernance_`` ingress. +Defining the contracts here — alongside :class:`EvaluatorProtocol` — +lets runtime consumers depend on stable protocols and receive a +concrete provider via constructor injection. Concrete providers live +outside this package; ``uipath-core`` does not import them. +""" + +from __future__ import annotations + +from typing import Any, Protocol, runtime_checkable + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from .models import EnforcementMode + +# ---------------------------------------------------------------------- +# Wire-format models +# ---------------------------------------------------------------------- + + +class PolicyContext(BaseModel): + """Caller-supplied selectors for the policy fetch. + + Wrapping the selectors in a model keeps the protocol surface stable + when the server grows new selector dimensions — adding a field here + doesn't change :meth:`GovernancePolicyProvider.get_policy`. + + Today carries only :attr:`is_conversational`; future selectors land + here. + """ + + model_config = ConfigDict(extra="ignore") + + is_conversational: bool | None = None + + +class PolicyResponse(BaseModel): + """Parsed governance backend response. + + Wire envelope:: + + { + "mode": "audit" | "enforce" | "disabled", + "policies": "" + } + + Attributes: + mode: Platform-controlled enforcement mode for the tenant. May + be ``None`` when the backend omits it. A wire value the SDK + doesn't know about parses as ``None`` rather than raising, + so a server-side mode addition can't break agent startup. + policies: Policy pack YAML the caller compiles into its policy + index. May be an empty string when no rules are configured. + """ + + model_config = ConfigDict(extra="ignore") + + mode: EnforcementMode | None = Field(default=None) + policies: str = Field(default="") + + @field_validator("mode", mode="before") + @classmethod + def _coerce_mode(cls, value: object) -> EnforcementMode | None: + if value is None or isinstance(value, EnforcementMode): + return value + try: + return EnforcementMode(value) + except ValueError: + return None + + +class FiredRule(BaseModel): + """Per-rule metadata carried in the ``/runtime/govern`` payload. + + One entry per matching ``guardrail_fallback`` condition. The server + writes one LLMOps trace record per entry, so callers must include + every fired rule even when multiple share the same ``validator``. + """ + + model_config = ConfigDict(populate_by_name=True) + + rule_id: str = Field(alias="ruleId") + rule_name: str = Field(alias="ruleName") + pack_name: str = Field(alias="packName") + validator: str + + +class GovernRequest(BaseModel): + """Request body for the ``/runtime/govern`` compensating governance POST. + + Field aliases match the on-the-wire JSON keys. ``src_timestamp`` is + snake_case on the wire (intentional — preserved verbatim); every + other key is camelCase. + + Job-context fields (``folder_key`` / ``job_key`` / ``process_key`` / + ``reference_id`` / ``agent_version``) are optional; callers omit + them by leaving them ``None``. How unset fields are resolved (e.g. + auto-filled from environment) is the concrete provider's concern, + not part of this wire contract. + """ + + model_config = ConfigDict(populate_by_name=True) + + validators: list[str] = Field(alias="type") + rules: list[FiredRule] + data: dict[str, Any] + hook: str + trace_id: str = Field(alias="traceId") + src_timestamp: str # wire key is intentionally snake_case + agent_name: str = Field(alias="agentName") + runtime_id: str = Field(alias="runtimeId") + + folder_key: str | None = Field(default=None, alias="folderKey") + job_key: str | None = Field(default=None, alias="jobKey") + process_key: str | None = Field(default=None, alias="processKey") + reference_id: str | None = Field(default=None, alias="referenceId") + agent_version: str | None = Field(default=None, alias="agentVersion") + + +# ---------------------------------------------------------------------- +# Provider protocols +# ---------------------------------------------------------------------- + + +@runtime_checkable +class GovernancePolicyProvider(Protocol): + """Contract for fetching the governance policy pack. + + Any object exposing a ``get_policy(context) -> PolicyResponse`` + method satisfies this protocol. + """ + + def get_policy(self, context: PolicyContext) -> PolicyResponse: + """Fetch the policy pack for the active org/tenant.""" + ... + + +@runtime_checkable +class GovernanceCompensationProvider(Protocol): + """Contract for firing the compensating ``/runtime/govern`` POST.""" + + def compensate(self, request: GovernRequest) -> None: + """Fire the compensating governance POST. Fire-and-forget.""" + ... diff --git a/packages/uipath-core/tests/governance/test_providers.py b/packages/uipath-core/tests/governance/test_providers.py new file mode 100644 index 000000000..083b62663 --- /dev/null +++ b/packages/uipath-core/tests/governance/test_providers.py @@ -0,0 +1,149 @@ +"""Tests for the governance provider protocols + wire-format models.""" + +from __future__ import annotations + +import pytest + +from uipath.core.governance import ( + EnforcementMode, + FiredRule, + GovernanceCompensationProvider, + GovernancePolicyProvider, + GovernRequest, + PolicyContext, + PolicyResponse, +) + + +class _FakePolicyProvider: + def __init__(self) -> None: + self.calls: list[PolicyContext] = [] + + def get_policy(self, context: PolicyContext) -> PolicyResponse: + self.calls.append(context) + return PolicyResponse(mode=EnforcementMode.ENFORCE, policies="rules: []") + + +class _FakeCompensationProvider: + def __init__(self) -> None: + self.calls: list[GovernRequest] = [] + + def compensate(self, request: GovernRequest) -> None: + self.calls.append(request) + + +def _make_request() -> GovernRequest: + return GovernRequest( + validators=["pii_detection"], + rules=[ + FiredRule( + rule_id="ASI-01", + rule_name="Block PII in flight", + pack_name="agent-safety", + validator="pii_detection", + ) + ], + data={"prompt": "hi"}, + hook="before_model", + trace_id="0123456789abcdef0123456789abcdef", + src_timestamp="2026-06-22T10:00:00Z", + agent_name="my-agent", + runtime_id="runtime-1", + ) + + +class TestPolicyContext: + def test_defaults(self) -> None: + ctx = PolicyContext() + assert ctx.is_conversational is None + + def test_ignores_unknown_fields(self) -> None: + ctx = PolicyContext.model_validate( + {"is_conversational": True, "future_selector": "x"} + ) + assert ctx.is_conversational is True + + +class TestPolicyResponse: + def test_defaults(self) -> None: + response = PolicyResponse() + assert response.mode is None + assert response.policies == "" + + @pytest.mark.parametrize( + ("wire_value", "expected"), + [ + ("audit", EnforcementMode.AUDIT), + ("enforce", EnforcementMode.ENFORCE), + ("disabled", EnforcementMode.DISABLED), + ], + ) + def test_parses_known_modes( + self, wire_value: str, expected: EnforcementMode + ) -> None: + response = PolicyResponse.model_validate({"mode": wire_value}) + assert response.mode is expected + + def test_unknown_mode_falls_back_to_none(self) -> None: + # Forward-compat: a server-added mode the SDK doesn't know about + # must not break agent startup. Parses as None so the runtime + # falls back to its safe default rather than raising. + response = PolicyResponse.model_validate({"mode": "ludicrous"}) + assert response.mode is None + + +class TestGovernRequest: + def test_serializes_wire_aliases(self) -> None: + payload = _make_request().model_dump(by_alias=True, exclude_none=True) + assert payload["type"] == ["pii_detection"] + assert payload["traceId"] == "0123456789abcdef0123456789abcdef" + assert payload["agentName"] == "my-agent" + assert payload["runtimeId"] == "runtime-1" + # src_timestamp is intentionally snake_case on the wire. + assert payload["src_timestamp"] == "2026-06-22T10:00:00Z" + # Optional job-context fields left None → excluded. + for absent in ( + "folderKey", + "jobKey", + "processKey", + "referenceId", + "agentVersion", + ): + assert absent not in payload + + +class TestProtocolConformance: + """`runtime_checkable` Protocols should accept structurally-matching objects.""" + + def test_fake_policy_provider_satisfies_protocol(self) -> None: + provider = _FakePolicyProvider() + assert isinstance(provider, GovernancePolicyProvider) + + def test_fake_compensation_provider_satisfies_protocol(self) -> None: + provider = _FakeCompensationProvider() + assert isinstance(provider, GovernanceCompensationProvider) + + def test_object_without_methods_rejected(self) -> None: + class _NotAProvider: + pass + + assert not isinstance(_NotAProvider(), GovernancePolicyProvider) + assert not isinstance(_NotAProvider(), GovernanceCompensationProvider) + + +class TestEndToEndDispatch: + """Caller passes a provider directly to the consumer (no global registry).""" + + def test_policy_round_trip(self) -> None: + provider = _FakePolicyProvider() + response = provider.get_policy(PolicyContext(is_conversational=True)) + + assert response.mode is EnforcementMode.ENFORCE + assert provider.calls == [PolicyContext(is_conversational=True)] + + def test_compensation_round_trip(self) -> None: + provider = _FakeCompensationProvider() + request = _make_request() + provider.compensate(request) + + assert provider.calls == [request] diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index ee08896ea..005bf9018 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.20" +version = "0.5.21" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 37d6fcdaa..b8effe0f0 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.71" +version = "0.1.73" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" @@ -8,7 +8,7 @@ dependencies = [ "httpx>=0.28.1", "tenacity>=9.0.0", "truststore>=0.10.1", - "uipath-core>=0.5.20, <0.6.0", + "uipath-core>=0.5.21, <0.6.0", "pydantic-function-models>=0.1.11", "sqlparse>=0.5.5", ] diff --git a/packages/uipath-platform/src/uipath/platform/_uipath.py b/packages/uipath-platform/src/uipath/platform/_uipath.py index 8e0e23867..0083388db 100644 --- a/packages/uipath-platform/src/uipath/platform/_uipath.py +++ b/packages/uipath-platform/src/uipath/platform/_uipath.py @@ -22,6 +22,7 @@ from .documents import DocumentsService from .entities import EntitiesService from .errors import BaseUrlMissingError, SecretMissingError +from .governance import GovernanceService from .guardrails import GuardrailsService from .memory import MemoryService from .orchestrator import ( @@ -169,6 +170,10 @@ def mcp(self) -> McpService: def guardrails(self) -> GuardrailsService: return GuardrailsService(self._config, self._execution_context) + @cached_property + def governance(self) -> GovernanceService: + return GovernanceService(self._config, self._execution_context) + @property def agenthub(self) -> AgentHubService: return AgentHubService(self._config, self._execution_context, self.folders) diff --git a/packages/uipath-platform/src/uipath/platform/common/__init__.py b/packages/uipath-platform/src/uipath/platform/common/__init__.py index cefd92075..555d6901d 100644 --- a/packages/uipath-platform/src/uipath/platform/common/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/common/__init__.py @@ -4,7 +4,7 @@ """ from ._api_client import ApiClient -from ._base_service import BaseService +from ._base_service import BaseService, resolve_trace_id from ._bindings import ( ConnectionResourceOverwrite, EntityResourceOverwrite, @@ -112,6 +112,7 @@ "_SpanUtils", "resolve_service_url", "inject_routing_headers", + "resolve_trace_id", ] from .validation import validate_pagination_params diff --git a/packages/uipath-platform/src/uipath/platform/common/_base_service.py b/packages/uipath-platform/src/uipath/platform/common/_base_service.py index 21d8fa404..95035b852 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_base_service.py +++ b/packages/uipath-platform/src/uipath/platform/common/_base_service.py @@ -66,6 +66,60 @@ def _get_caller_component() -> str: _TRACE_PARENT_HEADER = "x-uipath-traceparent-id" +def resolve_trace_id(fallback: str | None = None) -> str | None: + """Resolve the current UiPath trace id as a 32-char hex string. + + Same lookup chain :func:`_inject_trace_context` uses to compose the + ``x-uipath-traceparent-id`` header, exposed as a public helper so + callers can capture the value when they need it in a request body + (e.g. governance compensation) or before hopping to a background + thread that won't inherit the OpenTelemetry context. + + Resolution order (first hit wins): + + 1. :attr:`UiPathConfig.trace_id` (``UIPATH_TRACE_ID`` env var), + normalized via :meth:`_SpanUtils.normalize_trace_id`. This is the + canonical agent trace id the LLMOps exporter binds spans to. + 2. The LLMOps external span trace id, when a provider is registered + via :meth:`UiPathSpanUtils.register_current_span_provider`. + 3. The current OpenTelemetry span trace id. + 4. The caller-supplied ``fallback``. + + Args: + fallback: Returned when nothing above resolves. + + Returns: + Lower-case 32-char hex trace id, or ``fallback`` (which may be + ``None``) when no source yields a usable value. + + Thread Safety: + Steps 2 and 3 read OpenTelemetry's thread-local context. Call this + on the thread that owns the live span (e.g. the agent's hook + thread) and capture the result before submitting work to a + background pool — worker threads do not inherit the context. + """ + from uipath.core.tracing.span_utils import UiPathSpanUtils + + from ._config import UiPathConfig + from ._span_utils import _SpanUtils + + config_trace_id = UiPathConfig.trace_id + if config_trace_id: + try: + return _SpanUtils.normalize_trace_id(config_trace_id) + except ValueError: + # Malformed UIPATH_TRACE_ID — fall through to OTel context. + pass + + llmops_span = UiPathSpanUtils.get_external_current_span() + span = llmops_span or trace.get_current_span() + ctx = span.get_span_context() + if ctx.trace_id: + return format_trace_id(ctx.trace_id) + + return fallback + + def _inject_trace_context(headers: dict[str, str]) -> None: """Inject UiPath trace context header. diff --git a/packages/uipath-platform/src/uipath/platform/governance/__init__.py b/packages/uipath-platform/src/uipath/platform/governance/__init__.py new file mode 100644 index 000000000..e1f587606 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/governance/__init__.py @@ -0,0 +1,20 @@ +"""Governance services for the UiPath Platform. + +Exposes the agenticgovernance_ ingress: tenant-controlled policy packs +served centrally so policy decisions can change without redeploying +agents. +""" + +from ._governance_provider import UiPathPlatformGovernanceProvider +from ._governance_service import GovernanceService +from .compensate import FiredRule, GovernRequest +from .policy import PolicyContext, PolicyResponse + +__all__ = [ + "FiredRule", + "GovernRequest", + "GovernanceService", + "PolicyContext", + "PolicyResponse", + "UiPathPlatformGovernanceProvider", +] diff --git a/packages/uipath-platform/src/uipath/platform/governance/_governance_provider.py b/packages/uipath-platform/src/uipath/platform/governance/_governance_provider.py new file mode 100644 index 000000000..23f1464bb --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/governance/_governance_provider.py @@ -0,0 +1,81 @@ +"""Platform-backed implementation of the core governance provider protocols. + +Thin adapter around :class:`GovernanceService` that exposes only the +methods required by +:class:`uipath.core.governance.GovernancePolicyProvider` and +:class:`uipath.core.governance.GovernanceCompensationProvider`. + +Wrap an existing :class:`GovernanceService` (e.g. +``UiPathPlatformGovernanceProvider(service=UiPath().governance)``) or +pass ``config``/``execution_context`` to construct one inline. +""" + +from __future__ import annotations + +from uipath.core.governance import GovernRequest, PolicyContext, PolicyResponse + +from ..common._config import UiPathApiConfig +from ..common._execution_context import UiPathExecutionContext +from ._governance_service import GovernanceService + + +class UiPathPlatformGovernanceProvider: + """Platform-backed governance provider. + + Implements both + :class:`uipath.core.governance.GovernancePolicyProvider` and + :class:`uipath.core.governance.GovernanceCompensationProvider` by + delegating to :class:`GovernanceService`. + + Args: + service: Existing :class:`GovernanceService` to delegate to. + Useful for tests and for sharing an SDK service across + consumers. When omitted, a fresh service is built from the + ``config`` and ``execution_context`` kwargs. + config: Required when ``service`` is not supplied. + execution_context: Required when ``service`` is not supplied. + """ + + def __init__( + self, + service: GovernanceService | None = None, + *, + config: UiPathApiConfig | None = None, + execution_context: UiPathExecutionContext | None = None, + ) -> None: + if service is None: + if config is None or execution_context is None: + raise ValueError( + "UiPathPlatformGovernanceProvider requires either a " + "GovernanceService instance or both config and " + "execution_context." + ) + service = GovernanceService( + config=config, execution_context=execution_context + ) + self._service = service + + @property + def service(self) -> GovernanceService: + """The underlying :class:`GovernanceService` instance.""" + return self._service + + # ── GovernancePolicyProvider ───────────────────────────────────── + + def get_policy(self, context: PolicyContext) -> PolicyResponse: + """Fetch the policy pack — delegates to ``GovernanceService``.""" + return self._service.get_policy(context) + + async def get_policy_async(self, context: PolicyContext) -> PolicyResponse: + """Async variant of :meth:`get_policy`.""" + return await self._service.get_policy_async(context) + + # ── GovernanceCompensationProvider ─────────────────────────────── + + def compensate(self, request: GovernRequest) -> None: + """Fire the compensating ``/runtime/govern`` POST.""" + self._service._compensate(request) + + async def compensate_async(self, request: GovernRequest) -> None: + """Async variant of :meth:`compensate`.""" + await self._service._compensate_async(request) diff --git a/packages/uipath-platform/src/uipath/platform/governance/_governance_service.py b/packages/uipath-platform/src/uipath/platform/governance/_governance_service.py new file mode 100644 index 000000000..5ceabf479 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/governance/_governance_service.py @@ -0,0 +1,356 @@ +"""Service for the ``agenticgovernance_`` ingress. + +Wraps the two governance backend endpoints UiPath exposes: + +- ``GET /{org}/agenticgovernance_/api/v1/runtime/policy`` — fetch the + tenant-managed policy pack (see :meth:`GovernanceService.retrieve_policy`). +- ``POST /{org}/agenticgovernance_/api/v1/runtime/govern`` — compensating + governance call fired when a ``guardrail_fallback`` rule matches + (see :meth:`GovernanceService.compensate`). + +Org/tenant scoping is read from :class:`UiPathConfig`; auth, retries, +trace context, and error enrichment come from :class:`BaseService`. +""" + +from typing import Any, Optional + +from uipath.core import traced +from uipath.core.governance import ( + FiredRule, + GovernRequest, + PolicyContext, + PolicyResponse, +) + +from ..common._base_service import BaseService +from ..common._config import UiPathConfig +from ..common._service_url_overrides import ( + inject_routing_headers, + resolve_service_url, +) +from ..common.constants import HEADER_INTERNAL_TENANT_ID + +# The agenticgovernance_ ingress lives at a separate org-scoped path that +# uses the organization UUID (not the slug exposed by ``UIPATH_URL``). +GOVERNANCE_SERVICE_PREFIX = "agenticgovernance_" +POLICY_API_PATH = "api/v1/runtime/policy" +GOVERN_API_PATH = "api/v1/runtime/govern" +AGENT_TYPE_PARAM = "agentType" + + +class GovernanceService(BaseService): + """Service for the agenticgovernance_ ingress. + + Exposes two endpoints: + + - :meth:`retrieve_policy` — GET the tenant-managed policy pack. + - :meth:`compensate` — POST a compensating ``/runtime/govern`` call + so the server can run a disabled centralized guardrail and write + the per-rule LLMOps audit records itself. + + Org and tenant scoping come from :attr:`UiPathConfig.organization_id` + and :attr:`UiPathConfig.tenant_id`; the tenant travels in the + ``x-uipath-internal-tenantid`` header (the URL is org-scoped only). + + !!! info "Version Availability" + This service is available starting from **uipath** version **2.2.13**. + """ + + # ── Policy fetch ───────────────────────────────────────────────── + + @traced(name="governance_retrieve_policy", run_type="uipath") + def retrieve_policy( + self, + *, + is_conversational: Optional[bool] = None, + ) -> PolicyResponse: + """Fetch the governance policy pack for the active org/tenant. + + Args: + is_conversational: When the hosted agent's type is known, + selects the conversational (``True``) or autonomous + (``False``) policy view. ``None`` (default) omits the + ``agentType`` query param so the server applies its + default. + + Returns: + PolicyResponse: ``mode`` and the YAML ``policies`` string. + + Raises: + ValueError: If ``UiPathConfig.organization_id`` or + ``UiPathConfig.tenant_id`` is not set. + EnrichedException: If the backend returns a non-2xx response. + + Examples: + ```python + from uipath.platform import UiPath + + client = UiPath() + response = client.governance.retrieve_policy() + print(response.mode, len(response.policies)) + ``` + """ + url, headers = self._build_org_scoped_request(POLICY_API_PATH) + params = self._policy_params(is_conversational) + response = self.request("GET", url=url, params=params, headers=headers) + return PolicyResponse.model_validate(response.json()) + + @traced(name="governance_retrieve_policy", run_type="uipath") + async def retrieve_policy_async( + self, + *, + is_conversational: Optional[bool] = None, + ) -> PolicyResponse: + """Asynchronously fetch the governance policy pack. + + See :meth:`retrieve_policy` for parameter and return semantics. + """ + url, headers = self._build_org_scoped_request(POLICY_API_PATH) + params = self._policy_params(is_conversational) + response = await self.request_async( + "GET", url=url, params=params, headers=headers + ) + return PolicyResponse.model_validate(response.json()) + + # ── Policy provider adapter (GovernancePolicyProvider protocol) ─ + + def get_policy(self, context: PolicyContext) -> PolicyResponse: + """Fetch the policy pack — :class:`GovernancePolicyProvider` adapter. + + Thin wrapper over :meth:`retrieve_policy` that accepts the + context model the core protocol uses. Lets the runtime consume + governance through :class:`uipath.core.governance.GovernancePolicyProvider` + without importing this module. + """ + return self.retrieve_policy(is_conversational=context.is_conversational) + + async def get_policy_async(self, context: PolicyContext) -> PolicyResponse: + """Async variant of :meth:`get_policy`.""" + return await self.retrieve_policy_async( + is_conversational=context.is_conversational + ) + + # ── Compensating governance call ───────────────────────────────── + + def compensate( + self, + *, + hook: str, + validators: list[str], + rules: list[FiredRule], + data: dict[str, Any], + trace_id: str, + src_timestamp: str, + agent_name: str, + runtime_id: str, + folder_key: str | None = None, + job_key: str | None = None, + process_key: str | None = None, + reference_id: str | None = None, + agent_version: str | None = None, + ) -> None: + """POST a compensating ``/runtime/govern`` call. + + Fired when a ``guardrail_fallback`` rule matches: the centralized + guardrail is disabled, so the server is asked to run the + guardrail check server-side and write the per-rule LLMOps audit + records bound to ``trace_id``. The agent does not inspect the + response body. + + Job-context fields (``folder_key`` / ``job_key`` / + ``process_key`` / ``reference_id`` / ``agent_version``) are + auto-populated from :class:`UiPathConfig` when omitted. + Caller-supplied values — including the empty string — take + precedence. + + Args: + hook: Identifier of the agent hook that fired the rule + (e.g. ``"before_model"``). + validators: Validator names attached to the fired rules. + rules: Each rule that fired — one LLMOps audit record is + written per entry. + data: Hook payload the server replays through the + centralized guardrail. + trace_id: Canonical 32-char hex trace id. Capture via + :func:`resolve_trace_id` on the hook thread before + hopping to a background pool. + src_timestamp: ISO-8601 timestamp on the source side. + agent_name: Agent identifier as known to the platform. + runtime_id: Runtime instance identifier. + folder_key: Override the env-backed folder key. + job_key: Override the env-backed job key. + process_key: Override the env-backed process key. + reference_id: Override the env-backed agent id. + agent_version: Override the env-backed agent version. + + Raises: + ValueError: If ``UiPathConfig.organization_id`` or + ``UiPathConfig.tenant_id`` is not set. + EnrichedException: If the backend returns a non-2xx response. + + Threading: + ``trace_id`` must be the agent's canonical trace id, and + OpenTelemetry context is thread-local; capture it on the + hook thread (via :func:`resolve_trace_id`) before hopping + to a background pool. + """ + self._compensate( + GovernRequest( + hook=hook, + validators=validators, + rules=rules, + data=data, + trace_id=trace_id, + src_timestamp=src_timestamp, + agent_name=agent_name, + runtime_id=runtime_id, + folder_key=folder_key, + job_key=job_key, + process_key=process_key, + reference_id=reference_id, + agent_version=agent_version, + ) + ) + + async def compensate_async( + self, + *, + hook: str, + validators: list[str], + rules: list[FiredRule], + data: dict[str, Any], + trace_id: str, + src_timestamp: str, + agent_name: str, + runtime_id: str, + folder_key: str | None = None, + job_key: str | None = None, + process_key: str | None = None, + reference_id: str | None = None, + agent_version: str | None = None, + ) -> None: + """Asynchronously POST a compensating ``/runtime/govern`` call. + + See :meth:`compensate` for parameter semantics. + """ + await self._compensate_async( + GovernRequest( + hook=hook, + validators=validators, + rules=rules, + data=data, + trace_id=trace_id, + src_timestamp=src_timestamp, + agent_name=agent_name, + runtime_id=runtime_id, + folder_key=folder_key, + job_key=job_key, + process_key=process_key, + reference_id=reference_id, + agent_version=agent_version, + ) + ) + + # ── Internal worker for GovernRequest-shaped callers ───────────── + + @traced(name="governance_compensate", run_type="uipath") + def _compensate(self, request: GovernRequest) -> None: + """Fire a compensation call from a pre-built :class:`GovernRequest`. + + Internal helper used by the provider adapter + (:class:`uipath.platform.governance.UiPathPlatformGovernanceProvider`) + to satisfy :class:`uipath.core.governance.GovernanceCompensationProvider` + without unpacking the request. The public ergonomic counterpart + is :meth:`compensate`. + """ + url, headers = self._build_org_scoped_request(GOVERN_API_PATH) + payload = self._build_govern_payload(request) + self.request("POST", url=url, headers=headers, json=payload) + + @traced(name="governance_compensate", run_type="uipath") + async def _compensate_async(self, request: GovernRequest) -> None: + """Async variant of :meth:`_compensate`.""" + url, headers = self._build_org_scoped_request(GOVERN_API_PATH) + payload = self._build_govern_payload(request) + await self.request_async("POST", url=url, headers=headers, json=payload) + + # ── Internals ──────────────────────────────────────────────────── + + def _build_org_scoped_request(self, path: str) -> tuple[str, dict[str, str]]: + """Compose the agenticgovernance_ URL and the tenant header. + + Both governance endpoints share the same URL shape + (``{origin}/{org_id_uuid}/agenticgovernance_/{path}``) and the + same ``x-uipath-internal-tenantid`` header — neither matches + ``UiPathUrl.scope_url`` (slug-based), so the URL is composed + directly here. + + Honors ``UIPATH_SERVICE_URL_AGENTICGOVERNANCE`` for local dev: + when set, redirects to the override and injects routing headers + so the local server sees what the platform router would have + carried. ``BaseService.request`` does this same dance for paths + that fit ``scope_url``; the org-UUID-in-path shape forces us to + run it ourselves before composing the absolute URL. + """ + organization_id = UiPathConfig.organization_id + if not organization_id: + raise ValueError( + "Governance call requires UIPATH_ORGANIZATION_ID " + "to be set in the environment." + ) + tenant_id = UiPathConfig.tenant_id + if not tenant_id: + raise ValueError( + "Governance call requires UIPATH_TENANT_ID " + "to be set in the environment." + ) + + override = resolve_service_url(f"{GOVERNANCE_SERVICE_PREFIX}/{path}") + if override: + headers: dict[str, str] = {} + inject_routing_headers(headers) + return override, headers + + url = ( + f"{self._url.base_url}/{organization_id}/{GOVERNANCE_SERVICE_PREFIX}/{path}" + ) + return url, {HEADER_INTERNAL_TENANT_ID: tenant_id} + + @staticmethod + def _policy_params(is_conversational: Optional[bool]) -> dict[str, str]: + if is_conversational is None: + return {} + return { + AGENT_TYPE_PARAM: "conversational" if is_conversational else "autonomous" + } + + @staticmethod + def _build_govern_payload(request: GovernRequest) -> dict[str, Any]: + """Serialize the request and fill missing job-context from UiPathConfig. + + Auto-fill resolution order for each job-context field: caller + value > ``UiPathConfig`` (env-var-backed) > omit. + + ``model_dump(exclude_none=True)`` already drops fields the caller + left ``None``, so key presence — not truthiness — is the right + "was it supplied?" signal: a caller-supplied empty string is + still a caller value and must not be overridden by the env. + """ + payload = request.model_dump(by_alias=True, exclude_none=True) + for wire_key, config_attr in _JOB_CONTEXT_FIELDS: + if wire_key in payload: + continue + value = getattr(UiPathConfig, config_attr, None) + if value: + payload[wire_key] = value + return payload + + +# Wire-key → UiPathConfig attribute, for compensation payload auto-fill. +_JOB_CONTEXT_FIELDS: tuple[tuple[str, str], ...] = ( + ("folderKey", "folder_key"), + ("jobKey", "job_key"), + ("processKey", "process_uuid"), + ("referenceId", "agent_id"), + ("agentVersion", "process_version"), +) diff --git a/packages/uipath-platform/src/uipath/platform/governance/compensate.py b/packages/uipath-platform/src/uipath/platform/governance/compensate.py new file mode 100644 index 000000000..bad4845f9 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/governance/compensate.py @@ -0,0 +1,10 @@ +"""Re-exports of compensation models from :mod:`uipath.core.governance`. + +The wire-shape models live in ``uipath-core`` so the runtime can depend on +the protocol contract without importing ``uipath-platform``. This module +keeps the existing ``uipath.platform.governance`` import paths working. +""" + +from uipath.core.governance import FiredRule, GovernRequest + +__all__ = ["FiredRule", "GovernRequest"] diff --git a/packages/uipath-platform/src/uipath/platform/governance/policy.py b/packages/uipath-platform/src/uipath/platform/governance/policy.py new file mode 100644 index 000000000..27de1c9e7 --- /dev/null +++ b/packages/uipath-platform/src/uipath/platform/governance/policy.py @@ -0,0 +1,10 @@ +"""Re-exports of governance policy models from :mod:`uipath.core.governance`. + +The wire-shape models live in ``uipath-core`` so the runtime can depend on +the protocol contract without importing ``uipath-platform``. This module +keeps the existing ``uipath.platform.governance`` import paths working. +""" + +from uipath.core.governance import PolicyContext, PolicyResponse + +__all__ = ["PolicyContext", "PolicyResponse"] diff --git a/packages/uipath-platform/tests/services/test_governance_provider.py b/packages/uipath-platform/tests/services/test_governance_provider.py new file mode 100644 index 000000000..24e489f65 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_governance_provider.py @@ -0,0 +1,166 @@ +"""Tests for UiPathPlatformGovernanceProvider.""" + +from __future__ import annotations + +import pytest +from pytest_httpx import HTTPXMock +from uipath.core.governance import ( + EnforcementMode, + FiredRule, + GovernanceCompensationProvider, + GovernancePolicyProvider, + GovernRequest, + PolicyContext, +) + +from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.governance import ( + GovernanceService, + UiPathPlatformGovernanceProvider, +) + +ORG_ID = "11111111-1111-1111-1111-111111111111" +TENANT_ID = "22222222-2222-2222-2222-222222222222" + + +def _make_request() -> GovernRequest: + return GovernRequest( + validators=["pii_detection"], + rules=[ + FiredRule( + rule_id="ASI-01", + rule_name="Block PII in flight", + pack_name="agent-safety", + validator="pii_detection", + ) + ], + data={"prompt": "hello"}, + hook="before_model", + trace_id="0123456789abcdef0123456789abcdef", + src_timestamp="2026-06-22T10:00:00Z", + agent_name="my-agent", + runtime_id="runtime-1", + ) + + +@pytest.fixture +def provider( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, +) -> UiPathPlatformGovernanceProvider: + monkeypatch.setenv("UIPATH_ORGANIZATION_ID", ORG_ID) + monkeypatch.setenv("UIPATH_TENANT_ID", TENANT_ID) + service = GovernanceService(config=config, execution_context=execution_context) + return UiPathPlatformGovernanceProvider(service=service) + + +class TestConstruction: + def test_accepts_existing_service( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + service = GovernanceService(config=config, execution_context=execution_context) + provider = UiPathPlatformGovernanceProvider(service=service) + assert provider.service is service + + def test_builds_service_from_config( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + ) -> None: + provider = UiPathPlatformGovernanceProvider( + config=config, execution_context=execution_context + ) + assert isinstance(provider.service, GovernanceService) + + def test_requires_service_or_full_kwargs(self) -> None: + with pytest.raises(ValueError, match="GovernanceService"): + UiPathPlatformGovernanceProvider() + + +class TestProtocolConformance: + def test_satisfies_policy_provider_protocol( + self, provider: UiPathPlatformGovernanceProvider + ) -> None: + assert isinstance(provider, GovernancePolicyProvider) + + def test_satisfies_compensation_provider_protocol( + self, provider: UiPathPlatformGovernanceProvider + ) -> None: + assert isinstance(provider, GovernanceCompensationProvider) + + +class TestDelegation: + def test_get_policy_delegates_to_service( + self, + httpx_mock: HTTPXMock, + provider: UiPathPlatformGovernanceProvider, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=( + f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy" + "?agentType=conversational" + ), + status_code=200, + json={"mode": "enforce", "policies": "rules: []"}, + ) + + response = provider.get_policy(PolicyContext(is_conversational=True)) + + assert response.mode is EnforcementMode.ENFORCE + assert response.policies == "rules: []" + + async def test_get_policy_async_delegates_to_service( + self, + httpx_mock: HTTPXMock, + provider: UiPathPlatformGovernanceProvider, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy", + status_code=200, + json={"mode": "audit", "policies": ""}, + ) + + response = await provider.get_policy_async(PolicyContext()) + + assert response.mode is EnforcementMode.AUDIT + + def test_compensate_delegates_to_service( + self, + httpx_mock: HTTPXMock, + provider: UiPathPlatformGovernanceProvider, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + status_code=200, + json={}, + ) + + provider.compensate(_make_request()) + + requests = httpx_mock.get_requests() + assert len(requests) == 1 + assert requests[0].method == "POST" + + async def test_compensate_async_delegates_to_service( + self, + httpx_mock: HTTPXMock, + provider: UiPathPlatformGovernanceProvider, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + status_code=200, + json={}, + ) + + await provider.compensate_async(_make_request()) + + requests = httpx_mock.get_requests() + assert len(requests) == 1 + assert requests[0].method == "POST" diff --git a/packages/uipath-platform/tests/services/test_governance_service.py b/packages/uipath-platform/tests/services/test_governance_service.py new file mode 100644 index 000000000..e437fdda0 --- /dev/null +++ b/packages/uipath-platform/tests/services/test_governance_service.py @@ -0,0 +1,594 @@ +"""Tests for GovernanceService.""" + +from __future__ import annotations + +import json +from typing import Any + +import httpx +import pytest +from pytest_httpx import HTTPXMock +from uipath.core.governance import GovernancePolicyProvider, PolicyContext + +from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.common import resolve_trace_id +from uipath.platform.governance import ( + FiredRule, + GovernanceService, + PolicyResponse, +) + +ORG_ID = "11111111-1111-1111-1111-111111111111" +TENANT_ID = "22222222-2222-2222-2222-222222222222" +TENANT_ID_HEX = TENANT_ID.replace("-", "").lower() + + +def _compensate_kwargs(**overrides: Any) -> dict[str, Any]: + """Default kwargs for ``service.compensate(...)``.""" + defaults: dict[str, Any] = dict( + validators=["pii_detection"], + rules=[ + FiredRule( + rule_id="ASI-01", + rule_name="Block PII in flight", + pack_name="agent-safety", + validator="pii_detection", + ) + ], + data={"prompt": "hello"}, + hook="before_model", + trace_id="0123456789abcdef0123456789abcdef", + src_timestamp="2026-06-22T10:00:00Z", + agent_name="my-agent", + runtime_id="runtime-1", + ) + defaults.update(overrides) + return defaults + + +@pytest.fixture +def service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, +) -> GovernanceService: + monkeypatch.setenv("UIPATH_ORGANIZATION_ID", ORG_ID) + monkeypatch.setenv("UIPATH_TENANT_ID", TENANT_ID) + return GovernanceService(config=config, execution_context=execution_context) + + +class TestGovernanceService: + """Test GovernanceService functionality.""" + + class TestRetrievePolicy: + """Test retrieve_policy (sync).""" + + def test_returns_parsed_policy( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy", + status_code=200, + json={"mode": "enforce", "policies": "rules: []"}, + ) + + result = service.retrieve_policy() + + assert isinstance(result, PolicyResponse) + assert result.mode == "enforce" + assert result.policies == "rules: []" + + def test_defaults_when_fields_missing( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy", + status_code=200, + json={}, + ) + + result = service.retrieve_policy() + + assert result.mode is None + assert result.policies == "" + + def test_sends_tenant_header_and_bearer_token( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + secret: str, + ) -> None: + captured: dict[str, httpx.Request] = {} + + def capture(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response(200, json={"mode": "audit", "policies": ""}) + + httpx_mock.add_callback( + capture, + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy", + ) + + service.retrieve_policy() + + request = captured["request"] + assert request.method == "GET" + assert request.headers["x-uipath-internal-tenantid"] == TENANT_ID + assert request.headers["authorization"] == f"Bearer {secret}" + # No agentType query param when caller omits it. + assert "agentType" not in request.url.params + + @pytest.mark.parametrize( + ("is_conversational", "expected"), + [(True, "conversational"), (False, "autonomous")], + ) + def test_appends_agent_type_query_param( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + is_conversational: bool, + expected: str, + ) -> None: + captured: dict[str, httpx.Request] = {} + + def capture(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response(200, json={"mode": "audit", "policies": ""}) + + httpx_mock.add_callback( + capture, + url=( + f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy" + f"?agentType={expected}" + ), + ) + + service.retrieve_policy(is_conversational=is_conversational) + + assert captured["request"].url.params["agentType"] == expected + + def test_raises_when_organization_id_missing( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.delenv("UIPATH_ORGANIZATION_ID", raising=False) + monkeypatch.setenv("UIPATH_TENANT_ID", TENANT_ID) + service = GovernanceService( + config=config, execution_context=execution_context + ) + + with pytest.raises(ValueError, match="UIPATH_ORGANIZATION_ID"): + service.retrieve_policy() + + def test_raises_when_tenant_id_missing( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("UIPATH_ORGANIZATION_ID", ORG_ID) + monkeypatch.delenv("UIPATH_TENANT_ID", raising=False) + service = GovernanceService( + config=config, execution_context=execution_context + ) + + with pytest.raises(ValueError, match="UIPATH_TENANT_ID"): + service.retrieve_policy() + + def test_raises_on_http_error( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + ) -> None: + from uipath.platform.errors import EnrichedException + + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy", + status_code=500, + text="boom", + ) + + with pytest.raises(EnrichedException): + service.retrieve_policy() + + class TestRetrievePolicyAsync: + """Test retrieve_policy_async.""" + + async def test_returns_parsed_policy( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=( + f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy" + "?agentType=autonomous" + ), + status_code=200, + json={"mode": "audit", "policies": "rules: []"}, + ) + + result = await service.retrieve_policy_async(is_conversational=False) + + assert result.mode == "audit" + assert result.policies == "rules: []" + + class TestCompensate: + """Test compensate (sync).""" + + def test_posts_aliased_payload( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + secret: str, + ) -> None: + captured: dict[str, httpx.Request] = {} + + def capture(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response(200, json={}) + + httpx_mock.add_callback( + capture, + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + ) + + service.compensate(**_compensate_kwargs()) + + request = captured["request"] + assert request.method == "POST" + assert request.headers["x-uipath-internal-tenantid"] == TENANT_ID + assert request.headers["authorization"] == f"Bearer {secret}" + + body = json.loads(request.content) + assert body["type"] == ["pii_detection"] + assert body["rules"] == [ + { + "ruleId": "ASI-01", + "ruleName": "Block PII in flight", + "packName": "agent-safety", + "validator": "pii_detection", + } + ] + assert body["traceId"] == "0123456789abcdef0123456789abcdef" + assert body["src_timestamp"] == "2026-06-22T10:00:00Z" + assert body["agentName"] == "my-agent" + assert body["runtimeId"] == "runtime-1" + assert body["hook"] == "before_model" + assert body["data"] == {"prompt": "hello"} + + def test_autofills_job_context_from_config( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("UIPATH_FOLDER_KEY", "folder-from-env") + monkeypatch.setenv("UIPATH_JOB_KEY", "job-from-env") + monkeypatch.setenv("UIPATH_PROCESS_UUID", "process-from-env") + monkeypatch.setenv("UIPATH_AGENT_ID", "agent-from-env") + monkeypatch.setenv("UIPATH_PROCESS_VERSION", "1.2.3") + + captured: dict[str, httpx.Request] = {} + + def capture(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response(200, json={}) + + httpx_mock.add_callback( + capture, + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + ) + + service.compensate(**_compensate_kwargs()) + + body = json.loads(captured["request"].content) + assert body["folderKey"] == "folder-from-env" + assert body["jobKey"] == "job-from-env" + assert body["processKey"] == "process-from-env" + assert body["referenceId"] == "agent-from-env" + assert body["agentVersion"] == "1.2.3" + + def test_caller_overrides_take_precedence( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("UIPATH_FOLDER_KEY", "env-folder") + monkeypatch.setenv("UIPATH_JOB_KEY", "env-job") + + captured: dict[str, httpx.Request] = {} + + def capture(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response(200, json={}) + + httpx_mock.add_callback( + capture, + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + ) + + service.compensate(**_compensate_kwargs(folder_key="explicit-folder")) + + body = json.loads(captured["request"].content) + # Caller-supplied value wins. + assert body["folderKey"] == "explicit-folder" + # Env-backed fallback fills the unset one. + assert body["jobKey"] == "env-job" + # Unset and unbacked → key omitted. + assert "processKey" not in body + + def test_caller_empty_string_is_not_overridden_by_env( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("UIPATH_FOLDER_KEY", "env-folder") + + captured: dict[str, httpx.Request] = {} + + def capture(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response(200, json={}) + + httpx_mock.add_callback( + capture, + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + ) + + # Explicit empty string is still a caller value — must not be + # silently replaced by the env-backed UiPathConfig fallback. + service.compensate(**_compensate_kwargs(folder_key="")) + + body = json.loads(captured["request"].content) + assert body["folderKey"] == "" + + def test_omits_job_context_keys_with_no_value( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + for env_key in ( + "UIPATH_FOLDER_KEY", + "UIPATH_JOB_KEY", + "UIPATH_PROCESS_UUID", + "UIPATH_AGENT_ID", + "UIPATH_PROCESS_VERSION", + "UIPATH_PROJECT_ID", + ): + monkeypatch.delenv(env_key, raising=False) + + captured: dict[str, httpx.Request] = {} + + def capture(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response(200, json={}) + + httpx_mock.add_callback( + capture, + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + ) + + service.compensate(**_compensate_kwargs()) + + body = json.loads(captured["request"].content) + for absent in ( + "folderKey", + "jobKey", + "processKey", + "referenceId", + "agentVersion", + ): + assert absent not in body + + def test_raises_on_http_error( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + ) -> None: + from uipath.platform.errors import EnrichedException + + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + status_code=400, + text="bad payload", + ) + + with pytest.raises(EnrichedException): + service.compensate(**_compensate_kwargs()) + + def test_raises_when_org_id_missing( + self, + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.delenv("UIPATH_ORGANIZATION_ID", raising=False) + monkeypatch.setenv("UIPATH_TENANT_ID", TENANT_ID) + service = GovernanceService( + config=config, execution_context=execution_context + ) + + with pytest.raises(ValueError, match="UIPATH_ORGANIZATION_ID"): + service.compensate(**_compensate_kwargs()) + + class TestCompensateAsync: + """Test compensate_async.""" + + async def test_posts_aliased_payload( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/govern", + status_code=200, + json={}, + ) + + await service.compensate_async(**_compensate_kwargs()) + + requests = httpx_mock.get_requests() + assert len(requests) == 1 + assert requests[0].method == "POST" + + class TestProtocolConformance: + """``get_policy`` adapter is the only protocol-shaped surface left + on :class:`GovernanceService`; compensation conformance is tested + against :class:`UiPathPlatformGovernanceProvider`. + """ + + def test_satisfies_policy_provider_protocol( + self, service: GovernanceService + ) -> None: + assert isinstance(service, GovernancePolicyProvider) + + def test_get_policy_delegates_to_retrieve_policy( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=( + f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy" + "?agentType=conversational" + ), + status_code=200, + json={"mode": "enforce", "policies": "rules: []"}, + ) + + response = service.get_policy(PolicyContext(is_conversational=True)) + + assert response.mode == "enforce" + assert response.policies == "rules: []" + + async def test_get_policy_async_delegates( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + base_url: str, + ) -> None: + httpx_mock.add_response( + url=f"{base_url}/{ORG_ID}/agenticgovernance_/api/v1/runtime/policy", + status_code=200, + json={"mode": "audit", "policies": ""}, + ) + + response = await service.get_policy_async(PolicyContext()) + + assert response.mode == "audit" + + class TestServiceUrlOverride: + """Honor UIPATH_SERVICE_URL_AGENTICGOVERNANCE for local dev.""" + + def test_redirects_policy_fetch_to_override( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv( + "UIPATH_SERVICE_URL_AGENTICGOVERNANCE", "http://localhost:8123" + ) + captured: dict[str, httpx.Request] = {} + + def capture(request: httpx.Request) -> httpx.Response: + captured["request"] = request + return httpx.Response(200, json={"mode": "audit", "policies": ""}) + + httpx_mock.add_callback( + capture, url="http://localhost:8123/api/v1/runtime/policy" + ) + + service.retrieve_policy() + + request = captured["request"] + # Routing headers replace the platform router, org-UUID path is dropped. + assert request.headers["X-UiPath-Internal-TenantId"] == TENANT_ID + assert request.headers["X-UiPath-Internal-AccountId"] == ORG_ID + assert ORG_ID not in str(request.url) + + def test_redirects_compensate_to_override( + self, + httpx_mock: HTTPXMock, + service: GovernanceService, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv( + "UIPATH_SERVICE_URL_AGENTICGOVERNANCE", "http://localhost:8123" + ) + httpx_mock.add_response( + url="http://localhost:8123/api/v1/runtime/govern", + method="POST", + status_code=200, + json={}, + ) + + service.compensate(**_compensate_kwargs()) + + sent = httpx_mock.get_requests()[-1] + assert sent.method == "POST" + assert sent.headers["X-UiPath-Internal-AccountId"] == ORG_ID + + +class TestResolveTraceId: + """Test the resolve_trace_id helper.""" + + def test_returns_fallback_when_no_source_set( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv("UIPATH_TRACE_ID", raising=False) + + assert resolve_trace_id(fallback="fallback-id") == "fallback-id" + + def test_returns_none_when_no_fallback( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv("UIPATH_TRACE_ID", raising=False) + + assert resolve_trace_id() is None + + def test_reads_uipath_trace_id_in_hex_form( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("UIPATH_TRACE_ID", "0123456789abcdef0123456789abcdef") + + assert resolve_trace_id() == "0123456789abcdef0123456789abcdef" + + def test_normalizes_uipath_trace_id_in_uuid_form( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("UIPATH_TRACE_ID", TENANT_ID) + + assert resolve_trace_id() == TENANT_ID_HEX + + def test_falls_through_when_uipath_trace_id_is_malformed( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("UIPATH_TRACE_ID", "not-a-valid-trace-id") + + # No OTel context active → falls through to caller-supplied fallback. + assert resolve_trace_id(fallback="recovered") == "recovered" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index df0dc786a..9f696bfe4 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.20" +version = "0.5.21" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.71" +version = "0.1.73" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 827befcfb..1bf92e0e1 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -5,9 +5,9 @@ description = "Python SDK and CLI for UiPath Platform, enabling programmatic int readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ - "uipath-core>=0.5.17, <0.6.0", + "uipath-core>=0.5.21, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.68, <0.2.0", + "uipath-platform>=0.1.73, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 15873d997..94b6b38fb 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-20T16:42:14.097008Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.20" +version = "0.5.21" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.71" +version = "0.1.73" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 9038bc6be05709259937c271324d747e86d980a9 Mon Sep 17 00:00:00 2001 From: Radu Mihai Gheorghe Date: Tue, 23 Jun 2026 18:32:05 +0300 Subject: [PATCH 117/121] fix(agent-models): add a2aUrl to AgentA2aResourceConfig (#1743) --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/agent/models/agent.py | 1 + packages/uipath/tests/agent/models/test_agent.py | 5 +++++ packages/uipath/uv.lock | 4 ++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 1bf92e0e1..573dba7bb 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.11.9" +version = "2.11.10" 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/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 916417c94..719a698aa 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -544,6 +544,7 @@ class AgentA2aResourceConfig(BaseAgentResourceConfig): id: str slug: str = Field(..., alias="slug") folder_path: str = Field(alias="folderPath") + a2a_url: str = Field(..., alias="a2aUrl") cached_agent_card: Optional[Dict[str, Any]] = Field( default=None, alias="cachedAgentCard" ) diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 202962c64..0d99dad5a 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3995,6 +3995,7 @@ def test_a2a_resource(self): "slug": "philosopher-agent", "description": "A philosophical agent that answers questions with wisdom and philosopher quotes", "folderPath": "shared", + "a2a_url": "https://cloud.uipath.com/a2a/5045dca3", "cachedAgentCard": { "name": "Philosopher Agent", "description": "Philosopher Agent assistant", @@ -4070,6 +4071,7 @@ def test_a2a_resource(self): ) assert a2a_resource.id == "755e2f7d-5a3d-47f3-8e9d-7ff0bf226357" assert a2a_resource.folder_path == "shared" + assert a2a_resource.a2a_url == "https://cloud.uipath.com/a2a/5045dca3" # Validate cached agent card is a plain dict card = a2a_resource.cached_agent_card @@ -4108,6 +4110,7 @@ def test_a2a_resource_without_cached_card(self): "slug": "minimal-a2a", "description": "A minimal A2A agent", "folderPath": "shared", + "a2a_url": "https://cloud.uipath.com/a2a/abc-123", } ], "features": [], @@ -4127,6 +4130,7 @@ def test_a2a_resource_without_cached_card(self): assert a2a_resource.name == "Minimal A2A Agent" assert a2a_resource.slug == "minimal-a2a" assert a2a_resource.folder_path == "shared" + assert a2a_resource.a2a_url == "https://cloud.uipath.com/a2a/abc-123" assert a2a_resource.cached_agent_card is None def test_a2a_resource_case_insensitive(self): @@ -4154,6 +4158,7 @@ def test_a2a_resource_case_insensitive(self): "slug": "case-test", "description": "Testing case insensitive parsing", "folderPath": "shared", + "a2a_url": "https://cloud.uipath.com/a2a/case-test-id", } ], "features": [], diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 94b6b38fb..be6c3b203 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-06-21T14:10:18.2328398Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.9" +version = "2.11.10" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 7dec553e614be9e60ead63a0780422dcd0333640 Mon Sep 17 00:00:00 2001 From: Radu Mocanu Date: Tue, 23 Jun 2026 19:17:45 +0300 Subject: [PATCH 118/121] feat(core): add workspace hydration service protocols (#1744) --- packages/uipath-core/pyproject.toml | 2 +- .../src/uipath/core/workspace/__init__.py | 8 +++ .../src/uipath/core/workspace/protocols.py | 56 +++++++++++++++ .../uipath-core/tests/workspace/__init__.py | 0 .../tests/workspace/test_protocols.py | 68 +++++++++++++++++++ packages/uipath-core/uv.lock | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 8 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 packages/uipath-core/src/uipath/core/workspace/__init__.py create mode 100644 packages/uipath-core/src/uipath/core/workspace/protocols.py create mode 100644 packages/uipath-core/tests/workspace/__init__.py create mode 100644 packages/uipath-core/tests/workspace/test_protocols.py diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 56e0c4d4f..473d485a3 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.21" +version = "0.5.22" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/workspace/__init__.py b/packages/uipath-core/src/uipath/core/workspace/__init__.py new file mode 100644 index 000000000..c05992aae --- /dev/null +++ b/packages/uipath-core/src/uipath/core/workspace/__init__.py @@ -0,0 +1,8 @@ +"""UiPath workspace hydration shared contracts.""" + +from .protocols import AttachmentsProtocol, JobsProtocol + +__all__ = [ + "AttachmentsProtocol", + "JobsProtocol", +] diff --git a/packages/uipath-core/src/uipath/core/workspace/protocols.py b/packages/uipath-core/src/uipath/core/workspace/protocols.py new file mode 100644 index 000000000..c3f39c170 --- /dev/null +++ b/packages/uipath-core/src/uipath/core/workspace/protocols.py @@ -0,0 +1,56 @@ +"""Service protocols for workspace hydration backend interactions.""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable +from uuid import UUID + + +@runtime_checkable +class AttachmentsProtocol(Protocol): + """Subset of the UiPath attachments service used by workspace hydration.""" + + async def download_async( + self, + *, + key: UUID, + destination_path: str, + folder_key: str | None = None, + folder_path: str | None = None, + ) -> str: + """Download an attachment to a local path.""" + + async def upload_async( + self, + *, + name: str, + content: str | bytes | None = None, + source_path: str | None = None, + folder_key: str | None = None, + folder_path: str | None = None, + ) -> UUID: + """Upload content or a local file and return its attachment key.""" + + +@runtime_checkable +class JobsProtocol(Protocol): + """Subset of the UiPath jobs service used by workspace hydration.""" + + async def list_attachments_async( + self, + *, + job_key: UUID, + folder_key: str | None = None, + folder_path: str | None = None, + ) -> list[str]: + """List the attachment ids linked to a job.""" + + async def link_attachment_async( + self, + *, + job_key: UUID, + attachment_key: UUID, + folder_key: str | None = None, + folder_path: str | None = None, + ) -> None: + """Link an existing attachment to a job.""" diff --git a/packages/uipath-core/tests/workspace/__init__.py b/packages/uipath-core/tests/workspace/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/uipath-core/tests/workspace/test_protocols.py b/packages/uipath-core/tests/workspace/test_protocols.py new file mode 100644 index 000000000..0c9cd11c4 --- /dev/null +++ b/packages/uipath-core/tests/workspace/test_protocols.py @@ -0,0 +1,68 @@ +"""Structural-conformance tests for the workspace hydration protocols.""" + +from __future__ import annotations + +from uuid import UUID, uuid4 + +from uipath.core.workspace import AttachmentsProtocol, JobsProtocol + + +class _FakeAttachments: + async def download_async( + self, + *, + key: UUID, + destination_path: str, + folder_key: str | None = None, + folder_path: str | None = None, + ) -> str: + return destination_path + + async def upload_async( + self, + *, + name: str, + content: str | bytes | None = None, + source_path: str | None = None, + folder_key: str | None = None, + folder_path: str | None = None, + ) -> UUID: + return uuid4() + + +class _FakeJobs: + async def list_attachments_async( + self, + *, + job_key: UUID, + folder_key: str | None = None, + folder_path: str | None = None, + ) -> list[str]: + return [] + + async def link_attachment_async( + self, + *, + job_key: UUID, + attachment_key: UUID, + folder_key: str | None = None, + folder_path: str | None = None, + ) -> None: + return None + + +class _NotAService: + pass + + +def test_attachments_service_satisfies_protocol() -> None: + assert isinstance(_FakeAttachments(), AttachmentsProtocol) + + +def test_jobs_service_satisfies_protocol() -> None: + assert isinstance(_FakeJobs(), JobsProtocol) + + +def test_unrelated_object_does_not_satisfy_protocols() -> None: + assert not isinstance(_NotAService(), AttachmentsProtocol) + assert not isinstance(_NotAService(), JobsProtocol) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index 005bf9018..cfb903454 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.21" +version = "0.5.22" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 9f696bfe4..d6799b717 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.21" +version = "0.5.22" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index be6c3b203..e4a954bba 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.21" +version = "0.5.22" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, From 233f4e3d1d7f7acaaf2b6c2ffea906a3a9462806 Mon Sep 17 00:00:00 2001 From: Radu Mihai Gheorghe Date: Wed, 24 Jun 2026 16:24:16 +0300 Subject: [PATCH 119/121] feat(remote-a2a): resolve a2a agent via resource-override bindings (#1748) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/agenthub/_remote_a2a_service.py | 3 +++ packages/uipath-platform/uv.lock | 4 ++-- packages/uipath/pyproject.toml | 4 ++-- packages/uipath/src/uipath/agent/models/agent.py | 1 - packages/uipath/tests/agent/models/test_agent.py | 5 ----- packages/uipath/uv.lock | 6 +++--- 7 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index b8effe0f0..868a66130 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.73" +version = "0.1.74" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py b/packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py index c9475976c..2caf3f2f0 100644 --- a/packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py +++ b/packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py @@ -9,6 +9,7 @@ from typing import Any, List from ..common._base_service import BaseService +from ..common._bindings import resource_override from ..common._config import UiPathApiConfig from ..common._execution_context import UiPathExecutionContext from ..common._folder_context import FolderContext, header_folder @@ -149,6 +150,7 @@ async def main(): data = response.json() return [RemoteA2aAgent.model_validate(agent) for agent in data.get("value", [])] + @resource_override(resource_type="remoteA2aAgent", resource_identifier="slug") def retrieve( self, slug: str, @@ -190,6 +192,7 @@ def retrieve( ) return RemoteA2aAgent.model_validate(response.json()) + @resource_override(resource_type="remoteA2aAgent", resource_identifier="slug") async def retrieve_async( self, slug: str, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index d6799b717..1cb0ed18b 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-06-22T12:09:32.9206897Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.73" +version = "0.1.74" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 573dba7bb..434af1c00 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.11.10" +version = "2.11.11" 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" dependencies = [ "uipath-core>=0.5.21, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.73, <0.2.0", + "uipath-platform>=0.1.74, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/agent/models/agent.py b/packages/uipath/src/uipath/agent/models/agent.py index 719a698aa..916417c94 100644 --- a/packages/uipath/src/uipath/agent/models/agent.py +++ b/packages/uipath/src/uipath/agent/models/agent.py @@ -544,7 +544,6 @@ class AgentA2aResourceConfig(BaseAgentResourceConfig): id: str slug: str = Field(..., alias="slug") folder_path: str = Field(alias="folderPath") - a2a_url: str = Field(..., alias="a2aUrl") cached_agent_card: Optional[Dict[str, Any]] = Field( default=None, alias="cachedAgentCard" ) diff --git a/packages/uipath/tests/agent/models/test_agent.py b/packages/uipath/tests/agent/models/test_agent.py index 0d99dad5a..202962c64 100644 --- a/packages/uipath/tests/agent/models/test_agent.py +++ b/packages/uipath/tests/agent/models/test_agent.py @@ -3995,7 +3995,6 @@ def test_a2a_resource(self): "slug": "philosopher-agent", "description": "A philosophical agent that answers questions with wisdom and philosopher quotes", "folderPath": "shared", - "a2a_url": "https://cloud.uipath.com/a2a/5045dca3", "cachedAgentCard": { "name": "Philosopher Agent", "description": "Philosopher Agent assistant", @@ -4071,7 +4070,6 @@ def test_a2a_resource(self): ) assert a2a_resource.id == "755e2f7d-5a3d-47f3-8e9d-7ff0bf226357" assert a2a_resource.folder_path == "shared" - assert a2a_resource.a2a_url == "https://cloud.uipath.com/a2a/5045dca3" # Validate cached agent card is a plain dict card = a2a_resource.cached_agent_card @@ -4110,7 +4108,6 @@ def test_a2a_resource_without_cached_card(self): "slug": "minimal-a2a", "description": "A minimal A2A agent", "folderPath": "shared", - "a2a_url": "https://cloud.uipath.com/a2a/abc-123", } ], "features": [], @@ -4130,7 +4127,6 @@ def test_a2a_resource_without_cached_card(self): assert a2a_resource.name == "Minimal A2A Agent" assert a2a_resource.slug == "minimal-a2a" assert a2a_resource.folder_path == "shared" - assert a2a_resource.a2a_url == "https://cloud.uipath.com/a2a/abc-123" assert a2a_resource.cached_agent_card is None def test_a2a_resource_case_insensitive(self): @@ -4158,7 +4154,6 @@ def test_a2a_resource_case_insensitive(self): "slug": "case-test", "description": "Testing case insensitive parsing", "folderPath": "shared", - "a2a_url": "https://cloud.uipath.com/a2a/case-test-id", } ], "features": [], diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e4a954bba..e519d3074 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-21T14:10:18.2328398Z" +exclude-newer = "2026-06-22T12:09:29.4029891Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.10" +version = "2.11.11" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.73" +version = "0.1.74" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 3d0cfdc939e6a6fc59f1c31c6d2da839649406f7 Mon Sep 17 00:00:00 2001 From: Radu Mihai Gheorghe Date: Wed, 24 Jun 2026 17:51:00 +0300 Subject: [PATCH 120/121] fix(remote-a2a): fall back to folder context when folder_path is missing (#1749) --- packages/uipath-platform/pyproject.toml | 2 +- .../platform/agenthub/_remote_a2a_service.py | 9 ++- .../tests/services/test_remote_a2a_service.py | 55 +++++++++++++++++++ packages/uipath-platform/uv.lock | 4 +- packages/uipath/pyproject.toml | 4 +- packages/uipath/uv.lock | 6 +- 6 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 packages/uipath-platform/tests/services/test_remote_a2a_service.py diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 868a66130..eec99ca30 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.74" +version = "0.1.75" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py b/packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py index 2caf3f2f0..c8993e3ee 100644 --- a/packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py +++ b/packages/uipath-platform/src/uipath/platform/agenthub/_remote_a2a_service.py @@ -242,6 +242,13 @@ async def main(): def custom_headers(self) -> dict[str, str]: return self.folder_headers + def _resolve_folder_key(self, folder_path: str | None) -> str | None: + """Resolve folder key from folder_path, falling back to FolderContext.""" + if folder_path is not None: + return self._folders_service.retrieve_folder_key(folder_path) + + return self._folder_key + def _list_spec( self, *, @@ -276,7 +283,7 @@ def _retrieve_spec( *, folder_path: str | None, ) -> RequestSpec: - folder_key = self._folders_service.retrieve_folder_key(folder_path) + folder_key = self._resolve_folder_key(folder_path) return RequestSpec( method="GET", endpoint=Endpoint(f"/agenthub_/api/remote-a2a-agents/{slug}"), diff --git a/packages/uipath-platform/tests/services/test_remote_a2a_service.py b/packages/uipath-platform/tests/services/test_remote_a2a_service.py new file mode 100644 index 000000000..1f4239d4d --- /dev/null +++ b/packages/uipath-platform/tests/services/test_remote_a2a_service.py @@ -0,0 +1,55 @@ +import pytest + +from uipath.platform import UiPathApiConfig, UiPathExecutionContext +from uipath.platform.agenthub._remote_a2a_service import RemoteA2aService +from uipath.platform.common.constants import HEADER_FOLDER_KEY +from uipath.platform.orchestrator._folder_service import FolderService + + +@pytest.fixture +def folders_service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + monkeypatch: pytest.MonkeyPatch, +) -> FolderService: + monkeypatch.setenv("UIPATH_FOLDER_KEY", "context-folder-key") + return FolderService(config=config, execution_context=execution_context) + + +@pytest.fixture +def service( + config: UiPathApiConfig, + execution_context: UiPathExecutionContext, + folders_service: FolderService, + monkeypatch: pytest.MonkeyPatch, +) -> RemoteA2aService: + monkeypatch.setenv("UIPATH_FOLDER_KEY", "context-folder-key") + return RemoteA2aService( + config=config, + execution_context=execution_context, + folders_service=folders_service, + ) + + +class TestRetrieveSpecFolderResolution: + def test_falls_back_to_folder_context_when_folder_path_missing( + self, service: RemoteA2aService + ) -> None: + """No folder_path (e.g. local debug) must not raise; it falls back to context.""" + spec = service._retrieve_spec(slug="weather", folder_path=None) + + assert "remote-a2a-agents/weather" in str(spec.endpoint) + assert spec.headers[HEADER_FOLDER_KEY] == "context-folder-key" + + def test_resolves_explicit_folder_path( + self, service: RemoteA2aService, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr( + service._folders_service, + "retrieve_folder_key", + lambda folder_path: "resolved-folder-key", + ) + + spec = service._retrieve_spec(slug="weather", folder_path="MyFolder") + + assert spec.headers[HEADER_FOLDER_KEY] == "resolved-folder-key" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 1cb0ed18b..a9a4d2abc 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-22T12:09:32.9206897Z" +exclude-newer = "2026-06-22T13:55:56.0776194Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.74" +version = "0.1.75" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 434af1c00..fc45cd4be 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "uipath" -version = "2.11.11" +version = "2.11.12" 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" dependencies = [ "uipath-core>=0.5.21, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.74, <0.2.0", + "uipath-platform>=0.1.75, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index e519d3074..b4b11488e 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-06-22T12:09:29.4029891Z" +exclude-newer = "2026-06-22T13:56:19.8527915Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.11.11" +version = "2.11.12" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.74" +version = "0.1.75" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From ab8b7722dbb104e87f33576ac0ef4000a2fb8615 Mon Sep 17 00:00:00 2001 From: ctiliescuuipath Date: Wed, 24 Jun 2026 18:41:28 +0300 Subject: [PATCH 121/121] feat(platform): add guardrailName to guardrails validation payload (#1746) Co-authored-by: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/guardrails/_guardrails_service.py | 1 + .../tests/services/test_guardrails_service.py | 5 ++++- packages/uipath-platform/uv.lock | 2 +- packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 2 +- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index eec99ca30..cfe85a61e 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.75" +version = "0.1.76" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py index 424b35dad..86856a6b4 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py @@ -116,6 +116,7 @@ def evaluate_guardrail( "validator": guardrail.validator_type, "input": input_data if isinstance(input_data, str) else str(input_data), "parameters": parameters, + "guardrailName": guardrail.name, } spec = RequestSpec( method="POST", diff --git a/packages/uipath-platform/tests/services/test_guardrails_service.py b/packages/uipath-platform/tests/services/test_guardrails_service.py index dd20d5646..e9d73a06f 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_service.py +++ b/packages/uipath-platform/tests/services/test_guardrails_service.py @@ -262,15 +262,18 @@ def capture_request(request): # Parse the request payload request_payload = json.loads(captured_request.content) - # Verify the payload structure matches the reverted format: + # Verify the payload structure: # { # "validator": guardrail.validator_type, # "input": input_data, # "parameters": parameters, + # "guardrailName": guardrail.name, # } assert "validator" in request_payload assert "input" in request_payload assert "parameters" in request_payload + assert "guardrailName" in request_payload + assert request_payload["guardrailName"] == "PII detection guardrail" # Verify validator is a string (not an object) assert isinstance(request_payload["validator"], str) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index a9a4d2abc..2c3e5d025 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.75" +version = "0.1.76" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index fc45cd4be..7acd8465d 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.21, <0.6.0", "uipath-runtime>=0.11.0, <0.12.0", - "uipath-platform>=0.1.75, <0.2.0", + "uipath-platform>=0.1.76, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index b4b11488e..989e4b5ea 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2691,7 +2691,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.75" +version = "0.1.76" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" },

    $ 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