3333import logging
3434import math
3535from collections import Counter
36- from typing import List , Optional , TypedDict
36+ from typing import List , Literal , Optional , TypedDict
3737
3838from nemoguardrails import RailsConfig
3939from nemoguardrails .actions import action
40+ from nemoguardrails .actions .rail_outcome import RailOutcome , TransformTarget
4041
4142log = logging .getLogger (__name__ )
4243
@@ -54,6 +55,9 @@ class ContextBloatResult(TypedDict):
5455 metrics : dict
5556
5657
58+ ContextBloatSource = Literal ["input" , "retrieval" ]
59+
60+
5761def _stratified_sample (text : str , sample_chars : int ) -> str :
5862 third = sample_chars // 3
5963 mid = len (text ) // 2
@@ -162,21 +166,48 @@ def _check_repetition(text: str, cfg, detections: List[str], metrics: dict) -> O
162166 return None
163167
164168
169+ def _context_bloat_outcome (source : ContextBloatSource , original_text : str , result : ContextBloatResult ) -> RailOutcome :
170+ targets = {
171+ "input" : TransformTarget .USER_MESSAGE ,
172+ "retrieval" : TransformTarget .RELEVANT_CHUNKS ,
173+ }
174+
175+ metadata = {
176+ "is_bloat" : result ["is_bloat" ],
177+ "action" : result ["action" ],
178+ "detections" : result ["detections" ],
179+ "metrics" : result ["metrics" ],
180+ }
181+ if result ["is_bloat" ] and result ["action" ] == "reject" :
182+ return RailOutcome .block (reason = result ["reason" ], metadata = metadata )
183+ if result ["is_bloat" ] and result ["action" ] == "truncate" and result ["text" ] != original_text :
184+ return RailOutcome .transform (
185+ [(targets [source ], result ["text" ])],
186+ reason = result ["reason" ],
187+ metadata = metadata ,
188+ )
189+ return RailOutcome .allow (reason = result ["reason" ], metadata = metadata )
190+
191+
165192@action ()
166- async def context_bloat_detection (text : str , config : RailsConfig ) -> ContextBloatResult :
193+ async def context_bloat_detection (source : ContextBloatSource , text : str , config : RailsConfig ) -> RailOutcome :
167194 """Detect context-bloat / context-manipulation attacks.
168195 Check order is cheapest first to enable early-exit.
169196
170197 Args:
198+ source: Whether the text came from input or retrieved chunks.
171199 text: The text to inspect (joined chunks or user message).
172200 config: RailsConfig with rails.config.context_bloat_detection settings.
173201
174202 Returns:
175- ContextBloatResult with is_bloat flag, processed text, reason, metrics .
203+ The allow, block, or transform verdict with detection evidence .
176204 """
205+ if source not in ("input" , "retrieval" ):
206+ raise ValueError ("source must be either 'input' or 'retrieval'" )
177207 _validate_config (config )
178208 cfg = config .rails .config .context_bloat_detection
179209
210+ original_text = text
180211 char_count = len (text ) if text else 0
181212 detections : List [str ] = []
182213 metrics : dict = {"chars" : char_count }
@@ -185,13 +216,17 @@ async def context_bloat_detection(text: str, config: RailsConfig) -> ContextBloa
185216 detections .append ("size_cap_exceeded" )
186217 log .info (f"context bloat detected: size_cap_exceeded | chars={ char_count } " )
187218 if cfg .action == "reject" :
188- return ContextBloatResult (
189- is_bloat = True ,
190- action = cfg .action ,
191- text = text ,
192- reason = "size_cap_exceeded" ,
193- detections = detections ,
194- metrics = metrics ,
219+ return _context_bloat_outcome (
220+ source ,
221+ original_text ,
222+ ContextBloatResult (
223+ is_bloat = True ,
224+ action = cfg .action ,
225+ text = text ,
226+ reason = "size_cap_exceeded" ,
227+ detections = detections ,
228+ metrics = metrics ,
229+ ),
195230 )
196231 if cfg .action == "truncate" :
197232 text = text [: cfg .max_chars ]
@@ -200,17 +235,21 @@ async def context_bloat_detection(text: str, config: RailsConfig) -> ContextBloa
200235 for check in (_check_entropy , _check_longest_run , _check_repetition ):
201236 result = check (text , cfg , detections , metrics )
202237 if result is not None :
203- return result
238+ return _context_bloat_outcome ( source , original_text , result )
204239
205240 is_bloat = bool (detections )
206241 reason = ", " .join (detections ) if detections else None
207242 if is_bloat :
208243 log .info (f"context bloat detected: { reason } | metrics={ metrics } " )
209- return ContextBloatResult (
210- is_bloat = is_bloat ,
211- action = cfg .action ,
212- text = text ,
213- reason = reason ,
214- detections = detections ,
215- metrics = metrics ,
244+ return _context_bloat_outcome (
245+ source ,
246+ original_text = original_text ,
247+ result = ContextBloatResult (
248+ is_bloat = is_bloat ,
249+ action = cfg .action ,
250+ text = text ,
251+ reason = reason ,
252+ detections = detections ,
253+ metrics = metrics ,
254+ ),
216255 )
0 commit comments