Skip to content

Use case

Explaining is not recommending

The hard case is not the obvious one. It is the sentence that starts as an explanation and ends as a recommendation, and it is the distinction this detector was trained on.

classifiedmodel outputdeliveredblocked

The situation

A retail banking assistant is asked what an ETF is. Explaining the instrument is education, it is what the assistant is for, and no supervisor objects to it.

Two sentences later the same answer says that given the customer’s balance and their stated goal, a low-cost index fund would suit them. That is a personal recommendation, and in most European jurisdictions it is a regulated activity with suitability obligations attached to it.

What breaks without a check

The model does not know where that line is, and neither does a keyword filter: the words are identical on both sides of it. What changes is whether the sentence is about the instrument or about this customer. Without a check, the first time anyone notices is when the transcript is read back during a review.

Which detectors apply

The policy is data, not code. A reviewer who does not write Python should be able to read this and say whether it is right.

border-code.yaml
# default.yaml reports. bfsi.yaml blocks, which is the reason that file exists.detectors:  regulated_advice:    on_fail: block    threshold: 0.5  disclosure:    on_fail: block          # a missing AI disclosure is itself a finding here  groundedness:    on_fail: flag    always: true            # claims about money get checked every time

The same case in code

This check lives on the output side, because the line between explaining and recommending is crossed by the answer, not the question. A blocked answer turns into a refusal that carries the record id, so the interaction stays resolvable later.

advice_gate.py
out = scan_output(answer, policy) if out.verdict == "block":    # The customer sees a refusal that can be looked up later,    # not the recommendation that triggered it.    return refuse(reference=out.evidence.record_id) # groundedness runs as flag: the text passes, the finding is kept.for finding in out.findings:    if finding.detector_id == "regulated_advice":        review_queue.add(out.evidence.record_id,                         finding.label, finding.score)return out.text

What the stamp holds

That the check ran, what it returned, and against which model revision. If it flagged, the label and the score. If it did not, that is the more useful record: it is evidence that the control was applied to this specific answer, which is the thing a supervisor asks for and the thing almost nobody can produce.

Read by a compliance officer, and eventually a supervisor asking to see the control.

What this does not catch

  • The default policy still ships this detector at the 0.5 threshold with on_fail: flag. The model itself is calibrated at 0.72 on validation, so the shipped default is more permissive than the calibrated operating point.
  • It classifies the text, not the relationship. It cannot know whether your firm holds a licence that makes the advice lawful.
  • It was trained on synthetic examples generated per language. Real customer conversations are messier, and a harder evaluation against real transcripts is outstanding work.
  • It says nothing about whether the advice was good.

The full detector set, or the per-language numbers.