khronoz commited on
Commit
b4297ca
Β·
unverified Β·
1 Parent(s): af2f78e

V.0.2.0 (#23)

Browse files

* Bugfix: Not redirecting to page URL the sign-in is initiated from.

* Added base for switching between LLMs

* Update run.py to use environment settings

* Updated Context Prompt for Chat

* Updated README

* Updated auth with supabase backend & working SGID info retrieval & session checking

* Added proper auth checking for backend api endpoints

* Updated frontend functions to pass AccessToken in request header

* Bugfix: Slicing results only when results is not empty

* Profile Page Placeholder

* Package Updates

* Update gitignore

* Removed debug log of response sources

* Updated gitignore

* Updated example.env

* Added option for storing Vector DB in Supabase pgvector

* Fixed loading remote vector DB

* Added Disclaimer to Footer & Dialog

* Moved Disclaimer into input container

* Updated frontend for chat with option to select between PSSCOC & EIR docs

* Updated Search frontend with options to search in PSSCOC & EIR docs + Refactored coded

* Updated About Page

* Increased container width

* Update packages & fixed linting

* Updated backend to use remote vector DB and improved context prompt

* Updated function to send selected document set in req body

* Updated example .env

* Updated to use env val for api key header name

* Make supabase URL env var non public

* Added supabase to healthcheck

* Fixed query wrong count

* Updated Profile page to pull data from database & skeleton loading

* Fixed bug not able to sign-in when no callbackUrl is set

* Removed previous feat, added upgrade in progress banner

* Use default user icon if no imageURL

* Moved PSSCOC Documents into own folder

* Update .gitignore: ignore EIR data folder

* Fixed poetry lock file

* Updated profile api route

* Updated node.js action

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .github/workflows/node-js-tests.yml +8 -2
  2. README.md +2 -2
  3. backend/.gitignore +8 -1
  4. backend/backend/app/api/routers/chat.py +21 -9
  5. backend/backend/app/api/routers/healthcheck.py +34 -5
  6. backend/backend/app/api/routers/query.py +2 -1
  7. backend/backend/app/api/routers/search.py +4 -2
  8. backend/backend/app/utils/auth.py +141 -0
  9. backend/backend/app/utils/contants.py +5 -0
  10. backend/backend/app/utils/index.py +193 -75
  11. backend/backend/data/{About PSSCOC β†’ PSSCOC/About PSSCOC}/About PSSCOC.json +0 -0
  12. backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/list-of-amendments-for-psscoc-for-construction-works-2020.pdf +0 -0
  13. backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/option-module-c-on-collaborative-contracting-with-sidp.pdf +0 -0
  14. backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/option-module-e-on-collaborative-contracting---sent.pdf +0 -0
  15. backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/psscoc-for-construction-works-2020.pdf +0 -0
  16. backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/supplement-for-psscoc-for-construction-works-2020.docx +0 -0
  17. backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/list-of-amendments-psscoc-design-build-2020.pdf +0 -0
  18. backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/option-module-c-on-collaborative-contracting-wo-sidp.pdf +0 -0
  19. backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/option-module-c-on-collaborative-contracting.pdf +0 -0
  20. backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/psscoc-for-design-build-2020.pdf +0 -0
  21. backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/supplement-for-psscoc-for-design-build-2020.docx +0 -0
  22. backend/backend/data/{Standard Conditions for Nominated Sub-Contract (NSC) β†’ PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)}/1_nsc_constnwks.pdf +0 -0
  23. backend/backend/data/{Standard Conditions for Nominated Sub-Contract (NSC) β†’ PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)}/2_nsc_supplem.docx +0 -0
  24. backend/backend/data/{Standard Conditions for Nominated Sub-Contract (NSC) β†’ PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)}/3_nsc_clarify_amendm.pdf +0 -0
  25. backend/backend/data/{Standard Conditions for Nominated Sub-Contract (NSC) β†’ PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)}/4_nsc_amendm.pdf +0 -0
  26. backend/backend/main.py +10 -6
  27. backend/backend/run.py +38 -1
  28. backend/example.env +35 -1
  29. backend/poetry.lock +352 -79
  30. backend/pyproject.toml +3 -0
  31. frontend/app/about/page.tsx +58 -1
  32. frontend/app/api/profile/route.ts +46 -0
  33. frontend/app/api/status/route.ts +28 -0
  34. frontend/app/components/chat-section.tsx +48 -20
  35. frontend/app/components/footer.tsx +24 -7
  36. frontend/app/components/header.tsx +65 -9
  37. frontend/app/components/query-section.tsx +20 -4
  38. frontend/app/components/search-section.tsx +35 -20
  39. frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx +14 -5
  40. frontend/app/components/ui/autofill-prompt/autofill-prompt.interface.tsx +9 -1
  41. frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx +14 -4
  42. frontend/app/components/ui/autofill-prompt/index.ts +4 -0
  43. frontend/app/components/ui/chat/chat-input.tsx +29 -26
  44. frontend/app/components/ui/chat/chat-messages.tsx +19 -17
  45. frontend/app/components/ui/chat/chat-selection.tsx +55 -0
  46. frontend/app/components/ui/chat/chat.interface.ts +2 -0
  47. frontend/app/components/ui/chat/index.ts +2 -1
  48. frontend/app/components/ui/login-buttons.tsx +17 -4
  49. frontend/app/components/ui/mobilemenu.tsx +1 -1
  50. frontend/app/components/ui/navlink.tsx +5 -4
.github/workflows/node-js-tests.yml CHANGED
@@ -31,10 +31,16 @@ jobs:
31
  cache-dependency-path: ./frontend/package-lock.json
32
  - name: Install Dependencies
33
  working-directory: ./frontend
34
- run: npm ci
35
- - name: Build Package
 
 
 
36
  working-directory: ./frontend
37
  run: npm run build --if-present
 
 
 
38
  # - name: Run Test
39
  # working-directory: ./frontend
40
  # run: npm test
 
31
  cache-dependency-path: ./frontend/package-lock.json
32
  - name: Install Dependencies
33
  working-directory: ./frontend
34
+ run: npm install
35
+ - name: Disable Next.js Telemetry
36
+ working-directory: ./frontend
37
+ run: npx next telemetry disable
38
+ - name: Test Build Package
39
  working-directory: ./frontend
40
  run: npm run build --if-present
41
+ env:
42
+ SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
43
+ SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
44
  # - name: Run Test
45
  # working-directory: ./frontend
46
  # run: npm test
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: πŸ“
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
- python_version: 3.11.4
8
  app_port: 8000
9
  pinned: false
10
  ---
@@ -74,7 +74,7 @@ For more information, see the [DEPLOYMENT.md](./DEPLOYMENT.md).
74
  - [Python](https://python.org/) - Backend Server Environment
75
  - [FastAPI](https://fastapi.tiangolo.com/) - Backend API Web Framework
76
  - [LlamaIndex](https://www.llamaindex.ai/) - Data Framework for LLM
77
- - [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) - LlamaIndex Application Bootstrap Tool
78
 
79
  ## πŸ“‘ Contributing <a name = "contributing"></a>
80
 
 
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
+ python_version: 3.11.8
8
  app_port: 8000
9
  pinned: false
10
  ---
 
74
  - [Python](https://python.org/) - Backend Server Environment
75
  - [FastAPI](https://fastapi.tiangolo.com/) - Backend API Web Framework
76
  - [LlamaIndex](https://www.llamaindex.ai/) - Data Framework for LLM
77
+ - [create-llama](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) - LlamaIndex Application Bootstrap Tool
78
 
79
  ## πŸ“‘ Contributing <a name = "contributing"></a>
80
 
backend/.gitignore CHANGED
@@ -1,3 +1,10 @@
 
1
  __pycache__
2
  storage
3
- .env
 
 
 
 
 
 
 
1
+ # Production Files
2
  __pycache__
3
  storage
4
+ .env
5
+ **/data/EIR/
6
+
7
+ # Dev Folders
8
+ test.py
9
+ storage backup
10
+ data backup
backend/backend/app/api/routers/chat.py CHANGED
@@ -4,17 +4,17 @@ from typing import List
4
  from fastapi import APIRouter, Depends, HTTPException, Request, status
5
  from fastapi.responses import StreamingResponse
6
  from fastapi.websockets import WebSocketDisconnect
7
- from llama_index import VectorStoreIndex
8
  from llama_index.llms.base import ChatMessage
9
  from llama_index.llms.types import MessageRole
10
  from llama_index.memory import ChatMemoryBuffer
11
  from llama_index.prompts import PromptTemplate
12
  from pydantic import BaseModel
13
 
 
14
  from backend.app.utils.index import get_index
15
  from backend.app.utils.json import json_to_model
16
 
17
- chat_router = r = APIRouter()
18
 
19
  """
20
  This router is for chatbot functionality which consist of chat memory and chat engine.
@@ -34,6 +34,7 @@ class _Message(BaseModel):
34
 
35
  class _ChatData(BaseModel):
36
  messages: List[_Message]
 
37
 
38
 
39
  # custom prompt template to be used by chat engine
@@ -69,8 +70,13 @@ async def chat(
69
  # Note: To support clients sending a JSON object using content-type "text/plain",
70
  # we need to use Depends(json_to_model(_ChatData)) here
71
  data: _ChatData = Depends(json_to_model(_ChatData)),
72
- index: VectorStoreIndex = Depends(get_index),
73
  ):
 
 
 
 
 
 
74
  # check preconditions and get last message
75
  if len(data.messages) == 0:
76
  raise HTTPException(
@@ -92,8 +98,6 @@ async def chat(
92
  for m in data.messages
93
  ]
94
 
95
- logger = logging.getLogger("uvicorn")
96
-
97
  # query_engine = index.as_query_engine()
98
  # chat_engine = CondenseQuestionChatEngine.from_defaults(
99
  # query_engine=query_engine,
@@ -102,9 +106,11 @@ async def chat(
102
  # verbose=True,
103
  # )
104
 
 
 
105
  memory = ChatMemoryBuffer.from_defaults(
106
  chat_history=messages,
107
- token_limit=2000,
108
  )
109
 
110
  logger.info(f"Memory: {memory.get()}")
@@ -114,17 +120,23 @@ async def chat(
114
  chat_mode="condense_plus_context",
115
  memory=memory,
116
  context_prompt=(
117
- "You are a chatbot, able to have normal interactions, as well as talk"
118
- " about information from documents regarding Public Sector Standard Conditions Of Contract (PSSCOC)."
 
119
  "Here are the relevant documents for the context:\n"
120
  "{context_str}"
121
- "\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."
 
 
 
122
  ),
123
  )
124
  response = chat_engine.stream_chat(
125
  message=lastMessage.content, chat_history=messages
126
  )
127
 
 
 
128
  # stream response
129
  async def event_generator():
130
  try:
 
4
  from fastapi import APIRouter, Depends, HTTPException, Request, status
5
  from fastapi.responses import StreamingResponse
6
  from fastapi.websockets import WebSocketDisconnect
 
7
  from llama_index.llms.base import ChatMessage
8
  from llama_index.llms.types import MessageRole
9
  from llama_index.memory import ChatMemoryBuffer
10
  from llama_index.prompts import PromptTemplate
11
  from pydantic import BaseModel
12
 
13
+ from backend.app.utils import auth
14
  from backend.app.utils.index import get_index
15
  from backend.app.utils.json import json_to_model
16
 
17
+ chat_router = r = APIRouter(dependencies=[Depends(auth.validate_user)])
18
 
19
  """
20
  This router is for chatbot functionality which consist of chat memory and chat engine.
 
34
 
35
  class _ChatData(BaseModel):
36
  messages: List[_Message]
37
+ document: str
38
 
39
 
40
  # custom prompt template to be used by chat engine
 
70
  # Note: To support clients sending a JSON object using content-type "text/plain",
71
  # we need to use Depends(json_to_model(_ChatData)) here
72
  data: _ChatData = Depends(json_to_model(_ChatData)),
 
73
  ):
74
+ logger = logging.getLogger("uvicorn")
75
+ # get the document set selected from the request body
76
+ document_set = data.document
77
+ logger.info(f"Document Set: {document_set}")
78
+ # get the index for the selected document set
79
+ index = get_index(collection_name=document_set)
80
  # check preconditions and get last message
81
  if len(data.messages) == 0:
82
  raise HTTPException(
 
98
  for m in data.messages
99
  ]
100
 
 
 
101
  # query_engine = index.as_query_engine()
102
  # chat_engine = CondenseQuestionChatEngine.from_defaults(
103
  # query_engine=query_engine,
 
106
  # verbose=True,
107
  # )
108
 
109
+ logger.info(f"Messages: {messages}")
110
+
111
  memory = ChatMemoryBuffer.from_defaults(
112
  chat_history=messages,
113
+ token_limit=3900,
114
  )
115
 
116
  logger.info(f"Memory: {memory.get()}")
 
120
  chat_mode="condense_plus_context",
121
  memory=memory,
122
  context_prompt=(
123
+ "You are a helpful chatbot, able to have normal interactions, as well as answer questions"
124
+ " regarding information relating to the Public Sector Standard Conditions Of Contract (PSSCOC) Documents and JTC's Employer Information Requirements (EIR) Documents.\n"
125
+ "All the documents are in the context of the construction industry in Singapore.\n"
126
  "Here are the relevant documents for the context:\n"
127
  "{context_str}"
128
+ "\nInstruction: Based on the above documents, provide a detailed answer for the user question below.\n"
129
+ "If you cannot answer the question or are unsure of how to answer, inform the user that you do not know.\n"
130
+ "If you need to clarify the question, ask the user for clarification.\n"
131
+ "You are to provide the relevant sources of which you got the information from in the context in brackets."
132
  ),
133
  )
134
  response = chat_engine.stream_chat(
135
  message=lastMessage.content, chat_history=messages
136
  )
137
 
138
+ # logger.info(f"Response Sources: {response.source_nodes}")
139
+
140
  # stream response
141
  async def event_generator():
142
  try:
backend/backend/app/api/routers/healthcheck.py CHANGED
@@ -1,6 +1,12 @@
1
- from fastapi import APIRouter, Request
 
2
 
3
- healthcheck_router = r = APIRouter()
 
 
 
 
 
4
 
5
  """
6
  This router is for healthcheck functionality.
@@ -19,14 +25,37 @@ async def healthcheck(
19
  # else:
20
  # results["index"] = False
21
 
 
 
22
  # TODO: check if other services are ready
23
 
24
- # logger.info("Healthcheck: {results}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- results = {"status": "OK"}
 
27
  return results
28
 
29
 
30
  # Simple test to check if the healthcheck endpoint is working
31
  def test_healthcheck():
32
- assert healthcheck() == {"status": "OK"}
 
1
+ import logging
2
+ import os
3
 
4
+ from fastapi import APIRouter, Depends, Request
5
+ from supabase import Client, ClientOptions, create_client
6
+
7
+ from backend.app.utils import auth
8
+
9
+ healthcheck_router = r = APIRouter(dependencies=[Depends(auth.validate_user)])
10
 
11
  """
12
  This router is for healthcheck functionality.
 
25
  # else:
26
  # results["index"] = False
27
 
28
+ logger = logging.getLogger("uvicorn")
29
+
30
  # TODO: check if other services are ready
31
 
32
+ # Try to connect to supabase
33
+ supabase_url: str = os.environ.get("SUPABASE_URL")
34
+ supabase_key: str = os.environ.get("SUPABASE_ANON_KEY")
35
+
36
+ supabase: Client = create_client(
37
+ supabase_url=supabase_url,
38
+ supabase_key=supabase_key,
39
+ options=ClientOptions(
40
+ postgrest_client_timeout=10,
41
+ storage_client_timeout=10,
42
+ ),
43
+ )
44
+
45
+ response = supabase.table("users").select("id", count="exact").execute()
46
+
47
+ # logger.info(f"Supabase: {response}")
48
+
49
+ if response.count is not None:
50
+ results["supabase"] = True
51
+ else:
52
+ results["supabase"] = False
53
 
54
+ results["backend"] = True
55
+ logger.debug(f"Healthcheck: {results}")
56
  return results
57
 
58
 
59
  # Simple test to check if the healthcheck endpoint is working
60
  def test_healthcheck():
61
+ assert healthcheck() == {"status": True, "supabase": True}
backend/backend/app/api/routers/query.py CHANGED
@@ -8,10 +8,11 @@ from llama_index import VectorStoreIndex
8
  from llama_index.llms.types import MessageRole
9
  from pydantic import BaseModel
10
 
 
11
  from backend.app.utils.index import get_index
12
  from backend.app.utils.json import json_to_model
13
 
14
- query_router = r = APIRouter()
15
 
16
  """
17
  This router is for query functionality which consist of query engine.
 
8
  from llama_index.llms.types import MessageRole
9
  from pydantic import BaseModel
10
 
11
+ from backend.app.utils import auth
12
  from backend.app.utils.index import get_index
13
  from backend.app.utils.json import json_to_model
14
 
15
+ query_router = r = APIRouter(dependencies=[Depends(auth.validate_user)])
16
 
17
  """
18
  This router is for query functionality which consist of query engine.
backend/backend/app/api/routers/search.py CHANGED
@@ -6,9 +6,10 @@ from llama_index import VectorStoreIndex
6
  from llama_index.postprocessor import SimilarityPostprocessor
7
  from llama_index.retrievers import VectorIndexRetriever
8
 
 
9
  from backend.app.utils.index import get_index
10
 
11
- search_router = r = APIRouter()
12
 
13
  """
14
  This router is for search functionality which consist of query engine.
@@ -22,8 +23,9 @@ Instead it returns the relevant information from the index.
22
  async def search(
23
  request: Request,
24
  index: VectorStoreIndex = Depends(get_index),
 
25
  ):
26
- query = request.query_params.get("query")
27
  logger = logging.getLogger("uvicorn")
28
  logger.info(f"Search: {query}")
29
  if query is None:
 
6
  from llama_index.postprocessor import SimilarityPostprocessor
7
  from llama_index.retrievers import VectorIndexRetriever
8
 
9
+ from backend.app.utils import auth
10
  from backend.app.utils.index import get_index
11
 
12
+ search_router = r = APIRouter(dependencies=[Depends(auth.validate_user)])
13
 
14
  """
15
  This router is for search functionality which consist of query engine.
 
23
  async def search(
24
  request: Request,
25
  index: VectorStoreIndex = Depends(get_index),
26
+ query: str = None,
27
  ):
28
+ # query = request.query_params.get("query")
29
  logger = logging.getLogger("uvicorn")
30
  logger.info(f"Search: {query}")
31
  if query is None:
backend/backend/app/utils/auth.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import time
4
+ from typing import Dict
5
+
6
+ import jwt
7
+ from dotenv import load_dotenv
8
+ from fastapi import HTTPException, Security
9
+ from fastapi.security import APIKeyHeader
10
+ from supabase import Client, ClientOptions, create_client
11
+
12
+ load_dotenv()
13
+
14
+ # Retrieve the API key header name from the environment
15
+ API_AUTH_HEADER_NAME: str = os.getenv(
16
+ key="API_AUTH_HEADER_NAME", default="Authorization"
17
+ )
18
+
19
+ # Retrieve the Backend API key header from the environment
20
+ API_KEY_HEADER_NAME: str = os.getenv(key="API_KEY_HEADER_NAME", default="X-API-Key")
21
+
22
+ # Create an API key header instance
23
+ API_AUTH_HEADER = APIKeyHeader(name=API_AUTH_HEADER_NAME, auto_error=False)
24
+
25
+ # Retrieve the API key from the environment
26
+ BACKEND_API_KEY: str = os.getenv(key="BACKEND_API_KEY")
27
+
28
+ # Create an API key header instance
29
+ API_KEY_HEADER = APIKeyHeader(name=API_KEY_HEADER_NAME, auto_error=False)
30
+
31
+ JWT_SECRET: str = os.getenv("SUPABASE_JWT_SECRET")
32
+ JWT_ALGORITHM = "HS256"
33
+
34
+
35
+ def verify_jwt(jwtoken: str) -> bool:
36
+ """Verify the JWT token and return True if the token is valid, else return False"""
37
+ isTokenValid: bool = False
38
+
39
+ try:
40
+ payload = decodeJWT(jwtoken)
41
+ except Exception:
42
+ payload = None
43
+ if payload:
44
+ isTokenValid = True
45
+ return isTokenValid
46
+
47
+
48
+ def decodeJWT(token: str) -> Dict:
49
+ """Decode the JWT token and return the payload if the token is valid, else return None"""
50
+ try:
51
+ decoded_token = jwt.decode(
52
+ token, JWT_SECRET, algorithms=[JWT_ALGORITHM], options={"verify_aud": False}
53
+ )
54
+ return decoded_token if decoded_token["exp"] >= time.time() else None
55
+ except Exception:
56
+ return None
57
+
58
+
59
+ def get_user_from_JWT(token: str):
60
+ """Get the user id from the JWT token and return True if the user exists, else return False"""
61
+ supabase_url: str = os.environ.get("SUPABASE_URL")
62
+ supabase_key: str = os.environ.get("SUPABASE_ANON_KEY")
63
+
64
+ supabase: Client = create_client(
65
+ supabase_url=supabase_url,
66
+ supabase_key=supabase_key,
67
+ options=ClientOptions(
68
+ postgrest_client_timeout=10,
69
+ storage_client_timeout=10,
70
+ ),
71
+ )
72
+
73
+ payload = decodeJWT(token)
74
+ user_id = payload["sub"]
75
+
76
+ if user_id is not None:
77
+ # Try to get the user from the database using the user_id
78
+ response = supabase.table("users").select("*").eq("id", user_id).execute()
79
+ # print(response.data)
80
+ if len(response.data) == 0:
81
+ return False
82
+ return True
83
+ return False
84
+
85
+
86
+ async def validate_user(
87
+ auth_token: str = Security(dependency=API_AUTH_HEADER),
88
+ api_key: str = Security(dependency=API_KEY_HEADER),
89
+ ):
90
+ try:
91
+ logger = logging.getLogger("uvicorn")
92
+ # logger.debug(f"Auth Token: {auth_token} | API Key: {api_key}")
93
+ if auth_token is not None or api_key is not None:
94
+ # If the access token is empty, use the 'X-API-Key' from the header
95
+ if auth_token is None:
96
+ # Access the 'X-API-Key' header directly
97
+ if BACKEND_API_KEY is None:
98
+ raise ValueError("Backend API key is not set in Backend Service!")
99
+ # If the 'X-API-Key' does not match the backend API key, raise an error
100
+ if api_key != BACKEND_API_KEY:
101
+ raise ValueError(
102
+ "Invalid API key provided in the 'X-API-Key' header!"
103
+ )
104
+ else:
105
+ logger.info("Validated API key successfully!")
106
+ return None
107
+ else:
108
+ auth_token = (
109
+ auth_token.strip()
110
+ ) # Remove leading and trailing whitespaces
111
+ isBearer = auth_token.startswith(
112
+ "Bearer"
113
+ ) # Check if the token starts with 'Bearer'
114
+ jwtoken = auth_token.split("Bearer ")[
115
+ 1
116
+ ] # Extract the token from the 'Bearer' string
117
+ if JWT_SECRET is None:
118
+ raise ValueError(
119
+ "Supabase JWT Secret is not set in Backend Service!"
120
+ )
121
+ if not isBearer:
122
+ return (
123
+ "Invalid token scheme. Please use the format 'Bearer [token]'"
124
+ )
125
+ # Verify the JWT token is valid
126
+ if verify_jwt(jwtoken=jwtoken) is None:
127
+ return "Invalid token. Please provide a valid token."
128
+ # Check if the user exists in the database
129
+ if get_user_from_JWT(token=jwtoken):
130
+ logger.info("Validated User's Auth Token successfully!")
131
+ return None
132
+ else:
133
+ raise ValueError("User does not exist in the database!")
134
+ else:
135
+ raise ValueError(
136
+ "Either Access token [Authorization] or API key [X-API-Key] needed!"
137
+ )
138
+ except Exception as e:
139
+ logger = logging.getLogger("uvicorn")
140
+ logger.error(f"Error validating Auth Token / API key: {e}")
141
+ raise HTTPException(status_code=401, detail=f"Unauthorized - {e}")
backend/backend/app/utils/contants.py CHANGED
@@ -30,6 +30,11 @@ CHUNK_OVERLAP = 100
30
  # Embedding Model Constants
31
  EMBED_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
32
  EMBED_POOLING = "mean"
 
 
 
 
 
33
 
34
  # Prompt Helper Constants
35
  # set maximum input size
 
30
  # Embedding Model Constants
31
  EMBED_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
32
  EMBED_POOLING = "mean"
33
+ EMBED_MODEL_DIMENSIONS = 384 # MiniLM-L6-v2 uses 384 dimensions
34
+ DEF_EMBED_MODEL_DIMENSIONS = (
35
+ 1536 # Default embedding model dimensions used by OpenAI text-embedding-ada-002
36
+ )
37
+ EMBED_BATCH_SIZE = 100 # batch size for openai embeddings
38
 
39
  # Prompt Helper Constants
40
  # set maximum input size
backend/backend/app/utils/index.py CHANGED
@@ -1,9 +1,11 @@
1
  import logging
2
  import os
3
 
 
4
  from llama_index import (
5
  PromptHelper,
6
  ServiceContext,
 
7
  SimpleDirectoryReader,
8
  StorageContext,
9
  VectorStoreIndex,
@@ -11,11 +13,14 @@ from llama_index import (
11
  set_global_service_context,
12
  )
13
  from llama_index.embeddings import HuggingFaceEmbedding
14
- from llama_index.llms import LlamaCPP
 
15
  from llama_index.llms.llama_utils import (
16
  completion_to_prompt,
17
  messages_to_prompt,
18
  )
 
 
19
 
20
  from backend.app.utils.contants import (
21
  CHUNK_OVERLAP,
@@ -24,7 +29,10 @@ from backend.app.utils.contants import (
24
  CHUNK_SIZE_LIMIT,
25
  CONTEXT_SIZE,
26
  DATA_DIR,
 
27
  DEVICE_TYPE,
 
 
28
  EMBED_MODEL_NAME,
29
  EMBED_POOLING,
30
  LLM_MODEL_URL,
@@ -35,91 +43,201 @@ from backend.app.utils.contants import (
35
  STORAGE_DIR,
36
  )
37
 
38
- llm = LlamaCPP(
39
- model_url=LLM_MODEL_URL,
40
- temperature=LLM_TEMPERATURE,
41
- max_new_tokens=MAX_NEW_TOKENS,
42
- context_window=CONTEXT_SIZE,
43
- # kwargs to pass to __call__()
44
- generate_kwargs={},
45
- # kwargs to pass to __init__()
46
- model_kwargs=MODEL_KWARGS,
47
- # transform inputs into Llama2 format
48
- messages_to_prompt=messages_to_prompt,
49
- completion_to_prompt=completion_to_prompt,
50
- verbose=True,
51
- )
52
 
53
- embed_model = HuggingFaceEmbedding(
54
- model_name=EMBED_MODEL_NAME,
55
- pooling=EMBED_POOLING,
56
- device=DEVICE_TYPE,
57
- )
58
 
59
- prompt_helper = PromptHelper(
60
- chunk_size_limit=CHUNK_SIZE_LIMIT,
61
- chunk_overlap_ratio=CHUNK_OVERLAP_RATIO,
62
- num_output=NUM_OUTPUT,
63
- )
64
 
65
- service_context = ServiceContext.from_defaults(
66
- llm=llm,
67
- embed_model=embed_model,
68
- chunk_size=CHUNK_SIZE,
69
- chunk_overlap=CHUNK_OVERLAP,
70
- prompt_helper=prompt_helper,
71
- )
72
 
73
- set_global_service_context(service_context)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
 
76
  def create_index():
77
- logger = logging.getLogger("uvicorn")
78
- # check if storage already exists
79
- if not os.path.exists(STORAGE_DIR):
80
- logger.info("Creating new index")
81
- # load the documents and create the index
82
- try:
83
- documents = SimpleDirectoryReader(
84
- input_dir=DATA_DIR, recursive=True
85
- ).load_data()
86
- except ValueError as e:
87
- logger.error(f"{e}")
88
- index = VectorStoreIndex.from_documents(
89
- documents=documents, service_context=service_context, show_progress=True
90
- )
91
- # store it for later
92
- index.storage_context.persist(STORAGE_DIR)
93
- logger.info(f"Finished creating new index. Stored in {STORAGE_DIR}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  else:
95
- # do nothing
96
- logger.info(f"Index already exist at {STORAGE_DIR}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
 
99
- def load_existing_index():
100
  # load the existing index
101
- logger = logging.getLogger("uvicorn")
102
- logger.info(f"Loading index from {STORAGE_DIR}...")
103
- storage_context = StorageContext.from_defaults(persist_dir=STORAGE_DIR)
104
- index = load_index_from_storage(storage_context, service_context=service_context)
105
- logger.info(f"Finished loading index from {STORAGE_DIR}")
106
- return index
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
 
109
- def get_index():
110
- # check if storage already exists
111
- if not os.path.exists(STORAGE_DIR):
112
- # create the index if it does not exist
113
- create_index()
114
- # load the index from storage
115
- index = load_existing_index()
116
- # check if storage is empty, 4 files should be present if using simplevectorstore
117
- elif os.path.exists(STORAGE_DIR) and len(os.listdir(STORAGE_DIR)) < 4:
118
- # create the index if it does not exist
119
- create_index()
120
- # load the index from storage
121
- index = load_existing_index()
122
- else:
123
- # load the index from storage
124
- index = load_existing_index()
125
  return index
 
1
  import logging
2
  import os
3
 
4
+ from dotenv import load_dotenv
5
  from llama_index import (
6
  PromptHelper,
7
  ServiceContext,
8
+ # Document,
9
  SimpleDirectoryReader,
10
  StorageContext,
11
  VectorStoreIndex,
 
13
  set_global_service_context,
14
  )
15
  from llama_index.embeddings import HuggingFaceEmbedding
16
+ from llama_index.embeddings.openai import OpenAIEmbedding
17
+ from llama_index.llms import LlamaCPP, OpenAI
18
  from llama_index.llms.llama_utils import (
19
  completion_to_prompt,
20
  messages_to_prompt,
21
  )
22
+ from llama_index.vector_stores.supabase import SupabaseVectorStore
23
+ from vecs import IndexMeasure
24
 
25
  from backend.app.utils.contants import (
26
  CHUNK_OVERLAP,
 
29
  CHUNK_SIZE_LIMIT,
30
  CONTEXT_SIZE,
31
  DATA_DIR,
32
+ DEF_EMBED_MODEL_DIMENSIONS,
33
  DEVICE_TYPE,
34
+ EMBED_BATCH_SIZE,
35
+ EMBED_MODEL_DIMENSIONS,
36
  EMBED_MODEL_NAME,
37
  EMBED_POOLING,
38
  LLM_MODEL_URL,
 
43
  STORAGE_DIR,
44
  )
45
 
46
+ # from llama_index.vector_stores.supabase import SupabaseVectorStore
47
+ # import textwrap
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ load_dotenv()
50
+ logger = logging.getLogger("uvicorn")
 
 
 
51
 
52
+ # ENV variables
53
+ USE_LOCAL_LLM = bool(os.getenv("USE_LOCAL_LLM").lower() == "true")
54
+ USE_LOCAL_VECTOR_STORE = bool(os.getenv("USE_LOCAL_VECTOR_STORE").lower() == "true")
 
 
55
 
 
 
 
 
 
 
 
56
 
57
+ # use local LLM if USE_LOCAL_LLM is set to True, else use openai's API
58
+ if USE_LOCAL_LLM:
59
+ logger.info("Using local LLM...")
60
+ llm = LlamaCPP(
61
+ model_url=LLM_MODEL_URL,
62
+ temperature=LLM_TEMPERATURE,
63
+ max_new_tokens=MAX_NEW_TOKENS,
64
+ context_window=CONTEXT_SIZE,
65
+ # kwargs to pass to __call__()
66
+ generate_kwargs={},
67
+ # kwargs to pass to __init__()
68
+ model_kwargs=MODEL_KWARGS,
69
+ # transform inputs into Llama2 format
70
+ messages_to_prompt=messages_to_prompt,
71
+ completion_to_prompt=completion_to_prompt,
72
+ verbose=True,
73
+ )
74
+ embed_model = HuggingFaceEmbedding(
75
+ model_name=EMBED_MODEL_NAME,
76
+ pooling=EMBED_POOLING,
77
+ device=DEVICE_TYPE,
78
+ )
79
+
80
+ prompt_helper = PromptHelper(
81
+ chunk_size_limit=CHUNK_SIZE_LIMIT,
82
+ chunk_overlap_ratio=CHUNK_OVERLAP_RATIO,
83
+ num_output=NUM_OUTPUT,
84
+ )
85
+
86
+ service_context = ServiceContext.from_defaults(
87
+ llm=llm,
88
+ embed_model=embed_model,
89
+ chunk_size=CHUNK_SIZE,
90
+ chunk_overlap=CHUNK_OVERLAP,
91
+ prompt_helper=prompt_helper,
92
+ )
93
+
94
+ set_global_service_context(service_context)
95
+ else:
96
+ logger.info("Using OpenAI's API...")
97
+ llm = OpenAI(
98
+ model="gpt-3.5-turbo",
99
+ temperature=0.2,
100
+ api_key=os.getenv("OPENAI_API_KEY"),
101
+ )
102
+ # By default, LlamaIndex uses text-embedding-ada-002 from OpenAI
103
+ embed_model = OpenAIEmbedding(embed_batch_size=EMBED_BATCH_SIZE)
104
+
105
+ prompt_helper = PromptHelper(
106
+ chunk_size_limit=CHUNK_SIZE_LIMIT,
107
+ chunk_overlap_ratio=CHUNK_OVERLAP_RATIO,
108
+ num_output=NUM_OUTPUT,
109
+ )
110
+
111
+ service_context = ServiceContext.from_defaults(
112
+ llm=llm,
113
+ embed_model=embed_model,
114
+ chunk_size=CHUNK_SIZE,
115
+ chunk_overlap=CHUNK_OVERLAP,
116
+ prompt_helper=prompt_helper,
117
+ )
118
+
119
+ set_global_service_context(service_context)
120
 
121
 
122
  def create_index():
123
+ # if use local vector store, create & store the index locally
124
+ if USE_LOCAL_VECTOR_STORE:
125
+ # get the folders in the data directory
126
+ collection_names = os.listdir(DATA_DIR)
127
+ # to create each folder as a collection in local storage
128
+ for collection_name in collection_names:
129
+ logger.info(f"Checking if [{collection_names}] index exists locally...")
130
+ # build the new data directory
131
+ new_data_dir = os.path.join(DATA_DIR, collection_name)
132
+ # build the new storage directory
133
+ new_storage_dir = os.path.join(STORAGE_DIR, collection_name)
134
+ # check if storage folder and index files already exists
135
+ if (
136
+ not os.path.exists(new_storage_dir)
137
+ or len(os.listdir(new_storage_dir))
138
+ < 4 # 4 files should be present if using simplevectorstore
139
+ ):
140
+ logger.info(f"Creating [{collection_names}] index")
141
+ # load the documents and create the index
142
+ try:
143
+ documents = SimpleDirectoryReader(
144
+ input_dir=new_data_dir, recursive=True
145
+ ).load_data()
146
+ except ValueError as e:
147
+ logger.error(f"{e}")
148
+ index = VectorStoreIndex.from_documents(
149
+ documents=documents,
150
+ service_context=service_context,
151
+ show_progress=True,
152
+ )
153
+ # store it for later
154
+ index.storage_context.persist(STORAGE_DIR)
155
+ logger.info(f"Finished creating new index. Stored in {STORAGE_DIR}")
156
+ else:
157
+ # do nothing
158
+ logger.info(f"Index already exist at {STORAGE_DIR}...")
159
+ # else, create & store the index in Supabase pgvector
160
  else:
161
+ # get the folders in the data directory
162
+ collection_names = os.listdir(DATA_DIR)
163
+ # to create each folder as a collection in Supabase
164
+ for collection_name in collection_names:
165
+ # check if remote storage already exists
166
+ logger.info(f"Checking if [{collection_name}] index exists in Supabase...")
167
+ # set the dimension based on the LLM model used
168
+ dimension = (
169
+ EMBED_MODEL_DIMENSIONS if USE_LOCAL_LLM else DEF_EMBED_MODEL_DIMENSIONS
170
+ )
171
+ # create the vector store, will create the collection if it does not exist
172
+ vector_store = SupabaseVectorStore(
173
+ postgres_connection_string=os.getenv("POSTGRES_CONNECTION_STRING"),
174
+ collection_name=collection_name,
175
+ dimension=dimension,
176
+ )
177
+ # create the storage context
178
+ storage_context = StorageContext.from_defaults(vector_store=vector_store)
179
+ logger.info(f"Creating [{collection_name}] index")
180
+ # create the data directory
181
+ new_data_dir = os.path.join(DATA_DIR, collection_name)
182
+ # load the documents and create the index
183
+ try:
184
+ documents = SimpleDirectoryReader(
185
+ input_dir=new_data_dir, recursive=True
186
+ ).load_data()
187
+ except ValueError as e:
188
+ logger.error(f"{e}")
189
+ index = VectorStoreIndex.from_documents(
190
+ documents=documents,
191
+ storage_context=storage_context,
192
+ show_progress=True,
193
+ )
194
+ logger.info(f"Finished creating [{collection_name}] vector store")
195
 
196
 
197
+ def load_existing_index(collection_name="PSSCOC"):
198
  # load the existing index
199
+ if USE_LOCAL_VECTOR_STORE:
200
+ # create the storage directory
201
+ new_storage_dir = os.path.join(STORAGE_DIR, collection_name)
202
+ # load the index from local storage
203
+ logger.info(f"Loading [{collection_name}] index from {new_storage_dir}...")
204
+ storage_context = StorageContext.from_defaults(persist_dir=new_storage_dir)
205
+ index = load_index_from_storage(
206
+ storage_context, service_context=service_context
207
+ )
208
+ logger.info(
209
+ f"Finished loading [{collection_name}] index from {new_storage_dir}"
210
+ )
211
+ logger.info(f"Index ID: {index.index_id}")
212
+ return index
213
+ else:
214
+ # load the index from Supabase
215
+ logger.info(f"Loading [{collection_name}] index from Supabase...")
216
+ # set the dimension based on the LLM model used
217
+ dimension = (
218
+ EMBED_MODEL_DIMENSIONS if USE_LOCAL_LLM else DEF_EMBED_MODEL_DIMENSIONS
219
+ )
220
+ # create the vector store
221
+ vector_store = SupabaseVectorStore(
222
+ postgres_connection_string=os.getenv("POSTGRES_CONNECTION_STRING"),
223
+ collection_name=collection_name,
224
+ dimension=dimension,
225
+ )
226
+ # check if the vector store has been indexed
227
+ if not vector_store._collection.is_indexed_for_measure(
228
+ IndexMeasure.cosine_distance
229
+ ):
230
+ logger.info(f"Indexing [{collection_name}] vector store...")
231
+ vector_store._collection.create_index()
232
+ logger.info(f"Finished indexing [{collection_name}] vector store")
233
+ logger.info(vector_store._collection.name)
234
+ index = VectorStoreIndex.from_vector_store(vector_store=vector_store)
235
+ logger.info(f"Finished loading [{collection_name}] index from Supabase")
236
+ logger.info(f"Index ID: {index.index_id}")
237
+ return index
238
 
239
 
240
+ def get_index(collection_name):
241
+ # load the index from storage
242
+ index = load_existing_index(collection_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  return index
backend/backend/data/{About PSSCOC β†’ PSSCOC/About PSSCOC}/About PSSCOC.json RENAMED
File without changes
backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/list-of-amendments-for-psscoc-for-construction-works-2020.pdf RENAMED
File without changes
backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/option-module-c-on-collaborative-contracting-with-sidp.pdf RENAMED
File without changes
backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/option-module-e-on-collaborative-contracting---sent.pdf RENAMED
File without changes
backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/psscoc-for-construction-works-2020.pdf RENAMED
File without changes
backend/backend/data/{PSSCOC for Construction Works β†’ PSSCOC/PSSCOC for Construction Works}/supplement-for-psscoc-for-construction-works-2020.docx RENAMED
File without changes
backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/list-of-amendments-psscoc-design-build-2020.pdf RENAMED
File without changes
backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/option-module-c-on-collaborative-contracting-wo-sidp.pdf RENAMED
File without changes
backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/option-module-c-on-collaborative-contracting.pdf RENAMED
File without changes
backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/psscoc-for-design-build-2020.pdf RENAMED
File without changes
backend/backend/data/{PSSCOC for Design and Build β†’ PSSCOC/PSSCOC for Design and Build}/supplement-for-psscoc-for-design-build-2020.docx RENAMED
File without changes
backend/backend/data/{Standard Conditions for Nominated Sub-Contract (NSC) β†’ PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)}/1_nsc_constnwks.pdf RENAMED
File without changes
backend/backend/data/{Standard Conditions for Nominated Sub-Contract (NSC) β†’ PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)}/2_nsc_supplem.docx RENAMED
File without changes
backend/backend/data/{Standard Conditions for Nominated Sub-Contract (NSC) β†’ PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)}/3_nsc_clarify_amendm.pdf RENAMED
File without changes
backend/backend/data/{Standard Conditions for Nominated Sub-Contract (NSC) β†’ PSSCOC/Standard Conditions for Nominated Sub-Contract (NSC)}/4_nsc_amendm.pdf RENAMED
File without changes
backend/backend/main.py CHANGED
@@ -11,19 +11,20 @@ from backend.app.api.routers.chat import chat_router
11
  from backend.app.api.routers.healthcheck import healthcheck_router
12
  from backend.app.api.routers.query import query_router
13
  from backend.app.api.routers.search import search_router
14
- from backend.app.utils.index import create_index
15
 
16
  load_dotenv()
17
 
18
  app = FastAPI()
19
 
20
- environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set
21
 
22
  # Add allowed origins from environment variables
23
  allowed_origins = os.getenv("ALLOWED_ORIGINS", "*")
24
 
25
  if environment == "dev":
 
26
  logger = logging.getLogger("uvicorn")
 
27
  logger.warning("Running in development mode - allowing CORS for all origins")
28
  app.add_middleware(
29
  middleware_class=CORSMiddleware,
@@ -36,8 +37,9 @@ if environment == "dev":
36
  if environment == "prod":
37
  # In production, specify the allowed origins
38
  allowed_origins = allowed_origins.split(",") if allowed_origins != "*" else ["*"]
39
-
40
  logger = logging.getLogger("uvicorn")
 
41
  logger.info(f"Running in production mode - allowing CORS for {allowed_origins}")
42
  app.add_middleware(
43
  middleware_class=CORSMiddleware,
@@ -48,15 +50,17 @@ if environment == "prod":
48
  )
49
 
50
  logger.info(f"CUDA available: {is_cuda_available()}")
 
 
 
 
 
51
 
52
  app.include_router(chat_router, prefix="/api/chat")
53
  app.include_router(query_router, prefix="/api/query")
54
  app.include_router(search_router, prefix="/api/search")
55
  app.include_router(healthcheck_router, prefix="/api/healthcheck")
56
 
57
- # Try to create the index first on startup
58
- create_index()
59
-
60
 
61
  # Redirect to the /docs endpoint
62
  @app.get("/")
 
11
  from backend.app.api.routers.healthcheck import healthcheck_router
12
  from backend.app.api.routers.query import query_router
13
  from backend.app.api.routers.search import search_router
 
14
 
15
  load_dotenv()
16
 
17
  app = FastAPI()
18
 
19
+ environment = os.getenv("ENVIRONMENT", "dev") # Default to 'dev' if not set
20
 
21
  # Add allowed origins from environment variables
22
  allowed_origins = os.getenv("ALLOWED_ORIGINS", "*")
23
 
24
  if environment == "dev":
25
+ # In development, allow all origins, methods, and headers
26
  logger = logging.getLogger("uvicorn")
27
+ logger.level = logging.DEBUG
28
  logger.warning("Running in development mode - allowing CORS for all origins")
29
  app.add_middleware(
30
  middleware_class=CORSMiddleware,
 
37
  if environment == "prod":
38
  # In production, specify the allowed origins
39
  allowed_origins = allowed_origins.split(",") if allowed_origins != "*" else ["*"]
40
+ # Set the logger level to INFO
41
  logger = logging.getLogger("uvicorn")
42
+ logger.level = logging.INFO
43
  logger.info(f"Running in production mode - allowing CORS for {allowed_origins}")
44
  app.add_middleware(
45
  middleware_class=CORSMiddleware,
 
50
  )
51
 
52
  logger.info(f"CUDA available: {is_cuda_available()}")
53
+ logger.info("Use Local LLM: " + os.getenv("USE_LOCAL_LLM", "false"))
54
+ logger.info("Use Local Vector Store: " + os.getenv("USE_LOCAL_VECTOR_STORE", "false"))
55
+
56
+ # Set logger for httpx to WARNING
57
+ logging.getLogger("httpx").setLevel(logging.WARNING)
58
 
59
  app.include_router(chat_router, prefix="/api/chat")
60
  app.include_router(query_router, prefix="/api/query")
61
  app.include_router(search_router, prefix="/api/search")
62
  app.include_router(healthcheck_router, prefix="/api/healthcheck")
63
 
 
 
 
64
 
65
  # Redirect to the /docs endpoint
66
  @app.get("/")
backend/backend/run.py CHANGED
@@ -1,4 +1,41 @@
 
 
 
1
  import uvicorn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  if __name__ == "__main__":
4
- uvicorn.run(app="main:app", host="0.0.0.0", reload=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+
4
  import uvicorn
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ ENVIRONMENT = os.getenv("ENVIRONMENT", "dev") # Default to 'dev' if not set
10
+ CREATE_VECTOR_STORE = bool(
11
+ os.getenv("CREATE_VECTOR_STORE", "false").lower() == "true"
12
+ ) # Default to False if not set
13
+
14
+
15
+ def run_app():
16
+ # Run the app
17
+ if ENVIRONMENT == "dev":
18
+ # Run the app with the development settings to auto reload
19
+ uvicorn.run(app="main:app", host="0.0.0.0", reload=True)
20
+ if ENVIRONMENT == "prod":
21
+ # Run the app with the production settings, no auto reload
22
+ uvicorn.run(app="main:app", host="0.0.0.0", reload=False)
23
+
24
 
25
  if __name__ == "__main__":
26
+ logging_format = "%(levelname)s: %(message)s"
27
+ logging.basicConfig(level=logging.INFO, format=logging_format)
28
+ logger = logging.getLogger(__name__)
29
+ logger.info("Create vector store: " + str(CREATE_VECTOR_STORE))
30
+ if CREATE_VECTOR_STORE:
31
+ # Create the vector store
32
+ from backend.app.utils.index import create_index
33
+
34
+ logger.info("Creating vector stores first...")
35
+ create_index()
36
+ logger.info("Vector stores created successfully! Running App...")
37
+ # Run the app
38
+ run_app()
39
+ else:
40
+ # Run the app
41
+ run_app()
backend/example.env CHANGED
@@ -1 +1,35 @@
1
- ALLOWED_ORIGINS=http://localhost:3000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Setting up the environment
2
+
3
+ # Specify to start uvicorn app in dev or prod mode. default is dev if not set [prod|dev]
4
+ ENVIRONMENT=prod
5
+
6
+ # Allowed origins for CORS in production, separated by comma, no spaces
7
+ ALLOWED_ORIGINS=http://localhost:3000,https://smart-retrieval-demo.vercel.app
8
+
9
+ # Use Local LLM [true|false]
10
+ USE_LOCAL_LLM=true
11
+
12
+ # Use Local Vector Store [true|false]
13
+ USE_LOCAL_VECTOR_STORE=true
14
+
15
+ # Create the vector store [true|false], remember to change to false after creating for a one-time setup
16
+ CREATE_VECTOR_STORE=false
17
+
18
+ # OpenAI API Key
19
+ OPENAI_API_KEY=sk-YourOpenAIKey
20
+
21
+ # Backend API Authorization Settings
22
+ # Auth Header Name for the API
23
+ API_AUTH_HEADER_NAME=Authorization
24
+ # Alternate API Key Header Name for the API
25
+ API_KEY_HEADER_NAME=X-API-Key
26
+ # Ensure this key is the same in the frontend app environment
27
+ # https://generate-random.org/api-key-generator?count=1&length=128&type=mixed-numbers&prefix=sr-
28
+ BACKEND_API_KEY=sr-SomeRandomKey
29
+
30
+ # Supabase Settings
31
+ SUPABASE_URL=https://YourSupabaseProjectID.supabase.co
32
+ SUPABASE_ANON_KEY=YourSupabaseAnonKey
33
+ SUPABASE_JWT_SECRET=YourSupabaseJWTSecret
34
+ # Note: Rename 'postgres://' to 'postgresql://' in the connection string due to sqlalchemy not supporting 'postgres://'
35
+ POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/smart_retrieval
backend/poetry.lock CHANGED
@@ -349,6 +349,20 @@ wrapt = ">=1.10,<2"
349
  [package.extras]
350
  dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  [[package]]
353
  name = "dirtyjson"
354
  version = "1.0.8"
@@ -459,6 +473,22 @@ mccabe = ">=0.7.0,<0.8.0"
459
  pycodestyle = ">=2.11.0,<2.12.0"
460
  pyflakes = ">=3.2.0,<3.3.0"
461
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  [[package]]
463
  name = "frozenlist"
464
  version = "1.4.1"
@@ -580,6 +610,21 @@ smb = ["smbprotocol"]
580
  ssh = ["paramiko"]
581
  tqdm = ["tqdm"]
582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  [[package]]
584
  name = "greenlet"
585
  version = "3.0.3"
@@ -733,13 +778,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
733
 
734
  [[package]]
735
  name = "httpx"
736
- version = "0.27.0"
737
  description = "The next generation HTTP client."
738
  optional = false
739
  python-versions = ">=3.8"
740
  files = [
741
- {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
742
- {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
743
  ]
744
 
745
  [package.dependencies]
@@ -1560,6 +1605,19 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d
1560
  test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
1561
  xml = ["lxml (>=4.9.2)"]
1562
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1563
  [[package]]
1564
  name = "pluggy"
1565
  version = "1.4.0"
@@ -1575,6 +1633,104 @@ files = [
1575
  dev = ["pre-commit", "tox"]
1576
  testing = ["pytest", "pytest-benchmark"]
1577
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1578
  [[package]]
1579
  name = "pycodestyle"
1580
  version = "2.11.1"
@@ -1707,6 +1863,23 @@ files = [
1707
  {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
1708
  ]
1709
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1710
  [[package]]
1711
  name = "pypdf"
1712
  version = "3.17.4"
@@ -1832,7 +2005,6 @@ files = [
1832
  {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
1833
  {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
1834
  {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
1835
- {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
1836
  {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
1837
  {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
1838
  {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -1867,6 +2039,22 @@ files = [
1867
  {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
1868
  ]
1869
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1870
  [[package]]
1871
  name = "regex"
1872
  version = "2023.12.25"
@@ -2248,6 +2436,71 @@ anyio = ">=3.4.0,<5"
2248
  [package.extras]
2249
  full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
2250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2251
  [[package]]
2252
  name = "sympy"
2253
  version = "1.12"
@@ -2755,6 +3008,28 @@ files = [
2755
  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)"]
2756
  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)"]
2757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2758
  [[package]]
2759
  name = "watchfiles"
2760
  version = "0.21.0"
@@ -2844,83 +3119,81 @@ anyio = ">=3.0.0"
2844
 
2845
  [[package]]
2846
  name = "websockets"
2847
- version = "12.0"
2848
  description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
2849
  optional = false
2850
- python-versions = ">=3.8"
2851
  files = [
2852
- {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"},
2853
- {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"},
2854
- {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"},
2855
- {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"},
2856
- {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"},
2857
- {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"},
2858
- {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"},
2859
- {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"},
2860
- {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"},
2861
- {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"},
2862
- {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"},
2863
- {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"},
2864
- {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"},
2865
- {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"},
2866
- {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"},
2867
- {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"},
2868
- {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"},
2869
- {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"},
2870
- {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"},
2871
- {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"},
2872
- {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"},
2873
- {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"},
2874
- {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"},
2875
- {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"},
2876
- {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"},
2877
- {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"},
2878
- {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"},
2879
- {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"},
2880
- {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"},
2881
- {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"},
2882
- {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"},
2883
- {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"},
2884
- {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"},
2885
- {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"},
2886
- {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"},
2887
- {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"},
2888
- {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"},
2889
- {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"},
2890
- {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"},
2891
- {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"},
2892
- {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"},
2893
- {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"},
2894
- {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"},
2895
- {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"},
2896
- {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"},
2897
- {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"},
2898
- {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"},
2899
- {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"},
2900
- {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"},
2901
- {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"},
2902
- {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"},
2903
- {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"},
2904
- {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"},
2905
- {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"},
2906
- {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"},
2907
- {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"},
2908
- {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"},
2909
- {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"},
2910
- {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"},
2911
- {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"},
2912
- {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"},
2913
- {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"},
2914
- {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"},
2915
- {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"},
2916
- {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"},
2917
- {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"},
2918
- {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"},
2919
- {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"},
2920
- {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"},
2921
- {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"},
2922
- {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"},
2923
- {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
2924
  ]
2925
 
2926
  [[package]]
@@ -3108,4 +3381,4 @@ multidict = ">=4.0"
3108
  [metadata]
3109
  lock-version = "2.0"
3110
  python-versions = "^3.11,<3.12"
3111
- content-hash = "441dad0efba8e98e6ec61d954a54fa1542486a1aed72ca2c091da30e0531ec77"
 
349
  [package.extras]
350
  dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
351
 
352
+ [[package]]
353
+ name = "deprecation"
354
+ version = "2.1.0"
355
+ description = "A library to handle automated deprecations"
356
+ optional = false
357
+ python-versions = "*"
358
+ files = [
359
+ {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"},
360
+ {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"},
361
+ ]
362
+
363
+ [package.dependencies]
364
+ packaging = "*"
365
+
366
  [[package]]
367
  name = "dirtyjson"
368
  version = "1.0.8"
 
473
  pycodestyle = ">=2.11.0,<2.12.0"
474
  pyflakes = ">=3.2.0,<3.3.0"
475
 
476
+ [[package]]
477
+ name = "flupy"
478
+ version = "1.2.0"
479
+ description = "Method chaining built on generators"
480
+ optional = false
481
+ python-versions = "*"
482
+ files = [
483
+ {file = "flupy-1.2.0.tar.gz", hash = "sha256:12487a008e9744cd35d0f6ea3cfa06f4b2b27cb138bf57d0788f5c26e57afe69"},
484
+ ]
485
+
486
+ [package.dependencies]
487
+ typing_extensions = "*"
488
+
489
+ [package.extras]
490
+ dev = ["black", "mypy", "pre-commit", "pylint", "pytest", "pytest-benchmark", "pytest-cov"]
491
+
492
  [[package]]
493
  name = "frozenlist"
494
  version = "1.4.1"
 
610
  ssh = ["paramiko"]
611
  tqdm = ["tqdm"]
612
 
613
+ [[package]]
614
+ name = "gotrue"
615
+ version = "2.4.1"
616
+ description = "Python Client Library for GoTrue"
617
+ optional = false
618
+ python-versions = ">=3.8,<4.0"
619
+ files = [
620
+ {file = "gotrue-2.4.1-py3-none-any.whl", hash = "sha256:9647bb7a585c969d26667df21168fa20b18f91c5d6afe286af08d7a0610fd2cc"},
621
+ {file = "gotrue-2.4.1.tar.gz", hash = "sha256:8b260ef285f45a3a2f9b5a006f12afb9fad7a36a28fa277f19e733f22eb88584"},
622
+ ]
623
+
624
+ [package.dependencies]
625
+ httpx = ">=0.23,<0.26"
626
+ pydantic = ">=1.10,<3"
627
+
628
  [[package]]
629
  name = "greenlet"
630
  version = "3.0.3"
 
778
 
779
  [[package]]
780
  name = "httpx"
781
+ version = "0.25.2"
782
  description = "The next generation HTTP client."
783
  optional = false
784
  python-versions = ">=3.8"
785
  files = [
786
+ {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"},
787
+ {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"},
788
  ]
789
 
790
  [package.dependencies]
 
1605
  test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
1606
  xml = ["lxml (>=4.9.2)"]
1607
 
1608
+ [[package]]
1609
+ name = "pgvector"
1610
+ version = "0.1.8"
1611
+ description = "pgvector support for Python"
1612
+ optional = false
1613
+ python-versions = ">=3.6"
1614
+ files = [
1615
+ {file = "pgvector-0.1.8-py2.py3-none-any.whl", hash = "sha256:99dce3a6580ef73863edb9b8441937671f4e1a09383826e6b0838176cd441a96"},
1616
+ ]
1617
+
1618
+ [package.dependencies]
1619
+ numpy = "*"
1620
+
1621
  [[package]]
1622
  name = "pluggy"
1623
  version = "1.4.0"
 
1633
  dev = ["pre-commit", "tox"]
1634
  testing = ["pytest", "pytest-benchmark"]
1635
 
1636
+ [[package]]
1637
+ name = "postgrest"
1638
+ version = "0.16.1"
1639
+ description = "PostgREST client for Python. This library provides an ORM interface to PostgREST."
1640
+ optional = false
1641
+ python-versions = ">=3.8,<4.0"
1642
+ files = [
1643
+ {file = "postgrest-0.16.1-py3-none-any.whl", hash = "sha256:412ec6bf61c58f38c92b6b61f57ab50e25c73ca9ef415a6f56ed9cf5429614cb"},
1644
+ {file = "postgrest-0.16.1.tar.gz", hash = "sha256:d955824d37e7123a8313cbf10c8e0a8d42418fcb942cd8e1526e8509fb71574d"},
1645
+ ]
1646
+
1647
+ [package.dependencies]
1648
+ deprecation = ">=2.1.0,<3.0.0"
1649
+ httpx = ">=0.24,<0.26"
1650
+ pydantic = ">=1.9,<3.0"
1651
+ strenum = ">=0.4.9,<0.5.0"
1652
+
1653
+ [[package]]
1654
+ name = "psycopg2-binary"
1655
+ version = "2.9.9"
1656
+ description = "psycopg2 - Python-PostgreSQL Database Adapter"
1657
+ optional = false
1658
+ python-versions = ">=3.7"
1659
+ files = [
1660
+ {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
1661
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
1662
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
1663
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
1664
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
1665
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
1666
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
1667
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
1668
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
1669
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
1670
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
1671
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
1672
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
1673
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
1674
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
1675
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
1676
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
1677
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
1678
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
1679
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
1680
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
1681
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
1682
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
1683
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
1684
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
1685
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
1686
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
1687
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
1688
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
1689
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
1690
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
1691
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
1692
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
1693
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
1694
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
1695
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
1696
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
1697
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
1698
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
1699
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
1700
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
1701
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
1702
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
1703
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
1704
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
1705
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
1706
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
1707
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
1708
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
1709
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
1710
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
1711
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
1712
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
1713
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
1714
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
1715
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
1716
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
1717
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
1718
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
1719
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
1720
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
1721
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
1722
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
1723
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
1724
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
1725
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
1726
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
1727
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
1728
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
1729
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
1730
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
1731
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
1732
+ ]
1733
+
1734
  [[package]]
1735
  name = "pycodestyle"
1736
  version = "2.11.1"
 
1863
  {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
1864
  ]
1865
 
1866
+ [[package]]
1867
+ name = "pyjwt"
1868
+ version = "2.8.0"
1869
+ description = "JSON Web Token implementation in Python"
1870
+ optional = false
1871
+ python-versions = ">=3.7"
1872
+ files = [
1873
+ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
1874
+ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
1875
+ ]
1876
+
1877
+ [package.extras]
1878
+ crypto = ["cryptography (>=3.4.0)"]
1879
+ 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"]
1880
+ docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
1881
+ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
1882
+
1883
  [[package]]
1884
  name = "pypdf"
1885
  version = "3.17.4"
 
2005
  {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
2006
  {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
2007
  {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
 
2008
  {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
2009
  {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
2010
  {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
 
2039
  {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
2040
  ]
2041
 
2042
+ [[package]]
2043
+ name = "realtime"
2044
+ version = "1.0.2"
2045
+ description = ""
2046
+ optional = false
2047
+ python-versions = ">=3.8,<4.0"
2048
+ files = [
2049
+ {file = "realtime-1.0.2-py3-none-any.whl", hash = "sha256:8f8375199fd917cd0ded818702321f91b208ab72794ade0a33cee9d55ae30f11"},
2050
+ {file = "realtime-1.0.2.tar.gz", hash = "sha256:776170a4329edc869b91e104c554cda02c8bf8e052cbb93c377e22482870959c"},
2051
+ ]
2052
+
2053
+ [package.dependencies]
2054
+ python-dateutil = ">=2.8.1,<3.0.0"
2055
+ typing-extensions = ">=4.2.0,<5.0.0"
2056
+ websockets = ">=11.0,<12.0"
2057
+
2058
  [[package]]
2059
  name = "regex"
2060
  version = "2023.12.25"
 
2436
  [package.extras]
2437
  full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
2438
 
2439
+ [[package]]
2440
+ name = "storage3"
2441
+ version = "0.7.3"
2442
+ description = "Supabase Storage client for Python."
2443
+ optional = false
2444
+ python-versions = ">=3.8,<4.0"
2445
+ files = [
2446
+ {file = "storage3-0.7.3-py3-none-any.whl", hash = "sha256:dc6a59da801ee6fc00015da4967ac0b5c3e5508d31ffd796f0e4c83957e5c6a0"},
2447
+ {file = "storage3-0.7.3.tar.gz", hash = "sha256:943c31de4a7c7490ad7960d963a6b410979ebd0e1b3d320d76cb61564ab0b528"},
2448
+ ]
2449
+
2450
+ [package.dependencies]
2451
+ httpx = ">=0.24,<0.26"
2452
+ python-dateutil = ">=2.8.2,<3.0.0"
2453
+ typing-extensions = ">=4.2.0,<5.0.0"
2454
+
2455
+ [[package]]
2456
+ name = "strenum"
2457
+ version = "0.4.15"
2458
+ description = "An Enum that inherits from str."
2459
+ optional = false
2460
+ python-versions = "*"
2461
+ files = [
2462
+ {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"},
2463
+ {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"},
2464
+ ]
2465
+
2466
+ [package.extras]
2467
+ docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"]
2468
+ release = ["twine"]
2469
+ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
2470
+
2471
+ [[package]]
2472
+ name = "supabase"
2473
+ version = "2.4.0"
2474
+ description = "Supabase client for Python."
2475
+ optional = false
2476
+ python-versions = ">=3.8,<4.0"
2477
+ files = [
2478
+ {file = "supabase-2.4.0-py3-none-any.whl", hash = "sha256:f2f02b0e7903247ef9e2b3cb5dde067924a19a068f1c8befbdf40fb091bf8dd3"},
2479
+ {file = "supabase-2.4.0.tar.gz", hash = "sha256:d51556d3884f2e6f4588c33f1fcac954d4304238253bc35e9a87fdd22c43bafb"},
2480
+ ]
2481
+
2482
+ [package.dependencies]
2483
+ gotrue = ">=1.3,<3.0"
2484
+ httpx = ">=0.24,<0.26"
2485
+ postgrest = ">=0.10.8,<0.17.0"
2486
+ realtime = ">=1.0.0,<2.0.0"
2487
+ storage3 = ">=0.5.3,<0.8.0"
2488
+ supafunc = ">=0.3.1,<0.4.0"
2489
+
2490
+ [[package]]
2491
+ name = "supafunc"
2492
+ version = "0.3.3"
2493
+ description = "Library for Supabase Functions"
2494
+ optional = false
2495
+ python-versions = ">=3.8,<4.0"
2496
+ files = [
2497
+ {file = "supafunc-0.3.3-py3-none-any.whl", hash = "sha256:8260b4742335932f9cab64c8f66fb6998681b7e8ca7a46b559a4eb640cc0af80"},
2498
+ {file = "supafunc-0.3.3.tar.gz", hash = "sha256:c35897a2f40465b40d7a08ae11f872f08eb8d1390c3ebc72c80e27d33ba91b99"},
2499
+ ]
2500
+
2501
+ [package.dependencies]
2502
+ httpx = ">=0.24,<0.26"
2503
+
2504
  [[package]]
2505
  name = "sympy"
2506
  version = "1.12"
 
3008
  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)"]
3009
  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)"]
3010
 
3011
+ [[package]]
3012
+ name = "vecs"
3013
+ version = "0.4.3"
3014
+ description = "pgvector client"
3015
+ optional = false
3016
+ python-versions = "*"
3017
+ files = [
3018
+ {file = "vecs-0.4.3.tar.gz", hash = "sha256:0a60294143aec43bd0344bb9235b6e57f8f919d102538f6b989d7b85095a31ce"},
3019
+ ]
3020
+
3021
+ [package.dependencies]
3022
+ deprecated = "==1.2.*"
3023
+ flupy = "==1.*"
3024
+ pgvector = "==0.1.*"
3025
+ psycopg2-binary = "==2.9.*"
3026
+ sqlalchemy = "==2.*"
3027
+
3028
+ [package.extras]
3029
+ dev = ["numpy", "parse", "pytest", "pytest-cov"]
3030
+ docs = ["mike", "mkdocs", "pygments", "pymarkdown", "pymdown-extensions"]
3031
+ text-embedding = ["sentence-transformers (==2.*)"]
3032
+
3033
  [[package]]
3034
  name = "watchfiles"
3035
  version = "0.21.0"
 
3119
 
3120
  [[package]]
3121
  name = "websockets"
3122
+ version = "11.0.3"
3123
  description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
3124
  optional = false
3125
+ python-versions = ">=3.7"
3126
  files = [
3127
+ {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"},
3128
+ {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"},
3129
+ {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"},
3130
+ {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"},
3131
+ {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"},
3132
+ {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"},
3133
+ {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"},
3134
+ {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"},
3135
+ {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"},
3136
+ {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"},
3137
+ {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"},
3138
+ {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"},
3139
+ {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"},
3140
+ {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"},
3141
+ {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"},
3142
+ {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"},
3143
+ {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"},
3144
+ {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"},
3145
+ {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"},
3146
+ {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"},
3147
+ {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"},
3148
+ {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"},
3149
+ {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"},
3150
+ {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"},
3151
+ {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"},
3152
+ {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"},
3153
+ {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"},
3154
+ {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"},
3155
+ {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"},
3156
+ {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"},
3157
+ {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"},
3158
+ {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"},
3159
+ {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"},
3160
+ {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"},
3161
+ {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"},
3162
+ {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"},
3163
+ {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"},
3164
+ {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"},
3165
+ {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"},
3166
+ {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"},
3167
+ {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"},
3168
+ {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"},
3169
+ {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"},
3170
+ {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"},
3171
+ {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"},
3172
+ {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"},
3173
+ {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"},
3174
+ {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"},
3175
+ {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"},
3176
+ {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"},
3177
+ {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"},
3178
+ {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"},
3179
+ {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"},
3180
+ {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"},
3181
+ {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"},
3182
+ {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"},
3183
+ {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"},
3184
+ {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"},
3185
+ {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"},
3186
+ {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"},
3187
+ {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"},
3188
+ {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"},
3189
+ {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"},
3190
+ {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"},
3191
+ {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"},
3192
+ {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"},
3193
+ {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"},
3194
+ {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"},
3195
+ {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"},
3196
+ {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"},
 
 
3197
  ]
3198
 
3199
  [[package]]
 
3381
  [metadata]
3382
  lock-version = "2.0"
3383
  python-versions = "^3.11,<3.12"
3384
+ content-hash = "1d354d5ac24eb7e482fe9895fef5888582caeb9d6b52982db87f88a8f968da19"
backend/pyproject.toml CHANGED
@@ -17,6 +17,9 @@ llama-cpp-python = "^0.2.52"
17
  transformers = "^4.38.1"
18
  docx2txt = "^0.8"
19
  doc2docx = "^0.2.4"
 
 
 
20
 
21
  [tool.poetry.group.dev]
22
  optional = true
 
17
  transformers = "^4.38.1"
18
  docx2txt = "^0.8"
19
  doc2docx = "^0.2.4"
20
+ supabase = "^2.4.0"
21
+ pyjwt = "^2.8.0"
22
+ vecs = "^0.4.3"
23
 
24
  [tool.poetry.group.dev]
25
  optional = true
frontend/app/about/page.tsx CHANGED
@@ -4,7 +4,7 @@ export default function About() {
4
 
5
  return (
6
  <div className="rounded-xl shadow-xl p-4 max-w-5xl w-full">
7
- <div className="max-w-2xl mx-auto p-4">
8
  <div className="bg-gradient-to-r from-blue-500 to-indigo-500 text-white p-8 rounded-lg shadow-lg">
9
  <h1 className="text-2xl md:text-4xl font-bold mb-4">About Smart Retrieval</h1>
10
  <p className="text-l mb-4">
@@ -19,6 +19,63 @@ export default function About() {
19
  With cutting-edge technology, including Large Language Models (LLM) like GPT, BERT, and advanced chatbot
20
  integration, we aim to revolutionize the way JTC employees access and comprehend crucial documents.
21
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  </div>
23
  </div>
24
  </div>
 
4
 
5
  return (
6
  <div className="rounded-xl shadow-xl p-4 max-w-5xl w-full">
7
+ <div className="max-w-4xl mx-auto p-4">
8
  <div className="bg-gradient-to-r from-blue-500 to-indigo-500 text-white p-8 rounded-lg shadow-lg">
9
  <h1 className="text-2xl md:text-4xl font-bold mb-4">About Smart Retrieval</h1>
10
  <p className="text-l mb-4">
 
19
  With cutting-edge technology, including Large Language Models (LLM) like GPT, BERT, and advanced chatbot
20
  integration, we aim to revolutionize the way JTC employees access and comprehend crucial documents.
21
  </p>
22
+ <p className="text-l mb-4">
23
+ We are committed to providing accurate, reliable, and user-friendly information retrieval services that
24
+ empower JTC employees to make informed decisions and stay compliant with the latest regulations.
25
+ </p>
26
+ <p className="text-l mb-4">
27
+ Thank you for choosing Smart Retrieval. We look forward to serving you and making your work life easier.
28
+ </p>
29
+
30
+ <h2 className="text-2xl md:text-4xl font-bold mt-8 mb-4">Functions of Smart Retrieval</h2>
31
+ <p className="text-l mb-4">
32
+ Smart Retrieval is designed to provide users with a seamless and efficient experience when searching for
33
+ information. Some of the key functions of Smart Retrieval include:
34
+ </p>
35
+ <ul className="list-disc list-inside mb-4">
36
+ <li className="text-l">Chat</li>
37
+ <li className="text-l">Question & Answer (Q&A)</li>
38
+ <li className="text-l">Search</li>
39
+ </ul>
40
+ <p className="text-l mb-4">
41
+ These functions are designed to cater to different user preferences and requirements, ensuring that users
42
+ can easily access the information they need.
43
+ </p>
44
+
45
+ <h2 className="text-2xl md:text-4xl font-bold mt-8 mb-4">Functions In-Depth</h2>
46
+ <h3 className="text-xl md:text-2xl font-bold mt-4 mb-2">Chat</h3>
47
+ <p className="text-l mb-4">
48
+ The chat function allows users to interact with Smart Retrieval through a conversational interface via a fixed set of documents.
49
+ Users can ask questions, seek information, and engage in dialogue with the system to retrieve the information they
50
+ need. The chat function is designed to be intuitive, user-friendly, and responsive, providing users with a
51
+ seamless experience. Current document sets include:
52
+ </p>
53
+ <ul className="list-disc list-inside mb-4">
54
+ <li className="text-l">PSSCOC</li>
55
+ <li className="text-l">EIR</li>
56
+ <li className="text-l">And more...</li>
57
+ </ul>
58
+
59
+ <h3 className="text-xl md:text-2xl font-bold mt-4 mb-2">Question & Answer (Q&A)</h3>
60
+ <p className="text-l mb-4">
61
+ The Q&A function enables users to ask specific questions relating to their own documents and receive accurate answers from Smart Retrieval.
62
+ Users can simply upload their files and input their queries, and the system will provide relevant information based on the question asked.
63
+ The Q&A function is designed to provide users with precise and concise answers to their queries, enhancing
64
+ the information retrieval process.
65
+ </p>
66
+
67
+ <h3 className="text-xl md:text-2xl font-bold mt-4 mb-2">Search</h3>
68
+ <p className="text-l mb-4">
69
+ The search function allows users to input keywords or phrases to search for specific information within the
70
+ document repository. Users can enter their search queries, and the system will retrieve relevant documents
71
+ based on the search terms provided. The search function is designed to help users quickly locate the
72
+ information they need, streamlining the search process.
73
+ </p>
74
+
75
+ <h2 className="text-2xl md:text-4xl font-bold mt-8 mb-4">Disclaimer</h2>
76
+ <p className="text-l mb-4">
77
+ 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.
78
+ </p>
79
  </div>
80
  </div>
81
  </div>
frontend/app/api/profile/route.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ export async function GET(request: NextRequest) {
5
+ const { pathname, origin } = request.nextUrl;
6
+ const signinPage = new URL('/sign-in', origin);
7
+ // Retrieve the session token from the request cookies
8
+ const session = request.cookies.get('next-auth.session-token');
9
+
10
+ // Create a new Supabase client
11
+ const supabase = createClient(
12
+ process.env.SUPABASE_URL ?? '',
13
+ process.env.SUPABASE_SERVICE_ROLE_KEY ?? '',
14
+ { db: { schema: 'next_auth' } },
15
+ );
16
+
17
+ // Retrieve the user's ID from the session token
18
+ const { data: sessionData, error: sessionError } = await supabase
19
+ .from('sessions')
20
+ .select('userId')
21
+ .eq('sessionToken', session?.value)
22
+ .single();
23
+
24
+ const userId = sessionData?.userId;
25
+
26
+ if (sessionError) {
27
+ console.error('Error fetching session from database:', sessionError.message);
28
+ return NextResponse.redirect(signinPage.href, { status: 302 });
29
+ }
30
+
31
+ // Retrieve the user's profile data
32
+ const { data: userData, error: userError } = await supabase
33
+ .from('users')
34
+ .select('id, name, email, image')
35
+ .eq('id', userId)
36
+ .single();
37
+
38
+ if (userError) {
39
+ console.error('Error fetching user data from database:', userError.message);
40
+ return NextResponse.redirect(signinPage.href, { status: 302 });
41
+ }
42
+
43
+ // console.log('userData:', userData);
44
+
45
+ return NextResponse.json({ userData: userData });
46
+ }
frontend/app/api/status/route.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export async function GET(request: Request) {
2
+ const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API as string;
3
+
4
+ // Retrieve the session token from the request headers
5
+ let session = request.headers.get('Authorization');
6
+
7
+ console.log('Status API - headers:', request.headers);
8
+
9
+ // Public API key
10
+ let api_key = null;
11
+
12
+ // If no session, use the public API key
13
+ if (!session) {
14
+ api_key = process.env.BACKEND_API_KEY as string;
15
+ }
16
+
17
+ const res = await fetch(healthcheck_api, {
18
+ signal: AbortSignal.timeout(5000), // Abort the request if it takes longer than 5 seconds
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ 'Authorization': session,
22
+ 'X-API-Key': api_key,
23
+ } as any,
24
+ })
25
+ const data = await res.json()
26
+
27
+ return Response.json({ data })
28
+ }
frontend/app/components/chat-section.tsx CHANGED
@@ -2,9 +2,15 @@
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
 
5
  import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog";
 
 
6
 
7
  export default function ChatSection() {
 
 
 
8
  const {
9
  messages,
10
  input,
@@ -13,29 +19,51 @@ export default function ChatSection() {
13
  handleInputChange,
14
  reload,
15
  stop,
16
- } = useChat({ api: process.env.NEXT_PUBLIC_CHAT_API });
 
 
 
 
 
 
 
 
 
 
17
 
18
  return (
19
  <div className="space-y-4 max-w-5xl w-full relative">
20
- <ChatMessages
21
- messages={messages}
22
- isLoading={isLoading}
23
- reload={reload}
24
- stop={stop}
25
- />
26
- <AutofillQuestion
27
- messages={messages}
28
- isLoading={isLoading}
29
- handleSubmit={handleSubmit}
30
- handleInputChange={handleInputChange}
31
- input={input}
32
- />
33
- <ChatInput
34
- input={input}
35
- handleSubmit={handleSubmit}
36
- handleInputChange={handleInputChange}
37
- isLoading={isLoading}
38
- />
 
 
 
 
 
 
 
 
 
 
 
 
39
  </div>
40
  );
41
  }
 
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
+ import ChatSelection from "./ui/chat/chat-selection";
6
  import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog";
7
+ import { useSession } from "next-auth/react";
8
+ import { useState } from "react";
9
 
10
  export default function ChatSection() {
11
+ const { data: session } = useSession();
12
+ const supabaseAccessToken = session?.supabaseAccessToken;
13
+ const [docSelected, setDocSelected] = useState<string>('');
14
  const {
15
  messages,
16
  input,
 
19
  handleInputChange,
20
  reload,
21
  stop,
22
+ } = useChat({
23
+ api: process.env.NEXT_PUBLIC_CHAT_API,
24
+ headers: {
25
+ // Add the access token to the request headers
26
+ 'Authorization': `Bearer ${supabaseAccessToken}`,
27
+ },
28
+ body: {
29
+ // Add the selected document to the request body
30
+ document: docSelected,
31
+ },
32
+ });
33
 
34
  return (
35
  <div className="space-y-4 max-w-5xl w-full relative">
36
+ {docSelected ?
37
+ (
38
+ <>
39
+ <ChatMessages
40
+ messages={messages}
41
+ isLoading={isLoading}
42
+ reload={reload}
43
+ stop={stop}
44
+ />
45
+ <AutofillQuestion
46
+ docSelected={docSelected}
47
+ messages={messages}
48
+ isLoading={isLoading}
49
+ handleSubmit={handleSubmit}
50
+ handleInputChange={handleInputChange}
51
+ input={input}
52
+ />
53
+ <ChatInput
54
+ input={input}
55
+ handleSubmit={handleSubmit}
56
+ handleInputChange={handleInputChange}
57
+ isLoading={isLoading}
58
+ />
59
+ </>
60
+ )
61
+ :
62
+ <ChatSelection
63
+ docSelected={docSelected}
64
+ handleDocSelect={setDocSelected}
65
+ />
66
+ }
67
  </div>
68
  );
69
  }
frontend/app/components/footer.tsx CHANGED
@@ -1,32 +1,50 @@
1
  "use client";
2
 
 
3
  import { FooterNavLink } from "@/app/components/ui/navlink";
4
  import { IconGitHub } from "@/app/components/ui/icons";
5
- import { Text, Cookie } from "lucide-react";
6
 
7
  export default function Footer() {
 
 
 
 
 
 
8
  return (
9
- <footer>
10
  <div className="flex flex-col items-center justify-center bg-gray-800 text-white p-4 mb-4 rounded-lg shadow-xl">
11
- <div className="flex flex-col items-center">
 
 
 
 
 
 
 
 
 
 
 
12
  <p className="text-sm text-center">
13
  Β© 2024 JTC DBE. All rights reserved.
14
  </p>
15
  </div>
16
  <div className="flex items-center mt-2 gap-4">
17
- <FooterNavLink href="https://github.com/digitalbuiltenvironment/Smart-Retrieval/" target="_blank">
18
  <div className="text-sm text-center underline">
19
  <IconGitHub className="h-5 w-5 inline mr-2 mb-1" />
20
  Github
21
  </div>
22
  </FooterNavLink>
23
- <FooterNavLink href="/terms-of-service">
24
  <div className="text-sm text-center underline">
25
  <Text className="h-5 w-5 inline mr-2 mb-1" />
26
  Terms of Service
27
  </div>
28
  </FooterNavLink>
29
- <FooterNavLink href="/privacy-policy">
30
  <div className="text-sm text-center underline">
31
  <Cookie className="h-5 w-5 inline mr-2 mb-1" />
32
  Privacy Policy
@@ -37,4 +55,3 @@ export default function Footer() {
37
  </footer>
38
  );
39
  }
40
-
 
1
  "use client";
2
 
3
+ import { useState } from "react";
4
  import { FooterNavLink } from "@/app/components/ui/navlink";
5
  import { IconGitHub } from "@/app/components/ui/icons";
6
+ import { Text, Cookie, AlertCircle } from "lucide-react";
7
 
8
  export default function Footer() {
9
+ const [showDisclaimer, setShowDisclaimer] = useState(false);
10
+
11
+ const toggleDisclaimer = () => {
12
+ setShowDisclaimer(!showDisclaimer);
13
+ };
14
+
15
  return (
16
+ <footer className="z-10">
17
  <div className="flex flex-col items-center justify-center bg-gray-800 text-white p-4 mb-4 rounded-lg shadow-xl">
18
+ <div className="flex flex-col items-center text-red-500">
19
+ <button className="text-sm text-center underline" onClick={toggleDisclaimer} title="Disclaimer">
20
+ <AlertCircle className="h-5 w-5 inline mr-2 mb-1" />
21
+ Disclaimer
22
+ </button>
23
+ {showDisclaimer && (
24
+ <p className="text-sm text-center w-64 mb-2">
25
+ 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.
26
+ </p>
27
+ )}
28
+ </div>
29
+ <div className="flex flex-col items-center mt-2 gap-4">
30
  <p className="text-sm text-center">
31
  Β© 2024 JTC DBE. All rights reserved.
32
  </p>
33
  </div>
34
  <div className="flex items-center mt-2 gap-4">
35
+ <FooterNavLink href="https://github.com/digitalbuiltenvironment/Smart-Retrieval/" title="Github" target="_blank">
36
  <div className="text-sm text-center underline">
37
  <IconGitHub className="h-5 w-5 inline mr-2 mb-1" />
38
  Github
39
  </div>
40
  </FooterNavLink>
41
+ <FooterNavLink href="/terms-of-service" title="Terms Of Service">
42
  <div className="text-sm text-center underline">
43
  <Text className="h-5 w-5 inline mr-2 mb-1" />
44
  Terms of Service
45
  </div>
46
  </FooterNavLink>
47
+ <FooterNavLink href="/privacy-policy" title="Privacy Policy">
48
  <div className="text-sm text-center underline">
49
  <Cookie className="h-5 w-5 inline mr-2 mb-1" />
50
  Privacy Policy
 
55
  </footer>
56
  );
57
  }
 
frontend/app/components/header.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import Image from 'next/image';
4
- import { Home, InfoIcon, MessageCircle, Search, FileQuestion, Menu, X } from 'lucide-react';
5
  import { useTheme } from "next-themes";
6
  import { useEffect, useState } from "react";
7
  import { useMedia } from 'react-use';
@@ -9,7 +9,9 @@ import useSWR from 'swr';
9
  import logo from '@/public/smart-retrieval-logo.webp';
10
  import { HeaderNavLink } from '@/app/components/ui/navlink';
11
  import { MobileMenu } from '@/app/components/ui/mobilemenu';
12
- import { IconSpinner } from '@/app/components/ui/icons'
 
 
13
 
14
  const MobileMenuItems = [
15
  {
@@ -43,13 +45,28 @@ export default function Header() {
43
  const isLargeScreen = useMedia('(min-width: 1024px)', false);
44
  const [mounted, setMounted] = useState(false);
45
  const { theme, setTheme } = useTheme();
 
 
 
 
 
 
 
 
 
 
 
46
  // Use SWR for API status fetching
47
- const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API;
48
  const { data, error: apiError, isLoading } = useSWR(healthcheck_api, async (url) => {
49
  try {
50
  // Fetch the data
51
  const response = await fetch(url, {
52
  signal: AbortSignal.timeout(5000), // Abort the request if it takes longer than 5 seconds
 
 
 
 
53
  });
54
  if (!response.ok) {
55
  throw new Error(response.statusText || 'Unknown Error');
@@ -74,6 +91,7 @@ export default function Header() {
74
  }
75
  }
76
 
 
77
  useEffect(() => {
78
  setMounted(true);
79
  }, []);
@@ -135,31 +153,31 @@ export default function Header() {
135
  </button>
136
  </div>
137
  <div className={`hidden items-center gap-4 lg:flex`}>
138
- <HeaderNavLink href="/">
139
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
140
  <Home className="mr-1 h-4 w-4" />
141
  Home
142
  </div>
143
  </HeaderNavLink>
144
- <HeaderNavLink href="/about">
145
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
146
  <InfoIcon className="mr-1 h-4 w-4" />
147
  About
148
  </div>
149
  </HeaderNavLink>
150
- <HeaderNavLink href="/chat">
151
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
152
  <MessageCircle className="mr-1 h-4 w-4" />
153
  Chat
154
  </div>
155
  </HeaderNavLink>
156
- <HeaderNavLink href="/query">
157
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
158
  <FileQuestion className="mr-1 h-4 w-4" />
159
  Q&A
160
  </div>
161
  </HeaderNavLink>
162
- <HeaderNavLink href="/search">
163
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
164
  <Search className="mr-1 h-4 w-4" />
165
  Search
@@ -169,7 +187,7 @@ export default function Header() {
169
  <div className="flex items-center ml-auto">
170
  {/* Status Page Button/Indicator */}
171
  <span className='flex items-center mr-1'>API:</span>
172
- <HeaderNavLink href='/status'>
173
  <div className="flex items-center mr-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
174
  {isLoading ? (
175
  <IconSpinner className="mr-2 animate-spin" />
@@ -201,8 +219,46 @@ export default function Header() {
201
  </span>
202
  )}
203
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  </div>
205
  </div >
 
206
  {/* Mobile menu component */}
207
  < MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false)
208
  } logoSrc={logo} items={MobileMenuItems} />
 
1
  "use client";
2
 
3
  import Image from 'next/image';
4
+ import { Home, InfoIcon, MessageCircle, Search, FileQuestion, Menu, X, User2, LogOut, LogIn } from 'lucide-react';
5
  import { useTheme } from "next-themes";
6
  import { useEffect, useState } from "react";
7
  import { useMedia } from 'react-use';
 
9
  import logo from '@/public/smart-retrieval-logo.webp';
10
  import { HeaderNavLink } from '@/app/components/ui/navlink';
11
  import { MobileMenu } from '@/app/components/ui/mobilemenu';
12
+ import { IconSpinner } from '@/app/components/ui/icons';
13
+ import { useSession, signOut } from 'next-auth/react';
14
+ import { usePathname } from 'next/navigation';
15
 
16
  const MobileMenuItems = [
17
  {
 
45
  const isLargeScreen = useMedia('(min-width: 1024px)', false);
46
  const [mounted, setMounted] = useState(false);
47
  const { theme, setTheme } = useTheme();
48
+ // Get the current path
49
+ const currentPath = usePathname();
50
+ // Ensure the currentPath is encoded
51
+ const encodedPath = encodeURIComponent(currentPath);
52
+ // Add callbackUrl params to the signinPage URL
53
+ const signinPage = "/sign-in?callbackUrl=" + encodedPath;
54
+
55
+ // Get user session for conditional rendering of user profile and logout buttons and for fetching the API status
56
+ const { data: session, status } = useSession();
57
+ // console.log('session:', session, 'status:', status);
58
+ const supabaseAccessToken = session?.supabaseAccessToken;
59
  // Use SWR for API status fetching
60
+ const healthcheck_api = "/api/status";
61
  const { data, error: apiError, isLoading } = useSWR(healthcheck_api, async (url) => {
62
  try {
63
  // Fetch the data
64
  const response = await fetch(url, {
65
  signal: AbortSignal.timeout(5000), // Abort the request if it takes longer than 5 seconds
66
+ // Add the access token to the request headers
67
+ headers: {
68
+ 'Authorization': `Bearer ${supabaseAccessToken}`,
69
+ }
70
  });
71
  if (!response.ok) {
72
  throw new Error(response.statusText || 'Unknown Error');
 
91
  }
92
  }
93
 
94
+
95
  useEffect(() => {
96
  setMounted(true);
97
  }, []);
 
153
  </button>
154
  </div>
155
  <div className={`hidden items-center gap-4 lg:flex`}>
156
+ <HeaderNavLink href="/" title='Home'>
157
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
158
  <Home className="mr-1 h-4 w-4" />
159
  Home
160
  </div>
161
  </HeaderNavLink>
162
+ <HeaderNavLink href="/about" title='About Us'>
163
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
164
  <InfoIcon className="mr-1 h-4 w-4" />
165
  About
166
  </div>
167
  </HeaderNavLink>
168
+ <HeaderNavLink href="/chat" title='Chat'>
169
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
170
  <MessageCircle className="mr-1 h-4 w-4" />
171
  Chat
172
  </div>
173
  </HeaderNavLink>
174
+ <HeaderNavLink href="/query" title='Q&A'>
175
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
176
  <FileQuestion className="mr-1 h-4 w-4" />
177
  Q&A
178
  </div>
179
  </HeaderNavLink>
180
+ <HeaderNavLink href="/search" title='Search'>
181
  <div className="flex items-center transition duration-300 ease-in-out transform hover:scale-125">
182
  <Search className="mr-1 h-4 w-4" />
183
  Search
 
187
  <div className="flex items-center ml-auto">
188
  {/* Status Page Button/Indicator */}
189
  <span className='flex items-center mr-1'>API:</span>
190
+ <HeaderNavLink href='/status' title='API Status'>
191
  <div className="flex items-center mr-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
192
  {isLoading ? (
193
  <IconSpinner className="mr-2 animate-spin" />
 
219
  </span>
220
  )}
221
  </button>
222
+
223
+ <span className="lg:text-lg font-nunito ml-2 mr-2"> </span>
224
+
225
+ {/* Conditionally render the user profile and logout buttons based on the user's authentication status */}
226
+ {status === 'loading' ? (
227
+ <div className="flex items-center ml-2 mr-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
228
+ <IconSpinner className="mr-2 animate-spin" />
229
+ </div>
230
+ ) : session ? (
231
+ <>
232
+ {/* User Profile Button */}
233
+ <HeaderNavLink href="/profile" title='Profile'>
234
+ <div className="flex items-center ml-2 mr-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
235
+ <User2 className="mr-1 h-5 w-5" />
236
+ </div>
237
+ </HeaderNavLink>
238
+
239
+ {/* Sign Out Button */}
240
+ <button title='Sign Out'
241
+ onClick={
242
+ async () => {
243
+ await signOut();
244
+ }
245
+ }>
246
+ <div className="flex items-center ml-2 text-xl transition duration-300 ease-in-out transform hover:scale-125">
247
+ <LogOut className="mr-1 h-5 w-5" />
248
+ </div>
249
+ </button>
250
+ </>
251
+ ) : (
252
+ <HeaderNavLink href={signinPage} title='Sign In'>
253
+ <div className="flex items-center ml-2 transition duration-300 ease-in-out transform hover:scale-125">
254
+ <LogIn className="mr-1 h-5 w-5" />
255
+ Sign In
256
+ </div>
257
+ </HeaderNavLink>
258
+ )}
259
  </div>
260
  </div >
261
+
262
  {/* Mobile menu component */}
263
  < MobileMenu isOpen={isMobileMenuOpen} onClose={() => setMobileMenuOpen(false)
264
  } logoSrc={logo} items={MobileMenuItems} />
frontend/app/components/query-section.tsx CHANGED
@@ -2,9 +2,11 @@
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
- import AutofillQuestion from "@/app/components/ui/autofill-prompt/autofill-prompt-dialog";
 
6
 
7
  export default function QuerySection() {
 
8
  const {
9
  messages,
10
  input,
@@ -13,17 +15,24 @@ export default function QuerySection() {
13
  handleInputChange,
14
  reload,
15
  stop,
16
- } = useChat({ api: process.env.NEXT_PUBLIC_QUERY_API });
 
 
 
 
 
 
17
 
18
  return (
19
  <div className="space-y-4 max-w-5xl w-full">
20
- <ChatMessages
21
  messages={messages}
22
  isLoading={isLoading}
23
  reload={reload}
24
  stop={stop}
25
  />
26
  <AutofillQuestion
 
27
  messages={messages}
28
  isLoading={isLoading}
29
  handleSubmit={handleSubmit}
@@ -35,7 +44,14 @@ export default function QuerySection() {
35
  handleSubmit={handleSubmit}
36
  handleInputChange={handleInputChange}
37
  isLoading={isLoading}
38
- />
 
 
 
 
 
 
 
39
  </div>
40
  );
41
  }
 
2
 
3
  import { useChat } from "ai/react";
4
  import { ChatInput, ChatMessages } from "@/app/components/ui/chat";
5
+ import { AutofillQuestion } from "./ui/autofill-prompt";
6
+ import { useSession } from "next-auth/react";
7
 
8
  export default function QuerySection() {
9
+ const { data: session } = useSession();
10
  const {
11
  messages,
12
  input,
 
15
  handleInputChange,
16
  reload,
17
  stop,
18
+ } = useChat({
19
+ api: process.env.NEXT_PUBLIC_QUERY_API,
20
+ // Add the access token to the request headers
21
+ headers: {
22
+ 'Authorization': `Bearer ${session?.supabaseAccessToken}`,
23
+ }
24
+ });
25
 
26
  return (
27
  <div className="space-y-4 max-w-5xl w-full">
28
+ {/* <ChatMessages
29
  messages={messages}
30
  isLoading={isLoading}
31
  reload={reload}
32
  stop={stop}
33
  />
34
  <AutofillQuestion
35
+ docSelected="PSSCOC"
36
  messages={messages}
37
  isLoading={isLoading}
38
  handleSubmit={handleSubmit}
 
44
  handleSubmit={handleSubmit}
45
  handleInputChange={handleInputChange}
46
  isLoading={isLoading}
47
+ /> */}
48
+
49
+ {/* Maintenance Page */}
50
+ <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative text-center" role="alert">
51
+ <strong className="font-bold">A new feature is coming your way!</strong>
52
+ <br />
53
+ <span className="block sm:inline">The Q&A Page is currently undergoing upgrades. Please check back later.</span>
54
+ </div>
55
  </div>
56
  );
57
  }
frontend/app/components/search-section.tsx CHANGED
@@ -2,16 +2,14 @@
2
  "use client";
3
 
4
  import { useState, ChangeEvent, FormEvent } from "react";
5
- import useSearch from "@/app/components/ui/search/useSearch";
6
- import SearchResults from "@/app/components/ui/search/search-results";
7
- import SearchInput from "@/app/components/ui/search/search-input";
8
- import AutofillSearchQuery from "@/app/components/ui/autofill-prompt/autofill-search-prompt-dialog";
9
 
10
  const SearchSection: React.FC = () => {
11
  const [query, setQuery] = useState("");
12
  const { searchResults, isLoading, handleSearch } = useSearch();
13
  const [searchButtonPressed, setSearchButtonPressed] = useState(false);
14
-
15
 
16
  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
17
  setQuery(e.target.value);
@@ -26,21 +24,38 @@ const SearchSection: React.FC = () => {
26
 
27
  return (
28
  <div className="space-y-4 max-w-5xl w-full">
29
- <SearchInput
30
- query={query}
31
- isLoading={isLoading}
32
- results={searchResults}
33
- onInputChange={handleInputChange}
34
- onSearchSubmit={handleSearchSubmit}
35
- />
36
- <AutofillSearchQuery
37
- query={query}
38
- isLoading={isLoading}
39
- results={searchResults}
40
- onInputChange={handleInputChange}
41
- onSearchSubmit={handleSearchSubmit}
42
- />
43
- <SearchResults query={query} results={searchResults} isLoading={isLoading} searchButtonPressed={searchButtonPressed} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </div>
45
  );
46
  };
 
2
  "use client";
3
 
4
  import { useState, ChangeEvent, FormEvent } from "react";
5
+ import { AutofillSearchQuery } from "@/app/components/ui/autofill-prompt";
6
+ import { SearchSelection, useSearch, SearchResults, SearchInput } from "./ui/search";
 
 
7
 
8
  const SearchSection: React.FC = () => {
9
  const [query, setQuery] = useState("");
10
  const { searchResults, isLoading, handleSearch } = useSearch();
11
  const [searchButtonPressed, setSearchButtonPressed] = useState(false);
12
+ const [docSelected, setDocSelected] = useState<string>('');
13
 
14
  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
15
  setQuery(e.target.value);
 
24
 
25
  return (
26
  <div className="space-y-4 max-w-5xl w-full">
27
+ {docSelected ? (
28
+ <>
29
+ <h2 className="text-lg text-center font-semibold mb-4">Searching in {docSelected}</h2>
30
+ <SearchInput
31
+ docSelected={docSelected}
32
+ query={query}
33
+ isLoading={isLoading}
34
+ results={searchResults}
35
+ onInputChange={handleInputChange}
36
+ onSearchSubmit={handleSearchSubmit}
37
+ />
38
+ <AutofillSearchQuery
39
+ docSelected={docSelected}
40
+ query={query}
41
+ isLoading={isLoading}
42
+ results={searchResults}
43
+ onInputChange={handleInputChange}
44
+ onSearchSubmit={handleSearchSubmit}
45
+ />
46
+ <SearchResults
47
+ query={query}
48
+ results={searchResults}
49
+ isLoading={isLoading}
50
+ searchButtonPressed={searchButtonPressed}
51
+ />
52
+ </>
53
+ ) : (
54
+ <SearchSelection
55
+ docSelected={docSelected}
56
+ handleDocSelect={setDocSelected}
57
+ />
58
+ )}
59
  </div>
60
  );
61
  };
frontend/app/components/ui/autofill-prompt/autofill-prompt-dialog.tsx CHANGED
@@ -1,11 +1,11 @@
1
  import { useEffect, useState } from "react";
2
- import { QuestionsBankProp, questionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface";
3
  import { ChatHandler } from "@/app/components/ui/chat/chat.interface";
4
 
5
  export default function AutofillQuestion(
6
  props: Pick<
7
  ChatHandler,
8
- "messages" | "isLoading" | "handleSubmit" | "handleInputChange" | "input"
9
  >,
10
  ) {
11
  // Keep track of whether to show the overlay
@@ -14,6 +14,8 @@ export default function AutofillQuestion(
14
  const [randomQuestions, setRandomQuestions] = useState<QuestionsBankProp[]>([]);
15
  // Keep track of the current question index
16
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
 
 
17
 
18
  // Shuffle the array using Fisher-Yates algorithm
19
  function shuffleArray(array: any[]) {
@@ -28,6 +30,13 @@ export default function AutofillQuestion(
28
 
29
  // Randomly select a subset of 3-4 questions
30
  useEffect(() => {
 
 
 
 
 
 
 
31
  // Shuffle the questionsBank array
32
  const shuffledQuestions = shuffleArray(questionsBank);
33
  // Get a random subset of 3-4 questions
@@ -37,7 +46,7 @@ export default function AutofillQuestion(
37
  setTimeout(() => {
38
  setRandomQuestions(selectedQuestions);
39
  }, 300);
40
- }, []);
41
 
42
 
43
  // Hide overlay when there are messages
@@ -72,9 +81,9 @@ export default function AutofillQuestion(
72
  return (
73
  <>
74
  {showOverlay && (
75
- <div className="fixed inset-0 flex items-center justify-center">
76
  <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col divide-y overflow-y-auto pb-4">
77
- <h2 className="text-lg text-center font-semibold mb-4">How can I help you today?</h2>
78
  {randomQuestions.map((question, index) => (
79
  <ul>
80
  <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentQuestionIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
 
1
  import { useEffect, useState } from "react";
2
+ import { QuestionsBankProp, psscocQuestionsBank, eirQuestionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface";
3
  import { ChatHandler } from "@/app/components/ui/chat/chat.interface";
4
 
5
  export default function AutofillQuestion(
6
  props: Pick<
7
  ChatHandler,
8
+ "docSelected" | "messages" | "isLoading" | "handleSubmit" | "handleInputChange" | "input"
9
  >,
10
  ) {
11
  // Keep track of whether to show the overlay
 
14
  const [randomQuestions, setRandomQuestions] = useState<QuestionsBankProp[]>([]);
15
  // Keep track of the current question index
16
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
17
+ // Questions bank for PSSCOC or EIR
18
+ const [questionsBank, setQuestionsBank] = useState<QuestionsBankProp[]>(psscocQuestionsBank);
19
 
20
  // Shuffle the array using Fisher-Yates algorithm
21
  function shuffleArray(array: any[]) {
 
30
 
31
  // Randomly select a subset of 3-4 questions
32
  useEffect(() => {
33
+ // Select the questions bank based on the document set selected
34
+ if (props.docSelected === "EIR") {
35
+ setQuestionsBank(eirQuestionsBank);
36
+ }
37
+ else {
38
+ setQuestionsBank(psscocQuestionsBank);
39
+ }
40
  // Shuffle the questionsBank array
41
  const shuffledQuestions = shuffleArray(questionsBank);
42
  // Get a random subset of 3-4 questions
 
46
  setTimeout(() => {
47
  setRandomQuestions(selectedQuestions);
48
  }, 300);
49
+ }, [questionsBank, props.docSelected]);
50
 
51
 
52
  // Hide overlay when there are messages
 
81
  return (
82
  <>
83
  {showOverlay && (
84
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl pb-0">
85
  <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col divide-y overflow-y-auto pb-4">
86
+ <h2 className="text-lg text-center font-semibold mb-4">How can I help you with {props.docSelected} today?</h2>
87
  {randomQuestions.map((question, index) => (
88
  <ul>
89
  <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentQuestionIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
frontend/app/components/ui/autofill-prompt/autofill-prompt.interface.tsx CHANGED
@@ -2,7 +2,7 @@ export interface QuestionsBankProp {
2
  title: string;
3
  }
4
 
5
- export const questionsBank: QuestionsBankProp[] = [
6
  { title: "Under PSSCOC, what are the differences between the role of the SO Rep and SO?" },
7
  { title: "Who has the authority to appoint SO Rep assistants and what can the SO Rep assistants be authorized to do?" },
8
  { title: "What are the general obligations of the contractor and consultant?" },
@@ -11,4 +11,12 @@ export const questionsBank: QuestionsBankProp[] = [
11
  { title: "What are the requirements for the contractor to claim for loss and expense?" },
12
  { title: "Under PSSCOC, briefly describe the proper payment claim process prescribed." },
13
  // Add more common questions as needed
 
 
 
 
 
 
 
 
14
  ];
 
2
  title: string;
3
  }
4
 
5
+ export const psscocQuestionsBank: QuestionsBankProp[] = [
6
  { title: "Under PSSCOC, what are the differences between the role of the SO Rep and SO?" },
7
  { title: "Who has the authority to appoint SO Rep assistants and what can the SO Rep assistants be authorized to do?" },
8
  { title: "What are the general obligations of the contractor and consultant?" },
 
11
  { title: "What are the requirements for the contractor to claim for loss and expense?" },
12
  { title: "Under PSSCOC, briefly describe the proper payment claim process prescribed." },
13
  // Add more common questions as needed
14
+ ];
15
+
16
+ export const eirQuestionsBank: QuestionsBankProp[] = [
17
+ { 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." },
18
+ { 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?" },
19
+ { 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?" },
20
+ { title: "How many days do I have to submit the BIM Execution Plan and who should I submit it to?" },
21
+ // Add more common questions as needed
22
  ];
frontend/app/components/ui/autofill-prompt/autofill-search-prompt-dialog.tsx CHANGED
@@ -1,11 +1,11 @@
1
  import { useEffect, useState } from "react";
2
- import { QuestionsBankProp, questionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface";
3
  import { SearchHandler } from "@/app/components/ui/search/search.interface";
4
 
5
  export default function AutofillSearchQuery(
6
  props: Pick<
7
  SearchHandler,
8
- "query" | "isLoading" | "onSearchSubmit" | "onInputChange" | "results" | "searchButtonPressed"
9
  >,
10
  ) {
11
  // Keep track of whether to show the overlay
@@ -14,6 +14,8 @@ export default function AutofillSearchQuery(
14
  const [randomQuestions, setRandomQuestions] = useState<QuestionsBankProp[]>([]);
15
  // Keep track of the current question index
16
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
 
 
17
 
18
  // Shuffle the array using Fisher-Yates algorithm
19
  function shuffleArray(array: any[]) {
@@ -28,6 +30,13 @@ export default function AutofillSearchQuery(
28
 
29
  // Randomly select a subset of 3-4 questions
30
  useEffect(() => {
 
 
 
 
 
 
 
31
  // Shuffle the questionsBank array
32
  const shuffledQuestions = shuffleArray(questionsBank);
33
  // Get a random subset of 3-4 questions
@@ -37,7 +46,7 @@ export default function AutofillSearchQuery(
37
  setTimeout(() => {
38
  setRandomQuestions(selectedQuestions);
39
  }, 300);
40
- }, []);
41
 
42
 
43
  // Hide overlay when there are query
@@ -76,7 +85,8 @@ export default function AutofillSearchQuery(
76
  {showOverlay && (
77
  <div className="relative mx-auto">
78
  <div className="rounded-lg pt-5 pr-10 pl-10 flex flex-col divide-y overflow-y-auto pb-4 bg-white dark:bg-zinc-700/30 shadow-xl">
79
- <h2 className="text-lg text-center font-semibold mb-4">How can I help you today?</h2>
 
80
  {randomQuestions.map((question, index) => (
81
  <ul>
82
  <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentQuestionIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
 
1
  import { useEffect, useState } from "react";
2
+ import { QuestionsBankProp, psscocQuestionsBank, eirQuestionsBank } from "@/app/components/ui/autofill-prompt/autofill-prompt.interface";
3
  import { SearchHandler } from "@/app/components/ui/search/search.interface";
4
 
5
  export default function AutofillSearchQuery(
6
  props: Pick<
7
  SearchHandler,
8
+ "docSelected" | "query" | "isLoading" | "onSearchSubmit" | "onInputChange" | "results" | "searchButtonPressed"
9
  >,
10
  ) {
11
  // Keep track of whether to show the overlay
 
14
  const [randomQuestions, setRandomQuestions] = useState<QuestionsBankProp[]>([]);
15
  // Keep track of the current question index
16
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
17
+ // Questions bank for PSSCOC or EIR
18
+ const [questionsBank, setQuestionsBank] = useState<QuestionsBankProp[]>(psscocQuestionsBank);
19
 
20
  // Shuffle the array using Fisher-Yates algorithm
21
  function shuffleArray(array: any[]) {
 
30
 
31
  // Randomly select a subset of 3-4 questions
32
  useEffect(() => {
33
+ // Select the questions bank based on the document set selected
34
+ if (props.docSelected === "EIR") {
35
+ setQuestionsBank(eirQuestionsBank);
36
+ }
37
+ else {
38
+ setQuestionsBank(psscocQuestionsBank);
39
+ }
40
  // Shuffle the questionsBank array
41
  const shuffledQuestions = shuffleArray(questionsBank);
42
  // Get a random subset of 3-4 questions
 
46
  setTimeout(() => {
47
  setRandomQuestions(selectedQuestions);
48
  }, 300);
49
+ }, [questionsBank, props.docSelected]);
50
 
51
 
52
  // Hide overlay when there are query
 
85
  {showOverlay && (
86
  <div className="relative mx-auto">
87
  <div className="rounded-lg pt-5 pr-10 pl-10 flex flex-col divide-y overflow-y-auto pb-4 bg-white dark:bg-zinc-700/30 shadow-xl">
88
+ <h2 className="text-lg text-center font-semibold mb-4">How can I help with {props.docSelected} today?</h2>
89
+ {/* {dialogMessage && <p className="text-center text-sm text-gray-500 mb-4">{dialogMessage}</p>} */}
90
  {randomQuestions.map((question, index) => (
91
  <ul>
92
  <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentQuestionIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
frontend/app/components/ui/autofill-prompt/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import AutofillQuestion from "./autofill-prompt-dialog";
2
+ import AutofillSearchQuery from "./autofill-search-prompt-dialog";
3
+
4
+ export { AutofillQuestion, AutofillSearchQuery };
frontend/app/components/ui/chat/chat-input.tsx CHANGED
@@ -14,34 +14,37 @@ export default function ChatInput(
14
  return (
15
  <form
16
  onSubmit={props.handleSubmit}
17
- className="flex w-full items-start justify-between gap-4 rounded-xl bg-white dark:bg-zinc-700/30 p-4 shadow-xl"
18
  >
19
- <Input
20
- autoFocus
21
- name="message"
22
- placeholder="Type a Message"
23
- className="flex-1 bg-white dark:bg-zinc-500/30 z-10"
24
- value={props.input}
25
- onChange={props.handleInputChange}
26
- />
27
- <Button type="submit" disabled={props.isLoading} className="hidden md:flex items-center transition duration-300 ease-in-out transform hover:scale-110 z-10">
28
- {props.isLoading ? (
29
- <IconSpinner className="animate-spin" />
30
- ) : (
31
- // Fragment to avoid wrapping the text in a button
32
- <>
33
- <span className="pr-2">Send</span>
 
 
 
 
 
 
 
 
 
34
  <Send className="h-5 w-5" />
35
- </>
36
- )}
37
- </Button>
38
- <Button type="submit" disabled={props.isLoading} className="md:hidden z-10"> {/* Hide on larger screens */}
39
- {props.isLoading ? (
40
- <IconSpinner className="animate-spin" />
41
- ) : (
42
- <Send className="h-5 w-5" />
43
- )}
44
- </Button>
45
  </form>
46
  );
47
  }
 
14
  return (
15
  <form
16
  onSubmit={props.handleSubmit}
17
+ className="w-full items-start justify-between gap-4 rounded-xl bg-white dark:bg-zinc-700/30 p-4 shadow-xl"
18
  >
19
+ <div className="flex w-full items-start justify-between gap-4">
20
+ <Input
21
+ autoFocus
22
+ name="message"
23
+ placeholder="Type a Message"
24
+ className="flex-1 bg-white dark:bg-zinc-500/30"
25
+ value={props.input}
26
+ onChange={props.handleInputChange}
27
+ />
28
+ <Button type="submit" disabled={props.isLoading} className="hidden md:flex items-center transition duration-300 ease-in-out transform hover:scale-110 z-10">
29
+ {props.isLoading ? (
30
+ <IconSpinner className="animate-spin" />
31
+ ) : (
32
+ // Fragment to avoid wrapping the text in a button
33
+ <>
34
+ <span className="pr-2">Send</span>
35
+ <Send className="h-5 w-5" />
36
+ </>
37
+ )}
38
+ </Button>
39
+ <Button type="submit" disabled={props.isLoading} className="md:hidden z-10"> {/* Hide on larger screens */}
40
+ {props.isLoading ? (
41
+ <IconSpinner className="animate-spin" />
42
+ ) : (
43
  <Send className="h-5 w-5" />
44
+ )}
45
+ </Button>
46
+ </div>
47
+ <p className="text-center text-sm text-gray-400 mt-2">Smart Retrieval may not be 100% accurate. Consider checking important information.</p>
 
 
 
 
 
 
48
  </form>
49
  );
50
  }
frontend/app/components/ui/chat/chat-messages.tsx CHANGED
@@ -29,23 +29,25 @@ export default function ChatMessages(
29
  }, [messageLength, lastMessage]);
30
 
31
  return (
32
- <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl pb-0">
33
- <div
34
- className="flex h-[50vh] flex-col gap-5 divide-y overflow-y-auto pb-4"
35
- ref={scrollableChatContainerRef}
36
- >
37
- {props.messages.map((m) => (
38
- <ChatMessage key={m.id} {...m} />
39
- ))}
 
 
 
 
 
 
 
 
 
 
40
  </div>
41
- <div className="flex justify-end py-4">
42
- <ChatActions
43
- reload={props.reload}
44
- stop={props.stop}
45
- showReload={showReload}
46
- showStop={showStop}
47
- />
48
- </div>
49
- </div>
50
  );
51
  }
 
29
  }, [messageLength, lastMessage]);
30
 
31
  return (
32
+ messageLength > 0 && (
33
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl pb-0">
34
+ <div
35
+ className="flex h-[50vh] flex-col gap-5 divide-y overflow-y-auto pb-4"
36
+ ref={scrollableChatContainerRef}
37
+ >
38
+ {props.messages.map((m) => (
39
+ <ChatMessage key={m.id} {...m} />
40
+ ))}
41
+ </div>
42
+ <div className="flex justify-end py-4">
43
+ <ChatActions
44
+ reload={props.reload}
45
+ stop={props.stop}
46
+ showReload={showReload}
47
+ showStop={showStop}
48
+ />
49
+ </div>
50
  </div>
51
+ )
 
 
 
 
 
 
 
 
52
  );
53
  }
frontend/app/components/ui/chat/chat-selection.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { ChatHandler } from '@/app/components/ui/chat';
5
+
6
+ const DocumentSet = [
7
+ 'PSSCOC',
8
+ 'EIR',
9
+ // Add More Document Set as needed
10
+ ];
11
+
12
+ export default function ChatSelection(
13
+ props: Pick<ChatHandler, "docSelected" | "handleDocSelect">,
14
+ ) {
15
+ const [currentDocumentIndex, setCurrentDocumentIndex] = useState(0);
16
+
17
+ const handleDocumentSetChange = (documentSet: string) => {
18
+ props.handleDocSelect(documentSet);
19
+ };
20
+
21
+ // Automatically advance to the next document set after a delay
22
+ useEffect(() => {
23
+ const timer = setInterval(() => {
24
+ if (currentDocumentIndex < DocumentSet.length - 1) {
25
+ setCurrentDocumentIndex((prevIndex) => prevIndex + 1);
26
+ }
27
+ else {
28
+ clearInterval(timer); // Stop the timer when all document set have been displayed
29
+ }
30
+ }, 100); // Adjust the delay time as needed (e.g., 5000 milliseconds = 5 seconds)
31
+
32
+ return () => clearInterval(timer); // Cleanup the timer on component unmount
33
+ }, [currentDocumentIndex, DocumentSet]);
34
+
35
+ return (
36
+ <div className="w-full rounded-xl bg-white dark:bg-zinc-700/30 dark:from-inherit p-4 shadow-xl pb-0">
37
+ <div className="rounded-lg pt-5 pr-10 pl-10 flex h-[50vh] flex-col divide-y overflow-y-auto pb-4">
38
+ <h2 className="text-lg text-center font-semibold mb-4">Select Document Set to Chat with:</h2>
39
+ {/* <p className="text-center text-sm text-gray-500 mb-4">{dialogMessage}</p> */}
40
+ {DocumentSet.map((title, index) => (
41
+ <ul>
42
+ <li key={index} className={`p-2 mb-2 border border-zinc-500/30 dark:border-white rounded-lg hover:bg-zinc-500/30 transition duration-300 ease-in-out transform cursor-pointer ${index <= currentDocumentIndex ? 'opacity-100 duration-500' : 'opacity-0'}`}>
43
+ <button
44
+ className="text-blue-500 w-full text-left"
45
+ onClick={() => handleDocumentSetChange(title)}
46
+ >
47
+ {title}
48
+ </button>
49
+ </li>
50
+ </ul>
51
+ ))}
52
+ </div>
53
+ </div>
54
+ );
55
+ };
frontend/app/components/ui/chat/chat.interface.ts CHANGED
@@ -5,6 +5,8 @@ export interface Message {
5
  }
6
 
7
  export interface ChatHandler {
 
 
8
  messages: Message[];
9
  input: string;
10
  isLoading: boolean;
 
5
  }
6
 
7
  export interface ChatHandler {
8
+ docSelected: string;
9
+ handleDocSelect: (doc: string) => void;
10
  messages: Message[];
11
  input: string;
12
  isLoading: boolean;
frontend/app/components/ui/chat/index.ts CHANGED
@@ -1,5 +1,6 @@
1
  import ChatInput from "./chat-input";
2
  import ChatMessages from "./chat-messages";
 
3
 
4
  export { type ChatHandler, type Message } from "./chat.interface";
5
- export { ChatInput, ChatMessages };
 
1
  import ChatInput from "./chat-input";
2
  import ChatMessages from "./chat-messages";
3
+ import ChatSelection from "./chat-selection";
4
 
5
  export { type ChatHandler, type Message } from "./chat.interface";
6
+ export { ChatInput, ChatMessages, ChatSelection };
frontend/app/components/ui/login-buttons.tsx CHANGED
@@ -5,6 +5,7 @@ import { signIn } from 'next-auth/react'
5
  import { cn } from '@/app/components/ui/lib/utils'
6
  import { Button, type ButtonProps } from '@/app/components/ui/button'
7
  import { IconGoogle, IconSGid, IconSpinner } from '@/app/components/ui/icons'
 
8
 
9
  interface LoginButtonProps extends ButtonProps {
10
  showIcon?: boolean;
@@ -18,13 +19,19 @@ function GoogleLoginButton({
18
  ...props
19
  }: LoginButtonProps) {
20
  const [isLoading, setIsLoading] = useState(false);
21
-
 
 
 
 
 
 
22
  return (
23
  <Button
24
  variant="outline"
25
  onClick={() => {
26
  setIsLoading(true);
27
- signIn('google');
28
  }}
29
  className={cn(className)}
30
  {...props}
@@ -47,13 +54,19 @@ function SGIDLoginButton({
47
  ...props
48
  }: LoginButtonProps) {
49
  const [isLoading, setIsLoading] = useState(false);
50
-
 
 
 
 
 
 
51
  return (
52
  <Button
53
  variant="outline"
54
  onClick={() => {
55
  setIsLoading(true);
56
- signIn('sgid');
57
  }}
58
  className={cn(className)}
59
  {...props}
 
5
  import { cn } from '@/app/components/ui/lib/utils'
6
  import { Button, type ButtonProps } from '@/app/components/ui/button'
7
  import { IconGoogle, IconSGid, IconSpinner } from '@/app/components/ui/icons'
8
+ import { useSearchParams } from 'next/navigation'
9
 
10
  interface LoginButtonProps extends ButtonProps {
11
  showIcon?: boolean;
 
19
  ...props
20
  }: LoginButtonProps) {
21
  const [isLoading, setIsLoading] = useState(false);
22
+ const searchParams = useSearchParams()
23
+ let tempcallbackURL = searchParams.get("callbackUrl"); // Get the 'callbackURL' query parameter
24
+ let callbackURL = tempcallbackURL;
25
+ // if callbackURL is not provided, default to home page
26
+ if (!tempcallbackURL) {
27
+ callbackURL = '/';
28
+ }
29
  return (
30
  <Button
31
  variant="outline"
32
  onClick={() => {
33
  setIsLoading(true);
34
+ signIn('google', { redirect: true, callbackUrl: callbackURL as string });
35
  }}
36
  className={cn(className)}
37
  {...props}
 
54
  ...props
55
  }: LoginButtonProps) {
56
  const [isLoading, setIsLoading] = useState(false);
57
+ const searchParams = useSearchParams()
58
+ const tempcallbackURL = searchParams.get("callbackUrl"); // Get the 'callbackURL' query parameter
59
+ let callbackURL = tempcallbackURL;
60
+ // if callbackURL is not provided, default to home page
61
+ if (!tempcallbackURL) {
62
+ callbackURL = '/';
63
+ }
64
  return (
65
  <Button
66
  variant="outline"
67
  onClick={() => {
68
  setIsLoading(true);
69
+ signIn('sgid', { redirect: true, callbackUrl: callbackURL as string });
70
  }}
71
  className={cn(className)}
72
  {...props}
frontend/app/components/ui/mobilemenu.tsx CHANGED
@@ -70,7 +70,7 @@ const MobileMenu: React.FC<MobileMenuProps> = ({ isOpen, onClose, logoSrc, items
70
  {/* Mobile menu content */}
71
  <div className="w-64 p-4 rounded-r-md">
72
  {items.map((item, index) => (
73
- <HeaderNavLink key={index} href={item.href} onClick={onClose}>
74
  <div className="flex items-center mb-4">
75
  {item.icon}
76
  {item.label}
 
70
  {/* Mobile menu content */}
71
  <div className="w-64 p-4 rounded-r-md">
72
  {items.map((item, index) => (
73
+ <HeaderNavLink key={index} href={item.href} title={item.label} onClick={onClose}>
74
  <div className="flex items-center mb-4">
75
  {item.icon}
76
  {item.label}
frontend/app/components/ui/navlink.tsx CHANGED
@@ -5,12 +5,13 @@ import Link from 'next/link';
5
 
6
  export interface NavLinkProps {
7
  href: string;
 
8
  children: React.ReactNode;
9
  onClick?: () => void; // Include onClick as an optional prop
10
  target?: string;
11
  }
12
 
13
- const HeaderNavLink: React.FC<NavLinkProps> = ({ href, children, onClick }) => {
14
  // Use the useRouter hook to get information about the current route
15
  const pathname = usePathname();
16
 
@@ -24,7 +25,7 @@ const HeaderNavLink: React.FC<NavLinkProps> = ({ href, children, onClick }) => {
24
  };
25
 
26
  return (
27
- <Link href={href} passHref>
28
  {/* Add a class to highlight the active tab */}
29
  <div className={`flex items-center font-bold ${isActive ? 'text-blue-500' : ''}`} onClick={handleClick}>
30
  {children}
@@ -33,7 +34,7 @@ const HeaderNavLink: React.FC<NavLinkProps> = ({ href, children, onClick }) => {
33
  );
34
  };
35
 
36
- const FooterNavLink: React.FC<NavLinkProps> = ({ href, children, onClick, target }) => {
37
  const handleClick = () => {
38
  if (onClick) {
39
  onClick(); // Call the onClick handler if provided
@@ -41,7 +42,7 @@ const FooterNavLink: React.FC<NavLinkProps> = ({ href, children, onClick, target
41
  };
42
 
43
  return (
44
- <Link href={href} passHref target={target}>
45
  {/* Add a class to highlight the active tab */}
46
  <div className="flex items-center font-bold" onClick={handleClick}>
47
  {children}
 
5
 
6
  export interface NavLinkProps {
7
  href: string;
8
+ title: string;
9
  children: React.ReactNode;
10
  onClick?: () => void; // Include onClick as an optional prop
11
  target?: string;
12
  }
13
 
14
+ const HeaderNavLink: React.FC<NavLinkProps> = ({ href, title, children, onClick }) => {
15
  // Use the useRouter hook to get information about the current route
16
  const pathname = usePathname();
17
 
 
25
  };
26
 
27
  return (
28
+ <Link href={href} passHref title={title}>
29
  {/* Add a class to highlight the active tab */}
30
  <div className={`flex items-center font-bold ${isActive ? 'text-blue-500' : ''}`} onClick={handleClick}>
31
  {children}
 
34
  );
35
  };
36
 
37
+ const FooterNavLink: React.FC<NavLinkProps> = ({ href, title, children, onClick, target }) => {
38
  const handleClick = () => {
39
  if (onClick) {
40
  onClick(); // Call the onClick handler if provided
 
42
  };
43
 
44
  return (
45
+ <Link href={href} passHref target={target} title={title}>
46
  {/* Add a class to highlight the active tab */}
47
  <div className="flex items-center font-bold" onClick={handleClick}>
48
  {children}