-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Update .env example & next.config * Update README * Vector Search UI & Skeletal Code to send request to backend * Update themes to use system preference * Update <main> container to be reuseable * Update avatar and chat api endpoint * Update Header to be Mobile Responsive * Update README.md * Update README.md * Create LICENSE * Update README.md * Update README.md * Update README.md * Minor UI Updates * Add Backend API Status page and Status icon to Header * Add Query Function Page * Add Healthcheck Status API * Add Query API * Update Imports * Update API Routes * Update Indexing Function * Update Search Frontend & API * Update Dependencies * Simple healthcheck endpoint test * Update Imports & Dependencies * Update Page * Update README.md * Update README.md * Update Search Functions * Improved UI for table, added toast msgs * Fixed Z depth of sign-in page * Moved 'Header' & 'Main' component to layout
- Loading branch information
Showing
41 changed files
with
1,922 additions
and
1,204 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Digital Built Environment | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from fastapi import APIRouter, Request | ||
|
||
healthcheck_router = r = APIRouter() | ||
|
||
""" | ||
This router is for healthcheck functionality. | ||
""" | ||
|
||
|
||
@r.get("") | ||
async def healthcheck( | ||
request: Request, | ||
# index: VectorStoreIndex = Depends(get_index), | ||
): | ||
results = {} | ||
# check if index is ready | ||
# if index: | ||
# results["index"] = True | ||
# else: | ||
# results["index"] = False | ||
|
||
# TODO: check if other services are ready | ||
|
||
# logger.info("Healthcheck: {results}") | ||
|
||
results = {"status": "OK"} | ||
return results | ||
|
||
|
||
# Simple test to check if the healthcheck endpoint is working | ||
def test_healthcheck(): | ||
assert healthcheck() == {"status": "OK"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import logging | ||
from typing import List | ||
|
||
from app.utils.index import get_index | ||
from app.utils.json import json_to_model | ||
from fastapi import APIRouter, Depends, HTTPException, Request, status | ||
from fastapi.responses import StreamingResponse | ||
from fastapi.websockets import WebSocketDisconnect | ||
from llama_index import VectorStoreIndex | ||
from llama_index.llms.types import MessageRole | ||
from pydantic import BaseModel | ||
|
||
query_router = r = APIRouter() | ||
|
||
""" | ||
This router is for query functionality which consist of query engine. | ||
The query engine is used to query the index. | ||
There is no chat memory used here, every query is independent of each other. | ||
""" | ||
|
||
|
||
class _Message(BaseModel): | ||
role: MessageRole | ||
content: str | ||
|
||
|
||
class _ChatData(BaseModel): | ||
messages: List[_Message] | ||
|
||
|
||
@r.get("") | ||
async def search( | ||
request: Request, | ||
# Note: To support clients sending a JSON object using content-type "text/plain", | ||
# we need to use Depends(json_to_model(_ChatData)) here | ||
data: _ChatData = Depends(json_to_model(_ChatData)), | ||
index: VectorStoreIndex = Depends(get_index), | ||
): | ||
# check preconditions and get last message which is query | ||
if len(data.messages) == 0: | ||
raise HTTPException( | ||
status_code=status.HTTP_400_BAD_REQUEST, | ||
detail="No query provided", | ||
) | ||
query = data.messages.pop() | ||
if query.role != MessageRole.USER: | ||
raise HTTPException( | ||
status_code=status.HTTP_400_BAD_REQUEST, | ||
detail="Last message must be from user", | ||
) | ||
logger = logging.getLogger("uvicorn") | ||
logger.info(f"Query: {query}") | ||
|
||
# Query index | ||
query_engine = index.as_query_engine(streaming=True, similarity_top_k=1) | ||
response = query_engine.query(query) | ||
|
||
# stream response | ||
async def event_generator(): | ||
try: | ||
logger = logging.getLogger("uvicorn") | ||
for token in response.response_gen: | ||
# If client closes connection, stop sending events | ||
if await request.is_disconnected(): | ||
logger.info("Client disconnected, closing stream") | ||
break | ||
yield token | ||
except WebSocketDisconnect: | ||
# WebSocket was disconnected, gracefully handle it | ||
logger.info("Client disconnected, closing stream") | ||
|
||
return StreamingResponse(event_generator(), media_type="text/plain") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import logging | ||
import re | ||
|
||
from app.utils.index import get_index | ||
from fastapi import APIRouter, Depends, HTTPException, Request, status | ||
from llama_index import VectorStoreIndex | ||
from llama_index.postprocessor import SimilarityPostprocessor | ||
from llama_index.retrievers import VectorIndexRetriever | ||
|
||
search_router = r = APIRouter() | ||
|
||
""" | ||
This router is for search functionality which consist of query engine. | ||
The query engine is used to query the index. | ||
It is similar to query except that it does not return the formulated response. | ||
Instead it returns the relevant information from the index. | ||
""" | ||
|
||
|
||
@r.get("") | ||
async def search( | ||
request: Request, | ||
index: VectorStoreIndex = Depends(get_index), | ||
): | ||
query = request.query_params.get("query") | ||
logger = logging.getLogger("uvicorn") | ||
logger.info(f"Search: {query}") | ||
if query is None: | ||
raise HTTPException( | ||
status_code=status.HTTP_400_BAD_REQUEST, | ||
detail="No search info provided", | ||
) | ||
|
||
# configure retriever | ||
retriever = VectorIndexRetriever( | ||
index=index, | ||
similarity_top_k=10, | ||
) | ||
# similarity postprocessor: filter nodes below 0.45 similarity score | ||
node_postprocessor = SimilarityPostprocessor(similarity_cutoff=0.45) | ||
|
||
# retrieve results | ||
query_results = retriever.retrieve(query) | ||
|
||
query_results_scores = [result.get_score() for result in query_results] | ||
|
||
logger.info(f"Search results similarity score: {query_results_scores}") | ||
|
||
# postprocess results | ||
filtered_results = node_postprocessor.postprocess_nodes(query_results) | ||
|
||
filtered_results_scores = [result.get_score() for result in filtered_results] | ||
|
||
logger.info(f"Filtered Search results similarity score: {filtered_results_scores}") | ||
|
||
response = [] | ||
id = 1 | ||
for node in filtered_results: | ||
node_dict = node.to_dict()["node"] | ||
logger.debug(f"Node dict: {node_dict}") | ||
node_metadata = node_dict["metadata"] | ||
logger.debug(f"Node metadata: {node_metadata}") | ||
data = {} | ||
data["id"] = id | ||
data["file_name"] = node_metadata["file_name"] | ||
data["page_no"] = node_metadata["page_label"] | ||
cleaned_text = re.sub( | ||
"^_+ | _+$", "", node_dict["text"] | ||
) # remove leading and trailing underscores | ||
data["text"] = cleaned_text | ||
data["similarity_score"] = round( | ||
node.get_score(), 2 | ||
) # round to 2 decimal places | ||
response.append(data) | ||
id += 1 | ||
|
||
# TODO: do a reranking of the results and return them? | ||
# TODO: do a highlighting of the results in the relevant documents and return them? | ||
return response |
Oops, something went wrong.