Merge branch 'arc53:main' into main

This commit is contained in:
Taylor Svec
2023-02-19 13:28:48 -05:00
committed by GitHub
38 changed files with 690 additions and 6379 deletions

View File

@@ -61,5 +61,7 @@ Copy .env_sample and create .env with your openai api token
## [How to use any other documentation](https://github.com/arc53/docsgpt/wiki/How-to-train-on-other-documentation) ## [How to use any other documentation](https://github.com/arc53/docsgpt/wiki/How-to-train-on-other-documentation)
## [How to host it locally (so all data will stay on-premises)](https://github.com/arc53/DocsGPT/wiki/How-to-use-different-LLM's#hosting-everything-locally)
Built with [🦜️🔗 LangChain](https://github.com/hwchase17/langchain) Built with [🦜️🔗 LangChain](https://github.com/hwchase17/langchain)

View File

@@ -1,54 +1,88 @@
import os import os
import pickle import json
import dotenv import dotenv
import datetime
from flask import Flask, request, render_template
# os.environ["LANGCHAIN_HANDLER"] = "langchain"
import faiss
from langchain import OpenAI, VectorDBQA
from langchain.chains.question_answering import load_qa_chain
from langchain.prompts import PromptTemplate
import requests import requests
from flask import Flask, request, render_template
from langchain import FAISS
from langchain import OpenAI, VectorDBQA, HuggingFaceHub, Cohere
from langchain.chains.question_answering import load_qa_chain
from langchain.embeddings import OpenAIEmbeddings, HuggingFaceHubEmbeddings, CohereEmbeddings, HuggingFaceInstructEmbeddings
from langchain.prompts import PromptTemplate
# os.environ["LANGCHAIN_HANDLER"] = "langchain"
if os.getenv("LLM_NAME") is not None:
llm_choice = os.getenv("LLM_NAME")
else:
llm_choice = "openai"
if os.getenv("EMBEDDINGS_NAME") is not None:
embeddings_choice = os.getenv("EMBEDDINGS_NAME")
else:
embeddings_choice = "openai_text-embedding-ada-002"
if llm_choice == "manifest":
from manifest import Manifest
from langchain.llms.manifest import ManifestWrapper
manifest = Manifest(
client_name="huggingface",
client_connection="http://127.0.0.1:5000"
)
# Redirect PosixPath to WindowsPath on Windows # Redirect PosixPath to WindowsPath on Windows
import platform import platform
if platform.system() == "Windows": if platform.system() == "Windows":
import pathlib import pathlib
temp = pathlib.PosixPath temp = pathlib.PosixPath
pathlib.PosixPath = pathlib.WindowsPath pathlib.PosixPath = pathlib.WindowsPath
# loading the .env file # loading the .env file
dotenv.load_dotenv() dotenv.load_dotenv()
with open("combine_prompt.txt", "r") as f: with open("combine_prompt.txt", "r") as f:
template = f.read() template = f.read()
# check if OPENAI_API_KEY is set with open("combine_prompt_hist.txt", "r") as f:
if os.getenv("OPENAI_API_KEY") is not None: template_hist = f.read()
api_key_set = True
if os.getenv("API_KEY") is not None:
api_key_set = True
else: else:
api_key_set = False api_key_set = False
if os.getenv("EMBEDDINGS_KEY") is not None:
embeddings_key_set = True
else:
embeddings_key_set = False
app = Flask(__name__) app = Flask(__name__)
@app.route("/") @app.route("/")
def home(): def home():
return render_template("index.html", api_key_set=api_key_set) return render_template("index.html", api_key_set=api_key_set, llm_choice=llm_choice,
embeddings_choice=embeddings_choice)
@app.route("/api/answer", methods=["POST"]) @app.route("/api/answer", methods=["POST"])
def api_answer(): def api_answer():
data = request.get_json() data = request.get_json()
question = data["question"] question = data["question"]
history = data["history"]
if not api_key_set: if not api_key_set:
api_key = data["api_key"] api_key = data["api_key"]
else: else:
api_key = os.getenv("OPENAI_API_KEY") api_key = os.getenv("API_KEY")
if not embeddings_key_set:
embeddings_key = data["embeddings_key"]
else:
embeddings_key = os.getenv("EMBEDDINGS_KEY")
# check if the vectorstore is set # check if the vectorstore is set
if "active_docs" in data: if "active_docs" in data:
@@ -59,25 +93,37 @@ def api_answer():
vectorstore = "" vectorstore = ""
# loading the index and the store and the prompt template # loading the index and the store and the prompt template
index = faiss.read_index(f"{vectorstore}docs.index") # Note if you have used other embeddings than OpenAI, you need to change the embeddings
if embeddings_choice == "openai_text-embedding-ada-002":
docsearch = FAISS.load_local(vectorstore, OpenAIEmbeddings(openai_api_key=embeddings_key))
elif embeddings_choice == "huggingface_sentence-transformers/all-mpnet-base-v2":
docsearch = FAISS.load_local(vectorstore, HuggingFaceHubEmbeddings())
elif embeddings_choice == "huggingface_hkunlp/instructor-large":
docsearch = FAISS.load_local(vectorstore, HuggingFaceInstructEmbeddings())
elif embeddings_choice == "cohere_medium":
docsearch = FAISS.load_local(vectorstore, CohereEmbeddings(cohere_api_key=embeddings_key))
with open(f"{vectorstore}faiss_store.pkl", "rb") as f:
store = pickle.load(f)
store.index = index
# create a prompt template # create a prompt template
c_prompt = PromptTemplate(input_variables=["summaries", "question"], template=template) if history:
# create a chain with the prompt template and the store history = json.loads(history)
template_temp = template_hist.replace("{historyquestion}", history[0]).replace("{historyanswer}", history[1])
c_prompt = PromptTemplate(input_variables=["summaries", "question"], template=template_temp)
else:
c_prompt = PromptTemplate(input_variables=["summaries", "question"], template=template)
#chain = VectorDBQA.from_llm(llm=OpenAI(openai_api_key=api_key, temperature=0), vectorstore=store, combine_prompt=c_prompt) if llm_choice == "openai":
# chain = VectorDBQA.from_chain_type(llm=OpenAI(openai_api_key=api_key, temperature=0), chain_type='map_reduce', llm = OpenAI(openai_api_key=api_key, temperature=0)
# vectorstore=store) elif llm_choice == "manifest":
llm = ManifestWrapper(client=manifest, llm_kwargs={"temperature": 0.001, "max_tokens": 2048})
elif llm_choice == "huggingface":
llm = HuggingFaceHub(repo_id="bigscience/bloom", huggingfacehub_api_token=api_key)
elif llm_choice == "cohere":
llm = Cohere(model="command-xlarge-nightly", cohere_api_key=api_key)
qa_chain = load_qa_chain(OpenAI(openai_api_key=api_key, temperature=0), chain_type="map_reduce", qa_chain = load_qa_chain(llm=llm, chain_type="map_reduce",
combine_prompt=c_prompt) combine_prompt=c_prompt)
chain = VectorDBQA(combine_documents_chain=qa_chain, vectorstore=store)
chain = VectorDBQA(combine_documents_chain=qa_chain, vectorstore=docsearch, k=4)
# fetch the answer # fetch the answer
result = chain({"query": question}) result = chain({"query": question})
@@ -94,6 +140,7 @@ def api_answer():
# } # }
return result return result
@app.route("/api/docs_check", methods=["POST"]) @app.route("/api/docs_check", methods=["POST"])
def check_docs(): def check_docs():
# check if docs exist in a vectorstore folder # check if docs exist in a vectorstore folder
@@ -104,20 +151,21 @@ def check_docs():
if os.path.exists(vectorstore): if os.path.exists(vectorstore):
return {"status": 'exists'} return {"status": 'exists'}
else: else:
r = requests.get(base_path + vectorstore + "docs.index") r = requests.get(base_path + vectorstore + "index.faiss")
# save to vectors directory # save to vectors directory
# check if the directory exists # check if the directory exists
if not os.path.exists(vectorstore): if not os.path.exists(vectorstore):
os.makedirs(vectorstore) os.makedirs(vectorstore)
with open(vectorstore + "docs.index", "wb") as f: with open(vectorstore + "index.faiss", "wb") as f:
f.write(r.content) f.write(r.content)
# download the store # download the store
r = requests.get(base_path + vectorstore + "faiss_store.pkl") r = requests.get(base_path + vectorstore + "index.pkl")
with open(vectorstore + "faiss_store.pkl", "wb") as f: with open(vectorstore + "index.pkl", "wb") as f:
f.write(r.content) f.write(r.content)
return {"status": 'loaded'} return {"status": 'loaded'}
# handling CORS # handling CORS
@app.after_request @app.after_request

View File

@@ -0,0 +1,27 @@
You are a DocsGPT bot assistant by Arc53 that provides help with programming libraries. You give thorough answers with code examples.
Given the following extracted parts of a long document and a question, create a final answer with references ("SOURCES").
ALWAYS return a "SOURCES" part in your answer. You can also remeber things from previous questions and use them in your answer.
QUESTION: How to merge tables in pandas?
=========
Content: pandas provides various facilities for easily combining together Series or DataFrame with various kinds of set logic for the indexes and relational algebra functionality in the case of join / merge-type operations.
Source: 28-pl
Content: pandas provides a single function, merge(), as the entry point for all standard database join operations between DataFrame or named Series objects: \n\npandas.merge(left, right, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=False, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)
Source: 30-pl
=========
FINAL ANSWER: To merge two tables in pandas, you can use the pd.merge() function. The basic syntax is: \n\npd.merge(left, right, on, how) \n\nwhere left and right are the two tables to merge, on is the column to merge on, and how is the type of merge to perform. \n\nFor example, to merge the two tables df1 and df2 on the column 'id', you can use: \n\npd.merge(df1, df2, on='id', how='inner')
SOURCES: 28-pl 30-pl
QUESTION: {historyquestion}
=========
CONTENT:
SOURCE:
=========
FINAL ANSWER: {historyanswer}
SOURCES:
QUESTION: {question}
=========
{summaries}
=========
FINAL ANSWER:

BIN
application/index.faiss Normal file

Binary file not shown.

BIN
application/index.pkl Normal file

Binary file not shown.

View File

@@ -45,6 +45,7 @@ pytz==2022.7.1
PyYAML==6.0 PyYAML==6.0
regex==2022.10.31 regex==2022.10.31
requests==2.28.2 requests==2.28.2
retry==0.9.2
six==1.16.0 six==1.16.0
snowballstemmer==2.2.0 snowballstemmer==2.2.0
Sphinx==6.1.3 Sphinx==6.1.3
@@ -64,6 +65,6 @@ typer==0.7.0
typing-inspect==0.8.0 typing-inspect==0.8.0
typing_extensions==4.4.0 typing_extensions==4.4.0
urllib3==1.26.14 urllib3==1.26.14
Werkzeug==2.2.2 Werkzeug==2.2.3
XlsxWriter==3.0.8 XlsxWriter==3.0.8
yarl==1.8.2 yarl==1.8.2

View File

@@ -25,6 +25,8 @@ if (el) {
body: JSON.stringify({question: message, body: JSON.stringify({question: message,
api_key: localStorage.getItem('apiKey'), api_key: localStorage.getItem('apiKey'),
embeddings_key: localStorage.getItem('apiKey'),
history: localStorage.getItem('chatHistory'),
active_docs: localStorage.getItem('activeDocs')}), active_docs: localStorage.getItem('activeDocs')}),
}) })
.then(response => response.json()) .then(response => response.json())
@@ -38,9 +40,12 @@ if (el) {
chatWindow.scrollTop = chatWindow.scrollHeight; chatWindow.scrollTop = chatWindow.scrollHeight;
document.getElementById("button-submit").innerHTML = 'Send'; document.getElementById("button-submit").innerHTML = 'Send';
document.getElementById("button-submit").disabled = false; document.getElementById("button-submit").disabled = false;
let chatHistory = [message, data.answer];
localStorage.setItem('chatHistory', JSON.stringify(chatHistory));
}) })
.catch((error) => { .catch((error) => {
console.error('Error:', error); console.error('Error:', error);
console.log(error);
document.getElementById("button-submit").innerHTML = 'Send'; document.getElementById("button-submit").innerHTML = 'Send';
document.getElementById("button-submit").disabled = false; document.getElementById("button-submit").disabled = false;
}); });

View File

@@ -131,13 +131,17 @@ This will return a new DataFrame with all the columns from both tables, and only
var option = document.createElement("option"); var option = document.createElement("option");
if (docsIndex[key].name == docsIndex[key].language) { if (docsIndex[key].name == docsIndex[key].language) {
option.text = docsIndex[key].name + " " + docsIndex[key].version; option.text = docsIndex[key].name + " " + docsIndex[key].version;
option.value = docsIndex[key].name + "/" + ".project" + "/" + docsIndex[key].version + "/"; option.value = docsIndex[key].name + "/" + ".project" + "/" + docsIndex[key].version + "/{{ embeddings_choice }}/";
select.add(option); if (docsIndex[key].model == "{{ embeddings_choice }}") {
select.add(option);
}
} }
else { else {
option.text = docsIndex[key].name + " " + docsIndex[key].version; option.text = docsIndex[key].name + " " + docsIndex[key].version;
option.value = docsIndex[key].language + "/" + docsIndex[key].name + "/" + docsIndex[key].version + "/"; option.value = docsIndex[key].language + "/" + docsIndex[key].name + "/" + docsIndex[key].version + "/{{ embeddings_choice }}/";
select.add(option); if (docsIndex[key].model == "{{ embeddings_choice }}") {
select.add(option);
}
} }
} }

View File

@@ -19,6 +19,12 @@ module.exports = {
plugins: ['react'], plugins: ['react'],
rules: { rules: {
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
}, },
settings: { settings: {
'import/parsers': { 'import/parsers': {
@@ -34,10 +40,4 @@ module.exports = {
}, },
}, },
}, },
'prettier/prettier': [ };
'error',
{
endOfLine: 'auto',
},
],
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,11 @@
import { useSelector } from 'react-redux';
import { useMediaQuery } from '../hooks';
import { selectIsMenuOpen } from '../store';
//TODO - Add hyperlinks to text //TODO - Add hyperlinks to text
//TODO - Styling //TODO - Styling
export default function About() { export default function About() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isMenuOpen = useSelector(selectIsMenuOpen);
return ( return (
//Parent div for all content shown through App.tsx routing needs to have this styling. Might change when state management is updated. //Parent div for all content shown through App.tsx routing needs to have this styling. Might change when state management is updated.
<div <div className="grid min-h-screen">
className={`${ <article className=" mx-auto my-auto flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-lg bg-gray-100 p-6 text-jet lg:p-10 xl:p-16">
isMobile
? isMenuOpen
? 'mt-80'
: 'mt-16'
: isMenuOpen
? 'md:ml-72 lg:ml-96'
: 'ml-16'
} h-full w-full p-6 transition-all`}
>
<article className="mx-auto my-auto flex w-full max-w-6xl flex-col gap-6 rounded-lg bg-gray-100 p-6 text-jet lg:p-10 xl:p-16">
<p className="text-3xl font-semibold">About DocsGPT 🦖</p> <p className="text-3xl font-semibold">About DocsGPT 🦖</p>
<p className="mt-4 text-xl font-bold"> <p className="mt-4 text-xl font-bold">
Find the information in your documentation through AI-powered Find the information in your documentation through AI-powered

View File

@@ -1,18 +1,29 @@
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import Navigation from './components/Navigation'; import Navigation from './Navigation';
import Conversation from './components/Conversation/Conversation'; import Conversation from './conversation/Conversation';
import APIKeyModal from './components/APIKeyModal'; import About from './About';
import About from './components/About'; import { useState } from 'react';
import { ActiveState } from './models/misc';
export default function App() { export default function App() {
const [navState, setNavState] = useState<ActiveState>('ACTIVE');
return ( return (
<div className="relative flex flex-col transition-all md:flex-row"> <div className="min-h-full min-w-full">
<APIKeyModal /> <Navigation
<Navigation /> navState={navState}
<Routes> setNavState={(val: ActiveState) => setNavState(val)}
<Route path="/" element={<Conversation />} /> />
<Route path="/about" element={<About />} /> <div
</Routes> className={`transition-all duration-200 ${
navState === 'ACTIVE' ? 'ml-0 md:ml-72 lg:ml-96' : ' ml-0 md:ml-16'
}`}
>
<Routes>
<Route path="/" element={<Conversation />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
</div> </div>
); );
} }

9
frontend/src/Avatar.tsx Normal file
View File

@@ -0,0 +1,9 @@
export default function Avatar({
avatar,
size,
}: {
avatar: string;
size?: 'SMALL' | 'MEDIUM' | 'LARGE';
}) {
return <div>{avatar}</div>;
}

21
frontend/src/Hero.tsx Normal file
View File

@@ -0,0 +1,21 @@
export default function Hero({ className = '' }: { className?: string }) {
return (
<div className={`flex flex-col ${className}`}>
<p className="mb-10 text-center text-4xl font-semibold">
DocsGPT <span className="text-3xl">🦖</span>
</p>
<p className="mb-3 text-center">
Welcome to DocsGPT, your technical documentation assistant!
</p>
<p className="mb-3 text-center">
Enter a query related to the information in the documentation you
selected to receive and we will provide you with the most relevant
answers.
</p>
<p className="text-center">
Start by entering your query in the input field below and we will do the
rest!
</p>
</div>
);
}

View File

@@ -0,0 +1,97 @@
import { NavLink } from 'react-router-dom';
import Arrow1 from './assets/arrow.svg';
import Hamburger from './assets/hamburger.svg';
import Key from './assets/key.svg';
import Info from './assets/info.svg';
import Link from './assets/link.svg';
import { ActiveState } from './models/misc';
import APIKeyModal from './preferences/APIKeyModal';
import { useSelector } from 'react-redux';
import { selectApiKeyStatus } from './preferences/preferenceSlice';
import { useState } from 'react';
//TODO - Need to replace Chat button to open secondary nav with scrollable past chats option and new chat at top
//TODO - Need to add Discord and Github links
export default function Navigation({
navState,
setNavState,
}: {
navState: ActiveState;
setNavState: (val: ActiveState) => void;
}) {
const isApiKeySet = useSelector(selectApiKeyStatus);
const [apiKeyModalState, setApiKeyModalState] = useState<ActiveState>(
isApiKeySet ? 'INACTIVE' : 'ACTIVE',
);
return (
<>
<div
className={`${
navState === 'INACTIVE' && '-ml-96 md:-ml-60 lg:-ml-80'
} fixed z-10 flex h-full w-72 flex-col border-r-2 border-gray-100 bg-gray-50 transition-all duration-200 lg:w-96`}
>
<div className={'h-16 w-full border-b-2 border-gray-100'}>
<button
className="float-right mr-5 mt-5 h-5 w-5"
onClick={() =>
setNavState(navState === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE')
}
>
<img
src={Arrow1}
alt="menu toggle"
className={`${
navState === 'INACTIVE' ? 'rotate-180' : 'rotate-0'
} m-auto w-3 transition-all duration-200`}
/>
</button>
</div>
<div className="flex-grow border-b-2 border-gray-100"></div>
<div className="flex h-16 flex-col border-b-2 border-gray-100">
<div
className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100"
onClick={() => {
setApiKeyModalState('ACTIVE');
}}
>
<img src={Key} alt="key" className="ml-2 w-6" />
<p className="my-auto text-eerie-black">Reset Key</p>
</div>
</div>
<div className="flex h-48 flex-col border-b-2 border-gray-100">
<NavLink
to="/about"
className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100"
>
<img src={Info} alt="info" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">About</p>
</NavLink>
<div className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100">
<img src={Link} alt="link" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">Discord</p>
</div>
<div className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100">
<img src={Link} alt="link" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">Github</p>
</div>
</div>
</div>
<button
className="fixed mt-5 ml-6 h-6 w-6 md:hidden"
onClick={() => setNavState('ACTIVE')}
>
<img src={Hamburger} alt="menu toggle" className="w-7" />
</button>
<APIKeyModal
modalState={apiKeyModalState}
setModalState={setApiKeyModalState}
isCancellable={isApiKeySet}
/>
</>
);
}

View File

Before

Width:  |  Height:  |  Size: 200 B

After

Width:  |  Height:  |  Size: 200 B

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 254 B

View File

Before

Width:  |  Height:  |  Size: 273 B

After

Width:  |  Height:  |  Size: 273 B

View File

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 337 B

View File

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,3 @@
<svg width="21" height="18" viewBox="0 0 21 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.00999999 18L21 9L0.00999999 0L0 7L15 9L0 11L0.00999999 18Z" fill="black" fill-opacity="0.54"/>
</svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@@ -1,63 +0,0 @@
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
setApiKey,
toggleApiKeyModal,
selectIsApiKeyModalOpen,
} from '../store';
export default function APIKeyModal({}) {
//TODO - Add form validation?
//TODO - Connect to backend
//TODO - Add link to OpenAI API Key page
const dispatch = useDispatch();
const isApiModalOpen = useSelector(selectIsApiKeyModalOpen);
const [key, setKey] = useState('');
const [formError, setFormError] = useState(false);
function handleSubmit() {
if (key.length < 1) {
setFormError(true);
return;
}
dispatch(setApiKey(key));
dispatch(toggleApiKeyModal());
}
return (
<div
className={`${
isApiModalOpen ? 'visible' : 'hidden'
} absolute z-30 h-screen w-screen bg-gray-alpha`}
>
<article className="mx-auto mt-24 flex w-[90vw] max-w-lg flex-col gap-4 rounded-lg bg-white p-6 shadow-lg">
<p className="text-xl text-jet">OpenAI API Key</p>
<p className="text-lg leading-5 text-gray-500">
Before you can start using DocsGPT we need you to provide an API key
for llm. Currently, we support only OpenAI but soon many more. You can
find it here.
</p>
<input
type="text"
className="h-10 w-full border-b-2 border-jet focus:outline-none"
value={key}
maxLength={100}
placeholder="API Key"
onChange={(e) => setKey(e.target.value)}
/>
<div className="flex justify-between">
{formError && (
<p className="text-sm text-red-500">Please enter a valid API key</p>
)}
<button
onClick={() => handleSubmit()}
className="ml-auto h-10 w-20 rounded-lg bg-violet-800 text-white transition-all hover:bg-violet-700"
>
Save
</button>
</div>
</article>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import { useMediaQuery } from '../../hooks';
import { selectIsMenuOpen } from '../../store';
import { useSelector } from 'react-redux';
export default function Conversation() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isMenuOpen = useSelector(selectIsMenuOpen);
return (
//Parent div for all content shown through App.tsx routing needs to have this styling.
<div
className={`${
isMobile
? isMenuOpen
? 'mt-80'
: 'mt-16'
: isMenuOpen
? 'md:ml-72 lg:ml-96'
: 'ml-16'
} h-full w-full p-6 transition-all`}
>
Docs GPT Chat Placeholder
</div>
);
}

View File

@@ -1,163 +0,0 @@
import { useDispatch, useSelector } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { useMediaQuery } from '../hooks';
import {
toggleApiKeyModal,
selectIsMenuOpen,
toggleIsMenuOpen,
} from '../store';
import Arrow1 from '../imgs/arrow.svg';
import Hamburger from '../imgs/hamburger.svg';
import Key from '../imgs/key.svg';
import Info from '../imgs/info.svg';
import Link from '../imgs/link.svg';
import Exit from '../imgs/exit.svg';
//TODO - Need to replace Chat button to open secondary nav with scrollable past chats option and new chat at top
//TODO - Need to add Discord and Github links
function MobileNavigation({}) {
const dispatch = useDispatch();
const isMenuOpen = useSelector(selectIsMenuOpen);
return (
<div
className={`${
isMenuOpen ? 'border-b-2 border-gray-100' : 'h-16'
} fixed flex w-full flex-col bg-gray-50 transition-all`}
>
<div className="h-16 w-full border-b-2 border-gray-100">
{isMenuOpen ? (
<>
<button
className="mt-5 ml-6 h-6 w-6"
onClick={() => dispatch(toggleIsMenuOpen())}
>
<img src={Exit} alt="menu toggle" className="w-5" />
</button>
</>
) : (
<>
<button
className="mt-5 ml-6 h-6 w-6"
onClick={() => dispatch(toggleIsMenuOpen())}
>
<img src={Hamburger} alt="menu toggle" className="w-7" />
</button>
</>
)}
</div>
{isMenuOpen && (
<nav className="my-4 flex flex-col">
<NavLink
to="/"
className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100"
>
<img src={Info} alt="info" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">Chat</p>
</NavLink>
<NavLink
to="/about"
className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100"
>
<img src={Info} alt="info" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">About</p>
</NavLink>
<div className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100">
<img src={Link} alt="info" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">Discord</p>
</div>
<div className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100">
<img src={Link} alt="info" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">Github</p>
</div>
<div
className="flex h-12 cursor-pointer gap-4 rounded-md px-6 hover:bg-gray-100"
onClick={() => dispatch(toggleApiKeyModal())}
>
<img src={Key} alt="info" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">Reset Key</p>
</div>
</nav>
)}
</div>
);
}
function DesktopNavigation() {
const dispatch = useDispatch();
const isMenuOpen = useSelector(selectIsMenuOpen);
return (
<div
className={`${
isMenuOpen ? 'w-72 lg:w-96' : 'w-16'
} fixed flex h-screen flex-col border-r-2 border-gray-100 bg-gray-50 transition-all`}
>
<div
className={`${
isMenuOpen ? 'w-full' : 'w-16'
} ml-auto h-16 border-b-2 border-gray-100`}
>
<button
className="float-right mr-5 mt-5 h-5 w-5"
onClick={() => dispatch(toggleIsMenuOpen())}
>
<img
src={Arrow1}
alt="menu toggle"
className={`${
isMenuOpen ? 'rotate-0' : 'rotate-180'
} m-auto w-3 transition-all`}
/>
</button>
</div>
{isMenuOpen && (
<>
<div className="flex-grow border-b-2 border-gray-100"></div>
<div className="flex h-16 flex-col border-b-2 border-gray-100">
<div
className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100"
onClick={() => dispatch(toggleApiKeyModal())}
>
<img src={Key} alt="key" className="ml-2 w-6" />
<p className="my-auto text-eerie-black">Reset Key</p>
</div>
</div>
<div className="flex h-48 flex-col border-b-2 border-gray-100">
<NavLink
to="/about"
className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100"
>
<img src={Info} alt="info" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">About</p>
</NavLink>
<div className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100">
<img src={Link} alt="link" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">Discord</p>
</div>
<div className="my-auto mx-4 flex h-12 cursor-pointer gap-4 rounded-md hover:bg-gray-100">
<img src={Link} alt="link" className="ml-2 w-5" />
<p className="my-auto text-eerie-black">Github</p>
</div>
</div>
</>
)}
</div>
);
}
export default function Navigation() {
const isMobile = useMediaQuery('(max-width: 768px)');
if (isMobile) {
return <MobileNavigation />;
} else {
return <DesktopNavigation />;
}
}

View File

@@ -0,0 +1,33 @@
import { useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import Hero from '../Hero';
import ConversationBubble from './ConversationBubble';
import ConversationInput from './ConversationInput';
import { selectConversation } from './conversationSlice';
export default function Conversation() {
const messages = useSelector(selectConversation);
const endMessageRef = useRef<HTMLDivElement>(null);
useEffect(() => endMessageRef?.current?.scrollIntoView());
return (
<div className="flex justify-center p-6">
<div className="w-10/12 transition-all md:w-1/2">
{messages.map((message, index) => {
return (
<ConversationBubble
ref={index === messages.length - 1 ? endMessageRef : null}
className="mb-7"
key={index}
message={message.text}
type={message.type}
></ConversationBubble>
);
})}
{messages.length === 0 && <Hero className="mt-24"></Hero>}
</div>
<ConversationInput className="fixed bottom-2 w-10/12 md:w-[50%]"></ConversationInput>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { forwardRef } from 'react';
import Avatar from '../Avatar';
import { MESSAGE_TYPE } from './conversationModels';
const ConversationBubble = forwardRef<
HTMLDivElement,
{
message: string;
type: MESSAGE_TYPE;
className: string;
}
>(function ConversationBubble({ message, type, className }, ref) {
return (
<div
ref={ref}
className={`flex rounded-3xl ${
type === 'QUESTION' ? '' : 'bg-gray-1000'
} py-7 px-5 ${className}`}
>
<Avatar avatar={type === 'QUESTION' ? '👤' : '🦖'}></Avatar>
<p className="ml-5">{message}</p>
</div>
);
});
export default ConversationBubble;

View File

@@ -0,0 +1,21 @@
import Send from './../assets/send.svg';
export default function ConversationInput({
className,
}: {
className?: string;
}) {
return (
<div className={`${className} flex`}>
<div
contentEditable
className={`min-h-5 border-000000 overflow-x-hidden; max-h-24 w-full overflow-y-auto rounded-xl border bg-white p-2 pr-9 opacity-100 focus:border-2 focus:outline-none`}
></div>
<img
onClick={() => console.log('here')}
src={Send}
className="relative right-9"
></img>
</div>
);
}

View File

@@ -0,0 +1,10 @@
export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER';
export interface Message {
text: string;
type: MESSAGE_TYPE;
}
export interface ConversationState {
conversation: Message[];
}

View File

@@ -0,0 +1,50 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import store from '../store';
import { ConversationState, Message } from './conversationModels';
// harcoding the initial state just for demo
const initialState: ConversationState = {
conversation: [
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{ text: 'ChatGPT is large learning model', type: 'ANSWER' },
{ text: 'what is ChatGPT', type: 'QUESTION' },
{
text: 'ChatGPT is large learning model',
type: 'ANSWER',
},
],
};
export const conversationSlice = createSlice({
name: 'conversation',
initialState,
reducers: {
addMessage(state, action: PayloadAction<Message>) {
state.conversation.push(action.payload);
},
},
});
export const { addMessage } = conversationSlice.actions;
type RootState = ReturnType<typeof store.getState>;
export const selectConversation = (state: RootState) =>
state.conversation.conversation;
export default conversationSlice.reducer;

View File

@@ -1,22 +0,0 @@
import { useState, useEffect } from 'react';
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => {
setMatches(media.matches);
};
media.addEventListener('resize', listener);
return () => media.removeEventListener('resize', listener);
}, [matches, query]);
return matches;
}

View File

@@ -0,0 +1,5 @@
export type ActiveState = 'ACTIVE' | 'INACTIVE';
export type User = {
avatar: string;
};

View File

@@ -0,0 +1,83 @@
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { ActiveState } from '../models/misc';
import { setApiKey } from './preferenceSlice';
export default function APIKeyModal({
modalState,
setModalState,
isCancellable = true,
}: {
modalState: ActiveState;
setModalState: (val: ActiveState) => void;
isCancellable?: boolean;
}) {
const dispatch = useDispatch();
const [key, setKey] = useState('');
const [isError, setIsError] = useState(false);
function handleSubmit() {
if (key.length <= 1) {
setIsError(true);
} else {
dispatch(setApiKey(key));
setModalState('INACTIVE');
setKey('');
setIsError(false);
}
}
function handleCancel() {
setKey('');
setIsError(false);
setModalState('INACTIVE');
}
return (
<div
className={`${
modalState === 'ACTIVE' ? 'visible' : 'hidden'
} absolute z-30 h-screen w-screen bg-gray-alpha`}
>
<article className="mx-auto mt-24 flex w-[90vw] max-w-lg flex-col gap-4 rounded-lg bg-white p-6 shadow-lg">
<p className="text-xl text-jet">OpenAI API Key</p>
<p className="text-lg leading-5 text-gray-500">
Before you can start using DocsGPT we need you to provide an API key
for llm. Currently, we support only OpenAI but soon many more. You can
find it here.
</p>
<input
type="text"
className="h-10 w-full border-b-2 border-jet focus:outline-none"
value={key}
maxLength={100}
placeholder="API Key"
onChange={(e) => setKey(e.target.value)}
/>
<div className="flex flex-row-reverse">
<div>
<button
onClick={() => handleSubmit()}
className="ml-auto h-10 w-20 rounded-lg bg-violet-800 text-white transition-all hover:bg-violet-700"
>
Save
</button>
{isCancellable && (
<button
onClick={() => handleCancel()}
className="ml-5 h-10 w-20 rounded-lg border border-violet-700 bg-white text-violet-800 transition-all hover:bg-violet-700 hover:text-white"
>
Cancel
</button>
)}
</div>
{isError && (
<p className="mr-auto text-sm text-red-500">
Please enter a valid API key
</p>
)}
</div>
</article>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { createSlice } from '@reduxjs/toolkit';
import store from '../store';
interface Preference {
apiKey: string;
}
const initialState: Preference = {
apiKey: '',
};
export const prefSlice = createSlice({
name: 'preference',
initialState,
reducers: {
setApiKey: (state, action) => {
state.apiKey = action.payload;
},
},
});
export const { setApiKey } = prefSlice.actions;
export default prefSlice.reducer;
type RootState = ReturnType<typeof store.getState>;
export const selectApiKey = (state: RootState) => state.preference.apiKey;
export const selectApiKeyStatus = (state: RootState) =>
!!state.preference.apiKey;

View File

@@ -1,48 +1,12 @@
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import { conversationSlice } from './conversation/conversationSlice';
interface State { import { prefSlice } from './preferences/preferenceSlice';
isApiKeyModalOpen: boolean;
apiKey: string;
isMenuOpen: boolean;
}
const initialState: State = {
isApiKeyModalOpen: false,
apiKey: '',
isMenuOpen: false,
};
export const slice = createSlice({
name: 'app',
initialState,
reducers: {
toggleApiKeyModal: (state) => {
state.isApiKeyModalOpen = !state.isApiKeyModalOpen;
console.log('showApiKeyModal', state.isApiKeyModalOpen);
},
setApiKey: (state, action: PayloadAction<string>) => {
state.apiKey = action.payload;
console.log('setApiKey', action.payload);
},
toggleIsMenuOpen: (state) => {
state.isMenuOpen = !state.isMenuOpen;
},
},
});
export const { toggleApiKeyModal, setApiKey, toggleIsMenuOpen } = slice.actions;
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
app: slice.reducer, preference: prefSlice.reducer,
conversation: conversationSlice.reducer,
}, },
}); });
type RootState = ReturnType<typeof store.getState>;
export const selectIsApiKeyModalOpen = (state: RootState) =>
state.app.isApiKeyModalOpen;
export const selectApiKey = (state: RootState) => state.app.apiKey;
export const selectIsMenuOpen = (state: RootState) => state.app.isMenuOpen;
export default store; export default store;

View File

@@ -11,6 +11,7 @@ module.exports = {
'eerie-black': '#212121', 'eerie-black': '#212121',
jet: '#343541', jet: '#343541',
'gray-alpha': 'rgba(0,0,0, .1)', 'gray-alpha': 'rgba(0,0,0, .1)',
'gray-1000': '#F6F6F6',
}, },
}, },
}, },

View File

@@ -1,3 +1,5 @@
from collections import defaultdict
import os
import sys import sys
import nltk import nltk
import dotenv import dotenv
@@ -18,13 +20,17 @@ app = typer.Typer(add_completion=False)
nltk.download('punkt', quiet=True) nltk.download('punkt', quiet=True)
nltk.download('averaged_perceptron_tagger', quiet=True) nltk.download('averaged_perceptron_tagger', quiet=True)
#Splits all files in specified folder to documents #Splits all files in specified folder to documents
@app.command() @app.command()
def ingest(directory: Optional[str] = typer.Option("inputs", def ingest(yes: bool = typer.Option(False, "-y", "--yes", prompt=False,
help="Path to the directory for index creation."), help="Whether to skip price confirmation"),
files: Optional[List[str]] = typer.Option(None, dir: Optional[List[str]] = typer.Option(["inputs"],
help="""File paths to use (Optional; overrides directory). help="""List of paths to directory for index creation.
E.g. --files inputs/1.md --files inputs/2.md"""), E.g. --dir inputs --dir inputs2"""),
file: Optional[List[str]] = typer.Option(None,
help="""File paths to use (Optional; overrides dir).
E.g. --file inputs/1.md --file inputs/2.md"""),
recursive: Optional[bool] = typer.Option(True, recursive: Optional[bool] = typer.Option(True,
help="Whether to recursively search in subdirectories."), help="Whether to recursively search in subdirectories."),
limit: Optional[int] = typer.Option(None, limit: Optional[int] = typer.Option(None,
@@ -38,27 +44,39 @@ def ingest(directory: Optional[str] = typer.Option("inputs",
Creates index from specified location or files. Creates index from specified location or files.
By default /inputs folder is used, .rst and .md are parsed. By default /inputs folder is used, .rst and .md are parsed.
""" """
raw_docs = SimpleDirectoryReader(input_dir=directory, input_files=files, recursive=recursive,
required_exts=formats, num_files_limit=limit,
exclude_hidden=exclude).load_data()
raw_docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
print(raw_docs)
# Here we split the documents, as needed, into smaller chunks.
# We do this due to the context limits of the LLMs.
text_splitter = RecursiveCharacterTextSplitter()
docs = text_splitter.split_documents(raw_docs)
# Here we check for command line arguments for bot calls. def process_one_docs(directory, folder_name):
# If no argument exists or the permission_bypass_flag argument is not '-y', raw_docs = SimpleDirectoryReader(input_dir=directory, input_files=file, recursive=recursive,
# user permission is requested to call the API. required_exts=formats, num_files_limit=limit,
if len(sys.argv) > 1: exclude_hidden=exclude).load_data()
permission_bypass_flag = sys.argv[1] raw_docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
if permission_bypass_flag == '-y': # Here we split the documents, as needed, into smaller chunks.
call_openai_api(docs) # We do this due to the context limits of the LLMs.
text_splitter = RecursiveCharacterTextSplitter()
docs = text_splitter.split_documents(raw_docs)
# Here we check for command line arguments for bot calls.
# If no argument exists or the yes is not True, then the
# user permission is requested to call the API.
if len(sys.argv) > 1:
if yes:
call_openai_api(docs, folder_name)
else:
get_user_permission(docs, folder_name)
else: else:
get_user_permission(docs) get_user_permission(docs, folder_name)
else:
get_user_permission(docs) folder_counts = defaultdict(int)
folder_names = []
for dir_path in dir:
folder_name = os.path.basename(os.path.normpath(dir_path))
folder_counts[folder_name] += 1
if folder_counts[folder_name] > 1:
folder_name = f"{folder_name}_{folder_counts[folder_name]}"
folder_names.append(folder_name)
for directory, folder_name in zip(dir, folder_names):
process_one_docs(directory, folder_name)
if __name__ == "__main__": if __name__ == "__main__":
app() app()

View File

@@ -1,9 +1,17 @@
import os
import faiss import faiss
import pickle import pickle
import tiktoken import tiktoken
from langchain.vectorstores import FAISS from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings from langchain.embeddings import OpenAIEmbeddings
#from langchain.embeddings import HuggingFaceEmbeddings
#from langchain.embeddings import HuggingFaceInstructEmbeddings
#from langchain.embeddings import CohereEmbeddings
from retry import retry
def num_tokens_from_string(string: str, encoding_name: str) -> int: def num_tokens_from_string(string: str, encoding_name: str) -> int:
# Function to convert string to tokens and estimate user cost. # Function to convert string to tokens and estimate user cost.
@@ -12,8 +20,17 @@ def num_tokens_from_string(string: str, encoding_name: str) -> int:
total_price = ((num_tokens/1000) * 0.0004) total_price = ((num_tokens/1000) * 0.0004)
return num_tokens, total_price return num_tokens, total_price
def call_openai_api(docs): @retry(tries=10, delay=60)
def store_add_texts_with_retry(store, i):
store.add_texts([i.page_content], metadatas=[i.metadata])
def call_openai_api(docs, folder_name):
# Function to create a vector store from the documents and save it to disk. # Function to create a vector store from the documents and save it to disk.
# create output folder if it doesn't exist
if not os.path.exists(f"outputs/{folder_name}"):
os.makedirs(f"outputs/{folder_name}")
from tqdm import tqdm from tqdm import tqdm
docs_test = [docs[0]] docs_test = [docs[0]]
# remove the first element from docs # remove the first element from docs
@@ -22,34 +39,25 @@ def call_openai_api(docs):
#docs = docs[:n] #docs = docs[:n]
c1 = 0 c1 = 0
store = FAISS.from_documents(docs_test, OpenAIEmbeddings()) store = FAISS.from_documents(docs_test, OpenAIEmbeddings())
# Uncomment for MPNet embeddings
# model_name = "sentence-transformers/all-mpnet-base-v2"
# hf = HuggingFaceEmbeddings(model_name=model_name)
# store = FAISS.from_documents(docs_test, hf)
for i in tqdm(docs, desc="Embedding 🦖", unit="docs", total=len(docs), bar_format='{l_bar}{bar}| Time Left: {remaining}'): for i in tqdm(docs, desc="Embedding 🦖", unit="docs", total=len(docs), bar_format='{l_bar}{bar}| Time Left: {remaining}'):
try: try:
import time store_add_texts_with_retry(store, i)
store.add_texts([i.page_content], metadatas=[i.metadata])
except Exception as e: except Exception as e:
print(e) print(e)
print("Error on ", i) print("Error on ", i)
print("Saving progress") print("Saving progress")
print(f"stopped at {c1} out of {len(docs)}") print(f"stopped at {c1} out of {len(docs)}")
faiss.write_index(store.index, "docs.index") store.save_local(f"outputs/{folder_name}")
store_index_bak = store.index break
store.index = None
with open("faiss_store.pkl", "wb") as f:
pickle.dump(store, f)
print("Sleeping for 60 seconds and trying again")
time.sleep(60)
faiss.write_index(store_index_bak, "docs.index")
store.index = store_index_bak
store.add_texts([i.page_content], metadatas=[i.metadata])
c1 += 1 c1 += 1
store.save_local(f"outputs/{folder_name}")
def get_user_permission(docs, folder_name):
faiss.write_index(store.index, "docs.index")
store.index = None
with open("faiss_store.pkl", "wb") as f:
pickle.dump(store, f)
def get_user_permission(docs):
# Function to ask user permission to call the OpenAI api and spend their OpenAI funds. # Function to ask user permission to call the OpenAI api and spend their OpenAI funds.
# Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents. # Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents.
#docs_content = (" ".join(docs)) #docs_content = (" ".join(docs))
@@ -65,8 +73,8 @@ def get_user_permission(docs):
#Here we check for user permission before calling the API. #Here we check for user permission before calling the API.
user_input = input("Price Okay? (Y/N) \n").lower() user_input = input("Price Okay? (Y/N) \n").lower()
if user_input == "y": if user_input == "y":
call_openai_api(docs) call_openai_api(docs, folder_name)
elif user_input == "": elif user_input == "":
call_openai_api(docs) call_openai_api(docs, folder_name)
else: else:
print("The API was not called. No money was spent.") print("The API was not called. No money was spent.")