Skip to content

Commit f1ef956

Browse files
committed
feat:�:Fetch automationOps policy
1 parent f6d5fa0 commit f1ef956

10 files changed

Lines changed: 936 additions & 15 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
QueuesService,
3737
)
3838
from .resource_catalog import ResourceCatalogService
39+
from .semantic_proxy import SemanticProxyService
3940

4041

4142
def _has_valid_client_credentials(
@@ -183,6 +184,10 @@ def orchestrator_setup(self) -> OrchestratorSetupService:
183184
def automation_ops(self) -> AutomationOpsService:
184185
return AutomationOpsService(self._config, self._execution_context)
185186

187+
@property
188+
def semantic_proxy(self) -> SemanticProxyService:
189+
return SemanticProxyService(self._config, self._execution_context)
190+
186191
@property
187192
def automation_tracker(self) -> AutomationTrackerService:
188193
return AutomationTrackerService(self._config, self._execution_context)
Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
"""AutomationOps service for UiPath Platform.
22
3-
Provides methods for retrieving deployed policies from the RoboticsOps service.
3+
Provides methods for retrieving deployed policies from the AgentHub service.
44
"""
55

66
from typing import Any
77

8+
from uipath.core.tracing import traced
9+
810
from ..common._base_service import BaseService
911
from ..common._config import UiPathApiConfig
1012
from ..common._execution_context import UiPathExecutionContext
1113
from ..common._models import Endpoint, RequestSpec
1214

13-
14-
_DEPLOYED_POLICY_ENDPOINT = Endpoint("/roboticsops_/api/policy/deployed-policy")
15+
_DEPLOYED_POLICY_ENDPOINT = Endpoint("agenthub_/api/policies/deployed-policy")
1516

1617

1718
class AutomationOpsService(BaseService):
18-
"""Service for interacting with UiPath AutomationOps (RoboticsOps) policies."""
19+
"""Service for interacting with UiPath AutomationOps policies via AgentHub."""
1920

2021
def __init__(
2122
self,
@@ -24,38 +25,40 @@ def __init__(
2425
) -> None:
2526
super().__init__(config=config, execution_context=execution_context)
2627

28+
@traced(name="automation_ops_get_deployed_policy", run_type="uipath")
2729
def get_deployed_policy(self) -> dict[str, Any]:
2830
"""Retrieve the deployed policy.
2931
3032
Returns:
3133
The deployed policy response as a dictionary.
3234
"""
33-
spec = RequestSpec(
34-
method="GET",
35-
endpoint=_DEPLOYED_POLICY_ENDPOINT,
36-
)
35+
spec = self._deployed_policy_spec()
3736
response = self.request(
3837
spec.method,
3938
url=spec.endpoint,
4039
headers=spec.headers,
41-
scoped="org",
40+
scoped="tenant",
4241
)
4342
return response.json()
4443

44+
@traced(name="automation_ops_get_deployed_policy", run_type="uipath")
4545
async def get_deployed_policy_async(self) -> dict[str, Any]:
4646
"""Retrieve the deployed policy (async).
4747
4848
Returns:
4949
The deployed policy response as a dictionary.
5050
"""
51-
spec = RequestSpec(
52-
method="GET",
53-
endpoint=_DEPLOYED_POLICY_ENDPOINT,
54-
)
51+
spec = self._deployed_policy_spec()
5552
response = await self.request_async(
5653
spec.method,
5754
url=spec.endpoint,
5855
headers=spec.headers,
59-
scoped="org",
56+
scoped="tenant",
6057
)
6158
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)