├── .env.sample ├── .eslintrc.json ├── data ├── northwind.db └── seed.ts ├── public ├── favicon.ico ├── vercel.svg ├── thirteen.svg └── next.svg ├── postcss.config.js ├── next.config.js ├── src ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── chat.ts │ └── index.tsx ├── styles │ └── globals.css ├── components │ ├── SqlViewer.tsx │ ├── icons │ │ └── GithubIcon.tsx │ ├── Preloader.tsx │ ├── DataTable.tsx │ └── ChatForm.tsx └── lib │ └── prompt.ts ├── tailwind.config.js ├── .gitignore ├── README.md ├── tsconfig.json └── package.json /.env.sample: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=sk- -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /data/northwind.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayeru/chat-to-your-database/HEAD/data/northwind.db -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayeru/chat-to-your-database/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | @layer components { 7 | #__next { 8 | @apply h-full; 9 | } 10 | } -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | fontFamily: { 9 | sans: ["Inter"], 10 | }, 11 | }, 12 | plugins: [ 13 | require('@tailwindcss/forms'), 14 | require('tailwind-scrollbar')({ nocompatible: true }), 15 | ], 16 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .vscode -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat to your database 2 | 3 | This is an experimental app to test the abilities of LLMs to query SQL databases using natural language. 4 | To use it, you should add your OPENAI_API_KEY to the .env.local file. 5 | 6 | ## Installing the app 7 | 8 | ```bash! 9 | npm install 10 | ``` 11 | 12 | ## Running the app 13 | 14 | ```bash 15 | npm run dev 16 | ``` 17 | 18 | ## Sample database 19 | ![Sample northwind database](https://user-images.githubusercontent.com/1945179/233065892-25edda54-01a2-467d-8a72-b96a30c71a5a.png) 20 | 21 | ## See it in action 22 | https://user-images.githubusercontent.com/1945179/233063701-9c025c4a-5016-4d79-b842-62540359e033.mp4 23 | 24 | https://user-images.githubusercontent.com/1945179/233066403-a83af003-d02a-4ce1-9192-8737e4524d44.mp4 25 | 26 | -------------------------------------------------------------------------------- /src/components/SqlViewer.tsx: -------------------------------------------------------------------------------- 1 | import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; 2 | import { atomOneDark as theme } from "react-syntax-highlighter/dist/cjs/styles/hljs"; 3 | import sql from "react-syntax-highlighter/dist/cjs/languages/hljs/sql"; 4 | 5 | interface SqlViewerProps { 6 | content: string; 7 | } 8 | 9 | const SqlViewer = (props: SqlViewerProps) => { 10 | 11 | SyntaxHighlighter.registerLanguage("javascript", sql); 12 | 13 | return ( 14 | 22 | {props.content} 23 | 24 | ); 25 | }; 26 | 27 | export default SqlViewer; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "checkJs": true, 5 | "target": "es5", 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "paths": { 20 | "@/*": ["./src/*"], 21 | } 22 | }, 23 | "ts-node": { 24 | "compilerOptions": { 25 | "module": "commonjs" 26 | } 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /data/seed.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "sqlite3"; 2 | import fs from "fs"; 3 | 4 | const sqlStr = fs.readFileSync("data/your_db.sql").toString().split(";"); 5 | 6 | const db = new Database("database", (err) => { 7 | if (err) { 8 | return console.error(err.message); 9 | } 10 | 11 | console.log("Connected to the in-memory SQlite database."); 12 | }); 13 | 14 | db.serialize(() => { 15 | 16 | db.run("PRAGMA foreign_keys = OFF"); 17 | db.run("BEGIN TRANSACTION"); 18 | 19 | sqlStr.forEach((query) => { 20 | 21 | if (query) { 22 | db.run(query, (err) => { 23 | if (err) { 24 | return console.error(err.message); 25 | } 26 | }); 27 | } 28 | 29 | }); 30 | 31 | db.run("COMMIT"); 32 | 33 | }); 34 | 35 | db.close((err) => { 36 | if (err) { 37 | return console.error(err.message); 38 | } 39 | 40 | console.log("Closed the database connection."); 41 | }); -------------------------------------------------------------------------------- /src/components/icons/GithubIcon.tsx: -------------------------------------------------------------------------------- 1 | const GithubIcon = () => { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | }; 8 | 9 | export default GithubIcon; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-to-your-database", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "cross-env NODE_OPTIONS='--inspect' next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "seed": "npx ts-node ./data/seed.ts" 11 | }, 12 | "dependencies": { 13 | "@types/node": "18.15.11", 14 | "@types/react": "18.0.32", 15 | "@types/react-dom": "18.0.11", 16 | "eslint": "8.37.0", 17 | "eslint-config-next": "13.2.4", 18 | "langchain": "^0.0.45", 19 | "next": "13.2.4", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "react-syntax-highlighter": "^15.5.0", 23 | "sqlite3": "^5.1.6", 24 | "tailwindcss": "^3.3.1", 25 | "typeorm": "^0.3.12", 26 | "typescript": "5.0.3" 27 | }, 28 | "devDependencies": { 29 | "@tailwindcss/forms": "^0.5.3", 30 | "@types/react-syntax-highlighter": "^15.5.6", 31 | "autoprefixer": "^10.4.14", 32 | "cross-env": "^7.0.3", 33 | "postcss": "^8.4.21", 34 | "tailwind-scrollbar": "^3.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Preloader.tsx: -------------------------------------------------------------------------------- 1 | 2 | interface PreloaderProps { 3 | className?: string; 4 | "backgroundColor"?: string; 5 | "fillColor"?: string; 6 | } 7 | 8 | const Preloader = (props:PreloaderProps) => { 9 | return ( 10 |
11 | 12 | 16 | 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default Preloader; -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/prompt.ts: -------------------------------------------------------------------------------- 1 | export const SQL_PREFIX = `You are an agent designed to interact with a SQL database. 2 | Given an input question, create a syntactically correct {dialect} query to run, then look at the results of the query and return the answer. 3 | Always limit your query to at most {top_k} results using the LIMIT clause. 4 | You can order the results by a relevant column to return the most interesting examples in the database. 5 | Never query for all the columns from a specific table, only ask for a the few relevant columns given the question. 6 | If you get a "no such table" error, rewrite your query by using the table in quotes. 7 | DO NOT use a column name that does not exist in the table. 8 | You have access to tools for interacting with the database. 9 | Only use the below tools. Only use the information returned by the below tools to construct your final answer. 10 | You MUST double check your query before executing it. If you get an error while executing a query, rewrite a different query and try again. 11 | DO NOT try to execute the query more than three times. 12 | DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database. 13 | If the question does not seem related to the database, just return "I don't know" as the answer. 14 | If you cannot find a way to answer the question, just return the best answer you can find after trying at least three times.`; 15 | 16 | export const SQL_SUFFIX = `Begin! 17 | Question: {input} 18 | Thought: I should look at the tables in the database to see what I can query. 19 | {agent_scratchpad}`; -------------------------------------------------------------------------------- /src/components/DataTable.tsx: -------------------------------------------------------------------------------- 1 | interface DataTableProps { 2 | data?: Record[]; 3 | } 4 | 5 | const DataTable = ({data}: DataTableProps) => { 6 | 7 | if (!data || data.length === 0) return ( 8 |
9 |
10 | ); 11 | 12 | // From camelCase to Title Case 13 | const titles = Object.keys(data[0]).map((item: any) => { 14 | return item.replace(/([a-z])([A-Z])/g, "$1 $2"); 15 | }); 16 | 17 | const header = titles.map((item: any, index: number) => { 18 | return ( 19 | 20 | {item} 21 | 22 | ); 23 | }); 24 | 25 | // Fill cells with data 26 | const fillCells = (item: any) => { 27 | const result = []; 28 | const keys = Object.keys(item); 29 | 30 | for (let i = 0; i < keys.length; i++) { 31 | result.push( 32 | 36 | {item[keys[i]]} 37 | 38 | ); 39 | } 40 | 41 | return result; 42 | }; 43 | 44 | const rows = data.map((item: any, index: number) => { 45 | return ( 46 | 47 | {fillCells(item)} 48 | 49 | ); 50 | }); 51 | 52 | return ( 53 | 54 | 55 | {header} 56 | 57 | {rows} 58 |
59 | ); 60 | }; 61 | 62 | export default DataTable; 63 | -------------------------------------------------------------------------------- /src/components/ChatForm.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | interface ChatFormProps { 4 | 5 | onPrompt: (prompt: string) => void; 6 | 7 | } 8 | 9 | const ChatForm = (props: ChatFormProps) => { 10 | 11 | const inputRef = useRef(null); 12 | 13 | useEffect(() => { 14 | if (inputRef.current) { 15 | inputRef.current.focus(); 16 | } 17 | }, [inputRef]); 18 | 19 | const handleFormSubmit = (e: React.FormEvent) => { 20 | if (inputRef.current) { 21 | e.preventDefault(); 22 | const value = inputRef.current.value; 23 | inputRef.current.value = ""; 24 | props.onPrompt(value); 25 | } 26 | }; 27 | 28 | return ( 29 |
30 | 40 | 56 |
57 | ); 58 | }; 59 | 60 | export default ChatForm; 61 | -------------------------------------------------------------------------------- /src/pages/api/chat.ts: -------------------------------------------------------------------------------- 1 | import { SQL_PREFIX, SQL_SUFFIX } from "@/lib/prompt"; 2 | import { OpenAI } from "langchain"; 3 | import { SqlToolkit, createSqlAgent } from "langchain/agents"; 4 | import { SqlDatabase } from "langchain/sql_db"; 5 | import type { NextApiRequest, NextApiResponse } from "next"; 6 | import { DataSource } from "typeorm"; 7 | 8 | export const handler = async (req: NextApiRequest, res: NextApiResponse) => { 9 | const datasource = new DataSource({ 10 | type: "sqlite", 11 | database: "./data/northwind.db", 12 | }); 13 | 14 | const db = await SqlDatabase.fromDataSourceParams({ 15 | appDataSource: datasource, 16 | }); 17 | 18 | const toolkit = new SqlToolkit(db); 19 | const model = new OpenAI({ openAIApiKey: process.env.OPENAI_API_KEY, temperature: 0 }); 20 | const executor = createSqlAgent(model, toolkit, { topK: 10, prefix:SQL_PREFIX, suffix: SQL_SUFFIX }); 21 | const {query: prompt} = req.body; 22 | 23 | console.log("Prompt : " + prompt); 24 | 25 | let response = { 26 | prompt: prompt, 27 | sqlQuery: "", 28 | result: [], 29 | error: "" 30 | }; 31 | 32 | try { 33 | const result = await executor.call({ input: prompt }); 34 | 35 | result.intermediateSteps.forEach((step:any) => { 36 | 37 | if (step.action.tool === "query-sql") { 38 | response.prompt = prompt; 39 | response.sqlQuery = step.action.toolInput; 40 | response.result = JSON.parse(step.observation); 41 | } 42 | 43 | }); 44 | 45 | console.log(`Intermediate steps ${JSON.stringify(result.intermediateSteps, null, 2)}`); 46 | } catch (e:any) { 47 | console.log(e); 48 | response.error = "Server error. Try again with a different prompt."; 49 | res.status(200).json(response); 50 | } 51 | 52 | await datasource.destroy(); 53 | res.status(200).json(response); 54 | 55 | }; 56 | 57 | export default handler; 58 | 59 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import ChatForm from "@/components/ChatForm"; 2 | import DataTable from "@/components/DataTable"; 3 | import Preloader from "@/components/Preloader"; 4 | import SqlViewer from "@/components/SqlViewer"; 5 | import GithubIcon from "@/components/icons/GithubIcon"; 6 | import Head from "next/head"; 7 | import { useState } from "react"; 8 | 9 | interface ChatResponse { 10 | prompt: string; 11 | sqlQuery: string; 12 | result: Record[]; 13 | error: string; 14 | } 15 | 16 | export default function Home() { 17 | const [response, setResponse] = useState(null); 18 | const [waitingResponse, setWaitingResponse] = useState(false); 19 | const [firstRun, setFirstRun] = useState(true); 20 | const [prompt, setPrompt] = useState(""); 21 | 22 | const onPrompt = async (prompt: string) => { 23 | setFirstRun(false); 24 | setWaitingResponse(true); 25 | setPrompt(prompt); 26 | 27 | // Post value to API 28 | const res = await fetch("http://localhost:3000/api/chat", { 29 | method: "POST", 30 | headers: { 31 | "Content-Type": "application/json", 32 | }, 33 | 34 | body: JSON.stringify({ 35 | query: prompt, 36 | }), 37 | }); 38 | 39 | const data = await res.json(); 40 | setResponse(data); 41 | setWaitingResponse(false); 42 | }; 43 | 44 | const getSqlViewerContent = () => { 45 | if (response?.sqlQuery) { 46 | if (waitingResponse) { 47 | return `-- ${prompt}`; 48 | } else { 49 | return `-- ${prompt} \n${response.sqlQuery}`; 50 | } 51 | } 52 | 53 | if (waitingResponse) { 54 | return `-- ${prompt}`; 55 | } 56 | 57 | return "-- No prompt yet"; 58 | }; 59 | 60 | return ( 61 | <> 62 | 63 | Chat to your database 64 | 65 | 66 | 67 | 68 |
69 |
70 |
71 |
72 |

Chat to your database

73 | 74 | 75 |

v0.1.0

76 |
77 |
78 | {response?.error && response?.error !== "" && ( 79 |

{response.error}

80 | )} 81 |
82 | {waitingResponse || firstRun ? ( 83 |
84 | {waitingResponse && ( 85 | 86 | )} 87 | {firstRun &&

Type a prompt to get started

} 88 |
89 | ) : ( 90 | 91 | )} 92 |
93 |
94 |
95 |
96 | 97 |
98 |
99 | 100 |
101 |
102 |
103 |
104 | 105 | ); 106 | } 107 | --------------------------------------------------------------------------------