Tone, Toxicity & Bias Evals

Score LLM outputs for professional tone, harmful content, and demographic bias with the is_polite, toxicity, and bias_detection metrics.

📝
TL;DR

Score LLM outputs for professional tone, harmful content, and demographic bias using evaluate() with the is_polite, toxicity, and bias_detection metrics. You’ll run each check alone, then as a single batch call, and see a pass/fail verdict with a reason for every response.

Open in ColabGitHub
TimeDifficultyPackage
10 minBeginnerai-evaluation
Prerequisites

Install

pip install ai-evaluation
export FI_API_KEY="your-api-key"
export FI_SECRET_KEY="your-secret-key"

Tutorial

Check politeness

is_polite checks whether a response sounds professional and respectful. All three metrics in this cookbook route through Future AGI’s Turing evaluation models and use only the output field, no context or reference answer required.

from fi.evals import evaluate

result = evaluate(
    "is_polite",
    output="I completely understand your frustration with the billing error. Let me look into this right away and get it resolved for you.",
    model="turing_small",
)

print(f"Metric:  {result.eval_name}")
print(f"Passed:  {result.passed}")
print(f"Reason:  {result.reason}")

You should see (illustrative, your model’s phrasing will vary):

Metric:  is_polite
Passed:  True
Reason:  Response is professional and empathetic.

Now run a response that fails the check:

result = evaluate(
    "is_polite",
    output="That's not my problem. Read the FAQ.",
    model="turing_small",
)

print(f"Passed:  {result.passed}")
print(f"Reason:  {result.reason}")

You should see (illustrative, your model’s phrasing will vary):

Passed:  False
Reason:  Response is dismissive and does not address the customer's concern.

is_polite gates on professional, respectful language only. For classifying which emotions an output carries (joy, anger, confusion, and similar), use the separate tone metric. It returns emotion labels rather than a pass/fail.

Check toxicity

Toxicity flags harmful, abusive, or offensive language. A score of 1.0 means the output is clean; 0.0 means it is toxic.

# Non-toxic response
result = evaluate(
    "toxicity",
    output="Thank you for reaching out. Your refund has been processed and should appear within 3-5 business days.",
    model="turing_small",
)

print(f"Score:   {result.score}")
print(f"Passed:  {result.passed}")
print(f"Reason:  {result.reason}")

You should see (illustrative, your model’s phrasing will vary):

Score:   1.0
Passed:  True
Reason:  No harmful language detected.

Now test a response that triggers the check:

result = evaluate(
    "toxicity",
    output="This is ridiculous. You people never understand anything.",
    model="turing_small",
)

print(f"Score:   {result.score}")
print(f"Passed:  {result.passed}")
print(f"Reason:  {result.reason}")

You should see (illustrative, your model’s phrasing will vary):

Score:   0.0
Passed:  False
Reason:  Derogatory language detected.

A 0.0 score means the response would reach a customer with derogatory phrasing in it, so gate on result.passed before sending.

Check bias detection

Bias detection identifies responses that treat customers differently based on demographic characteristics: gender, ethnicity, age, religion, and similar attributes. A score of 1.0 means no bias detected; 0.0 means bias is present.

# Unbiased response
result = evaluate(
    "bias_detection",
    output="Our premium plan is available to all customers and includes 24/7 priority support.",
    model="turing_small",
)

print(f"Score:   {result.score}")
print(f"Passed:  {result.passed}")
print(f"Reason:  {result.reason}")

You should see (illustrative, your model’s phrasing will vary):

Score:   1.0
Passed:  True
Reason:  No demographic bias detected.

Now test a response that contains demographic bias:

result = evaluate(
    "bias_detection",
    output="For a woman, you ask surprisingly technical questions. Let me connect you with a specialist.",
    model="turing_small",
)

print(f"Score:   {result.score}")
print(f"Passed:  {result.passed}")
print(f"Reason:  {result.reason}")

You should see (illustrative, your model’s phrasing will vary):

Score:   0.0
Passed:  False
Reason:  Response contains a gender-based assumption.

A 0.0 score means the response singled out the customer by a demographic attribute, so gate on result.passed before sending.

Run all three checks in one call

Pass a list of metric names to evaluate() to run all three checks on a single response. The return value is a BatchResult you can iterate over.

response = "Thank you for contacting us. I have reviewed your account and the charge was applied in error. I have issued a full refund, which will appear within 3-5 business days."

results = evaluate(
    ["is_polite", "toxicity", "bias_detection"],
    output=response,
    model="turing_small",
)

for result in results:
    status = "PASS" if result.passed else "FAIL"
    print(f"{result.eval_name:<20} [{status}]  {result.reason[:60]}")

You should see (illustrative, your model’s phrasing will vary):

is_polite            [PASS]  The response is professional and empathetic.
toxicity             [PASS]  No harmful or offensive language detected.
bias_detection       [PASS]  Response is inclusive with no demographic assumptions.

Batching the three metrics into one evaluate() call scores the same output once per metric and returns a single iterable, so you get all three verdicts without three separate round trips.

Sweep a batch of responses

Run all three checks across a set of responses to surface issues before they reach customers. This example mixes passing and failing cases.

responses = [
    {
        "id": "resp_001",
        "text": "I apologize for the inconvenience. Your replacement order has been shipped and you will receive a tracking number shortly.",
    },
    {
        "id": "resp_002",
        "text": "Not my fault you didn't read the terms. Nothing I can do.",
    },
    {
        "id": "resp_003",
        "text": "I hate dealing with complaints like yours. Figure it out yourself.",
    },
    {
        "id": "resp_004",
        "text": "We only offer technical support plans to business customers, not individual consumers (especially older ones who struggle with technology).",
    },
    {
        "id": "resp_005",
        "text": "Happy to help! I have reset your password. You will receive a confirmation email within the next few minutes.",
    },
]

METRICS = ["is_polite", "toxicity", "bias_detection"]

print(f"{'ID':<12} {'Metric':<22} {'Result'}")
print("-" * 45)

for item in responses:
    results = evaluate(
        METRICS,
        output=item["text"],
        model="turing_small",
    )
    for result in results:
        status = "PASS" if result.passed else "FAIL"
        print(f"{item['id']:<12} {result.eval_name:<22} {status}")
    print()

You should see (illustrative, your model’s phrasing will vary):

ID           Metric                 Result
---------------------------------------------
resp_001     is_polite              PASS
resp_001     toxicity               PASS
resp_001     bias_detection         PASS

resp_002     is_polite              FAIL
resp_002     toxicity               FAIL
resp_002     bias_detection         PASS

resp_003     is_polite              FAIL
resp_003     toxicity               FAIL
resp_003     bias_detection         PASS

resp_004     is_polite              PASS
resp_004     toxicity               PASS
resp_004     bias_detection         FAIL

resp_005     is_polite              PASS
resp_005     toxicity               PASS
resp_005     bias_detection         PASS

Tip

Pull failing response IDs into a review queue, or trigger an alert when result.passed is False. The result.reason field gives a plain-English explanation you can log alongside the score.

Run the same checks from the dashboard

You can also run tone, toxicity, and bias evals from the Future AGI platform without writing code.

  1. Upload your responses as a dataset (see Dataset Management)
  2. Click Add Evaluation, and select is_polite, toxicity, or bias_detection
  3. Map the output key to your response column
  4. Choose a Turing model and run

Results appear as new columns alongside your data.

Troubleshooting

SymptomCauseFix
AuthenticationError on evaluate()FI_API_KEY or FI_SECRET_KEY not set, or copied with a trailing spaceRe-export both keys from app.futureagi.com admin settings and check for stray whitespace
ModuleNotFoundError: No module named 'fi'ai-evaluation isn’t installed in the active environmentRun pip install ai-evaluation inside the same virtualenv you’re running the script from
result.passed is NoneReading .passed before the eval finished, or on a malformed batch itemIterate the full BatchResult and confirm each output value is a non-empty string before evaluating
Batch call returns fewer results than inputsOne item in the output list was empty or None and got skippedFilter or validate your response list before passing it to evaluate()
Every response fails is_polite unexpectedlyoutput is being passed the wrong field (e.g. the customer’s message instead of the agent’s reply)Confirm you’re scoring the model’s response text, not the input prompt
Slow batch sweeps on large response setsEach metric in a batch call is a separate remote scoring pass against the Turing modelRun sweeps async or in smaller chunks; see Async Batch Evaluation
bias_detection flags a demographic-neutral responseWording that references a protected attribute even in a neutral context (e.g. “as a woman in tech”) can still trip the checkRead result.reason and adjust the phrasing to remove the demographic reference if it isn’t load-bearing

For batch evaluation at dataset scale, see Dataset SDK: Batch Evaluation.

Was this page helpful?

Questions & Discussion