11"""Project a run's job-attachment registry into guardrail attachment references.
22
3- Only the ``llm_as_judge`` validator consumes attachments today, only behind a feature flag,
4- and only when the guardrail's author scoped it to files, so all three gates live here: there
5- is no point building a reference for a validator -- or a guardrail configuration -- that will
6- ignore the result.
7-
8- The runtime no longer resolves attachments to a URL. It only forwards the Orchestrator
9- attachment id, file name and mime type; helix resolves the id through its own Orchestrator
10- client (folder-scoped when the run's folder key is available), so Orchestrator's own access
11- control decides whether the file can be read at all. That also means this module makes no
12- network call of its own any more -- there is nothing here that can fail transiently.
13-
14- Nothing in this module raises. The low-code guardrail node re-raises any exception it sees
15- (see ``guardrail_nodes._create_guardrail_node``), which terminates the agent run — so a
16- malformed attachment must never escape. An unreadable file degrades to "the guardrail
17- evaluates the text payload alone", which is the safer failure direction for a guardrail.
3+ Only the ``llm_as_judge`` validator uses attachments, behind a feature flag, and only when
4+ the guardrail is not scoped to prompts. The runtime forwards id, file name and mime type;
5+ the backend resolves the id through Orchestrator.
6+
7+ Nothing in this module raises: the guardrail node re-raises any exception, which would end
8+ the run over a single malformed attachment.
189"""
1910
2011import logging
2112import uuid
13+ from typing import Any
2214
2315from uipath .core .feature_flags import FeatureFlags
2416from uipath .platform .attachments import Attachment
2517from uipath .platform .guardrails import BuiltInValidatorGuardrail , GuardrailAttachment
2618
2719logger = logging .getLogger (__name__ )
2820
29- #: Kill switch. Names are case-sensitive and the env var is an exact concatenation:
30- #: ``UIPATH_FEATURE_GuardrailAttachmentsEnabled``.
21+ #: Env var: ``UIPATH_FEATURE_GuardrailAttachmentsEnabled`` (case-sensitive).
3122GUARDRAIL_ATTACHMENTS_FEATURE_FLAG = "GuardrailAttachmentsEnabled"
3223
33- #: The only validator that can read file contents today. The backend enforces this too.
3424_LLM_AS_JUDGE = "llm_as_judge"
35-
36- #: Matches the ceiling the validate API enforces; resolving more would be wasted work.
25+ #: Limits enforced by the validate API.
3726_MAX_ATTACHMENTS = 5
38-
39- #: The validate API rejects longer file names with a 400.
4027_MAX_FILE_NAME_LENGTH = 260
41-
42- #: Optional guardrail parameter scoping the evaluation to ``Prompts``, ``Files`` or ``Both``.
43- #: Parameter ids are matched case-insensitively, as the backend does.
28+ #: ``appliesTo`` guardrail parameter; only ``Prompts`` excludes files (default is ``Both``).
4429_APPLIES_TO_PARAMETER = "appliesto"
45-
46- #: The one value that puts files out of scope. Anything else -- including an absent parameter,
47- #: which is every guardrail configured before it existed -- keeps them in, matching the
48- #: backend's default of "Both". Narrowing on an unrecognized value would silently stop
49- #: scanning files for a guardrail whose author never asked for that.
5030_PROMPTS_ONLY = "prompts"
5131
52- #: What the backend can inspect. The runtime only forwards references — the backend decides
53- #: how to read each type, so this set exists to avoid spending an Orchestrator round-trip on a
54- #: file that would be skipped anyway.
32+ #: Types the backend can inspect; anything else is not worth forwarding.
5533SUPPORTED_MIME_TYPES = frozenset (
5634 {
57- # Already text: the backend decodes these and inlines them into the judged payload.
5835 "text/plain" ,
5936 "text/csv" ,
6037 "application/csv" ,
6340 "application/json" ,
6441 "text/xml" ,
6542 "application/xml" ,
66- # Binary: the backend sends these to a vision-capable judge model as content parts.
6743 "application/pdf" ,
6844 "image/png" ,
6945 "image/jpeg" ,
7450
7551
7652def _is_enabled (guardrail : BuiltInValidatorGuardrail ) -> bool :
77- """Both gates: the validator must be the judge and the feature flag must be on."""
7853 if getattr (guardrail , "validator_type" , None ) != _LLM_AS_JUDGE :
7954 return False
8055 return FeatureFlags .is_flag_enabled (
@@ -83,12 +58,6 @@ def _is_enabled(guardrail: BuiltInValidatorGuardrail) -> bool:
8358
8459
8560def _scope_includes_files (guardrail : BuiltInValidatorGuardrail ) -> bool :
86- """Whether the guardrail's ``appliesTo`` parameter puts the run's files in scope.
87-
88- The backend gates on this too, but only after helix has already resolved each id
89- through Orchestrator. Reading it here is what actually avoids sending references --
90- and the Orchestrator lookups they trigger downstream -- for a prompts-only guardrail.
91- """
9261 try :
9362 for parameter in getattr (guardrail , "validator_parameters" , None ) or []:
9463 if str (getattr (parameter , "id" , "" )).lower () != _APPLIES_TO_PARAMETER :
@@ -97,8 +66,6 @@ def _scope_includes_files(guardrail: BuiltInValidatorGuardrail) -> bool:
9766 if isinstance (value , str ):
9867 return value .strip ().lower () != _PROMPTS_ONLY
9968 except Exception :
100- # This module promises never to raise: the caller re-raises, which ends the run. A
101- # parameter list that is not shaped as expected falls back to the backend's default.
10269 logger .debug (
10370 "Could not read the guardrail scope; assuming files apply." , exc_info = True
10471 )
@@ -109,16 +76,10 @@ async def resolve_guardrail_attachments(
10976 job_attachments : dict [str , Attachment ],
11077 guardrail : BuiltInValidatorGuardrail ,
11178) -> list [GuardrailAttachment ]:
112- """Resolve the run's job attachments into references the guardrails API can read .
79+ """Return up to five attachment references for the guardrail, or an empty list .
11380
114- Args:
115- job_attachments: The per-run registry from ``state.inner_state.job_attachments``.
116- guardrail: The guardrail about to be evaluated; gates on its validator type.
117-
118- Returns:
119- Attachment references, or an empty list when the feature is off, the validator
120- cannot use them, the guardrail is scoped to prompts only, or none are of a
121- supported type. Never raises.
81+ Empty when the feature is off, the validator is not the judge, the guardrail is scoped
82+ to prompts, or no attachment is of a supported type. Never raises.
12283 """
12384 if not job_attachments or not _is_enabled (guardrail ):
12485 return []
@@ -129,48 +90,32 @@ async def resolve_guardrail_attachments(
12990 )
13091 return []
13192
132- candidates = [
133- attachment
134- for attachment in job_attachments .values ()
135- if attachment .id is not None
136- and (attachment .mime_type or "" ).lower () in SUPPORTED_MIME_TYPES
137- ][:_MAX_ATTACHMENTS ]
138-
13993 references : list [GuardrailAttachment ] = []
140- for attachment in candidates :
94+ for attachment in job_attachments . values () :
14195 reference = _to_reference (attachment )
14296 if reference is not None :
14397 references .append (reference )
98+ if len (references ) == _MAX_ATTACHMENTS :
99+ break
144100 return references
145101
146102
147- def _to_reference (attachment : Attachment ) -> GuardrailAttachment | None :
148- """Project one job attachment into a reference, or None if it cannot be forwarded."""
149- attachment_id = str (attachment .id )
150- try :
151- uuid .UUID (attachment_id )
152- except (ValueError , AttributeError , TypeError ):
153- # The validate API requires a GUID; helix has nothing to resolve otherwise.
154- logger .warning (
155- "Attachment '%s' has a non-UUID id; skipping for guardrail inspection." ,
156- getattr (attachment , "full_name" , "?" ),
157- )
158- return None
103+ def _to_reference (attachment : Any ) -> GuardrailAttachment | None :
104+ """Build one reference, or None when the attachment cannot be forwarded."""
159105 try :
160- # The validate API caps file names at 260 characters; a longer name would be a 400
161- # for the whole request, and this is a display label, not an identifier.
162- file_name = (attachment .full_name or "" )[:_MAX_FILE_NAME_LENGTH ]
106+ mime_type = str (getattr (attachment , "mime_type" , "" ) or "" )
107+ if mime_type .lower () not in SUPPORTED_MIME_TYPES :
108+ return None
109+ attachment_id = str (uuid .UUID (str (getattr (attachment , "id" , None ))))
110+ file_name = str (getattr (attachment , "full_name" , "" ) or "" )
163111 return GuardrailAttachment (
164112 id = attachment_id ,
165- file_name = file_name ,
166- mime_type = attachment . mime_type ,
113+ file_name = file_name [: _MAX_FILE_NAME_LENGTH ] ,
114+ mime_type = mime_type ,
167115 )
168116 except Exception :
169- # Deliberately broad: this module promises never to raise, and the caller
170- # re-raises, which would end the run over a single malformed attachment.
171117 logger .warning (
172- "Could not build a guardrail reference for attachment '%s'; "
173- "the guardrail will evaluate without it." ,
118+ "Skipping attachment '%s' for guardrail inspection: invalid reference." ,
174119 getattr (attachment , "full_name" , "?" ),
175120 exc_info = True ,
176121 )
0 commit comments