-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
data-access-ai.ts
267 lines (227 loc) · 8.2 KB
/
data-access-ai.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// based on:
// https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/pages/api/vector-search.ts
import {
PostgrestSingleResponse,
SupabaseClient,
createClient,
} from '@supabase/supabase-js';
import GPT3Tokenizer from 'gpt3-tokenizer';
import {
Configuration,
OpenAIApi,
CreateModerationResponse,
CreateEmbeddingResponse,
CreateCompletionResponseUsage,
} from 'openai';
import {
ApplicationError,
ChatItem,
PageSection,
UserError,
checkEnvVariables,
getListOfSources,
getMessageFromResponse,
initializeChat,
sanitizeLinksInResponse,
toMarkdownList,
} from './utils';
const DEFAULT_MATCH_THRESHOLD = 0.78;
const DEFAULT_MATCH_COUNT = 15;
const MIN_CONTENT_LENGTH = 50;
// This limits history to 30 messages back and forth
// It's arbitrary, but also generous
// History length should be based on token count
// This is a temporary solution
const MAX_HISTORY_LENGTH = 30;
const openAiKey = process.env['NX_OPENAI_KEY'];
const supabaseUrl = process.env['NX_NEXT_PUBLIC_SUPABASE_URL'];
const supabaseServiceKey = process.env['NX_SUPABASE_SERVICE_ROLE_KEY'];
const config = new Configuration({
apiKey: openAiKey,
});
const openai = new OpenAIApi(config);
let chatFullHistory: ChatItem[] = [];
let totalTokensSoFar = 0;
let supabaseClient: SupabaseClient<any, 'public', any>;
export async function queryAi(
query: string,
aiResponse?: string
): Promise<{
textResponse: string;
usage?: CreateCompletionResponseUsage;
sources: { heading: string; url: string }[];
sourcesMarkdown: string;
}> {
if (!supabaseClient) {
supabaseClient = createClient(
supabaseUrl as string,
supabaseServiceKey as string
);
}
if (chatFullHistory.length > MAX_HISTORY_LENGTH) {
chatFullHistory.slice(0, MAX_HISTORY_LENGTH - 4);
}
try {
checkEnvVariables(openAiKey, supabaseUrl, supabaseServiceKey);
if (!query) {
throw new UserError('Missing query in request data');
}
// Moderate the content to comply with OpenAI T&C
const sanitizedQuery = query.trim();
const moderationResponse: CreateModerationResponse = await openai
.createModeration({ input: sanitizedQuery })
.then((res) => res.data);
const [results] = moderationResponse.results;
if (results.flagged) {
throw new UserError('Flagged content', {
flagged: true,
categories: results.categories,
});
}
// Create embedding from query
// NOTE: Here, we may or may not want to include the previous AI response
/**
* For retrieving relevant Nx documentation sections via embeddings, it's a design decision.
* Including the prior response might give more contextually relevant sections,
* but just sending the query might suffice for many cases.
*
* We can experiment with this.
*
* How the solution looks like with previous response:
*
* const embeddingResponse = await openai.createEmbedding({
* model: 'text-embedding-ada-002',
* input: sanitizedQuery + aiResponse,
* });
*
* This costs more tokens, so if we see conts skyrocket we remove it.
* 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({
model: 'text-embedding-ada-002',
input: sanitizedQuery + aiResponse,
});
if (embeddingResponse.status !== 200) {
throw new ApplicationError(
'Failed to create embedding for question',
embeddingResponse
);
}
const {
data: [{ embedding }],
}: CreateEmbeddingResponse = embeddingResponse.data;
const { error: matchError, data: pageSections } = await supabaseClient.rpc(
'match_page_sections_2',
{
embedding,
match_threshold: DEFAULT_MATCH_THRESHOLD,
match_count: DEFAULT_MATCH_COUNT,
min_content_length: MIN_CONTENT_LENGTH,
}
);
if (matchError) {
throw new ApplicationError('Failed to match page sections', matchError);
}
// Note: this is experimental. I think it should work
// mainly because we're testing previous response + query.
if (!pageSections || pageSections.length === 0) {
throw new UserError('No results found.', { no_results: true });
}
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
let tokenCount = 0;
let contextText = '';
for (let i = 0; i < (pageSections as PageSection[]).length; i++) {
const pageSection: PageSection = pageSections[i];
const content = pageSection.content;
const encoded = tokenizer.encode(content);
tokenCount += encoded.text.length;
if (tokenCount >= 2500) {
break;
}
contextText += `${content.trim()}\n---\n`;
}
const prompt = `
${`
You are a knowledgeable Nx representative.
Your knowledge is based entirely on the official Nx Documentation.
You can answer queries using ONLY that information.
You cannot answer queries using your own knowledge or experience.
Answer in markdown format. Always give an example, answer as thoroughly as you can, and
always provide a link to relevant documentation
on the https://nx.dev website. All the links you find or post
that look like local or relative links, always prepend with "https://nx.dev".
Your answer should be in the form of a Markdown article
(including related code snippets if available), much like the
existing Nx documentation. Mark the titles and the subsections with the appropriate markdown syntax.
If you are unsure and cannot find an answer in the Nx Documentation, say
"Sorry, I don't know how to help with that. You can visit the [Nx documentation](https://nx.dev/getting-started/intro) for more info."
Remember, answer the question using ONLY the information provided in the Nx Documentation.
`
.replace(/\s+/g, ' ')
.trim()}
`;
const { chatMessages: chatGptMessages, chatHistory } = initializeChat(
chatFullHistory,
query,
contextText,
prompt,
aiResponse
);
chatFullHistory = chatHistory;
const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo-16k',
messages: chatGptMessages,
temperature: 0,
stream: false,
});
if (response.status !== 200) {
const error = response.data;
throw new ApplicationError('Failed to generate completion', error);
}
// Message asking to double-check
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';
// Append the warning message asking to double-check!
const message = [callout, getMessageFromResponse(response.data)].join('');
const responseWithoutBadLinks = await sanitizeLinksInResponse(message);
const sources = getListOfSources(pageSections);
totalTokensSoFar += response.data.usage?.total_tokens ?? 0;
return {
textResponse: responseWithoutBadLinks,
usage: response.data.usage as CreateCompletionResponseUsage,
sources,
sourcesMarkdown: toMarkdownList(sources),
};
} catch (err: unknown) {
if (err instanceof UserError) {
console.error(err.message);
} else if (err instanceof ApplicationError) {
// Print out application errors with their additional data
console.error(`${err.message}: ${JSON.stringify(err.data)}`);
} else {
// Print out unexpected errors as is to help with debugging
console.error(err);
}
// TODO: include more response info in debug environments
console.error(err);
throw err;
}
}
export function resetHistory() {
chatFullHistory = [];
totalTokensSoFar = 0;
}
export function getHistory(): ChatItem[] {
return chatFullHistory;
}
export async function sendFeedbackAnalytics(feedback: {}): Promise<
PostgrestSingleResponse<null>
> {
return supabaseClient.from('feedback').insert(feedback);
}
export async function sendQueryAnalytics(queryInfo: {}) {
const { error } = await supabaseClient.from('user_queries').insert(queryInfo);
if (error) {
console.error('Error storing the query info in Supabase: ', error);
}
}