From caae15fd854f28cfd0b476584f6e1bbd45c767b2 Mon Sep 17 00:00:00 2001 From: Yee Kit Date: Thu, 25 Jan 2024 14:24:36 +0800 Subject: [PATCH] V.0.1.0 (#3) * Update .env example & next.config * Update README * Vector Search UI & Skeletal Code to send request to backend * Update themes to use system preference * Update
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 --- LICENSE | 21 + README.md | 22 +- backend/README.md | 45 +- backend/backend/app/api/routers/chat.py | 14 +- .../backend/app/api/routers/healthcheck.py | 32 + backend/backend/app/api/routers/query.py | 72 + backend/backend/app/api/routers/search.py | 79 + backend/backend/app/utils/index.py | 50 +- backend/backend/app/utils/prompt_template.py | 9 +- backend/backend/main.py | 8 +- backend/poetry.lock | 103 +- frontend/README.md | 10 +- frontend/app/about/page.tsx | 39 +- frontend/app/chat/page.tsx | 6 +- frontend/app/components/chat-section.tsx | 2 +- frontend/app/components/header.tsx | 219 +- frontend/app/components/login-buttons.tsx | 6 +- frontend/app/components/query-section.tsx | 33 + frontend/app/components/search-section.tsx | 36 + frontend/app/components/ui/button.tsx | 5 +- .../app/components/ui/chat/chat-avatar.tsx | 9 +- .../app/components/ui/chat/chat-message.tsx | 3 + .../ui/chat/use-copy-to-clipboard.tsx | 12 +- frontend/app/components/ui/icons.tsx | 4 +- frontend/app/components/ui/input.tsx | 4 +- frontend/app/components/ui/main-container.tsx | 18 + .../app/components/ui/search/search-input.tsx | 44 + .../components/ui/search/search-results.tsx | 101 + .../app/components/ui/search/search-types.tsx | 9 + .../app/components/ui/search/useSearch.tsx | 63 + frontend/app/layout.tsx | 9 +- frontend/app/page.tsx | 72 +- frontend/app/providers.tsx | 2 +- frontend/app/query/page.tsx | 10 + frontend/app/search/page.tsx | 8 +- frontend/app/sign-in/page.tsx | 54 +- frontend/app/status/page.tsx | 74 + frontend/example.env | 9 +- frontend/next.config.js | 9 +- frontend/package-lock.json | 1792 ++++++++--------- frontend/package.json | 9 +- 41 files changed, 1922 insertions(+), 1204 deletions(-) create mode 100644 LICENSE create mode 100644 backend/backend/app/api/routers/healthcheck.py create mode 100644 backend/backend/app/api/routers/query.py create mode 100644 backend/backend/app/api/routers/search.py create mode 100644 frontend/app/components/query-section.tsx create mode 100644 frontend/app/components/search-section.tsx create mode 100644 frontend/app/components/ui/main-container.tsx create mode 100644 frontend/app/components/ui/search/search-input.tsx create mode 100644 frontend/app/components/ui/search/search-results.tsx create mode 100644 frontend/app/components/ui/search/search-types.tsx create mode 100644 frontend/app/components/ui/search/useSearch.tsx create mode 100644 frontend/app/query/page.tsx create mode 100644 frontend/app/status/page.tsx diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..05c6872 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index 498149a..4ba978a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
[![Status](https://img.shields.io/badge/status-active-success.svg)]() -[![GitHub Issues](https://img.shields.io/github/issues/digitalbuiltenvironment/Smart-Retrieval.svg)](https://github.com/digitalbuiltenvironment/Smart-Retrieval) +[![GitHub Issues](https://img.shields.io/github/issues/digitalbuiltenvironment/Smart-Retrieval.svg)](https://github.com/digitalbuiltenvironment/Smart-Retrieval/issues) [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/digitalbuiltenvironment/Smart-Retrieval.svg)](https://github.com/digitalbuiltenvironment/Smart-Retrieval/pulls) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) @@ -16,7 +16,7 @@ --- -

A Large Language Model (LLM) powered chatbot for information retrieval. +

A Large Language Model (LLM) powered platform for information retrieval.

@@ -26,14 +26,14 @@ - [Getting Started](#getting_started) - [Deployment](#deployment) - [Built Using](#built_using) -- [TODO](../TODO.md) - [Contributing](../CONTRIBUTING.md) - [Authors](#authors) - [Acknowledgments](#acknowledgement) ## 🧐 About -Write about 1-2 paragraphs describing the purpose of your project. +Smart Retrieval is a platform for efficient and streamlined information retrieval, especially in the realm of legal and compliance documents. +With the power of Open-Source Large Language Models (LLM) and Retrieval Augmented Generation (RAG), it aims to enhance user experiences at JTC by addressing key challenges such as manual search inefficiencies and rigid file naming conventions, revolutionizing the way JTC employees access and comprehend crucial documents Project files bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama). @@ -50,18 +50,20 @@ These instructions will get you a copy of the project up and running on your loc ## 🚀 Deployment -Add additional notes about how to deploy this on a live system. +How to deploy this on a live system. ## ⛏️ Built Using -- [MongoDB](https://www.mongodb.com/) - Database -- [Express](https://expressjs.com/) - Server Framework -- [VueJs](https://vuejs.org/) - Web Framework -- [NodeJs](https://nodejs.org/en/) - Server Environment +- [NextJs](https://nextjs.org/) - Frontend Web Framework +- [Vercel AI](https://vercel.com/ai) - AI SDK library for building AI-powered streaming text and chat UIs. +- [NodeJs](https://nodejs.org/en/) - Frontend Server Environment +- [Python](https://python.org/) - Backend Server Environment +- [FastAPI](https://fastapi.tiangolo.com/) - Backend API Web Framework +- [LlamaIndex](https://www.llamaindex.ai/) - Data Framework for LLM ## ✍️ Authors -- [@xkhronoz](https://github.com/xkhronoz) - Idea & Initial work +- [@xkhronoz](https://github.com/xkhronoz) - Initial work See also the list of [contributors](https://github.com/digitalbuiltenvironment/Smart-Retrieval/contributors) who participated in this project. diff --git a/backend/README.md b/backend/README.md index 56c003c..b25a231 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,23 +1,50 @@ -This is a [LlamaIndex](https://www.llamaindex.ai/) backend using [FastAPI](https://fastapi.tiangolo.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama). +# Smart Retrieval Backend + +The backend is built using Python & [FastAPI](https://fastapi.tiangolo.com/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama). + +## Requirements + +1. Python >= 3.11 +2. Poetry (To manage dependencies) + - ```pipx install poetry``` ## Getting Started -First, setup the environment: +First, setup the `pyproject.toml` file to install the correct version of PyTorch (CPU or GPU): +Comment/Uncomment the following block depending on your system. Use CPU only if you do not have a supported Cuda Device. + +```bash +# For CPU version: Windows and Linux and MacOS (arm64) +torch = [ + { url = "https://download.pytorch.org/whl/cpu/torch-2.1.1%2Bcpu-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.1.1%2Bcpu-cp311-cp311-linux_x86_64.whl", markers = "sys_platform == 'linux'" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.1.1-cp311-none-macosx_11_0_arm64.whl", markers = "sys_platform == 'darwin'" }, +] +## For GPU version: Windows and Linux and MacOS (arm64) +# torch = [ +# { url = "https://download.pytorch.org/whl/cu121/torch-2.1.1%2Bcu121-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'" }, +# { url = "https://download.pytorch.org/whl/cu121/torch-2.1.1%2Bcu121-cp311-cp311-linux_x86_64.whl", markers = "sys_platform == 'linux'" }, +# { url = "https://download.pytorch.org/whl/cu121/torch-2.1.1-cp311-none-macosx_11_0_arm64.whl", markers = "sys_platform == 'darwin'" }, +# ] ``` + +Second, setup the environment: + +```bash poetry install poetry shell ``` -Second, run the development server: +Third, run the development server: -``` -python main.py +```bash +python run.py ``` Then call the API endpoint `/api/chat` to see the result: -``` +```bash curl --location 'localhost:8000/api/chat' \ --header 'Content-Type: application/json' \ --data '{ "messages": [{ "role": "user", "content": "Hello" }] }' @@ -29,7 +56,7 @@ Open [http://localhost:8000/docs](http://localhost:8000/docs) with your browser The API allows CORS for all origins to simplify development. You can change this behavior by setting the `ENVIRONMENT` environment variable to `prod`: -``` +```bash ENVIRONMENT=prod uvicorn main:app ``` @@ -38,5 +65,5 @@ ENVIRONMENT=prod uvicorn main:app To learn more about LlamaIndex, take a look at the following resources: - [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex. - -You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome! +- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndexTS (Typescript features). +- [FastAPI Documentation](https://fastapi.tiangolo.com/) - learn about FastAPI. diff --git a/backend/backend/app/api/routers/chat.py b/backend/backend/app/api/routers/chat.py index 0d3ec29..ef9910c 100644 --- a/backend/backend/app/api/routers/chat.py +++ b/backend/backend/app/api/routers/chat.py @@ -7,13 +7,24 @@ from fastapi.responses import StreamingResponse from fastapi.websockets import WebSocketDisconnect from llama_index import VectorStoreIndex -from llama_index.llms.base import ChatMessage, MessageRole +from llama_index.llms.base import ChatMessage +from llama_index.llms.types import MessageRole from llama_index.memory import ChatMemoryBuffer from llama_index.prompts import PromptTemplate from pydantic import BaseModel chat_router = r = APIRouter() +""" +This router is for chatbot functionality which consist of chat memory and chat engine. +The chat memory is used to store the chat history and the chat engine is used to query the chat memory and context. +Chat engine is a wrapper around the query engine and it is used to query the chat memory and context. +Chat engine also does the following: +1. Condense the question based on the chat history +2. Add context to the question +3. Answer the question +""" + class _Message(BaseModel): role: MessageRole @@ -24,6 +35,7 @@ class _ChatData(BaseModel): messages: List[_Message] +# custom prompt template to be used by chat engine custom_prompt = PromptTemplate( """\ Given a conversation (between Human and Assistant) and a follow up message from Human, \ diff --git a/backend/backend/app/api/routers/healthcheck.py b/backend/backend/app/api/routers/healthcheck.py new file mode 100644 index 0000000..674b8cf --- /dev/null +++ b/backend/backend/app/api/routers/healthcheck.py @@ -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"} diff --git a/backend/backend/app/api/routers/query.py b/backend/backend/app/api/routers/query.py new file mode 100644 index 0000000..57e1b69 --- /dev/null +++ b/backend/backend/app/api/routers/query.py @@ -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") diff --git a/backend/backend/app/api/routers/search.py b/backend/backend/app/api/routers/search.py new file mode 100644 index 0000000..b234166 --- /dev/null +++ b/backend/backend/app/api/routers/search.py @@ -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 diff --git a/backend/backend/app/utils/index.py b/backend/backend/app/utils/index.py index 6dce017..9839db1 100644 --- a/backend/backend/app/utils/index.py +++ b/backend/backend/app/utils/index.py @@ -3,6 +3,7 @@ from pathlib import Path from llama_index import ( + PromptHelper, ServiceContext, SimpleDirectoryReader, StorageContext, @@ -50,14 +51,32 @@ verbose=True, ) +# define prompt helper +# set maximum input size +max_input_size = 4096 +# set number of output tokens +num_output = 256 +# set maximum chunk overlap +max_chunk_overlap = 0.2 + embed_model = HuggingFaceEmbedding( model_name="sentence-transformers/all-MiniLM-L6-v2", pooling="mean", device=DEVICE_TYPE, ) +prompt_helper = PromptHelper( + chunk_size_limit=4096, + chunk_overlap_ratio=0.2, + num_output=256, +) + service_context = ServiceContext.from_defaults( - llm=llm, embed_model=embed_model, chunk_size=1000, chunk_overlap=100 + llm=llm, + embed_model=embed_model, + chunk_size=1000, + chunk_overlap=100, + prompt_helper=prompt_helper, ) set_global_service_context(service_context) @@ -86,17 +105,30 @@ def create_index(): logger.info(f"Index already exist at {STORAGE_DIR}...") -def get_index(): +def load_existing_index(): + # load the existing index logger = logging.getLogger("uvicorn") + logger.info(f"Loading index from {STORAGE_DIR}...") + storage_context = StorageContext.from_defaults(persist_dir=STORAGE_DIR) + index = load_index_from_storage(storage_context, service_context=service_context) + logger.info(f"Finished loading index from {STORAGE_DIR}") + return index + + +def get_index(): # check if storage already exists if not os.path.exists(STORAGE_DIR): + # create the index if it does not exist create_index() + # load the index from storage + index = load_existing_index() + # check if storage is empty, 4 files should be present if using simplevectorstore + elif os.path.exists(STORAGE_DIR) and len(os.listdir(STORAGE_DIR)) < 4: + # create the index if it does not exist + create_index() + # load the index from storage + index = load_existing_index() else: - # load the existing index - logger.info(f"Loading index from {STORAGE_DIR}...") - storage_context = StorageContext.from_defaults(persist_dir=STORAGE_DIR) - index = load_index_from_storage( - storage_context, service_context=service_context - ) - logger.info(f"Finished loading index from {STORAGE_DIR}") + # load the index from storage + index = load_existing_index() return index diff --git a/backend/backend/app/utils/prompt_template.py b/backend/backend/app/utils/prompt_template.py index abf4df0..ebc0026 100644 --- a/backend/backend/app/utils/prompt_template.py +++ b/backend/backend/app/utils/prompt_template.py @@ -8,9 +8,12 @@ # this is specific to Llama-2. -system_prompt = """You are a helpful assistant, you will use the provided context to answer user questions. +system_prompt = """ +You are a helpful assistant, you will use the provided context to answer user questions. Read the given context before answering questions and think step by step. If you can not answer a user question based on -the provided context, inform the user. Do not use any other information for answering user. Provide a detailed answer to the question.""" +the provided context, inform the user. Do not use any other information for answering user. +Provide a detailed answer to the question. +""" def get_prompt_template( @@ -25,7 +28,7 @@ def get_prompt_template( {context_str} {chat_history} - + {question}""" prompt_template = B_INST + SYSTEM_PROMPT + instruction + E_INST diff --git a/backend/backend/main.py b/backend/backend/main.py index b32ec49..7e4d920 100644 --- a/backend/backend/main.py +++ b/backend/backend/main.py @@ -2,6 +2,9 @@ import os from app.api.routers.chat import chat_router +from app.api.routers.healthcheck import healthcheck_router +from app.api.routers.query import query_router +from app.api.routers.search import search_router from app.utils.index import create_index from dotenv import load_dotenv from fastapi import FastAPI @@ -26,6 +29,9 @@ ) app.include_router(chat_router, prefix="/api/chat") +app.include_router(query_router, prefix="/api/query") +app.include_router(search_router, prefix="/api/search") +app.include_router(healthcheck_router, prefix="/api/healthcheck") -# try create the index first +# try to create the index first on startup create_index() diff --git a/backend/poetry.lock b/backend/poetry.lock index 05dcbf6..ab370a7 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -109,20 +109,6 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" -[[package]] -name = "aiostream" -version = "0.5.2" -description = "Generator-based operators for asynchronous iteration" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiostream-0.5.2-py3-none-any.whl", hash = "sha256:054660370be9d37f6fe3ece3851009240416bd082e469fd90cc8673d3818cf71"}, - {file = "aiostream-0.5.2.tar.gz", hash = "sha256:b71b519a2d66c38f0872403ab86417955b77352f08d9ad02ad46fc3926b389f4"}, -] - -[package.dependencies] -typing-extensions = "*" - [[package]] name = "annotated-types" version = "0.6.0" @@ -799,20 +785,19 @@ test = ["httpx (>=0.24.1)", "pytest (>=7.4.0)"] [[package]] name = "llama-index" -version = "0.9.4" +version = "0.9.22" description = "Interface between LLMs and your data" optional = false -python-versions = ">=3.8.1,<3.12" +python-versions = ">=3.8.1,<4.0" files = [ - {file = "llama_index-0.9.4-py3-none-any.whl", hash = "sha256:c3250dd15f23e24b61db046c762b6f86420164a7d02f28dd693eea828e3f9786"}, - {file = "llama_index-0.9.4.tar.gz", hash = "sha256:79823e99d6a874019670ad057da1428ac620a4ce45b58b234aa483f40c537d10"}, + {file = "llama_index-0.9.22-py3-none-any.whl", hash = "sha256:8731b2137decfae9f56a2837f3791d8e4ce434a9ec9d6d74d6dafb2d736f05f7"}, + {file = "llama_index-0.9.22.tar.gz", hash = "sha256:7eecb62106398f9c5b1c04e79fee82a1004c26387055335c23a8f33833c8afff"}, ] [package.dependencies] aiohttp = ">=3.8.6,<4.0.0" -aiostream = ">=0.5.2,<0.6.0" beautifulsoup4 = ">=4.12.2,<5.0.0" -dataclasses-json = ">=0.5.7,<0.6.0" +dataclasses-json = "*" deprecated = ">=1.2.9.3" fsspec = ">=2023.5.0" httpx = "*" @@ -820,19 +805,20 @@ nest-asyncio = ">=1.5.8,<2.0.0" nltk = ">=3.8.1,<4.0.0" numpy = "*" openai = ">=1.1.0" -pandas = {version = "*", extras = ["jinja2"]} +pandas = "*" +requests = ">=2.31.0" SQLAlchemy = {version = ">=1.4.49", extras = ["asyncio"]} tenacity = ">=8.2.0,<9.0.0" tiktoken = ">=0.3.3" typing-extensions = ">=4.5.0" typing-inspect = ">=0.8.0" -urllib3 = "<2" [package.extras] +gradientai = ["gradientai (>=1.4.0)"] langchain = ["langchain (>=0.0.303)"] local-models = ["optimum[onnxruntime] (>=1.13.2,<2.0.0)", "sentencepiece (>=0.1.99,<0.2.0)", "transformers[torch] (>=4.34.0,<5.0.0)"] postgres = ["asyncpg (>=0.28.0,<0.29.0)", "pgvector (>=0.1.0,<0.2.0)", "psycopg-binary (>=3.1.12,<4.0.0)"] -query-tools = ["guidance (>=0.0.64,<0.0.65)", "jsonpath-ng (>=1.6.0,<2.0.0)", "lm-format-enforcer (>=0.4.3,<0.5.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "scikit-learn (<1.3.0)", "spacy (>=3.7.1,<4.0.0)"] +query-tools = ["guidance (>=0.0.64,<0.0.65)", "jsonpath-ng (>=1.6.0,<2.0.0)", "lm-format-enforcer (>=0.4.3,<0.5.0)", "rank-bm25 (>=0.2.2,<0.3.0)", "scikit-learn", "spacy (>=3.7.1,<4.0.0)"] [[package]] name = "markupsafe" @@ -2187,12 +2173,75 @@ testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"] [[package]] name = "torch" -version = "2.1.1+cu121" +version = "2.1.1" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "torch-2.1.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:e312f7e82e49565f7667b0bbf9559ab0c597063d93044740781c02acd5a87978"}, +] + +[package.dependencies] +filelock = "*" +fsspec = "*" +jinja2 = "*" +networkx = "*" +nvidia-cublas-cu12 = {version = "12.1.3.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-cupti-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-nvrtc-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cuda-runtime-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cudnn-cu12 = {version = "8.9.2.26", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cufft-cu12 = {version = "11.0.2.54", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-curand-cu12 = {version = "10.3.2.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusolver-cu12 = {version = "11.4.5.107", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-cusparse-cu12 = {version = "12.1.0.106", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nccl-cu12 = {version = "2.18.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +nvidia-nvtx-cu12 = {version = "12.1.105", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +sympy = "*" +triton = {version = "2.1.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} +typing-extensions = "*" + +[package.extras] +opt-einsum = ["opt-einsum (>=3.3)"] + +[package.source] +type = "url" +url = "https://download.pytorch.org/whl/cpu/torch-2.1.1-cp311-none-macosx_11_0_arm64.whl" + +[[package]] +name = "torch" +version = "2.1.1+cpu" +description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "torch-2.1.1+cpu-cp311-cp311-linux_x86_64.whl", hash = "sha256:d83b13cb17544f9851cc31fed197865eae0c0f5d32df9d8d6d8535df7d2e5109"}, +] + +[package.dependencies] +filelock = "*" +fsspec = "*" +jinja2 = "*" +networkx = "*" +sympy = "*" +typing-extensions = "*" + +[package.extras] +dynamo = ["jinja2"] +opt-einsum = ["opt-einsum (>=3.3)"] + +[package.source] +type = "url" +url = "https://download.pytorch.org/whl/cpu/torch-2.1.1%2Bcpu-cp311-cp311-linux_x86_64.whl" + +[[package]] +name = "torch" +version = "2.1.1+cpu" description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" optional = false python-versions = ">=3.8.0" files = [ - {file = "torch-2.1.1+cu121-cp311-cp311-win_amd64.whl", hash = "sha256:cbc1b55879aca47584172a885c35677073110311cdbba3589e80938b15bbc8ad"}, + {file = "torch-2.1.1+cpu-cp311-cp311-win_amd64.whl", hash = "sha256:23be0cb945970443c97d4f9ea61ed03b27f924d835de689dd4134f30966c13f7"}, ] [package.dependencies] @@ -2220,7 +2269,7 @@ opt-einsum = ["opt-einsum (>=3.3)"] [package.source] type = "url" -url = "https://download.pytorch.org/whl/cu121/torch-2.1.1%2Bcu121-cp311-cp311-win_amd64.whl" +url = "https://download.pytorch.org/whl/cpu/torch-2.1.1%2Bcpu-cp311-cp311-win_amd64.whl" [[package]] name = "tqdm" @@ -2794,4 +2843,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11,<3.12" -content-hash = "70e9e6888a129526bdc06eca1afbb2be0a2f48687efa8a841445e650c316e922" +content-hash = "4a40c3668c981baeda73805b148b5d3cd052f100304f19826ae8640693a3b628" diff --git a/frontend/README.md b/frontend/README.md index b9036d1..2ad2fc9 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,16 +1,18 @@ -This is a [LlamaIndex](https://www.llamaindex.ai/) frontend using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama). +# Smart Retrieval Frontend + +The frontend is built using [Next.js](https://nextjs.org/) & [Vercel AI](https://github.com/vercel/ai) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama). ## Getting Started First, install the dependencies: -``` +```bash npm install ``` Second, run the development server: -``` +```bash npm run dev ``` @@ -26,5 +28,3 @@ To learn more about LlamaIndex, take a look at the following resources: - [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features). - [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features). - -You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome! diff --git a/frontend/app/about/page.tsx b/frontend/app/about/page.tsx index 0333301..79a886b 100644 --- a/frontend/app/about/page.tsx +++ b/frontend/app/about/page.tsx @@ -1,31 +1,26 @@ "use client"; -import Header from "@/app/components/header"; - export default function About() { return ( -
-
-
-
-
-

About Smart Retrieval

-

- Welcome to Smart Retrieval, your go-to platform for efficient and streamlined information retrieval, - especially in the realm of legal and compliance documents. -

-

- Our mission is to enhance user experiences at JTC by addressing key challenges such as manual search - inefficiencies and rigid file naming conventions. -

-

- With cutting-edge technology, including Large Language Models (LLM) like GPT, BERT, and advanced chatbot - integration, we aim to revolutionize the way JTC employees access and comprehend crucial documents. -

-
+
+
+
+

About Smart Retrieval

+

+ Welcome to Smart Retrieval, your go-to platform for efficient and streamlined information retrieval, + especially in the realm of legal and compliance documents. +

+

+ Our mission is to enhance user experiences at JTC by addressing key challenges such as manual search + inefficiencies and rigid file naming conventions. +

+

+ With cutting-edge technology, including Large Language Models (LLM) like GPT, BERT, and advanced chatbot + integration, we aim to revolutionize the way JTC employees access and comprehend crucial documents. +

-
+
); } diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index 850111e..a4b48be 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -1,14 +1,10 @@ "use client"; -import Header from "@/app/components/header"; import ChatSection from "@/app/components/chat-section"; export default function Chat() { return ( -
-
- -
+ ); } diff --git a/frontend/app/components/chat-section.tsx b/frontend/app/components/chat-section.tsx index a2f9fdd..04098fc 100644 --- a/frontend/app/components/chat-section.tsx +++ b/frontend/app/components/chat-section.tsx @@ -12,7 +12,7 @@ export default function ChatSection() { handleInputChange, reload, stop, - } = useChat({ api: process.env.CHAT_API }); + } = useChat({ api: process.env.NEXT_PUBLIC_CHAT_API }); return (
diff --git a/frontend/app/components/header.tsx b/frontend/app/components/header.tsx index 4c90242..4b33842 100644 --- a/frontend/app/components/header.tsx +++ b/frontend/app/components/header.tsx @@ -2,28 +2,128 @@ import Link from 'next/link'; import Image from 'next/image'; -import { Home, InfoIcon, MessageCircle, Search } from 'lucide-react'; +import { Home, InfoIcon, MessageCircle, Search, FileQuestion, Menu, X } from 'lucide-react'; import { usePathname } from 'next/navigation'; import { useTheme } from "next-themes"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; +import { useMedia } from 'react-use'; +import useSWR from 'swr' import logo from '../../public/smart-retrieval-logo.webp' interface NavLinkProps { href: string; children: React.ReactNode; + onClick?: () => void; // Include onClick as an optional prop } -const NavLink: React.FC = ({ href, children }) => { +interface MobileMenuProps { + isOpen: boolean; + onClose: () => void; +} + +const MobileMenu: React.FC = ({ isOpen, onClose }) => { + const isLargeScreen = useMedia('(min-width: 1024px)', false); + const menuRef = useRef(null); + + useEffect(() => { + const handleOutsideClick = (event: MouseEvent | TouchEvent) => { + if ( + !isLargeScreen && + isOpen && + !menuRef.current?.contains(event.target as Node) && + !((event.target as HTMLElement).closest('.toggle-button')) // Exclude the toggle button + ) { + onClose(); // Close the menu + } + }; + + if (!isLargeScreen && isOpen) { + // Add event listeners for both mouse and touch events + document.addEventListener('mousedown', handleOutsideClick); + } + + return () => { + // Remove the event listener when the component unmounts + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, [isLargeScreen, isOpen, onClose]); + + useEffect(() => { + if (isLargeScreen && isOpen) { + onClose(); + } + }, [isLargeScreen, isOpen, onClose]); + return ( +
+
+ Logo +
+
+ {/* Mobile menu content */} +
+ +
+ + Home +
+
+ +
+ + About +
+
+ +
+ + Chat +
+
+ +
+ + Q&A +
+
+ +
+ + Search +
+
+
+
+
+ ); +}; + +const NavLink: React.FC = ({ href, children, onClick }) => { // Use the useRouter hook to get information about the current route const pathname = usePathname(); // Determine if the current tab is active const isActive = pathname === href; + const handleClick = () => { + if (onClick) { + onClick(); // Call the onClick handler if provided + } + }; + return ( {/* Add a class to highlight the active tab */} -
+
{children}
@@ -31,13 +131,51 @@ const NavLink: React.FC = ({ href, children }) => { }; export default function Header() { + const isLargeScreen = useMedia('(min-width: 1024px)', false); const [mounted, setMounted] = useState(false); const { theme, setTheme } = useTheme(); + // const [apiStatus, setApiStatus] = useState(false); + // Use SWR for API status fetching + const healthcheck_api = process.env.NEXT_PUBLIC_HEALTHCHECK_API; + const { data: apiStatus, error: apiError } = useSWR(healthcheck_api, async (url) => { + try { + // Fetch the data + const response = await fetch(url); + if (!response.ok) { + throw new Error(response.statusText || 'Unknown Error'); + } + const data = await response.json(); + return data; + } catch (error: any) { + console.error('Error fetching Backend API Status:', error.message); + throw error; + } + }, { + revalidateOnFocus: true, // Revalidate when the window gains focus + revalidateIfStale: true, // Revalidate if the data is stale + refreshInterval: 60000, // Revalidate every 60 seconds + }); + if (apiError) { + console.error('[Header] Error fetching Backend API Status:', apiError.message); + } useEffect(() => { setMounted(true); }, []); + const [isMobileMenuOpen, setMobileMenuOpen] = useState(false); + + const toggleMobileMenu = () => { + // Handle the toggle click here + if (isMobileMenuOpen) { + // If the menu is open, close it + setMobileMenuOpen(false); + } else { + // If the menu is closed, open it + setMobileMenuOpen(true); + } + }; + if (!mounted) return null; return ( @@ -45,24 +183,45 @@ export default function Header() { {/* Navigation Bar */}