diff --git a/plugins/index.ts b/plugins/index.ts index a21fa7d5..641738b5 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -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, }, diff --git a/plugins/qualifire/dangerousContent.ts b/plugins/qualifire/contentModeration.ts similarity index 94% rename from plugins/qualifire/dangerousContent.ts rename to plugins/qualifire/contentModeration.ts index b566d4c9..37e3af29 100644 --- a/plugins/qualifire/dangerousContent.ts +++ b/plugins/qualifire/contentModeration.ts @@ -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') { diff --git a/plugins/qualifire/globals.ts b/plugins/qualifire/globals.ts index b8c753ef..d02be1a9 100644 --- a/plugins/qualifire/globals.ts +++ b/plugins/qualifire/globals.ts @@ -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 }; diff --git a/plugins/qualifire/grounding.ts b/plugins/qualifire/grounding.ts index ea3d609d..07a015ed 100644 --- a/plugins/qualifire/grounding.ts +++ b/plugins/qualifire/grounding.ts @@ -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 { diff --git a/plugins/qualifire/hallucinations.ts b/plugins/qualifire/hallucinations.ts index 48115908..cf965b9e 100644 --- a/plugins/qualifire/hallucinations.ts +++ b/plugins/qualifire/hallucinations.ts @@ -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 { diff --git a/plugins/qualifire/harassment.ts b/plugins/qualifire/harassment.ts deleted file mode 100644 index 737d783c..00000000 --- a/plugins/qualifire/harassment.ts +++ /dev/null @@ -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 }; - } -}; diff --git a/plugins/qualifire/hateSpeech.ts b/plugins/qualifire/hateSpeech.ts deleted file mode 100644 index 33ddfba5..00000000 --- a/plugins/qualifire/hateSpeech.ts +++ /dev/null @@ -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 }; - } -}; diff --git a/plugins/qualifire/instructionFollowing.ts b/plugins/qualifire/instructionFollowing.ts deleted file mode 100644 index 77e574dc..00000000 --- a/plugins/qualifire/instructionFollowing.ts +++ /dev/null @@ -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 }; - } -}; diff --git a/plugins/qualifire/manifest.json b/plugins/qualifire/manifest.json index f8f50d59..98f8d1e8 100644 --- a/plugins/qualifire/manifest.json +++ b/plugins/qualifire/manifest.json @@ -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"] diff --git a/plugins/qualifire/policy.ts b/plugins/qualifire/policy.ts index adfb20f1..d438a2e9 100644 --- a/plugins/qualifire/policy.ts +++ b/plugins/qualifire/policy.ts @@ -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; } diff --git a/plugins/qualifire/qualifire.test.ts b/plugins/qualifire/qualifire.test.ts index 0f9a1394..80fca93c 100644 --- a/plugins/qualifire/qualifire.test.ts +++ b/plugins/qualifire/qualifire.test.ts @@ -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', () => { diff --git a/plugins/qualifire/sexualContent.ts b/plugins/qualifire/sexualContent.ts deleted file mode 100644 index 82618b6c..00000000 --- a/plugins/qualifire/sexualContent.ts +++ /dev/null @@ -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 }; - } -}; diff --git a/plugins/qualifire/toolUseQuality.ts b/plugins/qualifire/toolUseQuality.ts index 2731f52b..95854e70 100644 --- a/plugins/qualifire/toolUseQuality.ts +++ b/plugins/qualifire/toolUseQuality.ts @@ -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 {