Tone, Toxicity & Bias Evals
Score LLM outputs for professional tone, harmful content, and demographic bias with the is_polite, toxicity, and bias_detection metrics.
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.
| Time | Difficulty | Package |
|---|---|---|
| 10 min | Beginner | ai-evaluation |
- Future AGI account → app.futureagi.com
- API keys:
FI_API_KEYandFI_SECRET_KEY(see Get your API keys) - Python 3.11+
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 PASSTip
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.
- Upload your responses as a dataset (see Dataset Management)
- Click Add Evaluation, and select
is_polite,toxicity, orbias_detection - Map the
outputkey to your response column - Choose a Turing model and run
Results appear as new columns alongside your data.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
AuthenticationError on evaluate() | FI_API_KEY or FI_SECRET_KEY not set, or copied with a trailing space | Re-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 environment | Run pip install ai-evaluation inside the same virtualenv you’re running the script from |
result.passed is None | Reading .passed before the eval finished, or on a malformed batch item | Iterate the full BatchResult and confirm each output value is a non-empty string before evaluating |
| Batch call returns fewer results than inputs | One item in the output list was empty or None and got skipped | Filter or validate your response list before passing it to evaluate() |
Every response fails is_polite unexpectedly | output 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 sets | Each metric in a batch call is a separate remote scoring pass against the Turing model | Run sweeps async or in smaller chunks; see Async Batch Evaluation |
bias_detection flags a demographic-neutral response | Wording that references a protected attribute even in a neutral context (e.g. “as a woman in tech”) can still trip the check | Read 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.
Questions & Discussion