Fix compliance playground batch scoring bug, add display_name support

The compliance playground was sending all texts in a single batch API call,
but the content filter raises HTTPException on the first blocked text. This
caused a single blocked/allowed result to be applied to all rows, producing
incorrect scores (e.g. 41% instead of 100%). Fix by sending each text
individually to get per-text results with progressive UI updates.

Also add display_name field support for category YAML files so
denied_financial_advice shows as "Denied Financial / Investment Advice"
in the UI dropdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ishaan Jaffer 2026-02-20 17:41:44 -08:00
parent 116e0a8470
commit eed91b1dc4
3 changed files with 50 additions and 43 deletions

View file

@ -11,6 +11,7 @@
# Precision: 100%, Recall: 100%, F1: 100%, Latency: <0.1ms
# Run: pytest tests/.../topic_blocker/test_eval.py -k InvestmentContentFilter -v -s
category_name: "denied_financial_advice"
display_name: "Denied Financial / Investment Advice"
description: "Detects requests for personalized financial advice, investment recommendations, or financial planning that should be provided by licensed financial advisors"
default_action: "BLOCK"

View file

@ -162,9 +162,11 @@ def get_available_content_categories() -> List[Dict[str, str]]:
category_data = yaml.safe_load(f)
if category_data and "category_name" in category_data:
# Create display name from category name (convert harmful_self_harm -> Harmful Self Harm)
display_name = (
category_data["category_name"].replace("_", " ").title()
# Use explicit display_name from YAML if provided,
# otherwise auto-generate from category_name
display_name = category_data.get(
"display_name",
category_data["category_name"].replace("_", " ").title(),
)
available_categories.append(

View file

@ -532,55 +532,59 @@ export default function ComplianceUI({
status: "pending",
}));
setTestResults(pendingResults);
try {
const { inputs, guardrail_errors } = await testPoliciesAndGuardrails(
accessToken,
{
policy_names:
selectedPolicies.length > 0 ? selectedPolicies : undefined,
guardrail_names:
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
inputs: { texts: allTexts },
request_data: {},
input_type: "request",
}
);
const actualResult: "blocked" | "allowed" =
guardrail_errors.length > 0 ? "blocked" : "allowed";
const triggeredBy =
guardrail_errors.length > 0
? guardrail_errors
.map((e) => `${e.guardrail_name}: ${e.message}`)
.join("; ")
: undefined;
const returnedTexts: (string | undefined)[] =
Array.isArray(inputs?.texts) ? inputs.texts : [];
setTestResults(
pendingResults.map((row, index) => ({
...row,
// Send each text individually to get per-text blocked/allowed results.
// Sending all texts in a single batch doesn't work because the guardrail
// raises an HTTPException on the first blocked text, skipping the rest.
const updatedResults = [...pendingResults];
for (let i = 0; i < allTexts.length; i++) {
try {
const { inputs, guardrail_errors } = await testPoliciesAndGuardrails(
accessToken,
{
policy_names:
selectedPolicies.length > 0 ? selectedPolicies : undefined,
guardrail_names:
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
inputs: { texts: [allTexts[i]] },
request_data: {},
input_type: "request",
}
);
const actualResult: "blocked" | "allowed" =
guardrail_errors.length > 0 ? "blocked" : "allowed";
const triggeredBy =
guardrail_errors.length > 0
? guardrail_errors
.map((e) => `${e.guardrail_name}: ${e.message}`)
.join("; ")
: undefined;
const returnedText =
Array.isArray(inputs?.texts) && inputs.texts.length > 0
? inputs.texts[0]
: undefined;
updatedResults[i] = {
...updatedResults[i],
actualResult,
isMatch:
(row.expectedResult === "fail" && actualResult === "blocked") ||
(row.expectedResult === "pass" && actualResult === "allowed"),
(updatedResults[i].expectedResult === "fail" && actualResult === "blocked") ||
(updatedResults[i].expectedResult === "pass" && actualResult === "allowed"),
triggeredBy,
returnedText: returnedTexts[index],
returnedText,
status: "complete" as const,
}))
);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
setTestResults(
pendingResults.map((row) => ({
...row,
};
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
updatedResults[i] = {
...updatedResults[i],
actualResult: "blocked" as const,
isMatch: false,
triggeredBy: `Error: ${errorMessage}`,
status: "complete" as const,
}))
);
} finally {
setIsRunning(false);
};
}
setTestResults([...updatedResults]);
}
setIsRunning(false);
}, [
accessToken,
selectedPromptIds,