= {};
31 |
32 | documents.documents.forEach((document, i) => {
33 | const metadata = documents.metadatas[i] || undefined;
34 | const embedding = documents.embeddings ? documents.embeddings[i] : {};
35 | const id = documents.ids[i];
36 | const filename = metadata?.filename as string;
37 | const documentString = document as string;
38 | if (!objects[filename]) {
39 | objects[filename] = [];
40 | }
41 |
42 | objects[filename].push({
43 | metadata,
44 | embedding,
45 | document: documentString,
46 | id
47 | });
48 | });
49 |
50 | return objects;
51 | };
52 |
53 | export default processDocumentsIntoObjects;
54 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "savant",
3 | "version": "1.0.0",
4 | "description": "Ingest documents and query",
5 | "main": "server.ts",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "tsc",
9 | "start": "ts-node -r tsconfig-paths/register src/server.ts",
10 | "start:chroma": "cd ../ && cd chroma && docker-compose up -d --build",
11 | "lint": "eslint src --fix"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/foolishsailor/savant.git"
16 | },
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/foolishsailor/savant/issues"
21 | },
22 | "homepage": "https://github.com/foolishsailor/savant#readme",
23 | "devDependencies": {
24 | "@types/cors": "^2.8.13",
25 | "@types/express": "^4.17.17",
26 | "@types/multer": "^1.4.7",
27 | "@types/node": "^18.16.3",
28 | "@typescript-eslint/eslint-plugin": "^5.59.1",
29 | "@typescript-eslint/parser": "^5.59.1",
30 | "eslint": "^8.39.0",
31 | "ts-node": "^10.9.1",
32 | "typescript": "^5.0.4"
33 | },
34 | "dependencies": {
35 | "@types/chroma-js": "^2.4.0",
36 | "body-parser": "^1.20.2",
37 | "chalk": "^4.1.2",
38 | "chromadb": "^1.5.0",
39 | "console": "^0.7.2",
40 | "cors": "^2.8.5",
41 | "dotenv": "^16.0.3",
42 | "express": "^4.18.2",
43 | "install": "^0.13.0",
44 | "langchain": "^0.0.81",
45 | "mammoth": "^1.5.1",
46 | "module-alias": "^2.2.2",
47 | "multer": "^1.4.5-lts.1",
48 | "openai": "^3.2.1",
49 | "pdf-parse": "^1.1.1",
50 | "query-parser": "^0.0.1",
51 | "readline": "^1.3.0",
52 | "stream": "^0.0.2",
53 | "tsconfig-paths": "^4.2.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/frontend/src/components/revisionTabs.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Tab, Tabs, Box } from '@mui/material';
3 | import Markdown from './markdown';
4 |
5 | interface TabPanelProps {
6 | children?: React.ReactNode;
7 | index: number;
8 | value: number;
9 | }
10 |
11 | interface TabProps {
12 | label: string;
13 | value: number;
14 | }
15 |
16 | interface TabsProps {
17 | tabs: string[];
18 | }
19 |
20 | const TabPanel = ({ children, value, index }: TabPanelProps) => {
21 | return (
22 |
23 | {value === index && {children}}
24 |
25 | );
26 | };
27 |
28 | const RevisionTabs = ({ tabs }: TabsProps) => {
29 | const [value, setValue] = useState(0);
30 | const [numberTabs, setNumberTabs] = useState(0);
31 |
32 | const handleChange = (_event: React.SyntheticEvent, newValue: number) => {
33 | setValue(newValue);
34 | };
35 |
36 | useEffect(() => {
37 | if (tabs.length - 1 !== numberTabs) {
38 | setValue(tabs.length - 1);
39 | setNumberTabs(tabs.length - 1);
40 | }
41 | }, [tabs]);
42 |
43 | const renderTabs = () => {
44 | return tabs.map((tab, index) => (
45 |
50 | ));
51 | };
52 |
53 | const renderTabPanels = () => {
54 | return tabs.map((tab, index) => (
55 |
56 |
57 |
58 | ));
59 | };
60 |
61 | return (
62 |
63 |
64 | {renderTabs()}
65 |
66 | {renderTabPanels()}
67 |
68 | );
69 | };
70 |
71 | export default RevisionTabs;
72 |
--------------------------------------------------------------------------------
/frontend/src/services/apiService/useDocumentService.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 |
3 | import { useFetch } from 'hooks/useFetch';
4 | import { CollectionList } from 'types/collection';
5 | import { DocumentsObject, DocumentLoaderErrors } from 'types/documents';
6 |
7 | export interface AddDocumentsReturnType {
8 | documents: DocumentsObject;
9 | errors: DocumentLoaderErrors[];
10 | }
11 |
12 | export interface DeleteDocumentType {
13 | collectionName: string;
14 | fileName: string;
15 | }
16 |
17 | const useDocumentService = () => {
18 | const fetchService = useFetch();
19 |
20 | const addDocuments = useCallback(
21 | (files: FormData, signal?: AbortSignal) => {
22 | return fetchService.post(`/documents`, {
23 | body: files,
24 | signal
25 | });
26 | },
27 | [fetchService]
28 | );
29 |
30 | const deleteDocument = useCallback(
31 | (body: DeleteDocumentType, signal?: AbortSignal) => {
32 | return fetchService.post(`/documents/delete`, {
33 | body,
34 | signal
35 | });
36 | },
37 | [fetchService]
38 | );
39 |
40 | const getAllDocuments = useCallback(
41 | (signal?: AbortSignal) => {
42 | return fetchService.get(`/documents`, {
43 | signal
44 | });
45 | },
46 | [fetchService]
47 | );
48 |
49 | const getDocuments = useCallback(
50 | (collectionName: string, signal?: AbortSignal) => {
51 | return fetchService.get(
52 | `/documents?collectionName=${collectionName}`,
53 | {
54 | signal
55 | }
56 | );
57 | },
58 | [fetchService]
59 | );
60 |
61 | return {
62 | addDocuments,
63 | getDocuments,
64 | getAllDocuments,
65 | deleteDocument
66 | };
67 | };
68 |
69 | export default useDocumentService;
70 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Savant
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/frontend/src/components/markdown/markdown.tsx:
--------------------------------------------------------------------------------
1 | import rehypeMathjax from 'rehype-mathjax';
2 | import remarkGfm from 'remark-gfm';
3 | import remarkMath from 'remark-math';
4 | import ReactMarkdown, { Options } from 'react-markdown';
5 | import { CodeBlock } from '../codeBlock';
6 | import {
7 | Table,
8 | TableHead,
9 | TableRow,
10 | TableCell,
11 | TableBody
12 | } from '@mui/material';
13 |
14 | interface Props {
15 | message: string;
16 | }
17 |
18 | const Markdown = ({ message }: Props) => (
19 |
34 | ) : (
35 |
36 | {children}
37 |
38 | );
39 | },
40 | table({ children }) {
41 | return ;
42 | },
43 | thead({ children }) {
44 | return {children};
45 | },
46 | tbody({ children }) {
47 | return {children};
48 | },
49 | tr({ children }) {
50 | return {children};
51 | },
52 | th({ children }) {
53 | return {children};
54 | },
55 | td({ children }) {
56 | return {children};
57 | }
58 | }}
59 | >
60 | {message}
61 |
62 | );
63 |
64 | export default Markdown;
65 |
--------------------------------------------------------------------------------
/pyServer/server/routes/documents/routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request
2 | import json
3 | import os
4 | from typing import List
5 |
6 | from werkzeug.utils import secure_filename
7 | from werkzeug.datastructures import FileStorage
8 | from server.services.loaders import LoaderResult
9 | from server.services.vector_store import VectorStore
10 |
11 | from .service import DocumentService
12 |
13 | documents = Blueprint("documents", __name__)
14 |
15 | document_service = DocumentService()
16 |
17 |
18 | @documents.route("/documents", methods=["GET"])
19 | def get_documents_route():
20 | collection_name = request.args.get("collectionName")
21 |
22 | if not collection_name:
23 | return jsonify({"error": "collectionName is required"})
24 |
25 | return json.dumps(document_service.get_documents(collection_name))
26 |
27 |
28 | @documents.route("/documents", methods=["POST"])
29 | def post_documents_route():
30 | if "collectionName" not in request.form:
31 | return jsonify({"error": "collectionName is required"}), 400
32 |
33 | collection_name = request.form["collectionName"]
34 |
35 | documents: List[FileStorage] = request.files.getlist("documents")
36 |
37 | if documents is None:
38 | return jsonify({"error": "no files attached"}), 400
39 |
40 | return json.dumps(
41 | document_service.add_documents(collection_name, documents),
42 | default=lambda o: o.__dict__,
43 | )
44 |
45 |
46 | @documents.route("/documents/delete", methods=["POST"])
47 | def delete_document_route():
48 | data = request.get_json()
49 | collection_name = data.get("collectionName")
50 | file_name = data.get("fileName")
51 |
52 | if not collection_name or not file_name:
53 | return jsonify({"error": "collectionName and fileName is required"}), 400
54 |
55 | return json.dumps(document_service.delete_documents(collection_name, file_name))
56 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import {
4 | Container,
5 | CssBaseline,
6 | ThemeProvider,
7 | createTheme
8 | } from '@mui/material';
9 | import store from './store';
10 | import { ToastContainer } from 'react-toastify';
11 | import 'react-toastify/dist/ReactToastify.css';
12 | import {
13 | PageContainer,
14 | ContentContainer
15 | } from './components/containers/container.elements';
16 | import DocumentSideBar from './components/documentSideBar';
17 | import Conversation from './components/conversation';
18 | import DocumentLightBox from './components/documentLightBox';
19 | import { initializeApp } from 'firebase/app';
20 |
21 | import { AuthProvider } from 'providers/authProvider';
22 | import { firebaseConfig } from 'services/authService';
23 | import ConversationSideBar from 'components/conversationSideBar';
24 |
25 | const darkTheme = createTheme({
26 | palette: {
27 | mode: 'dark'
28 | }
29 | });
30 |
31 | export const firebaseApp = initializeApp(firebaseConfig);
32 |
33 | const App: React.FC = () => {
34 | return (
35 |
36 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default App;
66 |
--------------------------------------------------------------------------------
/frontend/src/components/containers/container.elements.tsx:
--------------------------------------------------------------------------------
1 | import { Grid } from '@mui/material';
2 | import { styled } from '@mui/material/styles';
3 |
4 | export const PageContainer = styled(Grid)(({ theme }) => ({
5 | minHeight: '100vh',
6 | maxHeight: '100vh',
7 | display: 'flex',
8 | justifyContent: 'center',
9 | flexDirection: 'column',
10 | flexWrap: 'nowrap',
11 | columnGap: theme.spacing(1)
12 | }));
13 |
14 | export const SidebarContainer = styled(Grid)(({ theme }) => ({
15 | display: 'flex',
16 | flexDirection: 'column',
17 | flex: 1,
18 | flexWrap: 'nowrap',
19 | minWidth: 250,
20 | backgroundColor: theme.palette.background.paper,
21 | justifyContent: 'center',
22 | '& > :not(:last-child)': {
23 | borderBottom: `solid 1px ${theme.palette.grey[700]}`
24 | }
25 | }));
26 |
27 | export const SidebarItem = styled(Grid)(({ theme }) => ({
28 | display: 'flex',
29 | p: 1,
30 | flex: 1,
31 | flexDirection: 'column',
32 | flexWrap: 'nowrap',
33 | padding: theme.spacing(1)
34 | }));
35 |
36 | export const ContentContainer = styled(Grid)(({ theme }) => ({
37 | flex: 1,
38 | display: 'flex',
39 | justifyContent: 'center',
40 | flexDirection: 'row',
41 | flexWrap: 'nowrap'
42 | }));
43 |
44 | export const ConversationContainer = styled(Grid)(({ theme }) => ({
45 | backgroundColor: theme.palette.grey[900],
46 | flex: 5,
47 | maxHeight: 'calc(100vh - 180px)',
48 | display: 'flex'
49 | }));
50 |
51 | export const ModalContentContainer = styled(Grid)(({ theme }) => ({
52 | backgroundColor: theme.palette.grey[800],
53 | flex: 1,
54 |
55 | display: 'flex',
56 | height: 'calc(100vh - 160px)'
57 | }));
58 |
59 | export const QueryContainer = styled(Grid)(({ theme }) => ({
60 | display: 'flex',
61 | flexDirection: 'column',
62 | flex: 4,
63 | p: 1,
64 | flexWrap: 'nowrap',
65 | minWidth: 200,
66 | justifyContent: 'center',
67 | backgroundColor: theme.palette.grey[900],
68 | overflow: 'hidden'
69 | }));
70 |
--------------------------------------------------------------------------------
/pyServer/server/utils/parse.py:
--------------------------------------------------------------------------------
1 | from chromadb.api.types import (
2 | Embedding,
3 | Metadata,
4 | Metadata,
5 | Document,
6 | GetResult,
7 | )
8 | from typing import Optional, Dict
9 |
10 |
11 | class DocumentsObjectInterface:
12 | def __init__(
13 | self,
14 | id: str,
15 | document: Optional[Document] = None,
16 | metadata: Optional[Metadata] = None,
17 | embedding: Optional[Embedding] = None,
18 | ):
19 | self.metadata = metadata or None
20 | self.embedding = embedding or None
21 | self.document = document or None
22 | self.id = id
23 |
24 | def to_dict(self):
25 | return {
26 | "metadata": self.metadata,
27 | "embedding": self.embedding,
28 | "document": self.document,
29 | "id": self.id,
30 | }
31 |
32 |
33 | class ProcessedDocumentReturnObject:
34 | id: str
35 | document: Optional[Document]
36 | metadata: Optional[Metadata]
37 | embedding: Optional[Embedding]
38 |
39 |
40 | def process_documents_into_objects(
41 | documents: GetResult,
42 | ) -> Dict[str, list[ProcessedDocumentReturnObject]]:
43 | objects = {}
44 | docs = documents.get("documents", [])
45 | metadatas = documents.get("metadatas", [])
46 | embeddings = documents.get("embeddings", [])
47 | ids = documents.get("ids", [])
48 |
49 | if not docs:
50 | return {}
51 |
52 | for i, document in enumerate(docs):
53 | metadata = metadatas[i] if metadatas else None
54 | embedding = embeddings[i] if embeddings else None
55 | doc_id = ids[i]
56 | filename = (
57 | str(metadata.get("filename"))
58 | if metadata and "filename" in metadata
59 | else "unknown"
60 | )
61 |
62 | if filename not in objects:
63 | objects[str(filename)] = []
64 | else:
65 | objects[filename].append(
66 | DocumentsObjectInterface(
67 | id=doc_id, document=document, metadata=metadata, embedding=embedding
68 | ).to_dict()
69 | )
70 |
71 | return objects
72 |
--------------------------------------------------------------------------------
/frontend/src/components/fileIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FileIcon, defaultStyles, DefaultExtensionType } from 'react-file-icon';
3 |
4 | interface FileIconProps {
5 | fileName: string;
6 | }
7 |
8 | const extensionToDefault: Record = {
9 | ai: 'ai',
10 | avi: 'avi',
11 | bmp: 'bmp',
12 | c: 'c',
13 | cpp: 'cpp',
14 | css: 'css',
15 | csv: 'csv',
16 | dmg: 'dmg',
17 | doc: 'doc',
18 | docx: 'docx',
19 | dwg: 'dwg',
20 | dxf: 'dxf',
21 | eps: 'eps',
22 | exe: 'exe',
23 | flv: 'flv',
24 | gif: 'gif',
25 | html: 'html',
26 | java: 'java',
27 | jpg: 'jpg',
28 | js: 'js',
29 | json: 'json',
30 | mid: 'mid',
31 | mp3: 'mp3',
32 | mp4: 'mp4',
33 | mpg: 'mpg',
34 | odp: 'odp',
35 | ods: 'ods',
36 | odt: 'odt',
37 | pdf: 'pdf',
38 | php: 'php',
39 | png: 'png',
40 | ppt: 'ppt',
41 | pptx: 'pptx',
42 | psd: 'psd',
43 | py: 'py',
44 | rar: 'rar',
45 | rb: 'rb',
46 | rtf: 'rtf',
47 | scss: 'scss',
48 | tif: 'tif',
49 | tiff: 'tiff',
50 | ts: 'ts',
51 | txt: 'txt',
52 | wav: 'wav',
53 | xls: 'xls',
54 | xlsx: 'xlsx',
55 | yml: 'yml',
56 | zip: 'zip'
57 | };
58 |
59 | const customStyles = {
60 | ...defaultStyles,
61 | borderRadius: '50%',
62 | boxShadow: '0px 0px 5px rgba(0, 0, 0, 0.1)',
63 | height: '50px',
64 | width: '50px'
65 | };
66 |
67 | const getFileExtension = (filename: string): string => {
68 | return filename
69 | .slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2)
70 | .toLowerCase();
71 | };
72 |
73 | const FileIconComponent: React.FC = ({ fileName }) => {
74 | const fileExtension = getFileExtension(fileName.toLowerCase());
75 | const defaultExtension = extensionToDefault[fileExtension];
76 | const fileIconStyles = customStyles[
77 | defaultExtension
78 | ] as Partial;
79 |
80 | return (
81 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default FileIconComponent;
95 |
--------------------------------------------------------------------------------
/pyServer/server/routes/collections/service.py:
--------------------------------------------------------------------------------
1 | import queue
2 | import threading
3 | from server.services.vector_store import VectorStore
4 | from chromadb.api.models.Collection import Collection
5 | from typing import Sequence
6 |
7 |
8 | class ThreadWithException(threading.Thread):
9 | def __init__(self, target, *args, **kwargs):
10 | super().__init__(target=target, *args, **kwargs)
11 | self.error = None
12 |
13 | def run(self):
14 | try:
15 | super().run()
16 | except Exception as e:
17 | self.error = e
18 |
19 |
20 | class CollectionService:
21 | vector_store = VectorStore()
22 |
23 | def get_collection(self, collection_name: str = ""):
24 | collections: Sequence[Collection] = []
25 |
26 | if collection_name:
27 | CollectionService.vector_store.set_create_chroma_store(collection_name)
28 |
29 | collection = CollectionService.vector_store.get_collection(collection_name)
30 | collections = [collection] if collection else []
31 | else:
32 | collections = CollectionService.vector_store.list_collections()
33 |
34 | result = [
35 | {"name": collection.name, "metadata": collection.metadata}
36 | for collection in collections
37 | ]
38 | return result
39 |
40 | def create_collection(self, collection_name: str):
41 | CollectionService.vector_store.create_collection(collection_name)
42 |
43 | collections = CollectionService.vector_store.list_collections()
44 |
45 | result = [
46 | {"name": collection.name, "metadata": collection.metadata}
47 | for collection in collections
48 | ]
49 | return result
50 |
51 | def delete_collection(self, collection_name: str):
52 | print("delete start", collection_name)
53 | CollectionService.vector_store.delete_collection(collection_name)
54 |
55 | print("delete complete", collection_name)
56 |
57 | collections = CollectionService.vector_store.list_collections()
58 |
59 | result = [
60 | {"name": collection.name, "metadata": collection.metadata}
61 | for collection in collections
62 | ]
63 | return result
64 |
--------------------------------------------------------------------------------
/server/src/routes/documents/controller.ts:
--------------------------------------------------------------------------------
1 | import { VectorStore } from '@/services/vector-store';
2 | import { Request, Response } from 'express';
3 |
4 | export interface Controller {
5 | getDocuments(req: Request, res: Response): Promise;
6 | addDocuments(req: Request, res: Response): Promise;
7 | deleteDocuments(req: Request, res: Response): Promise;
8 | clearDocuments(req: Request, res: Response): Promise;
9 | }
10 |
11 | export default () => {
12 | const vectorStore = new VectorStore();
13 |
14 | return {
15 | getDocuments: async (req: Request, res: Response) => {
16 | const { collectionName } = req.query;
17 |
18 | const collection = await vectorStore.getCollection(
19 | collectionName as string
20 | );
21 |
22 | const documents = await vectorStore.getDocuments(collection);
23 |
24 | res.json(documents);
25 | },
26 |
27 | addDocuments: async (req: Request, res: Response) => {
28 | const files = req.files as Express.Multer.File[];
29 |
30 | const collectionName = req.body.collectionName as string;
31 |
32 | const promises = files.map((file: Express.Multer.File) =>
33 | vectorStore.addDocuments(file)
34 | );
35 |
36 | try {
37 | const results = await Promise.all(promises).then((results) =>
38 | results.reduce(
39 | (acc, cur) => ({
40 | documents: [...acc.documents, ...cur.documents],
41 | errors: [...acc.errors, ...cur.errors]
42 | }),
43 | { documents: [], errors: [] }
44 | )
45 | );
46 |
47 | const collection = await vectorStore.getCollection(collectionName);
48 | const documents = await vectorStore.getDocuments(collection);
49 |
50 | res.json({ documents, errors: results.errors });
51 | } catch (error) {
52 | res.status(500).send('Internal Server Error');
53 | }
54 | },
55 |
56 | deleteDocuments: async (req: Request, res: Response) => {
57 | const { collectionName, fileName } = req.body;
58 |
59 | const updatedDocuments = await vectorStore.deleteDocuments(
60 | collectionName,
61 | fileName
62 | );
63 | res.json(updatedDocuments);
64 | },
65 |
66 | clearDocuments: async (req: Request, res: Response) => {
67 | res.json();
68 | }
69 | };
70 | };
71 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@azure/cosmos": "^3.17.3",
7 | "@emotion/react": "^11.10.8",
8 | "@emotion/styled": "^11.10.8",
9 | "@mui/icons-material": "^5.11.16",
10 | "@mui/material": "^5.12.2",
11 | "@reduxjs/toolkit": "^1.9.5",
12 | "@tabler/icons-react": "^2.17.0",
13 | "@typescript-eslint/eslint-plugin": "^5.59.7",
14 | "@typescript-eslint/parser": "^5.59.7",
15 | "eslint": "^8.41.0",
16 | "eslint-plugin-prettier": "^4.2.1",
17 | "file-icon": "^5.1.1",
18 | "firebase": "^9.22.1",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "react-dropzone": "^14.2.3",
22 | "react-file-icon": "^1.3.0",
23 | "react-icons": "^4.8.0",
24 | "react-markdown": "^8.0.7",
25 | "react-redux": "^8.0.5",
26 | "react-resizable": "^3.0.5",
27 | "react-scripts": "5.0.1",
28 | "react-syntax-highlighter": "^15.5.0",
29 | "react-toastify": "^9.1.2",
30 | "react-virtualized-auto-sizer": "^1.0.15",
31 | "react-window": "^1.8.9",
32 | "redux": "^4.2.1",
33 | "rehype-mathjax": "^4.0.2",
34 | "remark-gfm": "^3.0.1",
35 | "remark-math": "^5.1.1",
36 | "typescript": "^4.9.5",
37 | "web-vitals": "^2.1.4"
38 | },
39 | "devDependencies": {
40 | "@testing-library/jest-dom": "^5.16.5",
41 | "@testing-library/react": "^13.4.0",
42 | "@testing-library/user-event": "^13.5.0",
43 | "@types/bytes": "^3.1.1",
44 | "@types/jest": "^27.5.2",
45 | "@types/node": "^16.18.25",
46 | "@types/react": "^18.2.0",
47 | "@types/react-dom": "^18.2.1",
48 | "@types/react-dropzone": "^5.1.0",
49 | "@types/react-file-icon": "^1.0.1",
50 | "@types/react-resizable": "^3.0.4",
51 | "@types/react-syntax-highlighter": "^15.5.6",
52 | "@types/react-window": "^1.8.5",
53 | "@types/throttle-debounce": "^5.0.0",
54 | "prettier": "^2.8.8"
55 | },
56 | "scripts": {
57 | "start": "react-scripts start",
58 | "build": "react-scripts build",
59 | "test": "react-scripts test",
60 | "eject": "react-scripts eject"
61 | },
62 | "eslintConfig": {
63 | "extends": [
64 | "react-app",
65 | "react-app/jest"
66 | ]
67 | },
68 | "browserslist": {
69 | "production": [
70 | ">0.2%",
71 | "not dead",
72 | "not op_mini all"
73 | ],
74 | "development": [
75 | "last 1 chrome version",
76 | "last 1 firefox version",
77 | "last 1 safari version"
78 | ]
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/server/src/langchain/callbacks/console-callback-handler.ts:
--------------------------------------------------------------------------------
1 | import { BaseCallbackHandler } from 'langchain/callbacks';
2 | import chalk from 'chalk';
3 | import {
4 | ChainValues,
5 | AgentAction,
6 | AgentFinish,
7 | LLMResult
8 | } from 'langchain/schema';
9 |
10 | class ConsoleCallbackHandler extends BaseCallbackHandler {
11 | name = 'ConsoleCallbackHandler';
12 |
13 | async handleChainStart(chain: { name: string }) {
14 | console.log(
15 | chalk.blue(
16 | `===== Entering new ${chalk.blue.bold(chain.name)} chain =====`
17 | )
18 | );
19 | }
20 |
21 | async handleChainEnd(output: ChainValues) {
22 | console.log(chalk.blue(`==== Finished chain ====`));
23 | console.log(JSON.stringify(output, null, 2));
24 | }
25 |
26 | async handleLLMStart(
27 | llm: {
28 | name: string;
29 | },
30 | prompts: string[],
31 | runId: string,
32 | parentRunId?: string | undefined
33 | ) {
34 | console.log(chalk.blue(`==== LLM end ====`));
35 | console.log(`${chalk.green.bold(`llm:`)} ${llm.name}`);
36 | console.log(`${chalk.green.bold(`prompts:`)} ${prompts}`);
37 | console.log(`${chalk.green.bold(`runId:`)} ${runId}`);
38 | console.log(`${chalk.green.bold(`parentRunId:`)} ${parentRunId}`);
39 | }
40 |
41 | async handleLLMEnd(output: LLMResult) {
42 | console.log(
43 | chalk.blue(`==== LLM end ====`),
44 | JSON.stringify(output, null, 2)
45 | );
46 | }
47 |
48 | async handleAgentAction(action: AgentAction) {
49 | console.log(chalk.blue(`==== Agent Action ====`));
50 | console.log(action);
51 | console.log(`${chalk.magenta(' Agent Action RAW:')} ${action}`);
52 | console.log(`${chalk.green.bold(` Agent Tool:`)} ${action.tool}`);
53 | console.log(`${chalk.green.bold(` Agent Input:`)} ${action.toolInput}`);
54 | console.log(`${chalk.green.bold(` Agent Log:`)} ${action.log}`);
55 | }
56 |
57 | async handleToolEnd(output: string) {
58 | console.log(chalk.blue(`==== Tool End ====`));
59 | console.log(output);
60 | }
61 |
62 | async handleText(text: string) {
63 | console.log(chalk.blue(`==== Text ====`));
64 | console.log(text);
65 | }
66 |
67 | async handleAgentEnd(action: AgentFinish) {
68 | console.log(chalk.blue(`==== Agent Action End ====`));
69 | console.log(
70 | `${chalk.blue.bold(` Agent Return Values:`)} ${JSON.stringify(
71 | action.returnValues,
72 | null,
73 | 2
74 | )}`
75 | );
76 | }
77 | }
78 |
79 | export { ConsoleCallbackHandler };
80 |
--------------------------------------------------------------------------------
/server/src/routes/collections/controller.ts:
--------------------------------------------------------------------------------
1 | import { VectorStore } from '@/services/vector-store';
2 | import { Request, Response } from 'express';
3 | import { Readable } from 'stream';
4 |
5 | export interface Controller {
6 | getCollection(req: Request, res: Response): Promise;
7 | createCollection(req: Request, res: Response): Promise;
8 | deleteCollection(req: Request, res: Response): Promise;
9 | question(req: Request, res: Response): Promise;
10 | }
11 |
12 | export default () => {
13 | const vectorStore = new VectorStore();
14 | return {
15 | getCollection: async (req: Request, res: Response) => {
16 | const { collectionName } = req.query;
17 | let collections;
18 |
19 | if (collectionName) {
20 | await vectorStore.setCreateChromaStore(collectionName as string);
21 | const collection = await vectorStore.getCollection(
22 | collectionName as string
23 | );
24 | collections = collection ? [collection] : [];
25 | } else {
26 | collections = await vectorStore.listCollections();
27 | }
28 |
29 | res.json(collections);
30 | },
31 |
32 | createCollection: async (req: Request, res: Response) => {
33 | const { collectionName } = req.body;
34 |
35 | if (!collectionName) res.status(400).send('Bad Request');
36 |
37 | await vectorStore.createCollection(collectionName as string);
38 | const collections = await vectorStore.listCollections();
39 |
40 | res.json(collections);
41 | },
42 |
43 | deleteCollection: async (req: Request, res: Response) => {
44 | const { name } = req.params;
45 |
46 | const collection = await vectorStore.getCollection(name);
47 | if (collection) {
48 | await vectorStore.deleteCollection(name);
49 | }
50 |
51 | const collections = await vectorStore.listCollections();
52 | res.json(collections);
53 | },
54 |
55 | question: async (req: Request, res: Response) => {
56 | res.setHeader('Content-Type', 'application/json; charset=utf-8');
57 | res.setHeader('Transfer-Encoding', 'chunked');
58 |
59 | const { question, systemPrompt, queryType, temperature } = req.body;
60 |
61 | const stream = new Readable({
62 | read() {}
63 | });
64 |
65 | stream.pipe(res);
66 |
67 | const streamCallback = (token: string) => {
68 | stream.push(token);
69 | };
70 |
71 | await vectorStore.askQuestion(
72 | question,
73 | systemPrompt,
74 | queryType,
75 | temperature,
76 | streamCallback
77 | );
78 | res.end();
79 | }
80 | };
81 | };
82 |
--------------------------------------------------------------------------------
/pyServer/server/routes/documents/service.py:
--------------------------------------------------------------------------------
1 | import os
2 | from server.utils.parse import ProcessedDocumentReturnObject
3 | from server.services.loaders import LoaderError
4 | from server.utils.parse import DocumentsObjectInterface
5 | from server.services.vector_store import VectorStore
6 | from langchain.docstore.document import Document
7 | from werkzeug.utils import secure_filename
8 | from werkzeug.datastructures import FileStorage
9 | from server.services.loaders import LoaderResult
10 | from chromadb.api.models.Collection import Collection
11 | from typing import List, Dict
12 |
13 |
14 | class DocumentReturnObject:
15 | def __init__(
16 | self,
17 | documents: Dict[str, list[ProcessedDocumentReturnObject]],
18 | errors: List[LoaderError],
19 | ):
20 | self.documents = documents
21 | self.errors = errors
22 |
23 |
24 | class DocumentService:
25 | vector_store = VectorStore()
26 |
27 | def get_documents(
28 | self, collection_name
29 | ) -> Dict[str, list[ProcessedDocumentReturnObject]]:
30 | collection: Collection = DocumentService.vector_store.get_collection(
31 | collection_name
32 | )
33 |
34 | documents = DocumentService.vector_store.get_documents(collection)
35 |
36 | return documents
37 |
38 | def add_documents(self, collection_name: str, documents: List[FileStorage]):
39 | results: List[LoaderResult] = []
40 | errors: List[LoaderError] = []
41 | save_temp_folder = "server/save_temp_files"
42 |
43 | for file in documents:
44 | if file.filename:
45 | filename = secure_filename(file.filename)
46 | file_path = os.path.join(save_temp_folder, filename)
47 | file.save(file_path)
48 |
49 | results.append(
50 | DocumentService.vector_store.add_documents(
51 | collection_name, file_path, filename
52 | )
53 | )
54 | os.remove(file_path)
55 |
56 | for result in results:
57 | errors.extend(result.errors)
58 |
59 | collection: Collection = DocumentService.vector_store.get_collection(
60 | collection_name
61 | )
62 |
63 | returned_documents: Dict[
64 | str, list[ProcessedDocumentReturnObject]
65 | ] = DocumentService.vector_store.get_documents(collection)
66 |
67 | return DocumentReturnObject(documents=returned_documents, errors=errors)
68 |
69 | def delete_documents(self, collection_name: str, filename: str):
70 | return DocumentService.vector_store.delete_documents(collection_name, filename)
71 |
--------------------------------------------------------------------------------
/pyServer/server/services/loaders.py:
--------------------------------------------------------------------------------
1 | from werkzeug.datastructures import FileStorage
2 | from langchain.docstore.document import Document
3 | from langchain.document_loaders import (
4 | PyPDFLoader,
5 | JSONLoader,
6 | TextLoader,
7 | CSVLoader,
8 | Docx2txtLoader,
9 | )
10 | from langchain.text_splitter import RecursiveCharacterTextSplitter
11 | from typing import Union, List, Optional
12 |
13 | ALLOWED_EXTENSIONS = {
14 | "json",
15 | "txt",
16 | "pdf",
17 | "tsx",
18 | "ts",
19 | "js",
20 | "css",
21 | "csv",
22 | "docx",
23 | }
24 |
25 |
26 | class LoaderError:
27 | error: str
28 | item: str
29 |
30 | def __init__(self, error: str, item: str):
31 | self.error = error
32 | self.item = item
33 |
34 |
35 | class LoaderResult:
36 | documents: List[Document]
37 | errors: List[LoaderError]
38 |
39 | def __init__(
40 | self, documents: List[Document], errors: Optional[List[LoaderError]] = None
41 | ):
42 | self.documents = documents
43 | self.errors = errors if errors is not None else []
44 |
45 |
46 | text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
47 |
48 | loaders = {
49 | "json": lambda file: JSONLoader(file, "/texts"),
50 | "pdf": lambda file: PyPDFLoader(file),
51 | "txt": lambda file: TextLoader(file),
52 | "tsx": lambda file: TextLoader(file),
53 | "ts": lambda file: TextLoader(file),
54 | "js": lambda file: TextLoader(file),
55 | "css": lambda file: TextLoader(file),
56 | "csv": lambda file: CSVLoader(file),
57 | "docx": lambda file: Docx2txtLoader(file),
58 | }
59 |
60 |
61 | def get_file_extension(filename: str) -> str:
62 | return filename.rsplit(".", 1)[-1].lower()
63 |
64 |
65 | def loader(file_path: str, filename: str) -> LoaderResult:
66 | if not filename or not file_path:
67 | return LoaderResult([], [])
68 |
69 | errors: List[LoaderError] = []
70 |
71 | extension = get_file_extension(filename)
72 |
73 | load_fn = loaders.get(extension)
74 |
75 | if not load_fn:
76 | print(f"==========================Unsupported file extension: {extension}")
77 | errors.append(
78 | (
79 | LoaderError(
80 | error=f"Unsupported file extension: {extension}", item=filename
81 | )
82 | )
83 | )
84 |
85 | return LoaderResult([], errors)
86 |
87 | file_loader = load_fn(file_path)
88 |
89 | documents: List[Document] = file_loader.load_and_split(text_splitter)
90 |
91 | for doc in documents:
92 | metadata = doc.metadata
93 | metadata["filename"] = filename
94 |
95 | return LoaderResult(documents, errors)
96 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
--------------------------------------------------------------------------------
/server/src/loaders.ts:
--------------------------------------------------------------------------------
1 | import {
2 | JSONLoader,
3 | JSONLinesLoader
4 | } from 'langchain/document_loaders/fs/json';
5 |
6 | import { TextLoader } from 'langchain/document_loaders/fs/text';
7 | import { CSVLoader } from 'langchain/document_loaders/fs/csv';
8 | import { PDFLoader } from 'langchain/document_loaders/fs/pdf';
9 | import { DocxLoader } from 'langchain/document_loaders/fs/docx';
10 | import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
11 | import { Document } from 'langchain/dist/document';
12 |
13 | interface DocumentLoaders {
14 | [key: string]: (file: Blob) => Promise;
15 | }
16 |
17 | const getFileExtension = (filename: string): string => {
18 | return filename
19 | .slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2)
20 | .toLowerCase();
21 | };
22 |
23 | const getBlobFromFile = (file: Express.Multer.File): Blob => {
24 | const { buffer } = file;
25 | const type = file.mimetype;
26 | const blob = new Blob([buffer], { type });
27 | return blob;
28 | };
29 |
30 | const textSplitter = new RecursiveCharacterTextSplitter({
31 | chunkSize: 1000,
32 | chunkOverlap: 0
33 | });
34 |
35 | const loaders: DocumentLoaders = {
36 | json: async (file: Blob) => new JSONLoader(file, '/texts'),
37 | jsonl: async (file: Blob) => new JSONLinesLoader(file, '/html'),
38 | txt: async (file: Blob) => new TextLoader(file),
39 | tsx: async (file: Blob) => new TextLoader(file),
40 | ts: async (file: Blob) => new TextLoader(file),
41 | css: async (file: Blob) => new TextLoader(file),
42 | csv: async (file: Blob) => new CSVLoader(file),
43 | docx: async (file: Blob) => new DocxLoader(file),
44 | pdf: async (file: Blob) =>
45 | new PDFLoader(file, {
46 | splitPages: false
47 | })
48 | };
49 |
50 | export const loader = async (
51 | file: Express.Multer.File
52 | ): Promise<{
53 | documents: Document[];
54 | errors: { error: string; item: string }[];
55 | }> => {
56 | const documents: Document[] = [];
57 | const errors: { error: string; item: string }[] = [];
58 |
59 | const blob = getBlobFromFile(file);
60 | const extension = getFileExtension(file.originalname);
61 | const loadFn = loaders[extension];
62 |
63 | if (!loadFn) {
64 | errors.push({
65 | error: `Unsupported file extension: ${extension}`,
66 | item: file.originalname
67 | });
68 | } else {
69 | const fileLoader = await loadFn(blob);
70 | const docs: Document[] = await fileLoader.loadAndSplit(textSplitter);
71 | const customMetadata = { filename: file.originalname };
72 |
73 | documents.push(
74 | ...docs.map((doc: Document) => {
75 | return {
76 | ...doc,
77 | metadata: {
78 | ...doc.metadata,
79 | ...customMetadata
80 | }
81 | };
82 | })
83 | );
84 | }
85 |
86 | return { documents, errors };
87 | };
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Savant: Ask me a question, I'll tell you no lies.
4 |
5 | An AI document ingestion, analysis and query tool
6 |
7 | **_ in development _**
8 |
9 | ## How it works
10 |
11 | ### Clone
12 |
13 | ```bash
14 | git clone https://github.com/foolishsailor/savant.git
15 | ```
16 |
17 | ### Install Dependencies
18 |
19 | #### Frontend
20 |
21 | ```bash
22 | cd frontend
23 | npm i
24 | ```
25 |
26 | #### Server
27 |
28 | ```bash
29 | cd server
30 | npm i
31 | ```
32 |
33 | -- or --
34 |
35 | #### Python Server
36 |
37 | ##### Install poetry
38 |
39 | [Poetry Install Guide](https://python-poetry.org/docs/)
40 |
41 | **Linux, macOS, Windows (WSL)**
42 |
43 | ```
44 | curl -sSL https://install.python-poetry.org | python3 -
45 | ```
46 |
47 | **Windows (Powershell)**
48 |
49 | ```
50 | curl -sSL https://install.python-poetry.org | python3 -
51 | ```
52 |
53 | ```bash
54 | (Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | py -
55 | npm i
56 | ```
57 |
58 | ##### Configure Poetry
59 |
60 | **config poetry to creaty viertual env in locl directory**
61 |
62 | ```
63 | poetry config virtualenvs.in-project true
64 | ```
65 |
66 | **create poetry virtual env**
67 |
68 | ```
69 | poetry shell
70 | ```
71 |
72 | **install application in virtual env**
73 |
74 | ```
75 | poetry install
76 | ```
77 |
78 | **select the python.exe in the virtual environment as your interpreter**
79 |
80 | This will create a .venv in the pyServer directory.
81 |
82 | [How to select a python intrepreter](https://code.visualstudio.com/docs/python/environments)
83 |
84 | Following the above guide select the python.exe found in: pyServer/.venv/Scripts/python.exe
85 |
86 | ### Configure Settings
87 |
88 | Create a local .env file in the root of the repo using the .env.template as a guide for both Frontend and Backend
89 |
90 | ### Install External Applications
91 |
92 | [Chroma](https://www.trychroma.com/) local instance for a vector database
93 |
94 | [Docker](https://www.docker.com/) to run Chroma in a local docker container.
95 |
96 | #### 1. Download docker here: [Docker](https://www.docker.com/) and install
97 |
98 | ```bash
99 | git clone https://github.com/chroma-core/chroma.git
100 | ```
101 |
102 | ### Run Chroma
103 |
104 | ```bash
105 | cd chroma
106 | docker-compose up -d --build
107 | ```
108 |
109 | ### Run Savant BE
110 |
111 | ```bash
112 | cd server
113 | npm start
114 | ```
115 |
116 | -- or --
117 |
118 | ### Run Savant Python BE
119 |
120 | ```bash
121 | cd pyServer
122 | poetry run python main.py
123 | ```
124 |
125 | ### Run Savant FE
126 |
127 | ```bash
128 | cd frontend
129 | npm start
130 | ```
131 |
132 | ## Contact
133 |
134 | If you have any questions, find me on [Twitter](https://twitter.com/foolishsailor).
135 |
--------------------------------------------------------------------------------
/pyServer/server/routes/collections/routes.py:
--------------------------------------------------------------------------------
1 | import os
2 | import queue
3 | import threading
4 | from flask import (
5 | stream_with_context,
6 | Blueprint,
7 | jsonify,
8 | request,
9 | Response,
10 | make_response,
11 | g,
12 | )
13 |
14 | from server.services.vector_store import VectorStore
15 |
16 | from .service import CollectionService
17 | import json
18 |
19 |
20 | collections = Blueprint("collections", __name__)
21 |
22 | collection_service = CollectionService()
23 |
24 |
25 | @collections.route("/collections", methods=["GET"])
26 | def get_collections_route():
27 | collection_name = request.args.get("collectionName")
28 |
29 | return json.dumps(
30 | collection_service.get_collection(str(collection_name))
31 | if collection_name is not None
32 | else collection_service.get_collection()
33 | )
34 |
35 |
36 | @collections.route("/collections", methods=["POST"])
37 | def post_collections_route():
38 | data = request.get_json()
39 | collection_name = data.get("collectionName")
40 |
41 | if not collection_name:
42 | return jsonify({"error": "collectionName is required"})
43 |
44 | return json.dumps(collection_service.create_collection(collection_name))
45 |
46 |
47 | @collections.route("/collections/", methods=["DELETE"])
48 | def delete_collection_route(collection_name):
49 | print("DELETE collection_name: ", collection_name)
50 | if not collection_name:
51 | return jsonify({"error": "collectionName is required"})
52 |
53 | return json.dumps(collection_service.delete_collection(collection_name))
54 |
55 |
56 | @collections.route("/collections/question", methods=["POST"])
57 | def question_route():
58 | data = request.get_json()
59 | question = data.get("question")
60 | model_name = data.get("model")
61 | system_prompt = data.get("systemPrompt")
62 | query_type = data.get("queryType")
63 | temperature = data.get("temperature")
64 | collection_name = data.get("collectionName")
65 | vector_store = VectorStore()
66 | q = queue.Queue()
67 | stop_thread = threading.Event()
68 |
69 | def stream_callback(token):
70 | q.put(token)
71 |
72 | def generate():
73 | while True:
74 | token: str = q.get()
75 | if token is None:
76 | break
77 | yield token
78 | if request.environ.get("werkzeug.server.shutdown"):
79 | stop_thread.set()
80 |
81 | def query_and_signal_end():
82 | vector_store.ask_question(
83 | question,
84 | model_name,
85 | system_prompt,
86 | query_type,
87 | temperature,
88 | collection_name,
89 | stream_callback,
90 | )
91 | q.put(os.getenv("CHAT_END_TRIGGER_MESSAGE"))
92 |
93 | thread = threading.Thread(target=query_and_signal_end)
94 | thread.start()
95 |
96 | return Response(stream_with_context(generate()), mimetype="application/json")
97 |
--------------------------------------------------------------------------------
/frontend/src/components/codeBlock.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Grid } from '@mui/material';
2 |
3 | import DownloadIcon from '@mui/icons-material/Download';
4 | import ContentPasteIcon from '@mui/icons-material/ContentPaste';
5 | import CheckBoxIcon from '@mui/icons-material/CheckBox';
6 |
7 | import { FC, memo, useState } from 'react';
8 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
9 | import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
10 | import { useTheme } from '@mui/system';
11 |
12 | import { generateRandomString, programmingLanguages } from '../utils/codeblock';
13 |
14 | interface Props {
15 | language: string;
16 | value: string;
17 | }
18 |
19 | export const CodeBlock: FC = memo(({ language, value }) => {
20 | const theme = useTheme();
21 | const [isCopied, setIsCopied] = useState(false);
22 |
23 | const copyToClipboard = () => {
24 | if (!navigator.clipboard || !navigator.clipboard.writeText) {
25 | return;
26 | }
27 |
28 | navigator.clipboard.writeText(value).then(() => {
29 | setIsCopied(true);
30 |
31 | setTimeout(() => {
32 | setIsCopied(false);
33 | }, 2000);
34 | });
35 | };
36 | const downloadAsFile = () => {
37 | const fileExtension = programmingLanguages[language] || '.file';
38 | const suggestedFileName = `file-${generateRandomString(
39 | 3,
40 | true
41 | )}${fileExtension}`;
42 | const fileName = window.prompt('Enter file name' || '', suggestedFileName);
43 |
44 | if (!fileName) {
45 | // user pressed cancel on prompt
46 | return;
47 | }
48 |
49 | const blob = new Blob([value], { type: 'text/plain' });
50 | const url = URL.createObjectURL(blob);
51 | const link = document.createElement('a');
52 | link.download = fileName;
53 | link.href = url;
54 | link.style.display = 'none';
55 | document.body.appendChild(link);
56 | link.click();
57 | document.body.removeChild(link);
58 | URL.revokeObjectURL(url);
59 | };
60 | return (
61 |
64 |
65 | {language}
66 |
67 |
68 |
75 |
81 |
82 |
83 |
84 |
89 | {value}
90 |
91 |
92 | );
93 | });
94 | CodeBlock.displayName = 'CodeBlock';
95 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useFetch.ts:
--------------------------------------------------------------------------------
1 | export type RequestModel = {
2 | params?: object;
3 | headers?: object;
4 | signal?: AbortSignal;
5 | };
6 |
7 | export type RequestWithBodyModel = RequestModel & {
8 | body?: object | FormData;
9 | };
10 |
11 | export const useFetch = () => {
12 | const BASE_URL = process.env.REACT_APP_API_BASE_URL;
13 | const handleFetch = async (
14 | url: string,
15 | request: any,
16 | signal?: AbortSignal
17 | ) => {
18 | const requestUrl = request?.params
19 | ? `${BASE_URL}${url}${request.params}`
20 | : `${BASE_URL}${url}`;
21 |
22 | const requestBody = request?.body
23 | ? request.body instanceof FormData
24 | ? { ...request, body: request.body }
25 | : { ...request, body: JSON.stringify(request.body) }
26 | : request;
27 |
28 | const headers: any = {
29 | ...(request?.headers
30 | ? request.headers
31 | : request?.body && request.body instanceof FormData
32 | ? {}
33 | : { 'Content-type': 'application/json' })
34 | };
35 |
36 | try {
37 | const response: Response = await fetch(requestUrl, {
38 | ...requestBody,
39 | headers,
40 | signal
41 | });
42 |
43 | if (!response.ok) throw response;
44 |
45 | const contentType = response.headers.get('content-type');
46 | const contentDisposition = response.headers.get('content-disposition');
47 |
48 | const result =
49 | contentType &&
50 | (contentType?.indexOf('application/json') !== -1 ||
51 | contentType?.indexOf('text/plain') !== -1 ||
52 | contentType?.indexOf('text/html') !== -1)
53 | ? response.json()
54 | : contentDisposition?.indexOf('attachment') !== -1
55 | ? response.blob()
56 | : response;
57 |
58 | return result;
59 | } catch (err: any) {
60 | const contentType = err.headers.get('content-type');
61 |
62 | const errResult =
63 | contentType && contentType?.indexOf('application/problem+json') !== -1
64 | ? await err.json()
65 | : err;
66 |
67 | throw errResult;
68 | }
69 | };
70 |
71 | return {
72 | get: async (url: string, request?: RequestModel): Promise => {
73 | return handleFetch(url, { ...request, method: 'get' });
74 | },
75 | post: async (
76 | url: string,
77 | request?: RequestWithBodyModel
78 | ): Promise => {
79 | return handleFetch(url, { ...request, method: 'post' });
80 | },
81 | put: async (url: string, request?: RequestWithBodyModel): Promise => {
82 | return handleFetch(url, { ...request, method: 'put' });
83 | },
84 | patch: async (
85 | url: string,
86 | request?: RequestWithBodyModel
87 | ): Promise => {
88 | return handleFetch(url, { ...request, method: 'patch' });
89 | },
90 | delete: async (url: string, request?: RequestModel): Promise => {
91 | return handleFetch(url, { ...request, method: 'delete' });
92 | }
93 | };
94 | };
95 |
--------------------------------------------------------------------------------
/frontend/src/components/lists/conversationList.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { RootState } from '../../store';
3 | import { Grid, useTheme, Typography, Avatar } from '@mui/material';
4 |
5 | import Markdown from '../markdown';
6 | import { AiOutlineRobot } from 'react-icons/ai';
7 | import { BsPerson } from 'react-icons/bs';
8 | import RevisionTabs from '../revisionTabs';
9 |
10 | const ConversationList = () => {
11 | const theme = useTheme();
12 |
13 | const conversation = useSelector(
14 | (state: RootState) => state.conversation.conversation
15 | );
16 |
17 | return (
18 |
28 | {conversation.map((message, index) => {
29 | return (
30 |
49 |
50 | {message.source === 'assistant' ? (
51 |
58 |
59 |
60 | ) : (
61 |
68 |
69 |
70 | )}
71 |
72 |
73 |
74 | {message.content.length > 1 ? (
75 |
76 | ) : (
77 |
78 | )}
79 |
80 |
81 |
82 | );
83 | })}
84 |
90 |
91 | );
92 | };
93 |
94 | export default ConversationList;
95 |
--------------------------------------------------------------------------------
/.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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # Logs
26 | logs
27 | *.log
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | lerna-debug.log*
32 | .pnpm-debug.log*
33 |
34 | # Diagnostic reports (https://nodejs.org/api/report.html)
35 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
36 |
37 | # Runtime data
38 | pids
39 | *.pid
40 | *.seed
41 | *.pid.lock
42 |
43 | # Directory for instrumented libs generated by jscoverage/JSCover
44 | lib-cov
45 |
46 | # Coverage directory used by tools like istanbul
47 | coverage
48 | *.lcov
49 |
50 | # nyc test coverage
51 | .nyc_output
52 |
53 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
54 | .grunt
55 |
56 | # Bower dependency directory (https://bower.io/)
57 | bower_components
58 |
59 | # node-waf configuration
60 | .lock-wscript
61 |
62 | # Compiled binary addons (https://nodejs.org/api/addons.html)
63 | build/Release
64 |
65 | # Dependency directories
66 | node_modules/
67 | jspm_packages/
68 |
69 | # Snowpack dependency directory (https://snowpack.dev/)
70 | web_modules/
71 |
72 | # TypeScript cache
73 | *.tsbuildinfo
74 |
75 | # Optional npm cache directory
76 | .npm
77 |
78 | # Optional eslint cache
79 | .eslintcache
80 |
81 | # Optional stylelint cache
82 | .stylelintcache
83 |
84 | # Microbundle cache
85 | .rpt2_cache/
86 | .rts2_cache_cjs/
87 | .rts2_cache_es/
88 | .rts2_cache_umd/
89 |
90 | # Optional REPL history
91 | .node_repl_history
92 |
93 | # Output of 'npm pack'
94 | *.tgz
95 |
96 | # Yarn Integrity file
97 | .yarn-integrity
98 |
99 | # dotenv environment variable files
100 | .env
101 | .env.development.local
102 | .env.test.local
103 | .env.production.local
104 | .env.local
105 |
106 | # parcel-bundler cache (https://parceljs.org/)
107 | .cache
108 | .parcel-cache
109 |
110 | # Next.js build output
111 | .next
112 | out
113 |
114 | # Nuxt.js build / generate output
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 | .cache/
120 | # Comment in the public line in if your project uses Gatsby and not Next.js
121 | # https://nextjs.org/blog/next-9-1#public-directory-support
122 | # public
123 |
124 | # vuepress build output
125 | .vuepress/dist
126 |
127 | # vuepress v2.x temp and cache directory
128 | .temp
129 | .cache
130 |
131 | # Docusaurus cache and generated files
132 | .docusaurus
133 |
134 | # Serverless directories
135 | .serverless/
136 |
137 | # FuseBox cache
138 | .fusebox/
139 |
140 | # DynamoDB Local files
141 | .dynamodb/
142 |
143 | # TernJS port file
144 | .tern-port
145 |
146 | # Stores VSCode versions used for testing VSCode extensions
147 | .vscode-test
148 |
149 | # yarn v2
150 | .yarn/cache
151 | .yarn/unplugged
152 | .yarn/build-state.yml
153 | .yarn/install-state.gz
154 | .pnp.*
155 |
156 |
157 | # python
158 | **/__pycache__/
159 | *.venv
160 |
--------------------------------------------------------------------------------
/pyServer/server/langchain/callbacks/console_callback_handler.py:
--------------------------------------------------------------------------------
1 | import json
2 | from yachalk import chalk
3 | from langchain.callbacks.base import BaseCallbackHandler
4 | from langchain.schema import LLMResult, AgentAction, AgentFinish
5 |
6 | from typing import List, Dict, Union, Any
7 |
8 |
9 | class ConsoleCallbackHandler(BaseCallbackHandler):
10 | def on_llm_start(
11 | self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
12 | ) -> Any:
13 | print(chalk.blue("========= LLM Start ========="))
14 | print(chalk.green.bold("Serialized: "), json.dumps(serialized, indent=2))
15 | print(chalk.green.bold("Prompts: "), json.dumps(prompts, indent=2))
16 | print(chalk.green.bold("Other Args: "), kwargs)
17 |
18 | # def on_llm_new_token(self, token: str, **kwargs: Any) -> Any:
19 | # print(token)
20 |
21 | def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
22 | print(chalk.blue("========= LLM End ========="))
23 | print(response)
24 |
25 | def on_llm_error(
26 | self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
27 | ) -> Any:
28 | print(chalk.red("========= LLM Error ========="))
29 | print(json.dumps(error, indent=2))
30 |
31 | def on_chain_start(
32 | self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
33 | ) -> Any:
34 | print(chalk.blue("========= Chain Start ========="))
35 | print(chalk.green.bold("Serialized: "), json.dumps(serialized, indent=2))
36 | print(chalk.green.bold("Inputs: "), json.dumps(inputs, indent=2))
37 | # print(chalk.green.bold("Other Args: "), json.dumps(kwargs, indent=2))
38 |
39 | def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> Any:
40 | print(chalk.blue("========= Chain End ========="))
41 | print(chalk.green.bold("Output: "), json.dumps(outputs, indent=2))
42 |
43 | def on_chain_error(
44 | self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
45 | ) -> Any:
46 | print(chalk.red("========= Chain Error ========="))
47 | print(json.dumps(error, indent=2))
48 |
49 | def on_tool_start(
50 | self, serialized: Dict[str, Any], input_str: str, **kwargs: Any
51 | ) -> Any:
52 | print(chalk.blue("========= Tool Start ========="))
53 | print(chalk.green.bold("Serialized: "), json.dumps(serialized, indent=2))
54 | print(chalk.green.bold("Input: "), input_str)
55 | print(chalk.green.bold("Other Args: "), json.dumps(kwargs, indent=2))
56 |
57 | def on_tool_end(self, output: str, **kwargs: Any) -> Any:
58 | print(chalk.blue("========= Tool End ========="))
59 | print(chalk.green.bold("output: "), output)
60 | print(chalk.green.bold("Other Args: "), json.dumps(kwargs, indent=2))
61 |
62 | def on_tool_error(
63 | self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
64 | ) -> Any:
65 | print(chalk.red("========= Tool Error ========="))
66 | print(json.dumps(error, indent=2))
67 |
68 | def on_text(self, text: str, **kwargs: Any) -> Any:
69 | print(chalk.blue("========= On Text ========="))
70 | print(chalk.green.bold("Other Args: "), json.dumps(kwargs, indent=2))
71 |
72 | def on_agent_action(self, action: AgentAction, **kwargs: Any) -> Any:
73 | print(chalk.blue("==== Agent Action ===="))
74 | print(f"{chalk.magenta(' Agent Action RAW:')} {action}")
75 | print(f"{chalk.green.bold(' Agent Tool:')} {action.tool}")
76 | print(
77 | f"{chalk.green.bold(' Agent Input:')} {json.dumps(action.tool_input, indent=2)}"
78 | )
79 | print(f"{chalk.green.bold(' Agent Log:')} {action.log}")
80 |
81 | def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any:
82 | print(chalk.blue("==== Agent Finish ===="))
83 | print(f"{chalk.magenta(' Agent Action RAW:')} {finish}")
84 | print(
85 | f"{chalk.green.bold(' Agent Tool:')} {json.dumps(finish.return_values, indent=2)}"
86 | )
87 | print(f"{chalk.green.bold(' Agent Input:')} {finish.log}")
88 |
--------------------------------------------------------------------------------
/frontend/src/components/menus/conversationSettingsMenu.tsx:
--------------------------------------------------------------------------------
1 | import { styled, useTheme } from '@mui/material/styles';
2 | import {
3 | IconButton,
4 | Grid,
5 | Typography,
6 | List,
7 | ListItem,
8 | ListItemText,
9 | ListItemButton
10 | } from '@mui/material';
11 |
12 | import ScatterPlotOutlinedIcon from '@mui/icons-material/ScatterPlotOutlined';
13 | import ThermostatIcon from '@mui/icons-material/Thermostat';
14 | import FindInPageIcon from '@mui/icons-material/FindInPage';
15 | import Settings from '@mui/icons-material/Settings';
16 | import ConversationSettingsDrawer from 'components/drawers/conversationSettingDrawer';
17 | import { useState } from 'react';
18 | import useModelService from 'services/apiService/useModelService';
19 | import {
20 | Menu,
21 | MenuContent,
22 | MenuItem,
23 | MenuItemLabel
24 | } from 'components/menus/menu.elements';
25 | import { useDispatch, useSelector } from 'react-redux';
26 | import { RootState } from 'store';
27 | import { setModel } from 'store/conversationSlice';
28 | import ConversationTemperature from 'components/slider/conversationTemperature';
29 | import DocumentQueryRadio from 'components/radio/documentQueryRadio';
30 |
31 | const ConversationSettingsMenu = () => {
32 | const dispatch = useDispatch();
33 |
34 | const documentRetrievalType = useSelector(
35 | (state: RootState) => state.conversation.documentRetrievalType
36 | );
37 | const temperature = useSelector(
38 | (state: RootState) => state.conversation.temperature
39 | );
40 | const model = useSelector((state: RootState) => state.conversation.model);
41 |
42 | const [selectedItem, setSelectedItem] = useState(null);
43 | const [models, setModels] = useState([]);
44 |
45 | const { getModels } = useModelService();
46 |
47 | const toggleSettings = (item: string) => {
48 | if (selectedItem === item) {
49 | setSelectedItem(null);
50 | return;
51 | }
52 | setSelectedItem(item);
53 | };
54 |
55 | const handleModels = async () => {
56 | if (selectedItem === 'model') return setSelectedItem(null);
57 |
58 | const models = await getModels();
59 | setModels(models);
60 | toggleSettings('model');
61 | };
62 |
63 | const handleModelSelection = (event: any) => {
64 | const model = event.currentTarget.textContent;
65 | dispatch(setModel(model));
66 | setSelectedItem(null);
67 | };
68 |
69 | return (
70 |
113 | );
114 | };
115 |
116 | export default ConversationSettingsMenu;
117 |
--------------------------------------------------------------------------------
/frontend/src/components/lists/collectionList.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import {
3 | Grid,
4 | IconButton,
5 | List,
6 | ListItemButton,
7 | ListItemSecondaryAction,
8 | ListItemText
9 | } from '@mui/material';
10 | import DeleteIcon from '@mui/icons-material/Delete';
11 | import { useTheme } from '@mui/system';
12 |
13 | import { SidebarItem } from 'components/containers/container.elements';
14 | import { toast } from 'react-toastify';
15 |
16 | import SingleInputDropDown from 'components/dropdowns/singleInputDropDown';
17 | import { useSelector, useDispatch } from 'react-redux';
18 | import { RootState } from 'store';
19 | import {
20 | setDocuments,
21 | setSelectedCollection,
22 | setCollections
23 | } from 'store/documentsSlice';
24 |
25 | import useCollectionService from 'services/apiService/useCollectionService';
26 | import useDocumentService from 'services/apiService/useDocumentService';
27 |
28 | const CollectionsList = () => {
29 | const theme = useTheme();
30 | const dispatch = useDispatch();
31 | const { addCollection, deleteCollection, getCollections } =
32 | useCollectionService();
33 |
34 | const { getDocuments } = useDocumentService();
35 |
36 | const selectedCollection = useSelector(
37 | (state: RootState) => state.documents.selectedCollection
38 | );
39 |
40 | const collections = useSelector(
41 | (state: RootState) => state.documents.collections
42 | );
43 |
44 | const handleAddCollection = (collectionName: string) => {
45 | addCollection(collectionName)
46 | .then((data) => {
47 | dispatch(setCollections(data));
48 | })
49 | .catch((error) => {
50 | toast.error('Failed to add collection: ' + error);
51 | });
52 | };
53 |
54 | const handleDeleteCollection = (index: number) => {
55 | const collection = collections[index];
56 | deleteCollection(collection.name)
57 | .then((data) => {
58 | dispatch(setCollections(data));
59 | })
60 | .catch((error) => {
61 | toast.error('Failed to delete documents: ' + error);
62 | });
63 | };
64 |
65 | useEffect(() => {
66 | (async () => {
67 | try {
68 | const data = await getCollections();
69 | dispatch(setCollections(data));
70 |
71 | if (data.length > 0) {
72 | dispatch(setSelectedCollection(data[0]));
73 | }
74 | } catch (error) {
75 | toast.error('Failed to get collections: ' + error);
76 | }
77 | })();
78 | }, []);
79 |
80 | useEffect(() => {
81 | if (selectedCollection?.name)
82 | getDocuments(selectedCollection.name)
83 | .then((data) => {
84 | dispatch(setDocuments(data));
85 | })
86 | .catch((error) => {
87 | toast.error('Failed to get documents: ' + error);
88 | });
89 | }, [selectedCollection]);
90 |
91 | return (
92 |
93 |
97 |
98 | {collections.map((collection, index) => (
99 | dispatch(setSelectedCollection(collection))}
102 | sx={{
103 | cursor: 'pointer',
104 | backgroundColor:
105 | selectedCollection?.name === collection.name
106 | ? theme.palette.action.selected
107 | : 'transparent'
108 | }}
109 | >
110 |
111 |
112 | handleDeleteCollection(index)}
116 | >
117 |
118 |
119 |
120 |
121 |
122 |
123 | ))}
124 |
125 |
126 | );
127 | };
128 |
129 | export default CollectionsList;
130 |
--------------------------------------------------------------------------------
/frontend/src/components/lists/documentList.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import {
3 | Grid,
4 | IconButton,
5 | List,
6 | ListItem as ListItemButton,
7 | ListItemSecondaryAction,
8 | ListItemText,
9 | useTheme
10 | } from '@mui/material';
11 | import DeleteIcon from '@mui/icons-material/Delete';
12 | import UploadModal from '../modals/uploadModal';
13 | import FileIcon from '../fileIcon';
14 | import { SidebarItem } from '../containers/container.elements';
15 | import { DocumentsObject } from 'types/documents';
16 | import AddItemHeader from '../headers/addItemHeader';
17 | import { toast } from 'react-toastify';
18 | import { useDispatch, useSelector } from 'react-redux';
19 | import { RootState } from 'store';
20 | import useDocumentService from 'services/apiService/useDocumentService';
21 | import {
22 | setDocuments,
23 | setSelectedDocument,
24 | setDocumentLightBoxIsOpen
25 | } from 'store/documentsSlice';
26 |
27 | const DocumentsList = () => {
28 | const theme = useTheme();
29 | const dispatch = useDispatch();
30 |
31 | const { deleteDocument } = useDocumentService();
32 |
33 | const selectedCollection = useSelector(
34 | (state: RootState) => state.documents.selectedCollection
35 | );
36 | const documents = useSelector(
37 | (state: RootState) => state.documents.documents
38 | );
39 |
40 | const [selectedDocumentListItem, setSelectedDocumentListItem] = useState('');
41 | const [open, setOpen] = useState(false);
42 |
43 | const handleClose = () => {
44 | setOpen(false);
45 | };
46 |
47 | const selectDocumentHandler = (documentName: string) => {
48 | dispatch(setSelectedDocument(documentName));
49 | dispatch(setDocumentLightBoxIsOpen(true));
50 | setSelectedDocumentListItem(documentName);
51 | };
52 |
53 | const documentsUploadHandler = (documents: DocumentsObject) => {
54 | if (selectedCollection && selectedCollection.name) {
55 | return dispatch(setDocuments(documents));
56 | }
57 |
58 | toast.error('Please select a collection');
59 | };
60 |
61 | const deleteDocumentHandler = async (document: string) => {
62 | if (!selectedCollection) {
63 | throw new Error('Please select a collection');
64 | }
65 |
66 | try {
67 | const data = await deleteDocument({
68 | collectionName: selectedCollection.name,
69 | fileName: document
70 | });
71 |
72 | if (data) dispatch(setDocuments(data));
73 | } catch (error) {
74 | console.error(error);
75 | }
76 | };
77 |
78 | return (
79 |
80 | setOpen(true)}
83 | />
84 |
91 | {documents &&
92 | Object.keys(documents).map((document: string, index: number) => (
93 | selectDocumentHandler(document)}
96 | sx={{
97 | cursor: 'pointer',
98 | backgroundColor:
99 | selectedDocumentListItem === document
100 | ? theme.palette.action.selected
101 | : 'transparent',
102 | overflow: 'hidden',
103 | whiteSpace: 'nowrap'
104 | }}
105 | >
106 |
107 |
108 |
109 |
113 |
114 |
115 | deleteDocumentHandler(document)}
119 | >
120 |
121 |
122 |
123 |
124 |
125 |
126 | ))}
127 |
128 |
129 |
135 |
136 | );
137 | };
138 |
139 | export default DocumentsList;
140 |
--------------------------------------------------------------------------------
/frontend/src/components/modals/uploadModal copy.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | Typography,
4 | Button,
5 | Paper,
6 | Grid,
7 | CircularProgress
8 | } from '@mui/material';
9 | import { DropzoneOptions, useDropzone } from 'react-dropzone';
10 | import { useState } from 'react';
11 | import { useTheme } from '@mui/system';
12 | import UploadList from '../lists/uploadList';
13 | import { toast } from 'react-toastify';
14 | import { DocumentsObject } from '../../types/documents';
15 | import { CollectionList } from '../../types/collection';
16 | import useDocumentService from 'services/apiService/useDocumentService';
17 |
18 | interface Props {
19 | open: boolean;
20 | onClose: () => void;
21 | onUploadDocuments: (document: DocumentsObject) => void;
22 | selectedCollection?: CollectionList;
23 | }
24 |
25 | const UploadModal = ({
26 | open,
27 | onClose,
28 | onUploadDocuments,
29 | selectedCollection
30 | }: Props) => {
31 | const theme = useTheme();
32 | const [isLoading, setIsLoading] = useState(false);
33 | const [files, setFiles] = useState([]);
34 | const [uploadProgress, setUploadProgress] = useState([]);
35 |
36 | const { addDocuments } = useDocumentService();
37 |
38 | const onDrop = (acceptedFiles: File[]) => {
39 | const newFiles = acceptedFiles.map((file) => file);
40 | setFiles([...files, ...newFiles]);
41 | setUploadProgress([
42 | ...uploadProgress,
43 | ...new Array(newFiles.length).fill(0)
44 | ]);
45 | };
46 |
47 | const { getRootProps, getInputProps, isDragActive } = useDropzone({
48 | onDrop
49 | } as DropzoneOptions);
50 |
51 | const handleUpload = async () => {
52 | setIsLoading(true);
53 |
54 | const formData = new FormData();
55 | files.forEach((file) => {
56 | formData.append('documents', file);
57 | });
58 |
59 | if (selectedCollection?.name) {
60 | formData.append('collectionName', selectedCollection.name);
61 | }
62 |
63 | try {
64 | setIsLoading(false);
65 |
66 | const { documents, errors } = await addDocuments(formData);
67 |
68 | onUploadDocuments(documents);
69 | setFiles([]);
70 | toast.success('Files uploaded successfully');
71 |
72 | if (errors && errors.length > 0) {
73 | toast.warning(
74 | `Failed to upload some files:${JSON.stringify(errors, null, 2)}`
75 | );
76 | }
77 |
78 | onClose();
79 | } catch (error) {
80 | toast.error(`Failed to upload files:${error}`);
81 | setIsLoading(false);
82 | }
83 | };
84 |
85 | return (
86 |
97 |
106 | Upload Files
107 |
108 |
123 |
124 | {isDragActive ? (
125 | Drop the files here ...
126 | ) : (
127 |
128 | Drag and drop your files here
129 |
130 | or
131 |
132 | click to select files
133 |
134 | )}
135 |
136 |
137 |
150 |
151 |
152 | );
153 | };
154 |
155 | export default UploadModal;
156 |
--------------------------------------------------------------------------------
/frontend/src/components/modals/uploadModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | Typography,
4 | Button,
5 | Paper,
6 | Grid,
7 | CircularProgress
8 | } from '@mui/material';
9 | import { DropzoneOptions, useDropzone } from 'react-dropzone';
10 | import { useState } from 'react';
11 | import { useTheme } from '@mui/system';
12 | import UploadList from '../lists/uploadList';
13 | import { toast } from 'react-toastify';
14 | import { DocumentsObject } from '../../types/documents';
15 | import { CollectionList } from '../../types/collection';
16 | import useDocumentService from 'services/apiService/useDocumentService';
17 |
18 | interface Props {
19 | open: boolean;
20 | onClose: () => void;
21 | onUploadDocuments: (document: DocumentsObject) => void;
22 | selectedCollection?: CollectionList;
23 | }
24 |
25 | const UploadModal = ({
26 | open,
27 | onClose,
28 | onUploadDocuments,
29 | selectedCollection
30 | }: Props) => {
31 | const theme = useTheme();
32 | const [isLoading, setIsLoading] = useState(false);
33 | const [files, setFiles] = useState([]);
34 | const [uploadProgress, setUploadProgress] = useState([]);
35 |
36 | const { addDocuments } = useDocumentService();
37 |
38 | const onDrop = (acceptedFiles: File[]) => {
39 | const newFiles = acceptedFiles.map((file) => file);
40 | setFiles([...files, ...newFiles]);
41 | setUploadProgress([
42 | ...uploadProgress,
43 | ...new Array(newFiles.length).fill(0)
44 | ]);
45 | };
46 |
47 | const { getRootProps, getInputProps, isDragActive } = useDropzone({
48 | onDrop
49 | } as DropzoneOptions);
50 |
51 | const handleUpload = async () => {
52 | setIsLoading(true);
53 |
54 | const formData = new FormData();
55 | files.forEach((file) => {
56 | formData.append('documents', file);
57 | });
58 |
59 | if (!selectedCollection?.name) return toast.error('No colection selected');
60 |
61 | formData.append('collectionName', selectedCollection.name);
62 |
63 | try {
64 | setIsLoading(false);
65 |
66 | const { documents, errors } = await addDocuments(formData);
67 |
68 | onUploadDocuments(documents);
69 | setFiles([]);
70 | setUploadProgress([]);
71 | toast.success('Files uploaded successfully');
72 |
73 | if (errors && errors.length > 0) {
74 | toast.warning(
75 | `Failed to upload some files:${JSON.stringify(errors, null, 2)}`
76 | );
77 | }
78 |
79 | onClose();
80 | } catch (error) {
81 | toast.error(`Failed to upload files:${error}`);
82 | setIsLoading(false);
83 | }
84 | };
85 |
86 | return (
87 |
98 |
107 | Upload Files
108 |
109 |
124 |
125 | {isDragActive ? (
126 | Drop the files here ...
127 | ) : (
128 |
129 | Drag and drop your files here
130 |
131 | or
132 |
133 | click to select files
134 |
135 | )}
136 |
137 |
138 |
151 |
152 |
153 | );
154 | };
155 |
156 | export default UploadModal;
157 |
--------------------------------------------------------------------------------
/frontend/src/providers/authProvider.tsx:
--------------------------------------------------------------------------------
1 | // AuthContext.tsx
2 |
3 | import React, {
4 | createContext,
5 | useState,
6 | useEffect,
7 | ReactNode,
8 | useMemo,
9 | useCallback
10 | } from 'react';
11 |
12 | import {
13 | getAuth,
14 | onAuthStateChanged,
15 | GoogleAuthProvider,
16 | OAuthProvider,
17 | User,
18 | signInWithPopup
19 | } from 'firebase/auth';
20 | import { Box, Button, Grid, Typography, useTheme } from '@mui/material';
21 | import { Google as GoogleIcon } from '@mui/icons-material';
22 |
23 | import { SvgIcon } from '@mui/material';
24 |
25 | import logo from '../assets/logo192.png';
26 |
27 | const MicrosoftIcon = () => {
28 | return (
29 |
30 |
34 |
35 | );
36 | };
37 |
38 | interface AuthContextInterface {
39 | user: User | null;
40 | signInWithGoogle: () => void;
41 | }
42 |
43 | export const AuthContext = createContext(null);
44 |
45 | interface AuthProviderProps {
46 | children: ReactNode;
47 | }
48 |
49 | export const AuthProvider: React.FC = ({ children }) => {
50 | const theme = useTheme();
51 | const [user, setUser] = useState(null);
52 | const auth = getAuth();
53 |
54 | const signInWithGoogle = useCallback(async () => {
55 | const googleProvider = new GoogleAuthProvider();
56 |
57 | await signInWithPopup(auth, googleProvider)
58 | .then((result) => {
59 | setUser(result.user);
60 | })
61 | .catch((error) => {
62 | console.error(error);
63 | });
64 | }, [auth, setUser]);
65 |
66 | const signInWithMicrosoft = useCallback(async () => {
67 | const microsoftProvider = new OAuthProvider('microsoft.com');
68 |
69 | await signInWithPopup(auth, microsoftProvider)
70 | .then((result) => {
71 | setUser(result.user);
72 | })
73 | .catch((error) => {
74 | console.error(error);
75 | });
76 | }, [auth, setUser]);
77 |
78 | const providerValue = useMemo(
79 | () => ({ user, signInWithGoogle, signInWithMicrosoft }),
80 | [user, signInWithGoogle, signInWithMicrosoft]
81 | );
82 |
83 | useEffect(() => {
84 | const unsubscribe = onAuthStateChanged(auth, setUser);
85 | return unsubscribe;
86 | }, [auth]);
87 |
88 | return (
89 |
90 | {user ? (
91 | children
92 | ) : (
93 |
102 |
117 |
118 |
127 |
128 | Savant
129 |
130 |
131 | Please sign in:
132 | }
137 | onClick={signInWithGoogle}
138 | >
139 | Sign In with Google
140 |
141 | }
146 | onClick={signInWithMicrosoft}
147 | >
148 | Sign In with Microsoft
149 |
150 |
151 |
152 | )}
153 |
154 | );
155 | };
156 |
--------------------------------------------------------------------------------
/frontend/src/components/dropdowns/singleInputDropDown.tsx:
--------------------------------------------------------------------------------
1 | // src/components/SingleInputDropDown.tsx
2 | import React, { useState } from 'react';
3 | import {
4 | Grid,
5 | Typography,
6 | IconButton,
7 | TextField,
8 | FormHelperText,
9 | FormControl,
10 | CircularProgress,
11 | useTheme
12 | } from '@mui/material';
13 | import AddIcon from '@mui/icons-material/Add';
14 | import CancelIcon from '@mui/icons-material/Cancel';
15 | import CheckIcon from '@mui/icons-material/Check';
16 |
17 | interface SingleInputDropDownProps {
18 | title: string;
19 | handleAddCollection: (collectionName: string) => void;
20 | }
21 |
22 | const SingleInputDropDown: React.FC = ({
23 | title,
24 | handleAddCollection
25 | }) => {
26 | const theme = useTheme();
27 | const [isOpen, setIsOpen] = useState(false);
28 | const [isInvalid, setIsInvalid] = useState(false);
29 | const [inputValue, setInputValue] = useState('');
30 | const [loading, setLoading] = useState(false);
31 |
32 | const openSlider = () => {
33 | setIsOpen(!isOpen);
34 | };
35 |
36 | const closeSlider = () => {
37 | setInputValue('');
38 | setIsOpen(false);
39 | setIsInvalid(false);
40 | };
41 |
42 | const handleSave = () => {
43 | setLoading(true);
44 | handleAddCollection(inputValue);
45 | setLoading(false);
46 | closeSlider();
47 | };
48 |
49 | function handleInputChange(event: { target: { value: any } }) {
50 | const { value } = event.target;
51 | const isValid =
52 | /^[a-zA-Z0-9](?!.*\\.\\.)[a-zA-Z0-9_-]{1,61}[a-zA-Z0-9]$/.test(value);
53 |
54 | setInputValue(value);
55 | setIsInvalid(!isValid);
56 | }
57 |
58 | return (
59 |
60 |
72 |
73 | {title}
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
92 |
93 |
94 |
107 | {isInvalid && (
108 |
109 | Please enter a valid collection name that meets the requirements{' '}
110 |
111 | )}
112 |
113 |
114 |
115 |
116 |
122 |
123 |
124 |
125 |
126 |
132 | {loading ? (
133 |
134 | ) : (
135 |
136 | )}
137 |
138 |
139 |
140 |
141 |
142 | );
143 | };
144 |
145 | export default SingleInputDropDown;
146 |
--------------------------------------------------------------------------------
/frontend/src/components/documentLightBox/documentLightBox.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogContent,
4 | Grid,
5 | List,
6 | ListItem,
7 | Box,
8 | Typography,
9 | IconButton,
10 | ListItemButton,
11 | useTheme,
12 | Tab,
13 | Tabs
14 | } from '@mui/material';
15 | import {
16 | ModalContentContainer,
17 | ContentContainer
18 | } from '../containers/container.elements';
19 | import { setDocumentLightBoxIsOpen } from '../../store/documentsSlice';
20 | import { useState, useEffect, SetStateAction } from 'react';
21 | import { useSelector, useDispatch } from 'react-redux';
22 | import { RootState } from '../../store';
23 | import { Document } from '../../types/documents';
24 | import Markdown from '../markdown';
25 | import CloseIcon from '@mui/icons-material/Close';
26 |
27 | const DocumentLightBox = () => {
28 | const theme = useTheme();
29 | const dispatch = useDispatch();
30 | const selectedDocument = useSelector(
31 | (state: RootState) => state.documents.selectedDocument
32 | );
33 | const documents = useSelector(
34 | (state: RootState) => state.documents.documents
35 | );
36 | const documentLightBoxIsOpen = useSelector(
37 | (state: RootState) => state.documents.documentLightBoxIsOpen
38 | );
39 |
40 | const [selectedDocuments, setSelectedDocuments] = useState([]);
41 | const [selectedDocumentPiece, setSelectedDocumentPiece] =
42 | useState(null);
43 | const [selectedTab, setSelectedTab] = useState(0);
44 |
45 | const handleTabChange = (
46 | event: React.SyntheticEvent,
47 | newValue: SetStateAction
48 | ) => {
49 | setSelectedTab(newValue);
50 | };
51 |
52 | useEffect(() => {
53 | if (selectedDocument) {
54 | if (documents && documents[selectedDocument]) {
55 | setSelectedDocuments(documents[selectedDocument]);
56 | setSelectedDocumentPiece(documents[selectedDocument][0]); // Select the first document by default
57 | }
58 | }
59 | }, [selectedDocument, documents]);
60 |
61 | const handleClose = () => {
62 | setSelectedDocumentPiece(null);
63 | dispatch(setDocumentLightBoxIsOpen(false));
64 | };
65 |
66 | return (
67 |
155 | );
156 | };
157 |
158 | export default DocumentLightBox;
159 |
--------------------------------------------------------------------------------
/pyServer/install-pyenv-win.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Installs pyenv-win
4 |
5 | .DESCRIPTION
6 | Installs pyenv-win to $HOME\.pyenv
7 | If pyenv-win is already installed, try to update to the latest version.
8 |
9 | .PARAMETER Uninstall
10 | Uninstall pyenv-win. Note that this uninstalls any Python versions that were installed with pyenv-win.
11 |
12 | .INPUTS
13 | None.
14 |
15 | .OUTPUTS
16 | None.
17 |
18 | .EXAMPLE
19 | PS> install-pyenv-win.ps1
20 |
21 | .LINK
22 | Online version: https://pyenv-win.github.io/pyenv-win/
23 | #>
24 |
25 | param (
26 | [Switch] $Uninstall = $False
27 | )
28 |
29 | $PyEnvDir = "${env:USERPROFILE}\.pyenv"
30 | $PyEnvWinDir = "${PyEnvDir}\pyenv-win"
31 | $BinPath = "${PyEnvWinDir}\bin"
32 | $ShimsPath = "${PyEnvWinDir}\shims"
33 |
34 | Function Remove-PyEnvVars() {
35 | $PathParts = [System.Environment]::GetEnvironmentVariable('PATH', "User") -Split ";"
36 | $NewPathParts = $PathParts.Where{ $_ -ne $BinPath }.Where{ $_ -ne $ShimsPath }
37 | $NewPath = $NewPathParts -Join ";"
38 | [System.Environment]::SetEnvironmentVariable('PATH', $NewPath, "User")
39 |
40 | [System.Environment]::SetEnvironmentVariable('PYENV', $null, "User")
41 | [System.Environment]::SetEnvironmentVariable('PYENV_ROOT', $null, "User")
42 | [System.Environment]::SetEnvironmentVariable('PYENV_HOME', $null, "User")
43 | }
44 |
45 | Function Remove-PyEnv() {
46 | Write-Host "Removing $PyEnvDir..."
47 | If (Test-Path $PyEnvDir) {
48 | Remove-Item -Path $PyEnvDir -Recurse
49 | }
50 | Write-Host "Removing environment variables..."
51 | Remove-PyEnvVars
52 | }
53 |
54 | Function Get-CurrentVersion() {
55 | $VersionFilePath = "$PyEnvDir\.version"
56 | If (Test-Path $VersionFilePath) {
57 | $CurrentVersion = Get-Content $VersionFilePath
58 | }
59 | Else {
60 | $CurrentVersion = ""
61 | }
62 |
63 | Return $CurrentVersion
64 | }
65 |
66 | Function Get-LatestVersion() {
67 | $LatestVersionFilePath = "$PyEnvDir\latest.version"
68 | (New-Object System.Net.WebClient).DownloadFile("https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/.version", $LatestVersionFilePath)
69 | $LatestVersion = Get-Content $LatestVersionFilePath
70 |
71 | Remove-Item -Path $LatestVersionFilePath
72 |
73 | Return $LatestVersion
74 | }
75 |
76 | Function Main() {
77 | If ($Uninstall) {
78 | Remove-PyEnv
79 | If ($? -eq $True) {
80 | Write-Host "pyenv-win successfully uninstalled."
81 | }
82 | Else {
83 | Write-Host "Uninstallation failed."
84 | }
85 | exit
86 | }
87 |
88 | $BackupDir = "${env:Temp}/pyenv-win-backup"
89 |
90 | $CurrentVersion = Get-CurrentVersion
91 | If ($CurrentVersion) {
92 | Write-Host "pyenv-win $CurrentVersion installed."
93 | $LatestVersion = Get-LatestVersion
94 | If ($CurrentVersion -eq $LatestVersion) {
95 | Write-Host "No updates available."
96 | exit
97 | }
98 | Else {
99 | Write-Host "New version available: $LatestVersion. Updating..."
100 |
101 | Write-Host "Backing up existing Python installations..."
102 | $FoldersToBackup = "install_cache", "versions", "shims"
103 | ForEach ($Dir in $FoldersToBackup) {
104 | If (-not (Test-Path $BackupDir)) {
105 | New-Item -ItemType Directory -Path $BackupDir
106 | }
107 | Move-Item -Path "${PyEnvWinDir}/${Dir}" -Destination $BackupDir
108 | }
109 |
110 | Write-Host "Removing $PyEnvDir..."
111 | Remove-Item -Path $PyEnvDir -Recurse
112 | }
113 | }
114 |
115 | New-Item -Path $PyEnvDir -ItemType Directory
116 |
117 | $DownloadPath = "$PyEnvDir\pyenv-win.zip"
118 |
119 | (New-Object System.Net.WebClient).DownloadFile("https://github.com/pyenv-win/pyenv-win/archive/master.zip", $DownloadPath)
120 | Microsoft.PowerShell.Archive\Expand-Archive -Path $DownloadPath -DestinationPath $PyEnvDir
121 | Move-Item -Path "$PyEnvDir\pyenv-win-master\*" -Destination "$PyEnvDir"
122 | Remove-Item -Path "$PyEnvDir\pyenv-win-master" -Recurse
123 | Remove-Item -Path $DownloadPath
124 |
125 | # Update env vars
126 | [System.Environment]::SetEnvironmentVariable('PYENV', "${PyEnvWinDir}\", "User")
127 | [System.Environment]::SetEnvironmentVariable('PYENV_ROOT', "${PyEnvWinDir}\", "User")
128 | [System.Environment]::SetEnvironmentVariable('PYENV_HOME', "${PyEnvWinDir}\", "User")
129 |
130 | $PathParts = [System.Environment]::GetEnvironmentVariable('PATH', "User") -Split ";"
131 |
132 | # Remove existing paths, so we don't add duplicates
133 | $NewPathParts = $PathParts.Where{ $_ -ne $BinPath }.Where{ $_ -ne $ShimsPath }
134 | $NewPathParts = ($BinPath, $ShimsPath) + $NewPathParts
135 | $NewPath = $NewPathParts -Join ";"
136 | [System.Environment]::SetEnvironmentVariable('PATH', $NewPath, "User")
137 |
138 | If (Test-Path $BackupDir) {
139 | Write-Host "Restoring Python installations..."
140 | Move-Item -Path "$BackupDir/*" -Destination $PyEnvWinDir
141 | }
142 |
143 | If ($? -eq $True) {
144 | Write-Host "pyenv-win is successfully installed. You may need to close and reopen your terminal before using it."
145 | }
146 | Else {
147 | Write-Host "pyenv-win was not installed successfully. If this issue persists, please open a ticket: https://github.com/pyenv-win/pyenv-win/issues."
148 | }
149 | }
150 |
151 | Main
152 |
--------------------------------------------------------------------------------
/pyServer/server/services/vector_store.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dotenv import load_dotenv
3 |
4 | import chromadb
5 | from chromadb.config import Settings
6 | from chromadb.api.models.Collection import Collection
7 | from chromadb.api.types import GetResult
8 | from langchain import PromptTemplate
9 |
10 | from langchain.chat_models import ChatOpenAI
11 |
12 | from langchain.embeddings.openai import OpenAIEmbeddings
13 | from langchain.vectorstores import Chroma
14 |
15 | from langchain.chains import ConversationalRetrievalChain, LLMChain
16 | from langchain.chains.summarize import load_summarize_chain
17 | from langchain.chains.conversational_retrieval.prompts import CONDENSE_QUESTION_PROMPT
18 |
19 | from server.services.loaders import LoaderResult
20 |
21 | from server.langchain.callbacks.streaming_callback_handler import (
22 | StreamingCallbackHandler,
23 | )
24 | from server.langchain.callbacks.console_callback_handler import (
25 | ConsoleCallbackHandler,
26 | )
27 |
28 |
29 | from server.services.loaders import loader
30 | from server.utils.parse import process_documents_into_objects
31 |
32 | from typing import List, Dict, Optional
33 |
34 | load_dotenv()
35 |
36 |
37 | class VectorStore:
38 | store: Optional[Chroma] = None
39 | client = chromadb.Client(
40 | Settings(
41 | chroma_api_impl="rest",
42 | chroma_server_host="localhost",
43 | chroma_server_http_port="8000",
44 | )
45 | )
46 |
47 | chain: ConversationalRetrievalChain
48 | chat_history = []
49 |
50 | @classmethod
51 | def set_create_chroma_store(cls, name: str):
52 | VectorStore.store = Chroma(
53 | embedding_function=OpenAIEmbeddings(
54 | client="Test", openai_api_key=os.getenv("OPENAI_API_KEY")
55 | ),
56 | collection_name=name,
57 | client=VectorStore.client,
58 | )
59 |
60 | def list_collections(self):
61 | return self.client.list_collections()
62 |
63 | def delete_collection(self, name: str):
64 | self.client.delete_collection(name)
65 |
66 | def get_collection(self, name: str):
67 | return self.client.get_collection(name)
68 |
69 | def create_collection(self, name: str):
70 | result = self.client.create_collection(name=name, get_or_create=True)
71 | return result
72 |
73 | def get_documents(self, collection: Collection, query: Dict = {}):
74 | documents: GetResult = collection.get(where_document=query)
75 |
76 | return process_documents_into_objects(documents)
77 |
78 | def add_documents(
79 | self, collection_name: str, file_path: str, filename: str
80 | ) -> LoaderResult:
81 | results = loader(file_path, filename)
82 |
83 | print(f"Adding {results} to {results}")
84 |
85 | VectorStore.set_create_chroma_store(collection_name)
86 |
87 | if VectorStore.store and len(results.documents) > 0:
88 | VectorStore.store.add_documents(results.documents)
89 |
90 | return results
91 |
92 | def delete_documents(self, collection_name: str, filename: str):
93 | collection = self.get_collection(collection_name)
94 |
95 | print(f"Deleting {filename} from {collection_name}")
96 | print(f"Collection: {collection}")
97 |
98 | collection.delete(where={"filename": filename})
99 |
100 | documents: GetResult = collection.get()
101 |
102 | return process_documents_into_objects(documents)
103 |
104 | def clear_chat_history(self):
105 | self.chat_history = []
106 |
107 | def ask_question(
108 | self,
109 | question: str,
110 | model_name: str,
111 | system_prompt: str,
112 | query_type: str,
113 | temperature: int,
114 | collection_name: str,
115 | callback,
116 | ):
117 | openai_org_id = os.getenv("OPENAI_ORG_ID")
118 | openai_api_key = os.getenv("OPENAI_API_KEY")
119 | default_model = os.getenv("DEFAULT_OPENAI_MODEL") or "gpt-3.5-turbo"
120 |
121 | model_args = {
122 | "client": "Test",
123 | "openai_api_key": openai_api_key,
124 | "model": model_name if model_name else default_model,
125 | "callbacks": [StreamingCallbackHandler(), ConsoleCallbackHandler()],
126 | "streaming": True,
127 | "temperature": temperature,
128 | "verbose": True,
129 | }
130 |
131 | if openai_org_id:
132 | model_args["openai_organization"] = openai_org_id
133 |
134 | model = ChatOpenAI(**model_args)
135 |
136 | StreamingCallbackHandler.set_stream_callback(callback)
137 |
138 | if not VectorStore.store:
139 | VectorStore.set_create_chroma_store(collection_name)
140 |
141 | if VectorStore.store:
142 | if query_type == "refine":
143 | chain = ConversationalRetrievalChain(
144 | question_generator=LLMChain(
145 | llm=model, prompt=CONDENSE_QUESTION_PROMPT
146 | ),
147 | combine_docs_chain=load_summarize_chain(
148 | model,
149 | chain_type="refine",
150 | ),
151 | retriever=VectorStore.store.as_retriever(),
152 | )
153 |
154 | else:
155 | chain = ConversationalRetrievalChain.from_llm(
156 | llm=model,
157 | retriever=VectorStore.store.as_retriever(),
158 | verbose=True,
159 | )
160 |
161 | res = chain.run(
162 | {
163 | "question": question,
164 | "chat_history": self.chat_history,
165 | }
166 | )
167 | self.chat_history.append((question, res))
168 |
169 | else:
170 | raise ValueError(
171 | "Collection is not selected",
172 | )
173 |
--------------------------------------------------------------------------------
/server/src/services/vector-store.ts:
--------------------------------------------------------------------------------
1 | import { OpenAI } from 'langchain/llms/openai';
2 | import { RetrievalQAChain, loadQARefineChain } from 'langchain/chains';
3 | import { Chroma } from 'langchain/vectorstores/chroma';
4 | import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
5 | import { StreamingCallbackHandler } from '@/langchain/callbacks/streaming-callback-handler';
6 | import { ConsoleCallbackHandler } from '@/langchain/callbacks/console-callback-handler';
7 | import { loader } from '../loaders';
8 | import { PromptTemplate } from 'langchain/prompts';
9 | import { ChromaClient, Collection } from 'chromadb';
10 | import { processDocumentsIntoObjects } from '@/utils/parse';
11 | import { Document } from 'langchain/dist/document';
12 | import { Where, GetResponse, Metadata } from 'chromadb/dist/main/types';
13 |
14 | export interface DocumentsObjectInterface {
15 | metadata?: Metadata;
16 | embedding?: Record;
17 | document: string;
18 | id: string;
19 | }
20 |
21 | export interface DocumentsInterface {
22 | ids: string[];
23 | embeddings?: any[];
24 | documents: string[];
25 | metadatas?: any[];
26 | }
27 |
28 | export class VectorStore {
29 | private static store: Chroma | null;
30 |
31 | client: ChromaClient;
32 | model: OpenAI;
33 | chatHistory: string[];
34 |
35 | constructor() {
36 | this.client = new ChromaClient();
37 | this.model = new OpenAI(
38 | {
39 | openAIApiKey: process.env.OPENAI_API_KEY,
40 | modelName: process.env.DEFAULT_OPENAI_MODEL,
41 | streaming: true,
42 | verbose: true,
43 | temperature: 0.5
44 | }
45 | // { organization: process.env.OPENAI_ORG_ID }
46 | );
47 |
48 | this.chatHistory = [];
49 | }
50 |
51 | async setCreateChromaStore(name: string) {
52 | VectorStore.store = new Chroma(
53 | new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY }),
54 | {
55 | collectionName: name
56 | }
57 | );
58 | }
59 |
60 | async listCollections() {
61 | const collections = await this.client.listCollections();
62 | return collections;
63 | }
64 |
65 | async deleteCollection(name: string) {
66 | const result = await this.client.deleteCollection({ name });
67 | return result;
68 | }
69 |
70 | async getCollection(name: string) {
71 | return await this.client.getCollection({ name });
72 | }
73 |
74 | async createCollection(name: string) {
75 | return await this.client.createCollection({ name });
76 | }
77 |
78 | async getDocuments(
79 | collection: Collection,
80 | query?: object
81 | ): Promise> {
82 | const documents: GetResponse = await collection.get({
83 | where: query as Where
84 | });
85 |
86 | return processDocumentsIntoObjects(documents);
87 | }
88 |
89 | async addDocuments(file: Express.Multer.File) {
90 | const { documents, errors } = await loader(file);
91 | if (VectorStore.store) await VectorStore.store.addDocuments(documents);
92 |
93 | return { documents, errors };
94 | }
95 |
96 | async deleteDocuments(collectionName: string, filename: string) {
97 | const collection = await this.client.getCollection({
98 | name: collectionName
99 | });
100 |
101 | await collection.delete({ where: { filename } });
102 |
103 | const documents = await collection.get();
104 |
105 | return processDocumentsIntoObjects(documents);
106 | }
107 |
108 | clearChatHistory() {
109 | this.chatHistory.length = 0;
110 | }
111 |
112 | async askQuestion(
113 | question: string,
114 | systemPrompt: string,
115 | queryType: 'simple' | 'refine',
116 | temperature: number,
117 | callback: (token: string) => void
118 | ) {
119 | console.log(
120 | 'askQuestion',
121 | question,
122 | systemPrompt,
123 | queryType,
124 | temperature,
125 | callback
126 | );
127 | StreamingCallbackHandler.setStreamCallback(callback);
128 |
129 | const chatPrompt = PromptTemplate.fromTemplate(
130 | ` ${systemPrompt}
131 | You are an AI assistant. You will be asked questions about the given documents, you can only use the given documents for information. You can use your
132 | memory to help with context or analysis of the documents and to understand the information and question, but you cant make things up.
133 | Provide answers in a conversational manner.
134 | Dont answer with anything like "Based on the provided context" or "Based on Additional Context"
135 |
136 | Answer like a human would.
137 | If you don't know the answer, don't try to make up an answer
138 | Follow the user's instructions carefully.
139 | ALWAYS respond using markdown.
140 |
141 | Chat History for context:
142 | {chat_history}
143 |
144 | The Question to be answered: {question}`
145 | );
146 |
147 | const prompt = await chatPrompt.format({
148 | question,
149 | chat_history: [this.chatHistory]
150 | });
151 |
152 | if (VectorStore.store && queryType === 'refine') {
153 | const chain = new RetrievalQAChain({
154 | combineDocumentsChain: loadQARefineChain(this.model),
155 | retriever: VectorStore.store.asRetriever()
156 | });
157 |
158 | const res = await chain.call(
159 | {
160 | chainType: 'stuff',
161 | query: prompt,
162 | temperature
163 | },
164 | [new StreamingCallbackHandler(), new ConsoleCallbackHandler()]
165 | );
166 |
167 | this.chatHistory.push(res.output_text);
168 | } else {
169 | if (VectorStore.store) {
170 | const chain = RetrievalQAChain.fromLLM(
171 | this.model,
172 | VectorStore.store.asRetriever()
173 | );
174 |
175 | const res = await chain.call(
176 | {
177 | chainType: 'stuff',
178 | query: prompt,
179 | temperature
180 | },
181 | [new StreamingCallbackHandler(), new ConsoleCallbackHandler()]
182 | );
183 |
184 | this.chatHistory.push(res.text);
185 | }
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/frontend/src/components/queryInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { TextField, Grid, useTheme, Button } from '@mui/material';
3 | import RefreshIcon from '@mui/icons-material/Refresh';
4 | import StopIcon from '@mui/icons-material/Stop';
5 |
6 | import { Message } from '../types/message';
7 | import { toast } from 'react-toastify';
8 | import { useSelector } from 'react-redux';
9 | import { RootState } from 'store';
10 | import { useDispatch } from 'react-redux';
11 | import { setConversation as setConversationState } from 'store/conversationSlice';
12 |
13 | const QueryInput = () => {
14 | const theme = useTheme();
15 | const dispatch = useDispatch();
16 | let abortController: AbortController | undefined = undefined;
17 | const [reader, setReader] = useState<
18 | ReadableStreamDefaultReader | undefined
19 | >();
20 |
21 | const [isStreaming, setIsStreaming] = useState(false);
22 | const [conversation, setConversation] = useState([]);
23 | const model = useSelector((state: RootState) => state.conversation.model);
24 | const systemPrompt = useSelector(
25 | (state: RootState) => state.conversation.systemPrompt
26 | );
27 | const temperature = useSelector(
28 | (state: RootState) => state.conversation.temperature
29 | );
30 | const documentRetrievalType = useSelector(
31 | (state: RootState) => state.conversation.documentRetrievalType
32 | );
33 |
34 | const selectedCollection = useSelector(
35 | (state: RootState) => state.documents.selectedCollection
36 | );
37 |
38 | const [inputText, setInputText] = useState('');
39 |
40 | const {
41 | REACT_APP_CHAIN_END_TRIGGER_MESSAGE,
42 | REACT_APP_CHAT_END_TRIGGER_MESSAGE,
43 | REACT_APP_LLM_START_TRIGGER_MESSAGE
44 | } = process.env;
45 |
46 | const handleChange = (event: React.ChangeEvent) => {
47 | setInputText(event.target.value);
48 | };
49 |
50 | useEffect(() => {
51 | dispatch(setConversationState(conversation));
52 | }, [conversation]);
53 |
54 | const handleKeyDown = (event: React.KeyboardEvent) => {
55 | if (event.key === 'Enter' && !event.shiftKey) {
56 | event.preventDefault(); // prevent newline from being entered
57 | queryDocuments();
58 | }
59 | };
60 |
61 | const stopRequest = async () => {
62 | console.log('aborting request');
63 | abortController?.abort();
64 | await reader?.cancel();
65 | setReader(undefined);
66 | setIsStreaming(false);
67 | };
68 |
69 | const queryDocuments = async () => {
70 | abortController = new AbortController();
71 | let questionText = inputText;
72 | let updatedConversation = [...conversation];
73 | if (!inputText) {
74 | const lastUserMessage = updatedConversation
75 | .slice()
76 | .reverse()
77 | .find((message) => message.source === 'user');
78 | if (lastUserMessage) {
79 | questionText = lastUserMessage.content.join();
80 | updatedConversation = updatedConversation.slice(0, -2);
81 | }
82 | }
83 |
84 | setConversation(updatedConversation);
85 |
86 | setConversation((prev) => [
87 | ...prev,
88 | { source: 'user', content: [questionText] }
89 | ]);
90 | setConversation((prev) => [
91 | ...prev,
92 | { source: 'assistant', content: [''] }
93 | ]);
94 | setInputText('');
95 |
96 | try {
97 | const result = await fetch('http://localhost:4000/collections/question', {
98 | method: 'POST',
99 | signal: abortController.signal,
100 | headers: { 'Content-Type': 'application/json' },
101 | body: JSON.stringify({
102 | question: questionText,
103 | systemPrompt,
104 | queryType: documentRetrievalType,
105 | temperature,
106 | collectionName: selectedCollection.name,
107 | model
108 | })
109 | });
110 |
111 | if (!result.ok) {
112 | setIsStreaming(false);
113 | throw new Error(`HTTP error, status code: ${result.status}`);
114 | }
115 | // Read the response body as a stream
116 | setReader(result.body?.getReader());
117 | setIsStreaming(true);
118 | } catch (error) {
119 | setIsStreaming(false);
120 | toast.error('Failed query documents');
121 | return;
122 | }
123 | };
124 |
125 | // Function to process the stream
126 | const readStream = async () => {
127 | if (reader) {
128 | try {
129 | const { value } = await reader.read();
130 |
131 | const decodedChunk = new TextDecoder().decode(value);
132 |
133 | if (decodedChunk === REACT_APP_CHAT_END_TRIGGER_MESSAGE) {
134 | setIsStreaming(false);
135 | return;
136 | }
137 |
138 | if (
139 | REACT_APP_CHAIN_END_TRIGGER_MESSAGE &&
140 | REACT_APP_LLM_START_TRIGGER_MESSAGE
141 | )
142 | setConversation((prev) => {
143 | const commandFilteredOut = decodedChunk
144 | .split(REACT_APP_CHAIN_END_TRIGGER_MESSAGE)
145 | .join('')
146 | .split(REACT_APP_LLM_START_TRIGGER_MESSAGE)
147 | .join('');
148 |
149 | const lastElementIndex = prev.length - 1;
150 |
151 | const updatedAssistantMessage: Message = {
152 | source: 'assistant',
153 | content: [...prev[lastElementIndex].content]
154 | };
155 |
156 | if (decodedChunk === REACT_APP_CHAIN_END_TRIGGER_MESSAGE) {
157 | updatedAssistantMessage.content.push(' ');
158 | } else if (decodedChunk === REACT_APP_LLM_START_TRIGGER_MESSAGE) {
159 | const lastIndexInContentArray =
160 | updatedAssistantMessage.content.length - 1;
161 |
162 | updatedAssistantMessage.content[lastIndexInContentArray] +=
163 | '\n\n';
164 | } else {
165 | const lastIndexInContentArray =
166 | updatedAssistantMessage.content.length - 1;
167 |
168 | updatedAssistantMessage.content[lastIndexInContentArray] +=
169 | commandFilteredOut;
170 | }
171 |
172 | const newConversation = [...prev];
173 | newConversation[lastElementIndex] = updatedAssistantMessage;
174 | return newConversation;
175 | });
176 | } catch (error) {
177 | setIsStreaming(false);
178 | toast.error('Failed query documents');
179 | return;
180 | }
181 | }
182 | };
183 |
184 | useEffect(() => {
185 | let isMounted = true;
186 |
187 | const read = async () => {
188 | try {
189 | while (isStreaming && reader && isMounted) {
190 | // check isMounted before each iteration
191 | await readStream(); // await the readStream call
192 | }
193 | } catch (err) {
194 | console.error(err);
195 | }
196 | };
197 |
198 | read();
199 |
200 | return () => {
201 | isMounted = false; // set isMounted to false on unmount
202 | };
203 | }, [isStreaming, reader]);
204 |
205 | return (
206 |
220 | {(conversation.length > 0 || isStreaming) && (
221 |
222 |
239 |
240 | )}
241 |
247 |
267 |
268 |
269 | );
270 | };
271 |
272 | export default React.memo(QueryInput);
273 |
--------------------------------------------------------------------------------