diff --git a/.github/workflows/node-js-tests.yml b/.github/workflows/node-js-tests.yml index 5a18245..fa5c86a 100644 --- a/.github/workflows/node-js-tests.yml +++ b/.github/workflows/node-js-tests.yml @@ -31,10 +31,16 @@ jobs: cache-dependency-path: ./frontend/package-lock.json - name: Install Dependencies working-directory: ./frontend - run: npm ci - - name: Build Package + run: npm install + - name: Disable Next.js Telemetry + working-directory: ./frontend + run: npx next telemetry disable + - name: Test Build Package working-directory: ./frontend run: npm run build --if-present + env: + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} # - name: Run Test # working-directory: ./frontend # run: npm test diff --git a/README.md b/README.md index 8f11fa7..e44b83e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ emoji: 📝 colorFrom: blue colorTo: indigo sdk: docker -python_version: 3.11.4 +python_version: 3.11.8 app_port: 8000 pinned: false --- @@ -74,7 +74,7 @@ For more information, see the [DEPLOYMENT.md](./DEPLOYMENT.md). - [Python](https://python.org/) - Backend Server Environment - [FastAPI](https://fastapi.tiangolo.com/) - Backend API Web Framework - [LlamaIndex](https://www.llamaindex.ai/) - Data Framework for LLM -- [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) - LlamaIndex Application Bootstrap Tool +- [create-llama](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) - LlamaIndex Application Bootstrap Tool ## 📑 Contributing diff --git a/backend/.gitignore b/backend/.gitignore index 0fb5f02..fbd6b05 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,10 @@ +# Production Files __pycache__ storage -.env \ No newline at end of file +.env +**/data/EIR/ + +# Dev Folders +test.py +storage backup +data backup \ No newline at end of file diff --git a/backend/backend/app/api/routers/chat.py b/backend/backend/app/api/routers/chat.py index 2a32a1d..17cc4ed 100644 --- a/backend/backend/app/api/routers/chat.py +++ b/backend/backend/app/api/routers/chat.py @@ -4,17 +4,17 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import StreamingResponse from fastapi.websockets import WebSocketDisconnect -from llama_index import VectorStoreIndex from llama_index.llms.base import ChatMessage from llama_index.llms.types import MessageRole from llama_index.memory import ChatMemoryBuffer from llama_index.prompts import PromptTemplate from pydantic import BaseModel +from backend.app.utils import auth from backend.app.utils.index import get_index from backend.app.utils.json import json_to_model -chat_router = r = APIRouter() +chat_router = r = APIRouter(dependencies=[Depends(auth.validate_user)]) """ This router is for chatbot functionality which consist of chat memory and chat engine. @@ -34,6 +34,7 @@ class _Message(BaseModel): class _ChatData(BaseModel): messages: List[_Message] + document: str # custom prompt template to be used by chat engine @@ -69,8 +70,13 @@ async def chat( # Note: To support clients sending a JSON object using content-type "text/plain", # we need to use Depends(json_to_model(_ChatData)) here data: _ChatData = Depends(json_to_model(_ChatData)), - index: VectorStoreIndex = Depends(get_index), ): + logger = logging.getLogger("uvicorn") + # get the document set selected from the request body + document_set = data.document + logger.info(f"Document Set: {document_set}") + # get the index for the selected document set + index = get_index(collection_name=document_set) # check preconditions and get last message if len(data.messages) == 0: raise HTTPException( @@ -92,8 +98,6 @@ async def chat( for m in data.messages ] - logger = logging.getLogger("uvicorn") - # query_engine = index.as_query_engine() # chat_engine = CondenseQuestionChatEngine.from_defaults( # query_engine=query_engine, @@ -102,9 +106,11 @@ async def chat( # verbose=True, # ) + logger.info(f"Messages: {messages}") + memory = ChatMemoryBuffer.from_defaults( chat_history=messages, - token_limit=2000, + token_limit=3900, ) logger.info(f"Memory: {memory.get()}") @@ -114,17 +120,23 @@ async def chat( chat_mode="condense_plus_context", memory=memory, context_prompt=( - "You are a chatbot, able to have normal interactions, as well as talk" - " about information from documents regarding Public Sector Standard Conditions Of Contract (PSSCOC)." + "You are a helpful chatbot, able to have normal interactions, as well as answer questions" + " regarding information relating to the Public Sector Standard Conditions Of Contract (PSSCOC) Documents and JTC's Employer Information Requirements (EIR) Documents.\n" + "All the documents are in the context of the construction industry in Singapore.\n" "Here are the relevant documents for the context:\n" "{context_str}" - "\nInstruction: Based on the above documents, provide a detailed answer for the user question below. If you cannot answer the question, inform the user that you do not know." + "\nInstruction: Based on the above documents, provide a detailed answer for the user question below.\n" + "If you cannot answer the question or are unsure of how to answer, inform the user that you do not know.\n" + "If you need to clarify the question, ask the user for clarification.\n" + "You are to provide the relevant sources of which you got the information from in the context in brackets." ), ) response = chat_engine.stream_chat( message=lastMessage.content, chat_history=messages ) + # logger.info(f"Response Sources: {response.source_nodes}") + # stream response async def event_generator(): try: diff --git a/backend/backend/app/api/routers/healthcheck.py b/backend/backend/app/api/routers/healthcheck.py index 674b8cf..603fedc 100644 --- a/backend/backend/app/api/routers/healthcheck.py +++ b/backend/backend/app/api/routers/healthcheck.py @@ -1,6 +1,12 @@ -from fastapi import APIRouter, Request +import logging +import os -healthcheck_router = r = APIRouter() +from fastapi import APIRouter, Depends, Request +from supabase import Client, ClientOptions, create_client + +from backend.app.utils import auth + +healthcheck_router = r = APIRouter(dependencies=[Depends(auth.validate_user)]) """ This router is for healthcheck functionality. @@ -19,14 +25,37 @@ async def healthcheck( # else: # results["index"] = False + logger = logging.getLogger("uvicorn") + # TODO: check if other services are ready - # logger.info("Healthcheck: {results}") + # Try to connect to supabase + supabase_url: str = os.environ.get("SUPABASE_URL") + supabase_key: str = os.environ.get("SUPABASE_ANON_KEY") + + supabase: Client = create_client( + supabase_url=supabase_url, + supabase_key=supabase_key, + options=ClientOptions( + postgrest_client_timeout=10, + storage_client_timeout=10, + ), + ) + + response = supabase.table("users").select("id", count="exact").execute() + + # logger.info(f"Supabase: {response}") + + if response.count is not None: + results["supabase"] = True + else: + results["supabase"] = False - results = {"status": "OK"} + results["backend"] = True + logger.debug(f"Healthcheck: {results}") return results # Simple test to check if the healthcheck endpoint is working def test_healthcheck(): - assert healthcheck() == {"status": "OK"} + assert healthcheck() == {"status": True, "supabase": True} diff --git a/backend/backend/app/api/routers/query.py b/backend/backend/app/api/routers/query.py index 2defacb..3386c1f 100644 --- a/backend/backend/app/api/routers/query.py +++ b/backend/backend/app/api/routers/query.py @@ -8,10 +8,11 @@ from llama_index.llms.types import MessageRole from pydantic import BaseModel +from backend.app.utils import auth from backend.app.utils.index import get_index from backend.app.utils.json import json_to_model -query_router = r = APIRouter() +query_router = r = APIRouter(dependencies=[Depends(auth.validate_user)]) """ This router is for query functionality which consist of query engine. diff --git a/backend/backend/app/api/routers/search.py b/backend/backend/app/api/routers/search.py index 938be3b..bf7efe1 100644 --- a/backend/backend/app/api/routers/search.py +++ b/backend/backend/app/api/routers/search.py @@ -6,9 +6,10 @@ from llama_index.postprocessor import SimilarityPostprocessor from llama_index.retrievers import VectorIndexRetriever +from backend.app.utils import auth from backend.app.utils.index import get_index -search_router = r = APIRouter() +search_router = r = APIRouter(dependencies=[Depends(auth.validate_user)]) """ This router is for search functionality which consist of query engine. @@ -22,8 +23,9 @@ async def search( request: Request, index: VectorStoreIndex = Depends(get_index), + query: str = None, ): - query = request.query_params.get("query") + # query = request.query_params.get("query") logger = logging.getLogger("uvicorn") logger.info(f"Search: {query}") if query is None: diff --git a/backend/backend/app/utils/auth.py b/backend/backend/app/utils/auth.py new file mode 100644 index 0000000..c39db90 --- /dev/null +++ b/backend/backend/app/utils/auth.py @@ -0,0 +1,141 @@ +import logging +import os +import time +from typing import Dict + +import jwt +from dotenv import load_dotenv +from fastapi import HTTPException, Security +from fastapi.security import APIKeyHeader +from supabase import Client, ClientOptions, create_client + +load_dotenv() + +# Retrieve the API key header name from the environment +API_AUTH_HEADER_NAME: str = os.getenv( + key="API_AUTH_HEADER_NAME", default="Authorization" +) + +# Retrieve the Backend API key header from the environment +API_KEY_HEADER_NAME: str = os.getenv(key="API_KEY_HEADER_NAME", default="X-API-Key") + +# Create an API key header instance +API_AUTH_HEADER = APIKeyHeader(name=API_AUTH_HEADER_NAME, auto_error=False) + +# Retrieve the API key from the environment +BACKEND_API_KEY: str = os.getenv(key="BACKEND_API_KEY") + +# Create an API key header instance +API_KEY_HEADER = APIKeyHeader(name=API_KEY_HEADER_NAME, auto_error=False) + +JWT_SECRET: str = os.getenv("SUPABASE_JWT_SECRET") +JWT_ALGORITHM = "HS256" + + +def verify_jwt(jwtoken: str) -> bool: + """Verify the JWT token and return True if the token is valid, else return False""" + isTokenValid: bool = False + + try: + payload = decodeJWT(jwtoken) + except Exception: + payload = None + if payload: + isTokenValid = True + return isTokenValid + + +def decodeJWT(token: str) -> Dict: + """Decode the JWT token and return the payload if the token is valid, else return None""" + try: + decoded_token = jwt.decode( + token, JWT_SECRET, algorithms=[JWT_ALGORITHM], options={"verify_aud": False} + ) + return decoded_token if decoded_token["exp"] >= time.time() else None + except Exception: + return None + + +def get_user_from_JWT(token: str): + """Get the user id from the JWT token and return True if the user exists, else return False""" + supabase_url: str = os.environ.get("SUPABASE_URL") + supabase_key: str = os.environ.get("SUPABASE_ANON_KEY") + + supabase: Client = create_client( + supabase_url=supabase_url, + supabase_key=supabase_key, + options=ClientOptions( + postgrest_client_timeout=10, + storage_client_timeout=10, + ), + ) + + payload = decodeJWT(token) + user_id = payload["sub"] + + if user_id is not None: + # Try to get the user from the database using the user_id + response = supabase.table("users").select("*").eq("id", user_id).execute() + # print(response.data) + if len(response.data) == 0: + return False + return True + return False + + +async def validate_user( + auth_token: str = Security(dependency=API_AUTH_HEADER), + api_key: str = Security(dependency=API_KEY_HEADER), +): + try: + logger = logging.getLogger("uvicorn") + # logger.debug(f"Auth Token: {auth_token} | API Key: {api_key}") + if auth_token is not None or api_key is not None: + # If the access token is empty, use the 'X-API-Key' from the header + if auth_token is None: + # Access the 'X-API-Key' header directly + if BACKEND_API_KEY is None: + raise ValueError("Backend API key is not set in Backend Service!") + # If the 'X-API-Key' does not match the backend API key, raise an error + if api_key != BACKEND_API_KEY: + raise ValueError( + "Invalid API key provided in the 'X-API-Key' header!" + ) + else: + logger.info("Validated API key successfully!") + return None + else: + auth_token = ( + auth_token.strip() + ) # Remove leading and trailing whitespaces + isBearer = auth_token.startswith( + "Bearer" + ) # Check if the token starts with 'Bearer' + jwtoken = auth_token.split("Bearer ")[ + 1 + ] # Extract the token from the 'Bearer' string + if JWT_SECRET is None: + raise ValueError( + "Supabase JWT Secret is not set in Backend Service!" + ) + if not isBearer: + return ( + "Invalid token scheme. Please use the format 'Bearer [token]'" + ) + # Verify the JWT token is valid + if verify_jwt(jwtoken=jwtoken) is None: + return "Invalid token. Please provide a valid token." + # Check if the user exists in the database + if get_user_from_JWT(token=jwtoken): + logger.info("Validated User's Auth Token successfully!") + return None + else: + raise ValueError("User does not exist in the database!") + else: + raise ValueError( + "Either Access token [Authorization] or API key [X-API-Key] needed!" + ) + except Exception as e: + logger = logging.getLogger("uvicorn") + logger.error(f"Error validating Auth Token / API key: {e}") + raise HTTPException(status_code=401, detail=f"Unauthorized - {e}") diff --git a/backend/backend/app/utils/contants.py b/backend/backend/app/utils/contants.py index 65263f3..e148be2 100644 --- a/backend/backend/app/utils/contants.py +++ b/backend/backend/app/utils/contants.py @@ -30,6 +30,11 @@ # Embedding Model Constants EMBED_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2" EMBED_POOLING = "mean" +EMBED_MODEL_DIMENSIONS = 384 # MiniLM-L6-v2 uses 384 dimensions +DEF_EMBED_MODEL_DIMENSIONS = ( + 1536 # Default embedding model dimensions used by OpenAI text-embedding-ada-002 +) +EMBED_BATCH_SIZE = 100 # batch size for openai embeddings # Prompt Helper Constants # set maximum input size diff --git a/backend/backend/app/utils/index.py b/backend/backend/app/utils/index.py index c47ed6e..6316d79 100644 --- a/backend/backend/app/utils/index.py +++ b/backend/backend/app/utils/index.py @@ -1,9 +1,11 @@ import logging import os +from dotenv import load_dotenv from llama_index import ( PromptHelper, ServiceContext, + # Document, SimpleDirectoryReader, StorageContext, VectorStoreIndex, @@ -11,11 +13,14 @@ set_global_service_context, ) from llama_index.embeddings import HuggingFaceEmbedding -from llama_index.llms import LlamaCPP +from llama_index.embeddings.openai import OpenAIEmbedding +from llama_index.llms import LlamaCPP, OpenAI from llama_index.llms.llama_utils import ( completion_to_prompt, messages_to_prompt, ) +from llama_index.vector_stores.supabase import SupabaseVectorStore +from vecs import IndexMeasure from backend.app.utils.contants import ( CHUNK_OVERLAP, @@ -24,7 +29,10 @@ CHUNK_SIZE_LIMIT, CONTEXT_SIZE, DATA_DIR, + DEF_EMBED_MODEL_DIMENSIONS, DEVICE_TYPE, + EMBED_BATCH_SIZE, + EMBED_MODEL_DIMENSIONS, EMBED_MODEL_NAME, EMBED_POOLING, LLM_MODEL_URL, @@ -35,91 +43,201 @@ STORAGE_DIR, ) -llm = LlamaCPP( - model_url=LLM_MODEL_URL, - temperature=LLM_TEMPERATURE, - max_new_tokens=MAX_NEW_TOKENS, - context_window=CONTEXT_SIZE, - # kwargs to pass to __call__() - generate_kwargs={}, - # kwargs to pass to __init__() - model_kwargs=MODEL_KWARGS, - # transform inputs into Llama2 format - messages_to_prompt=messages_to_prompt, - completion_to_prompt=completion_to_prompt, - verbose=True, -) +# from llama_index.vector_stores.supabase import SupabaseVectorStore +# import textwrap -embed_model = HuggingFaceEmbedding( - model_name=EMBED_MODEL_NAME, - pooling=EMBED_POOLING, - device=DEVICE_TYPE, -) +load_dotenv() +logger = logging.getLogger("uvicorn") -prompt_helper = PromptHelper( - chunk_size_limit=CHUNK_SIZE_LIMIT, - chunk_overlap_ratio=CHUNK_OVERLAP_RATIO, - num_output=NUM_OUTPUT, -) +# ENV variables +USE_LOCAL_LLM = bool(os.getenv("USE_LOCAL_LLM").lower() == "true") +USE_LOCAL_VECTOR_STORE = bool(os.getenv("USE_LOCAL_VECTOR_STORE").lower() == "true") -service_context = ServiceContext.from_defaults( - llm=llm, - embed_model=embed_model, - chunk_size=CHUNK_SIZE, - chunk_overlap=CHUNK_OVERLAP, - prompt_helper=prompt_helper, -) -set_global_service_context(service_context) +# use local LLM if USE_LOCAL_LLM is set to True, else use openai's API +if USE_LOCAL_LLM: + logger.info("Using local LLM...") + llm = LlamaCPP( + model_url=LLM_MODEL_URL, + temperature=LLM_TEMPERATURE, + max_new_tokens=MAX_NEW_TOKENS, + context_window=CONTEXT_SIZE, + # kwargs to pass to __call__() + generate_kwargs={}, + # kwargs to pass to __init__() + model_kwargs=MODEL_KWARGS, + # transform inputs into Llama2 format + messages_to_prompt=messages_to_prompt, + completion_to_prompt=completion_to_prompt, + verbose=True, + ) + embed_model = HuggingFaceEmbedding( + model_name=EMBED_MODEL_NAME, + pooling=EMBED_POOLING, + device=DEVICE_TYPE, + ) + + prompt_helper = PromptHelper( + chunk_size_limit=CHUNK_SIZE_LIMIT, + chunk_overlap_ratio=CHUNK_OVERLAP_RATIO, + num_output=NUM_OUTPUT, + ) + + service_context = ServiceContext.from_defaults( + llm=llm, + embed_model=embed_model, + chunk_size=CHUNK_SIZE, + chunk_overlap=CHUNK_OVERLAP, + prompt_helper=prompt_helper, + ) + + set_global_service_context(service_context) +else: + logger.info("Using OpenAI's API...") + llm = OpenAI( + model="gpt-3.5-turbo", + temperature=0.2, + api_key=os.getenv("OPENAI_API_KEY"), + ) + # By default, LlamaIndex uses text-embedding-ada-002 from OpenAI + embed_model = OpenAIEmbedding(embed_batch_size=EMBED_BATCH_SIZE) + + prompt_helper = PromptHelper( + chunk_size_limit=CHUNK_SIZE_LIMIT, + chunk_overlap_ratio=CHUNK_OVERLAP_RATIO, + num_output=NUM_OUTPUT, + ) + + service_context = ServiceContext.from_defaults( + llm=llm, + embed_model=embed_model, + chunk_size=CHUNK_SIZE, + chunk_overlap=CHUNK_OVERLAP, + prompt_helper=prompt_helper, + ) + + set_global_service_context(service_context) def create_index(): - logger = logging.getLogger("uvicorn") - # check if storage already exists - if not os.path.exists(STORAGE_DIR): - logger.info("Creating new index") - # load the documents and create the index - try: - documents = SimpleDirectoryReader( - input_dir=DATA_DIR, recursive=True - ).load_data() - except ValueError as e: - logger.error(f"{e}") - index = VectorStoreIndex.from_documents( - documents=documents, service_context=service_context, show_progress=True - ) - # store it for later - index.storage_context.persist(STORAGE_DIR) - logger.info(f"Finished creating new index. Stored in {STORAGE_DIR}") + # if use local vector store, create & store the index locally + if USE_LOCAL_VECTOR_STORE: + # get the folders in the data directory + collection_names = os.listdir(DATA_DIR) + # to create each folder as a collection in local storage + for collection_name in collection_names: + logger.info(f"Checking if [{collection_names}] index exists locally...") + # build the new data directory + new_data_dir = os.path.join(DATA_DIR, collection_name) + # build the new storage directory + new_storage_dir = os.path.join(STORAGE_DIR, collection_name) + # check if storage folder and index files already exists + if ( + not os.path.exists(new_storage_dir) + or len(os.listdir(new_storage_dir)) + < 4 # 4 files should be present if using simplevectorstore + ): + logger.info(f"Creating [{collection_names}] index") + # load the documents and create the index + try: + documents = SimpleDirectoryReader( + input_dir=new_data_dir, recursive=True + ).load_data() + except ValueError as e: + logger.error(f"{e}") + index = VectorStoreIndex.from_documents( + documents=documents, + service_context=service_context, + show_progress=True, + ) + # store it for later + index.storage_context.persist(STORAGE_DIR) + logger.info(f"Finished creating new index. Stored in {STORAGE_DIR}") + else: + # do nothing + logger.info(f"Index already exist at {STORAGE_DIR}...") + # else, create & store the index in Supabase pgvector else: - # do nothing - logger.info(f"Index already exist at {STORAGE_DIR}...") + # get the folders in the data directory + collection_names = os.listdir(DATA_DIR) + # to create each folder as a collection in Supabase + for collection_name in collection_names: + # check if remote storage already exists + logger.info(f"Checking if [{collection_name}] index exists in Supabase...") + # set the dimension based on the LLM model used + dimension = ( + EMBED_MODEL_DIMENSIONS if USE_LOCAL_LLM else DEF_EMBED_MODEL_DIMENSIONS + ) + # create the vector store, will create the collection if it does not exist + vector_store = SupabaseVectorStore( + postgres_connection_string=os.getenv("POSTGRES_CONNECTION_STRING"), + collection_name=collection_name, + dimension=dimension, + ) + # create the storage context + storage_context = StorageContext.from_defaults(vector_store=vector_store) + logger.info(f"Creating [{collection_name}] index") + # create the data directory + new_data_dir = os.path.join(DATA_DIR, collection_name) + # load the documents and create the index + try: + documents = SimpleDirectoryReader( + input_dir=new_data_dir, recursive=True + ).load_data() + except ValueError as e: + logger.error(f"{e}") + index = VectorStoreIndex.from_documents( + documents=documents, + storage_context=storage_context, + show_progress=True, + ) + logger.info(f"Finished creating [{collection_name}] vector store") -def load_existing_index(): +def load_existing_index(collection_name="PSSCOC"): # load the existing index - logger = logging.getLogger("uvicorn") - logger.info(f"Loading index from {STORAGE_DIR}...") - storage_context = StorageContext.from_defaults(persist_dir=STORAGE_DIR) - index = load_index_from_storage(storage_context, service_context=service_context) - logger.info(f"Finished loading index from {STORAGE_DIR}") - return index + if USE_LOCAL_VECTOR_STORE: + # create the storage directory + new_storage_dir = os.path.join(STORAGE_DIR, collection_name) + # load the index from local storage + logger.info(f"Loading [{collection_name}] index from {new_storage_dir}...") + storage_context = StorageContext.from_defaults(persist_dir=new_storage_dir) + index = load_index_from_storage( + storage_context, service_context=service_context + ) + logger.info( + f"Finished loading [{collection_name}] index from {new_storage_dir}" + ) + logger.info(f"Index ID: {index.index_id}") + return index + else: + # load the index from Supabase + logger.info(f"Loading [{collection_name}] index from Supabase...") + # set the dimension based on the LLM model used + dimension = ( + EMBED_MODEL_DIMENSIONS if USE_LOCAL_LLM else DEF_EMBED_MODEL_DIMENSIONS + ) + # create the vector store + vector_store = SupabaseVectorStore( + postgres_connection_string=os.getenv("POSTGRES_CONNECTION_STRING"), + collection_name=collection_name, + dimension=dimension, + ) + # check if the vector store has been indexed + if not vector_store._collection.is_indexed_for_measure( + IndexMeasure.cosine_distance + ): + logger.info(f"Indexing [{collection_name}] vector store...") + vector_store._collection.create_index() + logger.info(f"Finished indexing [{collection_name}] vector store") + logger.info(vector_store._collection.name) + index = VectorStoreIndex.from_vector_store(vector_store=vector_store) + logger.info(f"Finished loading [{collection_name}] index from Supabase") + logger.info(f"Index ID: {index.index_id}") + return index -def get_index(): - # check if storage already exists - if not os.path.exists(STORAGE_DIR): - # create the index if it does not exist - create_index() - # load the index from storage - index = load_existing_index() - # check if storage is empty, 4 files should be present if using simplevectorstore - elif os.path.exists(STORAGE_DIR) and len(os.listdir(STORAGE_DIR)) < 4: - # create the index if it does not exist - create_index() - # load the index from storage - index = load_existing_index() - else: - # load the index from storage - index = load_existing_index() +def get_index(collection_name): + # load the index from storage + index = load_existing_index(collection_name) return index diff --git a/backend/backend/data/About PSSCOC/About PSSCOC.json b/backend/backend/data/PSSCOC/About PSSCOC/About PSSCOC.json similarity index 100% rename from backend/backend/data/About PSSCOC/About PSSCOC.json rename to backend/backend/data/PSSCOC/About PSSCOC/About PSSCOC.json diff --git a/backend/backend/data/PSSCOC for Construction Works/list-of-amendments-for-psscoc-for-construction-works-2020.pdf b/backend/backend/data/PSSCOC/PSSCOC for Construction Works/list-of-amendments-for-psscoc-for-construction-works-2020.pdf similarity index 100% rename from backend/backend/data/PSSCOC for Construction Works/list-of-amendments-for-psscoc-for-construction-works-2020.pdf rename to backend/backend/data/PSSCOC/PSSCOC for Construction Works/list-of-amendments-for-psscoc-for-construction-works-2020.pdf diff --git a/backend/backend/data/PSSCOC for Construction Works/option-module-c-on-collaborative-contracting-with-sidp.pdf b/backend/backend/data/PSSCOC/PSSCOC for Construction Works/option-module-c-on-collaborative-contracting-with-sidp.pdf similarity index 100% rename from backend/backend/data/PSSCOC for Construction Works/option-module-c-on-collaborative-contracting-with-sidp.pdf rename to backend/backend/data/PSSCOC/PSSCOC for Construction Works/option-module-c-on-collaborative-contracting-with-sidp.pdf diff --git a/backend/backend/data/PSSCOC for Construction Works/option-module-e-on-collaborative-contracting---sent.pdf b/backend/backend/data/PSSCOC/PSSCOC for Construction Works/option-module-e-on-collaborative-contracting---sent.pdf similarity index 100% rename from backend/backend/data/PSSCOC for Construction Works/option-module-e-on-collaborative-contracting---sent.pdf rename to backend/backend/data/PSSCOC/PSSCOC for Construction Works/option-module-e-on-collaborative-contracting---sent.pdf diff --git a/backend/backend/data/PSSCOC for Construction Works/psscoc-for-construction-works-2020.pdf b/backend/backend/data/PSSCOC/PSSCOC for Construction Works/psscoc-for-construction-works-2020.pdf similarity index 100% rename from backend/backend/data/PSSCOC for Construction Works/psscoc-for-construction-works-2020.pdf rename to backend/backend/data/PSSCOC/PSSCOC for Construction Works/psscoc-for-construction-works-2020.pdf diff --git a/backend/backend/data/PSSCOC for Construction Works/supplement-for-psscoc-for-construction-works-2020.docx b/backend/backend/data/PSSCOC/PSSCOC for Construction Works/supplement-for-psscoc-for-construction-works-2020.docx similarity index 100% rename from backend/backend/data/PSSCOC for Construction Works/supplement-for-psscoc-for-construction-works-2020.docx rename to backend/backend/data/PSSCOC/PSSCOC for Construction Works/supplement-for-psscoc-for-construction-works-2020.docx diff --git a/backend/backend/data/PSSCOC for Design and Build/list-of-amendments-psscoc-design-build-2020.pdf b/backend/backend/data/PSSCOC/PSSCOC for Design and Build/list-of-amendments-psscoc-design-build-2020.pdf similarity index 100% rename from backend/backend/data/PSSCOC for Design and Build/list-of-amendments-psscoc-design-build-2020.pdf rename to backend/backend/data/PSSCOC/PSSCOC for Design and Build/list-of-amendments-psscoc-design-build-2020.pdf diff --git a/backend/backend/data/PSSCOC for Design and Build/option-module-c-on-collaborative-contracting-wo-sidp.pdf b/backend/backend/data/PSSCOC/PSSCOC for Design and Build/option-module-c-on-collaborative-contracting-wo-sidp.pdf similarity index 100% rename from backend/backend/data/PSSCOC for Design and Build/option-module-c-on-collaborative-contracting-wo-sidp.pdf rename to backend/backend/data/PSSCOC/PSSCOC for Design and Build/option-module-c-on-collaborative-contracting-wo-sidp.pdf diff --git a/backend/backend/data/PSSCOC for Design and Build/option-module-c-on-collaborative-contracting.pdf b/backend/backend/data/PSSCOC/PSSCOC for Design and Build/option-module-c-on-collaborative-contracting.pdf similarity index 100% rename from backend/backend/data/PSSCOC for Design and Build/option-module-c-on-collaborative-contracting.pdf rename to backend/backend/data/PSSCOC/PSSCOC for Design and Build/option-module-c-on-collaborative-contracting.pdf diff --git a/backend/backend/data/PSSCOC for Design and Build/psscoc-for-design-build-2020.pdf b/backend/backend/data/PSSCOC/PSSCOC for Design and Build/psscoc-for-design-build-2020.pdf similarity index 100% rename from backend/backend/data/PSSCOC for Design and Build/psscoc-for-design-build-2020.pdf rename to backend/backend/data/PSSCOC/PSSCOC for Design and Build/psscoc-for-design-build-2020.pdf diff --git a/backend/backend/data/PSSCOC for Design and Build/supplement-for-psscoc-for-design-build-2020.docx b/backend/backend/data/PSSCOC/PSSCOC for Design and Build/supplement-for-psscoc-for-design-build-2020.docx similarity index 100% rename from backend/backend/data/PSSCOC for Design and Build/supplement-for-psscoc-for-design-build-2020.docx rename to backend/backend/data/PSSCOC/PSSCOC for Design and Build/supplement-for-psscoc-for-design-build-2020.docx diff --git a/backend/backend/data/Standard Conditions for Nominated Sub-Contract (NSC)/1_nsc_constnwks.pdf b/backend/backend/data/PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)/1_nsc_constnwks.pdf similarity index 100% rename from backend/backend/data/Standard Conditions for Nominated Sub-Contract (NSC)/1_nsc_constnwks.pdf rename to backend/backend/data/PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)/1_nsc_constnwks.pdf diff --git a/backend/backend/data/Standard Conditions for Nominated Sub-Contract (NSC)/2_nsc_supplem.docx b/backend/backend/data/PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)/2_nsc_supplem.docx similarity index 100% rename from backend/backend/data/Standard Conditions for Nominated Sub-Contract (NSC)/2_nsc_supplem.docx rename to backend/backend/data/PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)/2_nsc_supplem.docx diff --git a/backend/backend/data/Standard Conditions for Nominated Sub-Contract (NSC)/3_nsc_clarify_amendm.pdf b/backend/backend/data/PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)/3_nsc_clarify_amendm.pdf similarity index 100% rename from backend/backend/data/Standard Conditions for Nominated Sub-Contract (NSC)/3_nsc_clarify_amendm.pdf rename to backend/backend/data/PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)/3_nsc_clarify_amendm.pdf diff --git a/backend/backend/data/Standard Conditions for Nominated Sub-Contract (NSC)/4_nsc_amendm.pdf b/backend/backend/data/PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)/4_nsc_amendm.pdf similarity index 100% rename from backend/backend/data/Standard Conditions for Nominated Sub-Contract (NSC)/4_nsc_amendm.pdf rename to backend/backend/data/PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)/4_nsc_amendm.pdf diff --git a/backend/backend/main.py b/backend/backend/main.py index ad432a5..29d7469 100644 --- a/backend/backend/main.py +++ b/backend/backend/main.py @@ -11,19 +11,20 @@ from backend.app.api.routers.healthcheck import healthcheck_router from backend.app.api.routers.query import query_router from backend.app.api.routers.search import search_router -from backend.app.utils.index import create_index load_dotenv() app = FastAPI() -environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set +environment = os.getenv("ENVIRONMENT", "dev") # Default to 'dev' if not set # Add allowed origins from environment variables allowed_origins = os.getenv("ALLOWED_ORIGINS", "*") if environment == "dev": + # In development, allow all origins, methods, and headers logger = logging.getLogger("uvicorn") + logger.level = logging.DEBUG logger.warning("Running in development mode - allowing CORS for all origins") app.add_middleware( middleware_class=CORSMiddleware, @@ -36,8 +37,9 @@ if environment == "prod": # In production, specify the allowed origins allowed_origins = allowed_origins.split(",") if allowed_origins != "*" else ["*"] - + # Set the logger level to INFO logger = logging.getLogger("uvicorn") + logger.level = logging.INFO logger.info(f"Running in production mode - allowing CORS for {allowed_origins}") app.add_middleware( middleware_class=CORSMiddleware, @@ -48,15 +50,17 @@ ) logger.info(f"CUDA available: {is_cuda_available()}") +logger.info("Use Local LLM: " + os.getenv("USE_LOCAL_LLM", "false")) +logger.info("Use Local Vector Store: " + os.getenv("USE_LOCAL_VECTOR_STORE", "false")) + +# Set logger for httpx to WARNING +logging.getLogger("httpx").setLevel(logging.WARNING) app.include_router(chat_router, prefix="/api/chat") app.include_router(query_router, prefix="/api/query") app.include_router(search_router, prefix="/api/search") app.include_router(healthcheck_router, prefix="/api/healthcheck") -# Try to create the index first on startup -create_index() - # Redirect to the /docs endpoint @app.get("/") diff --git a/backend/backend/run.py b/backend/backend/run.py index b19ed46..df211b0 100644 --- a/backend/backend/run.py +++ b/backend/backend/run.py @@ -1,4 +1,41 @@ +import logging +import os + import uvicorn +from dotenv import load_dotenv + +load_dotenv() + +ENVIRONMENT = os.getenv("ENVIRONMENT", "dev") # Default to 'dev' if not set +CREATE_VECTOR_STORE = bool( + os.getenv("CREATE_VECTOR_STORE", "false").lower() == "true" +) # Default to False if not set + + +def run_app(): + # Run the app + if ENVIRONMENT == "dev": + # Run the app with the development settings to auto reload + uvicorn.run(app="main:app", host="0.0.0.0", reload=True) + if ENVIRONMENT == "prod": + # Run the app with the production settings, no auto reload + uvicorn.run(app="main:app", host="0.0.0.0", reload=False) + if __name__ == "__main__": - uvicorn.run(app="main:app", host="0.0.0.0", reload=True) + logging_format = "%(levelname)s: %(message)s" + logging.basicConfig(level=logging.INFO, format=logging_format) + logger = logging.getLogger(__name__) + logger.info("Create vector store: " + str(CREATE_VECTOR_STORE)) + if CREATE_VECTOR_STORE: + # Create the vector store + from backend.app.utils.index import create_index + + logger.info("Creating vector stores first...") + create_index() + logger.info("Vector stores created successfully! Running App...") + # Run the app + run_app() + else: + # Run the app + run_app() diff --git a/backend/example.env b/backend/example.env index 73f9e7f..decee12 100644 --- a/backend/example.env +++ b/backend/example.env @@ -1 +1,35 @@ -ALLOWED_ORIGINS=http://localhost:3000 \ No newline at end of file +# Setting up the environment + +# Specify to start uvicorn app in dev or prod mode. default is dev if not set [prod|dev] +ENVIRONMENT=prod + +# Allowed origins for CORS in production, separated by comma, no spaces +ALLOWED_ORIGINS=http://localhost:3000,https://smart-retrieval-demo.vercel.app + +# Use Local LLM [true|false] +USE_LOCAL_LLM=true + +# Use Local Vector Store [true|false] +USE_LOCAL_VECTOR_STORE=true + +# Create the vector store [true|false], remember to change to false after creating for a one-time setup +CREATE_VECTOR_STORE=false + +# OpenAI API Key +OPENAI_API_KEY=sk-YourOpenAIKey + +# Backend API Authorization Settings +# Auth Header Name for the API +API_AUTH_HEADER_NAME=Authorization +# Alternate API Key Header Name for the API +API_KEY_HEADER_NAME=X-API-Key +# Ensure this key is the same in the frontend app environment +# https://generate-random.org/api-key-generator?count=1&length=128&type=mixed-numbers&prefix=sr- +BACKEND_API_KEY=sr-SomeRandomKey + +# Supabase Settings +SUPABASE_URL=https://YourSupabaseProjectID.supabase.co +SUPABASE_ANON_KEY=YourSupabaseAnonKey +SUPABASE_JWT_SECRET=YourSupabaseJWTSecret +# Note: Rename 'postgres://' to 'postgresql://' in the connection string due to sqlalchemy not supporting 'postgres://' +POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/smart_retrieval \ No newline at end of file diff --git a/backend/poetry.lock b/backend/poetry.lock index 3705d3a..557fa27 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -349,6 +349,20 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] +[[package]] +name = "deprecation" +version = "2.1.0" +description = "A library to handle automated deprecations" +optional = false +python-versions = "*" +files = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] + +[package.dependencies] +packaging = "*" + [[package]] name = "dirtyjson" version = "1.0.8" @@ -459,6 +473,22 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.11.0,<2.12.0" pyflakes = ">=3.2.0,<3.3.0" +[[package]] +name = "flupy" +version = "1.2.0" +description = "Method chaining built on generators" +optional = false +python-versions = "*" +files = [ + {file = "flupy-1.2.0.tar.gz", hash = "sha256:12487a008e9744cd35d0f6ea3cfa06f4b2b27cb138bf57d0788f5c26e57afe69"}, +] + +[package.dependencies] +typing_extensions = "*" + +[package.extras] +dev = ["black", "mypy", "pre-commit", "pylint", "pytest", "pytest-benchmark", "pytest-cov"] + [[package]] name = "frozenlist" version = "1.4.1" @@ -580,6 +610,21 @@ smb = ["smbprotocol"] ssh = ["paramiko"] tqdm = ["tqdm"] +[[package]] +name = "gotrue" +version = "2.4.1" +description = "Python Client Library for GoTrue" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "gotrue-2.4.1-py3-none-any.whl", hash = "sha256:9647bb7a585c969d26667df21168fa20b18f91c5d6afe286af08d7a0610fd2cc"}, + {file = "gotrue-2.4.1.tar.gz", hash = "sha256:8b260ef285f45a3a2f9b5a006f12afb9fad7a36a28fa277f19e733f22eb88584"}, +] + +[package.dependencies] +httpx = ">=0.23,<0.26" +pydantic = ">=1.10,<3" + [[package]] name = "greenlet" version = "3.0.3" @@ -733,13 +778,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.27.0" +version = "0.25.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, ] [package.dependencies] @@ -1560,6 +1605,19 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pgvector" +version = "0.1.8" +description = "pgvector support for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pgvector-0.1.8-py2.py3-none-any.whl", hash = "sha256:99dce3a6580ef73863edb9b8441937671f4e1a09383826e6b0838176cd441a96"}, +] + +[package.dependencies] +numpy = "*" + [[package]] name = "pluggy" version = "1.4.0" @@ -1575,6 +1633,104 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "postgrest" +version = "0.16.1" +description = "PostgREST client for Python. This library provides an ORM interface to PostgREST." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "postgrest-0.16.1-py3-none-any.whl", hash = "sha256:412ec6bf61c58f38c92b6b61f57ab50e25c73ca9ef415a6f56ed9cf5429614cb"}, + {file = "postgrest-0.16.1.tar.gz", hash = "sha256:d955824d37e7123a8313cbf10c8e0a8d42418fcb942cd8e1526e8509fb71574d"}, +] + +[package.dependencies] +deprecation = ">=2.1.0,<3.0.0" +httpx = ">=0.24,<0.26" +pydantic = ">=1.9,<3.0" +strenum = ">=0.4.9,<0.5.0" + +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, +] + [[package]] name = "pycodestyle" version = "2.11.1" @@ -1707,6 +1863,23 @@ files = [ {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pypdf" version = "3.17.4" @@ -1832,7 +2005,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1867,6 +2039,22 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "realtime" +version = "1.0.2" +description = "" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "realtime-1.0.2-py3-none-any.whl", hash = "sha256:8f8375199fd917cd0ded818702321f91b208ab72794ade0a33cee9d55ae30f11"}, + {file = "realtime-1.0.2.tar.gz", hash = "sha256:776170a4329edc869b91e104c554cda02c8bf8e052cbb93c377e22482870959c"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1,<3.0.0" +typing-extensions = ">=4.2.0,<5.0.0" +websockets = ">=11.0,<12.0" + [[package]] name = "regex" version = "2023.12.25" @@ -2248,6 +2436,71 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "storage3" +version = "0.7.3" +description = "Supabase Storage client for Python." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "storage3-0.7.3-py3-none-any.whl", hash = "sha256:dc6a59da801ee6fc00015da4967ac0b5c3e5508d31ffd796f0e4c83957e5c6a0"}, + {file = "storage3-0.7.3.tar.gz", hash = "sha256:943c31de4a7c7490ad7960d963a6b410979ebd0e1b3d320d76cb61564ab0b528"}, +] + +[package.dependencies] +httpx = ">=0.24,<0.26" +python-dateutil = ">=2.8.2,<3.0.0" +typing-extensions = ">=4.2.0,<5.0.0" + +[[package]] +name = "strenum" +version = "0.4.15" +description = "An Enum that inherits from str." +optional = false +python-versions = "*" +files = [ + {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"}, + {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"}, +] + +[package.extras] +docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"] +release = ["twine"] +test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] + +[[package]] +name = "supabase" +version = "2.4.0" +description = "Supabase client for Python." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "supabase-2.4.0-py3-none-any.whl", hash = "sha256:f2f02b0e7903247ef9e2b3cb5dde067924a19a068f1c8befbdf40fb091bf8dd3"}, + {file = "supabase-2.4.0.tar.gz", hash = "sha256:d51556d3884f2e6f4588c33f1fcac954d4304238253bc35e9a87fdd22c43bafb"}, +] + +[package.dependencies] +gotrue = ">=1.3,<3.0" +httpx = ">=0.24,<0.26" +postgrest = ">=0.10.8,<0.17.0" +realtime = ">=1.0.0,<2.0.0" +storage3 = ">=0.5.3,<0.8.0" +supafunc = ">=0.3.1,<0.4.0" + +[[package]] +name = "supafunc" +version = "0.3.3" +description = "Library for Supabase Functions" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "supafunc-0.3.3-py3-none-any.whl", hash = "sha256:8260b4742335932f9cab64c8f66fb6998681b7e8ca7a46b559a4eb640cc0af80"}, + {file = "supafunc-0.3.3.tar.gz", hash = "sha256:c35897a2f40465b40d7a08ae11f872f08eb8d1390c3ebc72c80e27d33ba91b99"}, +] + +[package.dependencies] +httpx = ">=0.24,<0.26" + [[package]] name = "sympy" version = "1.12" @@ -2755,6 +3008,28 @@ files = [ docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +[[package]] +name = "vecs" +version = "0.4.3" +description = "pgvector client" +optional = false +python-versions = "*" +files = [ + {file = "vecs-0.4.3.tar.gz", hash = "sha256:0a60294143aec43bd0344bb9235b6e57f8f919d102538f6b989d7b85095a31ce"}, +] + +[package.dependencies] +deprecated = "==1.2.*" +flupy = "==1.*" +pgvector = "==0.1.*" +psycopg2-binary = "==2.9.*" +sqlalchemy = "==2.*" + +[package.extras] +dev = ["numpy", "parse", "pytest", "pytest-cov"] +docs = ["mike", "mkdocs", "pygments", "pymarkdown", "pymdown-extensions"] +text-embedding = ["sentence-transformers (==2.*)"] + [[package]] name = "watchfiles" version = "0.21.0" @@ -2844,83 +3119,81 @@ anyio = ">=3.0.0" [[package]] name = "websockets" -version = "12.0" +version = "11.0.3" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, - {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, - {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, - {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, - {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, - {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, - {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, - {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, - {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, - {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, - {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, - {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, - {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, - {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, - {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, - {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, - {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, - {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, - {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, - {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, - {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, - {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, - {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, - {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, - {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, + {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, + {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, + {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, + {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, + {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, + {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, + {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, + {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, + {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, + {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, + {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, + {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, + {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, + {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, + {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, + {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, + {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, + {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, + {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, + {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, + {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, + {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, + {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, + {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, + {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, + {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, + {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, + {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, + {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, + {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, ] [[package]] @@ -3108,4 +3381,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11,<3.12" -content-hash = "441dad0efba8e98e6ec61d954a54fa1542486a1aed72ca2c091da30e0531ec77" +content-hash = "1d354d5ac24eb7e482fe9895fef5888582caeb9d6b52982db87f88a8f968da19" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 7677e70..702cace 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -17,6 +17,9 @@ llama-cpp-python = "^0.2.52" transformers = "^4.38.1" docx2txt = "^0.8" doc2docx = "^0.2.4" +supabase = "^2.4.0" +pyjwt = "^2.8.0" +vecs = "^0.4.3" [tool.poetry.group.dev] optional = true diff --git a/frontend/app/about/page.tsx b/frontend/app/about/page.tsx index b2fb382..6e86a98 100644 --- a/frontend/app/about/page.tsx +++ b/frontend/app/about/page.tsx @@ -4,7 +4,7 @@ export default function About() { return (
-
+

About Smart Retrieval

@@ -19,6 +19,63 @@ export default function About() { With cutting-edge technology, including Large Language Models (LLM) like GPT, BERT, and advanced chatbot integration, we aim to revolutionize the way JTC employees access and comprehend crucial documents.

+

+ We are committed to providing accurate, reliable, and user-friendly information retrieval services that + empower JTC employees to make informed decisions and stay compliant with the latest regulations. +

+

+ Thank you for choosing Smart Retrieval. We look forward to serving you and making your work life easier. +

+ +

Functions of Smart Retrieval

+

+ Smart Retrieval is designed to provide users with a seamless and efficient experience when searching for + information. Some of the key functions of Smart Retrieval include: +

+
    +
  • Chat
  • +
  • Question & Answer (Q&A)
  • +
  • Search
  • +
+

+ These functions are designed to cater to different user preferences and requirements, ensuring that users + can easily access the information they need. +

+ +

Functions In-Depth

+

Chat

+

+ The chat function allows users to interact with Smart Retrieval through a conversational interface via a fixed set of documents. + Users can ask questions, seek information, and engage in dialogue with the system to retrieve the information they + need. The chat function is designed to be intuitive, user-friendly, and responsive, providing users with a + seamless experience. Current document sets include: +

+
    +
  • PSSCOC
  • +
  • EIR
  • +
  • And more...
  • +
+ +

Question & Answer (Q&A)

+

+ The Q&A function enables users to ask specific questions relating to their own documents and receive accurate answers from Smart Retrieval. + Users can simply upload their files and input their queries, and the system will provide relevant information based on the question asked. + The Q&A function is designed to provide users with precise and concise answers to their queries, enhancing + the information retrieval process. +

+ +

Search

+

+ The search function allows users to input keywords or phrases to search for specific information within the + document repository. Users can enter their search queries, and the system will retrieve relevant documents + based on the search terms provided. The search function is designed to help users quickly locate the + information they need, streamlining the search process. +

+ +

Disclaimer

+

+ The answer provided by Smart Retrieval may not be accurate and might be prone to hallucination. Users are advised to fact-check the answer and not use it as is. Smart Retrieval is not responsible for any consequences arising from the use of the answer. +

diff --git a/frontend/app/api/profile/route.ts b/frontend/app/api/profile/route.ts new file mode 100644 index 0000000..bc70e61 --- /dev/null +++ b/frontend/app/api/profile/route.ts @@ -0,0 +1,46 @@ +import { createClient } from '@supabase/supabase-js'; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + const { pathname, origin } = request.nextUrl; + const signinPage = new URL('/sign-in', origin); + // Retrieve the session token from the request cookies + const session = request.cookies.get('next-auth.session-token'); + + // Create a new Supabase client + const supabase = createClient( + process.env.SUPABASE_URL ?? '', + process.env.SUPABASE_SERVICE_ROLE_KEY ?? '', + { db: { schema: 'next_auth' } }, + ); + + // Retrieve the user's ID from the session token + const { data: sessionData, error: sessionError } = await supabase + .from('sessions') + .select('userId') + .eq('sessionToken', session?.value) + .single(); + + const userId = sessionData?.userId; + + if (sessionError) { + console.error('Error fetching session from database:', sessionError.message); + return NextResponse.redirect(signinPage.href, { status: 302 }); + } + + // Retrieve the user's profile data + const { data: userData, error: userError } = await supabase + .from('users') + .select('id, name, email, image') + .eq('id', userId) + .single(); + + if (userError) { + console.error('Error fetching user data from database:', userError.message); + return NextResponse.redirect(signinPage.href, { status: 302 }); + } + + // console.log('userData:', userData); + + return NextResponse.json({ userData: userData }); +} diff --git a/frontend/app/api/status/route.ts b/frontend/app/api/status/route.ts new file mode 100644 index 0000000..e924262 --- /dev/null +++ b/frontend/app/api/status/route.ts @@ -0,0 +1,28 @@ +export async function GET(request: Request) { + const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API as string; + + // Retrieve the session token from the request headers + let session = request.headers.get('Authorization'); + + console.log('Status API - headers:', request.headers); + + // Public API key + let api_key = null; + + // If no session, use the public API key + if (!session) { + api_key = process.env.BACKEND_API_KEY as string; + } + + const res = await fetch(healthcheck_api, { + signal: AbortSignal.timeout(5000), // Abort the request if it takes longer than 5 seconds + headers: { + 'Content-Type': 'application/json', + 'Authorization': session, + 'X-API-Key': api_key, + } as any, + }) + const data = await res.json() + + return Response.json({ data }) +} \ No newline at end of file diff --git a/frontend/app/components/chat-section.tsx b/frontend/app/components/chat-section.tsx index 8d948cf..4ca9de8 100644 --- a/frontend/app/components/chat-section.tsx +++ b/frontend/app/components/chat-section.tsx @@ -2,9 +2,15 @@ import { useChat } from "ai/react"; import { ChatInput, ChatMessages } from "@/app/components/ui/chat"; +import ChatSelection from "./ui/chat/chat-selection"; import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog"; +import { useSession } from "next-auth/react"; +import { useState } from "react"; export default function ChatSection() { + const { data: session } = useSession(); + const supabaseAccessToken = session?.supabaseAccessToken; + const [docSelected, setDocSelected] = useState(''); const { messages, input, @@ -13,29 +19,51 @@ export default function ChatSection() { handleInputChange, reload, stop, - } = useChat({ api: process.env.NEXT_PUBLIC_CHAT_API }); + } = useChat({ + api: process.env.NEXT_PUBLIC_CHAT_API, + headers: { + // Add the access token to the request headers + 'Authorization': `Bearer ${supabaseAccessToken}`, + }, + body: { + // Add the selected document to the request body + document: docSelected, + }, + }); return (
- - - + {docSelected ? + ( + <> + + + + + ) + : + + }
); } diff --git a/frontend/app/components/footer.tsx b/frontend/app/components/footer.tsx index 7df23ce..19ffa9f 100644 --- a/frontend/app/components/footer.tsx +++ b/frontend/app/components/footer.tsx @@ -1,32 +1,50 @@ "use client"; +import { useState } from "react"; import { FooterNavLink } from "@/app/components/ui/navlink"; import { IconGitHub } from "@/app/components/ui/icons"; -import { Text, Cookie } from "lucide-react"; +import { Text, Cookie, AlertCircle } from "lucide-react"; export default function Footer() { + const [showDisclaimer, setShowDisclaimer] = useState(false); + + const toggleDisclaimer = () => { + setShowDisclaimer(!showDisclaimer); + }; + return ( -
- +
Home
- +
About
- +
Chat
- +
Q&A
- +
Search @@ -169,7 +187,7 @@ export default function Header() {
{/* Status Page Button/Indicator */} API: - +
{isLoading ? ( @@ -201,8 +219,46 @@ export default function Header() { )} + + + + {/* Conditionally render the user profile and logout buttons based on the user's authentication status */} + {status === 'loading' ? ( +
+ +
+ ) : session ? ( + <> + {/* User Profile Button */} + +
+ +
+
+ + {/* Sign Out Button */} + + + ) : ( + +
+ + Sign In +
+
+ )}
+ {/* Mobile menu component */} < MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false) } logoSrc={logo} items={MobileMenuItems} /> diff --git a/frontend/app/components/query-section.tsx b/frontend/app/components/query-section.tsx index b9615ce..ddd3fbf 100644 --- a/frontend/app/components/query-section.tsx +++ b/frontend/app/components/query-section.tsx @@ -2,9 +2,11 @@ import { useChat } from "ai/react"; import { ChatInput, ChatMessages } from "@/app/components/ui/chat"; -import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog"; +import { AutofillQuestion } from "./ui/autofill-prompt"; +import { useSession } from "next-auth/react"; export default function QuerySection() { + const { data: session } = useSession(); const { messages, input, @@ -13,17 +15,24 @@ export default function QuerySection() { handleInputChange, reload, stop, - } = useChat({ api: process.env.NEXT_PUBLIC_QUERY_API }); + } = useChat({ + api: process.env.NEXT_PUBLIC_QUERY_API, + // Add the access token to the request headers + headers: { + 'Authorization': `Bearer ${session?.supabaseAccessToken}`, + } + }); return (
- + /> */} + + {/* Maintenance Page */} +
+ A new feature is coming your way! +
+ The Q&A Page is currently undergoing upgrades. Please check back later. +
); } diff --git a/frontend/app/components/search-section.tsx b/frontend/app/components/search-section.tsx index 8aec5b3..f2331a5 100644 --- a/frontend/app/components/search-section.tsx +++ b/frontend/app/components/search-section.tsx @@ -2,16 +2,14 @@ "use client"; import { useState, ChangeEvent, FormEvent } from "react"; -import useSearch from "@/app/components/ui/search/useSearch"; -import SearchResults from "@/app/components/ui/search/search-results"; -import SearchInput from "@/app/components/ui/search/search-input"; -import AutofillSearchQuery from "@/app/components/ui/autofill-prompt/autofill-search-prompt-dialog"; +import { AutofillSearchQuery } from "@/app/components/ui/autofill-prompt"; +import { SearchSelection, useSearch, SearchResults, SearchInput } from "./ui/search"; const SearchSection: React.FC = () => { const [query, setQuery] = useState(""); const { searchResults, isLoading, handleSearch } = useSearch(); const [searchButtonPressed, setSearchButtonPressed] = useState(false); - + const [docSelected, setDocSelected] = useState(''); const handleInputChange = (e: ChangeEvent) => { setQuery(e.target.value); @@ -26,21 +24,38 @@ const SearchSection: React.FC = () => { return (
- - - + {docSelected ? ( + <> +

Searching in {docSelected}

+ + + + + ) : ( + + )}
); }; diff --git a/frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx b/frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx index 704d40c..19053ae 100644 --- a/frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx +++ b/frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; -import { QuestionsBankProp, questionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface"; +import { QuestionsBankProp, psscocQuestionsBank, eirQuestionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface"; import { ChatHandler } from "@/app/components/ui/chat/chat.interface"; export default function AutofillQuestion( props: Pick< ChatHandler, - "messages" | "isLoading" | "handleSubmit" | "handleInputChange" | "input" + "docSelected" | "messages" | "isLoading" | "handleSubmit" | "handleInputChange" | "input" >, ) { // Keep track of whether to show the overlay @@ -14,6 +14,8 @@ export default function AutofillQuestion( const [randomQuestions, setRandomQuestions] = useState([]); // Keep track of the current question index const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); + // Questions bank for PSSCOC or EIR + const [questionsBank, setQuestionsBank] = useState(psscocQuestionsBank); // Shuffle the array using Fisher-Yates algorithm function shuffleArray(array: any[]) { @@ -28,6 +30,13 @@ export default function AutofillQuestion( // Randomly select a subset of 3-4 questions useEffect(() => { + // Select the questions bank based on the document set selected + if (props.docSelected === "EIR") { + setQuestionsBank(eirQuestionsBank); + } + else { + setQuestionsBank(psscocQuestionsBank); + } // Shuffle the questionsBank array const shuffledQuestions = shuffleArray(questionsBank); // Get a random subset of 3-4 questions @@ -37,7 +46,7 @@ export default function AutofillQuestion( setTimeout(() => { setRandomQuestions(selectedQuestions); }, 300); - }, []); + }, [questionsBank, props.docSelected]); // Hide overlay when there are messages @@ -72,9 +81,9 @@ export default function AutofillQuestion( return ( <> {showOverlay && ( -
+
-

How can I help you today?

+

How can I help you with {props.docSelected} today?

{randomQuestions.map((question, index) => (
  • diff --git a/frontend/app/components/ui/autofill-prompt/autofill-prompt.interface.tsx b/frontend/app/components/ui/autofill-prompt/autofill-prompt.interface.tsx index c9478bb..dacd037 100644 --- a/frontend/app/components/ui/autofill-prompt/autofill-prompt.interface.tsx +++ b/frontend/app/components/ui/autofill-prompt/autofill-prompt.interface.tsx @@ -2,7 +2,7 @@ export interface QuestionsBankProp { title: string; } -export const questionsBank: QuestionsBankProp[] = [ +export const psscocQuestionsBank: QuestionsBankProp[] = [ { title: "Under PSSCOC, what are the differences between the role of the SO Rep and SO?" }, { title: "Who has the authority to appoint SO Rep assistants and what can the SO Rep assistants be authorized to do?" }, { title: "What are the general obligations of the contractor and consultant?" }, @@ -11,4 +11,12 @@ export const questionsBank: QuestionsBankProp[] = [ { title: "What are the requirements for the contractor to claim for loss and expense?" }, { title: "Under PSSCOC, briefly describe the proper payment claim process prescribed." }, // Add more common questions as needed +]; + +export const eirQuestionsBank: QuestionsBankProp[] = [ + { title: "I have confidential projects that cannot go on the Internet. Can you list me all the clauses which specify the use of Internet platforms or tools, as I will need to remove them." }, + { title: "My BIM files have speciality equipment such as ovens, microwaves and refrigerators. What should I categorise them as according to the model content requirements. What parameters are required?" }, + { title: "I use my own platforms such as BIM360 and Novade for safety observations. Do I need to use JTC’s OPTIMUS for such issues or can I use my own platforms?" }, + { title: "How many days do I have to submit the BIM Execution Plan and who should I submit it to?" }, + // Add more common questions as needed ]; \ No newline at end of file diff --git a/frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx b/frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx index 9313c2b..fc73087 100644 --- a/frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx +++ b/frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; -import { QuestionsBankProp, questionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface"; +import { QuestionsBankProp, psscocQuestionsBank, eirQuestionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface"; import { SearchHandler } from "@/app/components/ui/search/search.interface"; export default function AutofillSearchQuery( props: Pick< SearchHandler, - "query" | "isLoading" | "onSearchSubmit" | "onInputChange" | "results" | "searchButtonPressed" + "docSelected" | "query" | "isLoading" | "onSearchSubmit" | "onInputChange" | "results" | "searchButtonPressed" >, ) { // Keep track of whether to show the overlay @@ -14,6 +14,8 @@ export default function AutofillSearchQuery( const [randomQuestions, setRandomQuestions] = useState([]); // Keep track of the current question index const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); + // Questions bank for PSSCOC or EIR + const [questionsBank, setQuestionsBank] = useState(psscocQuestionsBank); // Shuffle the array using Fisher-Yates algorithm function shuffleArray(array: any[]) { @@ -28,6 +30,13 @@ export default function AutofillSearchQuery( // Randomly select a subset of 3-4 questions useEffect(() => { + // Select the questions bank based on the document set selected + if (props.docSelected === "EIR") { + setQuestionsBank(eirQuestionsBank); + } + else { + setQuestionsBank(psscocQuestionsBank); + } // Shuffle the questionsBank array const shuffledQuestions = shuffleArray(questionsBank); // Get a random subset of 3-4 questions @@ -37,7 +46,7 @@ export default function AutofillSearchQuery( setTimeout(() => { setRandomQuestions(selectedQuestions); }, 300); - }, []); + }, [questionsBank, props.docSelected]); // Hide overlay when there are query @@ -76,7 +85,8 @@ export default function AutofillSearchQuery( {showOverlay && (
    -

    How can I help you today?

    +

    How can I help with {props.docSelected} today?

    + {/* {dialogMessage &&

    {dialogMessage}

    } */} {randomQuestions.map((question, index) => (
    • diff --git a/frontend/app/components/ui/autofill-prompt/index.ts b/frontend/app/components/ui/autofill-prompt/index.ts new file mode 100644 index 0000000..153d832 --- /dev/null +++ b/frontend/app/components/ui/autofill-prompt/index.ts @@ -0,0 +1,4 @@ +import AutofillQuestion from "./autofill-prompt-dialog"; +import AutofillSearchQuery from "./autofill-search-prompt-dialog"; + +export { AutofillQuestion, AutofillSearchQuery }; \ No newline at end of file diff --git a/frontend/app/components/ui/chat/chat-input.tsx b/frontend/app/components/ui/chat/chat-input.tsx index 1f0d62d..4ccfd10 100644 --- a/frontend/app/components/ui/chat/chat-input.tsx +++ b/frontend/app/components/ui/chat/chat-input.tsx @@ -14,34 +14,37 @@ export default function ChatInput( return (
      - - + - + )} + +
    +

    Smart Retrieval may not be 100% accurate. Consider checking important information.

    ); } diff --git a/frontend/app/components/ui/chat/chat-messages.tsx b/frontend/app/components/ui/chat/chat-messages.tsx index aa34037..bcd509a 100644 --- a/frontend/app/components/ui/chat/chat-messages.tsx +++ b/frontend/app/components/ui/chat/chat-messages.tsx @@ -29,23 +29,25 @@ export default function ChatMessages( }, [messageLength, lastMessage]); return ( -
    -
    - {props.messages.map((m) => ( - - ))} + messageLength > 0 && ( +
    +
    + {props.messages.map((m) => ( + + ))} +
    +
    + +
    -
    - -
    -
    + ) ); } diff --git a/frontend/app/components/ui/chat/chat-selection.tsx b/frontend/app/components/ui/chat/chat-selection.tsx new file mode 100644 index 0000000..7867e8c --- /dev/null +++ b/frontend/app/components/ui/chat/chat-selection.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { ChatHandler } from '@/app/components/ui/chat'; + +const DocumentSet = [ + 'PSSCOC', + 'EIR', + // Add More Document Set as needed +]; + +export default function ChatSelection( + props: Pick, +) { + const [currentDocumentIndex, setCurrentDocumentIndex] = useState(0); + + const handleDocumentSetChange = (documentSet: string) => { + props.handleDocSelect(documentSet); + }; + + // Automatically advance to the next document set after a delay + useEffect(() => { + const timer = setInterval(() => { + if (currentDocumentIndex < DocumentSet.length - 1) { + setCurrentDocumentIndex((prevIndex) => prevIndex + 1); + } + else { + clearInterval(timer); // Stop the timer when all document set have been displayed + } + }, 100); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds) + + return () => clearInterval(timer); // Cleanup the timer on component unmount + }, [currentDocumentIndex, DocumentSet]); + + return ( +
    +
    +

    Select Document Set to Chat with:

    + {/*

    {dialogMessage}

    */} + {DocumentSet.map((title, index) => ( +
      +
    • + +
    • +
    + ))} +
    +
    + ); +}; \ No newline at end of file diff --git a/frontend/app/components/ui/chat/chat.interface.ts b/frontend/app/components/ui/chat/chat.interface.ts index 3256f7f..2f77130 100644 --- a/frontend/app/components/ui/chat/chat.interface.ts +++ b/frontend/app/components/ui/chat/chat.interface.ts @@ -5,6 +5,8 @@ export interface Message { } export interface ChatHandler { + docSelected: string; + handleDocSelect: (doc: string) => void; messages: Message[]; input: string; isLoading: boolean; diff --git a/frontend/app/components/ui/chat/index.ts b/frontend/app/components/ui/chat/index.ts index c7990f9..f1b08d5 100644 --- a/frontend/app/components/ui/chat/index.ts +++ b/frontend/app/components/ui/chat/index.ts @@ -1,5 +1,6 @@ import ChatInput from "./chat-input"; import ChatMessages from "./chat-messages"; +import ChatSelection from "./chat-selection"; export { type ChatHandler, type Message } from "./chat.interface"; -export { ChatInput, ChatMessages }; +export { ChatInput, ChatMessages, ChatSelection }; diff --git a/frontend/app/components/ui/login-buttons.tsx b/frontend/app/components/ui/login-buttons.tsx index ace7e7d..b68b6f5 100644 --- a/frontend/app/components/ui/login-buttons.tsx +++ b/frontend/app/components/ui/login-buttons.tsx @@ -5,6 +5,7 @@ import { signIn } from 'next-auth/react' import { cn } from '@/app/components/ui/lib/utils' import { Button, type ButtonProps } from '@/app/components/ui/button' import { IconGoogle, IconSGid, IconSpinner } from '@/app/components/ui/icons' +import { useSearchParams } from 'next/navigation' interface LoginButtonProps extends ButtonProps { showIcon?: boolean; @@ -18,13 +19,19 @@ function GoogleLoginButton({ ...props }: LoginButtonProps) { const [isLoading, setIsLoading] = useState(false); - + const searchParams = useSearchParams() + let tempcallbackURL = searchParams.get("callbackUrl"); // Get the 'callbackURL' query parameter + let callbackURL = tempcallbackURL; + // if callbackURL is not provided, default to home page + if (!tempcallbackURL) { + callbackURL = '/'; + } return ( - - +
    + + + +
    +

    Smart Retrieval may not be 100% accurate. Consider checking important information.

    + ); }; - -export default SearchInput; diff --git a/frontend/app/components/ui/search/search-results.tsx b/frontend/app/components/ui/search/search-results.tsx index aaeb230..7df6380 100644 --- a/frontend/app/components/ui/search/search-results.tsx +++ b/frontend/app/components/ui/search/search-results.tsx @@ -5,22 +5,30 @@ import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { SearchHandler, SearchResult } from "@/app/components/ui/search/search.interface"; -const SearchResults: React.FC = ({ query, results, isLoading, searchButtonPressed }) => { +export default function SearchResults( + props: Pick + ) { const [sortedResults, setSortedResults] = useState([]); const [expandedResult, setExpandedResult] = useState(null); // Sort results by similarity score whenever results or query change useEffect(() => { - if (query.trim() === "" && !searchButtonPressed){ + if (props.query.trim() === "" && !props.searchButtonPressed) { // Reset sortedResults when query is empty setSortedResults([]); - } else if (query.trim() !== "" && searchButtonPressed) { - // Sort results by similarity score - const sorted = results.slice().sort((a, b) => b.similarity_score - a.similarity_score); - // Update sortedResults state - setSortedResults(sorted); + } else if (props.query.trim() !== "" && props.searchButtonPressed) { + // if results are empty + if (props.results.length === 0) { + setSortedResults([]); + } + else { + // Sort results by similarity score + const sorted = props.results.slice().sort((a, b) => b.similarity_score - a.similarity_score); + // Update sortedResults state + setSortedResults(sorted); + } } - }, [query, results]); + }, [props.query, props.results]); // Log sortedResults outside of useEffect to ensure you're getting the updated state // console.log("Sorted results:", sortedResults); @@ -36,19 +44,26 @@ const SearchResults: React.FC = ({ query, results, isLoading, sea // Handle Reseting the expanded result when the search button is pressed useEffect(() => { - if (searchButtonPressed) { + if (props.searchButtonPressed) { setExpandedResult(null); } - }, [searchButtonPressed]); + }, [props.searchButtonPressed]); + + // Handle when query is empty and search button is pressed + if (props.query.trim() === "" && props.searchButtonPressed) { + return ( + null + ); + } // Handle when there are no search results and search button is not pressed - if (sortedResults.length === 0 && !searchButtonPressed) { + if (sortedResults.length === 0 && !props.searchButtonPressed) { return ( null ); } - if (isLoading) { + if (props.isLoading) { return (
    @@ -58,7 +73,7 @@ const SearchResults: React.FC = ({ query, results, isLoading, sea } // Handle when there are no search results - if (sortedResults.length === 0 && query.trim() !== "" && searchButtonPressed) { + if (sortedResults.length === 0 && props.query.trim() !== "" && props.searchButtonPressed) { return (

    No results found.

    @@ -135,5 +150,3 @@ const SearchResults: React.FC = ({ query, results, isLoading, sea
    ); }; - -export default SearchResults; diff --git a/frontend/app/components/ui/search/search-selection.tsx b/frontend/app/components/ui/search/search-selection.tsx new file mode 100644 index 0000000..bac120f --- /dev/null +++ b/frontend/app/components/ui/search/search-selection.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { SearchHandler } from './search.interface'; + +const DocumentSet = [ + 'PSSCOC', + 'EIR', + // Add More Document Set as needed +]; + +export default function SearchSelection( + props: Pick, +) { + const [currentDocumentIndex, setCurrentDocumentIndex] = useState(0); + + const handleDocumentSetChange = (documentSet: string) => { + props.handleDocSelect(documentSet); + }; + + // Automatically advance to the next document set after a delay + useEffect(() => { + const timer = setInterval(() => { + if (currentDocumentIndex < DocumentSet.length - 1) { + setCurrentDocumentIndex((prevIndex) => prevIndex + 1); + } + else { + clearInterval(timer); // Stop the timer when all document set have been displayed + } + }, 100); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds) + + return () => clearInterval(timer); // Cleanup the timer on component unmount + }, [currentDocumentIndex, DocumentSet]); + + return ( +
    +
    +

    Select Document Set to Search in:

    + {/*

    {dialogMessage}

    */} + {DocumentSet.map((title, index) => ( +
      +
    • + +
    • +
    + ))} +
    +
    + ); +}; \ No newline at end of file diff --git a/frontend/app/components/ui/search/search.interface.ts b/frontend/app/components/ui/search/search.interface.ts index 7d390b9..89797c9 100644 --- a/frontend/app/components/ui/search/search.interface.ts +++ b/frontend/app/components/ui/search/search.interface.ts @@ -1,6 +1,8 @@ import { ChangeEvent, FormEvent } from "react"; export interface SearchHandler { + docSelected: string; + handleDocSelect: (doc: string) => void; query: string; isLoading: boolean; onInputChange?: (e: ChangeEvent) => void; diff --git a/frontend/app/components/ui/search/useSearch.tsx b/frontend/app/components/ui/search/useSearch.tsx index 67cb88a..f79e984 100644 --- a/frontend/app/components/ui/search/useSearch.tsx +++ b/frontend/app/components/ui/search/useSearch.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { SearchResult } from "@/app/components/ui/search/search.interface"; +import { useSession } from 'next-auth/react'; interface UseSearchResult { searchResults: SearchResult[]; @@ -15,6 +16,9 @@ const useSearch = (): UseSearchResult => { const [searchResults, setSearchResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isSearchButtonPressed, setIsSearchButtonPressed] = useState(false); + const { data: session, status } = useSession(); + // console.log('session:', session, 'status:', status); + const supabaseAccessToken = session?.supabaseAccessToken; const handleSearch = async (query: string): Promise => { setIsSearchButtonPressed(isSearchButtonPressed); @@ -38,6 +42,10 @@ const useSearch = (): UseSearchResult => { } const response = await fetch(`${search_api}?query=${query}`, { signal: AbortSignal.timeout(120000), // Abort the request if it takes longer than 120 seconds + // Add the access token to the request headers + headers: { + 'Authorization': `Bearer ${supabaseAccessToken}`, + } }); const data = await response.json(); setSearchResults(data); diff --git a/frontend/app/privacy-policy/page.tsx b/frontend/app/privacy-policy/page.tsx index 118c006..ecc0943 100644 --- a/frontend/app/privacy-policy/page.tsx +++ b/frontend/app/privacy-policy/page.tsx @@ -3,7 +3,7 @@ const PrivacyPolicyPage: React.FC = () => { return (
    -
    +

    Privacy Policy

    diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx new file mode 100644 index 0000000..0b72460 --- /dev/null +++ b/frontend/app/profile/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { Skeleton } from "@nextui-org/react"; +import Image from 'next/image'; +import { User2 } from 'lucide-react'; + +const ProfilePage: React.FC = () => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [imageURL, setImageURL] = useState(''); + const [isLoaded, setIsLoaded] = useState(false); + + const toggleIsLoaded = () => { + setIsLoaded(!isLoaded); + } + + const handleNameChange = (event: React.ChangeEvent) => { + setName(event.target.value); + }; + + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.target.value); + }; + + const handleImageURLChange = (event: React.ChangeEvent) => { + setImageURL(event.target.value); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // TODO: Handle form submission logic + }; + + // Prefill the form with the user's profile data + useEffect(() => { + // Fetch the user's profile data + fetch('/api/profile') + .then((response) => { + if (!response.ok) { + throw new Error('Failed to fetch profile data'); + } + return response.json(); + }) + .then((data) => { + console.log('Profile data:', data); + setName(data.userData.name); + setEmail(data.userData.email); + setImageURL(data.userData.image); + }) + .catch((error) => { + console.error('Error fetching profile data:', error); + }) + .finally(() => { + // Set loading state to false after the fetch request completes (whether successfully or with an error) + toggleIsLoaded(); + }); + }, []); + + return ( +
    +
    +
    +

    Profile Settings

    + + {/* use default user image if there is no imageURL */} + {imageURL ? {name} : } + +
    + + +