├── .example.env ├── .gitignore ├── README.md ├── app.vue ├── assets └── style │ └── vuetify.scss ├── components └── ui │ ├── Chat.vue │ ├── Header.vue │ └── Hero.vue ├── nuxt.config.ts ├── package.json ├── plugins ├── vercel.ts └── vuetify.ts ├── prettier.config.js ├── public ├── cover.jpg ├── favicon.ico └── img │ └── screenshot.jpg ├── scripts └── create-embeddings.js ├── server └── api │ └── chat.post.ts ├── tsconfig.json └── yarn.lock /.example.env: -------------------------------------------------------------------------------- 1 | NUXT_OPENAI_API_KEY= 2 | NUXT_SUPABASE_URL= 3 | NUXT_SUPABASE_KEY= 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | .env.build 62 | 63 | # parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js build output 67 | .next 68 | 69 | # nuxt.js build output 70 | .nuxt 71 | .output 72 | 73 | # Nuxt generate 74 | dist 75 | 76 | # vuepress build output 77 | .vuepress/dist 78 | 79 | # Serverless directories 80 | .serverless 81 | 82 | # IDE / Editor 83 | .idea 84 | 85 | # Service worker 86 | sw.* 87 | 88 | # macOS 89 | .DS_Store 90 | 91 | # Vim swap files 92 | *.swp 93 | 94 | # Vercel 95 | .vercel 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Discontinued, not hosted anymore. 2 | 3 | # [Replicate SupportGPT](https://replicate-support-gpt.vercel.app/) 4 | 5 | Make ChatGPT answer questions based on **your** documentation. 6 | 7 | [![Replicate SupportGPT](./public/img/screenshot.jpg)](https://replicate-support-gpt.vercel.app/) 8 | 9 | ## How it works 10 | 11 | The goal is to make ChatGPT answer questions within a limited context, where the context is a relevant section of a larger documentation. To do this we use [embeddings](https://platform.openai.com/docs/guides/embeddings). In short, embeddings are tokens converted into vectors that can be used to calculate how closely related two strings are. If we split the documentation into chunks and encode them as embeddings in a vector database, we can query relevant documentation chunks later if we use the same encoding on questions. The relevant documentation chunks will then be used as context for a ChatGPT session. 12 | 13 | This app is powered by: 14 | 15 | ⚡️ [Supabase](https://supabase.com/), for vector based database. 16 | 17 | ▲ [Vercel](https://vercel.com/), a platform for running web apps. 18 | 19 | ⚡️ Nuxt.js [server-side API handlers](server/api), for talking to the Supabase database. 20 | 21 | 📦 [Vuetify](https://vuetifyjs.com/en/), a Vue.js component framework for the browser UI. 22 | 23 | ## Setup Supabase 24 | 25 | Supabase supports vectors in their PostgreSQL database. Create an account and execute the following queries: 26 | 27 | Enable the vector extension: 28 | 29 | ```sql 30 | create extension vector; 31 | ``` 32 | 33 | Create a table for the documentation chunks: 34 | 35 | ```sql 36 | create table documents ( 37 | id bigserial primary key, 38 | content text, 39 | url text, 40 | embedding vector (1536) 41 | ); 42 | ``` 43 | 44 | Create a PostgreSQL function that uses the `<=>` cosine distance operator to get similar documentation chunks. 45 | 46 | ```sql 47 | create or replace function match_documents ( 48 | query_embedding vector(1536), 49 | similarity_threshold float, 50 | match_count int 51 | ) 52 | returns table ( 53 | id bigint, 54 | content text, 55 | url text, 56 | similarity float 57 | ) 58 | language plpgsql 59 | as $$ 60 | begin 61 | return query 62 | select 63 | documents.id, 64 | documents.content, 65 | documents.url, 66 | 1 - (documents.embedding <=> query_embedding) as similarity 67 | from documents 68 | where 1 - (documents.embedding <=> query_embedding) > similarity_threshold 69 | order by similarity desc 70 | limit match_count; 71 | end; 72 | $$; 73 | ``` 74 | 75 | ## Run it locally 76 | 77 | You need an [OpenAI API key](https://platform.openai.com/account/api-keys), a Supabase URL and Supabase API key (you can find these in the Supabase web portal under Project → API). Copy the contents of [.example.env](.example.env) into a new file in the root of your directory called `.env` and insert the API keys there, like this: 78 | 79 | ```bash 80 | NUXT_OPENAI_API_KEY= 81 | NUXT_SUPABASE_URL= 82 | NUXT_SUPABASE_KEY= 83 | ``` 84 | 85 | Then, install the dependencies and run the local development server: 86 | 87 | ```bash 88 | npm install 89 | npm run dev 90 | ``` 91 | 92 | Open http://localhost:3000 in your web browser. Done! 93 | 94 | ## Populate vector database with embeddings 95 | 96 | Modify the [scripts/create-embeddings.js](./scripts/create-embeddings.js) script to include URLs of the documentation to create embeddings of: 97 | 98 | ```js 99 | // Add documentation URLs to be fetched here 100 | const urls = [ 101 | 'https://replicate.com/home', 102 | 'https://replicate.com/docs', 103 | 'https://replicate.com/docs/get-started/python' 104 | // ... 105 | ] 106 | ``` 107 | 108 | And run it: 109 | 110 | ```bash 111 | node scripts/create-embeddings.js 112 | ``` 113 | 114 | ## One-click deploy 115 | 116 | Deploy this project using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=replicate-support-gpt): 117 | 118 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Pwntus/replicate-support-gpt&env=NUXT_OPENAI_API_KEY&env=NUXT_SUPABASE_URL&env=NUXT_SUPABASE_KEY&project-name=replicate-support-gpt&repo-name=replicate-support-gpt) 119 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 38 | -------------------------------------------------------------------------------- /assets/style/vuetify.scss: -------------------------------------------------------------------------------- 1 | $font: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue', 2 | 'Helvetica', Arial, sans-serif; 3 | 4 | @use 'vuetify/settings' with ( 5 | $body-font-family: $font 6 | ); 7 | -------------------------------------------------------------------------------- /components/ui/Chat.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 83 | 84 | 110 | -------------------------------------------------------------------------------- /components/ui/Header.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | 48 | -------------------------------------------------------------------------------- /components/ui/Hero.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import vuetify from 'vite-plugin-vuetify' 2 | 3 | export default defineNuxtConfig({ 4 | runtimeConfig: { 5 | openaiApiKey: process.env.NUXT_OPENAI_API_KEY || '', 6 | supabaseUrl: process.env.NUXT_SUPABASE_URL || '', 7 | supabaseKey: process.env.NUXT_SUPABASE_KEY || '' 8 | }, 9 | // Vercel edge functions are incompatible with supabase 10 | // nitro: { preset: 'vercel-edge' }, 11 | build: { 12 | transpile: ['vuetify'] 13 | }, 14 | sourcemap: { 15 | server: false, 16 | client: false 17 | }, 18 | css: ['vuetify/styles', '@mdi/font/css/materialdesignicons.css'], 19 | hooks: { 20 | 'vite:extendConfig': (config) => { 21 | config.plugins?.push( 22 | vuetify({ 23 | autoImport: true, 24 | styles: { configFile: './assets/style/vuetify.scss' } 25 | }) 26 | ) 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "postinstall": "nuxt prepare" 9 | }, 10 | "dependencies": { 11 | "@supabase/supabase-js": "^2.11.0", 12 | "@vercel/analytics": "^0.1.11", 13 | "gpt3-tokenizer": "^1.1.5", 14 | "markdown-it": "^13.0.1", 15 | "openai": "^3.2.1", 16 | "openai-streams": "^4.1.0", 17 | "vuetify": "^3.1.9" 18 | }, 19 | "devDependencies": { 20 | "@mdi/font": "^7.1.96", 21 | "@types/lodash": "^4.14.191", 22 | "cheerio": "^1.0.0-rc.12", 23 | "dotenv": "^16.0.3", 24 | "nuxt": "^3.3.0", 25 | "pug": "^3.0.2", 26 | "pug-plain-loader": "^1.1.0", 27 | "sass": "^1.59.3", 28 | "stylus": "^0.59.0", 29 | "stylus-loader": "^7.1.0", 30 | "vite-plugin-vuetify": "^1.0.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugins/vercel.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@vercel/analytics' 2 | 3 | export default defineNuxtPlugin(() => { 4 | inject() 5 | }) 6 | -------------------------------------------------------------------------------- /plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import { createVuetify, ThemeDefinition } from 'vuetify' 2 | 3 | const lightTheme: ThemeDefinition = { 4 | dark: false, 5 | colors: { 6 | background: '#f6f6f6', 7 | surface: '#FFFFFF', 8 | primary: '#1d1d1f', 9 | secondary: '#e39200', 10 | error: '#B00020', 11 | info: '#2196F3', 12 | success: '#4CAF50', 13 | warning: '#FB8C00' 14 | } 15 | } 16 | 17 | export default defineNuxtPlugin((nuxtApp) => { 18 | const vuetify = createVuetify({ 19 | ssr: true, 20 | theme: { 21 | defaultTheme: 'lightTheme', 22 | themes: { 23 | lightTheme 24 | } 25 | } 26 | }) 27 | 28 | nuxtApp.vueApp.use(vuetify) 29 | }) 30 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'none', 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | bracketSpacing: true 7 | } 8 | -------------------------------------------------------------------------------- /public/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwntus/replicate-support-gpt/a98df87f02529977140b3e13e02f8ba03a322dd6/public/cover.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwntus/replicate-support-gpt/a98df87f02529977140b3e13e02f8ba03a322dd6/public/favicon.ico -------------------------------------------------------------------------------- /public/img/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pwntus/replicate-support-gpt/a98df87f02529977140b3e13e02f8ba03a322dd6/public/img/screenshot.jpg -------------------------------------------------------------------------------- /scripts/create-embeddings.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const cheerio = require('cheerio') 3 | const { Configuration, OpenAIApi } = require('openai') 4 | const { createClient } = require('@supabase/supabase-js') 5 | 6 | // Embeddings document maximum size 7 | const DOCUMENT_SIZE = 1000 8 | 9 | const openai = new OpenAIApi( 10 | new Configuration({ 11 | apiKey: process.env.NUXT_OPENAI_API_KEY 12 | }) 13 | ) 14 | 15 | const supabase = createClient( 16 | process.env.NUXT_SUPABASE_URL, 17 | process.env.NUXT_SUPABASE_KEY 18 | ) 19 | 20 | const getDocuments = async (urls) => { 21 | const documents = [] 22 | for (const url of urls) { 23 | const response = await fetch(url) 24 | const html = await response.text() 25 | const $ = cheerio.load(html) 26 | const articleText = $('body').text() 27 | 28 | let start = 0 29 | while (start < articleText.length) { 30 | const end = start + DOCUMENT_SIZE 31 | const chunk = articleText.slice(start, end) 32 | documents.push({ url, body: chunk }) 33 | start = end 34 | } 35 | } 36 | return documents 37 | } 38 | 39 | const main = async () => { 40 | try { 41 | // Add documentation URLs to be fetched here 42 | const urls = [ 43 | 'https://replicate.com/home', 44 | 'https://replicate.com/docs', 45 | 'https://replicate.com/docs/get-started/python', 46 | 'https://replicate.com/docs/get-started/nextjs', 47 | 'https://replicate.com/docs/get-started/discord-bot', 48 | 'https://replicate.com/docs/how-does-replicate-work', 49 | 'https://replicate.com/docs/guides/get-a-gpu-machine', 50 | 'https://replicate.com/docs/guides/push-a-model', 51 | 'https://replicate.com/docs/guides/push-stable-diffusion', 52 | 'https://replicate.com/docs/reference/examples', 53 | 'https://replicate.com/docs/reference/client-libraries', 54 | 'https://replicate.com/docs/reference/http', 55 | 'https://replicate.com/docs/troubleshooting', 56 | 'https://replicate.com/about', 57 | 'https://replicate.com/pricing', 58 | 'https://replicate.com/blog/hello-world', 59 | 'https://replicate.com/blog/constraining-clipdraw', 60 | 'https://replicate.com/blog/model-docs', 61 | 'https://replicate.com/blog/exploring-text-to-image-models', 62 | 'https://replicate.com/blog/daily-news', 63 | 'https://replicate.com/blog/grab-hundreds-of-images-with-clip-and-laion', 64 | 'https://replicate.com/blog/uncanny-spaces', 65 | 'https://replicate.com/blog/build-a-robot-artist-for-your-discord-server-with-stable-diffusion', 66 | 'https://replicate.com/blog/run-stable-diffusion-with-an-api', 67 | 'https://replicate.com/blog/run-stable-diffusion-on-m1-mac', 68 | 'https://replicate.com/blog/dreambooth-api', 69 | 'https://replicate.com/blog/lora-faster-fine-tuning-of-stable-diffusion', 70 | 'https://replicate.com/blog/machine-learning-needs-better-tools', 71 | 'https://replicate.com/blog/replicate-alpaca', 72 | 'https://replicate.com/changelog', 73 | 'https://replicate.com/explore' 74 | ] 75 | const documents = await getDocuments(urls) 76 | 77 | for (const { url, body } of documents) { 78 | // OpenAI recommends replacing newlines with spaces for best results 79 | const input = body.replace(/\n/g, ' ') 80 | 81 | console.log('\nDocument length: \n', body.length) 82 | console.log('\nURL: \n', url) 83 | 84 | const embeddingResponse = await openai.createEmbedding({ 85 | model: 'text-embedding-ada-002', 86 | input 87 | }) 88 | 89 | const [{ embedding }] = embeddingResponse.data.data 90 | 91 | await supabase.from('documents').insert({ 92 | content: input, 93 | embedding, 94 | url 95 | }) 96 | } 97 | } catch (e) { 98 | console.error(e.message) 99 | } 100 | } 101 | 102 | main() 103 | -------------------------------------------------------------------------------- /server/api/chat.post.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, OpenAIApi } from 'openai' 2 | import { createClient } from '@supabase/supabase-js' 3 | import GPT3Tokenizer from 'gpt3-tokenizer' 4 | import { OpenAI } from 'openai-streams/node' 5 | import { sendStream } from 'h3' 6 | 7 | const openai = new OpenAIApi( 8 | new Configuration({ 9 | apiKey: useRuntimeConfig().openaiApiKey 10 | }) 11 | ) 12 | 13 | const supabase = createClient( 14 | useRuntimeConfig().supabaseUrl, 15 | useRuntimeConfig().supabaseKey 16 | ) 17 | 18 | // @ts-ignore 19 | const tokenizer = new GPT3Tokenizer.default({ type: 'gpt3' }) 20 | 21 | export default defineEventHandler(async (event) => { 22 | try { 23 | const { query } = await readBody(event) 24 | 25 | // OpenAI recommends replacing newlines with spaces for best results 26 | const input = query.replace(/\n/g, ' ') 27 | 28 | // Generate a one-time embedding for the query itself 29 | const embeddingResponse = await openai.createEmbedding({ 30 | model: 'text-embedding-ada-002', 31 | input 32 | }) 33 | 34 | const [{ embedding }] = embeddingResponse.data.data 35 | 36 | const { error, data: documents } = await supabase.rpc('match_documents', { 37 | query_embedding: embedding, 38 | similarity_threshold: 0.1, 39 | match_count: 30 40 | }) 41 | 42 | if (error) return { error: error.message } 43 | 44 | // Create context 45 | let tokenCount = 0 46 | let contextText = '' 47 | 48 | // Concat matched documents 49 | for (let i = 0; i < documents.length; i++) { 50 | const document = documents[i] 51 | const content = document.content 52 | const encoded = tokenizer.encode(content) 53 | const prevTokenCount = tokenCount 54 | tokenCount += encoded.text.length 55 | 56 | // Limit context tokens 57 | if (tokenCount > 8192) { 58 | console.log('Previous token count', prevTokenCount) 59 | break 60 | } 61 | 62 | contextText += `${content.trim()}\n---\n` 63 | } 64 | 65 | console.log( 66 | 'Context documents', 67 | documents.length, 68 | 'Context documents scores', 69 | documents.map((i: any) => i.similarity), 70 | 'token count', 71 | tokenCount 72 | ) 73 | 74 | const systemContent = `You are a very enthusiastic Replicate representative who loves to help people! Given the following sections from the Replicate documentation, answer the question using only that information, outputted in markdown format. If you are unsure and the answer is not explicitly written in the documentation, say "Sorry, I don't know how to help with that.".` 75 | 76 | const userContent = `Context sections: 77 | You can use Replicate to run machine learning models in the cloud from your own code, without having to set up any servers. Our community has published hundreds of open-source models that you can run, or you can run your own models. 78 | 79 | Question: 80 | what is replicate? 81 | ` 82 | 83 | const assistantContent = `Replicate lets you run machine learning models with a cloud API, without having to understand the intricacies of machine learning or manage your own infrastructure. You can run open-source models that other people have published, or package and publish your own models. Those models can be public or private.` 84 | 85 | const userMessage = `Context sections: 86 | ${contextText} 87 | 88 | Question: 89 | ${query}` 90 | 91 | const messages: any[] = [ 92 | { 93 | role: 'system', 94 | content: systemContent 95 | }, 96 | { 97 | role: 'user', 98 | content: userContent 99 | }, 100 | { 101 | role: 'assistant', 102 | content: assistantContent 103 | }, 104 | { 105 | role: 'user', 106 | content: userMessage 107 | } 108 | ] 109 | 110 | const stream = await OpenAI( 111 | 'chat', 112 | { 113 | model: 'gpt-4', 114 | messages, 115 | stream: true, 116 | temperature: 0, 117 | top_p: 1, 118 | frequency_penalty: 0, 119 | presence_penalty: 0, 120 | n: 1 121 | }, 122 | { apiKey: useRuntimeConfig().openaiApiKey } 123 | ) 124 | 125 | return sendStream(event, stream) 126 | } catch (e: any) { 127 | console.error(e) 128 | return { error: e.message } 129 | } 130 | }) 131 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | --------------------------------------------------------------------------------