feat(nx-dev): move openai call to edge function (#18747)
This commit is contained in:
parent
12db1e0c77
commit
bd76b6228f
@ -7,13 +7,7 @@ import {
|
|||||||
createClient,
|
createClient,
|
||||||
} from '@supabase/supabase-js';
|
} from '@supabase/supabase-js';
|
||||||
import GPT3Tokenizer from 'gpt3-tokenizer';
|
import GPT3Tokenizer from 'gpt3-tokenizer';
|
||||||
import {
|
import { CreateEmbeddingResponse, CreateCompletionResponseUsage } from 'openai';
|
||||||
Configuration,
|
|
||||||
OpenAIApi,
|
|
||||||
CreateModerationResponse,
|
|
||||||
CreateEmbeddingResponse,
|
|
||||||
CreateCompletionResponseUsage,
|
|
||||||
} from 'openai';
|
|
||||||
import {
|
import {
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
ChatItem,
|
ChatItem,
|
||||||
@ -23,6 +17,7 @@ import {
|
|||||||
getListOfSources,
|
getListOfSources,
|
||||||
getMessageFromResponse,
|
getMessageFromResponse,
|
||||||
initializeChat,
|
initializeChat,
|
||||||
|
openAiCall,
|
||||||
sanitizeLinksInResponse,
|
sanitizeLinksInResponse,
|
||||||
toMarkdownList,
|
toMarkdownList,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@ -37,13 +32,8 @@ const MIN_CONTENT_LENGTH = 50;
|
|||||||
// This is a temporary solution
|
// This is a temporary solution
|
||||||
const MAX_HISTORY_LENGTH = 30;
|
const MAX_HISTORY_LENGTH = 30;
|
||||||
|
|
||||||
const openAiKey = process.env['NX_OPENAI_KEY'];
|
|
||||||
const supabaseUrl = process.env['NX_NEXT_PUBLIC_SUPABASE_URL'];
|
const supabaseUrl = process.env['NX_NEXT_PUBLIC_SUPABASE_URL'];
|
||||||
const supabaseServiceKey = process.env['NX_SUPABASE_SERVICE_ROLE_KEY'];
|
const supabaseServiceKey = process.env['NX_SUPABASE_SERVICE_ROLE_KEY'];
|
||||||
const config = new Configuration({
|
|
||||||
apiKey: openAiKey,
|
|
||||||
});
|
|
||||||
const openai = new OpenAIApi(config);
|
|
||||||
|
|
||||||
let chatFullHistory: ChatItem[] = [];
|
let chatFullHistory: ChatItem[] = [];
|
||||||
|
|
||||||
@ -72,7 +62,7 @@ export async function queryAi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
checkEnvVariables(openAiKey, supabaseUrl, supabaseServiceKey);
|
checkEnvVariables(supabaseUrl, supabaseServiceKey);
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
throw new UserError('Missing query in request data');
|
throw new UserError('Missing query in request data');
|
||||||
@ -80,10 +70,12 @@ export async function queryAi(
|
|||||||
|
|
||||||
// Moderate the content to comply with OpenAI T&C
|
// Moderate the content to comply with OpenAI T&C
|
||||||
const sanitizedQuery = query.trim();
|
const sanitizedQuery = query.trim();
|
||||||
const moderationResponse: CreateModerationResponse = await openai
|
const moderationResponseObj = await openAiCall(
|
||||||
.createModeration({ input: sanitizedQuery })
|
{ input: sanitizedQuery },
|
||||||
.then((res) => res.data);
|
'moderation'
|
||||||
|
);
|
||||||
|
|
||||||
|
const moderationResponse = await moderationResponseObj.json();
|
||||||
const [results] = moderationResponse.results;
|
const [results] = moderationResponse.results;
|
||||||
|
|
||||||
if (results.flagged) {
|
if (results.flagged) {
|
||||||
@ -104,29 +96,29 @@ export async function queryAi(
|
|||||||
*
|
*
|
||||||
* How the solution looks like with previous response:
|
* How the solution looks like with previous response:
|
||||||
*
|
*
|
||||||
* const embeddingResponse = await openai.createEmbedding({
|
* const embeddingResponse = await openAiCall(
|
||||||
* model: 'text-embedding-ada-002',
|
* { input: sanitizedQuery + aiResponse },
|
||||||
* input: sanitizedQuery + aiResponse,
|
* 'embedding'
|
||||||
* });
|
* );
|
||||||
*
|
*
|
||||||
* This costs more tokens, so if we see conts skyrocket we remove it.
|
* This costs more tokens, so if we see costs skyrocket we remove it.
|
||||||
* As it says in the docs, it's a design decision, and it may or may not really improve results.
|
* As it says in the docs, it's a design decision, and it may or may not really improve results.
|
||||||
*/
|
*/
|
||||||
const embeddingResponse = await openai.createEmbedding({
|
const embeddingResponseObj = await openAiCall(
|
||||||
model: 'text-embedding-ada-002',
|
{ input: sanitizedQuery + aiResponse, model: 'text-embedding-ada-002' },
|
||||||
input: sanitizedQuery + aiResponse,
|
'embedding'
|
||||||
});
|
|
||||||
|
|
||||||
if (embeddingResponse.status !== 200) {
|
|
||||||
throw new ApplicationError(
|
|
||||||
'Failed to create embedding for question',
|
|
||||||
embeddingResponse
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!embeddingResponseObj.ok) {
|
||||||
|
throw new ApplicationError('Failed to create embedding for question', {
|
||||||
|
data: embeddingResponseObj.status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const embeddingResponse = await embeddingResponseObj.json();
|
||||||
const {
|
const {
|
||||||
data: [{ embedding }],
|
data: [{ embedding }],
|
||||||
}: CreateEmbeddingResponse = embeddingResponse.data;
|
}: CreateEmbeddingResponse = embeddingResponse;
|
||||||
|
|
||||||
const { error: matchError, data: pageSections } = await supabaseClient.rpc(
|
const { error: matchError, data: pageSections } = await supabaseClient.rpc(
|
||||||
'match_page_sections_2',
|
'match_page_sections_2',
|
||||||
@ -196,33 +188,39 @@ export async function queryAi(
|
|||||||
|
|
||||||
chatFullHistory = chatHistory;
|
chatFullHistory = chatHistory;
|
||||||
|
|
||||||
const response = await openai.createChatCompletion({
|
const responseObj = await openAiCall(
|
||||||
|
{
|
||||||
model: 'gpt-3.5-turbo-16k',
|
model: 'gpt-3.5-turbo-16k',
|
||||||
messages: chatGptMessages,
|
messages: chatGptMessages,
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
stream: false,
|
stream: false,
|
||||||
});
|
},
|
||||||
|
'chatCompletion'
|
||||||
|
);
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (!responseObj.ok) {
|
||||||
const error = response.data;
|
throw new ApplicationError('Failed to generate completion', {
|
||||||
throw new ApplicationError('Failed to generate completion', error);
|
data: responseObj.status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await responseObj.json();
|
||||||
|
|
||||||
// Message asking to double-check
|
// Message asking to double-check
|
||||||
const callout: string =
|
const callout: string =
|
||||||
'{% callout type="warning" title="Always double-check!" %}The results may not be accurate, so please always double check with our documentation.{% /callout %}\n';
|
'{% callout type="warning" title="Always double-check!" %}The results may not be accurate, so please always double check with our documentation.{% /callout %}\n';
|
||||||
// Append the warning message asking to double-check!
|
// Append the warning message asking to double-check!
|
||||||
const message = [callout, getMessageFromResponse(response.data)].join('');
|
const message = [callout, getMessageFromResponse(response)].join('');
|
||||||
|
|
||||||
const responseWithoutBadLinks = await sanitizeLinksInResponse(message);
|
const responseWithoutBadLinks = await sanitizeLinksInResponse(message);
|
||||||
|
|
||||||
const sources = getListOfSources(pageSections);
|
const sources = getListOfSources(pageSections);
|
||||||
|
|
||||||
totalTokensSoFar += response.data.usage?.total_tokens ?? 0;
|
totalTokensSoFar += response.usage?.total_tokens ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
textResponse: responseWithoutBadLinks,
|
textResponse: responseWithoutBadLinks,
|
||||||
usage: response.data.usage as CreateCompletionResponseUsage,
|
usage: response.usage as CreateCompletionResponseUsage,
|
||||||
sources,
|
sources,
|
||||||
sourcesMarkdown: toMarkdownList(sources),
|
sourcesMarkdown: toMarkdownList(sources),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -93,13 +93,9 @@ async function is404(url: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function checkEnvVariables(
|
export function checkEnvVariables(
|
||||||
openAiKey?: string,
|
|
||||||
supabaseUrl?: string,
|
supabaseUrl?: string,
|
||||||
supabaseServiceKey?: string
|
supabaseServiceKey?: string
|
||||||
) {
|
) {
|
||||||
if (!openAiKey) {
|
|
||||||
throw new ApplicationError('Missing environment variable NX_OPENAI_KEY');
|
|
||||||
}
|
|
||||||
if (!supabaseUrl) {
|
if (!supabaseUrl) {
|
||||||
throw new ApplicationError(
|
throw new ApplicationError(
|
||||||
'Missing environment variable NX_NEXT_PUBLIC_SUPABASE_URL'
|
'Missing environment variable NX_NEXT_PUBLIC_SUPABASE_URL'
|
||||||
@ -211,3 +207,17 @@ export interface ChatItem {
|
|||||||
role: ChatCompletionRequestMessageRoleEnum;
|
role: ChatCompletionRequestMessageRoleEnum;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openAiCall(
|
||||||
|
input: object,
|
||||||
|
action: 'moderation' | 'embedding' | 'chatCompletion'
|
||||||
|
) {
|
||||||
|
return fetch('/api/openai-handler', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action,
|
||||||
|
input: { ...input },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
45
nx-dev/nx-dev/pages/api/openai-handler.ts
Normal file
45
nx-dev/nx-dev/pages/api/openai-handler.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
const openAiKey = process.env['NX_OPENAI_KEY'];
|
||||||
|
export const config = {
|
||||||
|
runtime: 'edge',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function handler(request: NextRequest) {
|
||||||
|
const { action, input } = await request.json();
|
||||||
|
|
||||||
|
let apiUrl = 'https://api.openai.com/v1/';
|
||||||
|
|
||||||
|
if (action === 'embedding') {
|
||||||
|
apiUrl += 'embeddings';
|
||||||
|
} else if (action === 'chatCompletion') {
|
||||||
|
apiUrl += 'chat/completions';
|
||||||
|
} else if (action === 'moderation') {
|
||||||
|
apiUrl += 'moderations';
|
||||||
|
} else {
|
||||||
|
return new Response('Invalid action', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${openAiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(responseData), {
|
||||||
|
status: response.status,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error processing the request:', e.message);
|
||||||
|
return new Response(e.message, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user