Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Mutator Hooks #870

Merged
merged 26 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
afcb0e9
feat: add mutator hook type support in hooks middleware
VisargD Jan 17, 2025
cbaf20a
feat: add promptfoo redact pii plugin handler
VisargD Jan 17, 2025
6b3cd03
feat: add portkey redact pii plugin handler
VisargD Jan 17, 2025
702e18a
feat: add patronus redact pii plugin handler
VisargD Jan 17, 2025
45eea77
feat: add patronus redact phi plugin handler
VisargD Jan 17, 2025
94bfd36
feat: add pange redact pii plugin handler
VisargD Jan 17, 2025
a5c4292
chore: update portkey detect pii function to allow multiple text parts
VisargD Jan 17, 2025
2e0318d
chore: add patronus entity masking functions
VisargD Jan 17, 2025
f92d676
chore: add util functions for content part get and set
VisargD Jan 17, 2025
8ad2574
chore: update plugins index with new handlers
VisargD Jan 17, 2025
17e50a4
feat: use plugin transformed response if available
VisargD Jan 17, 2025
8ceae85
feat: use plugin transformed request if available
VisargD Jan 17, 2025
0848746
merge main into feat/mutator-hooks
VisargD Jan 17, 2025
55dc4c6
feat: add mutator shorthand handling in try targets method
VisargD Jan 17, 2025
af8a0ca
fix: try post variable uninitialized error
VisargD Jan 17, 2025
dde899e
Merge branch 'main' into feat/mutator-hooks
VisargD Jan 18, 2025
15119c7
Merge branch 'main' into feat/mutator-hooks
VisargD Jan 18, 2025
65a587b
chore: minor refactor
VisargD Jan 18, 2025
9aadb15
chore: minor cleanup
VisargD Jan 18, 2025
d89b121
Merge branch 'main' into feat/mutator-hooks
VisargD Jan 18, 2025
499e9ee
Merge branch 'main' into feat/mutator-hooks
VisargD Jan 20, 2025
47450f5
chore: minor refactor in patronus plugins
VisargD Jan 20, 2025
4820327
chore: streamline hook type enum naming
VisargD Jan 20, 2025
2c238e4
chore: minor cleanup
VisargD Jan 20, 2025
2631515
chore: add type in hook result object
VisargD Jan 20, 2025
0d8122b
Merge branch 'main' into feat/mutator-hooks
VisargD Jan 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ import { handler as patronustoxicity } from './patronus/toxicity';
import { handler as patronuscustom } from './patronus/custom';
import { mistralGuardrailHandler } from './mistral';
import { handler as pangeatextGuard } from './pangea/textGuard';
import { handler as portkeyredactPii } from './portkey/redactPii';
import { handler as promptfooRedactPii } from './promptfoo/redactPii';
import { handler as promptfooHarm } from './promptfoo/harm';
import { handler as promptfooGuard } from './promptfoo/guard';
import { handler as pangearedactPii } from './pangea/redactPii';
import { handler as patronusredactPii } from './patronus/redactPii';
import { handler as patronusredactPhi } from './patronus/redactPhi';

export const plugins = {
default: {
Expand All @@ -58,6 +65,7 @@ export const plugins = {
language: portkeylanguage,
pii: portkeypii,
gibberish: portkeygibberish,
redactPii: portkeyredactPii,
},
aporia: {
validateProject: aporiavalidateProject,
Expand All @@ -81,11 +89,19 @@ export const plugins = {
retrievalAnswerRelevance: patronusretrievalAnswerRelevance,
toxicity: patronustoxicity,
custom: patronuscustom,
redactPii: patronusredactPii,
redactPhi: patronusredactPhi,
VisargD marked this conversation as resolved.
Show resolved Hide resolved
},
mistral: {
moderateContent: mistralGuardrailHandler,
},
pangea: {
textGuard: pangeatextGuard,
redactPii: pangearedactPii,
},
promptfoo: {
redactPii: promptfooRedactPii,
harm: promptfooHarm,
guard: promptfooGuard,
},
};
95 changes: 95 additions & 0 deletions plugins/pangea/redactPii.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';
import { getCurrentContentPart, post, setCurrentContentPart } from '../utils';
import { VERSION } from './version';

export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
eventType: HookEventType
) => {
let transformedData = {
request: {
json: null,
},
response: {
json: null,
},
};

try {
if (!parameters.credentials?.domain) {
return {
error: `'parameters.credentials.domain' must be set`,
verdict: true,
data: null,
};
}

if (!parameters.credentials?.apiKey) {
return {
error: `'parameters.credentials.apiKey' must be set`,
verdict: true,
data: null,
};
}

const url = `https://redact.${parameters.credentials.domain}/v1/redact_structured`;

const { content } = getCurrentContentPart(context, eventType);

if (!content) {
return {
error: { message: 'request or response json is empty' },
verdict: true,
data: null,
transformedData,
};
}

const requestOptions = {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'portkey-ai-plugin/' + VERSION,
Authorization: `Bearer ${parameters.credentials.apiKey}`,
},
};
const request = {
data: content,
...(Array.isArray(content) && content[0]?.type === 'text'
? { jsonp: ['$[*].text'] }
: {}),
};

const response = await post(url, request, requestOptions);

if (response.result?.count > 0 && response.result.redacted_data) {
setCurrentContentPart(
context,
eventType,
transformedData,
response.result.redacted_data
);
}

return {
error: null,
verdict: true,
data: {
summary: response.summary,
},
transformedData,
};
} catch (e) {
return {
error: e as Error,
verdict: true,
data: null,
transformedData,
};
}
};
33 changes: 33 additions & 0 deletions plugins/patronus/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,36 @@ export const postPatronus = async (

return post(BASE_URL, body, options, timeout);
};

interface Position {
positions: [number, number][];
extra: any;
confidence_interval: any;
}

// For finding all longest positions of equal length
interface AllLongestPositionsResult {
positions: [number, number][];
length: number;
}

export function findAllLongestPositions(
data: Position
): AllLongestPositionsResult | null {
if (!data?.positions?.length) {
return null;
}

// Calculate max length
const maxLength = Math.max(...data.positions.map((pos) => pos[1] - pos[0]));

// Find all positions with max length
const longestPositions = data.positions.filter(
(pos) => pos[1] - pos[0] === maxLength
);

return {
positions: longestPositions,
length: maxLength,
};
}
95 changes: 95 additions & 0 deletions plugins/patronus/redactPhi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';
import { getCurrentContentPart, setCurrentContentPart } from '../utils';
import { findAllLongestPositions, postPatronus } from './globals';
import { maskEntities } from './redactPii';

const redactPhi = async (text: string, credentials: any) => {
const evaluator = 'phi';

const evaluationBody: any = {
output: text,
};

const result: any = await postPatronus(
evaluator,
credentials,
evaluationBody
);
VisargD marked this conversation as resolved.
Show resolved Hide resolved
const evalResult = result.results[0];

const positionsData = evalResult.evaluation_result.additional_info;
if (
positionsData &&
positionsData.positions &&
positionsData.positions.length > 0
) {
VisargD marked this conversation as resolved.
Show resolved Hide resolved
const longestPosition = findAllLongestPositions(positionsData);
if (longestPosition?.positions && longestPosition.positions.length > 0) {
const maskedText = maskEntities(text, longestPosition.positions);
return maskedText;
}
}
return text;
};

export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
eventType: HookEventType
) => {
const transformedData: Record<string, any> = {
request: {
json: null,
},
response: {
json: null,
},
};

try {
const { content, textArray } = getCurrentContentPart(context, eventType);

if (!content) {
return {
error: { message: 'request or response json is empty' },
verdict: true,
data: null,
transformedData,
};
}

const transformedTextPromise = textArray.map((text) =>
redactPhi(text, parameters.credentials)
);

const transformedText = await Promise.all(transformedTextPromise);

setCurrentContentPart(
context,
eventType,
transformedData,
null,
transformedText
);

return {
error: null,
verdict: true,
data: null,
transformedData,
};
} catch (e: any) {
delete e.stack;
return {
error: e as Error,
verdict: true,
data: null,
transformedData,
};
}
};
Loading
Loading