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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
10 changes: 10 additions & 0 deletions packages/uipath-platform/src/uipath/platform/_uipath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -35,6 +36,7 @@
QueuesService,
)
from .resource_catalog import ResourceCatalogService
from .semantic_proxy import SemanticProxyService


def _has_valid_client_credentials(
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what the return type is here... but it will be better to have Response object here so that its not free form any objects in response... and unexpected responses can be caught early... similar to PII

"""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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it POST ? for "Get" Policy?

endpoint=_DEPLOYED_POLICY_ENDPOINT,
)
Original file line number Diff line number Diff line change
@@ -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",
]
Original file line number Diff line number Diff line change
@@ -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),
)
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading