Rejects LLM output whose detected Tone is not in an allowed set.
An OutputGuardrail only: it checks what the model says, not what the user sends. Detection is a fixed keyword heuristic with no model call, so it is fast and deterministic, and it is crude. For a judgement that understands context, use LLMToneGuardrail instead.
The heuristic runs over a normalised copy of the text: Unicode NFKD (fullwidth letters and punctuation become ASCII, compatibility spaces such as a non-breaking space become an ASCII space), with every combining mark and every format character (zero-width space and joiners, byte-order mark, soft hyphen) removed, then lower-cased with Locale.ROOT (so the JVM's default locale has no effect) and the Turkish dotless ı read as i. So HI, Hİ, HI and h<U+200B>i are all hi; an accented spelling such as hí is too.
Detection looks at the whole text, line breaks included, and takes the first rule that applies, in this order:
- Tone.Excited: the text contains
!somewhere and some piece of it is "short". This is a rough token-count heuristic, not sentence detection: the text is cut at every.,!and?, each piece is split on ASCII whitespace, and a piece that yields fewer than five tokens is short. The token count depends on spacing as well as on words: whitespace at the start of a piece (such as the space after a delimiter) adds an empty token, a space that normalisation does not turn into an ASCII space (such as the line separator U+2028) does not separate tokens, and a piece with no words at all (the gap inside a run such as...or?!, or a space or line break after the final!) is short, though completely empty pieces at the very end of the text are dropped. The outcome therefore follows no fixed word count: whether a sentence of four or so words counts as short depends on the punctuation and spacing around it, and text made only of long sentences can still come out Excited. Treat the rule as a coarse signal. It wins over every keyword below. - Tone.Professional: contains
please,thank you,kindly,regardsorsincerely. - Tone.Casual: contains
hey,cool,awesome,yeahornah. - Tone.Friendly: contains
hi,hello,thanksorappreciate. - Tone.Formal: contains
furthermore,moreover,consequentlyortherefore. - Tone.Neutral: none of the above, including empty text.
Keywords are matched at regular-expression word boundaries (\b), so pleased is not please, though hi-fi does contain hi. A text with keywords of several tones is classified by the earliest rule above, not by how many keywords it has. Because the lists are short and in English, text in another language, or in none of these registers, usually comes out Neutral; allow Tone.Neutral (or use allowAll) if that should pass.
On a mismatch validate returns a org.llm4s.error.ValidationError for the field output, whose detail names the detected tone and the allowed ones, for example Output tone (Casual) not allowed. Allowed tones: Professional.
Value parameters
- allowedTones
-
The tones that pass. An empty set rejects everything;
Tone.allaccepts everything.
Attributes
- Example
-
import org.llm4s.agent.guardrails.builtin.{ Tone, ToneValidator } // ready-made sets val customerFacing = ToneValidator.professionalOrFriendly // or choose the allowed tones yourself val calm = ToneValidator(Set(Tone.Professional, Tone.Formal, Tone.Neutral)) calm.validate("Please find the report attached.") // Right(...): Professional calm.validate("Hey, that is cool.") // Left(ValidationError): Casual is not allowed agent.run(query, tools, outputGuardrails = Seq(customerFacing)) - Companion
- object
- Graph
-
- Supertypes