Skip to content

Commit

Permalink
feat(nx-dev): new page for ai docs (#18025)
Browse files Browse the repository at this point in the history
  • Loading branch information
mandarini authored Jul 13, 2023
1 parent e657de8 commit 6ccbbbc
Show file tree
Hide file tree
Showing 27 changed files with 1,001 additions and 92 deletions.
18 changes: 18 additions & 0 deletions nx-dev/data-access-ai/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
11 changes: 11 additions & 0 deletions nx-dev/data-access-ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# nx-dev-data-access-ai

This library was generated with [Nx](https://nx.dev).

## Building

Run `nx build nx-dev-data-access-ai` to build the library.

## Running unit tests

Run `nx test nx-dev-data-access-ai` to execute the unit tests via [Jest](https://jestjs.io).
11 changes: 11 additions & 0 deletions nx-dev/data-access-ai/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'nx-dev-data-access-ai',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/nx-dev/data-access-ai',
};
5 changes: 5 additions & 0 deletions nx-dev/data-access-ai/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@nx/nx-dev/data-access-ai",
"version": "0.0.1",
"type": "commonjs"
}
40 changes: 40 additions & 0 deletions nx-dev/data-access-ai/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "nx-dev-data-access-ai",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/data-access-ai/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/nx-dev/data-access-ai",
"main": "nx-dev/data-access-ai/src/index.ts",
"tsConfig": "nx-dev/data-access-ai/tsconfig.lib.json",
"assets": ["nx-dev/data-access-ai/*.md"]
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["nx-dev/data-access-ai/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "nx-dev/data-access-ai/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": []
}
2 changes: 2 additions & 0 deletions nx-dev/data-access-ai/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/data-access-ai';
export * from './lib/utils';
192 changes: 192 additions & 0 deletions nx-dev/data-access-ai/src/lib/data-access-ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// based on:
// https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/pages/api/vector-search.ts

import { createClient } from '@supabase/supabase-js';
import GPT3Tokenizer from 'gpt3-tokenizer';
import {
Configuration,
OpenAIApi,
CreateModerationResponse,
CreateEmbeddingResponse,
ChatCompletionRequestMessageRoleEnum,
CreateCompletionResponseUsage,
} from 'openai';
import { getMessageFromResponse, sanitizeLinksInResponse } from './utils';

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);

export async function nxDevDataAccessAi(
query: string
): Promise<{ textResponse: string; usage?: CreateCompletionResponseUsage }> {
try {
if (!openAiKey) {
throw new ApplicationError('Missing environment variable NX_OPENAI_KEY');
}

if (!supabaseUrl) {
throw new ApplicationError(
'Missing environment variable NX_NEXT_PUBLIC_SUPABASE_URL'
);
}

if (!supabaseServiceKey) {
throw new ApplicationError(
'Missing environment variable NX_SUPABASE_SERVICE_ROLE_KEY'
);
}

if (!query) {
throw new UserError('Missing query in request data');
}

const supabaseClient = createClient(supabaseUrl, supabaseServiceKey);

// 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
const embeddingResponse = await openai.createEmbedding({
model: 'text-embedding-ada-002',
input: sanitizedQuery,
});

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',
{
embedding,
match_threshold: 0.78,
match_count: 10,
min_content_length: 50,
}
);

if (matchError) {
throw new ApplicationError('Failed to match page sections', matchError);
}

const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
let tokenCount = 0;
let contextText = '';

for (let i = 0; i < pageSections.length; i++) {
const pageSection = pageSections[i];
const content = pageSection.content;
const encoded = tokenizer.encode(content);
tokenCount += encoded.text.length;

if (tokenCount >= 1500) {
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 should answer queries using ONLY that information.
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, much like the
existing Nx documentation. Include a title, and subsections, if it makes sense.
Mark the titles and the subsections with the appropriate markdown syntax.
If you are unsure and the answer is not explicitly written 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.
Answer as markdown (including related code snippets if available).
`
.replace(/\s+/g, ' ')
.trim()}
`;

const chatGptMessages = [
{
role: ChatCompletionRequestMessageRoleEnum.System,
content: prompt,
},
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: contextText,
},
{
role: ChatCompletionRequestMessageRoleEnum.User,
content: sanitizedQuery,
},
];

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);
}

const message = getMessageFromResponse(response.data);

const responseWithoutBadLinks = await sanitizeLinksInResponse(message);

return {
textResponse: responseWithoutBadLinks,
usage: response.data.usage,
};
} 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 class ApplicationError extends Error {
constructor(message: string, public data: Record<string, any> = {}) {
super(message);
}
}

export class UserError extends ApplicationError {}
50 changes: 50 additions & 0 deletions nx-dev/data-access-ai/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CreateChatCompletionResponse } from 'openai';

export function getMessageFromResponse(
response: CreateChatCompletionResponse
): string {
/**
*
* This function here will or may be enhanced
* once we add more functionality
*/
return response.choices[0].message?.content ?? '';
}

export async function sanitizeLinksInResponse(
response: string
): Promise<string> {
const regex = /https:\/\/nx\.dev[^) \n]*[^).]/g;
const urls = response.match(regex);

if (urls) {
for (const url of urls) {
const linkIsWrong = await is404(url);
if (linkIsWrong) {
response = response.replace(
url,
'https://nx.dev/getting-started/intro'
);
}
}
}

return response;
}

async function is404(url: string): Promise<boolean> {
try {
const response = await fetch(url.replace('https://nx.dev', ''));
if (response.status === 404) {
return true;
} else {
return false;
}
} catch (error) {
if ((error as any)?.response?.status === 404) {
return true;
} else {
return false;
}
}
}
24 changes: 24 additions & 0 deletions nx-dev/data-access-ai/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"target": "es2021",
"lib": ["es2021", "DOM"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
10 changes: 10 additions & 0 deletions nx-dev/data-access-ai/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
14 changes: 14 additions & 0 deletions nx-dev/data-access-ai/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
Loading

1 comment on commit 6ccbbbc

@vercel
Copy link

@vercel vercel bot commented on 6ccbbbc Jul 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx.dev
nx-dev-git-master-nrwl.vercel.app
nx-five.vercel.app
nx-dev-nrwl.vercel.app

Please sign in to comment.