Skip to content

Commit 6ccbbbc

Browse files
authored
feat(nx-dev): new page for ai docs (nrwl#18025)
1 parent e657de8 commit 6ccbbbc

27 files changed

+1001
-92
lines changed

nx-dev/data-access-ai/.eslintrc.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"extends": ["../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7+
"rules": {}
8+
},
9+
{
10+
"files": ["*.ts", "*.tsx"],
11+
"rules": {}
12+
},
13+
{
14+
"files": ["*.js", "*.jsx"],
15+
"rules": {}
16+
}
17+
]
18+
}

nx-dev/data-access-ai/README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# nx-dev-data-access-ai
2+
3+
This library was generated with [Nx](https://nx.dev).
4+
5+
## Building
6+
7+
Run `nx build nx-dev-data-access-ai` to build the library.
8+
9+
## Running unit tests
10+
11+
Run `nx test nx-dev-data-access-ai` to execute the unit tests via [Jest](https://jestjs.io).

nx-dev/data-access-ai/jest.config.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* eslint-disable */
2+
export default {
3+
displayName: 'nx-dev-data-access-ai',
4+
preset: '../../jest.preset.js',
5+
testEnvironment: 'node',
6+
transform: {
7+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
8+
},
9+
moduleFileExtensions: ['ts', 'js', 'html'],
10+
coverageDirectory: '../../coverage/nx-dev/data-access-ai',
11+
};

nx-dev/data-access-ai/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "@nx/nx-dev/data-access-ai",
3+
"version": "0.0.1",
4+
"type": "commonjs"
5+
}

nx-dev/data-access-ai/project.json

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "nx-dev-data-access-ai",
3+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "nx-dev/data-access-ai/src",
5+
"projectType": "library",
6+
"targets": {
7+
"build": {
8+
"executor": "@nx/js:tsc",
9+
"outputs": ["{options.outputPath}"],
10+
"options": {
11+
"outputPath": "dist/nx-dev/data-access-ai",
12+
"main": "nx-dev/data-access-ai/src/index.ts",
13+
"tsConfig": "nx-dev/data-access-ai/tsconfig.lib.json",
14+
"assets": ["nx-dev/data-access-ai/*.md"]
15+
}
16+
},
17+
"lint": {
18+
"executor": "@nx/linter:eslint",
19+
"outputs": ["{options.outputFile}"],
20+
"options": {
21+
"lintFilePatterns": ["nx-dev/data-access-ai/**/*.ts"]
22+
}
23+
},
24+
"test": {
25+
"executor": "@nx/jest:jest",
26+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
27+
"options": {
28+
"jestConfig": "nx-dev/data-access-ai/jest.config.ts",
29+
"passWithNoTests": true
30+
},
31+
"configurations": {
32+
"ci": {
33+
"ci": true,
34+
"codeCoverage": true
35+
}
36+
}
37+
}
38+
},
39+
"tags": []
40+
}

nx-dev/data-access-ai/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './lib/data-access-ai';
2+
export * from './lib/utils';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// based on:
2+
// https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/pages/api/vector-search.ts
3+
4+
import { createClient } from '@supabase/supabase-js';
5+
import GPT3Tokenizer from 'gpt3-tokenizer';
6+
import {
7+
Configuration,
8+
OpenAIApi,
9+
CreateModerationResponse,
10+
CreateEmbeddingResponse,
11+
ChatCompletionRequestMessageRoleEnum,
12+
CreateCompletionResponseUsage,
13+
} from 'openai';
14+
import { getMessageFromResponse, sanitizeLinksInResponse } from './utils';
15+
16+
const openAiKey = process.env['NX_OPENAI_KEY'];
17+
const supabaseUrl = process.env['NX_NEXT_PUBLIC_SUPABASE_URL'];
18+
const supabaseServiceKey = process.env['NX_SUPABASE_SERVICE_ROLE_KEY'];
19+
const config = new Configuration({
20+
apiKey: openAiKey,
21+
});
22+
const openai = new OpenAIApi(config);
23+
24+
export async function nxDevDataAccessAi(
25+
query: string
26+
): Promise<{ textResponse: string; usage?: CreateCompletionResponseUsage }> {
27+
try {
28+
if (!openAiKey) {
29+
throw new ApplicationError('Missing environment variable NX_OPENAI_KEY');
30+
}
31+
32+
if (!supabaseUrl) {
33+
throw new ApplicationError(
34+
'Missing environment variable NX_NEXT_PUBLIC_SUPABASE_URL'
35+
);
36+
}
37+
38+
if (!supabaseServiceKey) {
39+
throw new ApplicationError(
40+
'Missing environment variable NX_SUPABASE_SERVICE_ROLE_KEY'
41+
);
42+
}
43+
44+
if (!query) {
45+
throw new UserError('Missing query in request data');
46+
}
47+
48+
const supabaseClient = createClient(supabaseUrl, supabaseServiceKey);
49+
50+
// Moderate the content to comply with OpenAI T&C
51+
const sanitizedQuery = query.trim();
52+
const moderationResponse: CreateModerationResponse = await openai
53+
.createModeration({ input: sanitizedQuery })
54+
.then((res) => res.data);
55+
56+
const [results] = moderationResponse.results;
57+
58+
if (results.flagged) {
59+
throw new UserError('Flagged content', {
60+
flagged: true,
61+
categories: results.categories,
62+
});
63+
}
64+
65+
// Create embedding from query
66+
const embeddingResponse = await openai.createEmbedding({
67+
model: 'text-embedding-ada-002',
68+
input: sanitizedQuery,
69+
});
70+
71+
if (embeddingResponse.status !== 200) {
72+
throw new ApplicationError(
73+
'Failed to create embedding for question',
74+
embeddingResponse
75+
);
76+
}
77+
78+
const {
79+
data: [{ embedding }],
80+
}: CreateEmbeddingResponse = embeddingResponse.data;
81+
82+
const { error: matchError, data: pageSections } = await supabaseClient.rpc(
83+
'match_page_sections',
84+
{
85+
embedding,
86+
match_threshold: 0.78,
87+
match_count: 10,
88+
min_content_length: 50,
89+
}
90+
);
91+
92+
if (matchError) {
93+
throw new ApplicationError('Failed to match page sections', matchError);
94+
}
95+
96+
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
97+
let tokenCount = 0;
98+
let contextText = '';
99+
100+
for (let i = 0; i < pageSections.length; i++) {
101+
const pageSection = pageSections[i];
102+
const content = pageSection.content;
103+
const encoded = tokenizer.encode(content);
104+
tokenCount += encoded.text.length;
105+
106+
if (tokenCount >= 1500) {
107+
break;
108+
}
109+
110+
contextText += `${content.trim()}\n---\n`;
111+
}
112+
113+
const prompt = `
114+
${`
115+
You are a knowledgeable Nx representative.
116+
Your knowledge is based entirely on the official Nx documentation.
117+
You should answer queries using ONLY that information.
118+
Answer in markdown format. Always give an example, answer as thoroughly as you can, and
119+
always provide a link to relevant documentation
120+
on the https://nx.dev website. All the links you find or post
121+
that look like local or relative links, always prepend with "https://nx.dev".
122+
Your answer should be in the form of a Markdown article, much like the
123+
existing Nx documentation. Include a title, and subsections, if it makes sense.
124+
Mark the titles and the subsections with the appropriate markdown syntax.
125+
If you are unsure and the answer is not explicitly written in the Nx documentation, say
126+
"Sorry, I don't know how to help with that.
127+
You can visit the [Nx documentation](https://nx.dev/getting-started/intro) for more info."
128+
Remember, answer the question using ONLY the information provided in the Nx documentation.
129+
Answer as markdown (including related code snippets if available).
130+
`
131+
.replace(/\s+/g, ' ')
132+
.trim()}
133+
`;
134+
135+
const chatGptMessages = [
136+
{
137+
role: ChatCompletionRequestMessageRoleEnum.System,
138+
content: prompt,
139+
},
140+
{
141+
role: ChatCompletionRequestMessageRoleEnum.Assistant,
142+
content: contextText,
143+
},
144+
{
145+
role: ChatCompletionRequestMessageRoleEnum.User,
146+
content: sanitizedQuery,
147+
},
148+
];
149+
150+
const response = await openai.createChatCompletion({
151+
model: 'gpt-3.5-turbo-16k',
152+
messages: chatGptMessages,
153+
temperature: 0,
154+
stream: false,
155+
});
156+
157+
if (response.status !== 200) {
158+
const error = response.data;
159+
throw new ApplicationError('Failed to generate completion', error);
160+
}
161+
162+
const message = getMessageFromResponse(response.data);
163+
164+
const responseWithoutBadLinks = await sanitizeLinksInResponse(message);
165+
166+
return {
167+
textResponse: responseWithoutBadLinks,
168+
usage: response.data.usage,
169+
};
170+
} catch (err: unknown) {
171+
if (err instanceof UserError) {
172+
console.error(err.message);
173+
} else if (err instanceof ApplicationError) {
174+
// Print out application errors with their additional data
175+
console.error(`${err.message}: ${JSON.stringify(err.data)}`);
176+
} else {
177+
// Print out unexpected errors as is to help with debugging
178+
console.error(err);
179+
}
180+
181+
// TODO: include more response info in debug environments
182+
console.error(err);
183+
throw err;
184+
}
185+
}
186+
export class ApplicationError extends Error {
187+
constructor(message: string, public data: Record<string, any> = {}) {
188+
super(message);
189+
}
190+
}
191+
192+
export class UserError extends ApplicationError {}
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { CreateChatCompletionResponse } from 'openai';
2+
3+
export function getMessageFromResponse(
4+
response: CreateChatCompletionResponse
5+
): string {
6+
/**
7+
*
8+
* This function here will or may be enhanced
9+
* once we add more functionality
10+
*/
11+
return response.choices[0].message?.content ?? '';
12+
}
13+
14+
export async function sanitizeLinksInResponse(
15+
response: string
16+
): Promise<string> {
17+
const regex = /https:\/\/nx\.dev[^) \n]*[^).]/g;
18+
const urls = response.match(regex);
19+
20+
if (urls) {
21+
for (const url of urls) {
22+
const linkIsWrong = await is404(url);
23+
if (linkIsWrong) {
24+
response = response.replace(
25+
url,
26+
'https://nx.dev/getting-started/intro'
27+
);
28+
}
29+
}
30+
}
31+
32+
return response;
33+
}
34+
35+
async function is404(url: string): Promise<boolean> {
36+
try {
37+
const response = await fetch(url.replace('https://nx.dev', ''));
38+
if (response.status === 404) {
39+
return true;
40+
} else {
41+
return false;
42+
}
43+
} catch (error) {
44+
if ((error as any)?.response?.status === 404) {
45+
return true;
46+
} else {
47+
return false;
48+
}
49+
}
50+
}

nx-dev/data-access-ai/tsconfig.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"module": "commonjs",
5+
"forceConsistentCasingInFileNames": true,
6+
"strict": true,
7+
"noImplicitOverride": true,
8+
"noPropertyAccessFromIndexSignature": true,
9+
"noImplicitReturns": true,
10+
"noFallthroughCasesInSwitch": true,
11+
"target": "es2021",
12+
"lib": ["es2021", "DOM"]
13+
},
14+
"files": [],
15+
"include": [],
16+
"references": [
17+
{
18+
"path": "./tsconfig.lib.json"
19+
},
20+
{
21+
"path": "./tsconfig.spec.json"
22+
}
23+
]
24+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../../dist/out-tsc",
5+
"declaration": true,
6+
"types": ["node"]
7+
},
8+
"include": ["src/**/*.ts"],
9+
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
10+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../../dist/out-tsc",
5+
"module": "commonjs",
6+
"types": ["jest", "node"]
7+
},
8+
"include": [
9+
"jest.config.ts",
10+
"src/**/*.test.ts",
11+
"src/**/*.spec.ts",
12+
"src/**/*.d.ts"
13+
]
14+
}

0 commit comments

Comments
 (0)