Skip to content

Commit 59d8ca4

Browse files
authored
Integrate with AutomationOps and SemanticProxy (#1578)
1 parent 58adbab commit 59d8ca4

13 files changed

Lines changed: 995 additions & 3 deletions

File tree

packages/uipath-platform/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-platform"
3-
version = "0.1.33"
3+
version = "0.1.34"
44
description = "HTTP client library for programmatic access to UiPath Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-platform/src/uipath/platform/_uipath.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .action_center import TasksService
99
from .agenthub._agenthub_service import AgentHubService
1010
from .agenthub._remote_a2a_service import RemoteA2aService
11+
from .automation_ops import AutomationOpsService
1112
from .chat import ConversationsService, UiPathLlmChatService, UiPathOpenAIService
1213
from .common import (
1314
ApiClient,
@@ -35,6 +36,7 @@
3536
QueuesService,
3637
)
3738
from .resource_catalog import ResourceCatalogService
39+
from .semantic_proxy import SemanticProxyService
3840

3941

4042
def _has_valid_client_credentials(
@@ -178,6 +180,14 @@ def remote_a2a(self) -> RemoteA2aService:
178180
def orchestrator_setup(self) -> OrchestratorSetupService:
179181
return OrchestratorSetupService(self._config, self._execution_context)
180182

183+
@property
184+
def automation_ops(self) -> AutomationOpsService:
185+
return AutomationOpsService(self._config, self._execution_context)
186+
187+
@property
188+
def semantic_proxy(self) -> SemanticProxyService:
189+
return SemanticProxyService(self._config, self._execution_context)
190+
181191
@property
182192
def automation_tracker(self) -> AutomationTrackerService:
183193
return AutomationTrackerService(self._config, self._execution_context)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""AutomationOps service package.
2+
3+
Provides the ``AutomationOpsService`` client for retrieving deployed AI Trust
4+
Layer policies from AgentHub.
5+
"""
6+
7+
from ._automation_ops_service import AutomationOpsService
8+
9+
__all__ = ["AutomationOpsService"]
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""AutomationOps service for UiPath Platform.
2+
3+
Provides methods for retrieving deployed policies from the AgentHub service.
4+
"""
5+
6+
from typing import Any
7+
8+
from uipath.core.tracing import traced
9+
10+
from ..common._base_service import BaseService
11+
from ..common._config import UiPathApiConfig
12+
from ..common._execution_context import UiPathExecutionContext
13+
from ..common._models import Endpoint, RequestSpec
14+
15+
_DEPLOYED_POLICY_ENDPOINT = Endpoint("agenthub_/api/policies/deployed-policy")
16+
17+
18+
class AutomationOpsService(BaseService):
19+
"""Service for interacting with UiPath AutomationOps policies via AgentHub."""
20+
21+
def __init__(
22+
self,
23+
config: UiPathApiConfig,
24+
execution_context: UiPathExecutionContext,
25+
) -> None:
26+
super().__init__(config=config, execution_context=execution_context)
27+
28+
@traced(name="automation_ops_get_deployed_policy", run_type="uipath")
29+
def get_deployed_policy(self) -> dict[str, Any]:
30+
"""Retrieve the deployed policy.
31+
32+
Returns:
33+
The deployed policy response as a dictionary.
34+
"""
35+
spec = self._deployed_policy_spec()
36+
response = self.request(
37+
spec.method,
38+
url=spec.endpoint,
39+
headers=spec.headers,
40+
scoped="tenant",
41+
)
42+
return response.json()
43+
44+
@traced(name="automation_ops_get_deployed_policy", run_type="uipath")
45+
async def get_deployed_policy_async(self) -> dict[str, Any]:
46+
"""Retrieve the deployed policy (async).
47+
48+
Returns:
49+
The deployed policy response as a dictionary.
50+
"""
51+
spec = self._deployed_policy_spec()
52+
response = await self.request_async(
53+
spec.method,
54+
url=spec.endpoint,
55+
headers=spec.headers,
56+
scoped="tenant",
57+
)
58+
return response.json()
59+
60+
def _deployed_policy_spec(self) -> RequestSpec:
61+
return RequestSpec(
62+
method="POST",
63+
endpoint=_DEPLOYED_POLICY_ENDPOINT,
64+
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""SemanticProxy service package.
2+
3+
Provides the ``SemanticProxyService`` client, Pydantic request/response models for
4+
the PII detection endpoint, and utilities for rehydrating masked text with
5+
original PII values after LLM processing.
6+
"""
7+
8+
from ._semantic_proxy_service import SemanticProxyService
9+
from .pii_utilities import (
10+
rehydrate_from_pii_entities,
11+
rehydrate_from_pii_response,
12+
)
13+
from .semantic_proxy import (
14+
PiiDetectionRequest,
15+
PiiDetectionResponse,
16+
PiiDocument,
17+
PiiDocumentResult,
18+
PiiEntity,
19+
PiiEntityThreshold,
20+
PiiFile,
21+
PiiFileResult,
22+
)
23+
24+
__all__ = [
25+
"PiiDetectionRequest",
26+
"PiiDetectionResponse",
27+
"PiiDocument",
28+
"PiiDocumentResult",
29+
"PiiEntity",
30+
"PiiEntityThreshold",
31+
"PiiFile",
32+
"PiiFileResult",
33+
"SemanticProxyService",
34+
"rehydrate_from_pii_entities",
35+
"rehydrate_from_pii_response",
36+
]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""SemanticProxy service for UiPath Platform.
2+
3+
Provides methods for interacting with the SemanticProxy service (e.g. PII detection).
4+
"""
5+
6+
from uipath.core.tracing import traced
7+
8+
from ..common._base_service import BaseService
9+
from ..common._config import UiPathApiConfig
10+
from ..common._execution_context import UiPathExecutionContext
11+
from ..common._models import Endpoint, RequestSpec
12+
from .semantic_proxy import PiiDetectionRequest, PiiDetectionResponse
13+
14+
_PII_DETECTION_ENDPOINT = Endpoint("semanticproxy_/api/pii-detection")
15+
16+
17+
class SemanticProxyService(BaseService):
18+
"""Service for interacting with UiPath SemanticProxy."""
19+
20+
def __init__(
21+
self,
22+
config: UiPathApiConfig,
23+
execution_context: UiPathExecutionContext,
24+
) -> None:
25+
super().__init__(config=config, execution_context=execution_context)
26+
27+
@traced(name="semantic_proxy_detect_pii", run_type="uipath")
28+
def detect_pii(self, request: PiiDetectionRequest) -> PiiDetectionResponse:
29+
"""Detect PII in the provided documents and/or files.
30+
31+
Args:
32+
request: The PII detection request payload.
33+
34+
Returns:
35+
The PII detection response.
36+
"""
37+
spec = self._pii_detection_spec(request)
38+
response = self.request(
39+
spec.method,
40+
url=spec.endpoint,
41+
json=spec.json,
42+
headers=spec.headers,
43+
scoped="tenant",
44+
)
45+
return PiiDetectionResponse.model_validate(response.json())
46+
47+
@traced(name="semantic_proxy_detect_pii", run_type="uipath")
48+
async def detect_pii_async(
49+
self, request: PiiDetectionRequest
50+
) -> PiiDetectionResponse:
51+
"""Detect PII in the provided documents and/or files (async).
52+
53+
Args:
54+
request: The PII detection request payload.
55+
56+
Returns:
57+
The PII detection response.
58+
"""
59+
spec = self._pii_detection_spec(request)
60+
response = await self.request_async(
61+
spec.method,
62+
url=spec.endpoint,
63+
json=spec.json,
64+
headers=spec.headers,
65+
scoped="tenant",
66+
)
67+
return PiiDetectionResponse.model_validate(response.json())
68+
69+
def _pii_detection_spec(self, request: PiiDetectionRequest) -> RequestSpec:
70+
return RequestSpec(
71+
method="POST",
72+
endpoint=_PII_DETECTION_ENDPOINT,
73+
json=request.model_dump(by_alias=True, exclude_none=True),
74+
)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""Utility methods for working with PII data.
2+
3+
Python port of UiPath.SemanticProxy.Client.PiiUtilities (C#).
4+
"""
5+
6+
import json
7+
import re
8+
from typing import Callable, Iterable
9+
10+
from .semantic_proxy import PiiDetectionResponse, PiiEntity
11+
12+
13+
def rehydrate_from_pii_entities(
14+
masked_text: str, pii_entities: Iterable[PiiEntity]
15+
) -> str:
16+
"""Rehydrate masked text by replacing PII placeholders with original values.
17+
18+
Placeholders (e.g. ``[Person-1]``) are matched case-insensitively and replaced
19+
with the corresponding original PII text. The function also replaces variants
20+
without the surrounding brackets (e.g. ``Person-1``) in case the LLM stripped
21+
them in its output.
22+
23+
Args:
24+
masked_text: The masked text with PII placeholders.
25+
pii_entities: The PII entities containing the original values.
26+
27+
Returns:
28+
The rehydrated text with original PII values.
29+
"""
30+
if not masked_text:
31+
return masked_text
32+
33+
entities = [e for e in pii_entities if e.replacement_text]
34+
if not entities:
35+
return masked_text
36+
37+
# Sort by replacement text length descending to avoid substring collisions
38+
# (e.g. "[Person-10]" must be replaced before "[Person-1]").
39+
entities.sort(key=lambda e: len(e.replacement_text), reverse=True)
40+
41+
rehydrated = masked_text
42+
for entity in entities:
43+
if not entity.replacement_text or not entity.pii_text:
44+
continue
45+
escaped_pii = _add_escape_characters(entity.pii_text)
46+
# Replace the full placeholder (with brackets) case-insensitively.
47+
# ``_literal_replacer`` bypasses regex backreference interpretation in the
48+
# replacement string.
49+
rehydrated = re.sub(
50+
re.escape(entity.replacement_text),
51+
_literal_replacer(escaped_pii),
52+
rehydrated,
53+
flags=re.IGNORECASE,
54+
)
55+
# Also replace the content without brackets (in case the LLM dropped them).
56+
if entity.replacement_text.startswith("[") and entity.replacement_text.endswith(
57+
"]"
58+
):
59+
no_brackets = entity.replacement_text[1:-1]
60+
rehydrated = re.sub(
61+
re.escape(no_brackets),
62+
_literal_replacer(escaped_pii),
63+
rehydrated,
64+
flags=re.IGNORECASE,
65+
)
66+
67+
return rehydrated
68+
69+
70+
def _literal_replacer(replacement: str) -> Callable[[re.Match[str]], str]:
71+
"""Return a replacement function that ignores regex backreference syntax."""
72+
73+
def replace(_match: re.Match[str]) -> str:
74+
return replacement
75+
76+
return replace
77+
78+
79+
def rehydrate_from_pii_response(
80+
masked_text: str, response: PiiDetectionResponse
81+
) -> str:
82+
"""Rehydrate masked text using all PII entities from a detection response.
83+
84+
Merges entities from both ``response.response`` (detected in documents/prompts)
85+
and ``response.files`` (detected in files), so placeholders originating from
86+
either source are rehydrated.
87+
88+
Args:
89+
masked_text: The masked text with PII placeholders.
90+
response: The PII detection response containing entities to rehydrate.
91+
92+
Returns:
93+
The rehydrated text with original PII values.
94+
"""
95+
entities: list[PiiEntity] = []
96+
for doc in response.response:
97+
entities.extend(doc.pii_entities)
98+
for file in response.files:
99+
entities.extend(file.pii_entities)
100+
return rehydrate_from_pii_entities(masked_text, entities)
101+
102+
103+
def _add_escape_characters(text: str) -> str:
104+
"""Escape special characters in text using JSON serialization.
105+
106+
Mirrors C# ``AddEscapeCharacters`` — serializes as JSON then strips the
107+
surrounding quotes to get the escaped content.
108+
"""
109+
if not text:
110+
return ""
111+
try:
112+
serialized = json.dumps(text, ensure_ascii=False)
113+
return serialized[1:-1]
114+
except (TypeError, ValueError):
115+
return text

0 commit comments

Comments
 (0)