diff --git a/.gitignore b/.gitignore index a5530aba..224fccd9 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,4 @@ build .idea plugins/**/.creds.json plugins/**/creds.json +plugins/**/.parameters.json diff --git a/plugins/bedrock/bedrock.test.ts b/plugins/bedrock/bedrock.test.ts index 9e23e198..60e4ffab 100644 --- a/plugins/bedrock/bedrock.test.ts +++ b/plugins/bedrock/bedrock.test.ts @@ -2,15 +2,16 @@ import { HookEventType, PluginContext, PluginParameters } from '../types'; import { pluginHandler } from './index'; import testCreds from './.creds.json'; import { BedrockParameters } from './type'; +import parametersCreds from './.parameters.json'; /** * @example Parameters object * * { "credentials": { - "accessKeyId": "keyId", - "accessKeySecret": "keysecret", - "region": "us-east-1" + "awsAccessKeyId": "keyId", + "awsSecretAccessKey": "keysecret", + "awsRegion": "us-east-1" }, "guardrailId": "xyxyxyx", "guardrailVersion": "1" @@ -53,6 +54,7 @@ describe('Credentials check', () => { expect(result.verdict).toBe(true); expect(result.error).toBeDefined(); expect(result.data).toBeNull(); + expect(result.transformed).toBe(false); }); test('Should fail with wrong creds', async () => { @@ -91,6 +93,7 @@ describe('Credentials check', () => { expect(result.verdict).toBe(true); expect(result.error).toBeDefined(); expect(result.data).toBeNull(); + expect(result.transformed).toBe(false); }); it('should only detect PII', async () => { @@ -110,9 +113,9 @@ describe('Credentials check', () => { requestType: 'chatComplete', }; const parameters = { - credentials: testCreds, - guardrailId: testCreds.guardrailId, - guardrailVersion: testCreds.guardrailVersion, + credentials: testCreds as BedrockParameters['credentials'], + guardrailId: parametersCreds.guardrailId, + guardrailVersion: parametersCreds.guardrailVersion, }; const result = await pluginHandler( @@ -128,6 +131,7 @@ describe('Credentials check', () => { expect(result.error).toBeNull(); expect(result.data).toBeDefined(); expect(result.transformedData?.request?.json).toBeNull(); + expect(result.transformed).toBe(false); }); it('should detect and redact PII in request text', async () => { @@ -146,10 +150,10 @@ describe('Credentials check', () => { requestType: 'chatComplete', }; const parameters = { - credentials: testCreds, + credentials: testCreds as BedrockParameters['credentials'], redact: true, - guardrailId: testCreds.guardrailId, - guardrailVersion: testCreds.guardrailVersion, + guardrailId: parametersCreds.guardrailId, + guardrailVersion: parametersCreds.guardrailVersion, }; const result = await pluginHandler( @@ -166,6 +170,7 @@ describe('Credentials check', () => { expect(result.transformedData?.request?.json?.messages?.[0]?.content).toBe( 'My SSN is {US_SOCIAL_SECURITY_NUMBER} and some random text' ); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in request text with multiple content parts', async () => { @@ -193,10 +198,10 @@ describe('Credentials check', () => { requestType: 'chatComplete', }; const parameters = { - credentials: testCreds, + credentials: testCreds as BedrockParameters['credentials'], redact: true, - guardrailId: testCreds.guardrailId, - guardrailVersion: testCreds.guardrailVersion, + guardrailId: parametersCreds.guardrailId, + guardrailVersion: parametersCreds.guardrailVersion, }; const result = await pluginHandler( @@ -217,6 +222,7 @@ describe('Credentials check', () => { expect( result.transformedData?.request?.json?.messages?.[0]?.content?.[1]?.text ).toBe('My SSN is {US_SOCIAL_SECURITY_NUMBER} and some random text'); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in response text', async () => { @@ -237,10 +243,10 @@ describe('Credentials check', () => { requestType: 'chatComplete', }; const parameters = { - credentials: testCreds, + credentials: testCreds as BedrockParameters['credentials'], redact: true, - guardrailId: testCreds.guardrailId, - guardrailVersion: testCreds.guardrailVersion, + guardrailId: parametersCreds.guardrailId, + guardrailVersion: parametersCreds.guardrailVersion, }; const result = await pluginHandler( @@ -258,6 +264,7 @@ describe('Credentials check', () => { expect( result.transformedData?.response?.json?.choices?.[0]?.message?.content ).toBe('My SSN is {US_SOCIAL_SECURITY_NUMBER} and some random text'); + expect(result.transformed).toBe(true); }); it('should pass text without PII', async () => { @@ -279,9 +286,9 @@ describe('Credentials check', () => { requestType: 'chatComplete', }; const parameters = { - credentials: testCreds, - guardrailId: testCreds.guardrailId, - guardrailVersion: testCreds.guardrailVersion, + credentials: testCreds as BedrockParameters['credentials'], + guardrailId: parametersCreds.guardrailId, + guardrailVersion: parametersCreds.guardrailVersion, }; const result = await pluginHandler( @@ -297,5 +304,6 @@ describe('Credentials check', () => { expect(result.error).toBeNull(); expect(result.data).toBeDefined(); expect(result.transformedData?.response?.json).toBeNull(); + expect(result.transformed).toBe(false); }); }); diff --git a/plugins/bedrock/index.ts b/plugins/bedrock/index.ts index 3f18444d..1fd6b184 100644 --- a/plugins/bedrock/index.ts +++ b/plugins/bedrock/index.ts @@ -32,6 +32,7 @@ export const pluginHandler: PluginHandler< json: null, }, }; + let transformed = false; const credentials = parameters.credentials; const validate = validateCreds(credentials); @@ -48,6 +49,8 @@ export const pluginHandler: PluginHandler< verdict, error: { message: 'Missing required credentials' }, data, + transformed, + transformedData, }; } @@ -68,6 +71,7 @@ export const pluginHandler: PluginHandler< verdict: true, data: null, transformedData, + transformed, }; } @@ -118,6 +122,7 @@ export const pluginHandler: PluginHandler< ); setCurrentContentPart(context, eventType, transformedData, maskedTexts); + transformed = true; } if (hasPii && flaggedCategories.size === 1 && redact) { @@ -138,5 +143,6 @@ export const pluginHandler: PluginHandler< error, data, transformedData, + transformed, }; }; diff --git a/plugins/pangea/pangea.test.ts b/plugins/pangea/pangea.test.ts index 6326259c..6cad3789 100644 --- a/plugins/pangea/pangea.test.ts +++ b/plugins/pangea/pangea.test.ts @@ -235,6 +235,7 @@ describe('pii handler', () => { expect(result.error).toBeNull(); expect(result.data).toBeDefined(); expect(result.transformedData?.request?.json).toBeNull(); + expect(result.transformed).toBe(false); }); it('should detect and redact PII in request text', async () => { @@ -271,6 +272,7 @@ describe('pii handler', () => { expect(result.transformedData?.request?.json?.messages?.[0]?.content).toBe( 'My email is and some random text' ); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in request text with multiple content parts', async () => { @@ -320,6 +322,7 @@ describe('pii handler', () => { expect( result.transformedData?.request?.json?.messages?.[0]?.content?.[1]?.text ).toBe('My email is and some random text'); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in response text', async () => { @@ -359,6 +362,7 @@ describe('pii handler', () => { expect( result.transformedData?.response?.json?.choices?.[0]?.message?.content ).toBe('My email is and some random text'); + expect(result.transformed).toBe(true); }); it('should pass text without PII', async () => { @@ -393,5 +397,6 @@ describe('pii handler', () => { expect(result.verdict).toBe(true); expect(result.error).toBeNull(); expect(result.data).toBeDefined(); + expect(result.transformed).toBe(false); }); }); diff --git a/plugins/pangea/pii.ts b/plugins/pangea/pii.ts index b08e3260..5e8297b9 100644 --- a/plugins/pangea/pii.ts +++ b/plugins/pangea/pii.ts @@ -20,6 +20,7 @@ export const handler: PluginHandler = async ( json: null, }, }; + let transformed = false; const redact = parameters.redact || false; try { @@ -29,6 +30,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -37,6 +39,8 @@ export const handler: PluginHandler = async ( error: `'parameters.credentials.domain' must be set`, verdict: true, data: null, + transformedData, + transformed, }; } @@ -45,6 +49,8 @@ export const handler: PluginHandler = async ( error: `'parameters.credentials.apiKey' must be set`, verdict: true, data: null, + transformedData, + transformed, }; } @@ -58,6 +64,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -87,6 +94,7 @@ export const handler: PluginHandler = async ( response.result.redacted_data ); shouldBlock = false; + transformed = true; } return { @@ -96,6 +104,7 @@ export const handler: PluginHandler = async ( summary: response.summary, }, transformedData, + transformed, }; } catch (e) { return { @@ -103,6 +112,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } }; diff --git a/plugins/patronus/patronus.test.ts b/plugins/patronus/patronus.test.ts index ec244f24..1cc96dc6 100644 --- a/plugins/patronus/patronus.test.ts +++ b/plugins/patronus/patronus.test.ts @@ -4,7 +4,7 @@ import { handler as piiHandler } from './pii'; import { handler as toxicityHandler } from './toxicity'; import { handler as retrievalAnswerRelevanceHandler } from './retrievalAnswerRelevance'; import { handler as customHandler } from './custom'; -import { HookEventType, PluginContext } from '../types'; +import { PluginContext } from '../types'; describe('phi handler', () => { it('should pass when text is clean', async () => { @@ -35,6 +35,7 @@ describe('phi handler', () => { expect(result.verdict).toBe(true); expect(result.error).toBeNull(); expect(result.data).toBeDefined(); + expect(result.transformed).toBe(false); }); it('should fail when text contains PHI', async () => { @@ -67,6 +68,7 @@ describe('phi handler', () => { expect(result.data).toBeDefined(); expect(result.transformedData?.response?.json).toBeNull(); expect(result.transformedData?.request?.json).toBeNull(); + expect(result.transformed).toBe(false); }); it('should detect and redact PII in request text', async () => { @@ -104,6 +106,7 @@ describe('phi handler', () => { expect(result.transformedData?.request?.json?.messages?.[0]?.content).toBe( 'J******e has a history of heart disease' ); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in request text with multiple content parts', async () => { @@ -153,6 +156,7 @@ describe('phi handler', () => { expect( result.transformedData?.request?.json?.messages?.[0]?.content?.[1]?.text ).toBe('J******e has a history of heart disease and some random text'); + expect(result.transformed).toBe(true); }); it('should detect and redact PHI in response text', async () => { @@ -193,6 +197,7 @@ describe('phi handler', () => { expect( result.transformedData?.response?.json?.choices?.[0]?.message?.content ).toBe('J******e has a history of heart disease and some random text'); + expect(result.transformed).toBe(true); }); }); @@ -225,6 +230,7 @@ describe('pii handler', () => { expect(result.verdict).toBe(true); expect(result.error).toBeNull(); expect(result.data).toBeDefined(); + expect(result.transformed).toBe(false); }); it('should fail when text contains PII', async () => { @@ -257,6 +263,7 @@ describe('pii handler', () => { expect(result.data).toBeDefined(); expect(result.transformedData?.response?.json).toBeNull(); expect(result.transformedData?.request?.json).toBeNull(); + expect(result.transformed).toBe(false); }); it('should detect and redact PII in request text', async () => { @@ -294,6 +301,7 @@ describe('pii handler', () => { expect(result.transformedData?.request?.json?.messages?.[0]?.content).toBe( 'My email is a*********m and some random text' ); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in request text with multiple content parts', async () => { @@ -343,6 +351,7 @@ describe('pii handler', () => { expect( result.transformedData?.request?.json?.messages?.[0]?.content?.[1]?.text ).toBe('My email is a*********m and some random text'); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in response text', async () => { @@ -382,6 +391,7 @@ describe('pii handler', () => { expect( result.transformedData?.response?.json?.choices?.[0]?.message?.content ).toBe('My email is a*********m and some random text'); + expect(result.transformed).toBe(true); }); }); diff --git a/plugins/patronus/phi.ts b/plugins/patronus/phi.ts index aa3f6d3f..135b0e93 100644 --- a/plugins/patronus/phi.ts +++ b/plugins/patronus/phi.ts @@ -53,6 +53,7 @@ export const handler: PluginHandler = async ( json: null, }, }; + let transformed = false; try { if (context.requestType === 'embed' && parameters?.redact) { @@ -61,6 +62,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -72,6 +74,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -94,6 +97,7 @@ export const handler: PluginHandler = async ( const maskedTexts = results.map((result) => result?.maskedText ?? null); setCurrentContentPart(context, eventType, transformedData, maskedTexts); shouldBlock = false; + transformed = true; } // verdict can be true/false @@ -104,5 +108,5 @@ export const handler: PluginHandler = async ( error = e; } - return { error, verdict, data, transformedData }; + return { error, verdict, data, transformedData, transformed }; }; diff --git a/plugins/patronus/pii.ts b/plugins/patronus/pii.ts index 9c6ba333..687139c9 100644 --- a/plugins/patronus/pii.ts +++ b/plugins/patronus/pii.ts @@ -89,6 +89,7 @@ export const handler: PluginHandler = async ( json: null, }, }; + let transformed = false; try { if (context.requestType === 'embed' && parameters?.redact) { @@ -97,6 +98,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -108,6 +110,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -130,6 +133,7 @@ export const handler: PluginHandler = async ( const maskedTexts = results.map((result) => result?.maskedText ?? null); setCurrentContentPart(context, eventType, transformedData, maskedTexts); shouldBlock = false; + transformed = true; } // verdict can be true/false @@ -140,5 +144,5 @@ export const handler: PluginHandler = async ( error = e; } - return { error, verdict, data, transformedData }; + return { error, verdict, data, transformedData, transformed }; }; diff --git a/plugins/portkey/pii.ts b/plugins/portkey/pii.ts index b8488ea0..c683c69e 100644 --- a/plugins/portkey/pii.ts +++ b/plugins/portkey/pii.ts @@ -60,7 +60,7 @@ export const handler: PluginHandler = async ( json: null, }, }; - + let transformed = false; try { if (context.requestType === 'embed' && parameters?.redact) { return { @@ -68,6 +68,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -80,6 +81,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -89,6 +91,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -98,6 +101,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -130,7 +134,6 @@ export const handler: PluginHandler = async ( const hasPII = filteredCategories.length > 0; let shouldBlock = hasPII; - let wasRedacted = false; if (parameters.redact && hasPII) { setCurrentContentPart( context, @@ -139,14 +142,14 @@ export const handler: PluginHandler = async ( mappedTextArray ); shouldBlock = false; - wasRedacted = true; + transformed = true; } verdict = not ? hasPII : !shouldBlock; data = { verdict, not, - explanation: wasRedacted + explanation: transformed ? `Found and redacted PII in the text: ${filteredCategories.join(', ')}` : verdict ? not @@ -178,5 +181,5 @@ export const handler: PluginHandler = async ( }; } - return { error, verdict, data, transformedData }; + return { error, verdict, data, transformedData, transformed }; }; diff --git a/plugins/portkey/portkey.test.ts b/plugins/portkey/portkey.test.ts index 395234d6..c47d9e5a 100644 --- a/plugins/portkey/portkey.test.ts +++ b/plugins/portkey/portkey.test.ts @@ -136,6 +136,7 @@ describe('piiHandler', () => { restrictedCategories: ['CREDIT_CARD', 'LOCATION_ADDRESS'], }); expect(result.transformedData?.request?.json).toBeNull(); + expect(result.transformed).toBe(false); }); it('should detect and redact PII in request text', async () => { @@ -179,6 +180,7 @@ describe('piiHandler', () => { expect(result.transformedData?.request?.json?.messages?.[0]?.content).toBe( 'My credit card number is [CREDIT_CARD_1], and my email is [EMAIL_ADDRESS_1]' ); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in request text with multiple content parts', async () => { @@ -233,6 +235,7 @@ describe('piiHandler', () => { expect( result.transformedData?.request?.json?.messages?.[0]?.content?.[1]?.text ).toBe('and my email is [EMAIL_ADDRESS_1]'); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in response text', async () => { @@ -280,6 +283,7 @@ describe('piiHandler', () => { ).toBe( 'My credit card number is [CREDIT_CARD_1], and my email is [EMAIL_ADDRESS_1]' ); + expect(result.transformed).toBe(true); }); it('should pass text without PII', async () => { @@ -318,6 +322,7 @@ describe('piiHandler', () => { explanation: 'No restricted PII was found in the text.', }); expect(result.transformedData?.request?.json).toBeNull(); + expect(result.transformed).toBe(false); }); it('should handle inverted results with not=true', async () => { @@ -356,6 +361,7 @@ describe('piiHandler', () => { not: true, explanation: 'PII was found in the text as expected.', }); + expect(result.transformed).toBe(false); }); it('should handle and redact inverted results with not=true', async () => { @@ -395,6 +401,7 @@ describe('piiHandler', () => { not: true, explanation: expect.stringContaining('redacted'), }); + expect(result.transformed).toBe(true); }); }); @@ -403,7 +410,7 @@ describe('languageHandler', () => { it('should detect correct language', async () => { const context = { - request: { text: 'hola mundo' }, + request: { text: 'Por favor hola gracias' }, }; const parameters = { language: ['spa_Latn'], diff --git a/plugins/promptfoo/pii.ts b/plugins/promptfoo/pii.ts index 5ac2a8bc..a6da9583 100644 --- a/plugins/promptfoo/pii.ts +++ b/plugins/promptfoo/pii.ts @@ -59,7 +59,7 @@ export const handler: PluginHandler = async ( json: null, }, }; - + let transformed = false; try { if (context.requestType === 'embed' && parameters?.redact) { return { @@ -67,6 +67,7 @@ export const handler: PluginHandler = async ( verdict: true, data: null, transformedData, + transformed, }; } @@ -77,6 +78,8 @@ export const handler: PluginHandler = async ( error: { message: 'request or response json is empty' }, verdict: true, data: null, + transformedData, + transformed, }; } @@ -93,6 +96,7 @@ export const handler: PluginHandler = async ( const maskedTexts = results.map((result) => result?.maskedText ?? null); setCurrentContentPart(context, eventType, transformedData, maskedTexts); shouldBlock = false; + transformed = true; } verdict = !shouldBlock; @@ -102,5 +106,5 @@ export const handler: PluginHandler = async ( error = e; } - return { error, verdict, data, transformedData }; + return { error, verdict, data, transformedData, transformed }; }; diff --git a/plugins/promptfoo/promptfoo.test.ts b/plugins/promptfoo/promptfoo.test.ts index 8a053b10..cb6c9498 100644 --- a/plugins/promptfoo/promptfoo.test.ts +++ b/plugins/promptfoo/promptfoo.test.ts @@ -71,6 +71,7 @@ describe('pii handler', () => { expect(result.error).toBeNull(); expect(result.data).toBeDefined(); expect(result.transformedData?.request?.json).toBeNull(); + expect(result.transformed).toBe(false); }); it('should detect and redact PII in request text', async () => { @@ -106,6 +107,7 @@ describe('pii handler', () => { expect(result.transformedData?.request?.json?.messages?.[0]?.content).toBe( 'My SSN is [SOCIAL_SECURITY_NUMBER] and some random text' ); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in request text with multiple content parts', async () => { @@ -155,6 +157,7 @@ describe('pii handler', () => { expect( result.transformedData?.request?.json?.messages?.[0]?.content?.[1]?.text ).toBe('My SSN is [SOCIAL_SECURITY_NUMBER] and some random text'); + expect(result.transformed).toBe(true); }); it('should detect and redact PII in response text', async () => { @@ -194,6 +197,7 @@ describe('pii handler', () => { expect( result.transformedData?.response?.json?.choices?.[0]?.message?.content ).toBe('My SSN is [SOCIAL_SECURITY_NUMBER] and some random text'); + expect(result.transformed).toBe(true); }); it('should pass text without PII', async () => { @@ -226,6 +230,7 @@ describe('pii handler', () => { expect(result.verdict).toBe(true); expect(result.error).toBeNull(); expect(result.data).toBeDefined(); + expect(result.transformed).toBe(false); }); }); diff --git a/plugins/types.ts b/plugins/types.ts index 1ecfe83e..6f6f7314 100644 --- a/plugins/types.ts +++ b/plugins/types.ts @@ -15,6 +15,7 @@ export interface PluginHandlerResponse { // The data object can be any JSON object or null. data?: any | null; transformedData?: any; + transformed?: boolean; } export type HookEventType = 'beforeRequestHook' | 'afterRequestHook'; diff --git a/src/middlewares/hooks/index.ts b/src/middlewares/hooks/index.ts index ec03ddba..a545ed1f 100644 --- a/src/middlewares/hooks/index.ts +++ b/src/middlewares/hooks/index.ts @@ -297,6 +297,7 @@ export class HooksManager { ? { name: result.error.name, message: result.error.message } : undefined, execution_time: new Date().getTime() - createdAt.getTime(), + transformed: result.transformed || false, created_at: createdAt, }; } catch (err: any) { @@ -384,6 +385,7 @@ export class HooksManager { hookResult = { verdict: checkResults.every((result) => result.verdict || result.error), id: hook.id, + transformed: checkResults.some((result) => result.transformed), checks: checkResults, feedback: this.createFeedbackObject( checkResults, diff --git a/src/middlewares/hooks/types.ts b/src/middlewares/hooks/types.ts index 04f2cee0..4bfeaea5 100644 --- a/src/middlewares/hooks/types.ts +++ b/src/middlewares/hooks/types.ts @@ -67,6 +67,7 @@ export interface GuardrailCheckResult { error?: Error | null; data?: any; id: string; + transformed?: boolean; execution_time: number; created_at: Date; transformedData?: { @@ -83,6 +84,7 @@ export interface GuardrailResult { verdict: boolean; id: string; checks: GuardrailCheckResult[]; + transformed?: boolean; feedback: GuardrailFeedback; error?: Error | null; async: boolean;