Feat/user chat history (#275)

* ♻️ refactor backend main routes

* 🗃️ new user_id uuid column in users table

* 🗃️ new chats table

*  new chat endpoints

*  change chat routes post to handle undef chat_id

* ♻️ extract components from chat page

*  add chatId to useQuestion

*  new ChatsList

*  new optional dynamic route chat/{chat_id}

* 🩹 add setQuestion to speach utils

* feat: self supplied key (#286)

* feat(brain): increased size if api key and more

* fix(key): not displayed

* feat(apikey): now password input

* fix(twitter): handle wrong

* feat(chat): basic source documents support (#289)

* ♻️ refactor backend main routes

* 🗃️ new user_id uuid column in users table

* 🗃️ new chats table

*  new chat endpoints

*  change chat routes post to handle undef chat_id

* ♻️ extract components from chat page

*  add chatId to useQuestion

*  new ChatsList

*  new optional dynamic route chat/{chat_id}

* 🩹 add setQuestion to speach utils

* 🎨 separate creation and update endpoints for chat

* 🩹 add feat(chat): basic source documents support

*  add chatName upon creation and for chats list

* 💄 improve chatsList style

* User chat history and multiple chats (#290)

* ♻️ refactor backend main routes

* 🗃️ new user_id uuid column in users table

* 🗃️ new chats table

*  new chat endpoints

*  change chat routes post to handle undef chat_id

* ♻️ extract components from chat page

*  add chatId to useQuestion

*  new ChatsList

*  new optional dynamic route chat/{chat_id}

* refactor(chat): use layout to avoid refetching all chats on every chat

* refactor(chat): useChats hook instead of useQuestion

* fix(chat): fix errors

* refactor(chat): better folder structure

* feat: self supplied key (#286)

* feat(brain): increased size if api key and more

* fix(key): not displayed

* feat(apikey): now password input

* fix(twitter): handle wrong

* feat(chat): basic source documents support (#289)

* style(chat): better looking sidebar

* resume merge

* fix(backend): add os and logger imports

* small fixes

* chore(chat): remove empty interface

* chore(chat): use const

* fix(chat): merge errors

* fix(chat): remove useSpeech args

* chore(chat): remove unused args

* fix(chat): remove duplicate components

---------

Co-authored-by: gozineb <zinebe@theodo.fr>
Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Stan Girard <girard.stanislas@gmail.com>
Co-authored-by: xleven <xleven@outlook.com>

* fix and refactor errors

* fix(fresh): installation issues

* chore(conflict): merged old code

* fix(multi-chat): use update endpoint

* feat(embeddings): now using users api key

---------

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Stan Girard <girard.stanislas@gmail.com>
Co-authored-by: xleven <xleven@outlook.com>
Co-authored-by: Aditya Nandan <61308761+iMADi-ARCH@users.noreply.github.com>
Co-authored-by: iMADi-ARCH <nandanaditya985@gmail.com>
Co-authored-by: Mamadou DICKO <mamadoudicko100@gmail.com>
This commit is contained in:
Zineb El Bachiri 2023-06-10 23:59:16 +02:00 committed by GitHub
parent 5a0f8326df
commit 4112699db5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 985 additions and 493 deletions

View File

@ -1,21 +1,18 @@
import os
import shutil
import time
from tempfile import SpooledTemporaryFile
import pypandoc
from auth.auth_bearer import JWTBearer
from crawl.crawler import CrawlWebsite
from fastapi import Depends, FastAPI, Request, UploadFile
from llm.qa import get_qa_llm
from llm.summarization import llm_evaluate_summaries
from fastapi import FastAPI
from logger import get_logger
from middlewares.cors import add_cors_middleware
from models.chats import ChatMessage
from models.users import User
from parsers.github import process_github
from utils.file import convert_bytes, get_file_size
from utils.processors import filter_file
from routes.chat_routes import chat_router
from routes.crawl_routes import crawl_router
from routes.explore_routes import explore_router
from routes.misc_routes import misc_router
from routes.upload_routes import upload_router
from routes.user_routes import user_router
from utils.vectors import (CommonsDep, create_user, similarity_search,
update_user_request_count)
@ -34,223 +31,9 @@ async def startup_event():
pypandoc.download_pandoc()
@app.post("/upload", dependencies=[Depends(JWTBearer())])
async def upload_file(request: Request, commons: CommonsDep, file: UploadFile, enable_summarization: bool = False, credentials: dict = Depends(JWTBearer())):
user = User(email=credentials.get('email', 'none'))
user_vectors_response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact") \
.filter("user_id", "eq", user.email)\
.execute()
documents = user_vectors_response.data # Access the data from the response
# Convert each dictionary to a tuple of items, then to a set to remove duplicates, and then back to a dictionary
user_unique_vectors = [dict(t) for t in set(tuple(d.items()) for d in documents)]
current_brain_size = sum(float(doc['size']) for doc in user_unique_vectors)
file_size = get_file_size(file)
remaining_free_space = 0
if request.headers.get('Openai-Api-Key'):
remaining_free_space = float(max_brain_size_with_own_key) - (current_brain_size)
else:
remaining_free_space = float(max_brain_size) - (current_brain_size)
if remaining_free_space - file_size < 0:
message = {"message": f"❌ User's brain will exceed maximum capacity with this upload. Maximum file allowed is : {convert_bytes(remaining_free_space)}", "type": "error"}
else:
message = await filter_file(file, enable_summarization, commons['supabase'], user)
return message
@app.post("/chat/", dependencies=[Depends(JWTBearer())])
async def chat_endpoint(request: Request, commons: CommonsDep, chat_message: ChatMessage, credentials: dict = Depends(JWTBearer())):
user = User(email=credentials.get('email', 'none'))
date = time.strftime("%Y%m%d")
max_requests_number = os.getenv("MAX_REQUESTS_NUMBER")
user_openai_api_key = request.headers.get('Openai-Api-Key')
response = commons['supabase'].from_('users').select(
'*').filter("user_id", "eq", user.email).filter("date", "eq", date).execute()
userItem = next(iter(response.data or []), {"requests_count": 0})
old_request_count = userItem['requests_count']
history = chat_message.history
history.append(("user", chat_message.question))
qa = get_qa_llm(chat_message, user.email, user_openai_api_key)
if user_openai_api_key is None:
if old_request_count == 0:
create_user(user_id= user.email, date=date)
elif old_request_count < float(max_requests_number) :
update_user_request_count(user_id=user.email, date=date, requests_count= old_request_count+1)
else:
history.append(('assistant', "You have reached your requests limit"))
return {"history": history }
if chat_message.use_summarization:
# 1. get summaries from the vector store based on question
summaries = similarity_search(
chat_message.question, table='match_summaries')
# 2. evaluate summaries against the question
evaluations = llm_evaluate_summaries(
chat_message.question, summaries, chat_message.model)
# 3. pull in the top documents from summaries
logger.info('Evaluations: %s', evaluations)
if evaluations:
reponse = commons['supabase'].from_('vectors').select(
'*').in_('id', values=[e['document_id'] for e in evaluations]).execute()
# 4. use top docs as additional context
additional_context = '---\nAdditional Context={}'.format(
'---\n'.join(data['content'] for data in reponse.data)
) + '\n'
model_response = qa(
{"question": additional_context + chat_message.question})
else:
model_response = qa({"question": chat_message.question})
answer = model_response["answer"]
# append sources (file_name) to answer
if "source_documents" in model_response:
logger.debug('Source Documents: %s', model_response["source_documents"])
sources = [
doc.metadata["file_name"] for doc in model_response["source_documents"]
if "file_name" in doc.metadata]
logger.debug('Sources: %s', sources)
if sources:
files = dict.fromkeys(sources)
# # shall provide file links until pages available
# files = [f"[{f}](/explore/{f})" for f in files]
answer = answer + "\n\nRef: " + "; ".join(files)
history.append(("assistant", answer))
return {"history": history}
@app.post("/crawl/", dependencies=[Depends(JWTBearer())])
async def crawl_endpoint(request: Request,commons: CommonsDep, crawl_website: CrawlWebsite, enable_summarization: bool = False, credentials: dict = Depends(JWTBearer())):
max_brain_size = os.getenv("MAX_BRAIN_SIZE")
if request.headers.get('Openai-Api-Key'):
max_brain_size = os.getenv("MAX_BRAIN_SIZE_WITH_KEY",209715200)
user = User(email=credentials.get('email', 'none'))
user_vectors_response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact") \
.filter("user_id", "eq", user.email)\
.execute()
documents = user_vectors_response.data # Access the data from the response
# Convert each dictionary to a tuple of items, then to a set to remove duplicates, and then back to a dictionary
user_unique_vectors = [dict(t) for t in set(tuple(d.items()) for d in documents)]
current_brain_size = sum(float(doc['size']) for doc in user_unique_vectors)
file_size = 1000000
remaining_free_space = float(max_brain_size) - (current_brain_size)
if remaining_free_space - file_size < 0:
message = {"message": f"❌ User's brain will exceed maximum capacity with this upload. Maximum file allowed is : {convert_bytes(remaining_free_space)}", "type": "error"}
else:
user = User(email=credentials.get('email', 'none'))
if not crawl_website.checkGithub():
file_path, file_name = crawl_website.process()
# Create a SpooledTemporaryFile from the file_path
spooled_file = SpooledTemporaryFile()
with open(file_path, 'rb') as f:
shutil.copyfileobj(f, spooled_file)
# Pass the SpooledTemporaryFile to UploadFile
file = UploadFile(file=spooled_file, filename=file_name)
message = await filter_file(file, enable_summarization, commons['supabase'], user=user)
return message
else:
message = await process_github(crawl_website.url, "false", user=user, supabase=commons['supabase'])
@app.get("/explore", dependencies=[Depends(JWTBearer())])
async def explore_endpoint(commons: CommonsDep,credentials: dict = Depends(JWTBearer()) ):
user = User(email=credentials.get('email', 'none'))
response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact").filter("user_id", "eq", user.email).execute()
documents = response.data # Access the data from the response
# Convert each dictionary to a tuple of items, then to a set to remove duplicates, and then back to a dictionary
unique_data = [dict(t) for t in set(tuple(d.items()) for d in documents)]
# Sort the list of documents by size in decreasing order
unique_data.sort(key=lambda x: int(x['size']), reverse=True)
return {"documents": unique_data}
@app.delete("/explore/{file_name}", dependencies=[Depends(JWTBearer())])
async def delete_endpoint(commons: CommonsDep, file_name: str, credentials: dict = Depends(JWTBearer())):
user = User(email=credentials.get('email', 'none'))
# Cascade delete the summary from the database first, because it has a foreign key constraint
commons['supabase'].table("summaries").delete().match(
{"metadata->>file_name": file_name}).execute()
commons['supabase'].table("vectors").delete().match(
{"metadata->>file_name": file_name, "user_id": user.email}).execute()
return {"message": f"{file_name} of user {user.email} has been deleted."}
@app.get("/explore/{file_name}", dependencies=[Depends(JWTBearer())])
async def download_endpoint(commons: CommonsDep, file_name: str,credentials: dict = Depends(JWTBearer()) ):
user = User(email=credentials.get('email', 'none'))
response = commons['supabase'].table("vectors").select(
"metadata->>file_name, metadata->>file_size, metadata->>file_extension, metadata->>file_url", "content").match({"metadata->>file_name": file_name, "user_id": user.email}).execute()
documents = response.data
# Returns all documents with the same file name
return {"documents": documents}
@app.get("/user", dependencies=[Depends(JWTBearer())])
async def get_user_endpoint(request: Request, commons: CommonsDep, credentials: dict = Depends(JWTBearer())):
# Create a function that returns the unique documents out of the vectors
# Create a function that returns the list of documents that can take in what to put in the select + the filter
user = User(email=credentials.get('email', 'none'))
# Cascade delete the summary from the database first, because it has a foreign key constraint
user_vectors_response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact") \
.filter("user_id", "eq", user.email)\
.execute()
documents = user_vectors_response.data # Access the data from the response
# Convert each dictionary to a tuple of items, then to a set to remove duplicates, and then back to a dictionary
user_unique_vectors = [dict(t) for t in set(tuple(d.items()) for d in documents)]
current_brain_size = sum(float(doc['size']) for doc in user_unique_vectors)
max_brain_size = os.getenv("MAX_BRAIN_SIZE")
if request.headers.get('Openai-Api-Key'):
max_brain_size = max_brain_size_with_own_key
# Create function get user request stats -> nombre de requetes par jour + max number of requests -> svg to display the number of requests ? une fusee ?
user = User(email=credentials.get('email', 'none'))
date = time.strftime("%Y%m%d")
max_requests_number = os.getenv("MAX_REQUESTS_NUMBER")
if request.headers.get('Openai-Api-Key'):
max_requests_number = 1000000
requests_stats = commons['supabase'].from_('users').select(
'*').filter("user_id", "eq", user.email).execute()
return {"email":user.email,
"max_brain_size": max_brain_size,
"current_brain_size": current_brain_size,
"max_requests_number": max_requests_number,
"requests_stats" : requests_stats.data,
"date": date,
}
@app.get("/")
async def root():
return {"status": "OK"}
app.include_router(chat_router)
app.include_router(crawl_router)
app.include_router(explore_router)
app.include_router(misc_router)
app.include_router(upload_router)
app.include_router(user_router)

View File

@ -1,4 +1,5 @@
from typing import List, Tuple
from typing import List, Tuple, Optional
from uuid import UUID
from pydantic import BaseModel
@ -11,3 +12,4 @@ class ChatMessage(BaseModel):
temperature: float = 0.0
max_tokens: int = 256
use_summarization: bool = False
chat_id: Optional[UUID] = None,

View File

@ -32,7 +32,7 @@ from utils.vectors import documents_vector_store
# return transcript
# async def process_audio(upload_file: UploadFile, stats_db):
async def process_audio(upload_file: UploadFile, enable_summarization: bool, user):
async def process_audio(upload_file: UploadFile, enable_summarization: bool, user, user_openai_api_key):
file_sha = ""
dateshort = time.strftime("%Y%m%d-%H%M%S")
@ -40,6 +40,8 @@ async def process_audio(upload_file: UploadFile, enable_summarization: bool, use
# uploaded file to file object
openai_api_key = os.environ.get("OPENAI_API_KEY")
if user_openai_api_key:
openai_api_key = user_openai_api_key
# Here, we're writing the uploaded file to a temporary file, so we can use it with your existing code.
with tempfile.NamedTemporaryFile(delete=False, suffix=upload_file.filename) as tmp_file:

View File

@ -12,7 +12,7 @@ from utils.file import compute_sha1_from_content, compute_sha1_from_file
from utils.vectors import create_summary, create_vector, documents_vector_store
async def process_file(file: UploadFile, loader_class, file_suffix, enable_summarization, user):
async def process_file(file: UploadFile, loader_class, file_suffix, enable_summarization, user, user_openai_api_key):
documents = []
file_name = file.filename
file_size = file.file._file.tell() # Getting the size of the file
@ -51,7 +51,7 @@ async def process_file(file: UploadFile, loader_class, file_suffix, enable_summa
}
doc_with_metadata = Document(
page_content=doc.page_content, metadata=metadata)
create_vector(user.email, doc_with_metadata)
create_vector(user.email, doc_with_metadata, user_openai_api_key)
# add_usage(stats_db, "embedding", "audio", metadata={"file_name": file_meta_name,"file_type": ".txt", "chunk_size": chunk_size, "chunk_overlap": chunk_overlap})
if enable_summarization and ids and len(ids) > 0:

View File

@ -4,5 +4,5 @@ from langchain.document_loaders.csv_loader import CSVLoader
from .common import process_file
def process_csv(file: UploadFile, enable_summarization, user):
return process_file(file, CSVLoader, ".csv", enable_summarization, user)
def process_csv(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, CSVLoader, ".csv", enable_summarization, user, user_openai_api_key)

View File

@ -4,5 +4,5 @@ from langchain.document_loaders import Docx2txtLoader
from .common import process_file
def process_docx(file: UploadFile, enable_summarization, user):
return process_file(file, Docx2txtLoader, ".docx", enable_summarization, user)
def process_docx(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, Docx2txtLoader, ".docx", enable_summarization, user, user_openai_api_key)

View File

@ -4,5 +4,5 @@ from langchain.document_loaders.epub import UnstructuredEPubLoader
from .common import process_file
def process_epub(file: UploadFile, enable_summarization, user):
return process_file(file, UnstructuredEPubLoader, ".epub", enable_summarization, user)
def process_epub(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, UnstructuredEPubLoader, ".epub", enable_summarization, user, user_openai_api_key)

View File

@ -12,7 +12,7 @@ from utils.vectors import create_summary, create_vector, documents_vector_store
from .common import process_file
async def process_github(repo, enable_summarization, user, supabase):
async def process_github(repo, enable_summarization, user, supabase, user_openai_api_key):
random_dir_name = os.urandom(16).hex()
dateshort = time.strftime("%Y%m%d")
loader = GitLoader(
@ -46,7 +46,7 @@ async def process_github(repo, enable_summarization, user, supabase):
page_content=doc.page_content, metadata=metadata)
exist = await file_already_exists_from_content(supabase, doc.page_content.encode("utf-8"), user)
if not exist:
create_vector(user.email, doc_with_metadata)
create_vector(user.email, doc_with_metadata, user_openai_api_key)
print("Created vector for ", doc.metadata["file_name"])
return {"message": f"✅ Github with {len(documents)} files has been uploaded.", "type": "success"}

View File

@ -10,8 +10,8 @@ from langchain.document_loaders import UnstructuredHTMLLoader
from .common import process_file
def process_html(file: UploadFile, enable_summarization, user):
return process_file(file, UnstructuredHTMLLoader, ".html", enable_summarization, user)
def process_html(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, UnstructuredHTMLLoader, ".html", enable_summarization, user, user_openai_api_key)
def get_html(url):

View File

@ -4,5 +4,5 @@ from langchain.document_loaders import UnstructuredMarkdownLoader
from .common import process_file
def process_markdown(file: UploadFile, enable_summarization, user):
return process_file(file, UnstructuredMarkdownLoader, ".md", enable_summarization, user)
def process_markdown(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, UnstructuredMarkdownLoader, ".md", enable_summarization, user, user_openai_api_key)

View File

@ -4,5 +4,5 @@ from langchain.document_loaders import NotebookLoader
from .common import process_file
def process_ipnyb(file: UploadFile, enable_summarization, user):
return process_file(file, NotebookLoader, "ipynb", enable_summarization, user)
def process_ipnyb(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, NotebookLoader, "ipynb", enable_summarization, user, user_openai_api_key)

View File

@ -4,5 +4,5 @@ from langchain.document_loaders import UnstructuredODTLoader
from .common import process_file
def process_odt(file: UploadFile, enable_summarization, user):
return process_file(file, UnstructuredODTLoader, ".odt", enable_summarization, user)
def process_odt(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, UnstructuredODTLoader, ".odt", enable_summarization, user, user_openai_api_key)

View File

@ -4,5 +4,5 @@ from langchain.document_loaders import PyMuPDFLoader
from .common import process_file
def process_pdf(file: UploadFile, enable_summarization, user):
return process_file(file, PyMuPDFLoader, ".pdf", enable_summarization, user)
def process_pdf(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, PyMuPDFLoader, ".pdf", enable_summarization, user, user_openai_api_key)

View File

@ -4,5 +4,5 @@ from langchain.document_loaders import UnstructuredPowerPointLoader
from .common import process_file
def process_powerpoint(file: UploadFile, enable_summarization, user):
return process_file(file, UnstructuredPowerPointLoader, ".pptx", enable_summarization, user)
def process_powerpoint(file: UploadFile, enable_summarization, user, user_openai_api_key):
return process_file(file, UnstructuredPowerPointLoader, ".pptx", enable_summarization, user, user_openai_api_key)

View File

@ -4,5 +4,5 @@ from langchain.document_loaders import TextLoader
from .common import process_file
async def process_txt(file: UploadFile, enable_summarization, user):
return await process_file(file, TextLoader, ".txt", enable_summarization, user)
async def process_txt(file: UploadFile, enable_summarization, user, user_openai_api_key):
return await process_file(file, TextLoader, ".txt", enable_summarization, user,user_openai_api_key)

View File

View File

@ -0,0 +1,120 @@
import os
import time
from typing import Optional
from uuid import UUID
from auth.auth_bearer import JWTBearer
from fastapi import APIRouter, Depends, Request
from models.chats import ChatMessage
from models.users import User
from utils.vectors import (CommonsDep, create_chat, create_user,
fetch_user_id_from_credentials, get_answer,
get_chat_name_from_first_question, update_chat,
update_user_request_count)
chat_router = APIRouter()
# get all chats
@chat_router.get("/chat", dependencies=[Depends(JWTBearer())])
async def get_chats(commons: CommonsDep, credentials: dict = Depends(JWTBearer())):
date = time.strftime("%Y%m%d")
user_id = fetch_user_id_from_credentials(commons,date, credentials)
# Fetch all chats for the user
response = commons['supabase'].from_('chats').select('chatId:chat_id, chatName:chat_name').filter("user_id", "eq", user_id).execute()
chats = response.data
# TODO: Only get the chat name instead of the history
return {"chats": chats}
# get one chat
@chat_router.get("/chat/{chat_id}", dependencies=[Depends(JWTBearer())])
async def get_chats(commons: CommonsDep, chat_id: UUID):
# Fetch all chats for the user
response = commons['supabase'].from_('chats').select('*').filter("chat_id", "eq", chat_id).execute()
chats = response.data
print("/chat/{chat_id}",chats)
return {"chatId": chat_id, "history": chats[0]['history']}
# delete one chat
@chat_router.delete("/chat/{chat_id}", dependencies=[Depends(JWTBearer())])
async def delete_chat(commons: CommonsDep,chat_id: UUID):
commons['supabase'].table("chats").delete().match(
{"chat_id": chat_id}).execute()
return {"message": f"{chat_id} has been deleted."}
# update existing chat
@chat_router.put("/chat/{chat_id}", dependencies=[Depends(JWTBearer())])
async def chat_endpoint(request: Request,commons: CommonsDep, chat_id: UUID, chat_message: ChatMessage, credentials: dict = Depends(JWTBearer())):
user = User(email=credentials.get('email', 'none'))
date = time.strftime("%Y%m%d")
max_requests_number = os.getenv("MAX_REQUESTS_NUMBER")
user_openai_api_key = request.headers.get('Openai-Api-Key')
response = commons['supabase'].from_('users').select(
'*').filter("email", "eq", user.email).filter("date", "eq", date).execute()
userItem = next(iter(response.data or []), {"requests_count": 0})
old_request_count = userItem['requests_count']
history = chat_message.history
history.append(("user", chat_message.question))
if old_request_count == 0:
create_user(email= user.email, date=date)
elif old_request_count < float(max_requests_number) :
update_user_request_count(email=user.email, date=date, requests_count= old_request_count+1)
else:
history.append(('assistant', "You have reached your requests limit"))
update_chat(chat_id=chat_id, history=history)
return {"history": history }
answer = get_answer(commons, chat_message, user.email,user_openai_api_key)
history.append(("assistant", answer))
update_chat(chat_id=chat_id, history=history)
return {"history": history, "chatId": chat_id}
# create new chat
@chat_router.post("/chat", dependencies=[Depends(JWTBearer())])
async def chat_endpoint(request: Request,commons: CommonsDep, chat_message: ChatMessage, credentials: dict = Depends(JWTBearer())):
user = User(email=credentials.get('email', 'none'))
date = time.strftime("%Y%m%d")
user_id = fetch_user_id_from_credentials(commons, date,credentials)
max_requests_number = os.getenv("MAX_REQUESTS_NUMBER")
user_openai_api_key = request.headers.get('Openai-Api-Key')
response = commons['supabase'].from_('users').select(
'*').filter("email", "eq", user.email).filter("date", "eq", date).execute()
userItem = next(iter(response.data or []), {"requests_count": 0})
old_request_count = userItem['requests_count']
history = chat_message.history
history.append(("user", chat_message.question))
chat_name = get_chat_name_from_first_question(chat_message)
print('chat_name',chat_name)
if user_openai_api_key is None:
if old_request_count == 0:
create_user(email= user.email, date=date)
elif old_request_count < float(max_requests_number) :
update_user_request_count(email=user.email, date=date, requests_count= old_request_count+1)
else:
history.append(('assistant', "You have reached your requests limit"))
new_chat = create_chat(user_id, history, chat_name)
return {"history": history, "chatId": new_chat.data[0]['chat_id'] }
answer = get_answer(commons, chat_message, user.email, user_openai_api_key)
history.append(("assistant", answer))
new_chat = create_chat(user_id, history, chat_name)
return {"history": history, "chatId": new_chat.data[0]['chat_id'], "chatName":new_chat.data[0]['chat_name'] }

View File

@ -0,0 +1,56 @@
import os
import shutil
from tempfile import SpooledTemporaryFile
from auth.auth_bearer import JWTBearer
from crawl.crawler import CrawlWebsite
from fastapi import APIRouter, Depends, Request, UploadFile
from middlewares.cors import add_cors_middleware
from models.users import User
from parsers.github import process_github
from utils.file import convert_bytes
from utils.processors import filter_file
from utils.vectors import CommonsDep
crawl_router = APIRouter()
@crawl_router.post("/crawl/", dependencies=[Depends(JWTBearer())])
async def crawl_endpoint(request: Request,commons: CommonsDep, crawl_website: CrawlWebsite, enable_summarization: bool = False, credentials: dict = Depends(JWTBearer())):
max_brain_size = os.getenv("MAX_BRAIN_SIZE")
if request.headers.get('Openai-Api-Key'):
max_brain_size = os.getenv("MAX_BRAIN_SIZE_WITH_KEY",209715200)
user = User(email=credentials.get('email', 'none'))
user_vectors_response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact") \
.filter("user_id", "eq", user.email)\
.execute()
documents = user_vectors_response.data # Access the data from the response
# Convert each dictionary to a tuple of items, then to a set to remove duplicates, and then back to a dictionary
user_unique_vectors = [dict(t) for t in set(tuple(d.items()) for d in documents)]
current_brain_size = sum(float(doc['size']) for doc in user_unique_vectors)
file_size = 1000000
remaining_free_space = float(max_brain_size) - (current_brain_size)
if remaining_free_space - file_size < 0:
message = {"message": f"❌ User's brain will exceed maximum capacity with this upload. Maximum file allowed is : {convert_bytes(remaining_free_space)}", "type": "error"}
else:
user = User(email=credentials.get('email', 'none'))
if not crawl_website.checkGithub():
file_path, file_name = crawl_website.process()
# Create a SpooledTemporaryFile from the file_path
spooled_file = SpooledTemporaryFile()
with open(file_path, 'rb') as f:
shutil.copyfileobj(f, spooled_file)
# Pass the SpooledTemporaryFile to UploadFile
file = UploadFile(file=spooled_file, filename=file_name)
message = await filter_file(file, enable_summarization, commons['supabase'], user=user)
return message
else:
message = await process_github(crawl_website.url, "false", user=user, supabase=commons['supabase'])

View File

@ -0,0 +1,40 @@
from auth.auth_bearer import JWTBearer
from fastapi import APIRouter, Depends
from models.users import User
from utils.vectors import CommonsDep
explore_router = APIRouter()
@explore_router.get("/explore", dependencies=[Depends(JWTBearer())])
async def explore_endpoint(commons: CommonsDep,credentials: dict = Depends(JWTBearer()) ):
user = User(email=credentials.get('email', 'none'))
response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact").filter("user_id", "eq", user.email).execute()
documents = response.data # Access the data from the response
# Convert each dictionary to a tuple of items, then to a set to remove duplicates, and then back to a dictionary
unique_data = [dict(t) for t in set(tuple(d.items()) for d in documents)]
# Sort the list of documents by size in decreasing order
unique_data.sort(key=lambda x: int(x['size']), reverse=True)
return {"documents": unique_data}
@explore_router.delete("/explore/{file_name}", dependencies=[Depends(JWTBearer())])
async def delete_endpoint(commons: CommonsDep, file_name: str, credentials: dict = Depends(JWTBearer())):
user = User(email=credentials.get('email', 'none'))
# Cascade delete the summary from the database first, because it has a foreign key constraint
commons['supabase'].table("summaries").delete().match(
{"metadata->>file_name": file_name}).execute()
commons['supabase'].table("vectors").delete().match(
{"metadata->>file_name": file_name, "user_id": user.email}).execute()
return {"message": f"{file_name} of user {user.email} has been deleted."}
@explore_router.get("/explore/{file_name}", dependencies=[Depends(JWTBearer())])
async def download_endpoint(commons: CommonsDep, file_name: str,credentials: dict = Depends(JWTBearer()) ):
user = User(email=credentials.get('email', 'none'))
response = commons['supabase'].table("vectors").select(
"metadata->>file_name, metadata->>file_size, metadata->>file_extension, metadata->>file_url", "content").match({"metadata->>file_name": file_name, "user_id": user.email}).execute()
documents = response.data
# Returns all documents with the same file name
return {"documents": documents}

View File

@ -0,0 +1,7 @@
from fastapi import APIRouter
misc_router = APIRouter()
@misc_router.get("/")
async def root():
return {"status": "OK"}

View File

@ -0,0 +1,44 @@
import os
from tempfile import SpooledTemporaryFile
from auth.auth_bearer import JWTBearer
from crawl.crawler import CrawlWebsite
from fastapi import APIRouter, Depends, Request, UploadFile
from models.chats import ChatMessage
from models.users import User
from utils.file import convert_bytes, get_file_size
from utils.processors import filter_file
from utils.vectors import CommonsDep
upload_router = APIRouter()
@upload_router.post("/upload", dependencies=[Depends(JWTBearer())])
async def upload_file(request: Request,commons: CommonsDep, file: UploadFile, enable_summarization: bool = False, credentials: dict = Depends(JWTBearer())):
max_brain_size = os.getenv("MAX_BRAIN_SIZE")
max_brain_size_with_own_key = os.getenv("MAX_BRAIN_SIZE_WITH_KEY",209715200)
remaining_free_space = 0
user = User(email=credentials.get('email', 'none'))
user_vectors_response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact") \
.filter("user_id", "eq", user.email)\
.execute()
documents = user_vectors_response.data # Access the data from the response
# Convert each dictionary to a tuple of items, then to a set to remove duplicates, and then back to a dictionary
user_unique_vectors = [dict(t) for t in set(tuple(d.items()) for d in documents)]
current_brain_size = sum(float(doc['size']) for doc in user_unique_vectors)
if request.headers.get('Openai-Api-Key'):
remaining_free_space = float(max_brain_size_with_own_key) - (current_brain_size)
else:
remaining_free_space = float(max_brain_size) - (current_brain_size)
file_size = get_file_size(file)
if remaining_free_space - file_size < 0:
message = {"message": f"❌ User's brain will exceed maximum capacity with this upload. Maximum file allowed is : {convert_bytes(remaining_free_space)}", "type": "error"}
else:
message = await filter_file(file, enable_summarization, commons['supabase'], user, openai_api_key=request.headers.get('Openai-Api-Key', None))
return message

View File

@ -0,0 +1,51 @@
import os
import time
from auth.auth_bearer import JWTBearer
from fastapi import APIRouter, Depends, Request
from models.users import User
from utils.vectors import CommonsDep
user_router = APIRouter()
max_brain_size_with_own_key = os.getenv("MAX_BRAIN_SIZE_WITH_KEY",209715200)
@user_router.get("/user", dependencies=[Depends(JWTBearer())])
async def get_user_endpoint(request: Request,commons: CommonsDep, credentials: dict = Depends(JWTBearer())):
# Create a function that returns the unique documents out of the vectors
# Create a function that returns the list of documents that can take in what to put in the select + the filter
user = User(email=credentials.get('email', 'none'))
# Cascade delete the summary from the database first, because it has a foreign key constraint
user_vectors_response = commons['supabase'].table("vectors").select(
"name:metadata->>file_name, size:metadata->>file_size", count="exact") \
.filter("user_id", "eq", user.email)\
.execute()
documents = user_vectors_response.data # Access the data from the response
# Convert each dictionary to a tuple of items, then to a set to remove duplicates, and then back to a dictionary
user_unique_vectors = [dict(t) for t in set(tuple(d.items()) for d in documents)]
current_brain_size = sum(float(doc['size']) for doc in user_unique_vectors)
max_brain_size = os.getenv("MAX_BRAIN_SIZE")
if request.headers.get('Openai-Api-Key'):
max_brain_size = max_brain_size_with_own_key
# Create function get user request stats -> nombre de requetes par jour + max number of requests -> svg to display the number of requests ? une fusee ?
user = User(email=credentials.get('email', 'none'))
date = time.strftime("%Y%m%d")
max_requests_number = os.getenv("MAX_REQUESTS_NUMBER")
if request.headers.get('Openai-Api-Key'):
max_brain_size = max_brain_size_with_own_key
requests_stats = commons['supabase'].from_('users').select(
'*').filter("email", "eq", user.email).execute()
return {"email":user.email,
"max_brain_size": max_brain_size,
"current_brain_size": current_brain_size,
"max_requests_number": max_requests_number,
"requests_stats" : requests_stats.data,
"date": date,
}

View File

@ -14,6 +14,7 @@ from parsers.odt import process_odt
from parsers.pdf import process_pdf
from parsers.powerpoint import process_powerpoint
from parsers.txt import process_txt
from supabase import Client
file_processors = {
@ -40,7 +41,7 @@ file_processors = {
async def filter_file(file: UploadFile, enable_summarization: bool, supabase_client: Client, user: User):
async def filter_file(file: UploadFile, enable_summarization: bool, supabase_client: Client, user: User, openai_api_key):
if await file_already_exists(supabase_client, file, user):
return {"message": f"🤔 {file.filename} already exists.", "type": "warning"}
elif file.file._file.tell() < 1:
@ -48,7 +49,7 @@ async def filter_file(file: UploadFile, enable_summarization: bool, supabase_cli
else:
file_extension = os.path.splitext(file.filename)[-1].lower() # Convert file extension to lowercase
if file_extension in file_processors:
await file_processors[file_extension](file, enable_summarization, user)
await file_processors[file_extension](file, enable_summarization, user ,openai_api_key )
return {"message": f"{file.filename} has been uploaded.", "type": "success"}
else:
return {"message": f"{file.filename} is not supported.", "type": "error"}

View File

@ -1,13 +1,18 @@
import os
from typing import Annotated, List, Tuple
from auth.auth_bearer import JWTBearer
from fastapi import Depends, UploadFile
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.schema import Document
from langchain.vectorstores import SupabaseVectorStore
from llm.summarization import llm_summerize
from llm.qa import get_qa_llm
from llm.summarization import llm_evaluate_summaries, llm_summerize
from logger import get_logger
from models.chats import ChatMessage
from models.users import User
from pydantic import BaseModel
from supabase import Client, create_client
logger = get_logger(__name__)
@ -55,10 +60,13 @@ def create_summary(document_id, content, metadata):
supabase_client.table("summaries").update(
{"document_id": document_id}).match({"id": sids[0]}).execute()
def create_vector(user_id,doc):
def create_vector(user_id,doc, user_openai_api_key=None):
logger.info(f"Creating vector for document")
logger.info(f"Document: {doc}")
if user_openai_api_key:
documents_vector_store._embedding = embeddings_request = OpenAIEmbeddings(openai_api_key=user_openai_api_key)
try:
sids = documents_vector_store.add_documents(
[doc])
if sids and len(sids) > 0:
@ -67,16 +75,37 @@ def create_vector(user_id,doc):
except Exception as e:
logger.error(f"Error creating vector for document {e}")
def create_user(user_id, date):
logger.info(f"New user entry in db document for user {user_id}")
supabase_client.table("users").insert(
{"user_id": user_id, "date": date, "requests_count": 1}).execute()
def create_user(email, date):
logger.info(f"New user entry in db document for user {email}")
def update_user_request_count(user_id, date, requests_count):
logger.info(f"User {user_id} request count updated to {requests_count}")
return(supabase_client.table("users").insert(
{"email": email, "date": date, "requests_count": 1}).execute())
def update_user_request_count(email, date, requests_count):
logger.info(f"User {email} request count updated to {requests_count}")
supabase_client.table("users").update(
{ "requests_count": requests_count}).match({"user_id": user_id, "date": date}).execute()
{ "requests_count": requests_count}).match({"email": email, "date": date}).execute()
def create_chat(user_id, history, chat_name):
# Chat is created upon the user's first question asked
logger.info(f"New chat entry in chats table for user {user_id}")
# Insert a new row into the chats table
new_chat = {
"user_id": user_id,
"history": history, # Empty chat to start
"chat_name": chat_name
}
insert_response = supabase_client.table('chats').insert(new_chat).execute()
logger.info(f"Insert response {insert_response.data}")
return(insert_response)
def update_chat(chat_id, history):
supabase_client.table("chats").update(
{ "history": history}).match({"chat_id": chat_id}).execute()
logger.info(f"Chat {chat_id} updated")
def create_embedding(content):
return embeddings.embed_query(content)
@ -93,3 +122,75 @@ def similarity_search(query, table='match_summaries', top_k=5, threshold=0.5):
def fetch_user_id_from_credentials(commons: CommonsDep,date,credentials):
user = User(email=credentials.get('email', 'none'))
# Fetch the user's UUID based on their email
response = commons['supabase'].from_('users').select('user_id').filter("email", "eq", user.email).execute()
userItem = next(iter(response.data or []), {})
if userItem == {}:
create_user_response = create_user(email= user.email, date=date)
user_id = create_user_response.data[0]['user_id']
else:
user_id = userItem['user_id']
# if not(user_id):
# throw error
return user_id
def get_chat_name_from_first_question(chat_message: ChatMessage):
# Step 1: Get the summary of the first question
# first_question_summary = summerize_as_title(chat_message.question)
# Step 2: Process this summary to create a chat name by selecting the first three words
chat_name = ' '.join(chat_message.question.split()[:3])
print('chat_name')
return chat_name
def get_answer(commons: CommonsDep, chat_message: ChatMessage, email: str, user_openai_api_key:str):
qa = get_qa_llm(chat_message, email, user_openai_api_key)
if chat_message.use_summarization:
# 1. get summaries from the vector store based on question
summaries = similarity_search(
chat_message.question, table='match_summaries')
# 2. evaluate summaries against the question
evaluations = llm_evaluate_summaries(
chat_message.question, summaries, chat_message.model)
# 3. pull in the top documents from summaries
# logger.info('Evaluations: %s', evaluations)
if evaluations:
reponse = commons['supabase'].from_('vectors').select(
'*').in_('id', values=[e['document_id'] for e in evaluations]).execute()
# 4. use top docs as additional context
additional_context = '---\nAdditional Context={}'.format(
'---\n'.join(data['content'] for data in reponse.data)
) + '\n'
model_response = qa(
{"question": additional_context + chat_message.question})
else:
model_response = qa({"question": chat_message.question})
answer = model_response['answer']
# append sources (file_name) to answer
if "source_documents" in answer:
# logger.debug('Source Documents: %s', answer["source_documents"])
sources = [
doc.metadata["file_name"] for doc in answer["source_documents"]
if "file_name" in doc.metadata]
# logger.debug('Sources: %s', sources)
if sources:
files = dict.fromkeys(sources)
# # shall provide file links until pages available
# files = [f"[{f}](/explore/{f})" for f in files]
answer = answer + "\n\nRef: " + "; ".join(files)
return answer

View File

@ -0,0 +1,30 @@
"use client";
import { UUID } from "crypto";
import PageHeading from "../../components/ui/PageHeading";
import { ChatInput, ChatMessages } from "../components";
import useChats from "../hooks/useChats";
interface ChatPageProps {
params?: {
chatId?: UUID;
};
}
export default function ChatPage({ params }: ChatPageProps) {
const chatId: UUID | undefined = params?.chatId;
const { chat, ...others } = useChats(chatId);
return (
<main className="flex flex-col w-full">
<section className="flex flex-col items-center w-full overflow-auto">
<PageHeading
title="Chat with your brain"
subtitle="Talk to a language model about your uploaded data"
/>
{chat && <ChatMessages chat={chat} />}
<ChatInput chatId={chatId} {...others} />
</section>
</main>
);
}

View File

@ -1,40 +0,0 @@
"use client";
import { AnimatePresence } from "framer-motion";
import { FC, useEffect, useRef } from "react";
import ChatMessage from "./ChatMessage";
interface ChatMessagesProps {
history: Array<[string, string]>;
}
const ChatMessages: FC<ChatMessagesProps> = ({ history }) => {
const lastChatRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
lastChatRef.current?.scrollIntoView({ behavior: "auto", block: "start" });
}, [history]);
return (
<div className="space-y-8 grid grid-cols-1 overflow-hidden scrollbar scroll-smooth">
{history.length === 0 ? (
<div className="text-center opacity-50">
Ask a question, or describe a task.
</div>
) : (
<AnimatePresence initial={false}>
{history.map(([speaker, text], idx) => {
return (
<ChatMessage
ref={idx === history.length - 1 ? lastChatRef : null}
key={idx}
speaker={speaker}
text={text}
/>
);
})}
</AnimatePresence>
)}
</div>
);
};
export default ChatMessages;

View File

@ -0,0 +1,14 @@
"use client";
import Link from "next/link";
import { MdSettings } from "react-icons/md";
import Button from "../../../../components/ui/Button";
export function ConfigButton() {
return (
<Link href={"/config"}>
<Button className="px-3" variant={"tertiary"}>
<MdSettings className="text-2xl" />
</Button>
</Link>
);
}

View File

@ -0,0 +1,24 @@
"use client";
import { MdMic, MdMicOff } from "react-icons/md";
import Button from "../../../../components/ui/Button";
import { useSpeech } from "../../../hooks/useSpeech";
export function MicButton() {
const { isListening, speechSupported, startListening } = useSpeech();
return (
<Button
className="px-3"
variant={"tertiary"}
type="button"
onClick={startListening}
disabled={!speechSupported}
>
{isListening ? (
<MdMicOff className="text-2xl" />
) : (
<MdMic className="text-2xl" />
)}
</Button>
);
}

View File

@ -0,0 +1,52 @@
"use client";
import { ChatMessage } from "@/app/chat/types";
import { UUID } from "crypto";
import { Dispatch, SetStateAction } from "react";
import Button from "../../../../components/ui/Button";
import { ConfigButton } from "./ConfigButton";
import { MicButton } from "./MicButton";
interface ChatInputProps {
isSendingMessage: boolean;
sendMessage: (chatId?: UUID, msg?: ChatMessage) => Promise<void>;
setMessage: Dispatch<SetStateAction<ChatMessage>>;
message: ChatMessage;
chatId?: UUID;
}
export function ChatInput({
chatId,
isSendingMessage,
message,
sendMessage,
setMessage,
}: ChatInputProps) {
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (!isSendingMessage) sendMessage(chatId);
}}
className="fixed p-5 bg-white dark:bg-black rounded-t-md border border-black/10 dark:border-white/25 bottom-0 w-full max-w-3xl flex items-center justify-center gap-2 z-20"
>
<textarea
autoFocus
value={message[1]}
onChange={(e) => setMessage((msg) => [msg[0], e.target.value])}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); // Prevents the newline from being entered in the textarea
if (!isSendingMessage) sendMessage(chatId); // Call the submit function here
}
}}
className="w-full p-2 border border-gray-300 dark:border-gray-500 outline-none rounded dark:bg-gray-800"
placeholder="Begin conversation here..."
/>
<Button type="submit" isLoading={isSendingMessage}>
{isSendingMessage ? "Thinking..." : "Chat"}
</Button>
<MicButton />
<ConfigButton />
</form>
);
}

View File

@ -1,7 +1,8 @@
import { cn } from "@/lib/utils"
import { motion } from "framer-motion"
import { forwardRef, Ref } from "react"
import ReactMarkdown from "react-markdown"
"use client";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { forwardRef, Ref } from "react";
import ReactMarkdown from "react-markdown";
const ChatMessage = forwardRef(
(
@ -9,8 +10,8 @@ const ChatMessage = forwardRef(
speaker,
text,
}: {
speaker: string
text: string
speaker: string;
text: string;
},
ref
) => {
@ -26,28 +27,32 @@ const ChatMessage = forwardRef(
exit={{ y: -24, opacity: 0 }}
className={cn(
"py-3 px-3 md:px-6 w-full dark:border-white/25 flex flex-col max-w-4xl overflow-hidden scroll-pt-32",
speaker === "user" ? "" : "bg-gray-200 dark:bg-gray-800 bg-opacity-60 py-8",
speaker === "user"
? ""
: "bg-gray-200 dark:bg-gray-800 bg-opacity-60 py-8"
)}
style={speaker === "user" ? { whiteSpace: "pre-line" } : {}} // Add this line to preserve line breaks
>
<span
className={cn(
"capitalize text-xs bg-sky-200 rounded-xl p-1 px-2 mb-2 w-fit dark:bg-sky-700"
)}>
)}
>
{speaker}
</span>
<>
<ReactMarkdown
// remarkRehypeOptions={{}}
className="prose dark:prose-invert ml-[6px] mt-1">
className="prose dark:prose-invert ml-[6px] mt-1"
>
{text}
</ReactMarkdown>
</>
</motion.div>
)
);
}
)
);
ChatMessage.displayName = "ChatMessage"
ChatMessage.displayName = "ChatMessage";
export default ChatMessage
export default ChatMessage;

View File

@ -0,0 +1,44 @@
"use client";
import Card from "@/app/components/ui/Card";
import { AnimatePresence } from "framer-motion";
import { useEffect, useRef } from "react";
import { Chat } from "../../types";
import ChatMessage from "./ChatMessage";
interface ChatMessagesProps {
chat: Chat;
}
export const ChatMessages = ({ chat }: ChatMessagesProps): JSX.Element => {
const lastChatRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
lastChatRef.current?.scrollIntoView({ behavior: "auto", block: "start" });
}, [chat]);
return (
<Card className="p-5 max-w-3xl w-full flex-1 flex flex-col mb-8">
<div className="">
{chat.history.length === 0 ? (
<div className="text-center opacity-50">
Ask a question, or describe a task.
</div>
) : (
<AnimatePresence initial={false}>
{chat.history.map(([speaker, text], idx) => {
return (
<ChatMessage
ref={idx === chat.history.length - 1 ? lastChatRef : null}
key={idx}
speaker={speaker}
text={text}
/>
);
})}
</AnimatePresence>
)}
</div>
</Card>
);
};
export default ChatMessages;

View File

@ -0,0 +1,59 @@
import { cn } from "@/lib/utils";
import { UUID } from "crypto";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { FC } from "react";
import { FiTrash2 } from "react-icons/fi";
import { MdChatBubbleOutline } from "react-icons/md";
import { Chat } from "../../types";
interface ChatsListItemProps {
chat: Chat;
deleteChat: (id: UUID) => void;
}
const ChatsListItem: FC<ChatsListItemProps> = ({ chat, deleteChat }) => {
const pathname = usePathname()?.split("/").at(-1);
const selected = chat.chatId === pathname;
return (
<div
className={cn(
"w-full border-b border-black/10 dark:border-white/25 last:border-none relative group flex overflow-x-hidden hover:bg-gray-100 dark:hover:bg-gray-800",
selected ? "bg-gray-100 text-primary" : ""
)}
>
<Link
className="flex flex-col flex-1 min-w-0 p-4"
href={`/chat/${chat.chatId}`}
key={chat.chatId}
>
<div className="flex items-center gap-2">
<MdChatBubbleOutline className="text-xl" />
<p className="min-w-0 flex-1 whitespace-nowrap">{chat.chatName}</p>
</div>
<div className="grid-cols-2 text-xs opacity-50 whitespace-nowrap">
{chat.chatId}
</div>
</Link>
<div className="opacity-0 group-hover:opacity-100 flex items-center justify-center hover:text-red-700 bg-gradient-to-l from-white dark:from-black to-transparent z-10 transition-opacity">
<button
className="p-5"
type="button"
onClick={() => deleteChat(chat.chatId)}
>
<FiTrash2 />
</button>
</div>
{/* Fade to white */}
<div
aria-hidden
className="not-sr-only absolute left-1/2 top-0 bottom-0 right-0 bg-gradient-to-r from-transparent to-white dark:to-black pointer-events-none"
></div>
</div>
);
};
export default ChatsListItem;

View File

@ -0,0 +1,11 @@
import Link from "next/link";
import { BsPlusSquare } from "react-icons/bs";
export const NewChatButton = () => (
<Link
href="/chat"
className="px-4 py-2 mx-4 my-2 border border-primary hover:text-white hover:bg-primary shadow-lg rounded-lg flex items-center justify-center"
>
<BsPlusSquare className="h-6 w-6 mr-2" /> New Chat
</Link>
);

View File

@ -0,0 +1,21 @@
"use client";
import useChats from "../../hooks/useChats";
import ChatsListItem from "./ChatsListItem";
import { NewChatButton } from "./NewChatButton";
export function ChatsList() {
const { allChats, deleteChat } = useChats();
return (
<aside className="h-screen bg-white dark:bg-black max-w-xs w-full border-r border-black/10 dark:border-white/25 ">
<NewChatButton />
<div className="flex flex-col gap-0">
{allChats.map((chat) => (
<ChatsListItem
key={chat.chatId}
chat={chat}
deleteChat={deleteChat}
/>
))}
</div>
</aside>
);
}

View File

@ -0,0 +1,4 @@
export * from "./ChatMessages/ChatInput";
export * from "./ChatMessages/ChatMessage";
export * from "./ChatMessages";
export * from "./ChatsList";

View File

@ -0,0 +1,168 @@
import { useBrainConfig } from "@/lib/context/BrainConfigProvider/hooks/useBrainConfig";
import { useToast } from "@/lib/hooks/useToast";
import { useAxios } from "@/lib/useAxios";
import { UUID } from "crypto";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { Chat, ChatMessage } from "../types";
export default function useChats(chatId?: UUID) {
const [allChats, setAllChats] = useState<Chat[]>([]);
const [chat, setChat] = useState<Chat | null>(null);
const [isSendingMessage, setIsSendingMessage] = useState(false);
const [message, setMessage] = useState<ChatMessage>(["", ""]); // for optimistic updates
const { axiosInstance } = useAxios();
const {
config: { maxTokens, model, temperature },
} = useBrainConfig();
const router = useRouter();
const { publish } = useToast();
const fetchAllChats = useCallback(async () => {
try {
console.log("Fetching all chats");
const response = await axiosInstance.get<{
chats: Chat[];
}>(`/chat`);
setAllChats(response.data.chats);
console.log("Fetched all chats");
} catch (error) {
console.error(error);
publish({
variant: "danger",
text: "Error occured while fetching your chats",
});
}
}, []);
const fetchChat = useCallback(async (chatId?: UUID) => {
if (!chatId) return;
try {
console.log(`Fetching chat ${chatId}`);
const response = await axiosInstance.get<Chat>(`/chat/${chatId}`);
console.log(response.data);
setChat(response.data);
} catch (error) {
console.error(error);
publish({
variant: "danger",
text: `Error occured while fetching ${chatId}`,
});
}
}, []);
type ChatResponse = Omit<Chat, "chatId"> & { chatId: UUID | undefined };
const createChat = ({
options,
}: {
options: Record<string, string | unknown>;
}) => {
return axiosInstance.post<ChatResponse>(`/chat`, options);
};
const updateChat = ({
options,
}: {
options: Record<string, string | unknown>;
}) => {
return axiosInstance.put<ChatResponse>(`/chat/${options.chat_id}`, options);
};
const sendMessage = async (chatId?: UUID, msg?: ChatMessage) => {
setIsSendingMessage(true);
// const chat_id = {
// ...((chatId || currentChatId) && {
// chat_id: chatId ?? currentChatId,
// }),
// };
if (msg) setMessage(msg);
const options: Record<string, unknown> = {
// ...(chat_id && { chat_id }),
// chat_id gets set only if either chatId or currentChatId exists, by the priority of chatId
chat_id: chatId,
model,
question: msg ? msg[1] : message[1],
history: chat ? chat.history : [],
temperature,
max_tokens: maxTokens,
use_summarization: false,
};
const response = await (chatId !== undefined
? updateChat({ options })
: createChat({ options }));
// response.data.chatId can be undefined when the max number of requests has reached
if (!response.data.chatId) {
publish({
text: "You have reached max number of requests.",
variant: "danger",
});
setMessage(["", ""]);
setIsSendingMessage(false);
return;
}
const newChat = {
chatId: response.data.chatId,
history: response.data.history,
chatName: response.data.chatName,
};
if (!chatId) {
// Creating a new chat
// setAllChats((chats) => {
// console.log({ chats });
// return [...chats, newChat];
// });
console.log("---- Creating a new chat ----");
setChat(newChat);
router.push(`/chat/${response.data.chatId}`);
}
setChat(newChat);
setMessage(["", ""]);
setIsSendingMessage(false);
};
const deleteChat = async (chatId: UUID) => {
try {
await axiosInstance.delete(`/chat/${chatId}`);
setAllChats((chats) => chats.filter((chat) => chat.chatId !== chatId));
// TODO: Change route only when the current chat is being deleted
router.push("/chat");
publish({
text: `Chat sucessfully deleted. Id: ${chatId}`,
variant: "success",
});
} catch (error) {
console.error("Error deleting chat:", error);
publish({ text: `Error deleting chat: ${error}`, variant: "danger" });
}
};
useEffect(() => {
fetchAllChats();
console.log(chatId);
if (chatId) {
fetchChat(chatId);
}
}, [fetchAllChats, fetchChat, chatId]);
return {
allChats,
chat,
isSendingMessage,
message,
setMessage,
fetchAllChats,
fetchChat,
deleteChat,
sendMessage,
};
}

View File

@ -1,66 +0,0 @@
import { useEffect, useState } from "react";
import { useSupabase } from "@/app/supabase-provider";
import { useBrainConfig } from "@/lib/context/BrainConfigProvider/hooks/useBrainConfig";
import { useAxios } from "@/lib/useAxios";
import { redirect } from "next/navigation";
export const useQuestion = () => {
const [question, setQuestion] = useState("");
const [history, setHistory] = useState<Array<[string, string]>>([]);
const [isPending, setIsPending] = useState(false);
const { session } = useSupabase();
const { axiosInstance } = useAxios();
const {
config: { maxTokens, model, temperature },
} = useBrainConfig();
if (session === null) {
redirect("/login");
}
useEffect(() => {
// Check if history exists in local storage. If it does, fetch it and set it as history
(async () => {
const localHistory = localStorage.getItem("history");
if (localHistory) {
setHistory(JSON.parse(localHistory));
}
})();
}, []);
const askQuestion = async () => {
setHistory((hist) => [...hist, ["user", question]]);
setIsPending(true);
try {
const response = await axiosInstance.post(`/chat/`, {
model,
question,
history,
temperature,
max_tokens: maxTokens,
});
setHistory(response.data.history);
localStorage.setItem("history", JSON.stringify(response.data.history));
setQuestion("");
} catch (error) {
console.error(error);
} finally {
setIsPending(false);
}
};
const resetHistory = () => {
localStorage.setItem("history", JSON.stringify([]));
setHistory([]);
};
return {
isPending,
history,
question,
setQuestion,
resetHistory,
askQuestion,
};
};

View File

@ -1,11 +1,12 @@
import { useEffect, useState } from "react";
import { isSpeechRecognitionSupported } from "../helpers/isSpeechRecognitionSupported";
import { useQuestion } from "./useQuestion";
import useChats from "./useChats";
export const useSpeech = () => {
const [isListening, setIsListening] = useState(false);
const [speechSupported, setSpeechSupported] = useState(false);
const { setQuestion } = useQuestion();
const { setMessage } = useChats();
useEffect(() => {
if (isSpeechRecognitionSupported()) {
@ -35,7 +36,7 @@ export const useSpeech = () => {
mic.onresult = (event: SpeechRecognitionEvent) => {
const interimTranscript =
event.results[event.results.length - 1][0].transcript;
setQuestion((prevQuestion) => prevQuestion + interimTranscript);
setMessage((prevMessage) => ["user", prevMessage + interimTranscript]);
};
if (isListening) {
@ -48,7 +49,7 @@ export const useSpeech = () => {
}
};
}
}, [isListening, setQuestion]);
}, [isListening, setMessage]);
const startListening = () => {
setIsListening((prevIsListening) => !prevIsListening);

View File

@ -0,0 +1,25 @@
"use client";
import { redirect } from "next/navigation";
import { FC, ReactNode } from "react";
import { useSupabase } from "../supabase-provider";
import { ChatsList } from "./components";
interface LayoutProps {
children?: ReactNode;
}
const Layout: FC<LayoutProps> = ({ children }) => {
const { session } = useSupabase();
if (!session) redirect("/login");
return (
<div className="relative h-full w-full flex pt-20">
<div className="h-full">
<ChatsList />
</div>
{children}
</div>
);
};
export default Layout;

View File

@ -1,102 +1,4 @@
"use client";
import Link from "next/link";
import {
MdAutorenew,
MdMic,
MdMicOff,
MdSend,
MdSettings,
} from "react-icons/md";
import Button from "../components/ui/Button";
import Card from "../components/ui/Card";
import PageHeading from "../components/ui/PageHeading";
import ChatMessages from "./components/ChatMessages";
import { useQuestion } from "./hooks/useQuestion";
import { useSpeech } from "./hooks/useSpeech";
import ChatPage from "./[chatId]/page";
export default function ChatPage() {
const {
history,
isPending,
question,
askQuestion,
setQuestion,
resetHistory,
} = useQuestion();
const { isListening, speechSupported, startListening } = useSpeech();
return (
<main className="min-h-0 w-full flex flex-col pt-32 flex-1 overflow-hidden">
<section className="flex flex-col justify-center items-center gap-5 h-full overflow-auto style={{ marginBottom: '20px'}}">
<PageHeading
title="Chat with your brain"
subtitle="Talk to a language model about your uploaded data"
/>
{/* Chat */}
<Card className="py-4 max-w-3xl w-full flex-1 md:mb-24 overflow-auto flex flex-col hover:shadow-none shadow-none ">
<ChatMessages history={history} />
</Card>
<Card className="md:fixed md:rounded md:left-1/2 w-full max-w-3xl bg-gray-100 dark:bg-gray-800 md:-translate-x-1/2 md:bottom-16 px-5 py-5 md:mb-5 hover:shadow-none shadow-none">
<form
onSubmit={(e) => {
e.preventDefault();
if (!isPending) askQuestion();
}}
className="w-full flex flex-col md:flex-row items-center justify-center gap-2 "
>
<div className="flex gap-1 w-full">
<input
autoFocus
value={question}
onChange={(e) => setQuestion(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); // Prevents the newline from being entered in the textarea
if (!isPending) askQuestion(); // Call the submit function here
}
}}
className="w-full p-2 border border-gray-300 dark:border-gray-500 outline-none rounded dark:bg-gray-800"
placeholder="Begin conversation here..."
/>
<Button type="submit" isLoading={isPending}>
{isPending ? "" : <MdSend />}
</Button>
</div>
<div className="flex">
{/* Reset Button */}
<Button
className="px-3"
variant={"tertiary"}
type="button"
onClick={resetHistory}
disabled={isPending}
>
<MdAutorenew className="text-2xl" />
</Button>
{/* Mic Button */}
<Button
className="px-3"
variant={"tertiary"}
type="button"
onClick={startListening}
disabled={!speechSupported}
>
{isListening ? (
<MdMicOff className="text-2xl" />
) : (
<MdMic className="text-2xl" />
)}
</Button>
<Link href={"/config"}>
<Button className="px-3" variant={"tertiary"}>
<MdSettings className="text-2xl" />
</Button>
</Link>
</div>
</form>
</Card>
</section>
</main>
);
}
export default ChatPage;

View File

@ -0,0 +1,10 @@
import { UUID } from "crypto";
export interface Chat {
chatId: UUID;
chatName: string;
history: ChatHistory;
}
export type ChatMessage = [string, string];
export type ChatHistory = ChatMessage[];

View File

@ -55,7 +55,7 @@ export const NavItems: FC<NavItemsProps> = ({
{isUserLoggedIn && (
<>
<Link aria-label="account" className="" href={"/user"}>
<MdPerson />
<MdPerson className="text-2xl" />
</Link>
<Link href={"/config"}>
<Button

View File

@ -0,0 +1,7 @@
create table if not exists chats(
chat_id uuid default uuid_generate_v4() primary key,
user_id uuid references users(user_id),
creation_time timestamp default current_timestamp,
history jsonb,
chat_name text
);

View File

@ -1,5 +1,6 @@
create table if not exists users(
user_id uuid,
email text,
date text,
user_id text,
requests_count int
);
);

View File

@ -0,0 +1,13 @@
BEGIN;
-- Create a new column for email and copy the current user_id to it
ALTER TABLE users ADD COLUMN email text;
UPDATE users SET email = user_id;
-- Drop the current user_id column
ALTER TABLE users DROP COLUMN user_id;
-- Create a new UUID user_id column and set it as the primary key
ALTER TABLE users ADD COLUMN user_id UUID DEFAULT gen_random_uuid() PRIMARY KEY;
COMMIT;