Skip to main content

Output Controls

UPL Filter

upl-filter guards against the unauthorized practice of law (UPL) in model answers. It blocks answers that contain phrases you list, such as you should sue, and puts This is not legal advice. in front of answers that sound like legal advice.

It matches text, not legal meaning, and runs only with stream: false. Its educational option adds a notice but leaves the original advice in the answer.

Configuration​

Save this as policy-config.yaml:

policy-config.yaml
pack:
  name: upl-filter-example-1
  version: "1.0.0"
  enabled: true

policies:
  chain:
    - upl-filter

policy:
  upl-filter:
    blocked_patterns:
      - you should sue
      - file this motion
      - sign here
    require_disclaimer: true
    rewrite_to_educational: false

Verify the control through the gateway​

verdictan policy test does not run this control. Apply the file to a test agent with the config-first workflow, use a controlled provider response, and send requests with stream: false:

Upstream textResult
You should sue immediately.HTTP 400 and upl.blocked
You should retain counsel for this matter.HTTP 200, disclaimer added, upl.disclaimer_injected
Courts have different jurisdictions.HTTP 200, unchanged, upl.clean
This is not legal advice. You should retain counsel.HTTP 200, no second disclaimer, upl.clean

Then run the blocked case with rewrite_to_educational: true: the result is HTTP 200 with the educational notice and upl.rewritten_to_educational. For each request ID, check the nested reason code in verdictan events tail --since 10m --json and the body that the client received. The policy result alone does not prove that the prefix fits the returned JSON shape.

Fields​

FieldTypeDefaultDescription
blocked_patternsstring[]Built-in listSubstrings that block. Without the field: you should sue, file this motion, and sign here. Your list replaces them.
require_disclaimerbooleantrueAdds This is not legal advice. to an answer that sounds like legal advice
rewrite_to_educationalbooleanfalseAdds an educational notice to a matched answer instead of blocking it

blocked_patterns: [] means an empty block list. It does not bring back the built-in phrases. Leave the field out to use them.

How it works​

  1. The gateway takes the text of the buffered response. See Supported response shapes.
  2. It lowercases the text and looks for each blocked pattern as a substring.
  3. A match with rewrite_to_educational: false blocks with upl.blocked: HTTP 400.
  4. A match with rewrite_to_educational: true adds the educational notice and allows with upl.rewritten_to_educational.
  5. Without a match, the text sounds like advice if it contains you should, legal advice, or retain counsel, or both file and court.
  6. Advice with require_disclaimer: true gets This is not legal advice. in front, with upl.disclaimer_injected, unless the text already starts with that sentence.
  7. Otherwise, the answer is allowed with upl.clean.

The educational notice is this fixed text:

Note: The following is for educational purposes only and does not constitute legal advice. Please consult a qualified attorney for advice specific to your situation.

Each prefix ends with two newlines, followed by the original text.

Supported response shapes​

Text is read from:

  • Chat Completions: choices[*].message.content
  • Responses: a top-level output string
  • Responses: output[*].content[*].text items of type output_text

Prefixes are added to:

  • Each string in choices[*].message.content
  • Each output[*].content[*].text string of type output_text

A match in one segment decides for the whole response, and the prefix goes on every supported segment. A top-level output string is read but cannot get a prefix.

Phase and verdicts​

ItemValue
Input phaseNot checked
Output phaseThe buffered response text
Verdictsallow, block
Blockupl.blocked
Educational noticeupl.rewritten_to_educational
Disclaimerupl.disclaimer_injected
No changeupl.clean

A block records blocked: true. An educational notice records rewritten: true and original_would_block: true. A disclaimer or clean result records require_disclaimer, looks_like_legal_advice, and disclaimer_injected. The matched phrase is not recorded.

Examples​

Keep the built-in blocks​

Inside policy-config.yaml
policy:
  upl-filter:
    require_disclaimer: true

Extend the block list​

Your list replaces the built-in phrases, so repeat the ones you need:

Inside policy-config.yaml
policy:
  upl-filter:
    blocked_patterns:
      - you should sue
      - file this motion
      - sign here
      - execute this affidavit
    require_disclaimer: true
    rewrite_to_educational: false

Replace the block with a prefix​

Inside policy-config.yaml
policy:
  upl-filter:
    blocked_patterns:
      - you should sue
      - file this motion
      - sign here
    require_disclaimer: true
    rewrite_to_educational: true

The original text is still delivered after the notice. Use this only with approval from your legal owner.

Limits and failure behavior​

  • With stream: true, the control does not run.
  • Matching is by substring, without word boundaries, so quoted, historical, or educational text can match. Test for false positives.
  • rewrite_to_educational changes only the blocked-phrase path, not the disclaimer path.
  • The duplicate check looks only at the start of the text.
  • The educational notice leaves the original advice in place. Keep the default block if the text must not be delivered.
  • An unsupported or invalid JSON body is returned unchanged.
  • Your legal and compliance owners must approve the phrase list and disclaimer. The built-in strings are not legal advice for any jurisdiction.
  • A when condition does not limit this control. Use routes.
ProblemCauseAction
A built-in phrase does not blockYour blocked_patterns, maybe [], replaced the list.Leave the field out or include each phrase.
Unexpected matchesSubstring matching ignores case and word boundaries.Use longer phrases and add negative tests.
Two disclaimersThe answer did not start with the exact sentence.Use the same sentence, or remove the copy upstream.
upl.disclaimer_injected but a top-level output string is unchangedThat shape cannot get a prefix.Use the Responses output array shape.
A streamed answer did not changeThe control does not run on streams.Set stream: false.

Next steps​