1717from uipath .agent .models .agent import (
1818 AgentInternalToolResourceConfig ,
1919)
20- from uipath .core .feature_flags import FeatureFlags
2120from uipath .eval .mocks import mockable
2221from uipath .platform import UiPath
23- from uipath .platform .semantic_proxy import (
24- PiiDetectionRequest ,
25- PiiDetectionResponse ,
26- PiiDocument ,
27- PiiEntityThreshold ,
28- PiiFile ,
29- rehydrate_from_pii_response ,
30- )
3122from uipath .runtime .errors import UiPathErrorCategory
3223
3324from uipath_langchain .agent .exceptions import (
3930 build_file_content_blocks_for ,
4031)
4132from uipath_langchain .agent .react .jsonschema_pydantic_converter import create_model
33+ from uipath_langchain .agent .tools .internal_tools .pii_masker import PiiMasker
4234from uipath_langchain .agent .tools .structured_tool_with_argument_properties import (
4335 StructuredToolWithArgumentProperties ,
4436)
5648 "based on the extracted information."
5749)
5850
59- _PII_MASKING_FEATURE_FLAG = "File-Pii-Masking-Enabled"
60-
61-
62- def is_pii_policy_enabled (policy : dict [str , Any ] | None ) -> bool :
63- """Determine whether PII detection should run.
64-
65- Two gates (both must allow):
66-
67- 1. Local kill-switch — the ``File-Pii-Masking-Enabled`` feature flag
68- (defaults to ``True``; override via ``FeatureFlags.configure_flags``
69- or ``UIPATH_FEATURE_File-Pii-Masking-Enabled`` env var).
70- 2. Platform policy — ``data.container.pii-in-flight-agents`` from the
71- AutomationOps deployed-policy response.
72- """
73- if not FeatureFlags .is_flag_enabled (_PII_MASKING_FEATURE_FLAG , default = True ):
74- return False
75- if not policy :
76- return False
77- container = policy .get ("data" , {}).get ("container" , {})
78- return bool (container .get ("pii-in-flight-agents" , False ))
79-
80-
81- def _build_entity_thresholds_from_policy (
82- policy : dict [str , Any ] | None ,
83- ) -> list [PiiEntityThreshold ]:
84- """Extract enabled entity thresholds from the deployed policy.
85-
86- Filters ``data.pii-entity-table`` to entries where ``pii-entity-is-enabled``
87- is true and maps them to ``PiiEntityThreshold`` objects.
88- """
89- if not policy :
90- return []
91- table = policy .get ("data" , {}).get ("pii-entity-table" , [])
92- thresholds : list [PiiEntityThreshold ] = []
93- for entry in table :
94- if not entry .get ("pii-entity-is-enabled" , False ):
95- continue
96- category = entry .get ("pii-entity-category" )
97- confidence = entry .get ("pii-entity-confidence-threshold" )
98- if category is None or confidence is None :
99- continue
100- thresholds .append (
101- PiiEntityThreshold (
102- category = category ,
103- confidence_threshold = confidence ,
104- )
105- )
106- return thresholds
107-
108-
109- def _rename_for_masking (file : FileInfo , redacted_url : str ) -> FileInfo :
110- """Return a new ``FileInfo`` pointing at the redacted URL with a ``pii_masked_`` name prefix."""
111- if "." in file .name :
112- base , ext = file .name .rsplit ("." , 1 )
113- new_name = f"pii_masked_{ base } .{ ext } "
114- else :
115- new_name = f"pii_masked_{ file .name } "
116- return FileInfo (url = redacted_url , name = new_name , mime_type = file .mime_type )
117-
118-
119- async def _apply_pii_masking (
120- client : UiPath ,
121- policy : dict [str , Any ] | None ,
122- analysis_task : str ,
123- files : list [FileInfo ],
124- ) -> tuple [str , list [FileInfo ], PiiDetectionResponse ]:
125- """Run PII detection and return a masked prompt, redacted files, and the raw response.
126-
127- The returned response is retained so the LLM output can be rehydrated via
128- :func:`rehydrate_from_pii_response` after inference.
129- """
130- pii_request = PiiDetectionRequest (
131- documents = [PiiDocument (id = "user-prompt" , role = "user" , document = analysis_task )],
132- files = [
133- PiiFile (
134- file_name = f .name ,
135- file_url = f .url ,
136- file_type = f .name .rsplit ("." , 1 )[- 1 ].lower () if "." in f .name else "" ,
137- )
138- for f in files
139- ],
140- entity_thresholds = _build_entity_thresholds_from_policy (policy ) or None ,
141- )
142- pii_result = await client .semantic_proxy .detect_pii_async (pii_request )
143- logger .info (
144- "PII detection completed: %d document entities, %d file entities" ,
145- sum (len (d .pii_entities ) for d in pii_result .response ),
146- sum (len (f .pii_entities ) for f in pii_result .files ),
147- )
148-
149- masked_prompt = analysis_task
150- for doc in pii_result .response :
151- if doc .id == "user-prompt" :
152- if doc .masked_document != analysis_task :
153- logger .info (
154- "User prompt masked (%d entities replaced)" ,
155- len (doc .pii_entities ),
156- )
157- masked_prompt = doc .masked_document
158- break
159-
160- redacted_by_name = {f .file_name : f .file_url for f in pii_result .files }
161- if redacted_by_name :
162- masked_files = [
163- _rename_for_masking (f , redacted_by_name .get (f .name , f .url )) for f in files
164- ]
165- logger .info ("Renamed %d file(s) with pii_masked_ prefix" , len (masked_files ))
166- else :
167- masked_files = files
168-
169- return masked_prompt , masked_files , pii_result
170-
17151
17252def create_analyze_file_tool (
17353 resource : AgentInternalToolResourceConfig , llm : BaseChatModel
@@ -213,14 +93,18 @@ async def tool_fn(**kwargs: Any):
21393 except Exception :
21494 logger .exception ("Failed to fetch deployed policy" )
21595
216- pii_result : PiiDetectionResponse | None = None
217- if client is not None and is_pii_policy_enabled (policy ):
96+ masker : PiiMasker | None = None
97+ if client is not None and PiiMasker .is_policy_enabled (policy ):
98+ masker = PiiMasker (client , policy )
21899 try :
219- analysis_task , files , pii_result = await _apply_pii_masking (
220- client , policy , analysis_task , files
221- )
222- except Exception as e :
223- logger .error ("PII detection raised: %r" , e , exc_info = True )
100+ analysis_task , files = await masker .apply (analysis_task , files )
101+ except Exception as exc :
102+ raise AgentRuntimeError (
103+ code = AgentRuntimeErrorCode .UNEXPECTED_ERROR ,
104+ title = "PII masking failed" ,
105+ detail = f"PII detection raised: { exc !r} " ,
106+ category = UiPathErrorCategory .SYSTEM ,
107+ ) from exc
224108
225109 try :
226110 human_message = HumanMessage (content = analysis_task )
@@ -244,12 +128,9 @@ async def tool_fn(**kwargs: Any):
244128
245129 analysis_result = extract_text_content (result )
246130
247- if pii_result is not None :
131+ if masker is not None :
248132 try :
249- rehydrated = rehydrate_from_pii_response (analysis_result , pii_result )
250- if rehydrated != analysis_result :
251- logger .info ("Rehydrated LLM response with PII entities" )
252- analysis_result = rehydrated
133+ analysis_result = masker .rehydrate (analysis_result )
253134 except Exception :
254135 logger .exception ("Failed to rehydrate LLM response" )
255136
0 commit comments