├── .gitignore ├── LICENSE ├── README.md ├── env.example ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── src └── app │ ├── LICENSE │ ├── api │ ├── chat │ │ └── route.ts │ ├── document │ │ └── route.ts │ ├── login │ │ └── route.ts │ └── signup │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── home │ └── page.tsx │ ├── layout.tsx │ ├── lib │ ├── ollama.ts │ ├── prompts.ts │ ├── runnables.ts │ └── supabase.ts │ ├── page.tsx │ ├── signup │ └── page.tsx │ └── utils │ ├── env.ts │ └── helpers.ts ├── supabaseScripts.txt ├── tailwind.config.ts └── tsconfig.json /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OpenAI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RAG-Ollama-JS 2 | 3 | https://github.com/user-attachments/assets/e75e3571-098d-4654-b000-5fd23142f64f 4 | 5 | ## Introduction 6 | RAG-Ollama-JS is a Next.js application that implements Retrieval-Augmented Generation (RAG) using LangChain.js, Ollama, and Supabase. This project provides a user-friendly interface for document-based question-answering with PDF support. It also supports secured RAG with embeddings used only for logged in users. 7 | 8 | ## Features 9 | - **PDF Document Support**: Upload and view PDF documents with built-in navigation 10 | - **Real-time Chat Interface**: Interactive chat interface with streaming responses 11 | - **RAG Implementation**: 12 | - Uses LangChain.js for structured question processing 13 | - Integrates with Ollama for language model capabilities 14 | - Leverages Supabase for vector storage and document management 15 | - **Responsive UI**: Split-screen layout with PDF viewer and chat interface 16 | - **Context-Aware Responses**: Generates answers based on document content 17 | 18 | ## Prerequisites 19 | - Node.js (Latest LTS version) 20 | - Ollama running locally or remotely 21 | - Supabase account and project 22 | 23 | ## Installation 24 | 25 | 1. Clone the repository: 26 | ```bash 27 | git clone https://github.com/AbhisekMishra/rag-ollama-js.git 28 | cd rag-ollama-js 29 | ``` 30 | 31 | 2. Install dependencies: 32 | ```bash 33 | npm install 34 | ``` 35 | 36 | 3. Configure environment variables: 37 | - Copy `env.example` to `.env` 38 | - Update the following variables: 39 | ```plaintext 40 | SUPABASE_API_KEY=your_supabase_api_key 41 | SUPABASE_URL=your_supabase_project_url 42 | OLLAMA_LLM_BASE_URL=http://localhost:11434 43 | OLLAMA_LLM_MODEL=your_preferred_model 44 | OLLAMA_EMBEDDINGS_BASE_URL=http://localhost:11434 45 | OLLAMA_EMBEDDINGS_MODEL=nomic-embed-text 46 | ``` 47 | 4. Run the supabase commands mentioned in **[`supabaseScripts.txt`](https://github.com/AbhisekMishra/rag-ollama-js/blob/main/supabaseScripts.txt)** 48 | 49 | 5. Start the development server: 50 | ```bash 51 | npm run dev 52 | ``` 53 | 54 | ## Usage 55 | 56 | 1. **Upload Document**: 57 | - Click the "Upload File" button in the right panel 58 | - Select a PDF document to upload 59 | - The document will be processed and stored in Supabase 60 | 61 | 2. **View Document**: 62 | - Navigate through pages using arrow buttons or scroll 63 | - Page number indicator shows current position 64 | 65 | 3. **Ask Questions**: 66 | - Type your question in the chat input 67 | - Receive context-aware responses based on the document content 68 | - View conversation history in the chat panel 69 | 70 | ## Technical Stack 71 | 72 | - **Frontend**: Next.js with TypeScript 73 | - **UI Framework**: Tailwind CSS 74 | - **PDF Handling**: react-pdf 75 | - **Language Model**: Ollama 76 | - **Vector Store**: Supabase 77 | - **RAG Implementation**: LangChain.js 78 | 79 | ## Project Structure 80 | 81 | ```plaintext 82 | src/ 83 | ├── app/ 84 | │ ├── api/ # API routes for chat and document handling 85 | │ ├── home/ # Main application page 86 | │ ├── lib/ # Core libraries (Ollama, Supabase, prompts) 87 | │ └── utils/ # Helper functions and environment config 88 | ``` 89 | 90 | ## Contributing 91 | 92 | 1. Fork the repository 93 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 94 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 95 | 4. Push to the branch (`git push origin feature/amazing-feature`) 96 | 5. Open a Pull Request 97 | 98 | ## License 99 | 100 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 101 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | SUPABASE_API_KEY=xxxxxxxxxxxxxxxxxxx 2 | SUPABASE_URL=https://xxxx.supabase.co 3 | OLLAMA_LLM_BASE_URL=http://localhost:11434 4 | OLLAMA_LLM_MODEL=llama3.2 5 | OLLAMA_EMBEDDINGS_BASE_URL=http://localhost:11434 6 | OLLAMA_EMBEDDINGS_MODEL=nomic-embed-text -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | experimental: { 5 | turbo: { 6 | resolveAlias: { 7 | canvas: './empty-module.ts', 8 | }, 9 | }, 10 | }, 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "langchain-js", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@langchain/community": "^0.3.18", 13 | "@langchain/ollama": "^0.1.3", 14 | "@supabase/supabase-js": "^2.47.6", 15 | "ai": "^4.0.18", 16 | "font-awesome": "^4.7.0", 17 | "jose": "^5.9.6", 18 | "marked": "^15.0.4", 19 | "next": "15.1.0", 20 | "pdf-parse": "^1.1.1", 21 | "react": "^19.0.0", 22 | "react-dom": "^19.0.0", 23 | "react-pdf": "^9.2.1" 24 | }, 25 | "devDependencies": { 26 | "@eslint/eslintrc": "^3", 27 | "@types/jsonwebtoken": "^9.0.7", 28 | "@types/node": "^20", 29 | "@types/react": "^19", 30 | "@types/react-dom": "^19", 31 | "eslint": "^9", 32 | "eslint-config-next": "15.1.0", 33 | "postcss": "^8", 34 | "tailwindcss": "^3.4.1", 35 | "typescript": "^5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OpenAI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { outputChain } from "@/app/lib/runnables" 2 | 3 | export async function POST(req: Request) { 4 | const userId = req.headers.get('User-Id'); 5 | const { question, history } = await req.json(); 6 | const stream = await outputChain({ userId }).stream({ question, history }) 7 | 8 | return new Response(stream) 9 | } -------------------------------------------------------------------------------- /src/app/api/document/route.ts: -------------------------------------------------------------------------------- 1 | import { supabaseClient } from '@/app/lib/supabase'; 2 | import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf"; 3 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; 4 | 5 | import { vectorStore } from '@/app/lib/supabase'; 6 | 7 | export async function GET(req: Request) { 8 | const userId = req.headers.get('User-Id'); 9 | const { data, error } = await supabaseClient 10 | .storage 11 | .from('document_store') 12 | .list('', { search: userId || '' }); 13 | 14 | if (error || !data.length || !data[0].name.includes(userId || '')) { 15 | return new Response(error?.message || 'Document search failed', { status: 400 }); 16 | } 17 | 18 | const { data: fileData, error: fileError } = await supabaseClient 19 | .storage 20 | .from('document_store') 21 | .download(data[0].name); 22 | 23 | if (fileError) { 24 | return new Response(fileError?.message || 'Document fetch failed', { status: 400 }); 25 | } 26 | 27 | return new Response(fileData); 28 | } 29 | 30 | export async function POST(req: Request) { 31 | const userId = req.headers.get('User-Id'); 32 | if (!userId) return new Response('User ID is required', { status: 400 }); 33 | 34 | const formData = await req.formData(); 35 | const file = formData.get('file') as File; 36 | if (!file) return new Response('File is required', { status: 400 }); 37 | 38 | const fileExtension = file?.name.split('.').pop(); 39 | if (!fileExtension) return new Response('File extension could not be determined', { status: 400 }); 40 | 41 | const { data, error } = await supabaseClient 42 | .storage 43 | .from('document_store') 44 | .upload(`${userId}.${fileExtension}`, file, { upsert: true }); 45 | 46 | if (error) return new Response(error.message || 'Upload failed', { status: 400 }); 47 | 48 | const pdfLoader = new PDFLoader(file, { splitPages: true, parsedItemSeparator: '' }); 49 | const pdfDoc = await pdfLoader.load(); 50 | 51 | const pageContent = pdfDoc.map(doc => doc.pageContent); 52 | const pageHeaders = pdfDoc.map(doc => ({ 53 | documentName: `${userId}.${fileExtension}`, 54 | pageNumber: doc?.metadata?.loc?.pageNumber, 55 | userId, 56 | })); 57 | 58 | const splitter = new RecursiveCharacterTextSplitter({ 59 | chunkSize: 1000, 60 | chunkOverlap: 100, 61 | separators: ['\n\n', '\n', ' ', ''], 62 | }); 63 | 64 | const docOutput = await splitter.createDocuments([...pageContent], pageHeaders); 65 | const { error: deleteError } = await supabaseClient.rpc('delete_documents_by_user', { userid: userId }); 66 | if (deleteError) throw new Response('Error in deleting embeddings!', { status: 400 }); 67 | 68 | await vectorStore().addDocuments(docOutput); 69 | return new Response('', { status: 201 }); 70 | } -------------------------------------------------------------------------------- /src/app/api/login/route.ts: -------------------------------------------------------------------------------- 1 | import { supabaseClient } from "@/app/lib/supabase" 2 | 3 | interface LoginRequest { 4 | username: string; 5 | password: string; 6 | } 7 | 8 | export async function POST(req: Request): Promise { 9 | const { username, password }: LoginRequest = await req.json(); 10 | const { error } = await supabaseClient.rpc('verify_password', { input_username: username, input_password: password }); 11 | if (error) { 12 | console.log(error) 13 | throw new Response('Error in login !', { status: 400 }); 14 | } 15 | 16 | return new Response(JSON.stringify({ username }), { status: 200 }); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/api/signup/route.ts: -------------------------------------------------------------------------------- 1 | import { supabaseClient } from "@/app/lib/supabase" 2 | 3 | interface SignupRequest { 4 | firstname: string; 5 | lastname: string; 6 | username: string; 7 | password: string; 8 | } 9 | 10 | export async function POST(req: Request): Promise { 11 | const { firstname, lastname, username, password }: SignupRequest = await req.json(); 12 | const { error } = await supabaseClient.from('users').insert({ firstname, lastname, username, password }); 13 | if (error) { 14 | throw new Error('Error in insert !', error); 15 | } 16 | return new Response(); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhisekMishra/rag-ollama-js/30830979921f91785d216e2175d87f2573a7392c/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | .parsed-text a { 24 | text-decoration: underline; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/home/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useRef } from "react"; 4 | import { parse } from 'marked'; 5 | import { pdfjs, Document, Page } from 'react-pdf'; 6 | import 'react-pdf/dist/Page/TextLayer.css'; 7 | import 'react-pdf/dist/Page/AnnotationLayer.css'; 8 | 9 | pdfjs.GlobalWorkerOptions.workerSrc = new URL( 10 | 'pdfjs-dist/build/pdf.worker.min.mjs', 11 | import.meta.url, 12 | ).toString(); 13 | 14 | export default function Home() { 15 | const [messages, setMessages] = useState<{ text: string; sender: string }[]>([{ text: "Hi. How many I help you today ?", sender: "System" }]); 16 | const [input, setInput] = useState(""); 17 | const [loading, setLoading] = useState(false); 18 | const [pageNumber, setPageNumber] = useState(1); 19 | const [numPages, setNumPages] = useState(); 20 | const [user, setUser] = useState(''); 21 | const [file, setFile] = useState(); 22 | 23 | const pdfRef = useRef(null); 24 | const lastScrollTop = useRef(0); 25 | 26 | useEffect(() => { 27 | const handleScroll = () => { 28 | if (pdfRef.current) { 29 | const { scrollTop, clientHeight, scrollHeight } = pdfRef.current; 30 | const isAtBottom = scrollTop + clientHeight >= scrollHeight; 31 | const isAtTop = scrollTop === 0; 32 | 33 | if (isAtBottom && pageNumber < (numPages || 0) && lastScrollTop.current !== scrollTop) { 34 | lastScrollTop.current = scrollTop; 35 | handleNextPage(); 36 | } else if (isAtTop && pageNumber > 1 && lastScrollTop.current !== scrollTop) { 37 | lastScrollTop.current = scrollTop; 38 | handlePrevPage(); 39 | } 40 | } 41 | }; 42 | 43 | const currentPdfRef = pdfRef.current; 44 | currentPdfRef?.addEventListener("scroll", handleScroll); 45 | 46 | return () => { 47 | currentPdfRef?.removeEventListener("scroll", handleScroll); 48 | }; 49 | }, [pdfRef, pageNumber, numPages]); 50 | 51 | useEffect(() => { 52 | const userId = sessionStorage.getItem('userId') || ''; 53 | setUser(userId); 54 | getFile() 55 | }, []) 56 | 57 | useEffect(() => { 58 | getFile() 59 | }, [user]) 60 | 61 | const handleClick = (event: React.MouseEvent) => { 62 | const target = event.target as HTMLAnchorElement; // Type assertion 63 | const hrefAttribute = target?.attributes?.getNamedItem('href'); // Get the href attribute 64 | if (hrefAttribute?.value) { 65 | event.preventDefault(); 66 | const pageNum = hrefAttribute?.value.split(".")[0]?.replace('#', ''); 67 | setPageNumber(Number(pageNum)) 68 | } 69 | } 70 | 71 | const handleSendMessage = async (event: React.FormEvent) => { 72 | event.preventDefault(); // Prevents page refresh 73 | if (input.trim()) { 74 | setMessages([...messages, { text: input, sender: "User" }]); 75 | setInput(""); // Clear the input box 76 | setLoading(true); // Set loading to true 77 | 78 | try { 79 | const response = await fetch('/api/chat', { 80 | method: "POST", 81 | body: JSON.stringify({ question: input, history: messages }), 82 | headers: { 83 | 'User-Id': user 84 | } 85 | }); 86 | if (response.body) { 87 | const reader = response.body.getReader(); 88 | const decoder = new TextDecoder("utf-8"); 89 | let systemMessage = ""; // Initialize a variable to accumulate the message 90 | 91 | // Add an initial system message entry 92 | setMessages((prevMessages) => [...prevMessages, { text: "", sender: "System" }]); 93 | 94 | while (true) { 95 | const { done, value } = await reader.read(); 96 | if (done) break; 97 | const chunk = decoder.decode(value); 98 | systemMessage += chunk; // Accumulate the message 99 | // Update the last message in the messages array 100 | setMessages((prevMessages) => { 101 | const updatedMessages = [...prevMessages]; 102 | updatedMessages[updatedMessages.length - 1] = { text: systemMessage, sender: "System" }; 103 | return updatedMessages; 104 | }); 105 | } 106 | } else { 107 | console.error("Response body is null"); 108 | } 109 | } catch (error) { 110 | console.error("Error fetching response:", error); 111 | } finally { 112 | setLoading(false); // Set loading to false 113 | } 114 | } 115 | }; 116 | 117 | function onDocumentLoadSuccess({ numPages }: { numPages: number }): void { 118 | setNumPages(numPages); 119 | } 120 | 121 | const handleNextPage = () => { 122 | if (pageNumber < (numPages || 0)) { 123 | setPageNumber(pageNumber + 1); 124 | } 125 | }; 126 | 127 | const handlePrevPage = () => { 128 | if (pageNumber > 1) { 129 | setPageNumber(pageNumber - 1); 130 | } 131 | }; 132 | 133 | const getFile = async () => { 134 | const fileRes = await fetch('/api/document', { 135 | headers: { 136 | 'User-Id': user 137 | } 138 | }); 139 | if (!fileRes.ok) return; 140 | console.log(fileRes) 141 | const blob = await fileRes.blob(); // Convert Response to Blob 142 | const file = new File([blob], 'document.pdf'); // Create a File object 143 | setFile(file); // Set the File object 144 | } 145 | 146 | const handleUpload = async (file: File | null) => { 147 | if (file) { 148 | const formData = new FormData(); 149 | formData.append("file", file); 150 | 151 | await fetch("/api/document", { 152 | method: "POST", 153 | body: formData, 154 | headers: { 155 | 'User-Id': user 156 | } 157 | }); 158 | getFile() 159 | } 160 | } 161 | 162 | return ( 163 |
164 | {/* Chat Section */} 165 |
166 |
167 | {messages.map((message, index) => ( 168 |
176 | 181 |
182 | ))} 183 | {loading && ( 184 |
185 |
Loading...
186 |
187 | )} 188 |
189 |
190 |
191 | setInput(e.target.value)} 196 | placeholder="Type your message..." 197 | disabled={loading} 198 | /> 199 | 206 |
207 |
208 |
209 | 210 | {/* PDF Section */} 211 |
212 | {file ? ( 213 | <> 214 |
215 |
216 | 217 | 218 | 219 |

220 | Page {pageNumber} of {numPages} 221 |

222 |
223 |
224 |
225 | 236 | 247 |
248 | 249 | ) : ( 250 |
251 |
252 | handleUpload(e.target.files ? e.target.files[0] : null)} 257 | accept="application/pdf" 258 | /> 259 | 268 |
269 |
270 | )} 271 |
272 |
273 | ); 274 | } 275 | 276 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 |
31 |
32 |

Secure RAG with Ollama

33 |
34 | 35 | {children} 36 |
37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/lib/ollama.ts: -------------------------------------------------------------------------------- 1 | import { ChatOllama, OllamaEmbeddings } from "@langchain/ollama"; 2 | import { env } from "../utils/env"; 3 | 4 | const { ollama } = env; 5 | 6 | export const llm = new ChatOllama({ 7 | model: ollama.llm.model, 8 | temperature: 0, 9 | maxRetries: 2, 10 | baseUrl: ollama.llm.baseUrl, 11 | }); 12 | 13 | export const embeddings = new OllamaEmbeddings({ 14 | model: ollama.embeddings.model, 15 | baseUrl: ollama.embeddings.baseUrl, 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/lib/prompts.ts: -------------------------------------------------------------------------------- 1 | import { PromptTemplate } from "@langchain/core/prompts"; 2 | 3 | export const standaloneTemplate = PromptTemplate.fromTemplate( 4 | `Given some conversation history and a question, convert the question to a standalone question. 5 | conversation history: {history} 6 | question: {question} 7 | standalone question:` 8 | ); 9 | 10 | export const answerTemplate = PromptTemplate.fromTemplate(`You are a helpful and enthusiastic support bot who answers questions based on the provided context. 11 | The context is an array of chunks, each containing line numbers and a page number in the metadata, and page content. 12 | Your goal is to find the most relevant information from the context to answer the question. 13 | 14 | - If you don't know the answer or cannot find it in the context, say, "I don't know," and do not fabricate an answer. 15 | - Never mention the chunk number. 16 | - Always respond in a friendly and conversational tone. 17 | 18 | Context: 19 | {context} 20 | 21 | Question: 22 | {question} 23 | 24 | Answer:`); -------------------------------------------------------------------------------- /src/app/lib/runnables.ts: -------------------------------------------------------------------------------- 1 | import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables"; 2 | import { StringOutputParser } from "@langchain/core/output_parsers"; 3 | 4 | import { answerTemplate, standaloneTemplate } from "./prompts"; 5 | import { llm } from "./ollama"; 6 | import { retriever } from "./supabase"; 7 | import { combineDocuments } from "../utils/helpers"; 8 | 9 | const promptChain = RunnableSequence.from([ 10 | standaloneTemplate, 11 | llm, 12 | new StringOutputParser() 13 | ]) 14 | 15 | const retrieverChain = (filter: Record) => RunnableSequence.from([ 16 | result => result.standaloneQuestion, 17 | retriever(filter), 18 | combineDocuments 19 | ]) 20 | 21 | const answerChain = RunnableSequence.from([ 22 | answerTemplate, 23 | llm, 24 | new StringOutputParser() 25 | ]) 26 | 27 | export const outputChain = (filter: Record) => RunnableSequence.from([ 28 | { 29 | standaloneQuestion: promptChain, 30 | originalQuestion: new RunnablePassthrough() 31 | }, 32 | { 33 | context: retrieverChain(filter), 34 | question: ({ originalQuestion }) => originalQuestion.question, 35 | history: ({ originalQuestion }) => originalQuestion.history, 36 | }, 37 | answerChain 38 | ]) 39 | -------------------------------------------------------------------------------- /src/app/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { SupabaseVectorStore } from "@langchain/community/vectorstores/supabase"; 2 | import { createClient, } from "@supabase/supabase-js"; 3 | 4 | import { embeddings } from "./ollama"; 5 | import { env } from "../utils/env"; 6 | 7 | const { supabase: { url, apiKey } } = env; 8 | 9 | export const supabaseClient = createClient( 10 | url, 11 | apiKey, 12 | ); 13 | 14 | export const vectorStore = (filter?: any) => new SupabaseVectorStore(embeddings, { 15 | client: supabaseClient, 16 | tableName: "documents", 17 | queryName: "match_documents", 18 | filter: filter || {} 19 | }); 20 | 21 | supabaseClient.auth.signUp 22 | 23 | export const retriever = (filter: any) => vectorStore(filter).asRetriever(); -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import Link from 'next/link'; 5 | import { useRouter } from 'next/navigation' 6 | 7 | export default function Home() { 8 | const [username, setUsername] = useState(""); 9 | const [password, setPassword] = useState(""); 10 | const [loading, setLoading] = useState(false); 11 | const router = useRouter() 12 | 13 | const handleSubmit = async (event: React.FormEvent) => { 14 | event.preventDefault(); 15 | setLoading(true); 16 | const response = await fetch('/api/login', { 17 | method: "POST", 18 | body: JSON.stringify({ username, password }), 19 | headers: { 20 | "Content-Type": "application/json" 21 | } 22 | }); 23 | await response.json(); 24 | setLoading(false); 25 | if (response.ok) { 26 | sessionStorage.setItem('userId', username); 27 | router.push('/home') 28 | } 29 | }; 30 | 31 | return ( 32 |
33 | {loading &&
} 34 |
35 |

Login

36 |
37 | 38 | setUsername(e.target.value)} 43 | required 44 | /> 45 |
46 |
47 | 48 | setPassword(e.target.value)} 53 | required 54 | /> 55 |
56 | 59 |

60 | Don't have an account? 61 | 66 | Sign Up 67 | 68 |

69 |
70 |
71 | ); 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import Link from 'next/link'; 5 | import { useRouter } from 'next/navigation' 6 | 7 | export default function Home() { 8 | const [firstname, setFirstname] = useState(""); 9 | const [lastname, setLastname] = useState(""); 10 | const [username, setUsername] = useState(""); 11 | const [password, setPassword] = useState(""); 12 | const [confirmPassword, setConfirmPassword] = useState(""); 13 | const [loading, setLoading] = useState(false); 14 | const router = useRouter() 15 | 16 | const handleSubmit = async (event: React.FormEvent) => { 17 | event.preventDefault(); 18 | if (password !== confirmPassword) { 19 | alert("Passwords do not match!"); 20 | return; 21 | } 22 | setLoading(true); 23 | const response = await fetch('/api/signup', { 24 | method: "POST", 25 | body: JSON.stringify({ firstname, lastname, username, password }), 26 | headers: { 27 | "Content-Type": "application/json" 28 | } 29 | }); 30 | setLoading(false); 31 | if (response.ok) { 32 | router.push('/') 33 | } 34 | 35 | }; 36 | 37 | return ( 38 |
39 | {loading &&
Loading...
} 40 |
41 |

Signup

42 |
43 | 44 | setFirstname(e.target.value)} 49 | required 50 | /> 51 |
52 |
53 | 54 | setLastname(e.target.value)} 59 | required 60 | /> 61 |
62 |
63 | 64 | setUsername(e.target.value)} 69 | required 70 | /> 71 |
72 |
73 | 74 | setPassword(e.target.value)} 79 | required 80 | /> 81 |
82 |
83 | 84 | setConfirmPassword(e.target.value)} 89 | required 90 | /> 91 |
92 | 95 |

96 | Have an account? 97 | 102 | Login 103 | 104 |

105 |
106 |
107 | ); 108 | } 109 | 110 | -------------------------------------------------------------------------------- /src/app/utils/env.ts: -------------------------------------------------------------------------------- 1 | export const env = { 2 | supabase: { 3 | apiKey: process.env.SUPABASE_API_KEY || '', 4 | url: process.env.SUPABASE_URL || '', 5 | }, 6 | ollama: { 7 | llm: { 8 | baseUrl: process.env.OLLAMA_LLM_BASE_URL || '', 9 | model: process.env.OLLAMA_LLM_MODEL || '', 10 | }, 11 | embeddings: { 12 | baseUrl: process.env.OLLAMA_EMBEDDINGS_BASE_URL || '', 13 | model: process.env.OLLAMA_EMBEDDINGS_MODEL || '', 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | export function combineDocuments(retrievedDocs: { pageContent: string }[]) { 2 | return retrievedDocs.map((doc: { pageContent: string }) => doc.pageContent).join("\n"); 3 | } -------------------------------------------------------------------------------- /supabaseScripts.txt: -------------------------------------------------------------------------------- 1 | -- Enable the pgvector extension to work with embedding vectors 2 | -- create extension vector; 3 | 4 | -- Create a table to store your documents 5 | create table documents ( 6 | id serial primary key, --bigserial 7 | content text, -- corresponds to Document.pageContent 8 | metadata jsonb, -- corresponds to Document.metadata 9 | embedding vector(768) -- 768 works for ollama embeddings, change if needed 10 | ); 11 | 12 | -- Create a function to search for documents 13 | create function match_documents ( 14 | query_embedding vector(768), 15 | match_count int DEFAULT null, 16 | filter jsonb DEFAULT '{}' 17 | ) returns table ( 18 | id bigint, 19 | content text, 20 | metadata jsonb, 21 | embedding jsonb, 22 | similarity float 23 | ) 24 | language plpgsql 25 | as $$ 26 | #variable_conflict use_column 27 | begin 28 | return query 29 | select 30 | id, 31 | content, 32 | metadata, 33 | (embedding::text)::jsonb as embedding, 34 | 1 - (documents.embedding <=> query_embedding) as similarity 35 | from documents 36 | where metadata @> filter 37 | order by documents.embedding <=> query_embedding 38 | limit match_count; 39 | end; 40 | $$; 41 | 42 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 43 | 44 | -- Create a table to add users 45 | create table users ( 46 | id uuid DEFAULT gen_random_uuid() PRIMARY KEY, 47 | firstName text, -- corresponds to User.firstName 48 | lastName text, -- corresponds to User.lastName 49 | username text unique, -- corresponds to User.username 50 | password varchar 51 | ); 52 | 53 | -- Create function hash_password_trigger to hash the password before insert 54 | CREATE OR REPLACE FUNCTION hash_password_trigger() 55 | RETURNS TRIGGER AS $$ 56 | BEGIN 57 | NEW.password = crypt(NEW.password, gen_salt('bf')); 58 | RETURN NEW; 59 | END; 60 | $$ LANGUAGE plpgsql; 61 | CREATE TRIGGER before_insert_or_update 62 | BEFORE INSERT OR UPDATE ON users 63 | FOR EACH ROW 64 | EXECUTE FUNCTION hash_password_trigger(); 65 | 66 | -- Create function for authentication 67 | CREATE OR REPLACE FUNCTION verify_password (input_username TEXT, input_password TEXT) RETURNS RECORD AS $$ 68 | DECLARE 69 | user_record RECORD; 70 | BEGIN 71 | SELECT u.firstname, u.lastname, u.username 72 | INTO user_record 73 | FROM users u 74 | WHERE u.username = input_username 75 | AND u.password = crypt(input_password, u.password); 76 | 77 | IF user_record IS NULL THEN 78 | RAISE EXCEPTION 'Invalid username or password'; 79 | END IF; 80 | 81 | RETURN user_record; 82 | END; 83 | $$ LANGUAGE plpgsql; 84 | 85 | -- Function to delete documents by userid 86 | CREATE OR REPLACE FUNCTION delete_documents_by_user(userid TEXT) 87 | RETURNS VOID AS $$ 88 | BEGIN 89 | DELETE FROM documents 90 | WHERE metadata @> FORMAT('{"userId": "%s"}', userId)::JSONB; 91 | END; 92 | $$ LANGUAGE plpgsql; -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } satisfies Config; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------