mirror of
https://github.com/StanGirard/quivr.git
synced 2024-12-24 03:41:56 +03:00
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:
parent
5a0f8326df
commit
4112699db5
243
backend/main.py
243
backend/main.py
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"}
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
0
backend/routes/__init__.py
Normal file
0
backend/routes/__init__.py
Normal file
120
backend/routes/chat_routes.py
Normal file
120
backend/routes/chat_routes.py
Normal 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'] }
|
56
backend/routes/crawl_routes.py
Normal file
56
backend/routes/crawl_routes.py
Normal 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'])
|
40
backend/routes/explore_routes.py
Normal file
40
backend/routes/explore_routes.py
Normal 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}
|
7
backend/routes/misc_routes.py
Normal file
7
backend/routes/misc_routes.py
Normal file
@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
misc_router = APIRouter()
|
||||
|
||||
@misc_router.get("/")
|
||||
async def root():
|
||||
return {"status": "OK"}
|
44
backend/routes/upload_routes.py
Normal file
44
backend/routes/upload_routes.py
Normal 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
|
||||
|
51
backend/routes/user_routes.py
Normal file
51
backend/routes/user_routes.py
Normal 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,
|
||||
}
|
||||
|
@ -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"}
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
30
frontend/app/chat/[chatId]/page.tsx
Normal file
30
frontend/app/chat/[chatId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
44
frontend/app/chat/components/ChatMessages/index.tsx
Normal file
44
frontend/app/chat/components/ChatMessages/index.tsx
Normal 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;
|
59
frontend/app/chat/components/ChatsList/ChatsListItem.tsx
Normal file
59
frontend/app/chat/components/ChatsList/ChatsListItem.tsx
Normal 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;
|
11
frontend/app/chat/components/ChatsList/NewChatButton.tsx
Normal file
11
frontend/app/chat/components/ChatsList/NewChatButton.tsx
Normal 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>
|
||||
);
|
21
frontend/app/chat/components/ChatsList/index.tsx
Normal file
21
frontend/app/chat/components/ChatsList/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
4
frontend/app/chat/components/index.ts
Normal file
4
frontend/app/chat/components/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./ChatMessages/ChatInput";
|
||||
export * from "./ChatMessages/ChatMessage";
|
||||
export * from "./ChatMessages";
|
||||
export * from "./ChatsList";
|
168
frontend/app/chat/hooks/useChats.ts
Normal file
168
frontend/app/chat/hooks/useChats.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -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,
|
||||
};
|
||||
};
|
@ -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);
|
||||
|
25
frontend/app/chat/layout.tsx
Normal file
25
frontend/app/chat/layout.tsx
Normal 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;
|
@ -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;
|
||||
|
10
frontend/app/chat/types.ts
Normal file
10
frontend/app/chat/types.ts
Normal 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[];
|
@ -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
|
||||
|
7
scripts/supabase_chats_table.sql
Normal file
7
scripts/supabase_chats_table.sql
Normal 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
|
||||
);
|
@ -1,5 +1,6 @@
|
||||
create table if not exists users(
|
||||
user_id uuid,
|
||||
email text,
|
||||
date text,
|
||||
user_id text,
|
||||
requests_count int
|
||||
);
|
||||
);
|
||||
|
13
supabase/migrations/20230606131110_add_uuid_user_id.sql
Normal file
13
supabase/migrations/20230606131110_add_uuid_user_id.sql
Normal 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;
|
Loading…
Reference in New Issue
Block a user