├── pyServer ├── server │ ├── tests │ │ └── __init__.py │ ├── __pycache__ │ │ └── __init__.cpython-311.pyc │ ├── utils │ │ ├── __pycache__ │ │ │ └── parse.cpython-311.pyc │ │ └── parse.py │ ├── services │ │ ├── __pycache__ │ │ │ ├── loaders.cpython-311.pyc │ │ │ └── vector_store.cpython-311.pyc │ │ ├── request_service.py │ │ ├── loaders.py │ │ └── vector_store.py │ ├── routes │ │ ├── documents │ │ │ ├── __pycache__ │ │ │ │ ├── routes.cpython-311.pyc │ │ │ │ └── service.cpython-311.pyc │ │ │ ├── routes.py │ │ │ └── service.py │ │ ├── collections │ │ │ ├── __pycache__ │ │ │ │ ├── routes.cpython-311.pyc │ │ │ │ └── service.cpython-311.pyc │ │ │ ├── service.py │ │ │ └── routes.py │ │ └── models │ │ │ ├── routes.py │ │ │ └── service.py │ ├── langchain │ │ └── callbacks │ │ │ ├── __pycache__ │ │ │ ├── console_callback_handler.cpython-311.pyc │ │ │ └── streaming_callback_handler.cpython-311.pyc │ │ │ ├── streaming_callback_handler.py │ │ │ └── console_callback_handler.py │ └── __init__.py ├── main.py ├── .chroma │ └── index │ │ ├── index_2378901d-9cb0-4e1c-bfba-2582af5fe176.bin │ │ ├── id_to_uuid_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl │ │ ├── uuid_to_id_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl │ │ └── index_metadata_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl ├── .env.template ├── README.md ├── pyproject.toml └── install-pyenv-win.ps1 ├── server ├── src │ ├── controller │ │ └── documents.tsx │ ├── routes │ │ ├── collections │ │ │ ├── index.ts │ │ │ ├── routes.ts │ │ │ └── controller.ts │ │ └── documents │ │ │ ├── index.ts │ │ │ ├── routes.ts │ │ │ └── controller.ts │ ├── server.ts │ ├── langchain │ │ └── callbacks │ │ │ ├── streaming-callback-handler.ts │ │ │ └── console-callback-handler.ts │ ├── utils │ │ └── parse.ts │ ├── loaders.ts │ └── services │ │ └── vector-store.ts ├── .env.template ├── tsconfig.json ├── .eslintrc.json ├── package.json └── .gitignore ├── frontend ├── README.md ├── src │ ├── react-app-env.d.ts │ ├── components │ │ ├── markdown │ │ │ ├── index.ts │ │ │ └── markdown.tsx │ │ ├── documentLightBox │ │ │ ├── index.ts │ │ │ └── documentLightBox.tsx │ │ ├── conversationSideBar.tsx │ │ ├── documentSideBar.tsx │ │ ├── conversation.tsx │ │ ├── lists │ │ │ ├── conversationSidebarSettingsList.tsx │ │ │ ├── documentSidebarSettingsList.tsx │ │ │ ├── uploadList.tsx │ │ │ ├── conversationList.tsx │ │ │ ├── collectionList.tsx │ │ │ └── documentList.tsx │ │ ├── drawers │ │ │ └── conversationSettingDrawer.tsx │ │ ├── systemPrompt.tsx │ │ ├── headers │ │ │ └── addItemHeader.tsx │ │ ├── appBar.tsx │ │ ├── menus │ │ │ ├── menu.elements.tsx │ │ │ └── conversationSettingsMenu.tsx │ │ ├── radio │ │ │ └── documentQueryRadio.tsx │ │ ├── slider │ │ │ └── conversationTemperature.tsx │ │ ├── revisionTabs.tsx │ │ ├── containers │ │ │ └── container.elements.tsx │ │ ├── fileIcon.tsx │ │ ├── codeBlock.tsx │ │ ├── modals │ │ │ ├── uploadModal copy.tsx │ │ │ └── uploadModal.tsx │ │ ├── dropdowns │ │ │ └── singleInputDropDown.tsx │ │ └── queryInput.tsx │ ├── types │ │ ├── models.ts │ │ ├── collection.ts │ │ ├── message.ts │ │ └── documents.ts │ ├── store │ │ ├── index.ts │ │ ├── documentsSlice │ │ │ ├── index.ts │ │ │ ├── slice.ts │ │ │ ├── state.ts │ │ │ └── reducers.ts │ │ ├── conversationSlice │ │ │ ├── index.ts │ │ │ ├── slice.ts │ │ │ ├── state.ts │ │ │ └── reducers.ts │ │ └── store.ts │ ├── assets │ │ ├── logo192.png │ │ └── logo512.png │ ├── setupTests.ts │ ├── App.test.tsx │ ├── index.tsx │ ├── index.css │ ├── reportWebVitals.ts │ ├── services │ │ ├── apiService │ │ │ ├── useModelService.tsx │ │ │ ├── useCollectionService.tsx │ │ │ └── useDocumentService.tsx │ │ └── authService.ts │ ├── App.css │ ├── hooks │ │ ├── useCreateReducer.ts │ │ └── useFetch.ts │ ├── utils │ │ └── codeblock.ts │ ├── App.tsx │ └── providers │ │ └── authProvider.tsx ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── assets │ │ ├── logo192.png │ │ └── logo512.png │ ├── manifest.json │ └── index.html ├── .gitignore ├── .env.template ├── tsconfig.json ├── .eslintrc.json └── package.json ├── .vscode └── settings.json ├── LICENSE ├── README.md └── .gitIgnore /pyServer/server/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/src/controller/documents.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # savant fe 2 | 3 | Ingest any document type and query 4 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/components/markdown/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './markdown'; 2 | -------------------------------------------------------------------------------- /frontend/src/types/models.ts: -------------------------------------------------------------------------------- 1 | export type OpenAiModels = 'gpt-3.5-turbo' | 'gpt-4'; 2 | -------------------------------------------------------------------------------- /frontend/src/components/documentLightBox/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './documentLightBox'; 2 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './store'; 2 | export type { RootState } from './store'; 3 | -------------------------------------------------------------------------------- /frontend/src/assets/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/frontend/src/assets/logo192.png -------------------------------------------------------------------------------- /frontend/src/assets/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/frontend/src/assets/logo512.png -------------------------------------------------------------------------------- /frontend/src/types/collection.ts: -------------------------------------------------------------------------------- 1 | export interface CollectionList { 2 | name: string; 3 | metadata: any; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/store/documentsSlice/index.ts: -------------------------------------------------------------------------------- 1 | export * from './slice'; 2 | export type { DocumentState } from './state'; 3 | -------------------------------------------------------------------------------- /frontend/public/assets/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/frontend/public/assets/logo192.png -------------------------------------------------------------------------------- /frontend/public/assets/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/frontend/public/assets/logo512.png -------------------------------------------------------------------------------- /frontend/src/store/conversationSlice/index.ts: -------------------------------------------------------------------------------- 1 | export * from './slice'; 2 | export type { ConversationState } from './state'; 3 | -------------------------------------------------------------------------------- /frontend/src/types/message.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | source: 'assistant' | 'user'; 3 | content: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /pyServer/server/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/server/utils/__pycache__/parse.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/utils/__pycache__/parse.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/server/services/__pycache__/loaders.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/services/__pycache__/loaders.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/main.py: -------------------------------------------------------------------------------- 1 | from server import create_server # type: ignore 2 | 3 | server = create_server() 4 | 5 | if __name__ == "__main__": 6 | server.run(debug=True, port=4000) 7 | -------------------------------------------------------------------------------- /pyServer/server/services/__pycache__/vector_store.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/services/__pycache__/vector_store.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/server/routes/documents/__pycache__/routes.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/routes/documents/__pycache__/routes.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/server/routes/documents/__pycache__/service.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/routes/documents/__pycache__/service.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/.chroma/index/index_2378901d-9cb0-4e1c-bfba-2582af5fe176.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/.chroma/index/index_2378901d-9cb0-4e1c-bfba-2582af5fe176.bin -------------------------------------------------------------------------------- /pyServer/server/routes/collections/__pycache__/routes.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/routes/collections/__pycache__/routes.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/server/routes/collections/__pycache__/service.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/routes/collections/__pycache__/service.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/.chroma/index/id_to_uuid_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/.chroma/index/id_to_uuid_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl -------------------------------------------------------------------------------- /pyServer/.chroma/index/uuid_to_id_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/.chroma/index/uuid_to_id_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl -------------------------------------------------------------------------------- /pyServer/.chroma/index/index_metadata_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/.chroma/index/index_metadata_2378901d-9cb0-4e1c-bfba-2582af5fe176.pkl -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none", 6 | "python.analysis.typeCheckingMode": "basic" 7 | } 8 | -------------------------------------------------------------------------------- /pyServer/server/langchain/callbacks/__pycache__/console_callback_handler.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/langchain/callbacks/__pycache__/console_callback_handler.cpython-311.pyc -------------------------------------------------------------------------------- /pyServer/server/langchain/callbacks/__pycache__/streaming_callback_handler.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foolishsailor/savant/HEAD/pyServer/server/langchain/callbacks/__pycache__/streaming_callback_handler.cpython-311.pyc -------------------------------------------------------------------------------- /server/.env.template: -------------------------------------------------------------------------------- 1 | CHAIN_END_TRIGGER_MESSAGE=c0fb7f7030574dd7801ae6f2d53dfd51 # <== this is a bit of a hack but needs to be copied 2 | OPENAI_API_KEY= # your api key 3 | OPENAI_ORG_ID= # your org key or levae blank 4 | DEFAULT_OPENAI_MODEL=gpt-3.5-turbo # or your model of choice -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /server/src/routes/collections/index.ts: -------------------------------------------------------------------------------- 1 | import routes from './routes'; 2 | import controller from './controller'; 3 | import { Router } from 'express'; 4 | 5 | export interface DocumentRoutes { 6 | router: Router; 7 | } 8 | 9 | export default ({ router }: DocumentRoutes): Router => 10 | routes(router, controller()); 11 | -------------------------------------------------------------------------------- /server/src/routes/documents/index.ts: -------------------------------------------------------------------------------- 1 | import routes from './routes'; 2 | import controller from './controller'; 3 | import { Router } from 'express'; 4 | 5 | export interface DocumentRoutes { 6 | router: Router; 7 | } 8 | 9 | export default ({ router }: DocumentRoutes): Router => 10 | routes(router, controller()); 11 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/types/documents.ts: -------------------------------------------------------------------------------- 1 | interface Metadata { 2 | [key: string]: any; 3 | } 4 | 5 | export interface Document { 6 | embedding?: any; 7 | document: string; 8 | metadata?: Metadata; 9 | } 10 | 11 | export interface DocumentLoaderErrors { 12 | error: string; 13 | item: string; 14 | } 15 | 16 | export type DocumentsObject = Record; 17 | -------------------------------------------------------------------------------- /pyServer/.env.template: -------------------------------------------------------------------------------- 1 | CHAIN_END_TRIGGER_MESSAGE=c0fb7f7030574dd7801ae6f2d53dfd51 2 | CHAT_END_TRIGGER_MESSAGE=e22a47417bd74869b925b389e9136117 3 | LLM_START_TRIGGER_MESSAGE=61ef18ea4054434f9b53-61f9950f195f 4 | OPENAI_API_HOST=https://api.openai.com 5 | OPENAI_API_KEY= # your api key 6 | OPENAI_ORG_ID= # your org key or levae blank 7 | DEFAULT_OPENAI_MODEL=gpt-3.5-turbo # or your model of choice -------------------------------------------------------------------------------- /frontend/.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 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | reportWebVitals(); 17 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /pyServer/README.md: -------------------------------------------------------------------------------- 1 | COnfigure poetry to use the local repository 2 | 3 | ``` 4 | poetry config virtualenvs.in-project true 5 | ``` 6 | 7 | Create poetry virtual env 8 | 9 | ``` 10 | poetry shell 11 | ``` 12 | 13 | Install poetry 14 | 15 | ``` 16 | poetry install 17 | ``` 18 | 19 | You will now have a .venv in the pyServer directory 20 | 21 | Selecte the python executable in this directory as your interpreter 22 | 23 | ``` 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /pyServer/server/routes/models/routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, request 2 | import json 3 | 4 | from typing import List 5 | 6 | from .service import ModelService 7 | 8 | models = Blueprint("models", __name__) 9 | 10 | model_service = ModelService() 11 | 12 | 13 | @models.route("/models", methods=["GET"]) 14 | # `${OPENAI_API_HOST}/v1/models` 15 | def get_models_route(): 16 | return json.dumps(model_service.get_models()) 17 | -------------------------------------------------------------------------------- /frontend/src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | 3 | import conversationSlice from './conversationSlice/slice'; 4 | import documentsSlice from './documentsSlice/slice'; 5 | 6 | const store = configureStore({ 7 | reducer: { 8 | conversation: conversationSlice, 9 | documents: documentsSlice 10 | } 11 | }); 12 | 13 | export type RootState = ReturnType; 14 | 15 | export default store; 16 | -------------------------------------------------------------------------------- /server/src/routes/collections/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { Controller } from './controller'; 3 | 4 | export default (router: Router, controller: Controller) => { 5 | return router 6 | .get('/collections', controller.getCollection) 7 | .post('/collections', controller.createCollection) 8 | .delete('/collections/:name', controller.deleteCollection) 9 | .post('/collections/question', controller.question); 10 | }; 11 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "resolveJsonModule": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "baseUrl": "./", 12 | "paths": { 13 | "@/*": ["src/*"] 14 | } 15 | }, 16 | "include": ["src/**/*.ts", "src/services"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/conversationSideBar.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarContainer, SidebarItem } from './containers/container.elements'; 2 | import { ConversationSidebarSettingsList } from './lists/conversationSidebarSettingsList'; 3 | 4 | const ConversationSideBar = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default ConversationSideBar; 14 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/src/services/apiService/useModelService.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { useFetch } from 'hooks/useFetch'; 4 | 5 | const useModelService = () => { 6 | const fetchService = useFetch(); 7 | 8 | const getModels = useCallback( 9 | (signal?: AbortSignal) => { 10 | return fetchService.get(`/models`, { 11 | signal 12 | }); 13 | }, 14 | [fetchService] 15 | ); 16 | 17 | return { 18 | getModels 19 | }; 20 | }; 21 | 22 | export default useModelService; 23 | -------------------------------------------------------------------------------- /server/src/routes/documents/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { Controller } from './controller'; 3 | import multer from 'multer'; 4 | 5 | export default (router: Router, controller: Controller) => { 6 | const upload = multer(); 7 | 8 | return router 9 | .get('/documents', controller.getDocuments) 10 | .post('/documents', upload.array('documents'), controller.addDocuments) 11 | .post('/documents/delete', controller.deleteDocuments) 12 | .delete('/documents', controller.clearDocuments); 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/src/store/documentsSlice/slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | import { reducers } from './reducers'; 4 | import { initialState } from './state'; 5 | 6 | const slice = createSlice({ 7 | name: 'documents', 8 | initialState, 9 | reducers 10 | }); 11 | 12 | export const { 13 | setSelectedCollection, 14 | setDocuments, 15 | setCollections, 16 | setDocumentLightBoxIsOpen, 17 | setSelectedDocument 18 | } = slice.actions; 19 | 20 | const documentsSlice = slice.reducer; 21 | 22 | export default documentsSlice; 23 | -------------------------------------------------------------------------------- /pyServer/server/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from .routes.collections.routes import collections 3 | from .routes.documents.routes import documents 4 | from .routes.models.routes import models 5 | from flask_cors import CORS 6 | 7 | 8 | def create_server(): 9 | server = Flask(__name__) 10 | server.register_blueprint(collections) 11 | server.register_blueprint(documents) 12 | server.register_blueprint(models) 13 | 14 | CORS(server) 15 | 16 | return server 17 | 18 | 19 | if __name__ == "__main__": 20 | server = create_server 21 | -------------------------------------------------------------------------------- /frontend/src/components/documentSideBar.tsx: -------------------------------------------------------------------------------- 1 | import { DocumentSidebarSettingsList } from './lists/documentSidebarSettingsList'; 2 | import { SidebarContainer } from './containers/container.elements'; 3 | import DocumentsList from './lists/documentList'; 4 | import CollectionList from './lists/collectionList'; 5 | 6 | const DocumentSideBar = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default DocumentSideBar; 17 | -------------------------------------------------------------------------------- /frontend/src/store/conversationSlice/slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | import { reducers } from './reducers'; 4 | import { initialState } from './state'; 5 | 6 | const slice = createSlice({ 7 | name: 'conversation', 8 | initialState, 9 | reducers 10 | }); 11 | 12 | export const { 13 | setSystemPrompt, 14 | setConversation, 15 | addToConversation, 16 | setTemperature, 17 | setDocumentRetrievalType, 18 | setModel 19 | } = slice.actions; 20 | 21 | const conversationSlice = slice.reducer; 22 | 23 | export default conversationSlice; 24 | -------------------------------------------------------------------------------- /pyServer/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "savantpy" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["jc.durbin "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | flask = "^2.3.2" 11 | chromadb = "^0.3.23" 12 | 13 | yachalk = "^0.1.5" 14 | openai = "^0.27.7" 15 | langchain = "^0.0.177" 16 | flask-cors = "^3.0.10" 17 | tiktoken = "^0.4.0" 18 | pypdf = "^3.9.0" 19 | docx2txt = "^0.8" 20 | requests = "^2.31.0" 21 | 22 | 23 | [build-system] 24 | requires = ["poetry-core"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Savant", 3 | "name": "Savant", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/store/conversationSlice/state.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'types/message'; 2 | import { OpenAiModels } from 'types/models'; 3 | 4 | export interface ConversationState { 5 | systemPrompt: string; 6 | conversation: Message[]; 7 | temperature: number; 8 | documentRetrievalType: 'simple' | 'refine'; 9 | model: OpenAiModels; 10 | } 11 | 12 | export const initialState: ConversationState = { 13 | systemPrompt: '', 14 | conversation: [], 15 | temperature: 0.5, 16 | documentRetrievalType: 'simple', 17 | model: 18 | (process.env.REACT_APP_DEFAULT_MODEL as OpenAiModels) || 'gpt-3.5-turbo' 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/.env.template: -------------------------------------------------------------------------------- 1 | # choose a large UUID like string here. Must be the same as in the server env 2 | REACT_APP_CHAIN_END_TRIGGER_MESSAGE=c0fb7f7030574dd7801ae6f2d53dfd51 3 | REACT_APP_CHAT_END_TRIGGER_MESSAGE=e22a47417bd74869b925b389e9136117 4 | REACT_APP_LLM_START_TRIGGER_MESSAGE=61ef18ea4054434f9b53-61f9950f195f 5 | REACT_APP_API_BASE_URL=http://localhost:4000 6 | 7 | 8 | REACT_APP_FIREBASE_API_KEY= 9 | REACT_APP_FIREBASE_AUTH_DOMAIN= 10 | REACT_APP_FIREBASE_PROJECT_ID= 11 | REACT_APP_FIREBASE_STORAGE_BUCKET= 12 | REACT_APP_MESSAGING_SENDER_ID= 13 | REACT_APP_APP_ID= 14 | REACT_APP_MEASUREMENT_ID= 15 | REACT_APP_DEFAULT_MODEL=gpt-3.5-turbo -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es2019", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": false, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | "sourceMap": true, 17 | "isolatedModules": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/store/documentsSlice/state.ts: -------------------------------------------------------------------------------- 1 | import { DocumentsObject } from '../../types/documents'; 2 | import { CollectionList } from '../../types/collection'; 3 | 4 | export interface DocumentState { 5 | documentLightBoxIsOpen: boolean; 6 | selectedDocument: string | undefined; 7 | selectedCollection: CollectionList; 8 | collections: CollectionList[]; 9 | documents: DocumentsObject | undefined; 10 | } 11 | 12 | export const initialState: DocumentState = { 13 | documentLightBoxIsOpen: false, 14 | selectedDocument: undefined, 15 | selectedCollection: { 16 | name: '', 17 | metadata: {} 18 | }, 19 | collections: [], 20 | documents: undefined 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/components/conversation.tsx: -------------------------------------------------------------------------------- 1 | import { ConversationContainer } from './containers/container.elements'; 2 | import { QueryContainer } from './containers/container.elements'; 3 | import ConversationList from './lists/conversationList'; 4 | import ConversationSettingsMenu from './menus/conversationSettingsMenu'; 5 | import QueryInput from './queryInput'; 6 | 7 | const Conversation = () => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Conversation; 20 | -------------------------------------------------------------------------------- /frontend/src/services/authService.ts: -------------------------------------------------------------------------------- 1 | const { 2 | REACT_APP_FIREBASE_API_KEY, 3 | REACT_APP_FIREBASE_AUTH_DOMAIN, 4 | REACT_APP_FIREBASE_PROJECT_ID, 5 | REACT_APP_FIREBASE_STORAGE_BUCKET, 6 | REACT_APP_MESSAGING_SENDER_ID, 7 | REACT_APP_APP_ID, 8 | REACT_APP_MEASUREMENT_ID 9 | } = process.env; 10 | 11 | export const firebaseConfig = { 12 | apiKey: REACT_APP_FIREBASE_API_KEY, 13 | authDomain: REACT_APP_FIREBASE_AUTH_DOMAIN, 14 | projectId: REACT_APP_FIREBASE_PROJECT_ID, 15 | storageBucket: REACT_APP_FIREBASE_STORAGE_BUCKET, 16 | messagingSenderId: REACT_APP_MESSAGING_SENDER_ID, 17 | appId: REACT_APP_APP_ID, 18 | measurementId: REACT_APP_MEASUREMENT_ID 19 | }; 20 | 21 | // Initialize Firebase 22 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/components/lists/conversationSidebarSettingsList.tsx: -------------------------------------------------------------------------------- 1 | import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; 2 | 3 | import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; 4 | import { SidebarItem } from '../containers/container.elements'; 5 | 6 | export const ConversationSidebarSettingsList = () => { 7 | const onClearConversationClick = () => {}; 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "overrides": [], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "plugins": ["@typescript-eslint"], 14 | "ignorePatterns": ["tsconfig.json"], 15 | "rules": { 16 | "object-shorthand": "warn", 17 | "no-useless-rename": "warn", 18 | "prefer-destructuring": "warn", 19 | "@typescript-eslint/no-empty-function": "off", 20 | "@typescript-eslint/no-explicit-any": "off", 21 | "@typescript-eslint/no-non-null-assertion": "off", 22 | "linebreak-style": ["warn", "windows"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | import bodyParser from 'body-parser'; 4 | import cors from 'cors'; 5 | import { ChromaClient } from 'chromadb'; 6 | import * as dotenv from 'dotenv'; 7 | 8 | import documentRoutes from './routes/documents'; 9 | import collectionRoutes from './routes/collections'; 10 | 11 | dotenv.config(); 12 | 13 | (async () => { 14 | // const client = new ChromaClient(); 15 | // await client.reset(); 16 | const app = express(); 17 | const port = process.env.PORT || 4000; 18 | 19 | app 20 | .use(bodyParser.json()) 21 | .use(cors()) 22 | .use(documentRoutes({ router })) 23 | .use(collectionRoutes({ router })) 24 | 25 | .listen(port, () => { 26 | console.log(`Server running at http://localhost:${port}`); 27 | }); 28 | })(); 29 | -------------------------------------------------------------------------------- /frontend/src/components/drawers/conversationSettingDrawer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Grid, useTheme } from '@mui/material'; 3 | 4 | const ConversationSettingsDrawer = ({ 5 | isOpen, 6 | children 7 | }: { 8 | isOpen: boolean; 9 | children: ReactNode; 10 | }) => { 11 | const theme = useTheme(); 12 | 13 | return ( 14 | 27 | {children} 28 | 29 | ); 30 | }; 31 | 32 | export default ConversationSettingsDrawer; 33 | -------------------------------------------------------------------------------- /frontend/src/components/systemPrompt.tsx: -------------------------------------------------------------------------------- 1 | import { TextField } from '@mui/material'; 2 | 3 | import { useDispatch } from 'react-redux'; 4 | import { setSystemPrompt } from 'store/conversationSlice'; 5 | 6 | const SystemPrompt = () => { 7 | const dispatch = useDispatch(); 8 | 9 | const systemPromptHandler = (prompt: string) => { 10 | dispatch(setSystemPrompt(prompt)); 11 | }; 12 | 13 | return ( 14 | systemPromptHandler(event.target.value)} 18 | multiline 19 | rows={2} 20 | fullWidth 21 | InputProps={{ 22 | style: { 23 | color: '#EEE', 24 | height: '100%', 25 | alignItems: 'flex-start' 26 | } 27 | }} 28 | sx={{ label: { color: '#888' } }} 29 | /> 30 | ); 31 | }; 32 | 33 | export default SystemPrompt; 34 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "react-app" 10 | ], 11 | "overrides": [], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["prettier", "@typescript-eslint"], 18 | "ignorePatterns": ["tsconfig.json"], 19 | "rules": { 20 | "object-shorthand": "warn", 21 | "no-useless-rename": "warn", 22 | "prefer-destructuring": "warn", 23 | "@typescript-eslint/no-empty-function": "off", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | "@typescript-eslint/no-non-null-assertion": "off", 26 | "linebreak-style": ["warn", "windows"] 27 | }, 28 | "settings": { 29 | "import/resolver": { 30 | "typescript": {} 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/components/headers/addItemHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Typography, IconButton, useTheme } from '@mui/material'; 2 | 3 | import AddIcon from '@mui/icons-material/Add'; 4 | 5 | interface AddItemHeaderProps { 6 | title: string; 7 | handleAddCollection: () => void; 8 | } 9 | 10 | const AddItemHeader = ({ title, handleAddCollection }: AddItemHeaderProps) => { 11 | const theme = useTheme(); 12 | return ( 13 | 25 | 26 | {title} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default AddItemHeader; 38 | -------------------------------------------------------------------------------- /pyServer/server/services/request_service.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from typing import Dict, Optional, Union, Any, Tuple 3 | 4 | 5 | class RequestService: 6 | def __init__( 7 | self, 8 | url: str, 9 | method: str = "GET", 10 | headers: Optional[Dict[str, str]] = None, 11 | params: Optional[Dict[str, str]] = None, 12 | data: Optional[Dict[str, str]] = None, 13 | ) -> None: 14 | self.url = url 15 | self.method = method 16 | self.headers = headers if headers else {} 17 | self.params = params if params else {} 18 | self.data = data if data else {} 19 | 20 | def request(self) -> Any: 21 | response = requests.request( 22 | self.method, 23 | self.url, 24 | headers=self.headers, 25 | params=self.params, 26 | data=self.data, 27 | ) 28 | 29 | if response.status_code == 200: 30 | return response.json() 31 | else: 32 | return response.status_code, response.text 33 | -------------------------------------------------------------------------------- /frontend/src/components/appBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppBar, Grid, Toolbar, Typography } from '@mui/material'; 3 | import logo from '../assets/logo192.png'; 4 | import { useTheme } from '@mui/system'; 5 | 6 | const AppBarComponent: React.FC = () => { 7 | const theme = useTheme(); 8 | return ( 9 | 10 | 16 | 17 | Savant 26 | 27 | Savant 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default AppBarComponent; 36 | -------------------------------------------------------------------------------- /server/src/langchain/callbacks/streaming-callback-handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCallbackHandler } from 'langchain/callbacks'; 2 | import { 3 | ChainValues, 4 | AgentAction, 5 | AgentFinish, 6 | LLMResult 7 | } from 'langchain/schema'; 8 | 9 | class StreamingCallbackHandler extends BaseCallbackHandler { 10 | name = 'StreamingCallbackHandler'; 11 | private static streamCallback?: (token: string) => void; 12 | 13 | static setStreamCallback(callback: (token: string) => void) { 14 | StreamingCallbackHandler.streamCallback = callback; 15 | } 16 | 17 | async handleChainEnd(_output: ChainValues) { 18 | if ( 19 | StreamingCallbackHandler.streamCallback && 20 | process.env.CHAIN_END_TRIGGER_MESSAGE 21 | ) { 22 | StreamingCallbackHandler.streamCallback( 23 | process.env.CHAIN_END_TRIGGER_MESSAGE 24 | ); 25 | } 26 | } 27 | 28 | async handleLLMNewToken(token: string) { 29 | StreamingCallbackHandler.streamCallback && 30 | StreamingCallbackHandler.streamCallback(token); 31 | } 32 | } 33 | 34 | export { StreamingCallbackHandler }; 35 | -------------------------------------------------------------------------------- /frontend/src/components/lists/documentSidebarSettingsList.tsx: -------------------------------------------------------------------------------- 1 | import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; 2 | 3 | import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'; 4 | import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; 5 | import { SidebarItem } from '../containers/container.elements'; 6 | 7 | export const DocumentSidebarSettingsList = () => { 8 | const onClearDocumentsClick = () => {}; 9 | const onClearConversationClick = () => {}; 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/src/components/menus/menu.elements.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Button, Typography } from '@mui/material'; 2 | import { styled } from '@mui/material/styles'; 3 | 4 | export const Menu = styled(Grid)<{ 5 | size?: 'sm' | 'lg'; // Define size as an optional prop 6 | }>(({ theme, size }) => ({ 7 | height: size === 'sm' ? 40 : 80, 8 | position: 'relative', 9 | borderTop: `1px solid ${theme.palette.grey[900]}` 10 | })); 11 | 12 | export const MenuContent = styled(Grid)(({ theme }) => ({ 13 | display: 'flex', 14 | position: 'relative', 15 | zIndex: 2, 16 | backgroundColor: theme.palette.grey[800], 17 | height: '100%' 18 | })); 19 | 20 | export const MenuItem = styled(Button)(({ theme, size }) => ({ 21 | display: 'flex', 22 | alignItems: 'center', 23 | justifyContent: 'center', 24 | flexDirection: 'row', 25 | flexWrap: 'nowrap', 26 | padding: theme.spacing(0, 1), 27 | gap: theme.spacing(1), 28 | fontSize: 14, 29 | color: 'white' 30 | })); 31 | 32 | export const MenuItemLabel = styled(Typography)(({ theme }) => ({ 33 | fontSize: 'inherit', 34 | color: theme.palette.grey[400] 35 | })); 36 | -------------------------------------------------------------------------------- /frontend/src/components/radio/documentQueryRadio.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from 'react'; 2 | import { Grid, Radio, RadioGroup, FormControlLabel } from '@mui/material'; 3 | 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | import { setDocumentRetrievalType } from 'store/conversationSlice'; 6 | import { RootState } from 'store'; 7 | 8 | type RadioValue = 'simple' | 'refine'; 9 | 10 | const DocumentQueryRadio = () => { 11 | const dispatch = useDispatch(); 12 | 13 | const documentRetrievalType = useSelector( 14 | (state: RootState) => state.conversation.documentRetrievalType 15 | ); 16 | 17 | const handleRadioChange = (event: ChangeEvent) => { 18 | dispatch(setDocumentRetrievalType(event.target.value as RadioValue)); 19 | }; 20 | 21 | return ( 22 | 23 | } label="Simple" /> 24 | } label="Refine" /> 25 | 26 | ); 27 | }; 28 | 29 | export default DocumentQueryRadio; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 foolishsailor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/src/hooks/useCreateReducer.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useReducer } from 'react'; 2 | 3 | // Extracts property names from initial state of reducer to allow typesafe dispatch objects 4 | export type FieldNames = { 5 | [K in keyof T]: T[K] extends string ? K : K; 6 | }[keyof T]; 7 | 8 | // Returns the Action Type for the dispatch object to be used for typing in things like context 9 | export type ActionType = 10 | | { type: 'reset' } 11 | | { type?: 'change'; field: FieldNames; value: any }; 12 | 13 | // Returns a typed dispatch and state 14 | export const useCreateReducer = ({ initialState }: { initialState: T }) => { 15 | type Action = 16 | | { type: 'reset' } 17 | | { type?: 'change'; field: FieldNames; value: any }; 18 | 19 | const reducer = (state: T, action: Action) => { 20 | if (!action.type) return { ...state, [action.field]: action.value }; 21 | 22 | if (action.type === 'reset') return initialState; 23 | 24 | throw new Error(); 25 | }; 26 | 27 | const [state, dispatch] = useReducer(reducer, initialState); 28 | 29 | return useMemo(() => ({ state, dispatch }), [state, dispatch]); 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/src/utils/codeblock.ts: -------------------------------------------------------------------------------- 1 | interface languageMap { 2 | [key: string]: string | undefined; 3 | } 4 | 5 | export const programmingLanguages: languageMap = { 6 | javascript: '.js', 7 | python: '.py', 8 | java: '.java', 9 | c: '.c', 10 | cpp: '.cpp', 11 | 'c++': '.cpp', 12 | 'c#': '.cs', 13 | ruby: '.rb', 14 | php: '.php', 15 | swift: '.swift', 16 | 'objective-c': '.m', 17 | kotlin: '.kt', 18 | typescript: '.ts', 19 | go: '.go', 20 | perl: '.pl', 21 | rust: '.rs', 22 | scala: '.scala', 23 | haskell: '.hs', 24 | lua: '.lua', 25 | shell: '.sh', 26 | sql: '.sql', 27 | html: '.html', 28 | css: '.css' 29 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component 30 | }; 31 | 32 | export const generateRandomString = (length: number, lowercase = false) => { 33 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0 34 | let result = ''; 35 | for (let i = 0; i < length; i++) { 36 | result += chars.charAt(Math.floor(Math.random() * chars.length)); 37 | } 38 | return lowercase ? result.toLowerCase() : result; 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/src/store/documentsSlice/reducers.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction } from '@reduxjs/toolkit'; 2 | import { DocumentState } from './state'; 3 | import { DocumentsObject, Document } from '../../types/documents'; 4 | import { CollectionList } from '../../types/collection'; 5 | 6 | export const reducers = { 7 | setSelectedCollection: ( 8 | state: DocumentState, 9 | action: PayloadAction 10 | ) => { 11 | state.selectedCollection = action.payload; 12 | }, 13 | setDocuments: ( 14 | state: DocumentState, 15 | action: PayloadAction 16 | ) => { 17 | state.documents = action.payload; 18 | }, 19 | setCollections: ( 20 | state: DocumentState, 21 | action: PayloadAction 22 | ) => { 23 | state.collections = action.payload; 24 | }, 25 | setDocumentLightBoxIsOpen: ( 26 | state: DocumentState, 27 | action: PayloadAction 28 | ) => { 29 | state.documentLightBoxIsOpen = action.payload; 30 | }, 31 | setSelectedDocument: ( 32 | state: DocumentState, 33 | action: PayloadAction 34 | ) => { 35 | state.selectedDocument = action.payload; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /pyServer/server/routes/models/service.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, Any, Tuple, List 3 | from server.services.request_service import RequestService 4 | 5 | available_models = [ 6 | "gpt-4", 7 | # "gpt-4-0314", 8 | # "gpt-4-32k", 9 | # "gpt-4-32k-0314", 10 | "gpt-3.5-turbo", 11 | # "gpt-3.5-turbo-0301", 12 | ] 13 | 14 | 15 | def filter_models( 16 | all_models: List[Dict[str, Any]], available_models: List[str] 17 | ) -> List[str]: 18 | model_ids = [] 19 | for item in all_models: 20 | if item["id"] in available_models: 21 | model_ids.append(item["id"]) 22 | return model_ids 23 | 24 | 25 | class ModelService: 26 | def get_models(self) -> List[str]: 27 | open_ai_url = os.getenv("OPENAI_API_HOST") 28 | open_ai_key = os.getenv("OPENAI_API_KEY") 29 | 30 | headers: Dict[str, str] = { 31 | "Content-Type": "application/json", 32 | "Authorization": f"Bearer {open_ai_key}", 33 | } 34 | 35 | service = RequestService( 36 | f"{open_ai_url}/v1/models", method="GET", headers=headers 37 | ) 38 | 39 | all_models = service.request() 40 | 41 | return filter_models(all_models["data"], available_models) 42 | -------------------------------------------------------------------------------- /frontend/src/components/lists/uploadList.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Typography, LinearProgress } from '@mui/material'; 2 | 3 | import FileIcon from '../fileIcon'; 4 | 5 | interface UploadListProps { 6 | files: File[]; 7 | uploadProgress: number[]; 8 | } 9 | 10 | const UploadList = ({ files, uploadProgress }: UploadListProps) => { 11 | return ( 12 | 20 | {files.map((file, index) => ( 21 | 31 | 37 | 38 | 39 | {file.name} 40 | 41 | 42 | ))} 43 | 44 | ); 45 | }; 46 | 47 | export default UploadList; 48 | -------------------------------------------------------------------------------- /frontend/src/store/conversationSlice/reducers.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction } from '@reduxjs/toolkit'; 2 | import { ConversationState } from './state'; 3 | 4 | import { Message } from '../../types/message'; 5 | import { OpenAiModels } from 'types/models'; 6 | 7 | export const reducers = { 8 | setSystemPrompt: ( 9 | state: ConversationState, 10 | action: PayloadAction 11 | ) => { 12 | state.systemPrompt = action.payload; 13 | }, 14 | setConversation: ( 15 | state: ConversationState, 16 | action: PayloadAction 17 | ) => { 18 | state.conversation = action.payload; 19 | }, 20 | addToConversation: ( 21 | state: ConversationState, 22 | action: PayloadAction 23 | ) => { 24 | state.conversation = [...state.conversation, ...action.payload]; 25 | }, 26 | setTemperature: (state: ConversationState, action: PayloadAction) => { 27 | state.temperature = action.payload; 28 | }, 29 | setDocumentRetrievalType: ( 30 | state: ConversationState, 31 | action: PayloadAction<'simple' | 'refine'> 32 | ) => { 33 | state.documentRetrievalType = action.payload; 34 | }, 35 | setModel: (state: ConversationState, action: PayloadAction) => { 36 | state.model = action.payload; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/src/components/slider/conversationTemperature.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Slider } from '@mui/material'; 2 | 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { setTemperature } from 'store/conversationSlice'; 5 | import { RootState } from 'store'; 6 | 7 | const ConversationTemperature = () => { 8 | const dispatch = useDispatch(); 9 | const temperature = useSelector( 10 | (state: RootState) => state.conversation.temperature 11 | ); 12 | 13 | const handleSliderChange = (event: Event, newValue: number | number[]) => { 14 | dispatch(setTemperature(newValue as number)); 15 | }; 16 | 17 | return ( 18 | 27 | 41 | 42 | ); 43 | }; 44 | 45 | export default ConversationTemperature; 46 | -------------------------------------------------------------------------------- /pyServer/server/langchain/callbacks/streaming_callback_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from langchain.callbacks.base import BaseCallbackHandler 4 | from langchain.schema import LLMResult 5 | from typing import Dict, Any, List 6 | 7 | load_dotenv() 8 | 9 | 10 | class StreamingCallbackHandler(BaseCallbackHandler): 11 | _stream_callback = None 12 | 13 | @classmethod 14 | def set_stream_callback(cls, function): 15 | StreamingCallbackHandler._stream_callback = staticmethod(function) 16 | 17 | @property 18 | def stream_callback(self): 19 | return self._stream_callback 20 | 21 | @stream_callback.setter 22 | def stream_callback(self, function): 23 | StreamingCallbackHandler.set_stream_callback(function) 24 | 25 | def on_llm_new_token(self, token: str, **kwargs: Any) -> Any: 26 | if self.stream_callback: 27 | self.stream_callback(token) 28 | 29 | def on_llm_start( 30 | self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any 31 | ) -> Any: 32 | if self.stream_callback: 33 | self.stream_callback(os.getenv("LLM_START_TRIGGER_MESSAGE")) 34 | 35 | def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> Any: 36 | if self.stream_callback: 37 | self.stream_callback(os.getenv("CHAIN_END_TRIGGER_MESSAGE")) 38 | -------------------------------------------------------------------------------- /frontend/src/services/apiService/useCollectionService.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { useFetch } from 'hooks/useFetch'; 4 | import { CollectionList } from 'types/collection'; 5 | 6 | const useCollectionService = () => { 7 | const fetchService = useFetch(); 8 | 9 | const addCollection = useCallback( 10 | (collectionName: string, signal?: AbortSignal) => { 11 | return fetchService.post(`/collections`, { 12 | body: { collectionName }, 13 | headers: { 14 | 'Content-Type': 'application/json' 15 | }, 16 | signal 17 | }); 18 | }, 19 | [fetchService] 20 | ); 21 | 22 | const deleteCollection = useCallback( 23 | (collectionName: string, signal?: AbortSignal) => { 24 | return fetchService.delete( 25 | `/collections/${collectionName}`, 26 | { 27 | signal 28 | } 29 | ); 30 | }, 31 | [fetchService] 32 | ); 33 | 34 | const getCollections = useCallback( 35 | (collectionName?: string, signal?: AbortSignal) => { 36 | return fetchService.get( 37 | `/collections${collectionName ? '/' + collectionName : ''}`, 38 | { 39 | signal 40 | } 41 | ); 42 | }, 43 | [fetchService] 44 | ); 45 | 46 | return { 47 | getCollections, 48 | addCollection, 49 | deleteCollection 50 | }; 51 | }; 52 | 53 | export default useCollectionService; 54 | -------------------------------------------------------------------------------- /server/src/utils/parse.ts: -------------------------------------------------------------------------------- 1 | // import { 2 | // DocumentsInterface, 3 | // DocumentsObjectInterface 4 | // } from '@/services/vector-store'; 5 | 6 | // export const processDocumentsIntoObjects = ( 7 | // documents: DocumentsInterface 8 | // ): DocumentsObjectInterface[] => { 9 | // return documents.documents 10 | // .map((document, i) => { 11 | // return { 12 | // metadata: documents.metadatas ? documents.metadatas[i] : {}, 13 | // embedding: documents.embeddings ? documents.embeddings[i] : {}, 14 | // document, 15 | // id: documents.ids[i] 16 | // }; 17 | // }) 18 | // .sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true })); 19 | // }; 20 | 21 | import { 22 | DocumentsInterface, 23 | DocumentsObjectInterface 24 | } from '@/services/vector-store'; 25 | import { GetResponse } from 'chromadb/dist/main/types'; 26 | 27 | export const processDocumentsIntoObjects = ( 28 | documents: GetResponse 29 | ): Record => { 30 | const objects: Record = {}; 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 | 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 {children}
; 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 | ![savant-logo](./frontend/public/assets/logo512.png) 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 | 71 | 72 | 83 | 84 | 85 | {model} 86 | 87 | toggleSettings('temperature')}> 88 | 89 | {temperature} 90 | 91 | toggleSettings('retrievalType')}> 92 | 93 | {documentRetrievalType} 94 | 95 | 96 | 97 | 98 | 99 | {models.map((model, index) => ( 100 | 101 | 102 | 103 | ))} 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 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 | Savant 127 | 128 | Savant 129 | 130 | 131 | Please sign in: 132 | 141 | 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 | 73 | 74 | 75 | 76 | {selectedDocument} 77 | 78 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 101 | {selectedTab === 0 && ( 102 | 110 | 116 | 117 | )} 118 | {selectedTab === 1 && ( 119 | 127 | 134 | 135 | )} 136 | 137 | 138 | 139 | 140 | {selectedDocuments && 141 | selectedDocuments.map((document, idx) => ( 142 | setSelectedDocumentPiece(document)} 145 | selected={selectedDocumentPiece === document} 146 | > 147 | {idx} 148 | 149 | ))} 150 | 151 | 152 | 153 | 154 | 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 | --------------------------------------------------------------------------------