Merge pull request #1483 from qualifire-dev/feature/qualifire-update-apis

Qualifire | Update Guardrails
This commit is contained in:
Visarg Desai
2026-01-07 13:23:41 +05:30
committed by GitHub
13 changed files with 349 additions and 201 deletions
+2 -10
View File
@@ -14,15 +14,11 @@ import { handler as defaultalllowercase } from './default/alllowercase';
import { handler as defaultendsWith } from './default/endsWith';
import { handler as defaultmodelWhitelist } from './default/modelWhitelist';
import { handler as defaultnotNull } from './default/notNull';
import { handler as qualifireDangerousContent } from './qualifire/dangerousContent';
import { handler as qualifireContentModeration } from './qualifire/contentModeration';
import { handler as qualifireGrounding } from './qualifire/grounding';
import { handler as qualifireHarassment } from './qualifire/harassment';
import { handler as qualifireInstructionFollowing } from './qualifire/instructionFollowing';
import { handler as qualifirePolicy } from './qualifire/policy';
import { handler as qualifireSexualContent } from './qualifire/sexualContent';
import { handler as qualifireToolUseQuality } from './qualifire/toolUseQuality';
import { handler as qualifireHallucinations } from './qualifire/hallucinations';
import { handler as qualifireHateSpeech } from './qualifire/hateSpeech';
import { handler as qualifirePii } from './qualifire/pii';
import { handler as qualifirePromptInjections } from './qualifire/promptInjections';
import { handler as defaultaddPrefix } from './default/addPrefix';
@@ -97,15 +93,11 @@ export const plugins = {
notNull: defaultnotNull,
},
qualifire: {
dangerousContent: qualifireDangerousContent,
contentModeration: qualifireContentModeration,
grounding: qualifireGrounding,
harassment: qualifireHarassment,
instructionFollowing: qualifireInstructionFollowing,
policy: qualifirePolicy,
sexualContent: qualifireSexualContent,
toolUseQuality: qualifireToolUseQuality,
hallucinations: qualifireHallucinations,
hateSpeech: qualifireHateSpeech,
pii: qualifirePii,
promptInjections: qualifirePromptInjections,
},
@@ -13,7 +13,7 @@ export const handler: PluginHandler = async (
) => {
const evaluationBody: any = {
input: context.request.text,
dangerous_content_check: true,
content_moderation_check: true,
};
if (eventType === 'afterRequestHook') {
+1 -1
View File
@@ -38,7 +38,7 @@ export const postQualifire = async (
const result = await post(BASE_URL, body, options, timeout_millis || 60000);
const error = result?.error || null;
const verdict = result?.status === 'success';
const verdict = result?.status !== 'failed';
const data = result?.evaluationResults;
return { error, verdict, data };
+3
View File
@@ -22,10 +22,13 @@ export const handler: PluginHandler = async (
};
}
const mode = parameters?.mode || 'balanced';
const evaluationBody: any = {
input: context.request.text,
output: context.response.text,
grounding_check: true,
grounding_mode: mode,
};
try {
+3
View File
@@ -22,10 +22,13 @@ export const handler: PluginHandler = async (
};
}
const mode = parameters?.mode || 'balanced';
const evaluationBody: any = {
input: context.request.text,
output: context.response.text,
hallucinations_check: true,
hallucinations_mode: mode,
};
try {
-29
View File
@@ -1,29 +0,0 @@
import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';
import { postQualifire } from './globals';
export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
eventType: HookEventType
) => {
const evaluationBody: any = {
input: context.request.text,
harassment_check: true,
};
if (eventType === 'afterRequestHook') {
evaluationBody.output = context.response.text;
}
try {
return await postQualifire(evaluationBody, parameters?.credentials?.apiKey);
} catch (e: any) {
delete e.stack;
return { error: e, verdict: false, data: null };
}
};
-29
View File
@@ -1,29 +0,0 @@
import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';
import { postQualifire } from './globals';
export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
eventType: HookEventType
) => {
const evaluationBody: any = {
input: context.request.text,
hate_speech_check: true,
};
if (eventType === 'afterRequestHook') {
evaluationBody.output = context.response.text;
}
try {
return await postQualifire(evaluationBody, parameters?.credentials?.apiKey);
} catch (e: any) {
delete e.stack;
return { error: e, verdict: false, data: null };
}
};
-37
View File
@@ -1,37 +0,0 @@
import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';
import { postQualifire } from './globals';
export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
eventType: HookEventType
) => {
if (eventType !== 'afterRequestHook') {
return {
error: {
message:
'Qualifire Instruction Following guardrail only supports after_request_hooks.',
},
verdict: true,
data: null,
};
}
const evaluationBody: any = {
input: context.request.text,
output: context.response.text,
instructions_following_check: true,
};
try {
return await postQualifire(evaluationBody, parameters?.credentials?.apiKey);
} catch (e: any) {
delete e.stack;
return { error: e, verdict: false, data: null };
}
};
+53 -58
View File
@@ -15,66 +15,14 @@
},
"functions": [
{
"name": "Hate Speech Check",
"id": "hateSpeech",
"name": "Content Moderation Check",
"id": "contentModeration",
"supportedHooks": ["beforeRequestHook", "afterRequestHook"],
"type": "guardrail",
"description": [
{
"type": "subHeading",
"text": "Checks for hate speech in the user input or model output."
}
],
"parameters": {}
},
{
"name": "Dangerous Content Check",
"id": "dangerousContent",
"supportedHooks": ["beforeRequestHook", "afterRequestHook"],
"type": "guardrail",
"description": [
{
"type": "subHeading",
"text": "Checks for dangerous content in the user input or model output."
}
],
"parameters": {}
},
{
"name": "Sexual Content Check",
"id": "sexualContent",
"supportedHooks": ["beforeRequestHook", "afterRequestHook"],
"type": "guardrail",
"description": [
{
"type": "subHeading",
"text": "Checks for sexual content in the user input or model output."
}
],
"parameters": {}
},
{
"name": "Harassment Check",
"id": "harassment",
"supportedHooks": ["beforeRequestHook", "afterRequestHook"],
"type": "guardrail",
"description": [
{
"type": "subHeading",
"text": "Checks for harassment in the user input or model output."
}
],
"parameters": {}
},
{
"name": "Instruction Following Check",
"id": "instructionFollowing",
"supportedHooks": ["afterRequestHook"],
"type": "guardrail",
"description": [
{
"type": "subHeading",
"text": "Checks that the model followed the instructions provided in the prompt."
"text": "Checks for dangerous content, sexual content, harassment, and dangerous content in the user input or model output."
}
],
"parameters": {}
@@ -90,7 +38,18 @@
"text": "Checks that the model did not hallucinate."
}
],
"parameters": {}
"parameters": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"label": "Mode",
"description": "The mode to use for the check",
"enum": ["speed", "balanced", "quality"],
"default": "balanced"
}
}
}
},
{
"name": "PII Check",
@@ -129,7 +88,18 @@
"text": "Checks that the model is grounded in the context provided."
}
],
"parameters": {}
"parameters": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"label": "Mode",
"description": "The mode to use for the check",
"enum": ["speed", "balanced", "quality"],
"default": "balanced"
}
}
}
},
{
"name": "Tool Use Quality Check",
@@ -142,7 +112,18 @@
"text": "Checks the model's tool use quality. Including correct tool selection, correct tool parameters and values."
}
],
"parameters": {}
"parameters": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"label": "Mode",
"description": "The mode to use for the check",
"enum": ["speed", "balanced", "quality"],
"default": "balanced"
}
}
}
},
{
"name": "Policy Violations Check",
@@ -170,6 +151,20 @@
}
]
}
},
"mode": {
"type": "string",
"label": "Mode",
"description": "The mode to use for the check",
"enum": ["speed", "balanced", "quality"],
"default": "balanced"
},
"policy_target": {
"type": "string",
"label": "Policy Target",
"description": "Where to apply the policy check",
"enum": ["input", "output", "both"],
"default": "both"
}
},
"required": ["policies"]
+14 -2
View File
@@ -21,12 +21,24 @@ export const handler: PluginHandler = async (
};
}
const mode = parameters?.mode || 'balanced';
const policyTarget = parameters?.policy_target || 'both';
const evaluationBody: any = {
input: context.request.text,
assertions: parameters?.policies,
assertions_mode: mode,
};
if (eventType === 'afterRequestHook') {
// Add input based on policy_target
if (policyTarget === 'input' || policyTarget === 'both') {
evaluationBody.input = context.request.text;
}
// Add output based on policy_target and hook type
if (
eventType === 'afterRequestHook' &&
(policyTarget === 'output' || policyTarget === 'both')
) {
evaluationBody.output = context.response.text;
}
+269 -5
View File
@@ -519,7 +519,7 @@ describe('grounding handler', () => {
},
];
it('should handle successful evaluation for afterRequestHook', async () => {
it('should handle successful evaluation for afterRequestHook with default mode', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
@@ -534,6 +534,7 @@ describe('grounding handler', () => {
input: 'What is the capital of France?',
output: 'The capital of France is Paris.',
grounding_check: true,
grounding_mode: 'balanced',
},
'test-api-key'
);
@@ -555,11 +556,39 @@ describe('grounding handler', () => {
input: 'What is the capital of France?',
output: 'The capital of France is Paris.',
grounding_check: true,
grounding_mode: 'balanced',
},
'test-api-key'
);
expect(result).toEqual(testCases[1].mockResponse);
});
it('should use custom mode when provided', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
const customParameters = {
...mockParameters,
mode: 'quality',
};
const result = await groundingHandler(
mockContext,
customParameters,
'afterRequestHook' as HookEventType
);
expect(postQualifire).toHaveBeenCalledWith(
{
input: 'What is the capital of France?',
output: 'The capital of France is Paris.',
grounding_check: true,
grounding_mode: 'quality',
},
'test-api-key'
);
expect(result).toEqual(testCases[0].mockResponse);
});
});
describe('when called with unsupported event types', () => {
@@ -662,7 +691,7 @@ describe('hallucinations handler', () => {
},
];
it('should handle successful evaluation for afterRequestHook', async () => {
it('should handle successful evaluation for afterRequestHook with default mode', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
@@ -678,6 +707,7 @@ describe('hallucinations handler', () => {
output:
'Quantum computing features include superposition, entanglement, and quantum interference.',
hallucinations_check: true,
hallucinations_mode: 'balanced',
},
'test-api-key'
);
@@ -700,11 +730,40 @@ describe('hallucinations handler', () => {
output:
'Quantum computing features include superposition, entanglement, and quantum interference.',
hallucinations_check: true,
hallucinations_mode: 'balanced',
},
'test-api-key'
);
expect(result).toEqual(testCases[1].mockResponse);
});
it('should use custom mode when provided', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
const customParameters = {
...mockParameters,
mode: 'speed',
};
const result = await hallucinationsHandler(
mockContext,
customParameters,
'afterRequestHook' as HookEventType
);
expect(postQualifire).toHaveBeenCalledWith(
{
input: 'What are the main features of quantum computing?',
output:
'Quantum computing features include superposition, entanglement, and quantum interference.',
hallucinations_check: true,
hallucinations_mode: 'speed',
},
'test-api-key'
);
expect(result).toEqual(testCases[0].mockResponse);
});
});
describe('when called with unsupported event types', () => {
@@ -1679,7 +1738,7 @@ describe('policy handler', () => {
},
];
it('should handle successful evaluation for beforeRequestHook', async () => {
it('should handle successful evaluation for beforeRequestHook with default parameters', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
@@ -1696,6 +1755,7 @@ describe('policy handler', () => {
'The response must be polite',
"The assistant isn't allowed to provide any discounts, promotions or free items.",
],
assertions_mode: 'balanced',
},
'test-api-key'
);
@@ -1719,13 +1779,14 @@ describe('policy handler', () => {
'The response must be polite',
"The assistant isn't allowed to provide any discounts, promotions or free items.",
],
assertions_mode: 'balanced',
},
'test-api-key'
);
expect(result).toEqual(testCases[1].mockResponse);
});
it('should handle successful evaluation for afterRequestHook', async () => {
it('should handle successful evaluation for afterRequestHook with default parameters', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
@@ -1744,6 +1805,7 @@ describe('policy handler', () => {
'The response must be polite',
"The assistant isn't allowed to provide any discounts, promotions or free items.",
],
assertions_mode: 'balanced',
},
'test-api-key'
);
@@ -1769,11 +1831,131 @@ describe('policy handler', () => {
'The response must be polite',
"The assistant isn't allowed to provide any discounts, promotions or free items.",
],
assertions_mode: 'balanced',
},
'test-api-key'
);
expect(result).toEqual(testCases[1].mockResponse);
});
it('should use custom mode when provided', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
const customParameters = {
...mockParametersWithPolicies,
mode: 'quality',
};
const result = await policyHandler(
mockContext,
customParameters,
'beforeRequestHook' as HookEventType
);
expect(postQualifire).toHaveBeenCalledWith(
{
input: 'Can I get a discount?',
assertions: [
'The response must be polite',
"The assistant isn't allowed to provide any discounts, promotions or free items.",
],
assertions_mode: 'quality',
},
'test-api-key'
);
expect(result).toEqual(testCases[0].mockResponse);
});
it('should only check input when policy_target is "input"', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
const customParameters = {
...mockParametersWithPolicies,
policy_target: 'input',
};
const result = await policyHandler(
mockContext,
customParameters,
'afterRequestHook' as HookEventType
);
expect(postQualifire).toHaveBeenCalledWith(
{
input: 'Can I get a discount?',
assertions: [
'The response must be polite',
"The assistant isn't allowed to provide any discounts, promotions or free items.",
],
assertions_mode: 'balanced',
},
'test-api-key'
);
expect(result).toEqual(testCases[0].mockResponse);
});
it('should only check output when policy_target is "output"', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
const customParameters = {
...mockParametersWithPolicies,
policy_target: 'output',
};
const result = await policyHandler(
mockContext,
customParameters,
'afterRequestHook' as HookEventType
);
expect(postQualifire).toHaveBeenCalledWith(
{
output:
"I apologize, but I'm not able to provide any discounts, promotions, or free items. I'd be happy to help you with other questions or information about our products and services.",
assertions: [
'The response must be polite',
"The assistant isn't allowed to provide any discounts, promotions or free items.",
],
assertions_mode: 'balanced',
},
'test-api-key'
);
expect(result).toEqual(testCases[0].mockResponse);
});
it('should check both input and output when policy_target is "both"', async () => {
const { postQualifire } = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
const customParameters = {
...mockParametersWithPolicies,
policy_target: 'both',
};
const result = await policyHandler(
mockContext,
customParameters,
'afterRequestHook' as HookEventType
);
expect(postQualifire).toHaveBeenCalledWith(
{
input: 'Can I get a discount?',
output:
"I apologize, but I'm not able to provide any discounts, promotions, or free items. I'd be happy to help you with other questions or information about our products and services.",
assertions: [
'The response must be polite',
"The assistant isn't allowed to provide any discounts, promotions or free items.",
],
assertions_mode: 'balanced',
},
'test-api-key'
);
expect(result).toEqual(testCases[0].mockResponse);
});
});
describe('when policies are missing', () => {
@@ -1957,7 +2139,7 @@ describe('toolUseQuality handler', () => {
},
];
it('should handle successful evaluation for afterRequestHook', async () => {
it('should handle successful evaluation for afterRequestHook with default mode', async () => {
const {
postQualifire,
convertToMessages,
@@ -2031,6 +2213,7 @@ describe('toolUseQuality handler', () => {
},
],
tool_selection_quality_check: true,
tsq_mode: 'balanced',
},
'test-api-key'
);
@@ -2079,6 +2262,87 @@ describe('toolUseQuality handler', () => {
expect(result).toEqual(testCases[1].mockResponse);
});
it('should use custom mode when provided', async () => {
const {
postQualifire,
convertToMessages,
parseAvailableTools,
} = require('./globals');
(postQualifire as jest.Mock).mockResolvedValue(testCases[0].mockResponse);
(convertToMessages as jest.Mock).mockReturnValue([
{ role: 'user', content: "What's the weather like in New York?" },
{
role: 'assistant',
content: null,
tool_calls: [
{
id: 'call_123',
name: 'get_weather',
arguments: { location: 'New York' },
},
],
},
]);
(parseAvailableTools as jest.Mock).mockReturnValue([
{
name: 'get_weather',
description: 'Get weather information for a location',
parameters: {
type: 'object',
properties: {
location: { type: 'string' },
},
},
},
]);
const customParameters = {
...mockParameters,
mode: 'speed',
};
const result = await toolUseQualityHandler(
mockContext,
customParameters,
'afterRequestHook' as HookEventType
);
expect(postQualifire).toHaveBeenCalledWith(
{
messages: [
{ role: 'user', content: "What's the weather like in New York?" },
{
role: 'assistant',
content: null,
tool_calls: [
{
id: 'call_123',
name: 'get_weather',
arguments: { location: 'New York' },
},
],
},
],
available_tools: [
{
name: 'get_weather',
description: 'Get weather information for a location',
parameters: {
type: 'object',
properties: {
location: { type: 'string' },
},
},
},
],
tool_selection_quality_check: true,
tsq_mode: 'speed',
},
'test-api-key'
);
expect(result).toEqual(testCases[0].mockResponse);
});
});
describe('when called with unsupported event types', () => {
-29
View File
@@ -1,29 +0,0 @@
import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';
import { postQualifire } from './globals';
export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
eventType: HookEventType
) => {
const evaluationBody: any = {
input: context.request.text,
sexual_content_check: true,
};
if (eventType === 'afterRequestHook') {
evaluationBody.output = context.response.text;
}
try {
return await postQualifire(evaluationBody, parameters?.credentials?.apiKey);
} catch (e: any) {
delete e.stack;
return { error: e, verdict: false, data: null };
}
};
+3
View File
@@ -26,10 +26,13 @@ export const handler: PluginHandler = async (
};
}
const mode = parameters?.mode || 'balanced';
const evaluationBody: any = {
messages: convertToMessages(context.request, context.response),
available_tools: parseAvailableTools(context.request),
tool_selection_quality_check: true,
tsq_mode: mode,
};
try {