Class: Insika::Safety::OutputValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/safety/output_validator.rb

Overview

Post-turn output validator. Runs as an after_task hook: with the FINAL assistant text assembled, it does a richer check than the stream filter can — an unverified discount/price promise, a tone slip — and FLAGS it (audit) rather than blocking. It is honest about the streaming limit for a streaming agent the text is already out the door, so this is detection, not prevention; true pre-emission blocking needs streaming:false (a per-profile override left for a later slice).

Contract: as an after_task hook it receives the TurnState (the :task subject), appends any finding to state.guardrail_flags, and returns the state unchanged. The Executor (single emitter) turns each flag into a :guardrail_flagged event after the turn — the hook itself never emits.

Two tiers, like the input side:

1. deterministic — residual PII/secret in the final text (a belt to the
 stream filter's suspenders) via Detectors;
2. LLM validator (opt-in, same moderator model) — promise/tone judgment the
 regex can't make. Pure over an injected `ask`; fail-open on any error.

Instance Method Summary collapse

Constructor Details

#initialize(ask_factory: nil, grounding: nil) ⇒ OutputValidator

ask_factory (optional): ->(config) { ->(prompt)text | nil }, built by the Safety::Factory from the utility_model. nil = deterministic only. grounding (optional): a GroundingValidator — its step runs FIRST in #call, before the config.output gate (D9: grounding is evidence integrity, independent of the guardrails opt-in).



33
34
35
36
# File 'lib/insika/safety/output_validator.rb', line 33

def initialize(ask_factory: nil, grounding: nil)
  @ask_factory = ask_factory
  @grounding = grounding
end

Instance Method Details

#call(state) ⇒ Object

after_task hook body. Idempotent and defensive: never raises out (a hook error must not fail a committed turn). Grounding runs BEFORE the output gate so an agent with guardrails off and grounding on still gets the check.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/insika/safety/output_validator.rb', line 41

def call(state)
  state = @grounding&.call(state) || state
  config = Config.from_profile(state.profile)
  return state unless config.output

  text = state.response_content.to_s
  return state if text.empty?

  flags = []
  flags.concat(deterministic_flags(text, config.corpus))
  flags.concat(llm_flags(text, config)) if config.moderator?

  state.guardrail_flags = Array(state.guardrail_flags) + flags unless flags.empty?
  state
rescue StandardError
  state # fail-open: auditing must never break a completed turn
end