1111from opentelemetry import trace
1212
1313from app .core .config import settings
14- from app .models .llm .request import Validator
14+ from app .models .llm .request import InlineValidator , Validator
1515
1616logger = logging .getLogger (__name__ )
1717tracer = trace .get_tracer (__name__ )
1818
1919
2020GUARDRAILS_PROXY_TIMEOUT_SECONDS = 30.0
2121
22+ # The service logs validator_log.stage from the spec and defaults to input.
23+ GUARDRAIL_STAGE_INPUT = "input"
24+ GUARDRAIL_STAGE_OUTPUT = "output"
25+
2226
2327def _guardrails_headers (
2428 organization_id : int | None , project_id : int | None
@@ -148,7 +152,7 @@ def applied(self) -> bool:
148152def apply_guardrails (
149153 * ,
150154 text : str ,
151- validators : list [Validator ] | None ,
155+ validators : Sequence [Validator | InlineValidator ] | None ,
152156 job_id : UUID ,
153157 project_id : int | None ,
154158 organization_id : int | None ,
@@ -164,30 +168,45 @@ def apply_guardrails(
164168
165169 Args:
166170 text: Primary text to validate. Sent as `input` to the service.
167- validators: Validator references (config IDs). When None/empty the
168- function short-circuits with a no-op outcome.
171+ validators: Config-ID references and/or inline specs, in run order.
172+ Entries with is_enabled=False are skipped; when nothing enabled
173+ remains the function short-circuits with a no-op outcome.
169174 output_text: When provided, sent as `output` to the service for
170175 output-guardrail validators that compare input/output pairs.
171176 Also routes the IDs through the output-config fetch path.
172177 """
173- if not validators :
178+ enabled : list [Validator | InlineValidator ] = []
179+ for entry in validators or []:
180+ if entry .is_enabled :
181+ enabled .append (entry )
182+ if not enabled :
174183 return GuardrailsOutcome (
175184 safe_text = text , error = None , bypassed = False , rephrase_needed = False , raw = {}
176185 )
177186
178187 is_output = output_text is not None
179- input_cfgs , output_cfgs = list_validators_config (
180- organization_id = organization_id ,
181- project_id = project_id ,
182- input_validator_configs = None if is_output else validators ,
183- output_validator_configs = validators if is_output else None ,
184- )
185- resolved = output_cfgs if is_output else input_cfgs
188+ references : list [Validator ] = []
189+ for entry in enabled :
190+ if isinstance (entry , Validator ):
191+ references .append (entry )
192+
193+ fetched : list [dict [str , Any ]] = []
194+ if references :
195+ input_cfgs , output_cfgs = list_validators_config (
196+ organization_id = organization_id ,
197+ project_id = project_id ,
198+ input_validator_configs = None if is_output else references ,
199+ output_validator_configs = references if is_output else None ,
200+ )
201+ fetched = output_cfgs if is_output else input_cfgs
202+
203+ stage = GUARDRAIL_STAGE_OUTPUT if is_output else GUARDRAIL_STAGE_INPUT
204+ resolved = _prepare_validators (enabled , fetched , stage , job_id )
186205 if not resolved :
187206 logger .info (
188207 f"[apply_guardrails] No validator configs resolved upstream; skipping "
189208 f"POST /guardrails. job_id={ job_id } , requested_ids="
190- f"{ [str (v .validator_config_id ) for v in validators ]} , "
209+ f"{ [str (v .validator_config_id ) for v in references ]} , "
191210 f"is_output={ is_output } "
192211 )
193212 return GuardrailsOutcome (
@@ -237,6 +256,40 @@ def apply_guardrails(
237256 )
238257
239258
259+ def _prepare_validators (
260+ entries : Sequence [Validator | InlineValidator ],
261+ fetched_rows : list [dict [str , Any ]],
262+ stage : str ,
263+ job_id : UUID ,
264+ ) -> list [dict [str , Any ]]:
265+ """Build the POST `validators` list in blob order, since the service runs
266+ them in the order received while the fetch returns rows by created_at."""
267+ rows_by_id = {str (row .get ("id" )): row for row in fetched_rows }
268+
269+ prepared : list [dict [str , Any ]] = []
270+ for entry in entries :
271+ if isinstance (entry , InlineValidator ):
272+ # The service's validator configs are extra="forbid", so is_enabled must go.
273+ spec = entry .model_dump (mode = "json" , exclude = {"is_enabled" })
274+ # Service's on_fail is non-nullable; omitting it applies its default.
275+ if spec .get ("on_fail" ) is None :
276+ spec .pop ("on_fail" , None )
277+ spec ["stage" ] = stage
278+ prepared .append (spec )
279+ continue
280+
281+ config_id = str (entry .validator_config_id )
282+ row = rows_by_id .get (config_id )
283+ if row is None :
284+ logger .warning (
285+ f"[_prepare_validators] Validator config not returned upstream; "
286+ f"dropping it | job_id={ job_id } , validator_config_id={ config_id } "
287+ )
288+ continue
289+ prepared .append (row )
290+ return prepared
291+
292+
240293def summarize_validator_results (outcome : GuardrailsOutcome ) -> list [dict [str , Any ]]:
241294 """Per-validator name/outcome/text summary, extracted from the raw
242295 guardrails service response, for use in llm_call metadata."""
@@ -259,7 +312,7 @@ def summarize_validator_results(outcome: GuardrailsOutcome) -> list[dict[str, An
259312
260313def run_guardrails_validation (
261314 input_text : str ,
262- guardrail_config : Sequence [Validator | dict [str , Any ]],
315+ guardrail_config : Sequence [Validator | InlineValidator | dict [str , Any ]],
263316 job_id : UUID ,
264317 project_id : int | None ,
265318 organization_id : int | None ,
@@ -282,12 +335,13 @@ def run_guardrails_validation(
282335 Returns:
283336 JSON response from the guardrails service with validation results.
284337 """
285- validators = [
286- validator .model_dump (mode = "json" )
287- if isinstance (validator , Validator )
288- else validator
289- for validator in guardrail_config
290- ]
338+ validators : list [dict [str , Any ]] = []
339+ for validator in guardrail_config :
340+ if isinstance (validator , dict ):
341+ validators .append (validator )
342+ else :
343+ # Unknown to the service's extra="forbid" validator schemas.
344+ validators .append (validator .model_dump (mode = "json" , exclude = {"is_enabled" }))
291345
292346 payload = {
293347 "request_id" : str (job_id ),
0 commit comments