UNPKG

@twilio-alpha/assistants-eval

Version:

promptfoo extension for writing AI evaluations for Twilio AI Assistants

250 lines (219 loc) 6.89 kB
import { randomUUID } from 'crypto'; import { CallApiContextParams, CallApiOptionsParams, EnvOverrides, ProviderOptions, ProviderResponse, } from 'promptfoo'; import { getOptionalEnv, getRequiredEnv } from '../utils/envs'; import ExternalProvider from './external'; type Realm = 'dev' | 'stage' | 'prod'; type BaseResponse = { aborted: boolean; account_sid: string; status: 'Success' | 'Failure'; body: string; }; type SuccessResponse = BaseResponse & { status: 'Success'; body: string; flagged: boolean; session_id: string; }; type FailureResponse = BaseResponse & { status: 'Failure'; error: string; }; type AssistantResponse = SuccessResponse | FailureResponse; type EnvOverridesWithTwilio = EnvOverrides & { TWILIO_ACCOUNT_SID?: string; TWILIO_AUTH_TOKEN?: string; TWILIO_REGION?: string; TWILIO_ASSISTANT_ID?: string; }; export type TwilioProviderOptions = ProviderOptions & { config: { twilioRegion?: Realm; assistantId?: string; accountSid?: string; authTokenKey?: string; sessionId?: string; identity?: string; constellationOverride?: string; }; env: EnvOverridesWithTwilio; }; export type TwilioProviderResponse = ProviderResponse & { metadata?: { sessionId: string; assistantId: string; flagged: boolean; accountSid: string; }; }; export function getAssistantUrl(assistantId: string, region: string) { const sub = region === 'prod' ? '' : `${region}.`; return `https://assistants.${sub}twilio.com/v1/Assistants/${assistantId}/Messages`; } export default class TwilioProvider extends ExternalProvider { private readonly sessionId?: string; private readonly userIdentity: string; private readonly region: string; constructor(options: TwilioProviderOptions) { const { config, id, label, env } = options; const region = config.twilioRegion || env?.TWILIO_REGION || getOptionalEnv('TWILIO_REGION') || 'prod'; const assistantIdConfigValue = typeof config.assistantId === 'string' ? config.assistantId : config.assistantId?.[region]; const assistantId = assistantIdConfigValue || env?.TWILIO_ASSISTANT_ID || getOptionalEnv<string>('TWILIO_ASSISTANT_ID') || 'invalid-assistant-id'; // not throwing an error so that you can use the provider by setting assistantId only as variable on a test const url = getAssistantUrl(assistantId, region); const username = config.accountSid || env?.TWILIO_ACCOUNT_SID || getRequiredEnv<string>('TWILIO_ACCOUNT_SID'); const password = config.authTokenKey ? getRequiredEnv<string>(config.authTokenKey) : env?.TWILIO_AUTH_TOKEN || getRequiredEnv<string>('TWILIO_AUTH_TOKEN'); const auth = Buffer.from(`${username}:${password}`).toString('base64'); const headers = {}; if (config.constellationOverride) { headers['User-Agent'] = `constellationName/${config.constellationOverride}`; } super({ id, label, config: { url, identifier: 'twilio-provider', requestOptions: { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Basic ${auth}`, ...headers, }, }, }, env, }); this.userIdentity = config.identity ?? 'test-evaluator'; this.sessionId = config.sessionId; this.region = region; } protected getUrl( prompt: string, context?: CallApiContextParams, callApiOptions?: CallApiOptionsParams, ) { let url = super.getUrl(prompt, context, callApiOptions); if (typeof context?.vars.assistantId === 'string') { url = getAssistantUrl(context.vars.assistantId, this.region); } else if (typeof context?.vars.assistantId === 'object') { if (typeof context.vars.assistantId[this.region] === 'string') { url = getAssistantUrl( context.vars.assistantId[this.region], this.region, ); } } return url; } protected getBody( prompt: string, context?: CallApiContextParams, _callApiOptions?: CallApiOptionsParams, ): string { let sessionId = this.sessionId ?? randomUUID(); if (context?.vars.sessionId) { if (!context.vars.runId) { throw new Error( ` Invalid configuration. Can only use sessionId variable if you defined a runId as well. Make sure your promptfooconfig.yaml has the following set: defaultTest: vars: runId: package:@twilio-alpha/assistants-eval:variableHelpers.runId `.trim(), ); } sessionId = `${context.vars.sessionId}_${context.vars.runId}`; } let processedPrompt = prompt; try { // check if the input is actually an array of messages and just send the last one if (processedPrompt.trim().startsWith('[')) { const parsed = JSON.parse(processedPrompt); if ( Array.isArray(parsed) && parsed.every( (message) => typeof message.role === 'string' && typeof message.content === 'string', ) ) { if (!context?.vars.sessionId) { throw new Error( 'In order to use this feature you need to have a sessionId defined as variable.', ); } processedPrompt = parsed[parsed.length - 1].content; } } } catch (err) { this.logger.debug(err); } const identity = typeof context?.vars.identity === 'string' ? context?.vars.identity : this.userIdentity; if (identity.includes('{{runId}}')) { if (typeof context?.vars.runId !== 'string') { throw new Error( ` Invalid configuration. Can only use {{runId}} in the identity variable if you defined a runId as well. Make sure your promptfooconfig.yaml has the following set: defaultTest: vars: runId: file://../../node_modules/@twilio-alpha/assistants-ai/dist/utils/evalRunId.js `.trim(), ); } } return JSON.stringify({ body: processedPrompt, session_id: sessionId, identity: context?.vars.identity ?? this.userIdentity, }); } protected async getResponse( response: Response, ): Promise<TwilioProviderResponse> { const data = (await response.json()) as AssistantResponse; this.logger.debug('AI Assistant response is %s', JSON.stringify(data)); if (data.status === 'Success') { const url = new URL(response.url); const assistantId = url.pathname.split('/')[3]; return { output: data.body, metadata: { sessionId: `webhook:${data.session_id}`, assistantId, flagged: data.flagged, accountSid: data.account_sid, }, }; } return { error: data.error, }; } }