├── .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 |
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 | 
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------